# UI Composition
`deft/ui/compose` is how interfaces are created in Deft. Components are
constructed with flags and composed with `|>` pipelines into a
widget tree; the nested tagged-map tree is the underlying
representation, not the interface-building API.
from "deft/ui/compose" import div, child, children, props from "deft/ui/components/column" import column from "deft/ui/components/row" import row from "deft/ui/components/label" import label from "deft/ui/components/text-input" import text-input
## Construction
Every component constructor accepts a trailing options map or
`--flags`, validated against the component's `deftype` schema (missing
fields get their defaults):
[label --text "Hello" --fg "#ffffff"] [text-input --id "query" --placeholder "Search…" --flex 1] [column --direction :vertical --bg "#1e1e2e"]
Flags are the preferred construction form. The batch map form is the
same constructor and remains valid — useful when props are computed as a
map, and best avoided inline:
[label %{ text "Hello" :fg "#ffffff" }]
`[div]` is the generic vertical root (an empty `column`).
## Composition
| Helper | Signature | Behaviour |
|---|---|---|
| `child` | `{| w c |}` | Append one child. A vector appends each entry; `nil` is skipped; a scalar wraps in a `label` |
| `children` | `{| w ws |}` | Replace the child list; flattens nested vectors; skips `nil` |
| `props` | `{| w opts |}` | Merge a batch of typed options (trailing map or `--flags`) |
| field setters | `{| w v |}` | One per prop: `id fg bg flex gap padding direction min-width min-height tab-index visible tooltip scroll-y layer z bounds text value checked items selected wrap read-only focusable expanded on-click on-change on-key on-scroll on-focus on-blur` |
| `field` | `{| w k v |}` | Set an arbitrary key — escape hatch for props without a setter |
All helpers take the widget first, so they compose in a pipeline, and all
preserve the widget's tagged type (protocol dispatch is unaffected).
set title [label --text "Results"] |> [props --fg "#cfd8dc"] set toolbar [row --gap 1 --min-height 1 --flex 0] |> [child [text-input --id "query" --flex 1]] |> [child [button --text "Go" --on-click on-go]] |> [props --bg "#263238"] set body [column --direction :vertical --bg "#1e1e2e"] |> [children @{ $title [label --text "…"] }]
## Pipelines Are Statement-Level
`|>` threads at statement/assignment position. Inside a `@{}` literal
(e.g. a `children` list) you cannot start a nested pipeline — bind the
subtree to a local first, then reference it:
# ✅ bind first, compose by reference set header [row --flex 0] |> [child [label --text "Header"]] set panel [column] |> [children @{ $header $viewer }] # ❌ a pipeline nested inside a map literal is not a statement [column] |> [children @{ [row] |> [child ...] }]
## Coercion Rules
`child` / `children` reduce the usual wiring noise:
- `nil` entries are skipped — safe to write `[child (when visible?)]`
style conditionals inline.
- Plain strings and numbers become `label`s, so quick placeholders work:
`[child "Loading…"]`.
- `%MyWidget{…}` values pass through unchanged.
[column]
|> [children @{
$optional-banner # nil → dropped
"Section" # → label
[button --text "Retry"] }]
## Props and Type Checking
`props` uses the shared `UIProps` schema, so known fields are
type-checked at runtime (`--gap "nope"` raises) and defaults are filled
by the component itself. Use `field` only for keys the schema doesn't
know:
[select-list --items $xs --selected 2] |> [props %{ :min-height 6 :follow-selected false }] |> [field :custom-key "value"]
## Under the Hood: The Tree Is a Map
Composition builds a tree of tagged maps (`%UIColumn{}`, `%UILabel{}`, …).
This representation is an implementation detail — app code composes with
the helpers above and treats the tree as opaque — but knowing its shape
explains the framework features you'll meet in
Event Dispatch and
App Platform:
- `tui/set-widget-tree` registers the root; `find-by-id` /
`replace-by-id` locate and patch subtrees by `:id`.
- `preserve-state` carries scroll/selection across rebuilds by matching
structure.
- Data-driven props are still plain data: `tree` nodes are
`%{ :id :label :children … }` maps, `grid` rows are vectors of maps.
- Custom widgets declare their own `deftype … << UIWidget %{ … }` and are
constructed like any other component — see
Widgets and Protocols.
Reach for the raw maps (`assoc`/`merge`/`field`) only when mutating an
existing tree in place; creating an interface is always composition.
## Quick Reference
| Form | Purpose |
|---|---|
| `[label --text "hi"]` | Construct with flags |
| `[div]` | Generic vertical root |
| `\|> [child $w]` | Append a child |
| `\|> [children @{ … }]` | Replace children |
| `\|> [gap 1]` | Set one prop |
| `\|> [props --gap 1 --bg "#000"]` | Batch typed props |
| `\|> [field :k v]` | Arbitrary key escape hatch |
## See Also
- Widgets and Protocols — the component library and protocol model
- Event Dispatch — how the tree receives events
- Graphics — what custom widgets draw with