From ed788738b080f295e57343145d103449947d0ddd Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Sat, 7 Mar 2026 15:10:38 -0800 Subject: [PATCH] Introduce the View trait and ViewElement, a synthesis of the Entity and Component models. --- crates/gpui/src/view.rs | 322 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 321 insertions(+), 1 deletion(-) diff --git a/crates/gpui/src/view.rs b/crates/gpui/src/view.rs index 39b87dbb8039e774f2ec25fab660d46e7c72b01e..8fca738912e57bc68d33e137f96eb05e54042ad3 100644 --- a/crates/gpui/src/view.rs +++ b/crates/gpui/src/view.rs @@ -9,7 +9,7 @@ use collections::FxHashSet; use refineable::Refineable; use std::mem; use std::rc::Rc; -use std::{any::TypeId, fmt, ops::Range}; +use std::{any::TypeId, fmt, hash::Hash, ops::Range}; struct AnyViewState { prepaint_range: Range, @@ -310,6 +310,326 @@ mod any_view { } } +/// A component backed by an entity that participates in GPUI's reactive graph. +/// +/// Views combine the ergonomic builder-pattern API of components ([`RenderOnce`](crate::RenderOnce)) +/// with the push-pull reactivity of entities. When a view's entity calls +/// `cx.notify()`, only this view (and its dirty ancestors/children) need to +/// re-render. +/// +/// Unlike [`Render`], which puts the rendering trait on the entity's state type, +/// `View` goes on the *component* type — the struct that holds both the entity +/// handle and any display props. This means the consumer controls styling +/// through the builder pattern, while the entity provides reactive state. +/// +/// # Example +/// +/// ```ignore +/// struct CounterState { +/// count: usize, +/// } +/// +/// struct Counter { +/// state: Entity, +/// label: SharedString, +/// } +/// +/// impl Counter { +/// fn new(state: Entity) -> Self { +/// Self { state, label: "Count".into() } +/// } +/// +/// fn label(mut self, label: impl Into) -> Self { +/// self.label = label.into(); +/// self +/// } +/// } +/// +/// impl View for Counter { +/// type State = CounterState; +/// +/// fn entity(&self) -> &Entity { +/// &self.state +/// } +/// +/// +/// fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { +/// let count = self.state.read(cx).count; +/// div().child(format!("{}: {}", self.label, count)) +/// } +/// } +/// +/// // Usage in a parent's render: +/// // Counter::new(my_counter_entity).label("Total") +/// ``` +pub trait View: 'static + Sized + Hash { + /// The entity type that backs this view's state. + type State: 'static; + + /// Returns a reference to the entity that backs this view. + fn entity(&self) -> &Entity; + + /// Render this view into an element tree. Takes ownership of self, + /// consuming the component props. The entity state persists across frames. + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement; +} + +/// 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` +/// wraps a stateless [`RenderOnce`](crate::RenderOnce) type, `ViewElement` wraps a stateful +/// [`View`] type and hooks its entity into GPUI's push-pull reactive graph. +/// +/// You don't construct this directly. Instead, implement [`IntoElement`] for your +/// [`View`] type using [`ViewElement::new`]: +/// +/// ```ignore +/// impl IntoElement for Counter { +/// type Element = ViewElement; +/// fn into_element(self) -> Self::Element { +/// ViewElement::new(self) +/// } +/// } +/// ``` +#[doc(hidden)] +pub struct ViewElement { + view: Option, + entity_id: EntityId, + props_hash: u64, + cached_style: Option, + #[cfg(debug_assertions)] + source: &'static core::panic::Location<'static>, +} + +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().entity_id(); + 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, + 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 { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +struct ViewElementState { + prepaint_range: Range, + paint_range: Range, + cache_key: ViewElementCacheKey, + accessed_entities: FxHashSet, +} + +struct ViewElementCacheKey { + bounds: Bounds, + content_mask: ContentMask, + text_style: TextStyle, + props_hash: u64, +} + +impl Element for ViewElement { + type RequestLayoutState = Option; + type PrepaintState = Option; + + fn id(&self) -> Option { + Some(ElementId::View(self.entity_id)) + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + #[cfg(debug_assertions)] + return Some(self.source); + + #[cfg(not(debug_assertions))] + return None; + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + 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) + } + // Non-cached path: render eagerly. + _ => { + 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)) + } + } + }) + } + + fn prepaint( + &mut self, + global_id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + element: &mut Self::RequestLayoutState, + 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); + } + + // 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(); + + // 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 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 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, + }, + }, + ) + }, + ) + }) + } + + fn paint( + &mut self, + global_id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + element: &mut Self::PrepaintState, + 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); + } + }); + } +} + /// A view that renders nothing pub struct EmptyView;