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