feat: multi-user system, bug fixes, security & performance fixes, and more

This commit is contained in:
2026-03-14 13:28:46 +01:00
parent 576ad34f95
commit 261b536041
389 changed files with 231853 additions and 591 deletions
+4 -1
View File
@@ -18,6 +18,7 @@ endif
PROG_INC := ../../include
JPEG_LIB := ../../lib/libjpeg
LIBC_LIB := ../../lib/libc
BEARSSL := ../../lib/bearssl
LINK_LD := ../../link.ld
BINDIR := ../../bin
OBJDIR := obj
@@ -48,6 +49,8 @@ CXXFLAGS := \
-mcmodel=small \
-MMD -MP \
-I $(PROG_INC) \
-isystem $(PROG_INC)/libc \
-I $(BEARSSL)/inc \
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
-isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include
@@ -79,7 +82,7 @@ all: $(TARGET)
# ---- Libraries ----
LIBS := $(JPEG_LIB)/libjpeg.a $(LIBC_LIB)/liblibc.a
LIBS := $(JPEG_LIB)/libjpeg.a $(BEARSSL)/libbearssl.a $(LIBC_LIB)/liblibc.a
$(TARGET): $(OBJS) $(LIBS) $(LINK_LD) Makefile
mkdir -p $(BINDIR)/os
@@ -360,7 +360,12 @@ static void filemanager_go_up(FileManagerState* fm) {
if (fm->current_path[i] == '/') { last_slash = i; break; }
}
if (last_slash >= 0) {
fm->current_path[last_slash + 1] = '\0';
// Keep the slash only if it's the root slash (right after "N:")
if (last_slash > 0 && fm->current_path[last_slash - 1] == ':') {
fm->current_path[last_slash + 1] = '\0';
} else {
fm->current_path[last_slash] = '\0';
}
}
filemanager_push_history(fm);
filemanager_read_dir(fm);
+799 -10
View File
@@ -6,6 +6,8 @@
#include "apps_common.hpp"
#include "../wallpaper.hpp"
#include <montauk/config.h>
#include <montauk/user.h>
// ============================================================================
// Settings state
@@ -13,11 +15,20 @@
struct SettingsState {
DesktopState* desktop;
int active_tab; // 0=Appearance, 1=Display, 2=About
int active_tab; // 0=Appearance, 1=Display, 2=Users, 3=About
Montauk::SysInfo sys_info;
uint64_t uptime_ms;
WallpaperFileList wp_files;
bool wp_scanned;
// Users tab
montauk::user::UserInfo users[16];
int user_count;
int selected_user; // index into users[], or -1
bool users_loaded;
char status_msg[128];
uint64_t status_time;
};
// ============================================================================
@@ -69,8 +80,8 @@ static const Color accent_palette[SWATCH_COUNT] = {
// ============================================================================
static constexpr int TAB_BAR_H = 36;
static constexpr int TAB_COUNT = 3;
static const char* tab_labels[TAB_COUNT] = { "Appearance", "Display", "About" };
static constexpr int TAB_COUNT = 4;
static const char* tab_labels[TAB_COUNT] = { "Appearance", "Display", "Users", "About" };
// ============================================================================
// Helper: check if two colors match
@@ -166,11 +177,11 @@ static void settings_draw_appearance(Canvas& c, SettingsState* st) {
if (mode_image) {
// Scan for images lazily
if (!st->wp_scanned) {
wallpaper_scan_dir("0:/home", &st->wp_files);
wallpaper_scan_dir(st->desktop->home_dir, &st->wp_files);
st->wp_scanned = true;
}
c.text(x, y, "Images in 0:/home/", dim);
c.text(x, y, "Wallpapers", dim);
y += sfh + 6;
if (st->wp_files.count == 0) {
@@ -180,7 +191,8 @@ static void settings_draw_appearance(Canvas& c, SettingsState* st) {
for (int i = 0; i < st->wp_files.count && i < 8; i++) {
// Build full path for comparison
char fullpath[256];
montauk::strcpy(fullpath, "0:/home/");
montauk::strcpy(fullpath, st->desktop->home_dir);
str_append(fullpath, "/", 256);
str_append(fullpath, st->wp_files.names[i], 256);
bool selected = s.bg_image &&
@@ -333,6 +345,702 @@ static void settings_draw_about(Canvas& c, SettingsState* st) {
c.text(x, y, "Copyright (c) 2026 Daniel Hammer", dim);
}
// ============================================================================
// Config persistence
// ============================================================================
static int64_t color_to_int(Color c) {
return ((int64_t)c.r << 16) | ((int64_t)c.g << 8) | c.b;
}
static Color int_to_color(int64_t v) {
return Color::from_rgb((uint8_t)((v >> 16) & 0xFF),
(uint8_t)((v >> 8) & 0xFF),
(uint8_t)(v & 0xFF));
}
static void settings_persist(SettingsState* st) {
DesktopSettings& s = st->desktop->settings;
const char* user = st->desktop->current_user;
montauk::toml::Doc doc;
doc.init();
// Background mode
const char* mode = s.bg_image ? "image" : (s.bg_gradient ? "gradient" : "solid");
montauk::config::set_string(&doc, "background.mode", mode);
// Wallpaper path
if (s.bg_image && s.bg_image_path[0])
montauk::config::set_string(&doc, "wallpaper.path", s.bg_image_path);
// Background colors
montauk::config::set_int(&doc, "background.solid_color", color_to_int(s.bg_solid));
montauk::config::set_int(&doc, "background.grad_top", color_to_int(s.bg_grad_top));
montauk::config::set_int(&doc, "background.grad_bottom", color_to_int(s.bg_grad_bottom));
// Appearance
montauk::config::set_int(&doc, "appearance.panel_color", color_to_int(s.panel_color));
montauk::config::set_int(&doc, "appearance.accent_color", color_to_int(s.accent_color));
// Display
montauk::config::set_int(&doc, "display.ui_scale", s.ui_scale);
montauk::config::set_bool(&doc, "display.clock_24h", s.clock_24h);
montauk::config::set_bool(&doc, "display.show_shadows", s.show_shadows);
montauk::config::save_user(user, "desktop", &doc);
doc.destroy();
}
// ============================================================================
// Users tab
// ============================================================================
static void users_reload(SettingsState* st) {
st->user_count = montauk::user::load_users(st->users, 16);
st->users_loaded = true;
}
static constexpr int USER_BTN_H = 30;
static constexpr int USER_BTN_W = 100;
static constexpr int USER_FIELD_H = 32;
// Forward declarations for dialog openers
static void open_add_user_dialog(SettingsState* st);
static void open_change_pwd_dialog(SettingsState* st);
static void open_delete_user_dialog(SettingsState* st);
static int user_row_height() {
return system_font_height() * 2 + 12; // Two lines of text + padding
}
static void settings_draw_users(Canvas& c, SettingsState* st) {
if (!st->users_loaded) users_reload(st);
Color accent = st->desktop->settings.accent_color;
Color dim = Color::from_rgb(0x88, 0x88, 0x88);
Color card_bg = Color::from_rgb(0xF8, 0xF8, 0xF8);
int x = 16;
int y = 20;
int sfh = system_font_height();
int content_w = c.w - 2 * x;
int row_h = user_row_height();
if (!st->desktop->is_admin) {
c.text(x, y, "Admin access required to manage users.", dim);
return;
}
// User list
for (int i = 0; i < st->user_count; i++) {
bool sel = (i == st->selected_user);
bool is_current = montauk::streq(st->users[i].username, st->desktop->current_user);
if (sel) {
c.fill_rounded_rect(x, y, content_w, row_h, 4, accent);
} else if (i % 2 == 0) {
c.fill_rounded_rect(x, y, content_w, row_h, 4, card_bg);
}
Color tc = sel ? colors::WHITE : colors::TEXT_COLOR;
Color sc = sel ? Color::from_rgb(0xDD, 0xDD, 0xFF) : dim;
// Display name (primary line)
int text_y = y + 4;
c.text(x + 12, text_y, st->users[i].display_name, tc);
// Username (secondary line)
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, sc);
// Role badge
const char* role = st->users[i].role;
int rw = text_width(role) + 12;
int badge_x = c.w - x - rw - 8;
int badge_y = y + (row_h - sfh - 4) / 2;
if (sel) {
c.fill_rounded_rect(badge_x, badge_y, rw, sfh + 4, (sfh + 4) / 2,
Color::from_rgb(0xFF, 0xFF, 0xFF));
c.text(badge_x + 6, badge_y + 2, role, accent);
} else {
bool is_admin_role = montauk::streq(role, "admin");
Color badge_bg = is_admin_role
? Color::from_rgb(0xE8, 0xD8, 0xF0)
: Color::from_rgb(0xE0, 0xE8, 0xF0);
Color badge_fg = is_admin_role
? Color::from_rgb(0x7B, 0x3E, 0xB8)
: Color::from_rgb(0x36, 0x7B, 0xF0);
c.fill_rounded_rect(badge_x, badge_y, rw, sfh + 4, (sfh + 4) / 2, badge_bg);
c.text(badge_x + 6, badge_y + 2, role, badge_fg);
}
y += row_h + 4;
}
// Separator
y += 8;
c.hline(x, y, content_w, colors::BORDER);
y += 16;
// Status message
if (st->status_msg[0] && (montauk::get_milliseconds() - st->status_time < 3000)) {
c.text(x, y, st->status_msg, accent);
y += sfh + 8;
}
// Action buttons
int btn_x = x;
// Add User
c.fill_rounded_rect(btn_x, y, 90, USER_BTN_H, 4, accent);
{
int tw = text_width("Add User");
c.text(btn_x + (90 - tw) / 2, y + (USER_BTN_H - sfh) / 2, "Add User", colors::WHITE);
}
btn_x += 98;
// Change Password (disabled when no selection)
{
bool enabled = st->selected_user >= 0;
Color bg = enabled ? accent : Color::from_rgb(0xCC, 0xCC, 0xCC);
c.fill_rounded_rect(btn_x, y, USER_BTN_W, USER_BTN_H, 4, bg);
int tw = text_width("Change Pwd");
c.text(btn_x + (USER_BTN_W - tw) / 2, y + (USER_BTN_H - sfh) / 2, "Change Pwd", colors::WHITE);
}
btn_x += USER_BTN_W + 8;
// Delete (disabled when no selection)
{
bool enabled = st->selected_user >= 0;
Color del_bg = enabled ? Color::from_rgb(0xD0, 0x3E, 0x3E) : Color::from_rgb(0xCC, 0xCC, 0xCC);
c.fill_rounded_rect(btn_x, y, 70, USER_BTN_H, 4, del_bg);
int tw = text_width("Delete");
c.text(btn_x + (70 - tw) / 2, y + (USER_BTN_H - sfh) / 2, "Delete", colors::WHITE);
}
}
// ============================================================================
// Dialog helpers
// ============================================================================
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 void draw_input_field(Canvas& c, int x, int y, int w, int h,
const char* label, const char* value,
bool focused, bool masked, Color accent) {
int sfh = system_font_height();
Color dim = Color::from_rgb(0x88, 0x88, 0x88);
c.text(x, y, label, dim);
y += sfh + 2;
Color border = focused ? accent : colors::BORDER;
c.fill_rounded_rect(x, y, w, h, 3, Color::from_rgb(0xF5, 0xF5, 0xF5));
c.rect(x, y, w, h, border);
if (masked) {
int vlen = montauk::slen(value);
char dots[65];
for (int i = 0; i < vlen && i < 64; i++) dots[i] = '*';
dots[vlen < 64 ? vlen : 64] = '\0';
c.text(x + 8, y + (h - sfh) / 2, dots, colors::TEXT_COLOR);
} else {
c.text(x + 8, y + (h - sfh) / 2, value, colors::TEXT_COLOR);
}
if (focused) {
int tw = masked ? text_width("*") * montauk::slen(value) : text_width(value);
c.fill_rect(x + 8 + tw, y + 6, 2, h - 12, accent);
}
}
// ============================================================================
// Add User Dialog
// ============================================================================
struct AddUserDialogState {
DesktopState* ds;
SettingsState* parent;
Color accent;
int field; // 0=username, 1=display_name, 2=password
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) {
AddUserDialogState* st = (AddUserDialogState*)win->app_data;
if (!st) return;
Canvas c(win);
c.fill(colors::WINDOW_BG);
Color accent = st->accent;
int sfh = system_font_height();
int pad = 16;
int fw = c.w - 2 * pad;
int y = pad;
// Fields
draw_input_field(c, pad, y, fw, USER_FIELD_H, "Username", st->username,
st->field == 0, false, accent);
y += sfh + 2 + USER_FIELD_H + 10;
draw_input_field(c, pad, y, fw, USER_FIELD_H, "Display Name", st->display,
st->field == 1, false, accent);
y += sfh + 2 + USER_FIELD_H + 10;
draw_input_field(c, pad, y, fw, USER_FIELD_H, "Password", st->password,
st->field == 2, true, accent);
y += sfh + 2 + USER_FIELD_H + 12;
// Role toggle
Color dim = Color::from_rgb(0x88, 0x88, 0x88);
c.text(pad, y + 4, "Role:", dim);
int rbx = pad + 60;
draw_toggle_btn(c, rbx, y, 60, USER_BTN_H, "User", !st->role_admin, accent);
draw_toggle_btn(c, rbx + 68, y, 60, USER_BTN_H, "Admin", st->role_admin, accent);
y += USER_BTN_H + 14;
// Error message
if (st->error[0]) {
c.text(pad, y, st->error, Color::from_rgb(0xD0, 0x3E, 0x3E));
y += sfh + 6;
}
// Buttons at bottom
int btn_y = c.h - USER_BTN_H - pad;
c.fill_rounded_rect(pad, btn_y, 80, USER_BTN_H, 4, accent);
int tw = text_width("Create");
c.text(pad + (80 - tw) / 2, btn_y + (USER_BTN_H - sfh) / 2, "Create", colors::WHITE);
c.fill_rounded_rect(pad + 88, btn_y, 80, USER_BTN_H, 4, colors::WINDOW_BG);
c.rect(pad + 88, btn_y, 80, USER_BTN_H, colors::BORDER);
tw = text_width("Cancel");
c.text(pad + 88 + (80 - tw) / 2, btn_y + (USER_BTN_H - sfh) / 2, "Cancel", colors::TEXT_COLOR);
}
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;
montauk::strcpy(st->parent->status_msg, "User created");
st->parent->status_time = montauk::get_milliseconds();
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 || !ev.left_pressed()) return;
Rect cr = win->content_rect();
int mx = ev.x - cr.x;
int my = ev.y - cr.y;
int pad = 16;
int sfh = system_font_height();
int fw = win->content_w - 2 * pad;
int y = pad;
// Username field hit
y += sfh + 2;
if (my >= y && my < y + USER_FIELD_H) { st->field = 0; return; }
y += USER_FIELD_H + 10;
// Display field hit
y += sfh + 2;
if (my >= y && my < y + USER_FIELD_H) { st->field = 1; return; }
y += USER_FIELD_H + 10;
// Password field hit
y += sfh + 2;
if (my >= y && my < y + USER_FIELD_H) { st->field = 2; return; }
y += USER_FIELD_H + 12;
// Role toggle
int rbx = pad + 60;
if (mx >= rbx && mx < rbx + 60 && my >= y && my < y + USER_BTN_H) {
st->role_admin = false; return;
}
if (mx >= rbx + 68 && mx < rbx + 128 && my >= y && my < y + USER_BTN_H) {
st->role_admin = true; return;
}
// Bottom buttons
int btn_y = win->content_h - USER_BTN_H - pad;
if (my >= btn_y && my < btn_y + USER_BTN_H) {
if (mx >= pad && mx < pad + 80) { adduser_submit(st); return; }
if (mx >= pad + 88 && mx < pad + 168) { 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; }
}
static void open_add_user_dialog(SettingsState* parent) {
DesktopState* ds = parent->desktop;
int w = 340, 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;
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;
}
// ============================================================================
// Change Password Dialog
// ============================================================================
struct ChPwdDialogState {
DesktopState* ds;
SettingsState* parent;
Color accent;
char username[32];
char display_name[64];
int field; // 0=new, 1=confirm
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) {
ChPwdDialogState* st = (ChPwdDialogState*)win->app_data;
if (!st) return;
Canvas c(win);
c.fill(colors::WINDOW_BG);
Color accent = st->accent;
int sfh = system_font_height();
int pad = 16;
int fw = c.w - 2 * pad;
int y = pad;
// Title
char title[80];
snprintf(title, sizeof(title), "Change password for %s", st->display_name);
c.text(pad, y, title, colors::TEXT_COLOR);
y += sfh + 12;
draw_input_field(c, pad, y, fw, USER_FIELD_H, "New Password", st->new_pwd,
st->field == 0, true, accent);
y += sfh + 2 + USER_FIELD_H + 10;
draw_input_field(c, pad, y, fw, USER_FIELD_H, "Confirm Password", st->confirm,
st->field == 1, true, accent);
y += sfh + 2 + USER_FIELD_H + 12;
// Error
if (st->error[0]) {
c.text(pad, y, st->error, Color::from_rgb(0xD0, 0x3E, 0x3E));
}
// Buttons at bottom
int btn_y = c.h - USER_BTN_H - pad;
c.fill_rounded_rect(pad, btn_y, 80, USER_BTN_H, 4, accent);
int tw = text_width("Save");
c.text(pad + (80 - tw) / 2, btn_y + (USER_BTN_H - sfh) / 2, "Save", colors::WHITE);
c.fill_rounded_rect(pad + 88, btn_y, 80, USER_BTN_H, 4, colors::WINDOW_BG);
c.rect(pad + 88, btn_y, 80, USER_BTN_H, colors::BORDER);
tw = text_width("Cancel");
c.text(pad + 88 + (80 - tw) / 2, btn_y + (USER_BTN_H - sfh) / 2, "Cancel", colors::TEXT_COLOR);
}
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);
montauk::strcpy(st->parent->status_msg, "Password changed");
st->parent->status_time = montauk::get_milliseconds();
dialog_close_self(st->ds, st);
}
static void chpwd_on_mouse(Window* win, MouseEvent& ev) {
ChPwdDialogState* st = (ChPwdDialogState*)win->app_data;
if (!st || !ev.left_pressed()) return;
Rect cr = win->content_rect();
int mx = ev.x - cr.x;
int my = ev.y - cr.y;
int pad = 16;
int sfh = system_font_height();
int y = pad + sfh + 12;
// New password field
y += sfh + 2;
if (my >= y && my < y + USER_FIELD_H) { st->field = 0; return; }
y += USER_FIELD_H + 10;
// Confirm field
y += sfh + 2;
if (my >= y && my < y + USER_FIELD_H) { st->field = 1; return; }
// Bottom buttons
int btn_y = win->content_h - USER_BTN_H - pad;
if (my >= btn_y && my < btn_y + USER_BTN_H) {
if (mx >= pad && mx < pad + 80) { chpwd_submit(st); return; }
if (mx >= pad + 88 && mx < pad + 168) { 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; }
}
static void open_change_pwd_dialog(SettingsState* parent) {
if (parent->selected_user < 0) return;
DesktopState* ds = parent->desktop;
int w = 340, 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;
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;
}
// ============================================================================
// Delete User Dialog
// ============================================================================
struct DeleteUserDialogState {
DesktopState* ds;
SettingsState* parent;
char username[32];
bool hover_delete, hover_cancel;
};
static void deluser_on_draw(Window* win, Framebuffer& fb) {
DeleteUserDialogState* st = (DeleteUserDialogState*)win->app_data;
if (!st) return;
Canvas c(win);
c.fill(colors::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, colors::TEXT_COLOR);
const char* warn = "This action cannot be undone.";
tw = text_width(warn);
c.text((c.w - tw) / 2, 24 + sfh + 8, warn, Color::from_rgb(0x88, 0x88, 0x88));
// Buttons
int btn_w = 100, 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;
Color del_bg = st->hover_delete
? Color::from_rgb(0xDD, 0x44, 0x44)
: Color::from_rgb(0xD0, 0x3E, 0x3E);
c.button(bx, btn_y, btn_w, btn_h, "Delete", del_bg, colors::WHITE, 4);
Color cancel_bg = st->hover_cancel
? Color::from_rgb(0x99, 0x99, 0x99)
: Color::from_rgb(0x88, 0x88, 0x88);
c.button(bx + btn_w + gap, btn_y, btn_w, btn_h, "Cancel", cancel_bg, colors::WHITE, 4);
}
static void deluser_on_mouse(Window* win, MouseEvent& ev) {
DeleteUserDialogState* st = (DeleteUserDialogState*)win->app_data;
if (!st) return;
Rect cr = win->content_rect();
int lx = ev.x - cr.x;
int ly = ev.y - cr.y;
int btn_w = 100, 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 db = {bx, btn_y, btn_w, btn_h};
Rect cb = {bx + btn_w + gap, btn_y, btn_w, btn_h};
st->hover_delete = db.contains(lx, ly);
st->hover_cancel = cb.contains(lx, ly);
if (ev.left_pressed()) {
if (st->hover_delete) {
montauk::user::delete_user(st->username);
st->parent->users_loaded = false;
st->parent->selected_user = -1;
montauk::strcpy(st->parent->status_msg, "User deleted");
st->parent->status_time = montauk::get_milliseconds();
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;
montauk::strcpy(st->parent->status_msg, "User deleted");
st->parent->status_time = montauk::get_milliseconds();
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; }
}
static void open_delete_user_dialog(SettingsState* parent) {
if (parent->selected_user < 0) return;
DesktopState* ds = parent->desktop;
// Don't allow deleting yourself
if (montauk::streq(parent->users[parent->selected_user].username, ds->current_user)) {
montauk::strcpy(parent->status_msg, "Cannot delete current user");
parent->status_time = montauk::get_milliseconds();
return;
}
int w = 300, 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;
}
static void settings_on_draw(Window* win, Framebuffer& fb) {
SettingsState* st = (SettingsState*)win->app_data;
if (!st) return;
@@ -372,7 +1080,8 @@ static void settings_on_draw(Window* win, Framebuffer& fb) {
switch (st->active_tab) {
case 0: settings_draw_appearance(content, st); break;
case 1: settings_draw_display(content, st); break;
case 2: settings_draw_about(content, st); break;
case 2: settings_draw_users(content, st); break;
case 3: settings_draw_about(content, st); break;
}
}
@@ -432,12 +1141,14 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
if (mx >= x && mx < x + 100 && cy >= y && cy < y + 16) {
s.bg_gradient = true;
s.bg_image = false;
settings_persist(st);
return;
}
// Radio: Solid
if (mx >= x + 120 && mx < x + 210 && cy >= y && cy < y + 16) {
s.bg_gradient = false;
s.bg_image = false;
settings_persist(st);
return;
}
// Radio: Image
@@ -445,16 +1156,17 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
s.bg_image = true;
s.bg_gradient = false;
if (!st->wp_scanned) {
wallpaper_scan_dir("0:/home", &st->wp_files);
wallpaper_scan_dir(st->desktop->home_dir, &st->wp_files);
st->wp_scanned = true;
}
settings_persist(st);
return;
}
y += line_h + 4;
int idx;
if (mode_image) {
// "Images in 0:/home/" label
// Wallpaper file list
y += sfh + 6;
// File list clicks
@@ -463,10 +1175,12 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
mx >= x && mx < win->content_w - x) {
// Build full path and load wallpaper
char fullpath[256];
montauk::strcpy(fullpath, "0:/home/");
montauk::strcpy(fullpath, st->desktop->home_dir);
str_append(fullpath, "/", 256);
str_append(fullpath, st->wp_files.names[i], 256);
wallpaper_load(&s, fullpath,
st->desktop->screen_w, st->desktop->screen_h);
settings_persist(st);
return;
}
y += WP_ITEM_H;
@@ -477,6 +1191,7 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
// Top swatches
if (swatch_hit(mx, cy, x + 70, y, &idx)) {
s.bg_grad_top = bg_palette[idx];
settings_persist(st);
return;
}
y += SWATCH_SIZE + 14;
@@ -484,6 +1199,7 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
// Bottom swatches
if (swatch_hit(mx, cy, x + 70, y, &idx)) {
s.bg_grad_bottom = bg_palette[idx];
settings_persist(st);
return;
}
y += SWATCH_SIZE + 14;
@@ -491,6 +1207,7 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
// Solid color swatches
if (swatch_hit(mx, cy, x + 70, y, &idx)) {
s.bg_solid = bg_palette[idx];
settings_persist(st);
return;
}
y += SWATCH_SIZE + 14;
@@ -502,6 +1219,7 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
// Panel color swatches
if (swatch_hit(mx, cy, x + 110, y, &idx)) {
s.panel_color = panel_palette[idx];
settings_persist(st);
return;
}
y += SWATCH_SIZE + 14;
@@ -512,6 +1230,7 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
// Accent color swatches
if (swatch_hit(mx, cy, x + 110, y, &idx)) {
s.accent_color = accent_palette[idx];
settings_persist(st);
return;
}
} else if (st->active_tab == 1) {
@@ -525,11 +1244,13 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
// Window Shadows: On
if (mx >= bx && mx < bx + btn_w && cy >= y && cy < y + btn_h) {
s.show_shadows = true;
settings_persist(st);
return;
}
// Window Shadows: Off
if (mx >= bx + btn_w + 8 && mx < bx + btn_w * 2 + 8 && cy >= y && cy < y + btn_h) {
s.show_shadows = false;
settings_persist(st);
return;
}
y += btn_h + 20 + 16;
@@ -537,11 +1258,13 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
// Clock: 24h
if (mx >= bx && mx < bx + btn_w && cy >= y && cy < y + btn_h) {
s.clock_24h = true;
settings_persist(st);
return;
}
// Clock: 12h
if (mx >= bx + btn_w + 8 && mx < bx + btn_w * 2 + 8 && cy >= y && cy < y + btn_h) {
s.clock_24h = false;
settings_persist(st);
return;
}
y += btn_h + 20 + 16;
@@ -553,6 +1276,7 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
s.ui_scale = 0;
apply_ui_scale(0);
montauk::win_setscale(0);
settings_persist(st);
return;
}
// Default
@@ -560,6 +1284,7 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
s.ui_scale = 1;
apply_ui_scale(1);
montauk::win_setscale(1);
settings_persist(st);
return;
}
// Large
@@ -567,11 +1292,71 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
s.ui_scale = 2;
apply_ui_scale(2);
montauk::win_setscale(2);
settings_persist(st);
return;
}
} else if (st->active_tab == 2) {
// Users tab
if (!st->desktop->is_admin) return;
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();
// User list rows
for (int i = 0; i < st->user_count; i++) {
if (cy >= y && cy < y + row_h && mx >= x && mx < x + content_w) {
st->selected_user = (st->selected_user == i) ? -1 : i;
return;
}
y += row_h + 4;
}
// Separator + gap
y += 8 + 1 + 16;
// Status message (if shown, takes space)
if (st->status_msg[0] && (montauk::get_milliseconds() - st->status_time < 3000)) {
y += sfh + 8;
}
// Action buttons
int btn_x = x;
// Add User
if (mx >= btn_x && mx < btn_x + 90 && cy >= y && cy < y + USER_BTN_H) {
open_add_user_dialog(st);
return;
}
btn_x += 98;
// Change Password
if (mx >= btn_x && mx < btn_x + USER_BTN_W && cy >= y && cy < y + USER_BTN_H) {
if (st->selected_user >= 0) open_change_pwd_dialog(st);
return;
}
btn_x += USER_BTN_W + 8;
// Delete
if (mx >= btn_x && mx < btn_x + 70 && cy >= y && cy < y + USER_BTN_H) {
if (st->selected_user >= 0) open_delete_user_dialog(st);
return;
}
}
}
// ============================================================================
// Keyboard
// ============================================================================
static void settings_on_key(Window* win, const Montauk::KeyEvent& key) {
// Settings main window no longer needs keyboard handling —
// text input is handled by dialog windows.
(void)win; (void)key;
}
// ============================================================================
// Cleanup
// ============================================================================
@@ -600,9 +1385,13 @@ void open_settings(DesktopState* ds) {
st->uptime_ms = montauk::get_milliseconds();
st->wp_scanned = false;
st->wp_files.count = 0;
st->selected_user = -1;
st->users_loaded = false;
st->status_msg[0] = '\0';
win->app_data = st;
win->on_draw = settings_on_draw;
win->on_mouse = settings_on_mouse;
win->on_key = settings_on_key;
win->on_close = settings_on_close;
}
+155
View File
@@ -6,6 +6,149 @@
#include "desktop_internal.hpp"
// ============================================================================
// Lock Screen Drawing
// ============================================================================
static constexpr Color LOCK_CARD_BG = Color::from_rgb(0xFF, 0xFF, 0xFF);
static constexpr Color LOCK_FIELD_BG = Color::from_rgb(0xF5, 0xF5, 0xF5);
static constexpr Color LOCK_BORDER = Color::from_rgb(0xCC, 0xCC, 0xCC);
static constexpr Color LOCK_ACCENT = Color::from_rgb(0x36, 0x7B, 0xF0);
static constexpr Color LOCK_TEXT = Color::from_rgb(0x33, 0x33, 0x33);
static constexpr Color LOCK_DIM = Color::from_rgb(0x66, 0x66, 0x66);
static constexpr Color LOCK_ERROR = Color::from_rgb(0xE0, 0x40, 0x40);
static constexpr Color LOCK_BTN_TEXT = Color::from_rgb(0xFF, 0xFF, 0xFF);
static constexpr int LOCK_CARD_W = 360;
static constexpr int LOCK_FIELD_H = 36;
static constexpr int LOCK_BTN_H = 40;
void desktop_draw_lock_screen(DesktopState* ds) {
Framebuffer& fb = ds->fb;
int sw = ds->screen_w;
int sh = ds->screen_h;
int sfh = system_font_height();
// Dark overlay on top of background
fb.fill_rect_alpha(0, 0, sw, sh, Color::from_rgba(0, 0, 0, 0x80));
// Clock display above card
Montauk::DateTime dt;
montauk::gettime(&dt);
char time_str[12];
snprintf(time_str, sizeof(time_str), "%02d:%02d", (int)dt.Hour, (int)dt.Minute);
// Calculate card dimensions
int error_h = ds->lock_show_error ? sfh + 8 : 0;
int card_h = 20 + sfh + 16 + sfh + 12 + sfh + 4 + LOCK_FIELD_H + 16 + LOCK_BTN_H + error_h + 20;
int card_x = (sw - LOCK_CARD_W) / 2;
int card_y = (sh - card_h) / 2;
// Draw time above card
int time_w = text_width(time_str);
draw_text(fb, (sw - time_w) / 2, card_y - sfh * 3, time_str, Color::from_rgb(0xFF, 0xFF, 0xFF));
// Date below time
{
int month_idx = dt.Month > 0 && dt.Month <= 12 ? dt.Month - 1 : 0;
char date_str[32];
snprintf(date_str, sizeof(date_str), "%s %d", month_names[month_idx], (int)dt.Day);
int dw = text_width(date_str);
draw_text(fb, (sw - dw) / 2, card_y - sfh * 2 + 4, date_str, Color::from_rgb(0xCC, 0xCC, 0xCC));
}
// Card background with rounded corners
fill_rounded_rect(fb, card_x, card_y, LOCK_CARD_W, card_h, 12, LOCK_CARD_BG);
int x = card_x + 24;
int content_w = LOCK_CARD_W - 48;
int y = card_y + 20;
// Title (with optional lock icon beside it)
{
const char* title = "Locked";
int tw = text_width(title);
int icon_w = ds->icon_lock.pixels ? 20 : 0;
int gap = icon_w ? 8 : 0;
int total = icon_w + gap + tw;
int tx = card_x + (LOCK_CARD_W - total) / 2;
if (ds->icon_lock.pixels) {
fb.blit_alpha(tx, y + (sfh - 20) / 2, ds->icon_lock.width, ds->icon_lock.height, ds->icon_lock.pixels);
}
draw_text(fb, tx + icon_w + gap, y, title, LOCK_TEXT);
y += sfh + 16;
}
// Username display (cached at lock time)
{
int nw = text_width(ds->lock_display_name);
draw_text(fb, card_x + (LOCK_CARD_W - nw) / 2, y, ds->lock_display_name, LOCK_DIM);
y += sfh + 12;
}
// Password label
draw_text(fb, x, y, "Password", LOCK_DIM);
y += sfh + 4;
// Password field
{
fb.fill_rect(x, y, content_w, LOCK_FIELD_H, LOCK_FIELD_BG);
// 2px accent border (always active)
for (int i = 0; i < content_w; i++) { fb.put_pixel(x + i, y, LOCK_ACCENT); fb.put_pixel(x + i, y + 1, LOCK_ACCENT); }
for (int i = 0; i < content_w; i++) { fb.put_pixel(x + i, y + LOCK_FIELD_H - 1, LOCK_ACCENT); fb.put_pixel(x + i, y + LOCK_FIELD_H - 2, LOCK_ACCENT); }
for (int i = 0; i < LOCK_FIELD_H; i++) { fb.put_pixel(x, y + i, LOCK_ACCENT); fb.put_pixel(x + 1, y + i, LOCK_ACCENT); }
for (int i = 0; i < LOCK_FIELD_H; i++) { fb.put_pixel(x + content_w - 1, y + i, LOCK_ACCENT); fb.put_pixel(x + content_w - 2, y + i, LOCK_ACCENT); }
// Masked password text (clipped to field width)
char masked[65];
int len = ds->lock_password_len;
if (len > 64) len = 64;
int max_text_w = content_w - 10 - 10 - 2; // left pad, right pad, cursor
// Show only the trailing portion that fits
int vis = len;
{
char tmp[65];
for (int i = 0; i < len; i++) tmp[i] = '*';
tmp[len] = '\0';
while (vis > 0 && text_width(tmp + (len - vis)) > max_text_w)
vis--;
}
for (int i = 0; i < vis; i++) masked[i] = '*';
masked[vis] = '\0';
int ty = y + (LOCK_FIELD_H - sfh) / 2;
draw_text(fb, x + 10, ty, masked, LOCK_TEXT);
// Cursor
int cx = x + 10 + text_width(masked);
fb.fill_rect(cx, ty, 2, sfh, LOCK_ACCENT);
y += LOCK_FIELD_H + 16;
}
// Unlock button
{
fill_rounded_rect(fb, x, y, content_w, LOCK_BTN_H, 6, LOCK_ACCENT);
const char* label = "Unlock";
int tw = text_width(label);
int ty = y + (LOCK_BTN_H - sfh) / 2;
draw_text(fb, x + (content_w - tw) / 2, ty, label, LOCK_BTN_TEXT);
y += LOCK_BTN_H;
}
// Error message
if (ds->lock_show_error) {
y += 8;
int ew = text_width(ds->lock_error);
draw_text(fb, card_x + (LOCK_CARD_W - ew) / 2, y, ds->lock_error, LOCK_ERROR);
}
}
// ============================================================================
// Desktop Composition
// ============================================================================
void gui::desktop_compose(DesktopState* ds) {
Framebuffer& fb = ds->fb;
@@ -45,6 +188,13 @@ void gui::desktop_compose(DesktopState* ds) {
}
}
// Lock screen: draw overlay and card, then cursor, and return early
if (ds->screen_locked) {
desktop_draw_lock_screen(ds);
draw_cursor(fb, ds->mouse.x, ds->mouse.y);
return;
}
// Draw windows from bottom to top
for (int i = 0; i < ds->window_count; i++) {
if (ds->windows[i].state != WIN_MINIMIZED && ds->windows[i].state != WIN_CLOSED) {
@@ -65,6 +215,11 @@ void gui::desktop_compose(DesktopState* ds) {
desktop_draw_net_popup(ds);
}
// Draw volume popup if open
if (ds->vol_popup_open) {
desktop_draw_vol_popup(ds);
}
// Draw right-click context menu if open
if (ds->ctx_menu_open) {
static constexpr int CTX_MENU_W = 180;
@@ -115,6 +115,10 @@ gui::CursorStyle cursor_for_edge(gui::ResizeEdge edge);
// panel.cpp
void desktop_draw_app_menu(gui::DesktopState* ds);
void desktop_draw_net_popup(gui::DesktopState* ds);
void desktop_draw_vol_popup(gui::DesktopState* ds);
// compose.cpp
void desktop_draw_lock_screen(gui::DesktopState* ds);
// main.cpp
void desktop_scan_apps(gui::DesktopState* ds);
+247 -11
View File
@@ -5,8 +5,133 @@
*/
#include "desktop_internal.hpp"
#include <montauk/user.h>
// ============================================================================
// Lock Screen Input
// ============================================================================
static void lock_screen(DesktopState* ds) {
ds->screen_locked = true;
ds->lock_password[0] = '\0';
ds->lock_password_len = 0;
ds->lock_error[0] = '\0';
ds->lock_show_error = false;
ds->app_menu_open = false;
ds->ctx_menu_open = false;
ds->net_popup_open = false;
ds->vol_popup_open = false;
// Cache display name for lock screen rendering
montauk::strncpy(ds->lock_display_name, ds->current_user, sizeof(ds->lock_display_name));
montauk::user::UserInfo users[16];
int count = montauk::user::load_users(users, 16);
for (int i = 0; i < count; i++) {
if (montauk::streq(users[i].username, ds->current_user)) {
if (users[i].display_name[0])
montauk::strncpy(ds->lock_display_name, users[i].display_name, sizeof(ds->lock_display_name));
break;
}
}
}
static bool try_unlock(DesktopState* ds) {
ds->lock_show_error = false;
if (montauk::user::authenticate(ds->current_user, ds->lock_password)) {
ds->screen_locked = false;
ds->lock_password[0] = '\0';
ds->lock_password_len = 0;
return true;
}
montauk::strcpy(ds->lock_error, "Incorrect password");
ds->lock_show_error = true;
ds->lock_password[0] = '\0';
ds->lock_password_len = 0;
return false;
}
static void handle_lock_mouse(DesktopState* ds) {
int mx = ds->mouse.x;
int my = ds->mouse.y;
uint8_t buttons = ds->mouse.buttons;
uint8_t prev = ds->prev_buttons;
bool left_pressed = (buttons & 0x01) && !(prev & 0x01);
if (!left_pressed) return;
int sfh = system_font_height();
int card_w = 360;
int content_w = card_w - 48;
int field_h = 36;
int btn_h = 40;
// Calculate card layout to find button position
int error_h = ds->lock_show_error ? sfh + 8 : 0;
int card_h = 20 + sfh + 16 + sfh + 12 + sfh + 4 + field_h + 16 + btn_h + error_h + 20;
int card_x = (ds->screen_w - card_w) / 2;
int card_y = (ds->screen_h - card_h) / 2;
int x = card_x + 24;
// Walk through the layout to find the Unlock button y position
int y = card_y + 20;
y += sfh + 16; // title
y += sfh + 12; // username
y += sfh + 4; // "Password" label
y += field_h + 16; // field + gap
// Check Unlock button
if (mx >= x && mx < x + content_w && my >= y && my < y + btn_h) {
try_unlock(ds);
return;
}
// Check password field click (just keep focus, nothing else to switch to)
int field_y = y - field_h - 16;
if (mx >= x && mx < x + content_w && my >= field_y && my < field_y + field_h) {
ds->lock_show_error = false;
}
}
static void handle_lock_keyboard(DesktopState* ds, const Montauk::KeyEvent& key) {
if (!key.pressed) return;
// Enter submits
if (key.ascii == '\n' || key.ascii == '\r') {
try_unlock(ds);
return;
}
// Backspace
if (key.ascii == '\b' || key.scancode == 0x0E) {
if (ds->lock_password_len > 0) {
ds->lock_password_len--;
ds->lock_password[ds->lock_password_len] = '\0';
}
return;
}
// Printable characters
if (key.ascii >= 0x20 && key.ascii < 0x7F) {
if (ds->lock_password_len < 63) {
ds->lock_password[ds->lock_password_len] = key.ascii;
ds->lock_password_len++;
ds->lock_password[ds->lock_password_len] = '\0';
}
}
}
// ============================================================================
// Desktop Mouse Handling
// ============================================================================
void gui::desktop_handle_mouse(DesktopState* ds) {
if (ds->screen_locked) {
handle_lock_mouse(ds);
return;
}
int mx = ds->mouse.x;
int my = ds->mouse.y;
uint8_t buttons = ds->mouse.buttons;
@@ -213,8 +338,8 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
menu_cat_expanded[cur_cat] = !menu_cat_expanded[cur_cat];
} else if (!row.is_category) {
if (row.external) {
// Launch external app from manifest
montauk::spawn(row.binary_path);
// Launch external app with user's home dir
montauk::spawn(row.binary_path, ds->home_dir);
} else {
// Dispatch embedded app
switch (row.app_id) {
@@ -230,6 +355,8 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
case 12: open_reboot_dialog(ds); break;
case 14: open_shutdown_dialog(ds); break;
case 15: open_wordprocessor(ds); break;
case 16: montauk::exit(0); break; // Log Out
case 17: lock_screen(ds); break; // Lock Screen
}
}
ds->app_menu_open = false;
@@ -244,10 +371,104 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
}
}
// Handle volume popup interaction
if (ds->vol_popup_open) {
int popup_x = ds->vol_icon_rect.x + ds->vol_icon_rect.w - 200;
int popup_y = PANEL_HEIGHT + 2;
if (popup_x < 4) popup_x = 4;
Rect vol_rect = {popup_x, popup_y, 200, 120};
// Handle drag continuity
if (ds->vol_dragging) {
if (left_held) {
int slider_abs_x = popup_x + 16;
int v = ((mx - slider_abs_x) * 100) / (200 - 32);
if (v < 0) v = 0;
if (v > 100) v = 100;
ds->vol_muted = false;
ds->vol_level = v;
montauk::audio_set_volume(0, v);
return;
}
if (left_released) {
ds->vol_dragging = false;
}
}
if (left_pressed) {
if (vol_rect.contains(mx, my)) {
// Slider area (y = popup_y + 56, height = 8, with generous hit zone)
int slider_abs_x = popup_x + 16;
int slider_abs_y = popup_y + 56;
int slider_w = 200 - 32;
if (my >= slider_abs_y - 10 && my <= slider_abs_y + 8 + 10 &&
mx >= slider_abs_x - 8 && mx <= slider_abs_x + slider_w + 8) {
int v = ((mx - slider_abs_x) * 100) / slider_w;
if (v < 0) v = 0;
if (v > 100) v = 100;
ds->vol_muted = false;
ds->vol_level = v;
montauk::audio_set_volume(0, v);
ds->vol_dragging = true;
return;
}
// Buttons (y = popup_y + 78, h = 24)
int btn_h = 24;
int btn_y = popup_y + 78;
int minus_w = 36, plus_w = 36, mute_w = 50, gap = 8;
int total_w = minus_w + plus_w + mute_w + gap * 2;
int bx = popup_x + (200 - total_w) / 2;
if (my >= btn_y && my < btn_y + btn_h) {
// [-]
if (mx >= bx && mx < bx + minus_w) {
ds->vol_muted = false;
int v = ds->vol_level - 5;
if (v < 0) v = 0;
ds->vol_level = v;
montauk::audio_set_volume(0, v);
return;
}
bx += minus_w + gap;
// [+]
if (mx >= bx && mx < bx + plus_w) {
ds->vol_muted = false;
int v = ds->vol_level + 5;
if (v > 100) v = 100;
ds->vol_level = v;
montauk::audio_set_volume(0, v);
return;
}
bx += plus_w + gap;
// [Mute]
if (mx >= bx && mx < bx + mute_w) {
if (ds->vol_muted) {
ds->vol_muted = false;
montauk::audio_set_volume(0, ds->vol_pre_mute);
ds->vol_level = ds->vol_pre_mute;
} else {
ds->vol_pre_mute = ds->vol_level;
ds->vol_muted = true;
montauk::audio_set_volume(0, 0);
}
return;
}
}
return; // click inside popup but not on any control
} else if (!ds->vol_icon_rect.contains(mx, my)) {
ds->vol_popup_open = false;
ds->vol_dragging = false;
}
}
}
// Handle net popup clicks
if (ds->net_popup_open && left_pressed) {
int popup_w = 220;
int popup_h = 130;
int fh_net = system_font_height();
int row_h_net = fh_net + 8;
int popup_h = (row_h_net + 8) + row_h_net * 5 + 12;
int popup_x = ds->net_icon_rect.x + ds->net_icon_rect.w - popup_w;
int popup_y = PANEL_HEIGHT + 2;
if (popup_x < 4) popup_x = 4;
@@ -266,6 +487,17 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
if (mx < 36) {
ds->app_menu_open = !ds->app_menu_open;
ds->net_popup_open = false;
ds->vol_popup_open = false;
ds->ctx_menu_open = false;
return;
}
// Volume icon
if (ds->vol_icon_rect.w > 0 && ds->vol_icon_rect.contains(mx, my)) {
ds->vol_popup_open = !ds->vol_popup_open;
ds->vol_dragging = false;
ds->app_menu_open = false;
ds->net_popup_open = false;
ds->ctx_menu_open = false;
return;
}
@@ -274,6 +506,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
if (ds->net_icon_rect.w > 0 && ds->net_icon_rect.contains(mx, my)) {
ds->net_popup_open = !ds->net_popup_open;
ds->app_menu_open = false;
ds->vol_popup_open = false;
ds->ctx_menu_open = false;
return;
}
@@ -369,11 +602,6 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
if (win->state != WIN_MAXIMIZED) {
ResizeEdge edge = hit_test_resize_edge(win->frame, mx, my);
if (edge != RESIZE_NONE) {
win->resizing = true;
win->resize_edge = edge;
win->resize_start_frame = win->frame;
win->resize_start_mx = mx;
win->resize_start_my = my;
desktop_raise_window(ds, i);
int new_idx = ds->window_count - 1;
ds->windows[new_idx].resizing = true;
@@ -388,9 +616,6 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
// Check titlebar (start drag)
Rect tb = win->titlebar_rect();
if (tb.contains(mx, my)) {
win->dragging = true;
win->drag_offset_x = mx - win->frame.x;
win->drag_offset_y = my - win->frame.y;
desktop_raise_window(ds, i);
int new_idx = ds->window_count - 1;
ds->windows[new_idx].dragging = true;
@@ -433,6 +658,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
ds->app_menu_open = false;
ds->ctx_menu_open = false;
ds->vol_popup_open = false;
}
// Forward continuous mouse events to focused window (hover, drag, release)
@@ -499,13 +725,23 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
ds->ctx_menu_y = my;
ds->app_menu_open = false;
ds->net_popup_open = false;
ds->vol_popup_open = false;
}
}
}
void gui::desktop_handle_keyboard(DesktopState* ds, const Montauk::KeyEvent& key) {
if (ds->screen_locked) {
handle_lock_keyboard(ds, key);
return;
}
// Global shortcuts (only on key press)
if (key.pressed && key.ctrl && key.alt) {
if (key.ascii == 'l' || key.ascii == 'L') {
lock_screen(ds);
return;
}
if (key.ascii == 't' || key.ascii == 'T') {
open_terminal(ds);
return;
+117 -16
View File
@@ -5,6 +5,8 @@
*/
#include "desktop_internal.hpp"
#include <montauk/config.h>
#include <montauk/user.h>
// ============================================================================
// App Manifest Scanning
@@ -168,9 +170,11 @@ void desktop_build_menu(DesktopState* ds) {
// Divider + always-visible entries
menu_add_category(""); // divider (cat 4, always expanded)
menu_add_embedded("Settings", 11, &ds->icon_settings);
menu_add_embedded("Reboot", 12, &ds->icon_reboot);
menu_add_embedded("Shutdown", 14, &ds->icon_shutdown);
menu_add_embedded("Settings", 11, &ds->icon_settings);
menu_add_embedded("Lock Screen", 17, &ds->icon_lock);
menu_add_embedded("Log Out", 16, &ds->icon_logout);
menu_add_embedded("Reboot", 12, &ds->icon_reboot);
menu_add_embedded("Shutdown", 14, &ds->icon_shutdown);
}
// ============================================================================
@@ -224,9 +228,12 @@ void gui::desktop_init(DesktopState* ds) {
ds->icon_settings = svg_load("0:/icons/help-about.svg", 20, 20, defColor);
ds->icon_reboot = svg_load("0:/icons/system-reboot.svg", 20, 20, defColor);
ds->icon_shutdown = svg_load("0:/icons/system-shutdown.svg", 20, 20, defColor);
ds->icon_logout = svg_load("0:/icons/gnome-logout.svg", 20, 20, defColor);
ds->icon_procmgr = svg_load("0:/icons/system-monitor.svg", 20, 20, defColor);
ds->icon_mandelbrot = svg_load("0:/icons/applications-science.svg", 20, 20, defColor);
ds->icon_volume = svg_load("0:/icons/audio-volume-high-symbolic.svg", 16, 16, colors::PANEL_TEXT);
ds->icon_lock = svg_load("0:/icons/lock.svg", 20, 20, defColor);
// Scan 0:/apps/ for external app manifests and build the menu
desktop_scan_apps(ds);
@@ -248,10 +255,61 @@ void gui::desktop_init(DesktopState* ds) {
ds->settings.clock_24h = true;
ds->settings.ui_scale = 1;
// Try to load default wallpaper
wallpaper_load(&ds->settings, "0:/home/lucas-alexander-2dJn8XoIKCg-unsplash.jpg",
ds->screen_w, ds->screen_h);
montauk::win_setscale(1);
// Load per-user desktop settings
{
auto doc = montauk::config::load_user(ds->current_user, "desktop");
const char* wp = doc.get_string("wallpaper.path", "");
if (wp[0] != '\0') {
wallpaper_load(&ds->settings, wp, ds->screen_w, ds->screen_h);
} else {
// Fall back to system-wide default wallpaper from 0:/config/desktop.toml
auto sys = montauk::config::load("desktop");
const char* def_wp = sys.get_string("wallpaper.path", "");
if (def_wp[0] != '\0') {
wallpaper_load(&ds->settings, def_wp, ds->screen_w, ds->screen_h);
}
sys.destroy();
}
// Restore other settings from user config
const char* bg_mode = doc.get_string("background.mode", "");
if (montauk::streq(bg_mode, "solid")) {
ds->settings.bg_gradient = false;
ds->settings.bg_image = false;
} else if (montauk::streq(bg_mode, "gradient")) {
ds->settings.bg_gradient = true;
ds->settings.bg_image = false;
}
// (image mode is set by wallpaper_load above)
// Background colors
int64_t solid = doc.get_int("background.solid_color", -1);
if (solid >= 0) ds->settings.bg_solid = Color::from_rgb(
(uint8_t)((solid >> 16) & 0xFF), (uint8_t)((solid >> 8) & 0xFF), (uint8_t)(solid & 0xFF));
int64_t gtop = doc.get_int("background.grad_top", -1);
if (gtop >= 0) ds->settings.bg_grad_top = Color::from_rgb(
(uint8_t)((gtop >> 16) & 0xFF), (uint8_t)((gtop >> 8) & 0xFF), (uint8_t)(gtop & 0xFF));
int64_t gbot = doc.get_int("background.grad_bottom", -1);
if (gbot >= 0) ds->settings.bg_grad_bottom = Color::from_rgb(
(uint8_t)((gbot >> 16) & 0xFF), (uint8_t)((gbot >> 8) & 0xFF), (uint8_t)(gbot & 0xFF));
// Appearance colors
int64_t panel = doc.get_int("appearance.panel_color", -1);
if (panel >= 0) ds->settings.panel_color = Color::from_rgb(
(uint8_t)((panel >> 16) & 0xFF), (uint8_t)((panel >> 8) & 0xFF), (uint8_t)(panel & 0xFF));
int64_t accent = doc.get_int("appearance.accent_color", -1);
if (accent >= 0) ds->settings.accent_color = Color::from_rgb(
(uint8_t)((accent >> 16) & 0xFF), (uint8_t)((accent >> 8) & 0xFF), (uint8_t)(accent & 0xFF));
int64_t scale = doc.get_int("display.ui_scale", 1);
ds->settings.ui_scale = (int)scale;
ds->settings.clock_24h = doc.get_bool("display.clock_24h", true);
ds->settings.show_shadows = doc.get_bool("display.show_shadows", true);
doc.destroy();
}
montauk::win_setscale(ds->settings.ui_scale);
ds->ctx_menu_open = false;
ds->ctx_menu_x = 0;
@@ -262,8 +320,22 @@ void gui::desktop_init(DesktopState* ds) {
ds->net_cfg_last_poll = montauk::get_milliseconds();
ds->net_icon_rect = {0, 0, 0, 0};
ds->vol_popup_open = false;
ds->vol_icon_rect = {0, 0, 0, 0};
int vol = montauk::audio_get_volume(0);
ds->vol_level = vol >= 0 ? vol : 80;
ds->vol_muted = false;
ds->vol_pre_mute = ds->vol_level;
ds->vol_dragging = false;
ds->vol_last_poll = montauk::get_milliseconds();
ds->closing_ext_count = 0;
ds->screen_locked = false;
ds->lock_password[0] = '\0';
ds->lock_password_len = 0;
ds->lock_error[0] = '\0';
ds->lock_show_error = false;
}
// ============================================================================
@@ -412,21 +484,25 @@ void gui::desktop_run(DesktopState* ds) {
// Poll external windows (discover new, remove dead, update dirty)
desktop_poll_external_windows(ds);
// Poll windows that have a poll callback
for (int i = 0; i < ds->window_count; i++) {
Window* win = &ds->windows[i];
if (win->state == WIN_CLOSED) continue;
if (win->on_poll) {
win->on_poll(win);
if (!ds->screen_locked) {
// Poll windows that have a poll callback
for (int i = 0; i < ds->window_count; i++) {
Window* win = &ds->windows[i];
if (win->state == WIN_CLOSED) continue;
if (win->on_poll) {
win->on_poll(win);
}
}
}
// Handle mouse events
desktop_handle_mouse(ds);
// Re-poll external windows so that any killed during mouse/key
// handling are removed before we touch their pixel buffers.
desktop_poll_external_windows(ds);
if (!ds->screen_locked) {
// Re-poll external windows so that any killed during mouse/key
// handling are removed before we touch their pixel buffers.
desktop_poll_external_windows(ds);
}
// Compose and present
desktop_compose(ds);
@@ -450,6 +526,31 @@ extern "C" void _start() {
// Placement-new the Framebuffer since it has a constructor
new (&ds->fb) Framebuffer();
// Read username from spawn args
char username[32] = {};
montauk::getargs(username, 32);
if (username[0] == '\0') {
montauk::strcpy(username, "default");
}
montauk::strncpy(ds->current_user, username, 31);
// Build user paths
montauk::user::home_dir(username, ds->home_dir, sizeof(ds->home_dir));
montauk::user::config_dir(username, ds->user_config_dir, sizeof(ds->user_config_dir));
// Check if user is admin
ds->is_admin = false;
{
montauk::user::UserInfo users[16];
int count = montauk::user::load_users(users, 16);
for (int i = 0; i < count; i++) {
if (montauk::streq(users[i].username, username)) {
ds->is_admin = montauk::streq(users[i].role, "admin");
break;
}
}
}
g_desktop = ds;
desktop_init(ds);
+216 -36
View File
@@ -105,14 +105,42 @@ void gui::desktop_draw_panel(DesktopState* ds) {
int date_x = clock_x - date_w - 10;
draw_text(fb, date_x, clock_y, date_str, colors::PANEL_TEXT);
// Network icon (to the left of the date)
// Volume icon (to the left of the date)
uint64_t now = montauk::get_milliseconds();
if (now - ds->vol_last_poll > 5000) {
int v = montauk::audio_get_volume(0);
if (v >= 0 && !ds->vol_muted) ds->vol_level = v;
ds->vol_last_poll = now;
}
int vol_icon_x = date_x - 16 - 12;
int vol_icon_y = (PANEL_HEIGHT - 16) / 2;
ds->vol_icon_rect = {vol_icon_x, vol_icon_y, 16, 16};
if (ds->icon_volume.pixels) {
if (ds->vol_muted) {
// Tint red-ish when muted
uint32_t* src = ds->icon_volume.pixels;
int npx = 16 * 16;
uint32_t tinted[256];
for (int p = 0; p < npx; p++) {
uint32_t px = src[p];
uint8_t a = (px >> 24) & 0xFF;
tinted[p] = ((uint32_t)a << 24) | 0x00CC3333;
}
fb.blit_alpha(vol_icon_x, vol_icon_y, 16, 16, tinted);
} else {
fb.blit_alpha(vol_icon_x, vol_icon_y, ds->icon_volume.width, ds->icon_volume.height, ds->icon_volume.pixels);
}
}
// Network icon (to the left of the volume icon)
if (now - ds->net_cfg_last_poll > 5000) {
montauk::get_netcfg(&ds->cached_net_cfg);
ds->net_cfg_last_poll = now;
}
int net_icon_x = date_x - 16 - 12;
int net_icon_x = vol_icon_x - 16 - 10;
int net_icon_y = (PANEL_HEIGHT - 16) / 2;
ds->net_icon_rect = {net_icon_x, net_icon_y, 16, 16};
@@ -237,51 +265,203 @@ void desktop_draw_app_menu(DesktopState* ds) {
void desktop_draw_net_popup(DesktopState* ds) {
Framebuffer& fb = ds->fb;
Montauk::NetCfg& nc = ds->cached_net_cfg;
bool connected = nc.ipAddress != 0;
int popup_w = 220;
int popup_h = 130;
int fh = system_font_height();
int row_h = fh + 8;
int header_h = row_h + 8; // title + status row + padding
int body_rows = 5; // IP, Subnet, Gateway, DNS, MAC
int popup_h = header_h + row_h * body_rows + 12;
int popup_x = ds->net_icon_rect.x + ds->net_icon_rect.w - popup_w;
int popup_y = PANEL_HEIGHT + 2;
if (popup_x < 4) popup_x = 4;
draw_shadow(fb, popup_x, popup_y, popup_w, popup_h, 4, colors::SHADOW);
fb.fill_rect(popup_x, popup_y, popup_w, popup_h, colors::MENU_BG);
fill_rounded_rect(fb, popup_x, popup_y, popup_w, popup_h, 8, colors::MENU_BG);
draw_rect(fb, popup_x, popup_y, popup_w, popup_h, colors::BORDER);
int tx = popup_x + 12;
int lx = popup_x + 14;
int ty = popup_y + 10;
int line_h = system_font_height() + 6;
char line[64];
Montauk::NetCfg& nc = ds->cached_net_cfg;
// Header: "Ethernet" + status dot
draw_text(fb, lx, ty, "Ethernet", colors::TEXT_COLOR);
if (nc.ipAddress != 0) {
char ipbuf[20];
format_ip(ipbuf, nc.ipAddress);
snprintf(line, sizeof(line), "IP: %s", ipbuf);
} else {
snprintf(line, sizeof(line), "IP: Not connected");
// Status dot + label (right-aligned in header)
Color dot_color = connected
? Color::from_rgb(0x4C, 0xAF, 0x50) // green
: Color::from_rgb(0xCC, 0x33, 0x33); // red
const char* status_str = connected ? "Connected" : "Disconnected";
int sw = text_width(status_str);
int dot_r = 4;
int status_x = popup_x + popup_w - 14 - sw;
int dot_x = status_x - dot_r * 2 - 5;
int dot_cy = ty + fh / 2;
for (int dy = -dot_r; dy <= dot_r; dy++)
for (int dx = -dot_r; dx <= dot_r; dx++)
if (dx * dx + dy * dy <= dot_r * dot_r)
fb.put_pixel(dot_x + dot_r + dx, dot_cy + dy, dot_color);
Color dim = Color::from_rgb(0x66, 0x66, 0x66);
draw_text(fb, status_x, ty, status_str, dim);
ty += row_h + 2;
// Separator line
for (int sx = popup_x + 10; sx < popup_x + popup_w - 10; sx++)
fb.put_pixel(sx, ty, colors::BORDER);
ty += 6;
// Body rows: label (dim) + value (dark), two-column
int val_x = popup_x + 76; // fixed column for values
struct NetRow { const char* label; char value[24]; };
NetRow rows[5];
rows[0].label = "IP";
if (connected) format_ip(rows[0].value, nc.ipAddress);
else montauk::strcpy(rows[0].value, "\xE2\x80\x94"); // em dash
rows[1].label = "Subnet";
format_ip(rows[1].value, nc.subnetMask);
rows[2].label = "Gateway";
format_ip(rows[2].value, nc.gateway);
rows[3].label = "DNS";
format_ip(rows[3].value, nc.dnsServer);
rows[4].label = "MAC";
format_mac(rows[4].value, nc.macAddress);
for (int i = 0; i < body_rows; i++) {
draw_text(fb, lx, ty, rows[i].label, dim);
draw_text(fb, val_x, ty, rows[i].value, colors::TEXT_COLOR);
ty += row_h;
}
draw_text(fb, tx, ty, line, colors::TEXT_COLOR);
ty += line_h;
char buf[20];
format_ip(buf, nc.subnetMask);
snprintf(line, sizeof(line), "Subnet: %s", buf);
draw_text(fb, tx, ty, line, colors::TEXT_COLOR);
ty += line_h;
format_ip(buf, nc.gateway);
snprintf(line, sizeof(line), "Gateway: %s", buf);
draw_text(fb, tx, ty, line, colors::TEXT_COLOR);
ty += line_h;
format_ip(buf, nc.dnsServer);
snprintf(line, sizeof(line), "DNS: %s", buf);
draw_text(fb, tx, ty, line, colors::TEXT_COLOR);
ty += line_h;
format_mac(buf, nc.macAddress);
snprintf(line, sizeof(line), "MAC: %s", buf);
draw_text(fb, tx, ty, line, colors::TEXT_COLOR);
}
// ============================================================================
// Volume Popup
// ============================================================================
static constexpr int VOL_POPUP_W = 200;
static constexpr int VOL_POPUP_H = 120;
static constexpr int VOL_SLIDER_X = 16;
static constexpr int VOL_SLIDER_W = VOL_POPUP_W - 32;
static constexpr int VOL_SLIDER_H = 8;
static constexpr int VOL_KNOB_R = 8;
void desktop_draw_vol_popup(DesktopState* ds) {
Framebuffer& fb = ds->fb;
int popup_x = ds->vol_icon_rect.x + ds->vol_icon_rect.w - VOL_POPUP_W;
int popup_y = PANEL_HEIGHT + 2;
if (popup_x < 4) popup_x = 4;
draw_shadow(fb, popup_x, popup_y, VOL_POPUP_W, VOL_POPUP_H, 4, colors::SHADOW);
fill_rounded_rect(fb, popup_x, popup_y, VOL_POPUP_W, VOL_POPUP_H, 8, colors::MENU_BG);
draw_rect(fb, popup_x, popup_y, VOL_POPUP_W, VOL_POPUP_H, colors::BORDER);
int display_vol = ds->vol_muted ? 0 : ds->vol_level;
// Volume percentage label
char vol_str[8];
snprintf(vol_str, sizeof(vol_str), "%d%%", display_vol);
int vw = text_width(vol_str);
Color vol_color = ds->vol_muted ? Color::from_rgb(0xCC, 0x33, 0x33) : ds->settings.accent_color;
draw_text(fb, popup_x + (VOL_POPUP_W - vw) / 2, popup_y + 12, vol_str, vol_color);
// "Muted" sub-label
if (ds->vol_muted) {
const char* ml = "Muted";
int mw = text_width(ml);
draw_text(fb, popup_x + (VOL_POPUP_W - mw) / 2,
popup_y + 12 + system_font_height() + 2, ml,
Color::from_rgb(0xCC, 0x33, 0x33));
}
// Slider track
int slider_abs_x = popup_x + VOL_SLIDER_X;
int slider_abs_y = popup_y + 56;
fill_rounded_rect(fb, slider_abs_x, slider_abs_y, VOL_SLIDER_W, VOL_SLIDER_H, 4,
Color::from_rgb(0xDD, 0xDD, 0xDD));
// Filled portion
int fill_w = (display_vol * VOL_SLIDER_W) / 100;
if (fill_w > 0)
fill_rounded_rect(fb, slider_abs_x, slider_abs_y, fill_w, VOL_SLIDER_H, 4,
ds->settings.accent_color);
// Knob
int knob_cx = slider_abs_x + fill_w;
int knob_cy = slider_abs_y + VOL_SLIDER_H / 2;
// Draw filled circle for knob
for (int dy = -VOL_KNOB_R; dy <= VOL_KNOB_R; dy++) {
for (int dx = -VOL_KNOB_R; dx <= VOL_KNOB_R; dx++) {
if (dx * dx + dy * dy <= VOL_KNOB_R * VOL_KNOB_R) {
int px = knob_cx + dx;
int py = knob_cy + dy;
if (px >= 0 && px < ds->screen_w && py >= 0 && py < ds->screen_h)
fb.put_pixel(px, py, ds->settings.accent_color);
}
}
}
// White center
int inner_r = VOL_KNOB_R - 3;
for (int dy = -inner_r; dy <= inner_r; dy++) {
for (int dx = -inner_r; dx <= inner_r; dx++) {
if (dx * dx + dy * dy <= inner_r * inner_r) {
int px = knob_cx + dx;
int py = knob_cy + dy;
if (px >= 0 && px < ds->screen_w && py >= 0 && py < ds->screen_h)
fb.put_pixel(px, py, Color::from_rgb(0xFF, 0xFF, 0xFF));
}
}
}
// Buttons: [-] [+] [Mute]
int btn_h = 24;
int btn_y = popup_y + 78;
int btn_rad = 6;
int minus_w = 36;
int plus_w = 36;
int mute_w = 50;
int gap = 8;
int total_w = minus_w + plus_w + mute_w + gap * 2;
int bx = popup_x + (VOL_POPUP_W - total_w) / 2;
int mmx = ds->mouse.x;
int mmy = ds->mouse.y;
// [-] button
Color minus_bg = Color::from_rgb(0xE0, 0xE0, 0xE0);
Rect minus_r = {bx, btn_y, minus_w, btn_h};
if (minus_r.contains(mmx, mmy)) minus_bg = Color::from_rgb(0xD0, 0xD0, 0xD0);
fill_rounded_rect(fb, bx, btn_y, minus_w, btn_h, btn_rad, minus_bg);
int tw = text_width("-");
draw_text(fb, bx + (minus_w - tw) / 2, btn_y + (btn_h - system_font_height()) / 2, "-", colors::TEXT_COLOR);
bx += minus_w + gap;
// [+] button
Color plus_bg = Color::from_rgb(0xE0, 0xE0, 0xE0);
Rect plus_r = {bx, btn_y, plus_w, btn_h};
if (plus_r.contains(mmx, mmy)) plus_bg = Color::from_rgb(0xD0, 0xD0, 0xD0);
fill_rounded_rect(fb, bx, btn_y, plus_w, btn_h, btn_rad, plus_bg);
tw = text_width("+");
draw_text(fb, bx + (plus_w - tw) / 2, btn_y + (btn_h - system_font_height()) / 2, "+", colors::TEXT_COLOR);
bx += plus_w + gap;
// [Mute] button
Color mute_bg = ds->vol_muted ? Color::from_rgb(0xCC, 0x33, 0x33) : Color::from_rgb(0xE0, 0xE0, 0xE0);
Color mute_fg = ds->vol_muted ? Color::from_rgb(0xFF, 0xFF, 0xFF) : colors::TEXT_COLOR;
Rect mute_r = {bx, btn_y, mute_w, btn_h};
if (mute_r.contains(mmx, mmy)) {
if (ds->vol_muted) mute_bg = Color::from_rgb(0xAA, 0x22, 0x22);
else mute_bg = Color::from_rgb(0xD0, 0xD0, 0xD0);
}
fill_rounded_rect(fb, bx, btn_y, mute_w, btn_h, btn_rad, mute_bg);
tw = text_width("Mute");
draw_text(fb, bx + (mute_w - tw) / 2, btn_y + (btn_h - system_font_height()) / 2, "Mute", mute_fg);
}