feat: Desktop improvements
This commit is contained in:
@@ -11,3 +11,4 @@ programs/bin/
|
|||||||
programs/obj/
|
programs/obj/
|
||||||
programs/src/doom/obj/
|
programs/src/doom/obj/
|
||||||
programs/gui/icons/
|
programs/gui/icons/
|
||||||
|
programs/src/desktop/obj/
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ namespace Drivers::PS2::Mouse {
|
|||||||
g_State.X += deltaX;
|
g_State.X += deltaX;
|
||||||
g_State.Y += deltaY;
|
g_State.Y += deltaY;
|
||||||
g_State.Buttons = buttons;
|
g_State.Buttons = buttons;
|
||||||
g_State.ScrollDelta = scrollDelta;
|
g_State.ScrollDelta += scrollDelta;
|
||||||
|
|
||||||
// Clamp to screen bounds
|
// Clamp to screen bounds
|
||||||
if (g_State.X < 0) g_State.X = 0;
|
if (g_State.X < 0) g_State.X = 0;
|
||||||
@@ -175,6 +175,7 @@ namespace Drivers::PS2::Mouse {
|
|||||||
MouseState GetMouseState() {
|
MouseState GetMouseState() {
|
||||||
g_StateLock.Acquire();
|
g_StateLock.Acquire();
|
||||||
MouseState state = g_State;
|
MouseState state = g_State;
|
||||||
|
g_State.ScrollDelta = 0; // clear after read so deltas don't repeat
|
||||||
g_StateLock.Release();
|
g_StateLock.Release();
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@@ -202,7 +203,7 @@ namespace Drivers::PS2::Mouse {
|
|||||||
g_State.X += (int32_t)deltaX;
|
g_State.X += (int32_t)deltaX;
|
||||||
g_State.Y += (int32_t)deltaY;
|
g_State.Y += (int32_t)deltaY;
|
||||||
g_State.Buttons = buttons;
|
g_State.Buttons = buttons;
|
||||||
g_State.ScrollDelta = (int32_t)scroll;
|
g_State.ScrollDelta += (int32_t)scroll;
|
||||||
|
|
||||||
// Clamp to screen bounds
|
// Clamp to screen bounds
|
||||||
if (g_State.X < 0) g_State.X = 0;
|
if (g_State.X < 0) g_State.X = 0;
|
||||||
|
|||||||
@@ -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_file_lg;
|
||||||
SvgIcon icon_exec_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;
|
bool net_popup_open;
|
||||||
Zenith::NetCfg cached_net_cfg;
|
Zenith::NetCfg cached_net_cfg;
|
||||||
uint64_t net_cfg_last_poll;
|
uint64_t net_cfg_last_poll;
|
||||||
|
|||||||
@@ -217,20 +217,175 @@ static constexpr uint16_t cursor_fill[16] = {
|
|||||||
0x0000, // row 15: no fill
|
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)
|
// 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 black = colors::BLACK;
|
||||||
Color white = colors::WHITE;
|
Color white = colors::WHITE;
|
||||||
|
|
||||||
for (int row = 0; row < 16; row++) {
|
for (int row = 0; row < 16; row++) {
|
||||||
uint16_t outline = cursor_outline[row];
|
uint16_t outline = outline_data[row];
|
||||||
uint16_t fill = cursor_fill[row];
|
uint16_t fill = fill_data[row];
|
||||||
for (int col = 0; col < 16; col++) {
|
for (int col = 0; col < 16; col++) {
|
||||||
uint16_t mask = (uint16_t)(0x8000 >> col);
|
uint16_t mask = (uint16_t)(0x8000 >> col);
|
||||||
if (outline & mask) {
|
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) {
|
} 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 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 TITLEBAR_HEIGHT = 30;
|
||||||
static constexpr int BORDER_WIDTH = 1;
|
static constexpr int BORDER_WIDTH = 1;
|
||||||
static constexpr int SHADOW_SIZE = 3;
|
static constexpr int SHADOW_SIZE = 3;
|
||||||
static constexpr int BTN_RADIUS = 6;
|
static constexpr int BTN_RADIUS = 6;
|
||||||
static constexpr int MAX_TITLE_LEN = 64;
|
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;
|
struct Window;
|
||||||
using WindowDrawCallback = void (*)(Window* win, Framebuffer& fb);
|
using WindowDrawCallback = void (*)(Window* win, Framebuffer& fb);
|
||||||
@@ -42,6 +51,9 @@ struct Window {
|
|||||||
int drag_offset_x, drag_offset_y;
|
int drag_offset_x, drag_offset_y;
|
||||||
|
|
||||||
bool resizing;
|
bool resizing;
|
||||||
|
ResizeEdge resize_edge;
|
||||||
|
Rect resize_start_frame;
|
||||||
|
int resize_start_mx, resize_start_my;
|
||||||
|
|
||||||
Rect saved_frame;
|
Rect saved_frame;
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ LDFLAGS := \
|
|||||||
|
|
||||||
# ---- Source files ----
|
# ---- 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))
|
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||||
|
|
||||||
# ---- Target ----
|
# ---- Target ----
|
||||||
|
|||||||
@@ -222,29 +222,21 @@ static void calculator_on_draw(Window* win, Framebuffer& fb) {
|
|||||||
CalcState* cs = (CalcState*)win->app_data;
|
CalcState* cs = (CalcState*)win->app_data;
|
||||||
if (!cs) return;
|
if (!cs) return;
|
||||||
|
|
||||||
Rect cr = win->content_rect();
|
Canvas c(win);
|
||||||
int cw = cr.w;
|
|
||||||
int ch = cr.h;
|
|
||||||
uint32_t* pixels = win->content;
|
|
||||||
|
|
||||||
// Background
|
// Background
|
||||||
uint32_t bg_px = Color::from_rgb(0xF0, 0xF0, 0xF0).to_pixel();
|
c.fill(Color::from_rgb(0xF0, 0xF0, 0xF0));
|
||||||
for (int i = 0; i < cw * ch; i++) pixels[i] = bg_px;
|
|
||||||
|
|
||||||
// Display area
|
// Display area
|
||||||
uint32_t disp_px = Color::from_rgb(0x2D, 0x2D, 0x2D).to_pixel();
|
c.fill_rect(0, 0, c.w, CALC_DISPLAY_H, Color::from_rgb(0x2D, 0x2D, 0x2D));
|
||||||
for (int y = 0; y < CALC_DISPLAY_H && y < ch; y++)
|
|
||||||
for (int x = 0; x < cw; x++)
|
|
||||||
pixels[y * cw + x] = disp_px;
|
|
||||||
|
|
||||||
// Display text (right-aligned, 2x scale)
|
// Display text (right-aligned, 2x scale)
|
||||||
uint32_t disp_text = colors::WHITE.to_pixel();
|
|
||||||
int text_len = zenith::slen(cs->display_str);
|
int text_len = zenith::slen(cs->display_str);
|
||||||
int text_w = text_len * FONT_WIDTH * 2;
|
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;
|
int ty = (CALC_DISPLAY_H - FONT_HEIGHT * 2) / 2;
|
||||||
if (tx < 4) tx = 4;
|
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
|
// Button grid
|
||||||
int grid_y = CALC_DISPLAY_H + CALC_BTN_PAD;
|
int grid_y = CALC_DISPLAY_H + CALC_BTN_PAD;
|
||||||
@@ -264,30 +256,25 @@ static void calculator_on_draw(Window* win, Framebuffer& fb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Button color
|
// Button color
|
||||||
uint32_t btn_px;
|
Color btn_color;
|
||||||
if (col == 3) {
|
if (col == 3) {
|
||||||
// Operator column: accent blue
|
btn_color = colors::ACCENT;
|
||||||
btn_px = colors::ACCENT.to_pixel();
|
|
||||||
} else if (row == 0) {
|
} else if (row == 0) {
|
||||||
// Function row: gray
|
btn_color = Color::from_rgb(0xD0, 0xD0, 0xD0);
|
||||||
btn_px = Color::from_rgb(0xD0, 0xD0, 0xD0).to_pixel();
|
|
||||||
} else {
|
} else {
|
||||||
// Digit buttons: light gray
|
btn_color = Color::from_rgb(0xE8, 0xE8, 0xE8);
|
||||||
btn_px = Color::from_rgb(0xE8, 0xE8, 0xE8).to_pixel();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw button
|
// Draw button
|
||||||
for (int dy = 0; dy < CALC_BTN_H && by + dy < ch; dy++)
|
c.fill_rect(bx, by, bw, CALC_BTN_H, btn_color);
|
||||||
for (int dx = 0; dx < bw && bx + dx < cw; dx++)
|
|
||||||
pixels[(by + dy) * cw + (bx + dx)] = btn_px;
|
|
||||||
|
|
||||||
// Button text
|
// Button text
|
||||||
const char* label = calc_labels[row][col];
|
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 label_w = zenith::slen(label) * FONT_WIDTH;
|
||||||
int lx = bx + (bw - label_w) / 2;
|
int lx = bx + (bw - label_w) / 2;
|
||||||
int ly = by + (CALC_BTN_H - FONT_HEIGHT) / 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;
|
FileManagerState* fm = (FileManagerState*)win->app_data;
|
||||||
if (!fm) return;
|
if (!fm) return;
|
||||||
|
|
||||||
Rect cr = win->content_rect();
|
Canvas c(win);
|
||||||
int cw = cr.w;
|
c.fill(colors::WINDOW_BG);
|
||||||
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();
|
|
||||||
|
|
||||||
|
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;
|
DesktopState* ds = fm->desktop;
|
||||||
|
|
||||||
// ---- Toolbar (32px) ----
|
// ---- Toolbar (32px) ----
|
||||||
for (int y = 0; y < FM_TOOLBAR_H && y < ch; y++)
|
c.fill_rect(0, 0, c.w, FM_TOOLBAR_H, toolbar_color);
|
||||||
for (int x = 0; x < cw; x++)
|
|
||||||
pixels[y * cw + x] = toolbar_px;
|
|
||||||
|
|
||||||
// Toolbar buttons: Back, Forward, Up, Home
|
// Toolbar buttons: Back, Forward, Up, Home
|
||||||
struct ToolBtn { int x; SvgIcon* icon; };
|
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++) {
|
for (int i = 0; i < 4; i++) {
|
||||||
int bx = btns[i].x;
|
int bx = btns[i].x;
|
||||||
int by = 4;
|
int by = 4;
|
||||||
// Button background
|
c.fill_rect(bx, by, 24, 24, btn_bg);
|
||||||
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;
|
|
||||||
|
|
||||||
if (btns[i].icon) {
|
if (btns[i].icon) {
|
||||||
int ix = bx + (24 - btns[i].icon->width) / 2;
|
int ix = bx + (24 - btns[i].icon->width) / 2;
|
||||||
int iy = by + (24 - btns[i].icon->height) / 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)
|
// Toggle view button (5th toolbar button)
|
||||||
{
|
{
|
||||||
int bx = 120, by = 4;
|
int bx = 120, by = 4;
|
||||||
uint32_t btn_px = Color::from_rgb(0xE8, 0xE8, 0xE8).to_pixel();
|
c.fill_rect(bx, by, 24, 24, btn_bg);
|
||||||
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();
|
|
||||||
if (fm->grid_view) {
|
if (fm->grid_view) {
|
||||||
// Draw 4 small squares to indicate grid mode
|
// Draw 4 small squares to indicate grid mode
|
||||||
for (int r = 0; r < 2; r++)
|
for (int r = 0; r < 2; r++)
|
||||||
for (int c = 0; c < 2; c++)
|
for (int cc = 0; cc < 2; cc++)
|
||||||
for (int dy = 0; dy < 6; dy++)
|
c.fill_rect(bx + 5 + cc * 8, by + 5 + r * 8, 6, 6, colors::TEXT_COLOR);
|
||||||
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;
|
|
||||||
} else {
|
} else {
|
||||||
// Draw 3 horizontal lines to indicate list mode
|
// Draw 3 horizontal lines to indicate list mode
|
||||||
for (int r = 0; r < 3; r++)
|
for (int r = 0; r < 3; r++)
|
||||||
for (int dx = 0; dx < 14; dx++)
|
c.fill_rect(bx + 5, by + 5 + r * 5, 14, 2, colors::TEXT_COLOR);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toolbar separator
|
// Toolbar separator
|
||||||
int sep_y = FM_TOOLBAR_H - 1;
|
c.hline(0, FM_TOOLBAR_H - 1, c.w, colors::BORDER);
|
||||||
if (sep_y < ch) {
|
|
||||||
for (int x = 0; x < cw; x++)
|
|
||||||
pixels[sep_y * cw + x] = sep_px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Path bar ----
|
// ---- Path bar ----
|
||||||
int pathbar_y = FM_TOOLBAR_H;
|
int pathbar_y = FM_TOOLBAR_H;
|
||||||
uint32_t pathbar_px = Color::from_rgb(0xF0, 0xF0, 0xF0).to_pixel();
|
c.fill_rect(0, pathbar_y, c.w, FM_PATHBAR_H, Color::from_rgb(0xF0, 0xF0, 0xF0));
|
||||||
for (int y = pathbar_y; y < pathbar_y + FM_PATHBAR_H && y < ch; y++)
|
c.text(8, pathbar_y + 4, fm->current_path, colors::TEXT_COLOR);
|
||||||
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);
|
|
||||||
|
|
||||||
// Path bar separator
|
// Path bar separator
|
||||||
sep_y = pathbar_y + FM_PATHBAR_H - 1;
|
c.hline(0, pathbar_y + FM_PATHBAR_H - 1, c.w, colors::BORDER);
|
||||||
if (sep_y < ch) {
|
|
||||||
for (int x = 0; x < cw; x++)
|
|
||||||
pixels[sep_y * cw + x] = sep_px;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fm->grid_view) {
|
if (fm->grid_view) {
|
||||||
// ---- Grid View ----
|
// ---- Grid View ----
|
||||||
int list_y = FM_TOOLBAR_H + FM_PATHBAR_H;
|
int list_y = FM_TOOLBAR_H + FM_PATHBAR_H;
|
||||||
int list_h = ch - list_y;
|
int list_h = c.h - list_y;
|
||||||
int cols = (cw - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
|
int cols = (c.w - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
|
||||||
if (cols < 1) cols = 1;
|
if (cols < 1) cols = 1;
|
||||||
int rows = (fm->entry_count + cols - 1) / cols;
|
int rows = (fm->entry_count + cols - 1) / cols;
|
||||||
int content_h = rows * FM_GRID_CELL_H;
|
int content_h = rows * FM_GRID_CELL_H;
|
||||||
|
|
||||||
// Update scrollbar
|
// 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.content_height = content_h;
|
||||||
fm->scrollbar.view_height = list_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;
|
int cell_y = list_y + row * FM_GRID_CELL_H - fm->scrollbar.scroll_offset;
|
||||||
|
|
||||||
// Skip if entirely off-screen
|
// 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
|
// Selection highlight
|
||||||
if (i == fm->selected) {
|
if (i == fm->selected) {
|
||||||
uint32_t sel_px = colors::MENU_HOVER.to_pixel();
|
int sy = gui_max(cell_y, list_y);
|
||||||
for (int y = gui_max(cell_y, list_y); y < gui_min(cell_y + FM_GRID_CELL_H, ch); y++)
|
int sh = gui_min(cell_y + FM_GRID_CELL_H, c.h) - sy;
|
||||||
for (int x = cell_x; x < gui_min(cell_x + FM_GRID_CELL_W, cw - FM_SCROLLBAR_W); x++)
|
int sw = gui_min(FM_GRID_CELL_W, c.w - FM_SCROLLBAR_W - cell_x);
|
||||||
pixels[y * cw + x] = sel_px;
|
if (sh > 0 && sw > 0)
|
||||||
|
c.fill_rect(cell_x, sy, sw, sh, colors::MENU_HOVER);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Large icon centered horizontally
|
// Large icon centered horizontally
|
||||||
int icon_x = cell_x + (FM_GRID_CELL_W - FM_GRID_ICON) / 2;
|
int icon_x = cell_x + (FM_GRID_CELL_W - FM_GRID_ICON) / 2;
|
||||||
int icon_y = cell_y + FM_GRID_PAD;
|
int icon_y = cell_y + FM_GRID_PAD;
|
||||||
if (ds && fm->entry_types[i] == 1 && ds->icon_folder_lg.pixels) {
|
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) {
|
} 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) {
|
} 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 {
|
} else {
|
||||||
uint32_t icon_px = fm->is_dir[i]
|
Color icon_c = fm->is_dir[i]
|
||||||
? Color::from_rgb(0xFF, 0xBD, 0x2E).to_pixel()
|
? Color::from_rgb(0xFF, 0xBD, 0x2E)
|
||||||
: Color::from_rgb(0x90, 0x90, 0x90).to_pixel();
|
: Color::from_rgb(0x90, 0x90, 0x90);
|
||||||
for (int dy = 0; dy < FM_GRID_ICON && icon_y + dy < ch && icon_y + dy >= list_y; dy++)
|
int iy_clip = gui_max(icon_y, list_y);
|
||||||
for (int dx = 0; dx < FM_GRID_ICON && icon_x + dx < cw; dx++)
|
int ih_clip = FM_GRID_ICON - (iy_clip - icon_y);
|
||||||
pixels[(icon_y + dy) * cw + (icon_x + dx)] = icon_px;
|
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
|
// 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;
|
int tx = cell_x + (FM_GRID_CELL_W - tw) / 2;
|
||||||
if (tx < cell_x) tx = cell_x;
|
if (tx < cell_x) tx = cell_x;
|
||||||
int ty = icon_y + FM_GRID_ICON + 2;
|
int ty = icon_y + FM_GRID_ICON + 2;
|
||||||
if (ty >= list_y && ty + FONT_HEIGHT <= ch)
|
if (ty >= list_y && ty + FONT_HEIGHT <= c.h)
|
||||||
draw_text_to_pixels(pixels, cw, ch, tx, ty, label, text_px);
|
c.text(tx, ty, label, colors::TEXT_COLOR);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// ---- List View ----
|
// ---- List View ----
|
||||||
|
|
||||||
// ---- Column headers ----
|
// ---- Column headers ----
|
||||||
int header_y = FM_TOOLBAR_H + FM_PATHBAR_H;
|
int header_y = FM_TOOLBAR_H + FM_PATHBAR_H;
|
||||||
uint32_t header_px = Color::from_rgb(0xF8, 0xF8, 0xF8).to_pixel();
|
c.fill_rect(0, header_y, c.w, FM_HEADER_H, Color::from_rgb(0xF8, 0xF8, 0xF8));
|
||||||
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;
|
|
||||||
|
|
||||||
uint32_t dim_px = Color::from_rgb(0x88, 0x88, 0x88).to_pixel();
|
|
||||||
int name_col_x = 8;
|
int name_col_x = 8;
|
||||||
int size_col_x = cw - FM_SCROLLBAR_W - 120;
|
int size_col_x = c.w - FM_SCROLLBAR_W - 120;
|
||||||
int type_col_x = cw - FM_SCROLLBAR_W - 60;
|
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)
|
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)
|
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
|
// Header separator
|
||||||
sep_y = header_y + FM_HEADER_H - 1;
|
c.hline(0, header_y + FM_HEADER_H - 1, c.w, colors::BORDER);
|
||||||
if (sep_y < ch) {
|
|
||||||
for (int x = 0; x < cw; x++)
|
|
||||||
pixels[sep_y * cw + x] = sep_px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Column separator lines
|
// Column separator lines
|
||||||
if (size_col_x > 100) {
|
if (size_col_x > 100) {
|
||||||
for (int y = header_y; y < ch; y++)
|
c.vline(size_col_x - 4, header_y, c.h - header_y, colors::BORDER);
|
||||||
if (size_col_x - 4 >= 0 && size_col_x - 4 < cw)
|
|
||||||
pixels[y * cw + (size_col_x - 4)] = sep_px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- File entries ----
|
// ---- File entries ----
|
||||||
int list_y = header_y + FM_HEADER_H;
|
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 visible_items = list_h / FM_ITEM_H;
|
||||||
int content_h = fm->entry_count * FM_ITEM_H;
|
int content_h = fm->entry_count * FM_ITEM_H;
|
||||||
|
|
||||||
// Update scrollbar
|
// 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.content_height = content_h;
|
||||||
fm->scrollbar.view_height = list_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++) {
|
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);
|
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
|
// Highlight selected
|
||||||
if (i == fm->selected) {
|
if (i == fm->selected) {
|
||||||
uint32_t sel_px = colors::MENU_HOVER.to_pixel();
|
int sy = gui_max(iy, list_y);
|
||||||
for (int y = gui_max(iy, list_y); y < gui_min(iy + FM_ITEM_H, ch); y++)
|
int sh = gui_min(iy + FM_ITEM_H, c.h) - sy;
|
||||||
for (int x = 0; x < cw - FM_SCROLLBAR_W; x++)
|
if (sh > 0)
|
||||||
pixels[y * cw + x] = sel_px;
|
c.fill_rect(0, sy, c.w - FM_SCROLLBAR_W, sh, colors::MENU_HOVER);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Icon
|
// Icon
|
||||||
int icon_x = 8;
|
int ico_x = 8;
|
||||||
int icon_y = iy + (FM_ITEM_H - 16) / 2;
|
int ico_y = iy + (FM_ITEM_H - 16) / 2;
|
||||||
if (ds && fm->entry_types[i] == 1 && ds->icon_folder.pixels) {
|
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) {
|
} 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) {
|
} 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 {
|
} else {
|
||||||
uint32_t icon_px = fm->is_dir[i]
|
Color icon_c = fm->is_dir[i]
|
||||||
? Color::from_rgb(0xFF, 0xBD, 0x2E).to_pixel()
|
? Color::from_rgb(0xFF, 0xBD, 0x2E)
|
||||||
: Color::from_rgb(0x90, 0x90, 0x90).to_pixel();
|
: Color::from_rgb(0x90, 0x90, 0x90);
|
||||||
for (int dy = 0; dy < 16 && icon_y + dy < ch && icon_y + dy >= list_y; dy++)
|
int iy_clip = gui_max(ico_y, list_y);
|
||||||
for (int dx = 0; dx < 16 && icon_x + dx < cw; dx++)
|
int ih_clip = 16 - (iy_clip - ico_y);
|
||||||
pixels[(icon_y + dy) * cw + (icon_x + dx)] = icon_px;
|
if (ih_clip > 0)
|
||||||
|
c.fill_rect(ico_x, iy_clip, 16, ih_clip, icon_c);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name
|
// Name
|
||||||
int tx = 30;
|
int tx = 30;
|
||||||
int ty = iy + (FM_ITEM_H - FONT_HEIGHT) / 2;
|
int ty = iy + (FM_ITEM_H - FONT_HEIGHT) / 2;
|
||||||
if (ty >= list_y && ty + FONT_HEIGHT <= ch)
|
if (ty >= list_y && ty + FONT_HEIGHT <= c.h)
|
||||||
draw_text_to_pixels(pixels, cw, ch, tx, ty, fm->entry_names[i], text_px);
|
c.text(tx, ty, fm->entry_names[i], colors::TEXT_COLOR);
|
||||||
|
|
||||||
// Size
|
// 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];
|
char size_str[16];
|
||||||
format_size(size_str, fm->entry_sizes[i]);
|
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
|
// 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";
|
const char* type_str = "File";
|
||||||
if (fm->entry_types[i] == 1) type_str = "Dir";
|
if (fm->entry_types[i] == 1) type_str = "Dir";
|
||||||
else if (fm->entry_types[i] == 2) type_str = "Exec";
|
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 ----
|
// ---- Scrollbar ----
|
||||||
// Draw scrollbar directly to pixels
|
|
||||||
if (fm->scrollbar.content_height > fm->scrollbar.view_height) {
|
if (fm->scrollbar.content_height > fm->scrollbar.view_height) {
|
||||||
uint32_t sb_bg = colors::SCROLLBAR_BG.to_pixel();
|
Color sb_fg_color = (fm->scrollbar.hovered || fm->scrollbar.dragging)
|
||||||
uint32_t sb_fg = (fm->scrollbar.hovered || fm->scrollbar.dragging)
|
? fm->scrollbar.hover_fg : fm->scrollbar.fg;
|
||||||
? fm->scrollbar.hover_fg.to_pixel() : fm->scrollbar.fg.to_pixel();
|
|
||||||
|
|
||||||
int sbx = fm->scrollbar.bounds.x;
|
int sbx = fm->scrollbar.bounds.x;
|
||||||
int sby = fm->scrollbar.bounds.y;
|
int sby = fm->scrollbar.bounds.y;
|
||||||
int sbw = fm->scrollbar.bounds.w;
|
int sbw = fm->scrollbar.bounds.w;
|
||||||
int sbh = fm->scrollbar.bounds.h;
|
int sbh = fm->scrollbar.bounds.h;
|
||||||
|
|
||||||
for (int y = sby; y < sby + sbh && y < ch; y++)
|
c.fill_rect(sbx, sby, sbw, sbh, colors::SCROLLBAR_BG);
|
||||||
for (int x = sbx; x < sbx + sbw && x < cw; x++)
|
|
||||||
pixels[y * cw + x] = sb_bg;
|
|
||||||
|
|
||||||
int th = fm->scrollbar.thumb_height();
|
int th = fm->scrollbar.thumb_height();
|
||||||
int tty = fm->scrollbar.thumb_y();
|
int tty = fm->scrollbar.thumb_y();
|
||||||
for (int y = tty; y < tty + th && y < sby + sbh && y < ch; y++)
|
c.fill_rect(sbx + 1, tty, sbw - 2, th, sb_fg_color);
|
||||||
for (int x = sbx + 1; x < sbx + sbw - 1 && x < cw; x++)
|
|
||||||
pixels[y * cw + x] = sb_fg;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
SysInfoState* si = (SysInfoState*)win->app_data;
|
||||||
if (!si) return;
|
if (!si) return;
|
||||||
|
|
||||||
// Refresh uptime
|
|
||||||
si->uptime_ms = zenith::get_milliseconds();
|
si->uptime_ms = zenith::get_milliseconds();
|
||||||
|
|
||||||
Rect cr = win->content_rect();
|
Canvas c(win);
|
||||||
int cw = cr.w;
|
c.fill(colors::WINDOW_BG);
|
||||||
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;
|
|
||||||
|
|
||||||
uint32_t text_px = colors::TEXT_COLOR.to_pixel();
|
|
||||||
uint32_t accent_px = colors::ACCENT.to_pixel();
|
|
||||||
int y = 16;
|
int y = 16;
|
||||||
int x = 16;
|
int x = 16;
|
||||||
char line[128];
|
char line[128];
|
||||||
|
|
||||||
// Title
|
// 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;
|
y += FONT_HEIGHT + 12;
|
||||||
|
|
||||||
// Separator
|
// Separator
|
||||||
uint32_t sep_px = colors::BORDER.to_pixel();
|
c.hline(x, y, c.w - 2 * x, colors::BORDER);
|
||||||
for (int sx = x; sx < cw - x && y < ch; sx++)
|
|
||||||
pixels[y * cw + sx] = sep_px;
|
|
||||||
y += 8;
|
y += 8;
|
||||||
|
|
||||||
// OS Name
|
// OS Name
|
||||||
snprintf(line, sizeof(line), "OS: %s", si->sys_info.osName);
|
snprintf(line, sizeof(line), "OS: %s", si->sys_info.osName);
|
||||||
draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px);
|
c.kv_line(x, &y, line, colors::TEXT_COLOR);
|
||||||
y += FONT_HEIGHT + 6;
|
|
||||||
|
|
||||||
// OS Version
|
// OS Version
|
||||||
snprintf(line, sizeof(line), "Version: %s", si->sys_info.osVersion);
|
snprintf(line, sizeof(line), "Version: %s", si->sys_info.osVersion);
|
||||||
draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px);
|
c.kv_line(x, &y, line, colors::TEXT_COLOR);
|
||||||
y += FONT_HEIGHT + 6;
|
|
||||||
|
|
||||||
// API Version
|
// API Version
|
||||||
snprintf(line, sizeof(line), "API: %d", (int)si->sys_info.apiVersion);
|
snprintf(line, sizeof(line), "API: %d", (int)si->sys_info.apiVersion);
|
||||||
draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px);
|
c.kv_line(x, &y, line, colors::TEXT_COLOR);
|
||||||
y += FONT_HEIGHT + 6;
|
|
||||||
|
|
||||||
// Max Processes
|
// Max Processes
|
||||||
snprintf(line, sizeof(line), "Max PIDs: %d", (int)si->sys_info.maxProcesses);
|
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;
|
y += FONT_HEIGHT + 12;
|
||||||
|
|
||||||
// Uptime
|
// Uptime
|
||||||
@@ -73,15 +59,14 @@ static void sysinfo_on_draw(Window* win, Framebuffer& fb) {
|
|||||||
int up_min = up_sec / 60;
|
int up_min = up_sec / 60;
|
||||||
int up_hr = up_min / 60;
|
int up_hr = up_min / 60;
|
||||||
snprintf(line, sizeof(line), "Uptime: %d:%02d:%02d", up_hr, up_min % 60, up_sec % 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;
|
y += FONT_HEIGHT + 12;
|
||||||
|
|
||||||
// Network section
|
// 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;
|
y += FONT_HEIGHT + 8;
|
||||||
|
|
||||||
for (int sx = x; sx < cw - x && y < ch; sx++)
|
c.hline(x, y, c.w - 2 * x, colors::BORDER);
|
||||||
pixels[y * cw + sx] = sep_px;
|
|
||||||
y += 8;
|
y += 8;
|
||||||
|
|
||||||
// IP Address
|
// IP Address
|
||||||
@@ -89,39 +74,35 @@ static void sysinfo_on_draw(Window* win, Framebuffer& fb) {
|
|||||||
snprintf(line, sizeof(line), "IP: %d.%d.%d.%d",
|
snprintf(line, sizeof(line), "IP: %d.%d.%d.%d",
|
||||||
(int)(ip & 0xFF), (int)((ip >> 8) & 0xFF),
|
(int)(ip & 0xFF), (int)((ip >> 8) & 0xFF),
|
||||||
(int)((ip >> 16) & 0xFF), (int)((ip >> 24) & 0xFF));
|
(int)((ip >> 16) & 0xFF), (int)((ip >> 24) & 0xFF));
|
||||||
draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px);
|
c.kv_line(x, &y, line, colors::TEXT_COLOR);
|
||||||
y += FONT_HEIGHT + 6;
|
|
||||||
|
|
||||||
// Subnet
|
// Subnet
|
||||||
uint32_t mask = si->net_cfg.subnetMask;
|
uint32_t mask = si->net_cfg.subnetMask;
|
||||||
snprintf(line, sizeof(line), "Subnet: %d.%d.%d.%d",
|
snprintf(line, sizeof(line), "Subnet: %d.%d.%d.%d",
|
||||||
(int)(mask & 0xFF), (int)((mask >> 8) & 0xFF),
|
(int)(mask & 0xFF), (int)((mask >> 8) & 0xFF),
|
||||||
(int)((mask >> 16) & 0xFF), (int)((mask >> 24) & 0xFF));
|
(int)((mask >> 16) & 0xFF), (int)((mask >> 24) & 0xFF));
|
||||||
draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px);
|
c.kv_line(x, &y, line, colors::TEXT_COLOR);
|
||||||
y += FONT_HEIGHT + 6;
|
|
||||||
|
|
||||||
// Gateway
|
// Gateway
|
||||||
uint32_t gw = si->net_cfg.gateway;
|
uint32_t gw = si->net_cfg.gateway;
|
||||||
snprintf(line, sizeof(line), "Gateway: %d.%d.%d.%d",
|
snprintf(line, sizeof(line), "Gateway: %d.%d.%d.%d",
|
||||||
(int)(gw & 0xFF), (int)((gw >> 8) & 0xFF),
|
(int)(gw & 0xFF), (int)((gw >> 8) & 0xFF),
|
||||||
(int)((gw >> 16) & 0xFF), (int)((gw >> 24) & 0xFF));
|
(int)((gw >> 16) & 0xFF), (int)((gw >> 24) & 0xFF));
|
||||||
draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px);
|
c.kv_line(x, &y, line, colors::TEXT_COLOR);
|
||||||
y += FONT_HEIGHT + 6;
|
|
||||||
|
|
||||||
// DNS
|
// DNS
|
||||||
uint32_t dns = si->net_cfg.dnsServer;
|
uint32_t dns = si->net_cfg.dnsServer;
|
||||||
snprintf(line, sizeof(line), "DNS: %d.%d.%d.%d",
|
snprintf(line, sizeof(line), "DNS: %d.%d.%d.%d",
|
||||||
(int)(dns & 0xFF), (int)((dns >> 8) & 0xFF),
|
(int)(dns & 0xFF), (int)((dns >> 8) & 0xFF),
|
||||||
(int)((dns >> 16) & 0xFF), (int)((dns >> 24) & 0xFF));
|
(int)((dns >> 16) & 0xFF), (int)((dns >> 24) & 0xFF));
|
||||||
draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px);
|
c.kv_line(x, &y, line, colors::TEXT_COLOR);
|
||||||
y += FONT_HEIGHT + 6;
|
|
||||||
|
|
||||||
// MAC Address
|
// MAC Address
|
||||||
snprintf(line, sizeof(line), "MAC: %02x:%02x:%02x:%02x:%02x:%02x",
|
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[0], (unsigned)si->net_cfg.macAddress[1],
|
||||||
(unsigned)si->net_cfg.macAddress[2], (unsigned)si->net_cfg.macAddress[3],
|
(unsigned)si->net_cfg.macAddress[2], (unsigned)si->net_cfg.macAddress[3],
|
||||||
(unsigned)si->net_cfg.macAddress[4], (unsigned)si->net_cfg.macAddress[5]);
|
(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) {
|
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;
|
TextEditorState* te = (TextEditorState*)win->app_data;
|
||||||
if (!te) return;
|
if (!te) return;
|
||||||
|
|
||||||
Rect cr = win->content_rect();
|
Canvas c(win);
|
||||||
int cw = cr.w;
|
c.fill(colors::WINDOW_BG);
|
||||||
int ch = cr.h;
|
|
||||||
uint32_t* pixels = win->content;
|
|
||||||
|
|
||||||
// Background
|
int text_area_h = c.h - TE_STATUS_H;
|
||||||
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 visible_lines = text_area_h / FONT_HEIGHT;
|
int visible_lines = text_area_h / FONT_HEIGHT;
|
||||||
|
|
||||||
te_ensure_cursor_visible(te, visible_lines);
|
te_ensure_cursor_visible(te, visible_lines);
|
||||||
|
|
||||||
// Line number gutter background
|
// Line number gutter background
|
||||||
uint32_t gutter_px = Color::from_rgb(0xF0, 0xF0, 0xF0).to_pixel();
|
c.fill_rect(0, 0, TE_LINE_NUM_W, text_area_h, Color::from_rgb(0xF0, 0xF0, 0xF0));
|
||||||
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;
|
|
||||||
|
|
||||||
// Gutter separator
|
// Gutter separator
|
||||||
uint32_t sep_px = colors::BORDER.to_pixel();
|
c.vline(TE_LINE_NUM_W, 0, text_area_h, colors::BORDER);
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw lines
|
// 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 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;
|
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
|
// Cursor line highlighting
|
||||||
if (line == te->cursor_line) {
|
if (line == te->cursor_line) {
|
||||||
for (int y = py; y < py + FONT_HEIGHT && y < text_area_h; y++)
|
int hl_h = gui_min(FONT_HEIGHT, text_area_h - py);
|
||||||
for (int x = TE_LINE_NUM_W + 1; x < cw; x++)
|
if (hl_h > 0)
|
||||||
pixels[y * cw + x] = cursor_line_px;
|
c.fill_rect(TE_LINE_NUM_W + 1, py, c.w - TE_LINE_NUM_W - 1, hl_h, cursor_line_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Line number
|
// Line number
|
||||||
char num_str[8];
|
char num_str[8];
|
||||||
snprintf(num_str, 8, "%4d", line + 1);
|
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_start = te->line_offsets[line];
|
||||||
int line_len = te_line_length(te, line);
|
int line_len = te_line_length(te, line);
|
||||||
|
|
||||||
for (int ci = 0; ci < line_len; ci++) {
|
for (int ci = 0; ci < line_len; ci++) {
|
||||||
int px = text_start_x + ci * FONT_WIDTH - te->scroll_x;
|
int px = text_start_x + ci * FONT_WIDTH - te->scroll_x;
|
||||||
if (px + FONT_WIDTH <= TE_LINE_NUM_W + 1) continue;
|
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];
|
char ch = te->buffer[line_start + ci];
|
||||||
if (c >= 32 || c < 0) {
|
if (ch >= 32 || ch < 0) {
|
||||||
const uint8_t* glyph = &font_data[(unsigned char)c * FONT_HEIGHT];
|
const uint8_t* glyph = &font_data[(unsigned char)ch * FONT_HEIGHT];
|
||||||
for (int fy = 0; fy < FONT_HEIGHT && py + fy < text_area_h; fy++) {
|
for (int fy = 0; fy < FONT_HEIGHT && py + fy < text_area_h; fy++) {
|
||||||
uint8_t bits = glyph[fy];
|
uint8_t bits = glyph[fy];
|
||||||
for (int fx = 0; fx < FONT_WIDTH; fx++) {
|
for (int fx = 0; fx < FONT_WIDTH; fx++) {
|
||||||
if (bits & (0x80 >> fx)) {
|
if (bits & (0x80 >> fx)) {
|
||||||
int dx = px + fx;
|
int dx = px + fx;
|
||||||
int dy = py + fy;
|
int dy = py + fy;
|
||||||
if (dx > TE_LINE_NUM_W && dx < cw && dy >= 0 && dy < text_area_h)
|
if (dx > TE_LINE_NUM_W && dx < c.w && dy >= 0 && dy < text_area_h)
|
||||||
pixels[dy * cw + dx] = text_px;
|
c.pixels[dy * c.w + dx] = text_px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -370,24 +356,17 @@ static void texteditor_on_draw(Window* win, Framebuffer& fb) {
|
|||||||
// Draw cursor
|
// Draw cursor
|
||||||
if (line == te->cursor_line) {
|
if (line == te->cursor_line) {
|
||||||
int cx = text_start_x + te->cursor_col * FONT_WIDTH - te->scroll_x;
|
int cx = text_start_x + te->cursor_col * FONT_WIDTH - te->scroll_x;
|
||||||
if (cx > TE_LINE_NUM_W && cx + 2 <= cw) {
|
if (cx > TE_LINE_NUM_W && cx + 2 <= c.w) {
|
||||||
for (int y = py; y < py + FONT_HEIGHT && y < text_area_h; y++) {
|
int cur_h = gui_min(FONT_HEIGHT, text_area_h - py);
|
||||||
pixels[y * cw + cx] = cursor_px;
|
if (cur_h > 0)
|
||||||
if (cx + 1 < cw)
|
c.fill_rect(cx, py, 2, cur_h, colors::ACCENT);
|
||||||
pixels[y * cw + cx + 1] = cursor_px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Status bar ----
|
// ---- Status bar ----
|
||||||
int status_y = ch - TE_STATUS_H;
|
int status_y = c.h - TE_STATUS_H;
|
||||||
uint32_t status_bg = Color::from_rgb(0x2B, 0x3E, 0x50).to_pixel();
|
c.fill_rect(0, status_y, c.w, TE_STATUS_H, Color::from_rgb(0x2B, 0x3E, 0x50));
|
||||||
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;
|
|
||||||
|
|
||||||
// Filename + modified flag
|
// Filename + modified flag
|
||||||
char status_left[128];
|
char status_left[128];
|
||||||
@@ -396,16 +375,13 @@ static void texteditor_on_draw(Window* win, Framebuffer& fb) {
|
|||||||
} else {
|
} else {
|
||||||
snprintf(status_left, 128, " Untitled%s", te->modified ? " [modified]" : "");
|
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,
|
c.text(4, status_y + (TE_STATUS_H - FONT_HEIGHT) / 2, status_left, colors::PANEL_TEXT);
|
||||||
status_left, status_fg);
|
|
||||||
|
|
||||||
// Cursor position (right side)
|
// Cursor position (right side)
|
||||||
char status_right[32];
|
char status_right[32];
|
||||||
snprintf(status_right, 32, "Ln %d, Col %d ", te->cursor_line + 1, te->cursor_col + 1);
|
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;
|
int sr_w = zenith::slen(status_right) * FONT_WIDTH;
|
||||||
draw_text_to_pixels(pixels, cw, ch, cw - sr_w - 4,
|
c.text(c.w - sr_w - 4, status_y + (TE_STATUS_H - FONT_HEIGHT) / 2, status_right, colors::PANEL_TEXT);
|
||||||
status_y + (TE_STATUS_H - FONT_HEIGHT) / 2,
|
|
||||||
status_right, status_fg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ static constexpr int WIKI_SCROLLBAR_W = 12;
|
|||||||
|
|
||||||
struct WikiDisplayLine {
|
struct WikiDisplayLine {
|
||||||
char text[256];
|
char text[256];
|
||||||
uint32_t color;
|
Color color;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct WikiState {
|
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)
|
// 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;
|
if (ws->lineCount >= ws->lineCap) return;
|
||||||
WikiDisplayLine* dl = &ws->lines[ws->lineCount];
|
WikiDisplayLine* dl = &ws->lines[ws->lineCount];
|
||||||
int copyLen = len;
|
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,
|
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;
|
if (textLen <= 0 || maxChars <= 0) return;
|
||||||
const char* p = text;
|
const char* p = text;
|
||||||
const char* end = text + textLen;
|
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;
|
int maxChars = (contentW - 24 - WIKI_SCROLLBAR_W) / FONT_WIDTH;
|
||||||
if (maxChars < 20) maxChars = 20;
|
if (maxChars < 20) maxChars = 20;
|
||||||
|
|
||||||
uint32_t accent_px = colors::ACCENT.to_pixel();
|
Color accent_c = colors::ACCENT;
|
||||||
uint32_t green_px = Color::from_rgb(0x2E, 0x7D, 0x32).to_pixel();
|
Color green_c = Color::from_rgb(0x2E, 0x7D, 0x32);
|
||||||
uint32_t text_px = colors::TEXT_COLOR.to_pixel();
|
Color text_c = colors::TEXT_COLOR;
|
||||||
|
|
||||||
// Title
|
// Title
|
||||||
if (title && title[0]) {
|
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
|
// Blank separator
|
||||||
if (ws->lineCount > 0)
|
if (ws->lineCount > 0)
|
||||||
wiki_add_line(ws, "", 0, text_px);
|
wiki_add_line(ws, "", 0, text_c);
|
||||||
|
|
||||||
// Process extract line by line
|
// Process extract line by line
|
||||||
const char* p = extract;
|
const char* p = extract;
|
||||||
@@ -195,7 +195,7 @@ static void wiki_build_display(WikiState* ws, const char* title,
|
|||||||
if (p < end) p++;
|
if (p < end) p++;
|
||||||
|
|
||||||
if (lineLen == 0) {
|
if (lineLen == 0) {
|
||||||
wiki_add_line(ws, "", 0, text_px);
|
wiki_add_line(ws, "", 0, text_c);
|
||||||
continue;
|
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--;
|
||||||
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) {
|
if (ei > si) {
|
||||||
char secBuf[256];
|
char secBuf[256];
|
||||||
int secLen = ei - si;
|
int secLen = ei - si;
|
||||||
if (secLen > 255) secLen = 255;
|
if (secLen > 255) secLen = 255;
|
||||||
for (int i = 0; i < secLen; i++) secBuf[i] = lineStart[si + i];
|
for (int i = 0; i < secLen; i++) secBuf[i] = lineStart[si + i];
|
||||||
secBuf[secLen] = '\0';
|
secBuf[secLen] = '\0';
|
||||||
wiki_add_line(ws, secBuf, secLen, green_px);
|
wiki_add_line(ws, secBuf, secLen, green_c);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regular text
|
// 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;
|
WikiState* ws = (WikiState*)win->app_data;
|
||||||
if (!ws) return;
|
if (!ws) return;
|
||||||
|
|
||||||
Rect cr = win->content_rect();
|
Canvas c(win);
|
||||||
int cw = cr.w;
|
c.fill(colors::WINDOW_BG);
|
||||||
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;
|
|
||||||
|
|
||||||
// ---- Toolbar background ----
|
// ---- Toolbar background ----
|
||||||
uint32_t toolbar_bg = Color::from_rgb(0xF5, 0xF5, 0xF5).to_pixel();
|
c.fill_rect(0, 0, c.w, WIKI_TOOLBAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5));
|
||||||
for (int y = 0; y < WIKI_TOOLBAR_H && y < ch; y++)
|
|
||||||
for (int x = 0; x < cw; x++)
|
|
||||||
pixels[y * cw + x] = toolbar_bg;
|
|
||||||
|
|
||||||
// Toolbar separator line
|
// Toolbar separator line
|
||||||
uint32_t sep_px = colors::BORDER.to_pixel();
|
c.hline(0, WIKI_TOOLBAR_H, c.w, colors::BORDER);
|
||||||
if (WIKI_TOOLBAR_H < ch)
|
|
||||||
for (int x = 0; x < cw; x++)
|
|
||||||
pixels[WIKI_TOOLBAR_H * cw + x] = sep_px;
|
|
||||||
|
|
||||||
// ---- Draw search box outline ----
|
// ---- Draw search box outline ----
|
||||||
int tb_x = 8;
|
int tb_x = 8;
|
||||||
int tb_y = 6;
|
int tb_y = 6;
|
||||||
int tb_w = cw - 90;
|
int tb_w = c.w - 90;
|
||||||
int tb_h = 24;
|
int tb_h = 24;
|
||||||
if (tb_w < 100) tb_w = 100;
|
if (tb_w < 100) tb_w = 100;
|
||||||
|
|
||||||
// TextBox background
|
// TextBox background + border
|
||||||
uint32_t white_px = colors::WHITE.to_pixel();
|
c.fill_rect(tb_x, tb_y, tb_w, tb_h, colors::WHITE);
|
||||||
uint32_t border_px = colors::BORDER.to_pixel();
|
c.rect(tb_x, tb_y, tb_w, tb_h, colors::BORDER);
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search text
|
// Search text
|
||||||
uint32_t text_px = colors::TEXT_COLOR.to_pixel();
|
c.text(tb_x + 4, tb_y + (tb_h - FONT_HEIGHT) / 2, ws->searchQuery, colors::TEXT_COLOR);
|
||||||
draw_text_to_pixels(pixels, cw, ch, tb_x + 4, tb_y + (tb_h - FONT_HEIGHT) / 2,
|
|
||||||
ws->searchQuery, text_px);
|
|
||||||
|
|
||||||
// ---- Search button ----
|
// ---- Search button ----
|
||||||
int btn_x = tb_x + tb_w + 6;
|
int btn_x = tb_x + tb_w + 6;
|
||||||
int btn_y = tb_y;
|
int btn_y = tb_y;
|
||||||
int btn_w = 66;
|
int btn_w = 66;
|
||||||
int btn_h = tb_h;
|
int btn_h = tb_h;
|
||||||
uint32_t accent_px = colors::ACCENT.to_pixel();
|
c.button(btn_x, btn_y, btn_w, btn_h, "Search", colors::ACCENT, colors::WHITE, 0);
|
||||||
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);
|
|
||||||
|
|
||||||
// ---- Content area ----
|
// ---- Content area ----
|
||||||
int content_y = WIKI_TOOLBAR_H + 1;
|
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);
|
int visibleLines = content_h / (FONT_HEIGHT + 4);
|
||||||
if (visibleLines < 1) visibleLines = 1;
|
if (visibleLines < 1) visibleLines = 1;
|
||||||
|
|
||||||
if (ws->mode == WikiState::FETCHING) {
|
if (ws->mode == WikiState::FETCHING) {
|
||||||
draw_text_to_pixels(pixels, cw, ch, 16, content_y + 16,
|
c.text(16, content_y + 16, "Loading...", Color::from_rgb(0x88, 0x88, 0x88));
|
||||||
"Loading...", Color::from_rgb(0x88, 0x88, 0x88).to_pixel());
|
|
||||||
} else if (ws->mode == WikiState::WIKI_ERROR) {
|
} else if (ws->mode == WikiState::WIKI_ERROR) {
|
||||||
draw_text_to_pixels(pixels, cw, ch, 16, content_y + 16,
|
c.text(16, content_y + 16, ws->statusMsg, colors::CLOSE_BTN);
|
||||||
ws->statusMsg, colors::CLOSE_BTN.to_pixel());
|
|
||||||
} else if (ws->mode == WikiState::IDLE) {
|
} else if (ws->mode == WikiState::IDLE) {
|
||||||
draw_text_to_pixels(pixels, cw, ch, 16, content_y + 16,
|
c.text(16, content_y + 16,
|
||||||
"Type a topic and press Enter or click Search.",
|
"Type a topic and press Enter or click Search.",
|
||||||
Color::from_rgb(0x88, 0x88, 0x88).to_pixel());
|
Color::from_rgb(0x88, 0x88, 0x88));
|
||||||
} else if (ws->mode == WikiState::DONE && ws->lineCount > 0) {
|
} else if (ws->mode == WikiState::DONE && ws->lineCount > 0) {
|
||||||
int y = content_y + 8;
|
int y = content_y + 8;
|
||||||
int lineH = FONT_HEIGHT + 4;
|
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];
|
WikiDisplayLine* dl = &ws->lines[i];
|
||||||
if (dl->text[0] != '\0') {
|
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;
|
y += lineH;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Scrollbar ----
|
// ---- Scrollbar ----
|
||||||
if (ws->lineCount > visibleLines) {
|
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_y = content_y;
|
||||||
int sb_h = content_h;
|
int sb_h = content_h;
|
||||||
|
|
||||||
// Scrollbar track
|
c.fill_rect(sb_x, sb_y, WIKI_SCROLLBAR_W, sb_h, colors::SCROLLBAR_BG);
|
||||||
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;
|
|
||||||
|
|
||||||
// Thumb
|
// Thumb
|
||||||
int maxScroll = ws->lineCount - visibleLines;
|
int maxScroll = ws->lineCount - visibleLines;
|
||||||
@@ -393,10 +350,7 @@ static void wiki_on_draw(Window* win, Framebuffer& fb) {
|
|||||||
if (thumbH < 20) thumbH = 20;
|
if (thumbH < 20) thumbH = 20;
|
||||||
int thumbY = sb_y + (ws->scrollY * (sb_h - thumbH)) / maxScroll;
|
int thumbY = sb_y + (ws->scrollY * (sb_h - thumbH)) / maxScroll;
|
||||||
|
|
||||||
uint32_t sb_fg = colors::SCROLLBAR_FG.to_pixel();
|
c.fill_rect(sb_x + 2, thumbY, WIKI_SCROLLBAR_W - 4, thumbH, colors::SCROLLBAR_FG);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
#include <gui/svg.hpp>
|
#include <gui/svg.hpp>
|
||||||
#include <gui/widgets.hpp>
|
#include <gui/widgets.hpp>
|
||||||
#include <gui/window.hpp>
|
#include <gui/window.hpp>
|
||||||
|
#include <gui/canvas.hpp>
|
||||||
#include <gui/terminal.hpp>
|
#include <gui/terminal.hpp>
|
||||||
#include <gui/desktop.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]);
|
(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
|
// 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
|
// 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_texteditor_with_file(DesktopState* ds, const char* path);
|
||||||
void open_klog(DesktopState* ds);
|
void open_klog(DesktopState* ds);
|
||||||
void open_wiki(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_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_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;
|
ds->net_popup_open = false;
|
||||||
zenith::get_netcfg(&ds->cached_net_cfg);
|
zenith::get_netcfg(&ds->cached_net_cfg);
|
||||||
ds->net_cfg_last_poll = zenith::get_milliseconds();
|
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);
|
draw_text(fb, title_x, title_y, win->title, colors::TEXT_COLOR);
|
||||||
|
|
||||||
// Call app draw callback to render content
|
// Call app draw callback to render content (skip during resize — buffer is old size)
|
||||||
if (win->on_draw) {
|
if (win->on_draw && !win->resizing) {
|
||||||
win->on_draw(win, fb);
|
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();
|
Rect cr = win->content_rect();
|
||||||
if (win->content) {
|
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)
|
// 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_W = 220;
|
||||||
static constexpr int MENU_ITEM_H = 40;
|
static constexpr int MENU_ITEM_H = 40;
|
||||||
|
|
||||||
@@ -371,6 +380,8 @@ static void desktop_draw_app_menu(DesktopState* ds) {
|
|||||||
{ "Text Editor", &ds->icon_texteditor },
|
{ "Text Editor", &ds->icon_texteditor },
|
||||||
{ "Kernel Log", &ds->icon_terminal },
|
{ "Kernel Log", &ds->icon_terminal },
|
||||||
{ "Wikipedia", &ds->icon_wikipedia },
|
{ "Wikipedia", &ds->icon_wikipedia },
|
||||||
|
{ "About", &ds->icon_settings },
|
||||||
|
{ "Reboot", &ds->icon_reboot },
|
||||||
};
|
};
|
||||||
|
|
||||||
int mx = ds->mouse.x;
|
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++) {
|
for (int i = 0; i < MENU_ITEM_COUNT; i++) {
|
||||||
int iy = menu_y + 4 + i * MENU_ITEM_H;
|
int iy = menu_y + 4 + i * MENU_ITEM_H;
|
||||||
|
|
||||||
// Thin separator line after System Info (before utility apps)
|
// Thin separator lines before utility apps and before Settings
|
||||||
if (i == 3) {
|
if (i == 3 || i == 7) {
|
||||||
int sep_y = iy - 1;
|
int sep_y = iy - 1;
|
||||||
for (int sx = menu_x + 8; sx < menu_x + MENU_W - 8; sx++)
|
for (int sx = menu_x + 8; sx < menu_x + MENU_W - 8; sx++)
|
||||||
fb.put_pixel(sx, sep_y, colors::BORDER);
|
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);
|
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) {
|
void gui::desktop_compose(DesktopState* ds) {
|
||||||
Framebuffer& fb = ds->fb;
|
Framebuffer& fb = ds->fb;
|
||||||
|
|
||||||
@@ -508,8 +677,91 @@ void gui::desktop_compose(DesktopState* ds) {
|
|||||||
desktop_draw_net_popup(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 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) {
|
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_pressed = (buttons & 0x01) && !(prev & 0x01);
|
||||||
bool left_held = (buttons & 0x01);
|
bool left_held = (buttons & 0x01);
|
||||||
bool left_released = !(buttons & 0x01) && (prev & 0x01);
|
bool left_released = !(buttons & 0x01) && (prev & 0x01);
|
||||||
|
bool right_pressed = (buttons & 0x02) && !(prev & 0x02);
|
||||||
|
|
||||||
MouseEvent ev;
|
MouseEvent ev;
|
||||||
ev.x = mx;
|
ev.x = mx;
|
||||||
@@ -528,6 +781,42 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
|||||||
ev.prev_buttons = prev;
|
ev.prev_buttons = prev;
|
||||||
ev.scroll = ds->mouse.scrollDelta;
|
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
|
// Check for ongoing window drags first
|
||||||
for (int i = 0; i < ds->window_count; i++) {
|
for (int i = 0; i < ds->window_count; i++) {
|
||||||
Window* win = &ds->windows[i];
|
Window* win = &ds->windows[i];
|
||||||
@@ -542,6 +831,88 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
|||||||
}
|
}
|
||||||
if (left_released) {
|
if (left_released) {
|
||||||
win->dragging = false;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
@@ -566,6 +937,8 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
|||||||
case 4: open_texteditor(ds); break;
|
case 4: open_texteditor(ds); break;
|
||||||
case 5: open_klog(ds); break;
|
case 5: open_klog(ds); break;
|
||||||
case 6: open_wiki(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;
|
ds->app_menu_open = false;
|
||||||
}
|
}
|
||||||
@@ -597,6 +970,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
|||||||
if (mx < 36) {
|
if (mx < 36) {
|
||||||
ds->app_menu_open = !ds->app_menu_open;
|
ds->app_menu_open = !ds->app_menu_open;
|
||||||
ds->net_popup_open = false;
|
ds->net_popup_open = false;
|
||||||
|
ds->ctx_menu_open = false;
|
||||||
return;
|
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)) {
|
if (ds->net_icon_rect.w > 0 && ds->net_icon_rect.contains(mx, my)) {
|
||||||
ds->net_popup_open = !ds->net_popup_open;
|
ds->net_popup_open = !ds->net_popup_open;
|
||||||
ds->app_menu_open = false;
|
ds->app_menu_open = false;
|
||||||
|
ds->ctx_menu_open = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Window indicator buttons
|
// Window indicator buttons
|
||||||
@@ -683,6 +1058,26 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
|||||||
return;
|
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)
|
// Check titlebar (start drag)
|
||||||
Rect tb = win->titlebar_rect();
|
Rect tb = win->titlebar_rect();
|
||||||
if (tb.contains(mx, my)) {
|
if (tb.contains(mx, my)) {
|
||||||
@@ -718,6 +1113,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ds->app_menu_open = false;
|
ds->app_menu_open = false;
|
||||||
|
ds->ctx_menu_open = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle scroll events on focused window
|
// Handle scroll events on focused window
|
||||||
@@ -728,6 +1124,26 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
|||||||
win->on_mouse(win, ev);
|
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) {
|
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.
BIN
Binary file not shown.
@@ -50,6 +50,8 @@ ICONS=(
|
|||||||
"devices/scalable/network-wired.svg"
|
"devices/scalable/network-wired.svg"
|
||||||
"mimetypes/scalable/text-x-generic.svg"
|
"mimetypes/scalable/text-x-generic.svg"
|
||||||
"mimetypes/scalable/application-x-executable.svg"
|
"mimetypes/scalable/application-x-executable.svg"
|
||||||
|
"categories/scalable/help-about.svg"
|
||||||
|
"categories/scalable/system-reboot.svg"
|
||||||
)
|
)
|
||||||
|
|
||||||
copied=0
|
copied=0
|
||||||
|
|||||||
Reference in New Issue
Block a user