/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis
* 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 only uses `ElementStyle::margin` partially because `NSView` doesn't support //! margins, so we work them in as offsets in the layout constraints (they are applied based on the //! alignment). Once we no longer support OSX 10.15, we could use `NSView.layoutMarginsGuide` //! instead as it results in a layout almost identical to what the margins 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.
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();
// Run the main application loop unsafe { nsapp.run() };
});
}
pubfn 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 || { iflet 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 { pubfn 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 _,
objects
.as_slice()
.len()
.try_into()
.expect("usize can't fit in u64"),
)),
)
}
}
}
// # Safety // The array is static and cannot be changed. unsafeimpl Sync for RunloopModes {} unsafeimpl Send for RunloopModes {}
unsafeimpl 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()
}
objc_class! { impl Window: NSWindow /*<NSWindowDelegate>*/ { #[sel(init)] fn init(&mutself) -> cocoa::id { let style = &self.style; let title = &self.title; let w = cocoa::NSWindow(self.instance);
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(windowDidBecomeKey:)] fn window_did_become_key(&mutself, _notification: Ptr<cocoa::NSNotification>) { if matches!(self.window_type, WindowType::Main { make_main: true }) { let w = cocoa::NSWindow(self.instance); // In newer versions of macos, makeMainWindow doesn't seem to work reliably when // called from applicationDidFinishLaunching, so we call it here from // windowDidBecomeKey. unsafe {
w.center();
w.setContentSize_(w.minSize());
w.makeMainWindow();
} self.window_type = WindowType::Main { make_main: false };
}
}
#[sel(windowWillClose:)] fn window_will_close(&mutself, _notification: Ptr<cocoa::NSNotification>) { unsafe { let nsapp = cocoa::NSApplication::sharedApplication(); if matches!(self.window_type, WindowType::Modal) {
nsapp.stopModal();
} // We used to compare with NSApp.mainWindow (or check NSWindow.isMainWindow), but in // newer versions of macos this has become flaky in windowWillClose. We track the // main window ourselves, now. elseif matches!(self.window_type, WindowType::Main {..}) { // Stop the application, causing run_loop to exit.
nsapp.stop_(self.instance); // Send a dummy event to ensure the stop is witnessed. This is necessary because // we may be closing as a result of an `invoke()` rather than another event. let event = cocoa::NSEvent::otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2_(
cocoa::NSEventTypeApplicationDefined,
Default::default(),
Default::default(),
Default::default(),
Default::default(),
cocoa::NSGraphicsContext(std::ptr::null_mut()),
Default::default(), 0, 0
);
nsapp.postEvent_atStart_(event, runtime::YES);
}
}
}
}
}
#[sel(textView:doCommandBySelector:)] fn do_command_by_selector(&mutself, 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;
} elseif 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)
}
}
unsafefn constraint_equal<T, O>(anchor: T, to: O, margin: u32) where
T: INSLayoutAnchor<()> + std::ops::Deref,
T::Target: Message + Sized,
O: IntoNSLayoutAnchor,
{
anchor
.constraintEqualToAnchor_constant_(to.into_layout_anchor(), margin as f64)
.setActive_(runtime::YES);
}
// If there are no windows, the first window is always treated as the main window. let is_main = self.windows_to_retain.is_empty(); if is_main {
assert!(!modal, "main window cannot be modal");
}
let w = Window {
window_type: if is_main {
WindowType::Main { make_main: true }
} elseif modal {
WindowType::Modal
} else {
WindowType::Normal
},
title,
style,
}
.into_object();
let nswindow: StrongRef<cocoa::NSWindow> = w.clone().cast(); self.windows_to_retain.push(nswindow.clone());
unsafe { // Don't release windows when closed: we retain windows at the top-level.
nswindow.setReleasedWhenClosed_(runtime::NO);
iflet 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: 8.0,
height: 8.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);
impl ShowChild { pubfn show(&self, parent: cocoa::NSWindow, child: cocoa::NSWindow) { unsafe {
parent.addChildWindow_ordered_(child, cocoa::NSWindowAbove);
child.makeKeyAndOrderFront_(parent.0); ifself.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"),
}
}
if is_main {
nswindow.makeKeyAndOrderFront_(std::ptr::null_mut());
}
}
nswindow
}
}
/// Render the given element. /// /// Returns whether the element was rendered. pubfn render(
&self,
Element {
style,
element_type,
}: Element,
) -> bool { let Some(view) = render_element(element_type, &style, self.rtl) else { returnfalse;
};
(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_(ifself.rtl {
cocoa::NSUserInterfaceLayoutDirectionRightToLeft
} else {
cocoa::NSUserInterfaceLayoutDirectionLeftToRight
}); iflet Ok(control) = cocoa::NSControl::try_from(view) {
control.setBaseWritingDirection_(ifself.rtl {
cocoa::NSWritingDirectionRightToLeft
} else {
cocoa::NSWritingDirectionLeftToRight
});
}
}
// TODO: potentially use NSView layoutMarginsGuide when we no longer need to support macOS // 10.15. let outer = self.parent;
if !matches!(style.horizontal_alignment, Alignment::Fill) { iflet 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 = outer.leadingAnchor(); let pta = outer.trailingAnchor(); match style.horizontal_alignment {
Alignment::Fill => {
constraint_equal(la, pla, style.margin.start);
constraint_equal(ta, pta, style.margin.end); // 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, style.margin.start);
}
Alignment::Center => { let ca = view.centerXAnchor(); let pca = outer.centerXAnchor();
constraint_equal(ca, pca, 0);
}
Alignment::End => {
constraint_equal(ta, pta, style.margin.end);
}
}
}
}
if !matches!(style.vertical_alignment, Alignment::Fill) { iflet 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 = outer.topAnchor(); let pba = outer.bottomAnchor(); match style.vertical_alignment {
Alignment::Fill => {
constraint_equal(ta, pta, style.margin.top);
constraint_equal(ba, pba, style.margin.bottom); // 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, style.margin.top);
}
Alignment::Center => { let ca = view.centerYAnchor(); let pca = outer.centerYAnchor();
constraint_equal(ca, pca, 0);
}
Alignment::End => {
constraint_equal(ba, pba, style.margin.bottom);
}
}
}
}