Enhance example with Render and RenderOnce replacements.

Mikayla Maki created

Change summary

crates/gpui/examples/view_example/example_editor.rs    | 64 +++---------
crates/gpui/examples/view_example/example_input.rs     |  9 
crates/gpui/examples/view_example/example_text_area.rs |  9 
crates/gpui/src/view.rs                                | 62 +++++++++-
4 files changed, 77 insertions(+), 67 deletions(-)

Detailed changes

crates/gpui/examples/view_example/example_editor.rs 🔗

@@ -2,17 +2,14 @@
 //! blink state, and keyboard handling.
 //!
 //! 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;
+//! and paints the cursor.
 use std::ops::Range;
 use std::time::Duration;
 
 use gpui::{
     App, Bounds, Context, ElementInputHandler, Entity, EntityInputHandler, FocusHandle, Focusable,
-    Hsla, IntoViewElement, LayoutId, PaintQuad, Pixels, ShapedLine, SharedString, Task, TextRun,
-    UTF16Selection, Window, fill, hsla, point, prelude::*, px, relative, size,
+    LayoutId, PaintQuad, Pixels, ShapedLine, SharedString, Task, TextRun, UTF16Selection, Window,
+    fill, hsla, point, prelude::*, px, relative, size,
 };
 use unicode_segmentation::*;
 
@@ -263,7 +260,6 @@ impl EntityInputHandler for ExampleEditor {
 
 struct ExampleEditorText {
     editor: Entity<ExampleEditor>,
-    text_color: Hsla,
 }
 
 struct ExampleEditorTextPrepaintState {
@@ -272,8 +268,8 @@ struct ExampleEditorTextPrepaintState {
 }
 
 impl ExampleEditorText {
-    pub fn new(editor: Entity<ExampleEditor>, text_color: Hsla) -> Self {
-        Self { editor, text_color }
+    pub fn new(editor: Entity<ExampleEditor>) -> Self {
+        Self { editor }
     }
 }
 
@@ -328,6 +324,7 @@ impl Element for ExampleEditorText {
         let is_focused = editor.focus_handle.is_focused(window);
 
         let style = window.text_style();
+        let text_color = style.color;
         let font_size = style.font_size.to_pixels(window.rem_size());
         let line_height = window.line_height();
 
@@ -356,7 +353,7 @@ impl Element for ExampleEditorText {
                     let run = TextRun {
                         len: text.len(),
                         font: style.font(),
-                        color: self.text_color,
+                        color: text_color,
                         background_color: None,
                         underline: None,
                         strikethrough: None,
@@ -381,7 +378,7 @@ impl Element for ExampleEditorText {
                     ),
                     size(px(1.5), line_height),
                 ),
-                self.text_color,
+                text_color,
             ))
         } else if is_focused && cursor_visible && is_placeholder {
             Some(fill(
@@ -389,7 +386,7 @@ impl Element for ExampleEditorText {
                     point(bounds.left(), bounds.top()),
                     size(px(1.5), line_height),
                 ),
-                self.text_color,
+                text_color,
             ))
         } else {
             None
@@ -447,41 +444,12 @@ fn cursor_line_and_offset(content: &str, cursor: usize) -> (usize, usize) {
     (line_index, cursor - line_start)
 }
 
-// ---------------------------------------------------------------------------
-// ExampleEditorView — a cached View that pairs an ExampleEditor entity with ExampleEditorText
-// ---------------------------------------------------------------------------
-
-/// 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 ExampleEditorView {
-    editor: Entity<ExampleEditor>,
-    text_color: Hsla,
-}
-
-impl ExampleEditorView {
-    pub fn new(editor: Entity<ExampleEditor>) -> Self {
-        Self {
-            editor,
-            text_color: hsla(0., 0., 0.1, 1.),
-        }
-    }
-
-    pub fn text_color(mut self, color: Hsla) -> Self {
-        self.text_color = color;
-        self
-    }
-}
-
-impl gpui::View for ExampleEditorView {
-    type Entity = ExampleEditor;
-
-    fn entity(&self) -> Option<Entity<ExampleEditor>> {
-        Some(self.editor.clone())
-    }
-
-    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
-        ExampleEditorText::new(self.editor, self.text_color)
+impl gpui::EntityView for ExampleEditor {
+    fn render(
+        &mut self,
+        _window: &mut Window,
+        cx: &mut Context<ExampleEditor>,
+    ) -> impl IntoElement {
+        ExampleEditorText::new(cx.entity().clone())
     }
 }

crates/gpui/examples/view_example/example_input.rs 🔗

@@ -9,12 +9,11 @@ use std::time::Duration;
 
 use gpui::{
     Animation, AnimationExt as _, App, BoxShadow, CursorStyle, Entity, Hsla, IntoViewElement,
-    Pixels, SharedString, StyleRefinement, Window, bounce, div, ease_in_out, hsla, point,
-    prelude::*, px, white,
+    Pixels, SharedString, StyleRefinement, ViewElement, Window, bounce, div, ease_in_out, hsla,
+    point, prelude::*, px, white,
 };
 
 use crate::example_editor::ExampleEditor;
-use crate::example_editor::ExampleEditorView;
 use crate::{Backspace, Delete, End, Enter, Home, Left, Right};
 
 struct FlashState {
@@ -55,7 +54,7 @@ impl gpui::View for ExampleInput {
         Some(self.editor.clone())
     }
 
-    fn style(&self) -> Option<StyleRefinement> {
+    fn cache_style(&mut self, _window: &mut Window, _cx: &mut App) -> Option<StyleRefinement> {
         let mut style = StyleRefinement::default();
         if let Some(w) = self.width {
             style.size.width = Some(w.into());
@@ -154,7 +153,7 @@ impl gpui::View for ExampleInput {
             .line_height(px(20.))
             .text_size(px(14.))
             .text_color(text_color)
-            .child(ExampleEditorView::new(editor).text_color(text_color));
+            .child(ViewElement::new(editor));
 
         if count > 0 {
             base.with_animation(

crates/gpui/examples/view_example/example_text_area.rs 🔗

@@ -5,12 +5,11 @@
 //! components with different props and layouts.
 
 use gpui::{
-    App, BoxShadow, CursorStyle, Entity, Hsla, IntoViewElement, StyleRefinement, Window, div, hsla,
-    point, prelude::*, px, white,
+    App, BoxShadow, CursorStyle, Entity, Hsla, IntoViewElement, StyleRefinement, ViewElement,
+    Window, div, hsla, point, prelude::*, px, white,
 };
 
 use crate::example_editor::ExampleEditor;
-use crate::example_editor::ExampleEditorView;
 use crate::{Backspace, Delete, End, Enter, Home, Left, Right};
 
 #[derive(Hash, IntoViewElement)]
@@ -42,7 +41,7 @@ impl gpui::View for ExampleTextArea {
         Some(self.editor.clone())
     }
 
-    fn style(&self) -> Option<StyleRefinement> {
+    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();
@@ -129,6 +128,6 @@ impl gpui::View for ExampleTextArea {
             .line_height(row_height)
             .text_size(px(14.))
             .text_color(text_color)
-            .child(ExampleEditorView::new(editor).text_color(text_color))
+            .child(ViewElement::new(editor))
     }
 }

crates/gpui/src/view.rs 🔗

@@ -383,17 +383,14 @@ pub trait View: 'static + Sized + Hash {
     /// When `Some`, the view element will be cached using the given style for its outer layout.
     /// The default returns a full-size style refinement (`width: 100%, height: 100%`).
     /// Return `None` to disable caching.
-    fn style(&self) -> Option<StyleRefinement> {
+    fn cache_style(&mut self, _window: &mut Window, _cx: &mut App) -> Option<StyleRefinement> {
         Some(StyleRefinement::default().size_full())
     }
 }
 
 /// 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.
+/// This is the `View` equivalent of [`RenderOnce`](crate::RenderOnce).
 ///
 /// # Example
 ///
@@ -413,6 +410,11 @@ 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;
+
+    /// Indicate that this view should be cached
+    fn cache_style(&mut self, _window: &mut Window, _cx: &mut App) -> Option<StyleRefinement> {
+        None
+    }
 }
 
 impl<T: ComponentView> View for T {
@@ -422,15 +424,53 @@ impl<T: ComponentView> View for T {
         None
     }
 
+    #[inline]
     fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
         ComponentView::render(self, window, cx)
     }
 
-    fn style(&self) -> Option<StyleRefinement> {
+    #[inline]
+    fn cache_style(&mut self, window: &mut Window, cx: &mut App) -> Option<StyleRefinement> {
+        ComponentView::cache_style(self, window, cx)
+    }
+}
+
+/// For entities that require only one kind of rendering, this trait provides a simplified interface.
+/// Equivalent to the `Render` trait
+pub trait EntityView: 'static + Sized {
+    /// Render this entity into the element tree.
+    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement;
+    /// Indicate that this entity should be cached when using it as an element.
+    /// When using this method, the entity's previous layout and paint will be recycled from the previous frame if [Context::notify] has not been called since it was rendered.
+    fn cache_style(
+        &mut self,
+        _window: &mut Window,
+        _cx: &mut Context<Self>,
+    ) -> Option<StyleRefinement> {
         None
     }
 }
 
+impl<T: EntityView> View for Entity<T> {
+    type Entity = T;
+
+    fn entity(&self) -> Option<Entity<Self::Entity>> {
+        Some(self.clone())
+    }
+
+    #[inline]
+    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
+        self.update(cx, |this, cx| {
+            EntityView::render(this, window, cx).into_any_element()
+        })
+    }
+
+    #[inline]
+    fn cache_style(&mut self, window: &mut Window, cx: &mut App) -> Option<StyleRefinement> {
+        self.update(cx, |this, cx| EntityView::cache_style(this, window, cx))
+    }
+}
+
 /// 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>`
@@ -462,18 +502,16 @@ impl<V: View> ViewElement<V> {
     /// Create a new `ViewElement` wrapping the given [`View`].
     ///
     /// Use this in your [`IntoElement`] implementation.
-    #[track_caller]
     pub fn new(view: V) -> Self {
         use std::hash::Hasher;
         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,
+            cached_style: None,
             view: Some(view),
             #[cfg(debug_assertions)]
             source: core::panic::Location::caller(),
@@ -526,6 +564,12 @@ impl<V: View> Element for ViewElement<V> {
         window: &mut Window,
         cx: &mut App,
     ) -> (LayoutId, Self::RequestLayoutState) {
+        if self.cached_style.is_none() {
+            if let Some(view) = self.view.as_mut() {
+                self.cached_style = view.cache_style(window, cx);
+            }
+        }
+
         if let Some(entity_id) = self.entity_id {
             // Stateful path: create a reactive boundary.
             window.with_rendered_view(entity_id, |window| {