use std::cell::RefCell; use std::rc::Rc; use gpui::prelude::FluentBuilder as _; use gpui::{ Animation, AnimationExt as _, AnyElement, App, Bounds, ElementId, FontWeight, InteractiveElement as _, IntoElement, KeyDownEvent, MouseButton, MouseDownEvent, ParentElement as _, Pixels, RenderOnce, Role, SharedString, StatefulInteractiveElement as _, Styled as _, Window, canvas, div, ease_out_quint, relative, }; use gpui_native_theme::ActiveTheme as _; use crate::common::v_flex; /// Requests the dialog's close: `false` closes. pub type ChangeHandler = Rc; type ContentFn = Box AnyElement>; type ActionsFn = Box Vec>; /// Bounds captured during the last paint, shared with the scrim listener. type BoundsCell = Rc>>>; /// 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 and scrim clicks. 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, theme.colors.border, 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; // Keyboard focus rides the panel across frames. let focus_handle = window.use_keyed_state("dialog-focus", cx, |_, cx| cx.focus_handle()); let content = self.content.map(|build| build(window, cx)); let actions = self.actions.map(|build| build(window, cx)); let panel_bounds: BoundsCell = Rc::new(RefCell::new(None)); let panel = v_flex() .id("dialog-panel") .track_focus(&focus_handle.read(cx).clone()) .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_some(self.title, |panel, title| { panel.child( div() .text_size(title_size) .font_weight(FontWeight::SEMIBOLD) .child(title), ) }) .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( { let cell = panel_bounds.clone(); move |bounds, _window, _cx| *cell.borrow_mut() = Some(bounds) }, { let focus_handle = focus_handle.clone(); move |_, _, window, cx| { let handle = focus_handle.read(cx).clone(); if !handle.contains_focused(window, cx) { window.focus(&handle, cx); } } }, ) .absolute() .inset_0(), ); let scrim = div() .absolute() .inset_0() .bg(scrim_color) .occlude() .when(!alert, |scrim| { scrim.on_mouse_down(MouseButton::Left, { let panel_bounds = panel_bounds.clone(); let on_change = on_change.clone(); move |event: &MouseDownEvent, _window, cx| { let on_panel = panel_bounds .borrow() .is_some_and(|bounds| bounds.contains(&event.position)); if on_panel { return; } 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) .child(panel.with_animation( "dialog-enter", Animation::new(enter_duration).with_easing(ease_out_quint()), |panel, delta| panel.opacity(delta), )) } }