/* * dialogs.cpp * User-management dialogs for the embedded desktop user management applet * Copyright (c) 2026 Daniel Hammer */ #include "usermanager_internal.hpp" namespace usermanager_app { static mtk::WidgetState dialog_button_state(int mouse_x, int mouse_y, const Rect& rect, bool enabled = true, bool active = false) { return mtk::widget_state(active, rect.contains(mouse_x, mouse_y), enabled); } 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) { desktop_close_window(ds, i); return; } } } static Rect dialog_primary_button_rect(int pad, int content_h) { return {pad, content_h - USER_BTN_H - pad, 80, USER_BTN_H}; } static Rect dialog_secondary_button_rect(int pad, int content_h) { return {pad + 88, content_h - USER_BTN_H - pad, 80, USER_BTN_H}; } struct AddUserDialogState { DesktopState* ds; UserManagerState* parent; Color accent; int mouse_x; 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]; }; static void adduser_on_draw(Window* win, Framebuffer& fb) { (void)fb; AddUserDialogState* st = (AddUserDialogState*)win->app_data; if (!st) return; Canvas c(win); mtk::Theme theme = mtk::make_theme(st->accent); c.fill(theme.window_bg); int pad = 16; int fw = c.w - 2 * pad; int y = pad; int sfh = system_font_height(); mtk::draw_labeled_text_field(c, pad, y, fw, "Username", st->username, 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_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_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); Rect user_btn = {pad + 60, y, 60, USER_BTN_H}; Rect admin_btn = {pad + 128, y, 60, USER_BTN_H}; mtk::draw_button(c, user_btn, "User", mtk::BUTTON_SECONDARY, dialog_button_state(st->mouse_x, st->mouse_y, user_btn, true, !st->role_admin), theme); mtk::draw_button(c, admin_btn, "Admin", mtk::BUTTON_SECONDARY, dialog_button_state(st->mouse_x, st->mouse_y, admin_btn, true, st->role_admin), theme); y += USER_BTN_H + 14; if (st->error[0]) { c.text(pad, y, st->error, theme.danger); y += sfh + 6; } Rect create_btn = dialog_primary_button_rect(pad, c.h); Rect cancel_btn = dialog_secondary_button_rect(pad, c.h); mtk::draw_modal_actions(c, create_btn, "Create", mtk::BUTTON_PRIMARY, dialog_button_state(st->mouse_x, st->mouse_y, create_btn), 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) { if (st->username_len == 0) { montauk::strcpy(st->error, "Username is required"); return; } if (st->password_len == 0) { montauk::strcpy(st->error, "Password is required"); return; } const char* dname = st->display_len > 0 ? st->display : st->username; const char* role = st->role_admin ? "admin" : "user"; if (montauk::user::create_user(st->username, dname, st->password, role)) { st->parent->users_loaded = false; usermanager_set_status(st->parent, "User created"); dialog_close_self(st->ds, st); } else { montauk::strcpy(st->error, "Failed (username taken?)"); } } static void adduser_on_mouse(Window* win, MouseEvent& ev) { AddUserDialogState* st = (AddUserDialogState*)win->app_data; if (!st) return; mtk::DesktopHost host(win); int mx = 0; int my = 0; if (!host.map_mouse(ev, &mx, &my)) return; st->mouse_x = mx; st->mouse_y = my; 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 (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 (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 (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)) { st->role_admin = false; return; } if (admin_btn.contains(mx, my)) { st->role_admin = true; return; } Rect create_btn = dialog_primary_button_rect(pad, win->content_h); Rect cancel_btn = dialog_secondary_button_rect(pad, win->content_h); if (create_btn.contains(mx, my)) { adduser_submit(st); return; } if (cancel_btn.contains(mx, my)) { dialog_close_self(st->ds, st); return; } } static void adduser_on_key(Window* win, const montauk::abi::KeyEvent& key) { AddUserDialogState* st = (AddUserDialogState*)win->app_data; if (!st || !key.pressed) return; if (key.ascii == '\n' || key.ascii == '\r' || key.scancode == 0x1C) { adduser_submit(st); return; } if (key.scancode == 0x01) { dialog_close_self(st->ds, st); return; } if (key.scancode == 0x0F) { st->field = (st->field + 1) % 3; return; } 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; } } static void adduser_on_close(Window* win) { if (win->app_data) { montauk::mfree(win->app_data); win->app_data = nullptr; } } void open_add_user_dialog(UserManagerState* parent) { DesktopState* ds = parent->desktop; int w = 340; int h = 340; int wx = (ds->screen_w - w) / 2; int wy = (ds->screen_h - h) / 2; int idx = desktop_create_window(ds, "Add User", wx, wy, w, h); if (idx < 0) return; Window* win = &ds->windows[idx]; AddUserDialogState* st = (AddUserDialogState*)montauk::malloc(sizeof(AddUserDialogState)); montauk::memset(st, 0, sizeof(AddUserDialogState)); st->ds = ds; st->parent = parent; st->accent = ds->settings.accent_color; st->mouse_x = -1; st->mouse_y = -1; win->app_data = st; win->on_draw = adduser_on_draw; win->on_mouse = adduser_on_mouse; win->on_key = adduser_on_key; win->on_close = adduser_on_close; } struct ChPwdDialogState { DesktopState* ds; UserManagerState* parent; Color accent; int mouse_x; int mouse_y; char username[32]; 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]; }; static void chpwd_on_draw(Window* win, Framebuffer& fb) { (void)fb; ChPwdDialogState* st = (ChPwdDialogState*)win->app_data; if (!st) return; Canvas c(win); mtk::Theme theme = mtk::make_theme(st->accent); c.fill(theme.window_bg); int pad = 16; int fw = c.w - 2 * pad; int y = pad; int sfh = system_font_height(); char title[80]; snprintf(title, sizeof(title), "Change password for %s", st->display_name); c.text(pad, y, title, theme.text); y += sfh + 12; mtk::draw_labeled_text_field(c, pad, y, fw, "New Password", st->new_pwd, 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_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]) { c.text(pad, y, st->error, theme.danger); } Rect save_btn = dialog_primary_button_rect(pad, c.h); Rect cancel_btn = dialog_secondary_button_rect(pad, c.h); mtk::draw_modal_actions(c, save_btn, "Save", mtk::BUTTON_PRIMARY, dialog_button_state(st->mouse_x, st->mouse_y, save_btn), 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) { if (st->new_len == 0) { montauk::strcpy(st->error, "Password cannot be empty"); return; } if (!montauk::streq(st->new_pwd, st->confirm)) { montauk::strcpy(st->error, "Passwords don't match"); return; } montauk::user::change_password(st->username, st->new_pwd); usermanager_set_status(st->parent, "Password changed"); dialog_close_self(st->ds, st); } static void chpwd_on_mouse(Window* win, MouseEvent& ev) { ChPwdDialogState* st = (ChPwdDialogState*)win->app_data; if (!st) return; mtk::DesktopHost host(win); int mx = 0; int my = 0; if (!host.map_mouse(ev, &mx, &my)) return; st->mouse_x = mx; st->mouse_y = my; 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 (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 (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); if (save_btn.contains(mx, my)) { chpwd_submit(st); return; } if (cancel_btn.contains(mx, my)) { dialog_close_self(st->ds, st); return; } } static void chpwd_on_key(Window* win, const montauk::abi::KeyEvent& key) { ChPwdDialogState* st = (ChPwdDialogState*)win->app_data; if (!st || !key.pressed) return; if (key.ascii == '\n' || key.ascii == '\r' || key.scancode == 0x1C) { chpwd_submit(st); return; } if (key.scancode == 0x01) { dialog_close_self(st->ds, st); return; } if (key.scancode == 0x0F) { st->field = (st->field + 1) % 2; return; } 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)); } } static void chpwd_on_close(Window* win) { if (win->app_data) { montauk::mfree(win->app_data); win->app_data = nullptr; } } void open_change_pwd_dialog(UserManagerState* parent) { if (parent->selected_user < 0) return; DesktopState* ds = parent->desktop; int w = 340; int h = 260; int wx = (ds->screen_w - w) / 2; int wy = (ds->screen_h - h) / 2; int idx = desktop_create_window(ds, "Change Password", wx, wy, w, h); if (idx < 0) return; Window* win = &ds->windows[idx]; ChPwdDialogState* st = (ChPwdDialogState*)montauk::malloc(sizeof(ChPwdDialogState)); montauk::memset(st, 0, sizeof(ChPwdDialogState)); st->ds = ds; st->parent = parent; st->accent = ds->settings.accent_color; st->mouse_x = -1; st->mouse_y = -1; montauk::strncpy(st->username, parent->users[parent->selected_user].username, 31); montauk::strncpy(st->display_name, parent->users[parent->selected_user].display_name, 63); win->app_data = st; win->on_draw = chpwd_on_draw; win->on_mouse = chpwd_on_mouse; win->on_key = chpwd_on_key; win->on_close = chpwd_on_close; } struct DeleteUserDialogState { DesktopState* ds; UserManagerState* parent; char username[32]; bool hover_delete; bool hover_cancel; }; static void deluser_on_draw(Window* win, Framebuffer& fb) { (void)fb; DeleteUserDialogState* st = (DeleteUserDialogState*)win->app_data; if (!st) return; Canvas c(win); mtk::Theme theme = usermanager_theme(st->parent); c.fill(theme.window_bg); int sfh = system_font_height(); char msg[96]; snprintf(msg, sizeof(msg), "Delete user \"%s\"?", st->username); int tw = text_width(msg); c.text((c.w - tw) / 2, 24, msg, theme.text); const char* warn = "This action cannot be undone."; tw = text_width(warn); c.text((c.w - tw) / 2, 24 + sfh + 8, warn, theme.text_muted); int btn_w = 100; int btn_h = 32; int btn_y = c.h - btn_h - 20; int gap = 20; int total = btn_w * 2 + gap; int bx = (c.w - total) / 2; Rect delete_btn = {bx, btn_y, btn_w, btn_h}; Rect cancel_btn = {bx + btn_w + gap, btn_y, btn_w, btn_h}; mtk::draw_button(c, delete_btn, "Delete", mtk::BUTTON_DANGER, mtk::widget_state(false, st->hover_delete), theme); mtk::draw_button(c, cancel_btn, "Cancel", mtk::BUTTON_SECONDARY, mtk::widget_state(false, st->hover_cancel), theme); } static void deluser_on_mouse(Window* win, MouseEvent& ev) { DeleteUserDialogState* st = (DeleteUserDialogState*)win->app_data; if (!st) return; mtk::DesktopHost host(win); int mx = 0; int my = 0; if (!host.map_mouse(ev, &mx, &my)) return; int btn_w = 100; int btn_h = 32; int btn_y = win->content_h - btn_h - 20; int gap = 20; int total = btn_w * 2 + gap; int bx = (win->content_w - total) / 2; Rect delete_btn = {bx, btn_y, btn_w, btn_h}; Rect cancel_btn = {bx + btn_w + gap, btn_y, btn_w, btn_h}; bool old_delete = st->hover_delete; bool old_cancel = st->hover_cancel; st->hover_delete = delete_btn.contains(mx, my); st->hover_cancel = cancel_btn.contains(mx, my); if (old_delete != st->hover_delete || old_cancel != st->hover_cancel) { host.invalidate(); } if (ev.left_pressed()) { if (st->hover_delete) { montauk::user::delete_user(st->username); st->parent->users_loaded = false; st->parent->selected_user = -1; usermanager_set_status(st->parent, "User deleted"); dialog_close_self(st->ds, st); return; } if (st->hover_cancel) { dialog_close_self(st->ds, st); return; } } } static void deluser_on_key(Window* win, const montauk::abi::KeyEvent& key) { DeleteUserDialogState* st = (DeleteUserDialogState*)win->app_data; if (!st || !key.pressed) return; if (key.ascii == '\n' || key.ascii == '\r') { montauk::user::delete_user(st->username); st->parent->users_loaded = false; st->parent->selected_user = -1; usermanager_set_status(st->parent, "User deleted"); dialog_close_self(st->ds, st); } if (key.scancode == 0x01) { dialog_close_self(st->ds, st); } } static void deluser_on_close(Window* win) { if (win->app_data) { montauk::mfree(win->app_data); win->app_data = nullptr; } } void open_delete_user_dialog(UserManagerState* parent) { if (parent->selected_user < 0) return; DesktopState* ds = parent->desktop; if (montauk::streq(parent->users[parent->selected_user].username, ds->current_user)) { usermanager_set_status(parent, "Cannot delete current user"); return; } int w = 300; int h = 150; int wx = (ds->screen_w - w) / 2; int wy = (ds->screen_h - h) / 2; int idx = desktop_create_window(ds, "Delete User", wx, wy, w, h); if (idx < 0) return; Window* win = &ds->windows[idx]; DeleteUserDialogState* st = (DeleteUserDialogState*)montauk::malloc(sizeof(DeleteUserDialogState)); montauk::memset(st, 0, sizeof(DeleteUserDialogState)); st->ds = ds; st->parent = parent; montauk::strncpy(st->username, parent->users[parent->selected_user].username, 31); win->app_data = st; win->on_draw = deluser_on_draw; win->on_mouse = deluser_on_mouse; win->on_key = deluser_on_key; win->on_close = deluser_on_close; } } // namespace usermanager_app