diff --git a/programs/src/desktop/Makefile b/programs/src/desktop/Makefile index 1663a3f..4c261d3 100644 --- a/programs/src/desktop/Makefile +++ b/programs/src/desktop/Makefile @@ -63,8 +63,10 @@ LDFLAGS := \ # ---- C++ source files ---- -SRCS := main.cpp font_data.cpp stb_truetype_impl.cpp app_terminal.cpp app_filemanager.cpp app_sysinfo.cpp app_calculator.cpp app_texteditor.cpp app_wordprocessor.cpp app_spreadsheet.cpp app_klog.cpp app_wiki.cpp app_weather.cpp app_procmgr.cpp app_mandelbrot.cpp app_devexplorer.cpp app_settings.cpp app_doom.cpp -OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) +CORE_SRCS := main.cpp window.cpp panel.cpp compose.cpp input.cpp dialogs.cpp font_data.cpp stb_truetype_impl.cpp +APP_SRCS := $(wildcard apps/*.cpp) +SRCS := $(CORE_SRCS) $(APP_SRCS) +OBJS := $(addprefix $(OBJDIR)/,$(notdir $(SRCS:.cpp=.o))) # ---- Target ---- @@ -82,10 +84,15 @@ $(TARGET): $(OBJS) $(LIBS) $(LINK_LD) Makefile mkdir -p $(BINDIR)/os $(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@ -# C++ sources +# Core sources $(OBJDIR)/%.o: %.cpp Makefile mkdir -p $(OBJDIR) $(CXX) $(CXXFLAGS) -c $< -o $@ +# App sources +$(OBJDIR)/%.o: apps/%.cpp Makefile + mkdir -p $(OBJDIR) + $(CXX) $(CXXFLAGS) -c $< -o $@ + clean: rm -rf $(OBJDIR) $(TARGET) diff --git a/programs/src/desktop/app_calculator.cpp b/programs/src/desktop/apps/app_calculator.cpp similarity index 100% rename from programs/src/desktop/app_calculator.cpp rename to programs/src/desktop/apps/app_calculator.cpp diff --git a/programs/src/desktop/app_devexplorer.cpp b/programs/src/desktop/apps/app_devexplorer.cpp similarity index 100% rename from programs/src/desktop/app_devexplorer.cpp rename to programs/src/desktop/apps/app_devexplorer.cpp diff --git a/programs/src/desktop/app_doom.cpp b/programs/src/desktop/apps/app_doom.cpp similarity index 100% rename from programs/src/desktop/app_doom.cpp rename to programs/src/desktop/apps/app_doom.cpp diff --git a/programs/src/desktop/app_filemanager.cpp b/programs/src/desktop/apps/app_filemanager.cpp similarity index 100% rename from programs/src/desktop/app_filemanager.cpp rename to programs/src/desktop/apps/app_filemanager.cpp diff --git a/programs/src/desktop/app_klog.cpp b/programs/src/desktop/apps/app_klog.cpp similarity index 100% rename from programs/src/desktop/app_klog.cpp rename to programs/src/desktop/apps/app_klog.cpp diff --git a/programs/src/desktop/app_mandelbrot.cpp b/programs/src/desktop/apps/app_mandelbrot.cpp similarity index 100% rename from programs/src/desktop/app_mandelbrot.cpp rename to programs/src/desktop/apps/app_mandelbrot.cpp diff --git a/programs/src/desktop/app_procmgr.cpp b/programs/src/desktop/apps/app_procmgr.cpp similarity index 100% rename from programs/src/desktop/app_procmgr.cpp rename to programs/src/desktop/apps/app_procmgr.cpp diff --git a/programs/src/desktop/app_settings.cpp b/programs/src/desktop/apps/app_settings.cpp similarity index 99% rename from programs/src/desktop/app_settings.cpp rename to programs/src/desktop/apps/app_settings.cpp index 4b90239..70b9be9 100644 --- a/programs/src/desktop/app_settings.cpp +++ b/programs/src/desktop/apps/app_settings.cpp @@ -5,7 +5,7 @@ */ #include "apps_common.hpp" -#include "wallpaper.hpp" +#include "../wallpaper.hpp" // ============================================================================ // Settings state diff --git a/programs/src/desktop/app_spreadsheet.cpp b/programs/src/desktop/apps/app_spreadsheet.cpp similarity index 100% rename from programs/src/desktop/app_spreadsheet.cpp rename to programs/src/desktop/apps/app_spreadsheet.cpp diff --git a/programs/src/desktop/app_sysinfo.cpp b/programs/src/desktop/apps/app_sysinfo.cpp similarity index 100% rename from programs/src/desktop/app_sysinfo.cpp rename to programs/src/desktop/apps/app_sysinfo.cpp diff --git a/programs/src/desktop/app_terminal.cpp b/programs/src/desktop/apps/app_terminal.cpp similarity index 100% rename from programs/src/desktop/app_terminal.cpp rename to programs/src/desktop/apps/app_terminal.cpp diff --git a/programs/src/desktop/app_texteditor.cpp b/programs/src/desktop/apps/app_texteditor.cpp similarity index 100% rename from programs/src/desktop/app_texteditor.cpp rename to programs/src/desktop/apps/app_texteditor.cpp diff --git a/programs/src/desktop/app_weather.cpp b/programs/src/desktop/apps/app_weather.cpp similarity index 100% rename from programs/src/desktop/app_weather.cpp rename to programs/src/desktop/apps/app_weather.cpp diff --git a/programs/src/desktop/app_wiki.cpp b/programs/src/desktop/apps/app_wiki.cpp similarity index 100% rename from programs/src/desktop/app_wiki.cpp rename to programs/src/desktop/apps/app_wiki.cpp diff --git a/programs/src/desktop/app_wordprocessor.cpp b/programs/src/desktop/apps/app_wordprocessor.cpp similarity index 100% rename from programs/src/desktop/app_wordprocessor.cpp rename to programs/src/desktop/apps/app_wordprocessor.cpp diff --git a/programs/src/desktop/apps_common.hpp b/programs/src/desktop/apps/apps_common.hpp similarity index 100% rename from programs/src/desktop/apps_common.hpp rename to programs/src/desktop/apps/apps_common.hpp diff --git a/programs/src/desktop/compose.cpp b/programs/src/desktop/compose.cpp new file mode 100644 index 0000000..2d6595d --- /dev/null +++ b/programs/src/desktop/compose.cpp @@ -0,0 +1,154 @@ +/* + * compose.cpp + * Desktop composition: background, windows, overlays, cursor + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "desktop_internal.hpp" + +void gui::desktop_compose(DesktopState* ds) { + Framebuffer& fb = ds->fb; + + // Desktop background gradient + int sw = ds->screen_w; + int sh = ds->screen_h; + int grad_start = PANEL_HEIGHT; + int grad_range = sh - grad_start; + if (grad_range < 1) grad_range = 1; + + uint32_t* buf = fb.buffer(); + int pitch = fb.pitch(); + + for (int y = 0; y < sh; y++) { + uint32_t* row = (uint32_t*)((uint8_t*)buf + y * pitch); + if (ds->settings.bg_image && ds->settings.bg_wallpaper) { + // Wallpaper covers the entire screen; panel draws on top + uint32_t* wp = ds->settings.bg_wallpaper + y * ds->settings.bg_wallpaper_w; + int cw = sw < ds->settings.bg_wallpaper_w ? sw : ds->settings.bg_wallpaper_w; + for (int x = 0; x < cw; x++) row[x] = wp[x]; + } else if (y < grad_start) { + // Panel area - will be overwritten by panel drawing + uint32_t px = ds->settings.panel_color.to_pixel(); + for (int x = 0; x < sw; x++) row[x] = px; + } else if (ds->settings.bg_gradient) { + int t = y - grad_start; + Color top = ds->settings.bg_grad_top; + Color bot = ds->settings.bg_grad_bottom; + uint8_t r = top.r - (top.r - bot.r) * t / grad_range; + uint8_t g = top.g - (top.g - bot.g) * t / grad_range; + uint8_t b = top.b - (top.b - bot.b) * t / grad_range; + uint32_t px = 0xFF000000 | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b; + for (int x = 0; x < sw; x++) row[x] = px; + } else { + uint32_t px = ds->settings.bg_solid.to_pixel(); + for (int x = 0; x < sw; x++) row[x] = px; + } + } + + // 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) { + desktop_draw_window(ds, i); + } + } + + // Draw panel on top + desktop_draw_panel(ds); + + // Draw app menu if open + if (ds->app_menu_open) { + desktop_draw_app_menu(ds); + } + + // Draw network popup if open + if (ds->net_popup_open) { + desktop_draw_net_popup(ds); + } + + // Draw right-click context menu if open + if (ds->ctx_menu_open) { + static constexpr int CTX_MENU_W = 180; + static constexpr int CTX_ITEM_H = 36; + static constexpr int CTX_ITEM_COUNT = 5; + int cmx = ds->ctx_menu_x; + int cmy = ds->ctx_menu_y; + int cmh = CTX_ITEM_H * CTX_ITEM_COUNT + 8; + + // Clamp to screen + if (cmx + CTX_MENU_W > sw) cmx = sw - CTX_MENU_W; + if (cmy + cmh > sh) cmy = sh - cmh; + + draw_shadow(fb, cmx, cmy, CTX_MENU_W, cmh, 4, colors::SHADOW); + fill_rounded_rect(fb, cmx, cmy, CTX_MENU_W, cmh, 8, colors::MENU_BG); + draw_rect(fb, cmx, cmy, CTX_MENU_W, cmh, colors::BORDER); + + struct CtxItem { const char* label; SvgIcon* icon; }; + CtxItem ctx_items[CTX_ITEM_COUNT] = { + { "Terminal", &ds->icon_terminal }, + { "Files", &ds->icon_filemanager }, + { "About", &ds->icon_settings }, + { "Reboot", &ds->icon_reboot }, + { "Shutdown", &ds->icon_shutdown }, + }; + + int mmx = ds->mouse.x; + int mmy = ds->mouse.y; + + for (int i = 0; i < CTX_ITEM_COUNT; i++) { + int iy = cmy + 4 + i * CTX_ITEM_H; + Rect item_r = {cmx + 4, iy, CTX_MENU_W - 8, CTX_ITEM_H}; + + if (item_r.contains(mmx, mmy)) { + fill_rounded_rect(fb, item_r.x, item_r.y, item_r.w, item_r.h, 4, colors::MENU_HOVER); + } + + int icon_x = item_r.x + 8; + int icon_y = item_r.y + (CTX_ITEM_H - 20) / 2; + if (ctx_items[i].icon && ctx_items[i].icon->pixels) { + fb.blit_alpha(icon_x, icon_y, ctx_items[i].icon->width, ctx_items[i].icon->height, ctx_items[i].icon->pixels); + } + + int tx = icon_x + 28; + int ty = item_r.y + (CTX_ITEM_H - system_font_height()) / 2; + draw_text(fb, tx, ty, ctx_items[i].label, colors::TEXT_COLOR); + } + } + + // Draw snap preview overlay while dragging to screen edge + for (int i = 0; i < ds->window_count; i++) { + Window* win = &ds->windows[i]; + if (win->dragging) { + int dmx = ds->mouse.x; + if (dmx <= 0) { + fb.fill_rect_alpha(0, PANEL_HEIGHT, sw / 2, sh - PANEL_HEIGHT, + Color::from_rgba(0x33, 0x77, 0xCC, 0x30)); + } else if (dmx >= sw - 1) { + fb.fill_rect_alpha(sw / 2, PANEL_HEIGHT, sw / 2, sh - PANEL_HEIGHT, + Color::from_rgba(0x33, 0x77, 0xCC, 0x30)); + } + break; + } + } + + // Determine cursor style based on resize hover or active resize + CursorStyle cur_style = CURSOR_ARROW; + for (int i = ds->window_count - 1; i >= 0; i--) { + Window* win = &ds->windows[i]; + if (win->resizing) { + cur_style = cursor_for_edge(win->resize_edge); + break; + } + if (win->state == WIN_MINIMIZED || win->state == WIN_CLOSED || win->state == WIN_MAXIMIZED) + continue; + if (win->frame.contains(ds->mouse.x, ds->mouse.y)) { + ResizeEdge edge = hit_test_resize_edge(win->frame, ds->mouse.x, ds->mouse.y); + if (edge != RESIZE_NONE) { + cur_style = cursor_for_edge(edge); + } + break; + } + } + + // Draw cursor last + draw_cursor(fb, ds->mouse.x, ds->mouse.y, cur_style); +} diff --git a/programs/src/desktop/desktop_internal.hpp b/programs/src/desktop/desktop_internal.hpp new file mode 100644 index 0000000..2be5f16 --- /dev/null +++ b/programs/src/desktop/desktop_internal.hpp @@ -0,0 +1,101 @@ +/* + * desktop_internal.hpp + * Shared declarations for desktop source files + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once + +#include "apps/apps_common.hpp" +#include "wallpaper.hpp" + +// ============================================================================ +// App Menu Data +// ============================================================================ + +static constexpr int MENU_W = 220; +static constexpr int MENU_ITEM_H = 36; +static constexpr int MENU_CAT_H = 32; +static constexpr int MENU_DIV_H = 10; + +struct MenuRow { + bool is_category; + const char* label; // "" for divider-only rows + int app_id; // -1 for category headers / dividers +}; + +static constexpr int MENU_ROW_COUNT = 22; +static const MenuRow menu_rows[MENU_ROW_COUNT] = { + { true, "Applications", -1 }, // cat 0 + { false, "Terminal", 0 }, + { false, "Files", 1 }, + { false, "Text Editor", 4 }, + { false, "Word Processor", 15 }, + { false, "Spreadsheet", 16 }, + { false, "Calculator", 3 }, + { true, "Internet", -1 }, // cat 1 + { false, "Wikipedia", 9 }, + { false, "Weather", 13 }, + { true, "System", -1 }, // cat 2 + { false, "System Info", 2 }, + { false, "Kernel Log", 5 }, + { false, "Processes", 6 }, + { false, "Devices", 8 }, + { true, "Games", -1 }, // cat 3 + { false, "Mandelbrot", 7 }, + { false, "DOOM", 10 }, + { true, "", -1 }, // divider (always visible) + { false, "Settings", 11 }, + { false, "Reboot", 12 }, + { false, "Shutdown", 14 }, +}; + +// Collapsible category state (categories 0-3 are toggleable; divider category always expanded) +static constexpr int MENU_NUM_CATS = 5; +inline bool menu_cat_expanded[MENU_NUM_CATS] = { false, false, false, false, true }; + +inline int menu_get_cat(int row_idx) { + int cat = -1; + for (int i = 0; i <= row_idx; i++) + if (menu_rows[i].is_category) cat++; + return cat; +} + +inline bool menu_row_visible(int row_idx) { + const MenuRow& row = menu_rows[row_idx]; + if (row.is_category) return true; + int cat = menu_get_cat(row_idx); + if (cat < 0 || cat >= MENU_NUM_CATS) return true; + return menu_cat_expanded[cat]; +} + +inline int menu_row_height(const MenuRow& row) { + if (!row.is_category) return MENU_ITEM_H; + return row.label[0] ? MENU_CAT_H : MENU_DIV_H; +} + +inline int menu_total_height() { + int h = 10; // top + bottom padding + for (int i = 0; i < MENU_ROW_COUNT; i++) + if (menu_row_visible(i)) + h += menu_row_height(menu_rows[i]); + return h; +} + +// ============================================================================ +// Forward Declarations (internal functions) +// ============================================================================ + +// window.cpp +gui::ResizeEdge hit_test_resize_edge(const gui::Rect& f, int mx, int my); +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); + +// Month names (shared by panel clock) +inline const char* month_names[] = { + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" +}; diff --git a/programs/src/desktop/dialogs.cpp b/programs/src/desktop/dialogs.cpp new file mode 100644 index 0000000..d234080 --- /dev/null +++ b/programs/src/desktop/dialogs.cpp @@ -0,0 +1,236 @@ +/* + * dialogs.cpp + * Reboot and shutdown confirmation dialogs + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "desktop_internal.hpp" + +// ============================================================================ +// Reboot Dialog +// ============================================================================ + +struct RebootDialogState { + DesktopState* ds; + // Button layout (pixel-buffer-relative coordinates) + int btn_w, btn_h, btn_y, reboot_x, cancel_x; + bool hover_reboot, hover_cancel; +}; + +static void reboot_dialog_on_draw(Window* win, Framebuffer& fb) { + RebootDialogState* rs = (RebootDialogState*)win->app_data; + if (!rs) return; + + Canvas c(win); + c.fill(colors::WINDOW_BG); + + // "Reboot the system?" centered + const char* msg = "Reboot the system?"; + int tw = text_width(msg); + c.text((c.w - tw) / 2, 30, msg, colors::TEXT_COLOR); + + // Compute button layout + int btn_w = 100; + int btn_h = 32; + int btn_y = c.h - btn_h - 20; + int gap = 20; + int total_w = btn_w * 2 + gap; + int bx = (c.w - total_w) / 2; + rs->btn_w = btn_w; + rs->btn_h = btn_h; + rs->btn_y = btn_y; + rs->reboot_x = bx; + rs->cancel_x = bx + btn_w + gap; + + // Draw Reboot button + Color reboot_bg = rs->hover_reboot + ? Color::from_rgb(0xDD, 0x44, 0x44) + : Color::from_rgb(0xCC, 0x33, 0x33); + c.button(rs->reboot_x, btn_y, btn_w, btn_h, "Reboot", reboot_bg, colors::WHITE, 4); + + // Draw Cancel button + Color cancel_bg = rs->hover_cancel + ? Color::from_rgb(0x99, 0x99, 0x99) + : Color::from_rgb(0x88, 0x88, 0x88); + c.button(rs->cancel_x, btn_y, btn_w, btn_h, "Cancel", cancel_bg, colors::WHITE, 4); +} + +static void reboot_dialog_on_mouse(Window* win, MouseEvent& ev) { + RebootDialogState* rs = (RebootDialogState*)win->app_data; + if (!rs) return; + + Rect cr = win->content_rect(); + int lx = ev.x - cr.x; + int ly = ev.y - cr.y; + + Rect rb = {rs->reboot_x, rs->btn_y, rs->btn_w, rs->btn_h}; + Rect cb = {rs->cancel_x, rs->btn_y, rs->btn_w, rs->btn_h}; + rs->hover_reboot = rb.contains(lx, ly); + rs->hover_cancel = cb.contains(lx, ly); + + if (ev.left_pressed()) { + if (rs->hover_reboot) montauk::reset(); + if (rs->hover_cancel) { + for (int i = 0; i < rs->ds->window_count; i++) { + if (rs->ds->windows[i].app_data == rs) { + desktop_close_window(rs->ds, i); + return; + } + } + } + } +} + +static void reboot_dialog_on_key(Window* win, const Montauk::KeyEvent& key) { + RebootDialogState* rs = (RebootDialogState*)win->app_data; + if (!rs || !key.pressed) return; + + if (key.ascii == '\n' || key.ascii == '\r') { + montauk::reset(); + } + if (key.scancode == 0x01) { // Escape + for (int i = 0; i < rs->ds->window_count; i++) { + if (rs->ds->windows[i].app_data == rs) { + desktop_close_window(rs->ds, i); + return; + } + } + } +} + +static void reboot_dialog_on_close(Window* win) { + if (win->app_data) { + montauk::mfree(win->app_data); + win->app_data = nullptr; + } +} + +void open_reboot_dialog(DesktopState* ds) { + int wx = (ds->screen_w - 300) / 2; + int wy = (ds->screen_h - 150) / 2; + int idx = desktop_create_window(ds, "Reboot", wx, wy, 300, 150); + if (idx < 0) return; + + Window* win = &ds->windows[idx]; + RebootDialogState* rs = (RebootDialogState*)montauk::malloc(sizeof(RebootDialogState)); + montauk::memset(rs, 0, sizeof(RebootDialogState)); + rs->ds = ds; + + win->app_data = rs; + win->on_draw = reboot_dialog_on_draw; + win->on_mouse = reboot_dialog_on_mouse; + win->on_key = reboot_dialog_on_key; + win->on_close = reboot_dialog_on_close; +} + +// ============================================================================ +// Shutdown Dialog +// ============================================================================ + +struct ShutdownDialogState { + DesktopState* ds; + int btn_w, btn_h, btn_y, shutdown_x, cancel_x; + bool hover_shutdown, hover_cancel; +}; + +static void shutdown_dialog_on_draw(Window* win, Framebuffer& fb) { + ShutdownDialogState* ss = (ShutdownDialogState*)win->app_data; + if (!ss) return; + + Canvas c(win); + c.fill(colors::WINDOW_BG); + + const char* msg = "Shut down the system?"; + int tw = text_width(msg); + c.text((c.w - tw) / 2, 30, msg, colors::TEXT_COLOR); + + int btn_w = 100; + int btn_h = 32; + int btn_y = c.h - btn_h - 20; + int gap = 20; + int total_w = btn_w * 2 + gap; + int bx = (c.w - total_w) / 2; + ss->btn_w = btn_w; + ss->btn_h = btn_h; + ss->btn_y = btn_y; + ss->shutdown_x = bx; + ss->cancel_x = bx + btn_w + gap; + + Color shutdown_bg = ss->hover_shutdown + ? Color::from_rgb(0xDD, 0x44, 0x44) + : Color::from_rgb(0xCC, 0x33, 0x33); + c.button(ss->shutdown_x, btn_y, btn_w, btn_h, "Shut Down", shutdown_bg, colors::WHITE, 4); + + Color cancel_bg = ss->hover_cancel + ? Color::from_rgb(0x99, 0x99, 0x99) + : Color::from_rgb(0x88, 0x88, 0x88); + c.button(ss->cancel_x, btn_y, btn_w, btn_h, "Cancel", cancel_bg, colors::WHITE, 4); +} + +static void shutdown_dialog_on_mouse(Window* win, MouseEvent& ev) { + ShutdownDialogState* ss = (ShutdownDialogState*)win->app_data; + if (!ss) return; + + Rect cr = win->content_rect(); + int lx = ev.x - cr.x; + int ly = ev.y - cr.y; + + Rect sb = {ss->shutdown_x, ss->btn_y, ss->btn_w, ss->btn_h}; + Rect cb = {ss->cancel_x, ss->btn_y, ss->btn_w, ss->btn_h}; + ss->hover_shutdown = sb.contains(lx, ly); + ss->hover_cancel = cb.contains(lx, ly); + + if (ev.left_pressed()) { + if (ss->hover_shutdown) montauk::shutdown(); + if (ss->hover_cancel) { + for (int i = 0; i < ss->ds->window_count; i++) { + if (ss->ds->windows[i].app_data == ss) { + desktop_close_window(ss->ds, i); + return; + } + } + } + } +} + +static void shutdown_dialog_on_key(Window* win, const Montauk::KeyEvent& key) { + ShutdownDialogState* ss = (ShutdownDialogState*)win->app_data; + if (!ss || !key.pressed) return; + + if (key.ascii == '\n' || key.ascii == '\r') { + montauk::shutdown(); + } + if (key.scancode == 0x01) { // Escape + for (int i = 0; i < ss->ds->window_count; i++) { + if (ss->ds->windows[i].app_data == ss) { + desktop_close_window(ss->ds, i); + return; + } + } + } +} + +static void shutdown_dialog_on_close(Window* win) { + if (win->app_data) { + montauk::mfree(win->app_data); + win->app_data = nullptr; + } +} + +void open_shutdown_dialog(DesktopState* ds) { + int wx = (ds->screen_w - 300) / 2; + int wy = (ds->screen_h - 150) / 2; + int idx = desktop_create_window(ds, "Shut Down", wx, wy, 300, 150); + if (idx < 0) return; + + Window* win = &ds->windows[idx]; + ShutdownDialogState* ss = (ShutdownDialogState*)montauk::malloc(sizeof(ShutdownDialogState)); + montauk::memset(ss, 0, sizeof(ShutdownDialogState)); + ss->ds = ds; + + win->app_data = ss; + win->on_draw = shutdown_dialog_on_draw; + win->on_mouse = shutdown_dialog_on_mouse; + win->on_key = shutdown_dialog_on_key; + win->on_close = shutdown_dialog_on_close; +} diff --git a/programs/src/desktop/input.cpp b/programs/src/desktop/input.cpp new file mode 100644 index 0000000..e5ed94e --- /dev/null +++ b/programs/src/desktop/input.cpp @@ -0,0 +1,530 @@ +/* + * input.cpp + * Mouse and keyboard event handling + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "desktop_internal.hpp" + +void gui::desktop_handle_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); + bool left_held = (buttons & 0x01); + bool left_released = !(buttons & 0x01) && (prev & 0x01); + bool right_pressed = (buttons & 0x02) && !(prev & 0x02); + + MouseEvent ev; + ev.x = mx; + ev.y = my; + ev.buttons = buttons; + ev.prev_buttons = prev; + ev.scroll = ds->mouse.scrollDelta; + + // Handle context menu clicks + if (ds->ctx_menu_open) { + if (left_pressed) { + static constexpr int CTX_MENU_W = 180; + static constexpr int CTX_ITEM_H = 36; + static constexpr int CTX_ITEM_COUNT = 5; + int cmx = ds->ctx_menu_x; + int cmy = ds->ctx_menu_y; + int cmh = CTX_ITEM_H * CTX_ITEM_COUNT + 8; + if (cmx + CTX_MENU_W > ds->screen_w) cmx = ds->screen_w - CTX_MENU_W; + if (cmy + cmh > ds->screen_h) cmy = ds->screen_h - cmh; + + Rect ctx_rect = {cmx, cmy, CTX_MENU_W, cmh}; + if (ctx_rect.contains(mx, my)) { + int rel_y = my - cmy - 4; + int item_idx = rel_y / CTX_ITEM_H; + if (item_idx >= 0 && item_idx < CTX_ITEM_COUNT) { + ds->ctx_menu_open = false; + switch (item_idx) { + case 0: open_terminal(ds); break; + case 1: open_filemanager(ds); break; + case 2: open_settings(ds); break; + case 3: open_reboot_dialog(ds); break; + case 4: open_shutdown_dialog(ds); break; + } + return; + } + } + ds->ctx_menu_open = false; + return; + } + if (right_pressed) { + ds->ctx_menu_open = false; + return; + } + } + + // Check for ongoing window drags first + for (int i = 0; i < ds->window_count; i++) { + Window* win = &ds->windows[i]; + if (win->dragging) { + if (left_held) { + win->frame.x = mx - win->drag_offset_x; + win->frame.y = my - win->drag_offset_y; + if (win->frame.x < -win->frame.w + 50) win->frame.x = -win->frame.w + 50; + if (win->frame.y < 0) win->frame.y = 0; + if (win->frame.x > ds->screen_w - 50) win->frame.x = ds->screen_w - 50; + if (win->frame.y > ds->screen_h - 50) win->frame.y = ds->screen_h - 50; + } + if (left_released) { + win->dragging = false; + // Window edge snapping + if (mx <= 0) { + win->saved_frame = win->frame; + win->frame = {0, PANEL_HEIGHT, ds->screen_w / 2, ds->screen_h - PANEL_HEIGHT}; + win->state = WIN_MAXIMIZED; + if (!win->external) { + Rect cr = win->content_rect(); + if (cr.w != win->content_w || cr.h != win->content_h) { + if (win->content) montauk::free(win->content); + win->content_w = cr.w; + win->content_h = cr.h; + win->content = (uint32_t*)montauk::alloc(cr.w * cr.h * 4); + montauk::memset(win->content, 0xFF, cr.w * cr.h * 4); + } + } else { + Rect cr = win->content_rect(); + Montauk::WinEvent rev; + montauk::memset(&rev, 0, sizeof(rev)); + rev.type = 2; + rev.resize.w = cr.w; + rev.resize.h = cr.h; + montauk::win_sendevent(win->ext_win_id, &rev); + } + } else if (mx >= ds->screen_w - 1) { + win->saved_frame = win->frame; + win->frame = {ds->screen_w / 2, PANEL_HEIGHT, ds->screen_w / 2, ds->screen_h - PANEL_HEIGHT}; + win->state = WIN_MAXIMIZED; + if (!win->external) { + Rect cr = win->content_rect(); + if (cr.w != win->content_w || cr.h != win->content_h) { + if (win->content) montauk::free(win->content); + win->content_w = cr.w; + win->content_h = cr.h; + win->content = (uint32_t*)montauk::alloc(cr.w * cr.h * 4); + montauk::memset(win->content, 0xFF, cr.w * cr.h * 4); + } + } else { + Rect cr = win->content_rect(); + Montauk::WinEvent rev; + montauk::memset(&rev, 0, sizeof(rev)); + rev.type = 2; + rev.resize.w = cr.w; + rev.resize.h = cr.h; + montauk::win_sendevent(win->ext_win_id, &rev); + } + } + } + return; + } + } + + // Check for ongoing window resizes + for (int i = 0; i < ds->window_count; i++) { + Window* win = &ds->windows[i]; + if (win->resizing) { + if (left_held) { + int dx = mx - win->resize_start_mx; + int dy = my - win->resize_start_my; + Rect sf = win->resize_start_frame; + ResizeEdge edge = win->resize_edge; + + int new_x = sf.x, new_y = sf.y, new_w = sf.w, new_h = sf.h; + + if (edge == RESIZE_RIGHT || edge == RESIZE_TOP_RIGHT || edge == RESIZE_BOTTOM_RIGHT) + new_w = sf.w + dx; + if (edge == RESIZE_BOTTOM || edge == RESIZE_BOTTOM_LEFT || edge == RESIZE_BOTTOM_RIGHT) + new_h = sf.h + dy; + if (edge == RESIZE_LEFT || edge == RESIZE_TOP_LEFT || edge == RESIZE_BOTTOM_LEFT) { + new_x = sf.x + dx; + new_w = sf.w - dx; + } + if (edge == RESIZE_TOP || edge == RESIZE_TOP_LEFT || edge == RESIZE_TOP_RIGHT) { + new_y = sf.y + dy; + new_h = sf.h - dy; + } + + // Enforce minimum size + if (new_w < MIN_WINDOW_W) { + if (edge == RESIZE_LEFT || edge == RESIZE_TOP_LEFT || edge == RESIZE_BOTTOM_LEFT) + new_x = sf.x + sf.w - MIN_WINDOW_W; + new_w = MIN_WINDOW_W; + } + if (new_h < MIN_WINDOW_H) { + if (edge == RESIZE_TOP || edge == RESIZE_TOP_LEFT || edge == RESIZE_TOP_RIGHT) + new_y = sf.y + sf.h - MIN_WINDOW_H; + new_h = MIN_WINDOW_H; + } + + win->frame = {new_x, new_y, new_w, new_h}; + } + if (left_released) { + win->resizing = false; + // Reallocate content buffer if dimensions changed (skip for external) + if (!win->external) { + Rect cr = win->content_rect(); + if (cr.w != win->content_w || cr.h != win->content_h) { + if (win->content) montauk::free(win->content); + win->content_w = cr.w; + win->content_h = cr.h; + win->content = (uint32_t*)montauk::alloc(cr.w * cr.h * 4); + montauk::memset(win->content, 0xFF, cr.w * cr.h * 4); + } + win->dirty = true; + } else { + Rect cr = win->content_rect(); + Montauk::WinEvent rev; + montauk::memset(&rev, 0, sizeof(rev)); + rev.type = 2; + rev.resize.w = cr.w; + rev.resize.h = cr.h; + montauk::win_sendevent(win->ext_win_id, &rev); + } + } + return; + } + } + + // Handle app menu clicks + if (ds->app_menu_open && left_pressed) { + int menu_x = 4; + int menu_y = PANEL_HEIGHT + 2; + int menu_h = menu_total_height(); + Rect menu_rect = {menu_x, menu_y, MENU_W, menu_h}; + + if (menu_rect.contains(mx, my)) { + // Walk visible rows to find which one was clicked + int iy = menu_y + 5; + int cur_cat = -1; + for (int i = 0; i < MENU_ROW_COUNT; i++) { + const MenuRow& row = menu_rows[i]; + if (row.is_category) cur_cat++; + if (!menu_row_visible(i)) continue; + int row_h = menu_row_height(row); + if (my >= iy && my < iy + row_h) { + if (row.is_category && row.label[0] && cur_cat >= 0 && cur_cat < MENU_NUM_CATS - 1) { + // Toggle category expand/collapse + menu_cat_expanded[cur_cat] = !menu_cat_expanded[cur_cat]; + } else if (!row.is_category) { + switch (row.app_id) { + case 0: open_terminal(ds); break; + case 1: open_filemanager(ds); break; + case 2: open_sysinfo(ds); break; + case 3: open_calculator(ds); break; + case 4: open_texteditor(ds); break; + case 5: open_klog(ds); break; + case 6: open_procmgr(ds); break; + case 7: open_mandelbrot(ds); break; + case 8: open_devexplorer(ds); break; + case 9: open_wiki(ds); break; + case 10: open_doom(ds); break; + case 11: open_settings(ds); break; + case 12: open_reboot_dialog(ds); break; + case 13: open_weather(ds); break; + case 14: open_shutdown_dialog(ds); break; + case 15: open_wordprocessor(ds); break; + case 16: open_spreadsheet(ds); break; + } + ds->app_menu_open = false; + } + break; + } + iy += row_h; + } + return; + } else { + ds->app_menu_open = false; + } + } + + // Handle net popup clicks + if (ds->net_popup_open && left_pressed) { + int popup_w = 220; + int popup_h = 130; + 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; + Rect popup_rect = {popup_x, popup_y, popup_w, popup_h}; + + if (popup_rect.contains(mx, my)) { + return; + } else if (!ds->net_icon_rect.contains(mx, my)) { + ds->net_popup_open = false; + } + } + + // Panel click check + if (left_pressed && my < PANEL_HEIGHT) { + // App menu button + if (mx < 36) { + ds->app_menu_open = !ds->app_menu_open; + ds->net_popup_open = false; + ds->ctx_menu_open = false; + return; + } + + // Network icon + 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->ctx_menu_open = false; + return; + } + // Window indicator buttons + int indicator_x = 40; + for (int i = 0; i < ds->window_count; i++) { + Window* win = &ds->windows[i]; + if (win->state == WIN_CLOSED) continue; + + int tw = text_width(win->title); + int pad = 12; + int iw = tw + pad * 2; + if (iw > 150) iw = 150; + + Rect btn_rect = {indicator_x, 4, iw, 24}; + if (btn_rect.contains(mx, my)) { + if (win->state == WIN_MINIMIZED) { + win->state = WIN_NORMAL; + } + desktop_raise_window(ds, i); + return; + } + indicator_x += iw + 4; + } + return; + } + + // Window interaction: check from top (last) to bottom (first) + if (left_pressed) { + for (int i = ds->window_count - 1; i >= 0; i--) { + Window* win = &ds->windows[i]; + if (win->state == WIN_MINIMIZED || win->state == WIN_CLOSED) continue; + + // Check close button + Rect close_r = win->close_btn_rect(); + if (close_r.contains(mx, my)) { + desktop_close_window(ds, i); + return; + } + + // Check minimize button + Rect min_r = win->min_btn_rect(); + if (min_r.contains(mx, my)) { + win->state = WIN_MINIMIZED; + if (ds->focused_window == i) { + ds->focused_window = -1; + for (int j = ds->window_count - 1; j >= 0; j--) { + if (ds->windows[j].state == WIN_NORMAL || ds->windows[j].state == WIN_MAXIMIZED) { + ds->focused_window = j; + ds->windows[j].focused = true; + break; + } + } + } + return; + } + + // Check maximize button + Rect max_r = win->max_btn_rect(); + if (max_r.contains(mx, my)) { + if (win->state == WIN_MAXIMIZED) { + win->frame = win->saved_frame; + win->state = WIN_NORMAL; + } else { + win->saved_frame = win->frame; + win->frame = {0, PANEL_HEIGHT, ds->screen_w, ds->screen_h - PANEL_HEIGHT}; + win->state = WIN_MAXIMIZED; + } + // Reallocate content buffer for local windows only + if (!win->external) { + Rect cr = win->content_rect(); + if (cr.w != win->content_w || cr.h != win->content_h) { + if (win->content) montauk::free(win->content); + win->content_w = cr.w; + win->content_h = cr.h; + win->content = (uint32_t*)montauk::alloc(cr.w * cr.h * 4); + montauk::memset(win->content, 0xFF, cr.w * cr.h * 4); + } + } else { + Rect cr = win->content_rect(); + Montauk::WinEvent rev; + montauk::memset(&rev, 0, sizeof(rev)); + rev.type = 2; + rev.resize.w = cr.w; + rev.resize.h = cr.h; + montauk::win_sendevent(win->ext_win_id, &rev); + } + desktop_raise_window(ds, i); + return; + } + + // Check resize edges (before titlebar drag, so corner grabs work) + 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; + ds->windows[new_idx].resize_edge = edge; + ds->windows[new_idx].resize_start_frame = ds->windows[new_idx].frame; + ds->windows[new_idx].resize_start_mx = mx; + ds->windows[new_idx].resize_start_my = my; + return; + } + } + + // 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; + ds->windows[new_idx].drag_offset_x = mx - ds->windows[new_idx].frame.x; + ds->windows[new_idx].drag_offset_y = my - ds->windows[new_idx].frame.y; + return; + } + + // Check content area + Rect cr = win->content_rect(); + if (cr.contains(mx, my)) { + desktop_raise_window(ds, i); + int new_idx = ds->window_count - 1; + Window* raised = &ds->windows[new_idx]; + if (raised->external) { + // Forward mouse event to external window + Montauk::WinEvent wev; + montauk::memset(&wev, 0, sizeof(wev)); + wev.type = 1; // mouse + wev.mouse.x = mx - cr.x; + wev.mouse.y = my - cr.y; + wev.mouse.scroll = ev.scroll; + wev.mouse.buttons = buttons; + wev.mouse.prev_buttons = prev; + montauk::win_sendevent(raised->ext_win_id, &wev); + } else if (raised->on_mouse) { + ev.x = mx; + ev.y = my; + raised->on_mouse(raised, ev); + } + return; + } + + // Check full frame + if (win->frame.contains(mx, my)) { + desktop_raise_window(ds, i); + return; + } + } + + ds->app_menu_open = false; + ds->ctx_menu_open = false; + } + + // Handle scroll events on focused window + if (ev.scroll != 0 && ds->focused_window >= 0) { + Window* win = &ds->windows[ds->focused_window]; + Rect cr = win->content_rect(); + if (cr.contains(mx, my)) { + if (win->external) { + Montauk::WinEvent wev; + montauk::memset(&wev, 0, sizeof(wev)); + wev.type = 1; // mouse + wev.mouse.x = mx - cr.x; + wev.mouse.y = my - cr.y; + wev.mouse.scroll = ev.scroll; + wev.mouse.buttons = buttons; + wev.mouse.prev_buttons = prev; + montauk::win_sendevent(win->ext_win_id, &wev); + } else if (win->on_mouse) { + win->on_mouse(win, ev); + } + } + } + + // Right-click on desktop background opens context menu + if (right_pressed && my >= PANEL_HEIGHT) { + bool on_window = false; + for (int i = ds->window_count - 1; i >= 0; i--) { + Window* win = &ds->windows[i]; + if (win->state == WIN_MINIMIZED || win->state == WIN_CLOSED) continue; + if (win->frame.contains(mx, my)) { + on_window = true; + break; + } + } + if (!on_window) { + ds->ctx_menu_open = true; + ds->ctx_menu_x = mx; + ds->ctx_menu_y = my; + ds->app_menu_open = false; + ds->net_popup_open = false; + } + } +} + +void gui::desktop_handle_keyboard(DesktopState* ds, const Montauk::KeyEvent& key) { + // Global shortcuts (only on key press) + if (key.pressed && key.ctrl && key.alt) { + if (key.ascii == 't' || key.ascii == 'T') { + open_terminal(ds); + return; + } + if (key.ascii == 'f' || key.ascii == 'F') { + open_filemanager(ds); + return; + } + if (key.ascii == 'i' || key.ascii == 'I') { + open_sysinfo(ds); + return; + } + if (key.ascii == 'c' || key.ascii == 'C') { + open_calculator(ds); + return; + } + if (key.ascii == 'e' || key.ascii == 'E') { + open_texteditor(ds); + return; + } + if (key.ascii == 'w' || key.ascii == 'W') { + open_wordprocessor(ds); + return; + } + if (key.ascii == 'k' || key.ascii == 'K') { + open_klog(ds); + return; + } + if (key.ascii == 'd' || key.ascii == 'D') { + open_doom(ds); + return; + } + } + + // Dispatch to focused window + if (ds->focused_window >= 0 && ds->focused_window < ds->window_count) { + Window* win = &ds->windows[ds->focused_window]; + if (win->external) { + // Forward key event to external window via syscall + Montauk::WinEvent ev; + montauk::memset(&ev, 0, sizeof(ev)); + ev.type = 0; // key + ev.key = key; + montauk::win_sendevent(win->ext_win_id, &ev); + } else if (win->on_key) { + win->on_key(win, key); + } + } +} diff --git a/programs/src/desktop/main.cpp b/programs/src/desktop/main.cpp index b7411d3..ed1bb7b 100644 --- a/programs/src/desktop/main.cpp +++ b/programs/src/desktop/main.cpp @@ -1,21 +1,15 @@ /* * main.cpp - * MontaukOS Desktop Environment - window manager, compositor, and run loop + * MontaukOS Desktop Environment - initialization, external window polling, run loop * Copyright (c) 2026 Daniel Hammer */ -#include "apps_common.hpp" -#include "wallpaper.hpp" +#include "desktop_internal.hpp" // ============================================================================ -// Desktop Implementation +// Initialization // ============================================================================ -static const char* month_names[] = { - "Jan", "Feb", "Mar", "Apr", "May", "Jun", - "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" -}; - void gui::desktop_init(DesktopState* ds) { ds->screen_w = ds->fb.width(); ds->screen_h = ds->fb.height(); @@ -103,1510 +97,6 @@ void gui::desktop_init(DesktopState* ds) { } -int gui::desktop_create_window(DesktopState* ds, const char* title, int x, int y, int w, int h) { - if (ds->window_count >= MAX_WINDOWS) return -1; - - int idx = ds->window_count; - Window* win = &ds->windows[idx]; - montauk::memset(win, 0, sizeof(Window)); - - montauk::strncpy(win->title, title, MAX_TITLE_LEN); - win->frame = {x, y, w, h}; - win->state = WIN_NORMAL; - win->z_order = idx; - win->focused = true; - win->dirty = true; - win->dragging = false; - win->resizing = false; - win->saved_frame = win->frame; - - // Allocate content buffer - Rect cr = win->content_rect(); - win->content_w = cr.w; - win->content_h = cr.h; - int buf_size = cr.w * cr.h * 4; - win->content = (uint32_t*)montauk::alloc(buf_size); - montauk::memset(win->content, 0xFF, buf_size); - - win->on_draw = nullptr; - win->on_mouse = nullptr; - win->on_key = nullptr; - win->on_close = nullptr; - win->on_poll = nullptr; - win->app_data = nullptr; - win->external = false; - win->ext_win_id = -1; - - // Unfocus previous window - if (ds->focused_window >= 0 && ds->focused_window < ds->window_count) { - ds->windows[ds->focused_window].focused = false; - } - ds->focused_window = idx; - ds->window_count++; - - return idx; -} - -void gui::desktop_close_window(DesktopState* ds, int idx) { - if (idx < 0 || idx >= ds->window_count) return; - - Window* win = &ds->windows[idx]; - - // For external windows, send a close event instead of freeing the buffer - if (win->external) { - Montauk::WinEvent ev; - montauk::memset(&ev, 0, sizeof(ev)); - ev.type = 3; // close - montauk::win_sendevent(win->ext_win_id, &ev); - } - - if (win->on_close) win->on_close(win); - - // Free content buffer (skip for external windows — shared memory) - if (win->content && !win->external) { - montauk::free(win->content); - win->content = nullptr; - } - - // Shift remaining windows down - for (int i = idx; i < ds->window_count - 1; i++) { - ds->windows[i] = ds->windows[i + 1]; - } - ds->window_count--; - - // Fix focused window index - if (ds->focused_window == idx) { - ds->focused_window = ds->window_count > 0 ? ds->window_count - 1 : -1; - } else if (ds->focused_window > idx) { - ds->focused_window--; - } - - if (ds->focused_window >= 0 && ds->focused_window < ds->window_count) { - ds->windows[ds->focused_window].focused = true; - } -} - -void gui::desktop_raise_window(DesktopState* ds, int idx) { - if (idx < 0 || idx >= ds->window_count) return; - if (idx == ds->window_count - 1) { - // Already on top, just focus - if (ds->focused_window >= 0 && ds->focused_window < ds->window_count) - ds->windows[ds->focused_window].focused = false; - ds->focused_window = idx; - ds->windows[idx].focused = true; - return; - } - - // Unfocus current - if (ds->focused_window >= 0 && ds->focused_window < ds->window_count) { - ds->windows[ds->focused_window].focused = false; - } - - // Move window to end (top of stack) - Window tmp = ds->windows[idx]; - for (int i = idx; i < ds->window_count - 1; i++) { - ds->windows[i] = ds->windows[i + 1]; - } - ds->windows[ds->window_count - 1] = tmp; - - ds->focused_window = ds->window_count - 1; - ds->windows[ds->focused_window].focused = true; -} - -void gui::desktop_draw_window(DesktopState* ds, int idx) { - if (idx < 0 || idx >= ds->window_count) return; - Window* win = &ds->windows[idx]; - if (win->state == WIN_MINIMIZED || win->state == WIN_CLOSED) return; - - Framebuffer& fb = ds->fb; - int x = win->frame.x; - int y = win->frame.y; - int w = win->frame.w; - int h = win->frame.h; - - // Draw shadow - if (ds->settings.show_shadows) - draw_shadow(fb, x, y, w, h, SHADOW_SIZE, colors::SHADOW); - - // Draw window body - fb.fill_rect(x, y, w, h, colors::WINDOW_BG); - - // Draw titlebar - Color tb_bg = win->focused ? colors::TITLEBAR_BG : Color::from_rgb(0xE8, 0xE8, 0xE8); - fb.fill_rect(x, y, w, TITLEBAR_HEIGHT, tb_bg); - - // Draw border - draw_rect(fb, x, y, w, h, colors::BORDER); - // Titlebar bottom separator - draw_hline(fb, x, y + TITLEBAR_HEIGHT - 1, w, colors::BORDER); - - // Draw window buttons (macOS style: close, minimize, maximize) - Rect close_r = win->close_btn_rect(); - Rect min_r = win->min_btn_rect(); - Rect max_r = win->max_btn_rect(); - - fill_circle(fb, close_r.x + BTN_RADIUS, close_r.y + BTN_RADIUS, BTN_RADIUS, colors::CLOSE_BTN); - fill_circle(fb, min_r.x + BTN_RADIUS, min_r.y + BTN_RADIUS, BTN_RADIUS, colors::MIN_BTN); - fill_circle(fb, max_r.x + BTN_RADIUS, max_r.y + BTN_RADIUS, BTN_RADIUS, colors::MAX_BTN); - - // Draw title text centered in titlebar (after buttons) - int title_x = x + 12 + 44 + BTN_RADIUS * 2 + 12; // after buttons - int title_y = y + (TITLEBAR_HEIGHT - system_font_height()) / 2; - int title_w = text_width(win->title); - // Center in remaining space - int remaining_w = w - (title_x - x) - 12; - if (remaining_w > title_w) { - title_x += (remaining_w - title_w) / 2; - } - draw_text(fb, title_x, title_y, win->title, colors::TEXT_COLOR); - - // Call app draw callback to render content (skip during resize — buffer is old size) - if (win->on_draw && !win->resizing) { - win->on_draw(win, fb); - } - - // Blit content buffer to framebuffer (clip to actual buffer size during resize) - Rect cr = win->content_rect(); - if (win->content) { - if (win->external && (cr.w != win->content_w || cr.h != win->content_h)) { - // Nearest-neighbor scale for external windows (fixed-size shared buffer) - int src_w = win->content_w; - int src_h = win->content_h; - int dst_w = cr.w; - int dst_h = cr.h; - uint32_t* buf = fb.buffer(); - int pitch = fb.pitch(); - for (int y = 0; y < dst_h; y++) { - int dy = cr.y + y; - if (dy < 0 || dy >= fb.height()) continue; - int sy = y * src_h / dst_h; - uint32_t* dst_row = (uint32_t*)((uint8_t*)buf + dy * pitch); - uint32_t* src_row = win->content + sy * src_w; - for (int x = 0; x < dst_w; x++) { - int dx = cr.x + x; - if (dx < 0 || dx >= fb.width()) continue; - dst_row[dx] = src_row[x * src_w / dst_w]; - } - } - } else { - int blit_w = cr.w < win->content_w ? cr.w : win->content_w; - int blit_h = cr.h < win->content_h ? cr.h : win->content_h; - fb.blit(cr.x, cr.y, blit_w, blit_h, win->content); - } - } -} - -void gui::desktop_draw_panel(DesktopState* ds) { - Framebuffer& fb = ds->fb; - int sw = ds->screen_w; - - // Panel gradient background (slightly lighter at top) - Color pc = ds->settings.panel_color; - for (int y = 0; y < PANEL_HEIGHT; y++) { - int t = y * 255 / PANEL_HEIGHT; - uint8_t r = pc.r + (10 - t * 10 / 255); - uint8_t g = pc.g + (10 - t * 10 / 255); - uint8_t b = pc.b + (10 - t * 10 / 255); - fb.fill_rect(0, y, sw, 1, Color::from_rgb(r, g, b)); - } - - // Bottom highlight line (skip when wallpaper is set — the white bleeds through) - if (!ds->settings.bg_image) { - fb.fill_rect(0, PANEL_HEIGHT - 1, sw, 1, Color::from_rgba(0xFF, 0xFF, 0xFF, 0x10)); - } - - // App menu button (left side) - int btn_x = 4; - int btn_y = 2; - int btn_w = 28; - int btn_h = 28; - - if (ds->icon_appmenu.pixels) { - int ix = btn_x + (btn_w - ds->icon_appmenu.width) / 2; - int iy = btn_y + (btn_h - ds->icon_appmenu.height) / 2; - fb.blit_alpha(ix, iy, ds->icon_appmenu.width, ds->icon_appmenu.height, ds->icon_appmenu.pixels); - } else { - for (int gr = 0; gr < 3; gr++) { - for (int gc = 0; gc < 3; gc++) { - int dx = btn_x + 6 + gc * 6; - int dy = btn_y + 6 + gr * 6; - fb.fill_rect(dx, dy, 3, 3, colors::PANEL_TEXT); - } - } - } - - // Window indicator pills (center area) - int indicator_x = 40; - for (int i = 0; i < ds->window_count; i++) { - Window* win = &ds->windows[i]; - if (win->state == WIN_CLOSED) continue; - - int tw = text_width(win->title); - int pad = 12; - int iw = tw + pad * 2; - if (iw > 150) iw = 150; - - // Pre-blended indicator pill colors (opaque, blended against PANEL_BG) - Color pill_bg = (i == ds->focused_window) - ? colors::PANEL_INDICATOR_ACTIVE - : colors::PANEL_INDICATOR_INACTIVE; - - fill_rounded_rect(fb, indicator_x, 4, iw, 24, 6, pill_bg); - - // Active window accent underline bar - if (i == ds->focused_window) { - fb.fill_rect(indicator_x + 4, 26, iw - 8, 2, ds->settings.accent_color); - } - - // Truncate title if too long - char short_title[20]; - montauk::strncpy(short_title, win->title, 18); - - int tx = indicator_x + pad; - int ty = 4 + (24 - system_font_height()) / 2; - draw_text(fb, tx, ty, short_title, colors::PANEL_TEXT); - - indicator_x += iw + 4; - } - - // Date + Clock (right side) - Montauk::DateTime dt; - montauk::gettime(&dt); - - char clock_str[12]; - if (ds->settings.clock_24h) { - snprintf(clock_str, sizeof(clock_str), "%02d:%02d", (int)dt.Hour, (int)dt.Minute); - } else { - int h12 = (int)dt.Hour % 12; - if (h12 == 0) h12 = 12; - const char* ampm = dt.Hour < 12 ? "AM" : "PM"; - snprintf(clock_str, sizeof(clock_str), "%d:%02d %s", h12, (int)dt.Minute, ampm); - } - int clock_w = text_width(clock_str); - int clock_x = sw - clock_w - 12; - int clock_y = (PANEL_HEIGHT - system_font_height()) / 2; - draw_text(fb, clock_x, clock_y, clock_str, colors::PANEL_TEXT); - - // Date before clock - char date_str[12]; - int month_idx = dt.Month > 0 && dt.Month <= 12 ? dt.Month - 1 : 0; - snprintf(date_str, sizeof(date_str), "%s %d", month_names[month_idx], (int)dt.Day); - int date_w = text_width(date_str); - 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) - uint64_t now = montauk::get_milliseconds(); - 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_y = (PANEL_HEIGHT - 16) / 2; - ds->net_icon_rect = {net_icon_x, net_icon_y, 16, 16}; - - if (ds->icon_network.pixels) { - if (ds->cached_net_cfg.ipAddress == 0) { - uint32_t* src = ds->icon_network.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) | 0x004444CC; - } - fb.blit_alpha(net_icon_x, net_icon_y, 16, 16, tinted); - } else { - fb.blit_alpha(net_icon_x, net_icon_y, ds->icon_network.width, ds->icon_network.height, ds->icon_network.pixels); - } - } -} - -// ============================================================================ -// App Menu (categorized) -// ============================================================================ - -static constexpr int MENU_W = 220; -static constexpr int MENU_ITEM_H = 36; -static constexpr int MENU_CAT_H = 32; -static constexpr int MENU_DIV_H = 10; - -struct MenuRow { - bool is_category; - const char* label; // "" for divider-only rows - int app_id; // -1 for category headers / dividers -}; - -static constexpr int MENU_ROW_COUNT = 22; -static const MenuRow menu_rows[MENU_ROW_COUNT] = { - { true, "Applications", -1 }, // cat 0 - { false, "Terminal", 0 }, - { false, "Files", 1 }, - { false, "Text Editor", 4 }, - { false, "Word Processor", 15 }, - { false, "Spreadsheet", 16 }, - { false, "Calculator", 3 }, - { true, "Internet", -1 }, // cat 1 - { false, "Wikipedia", 9 }, - { false, "Weather", 13 }, - { true, "System", -1 }, // cat 2 - { false, "System Info", 2 }, - { false, "Kernel Log", 5 }, - { false, "Processes", 6 }, - { false, "Devices", 8 }, - { true, "Games", -1 }, // cat 3 - { false, "Mandelbrot", 7 }, - { false, "DOOM", 10 }, - { true, "", -1 }, // divider (always visible) - { false, "Settings", 11 }, - { false, "Reboot", 12 }, - { false, "Shutdown", 14 }, -}; - -// Collapsible category state (categories 0-3 are toggleable; divider category always expanded) -static constexpr int MENU_NUM_CATS = 5; -static bool menu_cat_expanded[MENU_NUM_CATS] = { false, false, false, false, true }; - -// Map a menu_rows index to its category index (0-4), or -1 for items before any category -static int menu_get_cat(int row_idx) { - int cat = -1; - for (int i = 0; i <= row_idx; i++) - if (menu_rows[i].is_category) cat++; - return cat; -} - -// Check if a row should be visible given current expand/collapse state -static bool menu_row_visible(int row_idx) { - const MenuRow& row = menu_rows[row_idx]; - if (row.is_category) return true; // category headers always visible - int cat = menu_get_cat(row_idx); - if (cat < 0 || cat >= MENU_NUM_CATS) return true; - return menu_cat_expanded[cat]; -} - -static int menu_row_height(const MenuRow& row) { - if (!row.is_category) return MENU_ITEM_H; - return row.label[0] ? MENU_CAT_H : MENU_DIV_H; -} - -static int menu_total_height() { - int h = 10; // top + bottom padding - for (int i = 0; i < MENU_ROW_COUNT; i++) - if (menu_row_visible(i)) - h += menu_row_height(menu_rows[i]); - return h; -} - -static void desktop_draw_app_menu(DesktopState* ds) { - Framebuffer& fb = ds->fb; - - int menu_x = 4; - int menu_y = PANEL_HEIGHT + 2; - int menu_h = menu_total_height(); - - // Menu shadow - draw_shadow(fb, menu_x, menu_y, MENU_W, menu_h, 4, colors::SHADOW); - - // Menu background with rounded corners - fill_rounded_rect(fb, menu_x, menu_y, MENU_W, menu_h, 8, colors::MENU_BG); - draw_rect(fb, menu_x, menu_y, MENU_W, menu_h, colors::BORDER); - - // Icon lookup by app_id - SvgIcon* icons[17] = { - &ds->icon_terminal, // 0 - &ds->icon_filemanager, // 1 - &ds->icon_sysinfo, // 2 - &ds->icon_calculator, // 3 - &ds->icon_texteditor, // 4 - &ds->icon_terminal, // 5 (Kernel Log) - &ds->icon_procmgr, // 6 - &ds->icon_mandelbrot, // 7 - &ds->icon_devexplorer, // 8 - &ds->icon_wikipedia, // 9 - &ds->icon_doom, // 10 - &ds->icon_settings, // 11 - &ds->icon_reboot, // 12 - &ds->icon_weather, // 13 - &ds->icon_shutdown, // 14 - &ds->icon_texteditor, // 15 - &ds->icon_spreadsheet, // 16 - }; - - int mx = ds->mouse.x; - int my = ds->mouse.y; - int iy = menu_y + 5; - int cur_cat = -1; - - for (int i = 0; i < MENU_ROW_COUNT; i++) { - const MenuRow& row = menu_rows[i]; - - if (row.is_category) cur_cat++; - if (!menu_row_visible(i)) continue; - - int row_h = menu_row_height(row); - - if (row.is_category) { - // Separator line above (except first category) - if (i > 0) { - for (int sx = menu_x + 8; sx < menu_x + MENU_W - 8; sx++) - fb.put_pixel(sx, iy + 1, colors::BORDER); - } - - // Category header with hover + expand/collapse indicator - if (row.label[0]) { - Rect cat_rect = {menu_x + 4, iy, MENU_W - 8, row_h}; - bool hovered = cat_rect.contains(mx, my); - if (hovered) - fill_rounded_rect(fb, cat_rect.x, cat_rect.y, cat_rect.w, cat_rect.h, 4, - Color::from_rgb(0xE8, 0xE8, 0xE8)); - - // Arrow indicator: > for collapsed, v for expanded - bool expanded = (cur_cat >= 0 && cur_cat < MENU_NUM_CATS) && menu_cat_expanded[cur_cat]; - int ax = menu_x + 10; - int ay = iy + row_h / 2; - Color arrow_color = Color::from_rgb(0x88, 0x88, 0x88); - if (expanded) { - // Down arrow (v shape) - for (int d = 0; d < 4; d++) { - fb.put_pixel(ax + d, ay - 2 + d, arrow_color); - fb.put_pixel(ax + d + 1, ay - 2 + d, arrow_color); - fb.put_pixel(ax + 7 - d, ay - 2 + d, arrow_color); - fb.put_pixel(ax + 6 - d, ay - 2 + d, arrow_color); - } - } else { - // Right arrow (> shape) - for (int d = 0; d < 4; d++) { - fb.put_pixel(ax + d, ay - 3 + d, arrow_color); - fb.put_pixel(ax + d, ay - 3 + d + 1, arrow_color); - fb.put_pixel(ax + d, ay + 3 - d, arrow_color); - fb.put_pixel(ax + d, ay + 3 - d - 1, arrow_color); - } - } - - int tx = menu_x + 24; - int ty = iy + (row_h - system_font_height()) / 2; - draw_text(fb, tx, ty, row.label, Color::from_rgb(0x66, 0x66, 0x66)); - } - } else { - Rect item_rect = {menu_x + 4, iy, MENU_W - 8, row_h}; - - // Hover highlight - if (item_rect.contains(mx, my)) { - fill_rounded_rect(fb, item_rect.x, item_rect.y, item_rect.w, item_rect.h, 4, colors::MENU_HOVER); - } - - // Icon - int icon_x = item_rect.x + 8; - int icon_y = item_rect.y + (row_h - 20) / 2; - if (row.app_id >= 0 && row.app_id < 17) { - SvgIcon* icon = icons[row.app_id]; - if (icon && icon->pixels) { - fb.blit_alpha(icon_x, icon_y, icon->width, icon->height, icon->pixels); - } - } - - // Label - int tx = icon_x + 28; - int ty = item_rect.y + (row_h - system_font_height()) / 2; - draw_text(fb, tx, ty, row.label, colors::TEXT_COLOR); - } - - iy += row_h; - } -} - -static void desktop_draw_net_popup(DesktopState* ds) { - Framebuffer& fb = ds->fb; - - int popup_w = 220; - int popup_h = 130; - 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); - draw_rect(fb, popup_x, popup_y, popup_w, popup_h, colors::BORDER); - - int tx = popup_x + 12; - int ty = popup_y + 10; - int line_h = system_font_height() + 6; - char line[64]; - - Montauk::NetCfg& nc = ds->cached_net_cfg; - - 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"); - } - 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); -} - -// ============================================================================ -// Reboot Dialog -// ============================================================================ - -struct RebootDialogState { - DesktopState* ds; - // Button layout (pixel-buffer-relative coordinates) - int btn_w, btn_h, btn_y, reboot_x, cancel_x; - bool hover_reboot, hover_cancel; -}; - -static void reboot_dialog_on_draw(Window* win, Framebuffer& fb) { - RebootDialogState* rs = (RebootDialogState*)win->app_data; - if (!rs) return; - - Canvas c(win); - c.fill(colors::WINDOW_BG); - - // "Reboot the system?" centered - const char* msg = "Reboot the system?"; - int tw = text_width(msg); - c.text((c.w - tw) / 2, 30, msg, colors::TEXT_COLOR); - - // Compute button layout - int btn_w = 100; - int btn_h = 32; - int btn_y = c.h - btn_h - 20; - int gap = 20; - int total_w = btn_w * 2 + gap; - int bx = (c.w - total_w) / 2; - rs->btn_w = btn_w; - rs->btn_h = btn_h; - rs->btn_y = btn_y; - rs->reboot_x = bx; - rs->cancel_x = bx + btn_w + gap; - - // Draw Reboot button - Color reboot_bg = rs->hover_reboot - ? Color::from_rgb(0xDD, 0x44, 0x44) - : Color::from_rgb(0xCC, 0x33, 0x33); - c.button(rs->reboot_x, btn_y, btn_w, btn_h, "Reboot", reboot_bg, colors::WHITE, 4); - - // Draw Cancel button - Color cancel_bg = rs->hover_cancel - ? Color::from_rgb(0x99, 0x99, 0x99) - : Color::from_rgb(0x88, 0x88, 0x88); - c.button(rs->cancel_x, btn_y, btn_w, btn_h, "Cancel", cancel_bg, colors::WHITE, 4); -} - -static void reboot_dialog_on_mouse(Window* win, MouseEvent& ev) { - RebootDialogState* rs = (RebootDialogState*)win->app_data; - if (!rs) return; - - Rect cr = win->content_rect(); - int lx = ev.x - cr.x; - int ly = ev.y - cr.y; - - Rect rb = {rs->reboot_x, rs->btn_y, rs->btn_w, rs->btn_h}; - Rect cb = {rs->cancel_x, rs->btn_y, rs->btn_w, rs->btn_h}; - rs->hover_reboot = rb.contains(lx, ly); - rs->hover_cancel = cb.contains(lx, ly); - - if (ev.left_pressed()) { - if (rs->hover_reboot) montauk::reset(); - if (rs->hover_cancel) { - for (int i = 0; i < rs->ds->window_count; i++) { - if (rs->ds->windows[i].app_data == rs) { - desktop_close_window(rs->ds, i); - return; - } - } - } - } -} - -static void reboot_dialog_on_key(Window* win, const Montauk::KeyEvent& key) { - RebootDialogState* rs = (RebootDialogState*)win->app_data; - if (!rs || !key.pressed) return; - - if (key.ascii == '\n' || key.ascii == '\r') { - montauk::reset(); - } - if (key.scancode == 0x01) { // Escape - for (int i = 0; i < rs->ds->window_count; i++) { - if (rs->ds->windows[i].app_data == rs) { - desktop_close_window(rs->ds, i); - return; - } - } - } -} - -static void reboot_dialog_on_close(Window* win) { - if (win->app_data) { - montauk::mfree(win->app_data); - win->app_data = nullptr; - } -} - -void open_reboot_dialog(DesktopState* ds) { - int wx = (ds->screen_w - 300) / 2; - int wy = (ds->screen_h - 150) / 2; - int idx = desktop_create_window(ds, "Reboot", wx, wy, 300, 150); - if (idx < 0) return; - - Window* win = &ds->windows[idx]; - RebootDialogState* rs = (RebootDialogState*)montauk::malloc(sizeof(RebootDialogState)); - montauk::memset(rs, 0, sizeof(RebootDialogState)); - rs->ds = ds; - - win->app_data = rs; - win->on_draw = reboot_dialog_on_draw; - win->on_mouse = reboot_dialog_on_mouse; - win->on_key = reboot_dialog_on_key; - win->on_close = reboot_dialog_on_close; -} - -// ============================================================================ -// Shutdown Dialog -// ============================================================================ - -struct ShutdownDialogState { - DesktopState* ds; - int btn_w, btn_h, btn_y, shutdown_x, cancel_x; - bool hover_shutdown, hover_cancel; -}; - -static void shutdown_dialog_on_draw(Window* win, Framebuffer& fb) { - ShutdownDialogState* ss = (ShutdownDialogState*)win->app_data; - if (!ss) return; - - Canvas c(win); - c.fill(colors::WINDOW_BG); - - const char* msg = "Shut down the system?"; - int tw = text_width(msg); - c.text((c.w - tw) / 2, 30, msg, colors::TEXT_COLOR); - - int btn_w = 100; - int btn_h = 32; - int btn_y = c.h - btn_h - 20; - int gap = 20; - int total_w = btn_w * 2 + gap; - int bx = (c.w - total_w) / 2; - ss->btn_w = btn_w; - ss->btn_h = btn_h; - ss->btn_y = btn_y; - ss->shutdown_x = bx; - ss->cancel_x = bx + btn_w + gap; - - Color shutdown_bg = ss->hover_shutdown - ? Color::from_rgb(0xDD, 0x44, 0x44) - : Color::from_rgb(0xCC, 0x33, 0x33); - c.button(ss->shutdown_x, btn_y, btn_w, btn_h, "Shut Down", shutdown_bg, colors::WHITE, 4); - - Color cancel_bg = ss->hover_cancel - ? Color::from_rgb(0x99, 0x99, 0x99) - : Color::from_rgb(0x88, 0x88, 0x88); - c.button(ss->cancel_x, btn_y, btn_w, btn_h, "Cancel", cancel_bg, colors::WHITE, 4); -} - -static void shutdown_dialog_on_mouse(Window* win, MouseEvent& ev) { - ShutdownDialogState* ss = (ShutdownDialogState*)win->app_data; - if (!ss) return; - - Rect cr = win->content_rect(); - int lx = ev.x - cr.x; - int ly = ev.y - cr.y; - - Rect sb = {ss->shutdown_x, ss->btn_y, ss->btn_w, ss->btn_h}; - Rect cb = {ss->cancel_x, ss->btn_y, ss->btn_w, ss->btn_h}; - ss->hover_shutdown = sb.contains(lx, ly); - ss->hover_cancel = cb.contains(lx, ly); - - if (ev.left_pressed()) { - if (ss->hover_shutdown) montauk::shutdown(); - if (ss->hover_cancel) { - for (int i = 0; i < ss->ds->window_count; i++) { - if (ss->ds->windows[i].app_data == ss) { - desktop_close_window(ss->ds, i); - return; - } - } - } - } -} - -static void shutdown_dialog_on_key(Window* win, const Montauk::KeyEvent& key) { - ShutdownDialogState* ss = (ShutdownDialogState*)win->app_data; - if (!ss || !key.pressed) return; - - if (key.ascii == '\n' || key.ascii == '\r') { - montauk::shutdown(); - } - if (key.scancode == 0x01) { // Escape - for (int i = 0; i < ss->ds->window_count; i++) { - if (ss->ds->windows[i].app_data == ss) { - desktop_close_window(ss->ds, i); - return; - } - } - } -} - -static void shutdown_dialog_on_close(Window* win) { - if (win->app_data) { - montauk::mfree(win->app_data); - win->app_data = nullptr; - } -} - -void open_shutdown_dialog(DesktopState* ds) { - int wx = (ds->screen_w - 300) / 2; - int wy = (ds->screen_h - 150) / 2; - int idx = desktop_create_window(ds, "Shut Down", wx, wy, 300, 150); - if (idx < 0) return; - - Window* win = &ds->windows[idx]; - ShutdownDialogState* ss = (ShutdownDialogState*)montauk::malloc(sizeof(ShutdownDialogState)); - montauk::memset(ss, 0, sizeof(ShutdownDialogState)); - ss->ds = ds; - - win->app_data = ss; - win->on_draw = shutdown_dialog_on_draw; - win->on_mouse = shutdown_dialog_on_mouse; - win->on_key = shutdown_dialog_on_key; - win->on_close = shutdown_dialog_on_close; -} - -static gui::ResizeEdge hit_test_resize_edge(const gui::Rect& f, int mx, int my) { - using namespace gui; - int G = RESIZE_GRAB; - if (!f.contains(mx, my)) return RESIZE_NONE; - - bool near_left = mx < f.x + G; - bool near_right = mx >= f.x + f.w - G; - bool near_top = my < f.y + G; - bool near_bottom = my >= f.y + f.h - G; - - // Corners first - if (near_top && near_left) return RESIZE_TOP_LEFT; - if (near_top && near_right) return RESIZE_TOP_RIGHT; - if (near_bottom && near_left) return RESIZE_BOTTOM_LEFT; - if (near_bottom && near_right) return RESIZE_BOTTOM_RIGHT; - - // Edges - if (near_top) return RESIZE_TOP; - if (near_bottom) return RESIZE_BOTTOM; - if (near_left) return RESIZE_LEFT; - if (near_right) return RESIZE_RIGHT; - - return RESIZE_NONE; -} - -static gui::CursorStyle cursor_for_edge(gui::ResizeEdge edge) { - using namespace gui; - switch (edge) { - case RESIZE_LEFT: case RESIZE_RIGHT: - return CURSOR_RESIZE_H; - case RESIZE_TOP: case RESIZE_BOTTOM: - return CURSOR_RESIZE_V; - case RESIZE_TOP_LEFT: case RESIZE_BOTTOM_RIGHT: - return CURSOR_RESIZE_NWSE; - case RESIZE_TOP_RIGHT: case RESIZE_BOTTOM_LEFT: - return CURSOR_RESIZE_NESW; - default: - return CURSOR_ARROW; - } -} - -void gui::desktop_compose(DesktopState* ds) { - Framebuffer& fb = ds->fb; - - // Desktop background gradient - int sw = ds->screen_w; - int sh = ds->screen_h; - int grad_start = PANEL_HEIGHT; - int grad_range = sh - grad_start; - if (grad_range < 1) grad_range = 1; - - uint32_t* buf = fb.buffer(); - int pitch = fb.pitch(); - - for (int y = 0; y < sh; y++) { - uint32_t* row = (uint32_t*)((uint8_t*)buf + y * pitch); - if (ds->settings.bg_image && ds->settings.bg_wallpaper) { - // Wallpaper covers the entire screen; panel draws on top - uint32_t* wp = ds->settings.bg_wallpaper + y * ds->settings.bg_wallpaper_w; - int cw = sw < ds->settings.bg_wallpaper_w ? sw : ds->settings.bg_wallpaper_w; - for (int x = 0; x < cw; x++) row[x] = wp[x]; - } else if (y < grad_start) { - // Panel area - will be overwritten by panel drawing - uint32_t px = ds->settings.panel_color.to_pixel(); - for (int x = 0; x < sw; x++) row[x] = px; - } else if (ds->settings.bg_gradient) { - int t = y - grad_start; - Color top = ds->settings.bg_grad_top; - Color bot = ds->settings.bg_grad_bottom; - uint8_t r = top.r - (top.r - bot.r) * t / grad_range; - uint8_t g = top.g - (top.g - bot.g) * t / grad_range; - uint8_t b = top.b - (top.b - bot.b) * t / grad_range; - uint32_t px = 0xFF000000 | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b; - for (int x = 0; x < sw; x++) row[x] = px; - } else { - uint32_t px = ds->settings.bg_solid.to_pixel(); - for (int x = 0; x < sw; x++) row[x] = px; - } - } - - // 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) { - desktop_draw_window(ds, i); - } - } - - // Draw panel on top - desktop_draw_panel(ds); - - // Draw app menu if open - if (ds->app_menu_open) { - desktop_draw_app_menu(ds); - } - - // Draw network popup if open - if (ds->net_popup_open) { - desktop_draw_net_popup(ds); - } - - // Draw right-click context menu if open - if (ds->ctx_menu_open) { - static constexpr int CTX_MENU_W = 180; - static constexpr int CTX_ITEM_H = 36; - static constexpr int CTX_ITEM_COUNT = 5; - int cmx = ds->ctx_menu_x; - int cmy = ds->ctx_menu_y; - int cmh = CTX_ITEM_H * CTX_ITEM_COUNT + 8; - - // Clamp to screen - if (cmx + CTX_MENU_W > sw) cmx = sw - CTX_MENU_W; - if (cmy + cmh > sh) cmy = sh - cmh; - - draw_shadow(fb, cmx, cmy, CTX_MENU_W, cmh, 4, colors::SHADOW); - fill_rounded_rect(fb, cmx, cmy, CTX_MENU_W, cmh, 8, colors::MENU_BG); - draw_rect(fb, cmx, cmy, CTX_MENU_W, cmh, colors::BORDER); - - struct CtxItem { const char* label; SvgIcon* icon; }; - CtxItem ctx_items[CTX_ITEM_COUNT] = { - { "Terminal", &ds->icon_terminal }, - { "Files", &ds->icon_filemanager }, - { "About", &ds->icon_settings }, - { "Reboot", &ds->icon_reboot }, - { "Shutdown", &ds->icon_shutdown }, - }; - - int mmx = ds->mouse.x; - int mmy = ds->mouse.y; - - for (int i = 0; i < CTX_ITEM_COUNT; i++) { - int iy = cmy + 4 + i * CTX_ITEM_H; - Rect item_r = {cmx + 4, iy, CTX_MENU_W - 8, CTX_ITEM_H}; - - if (item_r.contains(mmx, mmy)) { - fill_rounded_rect(fb, item_r.x, item_r.y, item_r.w, item_r.h, 4, colors::MENU_HOVER); - } - - int icon_x = item_r.x + 8; - int icon_y = item_r.y + (CTX_ITEM_H - 20) / 2; - if (ctx_items[i].icon && ctx_items[i].icon->pixels) { - fb.blit_alpha(icon_x, icon_y, ctx_items[i].icon->width, ctx_items[i].icon->height, ctx_items[i].icon->pixels); - } - - int tx = icon_x + 28; - int ty = item_r.y + (CTX_ITEM_H - system_font_height()) / 2; - draw_text(fb, tx, ty, ctx_items[i].label, colors::TEXT_COLOR); - } - } - - // Draw snap preview overlay while dragging to screen edge - for (int i = 0; i < ds->window_count; i++) { - Window* win = &ds->windows[i]; - if (win->dragging) { - int dmx = ds->mouse.x; - if (dmx <= 0) { - fb.fill_rect_alpha(0, PANEL_HEIGHT, sw / 2, sh - PANEL_HEIGHT, - Color::from_rgba(0x33, 0x77, 0xCC, 0x30)); - } else if (dmx >= sw - 1) { - fb.fill_rect_alpha(sw / 2, PANEL_HEIGHT, sw / 2, sh - PANEL_HEIGHT, - Color::from_rgba(0x33, 0x77, 0xCC, 0x30)); - } - break; - } - } - - // Determine cursor style based on resize hover or active resize - CursorStyle cur_style = CURSOR_ARROW; - for (int i = ds->window_count - 1; i >= 0; i--) { - Window* win = &ds->windows[i]; - if (win->resizing) { - cur_style = cursor_for_edge(win->resize_edge); - break; - } - if (win->state == WIN_MINIMIZED || win->state == WIN_CLOSED || win->state == WIN_MAXIMIZED) - continue; - if (win->frame.contains(ds->mouse.x, ds->mouse.y)) { - ResizeEdge edge = hit_test_resize_edge(win->frame, ds->mouse.x, ds->mouse.y); - if (edge != RESIZE_NONE) { - cur_style = cursor_for_edge(edge); - } - break; - } - } - - // Draw cursor last - draw_cursor(fb, ds->mouse.x, ds->mouse.y, cur_style); -} - -void gui::desktop_handle_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); - bool left_held = (buttons & 0x01); - bool left_released = !(buttons & 0x01) && (prev & 0x01); - bool right_pressed = (buttons & 0x02) && !(prev & 0x02); - - MouseEvent ev; - ev.x = mx; - ev.y = my; - ev.buttons = buttons; - ev.prev_buttons = prev; - ev.scroll = ds->mouse.scrollDelta; - - // Handle context menu clicks - if (ds->ctx_menu_open) { - if (left_pressed) { - static constexpr int CTX_MENU_W = 180; - static constexpr int CTX_ITEM_H = 36; - static constexpr int CTX_ITEM_COUNT = 5; - int cmx = ds->ctx_menu_x; - int cmy = ds->ctx_menu_y; - int cmh = CTX_ITEM_H * CTX_ITEM_COUNT + 8; - if (cmx + CTX_MENU_W > ds->screen_w) cmx = ds->screen_w - CTX_MENU_W; - if (cmy + cmh > ds->screen_h) cmy = ds->screen_h - cmh; - - Rect ctx_rect = {cmx, cmy, CTX_MENU_W, cmh}; - if (ctx_rect.contains(mx, my)) { - int rel_y = my - cmy - 4; - int item_idx = rel_y / CTX_ITEM_H; - if (item_idx >= 0 && item_idx < CTX_ITEM_COUNT) { - ds->ctx_menu_open = false; - switch (item_idx) { - case 0: open_terminal(ds); break; - case 1: open_filemanager(ds); break; - case 2: open_settings(ds); break; - case 3: open_reboot_dialog(ds); break; - case 4: open_shutdown_dialog(ds); break; - } - return; - } - } - ds->ctx_menu_open = false; - return; - } - if (right_pressed) { - ds->ctx_menu_open = false; - return; - } - } - - // Check for ongoing window drags first - for (int i = 0; i < ds->window_count; i++) { - Window* win = &ds->windows[i]; - if (win->dragging) { - if (left_held) { - win->frame.x = mx - win->drag_offset_x; - win->frame.y = my - win->drag_offset_y; - if (win->frame.x < -win->frame.w + 50) win->frame.x = -win->frame.w + 50; - if (win->frame.y < 0) win->frame.y = 0; - if (win->frame.x > ds->screen_w - 50) win->frame.x = ds->screen_w - 50; - if (win->frame.y > ds->screen_h - 50) win->frame.y = ds->screen_h - 50; - } - if (left_released) { - win->dragging = false; - // Window edge snapping - if (mx <= 0) { - win->saved_frame = win->frame; - win->frame = {0, PANEL_HEIGHT, ds->screen_w / 2, ds->screen_h - PANEL_HEIGHT}; - win->state = WIN_MAXIMIZED; - if (!win->external) { - Rect cr = win->content_rect(); - if (cr.w != win->content_w || cr.h != win->content_h) { - if (win->content) montauk::free(win->content); - win->content_w = cr.w; - win->content_h = cr.h; - win->content = (uint32_t*)montauk::alloc(cr.w * cr.h * 4); - montauk::memset(win->content, 0xFF, cr.w * cr.h * 4); - } - } else { - Rect cr = win->content_rect(); - Montauk::WinEvent rev; - montauk::memset(&rev, 0, sizeof(rev)); - rev.type = 2; - rev.resize.w = cr.w; - rev.resize.h = cr.h; - montauk::win_sendevent(win->ext_win_id, &rev); - } - } else if (mx >= ds->screen_w - 1) { - win->saved_frame = win->frame; - win->frame = {ds->screen_w / 2, PANEL_HEIGHT, ds->screen_w / 2, ds->screen_h - PANEL_HEIGHT}; - win->state = WIN_MAXIMIZED; - if (!win->external) { - Rect cr = win->content_rect(); - if (cr.w != win->content_w || cr.h != win->content_h) { - if (win->content) montauk::free(win->content); - win->content_w = cr.w; - win->content_h = cr.h; - win->content = (uint32_t*)montauk::alloc(cr.w * cr.h * 4); - montauk::memset(win->content, 0xFF, cr.w * cr.h * 4); - } - } else { - Rect cr = win->content_rect(); - Montauk::WinEvent rev; - montauk::memset(&rev, 0, sizeof(rev)); - rev.type = 2; - rev.resize.w = cr.w; - rev.resize.h = cr.h; - montauk::win_sendevent(win->ext_win_id, &rev); - } - } - } - return; - } - } - - // Check for ongoing window resizes - for (int i = 0; i < ds->window_count; i++) { - Window* win = &ds->windows[i]; - if (win->resizing) { - if (left_held) { - int dx = mx - win->resize_start_mx; - int dy = my - win->resize_start_my; - Rect sf = win->resize_start_frame; - ResizeEdge edge = win->resize_edge; - - int new_x = sf.x, new_y = sf.y, new_w = sf.w, new_h = sf.h; - - if (edge == RESIZE_RIGHT || edge == RESIZE_TOP_RIGHT || edge == RESIZE_BOTTOM_RIGHT) - new_w = sf.w + dx; - if (edge == RESIZE_BOTTOM || edge == RESIZE_BOTTOM_LEFT || edge == RESIZE_BOTTOM_RIGHT) - new_h = sf.h + dy; - if (edge == RESIZE_LEFT || edge == RESIZE_TOP_LEFT || edge == RESIZE_BOTTOM_LEFT) { - new_x = sf.x + dx; - new_w = sf.w - dx; - } - if (edge == RESIZE_TOP || edge == RESIZE_TOP_LEFT || edge == RESIZE_TOP_RIGHT) { - new_y = sf.y + dy; - new_h = sf.h - dy; - } - - // Enforce minimum size - if (new_w < MIN_WINDOW_W) { - if (edge == RESIZE_LEFT || edge == RESIZE_TOP_LEFT || edge == RESIZE_BOTTOM_LEFT) - new_x = sf.x + sf.w - MIN_WINDOW_W; - new_w = MIN_WINDOW_W; - } - if (new_h < MIN_WINDOW_H) { - if (edge == RESIZE_TOP || edge == RESIZE_TOP_LEFT || edge == RESIZE_TOP_RIGHT) - new_y = sf.y + sf.h - MIN_WINDOW_H; - new_h = MIN_WINDOW_H; - } - - win->frame = {new_x, new_y, new_w, new_h}; - } - if (left_released) { - win->resizing = false; - // Reallocate content buffer if dimensions changed (skip for external) - if (!win->external) { - Rect cr = win->content_rect(); - if (cr.w != win->content_w || cr.h != win->content_h) { - if (win->content) montauk::free(win->content); - win->content_w = cr.w; - win->content_h = cr.h; - win->content = (uint32_t*)montauk::alloc(cr.w * cr.h * 4); - montauk::memset(win->content, 0xFF, cr.w * cr.h * 4); - } - win->dirty = true; - } else { - Rect cr = win->content_rect(); - Montauk::WinEvent rev; - montauk::memset(&rev, 0, sizeof(rev)); - rev.type = 2; - rev.resize.w = cr.w; - rev.resize.h = cr.h; - montauk::win_sendevent(win->ext_win_id, &rev); - } - } - return; - } - } - - // Handle app menu clicks - if (ds->app_menu_open && left_pressed) { - int menu_x = 4; - int menu_y = PANEL_HEIGHT + 2; - int menu_h = menu_total_height(); - Rect menu_rect = {menu_x, menu_y, MENU_W, menu_h}; - - if (menu_rect.contains(mx, my)) { - // Walk visible rows to find which one was clicked - int iy = menu_y + 5; - int cur_cat = -1; - for (int i = 0; i < MENU_ROW_COUNT; i++) { - const MenuRow& row = menu_rows[i]; - if (row.is_category) cur_cat++; - if (!menu_row_visible(i)) continue; - int row_h = menu_row_height(row); - if (my >= iy && my < iy + row_h) { - if (row.is_category && row.label[0] && cur_cat >= 0 && cur_cat < MENU_NUM_CATS - 1) { - // Toggle category expand/collapse - menu_cat_expanded[cur_cat] = !menu_cat_expanded[cur_cat]; - } else if (!row.is_category) { - switch (row.app_id) { - case 0: open_terminal(ds); break; - case 1: open_filemanager(ds); break; - case 2: open_sysinfo(ds); break; - case 3: open_calculator(ds); break; - case 4: open_texteditor(ds); break; - case 5: open_klog(ds); break; - case 6: open_procmgr(ds); break; - case 7: open_mandelbrot(ds); break; - case 8: open_devexplorer(ds); break; - case 9: open_wiki(ds); break; - case 10: open_doom(ds); break; - case 11: open_settings(ds); break; - case 12: open_reboot_dialog(ds); break; - case 13: open_weather(ds); break; - case 14: open_shutdown_dialog(ds); break; - case 15: open_wordprocessor(ds); break; - case 16: open_spreadsheet(ds); break; - } - ds->app_menu_open = false; - } - break; - } - iy += row_h; - } - return; - } else { - ds->app_menu_open = false; - } - } - - // Handle net popup clicks - if (ds->net_popup_open && left_pressed) { - int popup_w = 220; - int popup_h = 130; - 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; - Rect popup_rect = {popup_x, popup_y, popup_w, popup_h}; - - if (popup_rect.contains(mx, my)) { - return; - } else if (!ds->net_icon_rect.contains(mx, my)) { - ds->net_popup_open = false; - } - } - - // Panel click check - if (left_pressed && my < PANEL_HEIGHT) { - // App menu button - if (mx < 36) { - ds->app_menu_open = !ds->app_menu_open; - ds->net_popup_open = false; - ds->ctx_menu_open = false; - return; - } - - // Network icon - 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->ctx_menu_open = false; - return; - } - // Window indicator buttons - int indicator_x = 40; - for (int i = 0; i < ds->window_count; i++) { - Window* win = &ds->windows[i]; - if (win->state == WIN_CLOSED) continue; - - int tw = text_width(win->title); - int pad = 12; - int iw = tw + pad * 2; - if (iw > 150) iw = 150; - - Rect btn_rect = {indicator_x, 4, iw, 24}; - if (btn_rect.contains(mx, my)) { - if (win->state == WIN_MINIMIZED) { - win->state = WIN_NORMAL; - } - desktop_raise_window(ds, i); - return; - } - indicator_x += iw + 4; - } - return; - } - - // Window interaction: check from top (last) to bottom (first) - if (left_pressed) { - for (int i = ds->window_count - 1; i >= 0; i--) { - Window* win = &ds->windows[i]; - if (win->state == WIN_MINIMIZED || win->state == WIN_CLOSED) continue; - - // Check close button - Rect close_r = win->close_btn_rect(); - if (close_r.contains(mx, my)) { - desktop_close_window(ds, i); - return; - } - - // Check minimize button - Rect min_r = win->min_btn_rect(); - if (min_r.contains(mx, my)) { - win->state = WIN_MINIMIZED; - if (ds->focused_window == i) { - ds->focused_window = -1; - for (int j = ds->window_count - 1; j >= 0; j--) { - if (ds->windows[j].state == WIN_NORMAL || ds->windows[j].state == WIN_MAXIMIZED) { - ds->focused_window = j; - ds->windows[j].focused = true; - break; - } - } - } - return; - } - - // Check maximize button - Rect max_r = win->max_btn_rect(); - if (max_r.contains(mx, my)) { - if (win->state == WIN_MAXIMIZED) { - win->frame = win->saved_frame; - win->state = WIN_NORMAL; - } else { - win->saved_frame = win->frame; - win->frame = {0, PANEL_HEIGHT, ds->screen_w, ds->screen_h - PANEL_HEIGHT}; - win->state = WIN_MAXIMIZED; - } - // Reallocate content buffer for local windows only - if (!win->external) { - Rect cr = win->content_rect(); - if (cr.w != win->content_w || cr.h != win->content_h) { - if (win->content) montauk::free(win->content); - win->content_w = cr.w; - win->content_h = cr.h; - win->content = (uint32_t*)montauk::alloc(cr.w * cr.h * 4); - montauk::memset(win->content, 0xFF, cr.w * cr.h * 4); - } - } else { - Rect cr = win->content_rect(); - Montauk::WinEvent rev; - montauk::memset(&rev, 0, sizeof(rev)); - rev.type = 2; - rev.resize.w = cr.w; - rev.resize.h = cr.h; - montauk::win_sendevent(win->ext_win_id, &rev); - } - desktop_raise_window(ds, i); - return; - } - - // Check resize edges (before titlebar drag, so corner grabs work) - 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; - ds->windows[new_idx].resize_edge = edge; - ds->windows[new_idx].resize_start_frame = ds->windows[new_idx].frame; - ds->windows[new_idx].resize_start_mx = mx; - ds->windows[new_idx].resize_start_my = my; - return; - } - } - - // 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; - ds->windows[new_idx].drag_offset_x = mx - ds->windows[new_idx].frame.x; - ds->windows[new_idx].drag_offset_y = my - ds->windows[new_idx].frame.y; - return; - } - - // Check content area - Rect cr = win->content_rect(); - if (cr.contains(mx, my)) { - desktop_raise_window(ds, i); - int new_idx = ds->window_count - 1; - Window* raised = &ds->windows[new_idx]; - if (raised->external) { - // Forward mouse event to external window - Montauk::WinEvent wev; - montauk::memset(&wev, 0, sizeof(wev)); - wev.type = 1; // mouse - wev.mouse.x = mx - cr.x; - wev.mouse.y = my - cr.y; - wev.mouse.scroll = ev.scroll; - wev.mouse.buttons = buttons; - wev.mouse.prev_buttons = prev; - montauk::win_sendevent(raised->ext_win_id, &wev); - } else if (raised->on_mouse) { - ev.x = mx; - ev.y = my; - raised->on_mouse(raised, ev); - } - return; - } - - // Check full frame - if (win->frame.contains(mx, my)) { - desktop_raise_window(ds, i); - return; - } - } - - ds->app_menu_open = false; - ds->ctx_menu_open = false; - } - - // Handle scroll events on focused window - if (ev.scroll != 0 && ds->focused_window >= 0) { - Window* win = &ds->windows[ds->focused_window]; - Rect cr = win->content_rect(); - if (cr.contains(mx, my)) { - if (win->external) { - Montauk::WinEvent wev; - montauk::memset(&wev, 0, sizeof(wev)); - wev.type = 1; // mouse - wev.mouse.x = mx - cr.x; - wev.mouse.y = my - cr.y; - wev.mouse.scroll = ev.scroll; - wev.mouse.buttons = buttons; - wev.mouse.prev_buttons = prev; - montauk::win_sendevent(win->ext_win_id, &wev); - } else if (win->on_mouse) { - win->on_mouse(win, ev); - } - } - } - - // Right-click on desktop background opens context menu - if (right_pressed && my >= PANEL_HEIGHT) { - bool on_window = false; - for (int i = ds->window_count - 1; i >= 0; i--) { - Window* win = &ds->windows[i]; - if (win->state == WIN_MINIMIZED || win->state == WIN_CLOSED) continue; - if (win->frame.contains(mx, my)) { - on_window = true; - break; - } - } - if (!on_window) { - ds->ctx_menu_open = true; - ds->ctx_menu_x = mx; - ds->ctx_menu_y = my; - ds->app_menu_open = false; - ds->net_popup_open = false; - } - } -} - -void gui::desktop_handle_keyboard(DesktopState* ds, const Montauk::KeyEvent& key) { - // Global shortcuts (only on key press) - if (key.pressed && key.ctrl && key.alt) { - if (key.ascii == 't' || key.ascii == 'T') { - open_terminal(ds); - return; - } - if (key.ascii == 'f' || key.ascii == 'F') { - open_filemanager(ds); - return; - } - if (key.ascii == 'i' || key.ascii == 'I') { - open_sysinfo(ds); - return; - } - if (key.ascii == 'c' || key.ascii == 'C') { - open_calculator(ds); - return; - } - if (key.ascii == 'e' || key.ascii == 'E') { - open_texteditor(ds); - return; - } - if (key.ascii == 'w' || key.ascii == 'W') { - open_wordprocessor(ds); - return; - } - if (key.ascii == 'k' || key.ascii == 'K') { - open_klog(ds); - return; - } - if (key.ascii == 'd' || key.ascii == 'D') { - open_doom(ds); - return; - } - } - - // Dispatch to focused window - if (ds->focused_window >= 0 && ds->focused_window < ds->window_count) { - Window* win = &ds->windows[ds->focused_window]; - if (win->external) { - // Forward key event to external window via syscall - Montauk::WinEvent ev; - montauk::memset(&ev, 0, sizeof(ev)); - ev.type = 0; // key - ev.key = key; - montauk::win_sendevent(win->ext_win_id, &ev); - } else if (win->on_key) { - win->on_key(win, key); - } - } -} - // ============================================================================ // External Window Polling // ============================================================================ @@ -1711,6 +201,10 @@ void desktop_poll_external_windows(DesktopState* ds) { } } +// ============================================================================ +// Run Loop +// ============================================================================ + void gui::desktop_run(DesktopState* ds) { for (;;) { // Poll mouse state diff --git a/programs/src/desktop/panel.cpp b/programs/src/desktop/panel.cpp new file mode 100644 index 0000000..3b1dfc6 --- /dev/null +++ b/programs/src/desktop/panel.cpp @@ -0,0 +1,311 @@ +/* + * panel.cpp + * Panel bar, app menu, and network popup drawing + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "desktop_internal.hpp" + +void gui::desktop_draw_panel(DesktopState* ds) { + Framebuffer& fb = ds->fb; + int sw = ds->screen_w; + + // Panel gradient background (slightly lighter at top) + Color pc = ds->settings.panel_color; + for (int y = 0; y < PANEL_HEIGHT; y++) { + int t = y * 255 / PANEL_HEIGHT; + uint8_t r = pc.r + (10 - t * 10 / 255); + uint8_t g = pc.g + (10 - t * 10 / 255); + uint8_t b = pc.b + (10 - t * 10 / 255); + fb.fill_rect(0, y, sw, 1, Color::from_rgb(r, g, b)); + } + + // Bottom highlight line (skip when wallpaper is set — the white bleeds through) + if (!ds->settings.bg_image) { + fb.fill_rect(0, PANEL_HEIGHT - 1, sw, 1, Color::from_rgba(0xFF, 0xFF, 0xFF, 0x10)); + } + + // App menu button (left side) + int btn_x = 4; + int btn_y = 2; + int btn_w = 28; + int btn_h = 28; + + if (ds->icon_appmenu.pixels) { + int ix = btn_x + (btn_w - ds->icon_appmenu.width) / 2; + int iy = btn_y + (btn_h - ds->icon_appmenu.height) / 2; + fb.blit_alpha(ix, iy, ds->icon_appmenu.width, ds->icon_appmenu.height, ds->icon_appmenu.pixels); + } else { + for (int gr = 0; gr < 3; gr++) { + for (int gc = 0; gc < 3; gc++) { + int dx = btn_x + 6 + gc * 6; + int dy = btn_y + 6 + gr * 6; + fb.fill_rect(dx, dy, 3, 3, colors::PANEL_TEXT); + } + } + } + + // Window indicator pills (center area) + int indicator_x = 40; + for (int i = 0; i < ds->window_count; i++) { + Window* win = &ds->windows[i]; + if (win->state == WIN_CLOSED) continue; + + int tw = text_width(win->title); + int pad = 12; + int iw = tw + pad * 2; + if (iw > 150) iw = 150; + + // Pre-blended indicator pill colors (opaque, blended against PANEL_BG) + Color pill_bg = (i == ds->focused_window) + ? colors::PANEL_INDICATOR_ACTIVE + : colors::PANEL_INDICATOR_INACTIVE; + + fill_rounded_rect(fb, indicator_x, 4, iw, 24, 6, pill_bg); + + // Active window accent underline bar + if (i == ds->focused_window) { + fb.fill_rect(indicator_x + 4, 26, iw - 8, 2, ds->settings.accent_color); + } + + // Truncate title if too long + char short_title[20]; + montauk::strncpy(short_title, win->title, 18); + + int tx = indicator_x + pad; + int ty = 4 + (24 - system_font_height()) / 2; + draw_text(fb, tx, ty, short_title, colors::PANEL_TEXT); + + indicator_x += iw + 4; + } + + // Date + Clock (right side) + Montauk::DateTime dt; + montauk::gettime(&dt); + + char clock_str[12]; + if (ds->settings.clock_24h) { + snprintf(clock_str, sizeof(clock_str), "%02d:%02d", (int)dt.Hour, (int)dt.Minute); + } else { + int h12 = (int)dt.Hour % 12; + if (h12 == 0) h12 = 12; + const char* ampm = dt.Hour < 12 ? "AM" : "PM"; + snprintf(clock_str, sizeof(clock_str), "%d:%02d %s", h12, (int)dt.Minute, ampm); + } + int clock_w = text_width(clock_str); + int clock_x = sw - clock_w - 12; + int clock_y = (PANEL_HEIGHT - system_font_height()) / 2; + draw_text(fb, clock_x, clock_y, clock_str, colors::PANEL_TEXT); + + // Date before clock + char date_str[12]; + int month_idx = dt.Month > 0 && dt.Month <= 12 ? dt.Month - 1 : 0; + snprintf(date_str, sizeof(date_str), "%s %d", month_names[month_idx], (int)dt.Day); + int date_w = text_width(date_str); + 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) + uint64_t now = montauk::get_milliseconds(); + 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_y = (PANEL_HEIGHT - 16) / 2; + ds->net_icon_rect = {net_icon_x, net_icon_y, 16, 16}; + + if (ds->icon_network.pixels) { + if (ds->cached_net_cfg.ipAddress == 0) { + uint32_t* src = ds->icon_network.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) | 0x004444CC; + } + fb.blit_alpha(net_icon_x, net_icon_y, 16, 16, tinted); + } else { + fb.blit_alpha(net_icon_x, net_icon_y, ds->icon_network.width, ds->icon_network.height, ds->icon_network.pixels); + } + } +} + +// ============================================================================ +// App Menu +// ============================================================================ + +void desktop_draw_app_menu(DesktopState* ds) { + Framebuffer& fb = ds->fb; + + int menu_x = 4; + int menu_y = PANEL_HEIGHT + 2; + int menu_h = menu_total_height(); + + // Menu shadow + draw_shadow(fb, menu_x, menu_y, MENU_W, menu_h, 4, colors::SHADOW); + + // Menu background with rounded corners + fill_rounded_rect(fb, menu_x, menu_y, MENU_W, menu_h, 8, colors::MENU_BG); + draw_rect(fb, menu_x, menu_y, MENU_W, menu_h, colors::BORDER); + + // Icon lookup by app_id + SvgIcon* icons[17] = { + &ds->icon_terminal, // 0 + &ds->icon_filemanager, // 1 + &ds->icon_sysinfo, // 2 + &ds->icon_calculator, // 3 + &ds->icon_texteditor, // 4 + &ds->icon_terminal, // 5 (Kernel Log) + &ds->icon_procmgr, // 6 + &ds->icon_mandelbrot, // 7 + &ds->icon_devexplorer, // 8 + &ds->icon_wikipedia, // 9 + &ds->icon_doom, // 10 + &ds->icon_settings, // 11 + &ds->icon_reboot, // 12 + &ds->icon_weather, // 13 + &ds->icon_shutdown, // 14 + &ds->icon_texteditor, // 15 + &ds->icon_spreadsheet, // 16 + }; + + int mx = ds->mouse.x; + int my = ds->mouse.y; + int iy = menu_y + 5; + int cur_cat = -1; + + for (int i = 0; i < MENU_ROW_COUNT; i++) { + const MenuRow& row = menu_rows[i]; + + if (row.is_category) cur_cat++; + if (!menu_row_visible(i)) continue; + + int row_h = menu_row_height(row); + + if (row.is_category) { + // Separator line above (except first category) + if (i > 0) { + for (int sx = menu_x + 8; sx < menu_x + MENU_W - 8; sx++) + fb.put_pixel(sx, iy + 1, colors::BORDER); + } + + // Category header with hover + expand/collapse indicator + if (row.label[0]) { + Rect cat_rect = {menu_x + 4, iy, MENU_W - 8, row_h}; + bool hovered = cat_rect.contains(mx, my); + if (hovered) + fill_rounded_rect(fb, cat_rect.x, cat_rect.y, cat_rect.w, cat_rect.h, 4, + Color::from_rgb(0xE8, 0xE8, 0xE8)); + + // Arrow indicator: > for collapsed, v for expanded + bool expanded = (cur_cat >= 0 && cur_cat < MENU_NUM_CATS) && menu_cat_expanded[cur_cat]; + int ax = menu_x + 10; + int ay = iy + row_h / 2; + Color arrow_color = Color::from_rgb(0x88, 0x88, 0x88); + if (expanded) { + // Down arrow (v shape) + for (int d = 0; d < 4; d++) { + fb.put_pixel(ax + d, ay - 2 + d, arrow_color); + fb.put_pixel(ax + d + 1, ay - 2 + d, arrow_color); + fb.put_pixel(ax + 7 - d, ay - 2 + d, arrow_color); + fb.put_pixel(ax + 6 - d, ay - 2 + d, arrow_color); + } + } else { + // Right arrow (> shape) + for (int d = 0; d < 4; d++) { + fb.put_pixel(ax + d, ay - 3 + d, arrow_color); + fb.put_pixel(ax + d, ay - 3 + d + 1, arrow_color); + fb.put_pixel(ax + d, ay + 3 - d, arrow_color); + fb.put_pixel(ax + d, ay + 3 - d - 1, arrow_color); + } + } + + int tx = menu_x + 24; + int ty = iy + (row_h - system_font_height()) / 2; + draw_text(fb, tx, ty, row.label, Color::from_rgb(0x66, 0x66, 0x66)); + } + } else { + Rect item_rect = {menu_x + 4, iy, MENU_W - 8, row_h}; + + // Hover highlight + if (item_rect.contains(mx, my)) { + fill_rounded_rect(fb, item_rect.x, item_rect.y, item_rect.w, item_rect.h, 4, colors::MENU_HOVER); + } + + // Icon + int icon_x = item_rect.x + 8; + int icon_y = item_rect.y + (row_h - 20) / 2; + if (row.app_id >= 0 && row.app_id < 17) { + SvgIcon* icon = icons[row.app_id]; + if (icon && icon->pixels) { + fb.blit_alpha(icon_x, icon_y, icon->width, icon->height, icon->pixels); + } + } + + // Label + int tx = icon_x + 28; + int ty = item_rect.y + (row_h - system_font_height()) / 2; + draw_text(fb, tx, ty, row.label, colors::TEXT_COLOR); + } + + iy += row_h; + } +} + +// ============================================================================ +// Network Popup +// ============================================================================ + +void desktop_draw_net_popup(DesktopState* ds) { + Framebuffer& fb = ds->fb; + + int popup_w = 220; + int popup_h = 130; + 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); + draw_rect(fb, popup_x, popup_y, popup_w, popup_h, colors::BORDER); + + int tx = popup_x + 12; + int ty = popup_y + 10; + int line_h = system_font_height() + 6; + char line[64]; + + Montauk::NetCfg& nc = ds->cached_net_cfg; + + 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"); + } + 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); +} diff --git a/programs/src/desktop/window.cpp b/programs/src/desktop/window.cpp new file mode 100644 index 0000000..8161c8a --- /dev/null +++ b/programs/src/desktop/window.cpp @@ -0,0 +1,241 @@ +/* + * window.cpp + * Window management: create, close, raise, draw + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "desktop_internal.hpp" + +int gui::desktop_create_window(DesktopState* ds, const char* title, int x, int y, int w, int h) { + if (ds->window_count >= MAX_WINDOWS) return -1; + + int idx = ds->window_count; + Window* win = &ds->windows[idx]; + montauk::memset(win, 0, sizeof(Window)); + + montauk::strncpy(win->title, title, MAX_TITLE_LEN); + win->frame = {x, y, w, h}; + win->state = WIN_NORMAL; + win->z_order = idx; + win->focused = true; + win->dirty = true; + win->dragging = false; + win->resizing = false; + win->saved_frame = win->frame; + + // Allocate content buffer + Rect cr = win->content_rect(); + win->content_w = cr.w; + win->content_h = cr.h; + int buf_size = cr.w * cr.h * 4; + win->content = (uint32_t*)montauk::alloc(buf_size); + montauk::memset(win->content, 0xFF, buf_size); + + win->on_draw = nullptr; + win->on_mouse = nullptr; + win->on_key = nullptr; + win->on_close = nullptr; + win->on_poll = nullptr; + win->app_data = nullptr; + win->external = false; + win->ext_win_id = -1; + + // Unfocus previous window + if (ds->focused_window >= 0 && ds->focused_window < ds->window_count) { + ds->windows[ds->focused_window].focused = false; + } + ds->focused_window = idx; + ds->window_count++; + + return idx; +} + +void gui::desktop_close_window(DesktopState* ds, int idx) { + if (idx < 0 || idx >= ds->window_count) return; + + Window* win = &ds->windows[idx]; + + // For external windows, send a close event instead of freeing the buffer + if (win->external) { + Montauk::WinEvent ev; + montauk::memset(&ev, 0, sizeof(ev)); + ev.type = 3; // close + montauk::win_sendevent(win->ext_win_id, &ev); + } + + if (win->on_close) win->on_close(win); + + // Free content buffer (skip for external windows — shared memory) + if (win->content && !win->external) { + montauk::free(win->content); + win->content = nullptr; + } + + // Shift remaining windows down + for (int i = idx; i < ds->window_count - 1; i++) { + ds->windows[i] = ds->windows[i + 1]; + } + ds->window_count--; + + // Fix focused window index + if (ds->focused_window == idx) { + ds->focused_window = ds->window_count > 0 ? ds->window_count - 1 : -1; + } else if (ds->focused_window > idx) { + ds->focused_window--; + } + + if (ds->focused_window >= 0 && ds->focused_window < ds->window_count) { + ds->windows[ds->focused_window].focused = true; + } +} + +void gui::desktop_raise_window(DesktopState* ds, int idx) { + if (idx < 0 || idx >= ds->window_count) return; + if (idx == ds->window_count - 1) { + // Already on top, just focus + if (ds->focused_window >= 0 && ds->focused_window < ds->window_count) + ds->windows[ds->focused_window].focused = false; + ds->focused_window = idx; + ds->windows[idx].focused = true; + return; + } + + // Unfocus current + if (ds->focused_window >= 0 && ds->focused_window < ds->window_count) { + ds->windows[ds->focused_window].focused = false; + } + + // Move window to end (top of stack) + Window tmp = ds->windows[idx]; + for (int i = idx; i < ds->window_count - 1; i++) { + ds->windows[i] = ds->windows[i + 1]; + } + ds->windows[ds->window_count - 1] = tmp; + + ds->focused_window = ds->window_count - 1; + ds->windows[ds->focused_window].focused = true; +} + +void gui::desktop_draw_window(DesktopState* ds, int idx) { + if (idx < 0 || idx >= ds->window_count) return; + Window* win = &ds->windows[idx]; + if (win->state == WIN_MINIMIZED || win->state == WIN_CLOSED) return; + + Framebuffer& fb = ds->fb; + int x = win->frame.x; + int y = win->frame.y; + int w = win->frame.w; + int h = win->frame.h; + + // Draw shadow + if (ds->settings.show_shadows) + draw_shadow(fb, x, y, w, h, SHADOW_SIZE, colors::SHADOW); + + // Draw window body + fb.fill_rect(x, y, w, h, colors::WINDOW_BG); + + // Draw titlebar + Color tb_bg = win->focused ? colors::TITLEBAR_BG : Color::from_rgb(0xE8, 0xE8, 0xE8); + fb.fill_rect(x, y, w, TITLEBAR_HEIGHT, tb_bg); + + // Draw border + draw_rect(fb, x, y, w, h, colors::BORDER); + // Titlebar bottom separator + draw_hline(fb, x, y + TITLEBAR_HEIGHT - 1, w, colors::BORDER); + + // Draw window buttons (macOS style: close, minimize, maximize) + Rect close_r = win->close_btn_rect(); + Rect min_r = win->min_btn_rect(); + Rect max_r = win->max_btn_rect(); + + fill_circle(fb, close_r.x + BTN_RADIUS, close_r.y + BTN_RADIUS, BTN_RADIUS, colors::CLOSE_BTN); + fill_circle(fb, min_r.x + BTN_RADIUS, min_r.y + BTN_RADIUS, BTN_RADIUS, colors::MIN_BTN); + fill_circle(fb, max_r.x + BTN_RADIUS, max_r.y + BTN_RADIUS, BTN_RADIUS, colors::MAX_BTN); + + // Draw title text centered in titlebar (after buttons) + int title_x = x + 12 + 44 + BTN_RADIUS * 2 + 12; // after buttons + int title_y = y + (TITLEBAR_HEIGHT - system_font_height()) / 2; + int title_w = text_width(win->title); + // Center in remaining space + int remaining_w = w - (title_x - x) - 12; + if (remaining_w > title_w) { + title_x += (remaining_w - title_w) / 2; + } + draw_text(fb, title_x, title_y, win->title, colors::TEXT_COLOR); + + // Call app draw callback to render content (skip during resize — buffer is old size) + if (win->on_draw && !win->resizing) { + win->on_draw(win, fb); + } + + // Blit content buffer to framebuffer (clip to actual buffer size during resize) + Rect cr = win->content_rect(); + if (win->content) { + if (win->external && (cr.w != win->content_w || cr.h != win->content_h)) { + // Nearest-neighbor scale for external windows (fixed-size shared buffer) + int src_w = win->content_w; + int src_h = win->content_h; + int dst_w = cr.w; + int dst_h = cr.h; + uint32_t* buf = fb.buffer(); + int pitch = fb.pitch(); + for (int y = 0; y < dst_h; y++) { + int dy = cr.y + y; + if (dy < 0 || dy >= fb.height()) continue; + int sy = y * src_h / dst_h; + uint32_t* dst_row = (uint32_t*)((uint8_t*)buf + dy * pitch); + uint32_t* src_row = win->content + sy * src_w; + for (int x = 0; x < dst_w; x++) { + int dx = cr.x + x; + if (dx < 0 || dx >= fb.width()) continue; + dst_row[dx] = src_row[x * src_w / dst_w]; + } + } + } else { + int blit_w = cr.w < win->content_w ? cr.w : win->content_w; + int blit_h = cr.h < win->content_h ? cr.h : win->content_h; + fb.blit(cr.x, cr.y, blit_w, blit_h, win->content); + } + } +} + +gui::ResizeEdge hit_test_resize_edge(const gui::Rect& f, int mx, int my) { + using namespace gui; + int G = RESIZE_GRAB; + if (!f.contains(mx, my)) return RESIZE_NONE; + + bool near_left = mx < f.x + G; + bool near_right = mx >= f.x + f.w - G; + bool near_top = my < f.y + G; + bool near_bottom = my >= f.y + f.h - G; + + // Corners first + if (near_top && near_left) return RESIZE_TOP_LEFT; + if (near_top && near_right) return RESIZE_TOP_RIGHT; + if (near_bottom && near_left) return RESIZE_BOTTOM_LEFT; + if (near_bottom && near_right) return RESIZE_BOTTOM_RIGHT; + + // Edges + if (near_top) return RESIZE_TOP; + if (near_bottom) return RESIZE_BOTTOM; + if (near_left) return RESIZE_LEFT; + if (near_right) return RESIZE_RIGHT; + + return RESIZE_NONE; +} + +gui::CursorStyle cursor_for_edge(gui::ResizeEdge edge) { + using namespace gui; + switch (edge) { + case RESIZE_LEFT: case RESIZE_RIGHT: + return CURSOR_RESIZE_H; + case RESIZE_TOP: case RESIZE_BOTTOM: + return CURSOR_RESIZE_V; + case RESIZE_TOP_LEFT: case RESIZE_BOTTOM_RIGHT: + return CURSOR_RESIZE_NWSE; + case RESIZE_TOP_RIGHT: case RESIZE_BOTTOM_LEFT: + return CURSOR_RESIZE_NESW; + default: + return CURSOR_ARROW; + } +}