A tour of Vortex's widget primitives and how apps drive them at runtime.
Vortex has fourteen widget primitives. Every widget in every app is one of these, given form by a style (see the styling guide) and rendered natively by each platform's reconciler. This guide tours the primitives, the library styles built from them, and the runtime API app code uses to drive them.
| Primitive | What it is |
|---|---|
| Box | The container. Owns layout of its children; every composite starts here. |
| Text | A single styled string. |
| Button | A clickable box; clicks dispatch to app code. |
| Icon | A flat SVG loaded by name and recolored to the style's Text Color. |
| Editor | A text field with a floating placeholder; single or multi-line, with Secret, Multiline, Read Only, and Wrap attributes. |
| Image | Raster media from a file or URL, rendered verbatim (Fit: Cover/Contain). SVG sources render as live vectors and can recolor like icons. |
| Video | Playable video from a file or URL, with Autoplay/Loop/Muted/Controls attributes. |
| Markdown | Rich text parsed from Markdown, rendered natively on each platform with themed code surfaces. |
| QR | A QR code painted from the style's Data payload. |
| Scanner | A live camera preview that reads QR codes (Android only; desktop and web render its Box base). Results arrive through set_scan_result_handler(). |
| Check Box | A clickable box owning a boolean Checked state. |
| Combo Box | A single-choice selector rendered as the platform's native select control (desktop QComboBox, web <select>, Android Spinner); the app feeds its options and selection. |
| Link | A real hyperlink (Href), emitted as <a href> by the static-web emitter. |
| Host | A raw-content slot for the static-web emitter; bound text is spliced verbatim into the page. |
Media widgets resolve their Source against the app's media directories (or a URL); icons resolve theirs against the app's icon directories first, then the framework's.
styles/widgets/ ships a ready-made style per primitive (vbox.lyr, vtext.lyr, vbutton.lyr, vcheckbox.lyr, vcombobox.lyr, veditor.lyr, vicon.lyr, vimage.lyr, vvideo.lyr, vmarkdown.lyr, vqr.lyr, vscanner.lyr, vlink.lyr, vhost.lyr) plus the composite shell parts apps inherit:
window.lyr is Main Window: titlebar, divider, app pane, settings menu, with desktop/narrow/maximized variants.titlebar.lyr, windowcontrol.lyr, vtab.lyr: the window chrome and its tabs/controls.vsettingsmenu.lyr, vthemebutton.lyr, vthemedirectoriesdialog.lyr, vupdatedialog.lyr, vdialog.lyr: the settings view, theme pickers, the update prompt, and dialog scaffolding.app.lyr is App, the base for an app's root layer: it carries the app's Icon and Name, which the titlebar links to.vform.lyr, vmeter.lyr, growbutton.lyr: composed pieces. Form is a rounded Tertiary column that stacks transparent Form Field editors with no spacing, separated by 2px Form Divider rules (author Field / Divider / Field / ... as its children); Meter is a progress bar driven by a Grow bind on its Fill Bar; Grow Button is a button that grows to fill its row, with a centered label.Interaction flows native → core: the reconciler forwards a widget's event to app.dispatch(path, event), and the core routes it: a click on a theme button switches themes, a click on a check box toggles its Checked state, a click on a bound widget invokes the app's handler. After handlers run, the core re-renders and the reconciler applies the diff.
App code wires clicks with bind_click:
app.bind_click("App/Main Window/Content/Send", [&app] {
// handle the click, rebind content; the next render picks it up
});
Styles author the static face of a widget; VApp binds overlay live values by style path:
| Bind | Drives |
|---|---|
bind_text(path, value) |
a Text/Editor/Markdown/Host node's content |
bind_media_source(path, value) |
an Image/Video source |
bind_href(path, value) |
a Link target |
bind_qr_data(path, value) |
a QR payload |
bind_visible(path, bool) |
show/hide (drops the node from the tree) |
bind_height(path, px) |
a fixed height override |
bind_click(path, handler) |
click handling |
set_editor_text() / editor_text() write and read a field's value. For search-as-you-type, set_editor_input_handler() receives each keystroke's text. The floating placeholder, secret masking, multi-line behavior, and read-only log semantics (auto-scroll to bottom on rebind) all come from the style's attributes.
A Check Box toggles its own Checked state on click; read and write it with checkbox_checked(path) / set_checkbox_checked(path, bool).
A Combo Box renders the platform's native single-choice selector inside the style's box. The platform owns the popup interaction, like an Editor owns text entry. App code feeds the choices, seeds the selection, and hears picks:
app.set_combo_options(path, {{"intro", "Introduction"}, {"api", "API Guide"}});
app.set_combo_selected(path, "api"); // seed a form (feed options first)
app.set_combo_input_handler(path, [](const std::string& id) {
// the user picked `id`
});
std::string current = app.combo_selected(path);
The option set is app data and rides the render tree (unlike an Editor's typed value, which never does); a user pick flows back through dispatch(path, "select:<id>"), so headless tests drive a combo the same way the reconcilers do. If a re-fed option set no longer contains the recorded selection, the selection resets to the first option, so combo_selected() always names a choice the widget actually offers.
Data-driven rows are generated from a template, not built by hand. A container style binds Bind: "List:<key>"; its first child is the row template (an optional second child is a separator template stamped between rows). App code supplies the rows:
app.set_list("contacts", {
{"id-1", {{"name", "Ada"}, {"status", "online"}}},
{"id-2", {{"name", "Alan"}, {"status", "away"}}},
});
Inside the row template, field binds pull each row's data:
"Field:<name>" fills a node's text, or an Image/Video source (empty collapses it)."Placeholder:<name>" fills an Editor's floating placeholder."Opacity:<name>" fades a per-row affordance from a boolean-ish field without collapsing its slot."MaxWidth:<name>" / "Indent:<name>" set numeric per-row layout: a width cap, or a left inset (e.g. threaded reply depth)."Aspect:<name>" sets a media node's Aspect Ratio (width over height) from a numeric field, so each row's image/video can hold its own shape; a non-positive or absent field keeps the authored ratio.A Bind value can compose several of these with ; (e.g. "Field:image; Aspect:aspect" binds one media node's source and shape). The structural binds (List:, ThemeList) stay whole-value.
Rows resolve with @First/@Last states folded in at the ends, so templates can round outer corners or hide a trailing divider. A row button declares its action as Intent: "list:<action>"; clicks arrive at the app's list action handler (set_list_action_handler) carrying the list key, row id, and action name.
The settings menu is built from the same machinery: Bind: "ThemeList" makes a container generate one button per loaded theme from its first child as a template, and ActiveThemeName / ActiveThemePublisher / ThemeName / ThemePublisher fill text nodes from theme metadata.