Source code
Revision control
Copy as Markdown
Other Tools
/* 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
//! 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.
#![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, 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, PNSObject,
};
use once_cell::sync::Lazy;
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.
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, 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);
}
#[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
});
}
}
// 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) {
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 = 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) {
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 = 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);
}
}
}
}
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
// 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()
}
})
}