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,
|
||||
|
||||
@@ -183,6 +183,7 @@ void filemanager_open_ctx_menu(FileManagerState* fm, int local_x, int local_y, i
|
||||
|
||||
void filemanager_close_ctx_menu(FileManagerState* fm) {
|
||||
fm->ctx_open = false;
|
||||
fm->ctx_hover = -1;
|
||||
}
|
||||
|
||||
} // namespace filemanager
|
||||
|
||||
@@ -25,27 +25,18 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
||||
|
||||
// ---- Context menu interaction ----
|
||||
if (fm->ctx_open) {
|
||||
int cmx = fm->ctx_x;
|
||||
int cmy = fm->ctx_y;
|
||||
int cmh = fm->ctx_item_count * CTX_ITEM_H + 8;
|
||||
if (cmx + CTX_MENU_W > cw) cmx = cw - CTX_MENU_W;
|
||||
if (cmy + cmh > win->content_h) cmy = win->content_h - cmh;
|
||||
if (cmx < 0) cmx = 0;
|
||||
if (cmy < 0) cmy = 0;
|
||||
mtk::ContextMenuState state = filemanager_context_state(fm);
|
||||
mtk::ContextMenuItem items[8];
|
||||
filemanager_build_context_items(fm, items, 8);
|
||||
int action = mtk::context_menu_handle_mouse(state, items,
|
||||
fm->ctx_item_count,
|
||||
local_x, local_y,
|
||||
ev.buttons, ev.prev_buttons,
|
||||
cw, win->content_h);
|
||||
filemanager_store_context_state(fm, state);
|
||||
|
||||
// Update hover
|
||||
if (local_x >= cmx && local_x < cmx + CTX_MENU_W &&
|
||||
local_y >= cmy + 4 && local_y < cmy + 4 + fm->ctx_item_count * CTX_ITEM_H) {
|
||||
fm->ctx_hover = (local_y - cmy - 4) / CTX_ITEM_H;
|
||||
} else {
|
||||
fm->ctx_hover = -1;
|
||||
}
|
||||
|
||||
if (ev.left_pressed()) {
|
||||
if (fm->ctx_hover >= 0 && fm->ctx_hover < fm->ctx_item_count) {
|
||||
int action = fm->ctx_items[fm->ctx_hover];
|
||||
if (action >= 0) {
|
||||
int target = fm->ctx_target_idx;
|
||||
filemanager_close_ctx_menu(fm);
|
||||
|
||||
// Select target if needed
|
||||
if (target >= 0 && target < fm->entry_count)
|
||||
@@ -60,15 +51,9 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
||||
case CTX_DELETE: filemanager_delete_selected(fm); break;
|
||||
case CTX_NEW_FOLDER: filemanager_new_folder(fm); break;
|
||||
}
|
||||
} else {
|
||||
filemanager_close_ctx_menu(fm);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (ev.right_pressed()) {
|
||||
filemanager_close_ctx_menu(fm);
|
||||
return;
|
||||
}
|
||||
if (action == mtk::CONTEXT_MENU_DISMISSED) return;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "../apps_common.hpp"
|
||||
#include <gui/mtk/widgets.hpp>
|
||||
|
||||
namespace filemanager {
|
||||
|
||||
@@ -131,9 +132,6 @@ inline constexpr int CTX_RENAME = 4;
|
||||
inline constexpr int CTX_DELETE = 5;
|
||||
inline constexpr int CTX_NEW_FOLDER = 6;
|
||||
|
||||
inline constexpr int CTX_MENU_W = 140;
|
||||
inline constexpr int CTX_ITEM_H = 24;
|
||||
|
||||
inline bool filemanager_is_virtual_view(const FileManagerState* fm) {
|
||||
return fm && fm->virtual_view != FM_VIRTUAL_VIEW_NONE;
|
||||
}
|
||||
@@ -153,6 +151,29 @@ inline const char* filemanager_virtual_view_label(FileManagerVirtualViewKind vie
|
||||
int special_folder_index(const char* name);
|
||||
const char* ctx_label(int action);
|
||||
|
||||
inline mtk::ContextMenuState filemanager_context_state(const FileManagerState* fm) {
|
||||
if (!fm) return {};
|
||||
return {fm->ctx_open, fm->ctx_x, fm->ctx_y, fm->ctx_hover};
|
||||
}
|
||||
|
||||
inline void filemanager_store_context_state(FileManagerState* fm,
|
||||
const mtk::ContextMenuState& state) {
|
||||
if (!fm) return;
|
||||
fm->ctx_open = state.open;
|
||||
fm->ctx_x = state.x;
|
||||
fm->ctx_y = state.y;
|
||||
fm->ctx_hover = state.hover;
|
||||
}
|
||||
|
||||
inline void filemanager_build_context_items(const FileManagerState* fm,
|
||||
mtk::ContextMenuItem* items,
|
||||
int max_items) {
|
||||
if (!fm || !items || max_items <= 0) return;
|
||||
int count = fm->ctx_item_count < max_items ? fm->ctx_item_count : max_items;
|
||||
for (int i = 0; i < count; i++)
|
||||
items[i] = {ctx_label(fm->ctx_items[i]), fm->ctx_items[i], true};
|
||||
}
|
||||
|
||||
bool str_ends_with(const char* s, const char* suffix);
|
||||
bool is_image_file(const char* name);
|
||||
bool is_font_file(const char* name);
|
||||
|
||||
@@ -576,33 +576,14 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
||||
|
||||
// ---- Context menu overlay ----
|
||||
if (fm->ctx_open && fm->ctx_item_count > 0) {
|
||||
int cmx = fm->ctx_x;
|
||||
int cmy = fm->ctx_y;
|
||||
int cmh = fm->ctx_item_count * CTX_ITEM_H + 8;
|
||||
mtk::Theme theme = mtk::make_theme(colors::ACCENT);
|
||||
theme.border = colors::BORDER;
|
||||
theme.text = colors::TEXT_COLOR;
|
||||
|
||||
// Clamp to window bounds
|
||||
if (cmx + CTX_MENU_W > c.w) cmx = c.w - CTX_MENU_W;
|
||||
if (cmy + cmh > c.h) cmy = c.h - cmh;
|
||||
if (cmx < 0) cmx = 0;
|
||||
if (cmy < 0) cmy = 0;
|
||||
|
||||
// Shadow
|
||||
c.fill_rect(cmx + 2, cmy + 2, CTX_MENU_W, cmh, Color::from_rgb(0x80, 0x80, 0x80));
|
||||
// Background
|
||||
c.fill_rounded_rect(cmx, cmy, CTX_MENU_W, cmh, 4, colors::WHITE);
|
||||
c.rect(cmx, cmy, CTX_MENU_W, cmh, colors::BORDER);
|
||||
|
||||
for (int i = 0; i < fm->ctx_item_count; i++) {
|
||||
int iy = cmy + 4 + i * CTX_ITEM_H;
|
||||
|
||||
// Hover highlight
|
||||
if (i == fm->ctx_hover)
|
||||
c.fill_rect(cmx + 2, iy, CTX_MENU_W - 4, CTX_ITEM_H, colors::MENU_HOVER);
|
||||
|
||||
int fh = system_font_height();
|
||||
int ty = iy + (CTX_ITEM_H - fh) / 2;
|
||||
c.text(cmx + 12, ty, ctx_label(fm->ctx_items[i]), colors::TEXT_COLOR);
|
||||
}
|
||||
mtk::ContextMenuItem items[8];
|
||||
filemanager_build_context_items(fm, items, 8);
|
||||
mtk::draw_context_menu(c, filemanager_context_state(fm), items,
|
||||
fm->ctx_item_count, theme);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,21 +15,6 @@ static mtk::WidgetState dialog_button_state(int mouse_x, int mouse_y,
|
||||
return mtk::widget_state(active, rect.contains(mouse_x, mouse_y), enabled);
|
||||
}
|
||||
|
||||
static void dialog_append(char* buf, int* len, int max, char ch) {
|
||||
if (*len < max - 1) {
|
||||
buf[*len] = ch;
|
||||
(*len)++;
|
||||
buf[*len] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
static void dialog_backspace(char* buf, int* len) {
|
||||
if (*len > 0) {
|
||||
(*len)--;
|
||||
buf[*len] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
static void dialog_close_self(DesktopState* ds, void* app_data) {
|
||||
for (int i = 0; i < ds->window_count; i++) {
|
||||
if (ds->windows[i].app_data == app_data) {
|
||||
@@ -55,8 +40,11 @@ struct AddUserDialogState {
|
||||
int mouse_y;
|
||||
int field;
|
||||
char username[32]; int username_len;
|
||||
mtk::TextInputState username_input;
|
||||
char display[64]; int display_len;
|
||||
mtk::TextInputState display_input;
|
||||
char password[64]; int password_len;
|
||||
mtk::TextInputState password_input;
|
||||
bool role_admin;
|
||||
char error[64];
|
||||
};
|
||||
@@ -76,15 +64,18 @@ static void adduser_on_draw(Window* win, Framebuffer& fb) {
|
||||
int sfh = system_font_height();
|
||||
|
||||
mtk::draw_labeled_text_field(c, pad, y, fw, "Username", st->username,
|
||||
st->username_len, st->field == 0, false, theme, USER_FIELD_H);
|
||||
st->username_input.cursor, st->field == 0, false,
|
||||
theme, USER_FIELD_H, st->username_input.selection_anchor);
|
||||
y += mtk::labeled_text_height(theme, USER_FIELD_H) + 10;
|
||||
|
||||
mtk::draw_labeled_text_field(c, pad, y, fw, "Display Name", st->display,
|
||||
st->display_len, st->field == 1, false, theme, USER_FIELD_H);
|
||||
st->display_input.cursor, st->field == 1, false,
|
||||
theme, USER_FIELD_H, st->display_input.selection_anchor);
|
||||
y += mtk::labeled_text_height(theme, USER_FIELD_H) + 10;
|
||||
|
||||
mtk::draw_labeled_text_field(c, pad, y, fw, "Password", st->password,
|
||||
st->password_len, st->field == 2, true, theme, USER_FIELD_H);
|
||||
st->password_input.cursor, st->field == 2, true,
|
||||
theme, USER_FIELD_H, st->password_input.selection_anchor);
|
||||
y += mtk::labeled_text_height(theme, USER_FIELD_H) + 12;
|
||||
|
||||
c.text(pad, y + 4, "Role:", theme.text_muted);
|
||||
@@ -112,6 +103,20 @@ static void adduser_on_draw(Window* win, Framebuffer& fb) {
|
||||
cancel_btn, "Cancel",
|
||||
dialog_button_state(st->mouse_x, st->mouse_y, cancel_btn),
|
||||
theme);
|
||||
|
||||
mtk::draw_text_input_context_menu(
|
||||
c, st->username_input, theme,
|
||||
mtk::text_input_has_selection(st->username_input,
|
||||
st->username, (int)sizeof(st->username)));
|
||||
mtk::draw_text_input_context_menu(
|
||||
c, st->display_input, theme,
|
||||
mtk::text_input_has_selection(st->display_input,
|
||||
st->display, (int)sizeof(st->display)));
|
||||
mtk::draw_text_input_context_menu(
|
||||
c, st->password_input, theme,
|
||||
mtk::text_input_has_selection(st->password_input,
|
||||
st->password, (int)sizeof(st->password)),
|
||||
true);
|
||||
}
|
||||
|
||||
static void adduser_submit(AddUserDialogState* st) {
|
||||
@@ -145,25 +150,90 @@ static void adduser_on_mouse(Window* win, MouseEvent& ev) {
|
||||
if (!host.map_mouse(ev, &mx, &my)) return;
|
||||
st->mouse_x = mx;
|
||||
st->mouse_y = my;
|
||||
if (!ev.left_pressed()) return;
|
||||
|
||||
mtk::Theme theme = mtk::make_theme(st->accent);
|
||||
int pad = 16;
|
||||
int fw = win->content_w - 2 * pad;
|
||||
int y = pad;
|
||||
bool right_pressed = ev.right_pressed();
|
||||
|
||||
Rect username_rect = mtk::labeled_text_input_rect(pad, y, fw, theme, USER_FIELD_H);
|
||||
if (username_rect.contains(mx, my)) { st->field = 0; return; }
|
||||
if (st->username_input.context.open || st->username_input.dragging) {
|
||||
int result = mtk::text_input_handle_mouse(
|
||||
st->username_input, username_rect, st->username, (int)sizeof(st->username),
|
||||
mx, my, ev.buttons, ev.prev_buttons, win->content_w, win->content_h,
|
||||
st->field == 0);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
st->username_len = mtk::text_input_len(st->username, (int)sizeof(st->username));
|
||||
return;
|
||||
}
|
||||
y += mtk::labeled_text_height(theme, USER_FIELD_H) + 10;
|
||||
|
||||
Rect display_rect = mtk::labeled_text_input_rect(pad, y, fw, theme, USER_FIELD_H);
|
||||
if (display_rect.contains(mx, my)) { st->field = 1; return; }
|
||||
if (st->display_input.context.open || st->display_input.dragging) {
|
||||
int result = mtk::text_input_handle_mouse(
|
||||
st->display_input, display_rect, st->display, (int)sizeof(st->display),
|
||||
mx, my, ev.buttons, ev.prev_buttons, win->content_w, win->content_h,
|
||||
st->field == 1);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
st->display_len = mtk::text_input_len(st->display, (int)sizeof(st->display));
|
||||
return;
|
||||
}
|
||||
y += mtk::labeled_text_height(theme, USER_FIELD_H) + 10;
|
||||
|
||||
Rect password_rect = mtk::labeled_text_input_rect(pad, y, fw, theme, USER_FIELD_H);
|
||||
if (password_rect.contains(mx, my)) { st->field = 2; return; }
|
||||
if (st->password_input.context.open || st->password_input.dragging) {
|
||||
int result = mtk::text_input_handle_mouse(
|
||||
st->password_input, password_rect, st->password, (int)sizeof(st->password),
|
||||
mx, my, ev.buttons, ev.prev_buttons, win->content_w, win->content_h,
|
||||
st->field == 2, true);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
st->password_len = mtk::text_input_len(st->password, (int)sizeof(st->password));
|
||||
return;
|
||||
}
|
||||
y += mtk::labeled_text_height(theme, USER_FIELD_H) + 12;
|
||||
|
||||
if (!ev.left_pressed() && !right_pressed) return;
|
||||
|
||||
if (username_rect.contains(mx, my)) {
|
||||
st->field = 0;
|
||||
mtk::context_menu_close(st->display_input.context);
|
||||
mtk::context_menu_close(st->password_input.context);
|
||||
int result = mtk::text_input_handle_mouse(
|
||||
st->username_input, username_rect, st->username, (int)sizeof(st->username),
|
||||
mx, my, ev.buttons, ev.prev_buttons, win->content_w, win->content_h,
|
||||
true);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
st->username_len = mtk::text_input_len(st->username, (int)sizeof(st->username));
|
||||
return;
|
||||
}
|
||||
if (display_rect.contains(mx, my)) {
|
||||
st->field = 1;
|
||||
mtk::context_menu_close(st->username_input.context);
|
||||
mtk::context_menu_close(st->password_input.context);
|
||||
int result = mtk::text_input_handle_mouse(
|
||||
st->display_input, display_rect, st->display, (int)sizeof(st->display),
|
||||
mx, my, ev.buttons, ev.prev_buttons, win->content_w, win->content_h,
|
||||
true);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
st->display_len = mtk::text_input_len(st->display, (int)sizeof(st->display));
|
||||
return;
|
||||
}
|
||||
if (password_rect.contains(mx, my)) {
|
||||
st->field = 2;
|
||||
mtk::context_menu_close(st->username_input.context);
|
||||
mtk::context_menu_close(st->display_input.context);
|
||||
int result = mtk::text_input_handle_mouse(
|
||||
st->password_input, password_rect, st->password, (int)sizeof(st->password),
|
||||
mx, my, ev.buttons, ev.prev_buttons, win->content_w, win->content_h,
|
||||
true, true);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
st->password_len = mtk::text_input_len(st->password, (int)sizeof(st->password));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ev.left_pressed()) return;
|
||||
|
||||
Rect user_btn = {pad + 60, y, 60, USER_BTN_H};
|
||||
Rect admin_btn = {pad + 128, y, 60, USER_BTN_H};
|
||||
if (user_btn.contains(mx, my)) {
|
||||
@@ -203,20 +273,28 @@ static void adduser_on_key(Window* win, const Montauk::KeyEvent& key) {
|
||||
st->field = (st->field + 1) % 3;
|
||||
return;
|
||||
}
|
||||
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||
|
||||
int result = mtk::TEXT_INPUT_NONE;
|
||||
switch (st->field) {
|
||||
case 0: dialog_backspace(st->username, &st->username_len); break;
|
||||
case 1: dialog_backspace(st->display, &st->display_len); break;
|
||||
case 2: dialog_backspace(st->password, &st->password_len); break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (key.ascii >= 0x20 && key.ascii < 0x7F) {
|
||||
switch (st->field) {
|
||||
case 0: dialog_append(st->username, &st->username_len, 31, key.ascii); break;
|
||||
case 1: dialog_append(st->display, &st->display_len, 63, key.ascii); break;
|
||||
case 2: dialog_append(st->password, &st->password_len, 63, key.ascii); break;
|
||||
}
|
||||
case 0:
|
||||
result = mtk::text_input_key(st->username_input, st->username,
|
||||
(int)sizeof(st->username), key);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
st->username_len = mtk::text_input_len(st->username, (int)sizeof(st->username));
|
||||
break;
|
||||
case 1:
|
||||
result = mtk::text_input_key(st->display_input, st->display,
|
||||
(int)sizeof(st->display), key);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
st->display_len = mtk::text_input_len(st->display, (int)sizeof(st->display));
|
||||
break;
|
||||
case 2:
|
||||
result = mtk::text_input_key(st->password_input, st->password,
|
||||
(int)sizeof(st->password), key,
|
||||
nullptr, nullptr, true);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
st->password_len = mtk::text_input_len(st->password, (int)sizeof(st->password));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,7 +340,9 @@ struct ChPwdDialogState {
|
||||
char display_name[64];
|
||||
int field;
|
||||
char new_pwd[64]; int new_len;
|
||||
mtk::TextInputState new_input;
|
||||
char confirm[64]; int confirm_len;
|
||||
mtk::TextInputState confirm_input;
|
||||
char error[64];
|
||||
};
|
||||
|
||||
@@ -286,11 +366,13 @@ static void chpwd_on_draw(Window* win, Framebuffer& fb) {
|
||||
y += sfh + 12;
|
||||
|
||||
mtk::draw_labeled_text_field(c, pad, y, fw, "New Password", st->new_pwd,
|
||||
st->new_len, st->field == 0, true, theme, USER_FIELD_H);
|
||||
st->new_input.cursor, st->field == 0, true,
|
||||
theme, USER_FIELD_H, st->new_input.selection_anchor);
|
||||
y += mtk::labeled_text_height(theme, USER_FIELD_H) + 10;
|
||||
|
||||
mtk::draw_labeled_text_field(c, pad, y, fw, "Confirm Password", st->confirm,
|
||||
st->confirm_len, st->field == 1, true, theme, USER_FIELD_H);
|
||||
st->confirm_input.cursor, st->field == 1, true,
|
||||
theme, USER_FIELD_H, st->confirm_input.selection_anchor);
|
||||
y += mtk::labeled_text_height(theme, USER_FIELD_H) + 12;
|
||||
|
||||
if (st->error[0]) {
|
||||
@@ -306,6 +388,17 @@ static void chpwd_on_draw(Window* win, Framebuffer& fb) {
|
||||
cancel_btn, "Cancel",
|
||||
dialog_button_state(st->mouse_x, st->mouse_y, cancel_btn),
|
||||
theme);
|
||||
|
||||
mtk::draw_text_input_context_menu(
|
||||
c, st->new_input, theme,
|
||||
mtk::text_input_has_selection(st->new_input,
|
||||
st->new_pwd, (int)sizeof(st->new_pwd)),
|
||||
true);
|
||||
mtk::draw_text_input_context_menu(
|
||||
c, st->confirm_input, theme,
|
||||
mtk::text_input_has_selection(st->confirm_input,
|
||||
st->confirm, (int)sizeof(st->confirm)),
|
||||
true);
|
||||
}
|
||||
|
||||
static void chpwd_submit(ChPwdDialogState* st) {
|
||||
@@ -333,19 +426,62 @@ static void chpwd_on_mouse(Window* win, MouseEvent& ev) {
|
||||
if (!host.map_mouse(ev, &mx, &my)) return;
|
||||
st->mouse_x = mx;
|
||||
st->mouse_y = my;
|
||||
if (!ev.left_pressed()) return;
|
||||
|
||||
mtk::Theme theme = mtk::make_theme(st->accent);
|
||||
int pad = 16;
|
||||
int fw = win->content_w - 2 * pad;
|
||||
int y = pad + system_font_height() + 12;
|
||||
bool right_pressed = ev.right_pressed();
|
||||
|
||||
Rect new_rect = mtk::labeled_text_input_rect(pad, y, fw, theme, USER_FIELD_H);
|
||||
if (new_rect.contains(mx, my)) { st->field = 0; return; }
|
||||
if (st->new_input.context.open || st->new_input.dragging) {
|
||||
int result = mtk::text_input_handle_mouse(
|
||||
st->new_input, new_rect, st->new_pwd, (int)sizeof(st->new_pwd),
|
||||
mx, my, ev.buttons, ev.prev_buttons, win->content_w, win->content_h,
|
||||
st->field == 0, true);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
st->new_len = mtk::text_input_len(st->new_pwd, (int)sizeof(st->new_pwd));
|
||||
return;
|
||||
}
|
||||
y += mtk::labeled_text_height(theme, USER_FIELD_H) + 10;
|
||||
|
||||
Rect confirm_rect = mtk::labeled_text_input_rect(pad, y, fw, theme, USER_FIELD_H);
|
||||
if (confirm_rect.contains(mx, my)) { st->field = 1; return; }
|
||||
if (st->confirm_input.context.open || st->confirm_input.dragging) {
|
||||
int result = mtk::text_input_handle_mouse(
|
||||
st->confirm_input, confirm_rect, st->confirm, (int)sizeof(st->confirm),
|
||||
mx, my, ev.buttons, ev.prev_buttons, win->content_w, win->content_h,
|
||||
st->field == 1, true);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
st->confirm_len = mtk::text_input_len(st->confirm, (int)sizeof(st->confirm));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ev.left_pressed() && !right_pressed) return;
|
||||
|
||||
if (new_rect.contains(mx, my)) {
|
||||
st->field = 0;
|
||||
mtk::context_menu_close(st->confirm_input.context);
|
||||
int result = mtk::text_input_handle_mouse(
|
||||
st->new_input, new_rect, st->new_pwd, (int)sizeof(st->new_pwd),
|
||||
mx, my, ev.buttons, ev.prev_buttons, win->content_w, win->content_h,
|
||||
true, true);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
st->new_len = mtk::text_input_len(st->new_pwd, (int)sizeof(st->new_pwd));
|
||||
return;
|
||||
}
|
||||
if (confirm_rect.contains(mx, my)) {
|
||||
st->field = 1;
|
||||
mtk::context_menu_close(st->new_input.context);
|
||||
int result = mtk::text_input_handle_mouse(
|
||||
st->confirm_input, confirm_rect, st->confirm, (int)sizeof(st->confirm),
|
||||
mx, my, ev.buttons, ev.prev_buttons, win->content_w, win->content_h,
|
||||
true, true);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
st->confirm_len = mtk::text_input_len(st->confirm, (int)sizeof(st->confirm));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ev.left_pressed()) return;
|
||||
|
||||
Rect save_btn = dialog_primary_button_rect(pad, win->content_h);
|
||||
Rect cancel_btn = dialog_secondary_button_rect(pad, win->content_h);
|
||||
@@ -375,14 +511,18 @@ static void chpwd_on_key(Window* win, const Montauk::KeyEvent& key) {
|
||||
st->field = (st->field + 1) % 2;
|
||||
return;
|
||||
}
|
||||
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||
if (st->field == 0) dialog_backspace(st->new_pwd, &st->new_len);
|
||||
else dialog_backspace(st->confirm, &st->confirm_len);
|
||||
return;
|
||||
}
|
||||
if (key.ascii >= 0x20 && key.ascii < 0x7F) {
|
||||
if (st->field == 0) dialog_append(st->new_pwd, &st->new_len, 63, key.ascii);
|
||||
else dialog_append(st->confirm, &st->confirm_len, 63, key.ascii);
|
||||
if (st->field == 0) {
|
||||
int result = mtk::text_input_key(st->new_input, st->new_pwd,
|
||||
(int)sizeof(st->new_pwd), key,
|
||||
nullptr, nullptr, true);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
st->new_len = mtk::text_input_len(st->new_pwd, (int)sizeof(st->new_pwd));
|
||||
} else {
|
||||
int result = mtk::text_input_key(st->confirm_input, st->confirm,
|
||||
(int)sizeof(st->confirm), key,
|
||||
nullptr, nullptr, true);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
st->confirm_len = mtk::text_input_len(st->confirm, (int)sizeof(st->confirm));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+83
-103
@@ -38,7 +38,7 @@ enum Tab {
|
||||
struct Field {
|
||||
const char* label;
|
||||
char value[FIELD_TEXT_CAP];
|
||||
int cursor;
|
||||
mtk::TextInputState input;
|
||||
};
|
||||
|
||||
static const char* const kTabLabels[TAB_COUNT] = {
|
||||
@@ -54,10 +54,10 @@ static int g_focus_field = -1;
|
||||
static Montauk::NetCfg g_cfg = {};
|
||||
static Montauk::NetStatus g_net = {};
|
||||
static Field g_fields[FIELD_COUNT] = {
|
||||
{"IP Address", {}, 0},
|
||||
{"Subnet Mask", {}, 0},
|
||||
{"Gateway", {}, 0},
|
||||
{"DNS Server", {}, 0},
|
||||
{"IP Address", {}, {}},
|
||||
{"Subnet Mask", {}, {}},
|
||||
{"Gateway", {}, {}},
|
||||
{"DNS Server", {}, {}},
|
||||
};
|
||||
static bool g_dirty = false;
|
||||
static char g_status[128] = {};
|
||||
@@ -108,6 +108,11 @@ static bool is_ipv4_input_char(char ch) {
|
||||
return (ch >= '0' && ch <= '9') || ch == '.';
|
||||
}
|
||||
|
||||
static bool accept_ipv4_input_char(char ch, void* userdata) {
|
||||
(void)userdata;
|
||||
return is_ipv4_input_char(ch);
|
||||
}
|
||||
|
||||
static void focus_field(int index, int cursor = -1) {
|
||||
if (index < 0) index = 0;
|
||||
if (index >= FIELD_COUNT) index = FIELD_COUNT - 1;
|
||||
@@ -118,7 +123,10 @@ static void focus_field(int index, int cursor = -1) {
|
||||
if (cursor > len) cursor = len;
|
||||
|
||||
g_focus_field = index;
|
||||
field.cursor = cursor;
|
||||
for (int i = 0; i < FIELD_COUNT; i++) {
|
||||
if (i != index) mtk::context_menu_close(g_fields[i].input.context);
|
||||
}
|
||||
mtk::text_input_reset(field.input, cursor);
|
||||
}
|
||||
|
||||
static bool mac_is_zero(const uint8_t* mac) {
|
||||
@@ -233,7 +241,7 @@ static void copy_cfg_to_fields() {
|
||||
format_ip(g_fields[2].value, sizeof(g_fields[2].value), g_cfg.gateway);
|
||||
format_ip(g_fields[3].value, sizeof(g_fields[3].value), g_cfg.dnsServer);
|
||||
for (int i = 0; i < FIELD_COUNT; i++) {
|
||||
g_fields[i].cursor = str_len(g_fields[i].value);
|
||||
mtk::text_input_reset(g_fields[i].input, str_len(g_fields[i].value));
|
||||
}
|
||||
g_dirty = false;
|
||||
}
|
||||
@@ -324,30 +332,6 @@ static int field_w(int index) {
|
||||
return config_two_columns() ? (available - GAP) / 2 : available;
|
||||
}
|
||||
|
||||
static int field_cursor_from_x(int index, int mx) {
|
||||
if (index < 0 || index >= FIELD_COUNT) return 0;
|
||||
|
||||
Rect input = field_input_rect(index);
|
||||
int rel_x = mx - (input.x + 8);
|
||||
if (rel_x <= 0) return 0;
|
||||
|
||||
Field& field = g_fields[index];
|
||||
int len = str_len(field.value);
|
||||
char prefix[FIELD_TEXT_CAP] = {};
|
||||
int prev_w = 0;
|
||||
|
||||
for (int i = 0; i < len; i++) {
|
||||
prefix[i] = field.value[i];
|
||||
prefix[i + 1] = '\0';
|
||||
int cur_w = text_width(prefix);
|
||||
int midpoint = prev_w + (cur_w - prev_w) / 2;
|
||||
if (rel_x < midpoint) return i;
|
||||
prev_w = cur_w;
|
||||
}
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
static void draw_kv(Canvas& c, int* y, const char* key, const char* value,
|
||||
const mtk::Theme& theme) {
|
||||
int fh = system_font_height();
|
||||
@@ -410,8 +394,19 @@ static void draw_config_tab(Canvas& c, const mtk::Theme& theme) {
|
||||
for (int i = 0; i < FIELD_COUNT; i++) {
|
||||
mtk::draw_labeled_text_field(c, field_x(i), field_label_y(i), field_w(i),
|
||||
g_fields[i].label, g_fields[i].value,
|
||||
g_fields[i].cursor, g_focus_field == i,
|
||||
false, theme, FIELD_H);
|
||||
g_fields[i].input.cursor, g_focus_field == i,
|
||||
false, theme, FIELD_H,
|
||||
g_fields[i].input.selection_anchor);
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_config_context_menus(Canvas& c, const mtk::Theme& theme) {
|
||||
for (int i = 0; i < FIELD_COUNT; i++) {
|
||||
mtk::draw_text_input_context_menu(
|
||||
c, g_fields[i].input, theme,
|
||||
mtk::text_input_has_selection(g_fields[i].input,
|
||||
g_fields[i].value,
|
||||
(int)sizeof(g_fields[i].value)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,6 +459,8 @@ static void render() {
|
||||
draw_config_tab(c, theme);
|
||||
}
|
||||
draw_footer(c, theme);
|
||||
if (g_tab == TAB_CONFIG)
|
||||
draw_config_context_menus(c, theme);
|
||||
|
||||
host.present();
|
||||
}
|
||||
@@ -530,17 +527,27 @@ static void set_tab(Tab tab) {
|
||||
focus_field(0);
|
||||
} else {
|
||||
g_focus_field = -1;
|
||||
for (int i = 0; i < FIELD_COUNT; i++) {
|
||||
g_fields[i].input.dragging = false;
|
||||
mtk::context_menu_close(g_fields[i].input.context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool handle_click(int mx, int my) {
|
||||
static bool handle_mouse(int mx, int my, uint8_t buttons, uint8_t prev_buttons) {
|
||||
bool left_pressed = (buttons & 1) && !(prev_buttons & 1);
|
||||
bool right_pressed = (buttons & 2) && !(prev_buttons & 2);
|
||||
|
||||
if (left_pressed) {
|
||||
int tab = mtk::hit_tab_bar(tab_bar_rect(), TAB_COUNT, mx, my);
|
||||
if (tab >= 0) {
|
||||
set_tab((Tab)tab);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (g_tab == TAB_STATUS) {
|
||||
if (!left_pressed) return false;
|
||||
if (status_refresh_button().contains(mx, my)) {
|
||||
refresh_state(!g_dirty);
|
||||
set_status("Network status refreshed");
|
||||
@@ -558,12 +565,44 @@ static bool handle_click(int mx, int my) {
|
||||
}
|
||||
|
||||
for (int i = 0; i < FIELD_COUNT; i++) {
|
||||
if (field_input_rect(i).contains(mx, my)) {
|
||||
focus_field(i, field_cursor_from_x(i, mx));
|
||||
if (!g_fields[i].input.context.open && !g_fields[i].input.dragging) continue;
|
||||
int result = mtk::text_input_handle_mouse(g_fields[i].input,
|
||||
field_input_rect(i),
|
||||
g_fields[i].value,
|
||||
(int)sizeof(g_fields[i].value),
|
||||
mx, my, buttons, prev_buttons,
|
||||
g_win.width, g_win.height,
|
||||
g_focus_field == i,
|
||||
false, accept_ipv4_input_char);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
g_dirty = true;
|
||||
return result != mtk::TEXT_INPUT_NONE;
|
||||
}
|
||||
|
||||
if (!left_pressed && !right_pressed) return false;
|
||||
|
||||
for (int i = 0; i < FIELD_COUNT; i++) {
|
||||
Rect input = field_input_rect(i);
|
||||
if (input.contains(mx, my)) {
|
||||
g_focus_field = i;
|
||||
for (int j = 0; j < FIELD_COUNT; j++) {
|
||||
if (j != i) mtk::context_menu_close(g_fields[j].input.context);
|
||||
}
|
||||
int result = mtk::text_input_handle_mouse(g_fields[i].input, input,
|
||||
g_fields[i].value,
|
||||
(int)sizeof(g_fields[i].value),
|
||||
mx, my, buttons, prev_buttons,
|
||||
g_win.width, g_win.height,
|
||||
true, false,
|
||||
accept_ipv4_input_char);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
g_dirty = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!left_pressed) return false;
|
||||
|
||||
if (config_dhcp_button().contains(mx, my)) {
|
||||
launch_dhcp();
|
||||
return true;
|
||||
@@ -584,38 +623,6 @@ static bool handle_click(int mx, int my) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static void delete_before_cursor(Field* field) {
|
||||
if (!field || field->cursor <= 0) return;
|
||||
int len = str_len(field->value);
|
||||
for (int i = field->cursor - 1; i < len; i++) {
|
||||
field->value[i] = field->value[i + 1];
|
||||
}
|
||||
field->cursor--;
|
||||
g_dirty = true;
|
||||
}
|
||||
|
||||
static void delete_at_cursor(Field* field) {
|
||||
if (!field) return;
|
||||
int len = str_len(field->value);
|
||||
if (field->cursor >= len) return;
|
||||
for (int i = field->cursor; i < len; i++) {
|
||||
field->value[i] = field->value[i + 1];
|
||||
}
|
||||
g_dirty = true;
|
||||
}
|
||||
|
||||
static void insert_char(Field* field, char ch) {
|
||||
if (!field) return;
|
||||
if (!((ch >= '0' && ch <= '9') || ch == '.')) return;
|
||||
int len = str_len(field->value);
|
||||
if (len >= FIELD_TEXT_CAP - 1) return;
|
||||
for (int i = len; i >= field->cursor; i--) {
|
||||
field->value[i + 1] = field->value[i];
|
||||
}
|
||||
field->value[field->cursor++] = ch;
|
||||
g_dirty = true;
|
||||
}
|
||||
|
||||
static bool handle_key(const Montauk::KeyEvent& key) {
|
||||
if (!key.pressed) return false;
|
||||
|
||||
@@ -664,38 +671,12 @@ static bool handle_key(const Montauk::KeyEvent& key) {
|
||||
}
|
||||
|
||||
Field* field = &g_fields[g_focus_field];
|
||||
int len = str_len(field->value);
|
||||
|
||||
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||
delete_before_cursor(field);
|
||||
return true;
|
||||
}
|
||||
if (key.scancode == 0x53) {
|
||||
delete_at_cursor(field);
|
||||
return true;
|
||||
}
|
||||
if (key.scancode == 0x4B) {
|
||||
if (field->cursor > 0) field->cursor--;
|
||||
return true;
|
||||
}
|
||||
if (key.scancode == 0x4D) {
|
||||
if (field->cursor < len) field->cursor++;
|
||||
return true;
|
||||
}
|
||||
if (key.scancode == 0x47) {
|
||||
field->cursor = 0;
|
||||
return true;
|
||||
}
|
||||
if (key.scancode == 0x4F) {
|
||||
field->cursor = len;
|
||||
return true;
|
||||
}
|
||||
if (is_ipv4_input_char(key.ascii)) {
|
||||
insert_char(field, key.ascii);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
int result = mtk::text_input_key(field->input, field->value,
|
||||
(int)sizeof(field->value), key,
|
||||
accept_ipv4_input_char);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
g_dirty = true;
|
||||
return (result & mtk::TEXT_INPUT_CONSUMED) != 0;
|
||||
}
|
||||
|
||||
extern "C" void _start() {
|
||||
@@ -737,9 +718,8 @@ extern "C" void _start() {
|
||||
g_mouse_x = ev.mouse.x;
|
||||
g_mouse_y = ev.mouse.y;
|
||||
redraw = true;
|
||||
|
||||
bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
|
||||
if (clicked && handle_click(ev.mouse.x, ev.mouse.y)) {
|
||||
if (handle_mouse(ev.mouse.x, ev.mouse.y,
|
||||
ev.mouse.buttons, ev.mouse.prev_buttons)) {
|
||||
redraw = true;
|
||||
}
|
||||
} else if (ev.type == 0) {
|
||||
|
||||
@@ -79,6 +79,7 @@ struct AppState {
|
||||
int config_button_down;
|
||||
char edit_uri[MAX_PATH_LEN];
|
||||
int edit_len;
|
||||
mtk::TextInputState edit_input;
|
||||
char edit_error[128];
|
||||
int mouse_x;
|
||||
int mouse_y;
|
||||
@@ -473,6 +474,7 @@ static void close_config_popup() {
|
||||
static void open_config_popup() {
|
||||
safe_copy(g_app.edit_uri, sizeof(g_app.edit_uri), g_app.printer_uri);
|
||||
g_app.edit_len = montauk::slen(g_app.edit_uri);
|
||||
mtk::text_input_reset(g_app.edit_input, g_app.edit_len);
|
||||
g_app.edit_error[0] = '\0';
|
||||
g_app.config_button_down = 0;
|
||||
g_app.config_mouse_x = -1;
|
||||
@@ -535,8 +537,10 @@ static void render_config_popup() {
|
||||
c.text(x, y, "Enter an IPP or IPPS destination URI.", theme.text_muted);
|
||||
|
||||
mtk::draw_labeled_text_field(c, lo.input.x, lo.input_label_y, lo.input.w,
|
||||
"Printer URI", g_app.edit_uri, g_app.edit_len,
|
||||
true, false, theme, lo.input.h);
|
||||
"Printer URI", g_app.edit_uri,
|
||||
g_app.edit_input.cursor,
|
||||
true, false, theme, lo.input.h,
|
||||
g_app.edit_input.selection_anchor);
|
||||
|
||||
y = lo.input.y + lo.input.h + 12;
|
||||
c.text(x, y, "Example: ipp://printer.local/ipp/print", theme.text_muted);
|
||||
@@ -552,6 +556,10 @@ static void render_config_popup() {
|
||||
lo.cancel_btn, "Cancel",
|
||||
config_button_state(lo.cancel_btn, true, g_app.config_button_down == 2),
|
||||
theme);
|
||||
mtk::draw_text_input_context_menu(
|
||||
c, g_app.edit_input, theme,
|
||||
mtk::text_input_has_selection(g_app.edit_input,
|
||||
g_app.edit_uri, MAX_PATH_LEN));
|
||||
montauk::win_present(g_config.win_id);
|
||||
}
|
||||
|
||||
@@ -563,8 +571,38 @@ static bool handle_config_mouse(int mx, int my, uint8_t buttons, uint8_t prev_bu
|
||||
|
||||
bool left_pressed = (buttons & 1) && !(prev_buttons & 1);
|
||||
bool left_released = !(buttons & 1) && (prev_buttons & 1);
|
||||
bool right_pressed = (buttons & 2) && !(prev_buttons & 2);
|
||||
|
||||
if (g_app.edit_input.context.open || g_app.edit_input.dragging) {
|
||||
int result = mtk::text_input_handle_mouse(g_app.edit_input, lo.input,
|
||||
g_app.edit_uri, MAX_PATH_LEN,
|
||||
mx, my, buttons, prev_buttons,
|
||||
g_config.width, g_config.height,
|
||||
true);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED) {
|
||||
g_app.edit_len = mtk::text_input_len(g_app.edit_uri, MAX_PATH_LEN);
|
||||
g_app.edit_error[0] = '\0';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (left_pressed || right_pressed) {
|
||||
if (lo.input.contains(mx, my)) {
|
||||
g_app.config_button_down = 0;
|
||||
int result = mtk::text_input_handle_mouse(g_app.edit_input, lo.input,
|
||||
g_app.edit_uri, MAX_PATH_LEN,
|
||||
mx, my, buttons, prev_buttons,
|
||||
g_config.width, g_config.height,
|
||||
true);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED) {
|
||||
g_app.edit_len = mtk::text_input_len(g_app.edit_uri, MAX_PATH_LEN);
|
||||
g_app.edit_error[0] = '\0';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!left_pressed) return false;
|
||||
|
||||
if (left_pressed) {
|
||||
int button_down = 0;
|
||||
if (lo.save_btn.contains(mx, my))
|
||||
button_down = 1;
|
||||
@@ -601,17 +639,11 @@ static bool handle_config_key(const Montauk::KeyEvent& key) {
|
||||
save_configured_printer();
|
||||
return !g_app.config_open;
|
||||
}
|
||||
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||
if (g_app.edit_len > 0) {
|
||||
g_app.edit_len--;
|
||||
g_app.edit_uri[g_app.edit_len] = '\0';
|
||||
g_app.edit_error[0] = '\0';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (key.ascii >= 0x20 && key.ascii < 0x7F && g_app.edit_len < MAX_PATH_LEN - 1) {
|
||||
g_app.edit_uri[g_app.edit_len++] = key.ascii;
|
||||
g_app.edit_uri[g_app.edit_len] = '\0';
|
||||
|
||||
int result = mtk::text_input_key(g_app.edit_input, g_app.edit_uri,
|
||||
MAX_PATH_LEN, key);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED) {
|
||||
g_app.edit_len = mtk::text_input_len(g_app.edit_uri, MAX_PATH_LEN);
|
||||
g_app.edit_error[0] = '\0';
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <gui/svg.hpp>
|
||||
#include <gui/dialogs.hpp>
|
||||
#include <gui/clipboard.hpp>
|
||||
#include <gui/mtk/widgets.hpp>
|
||||
#include <gui/stb_math.h>
|
||||
|
||||
extern "C" {
|
||||
@@ -59,6 +60,14 @@ static constexpr Color STATUS_TEXT = Color::from_rgb(0xE0, 0xE0, 0xE0);
|
||||
static constexpr Color PATHBAR_BG = Color::from_rgb(0xF0, 0xF0, 0xF0);
|
||||
static constexpr Color WHITE = Color::from_rgb(0xFF, 0xFF, 0xFF);
|
||||
|
||||
enum ContextItem {
|
||||
CTX_CUT = 0,
|
||||
CTX_COPY,
|
||||
CTX_PASTE,
|
||||
CTX_DELETE,
|
||||
CTX_COUNT,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Global state
|
||||
// ============================================================================
|
||||
@@ -101,6 +110,9 @@ int g_sel_end = 0;
|
||||
bool g_has_selection = false;
|
||||
bool g_mouse_selecting = false;
|
||||
|
||||
// Context menu
|
||||
mtk::ContextMenuState g_ctx_menu = {};
|
||||
|
||||
// Pathbar
|
||||
bool g_pathbar_open = false;
|
||||
bool g_pathbar_save = false;
|
||||
@@ -501,6 +513,57 @@ static bool paste_from_clipboard() {
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool context_item_enabled(int item) {
|
||||
int selected = selection_length();
|
||||
switch (item) {
|
||||
case CTX_CUT:
|
||||
case CTX_COPY:
|
||||
case CTX_DELETE:
|
||||
return selected > 0;
|
||||
case CTX_PASTE:
|
||||
return gui::clipboard_has_text();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static void build_context_items(mtk::ContextMenuItem items[CTX_COUNT]) {
|
||||
items[CTX_CUT] = {"Cut", CTX_CUT, context_item_enabled(CTX_CUT)};
|
||||
items[CTX_COPY] = {"Copy", CTX_COPY, context_item_enabled(CTX_COPY)};
|
||||
items[CTX_PASTE] = {"Paste", CTX_PASTE, context_item_enabled(CTX_PASTE)};
|
||||
items[CTX_DELETE] = {"Delete", CTX_DELETE, context_item_enabled(CTX_DELETE)};
|
||||
}
|
||||
|
||||
static bool selection_contains_pos(int pos) {
|
||||
if (!g_has_selection) return false;
|
||||
int ss, se;
|
||||
sel_range(&ss, &se);
|
||||
return pos >= ss && pos < se;
|
||||
}
|
||||
|
||||
static void close_context_menu() {
|
||||
mtk::context_menu_close(g_ctx_menu);
|
||||
}
|
||||
|
||||
static bool perform_context_action(int item) {
|
||||
if (!context_item_enabled(item)) return false;
|
||||
|
||||
switch (item) {
|
||||
case CTX_CUT:
|
||||
return cut_selection_to_clipboard();
|
||||
case CTX_COPY:
|
||||
return copy_selection_to_clipboard();
|
||||
case CTX_PASTE:
|
||||
return paste_from_clipboard();
|
||||
case CTX_DELETE:
|
||||
delete_selection();
|
||||
set_status("Deleted selection");
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool looks_binary(const char* data, int len) {
|
||||
if (!data || len <= 0) return false;
|
||||
|
||||
@@ -625,6 +688,22 @@ static int hit_test(int mx, int my, int editor_y) {
|
||||
return g_line_offsets[clicked_line] + clicked_col;
|
||||
}
|
||||
|
||||
static void draw_context_menu(uint32_t* pixels, int W, int H) {
|
||||
if (!g_ctx_menu.open) return;
|
||||
|
||||
Canvas c(pixels, W, H);
|
||||
mtk::Theme theme = mtk::make_theme(ACCENT);
|
||||
theme.border = BORDER_COLOR;
|
||||
theme.text = TEXT_COLOR;
|
||||
|
||||
mtk::ContextMenuItem items[CTX_COUNT];
|
||||
build_context_items(items);
|
||||
mtk::draw_context_menu(c, g_ctx_menu, items, CTX_COUNT, theme,
|
||||
mtk::CONTEXT_MENU_DEFAULT_W,
|
||||
mtk::CONTEXT_MENU_ITEM_H,
|
||||
g_font, FONT_SIZE);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Rendering
|
||||
// ============================================================================
|
||||
@@ -880,6 +959,8 @@ static void render(uint32_t* pixels) {
|
||||
int rw = g_font->measure_text(status_right, FONT_SIZE);
|
||||
g_font->draw_to_buffer(pixels, W, H, W - rw - 4, sty, status_right, STATUS_TEXT, FONT_SIZE);
|
||||
}
|
||||
|
||||
draw_context_menu(pixels, W, H);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -948,6 +1029,11 @@ static void open_pathbar_save() {
|
||||
static bool handle_key(Montauk::KeyEvent& key, int win_id) {
|
||||
if (!key.pressed) return false;
|
||||
|
||||
if (g_ctx_menu.open) {
|
||||
close_context_menu();
|
||||
if (key.scancode == 0x01) return true;
|
||||
}
|
||||
|
||||
// ---- Pathbar mode ----
|
||||
if (g_pathbar_open) {
|
||||
if (key.ascii == '\n' || key.ascii == '\r') {
|
||||
@@ -1140,6 +1226,7 @@ static bool handle_mouse(Montauk::WinEvent& ev, int win_id) {
|
||||
uint8_t btns = ev.mouse.buttons;
|
||||
uint8_t prev = ev.mouse.prev_buttons;
|
||||
bool clicked = (btns & 1) && !(prev & 1);
|
||||
bool right_clicked = (btns & 2) && !(prev & 2);
|
||||
bool held = (btns & 1) != 0;
|
||||
bool released = !(btns & 1) && (prev & 1);
|
||||
int scroll = ev.mouse.scroll;
|
||||
@@ -1148,6 +1235,22 @@ static bool handle_mouse(Montauk::WinEvent& ev, int win_id) {
|
||||
int editor_y = TOOLBAR_H + (g_pathbar_open ? PATHBAR_H : 0);
|
||||
int text_area_h = g_win_h - editor_y - STATUS_H;
|
||||
|
||||
if (g_ctx_menu.open) {
|
||||
if (scroll != 0) {
|
||||
close_context_menu();
|
||||
return true;
|
||||
}
|
||||
|
||||
mtk::ContextMenuItem items[CTX_COUNT];
|
||||
build_context_items(items);
|
||||
int action = mtk::context_menu_handle_mouse(g_ctx_menu, items, CTX_COUNT,
|
||||
mx, my, btns, prev,
|
||||
g_win_w, g_win_h);
|
||||
if (action >= 0)
|
||||
perform_context_action(action);
|
||||
return action != mtk::CONTEXT_MENU_NONE;
|
||||
}
|
||||
|
||||
// ---- Toolbar clicks ----
|
||||
if (clicked && my < TOOLBAR_H) {
|
||||
// Open button
|
||||
@@ -1180,6 +1283,19 @@ static bool handle_mouse(Montauk::WinEvent& ev, int win_id) {
|
||||
}
|
||||
|
||||
// ---- Editor click ----
|
||||
if (right_clicked && my >= editor_y && my < editor_y + text_area_h
|
||||
&& mx > LINE_NUM_W && mx < g_win_w - SCROLLBAR_W) {
|
||||
int pos = hit_test(mx, my, editor_y);
|
||||
if (!selection_contains_pos(pos)) {
|
||||
g_cursor_pos = pos;
|
||||
update_cursor_pos();
|
||||
clear_selection();
|
||||
}
|
||||
g_mouse_selecting = false;
|
||||
mtk::context_menu_open(g_ctx_menu, mx, my);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (clicked && my >= editor_y && my < editor_y + text_area_h
|
||||
&& mx > LINE_NUM_W && mx < g_win_w - SCROLLBAR_W) {
|
||||
int pos = hit_test(mx, my, editor_y);
|
||||
|
||||
@@ -83,6 +83,7 @@ static constexpr int READER_FONT_FACE_COUNT = (int)ReaderFontFace::COUNT;
|
||||
|
||||
static AppPhase g_phase = AppPhase::IDLE;
|
||||
static char g_query[256] = {};
|
||||
static mtk::TextInputState g_query_input = {};
|
||||
static char g_status[256] = {};
|
||||
static int g_scroll_y = 0;
|
||||
static int g_line_count = 0;
|
||||
@@ -959,8 +960,10 @@ static void render(Canvas& canvas) {
|
||||
|
||||
// ---- Toolbar ----
|
||||
canvas.fill_rect(toolbar.x, toolbar.y, toolbar.w, toolbar.h, theme.surface_alt);
|
||||
mtk::draw_text_field(canvas, lo.search_field, g_query, (int)strlen(g_query),
|
||||
g_search_focused, false, theme);
|
||||
mtk::draw_text_field(canvas, lo.search_field, g_query,
|
||||
g_query_input.cursor,
|
||||
g_search_focused, false, theme,
|
||||
g_query_input.selection_anchor);
|
||||
draw_display_toggle_button(canvas, lo.display_button,
|
||||
mtk::widget_state(g_display_controls_open,
|
||||
mouse_in_rect(lo.display_button), true),
|
||||
@@ -1029,6 +1032,11 @@ static void render(Canvas& canvas) {
|
||||
theme.radius_sm, thumb_color);
|
||||
}
|
||||
}
|
||||
|
||||
mtk::draw_text_input_context_menu(
|
||||
canvas, g_query_input, theme,
|
||||
mtk::text_input_has_selection(g_query_input,
|
||||
g_query, (int)sizeof(g_query)));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -1125,12 +1133,9 @@ extern "C" void _start() {
|
||||
if (g_search_focused) {
|
||||
if (ascii == '\n' || ascii == '\r') {
|
||||
search_pending = true;
|
||||
} else if (ascii == '\b' || scan == 0x0E) {
|
||||
int len = (int)strlen(g_query);
|
||||
if (len > 0) g_query[len - 1] = '\0';
|
||||
} else if (ascii >= 32 && ascii < 127) {
|
||||
int len = (int)strlen(g_query);
|
||||
if (len < 254) { g_query[len] = ascii; g_query[len + 1] = '\0'; }
|
||||
} else {
|
||||
mtk::text_input_key(g_query_input, g_query,
|
||||
(int)sizeof(g_query), ev.key);
|
||||
}
|
||||
} else if (g_phase == AppPhase::DONE) {
|
||||
// Navigation keys
|
||||
@@ -1151,12 +1156,26 @@ extern "C" void _start() {
|
||||
g_mouse_y = my;
|
||||
bool left_down = (ev.mouse.buttons & 1) != 0;
|
||||
bool just_clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
|
||||
bool just_right_clicked = (ev.mouse.buttons & 2) && !(ev.mouse.prev_buttons & 2);
|
||||
bool just_released = !(ev.mouse.buttons & 1) && (ev.mouse.prev_buttons & 1);
|
||||
|
||||
ReaderToolbarLayout lo = toolbar_layout();
|
||||
Rect scrollbar_track = scrollbar_track_rect();
|
||||
Rect scrollbar_thumb = scrollbar_thumb_rect();
|
||||
|
||||
if (g_query_input.context.open || g_query_input.dragging) {
|
||||
int result = mtk::text_input_handle_mouse(g_query_input, lo.search_field,
|
||||
g_query, (int)sizeof(g_query),
|
||||
mx, my, ev.mouse.buttons,
|
||||
ev.mouse.prev_buttons,
|
||||
g_win_w, g_win_h,
|
||||
g_search_focused);
|
||||
if (result & mtk::TEXT_INPUT_CONSUMED) {
|
||||
just_clicked = false;
|
||||
just_right_clicked = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (just_released) {
|
||||
g_scroll_dragging = false;
|
||||
}
|
||||
@@ -1165,23 +1184,33 @@ extern "C" void _start() {
|
||||
set_scroll_from_thumb_top(my - g_scroll_drag_offset);
|
||||
}
|
||||
|
||||
if (just_clicked) {
|
||||
g_search_focused = lo.search_field.contains(mx, my);
|
||||
|
||||
if (scrollbar_thumb.contains(mx, my)) {
|
||||
if (just_clicked || just_right_clicked) {
|
||||
if (lo.search_field.contains(mx, my)) {
|
||||
g_search_focused = true;
|
||||
mtk::text_input_handle_mouse(g_query_input, lo.search_field,
|
||||
g_query, (int)sizeof(g_query),
|
||||
mx, my, ev.mouse.buttons, ev.mouse.prev_buttons,
|
||||
g_win_w, g_win_h, true);
|
||||
} else if (scrollbar_thumb.contains(mx, my)) {
|
||||
g_search_focused = false;
|
||||
g_scroll_dragging = true;
|
||||
g_scroll_drag_offset = my - scrollbar_thumb.y;
|
||||
} else if (scrollbar_track.contains(mx, my)) {
|
||||
g_search_focused = false;
|
||||
g_scroll_dragging = true;
|
||||
g_scroll_drag_offset = scrollbar_thumb.h / 2;
|
||||
set_scroll_from_thumb_top(my - g_scroll_drag_offset);
|
||||
} else if (lo.display_button.contains(mx, my)) {
|
||||
g_search_focused = false;
|
||||
g_display_controls_open = !g_display_controls_open;
|
||||
update_toolbar_height();
|
||||
rebuild_article(true);
|
||||
clamp_scroll();
|
||||
} else if (handle_reader_option_click(mx, my)) {
|
||||
g_search_focused = false;
|
||||
// Reader controls handled.
|
||||
} else {
|
||||
g_search_focused = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -542,6 +542,7 @@ void wp_init_empty_document(WordProcessorState* wp) {
|
||||
wp->line_spacing_dropdown_open = false;
|
||||
wp->divider_flyout_open = false;
|
||||
wp->special_char_flyout_open = false;
|
||||
mtk::context_menu_reset(wp->context_menu);
|
||||
wp->page_view_mode = false;
|
||||
wp->pending_page_view_toggle = false;
|
||||
|
||||
|
||||
@@ -65,6 +65,59 @@ static bool wp_paste_from_clipboard(WordProcessorState* wp) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool wp_context_item_enabled(WordProcessorState* wp, int item) {
|
||||
switch (item) {
|
||||
case WP_CTX_CUT:
|
||||
case WP_CTX_COPY:
|
||||
case WP_CTX_DELETE:
|
||||
return wp && wp->has_selection;
|
||||
case WP_CTX_PASTE:
|
||||
return gui::clipboard_has_text();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void wp_build_context_items(WordProcessorState* wp,
|
||||
mtk::ContextMenuItem items[WP_CTX_COUNT]) {
|
||||
items[WP_CTX_CUT] = {"Cut", WP_CTX_CUT,
|
||||
wp_context_item_enabled(wp, WP_CTX_CUT)};
|
||||
items[WP_CTX_COPY] = {"Copy", WP_CTX_COPY,
|
||||
wp_context_item_enabled(wp, WP_CTX_COPY)};
|
||||
items[WP_CTX_PASTE] = {"Paste", WP_CTX_PASTE,
|
||||
wp_context_item_enabled(wp, WP_CTX_PASTE)};
|
||||
items[WP_CTX_DELETE] = {"Delete", WP_CTX_DELETE,
|
||||
wp_context_item_enabled(wp, WP_CTX_DELETE)};
|
||||
}
|
||||
|
||||
static bool wp_selection_contains_abs(WordProcessorState* wp, int abs) {
|
||||
if (!wp || !wp->has_selection) return false;
|
||||
int sel_s = 0;
|
||||
int sel_e = 0;
|
||||
wp_sel_range(wp, &sel_s, &sel_e);
|
||||
return abs >= sel_s && abs < sel_e;
|
||||
}
|
||||
|
||||
static bool wp_perform_context_action(WordProcessorState* wp, int item) {
|
||||
if (!wp_context_item_enabled(wp, item)) return false;
|
||||
|
||||
switch (item) {
|
||||
case WP_CTX_CUT:
|
||||
return wp_cut_selection_to_clipboard(wp);
|
||||
case WP_CTX_COPY:
|
||||
return wp_copy_selection_to_clipboard(wp);
|
||||
case WP_CTX_PASTE:
|
||||
return wp_paste_from_clipboard(wp);
|
||||
case WP_CTX_DELETE:
|
||||
wp_delete_selection(wp);
|
||||
wp_history_checkpoint(wp);
|
||||
wp_set_status(wp, "Deleted selection");
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static void wp_open_pathbar_for_open(WordProcessorState* wp) {
|
||||
char path[256] = {};
|
||||
char msg[160] = {};
|
||||
@@ -255,6 +308,26 @@ void wp_handle_mouse(const Montauk::WinEvent& ev) {
|
||||
recompute_w = WP_A4_PAGE_W;
|
||||
}
|
||||
|
||||
if (wp->context_menu.open) {
|
||||
if (ev.mouse.scroll != 0) {
|
||||
mtk::context_menu_close(wp->context_menu);
|
||||
return;
|
||||
}
|
||||
|
||||
mtk::ContextMenuItem items[WP_CTX_COUNT];
|
||||
wp_build_context_items(wp, items);
|
||||
int action = mtk::context_menu_handle_mouse(wp->context_menu,
|
||||
items, WP_CTX_COUNT,
|
||||
local_x, local_y,
|
||||
ev.mouse.buttons,
|
||||
ev.mouse.prev_buttons,
|
||||
g_win_w, g_win_h);
|
||||
if (action >= 0)
|
||||
wp_perform_context_action(wp, action);
|
||||
if (action != mtk::CONTEXT_MENU_NONE)
|
||||
return;
|
||||
}
|
||||
|
||||
wp->scrollbar.handle_mouse(local_x, local_y, ev.mouse.buttons, ev.mouse.prev_buttons, ev.mouse.scroll);
|
||||
|
||||
if (wp->font_dropdown_open && wp_left_pressed(ev.mouse.buttons, ev.mouse.prev_buttons)) {
|
||||
@@ -464,6 +537,27 @@ void wp_handle_mouse(const Montauk::WinEvent& ev) {
|
||||
|
||||
wp_recompute_wrap(wp, recompute_w, WP_SCREEN_DPI);
|
||||
|
||||
bool right_pressed = wp_right_pressed(ev.mouse.buttons, ev.mouse.prev_buttons);
|
||||
bool in_text_area = local_y >= edit_y && local_y < edit_y + text_area_h &&
|
||||
local_x >= 0 && local_x < g_win_w - WP_SCROLLBAR_W;
|
||||
if (in_text_area && wp->page_view_mode) {
|
||||
in_text_area = local_x >= page_off_x && local_x < page_off_x + WP_A4_PAGE_W &&
|
||||
local_y >= page_off_y && local_y < page_off_y + WP_A4_PAGE_H;
|
||||
}
|
||||
|
||||
if (right_pressed && in_text_area) {
|
||||
wp_close_dropdowns(wp);
|
||||
|
||||
int abs = wp_hit_test_text(wp, local_x, local_y, edit_y, page_off_x, page_off_y);
|
||||
if (!wp_selection_contains_abs(wp, abs)) {
|
||||
wp_pos_to_run(wp, abs, &wp->cursor_run, &wp->cursor_offset);
|
||||
wp_clear_selection(wp);
|
||||
}
|
||||
wp->mouse_selecting = false;
|
||||
mtk::context_menu_open(wp->context_menu, local_x, local_y);
|
||||
return;
|
||||
}
|
||||
|
||||
if (wp_left_pressed(ev.mouse.buttons, ev.mouse.prev_buttons) &&
|
||||
local_y >= edit_y && local_y < edit_y + text_area_h) {
|
||||
wp_close_dropdowns(wp);
|
||||
@@ -502,6 +596,11 @@ void wp_handle_key(const Montauk::KeyEvent& key) {
|
||||
WordProcessorState* wp = &g_wp;
|
||||
if (!key.pressed) return;
|
||||
|
||||
if (wp->context_menu.open) {
|
||||
mtk::context_menu_close(wp->context_menu);
|
||||
if (key.scancode == 0x01) return;
|
||||
}
|
||||
|
||||
if (wp->show_pathbar) {
|
||||
if (key.ascii == '\n' || key.ascii == '\r') {
|
||||
wp_commit_pathbar(wp);
|
||||
|
||||
@@ -35,6 +35,21 @@ static void wp_draw_ui_icon_button(Canvas& c, int x, int y, int w, int h,
|
||||
wp_draw_ui_button(c, x, y, w, h, fallback, bg, fg, radius);
|
||||
}
|
||||
|
||||
static void wp_draw_context_menu(Canvas& c, WordProcessorState* wp) {
|
||||
if (!wp || !wp->context_menu.open) return;
|
||||
|
||||
mtk::Theme theme = mtk::make_theme(colors::ACCENT);
|
||||
theme.border = colors::BORDER;
|
||||
theme.text = colors::TEXT_COLOR;
|
||||
|
||||
mtk::ContextMenuItem items[WP_CTX_COUNT];
|
||||
wp_build_context_items(wp, items);
|
||||
mtk::draw_context_menu(c, wp->context_menu, items, WP_CTX_COUNT, theme,
|
||||
mtk::CONTEXT_MENU_DEFAULT_W,
|
||||
mtk::CONTEXT_MENU_ITEM_H,
|
||||
g_ui_font, fonts::UI_SIZE);
|
||||
}
|
||||
|
||||
static int wp_special_char_flyout_x() {
|
||||
int dx = WP_BTN_SECTION_X + 24 - WP_SPECIAL_CHAR_FLYOUT_W;
|
||||
if (dx + WP_SPECIAL_CHAR_FLYOUT_W > g_win_w) dx = g_win_w - WP_SPECIAL_CHAR_FLYOUT_W;
|
||||
@@ -666,4 +681,6 @@ void wp_render() {
|
||||
WP_SPECIAL_CHAR_OPTIONS[i].label, colors::TEXT_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
wp_draw_context_menu(c, wp);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <gui/standalone.hpp>
|
||||
#include <gui/truetype.hpp>
|
||||
#include <gui/svg.hpp>
|
||||
#include <gui/mtk/widgets.hpp>
|
||||
|
||||
extern "C" {
|
||||
#include <string.h>
|
||||
@@ -239,6 +240,18 @@ inline bool wp_left_released(uint8_t buttons, uint8_t prev_buttons) {
|
||||
return !(buttons & 0x01) && (prev_buttons & 0x01);
|
||||
}
|
||||
|
||||
inline bool wp_right_pressed(uint8_t buttons, uint8_t prev_buttons) {
|
||||
return (buttons & 0x02) && !(prev_buttons & 0x02);
|
||||
}
|
||||
|
||||
enum WpContextItem : int {
|
||||
WP_CTX_CUT = 0,
|
||||
WP_CTX_COPY,
|
||||
WP_CTX_PASTE,
|
||||
WP_CTX_DELETE,
|
||||
WP_CTX_COUNT,
|
||||
};
|
||||
|
||||
struct WpScrollbar {
|
||||
Rect bounds;
|
||||
int content_height;
|
||||
@@ -370,6 +383,7 @@ struct WordProcessorState {
|
||||
bool line_spacing_dropdown_open;
|
||||
bool divider_flyout_open;
|
||||
bool special_char_flyout_open;
|
||||
mtk::ContextMenuState context_menu;
|
||||
bool page_view_mode;
|
||||
bool pending_page_view_toggle;
|
||||
|
||||
@@ -462,6 +476,9 @@ bool wp_redo(WordProcessorState* wp);
|
||||
|
||||
bool wp_print_document(WordProcessorState* wp, char* out_status, int out_status_len);
|
||||
bool wp_export_pdf_document(WordProcessorState* wp, const char* path, char* out_status, int out_status_len);
|
||||
bool wp_context_item_enabled(WordProcessorState* wp, int item);
|
||||
void wp_build_context_items(WordProcessorState* wp,
|
||||
mtk::ContextMenuItem items[WP_CTX_COUNT]);
|
||||
|
||||
void wp_render();
|
||||
void wp_handle_mouse(const Montauk::WinEvent& ev);
|
||||
|
||||
Reference in New Issue
Block a user