1use std::{rc::Rc, time::Duration};
2
3use gpui::{
4 anchored, div, hsla, point, prelude::FluentBuilder, px, relative, Animation, AnimationExt as _,
5 AnyElement, App, Axis, Bounds, BoxShadow, ClickEvent, Div, Edges, FocusHandle, Hsla,
6 InteractiveElement, IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point,
7 RenderOnce, SharedString, StyleRefinement, Styled, Window,
8};
9use rust_i18n::t;
10
11use crate::{
12 actions::{Cancel, Confirm},
13 animation::cubic_bezier,
14 button::{Button, ButtonVariant, ButtonVariants as _},
15 h_flex, v_flex, ActiveTheme as _, IconName, Root, Sizable as _, StyledExt, WindowExt as _,
16};
17
18const CONTEXT: &str = "Dialog";
19pub(crate) fn init(cx: &mut App) {
20 cx.bind_keys([
21 KeyBinding::new("escape", Cancel, Some(CONTEXT)),
22 KeyBinding::new("enter", Confirm { secondary: false }, Some(CONTEXT)),
23 ]);
24}
25
26type RenderButtonFn = Box<dyn FnOnce(&mut Window, &mut App) -> AnyElement>;
27type FooterFn =
28 Box<dyn Fn(RenderButtonFn, RenderButtonFn, &mut Window, &mut App) -> Vec<AnyElement>>;
29
30pub struct DialogButtonProps {
32 ok_text: Option<SharedString>,
33 ok_variant: ButtonVariant,
34 cancel_text: Option<SharedString>,
35 cancel_variant: ButtonVariant,
36}
37
38impl Default for DialogButtonProps {
39 fn default() -> Self {
40 Self {
41 ok_text: None,
42 ok_variant: ButtonVariant::Primary,
43 cancel_text: None,
44 cancel_variant: ButtonVariant::default(),
45 }
46 }
47}
48
49impl DialogButtonProps {
50 pub fn ok_text(mut self, ok_text: impl Into<SharedString>) -> Self {
52 self.ok_text = Some(ok_text.into());
53 self
54 }
55
56 pub fn ok_variant(mut self, ok_variant: ButtonVariant) -> Self {
58 self.ok_variant = ok_variant;
59 self
60 }
61
62 pub fn cancel_text(mut self, cancel_text: impl Into<SharedString>) -> Self {
64 self.cancel_text = Some(cancel_text.into());
65 self
66 }
67
68 pub fn cancel_variant(mut self, cancel_variant: ButtonVariant) -> Self {
70 self.cancel_variant = cancel_variant;
71 self
72 }
73}
74
75#[derive(IntoElement)]
77pub struct Dialog {
78 style: StyleRefinement,
79 title: Option<AnyElement>,
80 footer: Option<FooterFn>,
81 content: Div,
82 width: Pixels,
83 max_width: Option<Pixels>,
84 margin_top: Option<Pixels>,
85
86 on_close: Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
87 on_ok: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static>>,
88 on_cancel: Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static>,
89 button_props: DialogButtonProps,
90 close_button: bool,
91 overlay: bool,
92 overlay_closable: bool,
93 keyboard: bool,
94
95 pub(crate) focus_handle: FocusHandle,
97 pub(crate) layer_ix: usize,
98 pub(crate) overlay_visible: bool,
99}
100
101pub(crate) fn overlay_color(overlay: bool, cx: &App) -> Hsla {
102 if !overlay {
103 return hsla(0., 0., 0., 0.);
104 }
105
106 cx.theme().overlay
107}
108
109impl Dialog {
110 pub fn new(_: &mut Window, cx: &mut App) -> Self {
112 Self {
113 focus_handle: cx.focus_handle(),
114 style: StyleRefinement::default(),
115 title: None,
116 footer: None,
117 content: v_flex(),
118 margin_top: None,
119 width: px(480.),
120 max_width: None,
121 overlay: true,
122 keyboard: true,
123 layer_ix: 0,
124 overlay_visible: false,
125 on_close: Rc::new(|_, _, _| {}),
126 on_ok: None,
127 on_cancel: Rc::new(|_, _, _| true),
128 button_props: DialogButtonProps::default(),
129 close_button: true,
130 overlay_closable: true,
131 }
132 }
133
134 pub fn title(mut self, title: impl IntoElement) -> Self {
136 self.title = Some(title.into_any_element());
137 self
138 }
139
140 pub fn footer<E, F>(mut self, footer: F) -> Self
149 where
150 E: IntoElement,
151 F: Fn(RenderButtonFn, RenderButtonFn, &mut Window, &mut App) -> Vec<E> + 'static,
152 {
153 self.footer = Some(Box::new(move |ok, cancel, window, cx| {
154 footer(ok, cancel, window, cx)
155 .into_iter()
156 .map(|e| e.into_any_element())
157 .collect()
158 }));
159 self
160 }
161
162 pub fn confirm(self) -> Self {
166 self.footer(|ok, cancel, window, cx| vec![cancel(window, cx), ok(window, cx)])
167 .overlay_closable(false)
168 .close_button(false)
169 }
170
171 pub fn alert(self) -> Self {
175 self.footer(|ok, _, window, cx| vec![ok(window, cx)])
176 .overlay_closable(false)
177 .close_button(false)
178 }
179
180 pub fn button_props(mut self, button_props: DialogButtonProps) -> Self {
182 self.button_props = button_props;
183 self
184 }
185
186 pub fn on_close(
190 mut self,
191 on_close: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
192 ) -> Self {
193 self.on_close = Rc::new(on_close);
194 self
195 }
196
197 pub fn on_ok(
201 mut self,
202 on_ok: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
203 ) -> Self {
204 self.on_ok = Some(Rc::new(on_ok));
205 self
206 }
207
208 pub fn on_cancel(
212 mut self,
213 on_cancel: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
214 ) -> Self {
215 self.on_cancel = Rc::new(on_cancel);
216 self
217 }
218
219 pub fn close_button(mut self, close_button: bool) -> Self {
221 self.close_button = close_button;
222 self
223 }
224
225 pub fn margin_top(mut self, margin_top: Pixels) -> Self {
227 self.margin_top = Some(margin_top);
228 self
229 }
230
231 pub fn width(mut self, width: Pixels) -> Self {
233 self.width = width;
234 self
235 }
236
237 pub fn max_w(mut self, max_width: Pixels) -> Self {
239 self.max_width = Some(max_width);
240 self
241 }
242
243 pub fn overlay(mut self, overlay: bool) -> Self {
245 self.overlay = overlay;
246 self
247 }
248
249 pub fn overlay_closable(mut self, overlay_closable: bool) -> Self {
253 self.overlay_closable = overlay_closable;
254 self
255 }
256
257 pub fn keyboard(mut self, keyboard: bool) -> Self {
259 self.keyboard = keyboard;
260 self
261 }
262
263 pub(crate) fn has_overlay(&self) -> bool {
264 self.overlay
265 }
266}
267
268impl ParentElement for Dialog {
269 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
270 self.content.extend(elements);
271 }
272}
273
274impl Styled for Dialog {
275 fn style(&mut self) -> &mut gpui::StyleRefinement {
276 &mut self.style
277 }
278}
279
280impl RenderOnce for Dialog {
281 fn render(self, window: &mut Window, cx: &mut App) -> impl gpui::IntoElement {
282 let layer_ix = self.layer_ix;
283 let on_close = self.on_close.clone();
284 let on_ok = self.on_ok.clone();
285 let on_cancel = self.on_cancel.clone();
286
287 let render_ok: RenderButtonFn = Box::new({
288 let on_ok = on_ok.clone();
289 let on_close = on_close.clone();
290 let ok_text = self
291 .button_props
292 .ok_text
293 .unwrap_or_else(|| t!("Dialog.ok").into());
294 let ok_variant = self.button_props.ok_variant;
295 move |_, _| {
296 Button::new("ok")
297 .label(ok_text)
298 .with_variant(ok_variant)
299 .on_click({
300 let on_ok = on_ok.clone();
301 let on_close = on_close.clone();
302
303 move |_, window, cx| {
304 if let Some(on_ok) = &on_ok {
305 if !on_ok(&ClickEvent::default(), window, cx) {
306 return;
307 }
308 }
309
310 on_close(&ClickEvent::default(), window, cx);
311 window.close_dialog(cx);
312 }
313 })
314 .into_any_element()
315 }
316 });
317 let render_cancel: RenderButtonFn = Box::new({
318 let on_cancel = on_cancel.clone();
319 let on_close = on_close.clone();
320 let cancel_text = self
321 .button_props
322 .cancel_text
323 .unwrap_or_else(|| t!("Dialog.cancel").into());
324 let cancel_variant = self.button_props.cancel_variant;
325 move |_, _| {
326 Button::new("cancel")
327 .label(cancel_text)
328 .with_variant(cancel_variant)
329 .on_click({
330 let on_cancel = on_cancel.clone();
331 let on_close = on_close.clone();
332 move |_, window, cx| {
333 if !on_cancel(&ClickEvent::default(), window, cx) {
334 return;
335 }
336
337 on_close(&ClickEvent::default(), window, cx);
338 window.close_dialog(cx);
339 }
340 })
341 .into_any_element()
342 }
343 });
344
345 let window_paddings = crate::window_border::window_paddings(window);
346 let view_size = window.viewport_size()
347 - gpui::size(
348 window_paddings.left + window_paddings.right,
349 window_paddings.top + window_paddings.bottom,
350 );
351 let bounds = Bounds {
352 origin: Point::default(),
353 size: view_size,
354 };
355 let offset_top = px(layer_ix as f32 * 16.);
356 let y = self.margin_top.unwrap_or(view_size.height / 10.) + offset_top;
357 let x = bounds.center().x - self.width / 2.;
358
359 let base_size = window.text_style().font_size;
360 let rem_size = window.rem_size();
361
362 let mut paddings = Edges::all(px(24.));
363 if let Some(pl) = self.style.padding.left {
364 paddings.left = pl.to_pixels(base_size, rem_size);
365 }
366 if let Some(pr) = self.style.padding.right {
367 paddings.right = pr.to_pixels(base_size, rem_size);
368 }
369 if let Some(pt) = self.style.padding.top {
370 paddings.top = pt.to_pixels(base_size, rem_size);
371 }
372 if let Some(pb) = self.style.padding.bottom {
373 paddings.bottom = pb.to_pixels(base_size, rem_size);
374 }
375
376 let animation = Animation::new(Duration::from_secs_f64(0.25))
377 .with_easing(cubic_bezier(0.32, 0.72, 0., 1.));
378
379 anchored()
380 .position(point(window_paddings.left, window_paddings.top))
381 .snap_to_window()
382 .child(
383 div()
384 .id("dialog")
385 .occlude()
386 .w(view_size.width)
387 .h(view_size.height)
388 .when(self.overlay_visible, |this| {
389 this.bg(overlay_color(self.overlay, cx))
390 })
391 .when(self.overlay, |this| {
392 if (self.layer_ix + 1) != Root::read(window, cx).active_dialogs.len() {
394 return this;
395 }
396
397 this.on_any_mouse_down({
398 let on_cancel = on_cancel.clone();
399 let on_close = on_close.clone();
400 move |event, window, cx| {
401 cx.stop_propagation();
402
403 if self.overlay_closable && event.button == MouseButton::Left {
404 on_cancel(&ClickEvent::default(), window, cx);
405 on_close(&ClickEvent::default(), window, cx);
406 window.close_dialog(cx);
407 }
408 }
409 })
410 })
411 .child(
412 v_flex()
413 .id(layer_ix)
414 .bg(cx.theme().background)
415 .border_1()
416 .border_color(cx.theme().border)
417 .rounded(cx.theme().radius_lg)
418 .min_h_24()
419 .pt(paddings.top)
420 .pb(paddings.bottom)
421 .gap(paddings.top.min(px(16.)))
422 .refine_style(&self.style)
423 .px_0()
424 .key_context(CONTEXT)
425 .track_focus(&self.focus_handle)
426 .tab_group()
427 .when(self.keyboard, |this| {
428 this.on_action({
429 let on_cancel = on_cancel.clone();
430 let on_close = on_close.clone();
431 move |_: &Cancel, window, cx| {
432 on_cancel(&ClickEvent::default(), window, cx);
437 on_close(&ClickEvent::default(), window, cx);
438 window.close_dialog(cx);
439 }
440 })
441 .on_action({
442 let on_ok = on_ok.clone();
443 let on_close = on_close.clone();
444 let has_footer = self.footer.is_some();
445 move |_: &Confirm, window, cx| {
446 if let Some(on_ok) = &on_ok {
447 if on_ok(&ClickEvent::default(), window, cx) {
448 on_close(&ClickEvent::default(), window, cx);
449 window.close_dialog(cx);
450 }
451 } else if has_footer {
452 window.close_dialog(cx);
453 }
454 }
455 })
456 })
457 .absolute()
459 .occlude()
460 .relative()
461 .left(x)
462 .top(y)
463 .w(self.width)
464 .when_some(self.max_width, |this, w| this.max_w(w))
465 .when_some(self.title, |this, title| {
466 this.child(
467 div()
468 .pl(paddings.left)
469 .pr(paddings.right)
470 .line_height(relative(1.))
471 .font_semibold()
472 .child(title),
473 )
474 })
475 .children(self.close_button.then(|| {
476 let top = (paddings.top - px(10.)).max(px(8.));
477 let right = (paddings.right - px(10.)).max(px(8.));
478
479 Button::new("close")
480 .absolute()
481 .top(top)
482 .right(right)
483 .small()
484 .ghost()
485 .icon(IconName::Close)
486 .on_click({
487 let on_cancel = self.on_cancel.clone();
488 let on_close = self.on_close.clone();
489 move |_, window, cx| {
490 on_cancel(&ClickEvent::default(), window, cx);
491 on_close(&ClickEvent::default(), window, cx);
492 window.close_dialog(cx);
493 }
494 })
495 }))
496 .child(
497 div().w_full().flex_1().overflow_hidden().child(
498 v_flex()
499 .pl(paddings.left)
500 .pr(paddings.right)
501 .scrollable(Axis::Vertical)
502 .child(self.content),
503 ),
504 )
505 .when_some(self.footer, |this, footer| {
506 this.child(
507 h_flex()
508 .gap_2()
509 .pl(paddings.left)
510 .pr(paddings.right)
511 .line_height(relative(1.))
512 .justify_end()
513 .children(footer(render_ok, render_cancel, window, cx)),
514 )
515 })
516 .with_animation("slide-down", animation.clone(), move |this, delta| {
517 let y_offset = px(0.) + delta * px(30.);
518 let shadow = vec![
520 BoxShadow {
521 color: hsla(0., 0., 0., 0.1 * delta),
522 offset: point(px(0.), px(20.)),
523 blur_radius: px(25.),
524 spread_radius: px(-5.),
525 },
526 BoxShadow {
527 color: hsla(0., 0., 0., 0.1 * delta),
528 offset: point(px(0.), px(8.)),
529 blur_radius: px(10.),
530 spread_radius: px(-6.),
531 },
532 ];
533 this.top(y + y_offset).shadow(shadow)
534 }),
535 )
536 .with_animation("fade-in", animation, move |this, delta| this.opacity(delta)),
537 )
538 }
539}