swiftui-native 0.0.1

SwiftUI from pure Rust — no Swift bridge, no shim, only swift-runtime calls
Documentation
//! Window hosting — pure Rust.
//!
//! `NSWindow` on the Mac, `UIWindow` on iOS. The shapes differ: an `NSWindow`
//! takes a content *view*, a `UIWindow` takes a root view *controller*, and
//! only AppKit has a title bar to put a title in.
//!
//! - `NSHostingView.init(rootView:)` via **Swift CC** (see [`crate::hosting`])
//! - `NSWindow` / `NSApplication` via the **ObjC runtime** (see [`crate::objc`])
//!
//! Both paths use the same dlsym + asm approach — no Swift source code.

use crate::hosting::HostingView;
use crate::objc::{self, NSRect};
use crate::views::ViewHandle;
use core::ffi::c_void;

/// A native window hosting a SwiftUI view built from Rust.
///
/// The root view can be replaced at any time with [`Window::set_root`], which is
/// how a Rust-driven interface updates after it is already on screen.
pub struct Window {
    window: *mut c_void,
    hosting: HostingView,
}

#[cfg(target_os = "macos")]
impl Window {
    /// Create and show a window. Does not start the event loop.
    pub fn new(view: &ViewHandle, title: &str, width: f64, height: f64) -> Self {
        unsafe {
            let hosting = HostingView::new(view);
            let app = 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(objc::msg_send_addr());
            let win = init(
                win,
                objc::sel(c"initWithContentRect:styleMask:backing:defer:"),
                NSRect::new(0.0, 0.0, width, height),
                1 | 2 | 4 | 8, // titled | closable | miniaturizable | resizable
                2,             // buffered
                false,
            );

            objc::send_id(win, objc::sel(c"setTitle:"), objc::nsstring(title));
            objc::send_id(win, objc::sel(c"setContentView:"), hosting.as_ptr());
            objc::send(win, objc::sel(c"center"));
            objc::send_id(win, objc::sel(c"makeKeyAndOrderFront:"), core::ptr::null_mut());

            objc::send_i64(app, objc::sel(c"setActivationPolicy:"), 0);
            objc::send_bool(app, objc::sel(c"activateIgnoringOtherApps:"), true);

            Self { window: win, hosting }
        }
    }

    /// Replace the displayed view. SwiftUI diffs it against the previous tree.
    pub fn set_root(&self, view: &ViewHandle) {
        self.hosting.set_root(view);
    }

    /// The hosting view, for rendering or further configuration.
    pub fn hosting(&self) -> &HostingView {
        &self.hosting
    }

    pub fn as_ptr(&self) -> *mut c_void {
        self.window
    }

    /// Enter the AppKit event loop. Never returns.
    pub fn run(&self) -> ! {
        unsafe {
            let app = objc::shared_app();
            objc::send(app, objc::sel(c"run"));
        }
        std::process::exit(0)
    }
}

#[cfg(not(target_os = "macos"))]
impl Window {
    /// Create and show a window. Does not start the run loop.
    ///
    /// `title` is unused: a `UIWindow` has no title bar to put it in. It stays
    /// in the signature so a caller can be written once for both platforms.
    pub fn new(view: &ViewHandle, title: &str, width: f64, height: f64) -> Self {
        let _ = title;
        unsafe {
            let hosting = HostingView::new(view);
            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(objc::msg_send_addr());
            let window = init(
                window,
                objc::sel(c"initWithFrame:"),
                NSRect::new(0.0, 0.0, width, height),
            );
            objc::send_id(
                window,
                objc::sel(c"setRootViewController:"),
                hosting.controller(),
            );
            objc::send(window, objc::sel(c"makeKeyAndVisible"));
            Self { window, hosting }
        }
    }

    /// Replace the displayed view. SwiftUI diffs it against the previous tree.
    pub fn set_root(&self, view: &ViewHandle) {
        self.hosting.set_root(view);
    }

    /// The hosting view, for rendering or further configuration.
    pub fn hosting(&self) -> &HostingView {
        &self.hosting
    }

    pub fn as_ptr(&self) -> *mut c_void {
        self.window
    }

    /// Enter the UIKit event loop. Never returns.
    ///
    /// `UIApplicationMain` will not start without a delegate *class* to name,
    /// and it has to be a real Objective-C class — so one is built at runtime
    /// and its `application:didFinishLaunchingWithOptions:` puts this window on
    /// screen. See [`run_app`].
    ///
    /// Already inside a running application? Then there is no loop to enter:
    /// the window went on screen when it was created, and this just turns the
    /// run loop.
    pub fn run(&self) -> ! {
        unsafe {
            #[cfg(feature = "uikit-entry")]
            if objc::shared_app().is_null() {
                run_app(self.hosting.controller());
            }
            // An application is already running; keep this thread alive.
            unsafe extern "C" {
                fn CFRunLoopRun();
            }
            CFRunLoopRun();
        }
        std::process::exit(0)
    }
}

/// The root view controller the synthesised delegate installs on launch.
///
/// Gated on `uikit-entry` like the two functions that touch it: without the
/// feature there is no UIKit entry point to hand a root to, and the plain
/// `not(macos)` gate left it dead in the default iOS build.
#[cfg(all(not(target_os = "macos"), feature = "uikit-entry"))]
static PENDING_ROOT: std::sync::atomic::AtomicPtr<c_void> =
    std::sync::atomic::AtomicPtr::new(core::ptr::null_mut());

/// Start a UIKit application whose root view controller is `controller`.
///
/// UIKit's entry point takes the *name* of a delegate class and instantiates it
/// itself, so there has to be a class to name. Rather than ship an Objective-C
/// source file for it, the class is defined through the runtime — the same move
/// as synthesising a Swift type in [`crate::state`], one runtime down.
///
/// Never returns.
///
/// # Safety
/// `controller` must be a live `UIViewController`, and this must be called from
/// the main thread of a process that is not already running an application.
#[cfg(all(not(target_os = "macos"), feature = "uikit-entry"))]
pub unsafe fn run_app(controller: *mut c_void) -> ! {
    use std::sync::atomic::Ordering;
    unsafe extern "C" {
        fn UIApplicationMain(
            argc: i32,
            argv: *const *const core::ffi::c_char,
            principal: *mut c_void,
            delegate: *mut c_void,
        ) -> i32;
    }

    /// `-[RustAppDelegate application:didFinishLaunchingWithOptions:]`
    unsafe extern "C" fn did_finish_launching(
        this: *mut c_void,
        _cmd: *const c_void,
        _app: *mut c_void,
        _options: *mut c_void,
    ) -> bool {
        unsafe {
            let screen = objc::send(
                objc::class(c"OBJC_CLASS_$_UIScreen"),
                objc::sel(c"mainScreen"),
            );
            let bounds = objc::send_ret_rect(screen, objc::sel(c"bounds"));

            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, objc::NSRect) -> *mut c_void;
            let init: InitFrame = core::mem::transmute(objc::msg_send_addr());
            let window = init(window, objc::sel(c"initWithFrame:"), bounds);

            let root = PENDING_ROOT.load(Ordering::Acquire);
            if !root.is_null() {
                objc::send_id(window, objc::sel(c"setRootViewController:"), root);
            }
            objc::send(window, objc::sel(c"makeKeyAndVisible"));
            // Nothing here runs under ARC, so the window is still at the +1 it
            // came out of `alloc` with. Parking it in a static keeps that
            // reference owned by something, rather than merely leaked.
            let _ = this;
            WINDOW.store(window, Ordering::Release);
            true
        }
    }

    static WINDOW: std::sync::atomic::AtomicPtr<c_void> =
        std::sync::atomic::AtomicPtr::new(core::ptr::null_mut());

    unsafe {
        PENDING_ROOT.store(controller, Ordering::Release);

        const DELEGATE: &core::ffi::CStr = c"RustAppDelegate";
        if objc::class_named(DELEGATE).is_null() {
            objc::ClassBuilder::new(c"UIResponder", DELEGATE)
                .expect("RustAppDelegate")
                // `B@:@@` — returns BOOL, takes the application and a
                // launch-options dictionary after the implicit receiver and
                // selector.
                .method(
                    c"application:didFinishLaunchingWithOptions:",
                    did_finish_launching as *const c_void,
                    c"B@:@@",
                )
                .conforms(c"UIApplicationDelegate")
                .register();
        }

        let name = objc::nsstring("RustAppDelegate");
        let args: Vec<*const core::ffi::c_char> = std::env::args()
            .map(|a| {
                std::ffi::CString::new(a).unwrap_or_default().into_raw() as *const core::ffi::c_char
            })
            .collect();
        UIApplicationMain(
            args.len() as i32,
            args.as_ptr(),
            core::ptr::null_mut(),
            name,
        );
    }
    std::process::exit(0)
}

/// Show a view in a native window and run the event loop. Never returns.
#[cfg(target_os = "macos")]
pub fn show_window(view: &ViewHandle, title: &str, width: f64, height: f64) {
    Window::new(view, title, width, height).run()
}

/// Show a view in a native window and run the event loop. Never returns.
///
/// # One window, made by the delegate
///
/// This deliberately does *not* go through [`Window::new`]. That would build a
/// `UIWindow` and send it `makeKeyAndVisible` here — before `UIApplicationMain`
/// has run, so before UIKit is initialised — and the synthesised delegate then
/// builds a second window and gives it the same root view controller on launch.
///
/// A controller can only be the root of one window: UIKit moves its view to the
/// second, which is what gets drawn. The first stays visible and key, and since
/// hit-testing starts at the key window, every touch lands in a window with
/// nothing in it. The application renders correctly and ignores input — no
/// button, no toggle, no slider responds.
///
/// So only the controller is built here. The delegate makes the one window.
#[cfg(all(not(target_os = "macos"), feature = "uikit-entry"))]
pub fn show_window(view: &ViewHandle, title: &str, width: f64, height: f64) {
    use std::sync::atomic::Ordering;
    let _ = (title, width, height);
    let hosting = HostingView::new(view);
    let controller = hosting.controller();
    // Kept, not forgotten: [`set_root`] needs it to push a new tree later.
    let hosting: &'static HostingView = Box::leak(Box::new(hosting));
    HOSTING.store(hosting as *const HostingView as *mut c_void, Ordering::Release);
    unsafe { run_app(controller) }
}

/// The hosting view of the running application, for [`set_root`].
#[cfg(not(target_os = "macos"))]
static HOSTING: std::sync::atomic::AtomicPtr<c_void> =
    std::sync::atomic::AtomicPtr::new(core::ptr::null_mut());

/// Replace the running application's view tree.
///
/// SwiftUI cannot observe a Rust value: a `Binding` reads its getter only when
/// something else causes the body to be evaluated, so a state change made in
/// Rust is invisible until the tree is handed over again. This is that handover
/// — rebuild the view from the new state and give it to the hosting controller,
/// which diffs it as SwiftUI would diff any other update.
///
/// Must run on the main thread; see [`on_main`].
#[cfg(not(target_os = "macos"))]
pub fn set_root(view: &ViewHandle) {
    use std::sync::atomic::Ordering;
    let p = HOSTING.load(Ordering::Acquire);
    if p.is_null() {
        return;
    }
    unsafe { (*(p as *const HostingView)).set_root(view) }
}

/// Run `f` on the main thread.
///
/// UIKit and SwiftUI are main-thread only, and Rust state can be written from
/// anywhere. `dispatch_async_f` is the function-pointer form of `dispatch_async`
/// — no block, so nothing here has to synthesise one.
#[cfg(not(target_os = "macos"))]
pub fn on_main(f: unsafe extern "C" fn(*mut c_void), ctx: *mut c_void) {
    unsafe extern "C" {
        fn dispatch_async_f(queue: *mut c_void, ctx: *mut c_void, work: unsafe extern "C" fn(*mut c_void));
        /// `dispatch_get_main_queue()` is a macro over the address of this.
        static _dispatch_main_q: c_void;
    }
    unsafe {
        dispatch_async_f(
            &raw const _dispatch_main_q as *mut c_void,
            ctx,
            f,
        );
    }
}