fix: sync template sysroot, add script
This commit is contained in:
@@ -71,6 +71,35 @@ struct Canvas {
|
||||
}
|
||||
}
|
||||
|
||||
void fill_rect_alpha(int x, int y, int rw, int rh, Color c) {
|
||||
if (c.a == 0) return;
|
||||
if (c.a == 255) { fill_rect(x, y, rw, rh, c); return; }
|
||||
|
||||
int x0 = gui_max(x, 0), y0 = gui_max(y, 0);
|
||||
int x1 = gui_min(x + rw, w), y1 = gui_min(y + rh, 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 dy = y0; dy < y1; dy++) {
|
||||
uint32_t* row = pixels + dy * w;
|
||||
for (int dx = x0; dx < x1; dx++) {
|
||||
uint32_t p = row[dx];
|
||||
uint32_t dr = (p >> 16) & 0xFF;
|
||||
uint32_t dg = (p >> 8) & 0xFF;
|
||||
uint32_t db = p & 0xFF;
|
||||
uint32_t nr = (src_r + dr * inv_a) / 255;
|
||||
uint32_t ng = (src_g + dg * inv_a) / 255;
|
||||
uint32_t nb = (src_b + db * inv_a) / 255;
|
||||
row[dx] = (0xFFu << 24) | (nr << 16) | (ng << 8) | nb;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -135,51 +164,12 @@ struct Canvas {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* clipboard.hpp
|
||||
* Simple plain-text clipboard helpers for MontaukOS GUI apps
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/heap.h>
|
||||
|
||||
namespace gui {
|
||||
|
||||
inline bool clipboard_set_text(const char* text, int len) {
|
||||
if (len < 0) return false;
|
||||
if (len > 0 && text == nullptr) return false;
|
||||
return montauk::clipboard_set_text(text, (uint32_t)len) == 0;
|
||||
}
|
||||
|
||||
inline bool clipboard_set_text(const char* text) {
|
||||
return clipboard_set_text(text, text ? montauk::slen(text) : 0);
|
||||
}
|
||||
|
||||
inline bool clipboard_get_info(Montauk::ClipboardInfo* out) {
|
||||
if (out == nullptr) return false;
|
||||
return montauk::clipboard_get_info(out) == 0;
|
||||
}
|
||||
|
||||
inline bool clipboard_has_text() {
|
||||
Montauk::ClipboardInfo info = {};
|
||||
return clipboard_get_info(&info) && info.textBytes > 0;
|
||||
}
|
||||
|
||||
inline char* clipboard_get_text_alloc(int* out_len = nullptr, uint64_t* out_serial = nullptr) {
|
||||
if (out_len) *out_len = 0;
|
||||
if (out_serial) *out_serial = 0;
|
||||
|
||||
Montauk::ClipboardInfo info = {};
|
||||
if (!clipboard_get_info(&info) || info.textBytes == 0)
|
||||
return nullptr;
|
||||
|
||||
uint32_t cap = info.textBytes + 1;
|
||||
char* text = (char*)montauk::malloc(cap);
|
||||
if (text == nullptr) return nullptr;
|
||||
|
||||
uint32_t actual_len = 0;
|
||||
uint64_t serial = 0;
|
||||
if (montauk::clipboard_get_text(text, info.textBytes, &actual_len, &serial) != 0 ||
|
||||
actual_len > info.textBytes) {
|
||||
montauk::mfree(text);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
text[actual_len] = '\0';
|
||||
if (out_len) *out_len = (int)actual_len;
|
||||
if (out_serial) *out_serial = serial;
|
||||
return text;
|
||||
}
|
||||
|
||||
inline bool clipboard_clear() {
|
||||
return montauk::clipboard_clear() == 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,17 +18,37 @@ namespace gui {
|
||||
static constexpr int MAX_WINDOWS = 8;
|
||||
static constexpr int PANEL_HEIGHT = 32;
|
||||
static constexpr int MAX_EXTERNAL_APPS = 32;
|
||||
static constexpr int MAX_LAUNCHER_ITEMS = 64;
|
||||
|
||||
// External app discovered from 0:/apps/ manifest
|
||||
struct ExternalApp {
|
||||
char name[48];
|
||||
char binary_path[128];
|
||||
char icon_path[128];
|
||||
char category[24];
|
||||
SvgIcon icon;
|
||||
bool menu_visible;
|
||||
bool launch_with_home;
|
||||
};
|
||||
|
||||
enum LauncherItemKind : uint8_t {
|
||||
LAUNCHER_ITEM_EXTERNAL_APP = 0,
|
||||
LAUNCHER_ITEM_BUILTIN_FILES,
|
||||
LAUNCHER_ITEM_COMMAND_REBOOT,
|
||||
LAUNCHER_ITEM_COMMAND_SHUTDOWN,
|
||||
LAUNCHER_ITEM_SPECIAL_FOLDER,
|
||||
};
|
||||
|
||||
struct LauncherItem {
|
||||
char name[64];
|
||||
char category[48];
|
||||
char path[128];
|
||||
char search_text[192];
|
||||
SvgIcon* icon;
|
||||
LauncherItemKind kind;
|
||||
int ref_index;
|
||||
};
|
||||
|
||||
struct DesktopSettings {
|
||||
// Background
|
||||
bool bg_gradient; // true = gradient, false = solid
|
||||
@@ -72,6 +92,17 @@ struct DesktopState {
|
||||
uint8_t prev_buttons;
|
||||
|
||||
bool app_menu_open;
|
||||
bool launcher_open;
|
||||
char launcher_query[64];
|
||||
int launcher_query_len;
|
||||
LauncherItem launcher_items[MAX_LAUNCHER_ITEMS];
|
||||
int launcher_item_count;
|
||||
int launcher_results[MAX_LAUNCHER_ITEMS];
|
||||
int launcher_result_count;
|
||||
int launcher_selected;
|
||||
int launcher_scroll;
|
||||
bool super_left_down;
|
||||
bool super_right_down;
|
||||
|
||||
SvgIcon icon_terminal;
|
||||
SvgIcon icon_filemanager;
|
||||
@@ -80,11 +111,11 @@ struct DesktopState {
|
||||
SvgIcon icon_folder;
|
||||
SvgIcon icon_file;
|
||||
SvgIcon icon_network;
|
||||
SvgIcon icon_calculator;
|
||||
SvgIcon icon_go_up;
|
||||
SvgIcon icon_go_back;
|
||||
SvgIcon icon_go_forward;
|
||||
SvgIcon icon_home;
|
||||
SvgIcon icon_refresh;
|
||||
SvgIcon icon_exec;
|
||||
|
||||
SvgIcon icon_folder_lg;
|
||||
@@ -92,7 +123,26 @@ struct DesktopState {
|
||||
SvgIcon icon_exec_lg;
|
||||
SvgIcon icon_drive;
|
||||
SvgIcon icon_drive_lg;
|
||||
SvgIcon icon_drive_usb;
|
||||
SvgIcon icon_drive_usb_lg;
|
||||
SvgIcon icon_delete;
|
||||
SvgIcon icon_copy;
|
||||
SvgIcon icon_cut;
|
||||
SvgIcon icon_paste;
|
||||
SvgIcon icon_rename;
|
||||
SvgIcon icon_folder_new;
|
||||
SvgIcon icon_home_folder;
|
||||
SvgIcon icon_home_folder_lg;
|
||||
SvgIcon icon_apps;
|
||||
SvgIcon icon_apps_lg;
|
||||
SvgIcon icon_system_configuration_menu;
|
||||
SvgIcon icon_system_configuration;
|
||||
SvgIcon icon_system_configuration_lg;
|
||||
|
||||
// Special user folder icons (16x16 and 48x48)
|
||||
static constexpr int SPECIAL_FOLDER_COUNT = 6;
|
||||
SvgIcon icon_special_folder[SPECIAL_FOLDER_COUNT];
|
||||
SvgIcon icon_special_folder_lg[SPECIAL_FOLDER_COUNT];
|
||||
|
||||
SvgIcon icon_settings;
|
||||
SvgIcon icon_reboot;
|
||||
@@ -121,8 +171,19 @@ struct DesktopState {
|
||||
int vol_pre_mute; // volume before mute
|
||||
bool vol_dragging; // slider drag in progress
|
||||
uint64_t vol_last_poll;
|
||||
uint64_t vol_serial; // last seen mixer state serial
|
||||
|
||||
// Temperature monitoring
|
||||
static constexpr int MAX_THERMAL_ZONES = 8;
|
||||
Montauk::ThermalInfo thermal_zones[MAX_THERMAL_ZONES];
|
||||
int thermal_zone_count;
|
||||
uint64_t thermal_last_poll;
|
||||
Rect temp_icon_rect;
|
||||
|
||||
int screen_w, screen_h;
|
||||
uint32_t* background_cache;
|
||||
int background_cache_pitch;
|
||||
bool background_cache_dirty;
|
||||
|
||||
// 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
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
* dialogs.hpp
|
||||
* Shared dialog request/result structures and client helpers
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <montauk/string.h>
|
||||
|
||||
#ifndef GUI_DIALOGS_LIBRARY_BUILD
|
||||
#include <libloader/libloader.h>
|
||||
#endif
|
||||
|
||||
namespace gui::dialogs {
|
||||
|
||||
inline constexpr const char* LIBRARY_PATH = "0:/os/libdialogs.lib";
|
||||
inline constexpr const char* RUN_REQUEST_SYMBOL = "dialogs_run_request";
|
||||
inline constexpr const char* MESSAGE_BOX_SYMBOL = "dialogs_message_box";
|
||||
|
||||
enum RequestKind : uint8_t {
|
||||
REQUEST_KIND_FILE = 1,
|
||||
REQUEST_KIND_PRINT = 2,
|
||||
};
|
||||
|
||||
enum FileDialogMode : uint8_t {
|
||||
FILE_DIALOG_OPEN = 1,
|
||||
FILE_DIALOG_SAVE = 2,
|
||||
};
|
||||
|
||||
enum PrintDialogMode : uint8_t {
|
||||
PRINT_DIALOG_SETUP = 1,
|
||||
PRINT_DIALOG_SUBMIT = 2,
|
||||
};
|
||||
|
||||
enum MessageBoxButtons : uint8_t {
|
||||
MESSAGE_BOX_OK = 1,
|
||||
MESSAGE_BOX_OK_CANCEL = 2,
|
||||
MESSAGE_BOX_YES_NO = 3,
|
||||
MESSAGE_BOX_YES_NO_CANCEL = 4,
|
||||
};
|
||||
|
||||
enum MessageBoxResult : uint8_t {
|
||||
MESSAGE_BOX_RESULT_NONE = 0,
|
||||
MESSAGE_BOX_RESULT_OK = 1,
|
||||
MESSAGE_BOX_RESULT_CANCEL = 2,
|
||||
MESSAGE_BOX_RESULT_YES = 3,
|
||||
MESSAGE_BOX_RESULT_NO = 4,
|
||||
};
|
||||
|
||||
struct Request {
|
||||
uint8_t kind;
|
||||
uint8_t mode;
|
||||
char title[96];
|
||||
char initial_path[256];
|
||||
char suggested_name[128];
|
||||
char source_path[256];
|
||||
char job_name[128];
|
||||
char printer_uri[256];
|
||||
uint32_t copies;
|
||||
};
|
||||
|
||||
struct Result {
|
||||
char status[16];
|
||||
char path[256];
|
||||
char job_id[64];
|
||||
char printer_uri[256];
|
||||
char printer_name[128];
|
||||
uint32_t copies;
|
||||
char message[160];
|
||||
};
|
||||
|
||||
inline void safe_copy(char* dst, int dst_len, const char* src) {
|
||||
if (!dst || dst_len <= 0) return;
|
||||
if (!src) src = "";
|
||||
montauk::strncpy(dst, src, dst_len - 1);
|
||||
dst[dst_len - 1] = '\0';
|
||||
}
|
||||
|
||||
inline void reset_request(Request* req) {
|
||||
if (!req) return;
|
||||
montauk::memset(req, 0, sizeof(Request));
|
||||
}
|
||||
|
||||
inline void reset_result(Result* res) {
|
||||
if (!res) return;
|
||||
montauk::memset(res, 0, sizeof(Result));
|
||||
}
|
||||
|
||||
#ifndef GUI_DIALOGS_LIBRARY_BUILD
|
||||
|
||||
using RunRequestFn = bool (*)(const Request*, Result*);
|
||||
using MessageBoxFn = uint8_t (*)(const char*, const char*, uint8_t);
|
||||
|
||||
struct Runtime {
|
||||
LibHandle* handle;
|
||||
RunRequestFn run_request;
|
||||
MessageBoxFn message_box;
|
||||
};
|
||||
|
||||
inline Runtime g_runtime = {};
|
||||
|
||||
inline bool ensure_handle(char* out_message, int out_message_len) {
|
||||
if (g_runtime.handle) return true;
|
||||
g_runtime.handle = libloader::dlopen(LIBRARY_PATH);
|
||||
if (!g_runtime.handle) {
|
||||
if (out_message && out_message_len > 0)
|
||||
safe_copy(out_message, out_message_len, "failed to load dialog library");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline RunRequestFn resolve_run_request(char* out_message, int out_message_len) {
|
||||
if (g_runtime.run_request) return g_runtime.run_request;
|
||||
if (!ensure_handle(out_message, out_message_len)) return nullptr;
|
||||
g_runtime.run_request = (RunRequestFn)libloader::dlsym(g_runtime.handle, RUN_REQUEST_SYMBOL);
|
||||
if (!g_runtime.run_request && out_message && out_message_len > 0)
|
||||
safe_copy(out_message, out_message_len, "failed to resolve dialog entry point");
|
||||
return g_runtime.run_request;
|
||||
}
|
||||
|
||||
inline MessageBoxFn resolve_message_box(char* out_message, int out_message_len) {
|
||||
if (g_runtime.message_box) return g_runtime.message_box;
|
||||
if (!ensure_handle(out_message, out_message_len)) return nullptr;
|
||||
g_runtime.message_box = (MessageBoxFn)libloader::dlsym(g_runtime.handle, MESSAGE_BOX_SYMBOL);
|
||||
if (!g_runtime.message_box && out_message && out_message_len > 0)
|
||||
safe_copy(out_message, out_message_len, "failed to resolve message box entry point");
|
||||
return g_runtime.message_box;
|
||||
}
|
||||
|
||||
inline bool run_request(const Request* req, Result& res, char* out_message, int out_message_len) {
|
||||
if (out_message && out_message_len > 0) out_message[0] = '\0';
|
||||
if (!req) {
|
||||
if (out_message && out_message_len > 0)
|
||||
safe_copy(out_message, out_message_len, "invalid dialog request");
|
||||
return false;
|
||||
}
|
||||
|
||||
RunRequestFn run_fn = resolve_run_request(out_message, out_message_len);
|
||||
if (!run_fn) return false;
|
||||
if (!run_fn(req, &res)) {
|
||||
if (out_message && out_message_len > 0 && !out_message[0])
|
||||
safe_copy(out_message, out_message_len, "dialog library invocation failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (out_message && out_message_len > 0)
|
||||
safe_copy(out_message, out_message_len, res.message);
|
||||
return montauk::streq(res.status, "ok");
|
||||
}
|
||||
|
||||
inline bool open_file(const char* title,
|
||||
const char* initial_path,
|
||||
char* out_path, int out_path_len,
|
||||
char* out_message = nullptr, int out_message_len = 0) {
|
||||
Request req = {};
|
||||
req.kind = REQUEST_KIND_FILE;
|
||||
req.mode = FILE_DIALOG_OPEN;
|
||||
safe_copy(req.title, sizeof(req.title), title ? title : "Open");
|
||||
safe_copy(req.initial_path, sizeof(req.initial_path), initial_path);
|
||||
|
||||
Result res = {};
|
||||
bool ok = run_request(&req, res, out_message, out_message_len);
|
||||
if (ok && out_path && out_path_len > 0) safe_copy(out_path, out_path_len, res.path);
|
||||
return ok;
|
||||
}
|
||||
|
||||
inline bool save_file(const char* title,
|
||||
const char* initial_path,
|
||||
const char* suggested_name,
|
||||
char* out_path, int out_path_len,
|
||||
char* out_message = nullptr, int out_message_len = 0) {
|
||||
Request req = {};
|
||||
req.kind = REQUEST_KIND_FILE;
|
||||
req.mode = FILE_DIALOG_SAVE;
|
||||
safe_copy(req.title, sizeof(req.title), title ? title : "Save");
|
||||
safe_copy(req.initial_path, sizeof(req.initial_path), initial_path);
|
||||
safe_copy(req.suggested_name, sizeof(req.suggested_name), suggested_name);
|
||||
|
||||
Result res = {};
|
||||
bool ok = run_request(&req, res, out_message, out_message_len);
|
||||
if (ok && out_path && out_path_len > 0) safe_copy(out_path, out_path_len, res.path);
|
||||
return ok;
|
||||
}
|
||||
|
||||
inline bool print_file(const char* title,
|
||||
const char* source_path,
|
||||
const char* job_name,
|
||||
char* out_job_id, int out_job_id_len,
|
||||
char* out_message = nullptr, int out_message_len = 0) {
|
||||
Request req = {};
|
||||
req.kind = REQUEST_KIND_PRINT;
|
||||
req.mode = PRINT_DIALOG_SUBMIT;
|
||||
req.copies = 1;
|
||||
safe_copy(req.title, sizeof(req.title), title ? title : "Print");
|
||||
safe_copy(req.source_path, sizeof(req.source_path), source_path);
|
||||
safe_copy(req.job_name, sizeof(req.job_name), job_name);
|
||||
|
||||
Result res = {};
|
||||
bool ok = run_request(&req, res, out_message, out_message_len);
|
||||
if (ok && out_job_id && out_job_id_len > 0) safe_copy(out_job_id, out_job_id_len, res.job_id);
|
||||
return ok;
|
||||
}
|
||||
|
||||
inline bool configure_print(const char* title,
|
||||
const char* initial_printer_uri,
|
||||
const char* job_name,
|
||||
char* out_printer_uri, int out_printer_uri_len,
|
||||
char* out_printer_name, int out_printer_name_len,
|
||||
uint32_t* out_copies = nullptr,
|
||||
char* out_message = nullptr, int out_message_len = 0) {
|
||||
Request req = {};
|
||||
req.kind = REQUEST_KIND_PRINT;
|
||||
req.mode = PRINT_DIALOG_SETUP;
|
||||
req.copies = out_copies && *out_copies > 0 ? *out_copies : 1;
|
||||
safe_copy(req.title, sizeof(req.title), title ? title : "Print");
|
||||
safe_copy(req.job_name, sizeof(req.job_name), job_name ? job_name : "");
|
||||
safe_copy(req.printer_uri, sizeof(req.printer_uri), initial_printer_uri);
|
||||
|
||||
Result res = {};
|
||||
bool ok = run_request(&req, res, out_message, out_message_len);
|
||||
if (ok && out_printer_uri && out_printer_uri_len > 0) safe_copy(out_printer_uri, out_printer_uri_len, res.printer_uri);
|
||||
if (ok && out_printer_name && out_printer_name_len > 0) safe_copy(out_printer_name, out_printer_name_len, res.printer_name);
|
||||
if (ok && out_copies) *out_copies = res.copies > 0 ? res.copies : 1;
|
||||
return ok;
|
||||
}
|
||||
|
||||
inline MessageBoxResult message_box(const char* title,
|
||||
const char* message,
|
||||
MessageBoxButtons buttons = MESSAGE_BOX_OK,
|
||||
char* out_message = nullptr, int out_message_len = 0) {
|
||||
if (out_message && out_message_len > 0) out_message[0] = '\0';
|
||||
MessageBoxFn fn = resolve_message_box(out_message, out_message_len);
|
||||
if (!fn) return MESSAGE_BOX_RESULT_NONE;
|
||||
|
||||
uint8_t result = fn(title ? title : "Message",
|
||||
message ? message : "",
|
||||
(uint8_t)buttons);
|
||||
if (result == MESSAGE_BOX_RESULT_NONE && out_message && out_message_len > 0 && !out_message[0])
|
||||
safe_copy(out_message, out_message_len, "message box invocation failed");
|
||||
return (MessageBoxResult)result;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace gui::dialogs
|
||||
@@ -12,16 +12,17 @@ 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);
|
||||
fb.fill_rect(x, y, w, 1, 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);
|
||||
fb.fill_rect(x, y, 1, h, c);
|
||||
}
|
||||
|
||||
// Rectangle outline
|
||||
inline void draw_rect(Framebuffer& fb, int x, int y, int w, int h, Color c) {
|
||||
if (w <= 0 || h <= 0) return;
|
||||
draw_hline(fb, x, y, w, c);
|
||||
draw_hline(fb, x, y + h - 1, w, c);
|
||||
draw_vline(fb, x, y, h, c);
|
||||
|
||||
@@ -19,6 +19,46 @@ class Framebuffer {
|
||||
int fb_height;
|
||||
int fb_pitch; // in bytes
|
||||
|
||||
static inline void fill_pixels(uint32_t* dst, int count, uint32_t pixel) {
|
||||
if (!dst || count <= 0) return;
|
||||
|
||||
uint64_t pixel64 = ((uint64_t)pixel << 32) | pixel;
|
||||
if (((uint64_t)dst & 4) && count > 0) {
|
||||
*dst++ = pixel;
|
||||
count--;
|
||||
}
|
||||
|
||||
uint64_t* dst64 = (uint64_t*)dst;
|
||||
int pairs = count / 2;
|
||||
for (int i = 0; i < pairs; i++) {
|
||||
dst64[i] = pixel64;
|
||||
}
|
||||
|
||||
if (count & 1) {
|
||||
((uint32_t*)(dst64 + pairs))[0] = pixel;
|
||||
}
|
||||
}
|
||||
|
||||
static inline uint32_t blend_pixel(uint32_t dst, uint32_t src, uint32_t alpha) {
|
||||
uint32_t inv_a = 255 - alpha;
|
||||
uint32_t sr = (src >> 16) & 0xFF;
|
||||
uint32_t sg = (src >> 8) & 0xFF;
|
||||
uint32_t sb = src & 0xFF;
|
||||
uint32_t dr = (dst >> 16) & 0xFF;
|
||||
uint32_t dg = (dst >> 8) & 0xFF;
|
||||
uint32_t db = dst & 0xFF;
|
||||
|
||||
uint32_t rr = alpha * sr + inv_a * dr;
|
||||
uint32_t gg = alpha * sg + inv_a * dg;
|
||||
uint32_t bb = alpha * sb + inv_a * db;
|
||||
|
||||
rr = (rr + 1 + (rr >> 8)) >> 8;
|
||||
gg = (gg + 1 + (gg >> 8)) >> 8;
|
||||
bb = (bb + 1 + (bb >> 8)) >> 8;
|
||||
|
||||
return 0xFF000000 | (rr << 16) | (gg << 8) | bb;
|
||||
}
|
||||
|
||||
public:
|
||||
Framebuffer() : hw_fb(nullptr), back_buf(nullptr), fb_width(0), fb_height(0), fb_pitch(0) {
|
||||
Montauk::FbInfo info;
|
||||
@@ -76,6 +116,8 @@ public:
|
||||
}
|
||||
|
||||
inline void fill_rect(int x, int y, int w, int h, Color c) {
|
||||
if (!back_buf) return;
|
||||
|
||||
// Clip to screen bounds
|
||||
int x0 = x < 0 ? 0 : x;
|
||||
int y0 = y < 0 ? 0 : y;
|
||||
@@ -89,9 +131,7 @@ public:
|
||||
|
||||
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;
|
||||
}
|
||||
fill_pixels(dst, clipped_w, pixel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,56 +177,84 @@ public:
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
if (!back_buf || !pixels || w <= 0 || h <= 0) return;
|
||||
|
||||
int src_x = 0;
|
||||
int src_y = 0;
|
||||
int dst_x = x;
|
||||
int dst_y = y;
|
||||
int copy_w = w;
|
||||
int copy_h = h;
|
||||
|
||||
if (dst_x < 0) { src_x = -dst_x; copy_w += dst_x; dst_x = 0; }
|
||||
if (dst_y < 0) { src_y = -dst_y; copy_h += dst_y; dst_y = 0; }
|
||||
if (dst_x + copy_w > fb_width) copy_w = fb_width - dst_x;
|
||||
if (dst_y + copy_h > fb_height) copy_h = fb_height - dst_y;
|
||||
if (copy_w <= 0 || copy_h <= 0) return;
|
||||
|
||||
uint64_t row_bytes = (uint64_t)copy_w * sizeof(uint32_t);
|
||||
for (int row = 0; row < copy_h; row++) {
|
||||
uint32_t* dst = (uint32_t*)((uint8_t*)back_buf + (dst_y + row) * fb_pitch) + dst_x;
|
||||
const uint32_t* src = pixels + (src_y + row) * w + src_x;
|
||||
montauk::memcpy(dst, src, row_bytes);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
if (!back_buf || !pixels || w <= 0 || h <= 0) return;
|
||||
|
||||
uint32_t src = pixels[row * w + col];
|
||||
uint8_t sa = (src >> 24) & 0xFF;
|
||||
int src_x = 0;
|
||||
int src_y = 0;
|
||||
int dst_x = x;
|
||||
int dst_y = y;
|
||||
int copy_w = w;
|
||||
int copy_h = h;
|
||||
|
||||
if (dst_x < 0) { src_x = -dst_x; copy_w += dst_x; dst_x = 0; }
|
||||
if (dst_y < 0) { src_y = -dst_y; copy_h += dst_y; dst_y = 0; }
|
||||
if (dst_x + copy_w > fb_width) copy_w = fb_width - dst_x;
|
||||
if (dst_y + copy_h > fb_height) copy_h = fb_height - dst_y;
|
||||
if (copy_w <= 0 || copy_h <= 0) return;
|
||||
|
||||
for (int row = 0; row < copy_h; row++) {
|
||||
uint32_t* dst = (uint32_t*)((uint8_t*)back_buf + (dst_y + row) * fb_pitch) + dst_x;
|
||||
const uint32_t* src = pixels + (src_y + row) * w + src_x;
|
||||
for (int col = 0; col < copy_w; col++) {
|
||||
uint32_t s = src[col];
|
||||
uint8_t sa = (s >> 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;
|
||||
dst[col] = s;
|
||||
continue;
|
||||
}
|
||||
|
||||
Color sc = {sr, sg, sb, sa};
|
||||
put_pixel_alpha(dx, dy, sc);
|
||||
dst[col] = blend_pixel(dst[col], s, sa);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline void copy_from(const uint32_t* pixels, int src_pitch_bytes) {
|
||||
if (!back_buf || !pixels || src_pitch_bytes <= 0) return;
|
||||
uint64_t row_bytes = (uint64_t)fb_width * sizeof(uint32_t);
|
||||
for (int row = 0; row < fb_height; row++) {
|
||||
uint32_t* dst = (uint32_t*)((uint8_t*)back_buf + row * fb_pitch);
|
||||
const uint32_t* src = (const uint32_t*)((const uint8_t*)pixels + row * src_pitch_bytes);
|
||||
montauk::memcpy(dst, src, row_bytes);
|
||||
}
|
||||
}
|
||||
|
||||
inline void clear(Color c) {
|
||||
fill_rect(0, 0, fb_width, fb_height, c);
|
||||
}
|
||||
|
||||
inline void flip() {
|
||||
if (!hw_fb || !back_buf) return;
|
||||
|
||||
// Copy back buffer to hardware framebuffer, row by row (pitch may differ)
|
||||
uint64_t row_bytes = (uint64_t)fb_width * sizeof(uint32_t);
|
||||
for (int y = 0; y < fb_height; y++) {
|
||||
void* src = (void*)((uint8_t*)back_buf + y * fb_pitch);
|
||||
void* dst = (void*)((uint8_t*)hw_fb + y * fb_pitch);
|
||||
uint32_t* src = (uint32_t*)((uint8_t*)back_buf + y * fb_pitch);
|
||||
uint32_t* dst = (uint32_t*)((uint8_t*)hw_fb + y * fb_pitch);
|
||||
montauk::memcpy(dst, src, row_bytes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* mtk.hpp
|
||||
* Umbrella include for the Montauk Toolkit
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "gui/mtk/theme.hpp"
|
||||
#include "gui/mtk/widgets.hpp"
|
||||
#include "gui/mtk/graph.hpp"
|
||||
#include "gui/mtk/table.hpp"
|
||||
#include "gui/mtk/hosts.hpp"
|
||||
@@ -0,0 +1,290 @@
|
||||
/*
|
||||
* graph.hpp
|
||||
* Montauk Toolkit graph helpers
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "gui/mtk/widgets.hpp"
|
||||
|
||||
namespace gui::mtk {
|
||||
|
||||
struct GraphHistory {
|
||||
uint64_t* samples;
|
||||
int capacity;
|
||||
int count;
|
||||
int head;
|
||||
};
|
||||
|
||||
struct GraphStyle {
|
||||
Color background;
|
||||
Color border;
|
||||
Color plot_bg;
|
||||
Color grid;
|
||||
Color label;
|
||||
Color value;
|
||||
Color line;
|
||||
Color fill;
|
||||
int radius;
|
||||
int padding;
|
||||
int grid_x;
|
||||
int grid_y;
|
||||
int line_thickness;
|
||||
bool fill_area;
|
||||
};
|
||||
|
||||
inline GraphStyle make_graph_style(const Theme& theme, Color line) {
|
||||
return {
|
||||
theme.window_bg,
|
||||
theme.border,
|
||||
theme.surface_alt,
|
||||
mix(theme.surface_alt, line, 18),
|
||||
theme.text_subtle,
|
||||
theme.text,
|
||||
line,
|
||||
lighten(line, 218),
|
||||
theme.radius_md,
|
||||
8,
|
||||
4,
|
||||
4,
|
||||
2,
|
||||
true,
|
||||
};
|
||||
}
|
||||
|
||||
inline void graph_history_init(GraphHistory* history, uint64_t* samples, int capacity) {
|
||||
if (!history) return;
|
||||
history->samples = samples;
|
||||
history->capacity = capacity;
|
||||
history->count = 0;
|
||||
history->head = 0;
|
||||
}
|
||||
|
||||
inline void graph_history_clear(GraphHistory* history) {
|
||||
if (!history) return;
|
||||
history->count = 0;
|
||||
history->head = 0;
|
||||
}
|
||||
|
||||
inline void graph_history_push(GraphHistory* history, uint64_t value) {
|
||||
if (!history || !history->samples || history->capacity <= 0) return;
|
||||
history->samples[history->head] = value;
|
||||
history->head = (history->head + 1) % history->capacity;
|
||||
if (history->count < history->capacity)
|
||||
history->count++;
|
||||
}
|
||||
|
||||
inline uint64_t graph_history_at(const GraphHistory& history, int index) {
|
||||
if (!history.samples || history.capacity <= 0 ||
|
||||
index < 0 || index >= history.count) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int start = history.head - history.count;
|
||||
while (start < 0) start += history.capacity;
|
||||
return history.samples[(start + index) % history.capacity];
|
||||
}
|
||||
|
||||
inline uint64_t graph_history_max(const GraphHistory& history) {
|
||||
uint64_t max_value = 0;
|
||||
for (int i = 0; i < history.count; i++) {
|
||||
uint64_t value = graph_history_at(history, i);
|
||||
if (value > max_value) max_value = value;
|
||||
}
|
||||
return max_value;
|
||||
}
|
||||
|
||||
inline int graph_sample_x(const Rect& plot, int index, int count) {
|
||||
if (count <= 1) return plot.x + plot.w - 1;
|
||||
return plot.x + (int)(((int64_t)index * (plot.w - 1)) / (count - 1));
|
||||
}
|
||||
|
||||
inline int graph_sample_y(const Rect& plot, uint64_t sample, uint64_t max_value) {
|
||||
if (plot.h <= 1) return plot.y;
|
||||
if (max_value == 0) max_value = 1;
|
||||
if (sample > max_value) sample = max_value;
|
||||
uint64_t scaled = (sample * (uint64_t)(plot.h - 1)) / max_value;
|
||||
return plot.y + plot.h - 1 - (int)scaled;
|
||||
}
|
||||
|
||||
inline void graph_draw_point(Canvas& c,
|
||||
const Rect& clip,
|
||||
int x,
|
||||
int y,
|
||||
Color color,
|
||||
int thickness) {
|
||||
thickness = gui_max(thickness, 1);
|
||||
int radius = thickness / 2;
|
||||
for (int py = y - radius; py <= y - radius + thickness - 1; py++) {
|
||||
for (int px = x - radius; px <= x - radius + thickness - 1; px++) {
|
||||
if (clip.contains(px, py))
|
||||
c.put_pixel(px, py, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline void graph_draw_line(Canvas& c,
|
||||
const Rect& clip,
|
||||
int x0,
|
||||
int y0,
|
||||
int x1,
|
||||
int y1,
|
||||
Color color,
|
||||
int thickness) {
|
||||
int dx = gui_abs(x1 - x0);
|
||||
int sx = x0 < x1 ? 1 : -1;
|
||||
int dy = -gui_abs(y1 - y0);
|
||||
int sy = y0 < y1 ? 1 : -1;
|
||||
int err = dx + dy;
|
||||
|
||||
for (;;) {
|
||||
graph_draw_point(c, clip, x0, y0, color, thickness);
|
||||
if (x0 == x1 && y0 == y1) break;
|
||||
int e2 = 2 * err;
|
||||
if (e2 >= dy) {
|
||||
err += dy;
|
||||
x0 += sx;
|
||||
}
|
||||
if (e2 <= dx) {
|
||||
err += dx;
|
||||
y0 += sy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline void graph_fill_segment(Canvas& c,
|
||||
const Rect& clip,
|
||||
int x0,
|
||||
int y0,
|
||||
int x1,
|
||||
int y1,
|
||||
Color color) {
|
||||
int bottom = clip.y + clip.h - 1;
|
||||
if (x0 == x1) {
|
||||
int y = gui_min(y0, y1);
|
||||
if (clip.contains(x0, y))
|
||||
c.vline(x0, y, bottom - y + 1, color);
|
||||
return;
|
||||
}
|
||||
|
||||
if (x1 < x0) {
|
||||
int tx = x0; x0 = x1; x1 = tx;
|
||||
int ty = y0; y0 = y1; y1 = ty;
|
||||
}
|
||||
|
||||
for (int x = x0; x <= x1; x++) {
|
||||
if (x < clip.x || x >= clip.x + clip.w) continue;
|
||||
int y = y0 + (int)(((int64_t)(y1 - y0) * (x - x0)) / (x1 - x0));
|
||||
if (y < clip.y) y = clip.y;
|
||||
if (y > bottom) y = bottom;
|
||||
c.vline(x, y, bottom - y + 1, color);
|
||||
}
|
||||
}
|
||||
|
||||
inline void draw_line_graph(Canvas& c,
|
||||
const Rect& bounds,
|
||||
const GraphHistory& history,
|
||||
uint64_t max_value,
|
||||
const char* title,
|
||||
const char* value_label,
|
||||
const Theme& theme,
|
||||
const GraphStyle& style) {
|
||||
if (bounds.empty()) return;
|
||||
|
||||
draw_rounded_frame(c, bounds, style.radius, style.background, style.border);
|
||||
|
||||
int fh = system_font_height();
|
||||
int pad = gui_max(style.padding, 4);
|
||||
int title_y = bounds.y + pad;
|
||||
int title_x = bounds.x + pad;
|
||||
|
||||
if (title && title[0])
|
||||
c.text(title_x, title_y, title, style.label);
|
||||
|
||||
if (value_label && value_label[0]) {
|
||||
int value_w = text_width(value_label);
|
||||
int value_x = bounds.x + bounds.w - pad - value_w;
|
||||
if (value_x > title_x)
|
||||
c.text(value_x, title_y, value_label, style.value);
|
||||
}
|
||||
|
||||
Rect plot = {
|
||||
bounds.x + pad,
|
||||
bounds.y + pad + fh + 7,
|
||||
bounds.w - pad * 2,
|
||||
bounds.h - pad * 2 - fh - 7
|
||||
};
|
||||
if (plot.empty()) return;
|
||||
|
||||
c.fill_rect(plot.x, plot.y, plot.w, plot.h, style.plot_bg);
|
||||
|
||||
int sample_count = history.count;
|
||||
if (sample_count > 0) {
|
||||
if (max_value == 0)
|
||||
max_value = graph_history_max(history);
|
||||
if (max_value == 0)
|
||||
max_value = 1;
|
||||
|
||||
if (style.fill_area && sample_count > 1) {
|
||||
int prev_x = graph_sample_x(plot, 0, sample_count);
|
||||
int prev_y = graph_sample_y(plot, graph_history_at(history, 0), max_value);
|
||||
for (int i = 1; i < sample_count; i++) {
|
||||
int x = graph_sample_x(plot, i, sample_count);
|
||||
int y = graph_sample_y(plot, graph_history_at(history, i), max_value);
|
||||
graph_fill_segment(c, plot, prev_x, prev_y, x, y, style.fill);
|
||||
prev_x = x;
|
||||
prev_y = y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (style.grid_y > 0) {
|
||||
for (int i = 1; i < style.grid_y; i++) {
|
||||
int y = plot.y + (plot.h * i) / style.grid_y;
|
||||
c.hline(plot.x, y, plot.w, style.grid);
|
||||
}
|
||||
}
|
||||
|
||||
if (style.grid_x > 0) {
|
||||
for (int i = 1; i < style.grid_x; i++) {
|
||||
int x = plot.x + (plot.w * i) / style.grid_x;
|
||||
c.vline(x, plot.y, plot.h, style.grid);
|
||||
}
|
||||
}
|
||||
|
||||
c.rect(plot.x, plot.y, plot.w, plot.h, style.border);
|
||||
|
||||
if (sample_count == 1) {
|
||||
int x = graph_sample_x(plot, 0, sample_count);
|
||||
int y = graph_sample_y(plot, graph_history_at(history, 0), max_value);
|
||||
graph_draw_point(c, plot, x, y, style.line, style.line_thickness + 1);
|
||||
} else if (sample_count > 1) {
|
||||
int prev_x = graph_sample_x(plot, 0, sample_count);
|
||||
int prev_y = graph_sample_y(plot, graph_history_at(history, 0), max_value);
|
||||
for (int i = 1; i < sample_count; i++) {
|
||||
int x = graph_sample_x(plot, i, sample_count);
|
||||
int y = graph_sample_y(plot, graph_history_at(history, i), max_value);
|
||||
graph_draw_line(c, plot, prev_x, prev_y, x, y,
|
||||
style.line, style.line_thickness);
|
||||
prev_x = x;
|
||||
prev_y = y;
|
||||
}
|
||||
}
|
||||
|
||||
(void)theme;
|
||||
}
|
||||
|
||||
inline void draw_line_graph(Canvas& c,
|
||||
const Rect& bounds,
|
||||
const GraphHistory& history,
|
||||
uint64_t max_value,
|
||||
const char* title,
|
||||
const char* value_label,
|
||||
const Theme& theme,
|
||||
Color line) {
|
||||
GraphStyle style = make_graph_style(theme, line);
|
||||
draw_line_graph(c, bounds, history, max_value, title, value_label, theme, style);
|
||||
}
|
||||
|
||||
} // namespace gui::mtk
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* hosts.hpp
|
||||
* Montauk Toolkit host adapters for desktop and standalone apps
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "gui/canvas.hpp"
|
||||
#include "gui/standalone.hpp"
|
||||
#include "gui/window.hpp"
|
||||
|
||||
namespace gui::mtk {
|
||||
|
||||
struct DesktopHost {
|
||||
Window* window;
|
||||
|
||||
explicit DesktopHost(Window* win) : window(win) {}
|
||||
|
||||
Canvas canvas() const {
|
||||
return window ? Canvas(window) : Canvas(nullptr, 0, 0);
|
||||
}
|
||||
|
||||
Rect bounds() const {
|
||||
return {0, 0, window ? window->content_w : 0, window ? window->content_h : 0};
|
||||
}
|
||||
|
||||
bool map_mouse(const MouseEvent& ev, int* out_x, int* out_y) const {
|
||||
if (!window || !out_x || !out_y) return false;
|
||||
Rect cr = window->content_rect();
|
||||
*out_x = ev.x - cr.x;
|
||||
*out_y = ev.y - cr.y;
|
||||
return true;
|
||||
}
|
||||
|
||||
void invalidate() const {
|
||||
if (window) window->dirty = true;
|
||||
}
|
||||
};
|
||||
|
||||
struct StandaloneHost {
|
||||
WsWindow* window;
|
||||
|
||||
explicit StandaloneHost(WsWindow* win) : window(win) {}
|
||||
|
||||
Canvas canvas() const {
|
||||
return window ? window->canvas() : Canvas(nullptr, 0, 0);
|
||||
}
|
||||
|
||||
Rect bounds() const {
|
||||
return {0, 0, window ? window->width : 0, window ? window->height : 0};
|
||||
}
|
||||
|
||||
void present() const {
|
||||
if (window) window->present();
|
||||
}
|
||||
|
||||
void set_cursor(int cursor) const {
|
||||
if (window) window->set_cursor(cursor);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace gui::mtk
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* settings.hpp
|
||||
* Montauk Toolkit helpers for loading desktop appearance settings
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <montauk/config.h>
|
||||
#include <montauk/syscall.h>
|
||||
#include "gui/gui.hpp"
|
||||
|
||||
namespace gui::mtk {
|
||||
|
||||
inline Color color_from_rgb_int(int64_t rgb, Color fallback = colors::ACCENT) {
|
||||
if (rgb < 0) return fallback;
|
||||
return Color::from_rgb((uint8_t)((rgb >> 16) & 0xFF),
|
||||
(uint8_t)((rgb >> 8) & 0xFF),
|
||||
(uint8_t)(rgb & 0xFF));
|
||||
}
|
||||
|
||||
inline Color load_system_accent(Color fallback = colors::ACCENT) {
|
||||
char user[64] = {};
|
||||
if (montauk::getuser(user, sizeof(user)) > 0 && user[0]) {
|
||||
auto doc = montauk::config::load_user(user, "desktop");
|
||||
int64_t accent = doc.get_int("appearance.accent_color", -1);
|
||||
if (accent >= 0) {
|
||||
Color color = color_from_rgb_int(accent, fallback);
|
||||
doc.destroy();
|
||||
return color;
|
||||
}
|
||||
doc.destroy();
|
||||
}
|
||||
|
||||
auto doc = montauk::config::load("desktop");
|
||||
int64_t accent = doc.get_int("appearance.accent_color", -1);
|
||||
Color color = color_from_rgb_int(accent, fallback);
|
||||
doc.destroy();
|
||||
return color;
|
||||
}
|
||||
|
||||
} // namespace gui::mtk
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* table.hpp
|
||||
* Montauk Toolkit table/list helpers
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "gui/canvas.hpp"
|
||||
#include "gui/mtk/theme.hpp"
|
||||
|
||||
namespace gui::mtk {
|
||||
|
||||
struct TableColumn {
|
||||
const char* label;
|
||||
int x;
|
||||
int w;
|
||||
};
|
||||
|
||||
struct TableLayout {
|
||||
Rect header;
|
||||
Rect body;
|
||||
int row_h;
|
||||
};
|
||||
|
||||
struct TableStyle {
|
||||
Color header_bg;
|
||||
Color header_border;
|
||||
Color header_text;
|
||||
Color row_selected_bg;
|
||||
Color row_hover_bg;
|
||||
};
|
||||
|
||||
inline TableStyle make_table_style(const Theme& theme) {
|
||||
return {
|
||||
theme.surface_alt,
|
||||
theme.border,
|
||||
theme.text_subtle,
|
||||
theme.accent_soft,
|
||||
colors::TRANSPARENT,
|
||||
};
|
||||
}
|
||||
|
||||
inline int table_visible_rows(const TableLayout& layout) {
|
||||
if (layout.row_h <= 0 || layout.body.h <= 0) return 0;
|
||||
return layout.body.h / layout.row_h;
|
||||
}
|
||||
|
||||
inline int table_max_scroll(const TableLayout& layout, int row_count) {
|
||||
int visible = table_visible_rows(layout);
|
||||
if (row_count > visible && visible > 0) return row_count - visible;
|
||||
return 0;
|
||||
}
|
||||
|
||||
inline void clamp_table_scroll(int* scroll, const TableLayout& layout, int row_count) {
|
||||
if (!scroll) return;
|
||||
*scroll = gui_clamp(*scroll, 0, table_max_scroll(layout, row_count));
|
||||
}
|
||||
|
||||
inline void ensure_table_row_visible(int* scroll,
|
||||
const TableLayout& layout,
|
||||
int row_count,
|
||||
int selected) {
|
||||
if (!scroll || selected < 0) return;
|
||||
int visible = table_visible_rows(layout);
|
||||
if (visible <= 0) return;
|
||||
if (selected < *scroll) {
|
||||
*scroll = selected;
|
||||
} else if (selected >= *scroll + visible) {
|
||||
*scroll = selected - visible + 1;
|
||||
}
|
||||
clamp_table_scroll(scroll, layout, row_count);
|
||||
}
|
||||
|
||||
inline Rect table_row_rect(const TableLayout& layout, int scroll, int row_index) {
|
||||
return {
|
||||
layout.body.x,
|
||||
layout.body.y + (row_index - scroll) * layout.row_h,
|
||||
layout.body.w,
|
||||
layout.row_h
|
||||
};
|
||||
}
|
||||
|
||||
inline Rect table_cell_rect(const Rect& row_rect, const TableColumn& col) {
|
||||
return {row_rect.x + col.x, row_rect.y, col.w, row_rect.h};
|
||||
}
|
||||
|
||||
inline int table_hit_row(const TableLayout& layout,
|
||||
int scroll,
|
||||
int row_count,
|
||||
int mx,
|
||||
int my) {
|
||||
if (!layout.body.contains(mx, my) || layout.row_h <= 0) return -1;
|
||||
int row = (my - layout.body.y) / layout.row_h;
|
||||
int index = scroll + row;
|
||||
if (index < 0 || index >= row_count) return -1;
|
||||
return index;
|
||||
}
|
||||
|
||||
using TableRowRenderer = void (*)(Canvas& c,
|
||||
void* userdata,
|
||||
int row_index,
|
||||
const Rect& row_rect,
|
||||
int text_y,
|
||||
const TableColumn* cols,
|
||||
int col_count,
|
||||
const Theme& theme,
|
||||
const TableStyle& style);
|
||||
|
||||
inline void draw_table(Canvas& c,
|
||||
const TableLayout& layout,
|
||||
const TableColumn* cols,
|
||||
int col_count,
|
||||
int row_count,
|
||||
int scroll,
|
||||
int selected,
|
||||
int hovered,
|
||||
const Theme& theme,
|
||||
const TableStyle& style,
|
||||
TableRowRenderer draw_row,
|
||||
void* userdata) {
|
||||
c.fill_rect(layout.header.x, layout.header.y, layout.header.w, layout.header.h, style.header_bg);
|
||||
c.hline(layout.header.x, layout.header.y + layout.header.h - 1, layout.header.w, style.header_border);
|
||||
|
||||
int fh = system_font_height();
|
||||
int header_text_y = layout.header.y + (layout.header.h - fh) / 2;
|
||||
for (int i = 0; i < col_count; i++) {
|
||||
c.text(layout.header.x + cols[i].x, header_text_y, cols[i].label, style.header_text);
|
||||
}
|
||||
|
||||
int visible = table_visible_rows(layout);
|
||||
for (int row = 0; row < visible; row++) {
|
||||
int index = scroll + row;
|
||||
if (index >= row_count) break;
|
||||
|
||||
Rect row_rect = table_row_rect(layout, scroll, index);
|
||||
if (index == selected) {
|
||||
c.fill_rect(row_rect.x, row_rect.y, row_rect.w, row_rect.h, style.row_selected_bg);
|
||||
} else if (index == hovered && style.row_hover_bg.a > 0) {
|
||||
c.fill_rect(row_rect.x, row_rect.y, row_rect.w, row_rect.h, style.row_hover_bg);
|
||||
}
|
||||
|
||||
if (draw_row) {
|
||||
int text_y = row_rect.y + (layout.row_h - fh) / 2;
|
||||
draw_row(c, userdata, index, row_rect, text_y, cols, col_count, theme, style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace gui::mtk
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* theme.hpp
|
||||
* Montauk Toolkit theme tokens and color helpers
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "gui/gui.hpp"
|
||||
#include "gui/mtk/settings.hpp"
|
||||
|
||||
namespace gui::mtk {
|
||||
|
||||
inline Color mix(Color base, Color tint, uint8_t tint_alpha) {
|
||||
auto blend = [tint_alpha](uint8_t a, uint8_t b) -> uint8_t {
|
||||
return (uint8_t)((((uint16_t)a * (255 - tint_alpha)) +
|
||||
((uint16_t)b * tint_alpha) + 127) / 255);
|
||||
};
|
||||
return Color::from_rgb(blend(base.r, tint.r),
|
||||
blend(base.g, tint.g),
|
||||
blend(base.b, tint.b));
|
||||
}
|
||||
|
||||
inline Color lighten(Color c, uint8_t amount) {
|
||||
return mix(c, colors::WHITE, amount);
|
||||
}
|
||||
|
||||
inline Color darken(Color c, uint8_t amount) {
|
||||
return mix(c, colors::BLACK, amount);
|
||||
}
|
||||
|
||||
// Light selection/hover tint derived from the desktop accent. Returns the
|
||||
// canonical MENU_HOVER for the default blue accent so existing visuals are
|
||||
// preserved exactly; otherwise tints white with ~11% of the accent.
|
||||
inline Color accent_hover_tint(Color accent) {
|
||||
if (accent.r == colors::ACCENT.r && accent.g == colors::ACCENT.g
|
||||
&& accent.b == colors::ACCENT.b)
|
||||
return colors::MENU_HOVER;
|
||||
return mix(colors::WHITE, accent, 28);
|
||||
}
|
||||
|
||||
struct Theme {
|
||||
Color window_bg;
|
||||
Color surface;
|
||||
Color surface_alt;
|
||||
Color surface_hover;
|
||||
Color border;
|
||||
Color text;
|
||||
Color text_muted;
|
||||
Color text_subtle;
|
||||
Color text_inverse;
|
||||
|
||||
Color accent;
|
||||
Color accent_hover;
|
||||
Color accent_soft;
|
||||
Color accent_fg;
|
||||
Color selection;
|
||||
Color text_selection;
|
||||
|
||||
Color danger;
|
||||
Color danger_hover;
|
||||
Color danger_soft;
|
||||
Color danger_fg;
|
||||
|
||||
Color disabled_bg;
|
||||
Color disabled_fg;
|
||||
|
||||
Color badge_admin_bg;
|
||||
Color badge_admin_fg;
|
||||
Color badge_user_bg;
|
||||
Color badge_user_fg;
|
||||
|
||||
int radius_sm;
|
||||
int radius_md;
|
||||
int control_h;
|
||||
int tab_h;
|
||||
int gap_xs;
|
||||
int gap_sm;
|
||||
int gap_md;
|
||||
int gap_lg;
|
||||
};
|
||||
|
||||
inline Theme make_theme(Color accent) {
|
||||
Theme theme {};
|
||||
theme.window_bg = colors::WINDOW_BG;
|
||||
theme.surface = Color::from_rgb(0xF5, 0xF5, 0xF5);
|
||||
theme.surface_alt = Color::from_rgb(0xF8, 0xF8, 0xF8);
|
||||
theme.surface_hover = mix(theme.surface, accent, 18);
|
||||
theme.border = colors::BORDER;
|
||||
theme.text = colors::TEXT_COLOR;
|
||||
theme.text_muted = Color::from_rgb(0x88, 0x88, 0x88);
|
||||
theme.text_subtle = Color::from_rgb(0x66, 0x66, 0x66);
|
||||
theme.text_inverse = colors::WHITE;
|
||||
|
||||
theme.accent = accent;
|
||||
theme.accent_hover = darken(accent, 32);
|
||||
theme.accent_soft = lighten(accent, 214);
|
||||
theme.accent_fg = colors::WHITE;
|
||||
theme.selection = accent;
|
||||
theme.text_selection = Color::from_rgb(0xB0, 0xD0, 0xF0);
|
||||
|
||||
theme.danger = Color::from_rgb(0xD0, 0x3E, 0x3E);
|
||||
theme.danger_hover = Color::from_rgb(0xDD, 0x44, 0x44);
|
||||
theme.danger_soft = Color::from_rgb(0xF7, 0xDF, 0xDF);
|
||||
theme.danger_fg = colors::WHITE;
|
||||
|
||||
theme.disabled_bg = Color::from_rgb(0xCC, 0xCC, 0xCC);
|
||||
theme.disabled_fg = colors::WHITE;
|
||||
|
||||
theme.badge_admin_bg = Color::from_rgb(0xE8, 0xD8, 0xF0);
|
||||
theme.badge_admin_fg = Color::from_rgb(0x7B, 0x3E, 0xB8);
|
||||
theme.badge_user_bg = Color::from_rgb(0xE0, 0xE8, 0xF0);
|
||||
theme.badge_user_fg = Color::from_rgb(0x36, 0x7B, 0xF0);
|
||||
|
||||
theme.radius_sm = 3;
|
||||
theme.radius_md = 4;
|
||||
theme.control_h = 30;
|
||||
theme.tab_h = 36;
|
||||
theme.gap_xs = 2;
|
||||
theme.gap_sm = 6;
|
||||
theme.gap_md = 10;
|
||||
theme.gap_lg = 16;
|
||||
return theme;
|
||||
}
|
||||
|
||||
// Zero-arg overload pulls the accent from the desktop appearance settings, so
|
||||
// MTK widgets (buttons, menus, etc.) automatically pick up the user's chosen
|
||||
// accent without each caller having to plumb it through.
|
||||
inline Theme make_theme() {
|
||||
return make_theme(load_system_accent());
|
||||
}
|
||||
|
||||
} // namespace gui::mtk
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -123,6 +123,34 @@ struct SvgCssTable {
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Integer sqrt (binary method, no floating point).
|
||||
// Returns floor(sqrt(n)) for any uint64.
|
||||
// ---------------------------------------------------------------------------
|
||||
inline uint64_t svg_int64_sqrt(uint64_t n) {
|
||||
if (n == 0) return 0;
|
||||
uint64_t r = 0;
|
||||
uint64_t bit = (uint64_t)1 << 62;
|
||||
while (bit > n) bit >>= 2;
|
||||
while (bit != 0) {
|
||||
if (n >= r + bit) {
|
||||
n -= r + bit;
|
||||
r = (r >> 1) + bit;
|
||||
} else {
|
||||
r >>= 1;
|
||||
}
|
||||
bit >>= 2;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
// 16.16 fixed-point hypotenuse: sqrt(x*x + y*y).
|
||||
inline fixed_t svg_fixed_hypot(fixed_t x, fixed_t y) {
|
||||
int64_t sq = (int64_t)x * x + (int64_t)y * y;
|
||||
if (sq < 0) sq = 0;
|
||||
return (fixed_t)svg_int64_sqrt((uint64_t)sq);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fixed-point number parser (NO floating point)
|
||||
// Parses strings like "3.25", "-0.5", ".1115", "16"
|
||||
@@ -566,6 +594,47 @@ inline void svg_path_to_edges(SvgEdgeList& el, const char* d, int dLen,
|
||||
*oy = fixed_mul(y - off_y, scale_y);
|
||||
};
|
||||
|
||||
// Emit a circular arc on (cx, cy, r) from p1 to p2 by recursive
|
||||
// chord-midpoint subdivision. Assumes the arc is <= 180deg; callers split
|
||||
// large arcs at the far point first. Stack-based to avoid recursive lambdas.
|
||||
auto emit_small_arc = [&](fixed_t cx, fixed_t cy, fixed_t r,
|
||||
fixed_t p1x, fixed_t p1y,
|
||||
fixed_t p2x, fixed_t p2y) {
|
||||
constexpr int ARC_MAX_DEPTH = 4; // up to 16 chords per sub-arc
|
||||
struct Seg { fixed_t ax, ay, bx, by; int depth; };
|
||||
Seg stk[16];
|
||||
int sp = 0;
|
||||
stk[sp++] = { p1x, p1y, p2x, p2y, ARC_MAX_DEPTH };
|
||||
while (sp > 0) {
|
||||
Seg s = stk[--sp];
|
||||
if (s.depth == 0) {
|
||||
fixed_t sx0, sy0, sx1, sy1;
|
||||
scale_pt(s.ax, s.ay, &sx0, &sy0);
|
||||
scale_pt(s.bx, s.by, &sx1, &sy1);
|
||||
el.add(sx0, sy0, sx1, sy1);
|
||||
continue;
|
||||
}
|
||||
fixed_t mx = (s.ax + s.bx) / 2;
|
||||
fixed_t my = (s.ay + s.by) / 2;
|
||||
fixed_t vx = mx - cx;
|
||||
fixed_t vy = my - cy;
|
||||
fixed_t vlen = svg_fixed_hypot(vx, vy);
|
||||
if (vlen == 0) {
|
||||
// Degenerate: chord midpoint coincides with center.
|
||||
fixed_t sx0, sy0, sx1, sy1;
|
||||
scale_pt(s.ax, s.ay, &sx0, &sy0);
|
||||
scale_pt(s.bx, s.by, &sx1, &sy1);
|
||||
el.add(sx0, sy0, sx1, sy1);
|
||||
continue;
|
||||
}
|
||||
fixed_t k = fixed_div(r, vlen);
|
||||
fixed_t amx = cx + fixed_mul(vx, k);
|
||||
fixed_t amy = cy + fixed_mul(vy, k);
|
||||
stk[sp++] = { amx, amy, s.bx, s.by, s.depth - 1 };
|
||||
stk[sp++] = { s.ax, s.ay, amx, amy, s.depth - 1 };
|
||||
}
|
||||
};
|
||||
|
||||
while (pp.has_more()) {
|
||||
char cmd = '\0';
|
||||
|
||||
@@ -773,23 +842,90 @@ inline void svg_path_to_edges(SvgEdgeList& el, const char* d, int dLen,
|
||||
break;
|
||||
}
|
||||
case 'A': case 'a': {
|
||||
// Arc command: consume parameters but approximate as a line
|
||||
// (arcs are rare in these icons)
|
||||
fixed_t rx = pp.read_number();
|
||||
fixed_t ry = pp.read_number();
|
||||
pp.read_number(); // x-rotation
|
||||
pp.read_number(); // large-arc-flag
|
||||
pp.read_number(); // sweep-flag
|
||||
// Elliptical-arc command. We treat the arc as circular using
|
||||
// r = min(|rx|, |ry|), which is exact for symbolic icons (all use
|
||||
// equal axes) and a reasonable approximation otherwise.
|
||||
fixed_t rx_in = pp.read_number();
|
||||
fixed_t ry_in = pp.read_number();
|
||||
pp.read_number(); // x-axis rotation (ignored)
|
||||
fixed_t fa_f = pp.read_number(); // large-arc-flag
|
||||
fixed_t fs_f = pp.read_number(); // sweep-flag
|
||||
fixed_t x = pp.read_number();
|
||||
fixed_t y = pp.read_number();
|
||||
if (cmd == 'a') { x += cur_x; y += cur_y; }
|
||||
fixed_t sx0, sy0, sx1, sy1;
|
||||
scale_pt(cur_x, cur_y, &sx0, &sy0);
|
||||
scale_pt(x, y, &sx1, &sy1);
|
||||
el.add(sx0, sy0, sx1, sy1);
|
||||
|
||||
fixed_t r = (rx_in < 0) ? -rx_in : rx_in;
|
||||
fixed_t ry_abs = (ry_in < 0) ? -ry_in : ry_in;
|
||||
if (ry_abs < r) r = ry_abs;
|
||||
|
||||
int fa = (fa_f != 0) ? 1 : 0;
|
||||
int fs = (fs_f != 0) ? 1 : 0;
|
||||
|
||||
fixed_t dx = x - cur_x;
|
||||
fixed_t dy = y - cur_y;
|
||||
if ((dx == 0 && dy == 0) || r == 0) {
|
||||
// Per spec: degenerate arc becomes a line.
|
||||
fixed_t sx0, sy0, sx1, sy1;
|
||||
scale_pt(cur_x, cur_y, &sx0, &sy0);
|
||||
scale_pt(x, y, &sx1, &sy1);
|
||||
el.add(sx0, sy0, sx1, sy1);
|
||||
cur_x = x; cur_y = y;
|
||||
last_cmd = cmd;
|
||||
break;
|
||||
}
|
||||
|
||||
// Endpoint-to-center: midpoint of chord, half-chord length,
|
||||
// perpendicular height from chord to center.
|
||||
fixed_t mx = cur_x + dx / 2;
|
||||
fixed_t my = cur_y + dy / 2;
|
||||
fixed_t half_chord = svg_fixed_hypot(dx / 2, dy / 2);
|
||||
if (r < half_chord) r = half_chord; // SVG spec: snap radius up
|
||||
int64_t r_sq = (int64_t)r * r;
|
||||
int64_t hc_sq = (int64_t)half_chord * half_chord;
|
||||
int64_t h_sq = r_sq - hc_sq;
|
||||
if (h_sq < 0) h_sq = 0;
|
||||
fixed_t h = (fixed_t)svg_int64_sqrt((uint64_t)h_sq);
|
||||
|
||||
// Center = mid + sign * (h / chord_len) * (-dy, dx)
|
||||
// sign: spec F.6.5 says -1 when fa == fs, else +1.
|
||||
fixed_t chord_len = 2 * half_chord;
|
||||
if (chord_len == 0) chord_len = 1;
|
||||
fixed_t h_over_chord = fixed_div(h, chord_len);
|
||||
int sign = (fa == fs) ? -1 : 1;
|
||||
fixed_t off_x_perp = fixed_mul(-dy, h_over_chord);
|
||||
fixed_t off_y_perp = fixed_mul( dx, h_over_chord);
|
||||
if (sign < 0) { off_x_perp = -off_x_perp; off_y_perp = -off_y_perp; }
|
||||
fixed_t cx_ctr = mx + off_x_perp;
|
||||
fixed_t cy_ctr = my + off_y_perp;
|
||||
|
||||
if (fa == 1) {
|
||||
// Large arc: bisect at the far point (opposite side of center
|
||||
// from chord midpoint), then walk each half as a small arc.
|
||||
fixed_t vx = mx - cx_ctr;
|
||||
fixed_t vy = my - cy_ctr;
|
||||
fixed_t vlen = svg_fixed_hypot(vx, vy);
|
||||
fixed_t am_x, am_y;
|
||||
if (vlen == 0) {
|
||||
// 180-degree arc: bisect using chord-perpendicular instead.
|
||||
fixed_t k = fixed_div(r, chord_len);
|
||||
fixed_t px = fixed_mul(-dy, k);
|
||||
fixed_t py = fixed_mul( dx, k);
|
||||
if (fs == 0) { px = -px; py = -py; }
|
||||
am_x = cx_ctr + px;
|
||||
am_y = cy_ctr + py;
|
||||
} else {
|
||||
fixed_t k = fixed_div(-r, vlen);
|
||||
am_x = cx_ctr + fixed_mul(vx, k);
|
||||
am_y = cy_ctr + fixed_mul(vy, k);
|
||||
}
|
||||
emit_small_arc(cx_ctr, cy_ctr, r, cur_x, cur_y, am_x, am_y);
|
||||
emit_small_arc(cx_ctr, cy_ctr, r, am_x, am_y, x, y);
|
||||
} else {
|
||||
emit_small_arc(cx_ctr, cy_ctr, r, cur_x, cur_y, x, y);
|
||||
}
|
||||
|
||||
cur_x = x; cur_y = y;
|
||||
last_cmd = cmd;
|
||||
(void)rx; (void)ry;
|
||||
break;
|
||||
}
|
||||
case 'Z': case 'z': {
|
||||
@@ -1027,6 +1163,42 @@ inline bool svg_element_has_filter(const char* elem, int elemLen) {
|
||||
return svg_get_attr(elem, elemLen, " filter", buf, sizeof(buf)) > 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Parse transform="scale(x)" or transform="scale(x,y)" or translate(x,y)
|
||||
// Returns true if a scale was found; sx/sy are fixed-point scale factors.
|
||||
// ---------------------------------------------------------------------------
|
||||
inline bool svg_get_element_scale(const char* elem, int elemLen,
|
||||
fixed_t* sx, fixed_t* sy) {
|
||||
char buf[64];
|
||||
if (svg_get_attr(elem, elemLen, " transform", buf, sizeof(buf)) <= 0) {
|
||||
*sx = int_to_fixed(1);
|
||||
*sy = int_to_fixed(1);
|
||||
return false;
|
||||
}
|
||||
// Look for "scale("
|
||||
const char* sp = buf;
|
||||
while (*sp) {
|
||||
if (sp[0]=='s' && sp[1]=='c' && sp[2]=='a' && sp[3]=='l' && sp[4]=='e' && sp[5]=='(') {
|
||||
sp += 6;
|
||||
svg_parse_fixed(sp, sx);
|
||||
// Skip to comma or closing paren
|
||||
while (*sp && *sp != ',' && *sp != ')') ++sp;
|
||||
if (*sp == ',') {
|
||||
++sp;
|
||||
while (*sp == ' ') ++sp;
|
||||
svg_parse_fixed(sp, sy);
|
||||
} else {
|
||||
*sy = *sx; // uniform scale
|
||||
}
|
||||
return true;
|
||||
}
|
||||
++sp;
|
||||
}
|
||||
*sx = int_to_fixed(1);
|
||||
*sy = int_to_fixed(1);
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Scanline rasterizer with alpha blending (for multi-color SVGs)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1314,11 +1486,17 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t
|
||||
|
||||
int alpha = svg_get_element_opacity(elem_start, elem_len);
|
||||
|
||||
// Per-element transform (scale)
|
||||
fixed_t elem_sx, elem_sy;
|
||||
svg_get_element_scale(elem_start, elem_len, &elem_sx, &elem_sy);
|
||||
fixed_t eff_sx = fixed_mul(scale_x, elem_sx);
|
||||
fixed_t eff_sy = fixed_mul(scale_y, elem_sy);
|
||||
|
||||
// Extract and rasterize path
|
||||
int d_len = svg_get_attr(elem_start, elem_len, " d", d_buf, SVG_MAX_PATH_LEN);
|
||||
if (d_len > 0) {
|
||||
el.clear();
|
||||
svg_path_to_edges(el, d_buf, d_len, scale_x, scale_y, vb_x, vb_y);
|
||||
svg_path_to_edges(el, d_buf, d_len, eff_sx, eff_sy, vb_x, vb_y);
|
||||
if (el.count > 0)
|
||||
svg_rasterize_blend(el, icon.pixels, target_w, target_h, elem_color.to_pixel(), alpha);
|
||||
}
|
||||
@@ -1349,6 +1527,11 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t
|
||||
|
||||
int alpha = svg_get_element_opacity(elem_start, elem_len);
|
||||
|
||||
fixed_t elem_sx, elem_sy;
|
||||
svg_get_element_scale(elem_start, elem_len, &elem_sx, &elem_sy);
|
||||
fixed_t eff_sx = fixed_mul(scale_x, elem_sx);
|
||||
fixed_t eff_sy = fixed_mul(scale_y, elem_sy);
|
||||
|
||||
char attr_buf[32];
|
||||
fixed_t cx = 0, cy = 0, r = 0;
|
||||
if (svg_get_attr(elem_start, elem_len, " cx", attr_buf, sizeof(attr_buf)) > 0)
|
||||
@@ -1358,10 +1541,10 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t
|
||||
if (svg_get_attr(elem_start, elem_len, " r", attr_buf, sizeof(attr_buf)) > 0)
|
||||
svg_parse_fixed(attr_buf, &r);
|
||||
|
||||
fixed_t scx = fixed_mul(cx - vb_x, scale_x);
|
||||
fixed_t scy = fixed_mul(cy - vb_y, scale_y);
|
||||
fixed_t srx = fixed_mul(r, scale_x);
|
||||
fixed_t sry = fixed_mul(r, scale_y);
|
||||
fixed_t scx = fixed_mul(cx - vb_x, eff_sx);
|
||||
fixed_t scy = fixed_mul(cy - vb_y, eff_sy);
|
||||
fixed_t srx = fixed_mul(r, eff_sx);
|
||||
fixed_t sry = fixed_mul(r, eff_sy);
|
||||
fixed_t sr = (srx + sry) >> 1;
|
||||
|
||||
el.clear();
|
||||
@@ -1395,6 +1578,11 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t
|
||||
|
||||
int alpha = svg_get_element_opacity(elem_start, elem_len);
|
||||
|
||||
fixed_t elem_sx, elem_sy;
|
||||
svg_get_element_scale(elem_start, elem_len, &elem_sx, &elem_sy);
|
||||
fixed_t eff_sx = fixed_mul(scale_x, elem_sx);
|
||||
fixed_t eff_sy = fixed_mul(scale_y, elem_sy);
|
||||
|
||||
char attr_buf[32];
|
||||
fixed_t rx_val = 0, ry_val = 0, rw = 0, rh = 0, rrx = 0, rry = 0;
|
||||
|
||||
@@ -1411,12 +1599,12 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t
|
||||
if (svg_get_attr(elem_start, elem_len, " ry", attr_buf, sizeof(attr_buf)) > 0)
|
||||
svg_parse_fixed(attr_buf, &rry);
|
||||
|
||||
fixed_t sx = fixed_mul(rx_val - vb_x, scale_x);
|
||||
fixed_t sy = fixed_mul(ry_val - vb_y, scale_y);
|
||||
fixed_t sw = fixed_mul(rw, scale_x);
|
||||
fixed_t sh = fixed_mul(rh, scale_y);
|
||||
fixed_t srx = fixed_mul(rrx, scale_x);
|
||||
fixed_t sry = fixed_mul(rry, scale_y);
|
||||
fixed_t sx = fixed_mul(rx_val - vb_x, eff_sx);
|
||||
fixed_t sy = fixed_mul(ry_val - vb_y, eff_sy);
|
||||
fixed_t sw = fixed_mul(rw, eff_sx);
|
||||
fixed_t sh = fixed_mul(rh, eff_sy);
|
||||
fixed_t srx = fixed_mul(rrx, eff_sx);
|
||||
fixed_t sry = fixed_mul(rry, eff_sy);
|
||||
|
||||
el.clear();
|
||||
svg_rect_edges(el, sx, sy, sw, sh, srx, sry);
|
||||
@@ -1438,6 +1626,46 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t
|
||||
// ---------------------------------------------------------------------------
|
||||
// Load SVG from VFS and render
|
||||
// ---------------------------------------------------------------------------
|
||||
// Extract intrinsic dimensions from an SVG document. Prefers width/height
|
||||
// attributes, falls back to viewBox, then to a 256x256 default. Used by the
|
||||
// image viewer to render SVGs at their natural size rather than a fixed icon
|
||||
// size.
|
||||
inline void svg_get_natural_size(const char* svg_data, int svg_len,
|
||||
int* out_w, int* out_h) {
|
||||
int w = 0, h = 0;
|
||||
const char* svg_tag = svg_strstr(svg_data, svg_len, "<svg");
|
||||
if (svg_tag) {
|
||||
int tag_offset = (int)(svg_tag - svg_data);
|
||||
int tag_end = tag_offset;
|
||||
while (tag_end < svg_len && svg_data[tag_end] != '>') ++tag_end;
|
||||
int tag_len = tag_end - tag_offset + 1;
|
||||
|
||||
char attr_buf[64];
|
||||
if (svg_get_attr(svg_tag, tag_len, " width", attr_buf, sizeof(attr_buf)) > 0)
|
||||
w = svg_parse_int(attr_buf);
|
||||
if (svg_get_attr(svg_tag, tag_len, " height", attr_buf, sizeof(attr_buf)) > 0)
|
||||
h = svg_parse_int(attr_buf);
|
||||
|
||||
if ((w <= 0 || h <= 0) &&
|
||||
svg_get_attr(svg_tag, tag_len, " viewBox", attr_buf, sizeof(attr_buf)) > 0) {
|
||||
fixed_t vbx, vby, vbw, vbh;
|
||||
const char* vp = attr_buf;
|
||||
int c;
|
||||
c = svg_parse_fixed(vp, &vbx); vp += c; while (*vp && svg_char_is_sep(*vp)) ++vp;
|
||||
c = svg_parse_fixed(vp, &vby); vp += c; while (*vp && svg_char_is_sep(*vp)) ++vp;
|
||||
c = svg_parse_fixed(vp, &vbw); vp += c; while (*vp && svg_char_is_sep(*vp)) ++vp;
|
||||
svg_parse_fixed(vp, &vbh);
|
||||
(void)vbx; (void)vby;
|
||||
if (w <= 0) w = fixed_to_int(vbw);
|
||||
if (h <= 0) h = fixed_to_int(vbh);
|
||||
}
|
||||
}
|
||||
if (w <= 0) w = 256;
|
||||
if (h <= 0) h = 256;
|
||||
if (out_w) *out_w = w;
|
||||
if (out_h) *out_h = h;
|
||||
}
|
||||
|
||||
inline SvgIcon svg_load(const char* vfs_path, int target_w, int target_h, Color fill_color) {
|
||||
int fd = montauk::open(vfs_path);
|
||||
if (fd < 0) {
|
||||
|
||||
@@ -41,6 +41,41 @@
|
||||
|
||||
namespace gui {
|
||||
|
||||
inline int decode_single_byte_codepoint(int codepoint) {
|
||||
if (codepoint < 0 || codepoint > 255) return codepoint;
|
||||
|
||||
switch (codepoint) {
|
||||
case 0x80: return 0x20AC; // Euro Sign
|
||||
case 0x82: return 0x201A; // Single Low-9 Quotation Mark
|
||||
case 0x83: return 0x0192; // Latin Small Letter F With Hook
|
||||
case 0x84: return 0x201E; // Double Low-9 Quotation Mark
|
||||
case 0x85: return 0x2026; // Horizontal Ellipsis
|
||||
case 0x86: return 0x2020; // Dagger
|
||||
case 0x87: return 0x2021; // Double Dagger
|
||||
case 0x88: return 0x02C6; // Modifier Letter Circumflex Accent
|
||||
case 0x89: return 0x2030; // Per Mille Sign
|
||||
case 0x8A: return 0x0160; // Latin Capital Letter S With Caron
|
||||
case 0x8B: return 0x2039; // Single Left-Pointing Angle Quotation Mark
|
||||
case 0x8C: return 0x0152; // Latin Capital Ligature OE
|
||||
case 0x8E: return 0x017D; // Latin Capital Letter Z With Caron
|
||||
case 0x91: return 0x2018; // Left Single Quotation Mark
|
||||
case 0x92: return 0x2019; // Right Single Quotation Mark
|
||||
case 0x93: return 0x201C; // Left Double Quotation Mark
|
||||
case 0x94: return 0x201D; // Right Double Quotation Mark
|
||||
case 0x95: return 0x2022; // Bullet
|
||||
case 0x96: return 0x2013; // En Dash
|
||||
case 0x97: return 0x2014; // Em Dash
|
||||
case 0x98: return 0x02DC; // Small Tilde
|
||||
case 0x99: return 0x2122; // Trade Mark Sign
|
||||
case 0x9A: return 0x0161; // Latin Small Letter S With Caron
|
||||
case 0x9B: return 0x203A; // Single Right-Pointing Angle Quotation Mark
|
||||
case 0x9C: return 0x0153; // Latin Small Ligature OE
|
||||
case 0x9E: return 0x017E; // Latin Small Letter Z With Caron
|
||||
case 0x9F: return 0x0178; // Latin Capital Letter Y With Diaeresis
|
||||
default: return codepoint;
|
||||
}
|
||||
}
|
||||
|
||||
struct CachedGlyph {
|
||||
uint8_t* bitmap;
|
||||
int width, height;
|
||||
@@ -50,7 +85,7 @@ struct CachedGlyph {
|
||||
};
|
||||
|
||||
struct GlyphCache {
|
||||
CachedGlyph glyphs[256]; // Latin-1 Supplement (U+0080..U+00FF) included
|
||||
CachedGlyph glyphs[256]; // Single-byte text with Windows-1252 punctuation mapping
|
||||
int pixel_size;
|
||||
float scale;
|
||||
int ascent, descent, line_gap;
|
||||
@@ -155,12 +190,13 @@ struct TrueTypeFont {
|
||||
if (g->loaded) return g;
|
||||
|
||||
g->loaded = true;
|
||||
int font_codepoint = decode_single_byte_codepoint(codepoint);
|
||||
int advance, lsb;
|
||||
stbtt_GetCodepointHMetrics(&info, codepoint, &advance, &lsb);
|
||||
stbtt_GetCodepointHMetrics(&info, font_codepoint, &advance, &lsb);
|
||||
g->advance = (int)(advance * gc->scale);
|
||||
|
||||
int x0, y0, x1, y1;
|
||||
stbtt_GetCodepointBitmapBox(&info, codepoint, gc->scale, gc->scale,
|
||||
stbtt_GetCodepointBitmapBox(&info, font_codepoint, gc->scale, gc->scale,
|
||||
&x0, &y0, &x1, &y1);
|
||||
g->width = x1 - x0;
|
||||
g->height = y1 - y0;
|
||||
@@ -169,8 +205,10 @@ struct TrueTypeFont {
|
||||
|
||||
if (g->width > 0 && g->height > 0) {
|
||||
g->bitmap = (uint8_t*)montauk::malloc(g->width * g->height);
|
||||
if (!g->bitmap)
|
||||
return g;
|
||||
stbtt_MakeCodepointBitmap(&info, g->bitmap, g->width, g->height,
|
||||
g->width, gc->scale, gc->scale, codepoint);
|
||||
g->width, gc->scale, gc->scale, font_codepoint);
|
||||
}
|
||||
|
||||
return g;
|
||||
@@ -199,6 +237,13 @@ struct TrueTypeFont {
|
||||
GlyphCache* gc = get_cache(pixel_size);
|
||||
int cx = x;
|
||||
int baseline = y + gc->ascent;
|
||||
int fb_w = fb.width();
|
||||
int fb_h = fb.height();
|
||||
int fb_pitch = fb.pitch();
|
||||
uint32_t* fb_pixels = fb.buffer();
|
||||
if (!fb_pixels) return;
|
||||
uint32_t src_rgb = 0xFF000000 | ((uint32_t)color.r << 16) |
|
||||
((uint32_t)color.g << 8) | color.b;
|
||||
|
||||
for (int i = 0; text[i]; i++) {
|
||||
CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]);
|
||||
@@ -207,12 +252,41 @@ struct TrueTypeFont {
|
||||
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);
|
||||
|
||||
int row0 = gy < 0 ? -gy : 0;
|
||||
int col0 = gx < 0 ? -gx : 0;
|
||||
int row1 = gy + g->height > fb_h ? fb_h - gy : g->height;
|
||||
int col1 = gx + g->width > fb_w ? fb_w - gx : g->width;
|
||||
|
||||
if (row0 < row1 && col0 < col1) {
|
||||
for (int row = row0; row < row1; row++) {
|
||||
uint8_t* alpha_row = g->bitmap + row * g->width + col0;
|
||||
uint32_t* dst = (uint32_t*)((uint8_t*)fb_pixels + (gy + row) * fb_pitch) + gx + col0;
|
||||
|
||||
for (int col = col0; col < col1; col++) {
|
||||
uint8_t alpha = *alpha_row++;
|
||||
if (alpha == 0) {
|
||||
dst++;
|
||||
continue;
|
||||
}
|
||||
if (alpha == 255) {
|
||||
*dst++ = src_rgb;
|
||||
continue;
|
||||
}
|
||||
|
||||
uint32_t d = *dst;
|
||||
uint32_t inv_a = 255 - alpha;
|
||||
uint32_t dr = (d >> 16) & 0xFF;
|
||||
uint32_t dg = (d >> 8) & 0xFF;
|
||||
uint32_t db = d & 0xFF;
|
||||
uint32_t rr = alpha * color.r + inv_a * dr;
|
||||
uint32_t gg = alpha * color.g + inv_a * dg;
|
||||
uint32_t bb = alpha * color.b + inv_a * db;
|
||||
|
||||
rr = (rr + 1 + (rr >> 8)) >> 8;
|
||||
gg = (gg + 1 + (gg >> 8)) >> 8;
|
||||
bb = (bb + 1 + (bb >> 8)) >> 8;
|
||||
*dst++ = 0xFF000000 | (rr << 16) | (gg << 8) | bb;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -389,6 +463,7 @@ namespace fonts {
|
||||
inline bool init() {
|
||||
auto load = [](const char* path) -> TrueTypeFont* {
|
||||
TrueTypeFont* f = (TrueTypeFont*)montauk::malloc(sizeof(TrueTypeFont));
|
||||
if (!f) return nullptr;
|
||||
montauk::memset(f, 0, sizeof(TrueTypeFont));
|
||||
if (!f->init(path)) {
|
||||
montauk::mfree(f);
|
||||
|
||||
Reference in New Issue
Block a user