swiftui-native 0.0.1

SwiftUI from pure Rust — no Swift bridge, no shim, only swift-runtime calls
Documentation
//! The SwiftUI tutorial from swift.org, built from Rust.
//!
//! <https://www.swift.org/getting-started/swiftui/>
//!
//! The Swift it reproduces, in full:
//!
//! ```swift
//! struct ContentView: View {
//!     var activities = ["Archery", "Baseball", …]
//!     var colors: [Color] = [.blue, .cyan, …]
//!     @State private var selected = "Baseball"
//!     @State private var id = 1
//!
//!     var body: some View {
//!         VStack {
//!             Text("Why not try…")
//!                 .font(.largeTitle.bold())
//!             VStack {
//!                 Circle()
//!                     .fill(colors.randomElement() ?? .blue)
//!                     .padding()
//!                     .overlay(
//!                         Image(systemName: "figure.\(selected.lowercased())")
//!                             .font(.system(size: 144))
//!                             .foregroundColor(.white)
//!                     )
//!                 Text("\(selected)!")
//!                     .font(.title)
//!             }
//!             .transition(.slide)
//!             .id(id)
//!             Spacer()
//!             Button("Try again") {
//!                 withAnimation(.easeInOut(duration: 1)) {
//!                     selected = activities.randomElement() ?? "Archery"
//!                     id += 1
//!                 }
//!             }
//!             .buttonStyle(.borderedProminent)
//!         }
//!     }
//! }
//! ```
//!
//! Every line of that has a counterpart below, and nothing is approximated: the
//! `@State` is real `@State` on a type synthesised at run time, the transition
//! is `AnyTransition.slide`, and the button's action runs
//! `withAnimation(.easeInOut(duration: 1))` through SwiftUI's own entry point.
//!
//! ```sh
//! cargo run -p swiftui-native --example swift_org_tutorial            # macOS
//! cargo build -p swiftui-native --example swift_org_tutorial \
//!     --target aarch64-apple-ios-sim                                  # iOS
//! ```

use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering};
use swiftui_native::prelude::*;
use swiftui_native::state;
use swiftui_native::views::{ButtonStyle, TextStyle};

// ── The model ───────────────────────────────────────────────────────────────
//
// `var activities` and `var colors` are stored properties of the Swift struct.
// Here they are plain Rust data, which is the point: the state lives in Rust
// and SwiftUI reads it when it evaluates the body.

const ACTIVITIES: &[&str] = &[
    "Archery", "Baseball", "Basketball", "Bowling", "Boxing", "Cricket", "Curling", "Fencing",
    "Golf", "Hiking", "Lacrosse", "Rugby", "Squash",
];

/// `var colors: [Color] = [.blue, .cyan, .gray, .green, .indigo, .mint,
/// .orange, .pink, .purple, .red]`.
///
/// SwiftUI's own colours, asked for by name. They are not fixed triples —
/// `.blue` differs between macOS and iOS and again in dark appearance — so
/// naming them is both shorter and more correct than spelling out components.
const COLORS: &[SystemColor] = &[
    SystemColor::Blue,
    SystemColor::Cyan,
    SystemColor::Gray,
    SystemColor::Green,
    SystemColor::Indigo,
    SystemColor::Mint,
    SystemColor::Orange,
    SystemColor::Pink,
    SystemColor::Purple,
    SystemColor::Red,
];

// ── `@State private var selected` and `@State private var id` ───────────────
//
// Two pieces of state. `id` is the one SwiftUI tracks — bumping it is what
// re-runs the body and what `.id()` keys the transition on. `selected` is
// Rust's, read back every time the body runs.

/// `@State private var selected = "Baseball"` — held as an index into
/// [`ACTIVITIES`] so it is one word to write.
static SELECTED: AtomicUsize = AtomicUsize::new(1);

/// `@State private var id = 1`.
static ID: AtomicI64 = AtomicI64::new(1);

/// Which colour the circle is drawn in. `colors.randomElement()` is evaluated
/// every time the body runs, so this changes with the rest.
static COLOR: AtomicUsize = AtomicUsize::new(0);

fn selected() -> &'static str {
    ACTIVITIES[SELECTED.load(Ordering::Relaxed) % ACTIVITIES.len()]
}

/// Swift's `randomElement()`. A tiny xorshift rather than a dependency — the
/// tutorial only needs "a different one than last time".
fn random(upper: usize) -> usize {
    static SEED: AtomicUsize = AtomicUsize::new(0x2545_F491_4F6C_DD1D);
    let mut x = SEED.load(Ordering::Relaxed);
    x ^= x << 13;
    x ^= x >> 7;
    x ^= x << 17;
    SEED.store(x, Ordering::Relaxed);
    x % upper
}

// ── `var body: some View` ───────────────────────────────────────────────────
//
// Read this against the Swift at the top of the file. The shape of it is the
// same: a value, then modifiers on it, nested the way the braces nest.

fn body() -> View {
    let colour = COLORS[COLOR.load(Ordering::Relaxed) % COLORS.len()];

    vstack![
        // Text("Why not try…").font(.largeTitle.bold())
        text("Why not try…").text_style(TextStyle::LargeTitle).bold(),
        vstack![
            // Circle().fill(colors.randomElement() ?? .blue).padding().overlay(…)
            circle_shape().fill_system(colour).padding(16.0).overlay(
                // Image(systemName: "figure.\(selected.lowercased())")
                //     .font(.system(size: 144)).foregroundColor(.white)
                image!("figure.{}", selected().to_lowercase())
                    .font_size(144.0)
                    .fg(Color::WHITE),
            ),
            // Text("\(selected)!").font(.title)
            text!("{}!", selected()).text_style(TextStyle::Title),
        ]
        .transition(Transition::Slide)
        .id(ID.load(Ordering::Relaxed)),
        spacer(),
        // Button("Try again") { … }.buttonStyle(.borderedProminent)
        button("Try again", try_again).button_style(ButtonStyle::BorderedProminent),
    ]
}

/// The button's action:
///
/// ```swift
/// withAnimation(.easeInOut(duration: 1)) {
///     selected = activities.randomElement() ?? "Archery"
///     id += 1
/// }
/// ```
fn try_again() {
    SELECTED.store(random(ACTIVITIES.len()), Ordering::Relaxed);
    COLOR.store(random(COLORS.len()), Ordering::Relaxed);
    ID.fetch_add(1, Ordering::Relaxed);
    // Writing the `@State` inside `withAnimation` is what makes SwiftUI
    // interpolate: the body re-runs, `.id()` sees a new identity, and the old
    // card slides out while the new one slides in.
    state::refresh_animated(state::ease_in_out(1.0));
}

/// `ContentView` itself: a Swift type with real `@State`, synthesised at run
/// time, whose body is the Rust function above.
fn content_view() -> swiftui_native::ViewHandle {
    state::root_view(|_generation| body().into_handle())
}

/// Laying the bundle out and putting it on a simulator or a device.
#[cfg(target_os = "macos")]
#[path = "common/simulator.rs"]
mod simulator;

#[cfg(target_os = "macos")]
fn main() {
    // `--sim` and `--device` build for iOS and run it there; see `common/simulator.rs`.
    if std::env::args().any(|a| a == "--sim" || a == "device" || a == "ipa") {
        return simulator::main(&simulator::Bundle {
            example: "swift_org_tutorial",
            bundle_id: "dev.rswift.WhyNotTry",
            executable: "WhyNotTry",
            display_name: "Why not try…",
            launch_args: &[],
        });
    }
    // `--render` draws one frame to a PNG instead of opening a window, so the
    // result can be looked at without a display.
    if std::env::args().any(|a| a == "--render") {
        let out = concat!(env!("CARGO_MANIFEST_DIR"), "/../../target/tutorial.png");
        let frame = swiftui_native::render::render(&content_view(), 420, 640);
        frame.write_png(out);
        eprintln!("wrote {out} ({} colours)", frame.distinct());
        return;
    }
    // `WindowGroup { ContentView() }` — the same tree the tutorial's `App` hosts.
    show_window(&content_view(), "Why not try…", 420.0, 640.0);
}

#[cfg(not(target_os = "macos"))]
fn main() {
    swiftui_native::app::run(content_view)
}