| name | gpui-patterns |
| description | Common GPUI patterns including component composition, state management strategies, event handling, and action dispatching. Use when user needs guidance on GPUI patterns, component design, or state management approaches. Use when this capability is needed. |
| metadata | {"author":"geoffjay"} |
GPUI Patterns
Metadata
This skill provides comprehensive guidance on common GPUI patterns and best practices for building maintainable, performant applications.
Instructions
Component Composition Patterns
Basic Component Structure
use gpui::*;
struct MyView {
state: Model<MyState>,
_subscription: Subscription,
}
impl MyView {
fn new(state: Model<MyState>, cx: &mut ViewContext<Self>) -> Self {
let _subscription = cx.observe(&state, |_, _, cx| cx.notify());
Self { state, _subscription }
}
}
impl Render for MyView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let state = self.state.read(cx);
div()
.flex()
.flex_col()
.child(format!("Value: {}", state.value))
}
}
Container/Presenter Pattern
Container (manages state and logic):
struct Container {
model: Model<AppState>,
_subscription: Subscription,
}
impl Container {
fn new(model: Model<AppState>, cx: &mut ViewContext<Self>) -> Self {
let _subscription = cx.observe(&model, |_, _, cx| cx.notify());
Self { model, _subscription }
}
}
impl Render for Container {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let state = self.model.read(cx);
Presenter::new(state.data.clone())
}
}
Presenter (pure rendering):
struct Presenter {
data: String,
}
impl Presenter {
fn new(data: String) -> Self {
Self { data }
}
}
impl Render for Presenter {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div().child(self.data.as_str())
}
}
Compound Components
pub struct Tabs {
items: Vec<TabItem>,
active_index: usize,
}
pub struct TabItem {
label: String,
content: Box<dyn Fn() -> AnyElement>,
}
impl Tabs {
pub fn new() -> Self {
Self {
items: Vec::new(),
active_index: 0,
}
}
pub fn add_tab(
mut self,
label: impl Into<String>,
content: impl Fn() -> AnyElement + 'static,
) -> Self {
self.items.push(TabItem {
label: label.into(),
content: Box::new(content),
});
self
}
fn set_active(&mut self, index: usize, cx: & ViewContext<>) {
.active_index = index;
cx.();
}
}
{
(& , cx: & ViewContext<>) {
()
.()
.()
.(
()
.()
.(
.items.().().(|(i, item)| {
(&item.label, i == .active_index, || {
.(i, cx)
})
})
)
)
.(
(.items[.active_index].content)()
)
}
}
State Management Strategies
Model-View Pattern
#[derive(Clone)]
struct AppState {
count: usize,
items: Vec<String>,
}
struct AppView {
state: Model<AppState>,
_subscription: Subscription,
}
impl AppView {
fn new(state: Model<AppState>, cx: &mut ViewContext<Self>) -> Self {
let _subscription = cx.observe(&state, |_, _, cx| cx.notify());
Self { state, _subscription }
}
fn increment(&mut self, cx: &mut ViewContext<Self>) {
self.state.update(cx, |state, cx| {
state.count += 1;
cx.notify();
});
}
}
Context-Based State
#[derive(Clone)]
struct GlobalSettings {
theme: Theme,
language: String,
}
impl Global for GlobalSettings {}
fn init_app(cx: &mut AppContext) {
cx.set_global(GlobalSettings {
theme: Theme::Light,
language: "en".to_string(),
});
}
impl Render for MyView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let settings = cx.global::<GlobalSettings>();
div()
.child(format!("Language: {}", settings.language))
}
}
Subscription Patterns
Basic Subscription:
struct Observer {
model: Model<Data>,
_subscription: Subscription,
}
impl Observer {
fn new(model: Model<Data>, cx: &mut ViewContext<Self>) -> Self {
let _subscription = cx.observe(&model, |_, _, cx| {
cx.notify();
});
Self { model, _subscription }
}
}
Selective Updates:
impl Observer {
fn new(model: Model<Data>, cx: &mut ViewContext<Self>) -> Self {
let _subscription = cx.observe(&model, |this, model, cx| {
let data = model.read(cx);
if data.important_field != this.cached_field {
this.cached_field = data.important_field.clone();
cx.notify();
}
});
Self {
model,
cached_field: String::new(),
_subscription,
}
}
}
Multiple Subscriptions:
struct MultiObserver {
model_a: Model<DataA>,
model_b: Model<DataB>,
_subscriptions: Vec<Subscription>,
}
impl MultiObserver {
fn new(
model_a: Model<DataA>,
model_b: Model<DataB>,
cx: &mut ViewContext<Self>,
) -> Self {
let mut subscriptions = Vec::new();
subscriptions.push(cx.observe(&model_a, |_, _, cx| cx.notify()));
subscriptions.push(cx.observe(&model_b, |_, _, cx| cx.notify()));
Self {
model_a,
model_b,
_subscriptions: subscriptions,
}
}
}
Event Handling Patterns
Click Events
div()
.on_click(cx.listener(|this, event: &ClickEvent, cx| {
this.handle_click(cx);
}))
.child("Click me")
Keyboard Events
div()
.on_key_down(cx.listener(|this, event: &KeyDownEvent, cx| {
match event.key.as_str() {
"Enter" => this.submit(cx),
"Escape" => this.cancel(cx),
_ => {}
}
}))
Event Propagation
div()
.on_click(|event, cx| {
event.stop_propagation();
})
div()
.on_key_down(|event, cx| {
if event.key == "Tab" {
event.prevent_default();
}
})
Mouse Events
div()
.on_mouse_down(cx.listener(|this, event, cx| {
this.mouse_down_position = Some(event.position);
}))
.on_mouse_move(cx.listener(|this, event, cx| {
if let Some(start) = this.mouse_down_position {
let delta = event.position - start;
this.handle_drag(delta, cx);
}
}))
.on_mouse_up(cx.listener(|this, event, cx| {
this.mouse_down_position = None;
}))
Action System
Define Actions
use gpui::*;
actions!(app, [
Increment,
Decrement,
Reset,
SetValue
]);
#[derive(Clone, PartialEq)]
pub struct SetValue {
pub value: i32,
}
impl_actions!(app, [SetValue]);
Register Action Handlers
impl Counter {
fn register_actions(&mut self, cx: &mut ViewContext<Self>) {
cx.on_action(cx.listener(|this, _: &Increment, cx| {
this.model.update(cx, |state, cx| {
state.count += 1;
cx.notify();
});
}));
cx.on_action(cx.listener(|this, _: &Decrement, cx| {
this.model.update(cx, |state, cx| {
state.count = state.count.saturating_sub(1);
cx.notify();
});
}));
cx.on_action(cx.listener(|this, action: &SetValue, cx| {
this.model.update(cx, |state, cx| {
state.count = action.value;
cx.notify();
});
}));
}
}
Dispatch Actions
fn handle_button_click(&mut self, cx: &mut ViewContext<Self>) {
cx.dispatch_action(Increment);
}
fn set_specific_value(&mut self, value: i32, cx: &mut ViewContext<Self>) {
cx.dispatch_action(SetValue { value });
}
cx.dispatch_action_on_window(Reset, window_id);
Keybindings
fn register_keybindings(cx: &mut AppContext) {
cx.bind_keys([
KeyBinding::new("cmd-+", Increment, None),
KeyBinding::new("cmd--", Decrement, None),
KeyBinding::new("cmd-0", Reset, None),
]);
}
Element Composition
Builder Pattern
fn card(title: &str, content: impl IntoElement) -> impl IntoElement {
div()
.flex()
.flex_col()
.bg(white())
.border_1()
.rounded_lg()
.shadow_sm()
.p_6()
.child(
div()
.text_lg()
.font_semibold()
.mb_4()
.child(title)
)
.child(content)
}
Conditional Rendering
div()
.when(condition, |this| {
this.bg(blue_500())
})
.when_some(optional_value, |this, value| {
this.child(format!("Value: {}", value))
})
.map(|this| {
if complex_condition {
this.border_1()
} else {
this.border_2()
}
})
Dynamic Children
div()
.children(
items.iter().map(|item| {
div().child(item.name.as_str())
})
)
View Lifecycle
Initialization
impl MyView {
fn new(cx: &mut ViewContext<Self>) -> Self {
let model = cx.new_model(|_| MyState::default());
let subscription = cx.observe(&model, |_, _, cx| cx.notify());
cx.spawn(|this, mut cx| async move {
}).detach();
Self {
model,
_subscription: subscription,
}
}
}
Update Notifications
impl MyView {
fn update_state(&mut self, new_data: Data, cx: &mut ViewContext<Self>) {
self.model.update(cx, |state, cx| {
state.data = new_data;
cx.notify();
});
}
}
Cleanup
impl Drop for MyView {
fn drop(&mut self) {
}
}
Reactive Patterns
Derived State
impl Render for MyView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let state = self.model.read(cx);
let total = state.items.iter().map(|i| i.value).sum::<i32>();
let average = total / state.items.len() as i32;
div()
.child(format!("Total: {}", total))
.child(format!("Average: {}", average))
}
}
Async Updates
impl MyView {
fn load_data(&mut self, cx: &mut ViewContext<Self>) {
let model = self.model.clone();
cx.spawn(|_, mut cx| async move {
let data = fetch_data().await?;
cx.update_model(&model, |state, cx| {
state.data = data;
cx.notify();
})?;
Ok::<_, anyhow::Error>(())
}).detach();
}
}
Resources
Official Documentation
Common Patterns Reference
- Model-View: State management pattern
- Container-Presenter: Separation of concerns
- Compound Components: Related components working together
- Action System: Command pattern for user interactions
- Subscriptions: Observer pattern for reactive updates
Best Practices
- Store subscriptions to prevent cleanup
- Use
cx.notify() sparingly
- Prefer composition over inheritance
- Keep render methods pure
- Handle errors gracefully
- Document component APIs
- Test component behavior
Converted and distributed by TomeVault — claim your Tome and manage your conversions.