@@ -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();
});
}
@@ -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);
@@ -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.
@@ -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()"),
)
}