From 34b92c93b4183482972c2f81df1503d3f4c754b4 Mon Sep 17 00:00:00 2001 From: Alessandro Mauri Date: Sat, 13 Sep 2025 19:47:58 +0200 Subject: [PATCH] re-implemented text box also includes - small layout fix for grow elements - ElemEvents now includes has_focus flag --- lib/ugui.c3l/src/ugui_core.c3 | 17 ++-- lib/ugui.c3l/src/ugui_font.c3 | 27 +++++-- lib/ugui.c3l/src/ugui_input.c3 | 19 +++++ lib/ugui.c3l/src/ugui_layout.c3 | 6 ++ lib/ugui.c3l/src/ugui_textedit.c3 | 108 ++++++++++++++++++++++++++ lib/ugui.c3l/src/widgets/ugui_text.c3 | 67 +++++++++------- resources/style.css | 11 +++ src/main.c3 | 27 ++++++- 8 files changed, 237 insertions(+), 45 deletions(-) create mode 100644 lib/ugui.c3l/src/ugui_textedit.c3 diff --git a/lib/ugui.c3l/src/ugui_core.c3 b/lib/ugui.c3l/src/ugui_core.c3 index 14ff119..4e797ae 100644 --- a/lib/ugui.c3l/src/ugui_core.c3 +++ b/lib/ugui.c3l/src/ugui_core.c3 @@ -38,13 +38,14 @@ bitstruct ElemFlags : uint { bitstruct ElemEvents : uint { bool key_press : 0; bool key_release : 1; - bool key_hold : 2; + bool key_repeat : 2; bool mouse_hover : 3; bool mouse_press : 4; bool mouse_release : 5; bool mouse_hold : 6; bool update : 7; bool text_input : 8; + bool has_focus : 9; } // element structure @@ -334,7 +335,7 @@ macro bool Ctx.elem_focus(&ctx, Elem *elem) fn ElemEvents Ctx.get_elem_events(&ctx, Elem *elem) { bool hover = ctx.is_hovered(elem); - bool focus = ctx.focus_id == elem.id || (hover && ctx.is_mouse_pressed(BTN_LEFT)); + bool focus = ctx.elem_focus(elem) || (hover && ctx.is_mouse_pressed(BTN_LEFT)); if (ctx.is_mouse_pressed(BTN_ANY) && !hover){ focus = false; @@ -345,11 +346,15 @@ fn ElemEvents Ctx.get_elem_events(&ctx, Elem *elem) if (focus) { ctx.focus_id = elem.id; } ElemEvents ev = { - .mouse_hover = hover, - .mouse_press = hover && focus && ctx.is_mouse_pressed(BTN_ANY), + .has_focus = focus, + .mouse_hover = hover, + .mouse_press = hover && focus && ctx.is_mouse_pressed(BTN_ANY), .mouse_release = hover && focus && ctx.is_mouse_released(BTN_ANY), - .mouse_hold = hover && focus && ctx.is_mouse_down(BTN_ANY), - .text_input = focus && (ctx.input.keyboard.text_len || ctx.input.keyboard.modkeys & KMOD_TXT), + .mouse_hold = hover && focus && ctx.is_mouse_down(BTN_ANY), + .key_press = focus && ctx.input.events.key_press, + .key_release = focus && ctx.input.events.key_release, + .key_repeat = focus && ctx.input.events.key_repeat, + .text_input = focus && (ctx.input.keyboard.text_len || ctx.input.keyboard.modkeys & KMOD_TXT), }; return ev; } diff --git a/lib/ugui.c3l/src/ugui_font.c3 b/lib/ugui.c3l/src/ugui_font.c3 index 35bb941..0a275ff 100644 --- a/lib/ugui.c3l/src/ugui_font.c3 +++ b/lib/ugui.c3l/src/ugui_font.c3 @@ -300,15 +300,20 @@ 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) +// layout a string inside a bounding box, following the given alignment (anchor). +// returns the position of the cursor, the returned height is the line height and the width is the last +// character's advance value +fn Rect? Ctx.layout_string(&ctx, String text, Rect bounds, Anchor anchor, int z_index, Color hue, isz cursor = -1) { - if (text.len == 0 || bounds.w <= 0 || bounds.h <= 0) return; + Font* font = &ctx.font; + short line_height = (short)font.line_height(); + Rect cursor_rect = {.h = line_height}; + + if (text == "" || bounds.w <= 0 || bounds.h <= 0) return cursor_rect; 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; @@ -375,6 +380,7 @@ fn void? Ctx.layout_string(&ctx, String text, Rect bounds, Anchor anchor, int z_ if (line_end == line_start) break; // with the line width calculate the right origin and layout the line + origin.x = bounds.x; switch (anchor) { case TOP_LEFT: nextcase; case LEFT: nextcase; @@ -391,6 +397,9 @@ fn void? Ctx.layout_string(&ctx, String text, Rect bounds, Anchor anchor, int z_ origin.x += (short)(bounds.w - line_width); } + cursor_rect.x = origin.x; + cursor_rect.y = origin.y; + // 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]; @@ -419,9 +428,15 @@ fn void? Ctx.layout_string(&ctx, String text, Rect bounds, Anchor anchor, int z_ }; ctx.push_sprite(b, uv, texture_id, z_index, hue)!; //ctx.push_rect(b, z_index, &&(Style){.bg=0x0000ff66u.@to_rgba()})!; + + if (line_start + off < cursor) { + cursor_rect.x = origin.x + gp.adv; + cursor_rect.y = origin.y; + cursor_rect.w = gp.adv; + } + origin.x += gp.adv; } - } // done with the line line_start = line_end; @@ -429,4 +444,6 @@ fn void? Ctx.layout_string(&ctx, String text, Rect bounds, Anchor anchor, int z_ } while(line_end < text.len); ctx.reset_scissor(z_index)!; + + return cursor_rect; } diff --git a/lib/ugui.c3l/src/ugui_input.c3 b/lib/ugui.c3l/src/ugui_input.c3 index 6125ace..5603ce0 100644 --- a/lib/ugui.c3l/src/ugui_input.c3 +++ b/lib/ugui.c3l/src/ugui_input.c3 @@ -13,6 +13,9 @@ bitstruct InputEvents : uint { bool mouse_scroll : 4; // mouse scroll wheel. x or y bool text_input : 5; bool mod_key : 6; + bool key_press : 7; + bool key_release : 8; + bool key_repeat : 9; } bitstruct MouseButtons : uint { @@ -156,6 +159,21 @@ fn void Ctx.input_mouse_wheel(&ctx, short x, short y, float scale = 1.0) ctx.input.events.mouse_scroll = x !=0 || y != 0; } +fn void Ctx.input_key_press(&ctx) +{ + ctx.input.events.key_press = true; +} + +fn void Ctx.input_key_release(&ctx) +{ + ctx.input.events.key_release = true; +} + +fn void Ctx.input_key_repeat(&ctx) +{ + ctx.input.events.key_repeat = true; +} + // append utf-8 encoded text to the context text input fn void Ctx.input_text_utf8(&ctx, char[] text) { @@ -196,6 +214,7 @@ fn void Ctx.input_char(&ctx, char c) } fn String Ctx.get_keys(&ctx) => (String)ctx.input.keyboard.text[:ctx.input.keyboard.text_len]; +fn ModKeys Ctx.get_mod(&ctx) => ctx.input.keyboard.modkeys; // Modifier keys, like control or backspace // TODO: make this call repetible to input modkeys one by one diff --git a/lib/ugui.c3l/src/ugui_layout.c3 b/lib/ugui.c3l/src/ugui_layout.c3 index bc83c52..4a2e5c7 100644 --- a/lib/ugui.c3l/src/ugui_layout.c3 +++ b/lib/ugui.c3l/src/ugui_layout.c3 @@ -88,6 +88,12 @@ macro Point Layout.get_dimensions(&el) // GROSS HACK FOR EXACT DIMENSIONS if (el.w.@is_exact()) dim.x = el.w.min + el.content_offset.x + el.content_offset.w; if (el.h.@is_exact()) dim.y = el.h.min + el.content_offset.y + el.content_offset.h; + + // GROSS HACK FOR GROW DIMENSIONS + // FIXME: does this always work? + if (el.w.@is_grow()) dim.x = 0; + if (el.h.@is_grow()) dim.y = 0; + return dim; } diff --git a/lib/ugui.c3l/src/ugui_textedit.c3 b/lib/ugui.c3l/src/ugui_textedit.c3 new file mode 100644 index 0000000..2e3f1b6 --- /dev/null +++ b/lib/ugui.c3l/src/ugui_textedit.c3 @@ -0,0 +1,108 @@ +module ugui; + +import grapheme; +import std::ascii; + +struct TextEdit { + char[] buffer; + usz chars; + usz cursor; +} + +fn String TextEdit.to_string(&te) => (String)te.buffer[:te.chars]; +fn String TextEdit.until_cursor(&te) => (String)te.buffer[:te.cursor]; +fn String TextEdit.from_cursor(&te) => (String)te.buffer[te.cursor..]; + +// implement text editing operations on the buffer +// returns true if the buffer is full +fn bool Ctx.text_edit(&ctx, TextEdit* te) +{ + String in = ctx.get_keys(); + ModKeys mod = ctx.get_mod(); + usz free = te.buffer.len - te.chars; + usz after = te.chars - te.cursor; + + // append text input to the buffer + if (in.len <= free) { + // make space + te.buffer[te.cursor+in.len : after] = te.buffer[te.cursor : after]; + // insert characters + te.buffer[te.cursor : in.len] = in[..]; + // increment characters and cursor + te.chars += in.len; + te.cursor += in.len; + free -= in.len; + } else { + return true; + } + + // handle modkeys + if (te.chars) { + // handle backspace and delete + if (mod.bkspc) { + if (te.cursor > 0) { + // TODO: only delete until punctuation + usz how_many = mod & KMOD_CTRL ? te.until_cursor().prev_word_off() : te.until_cursor().prev_char_off(); + te.buffer[te.cursor-how_many : after] = te.buffer[te.cursor : after]; + te.cursor -= how_many; + te.chars -= how_many; + free += how_many; + } + } + if (mod.del) { + if (after > 0 && te.cursor < te.chars) { + usz how_many = mod & KMOD_CTRL ? te.from_cursor().next_word_off() : te.from_cursor().next_char_off(); + te.buffer[te.cursor : after] = te.buffer[te.cursor+how_many : after]; + te.chars -= how_many; + after -= how_many; + free += how_many; + } + } + + // handle arrow keys + if (mod.left) { + if (te.cursor > 0) { + usz how_many = mod & KMOD_CTRL ? te.until_cursor().prev_word_off() : te.until_cursor().prev_char_off(); + te.cursor -= how_many; + after += how_many; + } + } + if (mod.right) { + if (after > 0) { + usz how_many = mod & KMOD_CTRL ? te.from_cursor().next_word_off() : te.from_cursor().next_char_off(); + te.cursor += how_many; + after -= how_many; + } + } + if (mod.up) { + // TODO + } + if (mod.down) { + // TODO + } + } + + return free == 0; +} + +macro isz char[].next_char_off(b) => grapheme::next_character_break_utf8(b.ptr, b.len); +macro isz char[].next_word_off(b) => grapheme::next_word_break_utf8(b.ptr, b.len); + +fn isz char[].prev_char_off(b) +{ + foreach_r (off, c: b) { + if (c & 0xC0 == 0x80) continue; + return b.len - off; + } + return b.len; +} + +fn isz char[].prev_word_off(b) +{ + for (isz off = b.len-1; off > 0;) { + isz c_off = b[..off].prev_char_off(); + off -= c_off; + if (ascii::is_punct(b[off]) || ascii::is_space(b[off])) return b.len - off - 1; + } + return b.len; +} \ No newline at end of file diff --git a/lib/ugui.c3l/src/widgets/ugui_text.c3 b/lib/ugui.c3l/src/widgets/ugui_text.c3 index 64b1fd8..d4d4d28 100644 --- a/lib/ugui.c3l/src/widgets/ugui_text.c3 +++ b/lib/ugui.c3l/src/widgets/ugui_text.c3 @@ -3,9 +3,9 @@ module ugui; import std::io; struct ElemText { - usz cursor; // cursor offset Id hash; TextSize size; + TextEdit* te; } macro Ctx.text(&ctx, String text, ...) @@ -35,10 +35,10 @@ fn void? Ctx.text_id(&ctx, Id id, String text) 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) + +macro Ctx.text_box(&ctx, Size w, Size h, TextEdit* te, ...) + => ctx.text_box_id(@compute_id($vasplat), w, h, te); +fn ElemEvents? Ctx.text_box_id(&ctx, Id id, Size w, Size h, TextEdit* te) { id = ctx.gen_id(id)!; @@ -46,38 +46,45 @@ 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 = (String)text; + elem.text.te = te; - // layout the text box - elem.bounds = ctx.layout_element(parent, size, style); + Id text_hash = te.to_string().hash(); + if (elem.flags.is_new || elem.text.hash != text_hash) { + elem.text.size = ctx.measure_string(te.to_string())!; + } + elem.text.hash = text_hash; + + elem.layout.w = w; + elem.layout.h = h; + elem.layout.text = elem.text.size; + elem.layout.content_offset = style.margin + style.border + style.padding; + + update_parent_grow(elem, parent); + update_parent_size(elem, parent); // check input and update the text elem.events = ctx.get_elem_events(elem); - if (elem.events.text_input) { - usz l = ctx.input.keyboard.text_len; - char[] t = ctx.input.keyboard.text[..l]; - - if (l != 0 && l < text.len - *text_len) { - text[*text_len..*text_len+l] = t[..]; - *text_len += l; - } - - if (ctx.input.keyboard.modkeys.bkspc) { - *text_len = *text_len > 0 ? *text_len-1 : 0; - } - + if (elem.events.text_input || elem.events.key_press) { + ctx.text_edit(elem.text.te); } - elem.text.cursor = *text_len; // draw the box - short line_height = (short)ctx.font.line_height(); - 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, true)!; - - // TODO: draw cursor + Rect bg_bounds = elem.bounds.pad(style.margin); + Rect text_bounds = elem.bounds.pad(elem.layout.content_offset); + ctx.push_rect(bg_bounds, parent.div.z_index, style)!; + Rect cur; + cur = ctx.layout_string(elem.text.te.to_string(), text_bounds, TOP_LEFT, parent.div.z_index, style.fg, elem.text.te.cursor)!; + // draw the cursor if the element has focus + if (elem.events.has_focus) { + cur.w = 2; + // FIXME: gross hack for when text is empty + if (cur.x == 0 && cur.y == 0) { + cur.x = text_bounds.x; + cur.y = text_bounds.y; + } + ctx.push_rect(cur, parent.div.z_index, &&(Style){.bg = style.fg})!; + } return elem.events; -} -*/ \ No newline at end of file +} \ No newline at end of file diff --git a/resources/style.css b/resources/style.css index 3db3269..3f3cea2 100644 --- a/resources/style.css +++ b/resources/style.css @@ -61,3 +61,14 @@ slider { secondary: #458588ff; accent: #fabd2fff; } + +text-box { + bg: #4a4543ff; + fg: #fbf1c7ff; + primary: #cc241dff; + secondary: #458588ff; + accent: #fabd2fff; + border: 1; + padding: 4; + margin: 2; +} diff --git a/src/main.c3 b/src/main.c3 index b062c08..652019d 100644 --- a/src/main.c3 +++ b/src/main.c3 @@ -120,6 +120,13 @@ fn int main(String[] args) // ========================================================================================== // io::printfn("imported %d styles", ui.import_style_from_file(STYLESHEET_PATH)); + // ========================================================================================== // + // OTHER VARIABLES // + // ========================================================================================== // + TextEdit te; + te.buffer = mem::new_array(char, 256); + defer mem::free(te.buffer); + isz frame; double fps; time::Clock clock; @@ -128,7 +135,6 @@ fn int main(String[] args) Times ui_times; Times draw_times; - // ========================================================================================== // // MAIN LOOP // // ========================================================================================== // @@ -147,11 +153,21 @@ fn int main(String[] args) switch (e.type) { case EVENT_QUIT: quit = true; - case EVENT_KEY_UP: nextcase; + case EVENT_KEY_UP: + ui.input_key_release(); + nextcase; case EVENT_KEY_DOWN: + ui.input_key_press(); + if (e.key.repeat) ui.input_key_repeat(); + mod.rctrl = e.key.key == K_RCTRL ? !!(e.type == EVENT_KEY_DOWN) : mod.rctrl; mod.lctrl = e.key.key == K_LCTRL ? !!(e.type == EVENT_KEY_DOWN) : mod.lctrl; mod.bkspc = e.key.key == K_BACKSPACE ? !!(e.type == EVENT_KEY_DOWN) : mod.bkspc; + mod.del = e.key.key == K_DELETE ? !!(e.type == EVENT_KEY_DOWN) : mod.del; + mod.up = e.key.key == K_UP ? !!(e.type == EVENT_KEY_DOWN) : mod.up; + mod.down = e.key.key == K_DOWN ? !!(e.type == EVENT_KEY_DOWN) : mod.down; + mod.left = e.key.key == K_LEFT ? !!(e.type == EVENT_KEY_DOWN) : mod.left; + mod.right = e.key.key == K_RIGHT ? !!(e.type == EVENT_KEY_DOWN) : mod.right; // pressing ctrl+key or alt+key does not generate a character as such no // TEXT_INPUT event is generated. When those keys are pressed we have to @@ -206,7 +222,7 @@ $switch APPLICATION: $case "debug": debug_app(&ui); $case "calculator": - calculator(&ui); + calculator(&ui, &te); $endswitch // Timings counter @@ -326,7 +342,7 @@ fn void debug_app(ugui::Ctx* ui) import std::os::process; -fn void calculator(ugui::Ctx* ui) +fn void calculator(ugui::Ctx* ui, TextEdit* te) { static char[128] buffer; static usz len; @@ -421,6 +437,9 @@ fn void calculator(ugui::Ctx* ui) ui.slider_hor(ugui::@exact(100), ugui::@exact(20), &f)!!; ui.slider_ver(ugui::@exact(20), ugui::@exact(100), &f)!!; }!!; + ui.@div(ugui::@grow(), ugui::@fit(), anchor: CENTER, scroll_y: true) { + ui.text_box(ugui::@grow(), ugui::@exact(100), te)!!; + }!!; }!!; }!!; }