getting-started.md raw
Getting started
Requirements
- Rust (edition 2024 toolchain).
- Linux with a Wayland compositor. The GPU renderer needs Vulkan-capable drivers; the software renderer needs nothing beyond a compositor.
guiduck is not on crates.io yet; depend on it by path or git:
[dependencies]
guiduck = { path = "../guiduck/crates/guiduck" }
Your first app
A guiduck application is two files: a component file describing the interface, and Rust supplying the logic.
Component files live in a components/ directory under your crate root
(the component search path — configurable, see below), one component
per file, with the file named after the component: Hello lives in
components/Hello.gdc.
(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)))
Reading it top to bottom: the component declares one piece of state
(count, an i32 starting at 0) and one handler name (bump).
Then comes the widget tree — a column container holding a label and a
button. "Clicked {count} times" is a binding: it reads count, so
it re-renders whenever count changes. :on-click bump wires the
button’s click to the handler.
Notice what is not there. Nothing says how to lay the column out when the window resizes, nothing repaints the label, and nothing describes a frame. You declare the relationship — this text is that state — and the framework keeps it true.
On the Rust side:
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();
}
include_component!(Hello) finds Hello.gdc on the search path,
compiles it at build time, and expands to generated types:
HelloProps— one field per declared prop (here: none).HelloCx— what handlers receive: each state as a writable signal, each prop as a read-only signal, each output as an emitter.trait HelloLogic— one method per declared handler. Forgetting or misspelling a handler is a compile error at yourimpl.struct Hellowithmount(...)— builds the widget subtree, installs the bindings, wires the events.
guiduck::run(title, tree) opens a window and runs the event loop.
Closing the window exits.
This exact app ships as an example:
cargo run -p guiduck --example hello
Edit it while it runs
Leave it running and change components/Hello.gdc — the padding, the
label, the whole tree — and save. The window follows, and count keeps
its value.
That is not a separate mode you turned on. mount takes the structure
from the file when this is a development build and the file is where the
build found it, and from the compiled component otherwise, so a shipped
binary never looks for it. Your handlers stay compiled Rust either way:
what reloads is the shape, not the logic, which is why a reload cannot
break a type.
The two paths are held identical by a differential test suite that
replays real input through both, so which one you are running is not
something you should be able to tell. If you ever can, that is a bug in
guiduck, and guiduck::testing is the same oracle so you can say so
precisely.
Choosing a renderer
The GPU renderer (vello on wgpu) is the default. Set an environment variable to use the software renderer (tiny-skia) instead — same scene, near-identical pixels:
GUIDUCK_BACKEND=software cargo run -p guiduck --example hello
GUIDUCK_TRACE_FRAMES=1 logs a line per rendered frame to stderr;
you will see that an idle guiduck app renders nothing.
The component search path
Component names resolve by probing each directory of the search path
for Name.gdc, first hit wins. The default is a single components/
directory under the crate root. Override it in Cargo.toml:
[package.metadata.guiduck]
component-path = ["components", "src/widgets"]
The same path serves include_component! and references between
components (a capitalized name inside a .gdc file — see
the component language). Editing any referenced file
triggers a rebuild of the components that use it.
Fonts
WidgetTree::new() uses your system fonts. For hermetic rendering
(tests, reproducible goldens) or bundled fonts, construct the tree with
an explicit text context:
use guiduck::core::text::TextContext;
let tree = WidgetTree::with_text_context(
TextContext::with_extra_fonts([my_font_bytes]),
);
Where to go next
- The component language — everything a
.gdcfile can say. - Widgets — the builtin widget vocabulary.
- Writing a widget in Rust — implementing the
Widgettrait: layout, paint, input, theming, accessibility. - Widget manifests — naming your own Rust
widgets from a
.gdcwith a.gdwfile. - Theming — moving colors and fonts out of components.
- The Rust side — generated code in detail, signals,
hot reload, and skipping
.gdcentirely.