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:

  1. a Rust type that implements Widget — the implementation (see Writing a widget in Rust);
  2. a .gdw manifest — 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 ChartView and 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 .gdc against 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 .gdc and 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:

TypeWhat it is
i32, i64, f32, f64, bool, Stringthe .gdc scalars
Brusha solid color or gradient
Graphicvector geometry or a file’s pixels (an (asset …) / (path …))
RichTexta string with styled spans (a (rich …))
ScrollAxesone 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.