Skip to main content

guise/overlay/
modal.rs

1//! `Modal` — a centered dialog over a dimming backdrop.
2//!
3//! Controlled: the parent owns `opened` and renders the `Modal` only while it
4//! is true, passing an `on_close` handler. Place it as a child of a full-size
5//! root so the backdrop covers the window. Clicking the backdrop or the close
6//! button invokes `on_close`.
7
8use std::rc::Rc;
9
10use gpui::prelude::*;
11use gpui::{
12    deferred, div, px, AnyElement, App, ClickEvent, FontWeight, IntoElement, SharedString, Window,
13};
14
15use crate::devtools::ProbedAny;
16use crate::theme::{theme, Size};
17
18type CloseHandler = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
19
20/// A modal dialog.
21#[derive(IntoElement)]
22pub struct Modal {
23    title: Option<SharedString>,
24    children: Vec<AnyElement>,
25    width: f32,
26    padding: Size,
27    radius: Option<Size>,
28    on_close: Option<CloseHandler>,
29}
30
31impl Modal {
32    pub fn new() -> Self {
33        Modal {
34            title: None,
35            children: Vec::new(),
36            width: 440.0,
37            padding: Size::Lg,
38            radius: Some(Size::Md),
39            on_close: None,
40        }
41    }
42
43    pub fn title(mut self, title: impl Into<SharedString>) -> Self {
44        self.title = Some(title.into());
45        self
46    }
47
48    pub fn width(mut self, width: f32) -> Self {
49        self.width = width;
50        self
51    }
52
53    pub fn padding(mut self, padding: Size) -> Self {
54        self.padding = padding;
55        self
56    }
57
58    pub fn radius(mut self, radius: Size) -> Self {
59        self.radius = Some(radius);
60        self
61    }
62
63    /// Called when the backdrop or close button is clicked. Wire it with
64    /// `cx.listener(...)` to flip the parent's `opened` flag.
65    pub fn on_close(
66        mut self,
67        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
68    ) -> Self {
69        self.on_close = Some(Rc::new(handler));
70        self
71    }
72}
73
74impl Default for Modal {
75    fn default() -> Self {
76        Modal::new()
77    }
78}
79
80impl ParentElement for Modal {
81    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
82        self.children.extend(elements);
83    }
84}
85
86impl RenderOnce for Modal {
87    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
88        let t = theme(cx);
89        let radius = t.radius(self.radius.unwrap_or(Size::Md));
90        let padding = t.spacing(self.padding);
91        let gap = t.spacing(Size::Md);
92        let surface = t.surface().hsla();
93        let text = t.text().hsla();
94        let dimmed = t.dimmed().hsla();
95        let scrim = t.black.alpha(0.55);
96
97        let viewport = window.viewport_size();
98        let close = self.on_close.clone();
99
100        let mut dialog = div()
101            .id("guise-modal-dialog")
102            .occlude()
103            .flex()
104            .flex_col()
105            .gap(px(gap))
106            .w(px(self.width))
107            .bg(surface)
108            .rounded(px(radius))
109            .p(px(padding))
110            .shadow_xl()
111            .on_click(|_ev, _window, cx| cx.stop_propagation());
112
113        if self.title.is_some() || close.is_some() {
114            let mut header = div().flex().items_center().justify_between();
115            header = header.child(match self.title {
116                Some(title) => div()
117                    .text_size(px(18.0))
118                    .font_weight(FontWeight::BOLD)
119                    .text_color(text)
120                    .child(title),
121                None => div(),
122            });
123            if let Some(handler) = close.clone() {
124                header = header.child(
125                    div()
126                        .id("guise-modal-close")
127                        .px(px(6.0))
128                        .rounded(px(4.0))
129                        .text_size(px(18.0))
130                        .text_color(dimmed)
131                        .hover(move |s| s.text_color(text))
132                        .child(SharedString::new_static("\u{00d7}"))
133                        .on_click(move |ev, window, cx| {
134                            handler(ev, window, cx);
135                            cx.stop_propagation();
136                        }),
137                );
138            }
139            dialog = dialog.child(header);
140        }
141
142        dialog = dialog.children(self.children);
143
144        let mut backdrop = div()
145            .id("guise-modal-backdrop")
146            .occlude()
147            .absolute()
148            .top(px(0.0))
149            .left(px(0.0))
150            .w(viewport.width)
151            .h(viewport.height)
152            .flex()
153            .items_center()
154            .justify_center()
155            .bg(scrim)
156            .child(dialog);
157
158        if let Some(handler) = close {
159            backdrop = backdrop.on_click(move |ev, window, cx| handler(ev, window, cx));
160        }
161
162        deferred(backdrop).probe_any("Modal")
163    }
164}