/* * 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