Skip to main content

gpui_authoring/
view.rs

1use crate::{
2    AnyElement, AnyEntity, AnyWeakEntity, App, Bounds, ContentMask, Context, Element, ElementId,
3    Entity, EntityId, GlobalElementId, IntoElement, LayoutId, PaintIndex, Pixels,
4    PrepaintStateIndex, Render, RenderOnce, Style, StyleRefinement, TextStyle, WeakEntity,
5};
6use crate::{Empty, Window};
7use anyhow::Result;
8use collections::FxHashSet;
9use refineable::Refineable;
10use std::mem;
11use std::{
12    any::{TypeId, type_name},
13    fmt,
14    ops::Range,
15};
16
17/// A dynamically-typed view handle that can be downcast to a specific `Entity<V>`.
18///
19/// This is the type-erased counterpart to [`ViewElement`]: it holds an entity plus
20/// a function pointer to its render, and is itself a [`View`], so embedding it as an
21/// element goes through the same [`ViewElement`] machinery as any other view.
22#[derive(Clone, Debug)]
23pub struct AnyView {
24    entity: AnyEntity,
25    render: fn(&AnyView, &mut Window, &mut App) -> AnyElement,
26}
27
28impl<V: Render> From<Entity<V>> for AnyView {
29    fn from(value: Entity<V>) -> Self {
30        AnyView {
31            entity: value.into_any(),
32            render: any_view::render::<V>,
33        }
34    }
35}
36
37impl AnyView {
38    /// Embed this view as a cached [`ViewElement`] laid out at `style`.
39    ///
40    /// The rendered subtree is recycled from the previous frame unless
41    /// [Context::notify] was called on the backing entity since it was rendered
42    /// (or [Window::refresh] is called, which ignores caching).
43    pub fn cached(self, style: StyleRefinement) -> ViewElement<AnyView> {
44        ViewElement::new(self).cached(style)
45    }
46
47    /// Convert this to a weak handle.
48    pub fn downgrade(&self) -> AnyWeakView {
49        AnyWeakView {
50            entity: self.entity.downgrade(),
51            render: self.render,
52        }
53    }
54
55    /// Convert this to a [Entity] of a specific type.
56    /// If this handle does not contain a view of the specified type, returns itself in an `Err` variant.
57    pub fn downcast<T: 'static>(self) -> Result<Entity<T>, Self> {
58        match self.entity.downcast() {
59            Ok(entity) => Ok(entity),
60            Err(entity) => Err(Self {
61                entity,
62                render: self.render,
63            }),
64        }
65    }
66
67    /// Gets the [TypeId] of the underlying view.
68    pub fn entity_type(&self) -> TypeId {
69        self.entity.entity_type
70    }
71
72    /// The [`EntityId`] of this view.
73    pub fn entity_id(&self) -> EntityId {
74        self.entity.entity_id()
75    }
76}
77
78impl PartialEq for AnyView {
79    fn eq(&self, other: &Self) -> bool {
80        self.entity == other.entity
81    }
82}
83
84impl Eq for AnyView {}
85
86/// `AnyView` is the type-erased [`View`]: its `render` is a function pointer rather
87/// than a concrete type, but it participates in the reactive graph exactly like any
88/// other view via [`ViewElement`].
89impl View for AnyView {
90    fn entity_id(&self) -> Option<EntityId> {
91        Some(self.entity.entity_id())
92    }
93
94    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
95        (self.render)(&self, window, cx)
96    }
97}
98
99impl<V: 'static + Render> IntoElement for Entity<V> {
100    type Element = ViewElement<Entity<V>>;
101
102    fn into_element(self) -> Self::Element {
103        ViewElement::new(self)
104    }
105
106    #[inline(never)]
107    fn into_any_element(self) -> AnyElement {
108        self.into_element().into_any()
109    }
110}
111
112impl IntoElement for AnyView {
113    type Element = ViewElement<AnyView>;
114
115    fn into_element(self) -> Self::Element {
116        ViewElement::new(self)
117    }
118}
119
120/// A weak, dynamically-typed view handle.
121pub struct AnyWeakView {
122    entity: AnyWeakEntity,
123    render: fn(&AnyView, &mut Window, &mut App) -> AnyElement,
124}
125
126impl AnyWeakView {
127    /// Upgrade to a strong `AnyView` handle, if the view is still alive.
128    pub fn upgrade(&self) -> Option<AnyView> {
129        let entity = self.entity.upgrade()?;
130        Some(AnyView {
131            entity,
132            render: self.render,
133        })
134    }
135}
136
137impl<V: 'static + Render> From<WeakEntity<V>> for AnyWeakView {
138    fn from(view: WeakEntity<V>) -> Self {
139        AnyWeakView {
140            entity: view.into(),
141            render: any_view::render::<V>,
142        }
143    }
144}
145
146impl PartialEq for AnyWeakView {
147    fn eq(&self, other: &Self) -> bool {
148        self.entity == other.entity
149    }
150}
151
152impl std::fmt::Debug for AnyWeakView {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        f.debug_struct("AnyWeakView")
155            .field("entity_id", &self.entity.entity_id)
156            .finish_non_exhaustive()
157    }
158}
159
160mod any_view {
161    use crate::{AnyElement, AnyView, App, IntoElement, Render, Window};
162
163    pub(crate) fn render<V: 'static + Render>(
164        view: &AnyView,
165        window: &mut Window,
166        cx: &mut App,
167    ) -> AnyElement {
168        let view = view.clone().downcast::<V>().unwrap();
169        // Record the view's Render type name so the accessibility debug dump can
170        // attribute nodes to the view that produced them.
171        #[cfg(debug_assertions)]
172        window
173            .core
174            .a11y
175            .view_type_names
176            .insert(view.entity_id(), std::any::type_name::<V>());
177        view.update(cx, |view, cx| view.render(window, cx).into_any_element())
178    }
179}
180
181/// A renderable that participates in GPUI's reactive graph — the unifying model
182/// behind [`Render`] and [`RenderOnce`].
183///
184/// When `entity_id()` returns `Some`, that id becomes the view's identity: it gets
185/// a unique element-id space (so internal `use_state` / `.id(..)` never collide
186/// across siblings) and `cx.notify()` on that entity re-renders only this view's
187/// subtree. `None` behaves like a stateless component.
188///
189/// You rarely implement `View` directly. `Entity<T: Render>` and any `T: RenderOnce`
190/// get a blanket impl below; implement it by hand only when a component needs both
191/// parent-supplied props *and* a backing entity for identity.
192pub trait View: 'static + Sized {
193    /// This view's identity, if it has one. A view typically holds the backing
194    /// entity as a field and returns its [`EntityId`] here.
195    ///
196    /// The id becomes this view's [`ElementId`], so two views keyed on the same
197    /// entity must not be rendered at the same position in the element tree
198    /// (e.g. as siblings under the same parent): their internal element state
199    /// (`use_state`, scroll offsets, etc.) would silently collide. Nesting is
200    /// fine — the id is scoped by the parent path.
201    fn entity_id(&self) -> Option<EntityId>;
202
203    /// Render this view into an element tree, consuming `self`.
204    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement;
205}
206
207/// A stateless component (`RenderOnce`) is a `View` with no identity.
208impl<T: RenderOnce> View for T {
209    fn entity_id(&self) -> Option<EntityId> {
210        None
211    }
212
213    #[inline]
214    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
215        RenderOnce::render(self, window, cx)
216    }
217}
218
219/// An entity that renders itself (`Render`) is a `View` keyed on its own id.
220impl<T: Render> View for Entity<T> {
221    fn entity_id(&self) -> Option<EntityId> {
222        Some(Entity::entity_id(self))
223    }
224
225    #[inline]
226    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
227        self.update(cx, |this, cx| {
228            Render::render(this, window, cx).into_any_element()
229        })
230    }
231}
232
233impl<T: Render> Entity<T> {
234    /// Embed this entity as a cached [`ViewElement`] laid out at `style`.
235    ///
236    /// The rendered subtree is reused until the entity is notified (or the
237    /// cached bounds / text style change). Caching requires a definite size:
238    /// a cached view is laid out from `style` and is *not* measured from its
239    /// contents. Use [`ViewElement::new`] (or `.child(entity)`) for the
240    /// uncached case.
241    #[track_caller]
242    pub fn cached(self, style: StyleRefinement) -> ViewElement<Entity<T>> {
243        ViewElement::new(self).cached(style)
244    }
245}
246
247/// The element type for [`View`] implementations. Wraps a `View` and hooks it
248/// into layout, prepaint, and paint. Constructed via [`ViewElement::new`].
249#[doc(hidden)]
250pub struct ViewElement<V: View> {
251    view: Option<V>,
252    entity_id: Option<EntityId>,
253    cached_style: Option<StyleRefinement>,
254    #[cfg(debug_assertions)]
255    source: &'static core::panic::Location<'static>,
256}
257
258impl<V: View> ViewElement<V> {
259    /// Wrap a [`View`] as an element.
260    #[track_caller]
261    pub fn new(view: V) -> Self {
262        let entity_id = view.entity_id();
263        ViewElement {
264            entity_id,
265            cached_style: None,
266            view: Some(view),
267            #[cfg(debug_assertions)]
268            source: core::panic::Location::caller(),
269        }
270    }
271
272    /// Enable caching of this view's rendered subtree, laid out at `style`.
273    /// The composer supplies the layout style because caching skips rendering
274    /// the contents to measure them.
275    ///
276    /// Crate-private on purpose: caching is only sound for entity-backed views,
277    /// where [`Context::notify`] is the contract that busts the cache. A stateless
278    /// view has no such contract, so a frozen subtree could never be invalidated.
279    /// Reach this through [`Entity::cached`] or [`AnyView::cached`], which are
280    /// entity-backed by construction.
281    pub(crate) fn cached(mut self, style: StyleRefinement) -> Self {
282        self.cached_style = Some(style);
283        self
284    }
285}
286
287impl<V: View> IntoElement for ViewElement<V> {
288    type Element = Self;
289
290    fn into_element(self) -> Self::Element {
291        self
292    }
293}
294
295pub(crate) struct ViewElementState {
296    prepaint_range: Range<PrepaintStateIndex>,
297    paint_range: Range<PaintIndex>,
298    cache_key: ViewElementCacheKey,
299    accessed_entities: FxHashSet<EntityId>,
300}
301
302pub(crate) struct ViewElementCacheKey {
303    bounds: Bounds<Pixels>,
304    content_mask: ContentMask<Pixels>,
305    text_style: TextStyle,
306}
307
308impl<V: View> Element for ViewElement<V> {
309    type RequestLayoutState = Option<AnyElement>;
310    type PrepaintState = Option<AnyElement>;
311
312    fn id(&self) -> Option<ElementId> {
313        self.entity_id.map(ElementId::View)
314    }
315
316    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
317        #[cfg(debug_assertions)]
318        return Some(self.source);
319
320        #[cfg(not(debug_assertions))]
321        return None;
322    }
323
324    fn request_layout(
325        &mut self,
326        _id: Option<&GlobalElementId>,
327        window: &mut Window,
328        cx: &mut App,
329    ) -> (LayoutId, Self::RequestLayoutState) {
330        if let Some(entity_id) = self.entity_id {
331            // Stateful path: create a reactive boundary.
332            let view = &mut self.view;
333            request_layout_view(
334                entity_id,
335                self.cached_style.as_ref(),
336                window,
337                cx,
338                &mut |window, cx| view.take().unwrap().render(window, cx).into_any_element(),
339            )
340        } else {
341            // Stateless path: isolate subtree via type name (no entity identity).
342            request_layout_component(type_name::<V>(), window, cx, &mut |window, cx| {
343                self.view
344                    .take()
345                    .unwrap()
346                    .render(window, cx)
347                    .into_any_element()
348            })
349        }
350    }
351
352    fn prepaint(
353        &mut self,
354        global_id: Option<&GlobalElementId>,
355        bounds: Bounds<Pixels>,
356        element: &mut Self::RequestLayoutState,
357        window: &mut Window,
358        cx: &mut App,
359    ) -> Option<AnyElement> {
360        if let Some(entity_id) = self.entity_id {
361            // Stateful path.
362            window.set_view_id(entity_id);
363            window.with_rendered_view(entity_id, |window| {
364                if let Some(mut element) = element.take() {
365                    element.prepaint(window, cx);
366                    return Some(element);
367                }
368
369                prepaint_cached_view(entity_id, global_id, bounds, window, cx, |window, cx| {
370                    self.view
371                        .take()
372                        .unwrap()
373                        .render(window, cx)
374                        .into_any_element()
375                })
376            })
377        } else {
378            // Stateless path: just prepaint the element.
379            prepaint_component(type_name::<V>(), element, window, cx)
380        }
381    }
382
383    fn paint(
384        &mut self,
385        global_id: Option<&GlobalElementId>,
386        _bounds: Bounds<Pixels>,
387        _request_layout: &mut Self::RequestLayoutState,
388        element: &mut Self::PrepaintState,
389        window: &mut Window,
390        cx: &mut App,
391    ) {
392        if let Some(entity_id) = self.entity_id {
393            // Stateful path.
394            paint_view(
395                entity_id,
396                self.cached_style.is_some(),
397                global_id,
398                element,
399                window,
400                cx,
401            );
402        } else {
403            // Stateless path: just paint the element.
404            paint_component(std::any::type_name::<V>(), element, window, cx);
405        }
406    }
407}
408
409/// A view that renders nothing
410pub struct EmptyView;
411
412impl Render for EmptyView {
413    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
414        Empty
415    }
416}
417
418/// Prepaints a subtree that can be reused from the previous frame.
419///
420/// The subtree is built by `build` and cached against `entity_id`, the bounds it
421/// was laid out at, the content mask and text style in effect, and the element id
422/// of the caller. On a hit the previous frame's prepaint is replayed and `build`
423/// is not called at all.
424///
425/// Returns the new subtree, or `None` when the previous frame's was reused.
426pub(crate) fn prepaint_cached_view(
427    entity_id: EntityId,
428    global_id: Option<&GlobalElementId>,
429    bounds: Bounds<Pixels>,
430    window: &mut Window,
431    cx: &mut App,
432    build: impl FnOnce(&mut Window, &mut App) -> AnyElement,
433) -> Option<AnyElement> {
434    window.with_element_state::<ViewElementState, _>(global_id.unwrap(), |element_state, window| {
435        let content_mask = window.content_mask();
436        let text_style = window.text_style();
437
438        if let Some(mut element_state) = element_state
439            && element_state.cache_key.bounds == bounds
440            && element_state.cache_key.content_mask == content_mask
441            && element_state.cache_key.text_style == text_style
442            && !window.frame_state.dirty_views.contains(&entity_id)
443            && !window.core.refreshing
444        {
445            let prepaint_start = window.prepaint_index();
446            window.reuse_prepaint(element_state.prepaint_range.clone());
447            cx.entities
448                .extend_accessed(&element_state.accessed_entities);
449            let prepaint_end = window.prepaint_index();
450            element_state.prepaint_range = prepaint_start..prepaint_end;
451
452            return (None, element_state);
453        }
454
455        let refreshing = mem::replace(&mut window.core.refreshing, true);
456        let prepaint_start = window.prepaint_index();
457        let (mut element, accessed_entities) = cx.detect_accessed_entities(|cx| {
458            let mut element = build(window, cx);
459            element.layout_as_root(bounds.size.into(), window, cx);
460            element.prepaint_at(bounds.origin, window, cx);
461            element
462        });
463
464        let prepaint_end = window.prepaint_index();
465        window.core.refreshing = refreshing;
466
467        (
468            Some(element),
469            ViewElementState {
470                accessed_entities,
471                prepaint_range: prepaint_start..prepaint_end,
472                paint_range: PaintIndex::default()..PaintIndex::default(),
473                cache_key: ViewElementCacheKey {
474                    bounds,
475                    content_mask,
476                    text_style,
477                },
478            },
479        )
480    })
481}
482
483#[inline(never)]
484fn request_layout_view(
485    entity_id: EntityId,
486    cached_style: Option<&StyleRefinement>,
487    window: &mut Window,
488    cx: &mut App,
489    render: &mut dyn FnMut(&mut Window, &mut App) -> AnyElement,
490) -> (LayoutId, Option<AnyElement>) {
491    window.with_rendered_view(entity_id, |window| {
492        let caching_disabled = window.is_inspector_picking(cx);
493        match cached_style {
494            Some(style) if !caching_disabled => {
495                let mut root_style = Style::default();
496                root_style.refine(style);
497                let layout_id = window.request_layout(root_style, None, cx);
498                (layout_id, None)
499            }
500            _ => {
501                let mut element = render(window, cx);
502                let layout_id = element.request_layout(window, cx);
503                (layout_id, Some(element))
504            }
505        }
506    })
507}
508
509#[inline(never)]
510fn request_layout_component(
511    name: &'static str,
512    window: &mut Window,
513    cx: &mut App,
514    render: &mut dyn FnMut(&mut Window, &mut App) -> AnyElement,
515) -> (LayoutId, Option<AnyElement>) {
516    window.with_id(ElementId::from(name), |window| {
517        let mut element = render(window, cx);
518        let layout_id = element.request_layout(window, cx);
519        (layout_id, Some(element))
520    })
521}
522
523#[inline(never)]
524fn prepaint_component(
525    name: &'static str,
526    element: &mut Option<AnyElement>,
527    window: &mut Window,
528    cx: &mut App,
529) -> Option<AnyElement> {
530    window.with_id(ElementId::from(name), |window| {
531        element.as_mut().unwrap().prepaint(window, cx);
532    });
533    Some(element.take().unwrap())
534}
535
536#[inline(never)]
537pub(crate) fn paint_view(
538    entity_id: EntityId,
539    cached: bool,
540    global_id: Option<&GlobalElementId>,
541    element: &mut Option<AnyElement>,
542    window: &mut Window,
543    cx: &mut App,
544) {
545    window.with_rendered_view(entity_id, |window| {
546        let caching_disabled = window.is_inspector_picking(cx);
547        if cached && !caching_disabled {
548            window.with_element_state::<ViewElementState, _>(
549                global_id.unwrap(),
550                |element_state, window| {
551                    let mut element_state = element_state.unwrap();
552
553                    let paint_start = window.paint_index();
554
555                    if let Some(element) = element {
556                        let refreshing = mem::replace(&mut window.core.refreshing, true);
557                        element.paint(window, cx);
558                        window.core.refreshing = refreshing;
559                    } else {
560                        window.reuse_paint(element_state.paint_range.clone());
561                    }
562
563                    let paint_end = window.paint_index();
564                    element_state.paint_range = paint_start..paint_end;
565
566                    ((), element_state)
567                },
568            )
569        } else {
570            element.as_mut().unwrap().paint(window, cx);
571        }
572    });
573}
574
575#[inline(never)]
576fn paint_component(
577    name: &'static str,
578    element: &mut Option<AnyElement>,
579    window: &mut Window,
580    cx: &mut App,
581) {
582    window.with_id(ElementId::Name(name.into()), |window| {
583        element.as_mut().unwrap().paint(window, cx);
584    });
585}
586
587#[cfg(test)]
588mod tests {
589    use std::{cell::Cell, rc::Rc};
590
591    use crate::{
592        AnyWindowHandle, AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render,
593        StyleRefinement, Styled as _, TestAppContext, Window, div, green, px, size,
594    };
595
596    /// Counts the frames it renders in, so a test can tell a cache hit from a
597    /// rebuild.
598    struct CountedView {
599        renders: Rc<Cell<usize>>,
600    }
601
602    impl Render for CountedView {
603        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
604            self.renders.set(self.renders.get() + 1);
605            div().w(px(10.)).h(px(10.)).bg(green())
606        }
607    }
608
609    struct CachedViewHost {
610        view: Entity<CountedView>,
611    }
612
613    impl Render for CachedViewHost {
614        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
615            div().size_full().child(
616                self.view
617                    .clone()
618                    .cached(StyleRefinement::default().size_full()),
619            )
620        }
621    }
622
623    /// Draws the window, returning how many quads the frame painted.
624    fn draw(cx: &mut TestAppContext, window: AnyWindowHandle) -> usize {
625        cx.update_window(window, |_, window, cx| {
626            window.draw(cx).clear(cx);
627            window.frame_state.rendered_frame.scene.quads.len()
628        })
629        .unwrap()
630    }
631
632    #[gpui::test]
633    fn a_cached_view_reuses_its_subtree_until_it_is_notified_or_resized(cx: &mut TestAppContext) {
634        let renders = Rc::new(Cell::new(0));
635        let view = cx.new({
636            let renders = renders.clone();
637            move |_| CountedView { renders }
638        });
639        let window: AnyWindowHandle = cx
640            .add_window({
641                let view = view.clone();
642                move |_, _| CachedViewHost { view }
643            })
644            .into();
645
646        let first = draw(cx, window);
647        assert_eq!(renders.get(), 1, "the first frame renders the view");
648        assert!(first > 0, "the view's content is painted");
649
650        let second = draw(cx, window);
651        assert_eq!(renders.get(), 1, "a clean cached view is not re-rendered");
652        assert_eq!(
653            second, first,
654            "the reused subtree is still painted into the scene"
655        );
656
657        view.update(cx, |_, cx| cx.notify());
658        draw(cx, window);
659        assert!(renders.get() > 1, "notifying the view re-renders it");
660
661        // The view is clean again, but the bounds it is drawn at changed.
662        let renders_before_resize = renders.get();
663        cx.simulate_window_resize(window, size(px(400.), px(300.)));
664        draw(cx, window);
665        assert!(
666            renders.get() > renders_before_resize,
667            "resizing the window re-renders the cached view"
668        );
669    }
670}