diff --git a/kernel/src/Api/Syscall.cpp b/kernel/src/Api/Syscall.cpp index c406057..73df553 100644 --- a/kernel/src/Api/Syscall.cpp +++ b/kernel/src/Api/Syscall.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -53,11 +54,53 @@ namespace Zenith { return Sched::GetCurrentPid(); } + static void RingWrite(uint8_t* buf, uint32_t& head, uint32_t /*tail*/, uint32_t size, uint8_t byte) { + buf[head] = byte; + head = (head + 1) % size; + } + + static int RingRead(uint8_t* buf, uint32_t& head, uint32_t& tail, uint32_t size, uint8_t* out, int maxLen) { + int count = 0; + while (tail != head && count < maxLen) { + out[count++] = buf[tail]; + tail = (tail + 1) % size; + } + return count; + } + + // Find the process that owns the I/O ring buffers for a redirected process. + // If proc owns buffers itself (spawned via spawn_redir), returns proc. + // If proc inherited redirection (spawned via spawn from a redirected parent), + // follows parentPid to find the buffer owner. + static Sched::Process* GetRedirTarget(Sched::Process* proc) { + if (!proc || !proc->redirected) return nullptr; + if (proc->outBuf) return proc; // owns buffers + return Sched::GetProcessByPid(proc->parentPid); + } + static void Sys_Print(const char* text) { + auto* proc = Sched::GetCurrentProcessPtr(); + if (proc && proc->redirected) { + auto* target = GetRedirTarget(proc); + if (target && target->outBuf) { + for (int i = 0; text[i]; i++) { + RingWrite(target->outBuf, target->outHead, target->outTail, Sched::Process::IoBufSize, (uint8_t)text[i]); + } + return; + } + } Kt::Print(text); } static void Sys_Putchar(char c) { + auto* proc = Sched::GetCurrentProcessPtr(); + if (proc && proc->redirected) { + auto* target = GetRedirTarget(proc); + if (target && target->outBuf) { + RingWrite(target->outBuf, target->outHead, target->outTail, Sched::Process::IoBufSize, (uint8_t)c); + return; + } + } Kt::Putchar(c); } @@ -162,11 +205,29 @@ namespace Zenith { } static bool Sys_IsKeyAvailable() { + auto* proc = Sched::GetCurrentProcessPtr(); + if (proc && proc->redirected) { + auto* target = GetRedirTarget(proc); + if (target) return target->keyHead != target->keyTail; + } return Drivers::PS2::Keyboard::IsKeyAvailable(); } static void Sys_GetKey(KeyEvent* outEvent) { if (outEvent == nullptr) return; + auto* proc = Sched::GetCurrentProcessPtr(); + if (proc && proc->redirected) { + auto* target = GetRedirTarget(proc); + if (target) { + // Wait for key in target's keyBuf ring + while (target->keyHead == target->keyTail) { + Sched::Schedule(); + } + *outEvent = target->keyBuf[target->keyTail]; + target->keyTail = (target->keyTail + 1) % 64; + return; + } + } auto k = Drivers::PS2::Keyboard::GetKey(); outEvent->scancode = k.Scancode; outEvent->ascii = k.Ascii; @@ -177,6 +238,19 @@ namespace Zenith { } static char Sys_GetChar() { + auto* proc = Sched::GetCurrentProcessPtr(); + if (proc && proc->redirected) { + auto* target = GetRedirTarget(proc); + if (target && target->inBuf) { + // Wait for data in target's inBuf ring + while (target->inTail == target->inHead) { + Sched::Schedule(); // yield until parent writes + } + uint8_t c = target->inBuf[target->inTail]; + target->inTail = (target->inTail + 1) % Sched::Process::IoBufSize; + return (char)c; + } + } return Drivers::PS2::Keyboard::GetChar(); } @@ -249,7 +323,26 @@ namespace Zenith { } static int Sys_Spawn(const char* path, const char* args) { - return Sched::Spawn(path, args); + auto* parent = Sched::GetCurrentProcessPtr(); + int childPid = Sched::Spawn(path, args); + if (childPid < 0) return childPid; + + // Inherit I/O redirection: if the parent is redirected, the child + // is marked redirected too. It stores a parentPid pointing to the + // process that owns the actual ring buffers (the one spawned via + // spawn_redir). The child does NOT get its own buffers — Sys_Print + // et al. look up the buffer owner at write time. + if (parent && parent->redirected) { + auto* child = Sched::GetProcessByPid(childPid); + if (child) { + child->redirected = true; + // Point to the buffer owner: if parent owns buffers, target parent; + // if parent itself inherited, follow the chain. + child->parentPid = parent->outBuf ? parent->pid : parent->parentPid; + } + } + + return childPid; } static int Sys_GetArgs(char* buf, uint64_t maxLen) { @@ -264,6 +357,14 @@ namespace Zenith { } static uint64_t Sys_TermSize() { + // If the process is redirected to a GUI terminal, return those dimensions + auto* proc = Sched::GetCurrentProcessPtr(); + if (proc && proc->redirected) { + auto* target = GetRedirTarget(proc); + if (target && target->termCols > 0 && target->termRows > 0) { + return ((uint64_t)target->termRows << 32) | ((uint64_t)target->termCols & 0xFFFFFFFF); + } + } size_t cols = 0, rows = 0; flanterm_get_dimensions(Kt::ctx, &cols, &rows); return (rows << 32) | (cols & 0xFFFFFFFF); @@ -429,6 +530,81 @@ namespace Zenith { return (int64_t)len; } + // ---- Mouse syscalls ---- + + static void Sys_MouseState(MouseState* out) { + if (out == nullptr) return; + auto state = Drivers::PS2::Mouse::GetMouseState(); + out->x = state.X; + out->y = state.Y; + out->scrollDelta = state.ScrollDelta; + out->buttons = state.Buttons; + } + + static void Sys_SetMouseBounds(int32_t maxX, int32_t maxY) { + Drivers::PS2::Mouse::SetBounds(maxX, maxY); + } + + // ---- I/O redirection syscalls ---- + + static int Sys_SpawnRedir(const char* path, const char* args) { + int childPid = Sched::Spawn(path, args); + if (childPid < 0) return -1; + + auto* child = Sched::GetProcessByPid(childPid); + if (child == nullptr) return -1; + + // Allocate ring buffers + void* outPage = Memory::g_pfa->AllocateZeroed(); + void* inPage = Memory::g_pfa->AllocateZeroed(); + if (!outPage || !inPage) return -1; + + child->outBuf = (uint8_t*)outPage; + child->inBuf = (uint8_t*)inPage; + child->outHead = 0; + child->outTail = 0; + child->inHead = 0; + child->inTail = 0; + child->keyHead = 0; + child->keyTail = 0; + child->redirected = true; + child->parentPid = Sched::GetCurrentPid(); + + return childPid; + } + + static int Sys_ChildIoRead(int childPid, char* buf, int maxLen) { + auto* child = Sched::GetProcessByPid(childPid); + if (child == nullptr || !child->redirected || !child->outBuf) return -1; + return RingRead(child->outBuf, child->outHead, child->outTail, Sched::Process::IoBufSize, (uint8_t*)buf, maxLen); + } + + static int Sys_ChildIoWrite(int childPid, const char* data, int len) { + auto* child = Sched::GetProcessByPid(childPid); + if (child == nullptr || !child->redirected || !child->inBuf) return -1; + for (int i = 0; i < len; i++) { + RingWrite(child->inBuf, child->inHead, child->inTail, Sched::Process::IoBufSize, (uint8_t)data[i]); + } + return len; + } + + static int Sys_ChildIoWriteKey(int childPid, const KeyEvent* key) { + if (key == nullptr) return -1; + auto* child = Sched::GetProcessByPid(childPid); + if (child == nullptr || !child->redirected) return -1; + child->keyBuf[child->keyHead] = *key; + child->keyHead = (child->keyHead + 1) % 64; + return 0; + } + + static int Sys_ChildIoSetTermsz(int childPid, int cols, int rows) { + auto* child = Sched::GetProcessByPid(childPid); + if (child == nullptr || !child->redirected) return -1; + child->termCols = cols; + child->termRows = rows; + return 0; + } + // ---- Dispatch ---- extern "C" int64_t SyscallDispatch(SyscallFrame* frame) { @@ -551,6 +727,22 @@ namespace Zenith { return Sys_GetRandom((uint8_t*)frame->arg1, frame->arg2); case SYS_KLOG: return Kt::ReadKernelLog((char*)frame->arg1, frame->arg2); + case SYS_MOUSESTATE: + Sys_MouseState((MouseState*)frame->arg1); + return 0; + case SYS_SETMOUSEBOUNDS: + Sys_SetMouseBounds((int32_t)frame->arg1, (int32_t)frame->arg2); + return 0; + case SYS_SPAWN_REDIR: + return (int64_t)Sys_SpawnRedir((const char*)frame->arg1, (const char*)frame->arg2); + case SYS_CHILDIO_READ: + return (int64_t)Sys_ChildIoRead((int)frame->arg1, (char*)frame->arg2, (int)frame->arg3); + case SYS_CHILDIO_WRITE: + return (int64_t)Sys_ChildIoWrite((int)frame->arg1, (const char*)frame->arg2, (int)frame->arg3); + case SYS_CHILDIO_WRITEKEY: + return (int64_t)Sys_ChildIoWriteKey((int)frame->arg1, (const KeyEvent*)frame->arg2); + case SYS_CHILDIO_SETTERMSZ: + return (int64_t)Sys_ChildIoSetTermsz((int)frame->arg1, (int)frame->arg2, (int)frame->arg3); default: return -1; } @@ -577,7 +769,7 @@ namespace Zenith { Hal::WriteMSR(Hal::IA32_FMASK, 0x200); Kt::KernelLogStream(Kt::OK, "Syscall") << "SYSCALL/SYSRET initialized (LSTAR=" - << kcp::hex << (uint64_t)SyscallEntry << kcp::dec << ", 47 syscalls)"; + << kcp::hex << (uint64_t)SyscallEntry << kcp::dec << ", 53 syscalls)"; } } diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index 14aadbe..13e9026 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -57,7 +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_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; @@ -104,6 +111,13 @@ namespace Zenith { bool alt; }; + struct MouseState { + int32_t x; + int32_t y; + int32_t scrollDelta; + uint8_t buttons; + }; + // Stack frame pushed by SyscallEntry.asm struct SyscallFrame { uint64_t r15, r14, r13, r12, rbp, rbx; // callee-saved diff --git a/kernel/src/Sched/Scheduler.cpp b/kernel/src/Sched/Scheduler.cpp index 865b58a..7b2de94 100644 --- a/kernel/src/Sched/Scheduler.cpp +++ b/kernel/src/Sched/Scheduler.cpp @@ -76,6 +76,18 @@ namespace Sched { processTable[i].userStackTop = 0; processTable[i].heapNext = 0; processTable[i].args[0] = '\0'; + processTable[i].redirected = false; + processTable[i].parentPid = -1; + processTable[i].outBuf = nullptr; + processTable[i].outHead = 0; + processTable[i].outTail = 0; + processTable[i].inBuf = nullptr; + processTable[i].inHead = 0; + processTable[i].inTail = 0; + processTable[i].keyHead = 0; + processTable[i].keyTail = 0; + processTable[i].termCols = 0; + processTable[i].termRows = 0; } currentPid = -1; @@ -199,6 +211,19 @@ namespace Sched { proc.args[i] = '\0'; } + proc.redirected = false; + proc.parentPid = -1; + proc.outBuf = nullptr; + proc.outHead = 0; + proc.outTail = 0; + proc.inBuf = nullptr; + proc.inHead = 0; + proc.inTail = 0; + proc.keyHead = 0; + proc.keyTail = 0; + proc.termCols = 0; + proc.termRows = 0; + return proc.pid; } @@ -326,4 +351,15 @@ namespace Sched { return false; } + Process* GetProcessByPid(int pid) { + for (int i = 0; i < MaxProcesses; i++) { + if (processTable[i].pid == pid && + (processTable[i].state == ProcessState::Ready || + processTable[i].state == ProcessState::Running)) { + return &processTable[i]; + } + } + return nullptr; + } + } diff --git a/kernel/src/Sched/Scheduler.hpp b/kernel/src/Sched/Scheduler.hpp index d617953..04b35d9 100644 --- a/kernel/src/Sched/Scheduler.hpp +++ b/kernel/src/Sched/Scheduler.hpp @@ -6,6 +6,7 @@ #pragma once #include +#include namespace Sched { @@ -39,6 +40,24 @@ namespace Sched { uint64_t userStackTop; // User-space stack top uint64_t heapNext; // Simple bump allocator for user heap char args[256]; // Command-line arguments (set by parent via Spawn) + + // I/O redirection for GUI terminal + bool redirected = false; + int parentPid = -1; + uint8_t* outBuf = nullptr; // 4KB ring: child writes (print/putchar), parent reads + uint32_t outHead = 0; + uint32_t outTail = 0; + uint8_t* inBuf = nullptr; // 4KB ring: parent writes, child reads (getchar) + uint32_t inHead = 0; + uint32_t inTail = 0; + Zenith::KeyEvent keyBuf[64]; // parent injects, child reads (getkey/iskeyavailable) + uint32_t keyHead = 0; + uint32_t keyTail = 0; + static constexpr uint32_t IoBufSize = 4096; + + // GUI terminal dimensions (set by desktop, read by SYS_TERMSIZE) + int termCols = 0; + int termRows = 0; }; void Initialize(); @@ -60,4 +79,7 @@ namespace Sched { // Check if a process is still alive (Ready or Running) bool IsAlive(int pid); + // Find a process by PID (returns nullptr if not found or not alive) + Process* GetProcessByPid(int pid); + } diff --git a/kernel/src/Timekeeping/ApicTimer.cpp b/kernel/src/Timekeeping/ApicTimer.cpp index 3ccb57e..70e7350 100644 --- a/kernel/src/Timekeeping/ApicTimer.cpp +++ b/kernel/src/Timekeeping/ApicTimer.cpp @@ -149,7 +149,11 @@ namespace Timekeeping { void Sleep(uint64_t ms) { uint64_t target = g_tickCount + ms; while (g_tickCount < target) { - asm volatile("hlt"); + // Yield to other processes instead of hlt. Using hlt here causes + // a deadlock: if the timer ISR preempts us during hlt and context- + // switches away (via Tick → Schedule), EOI is never sent, so no + // more timer interrupts fire and any process doing hlt freezes. + Sched::Schedule(); } } }; diff --git a/programs/GNUmakefile b/programs/GNUmakefile index 7505807..0d8ecc1 100644 --- a/programs/GNUmakefile +++ b/programs/GNUmakefile @@ -57,7 +57,7 @@ BINDIR := bin PROGRAMS := $(notdir $(wildcard src/*)) # Programs with custom Makefiles (built separately). -CUSTOM_BUILDS := doom fetch wiki +CUSTOM_BUILDS := doom fetch wiki desktop SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS)) # Build targets: system programs go to bin/os/, games are handled separately. @@ -82,9 +82,9 @@ CA_CERTS := $(BINDIR)/etc/ca-certificates.crt # Home directory placeholder. HOMEKEEP := $(BINDIR)/home/.keep -.PHONY: all clean doom fetch wiki bearssl libc +.PHONY: all clean doom fetch wiki desktop icons bearssl libc -all: bearssl libc $(TARGETS) fetch wiki doom $(GAME_DATA) $(MANDST) $(WWWDST) $(CA_CERTS) $(HOMEKEEP) +all: bearssl libc $(TARGETS) fetch wiki doom desktop icons $(GAME_DATA) $(MANDST) $(WWWDST) $(CA_CERTS) $(HOMEKEEP) # Build BearSSL static library. bearssl: @@ -102,6 +102,14 @@ fetch: bearssl libc wiki: bearssl libc $(MAKE) -C src/wiki +# Build desktop via its own Makefile. +desktop: + $(MAKE) -C src/desktop + +# Copy SVG icons for the desktop into bin/icons/. +icons: + ../scripts/copy_icons.sh + # Build doom via its own Makefile. doom: $(MAKE) -C src/doom @@ -143,3 +151,4 @@ clean: $(MAKE) -C lib/libc clean $(MAKE) -C src/fetch clean $(MAKE) -C src/doom clean + $(MAKE) -C src/desktop clean diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index 9c00699..7ff7a20 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -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; + }; + } diff --git a/programs/include/gui/desktop.hpp b/programs/include/gui/desktop.hpp new file mode 100644 index 0000000..809bf3c --- /dev/null +++ b/programs/include/gui/desktop.hpp @@ -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 + +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 diff --git a/programs/include/gui/draw.hpp b/programs/include/gui/draw.hpp new file mode 100644 index 0000000..1362ed6 --- /dev/null +++ b/programs/include/gui/draw.hpp @@ -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 diff --git a/programs/include/gui/font.hpp b/programs/include/gui/font.hpp new file mode 100644 index 0000000..11c5f6a --- /dev/null +++ b/programs/include/gui/font.hpp @@ -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 diff --git a/programs/include/gui/framebuffer.hpp b/programs/include/gui/framebuffer.hpp new file mode 100644 index 0000000..6ae1619 --- /dev/null +++ b/programs/include/gui/framebuffer.hpp @@ -0,0 +1,196 @@ +/* + * framebuffer.hpp + * ZenithOS double-buffered framebuffer abstraction + * Copyright (c) 2025 Daniel Hammer +*/ + +#pragma once +#include +#include +#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 diff --git a/programs/include/gui/gui.hpp b/programs/include/gui/gui.hpp new file mode 100644 index 0000000..492159d --- /dev/null +++ b/programs/include/gui/gui.hpp @@ -0,0 +1,92 @@ +/* + * gui.hpp + * ZenithOS core GUI types and utilities + * Copyright (c) 2025 Daniel Hammer +*/ + +#pragma once +#include + +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 diff --git a/programs/include/gui/svg.hpp b/programs/include/gui/svg.hpp new file mode 100644 index 0000000..0c10b0b --- /dev/null +++ b/programs/include/gui/svg.hpp @@ -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 + +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 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 = end) break; + + int remaining = (int)(end - p); + + // Check for 5 && svg_strncmp(p, "' + 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 7 && svg_strncmp(p, " 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 5 && svg_strncmp(p, " 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 diff --git a/programs/include/gui/terminal.hpp b/programs/include/gui/terminal.hpp new file mode 100644 index 0000000..2f30b2a --- /dev/null +++ b/programs/include/gui/terminal.hpp @@ -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 +#include +#include + +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 diff --git a/programs/include/gui/widgets.hpp b/programs/include/gui/widgets.hpp new file mode 100644 index 0000000..121ebf8 --- /dev/null +++ b/programs/include/gui/widgets.hpp @@ -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 diff --git a/programs/include/gui/window.hpp b/programs/include/gui/window.hpp new file mode 100644 index 0000000..67f42a4 --- /dev/null +++ b/programs/include/gui/window.hpp @@ -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 + +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 diff --git a/programs/include/zenith/syscall.h b/programs/include/zenith/syscall.h index a07486b..9aae5e4 100644 --- a/programs/include/zenith/syscall.h +++ b/programs/include/zenith/syscall.h @@ -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); + } + } diff --git a/programs/obj/hello/main.o b/programs/obj/hello/main.o index dcdbf6c..bb43a93 100644 Binary files a/programs/obj/hello/main.o and b/programs/obj/hello/main.o differ diff --git a/programs/obj/shell/main.o b/programs/obj/shell/main.o index c92f767..9cb6b0a 100644 Binary files a/programs/obj/shell/main.o and b/programs/obj/shell/main.o differ diff --git a/programs/src/desktop/Makefile b/programs/src/desktop/Makefile new file mode 100644 index 0000000..9921374 --- /dev/null +++ b/programs/src/desktop/Makefile @@ -0,0 +1,86 @@ +# Makefile for desktop (GUI desktop environment) on ZenithOS +# Copyright (c) 2025 Daniel Hammer + +MAKEFLAGS += -rR +.SUFFIXES: + +# ---- Toolchain ---- + +TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf- +ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),) + CXX := $(TOOLCHAIN_PREFIX)g++ +else + CXX := g++ +endif + +# ---- Paths ---- + +PROG_INC := ../../include +LINK_LD := ../../link.ld +BINDIR := ../../bin +OBJDIR := obj + +# ---- Compiler flags ---- + +CXXFLAGS := \ + -std=gnu++20 \ + -g -O2 -pipe \ + -Wall \ + -Wextra \ + -Wno-unused-parameter \ + -Wno-unused-function \ + -nostdinc \ + -ffreestanding \ + -fno-stack-protector \ + -fno-stack-check \ + -fno-PIC \ + -fno-rtti \ + -fno-exceptions \ + -ffunction-sections \ + -fdata-sections \ + -m64 \ + -march=x86-64 \ + -mno-80387 \ + -mno-mmx \ + -mno-sse \ + -mno-sse2 \ + -mno-red-zone \ + -mcmodel=small \ + -I $(PROG_INC) \ + -isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \ + -isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include + +# ---- Linker flags ---- + +LDFLAGS := \ + -nostdlib \ + -static \ + -Wl,--build-id=none \ + -Wl,--gc-sections \ + -Wl,-m,elf_x86_64 \ + -z max-page-size=0x1000 \ + -T $(LINK_LD) + +# ---- Source files ---- + +SRCS := main.cpp font_data.cpp +OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) + +# ---- Target ---- + +TARGET := $(BINDIR)/os/desktop.elf + +.PHONY: all clean + +all: $(TARGET) + +$(TARGET): $(OBJS) $(LINK_LD) Makefile + mkdir -p $(BINDIR)/os + $(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) -o $@ + +$(OBJDIR)/%.o: %.cpp Makefile + mkdir -p $(OBJDIR) + $(CXX) $(CXXFLAGS) -c $< -o $@ + +clean: + rm -rf $(OBJDIR) $(TARGET) diff --git a/programs/src/desktop/font_data.cpp b/programs/src/desktop/font_data.cpp new file mode 100644 index 0000000..940f6e1 --- /dev/null +++ b/programs/src/desktop/font_data.cpp @@ -0,0 +1,783 @@ +/* + * font_data.cpp + * Standard VGA 8x16 bitmap font (Code Page 437) + * 256 characters, 16 bytes each (8 pixels wide, 1 bit per pixel, MSB left) + * Copyright (c) 2025 Daniel Hammer +*/ + +#include "gui/font.hpp" + +namespace gui { + +const uint8_t font_data[256 * 16] = { + // Character 0 (NUL) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 1 (smiley face) + 0x00, 0x00, 0x7E, 0x81, 0xA5, 0x81, 0x81, 0xBD, + 0x99, 0x81, 0x81, 0x7E, 0x00, 0x00, 0x00, 0x00, + // Character 2 (inverse smiley) + 0x00, 0x00, 0x7E, 0xFF, 0xDB, 0xFF, 0xFF, 0xC3, + 0xE7, 0xFF, 0xFF, 0x7E, 0x00, 0x00, 0x00, 0x00, + // Character 3 (heart) + 0x00, 0x00, 0x00, 0x00, 0x6C, 0xFE, 0xFE, 0xFE, + 0xFE, 0x7C, 0x38, 0x10, 0x00, 0x00, 0x00, 0x00, + // Character 4 (diamond) + 0x00, 0x00, 0x00, 0x00, 0x10, 0x38, 0x7C, 0xFE, + 0x7C, 0x38, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 5 (club) + 0x00, 0x00, 0x00, 0x18, 0x3C, 0x3C, 0xE7, 0xE7, + 0xE7, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00, + // Character 6 (spade) + 0x00, 0x00, 0x00, 0x18, 0x3C, 0x7E, 0xFF, 0xFF, + 0x7E, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00, + // Character 7 (bullet) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x3C, + 0x3C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 8 (inverse bullet) + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE7, 0xC3, + 0xC3, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + // Character 9 (circle) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x66, 0x42, + 0x42, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 10 (inverse circle) + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC3, 0x99, 0xBD, + 0xBD, 0x99, 0xC3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + // Character 11 (male sign) + 0x00, 0x00, 0x1E, 0x0E, 0x1A, 0x32, 0x78, 0xCC, + 0xCC, 0xCC, 0xCC, 0x78, 0x00, 0x00, 0x00, 0x00, + // Character 12 (female sign) + 0x00, 0x00, 0x3C, 0x66, 0x66, 0x66, 0x66, 0x3C, + 0x18, 0x7E, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, + // Character 13 (note) + 0x00, 0x00, 0x3F, 0x33, 0x3F, 0x30, 0x30, 0x30, + 0x30, 0x70, 0xF0, 0xE0, 0x00, 0x00, 0x00, 0x00, + // Character 14 (double note) + 0x00, 0x00, 0x7F, 0x63, 0x7F, 0x63, 0x63, 0x63, + 0x63, 0x67, 0xE7, 0xE6, 0xC0, 0x00, 0x00, 0x00, + // Character 15 (sun) + 0x00, 0x00, 0x00, 0x18, 0x18, 0xDB, 0x3C, 0xE7, + 0x3C, 0xDB, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, + // Character 16 (right triangle) + 0x00, 0x80, 0xC0, 0xE0, 0xF0, 0xF8, 0xFE, 0xF8, + 0xF0, 0xE0, 0xC0, 0x80, 0x00, 0x00, 0x00, 0x00, + // Character 17 (left triangle) + 0x00, 0x02, 0x06, 0x0E, 0x1E, 0x3E, 0xFE, 0x3E, + 0x1E, 0x0E, 0x06, 0x02, 0x00, 0x00, 0x00, 0x00, + // Character 18 (up-down arrow) + 0x00, 0x00, 0x18, 0x3C, 0x7E, 0x18, 0x18, 0x18, + 0x7E, 0x3C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 19 (double exclamation) + 0x00, 0x00, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, + 0x66, 0x00, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00, + // Character 20 (paragraph) + 0x00, 0x00, 0x7F, 0xDB, 0xDB, 0xDB, 0x7B, 0x1B, + 0x1B, 0x1B, 0x1B, 0x1B, 0x00, 0x00, 0x00, 0x00, + // Character 21 (section) + 0x00, 0x7C, 0xC6, 0x60, 0x38, 0x6C, 0xC6, 0xC6, + 0x6C, 0x38, 0x0C, 0xC6, 0x7C, 0x00, 0x00, 0x00, + // Character 22 (thick underscore) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFE, 0xFE, 0xFE, 0xFE, 0x00, 0x00, 0x00, 0x00, + // Character 23 (up-down arrow underlined) + 0x00, 0x00, 0x18, 0x3C, 0x7E, 0x18, 0x18, 0x18, + 0x7E, 0x3C, 0x18, 0x7E, 0x00, 0x00, 0x00, 0x00, + // Character 24 (up arrow) + 0x00, 0x00, 0x18, 0x3C, 0x7E, 0x18, 0x18, 0x18, + 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, + // Character 25 (down arrow) + 0x00, 0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, + 0x18, 0x7E, 0x3C, 0x18, 0x00, 0x00, 0x00, 0x00, + // Character 26 (right arrow) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0C, 0xFE, + 0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 27 (left arrow) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x60, 0xFE, + 0x60, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 28 (right angle) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xC0, + 0xC0, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 29 (left-right arrow) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x66, 0xFF, + 0x66, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 30 (up triangle) + 0x00, 0x00, 0x00, 0x00, 0x10, 0x38, 0x38, 0x7C, + 0x7C, 0xFE, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 31 (down triangle) + 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFE, 0x7C, 0x7C, + 0x38, 0x38, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 32 (space) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 33 '!' + 0x00, 0x00, 0x18, 0x3C, 0x3C, 0x3C, 0x18, 0x18, + 0x18, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, + // Character 34 '"' + 0x00, 0x66, 0x66, 0x66, 0x24, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 35 '#' + 0x00, 0x00, 0x00, 0x6C, 0x6C, 0xFE, 0x6C, 0x6C, + 0x6C, 0xFE, 0x6C, 0x6C, 0x00, 0x00, 0x00, 0x00, + // Character 36 '$' + 0x18, 0x18, 0x7C, 0xC6, 0xC2, 0xC0, 0x7C, 0x06, + 0x06, 0x86, 0xC6, 0x7C, 0x18, 0x18, 0x00, 0x00, + // Character 37 '%' + 0x00, 0x00, 0x00, 0x00, 0xC2, 0xC6, 0x0C, 0x18, + 0x30, 0x60, 0xC6, 0x86, 0x00, 0x00, 0x00, 0x00, + // Character 38 '&' + 0x00, 0x00, 0x38, 0x6C, 0x6C, 0x38, 0x76, 0xDC, + 0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, 0x00, + // Character 39 ''' + 0x00, 0x30, 0x30, 0x30, 0x60, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 40 '(' + 0x00, 0x00, 0x0C, 0x18, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x18, 0x0C, 0x00, 0x00, 0x00, 0x00, + // Character 41 ')' + 0x00, 0x00, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x18, 0x30, 0x00, 0x00, 0x00, 0x00, + // Character 42 '*' + 0x00, 0x00, 0x00, 0x00, 0x00, 0x66, 0x3C, 0xFF, + 0x3C, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 43 '+' + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x7E, + 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 44 ',' + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x18, 0x18, 0x18, 0x30, 0x00, 0x00, 0x00, + // Character 45 '-' + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 46 '.' + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, + // Character 47 '/' + 0x00, 0x00, 0x00, 0x00, 0x02, 0x06, 0x0C, 0x18, + 0x30, 0x60, 0xC0, 0x80, 0x00, 0x00, 0x00, 0x00, + // Character 48 '0' + 0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xCE, 0xDE, 0xF6, + 0xE6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00, + // Character 49 '1' + 0x00, 0x00, 0x18, 0x38, 0x78, 0x18, 0x18, 0x18, + 0x18, 0x18, 0x18, 0x7E, 0x00, 0x00, 0x00, 0x00, + // Character 50 '2' + 0x00, 0x00, 0x7C, 0xC6, 0x06, 0x0C, 0x18, 0x30, + 0x60, 0xC0, 0xC6, 0xFE, 0x00, 0x00, 0x00, 0x00, + // Character 51 '3' + 0x00, 0x00, 0x7C, 0xC6, 0x06, 0x06, 0x3C, 0x06, + 0x06, 0x06, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00, + // Character 52 '4' + 0x00, 0x00, 0x0C, 0x1C, 0x3C, 0x6C, 0xCC, 0xFE, + 0x0C, 0x0C, 0x0C, 0x1E, 0x00, 0x00, 0x00, 0x00, + // Character 53 '5' + 0x00, 0x00, 0xFE, 0xC0, 0xC0, 0xC0, 0xFC, 0x06, + 0x06, 0x06, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00, + // Character 54 '6' + 0x00, 0x00, 0x38, 0x60, 0xC0, 0xC0, 0xFC, 0xC6, + 0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00, + // Character 55 '7' + 0x00, 0x00, 0xFE, 0xC6, 0x06, 0x06, 0x0C, 0x18, + 0x30, 0x30, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00, + // Character 56 '8' + 0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0x7C, 0xC6, + 0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00, + // Character 57 '9' + 0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0x7E, 0x06, + 0x06, 0x06, 0x0C, 0x78, 0x00, 0x00, 0x00, 0x00, + // Character 58 ':' + 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, + 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 59 ';' + 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, + 0x00, 0x18, 0x18, 0x30, 0x00, 0x00, 0x00, 0x00, + // Character 60 '<' + 0x00, 0x00, 0x00, 0x06, 0x0C, 0x18, 0x30, 0x60, + 0x30, 0x18, 0x0C, 0x06, 0x00, 0x00, 0x00, 0x00, + // Character 61 '=' + 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, + 0x7E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 62 '>' + 0x00, 0x00, 0x00, 0x60, 0x30, 0x18, 0x0C, 0x06, + 0x0C, 0x18, 0x30, 0x60, 0x00, 0x00, 0x00, 0x00, + // Character 63 '?' + 0x00, 0x00, 0x7C, 0xC6, 0xC6, 0x0C, 0x18, 0x18, + 0x18, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, + // Character 64 '@' + 0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0xDE, 0xDE, + 0xDE, 0xDC, 0xC0, 0x7C, 0x00, 0x00, 0x00, 0x00, + // Character 65 'A' + 0x00, 0x00, 0x10, 0x38, 0x6C, 0xC6, 0xC6, 0xFE, + 0xC6, 0xC6, 0xC6, 0xC6, 0x00, 0x00, 0x00, 0x00, + // Character 66 'B' + 0x00, 0x00, 0xFC, 0x66, 0x66, 0x66, 0x7C, 0x66, + 0x66, 0x66, 0x66, 0xFC, 0x00, 0x00, 0x00, 0x00, + // Character 67 'C' + 0x00, 0x00, 0x3C, 0x66, 0xC2, 0xC0, 0xC0, 0xC0, + 0xC0, 0xC2, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00, + // Character 68 'D' + 0x00, 0x00, 0xF8, 0x6C, 0x66, 0x66, 0x66, 0x66, + 0x66, 0x66, 0x6C, 0xF8, 0x00, 0x00, 0x00, 0x00, + // Character 69 'E' + 0x00, 0x00, 0xFE, 0x66, 0x62, 0x68, 0x78, 0x68, + 0x60, 0x62, 0x66, 0xFE, 0x00, 0x00, 0x00, 0x00, + // Character 70 'F' + 0x00, 0x00, 0xFE, 0x66, 0x62, 0x68, 0x78, 0x68, + 0x60, 0x60, 0x60, 0xF0, 0x00, 0x00, 0x00, 0x00, + // Character 71 'G' + 0x00, 0x00, 0x3C, 0x66, 0xC2, 0xC0, 0xC0, 0xDE, + 0xC6, 0xC6, 0x66, 0x3A, 0x00, 0x00, 0x00, 0x00, + // Character 72 'H' + 0x00, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0xFE, 0xC6, + 0xC6, 0xC6, 0xC6, 0xC6, 0x00, 0x00, 0x00, 0x00, + // Character 73 'I' + 0x00, 0x00, 0x3C, 0x18, 0x18, 0x18, 0x18, 0x18, + 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00, + // Character 74 'J' + 0x00, 0x00, 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0xCC, 0xCC, 0xCC, 0x78, 0x00, 0x00, 0x00, 0x00, + // Character 75 'K' + 0x00, 0x00, 0xE6, 0x66, 0x66, 0x6C, 0x78, 0x78, + 0x6C, 0x66, 0x66, 0xE6, 0x00, 0x00, 0x00, 0x00, + // Character 76 'L' + 0x00, 0x00, 0xF0, 0x60, 0x60, 0x60, 0x60, 0x60, + 0x60, 0x62, 0x66, 0xFE, 0x00, 0x00, 0x00, 0x00, + // Character 77 'M' + 0x00, 0x00, 0xC6, 0xEE, 0xFE, 0xFE, 0xD6, 0xC6, + 0xC6, 0xC6, 0xC6, 0xC6, 0x00, 0x00, 0x00, 0x00, + // Character 78 'N' + 0x00, 0x00, 0xC6, 0xE6, 0xF6, 0xFE, 0xDE, 0xCE, + 0xC6, 0xC6, 0xC6, 0xC6, 0x00, 0x00, 0x00, 0x00, + // Character 79 'O' + 0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, + 0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00, + // Character 80 'P' + 0x00, 0x00, 0xFC, 0x66, 0x66, 0x66, 0x7C, 0x60, + 0x60, 0x60, 0x60, 0xF0, 0x00, 0x00, 0x00, 0x00, + // Character 81 'Q' + 0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, + 0xC6, 0xD6, 0xDE, 0x7C, 0x0C, 0x0E, 0x00, 0x00, + // Character 82 'R' + 0x00, 0x00, 0xFC, 0x66, 0x66, 0x66, 0x7C, 0x6C, + 0x66, 0x66, 0x66, 0xE6, 0x00, 0x00, 0x00, 0x00, + // Character 83 'S' + 0x00, 0x00, 0x7C, 0xC6, 0xC6, 0x60, 0x38, 0x0C, + 0x06, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00, + // Character 84 'T' + 0x00, 0x00, 0xFF, 0xDB, 0x99, 0x18, 0x18, 0x18, + 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00, + // Character 85 'U' + 0x00, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, + 0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00, + // Character 86 'V' + 0x00, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, + 0xC6, 0x6C, 0x38, 0x10, 0x00, 0x00, 0x00, 0x00, + // Character 87 'W' + 0x00, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0xD6, 0xD6, + 0xD6, 0xFE, 0xEE, 0x6C, 0x00, 0x00, 0x00, 0x00, + // Character 88 'X' + 0x00, 0x00, 0xC6, 0xC6, 0x6C, 0x7C, 0x38, 0x38, + 0x7C, 0x6C, 0xC6, 0xC6, 0x00, 0x00, 0x00, 0x00, + // Character 89 'Y' + 0x00, 0x00, 0xC6, 0xC6, 0xC6, 0x6C, 0x38, 0x18, + 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00, + // Character 90 'Z' + 0x00, 0x00, 0xFE, 0xC6, 0x86, 0x0C, 0x18, 0x30, + 0x60, 0xC2, 0xC6, 0xFE, 0x00, 0x00, 0x00, 0x00, + // Character 91 '[' + 0x00, 0x00, 0x3C, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x3C, 0x00, 0x00, 0x00, 0x00, + // Character 92 '\' + 0x00, 0x00, 0x00, 0x80, 0xC0, 0xE0, 0x70, 0x38, + 0x1C, 0x0E, 0x06, 0x02, 0x00, 0x00, 0x00, 0x00, + // Character 93 ']' + 0x00, 0x00, 0x3C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x3C, 0x00, 0x00, 0x00, 0x00, + // Character 94 '^' + 0x10, 0x38, 0x6C, 0xC6, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 95 '_' + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, + // Character 96 '`' + 0x30, 0x30, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 97 'a' + 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x0C, 0x7C, + 0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, 0x00, + // Character 98 'b' + 0x00, 0x00, 0xE0, 0x60, 0x60, 0x78, 0x6C, 0x66, + 0x66, 0x66, 0x66, 0x7C, 0x00, 0x00, 0x00, 0x00, + // Character 99 'c' + 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0xC6, 0xC0, + 0xC0, 0xC0, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00, + // Character 100 'd' + 0x00, 0x00, 0x1C, 0x0C, 0x0C, 0x3C, 0x6C, 0xCC, + 0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, 0x00, + // Character 101 'e' + 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0xC6, 0xFE, + 0xC0, 0xC0, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00, + // Character 102 'f' + 0x00, 0x00, 0x38, 0x6C, 0x64, 0x60, 0xF0, 0x60, + 0x60, 0x60, 0x60, 0xF0, 0x00, 0x00, 0x00, 0x00, + // Character 103 'g' + 0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0xCC, 0xCC, + 0xCC, 0xCC, 0xCC, 0x7C, 0x0C, 0xCC, 0x78, 0x00, + // Character 104 'h' + 0x00, 0x00, 0xE0, 0x60, 0x60, 0x6C, 0x76, 0x66, + 0x66, 0x66, 0x66, 0xE6, 0x00, 0x00, 0x00, 0x00, + // Character 105 'i' + 0x00, 0x00, 0x18, 0x18, 0x00, 0x38, 0x18, 0x18, + 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00, + // Character 106 'j' + 0x00, 0x00, 0x06, 0x06, 0x00, 0x0E, 0x06, 0x06, + 0x06, 0x06, 0x06, 0x06, 0x66, 0x66, 0x3C, 0x00, + // Character 107 'k' + 0x00, 0x00, 0xE0, 0x60, 0x60, 0x66, 0x6C, 0x78, + 0x78, 0x6C, 0x66, 0xE6, 0x00, 0x00, 0x00, 0x00, + // Character 108 'l' + 0x00, 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x18, + 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00, + // Character 109 'm' + 0x00, 0x00, 0x00, 0x00, 0x00, 0xEC, 0xFE, 0xD6, + 0xD6, 0xD6, 0xD6, 0xC6, 0x00, 0x00, 0x00, 0x00, + // Character 110 'n' + 0x00, 0x00, 0x00, 0x00, 0x00, 0xDC, 0x66, 0x66, + 0x66, 0x66, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00, + // Character 111 'o' + 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0xC6, 0xC6, + 0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00, + // Character 112 'p' + 0x00, 0x00, 0x00, 0x00, 0x00, 0xDC, 0x66, 0x66, + 0x66, 0x66, 0x66, 0x7C, 0x60, 0x60, 0xF0, 0x00, + // Character 113 'q' + 0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0xCC, 0xCC, + 0xCC, 0xCC, 0xCC, 0x7C, 0x0C, 0x0C, 0x1E, 0x00, + // Character 114 'r' + 0x00, 0x00, 0x00, 0x00, 0x00, 0xDC, 0x76, 0x66, + 0x60, 0x60, 0x60, 0xF0, 0x00, 0x00, 0x00, 0x00, + // Character 115 's' + 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0xC6, 0x60, + 0x38, 0x0C, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00, + // Character 116 't' + 0x00, 0x00, 0x10, 0x30, 0x30, 0xFC, 0x30, 0x30, + 0x30, 0x30, 0x36, 0x1C, 0x00, 0x00, 0x00, 0x00, + // Character 117 'u' + 0x00, 0x00, 0x00, 0x00, 0x00, 0xCC, 0xCC, 0xCC, + 0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, 0x00, + // Character 118 'v' + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC6, 0xC6, 0xC6, + 0xC6, 0x6C, 0x38, 0x10, 0x00, 0x00, 0x00, 0x00, + // Character 119 'w' + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC6, 0xC6, 0xD6, + 0xD6, 0xD6, 0xFE, 0x6C, 0x00, 0x00, 0x00, 0x00, + // Character 120 'x' + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC6, 0x6C, 0x38, + 0x38, 0x38, 0x6C, 0xC6, 0x00, 0x00, 0x00, 0x00, + // Character 121 'y' + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC6, 0xC6, 0xC6, + 0xC6, 0xC6, 0xC6, 0x7E, 0x06, 0x0C, 0xF8, 0x00, + // Character 122 'z' + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xCC, 0x18, + 0x30, 0x60, 0xC6, 0xFE, 0x00, 0x00, 0x00, 0x00, + // Character 123 '{' + 0x00, 0x00, 0x0E, 0x18, 0x18, 0x18, 0x70, 0x18, + 0x18, 0x18, 0x18, 0x0E, 0x00, 0x00, 0x00, 0x00, + // Character 124 '|' + 0x00, 0x00, 0x18, 0x18, 0x18, 0x18, 0x00, 0x18, + 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, + // Character 125 '}' + 0x00, 0x00, 0x70, 0x18, 0x18, 0x18, 0x0E, 0x18, + 0x18, 0x18, 0x18, 0x70, 0x00, 0x00, 0x00, 0x00, + // Character 126 '~' + 0x00, 0x00, 0x76, 0xDC, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 127 (DEL - block) + 0x00, 0x00, 0x00, 0x00, 0x10, 0x38, 0x6C, 0xC6, + 0xC6, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 128 (C cedilla) + 0x00, 0x00, 0x3C, 0x66, 0xC2, 0xC0, 0xC0, 0xC0, + 0xC2, 0x66, 0x3C, 0x0C, 0x06, 0x7C, 0x00, 0x00, + // Character 129 (u umlaut) + 0x00, 0x00, 0xCC, 0x00, 0x00, 0xCC, 0xCC, 0xCC, + 0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, 0x00, + // Character 130 (e acute) + 0x00, 0x0C, 0x18, 0x30, 0x00, 0x7C, 0xC6, 0xFE, + 0xC0, 0xC0, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00, + // Character 131 (a circumflex) + 0x00, 0x10, 0x38, 0x6C, 0x00, 0x78, 0x0C, 0x7C, + 0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, 0x00, + // Character 132 (a umlaut) + 0x00, 0x00, 0xCC, 0x00, 0x00, 0x78, 0x0C, 0x7C, + 0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, 0x00, + // Character 133 (a grave) + 0x00, 0x60, 0x30, 0x18, 0x00, 0x78, 0x0C, 0x7C, + 0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, 0x00, + // Character 134 (a ring) + 0x00, 0x38, 0x6C, 0x38, 0x00, 0x78, 0x0C, 0x7C, + 0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, 0x00, + // Character 135 (c cedilla) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0xC6, 0xC0, + 0xC0, 0xC6, 0x7C, 0x0C, 0x06, 0x7C, 0x00, 0x00, + // Character 136 (e circumflex) + 0x00, 0x10, 0x38, 0x6C, 0x00, 0x7C, 0xC6, 0xFE, + 0xC0, 0xC0, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00, + // Character 137 (e umlaut) + 0x00, 0x00, 0xC6, 0x00, 0x00, 0x7C, 0xC6, 0xFE, + 0xC0, 0xC0, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00, + // Character 138 (e grave) + 0x00, 0x60, 0x30, 0x18, 0x00, 0x7C, 0xC6, 0xFE, + 0xC0, 0xC0, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00, + // Character 139 (i umlaut) + 0x00, 0x00, 0x66, 0x00, 0x00, 0x38, 0x18, 0x18, + 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00, + // Character 140 (i circumflex) + 0x00, 0x18, 0x3C, 0x66, 0x00, 0x38, 0x18, 0x18, + 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00, + // Character 141 (i grave) + 0x00, 0x60, 0x30, 0x18, 0x00, 0x38, 0x18, 0x18, + 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00, + // Character 142 (A umlaut) + 0x00, 0xC6, 0x00, 0x10, 0x38, 0x6C, 0xC6, 0xC6, + 0xFE, 0xC6, 0xC6, 0xC6, 0x00, 0x00, 0x00, 0x00, + // Character 143 (A ring) + 0x38, 0x6C, 0x38, 0x10, 0x38, 0x6C, 0xC6, 0xC6, + 0xFE, 0xC6, 0xC6, 0xC6, 0x00, 0x00, 0x00, 0x00, + // Character 144 (E acute) + 0x0C, 0x18, 0x00, 0xFE, 0x66, 0x62, 0x68, 0x78, + 0x68, 0x62, 0x66, 0xFE, 0x00, 0x00, 0x00, 0x00, + // Character 145 (ae) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x6E, 0x3B, 0x1B, + 0x7E, 0xD8, 0xDC, 0x77, 0x00, 0x00, 0x00, 0x00, + // Character 146 (AE) + 0x00, 0x00, 0x3E, 0x6C, 0xCC, 0xCC, 0xFE, 0xCC, + 0xCC, 0xCC, 0xCC, 0xCE, 0x00, 0x00, 0x00, 0x00, + // Character 147 (o circumflex) + 0x00, 0x10, 0x38, 0x6C, 0x00, 0x7C, 0xC6, 0xC6, + 0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00, + // Character 148 (o umlaut) + 0x00, 0x00, 0xC6, 0x00, 0x00, 0x7C, 0xC6, 0xC6, + 0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00, + // Character 149 (o grave) + 0x00, 0x60, 0x30, 0x18, 0x00, 0x7C, 0xC6, 0xC6, + 0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00, + // Character 150 (u circumflex) + 0x00, 0x30, 0x78, 0xCC, 0x00, 0xCC, 0xCC, 0xCC, + 0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, 0x00, + // Character 151 (u grave) + 0x00, 0x60, 0x30, 0x18, 0x00, 0xCC, 0xCC, 0xCC, + 0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, 0x00, + // Character 152 (y umlaut) + 0x00, 0x00, 0xC6, 0x00, 0x00, 0xC6, 0xC6, 0xC6, + 0xC6, 0xC6, 0xC6, 0x7E, 0x06, 0x0C, 0x78, 0x00, + // Character 153 (O umlaut) + 0x00, 0xC6, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0xC6, + 0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00, + // Character 154 (U umlaut) + 0x00, 0xC6, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, + 0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00, + // Character 155 (cent) + 0x00, 0x18, 0x18, 0x7C, 0xC6, 0xC0, 0xC0, 0xC0, + 0xC6, 0x7C, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, + // Character 156 (pound) + 0x00, 0x38, 0x6C, 0x64, 0x60, 0xF0, 0x60, 0x60, + 0x60, 0x60, 0xE6, 0xFC, 0x00, 0x00, 0x00, 0x00, + // Character 157 (yen) + 0x00, 0x00, 0xC6, 0x6C, 0x38, 0x38, 0xFE, 0x38, + 0xFE, 0x38, 0x38, 0x38, 0x00, 0x00, 0x00, 0x00, + // Character 158 (Pt) + 0x00, 0xF8, 0xCC, 0xCC, 0xF8, 0xC4, 0xCC, 0xDE, + 0xCC, 0xCC, 0xCC, 0xC6, 0x00, 0x00, 0x00, 0x00, + // Character 159 (f hook) + 0x00, 0x0E, 0x1B, 0x18, 0x18, 0x18, 0x7E, 0x18, + 0x18, 0x18, 0xD8, 0x70, 0x00, 0x00, 0x00, 0x00, + // Character 160 (a acute) + 0x00, 0x18, 0x30, 0x60, 0x00, 0x78, 0x0C, 0x7C, + 0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, 0x00, + // Character 161 (i acute) + 0x00, 0x0C, 0x18, 0x30, 0x00, 0x38, 0x18, 0x18, + 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00, + // Character 162 (o acute) + 0x00, 0x18, 0x30, 0x60, 0x00, 0x7C, 0xC6, 0xC6, + 0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00, + // Character 163 (u acute) + 0x00, 0x18, 0x30, 0x60, 0x00, 0xCC, 0xCC, 0xCC, + 0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, 0x00, + // Character 164 (n tilde) + 0x00, 0x00, 0x76, 0xDC, 0x00, 0xDC, 0x66, 0x66, + 0x66, 0x66, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00, + // Character 165 (N tilde) + 0x76, 0xDC, 0x00, 0xC6, 0xE6, 0xF6, 0xFE, 0xDE, + 0xCE, 0xC6, 0xC6, 0xC6, 0x00, 0x00, 0x00, 0x00, + // Character 166 (feminine ordinal) + 0x00, 0x3C, 0x6C, 0x6C, 0x3E, 0x00, 0x7E, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 167 (masculine ordinal) + 0x00, 0x38, 0x6C, 0x6C, 0x38, 0x00, 0x7C, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 168 (inverted question) + 0x00, 0x00, 0x30, 0x30, 0x00, 0x30, 0x30, 0x60, + 0xC0, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00, + // Character 169 (not left) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xC0, + 0xC0, 0xC0, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 170 (not right) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x06, + 0x06, 0x06, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 171 (fraction 1/2) + 0x00, 0xC0, 0xC0, 0xC2, 0xC6, 0xCC, 0x18, 0x30, + 0x60, 0xDC, 0x86, 0x0C, 0x18, 0x3E, 0x00, 0x00, + // Character 172 (fraction 1/4) + 0x00, 0xC0, 0xC0, 0xC2, 0xC6, 0xCC, 0x18, 0x30, + 0x66, 0xCE, 0x9E, 0x3E, 0x06, 0x06, 0x00, 0x00, + // Character 173 (inverted exclamation) + 0x00, 0x00, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, + 0x3C, 0x3C, 0x3C, 0x18, 0x00, 0x00, 0x00, 0x00, + // Character 174 (double left angle) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x6C, 0xD8, + 0x6C, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 175 (double right angle) + 0x00, 0x00, 0x00, 0x00, 0x00, 0xD8, 0x6C, 0x36, + 0x6C, 0xD8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 176 (light shade) + 0x11, 0x44, 0x11, 0x44, 0x11, 0x44, 0x11, 0x44, + 0x11, 0x44, 0x11, 0x44, 0x11, 0x44, 0x11, 0x44, + // Character 177 (medium shade) + 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, + 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, + // Character 178 (dark shade) + 0xDD, 0x77, 0xDD, 0x77, 0xDD, 0x77, 0xDD, 0x77, + 0xDD, 0x77, 0xDD, 0x77, 0xDD, 0x77, 0xDD, 0x77, + // Character 179 (box vertical) + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, + // Character 180 (box vertical-left) + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xF8, + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, + // Character 181 (box double vertical-left single) + 0x18, 0x18, 0x18, 0x18, 0x18, 0xF8, 0x18, 0xF8, + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, + // Character 182 (box double vertical-left) + 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0xF6, + 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, + // Character 183 (box double horizontal-down) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, + 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, + // Character 184 (box double vertical-left single) + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x18, 0xF8, + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, + // Character 185 (box double vertical-left double) + 0x36, 0x36, 0x36, 0x36, 0x36, 0xF6, 0x06, 0xF6, + 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, + // Character 186 (box double vertical) + 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, + 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, + // Character 187 (box double down-left) + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x06, 0xF6, + 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, + // Character 188 (box double up-left) + 0x36, 0x36, 0x36, 0x36, 0x36, 0xF6, 0x06, 0xFE, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 189 (box double up-right) + 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0xFE, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 190 (box horizontal-down) + 0x18, 0x18, 0x18, 0x18, 0x18, 0xF8, 0x18, 0xF8, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 191 (box down-left) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, + // Character 192 (box up-right) + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 193 (box horizontal-up) + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 194 (box horizontal-down) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, + // Character 195 (box vertical-right) + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1F, + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, + // Character 196 (box horizontal) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 197 (box cross) + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xFF, + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, + // Character 198 (box single vert-right double) + 0x18, 0x18, 0x18, 0x18, 0x18, 0x1F, 0x18, 0x1F, + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, + // Character 199 (box double vert-right) + 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x37, + 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, + // Character 200 (box double up-right) + 0x36, 0x36, 0x36, 0x36, 0x36, 0x37, 0x30, 0x3F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 201 (box double down-right) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x30, 0x37, + 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, + // Character 202 (box double horizontal-up) + 0x36, 0x36, 0x36, 0x36, 0x36, 0xF7, 0x00, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 203 (box double horizontal-down) + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xF7, + 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, + // Character 204 (box double vertical-right) + 0x36, 0x36, 0x36, 0x36, 0x36, 0x37, 0x30, 0x37, + 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, + // Character 205 (box double horizontal) + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 206 (box double cross) + 0x36, 0x36, 0x36, 0x36, 0x36, 0xF7, 0x00, 0xF7, + 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, + // Character 207 (box single horiz-up double) + 0x18, 0x18, 0x18, 0x18, 0x18, 0xFF, 0x00, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 208 (box double horizontal-up single) + 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 209 (box single horiz-down double) + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xFF, + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, + // Character 210 (box double horizontal-down single) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, + // Character 211 (box double up-right single) + 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x3F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 212 (box single up-right double) + 0x18, 0x18, 0x18, 0x18, 0x18, 0x1F, 0x18, 0x1F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 213 (box single down-right double) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x18, 0x1F, + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, + // Character 214 (box double down-right single) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, + 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, + // Character 215 (box double cross single) + 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0xFF, + 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, + // Character 216 (box single cross double) + 0x18, 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0xFF, + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, + // Character 217 (box up-left) + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xF8, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 218 (box down-right) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, + // Character 219 (full block) + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + // Character 220 (bottom half block) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + // Character 221 (left half block) + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + // Character 222 (right half block) + 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, + 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, + // Character 223 (top half block) + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 224 (alpha) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0xDC, 0xD8, + 0xD8, 0xD8, 0xDC, 0x76, 0x00, 0x00, 0x00, 0x00, + // Character 225 (beta/sharp s) + 0x00, 0x00, 0x78, 0xCC, 0xCC, 0xCC, 0xD8, 0xCC, + 0xC6, 0xC6, 0xC6, 0xCC, 0x00, 0x00, 0x00, 0x00, + // Character 226 (gamma) + 0x00, 0x00, 0xFE, 0xC6, 0xC6, 0xC0, 0xC0, 0xC0, + 0xC0, 0xC0, 0xC0, 0xC0, 0x00, 0x00, 0x00, 0x00, + // Character 227 (pi) + 0x00, 0x00, 0x00, 0x00, 0xFE, 0x6C, 0x6C, 0x6C, + 0x6C, 0x6C, 0x6C, 0x6C, 0x00, 0x00, 0x00, 0x00, + // Character 228 (sigma uppercase) + 0x00, 0x00, 0x00, 0xFE, 0xC6, 0x60, 0x30, 0x18, + 0x30, 0x60, 0xC6, 0xFE, 0x00, 0x00, 0x00, 0x00, + // Character 229 (sigma lowercase) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0xD8, 0xD8, + 0xD8, 0xD8, 0xD8, 0x70, 0x00, 0x00, 0x00, 0x00, + // Character 230 (mu) + 0x00, 0x00, 0x00, 0x00, 0x66, 0x66, 0x66, 0x66, + 0x66, 0x7C, 0x60, 0x60, 0xC0, 0x00, 0x00, 0x00, + // Character 231 (tau) + 0x00, 0x00, 0x00, 0x00, 0x76, 0xDC, 0x18, 0x18, + 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, + // Character 232 (phi uppercase) + 0x00, 0x00, 0x00, 0x7E, 0x18, 0x3C, 0x66, 0x66, + 0x66, 0x3C, 0x18, 0x7E, 0x00, 0x00, 0x00, 0x00, + // Character 233 (theta) + 0x00, 0x00, 0x00, 0x38, 0x6C, 0xC6, 0xC6, 0xFE, + 0xC6, 0xC6, 0x6C, 0x38, 0x00, 0x00, 0x00, 0x00, + // Character 234 (omega) + 0x00, 0x00, 0x38, 0x6C, 0xC6, 0xC6, 0xC6, 0xC6, + 0x6C, 0x6C, 0x6C, 0xEE, 0x00, 0x00, 0x00, 0x00, + // Character 235 (delta) + 0x00, 0x00, 0x1E, 0x30, 0x18, 0x0C, 0x3E, 0x66, + 0x66, 0x66, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00, + // Character 236 (infinity) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0xDB, 0xDB, + 0xDB, 0x76, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 237 (phi lowercase) + 0x00, 0x00, 0x00, 0x02, 0x06, 0x7C, 0xCE, 0xDE, + 0xF6, 0xE6, 0x7C, 0xC0, 0x80, 0x00, 0x00, 0x00, + // Character 238 (epsilon) + 0x00, 0x00, 0x1C, 0x30, 0x60, 0x60, 0x7C, 0x60, + 0x60, 0x60, 0x30, 0x1C, 0x00, 0x00, 0x00, 0x00, + // Character 239 (intersection) + 0x00, 0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0xC6, + 0xC6, 0xC6, 0xC6, 0xC6, 0x00, 0x00, 0x00, 0x00, + // Character 240 (triple bar) + 0x00, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0xFE, + 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 241 (plus-minus) + 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x7E, 0x18, + 0x18, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, + // Character 242 (greater-equal) + 0x00, 0x00, 0x00, 0x30, 0x18, 0x0C, 0x06, 0x0C, + 0x18, 0x30, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x00, + // Character 243 (less-equal) + 0x00, 0x00, 0x00, 0x0C, 0x18, 0x30, 0x60, 0x30, + 0x18, 0x0C, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x00, + // Character 244 (top integral) + 0x00, 0x00, 0x0E, 0x1B, 0x1B, 0x18, 0x18, 0x18, + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, + // Character 245 (bottom integral) + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, + 0x18, 0xD8, 0xD8, 0xD8, 0x70, 0x00, 0x00, 0x00, + // Character 246 (division) + 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x7E, + 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 247 (approximately equal) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0xDC, 0x00, + 0x76, 0xDC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 248 (degree) + 0x00, 0x38, 0x6C, 0x6C, 0x38, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 249 (bullet operator) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, + 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 250 (middle dot) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 251 (square root) + 0x00, 0x0F, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0xEC, + 0x6C, 0x6C, 0x3C, 0x1C, 0x00, 0x00, 0x00, 0x00, + // Character 252 (superscript n) + 0x00, 0xD8, 0x6C, 0x6C, 0x6C, 0x6C, 0x6C, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 253 (superscript 2) + 0x00, 0x70, 0xD8, 0x30, 0x60, 0xC8, 0xF8, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 254 (filled square) + 0x00, 0x00, 0x00, 0x00, 0x7C, 0x7C, 0x7C, 0x7C, + 0x7C, 0x7C, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, + // Character 255 (non-breaking space) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +} // namespace gui diff --git a/programs/src/desktop/main.cpp b/programs/src/desktop/main.cpp new file mode 100644 index 0000000..6ec9f33 --- /dev/null +++ b/programs/src/desktop/main.cpp @@ -0,0 +1,1208 @@ +/* + * main.cpp + * ZenithOS Desktop Environment - window manager, compositor, and applications + * Copyright (c) 2026 Daniel Hammer +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Placement new for freestanding environment +inline void* operator new(unsigned long, void* p) { return p; } + +using namespace gui; + +// ============================================================================ +// Minimal snprintf (copied from init/main.cpp pattern) +// ============================================================================ + +using va_list = __builtin_va_list; +#define va_start __builtin_va_start +#define va_end __builtin_va_end +#define va_arg __builtin_va_arg + +struct PfState { char* buf; int pos; int max; }; + +static void pf_putc(PfState* st, char c) { + if (st->pos < st->max) st->buf[st->pos] = c; + st->pos++; +} + +static void pf_putnum(PfState* st, unsigned long val, int base, int width, char pad, int neg) { + char tmp[24]; int i = 0; + const char* digits = "0123456789abcdef"; + if (val == 0) { tmp[i++] = '0'; } + else { while (val > 0) { tmp[i++] = digits[val % base]; val /= base; } } + int total = (neg ? 1 : 0) + i; + if (neg && pad == '0') pf_putc(st, '-'); + for (int w = total; w < width; w++) pf_putc(st, pad); + if (neg && pad != '0') pf_putc(st, '-'); + while (i > 0) pf_putc(st, tmp[--i]); +} + +static int snprintf(char* buf, int size, const char* fmt, ...) { + va_list ap; va_start(ap, fmt); + PfState st; st.buf = buf; st.pos = 0; st.max = size > 0 ? size - 1 : 0; + while (*fmt) { + if (*fmt != '%') { pf_putc(&st, *fmt++); continue; } + fmt++; + char pad = ' '; + if (*fmt == '0') { pad = '0'; fmt++; } + int width = 0; + while (*fmt >= '0' && *fmt <= '9') { width = width * 10 + (*fmt - '0'); fmt++; } + if (*fmt == 'l') fmt++; + switch (*fmt) { + case 'd': case 'i': { + long val = va_arg(ap, int); + int neg = 0; unsigned long uval; + if (val < 0) { neg = 1; uval = (unsigned long)(-val); } else uval = (unsigned long)val; + pf_putnum(&st, uval, 10, width, pad, neg); break; + } + case 'u': { unsigned val = va_arg(ap, unsigned); pf_putnum(&st, val, 10, width, pad, 0); break; } + case 'x': { unsigned val = va_arg(ap, unsigned); pf_putnum(&st, val, 16, width, pad, 0); break; } + case 's': { + const char* s = va_arg(ap, const char*); if (!s) s = "(null)"; + int slen = 0; while (s[slen]) slen++; + for (int w = slen; w < width; w++) pf_putc(&st, ' '); + for (int j = 0; j < slen; j++) pf_putc(&st, s[j]); + break; + } + case 'c': { char c = (char)va_arg(ap, int); pf_putc(&st, c); break; } + case '%': pf_putc(&st, '%'); break; + default: pf_putc(&st, '%'); pf_putc(&st, *fmt); break; + } + if (*fmt) fmt++; + } + if (size > 0) { if (st.pos < size) st.buf[st.pos] = '\0'; else st.buf[size - 1] = '\0'; } + va_end(ap); return st.pos; +} + +// ============================================================================ +// String helpers +// ============================================================================ + +static void str_append(char* dst, const char* src, int max) { + int len = zenith::slen(dst); + int i = 0; + while (src[i] && len < max - 1) { + dst[len++] = src[i++]; + } + dst[len] = '\0'; +} + +// ============================================================================ +// File Manager Application +// ============================================================================ + +struct FileManagerState { + char current_path[256]; + char entry_names[64][64]; + int entry_count; + int selected; + int scroll_offset; + bool is_dir[64]; +}; + +static void filemanager_read_dir(FileManagerState* fm) { + const char* names[64]; + fm->entry_count = zenith::readdir(fm->current_path, names, 64); + if (fm->entry_count < 0) fm->entry_count = 0; + for (int i = 0; i < fm->entry_count; i++) { + zenith::strncpy(fm->entry_names[i], names[i], 63); + // Heuristic: entries ending with '/' or without '.' are directories + int len = zenith::slen(fm->entry_names[i]); + if (len > 0 && fm->entry_names[i][len - 1] == '/') { + fm->is_dir[i] = true; + fm->entry_names[i][len - 1] = '\0'; // strip trailing slash + } else { + // Check if entry has an extension (has a dot) + bool has_dot = false; + for (int j = 0; j < len; j++) { + if (fm->entry_names[i][j] == '.') { has_dot = true; break; } + } + fm->is_dir[i] = !has_dot; + } + } + fm->selected = -1; + fm->scroll_offset = 0; +} + +static void filemanager_navigate(FileManagerState* fm, const char* name) { + // Build new path + int path_len = zenith::slen(fm->current_path); + if (path_len > 0 && fm->current_path[path_len - 1] != '/') { + str_append(fm->current_path, "/", 256); + } + str_append(fm->current_path, name, 256); + filemanager_read_dir(fm); +} + +static void filemanager_go_up(FileManagerState* fm) { + int len = zenith::slen(fm->current_path); + if (len <= 3) return; // "0:/" is root + + // Remove trailing slash if present + if (len > 0 && fm->current_path[len - 1] == '/') { + fm->current_path[len - 1] = '\0'; + len--; + } + + // Find last slash + int last_slash = -1; + for (int i = len - 1; i >= 0; i--) { + if (fm->current_path[i] == '/') { last_slash = i; break; } + } + if (last_slash >= 0) { + fm->current_path[last_slash + 1] = '\0'; + } + filemanager_read_dir(fm); +} + +static void filemanager_on_draw(Window* win, Framebuffer& fb) { + FileManagerState* fm = (FileManagerState*)win->app_data; + if (!fm) return; + + Rect cr = win->content_rect(); + int cw = cr.w; + int ch = cr.h; + + // Render to content buffer + 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; + + // Draw path bar at top (24px tall, light gray background) + uint32_t pathbar_px = Color::from_rgb(0xF0, 0xF0, 0xF0).to_pixel(); + for (int y = 0; y < 24 && y < ch; y++) + for (int x = 0; x < cw; x++) + pixels[y * cw + x] = pathbar_px; + + // Draw path text + { + int tx = 8; + int ty = 4; + uint32_t fg_px = colors::TEXT_COLOR.to_pixel(); + for (int ci = 0; fm->current_path[ci] && tx + FONT_WIDTH <= cw; ci++) { + const uint8_t* glyph = &font_data[(unsigned char)fm->current_path[ci] * FONT_HEIGHT]; + for (int fy = 0; fy < FONT_HEIGHT && ty + fy < ch; fy++) { + uint8_t bits = glyph[fy]; + for (int fx = 0; fx < FONT_WIDTH; fx++) { + if (bits & (0x80 >> fx)) { + int dx = tx + fx; + int dy = ty + fy; + if (dx < cw && dy < ch) + pixels[dy * cw + dx] = fg_px; + } + } + } + tx += FONT_WIDTH; + } + } + + // Draw separator line + uint32_t sep_px = colors::BORDER.to_pixel(); + if (24 < ch) { + for (int x = 0; x < cw; x++) + pixels[24 * cw + x] = sep_px; + } + + // Draw file entries + int item_height = 24; + int start_y = 26; + int visible_items = (ch - start_y) / item_height; + + for (int i = fm->scroll_offset; i < fm->entry_count && (i - fm->scroll_offset) < visible_items; i++) { + int iy = start_y + (i - fm->scroll_offset) * item_height; + if (iy + item_height > ch) break; + + // Highlight selected + if (i == fm->selected) { + uint32_t sel_px = colors::MENU_HOVER.to_pixel(); + for (int y = iy; y < iy + item_height && y < ch; y++) + for (int x = 0; x < cw; x++) + pixels[y * cw + x] = sel_px; + } + + // Draw icon placeholder (small colored square) + uint32_t icon_px; + if (fm->is_dir[i]) { + icon_px = Color::from_rgb(0xFF, 0xBD, 0x2E).to_pixel(); // folder yellow + } else { + icon_px = Color::from_rgb(0x90, 0x90, 0x90).to_pixel(); // file gray + } + int icon_x = 8; + int icon_y = iy + 4; + for (int dy = 0; dy < 16 && icon_y + dy < ch; dy++) + for (int dx = 0; dx < 16 && icon_x + dx < cw; dx++) + pixels[(icon_y + dy) * cw + (icon_x + dx)] = icon_px; + + // Draw entry name + int tx = 30; + int ty = iy + 4; + uint32_t text_px = colors::TEXT_COLOR.to_pixel(); + for (int ci = 0; fm->entry_names[i][ci] && tx + FONT_WIDTH <= cw; ci++) { + const uint8_t* glyph = &font_data[(unsigned char)fm->entry_names[i][ci] * FONT_HEIGHT]; + for (int fy = 0; fy < FONT_HEIGHT && ty + fy < ch; fy++) { + uint8_t bits = glyph[fy]; + for (int fx = 0; fx < FONT_WIDTH; fx++) { + if (bits & (0x80 >> fx)) { + int dx = tx + fx; + int dy = ty + fy; + if (dx < cw && dy < ch) + pixels[dy * cw + dx] = text_px; + } + } + } + tx += FONT_WIDTH; + } + } +} + +static void filemanager_on_mouse(Window* win, MouseEvent& ev) { + FileManagerState* fm = (FileManagerState*)win->app_data; + if (!fm) return; + + Rect cr = win->content_rect(); + int local_y = ev.y - cr.y; + + if (ev.left_pressed()) { + int item_height = 24; + int start_y = 26; + int clicked_idx = fm->scroll_offset + (local_y - start_y) / item_height; + + if (local_y >= start_y && clicked_idx >= 0 && clicked_idx < fm->entry_count) { + if (fm->selected == clicked_idx) { + // Double-click: navigate into directory + if (fm->is_dir[clicked_idx]) { + filemanager_navigate(fm, fm->entry_names[clicked_idx]); + } + } else { + fm->selected = clicked_idx; + } + } + } + + // Scroll handling + if (ev.scroll != 0) { + fm->scroll_offset -= ev.scroll; + if (fm->scroll_offset < 0) fm->scroll_offset = 0; + int max_off = fm->entry_count - ((cr.h - 26) / 24); + if (max_off < 0) max_off = 0; + if (fm->scroll_offset > max_off) fm->scroll_offset = max_off; + } +} + +static void filemanager_on_key(Window* win, const Zenith::KeyEvent& key) { + FileManagerState* fm = (FileManagerState*)win->app_data; + if (!fm || !key.pressed) return; + + if (key.ascii == '\b' || key.scancode == 0x0E) { + filemanager_go_up(fm); + } else if (key.scancode == 0x48) { + // Up arrow + if (fm->selected > 0) fm->selected--; + } else if (key.scancode == 0x50) { + // Down arrow + if (fm->selected < fm->entry_count - 1) fm->selected++; + } else if (key.ascii == '\n' || key.ascii == '\r') { + if (fm->selected >= 0 && fm->selected < fm->entry_count) { + if (fm->is_dir[fm->selected]) { + filemanager_navigate(fm, fm->entry_names[fm->selected]); + } + } + } +} + +static void filemanager_on_close(Window* win) { + if (win->app_data) { + zenith::mfree(win->app_data); + win->app_data = nullptr; + } +} + +// ============================================================================ +// System Info Application +// ============================================================================ + +struct SysInfoState { + Zenith::SysInfo sys_info; + Zenith::NetCfg net_cfg; + uint64_t uptime_ms; +}; + +static 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; + } + } + } + } +} + +static void sysinfo_on_draw(Window* win, Framebuffer& fb) { + SysInfoState* si = (SysInfoState*)win->app_data; + if (!si) return; + + // Refresh uptime + si->uptime_ms = zenith::get_milliseconds(); + + Rect cr = win->content_rect(); + int cw = cr.w; + int ch = cr.h; + uint32_t* pixels = win->content; + + // Fill background + uint32_t bg_px = colors::WINDOW_BG.to_pixel(); + for (int i = 0; i < cw * ch; i++) pixels[i] = bg_px; + + uint32_t text_px = colors::TEXT_COLOR.to_pixel(); + uint32_t accent_px = colors::ACCENT.to_pixel(); + int y = 16; + int x = 16; + char line[128]; + + // Title + draw_text_to_pixels(pixels, cw, ch, x, y, "System Information", accent_px); + y += FONT_HEIGHT + 12; + + // Separator + uint32_t sep_px = colors::BORDER.to_pixel(); + for (int sx = x; sx < cw - x && y < ch; sx++) + pixels[y * cw + sx] = sep_px; + y += 8; + + // OS Name + snprintf(line, sizeof(line), "OS: %s", si->sys_info.osName); + draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); + y += FONT_HEIGHT + 6; + + // OS Version + snprintf(line, sizeof(line), "Version: %s", si->sys_info.osVersion); + draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); + y += FONT_HEIGHT + 6; + + // API Version + snprintf(line, sizeof(line), "API: %d", (int)si->sys_info.apiVersion); + draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); + y += FONT_HEIGHT + 6; + + // Max Processes + snprintf(line, sizeof(line), "Max PIDs: %d", (int)si->sys_info.maxProcesses); + draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); + y += FONT_HEIGHT + 12; + + // Uptime + int up_sec = (int)(si->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); + draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); + y += FONT_HEIGHT + 12; + + // Network section + draw_text_to_pixels(pixels, cw, ch, x, y, "Network", accent_px); + y += FONT_HEIGHT + 8; + + for (int sx = x; sx < cw - x && y < ch; sx++) + pixels[y * cw + sx] = sep_px; + y += 8; + + // IP Address + uint32_t ip = si->net_cfg.ipAddress; + snprintf(line, sizeof(line), "IP: %d.%d.%d.%d", + (int)(ip & 0xFF), (int)((ip >> 8) & 0xFF), + (int)((ip >> 16) & 0xFF), (int)((ip >> 24) & 0xFF)); + draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); + y += FONT_HEIGHT + 6; + + // Subnet + uint32_t mask = si->net_cfg.subnetMask; + snprintf(line, sizeof(line), "Subnet: %d.%d.%d.%d", + (int)(mask & 0xFF), (int)((mask >> 8) & 0xFF), + (int)((mask >> 16) & 0xFF), (int)((mask >> 24) & 0xFF)); + draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); + y += FONT_HEIGHT + 6; + + // Gateway + uint32_t gw = si->net_cfg.gateway; + snprintf(line, sizeof(line), "Gateway: %d.%d.%d.%d", + (int)(gw & 0xFF), (int)((gw >> 8) & 0xFF), + (int)((gw >> 16) & 0xFF), (int)((gw >> 24) & 0xFF)); + draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); + y += FONT_HEIGHT + 6; + + // DNS + uint32_t dns = si->net_cfg.dnsServer; + snprintf(line, sizeof(line), "DNS: %d.%d.%d.%d", + (int)(dns & 0xFF), (int)((dns >> 8) & 0xFF), + (int)((dns >> 16) & 0xFF), (int)((dns >> 24) & 0xFF)); + draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); + y += FONT_HEIGHT + 6; + + // MAC Address + snprintf(line, sizeof(line), "MAC: %02x:%02x:%02x:%02x:%02x:%02x", + (unsigned)si->net_cfg.macAddress[0], (unsigned)si->net_cfg.macAddress[1], + (unsigned)si->net_cfg.macAddress[2], (unsigned)si->net_cfg.macAddress[3], + (unsigned)si->net_cfg.macAddress[4], (unsigned)si->net_cfg.macAddress[5]); + draw_text_to_pixels(pixels, cw, ch, x, y, line, text_px); +} + +static void sysinfo_on_close(Window* win) { + if (win->app_data) { + zenith::mfree(win->app_data); + win->app_data = nullptr; + } +} + +// ============================================================================ +// Terminal Application +// ============================================================================ + +static void terminal_on_draw(Window* win, Framebuffer& fb) { + TerminalState* ts = (TerminalState*)win->app_data; + if (!ts) return; + + Rect cr = win->content_rect(); + terminal_render(ts, win->content, cr.w, cr.h); +} + +static void terminal_on_mouse(Window* win, MouseEvent& ev) { + // Terminal doesn't need mouse handling for now +} + +static void terminal_on_key(Window* win, const Zenith::KeyEvent& key) { + TerminalState* ts = (TerminalState*)win->app_data; + if (!ts) return; + terminal_handle_key(ts, key); +} + +static void terminal_on_close(Window* win) { + TerminalState* ts = (TerminalState*)win->app_data; + if (ts) { + if (ts->cells) zenith::mfree(ts->cells); + zenith::mfree(ts); + win->app_data = nullptr; + } +} + +// ============================================================================ +// Application Launchers +// ============================================================================ + +static DesktopState* g_desktop; + +static void open_terminal(DesktopState* ds) { + int idx = desktop_create_window(ds, "Terminal", 200, 80, 648, 480); + if (idx < 0) return; + + Window* win = &ds->windows[idx]; + Rect cr = win->content_rect(); + int cols = cr.w / FONT_WIDTH; + int rows = cr.h / FONT_HEIGHT; + + TerminalState* ts = (TerminalState*)zenith::malloc(sizeof(TerminalState)); + zenith::memset(ts, 0, sizeof(TerminalState)); + terminal_init(ts, cols, rows); + + win->app_data = ts; + win->on_draw = terminal_on_draw; + win->on_mouse = terminal_on_mouse; + win->on_key = terminal_on_key; + win->on_close = terminal_on_close; +} + +static void open_filemanager(DesktopState* ds) { + int idx = desktop_create_window(ds, "Files", 150, 120, 500, 400); + if (idx < 0) return; + + Window* win = &ds->windows[idx]; + FileManagerState* fm = (FileManagerState*)zenith::malloc(sizeof(FileManagerState)); + zenith::memset(fm, 0, sizeof(FileManagerState)); + zenith::strcpy(fm->current_path, "0:/"); + fm->selected = -1; + filemanager_read_dir(fm); + + win->app_data = fm; + win->on_draw = filemanager_on_draw; + win->on_mouse = filemanager_on_mouse; + win->on_key = filemanager_on_key; + win->on_close = filemanager_on_close; +} + +static void open_sysinfo(DesktopState* ds) { + int idx = desktop_create_window(ds, "System Info", 300, 100, 400, 380); + if (idx < 0) return; + + Window* win = &ds->windows[idx]; + SysInfoState* si = (SysInfoState*)zenith::malloc(sizeof(SysInfoState)); + zenith::memset(si, 0, sizeof(SysInfoState)); + zenith::get_info(&si->sys_info); + zenith::get_netcfg(&si->net_cfg); + si->uptime_ms = zenith::get_milliseconds(); + + win->app_data = si; + win->on_draw = sysinfo_on_draw; + win->on_mouse = nullptr; + win->on_key = nullptr; + win->on_close = sysinfo_on_close; +} + +// App menu click callbacks +static void on_click_terminal(void* ud) { + DesktopState* ds = (DesktopState*)ud; + open_terminal(ds); + ds->app_menu_open = false; +} + +static void on_click_filemanager(void* ud) { + DesktopState* ds = (DesktopState*)ud; + open_filemanager(ds); + ds->app_menu_open = false; +} + +static void on_click_sysinfo(void* ud) { + DesktopState* ds = (DesktopState*)ud; + open_sysinfo(ds); + ds->app_menu_open = false; +} + +// ============================================================================ +// Desktop Implementation +// ============================================================================ + +void gui::desktop_init(DesktopState* ds) { + ds->screen_w = ds->fb.width(); + ds->screen_h = ds->fb.height(); + + // Immediately clear the screen to hide flanterm boot text + ds->fb.clear(colors::DESKTOP_BG); + ds->fb.flip(); + + ds->window_count = 0; + ds->focused_window = -1; + ds->prev_buttons = 0; + ds->app_menu_open = false; + + zenith::memset(&ds->mouse, 0, sizeof(Zenith::MouseState)); + zenith::set_mouse_bounds(ds->screen_w - 1, ds->screen_h - 1); + + // Load SVG icons (filenames match those copied by scripts/copy_icons.sh) + ds->icon_terminal = svg_load("0:/icons/utilities-terminal-symbolic.svg", 20, 20, colors::ICON_COLOR); + ds->icon_filemanager = svg_load("0:/icons/system-file-manager-symbolic.svg", 20, 20, colors::ICON_COLOR); + ds->icon_sysinfo = svg_load("0:/icons/preferences-desktop-apps-symbolic.svg", 20, 20, colors::ICON_COLOR); + ds->icon_appmenu = svg_load("0:/icons/view-app-grid-symbolic.svg", 20, 20, colors::PANEL_TEXT); + ds->icon_folder = svg_load("0:/icons/folder-symbolic.svg", 16, 16, Color::from_rgb(0xFF, 0xBD, 0x2E)); + ds->icon_file = svg_load("0:/icons/text-x-generic-symbolic.svg", 16, 16, colors::ICON_COLOR); + ds->icon_computer = svg_load("0:/icons/computer-symbolic.svg", 20, 20, colors::ICON_COLOR); + + // Open initial terminal window + open_terminal(ds); +} + +int gui::desktop_create_window(DesktopState* ds, const char* title, int x, int y, int w, int h) { + if (ds->window_count >= MAX_WINDOWS) return -1; + + int idx = ds->window_count; + Window* win = &ds->windows[idx]; + zenith::memset(win, 0, sizeof(Window)); + + zenith::strncpy(win->title, title, MAX_TITLE_LEN); + win->frame = {x, y, w, h}; + win->state = WIN_NORMAL; + win->z_order = idx; + win->focused = true; + win->dirty = true; + win->dragging = false; + win->resizing = false; + win->saved_frame = win->frame; + + // Allocate content buffer + Rect cr = win->content_rect(); + win->content_w = cr.w; + win->content_h = cr.h; + int buf_size = cr.w * cr.h * 4; + win->content = (uint32_t*)zenith::alloc(buf_size); + zenith::memset(win->content, 0xFF, buf_size); + + win->on_draw = nullptr; + win->on_mouse = nullptr; + win->on_key = nullptr; + win->on_close = nullptr; + win->app_data = nullptr; + + // Unfocus previous window + if (ds->focused_window >= 0 && ds->focused_window < ds->window_count) { + ds->windows[ds->focused_window].focused = false; + } + ds->focused_window = idx; + ds->window_count++; + + return idx; +} + +void gui::desktop_close_window(DesktopState* ds, int idx) { + if (idx < 0 || idx >= ds->window_count) return; + + Window* win = &ds->windows[idx]; + if (win->on_close) win->on_close(win); + + // Free content buffer + if (win->content) { + zenith::free(win->content); + win->content = nullptr; + } + + // Shift remaining windows down + for (int i = idx; i < ds->window_count - 1; i++) { + ds->windows[i] = ds->windows[i + 1]; + } + ds->window_count--; + + // Fix focused window index + if (ds->focused_window == idx) { + ds->focused_window = ds->window_count > 0 ? ds->window_count - 1 : -1; + } else if (ds->focused_window > idx) { + ds->focused_window--; + } + + if (ds->focused_window >= 0 && ds->focused_window < ds->window_count) { + ds->windows[ds->focused_window].focused = true; + } +} + +void gui::desktop_raise_window(DesktopState* ds, int idx) { + if (idx < 0 || idx >= ds->window_count) return; + if (idx == ds->window_count - 1) { + // Already on top, just focus + if (ds->focused_window >= 0 && ds->focused_window < ds->window_count) + ds->windows[ds->focused_window].focused = false; + ds->focused_window = idx; + ds->windows[idx].focused = true; + return; + } + + // Unfocus current + if (ds->focused_window >= 0 && ds->focused_window < ds->window_count) { + ds->windows[ds->focused_window].focused = false; + } + + // Move window to end (top of stack) + Window tmp = ds->windows[idx]; + for (int i = idx; i < ds->window_count - 1; i++) { + ds->windows[i] = ds->windows[i + 1]; + } + ds->windows[ds->window_count - 1] = tmp; + + ds->focused_window = ds->window_count - 1; + ds->windows[ds->focused_window].focused = true; +} + +void gui::desktop_draw_window(DesktopState* ds, int idx) { + if (idx < 0 || idx >= ds->window_count) return; + Window* win = &ds->windows[idx]; + if (win->state == WIN_MINIMIZED || win->state == WIN_CLOSED) return; + + Framebuffer& fb = ds->fb; + int x = win->frame.x; + int y = win->frame.y; + int w = win->frame.w; + int h = win->frame.h; + + // Draw shadow + draw_shadow(fb, x, y, w, h, SHADOW_SIZE, colors::SHADOW); + + // Draw window body + fb.fill_rect(x, y, w, h, colors::WINDOW_BG); + + // Draw titlebar + Color tb_bg = win->focused ? colors::TITLEBAR_BG : Color::from_rgb(0xE8, 0xE8, 0xE8); + fb.fill_rect(x, y, w, TITLEBAR_HEIGHT, tb_bg); + + // Draw border + draw_rect(fb, x, y, w, h, colors::BORDER); + // Titlebar bottom separator + draw_hline(fb, x, y + TITLEBAR_HEIGHT - 1, w, colors::BORDER); + + // Draw window buttons (macOS style: close, minimize, maximize) + Rect close_r = win->close_btn_rect(); + Rect min_r = win->min_btn_rect(); + Rect max_r = win->max_btn_rect(); + + fill_circle(fb, close_r.x + BTN_RADIUS, close_r.y + BTN_RADIUS, BTN_RADIUS, colors::CLOSE_BTN); + fill_circle(fb, min_r.x + BTN_RADIUS, min_r.y + BTN_RADIUS, BTN_RADIUS, colors::MIN_BTN); + fill_circle(fb, max_r.x + BTN_RADIUS, max_r.y + BTN_RADIUS, BTN_RADIUS, colors::MAX_BTN); + + // Draw title text centered in titlebar (after buttons) + int title_x = x + 12 + 44 + BTN_RADIUS * 2 + 12; // after buttons + int title_y = y + (TITLEBAR_HEIGHT - FONT_HEIGHT) / 2; + int title_w = text_width(win->title); + // Center in remaining space + int remaining_w = w - (title_x - x) - 12; + if (remaining_w > title_w) { + title_x += (remaining_w - title_w) / 2; + } + draw_text(fb, title_x, title_y, win->title, colors::TEXT_COLOR); + + // Call app draw callback to render content + if (win->on_draw) { + win->on_draw(win, fb); + } + + // Blit content buffer to framebuffer + Rect cr = win->content_rect(); + if (win->content) { + fb.blit(cr.x, cr.y, cr.w, cr.h, win->content); + } +} + +void gui::desktop_draw_panel(DesktopState* ds) { + Framebuffer& fb = ds->fb; + int sw = ds->screen_w; + + // Panel background + fb.fill_rect(0, 0, sw, PANEL_HEIGHT, colors::PANEL_BG); + + // App menu button (left side) + int btn_x = 4; + int btn_y = 2; + int btn_w = 28; + int btn_h = 28; + + // Draw grid icon for app menu (3x3 dots) + if (ds->icon_appmenu.pixels) { + int ix = btn_x + (btn_w - ds->icon_appmenu.width) / 2; + int iy = btn_y + (btn_h - ds->icon_appmenu.height) / 2; + fb.blit_alpha(ix, iy, ds->icon_appmenu.width, ds->icon_appmenu.height, ds->icon_appmenu.pixels); + } else { + // Fallback: draw 3x3 grid of small squares + for (int gr = 0; gr < 3; gr++) { + for (int gc = 0; gc < 3; gc++) { + int dx = btn_x + 6 + gc * 6; + int dy = btn_y + 6 + gr * 6; + fb.fill_rect(dx, dy, 3, 3, colors::PANEL_TEXT); + } + } + } + + // Window indicator buttons (center area) + int indicator_x = 40; + for (int i = 0; i < ds->window_count; i++) { + Window* win = &ds->windows[i]; + if (win->state == WIN_CLOSED) continue; + + int tw = text_width(win->title); + int pad = 12; + int iw = tw + pad * 2; + if (iw > 150) iw = 150; + + Color btn_bg = (i == ds->focused_window) + ? Color::from_rgba(0xFF, 0xFF, 0xFF, 0x30) + : Color::from_rgba(0xFF, 0xFF, 0xFF, 0x10); + + fb.fill_rect_alpha(indicator_x, 4, iw, 24, btn_bg); + + // Truncate title if too long + char short_title[20]; + zenith::strncpy(short_title, win->title, 18); + + int tx = indicator_x + pad; + int ty = 4 + (24 - FONT_HEIGHT) / 2; + draw_text(fb, tx, ty, short_title, colors::PANEL_TEXT); + + indicator_x += iw + 4; + } + + // Clock (right side) + Zenith::DateTime dt; + zenith::gettime(&dt); + char clock_str[8]; + snprintf(clock_str, sizeof(clock_str), "%02d:%02d", (int)dt.Hour, (int)dt.Minute); + int clock_w = text_width(clock_str); + int clock_x = sw - clock_w - 12; + int clock_y = (PANEL_HEIGHT - FONT_HEIGHT) / 2; + draw_text(fb, clock_x, clock_y, clock_str, colors::PANEL_TEXT); +} + +static void desktop_draw_app_menu(DesktopState* ds) { + Framebuffer& fb = ds->fb; + + int menu_x = 4; + int menu_y = PANEL_HEIGHT + 2; + int menu_w = 200; + int item_h = 36; + int menu_h = item_h * 3 + 8; + + // Menu shadow + draw_shadow(fb, menu_x, menu_y, menu_w, menu_h, 4, colors::SHADOW); + + // Menu background + fb.fill_rect(menu_x, menu_y, menu_w, menu_h, colors::MENU_BG); + draw_rect(fb, menu_x, menu_y, menu_w, menu_h, colors::BORDER); + + // Menu items + struct MenuItem { + const char* label; + SvgIcon* icon; + }; + MenuItem items[3] = { + { "Terminal", &ds->icon_terminal }, + { "Files", &ds->icon_filemanager }, + { "System Info", &ds->icon_sysinfo } + }; + + int mx = ds->mouse.x; + int my = ds->mouse.y; + + for (int i = 0; i < 3; i++) { + int iy = menu_y + 4 + i * item_h; + Rect item_rect = {menu_x + 4, iy, menu_w - 8, item_h}; + + // Hover highlight + if (item_rect.contains(mx, my)) { + fill_rounded_rect(fb, item_rect.x, item_rect.y, item_rect.w, item_rect.h, 4, colors::MENU_HOVER); + } + + // Icon + int icon_x = item_rect.x + 8; + int icon_y = item_rect.y + (item_h - 20) / 2; + if (items[i].icon && items[i].icon->pixels) { + fb.blit_alpha(icon_x, icon_y, items[i].icon->width, items[i].icon->height, items[i].icon->pixels); + } + + // Label + int tx = icon_x + 28; + int ty = item_rect.y + (item_h - FONT_HEIGHT) / 2; + draw_text(fb, tx, ty, items[i].label, colors::TEXT_COLOR); + } +} + +void gui::desktop_compose(DesktopState* ds) { + Framebuffer& fb = ds->fb; + + // Desktop background + fb.clear(colors::DESKTOP_BG); + + // Draw windows from bottom to top (index 0 = bottom) + for (int i = 0; i < ds->window_count; i++) { + if (ds->windows[i].state != WIN_MINIMIZED && ds->windows[i].state != WIN_CLOSED) { + desktop_draw_window(ds, i); + } + } + + // Draw panel on top of everything + desktop_draw_panel(ds); + + // Draw app menu if open + if (ds->app_menu_open) { + desktop_draw_app_menu(ds); + } + + // Draw cursor last (on top of everything) + draw_cursor(fb, ds->mouse.x, ds->mouse.y); +} + +void gui::desktop_handle_mouse(DesktopState* ds) { + int mx = ds->mouse.x; + int my = ds->mouse.y; + uint8_t buttons = ds->mouse.buttons; + uint8_t prev = ds->prev_buttons; + bool left_pressed = (buttons & 0x01) && !(prev & 0x01); + bool left_held = (buttons & 0x01); + bool left_released = !(buttons & 0x01) && (prev & 0x01); + + // Build mouse event + MouseEvent ev; + ev.x = mx; + ev.y = my; + ev.buttons = buttons; + ev.prev_buttons = prev; + ev.scroll = ds->mouse.scrollDelta; + + // Check for ongoing window drags first + for (int i = 0; i < ds->window_count; i++) { + Window* win = &ds->windows[i]; + if (win->dragging) { + if (left_held) { + win->frame.x = mx - win->drag_offset_x; + win->frame.y = my - win->drag_offset_y; + // Clamp to screen + if (win->frame.x < -win->frame.w + 50) win->frame.x = -win->frame.w + 50; + if (win->frame.y < 0) win->frame.y = 0; + if (win->frame.x > ds->screen_w - 50) win->frame.x = ds->screen_w - 50; + if (win->frame.y > ds->screen_h - 50) win->frame.y = ds->screen_h - 50; + } + if (left_released) { + win->dragging = false; + } + return; + } + } + + // Handle app menu clicks + if (ds->app_menu_open && left_pressed) { + int menu_x = 4; + int menu_y = PANEL_HEIGHT + 2; + int menu_w = 200; + int item_h = 36; + int menu_h = item_h * 3 + 8; + Rect menu_rect = {menu_x, menu_y, menu_w, menu_h}; + + if (menu_rect.contains(mx, my)) { + // Which item was clicked? + int rel_y = my - menu_y - 4; + int item_idx = rel_y / item_h; + if (item_idx >= 0 && item_idx < 3) { + switch (item_idx) { + case 0: on_click_terminal(ds); break; + case 1: on_click_filemanager(ds); break; + case 2: on_click_sysinfo(ds); break; + } + } + return; + } else { + ds->app_menu_open = false; + // Fall through to handle click on other things + } + } + + // Panel click check + if (left_pressed && my < PANEL_HEIGHT) { + // App menu button + if (mx < 36) { + ds->app_menu_open = !ds->app_menu_open; + return; + } + + // Window indicator buttons + int indicator_x = 40; + for (int i = 0; i < ds->window_count; i++) { + Window* win = &ds->windows[i]; + if (win->state == WIN_CLOSED) continue; + + int tw = text_width(win->title); + int pad = 12; + int iw = tw + pad * 2; + if (iw > 150) iw = 150; + + Rect btn_rect = {indicator_x, 4, iw, 24}; + if (btn_rect.contains(mx, my)) { + if (win->state == WIN_MINIMIZED) { + win->state = WIN_NORMAL; + } + desktop_raise_window(ds, i); + return; + } + indicator_x += iw + 4; + } + return; + } + + // Window interaction: check from top (last) to bottom (first) + if (left_pressed) { + for (int i = ds->window_count - 1; i >= 0; i--) { + Window* win = &ds->windows[i]; + if (win->state == WIN_MINIMIZED || win->state == WIN_CLOSED) continue; + + // Check close button + Rect close_r = win->close_btn_rect(); + if (close_r.contains(mx, my)) { + desktop_close_window(ds, i); + return; + } + + // Check minimize button + Rect min_r = win->min_btn_rect(); + if (min_r.contains(mx, my)) { + win->state = WIN_MINIMIZED; + if (ds->focused_window == i) { + ds->focused_window = -1; + // Focus next visible window + for (int j = ds->window_count - 1; j >= 0; j--) { + if (ds->windows[j].state == WIN_NORMAL || ds->windows[j].state == WIN_MAXIMIZED) { + ds->focused_window = j; + ds->windows[j].focused = true; + break; + } + } + } + return; + } + + // Check maximize button + Rect max_r = win->max_btn_rect(); + if (max_r.contains(mx, my)) { + if (win->state == WIN_MAXIMIZED) { + // Restore + win->frame = win->saved_frame; + win->state = WIN_NORMAL; + } else { + // Maximize + win->saved_frame = win->frame; + win->frame = {0, PANEL_HEIGHT, ds->screen_w, ds->screen_h - PANEL_HEIGHT}; + win->state = WIN_MAXIMIZED; + } + // Reallocate content buffer for new size + 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); + } + desktop_raise_window(ds, i); + return; + } + + // Check titlebar (start drag) + Rect tb = win->titlebar_rect(); + if (tb.contains(mx, my)) { + win->dragging = true; + win->drag_offset_x = mx - win->frame.x; + win->drag_offset_y = my - win->frame.y; + desktop_raise_window(ds, ds->window_count - 1 == i ? i : i); + // After raise, this window is at the end + // Need to update the reference since raise moved it + int new_idx = ds->window_count - 1; + ds->windows[new_idx].dragging = true; + ds->windows[new_idx].drag_offset_x = mx - ds->windows[new_idx].frame.x; + ds->windows[new_idx].drag_offset_y = my - ds->windows[new_idx].frame.y; + return; + } + + // Check content area + Rect cr = win->content_rect(); + if (cr.contains(mx, my)) { + desktop_raise_window(ds, i); + // Dispatch mouse event to app + int new_idx = ds->window_count - 1; + if (ds->windows[new_idx].on_mouse) { + ev.x = mx; + ev.y = my; + ds->windows[new_idx].on_mouse(&ds->windows[new_idx], ev); + } + return; + } + + // Check full frame (catches border clicks) + if (win->frame.contains(mx, my)) { + desktop_raise_window(ds, i); + return; + } + } + + // Clicked on desktop background - close app menu + ds->app_menu_open = false; + } + + // Handle scroll events on focused window + if (ev.scroll != 0 && ds->focused_window >= 0) { + Window* win = &ds->windows[ds->focused_window]; + Rect cr = win->content_rect(); + if (cr.contains(mx, my) && win->on_mouse) { + win->on_mouse(win, ev); + } + } +} + +void gui::desktop_handle_keyboard(DesktopState* ds, const Zenith::KeyEvent& key) { + if (!key.pressed) return; + + // Global shortcuts + if (key.ctrl && key.alt) { + if (key.ascii == 't' || key.ascii == 'T') { + open_terminal(ds); + return; + } + if (key.ascii == 'f' || key.ascii == 'F') { + open_filemanager(ds); + return; + } + if (key.ascii == 'i' || key.ascii == 'I') { + open_sysinfo(ds); + return; + } + } + + // Dispatch to focused window + if (ds->focused_window >= 0 && ds->focused_window < ds->window_count) { + Window* win = &ds->windows[ds->focused_window]; + if (win->on_key) { + win->on_key(win, key); + } + } +} + +void gui::desktop_run(DesktopState* ds) { + for (;;) { + // Poll mouse state + ds->prev_buttons = ds->mouse.buttons; + zenith::mouse_state(&ds->mouse); + + // Poll keyboard events + while (zenith::is_key_available()) { + Zenith::KeyEvent key; + zenith::getkey(&key); + desktop_handle_keyboard(ds, key); + } + + // Poll terminal I/O for all terminal windows + for (int i = 0; i < ds->window_count; i++) { + Window* win = &ds->windows[i]; + if (win->state == WIN_CLOSED) continue; + if (win->app_data && win->on_draw == terminal_on_draw) { + TerminalState* ts = (TerminalState*)win->app_data; + terminal_poll(ts); + } + } + + // Handle mouse events + desktop_handle_mouse(ds); + + // Compose and present + desktop_compose(ds); + ds->fb.flip(); + + // Target ~60fps + zenith::sleep_ms(16); + } +} + +// ============================================================================ +// Entry Point +// ============================================================================ + +extern "C" void _start() { + DesktopState* ds = (DesktopState*)zenith::malloc(sizeof(DesktopState)); + zenith::memset(ds, 0, sizeof(DesktopState)); + + // Placement-new the Framebuffer since it has a constructor + new (&ds->fb) Framebuffer(); + + g_desktop = ds; + + desktop_init(ds); + desktop_run(ds); + + zenith::exit(0); +} diff --git a/programs/src/desktop/obj/font_data.o b/programs/src/desktop/obj/font_data.o new file mode 100644 index 0000000..ab24d2b Binary files /dev/null and b/programs/src/desktop/obj/font_data.o differ diff --git a/programs/src/desktop/obj/main.o b/programs/src/desktop/obj/main.o new file mode 100644 index 0000000..8f199c8 Binary files /dev/null and b/programs/src/desktop/obj/main.o differ diff --git a/programs/src/init/main.cpp b/programs/src/init/main.cpp index 364f4e8..1329f01 100644 --- a/programs/src/init/main.cpp +++ b/programs/src/init/main.cpp @@ -153,8 +153,11 @@ extern "C" void _start() { // ---- Stage 1: Network configuration ---- run_service("0:/os/dhcp.elf", "dhcp"); - // ---- Stage 2: Interactive shell ---- - run_service("0:/os/shell.elf", "shell"); + // ---- Stage 2: Desktop environment (falls back to shell) ---- + if (!run_service("0:/os/desktop.elf", "desktop")) { + log_warn("Desktop failed, falling back to shell"); + run_service("0:/os/shell.elf", "shell"); + } log_warn("All services exited"); diff --git a/ramdisk.tar b/ramdisk.tar index 8ee5e43..6a5e8fe 100644 Binary files a/ramdisk.tar and b/ramdisk.tar differ diff --git a/scripts/copy_icons.sh b/scripts/copy_icons.sh new file mode 100755 index 0000000..bb35f1d --- /dev/null +++ b/scripts/copy_icons.sh @@ -0,0 +1,48 @@ +#!/bin/bash +# copy_icons.sh - Copy selected SVG icons for the desktop environment +# Usage: ./scripts/copy_icons.sh (from project root or programs/ directory) + +set -e + +# Find project root relative to this script's location +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +ICON_SRC="$PROJECT_ROOT/programs/gui/icons/Flat-Remix-Blue-Light-darkPanel" +ICON_DST="$PROJECT_ROOT/programs/bin/icons" + +mkdir -p "$ICON_DST" + +# Selected icons for the desktop +ICONS=( + "actions/symbolic/view-app-grid-symbolic.svg" + "actions/symbolic/window-close-symbolic.svg" + "actions/symbolic/window-maximize-symbolic.svg" + "actions/symbolic/window-minimize-symbolic.svg" + "apps/symbolic/utilities-terminal-symbolic.svg" + "apps/symbolic/system-file-manager-symbolic.svg" + "apps/symbolic/preferences-desktop-apps-symbolic.svg" + "places/symbolic/folder-symbolic.svg" + "places/symbolic/folder-documents-symbolic.svg" + "places/symbolic/user-home-symbolic.svg" + "mimetypes/symbolic/text-x-generic-symbolic.svg" + "mimetypes/symbolic/application-x-executable-symbolic.svg" + "devices/symbolic/computer-symbolic.svg" + "devices/symbolic/network-wired-symbolic.svg" +) + +copied=0 +for icon in "${ICONS[@]}"; do + src="$ICON_SRC/$icon" + # Extract just the filename + name=$(basename "$icon") + dst="$ICON_DST/$name" + if [ -f "$src" ]; then + cp "$src" "$dst" + copied=$((copied + 1)) + else + echo "copy_icons: warning: $src not found" >&2 + fi +done + +echo "copy_icons: copied $copied icons to $ICON_DST"