From 043ddd83a93529067646ee9e1480a1de53260f2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Stormo?= Date: Wed, 22 May 2024 16:57:26 +0200 Subject: [PATCH 1/5] microui: textbox selection --- vendor/microui/microui.odin | 129 ++++++++++++++++++++++++++++++------ 1 file changed, 110 insertions(+), 19 deletions(-) diff --git a/vendor/microui/microui.odin b/vendor/microui/microui.odin index 495289ede..bf1e117db 100644 --- a/vendor/microui/microui.odin +++ b/vendor/microui/microui.odin @@ -29,6 +29,8 @@ import "core:sort" import "core:strings" import "core:strconv" import "core:math" +import "core:mem" +import textedit "core:text/edit" COMMAND_LIST_SIZE :: #config(MICROUI_COMMAND_LIST_SIZE, 256 * 1024) ROOT_LIST_SIZE :: #config(MICROUI_ROOT_LIST_SIZE, 32) @@ -51,6 +53,7 @@ Clip :: enum u32 { Color_Type :: enum u32 { TEXT, + SELECTION_BG, BORDER, WINDOW_BG, TITLE_BG, @@ -111,7 +114,13 @@ Key :: enum u32 { CTRL, ALT, BACKSPACE, + DELETE, RETURN, + LEFT, + RIGHT, + HOME, + END, + A, } Key_Set :: distinct bit_set[Key; u32] @@ -235,6 +244,8 @@ Context :: struct { key_down_bits, key_pressed_bits: Key_Set, _text_store: [MAX_TEXT_STORE]u8, text_input: strings.Builder, // uses `_text_store` as backing store with nil_allocator. + textbox_state: textedit.State, + textbox_offset: i32, } Stack :: struct($T: typeid, $N: int) { @@ -260,6 +271,7 @@ default_style := Style{ scrollbar_size = 12, thumb_size = 8, colors = { .TEXT = {230, 230, 230, 255}, + .SELECTION_BG = {90, 90, 90, 255}, .BORDER = {25, 25, 25, 255}, .WINDOW_BG = {50, 50, 50, 255}, .TITLE_BG = {25, 25, 25, 255}, @@ -967,23 +979,78 @@ checkbox :: proc(ctx: ^Context, label: string, state: ^bool) -> (res: Result_Set textbox_raw :: proc(ctx: ^Context, textbuf: []u8, textlen: ^int, id: Id, r: Rect, opt := Options{}) -> (res: Result_Set) { update_control(ctx, id, r, opt | {.HOLD_FOCUS}) + font := ctx.style.font + if ctx.focus_id == id { + /* create a builder backed by the user's buffer */ + builder := strings.builder_from_bytes(textbuf) + non_zero_resize(&builder.buf, textlen^) + ctx.textbox_state.builder = &builder + if ctx.textbox_state.id != u64(id) { + ctx.textbox_state.id = u64(id) + ctx.textbox_state.selection = {} + } + + /* check selection bounds */ + if ctx.textbox_state.selection[0] > textlen^ || ctx.textbox_state.selection[1] > textlen^ { + ctx.textbox_state.selection = {} + } + /* handle text input */ n := min(len(textbuf) - textlen^, strings.builder_len(ctx.text_input)) if n > 0 { - copy(textbuf[textlen^:], strings.to_string(ctx.text_input)[:n]) - textlen^ += n + s := strings.to_string(ctx.text_input)[:n] + textedit.input_text(&ctx.textbox_state, s) + textlen^ = strings.builder_len(builder) res += {.CHANGE} } - /* handle backspace */ - if .BACKSPACE in ctx.key_pressed_bits && textlen^ > 0 { - /* skip utf-8 continuation bytes */ - for textlen^ > 0 { - textlen^ -= 1 - if textbuf[textlen^] & 0xc0 != 0x80 { - break - } + /* handle ctrl+a */ + if .A in ctx.key_pressed_bits && .CTRL in ctx.key_down_bits && .ALT not_in ctx.key_down_bits { + ctx.textbox_state.selection = {textlen^, 0} + } + /* handle left/right */ + if .LEFT in ctx.key_pressed_bits { + move: textedit.Translation = .Word_Left if .CTRL in ctx.key_down_bits else .Left + if .SHIFT in ctx.key_down_bits { + textedit.select_to(&ctx.textbox_state, move) + } else { + textedit.move_to(&ctx.textbox_state, move) } + } + if .RIGHT in ctx.key_pressed_bits { + move: textedit.Translation = .Word_Right if .CTRL in ctx.key_down_bits else .Right + if .SHIFT in ctx.key_down_bits { + textedit.select_to(&ctx.textbox_state, move) + } else { + textedit.move_to(&ctx.textbox_state, move) + } + } + /* handle home/end */ + if .HOME in ctx.key_pressed_bits { + if .SHIFT in ctx.key_down_bits { + textedit.select_to(&ctx.textbox_state, .Start) + } else { + textedit.move_to(&ctx.textbox_state, .Start) + } + } + if .END in ctx.key_pressed_bits { + if .SHIFT in ctx.key_down_bits { + textedit.select_to(&ctx.textbox_state, .End) + } else { + textedit.move_to(&ctx.textbox_state, .End) + } + } + /* handle backspace/delete */ + if .BACKSPACE in ctx.key_pressed_bits && textlen^ > 0 { + move: textedit.Translation = .Word_Left if .CTRL in ctx.key_down_bits else .Left + textedit.delete_to(&ctx.textbox_state, move) + textlen^ = strings.builder_len(builder) + res += {.CHANGE} + } + if .DELETE in ctx.key_pressed_bits && textlen^ > 0 { + move: textedit.Translation = .Word_Right if .CTRL in ctx.key_down_bits else .Right + textedit.delete_to(&ctx.textbox_state, move) + textlen^ = strings.builder_len(builder) res += {.CHANGE} } /* handle return */ @@ -991,6 +1058,25 @@ textbox_raw :: proc(ctx: ^Context, textbuf: []u8, textlen: ^int, id: Id, r: Rect set_focus(ctx, 0) res += {.SUBMIT} } + + /* handle click/drag */ + if .LEFT in ctx.mouse_down_bits { + idx := textlen^ + for i in 0..= 0x80 && textbuf[i] < 0xc0 { + continue + } + if ctx.mouse_pos.x < r.x + ctx.textbox_offset + ctx.text_width(font, string(textbuf[:i])) { + idx = i + break + } + } + ctx.textbox_state.selection[0] = idx + if .LEFT in ctx.mouse_pressed_bits && .SHIFT not_in ctx.key_down_bits { + ctx.textbox_state.selection[1] = idx + } + } } textstr := string(textbuf[:textlen^]) @@ -998,16 +1084,21 @@ textbox_raw :: proc(ctx: ^Context, textbuf: []u8, textlen: ^int, id: Id, r: Rect /* draw */ draw_control_frame(ctx, id, r, .BASE, opt) if ctx.focus_id == id { - color := ctx.style.colors[.TEXT] - font := ctx.style.font - textw := ctx.text_width(font, textstr) - texth := ctx.text_height(font) - ofx := r.w - ctx.style.padding - textw - 1 - textx := r.x + min(ofx, ctx.style.padding) - texty := r.y + (r.h - texth) / 2 + text_color := ctx.style.colors[.TEXT] + sel_color := ctx.style.colors[.SELECTION_BG] + textw := ctx.text_width(font, textstr) + texth := ctx.text_height(font) + headx := ctx.text_width(font, textstr[:ctx.textbox_state.selection[0]]) + tailx := ctx.text_width(font, textstr[:ctx.textbox_state.selection[1]]) + ofmin := max(ctx.style.padding - headx, r.w - textw - ctx.style.padding) + ofmax := min(r.w - headx - ctx.style.padding, ctx.style.padding) + ctx.textbox_offset = clamp(ctx.textbox_offset, ofmin, ofmax) + textx := r.x + ctx.textbox_offset + texty := r.y + (r.h - texth) / 2 push_clip_rect(ctx, r) - draw_text(ctx, font, textstr, Vec2{textx, texty}, color) - draw_rect(ctx, Rect{textx + textw, texty, 1, texth}, color) + draw_rect(ctx, Rect{textx + min(headx, tailx), texty, abs(headx - tailx), texth}, sel_color) + draw_text(ctx, font, textstr, Vec2{textx, texty}, text_color) + draw_rect(ctx, Rect{textx + headx, texty, 1, texth}, text_color) pop_clip_rect(ctx) } else { draw_control_text(ctx, textstr, r, .TEXT, opt) From f411fcedb0ac259810600cc7d1e05859fa9c6b71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Stormo?= Date: Wed, 22 May 2024 17:44:47 +0200 Subject: [PATCH 2/5] microui: textbox cut/copy/paste --- vendor/microui/microui.odin | 46 ++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/vendor/microui/microui.odin b/vendor/microui/microui.odin index bf1e117db..f98fa1807 100644 --- a/vendor/microui/microui.odin +++ b/vendor/microui/microui.odin @@ -121,6 +121,9 @@ Key :: enum u32 { HOME, END, A, + X, + C, + V, } Key_Set :: distinct bit_set[Key; u32] @@ -317,12 +320,16 @@ default_draw_frame :: proc(ctx: ^Context, rect: Rect, colorid: Color_Type) { } } -init :: proc(ctx: ^Context) { +init :: proc(ctx: ^Context, set_clipboard: proc(user_data: rawptr, text: string) -> (ok: bool), get_clipboard: proc(user_data: rawptr) -> (text: string, ok: bool), clipboard_user_data: rawptr) { ctx^ = {} // zero memory ctx.draw_frame = default_draw_frame ctx._style = default_style ctx.style = &ctx._style ctx.text_input = strings.builder_from_bytes(ctx._text_store[:]) + + ctx.textbox_state.set_clipboard = set_clipboard + ctx.textbox_state.get_clipboard = get_clipboard + ctx.textbox_state.clipboard_user_data = clipboard_user_data } begin :: proc(ctx: ^Context) { @@ -977,6 +984,16 @@ checkbox :: proc(ctx: ^Context, label: string, state: ^bool) -> (res: Result_Set } textbox_raw :: proc(ctx: ^Context, textbuf: []u8, textlen: ^int, id: Id, r: Rect, opt := Options{}) -> (res: Result_Set) { + try_input_string :: proc(state: ^textedit.State, textbuf: []u8, textlen: ^int, s: string) -> bool { + n := min(len(textbuf) - textlen^, len(s)) + if n > 0 { + textedit.input_text(state, s[:n]) + textlen^ = strings.builder_len(state.builder^) + return true + } + return false + } + update_control(ctx, id, r, opt | {.HOLD_FOCUS}) font := ctx.style.font @@ -997,17 +1014,30 @@ textbox_raw :: proc(ctx: ^Context, textbuf: []u8, textlen: ^int, id: Id, r: Rect } /* handle text input */ - n := min(len(textbuf) - textlen^, strings.builder_len(ctx.text_input)) - if n > 0 { - s := strings.to_string(ctx.text_input)[:n] - textedit.input_text(&ctx.textbox_state, s) - textlen^ = strings.builder_len(builder) - res += {.CHANGE} - } + if try_input_string(&ctx.textbox_state, textbuf, textlen, strings.to_string(ctx.text_input)) do res += {.CHANGE} /* handle ctrl+a */ if .A in ctx.key_pressed_bits && .CTRL in ctx.key_down_bits && .ALT not_in ctx.key_down_bits { ctx.textbox_state.selection = {textlen^, 0} } + /* handle ctrl+x */ + if .X in ctx.key_pressed_bits && .CTRL in ctx.key_down_bits && .ALT not_in ctx.key_down_bits { + if textedit.cut(&ctx.textbox_state) { + textlen^ = strings.builder_len(builder) + res += {.CHANGE} + } + } + /* handle ctrl+c */ + if .C in ctx.key_pressed_bits && .CTRL in ctx.key_down_bits && .ALT not_in ctx.key_down_bits { + textedit.copy(&ctx.textbox_state) + } + /* handle ctrl+v */ + if .V in ctx.key_pressed_bits && .CTRL in ctx.key_down_bits && .ALT not_in ctx.key_down_bits { + if ctx.textbox_state.get_clipboard != nil { + if s, ok := ctx.textbox_state.get_clipboard(ctx.textbox_state.clipboard_user_data); ok { + if try_input_string(&ctx.textbox_state, textbuf, textlen, s) do res += {.CHANGE} + } + } + } /* handle left/right */ if .LEFT in ctx.key_pressed_bits { move: textedit.Translation = .Word_Left if .CTRL in ctx.key_down_bits else .Left From d3bbe29faa81ac215cea9bbaf91099ffd9bc307b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Stormo?= Date: Thu, 23 May 2024 23:22:32 +0200 Subject: [PATCH 3/5] text_edit: better handle failure to resize underlying buffer --- core/text/edit/text_edit.odin | 44 ++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/core/text/edit/text_edit.odin b/core/text/edit/text_edit.odin index 6f21c9860..c1d24a781 100644 --- a/core/text/edit/text_edit.odin +++ b/core/text/edit/text_edit.odin @@ -7,6 +7,7 @@ package text_edit */ import "base:runtime" +import "core:fmt" import "core:time" import "core:mem" import "core:strings" @@ -183,16 +184,17 @@ undo_check :: proc(s: ^State) { } // insert text into the edit state - deletes the current selection -input_text :: proc(s: ^State, text: string) { +input_text :: proc(s: ^State, text: string) -> int { if len(text) == 0 { - return + return 0 } if has_selection(s) { selection_delete(s) } - insert(s, s.selection[0], text) - offset := s.selection[0] + len(text) + n := insert(s, s.selection[0], text) + offset := s.selection[0] + n s.selection = {offset, offset} + return n } // insert slice of runes into the edit state - deletes the current selection @@ -206,8 +208,11 @@ input_runes :: proc(s: ^State, text: []rune) { offset := s.selection[0] for r in text { b, w := utf8.encode_rune(r) - insert(s, offset, string(b[:w])) - offset += w + n := insert(s, offset, string(b[:w])) + offset += n + if n != w { + break + } } s.selection = {offset, offset} } @@ -219,17 +224,29 @@ input_rune :: proc(s: ^State, r: rune) { } offset := s.selection[0] b, w := utf8.encode_rune(r) - insert(s, offset, string(b[:w])) - offset += w + n := insert(s, offset, string(b[:w])) + offset += n s.selection = {offset, offset} } // insert a single rune into the edit state - deletes the current selection -insert :: proc(s: ^State, at: int, text: string) { +insert :: proc(s: ^State, at: int, text: string) -> int { undo_check(s) if s.builder != nil { - inject_at(&s.builder.buf, at, text) + if ok, _ := inject_at(&s.builder.buf, at, text); !ok { + n := cap(s.builder.buf) - len(s.builder.buf) + assert(n < len(text)) + for is_continuation_byte(text[n]) { + n -= 1 + } + if ok, _ := inject_at(&s.builder.buf, at, text[:n]); !ok { + n = 0 + } + return n + } + return len(text) } + return 0 } // remove the wanted range withing, usually the selection within byte indices @@ -263,11 +280,12 @@ selection_delete :: proc(s: ^State) { s.selection = {lo, lo} } +is_continuation_byte :: proc(b: byte) -> bool { + return b >= 0x80 && b < 0xc0 +} + // translates the caret position translate_position :: proc(s: ^State, t: Translation) -> int { - is_continuation_byte :: proc(b: byte) -> bool { - return b >= 0x80 && b < 0xc0 - } is_space :: proc(b: byte) -> bool { return b == ' ' || b == '\t' || b == '\n' } From ee79c409b4d1bdfa6768c6b3b81612cd8f3826b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Stormo?= Date: Thu, 23 May 2024 23:28:58 +0200 Subject: [PATCH 4/5] microui: use the text_edit procs --- vendor/microui/microui.odin | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/vendor/microui/microui.odin b/vendor/microui/microui.odin index f98fa1807..89bebe4aa 100644 --- a/vendor/microui/microui.odin +++ b/vendor/microui/microui.odin @@ -984,16 +984,6 @@ checkbox :: proc(ctx: ^Context, label: string, state: ^bool) -> (res: Result_Set } textbox_raw :: proc(ctx: ^Context, textbuf: []u8, textlen: ^int, id: Id, r: Rect, opt := Options{}) -> (res: Result_Set) { - try_input_string :: proc(state: ^textedit.State, textbuf: []u8, textlen: ^int, s: string) -> bool { - n := min(len(textbuf) - textlen^, len(s)) - if n > 0 { - textedit.input_text(state, s[:n]) - textlen^ = strings.builder_len(state.builder^) - return true - } - return false - } - update_control(ctx, id, r, opt | {.HOLD_FOCUS}) font := ctx.style.font @@ -1014,7 +1004,12 @@ textbox_raw :: proc(ctx: ^Context, textbuf: []u8, textlen: ^int, id: Id, r: Rect } /* handle text input */ - if try_input_string(&ctx.textbox_state, textbuf, textlen, strings.to_string(ctx.text_input)) do res += {.CHANGE} + if strings.builder_len(ctx.text_input) > 0 { + if textedit.input_text(&ctx.textbox_state, strings.to_string(ctx.text_input)) > 0 { + textlen^ = strings.builder_len(builder) + res += {.CHANGE} + } + } /* handle ctrl+a */ if .A in ctx.key_pressed_bits && .CTRL in ctx.key_down_bits && .ALT not_in ctx.key_down_bits { ctx.textbox_state.selection = {textlen^, 0} @@ -1032,10 +1027,9 @@ textbox_raw :: proc(ctx: ^Context, textbuf: []u8, textlen: ^int, id: Id, r: Rect } /* handle ctrl+v */ if .V in ctx.key_pressed_bits && .CTRL in ctx.key_down_bits && .ALT not_in ctx.key_down_bits { - if ctx.textbox_state.get_clipboard != nil { - if s, ok := ctx.textbox_state.get_clipboard(ctx.textbox_state.clipboard_user_data); ok { - if try_input_string(&ctx.textbox_state, textbuf, textlen, s) do res += {.CHANGE} - } + if textedit.paste(&ctx.textbox_state) { + textlen^ = strings.builder_len(builder) + res += {.CHANGE} } } /* handle left/right */ From 4328562e2c376d4828a8ccbe24d451a0c112ed1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Stormo?= Date: Thu, 23 May 2024 23:50:26 +0200 Subject: [PATCH 5/5] Satisfy -vet --- core/text/edit/text_edit.odin | 3 +-- vendor/microui/microui.odin | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/core/text/edit/text_edit.odin b/core/text/edit/text_edit.odin index c1d24a781..a4f8c06b9 100644 --- a/core/text/edit/text_edit.odin +++ b/core/text/edit/text_edit.odin @@ -7,7 +7,6 @@ package text_edit */ import "base:runtime" -import "core:fmt" import "core:time" import "core:mem" import "core:strings" @@ -239,7 +238,7 @@ insert :: proc(s: ^State, at: int, text: string) -> int { for is_continuation_byte(text[n]) { n -= 1 } - if ok, _ := inject_at(&s.builder.buf, at, text[:n]); !ok { + if ok2, _ := inject_at(&s.builder.buf, at, text[:n]); !ok2 { n = 0 } return n diff --git a/vendor/microui/microui.odin b/vendor/microui/microui.odin index 89bebe4aa..cf39e2f55 100644 --- a/vendor/microui/microui.odin +++ b/vendor/microui/microui.odin @@ -29,7 +29,6 @@ import "core:sort" import "core:strings" import "core:strconv" import "core:math" -import "core:mem" import textedit "core:text/edit" COMMAND_LIST_SIZE :: #config(MICROUI_COMMAND_LIST_SIZE, 256 * 1024)