Guides
Build an Omarchy application from a first window to forms, menus and a docked workspace. These guides share one page so you can read through, jump to a chapter, or search for a component with your browser’s Find command.
The examples target gpui-omarchy 0.1.0 with GPUI Kit 0.6.0. They cover the public API defined by gpui-omarchy, including its modules, constructors, wrapper methods, state types and theme fields. GPUI Kit supplies the underlying framework and base builder APIs; their entire dependency APIs are not duplicated here.
Start an application
Add the library to a Rust 2024 application:
[dependencies]
gpui-kit = { version = "=0.6.0", default-features = false }
gpui-omarchy = "0.1.0"
Use gpui_kit directly for framework types and gpui_kit::application() for the desktop entry point. gpui_kit::base provides the underlying state and composition APIs. Disabling default features leaves the GPUI Kit component facade out, so gpui-omarchy supplies the Omarchy presentation. Call gpui_omarchy::init(cx) to initialize base behavior and the Omarchy theme.
Save this complete example as src/main.rs, then run cargo run. Install your platform’s GPUI build dependencies first.
use gpui_kit::{
AppContext, Context, IntoElement, ParentElement, Render, Styled, Window, WindowOptions,
};
use gpui_omarchy::{ActiveTheme, ButtonVariant, button, focus_scope, panel};
struct Hello {
clicks: usize,
}
impl Render for Hello {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
focus_scope("hello")
.size_full()
.bg(cx.omarchy().background)
.child(
panel("Welcome to Omarchy", cx).child(
button(
"hello",
format!("Clicked {} times", self.clicks),
ButtonVariant::Primary,
cx,
)
.on_click(cx.listener(|this, _, _, cx| {
this.clicks += 1;
cx.notify();
})),
),
)
}
}
fn main() {
gpui_kit::application().run(|cx| {
gpui_omarchy::init(cx);
cx.open_window(WindowOptions::default(), |_, cx| {
cx.new(|_| Hello { clicks: 0 })
})
.expect("open window");
cx.activate(true);
});
}
Call init(cx: &mut gpui_kit::App) once before creating components. It initializes gpui-base, installs the focus, option-group, popover and date-picker keybindings, and starts following the system theme. Reading cx.omarchy() before initialization has no installed theme to read.
Use base state and composition types through gpui_kit::base, for example gpui_kit::base::input::InputState. No separate gpui-base dependency is needed. GPUI Kit also exposes bundled assets through gpui_kit::assets when its assets feature is enabled.
Except for the complete applications explicitly labeled below, Rust examples are fragments for a view’s constructor or Render::render, as indicated. They assume the same framework imports, plus gpui_omarchy::* and gpui_kit::prelude::*. Fields such as self.name are entities retained by the application, not recreated on every render. Do not also import gpui_kit::*: names such as MenuItem would become ambiguous. Import base state from gpui_kit::base; keep Omarchy constructors under gpui_omarchy.
Compose a view and manage state
Build components inside Render. Their appearance is taken from the current theme on that render, so the next render picks up theme changes. Most functions return a gpui-base element; use its normal .child(...), .children(...), layout, event and style builders.
// Inside Render::render.
panel("Workspace", cx)
.child(badge("Connected", Status::Success, cx))
.child(separator(cx))
.child(button("save", "Save", ButtonVariant::Primary, cx))
There are two common ways to hold state:
- Values owned by your view: checkbox, switch, radio, toggle, tab and expandable-region values are passed on each render. Their callbacks must update your value and call
cx.notify(). - Entities owned by your view: inputs, choices, dates, sliders, trees and navigation retain editing or interaction state across renders. Create each
Entity<T>once withcx.new(...). Observe it when the parent needs to redraw, and subscribe to its events when your application needs a semantic action.
For example, in your view constructor:
let name = cx.new(|cx| {
gpui_kit::base::input::InputState::new(window, cx).placeholder("Workspace name")
});
cx.observe(&name, |_, _, cx| cx.notify()).detach();
// Store name in your view; render input("name", &self.name, window, cx).
Retain a subscription in your view or deliberately .detach() it so it remains registered for the entity’s lifetime. Recreating state during rendering resets editing and selection. A constructor reading a value does not make that value persistent for you.
Use stable component IDs, unique within the same parent. For repeated rows use tuples such as ("row", index). Constructors generally accept id: impl Into<ElementId> and labels as impl Into<SharedString>; owned strings work for dynamic labels.
Enable keyboard traversal
Wrap the window or form in focus_scope(id) -> Stateful<Div>. It enables Tab and Shift+Tab traversal while preserving text-field and popup keyboard contexts. A focus scope does not create your layout; add normal GPUI layout builders.
The public unit actions gpui_omarchy::focus::Next and gpui_omarchy::focus::Previous move focus forward and backward. init binds them to Tab and Shift+Tab in the OmarchyFocusScope context. You can dispatch them with window.dispatch_action(Box::new(gpui_omarchy::focus::Next), cx) or register additional GPUI keybindings for either action. They are available through the focus module, not re-exported at the crate root. The scope also handles copying selected rich text with the platform’s copy shortcut, letting an editor handle it when no rich-text selection is active.
Keep stable FocusHandles for modal content and the trigger that opens it. Focus the modal handle when opening, and restore the trigger on closing. Icon-only actions need an accessible name even when they have a tooltip.
Run a complete stateful form
This is a second complete desktop application. Use the two dependencies from the setup chapter and replace src/main.rs with the code below. It shows all fields, imports and initialization for an input, a controlled switch, a choice entity and a save callback. Saving updates the on-screen message in memory; no files are written.
use gpui_kit::{
AppContext, Context, Entity, IntoElement, ParentElement, Render,
SharedString, Styled, Window, WindowOptions,
};
use gpui_omarchy::{
ActiveTheme, ButtonVariant, ChoiceItem, ChoiceState,
button, focus_scope, input, panel, select, switch,
};
use gpui_kit::base::input::InputState;
struct Settings {
name: Entity<InputState>,
workspace: Entity<ChoiceState>,
notifications: bool,
saved: SharedString,
}
impl Settings {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let name = cx.new(|cx| InputState::new(window, cx).default_value("My workspace"));
let workspace = cx.new(|cx| {
ChoiceState::new(
vec![ChoiceItem::new("personal", "Personal"), ChoiceItem::new("team", "Team")],
window,
cx,
)
.label("Workspace type")
.default_selected(0)
});
cx.observe(&name, |_, _, cx| cx.notify()).detach();
cx.observe(&workspace, |_, _, cx| cx.notify()).detach();
Self { name, workspace, notifications: false, saved: "No changes saved".into() }
}
}
impl Render for Settings {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let changed = cx.listener(|this, enabled: &bool, _, cx| {
this.notifications = *enabled;
cx.notify();
});
focus_scope("settings")
.size_full()
.bg(cx.omarchy().background)
.child(panel("Workspace settings", cx)
.child("Name")
.child(input("name", &self.name, window, cx))
.child("Workspace type")
.child(select("workspace", &self.workspace, window, cx))
.child(switch("notifications", "Notifications", self.notifications, cx)
.on_change(move |enabled, _, window, cx| changed(&enabled, window, cx)))
.child(button("save", "Save", ButtonVariant::Primary, cx)
.on_click(cx.listener(|this, _, _, cx| {
let kind = this.workspace.read(cx).selected()
.map(|item| item.value.clone()).unwrap_or_else(|| "none".into());
this.saved = format!("Saved {} ({kind})", this.name.read(cx).value()).into();
cx.notify();
})))
.child(self.saved.clone()))
}
}
fn main() {
gpui_kit::application().run(|cx| {
gpui_omarchy::init(cx);
cx.open_window(WindowOptions::default(), |window, cx| cx.new(|cx| Settings::new(window, cx)))
.expect("open settings window");
cx.activate(true);
});
}
The parent retains editing state and observes it for redraws. The switch callback receives a value and updates a field; the select updates its own entity. The save callback reads both entities through the application context. These are the same patterns used by the fragments in the following chapters.
Follow and customize the theme
Most applications can let init follow the desktop theme. On native targets, gpui-omarchy reads:
$HOME/.local/state/omarchy/current/theme/colors.toml
$HOME/.local/state/omarchy/current/theme.name
It checks for changes once per second off the UI thread, including replaced theme symlinks. The legacy $HOME/.config/omarchy/current location is used only when the current state directory is absent. An existing but invalid current theme falls back to Tokyo Night rather than reviving a stale legacy theme.
For an application theme switcher, apply a palette explicitly:
// After gpui_omarchy::init(cx).
Theme::flexoki_light().apply(cx);
// Later, resume following the desktop:
Theme::follow_system(cx);
apply(self, cx: &mut App) consumes the palette, stops the system watcher, updates both Omarchy and base tokens, and refreshes all windows. It also installs square radius tokens and the library’s typography scale. Change a cloned palette, then apply the whole theme, so the two token sets stay in sync.
let mut theme = cx.omarchy().clone();
theme.font = ".SystemUIFont".into();
theme.apply(cx);
Import ActiveTheme to use omarchy(&self) -> &Theme on App and through contexts that dereference to it. The Theme type implements GPUI Global and supports Clone, Debug and PartialEq.
Load an explicit palette
| API | When to use it |
|---|---|
Theme::tokyo_night() -> Theme |
Construct the default dark palette without applying it. |
Theme::flexoki_light() -> Theme |
Construct the built-in warm light palette. |
Theme::system_or_default() -> Theme |
Read a one-time system snapshot, with atomic Tokyo Night fallback. This does not start watching. |
Theme::follow_system(&mut App) |
Apply the system palette and follow later changes. In browsers, apply Tokyo Night without filesystem monitoring. |
Theme::from_current_dir(path: impl AsRef<Path>) -> Result<Theme, ThemeLoadError> |
Read theme/colors.toml under a supplied current directory, with optional theme.name. |
Theme::from_colors_toml(name: &str, contents: &str) -> Result<Theme, ThemeLoadError> |
Parse palette text already loaded by your application. |
The parser accepts ANSI color0..15 and semantic Omarchy colors. Required keys are background, foreground, accent, and the status colors red/color1, yellow/color3, green/color2. Optional surfaces derive from that same palette. Color strings use #RRGGBB. mode, when provided, must be dark or light; otherwise appearance is inferred from background and foreground luminance. Explicit loading returns ThemeLoadError on malformed or incomplete palettes; it does not silently apply a partial theme. The error implements Debug, Display and std::error::Error, with a private message accessible through to_string().
shell.toml size and state overrides are not implemented. The library’s default font is .SystemUIFont; a browser application needs its own available fonts and bootstrap, as in the WASM gallery.
Choose colors by their purpose
All these Theme fields are public:
| Fields | Type and role |
|---|---|
name, font |
SharedString: display name and application font family. |
appearance |
gpui_kit::base::ThemeAppearance: light or dark. |
background, surface, inset |
Hsla: main canvas, raised content and inset surface. |
foreground, secondary, bright |
Hsla: normal, secondary and emphasized text. |
accent, on_accent |
Hsla: accent and its contrasting foreground. |
selection, border |
Hsla: palette selection and general surface edges. |
danger, warning, success |
Hsla: semantic status colors. |
For custom controls, use the theme’s control-state helpers instead of treating the palette’s selection as every selected fill:
Method on &Theme |
Return value |
|---|---|
normal_fill() |
Hsla, foreground at 4% opacity. |
hover_fill() |
Hsla, foreground at 8%. |
selected_fill() |
Hsla, foreground at 18%. |
pressed_fill() |
Hsla, foreground at 22%. |
control_border() |
Hsla, foreground at 40%. |
focus_border() |
Hsla, foreground at 25%. |
divider() |
Hsla, foreground at 12%. |
input_style() |
gpui_kit::base::input::InputEditorStyle, including editor selection at 35% foreground opacity. |
tokens() |
gpui_kit::base::ColorTokens, the palette mapping used when applying the theme. |
Add actions and links
Use button(id, label, variant, cx) -> Button for application commands. ButtonVariant offers Primary, Outline, Secondary (the default) and Danger. Native primary actions emphasize the label and outline; they are not the website’s filled primary buttons. Outline has a visible idle edge, while Secondary is quiet at rest.
button("save", "Save changes", ButtonVariant::Primary, cx)
.disabled(self.saving)
.on_click(cx.listener(|this, _, _, cx| {
this.saving = true;
cx.notify();
}))
Button is an Omarchy wrapper over base activation and focus behavior. Its final disabled state suppresses hover, active and focus-visible refinements, whether .disabled(true) comes before or after them. Ordinary styling and selected/disabled styles remain composable.
Refine a button
Button method |
Use |
|---|---|
new(id) |
Construct a raw wrapper without the themed constructor’s label or styling. |
disabled(bool), selected(bool) |
Set disabled interaction or persistent selection. Both return Self. |
styles(FnOnce(ButtonStyles) -> ButtonStyles) |
Configure base selected and disabled style slots. ButtonStyles comes from gpui-base. |
accessibility_label(label) |
Set an explicit accessible name. |
role(impl Into<gpui_kit::base::RoleOverride>) |
Override the base accessibility role. |
track_focus(&FocusHandle) |
Associate a stable focus handle. |
tab_index(isize), tab_stop(bool), focusable(bool) |
Refine tab order and focus participation. |
on_click(Fn(&ClickEvent, &mut Window, &mut App) + 'static) |
Handle activation; use cx.listener to update your view. |
It implements Styled, ParentElement, InteractiveElement, StatefulInteractiveElement, RenderOnce, IntoElement and gpui_kit::base::Selectable. Import the appropriate traits for .child, .hover, .focus_visible, .active and layout builders. Selectable::is_selected(&self) -> bool reads the selected state.
Open a destination
Use link(id, label, href, cx) -> Link when activation navigates to a URL:
link("project", "Project source", "https://github.com/huacnlee/gpui-omarchy", cx)
The constructor installs an opener using cx.open_url. The Link wrapper suppresses transient disabled styles just like Button.
Link method |
Use |
|---|---|
new(id) |
Raw wrapper; supply your own content, URL and opener. |
href(impl Into<SharedString>) |
Set the destination. |
open_with(Fn(&str, &ClickEvent, &mut Window, &mut App) + 'static) |
Replace URL opening, for example with an application router. |
on_activate(Fn(&ClickEvent, &mut Window, &mut App) + 'static) |
Handle link activation. |
disabled(bool) |
Disable activation and transient styling. |
styles(FnOnce(LinkStyles) -> LinkStyles) |
Configure base style slots. LinkStyles comes from gpui-base. |
accessibility_label(label), tab_index(isize), tab_stop(bool) |
Name and configure keyboard participation. |
These builders return Self. Link supports the same GPUI styling, children, interaction and rendering traits as Button, but does not expose Selectable or the button-only inherent methods.
Name icon actions
with_tooltip(
button("settings", "", ButtonVariant::Secondary, cx)
.accessibility_label("Open settings")
.child(icon(IconName::Settings)),
"Open settings",
)
icon(name: IconName) -> icon::Icon returns a 1rem, non-shrinking icon; use Styled to change size or text color. Icon implements RenderOnce and IntoElement. Icons inherit text color at render time and embed their assets, so you do not need to replace your application’s AssetSource.
IconName has these variants: Check, Minus, Plus, ChevronDown, ChevronRight, ChevronLeft, Calendar, Star, ExternalLink, Close, Search, Menu, Settings and TriangleAlert. IconName::path(self) -> &'static str returns the bundled icons/*.svg asset key, not a URL to fetch. IconName is re-exported at the crate root; the concrete icon type is gpui_omarchy::icon::Icon.
Build forms
Choose a control by the value the user edits. Keep labels visible and put validation feedback next to the field. Input editing, selection, clipboard and IME behavior come from gpui-base.
Edit text and numbers
Create states once in your view constructor:
let name = cx.new(|cx| {
gpui_kit::base::input::InputState::new(window, cx).placeholder("Workspace name")
});
let notes = cx.new(|cx| {
gpui_kit::base::input::TextareaState::new(window, cx)
.rows(4)
.placeholder("Add notes")
});
let copies = cx.new(|cx| {
gpui_kit::base::input::InputState::new(window, cx).default_value("1")
});
Store them in your view, then compose a form during rendering:
panel("Workspace settings", cx)
.child("Name")
.child(input("name", &self.name, window, cx))
.child("Notes")
.child(textarea("notes", &self.notes, window, cx))
.child("Copies")
.child(number_input(&self.copies, cx))
| Constructor | State, context and result |
|---|---|
input(id, &state, window, cx) |
Entity<gpui_kit::base::input::InputState>, &Window, &mut App; returns gpui_omarchy::Input. |
textarea(id, &state, window, cx) |
Entity<gpui_kit::base::input::TextareaState>, &Window, &mut App; returns gpui_kit::base::InputBase. |
number_input(&state, cx) |
Entity<InputState>, &mut App; returns gpui_kit::base::NumberInput. |
Configure values, placeholders and editor behavior on the state. Constructors refresh the state’s editor style and focus styling. Single-line Input supports .prefix(element) and .suffix(element) for text, icons or custom content inside the shared border. Both slots are empty by default and remain outside the editable value. The editor takes the remaining width. For example, input("price", &self.price, window, cx).prefix("$").suffix("USD"). Textarea returns the base styled frame. Number input centers the text and adds decrement/increment controls with base arrow-key stepping; configure numeric constraints through the base number-input API. Input validation and saving remain application responsibilities.
Toggle values
checkbox(id, label, state, cx) takes gpui_kit::base::CheckboxState::{Unchecked, Checked, Indeterminate} and returns gpui_kit::base::Checkbox. switch(id, label, checked, cx) and radio(id, label, checked, cx) take bool and return base Switch and Radio. toggle(id, label, pressed, cx) takes bool and returns base Toggle. All four take cx: &App.
A base on_change receives (next_value, &ClickEvent, &mut Window, &mut App). Bridge that value into a view listener:
let changed = cx.listener(|this, checked: &bool, _, cx| {
this.notifications = *checked;
cx.notify();
});
switch("notifications", "Notifications", self.notifications, cx)
.on_change(move |checked, _, window, cx| changed(&checked, window, cx))
Pass the current value into the constructor again on each render. Do not override .checked, .state or .pressed afterward: the indicator was composed from the constructor’s value and can become visually inconsistent. Disabled state uses the returned base control’s .disabled(true).
A radio does not coordinate its siblings for you. Use the next chapter’s button_group for an exclusive setting. For independent filters, compose toggles in toggle_group(id, cx) -> gpui_kit::base::ToggleGroup:
toggle_group("filters", cx)
.child(toggle("unread", "Unread", self.unread, cx))
.child(toggle("starred", "Starred", self.starred, cx))
// Attach an on_change callback to each child to update its own value.
Each toggle keeps its own tab stop and value. Icons are explicit children, not inferred from the label.
Enter a verification code
Create cx.new(|cx| gpui_kit::base::OtpState::new(6, window, cx)) once, then render:
otp_input(&self.code, window, cx).disabled(self.submitting)
otp_input(&Entity<gpui_kit::base::OtpState>, &Window, &App) -> OtpInput supplies the code slots and clipboard behavior. OtpInput::disabled(self, bool) -> Self disables interaction. The wrapper implements Styled, ParentElement, RenderOnce and IntoElement for layout, children and rendering; it does not expose all base OTP builders.
Paste replaces the code, filters non-digits, normalizes full-width digits and truncates to the configured length. Disabled controls ignore edits and paste. Observe the base state for code changes; your application decides when to submit and how to report a rejected code.
Choose options and switch pages
Use an inline group for a few visible choices, a Select for a longer list, or a Combobox when users need to search the list. Choices have stable values independent of their display labels.
Hold a selected option
Create a separate ChoiceState entity for each Select or Combobox in your view constructor:
let workspace = cx.new(|cx| {
ChoiceState::new(
vec![
ChoiceItem::new("personal", "Personal workspace"),
ChoiceItem::new("team", "Team workspace"),
ChoiceItem::new("archive", "Archived workspace").disabled(true),
],
window,
cx,
)
.label("Default workspace")
.placeholder("Choose a workspace…")
.default_selected(0)
});
cx.observe(&workspace, |_, _, cx| cx.notify()).detach();
During rendering use select("workspace", &self.workspace, window, cx) or combobox("workspace-search", &self.workspace, window, cx). Both accept &Entity<ChoiceState>, &mut Window and &mut App; they return base Select and Combobox respectively. Use one constructor per entity/control: their internal search and focus state is not intended to be shared by two simultaneous controls.
Read the result using self.workspace.read(cx).selected(), which returns Option<&ChoiceItem>. Persist the item’s value, rather than its label or position. Combobox performs case-insensitive substring searches of option labels; it selects existing options and does not create a free-text value.
Arrow Up/Down and Home/End navigate options. Enter confirms; Escape dismisses and returns focus. Disabled options are skipped. The searchable popup owns its text input so editing keys do not activate unrelated controls.
| Public choice API | Behavior |
|---|---|
ChoiceItem::new(value, label) |
Both arguments convert into SharedString; the item starts enabled. |
ChoiceItem::disabled(self, bool) -> Self |
Configure availability. |
ChoiceItem::{value, label, disabled} |
Public fields of types SharedString, SharedString, bool; items support Clone and Debug. |
ChoiceState::new(Vec<ChoiceItem>, &mut Window, &mut Context<Self>) -> Self |
Create choice and search state with no selected item. |
ChoiceState::label(self, label) -> Self |
Accessible control name. |
ChoiceState::placeholder(self, label) -> Self |
Text shown before selection. |
ChoiceState::disabled(self, bool) -> Self |
Disable the control at state construction. |
ChoiceState::default_selected(self, usize) -> Self |
Set an initial index; invalid or disabled entries leave no selection. |
ChoiceState::selected(&self) -> Option<&ChoiceItem> |
Read the committed option. |
ChoiceState::is_open(&self) -> bool |
Read whether its popup is open. |
ChoiceState::set_selected(&mut self, Option<usize>, &mut Context<Self>) |
Update inside Entity::update and notify observers. None, out-of-range and disabled indexes clear selection. |
Show an exclusive setting or a tab list
Both constructors take (id, items: Vec<ChoiceItem>, selected: Option<usize>, on_change, window: &mut Window, cx: &mut App):
button_group(...) -> gpui_kit::base::RadioGrouprepresents one exclusive setting.tab_list(...) -> gpui_kit::base::Tabsrepresents page navigation; render the corresponding content yourself.
Their on_change callback is Fn(usize, &mut Window, &mut App) + 'static. Keep the selected index in your view:
let changed = cx.listener(|this, index: &usize, _, cx| {
this.page = *index;
cx.notify();
});
tab_list(
"workspace-pages",
vec![ChoiceItem::new("files", "Files"), ChoiceItem::new("activity", "Activity")],
Some(self.page),
false, // show_shortcuts: optional one-based number hints
move |index, window, cx| changed(&index, window, cx),
window,
cx,
)
Each group has one Tab stop. Left/Right or h/l moves the keyboard cursor; Enter or Space commits. Moving the cursor alone does not switch the selected value. Disabled choices are skipped, and activating the current selection does not emit a change.
Tabs use a square segmented strip. Set show_shortcuts to true to render automatic 1-based number hints; bind the corresponding shortcuts in your application. button_group uses equal-width segments filling its container for form choices such as Limit / Market.
For custom compositions, use tabs(id, cx) -> gpui_kit::base::Tabs and tab(id, label, selected: bool, cx) -> gpui_kit::base::Tab. Both take &App. You own selected state, callbacks and composition; the low-level pair does not add tab_list’s option-group cursor handling.
Edit dates, ranges and colors
These controls keep richer state in an entity. Observe that state to redraw dependent labels or previews. Subscribe to its base events when you need to distinguish editing from committing.
Pick a date
For an always-visible calendar, create a base state once:
let calendar = cx.new(|cx| gpui_kit::base::CalendarState::new(window, cx));
Render calendar("schedule", &self.calendar, cx) -> gpui_kit::base::Calendar; it takes &Entity<gpui_kit::base::CalendarState> and &App. Base state configures single-date or range selection, month/year navigation and disabled-date matchers.
For a compact trigger and popup, create cx.new(|cx| DatePickerState::new(window, cx)), then render date_picker("due-date", &self.due_date, cx) -> gpui_kit::base::DatePicker.
DatePickerState::new(&mut Window, &mut Context<Self>) -> Self installs the calendar subscription and owns popup focus. Its public calendar: Entity<gpui_kit::base::CalendarState> is where you configure date rules and observe CalendarEvent::Selected. is_open(&self) -> bool reads popup visibility; there is no public open-state setter.
A completed selection closes the popup and returns focus to the trigger. A range remains open until the selection is complete. Escape and outside dismissal close it; implement your own draft/commit model if you need rollback of calendar edits. The returned picker supports the base .disabled(true) builder.
Adjust a numeric value or range
Create a single-value slider or range slider in your constructor:
let volume = cx.new(|_| gpui_kit::base::slider::SliderState::new().step(5.).default_value(40.));
let interval = cx.new(|_| {
gpui_kit::base::slider::SliderState::new().step(5.).default_value((20., 80.))
});
slider(&Entity<gpui_kit::base::slider::SliderState>, disabled: bool, &mut Window, &mut App) -> gpui_kit::base::Slider renders a horizontal slider. Set the disabled flag in the constructor so both track and thumbs are inert; reserve returned builders for layout and styling.
Arrow keys and h/l step the focused thumb. Home/End moves it to its allowed endpoint, and range thumbs cannot cross. Keyboard changes emit base SliderEvent::Change and SliderEvent::Release; pointer interaction comes from the base track/thumb components. Use SliderValue::Single or SliderValue::Range when consuming state.
Choose a color
In your constructor, create cx.new(|cx| gpui_kit::base::ColorPickerState::new(window, cx)), optionally with .default_value(color). During rendering:
color_picker("accent", &self.color, window, cx)
color_picker(id, &Entity<gpui_kit::base::ColorPickerState>, &mut Window, &mut App) -> gpui_kit::base::ColorPicker composes a trigger, Hex editor and HSLA sliders. Hex supports 3, 4, 6 or 8 digits with an optional #. Invalid text shows an error and disables Apply color. Enter or Apply commits valid Hex input; Escape or outside dismissal discards its uncommitted preview. Slider adjustments apply immediately.
Observe the state or subscribe to gpui_kit::base::ColorPickerEvent to update an application preview. Your application decides whether picking a color should also apply a new Theme.
Organize navigation
Choose between expanding content in place, requesting another data page, and navigating between retained views. These are separate state models.
Expand a section
accordion(id, cx) -> gpui_kit::base::Accordion supplies the outer surface. Compose base AccordionItem and AccordionHeader slots with accordion_trigger(id, label, open: bool, cx) -> gpui_kit::base::AccordionTrigger and accordion_panel(cx) -> gpui_kit::base::AccordionPanel. All constructors take &App.
let changed = cx.listener(|this, open: &bool, _, cx| {
this.expanded = *open;
cx.notify();
});
accordion("settings", cx).child(
gpui_kit::base::AccordionItem::new()
.open(self.expanded)
.header(gpui_kit::base::AccordionHeader::new(
accordion_trigger("advanced", "Advanced settings", self.expanded, cx)
.on_change(move |open, _, window, cx| changed(&open, window, cx)),
))
.panel(accordion_panel(cx).child("Advanced settings appear here.")),
)
The item’s open state and the trigger’s indicator must agree. Keep one value per section, or close peers in your callback if your application requires an exclusive accordion.
For a simpler disclosure, collapsible(open: bool, cx: &App) -> gpui_kit::base::Collapsible keeps ordinary children visible and conditionally renders its .content(...) slot. Supply your own trigger and update the open value.
Request another data page
pagination(id, state: gpui_kit::base::PaginationState, cx: &App) -> gpui_kit::base::Pagination builds page buttons and ellipses. Pass the current page and page count into base state, then handle requested pages:
let changed = cx.listener(|this, page: &usize, _, cx| {
this.current_page = *page;
cx.notify();
});
let state = gpui_kit::base::PaginationState::new(self.current_page, 12)
.on_change(move |page, window, cx| changed(&page, window, cx));
pagination("pages", state, cx)
Page numbers are one-based. The constructor requests a page through state; fetching, sorting and displaying that page’s records belong to your application.
Navigate between retained views
Create cx.new(|_| gpui_kit::base::NavStackState::new()) once. It starts empty: push an initial page entity before rendering. For example, in your constructor, with first_page: Entity<P> already created and P: Render:
let navigation = cx.new(|_| gpui_kit::base::NavStackState::new());
navigation.update(cx, |state, cx| {
state.push(first_page.clone(), gpui_kit::base::NavMotion::Immediate, cx);
});
Store navigation on your view, then render nav_stack(&self.navigation, cx) -> gpui_kit::base::NavStack with &Entity<gpui_kit::base::NavStackState> and &App. In callbacks, call state.push(next_page, gpui_kit::base::NavMotion::Immediate, cx), state.pop(gpui_kit::base::NavMotion::Immediate, cx) or state.forward(gpui_kit::base::NavMotion::Immediate, cx) inside self.navigation.update(cx, |state, cx| { ... }). pop keeps the root page; forward returns a previously popped page when available. A new push discards forward history.
The stack preserves the base navigation model; page content and navigation controls remain yours. Retain page entities when you want their editing state to survive navigation. The gallery’s navigation example demonstrates pages with persistent notes.
Open menus and contextual content
Use a menu for actions, a popover for interactive contextual content, and a hover card for supplementary previews. Keep essential information available without hovering.
Offer a menu of actions
menu(
"workspace-menu",
button("menu-trigger", "Workspace", ButtonVariant::Outline, cx),
vec![
MenuItem::new("Settings").icon(IconName::Settings),
MenuItem::new("Team workspace").checked(true),
MenuItem::new("Archive").separator_before().disabled(true),
],
|index, _, _| {
// Dispatch your application command for this item index.
println!("Selected item {index}");
},
)
menu(id, trigger, items: Vec<MenuItem>, on_select) -> gpui_kit::base::Popover requires a trigger implementing gpui_kit::base::Selectable + IntoElement + 'static. Its callback is Fn(usize, &mut Window, &mut App) + 'static, receiving the original item index. Disabled entries cannot be chosen. The popup handles keyboard navigation, outside/Escape dismissal and focus return.
MenuItem::new(label) starts with no icon, shortcut, check or preceding separator. Its consuming builders return Self: .icon(IconName), .shortcut(label), .disabled(bool), .checked(bool) and .separator_before().
The MenuItem fields are also public: label: SharedString, icon: Option<IconName>, shortcut: Option<SharedString>, disabled: bool, checked: Option<bool> and separator_before: bool. The type implements Clone. checked: None is an ordinary command; Some(bool) marks an exclusive choice with a trailing check when selected. A shortcut is display text only; register the corresponding action/keybinding in your application.
Put an editor in a popover
popover(
"workspace-help",
button("help-trigger", "Workspace details", ButtonVariant::Outline, cx),
|_, _, _| gpui_kit::div().child("Changes are saved to this workspace."),
)
popover<E>(id, trigger, content) -> gpui_kit::base::Popover requires E: IntoElement, the same selectable trigger bounds as menu, and a content builder FnOnce(&mut gpui_kit::base::PopoverState, &mut Window, &mut Context<gpui_kit::base::PopoverState>) -> E + 'static.
The content builder runs in the popup state’s context, not your parent view’s context. Move entity handles or a parent listener into it when content needs to update your view. The constructor wraps content in popover_surface(cx) -> Div, a square, 17.5rem-wide themed surface; use that helper directly when composing another popup. Popover content has its own keyboard context so editing does not activate the trigger. Escape dismisses and restores focus.
Add supplementary help
with_tooltip<T: StatefulInteractiveElement>(control: T, text: impl Into<SharedString>) -> T preserves the input control’s type and adds a tooltip after 400ms hover. tooltip(text, cx: &App) -> gpui_kit::base::Tooltip creates only the customizable tooltip surface, without a trigger or timer. A tooltip does not replace an accessible name.
hover_card<E>(id, trigger, content) -> gpui_kit::base::HoverCard accepts any IntoElement trigger and a content builder FnOnce(&mut gpui_kit::base::HoverCardState, &mut Window, &mut Context<gpui_kit::base::HoverCardState>) -> E + 'static, with E: IntoElement. It wraps content in a popover surface, opens after 400ms and closes after 200ms. Use it for a preview such as a person’s details; put actions essential to completing the task in an ordinary visible control or popover.
Ask for a decision or show a side panel
Modal overlays need an application-owned open flag, a stable modal focus handle and a trigger handle. Create handles in the view constructor, focus the modal when opening, and restore the trigger when closing. Render the overlay only while open.
Compose a dialog
dialog(&FocusHandle, &mut App) -> gpui_kit::base::Dialog creates a centered modal host with a backdrop. alert_dialog(&FocusHandle, &mut App) -> gpui_kit::base::AlertDialog creates an explicit-decision modal with its distinct accessibility role; backdrop presses do not dismiss an alert dialog.
// Inside the branch rendered while self.modal_open is true.
let close = cx.listener(|this, confirmed: &bool, window, cx| {
if *confirmed {
// Commit your draft here.
}
this.modal_open = false;
this.modal_trigger.focus(window, cx);
cx.notify();
});
dialog(&self.modal_focus, cx)
.popup(
dialog_popup(cx)
.child(dialog_title("Save workspace?", cx))
.child(dialog_description("Save the current workspace settings.", cx))
.child(dialog_button("cancel", "Cancel", ButtonVariant::Secondary, cx)
.on_click(|_, window, cx| {
window.dispatch_action(Box::new(gpui_kit::base::actions::Cancel), cx);
}))
.child(dialog_button("confirm", "Save", ButtonVariant::Primary, cx)
.on_click(|_, window, cx| {
window.dispatch_action(
Box::new(gpui_kit::base::actions::Confirm { secondary: false }), cx,
);
})),
)
.request_close(move |confirmed, window, cx| close(&confirmed, window, cx))
The footer dispatches base confirm and cancel actions. Base owns the focus trap, Escape and confirmation dispatch; your close handler decides how to persist or discard the draft. Use the dialog’s base .on_ok(...) builder to reject invalid drafts before closing.
| Constructor | What it supplies |
|---|---|
dialog_backdrop() -> gpui_kit::base::DialogBackdrop |
Full-size black scrim at 60% opacity; useful when composing a custom host. |
dialog_popup(cx: &App) -> gpui_kit::base::DialogPopup |
Square content surface, 26.25rem wide and capped at its available width. |
dialog_title(title, cx: &App) -> gpui_kit::base::DialogTitle |
Emphasized title slot. |
dialog_description(text, cx: &App) -> gpui_kit::base::DialogDescription |
Secondary descriptive text slot. |
dialog_button(id, label, ButtonVariant, cx: &App) -> Button |
Outlined footer action using the requested semantic variant. |
Show details at the edge
sheet(&FocusHandle, &mut App) -> gpui_kit::base::Sheet uses the same backdrop and base focus trapping. Compose the right-hand panel with sheet_surface(cx: &App) -> Div, then handle closing:
let close = cx.listener(|this, _: &(), window, cx| {
this.sheet_open = false;
this.sheet_trigger.focus(window, cx);
cx.notify();
});
sheet(&self.sheet_focus, cx)
.surface(sheet_surface(cx).child("Workspace details"))
.request_close(move |window, cx| close(&(), window, cx))
The default surface is 22.5rem wide, capped at the available width, anchored to the right edge and full height. Refine its width with normal Styled builders. Focus self.sheet_focus in your opening callback; Escape and backdrop dismissal request closing, while your application updates visibility and restores focus.
Present status and rich content
A surface communicates state but does not own its lifecycle. Compose status text with actions that make sense for your application.
panel("Import", cx)
.child(badge("In progress", Status::Neutral, cx))
.child(progress("import-progress", 42., cx))
.child(keycap("Escape", cx))
Constructor (with cx: &App) |
Return type and use |
|---|---|
panel(title, cx) |
Div: title and vertically arranged content with padding and an outer edge. |
separator(cx) |
Div: a one-pixel horizontal divider. |
vertical_separator(cx) |
Div: a thin, 1.25rem-high toolbar divider; override height when needed. |
keycap(key, cx) |
Div: a bordered keyboard hint. It does not bind that key. |
badge(label, status: Status, cx) |
Div: short status label with a matching border. |
empty_state(title, description, cx) |
Div: explain what belongs in the empty region; add a relevant action as a child. |
progress(id, value: f32, cx) |
gpui_kit::base::Progress: determinate percentage, clamped to 0..=100; NaN and infinities become zero. |
toast(id, cx) |
gpui_kit::base::Toast: notification surface with room for content and actions. |
avatar(initials, cx) |
gpui_kit::base::Avatar: square identity marker with an initials fallback. |
Status is Neutral (the default), Success, Warning or Error; these map to secondary, success, warning and danger colors. Like ButtonVariant, it supports Clone, Copy, Debug, Default, PartialEq and Eq.
Own notification timing
toast creates a surface, not a notification service. Keep placement, stacking, dismissal, timeout and retry behavior in your application; base ToastManager can help. The gallery’s six-second, hover/focus-paused success notifications are an example policy, not a constructor default. Persistent errors should offer a recovery action.
Show an identity image
avatar_image(source: impl Into<gpui_kit::ImageSource>) -> gpui_kit::base::AvatarImage sizes an image slot to the avatar and does not require an application context:
avatar("JL", cx).image(avatar_image(self.profile_image.clone()))
The application decides whether to provide the image or fall back to initials after loading failure. The initials slot does not imply an automatic network retry or failure policy.
Render a document
markdown(id, source, window, cx) and html(id, source, window, cx) both return gpui_kit::base::TextView. They accept id: impl Into<ElementId>, source: impl Into<SharedString>, window: &Window and cx: &App. The window supplies the current rem scale for heading sizes.
markdown("readme", "# Workspace\n\nSelect a file to begin.", window, cx)
These are read-only rich-text views with document typography, links and selectable content, not source editors or embedded web browsers. Install one gpui_kit::base::TextSelectionLayer as the root’s first child so selection is initialized before rendering text:
focus_scope("document")
.size_full()
.child(gpui_kit::base::TextSelectionLayer)
.child(markdown("readme", "# Workspace\n\nSelect a file to begin.", window, cx))
Do not add a separate selection layer to every text view.
For a state-backed base TextView, use text_view_style(window: &Window, cx: &App) -> gpui_kit::base::TextViewStyle. It maps Omarchy foreground, muted text, links, selection, borders and code surfaces into the base renderer’s style, so all three entry points share the same document presentation.
Display tables, lists and trees
Use a table for a modest structured dataset, a virtual list for many rows, and a tree for hierarchical items. Your application owns records, loading and selection policy.
Compose a table
table("files", cx)
.child(table_row("header", 1, cx)
.child(table_head("name", 1, cx).child("Name"))
.child(table_head("status", 2, cx).child("Status")))
.child(table_row("readme", 2, cx)
.child(table_cell("name", 1, cx).child("README.md"))
.child(table_cell("status", 2, cx).child("Saved")))
table(id, cx) -> gpui_kit::base::Table supplies the outer edge and text style. table_row(id, index, cx) -> gpui_kit::base::TableRow takes a one-based accessibility row index including the header. table_head(id, index, cx) -> gpui_kit::base::TableHead and table_cell(id, index, cx) -> gpui_kit::base::TableCell take a one-based column index. All use cx: &App and index: usize.
Cells share available width by default; refine widths consistently across the header and body. These are primitive table parts: sorting, pagination, virtualization and row selection are not added automatically.
Render only visible list rows
virtual_list creates a vertical gpui_kit::base::VirtualList. Give it your view entity, a stable ID, every row’s size and a closure that builds only the requested range:
let sizes = std::rc::Rc::new(vec![
gpui_kit::size(gpui_kit::Pixels::ZERO, gpui_kit::rems(1.75).to_pixels(window.rem_size()));
self.records.len()
]);
virtual_list(
cx.entity(),
"records",
sizes,
|this, range, _, _| {
range.map(|index| {
gpui_kit::div()
.h(gpui_kit::rems(1.75))
.child(this.records[index].clone())
}).collect()
},
cx,
)
The full parameter types are view: Entity<V>, id: impl Into<ElementId>, sizes: Rc<Vec<Size<Pixels>>>, render: impl Fn(&mut V, Range<usize>, &mut Window, &mut Context<V>) -> Vec<R> + 'static, and cx: &App, where V: Render and R: IntoElement.
Every size must match its row’s rendered height, including variable-height rows. Resolve row sizes using window.rem_size() and rebuild cached measurements when it changes. The default list height is 17.5rem; override it for your layout. Retain a base VirtualListScrollHandle and attach it with .track_scroll(&handle) to preserve position or navigate to an item.
Add a scrollbar
scrollbar(id, axis, &handle, window, cx) -> gpui_kit::base::Scrollbar accepts axis: gpui_kit::base::ScrollbarAxis, window: &Window, cx: &App and a handle implementing gpui_kit::base::ScrollbarHandle + Clone. Vertical, horizontal and both-axis modes are supported.
Share the handle with the scrollable content, and render the scrollbar after that content inside the same relative container. The default mode is ScrollbarMode::Always; base builders remain available for customization. A scrollbar does not make ordinary content scrollable by itself.
Navigate a tree
Create the tree state once:
let files = cx.new(|cx| {
gpui_kit::base::TreeState::new(cx).items(vec![
gpui_kit::base::TreeItem::new("documents", "Documents")
.expanded(true)
.child(gpui_kit::base::TreeItem::new("readme", "README.md")),
gpui_kit::base::TreeItem::new("archive", "Archive").disabled(true),
])
});
tree(&Entity<gpui_kit::base::TreeState>, cx: &App) -> gpui_kit::base::Tree adds indented 1.75rem rows, expand/collapse icons, disabled styling and selected fills to the base virtualized keyboard tree. It defaults to 17.5rem high. Read selection and update items through the base state, and observe the entity when dependent content needs to redraw.
Arrange a workspace
Use resizable panes when the layout has a fixed set of regions. Use a dock area when users need to move and regroup panels.
Resize adjacent panes
resizable("workspace", gpui_kit::Axis::Horizontal, cx)
.child(resizable_panel().child("Files"))
.child(resizable_panel().child("Editor"))
resizable(id, axis: gpui_kit::Axis, cx: &App) -> gpui_kit::base::ResizablePanelGroup installs a fine divider while base owns dragging, hit areas and constraints. resizable_panel() -> gpui_kit::base::ResizablePanel creates an empty pane. Configure initial sizes and constraints with the returned base panel builders; choose horizontal or vertical axes for side-by-side or stacked regions.
Dock and regroup panels
Create a dock area entity inside your view constructor:
let dock = cx.new(|cx| dock_area("workspace", window, cx));
dock_area(id: impl Into<SharedString>, window: &mut Window, cx: &mut Context<gpui_kit::base::dock::DockArea>) -> gpui_kit::base::dock::DockArea installs the Omarchy dock renderer. Unlike most constructors, it builds the stateful dock view inside that view’s own context. Retain the entity and render it as a child of your workspace.
An empty dock has no visible panels. A base gpui_kit::base::dock::Panel must implement Render, gpui_kit::Focusable, gpui_kit::EventEmitter<gpui_kit::base::dock::PanelEvent> and panel_name(&self) -> &'static str. Keep its focus handle stable and its panel name stable across saved layouts. This minimal panel type can be defined at module scope:
struct NotesPanel {
focus: gpui_kit::FocusHandle,
}
impl gpui_kit::EventEmitter<gpui_kit::base::dock::PanelEvent> for NotesPanel {}
impl gpui_kit::Focusable for NotesPanel {
fn focus_handle(&self, _: &gpui_kit::App) -> gpui_kit::FocusHandle {
self.focus.clone()
}
}
impl gpui_kit::base::dock::Panel for NotesPanel {
fn panel_name(&self) -> &'static str { "notes" }
}
impl Render for NotesPanel {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
gpui_kit::div().size_full().child("Workspace notes")
}
}
Then create the panel and assign a layout in your parent view’s constructor:
let notes = cx.new(|cx| NotesPanel { focus: cx.focus_handle() });
let dock = cx.new(|cx| dock_area("workspace", window, cx));
dock.update(cx, |state, cx| {
state.set_center(gpui_kit::base::dock::DockLayout::tabs().panel(notes), window, cx);
});
Retain dock and render its cloned entity in a container with available width and height. Base owns tab dragging, merging and split arrangements; gpui-omarchy supplies tab bars, surfaces and drop indicators. Floating layouts are not offered. The gallery’s dock example demonstrates multiple panels and a split/tab layout.
Find an API and continue
Use the guide with an AI coding assistant
Start from the plain Markdown at guides.md, discovered through the site’s llms.txt index or the Download Markdown link. The HTML and Markdown are generated from the same source. llms.txt is a convenience index; tools that do not discover it automatically can read the Markdown URL directly.
For an implementation task, follow this sequence:
- Read the dependency versions and complete application example. Use the code for this documented version; inspect your project’s
Cargo.lockbefore mixing in examples from newer base releases. - Find the task’s chapter and choose its state model. Create entities once in the view constructor and retain them as fields. Treat view, popup and component-state contexts as different types.
- Distinguish a complete application from a fragment. A fragment using
self.namerequires anEntity<InputState>field initialized earlier; it is not a standalone program. Use explicit framework imports to avoid name collisions. - Preserve constructor-owned state: update values through callbacks or entities, and rebuild the themed component on render. Do not overwrite checkbox indicators, choice popup state or slider disabled state through unrelated builders.
- Keep IDs and focus handles stable. Add required root layers, initial navigation pages and dock panels; constructing an empty state alone does not populate a view.
- Run
cargo checkfor types andcargo runto exercise interaction. Test Tab/Shift+Tab, disabled controls, editing, popup dismissal and focus return. Compilation does not prove runtime behavior.
When an API is not listed, inspect src/lib.rs, the relevant module and the installed gpui-base documentation produced by cargo doc. Do not invent a wrapper method or assume a base type is re-exported. For asynchronous work, use GPUI’s supported task/context APIs and update view state on the appropriate context; the snippets here do not define a background persistence service.
All guide content is in this Markdown page. Use browser Find for a function or type name; the chapter directory lets you jump between tasks without changing pages.
Public modules mirror the components, so gpui_omarchy::controls::button and the root gpui_omarchy::button name the same constructor. This map lists every public module and where it appears above:
| Public modules | Guide |
|---|---|
theme |
Follow and customize the theme |
focus |
Compose a view and manage state |
button, link, icon |
Add actions and links |
controls |
Build forms, Add actions and links, Choose options and switch pages |
input, otp_input |
Build forms |
button_group, select |
Choose options and switch pages |
calendar, date_picker, slider, color_picker |
Edit dates, ranges and colors |
navigation |
Organize navigation |
menu, popover, hover_card, tooltip |
Open menus and contextual content |
dialog, sheet |
Ask for a decision or show a side panel |
surface, text |
Present status and rich content |
table, list, tree |
Display tables, lists and trees |
resizable, dock |
Arrange a workspace |
The root re-exports all themed constructors, Button, Link, OtpInput, ChoiceItem, ChoiceState, DatePickerState, MenuItem, ButtonVariant, Status, IconName, Theme, ActiveTheme and ThemeLoadError. Framework types and the application entry point come directly from gpui_kit. icon::Icon is module-qualified. The theme loader’s implementation module is private; its public theme methods and error type remain available as described above.
For runnable examples in a checkout, start with examples/hello.rs, then explore examples/gallery/app.rs. Run the complete gallery with cargo run --example gallery. examples/gallery-wasm/README.md explains browser bootstrap, fonts and WebAssembly build requirements; browser applications use GPUI Kit’s web bootstrap instead of gpui_kit::application() and do not monitor desktop theme files.
For framework concepts, continue with GPUI Kit or its AI-readable documentation. Those docs also cover the optional component layer; keep the Omarchy setup in this guide when using this library. To inspect the exact base builders for your installed dependency versions, run cargo doc --open. Use these guides for the Omarchy presentation, state and composition conventions, and the generated dependency documentation for additional base and GPUI customization.