Skip to content

Guide Composing UI

MeowLynxSea edited this page Jun 18, 2026 · 5 revisions

Composing UI

How to combine Yororen UI primitives into common application layouts. Every code sample uses headless factories + .render(cx) so the renderer is free to paint the default, brutalism, or your custom way — without code changes in your app.

1) Toolbar + content

A fixed-height title row at the top, a flex-grown content area below.

use yororen_ui::headless::button::button;
use yororen_ui::headless::heading::{heading, HeadingLevel};

impl Render for MyApp {
    fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let entity_for_save = cx.entity();

        div().size_full().flex().flex_col()
            // toolbar
            .child(
                div().h(px(44.)).px_3().flex().items_center().justify_between()
                    .child(heading("settings-title", HeadingLevel::H2, "Settings", cx).render(cx))
                    .child(button("settings-save", cx)
                        .variant(yororen_ui::ActionVariantKind::Primary)
                        .on_click(move |_, _, cx| entity_for_save.update(cx, |s, _| s.save()))
                        .render(cx).child("Save"))
            )
            // content
            .child(
                div().flex_grow().p_3().id("settings-content").overflow_y_scroll()
                    .child(/* … your scroll body … */)
            )
    }
}

2) List rows (virtual_list + list_item)

The caller owns a VirtualListController; the per-row closure emits whatever element you want for each index.

use yororen_ui::headless::list_item::list_item;
use yororen_ui::headless::virtual_list::{VirtualListController, virtual_list};

pub struct FeedView {
    pub items: Vec<String>,
    pub controller: VirtualListController,
}

impl Render for FeedView {
    fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let controller = self.controller.clone();
        let items = self.items.clone();
        let entity = cx.entity();

        virtual_list("feed", &controller, cx)
            .row(move |ix, _w, cx| {
                let entity_for_row = entity.clone();
                let id = format!("feed-row-{ix}");
                let title = items.get(ix).cloned().unwrap_or_default();
                list_item(id, title, cx)
                    .on_click(move |_, _, cx| entity_for_row.update(cx, |s, _| s.pick(ix)))
                    .render(cx)
                    .into_any_element()
            })
            .render(cx)
    }
}

Use append-only data → key by index. Use mutable data → key by the row's primary key.

3) Modal-pinned layout

The modal lives as a sibling of the scroll root — wrapped in gpui::deferred(...).with_priority(2) so it sits above page content but below toasts.

use gpui::{deferred, div};
use yororen_ui::headless::modal::{modal, ModalState};

pub struct PageWithModal {
    pub modal_state: gpui::Entity<ModalState>,
}

impl Render for PageWithModal {
    fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let modal_state = self.modal_state.clone();

        let scroll_root = div()
            .size_full().p_4().id("page-scroll").overflow_y_scroll()
            .child(/* … page content … */);

        let modal = modal("page-modal", modal_state)
            .child(/* title */)
            .child(/* body */)
            .render(cx);

        div().size_full()
            .child(scroll_root)
            .child(deferred(modal).with_priority(2))
    }
}

ModalState::new(&mut App) mints the entity — store it on your view struct, don't re-mint per frame.

4) Scrollable settings form

The headless form factory owns field values + errors + a submit callback; each form_field is a labelled slot for an input.

use yororen_ui::headless::form::form;
use yororen_ui::headless::form_field::form_field;
use yororen_ui::headless::text_input::text_input;

impl Render for SettingsPage {
    fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let entity = cx.entity();
        let entity_for_form = entity.clone();

        let form_el = form("settings-form", &mut *cx)
            .value("email", self.email.clone())
            .error("email", self.email_error.as_deref())
            .submit("Save")
            .on_submit(move |vals, _w, cx| {
                entity_for_form.update(cx, |s, _| {
                    if let Some(e) = vals.get("email") { s.email = e.to_string(); }
                });
            });

        let submit_btn = form_el.submit_button(&mut *cx).unwrap();

        let email_field = form_field("settings-email", "email", &mut *cx)
            .label("Email")
            .required(true)
            .input(text_input("settings-email-input")
                .placeholder("you@example.com")
                .on_change({
                    let entity = entity.clone();
                    move |new: &str, _, cx| entity.update(cx, |s, _| s.email = new.to_string());
                })
                .render(cx, _w))
            .render(&mut *cx);

        div().size_full().p_4().flex().flex_col().gap_3()
            .id("settings-scroll").overflow_y_scroll()
            .child(email_field)
            // … more form_fields …
            .child(div().pt_2().flex().justify_end().child(submit_btn))
    }
}

form(id, cx) and form_field(id, name, cx) need &mut App. Inline &mut *cx at each call site — never store in a let.

5) Popover with a menu body

use yororen_ui::headless::button::button;
use yororen_ui::headless::dropdown_menu::{
    DropdownItem, DropdownMenuItem, DropdownMenuState, dropdown_menu,
};
use yororen_ui::headless::popover::{popover, PopoverState};

impl Render for UserMenu {
    fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let popover_state = self.popover_state.clone();
        let menu_state = self.menu_state.clone();
        let popover_for_btn = popover_state.clone();
        let popover_for_close = popover_state.clone();

        menu_state.update(cx, |s, _| {
            s.set_items(vec![
                DropdownItem::Item(DropdownMenuItem::new("profile", "Profile")),
                DropdownItem::Item(DropdownMenuItem::new("settings", "Settings")),
                DropdownItem::Separator,
                DropdownItem::Item(DropdownMenuItem::new("logout", "Logout")),
            ]);
            s.set_on_select(move |id, _w, cx| {
                match id.as_str() {
                    "profile"  => { /* … */ }
                    "settings" => { /* … */ }
                    "logout"   => { /* … */ }
                    _ => {}
                }
                popover_for_close.update(cx, |s, _| s.close());
            });
        });

        let trigger = button("user-menu-btn", cx)
            .on_click(move |_, _, cx| popover_for_btn.update(cx, |s, _| s.toggle()))
            .render(cx).child("User ▾");

        let content = dropdown_menu("user-menu", menu_state).render(cx);

        popover("user-menu", popover_state)
            .trigger(trigger.into_any_element())
            .content(content)
            .render(cx)
    }
}

See also

Clone this wiki locally