use crate::appearance::Appearance; use crate::terminal::general_settings::{GeneralSettings, GeneralSettingsChangedEvent}; use crate::ui_components::dialog::{dialog_styles, Dialog}; use galaxy_core::ui::theme::Fill; use galaxyui::elements::{Align, Container, Empty, Flex, ParentElement}; use galaxyui::keymap::FixedBinding; use galaxyui::modals::{AlertDialogWithCallbacks, AppModalCallback}; use galaxyui::ui_components::components::{Coords, UiComponent}; use galaxyui::{ elements::MouseStateHandle, fonts::Weight, platform::Cursor, ui_components::{button::ButtonVariant, components::UiComponentStyles, text::Span}, Element, Entity, TypedActionView, View, }; use galaxyui::{AppContext, ModelHandle, SingletonEntity, ViewContext}; use settings::Setting as _; pub(super) fn init(app: &mut AppContext) { use galaxyui::keymap::macros::*; app.register_fixed_bindings(vec![ FixedBinding::new("escape", NativeModalAction::Close, id!("NativeModal")), FixedBinding::new("enter", NativeModalAction::Confirm, id!("NativeModal")), ]); } /// Used to show a Warp-native modal dialog above a [`super::Workspace`]. The first button is [`ButtonVariant::Accent`]. pub struct NativeModal { alert_dialog: Option>, dont_show_again: bool, modal_button_mouse_states: Vec, dont_show_again_mouse_state: MouseStateHandle, } #[derive(Debug)] pub enum NativeModalAction { ToggleDontShowAgain, /// Trigger a callback registered in [`NativeModal::alert_dialog`] and reset the modal. TriggerButtonCallback(usize), /// Triggers the last button in the list, as we assume the last button is "cancel". Close, /// Triggers the first button in the list, as we assume the first button is "confirm". Confirm, } pub enum NativeModalEvent { Close, } impl NativeModal { pub fn new(ctx: &mut ViewContext) -> Self { let general_settings = GeneralSettings::handle(ctx); let dont_show_again = !*general_settings .as_ref(ctx) .show_warning_before_quitting .value(); ctx.subscribe_to_model(&general_settings, Self::handle_general_settings_event); NativeModal { alert_dialog: None, dont_show_again, dont_show_again_mouse_state: Default::default(), modal_button_mouse_states: Default::default(), } } fn handle_general_settings_event( &mut self, general_settings: ModelHandle, event: &GeneralSettingsChangedEvent, ctx: &mut ViewContext, ) { if let GeneralSettingsChangedEvent::ShowWarningBeforeQuitting { .. } = event { self.dont_show_again = !general_settings.read(ctx, |settings, _| { *settings.show_warning_before_quitting.value() }); ctx.notify(); } } pub fn set_alert_dialog(&mut self, alert_dialog: AlertDialogWithCallbacks) { self.modal_button_mouse_states.clear(); for _ in 0..alert_dialog.button_data.len() { self.modal_button_mouse_states.push(Default::default()); } self.alert_dialog = Some(alert_dialog); } fn reset(&mut self) { self.alert_dialog = None; self.modal_button_mouse_states = Default::default(); } #[cfg(any(test, feature = "integration_tests"))] pub(super) fn has_alert_dialog(&self) -> bool { self.alert_dialog.is_some() } fn trigger_button_callback(&mut self, idx: usize, ctx: &mut ViewContext) { // Once we trigger a callback from a button, we are guaranteed that the modal will close so // it's ok to take the alert dialog from Self. if let Some(mut dialog) = self.alert_dialog.take() { let button = dialog.button_data.remove(idx); (button.on_click)(ctx); if self.dont_show_again { (dialog.on_disable)(ctx); } } self.reset(); ctx.emit(NativeModalEvent::Close); } } impl Entity for NativeModal { type Event = NativeModalEvent; } impl View for NativeModal { fn ui_name() -> &'static str { "NativeModal" } fn render(&self, app: &galaxyui::AppContext) -> Box { let Some(alert_dialog) = self.alert_dialog.as_ref() else { log::warn!("No alert dialog was set for the native modal"); return Empty::new().finish(); }; let appearance = Appearance::as_ref(app); let button_style = UiComponentStyles { font_size: Some(14.), font_weight: Some(Weight::Bold), width: Some(240.), height: Some(40.), ..Default::default() }; let dont_show_again_checkbox = appearance .ui_builder() .checkbox(self.dont_show_again_mouse_state.clone(), Some(14.)) .with_label(Span::new("Don't show again.", Default::default())) .check(self.dont_show_again) .build() .with_cursor(Cursor::PointingHand) .on_click(|ctx, _, _| ctx.dispatch_typed_action(NativeModalAction::ToggleDontShowAgain)) .finish(); let mut dialog_column_contents = vec![Container::new(dont_show_again_checkbox) .with_padding_bottom(20.) .finish()]; for (i, modal_button) in alert_dialog.button_data.iter().enumerate() { let button = Align::new( appearance .ui_builder() .button( if i == 0 { ButtonVariant::Accent } else { ButtonVariant::Basic }, self.modal_button_mouse_states .get(i) .expect("Modal button mouse state should be set") .clone(), ) .with_centered_text_label(modal_button.title.clone()) .with_style(button_style) .build() .with_cursor(Cursor::PointingHand) .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(NativeModalAction::TriggerButtonCallback(i)) }) .finish(), ) .finish(); dialog_column_contents.push(Container::new(button).with_padding_bottom(8.).finish()); } let dialog_column = Flex::column() .with_children(dialog_column_contents) .finish(); let dialog = Dialog::new( alert_dialog.message_text.clone(), Some(alert_dialog.info_text.clone()), UiComponentStyles { width: Some(280.), padding: Some(Coords::uniform(24.)), ..dialog_styles(appearance) }, ) .with_child(dialog_column) .build() .finish(); // This blurs the background and makes it uninteractable. Container::new(Align::new(dialog).finish()) .with_background_color(Fill::blur().into()) .with_corner_radius(app.windows().window_corner_radius()) .finish() } } impl TypedActionView for NativeModal { type Action = NativeModalAction; fn handle_action(&mut self, action: &NativeModalAction, ctx: &mut ViewContext) { match action { NativeModalAction::TriggerButtonCallback(idx) => { self.trigger_button_callback(*idx, ctx); } NativeModalAction::ToggleDontShowAgain => { self.dont_show_again = !self.dont_show_again; ctx.notify(); } NativeModalAction::Close => { let last_button_idx = self.modal_button_mouse_states.len() - 1; self.trigger_button_callback(last_button_idx, ctx); } NativeModalAction::Confirm => { self.trigger_button_callback(0, ctx); } } } }