Add View, ComponentView traits and ViewElement for unified component model

Mikayla Maki created

Introduce View trait with Option<Entity> for stateful/stateless branching,
ComponentView trait with blanket View impl for RenderOnce-style components,
and ViewElement that handles both cached (reactive boundary) and uncached
(type-name isolation) paths.

Rename text_views example to view_example with ExampleEditor/Input/TextArea.

Change summary

crates/gpui/Cargo.toml                                 |   4 
crates/gpui/examples/view_example/example_editor.rs    |  58 +-
crates/gpui/examples/view_example/example_input.rs     |  30 
crates/gpui/examples/view_example/example_tests.rs     |  28 
crates/gpui/examples/view_example/example_text_area.rs |  28 
crates/gpui/examples/view_example/plan.md              |  13 
crates/gpui/examples/view_example/view_example_main.rs |  54 +
crates/gpui/src/view.rs                                | 331 +++++++----
crates/gpui_macros/src/derive_into_view_element.rs     |   7 
9 files changed, 319 insertions(+), 234 deletions(-)

Detailed changes

crates/gpui/Cargo.toml 🔗

@@ -240,5 +240,5 @@ name = "mouse_pressure"
 path = "examples/mouse_pressure.rs"
 
 [[example]]
-name = "text_views"
-path = "examples/text_views/main.rs"
+name = "view_example"
+path = "examples/view_example/view_example_main.rs"

crates/gpui/examples/text_views/editor.rs → crates/gpui/examples/view_example/example_editor.rs 🔗

@@ -1,9 +1,9 @@
-//! The `Editor` entity — owns the truth about text content, cursor position,
+//! The `ExampleEditor` entity — owns the truth about text content, cursor position,
 //! blink state, and keyboard handling.
 //!
-//! Also contains `EditorText`, the low-level custom `Element` that shapes text
-//! and paints the cursor, and `EditorView`, a cached `View` wrapper that
-//! automatically pairs an `Editor` entity with its `EditorText` element.
+//! Also contains `ExampleEditorText`, the low-level custom `Element` that shapes text
+//! and paints the cursor, and `ExampleEditorView`, a cached `View` wrapper that
+//! automatically pairs an `ExampleEditor` entity with its `ExampleEditorText` element.
 
 use std::hash::Hash;
 use std::ops::Range;
@@ -18,7 +18,7 @@ use unicode_segmentation::*;
 
 use crate::{Backspace, Delete, End, Home, Left, Right};
 
-pub struct Editor {
+pub struct ExampleEditor {
     pub focus_handle: FocusHandle,
     pub content: String,
     pub cursor: usize,
@@ -26,7 +26,7 @@ pub struct Editor {
     _blink_task: Task<()>,
 }
 
-impl Editor {
+impl ExampleEditor {
     pub fn new(cx: &mut Context<Self>) -> Self {
         let blink_task = Self::spawn_blink_task(cx);
 
@@ -165,13 +165,13 @@ impl Editor {
     }
 }
 
-impl Focusable for Editor {
+impl Focusable for ExampleEditor {
     fn focus_handle(&self, _cx: &App) -> FocusHandle {
         self.focus_handle.clone()
     }
 }
 
-impl EntityInputHandler for Editor {
+impl EntityInputHandler for ExampleEditor {
     fn text_for_range(
         &mut self,
         range_utf16: Range<usize>,
@@ -258,26 +258,26 @@ impl EntityInputHandler for Editor {
 }
 
 // ---------------------------------------------------------------------------
-// EditorText — custom Element that shapes text & paints the cursor
+// ExampleEditorText — custom Element that shapes text & paints the cursor
 // ---------------------------------------------------------------------------
 
-struct EditorText {
-    editor: Entity<Editor>,
+struct ExampleEditorText {
+    editor: Entity<ExampleEditor>,
     text_color: Hsla,
 }
 
-struct EditorTextPrepaintState {
+struct ExampleEditorTextPrepaintState {
     lines: Vec<ShapedLine>,
     cursor: Option<PaintQuad>,
 }
 
-impl EditorText {
-    pub fn new(editor: Entity<Editor>, text_color: Hsla) -> Self {
+impl ExampleEditorText {
+    pub fn new(editor: Entity<ExampleEditor>, text_color: Hsla) -> Self {
         Self { editor, text_color }
     }
 }
 
-impl IntoElement for EditorText {
+impl IntoElement for ExampleEditorText {
     type Element = Self;
 
     fn into_element(self) -> Self::Element {
@@ -285,9 +285,9 @@ impl IntoElement for EditorText {
     }
 }
 
-impl Element for EditorText {
+impl Element for ExampleEditorText {
     type RequestLayoutState = ();
-    type PrepaintState = EditorTextPrepaintState;
+    type PrepaintState = ExampleEditorTextPrepaintState;
 
     fn id(&self) -> Option<gpui::ElementId> {
         None
@@ -395,7 +395,7 @@ impl Element for EditorText {
             None
         };
 
-        EditorTextPrepaintState {
+        ExampleEditorTextPrepaintState {
             lines: shaped_lines,
             cursor,
         }
@@ -448,20 +448,20 @@ fn cursor_line_and_offset(content: &str, cursor: usize) -> (usize, usize) {
 }
 
 // ---------------------------------------------------------------------------
-// EditorView — a cached View that pairs an Editor entity with EditorText
+// ExampleEditorView — a cached View that pairs an ExampleEditor entity with ExampleEditorText
 // ---------------------------------------------------------------------------
 
-/// A simple cached view that renders an `Editor` entity via the `EditorText`
+/// A simple cached view that renders an `ExampleEditor` entity via the `ExampleEditorText`
 /// custom element. Use this when you want a bare editor display with automatic
 /// caching and no extra chrome.
 #[derive(IntoViewElement, Hash)]
-pub struct EditorView {
-    editor: Entity<Editor>,
+pub struct ExampleEditorView {
+    editor: Entity<ExampleEditor>,
     text_color: Hsla,
 }
 
-impl EditorView {
-    pub fn new(editor: Entity<Editor>) -> Self {
+impl ExampleEditorView {
+    pub fn new(editor: Entity<ExampleEditor>) -> Self {
         Self {
             editor,
             text_color: hsla(0., 0., 0.1, 1.),
@@ -474,14 +474,14 @@ impl EditorView {
     }
 }
 
-impl gpui::View for EditorView {
-    type State = Editor;
+impl gpui::View for ExampleEditorView {
+    type Entity = ExampleEditor;
 
-    fn entity(&self) -> &Entity<Editor> {
-        &self.editor
+    fn entity(&self) -> Option<Entity<ExampleEditor>> {
+        Some(self.editor.clone())
     }
 
     fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
-        EditorText::new(self.editor, self.text_color)
+        ExampleEditorText::new(self.editor, self.text_color)
     }
 }

crates/gpui/examples/text_views/input.rs → crates/gpui/examples/view_example/example_input.rs 🔗

@@ -1,6 +1,6 @@
-//! The `Input` view — a single-line text input component.
+//! The `ExampleInput` view — a single-line text input component.
 //!
-//! Composes `EditorText` inside a styled container with focus ring, border,
+//! 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()`.
@@ -13,8 +13,8 @@ use gpui::{
     prelude::*, px, white,
 };
 
-use crate::editor::Editor;
-use crate::editor::EditorView;
+use crate::example_editor::ExampleEditor;
+use crate::example_editor::ExampleEditorView;
 use crate::{Backspace, Delete, End, Enter, Home, Left, Right};
 
 struct FlashState {
@@ -22,14 +22,14 @@ struct FlashState {
 }
 
 #[derive(Hash, IntoViewElement)]
-pub struct Input {
-    editor: Entity<Editor>,
+pub struct ExampleInput {
+    editor: Entity<ExampleEditor>,
     width: Option<Pixels>,
     color: Option<Hsla>,
 }
 
-impl Input {
-    pub fn new(editor: Entity<Editor>) -> Self {
+impl ExampleInput {
+    pub fn new(editor: Entity<ExampleEditor>) -> Self {
         Self {
             editor,
             width: None,
@@ -48,11 +48,11 @@ impl Input {
     }
 }
 
-impl gpui::View for Input {
-    type State = Editor;
+impl gpui::View for ExampleInput {
+    type Entity = ExampleEditor;
 
-    fn entity(&self) -> &Entity<Editor> {
-        &self.editor
+    fn entity(&self) -> Option<Entity<ExampleEditor>> {
+        Some(self.editor.clone())
     }
 
     fn style(&self) -> Option<StyleRefinement> {
@@ -72,7 +72,7 @@ impl gpui::View for Input {
         let is_focused = focus_handle.is_focused(window);
         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.clone();
+        let editor = self.editor;
 
         let focused_border = hsla(220. / 360., 0.8, 0.5, 1.);
         let unfocused_border = hsla(0., 0., 0.75, 1.);
@@ -125,7 +125,7 @@ impl gpui::View for Input {
                 }
             })
             .on_action({
-                let flash_state = flash_state.clone();
+                let flash_state = flash_state;
                 move |_: &Enter, _window, cx| {
                     flash_state.update(cx, |state, cx| {
                         state.count += 1;
@@ -154,7 +154,7 @@ impl gpui::View for Input {
             .line_height(px(20.))
             .text_size(px(14.))
             .text_color(text_color)
-            .child(EditorView::new(editor).text_color(text_color));
+            .child(ExampleEditorView::new(editor).text_color(text_color));
 
         if count > 0 {
             base.with_animation(

crates/gpui/examples/text_views/editor_test.rs → crates/gpui/examples/view_example/example_tests.rs 🔗

@@ -1,4 +1,4 @@
-//! Tests for the `Editor` entity.
+//! Tests for the `ExampleEditor` entity.
 //!
 //! These use GPUI's test infrastructure which requires the `test-support` feature:
 //!
@@ -12,28 +12,28 @@ mod tests {
 
     use gpui::{Context, Entity, KeyBinding, TestAppContext, Window, prelude::*};
 
-    use crate::editor::Editor;
-    use crate::input::Input;
-    use crate::text_area::TextArea;
+    use crate::example_editor::ExampleEditor;
+    use crate::example_input::ExampleInput;
+    use crate::example_text_area::ExampleTextArea;
     use crate::{Backspace, Delete, End, Enter, Home, Left, Right};
 
     struct InputWrapper {
-        editor: Entity<Editor>,
+        editor: Entity<ExampleEditor>,
     }
 
     impl Render for InputWrapper {
         fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
-            Input::new(self.editor.clone())
+            ExampleInput::new(self.editor.clone())
         }
     }
 
     struct TextAreaWrapper {
-        editor: Entity<Editor>,
+        editor: Entity<ExampleEditor>,
     }
 
     impl Render for TextAreaWrapper {
         fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
-            TextArea::new(self.editor.clone(), 5)
+            ExampleTextArea::new(self.editor.clone(), 5)
         }
     }
 
@@ -51,11 +51,13 @@ mod tests {
         });
     }
 
-    fn init_input(cx: &mut TestAppContext) -> (Entity<Editor>, &mut gpui::VisualTestContext) {
+    fn init_input(
+        cx: &mut TestAppContext,
+    ) -> (Entity<ExampleEditor>, &mut gpui::VisualTestContext) {
         bind_keys(cx);
 
         let (wrapper, cx) = cx.add_window_view(|_window, cx| {
-            let editor = cx.new(|cx| Editor::new(cx));
+            let editor = cx.new(|cx| ExampleEditor::new(cx));
             InputWrapper { editor }
         });
 
@@ -69,11 +71,13 @@ mod tests {
         (editor, cx)
     }
 
-    fn init_textarea(cx: &mut TestAppContext) -> (Entity<Editor>, &mut gpui::VisualTestContext) {
+    fn init_textarea(
+        cx: &mut TestAppContext,
+    ) -> (Entity<ExampleEditor>, &mut gpui::VisualTestContext) {
         bind_keys(cx);
 
         let (wrapper, cx) = cx.add_window_view(|_window, cx| {
-            let editor = cx.new(|cx| Editor::new(cx));
+            let editor = cx.new(|cx| ExampleEditor::new(cx));
             TextAreaWrapper { editor }
         });
 

crates/gpui/examples/text_views/text_area.rs → crates/gpui/examples/view_example/example_text_area.rs 🔗

@@ -1,6 +1,6 @@
-//! The `TextArea` view — a multi-line text area component.
+//! The `ExampleTextArea` view — a multi-line text area component.
 //!
-//! Same `Editor` entity, different presentation: taller box with configurable
+//! Same `ExampleEditor` entity, different presentation: taller box with configurable
 //! row count. Demonstrates that the same entity type can back different `View`
 //! components with different props and layouts.
 
@@ -9,19 +9,19 @@ use gpui::{
     point, prelude::*, px, white,
 };
 
-use crate::editor::Editor;
-use crate::editor::EditorView;
+use crate::example_editor::ExampleEditor;
+use crate::example_editor::ExampleEditorView;
 use crate::{Backspace, Delete, End, Enter, Home, Left, Right};
 
 #[derive(Hash, IntoViewElement)]
-pub struct TextArea {
-    editor: Entity<Editor>,
+pub struct ExampleTextArea {
+    editor: Entity<ExampleEditor>,
     rows: usize,
     color: Option<Hsla>,
 }
 
-impl TextArea {
-    pub fn new(editor: Entity<Editor>, rows: usize) -> Self {
+impl ExampleTextArea {
+    pub fn new(editor: Entity<ExampleEditor>, rows: usize) -> Self {
         Self {
             editor,
             rows,
@@ -35,11 +35,11 @@ impl TextArea {
     }
 }
 
-impl gpui::View for TextArea {
-    type State = Editor;
+impl gpui::View for ExampleTextArea {
+    type Entity = ExampleEditor;
 
-    fn entity(&self) -> &Entity<Editor> {
-        &self.editor
+    fn entity(&self) -> Option<Entity<ExampleEditor>> {
+        Some(self.editor.clone())
     }
 
     fn style(&self) -> Option<StyleRefinement> {
@@ -57,7 +57,7 @@ impl gpui::View for TextArea {
         let text_color = self.color.unwrap_or(hsla(0., 0., 0.1, 1.));
         let row_height = px(20.);
         let box_height = row_height * self.rows as f32 + px(16.);
-        let editor = self.editor.clone();
+        let editor = self.editor;
 
         div()
             .id("text-area")
@@ -129,6 +129,6 @@ impl gpui::View for TextArea {
             .line_height(row_height)
             .text_size(px(14.))
             .text_color(text_color)
-            .child(EditorView::new(editor).text_color(text_color))
+            .child(ExampleEditorView::new(editor).text_color(text_color))
     }
 }

crates/gpui/examples/view_example/plan.md 🔗

@@ -0,0 +1,13 @@
+# View Example — Plan
+
+## Done
+
+- 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
+
+## 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.)

crates/gpui/examples/text_views/main.rs → crates/gpui/examples/view_example/view_example_main.rs 🔗

@@ -1,6 +1,6 @@
 #![cfg_attr(target_family = "wasm", no_main)]
 
-//! **text_views** — an end-to-end GPUI example demonstrating how Entity,
+//! **view_example** — an end-to-end GPUI example demonstrating how Entity,
 //! Element, View, and Render compose together to build rich text components.
 //!
 //! ## Architecture
@@ -11,28 +11,28 @@
 //! |-----------------|---------|----------------------------------------------------------|
 //! | `editor`        | Entity  | Owns text, cursor, blink task, `EntityInputHandler`      |
 //! | `editor_text`   | Element | Shapes text, paints cursor, wires `handle_input`         |
-//! | `input`         | View    | Single-line input — composes `EditorText` with styling   |
+//! | `input`         | View    | Single-line input — composes `ExampleEditorText` with styling   |
 //! | `text_area`     | View    | Multi-line text area — same entity, different layout      |
 //! | `main` (here)   | Render  | Root view — creates entities with `use_state`, assembles  |
 //!
 //! ## Running
 //!
 //! ```sh
-//! cargo run --example text_views -p gpui
+//! cargo run --example view_example -p gpui
 //! ```
 //!
 //! ## Testing
 //!
 //! ```sh
-//! cargo test --example text_views -p gpui
+//! cargo test --example view_example -p gpui
 //! ```
 
-mod editor;
-mod input;
-mod text_area;
+mod example_editor;
+mod example_input;
+mod example_text_area;
 
 #[cfg(test)]
-mod editor_test;
+mod example_tests;
 
 use gpui::{
     App, Bounds, Context, Hsla, KeyBinding, Window, WindowBounds, WindowOptions, actions, div,
@@ -40,25 +40,25 @@ use gpui::{
 };
 use gpui_platform::application;
 
-use editor::Editor;
-use input::Input;
-use text_area::TextArea;
+use example_editor::ExampleEditor;
+use example_input::ExampleInput;
+use example_text_area::ExampleTextArea;
 
 actions!(
-    text_views,
+    view_example,
     [Backspace, Delete, Left, Right, Home, End, Enter, Quit,]
 );
 
 // ---------------------------------------------------------------------------
-// Example — the root view using `Render` and `window.use_state()`
+// ViewExample — the root view using `Render` and `window.use_state()`
 // ---------------------------------------------------------------------------
 
-struct Example {
+struct ViewExample {
     input_color: Hsla,
     textarea_color: Hsla,
 }
 
-impl Example {
+impl ViewExample {
     fn new() -> Self {
         Self {
             input_color: hsla(0., 0., 0.1, 1.),
@@ -67,10 +67,10 @@ impl Example {
     }
 }
 
-impl Render for Example {
+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| Editor::new(cx));
-        let textarea_editor = window.use_state(cx, |_window, cx| Editor::new(cx));
+        let input_editor = window.use_state(cx, |_window, cx| ExampleEditor::new(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;
 
@@ -90,9 +90,13 @@ impl Render for Example {
                         div()
                             .text_sm()
                             .text_color(hsla(0., 0., 0.3, 1.))
-                            .child("Single-line input (Input — View with cached EditorText)"),
+                            .child("Single-line input (Input — View with cached ExampleEditorText)"),
                     )
-                    .child(Input::new(input_editor).width(px(320.)).color(input_color)),
+                    .child(
+                        ExampleInput::new(input_editor)
+                            .width(px(320.))
+                            .color(input_color),
+                    ),
             )
             .child(
                 div()
@@ -102,7 +106,7 @@ impl Render for Example {
                     .child(div().text_sm().text_color(hsla(0., 0., 0.3, 1.)).child(
                         "Multi-line text area (TextArea — same entity type, different View)",
                     ))
-                    .child(TextArea::new(textarea_editor, 5).color(textarea_color)),
+                    .child(ExampleTextArea::new(textarea_editor, 5).color(textarea_color)),
             )
             .child(
                 div()
@@ -112,9 +116,9 @@ impl Render for Example {
                     .mt(px(12.))
                     .text_xs()
                     .text_color(hsla(0., 0., 0.5, 1.))
-                    .child("• Editor entity owns state, blink task, EntityInputHandler")
-                    .child("• EditorText element shapes text, paints cursor, wires handle_input")
-                    .child("• Input / TextArea views compose EditorText with container styling")
+                    .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("• Entities created via window.use_state()"),
             )
@@ -144,7 +148,7 @@ fn run_example() {
                 window_bounds: Some(WindowBounds::Windowed(bounds)),
                 ..Default::default()
             },
-            |_, cx| cx.new(|_| Example::new()),
+            |_, cx| cx.new(|_| ViewExample::new()),
         )
         .unwrap();
 

crates/gpui/src/view.rs 🔗

@@ -346,10 +346,10 @@ mod any_view {
 /// }
 ///
 /// impl View for Counter {
-///     type State = CounterState;
+///     type Entity = CounterState;
 ///
-///     fn entity(&self) -> &Entity<CounterState> {
-///         &self.state
+///     fn entity(&self) -> Option<Entity<CounterState>> {
+///         Some(self.state.clone())
 ///     }
 ///
 ///
@@ -364,10 +364,16 @@ mod any_view {
 /// ```
 pub trait View: 'static + Sized + Hash {
     /// The entity type that backs this view's state.
-    type State: 'static;
+    type Entity: 'static;
 
-    /// Returns a reference to the entity that backs this view.
-    fn entity(&self) -> &Entity<Self::State>;
+    /// Returns the entity that backs this view, if any.
+    ///
+    /// When `Some`, the view creates a reactive boundary in the element tree —
+    /// `cx.notify()` on the entity only re-renders this view's subtree.
+    ///
+    /// When `None`, the view behaves like a stateless component with subtree
+    /// isolation via its type name (similar to [`RenderOnce`](crate::RenderOnce)).
+    fn entity(&self) -> Option<Entity<Self::Entity>>;
 
     /// Render this view into an element tree. Takes ownership of self,
     /// consuming the component props. The entity state persists across frames.
@@ -385,6 +391,49 @@ pub trait View: 'static + Sized + Hash {
     }
 }
 
+/// A stateless component that renders an element tree without an entity.
+///
+/// This is the `View` equivalent of [`RenderOnce`](crate::RenderOnce). Types that
+/// implement `ComponentView` get a blanket implementation of [`View`] with
+/// `entity()` returning `None` and `style()` returning `None` — meaning no
+/// reactive boundary, no caching, just subtree isolation via the type name.
+///
+/// # Example
+///
+/// ```ignore
+/// #[derive(Hash, IntoViewElement)]
+/// struct Greeting {
+///     name: SharedString,
+/// }
+///
+/// impl ComponentView for Greeting {
+///     fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
+///         div().child(format!("Hello, {}!", self.name))
+///     }
+/// }
+/// ```
+pub trait ComponentView: 'static + Sized + Hash {
+    /// Render this component into an element tree. Takes ownership of self,
+    /// consuming the component props.
+    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement;
+}
+
+impl<T: ComponentView> View for T {
+    type Entity = ();
+
+    fn entity(&self) -> Option<Entity<()>> {
+        None
+    }
+
+    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
+        ComponentView::render(self, window, cx)
+    }
+
+    fn style(&self) -> Option<StyleRefinement> {
+        None
+    }
+}
+
 /// An element that wraps a [`View`], creating a reactive boundary in the element tree.
 ///
 /// This is the stateful counterpart to [`Component`](crate::Component) — where `Component<C>`
@@ -405,7 +454,7 @@ pub trait View: 'static + Sized + Hash {
 #[doc(hidden)]
 pub struct ViewElement<V: View> {
     view: Option<V>,
-    entity_id: EntityId,
+    entity_id: Option<EntityId>,
     props_hash: u64,
     cached_style: Option<StyleRefinement>,
     #[cfg(debug_assertions)]
@@ -419,31 +468,20 @@ impl<V: View> ViewElement<V> {
     #[track_caller]
     pub fn new(view: V) -> Self {
         use std::hash::Hasher;
-        let entity_id = view.entity().entity_id();
+        let entity_id = view.entity().map(|e| e.entity_id());
+        let cached_style = view.style();
         let mut hasher = std::collections::hash_map::DefaultHasher::new();
         view.hash(&mut hasher);
         let props_hash = hasher.finish();
         ViewElement {
             entity_id,
             props_hash,
-            cached_style: None,
+            cached_style,
             view: Some(view),
             #[cfg(debug_assertions)]
             source: core::panic::Location::caller(),
         }
     }
-
-    /// Enable caching for this view element. When cached, the view's render,
-    /// prepaint, and paint will all be reused from the previous frame if the
-    /// entity hasn't been notified and the props hash hasn't changed.
-    ///
-    /// The provided style defines the outer layout of this view in its parent
-    /// (since the actual content render is deferred until we know if caching
-    /// can be used).
-    pub fn cached(mut self, style: StyleRefinement) -> Self {
-        self.cached_style = Some(style);
-        self
-    }
 }
 
 impl<V: View> IntoElement for ViewElement<V> {
@@ -473,7 +511,7 @@ impl<V: View> Element for ViewElement<V> {
     type PrepaintState = Option<AnyElement>;
 
     fn id(&self) -> Option<ElementId> {
-        Some(ElementId::View(self.entity_id))
+        self.entity_id.map(ElementId::View)
     }
 
     fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
@@ -491,19 +529,34 @@ impl<V: View> Element for ViewElement<V> {
         window: &mut Window,
         cx: &mut App,
     ) -> (LayoutId, Self::RequestLayoutState) {
-        window.with_rendered_view(self.entity_id, |window| {
-            let caching_disabled = window.is_inspector_picking(cx);
-            match self.cached_style.as_ref() {
-                // Cached path: defer render, use style for layout.
-                // Render will happen in prepaint only on cache miss.
-                Some(style) if !caching_disabled => {
-                    let mut root_style = Style::default();
-                    root_style.refine(style);
-                    let layout_id = window.request_layout(root_style, None, cx);
-                    (layout_id, None)
+        if let Some(entity_id) = self.entity_id {
+            // Stateful path: create a reactive boundary.
+            window.with_rendered_view(entity_id, |window| {
+                let caching_disabled = window.is_inspector_picking(cx);
+                match self.cached_style.as_ref() {
+                    Some(style) if !caching_disabled => {
+                        let mut root_style = Style::default();
+                        root_style.refine(style);
+                        let layout_id = window.request_layout(root_style, None, cx);
+                        (layout_id, None)
+                    }
+                    _ => {
+                        let mut element = self
+                            .view
+                            .take()
+                            .unwrap()
+                            .render(window, cx)
+                            .into_any_element();
+                        let layout_id = element.request_layout(window, cx);
+                        (layout_id, Some(element))
+                    }
                 }
-                // Non-cached path: render eagerly.
-                _ => {
+            })
+        } else {
+            // Stateless path: isolate subtree via type name (like Component<C>).
+            window.with_id(
+                ElementId::Name(std::any::type_name::<V>().into()),
+                |window| {
                     let mut element = self
                         .view
                         .take()
@@ -512,9 +565,9 @@ impl<V: View> Element for ViewElement<V> {
                         .into_any_element();
                     let layout_id = element.request_layout(window, cx);
                     (layout_id, Some(element))
-                }
-            }
-        })
+                },
+            )
+        }
     }
 
     fn prepaint(
@@ -526,76 +579,83 @@ impl<V: View> Element for ViewElement<V> {
         window: &mut Window,
         cx: &mut App,
     ) -> Option<AnyElement> {
-        window.set_view_id(self.entity_id);
-        window.with_rendered_view(self.entity_id, |window| {
-            // Non-cached path: element was rendered in request_layout, just prepaint it.
-            if let Some(mut element) = element.take() {
-                element.prepaint(window, cx);
-                return Some(element);
-            }
+        if let Some(entity_id) = self.entity_id {
+            // Stateful path.
+            window.set_view_id(entity_id);
+            window.with_rendered_view(entity_id, |window| {
+                if let Some(mut element) = element.take() {
+                    element.prepaint(window, cx);
+                    return Some(element);
+                }
 
-            // Cached path: check cache, render only on miss.
-            window.with_element_state::<ViewElementState, _>(
-                global_id.unwrap(),
-                |element_state, window| {
-                    let content_mask = window.content_mask();
-                    let text_style = window.text_style();
+                window.with_element_state::<ViewElementState, _>(
+                    global_id.unwrap(),
+                    |element_state, window| {
+                        let content_mask = window.content_mask();
+                        let text_style = window.text_style();
+
+                        if let Some(mut element_state) = element_state
+                            && element_state.cache_key.bounds == bounds
+                            && element_state.cache_key.content_mask == content_mask
+                            && element_state.cache_key.text_style == text_style
+                            && element_state.cache_key.props_hash == self.props_hash
+                            && !window.dirty_views.contains(&entity_id)
+                            && !window.refreshing
+                        {
+                            let prepaint_start = window.prepaint_index();
+                            window.reuse_prepaint(element_state.prepaint_range.clone());
+                            cx.entities
+                                .extend_accessed(&element_state.accessed_entities);
+                            let prepaint_end = window.prepaint_index();
+                            element_state.prepaint_range = prepaint_start..prepaint_end;
+
+                            return (None, element_state);
+                        }
 
-                    // Cache hit: entity clean, props unchanged, bounds stable.
-                    // Skip render, prepaint, and paint entirely.
-                    if let Some(mut element_state) = element_state
-                        && element_state.cache_key.bounds == bounds
-                        && element_state.cache_key.content_mask == content_mask
-                        && element_state.cache_key.text_style == text_style
-                        && element_state.cache_key.props_hash == self.props_hash
-                        && !window.dirty_views.contains(&self.entity_id)
-                        && !window.refreshing
-                    {
+                        let refreshing = mem::replace(&mut window.refreshing, true);
                         let prepaint_start = window.prepaint_index();
-                        window.reuse_prepaint(element_state.prepaint_range.clone());
-                        cx.entities
-                            .extend_accessed(&element_state.accessed_entities);
-                        let prepaint_end = window.prepaint_index();
-                        element_state.prepaint_range = prepaint_start..prepaint_end;
+                        let (mut element, accessed_entities) = cx.detect_accessed_entities(|cx| {
+                            let mut element = self
+                                .view
+                                .take()
+                                .unwrap()
+                                .render(window, cx)
+                                .into_any_element();
+                            element.layout_as_root(bounds.size.into(), window, cx);
+                            element.prepaint_at(bounds.origin, window, cx);
+                            element
+                        });
 
-                        return (None, element_state);
-                    }
-
-                    // Cache miss: render now, layout as root, prepaint.
-                    let refreshing = mem::replace(&mut window.refreshing, true);
-                    let prepaint_start = window.prepaint_index();
-                    let (mut element, accessed_entities) = cx.detect_accessed_entities(|cx| {
-                        let mut element = self
-                            .view
-                            .take()
-                            .unwrap()
-                            .render(window, cx)
-                            .into_any_element();
-                        element.layout_as_root(bounds.size.into(), window, cx);
-                        element.prepaint_at(bounds.origin, window, cx);
-                        element
-                    });
-
-                    let prepaint_end = window.prepaint_index();
-                    window.refreshing = refreshing;
-
-                    (
-                        Some(element),
-                        ViewElementState {
-                            accessed_entities,
-                            prepaint_range: prepaint_start..prepaint_end,
-                            paint_range: PaintIndex::default()..PaintIndex::default(),
-                            cache_key: ViewElementCacheKey {
-                                bounds,
-                                content_mask,
-                                text_style,
-                                props_hash: self.props_hash,
+                        let prepaint_end = window.prepaint_index();
+                        window.refreshing = refreshing;
+
+                        (
+                            Some(element),
+                            ViewElementState {
+                                accessed_entities,
+                                prepaint_range: prepaint_start..prepaint_end,
+                                paint_range: PaintIndex::default()..PaintIndex::default(),
+                                cache_key: ViewElementCacheKey {
+                                    bounds,
+                                    content_mask,
+                                    text_style,
+                                    props_hash: self.props_hash,
+                                },
                             },
-                        },
-                    )
+                        )
+                    },
+                )
+            })
+        } else {
+            // Stateless path: just prepaint the element.
+            window.with_id(
+                ElementId::Name(std::any::type_name::<V>().into()),
+                |window| {
+                    element.as_mut().unwrap().prepaint(window, cx);
                 },
-            )
-        })
+            );
+            Some(element.take().unwrap())
+        }
     }
 
     fn paint(
@@ -608,36 +668,45 @@ impl<V: View> Element for ViewElement<V> {
         window: &mut Window,
         cx: &mut App,
     ) {
-        window.with_rendered_view(self.entity_id, |window| {
-            let caching_disabled = window.is_inspector_picking(cx);
-            if self.cached_style.is_some() && !caching_disabled {
-                // Cached path: check if we rendered or reused.
-                window.with_element_state::<ViewElementState, _>(
-                    global_id.unwrap(),
-                    |element_state, window| {
-                        let mut element_state = element_state.unwrap();
-
-                        let paint_start = window.paint_index();
-
-                        if let Some(element) = element {
-                            let refreshing = mem::replace(&mut window.refreshing, true);
-                            element.paint(window, cx);
-                            window.refreshing = refreshing;
-                        } else {
-                            window.reuse_paint(element_state.paint_range.clone());
-                        }
-
-                        let paint_end = window.paint_index();
-                        element_state.paint_range = paint_start..paint_end;
-
-                        ((), element_state)
-                    },
-                )
-            } else {
-                // Non-cached path: just paint the element.
-                element.as_mut().unwrap().paint(window, cx);
-            }
-        });
+        if let Some(entity_id) = self.entity_id {
+            // Stateful path.
+            window.with_rendered_view(entity_id, |window| {
+                let caching_disabled = window.is_inspector_picking(cx);
+                if self.cached_style.is_some() && !caching_disabled {
+                    window.with_element_state::<ViewElementState, _>(
+                        global_id.unwrap(),
+                        |element_state, window| {
+                            let mut element_state = element_state.unwrap();
+
+                            let paint_start = window.paint_index();
+
+                            if let Some(element) = element {
+                                let refreshing = mem::replace(&mut window.refreshing, true);
+                                element.paint(window, cx);
+                                window.refreshing = refreshing;
+                            } else {
+                                window.reuse_paint(element_state.paint_range.clone());
+                            }
+
+                            let paint_end = window.paint_index();
+                            element_state.paint_range = paint_start..paint_end;
+
+                            ((), element_state)
+                        },
+                    )
+                } else {
+                    element.as_mut().unwrap().paint(window, cx);
+                }
+            });
+        } else {
+            // Stateless path: just paint the element.
+            window.with_id(
+                ElementId::Name(std::any::type_name::<V>().into()),
+                |window| {
+                    element.as_mut().unwrap().paint(window, cx);
+                },
+            );
+        }
     }
 }
 

crates/gpui_macros/src/derive_into_view_element.rs 🔗

@@ -14,12 +14,7 @@ pub fn derive_into_view_element(input: TokenStream) -> TokenStream {
             type Element = gpui::ViewElement<Self>;
 
             fn into_element(self) -> Self::Element {
-                let style = gpui::View::style(&self);
-                let element = gpui::ViewElement::new(self);
-                match style {
-                    Some(s) => element.cached(s),
-                    None => element,
-                }
+                gpui::ViewElement::new(self)
             }
         }
     };