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 }) .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}