diff options
Diffstat (limited to 'toolkit/crashreporter/client/app/src/ui/mod.rs')
-rw-r--r-- | toolkit/crashreporter/client/app/src/ui/mod.rs | 295 |
1 files changed, 295 insertions, 0 deletions
diff --git a/toolkit/crashreporter/client/app/src/ui/mod.rs b/toolkit/crashreporter/client/app/src/ui/mod.rs new file mode 100644 index 0000000000..8464b6a9b3 --- /dev/null +++ b/toolkit/crashreporter/client/app/src/ui/mod.rs @@ -0,0 +1,295 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! The UI model, UI implementations, and functions using them. +//! +//! UIs must implement: +//! * a `fn run_loop(&self, app: model::Application)` method which should display the UI and block while +//! handling events until the application terminates, +//! * a `fn invoke(&self, f: model::InvokeFn)` method which invokes the given function +//! asynchronously (without blocking) on the UI loop thread. + +use crate::std::{rc::Rc, sync::Arc}; +use crate::{ + async_task::AsyncTask, config::Config, data, logic::ReportCrash, settings::Settings, std, + thread_bound::ThreadBound, +}; +use model::{ui, Application}; +use ui_impl::UI; + +mod model; + +#[cfg(all(not(test), any(target_os = "linux", target_os = "windows")))] +mod icon { + // Must be DWORD-aligned for Win32 CreateIconFromResource. + #[repr(align(4))] + struct Aligned<Bytes: ?Sized>(Bytes); + static PNG_DATA_ALIGNMENT: &'static Aligned<[u8]> = + &Aligned(*include_bytes!("crashreporter.png")); + pub static PNG_DATA: &'static [u8] = &PNG_DATA_ALIGNMENT.0; +} + +#[cfg(test)] +pub mod test { + pub mod model { + pub use crate::ui::model::*; + } +} + +cfg_if::cfg_if! { + if #[cfg(test)] { + #[path = "test.rs"] + pub mod ui_impl; + } else if #[cfg(target_os = "linux")] { + #[path = "gtk.rs"] + mod ui_impl; + } else if #[cfg(target_os = "windows")] { + #[path = "windows/mod.rs"] + mod ui_impl; + } else if #[cfg(target_os = "macos")] { + #[path = "macos/mod.rs"] + mod ui_impl; + } else { + mod ui_impl { + #[derive(Default)] + pub struct UI; + + impl UI { + pub fn run_loop(&self, _app: super::model::Application) { + unimplemented!(); + } + + pub fn invoke(&self, _f: super::model::InvokeFn) { + unimplemented!(); + } + } + } + } +} + +/// Display an error dialog with the given message. +#[cfg_attr(mock, allow(unused))] +pub fn error_dialog<M: std::fmt::Display>(config: &Config, message: M) { + let close = data::Event::default(); + // Config may not have localized strings + let string_or = |name, fallback: &str| { + if config.strings.is_none() { + fallback.into() + } else { + config.string(name) + } + }; + + let details = if config.strings.is_none() { + format!("Details: {}", message) + } else { + config + .build_string("crashreporter-error-details") + .arg("details", message.to_string()) + .get() + }; + + let window = ui! { + Window title(string_or("crashreporter-branded-title", "Firefox Crash Reporter")) hsize(600) vsize(400) + close_when(&close) halign(Alignment::Fill) valign(Alignment::Fill) { + VBox margin(10) spacing(10) halign(Alignment::Fill) valign(Alignment::Fill) { + Label text(string_or( + "crashreporter-error", + "The application had a problem and crashed. \ + Unfortunately, the crash reporter is unable to submit a report for the crash." + )), + Label text(details), + Button["close"] halign(Alignment::End) on_click(move || close.fire(&())) { + Label text(string_or("crashreporter-button-close", "Close")) + } + } + } + }; + + UI::default().run_loop(Application { + windows: vec![window], + rtl: config.is_rtl(), + }); +} + +#[derive(Default, Debug, PartialEq, Eq)] +pub enum SubmitState { + #[default] + Initial, + InProgress, + Success, + Failure, +} + +/// The UI for the main crash reporter windows. +pub struct ReportCrashUI { + state: Arc<ThreadBound<ReportCrashUIState>>, + ui: Arc<UI>, + config: Arc<Config>, + logic: Rc<AsyncTask<ReportCrash>>, +} + +/// The state of the creash UI. +pub struct ReportCrashUIState { + pub send_report: data::Synchronized<bool>, + pub include_address: data::Synchronized<bool>, + pub show_details: data::Synchronized<bool>, + pub details: data::Synchronized<String>, + pub comment: data::OnDemand<String>, + pub submit_state: data::Synchronized<SubmitState>, + pub close_window: data::Event<()>, +} + +impl ReportCrashUI { + pub fn new( + initial_settings: &Settings, + config: Arc<Config>, + logic: AsyncTask<ReportCrash>, + ) -> Self { + let send_report = data::Synchronized::new(initial_settings.submit_report); + let include_address = data::Synchronized::new(initial_settings.include_url); + + ReportCrashUI { + state: Arc::new(ThreadBound::new(ReportCrashUIState { + send_report, + include_address, + show_details: Default::default(), + details: Default::default(), + comment: Default::default(), + submit_state: Default::default(), + close_window: Default::default(), + })), + ui: Default::default(), + config, + logic: Rc::new(logic), + } + } + + pub fn async_task(&self) -> AsyncTask<ReportCrashUIState> { + let state = self.state.clone(); + let ui = Arc::downgrade(&self.ui); + AsyncTask::new(move |f| { + let Some(ui) = ui.upgrade() else { return }; + ui.invoke(Box::new(cc! { (state) move || { + f(state.borrow()); + }})); + }) + } + + pub fn run(&self) { + let ReportCrashUI { + state, + ui, + config, + logic, + } = self; + let ReportCrashUIState { + send_report, + include_address, + show_details, + details, + comment, + submit_state, + close_window, + } = state.borrow(); + + send_report.on_change(cc! { (logic) move |v| { + let v = *v; + logic.push(move |s| s.settings.borrow_mut().submit_report = v); + }}); + include_address.on_change(cc! { (logic) move |v| { + let v = *v; + logic.push(move |s| s.settings.borrow_mut().include_url = v); + }}); + + let input_enabled = submit_state.mapped(|s| s == &SubmitState::Initial); + let send_report_and_input_enabled = + data::Synchronized::join(send_report, &input_enabled, |s, e| *s && *e); + + let submit_status_text = submit_state.mapped(cc! { (config) move |s| { + config.string(match s { + SubmitState::Initial => "crashreporter-submit-status", + SubmitState::InProgress => "crashreporter-submit-in-progress", + SubmitState::Success => "crashreporter-submit-success", + SubmitState::Failure => "crashreporter-submit-failure", + }) + }}); + + let progress_visible = submit_state.mapped(|s| s == &SubmitState::InProgress); + + let details_window = ui! { + Window["crash-details-window"] title(config.string("crashreporter-view-report-title")) + visible(show_details) modal(true) hsize(600) vsize(400) + halign(Alignment::Fill) valign(Alignment::Fill) + { + VBox margin(10) spacing(10) halign(Alignment::Fill) valign(Alignment::Fill) { + Scroll halign(Alignment::Fill) valign(Alignment::Fill) { + TextBox["details-text"] content(details) halign(Alignment::Fill) valign(Alignment::Fill) + }, + Button["close-details"] halign(Alignment::End) on_click(cc! { (show_details) move || *show_details.borrow_mut() = false }) { + Label text(config.string("crashreporter-button-ok")) + } + } + } + }; + + let main_window = ui! { + Window title(config.string("crashreporter-branded-title")) hsize(600) vsize(400) + halign(Alignment::Fill) valign(Alignment::Fill) close_when(close_window) + child_window(details_window) + { + VBox margin(10) spacing(10) halign(Alignment::Fill) valign(Alignment::Fill) { + Label text(config.string("crashreporter-apology")) bold(true), + Label text(config.string("crashreporter-crashed-and-restore")), + Label text(config.string("crashreporter-plea")), + Checkbox["send"] checked(send_report) label(config.string("crashreporter-send-report")) + enabled(&input_enabled), + VBox margin_start(20) spacing(5) halign(Alignment::Fill) valign(Alignment::Fill) { + Button["details"] enabled(&send_report_and_input_enabled) on_click(cc! { (config, details, show_details, logic) move || { + // Immediately display the window to feel responsive, even if forming + // the details string takes a little while (it really shouldn't + // though). + *details.borrow_mut() = config.string("crashreporter-loading-details"); + logic.push(|s| s.update_details()); + *show_details.borrow_mut() = true; + }}) + { + Label text(config.string("crashreporter-button-details")) + }, + Scroll halign(Alignment::Fill) valign(Alignment::Fill) { + TextBox["comment"] placeholder(config.string("crashreporter-comment-prompt")) + content(comment) + editable(true) + enabled(&send_report_and_input_enabled) + halign(Alignment::Fill) valign(Alignment::Fill) + }, + Checkbox["include-url"] checked(include_address) + label(config.string("crashreporter-include-url")) enabled(&send_report_and_input_enabled), + Label text(&submit_status_text) margin_top(20), + Progress halign(Alignment::Fill) visible(&progress_visible), + }, + HBox valign(Alignment::End) halign(Alignment::End) spacing(10) affirmative_order(true) + { + Button["restart"] visible(config.restart_command.is_some()) + on_click(cc! { (logic) move || logic.push(|s| s.restart()) }) + enabled(&input_enabled) hsize(160) + { + Label text(config.string("crashreporter-button-restart")) + }, + Button["quit"] on_click(cc! { (logic) move || logic.push(|s| s.quit()) }) + enabled(&input_enabled) hsize(160) + { + Label text(config.string("crashreporter-button-quit")) + } + } + } + } + }; + + ui.run_loop(Application { + windows: vec![main_window], + rtl: config.is_rtl(), + }); + } +} |