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)]
11#[non_exhaustive]
12pub enum DialogEntrance {
13 #[default]
15 SlideDown,
16 Fade,
18 FadeSlide,
23 None,
25}
26
27pub(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 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 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}