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}