Skip to main content

gpui_authoring/elements/
slot.rs

1//! A reactive boundary over an [`Entity`]'s state, driven by a closure.
2
3use crate::{
4    AnyElement, App, Bounds, Context, Element, ElementId, Entity, GlobalElementId, IntoElement,
5    LayoutId, Pixels, Style, StyleRefinement, Styled, Window,
6    view::{paint_view, prepaint_cached_view},
7};
8use refineable::Refineable;
9
10/// Renders an [`Entity`] through a closure instead of a [`Render`][crate::Render]
11/// implementation.
12///
13/// [`Entity::cached`] gets the same reuse for types that implement
14/// [`Render`][crate::Render]. This is for state that has no renderer of its own,
15/// or that is rendered differently in different places.
16pub trait EntitySlotExt<T: 'static> {
17    /// Renders this entity's state through `builder`, reusing the subtree it
18    /// produces until the entity is notified.
19    ///
20    /// `id` identifies the slot, and becomes the root of the element id space the
21    /// subtree's internal state lives in, so an entity rendered twice in
22    /// different places needs a slot id per place.
23    ///
24    /// A slot is laid out from its [own style][crate::Styled], not from its
25    /// contents: reusing a subtree means never rendering it to measure it. Give
26    /// the slot a definite size, such as `.size_full()`.
27    fn slot<F, E>(&self, id: impl Into<ElementId>, builder: F) -> SlotElement<T, F>
28    where
29        F: Fn(&T, &mut Window, &mut Context<T>) -> E + 'static,
30        E: IntoElement + 'static;
31}
32
33impl<T: 'static> EntitySlotExt<T> for Entity<T> {
34    #[track_caller]
35    fn slot<F, E>(&self, id: impl Into<ElementId>, builder: F) -> SlotElement<T, F>
36    where
37        F: Fn(&T, &mut Window, &mut Context<T>) -> E + 'static,
38        E: IntoElement + 'static,
39    {
40        SlotElement {
41            entity: self.clone(),
42            id: id.into(),
43            style: StyleRefinement::default(),
44            builder,
45            #[cfg(debug_assertions)]
46            source: core::panic::Location::caller(),
47        }
48    }
49}
50
51/// The element [`EntitySlotExt::slot`] returns.
52///
53/// Re-evaluates `builder` only when the entity was notified, the bounds it is
54/// drawn at change, or the text style or content mask around it change. Otherwise
55/// the previous frame's subtree is replayed, hitboxes and scene records included.
56pub struct SlotElement<T: 'static, F> {
57    entity: Entity<T>,
58    id: ElementId,
59    style: StyleRefinement,
60    builder: F,
61    #[cfg(debug_assertions)]
62    source: &'static core::panic::Location<'static>,
63}
64
65impl<T: 'static, F, E> SlotElement<T, F>
66where
67    F: Fn(&T, &mut Window, &mut Context<T>) -> E + 'static,
68    E: IntoElement + 'static,
69{
70    /// Runs `builder` against the entity's current state.
71    fn build(&self, window: &mut Window, cx: &mut App) -> AnyElement {
72        self.entity.update(cx, |state, cx| {
73            (self.builder)(state, window, cx).into_any_element()
74        })
75    }
76}
77
78impl<T: 'static, F, E> Styled for SlotElement<T, F>
79where
80    F: Fn(&T, &mut Window, &mut Context<T>) -> E + 'static,
81    E: IntoElement + 'static,
82{
83    fn style(&mut self) -> &mut StyleRefinement {
84        &mut self.style
85    }
86}
87
88impl<T: 'static, F, E> IntoElement for SlotElement<T, F>
89where
90    F: Fn(&T, &mut Window, &mut Context<T>) -> E + 'static,
91    E: IntoElement + 'static,
92{
93    type Element = Self;
94
95    fn into_element(self) -> Self::Element {
96        self
97    }
98}
99
100impl<T: 'static, F, E> Element for SlotElement<T, F>
101where
102    F: Fn(&T, &mut Window, &mut Context<T>) -> E + 'static,
103    E: IntoElement + 'static,
104{
105    type RequestLayoutState = Option<AnyElement>;
106    type PrepaintState = Option<AnyElement>;
107
108    fn id(&self) -> Option<ElementId> {
109        Some(self.id.clone())
110    }
111
112    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
113        #[cfg(debug_assertions)]
114        return Some(self.source);
115
116        #[cfg(not(debug_assertions))]
117        return None;
118    }
119
120    fn request_layout(
121        &mut self,
122        _id: Option<&GlobalElementId>,
123        window: &mut Window,
124        cx: &mut App,
125    ) -> (LayoutId, Self::RequestLayoutState) {
126        let entity_id = self.entity.entity_id();
127        window.with_rendered_view(entity_id, |window| {
128            if window.is_inspector_picking(cx) {
129                // The inspector picks through the tree it can see, so a subtree
130                // that is being reused has to be built for it.
131                let mut element = self.build(window, cx);
132                let layout_id = element.request_layout(window, cx);
133                (layout_id, Some(element))
134            } else {
135                let mut root_style = Style::default();
136                root_style.refine(&self.style);
137                let layout_id = window.request_layout(root_style, None, cx);
138                (layout_id, None)
139            }
140        })
141    }
142
143    fn prepaint(
144        &mut self,
145        global_id: Option<&GlobalElementId>,
146        bounds: Bounds<Pixels>,
147        element: &mut Self::RequestLayoutState,
148        window: &mut Window,
149        cx: &mut App,
150    ) -> Option<AnyElement> {
151        let entity_id = self.entity.entity_id();
152        window.set_view_id(entity_id);
153        window.with_rendered_view(entity_id, |window| {
154            if let Some(mut element) = element.take() {
155                element.prepaint(window, cx);
156                return Some(element);
157            }
158
159            prepaint_cached_view(entity_id, global_id, bounds, window, cx, |window, cx| {
160                self.build(window, cx)
161            })
162        })
163    }
164
165    fn paint(
166        &mut self,
167        global_id: Option<&GlobalElementId>,
168        _bounds: Bounds<Pixels>,
169        _request_layout: &mut Self::RequestLayoutState,
170        element: &mut Self::PrepaintState,
171        window: &mut Window,
172        cx: &mut App,
173    ) {
174        paint_view(
175            self.entity.entity_id(),
176            true,
177            global_id,
178            element,
179            window,
180            cx,
181        );
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use std::{cell::Cell, rc::Rc};
188
189    use crate::{
190        AnyWindowHandle, AppContext as _, Context, Entity, EntitySlotExt as _, IntoElement,
191        ParentElement as _, Render, Styled as _, TestAppContext, Window, div, green, px,
192    };
193
194    struct Counter(usize);
195
196    struct SlotView {
197        counter: Entity<Counter>,
198        builds: Rc<Cell<usize>>,
199    }
200
201    impl Render for SlotView {
202        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
203            let builds = self.builds.clone();
204            div().size_full().child(
205                self.counter
206                    .slot("counter", move |_state, _window, _cx| {
207                        builds.set(builds.get() + 1);
208                        div().w(px(10.)).h(px(10.)).bg(green())
209                    })
210                    .size_full(),
211            )
212        }
213    }
214
215    /// Draws the window, returning how many quads the frame painted.
216    fn draw(cx: &mut TestAppContext, window: AnyWindowHandle) -> usize {
217        cx.update_window(window, |_, window, cx| {
218            window.draw(cx).clear(cx);
219            window.frame_state.rendered_frame.scene.quads.len()
220        })
221        .unwrap()
222    }
223
224    #[gpui::test]
225    fn a_slot_reuses_its_subtree_until_the_entity_notifies(cx: &mut TestAppContext) {
226        let builds = Rc::new(Cell::new(0));
227        let counter = cx.new(|_| Counter(1));
228        let window: AnyWindowHandle = cx
229            .add_window({
230                let counter = counter.clone();
231                let builds = builds.clone();
232                move |_, _| SlotView { counter, builds }
233            })
234            .into();
235
236        let first = draw(cx, window);
237        assert_eq!(builds.get(), 1, "the first frame builds the slot");
238        assert!(first > 0, "the slot's content is painted");
239
240        let second = draw(cx, window);
241        assert_eq!(builds.get(), 1, "a clean slot is not rebuilt");
242        assert_eq!(
243            second, first,
244            "the reused subtree is still painted into the scene"
245        );
246
247        counter.update(cx, |counter, cx| {
248            counter.0 += 1;
249            cx.notify();
250        });
251
252        let third = draw(cx, window);
253        assert!(builds.get() > 1, "notifying the entity rebuilds the slot");
254        assert_eq!(third, first, "the rebuilt subtree paints the same content");
255    }
256}