-
Notifications
You must be signed in to change notification settings - Fork 9
Guide 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.
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 … */)
)
}
}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.
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.
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.
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)
}
}- Recommended practices — the rules every app converges on.
- Recipe: tree row.
- Recipe: settings form.
- Recipe: search + results.
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.