swiftui-native 0.0.1

SwiftUI from pure Rust — no Swift bridge, no shim, only swift-runtime calls
Documentation
//! A settings screen, written the way someone would actually write one.
//!
//! Not a feature list — a screen with state that has to survive being edited
//! from both sides. Every control here is a *bound* form, so what the user does
//! lands in Rust and what Rust does shows on screen.
//!
//! ```sh
//! cargo run -p swiftui-native --example settings           # macOS
//! cargo run -p swiftui-native --example settings -- --sim  # iOS simulator
//! ```

use std::sync::OnceLock;
use swiftui_native::prelude::*;
use swiftui_native::{state, views};

/// The whole of the app's state, on the Rust side.
///
/// `BoundState` is what a SwiftUI `Binding` reads and writes, so these are not
/// copies kept in step — they are the storage the controls edit.
struct Settings {
    name: views::BoundState<String>,
    notify: views::BoundState<bool>,
    volume: views::BoundState<f64>,
    theme: views::BoundState<i64>,
    refresh: views::BoundState<f64>,
    about: views::BoundState<bool>,
}

fn settings() -> &'static Settings {
    static S: OnceLock<Settings> = OnceLock::new();
    S.get_or_init(|| Settings {
        name: views::BoundState::new("Ada".into()),
        notify: views::BoundState::new(true),
        volume: views::BoundState::new(0.6),
        theme: views::BoundState::new(0),
        refresh: views::BoundState::new(5.0),
        about: views::BoundState::new(false),
    })
}

const THEMES: [&str; 3] = ["System", "Light", "Dark"];

fn body() -> View {
    let s = settings();

    form![
        section![header: text("Account");
            hstack![
                text("Name"),
                spacer(),
                textfield_bound("Your name", &s.name).text_field_style(
                    views::TextFieldStyle::RoundedBorderTextFieldStyle,
                ),
            ],
            toggle_bound("Notifications", &s.notify),
        ],
        section![header: text("Playback");
            vstack_aligned(&[
                hstack![
                    text("Volume"),
                    spacer(),
                    text!("{:.0}%", s.volume.get() * 100.0).fg_system(SystemColor::Gray),
                ],
                slider_bound(&s.volume, 0.0, 1.0),
            ], HorizontalAlignment::Leading),
            stepper_bound("Refresh every", &s.refresh, 1.0, 60.0, 1.0),
        ],
        section![header: text("Appearance");
            picker_bound!(
                "Theme",
                &s.theme,
                text(THEMES[0]).tag_int(0),
                text(THEMES[1]).tag_int(1),
                text(THEMES[2]).tag_int(2),
            )
            .picker_style(PickerStyle::InlinePickerStyle),
        ],
        section![
            button("About", show_about)
                .a11y_hint("Opens a panel describing this app"),
        ],
    ]
    .form_style(FormStyle::GroupedFormStyle)
    .sheet_bound(&s.about, about_panel())
}

/// The sheet, which the user can dismiss — the flag is Rust's, so closing it
/// writes `false` back rather than leaving the two sides disagreeing.
fn about_panel() -> View {
    let s = settings();
    vstack_spacing(
        &[
            system_image("gearshape.fill")
                .symbol_rendering_mode(Some(views::SymbolRenderingMode::Hierarchical))
                .view()
                .font_size(48.0)
                .fg_system(SystemColor::Blue),
            text("Settings").text_style(TextStyle::Title).bold(),
            text!("Signed in as {}", s.name.get()).fg_system(SystemColor::Gray),
            spacer(),
            button("Done", hide_about).button_style(views::ButtonStyle::BorderedProminent),
        ],
        16.0,
    )
    .padding(24.0)
    .presentation_detents(&[views::PresentationDetent::Medium])
    .presentation_drag_indicator(Visibility::Visible)
}

fn show_about() {
    settings().about.set(true);
    state::refresh();
}

fn hide_about() {
    settings().about.set(false);
    state::refresh();
}

fn content_view() -> swiftui_native::ViewHandle {
    state::root_view(|_| body().into_handle())
}

#[cfg(target_os = "macos")]
#[path = "common/simulator.rs"]
mod simulator;

#[cfg(target_os = "macos")]
fn main() {
    if std::env::args().any(|a| a == "--sim" || a == "device") {
        return simulator::main(&simulator::Bundle {
            example: "settings",
            bundle_id: "dev.rswift.Settings",
            executable: "Settings",
            display_name: "Settings",
            launch_args: &[],
        });
    }
    if std::env::args().any(|a| a == "--render") {
        let out = concat!(env!("CARGO_MANIFEST_DIR"), "/../../target/settings.png");
        swiftui_native::render::render(&content_view(), 420, 640).write_png(out);
        eprintln!("wrote {out}");
        return;
    }
    show_window(&content_view(), "Settings", 420.0, 640.0);
}

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