summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJoakim Hulthe <joakim@hulthe.net>2025-02-21 14:22:12 +0100
committerDavid Lönnhager <david.l@mullvad.net>2025-03-05 23:32:12 +0100
commit0fb0b75a41e30016d5bbfc9acc99ee869a880299 (patch)
tree974249749a89d5b2e3938d075c0707e2268f3e1f
parent20aa2428753e87102073fb16ea21c30b13ead36a (diff)
downloadmullvadvpn-0fb0b75a41e30016d5bbfc9acc99ee869a880299.tar.xz
mullvadvpn-0fb0b75a41e30016d5bbfc9acc99ee869a880299.zip
Implement error view for macos
-rw-r--r--installer-downloader/src/cacao_impl/delegate.rs68
-rw-r--r--installer-downloader/src/cacao_impl/ui.rs132
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();
+ }
+}