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

This commit is contained in:
2026-05-06 08:03:14 +02:00
parent f19e97966d
commit 8997164203
15 changed files with 1337 additions and 256 deletions
@@ -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,50 +25,35 @@ 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 (action >= 0) {
int target = fm->ctx_target_idx;
if (ev.left_pressed()) {
if (fm->ctx_hover >= 0 && fm->ctx_hover < fm->ctx_item_count) {
int action = fm->ctx_items[fm->ctx_hover];
int target = fm->ctx_target_idx;
filemanager_close_ctx_menu(fm);
// Select target if needed
if (target >= 0 && target < fm->entry_count)
fm->selected = target;
// Select target if needed
if (target >= 0 && target < fm->entry_count)
fm->selected = target;
switch (action) {
case CTX_OPEN: filemanager_open_entry(fm, target); break;
case CTX_COPY: filemanager_do_copy(fm); break;
case CTX_CUT: filemanager_do_cut(fm); break;
case CTX_PASTE: filemanager_do_paste(fm); break;
case CTX_RENAME: filemanager_start_rename(fm); break;
case CTX_DELETE: filemanager_delete_selected(fm); break;
case CTX_NEW_FOLDER: filemanager_new_folder(fm); break;
}
} else {
filemanager_close_ctx_menu(fm);
switch (action) {
case CTX_OPEN: filemanager_open_entry(fm, target); break;
case CTX_COPY: filemanager_do_copy(fm); break;
case CTX_CUT: filemanager_do_cut(fm); break;
case CTX_PASTE: filemanager_do_paste(fm); break;
case CTX_RENAME: filemanager_start_rename(fm); break;
case CTX_DELETE: filemanager_delete_selected(fm); break;
case CTX_NEW_FOLDER: filemanager_new_folder(fm); break;
}
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);
}
}
+189 -49
View File
@@ -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) {
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;
}
int result = mtk::TEXT_INPUT_NONE;
switch (st->field) {
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));
}
}
+87 -107
View File
@@ -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) {
int tab = mtk::hit_tab_bar(tab_bar_rect(), TAB_COUNT, mx, my);
if (tab >= 0) {
set_tab((Tab)tab);
return true;
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) {
+46 -14
View File
@@ -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;
+116
View File
@@ -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);
+41 -12
View File
@@ -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;
}
}
+1
View File
@@ -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;
+99
View File
@@ -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);
+17
View File
@@ -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);