feat: improve MTK input boxes, right-click context menus

This commit is contained in:
2026-05-06 08:03:14 +02:00
parent f19e97966d
commit 8997164203
15 changed files with 1337 additions and 256 deletions
+667 -7
View File
@@ -6,7 +6,10 @@
#pragma once
#include <Api/Syscall.hpp>
#include "gui/canvas.hpp"
#include "gui/clipboard.hpp"
#include "gui/font.hpp"
#include "gui/mtk/theme.hpp"
@@ -26,6 +29,52 @@ struct WidgetState {
bool enabled;
};
struct ContextMenuState {
bool open;
int x;
int y;
int hover;
};
struct ContextMenuItem {
const char* label;
int id;
bool enabled;
};
enum ContextMenuResult : int {
CONTEXT_MENU_NONE = -1,
CONTEXT_MENU_CONSUMED = -2,
CONTEXT_MENU_DISMISSED = -3,
};
static constexpr int CONTEXT_MENU_DEFAULT_W = 140;
static constexpr int CONTEXT_MENU_ITEM_H = 24;
static constexpr int CONTEXT_MENU_PAD_Y = 4;
struct TextInputState {
int cursor;
int selection_anchor;
bool dragging;
ContextMenuState context;
};
using TextInputCharFilter = bool (*)(char ch, void* userdata);
enum TextInputResult : uint8_t {
TEXT_INPUT_NONE = 0,
TEXT_INPUT_CONSUMED = 1,
TEXT_INPUT_CHANGED = 2,
};
enum TextInputContextItem : int {
TEXT_INPUT_CTX_CUT = 0,
TEXT_INPUT_CTX_COPY,
TEXT_INPUT_CTX_PASTE,
TEXT_INPUT_CTX_DELETE,
TEXT_INPUT_CTX_COUNT,
};
inline WidgetState widget_state(bool active = false, bool hovered = false, bool enabled = true) {
return {active, hovered, enabled};
}
@@ -36,12 +85,595 @@ struct ButtonColors {
Color fg;
};
inline void context_menu_reset(ContextMenuState& state) {
state.open = false;
state.x = 0;
state.y = 0;
state.hover = -1;
}
inline void context_menu_open(ContextMenuState& state, int x, int y) {
state.open = true;
state.x = x;
state.y = y;
state.hover = -1;
}
inline void context_menu_close(ContextMenuState& state) {
state.open = false;
state.hover = -1;
}
inline int context_menu_height(int item_count,
int item_h = CONTEXT_MENU_ITEM_H) {
return gui_max(item_count, 0) * item_h + CONTEXT_MENU_PAD_Y * 2;
}
inline Rect context_menu_rect(const ContextMenuState& state,
int item_count,
int view_w,
int view_h,
int menu_w = CONTEXT_MENU_DEFAULT_W,
int item_h = CONTEXT_MENU_ITEM_H) {
int menu_h = context_menu_height(item_count, item_h);
int x = state.x;
int y = state.y;
if (x + menu_w > view_w) x = view_w - menu_w;
if (y + menu_h > view_h) y = view_h - menu_h;
if (x < 0) x = 0;
if (y < 0) y = 0;
return {x, y, menu_w, menu_h};
}
inline int context_menu_hit_item(const ContextMenuState& state,
int item_count,
int mx,
int my,
int view_w,
int view_h,
int menu_w = CONTEXT_MENU_DEFAULT_W,
int item_h = CONTEXT_MENU_ITEM_H) {
if (!state.open || item_count <= 0) return -1;
Rect menu = context_menu_rect(state, item_count, view_w, view_h,
menu_w, item_h);
Rect items = {menu.x + 2, menu.y + CONTEXT_MENU_PAD_Y,
menu.w - 4, item_count * item_h};
if (!items.contains(mx, my)) return -1;
return (my - items.y) / item_h;
}
inline int context_menu_handle_mouse(ContextMenuState& state,
const ContextMenuItem* items,
int item_count,
int mx,
int my,
uint8_t buttons,
uint8_t prev_buttons,
int view_w,
int view_h,
int menu_w = CONTEXT_MENU_DEFAULT_W,
int item_h = CONTEXT_MENU_ITEM_H) {
if (!state.open) return CONTEXT_MENU_NONE;
bool left_pressed = (buttons & 1) && !(prev_buttons & 1);
bool right_pressed = (buttons & 2) && !(prev_buttons & 2);
state.hover = context_menu_hit_item(state, item_count, mx, my,
view_w, view_h, menu_w, item_h);
if (left_pressed) {
int hover = state.hover;
context_menu_close(state);
if (hover >= 0 && hover < item_count && items && items[hover].enabled)
return items[hover].id;
return CONTEXT_MENU_DISMISSED;
}
if (right_pressed) {
context_menu_close(state);
return CONTEXT_MENU_DISMISSED;
}
return CONTEXT_MENU_CONSUMED;
}
inline int context_menu_label_height(TrueTypeFont* font = nullptr,
int font_size = 0) {
if (font && font->valid && font_size > 0)
return font->get_line_height(font_size);
return system_font_height();
}
inline void context_menu_draw_label(Canvas& c,
int x,
int y,
const char* label,
Color color,
TrueTypeFont* font = nullptr,
int font_size = 0) {
if (font && font->valid && font_size > 0) {
font->draw_to_buffer(c.pixels, c.w, c.h, x, y, label, color, font_size);
return;
}
c.text(x, y, label, color);
}
inline void draw_context_menu(Canvas& c,
const ContextMenuState& state,
const ContextMenuItem* items,
int item_count,
const Theme& theme,
int menu_w = CONTEXT_MENU_DEFAULT_W,
int item_h = CONTEXT_MENU_ITEM_H,
TrueTypeFont* font = nullptr,
int font_size = 0) {
if (!state.open || !items || item_count <= 0) return;
Rect menu = context_menu_rect(state, item_count, c.w, c.h,
menu_w, item_h);
c.fill_rect(menu.x + 2, menu.y + 2, menu.w, menu.h,
Color::from_rgb(0x80, 0x80, 0x80));
c.fill_rounded_rect(menu.x, menu.y, menu.w, menu.h, 4,
colors::MENU_BG);
c.rect(menu.x, menu.y, menu.w, menu.h, theme.border);
int fh = context_menu_label_height(font, font_size);
for (int i = 0; i < item_count; i++) {
Rect item = {menu.x + 2, menu.y + CONTEXT_MENU_PAD_Y + i * item_h,
menu.w - 4, item_h};
if (state.hover == i)
c.fill_rect(item.x, item.y, item.w, item.h, colors::MENU_HOVER);
Color label_color = items[i].enabled ? theme.text : theme.text_muted;
context_menu_draw_label(c, menu.x + 12, item.y + (item.h - fh) / 2,
items[i].label, label_color, font, font_size);
}
}
inline int text_len(const char* text) {
int len = 0;
while (text && text[len]) len++;
return len;
}
inline int text_input_len(const char* text, int cap = 0) {
int len = 0;
int limit = cap > 0 ? cap - 1 : 0x7FFFFFFF;
while (text && text[len] && len < limit) len++;
return len;
}
inline int text_input_clamp_cursor(int cursor, int len) {
return gui_clamp(cursor, 0, gui_max(len, 0));
}
inline void text_input_reset(TextInputState& state, int cursor = 0) {
state.cursor = gui_max(cursor, 0);
state.selection_anchor = state.cursor;
state.dragging = false;
context_menu_reset(state.context);
}
inline void text_input_set_cursor(TextInputState& state, int len, int cursor, bool selecting = false) {
state.cursor = text_input_clamp_cursor(cursor, len);
state.selection_anchor = selecting
? text_input_clamp_cursor(state.selection_anchor, len)
: state.cursor;
}
inline bool text_input_has_selection(const TextInputState& state) {
return state.cursor != state.selection_anchor;
}
inline int text_input_selection_start(const TextInputState& state, int len) {
int cursor = text_input_clamp_cursor(state.cursor, len);
int anchor = text_input_clamp_cursor(state.selection_anchor, len);
return cursor < anchor ? cursor : anchor;
}
inline int text_input_selection_end(const TextInputState& state, int len) {
int cursor = text_input_clamp_cursor(state.cursor, len);
int anchor = text_input_clamp_cursor(state.selection_anchor, len);
return cursor > anchor ? cursor : anchor;
}
inline bool text_input_has_selection(const TextInputState& state, const char* text, int cap = 0) {
int len = text_input_len(text, cap);
return text_input_selection_start(state, len) != text_input_selection_end(state, len);
}
inline int text_input_prefix_width(const char* text, int count, bool masked) {
char prefix[512];
int len = 0;
while (text && text[len] && len < count && len < (int)sizeof(prefix) - 1) {
prefix[len] = masked ? '*' : text[len];
len++;
}
prefix[len] = '\0';
return text_width(prefix);
}
inline int text_input_index_from_x(const Rect& bounds, const char* value,
int mx, bool masked = false) {
int rel_x = mx - (bounds.x + 8);
if (rel_x <= 0) return 0;
int len = text_input_len(value);
int prev_w = 0;
for (int i = 0; i < len; i++) {
int cur_w = text_input_prefix_width(value, i + 1, masked);
int midpoint = prev_w + (cur_w - prev_w) / 2;
if (rel_x < midpoint) return i;
prev_w = cur_w;
}
return len;
}
inline int text_input_mouse(TextInputState& state,
const Rect& bounds,
const char* value,
int mx, int my,
uint8_t buttons,
uint8_t prev_buttons,
bool focused,
bool masked = false) {
bool left_down = (buttons & 1) != 0;
bool left_pressed = (buttons & 1) && !(prev_buttons & 1);
bool left_released = !(buttons & 1) && (prev_buttons & 1);
if (left_pressed && bounds.contains(mx, my)) {
int cursor = text_input_index_from_x(bounds, value, mx, masked);
state.cursor = cursor;
state.selection_anchor = cursor;
state.dragging = true;
return TEXT_INPUT_CONSUMED;
}
if (focused && state.dragging && left_down) {
int cursor = text_input_index_from_x(bounds, value, mx, masked);
if (state.cursor != cursor) {
state.cursor = cursor;
return TEXT_INPUT_CONSUMED;
}
return TEXT_INPUT_CONSUMED;
}
if (state.dragging && left_released) {
state.dragging = false;
return TEXT_INPUT_CONSUMED;
}
return TEXT_INPUT_NONE;
}
inline bool text_input_word_char(char ch) {
return (ch >= 'a' && ch <= 'z') ||
(ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') ||
ch == '_';
}
inline int text_input_prev_word_boundary(const char* text, int cursor) {
int pos = cursor;
if (pos <= 0) return 0;
pos--;
while (pos > 0 && !text_input_word_char(text[pos])) pos--;
while (pos > 0 && text_input_word_char(text[pos - 1])) pos--;
return pos;
}
inline int text_input_next_word_boundary(const char* text, int cursor) {
int len = text_input_len(text);
int pos = text_input_clamp_cursor(cursor, len);
if (pos >= len) return len;
while (pos < len && text_input_word_char(text[pos])) pos++;
while (pos < len && !text_input_word_char(text[pos])) pos++;
return pos;
}
inline void text_input_delete_range(char* text, int* len, int start, int end) {
if (!text || !len || start >= end) return;
for (int i = start; i <= *len - (end - start); i++)
text[i] = text[i + (end - start)];
*len -= (end - start);
}
inline bool text_input_char_allowed(char ch, TextInputCharFilter filter = nullptr,
void* userdata = nullptr) {
unsigned char u = (unsigned char)ch;
if (u < 0x20 || u >= 0x7F) return false;
return filter == nullptr || filter(ch, userdata);
}
inline bool text_input_delete_selection(TextInputState& state, char* text, int cap) {
int len = text_input_len(text, cap);
int sel_start = text_input_selection_start(state, len);
int sel_end = text_input_selection_end(state, len);
if (sel_start == sel_end) return false;
text_input_delete_range(text, &len, sel_start, sel_end);
state.cursor = sel_start;
state.selection_anchor = sel_start;
return true;
}
inline bool text_input_copy_selection(const TextInputState& state, const char* text,
int cap, bool masked = false) {
if (masked) return false;
int len = text_input_len(text, cap);
int sel_start = text_input_selection_start(state, len);
int sel_end = text_input_selection_end(state, len);
if (sel_start == sel_end) return false;
return gui::clipboard_set_text(text + sel_start, sel_end - sel_start);
}
inline bool text_input_replace_selection_with_text(TextInputState& state,
char* text,
int cap,
const char* insert,
int insert_len,
TextInputCharFilter filter = nullptr,
void* userdata = nullptr) {
if (!text || !insert || cap <= 0 || insert_len < 0) return false;
for (int i = 0; i < insert_len; i++) {
if (!text_input_char_allowed(insert[i], filter, userdata))
return false;
}
int len = text_input_len(text, cap);
state.cursor = text_input_clamp_cursor(state.cursor, len);
state.selection_anchor = text_input_clamp_cursor(state.selection_anchor, len);
int sel_start = text_input_selection_start(state, len);
int sel_end = text_input_selection_end(state, len);
int replaced = sel_end - sel_start;
if (len - replaced + insert_len >= cap) return false;
if (replaced > 0)
text_input_delete_range(text, &len, sel_start, sel_end);
for (int i = len; i >= sel_start; i--)
text[i + insert_len] = text[i];
for (int i = 0; i < insert_len; i++)
text[sel_start + i] = insert[i];
state.cursor = sel_start + insert_len;
state.selection_anchor = state.cursor;
return replaced > 0 || insert_len > 0;
}
inline bool text_input_paste_clipboard(TextInputState& state,
char* text,
int cap,
TextInputCharFilter filter = nullptr,
void* userdata = nullptr) {
int paste_len = 0;
char* paste = gui::clipboard_get_text_alloc(&paste_len);
if (paste == nullptr || paste_len <= 0) {
if (paste) montauk::mfree(paste);
return false;
}
bool changed = text_input_replace_selection_with_text(state, text, cap,
paste, paste_len,
filter, userdata);
montauk::mfree(paste);
return changed;
}
inline bool text_input_cut_selection(TextInputState& state, char* text,
int cap, bool masked = false) {
if (!text_input_copy_selection(state, text, cap, masked))
return false;
return text_input_delete_selection(state, text, cap);
}
inline Rect text_input_context_menu_rect(const TextInputState& state, int view_w, int view_h) {
return context_menu_rect(state.context, TEXT_INPUT_CTX_COUNT, view_w, view_h);
}
inline void text_input_context_items(ContextMenuItem items[TEXT_INPUT_CTX_COUNT],
bool has_selection,
bool masked) {
bool paste_enabled = gui::clipboard_has_text();
items[TEXT_INPUT_CTX_CUT] = {"Cut", TEXT_INPUT_CTX_CUT,
has_selection && !masked};
items[TEXT_INPUT_CTX_COPY] = {"Copy", TEXT_INPUT_CTX_COPY,
has_selection && !masked};
items[TEXT_INPUT_CTX_PASTE] = {"Paste", TEXT_INPUT_CTX_PASTE,
paste_enabled};
items[TEXT_INPUT_CTX_DELETE] = {"Delete", TEXT_INPUT_CTX_DELETE,
has_selection};
}
inline int text_input_handle_mouse(TextInputState& state,
const Rect& bounds,
char* text,
int cap,
int mx,
int my,
uint8_t buttons,
uint8_t prev_buttons,
int view_w,
int view_h,
bool focused,
bool masked = false,
TextInputCharFilter filter = nullptr,
void* userdata = nullptr) {
bool right_pressed = (buttons & 2) && !(prev_buttons & 2);
if (state.context.open) {
bool has_selection = text_input_has_selection(state, text, cap);
ContextMenuItem items[TEXT_INPUT_CTX_COUNT];
text_input_context_items(items, has_selection, masked);
int action = context_menu_handle_mouse(state.context, items,
TEXT_INPUT_CTX_COUNT,
mx, my, buttons, prev_buttons,
view_w, view_h);
if (action == CONTEXT_MENU_NONE) return TEXT_INPUT_NONE;
int result = TEXT_INPUT_CONSUMED;
switch (action) {
case TEXT_INPUT_CTX_CUT:
if (text_input_cut_selection(state, text, cap, masked))
result |= TEXT_INPUT_CHANGED;
break;
case TEXT_INPUT_CTX_COPY:
text_input_copy_selection(state, text, cap, masked);
break;
case TEXT_INPUT_CTX_PASTE:
if (text_input_paste_clipboard(state, text, cap, filter, userdata))
result |= TEXT_INPUT_CHANGED;
break;
case TEXT_INPUT_CTX_DELETE:
if (text_input_delete_selection(state, text, cap))
result |= TEXT_INPUT_CHANGED;
break;
}
return result;
}
if (right_pressed && bounds.contains(mx, my)) {
int len = text_input_len(text, cap);
int cursor = text_input_index_from_x(bounds, text, mx, masked);
int sel_start = text_input_selection_start(state, len);
int sel_end = text_input_selection_end(state, len);
if (sel_start == sel_end || cursor < sel_start || cursor > sel_end)
text_input_set_cursor(state, len, cursor, false);
state.dragging = false;
context_menu_open(state.context, mx, my);
return TEXT_INPUT_CONSUMED;
}
return text_input_mouse(state, bounds, text, mx, my, buttons, prev_buttons,
focused, masked);
}
inline int text_input_key(TextInputState& state,
char* text,
int cap,
const Montauk::KeyEvent& key,
TextInputCharFilter filter = nullptr,
void* userdata = nullptr,
bool masked = false) {
if (!key.pressed || !text || cap <= 0) return TEXT_INPUT_NONE;
int len = text_input_len(text, cap);
state.cursor = text_input_clamp_cursor(state.cursor, len);
state.selection_anchor = text_input_clamp_cursor(state.selection_anchor, len);
if (state.context.open) {
context_menu_close(state.context);
if (key.scancode == 0x01) return TEXT_INPUT_CONSUMED;
}
if (key.ctrl && (key.scancode == 0x1E || key.ascii == 'a' || key.ascii == 'A')) {
state.selection_anchor = 0;
state.cursor = len;
return TEXT_INPUT_CONSUMED;
}
int sel_start = text_input_selection_start(state, len);
int sel_end = text_input_selection_end(state, len);
if (key.ctrl && !key.alt && (key.ascii == 'c' || key.ascii == 'C')) {
text_input_copy_selection(state, text, cap, masked);
return TEXT_INPUT_CONSUMED;
}
if (key.ctrl && !key.alt && (key.ascii == 'x' || key.ascii == 'X')) {
bool changed = text_input_cut_selection(state, text, cap, masked);
return TEXT_INPUT_CONSUMED | (changed ? TEXT_INPUT_CHANGED : 0);
}
if (key.ctrl && !key.alt && (key.ascii == 'v' || key.ascii == 'V')) {
bool changed = text_input_paste_clipboard(state, text, cap, filter, userdata);
return TEXT_INPUT_CONSUMED | (changed ? TEXT_INPUT_CHANGED : 0);
}
if (key.scancode == 0x4B || key.scancode == 0x4D ||
key.scancode == 0x47 || key.scancode == 0x4F) {
int next = state.cursor;
if (!key.shift && sel_start != sel_end && key.scancode == 0x4B)
next = sel_start;
else if (!key.shift && sel_start != sel_end && key.scancode == 0x4D)
next = sel_end;
else if (key.scancode == 0x4B)
next = key.ctrl ? text_input_prev_word_boundary(text, state.cursor) : state.cursor - 1;
else if (key.scancode == 0x4D)
next = key.ctrl ? text_input_next_word_boundary(text, state.cursor) : state.cursor + 1;
else if (key.scancode == 0x47)
next = 0;
else if (key.scancode == 0x4F)
next = len;
text_input_set_cursor(state, len, next, key.shift);
return TEXT_INPUT_CONSUMED;
}
bool changed = false;
if (key.ascii == '\b' || key.scancode == 0x0E) {
if (sel_start != sel_end) {
text_input_delete_range(text, &len, sel_start, sel_end);
state.cursor = sel_start;
changed = true;
} else if (key.ctrl) {
int start = text_input_prev_word_boundary(text, state.cursor);
if (start != state.cursor) {
text_input_delete_range(text, &len, start, state.cursor);
state.cursor = start;
changed = true;
}
} else if (state.cursor > 0) {
text_input_delete_range(text, &len, state.cursor - 1, state.cursor);
state.cursor--;
changed = true;
}
state.selection_anchor = state.cursor;
return TEXT_INPUT_CONSUMED | (changed ? TEXT_INPUT_CHANGED : 0);
}
if (key.scancode == 0x53) {
if (sel_start != sel_end) {
text_input_delete_range(text, &len, sel_start, sel_end);
state.cursor = sel_start;
changed = true;
} else if (key.ctrl) {
int end = text_input_next_word_boundary(text, state.cursor);
if (end != state.cursor) {
text_input_delete_range(text, &len, state.cursor, end);
changed = true;
}
} else if (state.cursor < len) {
text_input_delete_range(text, &len, state.cursor, state.cursor + 1);
changed = true;
}
state.selection_anchor = state.cursor;
return TEXT_INPUT_CONSUMED | (changed ? TEXT_INPUT_CHANGED : 0);
}
if (key.ctrl || key.alt || !text_input_char_allowed(key.ascii, filter, userdata))
return TEXT_INPUT_NONE;
if (sel_start != sel_end) {
text_input_delete_range(text, &len, sel_start, sel_end);
state.cursor = sel_start;
changed = true;
}
if (len < cap - 1) {
for (int i = len; i >= state.cursor; i--)
text[i + 1] = text[i];
text[state.cursor++] = key.ascii;
len++;
changed = true;
}
state.selection_anchor = state.cursor;
return TEXT_INPUT_CONSUMED | (changed ? TEXT_INPUT_CHANGED : 0);
}
inline bool same_color(Color a, Color b) {
return a.r == b.r && a.g == b.g && a.b == b.b && a.a == b.a;
}
@@ -302,10 +934,12 @@ inline void draw_text_field(Canvas& c,
int cursor,
bool focused,
bool masked,
const Theme& theme) {
const Theme& theme,
int selection_anchor = -1) {
const char* text = value ? value : "";
char masked_text[65];
const char* shown = masked ? mask_text(text, masked_text, (int)sizeof(masked_text)) : text;
int len = text_len(text);
draw_rounded_frame(c, bounds, theme.radius_sm, theme.surface,
focused ? theme.accent : theme.border);
@@ -313,17 +947,42 @@ inline void draw_text_field(Canvas& c,
int fh = system_font_height();
int tx = bounds.x + 8;
int ty = bounds.y + (bounds.h - fh) / 2;
if (focused && selection_anchor >= 0) {
int cur = text_input_clamp_cursor(cursor, len);
int anchor = text_input_clamp_cursor(selection_anchor, len);
if (cur != anchor) {
int start = cur < anchor ? cur : anchor;
int end = cur > anchor ? cur : anchor;
int sx = tx + text_input_prefix_width(text, start, masked);
int ex = tx + text_input_prefix_width(text, end, masked);
int clip_x = gui_max(sx, bounds.x + 3);
int clip_r = gui_min(ex, bounds.x + bounds.w - 3);
if (clip_r > clip_x)
c.fill_rect(clip_x, bounds.y + 4, clip_r - clip_x,
gui_max(bounds.h - 8, 1), theme.text_selection);
}
}
c.text(tx, ty, shown, theme.text);
if (focused) {
int clamped = gui_clamp(cursor, 0, text_len(text));
char prefix[65];
copy_prefix(prefix, (int)sizeof(prefix), text, clamped, masked);
int cx = tx + text_width(prefix);
int clamped = gui_clamp(cursor, 0, len);
int cx = tx + text_input_prefix_width(text, clamped, masked);
c.fill_rect(cx, bounds.y + 6, 2, gui_max(bounds.h - 12, 1), theme.accent);
}
}
inline void draw_text_input_context_menu(Canvas& c,
const TextInputState& state,
const Theme& theme,
bool has_selection = true,
bool masked = false) {
ContextMenuItem items[TEXT_INPUT_CTX_COUNT];
text_input_context_items(items, has_selection, masked);
draw_context_menu(c, state.context, items, TEXT_INPUT_CTX_COUNT, theme);
}
inline Rect labeled_text_input_rect(int x, int y, int w, const Theme& theme, int field_h = 0) {
if (field_h <= 0) field_h = theme.control_h;
return {x, y + system_font_height() + theme.gap_xs, w, field_h};
@@ -342,11 +1001,12 @@ inline void draw_labeled_text_field(Canvas& c,
bool focused,
bool masked,
const Theme& theme,
int field_h = 0) {
int field_h = 0,
int selection_anchor = -1) {
if (field_h <= 0) field_h = theme.control_h;
c.text(x, y, label, theme.text_muted);
draw_text_field(c, labeled_text_input_rect(x, y, w, theme, field_h),
value, cursor, focused, masked, theme);
value, cursor, focused, masked, theme, selection_anchor);
}
inline void draw_modal_actions(Canvas& c,