From c3a639040473a2682d6b03ed6393e4fc020c6a7c Mon Sep 17 00:00:00 2001 From: Alessandro Mauri Date: Tue, 9 Sep 2025 19:10:04 +0200 Subject: [PATCH] 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) {