Source Code
//! Creating Components Example
//!
//! This example demonstrates three different approaches to creating interactive
//! stateful components in GPUI:
//!
//! 1. `use_state` - Hook-like state scoped to an element's lifetime
//! 2. `RenderOnce` - Stateless component that receives state from parent
//! 3. `Render` - Entity-backed view with persistent internal state
#[path = "../prelude.rs"]
mod example_prelude;
use example_prelude::init_example;
use gpui::{
App, Application, Bounds, Colors, Context, Entity, IntoElement, Render, RenderOnce, Window,
WindowBounds, WindowOptions, div, prelude::*, px, size,
};
// ============================================================================
// Approach 1: use_state
// ============================================================================
//
// `use_state` creates element-scoped state that persists across renders.
// It's similar to React's useState hook. The state is automatically tied
// to the element's identity via caller location or a provided key.
//
// Pros:
// - Simple, hook-like API
// - State is scoped to element lifetime
// - No boilerplate for simple state
//
// Cons:
// - Less explicit than Entity-backed state
// - State is tied to call site location
struct UseStateCounter {
count: i32,
}
fn use_state_counter(colors: &Colors, window: &mut Window, cx: &mut App) -> impl IntoElement {
let state: Entity<UseStateCounter> =
window.use_state(cx, |_window, _cx| UseStateCounter { count: 0 });
let count = state.read(cx).count;
let error = colors.error;
let error_hover = colors.error_hover;
let success = colors.success;
let success_hover = colors.success_hover;
div()
.id("use-state-counter")
.flex()
.flex_col()
.gap_2()
.p_4()
.rounded_lg()
.bg(colors.surface)
.child(
div()
.text_sm()
.text_color(colors.text_muted)
.child("use_state Counter"),
)
.child(
div()
.text_2xl()
.text_color(colors.text)
.child(format!("{}", count)),
)
.child(
div()
.flex()
.gap_2()
.child(
div()
.id("use-state-decrement")
.px_3()
.py_1()
.rounded_md()
.bg(error)
.text_color(colors.selected_text)
.cursor_pointer()
.hover(move |style| style.bg(error_hover))
.child("−")
.on_click({
let state = state.clone();
move |_, _, cx| {
state.update(cx, |state, cx| {
state.count -= 1;
cx.notify();
});
}
}),
)
.child(
div()
.id("use-state-increment")
.px_3()
.py_1()
.rounded_md()
.bg(success)
.text_color(colors.selected_text)
.cursor_pointer()
.hover(move |style| style.bg(success_hover))
.child("+")
.on_click(move |_, _, cx| {
state.update(cx, |state, cx| {
state.count += 1;
cx.notify();
});
}),
),
)
}
// ============================================================================
// Approach 2: RenderOnce
// ============================================================================
//
// `RenderOnce` components are stateless and consumed when rendered.
// They receive all data as props and delegate state management to the parent.
// This is the recommended approach for presentational components.
//
// Pros:
// - Clear data flow (props down, events up)
// - Lightweight (no Entity allocation)
// - Easy to test
// - Highly composable
//
// Cons:
// - Cannot maintain internal state
// - Parent must manage all state
#[derive(IntoElement)]
struct RenderOnceCounter {
colors: Colors,
count: i32,
on_increment: Option<Box<dyn Fn(&mut Window, &mut App) + 'static>>,
on_decrement: Option<Box<dyn Fn(&mut Window, &mut App) + 'static>>,
}
impl RenderOnceCounter {
fn new(colors: Colors, count: i32) -> Self {
Self {
colors,
count,
on_increment: None,
on_decrement: None,
}
}
fn on_increment(mut self, callback: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_increment = Some(Box::new(callback));
self
}
fn on_decrement(mut self, callback: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_decrement = Some(Box::new(callback));
self
}
}
impl RenderOnce for RenderOnceCounter {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
let colors = self.colors;
let error = colors.error;
let error_hover = colors.error_hover;
let success = colors.success;
let success_hover = colors.success_hover;
div()
.id("render-once-counter")
.flex()
.flex_col()
.gap_2()
.p_4()
.rounded_lg()
.bg(colors.surface)
.child(
div()
.text_sm()
.text_color(colors.text_muted)
.child("RenderOnce Counter"),
)
.child(
div()
.text_2xl()
.text_color(colors.text)
.child(format!("{}", self.count)),
)
.child(
div()
.flex()
.gap_2()
.child(
div()
.id("render-once-decrement")
.px_3()
.py_1()
.rounded_md()
.bg(error)
.text_color(colors.selected_text)
.cursor_pointer()
.hover(move |style| style.bg(error_hover))
.child("−")
.when_some(self.on_decrement, |element, callback| {
element.on_click(move |_, window, cx| callback(window, cx))
}),
)
.child(
div()
.id("render-once-increment")
.px_3()
.py_1()
.rounded_md()
.bg(success)
.text_color(colors.selected_text)
.cursor_pointer()
.hover(move |style| style.bg(success_hover))
.child("+")
.when_some(self.on_increment, |element, callback| {
element.on_click(move |_, window, cx| callback(window, cx))
}),
),
)
}
}
// ============================================================================
// Approach 3: Render (Entity-backed)
// ============================================================================
//
// `Render` components are backed by an `Entity<T>` and maintain their own
// internal state. This is the recommended approach for complex components
// that need to manage their own state, subscribe to events, or spawn tasks.
//
// Pros:
// - Full control over internal state
// - Can subscribe to events and observe other entities
// - Can spawn async tasks
// - Has identity (can be passed around as Entity<T>)
//
// Cons:
// - More boilerplate
// - Higher memory overhead
// - More complex lifecycle
struct RenderCounter {
count: i32,
}
impl RenderCounter {
fn new() -> Self {
Self { count: 0 }
}
fn increment(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
self.count += 1;
cx.notify();
}
fn decrement(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
self.count -= 1;
cx.notify();
}
}
impl Render for RenderCounter {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = Colors::for_appearance(window);
let error = colors.error;
let error_hover = colors.error_hover;
let success = colors.success;
let success_hover = colors.success_hover;
div()
.id("render-counter")
.flex()
.flex_col()
.gap_2()
.p_4()
.rounded_lg()
.bg(colors.surface)
.child(
div()
.text_sm()
.text_color(colors.text_muted)
.child("Render Counter"),
)
.child(
div()
.text_2xl()
.text_color(colors.text)
.child(format!("{}", self.count)),
)
.child(
div()
.flex()
.gap_2()
.child(
div()
.id("render-decrement")
.px_3()
.py_1()
.rounded_md()
.bg(error)
.text_color(colors.selected_text)
.cursor_pointer()
.hover(move |style| style.bg(error_hover))
.child("−")
.on_click(cx.listener(|this, _, window, cx| {
this.decrement(window, cx);
})),
)
.child(
div()
.id("render-increment")
.px_3()
.py_1()
.rounded_md()
.bg(success)
.text_color(colors.selected_text)
.cursor_pointer()
.hover(move |style| style.bg(success_hover))
.child("+")
.on_click(cx.listener(|this, _, window, cx| {
this.increment(window, cx);
})),
),
)
}
}
// ============================================================================
// Main Application
// ============================================================================
struct CreatingComponentsExample {
render_counter: Entity<RenderCounter>,
render_once_count: i32,
}
impl CreatingComponentsExample {
fn new(cx: &mut Context<Self>) -> Self {
Self {
render_counter: cx.new(|_| RenderCounter::new()),
render_once_count: 0,
}
}
}
impl Render for CreatingComponentsExample {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = Colors::for_appearance(window);
let render_once_count = self.render_once_count;
let handle = cx.entity().downgrade();
div()
.id("main")
.size_full()
.flex()
.flex_col()
.gap_6()
.p_8()
.bg(colors.background)
.overflow_scroll()
.child(
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.text_2xl()
.font_weight(gpui::FontWeight::BOLD)
.text_color(colors.text)
.child("Creating Components"),
)
.child(
div()
.text_sm()
.text_color(colors.text_muted)
.child("Three approaches to stateful components in GPUI"),
),
)
.child(
div()
.flex()
.flex_row()
.gap_4()
.child(use_state_counter(&colors, window, cx))
.child(
RenderOnceCounter::new(colors.clone(), render_once_count)
.on_increment({
let handle = handle.clone();
move |_window, cx| {
handle
.update(cx, |this, cx| {
this.render_once_count += 1;
cx.notify();
})
.ok();
}
})
.on_decrement(move |_window, cx| {
handle
.update(cx, |this, cx| {
this.render_once_count -= 1;
cx.notify();
})
.ok();
}),
)
.child(self.render_counter.clone()),
)
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(700.), px(400.)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|cx| CreatingComponentsExample::new(cx)),
)
.expect("Failed to open window");
init_example(cx, "Creating Components");
});
}