refactor: add website code
This commit is contained in:
@@ -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,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,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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user