feat: Desktop improvements
This commit is contained in:
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 ----
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <gui/svg.hpp>
|
||||
#include <gui/widgets.hpp>
|
||||
#include <gui/window.hpp>
|
||||
#include <gui/canvas.hpp>
|
||||
#include <gui/terminal.hpp>
|
||||
#include <gui/desktop.hpp>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user