swiftui-native 0.0.1

SwiftUI from pure Rust — no Swift bridge, no shim, only swift-runtime calls
Documentation
//! A whole macOS app — several scenes and a menu bar — from pure Rust.
//!
//! `examples/app.rs` is one `WindowGroup`. This is what an app actually is on
//! macOS: a main window, a preferences pane behind ⌘,, a second window that
//! `openWindow` can reach, an item in the system menu bar, and a menu bar with
//! a menu of its own plus items grafted into File and Help.
//!
//! Every one of those is SwiftUI's own scene type, built by calling its own
//! initialiser. `_TupleScene` and `TupleCommandContent` — what `SceneBuilder`
//! and `CommandsBuilder` produce, and what writing several scenes in a row
//! reduces to — are built the same way; both builders are
//! `@_alwaysEmitIntoClient` and export no symbol at all, so the binding calls
//! what their bodies call.
//!
//! ```bash
//! cargo run -p swiftui-native --example scenes
//!
//! # print what was built and exit, without entering the event loop
//! cargo run -p swiftui-native --example scenes -- --describe
//! ```
//!
//! Things to try once it is up: ⌘, opens Settings; the star in the menu bar has
//! a menu; the menu bar has a "Counter" menu of its own; File's New item has
//! been replaced; Help has an extra entry.

// This example builds for both platforms and each has a half the other does not:
// `Settings`, `Window`, `MenuBarExtra` and every command are macOS-only, so the
// views that feed them have no caller on iOS. Keeping them is what makes it
// visible *which* of the five features survive the crossing — see `ios_body`.
#![allow(dead_code)]

use swiftui_native::prelude::*;
use swiftui_native::scene::{self, SceneHandle};
// Only the macOS body uses the macro — the iOS one has no scene to declare.
#[cfg(target_os = "macos")]
use swiftui_native::scenes;
use swiftui_native::views::{BoundState, ViewHandle};
use swiftui_native::views;
use swiftui_native::app;

/// Shared between the windows and the menus, so the menu-bar item and the main
/// window show the same number and the menu's buttons move it.
fn counter() -> &'static BoundState<i64> {
    static C: std::sync::OnceLock<&'static BoundState<i64>> = std::sync::OnceLock::new();
    C.get_or_init(|| Box::leak(Box::new(BoundState::new(0))))
}

fn main_window() -> ViewHandle {
    vstack(&[
        text("A SwiftUI app, no Swift source")
            .font_size(20.0)
            .padding(8.0),
        divider(),
        text(&format!("count: {}", counter().get()))
            .font_size(34.0)
            .padding(12.0),
        hstack(&[
            button("−", || counter().set(counter().get() - 1)),
            button("+", || counter().set(counter().get() + 1)),
        ]),
        spacer(),
        text("⌘, for Settings · the ★ in the menu bar has a menu")
            .font_size(11.0)
            .opacity(0.6),
    ])
    .padding(24.0)
    .frame(420.0, 300.0)
    .focused_scene_value(counter_focus_key(), counter().get())
    .into_handle()
}

/// The focused-value key the window publishes its count under.
///
/// `.focusedSceneValue(\.key, value)` is how a window tells the commands above
/// it what is selected. Both halves are synthesised at run time: the
/// `FocusedValueKey` conformance *and* the `WritableKeyPath<FocusedValues, Int?>`
/// — `FocusedValues` exports no properties, so unlike `EnvironmentValues` there
/// is no SwiftUI modifier holding a key path to borrow.
///
/// The menu cannot *read* it yet: that needs `@FocusedValue`, a `DynamicProperty`
/// SwiftUI instantiates itself, which is a different mechanism from anything
/// here. What this demonstrates is the publishing half, in a real app.
fn counter_focus_key() -> &'static views::FocusKey {
    static K: std::sync::OnceLock<views::FocusKey> = std::sync::OnceLock::new();
    K.get_or_init(|| views::focus_key("sceneCounter"))
}

fn settings_pane() -> ViewHandle {
    vstack(&[
        text("Preferences").font_size(18.0),
        divider(),
        text("This pane is a `Settings` scene — the one ⌘, opens."),
        text(&format!("It sees the same state: {}", counter().get()))
            .opacity(0.7),
    ])
    .padding(24.0)
    .frame(360.0, 200.0)
    .into_handle()
}

fn utility_window() -> ViewHandle {
    vstack(&[
        text("Utility").font_size(16.0),
        text("A `Window`, not a `WindowGroup`: one of it, addressed by id.")
            .opacity(0.7),
    ])
    .padding(20.0)
    .frame(320.0, 140.0)
    .into_handle()
}

fn menu_bar_item() -> ViewHandle {
    vstack(&[
        text(&format!("count: {}", counter().get())),
        button("Reset", || counter().set(0)),
    ])
    .padding(10.0)
    .into_handle()
}

/// The whole app, as one scene.
///
/// Built afresh every time SwiftUI reads `App.body`, which is what makes the
/// numbers in it live rather than a snapshot taken at launch.
///
/// macOS only, and not by choice: `Settings`, `Window`, `MenuBarExtra` and the
/// whole of `Commands` are `@available(iOS, unavailable)`. iOS gets
/// [`ios_body`], which is the part of this that exists there.
#[cfg(target_os = "macos")]
fn body() -> SceneHandle {
    scenes! {
        window_group { main_window() }
        settings { settings_pane() }
        window("Utility", "utility") { utility_window() }
        menu_bar_extra("Count", "star.fill") { menu_bar_item() }
        commands {
            // A menu of its own, beside File and Edit.
            menu("Counter") {
                vstack(&[
                    button("Increment", || counter().set(counter().get() + 1)),
                    button("Decrement", || counter().set(counter().get() - 1)),
                    divider(),
                    button("Reset", || counter().set(0)),
                ])
            }
            // In place of the standard File ▸ New.
            replacing(NewItem) { button("New Count", || counter().set(0)) }
            // Added after the standard Help entry.
            after(Help) {
                button("About this Rust app", || {
                    eprintln!("built with no Swift compiler in sight");
                })
            }
        }
    }
}

/// What iOS has of the app above: one `WindowGroup`.
///
/// Kept beside the macOS body rather than left out, so the same example builds
/// for both and it is visible *which* of the five features survive the crossing.
#[cfg(not(target_os = "macos"))]
fn body() -> SceneHandle {
    scene::window_group(main_window)
}

// ── Verification, without a screen ──
//
// "It launched and did not crash" is weak evidence that four scenes and three
// menus were built. This asks AppKit instead: a little after the run loop
// starts, walk `NSApp.windows` and `NSApp.mainMenu` and print what SwiftUI
// actually made of the Rust-built scenes, then quit. No display needed, which
// is what makes it usable from a terminal or CI.
//
// The inspection has to run on the main thread — AppKit is not thread-safe, and
// a background thread reading a menu that is being built is a race — so it goes
// on the main queue through `dispatch_after_f` rather than on a `std::thread`.
#[cfg(target_os = "macos")]
mod probe {
    use core::ffi::c_void;
    use swiftui_native::objc;

    unsafe extern "C" {
        fn dispatch_time(base: u64, delta: i64) -> u64;
        fn dispatch_after_f(
            when: u64,
            queue: *const c_void,
            ctx: *mut c_void,
            work: unsafe extern "C" fn(*mut c_void),
        );
        /// `dispatch_get_main_queue()` is this symbol's address.
        static _dispatch_main_q: u8;
    }

    /// Queue the inspection for `seconds` from now.
    pub fn schedule(seconds: f64) {
        unsafe {
            let when = dispatch_time(0, (seconds * 1e9) as i64);
            dispatch_after_f(
                when,
                &_dispatch_main_q as *const u8 as *const c_void,
                core::ptr::null_mut(),
                inspect,
            );
        }
    }

    /// An `NSString`'s bytes, as Rust sees them.
    unsafe fn text(ns: *mut c_void) -> String {
        if ns.is_null() {
            return String::new();
        }
        let p = unsafe { objc::send_ret_ptr(ns, objc::sel(c"UTF8String")) };
        if p.is_null() {
            return String::new();
        }
        unsafe { core::ffi::CStr::from_ptr(p as *const core::ffi::c_char) }
            .to_string_lossy()
            .into_owned()
    }

    /// `array[i]`, for an `NSArray`.
    unsafe fn at(array: *mut c_void, i: u64) -> *mut c_void {
        unsafe { objc::send_u64(array, objc::sel(c"objectAtIndex:"), i) }
    }

    unsafe fn count(array: *mut c_void) -> u64 {
        if array.is_null() {
            0
        } else {
            unsafe { objc::send_ret_u64(array, objc::sel(c"count")) }
        }
    }

    unsafe extern "C" fn inspect(_: *mut c_void) {
        unsafe {
            let app = objc::shared_app();

            let windows = objc::send(app, objc::sel(c"windows"));
            let n = count(windows);
            println!("windows: {n}");
            for i in 0..n {
                let w = at(windows, i);
                let title = text(objc::send(w, objc::sel(c"title")));
                let visible = objc::send_ret_bool(w, objc::sel(c"isVisible"));
                let class = objc::send(w, objc::sel(c"class"));
                let kind = text(objc::send(class, objc::sel(c"description")));
                println!("  {kind:<24} visible={visible:<5} {title:?}");
            }

            let menu = objc::send(app, objc::sel(c"mainMenu"));
            let items = objc::send(menu, objc::sel(c"itemArray"));
            let n = count(items);
            println!("menu bar: {n} menus");
            for i in 0..n {
                let item = at(items, i);
                let title = text(objc::send(item, objc::sel(c"title")));
                let sub = objc::send(item, objc::sel(c"submenu"));
                let kids = objc::send(sub, objc::sel(c"itemArray"));
                let entries: Vec<String> = (0..count(kids))
                    .map(|j| text(objc::send(at(kids, j), objc::sel(c"title"))))
                    .filter(|t| !t.is_empty())
                    .collect();
                println!("  {title:?}: {}", entries.join(" · "));
            }

            // The menu-bar extra is an `NSStatusItem`, not a window or a menu,
            // so it is counted separately or it would look like nothing was
            // built for it.
            let bar = objc::class(c"OBJC_CLASS_$_NSStatusBar");
            let system = objc::send(bar, objc::sel(c"systemStatusBar"));
            println!(
                "status bar thickness: {}",
                objc::send_ret_f64(system, objc::sel(c"thickness")),
            );

            std::process::exit(0);
        }
    }
}

fn main() {
    // `--describe` asks the same question the event loop would and prints the
    // answer: what Swift types came out of this. Useful because the types are
    // the claim — `_TupleScene<(WindowGroup<AnyView>, Settings<AnyView>, …)>` is
    // what Swift builds from four scenes in a row, and nothing about a window
    // appearing would say whether that is what got built.
    #[cfg(target_os = "macos")]
    if std::env::args().any(|a| a == "--describe") {
        let whole = body();
        println!("App.Body\n  {}", whole.type_name());
        for (name, s) in [
            ("window_group", scene::window_group(main_window)),
            ("settings", scene::settings(settings_pane)),
            ("window", scene::window("Utility", "utility", utility_window)),
            ("menu_bar_extra", scene::menu_bar_extra("Count", "star.fill", menu_bar_item)),
        ] {
            println!("{name}\n  {}", s.type_name());
        }
        // Built through the macro, so this also checks that what `scenes!`
        // expands to is the same thing the free functions build.
        let one = scenes! { window_group { text("x") } commands { menu("M") { text("x") } } };
        println!("scenes! with one menu\n  {}", one.type_name());
        return;
    }

    // `--probe` launches for real and reports what AppKit built, then quits.
    #[cfg(target_os = "macos")]
    if std::env::args().any(|a| a == "--probe") {
        probe::schedule(2.5);
    }

    app::run_scene(body)
}