Skip to content

Design Logic

MeowLynxSea edited this page Jun 18, 2026 · 4 revisions

Design logic

The big mental shifts in v0.3.

Components vs widgets

  • Component — a primitive or composite that produces a single element. Returns a XxxProps builder from yororen-ui-core::headless::*. Stateless (pure data) or composite (owns an Entity<XxxState>).
  • Widget — orchestrates many elements: scrolling, virtualization, or wires multiple components together. Currently VirtualList is the only shipped widget.

State in Entity<T>, not Arc<Mutex<T>>

gpui-ce tracks which entities a render closure reads. Mutating an entity + cx.notify() invalidates the window automatically.

Headless vs renderer

Every UI primitive is split into two crates:

You want Use
Default rounded look headless::button::button(...).render(cx) after renderer::install(cx, ...)
Brutalism look Same call, after brutalism_renderer::install(cx)
Full visual control headless::button::button(...).apply(div().bg(...))
Brand-new animation / look Implement a custom XxxRenderer, register via cx.register_renderer_arc::<m::Button, dyn ButtonRenderer>(...)

The three render pathways

factory(id, cx) → XxxProps
   │
   ├── .apply(div)         → Stateful<Div>     (a11y only)
   ├── .render(cx)         → Stateful<Div>     (a11y + themed visual)
   └── .render(cx, window) → AnyElement        (text inputs only — IME handler)
  • 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"). The headless layer only contributes the focus handle and click handler.
  • Bespoke animation / brand identity: hand-roll a gpui::Element and call props.apply(...) for the a11y side. The MaterialRippleElement in crates/yororen-ui-demos/layers_demo/src/material_button.rs is the canonical example.

The factory always returns the same XxxProps — the choice is which terminal method you call.

Worked example — headless button

use yororen_ui::headless::button::button;

// Path 1: default look, painted by the registered renderer.
let themed = button("save", cx).caption("Save").on_click(|_, _, _| { /* … */ }).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(|_, _, _| { /* … */ }))
    .child("Delete");

Same factory. Same state. Same a11y. Different pixels.

See also

Clone this wiki locally