std1.0.0-beta-7 SafeC docs

Source gui/gui_widget.h

View in the repository

#pragma once
#include <std/erased.h>
// SafeC Standard Library — GUI: the retained-mode widget toolkit.
//
// A tree of Widgets — containers (vertical/horizontal boxes, grids,
// overlapping stacks, scroll views, splitters, tabs, group boxes, panels)
// and controls (labels, buttons, toggles, check boxes, radio groups,
// sliders, progress bars, number spinners, text inputs, multi-line text
// areas, combo boxes, list / tree / table views, menu bars and context
// menus, tooltips, modal dialogs and message boxes, images, a vector
// canvas, a color picker, separators, status bars) — laid out, painted
// with the anti-aliased canvas (gui_canvas.h) in a light or dark theme,
// and driven by GuiEvents with keyboard focus, Tab navigation, hover /
// pressed / disabled states and the clipboard.
//
//   &Widget root = Widget::vstack();
//   &Widget name = Widget::textinput("Your name");
//   root.add_child(Widget::label("Hello!"));
//   root.add_child(name);
//   root.add_child(Widget::button("OK").on_click(ok_clicked, (?&State)&state));
//
// Driving it: with gui_app.h's GuiApp (app.set_root(win, root) — events,
// layout, painting, timers and tooltips are then automatic, for any number
// of windows), or by hand, once per frame:
//
//   while (win.poll_event(&ev)) { root.dispatch_event(&ev); }
//   root.render(&win);          // layout + paint into win.pixels
//   win.present();
//
// The by-hand calls work on the root widget (as before this toolkit
// existed: root.layout(x, y, w, h), root.render(&win),
// root.dispatch_event(&ev)) and use a GuiUi created for it on first use;
// GuiUi::new(root) gives you that object to hold (themes, focus, popups,
// dialogs, tooltips, scale).
//
// Units: sizes given to widgets (set_pref_size, set_min_size, margins,
// padding, fonts) are logical; the GuiUi multiplies them by the window's
// scale factor, so a hiDpi window gets the same layout at twice the
// pixels. Widget rects (x, y, w, h) are in window pixels.
//
// Layout model: each widget has a natural size (its content, plus
// padding; set_pref_size() overrides either dimension; min/max sizes clamp
// it) and margins. Boxes place children in a row or column with 'spacing'
// between them, give extra space to children with a stretch weight
// (set_stretch, in proportion), and shrink stretching children (down to
// their minimum) when space is short; on the cross axis each child is
// aligned by set_alignment() (START/CENTER/END/STRETCH). Grids size
// columns and rows to their widest/tallest cell, with stretch weights per
// column/row (set_column_stretch / set_row_stretch) and cells spanning
// several. Stacks overlap children, each aligned on both axes. Scroll views
// give their child its natural size and scroll the rest into view.
//
// Callbacks receive the widget and a typed user-data reference (stored
// erased, see the note on Widget's callback fields):
//   on_click(cb, data)     buttons, menu items, toggles/check boxes (after the change), links
//   on_change(cb, data)    any value change: text, check state, slider/spinner value,
//                          selection, color, splitter position, tab
//   on_activate(cb, data)  double click / Enter on a list, tree or table row
//   on_draw(cb, data)      Widget::canvas: draw with the vector canvas
//
// Keyboard: Tab / Shift+Tab move focus through the focusable widgets in
// tree order (skipping hidden and disabled ones, staying inside an open
// modal dialog); Space/Enter activate buttons, toggles and check boxes;
// arrows move sliders, spinners, radio groups, list/tree/table selections
// and tabs; Escape closes a popup, a dialog, or clears focus; menu item
// shortcuts (set_shortcut("Ctrl+S"), or the primary modifier on macOS)
// fire from anywhere in the window. Text fields support selection with
// Shift+arrows/Home/End and the mouse (double click selects a word, triple
// click everything), word-wise moves with Ctrl (Alt on macOS), and
// Cut/Copy/Paste/Select-all through the system clipboard.

#include <std/gui/gui.h>
#include <std/gui/gui_draw.h>
#include <std/gui/gui_font.h>
#include <std/gui/gui_canvas.h>
#include <std/gui/gui_text.h>
#include <std/gui/gui_png.h>
#include <std/collections/vec.h>
#include <std/collections/string.h>

namespace std {

// ── widget kinds ─────────────────────────────────────────────────────────
#define WIDGET_VSTACK     0
#define WIDGET_HSTACK     1
#define WIDGET_BUTTON     2
#define WIDGET_LABEL      3
#define WIDGET_CHECKBOX   4
#define WIDGET_TEXTINPUT  5
#define WIDGET_SPACER     6
#define WIDGET_SLIDER     7
#define WIDGET_CUSTOM     8
#define WIDGET_TOGGLE     9
#define WIDGET_RADIO      10   // a radio group: one choice of several items
#define WIDGET_PROGRESS   11
#define WIDGET_SPINNER    12   // number input with up/down buttons
#define WIDGET_TEXTAREA   13
#define WIDGET_COMBO      14
#define WIDGET_LIST       15
#define WIDGET_TREE       16
#define WIDGET_TABLE      17
#define WIDGET_TABS       18
#define WIDGET_SCROLL     19
#define WIDGET_SPLITTER   20
#define WIDGET_MENUBAR    21
#define WIDGET_MENU       22   // a menu: a menubar entry, a submenu, or a popup/context menu
#define WIDGET_MENUITEM   23
#define WIDGET_DIALOG     24
#define WIDGET_IMAGE      25
#define WIDGET_CANVAS     26
#define WIDGET_COLOR      27   // color picker
#define WIDGET_GROUP      28   // titled group box
#define WIDGET_SEPARATOR  29
#define WIDGET_STATUSBAR  30
#define WIDGET_GRID       31
#define WIDGET_STACK      32   // children overlap
#define WIDGET_PANEL      33   // a box with background and border (a frame)
#define WIDGET_TOOLTIP    34   // shown by GuiUi
#define WIDGET_POPUPLIST  35   // a combo box's drop-down list
#define WIDGET_KIND_COUNT 36

// Alignment within the space a container gives a child.
#define WIDGET_ALIGN_START   0
#define WIDGET_ALIGN_CENTER  1
#define WIDGET_ALIGN_END     2
#define WIDGET_ALIGN_STRETCH 3

#define WIDGET_HORIZONTAL 0
#define WIDGET_VERTICAL   1

// Image view fit modes (Widget::set_fit).
#define WIDGET_FIT_CONTAIN 0   // whole image, aspect kept (default)
#define WIDGET_FIT_COVER   1   // fill, aspect kept, cropped
#define WIDGET_FIT_STRETCH 2
#define WIDGET_FIT_NONE    3   // natural size, centered

// Message box buttons (gui_message_box) and dialog results.
#define GUI_MB_OK      1
#define GUI_MB_CANCEL  2
#define GUI_MB_YES     4
#define GUI_MB_NO      8
#define GUI_RESULT_NONE   0
#define GUI_RESULT_OK     1
#define GUI_RESULT_CANCEL 2
#define GUI_RESULT_YES    4
#define GUI_RESULT_NO     8

// ── theme ────────────────────────────────────────────────────────────────

// Colors, fonts and metrics every built-in widget paints with. Metrics are
// logical units (scaled by the window's scale factor). Start from light()
// or dark() and change fields; GuiUi::set_theme() applies one.
struct GuiTheme {
    int dark;
    struct GuiColor window;          // window background
    struct GuiColor panel;           // group boxes, tab pages, menus' background
    struct GuiColor text;
    struct GuiColor textDim;         // placeholders, secondary text
    struct GuiColor textDisabled;
    struct GuiColor accent;          // selection, checked controls, focus ring
    struct GuiColor accentText;      // text on the accent color
    struct GuiColor button;
    struct GuiColor buttonHover;
    struct GuiColor buttonPressed;
    struct GuiColor input;           // text field / list background
    struct GuiColor border;
    struct GuiColor borderStrong;    // hovered / focused borders
    struct GuiColor selection;       // selected list rows, selected text
    struct GuiColor selectionText;
    struct GuiColor hover;           // hovered rows and menu items
    struct GuiColor header;          // table headers, menu bar
    struct GuiColor altRow;          // every other row in tables
    struct GuiColor scrollbar;
    struct GuiColor scrollbarHover;
    struct GuiColor tooltip;
    struct GuiColor tooltipText;
    struct GuiColor shadow;
    struct GuiColor overlay;         // dims the window behind a modal dialog
    float fontSize;
    float smallFontSize;
    float radius;                    // corner radius of controls
    float borderWidth;
    int padding;                     // inside controls
    int spacing;                     // between children of boxes
    int controlHeight;               // buttons, inputs, combo boxes
    int rowHeight;                   // list/tree/table rows, menu items
    int scrollbarSize;
    // light()/dark() pick the system's sans-serif (GuiTypeface::system);
    // set these to GuiTypeface::fallback() for output that doesn't depend
    // on the installed fonts (tests, golden images).
    const ?&GuiTypeface font;        // null: GuiTypeface::fallback()
    const ?&GuiTypeface boldFont;    // null: font

    static struct GuiTheme light();
    static struct GuiTheme dark();
    // The theme matching the system's dark-mode setting where it can be
    // read ($SAFEC_GUI_THEME=dark|light overrides), else light().
    static struct GuiTheme system();
};

// The original per-widget style. Each field, once set through a setter
// (set_bg, set_fg, set_border, set_padding, set_spacing, set_font_scale,
// set_font), overrides the theme for that widget; until then the theme
// decides. 'font' (a bitmap GuiFont) replaces the TrueType theme font when
// set; fontScale then multiplies its cells, and otherwise multiplies the
// theme's font size.
struct WidgetStyle {
    struct GuiColor bg;
    struct GuiColor fg;
    struct GuiColor border;
    int borderWidth;
    int padding;
    int spacing;               // gap between children, for boxes and grids
    int fontScale;             // 1, 2, 3, ...
    const ?&GuiFont font;      // empty (null) = the theme's TrueType font
    int setMask;               // WIDGET_STYLE_* bits: which fields were set
    float fontSize;            // 0 = the theme's
    int bold;
};

#define WIDGET_STYLE_BG        1
#define WIDGET_STYLE_FG        2
#define WIDGET_STYLE_BORDER    4
#define WIDGET_STYLE_PADDING   8
#define WIDGET_STYLE_SPACING   16
#define WIDGET_STYLE_FONT      32

// One node of a tree view.
struct WidgetTreeNode {
    struct String text;
    int parent;          // index of the parent node, -1 for a top-level node
    int expanded;
    int depth;
    long tag;            // yours
};

// One column of a table view.
struct WidgetColumn {
    struct String title;
    int width;           // logical; 0 = share the remaining width
    int align;           // GUI_TEXT_LEFT/CENTER/RIGHT
};

struct Widget {
    int kind;

    // box, computed by layout() each frame (window pixels)
    int x; int y; int w; int h;

    // sizing hints (all optional — 0/unset means "size to content")
    int prefW; int prefH;
    int stretch;    // 0 = natural size; >0 = flex weight along the parent box's main axis
    int alignment;  // WIDGET_ALIGN_* — this widget's position on the parent's cross axis

    struct WidgetStyle style;
    struct String text;
    int visible;
    int enabled;

    int checked;                            // check box, toggle, checkable menu item
    double sliderValue; double sliderMin; double sliderMax; // slider, progress, spinner, splitter (value)
    unsigned long caret;                     // text fields: the caret (a byte offset)

    int hover;
    int pressed;
    int focused;

    // Callback/hook slots: typed functions over an erased context (see
    // std/erased.h). The context's type is each caller's own — 'children'
    // below holds buttons, sliders, labels, ... with unrelated userData
    // types in one tree, which one generic Widget<T> couldn't — so the
    // generic setters (on_click<T> & co.) store fn(?&T) / ?&T as
    // fn(?&Erased) / ?&Erased, and every call passes the pair back together.
    ?fn void(&Widget, ?&Erased) onClick;
    ?&Erased onClickData;
    ?fn void(&Widget, ?&Erased) onChange;
    ?&Erased onChangeData;

    ?fn void(&GuiWindow, &Widget) customDraw;
    ?fn void(&Widget, int, int, int, int) customLayout;
    ?fn int(&Widget, &GuiEvent) customEvent;
    ?&Erased userData;

    struct Vec<&Widget> children;
    ?&Widget parent;        // empty (null) for the root widget

    // ── everything below was added with the full toolkit ──────────────────
    struct String name;          // find() looks widgets up by it
    struct String tooltip;
    struct String placeholder;   // text fields, combo boxes
    struct String shortcut;      // menu items: "Ctrl+S", "Ctrl+Shift+Z", "F5", "Delete"
    int minW; int minH;          // logical; 0 = none
    int maxW; int maxH;
    int marginL; int marginT; int marginR; int marginB;
    int padL; int padT; int padR; int padB;   // -1: use style.padding / the theme
    int valign;                  // stacks and grid cells: vertical alignment (-1: alignment)
    int gridRow; int gridCol; int gridRowSpan; int gridColSpan;
    int columns;                 // grids: number of columns
    struct Vec<int> colStretch;  // grids: per column / per row stretch weights
    struct Vec<int> rowStretch;
    double step;                 // slider/spinner increment (0 = continuous for sliders)
    int decimals;                // spinner/slider value display
    int selected;                // radio, combo, list, tabs, table row, tree node (-1 = none)
    int orientation;             // WIDGET_HORIZONTAL / WIDGET_VERTICAL
    struct Vec<struct String> items;          // radio, combo, list entries; tab titles
    struct Vec<struct WidgetTreeNode> nodes;  // tree view
    struct Vec<struct WidgetColumn> tableCols;
    struct Vec<struct String> cells;          // table: rows x columns, row-major
    int rowCount;
    float scrollX; float scrollY;    // scroll offsets (pixels)
    float contentW; float contentH;  // scrolled content size (pixels)
    unsigned long anchor;        // text fields: the other end of the selection
    float textScroll;            // single-line text: horizontal scroll
    int password;
    int readOnly;
    int wrap;                    // labels, text areas: word wrap
    int indeterminate;           // progress bar: busy animation
    double animPhase;
    int open;                    // combo / menu popup open
    int textAlign;               // GUI_TEXT_LEFT/CENTER/RIGHT
    const ?&GuiImage image;      // image view (the widget doesn't own it)
    int fit;                     // WIDGET_FIT_*
    struct GuiColor color;       // color picker
    float hue; float sat; float val;
    int dragPart;                // what the pointer is dragging (internal)
    float dragX; float dragY;
    double dragValue;
    int hotIndex;                // hovered item / row / tab / menu entry (internal)
    ?fn void(&Widget, ?&Erased) onActivate; ?&Erased onActivateData;
    ?fn void(&Widget, &GuiCanvas, ?&Erased) onDraw; ?&Erased onDrawData;
    // dialogs: run when it closes (internal: message boxes), and a message
    // box's own onClose(dialog, result, userData)
    ?fn void(&Widget, int, ?&Erased) onClose_; ?&Erased onCloseData_;
    ?fn void(&Widget, int, ?&Erased) mbUser_; ?&Erased mbUserData_;
    ?&Widget contextMenu;        // shown on right click
    ?&Widget popup;              // combo: its drop-down list; menu: its open submenu
    ?&Widget owner;              // popups/menus: the widget that opened it
    ?&struct GuiUi uiPtr;        // the GuiUi this tree belongs to (root and overlays)
    int result;                  // dialogs: GUI_RESULT_* when closed
    int focusable;               // -1 = by kind, 0 / 1 = forced
    double clickTime;
    long tag;                    // yours

    // ── constructors ─────────────────────────────────────────────────────
    // All return a freshly heap-allocated, non-null widget — a region-less
    // '&Widget' (see README's "Outliving references"), since nothing here
    // cares whether the tree it ends up in is heap/stack/static/arena-owned.
    // A widget is freed with its tree (free() on the root).
    static &Widget vstack();
    static &Widget hstack();
    static &Widget grid(int columns);
    static &Widget stack();
    static &Widget panel();
    static &Widget group(const &char title);
    static &Widget scroll();
    // Two children (add them with add_child), side by side
    // (WIDGET_HORIZONTAL) or one above the other, with a draggable divider.
    // value() is the first child's share, 0..1.
    static &Widget splitter(int orientation);
    // Pages: add_tab(title, page).
    static &Widget tabs();
    static &Widget spacer();
    static &Widget separator();
    static &Widget label(const &char text);
    static &Widget button(const &char text);
    static &Widget checkbox(const &char text, int checked);
    static &Widget toggle(const &char text, int on);
    // Items with add_item(); selected() / set_selected().
    static &Widget radio_group();
    static &Widget slider(double minValue, double maxValue, double value);
    static &Widget progress(double fraction);
    static &Widget spinner(double minValue, double maxValue, double value, double step);
    static &Widget textinput(const &char placeholder);
    static &Widget textarea();
    static &Widget combo();
    static &Widget list();
    static &Widget tree();
    static &Widget table();
    static &Widget image(const ?&GuiImage img);
    static &Widget color_picker(struct GuiColor initial);
    static &Widget statusbar();
    static &Widget menubar();
    // A menu: inside a menubar, its title is the bar entry; inside another
    // menu, a submenu; on its own, a popup/context menu.
    static &Widget menu(const &char title);
    static &Widget menu_item(const &char text);
    static &Widget menu_separator();
    // A modal dialog box with a title bar; add content and buttons as
    // children (it's a column). Show it with GuiUi::show_modal().
    static &Widget dialog(const &char title);
    // A WIDGET_CANVAS: draw(widget, canvas, userData) paints it; the
    // canvas is already translated to the widget's top-left and clipped to
    // it, in logical units (scaled for the window).
    generic<T>
    static &Widget canvas(fn void(&Widget, &GuiCanvas, ?&T) draw, ?&T userData);
    static &Widget canvas_raw_(fn void(&Widget, &GuiCanvas, ?&Erased) draw, ?&Erased userData);

    // A WIDGET_CUSTOM node drawn and laid out by your own functions (see
    // "Custom widgets" below); userData is the caller's type, stored erased.
    // The parameters are WidgetDrawFn / WidgetLayoutFn / WidgetCustomEventFn
    // (declared after this struct), spelled out: a compiler without forward
    // typedef resolution in struct-body declarations (before SafeC#43)
    // can't match them to the definitions otherwise.
    generic<T>
    static &Widget custom(fn void(&GuiWindow, &Widget) draw, fn void(&Widget, int, int, int, int) layout,
                          fn int(&Widget, &GuiEvent) eventFn, ?&T userData);
    static &Widget custom_raw_(fn void(&GuiWindow, &Widget) draw, fn void(&Widget, int, int, int, int) layout,
                               fn int(&Widget, &GuiEvent) eventFn, ?&Erased userData);

    // ── tree ─────────────────────────────────────────────────────────────
    void add_child(&Widget child);
    // Inserts at 'index' (clamped).
    void insert_child(unsigned long index, &Widget child);
    // Detaches 'child' (not freed: free() it, or add it elsewhere).
    void remove_child(&Widget child);
    // Removes and frees every child.
    void clear_children();
    unsigned long child_count() const;
    ?&Widget child_at(unsigned long i) const;
    // The first widget named 'name' in this subtree (depth first).
    ?&Widget find(const &char name);
    // The GuiUi this widget's tree is attached to (null before the first
    // render/dispatch or GuiUi::new).
    ?&GuiUi ui() const;
    // Marks the widget's window for repainting (and relayout).
    void invalidate();

    // ── customization (each returns this widget, so calls chain) ─────────
    &Widget set_bg(unsigned char r, unsigned char g, unsigned char b, unsigned char a);
    &Widget set_fg(unsigned char r, unsigned char g, unsigned char b, unsigned char a);
    &Widget set_border(unsigned char r, unsigned char g, unsigned char b,
                       unsigned char a, int width);
    &Widget set_padding(int padding);
    &Widget set_padding4(int left, int top, int right, int bottom);
    &Widget set_margin(int margin);
    &Widget set_margin4(int left, int top, int right, int bottom);
    &Widget set_spacing(int spacing);
    &Widget set_pref_size(int prefW, int prefH);
    &Widget set_min_size(int minW, int minH);
    &Widget set_max_size(int maxW, int maxH);
    &Widget set_stretch(int stretch);
    &Widget set_alignment(int alignment);
    // Both axes for stacks and grid cells (horizontal, vertical).
    &Widget set_align2(int halign, int valign);
    &Widget set_font_scale(int scale);
    &Widget set_font(const ?&GuiFont font);
    // TrueType font size (logical px) and weight for this widget's text.
    &Widget set_font_size(float size);
    &Widget set_bold(int bold);
    &Widget set_visible(int visible);
    &Widget set_enabled(int enabled);
    &Widget set_text(const &char text);
    &Widget set_name(const &char name);
    &Widget set_tooltip(const &char text);
    &Widget set_placeholder(const &char text);
    &Widget set_text_align(int align);
    &Widget set_wrap(int wrap);
    &Widget set_password(int on);
    &Widget set_read_only(int on);
    &Widget set_focusable(int on);
    // Grid placement (and spans of 1 or more cells).
    &Widget set_cell(int row, int col);
    &Widget set_span(int rows, int cols);
    &Widget set_column_stretch(int col, int stretch);
    &Widget set_row_stretch(int row, int stretch);
    &Widget set_orientation(int orientation);
    &Widget set_context_menu(?&Widget menu);
    &Widget set_tag(long tag);

    // ── values ───────────────────────────────────────────────────────────
    // slider / progress / spinner / splitter; clamped to the range, rounded
    // to 'step' when it's > 0. Setting a value doesn't fire on_change.
    &Widget set_value(double v);
    double  value() const;
    &Widget set_range(double minValue, double maxValue);
    &Widget set_step(double step);
    &Widget set_decimals(int decimals);
    &Widget set_indeterminate(int on);
    // check box / toggle / menu item
    &Widget set_checked(int checked);
    int     is_checked() const;
    // items of radio groups, combo boxes and list views
    &Widget add_item(const &char text);
    void    clear_items();
    unsigned long item_count() const;
    const &char item_text(unsigned long i) const;   // "" out of range
    &Widget set_selected(int index);
    int     selected_index() const;
    // tabs
    &Widget add_tab(const &char title, &Widget page);
    // tree views: a node under 'parent' (-1: top level); returns its index
    int     add_node(int parent, const &char text);
    &Widget set_expanded(int node, int expanded);
    int     node_count() const;
    const &char node_text(int node) const;
    // table views
    &Widget add_column(const &char title, int width);
    int     add_row();                               // returns the row index
    &Widget set_cell_text(int row, int col, const &char text);
    const &char cell_text(int row, int col) const;
    int     row_count() const;
    void    clear_rows();
    // text fields: the selection (byte offsets, start <= end)
    &Widget select(unsigned long start, unsigned long end);
    &Widget select_all();
    unsigned long selection_start() const;
    unsigned long selection_end() const;
    // image views
    &Widget set_image(const ?&GuiImage img);
    &Widget set_fit(int fit);
    // color pickers
    &Widget set_color(struct GuiColor c);
    struct GuiColor get_color() const;
    // menu items: the shortcut shown and triggered ("Ctrl+O", "Ctrl+Shift+S",
    // "F2", "Delete"; Ctrl means Command on macOS)
    &Widget set_shortcut(const &char shortcut);
    // dialogs: closes it (GuiUi::close_modal) with 'result'
    void    close_dialog(int result);

    // ── callbacks (each returns this widget, so calls chain) ─────────────
    // cb(widget, userData). userData's type is the caller's; the widget
    // stores it erased (see the field comment above).
    generic<T> &Widget on_click(fn void(&Widget, ?&T) cb, ?&T userData);
    generic<T> &Widget on_change(fn void(&Widget, ?&T) cb, ?&T userData);
    generic<T> &Widget on_activate(fn void(&Widget, ?&T) cb, ?&T userData);
    generic<T> &Widget on_draw(fn void(&Widget, &GuiCanvas, ?&T) cb, ?&T userData);
    &Widget on_click_raw_(fn void(&Widget, ?&Erased) cb, ?&Erased userData);
    &Widget on_change_raw_(fn void(&Widget, ?&Erased) cb, ?&Erased userData);
    &Widget on_activate_raw_(fn void(&Widget, ?&Erased) cb, ?&Erased userData);
    &Widget on_draw_raw_(fn void(&Widget, &GuiCanvas, ?&Erased) cb, ?&Erased userData);

    // ── per-frame pipeline (for driving a tree by hand) ──────────────────

    // Positions this widget and its whole subtree within the box
    // (x,y,w,h) — window pixels.
    void layout(int x, int y, int w, int h);

    // Lays the tree out to the window's size and paints it into 'win'.
    // (A tree that's already laid out to another box — layout() called
    // with a box not at the window's origin — keeps that box.)
    void render(&GuiWindow win);

    // Routes one event through the tree rooted here: hover/pressed/focus
    // state, clicks, value changes, text editing, scrolling, popups, and
    // a WIDGET_CUSTOM node's customEvent hook. Returns 1 if some widget
    // consumed the event.
    //
    // After that, for GUI_EVENT_KEY_DOWN, a default action runs — the same
    // idea as a browser's default DOM action for a key event — *unless*
    // ev.prevent_default() (see gui.h) was called on this event first
    // (typically from a WIDGET_CUSTOM node's WidgetCustomEventFn, the one
    // hook that gets the raw GuiEvent* and can act before the default
    // fires):
    //   Tab / Shift+Tab   moves focus to the next/previous focusable widget (wraps around)
    //   Enter / Space     activates the focused widget (button click / check box toggle)
    //   Escape            closes a popup or dialog, or clears focus
    //   Left/Right        nudges a focused slider by 5% of its range (or its step)
    // These are genuinely *default* actions: they still run even if
    // dispatch already returned 1 for this event (e.g. a focused text
    // input consuming Backspace doesn't block Tab from later moving focus
    // off it).
    int dispatch_event(&GuiEvent ev);

    // Recursively frees this widget and its whole subtree (children,
    // text, style, context menu), including the widget's own allocation.
    // Detach it from its parent first (remove_child) when it has one.
    void drop();
    void free() consume;   // = drop()
};

// onClick/onChange/onActivate(widget, userData).
typedef fn void(&Widget, ?&Erased) WidgetCallback;
// A canvas widget's draw(widget, canvas, userData).
typedef fn void(&Widget, &GuiCanvas, ?&Erased) WidgetCanvasFn;
// Custom draw(window, widget) — called instead of the built-in renderer
// for a WIDGET_CUSTOM node; must draw within [widget->x, widget->x+widget->w) x
// [widget->y, widget->y+widget->h) using GuiWindow's drawing methods (win.fill_rect(), win.draw_text(), ...).
typedef fn void(&GuiWindow, &Widget) WidgetDrawFn;
// Custom layout(widget, x, y, w, h) — called instead of the built-in
// layout for a WIDGET_CUSTOM node; must set widget->x/y/w/h (typically to
// the given box) and lay out+recurse into any children itself.
typedef fn void(&Widget, int, int, int, int) WidgetLayoutFn;
// Custom event(widget, event) -> 1 if handled (stops the event from
// reaching this widget's parent/siblings), 0 to let normal dispatch
// (hover/press/click detection) continue.
typedef fn int(&Widget, &GuiEvent) WidgetCustomEventFn;

// ── the UI context ───────────────────────────────────────────────────────

// Everything a widget tree needs that isn't a widget: the window it's
// shown in, the theme and scale, keyboard focus, the pointer, open popups
// (menus, drop-down lists, tooltips) and modal dialogs, and the canvas it
// paints with. One per window. GuiApp makes them (GuiApp::set_root);
// Widget::render/dispatch_event make one on first use for a root that has
// none; or make one yourself.
struct GuiUi {
    &Widget root;
    ?&GuiWindow win;              // set by attach() / render()
    struct GuiTheme theme;
    float scale;                  // the window's scale factor
    struct GuiCanvas canvas;
    int canvasReady;
    struct Vec<&Widget> overlays; // popups and dialogs, bottom to top
    ?&Widget focus;
    ?&Widget hoverW;
    ?&Widget capture;             // gets the pointer while a button is held
    float mouseX; float mouseY;
    int mouseInside;
    ?&Widget tipFor;              // tooltip candidate and since when
    double tipSince;
    &Widget tip;                  // the tooltip widget
    int tipShown;
    double nowMs;
    int needsLayout;
    int dirty;
    int layoutW; int layoutH;     // the size the tree was last laid out to
    int cursor;

    // A context for 'root' (attached to it: root.ui() returns it).
    static &GuiUi new(&Widget root);
    // The window it draws into and gets events from.
    void attach(&GuiWindow win);
    void set_theme(struct GuiTheme t);
    // Routes one event (see Widget::dispatch_event). Returns 1 if consumed.
    int  dispatch(&GuiEvent ev);
    // Lays out (if needed) and paints the tree and its overlays into the
    // attached window's buffer.
    void render();
    // Advances time-driven state — tooltips, caret blinking, busy
    // indicators — to 'nowMs' (gui_time_ms()). Marks the UI dirty when
    // something visible changed.
    void tick(double nowMs);
    // 1 while something animates (the next frame will look different).
    int  animating() const;
    // 1 when the UI needs repainting; render() clears it.
    int  needs_redraw() const;
    void invalidate();
    void request_layout();
    void set_focus(?&Widget w);
    // Shows a menu (or any widget) as a popup at window position (x, y),
    // kept on screen. A click outside closes it.
    void popup(&Widget menu, float x, float y);
    void close_popups();
    // Shows 'dialog' centered over the window, blocking input to
    // everything under it until close_modal() (or the dialog's
    // close_dialog()). The UI doesn't own a shown dialog: free it after
    // closing it (or reuse it).
    void show_modal(&Widget dialog);
    void close_modal(&Widget dialog);
    // The topmost open dialog, or null.
    ?&Widget modal() const;
    // Frees the UI and its whole widget tree.
    void drop();
    void free() consume;   // = drop()
};

// A standard message box: 'text' and a row of buttons (GUI_MB_* bits),
// shown modally. onClose(dialog, result) runs once a button (result
// GUI_RESULT_*) or Escape (GUI_RESULT_CANCEL) closed it; the box frees
// itself afterwards. The buttons are named "yes", "no", "ok" and "cancel"
// (Widget::find).
generic<T>
void gui_message_box(&GuiUi ui, const &char title, const &char text, int buttons,
                     fn void(&Widget, int, ?&T) onClose, ?&T userData);
void gui_message_box_raw_(&GuiUi ui, const &char title, const &char text, int buttons,
                          ?fn void(&Widget, int, ?&Erased) onClose, ?&Erased userData);

// Custom widgets: 'draw'/'layout' are required; 'eventFn' may be NULL (the
// widget then just participates in hit-testing/hover/press/click like a
// button would, with onClick/onChange still firing normally — set eventFn
// only when you need to intercept raw events yourself, e.g. for a custom
// slider/canvas that tracks drag deltas).
generic<T>
&Widget Widget::custom(WidgetDrawFn draw, WidgetLayoutFn layout,
                       WidgetCustomEventFn eventFn, ?&T userData) {
    unsafe { return Widget::custom_raw_(draw, layout, eventFn, (?&Erased)userData); }
}

generic<T>
&Widget Widget::canvas(fn void(&Widget, &GuiCanvas, ?&T) draw, ?&T userData) {
    unsafe { return Widget::canvas_raw_((fn void(&Widget, &GuiCanvas, ?&Erased))draw, (?&Erased)userData); }
}

generic<T>
&Widget Widget::on_click(fn void(&Widget, ?&T) cb, ?&T userData) {
    unsafe { return self.on_click_raw_((fn void(&Widget, ?&Erased))cb, (?&Erased)userData); }
}

generic<T>
&Widget Widget::on_change(fn void(&Widget, ?&T) cb, ?&T userData) {
    unsafe { return self.on_change_raw_((fn void(&Widget, ?&Erased))cb, (?&Erased)userData); }
}

generic<T>
&Widget Widget::on_activate(fn void(&Widget, ?&T) cb, ?&T userData) {
    unsafe { return self.on_activate_raw_((fn void(&Widget, ?&Erased))cb, (?&Erased)userData); }
}

generic<T>
&Widget Widget::on_draw(fn void(&Widget, &GuiCanvas, ?&T) cb, ?&T userData) {
    unsafe { return self.on_draw_raw_((fn void(&Widget, &GuiCanvas, ?&Erased))cb, (?&Erased)userData); }
}

generic<T>
void gui_message_box(&GuiUi ui, const &char title, const &char text, int buttons,
                     fn void(&Widget, int, ?&T) onClose, ?&T userData) {
    unsafe {
        gui_message_box_raw_(ui, title, text, buttons, (?fn void(&Widget, int, ?&Erased))onClose, (?&Erased)userData);
    }
}

} // namespace std