swiftui-native 0.0.1

SwiftUI from pure Rust — no Swift bridge, no shim, only swift-runtime calls
Documentation
//! The bridge that puts a SwiftUI view inside a native view.
//!
//! AppKit has `NSHostingView`, a view you can use directly. UIKit has no
//! equivalent: the only bridge is `UIHostingController`, and the view comes out
//! of its `.view` property. So on iOS this type holds both — the controller,
//! because that is what owns `rootView` and answers `sizeThatFits`, and the
//! view, because that is what everything else wants.

use crate::abi;
use crate::objc::{self, NSRect};
use crate::resolve;
use crate::views::ViewHandle;
use core::ffi::c_void;

/// A hosting view whose root view can be replaced at any time.
pub struct HostingView {
    /// The `NSView`/`UIView` SwiftUI draws into.
    ptr: *mut c_void,
    /// On UIKit the SwiftUI side lives on the controller, not the view.
    #[cfg(not(target_os = "macos"))]
    controller: *mut c_void,
    /// The offscreen window this view is captured in, made on first use.
    ///
    /// One per hosting view, not one per capture. Per capture leaks a window
    /// every frame — an application holds on to every window it makes — and
    /// closing them instead detaches the view between frames, which stops an
    /// animation dead. One window that lives exactly as long as the view it
    /// hosts does neither.
    window: core::cell::Cell<*mut c_void>,
}

impl HostingView {
    /// Wrap a view in a fresh hosting view.
    #[cfg(target_os = "macos")]
    pub fn new(view: &ViewHandle) -> Self {
        let _pool = objc::AutoreleasePool::new();
        Self {
            ptr: unsafe { make_hosting_view(view) },
            window: core::cell::Cell::new(core::ptr::null_mut()),
        }
    }

    /// Wrap a view in a fresh hosting controller, and take its view.
    #[cfg(not(target_os = "macos"))]
    pub fn new(view: &ViewHandle) -> Self {
        let _pool = objc::AutoreleasePool::new();
        unsafe {
            let controller = make_hosting_controller(view);
            // Asking for `.view` is what makes the controller load it.
            let ptr = objc::send(controller, objc::sel(c"view"));
            assert!(!ptr.is_null(), "UIHostingController produced no view");
            Self { ptr, controller, window: core::cell::Cell::new(core::ptr::null_mut()) }
        }
    }

    /// The native view SwiftUI draws into.
    pub fn as_ptr(&self) -> *mut c_void {
        self.ptr
    }

    /// The object that owns `rootView`: the view on AppKit, the controller on UIKit.
    fn root_owner(&self) -> *mut c_void {
        #[cfg(target_os = "macos")]
        {
            self.ptr
        }
        #[cfg(not(target_os = "macos"))]
        {
            self.controller
        }
    }

    /// The view controller, on platforms that have one.
    #[cfg(not(target_os = "macos"))]
    pub fn controller(&self) -> *mut c_void {
        self.controller
    }

    /// Swap the root view. SwiftUI diffs the new tree against the old one and
    /// updates the rendered output in place — this is how a Rust-driven UI
    /// changes after it is on screen.
    pub fn set_root(&self, view: &ViewHandle) {
        let _pool = objc::AutoreleasePool::new();
        unsafe {
            // `rootView` is a stored property on a generic class, so the setter
            // takes the new value indirectly (x0) with `self` in the Swift self
            // register (x20). It is reached through its dispatch thunk.
            // The setter consumes the value, and `to_anyview_word` already
            // mints a `+1` — it boxes a copy of the handle's own value rather
            // than lending the handle's reference, as it did when a handle *was*
            // an `AnyView`. The retain that used to balance this is now one too
            // many.
            let mut incoming: u64 = view.to_anyview_word();
            core::arch::asm!("blr {f}", f = in(reg) resolve::hosting_root_setter(),
                in("x0") &mut incoming as *mut u64,
                in("x20") self.root_owner(),
                lateout("x1") _, lateout("x2") _, lateout("x3") _,
                lateout("x4") _, lateout("x5") _, lateout("x6") _, lateout("x7") _,
                lateout("x8") _, lateout("x9") _, lateout("x10") _, lateout("x11") _,
                lateout("x12") _, lateout("x13") _, lateout("x14") _, lateout("x15") _,
                lateout("x16") _, lateout("x17") _, lateout("lr") _, lateout("x21") _,
                clobber_abi("C"),
            );
        }
    }

    /// Lay the view out at a fixed size.
    pub fn set_frame(&self, width: f64, height: f64) {
        let _pool = objc::AutoreleasePool::new();
        unsafe {
            objc::send_rect(
                self.ptr,
                objc::sel(c"setFrame:"),
                NSRect::new(0.0, 0.0, width, height),
            );
            #[cfg(target_os = "macos")]
            objc::send(self.ptr, objc::sel(c"layoutSubtreeIfNeeded"));
            #[cfg(not(target_os = "macos"))]
            {
                objc::send(self.ptr, objc::sel(c"setNeedsLayout"));
                objc::send(self.ptr, objc::sel(c"layoutIfNeeded"));
            }
        }
    }

    /// Whether SwiftUI should inset its content for the window's safe area.
    ///
    /// A hosting controller reads the safe area of whatever window it ends up
    /// in, so on a device with a notch a view asked for at 240x160 comes back
    /// with a band across the top that has nothing to do with the view. For
    /// rendering that is noise; for an app it is the point. `safeAreaRegions`
    /// is the supported way to say which you want.
    #[cfg(not(target_os = "macos"))]
    pub fn set_safe_area(&self, enabled: bool) {
        unsafe {
            // `SafeAreaRegions` is an OptionSet over a single word; the empty
            // set is zero and `.all` is every bit.
            let regions: u64 = if enabled { u64::MAX } else { 0 };
            core::arch::asm!("blr {f}", f = in(reg) resolve::hosting_ctrl_safe_area(),
                in("x0") regions,
                in("x20") self.controller,
                lateout("x1") _, lateout("x2") _, lateout("x3") _, lateout("x4") _,
                lateout("x5") _, lateout("x6") _, lateout("x7") _, lateout("x8") _,
                lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
                lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
                lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
            );
        }
    }

    /// The size SwiftUI would like this view to be.
    #[cfg(target_os = "macos")]
    pub fn fitting_size(&self) -> (f64, f64) {
        let s = unsafe { objc::send_ret_size(self.ptr, objc::sel(c"fittingSize")) };
        (s.width, s.height)
    }

    /// The size SwiftUI would like this view to be.
    ///
    /// `UIHostingController.sizeThatFits(in:)` rather than the view's own: only
    /// the controller asks SwiftUI. A zero target is UIKit's
    /// `layoutFittingCompressedSize`, i.e. "as small as you can be".
    #[cfg(not(target_os = "macos"))]
    pub fn fitting_size(&self) -> (f64, f64) {
        unsafe {
            let (w, h): (f64, f64);
            core::arch::asm!("blr {f}", f = in(reg) resolve::hosting_ctrl_size(),
                in("d0") 0.0f64, in("d1") 0.0f64,
                in("x20") self.controller,
                lateout("d0") w, lateout("d1") h,
                lateout("x0") _, lateout("x1") _, lateout("x2") _, lateout("x3") _,
                lateout("x4") _, lateout("x5") _, lateout("x6") _, lateout("x7") _,
                lateout("x8") _, lateout("x9") _, lateout("x10") _, lateout("x11") _,
                lateout("x12") _, lateout("x13") _, lateout("x14") _, lateout("x15") _,
                lateout("x16") _, lateout("x17") _, lateout("lr") _, lateout("x21") _,
                clobber_abi("C"),
            );
            (w, h)
        }
    }
}

impl HostingView {
    /// The offscreen window to capture this view in, created on first use and
    /// resized to `width` x `height`.
    ///
    /// # Safety
    /// Must be called on the main thread.
    pub(crate) unsafe fn offscreen(&self, width: f64, height: f64) -> *mut c_void {
        unsafe { offscreen_window(self, width, height) }
    }
}

impl Drop for HostingView {
    fn drop(&mut self) {
        let _pool = objc::AutoreleasePool::new();
        unsafe {
            let window = self.window.get();
            if !window.is_null() {
                #[cfg(target_os = "macos")]
                {
                    // Closing takes it out of the application's window list,
                    // which is the only thing keeping it alive. `release` alone
                    // frees nothing.
                    objc::send(window, objc::sel(c"close"));
                }
                #[cfg(not(target_os = "macos"))]
                {
                    // A `UIWindow` has no `close` — sending it one raises. It
                    // is hidden and released instead.
                    objc::send_bool(window, objc::sel(c"setHidden:"), true);
                    objc::send(window, objc::sel(c"release"));
                }
            }
        }
        // `init(rootView:)` hands back an owned reference. Anything this was
        // installed into — a window's content view, a window's root view
        // controller — took its own, so letting go of ours is safe and is the
        // difference between a render costing nothing and costing a hosting
        // view every time.
        unsafe {
            #[cfg(target_os = "macos")]
            objc::send(self.ptr, objc::sel(c"release"));
            #[cfg(not(target_os = "macos"))]
            objc::send(self.controller, objc::sel(c"release"));
        }
    }
}

/// Build `NSHostingView<AnyView>` and call `init(rootView:)` under the Swift CC.
#[cfg(target_os = "macos")]
unsafe fn make_hosting_view(view: &ViewHandle) -> *mut c_void {
    // `NSHostingView<V: View>` takes TWO key arguments: `V.Type` and the
    // `V: View` witness table. The class loads both straight back out of its own
    // metadata (`ldp x1, x2, [x20, #0x50]` in its init), so passing only
    // `V.Type` leaves the conformance slot holding register garbage — which
    // faults deep inside the Swift metadata cache rather than here.
    unsafe {
        let meta = abi::generic_metadata2(
            resolve::hosting_view_ma(),
            resolve::anyview_meta(),
            resolve::anyview_wt(),
        );
        init_root_view(resolve::hosting_view_init(), meta, view, "NSHostingView")
    }
}

/// Build `UIHostingController<AnyView>` and call `init(rootView:)`.
#[cfg(not(target_os = "macos"))]
unsafe fn make_hosting_controller(view: &ViewHandle) -> *mut c_void {
    unsafe {
        let meta = abi::generic_metadata2(
            resolve::hosting_ctrl_ma(),
            resolve::anyview_meta(),
            resolve::anyview_wt(),
        );
        init_root_view(resolve::hosting_ctrl_init(), meta, view, "UIHostingController")
    }
}

/// `init(rootView:)` — x0 = @in AnyView, x20 = the class's metadata.
unsafe fn init_root_view(
    init: *const c_void,
    meta: *const c_void,
    view: &ViewHandle,
    what: &str,
) -> *mut c_void {
    unsafe {
        let mut storage: u64 = view.to_anyview_word();
        swift_runtime_sys::RuntimeRaw::swift_retain(storage as *mut c_void);
        let out: *mut c_void;
        core::arch::asm!("blr {f}", f = in(reg) init,
            in("x0") &mut storage as *mut u64,
            in("x20") meta,
            lateout("x0") out,
            lateout("x1") _, lateout("x2") _, lateout("x3") _,
            lateout("x4") _, lateout("x5") _, lateout("x6") _, lateout("x7") _,
            lateout("x8") _, lateout("x9") _, lateout("x10") _, lateout("x11") _,
            lateout("x12") _, lateout("x13") _, lateout("x14") _, lateout("x15") _,
            lateout("x16") _, lateout("x17") _, lateout("lr") _, lateout("x21") _,
            clobber_abi("C"),
        );
        assert!(!out.is_null(), "{what} creation failed");
        out
    }
}

/// Make (once) and size the offscreen window a hosting view is captured in.
///
/// Borderless and buffered, so the content rect is the whole window, and pinned
/// to the light appearance so a capture does not depend on the host's system
/// setting.
#[cfg(target_os = "macos")]
unsafe fn offscreen_window(hv: &HostingView, width: f64, height: f64) -> *mut c_void {
    unsafe {
        let bounds = NSRect::new(0.0, 0.0, width, height);
        let existing = hv.window.get();
        if !existing.is_null() {
            type SetFrame = unsafe extern "C" fn(*mut c_void, *const c_void, NSRect, bool);
            let set_frame: SetFrame = core::mem::transmute(crate::objc::msg_send_addr());
            set_frame(existing, objc::sel(c"setFrame:display:"), bounds, false);
            return existing;
        }

        let _ = objc::shared_app();
        let win = objc::send(objc::class(c"OBJC_CLASS_$_NSWindow"), objc::sel(c"alloc"));
        type InitWin =
            unsafe extern "C" fn(*mut c_void, *const c_void, NSRect, u64, u64, bool) -> *mut c_void;
        let init: InitWin = core::mem::transmute(crate::objc::msg_send_addr());
        // borderless | buffered
        let win = init(
            win,
            objc::sel(c"initWithContentRect:styleMask:backing:defer:"),
            bounds,
            0,
            2,
            false,
        );
        let name = objc::nsstring("NSAppearanceNameAqua");
        let appearance = objc::send_id(
            objc::class(c"OBJC_CLASS_$_NSAppearance"),
            objc::sel(c"appearanceNamed:"),
            name,
        );
        if !appearance.is_null() {
            objc::send_id(win, objc::sel(c"setAppearance:"), appearance);
        }
        objc::send_id(win, objc::sel(c"setContentView:"), hv.as_ptr());
        hv.window.set(win);
        win
    }
}

/// UIKit's equivalent: a window purely so the view gets laid out.
#[cfg(not(target_os = "macos"))]
unsafe fn offscreen_window(hv: &HostingView, width: f64, height: f64) -> *mut c_void {
    unsafe {
        let bounds = NSRect::new(0.0, 0.0, width, height);
        let existing = hv.window.get();
        if !existing.is_null() {
            objc::send_rect(existing, objc::sel(c"setFrame:"), bounds);
            return existing;
        }
        let window = objc::send(objc::class(c"OBJC_CLASS_$_UIWindow"), objc::sel(c"alloc"));
        type InitFrame = unsafe extern "C" fn(*mut c_void, *const c_void, NSRect) -> *mut c_void;
        let init: InitFrame = core::mem::transmute(crate::objc::msg_send_addr());
        let window = init(window, objc::sel(c"initWithFrame:"), bounds);
        // The capture is of the view at exactly the size asked for, so the
        // window's safe area must not push the content around.
        hv.set_safe_area(false);
        // As a subview, not as `rootViewController`: a root view controller is
        // laid out inside the window's safe area, which on a device with a
        // notch inserts a band at the top that has nothing to do with the view
        // under test.
        objc::send_id(window, objc::sel(c"addSubview:"), hv.as_ptr());
        // Not `makeKeyAndVisible`: the window only has to exist for layout, and
        // a process spawned without an application should not try to present.
        objc::send_bool(window, objc::sel(c"setHidden:"), false);
        hv.window.set(window);
        window
    }
}