Skip to main content

gpui/elements/
deferred.rs

1use crate::{
2    AnyElement, App, Bounds, ContentMask, Element, GlobalElementId, InspectorElementId,
3    IntoElement, LayoutId, Pixels, Window,
4};
5
6/// Creates a [`Deferred`] element that draws `child` after the rest of the tree.
7///
8/// Use it for content that must appear on top of its surroundings, such as a popover, a menu or a
9/// drag preview. The child takes part in layout where it is, so its size and position follow the
10/// normal rules. Its prepaint and paint are postponed until the rest of the tree has been drawn.
11/// The priority defaults to `0`. See [`Deferred::priority`].
12///
13/// # Example
14///
15/// ```
16/// use gpui::{
17///     Context, IntoElement, Render, TestApp, Window, anchored, deferred, div, point, prelude::*,
18///     px,
19/// };
20///
21/// struct Overlay;
22///
23/// impl Render for Overlay {
24///     fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
25///         div().size_full().child(
26///             deferred(
27///                 anchored()
28///                     .position(point(px(10.), px(10.)))
29///                     .child(div().w(px(40.)).h(px(40.))),
30///             )
31///             .priority(1),
32///         )
33///     }
34/// }
35///
36/// let mut app = TestApp::new();
37/// let mut window = app.open_window(|_window, _cx| Overlay);
38/// window.draw();
39/// ```
40pub fn deferred(child: impl IntoElement) -> Deferred {
41    Deferred {
42        child: Some(child.into_any_element()),
43        priority: DeferredPriority::from(0),
44        content_mask: None,
45    }
46}
47
48/// An element that draws its child after the rest of the tree, in priority order.
49///
50/// The child is laid out in place, so it is part of the element tree. Its prepaint and paint run
51/// later, in a separate pass. Each deferred child is drawn after the elements around it, and
52/// deferred children are drawn in ascending priority, so a higher priority draws on top. Create
53/// one with [`deferred`].
54///
55/// A deferred child may create more deferred children. GPUI processes these in rounds, and the
56/// frame panics if they nest more than ten rounds deep. Keep such nesting shallow.
57#[derive(derive_setters::Setters)]
58#[setters(generate = false)]
59pub struct Deferred {
60    child: Option<AnyElement>,
61    /// Sets the draw priority. Deferred elements draw in ascending priority, so a higher value
62    /// draws on top of the ones below it. Accepts a number, or a [`DeferredPriority`].
63    #[setters(generate, into)]
64    priority: DeferredPriority,
65    /// Clips the deferred element to `mask` while it is prepainted and painted, so the clipped
66    /// area also doesn't receive mouse input.
67    ///
68    /// Without a mask, no extra clipping is applied.
69    #[setters(generate, strip_option)]
70    content_mask: Option<ContentMask<Pixels>>,
71}
72
73impl Element for Deferred {
74    type RequestLayoutState = ();
75    type PrepaintState = ();
76
77    fn id(&self) -> Option<crate::ElementId> {
78        None
79    }
80
81    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
82        None
83    }
84
85    fn request_layout(
86        &mut self,
87        _id: Option<&GlobalElementId>,
88        _inspector_id: Option<&InspectorElementId>,
89        window: &mut Window,
90        cx: &mut App,
91    ) -> (LayoutId, ()) {
92        let layout_id = self.child.as_mut().unwrap().request_layout(window, cx);
93        (layout_id, ())
94    }
95
96    fn prepaint(
97        &mut self,
98        _id: Option<&GlobalElementId>,
99        _inspector_id: Option<&InspectorElementId>,
100        _bounds: Bounds<Pixels>,
101        _request_layout: &mut Self::RequestLayoutState,
102        window: &mut Window,
103        _cx: &mut App,
104    ) {
105        let child = self.child.take().unwrap();
106        let element_offset = window.element_offset();
107        let priority = self.priority.evaluate(window);
108        window.defer_draw(child, element_offset, priority, self.content_mask);
109    }
110
111    fn paint(
112        &mut self,
113        _id: Option<&GlobalElementId>,
114        _inspector_id: Option<&InspectorElementId>,
115        _bounds: Bounds<Pixels>,
116        _request_layout: &mut Self::RequestLayoutState,
117        _prepaint: &mut Self::PrepaintState,
118        _window: &mut Window,
119        _cx: &mut App,
120    ) {
121    }
122}
123
124impl IntoElement for Deferred {
125    type Element = Self;
126
127    fn into_element(self) -> Self::Element {
128        self
129    }
130}
131
132impl Deferred {
133    /// Sets the priority to one more than the enclosing deferred element's, or to 1 when no deferred
134    /// element encloses this one.
135    ///
136    /// A deferred element is enclosing when it is being prepainted and creates this one, directly or
137    /// through other elements. Use it for nested popups, so that each level draws above the last.
138    pub fn priority_auto(mut self) -> Self {
139        self.priority = DeferredPriority::Auto;
140        self
141    }
142}
143
144/// How the draw priority of a [`Deferred`] element is chosen.
145///
146/// A number converts into `Value`, so `.priority(2)` works.
147pub enum DeferredPriority {
148    /// One more than the priority of the deferred element that created this one, or 1 when no
149    /// deferred element did. Use it for nested popups.
150    Auto,
151    /// An explicit priority. Higher values draw on top.
152    Value(usize),
153}
154impl From<usize> for DeferredPriority {
155    fn from(value: usize) -> Self {
156        Self::Value(value)
157    }
158}
159impl DeferredPriority {
160    /// Calculates the new priority based on the current deferred nesting depth.
161    /// Should only be called during prepaint of a [`Deferred`] element.
162    fn evaluate(&self, window: &Window) -> usize {
163        match self {
164            DeferredPriority::Value(value) => *value,
165            DeferredPriority::Auto => match window.deferred_priority() {
166                None => 1, // NOTE: functionality change. If using auto, we start at a priority of 1 instead of 0.
167                Some(prev_deferred_priority) => prev_deferred_priority.saturating_add(1),
168            },
169        }
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use crate::{
176        Bounds, ContentMask, Context, Entity, Modifiers, MouseButton, MouseDownEvent,
177        PlatformInput, StyleRefinement, TestAppContext, Window, anchored, deferred, div, point,
178        prelude::*, px, size,
179    };
180    use std::{cell::Cell, rc::Rc};
181
182    struct MaskedOverlay {
183        presses: Rc<Cell<usize>>,
184    }
185
186    impl Render for MaskedOverlay {
187        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
188            let presses = self.presses.clone();
189            deferred(div().id("masked-overlay").size(px(100.)).on_mouse_down(
190                MouseButton::Left,
191                move |_, _, _| {
192                    presses.set(presses.get() + 1);
193                },
194            ))
195            .content_mask(ContentMask {
196                bounds: Bounds::new(point(px(0.), px(0.)), size(px(40.), px(40.))),
197                fade_out: Default::default(),
198            })
199        }
200    }
201
202    struct MaskedOverlayRoot {
203        overlay: Entity<MaskedOverlay>,
204    }
205
206    impl Render for MaskedOverlayRoot {
207        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
208            div().size_full().child(
209                self.overlay
210                    .clone()
211                    .cached(StyleRefinement::default().size_full()),
212            )
213        }
214    }
215
216    /// A deferred element's content mask must clip its hitboxes, which are
217    /// inserted during prepaint, and not only its painting.
218    #[gpui::test]
219    fn test_deferred_mask_clips_input(cx: &mut TestAppContext) {
220        let presses = Rc::new(Cell::new(0));
221        let window = cx.open_window(size(px(200.), px(200.)), |_, cx| {
222            let overlay = cx.new(|_| MaskedOverlay {
223                presses: presses.clone(),
224            });
225            MaskedOverlayRoot { overlay }
226        });
227        cx.run_until_parked();
228
229        // The second pass reuses the cached overlay's prepaint from the first.
230        for pass in 0..2 {
231            window
232                .update(cx, |_, window, cx| {
233                    for position in [point(px(60.), px(60.)), point(px(20.), px(20.))] {
234                        window.dispatch_event(
235                            PlatformInput::MouseDown(MouseDownEvent {
236                                button: MouseButton::Left,
237                                position,
238                                modifiers: Modifiers::default(),
239                                click_count: 1,
240                                first_mouse: false,
241                            }),
242                            cx,
243                        );
244                    }
245                })
246                .unwrap();
247            assert_eq!(
248                presses.get(),
249                pass + 1,
250                "only the visible region accepts input"
251            );
252
253            window.update(cx, |_, _, cx| cx.notify()).unwrap();
254            cx.run_until_parked();
255        }
256    }
257
258    /// A stand-in for a dock panel hosting a popover (deferred draw) whose
259    /// content opens another popover (a deferred draw created while
260    /// prepainting the first one's content).
261    struct PanelView;
262
263    impl Render for PanelView {
264        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
265            div().key_context("Panel").size_full().child(
266                deferred(
267                    anchored().position(point(px(10.), px(10.))).child(
268                        div().key_context("Popover").w(px(200.)).h(px(200.)).child(
269                            deferred(
270                                anchored().position(point(px(30.), px(30.))).child(
271                                    div()
272                                        .key_context("NestedMenu")
273                                        .debug_selector(|| "NESTED_MENU".into())
274                                        .w(px(50.))
275                                        .h(px(50.)),
276                                ),
277                            )
278                            .priority(2),
279                        ),
280                    ),
281                )
282                .priority(1),
283            )
284        }
285    }
286
287    struct RootView {
288        panel: Entity<PanelView>,
289    }
290
291    impl Render for RootView {
292        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
293            div().key_context("Root").size_full().child(
294                self.panel
295                    .clone()
296                    .cached(StyleRefinement::default().size_full()),
297            )
298        }
299    }
300
301    /// Regression test for a crash with nested deferred draws (e.g. a popover
302    /// menu inside a popover hosted by a cached dock panel). Prepaint indices
303    /// recorded during the deferred draw rounds must index the same
304    /// `deferred_draws` vector that `reuse_prepaint` slices on the next frame;
305    /// previously they were measured against a transient per-round vector, so
306    /// reusing the panel's subtree grafted the wrong deferred draws and
307    /// panicked in the dispatch tree.
308    #[gpui::test]
309    fn test_nested_deferred_draws_with_reused_views(cx: &mut TestAppContext) {
310        let window = cx.open_window(size(px(800.), px(600.)), |_, cx| {
311            let panel = cx.new(|_| PanelView);
312            RootView { panel }
313        });
314        cx.run_until_parked();
315
316        let menu_bounds = window
317            .update(cx, |_, window, _| {
318                window
319                    .rendered_frame
320                    .debug_bounds
321                    .get("NESTED_MENU")
322                    .copied()
323            })
324            .unwrap()
325            .expect("NESTED_MENU debug bounds not found");
326        assert_eq!(menu_bounds.size, size(px(50.), px(50.)));
327
328        // Re-render only the root view; the panel is cached, so its subtree -
329        // including both deferred draw records - is reused from the previous
330        // frame.
331        window.update(cx, |_, _, cx| cx.notify()).unwrap();
332        cx.run_until_parked();
333
334        // Reuse the subtree a second time, exercising ranges that were
335        // themselves recorded during a reused frame.
336        window.update(cx, |_, _, cx| cx.notify()).unwrap();
337        cx.run_until_parked();
338
339        // Re-render the panel itself again to prove the popovers still draw.
340        window
341            .update(cx, |root, _, cx| {
342                root.panel.update(cx, |_, cx| cx.notify());
343            })
344            .unwrap();
345        cx.run_until_parked();
346
347        window
348            .update(cx, |_, window, _| {
349                assert_eq!(window.rendered_frame.deferred_draws.len(), 2);
350                assert!(
351                    window
352                        .rendered_frame
353                        .debug_bounds
354                        .contains_key("NESTED_MENU")
355                );
356            })
357            .unwrap();
358    }
359}