# Writing a widget in Rust The [builtin widgets](widgets.md) and the framework's own controls are Rust types that implement one trait, `guiduck_core::Widget`. When your application needs something the framework does not ship — a chart, a canvas, a document view, a custom gauge — you write it the same way, in your own crate, and it is a first-class widget: themed, laid out, hit-tested, and made accessible by the same machinery as a `button`. This is the guide to *implementing* a widget. To then **name it from a `.gdc`**, you declare its interface in a `.gdw` manifest — see [Widget manifests](widget-manifests.md). A widget you only ever build from Rust (the `WidgetTree` API below) needs no manifest at all. ## The model A `WidgetTree` is a retained tree of widgets. Each widget: - owns its **content and visual state** (a paragraph's text, a checkbox's checked flag, a scroll area's offset); - has a **layout style** that lives in [taffy](https://docs.rs/taffy), not in the widget — you pass a `taffy::Style` when you insert it, and taffy computes every widget's rectangle; - **paints itself** into a display-list fragment, which the tree composites with its children. A widget does **not** own its children — the tree does. A widget that needs to render several sub-parts (a document with many paragraphs) measures and paints them itself; it cannot insert child widgets of its own. Everything else is a `Widget` trait method, and every method has a default, so you implement only what your widget actually does. ```rust use guiduck_core::{Widget, taffy}; use guiduck_core::text::TextContext; use guiduck_scene::Fragment; use guiduck_scene::geom::Size; use guiduck_scene::paint::{Brush, color::palette::css}; /// A solid rectangle of a fixed color — about the smallest useful widget. pub struct Swatch { color: Brush, } impl Widget for Swatch { fn paint(&mut self, fragment: &mut Fragment, size: Size) { let bounds = guiduck_scene::geom::Rect::from_origin_size((0.0, 0.0), size); fragment.fill(bounds, self.color.clone()); } fn type_name(&self) -> &'static str { "swatch" } } ``` Add it to a tree: ```rust let mut tree = WidgetTree::new(); let root = tree.insert(Swatch { color: css::REBECCA_PURPLE.into() }, style, None); ``` `insert(widget, style, parent)` returns a `WidgetId`; pass `None` as the parent for the root, or an existing id to nest. The `taffy::Style` is the layout — flex direction, size, padding, grow/shrink — exactly as a `.gdc`'s layout properties compile to. Coordinates in `paint` (and everywhere else a widget sees them) are **local**: the widget's own top-left is `(0, 0)`, and `size` is the rectangle taffy gave it. The tree applies the widget's absolute position when it composites. ## Layout: `measure` and `finalize_layout` A **leaf** widget with intrinsic content — text, an image — reports its natural size so taffy can lay it out: ```rust fn measure( &mut self, text: &mut TextContext, known: taffy::Size>, available: taffy::Size, ) -> taffy::Size { … } ``` taffy calls `measure` only for widgets with no taffy children, and may call it several times per layout (probing min-content and max-content). `known` carries any axis already fixed by the style; `available` is what taffy offers on each axis. Return the size your content wants. A widget whose size comes entirely from its style (a container, the `Swatch` above) does not implement `measure`. `finalize_layout` runs once per layout, **after** taffy has assigned the final rectangle, before paint: ```rust fn finalize_layout(&mut self, text: &mut TextContext, size: Size, content_size: Size) { … } ``` This is where size-dependent content settles — a paragraph re-wraps to the assigned width here, so the glyphs painted next are the right ones. `content_size` is the extent of the (possibly overflowing) content, which a scroll container uses to learn its scrollable range. Text shaping needs the `TextContext` both methods receive; it holds the font collection and parley's caches. Use it to build a [`Paragraph`](../crates/guiduck-core/src/text/paragraph.rs) — the reusable "styled, wrapping, hit-testable text" primitive that `Text` and the markdown widget are both built from — rather than driving parley yourself. ## Painting `paint` emits display items into a `Fragment`: ```rust fn paint(&mut self, fragment: &mut Fragment, size: Size) { fragment.fill(shape, brush); // filled path/rect fragment.stroke(shape, brush, Stroke::new(w)); // outlined path fragment.image(image_brush, dest_rect); // raster fragment.push_clip(shape); /* … */ fragment.pop_clip(); // clip a region } ``` Shapes are [kurbo](https://docs.rs/kurbo) types (re-exported under `guiduck_scene::geom`): `Rect`, `RoundedRect`, `BezPath`, `Circle`. Brushes are [peniko](https://docs.rs/peniko) `Brush`es — a solid `Color`, a gradient — under `guiduck_scene::paint`. To draw text, build a `Paragraph` and call its `paint`, or use `guiduck_core::text::append_layout` for a parley `Layout` you hold. `paint_overlay` draws *over* the children (a scroll area paints its thumbs here); the tree calls it after compositing the children on top of `paint`'s output. Most widgets do not implement it. Do not decide *where* your widget is on screen or whether it is hovered/focused by painting differently based on remembered state you could get wrong — paint is a pure function of your content and `size`. Interaction state that changes appearance comes through the theme (see [Theming](#theming-apply_style) below). ## Change tracking: `Dirt` The framework repaints and re-lays-out **only what changed**. A widget with mutable state keeps a `Dirt` field and marks it in its setters: ```rust use guiduck_core::Dirt; pub struct Gauge { value: f32, dirt: Dirt, } impl Gauge { pub fn set_value(&mut self, value: f32) { // Compare-and-set: re-applying the same value marks nothing, which is // what makes a binding that re-sets the current value free. if self.value != value { self.value = value; self.dirt.mark_paint(); // value changes the drawing, not the size } } } impl Widget for Gauge { fn take_dirt(&mut self) -> Dirt { std::mem::take(&mut self.dirt) } // … measure / paint … } ``` The rule for **which** to mark: `mark_layout()` if the change can alter the widget's size or its content's geometry (new text, a new font size, a different number of segments); `mark_paint()` if only the pixels change at the same geometry (a color, a gauge needle angle). A layout change implies a repaint, so never mark both. If you are unsure, `mark_layout()` is the safe (if slightly pessimistic) choice. **Setters compare-and-set and mark dirt; that is the entire contract.** It is what lets the same setter serve a one-time build value and a reactive binding without either looping, and it is why a registered widget (below) needs nothing but `Default + Widget` plus one setter per property. ## Input Pointer, keyboard, and IME events arrive through hooks, all with **local** coordinates. ### Pointer ```rust fn on_pointer( &mut self, kind: EventKind, // PointerDown | PointerMove | PointerUp | Click | PointerEnter | PointerLeave event: &PointerEvent, // event.local, event.button, event.click_count text: &mut TextContext, clipboard: &mut dyn Clipboard, ) -> bool { … } ``` The gesture model, which the selectable `Text` and the text input both use: - A **press** (`PointerDown`) that returns `true` **captures the pointer**: the widget then receives every `PointerMove` until the release, even outside its bounds — this is how a drag (selection, a slider, a scrollbar thumb) works. A press that returns `false` does not capture. - `Click` is synthesized by the tree when a press and release land on the same widget. Its return value is *ignored* by the tree (only a press captures), so use it to act, not to gate. - `event.click_count` is 1/2/3 for single/double/triple, so a widget can select a word on double-click without tracking timing itself. A widget's own `on_pointer` runs *before* any app handler wired to it, so a checkbox toggles on its own click and then the app's `:on-toggle` sees it. `cursor(local)` returns the pointer shape for a given position — a pure function of where the pointer is, so a paragraph can show the hand only over its links without tracking hover separately: ```rust fn cursor(&self, local: Point) -> CursorShape { if self.hit_something(local) { CursorShape::Pointer } else { CursorShape::Default } } ``` ### Keyboard, IME, and focus `on_key` and `on_ime` are delivered only to the **focused** widget. `on_key` returns `true` to consume a key; an unconsumed key falls through to overlay dismissal, app shortcuts, and finally Tab traversal — so a text input's Ctrl+C wins over an app accelerator, and Tab still works when a widget declines it. To participate in focus at all, override `focusable`: ```rust fn focusable(&self) -> bool { true } ``` `tab_stop` defaults to `focusable`, and the two come apart only for a widget you focus in order to *operate* rather than to *edit* — a scroll area is click-focusable (so arrows scroll it) but not a Tab stop (it should not interrupt a Tab walk between the controls inside it). A selectable label is the same: focusable, not a Tab stop. Override `tab_stop` to `false` for those. `on_focus_changed(focused)` fires when focus arrives or leaves — a text input starts and stops its caret blink here; a label drops its selection on blur. The framework maintains a **focus invariant**: whenever any Tab-stop widget exists, one has focus. Your widget does not enforce it; making `focusable`/ `tab_stop` honest is all it owes. ### Reporting to the application A widget never calls an application handler directly. It **emits** a semantic event, and the tree collects it on the same borrow it collects dirt and dispatches it: ```rust fn take_emitted(&mut self) -> Vec { std::mem::take(&mut self.emitted) } ``` Push an `EventData` from an input hook when *user* action changed something — `Changed(String)`, `Toggled(bool)`, `ValueChanged(f64)`, `Selected`, `Link(String)`, or `User { name, payload }` for an event your `.gdw` declared. Pick by what the payload *is*: the compiler checks the wire against it, so reporting a number as a `Changed` string would make a handler's declared type a lie. **Programmatic setters must not emit** — only user input does — or a `:value`-style binding would loop: the handler writes state, the binding re-sets the widget, the widget re-emits, forever. Compare against the last emitted value if you have to (the text input keeps a `last_emitted` string for exactly this). ## Timers A widget that animates or blinks asks for a wake instead of reading a clock: ```rust fn take_wake(&mut self) -> Option { self.wake.take() } fn on_timer(&mut self) { /* advance, mark paint, re-request a wake to keep going */ } ``` `take_wake` is one-shot and collected with dirt; a steady beat re-requests from `on_timer`. The widget never sees the current time — the tree anchors the duration when the platform supplies "now" — which is what makes timed behavior deterministic in tests (they feed a fabricated clock). A widget that stops requesting wakes lets the app go idle, preserving the zero-idle-frames guarantee. ## Scrolling Two hooks make a widget a **scroll container**, and the tree applies them to painting, hit testing, and absolute-position math from one place, so pixels and pointer targets can never disagree: - `content_offset() -> Vec2` — the translation applied to children beyond their layout positions (the scroll offset, negated). - `clips_content() -> bool` — whether children are clipped to the widget's box. - `on_scroll(delta) -> bool` — handle a wheel/touchpad scroll; return `true` if it moved, `false` to let an ancestor scrollable take over at the end of range. - `adjust_style(&mut taffy::Style)` — stamp layout requirements the widget owns (a scroll container sets `overflow: scroll`) at insert time, so callers cannot forget them. To make a focused widget's important sub-rectangle (a caret) stay visible when it sits inside a scroll area, return it from `reveal_rect`; scroll containers implement `scroll_reveal` to bring a target into view. Most widgets need neither. ## Accessibility Every widget has a `role`; add labels, values, and states in `accessibility`: ```rust fn role(&self) -> accesskit::Role { accesskit::Role::Slider } fn accessibility(&self, node: &mut accesskit::Node) { node.set_value(self.value.to_string()); } ``` A widget that renders sub-parts an assistive technology should reach on their own — the links inside a paragraph — contributes child nodes in `accessibility_extended` (using `next_id` to allocate ids from the reserved range and pushing into `update`), and handles a screen reader acting on one of them in `accessibility_action`. A generated node has no widget id, so the tree routes the action back to the widget that made it; report the result through `take_emitted`, like any other input. If a screen reader can see and announce a control but cannot activate it, it is not accessible — this hook is what closes that gap. Accessibility is not optional polish here; it is designed in from the widget's first version, exactly as the builtins do it. ## Theming: `apply_style` Do not hard-code colors, fonts, or metrics. Declare them as **tokens** and read them from the widget's computed style, so a theme can move them: ```rust fn type_name(&self) -> &'static str { "gauge" } fn apply_style(&mut self, style: &ComputedStyle) { // The style engine matches theme rules on `type_name`. Read each token and // mark dirt only if it changed — the setters' compare-and-set does that. if let Some(track) = style.brush("track-color") { self.set_track(track.clone()); } if let Some(size) = style.number("needle-width") { self.set_needle_width(size); } } ``` The style pass calls `apply_style` before your widget's first paint and again whenever the widget's interaction state changes (hover, active, focus, disabled) or a theme is swapped — so **interaction-state appearance is theme-driven**: you do not check "am I hovered" in `paint`; the theme's `:hover` rule supplies the hovered tokens and `apply_style` delivers them. Widget-internal state that is not an interaction state (a checkbox's *checked*) the widget reads from its own field and selects among token variants (`box-fill` vs `box-fill-checked`). For your tokens to resolve, the widget's type must be known to the theme registry — which, for an application widget, is what the `.gdw` manifest's `(token …)` declarations do. A theme then writes `(rule (gauge .big) :track-color "#333")` with no code involved. See [Theming](theming.md) and [Widget manifests](widget-manifests.md). ## Deferred content (popups) Most widgets that have children mount them immediately. A **popup** — a menu, a dropdown, a dialog — is different: its content should not exist until it opens, and should be built into an overlay rather than under the widget. Your widget can do the same, through two trait methods: ```rust fn defers_content(&self) -> bool { true } fn set_content(&mut self, content: ContentBuilder) { self.content = content; // store it; run it when the popup opens } ``` When a `.gdc` gives your widget children, the tree consults `defers_content` at mount: `false` (the default) builds them inline, `true` hands them to `set_content` as a `ContentBuilder` — a closure you keep and call (as many times as you reopen) with the overlay you want them under. This is the exact seam the builtin menus use, and it needs **no manifest syntax**: whether a widget defers is its own runtime answer, invisible to the `.gdc` that writes it. A closed popup costs nothing — its content lays out nothing and subscribes to nothing until it opens. ## Hosting a popup A widget that opens a panel — a menu, a dropdown, a combo box — says so by answering `popup()`: ```rust fn popup(&self) -> Option { Some(Popup { content: self.content.clone(), beside: false, // below the opener, not out to the side match_width: true, // as wide as the opener }) } fn defers_content(&self) -> bool { true } fn set_content(&mut self, content: ContentBuilder) { self.content = content; } ``` Then report `EventData::MenuOpen` when the user asks for it, and the tree does the rest: it opens an overlay, builds the content into it, tracks the stack, runs the roving highlight and the dismissal walk, and tells you when the panel closes. Nothing here is builtin-only. The tree asks the widget rather than checking what kind it is, so your own widget can host a popup on exactly the terms a menu does. ## The registered-widget contract Implementing `Widget` gives you a widget you can build from Rust and add to a tree with `insert`. To also **name it from a `.gdc`**, you write a [`.gdw` manifest](widget-manifests.md) describing its interface and connect the two with one macro call in your crate: ```rust guiduck_core::register_widget!(Gauge, "widgets/gauge.gdw"); ``` `register_widget!` reads the manifest at build time, generates the factory that constructs your widget and drives its setters, and — the point — **type-checks that factory against your real methods**, so a manifest that promises a setter you do not have is a compile error at that line. For this to work, a registered widget must be **`Default + Widget` plus one setter per declared property**, and take **no constructor arguments** — a property *is* a setter call. That is what lets one code path build every widget (the interpreter constructs a widget it has never heard of and cannot pass arguments to) and what makes a property 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 (compare-and-set, mark dirt — the same contract as [above](#change-tracking-dirt)). The manifest maps `:property` to `set_property` by convention, with a `:setter` override where your method's own name is the right one. A widget you only build from Rust owes none of this — implement whatever constructor and setters suit it. See [Widget manifests](widget-manifests.md) for the manifest grammar, how discovery finds your `.gdw`, and the worked example. ## Where the builtins live The framework's own widgets are the worked examples, in `crates/guiduck-core/src/widget/`: - `container.rs` — the structural minimum (a background and children). - `text.rs` + `text/` — a selectable, linkable paragraph; the clearest example of `measure`/`finalize_layout`, pointer selection, and paragraph a11y. - `text_input.rs` — the full editable control: keyboard, IME, clipboard, the caret blink (timers), focus. - `scroll_area.rs` — the scroll hooks and keyboard operation. - `checkbox.rs` / `button.rs` — controlled state, activation, theme-driven five-state appearance. `crates/guiduck-markdown/` is a widget in a **separate crate**, depending only on `guiduck-core` — the proof that everything above is reachable from outside the framework, and the model to copy for your own.