From 63db56f3d7dcb2d02f07c0b72bd6c77a89729d12 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 11 Mar 2026 12:54:16 -0700 Subject: [PATCH] Enhance example with Render and RenderOnce replacements. --- .../examples/view_example/example_editor.rs | 64 +++++-------------- .../examples/view_example/example_input.rs | 9 ++- .../view_example/example_text_area.rs | 9 ++- crates/gpui/src/view.rs | 62 +++++++++++++++--- 4 files changed, 77 insertions(+), 67 deletions(-) diff --git a/crates/gpui/examples/view_example/example_editor.rs b/crates/gpui/examples/view_example/example_editor.rs index 2f249e80bf1093f09739f670d05e96f7097e32f1..662dea08aee76a347c9cc0d8a71021111b3673ba 100644 --- a/crates/gpui/examples/view_example/example_editor.rs +++ b/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, - text_color: Hsla, } struct ExampleEditorTextPrepaintState { @@ -272,8 +268,8 @@ struct ExampleEditorTextPrepaintState { } impl ExampleEditorText { - pub fn new(editor: Entity, text_color: Hsla) -> Self { - Self { editor, text_color } + pub fn new(editor: Entity) -> 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, - text_color: Hsla, -} - -impl ExampleEditorView { - pub fn new(editor: Entity) -> 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> { - 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, + ) -> impl IntoElement { + ExampleEditorText::new(cx.entity().clone()) } } diff --git a/crates/gpui/examples/view_example/example_input.rs b/crates/gpui/examples/view_example/example_input.rs index 822e79c919f73cbce41c2b75ed4ddfbd774b3cf0..c8b5b9ff59366fd857f2eaeff4aaa10a866ad090 100644 --- a/crates/gpui/examples/view_example/example_input.rs +++ b/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 { + fn cache_style(&mut self, _window: &mut Window, _cx: &mut App) -> Option { 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( diff --git a/crates/gpui/examples/view_example/example_text_area.rs b/crates/gpui/examples/view_example/example_text_area.rs index ee5eb0375e0f5fa5f30b5c427c90b6bdfe387817..8f5536c5680a5734eef1bcaead1e6fb04336f382 100644 --- a/crates/gpui/examples/view_example/example_text_area.rs +++ b/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 { + fn cache_style(&mut self, _window: &mut Window, _cx: &mut App) -> Option { 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)) } } diff --git a/crates/gpui/src/view.rs b/crates/gpui/src/view.rs index 25b2ef378a9bac4ddcafa82d02aa83c68c6694c9..c093fe31efef8d6c0aae51e3034a6b65932ec1cf 100644 --- a/crates/gpui/src/view.rs +++ b/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 { + fn cache_style(&mut self, _window: &mut Window, _cx: &mut App) -> Option { 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 { + None + } } impl View for T { @@ -422,15 +424,53 @@ impl 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 { + #[inline] + fn cache_style(&mut self, window: &mut Window, cx: &mut App) -> Option { + 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) -> 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, + ) -> Option { None } } +impl View for Entity { + type Entity = T; + + fn entity(&self) -> Option> { + 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 { + 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` @@ -462,18 +502,16 @@ impl ViewElement { /// 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 Element for ViewElement { 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| {