diff --git a/lib/ugui.c3l/src/string.c3 b/lib/ugui.c3l/src/string.c3 index ef74dcf..6c9d21f 100644 --- a/lib/ugui.c3l/src/string.c3 +++ b/lib/ugui.c3l/src/string.c3 @@ -244,6 +244,7 @@ fn Rect? GlyphIterator.next(&self) if (self.anchor == TOP_LEFT) { self.o.x = self.bounds.x; self.o.y += self.line_height + self.line_gap; + self.line_idx++; } break; case self.cp == '\t': @@ -254,10 +255,11 @@ fn Rect? GlyphIterator.next(&self) b = self.gp.bounds().off(self.o); b.y += self.baseline; if (self.anchor == TOP_LEFT) { - if (self.o.x == self.bounds.x) self.bounds.x -= self.gp.ox; + //if (self.o.x == self.bounds.x) self.bounds.x -= self.gp.ox; if (self.reflow && b.bottom_right().x > self.bounds.bottom_right().x) { self.o.x = self.bounds.x - self.gp.ox; self.o.y += self.line_height + self.line_gap; + self.line_idx++; b = self.gp.bounds().off(self.o); b.y += self.baseline; } @@ -286,7 +288,11 @@ fn bool GlyphIterator.has_next(&self) return true; } -fn usz GlyphIterator.current_offset(&self) => self.lines[self.line_idx].start + self.line_off; +fn usz GlyphIterator.current_offset(&self) +{ + if (self.anchor == TOP_LEFT) return self.line_off; + return self.lines[self.line_idx].start + self.line_off; +} // layout a string inside a bounding box, following the given alignment (anchor). @@ -315,6 +321,7 @@ fn void? Ctx.layout_string(&ctx, String text, Rect bounds, Anchor anchor, int z_ // ctx.dbg_rect(str_bounds.off(bounds.position())); } + // ---------------------------------------------------------------------------------- // // CURSOR AND MOUSE // // ---------------------------------------------------------------------------------- // @@ -435,6 +442,55 @@ fn usz? Ctx.hit_test_string(&ctx, String text, Rect bounds, Anchor anchor, Point return text.len; } +// TODO: implement a function `layout_string_with_selection` to avoid iterating over the string twice +fn void? Ctx.draw_string_selection(&ctx, String text, Rect bounds, Anchor anchor, usz start, usz end, int z_index, Color hue, bool reflow = false) +{ + if (text == "") return; + if (bounds.w <= 0 || bounds.h <= 0) return; + + if (start > end) @swap(start, end); + + // Ensure start < end + if (start > end) { + usz temp = start; + start = end; + end = temp; + } + + ctx.push_scissor(bounds, z_index)!; + + Font* font = &ctx.font; + + GlyphIterator gi; + gi.init(tmem, text, bounds, font, anchor, reflow, TAB_SIZE)!; + + Rect sel_rect = { .h = gi.line_height }; // selection rect + isz sel_line = -1; // selection line + while (gi.has_next()) { + Rect b = gi.next()!; + usz off = gi.current_offset()-1; + isz line = gi.line_idx; + bool in_selection = start <= off && off <= end; + + if (in_selection && line != sel_line) { + if (sel_line != -1) { + ctx.push_rect(sel_rect, z_index, &&{.bg = hue})!; + } + sel_rect = {.x = gi.o.x - b.w, .y = gi.o.y, .w = 0, .h = gi.line_height}; + sel_line = line; + } + if (in_selection) { + sel_rect.w = gi.o.x - sel_rect.x; + if (gi.cp == '\n') sel_rect.w += gi.space_width; + } + + if (off > end) break; + } + ctx.push_rect(sel_rect, z_index, &&{.bg = hue})!; + + ctx.reset_scissor(z_index)!; +} + // ---------------------------------------------------------------------------------- // // TEXT MEASUREMENT // diff --git a/lib/ugui.c3l/src/textedit.c3 b/lib/ugui.c3l/src/textedit.c3 index 761e25e..a47b9bd 100644 --- a/lib/ugui.c3l/src/textedit.c3 +++ b/lib/ugui.c3l/src/textedit.c3 @@ -7,6 +7,8 @@ struct TextEdit { char[] buffer; usz chars; usz cursor; + usz sel_start, sel_end; + bool selection; } fn String TextEdit.to_string(&te) => (String)te.buffer[:te.chars]; @@ -38,11 +40,15 @@ fn bool Ctx.text_edit(&ctx, TextEdit* te) // handle modkeys if (te.chars) { + // if not already in selection update the selection start/end to the cursor positon + if (!te.selection) { + te.sel_start = te.sel_end = te.cursor; + } + // 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(); + usz how_many = te.how_many_bw(mod); te.buffer[te.cursor-how_many : after] = te.buffer[te.cursor : after]; te.cursor -= how_many; te.chars -= how_many; @@ -51,7 +57,7 @@ fn bool Ctx.text_edit(&ctx, TextEdit* te) } 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(); + usz how_many = te.how_many_fw(mod); te.buffer[te.cursor : after] = te.buffer[te.cursor+how_many : after]; te.chars -= how_many; after -= how_many; @@ -62,16 +68,31 @@ fn bool Ctx.text_edit(&ctx, TextEdit* te) // 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(); + usz how_many = te.how_many_bw(mod); te.cursor -= how_many; after += how_many; + + if (mod & KMOD_SHIFT) { + te.selection = true; + if (!te.selection) te.sel_start = te.cursor; + te.sel_end = te.cursor; + } else { + te.selection = false; + } } } if (mod.right) { if (after > 0) { - usz how_many = mod & KMOD_CTRL ? te.from_cursor().next_word_off() : te.from_cursor().next_char_off(); + usz how_many = te.how_many_fw(mod); te.cursor += how_many; after -= how_many; + + if (mod & KMOD_SHIFT) { + te.sel_end = te.cursor-1; + te.selection = true; + } else { + te.selection = false; + } } } // FIXME: this still doesn't work @@ -103,10 +124,36 @@ fn bool Ctx.text_edit(&ctx, TextEdit* te) // TODO: mod.end // TODO: selection with shift+arrows } + println("cursor: ", te.cursor, " start: ", te.sel_start, " end: ", te.sel_end); return free == 0; } +fn usz TextEdit.how_many_fw(&te, ModKeys mod) +{ + if (mod & KMOD_CTRL) { + return te.from_cursor().next_word_off(); + } else if (te.selection && !(mod & KMOD_SHIFT)) { + // FIXME: +1 here is not correct, should use next_char_off() + return te.sel_start > te.sel_end ? te.sel_end - te.sel_start + 1 : te.from_cursor().next_char_off(); + } else { + return te.from_cursor().next_char_off(); + } +} + +// how many characters to jump backwards (left arrow) +fn usz TextEdit.how_many_bw(&te, ModKeys mod) +{ + if (mod & KMOD_CTRL) { + return te.until_cursor().prev_word_off(); + } else if (te.selection && !(mod & KMOD_SHIFT)){ + // FIXME: +1 here is not correct, should use prev_char_off() + return te.sel_start < te.sel_end ? te.sel_end - te.sel_start + 1 : te.until_cursor().prev_char_off(); + } else { + return te.until_cursor().prev_char_off(); + } +} + 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); diff --git a/lib/ugui.c3l/src/widgets/text.c3 b/lib/ugui.c3l/src/widgets/text.c3 index a814299..40d46d6 100644 --- a/lib/ugui.c3l/src/widgets/text.c3 +++ b/lib/ugui.c3l/src/widgets/text.c3 @@ -71,19 +71,23 @@ fn ElemEvents? Ctx.text_box_id(&ctx, Id id, Size w, Size h, TextEdit* te, Anchor ctx.text_edit(elem.text.te); } - // draw the box - Rect bg_bounds = elem.bounds.pad(style.margin); Rect text_bounds = elem.bounds.pad(elem.layout.content_offset); ctx.push_rect(bg_bounds, parent.z_index, style)!; - ctx.layout_string(elem.text.te.to_string(), text_bounds, text_alignment, parent.z_index, style.fg, reflow)!; + String s = elem.text.te.to_string(); + if (elem.text.te.selection) { + usz start = elem.text.te.sel_start; + usz end = elem.text.te.sel_end; + ctx.draw_string_selection(s, text_bounds, text_alignment, start, end, parent.z_index, style.accent, reflow)!; + } + ctx.layout_string(s, text_bounds, text_alignment, parent.z_index, style.fg, reflow)!; // draw the cursor if the element has focus if (elem.events.has_focus) { if (elem.events.mouse_press) { - elem.text.te.cursor = ctx.hit_test_string(elem.text.te.to_string(), text_bounds, text_alignment, ctx.input.mouse.pos, reflow)!; + elem.text.te.cursor = ctx.hit_test_string(s, text_bounds, text_alignment, ctx.input.mouse.pos, reflow)!; } - Rect cur = ctx.get_cursor_position(elem.text.te.to_string(), text_bounds, text_alignment, elem.text.te.cursor, reflow)!; + Rect cur = ctx.get_cursor_position(s, text_bounds, text_alignment, elem.text.te.cursor, reflow)!; cur.w = 2; ctx.push_rect(cur, parent.z_index, &&(Style){.bg = style.fg})!; }