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