use std::collections::HashMap;
use gpui::prelude::*;
use gpui::*;
use crate::theme::{get_theme_or, Theme};
use super::button::{primary_button, secondary_button, danger_button};
use super::focus_navigation::with_focus_actions;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum DialogStyle {
Info,
#[default]
Default,
Warning,
Danger,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DialogButton {
Primary,
Secondary,
Tertiary,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ConfirmationDialogEvent {
Primary,
Secondary,
Tertiary,
}
pub struct ConfirmationDialog {
title: SharedString,
message: SharedString,
style: DialogStyle,
primary_label: SharedString,
secondary_label: Option<SharedString>,
tertiary_label: Option<SharedString>,
key_mappings: HashMap<String, DialogButton>,
focus_handle: FocusHandle,
custom_theme: Option<Theme>,
previous_focus: Option<FocusHandle>,
}
impl EventEmitter<ConfirmationDialogEvent> for ConfirmationDialog {}
impl Focusable for ConfirmationDialog {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl ConfirmationDialog {
pub fn new(
title: impl Into<SharedString>,
message: impl Into<SharedString>,
cx: &mut Context<Self>,
) -> Self {
Self {
title: title.into(),
message: message.into(),
style: DialogStyle::default(),
primary_label: "OK".into(),
secondary_label: None,
tertiary_label: None,
key_mappings: HashMap::new(),
focus_handle: cx.focus_handle().tab_stop(true),
custom_theme: None,
previous_focus: None,
}
}
#[must_use]
pub fn primary_label(mut self, label: impl Into<SharedString>) -> Self {
self.primary_label = label.into();
self
}
#[must_use]
pub fn secondary_label(mut self, label: impl Into<SharedString>) -> Self {
self.secondary_label = Some(label.into());
self
}
#[must_use]
pub fn tertiary_label(mut self, label: impl Into<SharedString>) -> Self {
self.tertiary_label = Some(label.into());
self
}
#[must_use]
pub fn map_key(mut self, key: impl Into<String>, button: DialogButton) -> Self {
let key_lower = key.into().to_lowercase();
self.key_mappings.insert(key_lower, button);
self
}
#[must_use]
pub fn style(mut self, style: DialogStyle) -> Self {
self.style = style;
self
}
#[must_use]
pub fn theme(mut self, theme: Theme) -> Self {
self.custom_theme = Some(theme);
self
}
pub fn focus_handle(&self) -> &FocusHandle {
&self.focus_handle
}
fn emit_button(&mut self, button: DialogButton, window: &mut Window, cx: &mut Context<Self>) {
if let Some(prev_focus) = self.previous_focus.take() {
window.focus(&prev_focus);
}
let event = match button {
DialogButton::Primary => ConfirmationDialogEvent::Primary,
DialogButton::Secondary => ConfirmationDialogEvent::Secondary,
DialogButton::Tertiary => ConfirmationDialogEvent::Tertiary,
};
cx.emit(event);
}
}
impl Render for ConfirmationDialog {
fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
let theme = get_theme_or(cx, self.custom_theme.as_ref());
let title = self.title.clone();
let message = self.message.clone();
let primary_label = self.primary_label.clone();
let secondary_label = self.secondary_label.clone();
let tertiary_label = self.tertiary_label.clone();
let style = self.style;
let focus_handle = self.focus_handle.clone();
let key_mappings = self.key_mappings.clone();
let is_danger = style == DialogStyle::Danger;
let is_info = style == DialogStyle::Info;
let has_secondary = secondary_label.is_some();
let has_tertiary = tertiary_label.is_some();
if !focus_handle.is_focused(window) {
if self.previous_focus.is_none() {
self.previous_focus = window.focused(cx);
}
focus_handle.focus(window);
}
let title_color = match style {
DialogStyle::Info => theme.primary,
DialogStyle::Default => theme.text_primary,
DialogStyle::Warning => theme.warning,
DialogStyle::Danger => theme.error,
};
let primary_button_element = match style {
DialogStyle::Danger => {
danger_button("dialog_primary", &primary_label, true, cx)
.on_click(cx.listener(|dialog, _event: &ClickEvent, window, cx| {
dialog.emit_button(DialogButton::Primary, window, cx);
}))
}
_ => {
primary_button("dialog_primary", &primary_label, true, cx)
.on_click(cx.listener(|dialog, _event: &ClickEvent, window, cx| {
dialog.emit_button(DialogButton::Primary, window, cx);
}))
}
};
let mut buttons = div()
.w_full()
.flex()
.flex_row()
.gap_3()
.justify_end();
if let Some(label) = &tertiary_label {
buttons = buttons.child(
secondary_button("dialog_tertiary", label, cx)
.on_click(cx.listener(|dialog, _event: &ClickEvent, window, cx| {
dialog.emit_button(DialogButton::Tertiary, window, cx);
}))
);
}
if let Some(label) = &secondary_label {
buttons = buttons.child(
secondary_button("dialog_secondary", label, cx)
.on_click(cx.listener(|dialog, _event: &ClickEvent, window, cx| {
dialog.emit_button(DialogButton::Secondary, window, cx);
}))
);
}
buttons = buttons.child(primary_button_element);
let dialog_box = with_focus_actions(
div()
.id("ccf_confirmation_dialog_box")
.track_focus(&focus_handle)
.tab_stop(true)
.occlude(),
cx,
)
.on_key_down(cx.listener(|_dialog, event: &KeyDownEvent, window, _cx| {
if event.keystroke.key.as_str() == "tab" {
if event.keystroke.modifiers.shift {
window.focus_prev();
} else {
window.focus_next();
}
}
}))
.on_key_up(cx.listener(move |dialog, event: &KeyUpEvent, window, cx| {
let key = event.keystroke.key.as_str().to_lowercase();
if let Some(&button) = key_mappings.get(&key) {
let can_trigger = match button {
DialogButton::Primary => true,
DialogButton::Secondary => has_secondary,
DialogButton::Tertiary => has_tertiary,
};
if can_trigger {
dialog.emit_button(button, window, cx);
return;
}
}
match key.as_str() {
"escape" => {
if has_secondary {
dialog.emit_button(DialogButton::Secondary, window, cx);
} else {
dialog.emit_button(DialogButton::Primary, window, cx);
}
}
"enter" => {
if !is_danger {
dialog.emit_button(DialogButton::Primary, window, cx);
}
}
_ => {}
}
}))
.bg(rgb(theme.bg_secondary))
.border_1()
.border_color(rgb(theme.border_default))
.rounded_lg()
.shadow_lg()
.min_w(px(320.0))
.max_w(px(480.0))
.p(px(24.0))
.child(
div()
.text_lg()
.font_weight(FontWeight::BOLD)
.text_color(rgb(title_color))
.child(title)
)
.child(
div()
.mt_4()
.text_sm()
.text_color(rgb(theme.text_muted))
.child(message)
)
.child(
div()
.mt_4()
.child(buttons)
);
deferred(
div()
.id("ccf_confirmation_dialog")
.absolute()
.inset_0()
.occlude()
.flex()
.items_center()
.justify_center()
.bg(rgba(0x000000aa))
.on_mouse_down(MouseButton::Left, cx.listener(move |dialog, _event, window, cx| {
if is_info {
dialog.emit_button(DialogButton::Primary, window, cx);
} else if !is_danger && has_secondary {
dialog.emit_button(DialogButton::Secondary, window, cx);
}
}))
.child(dialog_box)
)
}
}