-
Notifications
You must be signed in to change notification settings - Fork 9
Guide Recommended Practices
The rules every app converges on. Each one corresponds to a real failure mode seen in the demos or in the headless factory contracts.
let inc = state.counter.clone();
let dec = state.counter.clone();
let reset = state.counter.clone();
button("dec", cx).on_click(move |_, _, cx| {
dec.update(cx, |c, cx| { c.value -= 1; cx.notify(); });
}).render(cx).child("-")A single clone cannot be shared across multiple closures — the first one to consume it moves it out.
entity.update(cx, |s, cx| {
s.value += 1;
cx.notify(); // required
});Without cx.notify(), the entity's data changes but the window won't repaint.
For everything owned by the root Entity<MyApp>:
text_input("email")
.placeholder("you@example.com")
.on_change({
let entity = cx.entity();
move |new: &str, _w, cx| {
entity.update(cx, |s, _cx| s.email = new.to_string());
}
})
.render(cx, window)Cloning cx.entity() is cheap (it's an Arc bump). Each move closure that needs it clones once at construction.
Every stateful child in a list, virtualized view, or any tree that can be reordered needs an id derived from your data:
- Append-only data → use the row index.
- Mutable data → use the row's primary key (id, uuid, path).
text_input(format!("row-email-{ix}")).render(cx, window)
list_item(format!("user-row-{}", user.id), &user.name, cx).render(cx)Without stable ids, gpui recycles element state across positions and your input focus / caret / selection get scrambled on every reorder.
A modal that lives inside a scrollable column will be clipped and won't receive Escape reliably:
root
├── scroll root (overflow_y_scroll)
│ └── page content
├── modal (gpui::deferred(...).with_priority(2))
└── toast host (gpui::deferred(...).with_priority(3))
The modal and toast host are siblings of the scroll root. Priority 2 keeps the modal above page content; priority 3 keeps toasts above the modal.
The following belong in event handlers, not in Render::render:
- Spawning tasks (
cx.spawn) - File I/O
- Network calls
- Mutating any global that doesn't go through
entity.update+cx.notify()
The exception: yororen_ui::theme::install(cx, …) is legal in render because it's idempotent and cheap. theme_showcase calls it every frame.
For lists with more than ~50 rows, use virtual_list (variable height) or uniform_virtual_list (fixed height, faster). The demos render 10 000 items without breaking a sweat.
Avoid rendering a single "giant item" that contains a huge subtree if you can split it into rows — virtualization clips what's offscreen, but it can't make a single visible item cheap.
Headless factories take &mut App. Inside Render::render you have &mut Context<MyApp>. Context<T>: DerefMut<Target = App>, so passing cx directly works:
fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
button("save", cx) // cx coerces to &mut App via DerefMut
.on_click(...)
.render(cx) // same here
}When you need to let-bind a state-minting call (e.g. ModalState::new(...)), use &mut **cx explicitly:
let modal_state = ModalState::new(&mut **cx);
let popover_state = PopoverState::new(&mut **cx);This is the v0.3 Context → App pattern. Clippy sees it as a redundant auto-deref; the explicit form is intentional.
| API | Returns | What it does |
|---|---|---|
props.apply(div) |
Stateful<Div> |
Sets id, focus tracking, click handler. No visual feedback. |
props.render(cx) |
Stateful<Div> (or AnyElement for inputs) |
Looks up the registered renderer, calls compose, layers a11y on top. |
Default rule: use .render(cx). It picks up the active renderer and theme.
Reach for .apply(div) only when:
- You need caller-owned visuals with no theme.
- You're writing a custom
gpui::Element(theMaterialRippleElementinlayers_democallsheadless::button(id, cx).on_click(...).apply(...)internally to keep the focus + click wiring).
If you register MyButtonRenderer for the Button marker, the previous TokenButtonRenderer is overwritten. There's no layering. For a hybrid, write a new renderer that does both.
For "Next theme" toolbar buttons, swap the theme (yororen_ui::theme::install(cx, theme)), not the renderer — that's the cheap path.
- Composing UI — concrete patterns.
-
Animation —
AnimatedVisibilityand the composite lifecycle.
Yororen UI v0.3.0 · repository · Apache-2.0 · This wiki documents Yororen UI v0.3.0.
This wiki documents Yororen UI v0.3.0 — the headless-core, swappable-renderer build.