Compare commits
5
Commits
80f5e9056a
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12fe6abc25 | ||
|
|
dd406fe7b4 | ||
|
|
5be35aa3de | ||
|
|
5e070521bc | ||
|
|
f06ac04ea9 |
No files matched your search
@@ -1,2 +1,31 @@
|
|||||||
# RDIC
|
# RDIC - Retained Data, Immediate Code
|
||||||
|
|
||||||
|
Serious GUI programming has long been dominated by retained mode APIs, where a tree of GUI objects that hold function pointers for callbacks is built and a central "do the GUI thing" procedure is called, which takes care of handling all the GUI functionality and calls the callbacks as necessary.
|
||||||
|
|
||||||
|
A competitor to retained mode GUIs is the immediate mode GUI paradigm, which prioritizes locality of GUI element definition and its behavior.
|
||||||
|
The specification of GUI elements is done by calling procedures that instantiate them every frame that they should appear on the screen.
|
||||||
|
For instance, a button is achieved by calling some procedure, e.g. `gui_button`, which would return true if the button was clicked on and false otherwise.
|
||||||
|
|
||||||
|
There are tradeoffs in choosing one or the other. Retained mode required you to use inversion of control, which to some is a violation of good programming principles.
|
||||||
|
Immediate mode is in many regards simpler, but has the downside that it forgoes exposing a data structure to the user at all, and has to use clever techniques to identify the same GUI elements across frames.
|
||||||
|
|
||||||
|
The RDIC approach combines retained and immediate techniques.
|
||||||
|
In retained mode it is normal to use handles into a behind-the-scenes data structure, which is used to make state changes requests in order to manipulate the GUI.
|
||||||
|
RDIC introduces the use of references into an exposed, retained data structure, where you re-use your references across frames, in an immediate style API.
|
||||||
|
When passing the same, stable reference to e.g. a button call every frame, it is determined that this is in fact the same button across consecutive frames.
|
||||||
|
|
||||||
|
RDIC example:
|
||||||
|
```C
|
||||||
|
static RDIC_Reference my_button = {0};
|
||||||
|
if(button(&my_button, "Button Label")) {
|
||||||
|
printf("Printed if button is clicked.\n");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The stable data structure in RDIC can be leveraged for features such as accessibility.
|
||||||
|
One example of how this data structure is used in the example GUI in this repository is conditional redrawing. It stores a "dirty" flag with each node, which is used to skip over non-"dirty" nodes when generating draw commands. This is very useful for the implementation, which draws using only the CPU.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
gcc -g -Wall -Wextra $1 -fsanitize=address,undefined gui.c stb_truetype.o -lX11 -lm
|
gcc -g -Wall -Wextra -Wno-stringop-overflow $1 -fsanitize=address,undefined gui.c stb_truetype.o -lX11 -lm
|
||||||
@@ -386,7 +386,7 @@ for(struct {TYPE temp; int i;} GUIWITH_INNER(__LINE__) = {.temp=(storage_variabl
|
|||||||
// TODO(Zelaven): The mouse coordinates are in the context, so no need to pass
|
// TODO(Zelaven): The mouse coordinates are in the context, so no need to pass
|
||||||
// them as parameters.
|
// them as parameters.
|
||||||
#undef ENABLE_DEBUG
|
#undef ENABLE_DEBUG
|
||||||
#define ENABLE_DEBUG 1
|
#define ENABLE_DEBUG 0
|
||||||
void gui_apply_input(
|
void gui_apply_input(
|
||||||
GUI_Context *context,
|
GUI_Context *context,
|
||||||
int mouse_x,
|
int mouse_x,
|
||||||
@@ -421,6 +421,30 @@ void gui_apply_input(
|
|||||||
|
|
||||||
assert(hit_root_subtree != NULL);
|
assert(hit_root_subtree != NULL);
|
||||||
|
|
||||||
|
GUI_Subtree *top_subtree_under_cursor = hit_root_subtree;
|
||||||
|
GUI_Subtree *current_subtree = GUI_SUBTREE_FIRST_CHILD(top_subtree_under_cursor);
|
||||||
|
int mouse_over_subtree = 0;
|
||||||
|
while(current_subtree != NULL)
|
||||||
|
{
|
||||||
|
// TODO(Zelaven): Should actually test against the rect-intersect between
|
||||||
|
// the clipnode and the subtree root. The clipnode is only a maximal bound.
|
||||||
|
mouse_over_subtree = point_in_rect_with_offset(
|
||||||
|
mouse_x, mouse_y,
|
||||||
|
// TODO(Zelaven): Test clipnode valid.
|
||||||
|
current_subtree->clipnode.node->rect,
|
||||||
|
hit_layer->offset_x, hit_layer->offset_y);
|
||||||
|
if(mouse_over_subtree)
|
||||||
|
{
|
||||||
|
top_subtree_under_cursor = current_subtree;
|
||||||
|
current_subtree = GUI_SUBTREE_FIRST_CHILD(current_subtree);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
current_subtree = GUI_SUBTREE_SIBLING(current_subtree);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hit_root_subtree = top_subtree_under_cursor;
|
||||||
|
|
||||||
GUI_Node *top_node_under_cursor = (GUI_Node*)hit_root_subtree->rdic.root;
|
GUI_Node *top_node_under_cursor = (GUI_Node*)hit_root_subtree->rdic.root;
|
||||||
GUI_Node *current_node = (GUI_Node*)top_node_under_cursor->rdic_node.first_child;
|
GUI_Node *current_node = (GUI_Node*)top_node_under_cursor->rdic_node.first_child;
|
||||||
int mouse_over_node = 0;
|
int mouse_over_node = 0;
|
||||||
@@ -586,7 +610,13 @@ GUI_Subtree_Reference gui_get_subtree(
|
|||||||
i = i->sibling)
|
i = i->sibling)
|
||||||
{
|
{
|
||||||
GUI_Subtree *subtree = (GUI_Subtree*)i;
|
GUI_Subtree *subtree = (GUI_Subtree*)i;
|
||||||
|
// NOTE(Zelaven): This line falsely triggers -Wstringop-overflow and the
|
||||||
|
// copy into a temporary variable seemingly fixes it.
|
||||||
|
// For this reason I simply opted to use -Wno-stringop-overflow, as I
|
||||||
|
// don't want to have a broken flag enabled.
|
||||||
rdic_cull_subtrees(&subtree->rdic, subtree->rdic.root);
|
rdic_cull_subtrees(&subtree->rdic, subtree->rdic.root);
|
||||||
|
//RDIC_Context *rdic_context = &subtree->rdic;
|
||||||
|
//rdic_cull_subtrees(rdic_context, subtree->rdic.root);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -870,8 +900,45 @@ void gui_layout_calculate_screen_rects(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void gui_layout_nodes(GUI_Node *root)
|
void gui_layout_nodes(GUI_Subtree *subtree)
|
||||||
{
|
{
|
||||||
|
#if 1
|
||||||
|
GUI_Node *root = (GUI_Node*)subtree->rdic.root;
|
||||||
|
assert(root != NULL);
|
||||||
|
for(GUI_Axis2 axis = 0; axis < GUI_AXIS2_COUNT; axis++)
|
||||||
|
{
|
||||||
|
gui_layout_calculate_standalone_sizes(root, axis);
|
||||||
|
gui_layout_calculate_parent_percent_sizes(root, axis);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(Zelaven): Test reference validity.
|
||||||
|
// TODO(Zelaven): This probably can be done once and passed in.
|
||||||
|
int x_offset = 0;
|
||||||
|
int y_offset = 0;
|
||||||
|
GUI_Node *relative_node = (GUI_Node*)subtree->relative_to.node;
|
||||||
|
if(relative_node != NULL){
|
||||||
|
GUI_Rectangle *relative_rectangle = &relative_node->rect;
|
||||||
|
x_offset = relative_rectangle->x0;
|
||||||
|
y_offset = relative_rectangle->y0;
|
||||||
|
//printf("%.*s: %d, %d\n", relative_node->debug_string.length, relative_node->debug_string.cstring, x_offset, y_offset);
|
||||||
|
int relative_width = relative_rectangle->x1 - relative_rectangle->x0;
|
||||||
|
int relative_height = relative_rectangle->y1 - relative_rectangle->y0;
|
||||||
|
x_offset +=
|
||||||
|
(relative_width * subtree->offset_relative_node_percentage_x) / 100;
|
||||||
|
y_offset +=
|
||||||
|
(relative_height * subtree->offset_relative_node_percentage_y) / 100;
|
||||||
|
}
|
||||||
|
x_offset += subtree->flat_offset_x;
|
||||||
|
y_offset += subtree->flat_offset_y;
|
||||||
|
|
||||||
|
x_offset +=
|
||||||
|
(root->computed_size[GUI_AXIS2_X] * subtree->offset_own_size_percentage_x) / 100;
|
||||||
|
y_offset +=
|
||||||
|
(root->computed_size[GUI_AXIS2_Y] * subtree->offset_own_size_percentage_y) / 100;
|
||||||
|
|
||||||
|
gui_layout_calculate_screen_rects(root, x_offset, y_offset, GUI_LAYOUT_NONE);
|
||||||
|
#else
|
||||||
|
|
||||||
//GUI_Node *root = (GUI_Node*)context->first_subtree->rdic.root;
|
//GUI_Node *root = (GUI_Node*)context->first_subtree->rdic.root;
|
||||||
for(GUI_Axis2 axis = 0; axis < GUI_AXIS2_COUNT; axis++)
|
for(GUI_Axis2 axis = 0; axis < GUI_AXIS2_COUNT; axis++)
|
||||||
{
|
{
|
||||||
@@ -879,6 +946,7 @@ void gui_layout_nodes(GUI_Node *root)
|
|||||||
gui_layout_calculate_parent_percent_sizes(root, axis);
|
gui_layout_calculate_parent_percent_sizes(root, axis);
|
||||||
}
|
}
|
||||||
gui_layout_calculate_screen_rects(root, 0, 0, GUI_LAYOUT_NONE);
|
gui_layout_calculate_screen_rects(root, 0, 0, GUI_LAYOUT_NONE);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -888,8 +956,8 @@ void gui_layout_nodes(GUI_Node *root)
|
|||||||
#undef ENABLE_DEBUG
|
#undef ENABLE_DEBUG
|
||||||
#define ENABLE_DEBUG 0
|
#define ENABLE_DEBUG 0
|
||||||
void gui_generate_draw_commands_inner(
|
void gui_generate_draw_commands_inner(
|
||||||
int x_offset,
|
//int x_offset,
|
||||||
int y_offset,
|
//int y_offset,
|
||||||
GUI_Node *node,
|
GUI_Node *node,
|
||||||
bool dirty, //TODO(Zelaven): Better name.
|
bool dirty, //TODO(Zelaven): Better name.
|
||||||
Memory_Arena *draw_command_arena,
|
Memory_Arena *draw_command_arena,
|
||||||
@@ -911,10 +979,12 @@ void gui_generate_draw_commands_inner(
|
|||||||
.text = node->text_string,
|
.text = node->text_string,
|
||||||
.image = node->image,
|
.image = node->image,
|
||||||
};
|
};
|
||||||
|
#if 0
|
||||||
(*draw_command).rectangle.x0 += x_offset;
|
(*draw_command).rectangle.x0 += x_offset;
|
||||||
(*draw_command).rectangle.x1 += x_offset;
|
(*draw_command).rectangle.x1 += x_offset;
|
||||||
(*draw_command).rectangle.y0 += y_offset;
|
(*draw_command).rectangle.y0 += y_offset;
|
||||||
(*draw_command).rectangle.y1 += y_offset;
|
(*draw_command).rectangle.y1 += y_offset;
|
||||||
|
#endif
|
||||||
*out_num_draw_commands += 1;
|
*out_num_draw_commands += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -922,7 +992,7 @@ void gui_generate_draw_commands_inner(
|
|||||||
if(sibling != NULL) {
|
if(sibling != NULL) {
|
||||||
GUI_Node *parent = GUI_NODE_PARENT(node);
|
GUI_Node *parent = GUI_NODE_PARENT(node);
|
||||||
gui_generate_draw_commands_inner(
|
gui_generate_draw_commands_inner(
|
||||||
x_offset, y_offset,
|
//x_offset, y_offset,
|
||||||
sibling,
|
sibling,
|
||||||
parent->dirty || sibling->dirty,
|
parent->dirty || sibling->dirty,
|
||||||
draw_command_arena, out_num_draw_commands);
|
draw_command_arena, out_num_draw_commands);
|
||||||
@@ -930,7 +1000,7 @@ void gui_generate_draw_commands_inner(
|
|||||||
GUI_Node *first_child = GUI_NODE_FIRST_CHILD(node);
|
GUI_Node *first_child = GUI_NODE_FIRST_CHILD(node);
|
||||||
if(node->rdic_node.first_child != NULL) {
|
if(node->rdic_node.first_child != NULL) {
|
||||||
gui_generate_draw_commands_inner(
|
gui_generate_draw_commands_inner(
|
||||||
x_offset, y_offset,
|
//x_offset, y_offset,
|
||||||
first_child,
|
first_child,
|
||||||
dirty || first_child->dirty,
|
dirty || first_child->dirty,
|
||||||
draw_command_arena, out_num_draw_commands);
|
draw_command_arena, out_num_draw_commands);
|
||||||
@@ -943,6 +1013,7 @@ void gui_generate_draw_commands(
|
|||||||
Memory_Arena *draw_command_arena,
|
Memory_Arena *draw_command_arena,
|
||||||
int *out_num_draw_commands)
|
int *out_num_draw_commands)
|
||||||
{
|
{
|
||||||
|
#if 0
|
||||||
// TODO(Zelaven): Test reference validity.
|
// TODO(Zelaven): Test reference validity.
|
||||||
// TODO(Zelaven): This probably can be done once and passed in.
|
// TODO(Zelaven): This probably can be done once and passed in.
|
||||||
int x_offset = 0;
|
int x_offset = 0;
|
||||||
@@ -969,6 +1040,10 @@ void gui_generate_draw_commands(
|
|||||||
(root->computed_size[GUI_AXIS2_X] * subtree->offset_own_size_percentage_x) / 100;
|
(root->computed_size[GUI_AXIS2_X] * subtree->offset_own_size_percentage_x) / 100;
|
||||||
y_offset +=
|
y_offset +=
|
||||||
(root->computed_size[GUI_AXIS2_Y] * subtree->offset_own_size_percentage_y) / 100;
|
(root->computed_size[GUI_AXIS2_Y] * subtree->offset_own_size_percentage_y) / 100;
|
||||||
|
#else
|
||||||
|
GUI_Node *root = (GUI_Node*)subtree->rdic.root;
|
||||||
|
assert(root != NULL);
|
||||||
|
#endif
|
||||||
|
|
||||||
assert(draw_command_arena != NULL);
|
assert(draw_command_arena != NULL);
|
||||||
assert(out_num_draw_commands != NULL);
|
assert(out_num_draw_commands != NULL);
|
||||||
@@ -980,7 +1055,7 @@ void gui_generate_draw_commands(
|
|||||||
dirtyness |= clipnode.node->dirty;
|
dirtyness |= clipnode.node->dirty;
|
||||||
}
|
}
|
||||||
gui_generate_draw_commands_inner(
|
gui_generate_draw_commands_inner(
|
||||||
x_offset, y_offset,
|
//x_offset, y_offset,
|
||||||
root, dirtyness,
|
root, dirtyness,
|
||||||
draw_command_arena, out_num_draw_commands);
|
draw_command_arena, out_num_draw_commands);
|
||||||
}
|
}
|
||||||
@@ -1099,7 +1174,7 @@ bool gui_mouseover_box(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#undef ENABLE_DEBUG
|
#undef ENABLE_DEBUG
|
||||||
#define ENABLE_DEBUG 1
|
#define ENABLE_DEBUG 0
|
||||||
bool gui_slider(
|
bool gui_slider(
|
||||||
GUI_Context *context,
|
GUI_Context *context,
|
||||||
GUI_Node_Reference *last_reference,
|
GUI_Node_Reference *last_reference,
|
||||||
@@ -1892,8 +1967,8 @@ void test_gui__subtree(
|
|||||||
context->top_layer.pixel_buffer->width,
|
context->top_layer.pixel_buffer->width,
|
||||||
context->top_layer.pixel_buffer->height,
|
context->top_layer.pixel_buffer->height,
|
||||||
GUI_LAYOUT_HORIZONTAL, &outer_style);
|
GUI_LAYOUT_HORIZONTAL, &outer_style);
|
||||||
context->top_layer.offset_x = 50;
|
context->top_layer.offset_x = 70;
|
||||||
context->top_layer.offset_y = 250;
|
context->top_layer.offset_y = 20;
|
||||||
|
|
||||||
|
|
||||||
static GUI_Node_Reference top = {0};
|
static GUI_Node_Reference top = {0};
|
||||||
@@ -1928,6 +2003,7 @@ void test_gui__subtree(
|
|||||||
&button_style, button_size))
|
&button_style, button_size))
|
||||||
{
|
{
|
||||||
should_dirty = true;
|
should_dirty = true;
|
||||||
|
printf("Dirtying middle.\n");
|
||||||
}
|
}
|
||||||
static GUI_Node_Reference dirty_button2 = {0};
|
static GUI_Node_Reference dirty_button2 = {0};
|
||||||
if(gui_dumb_button(
|
if(gui_dumb_button(
|
||||||
@@ -1944,6 +2020,7 @@ void test_gui__subtree(
|
|||||||
&button_style, button_size))
|
&button_style, button_size))
|
||||||
{
|
{
|
||||||
tooltip_toggle = !tooltip_toggle;
|
tooltip_toggle = !tooltip_toggle;
|
||||||
|
printf("Toggling tooltip.\n");
|
||||||
}
|
}
|
||||||
static GUI_Node_Reference dirty_button4 = {0};
|
static GUI_Node_Reference dirty_button4 = {0};
|
||||||
if(
|
if(
|
||||||
@@ -2000,6 +2077,14 @@ void test_gui__subtree(
|
|||||||
static GUI_Node_Reference inner_top = {0};
|
static GUI_Node_Reference inner_top = {0};
|
||||||
inner_top = gui_dumb_block(context, inner_top, &outer_style, element_size);
|
inner_top = gui_dumb_block(context, inner_top, &outer_style, element_size);
|
||||||
((GUI_Node*)inner_top.node)->image = &test_image;
|
((GUI_Node*)inner_top.node)->image = &test_image;
|
||||||
|
|
||||||
|
static GUI_Node_Reference subtree_button = {0};
|
||||||
|
if(gui_dumb_button(
|
||||||
|
context, &subtree_button, GUI_STRING("In subtree"),
|
||||||
|
&element_style, element_size))
|
||||||
|
{
|
||||||
|
printf("Hi from subtree!\n");
|
||||||
|
}
|
||||||
} gui_pop_subtree(context);
|
} gui_pop_subtree(context);
|
||||||
#endif
|
#endif
|
||||||
#if 0
|
#if 0
|
||||||
@@ -3652,7 +3737,8 @@ void gui_layout_and_draw_subtree(
|
|||||||
GUI_Subtree *subtree,
|
GUI_Subtree *subtree,
|
||||||
Pixel_Buffer *pixel_buffer)
|
Pixel_Buffer *pixel_buffer)
|
||||||
{
|
{
|
||||||
gui_layout_nodes((GUI_Node*)subtree->rdic.root);
|
//gui_layout_nodes((GUI_Node*)subtree->rdic.root);
|
||||||
|
gui_layout_nodes(subtree);
|
||||||
gui_generate_draw_commands(
|
gui_generate_draw_commands(
|
||||||
subtree,
|
subtree,
|
||||||
context->draw_command_arena,
|
context->draw_command_arena,
|
||||||
@@ -3761,8 +3847,10 @@ int main(void)
|
|||||||
struct File_Read_Result font_data = read_file__cruddy(font_file);
|
struct File_Read_Result font_data = read_file__cruddy(font_file);
|
||||||
// TODO(Zelaven): How much data do I have to make sure that I read at minimum?
|
// TODO(Zelaven): How much data do I have to make sure that I read at minimum?
|
||||||
assert(font_data.size > 16);
|
assert(font_data.size > 16);
|
||||||
g_font_bitmap = memory_arena_allocate(&font_arena, sizeof(char), _Alignof(char));
|
|
||||||
{
|
{
|
||||||
|
int font_bitmap_size = g_font_bitmap_width * g_font_bitmap_height;
|
||||||
|
g_font_bitmap = memory_arena_allocate(
|
||||||
|
&font_arena, font_bitmap_size, _Alignof(char));
|
||||||
int bake_font_return_value = stbtt_BakeFontBitmap(
|
int bake_font_return_value = stbtt_BakeFontBitmap(
|
||||||
font_data.data, 0,
|
font_data.data, 0,
|
||||||
20.0,
|
20.0,
|
||||||
@@ -3817,9 +3905,9 @@ int main(void)
|
|||||||
.height = xwindow.pixel_buffer.height,
|
.height = xwindow.pixel_buffer.height,
|
||||||
};
|
};
|
||||||
Pixel_Buffer top_pixel_buffer = (Pixel_Buffer){
|
Pixel_Buffer top_pixel_buffer = (Pixel_Buffer){
|
||||||
.pixels = malloc(300*200*sizeof(GUI_Color)),
|
.pixels = malloc(300*400*sizeof(GUI_Color)),
|
||||||
.width = 300,
|
.width = 300,
|
||||||
.height = 200,
|
.height = 400,
|
||||||
};
|
};
|
||||||
assert(background_pixel_buffer.pixels);
|
assert(background_pixel_buffer.pixels);
|
||||||
assert(top_pixel_buffer.pixels);
|
assert(top_pixel_buffer.pixels);
|
||||||
@@ -3953,6 +4041,7 @@ int main(void)
|
|||||||
#if 1
|
#if 1
|
||||||
gui_layout_and_draw(&context);
|
gui_layout_and_draw(&context);
|
||||||
gui_compose_layers(&context, &xwindow.pixel_buffer);
|
gui_compose_layers(&context, &xwindow.pixel_buffer);
|
||||||
|
//SET_PIXEL(&xwindow.pixel_buffer, 200, 388, 0x00ff0000);
|
||||||
#else
|
#else
|
||||||
for(
|
for(
|
||||||
GUI_Subtree *current = (GUI_Subtree*)context.background_layer.subtree_rdic.root;
|
GUI_Subtree *current = (GUI_Subtree*)context.background_layer.subtree_rdic.root;
|
||||||
|
|||||||
Reference in new issue
Block a user