# The component language A `.gdc` file declares one component: its typed interface (props, state, outputs, handlers) and its widget tree, as s-expressions. Comments run from `;` to end of line. ```lisp (component CounterButton (prop label String) (prop step i32 :default 1) (output stepped i32) (state count i32 :init 0) (handler increment) (container :direction row :gap 8 :align-items center (container :width 130 :height 32 :corner-radius 6 :background "#4682b4" :on-click increment (text :color "#ffffff" :text "{label} +{step}")) (text :text "= {count}"))) ``` One component per file; the file bears the component's name verbatim (`CounterButton` lives in `CounterButton.gdc`) and is found through the [component search path](getting-started.md#the-component-search-path). ## Declarations | Form | Meaning | | --- | --- | | `(prop name Type)` | An input the instantiator must supply | | `(prop name Type :default lit)` | An input with a fallback | | `(state name Type :init lit)` | Internal reactive state | | `(output name Type)` | A typed event the component emits | | `(handler name)` | A logic method, implemented in Rust | | `(handler name Type)` | A logic method receiving a payload | The type vocabulary is `i32 i64 f32 f64 bool String`, plus `(List T)` — `(state todos (List String) :init (list "a" "b"))` becomes a `Signal>`. List literals are written `(list …)`. **Records** model structured rows: ```lisp (record Todo :id i64 :label String :done bool) (state todos (List Todo) :init (list (Todo :id 1 :label "hi" :done false))) ``` A record declaration generates a Rust struct of the same name (`pub id: i64`, …). Literals mirror instance syntax and must supply every field. Fields are read with dots — `todo.label` in any expression, `{todo.label}` in interpolations, `:key todo.id` on a `for`. v1 scope, stated plainly: records are file-local and flat (scalar fields only), they live in `(List …)` state and `for` loops, and they do not cross component boundaries — props, outputs, and handler payloads stay scalar (pass a field, e.g. `todo.id`). Handler payloads may now number more than one: `(handler moved i64 i64)`. Prop defaults and state inits must be literals. All declared names share one namespace — and record names may not collide with the component or each other. Prop names may not use the spellings the instance syntax owns: `class`, `key`, anything starting with `on-`, or the item-layout names (`width height grow shrink basis align-self`). Because an `:init` is a literal, state whose value comes from outside the component language — a file, a database — is not declared here at all. It is seeded from the logic's `mounted` method, which the framework calls once the component is on screen; see [rust-api.md](rust-api.md#seeding-state-at-mount). `mounted` is therefore not available as a handler name. ## The widget tree Exactly one root node. A node is `(widget :property value … children…)`. When a component is mounted as the **tree's** root, its root node fills the window whatever size it declares: the root *is* the window, so there is no surrounding box for another size to be a share of. A component mounted as an instance is an ordinary flex item, and sizes itself as one — so `:width` on a root node is not an error, it simply has no one to answer to until the component is nested. Lowercase names are widgets (`container`, `text`, `text-input`, `scroll-area`, `button`, `checkbox` — see [Widgets](widgets.md) — plus any your app defines in Rust and declares in a [manifest](widget-manifests.md)); a **capitalized name instantiates another component** (below). Every node accepts: - **Layout properties** (static): `:width`, `:height` (a number in px, `"N%"`, or `auto`), `:padding`, `:gap` (px), `:direction` (`row`/`column`), `:align-items`, `:justify-content`, `:align-self` (`start`/`end`/`center`/`stretch`; `flex-start`/`flex-end` are accepted synonyms), `:grow`, `:shrink` (numbers), `:basis` (a dimension). Flexbox semantics via taffy. - **`:class "a b"`** — space-separated style classes for [theme](theming.md) rule matching. - **`:enabled expr`** — a boolean expression driving the widget's enabled flag. A disabled widget (and its whole subtree) is inert: it still occludes what is beneath it, but receives no pointer events, takes no `:hover`/`:active` state, and cannot be focused; its enabled container reacts as if the pointer were on its own padding. Themes can target the state with `:disabled` rules. - **`:focused expr`** — a boolean expression driving whether the widget holds keyboard focus. See [Focus](#focus-autofocus-and-focused). - **Event wires**: `:on-click`, `:on-counted-click`, `:on-pointer-enter`, `:on-pointer-leave`, `:on-pointer-down`, `:on-pointer-up`, `:on-file-drop`, `:on-focus-change`, and `:on-key` on any widget, plus widget-specific events like `text-input`'s `:on-change`. The value is a declared handler name — or an **invocation** passing arguments evaluated at dispatch time in the node's scope, loop variables included: `:on-click (remove todo.id)`. Argument types must match the handler's declared payloads. On wires that deliver a value — `:on-change`, `:on-link`, `:on-file-drop`, and instance output wires — the reserved name **`payload`** refers to it inside the arguments, so an editable row can report both what changed and where: ```lisp (for item items :key item.id (text-input :text item.label :on-change (rename item.id payload))) ``` with `(handler rename i64 String)`. A bare name on a payload wire remains shorthand for passing the payload as the only argument. - **Widget properties** (`:text`, `:background`, …) — per-widget; see [Widgets](widgets.md). These are where expressions go. ## Expressions and bindings A widget property's value is an expression. If it is a literal, it is applied once at construction. Anything that reads a prop or state becomes a **binding**: it re-evaluates when any signal it read changes, and updates exactly that widget property. - Literals: `1`, `2.5`, `true`, `"text"`, colors as strings (`"#rgb"`, `"#rrggbb"`, `"#rrggbbaa"`). - Names: any declared prop or state, read reactively. - String interpolation: `"{label}: {count}"` — the one embedded syntax. - Operators, prefix form: `(+ - * / %)`, comparisons `(< <= > >= = !=)`, boolean `(and or not)` (short-circuiting), and `(if cond then else)` as an expression. ```lisp :background (if pressed "#483d8b" (if hovered "#6495ed" "#4682b4")) :text "{label}: {count} of {step}" ``` Current restrictions, enforced with pointed diagnostics: layout properties take literals only (reactive layout is planned alongside the style engine's growth); color-valued expressions are built from color literals and `if` (color-typed props are a planned extension); dotted paths are reserved. ## Events and payloads Plain events wire to plain handlers. Payload-carrying events require the handler to declare the matching payload type, and the compiler checks the contract in both directions: | Payload | Events | | --- | --- | | `String` | `:on-change`, `:on-link`, `:on-file-drop` | | `bool` | `:on-toggle`, `:on-focus-change` | | `f64` | `:on-value-change` | | none | `:on-select`, `:on-close`, `:on-key`, the pointer family | The name says the payload, which is why a slider reports `:on-value-change` rather than `:on-change`: both a slider and a text field "change", and one wire name meaning two payload types is how a checked contract stops being one. ```lisp (handler edited String) (text-input :text current :on-change edited) ``` ```rust fn edited(&mut self, cx: FormCx, value: &str) { … } ``` The payload also composes with dispatch-time arguments through the `payload` keyword, so a row can report both its identity and the new value in one wire: ```lisp (handler set-flag i64 bool) (checkbox :label opt.label :checked opt.on :on-toggle (set-flag opt.id payload)) ``` ## Dynamic structure: `(if …)` and `(for …)` Two structural forms make the tree data-driven: ```lisp (if (> count 0) (text :text "{count} items") (text :class "empty" :text "nothing yet")) ; else-node optional (for todo todos :key todo :index i (container :class "row" (text :text "{i}: {todo}"))) ``` `if` takes a boolean expression and one or two nodes; when the condition flips, the old branch unmounts (its state with it) and the other mounts in the same place among its siblings. `for` iterates a `(List T)` prop or state by name, instantiating its single body node per item. The loop variable reads like any signal; `:index name` optionally binds the row's position. Rows reconcile **by key** — the `:key` expression evaluates over each item (defaults to the position): rows whose key survives a change keep their widgets and state (the loop variable updates in place), new keys mount, vanished keys unmount, and reordered keys move. Give lists a stable `:key` whenever items can move; positional keying rebuilds rows on reorder. Restrictions, checked at compile time: `if`/`for` cannot be the component root; `:autofocus`, `(slot)`, and instance `:key` cannot live inside them (their cardinality is unknowable); the `for` body is exactly one node — wrap siblings in a container. Structural forms nest: an `(if …)` can be a `(for …)`'s body and vice versa, so a list of rows that each swap between two shapes needs no wrapper box to hold it together. `:autofocus` still cannot live inside them; `:focused` is the property that can, and it is how a row-scoped editor gets the caret. ## Focus: `:autofocus` and `:focused` The framework maintains a focus invariant: if any focusable widget exists, one of them has focus. Two properties let a file say which. **`:autofocus true`** picks the widget that takes focus at mount. At most one widget in the *composed* tree (your file plus everything it instantiates) may carry it, and it cannot live inside `if` or `for` — it is an unconditional declaration, so a form that might produce none of it or ten of it has no answer. **`:focused expr`** drives focus from application state, and works anywhere — including inside `if` and `for`, which is what makes a row-scoped editor possible: ```lisp (for row rows :key row.id (container (if (= row.id editing) (text-input :text row.text :focused true :on-key (maybe-commit event.key) :on-focus-change (focus-changed row.id payload)) (text :text row.text :on-counted-click (open row.id event.click-count))))) ``` It is **controlled**, like `:checked` and `:open`: your state owns it, and the widget never flips it. Setting it true takes focus; setting it false gives focus up *only if this widget currently holds it*. That asymmetry is what makes it order-independent — when focus moves between two widgets driven by one piece of state, one binding falls as another rises, and either order lands in the same place. Exclusivity is a fact about your data, exactly as it is for a radio group's `:checked`. **`:on-focus-change`** is the other half. It delivers a `bool` — the new state — and fires for *every* transition, including the ones your state did not cause: a click elsewhere, Tab, a widget being disabled. Answering it is how the state that drives `:focused` stays true instead of drifting out of step with the screen. It is delivered to the widget itself and does not bubble: "am I focused" is not a fact about an ancestor. ## Keys: `:on-key` `:on-key` offers a widget a key the **focused widget declined**. It fires on the focused widget first and then bubbles outward, so a handler can sit on the control or on a container around it: ```lisp (handler pressed String) (text-input :text draft :on-key (pressed event.key)) ``` The order is what makes it safe: the focused widget's own handling always comes first, so a text input's Ctrl+C and every character you type stay the text input's and never reach a wire. Only what it declines — Escape, function keys, unclaimed shortcuts — is offered. After the wires come overlay dismissal (Escape), menu accelerators, and Tab traversal, so a wire on the focused widget is more specific than any of them. A handler claims the key with `cx.consume()`; nothing below sees it: ```rust fn pressed(&mut self, cx: EditorCx, key: &str) { if key == "Escape" { cx.editing.set(0); cx.consume(); } } ``` `event.key` is the keystroke's canonical spelling — the same notation an `:accel` is written in, modifiers first in a fixed order: `"Escape"`, `"S"`, `"Ctrl+S"`, `"Ctrl+Shift+S"`, `"Alt+Left"`. One string means one keystroke wherever it is written. A key the framework does not model has no spelling, and is not offered to a wire at all. ## Component composition A capitalized name in widget position instantiates another component, resolved through the search path: ```lisp (CounterButton :key "fives" :label "fives so far {total}" :step 5 :grow 1 :class "hero" :on-stepped tally) ``` On an instance: - **Props** — every keyword that isn't reserved is a child prop. Required props (no `:default`) must be given; this is checked against the child's declaration at compile time, with the error in *your* file. Prop expressions evaluate in **your** scope and stay live: a binding like `"fives so far {total}"` keeps driving the child's prop as your state changes. - **`:on-X handler`** — wires the child's output `X` to your declared handler; the payload types must match. - **`:class`** — merges into the child's root widget classes, so themes can target this instance. - **Item-layout properties** — `:width :height :grow :shrink :basis :align-self` patch the instance root's style: you control the instance *as a flex item*. Interior layout (`:direction`, `:gap`, `:padding`, `:align-items`, `:justify-content`) belongs to the child's own file and is rejected here. - **`:key "name"`** — explicit identity for hot-reload state matching (see below). A literal, unique per component type within the file. On the Rust side, your component's generated `Logic` trait gains one factory method per child type — `fn counter_button(&mut self) -> impl CounterButtonLogic` — called once per instance. See [the Rust side](rust-api.md#logic-factories). Instantiation cycles are compile errors, reported with the chain (`A → B → A`). ## Slots A component opts into receiving content by placing `(slot)` in its tree — at most one, not at the root, no properties or children of its own: ```lisp (component Card (prop title String) (container :class "card" :direction column :padding 8 :gap 6 (text :class "card-title" :text "{title}") (slot) (text :class "card-footer" :text "fin"))) ``` An instance of a slotted component may then have children, which project in at the marker — order-preserved between the component's own children: ```lisp (Card :title "First" (text :text "note: {note}") (Badge :text "{note}")) ``` Slot content is **your** content rendered in the child's frame: its expressions read your props and state, its events dispatch to your handlers, and components inside it use your logic factories. Instances of slotless components take no children (a compile error says so). ## Hot reload and instance identity In dev mode ([`DevMount`](rust-api.md#dev-mode-hot-reload)) a component file is re-interpreted on save. State survives the reload when it still matches: - Top-level state: by name and type. - State inside component instances: instances match by **identity** — their component type plus `:key`, or, unkeyed, their occurrence order among unkeyed same-type siblings. Give instances a `:key` when you expect to reorder them; keyed state follows its key. A state whose type changed restarts from its `:init`. A save that no longer compiles keeps the previous UI and prints the diagnostics. ## The `event` pseudo-record Inside the arguments of a wire that has an input event behind it, `event` names that event: ```lisp (handler pick i64 i32) (container :on-click (pick todo.id event.click-count)) ``` What it offers depends on the wire, because the two families describe different things — a pointer wire has no keystroke and a key wire has no position. Reading the wrong one is a compile error naming what *is* available there. On **pointer** wires — `:on-click`, `:on-counted-click`, and the `:on-pointer-…` family: | Field | Type | Notes | | --- | --- | --- | | `event.x`, `event.y` | `f64` | in the widget's *own* space | | `event.click-count` | `i32` | 1, 2, 3 … for single, double, triple | Coordinates are local — the frame the wire is written in, and the one that stays meaningful when the widget moves. On **`:on-key`**: | Field | Type | Notes | | --- | --- | --- | | `event.key` | `String` | canonical spelling, e.g. `"Ctrl+Shift+S"` | ## `:on-click` vs `:on-counted-click` `:on-click` fires immediately on release, carrying the running count — the right wire for a button or menu, where a click should act at once. But its `event.click-count` on the first press of a double is `1`: the framework cannot yet know a second press is coming, so an `:on-click` that navigated on `1` would fire before a double could override it. `:on-counted-click` resolves that. It fires **once per click burst**, after a short window (≤200ms from the first press), carrying the *settled* count — so `event.click-count == 1` means a confirmed single, `2` a double. Use it when a single and a double must do different things and the single must not act prematurely: ```lisp (handler act i32) (container :on-counted-click (act event.click-count)) ``` A widget opts a press's path into this accumulate-and-resolve behavior only by handling `:on-counted-click` (or being a paragraph with links, which follow a link on a confirmed single) — a plain `:on-click` is never delayed. ## Consuming an event A handler can stop an event from reaching handlers later in the dispatch — a bubbling ancestor, a deeper capture — by calling `cx.consume()`. An event bubbles from the innermost widget outward, so an inner handler can keep a click from also reaching the container around it: ```lisp (container :on-click (open-card) (container :class "button" :on-click (pin-card))) ``` ```rust fn pin_card(&mut self, cx: CardCx) { // …pin the card… cx.consume(); // the outer card's :on-click does not also fire } ``` Consumption is per-dispatch and opt-in: a handler that never calls `consume()` changes nothing, and off the event path (an instance output, a query) it is a no-op. The controls that own their click already do this for you: a `button` or a `checkbox` consumes its click, so clicking one inside a container with an `:on-click` fires only the control, not the container. A plain container is transparent — a click on a `text` label inside a clickable card still reaches the card. Like [`payload`](#the-payload-keyword), `event` is a **reserved name**, readable only where it exists: `payload` on wires that deliver a value, `event` on wires that have a pointer behind them. Reading it elsewhere, or reading a field it does not have, is a compile error that says so. It is flat, because records are — `event.x`, never `event.pos.x`. It is not a value in its own right: read a field. There is no record type behind it and it never becomes a struct.