feat: system settings stored as applets in virtual folder

This commit is contained in:
2026-04-23 15:36:03 +02:00
parent de96c1ca57
commit be67fce29a
21 changed files with 747 additions and 359 deletions
@@ -0,0 +1,69 @@
/*
* app.cpp
* User management applet launcher and desktop integration entry points
* Copyright (c) 2026 Daniel Hammer
*/
#include "usermanager_internal.hpp"
namespace usermanager_app {
static void usermanager_on_draw(Window* win, Framebuffer& fb) {
(void)fb;
UserManagerState* st = (UserManagerState*)win->app_data;
if (!st) return;
Canvas c(win);
usermanager_draw(c, st);
}
static void usermanager_on_mouse(Window* win, MouseEvent& ev) {
UserManagerState* st = (UserManagerState*)win->app_data;
if (!st) return;
mtk::DesktopHost host(win);
int mx = 0;
int my = 0;
if (!host.map_mouse(ev, &mx, &my)) return;
usermanager_update_mouse(st, mx, my);
if (ev.left_pressed()) {
usermanager_handle_click(win, st, mx, my);
}
}
static void usermanager_on_key(Window* win, const Montauk::KeyEvent& key) {
(void)win;
(void)key;
}
static void usermanager_on_close(Window* win) {
if (win->app_data) {
montauk::mfree(win->app_data);
win->app_data = nullptr;
}
}
} // namespace usermanager_app
void open_user_manager(DesktopState* ds) {
int idx = desktop_create_window(ds, "User Management", 200, 100, 480, 384);
if (idx < 0) return;
Window* win = &ds->windows[idx];
usermanager_app::UserManagerState* st =
(usermanager_app::UserManagerState*)montauk::malloc(sizeof(usermanager_app::UserManagerState));
montauk::memset(st, 0, sizeof(usermanager_app::UserManagerState));
st->desktop = ds;
st->selected_user = -1;
st->users_loaded = false;
st->status_msg[0] = '\0';
st->mouse_x = -1;
st->mouse_y = -1;
win->app_data = st;
win->on_draw = usermanager_app::usermanager_on_draw;
win->on_mouse = usermanager_app::usermanager_on_mouse;
win->on_key = usermanager_app::usermanager_on_key;
win->on_close = usermanager_app::usermanager_on_close;
}
@@ -0,0 +1,563 @@
/*
* 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_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) {
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;
char display[64]; int display_len;
char password[64]; int password_len;
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_len, st->field == 0, false, theme, USER_FIELD_H);
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);
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);
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);
}
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;
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;
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; }
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; }
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; }
y += mtk::labeled_text_height(theme, USER_FIELD_H) + 12;
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::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;
}
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;
}
}
}
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;
char confirm[64]; int confirm_len;
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_len, st->field == 0, true, theme, USER_FIELD_H);
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);
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);
}
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;
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;
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; }
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; }
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::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 (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);
}
}
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::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
@@ -0,0 +1,48 @@
/*
* usermanager_internal.hpp
* Shared declarations for the embedded desktop user management applet
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include "../apps_common.hpp"
#include <gui/mtk.hpp>
#include <montauk/user.h>
namespace usermanager_app {
struct UserManagerState {
DesktopState* desktop;
montauk::user::UserInfo users[16];
int user_count;
int selected_user;
bool users_loaded;
char status_msg[128];
uint64_t status_time;
int mouse_x;
int mouse_y;
};
inline constexpr int USER_BTN_H = 30;
inline constexpr int USER_BTN_W = 100;
inline constexpr int USER_FIELD_H = 32;
inline mtk::Theme usermanager_theme(const UserManagerState* st) {
return mtk::make_theme(st->desktop->settings.accent_color);
}
bool usermanager_status_visible(UserManagerState* st);
void usermanager_set_status(UserManagerState* st, const char* msg);
void usermanager_reload(UserManagerState* st);
int user_row_height();
void usermanager_update_mouse(UserManagerState* st, int mx, int my);
void usermanager_draw(Canvas& c, UserManagerState* st);
bool usermanager_handle_click(Window* win, UserManagerState* st, int mx, int my);
void open_add_user_dialog(UserManagerState* parent);
void open_change_pwd_dialog(UserManagerState* parent);
void open_delete_user_dialog(UserManagerState* parent);
} // namespace usermanager_app
@@ -0,0 +1,163 @@
/*
* users.cpp
* User management view for the embedded desktop user management applet
* Copyright (c) 2026 Daniel Hammer
*/
#include "usermanager_internal.hpp"
namespace usermanager_app {
static mtk::WidgetState button_state(const UserManagerState* st,
const Rect& rect,
bool enabled,
bool active = false) {
return mtk::widget_state(active, rect.contains(st->mouse_x, st->mouse_y), enabled);
}
void usermanager_reload(UserManagerState* st) {
st->user_count = montauk::user::load_users(st->users, 16);
st->users_loaded = true;
}
bool usermanager_status_visible(UserManagerState* st) {
return st->status_msg[0] &&
(montauk::get_milliseconds() - st->status_time < 4000);
}
void usermanager_set_status(UserManagerState* st, const char* msg) {
montauk::strncpy(st->status_msg, msg ? msg : "", (int)sizeof(st->status_msg));
st->status_time = montauk::get_milliseconds();
}
int user_row_height() {
return system_font_height() * 2 + 12;
}
void usermanager_update_mouse(UserManagerState* st, int mx, int my) {
if (!st) return;
st->mouse_x = mx;
st->mouse_y = my;
}
void usermanager_draw(Canvas& c, UserManagerState* st) {
if (!st->users_loaded) usermanager_reload(st);
mtk::Theme theme = usermanager_theme(st);
int x = 16;
int y = 20;
int sfh = system_font_height();
int content_w = c.w - 2 * x;
int row_h = user_row_height();
c.fill(theme.window_bg);
if (!st->desktop->is_admin) {
c.text(x, y, "Admin access required to manage users.", theme.text_muted);
return;
}
for (int i = 0; i < st->user_count; i++) {
bool selected = (i == st->selected_user);
bool is_current = montauk::streq(st->users[i].username, st->desktop->current_user);
Rect row = {x, y, content_w, row_h};
mtk::draw_list_row(c, row, selected, (i % 2) == 0, theme);
Color text_color = selected ? theme.text_inverse : theme.text;
Color sub_color = selected ? Color::from_rgb(0xE3, 0xEA, 0xFF) : theme.text_muted;
int text_y = y + 4;
c.text(x + 12, text_y, st->users[i].display_name, text_color);
char sub[48];
if (is_current) {
snprintf(sub, sizeof(sub), "@%s (you)", st->users[i].username);
} else {
snprintf(sub, sizeof(sub), "@%s", st->users[i].username);
}
c.text(x + 12, text_y + sfh + 2, sub, sub_color);
const char* role = st->users[i].role;
int badge_w = mtk::badge_width(role, theme);
int badge_h = mtk::badge_height();
int badge_x = c.w - x - badge_w - 8;
int badge_y = y + (row_h - badge_h) / 2;
if (selected) {
mtk::draw_badge(c, badge_x, badge_y, role, colors::WHITE, theme.accent, theme);
} else if (montauk::streq(role, "admin")) {
mtk::draw_badge(c, badge_x, badge_y, role,
theme.badge_admin_bg, theme.badge_admin_fg, theme);
} else {
mtk::draw_badge(c, badge_x, badge_y, role,
theme.badge_user_bg, theme.badge_user_fg, theme);
}
y += row_h + 4;
}
y += 8;
mtk::draw_separator(c, x, y, content_w, theme);
y += 16;
if (usermanager_status_visible(st)) {
c.text(x, y, st->status_msg, theme.accent);
y += sfh + 8;
}
Rect add_btn = {x, y, 90, USER_BTN_H};
Rect change_btn = {x + 98, y, USER_BTN_W, USER_BTN_H};
Rect delete_btn = {x + 98 + USER_BTN_W + 8, y, 70, USER_BTN_H};
bool has_selection = st->selected_user >= 0;
mtk::draw_button(c, add_btn, "Add User", mtk::BUTTON_PRIMARY,
button_state(st, add_btn, true), theme);
mtk::draw_button(c, change_btn, "Change Pwd", mtk::BUTTON_SECONDARY,
button_state(st, change_btn, has_selection), theme);
mtk::draw_button(c, delete_btn, "Delete", mtk::BUTTON_DANGER,
button_state(st, delete_btn, has_selection), theme);
}
bool usermanager_handle_click(Window* win, UserManagerState* st, int mx, int my) {
if (!st->desktop->is_admin) return false;
if (!st->users_loaded) usermanager_reload(st);
int x = 16;
int sfh = system_font_height();
int y = 20;
int content_w = win->content_w - 2 * x;
int row_h = user_row_height();
for (int i = 0; i < st->user_count; i++) {
Rect row = {x, y, content_w, row_h};
if (row.contains(mx, my)) {
st->selected_user = (st->selected_user == i) ? -1 : i;
return true;
}
y += row_h + 4;
}
y += 8 + 16;
if (usermanager_status_visible(st)) {
y += sfh + 8;
}
Rect add_btn = {x, y, 90, USER_BTN_H};
Rect change_btn = {x + 98, y, USER_BTN_W, USER_BTN_H};
Rect delete_btn = {x + 98 + USER_BTN_W + 8, y, 70, USER_BTN_H};
if (add_btn.contains(mx, my)) {
open_add_user_dialog(st);
return true;
}
if (change_btn.contains(mx, my)) {
if (st->selected_user >= 0) open_change_pwd_dialog(st);
return true;
}
if (delete_btn.contains(mx, my)) {
if (st->selected_user >= 0) open_delete_user_dialog(st);
return true;
}
return false;
}
} // namespace usermanager_app