Skip to main content

gpui_component/dialog/
entrance.rs

1use gpui::{
2    AnyElement, App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement,
3    LayoutId, Pixels, Window, point, px,
4};
5use serde::{Deserialize, Serialize};
6
7/// The entrance presentation shared by Dialog and AlertDialog.
8///
9/// Closing animates the surface and backdrop back out. Reduced motion displays
10/// the final state for every option, and [`DialogEntrance::None`] opens and
11/// closes in a single frame.
12#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
13#[non_exhaustive]
14pub enum DialogEntrance {
15    /// Slide down from the window top while fading in, preserving the legacy entrance.
16    #[default]
17    SlideDown,
18    /// Fade in at the final position without movement.
19    Fade,
20    /// Fade in while settling from just above the final position.
21    ///
22    /// Travel comes from the theme's short motion distance and is limited by
23    /// the space above the resolved surface inside the window.
24    FadeSlide,
25    /// Show the surface and backdrop immediately without entrance animation.
26    None,
27}
28
29/// Applies presentation travel after Positioner has resolved the resting bounds.
30/// Paint, hit testing, and accessibility follow the same offset.
31pub(super) struct EntranceSurface {
32    child: AnyElement,
33    travel: Pixels,
34    top_limit: Pixels,
35    pub(super) progress: f32,
36}
37
38impl EntranceSurface {
39    pub(super) fn new(child: AnyElement, travel: Pixels, top_limit: Pixels) -> Self {
40        Self {
41            child,
42            travel,
43            top_limit,
44            progress: 0.,
45        }
46    }
47}
48
49impl IntoElement for EntranceSurface {
50    type Element = Self;
51
52    fn into_element(self) -> Self::Element {
53        self
54    }
55}
56
57impl Element for EntranceSurface {
58    type RequestLayoutState = ();
59    type PrepaintState = ();
60
61    fn id(&self) -> Option<ElementId> {
62        None
63    }
64
65    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
66        None
67    }
68
69    fn request_layout(
70        &mut self,
71        _: Option<&GlobalElementId>,
72        _: Option<&InspectorElementId>,
73        window: &mut Window,
74        cx: &mut App,
75    ) -> (LayoutId, ()) {
76        (self.child.request_layout(window, cx), ())
77    }
78
79    fn prepaint(
80        &mut self,
81        _: Option<&GlobalElementId>,
82        _: Option<&InspectorElementId>,
83        bounds: Bounds<Pixels>,
84        _: &mut (),
85        window: &mut Window,
86        cx: &mut App,
87    ) {
88        let available = (bounds.top() - self.top_limit).max(px(0.));
89        let travel = self.travel.max(px(0.)).min(available);
90        let offset = point(px(0.), -travel * (1. - self.progress.clamp(0., 1.)));
91        window.with_element_offset(offset, |window| self.child.prepaint(window, cx));
92    }
93
94    fn paint(
95        &mut self,
96        _: Option<&GlobalElementId>,
97        _: Option<&InspectorElementId>,
98        _: Bounds<Pixels>,
99        _: &mut (),
100        _: &mut (),
101        window: &mut Window,
102        cx: &mut App,
103    ) {
104        self.child.paint(window, cx);
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use gpui::{
112        Anchor, Context, InteractiveElement as _, ParentElement as _, Render,
113        StatefulInteractiveElement as _, Styled as _, TestAppContext, div, size,
114    };
115
116    struct SurfaceProbe {
117        progress: f32,
118        requested_y: Pixels,
119        clicks: usize,
120    }
121
122    impl Render for SurfaceProbe {
123        fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
124            let mut surface = EntranceSurface::new(
125                div()
126                    .id("surface")
127                    .w(px(200.))
128                    .h(px(300.))
129                    .debug_selector(|| "entrance-surface".into())
130                    .on_click(cx.listener(|probe, _, _, _| probe.clicks += 1))
131                    .into_any_element(),
132                px(8.),
133                px(16.),
134            );
135            surface.progress = self.progress;
136            div().size_full().child(
137                gpui_base::Positioner::corner(Anchor::TopLeft, point(px(100.), self.requested_y))
138                    .margin(px(16.))
139                    .child(surface),
140            )
141        }
142    }
143
144    #[gpui::test]
145    fn fade_slide_uses_resolved_bounds_and_keeps_the_surface_inside_the_window(
146        cx: &mut TestAppContext,
147    ) {
148        let (view, cx) = cx.add_window_view(|_, _| SurfaceProbe {
149            progress: 0.,
150            requested_y: px(200.),
151            clicks: 0,
152        });
153        cx.simulate_resize(size(px(400.), px(400.)));
154        // Requested y=200 resolves to y=84 because the surface is 300px tall.
155        for (requested_y, positions) in [
156            (px(200.), [px(76.), px(80.), px(84.)]),
157            (px(20.), [px(16.), px(18.), px(20.)]),
158            (px(0.), [px(16.), px(16.), px(16.)]),
159        ] {
160            for (progress, expected_y) in [0., 0.5, 1.].into_iter().zip(positions) {
161                cx.update(|window, cx| {
162                    view.update(cx, |probe, cx| {
163                        probe.progress = progress;
164                        probe.requested_y = requested_y;
165                        cx.notify();
166                    });
167                    window.draw(cx).clear(cx);
168                });
169                let bounds = cx.debug_bounds("entrance-surface").unwrap();
170                assert_eq!(bounds.top(), expected_y);
171                assert!(bounds.top() >= px(16.) && bounds.bottom() <= px(384.));
172                // At the beginning of the clamped entrance this point is
173                // above the resting surface, so the hitbox must travel too.
174                cx.simulate_click(bounds.origin + point(px(10.), px(2.)), Default::default());
175            }
176        }
177        cx.update(|_, cx| assert_eq!(view.read(cx).clicks, 9));
178    }
179}