gpui_ui_kit/
dialog.rs

1//! Dialog/Modal component
2//!
3//! A modal dialog with backdrop, title, content, and footer sections.
4
5use gpui::prelude::*;
6use gpui::*;
7
8/// Dialog size variants
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum DialogSize {
11    /// Small dialog (320px)
12    Sm,
13    /// Medium dialog (480px)
14    #[default]
15    Md,
16    /// Large dialog (640px)
17    Lg,
18    /// Extra large dialog (800px)
19    Xl,
20    /// Full width (90%)
21    Full,
22}
23
24impl DialogSize {
25    fn width(&self) -> Rems {
26        match self {
27            DialogSize::Sm => Rems(20.0),
28            DialogSize::Md => Rems(30.0),
29            DialogSize::Lg => Rems(40.0),
30            DialogSize::Xl => Rems(50.0),
31            DialogSize::Full => Rems(60.0),
32        }
33    }
34}
35
36/// A modal dialog component
37pub struct Dialog {
38    id: ElementId,
39    title: Option<SharedString>,
40    size: DialogSize,
41    content: Option<AnyElement>,
42    footer: Option<AnyElement>,
43    show_close_button: bool,
44    close_on_backdrop: bool,
45    on_close: Option<Box<dyn Fn(&mut Window, &mut App) + 'static>>,
46}
47
48impl Dialog {
49    /// Create a new dialog
50    pub fn new(id: impl Into<ElementId>) -> Self {
51        Self {
52            id: id.into(),
53            title: None,
54            size: DialogSize::default(),
55            content: None,
56            footer: None,
57            show_close_button: true,
58            close_on_backdrop: true,
59            on_close: None,
60        }
61    }
62
63    /// Set the dialog title
64    pub fn title(mut self, title: impl Into<SharedString>) -> Self {
65        self.title = Some(title.into());
66        self
67    }
68
69    /// Set the dialog size
70    pub fn size(mut self, size: DialogSize) -> Self {
71        self.size = size;
72        self
73    }
74
75    /// Set the dialog content
76    pub fn content(mut self, element: impl IntoElement) -> Self {
77        self.content = Some(element.into_any_element());
78        self
79    }
80
81    /// Alias for content (matches adabraka-ui API)
82    pub fn child(self, element: impl IntoElement) -> Self {
83        self.content(element)
84    }
85
86    /// Set the dialog footer
87    pub fn footer(mut self, element: impl IntoElement) -> Self {
88        self.footer = Some(element.into_any_element());
89        self
90    }
91
92    /// Show or hide the close button
93    pub fn show_close_button(mut self, show: bool) -> Self {
94        self.show_close_button = show;
95        self
96    }
97
98    /// Close dialog when clicking backdrop
99    pub fn close_on_backdrop(mut self, close: bool) -> Self {
100        self.close_on_backdrop = close;
101        self
102    }
103
104    /// Set the close handler
105    pub fn on_close(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
106        self.on_close = Some(Box::new(handler));
107        self
108    }
109
110    /// Build the dialog into elements
111    pub fn build(self) -> Div {
112        let width = self.size.width();
113        let on_close = self.on_close;
114        let close_on_backdrop = self.close_on_backdrop;
115
116        // Backdrop
117        let mut backdrop = div()
118            .absolute()
119            .inset_0()
120            .flex()
121            .items_center()
122            .justify_center()
123            .bg(rgba(0x000000aa));
124
125        // Handle backdrop click
126        if close_on_backdrop {
127            if let Some(ref handler) = on_close {
128                let handler: *const dyn Fn(&mut Window, &mut App) = handler.as_ref();
129                backdrop = backdrop.on_mouse_down(MouseButton::Left, move |_event, window, cx| {
130                    // Safety: handler is valid for the lifetime of the dialog
131                    unsafe {
132                        (*handler)(window, cx);
133                    }
134                });
135            }
136        }
137
138        // Dialog container
139        let mut dialog = div()
140            .id(self.id)
141            .w(width)
142            .max_h(Rems(45.0))
143            .bg(rgb(0x1e1e1e))
144            .border_1()
145            .border_color(rgb(0x007acc))
146            .rounded_lg()
147            .shadow_lg()
148            .overflow_hidden()
149            .flex()
150            .flex_col()
151            // Stop propagation so clicking dialog doesn't close it
152            .on_mouse_down(MouseButton::Left, |_event, _window, _cx| {
153                // Consume the event
154            });
155
156        // Header with title and close button
157        if self.title.is_some() || self.show_close_button {
158            let mut header = div()
159                .flex()
160                .items_center()
161                .justify_between()
162                .px_4()
163                .py_3()
164                .border_b_1()
165                .border_color(rgb(0x3a3a3a));
166
167            if let Some(title) = self.title {
168                header = header.child(
169                    div()
170                        .text_lg()
171                        .font_weight(FontWeight::BOLD)
172                        .text_color(rgb(0xffffff))
173                        .child(title),
174                );
175            } else {
176                header = header.child(div()); // Spacer
177            }
178
179            if self.show_close_button {
180                if let Some(ref handler) = on_close {
181                    let handler: *const dyn Fn(&mut Window, &mut App) = handler.as_ref();
182                    header = header.child(
183                        div()
184                            .id("dialog-close-btn")
185                            .px_2()
186                            .py_1()
187                            .rounded(px(3.0))
188                            .cursor_pointer()
189                            .text_color(rgb(0x888888))
190                            .hover(|s| s.bg(rgb(0x3a3a3a)).text_color(rgb(0xffffff)))
191                            .on_mouse_up(MouseButton::Left, move |_event, window, cx| unsafe {
192                                (*handler)(window, cx);
193                            })
194                            .child("×"),
195                    );
196                }
197            }
198
199            dialog = dialog.child(header);
200        }
201
202        // Content
203        if let Some(content) = self.content {
204            dialog = dialog.child(
205                div()
206                    .id("dialog-content")
207                    .flex_1()
208                    .overflow_y_scroll()
209                    .px_4()
210                    .py_4()
211                    .child(content),
212            );
213        }
214
215        // Footer
216        if let Some(footer) = self.footer {
217            dialog = dialog.child(
218                div()
219                    .px_4()
220                    .py_3()
221                    .border_t_1()
222                    .border_color(rgb(0x3a3a3a))
223                    .child(footer),
224            );
225        }
226
227        backdrop.child(dialog)
228    }
229}
230
231impl IntoElement for Dialog {
232    type Element = Div;
233
234    fn into_element(self) -> Self::Element {
235        self.build()
236    }
237}