feat: multi-user system, bug fixes, security & performance fixes, and more

This commit is contained in:
2026-03-14 13:28:46 +01:00
parent 576ad34f95
commit 261b536041
389 changed files with 231853 additions and 591 deletions
+251
View File
@@ -0,0 +1,251 @@
/*
* canvas.hpp
* MontaukOS Canvas — drawing primitives for pixel buffer (uint32_t*) targets
* Mirrors Framebuffer API but operates directly on app content buffers.
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include "gui/gui.hpp"
#include "gui/font.hpp"
#include "gui/svg.hpp"
#include "gui/window.hpp"
namespace gui {
struct Canvas {
uint32_t* pixels;
int w, h;
// ---- Constructors ----
Canvas(uint32_t* px, int width, int height)
: pixels(px), w(width), h(height) {}
Canvas(Window* win)
: pixels(win->content), w(win->content_w), h(win->content_h) {}
// ---- Core drawing ----
void fill(Color c) {
uint32_t px = c.to_pixel();
uint64_t px2 = ((uint64_t)px << 32) | px;
int total = w * h;
int i = 0;
// Align to 8-byte boundary if needed
if (total > 0 && ((uint64_t)pixels & 4)) {
pixels[0] = px;
i = 1;
}
uint64_t* dst64 = (uint64_t*)(pixels + i);
int pairs = (total - i) / 2;
for (int p = 0; p < pairs; p++) dst64[p] = px2;
if ((total - i) & 1) pixels[total - 1] = px;
}
void put_pixel(int x, int y, Color c) {
if (x >= 0 && x < w && y >= 0 && y < h)
pixels[y * w + x] = c.to_pixel();
}
void fill_rect(int x, int y, int rw, int rh, Color c) {
uint32_t px = c.to_pixel();
uint64_t px2 = ((uint64_t)px << 32) | px;
int x0 = gui_max(x, 0), y0 = gui_max(y, 0);
int x1 = gui_min(x + rw, w), y1 = gui_min(y + rh, h);
for (int dy = y0; dy < y1; dy++) {
uint32_t* row = pixels + dy * w;
int dx = x0;
// Align to 8-byte boundary
if (dx < x1 && ((uint64_t)(row + dx) & 4)) {
row[dx] = px;
dx++;
}
// Bulk 2-pixel writes
uint64_t* dst64 = (uint64_t*)(row + dx);
int pairs = (x1 - dx) / 2;
for (int p = 0; p < pairs; p++) dst64[p] = px2;
dx += pairs * 2;
// Remainder
if (dx < x1) row[dx] = px;
}
}
void fill_rounded_rect(int x, int y, int rw, int rh, int radius, Color c) {
if (radius <= 0) { fill_rect(x, y, rw, rh, c); return; }
uint32_t px = c.to_pixel();
for (int row = 0; row < rh; row++) {
int dy = y + row;
if (dy < 0 || dy >= h) continue;
for (int col = 0; col < rw; col++) {
int dx = x + col;
if (dx < 0 || dx >= w) continue;
bool in_corner = false;
int cx_off = 0, cy_off = 0;
if (col < radius && row < radius) {
cx_off = radius - col; cy_off = radius - row; in_corner = true;
} else if (col >= rw - radius && row < radius) {
cx_off = col - (rw - radius - 1); cy_off = radius - row; in_corner = true;
} else if (col < radius && row >= rh - radius) {
cx_off = radius - col; cy_off = row - (rh - radius - 1); in_corner = true;
} else if (col >= rw - radius && row >= rh - radius) {
cx_off = col - (rw - radius - 1); cy_off = row - (rh - radius - 1); in_corner = true;
}
if (in_corner && cx_off * cx_off + cy_off * cy_off > radius * radius) continue;
pixels[dy * w + dx] = px;
}
}
}
void hline(int x, int y, int len, Color c) {
if (y < 0 || y >= h) return;
uint32_t px = c.to_pixel();
int x0 = gui_max(x, 0), x1 = gui_min(x + len, w);
uint32_t* row = pixels + y * w;
int dx = x0;
if (dx < x1 && ((uint64_t)(row + dx) & 4)) {
row[dx] = px;
dx++;
}
uint64_t px2 = ((uint64_t)px << 32) | px;
uint64_t* dst64 = (uint64_t*)(row + dx);
int pairs = (x1 - dx) / 2;
for (int p = 0; p < pairs; p++) dst64[p] = px2;
dx += pairs * 2;
if (dx < x1) row[dx] = px;
}
void vline(int x, int y, int len, Color c) {
if (x < 0 || x >= w) return;
uint32_t px = c.to_pixel();
int y0 = gui_max(y, 0), y1 = gui_min(y + len, h);
for (int dy = y0; dy < y1; dy++)
pixels[dy * w + x] = px;
}
void rect(int x, int y, int rw, int rh, Color c) {
hline(x, y, rw, c);
hline(x, y + rh - 1, rw, c);
vline(x, y, rh, c);
vline(x + rw - 1, y, rh, c);
}
// ---- Text ----
void text(int x, int y, const char* str, Color c) {
if (fonts::system_font && fonts::system_font->valid) {
fonts::system_font->draw_to_buffer(pixels, w, h, x, y, str, c, fonts::UI_SIZE);
return;
}
uint32_t px = c.to_pixel();
for (int i = 0; str[i] && x + (i + 1) * FONT_WIDTH <= w; i++) {
const uint8_t* glyph = &font_data[(unsigned char)str[i] * FONT_HEIGHT];
int cx = x + i * FONT_WIDTH;
for (int fy = 0; fy < FONT_HEIGHT && y + fy < h; fy++) {
uint8_t bits = glyph[fy];
for (int fx = 0; fx < FONT_WIDTH; fx++) {
if (bits & (0x80 >> fx)) {
int dx = cx + fx;
int dy = y + fy;
if (dx >= 0 && dx < w && dy >= 0)
pixels[dy * w + dx] = px;
}
}
}
}
}
void text_2x(int x, int y, const char* str, Color c) {
if (fonts::system_font && fonts::system_font->valid) {
fonts::system_font->draw_to_buffer(pixels, w, h, x, y, str, c, fonts::LARGE_SIZE);
return;
}
uint32_t px = c.to_pixel();
for (int i = 0; str[i] && x + (i + 1) * FONT_WIDTH * 2 <= w; i++) {
const uint8_t* glyph = &font_data[(unsigned char)str[i] * FONT_HEIGHT];
int cx = x + i * FONT_WIDTH * 2;
for (int fy = 0; fy < FONT_HEIGHT; fy++) {
uint8_t bits = glyph[fy];
for (int fx = 0; fx < FONT_WIDTH; fx++) {
if (bits & (0x80 >> fx)) {
int dx = cx + fx * 2;
int dy = y + fy * 2;
for (int sy = 0; sy < 2; sy++)
for (int sx = 0; sx < 2; sx++) {
int pdx = dx + sx;
int pdy = dy + sy;
if (pdx >= 0 && pdx < w && pdy >= 0 && pdy < h)
pixels[pdy * w + pdx] = px;
}
}
}
}
}
}
void text_mono(int x, int y, const char* str, Color c) {
if (fonts::mono && fonts::mono->valid) {
fonts::mono->draw_to_buffer(pixels, w, h, x, y, str, c, fonts::TERM_SIZE);
return;
}
text(x, y, str, c);
}
// ---- Icons ----
void icon(int x, int y, const SvgIcon& ic) {
if (!ic.pixels) return;
for (int row = 0; row < ic.height; row++) {
int dy = y + row;
if (dy < 0 || dy >= h) continue;
for (int col = 0; col < ic.width; col++) {
int dx = x + col;
if (dx < 0 || dx >= w) continue;
uint32_t src = ic.pixels[row * ic.width + col];
uint8_t sa = (src >> 24) & 0xFF;
if (sa == 0) continue;
if (sa == 255) {
pixels[dy * w + dx] = src;
} else {
uint32_t dst = pixels[dy * w + dx];
uint8_t sr = (src >> 16) & 0xFF;
uint8_t sg = (src >> 8) & 0xFF;
uint8_t sb = src & 0xFF;
uint8_t dr = (dst >> 16) & 0xFF;
uint8_t dg = (dst >> 8) & 0xFF;
uint8_t db = dst & 0xFF;
uint32_t a = sa, inv_a = 255 - sa;
uint32_t rr = (a * sr + inv_a * dr + 128) / 255;
uint32_t gg = (a * sg + inv_a * dg + 128) / 255;
uint32_t bb = (a * sb + inv_a * db + 128) / 255;
pixels[dy * w + dx] = 0xFF000000 | (rr << 16) | (gg << 8) | bb;
}
}
}
}
// ---- High-level helpers ----
void kv_line(int x, int* y, const char* line, Color c, int line_h = 0) {
if (line_h == 0) line_h = system_font_height() + 6;
text(x, *y, line, c);
*y += line_h;
}
void separator(int x_start, int x_end, int* y, Color c, int spacing = 8) {
hline(x_start, *y, x_end - x_start, c);
*y += spacing;
}
void button(int x, int y, int bw, int bh, const char* label,
Color bg, Color fg, int radius = 4) {
fill_rounded_rect(x, y, bw, bh, radius, bg);
int tw = text_width(label);
int fh = system_font_height();
int tx = x + (bw - tw) / 2;
int ty = y + (bh - fh) / 2;
text(tx, ty, label, fg);
}
};
} // namespace gui
+161
View File
@@ -0,0 +1,161 @@
/*
* desktop.hpp
* MontaukOS desktop state and compositor declarations
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include "gui/gui.hpp"
#include "gui/framebuffer.hpp"
#include "gui/svg.hpp"
#include "gui/window.hpp"
#include "gui/widgets.hpp"
#include "gui/terminal.hpp"
#include <Api/Syscall.hpp>
namespace gui {
static constexpr int MAX_WINDOWS = 8;
static constexpr int PANEL_HEIGHT = 32;
static constexpr int MAX_EXTERNAL_APPS = 16;
// External app discovered from 0:/apps/ manifest
struct ExternalApp {
char name[48];
char binary_path[128];
char category[24];
SvgIcon icon;
bool menu_visible;
};
struct DesktopSettings {
// Background
bool bg_gradient; // true = gradient, false = solid
bool bg_image; // true = JPEG wallpaper
Color bg_solid; // solid background color
Color bg_grad_top; // gradient top color
Color bg_grad_bottom; // gradient bottom color
// Wallpaper (valid when bg_image == true)
char bg_image_path[128]; // VFS path to current wallpaper
uint32_t* bg_wallpaper; // scaled ARGB pixel buffer (screen-sized)
int bg_wallpaper_w; // scaled width
int bg_wallpaper_h; // scaled height
// Panel
Color panel_color; // panel background color
// Accent
Color accent_color; // buttons, highlights, active indicators
// Display
bool show_shadows; // window shadows on/off
bool clock_24h; // 24-hour clock format
int ui_scale; // 0=Small, 1=Default, 2=Large
};
struct DesktopState {
Framebuffer fb;
Window windows[MAX_WINDOWS];
int window_count;
int focused_window;
// Current user context
char current_user[32];
char home_dir[128]; // "0:/users/<username>"
char user_config_dir[128]; // "0:/users/<username>/config"
bool is_admin;
Montauk::MouseState mouse;
uint8_t prev_buttons;
bool app_menu_open;
SvgIcon icon_terminal;
SvgIcon icon_filemanager;
SvgIcon icon_sysinfo;
SvgIcon icon_appmenu;
SvgIcon icon_folder;
SvgIcon icon_file;
SvgIcon icon_computer;
SvgIcon icon_network;
SvgIcon icon_calculator;
SvgIcon icon_texteditor;
SvgIcon icon_go_up;
SvgIcon icon_go_back;
SvgIcon icon_go_forward;
SvgIcon icon_save;
SvgIcon icon_home;
SvgIcon icon_exec;
SvgIcon icon_wikipedia;
SvgIcon icon_folder_lg;
SvgIcon icon_file_lg;
SvgIcon icon_exec_lg;
SvgIcon icon_drive;
SvgIcon icon_drive_lg;
SvgIcon icon_delete;
SvgIcon icon_settings;
SvgIcon icon_reboot;
SvgIcon icon_shutdown;
SvgIcon icon_logout;
SvgIcon icon_procmgr;
SvgIcon icon_mandelbrot;
SvgIcon icon_volume;
// External apps discovered from 0:/apps/ manifests
ExternalApp external_apps[MAX_EXTERNAL_APPS];
int external_app_count;
bool ctx_menu_open;
int ctx_menu_x, ctx_menu_y;
bool net_popup_open;
Montauk::NetCfg cached_net_cfg;
uint64_t net_cfg_last_poll;
Rect net_icon_rect;
bool vol_popup_open;
Rect vol_icon_rect;
int vol_level; // 0-100
bool vol_muted;
int vol_pre_mute; // volume before mute
bool vol_dragging; // slider drag in progress
uint64_t vol_last_poll;
int screen_w, screen_h;
// IDs of external windows we've sent a close event to but that haven't
// been destroyed yet by their owning process. Prevents the poll loop
// from re-creating them at the default position (visible flicker).
static constexpr int MAX_CLOSING = 8;
int closing_ext_ids[MAX_CLOSING];
int closing_ext_count;
DesktopSettings settings;
// Lock screen state
bool screen_locked;
char lock_password[64];
int lock_password_len;
char lock_error[128];
bool lock_show_error;
char lock_display_name[64];
SvgIcon icon_lock;
};
// Forward declarations - implemented in main.cpp
void desktop_init(DesktopState* ds);
void desktop_run(DesktopState* ds);
void desktop_compose(DesktopState* ds);
int desktop_create_window(DesktopState* ds, const char* title, int x, int y, int w, int h);
void desktop_close_window(DesktopState* ds, int idx);
void desktop_raise_window(DesktopState* ds, int idx);
void desktop_draw_panel(DesktopState* ds);
void desktop_draw_window(DesktopState* ds, int idx);
void desktop_handle_mouse(DesktopState* ds);
void desktop_handle_keyboard(DesktopState* ds, const Montauk::KeyEvent& key);
} // namespace gui
+394
View File
@@ -0,0 +1,394 @@
/*
* draw.hpp
* MontaukOS drawing primitives (lines, circles, rounded rects, cursor)
* Copyright (c) 2025 Daniel Hammer
*/
#pragma once
#include "gui/gui.hpp"
#include "gui/framebuffer.hpp"
namespace gui {
// Fast horizontal line
inline void draw_hline(Framebuffer& fb, int x, int y, int w, Color c) {
for (int i = 0; i < w; i++) fb.put_pixel(x + i, y, c);
}
// Fast vertical line
inline void draw_vline(Framebuffer& fb, int x, int y, int h, Color c) {
for (int i = 0; i < h; i++) fb.put_pixel(x, y + i, c);
}
// Rectangle outline
inline void draw_rect(Framebuffer& fb, int x, int y, int w, int h, Color c) {
draw_hline(fb, x, y, w, c);
draw_hline(fb, x, y + h - 1, w, c);
draw_vline(fb, x, y, h, c);
draw_vline(fb, x + w - 1, y, h, c);
}
// Filled rounded rectangle using corner circles
inline void fill_rounded_rect(Framebuffer& fb, int x, int y, int w, int h, int radius, Color c) {
if (radius <= 0) {
fb.fill_rect(x, y, w, h, c);
return;
}
// Clamp radius to half the smaller dimension
int max_r = gui_min(w / 2, h / 2);
if (radius > max_r) radius = max_r;
// Fill the center rectangle
fb.fill_rect(x + radius, y, w - 2 * radius, h, c);
// Fill the left and right strips (excluding corners)
fb.fill_rect(x, y + radius, radius, h - 2 * radius, c);
fb.fill_rect(x + w - radius, y + radius, radius, h - 2 * radius, c);
// Draw the four rounded corners using midpoint circle
int cx_tl = x + radius;
int cy_tl = y + radius;
int cx_tr = x + w - radius - 1;
int cy_tr = y + radius;
int cx_bl = x + radius;
int cy_bl = y + h - radius - 1;
int cx_br = x + w - radius - 1;
int cy_br = y + h - radius - 1;
int px = 0;
int py = radius;
int d = 1 - radius;
while (px <= py) {
// Top-left corner
draw_hline(fb, cx_tl - py, cy_tl - px, py - radius + radius, c);
draw_hline(fb, cx_tl - px, cy_tl - py, px, c);
// Top-right corner
draw_hline(fb, cx_tr + 1, cy_tr - px, py, c);
draw_hline(fb, cx_tr + 1, cy_tr - py, px, c);
// Bottom-left corner
draw_hline(fb, cx_bl - py, cy_bl + px, py, c);
draw_hline(fb, cx_bl - px, cy_bl + py, px, c);
// Bottom-right corner
draw_hline(fb, cx_br + 1, cy_br + px, py, c);
draw_hline(fb, cx_br + 1, cy_br + py, px, c);
if (d < 0) {
d += 2 * px + 3;
} else {
d += 2 * (px - py) + 5;
py--;
}
px++;
}
}
// Filled circle (midpoint algorithm)
inline void fill_circle(Framebuffer& fb, int cx, int cy, int r, Color c) {
if (r <= 0) return;
int x = 0;
int y = r;
int d = 1 - r;
// Draw initial horizontal lines
draw_hline(fb, cx - r, cy, 2 * r + 1, c);
while (x < y) {
if (d < 0) {
d += 2 * x + 3;
} else {
d += 2 * (x - y) + 5;
y--;
draw_hline(fb, cx - x, cy + y + 1, 2 * x + 1, c);
draw_hline(fb, cx - x, cy - y - 1, 2 * x + 1, c);
}
x++;
draw_hline(fb, cx - y, cy + x, 2 * y + 1, c);
draw_hline(fb, cx - y, cy - x, 2 * y + 1, c);
}
}
// Circle outline (midpoint algorithm)
inline void draw_circle(Framebuffer& fb, int cx, int cy, int r, Color c) {
if (r <= 0) {
fb.put_pixel(cx, cy, c);
return;
}
int x = 0;
int y = r;
int d = 1 - r;
while (x <= y) {
fb.put_pixel(cx + x, cy + y, c);
fb.put_pixel(cx - x, cy + y, c);
fb.put_pixel(cx + x, cy - y, c);
fb.put_pixel(cx - x, cy - y, c);
fb.put_pixel(cx + y, cy + x, c);
fb.put_pixel(cx - y, cy + x, c);
fb.put_pixel(cx + y, cy - x, c);
fb.put_pixel(cx - y, cy - x, c);
if (d < 0) {
d += 2 * x + 3;
} else {
d += 2 * (x - y) + 5;
y--;
}
x++;
}
}
// Bresenham line drawing
inline void draw_line(Framebuffer& fb, int x0, int y0, int x1, int y1, Color c) {
int dx = gui_abs(x1 - x0);
int dy = gui_abs(y1 - y0);
int sx = x0 < x1 ? 1 : -1;
int sy = y0 < y1 ? 1 : -1;
int err = dx - dy;
for (;;) {
fb.put_pixel(x0, y0, c);
if (x0 == x1 && y0 == y1) break;
int e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x0 += sx;
}
if (e2 < dx) {
err += dx;
y0 += sy;
}
}
}
// Drop shadow: offset darker rectangles below/right
inline void draw_shadow(Framebuffer& fb, int x, int y, int w, int h, int offset, Color shadow_color) {
// Bottom shadow strip
fb.fill_rect_alpha(x + offset, y + h, w, offset, shadow_color);
// Right shadow strip
fb.fill_rect_alpha(x + w, y + offset, offset, h, shadow_color);
// Corner
fb.fill_rect_alpha(x + w, y + h, offset, offset, shadow_color);
}
// 16x16 mouse cursor bitmaps
// Outline (black, where design has '1')
static constexpr uint16_t cursor_outline[16] = {
0x8000, // 1000000000000000
0xC000, // 1100000000000000
0xA000, // 1010000000000000
0x9000, // 1001000000000000
0x8800, // 1000100000000000
0x8400, // 1000010000000000
0x8200, // 1000001000000000
0x8100, // 1000000100000000
0x8080, // 1000000010000000
0x8040, // 1000000001000000
0x8780, // 1000011110000000 (changed: row 10 = 1 2 2 2 2 2 1 1 1 1)
0x9200, // 1001001000000000 (row 11 = 1 2 2 1 2 2 1)
0xA900, // 1010100100000000 (row 12 = 1 2 1 0 1 2 2 1)
0xC900, // 1100100100000000 (row 13 = 1 1 0 0 1 2 2 1)
0x8480, // 1000010010000000 (row 14 = 1 0 0 0 0 1 2 2 1)
0x0700, // 0000011100000000 (row 15 = 0 0 0 0 0 1 1 1)
};
// Fill (white, where design has '2')
static constexpr uint16_t cursor_fill[16] = {
0x0000, // row 0: no fill
0x0000, // row 1: no fill
0x4000, // row 2: 0100000000000000
0x6000, // row 3: 0110000000000000
0x7000, // row 4: 0111000000000000
0x7800, // row 5: 0111100000000000
0x7C00, // row 6: 0111110000000000
0x7E00, // row 7: 0111111000000000
0x7F00, // row 8: 0111111100000000
0x7F80, // row 9: 0111111110000000
0x7800, // row 10: 0111100000000000 (fill only in positions 1-5)
0x6C00, // row 11: 0110110000000000 (fill at positions 1,2 and 4,5)
0x4600, // row 12: 0100011000000000 (fill at position 1 and 5,6)
0x0600, // row 13: 0000011000000000 (fill at positions 5,6)
0x0300, // row 14: 0000001100000000 (fill at positions 6,7)
0x0000, // row 15: no fill
};
// Resize cursor: horizontal double arrow (left-right)
static constexpr uint16_t cursor_h_resize_outline[16] = {
0x0000, 0x0000, 0x0000, 0x0000,
0x0820, // 0000100000100000
0x1830, // 0001100000110000
0x3FF8, // 0011111111111000
0x7FFC, // 0111111111111100
0x3FF8, // 0011111111111000
0x1830, // 0001100000110000
0x0820, // 0000100000100000
0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
};
static constexpr uint16_t cursor_h_resize_fill[16] = {
0x0000, 0x0000, 0x0000, 0x0000,
0x0000,
0x0000,
0x1FF0, // 0001111111110000
0x3FF8, // 0011111111111000
0x1FF0, // 0001111111110000
0x0000,
0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
};
// Resize cursor: vertical double arrow (up-down)
static constexpr uint16_t cursor_v_resize_outline[16] = {
0x0000, 0x0000,
0x0200, // 0000001000000000
0x0700, // 0000011100000000
0x0F80, // 0000111110000000
0x0200, // 0000001000000000
0x0200, // 0000001000000000
0x0200, // 0000001000000000
0x0200, // 0000001000000000
0x0200, // 0000001000000000
0x0200, // 0000001000000000
0x0F80, // 0000111110000000
0x0700, // 0000011100000000
0x0200, // 0000001000000000
0x0000, 0x0000,
};
static constexpr uint16_t cursor_v_resize_fill[16] = {
0x0000, 0x0000, 0x0000,
0x0200, // 0000001000000000
0x0700, // 0000011100000000
0x0200, // 0000001000000000
0x0200, // 0000001000000000
0x0200, // 0000001000000000
0x0200, // 0000001000000000
0x0200, // 0000001000000000
0x0200, // 0000001000000000
0x0700, // 0000011100000000
0x0200, // 0000001000000000
0x0000, 0x0000, 0x0000,
};
// Resize cursor: diagonal NW-SE double arrow
static constexpr uint16_t cursor_nwse_resize_outline[16] = {
0x0000, 0x0000,
0x7C00, // 0111110000000000
0x6000, // 0110000000000000
0x5000, // 0101000000000000
0x4800, // 0100100000000000
0x2400, // 0010010000000000
0x1200, // 0001001000000000
0x0900, // 0000100100000000
0x0480, // 0000010010000000
0x0240, // 0000001001000000
0x0140, // 0000000101000000
0x00C0, // 0000000011000000
0x07C0, // 0000011111000000
0x0000, 0x0000,
};
static constexpr uint16_t cursor_nwse_resize_fill[16] = {
0x0000, 0x0000, 0x0000,
0x1C00, // 0001110000000000
0x2800, // 0010100000000000
0x0400, // 0000010000000000
0x0200, // 0000001000000000
0x0100, // 0000000100000000
0x0080, // 0000000010000000
0x0040, // 0000000001000000
0x0280, // 0000001010000000
0x0380, // 0000001110000000
0x0000, 0x0000, 0x0000, 0x0000,
};
// Resize cursor: diagonal NE-SW double arrow
static constexpr uint16_t cursor_nesw_resize_outline[16] = {
0x0000, 0x0000,
0x07C0, // 0000011111000000
0x00C0, // 0000000011000000
0x0140, // 0000000101000000
0x0240, // 0000001001000000
0x0480, // 0000010010000000
0x0900, // 0000100100000000
0x1200, // 0001001000000000
0x2400, // 0010010000000000
0x4800, // 0100100000000000
0x5000, // 0101000000000000
0x6000, // 0110000000000000
0x7C00, // 0111110000000000
0x0000, 0x0000,
};
static constexpr uint16_t cursor_nesw_resize_fill[16] = {
0x0000, 0x0000, 0x0000,
0x0380, // 0000001110000000
0x0280, // 0000001010000000
0x0040, // 0000000001000000
0x0080, // 0000000010000000
0x0100, // 0000000100000000
0x0200, // 0000001000000000
0x0400, // 0000010000000000
0x2800, // 0010100000000000
0x1C00, // 0001110000000000
0x0000, 0x0000, 0x0000, 0x0000,
};
enum CursorStyle {
CURSOR_ARROW = 0,
CURSOR_RESIZE_H, // left-right
CURSOR_RESIZE_V, // up-down
CURSOR_RESIZE_NWSE, // diagonal NW-SE
CURSOR_RESIZE_NESW, // diagonal NE-SW
};
// Draw the mouse cursor at (x, y)
inline void draw_cursor(Framebuffer& fb, int x, int y, CursorStyle style = CURSOR_ARROW) {
const uint16_t* outline_data = cursor_outline;
const uint16_t* fill_data = cursor_fill;
int ox = 0, oy = 0; // hotspot offset for centered cursors
switch (style) {
case CURSOR_RESIZE_H:
outline_data = cursor_h_resize_outline;
fill_data = cursor_h_resize_fill;
ox = -8; oy = -8;
break;
case CURSOR_RESIZE_V:
outline_data = cursor_v_resize_outline;
fill_data = cursor_v_resize_fill;
ox = -8; oy = -8;
break;
case CURSOR_RESIZE_NWSE:
outline_data = cursor_nwse_resize_outline;
fill_data = cursor_nwse_resize_fill;
ox = -8; oy = -8;
break;
case CURSOR_RESIZE_NESW:
outline_data = cursor_nesw_resize_outline;
fill_data = cursor_nesw_resize_fill;
ox = -8; oy = -8;
break;
default:
break;
}
Color black = colors::BLACK;
Color white = colors::WHITE;
for (int row = 0; row < 16; row++) {
uint16_t outline = outline_data[row];
uint16_t fill = fill_data[row];
for (int col = 0; col < 16; col++) {
uint16_t mask = (uint16_t)(0x8000 >> col);
if (outline & mask) {
fb.put_pixel(x + ox + col, y + oy + row, black);
} else if (fill & mask) {
fb.put_pixel(x + ox + col, y + oy + row, white);
}
}
}
}
} // namespace gui
+99
View File
@@ -0,0 +1,99 @@
/*
* font.hpp
* MontaukOS text rendering — TrueType with bitmap fallback
* Copyright (c) 2025 Daniel Hammer
*/
#pragma once
#include "gui/gui.hpp"
#include "gui/framebuffer.hpp"
#include "gui/truetype.hpp"
namespace gui {
static constexpr int FONT_WIDTH = 8;
static constexpr int FONT_HEIGHT = 16;
// Defined in font_data.cpp
extern const uint8_t font_data[256 * 16];
// Dynamic font height: TTF line height or 16 (bitmap fallback)
inline int system_font_height() {
if (fonts::system_font && fonts::system_font->valid)
return fonts::system_font->get_line_height(fonts::UI_SIZE);
return FONT_HEIGHT;
}
// Dynamic mono font cell dimensions
inline int mono_cell_width() {
if (fonts::mono && fonts::mono->valid) {
// Monospace: all glyphs have the same advance
GlyphCache* gc = fonts::mono->get_cache(fonts::TERM_SIZE);
CachedGlyph* g = fonts::mono->get_glyph(gc, 'M');
if (g) return g->advance;
}
return FONT_WIDTH;
}
inline int mono_cell_height() {
if (fonts::mono && fonts::mono->valid)
return fonts::mono->get_line_height(fonts::TERM_SIZE);
return FONT_HEIGHT;
}
inline void draw_char(Framebuffer& fb, int x, int y, char c, Color fg) {
const uint8_t* glyph = &font_data[(unsigned char)c * FONT_HEIGHT];
for (int row = 0; row < FONT_HEIGHT; row++) {
uint8_t bits = glyph[row];
for (int col = 0; col < FONT_WIDTH; col++) {
if (bits & (0x80 >> col)) {
fb.put_pixel(x + col, y + row, fg);
}
}
}
}
inline void draw_char_bg(Framebuffer& fb, int x, int y, char c, Color fg, Color bg) {
const uint8_t* glyph = &font_data[(unsigned char)c * FONT_HEIGHT];
for (int row = 0; row < FONT_HEIGHT; row++) {
uint8_t bits = glyph[row];
for (int col = 0; col < FONT_WIDTH; col++) {
if (bits & (0x80 >> col)) {
fb.put_pixel(x + col, y + row, fg);
} else {
fb.put_pixel(x + col, y + row, bg);
}
}
}
}
inline void draw_text(Framebuffer& fb, int x, int y, const char* text, Color fg) {
if (fonts::system_font && fonts::system_font->valid) {
fonts::system_font->draw(fb, x, y, text, fg, fonts::UI_SIZE);
return;
}
for (int i = 0; text[i]; i++) {
draw_char(fb, x + i * FONT_WIDTH, y, text[i], fg);
}
}
inline void draw_text_bg(Framebuffer& fb, int x, int y, const char* text, Color fg, Color bg) {
if (fonts::system_font && fonts::system_font->valid) {
fonts::system_font->draw_bg(fb, x, y, text, fg, bg, fonts::UI_SIZE);
return;
}
for (int i = 0; text[i]; i++) {
draw_char_bg(fb, x + i * FONT_WIDTH, y, text[i], fg, bg);
}
}
inline int text_width(const char* text) {
if (fonts::system_font && fonts::system_font->valid) {
return fonts::system_font->measure_text(text, fonts::UI_SIZE);
}
int len = 0;
while (text[len]) len++;
return len * FONT_WIDTH;
}
} // namespace gui
@@ -0,0 +1,196 @@
/*
* framebuffer.hpp
* MontaukOS double-buffered framebuffer abstraction
* Copyright (c) 2025 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <montauk/syscall.h>
#include "gui/gui.hpp"
namespace gui {
class Framebuffer {
uint32_t* hw_fb;
uint32_t* back_buf;
int fb_width;
int fb_height;
int fb_pitch; // in bytes
public:
Framebuffer() : hw_fb(nullptr), back_buf(nullptr), fb_width(0), fb_height(0), fb_pitch(0) {
Montauk::FbInfo info;
montauk::fb_info(&info);
fb_width = (int)info.width;
fb_height = (int)info.height;
fb_pitch = (int)info.pitch;
hw_fb = (uint32_t*)montauk::fb_map();
back_buf = (uint32_t*)montauk::alloc((uint64_t)fb_height * fb_pitch);
}
int width() const { return fb_width; }
int height() const { return fb_height; }
int pitch() const { return fb_pitch; }
uint32_t* buffer() { return back_buf; }
inline void put_pixel(int x, int y, Color c) {
if (x < 0 || x >= fb_width || y < 0 || y >= fb_height) return;
uint32_t* row = (uint32_t*)((uint8_t*)back_buf + y * fb_pitch);
row[x] = c.to_pixel();
}
inline void put_pixel_alpha(int x, int y, Color c) {
if (x < 0 || x >= fb_width || y < 0 || y >= fb_height) return;
if (c.a == 0) return;
if (c.a == 255) {
put_pixel(x, y, c);
return;
}
uint32_t* row = (uint32_t*)((uint8_t*)back_buf + y * fb_pitch);
uint32_t dst = row[x];
uint8_t dr = (dst >> 16) & 0xFF;
uint8_t dg = (dst >> 8) & 0xFF;
uint8_t db = dst & 0xFF;
uint32_t a = c.a;
uint32_t inv_a = 255 - a;
// Fast alpha blend: out = (src * alpha + dst * (255 - alpha) + 128) / 255
// Approximation: (x + 1 + (x >> 8)) >> 8 for division by 255
uint32_t rr = a * c.r + inv_a * dr;
uint32_t gg = a * c.g + inv_a * dg;
uint32_t bb = a * c.b + inv_a * db;
rr = (rr + 1 + (rr >> 8)) >> 8;
gg = (gg + 1 + (gg >> 8)) >> 8;
bb = (bb + 1 + (bb >> 8)) >> 8;
row[x] = (0xFF000000) | (rr << 16) | (gg << 8) | bb;
}
inline void fill_rect(int x, int y, int w, int h, Color c) {
// Clip to screen bounds
int x0 = x < 0 ? 0 : x;
int y0 = y < 0 ? 0 : y;
int x1 = (x + w) > fb_width ? fb_width : (x + w);
int y1 = (y + h) > fb_height ? fb_height : (y + h);
if (x0 >= x1 || y0 >= y1) return;
uint32_t pixel = c.to_pixel();
int clipped_w = x1 - x0;
for (int row = y0; row < y1; row++) {
uint32_t* dst = (uint32_t*)((uint8_t*)back_buf + row * fb_pitch) + x0;
for (int col = 0; col < clipped_w; col++) {
dst[col] = pixel;
}
}
}
inline void fill_rect_alpha(int x, int y, int w, int h, Color c) {
if (c.a == 0) return;
if (c.a == 255) {
fill_rect(x, y, w, h, c);
return;
}
int x0 = x < 0 ? 0 : x;
int y0 = y < 0 ? 0 : y;
int x1 = (x + w) > fb_width ? fb_width : (x + w);
int y1 = (y + h) > fb_height ? fb_height : (y + h);
if (x0 >= x1 || y0 >= y1) return;
uint32_t a = c.a;
uint32_t inv_a = 255 - a;
uint32_t src_r = a * c.r;
uint32_t src_g = a * c.g;
uint32_t src_b = a * c.b;
for (int row = y0; row < y1; row++) {
uint32_t* dst = (uint32_t*)((uint8_t*)back_buf + row * fb_pitch) + x0;
for (int col = 0; col < x1 - x0; col++) {
uint32_t d = dst[col];
uint32_t dr = (d >> 16) & 0xFF;
uint32_t dg = (d >> 8) & 0xFF;
uint32_t db = d & 0xFF;
uint32_t rr = src_r + inv_a * dr;
uint32_t gg = src_g + inv_a * dg;
uint32_t bb = src_b + inv_a * db;
rr = (rr + 1 + (rr >> 8)) >> 8;
gg = (gg + 1 + (gg >> 8)) >> 8;
bb = (bb + 1 + (bb >> 8)) >> 8;
dst[col] = (0xFF000000) | (rr << 16) | (gg << 8) | bb;
}
}
}
inline void blit(int x, int y, int w, int h, const uint32_t* pixels) {
for (int row = 0; row < h; row++) {
int dy = y + row;
if (dy < 0 || dy >= fb_height) continue;
for (int col = 0; col < w; col++) {
int dx = x + col;
if (dx < 0 || dx >= fb_width) continue;
uint32_t* dst_row = (uint32_t*)((uint8_t*)back_buf + dy * fb_pitch);
dst_row[dx] = pixels[row * w + col];
}
}
}
inline void blit_alpha(int x, int y, int w, int h, const uint32_t* pixels) {
for (int row = 0; row < h; row++) {
int dy = y + row;
if (dy < 0 || dy >= fb_height) continue;
for (int col = 0; col < w; col++) {
int dx = x + col;
if (dx < 0 || dx >= fb_width) continue;
uint32_t src = pixels[row * w + col];
uint8_t sa = (src >> 24) & 0xFF;
if (sa == 0) continue;
uint8_t sr = (src >> 16) & 0xFF;
uint8_t sg = (src >> 8) & 0xFF;
uint8_t sb = src & 0xFF;
if (sa == 255) {
uint32_t* dst_row = (uint32_t*)((uint8_t*)back_buf + dy * fb_pitch);
dst_row[dx] = src;
continue;
}
Color sc = {sr, sg, sb, sa};
put_pixel_alpha(dx, dy, sc);
}
}
}
inline void clear(Color c) {
fill_rect(0, 0, fb_width, fb_height, c);
}
inline void flip() {
// Copy back buffer to hardware framebuffer, row by row (pitch may differ)
int row_pixels = fb_width;
for (int y = 0; y < fb_height; y++) {
uint32_t* src = (uint32_t*)((uint8_t*)back_buf + y * fb_pitch);
uint32_t* dst = (uint32_t*)((uint8_t*)hw_fb + y * fb_pitch);
for (int x = 0; x < row_pixels; x++) {
dst[x] = src[x];
}
}
}
};
} // namespace gui
+94
View File
@@ -0,0 +1,94 @@
/*
* gui.hpp
* MontaukOS core GUI types and utilities
* Copyright (c) 2025 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace gui {
// 16.16 fixed-point type
using fixed_t = int32_t;
static constexpr int FIXED_SHIFT = 16;
inline fixed_t int_to_fixed(int v) { return v << FIXED_SHIFT; }
inline int fixed_to_int(fixed_t v) { return v >> FIXED_SHIFT; }
inline fixed_t fixed_mul(fixed_t a, fixed_t b) { return (int32_t)(((int64_t)a * b) >> FIXED_SHIFT); }
inline fixed_t fixed_div(fixed_t a, fixed_t b) { return (int32_t)(((int64_t)a << FIXED_SHIFT) / b); }
inline fixed_t fixed_from_parts(int whole, int frac_num, int frac_den) {
return int_to_fixed(whole) + (int32_t)(((int64_t)frac_num << FIXED_SHIFT) / frac_den);
}
struct Color {
uint8_t r, g, b, a;
static constexpr Color from_rgb(uint8_t r, uint8_t g, uint8_t b) { return {r, g, b, 255}; }
static constexpr Color from_rgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a) { return {r, g, b, a}; }
static constexpr Color from_hex(uint32_t hex) {
return {(uint8_t)((hex >> 16) & 0xFF), (uint8_t)((hex >> 8) & 0xFF), (uint8_t)(hex & 0xFF), 255};
}
constexpr uint32_t to_pixel() const { return ((uint32_t)a << 24) | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b; }
};
// Named colors for the desktop theme
namespace colors {
static constexpr Color DESKTOP_BG = {0xE0, 0xE0, 0xE0, 0xFF};
static constexpr Color PANEL_BG = {0x2B, 0x3E, 0x50, 0xFF};
static constexpr Color TITLEBAR_BG = {0xF5, 0xF5, 0xF5, 0xFF};
static constexpr Color WINDOW_BG = {0xFF, 0xFF, 0xFF, 0xFF};
static constexpr Color BORDER = {0xCC, 0xCC, 0xCC, 0xFF};
static constexpr Color TEXT_COLOR = {0x33, 0x33, 0x33, 0xFF};
static constexpr Color PANEL_TEXT = {0xFF, 0xFF, 0xFF, 0xFF};
static constexpr Color ACCENT = {0x36, 0x7B, 0xF0, 0xFF};
static constexpr Color CLOSE_BTN = {0xFF, 0x5F, 0x57, 0xFF};
static constexpr Color MAX_BTN = {0x28, 0xCA, 0x42, 0xFF};
static constexpr Color MIN_BTN = {0xFF, 0xBD, 0x2E, 0xFF};
static constexpr Color SHADOW = {0x00, 0x00, 0x00, 0x40};
static constexpr Color TRANSPARENT = {0x00, 0x00, 0x00, 0x00};
static constexpr Color BLACK = {0x00, 0x00, 0x00, 0xFF};
static constexpr Color WHITE = {0xFF, 0xFF, 0xFF, 0xFF};
static constexpr Color ICON_COLOR = {0x5C, 0x61, 0x6C, 0xFF};
static constexpr Color SCROLLBAR_BG = {0xF0, 0xF0, 0xF0, 0xFF};
static constexpr Color SCROLLBAR_FG = {0xC0, 0xC0, 0xC0, 0xFF};
static constexpr Color MENU_BG = {0xFF, 0xFF, 0xFF, 0xFF};
static constexpr Color MENU_HOVER = {0xE8, 0xF0, 0xFE, 0xFF};
static constexpr Color TERM_BG = {0x2D, 0x2D, 0x2D, 0xFF};
static constexpr Color TERM_FG = {0xCC, 0xCC, 0xCC, 0xFF};
static constexpr Color PANEL_INDICATOR_ACTIVE = {0x45, 0x58, 0x6A, 0xFF};
static constexpr Color PANEL_INDICATOR_INACTIVE = {0x35, 0x48, 0x5A, 0xFF};
}
struct Point {
int x, y;
};
struct Rect {
int x, y, w, h;
bool contains(int px, int py) const {
return px >= x && px < x + w && py >= y && py < y + h;
}
Rect intersect(const Rect& other) const {
int rx = x > other.x ? x : other.x;
int ry = y > other.y ? y : other.y;
int rx2 = (x + w) < (other.x + other.w) ? (x + w) : (other.x + other.w);
int ry2 = (y + h) < (other.y + other.h) ? (y + h) : (other.y + other.h);
if (rx2 <= rx || ry2 <= ry) return {0, 0, 0, 0};
return {rx, ry, rx2 - rx, ry2 - ry};
}
bool empty() const { return w <= 0 || h <= 0; }
};
// Simple inline utility functions
inline int gui_min(int a, int b) { return a < b ? a : b; }
inline int gui_max(int a, int b) { return a > b ? a : b; }
inline int gui_abs(int a) { return a < 0 ? -a : a; }
inline int gui_clamp(int v, int lo, int hi) { return v < lo ? lo : (v > hi ? hi : v); }
} // namespace gui
File diff suppressed because it is too large Load Diff
+95
View File
@@ -0,0 +1,95 @@
/*
* stb_math.h
* Math functions for stb_truetype in freestanding environment
* Copyright (c) 2026 Daniel Hammer
*/
#ifndef STB_MATH_H
#define STB_MATH_H
#ifdef __cplusplus
extern "C" {
#endif
static inline double stb_floor(double x) {
double i = (double)(long long)x;
return (x < i) ? i - 1.0 : i;
}
static inline double stb_ceil(double x) {
double f = stb_floor(x);
return (x > f) ? f + 1.0 : f;
}
static inline double stb_fabs(double x) {
return x < 0.0 ? -x : x;
}
static inline double stb_fmod(double x, double y) {
if (y == 0.0) return 0.0;
return x - (double)((long long)(x / y)) * y;
}
static inline double stb_sqrt(double x) {
if (x <= 0.0) return 0.0;
double guess = x;
for (int i = 0; i < 30; i++)
guess = (guess + x / guess) * 0.5;
return guess;
}
static inline double stb_pow(double base, double exp) {
if (exp == 0.0) return 1.0;
if (exp == 1.0) return base;
if (base == 0.0) return 0.0;
// Integer exponent fast path
if (exp == (double)(long long)exp) {
long long e = (long long)exp;
int neg = 0;
if (e < 0) { neg = 1; e = -e; }
double r = 1.0;
double b = base;
while (e > 0) {
if (e & 1) r *= b;
b *= b;
e >>= 1;
}
return neg ? 1.0 / r : r;
}
return 0.0;
}
static inline double stb_cos(double x) {
// Reduce to [0, 2*pi]
const double PI = 3.14159265358979323846;
const double TWO_PI = 6.28318530717958647692;
x = stb_fmod(stb_fabs(x), TWO_PI);
// Taylor series: cos(x) = 1 - x^2/2! + x^4/4! - x^6/6! + ...
double x2 = x * x;
double term = 1.0;
double result = 1.0;
for (int i = 1; i <= 10; i++) {
term *= -x2 / (double)((2 * i - 1) * (2 * i));
result += term;
}
return result;
}
static inline double stb_acos(double x) {
// Clamp input
if (x <= -1.0) return 3.14159265358979323846;
if (x >= 1.0) return 0.0;
// Polynomial approximation (Abramowitz & Stegun style)
double ax = stb_fabs(x);
double result = (-0.0187293 * ax + 0.0742610) * ax - 0.2121144;
result = (result * ax + 1.5707288) * stb_sqrt(1.0 - ax);
if (x < 0.0)
return 3.14159265358979323846 - result;
return result;
}
#ifdef __cplusplus
}
#endif
#endif // STB_MATH_H
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+688
View File
@@ -0,0 +1,688 @@
/*
* terminal.hpp
* MontaukOS terminal emulator with ANSI escape sequence support
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include "gui/gui.hpp"
#include "gui/font.hpp"
#include <montauk/syscall.h>
#include <montauk/string.h>
#include <Api/Syscall.hpp>
namespace gui {
struct TermCell {
char ch;
Color fg;
Color bg;
};
static constexpr int TERM_MAX_SCROLLBACK = 500;
struct TerminalState {
TermCell* cells;
TermCell* alt_cells; // alternate screen buffer
int cols, rows;
int cursor_x, cursor_y;
int saved_cursor_x, saved_cursor_y; // saved cursor for alternate screen
int scrollback_lines; // lines of history above visible screen (0..max_scrollback)
int max_scrollback; // 0 for viewers (klog), TERM_MAX_SCROLLBACK for terminal
int view_offset; // how many rows scrolled back (0 = live view)
int child_pid;
void* desktop; // DesktopState* for closing window on child exit
Color current_fg;
Color current_bg;
bool cursor_visible;
bool alt_screen_active;
bool reverse_video;
bool dirty; // true when content changed since last render
enum { STATE_NORMAL, STATE_ESC, STATE_CSI } parse_state;
bool csi_private; // true if '?' was seen after CSI
int csi_params[8];
int csi_param_count;
int csi_current_param;
};
// Standard ANSI color palette as ARGB pixels
static inline Color term_ansi_color(int idx) {
switch (idx) {
case 0: return Color::from_hex(0x000000);
case 1: return Color::from_hex(0xCC0000);
case 2: return Color::from_hex(0x4E9A06);
case 3: return Color::from_hex(0xC4A000);
case 4: return Color::from_hex(0x3465A4);
case 5: return Color::from_hex(0x75507B);
case 6: return Color::from_hex(0x06989A);
case 7: return Color::from_hex(0xD3D7CF);
case 8: return Color::from_hex(0x555753);
case 9: return Color::from_hex(0xEF2929);
case 10: return Color::from_hex(0x8AE234);
case 11: return Color::from_hex(0xFCE94F);
case 12: return Color::from_hex(0x729FCF);
case 13: return Color::from_hex(0xAD7FA8);
case 14: return Color::from_hex(0x34E2E2);
case 15: return Color::from_hex(0xEEEEEC);
default: return colors::TERM_FG;
}
}
// Helper: pointer to start of screen-relative row r in the cells buffer
static inline TermCell* term_screen_row(TerminalState* t, int r) {
return &t->cells[(t->scrollback_lines + r) * t->cols];
}
static inline void terminal_scroll_up(TerminalState* t) {
if (!t->alt_screen_active && t->scrollback_lines < t->max_scrollback) {
// Room for scrollback: top visible row becomes scrollback, no data movement
t->scrollback_lines++;
} else if (!t->alt_screen_active && t->max_scrollback > 0) {
// Scrollback full: discard oldest line, shift entire buffer up by one row
int total = (t->max_scrollback + t->rows - 1) * t->cols * sizeof(TermCell);
montauk::memmove(t->cells, t->cells + t->cols, total);
} else {
// Alt screen or no scrollback (klog): shift visible area up
TermCell* screen = term_screen_row(t, 0);
montauk::memmove(screen, screen + t->cols, (t->rows - 1) * t->cols * sizeof(TermCell));
}
// Clear the new bottom visible row
TermCell* bottom = term_screen_row(t, t->rows - 1);
for (int c = 0; c < t->cols; c++) {
bottom[c] = {' ', t->current_fg, colors::TERM_BG};
}
// Keep user's scrolled-back viewport stable
if (t->view_offset > 0) {
t->view_offset++;
if (t->view_offset > t->scrollback_lines)
t->view_offset = t->scrollback_lines;
}
}
// Initialize only the cell grid (no child process). Used by viewers like klog.
// max_sb = 0 for viewers, TERM_MAX_SCROLLBACK for the real terminal.
static inline void terminal_init_cells(TerminalState* t, int cols, int rows, int max_sb = 0) {
t->cols = cols;
t->rows = rows;
t->cursor_x = 0;
t->cursor_y = 0;
t->saved_cursor_x = 0;
t->saved_cursor_y = 0;
t->scrollback_lines = 0;
t->max_scrollback = max_sb;
t->view_offset = 0;
t->current_fg = colors::TERM_FG;
t->current_bg = colors::TERM_BG;
t->cursor_visible = false;
t->alt_screen_active = false;
t->reverse_video = false;
t->dirty = true;
t->parse_state = TerminalState::STATE_NORMAL;
t->csi_private = false;
t->csi_param_count = 0;
t->csi_current_param = 0;
t->child_pid = 0;
int total_cells = (rows + max_sb) * cols;
int screen_cells = rows * cols;
t->cells = (TermCell*)montauk::alloc(total_cells * sizeof(TermCell));
t->alt_cells = (TermCell*)montauk::alloc(screen_cells * sizeof(TermCell));
if (!t->cells || !t->alt_cells) {
// Allocation failed — leave terminal in a safe but unusable state
if (t->cells) { montauk::free(t->cells); t->cells = nullptr; }
if (t->alt_cells) { montauk::free(t->alt_cells); t->alt_cells = nullptr; }
t->cols = 0; t->rows = 0;
return;
}
for (int i = 0; i < total_cells; i++) {
t->cells[i] = {' ', colors::TERM_FG, colors::TERM_BG};
}
for (int i = 0; i < screen_cells; i++) {
t->alt_cells[i] = {' ', colors::TERM_FG, colors::TERM_BG};
}
}
static inline void terminal_init(TerminalState* t, int cols, int rows) {
terminal_init_cells(t, cols, rows, TERM_MAX_SCROLLBACK);
t->cursor_visible = true;
t->child_pid = montauk::spawn_redir("0:/os/shell.elf");
if (t->child_pid > 0)
montauk::childio_settermsz(t->child_pid, cols, rows);
}
static inline void terminal_put_char(TerminalState* t, char ch) {
if (t->cursor_x >= t->cols) {
t->cursor_x = 0;
t->cursor_y++;
}
if (t->cursor_y >= t->rows) {
terminal_scroll_up(t);
t->cursor_y = t->rows - 1;
}
TermCell* row = term_screen_row(t, t->cursor_y);
row[t->cursor_x].ch = ch;
row[t->cursor_x].fg = t->current_fg;
row[t->cursor_x].bg = t->current_bg;
t->cursor_x++;
}
static inline void terminal_enter_alt_screen(TerminalState* t) {
if (t->alt_screen_active) return;
t->alt_screen_active = true;
t->dirty = true;
// Save cursor
t->saved_cursor_x = t->cursor_x;
t->saved_cursor_y = t->cursor_y;
// Save visible screen to alt_cells, clear visible screen
int total = t->cols * t->rows;
TermCell* screen = term_screen_row(t, 0);
for (int i = 0; i < total; i++) {
t->alt_cells[i] = screen[i];
screen[i] = {' ', colors::TERM_FG, colors::TERM_BG};
}
t->view_offset = 0;
t->cursor_x = 0;
t->cursor_y = 0;
}
static inline void terminal_exit_alt_screen(TerminalState* t) {
if (!t->alt_screen_active) return;
t->alt_screen_active = false;
t->dirty = true;
// Restore visible screen from alt_cells
int total = t->cols * t->rows;
TermCell* screen = term_screen_row(t, 0);
for (int i = 0; i < total; i++) {
screen[i] = t->alt_cells[i];
}
// Restore cursor
t->cursor_x = t->saved_cursor_x;
t->cursor_y = t->saved_cursor_y;
}
static inline void terminal_process_private_mode(TerminalState* t, char cmd) {
int p0 = t->csi_param_count > 0 ? t->csi_params[0] : 0;
if (cmd == 'h') {
// Set private mode
if (p0 == 25) {
t->cursor_visible = true;
} else if (p0 == 1049) {
terminal_enter_alt_screen(t);
}
} else if (cmd == 'l') {
// Reset private mode
if (p0 == 25) {
t->cursor_visible = false;
} else if (p0 == 1049) {
terminal_exit_alt_screen(t);
}
}
}
static inline void terminal_process_csi(TerminalState* t, char cmd) {
// Finalize current param
if (t->csi_param_count < 8) {
t->csi_params[t->csi_param_count] = t->csi_current_param;
t->csi_param_count++;
}
// Handle private mode sequences (ESC[?...)
if (t->csi_private) {
terminal_process_private_mode(t, cmd);
return;
}
int p0 = t->csi_param_count > 0 ? t->csi_params[0] : 0;
int p1 = t->csi_param_count > 1 ? t->csi_params[1] : 0;
switch (cmd) {
case 'H': case 'f': {
// Cursor position: ESC[row;colH (1-based)
int row = (p0 > 0 ? p0 : 1) - 1;
int col = (p1 > 0 ? p1 : 1) - 1;
if (row < 0) row = 0;
if (row >= t->rows) row = t->rows - 1;
if (col < 0) col = 0;
if (col >= t->cols) col = t->cols - 1;
t->cursor_y = row;
t->cursor_x = col;
break;
}
case 'A': {
// Cursor up
int n = p0 > 0 ? p0 : 1;
t->cursor_y -= n;
if (t->cursor_y < 0) t->cursor_y = 0;
break;
}
case 'B': {
// Cursor down
int n = p0 > 0 ? p0 : 1;
t->cursor_y += n;
if (t->cursor_y >= t->rows) t->cursor_y = t->rows - 1;
break;
}
case 'C': {
// Cursor forward
int n = p0 > 0 ? p0 : 1;
t->cursor_x += n;
if (t->cursor_x >= t->cols) t->cursor_x = t->cols - 1;
break;
}
case 'D': {
// Cursor backward
int n = p0 > 0 ? p0 : 1;
t->cursor_x -= n;
if (t->cursor_x < 0) t->cursor_x = 0;
break;
}
case 'J': {
// Erase in display
if (p0 == 0) {
// Clear from cursor to end
TermCell* row = term_screen_row(t, t->cursor_y);
for (int x = t->cursor_x; x < t->cols; x++)
row[x] = {' ', t->current_fg, colors::TERM_BG};
for (int r = t->cursor_y + 1; r < t->rows; r++) {
TermCell* rp = term_screen_row(t, r);
for (int c = 0; c < t->cols; c++)
rp[c] = {' ', t->current_fg, colors::TERM_BG};
}
} else if (p0 == 1) {
// Clear from start to cursor
for (int r = 0; r < t->cursor_y; r++) {
TermCell* rp = term_screen_row(t, r);
for (int c = 0; c < t->cols; c++)
rp[c] = {' ', t->current_fg, colors::TERM_BG};
}
TermCell* row = term_screen_row(t, t->cursor_y);
for (int x = 0; x <= t->cursor_x; x++)
row[x] = {' ', t->current_fg, colors::TERM_BG};
} else if (p0 == 2) {
// Clear entire screen
for (int r = 0; r < t->rows; r++) {
TermCell* rp = term_screen_row(t, r);
for (int c = 0; c < t->cols; c++)
rp[c] = {' ', t->current_fg, colors::TERM_BG};
}
t->cursor_x = 0;
t->cursor_y = 0;
}
break;
}
case 'K': {
// Erase in line
int start = 0, end = t->cols;
if (p0 == 0) { start = t->cursor_x; end = t->cols; }
else if (p0 == 1) { start = 0; end = t->cursor_x + 1; }
else if (p0 == 2) { start = 0; end = t->cols; }
TermCell* row = term_screen_row(t, t->cursor_y);
for (int x = start; x < end; x++)
row[x] = {' ', t->current_fg, colors::TERM_BG};
break;
}
case 'm': {
// SGR - Set Graphics Rendition
for (int i = 0; i < t->csi_param_count; i++) {
int code = t->csi_params[i];
if (code == 0) {
t->current_fg = colors::TERM_FG;
t->current_bg = colors::TERM_BG;
t->reverse_video = false;
} else if (code == 1) {
// Bold: map to bright version of current color
uint8_t r = t->current_fg.r;
uint8_t g = t->current_fg.g;
uint8_t b = t->current_fg.b;
int add = 50;
r = (r + add > 255) ? 255 : r + add;
g = (g + add > 255) ? 255 : g + add;
b = (b + add > 255) ? 255 : b + add;
t->current_fg = Color::from_rgb(r, g, b);
} else if (code == 2) {
// Dim: darken current fg color
t->current_fg.r = t->current_fg.r / 2;
t->current_fg.g = t->current_fg.g / 2;
t->current_fg.b = t->current_fg.b / 2;
} else if (code == 7) {
// Reverse video
if (!t->reverse_video) {
t->reverse_video = true;
Color tmp = t->current_fg;
t->current_fg = t->current_bg;
t->current_bg = tmp;
}
} else if (code == 27) {
// Reverse off
if (t->reverse_video) {
t->reverse_video = false;
Color tmp = t->current_fg;
t->current_fg = t->current_bg;
t->current_bg = tmp;
}
} else if (code >= 30 && code <= 37) {
t->current_fg = term_ansi_color(code - 30);
if (t->reverse_video) {
// In reverse mode, fg is displayed as bg
Color tmp = t->current_fg;
t->current_fg = t->current_bg;
t->current_bg = tmp;
}
} else if (code >= 40 && code <= 47) {
t->current_bg = term_ansi_color(code - 40);
} else if (code >= 90 && code <= 97) {
t->current_fg = term_ansi_color(code - 90 + 8);
} else if (code >= 100 && code <= 107) {
t->current_bg = term_ansi_color(code - 100 + 8);
} else if (code == 39) {
t->current_fg = colors::TERM_FG;
} else if (code == 49) {
t->current_bg = colors::TERM_BG;
}
}
if (t->csi_param_count == 0) {
// ESC[m with no params = reset
t->current_fg = colors::TERM_FG;
t->current_bg = colors::TERM_BG;
t->reverse_video = false;
}
break;
}
default:
break;
}
}
static inline void terminal_feed(TerminalState* t, const char* data, int len) {
if (len > 0) t->dirty = true;
for (int i = 0; i < len; i++) {
char ch = data[i];
switch (t->parse_state) {
case TerminalState::STATE_NORMAL:
if (ch == '\033') {
t->parse_state = TerminalState::STATE_ESC;
} else if (ch == '\n') {
t->cursor_x = 0; // CR+LF: shell sends \n without \r
t->cursor_y++;
if (t->cursor_y >= t->rows) {
terminal_scroll_up(t);
t->cursor_y = t->rows - 1;
}
} else if (ch == '\r') {
t->cursor_x = 0;
} else if (ch == '\b') {
if (t->cursor_x > 0) t->cursor_x--;
} else if (ch == '\t') {
int next = (t->cursor_x + 8) & ~7;
if (next > t->cols) next = t->cols;
while (t->cursor_x < next) {
terminal_put_char(t, ' ');
}
} else if (ch >= 32 || ch < 0) {
// Printable character (also treat high-bit chars as printable)
terminal_put_char(t, ch);
}
break;
case TerminalState::STATE_ESC:
if (ch == '[') {
t->parse_state = TerminalState::STATE_CSI;
t->csi_private = false;
t->csi_param_count = 0;
t->csi_current_param = 0;
for (int j = 0; j < 8; j++) t->csi_params[j] = 0;
} else if (ch == 'c') {
// Reset terminal
t->current_fg = colors::TERM_FG;
t->current_bg = colors::TERM_BG;
t->cursor_x = 0;
t->cursor_y = 0;
t->parse_state = TerminalState::STATE_NORMAL;
} else {
// Unknown ESC sequence, ignore
t->parse_state = TerminalState::STATE_NORMAL;
}
break;
case TerminalState::STATE_CSI:
if (ch >= '0' && ch <= '9') {
t->csi_current_param = t->csi_current_param * 10 + (ch - '0');
} else if (ch == ';') {
if (t->csi_param_count < 8) {
t->csi_params[t->csi_param_count] = t->csi_current_param;
t->csi_param_count++;
}
t->csi_current_param = 0;
} else if (ch == '?') {
t->csi_private = true;
} else if (ch >= 0x40 && ch <= 0x7E) {
// Final byte - execute command
terminal_process_csi(t, ch);
t->parse_state = TerminalState::STATE_NORMAL;
} else {
// Unknown, abort CSI
t->parse_state = TerminalState::STATE_NORMAL;
}
break;
}
}
}
static inline void terminal_render(TerminalState* t, uint32_t* pixels, int pw, int ph) {
if (!t->dirty || !t->cells) return;
t->dirty = false;
int cell_w = mono_cell_width();
int cell_h = mono_cell_height();
bool use_ttf = fonts::mono && fonts::mono->valid;
GlyphCache* gc = use_ttf ? fonts::mono->get_cache(fonts::TERM_SIZE) : nullptr;
// Fill background using row-copy: fill first row, then memcpy to the rest
uint32_t bg_px = colors::TERM_BG.to_pixel();
int row_bytes = pw * sizeof(uint32_t);
for (int i = 0; i < pw; i++) pixels[i] = bg_px;
for (int r = 1; r < ph; r++) {
montauk::memcpy(&pixels[r * pw], pixels, row_bytes);
}
// Determine which rows of the buffer to display
int base_row = t->scrollback_lines - t->view_offset;
if (base_row < 0) base_row = 0;
// Render each visible cell
int visible_rows = ph / cell_h;
int visible_cols = pw / cell_w;
if (visible_rows > t->rows) visible_rows = t->rows;
if (visible_cols > t->cols) visible_cols = t->cols;
for (int r = 0; r < visible_rows; r++) {
int py = r * cell_h;
int src_row = base_row + r;
for (int c = 0; c < visible_cols; c++) {
int idx = src_row * t->cols + c;
TermCell& cell = t->cells[idx];
int px = c * cell_w;
// Only draw cell background if it differs from terminal bg
uint32_t cell_bg = cell.bg.to_pixel();
if (cell_bg != bg_px) {
for (int fy = 0; fy < cell_h && py + fy < ph; fy++) {
uint32_t* row = &pixels[(py + fy) * pw + px];
for (int fx = 0; fx < cell_w && px + fx < pw; fx++) {
row[fx] = cell_bg;
}
}
}
// Draw character glyph
if (cell.ch > 32 || cell.ch < 0) {
if (use_ttf) {
int baseline = py + gc->ascent;
fonts::mono->draw_char_to_buffer(pixels, pw, ph,
px, baseline, (unsigned char)cell.ch, cell.fg, gc);
} else {
uint32_t cell_fg = cell.fg.to_pixel();
const uint8_t* glyph = &font_data[(unsigned char)cell.ch * FONT_HEIGHT];
for (int fy = 0; fy < FONT_HEIGHT; fy++) {
int dy = py + fy;
if (dy >= ph) break;
uint8_t bits = glyph[fy];
for (int fx = 0; fx < FONT_WIDTH; fx++) {
if (bits & (0x80 >> fx)) {
int dx = px + fx;
if (dx >= pw) break;
pixels[dy * pw + dx] = cell_fg;
}
}
}
}
}
}
}
// Draw cursor (only when viewing live position)
if (t->view_offset == 0 && t->cursor_visible &&
t->cursor_x < visible_cols && t->cursor_y < visible_rows) {
int cx = t->cursor_x * cell_w;
int cy = t->cursor_y * cell_h;
uint32_t cursor_px = colors::WHITE.to_pixel();
for (int fy = 0; fy < cell_h; fy++) {
int dy = cy + fy;
if (dy >= ph) break;
for (int fx = 0; fx < cell_w; fx++) {
int dx = cx + fx;
if (dx >= pw) break;
pixels[dy * pw + dx] = cursor_px;
}
}
// Draw character on top of cursor in black
if (t->cursor_y < t->rows && t->cursor_x < t->cols) {
TermCell* row = term_screen_row(t, t->cursor_y);
char ch = row[t->cursor_x].ch;
if (ch > 32 || ch < 0) {
if (use_ttf) {
int baseline = cy + gc->ascent;
fonts::mono->draw_char_to_buffer(pixels, pw, ph,
cx, baseline, (unsigned char)ch, colors::BLACK, gc);
} else {
const uint8_t* glyph = &font_data[(unsigned char)ch * FONT_HEIGHT];
uint32_t black_px = colors::BLACK.to_pixel();
for (int fy = 0; fy < FONT_HEIGHT; fy++) {
int dy = cy + fy;
if (dy >= ph) break;
uint8_t bits = glyph[fy];
for (int fx = 0; fx < FONT_WIDTH; fx++) {
if (bits & (0x80 >> fx)) {
int dx = cx + fx;
if (dx >= pw) break;
pixels[dy * pw + dx] = black_px;
}
}
}
}
}
}
}
}
static inline void terminal_resize(TerminalState* t, int new_cols, int new_rows) {
if (new_cols == t->cols && new_rows == t->rows) return;
if (new_cols < 1 || new_rows < 1) return;
t->dirty = true;
int new_capacity = new_rows + t->max_scrollback;
int new_total = new_capacity * new_cols;
TermCell* new_cells = (TermCell*)montauk::alloc(new_total * sizeof(TermCell));
TermCell* new_alt = (TermCell*)montauk::alloc(new_rows * new_cols * sizeof(TermCell));
if (!new_cells || !new_alt) {
if (new_cells) montauk::free(new_cells);
if (new_alt) montauk::free(new_alt);
return; // keep existing buffers
}
// Clear new buffers
for (int i = 0; i < new_total; i++)
new_cells[i] = {' ', colors::TERM_FG, colors::TERM_BG};
for (int i = 0; i < new_rows * new_cols; i++)
new_alt[i] = {' ', colors::TERM_FG, colors::TERM_BG};
// Copy content: scrollback + visible screen
int old_content = t->scrollback_lines + t->rows;
int keep = old_content < new_capacity ? old_content : new_capacity;
int discard = old_content - keep;
int copy_cols = t->cols < new_cols ? t->cols : new_cols;
for (int r = 0; r < keep; r++) {
for (int c = 0; c < copy_cols; c++) {
new_cells[r * new_cols + c] = t->cells[(discard + r) * t->cols + c];
}
}
int new_scrollback = keep - new_rows;
if (new_scrollback < 0) new_scrollback = 0;
// Adjust cursor
int abs_cursor_y = t->scrollback_lines + t->cursor_y - discard;
int new_cursor_y = abs_cursor_y - new_scrollback;
if (new_cursor_y < 0) new_cursor_y = 0;
if (new_cursor_y >= new_rows) new_cursor_y = new_rows - 1;
int new_cursor_x = t->cursor_x < new_cols ? t->cursor_x : new_cols - 1;
if (t->cells) montauk::free(t->cells);
if (t->alt_cells) montauk::free(t->alt_cells);
t->cells = new_cells;
t->alt_cells = new_alt;
t->cols = new_cols;
t->rows = new_rows;
t->scrollback_lines = new_scrollback;
t->cursor_x = new_cursor_x;
t->cursor_y = new_cursor_y;
// Clamp view offset
if (t->view_offset > t->scrollback_lines)
t->view_offset = t->scrollback_lines;
// Notify child process of new terminal size
if (t->child_pid > 0) {
montauk::childio_settermsz(t->child_pid, new_cols, new_rows);
}
}
static inline void terminal_handle_key(TerminalState* t, const Montauk::KeyEvent& key) {
// Snap to live on any keyboard input
if (t->view_offset > 0) {
t->view_offset = 0;
t->dirty = true;
}
if (t->child_pid > 0) {
montauk::childio_writekey(t->child_pid, &key);
}
}
// Returns false if the child process has exited
static inline bool terminal_poll(TerminalState* t) {
if (t->child_pid <= 0) return false;
char buf[4096];
// Drain all available data so large output renders in one frame
for (;;) {
int n = montauk::childio_read(t->child_pid, buf, sizeof(buf));
if (n > 0) {
terminal_feed(t, buf, n);
} else {
// n == -1 means child process is gone; n == 0 means no data yet
if (n < 0) { t->child_pid = 0; return false; }
break;
}
}
return true;
}
} // namespace gui
+409
View File
@@ -0,0 +1,409 @@
/*
* truetype.hpp
* MontaukOS TrueType font rendering via stb_truetype
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <montauk/syscall.h>
#include <montauk/heap.h>
#include <montauk/string.h>
#include "gui/gui.hpp"
#include "gui/framebuffer.hpp"
// Forward-declare stbtt_fontinfo to avoid including stb_truetype.h in every TU.
// The actual struct is defined in stb_truetype.h and only used in truetype.hpp
// method bodies (which are inline but only instantiated where stb_truetype.h
// is also included — i.e. stb_truetype_impl.cpp). We include the full header
// here since our inline methods need the complete type.
#include "gui/stb_math.h"
// We need the stb macros defined before including stb_truetype.h (header-only mode)
#ifndef STBTT_ifloor
#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), montauk::malloc(x))
#define STBTT_free(x,u) ((void)(u), montauk::mfree(x))
#define STBTT_memcpy(d,s,n) montauk::memcpy(d,s,n)
#define STBTT_memset(d,v,n) montauk::memset(d,v,n)
#define STBTT_strlen(x) montauk::slen(x)
#define STBTT_assert(x) ((void)(x))
#endif
#include "gui/stb_truetype.h"
namespace gui {
struct CachedGlyph {
uint8_t* bitmap;
int width, height;
int xoff, yoff;
int advance;
bool loaded;
};
struct GlyphCache {
CachedGlyph glyphs[256]; // Latin-1 Supplement (U+0080..U+00FF) included
int pixel_size;
float scale;
int ascent, descent, line_gap;
int line_height;
};
struct TrueTypeFont {
stbtt_fontinfo info;
uint8_t* data;
GlyphCache caches[4];
int cache_count;
bool valid;
bool em_scaling; // true for PDF embedded fonts: scale by em square, not ascent-descent
bool init(const char* vfs_path) {
valid = false;
em_scaling = false;
data = nullptr;
cache_count = 0;
int fd = montauk::open(vfs_path);
if (fd < 0) return false;
uint64_t size = montauk::getsize(fd);
if (size == 0 || size > 1024 * 1024) {
montauk::close(fd);
return false;
}
data = (uint8_t*)montauk::alloc(size);
if (!data) {
montauk::close(fd);
return false;
}
montauk::read(fd, data, 0, size);
montauk::close(fd);
if (!stbtt_InitFont(&info, data, stbtt_GetFontOffsetForIndex(data, 0))) {
montauk::free(data);
data = nullptr;
return false;
}
valid = true;
return true;
}
void init_cache(GlyphCache* gc, int pixel_size) {
// Free any existing glyph bitmaps
for (int i = 0; i < 256; i++) {
if (gc->glyphs[i].bitmap) {
montauk::mfree(gc->glyphs[i].bitmap);
}
gc->glyphs[i].bitmap = nullptr;
gc->glyphs[i].loaded = false;
}
gc->pixel_size = pixel_size;
gc->scale = em_scaling
? stbtt_ScaleForMappingEmToPixels(&info, (float)pixel_size)
: stbtt_ScaleForPixelHeight(&info, (float)pixel_size);
int asc, desc, lg;
stbtt_GetFontVMetrics(&info, &asc, &desc, &lg);
gc->ascent = (int)(asc * gc->scale);
gc->descent = (int)(desc * gc->scale);
gc->line_gap = (int)(lg * gc->scale);
gc->line_height = gc->ascent - gc->descent + gc->line_gap;
}
GlyphCache* get_cache(int pixel_size) {
// Search existing caches
for (int i = 0; i < cache_count; i++) {
if (caches[i].pixel_size == pixel_size)
return &caches[i];
}
// Use a free slot if available
if (cache_count < 4) {
GlyphCache* gc = &caches[cache_count++];
init_cache(gc, pixel_size);
return gc;
}
// Evict oldest cache (slot 0) — free its glyph bitmaps
for (int g = 0; g < 256; g++) {
if (caches[0].glyphs[g].bitmap)
montauk::mfree(caches[0].glyphs[g].bitmap);
}
// Shift remaining caches down
for (int i = 0; i < 3; i++)
montauk::memcpy(&caches[i], &caches[i + 1], sizeof(GlyphCache));
montauk::memset(&caches[3], 0, sizeof(GlyphCache));
init_cache(&caches[3], pixel_size);
return &caches[3];
}
CachedGlyph* get_glyph(GlyphCache* gc, int codepoint) {
if (codepoint < 0 || codepoint >= 256) return nullptr;
CachedGlyph* g = &gc->glyphs[codepoint];
if (g->loaded) return g;
g->loaded = true;
int advance, lsb;
stbtt_GetCodepointHMetrics(&info, codepoint, &advance, &lsb);
g->advance = (int)(advance * gc->scale);
int x0, y0, x1, y1;
stbtt_GetCodepointBitmapBox(&info, codepoint, gc->scale, gc->scale,
&x0, &y0, &x1, &y1);
g->width = x1 - x0;
g->height = y1 - y0;
g->xoff = x0;
g->yoff = y0;
if (g->width > 0 && g->height > 0) {
g->bitmap = (uint8_t*)montauk::malloc(g->width * g->height);
stbtt_MakeCodepointBitmap(&info, g->bitmap, g->width, g->height,
g->width, gc->scale, gc->scale, codepoint);
}
return g;
}
int measure_text(const char* text, int pixel_size) {
if (!valid) return 0;
GlyphCache* gc = get_cache(pixel_size);
int w = 0;
for (int i = 0; text[i]; i++) {
CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]);
if (g) w += g->advance;
}
return w;
}
int get_line_height(int pixel_size) {
if (!valid) return 16;
GlyphCache* gc = get_cache(pixel_size);
return gc->line_height;
}
void draw(Framebuffer& fb, int x, int y, const char* text,
Color color, int pixel_size) {
if (!valid) return;
GlyphCache* gc = get_cache(pixel_size);
int cx = x;
int baseline = y + gc->ascent;
for (int i = 0; text[i]; i++) {
CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]);
if (!g) continue;
if (g->bitmap) {
int gx = cx + g->xoff;
int gy = baseline + g->yoff;
for (int row = 0; row < g->height; row++) {
for (int col = 0; col < g->width; col++) {
uint8_t alpha = g->bitmap[row * g->width + col];
if (alpha > 0) {
Color c = {color.r, color.g, color.b, alpha};
fb.put_pixel_alpha(gx + col, gy + row, c);
}
}
}
}
cx += g->advance;
}
}
void draw_bg(Framebuffer& fb, int x, int y, const char* text,
Color fg, Color bg, int pixel_size) {
if (!valid) return;
GlyphCache* gc = get_cache(pixel_size);
// Fill background for the text extent
int tw = measure_text(text, pixel_size);
fb.fill_rect(x, y, tw, gc->line_height, bg);
// Then draw foreground
draw(fb, x, y, text, fg, pixel_size);
}
void draw_to_buffer(uint32_t* pixels, int buf_w, int buf_h,
int x, int y, const char* text,
Color color, int pixel_size) {
if (!valid) return;
GlyphCache* gc = get_cache(pixel_size);
int cx = x;
int baseline = y + gc->ascent;
for (int i = 0; text[i]; i++) {
CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]);
if (!g) continue;
if (g->bitmap) {
int gx = cx + g->xoff;
int gy = baseline + g->yoff;
for (int row = 0; row < g->height; row++) {
int dy = gy + row;
if (dy < 0 || dy >= buf_h) continue;
for (int col = 0; col < g->width; col++) {
int dx = gx + col;
if (dx < 0 || dx >= buf_w) continue;
uint8_t alpha = g->bitmap[row * g->width + col];
if (alpha == 0) continue;
if (alpha == 255) {
pixels[dy * buf_w + dx] =
0xFF000000 | ((uint32_t)color.r << 16) |
((uint32_t)color.g << 8) | color.b;
} else {
uint32_t dst = pixels[dy * buf_w + dx];
uint8_t dr = (dst >> 16) & 0xFF;
uint8_t dg = (dst >> 8) & 0xFF;
uint8_t db = dst & 0xFF;
uint32_t a = alpha, inv_a = 255 - alpha;
uint32_t rr = (a * color.r + inv_a * dr + 128) / 255;
uint32_t gg = (a * color.g + inv_a * dg + 128) / 255;
uint32_t bb = (a * color.b + inv_a * db + 128) / 255;
pixels[dy * buf_w + dx] =
0xFF000000 | (rr << 16) | (gg << 8) | bb;
}
}
}
}
cx += g->advance;
}
}
// Draw text to buffer with clip rectangle (pixels outside clip_x..clip_x+clip_w are not drawn)
void draw_to_buffer_clipped(uint32_t* pixels, int buf_w, int buf_h,
int x, int y, const char* text,
Color color, int pixel_size,
int clip_x, int clip_y, int clip_w, int clip_h) {
if (!valid) return;
GlyphCache* gc = get_cache(pixel_size);
int cx = x;
int baseline = y + gc->ascent;
int clip_x2 = clip_x + clip_w;
int clip_y2 = clip_y + clip_h;
for (int i = 0; text[i]; i++) {
CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]);
if (!g) continue;
if (g->bitmap) {
int gx = cx + g->xoff;
int gy = baseline + g->yoff;
for (int row = 0; row < g->height; row++) {
int dy = gy + row;
if (dy < clip_y || dy >= clip_y2 || dy < 0 || dy >= buf_h) continue;
for (int col = 0; col < g->width; col++) {
int dx = gx + col;
if (dx < clip_x || dx >= clip_x2 || dx < 0 || dx >= buf_w) continue;
uint8_t alpha = g->bitmap[row * g->width + col];
if (alpha == 0) continue;
if (alpha == 255) {
pixels[dy * buf_w + dx] =
0xFF000000 | ((uint32_t)color.r << 16) |
((uint32_t)color.g << 8) | color.b;
} else {
uint32_t dst = pixels[dy * buf_w + dx];
uint8_t dr = (dst >> 16) & 0xFF;
uint8_t dg = (dst >> 8) & 0xFF;
uint8_t db = dst & 0xFF;
uint32_t a = alpha, inv_a = 255 - alpha;
uint32_t rr = (a * color.r + inv_a * dr + 128) / 255;
uint32_t gg = (a * color.g + inv_a * dg + 128) / 255;
uint32_t bb = (a * color.b + inv_a * db + 128) / 255;
pixels[dy * buf_w + dx] =
0xFF000000 | (rr << 16) | (gg << 8) | bb;
}
}
}
}
cx += g->advance;
}
}
// Draw single character to buffer, returning advance width
int draw_char_to_buffer(uint32_t* pixels, int buf_w, int buf_h,
int x, int baseline, int codepoint,
Color color, GlyphCache* gc) {
CachedGlyph* g = get_glyph(gc, codepoint);
if (!g) return 0;
if (g->bitmap) {
int gx = x + g->xoff;
int gy = baseline + g->yoff;
for (int row = 0; row < g->height; row++) {
int dy = gy + row;
if (dy < 0 || dy >= buf_h) continue;
for (int col = 0; col < g->width; col++) {
int dx = gx + col;
if (dx < 0 || dx >= buf_w) continue;
uint8_t alpha = g->bitmap[row * g->width + col];
if (alpha == 0) continue;
if (alpha == 255) {
pixels[dy * buf_w + dx] =
0xFF000000 | ((uint32_t)color.r << 16) |
((uint32_t)color.g << 8) | color.b;
} else {
uint32_t dst = pixels[dy * buf_w + dx];
uint8_t dr = (dst >> 16) & 0xFF;
uint8_t dg = (dst >> 8) & 0xFF;
uint8_t db = dst & 0xFF;
uint32_t a = alpha, inv_a = 255 - alpha;
uint32_t rr = (a * color.r + inv_a * dr + 128) / 255;
uint32_t gg = (a * color.g + inv_a * dg + 128) / 255;
uint32_t bb = (a * color.b + inv_a * db + 128) / 255;
pixels[dy * buf_w + dx] =
0xFF000000 | (rr << 16) | (gg << 8) | bb;
}
}
}
}
return g->advance;
}
};
// Global font manager
namespace fonts {
inline TrueTypeFont* system_font = nullptr;
inline TrueTypeFont* system_bold = nullptr;
inline TrueTypeFont* mono = nullptr;
inline TrueTypeFont* mono_bold = nullptr;
inline int UI_SIZE = 18;
inline int TITLE_SIZE = 18;
inline int TERM_SIZE = 18;
inline int LARGE_SIZE = 28;
inline bool init() {
auto load = [](const char* path) -> TrueTypeFont* {
TrueTypeFont* f = (TrueTypeFont*)montauk::malloc(sizeof(TrueTypeFont));
montauk::memset(f, 0, sizeof(TrueTypeFont));
if (!f->init(path)) {
montauk::mfree(f);
return nullptr;
}
return f;
};
system_font = load("0:/fonts/Roboto-Medium.ttf");
system_bold = load("0:/fonts/Roboto-Bold.ttf");
mono = load("0:/fonts/JetBrainsMono-Regular.ttf");
mono_bold = load("0:/fonts/JetBrainsMono-Bold.ttf");
return system_font != nullptr;
}
}
} // namespace gui
+359
View File
@@ -0,0 +1,359 @@
/*
* widgets.hpp
* MontaukOS GUI widget toolkit (Label, Button, TextBox, Scrollbar)
* Copyright (c) 2025 Daniel Hammer
*/
#pragma once
#include "gui/gui.hpp"
#include "gui/framebuffer.hpp"
#include "gui/font.hpp"
#include "gui/draw.hpp"
namespace gui {
// ---- Mouse event ----
struct MouseEvent {
int x, y;
uint8_t buttons;
uint8_t prev_buttons;
int32_t scroll;
bool left_held() const { return buttons & 0x01; }
bool right_held() const { return buttons & 0x02; }
bool middle_held() const { return buttons & 0x04; }
bool left_pressed() const { return (buttons & 0x01) && !(prev_buttons & 0x01); }
bool left_released() const { return !(buttons & 0x01) && (prev_buttons & 0x01); }
bool right_pressed() const { return (buttons & 0x02) && !(prev_buttons & 0x02); }
bool right_released() const { return !(buttons & 0x02) && (prev_buttons & 0x02); }
};
// ---- Callback types ----
using ClickCallback = void (*)(void* userdata);
// ---- Label ----
struct Label {
int x, y;
const char* text;
Color color;
void draw(Framebuffer& fb) const {
if (text) {
draw_text(fb, x, y, text, color);
}
}
};
// ---- Button ----
struct Button {
Rect bounds;
const char* text;
Color bg;
Color fg;
Color hover_bg;
bool hovered;
bool pressed;
ClickCallback on_click;
void* userdata;
void init(int x, int y, int w, int h, const char* label) {
bounds = {x, y, w, h};
text = label;
bg = colors::ACCENT;
fg = colors::WHITE;
hover_bg = Color::from_rgb(0x2B, 0x6B, 0xE0);
hovered = false;
pressed = false;
on_click = nullptr;
userdata = nullptr;
}
void draw(Framebuffer& fb) const {
Color bg_color = hovered ? hover_bg : bg;
fill_rounded_rect(fb, bounds.x, bounds.y, bounds.w, bounds.h, 4, bg_color);
// Center text
int tw = text_width(text);
int tx = bounds.x + (bounds.w - tw) / 2;
int ty = bounds.y + (bounds.h - system_font_height()) / 2;
draw_text(fb, tx, ty, text, fg);
}
bool handle_mouse(const MouseEvent& ev) {
hovered = bounds.contains(ev.x, ev.y);
if (hovered && ev.left_pressed()) {
pressed = true;
}
if (pressed && ev.left_released()) {
pressed = false;
if (hovered && on_click) {
on_click(userdata);
return true;
}
}
if (!ev.left_held()) {
pressed = false;
}
return false;
}
};
// ---- IconButton (for panel/menu items with SVG icon + optional text) ----
struct IconButton {
Rect bounds;
const char* text; // optional label (can be nullptr)
uint32_t* icon_pixels; // icon pixel data (ARGB)
int icon_w, icon_h;
Color bg;
Color hover_bg;
Color text_color;
bool hovered;
bool pressed;
ClickCallback on_click;
void* userdata;
void init(int x, int y, int w, int h) {
bounds = {x, y, w, h};
text = nullptr;
icon_pixels = nullptr;
icon_w = 0;
icon_h = 0;
bg = {0, 0, 0, 0}; // transparent by default
hover_bg = colors::MENU_HOVER;
text_color = colors::TEXT_COLOR;
hovered = false;
pressed = false;
on_click = nullptr;
userdata = nullptr;
}
void draw(Framebuffer& fb) const {
if (hovered) {
fill_rounded_rect(fb, bounds.x, bounds.y, bounds.w, bounds.h, 3, hover_bg);
} else if (bg.a > 0) {
fill_rounded_rect(fb, bounds.x, bounds.y, bounds.w, bounds.h, 3, bg);
}
int content_x = bounds.x + 6;
// Draw icon
if (icon_pixels && icon_w > 0 && icon_h > 0) {
int iy = bounds.y + (bounds.h - icon_h) / 2;
fb.blit_alpha(content_x, iy, icon_w, icon_h, icon_pixels);
content_x += icon_w + 6;
}
// Draw text
if (text) {
int ty = bounds.y + (bounds.h - system_font_height()) / 2;
draw_text(fb, content_x, ty, text, text_color);
}
}
bool handle_mouse(const MouseEvent& ev) {
hovered = bounds.contains(ev.x, ev.y);
if (hovered && ev.left_pressed()) {
pressed = true;
}
if (pressed && ev.left_released()) {
pressed = false;
if (hovered && on_click) {
on_click(userdata);
return true;
}
}
if (!ev.left_held()) {
pressed = false;
}
return false;
}
};
// ---- TextBox ----
struct TextBox {
Rect bounds;
char text[256];
int cursor;
int text_len;
bool focused;
Color bg;
Color fg;
Color border_color;
Color cursor_color;
void init(int x, int y, int w, int h) {
bounds = {x, y, w, h};
text[0] = '\0';
cursor = 0;
text_len = 0;
focused = false;
bg = colors::WHITE;
fg = colors::TEXT_COLOR;
border_color = colors::BORDER;
cursor_color = colors::ACCENT;
}
void draw(Framebuffer& fb) const {
fb.fill_rect(bounds.x, bounds.y, bounds.w, bounds.h, bg);
draw_rect(fb, bounds.x, bounds.y, bounds.w, bounds.h,
focused ? cursor_color : border_color);
// Draw text with 4px padding
int tx = bounds.x + 4;
int fh = system_font_height();
int ty = bounds.y + (bounds.h - fh) / 2;
draw_text(fb, tx, ty, text, fg);
// Draw cursor if focused
if (focused) {
// Measure text up to cursor position for proportional fonts
char prefix[256];
for (int i = 0; i < cursor && i < 255; i++) prefix[i] = text[i];
prefix[cursor < 255 ? cursor : 255] = '\0';
int cx = tx + text_width(prefix);
draw_vline(fb, cx, ty, fh, cursor_color);
}
}
void handle_mouse(const MouseEvent& ev) {
if (ev.left_pressed()) {
focused = bounds.contains(ev.x, ev.y);
}
}
void handle_key(const Montauk::KeyEvent& key) {
if (!focused || !key.pressed) return;
if (key.ascii == '\b' || key.scancode == 0x0E) {
// Backspace
if (cursor > 0) {
for (int i = cursor - 1; i < text_len - 1; i++) {
text[i] = text[i + 1];
}
text_len--;
cursor--;
text[text_len] = '\0';
}
} else if (key.ascii >= 32 && key.ascii < 127) {
// Printable character
if (text_len < 254) {
for (int i = text_len; i > cursor; i--) {
text[i] = text[i - 1];
}
text[cursor] = key.ascii;
cursor++;
text_len++;
text[text_len] = '\0';
}
} else if (key.scancode == 0x4B) {
// Left arrow
if (cursor > 0) cursor--;
} else if (key.scancode == 0x4D) {
// Right arrow
if (cursor < text_len) cursor++;
}
}
};
// ---- Scrollbar ----
struct Scrollbar {
Rect bounds;
int content_height;
int view_height;
int scroll_offset;
bool dragging;
int drag_start_y;
int drag_start_offset;
Color bg;
Color fg;
Color hover_fg;
bool hovered;
void init(int x, int y, int w, int h) {
bounds = {x, y, w, h};
content_height = 0;
view_height = h;
scroll_offset = 0;
dragging = false;
drag_start_y = 0;
drag_start_offset = 0;
bg = colors::SCROLLBAR_BG;
fg = colors::SCROLLBAR_FG;
hover_fg = Color::from_rgb(0xA0, 0xA0, 0xA0);
hovered = false;
}
int thumb_height() const {
if (content_height <= view_height) return bounds.h;
int th = (view_height * bounds.h) / content_height;
return th < 20 ? 20 : th;
}
int thumb_y() const {
if (content_height <= view_height) return bounds.y;
int range = bounds.h - thumb_height();
int max_scroll = content_height - view_height;
if (max_scroll <= 0) return bounds.y;
return bounds.y + (scroll_offset * range) / max_scroll;
}
int max_scroll() const {
int ms = content_height - view_height;
return ms > 0 ? ms : 0;
}
void draw(Framebuffer& fb) const {
if (content_height <= view_height) return; // no scrollbar needed
fb.fill_rect(bounds.x, bounds.y, bounds.w, bounds.h, bg);
int th = thumb_height();
int ty = thumb_y();
Color thumb_color = (hovered || dragging) ? hover_fg : fg;
fill_rounded_rect(fb, bounds.x + 1, ty, bounds.w - 2, th, 3, thumb_color);
}
void handle_mouse(const MouseEvent& ev) {
if (content_height <= view_height) return;
Rect thumb_rect = {bounds.x, thumb_y(), bounds.w, thumb_height()};
hovered = thumb_rect.contains(ev.x, ev.y);
if (hovered && ev.left_pressed()) {
dragging = true;
drag_start_y = ev.y;
drag_start_offset = scroll_offset;
}
if (dragging && ev.left_held()) {
int dy = ev.y - drag_start_y;
int range = bounds.h - thumb_height();
if (range > 0) {
int ms = max_scroll();
scroll_offset = drag_start_offset + (dy * ms) / range;
if (scroll_offset < 0) scroll_offset = 0;
if (scroll_offset > ms) scroll_offset = ms;
}
}
if (!ev.left_held()) {
dragging = false;
}
// Handle scroll wheel
if (bounds.contains(ev.x, ev.y) && ev.scroll != 0) {
scroll_offset += ev.scroll * 20;
int ms = max_scroll();
if (scroll_offset < 0) scroll_offset = 0;
if (scroll_offset > ms) scroll_offset = ms;
}
}
};
} // namespace gui
+96
View File
@@ -0,0 +1,96 @@
/*
* window.hpp
* MontaukOS window management types
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include "gui/gui.hpp"
#include "gui/framebuffer.hpp"
#include "gui/widgets.hpp"
#include <Api/Syscall.hpp>
namespace gui {
enum WindowState { WIN_NORMAL, WIN_MINIMIZED, WIN_MAXIMIZED, WIN_CLOSED };
enum ResizeEdge {
RESIZE_NONE = 0,
RESIZE_LEFT, RESIZE_RIGHT, RESIZE_TOP, RESIZE_BOTTOM,
RESIZE_TOP_LEFT, RESIZE_TOP_RIGHT, RESIZE_BOTTOM_LEFT, RESIZE_BOTTOM_RIGHT
};
static constexpr int TITLEBAR_HEIGHT = 30;
static constexpr int BORDER_WIDTH = 1;
static constexpr int SHADOW_SIZE = 3;
static constexpr int BTN_RADIUS = 6;
static constexpr int MAX_TITLE_LEN = 64;
static constexpr int RESIZE_GRAB = 6;
static constexpr int MIN_WINDOW_W = 120;
static constexpr int MIN_WINDOW_H = 80;
struct Window;
using WindowDrawCallback = void (*)(Window* win, Framebuffer& fb);
using WindowMouseCallback = void (*)(Window* win, MouseEvent& ev);
using WindowKeyCallback = void (*)(Window* win, const Montauk::KeyEvent& key);
using WindowCloseCallback = void (*)(Window* win);
using WindowPollCallback = void (*)(Window* win);
struct Window {
char title[MAX_TITLE_LEN];
Rect frame;
WindowState state;
int z_order;
bool focused;
bool dirty;
uint32_t* content;
int content_w, content_h;
bool dragging;
int drag_offset_x, drag_offset_y;
bool resizing;
ResizeEdge resize_edge;
Rect resize_start_frame;
int resize_start_mx, resize_start_my;
Rect saved_frame;
WindowDrawCallback on_draw;
WindowMouseCallback on_mouse;
WindowKeyCallback on_key;
WindowCloseCallback on_close;
WindowPollCallback on_poll;
void* app_data;
bool external; // true = shared-memory window from external process
int ext_win_id; // window server ID (valid when external == true)
uint8_t ext_cursor; // cursor style requested by external app (0=arrow, 1=resize_h, 2=resize_v)
Rect titlebar_rect() const {
return {frame.x, frame.y, frame.w, TITLEBAR_HEIGHT};
}
Rect content_rect() const {
return {frame.x + BORDER_WIDTH, frame.y + TITLEBAR_HEIGHT,
frame.w - 2 * BORDER_WIDTH, frame.h - TITLEBAR_HEIGHT - BORDER_WIDTH};
}
Rect close_btn_rect() const {
int by = frame.y + (TITLEBAR_HEIGHT - BTN_RADIUS * 2) / 2;
return {frame.x + 12, by, BTN_RADIUS * 2, BTN_RADIUS * 2};
}
Rect min_btn_rect() const {
int by = frame.y + (TITLEBAR_HEIGHT - BTN_RADIUS * 2) / 2;
return {frame.x + 12 + 22, by, BTN_RADIUS * 2, BTN_RADIUS * 2};
}
Rect max_btn_rect() const {
int by = frame.y + (TITLEBAR_HEIGHT - BTN_RADIUS * 2) / 2;
return {frame.x + 12 + 44, by, BTN_RADIUS * 2, BTN_RADIUS * 2};
}
};
} // namespace gui