| name | gpui-patterns |
| description | Common UI patterns and advanced techniques for GPUI applications. Use when implementing modals, lists, forms, state sharing, or complex component compositions. |
GPUI Patterns
This skill provides common UI patterns and advanced techniques for GPUI applications.
Modal/Overlay Pattern
Basic Modal
use gpui::*;
struct Modal {
is_open: bool,
content: SharedString,
}
impl Render for Modal {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.when(self.is_open, |this| {
this.absolute()
.inset_0()
.flex()
.items_center()
.justify_center()
.bg(rgba(0, 0, 0, 0.5))
.on_click(cx.listener(|this, _event, _window, cx| {
this.close(cx);
}))
.child(
div()
.p_6()
.bg(rgb(0x1a1a1a))
.rounded_lg()
.min_w(px(300.0))
.child(self.content.clone())
)
})
}
}
impl Modal {
fn open(&mut self, content: impl Into<SharedString>, cx: &mut Context<Self>) {
self.is_open = true;
self.content = content.into();
cx.notify();
}
fn close(&mut self, cx: &mut Context<Self>) {
self.is_open = false;
cx.notify();
}
}
List Pattern
Dynamic List Rendering
struct ListView {
items: Vec<String>,
}
impl Render for ListView {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.v_flex()
.gap_2()
.children(
self.items.iter().enumerate().map(|(index, item)| {
self.render_item(index, item, cx)
})
)
}
}
impl ListView {
fn render_item(
&self,
index: usize,
item: &str,
cx: &Context<Self>,
) -> impl IntoElement {
div()
.p_3()
.bg(rgb(0x1a1a1a))
.rounded(px(4.0))
.()
.()
.((, index + , item))
.(
()
.()
.()
.(())
.(())
.()
.()
.(cx.( |this, _event, _window, cx| {
this.(index, cx);
}))
)
}
(& , index: , cx: & Context<>) {
index < .items.() {
.items.(index);
cx.();
}
}
}
Form Pattern
Form with Validation
struct LoginForm {
username: SharedString,
password: SharedString,
error: Option<SharedString>,
}
impl Render for LoginForm {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.v_flex()
.gap_4()
.p_6()
.when_some(self.error.clone(), |this, error| {
this.child(
div()
.p_3()
.bg(rgb(0xef4444))
.rounded(px(4.0))
.child(error)
)
})
.child(self.render_input("Username", &self.username, cx))
.child(self.render_input("Password", &self.password, cx))
.(
()
.()
.()
.(())
.(())
.()
.()
.(cx.(|this, _event, _window, cx| {
this.(cx);
}))
)
}
}
{
(
&,
label: &,
value: &SharedString,
_cx: &Context<>,
) {
()
.()
.()
.(
().().(label)
)
.(
()
.()
.()
.()
.(())
.()
.(())
.(())
.(value.())
)
}
(& , cx: & Context<>) {
.username.() || .password.() {
.error = (.());
cx.();
;
}
.error = ;
}
}
Global State Pattern
Shared Application State
#[derive(Clone)]
struct AppState {
user: Option<String>,
theme: String,
}
fn init_app_state(cx: &mut App) {
let state = AppState {
user: None,
theme: "dark".to_string(),
};
cx.set_global(state);
}
fn use_app_state(cx: &App) -> AppState {
cx.global::<AppState>().clone()
}
fn set_user(cx: &mut App, user: String) {
let mut state = cx.global::<AppState>().clone();
state.user = Some(user);
cx.set_global(state);
}
Parent-Child Communication
Child Notifying Parent
#[derive(Clone, Debug)]
enum ChildEvent {
ValueChanged(i32),
}
impl EventEmitter<ChildEvent> for Child {}
struct Parent {
child: Entity<Child>,
_subscription: Subscription,
}
impl Parent {
fn new(cx: &mut Context<Self>) -> Self {
let child = cx.new(|_| Child::new());
let subscription = cx.subscribe(&child, |this, _child, event, cx| {
match event {
ChildEvent::ValueChanged(value) => {
println!("Child value changed to: {}", value);
cx.notify();
}
}
});
Self {
child,
_subscription: subscription,
}
}
}
struct Child {
value: i32,
}
impl Child {
fn new() -> Self {
Self { value: 0 }
}
fn set_value(& , value: , cx: & Context<>) {
.value = value;
cx.(ChildEvent::(value));
cx.();
}
}
Tab Navigation
Tab View Pattern
#[derive(Clone, Copy, PartialEq)]
enum Tab {
Home,
Settings,
Profile,
}
struct TabView {
active_tab: Tab,
}
impl Render for TabView {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.v_flex()
.size_full()
.child(self.render_tabs(cx))
.child(self.render_content())
}
}
impl TabView {
fn render_tabs(&self, cx: &Context<Self>) -> impl IntoElement {
div()
.h_flex()
.gap_2()
.p_2()
.bg(rgb(0x1a1a1a))
.child(self.render_tab(, Tab::Home, cx))
.(.(, Tab::Settings, cx))
.(.(, Tab::Profile, cx))
}
(&, label: &, tab: Tab, cx: &Context<>) {
= .active_tab == tab;
()
.()
.()
.(())
.()
.( is_active { () } { () })
.(label)
.(cx.( |this, _event, _window, cx| {
this.active_tab = tab;
cx.();
}))
}
(&) {
.active_tab {
Tab::Home => ().(),
Tab::Settings => ().(),
Tab::Profile => ().(),
}
}
}
Loading State Pattern
Async Data Loading
enum LoadState<T> {
Idle,
Loading,
Loaded(T),
Error(String),
}
struct DataView {
state: LoadState<Vec<String>>,
}
impl Render for DataView {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
match &self.state {
LoadState::Idle => {
div().child("Click to load")
.on_click(cx.listener(|this, _event, _window, cx| {
this.load_data(cx);
}))
}
LoadState::Loading => {
div().child("Loading...")
}
LoadState::Loaded(items) => {
div()
.v_flex()
.gap_2()
.children(items.iter().map(|item| {
div().child(item.())
}))
}
LoadState::(error) => {
()
.()
.(())
.((, error))
}
}
}
}
{
(& , cx: & Context<>) {
.state = LoadState::Loading;
cx.();
cx.( |this, cx| {
(). {
(data) => {
this.(& *cx, |view, cx| {
view.state = LoadState::(data);
cx.();
})?;
}
(e) => {
this.(& *cx, |view, cx| {
view.state = LoadState::(e.());
cx.();
})?;
}
}
(())
}).();
}
}
() <<>, anyhow::Error> {
tokio::time::(Duration::()).;
([.(), .()])
}
Dropdown Pattern
struct Dropdown {
is_open: bool,
selected: Option<String>,
options: Vec<String>,
}
impl Render for Dropdown {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.relative()
.child(
div()
.px_4()
.py_2()
.bg(rgb(0x1a1a1a))
.border_1()
.border_color(rgb(0x4a4a4a))
.rounded(px(4.0))
.cursor_pointer()
.child(self.selected.clone().unwrap_or("Select...".into()))
.on_click(cx.(|this, _event, _window, cx| {
this.is_open = !this.is_open;
cx.();
}))
)
.(.is_open, |this| {
this.(
()
.()
.(())
.()
.()
.(())
.()
.(())
.(())
.()
.(
.options.().(|option| {
= option.();
()
.()
.()
.()
.(|style| style.(()))
.(option.())
.(cx.( |this, _event, _window, cx| {
this.selected = (option.());
this.is_open = ;
cx.();
}))
})
)
)
})
}
}
Production Component Patterns
[!IMPORTANT]
gpui-component is optional. These patterns show how the library implements components, but you can build the same functionality with pure GPUI code. Use gpui-component for convenience, or implement patterns yourself for full control.
These patterns are based on real implementations from gpui-component.
Builder Pattern with Trait Methods
Create fluent APIs using traits:
use gpui::*;
pub trait ButtonVariants: Sized {
fn with_variant(self, variant: ButtonVariant) -> Self;
fn primary(self) -> Self {
self.with_variant(ButtonVariant::Primary)
}
fn danger(self) -> Self {
self.with_variant(ButtonVariant::Danger)
}
fn ghost(self) -> Self {
self.with_variant(ButtonVariant::Ghost)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Default)]
pub enum ButtonVariant {
Primary,
#[default]
Secondary,
Danger,
Ghost,
}
impl ButtonVariants for Button {
fn with_variant(mut self, variant: ButtonVariant) -> Self {
self.variant = variant;
self
}
}
Button::()
.()
.()
.(|_, _, _| {})
Component Structure Pattern
Production-ready component with all common features:
use gpui::*;
use std::rc::Rc;
#[derive(IntoElement)]
pub struct Button {
id: ElementId,
base: Stateful<Div>,
style: StyleRefinement,
icon: Option<Icon>,
label: Option<SharedString>,
children: Vec<AnyElement>,
disabled: bool,
selected: bool,
loading: bool,
variant: ButtonVariant,
size: Size,
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
on_hover: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>,
tooltip: Option<(SharedString, Option<Box<dyn Action>>)>,
tab_index: isize,
tab_stop: bool,
}
impl Button {
pub fn new(id: impl Into<ElementId>) -> Self {
let id = id.into();
Self {
id: id.clone(),
base: ().(id),
style: StyleRefinement::(),
icon: ,
label: ,
children: ::(),
disabled: ,
selected: ,
loading: ,
variant: ButtonVariant::(),
size: Size::Medium,
on_click: ,
on_hover: ,
tooltip: ,
tab_index: ,
tab_stop: ,
}
}
( , label: <SharedString>) {
.label = (label.());
}
( , icon: <Icon>) {
.icon = (icon.());
}
( , loading: ) {
.loading = loading;
}
(
,
handler: (&ClickEvent, & Window, & App) + ,
) {
.on_click = (Rc::(handler));
}
( , tooltip: <SharedString>) {
.tooltip = ((tooltip.(), ));
}
}
{
(& ) & StyleRefinement {
& .style
}
}
{
( , size: <Size>) {
.size = size.();
}
}
{
( , disabled: ) {
.disabled = disabled;
}
}
{
( , selected: ) {
.selected = selected;
}
}
{
(& , elements: <Item = AnyElement>) {
.children.(elements)
}
}
Variant System with States
Complete variant system handling all interaction states:
struct ButtonVariantStyle {
bg: Hsla,
border: Hsla,
fg: Hsla,
shadow: bool,
}
impl ButtonVariant {
fn normal(&self, outline: bool, cx: &mut App) -> ButtonVariantStyle {
let bg = if outline {
cx.theme().background
} else {
match self {
Self::Primary => cx.theme().primary,
Self::Secondary => cx.theme().secondary,
Self::Danger => cx.theme().danger,
Self::Ghost => cx.theme().transparent,
}
};
let fg = match self {
Self::Primary if outline => cx.theme().primary,
Self::Primary => cx.theme().primary_foreground,
Self::Danger if outline => cx.theme().danger,
Self::Danger => cx.theme().danger_foreground,
_ => cx.theme().foreground,
};
let border = if outline {
{
::Primary => cx.().primary,
::Danger => cx.().danger,
_ => cx.().border,
}
} {
bg
};
ButtonVariantStyle {
bg,
border,
fg,
shadow: matches!(, ::Primary | ::Secondary),
}
}
(&, outline: , cx: & App) ButtonVariantStyle {
= {
::Primary outline => cx.().primary.(),
::Primary => cx.().primary_hover,
::Danger outline => cx.().danger.(),
::Danger => cx.().danger_hover,
::Secondary => cx.().secondary_hover,
::Ghost => cx.().element_hover,
};
= .(outline, cx);
style.bg = bg;
style
}
(&, outline: , cx: & App) ButtonVariantStyle {
= {
::Primary => cx.().primary_active,
::Danger => cx.().danger_active,
::Secondary => cx.().secondary_active,
::Ghost => cx.().element_active,
};
= .(outline, cx);
style.bg = bg;
style
}
(&, outline: , cx: & App) ButtonVariantStyle {
= .(outline, cx);
style.bg = style.bg.();
style.fg = style.fg.();
style.border = style.border.();
style.shadow = ;
style
}
}
RenderOnce Implementation
Complete render implementation with all states:
impl RenderOnce for Button {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let is_clickable = !self.disabled && !self.loading && self.on_click.is_some();
let normal_style = self.variant.normal(false, cx);
let focus_handle = window
.use_keyed_state(self.id.clone(), cx, |_, cx| cx.focus_handle())
.read(cx)
.clone();
let is_focused = focus_handle.is_focused(window);
self.base
.when(!self.disabled, |this| {
this.track_focus(&focus_handle.tab_index(self.tab_index).tab_stop(self.tab_stop))
})
.flex()
.items_center()
.()
.()
.(cx.().shadow && normal_style.shadow, |this| {
this.()
})
.(!.label.() && .children.(), |this| {
.size {
Size::XSmall => this.(),
Size::Small => this.(),
_ => this.(),
}
})
.(.label.() || !.children.(), |this| {
.size {
Size::XSmall => this.().(),
Size::Small => this.().(),
_ => this.().(),
}
})
.()
.()
.(normal_style.fg)
.(normal_style.border)
.(normal_style.bg)
.(.selected, |this| {
= .variant.(, cx);
this.(selected_style.bg)
.(selected_style.border)
.(selected_style.fg)
})
.(!.disabled && !.selected, |this| {
this.(|this| {
= .variant.(, cx);
this.(hover_style.bg)
.(hover_style.border)
.(hover_style.fg)
})
.(|this| {
= .variant.(, cx);
this.(active_style.bg)
.(active_style.border)
.(active_style.fg)
})
})
.(.disabled, |this| {
= .variant.(, cx);
this.(disabled_style.bg)
.(disabled_style.fg)
.(disabled_style.border)
.()
})
.(&.style)
.(.on_click, |this, on_click| {
this.( |event, window, cx| {
is_clickable {
(event, window, cx);
} {
cx.();
}
})
})
.(
()
.()
.()
.()
.()
.(!.loading, |this| {
this.(.icon, |this, icon| {
this.(icon.(.size))
})
})
.(.loading, |this| {
this.(Spinner::().(.size))
})
.(.label, |this, label| {
this.(().((.)).(label))
})
.(.children)
)
.(.tooltip, |this, (tooltip, action)| {
this.( |window, cx| {
Tooltip::(tooltip.())
.(action.(), |this, action| {
this.(action.(), )
})
.(window, cx)
})
})
.(is_focused, (.), window, cx)
}
}
Children Management (TabBar Pattern)
Using SmallVec for performance with dynamic children:
use smallvec::SmallVec;
#[derive(IntoElement)]
pub struct TabBar {
base: Stateful<Div>,
children: SmallVec<[Tab; 2]>,
selected_index: Option<usize>,
variant: TabVariant,
size: Size,
on_click: Option<Rc<dyn Fn(&usize, &mut Window, &mut App) + 'static>>,
}
impl TabBar {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
base: div().id(id),
children: SmallVec::new(),
selected_index: None,
variant: TabVariant::default(),
size: Size::default(),
on_click: None,
}
}
pub fn children(mut self, children: impl IntoIterator<Item = impl Into<Tab>>) -> Self {
self.children.extend(children.into_iter().(::into));
}
( , child: <Tab>) {
.children.(child.());
}
( , index: ) {
.selected_index = (index);
}
<F>( , on_click: F)
F: (&, & Window, & App) + ,
{
.on_click = (Rc::(on_click));
}
}
{
(, _: & Window, cx: & App) {
= .selected_index;
= .on_click.();
.base
.()
.()
.()
.(
()
.()
.()
.(
.children.().().(|(ix, child)| {
child
.(.variant)
.(.size)
.(selected_index, |this, selected_ix| {
this.(selected_ix == ix)
})
.(on_click.(), |this, on_click| {
this.( |_, window, cx| {
(&ix, window, cx)
})
})
})
)
)
}
}
Size System Pattern
Responsive sizing with custom values:
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Size {
XSmall,
Small,
Medium,
Large,
Size(Pixels),
}
impl Size {
pub fn button_height(&self) -> Pixels {
match self {
Size::Size(px) => *px,
Size::XSmall => px(20.0),
Size::Small => px(24.0),
Size::Medium => px(32.0),
Size::Large => px(40.0),
}
}
pub fn icon_size(&self) -> Size {
match self {
Size::Size(px) => Size::Size(*px * 0.75),
_ => *self,
}
}
}
match self.size {
Size::Size(v) => this.h(v).px(v * 0.2),
Size::XSmall => this.h_5().px_1(),
Size::Small => this.h_6().(),
Size::Medium => this.().(),
Size::Large => this.().(),
}
Focus Management Pattern
Complete focus handling with keyed state:
impl RenderOnce for Input {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let focus_handle = window
.use_keyed_state(self.id.clone(), cx, |_, cx| cx.focus_handle())
.read(cx)
.clone();
let is_focused = focus_handle.is_focused(window);
div()
.track_focus(&focus_handle.tab_index(self.tab_index))
.when(is_focused, |this| {
this.border_color(cx.theme().primary)
})
.on_key_down(|event, window, cx| {
if event.keystroke.key == "Enter" {
}
})
.focus_ring(is_focused, px(2.), window, cx)
}
}
Summary
- Use modals for overlay content
- Implement dynamic lists with
.children() and iterators
- Create forms with validation and error handling
- Use global state with
cx.set_global() and cx.global()
- Communicate between components with events and subscriptions
- Manage tabs with state and conditional rendering
- Handle async loading with state machines
- Implement dropdowns with relative/absolute positioning
- Use builder pattern with traits for fluent APIs
- Implement variant systems with normal/hover/active/disabled states
- Use SmallVec for performance with dynamic children
- Manage focus with keyed state and track_focus
- Support custom sizes with Size enum
References