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