summaryrefslogtreecommitdiffstats
path: root/toolkit/crashreporter/client/app/src/ui/model
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/crashreporter/client/app/src/ui/model')
-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
10 files changed, 579 insertions, 0 deletions
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);
+ }
+}