diff options
| author | Joakim Hulthe <joakim@hulthe.net> | 2025-02-21 14:22:12 +0100 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2025-03-05 23:32:12 +0100 |
| commit | 0fb0b75a41e30016d5bbfc9acc99ee869a880299 (patch) | |
| tree | 974249749a89d5b2e3938d075c0707e2268f3e1f | |
| parent | 20aa2428753e87102073fb16ea21c30b13ead36a (diff) | |
| download | mullvadvpn-0fb0b75a41e30016d5bbfc9acc99ee869a880299.tar.xz mullvadvpn-0fb0b75a41e30016d5bbfc9acc99ee869a880299.zip | |
Implement error view for macos
| -rw-r--r-- | installer-downloader/src/cacao_impl/delegate.rs | 68 | ||||
| -rw-r--r-- | installer-downloader/src/cacao_impl/ui.rs | 132 |
2 files changed, 192 insertions, 8 deletions
diff --git a/installer-downloader/src/cacao_impl/delegate.rs b/installer-downloader/src/cacao_impl/delegate.rs index 80d080932b..0a09b3324d 100644 --- a/installer-downloader/src/cacao_impl/delegate.rs +++ b/installer-downloader/src/cacao_impl/delegate.rs @@ -2,7 +2,7 @@ use std::sync::{Arc, Mutex}; use cacao::{control::Control, layout::Layout}; -use super::ui::{Action, AppWindow}; +use super::ui::{Action, AppWindow, ErrorView}; use crate::delegate::{AppDelegate, AppDelegateQueue}; impl AppDelegate for AppWindow { @@ -30,6 +30,12 @@ impl AppDelegate for AppWindow { }); } + fn on_beta_link<F>(&mut self, callback: F) + where + F: Fn() + Send + 'static, + { + } + fn set_status_text(&mut self, text: &str) { self.status_text.set_text(text); } @@ -95,6 +101,66 @@ impl AppDelegate for AppWindow { fn queue(&self) -> Self::Queue { Queue {} } + + fn quit(&mut self) { + cacao::appkit::App::<super::ui::AppImpl, _>::dispatch_main(Action::Quit); + } + + fn on_stable_link<F>(&mut self, callback: F) + where + F: Fn() + Send + 'static, + { + println!("todo. on stable link"); + } + + fn show_stable_text(&mut self) { + println!("todo. show stable text"); + } + + fn hide_stable_text(&mut self) { + println!("todo. hide stable text"); + } + + fn show_error_message(&mut self, message: installer_downloader::delegate::ErrorMessage) { + let on_cancel = self.error_cancel_callback.clone().map(|cb| { + move || { + let cb = Action::ErrorCancel(cb.clone()); + cacao::appkit::App::<super::ui::AppImpl, _>::dispatch_main(cb); + } + }); + + let on_retry = self.error_retry_callback.clone().map(|cb| { + move || { + let cb = Action::ErrorRetry(cb.clone()); + cacao::appkit::App::<super::ui::AppImpl, _>::dispatch_main(cb); + } + }); + + self.error_view = Some(ErrorView::new( + &self.main_view, + message, + on_retry, + on_cancel, + )); + } + + fn hide_error_message(&mut self) { + self.error_view.take(); + } + + fn on_error_message_retry<F>(&mut self, callback: F) + where + F: Fn() + Send + 'static, + { + self.error_retry_callback = Some(Self::sync_callback(callback)); + } + + fn on_error_message_cancel<F>(&mut self, callback: F) + where + F: Fn() + Send + 'static, + { + self.error_cancel_callback = Some(Self::sync_callback(callback)); + } } impl AppWindow { diff --git a/installer-downloader/src/cacao_impl/ui.rs b/installer-downloader/src/cacao_impl/ui.rs index 85f7554c05..5981b9465e 100644 --- a/installer-downloader/src/cacao_impl/ui.rs +++ b/installer-downloader/src/cacao_impl/ui.rs @@ -12,6 +12,7 @@ use cacao::objc::{class, msg_send, sel, sel_impl}; use cacao::progress::ProgressIndicator; use cacao::text::{AttributedString, Label}; use cacao::view::View; +use installer_downloader::delegate::ErrorMessage; use objc_id::Id; use crate::resource::{ @@ -25,6 +26,8 @@ const LOGO_IMAGE_DATA: &[u8] = include_bytes!("../../assets/logo-icon.svg"); /// Logo banner text const LOGO_TEXT_DATA: &[u8] = include_bytes!("../../assets/logo-text.svg"); +const ALERT_CIRCLE_IMAGE_DATA: &[u8] = include_bytes!("../../assets/alert-circle.svg"); + /// Banner background color: #192e45 static BANNER_COLOR: LazyLock<Color> = LazyLock::new(|| { let r = 0x19 as f64 / 255.; @@ -44,6 +47,7 @@ static BANNER_COLOR: LazyLock<Color> = LazyLock::new(|| { static LOGO: LazyLock<Image> = LazyLock::new(|| Image::with_data(LOGO_IMAGE_DATA)); static LOGO_TEXT: LazyLock<Image> = LazyLock::new(|| Image::with_data(LOGO_TEXT_DATA)); +static ALERT_CIRCLE: LazyLock<Image> = LazyLock::new(|| Image::with_data(ALERT_CIRCLE_IMAGE_DATA)); pub struct AppImpl { window: Window<AppWindowWrapper>, @@ -80,6 +84,12 @@ pub enum Action { CancelClick(Arc<Mutex<Box<dyn Fn() + Send>>>), /// Run callback on main thread QueueMain(Mutex<Option<Box<dyn for<'a> FnOnce(&'a mut AppWindow) + Send>>>), + /// User clicked the retry button in the error view + ErrorRetry(Arc<Mutex<Box<dyn Fn() + Send>>>), + /// User clicked the cancel button in the error view + ErrorCancel(Arc<Mutex<Box<dyn Fn() + Send>>>), + /// Quit the application. + Quit, } impl Dispatcher for AppImpl { @@ -102,6 +112,17 @@ impl Dispatcher for AppImpl { let cb = cb.lock().unwrap().take().unwrap(); cb(&mut borrowed); } + Action::ErrorRetry(cb) => { + let cb = cb.lock().unwrap(); + cb(); + } + Action::ErrorCancel(cb) => { + let cb = cb.lock().unwrap(); + cb(); + } + Action::Quit => { + self.window.close(); + } } } @@ -132,12 +153,25 @@ pub struct AppWindow { pub progress: ProgressIndicator, pub status_text: Label, + + pub error_view: Option<ErrorView>, + pub error_retry_callback: Option<Arc<Mutex<Box<dyn Fn() + Send>>>>, + pub error_cancel_callback: Option<Arc<Mutex<Box<dyn Fn() + Send>>>>, + pub download_text: Label, pub beta_link_preface: Label, pub beta_link: Label, } +pub struct ErrorView { + pub view: View, + pub text: Label, + pub circle: ImageView, + pub retry_button: Button, + pub cancel_button: Button, +} + pub struct DownloadButton { pub button: Button, } @@ -259,13 +293,6 @@ impl AppWindow { self.main_view.add_subview(&self.beta_link); LayoutConstraint::activate(&[ - self.status_text - .top - .constraint_greater_than_or_equal_to(&self.main_view.top) - .offset(24.), - self.status_text - .center_x - .constraint_equal_to(&self.main_view.center_x), self.download_text .top .constraint_equal_to(&self.status_text.bottom) @@ -333,3 +360,94 @@ impl WindowDelegate for AppWindowWrapper { window.set_content_view(&self.inner.borrow().content); } } + +impl ErrorView { + pub fn new( + main_view: &View, + message: ErrorMessage, + on_retry: Option<impl Fn() + Send + Sync + 'static>, + on_cancel: Option<impl Fn() + Send + Sync + 'static>, + ) -> Self { + let mut error_view = ErrorView { + view: Default::default(), + text: Default::default(), + circle: Default::default(), + retry_button: Button::new(&message.retry_button_text), + cancel_button: Button::new(&message.cancel_button_text), + }; + + let ErrorView { + view, + text, + circle, + retry_button, + cancel_button, + } = &mut error_view; + + text.set_text(message.status_text); + circle.set_image(&ALERT_CIRCLE); + + if let Some(on_cancel) = on_cancel { + cancel_button.set_action(on_cancel); + } + if let Some(on_retry) = on_retry { + retry_button.set_action(on_retry); + } + + view.add_subview(text); + view.add_subview(circle); + main_view.add_subview(view); + main_view.add_subview(retry_button); + main_view.add_subview(cancel_button); + + LayoutConstraint::activate(&[ + view.center_x.constraint_equal_to(&main_view.center_x), + view.center_y + .constraint_equal_to(&main_view.top) + .offset(74.), + view.width.constraint_equal_to_constant(536.), + text.center_y.constraint_equal_to(&view.center_y), + text.left.constraint_equal_to(&circle.right).offset(16.), + text.right.constraint_equal_to(&view.right), + circle.left.constraint_equal_to(&view.left), + circle.center_y.constraint_equal_to(&text.center_y), + retry_button + .top + .constraint_equal_to(&text.bottom) + .offset(24.), + cancel_button + .top + .constraint_equal_to(&text.bottom) + .offset(24.), + retry_button + .left + .constraint_equal_to(&view.center_x) + .offset(8.), + cancel_button + .right + .constraint_equal_to(&view.center_x) + .offset(-8.), + retry_button.width.constraint_equal_to_constant(213.), + cancel_button.width.constraint_equal_to_constant(213.), + ]); + + error_view + } +} + +impl Drop for ErrorView { + fn drop(&mut self) { + let ErrorView { + view, + text, + circle, + retry_button, + cancel_button, + } = self; + view.remove_from_superview(); + text.remove_from_superview(); + circle.remove_from_superview(); + retry_button.remove_from_superview(); + cancel_button.remove_from_superview(); + } +} |
