better text edit

This commit is contained in:
Alessandro Mauri 2025-10-19 21:10:51 +02:00
parent 546f3628c7
commit 2eec1fb710
2 changed files with 150 additions and 145 deletions

View File

@ -1,14 +1,12 @@
module ugui; module ugui;
import grapheme; import ugui::textedit::te;
import std::ascii;
struct TextEdit { struct TextEdit {
char[] buffer; char[] buffer;
usz chars; usz chars;
usz cursor; usz cursor;
usz sel_start, sel_end; isz sel_len;
bool selection;
} }
fn String TextEdit.to_string(&te) => (String)te.buffer[:te.chars]; fn String TextEdit.to_string(&te) => (String)te.buffer[:te.chars];
@ -21,157 +19,163 @@ fn bool Ctx.text_edit(&ctx, TextEdit* te)
{ {
String in = ctx.get_keys(); String in = ctx.get_keys();
ModKeys mod = ctx.get_mod(); ModKeys mod = ctx.get_mod();
usz free = te.buffer.len - te.chars;
usz after = te.chars - te.cursor;
// append text input to the buffer te.insert_utf8(in);
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) {
// 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 // handle backspace and delete
if (mod.bkspc) { if (mod.bkspc) {
if (te.cursor > 0) { te.remove_character(false);
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;
free += how_many;
}
} }
if (mod.del) { if (mod.del) {
if (after > 0 && te.cursor < te.chars) { te.remove_character(true);
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;
free += how_many;
}
} }
// handle arrow keys // handle arrow keys
if (mod.left) { if (mod.left) {
if (te.cursor > 0) { te.move_cursor(false, !!(mod & KMOD_SHIFT));
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 (mod.right) {
if (after > 0) { te.move_cursor(true, !!(mod & KMOD_SHIFT));
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
if (mod.up) {
// back up to previous line
if (te.cursor > 0) {
usz curr_line_start = te.until_cursor().rindex_of_char('\n') ?? 0;
usz prev_line_start = curr_line_start ? te.until_cursor()[..curr_line_start-1].rindex_of_char('\n') ?? 0 : 0;
usz curr_line_off = te.cursor - curr_line_start;
usz prev_line_len = curr_line_start - prev_line_start;
te.cursor = prev_line_start + min(curr_line_off-1, prev_line_len);
after = te.chars - te.cursor;
}
}
if (mod.down) {
// down to the next line
if (after > 0) {
usz curr_line_start = te.until_cursor().rindex_of_char('\n') ?? 0;
usz curr_line_off = te.cursor - curr_line_start;
usz next_line_start = te.from_cursor().index_of_char('\n') + te.cursor + 1 ?? te.chars;
usz next_line_end = ((String)te.buffer[next_line_start..]).index_of_char('\n') + next_line_start ?? te.chars;
usz next_line_len = next_line_end - next_line_start;
te.cursor = next_line_start + min(curr_line_off, next_line_len);
after = te.chars - te.cursor;
}
} }
// TODO: up, down
// TODO: mod.home // TODO: mod.home
// TODO: mod.end // TODO: mod.end
// TODO: selection with shift+arrows // TODO: selection with shift+arrows
}
println("cursor: ", te.cursor, " start: ", te.sel_start, " end: ", te.sel_end);
return free == 0; println("cursor: ", te.cursor, " sel_len: ", te.sel_len);
return te.chars < te.buffer.len;
} }
fn usz TextEdit.how_many_fw(&te, ModKeys mod) module ugui::textedit::te;
import std::core::string;
// returns the offset of the next codepoint in the buffer from the cursor
fn usz TextEdit.next_char_off(&te)
{ {
if (mod & KMOD_CTRL) { usz len = min(te.chars - te.cursor, 4);
return te.from_cursor().next_word_off(); if (len == 0) return len;
} else if (te.selection && !(mod & KMOD_SHIFT)) { conv::utf8_to_char32(&te.buffer[te.cursor], &len)!!;
// FIXME: +1 here is not correct, should use next_char_off() return len;
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) // returns the offset of the previous codepoint in the buffer from the cursor
fn usz TextEdit.how_many_bw(&te, ModKeys mod) fn usz TextEdit.prev_char_off(&te)
{
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);
fn isz char[].prev_char_off(b)
{ {
if (te.cursor == 0) return 0;
String b = (String)te.buffer[..te.cursor];
usz len;
foreach_r (off, c: b) { foreach_r (off, c: b) {
if (c & 0xC0 == 0x80) continue; if (c & 0xC0 == 0x80) continue;
return b.len - off; len = b.len - off;
break;
} }
return b.len; // verify the utf8 character
conv::utf8_to_char32(&b[te.cursor - len], &len)!!;
return len;
} }
fn isz char[].prev_word_off(b) // moves the cursor forwards or backwards by one codepoint without exiting the bounds, if select is
// true also change the selection width
fn void TextEdit.move_cursor(&te, bool forward, bool select)
{ {
for (isz off = b.len-1; off > 0;) { // in selection but trying to move without selecting, snap the cursor and reset selection
isz c_off = b[..off].prev_char_off(); if (te.sel_len != 0 && !select) {
off -= c_off; if (te.sel_len > 0 && forward) {
if (ascii::is_punct(b[off]) || ascii::is_space(b[off])) return b.len - off - 1; // selection is in front of the cursor and trying to move right, snap the cursor to the
// end of selection
te.cursor += te.sel_len;
} else if (te.sel_len < 0 && !forward) {
// selection is behind the cursor and trying to move left, snap the cursor to the start
// of selection
te.cursor += te.sel_len;
} }
return b.len; te.sel_len = 0;
} else {
isz off = forward ? te.next_char_off() : -te.prev_char_off();
if (te.cursor + off < 0 || te.cursor + off > te.chars) return;
te.cursor += off;
// if trying to select increment selection width
if (select) {
te.sel_len -= off;
}
}
}
// set the cursor to the exact offset provided, the selection flags controls wether the selection has
// expanded or rese
fn void TextEdit.set_cursor(&te, usz cur, bool select)
{
if (!select) te.sel_len = 0;
if (cur == te.cursor) return;
usz prev_cur = te.cursor;
te.cursor = cur;
if (select) {
te.sel_len += prev_cur - cur;
}
}
fn void TextEdit.delete_selection(&te)
{
if (te.sel_len == 0) return;
usz start = te.sel_len > 0 ? te.cursor : te.cursor + te.sel_len;
usz end = te.sel_len > 0 ? te.cursor + te.sel_len : te.cursor;
usz len = te.chars - end;
te.buffer[start:len] = te.buffer[end:len];
te.cursor -= te.sel_len < 0 ? -te.sel_len : 0;
te.chars -= te.sel_len < 0 ? -te.sel_len : te.sel_len;
te.sel_len = 0;
}
// removes the character before or after the cursor
fn void TextEdit.remove_character(&te, bool forward)
{
// if there is a selection active then delete that selection
if (te.sel_len) {
te.delete_selection();
return;
}
if (te.chars == 0) return;
if (forward) {
usz rem = te.chars - te.cursor;
if (rem > 0) {
usz len = te.next_char_off();
te.buffer[te.cursor:rem-len] = te.buffer[te.cursor+len:rem-len];
te.chars -= len;
}
} else {
if (te.cursor > 0) {
usz len = te.prev_char_off();
te.buffer[te.cursor-len:te.chars-te.cursor] = te.buffer[te.cursor:te.chars-te.cursor];
te.chars -= len;
te.cursor -= len;
}
}
}
// insert a character at the cursor and update the cursor
fn void TextEdit.insert_character(&te, uint cp)
{
char[4] b;
usz len = conv::char32_to_utf8(cp, b[..])!!;
te.insert_utf8((String)b[:len]);
}
fn void TextEdit.insert_utf8(&te, String s)
{
if (s.len == 0) return;
if (s.len + te.chars > te.buffer.len) return;
te.delete_selection();
te.buffer[te.cursor+s.len : te.chars-te.cursor] = te.buffer[te.cursor : te.chars-te.cursor];
te.buffer[te.cursor : s.len] = s[..];
te.chars += s.len;
te.cursor += s.len;
} }

View File

@ -75,19 +75,20 @@ fn ElemEvents? Ctx.text_box_id(&ctx, Id id, Size w, Size h, TextEdit* te, Anchor
Rect text_bounds = elem.bounds.pad(elem.layout.content_offset); Rect text_bounds = elem.bounds.pad(elem.layout.content_offset);
ctx.push_rect(bg_bounds, parent.z_index, style)!; ctx.push_rect(bg_bounds, parent.z_index, style)!;
String s = elem.text.te.to_string(); String s = elem.text.te.to_string();
if (elem.text.te.selection) { if (te.sel_len) {
usz start = elem.text.te.sel_start; usz start = te.sel_len > 0 ? te.cursor : te.cursor + te.sel_len;
usz end = elem.text.te.sel_end; usz end = (te.sel_len > 0 ? te.cursor + te.sel_len : te.cursor) - 1;
ctx.draw_string_selection(s, text_bounds, text_alignment, start, end, parent.z_index, style.accent, reflow)!; 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)!; ctx.layout_string(s, text_bounds, text_alignment, parent.z_index, style.fg, reflow)!;
// draw the cursor if the element has focus // draw the cursor if the element has focus
if (elem.events.has_focus) { if (elem.events.has_focus) {
if (elem.events.mouse_press) { if (elem.events.mouse_press || elem.events.mouse_hold) {
elem.text.te.cursor = ctx.hit_test_string(s, text_bounds, text_alignment, ctx.input.mouse.pos, reflow)!; usz cur = ctx.hit_test_string(s, text_bounds, text_alignment, ctx.input.mouse.pos, reflow)!;
te.set_cursor(cur, elem.events.mouse_hold & !elem.events.mouse_press);
} }
Rect cur = ctx.get_cursor_position(s, text_bounds, text_alignment, elem.text.te.cursor, reflow)!; Rect cur = ctx.get_cursor_position(s, text_bounds, text_alignment, te.cursor, reflow)!;
cur.w = 2; cur.w = 2;
ctx.push_rect(cur, parent.z_index, &&(Style){.bg = style.fg})!; ctx.push_rect(cur, parent.z_index, &&(Style){.bg = style.fg})!;
} }