feat: rudimentary userspace desktop environment

This commit is contained in:
2026-02-20 00:48:51 +01:00
parent 3753ebd4f4
commit 6eebc64863
27 changed files with 5027 additions and 9 deletions
+15
View File
@@ -57,6 +57,14 @@ namespace Zenith {
static constexpr uint64_t SYS_TERMSCALE = 43;
static constexpr uint64_t SYS_RESOLVE = 44;
static constexpr uint64_t SYS_GETRANDOM = 45;
static constexpr uint64_t SYS_KLOG = 46;
static constexpr uint64_t SYS_MOUSESTATE = 47;
static constexpr uint64_t SYS_SETMOUSEBOUNDS = 48;
static constexpr uint64_t SYS_SPAWN_REDIR = 49;
static constexpr uint64_t SYS_CHILDIO_READ = 50;
static constexpr uint64_t SYS_CHILDIO_WRITE = 51;
static constexpr uint64_t SYS_CHILDIO_WRITEKEY = 52;
static constexpr uint64_t SYS_CHILDIO_SETTERMSZ = 53;
static constexpr int SOCK_TCP = 1;
static constexpr int SOCK_UDP = 2;
@@ -103,4 +111,11 @@ namespace Zenith {
bool alt;
};
struct MouseState {
int32_t x;
int32_t y;
int32_t scrollDelta;
uint8_t buttons;
};
}
+55
View File
@@ -0,0 +1,55 @@
/*
* desktop.hpp
* ZenithOS desktop state and compositor declarations
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include "gui/gui.hpp"
#include "gui/framebuffer.hpp"
#include "gui/svg.hpp"
#include "gui/window.hpp"
#include "gui/widgets.hpp"
#include "gui/terminal.hpp"
#include <Api/Syscall.hpp>
namespace gui {
static constexpr int MAX_WINDOWS = 8;
static constexpr int PANEL_HEIGHT = 32;
struct DesktopState {
Framebuffer fb;
Window windows[MAX_WINDOWS];
int window_count;
int focused_window;
Zenith::MouseState mouse;
uint8_t prev_buttons;
bool app_menu_open;
SvgIcon icon_terminal;
SvgIcon icon_filemanager;
SvgIcon icon_sysinfo;
SvgIcon icon_appmenu;
SvgIcon icon_folder;
SvgIcon icon_file;
SvgIcon icon_computer;
int screen_w, screen_h;
};
// Forward declarations - implemented in main.cpp
void desktop_init(DesktopState* ds);
void desktop_run(DesktopState* ds);
void desktop_compose(DesktopState* ds);
int desktop_create_window(DesktopState* ds, const char* title, int x, int y, int w, int h);
void desktop_close_window(DesktopState* ds, int idx);
void desktop_raise_window(DesktopState* ds, int idx);
void desktop_draw_panel(DesktopState* ds);
void desktop_draw_window(DesktopState* ds, int idx);
void desktop_handle_mouse(DesktopState* ds);
void desktop_handle_keyboard(DesktopState* ds, const Zenith::KeyEvent& key);
} // namespace gui
+239
View File
@@ -0,0 +1,239 @@
/*
* draw.hpp
* ZenithOS drawing primitives (lines, circles, rounded rects, cursor)
* Copyright (c) 2025 Daniel Hammer
*/
#pragma once
#include "gui/gui.hpp"
#include "gui/framebuffer.hpp"
namespace gui {
// Fast horizontal line
inline void draw_hline(Framebuffer& fb, int x, int y, int w, Color c) {
for (int i = 0; i < w; i++) fb.put_pixel(x + i, y, c);
}
// Fast vertical line
inline void draw_vline(Framebuffer& fb, int x, int y, int h, Color c) {
for (int i = 0; i < h; i++) fb.put_pixel(x, y + i, c);
}
// Rectangle outline
inline void draw_rect(Framebuffer& fb, int x, int y, int w, int h, Color c) {
draw_hline(fb, x, y, w, c);
draw_hline(fb, x, y + h - 1, w, c);
draw_vline(fb, x, y, h, c);
draw_vline(fb, x + w - 1, y, h, c);
}
// Filled rounded rectangle using corner circles
inline void fill_rounded_rect(Framebuffer& fb, int x, int y, int w, int h, int radius, Color c) {
if (radius <= 0) {
fb.fill_rect(x, y, w, h, c);
return;
}
// Clamp radius to half the smaller dimension
int max_r = gui_min(w / 2, h / 2);
if (radius > max_r) radius = max_r;
// Fill the center rectangle
fb.fill_rect(x + radius, y, w - 2 * radius, h, c);
// Fill the left and right strips (excluding corners)
fb.fill_rect(x, y + radius, radius, h - 2 * radius, c);
fb.fill_rect(x + w - radius, y + radius, radius, h - 2 * radius, c);
// Draw the four rounded corners using midpoint circle
int cx_tl = x + radius;
int cy_tl = y + radius;
int cx_tr = x + w - radius - 1;
int cy_tr = y + radius;
int cx_bl = x + radius;
int cy_bl = y + h - radius - 1;
int cx_br = x + w - radius - 1;
int cy_br = y + h - radius - 1;
int px = 0;
int py = radius;
int d = 1 - radius;
while (px <= py) {
// Top-left corner
draw_hline(fb, cx_tl - py, cy_tl - px, py - radius + radius, c);
draw_hline(fb, cx_tl - px, cy_tl - py, px, c);
// Top-right corner
draw_hline(fb, cx_tr + 1, cy_tr - px, py, c);
draw_hline(fb, cx_tr + 1, cy_tr - py, px, c);
// Bottom-left corner
draw_hline(fb, cx_bl - py, cy_bl + px, py, c);
draw_hline(fb, cx_bl - px, cy_bl + py, px, c);
// Bottom-right corner
draw_hline(fb, cx_br + 1, cy_br + px, py, c);
draw_hline(fb, cx_br + 1, cy_br + py, px, c);
if (d < 0) {
d += 2 * px + 3;
} else {
d += 2 * (px - py) + 5;
py--;
}
px++;
}
}
// Filled circle (midpoint algorithm)
inline void fill_circle(Framebuffer& fb, int cx, int cy, int r, Color c) {
if (r <= 0) return;
int x = 0;
int y = r;
int d = 1 - r;
// Draw initial horizontal lines
draw_hline(fb, cx - r, cy, 2 * r + 1, c);
while (x < y) {
if (d < 0) {
d += 2 * x + 3;
} else {
d += 2 * (x - y) + 5;
y--;
draw_hline(fb, cx - x, cy + y + 1, 2 * x + 1, c);
draw_hline(fb, cx - x, cy - y - 1, 2 * x + 1, c);
}
x++;
draw_hline(fb, cx - y, cy + x, 2 * y + 1, c);
draw_hline(fb, cx - y, cy - x, 2 * y + 1, c);
}
}
// Circle outline (midpoint algorithm)
inline void draw_circle(Framebuffer& fb, int cx, int cy, int r, Color c) {
if (r <= 0) {
fb.put_pixel(cx, cy, c);
return;
}
int x = 0;
int y = r;
int d = 1 - r;
while (x <= y) {
fb.put_pixel(cx + x, cy + y, c);
fb.put_pixel(cx - x, cy + y, c);
fb.put_pixel(cx + x, cy - y, c);
fb.put_pixel(cx - x, cy - y, c);
fb.put_pixel(cx + y, cy + x, c);
fb.put_pixel(cx - y, cy + x, c);
fb.put_pixel(cx + y, cy - x, c);
fb.put_pixel(cx - y, cy - x, c);
if (d < 0) {
d += 2 * x + 3;
} else {
d += 2 * (x - y) + 5;
y--;
}
x++;
}
}
// Bresenham line drawing
inline void draw_line(Framebuffer& fb, int x0, int y0, int x1, int y1, Color c) {
int dx = gui_abs(x1 - x0);
int dy = gui_abs(y1 - y0);
int sx = x0 < x1 ? 1 : -1;
int sy = y0 < y1 ? 1 : -1;
int err = dx - dy;
for (;;) {
fb.put_pixel(x0, y0, c);
if (x0 == x1 && y0 == y1) break;
int e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x0 += sx;
}
if (e2 < dx) {
err += dx;
y0 += sy;
}
}
}
// Drop shadow: offset darker rectangles below/right
inline void draw_shadow(Framebuffer& fb, int x, int y, int w, int h, int offset, Color shadow_color) {
// Bottom shadow strip
fb.fill_rect_alpha(x + offset, y + h, w, offset, shadow_color);
// Right shadow strip
fb.fill_rect_alpha(x + w, y + offset, offset, h, shadow_color);
// Corner
fb.fill_rect_alpha(x + w, y + h, offset, offset, shadow_color);
}
// 16x16 mouse cursor bitmaps
// Outline (black, where design has '1')
static constexpr uint16_t cursor_outline[16] = {
0x8000, // 1000000000000000
0xC000, // 1100000000000000
0xA000, // 1010000000000000
0x9000, // 1001000000000000
0x8800, // 1000100000000000
0x8400, // 1000010000000000
0x8200, // 1000001000000000
0x8100, // 1000000100000000
0x8080, // 1000000010000000
0x8040, // 1000000001000000
0x8780, // 1000011110000000 (changed: row 10 = 1 2 2 2 2 2 1 1 1 1)
0x9200, // 1001001000000000 (row 11 = 1 2 2 1 2 2 1)
0xA900, // 1010100100000000 (row 12 = 1 2 1 0 1 2 2 1)
0xC900, // 1100100100000000 (row 13 = 1 1 0 0 1 2 2 1)
0x8480, // 1000010010000000 (row 14 = 1 0 0 0 0 1 2 2 1)
0x0700, // 0000011100000000 (row 15 = 0 0 0 0 0 1 1 1)
};
// Fill (white, where design has '2')
static constexpr uint16_t cursor_fill[16] = {
0x0000, // row 0: no fill
0x0000, // row 1: no fill
0x4000, // row 2: 0100000000000000
0x6000, // row 3: 0110000000000000
0x7000, // row 4: 0111000000000000
0x7800, // row 5: 0111100000000000
0x7C00, // row 6: 0111110000000000
0x7E00, // row 7: 0111111000000000
0x7F00, // row 8: 0111111100000000
0x7F80, // row 9: 0111111110000000
0x7800, // row 10: 0111100000000000 (fill only in positions 1-5)
0x6C00, // row 11: 0110110000000000 (fill at positions 1,2 and 4,5)
0x4600, // row 12: 0100011000000000 (fill at position 1 and 5,6)
0x0600, // row 13: 0000011000000000 (fill at positions 5,6)
0x0300, // row 14: 0000001100000000 (fill at positions 6,7)
0x0000, // row 15: no fill
};
// Draw the mouse cursor at (x, y)
inline void draw_cursor(Framebuffer& fb, int x, int y) {
Color black = colors::BLACK;
Color white = colors::WHITE;
for (int row = 0; row < 16; row++) {
uint16_t outline = cursor_outline[row];
uint16_t fill = cursor_fill[row];
for (int col = 0; col < 16; col++) {
uint16_t mask = (uint16_t)(0x8000 >> col);
if (outline & mask) {
fb.put_pixel(x + col, y + row, black);
} else if (fill & mask) {
fb.put_pixel(x + col, y + row, white);
}
}
}
}
} // namespace gui
+63
View File
@@ -0,0 +1,63 @@
/*
* font.hpp
* ZenithOS 8x16 VGA bitmap font rendering
* Copyright (c) 2025 Daniel Hammer
*/
#pragma once
#include "gui/gui.hpp"
#include "gui/framebuffer.hpp"
namespace gui {
static constexpr int FONT_WIDTH = 8;
static constexpr int FONT_HEIGHT = 16;
// Defined in font_data.cpp
extern const uint8_t font_data[256 * 16];
inline void draw_char(Framebuffer& fb, int x, int y, char c, Color fg) {
const uint8_t* glyph = &font_data[(unsigned char)c * FONT_HEIGHT];
for (int row = 0; row < FONT_HEIGHT; row++) {
uint8_t bits = glyph[row];
for (int col = 0; col < FONT_WIDTH; col++) {
if (bits & (0x80 >> col)) {
fb.put_pixel(x + col, y + row, fg);
}
}
}
}
inline void draw_char_bg(Framebuffer& fb, int x, int y, char c, Color fg, Color bg) {
const uint8_t* glyph = &font_data[(unsigned char)c * FONT_HEIGHT];
for (int row = 0; row < FONT_HEIGHT; row++) {
uint8_t bits = glyph[row];
for (int col = 0; col < FONT_WIDTH; col++) {
if (bits & (0x80 >> col)) {
fb.put_pixel(x + col, y + row, fg);
} else {
fb.put_pixel(x + col, y + row, bg);
}
}
}
}
inline void draw_text(Framebuffer& fb, int x, int y, const char* text, Color fg) {
for (int i = 0; text[i]; i++) {
draw_char(fb, x + i * FONT_WIDTH, y, text[i], fg);
}
}
inline void draw_text_bg(Framebuffer& fb, int x, int y, const char* text, Color fg, Color bg) {
for (int i = 0; text[i]; i++) {
draw_char_bg(fb, x + i * FONT_WIDTH, y, text[i], fg, bg);
}
}
inline int text_width(const char* text) {
int len = 0;
while (text[len]) len++;
return len * FONT_WIDTH;
}
} // namespace gui
+196
View File
@@ -0,0 +1,196 @@
/*
* framebuffer.hpp
* ZenithOS double-buffered framebuffer abstraction
* Copyright (c) 2025 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <zenith/syscall.h>
#include "gui/gui.hpp"
namespace gui {
class Framebuffer {
uint32_t* hw_fb;
uint32_t* back_buf;
int fb_width;
int fb_height;
int fb_pitch; // in bytes
public:
Framebuffer() : hw_fb(nullptr), back_buf(nullptr), fb_width(0), fb_height(0), fb_pitch(0) {
Zenith::FbInfo info;
zenith::fb_info(&info);
fb_width = (int)info.width;
fb_height = (int)info.height;
fb_pitch = (int)info.pitch;
hw_fb = (uint32_t*)zenith::fb_map();
back_buf = (uint32_t*)zenith::alloc((uint64_t)fb_height * fb_pitch);
}
int width() const { return fb_width; }
int height() const { return fb_height; }
int pitch() const { return fb_pitch; }
uint32_t* buffer() { return back_buf; }
inline void put_pixel(int x, int y, Color c) {
if (x < 0 || x >= fb_width || y < 0 || y >= fb_height) return;
uint32_t* row = (uint32_t*)((uint8_t*)back_buf + y * fb_pitch);
row[x] = c.to_pixel();
}
inline void put_pixel_alpha(int x, int y, Color c) {
if (x < 0 || x >= fb_width || y < 0 || y >= fb_height) return;
if (c.a == 0) return;
if (c.a == 255) {
put_pixel(x, y, c);
return;
}
uint32_t* row = (uint32_t*)((uint8_t*)back_buf + y * fb_pitch);
uint32_t dst = row[x];
uint8_t dr = (dst >> 16) & 0xFF;
uint8_t dg = (dst >> 8) & 0xFF;
uint8_t db = dst & 0xFF;
uint32_t a = c.a;
uint32_t inv_a = 255 - a;
// Fast alpha blend: out = (src * alpha + dst * (255 - alpha) + 128) / 255
// Approximation: (x + 1 + (x >> 8)) >> 8 for division by 255
uint32_t rr = a * c.r + inv_a * dr;
uint32_t gg = a * c.g + inv_a * dg;
uint32_t bb = a * c.b + inv_a * db;
rr = (rr + 1 + (rr >> 8)) >> 8;
gg = (gg + 1 + (gg >> 8)) >> 8;
bb = (bb + 1 + (bb >> 8)) >> 8;
row[x] = (0xFF000000) | (rr << 16) | (gg << 8) | bb;
}
inline void fill_rect(int x, int y, int w, int h, Color c) {
// Clip to screen bounds
int x0 = x < 0 ? 0 : x;
int y0 = y < 0 ? 0 : y;
int x1 = (x + w) > fb_width ? fb_width : (x + w);
int y1 = (y + h) > fb_height ? fb_height : (y + h);
if (x0 >= x1 || y0 >= y1) return;
uint32_t pixel = c.to_pixel();
int clipped_w = x1 - x0;
for (int row = y0; row < y1; row++) {
uint32_t* dst = (uint32_t*)((uint8_t*)back_buf + row * fb_pitch) + x0;
for (int col = 0; col < clipped_w; col++) {
dst[col] = pixel;
}
}
}
inline void fill_rect_alpha(int x, int y, int w, int h, Color c) {
if (c.a == 0) return;
if (c.a == 255) {
fill_rect(x, y, w, h, c);
return;
}
int x0 = x < 0 ? 0 : x;
int y0 = y < 0 ? 0 : y;
int x1 = (x + w) > fb_width ? fb_width : (x + w);
int y1 = (y + h) > fb_height ? fb_height : (y + h);
if (x0 >= x1 || y0 >= y1) return;
uint32_t a = c.a;
uint32_t inv_a = 255 - a;
uint32_t src_r = a * c.r;
uint32_t src_g = a * c.g;
uint32_t src_b = a * c.b;
for (int row = y0; row < y1; row++) {
uint32_t* dst = (uint32_t*)((uint8_t*)back_buf + row * fb_pitch) + x0;
for (int col = 0; col < x1 - x0; col++) {
uint32_t d = dst[col];
uint32_t dr = (d >> 16) & 0xFF;
uint32_t dg = (d >> 8) & 0xFF;
uint32_t db = d & 0xFF;
uint32_t rr = src_r + inv_a * dr;
uint32_t gg = src_g + inv_a * dg;
uint32_t bb = src_b + inv_a * db;
rr = (rr + 1 + (rr >> 8)) >> 8;
gg = (gg + 1 + (gg >> 8)) >> 8;
bb = (bb + 1 + (bb >> 8)) >> 8;
dst[col] = (0xFF000000) | (rr << 16) | (gg << 8) | bb;
}
}
}
inline void blit(int x, int y, int w, int h, const uint32_t* pixels) {
for (int row = 0; row < h; row++) {
int dy = y + row;
if (dy < 0 || dy >= fb_height) continue;
for (int col = 0; col < w; col++) {
int dx = x + col;
if (dx < 0 || dx >= fb_width) continue;
uint32_t* dst_row = (uint32_t*)((uint8_t*)back_buf + dy * fb_pitch);
dst_row[dx] = pixels[row * w + col];
}
}
}
inline void blit_alpha(int x, int y, int w, int h, const uint32_t* pixels) {
for (int row = 0; row < h; row++) {
int dy = y + row;
if (dy < 0 || dy >= fb_height) continue;
for (int col = 0; col < w; col++) {
int dx = x + col;
if (dx < 0 || dx >= fb_width) continue;
uint32_t src = pixels[row * w + col];
uint8_t sa = (src >> 24) & 0xFF;
if (sa == 0) continue;
uint8_t sr = (src >> 16) & 0xFF;
uint8_t sg = (src >> 8) & 0xFF;
uint8_t sb = src & 0xFF;
if (sa == 255) {
uint32_t* dst_row = (uint32_t*)((uint8_t*)back_buf + dy * fb_pitch);
dst_row[dx] = src;
continue;
}
Color sc = {sr, sg, sb, sa};
put_pixel_alpha(dx, dy, sc);
}
}
}
inline void clear(Color c) {
fill_rect(0, 0, fb_width, fb_height, c);
}
inline void flip() {
// Copy back buffer to hardware framebuffer, row by row (pitch may differ)
int row_pixels = fb_width;
for (int y = 0; y < fb_height; y++) {
uint32_t* src = (uint32_t*)((uint8_t*)back_buf + y * fb_pitch);
uint32_t* dst = (uint32_t*)((uint8_t*)hw_fb + y * fb_pitch);
for (int x = 0; x < row_pixels; x++) {
dst[x] = src[x];
}
}
}
};
} // namespace gui
+92
View File
@@ -0,0 +1,92 @@
/*
* gui.hpp
* ZenithOS core GUI types and utilities
* Copyright (c) 2025 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace gui {
// 16.16 fixed-point type
using fixed_t = int32_t;
static constexpr int FIXED_SHIFT = 16;
inline fixed_t int_to_fixed(int v) { return v << FIXED_SHIFT; }
inline int fixed_to_int(fixed_t v) { return v >> FIXED_SHIFT; }
inline fixed_t fixed_mul(fixed_t a, fixed_t b) { return (int32_t)(((int64_t)a * b) >> FIXED_SHIFT); }
inline fixed_t fixed_div(fixed_t a, fixed_t b) { return (int32_t)(((int64_t)a << FIXED_SHIFT) / b); }
inline fixed_t fixed_from_parts(int whole, int frac_num, int frac_den) {
return int_to_fixed(whole) + (int32_t)(((int64_t)frac_num << FIXED_SHIFT) / frac_den);
}
struct Color {
uint8_t r, g, b, a;
static Color from_rgb(uint8_t r, uint8_t g, uint8_t b) { return {r, g, b, 255}; }
static Color from_rgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a) { return {r, g, b, a}; }
static Color from_hex(uint32_t hex) {
return {(uint8_t)((hex >> 16) & 0xFF), (uint8_t)((hex >> 8) & 0xFF), (uint8_t)(hex & 0xFF), 255};
}
uint32_t to_pixel() const { return ((uint32_t)a << 24) | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b; }
};
// Named colors for the desktop theme
namespace colors {
static constexpr Color DESKTOP_BG = {0xE0, 0xE0, 0xE0, 0xFF};
static constexpr Color PANEL_BG = {0x2B, 0x3E, 0x50, 0xFF};
static constexpr Color TITLEBAR_BG = {0xF5, 0xF5, 0xF5, 0xFF};
static constexpr Color WINDOW_BG = {0xFF, 0xFF, 0xFF, 0xFF};
static constexpr Color BORDER = {0xCC, 0xCC, 0xCC, 0xFF};
static constexpr Color TEXT_COLOR = {0x33, 0x33, 0x33, 0xFF};
static constexpr Color PANEL_TEXT = {0xFF, 0xFF, 0xFF, 0xFF};
static constexpr Color ACCENT = {0x36, 0x7B, 0xF0, 0xFF};
static constexpr Color CLOSE_BTN = {0xFF, 0x5F, 0x57, 0xFF};
static constexpr Color MAX_BTN = {0x28, 0xCA, 0x42, 0xFF};
static constexpr Color MIN_BTN = {0xFF, 0xBD, 0x2E, 0xFF};
static constexpr Color SHADOW = {0x00, 0x00, 0x00, 0x40};
static constexpr Color TRANSPARENT = {0x00, 0x00, 0x00, 0x00};
static constexpr Color BLACK = {0x00, 0x00, 0x00, 0xFF};
static constexpr Color WHITE = {0xFF, 0xFF, 0xFF, 0xFF};
static constexpr Color ICON_COLOR = {0x5C, 0x61, 0x6C, 0xFF};
static constexpr Color SCROLLBAR_BG = {0xF0, 0xF0, 0xF0, 0xFF};
static constexpr Color SCROLLBAR_FG = {0xC0, 0xC0, 0xC0, 0xFF};
static constexpr Color MENU_BG = {0xFF, 0xFF, 0xFF, 0xFF};
static constexpr Color MENU_HOVER = {0xE8, 0xF0, 0xFE, 0xFF};
static constexpr Color TERM_BG = {0x2D, 0x2D, 0x2D, 0xFF};
static constexpr Color TERM_FG = {0xCC, 0xCC, 0xCC, 0xFF};
}
struct Point {
int x, y;
};
struct Rect {
int x, y, w, h;
bool contains(int px, int py) const {
return px >= x && px < x + w && py >= y && py < y + h;
}
Rect intersect(const Rect& other) const {
int rx = x > other.x ? x : other.x;
int ry = y > other.y ? y : other.y;
int rx2 = (x + w) < (other.x + other.w) ? (x + w) : (other.x + other.w);
int ry2 = (y + h) < (other.y + other.h) ? (y + h) : (other.y + other.h);
if (rx2 <= rx || ry2 <= ry) return {0, 0, 0, 0};
return {rx, ry, rx2 - rx, ry2 - ry};
}
bool empty() const { return w <= 0 || h <= 0; }
};
// Simple inline utility functions
inline int gui_min(int a, int b) { return a < b ? a : b; }
inline int gui_max(int a, int b) { return a > b ? a : b; }
inline int gui_abs(int a) { return a < 0 ? -a : a; }
inline int gui_clamp(int v, int lo, int hi) { return v < lo ? lo : (v > hi ? hi : v); }
} // namespace gui
+972
View File
@@ -0,0 +1,972 @@
/*
* svg.hpp
* ZenithOS SVG icon parser and scanline rasterizer
* Handles the Flat-Remix symbolic icon subset (path, circle, rect)
* All math uses 16.16 fixed-point -- NO floating point.
* Copyright (c) 2025 Daniel Hammer
*/
#pragma once
#include "gui/gui.hpp"
#include <zenith/syscall.h>
namespace gui {
// ---------------------------------------------------------------------------
// SVG icon result
// ---------------------------------------------------------------------------
struct SvgIcon {
uint32_t* pixels; // ARGB pixel data (heap-allocated)
int width;
int height;
};
// ---------------------------------------------------------------------------
// Edge used by the scanline rasterizer
// ---------------------------------------------------------------------------
struct SvgEdge {
fixed_t x0, y0, x1, y1;
};
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
static constexpr int SVG_MAX_EDGES = 4096;
static constexpr int SVG_MAX_PATH_LEN = 4096;
static constexpr int SVG_MAX_FILE_SIZE = 32768;
static constexpr int SVG_BEZIER_STEPS = 8;
// ---------------------------------------------------------------------------
// Fixed-point number parser (NO floating point)
// Parses strings like "3.25", "-0.5", ".1115", "16"
// Returns the number of characters consumed.
// ---------------------------------------------------------------------------
inline int svg_parse_fixed(const char* s, fixed_t* out) {
const char* p = s;
bool neg = false;
if (*p == '-') { neg = true; ++p; }
else if (*p == '+') { ++p; }
// Integer part
int32_t integer = 0;
while (*p >= '0' && *p <= '9') {
integer = integer * 10 + (*p - '0');
++p;
}
// Fractional part
int32_t frac = 0;
int32_t frac_div = 1;
if (*p == '.') {
++p;
while (*p >= '0' && *p <= '9') {
if (frac_div < 100000) { // prevent overflow
frac = frac * 10 + (*p - '0');
frac_div *= 10;
}
++p;
}
}
fixed_t val = int_to_fixed(integer);
if (frac_div > 1) {
val += (int32_t)(((int64_t)frac << 16) / frac_div);
}
if (neg) val = -val;
*out = val;
return (int)(p - s);
}
// ---------------------------------------------------------------------------
// String helpers (no stdlib)
// ---------------------------------------------------------------------------
inline bool svg_char_is_ws(char c) {
return c == ' ' || c == '\t' || c == '\n' || c == '\r';
}
inline bool svg_char_is_sep(char c) {
return svg_char_is_ws(c) || c == ',';
}
inline bool svg_char_is_num_start(char c) {
return (c >= '0' && c <= '9') || c == '-' || c == '+' || c == '.';
}
inline bool svg_char_is_cmd(char c) {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
}
inline int svg_strlen(const char* s) {
int n = 0;
while (s[n]) ++n;
return n;
}
inline bool svg_strncmp(const char* a, const char* b, int n) {
for (int i = 0; i < n; ++i)
if (a[i] != b[i]) return false;
return true;
}
inline void svg_memset(void* dst, uint8_t val, int n) {
auto* d = (uint8_t*)dst;
for (int i = 0; i < n; ++i) d[i] = val;
}
inline void svg_memcpy(void* dst, const void* src, int n) {
auto* d = (uint8_t*)dst;
auto* s = (const uint8_t*)src;
for (int i = 0; i < n; ++i) d[i] = s[i];
}
// ---------------------------------------------------------------------------
// Mini XML attribute extraction
// ---------------------------------------------------------------------------
// Find the next occurrence of needle in haystack (haystack has length hLen).
// Returns pointer to start of match, or nullptr.
inline const char* svg_strstr(const char* haystack, int hLen, const char* needle) {
int nLen = svg_strlen(needle);
if (nLen == 0 || nLen > hLen) return nullptr;
for (int i = 0; i <= hLen - nLen; ++i) {
if (svg_strncmp(haystack + i, needle, nLen))
return haystack + i;
}
return nullptr;
}
// Check if a character can be part of an XML attribute name
inline bool svg_char_is_attrname(char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') || c == '-' || c == '_' || c == ':';
}
// Extract the value of an XML attribute: attr="value"
// The attr string should include a leading space (e.g., " cx") to distinguish
// from substrings of other attribute names.
// Writes into buf (up to maxLen-1 chars), returns length or -1 if not found.
inline int svg_get_attr(const char* tag, int tagLen, const char* attr, char* buf, int maxLen) {
int attrLen = svg_strlen(attr);
const char* search_start = tag;
int search_len = tagLen;
while (search_len > 0) {
const char* p = svg_strstr(search_start, search_len, attr);
if (!p) return -1;
// Ensure this is the exact attribute name, not a prefix of another
// (e.g., " r" should not match " rx" or " ry")
const char* after_name = p + attrLen;
if (after_name < tag + tagLen && svg_char_is_attrname(*after_name)) {
// This is a prefix of a longer attribute name, skip and search again
search_start = after_name;
search_len = tagLen - (int)(search_start - tag);
continue;
}
p = after_name;
// skip whitespace around '='
while (p < tag + tagLen && svg_char_is_ws(*p)) ++p;
if (p >= tag + tagLen || *p != '=') return -1;
++p;
while (p < tag + tagLen && svg_char_is_ws(*p)) ++p;
if (p >= tag + tagLen) return -1;
char quote = *p;
if (quote != '"' && quote != '\'') return -1;
++p;
int len = 0;
while (p < tag + tagLen && *p != quote && len < maxLen - 1) {
buf[len++] = *p++;
}
buf[len] = '\0';
return len;
}
return -1;
}
// Parse an integer from a string (no sign, simple decimal).
inline int svg_parse_int(const char* s) {
int v = 0;
while (*s >= '0' && *s <= '9') {
v = v * 10 + (*s - '0');
++s;
}
return v;
}
// Parse a hex color like "#5c616c" into a Color.
inline Color svg_parse_hex_color(const char* s) {
if (*s == '#') ++s;
auto hexval = [](char c) -> uint8_t {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return 10 + (c - 'a');
if (c >= 'A' && c <= 'F') return 10 + (c - 'A');
return 0;
};
uint8_t r = (hexval(s[0]) << 4) | hexval(s[1]);
uint8_t g = (hexval(s[2]) << 4) | hexval(s[3]);
uint8_t b = (hexval(s[4]) << 4) | hexval(s[5]);
return Color::from_rgb(r, g, b);
}
// ---------------------------------------------------------------------------
// Edge list builder
// ---------------------------------------------------------------------------
struct SvgEdgeList {
SvgEdge* edges;
int count;
int capacity;
void init(int cap) {
edges = (SvgEdge*)zenith::alloc(cap * sizeof(SvgEdge));
count = 0;
capacity = cap;
}
void add(fixed_t x0, fixed_t y0, fixed_t x1, fixed_t y1) {
if (count >= capacity) return;
// skip horizontal edges (they don't contribute to scanline crossings)
if (y0 == y1) return;
edges[count++] = {x0, y0, x1, y1};
}
};
// ---------------------------------------------------------------------------
// Bezier flattening (fixed-point)
// ---------------------------------------------------------------------------
// Cubic bezier: add line segments approximating B(t) for t in [0,1]
inline void svg_flatten_cubic(SvgEdgeList& el,
fixed_t x0, fixed_t y0,
fixed_t x1, fixed_t y1,
fixed_t x2, fixed_t y2,
fixed_t x3, fixed_t y3) {
constexpr int N = SVG_BEZIER_STEPS;
fixed_t px = x0, py = y0;
for (int i = 1; i <= N; ++i) {
// t = i/N in 16.16: (i << 16) / N
fixed_t t = (int32_t)(((int64_t)i << 16) / N);
fixed_t omt = int_to_fixed(1) - t; // 1 - t
// (1-t)^2 and t^2
fixed_t omt2 = fixed_mul(omt, omt);
fixed_t t2 = fixed_mul(t, t);
// (1-t)^3 and t^3
fixed_t omt3 = fixed_mul(omt2, omt);
fixed_t t3 = fixed_mul(t2, t);
// 3*(1-t)^2*t and 3*(1-t)*t^2
fixed_t c1 = fixed_mul(omt2, t) * 3;
fixed_t c2 = fixed_mul(omt, t2) * 3;
fixed_t nx = fixed_mul(omt3, x0) + fixed_mul(c1, x1) + fixed_mul(c2, x2) + fixed_mul(t3, x3);
fixed_t ny = fixed_mul(omt3, y0) + fixed_mul(c1, y1) + fixed_mul(c2, y2) + fixed_mul(t3, y3);
el.add(px, py, nx, ny);
px = nx;
py = ny;
}
}
// Quadratic bezier: add line segments approximating B(t) for t in [0,1]
inline void svg_flatten_quad(SvgEdgeList& el,
fixed_t x0, fixed_t y0,
fixed_t x1, fixed_t y1,
fixed_t x2, fixed_t y2) {
constexpr int N = SVG_BEZIER_STEPS;
fixed_t px = x0, py = y0;
for (int i = 1; i <= N; ++i) {
fixed_t t = (int32_t)(((int64_t)i << 16) / N);
fixed_t omt = int_to_fixed(1) - t;
// (1-t)^2*P0 + 2*(1-t)*t*P1 + t^2*P2
fixed_t omt2 = fixed_mul(omt, omt);
fixed_t t2 = fixed_mul(t, t);
fixed_t c1 = fixed_mul(omt, t) * 2;
fixed_t nx = fixed_mul(omt2, x0) + fixed_mul(c1, x1) + fixed_mul(t2, x2);
fixed_t ny = fixed_mul(omt2, y0) + fixed_mul(c1, y1) + fixed_mul(t2, y2);
el.add(px, py, nx, ny);
px = nx;
py = ny;
}
}
// ---------------------------------------------------------------------------
// Circle to edges: approximate a circle as N line segments
// ---------------------------------------------------------------------------
inline void svg_circle_edges(SvgEdgeList& el, fixed_t cx, fixed_t cy, fixed_t r) {
// Approximate circle with 16 segments using a precomputed sin/cos table
// for angles 0, 22.5, 45, ... 337.5 degrees.
// sin/cos in 16.16 fixed-point for 16 evenly-spaced angles:
static const fixed_t cos16[16] = {
65536, 60547, 46341, 25080, 0, -25080, -46341, -60547,
-65536, -60547, -46341, -25080, 0, 25080, 46341, 60547
};
static const fixed_t sin16[16] = {
0, 25080, 46341, 60547, 65536, 60547, 46341, 25080,
0, -25080, -46341, -60547, -65536, -60547, -46341, -25080
};
fixed_t px = cx + fixed_mul(r, cos16[0]);
fixed_t py = cy + fixed_mul(r, sin16[0]);
for (int i = 1; i <= 16; ++i) {
int idx = i & 15;
fixed_t nx = cx + fixed_mul(r, cos16[idx]);
fixed_t ny = cy + fixed_mul(r, sin16[idx]);
el.add(px, py, nx, ny);
px = nx;
py = ny;
}
}
// ---------------------------------------------------------------------------
// Rounded rect to edges
// ---------------------------------------------------------------------------
inline void svg_rect_edges(SvgEdgeList& el, fixed_t x, fixed_t y, fixed_t w, fixed_t h,
fixed_t rx, fixed_t ry) {
if (rx <= 0 && ry <= 0) {
// Simple rectangle: 4 edges
fixed_t x2 = x + w;
fixed_t y2 = y + h;
el.add(x, y, x2, y); // top
el.add(x2, y, x2, y2); // right
el.add(x2, y2, x, y2); // bottom
el.add(x, y2, x, y); // left
return;
}
// Clamp radii
fixed_t half_w = w >> 1;
fixed_t half_h = h >> 1;
if (rx > half_w) rx = half_w;
if (ry > half_h) ry = half_h;
// Quarter-circle corner with 4 segments per corner.
// cos/sin for 0, 22.5, 45, 67.5, 90 degrees (5 points, 4 segments):
static const fixed_t qcos[5] = { 65536, 60547, 46341, 25080, 0 };
static const fixed_t qsin[5] = { 0, 25080, 46341, 60547, 65536 };
// Corners: top-right, bottom-right, bottom-left, top-left
// Each corner has a center and a quadrant direction for cos/sin application
struct Corner { fixed_t cx, cy; int sx, sy; };
Corner corners[4] = {
{ x + w - rx, y + ry, 1, -1 }, // top-right
{ x + w - rx, y + h - ry, 1, 1 }, // bottom-right
{ x + rx, y + h - ry, -1, 1 }, // bottom-left
{ x + rx, y + ry, -1, -1 }, // top-left
};
for (int c = 0; c < 4; ++c) {
Corner& cn = corners[c];
fixed_t px = cn.cx + fixed_mul(rx, qcos[0]) * cn.sx;
fixed_t py = cn.cy + fixed_mul(ry, qsin[0]) * cn.sy;
for (int i = 1; i <= 4; ++i) {
fixed_t nx = cn.cx + fixed_mul(rx, qcos[i]) * cn.sx;
fixed_t ny = cn.cy + fixed_mul(ry, qsin[i]) * cn.sy;
el.add(px, py, nx, ny);
px = nx;
py = ny;
}
}
// Straight edges between corners
// Top edge: top-left corner end -> top-right corner start
el.add(x + rx, y, x + w - rx, y);
// Right edge: top-right corner end -> bottom-right corner start
el.add(x + w, y + ry, x + w, y + h - ry);
// Bottom edge: bottom-right corner end -> bottom-left corner start
el.add(x + w - rx, y + h, x + rx, y + h);
// Left edge: bottom-left corner end -> top-left corner start
el.add(x, y + h - ry, x, y + ry);
}
// ---------------------------------------------------------------------------
// Path command parser: tokenize the 'd' attribute
// ---------------------------------------------------------------------------
struct SvgPathParser {
const char* data;
int len;
int pos;
void init(const char* d, int l) { data = d; len = l; pos = 0; }
void skip_separators() {
while (pos < len && svg_char_is_sep(data[pos])) ++pos;
}
bool has_more() const { return pos < len; }
// Peek at what's next: command letter, number, or end
bool next_is_number() {
skip_separators();
if (pos >= len) return false;
return svg_char_is_num_start(data[pos]);
}
char read_command() {
skip_separators();
if (pos >= len) return '\0';
if (svg_char_is_cmd(data[pos]) && data[pos] != 'e' && data[pos] != 'E') {
return data[pos++];
}
return '\0';
}
fixed_t read_number() {
skip_separators();
if (pos >= len) return 0;
fixed_t val = 0;
int consumed = svg_parse_fixed(data + pos, &val);
pos += consumed;
return val;
}
};
// ---------------------------------------------------------------------------
// Process an SVG path 'd' attribute into edges
// ---------------------------------------------------------------------------
inline void svg_path_to_edges(SvgEdgeList& el, const char* d, int dLen,
fixed_t scale_x, fixed_t scale_y,
fixed_t off_x, fixed_t off_y) {
SvgPathParser pp;
pp.init(d, dLen);
fixed_t cur_x = 0, cur_y = 0; // current point
fixed_t start_x = 0, start_y = 0; // subpath start
fixed_t last_cx = 0, last_cy = 0; // last control point (for S/T)
char last_cmd = '\0';
auto scale_pt = [&](fixed_t x, fixed_t y, fixed_t* ox, fixed_t* oy) {
*ox = fixed_mul(x - off_x, scale_x);
*oy = fixed_mul(y - off_y, scale_y);
};
while (pp.has_more()) {
char cmd = '\0';
// Try reading a command letter
pp.skip_separators();
if (pp.pos < pp.len && svg_char_is_cmd(pp.data[pp.pos]) &&
pp.data[pp.pos] != 'e' && pp.data[pp.pos] != 'E') {
cmd = pp.data[pp.pos++];
} else if (pp.next_is_number()) {
// Implicit repeat of last command
// After M, implicit repeat is L; after m, implicit repeat is l
if (last_cmd == 'M') cmd = 'L';
else if (last_cmd == 'm') cmd = 'l';
else cmd = last_cmd;
} else {
// Skip unknown character
if (pp.pos < pp.len) pp.pos++;
continue;
}
if (cmd == '\0') break;
switch (cmd) {
case 'M': {
fixed_t x = pp.read_number();
fixed_t y = pp.read_number();
cur_x = x; cur_y = y;
start_x = x; start_y = y;
last_cmd = 'M';
break;
}
case 'm': {
fixed_t dx = pp.read_number();
fixed_t dy = pp.read_number();
cur_x += dx; cur_y += dy;
start_x = cur_x; start_y = cur_y;
last_cmd = 'm';
break;
}
case 'L': {
fixed_t x = pp.read_number();
fixed_t y = pp.read_number();
fixed_t sx0, sy0, sx1, sy1;
scale_pt(cur_x, cur_y, &sx0, &sy0);
scale_pt(x, y, &sx1, &sy1);
el.add(sx0, sy0, sx1, sy1);
cur_x = x; cur_y = y;
last_cmd = 'L';
break;
}
case 'l': {
fixed_t dx = pp.read_number();
fixed_t dy = pp.read_number();
fixed_t nx = cur_x + dx, ny = cur_y + dy;
fixed_t sx0, sy0, sx1, sy1;
scale_pt(cur_x, cur_y, &sx0, &sy0);
scale_pt(nx, ny, &sx1, &sy1);
el.add(sx0, sy0, sx1, sy1);
cur_x = nx; cur_y = ny;
last_cmd = 'l';
break;
}
case 'H': {
fixed_t x = pp.read_number();
fixed_t sx0, sy0, sx1, sy1;
scale_pt(cur_x, cur_y, &sx0, &sy0);
scale_pt(x, cur_y, &sx1, &sy1);
el.add(sx0, sy0, sx1, sy1);
cur_x = x;
last_cmd = 'H';
break;
}
case 'h': {
fixed_t dx = pp.read_number();
fixed_t nx = cur_x + dx;
fixed_t sx0, sy0, sx1, sy1;
scale_pt(cur_x, cur_y, &sx0, &sy0);
scale_pt(nx, cur_y, &sx1, &sy1);
el.add(sx0, sy0, sx1, sy1);
cur_x = nx;
last_cmd = 'h';
break;
}
case 'V': {
fixed_t y = pp.read_number();
fixed_t sx0, sy0, sx1, sy1;
scale_pt(cur_x, cur_y, &sx0, &sy0);
scale_pt(cur_x, y, &sx1, &sy1);
el.add(sx0, sy0, sx1, sy1);
cur_y = y;
last_cmd = 'V';
break;
}
case 'v': {
fixed_t dy = pp.read_number();
fixed_t ny = cur_y + dy;
fixed_t sx0, sy0, sx1, sy1;
scale_pt(cur_x, cur_y, &sx0, &sy0);
scale_pt(cur_x, ny, &sx1, &sy1);
el.add(sx0, sy0, sx1, sy1);
cur_y = ny;
last_cmd = 'v';
break;
}
case 'C': {
fixed_t x1 = pp.read_number(), y1 = pp.read_number();
fixed_t x2 = pp.read_number(), y2 = pp.read_number();
fixed_t x3 = pp.read_number(), y3 = pp.read_number();
fixed_t sx0, sy0, sx1, sy1, sx2, sy2, sx3, sy3;
scale_pt(cur_x, cur_y, &sx0, &sy0);
scale_pt(x1, y1, &sx1, &sy1);
scale_pt(x2, y2, &sx2, &sy2);
scale_pt(x3, y3, &sx3, &sy3);
svg_flatten_cubic(el, sx0, sy0, sx1, sy1, sx2, sy2, sx3, sy3);
last_cx = x2; last_cy = y2;
cur_x = x3; cur_y = y3;
last_cmd = 'C';
break;
}
case 'c': {
fixed_t dx1 = pp.read_number(), dy1 = pp.read_number();
fixed_t dx2 = pp.read_number(), dy2 = pp.read_number();
fixed_t dx3 = pp.read_number(), dy3 = pp.read_number();
fixed_t x1 = cur_x + dx1, y1 = cur_y + dy1;
fixed_t x2 = cur_x + dx2, y2 = cur_y + dy2;
fixed_t x3 = cur_x + dx3, y3 = cur_y + dy3;
fixed_t sx0, sy0, sx1, sy1, sx2, sy2, sx3, sy3;
scale_pt(cur_x, cur_y, &sx0, &sy0);
scale_pt(x1, y1, &sx1, &sy1);
scale_pt(x2, y2, &sx2, &sy2);
scale_pt(x3, y3, &sx3, &sy3);
svg_flatten_cubic(el, sx0, sy0, sx1, sy1, sx2, sy2, sx3, sy3);
last_cx = x2; last_cy = y2;
cur_x = x3; cur_y = y3;
last_cmd = 'c';
break;
}
case 'S': {
// Smooth cubic: reflect last control point
fixed_t rcx = cur_x * 2 - last_cx;
fixed_t rcy = cur_y * 2 - last_cy;
if (last_cmd != 'C' && last_cmd != 'c' && last_cmd != 'S' && last_cmd != 's') {
rcx = cur_x; rcy = cur_y;
}
fixed_t x2 = pp.read_number(), y2 = pp.read_number();
fixed_t x3 = pp.read_number(), y3 = pp.read_number();
fixed_t sx0, sy0, sx1, sy1, sx2, sy2, sx3, sy3;
scale_pt(cur_x, cur_y, &sx0, &sy0);
scale_pt(rcx, rcy, &sx1, &sy1);
scale_pt(x2, y2, &sx2, &sy2);
scale_pt(x3, y3, &sx3, &sy3);
svg_flatten_cubic(el, sx0, sy0, sx1, sy1, sx2, sy2, sx3, sy3);
last_cx = x2; last_cy = y2;
cur_x = x3; cur_y = y3;
last_cmd = 'S';
break;
}
case 's': {
fixed_t rcx = cur_x * 2 - last_cx;
fixed_t rcy = cur_y * 2 - last_cy;
if (last_cmd != 'C' && last_cmd != 'c' && last_cmd != 'S' && last_cmd != 's') {
rcx = cur_x; rcy = cur_y;
}
fixed_t dx2 = pp.read_number(), dy2 = pp.read_number();
fixed_t dx3 = pp.read_number(), dy3 = pp.read_number();
fixed_t x2 = cur_x + dx2, y2 = cur_y + dy2;
fixed_t x3 = cur_x + dx3, y3 = cur_y + dy3;
fixed_t sx0, sy0, sx1, sy1, sx2, sy2, sx3, sy3;
scale_pt(cur_x, cur_y, &sx0, &sy0);
scale_pt(rcx, rcy, &sx1, &sy1);
scale_pt(x2, y2, &sx2, &sy2);
scale_pt(x3, y3, &sx3, &sy3);
svg_flatten_cubic(el, sx0, sy0, sx1, sy1, sx2, sy2, sx3, sy3);
last_cx = x2; last_cy = y2;
cur_x = x3; cur_y = y3;
last_cmd = 's';
break;
}
case 'Q': {
fixed_t x1 = pp.read_number(), y1 = pp.read_number();
fixed_t x2 = pp.read_number(), y2 = pp.read_number();
fixed_t sx0, sy0, sx1, sy1, sx2, sy2;
scale_pt(cur_x, cur_y, &sx0, &sy0);
scale_pt(x1, y1, &sx1, &sy1);
scale_pt(x2, y2, &sx2, &sy2);
svg_flatten_quad(el, sx0, sy0, sx1, sy1, sx2, sy2);
last_cx = x1; last_cy = y1;
cur_x = x2; cur_y = y2;
last_cmd = 'Q';
break;
}
case 'q': {
fixed_t dx1 = pp.read_number(), dy1 = pp.read_number();
fixed_t dx2 = pp.read_number(), dy2 = pp.read_number();
fixed_t x1 = cur_x + dx1, y1 = cur_y + dy1;
fixed_t x2 = cur_x + dx2, y2 = cur_y + dy2;
fixed_t sx0, sy0, sx1, sy1, sx2, sy2;
scale_pt(cur_x, cur_y, &sx0, &sy0);
scale_pt(x1, y1, &sx1, &sy1);
scale_pt(x2, y2, &sx2, &sy2);
svg_flatten_quad(el, sx0, sy0, sx1, sy1, sx2, sy2);
last_cx = x1; last_cy = y1;
cur_x = x2; cur_y = y2;
last_cmd = 'q';
break;
}
case 'A': case 'a': {
// Arc command: consume parameters but approximate as a line
// (arcs are rare in these icons)
fixed_t rx = pp.read_number();
fixed_t ry = pp.read_number();
pp.read_number(); // x-rotation
pp.read_number(); // large-arc-flag
pp.read_number(); // sweep-flag
fixed_t x = pp.read_number();
fixed_t y = pp.read_number();
if (cmd == 'a') { x += cur_x; y += cur_y; }
fixed_t sx0, sy0, sx1, sy1;
scale_pt(cur_x, cur_y, &sx0, &sy0);
scale_pt(x, y, &sx1, &sy1);
el.add(sx0, sy0, sx1, sy1);
cur_x = x; cur_y = y;
last_cmd = cmd;
(void)rx; (void)ry;
break;
}
case 'Z': case 'z': {
if (cur_x != start_x || cur_y != start_y) {
fixed_t sx0, sy0, sx1, sy1;
scale_pt(cur_x, cur_y, &sx0, &sy0);
scale_pt(start_x, start_y, &sx1, &sy1);
el.add(sx0, sy0, sx1, sy1);
}
cur_x = start_x; cur_y = start_y;
last_cmd = 'Z';
break;
}
default:
// Unknown command, skip
break;
}
}
}
// ---------------------------------------------------------------------------
// Scanline rasterizer (even-odd fill rule)
// ---------------------------------------------------------------------------
inline void svg_rasterize(const SvgEdgeList& el, uint32_t* pixels, int w, int h, uint32_t fill) {
// Temporary array for x-intersections on each scanline
// Allocate enough for all edges (each edge can intersect at most once per scanline)
int maxIsect = el.count + 16;
fixed_t* isect = (fixed_t*)zenith::alloc(maxIsect * sizeof(fixed_t));
for (int y = 0; y < h; ++y) {
// Scanline center in fixed-point
fixed_t scanY = int_to_fixed(y) + (1 << 15); // y + 0.5
int isectCount = 0;
// Find intersections with all edges
for (int i = 0; i < el.count; ++i) {
const SvgEdge& e = el.edges[i];
fixed_t ey0 = e.y0, ey1 = e.y1;
// Ensure ey0 <= ey1 for the range check
fixed_t emin = ey0 < ey1 ? ey0 : ey1;
fixed_t emax = ey0 > ey1 ? ey0 : ey1;
// Does this edge cross scanY?
if (scanY < emin || scanY >= emax) continue;
// Compute x at intersection: x = x0 + (scanY - y0) * (x1 - x0) / (y1 - y0)
fixed_t dy = ey1 - ey0;
if (dy == 0) continue; // horizontal, skip
fixed_t dx = e.x1 - e.x0;
fixed_t t_num = scanY - ey0;
// x_intersect = x0 + dx * t_num / dy
fixed_t x_int = e.x0 + (int32_t)(((int64_t)dx * t_num) / dy);
if (isectCount < maxIsect)
isect[isectCount++] = x_int;
}
// Sort intersections (simple insertion sort -- usually very few)
for (int i = 1; i < isectCount; ++i) {
fixed_t key = isect[i];
int j = i - 1;
while (j >= 0 && isect[j] > key) {
isect[j + 1] = isect[j];
--j;
}
isect[j + 1] = key;
}
// Fill between pairs (even-odd rule)
for (int i = 0; i + 1 < isectCount; i += 2) {
int x0 = fixed_to_int(isect[i]);
int x1 = fixed_to_int(isect[i + 1]);
// Clamp to pixel bounds
if (x0 < 0) x0 = 0;
if (x1 > w) x1 = w;
for (int x = x0; x < x1; ++x) {
pixels[y * w + x] = fill;
}
}
}
zenith::free(isect);
}
// ---------------------------------------------------------------------------
// SVG document parser: extract paths, circles, rects and rasterize
// ---------------------------------------------------------------------------
inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int target_h, Color fill_color) {
SvgIcon icon;
icon.width = target_w;
icon.height = target_h;
icon.pixels = (uint32_t*)zenith::alloc(target_w * target_h * sizeof(uint32_t));
// Clear to transparent
svg_memset(icon.pixels, 0, target_w * target_h * sizeof(uint32_t));
uint32_t fill_px = fill_color.to_pixel();
// Parse SVG dimensions: width and height
int svg_w = 16, svg_h = 16;
fixed_t vb_x = 0, vb_y = 0, vb_w = 0, vb_h = 0;
bool has_viewbox = false;
// Find <svg tag
const char* svg_tag = svg_strstr(svg_data, svg_len, "<svg");
if (svg_tag) {
// Find the end of the <svg ...> tag
int tag_offset = (int)(svg_tag - svg_data);
int tag_end = tag_offset;
while (tag_end < svg_len && svg_data[tag_end] != '>') ++tag_end;
int tag_len = tag_end - tag_offset + 1;
char attr_buf[64];
// width
if (svg_get_attr(svg_tag, tag_len, " width", attr_buf, sizeof(attr_buf)) > 0) {
svg_w = svg_parse_int(attr_buf);
}
// height
if (svg_get_attr(svg_tag, tag_len, " height", attr_buf, sizeof(attr_buf)) > 0) {
svg_h = svg_parse_int(attr_buf);
}
// viewBox
if (svg_get_attr(svg_tag, tag_len, " viewBox", attr_buf, sizeof(attr_buf)) > 0) {
has_viewbox = true;
const char* vp = attr_buf;
int c;
c = svg_parse_fixed(vp, &vb_x); vp += c; while (*vp && svg_char_is_sep(*vp)) ++vp;
c = svg_parse_fixed(vp, &vb_y); vp += c; while (*vp && svg_char_is_sep(*vp)) ++vp;
c = svg_parse_fixed(vp, &vb_w); vp += c; while (*vp && svg_char_is_sep(*vp)) ++vp;
svg_parse_fixed(vp, &vb_h);
}
}
if (!has_viewbox) {
vb_x = 0;
vb_y = 0;
vb_w = int_to_fixed(svg_w);
vb_h = int_to_fixed(svg_h);
}
// Compute scale: pixel = (svg_coord - vb_origin) * target_size / vb_size
fixed_t scale_x = vb_w > 0 ? fixed_div(int_to_fixed(target_w), vb_w) : int_to_fixed(1);
fixed_t scale_y = vb_h > 0 ? fixed_div(int_to_fixed(target_h), vb_h) : int_to_fixed(1);
// Edge list
SvgEdgeList el;
el.init(SVG_MAX_EDGES);
// Scan for <path, <circle, <rect elements
const char* p = svg_data;
const char* end = svg_data + svg_len;
while (p < end) {
// Find next '<'
while (p < end && *p != '<') ++p;
if (p >= end) break;
int remaining = (int)(end - p);
// Check for <path
if (remaining > 5 && svg_strncmp(p, "<path", 5) && (svg_char_is_ws(p[5]) || p[5] == '/')) {
// Find end of this element
const char* elem_start = p;
const char* elem_end = p;
while (elem_end < end && *elem_end != '>') ++elem_end;
if (elem_end < end) ++elem_end; // include '>'
int elem_len = (int)(elem_end - elem_start);
// Extract d attribute
char d_buf[SVG_MAX_PATH_LEN];
int d_len = svg_get_attr(elem_start, elem_len, " d", d_buf, SVG_MAX_PATH_LEN);
if (d_len > 0) {
svg_path_to_edges(el, d_buf, d_len, scale_x, scale_y, vb_x, vb_y);
}
p = elem_end;
continue;
}
// Check for <circle
if (remaining > 7 && svg_strncmp(p, "<circle", 7) && (svg_char_is_ws(p[7]) || p[7] == '/')) {
const char* elem_start = p;
const char* elem_end = p;
while (elem_end < end && *elem_end != '>') ++elem_end;
if (elem_end < end) ++elem_end;
int elem_len = (int)(elem_end - elem_start);
char attr_buf[32];
fixed_t cx = 0, cy = 0, r = 0;
if (svg_get_attr(elem_start, elem_len, " cx", attr_buf, sizeof(attr_buf)) > 0)
svg_parse_fixed(attr_buf, &cx);
if (svg_get_attr(elem_start, elem_len, " cy", attr_buf, sizeof(attr_buf)) > 0)
svg_parse_fixed(attr_buf, &cy);
if (svg_get_attr(elem_start, elem_len, " r", attr_buf, sizeof(attr_buf)) > 0)
svg_parse_fixed(attr_buf, &r);
// Scale to target coordinates
fixed_t scx = fixed_mul(cx - vb_x, scale_x);
fixed_t scy = fixed_mul(cy - vb_y, scale_y);
fixed_t srx = fixed_mul(r, scale_x);
fixed_t sry = fixed_mul(r, scale_y);
// Use average of scaled radii
fixed_t sr = (srx + sry) >> 1;
svg_circle_edges(el, scx, scy, sr);
p = elem_end;
continue;
}
// Check for <rect
if (remaining > 5 && svg_strncmp(p, "<rect", 5) && (svg_char_is_ws(p[5]) || p[5] == '/')) {
const char* elem_start = p;
const char* elem_end = p;
while (elem_end < end && *elem_end != '>') ++elem_end;
if (elem_end < end) ++elem_end;
int elem_len = (int)(elem_end - elem_start);
char attr_buf[32];
fixed_t rx_val = 0, ry_val = 0, rw = 0, rh = 0, rrx = 0, rry = 0;
if (svg_get_attr(elem_start, elem_len, " x", attr_buf, sizeof(attr_buf)) > 0)
svg_parse_fixed(attr_buf, &rx_val);
if (svg_get_attr(elem_start, elem_len, " y", attr_buf, sizeof(attr_buf)) > 0)
svg_parse_fixed(attr_buf, &ry_val);
if (svg_get_attr(elem_start, elem_len, " width", attr_buf, sizeof(attr_buf)) > 0)
svg_parse_fixed(attr_buf, &rw);
if (svg_get_attr(elem_start, elem_len, " height", attr_buf, sizeof(attr_buf)) > 0)
svg_parse_fixed(attr_buf, &rh);
if (svg_get_attr(elem_start, elem_len, " rx", attr_buf, sizeof(attr_buf)) > 0)
svg_parse_fixed(attr_buf, &rrx);
if (svg_get_attr(elem_start, elem_len, " ry", attr_buf, sizeof(attr_buf)) > 0)
svg_parse_fixed(attr_buf, &rry);
// Scale all to target pixel space
fixed_t sx = fixed_mul(rx_val - vb_x, scale_x);
fixed_t sy = fixed_mul(ry_val - vb_y, scale_y);
fixed_t sw = fixed_mul(rw, scale_x);
fixed_t sh = fixed_mul(rh, scale_y);
fixed_t srx = fixed_mul(rrx, scale_x);
fixed_t sry = fixed_mul(rry, scale_y);
svg_rect_edges(el, sx, sy, sw, sh, srx, sry);
p = elem_end;
continue;
}
++p;
}
// Rasterize all accumulated edges
if (el.count > 0) {
svg_rasterize(el, icon.pixels, target_w, target_h, fill_px);
}
zenith::free(el.edges);
return icon;
}
// ---------------------------------------------------------------------------
// Load SVG from VFS and render
// ---------------------------------------------------------------------------
inline SvgIcon svg_load(const char* vfs_path, int target_w, int target_h, Color fill_color) {
int fd = zenith::open(vfs_path);
if (fd < 0) {
return {nullptr, 0, 0};
}
uint64_t size = zenith::getsize(fd);
if (size == 0 || size > SVG_MAX_FILE_SIZE) {
zenith::close(fd);
return {nullptr, 0, 0};
}
char* buf = (char*)zenith::alloc(size + 1);
zenith::read(fd, (uint8_t*)buf, 0, size);
zenith::close(fd);
buf[size] = '\0';
SvgIcon icon = svg_render(buf, (int)size, target_w, target_h, fill_color);
zenith::free(buf);
return icon;
}
// ---------------------------------------------------------------------------
// Free icon pixel data
// ---------------------------------------------------------------------------
inline void svg_free(SvgIcon& icon) {
if (icon.pixels) zenith::free(icon.pixels);
icon.pixels = nullptr;
icon.width = 0;
icon.height = 0;
}
} // namespace gui
+526
View File
@@ -0,0 +1,526 @@
/*
* terminal.hpp
* ZenithOS terminal emulator with ANSI escape sequence support
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include "gui/gui.hpp"
#include "gui/font.hpp"
#include <zenith/syscall.h>
#include <zenith/string.h>
#include <Api/Syscall.hpp>
namespace gui {
struct TermCell {
char ch;
Color fg;
Color bg;
};
static constexpr int TERM_MAX_SCROLLBACK = 500;
struct TerminalState {
TermCell* cells;
TermCell* alt_cells; // alternate screen buffer
int cols, rows;
int cursor_x, cursor_y;
int saved_cursor_x, saved_cursor_y; // saved cursor for alternate screen
int scroll_top;
int total_rows;
int child_pid;
Color current_fg;
Color current_bg;
bool cursor_visible;
bool alt_screen_active;
bool reverse_video;
enum { STATE_NORMAL, STATE_ESC, STATE_CSI } parse_state;
bool csi_private; // true if '?' was seen after CSI
int csi_params[8];
int csi_param_count;
int csi_current_param;
};
// Standard ANSI color palette as ARGB pixels
static inline Color term_ansi_color(int idx) {
switch (idx) {
case 0: return Color::from_hex(0x000000);
case 1: return Color::from_hex(0xCC0000);
case 2: return Color::from_hex(0x4E9A06);
case 3: return Color::from_hex(0xC4A000);
case 4: return Color::from_hex(0x3465A4);
case 5: return Color::from_hex(0x75507B);
case 6: return Color::from_hex(0x06989A);
case 7: return Color::from_hex(0xD3D7CF);
case 8: return Color::from_hex(0x555753);
case 9: return Color::from_hex(0xEF2929);
case 10: return Color::from_hex(0x8AE234);
case 11: return Color::from_hex(0xFCE94F);
case 12: return Color::from_hex(0x729FCF);
case 13: return Color::from_hex(0xAD7FA8);
case 14: return Color::from_hex(0x34E2E2);
case 15: return Color::from_hex(0xEEEEEC);
default: return colors::TERM_FG;
}
}
static inline void terminal_scroll_up(TerminalState* t) {
// Move all rows up by one
for (int r = 0; r < t->rows - 1; r++) {
for (int c = 0; c < t->cols; c++) {
t->cells[r * t->cols + c] = t->cells[(r + 1) * t->cols + c];
}
}
// Clear last row
int last = t->rows - 1;
for (int c = 0; c < t->cols; c++) {
t->cells[last * t->cols + c] = {' ', t->current_fg, colors::TERM_BG};
}
}
static inline void terminal_init(TerminalState* t, int cols, int rows) {
t->cols = cols;
t->rows = rows;
t->cursor_x = 0;
t->cursor_y = 0;
t->saved_cursor_x = 0;
t->saved_cursor_y = 0;
t->scroll_top = 0;
t->total_rows = rows;
t->current_fg = colors::TERM_FG;
t->current_bg = colors::TERM_BG;
t->cursor_visible = true;
t->alt_screen_active = false;
t->reverse_video = false;
t->parse_state = TerminalState::STATE_NORMAL;
t->csi_private = false;
t->csi_param_count = 0;
t->csi_current_param = 0;
int total_cells = cols * rows;
t->cells = (TermCell*)zenith::alloc(total_cells * sizeof(TermCell));
t->alt_cells = (TermCell*)zenith::alloc(total_cells * sizeof(TermCell));
for (int i = 0; i < total_cells; i++) {
t->cells[i] = {' ', colors::TERM_FG, colors::TERM_BG};
t->alt_cells[i] = {' ', colors::TERM_FG, colors::TERM_BG};
}
t->child_pid = zenith::spawn_redir("0:/os/shell.elf");
zenith::childio_settermsz(t->child_pid, cols, rows);
}
static inline void terminal_put_char(TerminalState* t, char ch) {
if (t->cursor_x >= t->cols) {
t->cursor_x = 0;
t->cursor_y++;
}
if (t->cursor_y >= t->rows) {
terminal_scroll_up(t);
t->cursor_y = t->rows - 1;
}
int idx = t->cursor_y * t->cols + t->cursor_x;
t->cells[idx].ch = ch;
t->cells[idx].fg = t->current_fg;
t->cells[idx].bg = t->current_bg;
t->cursor_x++;
}
static inline void terminal_enter_alt_screen(TerminalState* t) {
if (t->alt_screen_active) return;
t->alt_screen_active = true;
// Save cursor
t->saved_cursor_x = t->cursor_x;
t->saved_cursor_y = t->cursor_y;
// Swap buffers: save main screen to alt_cells, clear main
int total = t->cols * t->rows;
for (int i = 0; i < total; i++) {
t->alt_cells[i] = t->cells[i];
t->cells[i] = {' ', colors::TERM_FG, colors::TERM_BG};
}
t->cursor_x = 0;
t->cursor_y = 0;
}
static inline void terminal_exit_alt_screen(TerminalState* t) {
if (!t->alt_screen_active) return;
t->alt_screen_active = false;
// Restore main screen from alt_cells
int total = t->cols * t->rows;
for (int i = 0; i < total; i++) {
t->cells[i] = t->alt_cells[i];
}
// Restore cursor
t->cursor_x = t->saved_cursor_x;
t->cursor_y = t->saved_cursor_y;
}
static inline void terminal_process_private_mode(TerminalState* t, char cmd) {
int p0 = t->csi_param_count > 0 ? t->csi_params[0] : 0;
if (cmd == 'h') {
// Set private mode
if (p0 == 25) {
t->cursor_visible = true;
} else if (p0 == 1049) {
terminal_enter_alt_screen(t);
}
} else if (cmd == 'l') {
// Reset private mode
if (p0 == 25) {
t->cursor_visible = false;
} else if (p0 == 1049) {
terminal_exit_alt_screen(t);
}
}
}
static inline void terminal_process_csi(TerminalState* t, char cmd) {
// Finalize current param
if (t->csi_param_count < 8) {
t->csi_params[t->csi_param_count] = t->csi_current_param;
t->csi_param_count++;
}
// Handle private mode sequences (ESC[?...)
if (t->csi_private) {
terminal_process_private_mode(t, cmd);
return;
}
int p0 = t->csi_param_count > 0 ? t->csi_params[0] : 0;
int p1 = t->csi_param_count > 1 ? t->csi_params[1] : 0;
switch (cmd) {
case 'H': case 'f': {
// Cursor position: ESC[row;colH (1-based)
int row = (p0 > 0 ? p0 : 1) - 1;
int col = (p1 > 0 ? p1 : 1) - 1;
if (row < 0) row = 0;
if (row >= t->rows) row = t->rows - 1;
if (col < 0) col = 0;
if (col >= t->cols) col = t->cols - 1;
t->cursor_y = row;
t->cursor_x = col;
break;
}
case 'A': {
// Cursor up
int n = p0 > 0 ? p0 : 1;
t->cursor_y -= n;
if (t->cursor_y < 0) t->cursor_y = 0;
break;
}
case 'B': {
// Cursor down
int n = p0 > 0 ? p0 : 1;
t->cursor_y += n;
if (t->cursor_y >= t->rows) t->cursor_y = t->rows - 1;
break;
}
case 'C': {
// Cursor forward
int n = p0 > 0 ? p0 : 1;
t->cursor_x += n;
if (t->cursor_x >= t->cols) t->cursor_x = t->cols - 1;
break;
}
case 'D': {
// Cursor backward
int n = p0 > 0 ? p0 : 1;
t->cursor_x -= n;
if (t->cursor_x < 0) t->cursor_x = 0;
break;
}
case 'J': {
// Erase in display
if (p0 == 0) {
// Clear from cursor to end
for (int x = t->cursor_x; x < t->cols; x++)
t->cells[t->cursor_y * t->cols + x] = {' ', t->current_fg, colors::TERM_BG};
for (int r = t->cursor_y + 1; r < t->rows; r++)
for (int c = 0; c < t->cols; c++)
t->cells[r * t->cols + c] = {' ', t->current_fg, colors::TERM_BG};
} else if (p0 == 1) {
// Clear from start to cursor
for (int r = 0; r < t->cursor_y; r++)
for (int c = 0; c < t->cols; c++)
t->cells[r * t->cols + c] = {' ', t->current_fg, colors::TERM_BG};
for (int x = 0; x <= t->cursor_x; x++)
t->cells[t->cursor_y * t->cols + x] = {' ', t->current_fg, colors::TERM_BG};
} else if (p0 == 2) {
// Clear entire screen
for (int i = 0; i < t->rows * t->cols; i++)
t->cells[i] = {' ', t->current_fg, colors::TERM_BG};
t->cursor_x = 0;
t->cursor_y = 0;
}
break;
}
case 'K': {
// Erase in line
int start = 0, end = t->cols;
if (p0 == 0) { start = t->cursor_x; end = t->cols; }
else if (p0 == 1) { start = 0; end = t->cursor_x + 1; }
else if (p0 == 2) { start = 0; end = t->cols; }
for (int x = start; x < end; x++)
t->cells[t->cursor_y * t->cols + x] = {' ', t->current_fg, colors::TERM_BG};
break;
}
case 'm': {
// SGR - Set Graphics Rendition
for (int i = 0; i < t->csi_param_count; i++) {
int code = t->csi_params[i];
if (code == 0) {
t->current_fg = colors::TERM_FG;
t->current_bg = colors::TERM_BG;
t->reverse_video = false;
} else if (code == 1) {
// Bold: map to bright version of current color
uint8_t r = t->current_fg.r;
uint8_t g = t->current_fg.g;
uint8_t b = t->current_fg.b;
int add = 50;
r = (r + add > 255) ? 255 : r + add;
g = (g + add > 255) ? 255 : g + add;
b = (b + add > 255) ? 255 : b + add;
t->current_fg = Color::from_rgb(r, g, b);
} else if (code == 2) {
// Dim: darken current fg color
t->current_fg.r = t->current_fg.r / 2;
t->current_fg.g = t->current_fg.g / 2;
t->current_fg.b = t->current_fg.b / 2;
} else if (code == 7) {
// Reverse video
if (!t->reverse_video) {
t->reverse_video = true;
Color tmp = t->current_fg;
t->current_fg = t->current_bg;
t->current_bg = tmp;
}
} else if (code == 27) {
// Reverse off
if (t->reverse_video) {
t->reverse_video = false;
Color tmp = t->current_fg;
t->current_fg = t->current_bg;
t->current_bg = tmp;
}
} else if (code >= 30 && code <= 37) {
t->current_fg = term_ansi_color(code - 30);
if (t->reverse_video) {
// In reverse mode, fg is displayed as bg
Color tmp = t->current_fg;
t->current_fg = t->current_bg;
t->current_bg = tmp;
}
} else if (code >= 40 && code <= 47) {
t->current_bg = term_ansi_color(code - 40);
} else if (code >= 90 && code <= 97) {
t->current_fg = term_ansi_color(code - 90 + 8);
} else if (code >= 100 && code <= 107) {
t->current_bg = term_ansi_color(code - 100 + 8);
} else if (code == 39) {
t->current_fg = colors::TERM_FG;
} else if (code == 49) {
t->current_bg = colors::TERM_BG;
}
}
if (t->csi_param_count == 0) {
// ESC[m with no params = reset
t->current_fg = colors::TERM_FG;
t->current_bg = colors::TERM_BG;
t->reverse_video = false;
}
break;
}
default:
break;
}
}
static inline void terminal_feed(TerminalState* t, const char* data, int len) {
for (int i = 0; i < len; i++) {
char ch = data[i];
switch (t->parse_state) {
case TerminalState::STATE_NORMAL:
if (ch == '\033') {
t->parse_state = TerminalState::STATE_ESC;
} else if (ch == '\n') {
t->cursor_x = 0; // CR+LF: shell sends \n without \r
t->cursor_y++;
if (t->cursor_y >= t->rows) {
terminal_scroll_up(t);
t->cursor_y = t->rows - 1;
}
} else if (ch == '\r') {
t->cursor_x = 0;
} else if (ch == '\b') {
if (t->cursor_x > 0) t->cursor_x--;
} else if (ch == '\t') {
int next = (t->cursor_x + 8) & ~7;
if (next > t->cols) next = t->cols;
while (t->cursor_x < next) {
terminal_put_char(t, ' ');
}
} else if (ch >= 32 || ch < 0) {
// Printable character (also treat high-bit chars as printable)
terminal_put_char(t, ch);
}
break;
case TerminalState::STATE_ESC:
if (ch == '[') {
t->parse_state = TerminalState::STATE_CSI;
t->csi_private = false;
t->csi_param_count = 0;
t->csi_current_param = 0;
for (int j = 0; j < 8; j++) t->csi_params[j] = 0;
} else if (ch == 'c') {
// Reset terminal
t->current_fg = colors::TERM_FG;
t->current_bg = colors::TERM_BG;
t->cursor_x = 0;
t->cursor_y = 0;
t->parse_state = TerminalState::STATE_NORMAL;
} else {
// Unknown ESC sequence, ignore
t->parse_state = TerminalState::STATE_NORMAL;
}
break;
case TerminalState::STATE_CSI:
if (ch >= '0' && ch <= '9') {
t->csi_current_param = t->csi_current_param * 10 + (ch - '0');
} else if (ch == ';') {
if (t->csi_param_count < 8) {
t->csi_params[t->csi_param_count] = t->csi_current_param;
t->csi_param_count++;
}
t->csi_current_param = 0;
} else if (ch == '?') {
t->csi_private = true;
} else if (ch >= 0x40 && ch <= 0x7E) {
// Final byte - execute command
terminal_process_csi(t, ch);
t->parse_state = TerminalState::STATE_NORMAL;
} else {
// Unknown, abort CSI
t->parse_state = TerminalState::STATE_NORMAL;
}
break;
}
}
}
static inline void terminal_render(TerminalState* t, uint32_t* pixels, int pw, int ph) {
// Fill background
uint32_t bg_px = colors::TERM_BG.to_pixel();
int total = pw * ph;
for (int i = 0; i < total; i++) {
pixels[i] = bg_px;
}
// Render each visible cell
int visible_rows = ph / FONT_HEIGHT;
int visible_cols = pw / FONT_WIDTH;
if (visible_rows > t->rows) visible_rows = t->rows;
if (visible_cols > t->cols) visible_cols = t->cols;
for (int r = 0; r < visible_rows; r++) {
for (int c = 0; c < visible_cols; c++) {
int idx = r * t->cols + c;
TermCell& cell = t->cells[idx];
int px = c * FONT_WIDTH;
int py = r * FONT_HEIGHT;
uint32_t cell_bg = cell.bg.to_pixel();
uint32_t cell_fg = cell.fg.to_pixel();
// Draw cell background
for (int fy = 0; fy < FONT_HEIGHT; fy++) {
int dy = py + fy;
if (dy >= ph) break;
for (int fx = 0; fx < FONT_WIDTH; fx++) {
int dx = px + fx;
if (dx >= pw) break;
pixels[dy * pw + dx] = cell_bg;
}
}
// Draw character glyph
if (cell.ch > 32 || cell.ch < 0) {
const uint8_t* glyph = &font_data[(unsigned char)cell.ch * FONT_HEIGHT];
for (int fy = 0; fy < FONT_HEIGHT; fy++) {
int dy = py + fy;
if (dy >= ph) break;
uint8_t bits = glyph[fy];
for (int fx = 0; fx < FONT_WIDTH; fx++) {
if (bits & (0x80 >> fx)) {
int dx = px + fx;
if (dx >= pw) break;
pixels[dy * pw + dx] = cell_fg;
}
}
}
}
}
}
// Draw cursor
if (t->cursor_visible && t->cursor_x < visible_cols && t->cursor_y < visible_rows) {
int cx = t->cursor_x * FONT_WIDTH;
int cy = t->cursor_y * FONT_HEIGHT;
uint32_t cursor_px = colors::WHITE.to_pixel();
for (int fy = 0; fy < FONT_HEIGHT; fy++) {
int dy = cy + fy;
if (dy >= ph) break;
for (int fx = 0; fx < FONT_WIDTH; fx++) {
int dx = cx + fx;
if (dx >= pw) break;
pixels[dy * pw + dx] = cursor_px;
}
}
// Draw character on top of cursor in black
if (t->cursor_y < t->rows && t->cursor_x < t->cols) {
int idx = t->cursor_y * t->cols + t->cursor_x;
char ch = t->cells[idx].ch;
if (ch > 32 || ch < 0) {
const uint8_t* glyph = &font_data[(unsigned char)ch * FONT_HEIGHT];
uint32_t black_px = colors::BLACK.to_pixel();
for (int fy = 0; fy < FONT_HEIGHT; fy++) {
int dy = cy + fy;
if (dy >= ph) break;
uint8_t bits = glyph[fy];
for (int fx = 0; fx < FONT_WIDTH; fx++) {
if (bits & (0x80 >> fx)) {
int dx = cx + fx;
if (dx >= pw) break;
pixels[dy * pw + dx] = black_px;
}
}
}
}
}
}
}
static inline void terminal_handle_key(TerminalState* t, const Zenith::KeyEvent& key) {
if (t->child_pid > 0) {
zenith::childio_writekey(t->child_pid, &key);
}
}
static inline void terminal_poll(TerminalState* t) {
if (t->child_pid <= 0) return;
char buf[512];
int n = zenith::childio_read(t->child_pid, buf, sizeof(buf));
if (n > 0) {
terminal_feed(t, buf, n);
}
}
} // namespace gui
+354
View File
@@ -0,0 +1,354 @@
/*
* widgets.hpp
* ZenithOS GUI widget toolkit (Label, Button, TextBox, Scrollbar)
* Copyright (c) 2025 Daniel Hammer
*/
#pragma once
#include "gui/gui.hpp"
#include "gui/framebuffer.hpp"
#include "gui/font.hpp"
#include "gui/draw.hpp"
namespace gui {
// ---- Mouse event ----
struct MouseEvent {
int x, y;
uint8_t buttons;
uint8_t prev_buttons;
int32_t scroll;
bool left_held() const { return buttons & 0x01; }
bool right_held() const { return buttons & 0x02; }
bool middle_held() const { return buttons & 0x04; }
bool left_pressed() const { return (buttons & 0x01) && !(prev_buttons & 0x01); }
bool left_released() const { return !(buttons & 0x01) && (prev_buttons & 0x01); }
bool right_pressed() const { return (buttons & 0x02) && !(prev_buttons & 0x02); }
bool right_released() const { return !(buttons & 0x02) && (prev_buttons & 0x02); }
};
// ---- Callback types ----
using ClickCallback = void (*)(void* userdata);
// ---- Label ----
struct Label {
int x, y;
const char* text;
Color color;
void draw(Framebuffer& fb) const {
if (text) {
draw_text(fb, x, y, text, color);
}
}
};
// ---- Button ----
struct Button {
Rect bounds;
const char* text;
Color bg;
Color fg;
Color hover_bg;
bool hovered;
bool pressed;
ClickCallback on_click;
void* userdata;
void init(int x, int y, int w, int h, const char* label) {
bounds = {x, y, w, h};
text = label;
bg = colors::ACCENT;
fg = colors::WHITE;
hover_bg = Color::from_rgb(0x2B, 0x6B, 0xE0);
hovered = false;
pressed = false;
on_click = nullptr;
userdata = nullptr;
}
void draw(Framebuffer& fb) const {
Color bg_color = hovered ? hover_bg : bg;
fill_rounded_rect(fb, bounds.x, bounds.y, bounds.w, bounds.h, 4, bg_color);
// Center text
int tw = text_width(text);
int tx = bounds.x + (bounds.w - tw) / 2;
int ty = bounds.y + (bounds.h - FONT_HEIGHT) / 2;
draw_text(fb, tx, ty, text, fg);
}
bool handle_mouse(const MouseEvent& ev) {
hovered = bounds.contains(ev.x, ev.y);
if (hovered && ev.left_pressed()) {
pressed = true;
}
if (pressed && ev.left_released()) {
pressed = false;
if (hovered && on_click) {
on_click(userdata);
return true;
}
}
if (!ev.left_held()) {
pressed = false;
}
return false;
}
};
// ---- IconButton (for panel/menu items with SVG icon + optional text) ----
struct IconButton {
Rect bounds;
const char* text; // optional label (can be nullptr)
uint32_t* icon_pixels; // icon pixel data (ARGB)
int icon_w, icon_h;
Color bg;
Color hover_bg;
Color text_color;
bool hovered;
bool pressed;
ClickCallback on_click;
void* userdata;
void init(int x, int y, int w, int h) {
bounds = {x, y, w, h};
text = nullptr;
icon_pixels = nullptr;
icon_w = 0;
icon_h = 0;
bg = {0, 0, 0, 0}; // transparent by default
hover_bg = colors::MENU_HOVER;
text_color = colors::TEXT_COLOR;
hovered = false;
pressed = false;
on_click = nullptr;
userdata = nullptr;
}
void draw(Framebuffer& fb) const {
if (hovered) {
fill_rounded_rect(fb, bounds.x, bounds.y, bounds.w, bounds.h, 3, hover_bg);
} else if (bg.a > 0) {
fill_rounded_rect(fb, bounds.x, bounds.y, bounds.w, bounds.h, 3, bg);
}
int content_x = bounds.x + 6;
// Draw icon
if (icon_pixels && icon_w > 0 && icon_h > 0) {
int iy = bounds.y + (bounds.h - icon_h) / 2;
fb.blit_alpha(content_x, iy, icon_w, icon_h, icon_pixels);
content_x += icon_w + 6;
}
// Draw text
if (text) {
int ty = bounds.y + (bounds.h - FONT_HEIGHT) / 2;
draw_text(fb, content_x, ty, text, text_color);
}
}
bool handle_mouse(const MouseEvent& ev) {
hovered = bounds.contains(ev.x, ev.y);
if (hovered && ev.left_pressed()) {
pressed = true;
}
if (pressed && ev.left_released()) {
pressed = false;
if (hovered && on_click) {
on_click(userdata);
return true;
}
}
if (!ev.left_held()) {
pressed = false;
}
return false;
}
};
// ---- TextBox ----
struct TextBox {
Rect bounds;
char text[256];
int cursor;
int text_len;
bool focused;
Color bg;
Color fg;
Color border_color;
Color cursor_color;
void init(int x, int y, int w, int h) {
bounds = {x, y, w, h};
text[0] = '\0';
cursor = 0;
text_len = 0;
focused = false;
bg = colors::WHITE;
fg = colors::TEXT_COLOR;
border_color = colors::BORDER;
cursor_color = colors::ACCENT;
}
void draw(Framebuffer& fb) const {
fb.fill_rect(bounds.x, bounds.y, bounds.w, bounds.h, bg);
draw_rect(fb, bounds.x, bounds.y, bounds.w, bounds.h,
focused ? cursor_color : border_color);
// Draw text with 4px padding
int tx = bounds.x + 4;
int ty = bounds.y + (bounds.h - FONT_HEIGHT) / 2;
draw_text(fb, tx, ty, text, fg);
// Draw cursor if focused
if (focused) {
int cx = tx + cursor * FONT_WIDTH;
draw_vline(fb, cx, ty, FONT_HEIGHT, cursor_color);
}
}
void handle_mouse(const MouseEvent& ev) {
if (ev.left_pressed()) {
focused = bounds.contains(ev.x, ev.y);
}
}
void handle_key(const Zenith::KeyEvent& key) {
if (!focused || !key.pressed) return;
if (key.ascii == '\b' || key.scancode == 0x0E) {
// Backspace
if (cursor > 0) {
for (int i = cursor - 1; i < text_len - 1; i++) {
text[i] = text[i + 1];
}
text_len--;
cursor--;
text[text_len] = '\0';
}
} else if (key.ascii >= 32 && key.ascii < 127) {
// Printable character
if (text_len < 254) {
for (int i = text_len; i > cursor; i--) {
text[i] = text[i - 1];
}
text[cursor] = key.ascii;
cursor++;
text_len++;
text[text_len] = '\0';
}
} else if (key.scancode == 0x4B) {
// Left arrow
if (cursor > 0) cursor--;
} else if (key.scancode == 0x4D) {
// Right arrow
if (cursor < text_len) cursor++;
}
}
};
// ---- Scrollbar ----
struct Scrollbar {
Rect bounds;
int content_height;
int view_height;
int scroll_offset;
bool dragging;
int drag_start_y;
int drag_start_offset;
Color bg;
Color fg;
Color hover_fg;
bool hovered;
void init(int x, int y, int w, int h) {
bounds = {x, y, w, h};
content_height = 0;
view_height = h;
scroll_offset = 0;
dragging = false;
drag_start_y = 0;
drag_start_offset = 0;
bg = colors::SCROLLBAR_BG;
fg = colors::SCROLLBAR_FG;
hover_fg = Color::from_rgb(0xA0, 0xA0, 0xA0);
hovered = false;
}
int thumb_height() const {
if (content_height <= view_height) return bounds.h;
int th = (view_height * bounds.h) / content_height;
return th < 20 ? 20 : th;
}
int thumb_y() const {
if (content_height <= view_height) return bounds.y;
int range = bounds.h - thumb_height();
int max_scroll = content_height - view_height;
if (max_scroll <= 0) return bounds.y;
return bounds.y + (scroll_offset * range) / max_scroll;
}
int max_scroll() const {
int ms = content_height - view_height;
return ms > 0 ? ms : 0;
}
void draw(Framebuffer& fb) const {
if (content_height <= view_height) return; // no scrollbar needed
fb.fill_rect(bounds.x, bounds.y, bounds.w, bounds.h, bg);
int th = thumb_height();
int ty = thumb_y();
Color thumb_color = (hovered || dragging) ? hover_fg : fg;
fill_rounded_rect(fb, bounds.x + 1, ty, bounds.w - 2, th, 3, thumb_color);
}
void handle_mouse(const MouseEvent& ev) {
if (content_height <= view_height) return;
Rect thumb_rect = {bounds.x, thumb_y(), bounds.w, thumb_height()};
hovered = thumb_rect.contains(ev.x, ev.y);
if (hovered && ev.left_pressed()) {
dragging = true;
drag_start_y = ev.y;
drag_start_offset = scroll_offset;
}
if (dragging && ev.left_held()) {
int dy = ev.y - drag_start_y;
int range = bounds.h - thumb_height();
if (range > 0) {
int ms = max_scroll();
scroll_offset = drag_start_offset + (dy * ms) / range;
if (scroll_offset < 0) scroll_offset = 0;
if (scroll_offset > ms) scroll_offset = ms;
}
}
if (!ev.left_held()) {
dragging = false;
}
// Handle scroll wheel
if (bounds.contains(ev.x, ev.y) && ev.scroll != 0) {
scroll_offset += ev.scroll * 20;
int ms = max_scroll();
if (scroll_offset < 0) scroll_offset = 0;
if (scroll_offset > ms) scroll_offset = ms;
}
}
};
} // namespace gui
+78
View File
@@ -0,0 +1,78 @@
/*
* window.hpp
* ZenithOS window management types
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include "gui/gui.hpp"
#include "gui/framebuffer.hpp"
#include "gui/widgets.hpp"
#include <Api/Syscall.hpp>
namespace gui {
enum WindowState { WIN_NORMAL, WIN_MINIMIZED, WIN_MAXIMIZED, WIN_CLOSED };
static constexpr int TITLEBAR_HEIGHT = 30;
static constexpr int BORDER_WIDTH = 1;
static constexpr int SHADOW_SIZE = 3;
static constexpr int BTN_RADIUS = 6;
static constexpr int MAX_TITLE_LEN = 64;
struct Window;
using WindowDrawCallback = void (*)(Window* win, Framebuffer& fb);
using WindowMouseCallback = void (*)(Window* win, MouseEvent& ev);
using WindowKeyCallback = void (*)(Window* win, const Zenith::KeyEvent& key);
using WindowCloseCallback = void (*)(Window* win);
struct Window {
char title[MAX_TITLE_LEN];
Rect frame;
WindowState state;
int z_order;
bool focused;
bool dirty;
uint32_t* content;
int content_w, content_h;
bool dragging;
int drag_offset_x, drag_offset_y;
bool resizing;
Rect saved_frame;
WindowDrawCallback on_draw;
WindowMouseCallback on_mouse;
WindowKeyCallback on_key;
WindowCloseCallback on_close;
void* app_data;
Rect titlebar_rect() const {
return {frame.x, frame.y, frame.w, TITLEBAR_HEIGHT};
}
Rect content_rect() const {
return {frame.x + BORDER_WIDTH, frame.y + TITLEBAR_HEIGHT,
frame.w - 2 * BORDER_WIDTH, frame.h - TITLEBAR_HEIGHT - BORDER_WIDTH};
}
Rect close_btn_rect() const {
int by = frame.y + (TITLEBAR_HEIGHT - BTN_RADIUS * 2) / 2;
return {frame.x + 12, by, BTN_RADIUS * 2, BTN_RADIUS * 2};
}
Rect min_btn_rect() const {
int by = frame.y + (TITLEBAR_HEIGHT - BTN_RADIUS * 2) / 2;
return {frame.x + 12 + 22, by, BTN_RADIUS * 2, BTN_RADIUS * 2};
}
Rect max_btn_rect() const {
int by = frame.y + (TITLEBAR_HEIGHT - BTN_RADIUS * 2) / 2;
return {frame.x + 12 + 44, by, BTN_RADIUS * 2, BTN_RADIUS * 2};
}
};
} // namespace gui
+23
View File
@@ -256,4 +256,27 @@ namespace zenith {
__builtin_unreachable();
}
// Mouse
inline void mouse_state(Zenith::MouseState* out) { syscall1(Zenith::SYS_MOUSESTATE, (uint64_t)out); }
inline void set_mouse_bounds(int32_t maxX, int32_t maxY) {
syscall2(Zenith::SYS_SETMOUSEBOUNDS, (uint64_t)maxX, (uint64_t)maxY);
}
// I/O redirection
inline int spawn_redir(const char* path, const char* args = nullptr) {
return (int)syscall2(Zenith::SYS_SPAWN_REDIR, (uint64_t)path, (uint64_t)args);
}
inline int childio_read(int childPid, char* buf, int maxLen) {
return (int)syscall3(Zenith::SYS_CHILDIO_READ, (uint64_t)childPid, (uint64_t)buf, (uint64_t)maxLen);
}
inline int childio_write(int childPid, const char* data, int len) {
return (int)syscall3(Zenith::SYS_CHILDIO_WRITE, (uint64_t)childPid, (uint64_t)data, (uint64_t)len);
}
inline int childio_writekey(int childPid, const Zenith::KeyEvent* key) {
return (int)syscall2(Zenith::SYS_CHILDIO_WRITEKEY, (uint64_t)childPid, (uint64_t)key);
}
inline int childio_settermsz(int childPid, int cols, int rows) {
return (int)syscall3(Zenith::SYS_CHILDIO_SETTERMSZ, (uint64_t)childPid, (uint64_t)cols, (uint64_t)rows);
}
}