Skip to main content

liora_components/
dialog.rs

1use crate::gpui_compat::element_id;
2use crate::motion::{fade_in, pop_in};
3use gpui::{
4    AnyElement, App, Context, IntoElement, KeyBinding, MouseButton, Render, SharedString, Window,
5    actions, div, prelude::*, px,
6};
7use liora_core::Config;
8use liora_icons::Icon;
9use liora_icons_lucide::IconName;
10use std::sync::Arc;
11
12actions!(dialog, [DialogClose]);
13
14pub struct Dialog {
15    id: SharedString,
16    title: SharedString,
17    content: Arc<dyn Fn(&mut Window, &mut Context<DialogView>) -> AnyElement + 'static>,
18    close_on_click_outside: bool,
19    close_on_escape: bool,
20}
21
22pub struct DialogView {
23    id: SharedString,
24    title: SharedString,
25    content: Arc<dyn Fn(&mut Window, &mut Context<Self>) -> AnyElement + 'static>,
26    close_on_click_outside: bool,
27    close_on_escape: bool,
28    on_close: Arc<dyn Fn(&mut Window, &mut App) + 'static>,
29}
30
31impl DialogView {
32    fn new(
33        id: SharedString,
34        title: SharedString,
35        content: Arc<dyn Fn(&mut Window, &mut Context<Self>) -> AnyElement + 'static>,
36        close_on_click_outside: bool,
37        close_on_escape: bool,
38        on_close: impl Fn(&mut Window, &mut App) + 'static,
39    ) -> Self {
40        Self {
41            id,
42            title,
43            content,
44            close_on_click_outside,
45            close_on_escape,
46            on_close: Arc::new(on_close),
47        }
48    }
49}
50
51impl Render for DialogView {
52    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
53        let theme = cx.global::<Config>().theme.clone();
54        let id = self.id.clone();
55        let title = self.title.clone();
56        let content_fn = self.content.clone();
57        let on_close = self.on_close.clone();
58        let close_on_click_outside = self.close_on_click_outside;
59        let close_on_escape = self.close_on_escape;
60
61        fade_in(
62            element_id(format!("{id}-overlay-motion")),
63            div()
64                .id(id.clone())
65                .absolute()
66                .size_full()
67                .cursor_default()
68                .bg(theme.neutral.overlay)
69                .flex()
70                .items_center()
71                .justify_center()
72                .on_mouse_move(|_, _, cx| {
73                    cx.stop_propagation();
74                })
75                .when(close_on_click_outside, |s| {
76                    s.on_mouse_down(MouseButton::Left, {
77                        let on_close = on_close.clone();
78                        move |_, window, cx| {
79                            on_close(window, cx);
80                        }
81                    })
82                })
83                .when(close_on_escape, |s| {
84                    s.on_action(cx.listener({
85                        let on_close = on_close.clone();
86                        move |_, _action: &DialogClose, window, cx| {
87                            on_close(window, cx);
88                        }
89                    }))
90                })
91                .child(pop_in(
92                    element_id(format!("{id}-panel-motion")),
93                    div()
94                        .w(px(400.0))
95                        .bg(theme.neutral.card)
96                        .cursor_default()
97                        .rounded(px(theme.radius.md))
98                        .shadow_xl()
99                        .on_mouse_move(|_, _, cx| {
100                            cx.stop_propagation();
101                        })
102                        .on_mouse_down(MouseButton::Left, |_, _, cx| {
103                            cx.stop_propagation();
104                        }) // Consume click so it doesn't trigger the background
105                        .child(
106                            div()
107                                .p_4()
108                                .border_b_1()
109                                .border_color(theme.neutral.border)
110                                .flex()
111                                .justify_between()
112                                .items_center()
113                                .child(div().font_weight(gpui::FontWeight::BOLD).child(title))
114                                .child(
115                                    div()
116                                        .id(element_id(format!("{id}-close-btn")))
117                                        .cursor_pointer()
118                                        .child(
119                                            Icon::new(IconName::X)
120                                                .size(px(16.0))
121                                                .color(theme.neutral.icon),
122                                        )
123                                        .on_mouse_down(MouseButton::Left, move |_, window, cx| {
124                                            on_close(window, cx);
125                                        }),
126                                ),
127                        )
128                        .child(div().p_4().child(content_fn(_window, cx))),
129                )),
130        )
131    }
132}
133
134#[cfg(test)]
135mod motion_tests {
136    #[test]
137    fn dialog_uses_liora_motion_on_overlay_and_panel() {
138        let source = include_str!("dialog.rs")
139            .split("#[cfg(test)]")
140            .next()
141            .unwrap();
142
143        assert!(source.contains("fade_in("));
144        assert!(source.contains("pop_in("));
145        assert!(source.contains("panel-motion"));
146    }
147}
148
149impl Dialog {
150    pub fn register_key_bindings(cx: &mut App) {
151        cx.bind_keys([KeyBinding::new("escape", DialogClose, None)]);
152    }
153
154    pub fn new() -> Self {
155        Self {
156            id: liora_core::unique_id("dialog"),
157            title: SharedString::default(),
158            content: Arc::new(|_, _| div().child("Dialog Content").into_any_element()),
159            close_on_click_outside: true,
160            close_on_escape: true,
161        }
162    }
163
164    pub fn id(mut self, id: impl Into<SharedString>) -> Self {
165        self.id = id.into();
166        self
167    }
168
169    pub fn title(mut self, title: impl Into<SharedString>) -> Self {
170        self.title = title.into();
171        self
172    }
173
174    pub fn close_on_click_outside(mut self, c: bool) -> Self {
175        self.close_on_click_outside = c;
176        self
177    }
178
179    pub fn close_on_escape(mut self, c: bool) -> Self {
180        self.close_on_escape = c;
181        self
182    }
183
184    pub fn content<F, E>(mut self, f: F) -> Self
185    where
186        F: Fn(&mut Window, &mut Context<DialogView>) -> E + 'static,
187        E: IntoElement,
188    {
189        self.content = Arc::new(move |window, cx| f(window, cx).into_any_element());
190        self
191    }
192
193    pub fn show(self, cx: &mut App) {
194        let id = self.id;
195        let title = self.title;
196        let content = self.content;
197        let close_on_click_outside = self.close_on_click_outside;
198        let close_on_escape = self.close_on_escape;
199
200        let id_for_close = id.clone();
201        let view = cx.new(|_cx| {
202            DialogView::new(
203                id.clone(),
204                title,
205                content,
206                close_on_click_outside,
207                close_on_escape,
208                move |_window, _cx| {
209                    liora_core::clear_modal(&id_for_close, _cx);
210                },
211            )
212        });
213
214        liora_core::set_active_modal(id, view.into(), cx);
215    }
216
217    pub fn close(cx: &mut App) {
218        liora_core::clear_active_modal(cx);
219    }
220
221    pub fn close_id(id: impl Into<SharedString>, cx: &mut App) {
222        let id = id.into();
223        liora_core::clear_modal(&id, cx);
224    }
225}