From e2069eea8d7044dcaf955fa91c96b3a757d48216 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 9 Mar 2026 22:34:40 -0700 Subject: [PATCH] Add View, ComponentView traits and ViewElement for unified component model Introduce View trait with Option 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. --- crates/gpui/Cargo.toml | 4 +- .../example_editor.rs} | 58 +-- .../example_input.rs} | 30 +- .../example_tests.rs} | 28 +- .../example_text_area.rs} | 28 +- crates/gpui/examples/view_example/plan.md | 13 + .../view_example_main.rs} | 54 +-- crates/gpui/src/view.rs | 331 +++++++++++------- .../src/derive_into_view_element.rs | 7 +- 9 files changed, 319 insertions(+), 234 deletions(-) rename crates/gpui/examples/{text_views/editor.rs => view_example/example_editor.rs} (90%) rename crates/gpui/examples/{text_views/input.rs => view_example/example_input.rs} (88%) rename crates/gpui/examples/{text_views/editor_test.rs => view_example/example_tests.rs} (88%) rename crates/gpui/examples/{text_views/text_area.rs => view_example/example_text_area.rs} (85%) create mode 100644 crates/gpui/examples/view_example/plan.md rename crates/gpui/examples/{text_views/main.rs => view_example/view_example_main.rs} (77%) diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 1a65dcd84ad41a8d745ee31d0a483d3e5c6b0b48..e63300ccc49b152f456c092f051ccc080bd69d28 100644 --- a/crates/gpui/Cargo.toml +++ b/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" diff --git a/crates/gpui/examples/text_views/editor.rs b/crates/gpui/examples/view_example/example_editor.rs similarity index 90% rename from crates/gpui/examples/text_views/editor.rs rename to crates/gpui/examples/view_example/example_editor.rs index 4c1629a12bc4aa07391cb3df5a3ac78f98273e6e..2f249e80bf1093f09739f670d05e96f7097e32f1 100644 --- a/crates/gpui/examples/text_views/editor.rs +++ b/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 { 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, @@ -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, +struct ExampleEditorText { + editor: Entity, text_color: Hsla, } -struct EditorTextPrepaintState { +struct ExampleEditorTextPrepaintState { lines: Vec, cursor: Option, } -impl EditorText { - pub fn new(editor: Entity, text_color: Hsla) -> Self { +impl ExampleEditorText { + pub fn new(editor: Entity, 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 { 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, +pub struct ExampleEditorView { + editor: Entity, text_color: Hsla, } -impl EditorView { - pub fn new(editor: Entity) -> Self { +impl ExampleEditorView { + pub fn new(editor: Entity) -> 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 { - &self.editor + fn entity(&self) -> Option> { + 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) } } diff --git a/crates/gpui/examples/text_views/input.rs b/crates/gpui/examples/view_example/example_input.rs similarity index 88% rename from crates/gpui/examples/text_views/input.rs rename to crates/gpui/examples/view_example/example_input.rs index 184cb92eb5187284858b1142bec7844adbd1340b..822e79c919f73cbce41c2b75ed4ddfbd774b3cf0 100644 --- a/crates/gpui/examples/text_views/input.rs +++ b/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, +pub struct ExampleInput { + editor: Entity, width: Option, color: Option, } -impl Input { - pub fn new(editor: Entity) -> Self { +impl ExampleInput { + pub fn new(editor: Entity) -> 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 { - &self.editor + fn entity(&self) -> Option> { + Some(self.editor.clone()) } fn style(&self) -> Option { @@ -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( diff --git a/crates/gpui/examples/text_views/editor_test.rs b/crates/gpui/examples/view_example/example_tests.rs similarity index 88% rename from crates/gpui/examples/text_views/editor_test.rs rename to crates/gpui/examples/view_example/example_tests.rs index 66e2c1af96650ef5853af1111ae7214cad79d11b..81fb8648151e512117d548116317ab43c808c152 100644 --- a/crates/gpui/examples/text_views/editor_test.rs +++ b/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: Entity, } impl Render for InputWrapper { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - Input::new(self.editor.clone()) + ExampleInput::new(self.editor.clone()) } } struct TextAreaWrapper { - editor: Entity, + editor: Entity, } impl Render for TextAreaWrapper { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> 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, &mut gpui::VisualTestContext) { + fn init_input( + cx: &mut TestAppContext, + ) -> (Entity, &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, &mut gpui::VisualTestContext) { + fn init_textarea( + cx: &mut TestAppContext, + ) -> (Entity, &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 } }); diff --git a/crates/gpui/examples/text_views/text_area.rs b/crates/gpui/examples/view_example/example_text_area.rs similarity index 85% rename from crates/gpui/examples/text_views/text_area.rs rename to crates/gpui/examples/view_example/example_text_area.rs index 91dafee302cac73adcf3b3dd83246d2e24563066..ee5eb0375e0f5fa5f30b5c427c90b6bdfe387817 100644 --- a/crates/gpui/examples/text_views/text_area.rs +++ b/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, +pub struct ExampleTextArea { + editor: Entity, rows: usize, color: Option, } -impl TextArea { - pub fn new(editor: Entity, rows: usize) -> Self { +impl ExampleTextArea { + pub fn new(editor: Entity, 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 { - &self.editor + fn entity(&self) -> Option> { + Some(self.editor.clone()) } fn style(&self) -> Option { @@ -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)) } } diff --git a/crates/gpui/examples/view_example/plan.md b/crates/gpui/examples/view_example/plan.md new file mode 100644 index 0000000000000000000000000000000000000000..dfc25039abc1fc3d708fbd3530d27f3d8383a237 --- /dev/null +++ b/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.) diff --git a/crates/gpui/examples/text_views/main.rs b/crates/gpui/examples/view_example/view_example_main.rs similarity index 77% rename from crates/gpui/examples/text_views/main.rs rename to crates/gpui/examples/view_example/view_example_main.rs index e2b78d17d6b51c027ba269cb23b5476692478941..04f616e76e254b4b349abf6e869832b571a8d4c6 100644 --- a/crates/gpui/examples/text_views/main.rs +++ b/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) -> 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(); diff --git a/crates/gpui/src/view.rs b/crates/gpui/src/view.rs index a04d9f235a04e95ceb390800b7ff81a0042d5638..424f96cbfc88e6eba2893cd1d41a43d618aa1c6f 100644 --- a/crates/gpui/src/view.rs +++ b/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 { -/// &self.state +/// fn entity(&self) -> Option> { +/// 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; + /// 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>; /// 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 View for T { + type Entity = (); + + fn entity(&self) -> Option> { + None + } + + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + ComponentView::render(self, window, cx) + } + + fn style(&self) -> Option { + 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` @@ -405,7 +454,7 @@ pub trait View: 'static + Sized + Hash { #[doc(hidden)] pub struct ViewElement { view: Option, - entity_id: EntityId, + entity_id: Option, props_hash: u64, cached_style: Option, #[cfg(debug_assertions)] @@ -419,31 +468,20 @@ impl ViewElement { #[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 IntoElement for ViewElement { @@ -473,7 +511,7 @@ impl Element for ViewElement { type PrepaintState = Option; fn id(&self) -> Option { - 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 Element for ViewElement { 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). + window.with_id( + ElementId::Name(std::any::type_name::().into()), + |window| { let mut element = self .view .take() @@ -512,9 +565,9 @@ impl Element for ViewElement { .into_any_element(); let layout_id = element.request_layout(window, cx); (layout_id, Some(element)) - } - } - }) + }, + ) + } } fn prepaint( @@ -526,76 +579,83 @@ impl Element for ViewElement { window: &mut Window, cx: &mut App, ) -> Option { - 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::( - global_id.unwrap(), - |element_state, window| { - let content_mask = window.content_mask(); - let text_style = window.text_style(); + window.with_element_state::( + 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::().into()), + |window| { + element.as_mut().unwrap().prepaint(window, cx); }, - ) - }) + ); + Some(element.take().unwrap()) + } } fn paint( @@ -608,36 +668,45 @@ impl Element for ViewElement { 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::( - 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::( + 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::().into()), + |window| { + element.as_mut().unwrap().paint(window, cx); + }, + ); + } } } diff --git a/crates/gpui_macros/src/derive_into_view_element.rs b/crates/gpui_macros/src/derive_into_view_element.rs index 0e531e4f56fc7530ee7b910361ccf6443f9c0a6a..f00ab62a8a752cbcdf68b6ded6058bcfd628aa9f 100644 --- a/crates/gpui_macros/src/derive_into_view_element.rs +++ b/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; 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) } } };