feat: multi-user system, bug fixes, security & performance fixes, and more
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user