mirror of
https://github.com/Ed94/Odin.git
synced 2026-08-06 07:38:48 +00:00
Merge pull request #3611 from hstormo/textbox
vendor/microui: nicer textbox
This commit is contained in:
@@ -183,16 +183,17 @@ undo_check :: proc(s: ^State) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// insert text into the edit state - deletes the current selection
|
// 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 {
|
if len(text) == 0 {
|
||||||
return
|
return 0
|
||||||
}
|
}
|
||||||
if has_selection(s) {
|
if has_selection(s) {
|
||||||
selection_delete(s)
|
selection_delete(s)
|
||||||
}
|
}
|
||||||
insert(s, s.selection[0], text)
|
n := insert(s, s.selection[0], text)
|
||||||
offset := s.selection[0] + len(text)
|
offset := s.selection[0] + n
|
||||||
s.selection = {offset, offset}
|
s.selection = {offset, offset}
|
||||||
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
// insert slice of runes into the edit state - deletes the current selection
|
// insert slice of runes into the edit state - deletes the current selection
|
||||||
@@ -206,8 +207,11 @@ input_runes :: proc(s: ^State, text: []rune) {
|
|||||||
offset := s.selection[0]
|
offset := s.selection[0]
|
||||||
for r in text {
|
for r in text {
|
||||||
b, w := utf8.encode_rune(r)
|
b, w := utf8.encode_rune(r)
|
||||||
insert(s, offset, string(b[:w]))
|
n := insert(s, offset, string(b[:w]))
|
||||||
offset += w
|
offset += n
|
||||||
|
if n != w {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
s.selection = {offset, offset}
|
s.selection = {offset, offset}
|
||||||
}
|
}
|
||||||
@@ -219,17 +223,29 @@ input_rune :: proc(s: ^State, r: rune) {
|
|||||||
}
|
}
|
||||||
offset := s.selection[0]
|
offset := s.selection[0]
|
||||||
b, w := utf8.encode_rune(r)
|
b, w := utf8.encode_rune(r)
|
||||||
insert(s, offset, string(b[:w]))
|
n := insert(s, offset, string(b[:w]))
|
||||||
offset += w
|
offset += n
|
||||||
s.selection = {offset, offset}
|
s.selection = {offset, offset}
|
||||||
}
|
}
|
||||||
|
|
||||||
// insert a single rune into the edit state - deletes the current selection
|
// 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)
|
undo_check(s)
|
||||||
if s.builder != nil {
|
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 ok2, _ := inject_at(&s.builder.buf, at, text[:n]); !ok2 {
|
||||||
|
n = 0
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
return len(text)
|
||||||
}
|
}
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove the wanted range withing, usually the selection within byte indices
|
// remove the wanted range withing, usually the selection within byte indices
|
||||||
@@ -263,11 +279,12 @@ selection_delete :: proc(s: ^State) {
|
|||||||
s.selection = {lo, lo}
|
s.selection = {lo, lo}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is_continuation_byte :: proc(b: byte) -> bool {
|
||||||
|
return b >= 0x80 && b < 0xc0
|
||||||
|
}
|
||||||
|
|
||||||
// translates the caret position
|
// translates the caret position
|
||||||
translate_position :: proc(s: ^State, t: Translation) -> int {
|
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 {
|
is_space :: proc(b: byte) -> bool {
|
||||||
return b == ' ' || b == '\t' || b == '\n'
|
return b == ' ' || b == '\t' || b == '\n'
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+137
-23
@@ -29,6 +29,7 @@ import "core:sort"
|
|||||||
import "core:strings"
|
import "core:strings"
|
||||||
import "core:strconv"
|
import "core:strconv"
|
||||||
import "core:math"
|
import "core:math"
|
||||||
|
import textedit "core:text/edit"
|
||||||
|
|
||||||
COMMAND_LIST_SIZE :: #config(MICROUI_COMMAND_LIST_SIZE, 256 * 1024)
|
COMMAND_LIST_SIZE :: #config(MICROUI_COMMAND_LIST_SIZE, 256 * 1024)
|
||||||
ROOT_LIST_SIZE :: #config(MICROUI_ROOT_LIST_SIZE, 32)
|
ROOT_LIST_SIZE :: #config(MICROUI_ROOT_LIST_SIZE, 32)
|
||||||
@@ -51,6 +52,7 @@ Clip :: enum u32 {
|
|||||||
|
|
||||||
Color_Type :: enum u32 {
|
Color_Type :: enum u32 {
|
||||||
TEXT,
|
TEXT,
|
||||||
|
SELECTION_BG,
|
||||||
BORDER,
|
BORDER,
|
||||||
WINDOW_BG,
|
WINDOW_BG,
|
||||||
TITLE_BG,
|
TITLE_BG,
|
||||||
@@ -111,7 +113,16 @@ Key :: enum u32 {
|
|||||||
CTRL,
|
CTRL,
|
||||||
ALT,
|
ALT,
|
||||||
BACKSPACE,
|
BACKSPACE,
|
||||||
|
DELETE,
|
||||||
RETURN,
|
RETURN,
|
||||||
|
LEFT,
|
||||||
|
RIGHT,
|
||||||
|
HOME,
|
||||||
|
END,
|
||||||
|
A,
|
||||||
|
X,
|
||||||
|
C,
|
||||||
|
V,
|
||||||
}
|
}
|
||||||
Key_Set :: distinct bit_set[Key; u32]
|
Key_Set :: distinct bit_set[Key; u32]
|
||||||
|
|
||||||
@@ -235,6 +246,8 @@ Context :: struct {
|
|||||||
key_down_bits, key_pressed_bits: Key_Set,
|
key_down_bits, key_pressed_bits: Key_Set,
|
||||||
_text_store: [MAX_TEXT_STORE]u8,
|
_text_store: [MAX_TEXT_STORE]u8,
|
||||||
text_input: strings.Builder, // uses `_text_store` as backing store with nil_allocator.
|
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) {
|
Stack :: struct($T: typeid, $N: int) {
|
||||||
@@ -260,6 +273,7 @@ default_style := Style{
|
|||||||
scrollbar_size = 12, thumb_size = 8,
|
scrollbar_size = 12, thumb_size = 8,
|
||||||
colors = {
|
colors = {
|
||||||
.TEXT = {230, 230, 230, 255},
|
.TEXT = {230, 230, 230, 255},
|
||||||
|
.SELECTION_BG = {90, 90, 90, 255},
|
||||||
.BORDER = {25, 25, 25, 255},
|
.BORDER = {25, 25, 25, 255},
|
||||||
.WINDOW_BG = {50, 50, 50, 255},
|
.WINDOW_BG = {50, 50, 50, 255},
|
||||||
.TITLE_BG = {25, 25, 25, 255},
|
.TITLE_BG = {25, 25, 25, 255},
|
||||||
@@ -305,12 +319,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^ = {} // zero memory
|
||||||
ctx.draw_frame = default_draw_frame
|
ctx.draw_frame = default_draw_frame
|
||||||
ctx._style = default_style
|
ctx._style = default_style
|
||||||
ctx.style = &ctx._style
|
ctx.style = &ctx._style
|
||||||
ctx.text_input = strings.builder_from_bytes(ctx._text_store[:])
|
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) {
|
begin :: proc(ctx: ^Context) {
|
||||||
@@ -967,23 +985,95 @@ 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) {
|
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})
|
update_control(ctx, id, r, opt | {.HOLD_FOCUS})
|
||||||
|
|
||||||
|
font := ctx.style.font
|
||||||
|
|
||||||
if ctx.focus_id == id {
|
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 */
|
/* handle text input */
|
||||||
n := min(len(textbuf) - textlen^, strings.builder_len(ctx.text_input))
|
if strings.builder_len(ctx.text_input) > 0 {
|
||||||
if n > 0 {
|
if textedit.input_text(&ctx.textbox_state, strings.to_string(ctx.text_input)) > 0 {
|
||||||
copy(textbuf[textlen^:], strings.to_string(ctx.text_input)[:n])
|
textlen^ = strings.builder_len(builder)
|
||||||
textlen^ += n
|
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 textedit.paste(&ctx.textbox_state) {
|
||||||
|
textlen^ = strings.builder_len(builder)
|
||||||
|
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
|
||||||
|
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}
|
res += {.CHANGE}
|
||||||
}
|
}
|
||||||
/* handle backspace */
|
if .DELETE in ctx.key_pressed_bits && textlen^ > 0 {
|
||||||
if .BACKSPACE in ctx.key_pressed_bits && textlen^ > 0 {
|
move: textedit.Translation = .Word_Right if .CTRL in ctx.key_down_bits else .Right
|
||||||
/* skip utf-8 continuation bytes */
|
textedit.delete_to(&ctx.textbox_state, move)
|
||||||
for textlen^ > 0 {
|
textlen^ = strings.builder_len(builder)
|
||||||
textlen^ -= 1
|
|
||||||
if textbuf[textlen^] & 0xc0 != 0x80 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res += {.CHANGE}
|
res += {.CHANGE}
|
||||||
}
|
}
|
||||||
/* handle return */
|
/* handle return */
|
||||||
@@ -991,6 +1081,25 @@ textbox_raw :: proc(ctx: ^Context, textbuf: []u8, textlen: ^int, id: Id, r: Rect
|
|||||||
set_focus(ctx, 0)
|
set_focus(ctx, 0)
|
||||||
res += {.SUBMIT}
|
res += {.SUBMIT}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* handle click/drag */
|
||||||
|
if .LEFT in ctx.mouse_down_bits {
|
||||||
|
idx := textlen^
|
||||||
|
for i in 0..<textlen^ {
|
||||||
|
/* skip continuation bytes */
|
||||||
|
if textbuf[i] >= 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^])
|
textstr := string(textbuf[:textlen^])
|
||||||
@@ -998,16 +1107,21 @@ textbox_raw :: proc(ctx: ^Context, textbuf: []u8, textlen: ^int, id: Id, r: Rect
|
|||||||
/* draw */
|
/* draw */
|
||||||
draw_control_frame(ctx, id, r, .BASE, opt)
|
draw_control_frame(ctx, id, r, .BASE, opt)
|
||||||
if ctx.focus_id == id {
|
if ctx.focus_id == id {
|
||||||
color := ctx.style.colors[.TEXT]
|
text_color := ctx.style.colors[.TEXT]
|
||||||
font := ctx.style.font
|
sel_color := ctx.style.colors[.SELECTION_BG]
|
||||||
textw := ctx.text_width(font, textstr)
|
textw := ctx.text_width(font, textstr)
|
||||||
texth := ctx.text_height(font)
|
texth := ctx.text_height(font)
|
||||||
ofx := r.w - ctx.style.padding - textw - 1
|
headx := ctx.text_width(font, textstr[:ctx.textbox_state.selection[0]])
|
||||||
textx := r.x + min(ofx, ctx.style.padding)
|
tailx := ctx.text_width(font, textstr[:ctx.textbox_state.selection[1]])
|
||||||
texty := r.y + (r.h - texth) / 2
|
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)
|
push_clip_rect(ctx, r)
|
||||||
draw_text(ctx, font, textstr, Vec2{textx, texty}, color)
|
draw_rect(ctx, Rect{textx + min(headx, tailx), texty, abs(headx - tailx), texth}, sel_color)
|
||||||
draw_rect(ctx, Rect{textx + textw, texty, 1, texth}, 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)
|
pop_clip_rect(ctx)
|
||||||
} else {
|
} else {
|
||||||
draw_control_text(ctx, textstr, r, .TEXT, opt)
|
draw_control_text(ctx, textstr, r, .TEXT, opt)
|
||||||
|
|||||||
Reference in New Issue
Block a user