fix: sync template sysroot, add script

This commit is contained in:
2026-06-12 11:56:20 +02:00
parent 22691c5c17
commit 44f86a8b19
33 changed files with 5172 additions and 147 deletions
+290
View File
@@ -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
+150
View File
@@ -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
+133
View File
@@ -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