diff --git a/.gitignore b/.gitignore index ffe1dfe..6f09e53 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ programs/bin/ programs/obj/ programs/src/doom/obj/ programs/gui/icons/ +programs/src/desktop/obj/ diff --git a/kernel/src/Drivers/PS2/Mouse.cpp b/kernel/src/Drivers/PS2/Mouse.cpp index 2916e5a..419390b 100644 --- a/kernel/src/Drivers/PS2/Mouse.cpp +++ b/kernel/src/Drivers/PS2/Mouse.cpp @@ -161,7 +161,7 @@ namespace Drivers::PS2::Mouse { g_State.X += deltaX; g_State.Y += deltaY; g_State.Buttons = buttons; - g_State.ScrollDelta = scrollDelta; + g_State.ScrollDelta += scrollDelta; // Clamp to screen bounds if (g_State.X < 0) g_State.X = 0; @@ -175,6 +175,7 @@ namespace Drivers::PS2::Mouse { MouseState GetMouseState() { g_StateLock.Acquire(); MouseState state = g_State; + g_State.ScrollDelta = 0; // clear after read so deltas don't repeat g_StateLock.Release(); return state; } @@ -202,7 +203,7 @@ namespace Drivers::PS2::Mouse { g_State.X += (int32_t)deltaX; g_State.Y += (int32_t)deltaY; g_State.Buttons = buttons; - g_State.ScrollDelta = (int32_t)scroll; + g_State.ScrollDelta += (int32_t)scroll; // Clamp to screen bounds if (g_State.X < 0) g_State.X = 0; diff --git a/programs/include/gui/canvas.hpp b/programs/include/gui/canvas.hpp new file mode 100644 index 0000000..10c16fd --- /dev/null +++ b/programs/include/gui/canvas.hpp @@ -0,0 +1,199 @@ +/* + * canvas.hpp + * ZenithOS Canvas — drawing primitives for pixel buffer (uint32_t*) targets + * Mirrors Framebuffer API but operates directly on app content buffers. + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once +#include "gui/gui.hpp" +#include "gui/font.hpp" +#include "gui/svg.hpp" +#include "gui/window.hpp" + +namespace gui { + +struct Canvas { + uint32_t* pixels; + int w, h; + + // ---- Constructors ---- + + Canvas(uint32_t* px, int width, int height) + : pixels(px), w(width), h(height) {} + + Canvas(Window* win) + : pixels(win->content), w(win->content_w), h(win->content_h) {} + + // ---- Core drawing ---- + + void fill(Color c) { + uint32_t px = c.to_pixel(); + int total = w * h; + for (int i = 0; i < total; i++) pixels[i] = px; + } + + void put_pixel(int x, int y, Color c) { + if (x >= 0 && x < w && y >= 0 && y < h) + pixels[y * w + x] = c.to_pixel(); + } + + void fill_rect(int x, int y, int rw, int rh, Color c) { + uint32_t px = c.to_pixel(); + int x0 = gui_max(x, 0), y0 = gui_max(y, 0); + int x1 = gui_min(x + rw, w), y1 = gui_min(y + rh, h); + for (int dy = y0; dy < y1; dy++) + for (int dx = x0; dx < x1; dx++) + pixels[dy * w + dx] = px; + } + + void fill_rounded_rect(int x, int y, int rw, int rh, int radius, Color c) { + if (radius <= 0) { fill_rect(x, y, rw, rh, c); return; } + uint32_t px = c.to_pixel(); + for (int row = 0; row < rh; row++) { + int dy = y + row; + if (dy < 0 || dy >= h) continue; + for (int col = 0; col < rw; col++) { + int dx = x + col; + if (dx < 0 || dx >= w) continue; + bool in_corner = false; + int cx_off = 0, cy_off = 0; + if (col < radius && row < radius) { + cx_off = radius - col; cy_off = radius - row; in_corner = true; + } else if (col >= rw - radius && row < radius) { + cx_off = col - (rw - radius - 1); cy_off = radius - row; in_corner = true; + } else if (col < radius && row >= rh - radius) { + cx_off = radius - col; cy_off = row - (rh - radius - 1); in_corner = true; + } else if (col >= rw - radius && row >= rh - radius) { + cx_off = col - (rw - radius - 1); cy_off = row - (rh - radius - 1); in_corner = true; + } + if (in_corner && cx_off * cx_off + cy_off * cy_off > radius * radius) continue; + pixels[dy * w + dx] = px; + } + } + } + + void hline(int x, int y, int len, Color c) { + if (y < 0 || y >= h) return; + uint32_t px = c.to_pixel(); + int x0 = gui_max(x, 0), x1 = gui_min(x + len, w); + for (int dx = x0; dx < x1; dx++) + pixels[y * w + dx] = px; + } + + void vline(int x, int y, int len, Color c) { + if (x < 0 || x >= w) return; + uint32_t px = c.to_pixel(); + int y0 = gui_max(y, 0), y1 = gui_min(y + len, h); + for (int dy = y0; dy < y1; dy++) + pixels[dy * w + x] = px; + } + + void rect(int x, int y, int rw, int rh, Color c) { + hline(x, y, rw, c); + hline(x, y + rh - 1, rw, c); + vline(x, y, rh, c); + vline(x + rw - 1, y, rh, c); + } + + // ---- Text ---- + + void text(int x, int y, const char* str, Color c) { + uint32_t px = c.to_pixel(); + for (int i = 0; str[i] && x + (i + 1) * FONT_WIDTH <= w; i++) { + const uint8_t* glyph = &font_data[(unsigned char)str[i] * FONT_HEIGHT]; + int cx = x + i * FONT_WIDTH; + for (int fy = 0; fy < FONT_HEIGHT && y + fy < h; fy++) { + uint8_t bits = glyph[fy]; + for (int fx = 0; fx < FONT_WIDTH; fx++) { + if (bits & (0x80 >> fx)) { + int dx = cx + fx; + int dy = y + fy; + if (dx >= 0 && dx < w && dy >= 0) + pixels[dy * w + dx] = px; + } + } + } + } + } + + void text_2x(int x, int y, const char* str, Color c) { + uint32_t px = c.to_pixel(); + for (int i = 0; str[i] && x + (i + 1) * FONT_WIDTH * 2 <= w; i++) { + const uint8_t* glyph = &font_data[(unsigned char)str[i] * FONT_HEIGHT]; + int cx = x + i * FONT_WIDTH * 2; + for (int fy = 0; fy < FONT_HEIGHT; fy++) { + uint8_t bits = glyph[fy]; + for (int fx = 0; fx < FONT_WIDTH; fx++) { + if (bits & (0x80 >> fx)) { + int dx = cx + fx * 2; + int dy = y + fy * 2; + for (int sy = 0; sy < 2; sy++) + for (int sx = 0; sx < 2; sx++) { + int pdx = dx + sx; + int pdy = dy + sy; + if (pdx >= 0 && pdx < w && pdy >= 0 && pdy < h) + pixels[pdy * w + pdx] = px; + } + } + } + } + } + } + + // ---- Icons ---- + + void icon(int x, int y, const SvgIcon& ic) { + if (!ic.pixels) return; + for (int row = 0; row < ic.height; row++) { + int dy = y + row; + if (dy < 0 || dy >= h) continue; + for (int col = 0; col < ic.width; col++) { + int dx = x + col; + if (dx < 0 || dx >= w) continue; + uint32_t src = ic.pixels[row * ic.width + col]; + uint8_t sa = (src >> 24) & 0xFF; + if (sa == 0) continue; + if (sa == 255) { + pixels[dy * w + dx] = src; + } else { + uint32_t dst = pixels[dy * w + dx]; + uint8_t sr = (src >> 16) & 0xFF; + uint8_t sg = (src >> 8) & 0xFF; + uint8_t sb = src & 0xFF; + uint8_t dr = (dst >> 16) & 0xFF; + uint8_t dg = (dst >> 8) & 0xFF; + uint8_t db = dst & 0xFF; + uint32_t a = sa, inv_a = 255 - sa; + uint32_t rr = (a * sr + inv_a * dr + 128) / 255; + uint32_t gg = (a * sg + inv_a * dg + 128) / 255; + uint32_t bb = (a * sb + inv_a * db + 128) / 255; + pixels[dy * w + dx] = 0xFF000000 | (rr << 16) | (gg << 8) | bb; + } + } + } + } + + // ---- High-level helpers ---- + + void kv_line(int x, int* y, const char* line, Color c, int line_h = FONT_HEIGHT + 6) { + text(x, *y, line, c); + *y += line_h; + } + + void separator(int x_start, int x_end, int* y, Color c, int spacing = 8) { + hline(x_start, *y, x_end - x_start, c); + *y += spacing; + } + + void button(int x, int y, int bw, int bh, const char* label, + Color bg, Color fg, int radius = 4) { + fill_rounded_rect(x, y, bw, bh, radius, bg); + int tw = text_width(label); + int tx = x + (bw - tw) / 2; + int ty = y + (bh - FONT_HEIGHT) / 2; + text(tx, ty, label, fg); + } +}; + +} // namespace gui diff --git a/programs/include/gui/desktop.hpp b/programs/include/gui/desktop.hpp index d46d44b..51b5689 100644 --- a/programs/include/gui/desktop.hpp +++ b/programs/include/gui/desktop.hpp @@ -51,6 +51,12 @@ struct DesktopState { SvgIcon icon_file_lg; SvgIcon icon_exec_lg; + SvgIcon icon_settings; + SvgIcon icon_reboot; + + bool ctx_menu_open; + int ctx_menu_x, ctx_menu_y; + bool net_popup_open; Zenith::NetCfg cached_net_cfg; uint64_t net_cfg_last_poll; diff --git a/programs/include/gui/draw.hpp b/programs/include/gui/draw.hpp index 1362ed6..b9d7024 100644 --- a/programs/include/gui/draw.hpp +++ b/programs/include/gui/draw.hpp @@ -217,20 +217,175 @@ static constexpr uint16_t cursor_fill[16] = { 0x0000, // row 15: no fill }; +// Resize cursor: horizontal double arrow (left-right) +static constexpr uint16_t cursor_h_resize_outline[16] = { + 0x0000, 0x0000, 0x0000, 0x0000, + 0x0820, // 0000100000100000 + 0x1830, // 0001100000110000 + 0x3FF8, // 0011111111111000 + 0x7FFC, // 0111111111111100 + 0x3FF8, // 0011111111111000 + 0x1830, // 0001100000110000 + 0x0820, // 0000100000100000 + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, +}; +static constexpr uint16_t cursor_h_resize_fill[16] = { + 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, + 0x0000, + 0x1FF0, // 0001111111110000 + 0x3FF8, // 0011111111111000 + 0x1FF0, // 0001111111110000 + 0x0000, + 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, +}; + +// Resize cursor: vertical double arrow (up-down) +static constexpr uint16_t cursor_v_resize_outline[16] = { + 0x0000, 0x0000, + 0x0200, // 0000001000000000 + 0x0700, // 0000011100000000 + 0x0F80, // 0000111110000000 + 0x0200, // 0000001000000000 + 0x0200, // 0000001000000000 + 0x0200, // 0000001000000000 + 0x0200, // 0000001000000000 + 0x0200, // 0000001000000000 + 0x0200, // 0000001000000000 + 0x0F80, // 0000111110000000 + 0x0700, // 0000011100000000 + 0x0200, // 0000001000000000 + 0x0000, 0x0000, +}; +static constexpr uint16_t cursor_v_resize_fill[16] = { + 0x0000, 0x0000, 0x0000, + 0x0200, // 0000001000000000 + 0x0700, // 0000011100000000 + 0x0200, // 0000001000000000 + 0x0200, // 0000001000000000 + 0x0200, // 0000001000000000 + 0x0200, // 0000001000000000 + 0x0200, // 0000001000000000 + 0x0200, // 0000001000000000 + 0x0700, // 0000011100000000 + 0x0200, // 0000001000000000 + 0x0000, 0x0000, 0x0000, +}; + +// Resize cursor: diagonal NW-SE double arrow +static constexpr uint16_t cursor_nwse_resize_outline[16] = { + 0x0000, 0x0000, + 0x7C00, // 0111110000000000 + 0x6000, // 0110000000000000 + 0x5000, // 0101000000000000 + 0x4800, // 0100100000000000 + 0x2400, // 0010010000000000 + 0x1200, // 0001001000000000 + 0x0900, // 0000100100000000 + 0x0480, // 0000010010000000 + 0x0240, // 0000001001000000 + 0x0140, // 0000000101000000 + 0x00C0, // 0000000011000000 + 0x07C0, // 0000011111000000 + 0x0000, 0x0000, +}; +static constexpr uint16_t cursor_nwse_resize_fill[16] = { + 0x0000, 0x0000, 0x0000, + 0x1C00, // 0001110000000000 + 0x2800, // 0010100000000000 + 0x0400, // 0000010000000000 + 0x0200, // 0000001000000000 + 0x0100, // 0000000100000000 + 0x0080, // 0000000010000000 + 0x0040, // 0000000001000000 + 0x0280, // 0000001010000000 + 0x0380, // 0000001110000000 + 0x0000, 0x0000, 0x0000, 0x0000, +}; + +// Resize cursor: diagonal NE-SW double arrow +static constexpr uint16_t cursor_nesw_resize_outline[16] = { + 0x0000, 0x0000, + 0x07C0, // 0000011111000000 + 0x00C0, // 0000000011000000 + 0x0140, // 0000000101000000 + 0x0240, // 0000001001000000 + 0x0480, // 0000010010000000 + 0x0900, // 0000100100000000 + 0x1200, // 0001001000000000 + 0x2400, // 0010010000000000 + 0x4800, // 0100100000000000 + 0x5000, // 0101000000000000 + 0x6000, // 0110000000000000 + 0x7C00, // 0111110000000000 + 0x0000, 0x0000, +}; +static constexpr uint16_t cursor_nesw_resize_fill[16] = { + 0x0000, 0x0000, 0x0000, + 0x0380, // 0000001110000000 + 0x0280, // 0000001010000000 + 0x0040, // 0000000001000000 + 0x0080, // 0000000010000000 + 0x0100, // 0000000100000000 + 0x0200, // 0000001000000000 + 0x0400, // 0000010000000000 + 0x2800, // 0010100000000000 + 0x1C00, // 0001110000000000 + 0x0000, 0x0000, 0x0000, 0x0000, +}; + +enum CursorStyle { + CURSOR_ARROW = 0, + CURSOR_RESIZE_H, // left-right + CURSOR_RESIZE_V, // up-down + CURSOR_RESIZE_NWSE, // diagonal NW-SE + CURSOR_RESIZE_NESW, // diagonal NE-SW +}; + // Draw the mouse cursor at (x, y) -inline void draw_cursor(Framebuffer& fb, int x, int y) { +inline void draw_cursor(Framebuffer& fb, int x, int y, CursorStyle style = CURSOR_ARROW) { + const uint16_t* outline_data = cursor_outline; + const uint16_t* fill_data = cursor_fill; + int ox = 0, oy = 0; // hotspot offset for centered cursors + + switch (style) { + case CURSOR_RESIZE_H: + outline_data = cursor_h_resize_outline; + fill_data = cursor_h_resize_fill; + ox = -8; oy = -8; + break; + case CURSOR_RESIZE_V: + outline_data = cursor_v_resize_outline; + fill_data = cursor_v_resize_fill; + ox = -8; oy = -8; + break; + case CURSOR_RESIZE_NWSE: + outline_data = cursor_nwse_resize_outline; + fill_data = cursor_nwse_resize_fill; + ox = -8; oy = -8; + break; + case CURSOR_RESIZE_NESW: + outline_data = cursor_nesw_resize_outline; + fill_data = cursor_nesw_resize_fill; + ox = -8; oy = -8; + break; + default: + break; + } + Color black = colors::BLACK; Color white = colors::WHITE; for (int row = 0; row < 16; row++) { - uint16_t outline = cursor_outline[row]; - uint16_t fill = cursor_fill[row]; + uint16_t outline = outline_data[row]; + uint16_t fill = fill_data[row]; for (int col = 0; col < 16; col++) { uint16_t mask = (uint16_t)(0x8000 >> col); if (outline & mask) { - fb.put_pixel(x + col, y + row, black); + fb.put_pixel(x + ox + col, y + oy + row, black); } else if (fill & mask) { - fb.put_pixel(x + col, y + row, white); + fb.put_pixel(x + ox + col, y + oy + row, white); } } } diff --git a/programs/include/gui/window.hpp b/programs/include/gui/window.hpp index 1f58d69..904e78b 100644 --- a/programs/include/gui/window.hpp +++ b/programs/include/gui/window.hpp @@ -14,11 +14,20 @@ namespace gui { enum WindowState { WIN_NORMAL, WIN_MINIMIZED, WIN_MAXIMIZED, WIN_CLOSED }; +enum ResizeEdge { + RESIZE_NONE = 0, + RESIZE_LEFT, RESIZE_RIGHT, RESIZE_TOP, RESIZE_BOTTOM, + RESIZE_TOP_LEFT, RESIZE_TOP_RIGHT, RESIZE_BOTTOM_LEFT, RESIZE_BOTTOM_RIGHT +}; + static constexpr int TITLEBAR_HEIGHT = 30; static constexpr int BORDER_WIDTH = 1; static constexpr int SHADOW_SIZE = 3; static constexpr int BTN_RADIUS = 6; static constexpr int MAX_TITLE_LEN = 64; +static constexpr int RESIZE_GRAB = 6; +static constexpr int MIN_WINDOW_W = 120; +static constexpr int MIN_WINDOW_H = 80; struct Window; using WindowDrawCallback = void (*)(Window* win, Framebuffer& fb); @@ -42,6 +51,9 @@ struct Window { int drag_offset_x, drag_offset_y; bool resizing; + ResizeEdge resize_edge; + Rect resize_start_frame; + int resize_start_mx, resize_start_my; Rect saved_frame; diff --git a/programs/src/desktop/Makefile b/programs/src/desktop/Makefile index 855acf5..4f43cd3 100644 --- a/programs/src/desktop/Makefile +++ b/programs/src/desktop/Makefile @@ -63,7 +63,7 @@ LDFLAGS := \ # ---- Source files ---- -SRCS := main.cpp font_data.cpp app_terminal.cpp app_filemanager.cpp app_sysinfo.cpp app_calculator.cpp app_texteditor.cpp app_klog.cpp app_wiki.cpp +SRCS := main.cpp font_data.cpp app_terminal.cpp app_filemanager.cpp app_sysinfo.cpp app_calculator.cpp app_texteditor.cpp app_klog.cpp app_wiki.cpp app_settings.cpp OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) # ---- Target ---- diff --git a/programs/src/desktop/app_calculator.cpp b/programs/src/desktop/app_calculator.cpp index ab1fe34..6ddfad5 100644 --- a/programs/src/desktop/app_calculator.cpp +++ b/programs/src/desktop/app_calculator.cpp @@ -222,29 +222,21 @@ static void calculator_on_draw(Window* win, Framebuffer& fb) { CalcState* cs = (CalcState*)win->app_data; if (!cs) return; - Rect cr = win->content_rect(); - int cw = cr.w; - int ch = cr.h; - uint32_t* pixels = win->content; + Canvas c(win); // Background - uint32_t bg_px = Color::from_rgb(0xF0, 0xF0, 0xF0).to_pixel(); - for (int i = 0; i < cw * ch; i++) pixels[i] = bg_px; + c.fill(Color::from_rgb(0xF0, 0xF0, 0xF0)); // Display area - uint32_t disp_px = Color::from_rgb(0x2D, 0x2D, 0x2D).to_pixel(); - for (int y = 0; y < CALC_DISPLAY_H && y < ch; y++) - for (int x = 0; x < cw; x++) - pixels[y * cw + x] = disp_px; + c.fill_rect(0, 0, c.w, CALC_DISPLAY_H, Color::from_rgb(0x2D, 0x2D, 0x2D)); // Display text (right-aligned, 2x scale) - uint32_t disp_text = colors::WHITE.to_pixel(); int text_len = zenith::slen(cs->display_str); int text_w = text_len * FONT_WIDTH * 2; - int tx = cw - text_w - 12; + int tx = c.w - text_w - 12; int ty = (CALC_DISPLAY_H - FONT_HEIGHT * 2) / 2; if (tx < 4) tx = 4; - draw_text_to_pixels_2x(pixels, cw, ch, tx, ty, cs->display_str, disp_text); + c.text_2x(tx, ty, cs->display_str, colors::WHITE); // Button grid int grid_y = CALC_DISPLAY_H + CALC_BTN_PAD; @@ -264,30 +256,25 @@ static void calculator_on_draw(Window* win, Framebuffer& fb) { } // Button color - uint32_t btn_px; + Color btn_color; if (col == 3) { - // Operator column: accent blue - btn_px = colors::ACCENT.to_pixel(); + btn_color = colors::ACCENT; } else if (row == 0) { - // Function row: gray - btn_px = Color::from_rgb(0xD0, 0xD0, 0xD0).to_pixel(); + btn_color = Color::from_rgb(0xD0, 0xD0, 0xD0); } else { - // Digit buttons: light gray - btn_px = Color::from_rgb(0xE8, 0xE8, 0xE8).to_pixel(); + btn_color = Color::from_rgb(0xE8, 0xE8, 0xE8); } // Draw button - for (int dy = 0; dy < CALC_BTN_H && by + dy < ch; dy++) - for (int dx = 0; dx < bw && bx + dx < cw; dx++) - pixels[(by + dy) * cw + (bx + dx)] = btn_px; + c.fill_rect(bx, by, bw, CALC_BTN_H, btn_color); // Button text const char* label = calc_labels[row][col]; - uint32_t label_px = (col == 3) ? colors::WHITE.to_pixel() : colors::TEXT_COLOR.to_pixel(); + Color label_color = (col == 3) ? colors::WHITE : colors::TEXT_COLOR; int label_w = zenith::slen(label) * FONT_WIDTH; int lx = bx + (bw - label_w) / 2; int ly = by + (CALC_BTN_H - FONT_HEIGHT) / 2; - draw_text_to_pixels(pixels, cw, ch, lx, ly, label, label_px); + c.text(lx, ly, label, label_color); } } } diff --git a/programs/src/desktop/app_filemanager.cpp b/programs/src/desktop/app_filemanager.cpp index 28512a8..2c2220e 100644 --- a/programs/src/desktop/app_filemanager.cpp +++ b/programs/src/desktop/app_filemanager.cpp @@ -248,24 +248,16 @@ static void filemanager_on_draw(Window* win, Framebuffer& fb) { FileManagerState* fm = (FileManagerState*)win->app_data; if (!fm) return; - Rect cr = win->content_rect(); - int cw = cr.w; - int ch = cr.h; - uint32_t* pixels = win->content; - - uint32_t bg_px = colors::WINDOW_BG.to_pixel(); - for (int i = 0; i < cw * ch; i++) pixels[i] = bg_px; - - uint32_t text_px = colors::TEXT_COLOR.to_pixel(); - uint32_t sep_px = colors::BORDER.to_pixel(); - uint32_t toolbar_px = Color::from_rgb(0xF5, 0xF5, 0xF5).to_pixel(); + Canvas c(win); + c.fill(colors::WINDOW_BG); + Color toolbar_color = Color::from_rgb(0xF5, 0xF5, 0xF5); + Color btn_bg = Color::from_rgb(0xE8, 0xE8, 0xE8); + Color dim = Color::from_rgb(0x88, 0x88, 0x88); DesktopState* ds = fm->desktop; // ---- Toolbar (32px) ---- - for (int y = 0; y < FM_TOOLBAR_H && y < ch; y++) - for (int x = 0; x < cw; x++) - pixels[y * cw + x] = toolbar_px; + c.fill_rect(0, 0, c.w, FM_TOOLBAR_H, toolbar_color); // Toolbar buttons: Back, Forward, Up, Home struct ToolBtn { int x; SvgIcon* icon; }; @@ -279,79 +271,53 @@ static void filemanager_on_draw(Window* win, Framebuffer& fb) { for (int i = 0; i < 4; i++) { int bx = btns[i].x; int by = 4; - // Button background - uint32_t btn_px = Color::from_rgb(0xE8, 0xE8, 0xE8).to_pixel(); - for (int dy = 0; dy < 24 && by + dy < ch; dy++) - for (int dx = 0; dx < 24 && bx + dx < cw; dx++) - pixels[(by + dy) * cw + (bx + dx)] = btn_px; + c.fill_rect(bx, by, 24, 24, btn_bg); if (btns[i].icon) { int ix = bx + (24 - btns[i].icon->width) / 2; int iy = by + (24 - btns[i].icon->height) / 2; - blit_icon_to_pixels(pixels, cw, ch, ix, iy, *btns[i].icon); + c.icon(ix, iy, *btns[i].icon); } } // Toggle view button (5th toolbar button) { int bx = 120, by = 4; - uint32_t btn_px = Color::from_rgb(0xE8, 0xE8, 0xE8).to_pixel(); - for (int dy = 0; dy < 24 && by + dy < ch; dy++) - for (int dx = 0; dx < 24 && bx + dx < cw; dx++) - pixels[(by + dy) * cw + (bx + dx)] = btn_px; - uint32_t glyph_px = colors::TEXT_COLOR.to_pixel(); + c.fill_rect(bx, by, 24, 24, btn_bg); if (fm->grid_view) { // Draw 4 small squares to indicate grid mode for (int r = 0; r < 2; r++) - for (int c = 0; c < 2; c++) - for (int dy = 0; dy < 6; dy++) - for (int dx = 0; dx < 6; dx++) - if (by + 5 + r * 8 + dy < ch && bx + 5 + c * 8 + dx < cw) - pixels[(by + 5 + r * 8 + dy) * cw + (bx + 5 + c * 8 + dx)] = glyph_px; + for (int cc = 0; cc < 2; cc++) + c.fill_rect(bx + 5 + cc * 8, by + 5 + r * 8, 6, 6, colors::TEXT_COLOR); } else { // Draw 3 horizontal lines to indicate list mode for (int r = 0; r < 3; r++) - for (int dx = 0; dx < 14; dx++) - for (int dy = 0; dy < 2; dy++) - if (by + 5 + r * 5 + dy < ch && bx + 5 + dx < cw) - pixels[(by + 5 + r * 5 + dy) * cw + (bx + 5 + dx)] = glyph_px; + c.fill_rect(bx + 5, by + 5 + r * 5, 14, 2, colors::TEXT_COLOR); } } // Toolbar separator - int sep_y = FM_TOOLBAR_H - 1; - if (sep_y < ch) { - for (int x = 0; x < cw; x++) - pixels[sep_y * cw + x] = sep_px; - } + c.hline(0, FM_TOOLBAR_H - 1, c.w, colors::BORDER); // ---- Path bar ---- int pathbar_y = FM_TOOLBAR_H; - uint32_t pathbar_px = Color::from_rgb(0xF0, 0xF0, 0xF0).to_pixel(); - for (int y = pathbar_y; y < pathbar_y + FM_PATHBAR_H && y < ch; y++) - for (int x = 0; x < cw; x++) - pixels[y * cw + x] = pathbar_px; - - draw_text_to_pixels(pixels, cw, ch, 8, pathbar_y + 4, fm->current_path, text_px); + c.fill_rect(0, pathbar_y, c.w, FM_PATHBAR_H, Color::from_rgb(0xF0, 0xF0, 0xF0)); + c.text(8, pathbar_y + 4, fm->current_path, colors::TEXT_COLOR); // Path bar separator - sep_y = pathbar_y + FM_PATHBAR_H - 1; - if (sep_y < ch) { - for (int x = 0; x < cw; x++) - pixels[sep_y * cw + x] = sep_px; - } + c.hline(0, pathbar_y + FM_PATHBAR_H - 1, c.w, colors::BORDER); if (fm->grid_view) { // ---- Grid View ---- int list_y = FM_TOOLBAR_H + FM_PATHBAR_H; - int list_h = ch - list_y; - int cols = (cw - FM_SCROLLBAR_W) / FM_GRID_CELL_W; + int list_h = c.h - list_y; + int cols = (c.w - FM_SCROLLBAR_W) / FM_GRID_CELL_W; if (cols < 1) cols = 1; int rows = (fm->entry_count + cols - 1) / cols; int content_h = rows * FM_GRID_CELL_H; // Update scrollbar - fm->scrollbar.bounds = {cw - FM_SCROLLBAR_W, list_y, FM_SCROLLBAR_W, list_h}; + fm->scrollbar.bounds = {c.w - FM_SCROLLBAR_W, list_y, FM_SCROLLBAR_W, list_h}; fm->scrollbar.content_height = content_h; fm->scrollbar.view_height = list_h; @@ -362,32 +328,34 @@ static void filemanager_on_draw(Window* win, Framebuffer& fb) { int cell_y = list_y + row * FM_GRID_CELL_H - fm->scrollbar.scroll_offset; // Skip if entirely off-screen - if (cell_y + FM_GRID_CELL_H <= list_y || cell_y >= ch) continue; + if (cell_y + FM_GRID_CELL_H <= list_y || cell_y >= c.h) continue; // Selection highlight if (i == fm->selected) { - uint32_t sel_px = colors::MENU_HOVER.to_pixel(); - for (int y = gui_max(cell_y, list_y); y < gui_min(cell_y + FM_GRID_CELL_H, ch); y++) - for (int x = cell_x; x < gui_min(cell_x + FM_GRID_CELL_W, cw - FM_SCROLLBAR_W); x++) - pixels[y * cw + x] = sel_px; + int sy = gui_max(cell_y, list_y); + int sh = gui_min(cell_y + FM_GRID_CELL_H, c.h) - sy; + int sw = gui_min(FM_GRID_CELL_W, c.w - FM_SCROLLBAR_W - cell_x); + if (sh > 0 && sw > 0) + c.fill_rect(cell_x, sy, sw, sh, colors::MENU_HOVER); } // Large icon centered horizontally int icon_x = cell_x + (FM_GRID_CELL_W - FM_GRID_ICON) / 2; int icon_y = cell_y + FM_GRID_PAD; if (ds && fm->entry_types[i] == 1 && ds->icon_folder_lg.pixels) { - blit_icon_to_pixels(pixels, cw, ch, icon_x, icon_y, ds->icon_folder_lg); + c.icon(icon_x, icon_y, ds->icon_folder_lg); } else if (ds && fm->entry_types[i] == 2 && ds->icon_exec_lg.pixels) { - blit_icon_to_pixels(pixels, cw, ch, icon_x, icon_y, ds->icon_exec_lg); + c.icon(icon_x, icon_y, ds->icon_exec_lg); } else if (ds && ds->icon_file_lg.pixels) { - blit_icon_to_pixels(pixels, cw, ch, icon_x, icon_y, ds->icon_file_lg); + c.icon(icon_x, icon_y, ds->icon_file_lg); } else { - uint32_t icon_px = fm->is_dir[i] - ? Color::from_rgb(0xFF, 0xBD, 0x2E).to_pixel() - : Color::from_rgb(0x90, 0x90, 0x90).to_pixel(); - for (int dy = 0; dy < FM_GRID_ICON && icon_y + dy < ch && icon_y + dy >= list_y; dy++) - for (int dx = 0; dx < FM_GRID_ICON && icon_x + dx < cw; dx++) - pixels[(icon_y + dy) * cw + (icon_x + dx)] = icon_px; + Color icon_c = fm->is_dir[i] + ? Color::from_rgb(0xFF, 0xBD, 0x2E) + : Color::from_rgb(0x90, 0x90, 0x90); + int iy_clip = gui_max(icon_y, list_y); + int ih_clip = FM_GRID_ICON - (iy_clip - icon_y); + if (ih_clip > 0) + c.fill_rect(icon_x, iy_clip, FM_GRID_ICON, ih_clip, icon_c); } // Filename centered below icon, truncated if needed @@ -405,52 +373,42 @@ static void filemanager_on_draw(Window* win, Framebuffer& fb) { int tx = cell_x + (FM_GRID_CELL_W - tw) / 2; if (tx < cell_x) tx = cell_x; int ty = icon_y + FM_GRID_ICON + 2; - if (ty >= list_y && ty + FONT_HEIGHT <= ch) - draw_text_to_pixels(pixels, cw, ch, tx, ty, label, text_px); + if (ty >= list_y && ty + FONT_HEIGHT <= c.h) + c.text(tx, ty, label, colors::TEXT_COLOR); } } else { // ---- List View ---- // ---- Column headers ---- int header_y = FM_TOOLBAR_H + FM_PATHBAR_H; - uint32_t header_px = Color::from_rgb(0xF8, 0xF8, 0xF8).to_pixel(); - for (int y = header_y; y < header_y + FM_HEADER_H && y < ch; y++) - for (int x = 0; x < cw; x++) - pixels[y * cw + x] = header_px; + c.fill_rect(0, header_y, c.w, FM_HEADER_H, Color::from_rgb(0xF8, 0xF8, 0xF8)); - uint32_t dim_px = Color::from_rgb(0x88, 0x88, 0x88).to_pixel(); int name_col_x = 8; - int size_col_x = cw - FM_SCROLLBAR_W - 120; - int type_col_x = cw - FM_SCROLLBAR_W - 60; + int size_col_x = c.w - FM_SCROLLBAR_W - 120; + int type_col_x = c.w - FM_SCROLLBAR_W - 60; - draw_text_to_pixels(pixels, cw, ch, name_col_x, header_y + 2, "Name", dim_px); + c.text(name_col_x, header_y + 2, "Name", dim); if (size_col_x > 100) - draw_text_to_pixels(pixels, cw, ch, size_col_x, header_y + 2, "Size", dim_px); + c.text(size_col_x, header_y + 2, "Size", dim); if (type_col_x > 160) - draw_text_to_pixels(pixels, cw, ch, type_col_x, header_y + 2, "Type", dim_px); + c.text(type_col_x, header_y + 2, "Type", dim); // Header separator - sep_y = header_y + FM_HEADER_H - 1; - if (sep_y < ch) { - for (int x = 0; x < cw; x++) - pixels[sep_y * cw + x] = sep_px; - } + c.hline(0, header_y + FM_HEADER_H - 1, c.w, colors::BORDER); // Column separator lines if (size_col_x > 100) { - for (int y = header_y; y < ch; y++) - if (size_col_x - 4 >= 0 && size_col_x - 4 < cw) - pixels[y * cw + (size_col_x - 4)] = sep_px; + c.vline(size_col_x - 4, header_y, c.h - header_y, colors::BORDER); } // ---- File entries ---- int list_y = header_y + FM_HEADER_H; - int list_h = ch - list_y; + int list_h = c.h - list_y; int visible_items = list_h / FM_ITEM_H; int content_h = fm->entry_count * FM_ITEM_H; // Update scrollbar - fm->scrollbar.bounds = {cw - FM_SCROLLBAR_W, list_y, FM_SCROLLBAR_W, list_h}; + fm->scrollbar.bounds = {c.w - FM_SCROLLBAR_W, list_y, FM_SCROLLBAR_W, list_h}; fm->scrollbar.content_height = content_h; fm->scrollbar.view_height = list_h; @@ -458,78 +416,73 @@ static void filemanager_on_draw(Window* win, Framebuffer& fb) { for (int i = scroll_items; i < fm->entry_count && (i - scroll_items) < visible_items + 1; i++) { int iy = list_y + (i - scroll_items) * FM_ITEM_H - (fm->scrollbar.scroll_offset % FM_ITEM_H); - if (iy + FM_ITEM_H <= list_y || iy >= ch) continue; + if (iy + FM_ITEM_H <= list_y || iy >= c.h) continue; // Highlight selected if (i == fm->selected) { - uint32_t sel_px = colors::MENU_HOVER.to_pixel(); - for (int y = gui_max(iy, list_y); y < gui_min(iy + FM_ITEM_H, ch); y++) - for (int x = 0; x < cw - FM_SCROLLBAR_W; x++) - pixels[y * cw + x] = sel_px; + int sy = gui_max(iy, list_y); + int sh = gui_min(iy + FM_ITEM_H, c.h) - sy; + if (sh > 0) + c.fill_rect(0, sy, c.w - FM_SCROLLBAR_W, sh, colors::MENU_HOVER); } // Icon - int icon_x = 8; - int icon_y = iy + (FM_ITEM_H - 16) / 2; + int ico_x = 8; + int ico_y = iy + (FM_ITEM_H - 16) / 2; if (ds && fm->entry_types[i] == 1 && ds->icon_folder.pixels) { - blit_icon_to_pixels(pixels, cw, ch, icon_x, icon_y, ds->icon_folder); + c.icon(ico_x, ico_y, ds->icon_folder); } else if (ds && fm->entry_types[i] == 2 && ds->icon_exec.pixels) { - blit_icon_to_pixels(pixels, cw, ch, icon_x, icon_y, ds->icon_exec); + c.icon(ico_x, ico_y, ds->icon_exec); } else if (ds && ds->icon_file.pixels) { - blit_icon_to_pixels(pixels, cw, ch, icon_x, icon_y, ds->icon_file); + c.icon(ico_x, ico_y, ds->icon_file); } else { - uint32_t icon_px = fm->is_dir[i] - ? Color::from_rgb(0xFF, 0xBD, 0x2E).to_pixel() - : Color::from_rgb(0x90, 0x90, 0x90).to_pixel(); - for (int dy = 0; dy < 16 && icon_y + dy < ch && icon_y + dy >= list_y; dy++) - for (int dx = 0; dx < 16 && icon_x + dx < cw; dx++) - pixels[(icon_y + dy) * cw + (icon_x + dx)] = icon_px; + Color icon_c = fm->is_dir[i] + ? Color::from_rgb(0xFF, 0xBD, 0x2E) + : Color::from_rgb(0x90, 0x90, 0x90); + int iy_clip = gui_max(ico_y, list_y); + int ih_clip = 16 - (iy_clip - ico_y); + if (ih_clip > 0) + c.fill_rect(ico_x, iy_clip, 16, ih_clip, icon_c); } // Name int tx = 30; int ty = iy + (FM_ITEM_H - FONT_HEIGHT) / 2; - if (ty >= list_y && ty + FONT_HEIGHT <= ch) - draw_text_to_pixels(pixels, cw, ch, tx, ty, fm->entry_names[i], text_px); + if (ty >= list_y && ty + FONT_HEIGHT <= c.h) + c.text(tx, ty, fm->entry_names[i], colors::TEXT_COLOR); // Size - if (size_col_x > 100 && !fm->is_dir[i] && ty >= list_y && ty + FONT_HEIGHT <= ch) { + if (size_col_x > 100 && !fm->is_dir[i] && ty >= list_y && ty + FONT_HEIGHT <= c.h) { char size_str[16]; format_size(size_str, fm->entry_sizes[i]); - draw_text_to_pixels(pixels, cw, ch, size_col_x, ty, size_str, dim_px); + c.text(size_col_x, ty, size_str, dim); } // Type - if (type_col_x > 160 && ty >= list_y && ty + FONT_HEIGHT <= ch) { + if (type_col_x > 160 && ty >= list_y && ty + FONT_HEIGHT <= c.h) { const char* type_str = "File"; if (fm->entry_types[i] == 1) type_str = "Dir"; else if (fm->entry_types[i] == 2) type_str = "Exec"; - draw_text_to_pixels(pixels, cw, ch, type_col_x, ty, type_str, dim_px); + c.text(type_col_x, ty, type_str, dim); } } } // ---- Scrollbar ---- - // Draw scrollbar directly to pixels if (fm->scrollbar.content_height > fm->scrollbar.view_height) { - uint32_t sb_bg = colors::SCROLLBAR_BG.to_pixel(); - uint32_t sb_fg = (fm->scrollbar.hovered || fm->scrollbar.dragging) - ? fm->scrollbar.hover_fg.to_pixel() : fm->scrollbar.fg.to_pixel(); + Color sb_fg_color = (fm->scrollbar.hovered || fm->scrollbar.dragging) + ? fm->scrollbar.hover_fg : fm->scrollbar.fg; int sbx = fm->scrollbar.bounds.x; int sby = fm->scrollbar.bounds.y; int sbw = fm->scrollbar.bounds.w; int sbh = fm->scrollbar.bounds.h; - for (int y = sby; y < sby + sbh && y < ch; y++) - for (int x = sbx; x < sbx + sbw && x < cw; x++) - pixels[y * cw + x] = sb_bg; + c.fill_rect(sbx, sby, sbw, sbh, colors::SCROLLBAR_BG); int th = fm->scrollbar.thumb_height(); int tty = fm->scrollbar.thumb_y(); - for (int y = tty; y < tty + th && y < sby + sbh && y < ch; y++) - for (int x = sbx + 1; x < sbx + sbw - 1 && x < cw; x++) - pixels[y * cw + x] = sb_fg; + c.fill_rect(sbx + 1, tty, sbw - 2, th, sb_fg_color); } } diff --git a/programs/src/desktop/app_settings.cpp b/programs/src/desktop/app_settings.cpp new file mode 100644 index 0000000..3f9cec9 --- /dev/null +++ b/programs/src/desktop/app_settings.cpp @@ -0,0 +1,88 @@ +/* + * app_settings.cpp + * ZenithOS Desktop - About application + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "apps_common.hpp" + +// ============================================================================ +// About state and callbacks +// ============================================================================ + +struct AboutState { + Zenith::SysInfo sys_info; + uint64_t uptime_ms; +}; + +static void about_on_draw(Window* win, Framebuffer& fb) { + AboutState* st = (AboutState*)win->app_data; + if (!st) return; + + st->uptime_ms = zenith::get_milliseconds(); + + Canvas c(win); + c.fill(colors::WINDOW_BG); + + Color dim = Color::from_rgb(0x88, 0x88, 0x88); + int x = 16; + int y = 20; + char line[128]; + int line_h = FONT_HEIGHT + 6; + + // OS name in 2x size + c.text_2x(x, y, st->sys_info.osName, colors::ACCENT); + y += FONT_HEIGHT * 2 + 8; + + snprintf(line, sizeof(line), "Version %s", st->sys_info.osVersion); + c.text(x, y, line, colors::TEXT_COLOR); + y += line_h + 8; + + c.hline(x, y, c.w - 2 * x, colors::BORDER); + y += 12; + + snprintf(line, sizeof(line), "API version: %d", (int)st->sys_info.apiVersion); + c.kv_line(x, &y, line, colors::TEXT_COLOR, line_h); + + int up_sec = (int)(st->uptime_ms / 1000); + int up_min = up_sec / 60; + int up_hr = up_min / 60; + snprintf(line, sizeof(line), "Uptime: %d:%02d:%02d", up_hr, up_min % 60, up_sec % 60); + c.kv_line(x, &y, line, colors::TEXT_COLOR, line_h); + + snprintf(line, sizeof(line), "Build: %s %s", __DATE__, __TIME__); + c.text(x, y, line, colors::TEXT_COLOR); + y += line_h + 16; + + c.hline(x, y, c.w - 2 * x, colors::BORDER); + y += 12; + + c.kv_line(x, &y, "A hobby operating system built from scratch.", dim, line_h); + c.text(x, y, "Copyright (c) 2026 Daniel Hammer", dim); +} + +static void about_on_close(Window* win) { + if (win->app_data) { + zenith::mfree(win->app_data); + win->app_data = nullptr; + } +} + +// ============================================================================ +// About launcher +// ============================================================================ + +void open_settings(DesktopState* ds) { + int idx = desktop_create_window(ds, "About", 280, 150, 380, 280); + if (idx < 0) return; + + Window* win = &ds->windows[idx]; + AboutState* st = (AboutState*)zenith::malloc(sizeof(AboutState)); + zenith::memset(st, 0, sizeof(AboutState)); + zenith::get_info(&st->sys_info); + st->uptime_ms = zenith::get_milliseconds(); + + win->app_data = st; + win->on_draw = about_on_draw; + win->on_close = about_on_close; +} diff --git a/programs/src/desktop/app_sysinfo.cpp b/programs/src/desktop/app_sysinfo.cpp index d817864..ed04fdb 100644 --- a/programs/src/desktop/app_sysinfo.cpp +++ b/programs/src/desktop/app_sysinfo.cpp @@ -20,52 +20,38 @@ static void sysinfo_on_draw(Window* win, Framebuffer& fb) { SysInfoState* si = (SysInfoState*)win->app_data; if (!si) return; - // Refresh uptime si->uptime_ms = zenith::get_milliseconds(); - Rect cr = win->content_rect(); - int cw = cr.w; - int ch = cr.h; - uint32_t* pixels = win->content; + Canvas c(win); + c.fill(colors::WINDOW_BG); - // Fill background - uint32_t bg_px = colors::WINDOW_BG.to_pixel(); - for (int i = 0; i < cw * ch; i++) pixels[i] = bg_px; - - uint32_t text_px = colors::TEXT_COLOR.to_pixel(); - uint32_t accent_px = colors::ACCENT.to_pixel(); int y = 16; int x = 16; char line[128]; // Title - draw_text_to_pixels(pixels, cw, ch, x, y, "System Information", accent_px); + c.text(x, y, "System Information", colors::ACCENT); y += FONT_HEIGHT + 12; // Separator - uint32_t sep_px = colors::BORDER.to_pixel(); - for (int sx = x; sx < cw - x && y < ch; sx++) - pixels[y * cw + sx] = sep_px; + c.hline(x, y, c.w - 2 * x, colors::BORDER); y += 8; // OS Name snprintf(line, sizeof(line), "OS: %s", si->sys_info.osName); - draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); - y += FONT_HEIGHT + 6; + c.kv_line(x, &y, line, colors::TEXT_COLOR); // OS Version snprintf(line, sizeof(line), "Version: %s", si->sys_info.osVersion); - draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); - y += FONT_HEIGHT + 6; + c.kv_line(x, &y, line, colors::TEXT_COLOR); // API Version snprintf(line, sizeof(line), "API: %d", (int)si->sys_info.apiVersion); - draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); - y += FONT_HEIGHT + 6; + c.kv_line(x, &y, line, colors::TEXT_COLOR); // Max Processes snprintf(line, sizeof(line), "Max PIDs: %d", (int)si->sys_info.maxProcesses); - draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); + c.text(x, y, line, colors::TEXT_COLOR); y += FONT_HEIGHT + 12; // Uptime @@ -73,15 +59,14 @@ static void sysinfo_on_draw(Window* win, Framebuffer& fb) { int up_min = up_sec / 60; int up_hr = up_min / 60; snprintf(line, sizeof(line), "Uptime: %d:%02d:%02d", up_hr, up_min % 60, up_sec % 60); - draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); + c.text(x, y, line, colors::TEXT_COLOR); y += FONT_HEIGHT + 12; // Network section - draw_text_to_pixels(pixels, cw, ch, x, y, "Network", accent_px); + c.text(x, y, "Network", colors::ACCENT); y += FONT_HEIGHT + 8; - for (int sx = x; sx < cw - x && y < ch; sx++) - pixels[y * cw + sx] = sep_px; + c.hline(x, y, c.w - 2 * x, colors::BORDER); y += 8; // IP Address @@ -89,39 +74,35 @@ static void sysinfo_on_draw(Window* win, Framebuffer& fb) { snprintf(line, sizeof(line), "IP: %d.%d.%d.%d", (int)(ip & 0xFF), (int)((ip >> 8) & 0xFF), (int)((ip >> 16) & 0xFF), (int)((ip >> 24) & 0xFF)); - draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); - y += FONT_HEIGHT + 6; + c.kv_line(x, &y, line, colors::TEXT_COLOR); // Subnet uint32_t mask = si->net_cfg.subnetMask; snprintf(line, sizeof(line), "Subnet: %d.%d.%d.%d", (int)(mask & 0xFF), (int)((mask >> 8) & 0xFF), (int)((mask >> 16) & 0xFF), (int)((mask >> 24) & 0xFF)); - draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); - y += FONT_HEIGHT + 6; + c.kv_line(x, &y, line, colors::TEXT_COLOR); // Gateway uint32_t gw = si->net_cfg.gateway; snprintf(line, sizeof(line), "Gateway: %d.%d.%d.%d", (int)(gw & 0xFF), (int)((gw >> 8) & 0xFF), (int)((gw >> 16) & 0xFF), (int)((gw >> 24) & 0xFF)); - draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); - y += FONT_HEIGHT + 6; + c.kv_line(x, &y, line, colors::TEXT_COLOR); // DNS uint32_t dns = si->net_cfg.dnsServer; snprintf(line, sizeof(line), "DNS: %d.%d.%d.%d", (int)(dns & 0xFF), (int)((dns >> 8) & 0xFF), (int)((dns >> 16) & 0xFF), (int)((dns >> 24) & 0xFF)); - draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); - y += FONT_HEIGHT + 6; + c.kv_line(x, &y, line, colors::TEXT_COLOR); // MAC Address snprintf(line, sizeof(line), "MAC: %02x:%02x:%02x:%02x:%02x:%02x", (unsigned)si->net_cfg.macAddress[0], (unsigned)si->net_cfg.macAddress[1], (unsigned)si->net_cfg.macAddress[2], (unsigned)si->net_cfg.macAddress[3], (unsigned)si->net_cfg.macAddress[4], (unsigned)si->net_cfg.macAddress[5]); - draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); + c.text(x, y, line, colors::TEXT_COLOR); } static void sysinfo_on_close(Window* win) { diff --git a/programs/src/desktop/app_texteditor.cpp b/programs/src/desktop/app_texteditor.cpp index 08529d4..6014685 100644 --- a/programs/src/desktop/app_texteditor.cpp +++ b/programs/src/desktop/app_texteditor.cpp @@ -287,38 +287,24 @@ static void texteditor_on_draw(Window* win, Framebuffer& fb) { TextEditorState* te = (TextEditorState*)win->app_data; if (!te) return; - Rect cr = win->content_rect(); - int cw = cr.w; - int ch = cr.h; - uint32_t* pixels = win->content; + Canvas c(win); + c.fill(colors::WINDOW_BG); - // Background - uint32_t bg_px = colors::WINDOW_BG.to_pixel(); - for (int i = 0; i < cw * ch; i++) pixels[i] = bg_px; - - int text_area_h = ch - TE_STATUS_H; + int text_area_h = c.h - TE_STATUS_H; int visible_lines = text_area_h / FONT_HEIGHT; te_ensure_cursor_visible(te, visible_lines); // Line number gutter background - uint32_t gutter_px = Color::from_rgb(0xF0, 0xF0, 0xF0).to_pixel(); - for (int y = 0; y < text_area_h && y < ch; y++) - for (int x = 0; x < TE_LINE_NUM_W && x < cw; x++) - pixels[y * cw + x] = gutter_px; + c.fill_rect(0, 0, TE_LINE_NUM_W, text_area_h, Color::from_rgb(0xF0, 0xF0, 0xF0)); // Gutter separator - uint32_t sep_px = colors::BORDER.to_pixel(); - if (TE_LINE_NUM_W < cw) { - for (int y = 0; y < text_area_h && y < ch; y++) - pixels[y * cw + TE_LINE_NUM_W] = sep_px; - } + c.vline(TE_LINE_NUM_W, 0, text_area_h, colors::BORDER); // Draw lines + Color linenum_color = Color::from_rgb(0x99, 0x99, 0x99); + Color cursor_line_color = Color::from_rgb(0xFF, 0xFD, 0xE8); uint32_t text_px = colors::TEXT_COLOR.to_pixel(); - uint32_t linenum_px = Color::from_rgb(0x99, 0x99, 0x99).to_pixel(); - uint32_t cursor_line_px = Color::from_rgb(0xFF, 0xFD, 0xE8).to_pixel(); - uint32_t cursor_px = colors::ACCENT.to_pixel(); int text_start_x = TE_LINE_NUM_W + 4; @@ -331,36 +317,36 @@ static void texteditor_on_draw(Window* win, Framebuffer& fb) { // Cursor line highlighting if (line == te->cursor_line) { - for (int y = py; y < py + FONT_HEIGHT && y < text_area_h; y++) - for (int x = TE_LINE_NUM_W + 1; x < cw; x++) - pixels[y * cw + x] = cursor_line_px; + int hl_h = gui_min(FONT_HEIGHT, text_area_h - py); + if (hl_h > 0) + c.fill_rect(TE_LINE_NUM_W + 1, py, c.w - TE_LINE_NUM_W - 1, hl_h, cursor_line_color); } // Line number char num_str[8]; snprintf(num_str, 8, "%4d", line + 1); - draw_text_to_pixels(pixels, cw, ch, 4, py, num_str, linenum_px); + c.text(4, py, num_str, linenum_color); - // Line text + // Line text (per-character rendering with horizontal scroll clipping) int line_start = te->line_offsets[line]; int line_len = te_line_length(te, line); for (int ci = 0; ci < line_len; ci++) { int px = text_start_x + ci * FONT_WIDTH - te->scroll_x; if (px + FONT_WIDTH <= TE_LINE_NUM_W + 1) continue; - if (px >= cw) break; + if (px >= c.w) break; - char c = te->buffer[line_start + ci]; - if (c >= 32 || c < 0) { - const uint8_t* glyph = &font_data[(unsigned char)c * FONT_HEIGHT]; + char ch = te->buffer[line_start + ci]; + if (ch >= 32 || ch < 0) { + const uint8_t* glyph = &font_data[(unsigned char)ch * FONT_HEIGHT]; for (int fy = 0; fy < FONT_HEIGHT && py + fy < text_area_h; fy++) { uint8_t bits = glyph[fy]; for (int fx = 0; fx < FONT_WIDTH; fx++) { if (bits & (0x80 >> fx)) { int dx = px + fx; int dy = py + fy; - if (dx > TE_LINE_NUM_W && dx < cw && dy >= 0 && dy < text_area_h) - pixels[dy * cw + dx] = text_px; + if (dx > TE_LINE_NUM_W && dx < c.w && dy >= 0 && dy < text_area_h) + c.pixels[dy * c.w + dx] = text_px; } } } @@ -370,24 +356,17 @@ static void texteditor_on_draw(Window* win, Framebuffer& fb) { // Draw cursor if (line == te->cursor_line) { int cx = text_start_x + te->cursor_col * FONT_WIDTH - te->scroll_x; - if (cx > TE_LINE_NUM_W && cx + 2 <= cw) { - for (int y = py; y < py + FONT_HEIGHT && y < text_area_h; y++) { - pixels[y * cw + cx] = cursor_px; - if (cx + 1 < cw) - pixels[y * cw + cx + 1] = cursor_px; - } + if (cx > TE_LINE_NUM_W && cx + 2 <= c.w) { + int cur_h = gui_min(FONT_HEIGHT, text_area_h - py); + if (cur_h > 0) + c.fill_rect(cx, py, 2, cur_h, colors::ACCENT); } } } // ---- Status bar ---- - int status_y = ch - TE_STATUS_H; - uint32_t status_bg = Color::from_rgb(0x2B, 0x3E, 0x50).to_pixel(); - uint32_t status_fg = colors::PANEL_TEXT.to_pixel(); - - for (int y = status_y; y < ch; y++) - for (int x = 0; x < cw; x++) - pixels[y * cw + x] = status_bg; + int status_y = c.h - TE_STATUS_H; + c.fill_rect(0, status_y, c.w, TE_STATUS_H, Color::from_rgb(0x2B, 0x3E, 0x50)); // Filename + modified flag char status_left[128]; @@ -396,16 +375,13 @@ static void texteditor_on_draw(Window* win, Framebuffer& fb) { } else { snprintf(status_left, 128, " Untitled%s", te->modified ? " [modified]" : ""); } - draw_text_to_pixels(pixels, cw, ch, 4, status_y + (TE_STATUS_H - FONT_HEIGHT) / 2, - status_left, status_fg); + c.text(4, status_y + (TE_STATUS_H - FONT_HEIGHT) / 2, status_left, colors::PANEL_TEXT); // Cursor position (right side) char status_right[32]; snprintf(status_right, 32, "Ln %d, Col %d ", te->cursor_line + 1, te->cursor_col + 1); int sr_w = zenith::slen(status_right) * FONT_WIDTH; - draw_text_to_pixels(pixels, cw, ch, cw - sr_w - 4, - status_y + (TE_STATUS_H - FONT_HEIGHT) / 2, - status_right, status_fg); + c.text(c.w - sr_w - 4, status_y + (TE_STATUS_H - FONT_HEIGHT) / 2, status_right, colors::PANEL_TEXT); } // ============================================================================ diff --git a/programs/src/desktop/app_wiki.cpp b/programs/src/desktop/app_wiki.cpp index 35f968a..4489875 100644 --- a/programs/src/desktop/app_wiki.cpp +++ b/programs/src/desktop/app_wiki.cpp @@ -22,7 +22,7 @@ static constexpr int WIKI_SCROLLBAR_W = 12; struct WikiDisplayLine { char text[256]; - uint32_t color; + Color color; }; struct WikiState { @@ -121,7 +121,7 @@ static int wiki_extract_json_string(const char* buf, int len, const char* key, // Display line building (word-wrap adapted for pixel widths) // ============================================================================ -static void wiki_add_line(WikiState* ws, const char* text, int len, uint32_t color) { +static void wiki_add_line(WikiState* ws, const char* text, int len, Color color) { if (ws->lineCount >= ws->lineCap) return; WikiDisplayLine* dl = &ws->lines[ws->lineCount]; int copyLen = len; @@ -133,7 +133,7 @@ static void wiki_add_line(WikiState* ws, const char* text, int len, uint32_t col } static void wiki_wrap_text(WikiState* ws, const char* text, int textLen, - int maxChars, uint32_t color) { + int maxChars, Color color) { if (textLen <= 0 || maxChars <= 0) return; const char* p = text; const char* end = text + textLen; @@ -171,18 +171,18 @@ static void wiki_build_display(WikiState* ws, const char* title, int maxChars = (contentW - 24 - WIKI_SCROLLBAR_W) / FONT_WIDTH; if (maxChars < 20) maxChars = 20; - uint32_t accent_px = colors::ACCENT.to_pixel(); - uint32_t green_px = Color::from_rgb(0x2E, 0x7D, 0x32).to_pixel(); - uint32_t text_px = colors::TEXT_COLOR.to_pixel(); + Color accent_c = colors::ACCENT; + Color green_c = Color::from_rgb(0x2E, 0x7D, 0x32); + Color text_c = colors::TEXT_COLOR; // Title if (title && title[0]) { - wiki_wrap_text(ws, title, slen(title), maxChars, accent_px); + wiki_wrap_text(ws, title, slen(title), maxChars, accent_c); } // Blank separator if (ws->lineCount > 0) - wiki_add_line(ws, "", 0, text_px); + wiki_add_line(ws, "", 0, text_c); // Process extract line by line const char* p = extract; @@ -195,7 +195,7 @@ static void wiki_build_display(WikiState* ws, const char* title, if (p < end) p++; if (lineLen == 0) { - wiki_add_line(ws, "", 0, text_px); + wiki_add_line(ws, "", 0, text_c); continue; } @@ -208,20 +208,20 @@ static void wiki_build_display(WikiState* ws, const char* title, while (ei > si && lineStart[ei - 1] == '=') ei--; while (ei > si && lineStart[ei - 1] == ' ') ei--; - wiki_add_line(ws, "", 0, text_px); + wiki_add_line(ws, "", 0, text_c); if (ei > si) { char secBuf[256]; int secLen = ei - si; if (secLen > 255) secLen = 255; for (int i = 0; i < secLen; i++) secBuf[i] = lineStart[si + i]; secBuf[secLen] = '\0'; - wiki_add_line(ws, secBuf, secLen, green_px); + wiki_add_line(ws, secBuf, secLen, green_c); } continue; } // Regular text - wiki_wrap_text(ws, lineStart, lineLen, maxChars, text_px); + wiki_wrap_text(ws, lineStart, lineLen, maxChars, text_c); } } @@ -280,111 +280,68 @@ static void wiki_on_draw(Window* win, Framebuffer& fb) { WikiState* ws = (WikiState*)win->app_data; if (!ws) return; - Rect cr = win->content_rect(); - int cw = cr.w; - int ch = cr.h; - uint32_t* pixels = win->content; - - // Fill background - uint32_t bg_px = colors::WINDOW_BG.to_pixel(); - for (int i = 0; i < cw * ch; i++) pixels[i] = bg_px; + Canvas c(win); + c.fill(colors::WINDOW_BG); // ---- Toolbar background ---- - uint32_t toolbar_bg = Color::from_rgb(0xF5, 0xF5, 0xF5).to_pixel(); - for (int y = 0; y < WIKI_TOOLBAR_H && y < ch; y++) - for (int x = 0; x < cw; x++) - pixels[y * cw + x] = toolbar_bg; + c.fill_rect(0, 0, c.w, WIKI_TOOLBAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5)); // Toolbar separator line - uint32_t sep_px = colors::BORDER.to_pixel(); - if (WIKI_TOOLBAR_H < ch) - for (int x = 0; x < cw; x++) - pixels[WIKI_TOOLBAR_H * cw + x] = sep_px; + c.hline(0, WIKI_TOOLBAR_H, c.w, colors::BORDER); // ---- Draw search box outline ---- int tb_x = 8; int tb_y = 6; - int tb_w = cw - 90; + int tb_w = c.w - 90; int tb_h = 24; if (tb_w < 100) tb_w = 100; - // TextBox background - uint32_t white_px = colors::WHITE.to_pixel(); - uint32_t border_px = colors::BORDER.to_pixel(); - for (int y = tb_y; y < tb_y + tb_h && y < ch; y++) - for (int x = tb_x; x < tb_x + tb_w && x < cw; x++) - pixels[y * cw + x] = white_px; - // Border - for (int x = tb_x; x < tb_x + tb_w && x < cw; x++) { - if (tb_y < ch) pixels[tb_y * cw + x] = border_px; - if (tb_y + tb_h - 1 < ch) pixels[(tb_y + tb_h - 1) * cw + x] = border_px; - } - for (int y = tb_y; y < tb_y + tb_h && y < ch; y++) { - if (tb_x < cw) pixels[y * cw + tb_x] = border_px; - if (tb_x + tb_w - 1 < cw) pixels[y * cw + (tb_x + tb_w - 1)] = border_px; - } + // TextBox background + border + c.fill_rect(tb_x, tb_y, tb_w, tb_h, colors::WHITE); + c.rect(tb_x, tb_y, tb_w, tb_h, colors::BORDER); // Search text - uint32_t text_px = colors::TEXT_COLOR.to_pixel(); - draw_text_to_pixels(pixels, cw, ch, tb_x + 4, tb_y + (tb_h - FONT_HEIGHT) / 2, - ws->searchQuery, text_px); + c.text(tb_x + 4, tb_y + (tb_h - FONT_HEIGHT) / 2, ws->searchQuery, colors::TEXT_COLOR); // ---- Search button ---- int btn_x = tb_x + tb_w + 6; int btn_y = tb_y; int btn_w = 66; int btn_h = tb_h; - uint32_t accent_px = colors::ACCENT.to_pixel(); - for (int y = btn_y; y < btn_y + btn_h && y < ch; y++) - for (int x = btn_x; x < btn_x + btn_w && x < cw; x++) - pixels[y * cw + x] = accent_px; - // Button text - uint32_t white_text = colors::WHITE.to_pixel(); - const char* btnLabel = "Search"; - int labelW = zenith::slen(btnLabel) * FONT_WIDTH; - draw_text_to_pixels(pixels, cw, ch, - btn_x + (btn_w - labelW) / 2, - btn_y + (btn_h - FONT_HEIGHT) / 2, - btnLabel, white_text); + c.button(btn_x, btn_y, btn_w, btn_h, "Search", colors::ACCENT, colors::WHITE, 0); // ---- Content area ---- int content_y = WIKI_TOOLBAR_H + 1; - int content_h = ch - content_y; + int content_h = c.h - content_y; int visibleLines = content_h / (FONT_HEIGHT + 4); if (visibleLines < 1) visibleLines = 1; if (ws->mode == WikiState::FETCHING) { - draw_text_to_pixels(pixels, cw, ch, 16, content_y + 16, - "Loading...", Color::from_rgb(0x88, 0x88, 0x88).to_pixel()); + c.text(16, content_y + 16, "Loading...", Color::from_rgb(0x88, 0x88, 0x88)); } else if (ws->mode == WikiState::WIKI_ERROR) { - draw_text_to_pixels(pixels, cw, ch, 16, content_y + 16, - ws->statusMsg, colors::CLOSE_BTN.to_pixel()); + c.text(16, content_y + 16, ws->statusMsg, colors::CLOSE_BTN); } else if (ws->mode == WikiState::IDLE) { - draw_text_to_pixels(pixels, cw, ch, 16, content_y + 16, - "Type a topic and press Enter or click Search.", - Color::from_rgb(0x88, 0x88, 0x88).to_pixel()); + c.text(16, content_y + 16, + "Type a topic and press Enter or click Search.", + Color::from_rgb(0x88, 0x88, 0x88)); } else if (ws->mode == WikiState::DONE && ws->lineCount > 0) { int y = content_y + 8; int lineH = FONT_HEIGHT + 4; - for (int i = ws->scrollY; i < ws->lineCount && y + FONT_HEIGHT < ch; i++) { + for (int i = ws->scrollY; i < ws->lineCount && y + FONT_HEIGHT < c.h; i++) { WikiDisplayLine* dl = &ws->lines[i]; if (dl->text[0] != '\0') { - draw_text_to_pixels(pixels, cw, ch, 12, y, dl->text, dl->color); + c.text(12, y, dl->text, dl->color); } y += lineH; } // ---- Scrollbar ---- if (ws->lineCount > visibleLines) { - int sb_x = cw - WIKI_SCROLLBAR_W; + int sb_x = c.w - WIKI_SCROLLBAR_W; int sb_y = content_y; int sb_h = content_h; - // Scrollbar track - uint32_t sb_bg = colors::SCROLLBAR_BG.to_pixel(); - for (int y2 = sb_y; y2 < sb_y + sb_h && y2 < ch; y2++) - for (int x = sb_x; x < cw; x++) - pixels[y2 * cw + x] = sb_bg; + c.fill_rect(sb_x, sb_y, WIKI_SCROLLBAR_W, sb_h, colors::SCROLLBAR_BG); // Thumb int maxScroll = ws->lineCount - visibleLines; @@ -393,10 +350,7 @@ static void wiki_on_draw(Window* win, Framebuffer& fb) { if (thumbH < 20) thumbH = 20; int thumbY = sb_y + (ws->scrollY * (sb_h - thumbH)) / maxScroll; - uint32_t sb_fg = colors::SCROLLBAR_FG.to_pixel(); - for (int y2 = thumbY; y2 < thumbY + thumbH && y2 < ch; y2++) - for (int x = sb_x + 2; x < cw - 2; x++) - pixels[y2 * cw + x] = sb_fg; + c.fill_rect(sb_x + 2, thumbY, WIKI_SCROLLBAR_W - 4, thumbH, colors::SCROLLBAR_FG); } } } diff --git a/programs/src/desktop/apps_common.hpp b/programs/src/desktop/apps_common.hpp index 8f9e940..94147ed 100644 --- a/programs/src/desktop/apps_common.hpp +++ b/programs/src/desktop/apps_common.hpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -128,53 +129,6 @@ inline void format_mac(char* buf, const uint8_t* mac) { (unsigned)mac[3], (unsigned)mac[4], (unsigned)mac[5]); } -// ============================================================================ -// Text rendering to pixel buffers (for app content areas) -// ============================================================================ - -inline void draw_text_to_pixels(uint32_t* pixels, int pw, int ph, - int tx, int ty, const char* text, uint32_t color_px) { - for (int i = 0; text[i] && tx + (i + 1) * FONT_WIDTH <= pw; i++) { - const uint8_t* glyph = &font_data[(unsigned char)text[i] * FONT_HEIGHT]; - int cx = tx + i * FONT_WIDTH; - for (int fy = 0; fy < FONT_HEIGHT && ty + fy < ph; fy++) { - uint8_t bits = glyph[fy]; - for (int fx = 0; fx < FONT_WIDTH; fx++) { - if (bits & (0x80 >> fx)) { - int dx = cx + fx; - int dy = ty + fy; - if (dx >= 0 && dx < pw && dy >= 0 && dy < ph) - pixels[dy * pw + dx] = color_px; - } - } - } - } -} - -inline void draw_text_to_pixels_2x(uint32_t* pixels, int pw, int ph, - int tx, int ty, const char* text, uint32_t color_px) { - for (int i = 0; text[i] && tx + (i + 1) * FONT_WIDTH * 2 <= pw; i++) { - const uint8_t* glyph = &font_data[(unsigned char)text[i] * FONT_HEIGHT]; - int cx = tx + i * FONT_WIDTH * 2; - for (int fy = 0; fy < FONT_HEIGHT; fy++) { - uint8_t bits = glyph[fy]; - for (int fx = 0; fx < FONT_WIDTH; fx++) { - if (bits & (0x80 >> fx)) { - int dx = cx + fx * 2; - int dy = ty + fy * 2; - for (int sy = 0; sy < 2; sy++) - for (int sx = 0; sx < 2; sx++) { - int px = dx + sx; - int py = dy + sy; - if (px >= 0 && px < pw && py >= 0 && py < ph) - pixels[py * pw + px] = color_px; - } - } - } - } - } -} - // ============================================================================ // File size formatting // ============================================================================ @@ -201,42 +155,6 @@ inline void format_size(char* buf, int size) { } } -// ============================================================================ -// Icon rendering to pixel buffer -// ============================================================================ - -inline void blit_icon_to_pixels(uint32_t* pixels, int pw, int ph, - int ix, int iy, const SvgIcon& icon) { - if (!icon.pixels) return; - for (int row = 0; row < icon.height; row++) { - int dy = iy + row; - if (dy < 0 || dy >= ph) continue; - for (int col = 0; col < icon.width; col++) { - int dx = ix + col; - if (dx < 0 || dx >= pw) continue; - uint32_t src = icon.pixels[row * icon.width + col]; - uint8_t sa = (src >> 24) & 0xFF; - if (sa == 0) continue; - if (sa == 255) { - pixels[dy * pw + dx] = src; - } else { - uint32_t dst = pixels[dy * pw + dx]; - uint8_t sr = (src >> 16) & 0xFF; - uint8_t sg = (src >> 8) & 0xFF; - uint8_t sb = src & 0xFF; - uint8_t dr = (dst >> 16) & 0xFF; - uint8_t dg = (dst >> 8) & 0xFF; - uint8_t db = dst & 0xFF; - uint32_t a = sa, inv_a = 255 - sa; - uint32_t rr = (a * sr + inv_a * dr + 128) / 255; - uint32_t gg = (a * sg + inv_a * dg + 128) / 255; - uint32_t bb = (a * sb + inv_a * db + 128) / 255; - pixels[dy * pw + dx] = 0xFF000000 | (rr << 16) | (gg << 8) | bb; - } - } - } -} - // ============================================================================ // Forward declarations for app launchers // ============================================================================ @@ -249,3 +167,5 @@ void open_texteditor(DesktopState* ds); void open_texteditor_with_file(DesktopState* ds, const char* path); void open_klog(DesktopState* ds); void open_wiki(DesktopState* ds); +void open_settings(DesktopState* ds); +void open_reboot_dialog(DesktopState* ds); diff --git a/programs/src/desktop/main.cpp b/programs/src/desktop/main.cpp index 026fa32..4cb4b65 100644 --- a/programs/src/desktop/main.cpp +++ b/programs/src/desktop/main.cpp @@ -55,6 +55,13 @@ void gui::desktop_init(DesktopState* ds) { ds->icon_file_lg = svg_load("0:/icons/text-x-generic.svg", 48, 48, defColor); ds->icon_exec_lg = svg_load("0:/icons/utilities-terminal.svg", 48, 48, defColor); + ds->icon_settings = svg_load("0:/icons/help-about.svg", 20, 20, defColor); + ds->icon_reboot = svg_load("0:/icons/system-reboot.svg", 20, 20, defColor); + + ds->ctx_menu_open = false; + ds->ctx_menu_x = 0; + ds->ctx_menu_y = 0; + ds->net_popup_open = false; zenith::get_netcfg(&ds->cached_net_cfg); ds->net_cfg_last_poll = zenith::get_milliseconds(); @@ -207,15 +214,17 @@ void gui::desktop_draw_window(DesktopState* ds, int idx) { } draw_text(fb, title_x, title_y, win->title, colors::TEXT_COLOR); - // Call app draw callback to render content - if (win->on_draw) { + // 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 + // Blit content buffer to framebuffer (clip to actual buffer size during resize) Rect cr = win->content_rect(); if (win->content) { - fb.blit(cr.x, cr.y, cr.w, cr.h, win->content); + 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); } } @@ -340,7 +349,7 @@ void gui::desktop_draw_panel(DesktopState* ds) { // App Menu (5 items with separator and rounded corners) // ============================================================================ -static constexpr int MENU_ITEM_COUNT = 7; +static constexpr int MENU_ITEM_COUNT = 9; static constexpr int MENU_W = 220; static constexpr int MENU_ITEM_H = 40; @@ -371,6 +380,8 @@ static void desktop_draw_app_menu(DesktopState* ds) { { "Text Editor", &ds->icon_texteditor }, { "Kernel Log", &ds->icon_terminal }, { "Wikipedia", &ds->icon_wikipedia }, + { "About", &ds->icon_settings }, + { "Reboot", &ds->icon_reboot }, }; int mx = ds->mouse.x; @@ -379,8 +390,8 @@ static void desktop_draw_app_menu(DesktopState* ds) { for (int i = 0; i < MENU_ITEM_COUNT; i++) { int iy = menu_y + 4 + i * MENU_ITEM_H; - // Thin separator line after System Info (before utility apps) - if (i == 3) { + // Thin separator lines before utility apps and before Settings + if (i == 3 || i == 7) { int sep_y = iy - 1; for (int sx = menu_x + 8; sx < menu_x + MENU_W - 8; sx++) fb.put_pixel(sx, sep_y, colors::BORDER); @@ -459,6 +470,164 @@ static void desktop_draw_net_popup(DesktopState* ds) { 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) zenith::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 Zenith::KeyEvent& key) { + RebootDialogState* rs = (RebootDialogState*)win->app_data; + if (!rs || !key.pressed) return; + + if (key.ascii == '\n' || key.ascii == '\r') { + zenith::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) { + zenith::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*)zenith::malloc(sizeof(RebootDialogState)); + zenith::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; +} + +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; @@ -508,8 +677,91 @@ void gui::desktop_compose(DesktopState* ds) { 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 = 4; + 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 }, + }; + + 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 - 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); + draw_cursor(fb, ds->mouse.x, ds->mouse.y, cur_style); } void gui::desktop_handle_mouse(DesktopState* ds) { @@ -520,6 +772,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) { 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; @@ -528,6 +781,42 @@ void gui::desktop_handle_mouse(DesktopState* ds) { 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 = 4; + 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; + } + 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]; @@ -542,6 +831,88 @@ void gui::desktop_handle_mouse(DesktopState* ds) { } 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; + Rect cr = win->content_rect(); + if (cr.w != win->content_w || cr.h != win->content_h) { + if (win->content) zenith::free(win->content); + win->content_w = cr.w; + win->content_h = cr.h; + win->content = (uint32_t*)zenith::alloc(cr.w * cr.h * 4); + zenith::memset(win->content, 0xFF, cr.w * cr.h * 4); + } + } 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; + Rect cr = win->content_rect(); + if (cr.w != win->content_w || cr.h != win->content_h) { + if (win->content) zenith::free(win->content); + win->content_w = cr.w; + win->content_h = cr.h; + win->content = (uint32_t*)zenith::alloc(cr.w * cr.h * 4); + zenith::memset(win->content, 0xFF, cr.w * cr.h * 4); + } + } + } + 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 + Rect cr = win->content_rect(); + if (cr.w != win->content_w || cr.h != win->content_h) { + if (win->content) zenith::free(win->content); + win->content_w = cr.w; + win->content_h = cr.h; + win->content = (uint32_t*)zenith::alloc(cr.w * cr.h * 4); + zenith::memset(win->content, 0xFF, cr.w * cr.h * 4); + } + win->dirty = true; } return; } @@ -566,6 +937,8 @@ void gui::desktop_handle_mouse(DesktopState* ds) { case 4: open_texteditor(ds); break; case 5: open_klog(ds); break; case 6: open_wiki(ds); break; + case 7: open_settings(ds); break; + case 8: open_reboot_dialog(ds); break; } ds->app_menu_open = false; } @@ -597,6 +970,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) { if (mx < 36) { ds->app_menu_open = !ds->app_menu_open; ds->net_popup_open = false; + ds->ctx_menu_open = false; return; } @@ -604,6 +978,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) { if (ds->net_icon_rect.w > 0 && ds->net_icon_rect.contains(mx, my)) { ds->net_popup_open = !ds->net_popup_open; ds->app_menu_open = false; + ds->ctx_menu_open = false; return; } // Window indicator buttons @@ -683,6 +1058,26 @@ void gui::desktop_handle_mouse(DesktopState* ds) { 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)) { @@ -718,6 +1113,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) { } ds->app_menu_open = false; + ds->ctx_menu_open = false; } // Handle scroll events on focused window @@ -728,6 +1124,26 @@ void gui::desktop_handle_mouse(DesktopState* ds) { 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 Zenith::KeyEvent& key) { diff --git a/programs/src/desktop/obj/app_calculator.o b/programs/src/desktop/obj/app_calculator.o index 4ba5de3..a556d6f 100644 Binary files a/programs/src/desktop/obj/app_calculator.o and b/programs/src/desktop/obj/app_calculator.o differ diff --git a/programs/src/desktop/obj/app_filemanager.o b/programs/src/desktop/obj/app_filemanager.o index 4956af8..910a753 100644 Binary files a/programs/src/desktop/obj/app_filemanager.o and b/programs/src/desktop/obj/app_filemanager.o differ diff --git a/programs/src/desktop/obj/app_klog.o b/programs/src/desktop/obj/app_klog.o index 35f9677..0b2cb18 100644 Binary files a/programs/src/desktop/obj/app_klog.o and b/programs/src/desktop/obj/app_klog.o differ diff --git a/programs/src/desktop/obj/app_sysinfo.o b/programs/src/desktop/obj/app_sysinfo.o index bd34efb..a13ada2 100644 Binary files a/programs/src/desktop/obj/app_sysinfo.o and b/programs/src/desktop/obj/app_sysinfo.o differ diff --git a/programs/src/desktop/obj/app_terminal.o b/programs/src/desktop/obj/app_terminal.o index 022381c..0043f58 100644 Binary files a/programs/src/desktop/obj/app_terminal.o and b/programs/src/desktop/obj/app_terminal.o differ diff --git a/programs/src/desktop/obj/app_texteditor.o b/programs/src/desktop/obj/app_texteditor.o index 04815e8..4a4d990 100644 Binary files a/programs/src/desktop/obj/app_texteditor.o and b/programs/src/desktop/obj/app_texteditor.o differ diff --git a/programs/src/desktop/obj/app_wiki.o b/programs/src/desktop/obj/app_wiki.o index 09a7de6..8bb92c9 100644 Binary files a/programs/src/desktop/obj/app_wiki.o and b/programs/src/desktop/obj/app_wiki.o differ diff --git a/programs/src/desktop/obj/main.o b/programs/src/desktop/obj/main.o index 035784d..720dc4d 100644 Binary files a/programs/src/desktop/obj/main.o and b/programs/src/desktop/obj/main.o differ diff --git a/ramdisk.tar b/ramdisk.tar index f41b4a3..115da62 100644 Binary files a/ramdisk.tar and b/ramdisk.tar differ diff --git a/scripts/copy_icons.sh b/scripts/copy_icons.sh index 06f286f..5dd5c0b 100755 --- a/scripts/copy_icons.sh +++ b/scripts/copy_icons.sh @@ -50,6 +50,8 @@ ICONS=( "devices/scalable/network-wired.svg" "mimetypes/scalable/text-x-generic.svg" "mimetypes/scalable/application-x-executable.svg" + "categories/scalable/help-about.svg" + "categories/scalable/system-reboot.svg" ) copied=0