Common GPUI patterns including component composition, state management strategies, event handling, and action dispatching...
This skill provides comprehensive guidance on common GPUI patterns and best practices for building maintainable, performant applications.
use gpui::*;
// View component with state
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 (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);
// Pass data to presenter
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())
}
}
// Parent component with shared context
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: &mut ViewContext<Self>) {
self.active_index = index;
cx.notify();
}
}
impl Render for Tabs {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.flex()
.flex_col()
.child(
// Tab headers
div()
.flex()
.children(
self.items.iter().enumerate().map(|(i, item)| {
tab_header(&item.label, i == self.active_index, || {
self.set_active(i, cx)
})
})
)
)
.child(
// Active tab content
(self.items[self.active_index].content)()
)
}
}
// Model: Application state
#[derive(Clone)]
struct AppState {
count: usize,
items: Vec<String>,
}
// View: Observes and renders state
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();
});
}
}
// Global state via context
#[derive(Clone)]
struct GlobalSettings {
theme: Theme,
language: String,
}
impl Global for GlobalSettings {}
// Initialize in app
fn init_app(cx: &mut AppContext) {
cx.set_global(GlobalSettings {
theme: Theme::Light,
language: "en".to_string(),
});
}
// Access in components
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))
}
}
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(); // Rerender on change
});
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);
// Only rerender if specific field changed
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,
}
}
}
div()
.on_click(cx.listener(|this, event: &ClickEvent, cx| {
// Handle click
this.handle_click(cx);
}))
.child("Click me")
div()
.on_key_down(cx.listener(|this, event: &KeyDownEvent, cx| {
match event.key.as_str() {
"Enter" => this.submit(cx),
"Escape" => this.cancel(cx),
_ => {}
}
}))
// Stop propagation
div()
.on_click(|event, cx| {
event.stop_propagation();
// Handle click
})
// Prevent default
div()
.on_key_down(|event, cx| {
if event.key == "Tab" {
event.prevent_default();
// Custom tab handling
}
})
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;
}))
use gpui::*;
actions!(app, [
Increment,
Decrement,
Reset,
SetValue
]);
// Action with data
#[derive(Clone, PartialEq)]
pub struct SetValue {
pub value: i32,
}
impl_actions!(app, [SetValue]);
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();
});
}));
}
}
// From within component
fn handle_button_click(&mut self, cx: &mut ViewContext<Self>) {
cx.dispatch_action(Increment);
}
// With data
fn set_specific_value(&mut self, value: i32, cx: &mut ViewContext<Self>) {
cx.dispatch_action(SetValue { value });
}
// Global action dispatch
cx.dispatch_action_on_window(Reset, window_id);
// Register global 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),
]);
}
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)
}
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()
}
})
div()
.children(
items.iter().map(|item| {
div().child(item.name.as_str())
})
)
impl MyView {
fn new(cx: &mut ViewContext<Self>) -> Self {
// Initialize state
let model = cx.new_model(|_| MyState::default());
// Set up subscriptions
let subscription = cx.observe(&model, |_, _, cx| cx.notify());
// Spawn async tasks
cx.spawn(|this, mut cx| async move {
// Async initialization
}).detach();
Self {
model,
_subscription: subscription,
}
}
}
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(); // Trigger rerender
});
}
}
impl Drop for MyView {
fn drop(&mut self) {
// Manual cleanup if needed
// Subscriptions are automatically dropped
}
}
impl Render for MyView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let state = self.model.read(cx);
// Compute derived values
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))
}
}
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();
}
}
cx.notify() sparingly