refactor: add website code

This commit is contained in:
2026-04-11 09:34:55 +02:00
parent fd7fcbbbc8
commit 01546806f0
134 changed files with 1939 additions and 0 deletions
+63
View File
@@ -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
+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
+113
View File
@@ -0,0 +1,113 @@
/*
* theme.hpp
* Montauk Toolkit theme tokens and color helpers
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include "gui/gui.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);
}
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 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 = colors::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.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;
}
} // namespace gui::mtk
+365
View File
@@ -0,0 +1,365 @@
/*
* widgets.hpp
* Montauk Toolkit immediate-mode widgets
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include "gui/canvas.hpp"
#include "gui/font.hpp"
#include "gui/mtk/theme.hpp"
namespace gui::mtk {
enum ButtonVariant : uint8_t {
BUTTON_PRIMARY = 0,
BUTTON_SECONDARY,
BUTTON_DANGER,
BUTTON_GHOST,
BUTTON_TONAL,
};
struct WidgetState {
bool active;
bool hovered;
bool enabled;
};
inline WidgetState widget_state(bool active = false, bool hovered = false, bool enabled = true) {
return {active, hovered, enabled};
}
struct ButtonColors {
Color bg;
Color border;
Color fg;
};
inline int text_len(const char* text) {
int len = 0;
while (text && text[len]) len++;
return len;
}
inline bool same_color(Color a, Color b) {
return a.r == b.r && a.g == b.g && a.b == b.b && a.a == b.a;
}
inline void draw_rounded_frame(Canvas& c,
const Rect& bounds,
int radius,
Color fill,
Color border,
int border_w = 1) {
if (bounds.empty()) return;
if (border_w <= 0 || same_color(fill, border)) {
c.fill_rounded_rect(bounds.x, bounds.y, bounds.w, bounds.h, gui_max(radius, 0), fill);
return;
}
c.fill_rounded_rect(bounds.x, bounds.y, bounds.w, bounds.h, gui_max(radius, 0), border);
int inner_w = bounds.w - border_w * 2;
int inner_h = bounds.h - border_w * 2;
if (inner_w <= 0 || inner_h <= 0) return;
c.fill_rounded_rect(bounds.x + border_w, bounds.y + border_w,
inner_w, inner_h, gui_max(radius - border_w, 0), fill);
}
inline ButtonColors resolve_button_colors(ButtonVariant variant,
const WidgetState& state,
const Theme& theme) {
if (!state.enabled) {
return {theme.disabled_bg, theme.disabled_bg, theme.disabled_fg};
}
switch (variant) {
case BUTTON_PRIMARY:
return {
state.hovered ? theme.accent_hover : theme.accent,
state.hovered ? theme.accent_hover : theme.accent,
theme.accent_fg
};
case BUTTON_DANGER:
return {
state.hovered ? theme.danger_hover : theme.danger,
state.hovered ? theme.danger_hover : theme.danger,
theme.danger_fg
};
case BUTTON_GHOST:
if (state.active) {
return {theme.accent_soft, theme.accent_soft, theme.accent};
}
return {
state.hovered ? theme.surface_hover : theme.window_bg,
theme.border,
theme.text
};
case BUTTON_TONAL:
{
Color fill = state.hovered ? mix(theme.surface, theme.accent, 40)
: mix(theme.surface, theme.accent, 24);
return {fill, fill, theme.text};
}
case BUTTON_SECONDARY:
default:
if (state.active) {
return {
state.hovered ? theme.accent_hover : theme.accent,
state.hovered ? theme.accent_hover : theme.accent,
theme.accent_fg
};
}
return {
state.hovered ? theme.surface_hover : theme.window_bg,
theme.border,
theme.text
};
}
}
inline void draw_button(Canvas& c,
const Rect& bounds,
const char* label,
ButtonVariant variant,
const WidgetState& state,
const Theme& theme) {
if (bounds.empty()) return;
ButtonColors colors = resolve_button_colors(variant, state, theme);
draw_rounded_frame(c, bounds, theme.radius_md, colors.bg, colors.border);
if (label && label[0]) {
int tw = text_width(label);
int fh = system_font_height();
int tx = bounds.x + (bounds.w - tw) / 2;
int ty = bounds.y + (bounds.h - fh) / 2;
c.text(tx, ty, label, colors.fg);
}
}
inline void draw_separator(Canvas& c, int x, int y, int w, const Theme& theme) {
c.hline(x, y, w, theme.border);
}
inline Rect tab_rect(const Rect& bar, int count, int index) {
if (count <= 0) return {bar.x, bar.y, 0, bar.h};
int slot_w = bar.w / count;
int rx = bar.x + slot_w * index;
int rw = (index == count - 1) ? (bar.w - slot_w * index) : slot_w;
return {rx, bar.y, rw, bar.h};
}
inline void draw_tab_bar(Canvas& c,
const Rect& bar,
const char* const* labels,
int count,
int active,
const Theme& theme) {
c.fill_rect(bar.x, bar.y, bar.w, bar.h, theme.surface);
c.hline(bar.x, bar.y + bar.h - 1, bar.w, theme.border);
int fh = system_font_height();
for (int i = 0; i < count; i++) {
Rect item = tab_rect(bar, count, i);
bool is_active = (i == active);
if (is_active) {
c.fill_rect(item.x, item.y, item.w, item.h, theme.window_bg);
if (item.w > 8) {
c.fill_rect(item.x + 4, item.y + item.h - 3, item.w - 8, 3, theme.selection);
}
}
Color tc = is_active ? theme.accent : theme.text_subtle;
int tw = text_width(labels[i]);
c.text(item.x + (item.w - tw) / 2,
item.y + (item.h - fh) / 2,
labels[i], tc);
}
}
inline int hit_tab_bar(const Rect& bar, int count, int mx, int my) {
if (!bar.contains(mx, my) || count <= 0) return -1;
for (int i = 0; i < count; i++) {
if (tab_rect(bar, count, i).contains(mx, my)) return i;
}
return -1;
}
inline Rect radio_indicator_rect(const Rect& option) {
int size = 14;
return {option.x, option.y + (option.h - size) / 2, size, size};
}
inline void draw_radio(Canvas& c,
const Rect& option,
const char* label,
bool selected,
const Theme& theme) {
Rect bubble = radio_indicator_rect(option);
int radius = bubble.w / 2;
c.fill_rounded_rect(bubble.x, bubble.y, bubble.w, bubble.h, radius, theme.border);
c.fill_rounded_rect(bubble.x + 1, bubble.y + 1, bubble.w - 2, bubble.h - 2,
gui_max(radius - 1, 1), colors::WHITE);
if (selected) {
int inner = gui_max(bubble.w - 8, 2);
c.fill_rounded_rect(bubble.x + 4, bubble.y + 4, inner, inner,
gui_max(inner / 2, 1), theme.accent);
}
int fh = system_font_height();
c.text(bubble.x + bubble.w + theme.gap_sm,
option.y + (option.h - fh) / 2,
label, theme.text);
}
inline Rect swatch_rect(int row_x, int row_y, int index, int size = 24, int gap = 6) {
return {row_x + index * (size + gap), row_y, size, size};
}
inline int hit_swatch_row(int mx, int my,
int row_x, int row_y,
int count, int size = 24, int gap = 6) {
for (int i = 0; i < count; i++) {
if (swatch_rect(row_x, row_y, i, size, gap).contains(mx, my)) return i;
}
return -1;
}
inline void draw_swatch_row(Canvas& c,
int row_x, int row_y,
const Color* palette,
int count,
int selected_index,
const Theme& theme,
int size = 24,
int gap = 6) {
for (int i = 0; i < count; i++) {
Rect swatch = swatch_rect(row_x, row_y, i, size, gap);
if (i == selected_index) {
c.fill_rounded_rect(swatch.x - 2, swatch.y - 2,
swatch.w + 4, swatch.h + 4, 4, theme.accent);
}
c.fill_rounded_rect(swatch.x, swatch.y, swatch.w, swatch.h, 3, palette[i]);
c.rect(swatch.x, swatch.y, swatch.w, swatch.h, Color::from_rgb(0xCC, 0xCC, 0xCC));
}
}
inline void draw_list_row(Canvas& c,
const Rect& row,
bool selected,
bool alternate,
const Theme& theme) {
if (selected) {
c.fill_rounded_rect(row.x, row.y, row.w, row.h, theme.radius_md, theme.selection);
} else if (alternate) {
c.fill_rounded_rect(row.x, row.y, row.w, row.h, theme.radius_md, theme.surface_alt);
}
}
inline int badge_width(const char* label, const Theme& theme) {
return text_width(label) + theme.gap_sm * 2;
}
inline int badge_height() {
return system_font_height() + 4;
}
inline void draw_badge(Canvas& c,
int x, int y,
const char* label,
Color bg, Color fg,
const Theme& theme) {
int bw = badge_width(label, theme);
int bh = badge_height();
c.fill_rounded_rect(x, y, bw, bh, bh / 2, bg);
c.text(x + theme.gap_sm, y + 2, label, fg);
}
inline const char* mask_text(const char* value, char* buffer, int cap) {
int len = 0;
while (value && value[len] && len < cap - 1) {
buffer[len] = '*';
len++;
}
buffer[len] = '\0';
return buffer;
}
inline void copy_prefix(char* buffer, int cap, const char* value, int count, bool masked) {
if (cap <= 0) return;
int len = 0;
while (value && value[len] && len < count && len < cap - 1) {
buffer[len] = masked ? '*' : value[len];
len++;
}
buffer[len] = '\0';
}
inline void draw_text_field(Canvas& c,
const Rect& bounds,
const char* value,
int cursor,
bool focused,
bool masked,
const Theme& theme) {
const char* text = value ? value : "";
char masked_text[65];
const char* shown = masked ? mask_text(text, masked_text, (int)sizeof(masked_text)) : text;
draw_rounded_frame(c, bounds, theme.radius_sm, theme.surface,
focused ? theme.accent : theme.border);
int fh = system_font_height();
int tx = bounds.x + 8;
int ty = bounds.y + (bounds.h - fh) / 2;
c.text(tx, ty, shown, theme.text);
if (focused) {
int clamped = gui_clamp(cursor, 0, text_len(text));
char prefix[65];
copy_prefix(prefix, (int)sizeof(prefix), text, clamped, masked);
int cx = tx + text_width(prefix);
c.fill_rect(cx, bounds.y + 6, 2, gui_max(bounds.h - 12, 1), theme.accent);
}
}
inline Rect labeled_text_input_rect(int x, int y, int w, const Theme& theme, int field_h = 0) {
if (field_h <= 0) field_h = theme.control_h;
return {x, y + system_font_height() + theme.gap_xs, w, field_h};
}
inline int labeled_text_height(const Theme& theme, int field_h = 0) {
if (field_h <= 0) field_h = theme.control_h;
return system_font_height() + theme.gap_xs + field_h;
}
inline void draw_labeled_text_field(Canvas& c,
int x, int y, int w,
const char* label,
const char* value,
int cursor,
bool focused,
bool masked,
const Theme& theme,
int field_h = 0) {
if (field_h <= 0) field_h = theme.control_h;
c.text(x, y, label, theme.text_muted);
draw_text_field(c, labeled_text_input_rect(x, y, w, theme, field_h),
value, cursor, focused, masked, theme);
}
inline void draw_modal_actions(Canvas& c,
const Rect& primary,
const char* primary_label,
ButtonVariant primary_variant,
const WidgetState& primary_state,
const Rect& secondary,
const char* secondary_label,
const WidgetState& secondary_state,
const Theme& theme) {
draw_button(c, primary, primary_label, primary_variant, primary_state, theme);
draw_button(c, secondary, secondary_label, BUTTON_SECONDARY, secondary_state, theme);
}
} // namespace gui::mtk