-
Notifications
You must be signed in to change notification settings - Fork 9
Design Keyed State
State survives re-renders because gpui keys it on a stable ElementId. Every headless factory takes an impl Into<ElementId> as its first argument.
use yororen_ui::headless::button::button;
use yororen_ui::headless::text_input::text_input;
button("submit-btn", cx).caption("Save")
text_input("username").placeholder("Name")Simple string — unique on the page:
button("submit-btn", cx)Tuple — namespaced:
text_input(("settings", "username"))
switch(("settings", "notifications"))Tuple ids create a hierarchical structure ("settings" namespace) that makes debug logs and focus traversal easier to follow.
-
A stable id on the headless factory keeps the component's internal state alive (caret, selection, scroll, IME for text inputs).
// frame N text_input("email").placeholder("you@example.com") // frame N+1 — same id, caret / selection / value restored text_input("email").placeholder("you@example.com")
-
cx.entity().clone()for closures keeps your app state reachable from callbacks:let entity = cx.entity(); text_input("email") .on_change({ let entity = entity.clone(); move |new: &str, _w, cx| { entity.update(cx, |s, _| s.email = new.to_string()); } }) .render(cx, window)
The id keeps the component's internal state alive; the entity clone keeps your app's state reachable. Both are required.
Yororen UI inputs are uncontrolled by default. The component owns caret / selection / IME state internally; on_change is the bridge from the component to your app state. If you need to reset the field, bump the id so a fresh state is minted.
In a virtualised list, the row closure runs inside a row-scoped element namespace — child ids collide freely across rows. Without this isolation, recycled rows would inherit each other's state ("state bleeding"). Two consequences:
- Derive ids from your data model (a UUID, a primary key, a path), not the row index.
- You don't need to manually namespace child ids inside a row closure — the renderer does it for you.
virtual_list("rows", &controller, cx)
.row(move |ix, _w, cx| {
let item = &items[ix];
list_item(("task-list", item.id), &item.title, cx).render(cx).into_any_element()
})- Derive ids from your data model. Index-based ids shift after insert/delete.
-
Tuple ids for logical grouping. Settings row ids under
("settings", …); sidebar under("sidebar", …). -
One entity clone per closure. Each
moveclosure that reaches back into your app state mustlet entity = entity.clone()at construction. -
Don't re-mint composite
Entity<XxxState>. Create once (XxxState::new(&mut *cx)) and reuse across renders; re-minting resets open/closed state every frame.
- 3-layer architecture — id is part of the headless contract.
- Virtualization — namespace isolation per row.
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.