Refine View example: ExampleInput as View with own state, ExampleTextArea as ComponentView

Mikayla Maki created

ExampleInput is now a proper View backed by ExampleInputState, which:
- Creates and owns its editor entity internally (cx.new in state constructor)
- Tracks focus via on_focus/on_blur listeners on its own state entity
- Never reads the editor entity during render — only reads its own state
- Gets an independent caching boundary from both parent and child editor

ExampleTextArea is now a ComponentView (stateless wrapper) where the inner
EntityView does the caching. This demonstrates the cheaper pattern for
components that don't need their own reactive boundary.

The parent (ViewExample) allocates ExampleInputState via use_state, which
internally chains to cx.new() for the editor. ExampleInput::new() is a
pure constructor taking just the state handle.

Design analysis documented in plan.md:
- use_state observer is necessary (non-view entities aren't in dispatch tree)
- View vs ComponentView vs EntityView taxonomy with clear use cases
- Entity reactivity taxonomy (view, component-local, shared-universal, shared-selective)
- V2 use_global API concept for theme/settings auto-reactivity

Change summary

crates/gpui/examples/view_example/example_input.rs     | 75 ++++++++---
crates/gpui/examples/view_example/example_text_area.rs | 21 --
crates/gpui/examples/view_example/plan.md              | 60 ++++++++
crates/gpui/examples/view_example/view_example_main.rs | 23 +-
4 files changed, 122 insertions(+), 57 deletions(-)

Detailed changes

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<ExampleEditor>,
+    focus_handle: FocusHandle,
+    is_focused: bool,
+    flash_count: usize,
+    _subscriptions: Vec<Subscription>,
+}
+
+impl ExampleInputState {
+    pub fn new(window: &mut Window, cx: &mut Context<Self>) -> 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<ExampleEditor>,
+    state: Entity<ExampleInputState>,
     width: Option<Pixels>,
     color: Option<Hsla>,
 }
 
 impl ExampleInput {
-    pub fn new(editor: Entity<ExampleEditor>) -> Self {
+    pub fn new(state: Entity<ExampleInputState>) -> 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<Entity<ExampleEditor>> {
-        Some(self.editor.clone())
+    fn entity(&self) -> Option<Entity<ExampleInputState>> {
+        Some(self.state.clone())
     }
 
     fn cache_style(&mut self, _window: &mut Window, _cx: &mut App) -> Option<StyleRefinement> {
@@ -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();
                     });
                 }

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<Entity<ExampleEditor>> {
-        Some(self.editor.clone())
-    }
-
-    fn cache_style(&mut self, _window: &mut Window, _cx: &mut App) -> Option<StyleRefinement> {
-        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);

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<V>` 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::<ThemeSettings>()` 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.

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<Self>) -> 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()"),
             )
     }