1use gpui::{
2 AnyElement, App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement,
3 LayoutId, Pixels, Window, point, px,
4};
5use serde::{Deserialize, Serialize};
6
7#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
13#[non_exhaustive]
14pub enum DialogEntrance {
15 #[default]
17 SlideDown,
18 Fade,
20 FadeSlide,
25 None,
27}
28
29pub(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 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 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}