feat: improve MTK input boxes, right-click context menus
This commit is contained in:
@@ -44,6 +44,7 @@ struct Theme {
|
||||
Color accent_soft;
|
||||
Color accent_fg;
|
||||
Color selection;
|
||||
Color text_selection;
|
||||
|
||||
Color danger;
|
||||
Color danger_hover;
|
||||
@@ -85,6 +86,7 @@ inline Theme make_theme(Color accent = colors::ACCENT) {
|
||||
theme.accent_soft = lighten(accent, 214);
|
||||
theme.accent_fg = colors::WHITE;
|
||||
theme.selection = accent;
|
||||
theme.text_selection = Color::from_rgb(0xB0, 0xD0, 0xF0);
|
||||
|
||||
theme.danger = Color::from_rgb(0xD0, 0x3E, 0x3E);
|
||||
theme.danger_hover = Color::from_rgb(0xDD, 0x44, 0x44);
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user