From be00c87c6a47ed41dc0918003a1d808beeefda62 Mon Sep 17 00:00:00 2001 From: Alessandro Mauri Date: Thu, 14 Aug 2025 22:16:54 +0200 Subject: [PATCH 01/13] implement a convenient macro to start and end a div --- lib/ugui.c3l/src/ugui_div.c3 | 9 ++++ src/main.c3 | 80 ++++++++++++++++++------------------ 2 files changed, 48 insertions(+), 41 deletions(-) diff --git a/lib/ugui.c3l/src/ugui_div.c3 b/lib/ugui.c3l/src/ugui_div.c3 index fb1e42a..5bec047 100644 --- a/lib/ugui.c3l/src/ugui_div.c3 +++ b/lib/ugui.c3l/src/ugui_div.c3 @@ -23,6 +23,15 @@ struct ElemDiv { Point origin_r, origin_c; } + +// useful macro to start and end a div, capturing the trailing block +macro Ctx.@div(&ctx, Rect size, bool scroll_x = false, bool scroll_y = false, ...; @body()) +{ + ctx.div_begin(size, scroll_x, scroll_y, $vasplat)!; + @body(); + ctx.div_end()!; +} + // begin a widget container, or div, the size determines the offset (x,y) width and height. // if the width or height are zero the width or height are set to the maximum available. // if the width or height are negative the width or height will be calculated based on the children size diff --git a/src/main.c3 b/src/main.c3 index 89f623b..cfd51c3 100644 --- a/src/main.c3 +++ b/src/main.c3 @@ -199,12 +199,12 @@ fn int main(String[] args) if (ui.check_key_combo(ugui::KMOD_CTRL, "q")) quit = true; const String APPLICATION = "calculator"; -$if APPLICATION == "debug": +$switch APPLICATION: +$case "debug": debug_app(&ui); -$endif -$if APPLICATION == "calculator": +$case "calculator": calculator(&ui); -$endif +$endswitch // Timings counter TimeStats dts = draw_times.get_stats(); @@ -287,13 +287,12 @@ fn void debug_app(ugui::Ctx* ui) ui.checkbox("", {}, &check, "tick")!!; ui.checkbox("", {}, &check)!!; ui.toggle("", {}, &toggle)!!; + + ui.sprite("tux")!!; + static char[128] text_box = "ciao mamma"; + static usz text_len = "ciao mamma".len; + ui.text_box({0,0,200,200}, text_box[..], &text_len)!!; }; - ui.sprite("tux")!!; - - static char[128] text_box = "ciao mamma"; - static usz text_len = "ciao mamma".len; - ui.text_box({0,0,200,200}, text_box[..], &text_len)!!; - ui.div_end()!!; ui.div_begin(ugui::DIV_FILL, scroll_x: true, scroll_y: true)!!; @@ -324,36 +323,35 @@ fn void debug_app(ugui::Ctx* ui) fn void calculator(ugui::Ctx* ui) { - ui.div_begin(ugui::DIV_FILL)!!; + ui.@div(ugui::DIV_FILL) { + ui.@div({0,0,-300,50}) { + ui.text_unbounded("80085")!!; + }!!; - ui.layout_set_row()!!; - ui.div_begin({0,0,-300,50})!!; - ui.text_unbounded("80085")!!; - ui.div_end()!!; - ui.layout_next_row()!!; - - ui.button("7")!!; - ui.button("8")!!; - ui.button("9")!!; - ui.layout_next_row()!!; - ui.button("4")!!; - ui.button("5")!!; - ui.button("6")!!; - ui.layout_next_row()!!; - ui.button("3")!!; - ui.button("2")!!; - ui.button("1")!!; - ui.layout_next_row()!!; - ui.button(".")!!; - ui.button("0")!!; - ui.button("")!!; - - ui.layout_next_column()!!; - ui.layout_set_column()!!; - ui.button("+")!!; - ui.button("-")!!; - ui.button("*")!!; - ui.button("/")!!; - - ui.div_end()!!; + ui.layout_next_row()!!; + + ui.button("7")!!; + ui.button("8")!!; + ui.button("9")!!; + ui.layout_next_row()!!; + ui.button("4")!!; + ui.button("5")!!; + ui.button("6")!!; + ui.layout_next_row()!!; + ui.button("3")!!; + ui.button("2")!!; + ui.button("1")!!; + ui.layout_next_row()!!; + ui.button(".")!!; + ui.button("0")!!; + ui.button("")!!; + + ui.layout_next_column()!!; + ui.layout_set_column()!!; + ui.button("+")!!; + ui.button("-")!!; + ui.button("*")!!; + ui.button("/")!!; + + }!!; } From 00299bec0bffbc8fe5a654c134443544ff168637 Mon Sep 17 00:00:00 2001 From: Alessandro Mauri Date: Sat, 16 Aug 2025 10:10:06 +0200 Subject: [PATCH 02/13] fix calculator demo turns out it was an incorrect handling of scissor test --- TODO | 12 ++++++- lib/ugui.c3l/src/ugui_button.c3 | 13 +++---- lib/ugui.c3l/src/ugui_cmd.c3 | 36 ++++++++++++++++--- lib/ugui.c3l/src/ugui_core.c3 | 5 +++ src/main.c3 | 61 +++++++++++++++++---------------- 5 files changed, 85 insertions(+), 42 deletions(-) diff --git a/TODO b/TODO index 07b3bfb..f1e191d 100644 --- a/TODO +++ b/TODO @@ -56,12 +56,22 @@ to maintain focus until mouse release (fix scroll bars) [ ] Consider a multi-pass recursive approach to layout (like https://github.com/nicbarker/clay) instead of the curren multi-frame approach. [ ] Implement column/row sizing (min, max) +[ ] Implement a way to size the element as the current row/column size + * +-------------+ + * | | + * +-------------+ + * +--+ + * | | + * +--+ + * <-------------> + * column size + See the calculator example for why it is useful [ ] Find a way to concile pixel measurements to the mm ones used in css, for example in min/max sizing of elements [ ] Center elements to div (center children_bounds to the center of the div bounds and shift the origin accordingly) [x] Use containing_rect() in position_element() to skip some computing and semplify the function [x] Rename position_element() to layout_element() -[ ] Make functions to mark rows/columns as full, to fix the calculator demo +[x] Make functions to mark rows/columns as full, to fix the calculator demo ## Input diff --git a/lib/ugui.c3l/src/ugui_button.c3 b/lib/ugui.c3l/src/ugui_button.c3 index 064a350..f2132be 100644 --- a/lib/ugui.c3l/src/ugui_button.c3 +++ b/lib/ugui.c3l/src/ugui_button.c3 @@ -64,7 +64,7 @@ fn ElemEvents? Ctx.button_id(&ctx, Id id, String label, String icon) .h = (short)max(icon_size.h, content_bounds.h) }; icon_bounds = icon_size.center_to(icon_bounds); - + bool is_active = ctx.elem_focus(elem) || elem.events.mouse_hover; Style s = *style; if (is_active) { @@ -73,11 +73,12 @@ fn ElemEvents? Ctx.button_id(&ctx, Id id, String label, String icon) } ctx.push_rect(elem.bounds, parent.div.z_index, &s)!; - ctx.push_sprite(icon_bounds, sprite.uv(), ctx.sprite_atlas.id, parent.div.z_index, type: sprite.type)!; -// Style ss = {.bg = 0x000000ffu.@to_rgba()}; -// ctx.push_rect(content_bounds, parent.div.z_index, &ss)!; - ctx.push_string(text_bounds, label, parent.div.z_index, style.fg)!; - + if (icon != "") { + ctx.push_sprite(icon_bounds, sprite.uv(), ctx.sprite_atlas.id, parent.div.z_index, type: sprite.type)!; + } + if (label != "") { + ctx.push_string(text_bounds, label, parent.div.z_index, style.fg)!; + } return elem.events; } diff --git a/lib/ugui.c3l/src/ugui_cmd.c3 b/lib/ugui.c3l/src/ugui_cmd.c3 index 4d6e09c..463605e 100644 --- a/lib/ugui.c3l/src/ugui_cmd.c3 +++ b/lib/ugui.c3l/src/ugui_cmd.c3 @@ -58,7 +58,26 @@ fn int Cmd.compare_to(Cmd a, Cmd b) // implement the Printable interface fn usz? Cmd.to_format(Cmd* cmd, Formatter *f) @dynamic { - return f.printf("Cmd{ type: %s, z_index: %d }", cmd.type, cmd.z_index); + usz ret; + + ret += f.printf("Cmd{ type: %s, z_index: %d, ", cmd.type, cmd.z_index)!; + switch (cmd.type) { + case CMD_RECT: + ret += f.print("CmdRect")!; + ret += io::struct_to_format(cmd.rect, f, false)!; + case CMD_SCISSOR: + ret += f.print("CmdScissor")!; + ret += io::struct_to_format(cmd.scissor, f, false)!; + case CMD_SPRITE: + ret += f.print("CmdSprite")!; + ret += io::struct_to_format(cmd.sprite, f, false)!; + case CMD_UPDATE_ATLAS: + ret += f.print("CmdUpdateAtlas")!; + ret += io::struct_to_format(cmd.update_atlas, f, false)!; + } + + ret += f.print("}")!; + return ret; } macro bool cull_rect(Rect rect, Rect clip = {0,0,short.max,short.max}) @@ -77,7 +96,13 @@ macro Ctx.push_cmd(&ctx, Cmd *cmd, int z_index) case CMD_SPRITE: rect = cmd.sprite.rect; default: return ctx.cmd_queue.enqueue(cmd); } - if (cull_rect(rect, ctx.div_scissor)) return; + if (cull_rect(rect, ctx.div_scissor)) { + io::print("NOPE: "); + io::print(cmd.rect.rect); + io::printn(cmd.z_index); +// unreachable(); + return; + } return ctx.cmd_queue.enqueue(cmd); } @@ -90,6 +115,8 @@ fn void? Ctx.push_scissor(&ctx, Rect rect, int z_index) ctx.push_cmd(&sc, z_index)!; } +fn void? Ctx.reset_scissor(&ctx, int z_index) => ctx.push_cmd(&&(Cmd){.type=CMD_SCISSOR,.scissor.rect=ctx.div_scissor}, z_index)!; + fn void? Ctx.push_rect(&ctx, Rect rect, int z_index, Style* style) { Rect border = style.border; @@ -119,7 +146,6 @@ fn void? Ctx.push_rect(&ctx, Rect rect, int z_index, Style* style) .rect.color = bg, .rect.radius = radius, }; - if (cull_rect(cmd.rect.rect, ctx.div_scissor)) return; ctx.push_cmd(&cmd, z_index)!; } @@ -157,8 +183,8 @@ fn TextInfo? Ctx.push_string(&ctx, Rect bounds, char[] text, int z_index, Color ctx.push_sprite(ti.glyph_bounds, ti.glyph_uv, texture_id, z_index, hue)!; } } - - ctx.push_scissor({}, z_index)!; + + ctx.reset_scissor(z_index)!; return ti; } diff --git a/lib/ugui.c3l/src/ugui_core.c3 b/lib/ugui.c3l/src/ugui_core.c3 index 144d236..b85e996 100644 --- a/lib/ugui.c3l/src/ugui_core.c3 +++ b/lib/ugui.c3l/src/ugui_core.c3 @@ -291,6 +291,11 @@ $endif // sort the command buffer by the z-index ctx.cmd_queue.sort()!; + +// foreach (i, c: ctx.cmd_queue) { +// io::printf("[%d]: ", i); +// io::printn(c); +// } } <* diff --git a/src/main.c3 b/src/main.c3 index cfd51c3..307e34d 100644 --- a/src/main.c3 +++ b/src/main.c3 @@ -212,14 +212,12 @@ $endswitch ui.layout_set_floating()!!; // FIXME: I cannot anchor shit to the bottom of the screen - ui.div_begin({0, ui.height-150, -300, 150})!!; - { + ui.@div({0, ui.height-150, -300, 150}) { ui.layout_set_column()!!; ui.text_unbounded(string::tformat("frame %d, fps = %.2f", frame, fps))!!; ui.text_unbounded(string::tformat("ui avg: %s\ndraw avg: %s\nTOT: %s", uts.avg, dts.avg, uts.avg+dts.avg))!!; ui.text_unbounded(string::tformat("%s %s", mod.lctrl, (String)ui.input.keyboard.text[:ui.input.keyboard.text_len]))!!; - }; - ui.div_end()!!; + }!!; ui.frame_end()!!; /* End UI Handling */ @@ -324,34 +322,37 @@ fn void debug_app(ugui::Ctx* ui) fn void calculator(ugui::Ctx* ui) { ui.@div(ugui::DIV_FILL) { - ui.@div({0,0,-300,50}) { + ui.layout_set_column()!!; + + ui.@div({0,0,250,50}) { ui.text_unbounded("80085")!!; }!!; - ui.layout_next_row()!!; - - ui.button("7")!!; - ui.button("8")!!; - ui.button("9")!!; - ui.layout_next_row()!!; - ui.button("4")!!; - ui.button("5")!!; - ui.button("6")!!; - ui.layout_next_row()!!; - ui.button("3")!!; - ui.button("2")!!; - ui.button("1")!!; - ui.layout_next_row()!!; - ui.button(".")!!; - ui.button("0")!!; - ui.button("")!!; - - ui.layout_next_column()!!; - ui.layout_set_column()!!; - ui.button("+")!!; - ui.button("-")!!; - ui.button("*")!!; - ui.button("/")!!; - + ui.@div({0,0,250,-100}) { + ui.layout_set_row()!!; + + ui.button("7")!!; + ui.button("8")!!; + ui.button("9")!!; + ui.layout_next_row()!!; + ui.button("4")!!; + ui.button("5")!!; + ui.button("6")!!; + ui.layout_next_row()!!; + ui.button("3")!!; + ui.button("2")!!; + ui.button("1")!!; + ui.layout_next_row()!!; + ui.button(".")!!; + ui.button("0")!!; + //ui.button("")!!; + + ui.layout_next_column()!!; + ui.layout_set_column()!!; + ui.button("+")!!; + ui.button("-")!!; + ui.button("*")!!; + ui.button("/")!!; + }!!; }!!; } From 62ebd6592d5b5162def64b916683a9d0cdc8b639 Mon Sep 17 00:00:00 2001 From: Alessandro Mauri Date: Wed, 20 Aug 2025 17:32:22 +0200 Subject: [PATCH 03/13] fix wrong scissor --- lib/ugui.c3l/src/ugui_div.c3 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ugui.c3l/src/ugui_div.c3 b/lib/ugui.c3l/src/ugui_div.c3 index 5bec047..10d6c40 100644 --- a/lib/ugui.c3l/src/ugui_div.c3 +++ b/lib/ugui.c3l/src/ugui_div.c3 @@ -63,7 +63,7 @@ fn void? Ctx.div_begin_id(&ctx, Id id, Rect size, bool scroll_x, bool scroll_y) .h = size.h < 0 ? max(elem.div.pcb.h, (short)-size.h) : size.h, }; elem.bounds = ctx.layout_element(parent, wanted_size, style); - elem.div.children_bounds = {}; + elem.div.children_bounds = {.x = elem.bounds.x, .y = elem.bounds.y}; // update the ctx scissor ctx.div_scissor = elem.bounds; From 4a690fdeb59804a8b573613f8cccfb8a33d64e71 Mon Sep 17 00:00:00 2001 From: Alessandro Mauri Date: Wed, 20 Aug 2025 17:33:25 +0200 Subject: [PATCH 04/13] actually useful calculator example --- lib/ugui.c3l/src/ugui_input.c3 | 2 + src/main.c3 | 87 +++++++++++++++++++++++++++------- 2 files changed, 71 insertions(+), 18 deletions(-) diff --git a/lib/ugui.c3l/src/ugui_input.c3 b/lib/ugui.c3l/src/ugui_input.c3 index ac70110..6125ace 100644 --- a/lib/ugui.c3l/src/ugui_input.c3 +++ b/lib/ugui.c3l/src/ugui_input.c3 @@ -195,6 +195,8 @@ fn void Ctx.input_char(&ctx, char c) ctx.input_text_utf8(b[..]); } +fn String Ctx.get_keys(&ctx) => (String)ctx.input.keyboard.text[:ctx.input.keyboard.text_len]; + // Modifier keys, like control or backspace // TODO: make this call repetible to input modkeys one by one fn void Ctx.input_mod_keys(&ctx, ModKeys modkeys) diff --git a/src/main.c3 b/src/main.c3 index 307e34d..000a103 100644 --- a/src/main.c3 +++ b/src/main.c3 @@ -319,40 +319,91 @@ fn void debug_app(ugui::Ctx* ui) ui.div_end()!!; } +import std::os::process; + fn void calculator(ugui::Ctx* ui) { + static char[128] buffer; + static usz len; + bool eval; + + // keyboard input + switch(ui.get_keys()) { + case "+": nextcase; + case "-": nextcase; + case "*": nextcase; + case "/": nextcase; + case "(": nextcase; + case ")": nextcase; + case "0": nextcase; + case "1": nextcase; + case "2": nextcase; + case "3": nextcase; + case "4": nextcase; + case "5": nextcase; + case "6": nextcase; + case "7": nextcase; + case "8": nextcase; + case "9": + buffer[len++] = ui.get_keys()[0]; + case "\n": + eval = true; + case "c": + len = 0; + case "d": + len--; + } + + // ui input/output ui.@div(ugui::DIV_FILL) { ui.layout_set_column()!!; ui.@div({0,0,250,50}) { - ui.text_unbounded("80085")!!; + ui.text_unbounded((String)buffer[:len])!!; }!!; ui.@div({0,0,250,-100}) { ui.layout_set_row()!!; - ui.button("7")!!; - ui.button("8")!!; - ui.button("9")!!; + ui.button("7")!!.mouse_press ? buffer[len++] = '7' : 0; + ui.button("8")!!.mouse_press ? buffer[len++] = '8' : 0; + ui.button("9")!!.mouse_press ? buffer[len++] = '9' : 0; ui.layout_next_row()!!; - ui.button("4")!!; - ui.button("5")!!; - ui.button("6")!!; + ui.button("4")!!.mouse_press ? buffer[len++] = '4' : 0; + ui.button("5")!!.mouse_press ? buffer[len++] = '5' : 0; + ui.button("6")!!.mouse_press ? buffer[len++] = '6' : 0; ui.layout_next_row()!!; - ui.button("3")!!; - ui.button("2")!!; - ui.button("1")!!; + ui.button("3")!!.mouse_press ? buffer[len++] = '3' : 0; + ui.button("2")!!.mouse_press ? buffer[len++] = '2' : 0; + ui.button("1")!!.mouse_press ? buffer[len++] = '1' : 0; ui.layout_next_row()!!; - ui.button(".")!!; - ui.button("0")!!; - //ui.button("")!!; + ui.button("0")!!.mouse_press ? buffer[len++] = '0' : 0; + ui.button(".")!!.mouse_press ? buffer[len++] = '.' : 0; + ui.button("(")!!.mouse_press ? buffer[len++] = '(' : 0; ui.layout_next_column()!!; - ui.layout_set_column()!!; - ui.button("+")!!; - ui.button("-")!!; - ui.button("*")!!; - ui.button("/")!!; + + ui.@div({0,0,0,-1}) { + ui.button("C")!!.mouse_press ? len = 0 : 0; + ui.button("D")!!.mouse_press ? len-- : 0; + ui.layout_next_row()!!; + ui.button("x")!!.mouse_press ? buffer[len++] = '*' : 0; + ui.button("/")!!.mouse_press ? buffer[len++] = '/' : 0; + ui.layout_next_row()!!; + ui.button("+")!!.mouse_press ? buffer[len++] = '+' : 0; + ui.button("-")!!.mouse_press ? buffer[len++] = '-' : 0; + ui.layout_next_row()!!; + ui.button(")")!!.mouse_press ? buffer[len++] = ')' : 0; + + // eval the expression with 'bc' + if (ui.button("=")!!.mouse_press || eval) { + char[128] out; + String y = string::tformat("echo '%s' | bc", (String)buffer[:len]); + String x = process::execute_stdout_to_buffer(out[:128], (String[]){"sh", "-c", y}) ?? ""; + buffer[:x.len] = x[..]; + len = x.len; + } + }!!; }!!; }!!; } From 5e5c912092bacb63ce757c791a4efa9884206fec Mon Sep 17 00:00:00 2001 From: Alessandro Mauri Date: Fri, 29 Aug 2025 19:24:52 +0200 Subject: [PATCH 05/13] use the specified allocator for the element caches --- TODO | 2 +- lib/ugui.c3l/src/cache.c3 | 10 +++++--- lib/ugui.c3l/src/fifo.c3 | 12 +++++---- lib/ugui.c3l/src/ugui_core.c3 | 10 ++++---- lib/ugui.c3l/src/vtree.c3 | 46 +++++++++++++++++++---------------- src/main.c3 | 8 +++++- 6 files changed, 51 insertions(+), 37 deletions(-) diff --git a/TODO b/TODO index f1e191d..c678e87 100644 --- a/TODO +++ b/TODO @@ -5,7 +5,7 @@ [x] Port font system from C to C3 (rewrite1) [ ] Update ARCHITECTURE.md [ ] Write a README.md -[ ] Use an arena allocator for cache +[x] Use an arena allocator for cache [ ] Do not redraw if there was no update (no layout and no draw) [ ] Do command buffer damage tracking based on a context grid (see rxi writeup) [x] Better handling of the active and focused widgets, try diff --git a/lib/ugui.c3l/src/cache.c3 b/lib/ugui.c3l/src/cache.c3 index 130ffd1..04ea3a3 100644 --- a/lib/ugui.c3l/src/cache.c3 +++ b/lib/ugui.c3l/src/cache.c3 @@ -25,6 +25,7 @@ alias IdTableEntry = map::Entry{Key, usz}; const usz CACHE_NCYCLES = (usz)(SIZE * 2.0/3.0); struct Cache { + Allocator allocator; BitArr present, used; IdTable table; Value[] pool; @@ -43,19 +44,20 @@ macro Cache.cycle(&cache) @private { } } -fn void? Cache.init(&cache) +fn void? Cache.init(&cache, Allocator allocator) { - cache.table.init(allocator::heap(), capacity: SIZE); + cache.allocator = allocator; + cache.table.init(allocator, capacity: SIZE); // FIXME: this shit is SLOW foreach (idx, bit : cache.used) { cache.used[idx] = false; } foreach (idx, bit : cache.present) { cache.present[idx] = false; } - cache.pool = mem::new_array(Value, SIZE); + cache.pool = allocator::new_array(allocator, Value, SIZE); } fn void Cache.free(&cache) { (void)cache.table.free(); - (void)mem::free(cache.pool); + (void)allocator::free(cache.allocator, cache.pool); } fn Value*? Cache.search(&cache, Key id) diff --git a/lib/ugui.c3l/src/fifo.c3 b/lib/ugui.c3l/src/fifo.c3 index d9fc52e..c7dff8f 100644 --- a/lib/ugui.c3l/src/fifo.c3 +++ b/lib/ugui.c3l/src/fifo.c3 @@ -9,21 +9,23 @@ import std::sort; // TODO: specify the allocator struct Fifo { + Allocator allocator; Type[] arr; usz out; usz count; } -fn void? Fifo.init(&fifo, usz size) +fn void? Fifo.init(&fifo, usz size, Allocator allocator) { - fifo.arr = mem::new_array(Type, size); + fifo.allocator = allocator; + fifo.arr = allocator::new_array(fifo.allocator, Type, size); fifo.out = 0; fifo.count = 0; } fn void Fifo.free(&fifo) { - (void)mem::free(fifo.arr); + (void)allocator::free(fifo.allocator, fifo.arr); } fn void? Fifo.enqueue(&fifo, Type *elem) @@ -69,8 +71,8 @@ macro usz Fifo.len(&fifo) @operator(len) fn void? Fifo.sort(&fifo) { - Type[] arr = mem::new_array(Type, fifo.count); - defer mem::free(arr); + Type[] arr = allocator::new_array(fifo.allocator, Type, fifo.count); + defer allocator::free(fifo.allocator, arr); foreach(i, c: fifo) { arr[i] = c; diff --git a/lib/ugui.c3l/src/ugui_core.c3 b/lib/ugui.c3l/src/ugui_core.c3 index b85e996..e3f1a01 100644 --- a/lib/ugui.c3l/src/ugui_core.c3 +++ b/lib/ugui.c3l/src/ugui_core.c3 @@ -187,18 +187,18 @@ fn Elem*? Ctx.get_active_div(&ctx) return ctx.cache.search(id); } -fn void? Ctx.init(&ctx) +fn void? Ctx.init(&ctx, Allocator allocator) { - ctx.tree.init(MAX_ELEMENTS)!; + ctx.tree.init(MAX_ELEMENTS, allocator)!; defer catch { (void)ctx.tree.free(); } - ctx.cache.init()!; + ctx.cache.init(allocator)!; defer catch { (void)ctx.cache.free(); } - ctx.cmd_queue.init(MAX_CMDS)!; + ctx.cmd_queue.init(MAX_CMDS, allocator)!; defer catch { (void)ctx.cmd_queue.free(); } - ctx.styles.init(allocator::heap()); + ctx.styles.init(allocator); ctx.styles.register_style(&DEFAULT_STYLE, @str_hash("default")); defer catch { ctx.styles.free(); } diff --git a/lib/ugui.c3l/src/vtree.c3 b/lib/ugui.c3l/src/vtree.c3 index d18b749..7901a58 100644 --- a/lib/ugui.c3l/src/vtree.c3 +++ b/lib/ugui.c3l/src/vtree.c3 @@ -4,39 +4,43 @@ faultdef CANNOT_SHRINK, INVALID_REFERENCE, TREE_FULL, REFERENCE_NOT_PRESENT, INV module vtree{ElemType}; import std::core::mem; +import std::core::mem::allocator; import std::io; struct VTree { + Allocator allocator; usz elements; ElemType[] vector; // vector of element ids isz[] refs, ordered_refs; } -macro VTree.ref_is_valid(&tree, isz ref) { return (ref >= 0 && ref < tree.refs.len); } -macro VTree.ref_is_present(&tree, isz ref) { return tree.refs[ref] >= 0; } -macro VTree.size(&tree) { return tree.refs.len; } +macro VTree.ref_is_valid(&tree, isz ref) => (ref >= 0 && ref < tree.refs.len); +macro VTree.ref_is_present(&tree, isz ref) => tree.refs[ref] >= 0; +macro VTree.size(&tree) => tree.refs.len; -// macro to zero an elemen +// macro to zero an element macro @zero() { - $if $assignable(0, ElemType): + $if @assignable_to(0, ElemType): return 0; $else return {}; $endif } -fn void? VTree.init(&tree, usz size) +fn void? VTree.init(&tree, usz size, Allocator allocator) { - tree.vector = mem::new_array(ElemType, size); - defer catch { (void)mem::free(tree.vector); } + tree.allocator = allocator; - tree.refs = mem::new_array(isz, size); - defer catch { (void)mem::free(tree.refs); } + tree.vector = allocator::new_array(tree.allocator, ElemType, size); + defer catch { (void)allocator::free(tree.allocator, tree.vector); } - tree.ordered_refs = mem::new_array(isz, size); - defer catch { (void)mem::free(tree.ordered_refs); } + tree.refs = allocator::new_array(tree.allocator, isz, size); + defer catch { (void)allocator::free(tree.allocator, tree.refs); } + + tree.ordered_refs = allocator::new_array(tree.allocator, isz, size); + defer catch { (void)allocator::free(tree.allocator, tree.ordered_refs); } // set all refs to -1, meaning invalid (free) element tree.refs[..] = -1; @@ -46,9 +50,9 @@ fn void? VTree.init(&tree, usz size) fn void VTree.free(&tree) { - (void)mem::free(tree.vector); - (void)mem::free(tree.refs); - (void)mem::free(tree.ordered_refs); + (void)allocator::free(tree.allocator, tree.vector); + (void)allocator::free(tree.allocator, tree.refs); + (void)allocator::free(tree.allocator, tree.ordered_refs); } fn void VTree.pack(&tree) @@ -102,14 +106,14 @@ fn void? VTree.resize(&tree, usz newsize) usz old_size = tree.size(); - tree.vector = ((ElemType*)mem::realloc(tree.vector, newsize*ElemType.sizeof))[:newsize]; - defer catch { (void)mem::free(tree.vector); } + tree.vector = ((ElemType*)allocator::realloc(tree.allocator, tree.vector, newsize*ElemType.sizeof))[:newsize]; + defer catch { (void)allocator::free(tree.allocator, tree.vector); } - tree.refs = ((isz*)mem::realloc(tree.refs, newsize*isz.sizeof))[:newsize]; - defer catch { (void)mem::free(tree.refs); } + tree.refs = ((isz*)allocator::realloc(tree.allocator, tree.refs, newsize*isz.sizeof))[:newsize]; + defer catch { (void)allocator::free(tree.allocator, tree.refs); } - tree.ordered_refs = ((isz*)mem::realloc(tree.ordered_refs, newsize*isz.sizeof))[:newsize]; - defer catch { (void)mem::free(tree.ordered_refs); } + tree.ordered_refs = ((isz*)allocator::realloc(tree.allocator, tree.ordered_refs, newsize*isz.sizeof))[:newsize]; + defer catch { (void)allocator::free(tree.allocator, tree.ordered_refs); } if (newsize > tree.size()) { tree.vector[old_size..newsize-1] = @zero(); diff --git a/src/main.c3 b/src/main.c3 index 000a103..7e6fed6 100644 --- a/src/main.c3 +++ b/src/main.c3 @@ -6,6 +6,7 @@ import std::time; import std::collections::ringbuffer; import std::core::string; import std::ascii; +import std::core::mem::allocator; import sdlrenderer::ren; import sdl3::sdl; @@ -58,8 +59,13 @@ const char[*] STYLESHEET_PATH = "resources/style.css"; fn int main(String[] args) { + ArenaAllocator arena; + char[] mem = mem::new_array(char, 1024*1024); + defer (void)mem::free(mem); + arena.init(mem); + ugui::Ctx ui; - ui.init()!!; + ui.init(&arena)!!; defer ui.free(); ren::Renderer ren; From 24ac28e0d9ad060ec91109942615879d485f3a63 Mon Sep 17 00:00:00 2001 From: Alessandro Mauri Date: Fri, 29 Aug 2025 19:25:18 +0200 Subject: [PATCH 06/13] update sdl3.c3l --- lib/sdl3.c3l | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/sdl3.c3l b/lib/sdl3.c3l index 076355e..e7356df 160000 --- a/lib/sdl3.c3l +++ b/lib/sdl3.c3l @@ -1 +1 @@ -Subproject commit 076355e2d126e7546e53663b97e8dec22667d34d +Subproject commit e7356df5d1d0c22a6bba822bda6994062b0b75d7 From 2619873ca7242e72c73fd3385492d3b543be7f77 Mon Sep 17 00:00:00 2001 From: Alessandro Mauri Date: Wed, 3 Sep 2025 23:29:20 +0200 Subject: [PATCH 07/13] tested new layout system --- lib/ugui.c3l/LAYOUT | 220 +++++++++++++++++++ lib/ugui.c3l/src/vtree.c3 | 12 +- test/test_tree_layout.c3 | 436 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 667 insertions(+), 1 deletion(-) create mode 100644 lib/ugui.c3l/LAYOUT create mode 100644 test/test_tree_layout.c3 diff --git a/lib/ugui.c3l/LAYOUT b/lib/ugui.c3l/LAYOUT new file mode 100644 index 0000000..33c804e --- /dev/null +++ b/lib/ugui.c3l/LAYOUT @@ -0,0 +1,220 @@ + Div Children Alignment ++------------------------------------------------+ +|TOP-LEFT TOP TOP-RIGHT| +| | +| | +| | +| | +| | +| | +|LEFT CENTER RIGHT| +| | +| | +| | +| | +| | +| | +|BOTTOM-LEFT BOTTOM BOTTOM-RIGHT| ++------------------------------------------------+ + +ALIGNMENT CHART: ++------------------------------+----------------------+------------------------------+-----------------------+------------------------------+----------------------+ +| TOP-LEFT, ROW: | TOP-LEFT, COLUMN: | BOTTOM, ROW: | BOTTOM, COLUMN: | TOP-RIGHT, ROW: | TOP-RIGHT, COLUMN: | +| | | | | | | +| +------------------------- - | +----------- - | | +----+ | | - -----------+ | +| |+-------++-----++-----+ | |+-------+ | | | E1 | | | +-------+| | +| || E1 || E2 || | | || E1 | | | | | | - -----------------------+ | | E1 || | +| || |+-----+| E3 | | || | | | +----+ | +-------++-----++-----+| | | || | +| |+-------+ | | | |+-------+ | +-------+ +---+ | +------+ | | E1 || E2 || || | +-------+| | +| | +-----+ | |+----+ | | E1 |+------+|E3 | | | E2 | | | |+-----+| E3 || | +----+| | +| ' | || E2 | | | || E2 || | | | | | +-------+ | || | | E2 || | +| ' | |+----+ | +-------++------++---+ | +------+ | +-----+| | +----+| | +| | |+---------+ | - ------------------------ - | +--+ | ' | +---------+| | +| | || E3 | | | |E3| | ' | | E3 || | +| | |+---------+ | | +--+ | | +---------+| | +| | ' | | - ----------- - | | ' | +| | ' | | | | ' | +| | | | | | | ++------------------------------+----------------------+------------------------------+-----------------------+------------------------------+----------------------+ +| LEFT, ROW: | LEFT, COLUMN: | BOTTOM-RIGHT, ROW: | BOTTOM-RIGHT, COLUMN: | TOP, ROW: | TOP, COLUMN: | +| | | | | | | +| | ' | | ' | | - -------------- - | +| ' | |+-------+ | | +-------+| | | +----------+ | +| | +----+ | || E1 | | ' | | E1 || | - ----------------------- - | | E1 | | +| |+------+ | | | || | | +-----+| | | || | +------++----++-----+ | | | | +| || |+-----+| | | |+-------+ | +-------+ | || | +-------+| | | E1 || E2 || E3 | | +----------+ | +| || E1 || E2 || E3 | | |+----+ | | E1 |+-----+| E3 || | +----+| | | |+----+| | | +--------+ | +| || |+-----+| | | || E2 | | | || E2 || || | | E2 || | +------+ | | | | E2 | | +| |+------+ | | | |+----+ | +-------++-----++-----+| | +----+| | +-----+ | +--------+ | +| | +----+ | |+---------+ | - -----------------------+ | +---------+| | | +------+ | +| ' | || E3 | | | | E3 || | | | E3 | | +| ' | |+---------+ | | +---------+| | | | | | +| | ' | | - -----------+ | | +------+ | +| | ' | | | | | ++------------------------------+----------------------+------------------------------+-----------------------+------------------------------+----------------------+ +| BOTTOM-LEFT, ROW: | BOTTOM-LEFT, COLUMN: | RIGHT, ROW: | RIGHT, COLUMN: | CENTER, ROW: | CENTER, COLUMN: | +| | | | | | | +| | ' | | ' | | | | +| | |+-------+ | | +-------+| | | | +-----------+ | +| | || E1 | | ' | | E1 || | | | | E1 | | +| ' | || | | +----+| | | || | | +----+ | | | | | +| | +-----+ | |+-------+ | +------+ | || | +-------+| | +------+ | | | | +-----------+ | +| |+-------+ | | | |+----+ | | |+-----+| || | +----+| | | |+----+| | | +---------+ | +| || E1 |+-----+| E3 | | || E2 | | | E1 || E2 || E3 || | | E2 || | ---|--E1--||-E2-||-E3-|--- | ----|---E2----|---- | +| || || E2 || | | |+----+ | | |+-----+| || | +----+| | | |+----+| | | +---------+ | +| |+-------++-----++-----+ | |+---------+ | +------+ | || | +---------+| | +------+ | | | | +-------+ | +| +------------------------- - | || E3 | | +----+| | | E3 || | | +----+ | | E3 | | +| | |+---------+ | ' | +---------+| | | | | | | | +| | +----------- - | ' | ' | | | +-------+ | +| | | | ' | | | | +| | | | | | | ++------------------------------+----------------------+------------------------------+-----------------------+------------------------------+----------------------+ + +div ( + align: TOP-LEFT | LEFT | BOTTOM-LEFT | BOTTOM | BOTTOM-RIGHT | RIGHT | TOP-RIGHT | RIGHT | CENTER + size_x/y: EXACT(x) | GROW() | FIT(min, max) + scroll_x/y: true | false + resize_x/y: true | false + layout: ROW | COLUMN +) + +align: alignment of the children elements +size: how the div should be sized +scroll: enables scrollbars +layout: the layout direction of the children + + COLUMN ROW + +--------------------+ +----------------------------------------------------+ + | +----------------+ | |+----------------+ +------------------+| + | | | | || |+------------+| || + | | | | || || || E3 || + | | E1 | | || E1 || E2 || || + | | | | || || || || + | | | | || |+------------++------------------+| + | +----------------+ | |+----------------+ | + | +------------+ | +----------------------------------------------------+ + | | | | + | | E2 | | + | | | | + | +------------+ | + |+------------------+| + || || + || E3 || + || || + || || + |+------------------+| + +--------------------+ + +Element { + id: uint + sizing: { min_w, min_h max_w, max_h } + bounds: { x, y, w, h } +} + +id: unique identifier of the element +sizing: the size that the element wants +bounds: the absoulte bounds that the element got assigned + +Rendering +========= + +Rendering happens when the element is called (immediately for leaf widgets like buttons and at the end +for root widgets like divs). The drawing is done on the bounds assigned to the widget, these bounds +have a one-frame delay on the current layout. + +The layout is calculated by each div at the end of their block and at frame end all the sizes and positions +are assigned at frame end by iterating the element tree. + +ElemDiv { + align: TOP-LEFT | LEFT | BOTTOM-LEFT | BOTTOM | BOTTOM-RIGHT | RIGHT | TOP-RIGHT | RIGHT | CENTER + size_x/y: { min, max } + scroll_x/y: true | false + layout: ROW | COLUMN + children_size_x/y: { min, max } +} + +size: + - min != max -> FIT sizing, fit to the content but respect the min and max size + - min == max == 0 -> GROW sizing, grow to the max amount of space possible + - min == max != 0 -> EXACT sizing +children_size: the size of the combined children sizes + + + +root(size_x: screen width, size_y: screen height, layout: ROW) { + + div1(layout: COLUMN, size_x: FIT, size_y GROW, resize_x: true) { + E1() + E2() + E3() + E4() + } <-(end div 1) + + div2(size_x: GROW, size_y: GROW) { + ... + } <-(end div 2) + + div3(layout: COLUMN, size_x: FIT, size_y: GROW) { + E5() + E6() + E7() + } <-(end div 3) + +} <-(end root) +(frame end) + + ++-Root-Div------------------------------------------------+ +|+-Div-1----------++-Div-2--------------------++-Div-3---+| +||+--------------+|| ||+-------+|| +||| E1 ||| ||| E5 ||| +||| ||| ||| ||| +||+--------------+|| ||+-------+|| [Root Div] +||+--------------+|| ||+-------+|| | +||| E2 ||| ||| E6 ||| +----------+----+-------+ +||| ||| ||| ||| v v v +||+--------------+|| ||+-------+|| [Div 1] [Div 2] [Div 3] +||+------+ || ||+-------+|| | | +||| | || ||| E7 ||| +----+----+----+ | +||| E3 | || ||| ||| v v v v | +||| | || ||+-------+|| [E1] [E2] [E3] [E4] +----+----+ +||+------+ || || || v v v +||+------+ || || || [E5] [E6] [E7] +||| | || || || +||| E4 | || || || +||| | || || || +||+------+ || || || +|| || || || +|+----------------++--------------------------++---------+| ++---------------------------------------------------------+ + +the call order is as follows + +E1() -> updates the children size of div1 +E2() -> " " +E3() -> " " +E4() -> " " +end div1() -> updates the children size of root +end div2() -> updates the children size of root +E5() -> updates the children size of div3 +E6() -> " " +E7() -> " " +end root() -> does nothing + +at frame end: + * Root: the root has a size constraint of fit so the bounds get assigned the whole window + * Div 1: the width has a size of fit, so it gets set to the children bounds, the height is set + to the root height since it has a height of GROW + - E1 to E4 get laid out + * Div 2: it has a width of GROW which is **along** the layout axis, so it gets added to grow list + the height gets set to the root height + * Div 3: the width is FIT, so it gets set to the content width, the height gets se to the root + height. + - E5 to E7 get laid out + * Div 2: is given a width (if there were other contending grow divs along the layout axis they + would also get sized). + - Now that div 2 has a size all it's children can be given a size + +Styling +======= + diff --git a/lib/ugui.c3l/src/vtree.c3 b/lib/ugui.c3l/src/vtree.c3 index 7901a58..fdc94eb 100644 --- a/lib/ugui.c3l/src/vtree.c3 +++ b/lib/ugui.c3l/src/vtree.c3 @@ -24,9 +24,17 @@ macro @zero() { $if @assignable_to(0, ElemType): return 0; - $else + $endif + + $if @assignable_to(null, ElemType): + return null; + $endif + + $if @assignable_to({}, ElemType): return {}; $endif + + //$assert true == false : ElemType.nameof +++ " is not assignable to zero or equivalent"; } fn void? VTree.init(&tree, usz size, Allocator allocator) @@ -217,6 +225,8 @@ fn usz? VTree.subtree_size(&tree, isz ref) return count; } +fn bool? VTree.is_root(&tree, isz node) => node == tree.parentof(node)!; + // iterate through the first level children, use a cursor like strtok_r fn isz? VTree.children_it(&tree, isz parent, isz *cursor) { diff --git a/test/test_tree_layout.c3 b/test/test_tree_layout.c3 new file mode 100644 index 0000000..37d9920 --- /dev/null +++ b/test/test_tree_layout.c3 @@ -0,0 +1,436 @@ +import vtree; +import std::io; +import std::math; +import std::thread; + +const short WIDTH = 128; +const short HEIGHT = 64; + +struct Size { + short min, max; +} +macro Size @grow() => {.min = 0, .max = 0}; +macro Size @exact(short s) => {.min = s, .max = s}; +macro Size @fit(short min = 0, short max = short.max) => {.min = min, .max = max}; +macro bool Size.@is_grow(s) => (s.min == 0 && s.max == 0); +macro bool Size.@is_exact(s) => (s.min == s.max && s.min != 0); +macro bool Size.@is_fit(s) => (s.min != s.max); + + + +struct Rect { + short x, y, w, h; +} + +enum LayoutDirection { + ROW, + COLUMN +} + +enum ElemType { + DIV, + ELEM +} + +enum Anchor { + TOP_LEFT, + LEFT, + BOTTOM_LEFT, + BOTTOM, + BOTTOM_RIGHT, + RIGHT, + TOP_RIGHT, + TOP, + CENTER +} + +struct Elem { + ElemType type; + Size w, h; + Rect bounds; + Size ch_w, ch_h; // children width / height + uint grow_children; // how many children want to grow, decreased once a child has grown + short orig_x, orig_y; + short occupied; // occupied space in the layout direction + LayoutDirection layout_dir; + Anchor anchor; +} + +alias ElemTree = vtree::VTree{Elem*}; + + +char[HEIGHT][WIDTH] screen; +fn void paint(Rect bounds, char c) +{ + for (short x = bounds.x; x < WIDTH && x < bounds.x + bounds.w; x++) { + for (short y = bounds.y; y < HEIGHT && y < bounds.y + bounds.h; y++) { + screen[x][y] = c; + } + } +} + +fn isz Elem.div_start(&e, ElemTree* tree, isz parent, Size w, Size h, LayoutDirection dir = ROW, Anchor anchor = TOP_LEFT, char c = ' ') +{ + e.type = DIV; + e.w = w; + e.h = h; + e.layout_dir = dir; + e.anchor = anchor; + + e.grow_children = 0; + e.occupied = 0; + e.ch_w = e.ch_h = {}; + e.orig_x = e.orig_y = 0; + + // update grow children if necessary + Elem* p = tree.get(parent) ?? &&{}; + if ((p.layout_dir == ROW && e.w.@is_grow()) || ((p.layout_dir == COLUMN && e.h.@is_grow()))) { + p.grow_children++; + } + + paint(e.bounds, c); + return tree.add(e, parent)!!; +} + +fn void update_parent_size(Elem* parent, Elem* child) +{ + // update the parent children size + switch (parent.layout_dir) { + case ROW: // on rows grow the ch width by the child width and only grow ch height if it exceeds + parent.ch_w.min += child.w.min; + parent.ch_w.max += child.w.max; + parent.ch_h.min = math::max(child.h.min, parent.ch_h.min); + parent.ch_h.max = math::max(child.h.max, parent.ch_h.max); + case COLUMN: // do the opposite on column + parent.ch_w.min = math::max(child.w.min, parent.ch_w.min); + parent.ch_w.max = math::max(child.w.max, parent.ch_w.max); + parent.ch_h.min += child.h.min; + parent.ch_h.max += child.h.max; + } +} + +fn isz Elem.div_end(&e, ElemTree* tree, isz node) +{ + isz parent = tree.parentof(node) ?? -1; + if (parent > 0) { + Elem* p = tree.get(parent)!!; + update_parent_size(p, e); + } + return parent; +} + +fn void resolve_dimensions(Elem* e, Elem* p) +{ + // ASSIGN WIDTH + switch { + case e.w.@is_exact(): + e.bounds.w = e.w.min; + case e.w.@is_grow(): + break; + // done in another pass + case e.w.@is_fit(): // fit the element's children + short min = math::max(e.ch_w.min, e.w.min); + short max = math::min(e.ch_w.max, e.w.max); + if (max >= min) { // OK! + e.bounds.w = max; + } else { + unreachable("cannot fit children"); + } + default: unreachable("width is not exact, grow or fit"); + } + + // ASSIGN HEIGHT + switch { + case e.h.@is_exact(): + e.bounds.h = e.h.min; + case e.h.@is_grow(): + break; + // done in another pass + case e.h.@is_fit(): // fit the element's children + short min = math::max(e.ch_h.min, e.h.min); + short max = math::min(e.ch_h.max, e.h.max); + if (max >= min) { // OK! + e.bounds.h = max; + } else { + unreachable("cannot fit children"); + } + default: unreachable("width is not exact, grow or fit"); + } + + switch (p.layout_dir) { + case ROW: + if (!e.w.@is_grow()) p.occupied += e.bounds.w; + case COLUMN: + if (!e.h.@is_grow()) p.occupied += e.bounds.h; + } +} + +fn void resolve_grow_elements(Elem* e, Elem* p) +{ + // WIDTH + if (e.w.@is_grow()) { + if (p.layout_dir == ROW) { // grow along the axis, divide the parent size + e.bounds.w = (short)((int)(p.bounds.w - p.occupied) / (int)p.grow_children); + p.grow_children--; + p.occupied += e.bounds.w; + } else if (p.layout_dir == COLUMN) { // grow across the layout axis, inherit width of the parent + e.bounds.w = p.bounds.w; + } + } + + // HEIGHT + if (e.h.@is_grow()) { + if (p.layout_dir == COLUMN) { // grow along the axis, divide the parent size + e.bounds.h = (short)((int)(p.bounds.h - p.occupied) / (int)p.grow_children); + p.grow_children--; + p.occupied += e.bounds.h; + } else if (p.layout_dir == ROW) { // grow across the layout axis, inherit width of the parent + e.bounds.h = p.bounds.h; + } + } +} + +fn void resolve_placement(Elem* e, Elem* p) +{ + switch (p.anchor) { + case TOP_LEFT: + e.bounds.x = p.bounds.x + p.orig_x; + e.bounds.y = p.bounds.y + p.orig_y; + case LEFT: + e.bounds.x = p.bounds.x + p.orig_x; + e.bounds.y = p.bounds.y + p.orig_y + p.bounds.h/2; + if (p.layout_dir == COLUMN) { + e.bounds.y -= p.occupied/2; + } else if (p.layout_dir == ROW) { + e.bounds.y -= e.bounds.h/2; + } + case BOTTOM_LEFT: + e.bounds.x = p.bounds.x + p.orig_x; + e.bounds.y = p.bounds.y + p.bounds.h + p.orig_y; + if (p.layout_dir == COLUMN) { + e.bounds.y -= p.occupied; + } else if (p.layout_dir == ROW) { + e.bounds.y -= e.bounds.h; + } + case BOTTOM: + e.bounds.x = p.bounds.x + p.orig_x + p.bounds.w/2; + e.bounds.y = p.bounds.y + p.bounds.h + p.orig_y; + if (p.layout_dir == COLUMN) { + e.bounds.y -= p.occupied; + e.bounds.x -= e.bounds.w/2; + } else if (p.layout_dir == ROW) { + e.bounds.y -= e.bounds.h; + e.bounds.x -= p.occupied/2; + } + case BOTTOM_RIGHT: + e.bounds.x = p.bounds.x + p.orig_x + p.bounds.w; + e.bounds.y = p.bounds.y + p.bounds.h + p.orig_y; + if (p.layout_dir == COLUMN) { + e.bounds.y -= p.occupied; + e.bounds.x -= e.bounds.w; + } else if (p.layout_dir == ROW) { + e.bounds.y -= e.bounds.h; + e.bounds.x -= p.occupied; + } + case RIGHT: + e.bounds.x = p.bounds.x + p.orig_x + p.bounds.w; + e.bounds.y = p.bounds.y + p.orig_y + p.bounds.h/2; + if (p.layout_dir == COLUMN) { + e.bounds.y -= p.occupied/2; + e.bounds.x -= e.bounds.w; + } else if (p.layout_dir == ROW) { + e.bounds.y -= e.bounds.h/2; + e.bounds.x -= p.occupied; + } + case TOP_RIGHT: + e.bounds.x = p.bounds.x + p.orig_x + p.bounds.w; + e.bounds.y = p.bounds.y + p.orig_y; + if (p.layout_dir == COLUMN) { + e.bounds.x -= e.bounds.w; + } else if (p.layout_dir == ROW) { + e.bounds.x -= p.occupied; + } + case TOP: + e.bounds.x = p.bounds.x + p.orig_x + p.bounds.w/2; + e.bounds.y = p.bounds.y + p.orig_y; + if (p.layout_dir == COLUMN) { + e.bounds.x -= e.bounds.w/2; + } else if (p.layout_dir == ROW) { + e.bounds.x -= p.occupied/2; + } + case CENTER: + e.bounds.x = p.bounds.x + p.orig_x + p.bounds.w/2; + e.bounds.y = p.bounds.y + p.orig_y + p.bounds.h/2; + if (p.layout_dir == COLUMN) { + e.bounds.x -= e.bounds.w/2; + e.bounds.y -= p.occupied/2; + } else if (p.layout_dir == ROW) { + e.bounds.x -= p.occupied/2; + e.bounds.y -= e.bounds.h/2; + } + break; + } + +/* + e.bounds.x = p.bounds.x + p.orig_x; + e.bounds.y = p.bounds.y + p.orig_y; +*/ + + switch (p.layout_dir) { + case ROW: + p.orig_x += e.bounds.w; + case COLUMN: + p.orig_y += e.bounds.h; + default: unreachable("unknown layout direction"); + } +} + +fn void frame_end(ElemTree* tree, isz root) +{ + // assign the element bounds + isz cursor = -1; + + /* + // RESOLVE DIMENSIONS + isz current = tree.level_order_it(root, &cursor)!!; + for (; current >= 0; current = tree.level_order_it(root, &cursor)!!) { + Elem* e = tree.get(current)!!; + isz pi = tree.parentof(current)!!; + Elem* p = (pi != current) ? tree.get(pi) ?? &&{} : &&{}; + resolve_dimensions(e, p); + } + + // RESOLVE GROW ELEMENTS + cursor = -1; + current = tree.level_order_it(root, &cursor)!!; + for (; current >= 0; current = tree.level_order_it(root, &cursor)!!) { + Elem* e = tree.get(current)!!; + isz pi = tree.parentof(current)!!; if (ch == current) continue; + Elem* p = (pi != current) ? tree.get(pi) ?? &&{} : &&{}; + + resolve_grow_elements(e, p); + } + + + // RESOLVE PLACEMENT + cursor = -1; + current = tree.level_order_it(root, &cursor)!!; + for (; current >= 0; current = tree.level_order_it(root, &cursor)!!) { + Elem* e = tree.get(current)!!; + isz pi = tree.parentof(current)!!; + Elem* p = (pi != current) ? tree.get(pi) ?? &&{} : &&{}; + + resolve_placement(e, p); + } + */ + + cursor = -1; + isz current = tree.level_order_it(root, &cursor)!!; + for (; current >= 0; current = tree.level_order_it(root, &cursor)!!) { + Elem* p = tree.get(current)!!; + + // RESOLVE KNOWN DIMENSIONS + isz ch_cur = 0; + isz ch = tree.children_it(current, &ch_cur)!!; + for (; ch >= 0; ch = tree.children_it(current, &ch_cur)!!) { + Elem* c = tree.get(ch)!!; + if (tree.is_root(ch)!!) { + resolve_dimensions(p, &&{}); + } else { + resolve_dimensions(c, p); + } + } + + // RESOLVE GROW CHILDREN + ch_cur = 0; + ch = tree.children_it(current, &ch_cur)!!; + for (; ch >= 0; ch = tree.children_it(current, &ch_cur)!!) { + Elem* c = tree.get(ch)!!; + if (tree.is_root(ch)!!) { + resolve_grow_elements(p, &&{}); + } else { + resolve_grow_elements(c, p); + } + } + + // RESOLVE CHILDREN PLACEMENT + ch_cur = 0; + ch = tree.children_it(current, &ch_cur)!!; + for (; ch >= 0; ch = tree.children_it(current, &ch_cur)!!) { + Elem* c = tree.get(ch)!!; + if (tree.is_root(ch)!!) { + resolve_placement(p, &&{}); + } else { + resolve_placement(c, p); + } + } + } +} + +fn void main() +{ + ElemTree tree; + tree.init(64, mem)!!; + isz parent; + defer (void)tree.free(); + + Elem root; // root div + Elem div1, div2, div3, div4; + usz frame; + while (true) { + parent = root.div_start(&tree, parent, @exact(WIDTH), @exact(HEIGHT), ROW, anchor: RIGHT); + /* + { + parent = div1.div_start(&tree, parent, @grow(), @grow(), dir: ROW, c: '1'); + { + parent = div4.div_start(&tree, parent, @exact(30), @exact(30), dir: ROW, c: '4'); + parent = div4.div_end(&tree, parent); + } + parent = div1.div_end(&tree, parent); + + if (frame < 200) { + parent = div2.div_start(&tree, parent, @exact(20), @fit(), dir: COLUMN, c: '2'); + { + parent = div3.div_start(&tree, parent, @exact(10), @exact(10), dir: ROW, c: '3'); + parent = div3.div_end(&tree, parent); + } + parent = div2.div_end(&tree, parent); + } + } + */ + parent = div3.div_start(&tree, parent, @fit(), @fit(), COLUMN, anchor: CENTER); + { + parent = div1.div_start(&tree, parent, @exact(20), @exact(20), dir: ROW, c: '1'); + parent = div1.div_end(&tree, parent); + + parent = div2.div_start(&tree, parent, @exact(10), @exact(10), dir: ROW, c: '2'); + parent = div2.div_end(&tree, parent); + } + parent = div3.div_end(&tree, parent); + + parent = root.div_end(&tree, parent); + + frame_end(&tree, parent); + tree.nuke(); + + + // draw the screen + //io::print("\e[1;1H\e[2J"); + for (short x = 0; x < WIDTH+2; x++) io::printf("%c", x == 0 || x == WIDTH+1 ? '+' : '-'); + io::printn(); + for (short y = 0; y < HEIGHT; y++) { + io::print("|"); + for (short x = 0; x < WIDTH; x++) { + char c = screen[x][y] == 0 ? 'x' : screen[x][y]; + io::printf("%c", c); + } + io::print("|"); + io::printn(); + } + for (short x = 0; x < WIDTH+2; x++) io::printf("%c", x == 0 || x == WIDTH+1 ? '+' : '-'); + io::printn("\n\n"); + + thread::sleep_ms(10); + frame++; + } +} \ No newline at end of file From 0f7d5a6506c0e7b721deb82391eccab486cdd03f Mon Sep 17 00:00:00 2001 From: Alessandro Mauri Date: Fri, 5 Sep 2025 13:10:16 +0200 Subject: [PATCH 08/13] working example of the new layout system --- lib/ugui.c3l/LAYOUT | 19 ++ lib/ugui.c3l/src/ugui_button.c3 | 41 ++- lib/ugui.c3l/src/ugui_cmd.c3 | 6 +- lib/ugui.c3l/src/ugui_core.c3 | 32 ++- lib/ugui.c3l/src/ugui_div.c3 | 83 +++--- lib/ugui.c3l/src/ugui_font.c3 | 146 +++++++++-- lib/ugui.c3l/src/ugui_layout.c3 | 437 ++++++++++++++++++++++++-------- lib/ugui.c3l/src/ugui_shapes.c3 | 73 +++--- lib/ugui.c3l/src/ugui_slider.c3 | 5 +- lib/ugui.c3l/src/ugui_sprite.c3 | 6 +- lib/ugui.c3l/src/ugui_text.c3 | 2 + resources/style.css | 4 +- src/main.c3 | 32 ++- 13 files changed, 639 insertions(+), 247 deletions(-) diff --git a/lib/ugui.c3l/LAYOUT b/lib/ugui.c3l/LAYOUT index 33c804e..717ac05 100644 --- a/lib/ugui.c3l/LAYOUT +++ b/lib/ugui.c3l/LAYOUT @@ -218,3 +218,22 @@ at frame end: Styling ======= ++-----------------------------------------+ +| MARGIN | +| | +| +-----------------------------------+ | +| |xxxxxxxxxxx BORDER xxxxxxxxxxxx| | +| |x+-------------------------------+x| | +| |x| PADDING |x| | +| |x| +-----------------------+ |x| | +| |x| | | |x| | +| |x| | CONTENT | |x| | +| |x| +-----------------------+ |x| | +| |x| |x| | +| |x+-------------------------------+x| | +| |xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx| | +| +-----------------------------------+ | +| | +| | ++-----------------------------------------+ + diff --git a/lib/ugui.c3l/src/ugui_button.c3 b/lib/ugui.c3l/src/ugui_button.c3 index f2132be..c3603fe 100644 --- a/lib/ugui.c3l/src/ugui_button.c3 +++ b/lib/ugui.c3l/src/ugui_button.c3 @@ -19,13 +19,14 @@ fn ElemEvents? Ctx.button_id(&ctx, Id id, String label, String icon) Sprite* sprite = icon != "" ? ctx.sprite_atlas.get(icon)! : &&(Sprite){}; - Rect min_size = {0, 0, style.size, style.size}; - Rect text_size = ctx.get_text_bounds(label)!; + // TODO: get min size by style + TextSize text_size = ctx.measure_string(label)!; Rect icon_size = sprite.rect(); - - ushort h_lh = (ushort)(ctx.font.line_height() / 2); - ushort left_pad = label != "" ? h_lh : 0; - ushort inner_pad = label != "" && icon != "" ? h_lh : 0; + + ushort min_size = style.size; + ushort half_lh = (ushort)(ctx.font.line_height() / 2); + ushort left_pad = label != "" ? half_lh : 0; + ushort inner_pad = label != "" && icon != "" ? half_lh : 0; ushort right_pad = left_pad; /* |left_pad @@ -39,13 +40,25 @@ fn ElemEvents? Ctx.button_id(&ctx, Id id, String label, String icon) * |inner_pad |right_pad */ - Rect tot_size = { - .w = (short)max(min_size.w, icon_size.w + text_size.w + left_pad + inner_pad + right_pad), - .h = (short)max(min_size.h, max(icon_size.h, text_size.h)), + elem.layout.w = { + .min = (short)max(min_size, left_pad + icon_size.w + inner_pad + text_size.width.min + right_pad), + .max = (short)max(min_size, left_pad + icon_size.w + inner_pad + text_size.width.max + right_pad), }; - - elem.bounds = ctx.layout_element(parent, tot_size, style); - if (elem.bounds.is_null()) return {}; + elem.layout.h = { + .min = (short)max(min_size, left_pad + icon_size.h + text_size.height.min + right_pad), + .max = (short)max(min_size, left_pad + icon_size.w + text_size.height.max + right_pad), + }; + // add style border and padding + elem.layout.margin = style.margin; + elem.layout.border = style.border; + elem.layout.padding = style.padding; + + elem.layout.children.w = elem.layout.w; + elem.layout.children.h = elem.layout.h; + + update_parent_grow(elem, parent); + update_parent_size(elem, parent); + elem.events = ctx.get_elem_events(elem); Rect content_bounds = elem.content_bounds(style); @@ -55,7 +68,7 @@ fn ElemEvents? Ctx.button_id(&ctx, Id id, String label, String icon) .w = content_bounds.w - icon_size.w - left_pad - inner_pad - right_pad, .h = content_bounds.h }; - text_bounds = text_size.center_to(text_bounds); + //text_bounds = text_size.center_to(text_bounds); Rect icon_bounds = { .x = content_bounds.x, @@ -82,6 +95,7 @@ fn ElemEvents? Ctx.button_id(&ctx, Id id, String label, String icon) return elem.events; } +/* // FIXME: this should be inside the style macro Ctx.checkbox(&ctx, String desc, Point off, bool* active, String tick_sprite = {}, ...) @@ -152,3 +166,4 @@ fn void? Ctx.toggle_id(&ctx, Id id, String description, Point off, bool* active) s.margin = s.border = s.padding = {}; ctx.push_rect(t, parent.div.z_index, &s)!; } +*/ \ No newline at end of file diff --git a/lib/ugui.c3l/src/ugui_cmd.c3 b/lib/ugui.c3l/src/ugui_cmd.c3 index 463605e..804f1c4 100644 --- a/lib/ugui.c3l/src/ugui_cmd.c3 +++ b/lib/ugui.c3l/src/ugui_cmd.c3 @@ -97,9 +97,9 @@ macro Ctx.push_cmd(&ctx, Cmd *cmd, int z_index) default: return ctx.cmd_queue.enqueue(cmd); } if (cull_rect(rect, ctx.div_scissor)) { - io::print("NOPE: "); - io::print(cmd.rect.rect); - io::printn(cmd.z_index); +// io::print("NOPE: "); +// io::print(cmd.rect.rect); +// io::printn(cmd.z_index); // unreachable(); return; } diff --git a/lib/ugui.c3l/src/ugui_core.c3 b/lib/ugui.c3l/src/ugui_core.c3 index e3f1a01..19f0a83 100644 --- a/lib/ugui.c3l/src/ugui_core.c3 +++ b/lib/ugui.c3l/src/ugui_core.c3 @@ -9,6 +9,15 @@ import std::core::string; import std::core::mem::allocator; +macro println(...) +{ + $for var $i = 0; $i < $vacount; $i++: + io::print($vaexpr[$i]); + $endfor + io::printn(); +} + + // element ids are just long ints alias Id = uint; @@ -45,7 +54,9 @@ struct Elem { ElemFlags flags; ElemEvents events; Rect bounds; + Rect children_bounds; ElemType type; + Layout layout; union { ElemDiv div; ElemButton button; @@ -131,6 +142,7 @@ const uint GOLDEN_RATIO = 0x9E3779B9; // with the Cantor pairing function fn Id? Ctx.gen_id(&ctx, Id id2) { + // FIXME: this is SHIT Id id1 = ctx.tree.get(ctx.active_div)!; // Mix the two IDs non-linearly Id mixed = id1 ^ id2.rotate_left(13); @@ -160,6 +172,7 @@ fn Elem*? Ctx.get_elem(&ctx, Id id, ElemType type) elem.flags = (ElemFlags)0; elem.flags.is_new = is_new; elem.id = id; + elem.layout = {}; if (is_new == false && elem.type != type) { return WRONG_ELEMENT_TYPE?; } else { @@ -232,16 +245,15 @@ fn void? Ctx.frame_begin(&ctx) //elem.flags.has_focus = ctx.has_focus; elem.bounds = {0, 0, ctx.width, ctx.height}; - elem.div.layout = LAYOUT_ROW; elem.div.z_index = 0; - elem.div.children_bounds = elem.bounds; elem.div.scroll_x.enabled = false; elem.div.scroll_y.enabled = false; - elem.div.pcb = {}; - elem.div.origin_c = {}; - elem.div.origin_r = {}; + elem.layout.dir = ROW; + elem.layout.anchor = TOP_LEFT; + elem.layout.w = @exact(ctx.width); + elem.layout.h = @exact(ctx.height); - ctx.div_scissor = {0, 0, ctx.width, ctx.height}; + ctx.div_scissor = elem.bounds; // The root element does not push anything to the stack // TODO: add a background color taken from a theme or config @@ -253,7 +265,13 @@ fn void? Ctx.frame_end(&ctx) { // FIXME: this is not guaranteed to be root. the user might forget to close a div or some other element Elem* root = ctx.get_active_div()!; - if (root.id != ROOT_ID) return WRONG_ID?; + if (root.id != ROOT_ID) { + io::printn(root.id); + return WRONG_ID?; + } + + // DO THE LAYOUT + ctx.layout_element_tree(); // 1. clear the tree ctx.tree.nuke(); diff --git a/lib/ugui.c3l/src/ugui_div.c3 b/lib/ugui.c3l/src/ugui_div.c3 index 10d6c40..4e738db 100644 --- a/lib/ugui.c3l/src/ugui_div.c3 +++ b/lib/ugui.c3l/src/ugui_div.c3 @@ -5,7 +5,6 @@ import std::math; // div element struct ElemDiv { - Layout layout; struct scroll_x { bool enabled; bool on; @@ -18,28 +17,42 @@ struct ElemDiv { } ushort scroll_size; int z_index; - Rect children_bounds; // current frame children bounds - Rect pcb; // previous frame children bounds - Point origin_r, origin_c; } // useful macro to start and end a div, capturing the trailing block -macro Ctx.@div(&ctx, Rect size, bool scroll_x = false, bool scroll_y = false, ...; @body()) +macro Ctx.@div(&ctx, + Size width = @grow, Size height = @grow, + LayoutDirection dir = ROW, + Anchor anchor = TOP_LEFT, + bool scroll_x = false, bool scroll_y = false, + ...; + @body() + ) { - ctx.div_begin(size, scroll_x, scroll_y, $vasplat)!; + ctx.div_begin(width, height, dir, anchor, scroll_x, scroll_y, $vasplat)!; @body(); ctx.div_end()!; } -// begin a widget container, or div, the size determines the offset (x,y) width and height. -// if the width or height are zero the width or height are set to the maximum available. -// if the width or height are negative the width or height will be calculated based on the children size -// sort similar to a flexbox, and the minimum size is set by the negative of the width or height -// FIXME: there is a bug if the size.w or size.h == -0 -macro Ctx.div_begin(&ctx, Rect size, bool scroll_x = false, bool scroll_y = false, ...) - => ctx.div_begin_id(@compute_id($vasplat), size, scroll_x, scroll_y); -fn void? Ctx.div_begin_id(&ctx, Id id, Rect size, bool scroll_x, bool scroll_y) +macro Ctx.div_begin(&ctx, + Size width = @grow(), Size height = @grow(), + LayoutDirection dir = ROW, + Anchor anchor = TOP_LEFT, + bool scroll_x = false, bool scroll_y = false, + ... + ) +{ + return ctx.div_begin_id(@compute_id($vasplat), width, height, dir, anchor, scroll_x, scroll_y); +} + +fn void? Ctx.div_begin_id(&ctx, + Id id, + Size width, Size height, + LayoutDirection dir, + Anchor anchor, + bool scroll_x, bool scroll_y + ) { id = ctx.gen_id(id)!; @@ -55,30 +68,24 @@ fn void? Ctx.div_begin_id(&ctx, Id id, Rect size, bool scroll_x, bool scroll_y) elem.div.scroll_size = slider_style.size ? slider_style.size : (style.size ? style.size : DEFAULT_STYLE.size); elem.div.z_index = parent.div.z_index + 1; - // 2. layout the element - Rect wanted_size = { - .x = size.x, - .y = size.y, - .w = size.w < 0 ? max(elem.div.pcb.w, (short)-size.w) : size.w, - .h = size.h < 0 ? max(elem.div.pcb.h, (short)-size.h) : size.h, - }; - elem.bounds = ctx.layout_element(parent, wanted_size, style); - elem.div.children_bounds = {.x = elem.bounds.x, .y = elem.bounds.y}; - // update the ctx scissor ctx.div_scissor = elem.bounds; ctx.push_scissor(elem.bounds, elem.div.z_index)!; - // 4. Fill the div fields - elem.div.origin_c = { - .x = elem.bounds.x, - .y = elem.bounds.y + // update layout with correct info + elem.layout = { + .w = width, + .h = height, + .dir = dir, + .anchor = anchor, + .margin = style.margin, + .border = style.border, + .padding = style.padding, }; - elem.div.origin_r = elem.div.origin_c; - elem.div.layout = parent.div.layout; + + // update parent grow children + update_parent_grow(elem, parent); - // Add the background to the draw stack - bool do_border = parent.div.layout == LAYOUT_FLOATING; ctx.push_rect(elem.bounds, elem.div.z_index, style)!; elem.events = ctx.get_elem_events(elem); @@ -89,14 +96,9 @@ fn void? Ctx.div_begin_id(&ctx, Id id, Rect size, bool scroll_x, bool scroll_y) fn void? Ctx.div_end(&ctx) { - // swap the children bounds Elem* elem = ctx.get_active_div()!; - elem.div.pcb = elem.div.children_bounds; - - // FIXME: this causes all elements inside the div to loose focus since the mouse press happens - // both inside the element and inside the div bounds - //elem.events = ctx.get_elem_events(elem); +/* FIXME: Redo slider functionality Rect cb = elem.div.pcb; // children bounds bottom-right corner Point cbc = { @@ -155,10 +157,13 @@ fn void? Ctx.div_end(&ctx) ctx.slider_hor_id(hsid_raw, hslider, &elem.div.scroll_x.value, max((float)bc.x / cbc.x, (float)0.15))!; elem.div.layout = prev_l; } +*/ // the active_div returns to the parent of the current one ctx.active_div = ctx.tree.parentof(ctx.active_div)!; Elem* parent = ctx.get_parent()!; - // TODO: reset the scissor back to the parent div ctx.div_scissor = parent.bounds; + ctx.push_scissor(parent.bounds, elem.div.z_index)!; + + update_parent_size(elem, parent); } diff --git a/lib/ugui.c3l/src/ugui_font.c3 b/lib/ugui.c3l/src/ugui_font.c3 index 17a36ac..51ff0b9 100644 --- a/lib/ugui.c3l/src/ugui_font.c3 +++ b/lib/ugui.c3l/src/ugui_font.c3 @@ -9,11 +9,34 @@ import std::io; import std::ascii; +// ---------------------------------------------------------------------------------- // +// CODEPOINT // +// ---------------------------------------------------------------------------------- // + // unicode code point, different type for a different hash alias Codepoint = uint; +<* +@require off != null +@require str.ptr != null +*> +fn Codepoint str_to_codepoint(char[] str, usz* off) +{ + Codepoint cp; + isz b = grapheme::decode_utf8(str, str.len, (uint*)&cp); + if (b == 0 || b > str.len) { + return 0; + } + *off = b; + return cp; +} + //macro uint Codepoint.hash(self) => ((uint)self).hash(); +// ---------------------------------------------------------------------------------- // +// FONT ATLAS // +// ---------------------------------------------------------------------------------- // + /* width and height of a glyph contain the kering advance * (u,v) * +-------------*---+ - @@ -60,7 +83,7 @@ struct Font { fn void? Font.load(&font, String name, ZString path, uint height, float scale) { - font.table.init(allocator::heap(), capacity: FONT_CACHED); + font.table.init(allocator::mem, capacity: FONT_CACHED); font.id = name.hash(); font.size = height*scale; @@ -160,26 +183,39 @@ fn void Font.free(&font) schrift::freefont(font.sft.font); } + +// ---------------------------------------------------------------------------------- // +// FONT LOAD AND QUERY // +// ---------------------------------------------------------------------------------- // + fn void? Ctx.load_font(&ctx, String name, ZString path, uint height, float scale = 1.0) { return ctx.font.load(name, path, height, scale); } -<* -@require off != null -@require str.ptr != null -*> -fn Codepoint str_to_codepoint(char[] str, usz* off) +// TODO: check if the font is present in the context +fn Id Ctx.get_font_id(&ctx, String label) { - Codepoint cp; - isz b = grapheme::decode_utf8(str, str.len, (uint*)&cp); - if (b == 0 || b > str.len) { - return 0; - } - *off = b; - return cp; + return (Id)label.hash(); } +fn Atlas*? Ctx.get_font_atlas(&ctx, String name) +{ + // TODO: use the font name, for now there is only one font + if (name.hash() != ctx.font.id) { + return WRONG_ID?; + } + + return &ctx.font.atlas; +} + +fn int Font.line_height(&font) => (int)(font.ascender - font.descender + (float)0.5); + +// ---------------------------------------------------------------------------------- // +// TEXT MEASUREMENT // +// ---------------------------------------------------------------------------------- // + + const uint TAB_SIZE = 4; // TODO: change the name @@ -189,6 +225,7 @@ struct TextInfo { Rect bounds; String text; usz off; + Size width, height; // current glyph info Point origin; @@ -295,20 +332,81 @@ fn Rect? Ctx.get_text_bounds(&ctx, String text) return ti.text_bounds; } -// TODO: check if the font is present in the context -fn Id Ctx.get_font_id(&ctx, String label) -{ - return (Id)label.hash(); + +struct TextSize { + Size width, height; } -fn Atlas*? Ctx.get_font_atlas(&ctx, String name) +// Measeure the size of a string. +// width.min: as if each word is broken up by a new line +// width.max: the width of the string left as-is +// height.min: the height of the string left as-is +// height.max: the height of the string with each word broken up by a new line +fn TextSize? Ctx.measure_string(&ctx, String text) { - // TODO: use the font name, for now there is only one font - if (name.hash() != ctx.font.id) { - return WRONG_ID?; + Font* font = &ctx.font; + short baseline = (short)font.ascender; + short line_height = (short)font.line_height(); + short line_gap = (short)font.linegap; + short space_width = font.get_glyph(' ').adv!; + short tab_width = space_width * TAB_SIZE; + + isz off; + usz x; + + TextSize ts; + + short word_width; + short words = 1; + Rect bounds; // unaltered text bounds; + Point origin; + + Codepoint cp = str_to_codepoint(text[off..], &x); + for (; cp != 0; cp = str_to_codepoint(text[off..], &x)) { + off += x; + Glyph* gp = font.get_glyph(cp)!; + + // update the text bounds + switch { + case cp == '\n': + origin.x = 0; + origin.y += line_height + line_gap; + case cp == '\t': + origin.x += tab_width; + case ascii::is_cntrl((char)cp): + break; + default: + Rect b = { + .x = origin.x + gp.ox, + .y = origin.y + gp.oy + baseline, + .w = gp.w, + .h = gp.h, + }; + bounds = containing_rect(bounds, b); + origin.x += gp.adv; + } + + // update the word width + switch { + case ascii::is_space((char)cp): + if (word_width > ts.width.min) ts.width.min = word_width; + word_width = 0; + words++; + default: + word_width += gp.w + gp.ox; + if (off < text.len) word_width += gp.adv; + } } + // end of string is also end of word + if (word_width > ts.width.min) ts.width.min = word_width; - return &ctx.font.atlas; + ts.width.max = bounds.w; + ts.height.min = bounds.h; + ts.height.max = words * line_height + line_gap * (words-1); + + //io::print("'"); + //io::print(text); + //io::print("': "); + //io::printn(ts); + return ts; } - -fn int Font.line_height(&font) => (int)(font.ascender - font.descender + (float)0.5); diff --git a/lib/ugui.c3l/src/ugui_layout.c3 b/lib/ugui.c3l/src/ugui_layout.c3 index 4315e61..5d5eae9 100644 --- a/lib/ugui.c3l/src/ugui_layout.c3 +++ b/lib/ugui.c3l/src/ugui_layout.c3 @@ -1,54 +1,102 @@ module ugui; -enum Layout { - LAYOUT_ROW, - LAYOUT_COLUMN, - LAYOUT_FLOATING, - LAYOUT_ABSOLUTE, +import std::math; +import std::io; + +enum LayoutDirection { + ROW, + COLUMN, } -fn void? Ctx.layout_set_row(&ctx) -{ - Elem *parent = ctx.get_parent()!; - parent.div.layout = LAYOUT_ROW; +enum Anchor { + TOP_LEFT, + LEFT, + BOTTOM_LEFT, + BOTTOM, + BOTTOM_RIGHT, + RIGHT, + TOP_RIGHT, + TOP, + CENTER } -fn void? Ctx.layout_set_column(&ctx) -{ - Elem *parent = ctx.get_parent()!; - parent.div.layout = LAYOUT_COLUMN; +struct Layout { + Size w, h; + struct children { + Size w, h; + } + ushort grow_children; + short occupied; + struct origin { + short x, y; + } + LayoutDirection dir; + Anchor anchor; + // FIXME: all this cruft with margin border and padding could be resolved by simply providing + // a "Rect content_offset" that represents the combined effect of all three above + Rect margin, border, padding; } -fn void? Ctx.layout_set_floating(&ctx) +// Total width of a layout element, complete with margin, border and padding +macro Size Layout.total_width(&el) { - Elem *parent = ctx.get_parent()!; - parent.div.layout = LAYOUT_FLOATING; + Rect min = (Rect){.w = el.w.min, .h = el.h.min}.expand(el.margin).expand(el.border).expand(el.padding); + Rect max = (Rect){.w = el.w.max, .h = el.h.max}.expand(el.margin).expand(el.border).expand(el.padding); + return {.min = min.w, .max = max.w}; } -fn void? Ctx.layout_next_row(&ctx) +// Total height of a layout element, complete with margin, border and padding +macro Size Layout.total_height(&el) { - Elem *parent = ctx.get_parent()!; + Rect min = (Rect){.w = el.w.min, .h = el.h.min}.expand(el.margin).expand(el.border).expand(el.padding); + Rect max = (Rect){.w = el.w.max, .h = el.h.max}.expand(el.margin).expand(el.border).expand(el.padding); + return {.min = min.h, .max = max.h}; +} - parent.div.origin_r = { - .x = parent.bounds.x, - .y = parent.div.children_bounds.bottom_right().y, +// The content space of the element +macro Point Elem.content_space(&e) +{ + return { + .x = e.bounds.w - e.layout.border.x - e.layout.border.w - e.layout.padding.x - e.layout.padding.w, + .y = e.bounds.h - e.layout.border.y - e.layout.border.h - e.layout.padding.y - e.layout.padding.h, }; - parent.div.origin_c = parent.div.origin_r; + } + +// Update the parent element's grow children counter +fn void update_parent_grow(Elem* child, Elem* parent) +{ + switch (parent.layout.dir) { + case ROW: + if (child.layout.w.@is_grow()) parent.layout.grow_children++; + case COLUMN: + if (child.layout.h.@is_grow()) parent.layout.grow_children++; + } } -fn void? Ctx.layout_next_column(&ctx) +// Update the parent element's children size +fn void update_parent_size(Elem* child, Elem* parent) { - Elem *parent = ctx.get_parent()!; + Layout* cl = &child.layout; + Layout* pl = &parent.layout; - parent.div.origin_c = { - .x = parent.div.children_bounds.bottom_right().x, - .y = parent.bounds.y, - }; - parent.div.origin_r = parent.div.origin_c; + switch (pl.dir) { + case ROW: // on rows grow the ch width by the child width and only grow ch height if it exceeds + pl.children.w += cl.total_width(); + pl.children.h = pl.children.h.comb_max(cl.total_height()); + case COLUMN: // do the opposite on column + pl.children.w = pl.children.w.comb_max(cl.total_width()); + pl.children.h += cl.total_height(); + } +} + +fn void update_children_bounds(Elem* child, Elem* parent) +{ + parent.children_bounds = containing_rect(child.bounds, parent.bounds); } macro Rect Elem.content_bounds(&elem, style) => elem.bounds.pad(style.border).pad(style.padding); +/* macro Rect Elem.get_view(&elem) { Rect off; @@ -67,91 +115,266 @@ macro Point Elem.get_view_off(&elem) { return elem.get_view().sub(elem.bounds).position(); } +*/ -// position the rectangle inside the parent according to the layout -// parent: parent div -// rect: the requested size -// style: apply style -<* -@require ctx != null -@require parent.type == ETYPE_DIV -*> -fn Rect Ctx.layout_element(&ctx, Elem *parent, Rect rect, Style* style) +// Assign the width and height of an element in the directions that it doesn't need to grow +fn void resolve_dimensions(Elem* e, Elem* p) { - ElemDiv* div = &parent.div; + // The element's border and paddign total width and height + Point m_tot = {.x = e.layout.margin.x + e.layout.margin.w, .y = e.layout.margin.y + e.layout.margin.h}; + Point b_tot = {.x = e.layout.border.x + e.layout.border.w, .y = e.layout.border.y + e.layout.border.h}; + Point p_tot = {.x = e.layout.padding.x + e.layout.padding.w, .y = e.layout.padding.y + e.layout.padding.h}; - Rect parent_bounds, parent_view; - Rect child_placement, child_occupied; + // ASSIGN WIDTH + switch { + case e.layout.w.@is_exact(): + // if width is exact then assign it to the bounds and add border and paddign + e.bounds.w = e.layout.w.min + b_tot.x + p_tot.x; + case e.layout.w.@is_grow(): + // done in another pass + break; + case e.layout.w.@is_fit(): // fit the element's children + Size elem_width = e.layout.w; // the element's content width + Size children_width = e.layout.children.w; // element children (content) width + Size combined = elem_width.combine(children_width); - // 1. Select the right origin - Point origin; - switch (div.layout) { - case LAYOUT_ROW: - origin = div.origin_r; - case LAYOUT_COLUMN: - origin = div.origin_c; - case LAYOUT_FLOATING: // none, relative to zero zero - case LAYOUT_ABSOLUTE: // absolute position, this is a no-op, return the rect - return rect; - default: // error - return {}; + if (combined.min <= combined.max) { // OK! + // the final element width is the content width plus padding and border + e.bounds.w = min(elem_width.max, children_width.max) + b_tot.x + p_tot.x; + } else { + unreachable("Cannot fit children width: min:%d max:%d", combined.min, combined.max); + } + /* + Size w = e.needed_width().combine(e.layout.children.w); + if (w.max >= w.min) { // OK! + e.bounds.w = w.min; + } else { + println(e.needed_width(), " ", e.needed_height()); + println(e.layout.children.w, " ", e.layout.children.h); + unreachable("Cannot fit children width: min:%d max:%d", w.min, w.max); + } + */ + default: unreachable("width is not exact, grow or fit"); } - // 2. Compute the parent's view - parent_bounds = parent.bounds; - parent_view = parent.get_view(); + // ASSIGN HEIGHT + switch { + case e.layout.h.@is_exact(): + // if width is exact then assign it to the bounds and add border and paddign + e.bounds.h = e.layout.h.min + b_tot.y + p_tot.y; + case e.layout.h.@is_grow(): + break; + // done in another pass + case e.layout.h.@is_fit(): // fit the element's children + Size elem_height = e.layout.h; // the element's content width + Size children_height = e.layout.children.h; // element children (content) width + Size combined = elem_height.combine(children_height); - // 3. Compute the placement and occupied area + if (combined.min <= combined.max) { // OK! + // the final element width is the content width plus padding and border + e.bounds.h = min(elem_height.max, children_height.max) + b_tot.y + p_tot.y; + } else { + unreachable("Cannot fit children height: min:%d max:%d", combined.min, combined.max); + } + /* + Size h = e.needed_height().combine(e.layout.children.h); + if (h.max >= h.min) { // OK! + e.bounds.h = h.max; + } else { + unreachable("Cannot fit children height: min:%d max:%d", h.min, h.max); + } + */ + default: unreachable("width is not exact, grow or fit"); + } - // grow rect (wanted size) when widht or height are less than zero - bool adapt_x = rect.w <= 0; - bool adapt_y = rect.h <= 0; - if (adapt_x) rect.w = parent_bounds.w - parent_bounds.x - origin.x; - if (adapt_y) rect.h = parent_bounds.h - parent_bounds.y - origin.y; - - // offset placement and area - child_placement = child_placement.off(origin.add(rect.position())); - child_occupied = child_occupied.off(origin.add(rect.position())); - - Rect margin = style.margin; - Rect border = style.border; - Rect padding = style.padding; - - // padding, grows both the placement and occupied area - child_placement = child_placement.grow(padding.position().add(padding.size())); - child_occupied = child_occupied.grow(padding.position().add(padding.size())); - // border, grows both the placement and occupied area - child_placement = child_placement.grow(border.position().add(border.size())); - child_occupied = child_occupied.grow(border.position().add(border.size())); - // margin, offsets the placement and grows the occupied area - child_placement = child_placement.off(margin.position()); - child_occupied = child_occupied.grow(margin.position().add(margin.size())); - - // oh yeah also adjust the rect if i was to grow - if (adapt_x) rect.w -= padding.x+padding.w + border.x+border.w + margin.x+margin.w; - if (adapt_y) rect.h -= padding.y+padding.h + border.y+border.h + margin.y+margin.h; - - // set the size - child_placement = child_placement.grow(rect.size()); - child_occupied = child_occupied.grow(rect.size()); - - // 4. Update the parent's origin - div.origin_r = { - .x = child_occupied.bottom_right().x, - .y = origin.y, - }; - div.origin_c = { - .x = origin.x, - .y = child_occupied.bottom_right().y, - }; - - // 5. Update the parent's children bounds - div.children_bounds = containing_rect(div.children_bounds, child_occupied); - - // 99. return the placement - if (child_placement.collides(parent_view)) { - return child_placement.off(parent.get_view_off().neg()); - } else { - return {}; + // the occupied space must account for the element's margin as well + switch (p.layout.dir) { + case ROW: + if (!e.layout.w.@is_grow()) p.layout.occupied += e.bounds.w + m_tot.x; + case COLUMN: + if (!e.layout.h.@is_grow()) p.layout.occupied += e.bounds.h + m_tot.y; + } +} + +fn void resolve_grow_elements(Elem* e, Elem* p) +{ + Point m_tot = {.x = e.layout.margin.x + e.layout.margin.w, .y = e.layout.margin.y + e.layout.margin.h}; + Point b_tot = {.x = e.layout.border.x + e.layout.border.w, .y = e.layout.border.y + e.layout.border.h}; + Point p_tot = {.x = e.layout.padding.x + e.layout.padding.w, .y = e.layout.padding.y + e.layout.padding.h}; + + // WIDTH + if (e.layout.w.@is_grow()) { + if (p.layout.dir == ROW) { // grow along the axis, divide the parent size + short slot = (short)((p.content_space().x - p.layout.occupied) / p.layout.grow_children); + // the space slot accounts for the total size, while the element bounds does not account + // for the margin + e.bounds.w = slot - m_tot.x; + p.layout.grow_children--; + p.layout.occupied += slot; + } else if (p.layout.dir == COLUMN) { // grow across the layout axis, inherit width of the parent + e.bounds.w = p.content_space().x - m_tot.x; + } + } + + // HEIGHT + if (e.layout.h.@is_grow()) { + if (p.layout.dir == COLUMN) { // grow along the axis, divide the parent size + short slot = (short)((p.content_space().y - p.layout.occupied) / p.layout.grow_children); + e.bounds.h = slot - m_tot.y; + p.layout.grow_children--; + p.layout.occupied += slot; + } else if (p.layout.dir == ROW) { // grow across the layout axis, inherit width of the parent + e.bounds.h = p.content_space().y - m_tot.y; + } + } +} + +fn void resolve_placement(Elem* c, Elem* p) +{ + Layout* pl = &p.layout; + Layout* cl = &c.layout; + + Point off = { + .x = p.bounds.x + pl.origin.x + cl.margin.x, + .y = p.bounds.y + pl.origin.y + cl.margin.x, + }; + + switch (pl.anchor) { + case TOP_LEFT: + c.bounds.x = off.x; + c.bounds.y = off.y; + case LEFT: + c.bounds.x = off.x; + c.bounds.y = off.y + p.bounds.h/2; + if (pl.dir == COLUMN) { + c.bounds.y -= pl.occupied/2; + } else if (pl.dir == ROW) { + c.bounds.y -= c.bounds.h/2; + } + case BOTTOM_LEFT: + c.bounds.x = off.x; + c.bounds.y = off.y + p.bounds.h ; + if (pl.dir == COLUMN) { + c.bounds.y -= pl.occupied; + } else if (pl.dir == ROW) { + c.bounds.y -= c.bounds.h; + } + case BOTTOM: + c.bounds.x = off.x + p.bounds.w/2; + c.bounds.y = off.y + p.bounds.h; + if (pl.dir == COLUMN) { + c.bounds.y -= pl.occupied; + c.bounds.x -= c.bounds.w/2; + } else if (pl.dir == ROW) { + c.bounds.y -= c.bounds.h; + c.bounds.x -= pl.occupied/2; + } + case BOTTOM_RIGHT: + c.bounds.x = off.x + p.bounds.w; + c.bounds.y = off.y + p.bounds.h; + if (pl.dir == COLUMN) { + c.bounds.y -= pl.occupied; + c.bounds.x -= c.bounds.w; + } else if (pl.dir == ROW) { + c.bounds.y -= c.bounds.h; + c.bounds.x -= pl.occupied; + } + case RIGHT: + c.bounds.x = off.x + p.bounds.w; + c.bounds.y = off.y + p.bounds.h/2; + if (pl.dir == COLUMN) { + c.bounds.y -= pl.occupied/2; + c.bounds.x -= c.bounds.w; + } else if (pl.dir == ROW) { + c.bounds.y -= c.bounds.h/2; + c.bounds.x -= pl.occupied; + } + case TOP_RIGHT: + c.bounds.x = off.x + p.bounds.w; + c.bounds.y = off.y; + if (pl.dir == COLUMN) { + c.bounds.x -= c.bounds.w; + } else if (pl.dir == ROW) { + c.bounds.x -= pl.occupied; + } + case TOP: + c.bounds.x = off.x + p.bounds.w/2; + c.bounds.y = off.y; + if (pl.dir == COLUMN) { + c.bounds.x -= c.bounds.w/2; + } else if (pl.dir == ROW) { + c.bounds.x -= pl.occupied/2; + } + case CENTER: + c.bounds.x = off.x + p.bounds.w/2; + c.bounds.y = off.y + p.bounds.h/2; + if (pl.dir == COLUMN) { + c.bounds.x -= c.bounds.w/2; + c.bounds.y -= pl.occupied/2; + } else if (pl.dir == ROW) { + c.bounds.x -= pl.occupied/2; + c.bounds.y -= c.bounds.h/2; + } + break; + } + + switch (pl.dir) { + case ROW: + pl.origin.x += c.bounds.w + cl.margin.x + cl.margin.w; + case COLUMN: + pl.origin.y += c.bounds.h + cl.margin.x + cl.margin.w; + default: unreachable("unknown layout direction"); + } + + //sprintln("origin: ", p.layout.origin, " bounds: ", c.bounds); +} + +fn void Ctx.layout_element_tree(&ctx) +{ + isz cursor = -1; + isz current = ctx.tree.level_order_it(0, &cursor)!!; + for (; current >= 0; current = ctx.tree.level_order_it(0, &cursor)!!) { + Elem* p = ctx.find_elem(ctx.tree.get(current))!!; + // offset the origin by the margin + p.layout.origin.x = p.layout.border.x + p.layout.padding.x; + p.layout.origin.y = p.layout.border.y + p.layout.padding.y; + + // RESOLVE KNOWN DIMENSIONS + isz ch_cur = 0; + isz ch = ctx.tree.children_it(current, &ch_cur)!!; + for (; ch >= 0; ch = ctx.tree.children_it(current, &ch_cur)!!) { + Elem* c = ctx.find_elem(ctx.tree.get(ch))!!; + if (ctx.tree.is_root(ch)!!) { + resolve_dimensions(p, &&{}); + } else { + resolve_dimensions(c, p); + } + } + + // RESOLVE GROW CHILDREN + ch_cur = 0; + ch = ctx.tree.children_it(current, &ch_cur)!!; + for (; ch >= 0; ch = ctx.tree.children_it(current, &ch_cur)!!) { + Elem* c = ctx.find_elem(ctx.tree.get(ch))!!; + if (ctx.tree.is_root(ch)!!) { + resolve_grow_elements(p, &&{}); + } else { + resolve_grow_elements(c, p); + } + } + + // RESOLVE CHILDREN PLACEMENT + ch_cur = 0; + ch = ctx.tree.children_it(current, &ch_cur)!!; + for (; ch >= 0; ch = ctx.tree.children_it(current, &ch_cur)!!) { + Elem* c = ctx.find_elem(ctx.tree.get(ch))!!; + if (ctx.tree.is_root(ch)!!) { + resolve_placement(p, &&{}); + update_children_bounds(c, p); + } else { + resolve_placement(c, p); + update_children_bounds(c, p); + } + } } } diff --git a/lib/ugui.c3l/src/ugui_shapes.c3 b/lib/ugui.c3l/src/ugui_shapes.c3 index 6c8e592..b134203 100644 --- a/lib/ugui.c3l/src/ugui_shapes.c3 +++ b/lib/ugui.c3l/src/ugui_shapes.c3 @@ -174,6 +174,16 @@ macro Rect Rect.pad(Rect a, Rect b) }; } +macro Rect Rect.expand(Rect a, Rect b) +{ + return { + .x = a.x - b.x, + .y = a.y - b.y, + .w = a.w + b.x + b.w, + .h = a.h + b.y + b.h, + }; +} + // ---------------------------------------------------------------------------------- // // POINT // @@ -189,39 +199,12 @@ macro bool Point.in_rect(Point p, Rect r) return (p.x >= r.x && p.x <= r.x + r.w) && (p.y >= r.y && p.y <= r.y + r.h); } -macro Point Point.add(Point a, Point b) -{ - return { - .x = a.x + b.x, - .y = a.y + b.y, - }; -} +macro Point Point.add(Point a, Point b) @operator_s(+) => {.x = a.x+b.x, .y = a.y+b.y}; +macro Point Point.sub(Point a, Point b) @operator_s(-) => {.x = a.x-b.x, .y = a.y-b.y}; +macro Point Point.neg(Point p) @operator_s(-) => {-p.x, -p.y}; -macro Point Point.sub(Point a, Point b) -{ - return { - .x = a.x - b.x, - .y = a.y - b.y, - }; -} - -macro Point Point.neg(Point p) => {-p.x, -p.y}; - -macro Point Point.max(Point a, Point b) -{ - return { - .x = max(a.x, b.x), - .y = max(a.y, b.y), - }; -} - -macro Point Point.min(Point a, Point b) -{ - return { - .x = min(a.x, b.x), - .y = min(a.y, b.y), - }; -} +macro Point Point.max(Point a, Point b) => {.x = max(a.x, b.x), .y = max(a.y, b.y)}; +macro Point Point.min(Point a, Point b) => {.x = min(a.x, b.x), .y = min(a.y, b.y)}; // ---------------------------------------------------------------------------------- // // COLOR // @@ -252,8 +235,26 @@ macro Color uint.@to_rgba($u) }; } -macro uint Color.to_uint(c) -{ - uint u = c.r | (c.g << 8) | (c.b << 16) | (c.a << 24); - return u; +macro uint Color.to_uint(c) => c.r | (c.g << 8) | (c.b << 16) | (c.a << 24); + +// ---------------------------------------------------------------------------------- // +// SIZE // +// ---------------------------------------------------------------------------------- // + +struct Size { + short min, max; } + +macro Size @grow() => {.min = 0, .max = 0}; +macro Size @exact(short s) => {.min = s, .max = s}; +macro Size @fit(short min = 0, short max = short.max/2) => {.min = min, .max = max}; +macro bool Size.@is_grow(s) => (s.min == 0 && s.max == 0); +macro bool Size.@is_exact(s) => (s.min == s.max && s.min != 0); +macro bool Size.@is_fit(s) => (s.min != s.max); + +macro Size Size.add(a, Size b) @operator_s(+) => {.min = a.min+b.min, .max = a.max+b.max}; +macro Size Size.sub(a, Size b) @operator_s(-) => {.min = a.min-b.min, .max = a.max-b.max}; + +macro Size Size.combine(a, Size b) => {.min = max(a.min, b.min), .max = min(a.max, b.max)}; +macro Size Size.comb_max(a, Size b) => {.min = max(a.min, b.min), .max = max(a.max, b.max)}; +macro Size Size.comb_min(a, Size b) => {.min = min(a.min, b.min), .max = min(a.max, b.max)}; diff --git a/lib/ugui.c3l/src/ugui_slider.c3 b/lib/ugui.c3l/src/ugui_slider.c3 index bacd4b6..709f087 100644 --- a/lib/ugui.c3l/src/ugui_slider.c3 +++ b/lib/ugui.c3l/src/ugui_slider.c3 @@ -8,12 +8,12 @@ struct ElemSlider { Rect handle; } - /* handle * +----+-----+---------------------+ * | |#####| | * +----+-----+---------------------+ */ +/* macro Ctx.slider_hor(&ctx, Rect size, float* value, float hpercent = 0.25, ...) => ctx.slider_hor_id(@compute_id($vasplat), size, value, hpercent); <* @@ -63,6 +63,7 @@ fn ElemEvents? Ctx.slider_hor_id(&ctx, Id id, Rect size, float* value, float hpe return elem.events; } +*/ /* @@ -77,6 +78,7 @@ fn ElemEvents? Ctx.slider_hor_id(&ctx, Id id, Rect size, float* value, float hpe * | | * +--+ */ +/* macro Ctx.slider_ver(&ctx, Rect size, float* value, float hpercent = 0.25, ...) => ctx.slider_ver_id(@compute_id($vasplat), size, value, hpercent); fn ElemEvents? Ctx.slider_ver_id(&ctx, Id id, Rect size, float* value, float hpercent = 0.25) @@ -134,3 +136,4 @@ fn ElemEvents? Ctx.slider_ver_id(&ctx, Id id, Rect size, float* value, float hpe macro short calc_slider(short off, short dim, float value) => (short)off + (short)(dim * value); macro float calc_value(short off, short mouse, short dim, short slider) => math::clamp((float)(mouse-off-slider/2)/(float)(dim-slider), 0.0f, 1.0f); +*/ \ No newline at end of file diff --git a/lib/ugui.c3l/src/ugui_sprite.c3 b/lib/ugui.c3l/src/ugui_sprite.c3 index 152f109..edd3b8f 100644 --- a/lib/ugui.c3l/src/ugui_sprite.c3 +++ b/lib/ugui.c3l/src/ugui_sprite.c3 @@ -44,7 +44,7 @@ fn void? SpriteAtlas.init(&this, String name, AtlasType type, ushort width, usho this.id = name.hash(); this.atlas.new(this.id, AtlasType.ATLAS_R8G8B8A8, width, height)!; - this.sprites.init(allocator::heap(), capacity: SRITES_PER_ATLAS); + this.sprites.init(allocator::mem, capacity: SRITES_PER_ATLAS); this.should_update = false; } @@ -104,13 +104,14 @@ fn void? Ctx.import_sprite_file_qoi(&ctx, String name, String path, SpriteType t { QOIDesc desc; - char[] pixels = qoi::read(allocator::heap(), path, &desc, QOIChannels.RGBA)!; + char[] pixels = qoi::read(allocator::mem, path, &desc, QOIChannels.RGBA)!; defer mem::free(pixels); ctx.sprite_atlas.insert(name, type, pixels, (ushort)desc.width, (ushort)desc.height, (ushort)desc.width)!; } +/* macro Ctx.sprite(&ctx, String name, Point off = {0,0}, ...) => ctx.sprite_id(@compute_id($vasplat), name, off); fn void? Ctx.sprite_id(&ctx, Id id, String name, Point off) @@ -151,3 +152,4 @@ fn void? Ctx.draw_sprite_raw(&ctx, String name, Rect bounds, bool center = false return ctx.push_sprite(bounds, sprite.uv(), tex_id, parent.div.z_index, type: sprite.type)!; } +*/ \ No newline at end of file diff --git a/lib/ugui.c3l/src/ugui_text.c3 b/lib/ugui.c3l/src/ugui_text.c3 index f1513a9..dd19776 100644 --- a/lib/ugui.c3l/src/ugui_text.c3 +++ b/lib/ugui.c3l/src/ugui_text.c3 @@ -9,6 +9,7 @@ struct ElemText { Rect bounds; } +/* macro Ctx.text_unbounded(&ctx, String text, ...) => ctx.text_unbounded_id(@compute_id($vasplat), text); fn void? Ctx.text_unbounded_id(&ctx, Id id, String text) @@ -77,3 +78,4 @@ fn ElemEvents? Ctx.text_box_id(&ctx, Id id, Rect size, char[] text, usz* text_le return elem.events; } +*/ \ No newline at end of file diff --git a/resources/style.css b/resources/style.css index 33362ca..4681c69 100644 --- a/resources/style.css +++ b/resources/style.css @@ -4,7 +4,9 @@ default { primary: #cc241dff; secondary: #458588ff; accent: #fabd2fff; - border: 1; + border: 0; + padding: 0; + margin: 0; } button { diff --git a/src/main.c3 b/src/main.c3 index 7e6fed6..271f696 100644 --- a/src/main.c3 +++ b/src/main.c3 @@ -216,6 +216,7 @@ $endswitch TimeStats dts = draw_times.get_stats(); TimeStats uts = ui_times.get_stats(); +/* ui.layout_set_floating()!!; // FIXME: I cannot anchor shit to the bottom of the screen ui.@div({0, ui.height-150, -300, 150}) { @@ -224,13 +225,13 @@ $endswitch ui.text_unbounded(string::tformat("ui avg: %s\ndraw avg: %s\nTOT: %s", uts.avg, dts.avg, uts.avg+dts.avg))!!; ui.text_unbounded(string::tformat("%s %s", mod.lctrl, (String)ui.input.keyboard.text[:ui.input.keyboard.text_len]))!!; }!!; +*/ ui.frame_end()!!; /* End UI Handling */ ui_times.push(clock.mark()); //ui_times.print_stats(); - /* Start UI Drawing */ ren.begin_render(true); @@ -253,6 +254,7 @@ $endswitch return 0; } +/* fn void debug_app(ugui::Ctx* ui) { static bool toggle; @@ -324,6 +326,7 @@ fn void debug_app(ugui::Ctx* ui) }; ui.div_end()!!; } +*/ import std::os::process; @@ -361,34 +364,36 @@ fn void calculator(ugui::Ctx* ui) } // ui input/output - ui.@div(ugui::DIV_FILL) { - ui.layout_set_column()!!; + ui.@div(ugui::@grow(), ugui::@grow(), ROW, CENTER) { +/* ui.@div({0,0,250,50}) { ui.text_unbounded((String)buffer[:len])!!; }!!; +*/ - ui.@div({0,0,250,-100}) { - ui.layout_set_row()!!; - + ui.@div(ugui::@fit(), ugui::@fit(), COLUMN) { ui.button("7")!!.mouse_press ? buffer[len++] = '7' : 0; ui.button("8")!!.mouse_press ? buffer[len++] = '8' : 0; ui.button("9")!!.mouse_press ? buffer[len++] = '9' : 0; - ui.layout_next_row()!!; + }!!; + ui.@div(ugui::@fit(), ugui::@fit(), COLUMN) { ui.button("4")!!.mouse_press ? buffer[len++] = '4' : 0; ui.button("5")!!.mouse_press ? buffer[len++] = '5' : 0; ui.button("6")!!.mouse_press ? buffer[len++] = '6' : 0; - ui.layout_next_row()!!; + }!!; + ui.@div(ugui::@fit(), ugui::@fit(), COLUMN) { ui.button("3")!!.mouse_press ? buffer[len++] = '3' : 0; ui.button("2")!!.mouse_press ? buffer[len++] = '2' : 0; ui.button("1")!!.mouse_press ? buffer[len++] = '1' : 0; - ui.layout_next_row()!!; + }!!; + ui.@div(ugui::@fit(), ugui::@fit(), COLUMN) { ui.button("0")!!.mouse_press ? buffer[len++] = '0' : 0; ui.button(".")!!.mouse_press ? buffer[len++] = '.' : 0; ui.button("(")!!.mouse_press ? buffer[len++] = '(' : 0; - - ui.layout_next_column()!!; - + }!!; + + /* ui.@div({0,0,0,-1}) { ui.button("C")!!.mouse_press ? len = 0 : 0; ui.button("D")!!.mouse_press ? len-- : 0; @@ -409,7 +414,6 @@ fn void calculator(ugui::Ctx* ui) buffer[:x.len] = x[..]; len = x.len; } - }!!; - }!!; + */ }!!; } From 335624fcbe2da9c8afac1b30e2fc4e24fb628c10 Mon Sep 17 00:00:00 2001 From: Alessandro Mauri Date: Fri, 5 Sep 2025 18:53:47 +0200 Subject: [PATCH 09/13] in layout use a combined version of margin, border and padding --- lib/ugui.c3l/src/ugui_button.c3 | 9 ++-- lib/ugui.c3l/src/ugui_div.c3 | 6 +-- lib/ugui.c3l/src/ugui_layout.c3 | 84 +++++++++++---------------------- 3 files changed, 33 insertions(+), 66 deletions(-) diff --git a/lib/ugui.c3l/src/ugui_button.c3 b/lib/ugui.c3l/src/ugui_button.c3 index c3603fe..b4175e1 100644 --- a/lib/ugui.c3l/src/ugui_button.c3 +++ b/lib/ugui.c3l/src/ugui_button.c3 @@ -49,9 +49,7 @@ fn ElemEvents? Ctx.button_id(&ctx, Id id, String label, String icon) .max = (short)max(min_size, left_pad + icon_size.w + text_size.height.max + right_pad), }; // add style border and padding - elem.layout.margin = style.margin; - elem.layout.border = style.border; - elem.layout.padding = style.padding; + elem.layout.content_offset = style.margin.add(style.border).add(style.padding); elem.layout.children.w = elem.layout.w; elem.layout.children.h = elem.layout.h; @@ -60,7 +58,7 @@ fn ElemEvents? Ctx.button_id(&ctx, Id id, String label, String icon) update_parent_size(elem, parent); elem.events = ctx.get_elem_events(elem); - Rect content_bounds = elem.content_bounds(style); + Rect content_bounds = elem.content_bounds(); Rect text_bounds = { .x = content_bounds.x + icon_size.w + left_pad + inner_pad, @@ -85,7 +83,8 @@ fn ElemEvents? Ctx.button_id(&ctx, Id id, String label, String icon) s.bg = s.accent; } - ctx.push_rect(elem.bounds, parent.div.z_index, &s)!; + Rect border_bounds = elem.bounds.pad(style.margin); + ctx.push_rect(border_bounds, parent.div.z_index, &s)!; if (icon != "") { ctx.push_sprite(icon_bounds, sprite.uv(), ctx.sprite_atlas.id, parent.div.z_index, type: sprite.type)!; } diff --git a/lib/ugui.c3l/src/ugui_div.c3 b/lib/ugui.c3l/src/ugui_div.c3 index 4e738db..09c1eec 100644 --- a/lib/ugui.c3l/src/ugui_div.c3 +++ b/lib/ugui.c3l/src/ugui_div.c3 @@ -78,15 +78,13 @@ fn void? Ctx.div_begin_id(&ctx, .h = height, .dir = dir, .anchor = anchor, - .margin = style.margin, - .border = style.border, - .padding = style.padding, + .content_offset = style.margin.add(style.border).add(style.padding), }; // update parent grow children update_parent_grow(elem, parent); - ctx.push_rect(elem.bounds, elem.div.z_index, style)!; + ctx.push_rect(elem.bounds.pad(style.margin), elem.div.z_index, style)!; elem.events = ctx.get_elem_events(elem); diff --git a/lib/ugui.c3l/src/ugui_layout.c3 b/lib/ugui.c3l/src/ugui_layout.c3 index 5d5eae9..c950485 100644 --- a/lib/ugui.c3l/src/ugui_layout.c3 +++ b/lib/ugui.c3l/src/ugui_layout.c3 @@ -21,8 +21,8 @@ enum Anchor { } struct Layout { - Size w, h; - struct children { + Size w, h; // size of the CONTENT, does not include margin, border and padding + struct children { // the children size includes the children's margin/border/pading Size w, h; } ushort grow_children; @@ -32,24 +32,22 @@ struct Layout { } LayoutDirection dir; Anchor anchor; - // FIXME: all this cruft with margin border and padding could be resolved by simply providing - // a "Rect content_offset" that represents the combined effect of all three above - Rect margin, border, padding; + Rect content_offset; // combined effect of margin, border and padding } // Total width of a layout element, complete with margin, border and padding macro Size Layout.total_width(&el) { - Rect min = (Rect){.w = el.w.min, .h = el.h.min}.expand(el.margin).expand(el.border).expand(el.padding); - Rect max = (Rect){.w = el.w.max, .h = el.h.max}.expand(el.margin).expand(el.border).expand(el.padding); + Rect min = (Rect){.w = el.w.min, .h = el.h.min}.expand(el.content_offset); + Rect max = (Rect){.w = el.w.max, .h = el.h.max}.expand(el.content_offset); return {.min = min.w, .max = max.w}; } // Total height of a layout element, complete with margin, border and padding macro Size Layout.total_height(&el) { - Rect min = (Rect){.w = el.w.min, .h = el.h.min}.expand(el.margin).expand(el.border).expand(el.padding); - Rect max = (Rect){.w = el.w.max, .h = el.h.max}.expand(el.margin).expand(el.border).expand(el.padding); + Rect min = (Rect){.w = el.w.min, .h = el.h.min}.expand(el.content_offset); + Rect max = (Rect){.w = el.w.max, .h = el.h.max}.expand(el.content_offset); return {.min = min.h, .max = max.h}; } @@ -57,8 +55,8 @@ macro Size Layout.total_height(&el) macro Point Elem.content_space(&e) { return { - .x = e.bounds.w - e.layout.border.x - e.layout.border.w - e.layout.padding.x - e.layout.padding.w, - .y = e.bounds.h - e.layout.border.y - e.layout.border.h - e.layout.padding.y - e.layout.padding.h, + .x = e.bounds.w - e.layout.content_offset.x - e.layout.content_offset.w, + .y = e.bounds.h - e.layout.content_offset.y - e.layout.content_offset.h, }; } @@ -94,7 +92,7 @@ fn void update_children_bounds(Elem* child, Elem* parent) parent.children_bounds = containing_rect(child.bounds, parent.bounds); } -macro Rect Elem.content_bounds(&elem, style) => elem.bounds.pad(style.border).pad(style.padding); +macro Rect Elem.content_bounds(&elem) => elem.bounds.pad(elem.layout.content_offset); /* macro Rect Elem.get_view(&elem) @@ -120,16 +118,11 @@ macro Point Elem.get_view_off(&elem) // Assign the width and height of an element in the directions that it doesn't need to grow fn void resolve_dimensions(Elem* e, Elem* p) { - // The element's border and paddign total width and height - Point m_tot = {.x = e.layout.margin.x + e.layout.margin.w, .y = e.layout.margin.y + e.layout.margin.h}; - Point b_tot = {.x = e.layout.border.x + e.layout.border.w, .y = e.layout.border.y + e.layout.border.h}; - Point p_tot = {.x = e.layout.padding.x + e.layout.padding.w, .y = e.layout.padding.y + e.layout.padding.h}; - // ASSIGN WIDTH switch { case e.layout.w.@is_exact(): - // if width is exact then assign it to the bounds and add border and paddign - e.bounds.w = e.layout.w.min + b_tot.x + p_tot.x; + // if width is exact then assign it to the bounds + e.bounds.w = e.layout.total_width().min; case e.layout.w.@is_grow(): // done in another pass break; @@ -140,20 +133,10 @@ fn void resolve_dimensions(Elem* e, Elem* p) if (combined.min <= combined.max) { // OK! // the final element width is the content width plus padding and border - e.bounds.w = min(elem_width.max, children_width.max) + b_tot.x + p_tot.x; + e.bounds.w = min(elem_width.max, children_width.max) + e.layout.content_offset.x + e.layout.content_offset.w; } else { unreachable("Cannot fit children width: min:%d max:%d", combined.min, combined.max); } - /* - Size w = e.needed_width().combine(e.layout.children.w); - if (w.max >= w.min) { // OK! - e.bounds.w = w.min; - } else { - println(e.needed_width(), " ", e.needed_height()); - println(e.layout.children.w, " ", e.layout.children.h); - unreachable("Cannot fit children width: min:%d max:%d", w.min, w.max); - } - */ default: unreachable("width is not exact, grow or fit"); } @@ -161,7 +144,7 @@ fn void resolve_dimensions(Elem* e, Elem* p) switch { case e.layout.h.@is_exact(): // if width is exact then assign it to the bounds and add border and paddign - e.bounds.h = e.layout.h.min + b_tot.y + p_tot.y; + e.bounds.h = e.layout.total_height().min; case e.layout.h.@is_grow(): break; // done in another pass @@ -172,47 +155,34 @@ fn void resolve_dimensions(Elem* e, Elem* p) if (combined.min <= combined.max) { // OK! // the final element width is the content width plus padding and border - e.bounds.h = min(elem_height.max, children_height.max) + b_tot.y + p_tot.y; + e.bounds.h = min(elem_height.max, children_height.max) + e.layout.content_offset.y + e.layout.content_offset.h; } else { unreachable("Cannot fit children height: min:%d max:%d", combined.min, combined.max); } - /* - Size h = e.needed_height().combine(e.layout.children.h); - if (h.max >= h.min) { // OK! - e.bounds.h = h.max; - } else { - unreachable("Cannot fit children height: min:%d max:%d", h.min, h.max); - } - */ default: unreachable("width is not exact, grow or fit"); } - // the occupied space must account for the element's margin as well switch (p.layout.dir) { case ROW: - if (!e.layout.w.@is_grow()) p.layout.occupied += e.bounds.w + m_tot.x; + if (!e.layout.w.@is_grow()) p.layout.occupied += e.bounds.w; case COLUMN: - if (!e.layout.h.@is_grow()) p.layout.occupied += e.bounds.h + m_tot.y; + if (!e.layout.h.@is_grow()) p.layout.occupied += e.bounds.h; } } fn void resolve_grow_elements(Elem* e, Elem* p) { - Point m_tot = {.x = e.layout.margin.x + e.layout.margin.w, .y = e.layout.margin.y + e.layout.margin.h}; - Point b_tot = {.x = e.layout.border.x + e.layout.border.w, .y = e.layout.border.y + e.layout.border.h}; - Point p_tot = {.x = e.layout.padding.x + e.layout.padding.w, .y = e.layout.padding.y + e.layout.padding.h}; - // WIDTH if (e.layout.w.@is_grow()) { if (p.layout.dir == ROW) { // grow along the axis, divide the parent size short slot = (short)((p.content_space().x - p.layout.occupied) / p.layout.grow_children); // the space slot accounts for the total size, while the element bounds does not account // for the margin - e.bounds.w = slot - m_tot.x; + e.bounds.w = slot; p.layout.grow_children--; p.layout.occupied += slot; } else if (p.layout.dir == COLUMN) { // grow across the layout axis, inherit width of the parent - e.bounds.w = p.content_space().x - m_tot.x; + e.bounds.w = p.content_space().x; } } @@ -220,11 +190,11 @@ fn void resolve_grow_elements(Elem* e, Elem* p) if (e.layout.h.@is_grow()) { if (p.layout.dir == COLUMN) { // grow along the axis, divide the parent size short slot = (short)((p.content_space().y - p.layout.occupied) / p.layout.grow_children); - e.bounds.h = slot - m_tot.y; + e.bounds.h = slot; p.layout.grow_children--; p.layout.occupied += slot; } else if (p.layout.dir == ROW) { // grow across the layout axis, inherit width of the parent - e.bounds.h = p.content_space().y - m_tot.y; + e.bounds.h = p.content_space().y; } } } @@ -235,8 +205,8 @@ fn void resolve_placement(Elem* c, Elem* p) Layout* cl = &c.layout; Point off = { - .x = p.bounds.x + pl.origin.x + cl.margin.x, - .y = p.bounds.y + pl.origin.y + cl.margin.x, + .x = p.bounds.x + pl.origin.x, + .y = p.bounds.y + pl.origin.y, }; switch (pl.anchor) { @@ -320,9 +290,9 @@ fn void resolve_placement(Elem* c, Elem* p) switch (pl.dir) { case ROW: - pl.origin.x += c.bounds.w + cl.margin.x + cl.margin.w; + pl.origin.x += c.bounds.w; case COLUMN: - pl.origin.y += c.bounds.h + cl.margin.x + cl.margin.w; + pl.origin.y += c.bounds.h; default: unreachable("unknown layout direction"); } @@ -336,8 +306,8 @@ fn void Ctx.layout_element_tree(&ctx) for (; current >= 0; current = ctx.tree.level_order_it(0, &cursor)!!) { Elem* p = ctx.find_elem(ctx.tree.get(current))!!; // offset the origin by the margin - p.layout.origin.x = p.layout.border.x + p.layout.padding.x; - p.layout.origin.y = p.layout.border.y + p.layout.padding.y; + p.layout.origin.x = p.layout.content_offset.x; + p.layout.origin.y = p.layout.content_offset.y; // RESOLVE KNOWN DIMENSIONS isz ch_cur = 0; From 3d7be2a2df0d003735cde849e84d644fef4be30e Mon Sep 17 00:00:00 2001 From: Alessandro Mauri Date: Fri, 5 Sep 2025 19:56:59 +0200 Subject: [PATCH 10/13] implement operator overloading for rects --- lib/ugui.c3l/src/ugui_button.c3 | 6 +++--- lib/ugui.c3l/src/ugui_div.c3 | 2 +- lib/ugui.c3l/src/ugui_shapes.c3 | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/ugui.c3l/src/ugui_button.c3 b/lib/ugui.c3l/src/ugui_button.c3 index b4175e1..e255476 100644 --- a/lib/ugui.c3l/src/ugui_button.c3 +++ b/lib/ugui.c3l/src/ugui_button.c3 @@ -49,7 +49,7 @@ fn ElemEvents? Ctx.button_id(&ctx, Id id, String label, String icon) .max = (short)max(min_size, left_pad + icon_size.w + text_size.height.max + right_pad), }; // add style border and padding - elem.layout.content_offset = style.margin.add(style.border).add(style.padding); + elem.layout.content_offset = style.margin + style.border + style.padding; elem.layout.children.w = elem.layout.w; elem.layout.children.h = elem.layout.h; @@ -83,12 +83,12 @@ fn ElemEvents? Ctx.button_id(&ctx, Id id, String label, String icon) s.bg = s.accent; } - Rect border_bounds = elem.bounds.pad(style.margin); - ctx.push_rect(border_bounds, parent.div.z_index, &s)!; + ctx.push_rect(elem.bounds.pad(style.margin), parent.div.z_index, &s)!; if (icon != "") { ctx.push_sprite(icon_bounds, sprite.uv(), ctx.sprite_atlas.id, parent.div.z_index, type: sprite.type)!; } if (label != "") { + ctx.push_rect(text_bounds, parent.div.z_index, &&{.bg=0xff0000abu.@to_rgba()})!; ctx.push_string(text_bounds, label, parent.div.z_index, style.fg)!; } return elem.events; diff --git a/lib/ugui.c3l/src/ugui_div.c3 b/lib/ugui.c3l/src/ugui_div.c3 index 09c1eec..a53cd0c 100644 --- a/lib/ugui.c3l/src/ugui_div.c3 +++ b/lib/ugui.c3l/src/ugui_div.c3 @@ -78,7 +78,7 @@ fn void? Ctx.div_begin_id(&ctx, .h = height, .dir = dir, .anchor = anchor, - .content_offset = style.margin.add(style.border).add(style.padding), + .content_offset = style.margin + style.border + style.padding, }; // update parent grow children diff --git a/lib/ugui.c3l/src/ugui_shapes.c3 b/lib/ugui.c3l/src/ugui_shapes.c3 index b134203..e3a6e81 100644 --- a/lib/ugui.c3l/src/ugui_shapes.c3 +++ b/lib/ugui.c3l/src/ugui_shapes.c3 @@ -55,7 +55,7 @@ macro Rect containing_rect(Rect a, Rect b) macro bool Rect.is_null(Rect r) => r.x == 0 && r.y == 0 && r.x == 0 && r.w == 0; // returns the element-wise addition of r1 and r2 -macro Rect Rect.add(Rect r1, Rect r2) +macro Rect Rect.add(Rect r1, Rect r2) @operator_s(+) { return { .x = r1.x + r2.x, @@ -66,7 +66,7 @@ macro Rect Rect.add(Rect r1, Rect r2) } // returns the element-wise subtraction of r1 and r2 -macro Rect Rect.sub(Rect r1, Rect r2) +macro Rect Rect.sub(Rect r1, Rect r2) @operator_s(-) { return { .x = r1.x - r2.x, @@ -77,7 +77,7 @@ macro Rect Rect.sub(Rect r1, Rect r2) } // returns the element-wise multiplication of r1 and r2 -macro Rect Rect.mul(Rect r1, Rect r2) +macro Rect Rect.mul(Rect r1, Rect r2) @operator_s(*) { return { .x = r1.x * r2.x, From 869c7871f9a444c44ce0c10d497a04778084d13b Mon Sep 17 00:00:00 2001 From: Alessandro Mauri Date: Sat, 6 Sep 2025 12:50:36 +0200 Subject: [PATCH 11/13] minor changes --- lib/ugui.c3l/LAYOUT | 54 ++++++++++++++------------- lib/ugui.c3l/src/ugui_font.c3 | 4 -- lib/ugui.c3l/src/ugui_layout.c3 | 39 ++++++++++---------- src/main.c3 | 65 ++++++++++++++++----------------- 4 files changed, 80 insertions(+), 82 deletions(-) diff --git a/lib/ugui.c3l/LAYOUT b/lib/ugui.c3l/LAYOUT index 717ac05..700b43d 100644 --- a/lib/ugui.c3l/LAYOUT +++ b/lib/ugui.c3l/LAYOUT @@ -85,17 +85,17 @@ layout: the layout direction of the children COLUMN ROW +--------------------+ +----------------------------------------------------+ - | +----------------+ | |+----------------+ +------------------+| - | | | | || |+------------+| || - | | | | || || || E3 || - | | E1 | | || E1 || E2 || || - | | | | || || || || - | | | | || |+------------++------------------+| + | +----------------+ | |+----------------+ | + | | | | || |+------------+ | + | | | | || || |+------------------+| + | | E1 | | || E1 || E2 || E3 || + | | | | || || |+------------------+| + | | | | || |+------------+ | | +----------------+ | |+----------------+ | | +------------+ | +----------------------------------------------------+ | | | | | | E2 | | - | | | | + | | | | (both have center alignment) | +------------+ | |+------------------+| || || @@ -218,22 +218,26 @@ at frame end: Styling ======= -+-----------------------------------------+ -| MARGIN | -| | -| +-----------------------------------+ | -| |xxxxxxxxxxx BORDER xxxxxxxxxxxx| | -| |x+-------------------------------+x| | -| |x| PADDING |x| | -| |x| +-----------------------+ |x| | -| |x| | | |x| | -| |x| | CONTENT | |x| | -| |x| +-----------------------+ |x| | -| |x| |x| | -| |x+-------------------------------+x| | -| |xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx| | -| +-----------------------------------+ | -| | -| | -+-----------------------------------------+ +The element bounds include the whole CSS box model: ++---------------------------------------------+ +| MARGIN | +| +-----------------------------------+ | +| |xxxxxxxxxxx BORDER xxxxxxxxxxxx| | +| |x+-------------------------------+x| | +| |x| PADDING |x| | +| |x| +-----------------------+ |x| | +| |x| | | |x| | +| |x| | CONTENT | |x| | +| |x| | | |x| | +| |x| +-----------------------+ |x| | +| |x| |x| | +| |x+-------------------------------+x| | +| |xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx| | +| +-----------------------------------+ | +| | ++---------------------------------------------+ + +Styling happens via a .css file, the sizing strictly refers to the content, so if the the user +requests an exact size of 100px*100px the content box will have those dimensions, but the element +bounds will be larger. diff --git a/lib/ugui.c3l/src/ugui_font.c3 b/lib/ugui.c3l/src/ugui_font.c3 index 51ff0b9..ed9c112 100644 --- a/lib/ugui.c3l/src/ugui_font.c3 +++ b/lib/ugui.c3l/src/ugui_font.c3 @@ -404,9 +404,5 @@ fn TextSize? Ctx.measure_string(&ctx, String text) ts.height.min = bounds.h; ts.height.max = words * line_height + line_gap * (words-1); - //io::print("'"); - //io::print(text); - //io::print("': "); - //io::printn(ts); return ts; } diff --git a/lib/ugui.c3l/src/ugui_layout.c3 b/lib/ugui.c3l/src/ugui_layout.c3 index c950485..b6854b7 100644 --- a/lib/ugui.c3l/src/ugui_layout.c3 +++ b/lib/ugui.c3l/src/ugui_layout.c3 @@ -118,22 +118,25 @@ macro Point Elem.get_view_off(&elem) // Assign the width and height of an element in the directions that it doesn't need to grow fn void resolve_dimensions(Elem* e, Elem* p) { + Layout* el = &e.layout; + Layout* pl = &p.layout; + // ASSIGN WIDTH switch { - case e.layout.w.@is_exact(): + case el.w.@is_exact(): // if width is exact then assign it to the bounds - e.bounds.w = e.layout.total_width().min; - case e.layout.w.@is_grow(): + e.bounds.w = el.total_width().min; + case el.w.@is_grow(): // done in another pass break; - case e.layout.w.@is_fit(): // fit the element's children - Size elem_width = e.layout.w; // the element's content width - Size children_width = e.layout.children.w; // element children (content) width + case el.w.@is_fit(): // fit the element's children + Size elem_width = el.w; // the element's content width + Size children_width = el.children.w; // element children (content) width Size combined = elem_width.combine(children_width); if (combined.min <= combined.max) { // OK! // the final element width is the content width plus padding and border - e.bounds.w = min(elem_width.max, children_width.max) + e.layout.content_offset.x + e.layout.content_offset.w; + e.bounds.w = min(elem_width.max, children_width.max) + el.content_offset.x + el.content_offset.w; } else { unreachable("Cannot fit children width: min:%d max:%d", combined.min, combined.max); } @@ -142,31 +145,31 @@ fn void resolve_dimensions(Elem* e, Elem* p) // ASSIGN HEIGHT switch { - case e.layout.h.@is_exact(): + case el.h.@is_exact(): // if width is exact then assign it to the bounds and add border and paddign - e.bounds.h = e.layout.total_height().min; - case e.layout.h.@is_grow(): + e.bounds.h = el.total_height().min; + case el.h.@is_grow(): break; // done in another pass - case e.layout.h.@is_fit(): // fit the element's children - Size elem_height = e.layout.h; // the element's content width - Size children_height = e.layout.children.h; // element children (content) width + case el.h.@is_fit(): // fit the element's children + Size elem_height = el.h; // the element's content width + Size children_height = el.children.h; // element children (content) width Size combined = elem_height.combine(children_height); if (combined.min <= combined.max) { // OK! // the final element width is the content width plus padding and border - e.bounds.h = min(elem_height.max, children_height.max) + e.layout.content_offset.y + e.layout.content_offset.h; + e.bounds.h = min(elem_height.max, children_height.max) + el.content_offset.y + el.content_offset.h; } else { unreachable("Cannot fit children height: min:%d max:%d", combined.min, combined.max); } default: unreachable("width is not exact, grow or fit"); } - switch (p.layout.dir) { + switch (pl.dir) { case ROW: - if (!e.layout.w.@is_grow()) p.layout.occupied += e.bounds.w; + if (!el.w.@is_grow()) pl.occupied += e.bounds.w; case COLUMN: - if (!e.layout.h.@is_grow()) p.layout.occupied += e.bounds.h; + if (!el.h.@is_grow()) pl.occupied += e.bounds.h; } } @@ -295,8 +298,6 @@ fn void resolve_placement(Elem* c, Elem* p) pl.origin.y += c.bounds.h; default: unreachable("unknown layout direction"); } - - //sprintln("origin: ", p.layout.origin, " bounds: ", c.bounds); } fn void Ctx.layout_element_tree(&ctx) diff --git a/src/main.c3 b/src/main.c3 index 271f696..83848bb 100644 --- a/src/main.c3 +++ b/src/main.c3 @@ -218,7 +218,6 @@ $endswitch /* ui.layout_set_floating()!!; - // FIXME: I cannot anchor shit to the bottom of the screen ui.@div({0, ui.height-150, -300, 150}) { ui.layout_set_column()!!; ui.text_unbounded(string::tformat("frame %d, fps = %.2f", frame, fps))!!; @@ -374,46 +373,44 @@ fn void calculator(ugui::Ctx* ui) ui.@div(ugui::@fit(), ugui::@fit(), COLUMN) { ui.button("7")!!.mouse_press ? buffer[len++] = '7' : 0; - ui.button("8")!!.mouse_press ? buffer[len++] = '8' : 0; - ui.button("9")!!.mouse_press ? buffer[len++] = '9' : 0; - }!!; - ui.@div(ugui::@fit(), ugui::@fit(), COLUMN) { ui.button("4")!!.mouse_press ? buffer[len++] = '4' : 0; - ui.button("5")!!.mouse_press ? buffer[len++] = '5' : 0; - ui.button("6")!!.mouse_press ? buffer[len++] = '6' : 0; - }!!; - ui.@div(ugui::@fit(), ugui::@fit(), COLUMN) { - ui.button("3")!!.mouse_press ? buffer[len++] = '3' : 0; - ui.button("2")!!.mouse_press ? buffer[len++] = '2' : 0; ui.button("1")!!.mouse_press ? buffer[len++] = '1' : 0; + ui.button("0")!!.mouse_press ? buffer[len++] = '0' : 0; }!!; ui.@div(ugui::@fit(), ugui::@fit(), COLUMN) { - ui.button("0")!!.mouse_press ? buffer[len++] = '0' : 0; + ui.button("8")!!.mouse_press ? buffer[len++] = '8' : 0; + ui.button("5")!!.mouse_press ? buffer[len++] = '5' : 0; + ui.button("2")!!.mouse_press ? buffer[len++] = '2' : 0; ui.button(".")!!.mouse_press ? buffer[len++] = '.' : 0; + }!!; + ui.@div(ugui::@fit(), ugui::@fit(), COLUMN) { + ui.button("9")!!.mouse_press ? buffer[len++] = '9' : 0; + ui.button("6")!!.mouse_press ? buffer[len++] = '6' : 0; + ui.button("3")!!.mouse_press ? buffer[len++] = '3' : 0; ui.button("(")!!.mouse_press ? buffer[len++] = '(' : 0; }!!; + + ui.@div(ugui::@exact(10), ugui::@exact(10)) {}!!; - /* - ui.@div({0,0,0,-1}) { - ui.button("C")!!.mouse_press ? len = 0 : 0; - ui.button("D")!!.mouse_press ? len-- : 0; - ui.layout_next_row()!!; - ui.button("x")!!.mouse_press ? buffer[len++] = '*' : 0; - ui.button("/")!!.mouse_press ? buffer[len++] = '/' : 0; - ui.layout_next_row()!!; - ui.button("+")!!.mouse_press ? buffer[len++] = '+' : 0; - ui.button("-")!!.mouse_press ? buffer[len++] = '-' : 0; - ui.layout_next_row()!!; - ui.button(")")!!.mouse_press ? buffer[len++] = ')' : 0; - - // eval the expression with 'bc' - if (ui.button("=")!!.mouse_press || eval) { - char[128] out; - String y = string::tformat("echo '%s' | bc", (String)buffer[:len]); - String x = process::execute_stdout_to_buffer(out[:128], (String[]){"sh", "-c", y}) ?? ""; - buffer[:x.len] = x[..]; - len = x.len; - } - */ + ui.@div(ugui::@fit(), ugui::@fit(), COLUMN) { + ui.button("x")!!.mouse_press ? buffer[len++] = '*' : 0; + ui.button("/")!!.mouse_press ? buffer[len++] = '/' : 0; + ui.button("+")!!.mouse_press ? buffer[len++] = '+' : 0; + ui.button(")")!!.mouse_press ? buffer[len++] = ')' : 0; + }!!; + ui.@div(ugui::@fit(), ugui::@fit(), COLUMN) { + ui.button("C")!!.mouse_press ? len = 0 : 0; + ui.button("D")!!.mouse_press ? len-- : 0; + ui.button("-")!!.mouse_press ? buffer[len++] = '-' : 0; + + // eval the expression with 'bc' + if (ui.button("=")!!.mouse_press || eval) { + char[128] out; + String y = string::tformat("echo '%s' | bc", (String)buffer[:len]); + String x = process::execute_stdout_to_buffer(out[:128], (String[]){"sh", "-c", y}) ?? ""; + buffer[:x.len] = x[..]; + len = x.len; + } + }!!; }!!; } From db63b2c6b19c2d28addb44d52bfe131b236953f7 Mon Sep 17 00:00:00 2001 From: Alessandro Mauri Date: Sat, 6 Sep 2025 23:07:43 +0200 Subject: [PATCH 12/13] layout actually works now --- lib/ugui.c3l/src/ugui_button.c3 | 61 ++++++++++------------ lib/ugui.c3l/src/ugui_font.c3 | 10 +++- lib/ugui.c3l/src/ugui_layout.c3 | 62 +++++++++++++--------- lib/ugui.c3l/src/ugui_shapes.c3 | 5 +- resources/style.css | 4 +- src/main.c3 | 92 ++++++++++++++++----------------- 6 files changed, 125 insertions(+), 109 deletions(-) diff --git a/lib/ugui.c3l/src/ugui_button.c3 b/lib/ugui.c3l/src/ugui_button.c3 index e255476..d963713 100644 --- a/lib/ugui.c3l/src/ugui_button.c3 +++ b/lib/ugui.c3l/src/ugui_button.c3 @@ -25,57 +25,51 @@ fn ElemEvents? Ctx.button_id(&ctx, Id id, String label, String icon) ushort min_size = style.size; ushort half_lh = (ushort)(ctx.font.line_height() / 2); - ushort left_pad = label != "" ? half_lh : 0; ushort inner_pad = label != "" && icon != "" ? half_lh : 0; - ushort right_pad = left_pad; - - /* |left_pad - * +--v-----------------------------------+ - * |<->+--------+ | + /* + * +--------------------------------------+ + * | +--------+ | * | | | +-----------------+ | * | | icon | | label | | * | | | +-----------------+ | - * | +--------+<->| |<->| - * +-------------^--------------------^---+ - * |inner_pad |right_pad + * | +--------+<->| | + * +-------------^------------------------+ + * |inner_pad */ - - elem.layout.w = { - .min = (short)max(min_size, left_pad + icon_size.w + inner_pad + text_size.width.min + right_pad), - .max = (short)max(min_size, left_pad + icon_size.w + inner_pad + text_size.width.max + right_pad), + Point content_size = { + .x = icon_size.w + inner_pad, // text sizing is handled differently + .y = icon_size.h + inner_pad, }; - elem.layout.h = { - .min = (short)max(min_size, left_pad + icon_size.h + text_size.height.min + right_pad), - .max = (short)max(min_size, left_pad + icon_size.w + text_size.height.max + right_pad), - }; - // add style border and padding + //elem.layout.w = @fit(min_size); + elem.layout.w = @fit(min_size); + elem.layout.h = @fit(min_size); + elem.layout.children.w = @exact(content_size.x); + elem.layout.children.h = @exact(content_size.y); + elem.layout.text = text_size; elem.layout.content_offset = style.margin + style.border + style.padding; - elem.layout.children.w = elem.layout.w; - elem.layout.children.h = elem.layout.h; - update_parent_grow(elem, parent); update_parent_size(elem, parent); elem.events = ctx.get_elem_events(elem); Rect content_bounds = elem.content_bounds(); - - Rect text_bounds = { - .x = content_bounds.x + icon_size.w + left_pad + inner_pad, - .y = content_bounds.y, - .w = content_bounds.w - icon_size.w - left_pad - inner_pad - right_pad, - .h = content_bounds.h - }; - //text_bounds = text_size.center_to(text_bounds); - + Rect icon_bounds = { .x = content_bounds.x, .y = content_bounds.y, - .w = icon_size.w + right_pad + inner_pad, - .h = (short)max(icon_size.h, content_bounds.h) + .w = icon_size.w, + .h = icon_size.h }; icon_bounds = icon_size.center_to(icon_bounds); + Rect text_bounds = { + .x = content_bounds.x + icon_bounds.w + inner_pad, + .y = content_bounds.y, + .w = content_bounds.w - icon_bounds.w - inner_pad, + .h = content_bounds.h, + }; + //text_bounds = text_size.center_to(text_bounds); + bool is_active = ctx.elem_focus(elem) || elem.events.mouse_hover; Style s = *style; if (is_active) { @@ -88,8 +82,7 @@ fn ElemEvents? Ctx.button_id(&ctx, Id id, String label, String icon) ctx.push_sprite(icon_bounds, sprite.uv(), ctx.sprite_atlas.id, parent.div.z_index, type: sprite.type)!; } if (label != "") { - ctx.push_rect(text_bounds, parent.div.z_index, &&{.bg=0xff0000abu.@to_rgba()})!; - ctx.push_string(text_bounds, label, parent.div.z_index, style.fg)!; + ctx.push_string(text_bounds, label, parent.div.z_index, style.fg, true)!; } return elem.events; } diff --git a/lib/ugui.c3l/src/ugui_font.c3 b/lib/ugui.c3l/src/ugui_font.c3 index ed9c112..12fca7c 100644 --- a/lib/ugui.c3l/src/ugui_font.c3 +++ b/lib/ugui.c3l/src/ugui_font.c3 @@ -335,6 +335,7 @@ fn Rect? Ctx.get_text_bounds(&ctx, String text) struct TextSize { Size width, height; + int area; } // Measeure the size of a string. @@ -393,8 +394,12 @@ fn TextSize? Ctx.measure_string(&ctx, String text) word_width = 0; words++; default: - word_width += gp.w + gp.ox; - if (off < text.len) word_width += gp.adv; + //word_width += gp.w + gp.ox; + if (off < text.len) { + word_width += gp.adv; + } else { + word_width += gp.w + gp.ox; + } } } // end of string is also end of word @@ -403,6 +408,7 @@ fn TextSize? Ctx.measure_string(&ctx, String text) ts.width.max = bounds.w; ts.height.min = bounds.h; ts.height.max = words * line_height + line_gap * (words-1); + ts.area = bounds.w * bounds.h; return ts; } diff --git a/lib/ugui.c3l/src/ugui_layout.c3 b/lib/ugui.c3l/src/ugui_layout.c3 index b6854b7..bae4ea7 100644 --- a/lib/ugui.c3l/src/ugui_layout.c3 +++ b/lib/ugui.c3l/src/ugui_layout.c3 @@ -25,6 +25,7 @@ struct Layout { struct children { // the children size includes the children's margin/border/pading Size w, h; } + TextSize text; ushort grow_children; short occupied; struct origin { @@ -51,6 +52,32 @@ macro Size Layout.total_height(&el) return {.min = min.h, .max = max.h}; } +// Returns the width and height of a @FIT() element based on it's wanted size (min/max) +// and the content size, this function is used to both update the parent's children size and +// give the dimensions of a fit element +macro Point Layout.get_dimensions(&el) +{ + Size content_width = el.children.w + el.text.width; + Size width = el.w.combine(content_width); + short final_width = width.greater(); + + short text_height; + if (el.text.area != 0) { + short text_width = (@exact(final_width) - el.children.w).combine(el.text.width).min; + text_height = @exact((short)(el.text.area / text_width)).combine(el.text.height).min; + } + + Size content_height = el.children.h + @exact(text_height); + Size height = el.h.combine(content_height); + short final_height = height.greater(); + + Point dim = { + .x = final_width + el.content_offset.x + el.content_offset.w, + .y = final_height + el.content_offset.y + el.content_offset.h, + }; + return dim; +} + // The content space of the element macro Point Elem.content_space(&e) { @@ -76,14 +103,16 @@ fn void update_parent_size(Elem* child, Elem* parent) { Layout* cl = &child.layout; Layout* pl = &parent.layout; + + Point child_size = cl.get_dimensions(); switch (pl.dir) { case ROW: // on rows grow the ch width by the child width and only grow ch height if it exceeds - pl.children.w += cl.total_width(); - pl.children.h = pl.children.h.comb_max(cl.total_height()); + pl.children.w += @exact(child_size.x); + pl.children.h = pl.children.h.comb_max(@exact(child_size.y)); case COLUMN: // do the opposite on column - pl.children.w = pl.children.w.comb_max(cl.total_width()); - pl.children.h += cl.total_height(); + pl.children.w = pl.children.w.comb_max(@exact(child_size.x)); + pl.children.h += @exact(child_size.y); } } @@ -120,7 +149,10 @@ fn void resolve_dimensions(Elem* e, Elem* p) { Layout* el = &e.layout; Layout* pl = &p.layout; + + Point elem_dimensions = el.get_dimensions(); + short text_min_height; // ASSIGN WIDTH switch { case el.w.@is_exact(): @@ -130,16 +162,7 @@ fn void resolve_dimensions(Elem* e, Elem* p) // done in another pass break; case el.w.@is_fit(): // fit the element's children - Size elem_width = el.w; // the element's content width - Size children_width = el.children.w; // element children (content) width - Size combined = elem_width.combine(children_width); - - if (combined.min <= combined.max) { // OK! - // the final element width is the content width plus padding and border - e.bounds.w = min(elem_width.max, children_width.max) + el.content_offset.x + el.content_offset.w; - } else { - unreachable("Cannot fit children width: min:%d max:%d", combined.min, combined.max); - } + e.bounds.w = elem_dimensions.x; default: unreachable("width is not exact, grow or fit"); } @@ -152,16 +175,7 @@ fn void resolve_dimensions(Elem* e, Elem* p) break; // done in another pass case el.h.@is_fit(): // fit the element's children - Size elem_height = el.h; // the element's content width - Size children_height = el.children.h; // element children (content) width - Size combined = elem_height.combine(children_height); - - if (combined.min <= combined.max) { // OK! - // the final element width is the content width plus padding and border - e.bounds.h = min(elem_height.max, children_height.max) + el.content_offset.y + el.content_offset.h; - } else { - unreachable("Cannot fit children height: min:%d max:%d", combined.min, combined.max); - } + e.bounds.h = elem_dimensions.y; default: unreachable("width is not exact, grow or fit"); } diff --git a/lib/ugui.c3l/src/ugui_shapes.c3 b/lib/ugui.c3l/src/ugui_shapes.c3 index e3a6e81..53a7635 100644 --- a/lib/ugui.c3l/src/ugui_shapes.c3 +++ b/lib/ugui.c3l/src/ugui_shapes.c3 @@ -247,7 +247,8 @@ struct Size { macro Size @grow() => {.min = 0, .max = 0}; macro Size @exact(short s) => {.min = s, .max = s}; -macro Size @fit(short min = 0, short max = short.max/2) => {.min = min, .max = max}; + // PROBLEM WE ARE OVERFLOWING +macro Size @fit(short min = 0, short max = 999) => {.min = min, .max = max}; macro bool Size.@is_grow(s) => (s.min == 0 && s.max == 0); macro bool Size.@is_exact(s) => (s.min == s.max && s.min != 0); macro bool Size.@is_fit(s) => (s.min != s.max); @@ -258,3 +259,5 @@ macro Size Size.sub(a, Size b) @operator_s(-) => {.min = a.min-b.min, .max = a.m macro Size Size.combine(a, Size b) => {.min = max(a.min, b.min), .max = min(a.max, b.max)}; macro Size Size.comb_max(a, Size b) => {.min = max(a.min, b.min), .max = max(a.max, b.max)}; macro Size Size.comb_min(a, Size b) => {.min = min(a.min, b.min), .max = min(a.max, b.max)}; + +macro short Size.greater(a) => a.min > a.max ? a.min : a.max; diff --git a/resources/style.css b/resources/style.css index 4681c69..4564354 100644 --- a/resources/style.css +++ b/resources/style.css @@ -2,9 +2,9 @@ default { bg: #282828ff; fg: #fbf1c7ff; primary: #cc241dff; - secondary: #458588ff; + secondary: #6c19ca8f; accent: #fabd2fff; - border: 0; + border: 1; padding: 0; margin: 0; } diff --git a/src/main.c3 b/src/main.c3 index 83848bb..53fb2a6 100644 --- a/src/main.c3 +++ b/src/main.c3 @@ -363,54 +363,54 @@ fn void calculator(ugui::Ctx* ui) } // ui input/output - ui.@div(ugui::@grow(), ugui::@grow(), ROW, CENTER) { + ui.@div(ugui::@grow(), ugui::@grow(), ROW, CENTER) { // center everything on the screen + ui.@div(ugui::@fit(), ugui::@fit(), COLUMN, TOP_LEFT) { -/* - ui.@div({0,0,250,50}) { - ui.text_unbounded((String)buffer[:len])!!; + ui.@div(ugui::@grow(), ugui::@exact(100), ROW, RIGHT) { + ui.button("TODO")!!; }!!; -*/ - - ui.@div(ugui::@fit(), ugui::@fit(), COLUMN) { - ui.button("7")!!.mouse_press ? buffer[len++] = '7' : 0; - ui.button("4")!!.mouse_press ? buffer[len++] = '4' : 0; - ui.button("1")!!.mouse_press ? buffer[len++] = '1' : 0; - ui.button("0")!!.mouse_press ? buffer[len++] = '0' : 0; - }!!; - ui.@div(ugui::@fit(), ugui::@fit(), COLUMN) { - ui.button("8")!!.mouse_press ? buffer[len++] = '8' : 0; - ui.button("5")!!.mouse_press ? buffer[len++] = '5' : 0; - ui.button("2")!!.mouse_press ? buffer[len++] = '2' : 0; - ui.button(".")!!.mouse_press ? buffer[len++] = '.' : 0; - }!!; - ui.@div(ugui::@fit(), ugui::@fit(), COLUMN) { - ui.button("9")!!.mouse_press ? buffer[len++] = '9' : 0; - ui.button("6")!!.mouse_press ? buffer[len++] = '6' : 0; - ui.button("3")!!.mouse_press ? buffer[len++] = '3' : 0; - ui.button("(")!!.mouse_press ? buffer[len++] = '(' : 0; - }!!; - - ui.@div(ugui::@exact(10), ugui::@exact(10)) {}!!; - - ui.@div(ugui::@fit(), ugui::@fit(), COLUMN) { - ui.button("x")!!.mouse_press ? buffer[len++] = '*' : 0; - ui.button("/")!!.mouse_press ? buffer[len++] = '/' : 0; - ui.button("+")!!.mouse_press ? buffer[len++] = '+' : 0; - ui.button(")")!!.mouse_press ? buffer[len++] = ')' : 0; - }!!; - ui.@div(ugui::@fit(), ugui::@fit(), COLUMN) { - ui.button("C")!!.mouse_press ? len = 0 : 0; - ui.button("D")!!.mouse_press ? len-- : 0; - ui.button("-")!!.mouse_press ? buffer[len++] = '-' : 0; + ui.@div(ugui::@fit(), ugui::@fit(), ROW, TOP_LEFT) { + ui.@div(ugui::@fit(), ugui::@fit(), COLUMN) { + ui.button("7")!!.mouse_press ? buffer[len++] = '7' : 0; + ui.button("4")!!.mouse_press ? buffer[len++] = '4' : 0; + ui.button("1")!!.mouse_press ? buffer[len++] = '1' : 0; + ui.button("0")!!.mouse_press ? buffer[len++] = '0' : 0; + }!!; + ui.@div(ugui::@fit(), ugui::@fit(), COLUMN) { + ui.button("8")!!.mouse_press ? buffer[len++] = '8' : 0; + ui.button("5")!!.mouse_press ? buffer[len++] = '5' : 0; + ui.button("2")!!.mouse_press ? buffer[len++] = '2' : 0; + ui.button(".")!!.mouse_press ? buffer[len++] = '.' : 0; + }!!; + ui.@div(ugui::@fit(), ugui::@fit(), COLUMN) { + ui.button("9")!!.mouse_press ? buffer[len++] = '9' : 0; + ui.button("6")!!.mouse_press ? buffer[len++] = '6' : 0; + ui.button("3")!!.mouse_press ? buffer[len++] = '3' : 0; + ui.button("(")!!.mouse_press ? buffer[len++] = '(' : 0; + }!!; + + ui.@div(ugui::@exact(10), ugui::@exact(10)) {}!!; - // eval the expression with 'bc' - if (ui.button("=")!!.mouse_press || eval) { - char[128] out; - String y = string::tformat("echo '%s' | bc", (String)buffer[:len]); - String x = process::execute_stdout_to_buffer(out[:128], (String[]){"sh", "-c", y}) ?? ""; - buffer[:x.len] = x[..]; - len = x.len; - } + ui.@div(ugui::@fit(), ugui::@fit(), COLUMN) { + ui.button("x")!!.mouse_press ? buffer[len++] = '*' : 0; + ui.button("/")!!.mouse_press ? buffer[len++] = '/' : 0; + ui.button("+")!!.mouse_press ? buffer[len++] = '+' : 0; + ui.button(")")!!.mouse_press ? buffer[len++] = ')' : 0; + }!!; + ui.@div(ugui::@fit(), ugui::@fit(), COLUMN) { + ui.button("C")!!.mouse_press ? len = 0 : 0; + ui.button("D")!!.mouse_press ? len-- : 0; + ui.button("-")!!.mouse_press ? buffer[len++] = '-' : 0; + + // eval the expression with 'bc' + if (ui.button("=")!!.mouse_press || eval) { + char[128] out; + String y = string::tformat("echo '%s' | bc", (String)buffer[:len]); + String x = process::execute_stdout_to_buffer(out[:128], (String[]){"sh", "-c", y}) ?? ""; + buffer[:x.len] = x[..]; + len = x.len; + } + }!!; }!!; - }!!; + }!!; }!!; } From c3a639040473a2682d6b03ed6393e4fc020c6a7c Mon Sep 17 00:00:00 2001 From: Alessandro Mauri Date: Tue, 9 Sep 2025 19:10:04 +0200 Subject: [PATCH 13/13] draw text correctly --- TODO | 12 +- lib/ugui.c3l/src/ugui_button.c3 | 3 +- lib/ugui.c3l/src/ugui_cmd.c3 | 26 ---- lib/ugui.c3l/src/ugui_font.c3 | 248 +++++++++++++++++--------------- lib/ugui.c3l/src/ugui_text.c3 | 26 ++-- src/main.c3 | 3 +- 6 files changed, 156 insertions(+), 162 deletions(-) diff --git a/TODO b/TODO index c678e87..36963d9 100644 --- a/TODO +++ b/TODO @@ -44,19 +44,19 @@ to maintain focus until mouse release (fix scroll bars) [x] Fix how padding is applied in push_rect. In CSS padding is applied between the border and the content, the background color is applied starting from the border. Right now push_rect() offsets the background rect by both border and padding -[ ] Investigate why the debug pointer (cyan rectangle) disappears... +[x] Investigate why the debug pointer (cyan rectangle) disappears... ## Layout [x] Flexbox -[ ] Center elements to the row/column +[x] Center elements to the row/column [x] Text wrapping / reflow [x] Implement a better and unified way to place a glyph and get the cursor position, maybe with a struct [ ] Correct whitespace handling in text (\t \r etc) -[ ] Consider a multi-pass recursive approach to layout (like https://github.com/nicbarker/clay) +[x] Consider a multi-pass recursive approach to layout (like https://github.com/nicbarker/clay) instead of the curren multi-frame approach. -[ ] Implement column/row sizing (min, max) -[ ] Implement a way to size the element as the current row/column size +[x] Implement column/row sizing (min, max) +[x] Implement a way to size the element as the current row/column size * +-------------+ * | | * +-------------+ @@ -68,7 +68,7 @@ to maintain focus until mouse release (fix scroll bars) See the calculator example for why it is useful [ ] Find a way to concile pixel measurements to the mm ones used in css, for example in min/max sizing of elements -[ ] Center elements to div (center children_bounds to the center of the div bounds and shift the origin accordingly) +[x] Center elements to div (center children_bounds to the center of the div bounds and shift the origin accordingly) [x] Use containing_rect() in position_element() to skip some computing and semplify the function [x] Rename position_element() to layout_element() [x] Make functions to mark rows/columns as full, to fix the calculator demo diff --git a/lib/ugui.c3l/src/ugui_button.c3 b/lib/ugui.c3l/src/ugui_button.c3 index d963713..00b2b5c 100644 --- a/lib/ugui.c3l/src/ugui_button.c3 +++ b/lib/ugui.c3l/src/ugui_button.c3 @@ -40,7 +40,6 @@ fn ElemEvents? Ctx.button_id(&ctx, Id id, String label, String icon) .x = icon_size.w + inner_pad, // text sizing is handled differently .y = icon_size.h + inner_pad, }; - //elem.layout.w = @fit(min_size); elem.layout.w = @fit(min_size); elem.layout.h = @fit(min_size); elem.layout.children.w = @exact(content_size.x); @@ -82,7 +81,7 @@ fn ElemEvents? Ctx.button_id(&ctx, Id id, String label, String icon) ctx.push_sprite(icon_bounds, sprite.uv(), ctx.sprite_atlas.id, parent.div.z_index, type: sprite.type)!; } if (label != "") { - ctx.push_string(text_bounds, label, parent.div.z_index, style.fg, true)!; + ctx.layout_string(label, text_bounds, CENTER, parent.div.z_index, style.fg)!; } return elem.events; } diff --git a/lib/ugui.c3l/src/ugui_cmd.c3 b/lib/ugui.c3l/src/ugui_cmd.c3 index 804f1c4..a4982fc 100644 --- a/lib/ugui.c3l/src/ugui_cmd.c3 +++ b/lib/ugui.c3l/src/ugui_cmd.c3 @@ -163,32 +163,6 @@ fn void? Ctx.push_sprite(&ctx, Rect bounds, Rect texture, Id texture_id, int z_i ctx.push_cmd(&cmd, z_index)!; } -// TODO: do not return the WHOLE TextInfo but instead something smaller -fn TextInfo? Ctx.push_string(&ctx, Rect bounds, char[] text, int z_index, Color hue, bool reflow = false) -{ - if (text.len == 0) { - return {}; - } - - ctx.push_scissor(bounds, z_index)!; - - Id texture_id = ctx.font.id; // or ctx.font.atlas.id - Rect text_bounds = {bounds.x, bounds.y, 0, 0}; - - TextInfo ti; - ti.init(&ctx.font, (String)text, bounds); - - while (ti.place_glyph(reflow)!) { - if (!cull_rect(ti.glyph_bounds, bounds)) { - ctx.push_sprite(ti.glyph_bounds, ti.glyph_uv, texture_id, z_index, hue)!; - } - } - - ctx.reset_scissor(z_index)!; - - return ti; -} - fn void? Ctx.push_update_atlas(&ctx, Atlas* atlas) { Cmd up = { diff --git a/lib/ugui.c3l/src/ugui_font.c3 b/lib/ugui.c3l/src/ugui_font.c3 index 12fca7c..35bb941 100644 --- a/lib/ugui.c3l/src/ugui_font.c3 +++ b/lib/ugui.c3l/src/ugui_font.c3 @@ -218,121 +218,6 @@ fn int Font.line_height(&font) => (int)(font.ascender - font.descender + (float) const uint TAB_SIZE = 4; -// TODO: change the name -// TODO: reorder to make smaller -struct TextInfo { - Font* font; - Rect bounds; - String text; - usz off; - Size width, height; - - // current glyph info - Point origin; - Rect glyph_bounds; - Rect glyph_uv; - - // cursor info - usz cursor_idx; - Point cursor_pos; - - // current text bounds - Rect text_bounds; -} - -<* -@require f != null -*> -fn void TextInfo.init(&self, Font* f, String s, Rect bounds = RECT_MAX) -{ - *self = {}; - self.font = f; - self.text = s; - self.bounds = bounds; - self.origin = bounds.position(); - self.text_bounds = { .x = bounds.x, .y = bounds.y }; -} - -fn void TextInfo.reset(&self) -{ - TextInfo old = *self; - self.init(old.font, old.text, old.bounds); -} - -fn bool? TextInfo.place_glyph(&self, bool reflow) -{ - if (self.off >= self.text.len) { - return false; - } - if (!self.origin.in_rect(self.bounds)) { - return false; - } - - short baseline = (short)self.font.ascender; - short line_height = (short)self.font.ascender - (short)self.font.descender; - short line_gap = (short)self.font.linegap; - - Codepoint cp; - Glyph* gp; - usz x; - - cp = str_to_codepoint(self.text[self.off..], &x); - if (cp == 0) return false; - self.off += x; - - if (ascii::is_cntrl((char)cp) == false) { - gp = self.font.get_glyph(cp)!; - self.glyph_uv = { - .x = gp.u, - .y = gp.v, - .w = gp.w, - .h = gp.h, - }; - self.glyph_bounds = { - .x = self.origin.x + gp.ox, - .y = self.origin.y + gp.oy + baseline, - .w = gp.w, - .h = gp.h, - }; - // try to wrap the text if the charcater goes outside of the bounds - if (reflow && !self.bounds.contains(self.glyph_bounds)) { - self.origin.y += line_height + line_gap; - self.glyph_bounds.y += line_height + line_gap; - - self.origin.x = self.bounds.x; - self.glyph_bounds.x = self.bounds.x; - } - - // handle tab - if (cp == '\t') { - self.origin.x += gp.adv*TAB_SIZE; - } else { - self.origin.x += gp.adv; - } - - } else if (cp == '\n'){ - self.origin.y += line_height + line_gap; - self.origin.x = self.bounds.x; - } - - self.text_bounds = containing_rect(self.text_bounds, self.glyph_bounds); - - if (self.off == self.cursor_idx) { - self.cursor_pos = self.origin; - } - - return true; -} - -fn Rect? Ctx.get_text_bounds(&ctx, String text) -{ - TextInfo ti; - ti.init(&ctx.font, text); - while (ti.place_glyph(false)!); - return ti.text_bounds; -} - - struct TextSize { Size width, height; int area; @@ -345,6 +230,8 @@ struct TextSize { // height.max: the height of the string with each word broken up by a new line fn TextSize? Ctx.measure_string(&ctx, String text) { + if (text == "") return (TextSize){}; + Font* font = &ctx.font; short baseline = (short)font.ascender; short line_height = (short)font.line_height(); @@ -412,3 +299,134 @@ fn TextSize? Ctx.measure_string(&ctx, String text) return ts; } + +fn void? Ctx.layout_string(&ctx, String text, Rect bounds, Anchor anchor, int z_index, Color hue) +{ + if (text.len == 0 || bounds.w <= 0 || bounds.h <= 0) return; + ctx.push_scissor(bounds, z_index)!; + + Font* font = &ctx.font; + Id texture_id = font.id; + short baseline = (short)font.ascender; + short line_height = (short)font.line_height(); + short line_gap = (short)font.linegap; + short space_width = font.get_glyph(' ').adv!; + short tab_width = space_width * TAB_SIZE; + + Point origin = bounds.position(); + + // measure string height inside the bounds to center it vertically + // FIXME: this is fast but it just gives back the height in LINES, most lines have only short + // characters which would result in a lower height + int string_height = line_height; + foreach (c: text) { + string_height += (line_height + line_gap) * (int)(c == '\n'); + } + + switch (anchor) { + case TOP_LEFT: nextcase; + case TOP: nextcase; + case TOP_RIGHT: + origin.y += 0; + case LEFT: nextcase; + case CENTER: nextcase; + case RIGHT: + origin.y += (short)(bounds.h - string_height)/2; + case BOTTOM_LEFT: nextcase; + case BOTTOM: nextcase; + case BOTTOM_RIGHT: + origin.y += (short)(bounds.h - string_height); + } + + // measure the line until it exits the bounds or the string ends + usz line_start, line_end; + do { + int line_width; + Point o = {.x = bounds.x, .y = bounds.y}; + + Codepoint cp; + isz off = line_start; + for ITER: (usz x; (cp = str_to_codepoint(text[off..], &x)) != 0; off += x) { + Glyph* gp = font.get_glyph(cp)!; + + switch { + case cp == '\n': + break ITER; + case cp == '\t': + o.x += tab_width; + case ascii::is_cntrl((char)cp): + break; + default: + Rect b = { + .x = o.x + gp.ox, + .y = o.y + gp.oy + baseline, + .w = gp.w, + .h = gp.h, + }; + + if (b.x + b.w > bounds.x + bounds.w) { + break ITER; + } + o.x += gp.adv; + line_width += gp.adv; + } + } + line_end = off; + if (line_end == line_start) break; + + // with the line width calculate the right origin and layout the line + switch (anchor) { + case TOP_LEFT: nextcase; + case LEFT: nextcase; + case BOTTOM_LEFT: + // TODO: we didn't need to measure the line width with this alignment + origin.x += 0; + case TOP: nextcase; + case CENTER: nextcase; + case BOTTOM: + origin.x += (short)(bounds.w - line_width)/2+1; + case TOP_RIGHT: nextcase; + case RIGHT: nextcase; + case BOTTOM_RIGHT: + origin.x += (short)(bounds.w - line_width); + } + + // see the fixme when measuring the height + //ctx.push_rect({.x = origin.x,.y=origin.y,.w=(short)line_width,.h=(short)string_height}, z_index, &&(Style){.bg=0xff000042u.@to_rgba()})!; + String line = text[line_start:line_end-line_start]; + off = 0; + for (usz x; (cp = str_to_codepoint(line[off..], &x)) != 0 && off < line.len; off += x) { + Glyph* gp = font.get_glyph(cp)!; + + // update the text bounds + switch { + case cp == '\t': + origin.x += tab_width; + case ascii::is_cntrl((char)cp): + break; + default: + Rect b = { + .x = origin.x + gp.ox, + .y = origin.y + gp.oy + baseline, + .w = gp.w, + .h = gp.h + }; + Rect uv = { + .x = gp.u, + .y = gp.v, + .w = gp.w, + .h = gp.h + }; + ctx.push_sprite(b, uv, texture_id, z_index, hue)!; + //ctx.push_rect(b, z_index, &&(Style){.bg=0x0000ff66u.@to_rgba()})!; + origin.x += gp.adv; + } + + } + // done with the line + line_start = line_end; + origin.y += line_height + line_gap; + } while(line_end < text.len); + + ctx.reset_scissor(z_index)!; +} diff --git a/lib/ugui.c3l/src/ugui_text.c3 b/lib/ugui.c3l/src/ugui_text.c3 index dd19776..64b1fd8 100644 --- a/lib/ugui.c3l/src/ugui_text.c3 +++ b/lib/ugui.c3l/src/ugui_text.c3 @@ -3,16 +3,14 @@ module ugui; import std::io; struct ElemText { - String str; usz cursor; // cursor offset Id hash; - Rect bounds; + TextSize size; } -/* -macro Ctx.text_unbounded(&ctx, String text, ...) - => ctx.text_unbounded_id(@compute_id($vasplat), text); -fn void? Ctx.text_unbounded_id(&ctx, Id id, String text) +macro Ctx.text(&ctx, String text, ...) + => ctx.text_id(@compute_id($vasplat), text); +fn void? Ctx.text_id(&ctx, Id id, String text) { id = ctx.gen_id(id)!; @@ -22,18 +20,22 @@ fn void? Ctx.text_unbounded_id(&ctx, Id id, String text) Id text_hash = text.hash(); if (elem.flags.is_new || elem.text.hash != text_hash) { - elem.text.bounds = ctx.get_text_bounds(text)!; + elem.text.size = ctx.measure_string(text)!; } - elem.text.str = text; elem.text.hash = text_hash; - // 2. Layout - elem.bounds = ctx.layout_element(parent, elem.text.bounds, style); - if (elem.bounds.is_null()) { return; } + elem.layout.w = @fit(style.size); + elem.layout.h = @fit(style.size); + elem.layout.text = elem.text.size; + elem.layout.content_offset = style.margin + style.border + style.padding; - ctx.push_string(elem.bounds, text, parent.div.z_index, style.fg)!; + update_parent_grow(elem, parent); + update_parent_size(elem, parent); + + ctx.layout_string(text, elem.bounds.pad(elem.layout.content_offset), TOP_LEFT, parent.div.z_index, style.fg)!; } +/* macro Ctx.text_box(&ctx, Rect size, char[] text, usz* text_len, ...) => ctx.text_box_id(@compute_id($vasplat), size, text, text_len); fn ElemEvents? Ctx.text_box_id(&ctx, Id id, Rect size, char[] text, usz* text_len) diff --git a/src/main.c3 b/src/main.c3 index 53fb2a6..f92778e 100644 --- a/src/main.c3 +++ b/src/main.c3 @@ -343,6 +343,7 @@ fn void calculator(ugui::Ctx* ui) case "/": nextcase; case "(": nextcase; case ")": nextcase; + case ".": nextcase; case "0": nextcase; case "1": nextcase; case "2": nextcase; @@ -367,7 +368,7 @@ fn void calculator(ugui::Ctx* ui) ui.@div(ugui::@fit(), ugui::@fit(), COLUMN, TOP_LEFT) { ui.@div(ugui::@grow(), ugui::@exact(100), ROW, RIGHT) { - ui.button("TODO")!!; + ui.text((String)buffer[:len])!!; }!!; ui.@div(ugui::@fit(), ugui::@fit(), ROW, TOP_LEFT) { ui.@div(ugui::@fit(), ugui::@fit(), COLUMN) {