* Update crate name * wip * reduce to just the gpui crate * Fix misisng example images * Delete ownership_post.rs * Update Cargo.toml * Update README.md * bump to 0.3.3 * organize * organize * Add more learn examples * Delete _ownership_and_data_flow.rs * wip learn examples * Fix colors * tidy * Move _template up * add animation example * Update examples * remove legacy examples from toml * Delete examples.toml
435 lines
14 KiB
Rust
435 lines
14 KiB
Rust
//! 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");
|
||
});
|
||
}
|