use gpui::prelude::FluentBuilder as _; use std::rc::Rc; use gpui::{ Animation, AnimationExt as _, AnyElement, App, ElementId, FontWeight, InteractiveElement as _, IntoElement, KeyDownEvent, MouseButton, ParentElement as _, RenderOnce, Role, SharedString, StatefulInteractiveElement as _, Styled as _, Window, canvas, div, ease_out_quint, relative, }; use gpui_native_icons::IconRole; use gpui_native_theme::ActiveTheme as _; use crate::button::{Button, ButtonVariant}; use crate::common::{Sizable, v_flex}; /// Requests the dialog's close: `false` closes. pub type ChangeHandler = Rc; type ContentFn = Box AnyElement>; type ActionsFn = Box Vec>; /// A centred panel over a dim scrim: the platform's modal. #[derive(IntoElement)] pub struct Dialog { id: ElementId, open: bool, alert: bool, title: Option, a11y_label: Option, on_change: Option, content: Option, actions: Option, } impl Dialog { /// Creates a closed dialog. pub fn new(id: I) -> Self where I: Into, { Self { id: id.into(), open: false, alert: false, title: None, a11y_label: None, on_change: None, content: None, actions: None, } } /// Sets the current open state. pub fn open(mut self, open: bool) -> Self { self.open = open; self } /// Sets the alert mode. pub fn alert(mut self, alert: bool) -> Self { self.alert = alert; self } /// Requests close through `on_change` on escape, scrim clicks, and the close button. pub fn on_change(mut self, handler: F) -> Self where F: Fn(bool, &mut Window, &mut App) + 'static, { self.on_change = Some(Rc::new(handler)); self } /// Sets the dialog's title. pub fn title(mut self, title: L) -> Self where L: Into, { self.title = Some(title.into()); self } /// Sets the body content between the title and the actions. pub fn content(mut self, content: F) -> Self where F: Fn(&mut Window, &mut App) -> AnyElement + 'static, { self.content = Some(Box::new(content)); self } /// Sets the footer actions, laid out right-aligned in the given order. pub fn actions(mut self, actions: F) -> Self where F: Fn(&mut Window, &mut App) -> Vec + 'static, { self.actions = Some(Box::new(actions)); self } /// Overrides the accessibility label of the panel. pub fn accessibility_label(mut self, label: L) -> Self where L: Into, { self.a11y_label = Some(label.into()); self } } impl RenderOnce for Dialog { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let root = div().absolute().inset_0(); if !self.open { return root; } let ( scrim_color, border, foreground, muted_foreground, surface, radius, shadows, title_size, enter_duration, ) = { let theme = cx.theme(); ( theme.colors.overlay, if theme.borders.overlays { theme.colors.border } else { theme.colors.surface }, theme.colors.foreground, theme.colors.muted_foreground, theme.colors.surface, theme.radius.overlay, theme.elevations.dialog.shadows(), theme.sizes.medium.text_size, theme.motion.overlay, ) }; let role = if self.alert { Role::AlertDialog } else { Role::Dialog }; let label = self .a11y_label .clone() .or_else(|| self.title.clone()) .unwrap_or_else(|| SharedString::from(self.id.to_string())); let on_change = self.on_change.clone(); let alert = self.alert; let show_close = !alert && on_change.is_some(); // Keyboard focus rides the panel across frames. let focus = gpui_native_utils::focus_handle(&self.id, window, cx); let content = self.content.map(|build| build(window, cx)); let actions = self.actions.map(|build| build(window, cx)); let panel = v_flex() .id("dialog-panel") .track_focus(&focus) .role(role) .aria_label(label) .min_w_64() .max_w(relative(0.9)) .p_4() .gap_3() .bg(surface) .text_color(foreground) .rounded(radius) .border_1() .border_color(border) .shadow(shadows) .overflow_hidden() .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) .on_key_down({ let on_change = on_change.clone(); move |event: &KeyDownEvent, window, cx| { if event.keystroke.key == "escape" && !alert && let Some(on_change) = &on_change { on_change(false, window, cx); } cx.stop_propagation(); } }) .when(self.title.is_some() || show_close, |panel| { panel.child( div() .flex() .items_center() .justify_end() .gap_2() .when_some(self.title.clone(), |row, title| { row.child( div() .flex_1() .text_size(title_size) .font_weight(FontWeight::SEMIBOLD) .child(title), ) }) .when(show_close, |row| { row.child( Button::new("dialog-close") .icon(IconRole::Close) .variant(ButtonVariant::Ghost) .small() .debug_selector(|| "dialog-close".to_string()) .accessibility_label("Close") .on_click({ let on_change = on_change.clone(); move |_, window, cx| { if let Some(on_change) = &on_change { on_change(false, window, cx); } } }), ) }), ) }) .when_some(content, |panel, content| { panel.child(div().text_color(muted_foreground).child(content)) }) .when_some(actions, |panel, actions| { panel.child(div().flex().justify_end().gap_2().children(actions)) }) .child( canvas(|_, _, _| {}, { move |_, _, window, cx| { if !focus.contains_focused(window, cx) { window.focus(&focus, cx); } } }) .absolute() .inset_0(), ); let scrim = div() .absolute() .inset_0() .bg(scrim_color) .occlude() .when(!alert, |scrim| { scrim.on_mouse_down(MouseButton::Left, { let on_change = on_change.clone(); move |_, _window, cx| { if let Some(on_change) = &on_change { on_change(false, _window, cx); } cx.stop_propagation(); } }) }); root.flex() .items_center() .justify_center() .p_4() .child(scrim.with_animation( "dialog-scrim-enter", Animation::new(enter_duration).with_easing(ease_out_quint()), |scrim, delta| scrim.opacity(delta), )) .child(panel.with_animation( "dialog-enter", Animation::new(enter_duration).with_easing(ease_out_quint()), |panel, delta| panel.opacity(delta), )) } }