feat: TrueType (TTF) font rendering, many new desktop applications and DOOM support, among other improvements

This commit is contained in:
2026-02-20 22:46:41 +01:00
parent a0db5899ef
commit 596be25eaf
124 changed files with 9021 additions and 355 deletions
+1 -15
View File
@@ -1,26 +1,12 @@
/*
* main.cpp
* clear - Clear terminal screen and framebuffer
* clear - Clear terminal screen
* Copyright (c) 2025-2026 Daniel Hammer
*/
#include <zenith/syscall.h>
extern "C" void _start() {
// Clear the raw framebuffer (needed after graphical programs like DOOM)
Zenith::FbInfo fb;
zenith::fb_info(&fb);
uint8_t* pixels = (uint8_t*)zenith::fb_map();
if (pixels) {
for (uint64_t y = 0; y < fb.height; y++) {
uint32_t* row = (uint32_t*)(pixels + y * fb.pitch);
for (uint64_t x = 0; x < fb.width; x++) {
row[x] = 0x00000000;
}
}
}
// Reset the text console
zenith::print("\033[2J"); // Clear entire screen
zenith::print("\033[H"); // Move cursor to top-left
zenith::exit(0);
+6 -7
View File
@@ -20,7 +20,7 @@ LINK_LD := ../../link.ld
BINDIR := ../../bin
OBJDIR := obj
# ---- Compiler flags ----
# ---- C++ compiler flags ----
CXXFLAGS := \
-std=gnu++20 \
@@ -40,10 +40,8 @@ CXXFLAGS := \
-fdata-sections \
-m64 \
-march=x86-64 \
-mno-80387 \
-mno-mmx \
-mno-sse \
-mno-sse2 \
-msse \
-msse2 \
-mno-red-zone \
-mcmodel=small \
-I $(PROG_INC) \
@@ -61,9 +59,9 @@ LDFLAGS := \
-z max-page-size=0x1000 \
-T $(LINK_LD)
# ---- Source files ----
# ---- C++ source files ----
SRCS := main.cpp font_data.cpp app_terminal.cpp app_filemanager.cpp app_sysinfo.cpp app_calculator.cpp app_texteditor.cpp app_klog.cpp app_wiki.cpp app_settings.cpp
SRCS := main.cpp font_data.cpp stb_truetype_impl.cpp app_terminal.cpp app_filemanager.cpp app_sysinfo.cpp app_calculator.cpp app_texteditor.cpp app_klog.cpp app_wiki.cpp app_procmgr.cpp app_mandelbrot.cpp app_devexplorer.cpp app_settings.cpp app_doom.cpp
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
# ---- Target ----
@@ -78,6 +76,7 @@ $(TARGET): $(OBJS) $(LINK_LD) Makefile
mkdir -p $(BINDIR)/os
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) -o $@
# C++ sources
$(OBJDIR)/%.o: %.cpp Makefile
mkdir -p $(OBJDIR)
$(CXX) $(CXXFLAGS) -c $< -o $@
+13 -5
View File
@@ -231,10 +231,18 @@ static void calculator_on_draw(Window* win, Framebuffer& fb) {
c.fill_rect(0, 0, c.w, CALC_DISPLAY_H, Color::from_rgb(0x2D, 0x2D, 0x2D));
// Display text (right-aligned, 2x scale)
int text_len = zenith::slen(cs->display_str);
int text_w = text_len * FONT_WIDTH * 2;
int text_w;
int large_h;
if (fonts::system_font && fonts::system_font->valid) {
text_w = fonts::system_font->measure_text(cs->display_str, fonts::LARGE_SIZE);
large_h = fonts::system_font->get_line_height(fonts::LARGE_SIZE);
} else {
int text_len = zenith::slen(cs->display_str);
text_w = text_len * FONT_WIDTH * 2;
large_h = FONT_HEIGHT * 2;
}
int tx = c.w - text_w - 12;
int ty = (CALC_DISPLAY_H - FONT_HEIGHT * 2) / 2;
int ty = (CALC_DISPLAY_H - large_h) / 2;
if (tx < 4) tx = 4;
c.text_2x(tx, ty, cs->display_str, colors::WHITE);
@@ -271,9 +279,9 @@ static void calculator_on_draw(Window* win, Framebuffer& fb) {
// Button text
const char* label = calc_labels[row][col];
Color label_color = (col == 3) ? colors::WHITE : colors::TEXT_COLOR;
int label_w = zenith::slen(label) * FONT_WIDTH;
int label_w = text_width(label);
int lx = bx + (bw - label_w) / 2;
int ly = by + (CALC_BTN_H - FONT_HEIGHT) / 2;
int ly = by + (CALC_BTN_H - system_font_height()) / 2;
c.text(lx, ly, label, label_color);
}
}
+475
View File
@@ -0,0 +1,475 @@
/*
* app_devexplorer.cpp
* ZenithOS Desktop - Device Explorer (lists hardware detected by the kernel)
* Copyright (c) 2026 Daniel Hammer
*/
#include "apps_common.hpp"
// ============================================================================
// Device Explorer state
// ============================================================================
static constexpr int DE_TOOLBAR_H = 36;
static constexpr int DE_CAT_H = 28;
static constexpr int DE_ITEM_H = 24;
static constexpr int DE_MAX_DEVS = 64;
static constexpr int DE_POLL_MS = 2000;
static constexpr int DE_INDENT = 28;
// Category names matching DevInfo.category values
static const char* category_names[] = {
"CPU", // 0
"Interrupt", // 1
"Timer", // 2
"Input", // 3
"USB", // 4
"Network", // 5
"Display", // 6
"PCI", // 7
};
static constexpr int NUM_CATEGORIES = 8;
static Color category_colors[] = {
Color::from_rgb(0x33, 0x66, 0xCC), // CPU - blue
Color::from_rgb(0x88, 0x44, 0xAA), // Interrupt - purple
Color::from_rgb(0x22, 0x88, 0x22), // Timer - green
Color::from_rgb(0xCC, 0x88, 0x00), // Input - amber
Color::from_rgb(0x00, 0x88, 0x88), // USB - teal
Color::from_rgb(0xCC, 0x55, 0x22), // Network - orange
Color::from_rgb(0x44, 0x66, 0xCC), // Display - indigo
Color::from_rgb(0x66, 0x66, 0x66), // PCI - gray
};
struct DevExplorerState {
DesktopState* desktop;
Zenith::DevInfo devs[DE_MAX_DEVS];
int dev_count;
bool collapsed[NUM_CATEGORIES]; // per-category collapse state
int selected_row; // index into visible display rows (-1 = none)
int scroll_y; // scroll offset in display rows
uint64_t last_poll_ms;
};
// ============================================================================
// Display row model
// ============================================================================
enum RowType { ROW_CATEGORY, ROW_DEVICE };
struct DisplayRow {
RowType type;
int category; // category index (0..7)
int dev_index; // index into devs[] (only valid for ROW_DEVICE)
};
static constexpr int MAX_DISPLAY_ROWS = DE_MAX_DEVS + NUM_CATEGORIES;
static int build_display_rows(DevExplorerState* de, DisplayRow* rows) {
int count = 0;
for (int cat = 0; cat < NUM_CATEGORIES; cat++) {
// Count devices in this category
int cat_count = 0;
for (int d = 0; d < de->dev_count; d++) {
if (de->devs[d].category == cat) cat_count++;
}
if (cat_count == 0) continue;
// Emit category header
rows[count].type = ROW_CATEGORY;
rows[count].category = cat;
rows[count].dev_index = -1;
count++;
// Emit device rows if expanded
if (!de->collapsed[cat]) {
for (int d = 0; d < de->dev_count; d++) {
if (de->devs[d].category == cat) {
rows[count].type = ROW_DEVICE;
rows[count].category = cat;
rows[count].dev_index = d;
count++;
}
}
}
}
return count;
}
// ============================================================================
// Triangle drawing helpers
// ============================================================================
static void draw_triangle_right(Canvas& c, int x, int y, int size, Color col) {
// Right-pointing filled triangle (▶)
int half = size / 2;
for (int row = 0; row < size; row++) {
int dist = (row <= half) ? row : (size - 1 - row);
for (int col_px = 0; col_px <= dist; col_px++) {
c.put_pixel(x + col_px, y + row, col);
}
}
}
static void draw_triangle_down(Canvas& c, int x, int y, int size, Color col) {
// Down-pointing filled triangle (▼)
int half = size / 2;
for (int row = 0; row < half + 1; row++) {
int w = size - row * 2;
for (int col_px = 0; col_px < w; col_px++) {
c.put_pixel(x + row + col_px, y + row, col);
}
}
}
// ============================================================================
// Callbacks
// ============================================================================
static void devexplorer_on_poll(Window* win) {
DevExplorerState* de = (DevExplorerState*)win->app_data;
if (!de) return;
uint64_t now = zenith::get_milliseconds();
if (now - de->last_poll_ms < DE_POLL_MS) return;
de->last_poll_ms = now;
de->dev_count = zenith::devlist(de->devs, DE_MAX_DEVS);
}
static void devexplorer_on_draw(Window* win, Framebuffer& fb) {
DevExplorerState* de = (DevExplorerState*)win->app_data;
if (!de) return;
Canvas c(win);
c.fill(colors::WINDOW_BG);
int fh = system_font_height();
// --- Toolbar ---
c.fill_rect(0, 0, c.w, DE_TOOLBAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5));
c.hline(0, DE_TOOLBAR_H - 1, c.w, colors::BORDER);
// "Refresh" button
int btn_w = 80;
int btn_h = 26;
int btn_x = 8;
int btn_y = (DE_TOOLBAR_H - btn_h) / 2;
c.button(btn_x, btn_y, btn_w, btn_h, "Refresh",
Color::from_rgb(0x33, 0x66, 0xCC), colors::WHITE, 4);
// Device count on right
char count_str[24];
snprintf(count_str, sizeof(count_str), "%d devices", de->dev_count);
int cw = text_width(count_str);
c.text(c.w - cw - 12, (DE_TOOLBAR_H - fh) / 2, count_str, colors::TEXT_COLOR);
// --- Build display rows ---
DisplayRow rows[MAX_DISPLAY_ROWS];
int row_count = build_display_rows(de, rows);
// --- Compute visible area ---
int list_y = DE_TOOLBAR_H;
int list_h = c.h - list_y;
if (list_h < 1) return;
// Clamp scroll
// Calculate total content height
int total_h = 0;
for (int i = 0; i < row_count; i++) {
total_h += (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H;
}
int max_scroll_px = total_h - list_h;
if (max_scroll_px < 0) max_scroll_px = 0;
// Convert scroll_y (in rows) to pixel offset for simplicity,
// but keep the row-based model: scroll_y = row offset
// We'll compute cumulative pixel offsets
int scroll_px = 0;
for (int i = 0; i < de->scroll_y && i < row_count; i++) {
scroll_px += (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H;
}
if (scroll_px > max_scroll_px) {
scroll_px = max_scroll_px;
// Recalculate scroll_y to match
de->scroll_y = 0;
int acc = 0;
for (int i = 0; i < row_count; i++) {
int rh = (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H;
if (acc + rh > max_scroll_px) break;
acc += rh;
de->scroll_y = i + 1;
}
}
if (de->scroll_y < 0) de->scroll_y = 0;
// Clamp selected_row
if (de->selected_row >= row_count) de->selected_row = row_count - 1;
// --- Draw rows ---
int draw_y = list_y - scroll_px;
// Advance to first visible area by recomputing from scroll offset
draw_y = list_y;
int first_visible = de->scroll_y;
// Compute starting y by accumulating heights of skipped rows
// Actually, let's just iterate all rows and skip those above the viewport
draw_y = list_y;
for (int i = 0; i < first_visible && i < row_count; i++) {
draw_y -= (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H;
}
// Draw from first_visible onward
int cur_y = list_y;
for (int i = first_visible; i < row_count; i++) {
int row_h = (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H;
if (cur_y >= c.h) break; // Past bottom of window
if (rows[i].type == ROW_CATEGORY) {
// Category header
int cat = rows[i].category;
c.fill_rect(0, cur_y, c.w, DE_CAT_H, Color::from_rgb(0xF0, 0xF0, 0xF0));
// Highlight if selected
if (i == de->selected_row) {
c.fill_rect(0, cur_y, c.w, DE_CAT_H, colors::MENU_HOVER);
}
// Expand/collapse triangle
int tri_x = 10;
int tri_y = cur_y + (DE_CAT_H - 8) / 2;
Color tri_color = Color::from_rgb(0x55, 0x55, 0x55);
if (de->collapsed[cat]) {
draw_triangle_right(c, tri_x, tri_y, 8, tri_color);
} else {
draw_triangle_down(c, tri_x, tri_y, 8, tri_color);
}
// Colored dot
int dot_x = 24;
int dot_y = cur_y + (DE_CAT_H - 8) / 2;
Color cat_col = (cat >= 0 && cat < NUM_CATEGORIES)
? category_colors[cat] : colors::TEXT_COLOR;
c.fill_rounded_rect(dot_x, dot_y, 8, 8, 4, cat_col);
// Category name
const char* cat_name = (cat >= 0 && cat < NUM_CATEGORIES)
? category_names[cat] : "?";
int text_y = cur_y + (DE_CAT_H - fh) / 2;
c.text(36, text_y, cat_name, Color::from_rgb(0x33, 0x33, 0x33));
// Device count in parentheses on right
int cat_count = 0;
for (int d = 0; d < de->dev_count; d++) {
if (de->devs[d].category == cat) cat_count++;
}
char cnt_buf[16];
snprintf(cnt_buf, sizeof(cnt_buf), "(%d)", cat_count);
int name_w = text_width(cat_name);
c.text(36 + name_w + 8, text_y, cnt_buf,
Color::from_rgb(0x88, 0x88, 0x88));
// Bottom border
c.hline(0, cur_y + DE_CAT_H - 1, c.w, Color::from_rgb(0xE0, 0xE0, 0xE0));
} else {
// Device item row
int di = rows[i].dev_index;
// Selected row highlight
if (i == de->selected_row) {
c.fill_rect(0, cur_y, c.w, DE_ITEM_H, colors::MENU_HOVER);
}
int text_y = cur_y + (DE_ITEM_H - fh) / 2;
// Device name (indented)
c.text(DE_INDENT + 10, text_y, de->devs[di].name, colors::TEXT_COLOR);
// Detail string (right column, gray)
int detail_x = c.w / 2 + 20;
c.text(detail_x, text_y, de->devs[di].detail,
Color::from_rgb(0x66, 0x66, 0x66));
}
cur_y += row_h;
}
// --- Scrollbar ---
if (total_h > list_h) {
int sb_x = c.w - 6;
int thumb_h = (list_h * list_h) / total_h;
if (thumb_h < 20) thumb_h = 20;
int thumb_y = list_y + (scroll_px * (list_h - thumb_h)) / max_scroll_px;
c.fill_rect(sb_x, list_y, 4, list_h, Color::from_rgb(0xE0, 0xE0, 0xE0));
c.fill_rect(sb_x, thumb_y, 4, thumb_h, Color::from_rgb(0xAA, 0xAA, 0xAA));
}
}
static void devexplorer_on_mouse(Window* win, MouseEvent& ev) {
DevExplorerState* de = (DevExplorerState*)win->app_data;
if (!de) return;
Rect cr = win->content_rect();
int lx = ev.x - cr.x;
int ly = ev.y - cr.y;
// Scroll
if (ev.scroll != 0) {
de->scroll_y -= ev.scroll;
if (de->scroll_y < 0) de->scroll_y = 0;
return;
}
if (ev.left_pressed()) {
// Check "Refresh" button click
int btn_w = 80;
int btn_h = 26;
int btn_x = 8;
int btn_y = (DE_TOOLBAR_H - btn_h) / 2;
Rect btn_rect = {btn_x, btn_y, btn_w, btn_h};
if (btn_rect.contains(lx, ly)) {
de->last_poll_ms = 0; // force refresh
return;
}
// Check row click in list area
int list_y = DE_TOOLBAR_H;
if (ly >= list_y) {
// Build display rows to map click to row
DisplayRow rows[MAX_DISPLAY_ROWS];
int row_count = build_display_rows(de, rows);
// Walk rows from scroll offset, accumulating y
int cur_y = list_y;
for (int i = de->scroll_y; i < row_count; i++) {
int row_h = (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H;
if (ly >= cur_y && ly < cur_y + row_h) {
// Hit this row
if (rows[i].type == ROW_CATEGORY) {
// Toggle collapse
int cat = rows[i].category;
de->collapsed[cat] = !de->collapsed[cat];
de->selected_row = -1;
} else {
de->selected_row = i;
}
return;
}
cur_y += row_h;
if (cur_y >= win->content_h) break;
}
// Clicked below all rows
de->selected_row = -1;
}
}
}
static void devexplorer_on_key(Window* win, const Zenith::KeyEvent& key) {
DevExplorerState* de = (DevExplorerState*)win->app_data;
if (!de || !key.pressed) return;
DisplayRow rows[MAX_DISPLAY_ROWS];
int row_count = build_display_rows(de, rows);
if (row_count == 0) return;
if (key.scancode == 0x48) { // Up arrow
if (de->selected_row <= 0) {
de->selected_row = 0;
} else {
de->selected_row--;
}
// Scroll to keep selection visible
if (de->selected_row < de->scroll_y) {
de->scroll_y = de->selected_row;
}
} else if (key.scancode == 0x50) { // Down arrow
if (de->selected_row < row_count - 1) {
de->selected_row++;
}
// Scroll to keep selection visible — estimate visible rows
int list_h = win->content_h - DE_TOOLBAR_H;
int cur_h = 0;
int last_visible = de->scroll_y;
for (int i = de->scroll_y; i < row_count; i++) {
int rh = (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H;
if (cur_h + rh > list_h) break;
cur_h += rh;
last_visible = i;
}
if (de->selected_row > last_visible) {
de->scroll_y += (de->selected_row - last_visible);
}
} else if (key.scancode == 0x4B) { // Left arrow — collapse
if (de->selected_row >= 0 && de->selected_row < row_count) {
int cat = rows[de->selected_row].category;
if (!de->collapsed[cat]) {
de->collapsed[cat] = true;
// If we were on a device row, move selection to the category header
if (rows[de->selected_row].type == ROW_DEVICE) {
// Find the category header row for this category
for (int i = de->selected_row - 1; i >= 0; i--) {
if (rows[i].type == ROW_CATEGORY && rows[i].category == cat) {
de->selected_row = i;
break;
}
}
// After collapsing, rebuild and clamp
int new_count = build_display_rows(de, rows);
if (de->selected_row >= new_count)
de->selected_row = new_count - 1;
}
}
}
} else if (key.scancode == 0x4D) { // Right arrow — expand
if (de->selected_row >= 0 && de->selected_row < row_count) {
int cat = rows[de->selected_row].category;
de->collapsed[cat] = false;
}
} else if (key.scancode == 0x1C) { // Enter — toggle on category row
if (de->selected_row >= 0 && de->selected_row < row_count) {
if (rows[de->selected_row].type == ROW_CATEGORY) {
int cat = rows[de->selected_row].category;
de->collapsed[cat] = !de->collapsed[cat];
}
}
}
}
static void devexplorer_on_close(Window* win) {
if (win->app_data) {
zenith::mfree(win->app_data);
win->app_data = nullptr;
}
}
// ============================================================================
// Device Explorer launcher
// ============================================================================
void open_devexplorer(DesktopState* ds) {
int idx = desktop_create_window(ds, "Devices", 140, 70, 640, 460);
if (idx < 0) return;
Window* win = &ds->windows[idx];
DevExplorerState* de = (DevExplorerState*)zenith::malloc(sizeof(DevExplorerState));
zenith::memset(de, 0, sizeof(DevExplorerState));
de->desktop = ds;
de->selected_row = -1;
de->scroll_y = 0;
de->last_poll_ms = 0;
// All categories start expanded
for (int i = 0; i < NUM_CATEGORIES; i++)
de->collapsed[i] = false;
// Initial poll
de->dev_count = zenith::devlist(de->devs, DE_MAX_DEVS);
win->app_data = de;
win->on_draw = devexplorer_on_draw;
win->on_mouse = devexplorer_on_mouse;
win->on_key = devexplorer_on_key;
win->on_close = devexplorer_on_close;
win->on_poll = devexplorer_on_poll;
}
+12
View File
@@ -0,0 +1,12 @@
/*
* app_doom.cpp
* ZenithOS Desktop - DOOM launcher (spawns standalone doom.elf)
* Copyright (c) 2026 Daniel Hammer
*/
#include "apps_common.hpp"
void open_doom(DesktopState* ds) {
(void)ds;
zenith::spawn("0:/games/doom.elf");
}
+6 -5
View File
@@ -373,7 +373,7 @@ static void filemanager_on_draw(Window* win, Framebuffer& fb) {
int tx = cell_x + (FM_GRID_CELL_W - tw) / 2;
if (tx < cell_x) tx = cell_x;
int ty = icon_y + FM_GRID_ICON + 2;
if (ty >= list_y && ty + FONT_HEIGHT <= c.h)
if (ty >= list_y && ty + system_font_height() <= c.h)
c.text(tx, ty, label, colors::TEXT_COLOR);
}
} else {
@@ -447,19 +447,20 @@ static void filemanager_on_draw(Window* win, Framebuffer& fb) {
// Name
int tx = 30;
int ty = iy + (FM_ITEM_H - FONT_HEIGHT) / 2;
if (ty >= list_y && ty + FONT_HEIGHT <= c.h)
int fm_sfh = system_font_height();
int ty = iy + (FM_ITEM_H - fm_sfh) / 2;
if (ty >= list_y && ty + fm_sfh <= c.h)
c.text(tx, ty, fm->entry_names[i], colors::TEXT_COLOR);
// Size
if (size_col_x > 100 && !fm->is_dir[i] && ty >= list_y && ty + FONT_HEIGHT <= c.h) {
if (size_col_x > 100 && !fm->is_dir[i] && ty >= list_y && ty + fm_sfh <= c.h) {
char size_str[16];
format_size(size_str, fm->entry_sizes[i]);
c.text(size_col_x, ty, size_str, dim);
}
// Type
if (type_col_x > 160 && ty >= list_y && ty + FONT_HEIGHT <= c.h) {
if (type_col_x > 160 && ty >= list_y && ty + fm_sfh <= c.h) {
const char* type_str = "File";
if (fm->entry_types[i] == 1) type_str = "Dir";
else if (fm->entry_types[i] == 2) type_str = "Exec";
+2 -2
View File
@@ -119,8 +119,8 @@ void open_klog(DesktopState* ds) {
Window* win = &ds->windows[idx];
Rect cr = win->content_rect();
int cols = cr.w / FONT_WIDTH;
int rows = cr.h / FONT_HEIGHT;
int cols = cr.w / mono_cell_width();
int rows = cr.h / mono_cell_height();
KlogState* klog = (KlogState*)zenith::malloc(sizeof(KlogState));
zenith::memset(klog, 0, sizeof(KlogState));
+296
View File
@@ -0,0 +1,296 @@
/*
* app_mandelbrot.cpp
* ZenithOS Desktop - Mandelbrot set visualizer
* Supports zoom (scroll wheel), pan (drag), and reset (R key)
* Copyright (c) 2026 Daniel Hammer
*/
#include "apps_common.hpp"
// ============================================================================
// Fixed-point Mandelbrot (avoids FPU/SSE dependency issues)
// Uses 28.36 fixed-point: 36 fractional bits gives ~1e-10 precision
// ============================================================================
using fp_t = int64_t;
static constexpr int FP_SHIFT = 36;
static constexpr fp_t FP_ONE = (fp_t)1 << FP_SHIFT;
static inline fp_t fp_from_int(int v) { return (fp_t)v << FP_SHIFT; }
static inline fp_t fp_mul(fp_t a, fp_t b) {
// Split to avoid overflow: a * b >> SHIFT
// Use 128-bit intermediate via compiler builtin
__int128 r = (__int128)a * b;
return (fp_t)(r >> FP_SHIFT);
}
// fp_div for small numerators (avoids __divti3 by using 64-bit division)
// Only valid when a is small enough that a << FP_SHIFT fits in int64_t
static inline fp_t fp_div_small(int a, int b) {
return ((fp_t)a << FP_SHIFT) / (fp_t)b;
}
// ============================================================================
// State
// ============================================================================
static constexpr int MB_MAX_ITER = 256;
static constexpr int MB_TOOLBAR_H = 32;
struct MandelbrotState {
DesktopState* desktop;
fp_t center_x, center_y; // center of view in fractal coords
fp_t scale; // units per pixel (fixed-point)
int max_iter;
bool needs_render;
// Drag state
bool dragging;
int drag_start_x, drag_start_y;
fp_t drag_start_cx, drag_start_cy;
};
// ============================================================================
// Color palette
// ============================================================================
static uint32_t mandelbrot_color(int iter, int max_iter) {
if (iter >= max_iter) return 0xFF000000; // black for inside set
// Smooth cycling palette using bit manipulation
int t = (iter * 7) & 0xFF;
int phase = (iter * 7) >> 8;
uint8_t r, g, b;
switch (phase % 6) {
case 0: r = 255; g = (uint8_t)t; b = 0; break;
case 1: r = (uint8_t)(255 - t); g = 255; b = 0; break;
case 2: r = 0; g = 255; b = (uint8_t)t; break;
case 3: r = 0; g = (uint8_t)(255 - t); b = 255; break;
case 4: r = (uint8_t)t; g = 0; b = 255; break;
default: r = 255; g = 0; b = (uint8_t)(255 - t); break;
}
return 0xFF000000 | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
}
// ============================================================================
// Render
// ============================================================================
static void mandelbrot_render(MandelbrotState* mb, uint32_t* pixels, int w, int h) {
int render_h = h - MB_TOOLBAR_H;
if (render_h <= 0) return;
fp_t half_w = fp_mul(fp_from_int(w / 2), mb->scale);
fp_t half_h = fp_mul(fp_from_int(render_h / 2), mb->scale);
fp_t x_min = mb->center_x - half_w;
fp_t y_min = mb->center_y - half_h;
fp_t bailout = fp_from_int(4);
for (int py = 0; py < render_h; py++) {
fp_t ci = y_min + fp_mul(fp_from_int(py), mb->scale);
uint32_t* row = pixels + (py + MB_TOOLBAR_H) * w;
for (int px = 0; px < w; px++) {
fp_t cr = x_min + fp_mul(fp_from_int(px), mb->scale);
fp_t zr = 0, zi = 0;
int iter = 0;
while (iter < mb->max_iter) {
fp_t zr2 = fp_mul(zr, zr);
fp_t zi2 = fp_mul(zi, zi);
if (zr2 + zi2 > bailout) break;
fp_t new_zr = zr2 - zi2 + cr;
zi = fp_mul(fp_from_int(2), fp_mul(zr, zi)) + ci;
zr = new_zr;
iter++;
}
row[px] = mandelbrot_color(iter, mb->max_iter);
}
}
mb->needs_render = false;
}
// ============================================================================
// Callbacks
// ============================================================================
static void mb_on_draw(Window* win, Framebuffer& fb) {
MandelbrotState* mb = (MandelbrotState*)win->app_data;
if (!mb) return;
Canvas c(win);
if (mb->needs_render) {
mandelbrot_render(mb, win->content, c.w, c.h);
}
// Draw toolbar over the render
c.fill_rect(0, 0, c.w, MB_TOOLBAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5));
c.hline(0, MB_TOOLBAR_H - 1, c.w, colors::BORDER);
// Reset button
c.button(8, 4, 60, 24, "Reset", colors::ACCENT, colors::WHITE, 4);
// Iter +/- buttons
char iter_str[24];
snprintf(iter_str, sizeof(iter_str), "Iter: %d", mb->max_iter);
int fh = system_font_height();
c.text(80, (MB_TOOLBAR_H - fh) / 2, iter_str, colors::TEXT_COLOR);
int iter_text_w = text_width(iter_str);
int btn_x = 80 + iter_text_w + 8;
c.button(btn_x, 4, 24, 24, "-", Color::from_rgb(0xAA, 0xAA, 0xAA), colors::WHITE, 4);
c.button(btn_x + 28, 4, 24, 24, "+", Color::from_rgb(0xAA, 0xAA, 0xAA), colors::WHITE, 4);
// Hint
const char* hint = "Scroll=zoom Drag=pan";
int hw = text_width(hint);
c.text(c.w - hw - 8, (MB_TOOLBAR_H - fh) / 2, hint, Color::from_rgb(0x99, 0x99, 0x99));
}
static void mb_on_mouse(Window* win, MouseEvent& ev) {
MandelbrotState* mb = (MandelbrotState*)win->app_data;
if (!mb) return;
Rect cr = win->content_rect();
int lx = ev.x - cr.x;
int ly = ev.y - cr.y;
// Handle toolbar clicks
if (ev.left_pressed() && ly < MB_TOOLBAR_H) {
// Reset button
Rect reset_r = {8, 4, 60, 24};
if (reset_r.contains(lx, ly)) {
mb->center_x = -fp_from_int(1) / 2; // -0.5
mb->center_y = 0;
mb->scale = fp_div_small(3, cr.w > 0 ? cr.w : 400);
mb->max_iter = MB_MAX_ITER;
mb->needs_render = true;
return;
}
// Iter buttons
char iter_str[24];
snprintf(iter_str, sizeof(iter_str), "Iter: %d", mb->max_iter);
int iter_text_w = text_width(iter_str);
int btn_x = 80 + iter_text_w + 8;
Rect minus_r = {btn_x, 4, 24, 24};
Rect plus_r = {btn_x + 28, 4, 24, 24};
if (minus_r.contains(lx, ly)) {
if (mb->max_iter > 32) { mb->max_iter /= 2; mb->needs_render = true; }
return;
}
if (plus_r.contains(lx, ly)) {
if (mb->max_iter < 4096) { mb->max_iter *= 2; mb->needs_render = true; }
return;
}
return;
}
// Drag panning
if (ev.left_pressed() && ly >= MB_TOOLBAR_H) {
mb->dragging = true;
mb->drag_start_x = lx;
mb->drag_start_y = ly;
mb->drag_start_cx = mb->center_x;
mb->drag_start_cy = mb->center_y;
return;
}
if (mb->dragging && ev.left_held()) {
int dx = lx - mb->drag_start_x;
int dy = ly - mb->drag_start_y;
mb->center_x = mb->drag_start_cx - fp_mul(fp_from_int(dx), mb->scale);
mb->center_y = mb->drag_start_cy - fp_mul(fp_from_int(dy), mb->scale);
mb->needs_render = true;
return;
}
if (mb->dragging && !ev.left_held()) {
mb->dragging = false;
}
// Scroll zoom (centered on mouse position)
if (ev.scroll != 0 && ly >= MB_TOOLBAR_H) {
int render_h = cr.h - MB_TOOLBAR_H;
// Mouse position in fractal coords before zoom
fp_t mx_frac = mb->center_x + fp_mul(fp_from_int(lx - cr.w / 2), mb->scale);
fp_t my_frac = mb->center_y + fp_mul(fp_from_int((ly - MB_TOOLBAR_H) - render_h / 2), mb->scale);
if (ev.scroll < 0) {
// Zoom in
mb->scale = fp_mul(mb->scale, fp_from_int(3) / 4); // * 0.75
} else {
// Zoom out
mb->scale = fp_mul(mb->scale, fp_from_int(4) / 3); // * 1.33
}
// Adjust center so mouse stays at same fractal point
fp_t new_mx = mb->center_x + fp_mul(fp_from_int(lx - cr.w / 2), mb->scale);
fp_t new_my = mb->center_y + fp_mul(fp_from_int((ly - MB_TOOLBAR_H) - render_h / 2), mb->scale);
mb->center_x += mx_frac - new_mx;
mb->center_y += my_frac - new_my;
mb->needs_render = true;
}
}
static void mb_on_key(Window* win, const Zenith::KeyEvent& key) {
MandelbrotState* mb = (MandelbrotState*)win->app_data;
if (!mb || !key.pressed) return;
if (key.ascii == 'r' || key.ascii == 'R') {
Rect cr = win->content_rect();
mb->center_x = -fp_from_int(1) / 2;
mb->center_y = 0;
mb->scale = fp_div_small(3, cr.w > 0 ? cr.w : 400);
mb->max_iter = MB_MAX_ITER;
mb->needs_render = true;
} else if (key.ascii == '+' || key.ascii == '=') {
if (mb->max_iter < 4096) { mb->max_iter *= 2; mb->needs_render = true; }
} else if (key.ascii == '-') {
if (mb->max_iter > 32) { mb->max_iter /= 2; mb->needs_render = true; }
}
}
static void mb_on_close(Window* win) {
if (win->app_data) {
zenith::mfree(win->app_data);
win->app_data = nullptr;
}
}
// ============================================================================
// Mandelbrot launcher
// ============================================================================
void open_mandelbrot(DesktopState* ds) {
int idx = desktop_create_window(ds, "Mandelbrot", 120, 60, 500, 400);
if (idx < 0) return;
Window* win = &ds->windows[idx];
Rect cr = win->content_rect();
MandelbrotState* mb = (MandelbrotState*)zenith::malloc(sizeof(MandelbrotState));
zenith::memset(mb, 0, sizeof(MandelbrotState));
mb->desktop = ds;
mb->center_x = -fp_from_int(1) / 2; // -0.5
mb->center_y = 0;
mb->scale = fp_div_small(3, cr.w > 0 ? cr.w : 400);
mb->max_iter = MB_MAX_ITER;
mb->needs_render = true;
mb->dragging = false;
win->app_data = mb;
win->on_draw = mb_on_draw;
win->on_mouse = mb_on_mouse;
win->on_key = mb_on_key;
win->on_close = mb_on_close;
}
+244
View File
@@ -0,0 +1,244 @@
/*
* app_procmgr.cpp
* ZenithOS Desktop - Process Manager
* Copyright (c) 2026 Daniel Hammer
*/
#include "apps_common.hpp"
// ============================================================================
// Process Manager state
// ============================================================================
static constexpr int PM_TOOLBAR_H = 36;
static constexpr int PM_HEADER_H = 24;
static constexpr int PM_ITEM_H = 28;
static constexpr int PM_MAX_PROCS = 16;
static constexpr int PM_POLL_MS = 1000;
struct ProcMgrState {
DesktopState* desktop;
Zenith::ProcInfo procs[PM_MAX_PROCS];
int proc_count;
int selected; // selected row index (-1 = none)
uint64_t last_poll_ms;
};
// ============================================================================
// Callbacks
// ============================================================================
static void procmgr_on_poll(Window* win) {
ProcMgrState* pm = (ProcMgrState*)win->app_data;
if (!pm) return;
uint64_t now = zenith::get_milliseconds();
if (now - pm->last_poll_ms < PM_POLL_MS) return;
pm->last_poll_ms = now;
// Remember previously selected PID
int prev_pid = -1;
if (pm->selected >= 0 && pm->selected < pm->proc_count) {
prev_pid = pm->procs[pm->selected].pid;
}
pm->proc_count = zenith::proclist(pm->procs, PM_MAX_PROCS);
// Restore selection by matching PID
pm->selected = -1;
if (prev_pid >= 0) {
for (int i = 0; i < pm->proc_count; i++) {
if (pm->procs[i].pid == prev_pid) {
pm->selected = i;
break;
}
}
}
}
static void procmgr_on_draw(Window* win, Framebuffer& fb) {
ProcMgrState* pm = (ProcMgrState*)win->app_data;
if (!pm) return;
Canvas c(win);
c.fill(colors::WINDOW_BG);
int fh = system_font_height();
// --- Toolbar ---
c.fill_rect(0, 0, c.w, PM_TOOLBAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5));
c.hline(0, PM_TOOLBAR_H - 1, c.w, colors::BORDER);
// "End Process" button
int btn_w = 100;
int btn_h = 26;
int btn_x = 8;
int btn_y = (PM_TOOLBAR_H - btn_h) / 2;
Color btn_bg = (pm->selected >= 0 && pm->procs[pm->selected].pid != 0)
? Color::from_rgb(0xCC, 0x33, 0x33)
: Color::from_rgb(0xAA, 0xAA, 0xAA);
c.button(btn_x, btn_y, btn_w, btn_h, "End Process", btn_bg, colors::WHITE, 4);
// Process count on right
char count_str[24];
snprintf(count_str, sizeof(count_str), "%d processes", pm->proc_count);
int cw = text_width(count_str);
c.text(c.w - cw - 12, (PM_TOOLBAR_H - fh) / 2, count_str, colors::TEXT_COLOR);
// --- Header row ---
int header_y = PM_TOOLBAR_H;
c.fill_rect(0, header_y, c.w, PM_HEADER_H, Color::from_rgb(0xF0, 0xF0, 0xF0));
int col_pid = 12;
int col_name = 64;
int col_mem = c.w - 120;
int col_state = c.w - 52;
int ty = header_y + (PM_HEADER_H - fh) / 2;
c.text(col_pid, ty, "PID", Color::from_rgb(0x66, 0x66, 0x66));
c.text(col_name, ty, "Name", Color::from_rgb(0x66, 0x66, 0x66));
c.text(col_mem, ty, "Mem", Color::from_rgb(0x66, 0x66, 0x66));
c.text(col_state, ty, "St", Color::from_rgb(0x66, 0x66, 0x66));
c.hline(0, header_y + PM_HEADER_H - 1, c.w, colors::BORDER);
// --- Process rows ---
int list_y = PM_TOOLBAR_H + PM_HEADER_H;
for (int i = 0; i < pm->proc_count; i++) {
int row_y = list_y + i * PM_ITEM_H;
if (row_y + PM_ITEM_H > c.h) break;
// Selected row highlight
if (i == pm->selected) {
c.fill_rect(0, row_y, c.w, PM_ITEM_H, colors::MENU_HOVER);
}
int ry = row_y + (PM_ITEM_H - fh) / 2;
// PID (right-aligned in PID column)
char pid_str[8];
snprintf(pid_str, sizeof(pid_str), "%d", (int)pm->procs[i].pid);
int pid_w = text_width(pid_str);
c.text(col_pid + 32 - pid_w, ry, pid_str, colors::TEXT_COLOR);
// Name (truncated to fit)
c.text(col_name, ry, pm->procs[i].name, colors::TEXT_COLOR);
// Memory
char mem_str[16];
format_size(mem_str, (int)pm->procs[i].heapUsed);
c.text(col_mem, ry, mem_str, colors::TEXT_COLOR);
// State
const char* st_str;
Color st_color;
switch (pm->procs[i].state) {
case 2: // Running
st_str = "Run";
st_color = Color::from_rgb(0x22, 0x88, 0x22);
break;
case 1: // Ready
st_str = "Rdy";
st_color = Color::from_rgb(0x33, 0x66, 0xCC);
break;
case 3: // Terminated
st_str = "Term";
st_color = Color::from_rgb(0xCC, 0x33, 0x33);
break;
default:
st_str = "?";
st_color = colors::TEXT_COLOR;
break;
}
c.text(col_state, ry, st_str, st_color);
}
}
static void procmgr_on_mouse(Window* win, MouseEvent& ev) {
ProcMgrState* pm = (ProcMgrState*)win->app_data;
if (!pm) return;
Rect cr = win->content_rect();
int lx = ev.x - cr.x;
int ly = ev.y - cr.y;
if (ev.left_pressed()) {
// Check "End Process" button click
int btn_w = 100;
int btn_h = 26;
int btn_x = 8;
int btn_y = (PM_TOOLBAR_H - btn_h) / 2;
Rect btn_rect = {btn_x, btn_y, btn_w, btn_h};
if (btn_rect.contains(lx, ly)) {
if (pm->selected >= 0 && pm->selected < pm->proc_count
&& pm->procs[pm->selected].pid != 0) {
zenith::kill(pm->procs[pm->selected].pid);
pm->last_poll_ms = 0; // force refresh
}
return;
}
// Check row click
int list_y = PM_TOOLBAR_H + PM_HEADER_H;
if (ly >= list_y) {
int row = (ly - list_y) / PM_ITEM_H;
if (row >= 0 && row < pm->proc_count) {
pm->selected = row;
} else {
pm->selected = -1;
}
}
}
}
static void procmgr_on_key(Window* win, const Zenith::KeyEvent& key) {
ProcMgrState* pm = (ProcMgrState*)win->app_data;
if (!pm || !key.pressed) return;
if (key.scancode == 0x48) { // Up arrow
if (pm->selected > 0) pm->selected--;
else if (pm->proc_count > 0) pm->selected = 0;
} else if (key.scancode == 0x50) { // Down arrow
if (pm->selected < pm->proc_count - 1) pm->selected++;
} else if (key.scancode == 0x53) { // Delete key
if (pm->selected >= 0 && pm->selected < pm->proc_count
&& pm->procs[pm->selected].pid != 0) {
zenith::kill(pm->procs[pm->selected].pid);
pm->last_poll_ms = 0; // force refresh
}
}
}
static void procmgr_on_close(Window* win) {
if (win->app_data) {
zenith::mfree(win->app_data);
win->app_data = nullptr;
}
}
// ============================================================================
// Process Manager launcher
// ============================================================================
void open_procmgr(DesktopState* ds) {
int idx = desktop_create_window(ds, "Processes", 180, 80, 520, 400);
if (idx < 0) return;
Window* win = &ds->windows[idx];
ProcMgrState* pm = (ProcMgrState*)zenith::malloc(sizeof(ProcMgrState));
zenith::memset(pm, 0, sizeof(ProcMgrState));
pm->desktop = ds;
pm->selected = -1;
pm->last_poll_ms = 0;
// Initial poll
pm->proc_count = zenith::proclist(pm->procs, PM_MAX_PROCS);
win->app_data = pm;
win->on_draw = procmgr_on_draw;
win->on_mouse = procmgr_on_mouse;
win->on_key = procmgr_on_key;
win->on_close = procmgr_on_close;
win->on_poll = procmgr_on_poll;
}
+444 -23
View File
@@ -1,38 +1,252 @@
/*
* app_settings.cpp
* ZenithOS Desktop - About application
* ZenithOS Desktop - Settings application
* Copyright (c) 2026 Daniel Hammer
*/
#include "apps_common.hpp"
// ============================================================================
// About state and callbacks
// Settings state
// ============================================================================
struct AboutState {
struct SettingsState {
DesktopState* desktop;
int active_tab; // 0=Appearance, 1=Display, 2=About
Zenith::SysInfo sys_info;
uint64_t uptime_ms;
};
static void about_on_draw(Window* win, Framebuffer& fb) {
AboutState* st = (AboutState*)win->app_data;
if (!st) return;
// ============================================================================
// Color palette presets
// ============================================================================
static constexpr int SWATCH_COUNT = 8;
static constexpr int SWATCH_SIZE = 24;
static constexpr int SWATCH_GAP = 6;
// Background colors (light tones)
static const Color bg_palette[SWATCH_COUNT] = {
Color::from_rgb(0xD0, 0xD8, 0xE8), // light blue-gray (default)
Color::from_rgb(0xE8, 0xDD, 0xCB), // warm beige
Color::from_rgb(0xC8, 0xE6, 0xD0), // mint green
Color::from_rgb(0xD8, 0xD0, 0xE8), // lavender
Color::from_rgb(0xB8, 0xBE, 0xC8), // slate
Color::from_rgb(0xF0, 0xF0, 0xF0), // white
Color::from_rgb(0xE8, 0xD0, 0xD8), // soft pink
Color::from_rgb(0xE8, 0xE0, 0xC8), // light gold
};
// Panel colors (dark tones)
static const Color panel_palette[SWATCH_COUNT] = {
Color::from_rgb(0x2B, 0x3E, 0x50), // dark blue-gray (default)
Color::from_rgb(0x2D, 0x2D, 0x2D), // dark charcoal
Color::from_rgb(0x1B, 0x2A, 0x4A), // navy
Color::from_rgb(0x1A, 0x3A, 0x3A), // dark teal
Color::from_rgb(0x1A, 0x3A, 0x1A), // dark green
Color::from_rgb(0x30, 0x20, 0x40), // dark purple
Color::from_rgb(0x40, 0x1A, 0x1A), // dark red
Color::from_rgb(0x10, 0x10, 0x10), // black
};
// Accent colors
static const Color accent_palette[SWATCH_COUNT] = {
Color::from_rgb(0x36, 0x7B, 0xF0), // blue (default)
Color::from_rgb(0x00, 0x9B, 0x9B), // teal
Color::from_rgb(0x2E, 0x9E, 0x3E), // green
Color::from_rgb(0xE0, 0x8A, 0x20), // orange
Color::from_rgb(0xD0, 0x3E, 0x3E), // red
Color::from_rgb(0x7B, 0x3E, 0xB8), // purple
Color::from_rgb(0xD0, 0x5C, 0x9E), // pink
Color::from_rgb(0x44, 0x44, 0xCC), // indigo
};
// ============================================================================
// Layout constants
// ============================================================================
static constexpr int TAB_BAR_H = 36;
static constexpr int TAB_COUNT = 3;
static const char* tab_labels[TAB_COUNT] = { "Appearance", "Display", "About" };
// ============================================================================
// Helper: check if two colors match
// ============================================================================
static bool color_eq(Color a, Color b) {
return a.r == b.r && a.g == b.g && a.b == b.b;
}
// ============================================================================
// Helper: find selected swatch index in a palette
// ============================================================================
static int find_swatch(const Color* palette, Color current) {
for (int i = 0; i < SWATCH_COUNT; i++) {
if (color_eq(palette[i], current)) return i;
}
return -1;
}
// ============================================================================
// Drawing
// ============================================================================
static void draw_swatch_row(Canvas& c, int x, int y, const Color* palette,
Color selected, Color accent) {
int sel = find_swatch(palette, selected);
for (int i = 0; i < SWATCH_COUNT; i++) {
int sx = x + i * (SWATCH_SIZE + SWATCH_GAP);
// Draw selection border
if (i == sel) {
c.fill_rounded_rect(sx - 2, y - 2, SWATCH_SIZE + 4, SWATCH_SIZE + 4, 4, accent);
}
c.fill_rounded_rect(sx, y, SWATCH_SIZE, SWATCH_SIZE, 3, palette[i]);
// Thin border for light colors
c.rect(sx, y, SWATCH_SIZE, SWATCH_SIZE, Color::from_rgb(0xCC, 0xCC, 0xCC));
}
}
static void draw_radio(Canvas& c, int x, int y, bool selected, Color accent) {
int r = 7;
// Outer circle (simple square approximation with rounded rect)
c.fill_rounded_rect(x, y, r * 2, r * 2, r, Color::from_rgb(0xCC, 0xCC, 0xCC));
c.fill_rounded_rect(x + 1, y + 1, r * 2 - 2, r * 2 - 2, r - 1, colors::WHITE);
if (selected) {
c.fill_rounded_rect(x + 4, y + 4, r * 2 - 8, r * 2 - 8, r - 4, accent);
}
}
static void draw_toggle_btn(Canvas& c, int x, int y, int bw, int bh,
const char* label, bool active, Color accent) {
Color bg = active ? accent : colors::WINDOW_BG;
Color fg = active ? colors::WHITE : colors::TEXT_COLOR;
c.fill_rounded_rect(x, y, bw, bh, 4, bg);
if (!active) {
c.rect(x, y, bw, bh, colors::BORDER);
}
int tw = text_width(label);
int fh = system_font_height();
c.text(x + (bw - tw) / 2, y + (bh - fh) / 2, label, fg);
}
static void settings_draw_appearance(Canvas& c, SettingsState* st) {
DesktopSettings& s = st->desktop->settings;
Color accent = s.accent_color;
int x = 16;
int y = 12;
int sfh = system_font_height();
int line_h = sfh + 10;
// Section: Background
c.text(x, y, "Background", colors::TEXT_COLOR);
y += line_h;
// Radio buttons: Gradient / Solid
draw_radio(c, x, y, s.bg_gradient, accent);
c.text(x + 20, y + 2, "Gradient", colors::TEXT_COLOR);
draw_radio(c, x + 120, y, !s.bg_gradient, accent);
c.text(x + 140, y + 2, "Solid Color", colors::TEXT_COLOR);
y += line_h + 4;
if (s.bg_gradient) {
// Top color swatches
c.text(x, y + 4, "Top", Color::from_rgb(0x88, 0x88, 0x88));
draw_swatch_row(c, x + 70, y, bg_palette, s.bg_grad_top, accent);
y += SWATCH_SIZE + 14;
// Bottom color swatches
c.text(x, y + 4, "Bottom", Color::from_rgb(0x88, 0x88, 0x88));
draw_swatch_row(c, x + 70, y, bg_palette, s.bg_grad_bottom, accent);
y += SWATCH_SIZE + 14;
} else {
// Solid color swatches
c.text(x, y + 4, "Color", Color::from_rgb(0x88, 0x88, 0x88));
draw_swatch_row(c, x + 70, y, bg_palette, s.bg_solid, accent);
y += SWATCH_SIZE + 14;
}
// Separator
c.hline(x, y, c.w - 2 * x, colors::BORDER);
y += 12;
// Panel color
c.text(x, y + 4, "Panel Color", colors::TEXT_COLOR);
draw_swatch_row(c, x + 110, y, panel_palette, s.panel_color, accent);
y += SWATCH_SIZE + 14;
// Separator
c.hline(x, y, c.w - 2 * x, colors::BORDER);
y += 12;
// Accent color
c.text(x, y + 4, "Accent Color", colors::TEXT_COLOR);
draw_swatch_row(c, x + 110, y, accent_palette, s.accent_color, accent);
}
static void apply_ui_scale(int scale) {
switch (scale) {
case 0: fonts::UI_SIZE=14; fonts::TITLE_SIZE=14; fonts::TERM_SIZE=14; fonts::LARGE_SIZE=22; break;
case 2: fonts::UI_SIZE=22; fonts::TITLE_SIZE=22; fonts::TERM_SIZE=22; fonts::LARGE_SIZE=34; break;
default: fonts::UI_SIZE=18; fonts::TITLE_SIZE=18; fonts::TERM_SIZE=18; fonts::LARGE_SIZE=28; break;
}
}
static void settings_draw_display(Canvas& c, SettingsState* st) {
DesktopSettings& s = st->desktop->settings;
Color accent = s.accent_color;
int x = 16;
int y = 20;
int btn_w = 60;
int btn_h = 28;
// Window Shadows
c.text(x, y + 6, "Window Shadows", colors::TEXT_COLOR);
int bx = x + 180;
draw_toggle_btn(c, bx, y, btn_w, btn_h, "On", s.show_shadows, accent);
draw_toggle_btn(c, bx + btn_w + 8, y, btn_w, btn_h, "Off", !s.show_shadows, accent);
y += btn_h + 20;
// Separator
c.hline(x, y, c.w - 2 * x, colors::BORDER);
y += 16;
// Clock Format
c.text(x, y + 6, "Clock Format", colors::TEXT_COLOR);
bx = x + 180;
draw_toggle_btn(c, bx, y, btn_w, btn_h, "24h", s.clock_24h, accent);
draw_toggle_btn(c, bx + btn_w + 8, y, btn_w, btn_h, "12h", !s.clock_24h, accent);
y += btn_h + 20;
// Separator
c.hline(x, y, c.w - 2 * x, colors::BORDER);
y += 16;
// UI Scale
c.text(x, y + 6, "UI Scale", colors::TEXT_COLOR);
bx = x + 180;
int sbw = 68;
draw_toggle_btn(c, bx, y, sbw, btn_h, "Small", s.ui_scale == 0, accent);
draw_toggle_btn(c, bx + sbw + 8, y, sbw, btn_h, "Default", s.ui_scale == 1, accent);
draw_toggle_btn(c, bx + (sbw + 8) * 2, y, sbw, btn_h, "Large", s.ui_scale == 2, accent);
}
static void settings_draw_about(Canvas& c, SettingsState* st) {
st->uptime_ms = zenith::get_milliseconds();
Canvas c(win);
c.fill(colors::WINDOW_BG);
Color dim = Color::from_rgb(0x88, 0x88, 0x88);
int x = 16;
int y = 20;
char line[128];
int line_h = FONT_HEIGHT + 6;
int sfh = system_font_height();
int line_h = sfh + 6;
// OS name in 2x size
c.text_2x(x, y, st->sys_info.osName, colors::ACCENT);
y += FONT_HEIGHT * 2 + 8;
c.text_2x(x, y, st->sys_info.osName, st->desktop->settings.accent_color);
int large_h = (fonts::system_font && fonts::system_font->valid)
? fonts::system_font->get_line_height(fonts::LARGE_SIZE) : (FONT_HEIGHT * 2);
y += large_h + 8;
snprintf(line, sizeof(line), "Version %s", st->sys_info.osVersion);
c.text(x, y, line, colors::TEXT_COLOR);
@@ -41,27 +255,231 @@ static void about_on_draw(Window* win, Framebuffer& fb) {
c.hline(x, y, c.w - 2 * x, colors::BORDER);
y += 12;
snprintf(line, sizeof(line), "API version: %d", (int)st->sys_info.apiVersion);
snprintf(line, sizeof(line), "API version: %d", (int)st->sys_info.apiVersion);
c.kv_line(x, &y, line, colors::TEXT_COLOR, line_h);
int up_sec = (int)(st->uptime_ms / 1000);
int up_min = up_sec / 60;
int up_hr = up_min / 60;
snprintf(line, sizeof(line), "Uptime: %d:%02d:%02d", up_hr, up_min % 60, up_sec % 60);
snprintf(line, sizeof(line), "Uptime: %d:%02d:%02d", up_hr, up_min % 60, up_sec % 60);
c.kv_line(x, &y, line, colors::TEXT_COLOR, line_h);
snprintf(line, sizeof(line), "Build: %s %s", __DATE__, __TIME__);
snprintf(line, sizeof(line), "Build: %s %s", __DATE__, __TIME__);
c.text(x, y, line, colors::TEXT_COLOR);
y += line_h + 16;
c.hline(x, y, c.w - 2 * x, colors::BORDER);
y += 12;
c.kv_line(x, &y, "A hobby operating system built from scratch.", dim, line_h);
c.text(x, y, "Copyright (c) 2026 Daniel Hammer", dim);
}
static void about_on_close(Window* win) {
static void settings_on_draw(Window* win, Framebuffer& fb) {
SettingsState* st = (SettingsState*)win->app_data;
if (!st) return;
Canvas c(win);
c.fill(colors::WINDOW_BG);
Color accent = st->desktop->settings.accent_color;
int sfh = system_font_height();
// Draw tab bar background
c.fill_rect(0, 0, c.w, TAB_BAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5));
c.hline(0, TAB_BAR_H - 1, c.w, colors::BORDER);
// Draw tabs
int tab_w = c.w / TAB_COUNT;
for (int i = 0; i < TAB_COUNT; i++) {
int tx = i * tab_w;
bool active = (i == st->active_tab);
if (active) {
c.fill_rect(tx, 0, tab_w, TAB_BAR_H, colors::WINDOW_BG);
// Active tab underline
c.fill_rect(tx + 4, TAB_BAR_H - 3, tab_w - 8, 3, accent);
}
int tw = text_width(tab_labels[i]);
Color tc = active ? accent : Color::from_rgb(0x66, 0x66, 0x66);
c.text(tx + (tab_w - tw) / 2, (TAB_BAR_H - sfh) / 2, tab_labels[i], tc);
}
// Draw tab content below the tab bar
// Create a sub-canvas offset by TAB_BAR_H
Canvas content(win->content + TAB_BAR_H * win->content_w,
win->content_w, win->content_h - TAB_BAR_H);
switch (st->active_tab) {
case 0: settings_draw_appearance(content, st); break;
case 1: settings_draw_display(content, st); break;
case 2: settings_draw_about(content, st); break;
}
}
// ============================================================================
// Mouse interaction
// ============================================================================
static bool swatch_hit(int mx, int my, int row_x, int row_y, int* out_idx) {
for (int i = 0; i < SWATCH_COUNT; i++) {
int sx = row_x + i * (SWATCH_SIZE + SWATCH_GAP);
if (mx >= sx && mx < sx + SWATCH_SIZE && my >= row_y && my < row_y + SWATCH_SIZE) {
*out_idx = i;
return true;
}
}
return false;
}
static void settings_on_mouse(Window* win, MouseEvent& ev) {
SettingsState* st = (SettingsState*)win->app_data;
if (!st) return;
if (!ev.left_pressed()) return;
Rect cr = win->content_rect();
int mx = ev.x - cr.x;
int my = ev.y - cr.y;
// Tab bar click
if (my >= 0 && my < TAB_BAR_H) {
int tab_w = win->content_w / TAB_COUNT;
int tab = mx / tab_w;
if (tab >= 0 && tab < TAB_COUNT) {
st->active_tab = tab;
}
return;
}
// Content area (offset by TAB_BAR_H)
int cy = my - TAB_BAR_H;
DesktopSettings& s = st->desktop->settings;
if (st->active_tab == 0) {
// Appearance tab
int x = 16;
int sfh = system_font_height();
int line_h = sfh + 10;
int y = 12;
// "Background" label
y += line_h;
// Radio: Gradient
if (mx >= x && mx < x + 100 && cy >= y && cy < y + 16) {
s.bg_gradient = true;
return;
}
// Radio: Solid
if (mx >= x + 120 && mx < x + 260 && cy >= y && cy < y + 16) {
s.bg_gradient = false;
return;
}
y += line_h + 4;
int idx;
if (s.bg_gradient) {
// Top swatches
if (swatch_hit(mx, cy, x + 70, y, &idx)) {
s.bg_grad_top = bg_palette[idx];
return;
}
y += SWATCH_SIZE + 14;
// Bottom swatches
if (swatch_hit(mx, cy, x + 70, y, &idx)) {
s.bg_grad_bottom = bg_palette[idx];
return;
}
y += SWATCH_SIZE + 14;
} else {
// Solid color swatches
if (swatch_hit(mx, cy, x + 70, y, &idx)) {
s.bg_solid = bg_palette[idx];
return;
}
y += SWATCH_SIZE + 14;
}
// Separator
y += 12;
// Panel color swatches
if (swatch_hit(mx, cy, x + 110, y, &idx)) {
s.panel_color = panel_palette[idx];
return;
}
y += SWATCH_SIZE + 14;
// Separator
y += 12;
// Accent color swatches
if (swatch_hit(mx, cy, x + 110, y, &idx)) {
s.accent_color = accent_palette[idx];
return;
}
} else if (st->active_tab == 1) {
// Display tab
int x = 16;
int y = 20;
int btn_w = 60;
int btn_h = 28;
int bx = x + 180;
// Window Shadows: On
if (mx >= bx && mx < bx + btn_w && cy >= y && cy < y + btn_h) {
s.show_shadows = true;
return;
}
// Window Shadows: Off
if (mx >= bx + btn_w + 8 && mx < bx + btn_w * 2 + 8 && cy >= y && cy < y + btn_h) {
s.show_shadows = false;
return;
}
y += btn_h + 20 + 16;
// Clock: 24h
if (mx >= bx && mx < bx + btn_w && cy >= y && cy < y + btn_h) {
s.clock_24h = true;
return;
}
// Clock: 12h
if (mx >= bx + btn_w + 8 && mx < bx + btn_w * 2 + 8 && cy >= y && cy < y + btn_h) {
s.clock_24h = false;
return;
}
y += btn_h + 20 + 16;
// UI Scale buttons
int sbw = 68;
// Small
if (mx >= bx && mx < bx + sbw && cy >= y && cy < y + btn_h) {
s.ui_scale = 0;
apply_ui_scale(0);
return;
}
// Default
if (mx >= bx + sbw + 8 && mx < bx + sbw * 2 + 8 && cy >= y && cy < y + btn_h) {
s.ui_scale = 1;
apply_ui_scale(1);
return;
}
// Large
if (mx >= bx + (sbw + 8) * 2 && mx < bx + sbw * 3 + 16 && cy >= y && cy < y + btn_h) {
s.ui_scale = 2;
apply_ui_scale(2);
return;
}
}
}
// ============================================================================
// Cleanup
// ============================================================================
static void settings_on_close(Window* win) {
if (win->app_data) {
zenith::mfree(win->app_data);
win->app_data = nullptr;
@@ -69,20 +487,23 @@ static void about_on_close(Window* win) {
}
// ============================================================================
// About launcher
// Settings launcher
// ============================================================================
void open_settings(DesktopState* ds) {
int idx = desktop_create_window(ds, "About", 280, 150, 380, 280);
int idx = desktop_create_window(ds, "Settings", 200, 100, 480, 420);
if (idx < 0) return;
Window* win = &ds->windows[idx];
AboutState* st = (AboutState*)zenith::malloc(sizeof(AboutState));
zenith::memset(st, 0, sizeof(AboutState));
SettingsState* st = (SettingsState*)zenith::malloc(sizeof(SettingsState));
zenith::memset(st, 0, sizeof(SettingsState));
st->desktop = ds;
st->active_tab = 0;
zenith::get_info(&st->sys_info);
st->uptime_ms = zenith::get_milliseconds();
win->app_data = st;
win->on_draw = about_on_draw;
win->on_close = about_on_close;
win->on_draw = settings_on_draw;
win->on_mouse = settings_on_mouse;
win->on_close = settings_on_close;
}
+4 -4
View File
@@ -31,7 +31,7 @@ static void sysinfo_on_draw(Window* win, Framebuffer& fb) {
// Title
c.text(x, y, "System Information", colors::ACCENT);
y += FONT_HEIGHT + 12;
y += system_font_height() + 12;
// Separator
c.hline(x, y, c.w - 2 * x, colors::BORDER);
@@ -52,7 +52,7 @@ static void sysinfo_on_draw(Window* win, Framebuffer& fb) {
// Max Processes
snprintf(line, sizeof(line), "Max PIDs: %d", (int)si->sys_info.maxProcesses);
c.text(x, y, line, colors::TEXT_COLOR);
y += FONT_HEIGHT + 12;
y += system_font_height() + 12;
// Uptime
int up_sec = (int)(si->uptime_ms / 1000);
@@ -60,11 +60,11 @@ static void sysinfo_on_draw(Window* win, Framebuffer& fb) {
int up_hr = up_min / 60;
snprintf(line, sizeof(line), "Uptime: %d:%02d:%02d", up_hr, up_min % 60, up_sec % 60);
c.text(x, y, line, colors::TEXT_COLOR);
y += FONT_HEIGHT + 12;
y += system_font_height() + 12;
// Network section
c.text(x, y, "Network", colors::ACCENT);
y += FONT_HEIGHT + 8;
y += system_font_height() + 8;
c.hline(x, y, c.w - 2 * x, colors::BORDER);
y += 8;
+10 -2
View File
@@ -15,6 +15,14 @@ static void terminal_on_draw(Window* win, Framebuffer& fb) {
if (!ts) return;
Rect cr = win->content_rect();
// Check if window was resized and update terminal grid
int new_cols = cr.w / mono_cell_width();
int new_rows = cr.h / mono_cell_height();
if (new_cols != ts->cols || new_rows != ts->rows) {
terminal_resize(ts, new_cols, new_rows);
}
terminal_render(ts, win->content, cr.w, cr.h);
}
@@ -52,8 +60,8 @@ void open_terminal(DesktopState* ds) {
Window* win = &ds->windows[idx];
Rect cr = win->content_rect();
int cols = cr.w / FONT_WIDTH;
int rows = cr.h / FONT_HEIGHT;
int cols = cr.w / mono_cell_width();
int rows = cr.h / mono_cell_height();
TerminalState* ts = (TerminalState*)zenith::malloc(sizeof(TerminalState));
zenith::memset(ts, 0, sizeof(TerminalState));
+252 -45
View File
@@ -11,6 +11,8 @@
// Text Editor state
// ============================================================================
static constexpr int TE_TOOLBAR_H = 36;
static constexpr int TE_PATHBAR_H = 32;
static constexpr int TE_STATUS_H = 24;
static constexpr int TE_LINE_NUM_W = 48;
static constexpr int TE_INIT_CAP = 4096;
@@ -33,6 +35,11 @@ struct TextEditorState {
char filepath[256];
char filename[64];
DesktopState* desktop;
bool show_pathbar;
char pathbar_text[256];
int pathbar_cursor;
int pathbar_len;
};
// ============================================================================
@@ -203,7 +210,7 @@ static void te_move_end(TextEditorState* te) {
// Scrolling
// ============================================================================
static void te_ensure_cursor_visible(TextEditorState* te, int visible_lines) {
static void te_ensure_cursor_visible(TextEditorState* te, int visible_lines, int content_w) {
if (te->cursor_line < te->scroll_y) {
te->scroll_y = te->cursor_line;
}
@@ -212,13 +219,14 @@ static void te_ensure_cursor_visible(TextEditorState* te, int visible_lines) {
}
// Horizontal scroll
int cursor_px = te->cursor_col * FONT_WIDTH;
int view_w = 580 - TE_LINE_NUM_W; // approximate
if (cursor_px - te->scroll_x > view_w - FONT_WIDTH * 2) {
te->scroll_x = cursor_px - view_w + FONT_WIDTH * 4;
int cell_w = mono_cell_width();
int cursor_px = te->cursor_col * cell_w;
int view_w = content_w - TE_LINE_NUM_W;
if (cursor_px - te->scroll_x > view_w - cell_w * 2) {
te->scroll_x = cursor_px - view_w + cell_w * 4;
}
if (cursor_px < te->scroll_x) {
te->scroll_x = cursor_px - FONT_WIDTH * 2;
te->scroll_x = cursor_px - cell_w * 2;
if (te->scroll_x < 0) te->scroll_x = 0;
}
}
@@ -290,21 +298,96 @@ static void texteditor_on_draw(Window* win, Framebuffer& fb) {
Canvas c(win);
c.fill(colors::WINDOW_BG);
int text_area_h = c.h - TE_STATUS_H;
int visible_lines = text_area_h / FONT_HEIGHT;
int cell_w = mono_cell_width();
int cell_h = mono_cell_height();
te_ensure_cursor_visible(te, visible_lines);
// ---- Toolbar (36px) ----
c.fill_rect(0, 0, c.w, TE_TOOLBAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5));
// Open button
Color btn_bg = Color::from_rgb(0xE8, 0xE8, 0xE8);
c.fill_rounded_rect(4, 6, 24, 24, 3, btn_bg);
if (te->desktop && te->desktop->icon_folder.pixels)
c.icon(8, 10, te->desktop->icon_folder);
// Save button
c.fill_rounded_rect(32, 6, 24, 24, 3, btn_bg);
if (te->desktop && te->desktop->icon_save.pixels)
c.icon(36, 10, te->desktop->icon_save);
// Vertical separator
c.vline(60, 4, 28, colors::BORDER);
// Filename + modified flag
char toolbar_label[128];
if (te->filename[0]) {
snprintf(toolbar_label, 128, "%s%s", te->filename, te->modified ? " [modified]" : "");
} else {
snprintf(toolbar_label, 128, "Untitled%s", te->modified ? " [modified]" : "");
}
int sfh = system_font_height();
c.text(68, (TE_TOOLBAR_H - sfh) / 2, toolbar_label, colors::TEXT_COLOR);
// Toolbar bottom separator
c.hline(0, TE_TOOLBAR_H - 1, c.w, colors::BORDER);
// ---- Path bar (conditional, 32px) ----
int editor_y_start = TE_TOOLBAR_H;
if (te->show_pathbar) {
int pb_y = TE_TOOLBAR_H;
c.fill_rect(0, pb_y, c.w, TE_PATHBAR_H, Color::from_rgb(0xF0, 0xF0, 0xF0));
// Text input box
int inp_x = 8;
int inp_y = pb_y + 4;
int btn_w = 56;
int inp_w = c.w - inp_x - btn_w - 12;
int inp_h = 24;
c.fill_rect(inp_x, inp_y, inp_w, inp_h, colors::WHITE);
c.rect(inp_x, inp_y, inp_w, inp_h, colors::ACCENT);
// Path text
int text_y = inp_y + (inp_h - sfh) / 2;
c.text(inp_x + 4, text_y, te->pathbar_text, colors::TEXT_COLOR);
// Blinking cursor in path input
char prefix[256];
int plen = te->pathbar_cursor;
if (plen > 255) plen = 255;
for (int i = 0; i < plen; i++) prefix[i] = te->pathbar_text[i];
prefix[plen] = '\0';
int cx = inp_x + 4 + text_width(prefix);
c.fill_rect(cx, inp_y + 3, 2, inp_h - 6, colors::ACCENT);
// Open button
int ob_x = inp_x + inp_w + 6;
c.button(ob_x, inp_y, btn_w, inp_h, "Open", colors::ACCENT, colors::WHITE, 3);
// Path bar bottom separator
c.hline(0, pb_y + TE_PATHBAR_H - 1, c.w, colors::BORDER);
editor_y_start = TE_TOOLBAR_H + TE_PATHBAR_H;
}
// ---- Editor area ----
int text_area_h = c.h - editor_y_start - TE_STATUS_H;
int visible_lines = text_area_h / cell_h;
if (visible_lines < 1) visible_lines = 1;
te_ensure_cursor_visible(te, visible_lines, c.w);
// Line number gutter background
c.fill_rect(0, 0, TE_LINE_NUM_W, text_area_h, Color::from_rgb(0xF0, 0xF0, 0xF0));
c.fill_rect(0, editor_y_start, TE_LINE_NUM_W, text_area_h, Color::from_rgb(0xF0, 0xF0, 0xF0));
// Gutter separator
c.vline(TE_LINE_NUM_W, 0, text_area_h, colors::BORDER);
c.vline(TE_LINE_NUM_W, editor_y_start, text_area_h, colors::BORDER);
// Draw lines
Color linenum_color = Color::from_rgb(0x99, 0x99, 0x99);
Color cursor_line_color = Color::from_rgb(0xFF, 0xFD, 0xE8);
uint32_t text_px = colors::TEXT_COLOR.to_pixel();
Color text_color = colors::TEXT_COLOR;
int text_start_x = TE_LINE_NUM_W + 4;
@@ -312,12 +395,12 @@ static void texteditor_on_draw(Window* win, Framebuffer& fb) {
int line = te->scroll_y + vis;
if (line >= te->line_count) break;
int py = vis * FONT_HEIGHT;
if (py >= text_area_h) break;
int py = editor_y_start + vis * cell_h;
if (py >= editor_y_start + text_area_h) break;
// Cursor line highlighting
if (line == te->cursor_line) {
int hl_h = gui_min(FONT_HEIGHT, text_area_h - py);
int hl_h = gui_min(cell_h, editor_y_start + text_area_h - py);
if (hl_h > 0)
c.fill_rect(TE_LINE_NUM_W + 1, py, c.w - TE_LINE_NUM_W - 1, hl_h, cursor_line_color);
}
@@ -325,28 +408,37 @@ static void texteditor_on_draw(Window* win, Framebuffer& fb) {
// Line number
char num_str[8];
snprintf(num_str, 8, "%4d", line + 1);
c.text(4, py, num_str, linenum_color);
c.text_mono(4, py, num_str, linenum_color);
// Line text (per-character rendering with horizontal scroll clipping)
int line_start = te->line_offsets[line];
int line_len = te_line_length(te, line);
for (int ci = 0; ci < line_len; ci++) {
int px = text_start_x + ci * FONT_WIDTH - te->scroll_x;
if (px + FONT_WIDTH <= TE_LINE_NUM_W + 1) continue;
int px = text_start_x + ci * cell_w - te->scroll_x;
if (px + cell_w <= TE_LINE_NUM_W + 1) continue;
if (px >= c.w) break;
char ch = te->buffer[line_start + ci];
if (ch >= 32 || ch < 0) {
const uint8_t* glyph = &font_data[(unsigned char)ch * FONT_HEIGHT];
for (int fy = 0; fy < FONT_HEIGHT && py + fy < text_area_h; fy++) {
uint8_t bits = glyph[fy];
for (int fx = 0; fx < FONT_WIDTH; fx++) {
if (bits & (0x80 >> fx)) {
int dx = px + fx;
int dy = py + fy;
if (dx > TE_LINE_NUM_W && dx < c.w && dy >= 0 && dy < text_area_h)
c.pixels[dy * c.w + dx] = text_px;
if (fonts::mono && fonts::mono->valid) {
GlyphCache* gc = fonts::mono->get_cache(fonts::TERM_SIZE);
fonts::mono->draw_char_to_buffer(
c.pixels, c.w, c.h, px, py + gc->ascent,
ch, text_color, gc);
} else {
// bitmap fallback
uint32_t text_px = text_color.to_pixel();
const uint8_t* glyph = &font_data[(unsigned char)ch * FONT_HEIGHT];
for (int fy = 0; fy < FONT_HEIGHT && py + fy < editor_y_start + text_area_h; fy++) {
uint8_t bits = glyph[fy];
for (int fx = 0; fx < FONT_WIDTH; fx++) {
if (bits & (0x80 >> fx)) {
int dx = px + fx;
int dy = py + fy;
if (dx > TE_LINE_NUM_W && dx < c.w && dy >= 0 && dy < c.h)
c.pixels[dy * c.w + dx] = text_px;
}
}
}
}
@@ -355,9 +447,9 @@ static void texteditor_on_draw(Window* win, Framebuffer& fb) {
// Draw cursor
if (line == te->cursor_line) {
int cx = text_start_x + te->cursor_col * FONT_WIDTH - te->scroll_x;
int cx = text_start_x + te->cursor_col * cell_w - te->scroll_x;
if (cx > TE_LINE_NUM_W && cx + 2 <= c.w) {
int cur_h = gui_min(FONT_HEIGHT, text_area_h - py);
int cur_h = gui_min(cell_h, editor_y_start + text_area_h - py);
if (cur_h > 0)
c.fill_rect(cx, py, 2, cur_h, colors::ACCENT);
}
@@ -368,20 +460,21 @@ static void texteditor_on_draw(Window* win, Framebuffer& fb) {
int status_y = c.h - TE_STATUS_H;
c.fill_rect(0, status_y, c.w, TE_STATUS_H, Color::from_rgb(0x2B, 0x3E, 0x50));
// Filename + modified flag
// Cursor position (right side)
char status_right[32];
snprintf(status_right, 32, "Ln %d, Col %d ", te->cursor_line + 1, te->cursor_col + 1);
int sr_w = text_width(status_right);
int status_text_y = status_y + (TE_STATUS_H - sfh) / 2;
c.text(c.w - sr_w - 4, status_text_y, status_right, colors::PANEL_TEXT);
// Filename + modified flag (left side)
char status_left[128];
if (te->filename[0]) {
snprintf(status_left, 128, " %s%s", te->filename, te->modified ? " [modified]" : "");
} else {
snprintf(status_left, 128, " Untitled%s", te->modified ? " [modified]" : "");
}
c.text(4, status_y + (TE_STATUS_H - FONT_HEIGHT) / 2, status_left, colors::PANEL_TEXT);
// Cursor position (right side)
char status_right[32];
snprintf(status_right, 32, "Ln %d, Col %d ", te->cursor_line + 1, te->cursor_col + 1);
int sr_w = zenith::slen(status_right) * FONT_WIDTH;
c.text(c.w - sr_w - 4, status_y + (TE_STATUS_H - FONT_HEIGHT) / 2, status_right, colors::PANEL_TEXT);
c.text(4, status_text_y, status_left, colors::PANEL_TEXT);
}
// ============================================================================
@@ -395,15 +488,62 @@ static void texteditor_on_mouse(Window* win, MouseEvent& ev) {
Rect cr = win->content_rect();
int local_x = ev.x - cr.x;
int local_y = ev.y - cr.y;
int text_area_h = cr.h - TE_STATUS_H;
if (ev.left_pressed() && local_y < text_area_h && local_x > TE_LINE_NUM_W) {
// Click to position cursor
int clicked_line = te->scroll_y + local_y / FONT_HEIGHT;
int cell_w = mono_cell_width();
int cell_h = mono_cell_height();
int editor_y_start = TE_TOOLBAR_H + (te->show_pathbar ? TE_PATHBAR_H : 0);
int text_area_h = cr.h - editor_y_start - TE_STATUS_H;
// ---- Toolbar clicks ----
if (ev.left_pressed() && local_y < TE_TOOLBAR_H) {
// Open button
if (local_x >= 4 && local_x < 28 && local_y >= 6 && local_y < 30) {
te->show_pathbar = !te->show_pathbar;
if (te->show_pathbar) {
// Pre-fill with current filepath
zenith::strncpy(te->pathbar_text, te->filepath, 255);
te->pathbar_len = zenith::slen(te->pathbar_text);
te->pathbar_cursor = te->pathbar_len;
}
return;
}
// Save button
if (local_x >= 32 && local_x < 56 && local_y >= 6 && local_y < 30) {
te_save_file(te);
return;
}
return;
}
// ---- Path bar clicks ----
if (te->show_pathbar && local_y >= TE_TOOLBAR_H && local_y < TE_TOOLBAR_H + TE_PATHBAR_H) {
if (ev.left_pressed()) {
// Check "Open" button region
int btn_w = 56;
int inp_w = cr.w - 8 - btn_w - 12;
int ob_x = 8 + inp_w + 6;
if (local_x >= ob_x && local_x < ob_x + btn_w) {
if (te->pathbar_text[0]) {
te_load_file(te, te->pathbar_text);
// Update window title
char title[64];
snprintf(title, 64, "%s - Editor", te->filename);
zenith::strncpy(win->title, title, 63);
te->show_pathbar = false;
}
}
}
return;
}
// ---- Editor area clicks ----
if (ev.left_pressed() && local_y >= editor_y_start && local_y < editor_y_start + text_area_h && local_x > TE_LINE_NUM_W) {
int clicked_line = te->scroll_y + (local_y - editor_y_start) / cell_h;
if (clicked_line >= te->line_count) clicked_line = te->line_count - 1;
if (clicked_line < 0) clicked_line = 0;
int clicked_col = (local_x - TE_LINE_NUM_W - 4 + te->scroll_x + FONT_WIDTH / 2) / FONT_WIDTH;
int clicked_col = (local_x - TE_LINE_NUM_W - 4 + te->scroll_x + cell_w / 2) / cell_w;
if (clicked_col < 0) clicked_col = 0;
int line_len = te_line_length(te, clicked_line);
if (clicked_col > line_len) clicked_col = line_len;
@@ -412,11 +552,11 @@ static void texteditor_on_mouse(Window* win, MouseEvent& ev) {
te_update_cursor_pos(te);
}
// Scroll
if (ev.scroll != 0 && local_y < text_area_h) {
// ---- Scroll ----
if (ev.scroll != 0 && local_y >= editor_y_start && local_y < editor_y_start + text_area_h) {
te->scroll_y -= ev.scroll * 3;
if (te->scroll_y < 0) te->scroll_y = 0;
int max_scroll = te->line_count - (text_area_h / FONT_HEIGHT) + 1;
int max_scroll = te->line_count - (text_area_h / cell_h) + 1;
if (max_scroll < 0) max_scroll = 0;
if (te->scroll_y > max_scroll) te->scroll_y = max_scroll;
}
@@ -430,12 +570,71 @@ static void texteditor_on_key(Window* win, const Zenith::KeyEvent& key) {
TextEditorState* te = (TextEditorState*)win->app_data;
if (!te || !key.pressed) return;
// ---- Path bar input mode ----
if (te->show_pathbar) {
if (key.ascii == '\n' || key.ascii == '\r') {
if (te->pathbar_text[0]) {
te_load_file(te, te->pathbar_text);
char title[64];
snprintf(title, 64, "%s - Editor", te->filename);
zenith::strncpy(win->title, title, 63);
te->show_pathbar = false;
}
return;
}
if (key.scancode == 0x01) { // Escape
te->show_pathbar = false;
return;
}
if (key.ascii == '\b' || key.scancode == 0x0E) {
if (te->pathbar_cursor > 0) {
for (int i = te->pathbar_cursor - 1; i < te->pathbar_len - 1; i++)
te->pathbar_text[i] = te->pathbar_text[i + 1];
te->pathbar_len--;
te->pathbar_cursor--;
te->pathbar_text[te->pathbar_len] = '\0';
}
return;
}
if (key.scancode == 0x4B) { // Left
if (te->pathbar_cursor > 0) te->pathbar_cursor--;
return;
}
if (key.scancode == 0x4D) { // Right
if (te->pathbar_cursor < te->pathbar_len) te->pathbar_cursor++;
return;
}
if (key.ascii >= 32 && key.ascii < 127 && te->pathbar_len < 254) {
for (int i = te->pathbar_len; i > te->pathbar_cursor; i--)
te->pathbar_text[i] = te->pathbar_text[i - 1];
te->pathbar_text[te->pathbar_cursor] = key.ascii;
te->pathbar_cursor++;
te->pathbar_len++;
te->pathbar_text[te->pathbar_len] = '\0';
return;
}
return;
}
// ---- Normal editor mode ----
// Ctrl+S: save
if (key.ctrl && (key.ascii == 's' || key.ascii == 'S')) {
te_save_file(te);
return;
}
// Ctrl+O: open
if (key.ctrl && (key.ascii == 'o' || key.ascii == 'O')) {
te->show_pathbar = !te->show_pathbar;
if (te->show_pathbar) {
zenith::strncpy(te->pathbar_text, te->filepath, 255);
te->pathbar_len = zenith::slen(te->pathbar_text);
te->pathbar_cursor = te->pathbar_len;
}
return;
}
// Arrow keys
if (key.scancode == 0x48) { te_move_up(te); return; }
if (key.scancode == 0x50) { te_move_down(te); return; }
@@ -503,6 +702,10 @@ void open_texteditor(DesktopState* ds) {
te->buf_len = 0;
te->modified = false;
te->desktop = ds;
te->show_pathbar = false;
te->pathbar_text[0] = '\0';
te->pathbar_cursor = 0;
te->pathbar_len = 0;
te_recompute_lines(te);
te_update_cursor_pos(te);
@@ -536,6 +739,10 @@ void open_texteditor_with_file(DesktopState* ds, const char* path) {
te->buf_len = 0;
te->modified = false;
te->desktop = ds;
te->show_pathbar = false;
te->pathbar_text[0] = '\0';
te->pathbar_cursor = 0;
te->pathbar_len = 0;
// Initialize line index for empty document first (ensures line_offsets
// is non-null even if te_load_file fails to open the file)
+9 -7
View File
@@ -168,7 +168,8 @@ static void wiki_build_display(WikiState* ws, const char* title,
ws->lineCount = 0;
ws->scrollY = 0;
int maxChars = (contentW - 24 - WIKI_SCROLLBAR_W) / FONT_WIDTH;
int char_w = mono_cell_width();
int maxChars = (contentW - 24 - WIKI_SCROLLBAR_W) / char_w;
if (maxChars < 20) maxChars = 20;
Color accent_c = colors::ACCENT;
@@ -301,7 +302,7 @@ static void wiki_on_draw(Window* win, Framebuffer& fb) {
c.rect(tb_x, tb_y, tb_w, tb_h, colors::BORDER);
// Search text
c.text(tb_x + 4, tb_y + (tb_h - FONT_HEIGHT) / 2, ws->searchQuery, colors::TEXT_COLOR);
c.text(tb_x + 4, tb_y + (tb_h - system_font_height()) / 2, ws->searchQuery, colors::TEXT_COLOR);
// ---- Search button ----
int btn_x = tb_x + tb_w + 6;
@@ -313,7 +314,8 @@ static void wiki_on_draw(Window* win, Framebuffer& fb) {
// ---- Content area ----
int content_y = WIKI_TOOLBAR_H + 1;
int content_h = c.h - content_y;
int visibleLines = content_h / (FONT_HEIGHT + 4);
int wiki_sfh = system_font_height();
int visibleLines = content_h / (wiki_sfh + 4);
if (visibleLines < 1) visibleLines = 1;
if (ws->mode == WikiState::FETCHING) {
@@ -326,8 +328,8 @@ static void wiki_on_draw(Window* win, Framebuffer& fb) {
Color::from_rgb(0x88, 0x88, 0x88));
} else if (ws->mode == WikiState::DONE && ws->lineCount > 0) {
int y = content_y + 8;
int lineH = FONT_HEIGHT + 4;
for (int i = ws->scrollY; i < ws->lineCount && y + FONT_HEIGHT < c.h; i++) {
int lineH = wiki_sfh + 4;
for (int i = ws->scrollY; i < ws->lineCount && y + wiki_sfh < c.h; i++) {
WikiDisplayLine* dl = &ws->lines[i];
if (dl->text[0] != '\0') {
c.text(12, y, dl->text, dl->color);
@@ -412,7 +414,7 @@ static void wiki_on_mouse(Window* win, MouseEvent& ev) {
if (ev.scroll != 0 && ws->mode == WikiState::DONE && ws->lineCount > 0) {
int ch = cr.h;
int content_h = ch - WIKI_TOOLBAR_H - 1;
int visibleLines = content_h / (FONT_HEIGHT + 4);
int visibleLines = content_h / (system_font_height() + 4);
int maxScroll = ws->lineCount - visibleLines;
if (maxScroll < 0) maxScroll = 0;
@@ -443,7 +445,7 @@ static void wiki_on_key(Window* win, const Zenith::KeyEvent& key) {
}
}
int content_h = cr.h - WIKI_TOOLBAR_H - 1;
int visibleLines = content_h / (FONT_HEIGHT + 4);
int visibleLines = content_h / (system_font_height() + 4);
if (visibleLines < 1) visibleLines = 1;
int maxScroll = ws->lineCount - visibleLines;
if (maxScroll < 0) maxScroll = 0;
+5
View File
@@ -167,5 +167,10 @@ void open_texteditor(DesktopState* ds);
void open_texteditor_with_file(DesktopState* ds, const char* path);
void open_klog(DesktopState* ds);
void open_wiki(DesktopState* ds);
void open_procmgr(DesktopState* ds);
void open_mandelbrot(DesktopState* ds);
void open_devexplorer(DesktopState* ds);
void open_settings(DesktopState* ds);
void open_doom(DesktopState* ds);
void open_reboot_dialog(DesktopState* ds);
void desktop_poll_external_windows(DesktopState* ds);
+281 -69
View File
@@ -23,6 +23,9 @@ void gui::desktop_init(DesktopState* ds) {
ds->fb.clear(colors::DESKTOP_BG);
ds->fb.flip();
// Load TrueType fonts
fonts::init();
ds->window_count = 0;
ds->focused_window = -1;
ds->prev_buttons = 0;
@@ -58,6 +61,22 @@ void gui::desktop_init(DesktopState* ds) {
ds->icon_settings = svg_load("0:/icons/help-about.svg", 20, 20, defColor);
ds->icon_reboot = svg_load("0:/icons/system-reboot.svg", 20, 20, defColor);
ds->icon_doom = svg_load("0:/icons/doom.svg", 20, 20, defColor);
ds->icon_procmgr = svg_load("0:/icons/system-monitor.svg", 20, 20, defColor);
ds->icon_mandelbrot = svg_load("0:/icons/applications-science.svg", 20, 20, defColor);
ds->icon_devexplorer = svg_load("0:/icons/hardware.svg", 20, 20, defColor);
// Settings defaults
ds->settings.bg_gradient = true;
ds->settings.bg_grad_top = Color::from_rgb(0xD0, 0xD8, 0xE8);
ds->settings.bg_grad_bottom = Color::from_rgb(0xA0, 0xA8, 0xB8);
ds->settings.bg_solid = Color::from_rgb(0xD0, 0xD8, 0xE8);
ds->settings.panel_color = colors::PANEL_BG;
ds->settings.accent_color = colors::ACCENT;
ds->settings.show_shadows = true;
ds->settings.clock_24h = true;
ds->settings.ui_scale = 1;
ds->ctx_menu_open = false;
ds->ctx_menu_x = 0;
ds->ctx_menu_y = 0;
@@ -100,6 +119,8 @@ int gui::desktop_create_window(DesktopState* ds, const char* title, int x, int y
win->on_close = nullptr;
win->on_poll = nullptr;
win->app_data = nullptr;
win->external = false;
win->ext_win_id = -1;
// Unfocus previous window
if (ds->focused_window >= 0 && ds->focused_window < ds->window_count) {
@@ -115,10 +136,19 @@ void gui::desktop_close_window(DesktopState* ds, int idx) {
if (idx < 0 || idx >= ds->window_count) return;
Window* win = &ds->windows[idx];
// For external windows, send a close event instead of freeing the buffer
if (win->external) {
Zenith::WinEvent ev;
zenith::memset(&ev, 0, sizeof(ev));
ev.type = 3; // close
zenith::win_sendevent(win->ext_win_id, &ev);
}
if (win->on_close) win->on_close(win);
// Free content buffer
if (win->content) {
// Free content buffer (skip for external windows — shared memory)
if (win->content && !win->external) {
zenith::free(win->content);
win->content = nullptr;
}
@@ -180,7 +210,8 @@ void gui::desktop_draw_window(DesktopState* ds, int idx) {
int h = win->frame.h;
// Draw shadow
draw_shadow(fb, x, y, w, h, SHADOW_SIZE, colors::SHADOW);
if (ds->settings.show_shadows)
draw_shadow(fb, x, y, w, h, SHADOW_SIZE, colors::SHADOW);
// Draw window body
fb.fill_rect(x, y, w, h, colors::WINDOW_BG);
@@ -205,7 +236,7 @@ void gui::desktop_draw_window(DesktopState* ds, int idx) {
// 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_y = y + (TITLEBAR_HEIGHT - system_font_height()) / 2;
int title_w = text_width(win->title);
// Center in remaining space
int remaining_w = w - (title_x - x) - 12;
@@ -222,9 +253,31 @@ void gui::desktop_draw_window(DesktopState* ds, int idx) {
// Blit content buffer to framebuffer (clip to actual buffer size during resize)
Rect cr = win->content_rect();
if (win->content) {
int blit_w = cr.w < win->content_w ? cr.w : win->content_w;
int blit_h = cr.h < win->content_h ? cr.h : win->content_h;
fb.blit(cr.x, cr.y, blit_w, blit_h, win->content);
if (win->external && (cr.w != win->content_w || cr.h != win->content_h)) {
// Nearest-neighbor scale for external windows (fixed-size shared buffer)
int src_w = win->content_w;
int src_h = win->content_h;
int dst_w = cr.w;
int dst_h = cr.h;
uint32_t* buf = fb.buffer();
int pitch = fb.pitch();
for (int y = 0; y < dst_h; y++) {
int dy = cr.y + y;
if (dy < 0 || dy >= fb.height()) continue;
int sy = y * src_h / dst_h;
uint32_t* dst_row = (uint32_t*)((uint8_t*)buf + dy * pitch);
uint32_t* src_row = win->content + sy * src_w;
for (int x = 0; x < dst_w; x++) {
int dx = cr.x + x;
if (dx < 0 || dx >= fb.width()) continue;
dst_row[dx] = src_row[x * src_w / dst_w];
}
}
} else {
int blit_w = cr.w < win->content_w ? cr.w : win->content_w;
int blit_h = cr.h < win->content_h ? cr.h : win->content_h;
fb.blit(cr.x, cr.y, blit_w, blit_h, win->content);
}
}
}
@@ -233,11 +286,12 @@ void gui::desktop_draw_panel(DesktopState* ds) {
int sw = ds->screen_w;
// Panel gradient background (slightly lighter at top)
Color pc = ds->settings.panel_color;
for (int y = 0; y < PANEL_HEIGHT; y++) {
int t = y * 255 / PANEL_HEIGHT;
uint8_t r = colors::PANEL_BG.r + (10 - t * 10 / 255);
uint8_t g = colors::PANEL_BG.g + (10 - t * 10 / 255);
uint8_t b = colors::PANEL_BG.b + (10 - t * 10 / 255);
uint8_t r = pc.r + (10 - t * 10 / 255);
uint8_t g = pc.g + (10 - t * 10 / 255);
uint8_t b = pc.b + (10 - t * 10 / 255);
fb.fill_rect(0, y, sw, 1, Color::from_rgb(r, g, b));
}
@@ -284,7 +338,7 @@ void gui::desktop_draw_panel(DesktopState* ds) {
// Active window accent underline bar
if (i == ds->focused_window) {
fb.fill_rect(indicator_x + 4, 26, iw - 8, 2, colors::ACCENT);
fb.fill_rect(indicator_x + 4, 26, iw - 8, 2, ds->settings.accent_color);
}
// Truncate title if too long
@@ -292,7 +346,7 @@ void gui::desktop_draw_panel(DesktopState* ds) {
zenith::strncpy(short_title, win->title, 18);
int tx = indicator_x + pad;
int ty = 4 + (24 - FONT_HEIGHT) / 2;
int ty = 4 + (24 - system_font_height()) / 2;
draw_text(fb, tx, ty, short_title, colors::PANEL_TEXT);
indicator_x += iw + 4;
@@ -302,11 +356,18 @@ void gui::desktop_draw_panel(DesktopState* ds) {
Zenith::DateTime dt;
zenith::gettime(&dt);
char clock_str[8];
snprintf(clock_str, sizeof(clock_str), "%02d:%02d", (int)dt.Hour, (int)dt.Minute);
char clock_str[12];
if (ds->settings.clock_24h) {
snprintf(clock_str, sizeof(clock_str), "%02d:%02d", (int)dt.Hour, (int)dt.Minute);
} else {
int h12 = (int)dt.Hour % 12;
if (h12 == 0) h12 = 12;
const char* ampm = dt.Hour < 12 ? "AM" : "PM";
snprintf(clock_str, sizeof(clock_str), "%d:%02d %s", h12, (int)dt.Minute, ampm);
}
int clock_w = text_width(clock_str);
int clock_x = sw - clock_w - 12;
int clock_y = (PANEL_HEIGHT - FONT_HEIGHT) / 2;
int clock_y = (PANEL_HEIGHT - system_font_height()) / 2;
draw_text(fb, clock_x, clock_y, clock_str, colors::PANEL_TEXT);
// Date before clock
@@ -314,7 +375,7 @@ void gui::desktop_draw_panel(DesktopState* ds) {
int month_idx = dt.Month > 0 && dt.Month <= 12 ? dt.Month - 1 : 0;
snprintf(date_str, sizeof(date_str), "%s %d", month_names[month_idx], (int)dt.Day);
int date_w = text_width(date_str);
int date_x = clock_x - date_w - 16;
int date_x = clock_x - date_w - 10;
draw_text(fb, date_x, clock_y, date_str, colors::PANEL_TEXT);
// Network icon (to the left of the date)
@@ -349,7 +410,7 @@ void gui::desktop_draw_panel(DesktopState* ds) {
// App Menu (5 items with separator and rounded corners)
// ============================================================================
static constexpr int MENU_ITEM_COUNT = 9;
static constexpr int MENU_ITEM_COUNT = 13;
static constexpr int MENU_W = 220;
static constexpr int MENU_ITEM_H = 40;
@@ -379,8 +440,12 @@ static void desktop_draw_app_menu(DesktopState* ds) {
{ "Calculator", &ds->icon_calculator },
{ "Text Editor", &ds->icon_texteditor },
{ "Kernel Log", &ds->icon_terminal },
{ "Processes", &ds->icon_procmgr },
{ "Mandelbrot", &ds->icon_mandelbrot },
{ "Devices", &ds->icon_devexplorer },
{ "Wikipedia", &ds->icon_wikipedia },
{ "About", &ds->icon_settings },
{ "DOOM", &ds->icon_doom },
{ "Settings", &ds->icon_settings },
{ "Reboot", &ds->icon_reboot },
};
@@ -391,7 +456,7 @@ static void desktop_draw_app_menu(DesktopState* ds) {
int iy = menu_y + 4 + i * MENU_ITEM_H;
// Thin separator lines before utility apps and before Settings
if (i == 3 || i == 7) {
if (i == 3 || i == 11) {
int sep_y = iy - 1;
for (int sx = menu_x + 8; sx < menu_x + MENU_W - 8; sx++)
fb.put_pixel(sx, sep_y, colors::BORDER);
@@ -414,7 +479,7 @@ static void desktop_draw_app_menu(DesktopState* ds) {
// Label
int tx = icon_x + 28;
int ty = item_rect.y + (MENU_ITEM_H - FONT_HEIGHT) / 2;
int ty = item_rect.y + (MENU_ITEM_H - system_font_height()) / 2;
draw_text(fb, tx, ty, items[i].label, colors::TEXT_COLOR);
}
}
@@ -434,7 +499,7 @@ static void desktop_draw_net_popup(DesktopState* ds) {
int tx = popup_x + 12;
int ty = popup_y + 10;
int line_h = FONT_HEIGHT + 6;
int line_h = system_font_height() + 6;
char line[64];
Zenith::NetCfg& nc = ds->cached_net_cfg;
@@ -645,14 +710,19 @@ void gui::desktop_compose(DesktopState* ds) {
uint32_t* row = (uint32_t*)((uint8_t*)buf + y * pitch);
if (y < grad_start) {
// Panel area - will be overwritten by panel drawing
uint32_t px = colors::PANEL_BG.to_pixel();
uint32_t px = ds->settings.panel_color.to_pixel();
for (int x = 0; x < sw; x++) row[x] = px;
} else if (ds->settings.bg_gradient) {
int t = y - grad_start;
Color top = ds->settings.bg_grad_top;
Color bot = ds->settings.bg_grad_bottom;
uint8_t r = top.r - (top.r - bot.r) * t / grad_range;
uint8_t g = top.g - (top.g - bot.g) * t / grad_range;
uint8_t b = top.b - (top.b - bot.b) * t / grad_range;
uint32_t px = 0xFF000000 | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
for (int x = 0; x < sw; x++) row[x] = px;
} else {
int t = y - grad_start;
uint8_t r = 0xD0 - (0xD0 - 0xA0) * t / grad_range;
uint8_t g = 0xD8 - (0xD8 - 0xA8) * t / grad_range;
uint8_t b = 0xE8 - (0xE8 - 0xB8) * t / grad_range;
uint32_t px = 0xFF000000 | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
uint32_t px = ds->settings.bg_solid.to_pixel();
for (int x = 0; x < sw; x++) row[x] = px;
}
}
@@ -720,7 +790,7 @@ void gui::desktop_compose(DesktopState* ds) {
}
int tx = icon_x + 28;
int ty = item_r.y + (CTX_ITEM_H - FONT_HEIGHT) / 2;
int ty = item_r.y + (CTX_ITEM_H - system_font_height()) / 2;
draw_text(fb, tx, ty, ctx_items[i].label, colors::TEXT_COLOR);
}
}
@@ -836,25 +906,29 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
win->saved_frame = win->frame;
win->frame = {0, PANEL_HEIGHT, ds->screen_w / 2, ds->screen_h - PANEL_HEIGHT};
win->state = WIN_MAXIMIZED;
Rect cr = win->content_rect();
if (cr.w != win->content_w || cr.h != win->content_h) {
if (win->content) zenith::free(win->content);
win->content_w = cr.w;
win->content_h = cr.h;
win->content = (uint32_t*)zenith::alloc(cr.w * cr.h * 4);
zenith::memset(win->content, 0xFF, cr.w * cr.h * 4);
if (!win->external) {
Rect cr = win->content_rect();
if (cr.w != win->content_w || cr.h != win->content_h) {
if (win->content) zenith::free(win->content);
win->content_w = cr.w;
win->content_h = cr.h;
win->content = (uint32_t*)zenith::alloc(cr.w * cr.h * 4);
zenith::memset(win->content, 0xFF, cr.w * cr.h * 4);
}
}
} else if (mx >= ds->screen_w - 1) {
win->saved_frame = win->frame;
win->frame = {ds->screen_w / 2, PANEL_HEIGHT, ds->screen_w / 2, ds->screen_h - PANEL_HEIGHT};
win->state = WIN_MAXIMIZED;
Rect cr = win->content_rect();
if (cr.w != win->content_w || cr.h != win->content_h) {
if (win->content) zenith::free(win->content);
win->content_w = cr.w;
win->content_h = cr.h;
win->content = (uint32_t*)zenith::alloc(cr.w * cr.h * 4);
zenith::memset(win->content, 0xFF, cr.w * cr.h * 4);
if (!win->external) {
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);
}
}
}
}
@@ -903,14 +977,16 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
}
if (left_released) {
win->resizing = false;
// Reallocate content buffer if dimensions changed
Rect cr = win->content_rect();
if (cr.w != win->content_w || cr.h != win->content_h) {
if (win->content) zenith::free(win->content);
win->content_w = cr.w;
win->content_h = cr.h;
win->content = (uint32_t*)zenith::alloc(cr.w * cr.h * 4);
zenith::memset(win->content, 0xFF, cr.w * cr.h * 4);
// Reallocate content buffer if dimensions changed (skip for external)
if (!win->external) {
Rect cr = win->content_rect();
if (cr.w != win->content_w || cr.h != win->content_h) {
if (win->content) zenith::free(win->content);
win->content_w = cr.w;
win->content_h = cr.h;
win->content = (uint32_t*)zenith::alloc(cr.w * cr.h * 4);
zenith::memset(win->content, 0xFF, cr.w * cr.h * 4);
}
}
win->dirty = true;
}
@@ -936,9 +1012,13 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
case 3: open_calculator(ds); break;
case 4: open_texteditor(ds); break;
case 5: open_klog(ds); break;
case 6: open_wiki(ds); break;
case 7: open_settings(ds); break;
case 8: open_reboot_dialog(ds); break;
case 6: open_procmgr(ds); break;
case 7: open_mandelbrot(ds); break;
case 8: open_devexplorer(ds); break;
case 9: open_wiki(ds); break;
case 10: open_doom(ds); break;
case 11: open_settings(ds); break;
case 12: open_reboot_dialog(ds); break;
}
ds->app_menu_open = false;
}
@@ -1046,13 +1126,16 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
win->frame = {0, PANEL_HEIGHT, ds->screen_w, ds->screen_h - PANEL_HEIGHT};
win->state = WIN_MAXIMIZED;
}
Rect cr = win->content_rect();
if (cr.w != win->content_w || cr.h != win->content_h) {
if (win->content) zenith::free(win->content);
win->content_w = cr.w;
win->content_h = cr.h;
win->content = (uint32_t*)zenith::alloc(cr.w * cr.h * 4);
zenith::memset(win->content, 0xFF, cr.w * cr.h * 4);
// Reallocate content buffer for local windows only
if (!win->external) {
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;
@@ -1097,10 +1180,22 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
if (cr.contains(mx, my)) {
desktop_raise_window(ds, i);
int new_idx = ds->window_count - 1;
if (ds->windows[new_idx].on_mouse) {
Window* raised = &ds->windows[new_idx];
if (raised->external) {
// Forward mouse event to external window
Zenith::WinEvent wev;
zenith::memset(&wev, 0, sizeof(wev));
wev.type = 1; // mouse
wev.mouse.x = mx - cr.x;
wev.mouse.y = my - cr.y;
wev.mouse.scroll = ev.scroll;
wev.mouse.buttons = buttons;
wev.mouse.prev_buttons = prev;
zenith::win_sendevent(raised->ext_win_id, &wev);
} else if (raised->on_mouse) {
ev.x = mx;
ev.y = my;
ds->windows[new_idx].on_mouse(&ds->windows[new_idx], ev);
raised->on_mouse(raised, ev);
}
return;
}
@@ -1120,8 +1215,20 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
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);
if (cr.contains(mx, my)) {
if (win->external) {
Zenith::WinEvent wev;
zenith::memset(&wev, 0, sizeof(wev));
wev.type = 1; // mouse
wev.mouse.x = mx - cr.x;
wev.mouse.y = my - cr.y;
wev.mouse.scroll = ev.scroll;
wev.mouse.buttons = buttons;
wev.mouse.prev_buttons = prev;
zenith::win_sendevent(win->ext_win_id, &wev);
} else if (win->on_mouse) {
win->on_mouse(win, ev);
}
}
}
@@ -1147,10 +1254,8 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
}
void gui::desktop_handle_keyboard(DesktopState* ds, const Zenith::KeyEvent& key) {
if (!key.pressed) return;
// Global shortcuts
if (key.ctrl && key.alt) {
// Global shortcuts (only on key press)
if (key.pressed && key.ctrl && key.alt) {
if (key.ascii == 't' || key.ascii == 'T') {
open_terminal(ds);
return;
@@ -1175,17 +1280,121 @@ void gui::desktop_handle_keyboard(DesktopState* ds, const Zenith::KeyEvent& key)
open_klog(ds);
return;
}
if (key.ascii == 'd' || key.ascii == 'D') {
open_doom(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) {
if (win->external) {
// Forward key event to external window via syscall
Zenith::WinEvent ev;
zenith::memset(&ev, 0, sizeof(ev));
ev.type = 0; // key
ev.key = key;
zenith::win_sendevent(win->ext_win_id, &ev);
} else if (win->on_key) {
win->on_key(win, key);
}
}
}
// ============================================================================
// External Window Polling
// ============================================================================
void desktop_poll_external_windows(DesktopState* ds) {
Zenith::WinInfo extWins[8];
int extCount = zenith::win_enumerate(extWins, 8);
// Check for new external windows and map them
for (int e = 0; e < extCount; e++) {
int extId = extWins[e].id;
// Check if we already have this window
bool found = false;
for (int i = 0; i < ds->window_count; i++) {
if (ds->windows[i].external && ds->windows[i].ext_win_id == extId) {
found = true;
// Update dirty flag
if (extWins[e].dirty) {
ds->windows[i].dirty = true;
}
break;
}
}
if (!found && ds->window_count < MAX_WINDOWS) {
// Map the pixel buffer into our address space
uint64_t va = zenith::win_map(extId);
if (va == 0) continue;
int idx = ds->window_count;
Window* win = &ds->windows[idx];
zenith::memset(win, 0, sizeof(Window));
zenith::strncpy(win->title, extWins[e].title, MAX_TITLE_LEN);
int w = extWins[e].width;
int h = extWins[e].height;
// Position the window centered-ish
int wx = (ds->screen_w - w - 2 * BORDER_WIDTH) / 2 + idx * 30;
int wy = PANEL_HEIGHT + 20 + idx * 30;
win->frame = {wx, wy, w + 2 * BORDER_WIDTH, h + TITLEBAR_HEIGHT + BORDER_WIDTH};
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;
// Point content to the shared pixel buffer
win->content = (uint32_t*)va;
win->content_w = w;
win->content_h = h;
win->on_draw = nullptr;
win->on_mouse = nullptr;
win->on_key = nullptr;
win->on_close = nullptr;
win->on_poll = nullptr;
win->app_data = nullptr;
win->external = true;
win->ext_win_id = extId;
// 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++;
}
}
// Check for removed external windows (process exited)
for (int i = ds->window_count - 1; i >= 0; i--) {
if (!ds->windows[i].external) continue;
int extId = ds->windows[i].ext_win_id;
bool stillExists = false;
for (int e = 0; e < extCount; e++) {
if (extWins[e].id == extId) {
stillExists = true;
break;
}
}
if (!stillExists) {
// Window gone — remove without freeing content (shared memory)
ds->windows[i].content = nullptr; // prevent free
gui::desktop_close_window(ds, i);
}
}
}
void gui::desktop_run(DesktopState* ds) {
for (;;) {
// Poll mouse state
@@ -1199,6 +1408,9 @@ void gui::desktop_run(DesktopState* ds) {
desktop_handle_keyboard(ds, key);
}
// Poll external windows (discover new, remove dead, update dirty)
desktop_poll_external_windows(ds);
// Poll windows that have a poll callback
for (int i = 0; i < ds->window_count; i++) {
Window* win = &ds->windows[i];
@@ -0,0 +1,35 @@
/*
* stb_truetype_impl.cpp
* Single compilation unit for stb_truetype in ZenithOS freestanding environment
* Copyright (c) 2026 Daniel Hammer
*/
#include <cstdint>
#include <cstddef>
#include <zenith/heap.h>
#include <zenith/string.h>
#include <gui/stb_math.h>
// Override all stb_truetype dependencies before including the implementation
#define STBTT_ifloor(x) ((int) stb_floor(x))
#define STBTT_iceil(x) ((int) stb_ceil(x))
#define STBTT_sqrt(x) stb_sqrt(x)
#define STBTT_pow(x,y) stb_pow(x,y)
#define STBTT_fmod(x,y) stb_fmod(x,y)
#define STBTT_cos(x) stb_cos(x)
#define STBTT_acos(x) stb_acos(x)
#define STBTT_fabs(x) stb_fabs(x)
#define STBTT_malloc(x,u) ((void)(u), zenith::malloc(x))
#define STBTT_free(x,u) ((void)(u), zenith::mfree(x))
#define STBTT_memcpy(d,s,n) zenith::memcpy(d,s,n)
#define STBTT_memset(d,v,n) zenith::memset(d,v,n)
#define STBTT_strlen(x) zenith::slen(x)
#define STBTT_assert(x) ((void)(x))
#define STB_TRUETYPE_IMPLEMENTATION
#include <gui/stb_truetype.h>
+86 -61
View File
@@ -1,6 +1,6 @@
/*
* doomgeneric_zenith.c
* DOOM platform implementation for ZenithOS
* DOOM platform implementation for ZenithOS (standalone window server client)
* Copyright (c) 2025 Daniel Hammer
*/
@@ -30,41 +30,70 @@ static inline long _zos_syscall1(long nr, long a1) {
return ret;
}
static inline long _zos_syscall2(long nr, long a1, long a2) {
long ret;
__asm__ volatile(
"mov %[a1], %%rdi\n\t"
"mov %[a2], %%rsi\n\t"
"syscall"
: "=a"(ret)
: "a"(nr), [a1] "r"(a1), [a2] "r"(a2)
: "rcx", "r11", "rdi", "rsi", "rdx", "r8", "r9", "r10", "memory");
return ret;
}
static inline long _zos_syscall4(long nr, long a1, long a2, long a3, long a4) {
long ret;
__asm__ volatile(
"mov %[a1], %%rdi\n\t"
"mov %[a2], %%rsi\n\t"
"mov %[a3], %%rdx\n\t"
"mov %[a4], %%r10\n\t"
"syscall"
: "=a"(ret)
: "a"(nr), [a1] "r"(a1), [a2] "r"(a2), [a3] "r"(a3), [a4] "r"(a4)
: "rcx", "r11", "rdi", "rsi", "rdx", "r8", "r9", "r10", "memory");
return ret;
}
/* Syscall numbers (must match kernel/src/Api/Syscall.hpp) */
#define SYS_EXIT 0
#define SYS_SLEEP_MS 2
#define SYS_PRINT 4
#define SYS_GETMILLISECONDS 14
#define SYS_ISKEYAVAILABLE 16
#define SYS_GETKEY 17
#define SYS_FBINFO 21
#define SYS_FBMAP 22
#define SYS_WINCREATE 54
#define SYS_WINDESTROY 55
#define SYS_WINPRESENT 56
#define SYS_WINPOLL 57
/* FbInfo struct (must match kernel definition) */
struct FbInfo {
unsigned long width;
unsigned long height;
unsigned long pitch;
unsigned long bpp;
unsigned long userAddr;
/* Window server structs (must match Zenith::WinCreateResult and Zenith::WinEvent) */
struct WinCreateResult {
int id; /* -1 on failure */
unsigned _pad;
unsigned long pixelVa; /* VA of pixel buffer in caller's address space */
};
/* KeyEvent struct (must match kernel definition) */
struct KeyEvent {
unsigned char scancode;
char ascii;
unsigned char pressed;
unsigned char shift;
unsigned char ctrl;
unsigned char alt;
struct WinEvent {
unsigned char type; /* 0=key, 1=mouse, 2=resize, 3=close */
unsigned char _pad[3];
union {
struct {
unsigned char scancode;
char ascii;
unsigned char pressed;
unsigned char shift;
unsigned char ctrl;
unsigned char alt;
} key;
struct { int x, y, scroll; unsigned char buttons, prev_buttons; } mouse;
struct { int w, h; } resize;
};
};
/* ---- Framebuffer state ---- */
/* ---- Window state ---- */
static uint32_t* g_fbPtr = 0;
static uint32_t g_fbWidth = 0;
static uint32_t g_fbHeight = 0;
static uint32_t g_fbPitch = 0; /* bytes per scanline */
static int g_winId = -1;
static uint32_t* g_pixBuf = 0;
/* ---- Circular key queue ---- */
@@ -115,8 +144,8 @@ static unsigned char scancode_to_doomkey(unsigned char scancode, char ascii) {
case 0x4D: return KEY_RIGHTARROW;
case 0x1C: return KEY_ENTER;
case 0x01: return KEY_ESCAPE;
case 0x39: return ' '; /* Space = use */
case 0x1D: return KEY_RCTRL; /* LCtrl = fire */
case 0x39: return KEY_USE; /* Space = use */
case 0x1D: return KEY_FIRE; /* LCtrl = fire */
case 0x2A: return KEY_RSHIFT; /* LShift = run */
case 0x36: return KEY_RSHIFT; /* RShift = run */
case 0x38: return KEY_RALT; /* Alt = strafe */
@@ -146,22 +175,26 @@ static unsigned char scancode_to_doomkey(unsigned char scancode, char ascii) {
}
}
/* ---- Poll keyboard and enqueue events ---- */
/* ---- Poll window events and enqueue key events ---- */
static void poll_keyboard(void) {
while (_zos_syscall0(SYS_ISKEYAVAILABLE)) {
struct KeyEvent evt;
_zos_syscall1(SYS_GETKEY, (long)&evt);
struct WinEvent evt;
while (_zos_syscall2(SYS_WINPOLL, (long)g_winId, (long)&evt) > 0) {
if (evt.type == 0) {
/* Key event */
unsigned char baseSc = evt.key.scancode & 0x7F;
unsigned char baseSc = evt.scancode & 0x7F; /* strip break bit */
char ascii = 0;
if (baseSc < 128)
ascii = scancode_to_ascii[baseSc];
char ascii = 0;
if (baseSc < 128)
ascii = scancode_to_ascii[baseSc];
unsigned char dk = scancode_to_doomkey(baseSc, ascii);
if (dk != 0) {
key_queue_push(evt.pressed ? 1 : 0, dk);
unsigned char dk = scancode_to_doomkey(baseSc, ascii);
if (dk != 0) {
key_queue_push(evt.key.pressed ? 1 : 0, dk);
}
} else if (evt.type == 3) {
/* Close event — exit the process */
_zos_syscall1(SYS_EXIT, 0);
}
}
}
@@ -169,38 +202,30 @@ static void poll_keyboard(void) {
/* ---- DG platform functions ---- */
void DG_Init(void) {
struct FbInfo info;
_zos_syscall1(SYS_FBINFO, (long)&info);
struct WinCreateResult result;
_zos_syscall4(SYS_WINCREATE, (long)"DOOM", (long)DOOMGENERIC_RESX,
(long)DOOMGENERIC_RESY, (long)&result);
g_fbWidth = (uint32_t)info.width;
g_fbHeight = (uint32_t)info.height;
g_fbPitch = (uint32_t)info.pitch;
if (result.id < 0) {
_zos_syscall1(SYS_EXIT, 1);
}
g_fbPtr = (uint32_t*)(unsigned long)_zos_syscall0(SYS_FBMAP);
printf("DOOM: framebuffer %ux%u pitch=%u mapped at %p\n",
g_fbWidth, g_fbHeight, g_fbPitch, (void*)g_fbPtr);
g_winId = result.id;
g_pixBuf = (uint32_t*)result.pixelVa;
}
void DG_DrawFrame(void) {
/* Poll keyboard first */
poll_keyboard();
/* Copy DG_ScreenBuffer (DOOMGENERIC_RESX x DOOMGENERIC_RESY) to framebuffer */
if (g_fbPtr == 0 || DG_ScreenBuffer == 0) return;
/* Copy DG_ScreenBuffer into the shared pixel buffer */
if (g_pixBuf == 0 || DG_ScreenBuffer == 0) return;
uint32_t copyW = DOOMGENERIC_RESX;
uint32_t copyH = DOOMGENERIC_RESY;
if (copyW > g_fbWidth) copyW = g_fbWidth;
if (copyH > g_fbHeight) copyH = g_fbHeight;
memcpy(g_pixBuf, DG_ScreenBuffer,
DOOMGENERIC_RESX * DOOMGENERIC_RESY * sizeof(uint32_t));
uint32_t fbStride = g_fbPitch / 4; /* pixels per scanline */
for (uint32_t y = 0; y < copyH; y++) {
uint32_t* dst = g_fbPtr + y * fbStride;
uint32_t* src = DG_ScreenBuffer + y * DOOMGENERIC_RESX;
memcpy(dst, src, copyW * sizeof(uint32_t));
}
/* Mark window as dirty so the compositor picks it up */
_zos_syscall1(SYS_WINPRESENT, (long)g_winId);
}
void DG_SleepMs(uint32_t ms) {
+14 -48
View File
@@ -712,27 +712,22 @@ int sprintf(char *buf, const char *fmt, ...) {
static char _printbuf[4096];
int vprintf(const char *fmt, va_list ap) {
int ret = vsnprintf(_printbuf, sizeof(_printbuf), fmt, ap);
_zos_syscall1(SYS_PRINT, (long)_printbuf);
return ret;
(void)fmt; (void)ap;
return 0;
}
int printf(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
int ret = vprintf(fmt, ap);
va_end(ap);
return ret;
(void)fmt;
return 0;
}
int puts(const char *s) {
_zos_syscall1(SYS_PRINT, (long)s);
_zos_syscall1(SYS_PUTCHAR, (long)'\n');
(void)s;
return 0;
}
int putchar(int c) {
_zos_syscall1(SYS_PUTCHAR, (long)c);
(void)c;
return c;
}
@@ -862,20 +857,8 @@ size_t fread(void *ptr, size_t size, size_t nmemb, FILE *fp) {
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *fp) {
if (fp == NULL) return 0;
/* stdout/stderr: write to terminal */
/* stdout/stderr: discard (no terminal in windowed mode) */
if (fp->is_std == 1 || fp->is_std == 2) {
/* Write as string to terminal */
size_t total = size * nmemb;
const char *s = (const char *)ptr;
char buf[512];
while (total > 0) {
size_t chunk = total > 511 ? 511 : total;
memcpy(buf, s, chunk);
buf[chunk] = '\0';
_zos_syscall1(SYS_PRINT, (long)buf);
s += chunk;
total -= chunk;
}
return nmemb;
}
@@ -970,31 +953,18 @@ char *fgets(char *s, int size, FILE *fp) {
}
int fputs(const char *s, FILE *fp) {
size_t len = strlen(s);
return fwrite(s, 1, len, fp) > 0 ? 0 : -1;
(void)s; (void)fp;
return 0;
}
int fprintf(FILE *fp, const char *fmt, ...) {
char buf[4096];
va_list ap;
va_start(ap, fmt);
int ret = vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
if (fp == stdout || fp == stderr || (fp && fp->is_std)) {
_zos_syscall1(SYS_PRINT, (long)buf);
}
return ret;
(void)fp; (void)fmt;
return 0;
}
int vfprintf(FILE *fp, const char *fmt, va_list ap) {
char buf[4096];
int ret = vsnprintf(buf, sizeof(buf), fmt, ap);
if (fp == stdout || fp == stderr || (fp && fp->is_std)) {
_zos_syscall1(SYS_PRINT, (long)buf);
}
return ret;
(void)fp; (void)fmt; (void)ap;
return 0;
}
int sscanf(const char *str, const char *fmt, ...) {
@@ -1069,11 +1039,7 @@ int sscanf(const char *str, const char *fmt, ...) {
}
void perror(const char *s) {
if (s && *s) {
_zos_syscall1(SYS_PRINT, (long)s);
_zos_syscall1(SYS_PRINT, (long)": ");
}
_zos_syscall1(SYS_PRINT, (long)"error\n");
(void)s;
}
int rename(const char *old, const char *new_) {