# guiduck A data-driven GUI framework for Rust. You describe *what your interface is* in a small declarative file; you write *what it does* in plain, typed Rust. guiduck wires the two together with fine-grained reactivity, checks the whole thing with rustc, and repaints only what changed. ## A complete app `components/Hello.gdc` — the structure, layout, and wiring: ```lisp (component Hello (state count i32 :init 0) (handler bump) (container :direction column :padding 16 :gap 12 :background "#ffffff" (text :font-size 18 :text "Clicked {count} times") (button :label "Click me" :width 120 :height 36 :on-click bump))) ``` `main.rs` — the logic: ```rust use guiduck::core::WidgetTree; guiduck::include_component!(Hello); struct Logic; impl HelloLogic for Logic { fn bump(&mut self, cx: HelloCx) { cx.count.set(cx.count.get_untracked() + 1); } } fn main() { let mut tree = WidgetTree::new(); Hello::mount(&mut tree, None, HelloProps::default(), Logic); guiduck::run("hello", tree).unwrap(); } ``` That’s the whole program. `{count}` in the label is a live binding: when the handler writes the signal, exactly that text repaints — no diffing, no manual invalidation, and no frames at all while nothing changes. `include_component!` compiles the file at build time into real Rust, so **rustc checks everything**: `HelloLogic` is a generated trait with one method per declared handler — misspell `bump` or forget it and your program refuses to compile, with a helpful error message. Bindings type-check too. ## Easy to grow, hard to outgrow **Compose components like widgets.** A capitalized name instantiates another component; props flow down (reactively — a parent binding keeps driving a child prop after mount), outputs wire back up to your handlers, and `(slot)` lets a component accept content: ```lisp (Card :title "Totals: {total}" (CounterButton :key "fives" :label "fives" :step 5 :on-stepped tally)) ``` **Theme without touching code.** A widget's appearance is *data*: rules in a theme file supply it, keyed on widget type, style class, and interaction state, and are swappable at runtime. ```lisp (rule (button .primary) :background accent :corner-radius 8) (rule (button .primary :hover) :background accent-hover) ``` That includes shapes, not just colours — a checkbox's mark is a `(path …)` or an `(asset …)`, interchangeably — so restyling a control is a rule rather than a patch. A built-in default theme means controls look right with no theme at all, and yours overrides only what it names. **Edit the UI while it runs.** A development build takes its structure from the component file itself and hot-reloads on save — state survives, handlers stay compiled Rust — while anything you ship uses the compiled path. There is no flag and no branch in `main`: `mount` chooses. That is only safe because the two paths are *held* identical. A differential suite replays real input through both and requires the same scene after every step, and the oracle is public — `guiduck::testing` — so your own components can be held to it too. **Two renderers, one scene.** GPU (vello/wgpu) by default, a tiny-skia software renderer with `GUIDUCK_BACKEND=software` — same display list, continuously held to near-identical output, so headless tests and GPU-less machines are first-class. **A real widget set, and no models.** Buttons, checkboxes, radios, switches, text inputs (single- and multi-line), sliders, steppers, progress bars, images, scroll areas, tabs, lists, expanders, separators — and menus with submenus and accelerators, context menus, dropdowns, combo boxes, tooltips, and dialogs. None of them holds your data. A menu's rows, a list's rows, and a tab strip are all a `for` over *your* records, and the wire carries identity: ```lisp (menu :label "Recent" (for doc recents :key doc.id (menu-item :label doc.name :on-select (open-recent doc.id)))) ``` Popups are built when they open and disposed when they close, all the way down — a closed menu's rows do not exist, lay out nothing, and subscribe to nothing, so only the path you actually opened costs anything. The same idea runs through the *values*. A checkbox, a slider, a dialog, and a tab are all **controlled**: your state is the truth, the widget reports what the user did, and your binding decides what happens. That is why there is no radio-group widget and no tab-group widget — "one of these" is a comparison in a binding, not a container holding a selection. ```lisp (for choice choices :key choice.id (radio :label choice.name :checked (= choice.id chosen) :on-select (choose choice.id))) ``` **Your own widgets are first-class.** Implement `Widget`, describe its interface in a `.gdw` manifest, and an application that depends on your crate can name it in a `.gdc` with no configuration at all. The framework's own widgets go through exactly that path — they are declared in the same grammar and registered the same way — so there is no privileged set to be second-class against. **The platform essentials, built in.** Flexbox layout (taffy), proper text shaping (parley) with IME support, rich text, clipboard (*and* primary selection on supported platforms), double/triple-click selection, Tab focus traversal, keyboard scrolling, the desktop's own file chooser through xdg-desktop-portal, and accessibility designed in from the first line — not retrofitted. Every control announces what it *is*, which is why a radio is a widget rather than a rounded checkbox. ## Documentation - [Getting started](docs/getting-started.md) — from empty project to a running app. - [The component language](docs/components.md) — the `.gdc` reference: declarations, expressions, bindings, events, composition, slots. - [Widgets](docs/widgets.md) — the builtin widgets and their properties, events, and behavior. - [Writing a widget in Rust](docs/writing-widgets.md) — implementing the `Widget` trait: layout, paint, input, timers, theming, accessibility. - [Widget manifests](docs/widget-manifests.md) — naming a Rust widget from a `.gdc` with a `.gdw` file. - [Theming](docs/theming.md) — the `.gdt` theme file reference. - [The Rust side](docs/rust-api.md) — generated code, logic traits, signals, dev mode, testing, and building trees in plain Rust. ## Repository layout | Crate | What it is | | --- | --- | | `guiduck` | The facade applications depend on; examples live here | | `guiduck-scene` | Display list, fragment store, render-backend trait | | `guiduck-render-vello` | GPU backend (vello on wgpu) | | `guiduck-render-tinyskia` | Software backend; golden-image harness | | `guiduck-signals` | Fine-grained signal runtime (signals, memos, effects, scopes) | | `guiduck-core` | Widget tree, layout, events, styles, focus, accessibility | | `guiduck-component-core` | The `.gdc`/`.gdt` compiler: reader → AST → validate → IR | | `guiduck-component-macro` | `include_component!` — IR to typed Rust | | `guiduck-component-rt` | The IR interpreter and hot reload (dev mode) | | `guiduck-app` | The winit shell: window, input, frame scheduling | ## Development ```sh cargo check --workspace # compile status cargo test --workspace # unit, integration, golden, differential ./scripts/parity.sh # render every example on both backends, compare ./scripts/counter-interaction.sh # compositor-driven interaction tests ./scripts/m4-theme.sh # (each milestone has one; they drive a real ./scripts/m10-nested.sh # Wayland compositor via quibble) ./scripts/shutdown.sh # every example exits cleanly when its window closes ``` The suite is the gate, and the interactive half is not optional: goldens and differentials pin what is *drawn*, and the scripts are what exercise what is *registered* — a keystroke that never reached its handler renders identically to one that did. `shutdown.sh` covers the piece neither can: an application's own exit path, which the other scripts skip by tearing the compositor down underneath a still-running window. Examples run with `cargo run -p guiduck --example `; set `GUIDUCK_BACKEND=software` to use the software renderer and `GUIDUCK_TRACE_FRAMES=1` to log each rendered frame