diff --git a/TODO b/TODO index b857d31..b995b2a 100644 --- a/TODO +++ b/TODO @@ -48,7 +48,9 @@ to maintain focus until mouse release (fix scroll bars) [x] Flexbox [ ] Center elements to the row/column -[ ] Text wrapping / reflow +[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) instead of the curren multi-frame approach. @@ -59,6 +61,8 @@ to maintain focus until mouse release (fix scroll bars) [ ] Touch input [x] Do not set input event to true if the movement was zero (like no mouse movement) [ ] Use input event flags, for example to consume the input event +[ ] Fix bug in text box: when spamming keys you can get multiple characters in the text input field + of the context, this causes a bug where only the first char is actually used ## Commands @@ -67,7 +71,7 @@ to maintain focus until mouse release (fix scroll bars) - border radius [x] add a command to update an atlas [ ] New window command, useful for popups -[ ] Text command returns the text bounds, this way we can avoid the pattern +[x] Text command returns the text bounds, this way we can avoid the pattern draw_text(a, pos) -> off = compute_bounds(a) -> draw_text(b, pos+off) -> ... ## Atlas @@ -91,11 +95,6 @@ to maintain focus until mouse release (fix scroll bars) [x] Checkbox [ ] Selectable text box -## Main / exaple - -[ ] Create maps from ids to textures and images instead of hardcoding them - - ## API [ ] Introduce a Layout structure that specifies the positioning of elements inside diff --git a/lib/ugui.c3l/src/ugui_button.c3 b/lib/ugui.c3l/src/ugui_button.c3 index 7635973..b24a3d4 100644 --- a/lib/ugui.c3l/src/ugui_button.c3 +++ b/lib/ugui.c3l/src/ugui_button.c3 @@ -44,17 +44,20 @@ fn ElemEvents? Ctx.button_label_id(&ctx, Id id, String label, Rect size, bool ac Elem *parent = ctx.get_parent()!; Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!; - - short line_height = (short)ctx.font.ascender - (short)ctx.font.descender; - Rect text_size = ctx.get_text_bounds(label)!; - Rect btn_size = text_size.add({0,0,10,10}); Style* style_norm = ctx.styles.get_style(@str_hash("button")); + Style* style_active = ctx.styles.get_style(@str_hash("button-active")); + + // TODO: cache text_size just like text_unbounded() does + Rect text_size = ctx.get_text_bounds(label)!; + Rect btn_size = text_size.grow({10,10}); // 2. Layout elem.bounds = ctx.position_element(parent, btn_size, style_norm); if (elem.bounds.is_null()) { return {}; } - - Style* style_active = ctx.styles.get_style(@str_hash("button-active")); + + text_size.x = elem.bounds.x; + text_size.y = elem.bounds.y; + text_size = text_size.center_to(elem.bounds); elem.events = ctx.get_elem_events(elem); @@ -62,11 +65,6 @@ fn ElemEvents? Ctx.button_label_id(&ctx, Id id, String label, Rect size, bool ac Style* style = is_active ? style_active : style_norm; // Draw the button - text_size.x = elem.bounds.x; - text_size.y = elem.bounds.y; - Point off = ctx.center_text(text_size, elem.bounds); - text_size.x += off.x; - text_size.y += off.y; ctx.push_rect(elem.bounds, parent.div.z_index, style)!; ctx.push_string(text_size, label, parent.div.z_index, style.fg)!; diff --git a/lib/ugui.c3l/src/ugui_cmd.c3 b/lib/ugui.c3l/src/ugui_cmd.c3 index 2919436..e282a18 100644 --- a/lib/ugui.c3l/src/ugui_cmd.c3 +++ b/lib/ugui.c3l/src/ugui_cmd.c3 @@ -136,56 +136,30 @@ fn void? Ctx.push_sprite(&ctx, Rect bounds, Rect texture, Id texture_id, int z_i ctx.push_cmd(&cmd, z_index)!; } -fn void? Ctx.push_string(&ctx, Rect bounds, char[] text, int z_index, Color hue) +// 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; + return {}; } ctx.push_scissor(bounds, z_index)!; - short baseline = (short)ctx.font.ascender; - short line_height = (short)ctx.font.ascender - (short)ctx.font.descender; - short line_gap = (short)ctx.font.linegap; Id texture_id = ctx.font.id; // or ctx.font.atlas.id - Point orig = { - .x = bounds.x, - .y = bounds.y, - }; + Rect text_bounds = {bounds.x, bounds.y, 0, 0}; - short line_len; - Codepoint cp; - usz off, x; - while (off < text.len && (cp = str_to_codepoint(text[off..], &x)) != 0) { - off += x; - Glyph* gp; - if (!ascii::is_cntrl((char)cp)) { - gp = ctx.font.get_glyph(cp)!; - Rect gb = { - .x = orig.x + line_len + gp.ox, - .y = orig.y + gp.oy + baseline, - .w = gp.w, - .h = gp.h, - }; - Rect gt = { - .x = gp.u, - .y = gp.v, - .w = gp.w, - .h = gp.h, - }; - // push the sprite only if it collides with the bounds - if (!cull_rect(gb, bounds)) ctx.push_sprite(gb, gt, texture_id, z_index, hue)!; - line_len += gp.adv; - } else if (cp == '\n'){ - orig.y += line_height + line_gap; - line_len = 0; - } else { - continue; + 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)!; } } - - // FIXME: we never get here if an error was thrown before + ctx.push_scissor({}, z_index)!; + + return ti; } fn void? Ctx.push_update_atlas(&ctx, Atlas* atlas) diff --git a/lib/ugui.c3l/src/ugui_font.c3 b/lib/ugui.c3l/src/ugui_font.c3 index 020ed75..17a36ac 100644 --- a/lib/ugui.c3l/src/ugui_font.c3 +++ b/lib/ugui.c3l/src/ugui_font.c3 @@ -167,6 +167,7 @@ fn void? Ctx.load_font(&ctx, String name, ZString path, uint height, float scale <* @require off != null +@require str.ptr != null *> fn Codepoint str_to_codepoint(char[] str, usz* off) { @@ -179,72 +180,119 @@ fn Codepoint str_to_codepoint(char[] str, usz* off) return cp; } +const uint TAB_SIZE = 4; + +// TODO: change the name +// TODO: reorder to make smaller +struct TextInfo { + Font* font; + Rect bounds; + String text; + usz off; + + // 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) { - Rect text_bounds; - short line_height = (short)ctx.font.ascender - (short)ctx.font.descender; - short line_gap = (short)ctx.font.linegap; - text_bounds.h = line_height; - Glyph* gp; - - // TODO: account for unicode codepoints - short line_len; - Codepoint cp; - usz off, x; - while ((cp = str_to_codepoint(text[off..], &x)) != 0) { - off += x; - bool n; - if (!ascii::is_cntrl((char)cp)) { - gp = ctx.font.get_glyph(cp)!; - line_len += gp.adv; - } else if (cp == '\n'){ - text_bounds.h += line_height + line_gap; - line_len = 0; - } else { - continue; - } - if (line_len > text_bounds.w) { - text_bounds.w = line_len; - } - } - - return text_bounds; -} - -fn Point? Ctx.get_cursor_position(&ctx, String text) -{ - short line_height = (short)ctx.font.ascender - (short)ctx.font.descender; - short line_gap = (short)ctx.font.linegap; - Glyph* gp; - - // TODO: account for unicode codepoints - Point line; - Codepoint cp; - usz off, x; - while ((cp = str_to_codepoint(text[off..], &x)) != 0) { - off += x; - bool n; - if (!ascii::is_cntrl((char)cp)) { - gp = ctx.font.get_glyph(cp)!; - line.x += gp.adv; - } else if (cp == '\n'){ - line.y += line_height + line_gap; - line.x = 0; - } else { - continue; - } - } - - return line; -} - - -fn Point Ctx.center_text(&ctx, Rect text_bounds, Rect bounds) -{ - short dw = bounds.w - text_bounds.w; - short dh = bounds.h - text_bounds.h; - - return {.x = dw/2, .y = dh/2}; + TextInfo ti; + ti.init(&ctx.font, text); + while (ti.place_glyph(false)!); + return ti.text_bounds; } // TODO: check if the font is present in the context diff --git a/lib/ugui.c3l/src/ugui_shapes.c3 b/lib/ugui.c3l/src/ugui_shapes.c3 index 9e778b7..d268229 100644 --- a/lib/ugui.c3l/src/ugui_shapes.c3 +++ b/lib/ugui.c3l/src/ugui_shapes.c3 @@ -10,6 +10,9 @@ struct Rect { short x, y, w, h; } +// TODO: find another name +const Rect RECT_MAX = {0, 0, short.max, short.max}; + // return true if rect a contains b macro bool Rect.contains(Rect a, Rect b) { @@ -33,6 +36,21 @@ macro bool Rect.collides(Rect a, Rect b) return !(a.x > b.x+b.w || a.x+a.w < b.x || a.y > b.y+b.h || a.y+a.h < b.y); } +// return a rect that contains both rects, a bounding box of both +macro Rect containing_rect(Rect a, Rect b) +{ + short min_x = (short)min(a.x, b.x); + short min_y = (short)min(a.y, b.y); + short max_x = (short)max(a.x + a.w, b.x + b.w); + short max_y = (short)max(a.y + a.h, b.y + b.h); + return { + .x = min_x, + .y = min_y, + .w = (short)(max_x - min_x), + .h = (short)(max_y - min_y) + }; +} + // check for empty rect macro bool Rect.is_null(Rect r) => r.x == 0 && r.y == 0 && r.x == 0 && r.w == 0; @@ -136,6 +154,12 @@ macro Point Rect.bottom_right(Rect r) }; } +macro Rect Rect.center_to(Rect a, Rect b) +{ + Point off = {.x = (b.w - a.w)/2, .y = (b.h - a.h)/2}; + return a.off(off); +} + // ---------------------------------------------------------------------------------- // // POINT // diff --git a/lib/ugui.c3l/src/ugui_text.c3 b/lib/ugui.c3l/src/ugui_text.c3 index 7d2d422..f9afcb0 100644 --- a/lib/ugui.c3l/src/ugui_text.c3 +++ b/lib/ugui.c3l/src/ugui_text.c3 @@ -3,8 +3,10 @@ module ugui; import std::io; struct ElemText { - char[] str; + String str; usz cursor; // cursor offset + Id hash; + Rect bounds; } macro Ctx.text_unbounded(&ctx, String text, ...) @@ -17,12 +19,15 @@ fn void? Ctx.text_unbounded_id(&ctx, Id id, String text) Elem *elem = ctx.get_elem(id, ETYPE_TEXT)!; Style* style = ctx.styles.get_style(@str_hash("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.str = text; + elem.text.hash = text_hash; - // if the element is new or the parent was updated then redo layout - Rect text_size = ctx.get_text_bounds(text)!; // 2. Layout - elem.bounds = ctx.position_element(parent, text_size, style); + elem.bounds = ctx.position_element(parent, elem.text.bounds, style); if (elem.bounds.is_null()) { return; } ctx.push_string(elem.bounds, text, parent.div.z_index, style.fg)!; @@ -38,7 +43,7 @@ fn ElemEvents? Ctx.text_box_id(&ctx, Id id, Rect size, char[] text, usz* text_le Elem *elem = ctx.get_elem(id, ETYPE_TEXT)!; Style* style = ctx.styles.get_style(@str_hash("text-box")); - elem.text.str = text; + elem.text.str = (String)text; // layout the text box elem.bounds = ctx.position_element(parent, size, style); @@ -64,27 +69,11 @@ fn ElemEvents? Ctx.text_box_id(&ctx, Id id, Rect size, char[] text, usz* text_le // draw the box short line_height = (short)ctx.font.line_height(); - Rect text_box = elem.bounds.sub({0,0,0,line_height}); - Rect input_box = { - .x = elem.bounds.x, - .y = elem.bounds.y + elem.bounds.h - line_height, - .w = elem.bounds.w, - .h = line_height, - }; - Rect cursor; - Point b = ctx.get_cursor_position((String)text[:elem.text.cursor])!; - cursor = { - .x = b.x, - .y = b.y, - .w = 3, - .h = line_height, - }; - cursor = cursor.off(elem.bounds.position()); - + Rect text_box = elem.bounds; ctx.push_rect(text_box, parent.div.z_index, style)!; - ctx.push_string(text_box, text[:*text_len], parent.div.z_index, style.fg)!; - ctx.push_rect(input_box, parent.div.z_index, style)!; - ctx.push_rect(cursor, parent.div.z_index, style)!; + ctx.push_string(text_box, text[:*text_len], parent.div.z_index, style.fg, true)!; + + // TODO: draw cursor return elem.events; } diff --git a/src/main.c3 b/src/main.c3 index cd1f341..ea4dc0d 100644 --- a/src/main.c3 +++ b/src/main.c3 @@ -279,7 +279,7 @@ fn int main(String[] args) 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.text_unbounded(string::tformat("%s %s", mod.lctrl, (String)ui.input.keyboard.text[:ui.input.keyboard.text_len]))!!; }; ui.div_end()!!;