Yororen UI

repository·main·Indexed 43 days ago

https://github.com/meowlynxsea/yororen-ui

A headless-first Rust UI library built on top of gpui-ce 0.3. It utilizes a three-layer architecture separating component logic (headless), visual presentation (renderers), and a JSON-driven theme system. Features include a Notification Center with persistence, an optional XML DSL via yororen-ui-xml, and multiple renderers including a default system theme and a 'Brutalism' style.

Tokens
63.6K
Snippets
196
Records
260
Agent score
53%

What's inside Yororen UI

  1. Notification Center features

    main

    The Notification Center provides a unified system for managing toast-like notifications with the following capabilities:

    • Queue management: Limits the number of active notifications (default max_queue_len = 5).
    • Persistence: Supports sticky notifications that survive state restores.
    • Callbacks: Supports both click and dismiss callbacks.
    • Dismiss strategies: Supports both manual and auto-dismiss behaviors.
  2. How to render headless components

    main

    Yororen UI uses a headless-first approach. Components are defined by their logic and accessibility (headless) and are visually rendered by a registered renderer.

    When building your UI in a Render implementation, you use headless factory functions (e.g., button, label). These factories provide two primary ways to use them:

    • .apply(div): Injects only accessibility (a11y) attributes into an existing element.
    • .render(cx): Renders the full visual component using the currently registered renderer.

    Special Case: Text Inputs Text inputs require a two-parameter call to register the IME (Input Method Editor) processor on the window:

    • .render(cx, window)
    use yororen_ui::headless::button::button;
    use yororen_ui::headless::label::label;
    
    fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        div().size_full().flex().items_center().justify_center().gap_2()
            .child(label("count", "0", cx).render(cx))
            .child(button("inc", cx).caption("+").render(cx))
    }
  3. Choose the correct render pathway for UI components

    main

    Every headless factory provides different ways to render components depending on how much visual control you need:

    1. .render(cx) (Default/Fastest): Uses the registered renderer to paint background, borders, padding, radius, and hover/active states from the theme. Use this for standard UI.
    2. .apply(div) (Caller-controlled visuals): Returns a Stateful<Div> that sets up id, track_focus, and on_click, but provides no visual feedback. Use this when you want to provide your own div() and only want the component to contribute accessibility and click logic.
    3. Hand-rolled (Bespoke): Implement a custom gpui::Element for unique brand identities or animations, then use .apply(...) to retain focus and click wiring.
  4. Implement the Entity<T> state pattern

    main

    Yororen UI uses gpui's Entity<T> for all application state. This allows the render layer to track which entities were read, enabling efficient invalidation via cx.notify().

    Key Rules:

    1. Cloning: Every move closure that touches state needs its own Entity<T> clone (e.g., let inc = state.counter.clone();).
    2. Mutation: Mutate the data inside an update block, then call cx.notify() to trigger a re-render, as gpui-ce 0.3 does not auto-notify on entity updates.
    // Inside a render implementation
    let state = cx.global::<AppState>();
    let count = state.counter.read(cx).value;
    let inc = state.counter.clone(); // Clone for the closure
    
    // In a click handler
    inc.update(cx, |c, cx| {
        c.value += 1;
        cx.notify(); // Required to trigger re-render
    });
  5. Position render overlays at the scroll root

    main

    To prevent modals and toasts from being clipped by scrollable containers and to ensure they receive keyboard events (like Escape) reliably, do not render them inside a scrollable section.

    Instead, build the scroll root and add the modal and toast host as siblings at the top level. Wrap the modal and toast in gpui::deferred(...).with_priority(N) to manage z-index and rendering priority.

    // Correct structure at the scroll root:
    .child(content)
    .child(modal)
    .child(toast_host)
  6. Maintain stable identity for stateful components

    main

    To ensure stateful children (like inputs in a list or virtualized views) maintain their state during reordering or updates, you must provide a unique, stable ID using .id(...).

    • For append-only data: Use the row index as the ID.
    • For mutable data: Use the data's primary key.

    Failure to provide stable IDs will cause components to lose focus or state when the list reorders.

    // Example: Using row index for a virtual list
    text_input(format!("row-email-{ix}"))
    
    // Example: Using a unique row ID for stable scope
    format!("email-{row_id}")
  7. Understand what constitutes the public API surface

    main

    The cargo public-api tool defines the public surface based on the following rules:

    Included in the baseline:

    • Every pub fn, pub struct, pub enum, pub trait, pub type, and pub const re-exported from the crate root.
    • Re-exports (e.g., pub use foo::Bar).

    Excluded from the baseline:

    • Items within #[cfg(test)] modules.
    • Items gated behind non-default feature flags (the tool runs with default features only).
    • Items marked with #[doc(hidden)].
    • Trait implementations (these are considered implicit and are not listed separately).

    Limitations to note:

    • Because --simplified does not surface trait implementations separately, a breaking change where a type no longer implements a specific trait will not be caught by a baseline diff. Use downstream smoke tests for this coverage.
  8. Understand renderer registration constraints

    main
    The system follows a 'one renderer per (Marker, dyn Trait) slot' rule. When you register a new renderer for a specific marker, it overwrites any previously registered renderer for that same marker. There is no automatic layering of renderers. If you require a hybrid visual style, you must implement a single new renderer that handles both styles.
  9. Understand the Yororen UI State vs. App State mental model

    main

    Yororen UI separates interaction state from business state to prevent feedback loops:

    • Component (Interaction State): Managed internally by the component. Includes things like caret position, selection, scroll, blink epoch, IME composition, or whether a Select is currently open or which index is highlighted. This is not in your app state.
    • Your App (Business State): Managed by you. Includes the actual text content, the selected value, validation errors, or form submission status.

    Communication happens via callbacks like on_change, on_pick, on_close, or on_toggle, which you use to write updates back into your Entity<MyState>.

  10. Manage overlay paint priority with `gpui::deferred`

    main

    In gpui, overlays like popovers, modals, and toasts do not automatically float on the z-axis. Using .absolute() only removes an element from the layout flow; the paint order remains determined by DOM order.

    To ensure deterministic paint order (where higher priority elements are painted on top of lower ones), wrap your overlay elements in gpui::deferred(child).with_priority(N). This defers the element's paint until the end of the parent's prepaint, allowing it to sit above other elements regardless of nesting depth.

    // Example of wrapping an element to ensure it paints on top
    let my_overlay = gpui::deferred(overlay_element).with_priority(2);
  11. How stateful composite renderers work

    main

    For complex components like Select, Modal, or Popover, the renderer's compose method receives an Entity<XxxState> inside the XxxProps.

    Responsibilities of a Composite Renderer:

    1. Read State: Use props.state.read(cx) to access properties like .is_open(), .is_visible(), .value(), or .highlighted_index() to determine the visual state.
    2. Handle Interactions: The renderer's render closure must register mouse/keyboard handlers that update the state. For example, a trigger should call state.update(cx, |s, _| s.toggle()), and an option row should call state.update(cx, |s, cx| s.pick(value, w, &mut *cx)).
    // Example signature for a composite renderer
    pub trait SelectRenderer: 'static {
        fn compose(
            &self,
            props: &SelectProps, // contains Entity<SelectState>
            cx: &App,
        ) -> Stateful<Div>;
    }