-
Notifications
You must be signed in to change notification settings - Fork 9
Design Logic
Yororen UI splits UI building blocks into two layers:
- Components — primitives and composites that produce a single element on screen. They should be composable and predictable.
- Widgets — containers / behaviors that orchestrate many elements: scrolling, virtualization control, higher-level layout.
This separation keeps Components small and reusable, while Widgets
can focus on behavior and performance. In v0.3 the line is drawn
mostly by what owns state: every Component factory in
yororen-ui-core/src/headless/ returns a *Props builder; every
Widget either owns the data layer (e.g. virtual_list owns a
VirtualListController) or wraps gpui::Div directly.
-
Make the fast path the default.
.render(cx)always paints a styledDivfrom the theme. No work for the caller. - Use virtualization for long scrolling content. See Design-Virtualization.
-
Ensure stable identity (
.id(...)) for stateful elements, especially under virtualization. See Design-Keyed-State. -
State goes in
Entity<T>, notArc<Mutex<T>>. gpui-ce tracks which entities a render closure reads;cx.notify()on the entity invalidates the window. See Composing UI.
The single biggest v0.3 mental shift: every UI primitive is split
into two crates — a headless data + state machine layer
(yororen-ui-core) and a swappable visual layer
(yororen-ui-default-renderer, yororen-ui-brutalism-renderer).
See Design-Three-Layer-Architecture
for the full picture.
The practical consequence for app code:
| You want | Use | Crate |
|---|---|---|
| Default rounded look | headless::button::button(...).render(cx) |
yororen-ui-core + yororen-ui-default-renderer
|
| Brutalism look (sharp corners, hard shadows) | same call, after brutalism_renderer::install(cx)
|
yororen-ui-core + yororen-ui-brutalism-renderer
|
| Full visual control | headless::button::button(...).apply(div().bg(...)) |
yororen-ui-core only |
| A brand-new animation / look | write a custom XxxRenderer, register via cx.register_renderer_arc::<m::Button, dyn ButtonRenderer>(...)
|
both |
The headless factory is the public contract. The renderer is a plug-in. A button is "a focusable, clickable thing with a label and an optional icon" — the factory says so; the visual is whatever the registered renderer chooses.
Every headless factory exposes both .apply(div) and
.render(cx) — they do very different things. A third pathway lets
you hand-roll a custom element.
factory(id, cx) → XxxProps
│
├── .apply(div) ─────────── Stateful<Div> (a11y only)
├── .render(cx) ─────────── Stateful<Div> (a11y + themed visual)
└── .render(cx, window) ─── AnyElement (text inputs only)
| API | What it returns | What it does |
|---|---|---|
props.apply(div) |
Stateful<Div> |
Sets id, track_focus, on_click. No visual feedback.
|
props.render(cx) |
Stateful<Div> (or AnyElement for inputs) |
Looks up the registered renderer, calls compose, then wires the same a11y callbacks on top. |
| (Custom) | anything you build | You write the painter yourself (see material_button in layers_demo) |
-
Default look, fastest path:
.render(cx). The renderer paints bg / border / padding / radius / hover / active from the theme. -
Caller controls every visual:
.apply(div())...child("Save"). You write thediv(); the renderer only contributes the focus ring and click handler. -
Bespoke animation / brand identity: hand-roll a
gpui::Element(theMaterialRippleElementincrates/yororen-ui-demos/layers_demo/src/material_button.rsis the canonical example). You still callprops.apply(...)to keep the focus + click wiring.
The layers_demo puts all three in one window — read it to see them
side by side.
The full three-layer story for a single button, in one file:
use gpui::{div, App, Context, IntoElement, ParentElement, Render, Window};
use yororen_ui::headless::button::button;
pub struct MyApp;
impl Render for MyApp {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
// Path 1 — default look, fully painted by the renderer.
let themed = button("save", cx)
.caption("Save")
.on_click(|_, _, cx| { /* ... */ })
.render(cx);
// Path 2 — caller-controlled visual, only a11y from the headless layer.
let custom = div()
.bg(gpui::red())
.rounded(px(8.))
.p_2()
.apply(button("danger", cx).on_click(|_, _, cx| { /* ... */ }))
.child("Delete");
div().flex().flex_col().gap_2().child(themed).child(custom)
}
}Both calls go through yororen_ui::headless::button::button(...)
(the factory). Path 1 delegates the visual to the registered
ButtonRenderer (whatever was installed at boot). Path 2 keeps the
visual with the caller; the headless layer only contributes the
focus handle, click handler, and id.
The factory always returns a ButtonProps — there is no "themed
button" vs "headless button" split in the type system. The choice is
which terminal method you call: .apply(...) vs .render(cx).
- Design-Three-Layer-Architecture — the canonical mental model.
- Layout rules — what the headless layer doesn't decide.
- Keyed state — stable ids and virtualization.
- Virtualization — closure-driven headless variant.
- Accessibility — the a11y modules.
- Internationalization — locale + RTL.
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.