summaryrefslogtreecommitdiffstats
path: root/toolkit/crashreporter/client/app/src/ui
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-15 03:35:49 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-15 03:35:49 +0000
commitd8bbc7858622b6d9c278469aab701ca0b609cddf (patch)
treeeff41dc61d9f714852212739e6b3738b82a2af87 /toolkit/crashreporter/client/app/src/ui
parentReleasing progress-linux version 125.0.3-1~progress7.99u1. (diff)
downloadfirefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.tar.xz
firefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.zip
Merging upstream version 126.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/crashreporter/client/app/src/ui')
-rw-r--r--toolkit/crashreporter/client/app/src/ui/crashreporter.pngbin0 -> 2001 bytes
-rw-r--r--toolkit/crashreporter/client/app/src/ui/gtk.rs841
-rw-r--r--toolkit/crashreporter/client/app/src/ui/macos/mod.rs1122
-rw-r--r--toolkit/crashreporter/client/app/src/ui/macos/objc.rs242
-rw-r--r--toolkit/crashreporter/client/app/src/ui/macos/plist.rs44
-rw-r--r--toolkit/crashreporter/client/app/src/ui/mod.rs295
-rw-r--r--toolkit/crashreporter/client/app/src/ui/model/button.rs26
-rw-r--r--toolkit/crashreporter/client/app/src/ui/model/checkbox.rs22
-rw-r--r--toolkit/crashreporter/client/app/src/ui/model/hbox.rs34
-rw-r--r--toolkit/crashreporter/client/app/src/ui/model/label.rs22
-rw-r--r--toolkit/crashreporter/client/app/src/ui/model/mod.rs344
-rw-r--r--toolkit/crashreporter/client/app/src/ui/model/progress.rs19
-rw-r--r--toolkit/crashreporter/client/app/src/ui/model/scroll.rs17
-rw-r--r--toolkit/crashreporter/client/app/src/ui/model/textbox.rs27
-rw-r--r--toolkit/crashreporter/client/app/src/ui/model/vbox.rs22
-rw-r--r--toolkit/crashreporter/client/app/src/ui/model/window.rs46
-rw-r--r--toolkit/crashreporter/client/app/src/ui/test.rs270
-rw-r--r--toolkit/crashreporter/client/app/src/ui/windows/font.rs56
-rw-r--r--toolkit/crashreporter/client/app/src/ui/windows/gdi.rs43
-rw-r--r--toolkit/crashreporter/client/app/src/ui/windows/layout.rs436
-rw-r--r--toolkit/crashreporter/client/app/src/ui/windows/mod.rs949
-rw-r--r--toolkit/crashreporter/client/app/src/ui/windows/quit_token.rs33
-rw-r--r--toolkit/crashreporter/client/app/src/ui/windows/twoway.rs36
-rw-r--r--toolkit/crashreporter/client/app/src/ui/windows/widestring.rs36
-rw-r--r--toolkit/crashreporter/client/app/src/ui/windows/window.rs302
25 files changed, 5284 insertions, 0 deletions
diff --git a/toolkit/crashreporter/client/app/src/ui/crashreporter.png b/toolkit/crashreporter/client/app/src/ui/crashreporter.png
new file mode 100644
index 0000000000..5e68bac17c
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/crashreporter.png
Binary files differ
diff --git a/toolkit/crashreporter/client/app/src/ui/gtk.rs b/toolkit/crashreporter/client/app/src/ui/gtk.rs
new file mode 100644
index 0000000000..a76f99b0bd
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/gtk.rs
@@ -0,0 +1,841 @@
+/* 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/. */
+
+use super::model::{self, Alignment, Application, Element};
+use crate::std::{
+ cell::RefCell,
+ ffi::{c_char, CString},
+ rc::Rc,
+ sync::atomic::{AtomicBool, Ordering::Relaxed},
+};
+use crate::{
+ data::{Event, Property, Synchronized},
+ std,
+};
+
+/// Create a `std::ffi::CStr` directly from a literal string.
+///
+/// The argument is an `expr` rather than `literal` so that other macros can be used (such as
+/// `stringify!`).
+macro_rules! cstr {
+ ( $str:expr ) => {
+ #[allow(unused_unsafe)]
+ unsafe { std::ffi::CStr::from_bytes_with_nul_unchecked(concat!($str, "\0").as_bytes()) }
+ .as_ptr()
+ };
+}
+
+/// A GTK+ UI implementation.
+#[derive(Default)]
+pub struct UI {
+ running: AtomicBool,
+}
+
+impl UI {
+ pub fn run_loop(&self, app: Application) {
+ unsafe {
+ let stream = gtk::g_memory_input_stream_new_from_data(
+ super::icon::PNG_DATA.as_ptr() as _,
+ // unwrap() because the PNG_DATA length will be well within gssize limits (32-bit
+ // at the smallest).
+ super::icon::PNG_DATA.len().try_into().unwrap(),
+ None,
+ );
+ let icon_pixbuf =
+ gtk::gdk_pixbuf_new_from_stream(stream, std::ptr::null_mut(), std::ptr::null_mut());
+ gtk::g_object_unref(stream as _);
+
+ gtk::gtk_window_set_default_icon(icon_pixbuf);
+
+ let app_ptr = gtk::gtk_application_new(
+ std::ptr::null(),
+ gtk::GApplicationFlags_G_APPLICATION_FLAGS_NONE,
+ );
+ gtk::g_signal_connect_data(
+ app_ptr as *mut _,
+ cstr!("activate"),
+ Some(std::mem::transmute(
+ render_application
+ as for<'a> unsafe extern "C" fn(*mut gtk::GtkApplication, &'a Application),
+ )),
+ &app as *const Application as *mut Application as _,
+ None,
+ 0,
+ );
+ self.running.store(true, Relaxed);
+ gtk::g_application_run(app_ptr as *mut gtk::GApplication, 0, std::ptr::null_mut());
+ self.running.store(false, Relaxed);
+ gtk::g_object_unref(app_ptr as *mut _);
+ gtk::g_object_unref(icon_pixbuf as _);
+ }
+ }
+
+ pub fn invoke(&self, f: model::InvokeFn) {
+ if !self.running.load(Relaxed) {
+ log::debug!("ignoring `invoke` as main loop isn't running");
+ return;
+ }
+ type BoxedData = Option<model::InvokeFn>;
+
+ unsafe extern "C" fn call(ptr: gtk::gpointer) -> gtk::gboolean {
+ let f: &mut BoxedData = ToPointer::from_ptr(ptr as _);
+ f.take().unwrap()();
+ false.into()
+ }
+
+ unsafe extern "C" fn drop(ptr: gtk::gpointer) {
+ let _: Box<BoxedData> = ToPointer::from_ptr(ptr as _);
+ }
+
+ let data: Box<BoxedData> = Box::new(Some(f));
+
+ unsafe {
+ let main_context = gtk::g_main_context_default();
+ gtk::g_main_context_invoke_full(
+ main_context,
+ 0, // G_PRIORITY_DEFAULT
+ Some(call as unsafe extern "C" fn(gtk::gpointer) -> gtk::gboolean),
+ data.to_ptr() as _,
+ Some(drop as unsafe extern "C" fn(gtk::gpointer)),
+ );
+ }
+ }
+}
+
+/// Types that can be converted to and from a pointer.
+///
+/// These types must be sized to avoid fat pointers (i.e., the pointers must be FFI-compatible, the
+/// same size as usize).
+trait ToPointer: Sized {
+ fn to_ptr(self) -> *mut ();
+ /// # Safety
+ /// The caller must ensure that the pointer was created as the result of `to_ptr` on the same
+ /// or a compatible type, and that the data is still valid.
+ unsafe fn from_ptr(ptr: *mut ()) -> Self;
+}
+
+/// Types that can be attached to a GLib object to be dropped when the widget is dropped.
+trait DropWithObject: Sized {
+ fn drop_with_object(self, object: *mut gtk::GObject);
+ fn drop_with_widget(self, widget: *mut gtk::GtkWidget) {
+ self.drop_with_object(widget as *mut _);
+ }
+
+ fn set_data(self, object: *mut gtk::GObject, key: *const c_char);
+}
+
+impl<T: ToPointer> DropWithObject for T {
+ fn drop_with_object(self, object: *mut gtk::GObject) {
+ unsafe extern "C" fn free_ptr<T: ToPointer>(
+ ptr: gtk::gpointer,
+ _object: *mut gtk::GObject,
+ ) {
+ drop(T::from_ptr(ptr as *mut ()));
+ }
+ unsafe { gtk::g_object_weak_ref(object, Some(free_ptr::<T>), self.to_ptr() as *mut _) };
+ }
+
+ fn set_data(self, object: *mut gtk::GObject, key: *const c_char) {
+ unsafe extern "C" fn free_ptr<T: ToPointer>(ptr: gtk::gpointer) {
+ drop(T::from_ptr(ptr as *mut ()));
+ }
+ unsafe {
+ gtk::g_object_set_data_full(object, key, self.to_ptr() as *mut _, Some(free_ptr::<T>))
+ };
+ }
+}
+
+impl ToPointer for CString {
+ fn to_ptr(self) -> *mut () {
+ self.into_raw() as _
+ }
+
+ unsafe fn from_ptr(ptr: *mut ()) -> Self {
+ CString::from_raw(ptr as *mut c_char)
+ }
+}
+
+impl<T> ToPointer for Rc<T> {
+ fn to_ptr(self) -> *mut () {
+ Rc::into_raw(self) as *mut T as *mut ()
+ }
+
+ unsafe fn from_ptr(ptr: *mut ()) -> Self {
+ Rc::from_raw(ptr as *mut T as *const T)
+ }
+}
+
+impl<T> ToPointer for Box<T> {
+ fn to_ptr(self) -> *mut () {
+ Box::into_raw(self) as _
+ }
+
+ unsafe fn from_ptr(ptr: *mut ()) -> Self {
+ Box::from_raw(ptr as _)
+ }
+}
+
+impl<T: Sized> ToPointer for &mut T {
+ fn to_ptr(self) -> *mut () {
+ self as *mut T as _
+ }
+
+ unsafe fn from_ptr(ptr: *mut ()) -> Self {
+ &mut *(ptr as *mut T)
+ }
+}
+
+/// Connect a GTK+ object signal to a function, providing an additional context value (by
+/// reference).
+macro_rules! connect_signal {
+ ( object $object:expr ; with $with:expr ;
+ signal $name:ident ($target:ident : &$type:ty $(, $argname:ident : $argtype:ty )* ) $( -> $ret:ty )? $body:block
+ ) => {{
+ unsafe extern "C" fn $name($($argname : $argtype ,)* $target: &$type) $( -> $ret )? $body
+ #[allow(unused_unsafe)]
+ unsafe {
+ gtk::g_signal_connect_data(
+ $object as *mut _,
+ cstr!(stringify!($name)),
+ Some(std::mem::transmute(
+ $name
+ as for<'a> unsafe extern "C" fn(
+ $($argtype,)*
+ &'a $type,
+ ) $( -> $ret )?,
+ )),
+ $with as *const $type as *mut $type as _,
+ None,
+ 0,
+ );
+ }
+ }};
+}
+
+/// Bind a read only (from the renderer perspective) property to a widget.
+///
+/// The `set` function is called initially and when the property value changes.
+macro_rules! property_read_only {
+ ( property $property:expr ;
+ fn set( $name:ident : & $type:ty ) $setbody:block
+ ) => {{
+ let prop: &Property<$type> = $property;
+ match prop {
+ Property::Static($name) => $setbody,
+ Property::Binding(v) => {
+ {
+ let $name = v.borrow();
+ $setbody
+ }
+ v.on_change(move |$name| $setbody);
+ }
+ Property::ReadOnly(_) => (),
+ }
+ }};
+}
+
+/// Bind a read/write property to a widget signal.
+///
+/// This currently only allows signals which are of the form
+/// `void(*)(SomeGtkObject*, gpointer user_data)`.
+macro_rules! property_with_signal {
+ ( object $object:expr ; property $property:expr ; signal $signame:ident ;
+ fn set( $name:ident : & $type:ty ) $setbody:block
+ fn get( $getobj:ident : $gettype:ty ) -> $result:ty $getbody:block
+ ) => {{
+ let prop: &Property<$type> = $property;
+ match prop {
+ Property::Static($name) => $setbody,
+ Property::Binding(v) => {
+ {
+ let $name = v.borrow();
+ $setbody
+ }
+ let changing = Rc::new(RefCell::new(false));
+ struct SignalData {
+ changing: Rc<RefCell<bool>>,
+ value: Synchronized<$result>,
+ }
+ let signal_data = Box::new(SignalData {
+ changing: changing.clone(),
+ value: v.clone(),
+ });
+ v.on_change(move |$name| {
+ if !*changing.borrow() {
+ *changing.borrow_mut() = true;
+ $setbody;
+ *changing.borrow_mut() = false;
+ }
+ });
+ connect_signal! {
+ object $object;
+ with signal_data.as_ref();
+ signal $signame(signal_data: &SignalData, $getobj: $gettype) {
+ let new_value = (|| $getbody)();
+ if !*signal_data.changing.borrow() {
+ *signal_data.changing.borrow_mut() = true;
+ *signal_data.value.borrow_mut() = new_value;
+ *signal_data.changing.borrow_mut() = false;
+ }
+ }
+ }
+ signal_data.drop_with_object($object as _);
+ }
+ Property::ReadOnly(v) => {
+ v.register(move |target: &mut $result| {
+ let $getobj: $gettype = $object as _;
+ *target = (|| $getbody)();
+ });
+ }
+ }
+ }};
+}
+
+unsafe extern "C" fn render_application(app_ptr: *mut gtk::GtkApplication, app: &Application) {
+ unsafe {
+ gtk::gtk_widget_set_default_direction(if app.rtl {
+ gtk::GtkTextDirection_GTK_TEXT_DIR_RTL
+ } else {
+ gtk::GtkTextDirection_GTK_TEXT_DIR_LTR
+ });
+ }
+ for window in &app.windows {
+ let window_ptr = render_window(&window.element_type);
+ let style = &window.style;
+
+ // Set size before applying style (since the style will set the visibility and show the
+ // window). Note that we take the size request as an initial size here.
+ //
+ // `gtk_window_set_default_size` doesn't work; it resizes to the size request of the inner
+ // labels (instead of wrapping them) since it doesn't know how small they should be (that's
+ // dictated by the window size!).
+ unsafe {
+ gtk::gtk_window_resize(
+ window_ptr as _,
+ style
+ .horizontal_size_request
+ .map(|v| v as i32)
+ .unwrap_or(-1),
+ style.vertical_size_request.map(|v| v as i32).unwrap_or(-1),
+ );
+ }
+
+ apply_style(window_ptr, style);
+ unsafe {
+ gtk::gtk_application_add_window(app_ptr, window_ptr as *mut _);
+ }
+ }
+}
+
+fn render(element: &Element) -> Option<*mut gtk::GtkWidget> {
+ let widget = render_element_type(&element.element_type)?;
+ apply_style(widget, &element.style);
+ Some(widget)
+}
+
+fn apply_style(widget: *mut gtk::GtkWidget, style: &model::ElementStyle) {
+ unsafe {
+ gtk::gtk_widget_set_halign(widget, alignment(&style.horizontal_alignment));
+ if style.horizontal_alignment == Alignment::Fill {
+ gtk::gtk_widget_set_hexpand(widget, true.into());
+ }
+ gtk::gtk_widget_set_valign(widget, alignment(&style.vertical_alignment));
+ if style.vertical_alignment == Alignment::Fill {
+ gtk::gtk_widget_set_vexpand(widget, true.into());
+ }
+ gtk::gtk_widget_set_size_request(
+ widget,
+ style
+ .horizontal_size_request
+ .map(|v| v as i32)
+ .unwrap_or(-1),
+ style.vertical_size_request.map(|v| v as i32).unwrap_or(-1),
+ );
+
+ gtk::gtk_widget_set_margin_start(widget, style.margin.start as i32);
+ gtk::gtk_widget_set_margin_end(widget, style.margin.end as i32);
+ gtk::gtk_widget_set_margin_top(widget, style.margin.top as i32);
+ gtk::gtk_widget_set_margin_bottom(widget, style.margin.bottom as i32);
+ }
+ property_read_only! {
+ property &style.visible;
+ fn set(new_value: &bool) {
+ unsafe {
+ gtk::gtk_widget_set_visible(widget, new_value.clone().into());
+ };
+ }
+ }
+ property_read_only! {
+ property &style.enabled;
+ fn set(new_value: &bool) {
+ unsafe {
+ gtk::gtk_widget_set_sensitive(widget, new_value.clone().into());
+ }
+ }
+ }
+}
+
+fn alignment(align: &Alignment) -> gtk::GtkAlign {
+ use Alignment::*;
+ match align {
+ Start => gtk::GtkAlign_GTK_ALIGN_START,
+ Center => gtk::GtkAlign_GTK_ALIGN_CENTER,
+ End => gtk::GtkAlign_GTK_ALIGN_END,
+ Fill => gtk::GtkAlign_GTK_ALIGN_FILL,
+ }
+}
+
+struct PangoAttrList {
+ list: *mut gtk::PangoAttrList,
+}
+
+impl PangoAttrList {
+ pub fn new() -> Self {
+ PangoAttrList {
+ list: unsafe { gtk::pango_attr_list_new() },
+ }
+ }
+
+ pub fn bold(&mut self) -> &mut Self {
+ unsafe {
+ gtk::pango_attr_list_insert(
+ self.list,
+ gtk::pango_attr_weight_new(gtk::PangoWeight_PANGO_WEIGHT_BOLD),
+ )
+ };
+ self
+ }
+}
+
+impl std::ops::Deref for PangoAttrList {
+ type Target = *mut gtk::PangoAttrList;
+
+ fn deref(&self) -> &Self::Target {
+ &self.list
+ }
+}
+
+impl std::ops::DerefMut for PangoAttrList {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.list
+ }
+}
+
+impl Drop for PangoAttrList {
+ fn drop(&mut self) {
+ unsafe { gtk::pango_attr_list_unref(self.list) };
+ }
+}
+
+fn render_element_type(element_type: &model::ElementType) -> Option<*mut gtk::GtkWidget> {
+ use model::ElementType::*;
+ Some(match element_type {
+ Label(model::Label { text, bold }) => {
+ let label_ptr = unsafe { gtk::gtk_label_new(std::ptr::null()) };
+ match text {
+ Property::Static(text) => {
+ let text = CString::new(text.clone()).ok()?;
+ unsafe { gtk::gtk_label_set_text(label_ptr as _, text.as_ptr()) };
+ text.drop_with_widget(label_ptr);
+ }
+ Property::Binding(b) => {
+ let label_text = Rc::new(RefCell::new(CString::new(b.borrow().clone()).ok()?));
+ unsafe {
+ gtk::gtk_label_set_text(label_ptr as _, label_text.borrow().as_ptr())
+ };
+ let lt = label_text.clone();
+ label_text.drop_with_widget(label_ptr);
+ b.on_change(move |t| {
+ let Some(cstr) = CString::new(t.clone()).ok() else {
+ return;
+ };
+ unsafe { gtk::gtk_label_set_text(label_ptr as _, cstr.as_ptr()) };
+ *lt.borrow_mut() = cstr;
+ });
+ }
+ Property::ReadOnly(_) => unimplemented!("ReadOnly not supported for Label::text"),
+ }
+ unsafe { gtk::gtk_label_set_line_wrap(label_ptr as _, true.into()) };
+ // This is gtk_label_set_{xalign,yalign} in gtk 3.16+
+ unsafe { gtk::gtk_misc_set_alignment(label_ptr as _, 0.0, 0.5) };
+ if *bold {
+ unsafe {
+ gtk::gtk_label_set_attributes(label_ptr as _, **PangoAttrList::new().bold())
+ };
+ }
+ label_ptr
+ }
+ HBox(model::HBox {
+ items,
+ spacing,
+ affirmative_order,
+ }) => {
+ let box_ptr =
+ unsafe { gtk::gtk_box_new(gtk::GtkOrientation_GTK_ORIENTATION_HORIZONTAL, 0) };
+ unsafe { gtk::gtk_box_set_spacing(box_ptr as *mut _, *spacing as i32) };
+ let items_iter: Box<dyn Iterator<Item = &Element>> = if *affirmative_order {
+ Box::new(items.iter().rev())
+ } else {
+ Box::new(items.iter())
+ };
+ for item in items_iter {
+ if let Some(widget) = render(item) {
+ unsafe {
+ gtk::gtk_container_add(box_ptr as *mut gtk::GtkContainer, widget);
+ }
+ // Special case horizontal alignment to pack into the end if appropriate
+ if item.style.horizontal_alignment == Alignment::End {
+ unsafe {
+ gtk::gtk_box_set_child_packing(
+ box_ptr as _,
+ widget,
+ false.into(),
+ false.into(),
+ 0,
+ gtk::GtkPackType_GTK_PACK_END,
+ );
+ }
+ }
+ }
+ }
+ box_ptr
+ }
+ VBox(model::VBox { items, spacing }) => {
+ let box_ptr =
+ unsafe { gtk::gtk_box_new(gtk::GtkOrientation_GTK_ORIENTATION_VERTICAL, 0) };
+ unsafe { gtk::gtk_box_set_spacing(box_ptr as *mut _, *spacing as i32) };
+ for item in items {
+ if let Some(widget) = render(item) {
+ unsafe {
+ gtk::gtk_container_add(box_ptr as *mut gtk::GtkContainer, widget);
+ }
+ // Special case vertical alignment to pack into the end if appropriate
+ if item.style.vertical_alignment == Alignment::End {
+ unsafe {
+ gtk::gtk_box_set_child_packing(
+ box_ptr as _,
+ widget,
+ false.into(),
+ false.into(),
+ 0,
+ gtk::GtkPackType_GTK_PACK_END,
+ );
+ }
+ }
+ }
+ }
+ box_ptr
+ }
+ Button(model::Button { content, click }) => {
+ let button_ptr = unsafe { gtk::gtk_button_new() };
+ if let Some(widget) = content.as_deref().and_then(render) {
+ unsafe {
+ // Always center widgets in buttons.
+ gtk::gtk_widget_set_valign(widget, alignment(&Alignment::Center));
+ gtk::gtk_widget_set_halign(widget, alignment(&Alignment::Center));
+ gtk::gtk_container_add(button_ptr as *mut gtk::GtkContainer, widget);
+ }
+ }
+ connect_signal! {
+ object button_ptr;
+ with click;
+ signal clicked(event: &Event<()>, _button: *mut gtk::GtkButton) {
+ event.fire(&());
+ }
+ }
+ button_ptr
+ }
+ Checkbox(model::Checkbox { checked, label }) => {
+ let cb_ptr = match label {
+ None => unsafe { gtk::gtk_check_button_new() },
+ Some(s) => {
+ let label = CString::new(s.clone()).ok()?;
+ let cb = unsafe { gtk::gtk_check_button_new_with_label(label.as_ptr()) };
+ label.drop_with_widget(cb);
+ cb
+ }
+ };
+ property_with_signal! {
+ object cb_ptr;
+ property checked;
+ signal toggled;
+ fn set(new_value: &bool) {
+ unsafe {
+ gtk::gtk_toggle_button_set_active(cb_ptr as *mut _, new_value.clone().into())
+ };
+ }
+ fn get(button: *mut gtk::GtkToggleButton) -> bool {
+ unsafe {
+ gtk::gtk_toggle_button_get_active(button) == 1
+ }
+ }
+ }
+ cb_ptr
+ }
+ TextBox(model::TextBox {
+ placeholder,
+ content,
+ editable,
+ }) => {
+ let text_ptr = unsafe { gtk::gtk_text_view_new() };
+ unsafe {
+ const GTK_WRAP_WORD_CHAR: u32 = 3;
+ gtk::gtk_text_view_set_wrap_mode(text_ptr as *mut _, GTK_WRAP_WORD_CHAR);
+ gtk::gtk_text_view_set_editable(text_ptr as *mut _, editable.clone().into());
+ gtk::gtk_text_view_set_accepts_tab(text_ptr as *mut _, false.into());
+ }
+ let buffer = unsafe { gtk::gtk_text_view_get_buffer(text_ptr as *mut _) };
+
+ struct State {
+ placeholder: Option<Placeholder>,
+ }
+
+ struct Placeholder {
+ string: CString,
+ visible: RefCell<bool>,
+ }
+
+ impl Placeholder {
+ fn focus(&self, widget: *mut gtk::GtkWidget) {
+ if *self.visible.borrow() {
+ unsafe {
+ let buffer = gtk::gtk_text_view_get_buffer(widget as *mut _);
+ gtk::gtk_text_buffer_set_text(buffer, self.string.as_ptr(), 0);
+ gtk::gtk_widget_override_color(
+ widget,
+ gtk::GtkStateFlags_GTK_STATE_FLAG_NORMAL,
+ std::ptr::null(),
+ );
+ }
+ *self.visible.borrow_mut() = false;
+ }
+ }
+
+ fn unfocus(&self, widget: *mut gtk::GtkWidget) {
+ unsafe {
+ let buffer = gtk::gtk_text_view_get_buffer(widget as *mut _);
+
+ let mut end_iter = gtk::GtkTextIter::default();
+ gtk::gtk_text_buffer_get_end_iter(buffer, &mut end_iter);
+ let is_empty = gtk::gtk_text_iter_get_offset(&end_iter) == 0;
+
+ if is_empty && !*self.visible.borrow() {
+ gtk::gtk_text_buffer_set_text(buffer, self.string.as_ptr(), -1);
+ let context = gtk::gtk_widget_get_style_context(widget);
+ let mut color = gtk::GdkRGBA::default();
+ gtk::gtk_style_context_get_color(
+ context,
+ gtk::GtkStateFlags_GTK_STATE_FLAG_INSENSITIVE,
+ &mut color,
+ );
+ gtk::gtk_widget_override_color(
+ widget,
+ gtk::GtkStateFlags_GTK_STATE_FLAG_NORMAL,
+ &color,
+ );
+ *self.visible.borrow_mut() = true;
+ }
+ }
+ }
+ }
+
+ let mut state = Box::new(State { placeholder: None });
+
+ if let Some(placeholder) = placeholder {
+ state.placeholder = Some(Placeholder {
+ string: CString::new(placeholder.clone()).ok()?,
+ visible: RefCell::new(false),
+ });
+
+ let placeholder = state.placeholder.as_ref().unwrap();
+
+ placeholder.unfocus(text_ptr);
+
+ connect_signal! {
+ object text_ptr;
+ with placeholder;
+ signal focus_in_event(placeholder: &Placeholder, widget: *mut gtk::GtkWidget,
+ _event: *mut gtk::GdkEventFocus) -> gtk::gboolean {
+ placeholder.focus(widget);
+ false.into()
+ }
+ }
+ connect_signal! {
+ object text_ptr;
+ with placeholder;
+ signal focus_out_event(placeholder: &Placeholder, widget: *mut gtk::GtkWidget,
+ _event: *mut gtk::GdkEventFocus) -> gtk::gboolean {
+ placeholder.unfocus(widget);
+ false.into()
+ }
+ }
+ }
+
+ // Attach the state so that we can access it in the changed signal.
+ // This is kind of ugly; in the future it might be a nicer developer interface to simply
+ // always use a closure as the user data (which itself can capture arbitrary things). This
+ // would move the data management from GTK to rust.
+ state.set_data(buffer as *mut _, cstr!("textview-state"));
+
+ property_with_signal! {
+ object buffer;
+ property content;
+ signal changed;
+ fn set(new_value: &String) {
+ unsafe {
+ gtk::gtk_text_buffer_set_text(buffer, new_value.as_ptr() as *const c_char, new_value.len().try_into().unwrap());
+ }
+ }
+ fn get(buffer: *mut gtk::GtkTextBuffer) -> String {
+ let state = unsafe {
+ gtk::g_object_get_data(buffer as *mut _, cstr!("textview-state"))
+ };
+ if !state.is_null() {
+ let s: &State = unsafe { &*(state as *mut State as *const State) };
+ if let Some(placeholder) = &s.placeholder {
+ if *placeholder.visible.borrow() {
+ return "".to_owned();
+ }
+ }
+ }
+ let mut start_iter = gtk::GtkTextIter::default();
+ unsafe {
+ gtk::gtk_text_buffer_get_start_iter(buffer, &mut start_iter);
+ }
+
+ let mut s = String::new();
+ loop {
+ let c = unsafe { gtk::gtk_text_iter_get_char(&start_iter) };
+ if c == 0 {
+ break;
+ }
+ // Safety:
+ // gunichar is guaranteed to be a valid unicode codepoint (if nonzero).
+ s.push(unsafe { char::from_u32_unchecked(c) });
+ unsafe {
+ gtk::gtk_text_iter_forward_char(&mut start_iter);
+ }
+ }
+ s
+ }
+ }
+ text_ptr
+ }
+ Scroll(model::Scroll { content }) => {
+ let scroll_ptr =
+ unsafe { gtk::gtk_scrolled_window_new(std::ptr::null_mut(), std::ptr::null_mut()) };
+ unsafe {
+ gtk::gtk_scrolled_window_set_policy(
+ scroll_ptr as *mut _,
+ gtk::GtkPolicyType_GTK_POLICY_NEVER,
+ gtk::GtkPolicyType_GTK_POLICY_ALWAYS,
+ );
+ gtk::gtk_scrolled_window_set_shadow_type(
+ scroll_ptr as *mut _,
+ gtk::GtkShadowType_GTK_SHADOW_IN,
+ );
+ };
+ if let Some(widget) = content.as_deref().and_then(render) {
+ unsafe {
+ gtk::gtk_container_add(scroll_ptr as *mut gtk::GtkContainer, widget);
+ }
+ }
+ scroll_ptr
+ }
+ Progress(model::Progress { amount }) => {
+ let progress_ptr = unsafe { gtk::gtk_progress_bar_new() };
+ property_read_only! {
+ property amount;
+ fn set(value: &Option<f32>) {
+ match &*value {
+ Some(v) => unsafe {
+ gtk::gtk_progress_bar_set_fraction(
+ progress_ptr as *mut _,
+ v.clamp(0f32,1f32) as f64,
+ );
+ }
+ None => unsafe {
+ gtk::gtk_progress_bar_pulse(progress_ptr as *mut _);
+
+ fn auto_pulse_progress_bar(progress: *mut gtk::GtkProgressBar) {
+ unsafe extern fn pulse(progress: *mut std::ffi::c_void) -> gtk::gboolean {
+ if gtk::gtk_widget_is_visible(progress as _) == 0 {
+ false.into()
+ } else {
+ gtk::gtk_progress_bar_pulse(progress as _);
+ true.into()
+ }
+ }
+ unsafe {
+ gtk::g_timeout_add(100, Some(pulse as unsafe extern fn(*mut std::ffi::c_void) -> gtk::gboolean), progress as _);
+ }
+
+ }
+
+ connect_signal! {
+ object progress_ptr;
+ with std::ptr::null_mut();
+ signal show(_user_data: &(), progress: *mut gtk::GtkWidget) {
+ auto_pulse_progress_bar(progress as *mut _);
+ }
+ }
+ auto_pulse_progress_bar(progress_ptr as *mut _);
+ }
+ }
+ }
+ }
+ progress_ptr
+ }
+ })
+}
+
+fn render_window(
+ model::Window {
+ title,
+ content,
+ children,
+ modal,
+ close,
+ }: &model::Window,
+) -> *mut gtk::GtkWidget {
+ unsafe {
+ let window_ptr = gtk::gtk_window_new(gtk::GtkWindowType_GTK_WINDOW_TOPLEVEL);
+ if !title.is_empty() {
+ if let Some(title) = CString::new(title.clone()).ok() {
+ gtk::gtk_window_set_title(window_ptr as *mut _, title.as_ptr());
+ title.drop_with_widget(window_ptr);
+ }
+ }
+ if let Some(content) = content {
+ if let Some(widget) = render(content) {
+ gtk::gtk_container_add(window_ptr as *mut gtk::GtkContainer, widget);
+ }
+ }
+ for child in children {
+ let widget = render_window(&child.element_type);
+ apply_style(widget, &child.style);
+ gtk::gtk_window_set_transient_for(widget as *mut _, window_ptr as *mut _);
+ // Delete should hide the window.
+ gtk::g_signal_connect_data(
+ widget as *mut _,
+ cstr!("delete-event"),
+ Some(std::mem::transmute(
+ gtk::gtk_widget_hide_on_delete
+ as unsafe extern "C" fn(*mut gtk::GtkWidget) -> i32,
+ )),
+ std::ptr::null_mut(),
+ None,
+ 0,
+ );
+ }
+ if *modal {
+ gtk::gtk_window_set_modal(window_ptr as *mut _, true.into());
+ }
+ if let Some(close) = close {
+ close.subscribe(move |&()| gtk::gtk_window_close(window_ptr as *mut _));
+ }
+
+ window_ptr
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/macos/mod.rs b/toolkit/crashreporter/client/app/src/ui/macos/mod.rs
new file mode 100644
index 0000000000..6520d16472
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/macos/mod.rs
@@ -0,0 +1,1122 @@
+/* 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/. */
+
+//! A UI using the macos cocoa API.
+//!
+//! This UI contains some edge cases that aren't implemented, for instance:
+//! * there are a few cases where specific hierarchies are handled differently (e.g. a Button
+//! containing Label), etc.
+//! * not all controls handle all Property variants (e.g. Checkbox doesn't handle ReadOnly, Text
+//! doesn't handle Binding, etc).
+//!
+//! The rendering ignores `ElementStyle::margin` entirely, because
+//! * `NSView` doesn't support margins (so working them into constraints would be a bit annoying),
+//! and
+//! * `NSView.layoutMarginsGuide` results in a layout almost identical to what the margins (at the
+//! time of this writing) in the UI layouts are achieving.
+//!
+//! In a few cases, init or creation functions are called which _could_ return nil and are wrapped
+//! in their type wrapper (as those functions return `instancetype`/`id`). We consider this safe
+//! enough because it won't cause unsoundness (they are only passed to objc functions which can
+//! take nil arguments) and the failure case is very unlikely.
+
+#![allow(non_upper_case_globals)]
+
+use self::objc::*;
+use super::model::{self, Alignment, Application, Element, TypedElement};
+use crate::data::Property;
+use cocoa::{
+ INSApplication, INSBox, INSButton, INSColor, INSControl, INSFont, INSLayoutAnchor,
+ INSLayoutConstraint, INSLayoutDimension, INSLayoutGuide, INSMenu, INSMenuItem,
+ INSMutableParagraphStyle, INSObject, INSProcessInfo, INSProgressIndicator, INSRunLoop,
+ INSScrollView, INSStackView, INSText, INSTextContainer, INSTextField, INSTextView, INSView,
+ INSWindow, NSArray_NSArrayCreation, NSAttributedString_NSExtendedAttributedString,
+ NSDictionary_NSDictionaryCreation, NSRunLoop_NSRunLoopConveniences,
+ NSStackView_NSStackViewGravityAreas, NSString_NSStringExtensionMethods,
+ NSTextField_NSTextFieldConvenience, NSView_NSConstraintBasedLayoutInstallingConstraints,
+ NSView_NSConstraintBasedLayoutLayering, NSView_NSSafeAreas, PNSObject,
+};
+use once_cell::sync::Lazy;
+
+/// https://developer.apple.com/documentation/foundation/1497293-string_encodings/nsutf8stringencoding?language=objc
+const NSUTF8StringEncoding: cocoa::NSStringEncoding = 4;
+
+/// Constant from NSCell.h
+const NSControlStateValueOn: cocoa::NSControlStateValue = 1;
+
+/// Constant from NSLayoutConstraint.h
+const NSLayoutPriorityDefaultHigh: cocoa::NSLayoutPriority = 750.0;
+
+mod objc;
+
+/// A MacOS Cocoa UI implementation.
+#[derive(Default)]
+pub struct UI;
+
+impl UI {
+ pub fn run_loop(&self, app: Application) {
+ let nsapp = unsafe { cocoa::NSApplication::sharedApplication() };
+
+ Objc::<AppDelegate>::register();
+ Objc::<Button>::register();
+ Objc::<Checkbox>::register();
+ Objc::<TextView>::register();
+ Objc::<Window>::register();
+
+ rc::autoreleasepool(|| {
+ let delegate = AppDelegate::new(app).into_object();
+ // Set delegate
+ unsafe { nsapp.setDelegate_(delegate.instance as *mut _) };
+
+ // Set up the main menu
+ unsafe {
+ let appname = read_nsstring(cocoa::NSProcessInfo::processInfo().processName());
+ let mainmenu = StrongRef::new(cocoa::NSMenu::alloc());
+ mainmenu.init();
+
+ {
+ // We don't need a title for the app menu item nor menu; it will always come from
+ // the process name regardless of what we set.
+ let appmenuitem = StrongRef::new(cocoa::NSMenuItem::alloc()).autorelease();
+ appmenuitem.init();
+ mainmenu.addItem_(appmenuitem);
+
+ let appmenu = StrongRef::new(cocoa::NSMenu::alloc());
+ appmenu.init();
+
+ {
+ let quit = StrongRef::new(cocoa::NSMenuItem::alloc());
+ quit.initWithTitle_action_keyEquivalent_(
+ nsstring(&format!("Quit {appname}")),
+ sel!(terminate:),
+ nsstring("q"),
+ );
+ appmenu.addItem_(quit.autorelease());
+ }
+ appmenuitem.setSubmenu_(appmenu.autorelease());
+ }
+ {
+ let editmenuitem = StrongRef::new(cocoa::NSMenuItem::alloc()).autorelease();
+ editmenuitem.initWithTitle_action_keyEquivalent_(
+ nsstring("Edit"),
+ runtime::Sel::from_ptr(std::ptr::null()),
+ nsstring(""),
+ );
+ mainmenu.addItem_(editmenuitem);
+
+ let editmenu = StrongRef::new(cocoa::NSMenu::alloc());
+ editmenu.initWithTitle_(nsstring("Edit"));
+
+ let add_item = |name, selector, shortcut| {
+ let item = StrongRef::new(cocoa::NSMenuItem::alloc());
+ item.initWithTitle_action_keyEquivalent_(
+ nsstring(name),
+ selector,
+ nsstring(shortcut),
+ );
+ editmenu.addItem_(item.autorelease());
+ };
+
+ add_item("Undo", sel!(undo:), "z");
+ add_item("Redo", sel!(redo:), "Z");
+ editmenu.addItem_(cocoa::NSMenuItem::separatorItem());
+ add_item("Cut", sel!(cut:), "x");
+ add_item("Copy", sel!(copy:), "c");
+ add_item("Paste", sel!(paste:), "v");
+ add_item("Delete", sel!(delete:), "");
+ add_item("Select All", sel!(selectAll:), "a");
+
+ editmenuitem.setSubmenu_(editmenu.autorelease());
+ }
+
+ nsapp.setMainMenu_(mainmenu.autorelease());
+ }
+
+ // Run the main application loop
+ unsafe { nsapp.run() };
+ });
+ }
+
+ pub fn invoke(&self, f: model::InvokeFn) {
+ // Blocks only take `Fn`, so we have to wrap the boxed function.
+ let f = std::cell::RefCell::new(Some(f));
+ enqueue(move || {
+ if let Some(f) = f.borrow_mut().take() {
+ f();
+ }
+ });
+ }
+}
+
+fn enqueue<F: Fn() + 'static>(f: F) {
+ let block = block::ConcreteBlock::new(f);
+ // The block must be an RcBlock so addOperationWithBlock can retain it.
+ // https://docs.rs/block/latest/block/#creating-blocks
+ let block = block.copy();
+
+ // We need to explicitly signal that the enqueued blocks can run in both the default mode (the
+ // main loop) and modal mode, otherwise when modal windows are opened things get stuck.
+ struct RunloopModes(cocoa::NSArray);
+
+ impl RunloopModes {
+ pub fn new() -> Self {
+ unsafe {
+ let objects: [cocoa::id; 2] = [
+ cocoa::NSDefaultRunLoopMode.0,
+ cocoa::NSModalPanelRunLoopMode.0,
+ ];
+ RunloopModes(
+ cocoa::NSArray(<cocoa::NSArray as NSArray_NSArrayCreation<
+ cocoa::NSRunLoopMode,
+ >>::arrayWithObjects_count_(
+ objects.as_slice().as_ptr() as *const *mut u64,
+ objects
+ .as_slice()
+ .len()
+ .try_into()
+ .expect("usize can't fit in u64"),
+ )),
+ )
+ }
+ }
+ }
+
+ // # Safety
+ // The array is static and cannot be changed.
+ unsafe impl Sync for RunloopModes {}
+ unsafe impl Send for RunloopModes {}
+
+ static RUNLOOP_MODES: Lazy<RunloopModes> = Lazy::new(RunloopModes::new);
+
+ unsafe {
+ cocoa::NSRunLoop::mainRunLoop().performInModes_block_(RUNLOOP_MODES.0, &*block);
+ }
+}
+
+#[repr(transparent)]
+struct Rect(pub cocoa::NSRect);
+
+unsafe impl Encode for Rect {
+ fn encode() -> Encoding {
+ unsafe { Encoding::from_str("{CGRect={CGPoint=dd}{CGSize=dd}}") }
+ }
+}
+
+/// Create an NSString by copying a str.
+fn nsstring(v: &str) -> cocoa::NSString {
+ unsafe {
+ StrongRef::new(cocoa::NSString(
+ cocoa::NSString::alloc().initWithBytes_length_encoding_(
+ v.as_ptr() as *const _,
+ v.len().try_into().expect("usize can't fit in u64"),
+ NSUTF8StringEncoding,
+ ),
+ ))
+ }
+ .autorelease()
+}
+
+/// Create a String by copying an NSString
+fn read_nsstring(s: cocoa::NSString) -> String {
+ let c_str = unsafe { std::ffi::CStr::from_ptr(s.UTF8String()) };
+ c_str.to_str().expect("NSString isn't UTF8").to_owned()
+}
+
+fn nsrect<X: Into<f64>, Y: Into<f64>, W: Into<f64>, H: Into<f64>>(
+ x: X,
+ y: Y,
+ w: W,
+ h: H,
+) -> cocoa::NSRect {
+ cocoa::NSRect {
+ origin: cocoa::NSPoint {
+ x: x.into(),
+ y: y.into(),
+ },
+ size: cocoa::NSSize {
+ width: w.into(),
+ height: h.into(),
+ },
+ }
+}
+
+struct AppDelegate {
+ app: Option<Application>,
+ windows: Vec<StrongRef<cocoa::NSWindow>>,
+}
+
+impl AppDelegate {
+ pub fn new(app: Application) -> Self {
+ AppDelegate {
+ app: Some(app),
+ windows: Default::default(),
+ }
+ }
+}
+
+objc_class! {
+ impl AppDelegate: NSObject /*<NSApplicationDelegate>*/ {
+ #[sel(applicationDidFinishLaunching:)]
+ fn application_did_finish_launching(&mut self, _notification: Ptr<cocoa::NSNotification>) {
+ // Activate the application (bringing windows to the active foreground later)
+ unsafe { cocoa::NSApplication::sharedApplication().activateIgnoringOtherApps_(runtime::YES) };
+
+ let mut first = true;
+ let mut windows = WindowRenderer::default();
+ let app = self.app.take().unwrap();
+ windows.rtl = app.rtl;
+ for window in app.windows {
+ let w = windows.render(window);
+ unsafe {
+ if first {
+ w.makeKeyAndOrderFront_(self.instance);
+ w.makeMainWindow();
+ first = false;
+ }
+ }
+ }
+ self.windows = windows.unwrap();
+
+ }
+
+ #[sel(applicationShouldTerminateAfterLastWindowClosed:)]
+ fn application_should_terminate_after_window_closed(&mut self, _app: Ptr<cocoa::NSApplication>) -> runtime::BOOL {
+ runtime::YES
+ }
+ }
+}
+
+struct Window {
+ modal: bool,
+ title: String,
+ style: model::ElementStyle,
+}
+
+objc_class! {
+ impl Window: NSWindow /*<NSWindowDelegate>*/ {
+ #[sel(init)]
+ fn init(&mut self) -> cocoa::id {
+ let style = &self.style;
+ let title = &self.title;
+ let w = cocoa::NSWindow(self.instance);
+
+ unsafe {
+ if w.initWithContentRect_styleMask_backing_defer_(
+ nsrect(
+ 0,
+ 0,
+ style.horizontal_size_request.unwrap_or(800),
+ style.vertical_size_request.unwrap_or(500),
+ ),
+ cocoa::NSWindowStyleMaskTitled
+ | cocoa::NSWindowStyleMaskClosable
+ | cocoa::NSWindowStyleMaskResizable
+ | cocoa::NSWindowStyleMaskMiniaturizable,
+ cocoa::NSBackingStoreBuffered,
+ runtime::NO,
+ ).is_null() {
+ return std::ptr::null_mut();
+ }
+
+ w.setDelegate_(self.instance as _);
+ w.setMinSize_(cocoa::NSSize {
+ width: style.horizontal_size_request.unwrap_or(0) as f64,
+ height: style.vertical_size_request.unwrap_or(0) as f64,
+ });
+
+ if !title.is_empty() {
+ w.setTitle_(nsstring(title.as_str()));
+ }
+ }
+
+ self.instance
+ }
+
+ #[sel(windowWillClose:)]
+ fn window_will_close(&mut self, _notification: Ptr<cocoa::NSNotification>) {
+ if self.modal {
+ unsafe {
+ let nsapp = cocoa::NSApplication::sharedApplication();
+ nsapp.stopModal();
+ }
+ }
+ }
+ }
+}
+
+impl From<Objc<Window>> for cocoa::NSWindow {
+ fn from(ptr: Objc<Window>) -> Self {
+ cocoa::NSWindow(ptr.instance)
+ }
+}
+
+struct Button {
+ element: model::Button,
+}
+
+impl Button {
+ pub fn with_title(self, title: &str) -> cocoa::NSButton {
+ let obj = self.into_object();
+ unsafe {
+ let () = msg_send![obj.instance, setTitle: nsstring(title)];
+ }
+ // # Safety
+ // NSButton is the superclass of Objc<Button>.
+ unsafe { std::mem::transmute(obj.autorelease()) }
+ }
+}
+
+objc_class! {
+ impl Button: NSButton {
+ #[sel(initWithFrame:)]
+ fn init_with_frame(&mut self, frame_rect: Rect) -> cocoa::id {
+ unsafe {
+ let object: cocoa::id = msg_send![super(self.instance, class!(NSButton)), initWithFrame: frame_rect.0];
+ if object.is_null() {
+ return object;
+ }
+ let () = msg_send![object, setBezelStyle: cocoa::NSBezelStyleRounded];
+ let () = msg_send![object, setAction: sel!(didClick)];
+ let () = msg_send![object, setTarget: object];
+ object
+ }
+ }
+
+ #[sel(didClick)]
+ fn did_click(&mut self) {
+ self.element.click.fire(&());
+ }
+ }
+}
+
+struct Checkbox {
+ element: model::Checkbox,
+}
+
+objc_class! {
+ impl Checkbox: NSButton {
+ #[sel(initWithFrame:)]
+ fn init_with_frame(&mut self, frame_rect: Rect) -> cocoa::id {
+ unsafe {
+ let object: cocoa::id = msg_send![super(self.instance, class!(NSButton)), initWithFrame: frame_rect.0];
+ if object.is_null() {
+ return object;
+ }
+ let () = msg_send![object, setButtonType: cocoa::NSButtonTypeSwitch];
+ if let Some(label) = &self.element.label {
+ let () = msg_send![object, setTitle: nsstring(label.as_str())];
+ }
+ let () = msg_send![object, setAction: sel!(didClick:)];
+ let () = msg_send![object, setTarget: object];
+
+ match &self.element.checked {
+ Property::Binding(s) => {
+ if *s.borrow() {
+ let () = msg_send![object, setState: NSControlStateValueOn];
+ }
+ }
+ Property::ReadOnly(_) => (),
+ Property::Static(_) => (),
+ }
+
+ object
+ }
+ }
+
+ #[sel(didClick:)]
+ fn did_click(&mut self, button: Objc<Checkbox>) {
+ match &self.element.checked {
+ Property::Binding(s) => {
+ let state = unsafe { std::mem::transmute::<_, cocoa::NSButton>(button).state() };
+ *s.borrow_mut() = state == NSControlStateValueOn;
+ }
+ Property::ReadOnly(_) => (),
+ Property::Static(_) => (),
+ }
+ }
+ }
+}
+
+impl Checkbox {
+ pub fn into_button(self) -> cocoa::NSButton {
+ let obj = self.into_object();
+ // # Safety
+ // NSButton is the superclass of Objc<Checkbox>.
+ unsafe { std::mem::transmute(obj.autorelease()) }
+ }
+}
+
+struct TextView;
+
+objc_class! {
+ impl TextView: NSTextView /*<NSTextViewDelegate>*/ {
+ #[sel(initWithFrame:)]
+ fn init_with_frame(&mut self, frame_rect: Rect) -> cocoa::id {
+ unsafe {
+ let object: cocoa::id = msg_send![super(self.instance, class!(NSTextView)), initWithFrame: frame_rect.0];
+ if object.is_null() {
+ return object;
+ }
+ let () = msg_send![object, setDelegate: self.instance];
+ object
+ }
+ }
+
+ #[sel(textView:doCommandBySelector:)]
+ fn do_command_by_selector(&mut self, text_view: Ptr<cocoa::NSTextView>, selector: runtime::Sel) -> runtime::BOOL {
+ let Ptr(text_view) = text_view;
+ // Make Tab/Backtab navigate to key views rather than inserting tabs in the text view.
+ // We can't use the `NSText` `fieldEditor` property to implement this behavior because
+ // that will disable the Enter key.
+ if selector == sel!(insertTab:) {
+ unsafe { text_view.window().selectNextKeyView_(text_view.0) };
+ return runtime::YES;
+ } else if selector == sel!(insertBacktab:) {
+ unsafe { text_view.window().selectPreviousKeyView_(text_view.0) };
+ return runtime::YES;
+ }
+ runtime::NO
+ }
+ }
+}
+
+impl From<Objc<TextView>> for cocoa::NSTextView {
+ fn from(tv: Objc<TextView>) -> Self {
+ // # Safety
+ // NSTextView is the superclass of Objc<TextView>.
+ unsafe { std::mem::transmute(tv) }
+ }
+}
+
+// For some reason the bindgen code for the nslayoutanchor subclasses doesn't have
+// `Into<NSLayoutAnchor>`, so we add our own.
+trait IntoNSLayoutAnchor {
+ fn into_layout_anchor(self) -> cocoa::NSLayoutAnchor;
+}
+
+impl IntoNSLayoutAnchor for cocoa::NSLayoutXAxisAnchor {
+ fn into_layout_anchor(self) -> cocoa::NSLayoutAnchor {
+ // # Safety
+ // NSLayoutXAxisAnchor is a subclass of NSLayoutAnchor
+ cocoa::NSLayoutAnchor(self.0)
+ }
+}
+
+impl IntoNSLayoutAnchor for cocoa::NSLayoutYAxisAnchor {
+ fn into_layout_anchor(self) -> cocoa::NSLayoutAnchor {
+ // # Safety
+ // NSLayoutYAxisAnchor is a subclass of NSLayoutAnchor
+ cocoa::NSLayoutAnchor(self.0)
+ }
+}
+
+unsafe fn constraint_equal<T, O>(anchor: T, to: O)
+where
+ T: INSLayoutAnchor<()> + std::ops::Deref,
+ T::Target: Message + Sized,
+ O: IntoNSLayoutAnchor,
+{
+ anchor
+ .constraintEqualToAnchor_(to.into_layout_anchor())
+ .setActive_(runtime::YES);
+}
+
+#[derive(Default)]
+struct WindowRenderer {
+ windows_to_retain: Vec<StrongRef<cocoa::NSWindow>>,
+ rtl: bool,
+}
+
+impl WindowRenderer {
+ pub fn unwrap(self) -> Vec<StrongRef<cocoa::NSWindow>> {
+ self.windows_to_retain
+ }
+
+ pub fn render(&mut self, window: TypedElement<model::Window>) -> StrongRef<cocoa::NSWindow> {
+ let style = window.style;
+ let model::Window {
+ close,
+ children,
+ content,
+ modal,
+ title,
+ } = window.element_type;
+
+ let w = Window {
+ modal,
+ title,
+ style,
+ }
+ .into_object();
+
+ let nswindow: StrongRef<cocoa::NSWindow> = w.clone().cast();
+
+ unsafe {
+ // Don't release windows when closed: we retain windows at the top-level.
+ nswindow.setReleasedWhenClosed_(runtime::NO);
+
+ if let Some(close) = close {
+ let nswindow = nswindow.weak();
+ close.subscribe(move |&()| {
+ if let Some(nswindow) = nswindow.lock() {
+ nswindow.close();
+ }
+ });
+ }
+
+ if let Some(e) = content {
+ // Use an NSBox as a container view so that the window's content can easily have
+ // constraints set up relative to the parent (they can't be set relative to the
+ // window).
+ let content_parent: StrongRef<cocoa::NSBox> = msg_send![class!(NSBox), new];
+ content_parent.setTitlePosition_(cocoa::NSNoTitle);
+ content_parent.setTransparent_(runtime::YES);
+ content_parent.setContentViewMargins_(cocoa::NSSize {
+ width: 5.0,
+ height: 5.0,
+ });
+ if ViewRenderer::new_with_selector(self.rtl, *content_parent, sel!(setContentView:))
+ .render(*e)
+ {
+ nswindow.setContentView_((*content_parent).into());
+ }
+ }
+
+ for child in children {
+ let modal = child.element_type.modal;
+ let visible = child.style.visible.clone();
+ let child_window = self.render(child);
+
+ #[derive(Clone, Copy)]
+ struct ShowChild {
+ modal: bool,
+ }
+
+ impl ShowChild {
+ pub fn show(&self, parent: cocoa::NSWindow, child: cocoa::NSWindow) {
+ unsafe {
+ parent.addChildWindow_ordered_(child, cocoa::NSWindowAbove);
+ child.makeKeyAndOrderFront_(parent.0);
+ if self.modal {
+ // Run the modal from the main nsapp.run() loop to prevent binding
+ // updates from being nested (as this will block until the modal is
+ // stopped).
+ enqueue(move || {
+ let nsapp = cocoa::NSApplication::sharedApplication();
+ nsapp.runModalForWindow_(child);
+ });
+ }
+ }
+ }
+ }
+
+ let show_child = ShowChild { modal };
+
+ match visible {
+ Property::Static(visible) => {
+ if visible {
+ show_child.show(*nswindow, *child_window);
+ }
+ }
+ Property::Binding(b) => {
+ let child = child_window.weak();
+ let parent = nswindow.weak();
+ b.on_change(move |visible| {
+ let Some((w, child_window)) = parent.lock().zip(child.lock()) else {
+ return;
+ };
+ if *visible {
+ show_child.show(*w, *child_window);
+ } else {
+ child_window.close();
+ }
+ });
+ if *b.borrow() {
+ show_child.show(*nswindow, *child_window);
+ }
+ }
+ Property::ReadOnly(_) => panic!("window visibility cannot be ReadOnly"),
+ }
+ }
+ }
+ self.windows_to_retain.push(nswindow.clone());
+ nswindow
+ }
+}
+
+struct ViewRenderer {
+ parent: cocoa::NSView,
+ add_subview: Box<dyn Fn(cocoa::NSView, &model::ElementStyle, cocoa::NSView)>,
+ ignore_vertical: bool,
+ ignore_horizontal: bool,
+ rtl: bool,
+}
+
+impl ViewRenderer {
+ /// add_subview should add the rendered child view.
+ pub fn new<F>(rtl: bool, parent: impl Into<cocoa::NSView>, add_subview: F) -> Self
+ where
+ F: Fn(cocoa::NSView, &model::ElementStyle, cocoa::NSView) + 'static,
+ {
+ ViewRenderer {
+ parent: parent.into(),
+ add_subview: Box::new(add_subview),
+ ignore_vertical: false,
+ ignore_horizontal: false,
+ rtl,
+ }
+ }
+
+ /// add_subview should be the selector to call on the parent view to add the rendered child view.
+ pub fn new_with_selector(
+ rtl: bool,
+ parent: impl Into<cocoa::NSView>,
+ add_subview: runtime::Sel,
+ ) -> Self {
+ Self::new(rtl, parent, move |parent, _style, child| {
+ let () = unsafe { (*parent.0).send_message(add_subview, (child,)) }.unwrap();
+ })
+ }
+
+ /// Ignore vertical layout settings when rendering views.
+ pub fn ignore_vertical(mut self, setting: bool) -> Self {
+ self.ignore_vertical = setting;
+ self
+ }
+
+ /// Ignore horizontal layout settings when rendering views.
+ pub fn ignore_horizontal(mut self, setting: bool) -> Self {
+ self.ignore_horizontal = setting;
+ self
+ }
+
+ /// Render the given element.
+ ///
+ /// Returns whether the element was rendered.
+ pub fn render(
+ &self,
+ Element {
+ style,
+ element_type,
+ }: Element,
+ ) -> bool {
+ let Some(view) = render_element(element_type, &style, self.rtl) else {
+ return false;
+ };
+
+ (self.add_subview)(self.parent, &style, view);
+
+ // Setting the content hugging priority to a high value causes stackviews to not stretch
+ // subviews during autolayout.
+ unsafe {
+ view.setContentHuggingPriority_forOrientation_(
+ NSLayoutPriorityDefaultHigh,
+ cocoa::NSLayoutConstraintOrientationHorizontal,
+ );
+ view.setContentHuggingPriority_forOrientation_(
+ NSLayoutPriorityDefaultHigh,
+ cocoa::NSLayoutConstraintOrientationVertical,
+ );
+ }
+
+ // Set layout and writing direction based on RTL.
+ unsafe {
+ view.setUserInterfaceLayoutDirection_(if self.rtl {
+ cocoa::NSUserInterfaceLayoutDirectionRightToLeft
+ } else {
+ cocoa::NSUserInterfaceLayoutDirectionLeftToRight
+ });
+ if let Ok(control) = cocoa::NSControl::try_from(view) {
+ control.setBaseWritingDirection_(if self.rtl {
+ cocoa::NSWritingDirectionRightToLeft
+ } else {
+ cocoa::NSWritingDirectionLeftToRight
+ });
+ }
+ }
+
+ let lmg = unsafe { self.parent.layoutMarginsGuide() };
+
+ if !matches!(style.horizontal_alignment, Alignment::Fill) {
+ if let Some(size) = style.horizontal_size_request {
+ unsafe {
+ view.widthAnchor()
+ .constraintGreaterThanOrEqualToConstant_(size as _)
+ .setActive_(runtime::YES);
+ }
+ }
+ }
+
+ if !self.ignore_horizontal {
+ unsafe {
+ let la = view.leadingAnchor();
+ let ta = view.trailingAnchor();
+ let pla = lmg.leadingAnchor();
+ let pta = lmg.trailingAnchor();
+ match style.horizontal_alignment {
+ Alignment::Fill => {
+ constraint_equal(la, pla);
+ constraint_equal(ta, pta);
+ // Without the autoresizing mask set, Text within Scroll doesn't display
+ // properly (it shrinks to 0-width, likely due to some specific interaction
+ // of NSScrollView with autolayout).
+ view.setAutoresizingMask_(cocoa::NSViewWidthSizable);
+ }
+ Alignment::Start => {
+ constraint_equal(la, pla);
+ }
+ Alignment::Center => {
+ let ca = view.centerXAnchor();
+ let pca = lmg.centerXAnchor();
+ constraint_equal(ca, pca);
+ }
+ Alignment::End => {
+ constraint_equal(ta, pta);
+ }
+ }
+ }
+ }
+
+ if !matches!(style.vertical_alignment, Alignment::Fill) {
+ if let Some(size) = style.vertical_size_request {
+ unsafe {
+ view.heightAnchor()
+ .constraintGreaterThanOrEqualToConstant_(size as _)
+ .setActive_(runtime::YES);
+ }
+ }
+ }
+
+ if !self.ignore_vertical {
+ unsafe {
+ let ta = view.topAnchor();
+ let ba = view.bottomAnchor();
+ let pta = lmg.topAnchor();
+ let pba = lmg.bottomAnchor();
+ match style.vertical_alignment {
+ Alignment::Fill => {
+ constraint_equal(ta, pta);
+ constraint_equal(ba, pba);
+ // Set the autoresizing mask to be consistent with the horizontal settings
+ // (see the comment there as to why it's necessary).
+ view.setAutoresizingMask_(cocoa::NSViewHeightSizable);
+ }
+ Alignment::Start => {
+ constraint_equal(ta, pta);
+ }
+ Alignment::Center => {
+ let ca = view.centerYAnchor();
+ let pca = lmg.centerYAnchor();
+ constraint_equal(ca, pca);
+ }
+ Alignment::End => {
+ constraint_equal(ba, pba);
+ }
+ }
+ }
+ }
+
+ match &style.visible {
+ Property::Static(ref v) => {
+ unsafe { view.setHidden_((!v).into()) };
+ }
+ Property::Binding(b) => {
+ b.on_change(move |&visible| unsafe {
+ view.setHidden_((!visible).into());
+ });
+ unsafe { view.setHidden_((!*b.borrow()).into()) };
+ }
+ Property::ReadOnly(_) => {
+ unimplemented!("ElementStyle::visible doesn't support ReadOnly")
+ }
+ }
+
+ if let Ok(control) = cocoa::NSControl::try_from(view) {
+ match &style.enabled {
+ Property::Static(e) => {
+ unsafe { control.setEnabled_((*e).into()) };
+ }
+ Property::Binding(b) => {
+ b.on_change(move |&enabled| unsafe {
+ control.setEnabled_(enabled.into());
+ });
+ unsafe { control.setEnabled_((*b.borrow()).into()) };
+ }
+ Property::ReadOnly(_) => {
+ unimplemented!("ElementStyle::enabled doesn't support ReadOnly")
+ }
+ }
+ } else if let Ok(text) = cocoa::NSText::try_from(view) {
+ let normally_editable = unsafe { text.isEditable() } == runtime::YES;
+ let normally_selectable = unsafe { text.isSelectable() } == runtime::YES;
+ let set_enabled = move |enabled: bool| unsafe {
+ if !enabled {
+ let mut range = text.selectedRange();
+ range.length = 0;
+ text.setSelectedRange_(range);
+ }
+ text.setEditable_((enabled && normally_editable).into());
+ text.setSelectable_((enabled && normally_selectable).into());
+ text.setBackgroundColor_(if enabled {
+ cocoa::NSColor::textBackgroundColor()
+ } else {
+ cocoa::NSColor::windowBackgroundColor()
+ });
+ text.setTextColor_(if enabled {
+ cocoa::NSColor::textColor()
+ } else {
+ cocoa::NSColor::disabledControlTextColor()
+ });
+ };
+ match &style.enabled {
+ Property::Static(e) => set_enabled(*e),
+ Property::Binding(b) => {
+ b.on_change(move |&enabled| set_enabled(enabled));
+ set_enabled(*b.borrow());
+ }
+ Property::ReadOnly(_) => {
+ unimplemented!("ElementStyle::enabled doesn't support ReadOnly")
+ }
+ }
+ }
+
+ unsafe { view.setNeedsDisplay_(runtime::YES) };
+
+ true
+ }
+}
+
+fn render_element(
+ element_type: model::ElementType,
+ style: &model::ElementStyle,
+ rtl: bool,
+) -> Option<cocoa::NSView> {
+ use model::ElementType::*;
+ Some(match element_type {
+ VBox(model::VBox { items, spacing }) => {
+ let sv = unsafe { StrongRef::new(cocoa::NSStackView::alloc()) }.autorelease();
+ unsafe {
+ sv.init();
+ sv.setOrientation_(cocoa::NSUserInterfaceLayoutOrientationVertical);
+ sv.setAlignment_(cocoa::NSLayoutAttributeLeading);
+ sv.setSpacing_(spacing as _);
+ if style.vertical_alignment != Alignment::Fill {
+ // Make sure the vbox stays as small as its content.
+ sv.setHuggingPriority_forOrientation_(
+ NSLayoutPriorityDefaultHigh,
+ cocoa::NSLayoutConstraintOrientationVertical,
+ );
+ }
+ }
+ let renderer = ViewRenderer::new(rtl, sv, |parent, style, child| {
+ let gravity: cocoa::NSInteger = match style.vertical_alignment {
+ Alignment::Start | Alignment::Fill => 1,
+ Alignment::Center => 2,
+ Alignment::End => 3,
+ };
+ let parent: cocoa::NSStackView = parent.try_into().unwrap();
+ unsafe { parent.addView_inGravity_(child, gravity) };
+ })
+ .ignore_vertical(true);
+ for item in items {
+ renderer.render(item);
+ }
+ sv.into()
+ }
+ HBox(model::HBox {
+ mut items,
+ spacing,
+ affirmative_order,
+ }) => {
+ if affirmative_order {
+ items.reverse();
+ }
+ let sv = unsafe { StrongRef::new(cocoa::NSStackView::alloc()) }.autorelease();
+ unsafe {
+ sv.init();
+ sv.setOrientation_(cocoa::NSUserInterfaceLayoutOrientationHorizontal);
+ sv.setAlignment_(cocoa::NSLayoutAttributeTop);
+ sv.setSpacing_(spacing as _);
+ if style.horizontal_alignment != Alignment::Fill {
+ // Make sure the hbox stays as small as its content.
+ sv.setHuggingPriority_forOrientation_(
+ NSLayoutPriorityDefaultHigh,
+ cocoa::NSLayoutConstraintOrientationHorizontal,
+ );
+ }
+ }
+ let renderer = ViewRenderer::new(rtl, sv, |parent, style, child| {
+ let gravity: cocoa::NSInteger = match style.horizontal_alignment {
+ Alignment::Start | Alignment::Fill => 1,
+ Alignment::Center => 2,
+ Alignment::End => 3,
+ };
+ let parent: cocoa::NSStackView = parent.try_into().unwrap();
+ unsafe { parent.addView_inGravity_(child, gravity) };
+ })
+ .ignore_horizontal(true);
+ for item in items {
+ renderer.render(item);
+ }
+ sv.into()
+ }
+ Button(mut b) => {
+ if let Some(Label(model::Label {
+ text: Property::Static(text),
+ ..
+ })) = b.content.take().map(|e| e.element_type)
+ {
+ let button = self::Button { element: b }.with_title(text.as_str());
+ button.into()
+ } else {
+ return None;
+ }
+ }
+ Checkbox(cb) => {
+ let button = self::Checkbox { element: cb }.into_button();
+ button.into()
+ }
+ Label(model::Label { text, bold }) => {
+ let tf = cocoa::NSTextField(unsafe {
+ cocoa::NSTextField::wrappingLabelWithString_(nsstring(""))
+ });
+ unsafe { tf.setSelectable_(runtime::NO) };
+ if bold {
+ unsafe { tf.setFont_(cocoa::NSFont::boldSystemFontOfSize_(0.0)) };
+ }
+ match text {
+ Property::Static(text) => {
+ unsafe { tf.setStringValue_(nsstring(text.as_str())) };
+ }
+ Property::Binding(b) => {
+ unsafe { tf.setStringValue_(nsstring(b.borrow().as_str())) };
+ b.on_change(move |s| unsafe { tf.setStringValue_(nsstring(s)) });
+ }
+ Property::ReadOnly(_) => unimplemented!("ReadOnly not supported for Label::text"),
+ }
+ tf.into()
+ }
+ Progress(model::Progress { amount }) => {
+ fn update(progress: cocoa::NSProgressIndicator, value: Option<f32>) {
+ unsafe {
+ match value {
+ None => {
+ progress.setIndeterminate_(runtime::YES);
+ progress.startAnimation_(progress.0);
+ }
+ Some(v) => {
+ progress.setDoubleValue_(v as f64);
+ progress.setIndeterminate_(runtime::NO);
+ }
+ }
+ }
+ }
+
+ let progress = unsafe { StrongRef::new(cocoa::NSProgressIndicator::alloc()) };
+ unsafe {
+ progress.init();
+ progress.setMinValue_(0.0);
+ progress.setMaxValue_(1.0);
+ }
+ match amount {
+ Property::Static(v) => update(*progress, v),
+ Property::Binding(s) => {
+ update(*progress, *s.borrow());
+ let weak = progress.weak();
+ s.on_change(move |v| {
+ if let Some(r) = weak.lock() {
+ update(*r, *v);
+ }
+ });
+ }
+ Property::ReadOnly(_) => (),
+ }
+ progress.autorelease().into()
+ }
+ Scroll(model::Scroll { content }) => {
+ let sv = unsafe { StrongRef::new(cocoa::NSScrollView::alloc()) }.autorelease();
+ unsafe {
+ sv.init();
+ sv.setHasVerticalScroller_(runtime::YES);
+ }
+ if let Some(content) = content {
+ ViewRenderer::new_with_selector(rtl, sv, sel!(setDocumentView:))
+ .ignore_vertical(true)
+ .render(*content);
+ }
+ sv.into()
+ }
+ TextBox(model::TextBox {
+ placeholder,
+ content,
+ editable,
+ }) => {
+ let tv: StrongRef<cocoa::NSTextView> = TextView.into_object().cast();
+ unsafe {
+ tv.setEditable_(editable.into());
+
+ cocoa::NSTextView_NSSharing::setAllowsUndo_(&*tv, runtime::YES);
+ tv.setVerticallyResizable_(runtime::YES);
+ if rtl {
+ let ps = StrongRef::new(cocoa::NSMutableParagraphStyle::alloc());
+ ps.init();
+ ps.setAlignment_(cocoa::NSTextAlignmentRight);
+ // We don't `use cocoa::NSTextView_NSSharing` because it has some methods which
+ // conflict with others that make it inconvenient.
+ cocoa::NSTextView_NSSharing::setDefaultParagraphStyle_(&*tv, (*ps).into());
+ }
+ {
+ let container = tv.textContainer();
+ container.setSize_(cocoa::NSSize {
+ width: f64::MAX,
+ height: f64::MAX,
+ });
+ container.setWidthTracksTextView_(runtime::YES);
+ }
+ if let Some(placeholder) = placeholder {
+ // It's unclear why dictionaryWithObject_forKey_ takes `u64` rather than `id`
+ // arguments.
+ let attrs = cocoa::NSDictionary(
+ <cocoa::NSDictionary as NSDictionary_NSDictionaryCreation<
+ cocoa::NSAttributedStringKey,
+ cocoa::id,
+ >>::dictionaryWithObject_forKey_(
+ cocoa::NSColor::placeholderTextColor().0 as u64,
+ cocoa::NSForegroundColorAttributeName.0 as u64,
+ ),
+ );
+ let string = StrongRef::new(cocoa::NSAttributedString(
+ cocoa::NSAttributedString::alloc()
+ .initWithString_attributes_(nsstring(placeholder.as_str()), attrs),
+ ));
+ // XXX: `setPlaceholderAttributedString` is undocumented (discovered at
+ // https://stackoverflow.com/a/43028577 and works identically to NSTextField),
+ // though hopefully it will be exposed in a public API some day.
+ tv.performSelector_withObject_(sel!(setPlaceholderAttributedString:), string.0);
+ }
+ }
+ match content {
+ Property::Static(s) => unsafe { tv.setString_(nsstring(s.as_str())) },
+ Property::ReadOnly(od) => {
+ let weak = tv.weak();
+ od.register(move |s| {
+ if let Some(tv) = weak.lock() {
+ *s = read_nsstring(unsafe { tv.string() });
+ }
+ });
+ }
+ Property::Binding(b) => {
+ let weak = tv.weak();
+ b.on_change(move |s| {
+ if let Some(tv) = weak.lock() {
+ unsafe { tv.setString_(nsstring(s.as_str())) };
+ }
+ });
+ unsafe { tv.setString_(nsstring(b.borrow().as_str())) };
+ }
+ }
+ tv.autorelease().into()
+ }
+ })
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/macos/objc.rs b/toolkit/crashreporter/client/app/src/ui/macos/objc.rs
new file mode 100644
index 0000000000..d4f3f1c419
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/macos/objc.rs
@@ -0,0 +1,242 @@
+/* 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/. */
+
+//! Objective-C bindings and helpers.
+
+// Forward all exports from the `objc` crate.
+pub use objc::*;
+
+/// An objc class instance which contains rust data `T`.
+#[derive(Clone, Copy)]
+#[repr(transparent)]
+pub struct Objc<T> {
+ pub instance: cocoa::id,
+ _phantom: std::marker::PhantomData<*mut T>,
+}
+
+impl<T> Objc<T> {
+ pub fn new(instance: cocoa::id) -> Self {
+ Objc {
+ instance,
+ _phantom: std::marker::PhantomData,
+ }
+ }
+
+ pub fn data(&self) -> &T {
+ let data = *unsafe { (*self.instance).get_ivar::<usize>("rust_self") } as *mut T;
+ unsafe { &*data }
+ }
+
+ pub fn data_mut(&mut self) -> &mut T {
+ let data = *unsafe { (*self.instance).get_ivar::<usize>("rust_self") } as *mut T;
+ unsafe { &mut *data }
+ }
+}
+
+impl<T> std::ops::Deref for Objc<T> {
+ type Target = T;
+ fn deref(&self) -> &Self::Target {
+ self.data()
+ }
+}
+
+impl<T> std::ops::DerefMut for Objc<T> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ self.data_mut()
+ }
+}
+
+unsafe impl<T> Encode for Objc<T> {
+ fn encode() -> Encoding {
+ cocoa::id::encode()
+ }
+}
+
+/// Wrapper to provide `Encode` for bindgen-generated types (bindgen should do this in the future).
+#[repr(transparent)]
+pub struct Ptr<T>(pub T);
+
+unsafe impl<T> Encode for Ptr<T> {
+ fn encode() -> Encoding {
+ cocoa::id::encode()
+ }
+}
+
+/// A strong objective-c reference to `T`.
+#[repr(transparent)]
+pub struct StrongRef<T> {
+ ptr: rc::StrongPtr,
+ _phantom: std::marker::PhantomData<T>,
+}
+
+impl<T> Clone for StrongRef<T> {
+ fn clone(&self) -> Self {
+ StrongRef {
+ ptr: self.ptr.clone(),
+ _phantom: self._phantom,
+ }
+ }
+}
+
+impl<T> std::ops::Deref for StrongRef<T> {
+ type Target = T;
+ fn deref(&self) -> &Self::Target {
+ let obj: &cocoa::id = &*self.ptr;
+ unsafe { std::mem::transmute(obj) }
+ }
+}
+
+impl<T> StrongRef<T> {
+ /// Assume the given pointer-wrapper is an already-retained strong reference.
+ ///
+ /// # Safety
+ /// The type _must_ be the same size as cocoa::id and contain only a cocoa::id.
+ pub unsafe fn new(v: T) -> Self {
+ std::mem::transmute_copy(&v)
+ }
+
+ /// Retain the given pointer-wrapper.
+ ///
+ /// # Safety
+ /// The type _must_ be the same size as cocoa::id and contain only a cocoa::id.
+ #[allow(dead_code)]
+ pub unsafe fn retain(v: T) -> Self {
+ let obj: cocoa::id = std::mem::transmute_copy(&v);
+ StrongRef {
+ ptr: rc::StrongPtr::retain(obj),
+ _phantom: std::marker::PhantomData,
+ }
+ }
+
+ pub fn autorelease(self) -> T {
+ let obj = self.ptr.autorelease();
+ unsafe { std::mem::transmute_copy(&obj) }
+ }
+
+ pub fn weak(&self) -> WeakRef<T> {
+ WeakRef {
+ ptr: self.ptr.weak(),
+ _phantom: std::marker::PhantomData,
+ }
+ }
+
+ /// Unwrap the StrongRef value without affecting reference counts.
+ ///
+ /// This is the opposite of `new`.
+ #[allow(dead_code)]
+ pub fn unwrap(self: Self) -> T {
+ let v = unsafe { std::mem::transmute_copy(&self) };
+ std::mem::forget(self);
+ v
+ }
+
+ /// Cast to a base class.
+ ///
+ /// Bindgen pointer-wrappers have trival `From<Derived> for Base` implementations.
+ pub fn cast<U: From<T>>(self) -> StrongRef<U> {
+ StrongRef {
+ ptr: self.ptr,
+ _phantom: std::marker::PhantomData,
+ }
+ }
+}
+
+/// A weak objective-c reference to `T`.
+#[derive(Clone)]
+#[repr(transparent)]
+pub struct WeakRef<T> {
+ ptr: rc::WeakPtr,
+ _phantom: std::marker::PhantomData<T>,
+}
+
+impl<T> WeakRef<T> {
+ pub fn lock(&self) -> Option<StrongRef<T>> {
+ let ptr = self.ptr.load();
+ if ptr.is_null() {
+ None
+ } else {
+ Some(StrongRef {
+ ptr,
+ _phantom: std::marker::PhantomData,
+ })
+ }
+ }
+}
+
+/// A macro for creating an objc class.
+///
+/// Classes _must_ be registered before use (`Objc<T>::register()`).
+///
+/// Example:
+/// ```
+/// struct Foo(u8);
+///
+/// objc_class! {
+/// impl Foo: NSObject {
+/// #[sel(mySelector:)]
+/// fn my_selector(&mut self, arg: u8) -> u8 {
+/// self.0 + arg
+/// }
+/// }
+/// }
+///
+/// fn make_foo() -> StrongRef<Objc<Foo>> {
+/// Foo(42).into_object()
+/// }
+/// ```
+///
+/// Call `T::into_object()` to create the objective-c class instance.
+macro_rules! objc_class {
+ ( impl $name:ident : $base:ident $(<$($protocol:ident),+>)? {
+ $(
+ #[sel($($sel:tt)+)]
+ fn $mname:ident (&mut $self:ident $(, $argname:ident : $argtype:ty )*) $(-> $rettype:ty)? $body:block
+ )*
+ }) => {
+ impl Objc<$name> {
+ pub fn register() {
+ let mut decl = declare::ClassDecl::new(concat!("CR", stringify!($name)), class!($base)).expect(concat!("failed to declare ", stringify!($name), " class"));
+ $($(decl.add_protocol(runtime::Protocol::get(stringify!($protocol)).expect(concat!("failed to find ",stringify!($protocol)," protocol")));)+)?
+ decl.add_ivar::<usize>("rust_self");
+ $({
+ extern fn method_impl(obj: &mut runtime::Object, _: runtime::Sel $(, $argname: $argtype )*) $(-> $rettype)? {
+ Objc::<$name>::new(obj).$mname($($argname),*)
+ }
+ unsafe {
+ decl.add_method(sel!($($sel)+), method_impl as extern fn(&mut runtime::Object, runtime::Sel $(, $argname: $argtype )*) $(-> $rettype)?);
+ }
+ })*
+ {
+ extern fn dealloc_impl(obj: &runtime::Object, _: runtime::Sel) {
+ drop(unsafe { Box::from_raw(*obj.get_ivar::<usize>("rust_self") as *mut $name) });
+ unsafe {
+ let _: () = msg_send![super(obj, class!(NSObject)), dealloc];
+ }
+ }
+ unsafe {
+ decl.add_method(sel!(dealloc), dealloc_impl as extern fn(&runtime::Object, runtime::Sel));
+ }
+ }
+ decl.register();
+ }
+
+ pub fn class() -> &'static runtime::Class {
+ runtime::Class::get(concat!("CR", stringify!($name))).expect("class not registered")
+ }
+
+ $(fn $mname (&mut $self $(, $argname : $argtype )*) $(-> $rettype)? $body)*
+ }
+
+ impl $name {
+ pub fn into_object(self) -> StrongRef<Objc<$name>> {
+ let obj: *mut runtime::Object = unsafe { msg_send![Objc::<Self>::class(), alloc] };
+ unsafe { (*obj).set_ivar("rust_self", Box::into_raw(Box::new(self)) as usize) };
+ let obj: *mut runtime::Object = unsafe { msg_send![obj, init] };
+ unsafe { StrongRef::new(Objc::new(obj)) }
+ }
+ }
+ }
+}
+
+pub(crate) use objc_class;
diff --git a/toolkit/crashreporter/client/app/src/ui/macos/plist.rs b/toolkit/crashreporter/client/app/src/ui/macos/plist.rs
new file mode 100644
index 0000000000..a5bbe0aa0a
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/macos/plist.rs
@@ -0,0 +1,44 @@
+/* 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 embedded Info.plist file.
+
+const DATA: &[u8] = br#"<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>English</string>
+ <key>CFBundleDisplayName</key>
+ <string>Crash Reporter</string>
+ <key>CFBundleExecutable</key>
+ <string>crashreporter</string>
+ <key>CFBundleIdentifier</key>
+ <string>org.mozilla.crashreporter</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>Crash Reporter</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleVersion</key>
+ <string>1.0</string>
+ <key>LSHasLocalizedDisplayName</key>
+ <true/>
+ <key>NSRequiresAquaSystemAppearance</key>
+ <false/>
+ <key>NSPrincipalClass</key>
+ <string>NSApplication</string>
+</dict>
+</plist>"#;
+
+const N: usize = DATA.len();
+
+const PTR: *const [u8; N] = DATA.as_ptr() as *const [u8; N];
+
+#[used]
+#[link_section = "__TEXT,__info_plist"]
+// # Safety
+// The array pointer is created from `DATA` (a slice pointer) with `DATA.len()` as the length.
+static PLIST: [u8; N] = unsafe { *PTR };
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(),
+ });
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/model/button.rs b/toolkit/crashreporter/client/app/src/ui/model/button.rs
new file mode 100644
index 0000000000..d522fad6fc
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/model/button.rs
@@ -0,0 +1,26 @@
+/* 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/. */
+
+use super::{Element, ElementBuilder};
+use crate::data::Event;
+
+/// A clickable button.
+#[derive(Default, Debug)]
+pub struct Button {
+ pub content: Option<Box<Element>>,
+ pub click: Event<()>,
+}
+
+impl ElementBuilder<Button> {
+ pub fn on_click<F>(&mut self, f: F)
+ where
+ F: Fn() + 'static,
+ {
+ self.element_type.click.subscribe(move |_| f());
+ }
+
+ pub fn add_child(&mut self, child: Element) {
+ Self::single_child(&mut self.element_type.content, child);
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/model/checkbox.rs b/toolkit/crashreporter/client/app/src/ui/model/checkbox.rs
new file mode 100644
index 0000000000..8923e33558
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/model/checkbox.rs
@@ -0,0 +1,22 @@
+/* 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/. */
+
+use crate::data::Property;
+
+/// A checkbox (with optional label).
+#[derive(Default, Debug)]
+pub struct Checkbox {
+ pub checked: Property<bool>,
+ pub label: Option<String>,
+}
+
+impl super::ElementBuilder<Checkbox> {
+ pub fn checked(&mut self, value: impl Into<Property<bool>>) {
+ self.element_type.checked = value.into();
+ }
+
+ pub fn label<S: Into<String>>(&mut self, label: S) {
+ self.element_type.label = Some(label.into());
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/model/hbox.rs b/toolkit/crashreporter/client/app/src/ui/model/hbox.rs
new file mode 100644
index 0000000000..b6c0e27e8c
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/model/hbox.rs
@@ -0,0 +1,34 @@
+/* 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/. */
+
+use super::{Element, ElementBuilder};
+
+/// A box which lays out contents horizontally.
+#[derive(Default, Debug)]
+pub struct HBox {
+ pub items: Vec<Element>,
+ pub spacing: u32,
+ pub affirmative_order: bool,
+}
+
+impl ElementBuilder<HBox> {
+ pub fn spacing(&mut self, value: u32) {
+ self.element_type.spacing = value;
+ }
+
+ /// Whether children are in affirmative order (and should be reordered based on platform
+ /// conventions).
+ ///
+ /// The children passed to `add_child` should be in most-affirmative to least-affirmative order
+ /// (e.g., "OK" then "Cancel" buttons).
+ ///
+ /// This is mainly useful for dialog buttons.
+ pub fn affirmative_order(&mut self, value: bool) {
+ self.element_type.affirmative_order = value;
+ }
+
+ pub fn add_child(&mut self, child: Element) {
+ self.element_type.items.push(child);
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/model/label.rs b/toolkit/crashreporter/client/app/src/ui/model/label.rs
new file mode 100644
index 0000000000..096ce022e3
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/model/label.rs
@@ -0,0 +1,22 @@
+/* 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/. */
+
+use crate::data::Property;
+
+/// A text label.
+#[derive(Debug, Default)]
+pub struct Label {
+ pub text: Property<String>,
+ pub bold: bool,
+}
+
+impl super::ElementBuilder<Label> {
+ pub fn text(&mut self, s: impl Into<Property<String>>) {
+ self.element_type.text = s.into();
+ }
+
+ pub fn bold(&mut self, value: bool) {
+ self.element_type.bold = value;
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/model/mod.rs b/toolkit/crashreporter/client/app/src/ui/model/mod.rs
new file mode 100644
index 0000000000..5ea2ddc59a
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/model/mod.rs
@@ -0,0 +1,344 @@
+/* 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.
+//!
+//! Model elements should generally be declared as types with all fields `pub` (to be accessed by
+//! UI implementations), though accessor methods are acceptable if needed. An
+//! `ElementBuilder<TYPE>` impl should be provided to create methods that will be used in the
+//! [`ui!`] macro. The model types are accessible when being _consumed_ by a UI implementation,
+//! whereas the `ElementBuilder` types are accessible when the model is being _created_.
+//!
+//! All elements should be listed in the `element_types!` macro in this file (note that [`Window`],
+//! while an element, isn't listed here as it cannot be a child element). This populates the
+//! `ElementType` enum and generates `From<Element>` for `ElementType`, and `TryFrom<ElementType>`
+//! for the element (as well as reference `TryFrom`).
+//!
+//! The model is written to accommodate layout and text direction differences (e.g. for RTL
+//! languages), and UI implementations are expected to account for this correctly.
+
+use crate::data::Property;
+pub use button::Button;
+pub use checkbox::Checkbox;
+pub use hbox::HBox;
+pub use label::Label;
+pub use progress::Progress;
+pub use scroll::Scroll;
+pub use textbox::TextBox;
+pub use vbox::VBox;
+pub use window::Window;
+
+mod button;
+mod checkbox;
+mod hbox;
+mod label;
+mod progress;
+mod scroll;
+mod textbox;
+mod vbox;
+mod window;
+
+/// A GUI element, including general style attributes and a more specific type.
+///
+/// `From<ElementBuilder<...>>` is implemented for all elements listed in `element_types!`.
+#[derive(Debug)]
+pub struct Element {
+ pub style: ElementStyle,
+ pub element_type: ElementType,
+}
+
+// This macro creates the `ElementType` enum and corresponding `From<ElementBuilder>` impls for
+// Element. The `ElementType` discriminants match the element type names.
+macro_rules! element_types {
+ ( $($name:ident),* ) => {
+ /// A type of GUI element.
+ #[derive(Debug)]
+ pub enum ElementType {
+ $($name($name)),*
+ }
+
+ $(
+ impl From<$name> for ElementType {
+ fn from(e: $name) -> ElementType {
+ ElementType::$name(e)
+ }
+ }
+
+ impl TryFrom<ElementType> for $name {
+ type Error = &'static str;
+
+ fn try_from(et: ElementType) -> Result<Self, Self::Error> {
+ if let ElementType::$name(v) = et {
+ Ok(v)
+ } else {
+ Err(concat!("ElementType was not ", stringify!($name)))
+ }
+ }
+ }
+
+ impl<'a> TryFrom<&'a ElementType> for &'a $name {
+ type Error = &'static str;
+
+ fn try_from(et: &'a ElementType) -> Result<Self, Self::Error> {
+ if let ElementType::$name(v) = et {
+ Ok(v)
+ } else {
+ Err(concat!("ElementType was not ", stringify!($name)))
+ }
+ }
+ }
+
+ impl From<ElementBuilder<$name>> for Element {
+ fn from(b: ElementBuilder<$name>) -> Self {
+ Element {
+ style: b.style,
+ element_type: b.element_type.into(),
+ }
+ }
+ }
+ )*
+ }
+}
+element_types! {
+ Button, Checkbox, HBox, Label, Progress, Scroll, TextBox, VBox
+}
+
+/// Common element style values.
+#[derive(Debug)]
+pub struct ElementStyle {
+ pub horizontal_alignment: Alignment,
+ pub vertical_alignment: Alignment,
+ pub horizontal_size_request: Option<u32>,
+ pub vertical_size_request: Option<u32>,
+ pub margin: Margin,
+ pub visible: Property<bool>,
+ pub enabled: Property<bool>,
+ #[cfg(test)]
+ pub id: Option<String>,
+}
+
+impl Default for ElementStyle {
+ fn default() -> Self {
+ ElementStyle {
+ horizontal_alignment: Default::default(),
+ vertical_alignment: Default::default(),
+ horizontal_size_request: Default::default(),
+ vertical_size_request: Default::default(),
+ margin: Default::default(),
+ visible: true.into(),
+ enabled: true.into(),
+ #[cfg(test)]
+ id: Default::default(),
+ }
+ }
+}
+
+/// A builder for `Element`s.
+///
+/// Each element should add an `impl ElementBuilder<TYPE>` to add methods to their builder.
+#[derive(Debug, Default)]
+pub struct ElementBuilder<T> {
+ pub style: ElementStyle,
+ pub element_type: T,
+}
+
+impl<T> ElementBuilder<T> {
+ /// Set horizontal alignment.
+ pub fn halign(&mut self, alignment: Alignment) {
+ self.style.horizontal_alignment = alignment;
+ }
+
+ /// Set vertical alignment.
+ pub fn valign(&mut self, alignment: Alignment) {
+ self.style.vertical_alignment = alignment;
+ }
+
+ /// Set the horizontal size request.
+ pub fn hsize(&mut self, value: u32) {
+ assert!(value <= i32::MAX as u32);
+ self.style.horizontal_size_request = Some(value);
+ }
+
+ /// Set the vertical size request.
+ pub fn vsize(&mut self, value: u32) {
+ assert!(value <= i32::MAX as u32);
+ self.style.vertical_size_request = Some(value);
+ }
+
+ /// Set start margin.
+ pub fn margin_start(&mut self, amount: u32) {
+ self.style.margin.start = amount;
+ }
+
+ /// Set end margin.
+ pub fn margin_end(&mut self, amount: u32) {
+ self.style.margin.end = amount;
+ }
+
+ /// Set start and end margins.
+ pub fn margin_horizontal(&mut self, amount: u32) {
+ self.margin_start(amount);
+ self.margin_end(amount)
+ }
+
+ /// Set top margin.
+ pub fn margin_top(&mut self, amount: u32) {
+ self.style.margin.top = amount;
+ }
+
+ /// Set bottom margin.
+ pub fn margin_bottom(&mut self, amount: u32) {
+ self.style.margin.bottom = amount;
+ }
+
+ /// Set top and bottom margins.
+ pub fn margin_vertical(&mut self, amount: u32) {
+ self.margin_top(amount);
+ self.margin_bottom(amount)
+ }
+
+ /// Set all margins.
+ pub fn margin(&mut self, amount: u32) {
+ self.margin_horizontal(amount);
+ self.margin_vertical(amount)
+ }
+
+ /// Set visibility.
+ pub fn visible(&mut self, value: impl Into<Property<bool>>) {
+ self.style.visible = value.into();
+ }
+
+ /// Set whether an element is enabled.
+ ///
+ /// This generally should enable/disable interaction with an element.
+ pub fn enabled(&mut self, value: impl Into<Property<bool>>) {
+ self.style.enabled = value.into();
+ }
+
+ /// Set the element identifier.
+ #[cfg(test)]
+ pub fn id(&mut self, value: impl Into<String>) {
+ self.style.id = Some(value.into());
+ }
+
+ /// Set the element identifier (stub).
+ #[cfg(not(test))]
+ pub fn id(&mut self, _value: impl Into<String>) {}
+
+ fn single_child(slot: &mut Option<Box<Element>>, child: Element) {
+ if slot.replace(Box::new(child)).is_some() {
+ panic!("{} can only have one child", std::any::type_name::<T>());
+ }
+ }
+}
+
+/// A typed [`Element`].
+///
+/// This is useful for the [`ui!`] macro when a method should accept a specific element type, since
+/// the macro always creates [`ElementBuilder<T>`](ElementBuilder) and ends with a `.into()` (and this implements
+/// `From<ElementBuilder<T>>`).
+#[derive(Debug, Default)]
+pub struct TypedElement<T> {
+ pub style: ElementStyle,
+ pub element_type: T,
+}
+
+impl<T> From<ElementBuilder<T>> for TypedElement<T> {
+ fn from(b: ElementBuilder<T>) -> Self {
+ TypedElement {
+ style: b.style,
+ element_type: b.element_type,
+ }
+ }
+}
+
+/// The alignment of an element in one direction.
+///
+/// Note that rather than `Left`/`Right`, this class has `Start`/`End` as it is meant to be
+/// layout-direction-aware.
+#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
+#[allow(dead_code)]
+pub enum Alignment {
+ /// Align to the start of the direction.
+ #[default]
+ Start,
+ /// Align to the center of the direction.
+ Center,
+ /// Align to the end of the direction.
+ End,
+ /// Fill all available space.
+ Fill,
+}
+
+/// The margins of an element.
+///
+/// These are RTL-aware: for instance, `start` is the left margin in left-to-right languages and
+/// the right margin in right-to-left languages.
+#[derive(Default, Debug)]
+pub struct Margin {
+ pub start: u32,
+ pub end: u32,
+ pub top: u32,
+ pub bottom: u32,
+}
+
+/// A macro to allow a convenient syntax for creating elements.
+///
+/// The macro expects the following syntax:
+/// ```
+/// ElementTypeName some_method(arg1, arg2) other_method() {
+/// Child ...,
+/// Child2 ...
+/// }
+/// ```
+///
+/// The type is wrapped in an `ElementBuilder`, and methods are called on this builder with a
+/// mutable reference. This means that element types must implement Default and must implement
+/// builder methods on `ElementBuilder<ElementTypeName>`. The children block is optional, and calls
+/// `add_child(child: Element)` for each provided child (so implement this method if desired).
+///
+/// For testing, a string identifier can be set on any element with a `["my_identifier"]` following
+/// the element type.
+macro_rules! ui {
+ ( $el:ident
+ $([ $id:literal ])?
+ $( $method:ident $methodargs:tt )*
+ $({ $($contents:tt)* })?
+ ) => {
+ {
+ #[allow(unused_imports)]
+ use $crate::ui::model::*;
+ let mut el: ElementBuilder<$el> = Default::default();
+ $( el.id($id); )?
+ $( el.$method $methodargs ; )*
+ $( ui! { @children (el) $($contents)* } )?
+ el.into()
+ }
+ };
+ ( @children ($parent:expr) ) => {};
+ ( @children ($parent:expr)
+ $el:ident
+ $([ $id:literal ])?
+ $( $method:ident $methodargs:tt )*
+ $({ $($contents:tt)* })?
+ $(, $($rest:tt)* )?
+ ) => {
+ $parent.add_child(ui!( $el $([$id])? $( $method $methodargs )* $({ $($contents)* })? ));
+ $(ui!( @children ($parent) $($rest)* ))?
+ };
+}
+
+pub(crate) use ui;
+
+/// An application, defined as a set of windows.
+///
+/// When all windows are closed, the application is considered complete (and loops should exit).
+pub struct Application {
+ pub windows: Vec<TypedElement<Window>>,
+ /// Whether the text direction should be right-to-left.
+ pub rtl: bool,
+}
+
+/// A function to be invoked in the UI loop.
+pub type InvokeFn = Box<dyn FnOnce() + Send + 'static>;
diff --git a/toolkit/crashreporter/client/app/src/ui/model/progress.rs b/toolkit/crashreporter/client/app/src/ui/model/progress.rs
new file mode 100644
index 0000000000..f3e4e4bf77
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/model/progress.rs
@@ -0,0 +1,19 @@
+/* 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/. */
+
+use crate::data::Property;
+
+/// A progress indicator.
+#[derive(Debug, Default)]
+pub struct Progress {
+ /// Progress between 0 and 1, or None if indeterminate.
+ pub amount: Property<Option<f32>>,
+}
+
+impl super::ElementBuilder<Progress> {
+ #[allow(dead_code)]
+ pub fn amount(&mut self, value: impl Into<Property<Option<f32>>>) {
+ self.element_type.amount = value.into();
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/model/scroll.rs b/toolkit/crashreporter/client/app/src/ui/model/scroll.rs
new file mode 100644
index 0000000000..47efa4a81e
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/model/scroll.rs
@@ -0,0 +1,17 @@
+/* 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/. */
+
+use super::{Element, ElementBuilder};
+
+/// A scrollable region.
+#[derive(Debug, Default)]
+pub struct Scroll {
+ pub content: Option<Box<Element>>,
+}
+
+impl ElementBuilder<Scroll> {
+ pub fn add_child(&mut self, child: Element) {
+ Self::single_child(&mut self.element_type.content, child);
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/model/textbox.rs b/toolkit/crashreporter/client/app/src/ui/model/textbox.rs
new file mode 100644
index 0000000000..08cd9ca1bc
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/model/textbox.rs
@@ -0,0 +1,27 @@
+/* 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/. */
+
+use crate::data::Property;
+
+/// A text box.
+#[derive(Debug, Default)]
+pub struct TextBox {
+ pub placeholder: Option<String>,
+ pub content: Property<String>,
+ pub editable: bool,
+}
+
+impl super::ElementBuilder<TextBox> {
+ pub fn placeholder(&mut self, text: impl Into<String>) {
+ self.element_type.placeholder = Some(text.into());
+ }
+
+ pub fn content(&mut self, value: impl Into<Property<String>>) {
+ self.element_type.content = value.into();
+ }
+
+ pub fn editable(&mut self, value: bool) {
+ self.element_type.editable = value;
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/model/vbox.rs b/toolkit/crashreporter/client/app/src/ui/model/vbox.rs
new file mode 100644
index 0000000000..6f1b09b1e2
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/model/vbox.rs
@@ -0,0 +1,22 @@
+/* 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/. */
+
+use super::{Element, ElementBuilder};
+
+/// A box which lays out contents vertically.
+#[derive(Debug, Default)]
+pub struct VBox {
+ pub items: Vec<Element>,
+ pub spacing: u32,
+}
+
+impl ElementBuilder<VBox> {
+ pub fn spacing(&mut self, value: u32) {
+ self.element_type.spacing = value;
+ }
+
+ pub fn add_child(&mut self, child: Element) {
+ self.element_type.items.push(child);
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/model/window.rs b/toolkit/crashreporter/client/app/src/ui/model/window.rs
new file mode 100644
index 0000000000..b56071ca19
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/model/window.rs
@@ -0,0 +1,46 @@
+/* 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/. */
+
+use super::{Element, ElementBuilder, TypedElement};
+use crate::data::Event;
+
+/// A window.
+#[derive(Debug, Default)]
+pub struct Window {
+ pub title: String,
+ /// The window content is the first element.
+ pub content: Option<Box<Element>>,
+ /// Logical child windows.
+ pub children: Vec<TypedElement<Self>>,
+ pub modal: bool,
+ pub close: Option<Event<()>>,
+}
+
+impl ElementBuilder<Window> {
+ /// Set the window title.
+ pub fn title(&mut self, s: impl Into<String>) {
+ self.element_type.title = s.into();
+ }
+
+ /// Set whether the window is modal (blocking interaction with other windows when displayed).
+ pub fn modal(&mut self, value: bool) {
+ self.element_type.modal = value;
+ }
+
+ /// Register an event to close the window.
+ pub fn close_when(&mut self, event: &Event<()>) {
+ self.element_type.close = Some(event.clone());
+ }
+
+ /// Add a window as a logical child of this one.
+ ///
+ /// Logical children are always displayed above their parents.
+ pub fn child_window(&mut self, window: TypedElement<Window>) {
+ self.element_type.children.push(window);
+ }
+
+ pub fn add_child(&mut self, child: Element) {
+ Self::single_child(&mut self.element_type.content, child);
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/test.rs b/toolkit/crashreporter/client/app/src/ui/test.rs
new file mode 100644
index 0000000000..db98c072da
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/test.rs
@@ -0,0 +1,270 @@
+/* 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/. */
+
+//! A renderer for use in tests, which doesn't actually render a GUI but allows programmatic
+//! interaction.
+//!
+//! The [`ui!`](super::ui) macro supports labeling any element with a string identifier, which can
+//! be used to access the element in this UI.
+//!
+//! The [`Interact`] hook must be created to interact with the test UI, before the UI is run and on
+//! the same thread as the UI.
+//!
+//! See how this UI is used in [`crate::test`].
+
+use super::model::{self, Application, Element};
+use std::cell::RefCell;
+use std::collections::HashMap;
+use std::sync::{
+ atomic::{AtomicBool, AtomicU8, Ordering::Relaxed},
+ mpsc, Arc, Condvar, Mutex,
+};
+
+thread_local! {
+ static INTERACT: RefCell<Option<Arc<State>>> = Default::default();
+}
+
+/// A test UI which allows access to the UI elements.
+#[derive(Default)]
+pub struct UI {
+ interface: Mutex<Option<UIInterface>>,
+}
+
+impl UI {
+ pub fn run_loop(&self, app: Application) {
+ let (tx, rx) = mpsc::channel();
+ let interface = UIInterface { work: tx };
+
+ let elements = id_elements(&app);
+ INTERACT.with(cc! { (interface) move |r| {
+ if let Some(state) = &*r.borrow() {
+ state.set_interface(interface);
+ }
+ }});
+ *self.interface.lock().unwrap() = Some(interface.clone());
+
+ // Close the UI when the root windows are closed.
+ // Use a bitfield rather than a count in case the `close` event is fired multiple times.
+ assert!(app.windows.len() <= 8);
+ let mut windows = 0u8;
+ for i in 0..app.windows.len() {
+ windows |= 1 << i;
+ }
+ let windows = Arc::new(AtomicU8::new(windows));
+ for (index, window) in app.windows.iter().enumerate() {
+ if let Some(c) = &window.element_type.close {
+ c.subscribe(cc! { (interface, windows) move |&()| {
+ let old = windows
+ .fetch_update(Relaxed, Relaxed, |x| Some(x & !(1u8 << index)))
+ .unwrap();
+ if old == 1u8 << index {
+ interface.work.send(Command::Finish).unwrap();
+ }
+ }});
+ } else {
+ // No close event, so we must assume a closed state (and assume that _some_ window
+ // will have a close event registered so we don't drop the interface now).
+ windows
+ .fetch_update(Relaxed, Relaxed, |x| Some(x & !(1u8 << index)))
+ .unwrap();
+ }
+ }
+
+ while let Ok(f) = rx.recv() {
+ match f {
+ Command::Invoke(f) => f(),
+ Command::Interact(f) => f(&elements),
+ Command::Finish => break,
+ }
+ }
+
+ *self.interface.lock().unwrap() = None;
+ INTERACT.with(|r| {
+ if let Some(state) = &*r.borrow() {
+ state.clear_interface();
+ }
+ });
+ }
+
+ pub fn invoke(&self, f: model::InvokeFn) {
+ let guard = self.interface.lock().unwrap();
+ if let Some(interface) = &*guard {
+ let _ = interface.work.send(Command::Invoke(f));
+ }
+ }
+}
+
+/// Test interaction hook.
+#[derive(Clone)]
+pub struct Interact {
+ state: Arc<State>,
+}
+
+impl Interact {
+ /// Create an interaction hook for the test UI.
+ ///
+ /// This should be done before running the UI, and must be done on the same thread that
+ /// later runs it.
+ pub fn hook() -> Self {
+ let v = Interact {
+ state: Default::default(),
+ };
+ {
+ let state = v.state.clone();
+ INTERACT.with(move |r| *r.borrow_mut() = Some(state));
+ }
+ v
+ }
+
+ /// Wait for the render thread to be ready for interaction.
+ pub fn wait_for_ready(&self) {
+ let mut guard = self.state.interface.lock().unwrap();
+ while guard.is_none() && !self.state.cancel.load(Relaxed) {
+ guard = self.state.waiting_for_interface.wait(guard).unwrap();
+ }
+ }
+
+ /// Cancel an Interact (which causes `wait_for_ready` to always return).
+ pub fn cancel(&self) {
+ self.state.cancel.store(true, Relaxed);
+ self.state.waiting_for_interface.notify_all();
+ }
+
+ /// Run the given function on the element with the given type and identity.
+ ///
+ /// Panics if either the id is missing or the type is incorrect.
+ pub fn element<'a, 'b, T: 'b, F, R>(&self, id: &'a str, f: F) -> R
+ where
+ &'b T: TryFrom<&'b model::ElementType>,
+ F: FnOnce(&model::ElementStyle, &T) -> R + Send + 'a,
+ R: Send + 'a,
+ {
+ self.interact(id, move |element: &IdElement| match element {
+ IdElement::Generic(e) => Some(f(&e.style, (&e.element_type).try_into().ok()?)),
+ IdElement::Window(_) => None,
+ })
+ .expect("incorrect element type")
+ }
+
+ /// Run the given function on the window with the given identity.
+ ///
+ /// Panics if the id is missing or the type is incorrect.
+ pub fn window<'a, F, R>(&self, id: &'a str, f: F) -> R
+ where
+ F: FnOnce(&model::ElementStyle, &model::Window) -> R + Send + 'a,
+ R: Send + 'a,
+ {
+ self.interact(id, move |element| match element {
+ IdElement::Window(e) => Some(f(&e.style, &e.element_type)),
+ IdElement::Generic(_) => None,
+ })
+ .expect("incorrect element type")
+ }
+
+ fn interact<'a, 'b, F, R>(&self, id: &'a str, f: F) -> R
+ where
+ F: FnOnce(&IdElement<'b>) -> R + Send + 'a,
+ R: Send + 'a,
+ {
+ let (send, recv) = std::sync::mpsc::sync_channel(0);
+ {
+ let f: Box<dyn FnOnce(&IdElements<'b>) + Send + 'a> = Box::new(move |elements| {
+ let _ = send.send(elements.get(id).map(f));
+ });
+
+ // # Safety
+ // The function is run while `'a` is still valid (we wait here for it to complete).
+ let f: Box<dyn FnOnce(&IdElements) + Send + 'static> =
+ unsafe { std::mem::transmute(f) };
+
+ let guard = self.state.interface.lock().unwrap();
+ let interface = guard.as_ref().expect("renderer is not running");
+ let _ = interface.work.send(Command::Interact(f));
+ }
+ recv.recv().unwrap().expect("failed to get element")
+ }
+}
+
+#[derive(Clone)]
+struct UIInterface {
+ work: mpsc::Sender<Command>,
+}
+
+enum Command {
+ Invoke(Box<dyn FnOnce() + Send + 'static>),
+ Interact(Box<dyn FnOnce(&IdElements) + Send + 'static>),
+ Finish,
+}
+
+enum IdElement<'a> {
+ Generic(&'a Element),
+ Window(&'a model::TypedElement<model::Window>),
+}
+
+type IdElements<'a> = HashMap<String, IdElement<'a>>;
+
+#[derive(Default)]
+struct State {
+ interface: Mutex<Option<UIInterface>>,
+ waiting_for_interface: Condvar,
+ cancel: AtomicBool,
+}
+
+impl State {
+ /// Set the interface for the interaction client to use.
+ pub fn set_interface(&self, interface: UIInterface) {
+ *self.interface.lock().unwrap() = Some(interface);
+ self.waiting_for_interface.notify_all();
+ }
+
+ /// Clear the UI interface.
+ pub fn clear_interface(&self) {
+ *self.interface.lock().unwrap() = None;
+ }
+}
+
+fn id_elements<'a>(app: &'a Application) -> IdElements<'a> {
+ let mut elements: IdElements<'a> = Default::default();
+
+ let mut windows_to_visit: Vec<_> = app.windows.iter().collect();
+
+ let mut to_visit: Vec<&'a Element> = Vec::new();
+ while let Some(window) = windows_to_visit.pop() {
+ if let Some(id) = &window.style.id {
+ elements.insert(id.to_owned(), IdElement::Window(window));
+ }
+ windows_to_visit.extend(&window.element_type.children);
+
+ if let Some(content) = &window.element_type.content {
+ to_visit.push(content);
+ }
+ }
+
+ while let Some(el) = to_visit.pop() {
+ if let Some(id) = &el.style.id {
+ elements.insert(id.to_owned(), IdElement::Generic(el));
+ }
+
+ use model::ElementType::*;
+ match &el.element_type {
+ Button(model::Button {
+ content: Some(content),
+ ..
+ })
+ | Scroll(model::Scroll {
+ content: Some(content),
+ }) => {
+ to_visit.push(content);
+ }
+ VBox(model::VBox { items, .. }) | HBox(model::HBox { items, .. }) => {
+ for item in items {
+ to_visit.push(item)
+ }
+ }
+ _ => (),
+ }
+ }
+
+ elements
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/windows/font.rs b/toolkit/crashreporter/client/app/src/ui/windows/font.rs
new file mode 100644
index 0000000000..3ec48316eb
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/windows/font.rs
@@ -0,0 +1,56 @@
+/* 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/. */
+
+use windows_sys::Win32::{Foundation::S_OK, Graphics::Gdi, UI::Controls};
+
+/// Windows font handle (`HFONT`).
+pub struct Font(Gdi::HFONT);
+
+impl Font {
+ /// Get the system theme caption font.
+ ///
+ /// Panics if the font cannot be retrieved.
+ pub fn caption() -> Self {
+ unsafe {
+ let mut font = std::mem::zeroed::<Gdi::LOGFONTW>();
+ success!(hresult
+ Controls::GetThemeSysFont(0, Controls::TMT_CAPTIONFONT as i32, &mut font)
+ );
+ Font(success!(pointer Gdi::CreateFontIndirectW(&font)))
+ }
+ }
+
+ /// Get the system theme bold caption font.
+ ///
+ /// Returns `None` if the font cannot be retrieved.
+ pub fn caption_bold() -> Option<Self> {
+ unsafe {
+ let mut font = std::mem::zeroed::<Gdi::LOGFONTW>();
+ if Controls::GetThemeSysFont(0, Controls::TMT_CAPTIONFONT as i32, &mut font) != S_OK {
+ return None;
+ }
+ font.lfWeight = Gdi::FW_BOLD as i32;
+
+ let ptr = Gdi::CreateFontIndirectW(&font);
+ if ptr == 0 {
+ return None;
+ }
+ Some(Font(ptr))
+ }
+ }
+}
+
+impl std::ops::Deref for Font {
+ type Target = Gdi::HFONT;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl Drop for Font {
+ fn drop(&mut self) {
+ unsafe { Gdi::DeleteObject(self.0 as _) };
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/windows/gdi.rs b/toolkit/crashreporter/client/app/src/ui/windows/gdi.rs
new file mode 100644
index 0000000000..89828987bc
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/windows/gdi.rs
@@ -0,0 +1,43 @@
+/* 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/. */
+
+//! GDI helpers.
+
+use windows_sys::Win32::{
+ Foundation::HWND,
+ Graphics::Gdi::{self, GDI_ERROR, HDC, HGDIOBJ},
+};
+
+/// A GDI drawing context.
+pub struct DC {
+ hwnd: HWND,
+ hdc: HDC,
+}
+
+impl DC {
+ /// Create a new DC.
+ pub fn new(hwnd: HWND) -> Option<Self> {
+ let hdc = unsafe { Gdi::GetDC(hwnd) };
+ (hdc != 0).then_some(DC { hwnd, hdc })
+ }
+
+ /// Call the given function with a gdi object selected.
+ pub fn with_object_selected<R>(&self, object: HGDIOBJ, f: impl FnOnce(HDC) -> R) -> Option<R> {
+ let old_object = unsafe { Gdi::SelectObject(self.hdc, object) };
+ if old_object == 0 || old_object == GDI_ERROR as isize {
+ return None;
+ }
+ let ret = f(self.hdc);
+ // The prior object must be selected before releasing the DC. Ignore errors; this is
+ // best-effort.
+ unsafe { Gdi::SelectObject(self.hdc, old_object) };
+ Some(ret)
+ }
+}
+
+impl Drop for DC {
+ fn drop(&mut self) {
+ unsafe { Gdi::ReleaseDC(self.hwnd, self.hdc) };
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/windows/layout.rs b/toolkit/crashreporter/client/app/src/ui/windows/layout.rs
new file mode 100644
index 0000000000..7563b6b2f0
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/windows/layout.rs
@@ -0,0 +1,436 @@
+/* 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/. */
+
+//! Helpers for window layout.
+
+use super::{
+ model::{self, Alignment, Element, ElementStyle, Margin},
+ ElementRef, WideString,
+};
+use crate::data::Property;
+use std::collections::HashMap;
+use windows_sys::Win32::{
+ Foundation::{HWND, SIZE},
+ Graphics::Gdi,
+ UI::WindowsAndMessaging as win,
+};
+
+pub(super) type ElementMapping = HashMap<ElementRef, HWND>;
+
+/// Handles the layout of windows.
+///
+/// This is done in two passes. The first pass calculates the sizes for all elements in the tree.
+/// Once sizes are known, the second pass can appropriately position the elements (taking alignment
+/// into account).
+///
+/// Currently, the resize/reposition logic is tied into the methods here, which is an inconvenient
+/// design because when adding support for a new element type you have to add new information in
+/// disparate locations.
+pub struct Layout<'a> {
+ elements: &'a ElementMapping,
+ sizes: HashMap<ElementRef, Size>,
+ last_positioned: Option<HWND>,
+}
+
+// Unfortunately, there's no good way to get these margins. I just guessed and the first guesses
+// seemed to be close enough.
+const BUTTON_MARGIN: Margin = Margin {
+ start: 5,
+ end: 5,
+ top: 5,
+ bottom: 5,
+};
+const CHECKBOX_MARGIN: Margin = Margin {
+ start: 15,
+ end: 0,
+ top: 0,
+ bottom: 0,
+};
+
+impl<'a> Layout<'a> {
+ pub(super) fn new(elements: &'a ElementMapping) -> Self {
+ Layout {
+ elements,
+ sizes: Default::default(),
+ last_positioned: None,
+ }
+ }
+
+ /// Perform a layout of the element and all child elements.
+ pub fn layout(mut self, element: &Element, max_width: u32, max_height: u32) {
+ let max_size = Size {
+ width: max_width,
+ height: max_height,
+ };
+ self.resize(element, &max_size);
+ self.reposition(element, &Position::default(), &max_size);
+ }
+
+ fn resize(&mut self, element: &Element, max_size: &Size) -> Size {
+ let style = &element.style;
+
+ let mut inner_size = max_size.inner_size(style);
+ let mut content_size = None;
+
+ if !is_visible(style) {
+ self.sizes
+ .insert(ElementRef::new(element), Default::default());
+ return Default::default();
+ }
+
+ // Resize inner content.
+ //
+ // These cases should result in `content_size` being set, if relevant.
+ use model::ElementType::*;
+ match &element.element_type {
+ Button(model::Button {
+ content: Some(content),
+ ..
+ }) => {
+ // Special case for buttons with a label.
+ if let Label(model::Label {
+ text: Property::Static(text),
+ ..
+ }) = &content.element_type
+ {
+ let mut size = inner_size.less_margin(&BUTTON_MARGIN);
+ self.measure_text(text.as_str(), element, &mut size);
+ content_size = Some(size.plus_margin(&BUTTON_MARGIN));
+ }
+ }
+ Checkbox(model::Checkbox {
+ label: Some(label), ..
+ }) => {
+ let mut size = inner_size.less_margin(&CHECKBOX_MARGIN);
+ self.measure_text(label.as_str(), element, &mut size);
+ content_size = Some(size.plus_margin(&CHECKBOX_MARGIN));
+ }
+ Label(model::Label { text, bold: _ }) => {
+ let mut size = inner_size.clone();
+ match text {
+ Property::Static(text) => self.measure_text(text.as_str(), element, &mut size),
+ Property::Binding(b) => {
+ self.measure_text(b.borrow().as_str(), element, &mut size)
+ }
+ Property::ReadOnly(_) => {
+ unimplemented!("Label::text does not support ReadOnly")
+ }
+ }
+ content_size = Some(size);
+ }
+ VBox(model::VBox { items, spacing }) => {
+ let mut height = 0;
+ let mut max_width = 0;
+ let mut remaining_size = inner_size.clone();
+ let mut resize_child = |c| {
+ let child_size = self.resize(c, &remaining_size);
+ height += child_size.height;
+ max_width = std::cmp::max(child_size.width, max_width);
+ remaining_size.height = remaining_size
+ .height
+ .saturating_sub(child_size.height + spacing);
+ };
+ // First resize all non-Fill items; Fill items get the remaining space.
+ for item in items
+ .iter()
+ .filter(|i| i.style.vertical_alignment != Alignment::Fill)
+ {
+ resize_child(item);
+ }
+ for item in items
+ .iter()
+ .filter(|i| i.style.vertical_alignment == Alignment::Fill)
+ {
+ resize_child(item);
+ }
+ content_size = Some(Size {
+ width: max_width,
+ height: height + spacing * (items.len().saturating_sub(1) as u32),
+ });
+ }
+ HBox(model::HBox {
+ items,
+ spacing,
+ affirmative_order: _,
+ }) => {
+ let mut width = 0;
+ let mut max_height = 0;
+ let mut remaining_size = inner_size.clone();
+ let mut resize_child = |c| {
+ let child_size = self.resize(c, &remaining_size);
+ width += child_size.width;
+ max_height = std::cmp::max(child_size.height, max_height);
+ remaining_size.width = remaining_size
+ .width
+ .saturating_sub(child_size.width + spacing);
+ };
+ // First resize all non-Fill items; Fill items get the remaining space.
+ for item in items
+ .iter()
+ .filter(|i| i.style.horizontal_alignment != Alignment::Fill)
+ {
+ resize_child(item);
+ }
+ for item in items
+ .iter()
+ .filter(|i| i.style.horizontal_alignment == Alignment::Fill)
+ {
+ resize_child(item);
+ }
+ content_size = Some(Size {
+ width: width + spacing * (items.len().saturating_sub(1) as u32),
+ height: max_height,
+ });
+ }
+ Scroll(model::Scroll {
+ content: Some(content),
+ }) => {
+ content_size = Some(self.resize(content, &inner_size));
+ }
+ Progress(model::Progress { .. }) => {
+ // Min size recommended by windows uxguide
+ content_size = Some(Size {
+ width: 160,
+ height: 15,
+ });
+ }
+ // We don't support sizing by textbox content yet (need to read from the HWND due to
+ // Property::ReadOnly).
+ TextBox(_) => (),
+ _ => (),
+ }
+
+ // Adjust from content size.
+ if let Some(content_size) = content_size {
+ inner_size.from_content_size(style, &content_size);
+ }
+
+ // Compute/store (outer) size and return.
+ let size = inner_size.plus_margin(&style.margin);
+ self.sizes.insert(ElementRef::new(element), size);
+ size
+ }
+
+ fn get_size(&self, element: &Element) -> &Size {
+ self.sizes
+ .get(&ElementRef::new(element))
+ .expect("element not resized")
+ }
+
+ fn reposition(&mut self, element: &Element, position: &Position, parent_size: &Size) {
+ let style = &element.style;
+ if !is_visible(style) {
+ return;
+ }
+ let size = self.get_size(element);
+
+ let start_offset = match style.horizontal_alignment {
+ Alignment::Fill | Alignment::Start => 0,
+ Alignment::Center => parent_size.width.saturating_sub(size.width) / 2,
+ Alignment::End => parent_size.width.saturating_sub(size.width),
+ };
+ let top_offset = match style.vertical_alignment {
+ Alignment::Fill | Alignment::Start => 0,
+ Alignment::Center => parent_size.height.saturating_sub(size.height) / 2,
+ Alignment::End => parent_size.height.saturating_sub(size.height),
+ };
+
+ let inner_position = Position {
+ start: position.start + start_offset,
+ top: position.top + top_offset,
+ }
+ .less_margin(&style.margin);
+ let inner_size = size.less_margin(&style.margin);
+
+ // Set the window size/position if there is a handle associated with the element.
+ if let Some(&hwnd) = self.elements.get(&ElementRef::new(element)) {
+ unsafe {
+ win::SetWindowPos(
+ hwnd,
+ self.last_positioned.unwrap_or(win::HWND_TOP),
+ inner_position.start.try_into().unwrap(),
+ inner_position.top.try_into().unwrap(),
+ inner_size.width.try_into().unwrap(),
+ inner_size.height.try_into().unwrap(),
+ 0,
+ );
+ Gdi::InvalidateRect(hwnd, std::ptr::null(), 1);
+ }
+ self.last_positioned = Some(hwnd);
+ }
+
+ // Reposition content.
+ match &element.element_type {
+ model::ElementType::VBox(model::VBox { items, spacing }) => {
+ let mut position = inner_position;
+ let mut size = inner_size;
+ for item in items {
+ self.reposition(item, &position, &size);
+ let consumed = self.get_size(item).height + spacing;
+ if item.style.vertical_alignment != Alignment::End {
+ position.top += consumed;
+ }
+ size.height = size.height.saturating_sub(consumed);
+ }
+ }
+ model::ElementType::HBox(model::HBox {
+ items,
+ spacing,
+ // The default ordering matches the windows platform order
+ affirmative_order: _,
+ }) => {
+ let mut position = inner_position;
+ let mut size = inner_size;
+ for item in items {
+ self.reposition(item, &position, &inner_size);
+ let consumed = self.get_size(item).width + spacing;
+ if item.style.horizontal_alignment != Alignment::End {
+ position.start += consumed;
+ }
+ size.width = size.width.saturating_sub(consumed);
+ }
+ }
+ model::ElementType::Scroll(model::Scroll {
+ content: Some(content),
+ }) => {
+ self.reposition(content, &inner_position, &inner_size);
+ }
+ _ => (),
+ }
+ }
+
+ /// The `size` represents the maximum size permitted for the text (which is used for word
+ /// breaking), and it will be set to the precise width and height of the text. The width should
+ /// not exceed the input `size` width, but the height may.
+ fn measure_text(&mut self, text: &str, element: &Element, size: &mut Size) {
+ let Some(&window) = self.elements.get(&ElementRef::new(element)) else {
+ return;
+ };
+ let hdc = unsafe { Gdi::GetDC(window) };
+ unsafe { Gdi::SelectObject(hdc, win::SendMessageW(window, win::WM_GETFONT, 0, 0) as _) };
+ let mut height: u32 = 0;
+ let mut max_width: u32 = 0;
+ let mut char_fit = 0i32;
+ let mut win_size = unsafe { std::mem::zeroed::<SIZE>() };
+ for mut line in text.lines() {
+ if line.is_empty() {
+ line = " ";
+ }
+ let text = WideString::new(line);
+ let mut text = text.as_slice();
+ let mut extents = vec![0i32; text.len()];
+ while !text.is_empty() {
+ unsafe {
+ Gdi::GetTextExtentExPointW(
+ hdc,
+ text.as_ptr(),
+ text.len() as i32,
+ size.width.try_into().unwrap(),
+ &mut char_fit,
+ extents.as_mut_ptr(),
+ &mut win_size,
+ );
+ }
+ if char_fit == 0 {
+ return;
+ }
+ let mut split = char_fit as usize;
+ let mut split_end = split.saturating_sub(1);
+ if (char_fit as usize) < text.len() {
+ for i in (0..char_fit as usize).rev() {
+ // FIXME safer utf16 handling?
+ if text[i] == b' ' as u16 {
+ split = i + 1;
+ split_end = i.saturating_sub(1);
+ break;
+ }
+ }
+ }
+ text = &text[split..];
+ max_width = std::cmp::max(max_width, extents[split_end].try_into().unwrap());
+ let measured_height: u32 = win_size.cy.try_into().unwrap();
+ height += measured_height;
+ }
+ }
+ unsafe { Gdi::ReleaseDC(window, hdc) };
+
+ assert!(max_width <= size.width);
+ size.width = max_width;
+ size.height = height;
+ }
+}
+
+#[derive(Debug, Default, Clone, Copy)]
+struct Size {
+ pub width: u32,
+ pub height: u32,
+}
+
+impl Size {
+ pub fn inner_size(&self, style: &ElementStyle) -> Self {
+ let mut ret = self.less_margin(&style.margin);
+ if let Some(width) = style.horizontal_size_request {
+ ret.width = width;
+ }
+ if let Some(height) = style.vertical_size_request {
+ ret.height = height;
+ }
+ ret
+ }
+
+ pub fn from_content_size(&mut self, style: &ElementStyle, content_size: &Self) {
+ if style.horizontal_size_request.is_none() && style.horizontal_alignment != Alignment::Fill
+ {
+ self.width = content_size.width;
+ }
+ if style.vertical_size_request.is_none() && style.vertical_alignment != Alignment::Fill {
+ self.height = content_size.height;
+ }
+ }
+
+ pub fn plus_margin(&self, margin: &Margin) -> Self {
+ let mut ret = self.clone();
+ ret.width += margin.start + margin.end;
+ ret.height += margin.top + margin.bottom;
+ ret
+ }
+
+ pub fn less_margin(&self, margin: &Margin) -> Self {
+ let mut ret = self.clone();
+ ret.width = ret.width.saturating_sub(margin.start + margin.end);
+ ret.height = ret.height.saturating_sub(margin.top + margin.bottom);
+ ret
+ }
+}
+
+#[derive(Debug, Default, Clone, Copy)]
+struct Position {
+ pub start: u32,
+ pub top: u32,
+}
+
+impl Position {
+ #[allow(dead_code)]
+ pub fn plus_margin(&self, margin: &Margin) -> Self {
+ let mut ret = self.clone();
+ ret.start = ret.start.saturating_sub(margin.start);
+ ret.top = ret.top.saturating_sub(margin.top);
+ ret
+ }
+
+ pub fn less_margin(&self, margin: &Margin) -> Self {
+ let mut ret = self.clone();
+ ret.start += margin.start;
+ ret.top += margin.top;
+ ret
+ }
+}
+
+fn is_visible(style: &ElementStyle) -> bool {
+ match &style.visible {
+ Property::Static(v) => *v,
+ Property::Binding(s) => *s.borrow(),
+ _ => true,
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/windows/mod.rs b/toolkit/crashreporter/client/app/src/ui/windows/mod.rs
new file mode 100644
index 0000000000..c2f396b80d
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/windows/mod.rs
@@ -0,0 +1,949 @@
+/* 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/. */
+
+//! A UI using the windows API.
+//!
+//! This UI contains some edge cases that aren't implemented, for instance:
+//! * there are a few cases where specific hierarchies are handled differently (e.g. a Button
+//! containing Label, Scroll behavior, etc).
+//! * not all controls handle all Property variants (e.g. Checkbox doesn't handle ReadOnly, TextBox
+//! doesn't handle Binding, etc).
+//!
+//! The error handling is also a _little_ fast-and-loose, as many functions return an error value
+//! that is acceptable to following logic (though it still would be a good idea to improve this).
+//!
+//! The rendering treats VBox, HBox, and Scroll as strictly layout-only: they do not create any
+//! associated windows, and the layout logic handles their behavior.
+
+// Our windows-targets doesn't link uxtheme correctly for GetThemeSysFont/GetThemeSysColor.
+// This was working in windows-sys 0.48.
+#[link(name = "uxtheme", kind = "static")]
+extern "C" {}
+
+use super::model::{self, Application, Element, ElementStyle, TypedElement};
+use crate::data::Property;
+use font::Font;
+use once_cell::sync::Lazy;
+use quit_token::QuitToken;
+use std::cell::RefCell;
+use std::collections::HashMap;
+use std::pin::Pin;
+use std::rc::Rc;
+use widestring::WideString;
+use window::{CustomWindowClass, Window, WindowBuilder};
+use windows_sys::Win32::{
+ Foundation::{HINSTANCE, HWND, LPARAM, LRESULT, RECT, WPARAM},
+ Graphics::Gdi,
+ System::{LibraryLoader::GetModuleHandleW, SystemServices, Threading::GetCurrentThreadId},
+ UI::{Controls, Input::KeyboardAndMouse, Shell, WindowsAndMessaging as win},
+};
+
+macro_rules! success {
+ ( nonzero $e:expr ) => {{
+ let value = $e;
+ assert_ne!(value, 0);
+ value
+ }};
+ ( lasterror $e:expr ) => {{
+ unsafe { windows_sys::Win32::Foundation::SetLastError(0) };
+ let value = $e;
+ assert!(value != 0 || windows_sys::Win32::Foundation::GetLastError() == 0);
+ value
+ }};
+ ( hresult $e:expr ) => {
+ assert_eq!($e, windows_sys::Win32::Foundation::S_OK);
+ };
+ ( pointer $e:expr ) => {{
+ let ptr = $e;
+ assert_ne!(ptr, 0);
+ ptr
+ }};
+}
+
+mod font;
+mod gdi;
+mod layout;
+mod quit_token;
+mod twoway;
+mod widestring;
+mod window;
+
+/// A Windows API UI implementation.
+pub struct UI {
+ thread_id: u32,
+}
+
+/// Custom user messages.
+#[repr(u32)]
+enum UserMessage {
+ Invoke = win::WM_USER,
+}
+
+fn get_invoke(msg: &win::MSG) -> Option<Box<model::InvokeFn>> {
+ if msg.message == UserMessage::Invoke as u32 {
+ Some(unsafe { Box::from_raw(msg.lParam as *mut model::InvokeFn) })
+ } else {
+ None
+ }
+}
+
+impl UI {
+ pub fn run_loop(&self, app: Application) {
+ // Initialize common controls.
+ {
+ let icc = Controls::INITCOMMONCONTROLSEX {
+ dwSize: std::mem::size_of::<Controls::INITCOMMONCONTROLSEX>() as _,
+ // Buttons, edit controls, and static controls are all included in 'standard'.
+ dwICC: Controls::ICC_STANDARD_CLASSES | Controls::ICC_PROGRESS_CLASS,
+ };
+ success!(nonzero unsafe { Controls::InitCommonControlsEx(&icc) });
+ }
+
+ // Enable font smoothing (per
+ // https://learn.microsoft.com/en-us/windows/win32/gdi/cleartype-antialiasing ).
+ unsafe {
+ // We don't check for failure on these, they are best-effort.
+ win::SystemParametersInfoW(
+ win::SPI_SETFONTSMOOTHING,
+ 1,
+ std::ptr::null_mut(),
+ win::SPIF_UPDATEINIFILE | win::SPIF_SENDCHANGE,
+ );
+ win::SystemParametersInfoW(
+ win::SPI_SETFONTSMOOTHINGTYPE,
+ 0,
+ win::FE_FONTSMOOTHINGCLEARTYPE as _,
+ win::SPIF_UPDATEINIFILE | win::SPIF_SENDCHANGE,
+ );
+ }
+
+ // Enable correct layout direction.
+ if unsafe { win::SetProcessDefaultLayout(if app.rtl { Gdi::LAYOUT_RTL } else { 0 }) } == 0 {
+ log::warn!("failed to set process layout direction");
+ }
+
+ let module: HINSTANCE = unsafe { GetModuleHandleW(std::ptr::null()) };
+
+ // Register custom classes.
+ AppWindow::register(module).expect("failed to register AppWindow window class");
+
+ {
+ // The quit token is cloned for each top-level window and dropped at the end of this
+ // scope.
+ let quit_token = QuitToken::new();
+
+ for window in app.windows {
+ let name = WideString::new(window.element_type.title.as_str());
+ let w = top_level_window(
+ module,
+ AppWindow::new(
+ WindowRenderer::new(module, window.element_type, &window.style),
+ Some(quit_token.clone()),
+ ),
+ &name,
+ &window.style,
+ );
+
+ unsafe { win::ShowWindow(w.handle, win::SW_NORMAL) };
+ unsafe { Gdi::UpdateWindow(w.handle) };
+ }
+ }
+
+ // Run the event loop.
+ let mut msg = unsafe { std::mem::zeroed::<win::MSG>() };
+ while unsafe { win::GetMessageW(&mut msg, 0, 0, 0) } > 0 {
+ if let Some(f) = get_invoke(&msg) {
+ f();
+ continue;
+ }
+
+ unsafe {
+ // IsDialogMessageW is necessary to handle niceties like tab navigation
+ if win::IsDialogMessageW(win::GetAncestor(msg.hwnd, win::GA_ROOT), &mut msg) == 0 {
+ win::TranslateMessage(&msg);
+ win::DispatchMessageW(&msg);
+ }
+ }
+ }
+
+ // Flush queue to properly drop late invokes (this is a very unlikely case)
+ while unsafe { win::PeekMessageW(&mut msg, 0, 0, 0, win::PM_REMOVE) } > 0 {
+ if let Some(f) = get_invoke(&msg) {
+ drop(f);
+ }
+ }
+ }
+
+ pub fn invoke(&self, f: model::InvokeFn) {
+ let ptr: *mut model::InvokeFn = Box::into_raw(Box::new(f));
+ if unsafe {
+ win::PostThreadMessageW(self.thread_id, UserMessage::Invoke as u32, 0, ptr as _)
+ } == 0
+ {
+ let _ = unsafe { Box::from_raw(ptr) };
+ log::warn!("failed to invoke function on thread message queue");
+ }
+ }
+}
+
+impl Default for UI {
+ fn default() -> Self {
+ UI {
+ thread_id: unsafe { GetCurrentThreadId() },
+ }
+ }
+}
+
+/// A reference to an Element.
+#[derive(PartialEq, Eq, Hash, Clone, Copy)]
+struct ElementRef(*const Element);
+
+impl ElementRef {
+ pub fn new(element: &Element) -> Self {
+ ElementRef(element as *const Element)
+ }
+
+ /// # Safety
+ /// You must ensure the reference is still valid.
+ pub unsafe fn get(&self) -> &Element {
+ &*self.0
+ }
+}
+
+// Equivalent of win32 HIWORD macro
+fn hiword(v: u32) -> u16 {
+ (v >> 16) as u16
+}
+
+// Equivalent of win32 LOWORD macro
+fn loword(v: u32) -> u16 {
+ v as u16
+}
+
+// Equivalent of win32 MAKELONG macro
+fn makelong(low: u16, high: u16) -> u32 {
+ (high as u32) << 16 | low as u32
+}
+
+fn top_level_window<W: window::WindowClass + window::WindowData>(
+ module: HINSTANCE,
+ class: W,
+ title: &WideString,
+ style: &ElementStyle,
+) -> Window<W> {
+ class
+ .builder(module)
+ .name(title)
+ .style(win::WS_OVERLAPPEDWINDOW)
+ .pos(win::CW_USEDEFAULT, win::CW_USEDEFAULT)
+ .size(
+ style
+ .horizontal_size_request
+ .and_then(|i| i.try_into().ok())
+ .unwrap_or(win::CW_USEDEFAULT),
+ style
+ .vertical_size_request
+ .and_then(|i| i.try_into().ok())
+ .unwrap_or(win::CW_USEDEFAULT),
+ )
+ .create()
+}
+
+window::basic_window_classes! {
+ /// Static control (text, image, etc) class.
+ struct Static => "STATIC";
+
+ /// Button control class.
+ struct Button => "BUTTON";
+
+ /// Edit control class.
+ struct Edit => "EDIT";
+
+ /// Progress control class.
+ struct Progress => "msctls_progress32";
+}
+
+/// A top-level application window.
+///
+/// This is used for the main window and modal windows.
+struct AppWindow {
+ renderer: WindowRenderer,
+ _quit_token: Option<QuitToken>,
+}
+
+impl AppWindow {
+ pub fn new(renderer: WindowRenderer, quit_token: Option<QuitToken>) -> Self {
+ AppWindow {
+ renderer,
+ _quit_token: quit_token,
+ }
+ }
+}
+
+impl window::WindowClass for AppWindow {
+ fn class_name() -> WideString {
+ WideString::new("App Window")
+ }
+}
+
+impl CustomWindowClass for AppWindow {
+ fn icon() -> win::HICON {
+ static ICON: Lazy<win::HICON> = Lazy::new(|| unsafe {
+ // If CreateIconFromResource fails it returns NULL, which is fine (a default icon will be
+ // used).
+ win::CreateIconFromResource(
+ // We take advantage of the fact that since Windows Vista, an RT_ICON resource entry
+ // can simply be a PNG image.
+ super::icon::PNG_DATA.as_ptr(),
+ super::icon::PNG_DATA.len() as u32,
+ true.into(),
+ // The 0x00030000 constant isn't available anywhere; the docs basically say to just
+ // pass it...
+ 0x00030000,
+ )
+ });
+
+ *ICON
+ }
+
+ fn message(
+ data: &RefCell<Self>,
+ hwnd: HWND,
+ umsg: u32,
+ wparam: WPARAM,
+ lparam: LPARAM,
+ ) -> Option<LRESULT> {
+ let me = data.borrow();
+ let model = me.renderer.model();
+ match umsg {
+ win::WM_CREATE => {
+ if let Some(close) = &model.close {
+ close.subscribe(move |&()| unsafe {
+ win::SendMessageW(hwnd, win::WM_CLOSE, 0, 0);
+ });
+ }
+
+ let mut renderer = me.renderer.child_renderer(hwnd);
+ if let Some(child) = &model.content {
+ renderer.render_child(child);
+ }
+
+ drop(model);
+ let children = std::mem::take(&mut me.renderer.model_mut().children);
+ for child in children {
+ renderer.render_window(child);
+ }
+ }
+ win::WM_CLOSE => {
+ if model.modal {
+ // Modal windows should hide themselves rather than closing/destroying.
+ unsafe { win::ShowWindow(hwnd, win::SW_HIDE) };
+ return Some(0);
+ }
+ }
+ win::WM_SHOWWINDOW => {
+ if model.modal {
+ // Modal windows should disable/enable their parent as they are shown/hid,
+ // respectively.
+ let shown = wparam != 0;
+ unsafe {
+ KeyboardAndMouse::EnableWindow(
+ win::GetWindow(hwnd, win::GW_OWNER),
+ (!shown).into(),
+ )
+ };
+ return Some(0);
+ }
+ }
+ win::WM_GETMINMAXINFO => {
+ let minmaxinfo = unsafe { (lparam as *mut win::MINMAXINFO).as_mut().unwrap() };
+ minmaxinfo.ptMinTrackSize.x = me.renderer.min_size.0.try_into().unwrap();
+ minmaxinfo.ptMinTrackSize.y = me.renderer.min_size.1.try_into().unwrap();
+ return Some(0);
+ }
+ win::WM_SIZE => {
+ // When resized, recompute the layout.
+ let width = loword(lparam as _) as u32;
+ let height = hiword(lparam as _) as u32;
+
+ if let Some(child) = &model.content {
+ me.renderer.layout(child, width, height);
+ unsafe { Gdi::UpdateWindow(hwnd) };
+ }
+ return Some(0);
+ }
+ win::WM_GETFONT => return Some(**me.renderer.font() as _),
+ win::WM_COMMAND => {
+ let child = lparam as HWND;
+ let windows = me.renderer.windows.borrow();
+ if let Some(&element) = windows.reverse().get(&child) {
+ // # Safety
+ // The ElementRefs all pertain to the model stored in the renderer.
+ let element = unsafe { element.get() };
+ // Handle button presses.
+ use model::ElementType::*;
+ match &element.element_type {
+ Button(model::Button { click, .. }) => {
+ let code = hiword(wparam as _) as u32;
+ if code == win::BN_CLICKED {
+ click.fire(&());
+ return Some(0);
+ }
+ }
+ Checkbox(model::Checkbox { checked, .. }) => {
+ let code = hiword(wparam as _) as u32;
+ if code == win::BN_CLICKED {
+ let check_state =
+ unsafe { win::SendMessageW(child, win::BM_GETCHECK, 0, 0) };
+ if let Property::Binding(s) = checked {
+ *s.borrow_mut() = check_state == Controls::BST_CHECKED as isize;
+ }
+ return Some(0);
+ }
+ }
+ _ => (),
+ }
+ }
+ }
+ _ => (),
+ }
+ None
+ }
+}
+
+/// State used while creating and updating windows.
+struct WindowRenderer {
+ // We wrap with an Rc to get weak references in property callbacks (like that of
+ // `ElementStyle::visible`).
+ inner: Rc<WindowRendererInner>,
+}
+
+impl std::ops::Deref for WindowRenderer {
+ type Target = WindowRendererInner;
+
+ fn deref(&self) -> &Self::Target {
+ &self.inner
+ }
+}
+
+struct WindowRendererInner {
+ pub module: HINSTANCE,
+ /// The model is pinned and boxed to ensure that references in `windows` remain valid.
+ ///
+ /// We need to keep the model around so we can correctly perform layout as the window size
+ /// changes. Unfortunately the win32 API doesn't have any nice ways to automatically perform
+ /// layout.
+ pub model: RefCell<Pin<Box<model::Window>>>,
+ pub min_size: (u32, u32),
+ /// Mapping between model elements and windows.
+ ///
+ /// Element references pertain to elements in `model`.
+ pub windows: RefCell<twoway::TwoWay<ElementRef, HWND>>,
+ pub font: Font,
+ pub bold_font: Font,
+}
+
+impl WindowRenderer {
+ pub fn new(module: HINSTANCE, model: model::Window, style: &model::ElementStyle) -> Self {
+ WindowRenderer {
+ inner: Rc::new(WindowRendererInner {
+ module,
+ model: RefCell::new(Box::pin(model)),
+ min_size: (
+ style.horizontal_size_request.unwrap_or(0),
+ style.vertical_size_request.unwrap_or(0),
+ ),
+ windows: Default::default(),
+ font: Font::caption(),
+ bold_font: Font::caption_bold().unwrap_or_else(Font::caption),
+ }),
+ }
+ }
+
+ pub fn child_renderer(&self, window: HWND) -> WindowChildRenderer {
+ WindowChildRenderer {
+ renderer: &self.inner,
+ window,
+ child_id: 0,
+ scroll: false,
+ }
+ }
+
+ pub fn layout(&self, element: &Element, max_width: u32, max_height: u32) {
+ layout::Layout::new(self.inner.windows.borrow().forward())
+ .layout(element, max_width, max_height);
+ }
+
+ pub fn model(&self) -> std::cell::Ref<'_, model::Window> {
+ std::cell::Ref::map(self.inner.model.borrow(), |b| &**b)
+ }
+
+ pub fn model_mut(&self) -> std::cell::RefMut<'_, model::Window> {
+ std::cell::RefMut::map(self.inner.model.borrow_mut(), |b| &mut **b)
+ }
+
+ pub fn font(&self) -> &Font {
+ &self.inner.font
+ }
+}
+
+struct WindowChildRenderer<'a> {
+ renderer: &'a Rc<WindowRendererInner>,
+ window: HWND,
+ child_id: i32,
+ scroll: bool,
+}
+
+impl<'a> WindowChildRenderer<'a> {
+ fn add_child<W: window::WindowClass>(&mut self, class: W) -> WindowBuilder<W> {
+ let builder = class
+ .builder(self.renderer.module)
+ .style(win::WS_CHILD | win::WS_VISIBLE)
+ .parent(self.window)
+ .child_id(self.child_id);
+ self.child_id += 1;
+ builder
+ }
+
+ fn add_window<W: window::WindowClass>(&mut self, class: W) -> WindowBuilder<W> {
+ class
+ .builder(self.renderer.module)
+ .style(win::WS_OVERLAPPEDWINDOW)
+ .pos(win::CW_USEDEFAULT, win::CW_USEDEFAULT)
+ .parent(self.window)
+ }
+
+ fn render_window(&mut self, model: TypedElement<model::Window>) -> Window {
+ let name = WideString::new(model.element_type.title.as_str());
+ let style = model.style;
+ let w = self
+ .add_window(AppWindow::new(
+ WindowRenderer::new(self.renderer.module, model.element_type, &style),
+ None,
+ ))
+ .size(
+ style
+ .horizontal_size_request
+ .and_then(|i| i.try_into().ok())
+ .unwrap_or(win::CW_USEDEFAULT),
+ style
+ .vertical_size_request
+ .and_then(|i| i.try_into().ok())
+ .unwrap_or(win::CW_USEDEFAULT),
+ )
+ .name(&name)
+ .create();
+
+ enabled_property(&style.enabled, w.handle);
+
+ let hwnd = w.handle;
+ let set_visible = move |visible| unsafe {
+ win::ShowWindow(hwnd, if visible { win::SW_SHOW } else { win::SW_HIDE });
+ };
+
+ match &style.visible {
+ Property::Static(false) => set_visible(false),
+ Property::Binding(s) => {
+ s.on_change(move |v| set_visible(*v));
+ if !*s.borrow() {
+ set_visible(false);
+ }
+ }
+ _ => (),
+ }
+
+ w.generic()
+ }
+
+ fn render_child(&mut self, element: &Element) {
+ if let Some(mut window) = self.render_element_type(&element.element_type) {
+ window.set_default_font(&self.renderer.font);
+
+ // Store the element to handle mapping.
+ self.renderer
+ .windows
+ .borrow_mut()
+ .insert(ElementRef::new(element), window.handle);
+
+ enabled_property(&element.style.enabled, window.handle);
+ }
+
+ // Handle visibility properties.
+ match &element.style.visible {
+ Property::Static(false) => {
+ set_visibility(element, false, self.renderer.windows.borrow().forward())
+ }
+ Property::Binding(s) => {
+ let weak_renderer = Rc::downgrade(self.renderer);
+ let element_ref = ElementRef::new(element);
+ let parent = self.window;
+ s.on_change(move |visible| {
+ let Some(renderer) = weak_renderer.upgrade() else {
+ return;
+ };
+ // # Safety
+ // ElementRefs are valid as long as the renderer is (and we have a strong
+ // reference to it).
+ let element = unsafe { element_ref.get() };
+ set_visibility(element, *visible, renderer.windows.borrow().forward());
+ // Send WM_SIZE so that the parent recomputes the layout.
+ unsafe {
+ let mut rect = std::mem::zeroed::<RECT>();
+ win::GetClientRect(parent, &mut rect);
+ win::SendMessageW(
+ parent,
+ win::WM_SIZE,
+ 0,
+ makelong(
+ (rect.right - rect.left) as u16,
+ (rect.bottom - rect.top) as u16,
+ ) as isize,
+ );
+ }
+ });
+ if !*s.borrow() {
+ set_visibility(element, false, self.renderer.windows.borrow().forward());
+ }
+ }
+ _ => (),
+ }
+ }
+
+ fn render_element_type(&mut self, element_type: &model::ElementType) -> Option<Window> {
+ use model::ElementType as ET;
+ match element_type {
+ ET::Label(model::Label { text, bold }) => {
+ let mut window = match text {
+ Property::Static(text) => {
+ let text = WideString::new(text.as_str());
+ self.add_child(Static)
+ .name(&text)
+ .add_style(SystemServices::SS_LEFT | SystemServices::SS_NOPREFIX)
+ .create()
+ }
+ Property::Binding(b) => {
+ let text = WideString::new(b.borrow().as_str());
+ let window = self
+ .add_child(Static)
+ .name(&text)
+ .add_style(SystemServices::SS_LEFT | SystemServices::SS_NOPREFIX)
+ .create();
+ let handle = window.handle;
+ b.on_change(move |text| {
+ let text = WideString::new(text.as_str());
+ unsafe { win::SetWindowTextW(handle, text.pcwstr()) };
+ });
+ window
+ }
+ Property::ReadOnly(_) => {
+ unimplemented!("ReadOnly property not supported for Label::text")
+ }
+ };
+ if *bold {
+ window.set_font(&self.renderer.bold_font);
+ }
+ Some(window.generic())
+ }
+ ET::TextBox(model::TextBox {
+ placeholder,
+ content,
+ editable,
+ }) => {
+ let scroll = self.scroll;
+ let window = self
+ .add_child(Edit)
+ .add_style(
+ (win::ES_LEFT
+ | win::ES_MULTILINE
+ | win::ES_WANTRETURN
+ | if *editable { 0 } else { win::ES_READONLY })
+ as u32
+ | win::WS_BORDER
+ | win::WS_TABSTOP
+ | if scroll { win::WS_VSCROLL } else { 0 },
+ )
+ .create();
+
+ fn to_control_text(s: &str) -> String {
+ s.replace("\n", "\r\n")
+ }
+
+ fn from_control_text(s: &str) -> String {
+ s.replace("\r\n", "\n")
+ }
+
+ struct SubClassData {
+ placeholder: Option<WideString>,
+ }
+
+ // EM_SETCUEBANNER doesn't work with multiline edit controls (for no particular
+ // reason?), so we have to draw it ourselves.
+ unsafe extern "system" fn subclass_proc(
+ hwnd: HWND,
+ msg: u32,
+ wparam: WPARAM,
+ lparam: LPARAM,
+ _uidsubclass: usize,
+ dw_ref_data: usize,
+ ) -> LRESULT {
+ let ret = Shell::DefSubclassProc(hwnd, msg, wparam, lparam);
+ if msg == win::WM_PAINT
+ && KeyboardAndMouse::GetFocus() != hwnd
+ && win::GetWindowTextLengthW(hwnd) == 0
+ {
+ let data = (dw_ref_data as *const SubClassData).as_ref().unwrap();
+ if let Some(placeholder) = &data.placeholder {
+ let mut rect = std::mem::zeroed::<RECT>();
+ win::GetClientRect(hwnd, &mut rect);
+ Gdi::InflateRect(&mut rect, -2, -2);
+
+ let dc = gdi::DC::new(hwnd).expect("failed to create GDI DC");
+ dc.with_object_selected(
+ win::SendMessageW(hwnd, win::WM_GETFONT, 0, 0) as _,
+ |hdc| {
+ Gdi::SetTextColor(
+ hdc,
+ Controls::GetThemeSysColor(0, Gdi::COLOR_GRAYTEXT),
+ );
+ Gdi::SetBkMode(hdc, Gdi::TRANSPARENT as i32);
+ success!(nonzero Gdi::DrawTextW(
+ hdc,
+ placeholder.pcwstr(),
+ -1,
+ &mut rect,
+ Gdi::DT_LEFT | Gdi::DT_TOP | Gdi::DT_WORDBREAK,
+ ));
+ },
+ )
+ .expect("failed to select font gdi object");
+ }
+ }
+
+ // Multiline edit controls capture the tab key. We want it to work as usual in
+ // the dialog (focusing the next input control).
+ if msg == win::WM_GETDLGCODE && wparam == KeyboardAndMouse::VK_TAB as usize {
+ return 0;
+ }
+
+ if msg == win::WM_DESTROY {
+ drop(unsafe { Box::from_raw(dw_ref_data as *mut SubClassData) });
+ }
+ return ret;
+ }
+
+ let subclassdata = Box::into_raw(Box::new(SubClassData {
+ placeholder: placeholder
+ .as_ref()
+ .map(|s| WideString::new(to_control_text(s))),
+ }));
+
+ unsafe {
+ Shell::SetWindowSubclass(
+ window.handle,
+ Some(subclass_proc),
+ 0,
+ subclassdata as _,
+ );
+ }
+
+ // Set up content property.
+ match content {
+ Property::ReadOnly(od) => {
+ let handle = window.handle;
+ od.register(move |target| {
+ // GetWindowText requires the buffer be large enough for the terminating
+ // null character (otherwise it truncates the string), but
+ // GetWindowTextLength returns the length without the null character, so we
+ // add 1.
+ let length = unsafe { win::GetWindowTextLengthW(handle) } + 1;
+ let mut buf = vec![0u16; length as usize];
+ unsafe { win::GetWindowTextW(handle, buf.as_mut_ptr(), length) };
+ buf.pop(); // null character; `String` doesn't want that
+ *target = from_control_text(&String::from_utf16_lossy(&buf));
+ });
+ }
+ Property::Static(s) => {
+ let text = WideString::new(to_control_text(s));
+ unsafe { win::SetWindowTextW(window.handle, text.pcwstr()) };
+ }
+ Property::Binding(b) => {
+ let handle = window.handle;
+ b.on_change(move |text| {
+ let text = WideString::new(to_control_text(text.as_str()));
+ unsafe { win::SetWindowTextW(handle, text.pcwstr()) };
+ });
+ let text = WideString::new(to_control_text(b.borrow().as_str()));
+ unsafe { win::SetWindowTextW(window.handle, text.pcwstr()) };
+ }
+ }
+ Some(window.generic())
+ }
+ ET::Scroll(model::Scroll { content }) => {
+ if let Some(content) = content {
+ // Scrolling is implemented in a cooperative, non-universal way right now.
+ self.scroll = true;
+ self.render_child(content);
+ self.scroll = false;
+ }
+ None
+ }
+ ET::Button(model::Button { content, .. }) => {
+ if let Some(ET::Label(model::Label {
+ text: Property::Static(text),
+ ..
+ })) = content.as_ref().map(|e| &e.element_type)
+ {
+ let text = WideString::new(text);
+
+ let window = self
+ .add_child(Button)
+ .add_style(win::BS_PUSHBUTTON as u32 | win::WS_TABSTOP)
+ .name(&text)
+ .create();
+ Some(window.generic())
+ } else {
+ None
+ }
+ }
+ ET::Checkbox(model::Checkbox { checked, label }) => {
+ let label = label.as_ref().map(WideString::new);
+ let mut builder = self
+ .add_child(Button)
+ .add_style((win::BS_AUTOCHECKBOX | win::BS_MULTILINE) as u32 | win::WS_TABSTOP);
+ if let Some(label) = &label {
+ builder = builder.name(label);
+ }
+ let window = builder.create();
+
+ fn set_check(handle: HWND, value: bool) {
+ unsafe {
+ win::SendMessageW(
+ handle,
+ win::BM_SETCHECK,
+ if value {
+ Controls::BST_CHECKED
+ } else {
+ Controls::BST_UNCHECKED
+ } as usize,
+ 0,
+ );
+ }
+ }
+
+ match checked {
+ Property::Static(checked) => set_check(window.handle, *checked),
+ Property::Binding(s) => {
+ let handle = window.handle;
+ s.on_change(move |v| {
+ set_check(handle, *v);
+ });
+ set_check(window.handle, *s.borrow());
+ }
+ _ => unimplemented!("ReadOnly properties not supported for Checkbox"),
+ }
+
+ Some(window.generic())
+ }
+ ET::Progress(model::Progress { amount }) => {
+ let window = self
+ .add_child(Progress)
+ .add_style(Controls::PBS_MARQUEE)
+ .create();
+
+ fn set_amount(handle: HWND, value: Option<f32>) {
+ match value {
+ None => unsafe {
+ win::SendMessageW(handle, Controls::PBM_SETMARQUEE, 1, 0);
+ },
+ Some(v) => unsafe {
+ win::SendMessageW(handle, Controls::PBM_SETMARQUEE, 0, 0);
+ win::SendMessageW(
+ handle,
+ Controls::PBM_SETPOS,
+ (v.clamp(0f32, 1f32) * 100f32) as usize,
+ 0,
+ );
+ },
+ }
+ }
+
+ match amount {
+ Property::Static(v) => set_amount(window.handle, *v),
+ Property::Binding(s) => {
+ let handle = window.handle;
+ s.on_change(move |v| set_amount(handle, *v));
+ set_amount(window.handle, *s.borrow());
+ }
+ _ => unimplemented!("ReadOnly properties not supported for Progress"),
+ }
+
+ Some(window.generic())
+ }
+ // VBox/HBox are virtual, their behaviors are implemented entirely in the renderer layout.
+ // No need for additional windows.
+ ET::VBox(model::VBox { items, .. }) => {
+ for item in items {
+ self.render_child(item);
+ }
+ None
+ }
+ ET::HBox(model::HBox { items, .. }) => {
+ for item in items {
+ self.render_child(item);
+ }
+ None
+ }
+ }
+ }
+}
+
+/// Handle the enabled property.
+///
+/// This function assumes the default state of the window is enabled.
+fn enabled_property(enabled: &Property<bool>, window: HWND) {
+ match enabled {
+ Property::Static(false) => unsafe {
+ KeyboardAndMouse::EnableWindow(window, false.into());
+ },
+ Property::Binding(s) => {
+ let handle = window;
+ s.on_change(move |enabled| {
+ unsafe { KeyboardAndMouse::EnableWindow(handle, (*enabled).into()) };
+ });
+ if !*s.borrow() {
+ unsafe { KeyboardAndMouse::EnableWindow(window, false.into()) };
+ }
+ }
+ _ => (),
+ }
+}
+
+/// Set the visibility of the given element. This recurses down the element tree and hides children
+/// as necessary.
+fn set_visibility(element: &Element, visible: bool, windows: &HashMap<ElementRef, HWND>) {
+ if let Some(&hwnd) = windows.get(&ElementRef::new(element)) {
+ unsafe {
+ win::ShowWindow(hwnd, if visible { win::SW_SHOW } else { win::SW_HIDE });
+ }
+ } else {
+ match &element.element_type {
+ model::ElementType::VBox(model::VBox { items, .. }) => {
+ for item in items {
+ set_visibility(item, visible, windows);
+ }
+ }
+ model::ElementType::HBox(model::HBox { items, .. }) => {
+ for item in items {
+ set_visibility(item, visible, windows);
+ }
+ }
+ model::ElementType::Scroll(model::Scroll {
+ content: Some(content),
+ }) => {
+ set_visibility(&*content, visible, windows);
+ }
+ _ => (),
+ }
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/windows/quit_token.rs b/toolkit/crashreporter/client/app/src/ui/windows/quit_token.rs
new file mode 100644
index 0000000000..f952db3db4
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/windows/quit_token.rs
@@ -0,0 +1,33 @@
+/* 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/. */
+
+use std::rc::Rc;
+use windows_sys::Win32::UI::WindowsAndMessaging::PostQuitMessage;
+
+/// A Cloneable token which will post a quit message (with code 0) to the main loop when the last
+/// instance is dropped.
+#[derive(Clone, Default)]
+pub struct QuitToken(#[allow(dead_code)] Rc<QuitTokenInternal>);
+
+impl QuitToken {
+ pub fn new() -> Self {
+ Self::default()
+ }
+}
+
+impl std::fmt::Debug for QuitToken {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ f.debug_struct(std::any::type_name::<Self>())
+ .finish_non_exhaustive()
+ }
+}
+
+#[derive(Default)]
+struct QuitTokenInternal;
+
+impl Drop for QuitTokenInternal {
+ fn drop(&mut self) {
+ unsafe { PostQuitMessage(0) };
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/windows/twoway.rs b/toolkit/crashreporter/client/app/src/ui/windows/twoway.rs
new file mode 100644
index 0000000000..8a18162e08
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/windows/twoway.rs
@@ -0,0 +1,36 @@
+/* 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/. */
+
+use std::collections::HashMap;
+
+/// A two-way hashmap.
+#[derive(Debug)]
+pub struct TwoWay<K, V> {
+ forward: HashMap<K, V>,
+ reverse: HashMap<V, K>,
+}
+
+impl<K, V> Default for TwoWay<K, V> {
+ fn default() -> Self {
+ TwoWay {
+ forward: Default::default(),
+ reverse: Default::default(),
+ }
+ }
+}
+
+impl<K: Eq + std::hash::Hash + Clone, V: Eq + std::hash::Hash + Clone> TwoWay<K, V> {
+ pub fn insert(&mut self, key: K, value: V) {
+ self.forward.insert(key.clone(), value.clone());
+ self.reverse.insert(value, key);
+ }
+
+ pub fn forward(&self) -> &HashMap<K, V> {
+ &self.forward
+ }
+
+ pub fn reverse(&self) -> &HashMap<V, K> {
+ &self.reverse
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/windows/widestring.rs b/toolkit/crashreporter/client/app/src/ui/windows/widestring.rs
new file mode 100644
index 0000000000..0dc713352b
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/windows/widestring.rs
@@ -0,0 +1,36 @@
+/* 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/. */
+
+use std::ffi::OsStr;
+use std::os::windows::ffi::OsStrExt;
+use windows_sys::core::PCWSTR;
+
+/// Windows wide strings.
+///
+/// These are utf16 encoded with a terminating null character (0).
+pub struct WideString(Vec<u16>);
+
+impl WideString {
+ pub fn new(os_str: impl AsRef<OsStr>) -> Self {
+ // TODO: doesn't check whether the OsStr contains a null character, which could be treated
+ // as an error (as `CString::new` does).
+ WideString(
+ os_str
+ .as_ref()
+ .encode_wide()
+ .chain(std::iter::once(0))
+ // Remove unicode BIDI markers (from fluent) which aren't rendered correctly.
+ .filter(|c| *c != 0x2068 && *c != 0x2069)
+ .collect(),
+ )
+ }
+
+ pub fn pcwstr(&self) -> PCWSTR {
+ self.0.as_ptr()
+ }
+
+ pub fn as_slice(&self) -> &[u16] {
+ &self.0
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/ui/windows/window.rs b/toolkit/crashreporter/client/app/src/ui/windows/window.rs
new file mode 100644
index 0000000000..7e5e8f3f2a
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/ui/windows/window.rs
@@ -0,0 +1,302 @@
+/* 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/. */
+
+//! Types and helpers relating to windows and window classes.
+
+use super::Font;
+use super::WideString;
+use std::cell::RefCell;
+use windows_sys::Win32::{
+ Foundation::{HINSTANCE, HWND, LPARAM, LRESULT, WPARAM},
+ Graphics::Gdi::{self, HBRUSH},
+ UI::WindowsAndMessaging::{self as win, HCURSOR, HICON},
+};
+
+/// Types representing a window class.
+pub trait WindowClass: Sized + 'static {
+ fn class_name() -> WideString;
+
+ fn builder(self, module: HINSTANCE) -> WindowBuilder<'static, Self> {
+ WindowBuilder {
+ name: None,
+ style: None,
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 0,
+ parent: None,
+ child_id: 0,
+ module,
+ data: self,
+ }
+ }
+}
+
+/// Window classes which have their own message handler.
+///
+/// A type implementing this trait provides its data to a single window.
+///
+/// `register` must be called before use.
+pub trait CustomWindowClass: WindowClass {
+ /// Handle a message.
+ fn message(
+ data: &RefCell<Self>,
+ hwnd: HWND,
+ umsg: u32,
+ wparam: WPARAM,
+ lparam: LPARAM,
+ ) -> Option<LRESULT>;
+
+ /// The class's default background brush.
+ fn background() -> HBRUSH {
+ (Gdi::COLOR_3DFACE + 1) as HBRUSH
+ }
+
+ /// The class's default cursor.
+ fn cursor() -> HCURSOR {
+ unsafe { win::LoadCursorW(0, win::IDC_ARROW) }
+ }
+
+ /// The class's default icon.
+ fn icon() -> HICON {
+ 0
+ }
+
+ /// Register the class.
+ fn register(module: HINSTANCE) -> anyhow::Result<()> {
+ unsafe extern "system" fn wnd_proc<W: CustomWindowClass>(
+ hwnd: HWND,
+ umsg: u32,
+ wparam: WPARAM,
+ lparam: LPARAM,
+ ) -> LRESULT {
+ if umsg == win::WM_CREATE {
+ let create_struct = &*(lparam as *const win::CREATESTRUCTW);
+ success!(lasterror win::SetWindowLongPtrW(hwnd, 0, create_struct.lpCreateParams as _));
+ // Changes made with SetWindowLongPtr don't take effect until SetWindowPos is called...
+ success!(nonzero win::SetWindowPos(
+ hwnd,
+ win::HWND_TOP,
+ 0,
+ 0,
+ 0,
+ 0,
+ win::SWP_NOMOVE | win::SWP_NOSIZE | win::SWP_NOZORDER | win::SWP_FRAMECHANGED,
+ ));
+ }
+
+ let result = unsafe { W::get(hwnd).as_ref() }
+ .and_then(|data| W::message(data, hwnd, umsg, wparam, lparam));
+ if umsg == win::WM_DESTROY {
+ drop(Box::from_raw(
+ win::GetWindowLongPtrW(hwnd, 0) as *mut RefCell<W>
+ ));
+ }
+ result.unwrap_or_else(|| win::DefWindowProcW(hwnd, umsg, wparam, lparam))
+ }
+
+ let class_name = Self::class_name();
+ let window_class = win::WNDCLASSW {
+ lpfnWndProc: Some(wnd_proc::<Self>),
+ hInstance: module,
+ lpszClassName: class_name.pcwstr(),
+ hbrBackground: Self::background(),
+ hIcon: Self::icon(),
+ hCursor: Self::cursor(),
+ cbWndExtra: std::mem::size_of::<isize>() as i32,
+ ..unsafe { std::mem::zeroed() }
+ };
+
+ if unsafe { win::RegisterClassW(&window_class) } == 0 {
+ anyhow::bail!("RegisterClassW failed")
+ }
+ Ok(())
+ }
+
+ /// Get the window data from a window created with this class.
+ ///
+ /// # Safety
+ /// This must only be called on window handles which were created with this class.
+ unsafe fn get(hwnd: HWND) -> *const RefCell<Self> {
+ win::GetWindowLongPtrW(hwnd, 0) as *const RefCell<Self>
+ }
+}
+
+/// Types that can be stored as associated window data.
+pub trait WindowData: Sized {
+ fn to_ptr(self) -> *mut RefCell<Self> {
+ std::ptr::null_mut()
+ }
+}
+
+impl<T: CustomWindowClass> WindowData for T {
+ fn to_ptr(self) -> *mut RefCell<Self> {
+ Box::into_raw(Box::new(RefCell::new(self)))
+ }
+}
+
+macro_rules! basic_window_classes {
+ () => {};
+ ( $(#[$attr:meta])* struct $name:ident => $class:expr; $($rest:tt)* ) => {
+ #[derive(Default)]
+ $(#[$attr])*
+ struct $name;
+
+ impl $crate::ui::ui_impl::window::WindowClass for $name {
+ fn class_name() -> $crate::ui::ui_impl::WideString {
+ $crate::ui::ui_impl::WideString::new($class)
+ }
+ }
+
+ impl $crate::ui::ui_impl::window::WindowData for $name {}
+
+ $crate::ui::ui_impl::window::basic_window_classes!($($rest)*);
+ }
+}
+
+pub(crate) use basic_window_classes;
+
+pub struct WindowBuilder<'a, W> {
+ name: Option<&'a WideString>,
+ style: Option<u32>,
+ x: i32,
+ y: i32,
+ width: i32,
+ height: i32,
+ parent: Option<HWND>,
+ child_id: i32,
+ module: HINSTANCE,
+ data: W,
+}
+
+impl<'a, W> WindowBuilder<'a, W> {
+ #[must_use]
+ pub fn name<'b>(self, s: &'b WideString) -> WindowBuilder<'b, W> {
+ WindowBuilder {
+ name: Some(s),
+ style: self.style,
+ x: self.x,
+ y: self.y,
+ width: self.width,
+ height: self.height,
+ parent: self.parent,
+ child_id: self.child_id,
+ module: self.module,
+ data: self.data,
+ }
+ }
+
+ #[must_use]
+ pub fn style(mut self, d: u32) -> Self {
+ self.style = Some(d);
+ self
+ }
+
+ #[must_use]
+ pub fn add_style(mut self, d: u32) -> Self {
+ *self.style.get_or_insert(0) |= d;
+ self
+ }
+
+ #[must_use]
+ pub fn pos(mut self, x: i32, y: i32) -> Self {
+ self.x = x;
+ self.y = y;
+ self
+ }
+
+ #[must_use]
+ pub fn size(mut self, width: i32, height: i32) -> Self {
+ self.width = width;
+ self.height = height;
+ self
+ }
+
+ #[must_use]
+ pub fn parent(mut self, parent: HWND) -> Self {
+ self.parent = Some(parent);
+ self
+ }
+
+ #[must_use]
+ pub fn child_id(mut self, id: i32) -> Self {
+ self.child_id = id;
+ self
+ }
+
+ pub fn create(self) -> Window<W>
+ where
+ W: WindowClass + WindowData,
+ {
+ let class_name = W::class_name();
+ let handle = unsafe {
+ win::CreateWindowExW(
+ 0,
+ class_name.pcwstr(),
+ self.name.map(|n| n.pcwstr()).unwrap_or(std::ptr::null()),
+ self.style.unwrap_or_default(),
+ self.x,
+ self.y,
+ self.width,
+ self.height,
+ self.parent.unwrap_or_default(),
+ self.child_id as _,
+ self.module,
+ self.data.to_ptr() as _,
+ )
+ };
+ assert!(handle != 0);
+
+ Window {
+ handle,
+ child_id: self.child_id,
+ font_set: false,
+ _class: std::marker::PhantomData,
+ }
+ }
+}
+
+/// A window handle with a known class type.
+///
+/// Without a type parameter (defaulting to `()`), the window handle is generic (class type
+/// unknown).
+pub struct Window<W: 'static = ()> {
+ pub handle: HWND,
+ pub child_id: i32,
+ font_set: bool,
+ _class: std::marker::PhantomData<&'static RefCell<W>>,
+}
+
+impl<W: CustomWindowClass> Window<W> {
+ /// Get the window data of the window.
+ #[allow(dead_code)]
+ pub fn data(&self) -> &RefCell<W> {
+ unsafe { W::get(self.handle).as_ref().unwrap() }
+ }
+}
+
+impl<W> Window<W> {
+ /// Get a generic window handle.
+ pub fn generic(self) -> Window {
+ Window {
+ handle: self.handle,
+ child_id: self.child_id,
+ font_set: self.font_set,
+ _class: std::marker::PhantomData,
+ }
+ }
+
+ /// Set a window's font.
+ pub fn set_font(&mut self, font: &Font) {
+ unsafe { win::SendMessageW(self.handle, win::WM_SETFONT, **font as _, 1 as _) };
+ self.font_set = true;
+ }
+
+ /// Set a window's font if not already set.
+ pub fn set_default_font(&mut self, font: &Font) {
+ if !self.font_set {
+ self.set_font(font);
+ }
+ }
+}