widget-manifests.md raw
Naming your Rust widget from a .gdc: the .gdw manifest
The builtin widgets are Rust code, and so is every control a real
app eventually needs that the framework does not ship — a chart, a canvas, a
map, a document view. You write one in Rust, in your own crate (see
Writing a widget in Rust), and then name it from a
.gdc exactly as you name a container:
(chart :source "revenue.csv" :on-point (inspect payload))
To get there you write one small file — a .gdw manifest — describing the
widget’s interface, and connect it to your Rust type with one macro call. This
is exactly how the framework’s own widgets work: there is no privileged path,
and container is registered under the same contract your chart is. The
framework’s markdown widget lives in guiduck-markdown, a crate that depends on
guiduck-core and pulldown-cmark and nothing else guiduck ships — nothing in
guiduck-core, the component compiler, or either code-generation back end knows
the word “markdown”. It is spelled in one manifest, and there only.
The problem a manifest solves
The component compiler runs as a proc-macro at build time, and a proc-macro
cannot see another crate’s types. There is no reflection: nothing in the build
can discover your Widget impl, read its method names, or learn what type a
setter takes. So the compiler cannot, on its own, know that chart means your
ChartView, that it has a set_source method, or that the method takes a
String.
A .gdw manifest is how you tell it. It is a small data file that lists the
widget’s name, the properties and events it offers, and the Rust type behind it.
The compiler reads the manifest, type-checks every .gdc that uses the widget
against it, and generates the code that drives your setters. Think of a manifest
as a typed use statement for the widget vocabulary: it says “this name is a
widget, and here is what it accepts.”
Nothing in the manifest is trusted blindly. A manifest is a promise about your
Rust type, and register_widget! (next) generates code that actually calls your
methods — so rustc checks the promise against reality. A manifest that names a
setter your widget lacks, or a wrong type, is a compile error.
A manifest is an s-expression file, like a .gdc component or a .gdt theme.
Its extension is .gdw.
The two files and the line that connects them
A widget you name from a .gdc is, in your crate:
- a Rust type that implements
Widget— the implementation (see Writing a widget in Rust); - a
.gdwmanifest — the interface;
joined by one macro call:
guiduck_core::register_widget!(ChartView, "widgets/chart.gdw");
register_widget! is the linchpin. At build time it reads the manifest and:
- generates the runtime factory — a function that builds a
ChartView::default()and, for each property the manifest declares, calls the matching setter with the incoming value. Because a widget is built this way — default, then setters — the framework can construct your widget without naming its type, which is what lets one code path serve every widget and what the hot-reload interpreter needs (it builds a widget it has never heard of). - type-checks that factory against your real methods. The generated setter
calls name
ChartViewand the value types the manifest declared, so if the manifest and your Rust disagree, rustc reports it at this line — the manifest can never validate a.gdcagainst a lie. - registers the factory in a link-time table, so any application that links
your crate can build the widget, in a compiled build and under hot reload
alike — the app declares your widget in a
.gdcand never has to hand the framework a constructor.
The manifest path is relative to your crate root. That is the entire connection:
implement Widget, write the .gdw, call register_widget!.
A widget crate’s whole relationship with the framework is a single dependency,
guiduck-core — it re-exports register_widget!, so you need no facade crate,
no macro crate, no interpreter, and no build script.
The grammar
(widget chart
(type "::my_charts::ChartView")
(prop source String)
(prop stacked bool)
(event on-point :payload i64)
(token line-color Brush))
A .gdw file holds one or more (widget …) forms — a crate’s whole widget set
can be one file. Each form is a lowercase name followed by these items.
(type "…") — required, exactly one
The Rust path used to construct the widget. It is a string, not a symbol: the
reader ends symbols at :, so a path like ::my_charts::ChartView cannot be
one, and the quotes are the grammar being honest about its own lexer rather than
special-casing it.
The compiler never resolves this path. It is carried verbatim into generated code, and rustc reports a bad path against the construction it generates. That is the whole point of the split: the interface is data the compiler can check, and the implementation stays Rust that the compiler you already trust checks.
(prop name Type) — a settable property
Each prop becomes a property a .gdc may set once at build or drive from a
binding. Type is one of:
| Type | What it is |
|---|---|
i32, i64, f32, f64, bool, String | the .gdc scalars |
Brush | a solid color or gradient |
Graphic | vector geometry or a file’s pixels (an (asset …) / (path …)) |
RichText | a string with styled spans (a (rich …)) |
ScrollAxes | one of vertical / horizontal / both |
This is the framework’s own writable property vocabulary — the same set the
builtins draw from. A (prop icon Graphic) on your widget is checked, set, and
bound by exactly the code that serves an image’s :source, so your widget is
not a second-class citizen: it can take a themeable graphic or rich text for the
same reason an image and a text can. (Records are file-local to a .gdc, and
lists have no setter convention, so neither crosses this boundary — data-driving
a widget is composition in the .gdc, with for, not a prop type.)
The setter convention. A prop named source is set by calling set_source;
line-color maps to set_line_color (kebab-case to snake_case). If your method
is not spelled that way, name it:
(prop scale f32 :setter set_zoom)
This is the same override the builtins use — :text is set_content on a
text-input but set_text on a text. Reach for it when the method’s own name
is the right one and the convention would miss it, not to satisfy the convention
by renaming a good method.
(event on-name) / (event on-name :payload Type)
An event your widget raises, wired in a .gdc to a handler. Names start with
on-. A payload-less event is a bare request:
(event on-dismiss)
A payload event delivers a value the wire’s payload keyword reads:
(event on-point :payload i64)
(chart :on-point (inspect payload))
The payload Type is a scalar (i32, i64, f32, f64, bool, String).
What the value means is the application’s: guiduck delivers it and the handler
interprets it. Your widget raises the event by pushing an
EventData::User { name, payload } from an input hook — see
Reporting to the application.
(query name :payload Type :setter method :returns "Path")
A query is a wire whose handler returns a value to the widget, rather than being dispatched as a fire-and-forget event. The markdown widget’s image resolution is what this exists for — the document holds link targets and image sources, opaque strings only the application can turn into pixels:
(query get-image :payload String :setter set_image_resolver
:returns "::guiduck_markdown::ImageResult")
A .gdc wires it like an event — with the payload:
(markdown :source page :get-image (load payload))
but the handler returns the query’s type, and codegen installs it as a resolver
through the :setter rather than registering it as an event:
fn load(&mut self, cx: PageQueryCx, url: &str) -> guiduck_markdown::ImageResult { … }
So the widget calls the handler synchronously when it needs the value. On your
Rust type the contract is a method
fn set_image_resolver(&mut self, impl Fn(&PayloadType) -> ReturnType + 'static).
:returns is a verbatim Rust path (like (type …)) — the compiler never
resolves it; rustc checks it against the generated closure. The return type must
be Default (its default is the “no value” answer the interpreter falls back to
when it cannot reassemble the handler’s context).
A query handler receives a read-only context (XQueryCx, not XCx): it may
read the component’s props and state to inform its answer but cannot set state.
That is deliberate — a query answers, it does not act, and it runs inside the
widget’s layout pass, a phase where mutating state would be out of order with the
frame pipeline. (An event handler, which runs at the designed mutation point,
gets the writable context and mutates state freely.)
(token name Type) — a theme token
A value the widget reads from its computed style, resolved through
the same style engine every builtin uses. Type is Brush, Number, Str, or
Graphic. A theme sets it with an ordinary rule:
(rule (chart .revenue) :line-color "#3b6ea5")
Tokens are for anything that is a convention rather than a law — “a link is
blue and underlined”, “a bullet is •”, “the line is 2px”. A widget that
hard-coded those could not be themed. A Graphic token lets a theme swap vector
geometry for a raster (or the reverse) with no widget change, exactly as a
checkbox’s check mark can be replaced. On the Rust side you read tokens in
apply_style — see Theming.
Structural capabilities
Beyond properties, events, and tokens, a .gdw can declare a few facts about
how the widget fits into the tree. These used to be things only the framework’s
own widgets could say; they are ordinary manifest forms now, so an application’s
widget can be a list row, a panel a parent builds, or a menu item.
(parents name…) — where the widget may appear
Restricts the widget to being a direct child of the named widgets. A row that only makes sense inside a list, a tab inside a tab bar:
(widget tab
(type "::my_tabs::Tab")
(parents tab-bar))
Writing (tab …) anywhere but inside a (tab-bar …) is then a compile error
that names where it belongs. Omit the form for a widget that may appear anywhere
(the common case). The names are checked against the real vocabulary, so a typo
in a parent name is caught too.
(internal) — a widget a parent builds
Marks a widget that no .gdc writes, because a parent widget builds it in Rust —
the popup panel a menu opens is the framework’s example. An (internal) widget
still has a name (so a theme can target it) and still takes tokens, but it is
rejected in widget position in a .gdc. Most application widgets are not
internal; you need this only for a sub-widget your own widget constructs.
(accel :setter method) — a keyboard accelerator
Declares that the widget carries a menu-style accelerator (:accel "Ctrl+S" in a
.gdc). This is specialized — it is what a menu-item-like widget uses — and it
does two things at once: the :setter method receives the accelerator’s rendered
text, and the tree registers the keystroke so it fires the widget’s :on-select
wire even while the widget is not on screen. Reach for it only if you are
building something menu-item-shaped.
Deferred content is a Rust capability, not a manifest one
A widget whose children are a popup — built when it opens, not at mount — opts
in through two Widget trait methods (defers_content and set_content), with
no manifest syntax at all. Whether your widget defers its content is its own
runtime answer, invisible to the .gdc that writes it, so it belongs with the
implementation. See
Deferred content. This is the same
seam menus, dropdowns, and dialogs use, and it is open to your widgets too.
The one thing genuinely reserved to the framework is the closed set of typed
events its own controls emit (a checkbox’s toggle, a text’s link). Your
widget’s events are (event …) declarations that ride the general
EventData::User path — which is all an application ever needs, and what the
markdown widget’s on-link uses.
The contract your Rust widget owes
A registered widget is Default + Widget plus one setter per declared prop.
There are no constructor arguments — a prop is a setter call. This is what lets
one code path serve builtins and app widgets alike, and what the runtime
interpreter needs, since it constructs a widget it has never heard of and cannot
pass arguments to.
So a prop must be settable at build and re-settable from a binding through the
same method. Give the widget sensible defaults via Default, and let each
set_* both initialize and update, comparing-and-setting so a binding that
re-sets the current value marks nothing and does not loop. The full
implementation guide, including this contract, is
Writing a widget in Rust.
Using the widget from an application
An application names your widget in a .gdc with no configuration at all.
It depends on your crate (which it must, to link the register_widget!
registration and to name the type in generated code), and include_component!
finds your .gdw automatically: it runs cargo metadata for the crate being
built and reads the widget directory of every resolved dependency, so a
crates.io, git, path, or workspace dependency all resolve the same way. There is
nothing to copy and nothing to point at.
The one requirement on your side, as the widget’s author, is that the .gdw
ships in your package and sits where discovery looks. The default is a widgets/
directory under your crate root — where register_widget!("widgets/chart.gdw")
already reads it from, and which Cargo includes in a published package by
default. To use a different directory, name it in your Cargo.toml:
[package.metadata.guiduck]
widget-path = ["widgets", "vendor/widgets"]
Directories are probed in order, first hit wins — the same search-path mechanism the component and asset paths use, under a different key.
Editing a .gdw rebuilds the crate exactly as editing a .gdc does (the
manifest is tracked as a build input), and in dev mode a live edit that adds a
(chart …) to a running .gdc resolves without a rebuild, because the whole
linked widget vocabulary is available to the interpreter.
A complete example
A widget crate, depending only on guiduck-core:
// my-widgets/src/lib.rs
use guiduck_core::{Widget, register_widget};
#[derive(Default)]
pub struct NoteView { /* … */ }
impl NoteView {
pub fn set_title(&mut self, title: String) { /* compare-and-set, mark dirt */ }
pub fn set_zoom(&mut self, scale: f32) { /* … */ }
}
impl Widget for NoteView { /* paint, measure, on_pointer, … */ }
// Reads the manifest, generates the rustc-checked factory, and registers it.
register_widget!(NoteView, "widgets/note.gdw");
Its manifest, my-widgets/widgets/note.gdw:
(widget note
(type "::my_widgets::NoteView")
(prop title String)
(prop scale f32 :setter set_zoom)
(event on-open :payload String)
(event on-dismiss)
(token ink Brush))
An application that depends on my-widgets writes it in a .gdc, with a theme
dressing its token — checked exactly the way a builtin is:
(note :title "Draft" :scale zoom
:on-open (open-note payload) :on-dismiss close-note :class "pinned")
(rule (note .pinned) :ink "#b58900")
No widget-path in the application, no copied files: the note.gdw shipped in
my-widgets is discovered through the dependency.
Diagnostics
A malformed manifest is a compile error pointing into the .gdw: a missing or
duplicate (type …), an unknown prop or token type (the message lists the ones
it knows), an event name without on-, a prop declared twice, a name that
redeclares a property every widget already has (:width, :class, :autofocus,
:on-click, …), a capitalized widget name (that would be a component reference,
so nothing could ever write the widget), or a name that collides with a builtin
or another manifest. A stale type path or setter name is caught one level
down, by rustc, against the code the manifest generates — which is the division
the whole design turns on: the compiler checks the interface, rustc checks that
the interface tells the truth about the implementation.