Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  mod.rs   Sprache: unbekannt

 
/* 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 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;

/// 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, 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
                    // 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()
        }
    })
}

[ Dauer der Verarbeitung: 0.45 Sekunden  (vorverarbeitet)  ]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge