summaryrefslogtreecommitdiffstats
path: root/toolkit/crashreporter/client/app/src/ui/macos/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/crashreporter/client/app/src/ui/macos/mod.rs')
-rw-r--r--toolkit/crashreporter/client/app/src/ui/macos/mod.rs1122
1 files changed, 1122 insertions, 0 deletions
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()
+ }
+ })
+}