diff --git a/crates/gpui/examples/view_example/example_input.rs b/crates/gpui/examples/view_example/example_input.rs index c8b5b9ff59366fd857f2eaeff4aaa10a866ad090..551a4d33af5ee52be5d8c9fc282076242d8747df 100644 --- a/crates/gpui/examples/view_example/example_input.rs +++ b/crates/gpui/examples/view_example/example_input.rs @@ -1,36 +1,64 @@ //! The `ExampleInput` view — a single-line text input component. //! //! Composes `ExampleEditorText` inside a styled container with focus ring, border, -//! and action handlers. Implements the `View` trait with `#[derive(Hash)]` -//! so that prop changes (color, width) automatically invalidate the render -//! cache via `ViewElement::cached()`. +//! and action handlers. Implements the `View` trait backed by its own +//! `ExampleInputState` entity, giving it an independent caching boundary +//! from both its parent and the inner editor. use std::time::Duration; use gpui::{ - Animation, AnimationExt as _, App, BoxShadow, CursorStyle, Entity, Hsla, IntoViewElement, - Pixels, SharedString, StyleRefinement, ViewElement, Window, bounce, div, ease_in_out, hsla, - point, prelude::*, px, white, + Animation, AnimationExt as _, App, BoxShadow, Context, CursorStyle, Entity, FocusHandle, Hsla, + IntoViewElement, Pixels, SharedString, StyleRefinement, Subscription, ViewElement, Window, + bounce, div, ease_in_out, hsla, point, prelude::*, px, white, }; use crate::example_editor::ExampleEditor; use crate::{Backspace, Delete, End, Enter, Home, Left, Right}; -struct FlashState { - count: usize, +pub struct ExampleInputState { + editor: Entity, + focus_handle: FocusHandle, + is_focused: bool, + flash_count: usize, + _subscriptions: Vec, +} + +impl ExampleInputState { + pub fn new(window: &mut Window, cx: &mut Context) -> Self { + let editor = cx.new(|cx| ExampleEditor::new(cx)); + let focus_handle = editor.read(cx).focus_handle.clone(); + + let focus_sub = cx.on_focus(&focus_handle, window, |this, _window, cx| { + this.is_focused = true; + cx.notify(); + }); + let blur_sub = cx.on_blur(&focus_handle, window, |this, _window, cx| { + this.is_focused = false; + cx.notify(); + }); + + Self { + editor, + focus_handle, + is_focused: false, + flash_count: 0, + _subscriptions: vec![focus_sub, blur_sub], + } + } } #[derive(Hash, IntoViewElement)] pub struct ExampleInput { - editor: Entity, + state: Entity, width: Option, color: Option, } impl ExampleInput { - pub fn new(editor: Entity) -> Self { + pub fn new(state: Entity) -> Self { Self { - editor, + state, width: None, color: None, } @@ -48,10 +76,10 @@ impl ExampleInput { } impl gpui::View for ExampleInput { - type Entity = ExampleEditor; + type Entity = ExampleInputState; - fn entity(&self) -> Option> { - Some(self.editor.clone()) + fn entity(&self) -> Option> { + Some(self.state.clone()) } fn cache_style(&mut self, _window: &mut Window, _cx: &mut App) -> Option { @@ -63,15 +91,15 @@ impl gpui::View for ExampleInput { Some(style) } - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let flash_state = window.use_state(cx, |_window, _cx| FlashState { count: 0 }); - let count = flash_state.read(cx).count; - - let focus_handle = self.editor.read(cx).focus_handle.clone(); - let is_focused = focus_handle.is_focused(window); + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + let input_state = self.state.read(cx); + let count = input_state.flash_count; + let editor = input_state.editor.clone(); + let focus_handle = input_state.focus_handle.clone(); + let is_focused = input_state.is_focused; let text_color = self.color.unwrap_or(hsla(0., 0., 0.1, 1.)); let box_width = self.width.unwrap_or(px(300.)); - let editor = self.editor; + let state = self.state; let focused_border = hsla(220. / 360., 0.8, 0.5, 1.); let unfocused_border = hsla(0., 0., 0.75, 1.); @@ -124,10 +152,9 @@ impl gpui::View for ExampleInput { } }) .on_action({ - let flash_state = flash_state; move |_: &Enter, _window, cx| { - flash_state.update(cx, |state, cx| { - state.count += 1; + state.update(cx, |state, cx| { + state.flash_count += 1; cx.notify(); }); } diff --git a/crates/gpui/examples/view_example/example_text_area.rs b/crates/gpui/examples/view_example/example_text_area.rs index 8f5536c5680a5734eef1bcaead1e6fb04336f382..9bd53544d24e12c06d8a1f2bbbb1b665cc9fefd2 100644 --- a/crates/gpui/examples/view_example/example_text_area.rs +++ b/crates/gpui/examples/view_example/example_text_area.rs @@ -5,8 +5,8 @@ //! components with different props and layouts. use gpui::{ - App, BoxShadow, CursorStyle, Entity, Hsla, IntoViewElement, StyleRefinement, ViewElement, - Window, div, hsla, point, prelude::*, px, white, + App, BoxShadow, CursorStyle, Entity, Hsla, IntoViewElement, ViewElement, Window, div, hsla, + point, prelude::*, px, white, }; use crate::example_editor::ExampleEditor; @@ -34,22 +34,7 @@ impl ExampleTextArea { } } -impl gpui::View for ExampleTextArea { - type Entity = ExampleEditor; - - fn entity(&self) -> Option> { - Some(self.editor.clone()) - } - - fn cache_style(&mut self, _window: &mut Window, _cx: &mut App) -> Option { - let row_height = px(20.); - let box_height = row_height * self.rows as f32 + px(16.); - let mut style = StyleRefinement::default(); - style.size.width = Some(px(400.).into()); - style.size.height = Some(box_height.into()); - Some(style) - } - +impl gpui::ComponentView for ExampleTextArea { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let focus_handle = self.editor.read(cx).focus_handle.clone(); let is_focused = focus_handle.is_focused(window); diff --git a/crates/gpui/examples/view_example/plan.md b/crates/gpui/examples/view_example/plan.md index dfc25039abc1fc3d708fbd3530d27f3d8383a237..d911744079347957b1186eb1450765e1246df51d 100644 --- a/crates/gpui/examples/view_example/plan.md +++ b/crates/gpui/examples/view_example/plan.md @@ -4,10 +4,62 @@ - Introduced `View` trait, `ComponentView` trait, and `ViewElement` struct as a unification of `Component`, `RenderOnce`, `AnyView`, and `Render` - Initialized example of the composition this can achieve with the editor +- Made `ExampleInput` a proper `View` with its own `ExampleInputState` entity — demonstrates independent caching boundaries, focus tracking, and separation of concerns +- Made `ExampleTextArea` a `ComponentView` — demonstrates stateless wrapper pattern where the inner `EntityView` does the caching +- Analyzed `use_state` observer behavior — confirmed it's necessary and correct for component-local state (non-view entities aren't in the dispatch tree, so `mark_view_dirty` can't propagate) ## Next -- Add a render log showing coarse-grained caching (rather than existing spot-caching) -- Add tab index support so that the demo doesn't need mouse movement -- Move focus handles out to the input and textarea, and stop blinking when not focused -- De-fluff LLM generated code (remove excessive comments, simplify implementations, etc.) +- Add a render log showing coarse-grained caching — log `render()` calls on ExampleInput, ExampleTextArea, and ExampleEditor to show that sibling isolation works and FlashState only re-renders chrome +- RootView type — a window root that takes an `Entity` directly. Introduce in a backwards-compatible way alongside `open_window`. Use in this example. +- Move focus handles out to the input and textarea, stop blinking when not focused +- Tab index support so the demo doesn't need mouse movement +- De-fluff LLM generated code + +## Design Decisions + +### View as a separation-of-concerns boundary + +The `ExampleInput` View demonstrates the key principle: a View should own its concerns and delegate everything else. + +- `ExampleInputState` creates the editor internally (`cx.new(|cx| ExampleEditor::new(cx))`) — the parent never sees it +- Focus is tracked via `on_focus`/`on_blur` listeners on the state entity — render never reads the editor +- The editor is passed as a `ViewElement` child and trusted to manage its own rendering (blink, text, cursor) +- `render()` only reads from `self.state` (ExampleInputState), giving it a clean reactive boundary + +The parent just does: +``` +let input_state = window.use_state(cx, |window, cx| ExampleInputState::new(window, cx)); +ExampleInput::new(input_state).width(px(320.)).color(input_color) +``` + +### `use_state` as the allocation pattern for View entities + +`use_state` in the parent creates the View's backing entity. The View's `new()` is a pure constructor that takes the entity handle. The entity's `new()` can internally chain to `cx.new()` to create child entities (like the editor), keeping allocation hierarchical and self-contained. + +### `use_state` observer — keep as-is + +`use_state` creates an observer that notifies the parent view when the state entity changes. This is correct because: + +1. Non-view entities aren't in the dispatch tree, so `mark_view_dirty` can't propagate their notifications upward +2. `use_state` entities are component-local (single owner), so notifying the parent is always the right granularity +3. The alternative (fixing the tracking layer to auto-propagate all entity reads) would be catastrophic for large shared entities like `Project`, where dozens of views read it but only care about specific changes + +### Entity taxonomy for reactivity + +| Entity type | On notification | Mechanism | +|---|---|---| +| View entity (ExampleEditor) | Own ViewElement cache invalidates | `dirty_views` + dispatch tree | +| Component-local (use_state) | Parent view re-renders | `use_state` observer | +| Shared, universally relevant (Theme) | All readers re-render | Explicit `observe` today; `use_global` (v2) | +| Shared, selectively relevant (Project) | Only subscribers re-render | Explicit `subscribe` | + +### View vs ComponentView vs EntityView + +- **View** = has its own entity, gets caching, creates a reactive boundary. The component owns its state and delegates child rendering. Use when the component has state or when caching its render output matters for performance (ExampleInput). +- **ComponentView** = stateless wrapper, no caching, always re-renders when parent does. Use when the component is cheap and the inner child has its own cache (ExampleTextArea). +- **EntityView** = the entity IS the view. The data-owning entity renders itself directly. Use for entities that need full control over their element tree (ExampleEditor). + +### V2: `use_global` for universal reactivity + +A future `cx.use_global::()` API that auto-wires reactive dependencies for globally-shared "value" entities (theme, settings). Would eliminate dozens of boilerplate `observe_global` + `cx.notify()` callbacks throughout the codebase. diff --git a/crates/gpui/examples/view_example/view_example_main.rs b/crates/gpui/examples/view_example/view_example_main.rs index 04f616e76e254b4b349abf6e869832b571a8d4c6..f20a6e58d22d926cb07d2dbfc3bf91d1fe3b4440 100644 --- a/crates/gpui/examples/view_example/view_example_main.rs +++ b/crates/gpui/examples/view_example/view_example_main.rs @@ -41,7 +41,7 @@ use gpui::{ use gpui_platform::application; use example_editor::ExampleEditor; -use example_input::ExampleInput; +use example_input::{ExampleInput, ExampleInputState}; use example_text_area::ExampleTextArea; actions!( @@ -69,7 +69,7 @@ impl ViewExample { impl Render for ViewExample { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let input_editor = window.use_state(cx, |_window, cx| ExampleEditor::new(cx)); + let input_state = window.use_state(cx, |window, cx| ExampleInputState::new(window, cx)); let textarea_editor = window.use_state(cx, |_window, cx| ExampleEditor::new(cx)); let input_color = self.input_color; let textarea_color = self.textarea_color; @@ -87,13 +87,12 @@ impl Render for ViewExample { .flex_col() .gap(px(4.)) .child( - div() - .text_sm() - .text_color(hsla(0., 0., 0.3, 1.)) - .child("Single-line input (Input — View with cached ExampleEditorText)"), + div().text_sm().text_color(hsla(0., 0., 0.3, 1.)).child( + "Single-line input (Input — View with own state + cached editor)", + ), ) .child( - ExampleInput::new(input_editor) + ExampleInput::new(input_state) .width(px(320.)) .color(input_color), ), @@ -116,10 +115,12 @@ impl Render for ViewExample { .mt(px(12.)) .text_xs() .text_color(hsla(0., 0., 0.5, 1.)) - .child("• ExampleEditor entity owns state, blink task, EntityInputHandler") - .child("• ExampleEditorText element shapes text, paints cursor, wires handle_input") - .child("• Input / TextArea views compose ExampleEditorText with container styling") - .child("• ViewElement::cached() enables render caching via #[derive(Hash)]") + .child("• ExampleEditor entity owns text, cursor, blink (EntityView)") + .child("• ExampleInput is a View with its own state — caches independently") + .child( + "• ExampleTextArea is a ComponentView — stateless wrapper, editor caches", + ) + .child("• Press Enter in input to flash border (only chrome re-renders)") .child("• Entities created via window.use_state()"), ) }