1use std::rc::Rc;
8
9use gpui::{
10 Anchor, AnyElement, App, ClickEvent, Div, ElementId, IntoElement, Pixels, Point, RenderOnce,
11 Stateful, Window, div, prelude::*, px,
12};
13use gpui_kit_theme::{Elevation, Layer, Theme};
14
15use crate::foundation::{ActiveTheme, Ident, StyledExt};
16
17type DismissHandler = Rc<dyn Fn(&mut Window, &mut App)>;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum Edge {
22 Left,
23 Right,
24 Top,
25 Bottom,
26}
27
28impl Edge {
29 pub fn is_horizontal(self) -> bool {
31 matches!(self, Self::Left | Self::Right)
32 }
33
34 pub fn is_leading(self) -> bool {
36 matches!(self, Self::Left | Self::Top)
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq)]
42pub enum Placement {
43 Below,
45 Above,
47 At(Point<Pixels>),
50 Center,
52 Edge(Edge),
54}
55
56#[derive(IntoElement)]
61pub struct Overlay {
62 ident: Ident,
63 layer: Layer,
64 placement: Placement,
65 window_snap_margin: Option<Pixels>,
66 scrim: bool,
67 content: Option<AnyElement>,
68 on_dismiss: Option<DismissHandler>,
69}
70
71impl std::fmt::Debug for Overlay {
72 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 formatter
74 .debug_struct("Overlay")
75 .field("ident", &self.ident)
76 .field("layer", &self.layer)
77 .field("placement", &self.placement)
78 .field("window_snap_margin", &self.window_snap_margin)
79 .field("scrim", &self.scrim)
80 .field("dismissible", &self.on_dismiss.is_some())
81 .finish()
82 }
83}
84
85impl Overlay {
86 pub fn new(ident: impl Into<Ident>) -> Self {
87 Self {
88 ident: ident.into(),
89 layer: Layer::Popover,
90 placement: Placement::Below,
91 window_snap_margin: None,
92 scrim: false,
93 content: None,
94 on_dismiss: None,
95 }
96 }
97
98 pub fn modal(ident: impl Into<Ident>) -> Self {
100 Self::new(ident)
101 .layer(Layer::Modal)
102 .placement(Placement::Center)
103 .scrim(true)
104 }
105
106 pub fn edge(ident: impl Into<Ident>, edge: Edge) -> Self {
109 Self::new(ident)
110 .layer(Layer::Modal)
111 .placement(Placement::Edge(edge))
112 .scrim(true)
113 }
114
115 pub fn layer(mut self, layer: Layer) -> Self {
116 self.layer = layer;
117 self
118 }
119
120 pub fn placement(mut self, placement: Placement) -> Self {
121 self.placement = placement;
122 self
123 }
124
125 pub(crate) fn window_snap_margin(mut self, margin: Pixels) -> Self {
131 self.window_snap_margin = Some(margin);
132 self
133 }
134
135 pub fn scrim(mut self, scrim: bool) -> Self {
137 self.scrim = scrim;
138 self
139 }
140
141 pub fn child(mut self, content: impl IntoElement) -> Self {
142 self.content = Some(content.into_any_element());
143 self
144 }
145
146 pub fn on_dismiss(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
149 self.on_dismiss = Some(Rc::new(handler));
150 self
151 }
152
153 fn anchor(&self) -> Anchor {
154 match self.placement {
155 Placement::Above => Anchor::BottomLeft,
156 _ => Anchor::TopLeft,
157 }
158 }
159}
160
161impl RenderOnce for Overlay {
162 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
163 let theme = cx.theme().clone();
164 let viewport = window.viewport_size();
167 let element_id: ElementId = self.ident.element_id();
168 let anchor = self.anchor();
169 let content = self.content.unwrap_or_else(|| div().into_any_element());
170
171 let surface = div()
172 .id(element_id)
173 .occlude()
174 .child(content)
175 .into_any_element();
176
177 let mut anchored = gpui::anchored().anchor(anchor);
181 if let Placement::At(position) = self.placement {
182 anchored = anchored.position(position);
183 }
184 if let Some(margin) = self.window_snap_margin {
185 anchored = anchored.snap_to_window_with_margin(margin);
186 } else if self.placement == Placement::Center {
187 anchored = anchored.snap_to_window_with_margin(px(theme.spacing.sm));
188 }
189
190 let placed = match self.placement {
191 Placement::Center => scrim_frame(&theme, viewport, self.scrim, self.on_dismiss.clone())
192 .items_center()
193 .justify_center()
194 .child(surface)
195 .into_any_element(),
196 Placement::Edge(edge) => {
200 scrim_frame(&theme, viewport, self.scrim, self.on_dismiss.clone())
201 .map(|frame| {
202 if edge.is_horizontal() {
203 frame.flex_row()
204 } else {
205 frame.flex_col()
206 }
207 })
208 .map(|frame| {
209 if edge.is_leading() {
210 frame.justify_start()
211 } else {
212 frame.justify_end()
213 }
214 })
215 .child(surface)
216 .into_any_element()
217 }
218 _ if self.scrim => scrim_frame(&theme, viewport, true, self.on_dismiss.clone())
219 .child(anchored.child(surface))
220 .into_any_element(),
221 _ => anchored.child(surface).into_any_element(),
222 };
223
224 pinned(
227 gpui::deferred(placed)
228 .priority(priority(&theme, self.layer))
229 .into_any_element(),
230 )
231 }
232}
233
234pub fn surface(theme: &Theme, elevation: Elevation) -> Div {
236 div()
237 .column()
238 .bg(theme.colors.overlay)
239 .radius(theme, gpui_kit_theme::Radius::Card)
240 .elevation(theme, elevation)
241 .overflow_hidden()
242 .text_color(theme.colors.text)
243}
244
245pub fn priority(theme: &Theme, layer: Layer) -> usize {
247 theme.layer(layer).max(0) as usize
248}
249
250fn scrim_frame(
251 theme: &Theme,
252 viewport: gpui::Size<Pixels>,
253 visible: bool,
254 on_dismiss: Option<DismissHandler>,
255) -> Stateful<Div> {
256 let mut frame = div()
257 .id("overlay.scrim")
258 .occlude()
259 .absolute()
260 .top_0()
261 .left_0()
262 .w(viewport.width)
263 .h(viewport.height)
264 .flex();
265 if visible {
266 frame = frame.bg(gpui::black().opacity(theme.opacity.scrim));
267 }
268 if let Some(handler) = on_dismiss {
269 frame = frame.on_click(move |_: &ClickEvent, window, cx| handler(window, cx));
270 }
271 frame
272}
273
274pub(crate) fn pinned(layer: AnyElement) -> AnyElement {
277 div()
278 .absolute()
279 .top_0()
280 .left_0()
281 .size_0()
282 .child(layer)
283 .into_any_element()
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 #[test]
291 fn layers_paint_in_token_order() {
292 let theme = Theme::studio_dark();
293 assert!(priority(&theme, Layer::Tooltip) > priority(&theme, Layer::Popover));
294 assert!(priority(&theme, Layer::Toast) > priority(&theme, Layer::Modal));
295 assert_eq!(priority(&theme, Layer::Content), 0);
296 }
297
298 #[test]
299 fn a_modal_defaults_to_a_centered_scrimmed_dialog() {
300 let overlay = Overlay::modal("confirm");
301 assert_eq!(overlay.layer, Layer::Modal);
302 assert_eq!(overlay.placement, Placement::Center);
303 assert!(overlay.scrim);
304 }
305
306 #[test]
307 fn placement_decides_which_edge_the_surface_hangs_from() {
308 assert_eq!(
309 Overlay::new("menu").placement(Placement::Above).anchor(),
310 Anchor::BottomLeft
311 );
312 assert_eq!(
313 Overlay::new("menu").placement(Placement::Below).anchor(),
314 Anchor::TopLeft
315 );
316 }
317}