mar 9 gen 2024, 16:00:03, CET

rewrite2
Alessandro Mauri 4 months ago
parent 7909306d7a
commit 28b5ee16fb
  1. 138
      ARCHITECTURE.md
  2. 376
      ugui.c
  3. 46
      ugui.h

@ -1,8 +1,8 @@
## High level overview ## High level overview
Under the hood every element has an id, this id allows the library to store state Under the hood every element has an id, this id allows the library to store state
between frames. between frames.
Elements are also cached such that when the ui tree is rebuilt at the beginning of Elements are also cached such that when the ui tree is rebuilt at the beginning of
every frame the element data structure doesn't have to be rebuilt. every frame the element data structure doesn't have to be rebuilt.
Elements are arranged in a tree, nodes are container elements that can contain other Elements are arranged in a tree, nodes are container elements that can contain other
@ -11,7 +11,7 @@ elements, leafs are elements that cannot contain other elements.
Every element has a size and a position, containers also have to keep track of their Every element has a size and a position, containers also have to keep track of their
layout information and some other state. layout information and some other state.
Elements can push commands into the draw stack, which is a structure that contains Elements can push commands into the draw stack, which is a structure that contains
all the draw commands that the user application has to perform do display the ui all the draw commands that the user application has to perform do display the ui
correctly, such commands include drawing lines, rectangles, sprites, text, etc. correctly, such commands include drawing lines, rectangles, sprites, text, etc.
@ -90,75 +90,59 @@ multiple +--------v---------+ |
### Layouting ### Layouting
Layouting happens in a dynamic grid, when a new element is inserted in a non-floating
manner it reserves a space in the grid, new elements are placed following this grid.
Every div has two points of origin, one for the row layout and one for the column
layout, named origin_r and origin_c respectively
origin_r is used when the row layout is used and it is used to position the child
elements one next to the other, as such it always points to the top-right edge
of the last row element
```text ```text
parent div Layout: row
[0,0]->+------------------------------------------------+ #: lost space
| | Parent div
| | x---------------------------------+
| | |[origin_c] |
| | |[origin_r] |
| | | |
| | | |
| | | |
| space_r | | |
| space_c | begin div1 | |
| | ----+ | |
| | | | |
| | | +---------------------------------+
| | |
| | | Parent div
| | | +-----x---------------------------+
| | | | |[origin_r] |
| | | | E1 | |
+------------------------------------------------+ | | | |
| x-----+---------------------------+
| |[origin_c] |
parent div | | | |
[0,0]->+----------------------+-------------------------+ | | | |
| | | | | | |
| | | | | | |
| | | <---+ | | |
| div 1 | | end div1 +-----+---------------------------+
| | |
| | | Parent div
| | | +-----+----------+-----x----------+
| | | | | E2 | |[origin_r]|
+----------------------+ space_r | | E1 +----------+ | |
| | | | |##########| E3 | |
| | | begin div2 +-----+##########| | |
| | | ----+ |################| | |
| | | | +----------------x-----+----------+
| space_c | | | | [origin_c] |
| | | | | | |
| | | | | | |
| | | | | | |
+----------------------+-------------------------+ | +----------------+----------------+
|
|
parent div |
[0,0]->+----------------------+--------+----------------+ |
| | | | |
| | div 2 | | |
| | | | |
| div 1 +--------+ | |
| |xxxxxxxx| | |
| |xxxxxxxx| | <---+
| |xxxxxxxx| | end div2
| |xxxxxxxx| |
+----------------------+--------+ space_r |
| | |
| | |
| | |
| | |
| space_c | |
| | |
| | |
| | |
+-------------------------------+----------------+
+-+
|x|: Lost Space
+-+
``` ```
TODO: handle when the content overflows the div TODO: handle when the content overflows the div
@ -170,6 +154,10 @@ is clipped to the view and scrollbars are shown
- individual elements accept dimensions and the x/y coordinates could be interpreted - individual elements accept dimensions and the x/y coordinates could be interpreted
as offset if the layout is row/column or absolute coordinates if the leayout is floating as offset if the layout is row/column or absolute coordinates if the leayout is floating
A div can be marked resizeable or fixed, and static or dynamic. The difference being
that resizeable adds a resize handle to the div and dynamic lets the content overflow
causing scrollbars to be drawn
### Notes ### Notes
How elements determine if they have focus or not How elements determine if they have focus or not
@ -185,6 +173,12 @@ set has_focus property
if(PARENT_HAS_FOCUS()) { if(PARENT_HAS_FOCUS()) {
update stuff update stuff
} else { } else {
fast path to return fast path to return
} }
``` ```
How to get ids:
1. use a name for each element
2. supply an id for each element
3. use a macro and the line position as id and then hash it
4. use a macro, get the code line and hash it

376
ugui.c

@ -8,6 +8,20 @@
#include "raylib.h" #include "raylib.h"
#include "ugui.h" #include "ugui.h"
#define RGBA(u) \
(UgColor) \
{ \
.r = (u >> 24) & 0xff, .g = (u >> 16) & 0xff, .b = (u >> 8) & 0xff, \
.a = (u >> 0) & 0xff \
}
#define DIV_FILL \
(UgRect) { .x = 0, .y = 0, .w = 0, .h = 0 }
#define STACK_STEP 10
#define MAX_ELEMS 128
#define MAX_CMDS 256
#define ROOT_ID 1
typedef struct _UgCmd { typedef struct _UgCmd {
enum { enum {
CMD_RECT = 0, CMD_RECT = 0,
@ -15,15 +29,15 @@ typedef struct _UgCmd {
union { union {
struct { struct {
int32_t x, y, w, h; UgRect rect;
uint8_t color[4]; // rgba, [0] = r UgColor color;
} rect; } rect;
}; };
} UgCmd; } UgCmd;
typedef struct _UgStack { typedef struct _UgStack {
int size, count; int size, count;
UgCmd *stack; UgCmd *stack;
} UgStack; } UgStack;
int ug_stack_init(UgStack *stack, uint32_t size); int ug_stack_init(UgStack *stack, uint32_t size);
@ -58,8 +72,6 @@ int ug_stack_free(UgStack *stack)
return 0; return 0;
} }
#define STACK_STEP 10
int ug_stack_push(UgStack *stack, UgCmd *cmd) int ug_stack_push(UgStack *stack, UgCmd *cmd)
{ {
if (stack == NULL || cmd == NULL) { if (stack == NULL || cmd == NULL) {
@ -100,6 +112,10 @@ int ug_stack_pop(UgStack *stack, UgCmd *cmd)
return 0; return 0;
} }
enum InputEventFlags {
INPUT_CTX_SIZE = 1 << 0, // window size was changed
};
struct _UgCtx { struct _UgCtx {
enum { enum {
row = 0, row = 0,
@ -115,14 +131,29 @@ struct _UgCtx {
int width, height; int width, height;
} size; } size;
// input structure, it describes the events received between frames
struct {
uint32_t flags;
} input;
int div_using; // tree node indicating the current active div int div_using; // tree node indicating the current active div
}; };
int ug_div_begin(UgCtx *ctx, const char *name, UgRect div); // layouting
int ug_layout_row(void);
int ug_layout_column(void);
int ug_layout_floating(void);
int ug_layout_next_row(void);
int ug_layout_next_column(void);
int ug_div_begin(UgCtx *ctx, const char *label, UgRect div);
int ug_div_end(UgCtx *ctx); int ug_div_end(UgCtx *ctx);
int ug_input_window_size(UgCtx *ctx, int width, int height); int ug_input_window_size(UgCtx *ctx, int width, int height);
UgId djb2(const char *str); UgId djb2(const char *str);
int ug_button(UgCtx *ctx, const char *label, UgRect size);
int main(void) int main(void)
{ {
UgCtx ctx; UgCtx ctx;
@ -136,18 +167,21 @@ int main(void)
// Main loop // Main loop
while (!WindowShouldClose()) { while (!WindowShouldClose()) {
// UI handling // Input handling
ug_frame_begin(&ctx);
if (IsWindowResized()) { if (IsWindowResized()) {
width = GetScreenWidth(); width = GetScreenWidth();
height = GetScreenHeight(); height = GetScreenHeight();
ug_input_window_size(&ctx, width, height); ug_input_window_size(&ctx, width, height);
} }
// main div, fill the whole window // UI handling
#define DIV_FILL (UgRect) {.x = 0, .y = 0, .w = -1, .h = -1} ug_frame_begin(&ctx);
// main div, fill the whole window
ug_div_begin(&ctx, "main", DIV_FILL); ug_div_begin(&ctx, "main", DIV_FILL);
ug_button(&ctx, "button0", (UgRect){.w = 100, .h = 16});
ug_div_end(&ctx); ug_div_end(&ctx);
ug_frame_end(&ctx); ug_frame_end(&ctx);
@ -156,26 +190,30 @@ int main(void)
BeginDrawing(); BeginDrawing();
ClearBackground(BLACK); ClearBackground(BLACK);
Rectangle r; Color c;
Color c;
for (UgCmd cmd; ug_stack_pop(&ctx.stack, &cmd) >= 0;) { for (UgCmd cmd; ug_stack_pop(&ctx.stack, &cmd) >= 0;) {
printf("bruh\n");
switch (cmd.type) { switch (cmd.type) {
case CMD_RECT: case CMD_RECT:
printf( //printf(
"rect x=%d y=%d w=%d h=%d\n", // "rect x=%d y=%d w=%d h=%d\n",
cmd.rect.x, // cmd.rect.rect.x,
cmd.rect.y, // cmd.rect.rect.y,
cmd.rect.w, // cmd.rect.rect.w,
cmd.rect.h // cmd.rect.rect.h
); //);
c = (Color) { c = (Color) {
.r = cmd.rect.color[0], .r = cmd.rect.color.r,
.g = cmd.rect.color[1], .g = cmd.rect.color.g,
.b = cmd.rect.color[2], .b = cmd.rect.color.b,
.a = cmd.rect.color[3], .a = cmd.rect.color.a,
}; };
DrawRectangle( DrawRectangle(
cmd.rect.x, cmd.rect.y, cmd.rect.w, cmd.rect.h, c cmd.rect.rect.x,
cmd.rect.rect.y,
cmd.rect.rect.w,
cmd.rect.rect.h,
c
); );
break; break;
default: default:
@ -195,9 +233,6 @@ int main(void)
return 0; return 0;
} }
#define MAX_ELEMS 128
#define MAX_CMDS 256
int ug_init(UgCtx *ctx) int ug_init(UgCtx *ctx)
{ {
if (ctx == NULL) { if (ctx == NULL) {
@ -226,9 +261,80 @@ int ug_destroy(UgCtx *ctx)
return 0; return 0;
} }
int ug_frame_begin(UgCtx *ctx) { return 0; } void print_tree(UgCtx *ctx)
{
printf("ctx->tree: [");
for (int c = -1, x; (x = ug_tree_level_order_it(&ctx->tree, 0, &c)) != -1;) {
printf("%d, ", x);
}
printf("-1]\n");
}
int ug_frame_begin(UgCtx *ctx)
{
if (ctx == NULL) {
return -1;
}
// 1. Create the root div element
UgRect space = {
.x = 0,
.y = 0,
.w = ctx->size.width,
.h = ctx->size.height,
};
UgElem root = {
.id = ROOT_ID,
.type = ETYPE_DIV,
.flags = 0,
.rect = space,
.div =
{
.layout = DIV_LAYOUT_ROW,
.origin_r = {0},
.origin_c = {0},
.color_bg = {0},
},
};
// The root should have the updated flag only if the size of the window
// was changed between frames, this propagates an element size recalculation
// down the element tree
if (ctx->input.flags & INPUT_CTX_SIZE) {
root.flags |= ELEM_UPDATED;
}
// FIXME: check errors
uint32_t cache_idx;
ug_cache_insert(&ctx->cache, &root, &cache_idx);
ctx->div_using = ug_tree_add(&ctx->tree, root.id, 0);
if (ctx->div_using < 0) {
printf("why?\n");
return -1;
}
//print_tree(ctx);
// The root element does not push anything to the stack
// TODO: add a background color taken from a theme or config
return 0;
}
int ug_frame_end(UgCtx *ctx)
{
if (ctx == NULL) {
return -1;
}
// 1. clear the tree
ug_tree_prune(&ctx->tree, 0);
// 2. clear input fields
ctx->input.flags = 0;
int ug_frame_end(UgCtx *ctx) { return 0; } return 0;
}
int ug_input_window_size(UgCtx *ctx, int width, int height) int ug_input_window_size(UgCtx *ctx, int width, int height)
{ {
@ -262,28 +368,34 @@ UgId djb2(const char *str)
return hash; return hash;
} }
int ug_div_begin(UgCtx *ctx, const char *name, UgRect div) int ug_div_begin(UgCtx *ctx, const char *label, UgRect div)
{ {
if (ctx == NULL || name == NULL) { if (ctx == NULL || label == NULL) {
return -1; return -1;
} }
UgId id = djb2(name); UgId id = djb2(label);
// TODO: do layouting if the element is new or the parent has updated
int is_new_elem = 0;
// add the element if it does not exist // add the element if it does not exist
UgElem *c_elem = ug_cache_search(&ctx->cache, id); UgElem *c_elem = ug_cache_search(&ctx->cache, id);
if (c_elem == NULL) { if (c_elem == NULL) {
UgElem elem = { UgElem elem = {0};
.id = id,
.type = ETYPE_DIV,
.rec = (UgRect) {
.x = div.x,
.y = div.y,
.w = div.w >= 0 ? div.w : ctx->size.width,
.h = div.h >= 0 ? div.h : ctx->size.height,
}};
uint32_t c_idx; uint32_t c_idx;
c_elem = ug_cache_insert(&ctx->cache, &elem, &c_idx); c_elem = ug_cache_insert(&ctx->cache, &elem, &c_idx);
is_new_elem = 1;
}
// take a reference to the parent
// FIXME: if the tree held pointers to the elements then no more
// redundant cache search
UgId parent_id = ctx->tree.vector[ctx->div_using];
UgElem *parent = ug_cache_search(&ctx->cache, parent_id);
if (parent == NULL) {
// Error, did you forget to do frame_begin()?
return -1;
} }
// FIXME: why save the id in the tree and not something more direct like // FIXME: why save the id in the tree and not something more direct like
@ -295,22 +407,78 @@ int ug_div_begin(UgCtx *ctx, const char *name, UgRect div)
} }
ctx->div_using = div_node; ctx->div_using = div_node;
// printf( //print_tree(ctx);
// "elem.rect: x=%d x=%d w=%d h=%d\n",
// c_elem->rec.x, // layouting
// c_elem->rec.y, // TODO: do layout
// c_elem->rec.w, if (is_new_elem || parent->flags & ELEM_UPDATED) {
// c_elem->rec.h c_elem->id = id;
// ); c_elem->type = ETYPE_DIV;
// 1. Select the right origin offset
switch (parent->div.layout) {
case DIV_LAYOUT_ROW:
c_elem->rect = (UgRect) {
.x = parent->div.origin_r.x + div.x,
.y = parent->div.origin_r.y + div.y,
};
break;
case DIV_LAYOUT_COLUMN:
c_elem->rect = (UgRect) {
.x = parent->div.origin_c.x + div.x,
.y = parent->div.origin_c.y + div.y,
};
break;
case DIV_LAYOUT_FLOATING:
c_elem->rect = (UgRect) {
.x = div.x,
.y = div.y,
};
break;
default:
// Error
break;
}
// 2. Calculate width & height
// TODO: what about negative values?
c_elem->rect.w = div.w != 0 ? div.w : parent->rect.w;
c_elem->rect.h = div.h != 0 ? div.h : parent->rect.h;
// 3. Mark the element as updated
c_elem->flags |= ELEM_UPDATED;
// 4. Set div information
c_elem->div.layout = parent->div.layout;
c_elem->div.origin_c = (UgPoint) {
.x = c_elem->rect.x,
.y = c_elem->rect.y,
};
c_elem->div.origin_r = c_elem->div.origin_c;
c_elem->div.color_bg = RGBA(0xff0000ff);
// 4. Update the origins of the parent
parent->div.origin_r = (UgPoint) {
.x = c_elem->rect.x + c_elem->rect.w,
.y = c_elem->rect.y,
};
parent->div.origin_c = (UgPoint) {
.x = c_elem->rect.x,
.y = c_elem->rect.y + c_elem->rect.h,
};
} else {
// TODO: check active
// TODO: check resizeable
// TODO: check scrollbars
}
// Add the background to the draw stack
UgCmd cmd = { UgCmd cmd = {
.type = CMD_RECT, .type = CMD_RECT,
.rect = .rect =
{ {
.x = c_elem->rec.x, .rect = c_elem->rect,
.y = c_elem->rec.y, .color = c_elem->div.color_bg,
.w = c_elem->rec.w,
.h = c_elem->rec.h,
.color = {0xff, 0x00, 0x00, 0xff}, // red
}, },
}; };
ug_stack_push(&ctx->stack, &cmd); ug_stack_push(&ctx->stack, &cmd);
@ -324,3 +492,99 @@ int ug_div_end(UgCtx *ctx)
ctx->div_using = ug_tree_parentof(&ctx->tree, ctx->div_using); ctx->div_using = ug_tree_parentof(&ctx->tree, ctx->div_using);
return 0; return 0;
} }
int ug_button(UgCtx *ctx, const char *label, UgRect size)
{
if (ctx == NULL || label == NULL) {
return -1;
}
UgId id = djb2(label);
// TODO: do layouting if the element is new or the parent has updated
int is_new_elem = 0;
// add the element if it does not exist
UgElem *c_elem = ug_cache_search(&ctx->cache, id);
if (c_elem == NULL) {
UgElem elem = {0};
uint32_t c_idx;
c_elem = ug_cache_insert(&ctx->cache, &elem, &c_idx);
is_new_elem = 1;
}
// take a reference to the parent
// FIXME: if the tree held pointers to the elements then no more
// redundant cache search
UgId parent_id = ctx->tree.vector[ctx->div_using];
UgElem *parent = ug_cache_search(&ctx->cache, parent_id);
if (parent == NULL) {
// Error, did you forget to do frame_begin()?
return -1;
}
// if the element is new or the parent was updated then redo layout
if (is_new_elem || parent->flags & ELEM_UPDATED) {
c_elem->id = id;
c_elem->type = ETYPE_BUTTON;
// 1. Select the right origin offset
switch (parent->div.layout) {
case DIV_LAYOUT_ROW:
c_elem->rect = (UgRect) {
.x = parent->div.origin_r.x + size.x,
.y = parent->div.origin_r.y + size.y,
};
break;
case DIV_LAYOUT_COLUMN:
c_elem->rect = (UgRect) {
.x = parent->div.origin_c.x + size.x,
.y = parent->div.origin_c.y + size.y,
};
break;
case DIV_LAYOUT_FLOATING:
c_elem->rect = (UgRect) {
.x = size.x,
.y = size.y,
};
break;
default:
// Error
break;
}
// 2. Calculate width & height
// TODO: what about negative values?
c_elem->rect.w = size.w != 0 ? size.w : parent->rect.w;
c_elem->rect.h = size.h != 0 ? size.h : parent->rect.h;
/// Not a div, so not needed
// 3. Mark the element as updated
// 4. Set div information
// 4. Update the origins of the parent
parent->div.origin_r = (UgPoint) {
.x = c_elem->rect.x + c_elem->rect.w,
.y = c_elem->rect.y,
};
parent->div.origin_c = (UgPoint) {
.x = c_elem->rect.x,
.y = c_elem->rect.y + c_elem->rect.h,
};
} else {
// TODO: Check for interactions
}
// Draw the button
UgCmd cmd = {
.type = CMD_RECT,
.rect =
{
.rect = c_elem->rect,
.color = RGBA(0x0000ffff),
},
};
ug_stack_push(&ctx->stack, &cmd);
return 0;
}

@ -20,31 +20,51 @@ typedef uint64_t UgId;
typedef enum { typedef enum {
ETYPE_NONE = 0, ETYPE_NONE = 0,
ETYPE_DIV, ETYPE_DIV,
ETYPE_BUTTON,
} UgElemType; } UgElemType;
enum UgElemFlags {
ELEM_UPDATED = 1 << 0,
};
typedef struct { typedef struct {
UgId id; UgId id;
UgRect rec; uint32_t flags;
UgRect rect;
union { union {
uint32_t type_int; uint32_t type_int;
UgElemType type; UgElemType type;
}; };
// type-specific fields
union {
struct {
enum {
DIV_LAYOUT_ROW = 0,
DIV_LAYOUT_COLUMN,
DIV_LAYOUT_FLOATING,
} layout;
UgPoint origin_r, origin_c;
UgColor color_bg;
} div; // Div
};
} UgElem; } UgElem;
// TODO: add a packed flag // TODO: add a packed flag
// TODO: add a fill index to skip some searching for free spots // TODO: add a fill index to skip some searching for free spots
typedef struct { typedef struct {
int size, elements; int size, elements;
UgId *vector; // vector of element ids UgId *vector; // vector of element ids
int *refs, *ordered_refs; int *refs, *ordered_refs;
} UgTree; } UgTree;
typedef struct { typedef struct {
struct _IdTable *table; struct _IdTable *table;
UgElem *array; UgElem *array;
uint64_t *present, *used; uint64_t *present, *used;
int cycles; int cycles;
} UgElemCache; } UgElemCache;
typedef struct _UgCtx UgCtx; typedef struct _UgCtx UgCtx;
@ -63,10 +83,9 @@ int ug_tree_destroy(UgTree *tree);
// cache implementation // cache implementation
UgElemCache ug_cache_init(void); UgElemCache ug_cache_init(void);
void ug_cache_free(UgElemCache *cache); void ug_cache_free(UgElemCache *cache);
UgElem *ug_cache_search(UgElemCache *cache, UgId id); UgElem *ug_cache_search(UgElemCache *cache, UgId id);
UgElem *ug_cache_insert(UgElemCache *cache, const UgElem *g, uint32_t *index); UgElem *ug_cache_insert(UgElemCache *cache, const UgElem *g, uint32_t *index);
int ug_init(UgCtx *ctx); int ug_init(UgCtx *ctx);
int ug_destroy(UgCtx *ctx); int ug_destroy(UgCtx *ctx);
@ -74,4 +93,3 @@ int ug_frame_begin(UgCtx *ctx);
int ug_frame_end(UgCtx *ctx); int ug_frame_end(UgCtx *ctx);
#endif // _UGUI_H #endif // _UGUI_H

Loading…
Cancel
Save