swiftui-native 0.0.1

SwiftUI from pure Rust — no Swift bridge, no shim, only swift-runtime calls
Documentation
//! A SwiftUI screen whose *types* are chosen at run time.
//!
//! This is the claim the crate exists to make. A SwiftUI view is a type:
//! `VStack { Text("hi"); Divider() }` is
//! `VStack<TupleView<(Text, Divider)>>`, and Swift decides that when it
//! compiles the file. There is no Swift compiler in this process and no Swift
//! source anywhere in it — so the type is decided by whatever text arrives,
//! and instantiated through the Swift runtime's own metadata accessors.
//!
//! The difference is easy to state and hard to fake: **this binary renders
//! screens that did not exist when it was built.**
//!
//! ```sh
//! # The built-in specs, side by side as PNGs
//! cargo run -p swiftui-native --example dynamic_ui -- --render
//!
//! # Any file, in a window
//! cargo run -p swiftui-native --example dynamic_ui -- path/to/screen.ui
//!
//! # Type a screen and watch it appear
//! cargo run -p swiftui-native --example dynamic_ui -- --repl
//!
//! # What the format accepts, and the Swift type a spec produces
//! cargo run -p swiftui-native --example dynamic_ui -- --names
//! cargo run -p swiftui-native --example dynamic_ui -- --type demo.ui
//! ```

// The iOS `main` hands the whole screen to `app::run`, so the demo table and
// the argument parsing the macOS one uses have no caller there. Keeping them
// in one file is what shows the *interpreter* is platform-independent and only
// the entry point differs.
#![allow(dead_code)]

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

#[path = "common/spec.rs"]
mod spec;

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

/// The spec the body renders, replaced from outside while the app runs.
///
/// A `static` rather than a captured variable because `state::root_view` takes
/// a plain `fn` — SwiftUI calls the body, so there is nowhere for a closure's
/// environment to live. That constraint is also what makes reloading work: the
/// body reads the current text every time it is evaluated.
fn current() -> &'static Mutex<String> {
    static CURRENT: OnceLock<Mutex<String>> = OnceLock::new();
    CURRENT.get_or_init(|| Mutex::new(DEMOS[0].1.to_string()))
}

pub fn set_spec(text: &str) {
    *current().lock().expect("spec") = text.to_string();
}

/// Build the view the spec describes, or a legible error screen.
///
/// A spec comes from outside, so a mistake in it is not a bug in this program.
/// Rendering the message is more useful than a panic: the window stays up and
/// the next edit can fix it, which is the whole point of reloading.
pub fn body() -> View {
    let text_src = current().lock().expect("spec").clone();
    match spec::parse(&text_src).and_then(|nodes| spec::build(&nodes)) {
        Ok(v) => v,
        Err(e) => vstack_aligned_spacing(
            &[
                text("Could not build that screen")
                    .text_style(TextStyle::Headline)
                    .fg_system(SystemColor::Red),
                text(&e).font_size(12.0).fg_system(SystemColor::Secondary),
            ],
            HorizontalAlignment::Leading,
            Some(8.0),
        )
        .padding(20.0),
    }
}

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

/// Screens written here as *data*, not as Rust.
///
/// Each is a string this file could have read from disk instead; they are
/// inline so the example runs with no arguments and so the renders are
/// reproducible. Between them they reach stacks, shapes, controls, text
/// styling and nesting — the same span the hand-written examples cover, with
/// nothing about them in the compiled code.
const DEMOS: &[(&str, &str)] = &[
    (
        "readout",
        r#"
// A status readout. Nothing here is a Rust type.
vstack spacing=14 pad=20 align=leading {
  text "Deployment" style=large_title bold
  text "3 of 4 regions healthy" color=secondary size=13

  divider

  hstack spacing=12 {
    circle color=green frame=14x14
    text "us-east" size=14
    spacer
    text "ok" color=green size=13
  }
  hstack spacing=12 {
    circle color=green frame=14x14
    text "us-west" size=14
    spacer
    text "ok" color=green size=13
  }
  hstack spacing=12 {
    circle color=orange frame=14x14
    text "eu-central" size=14
    spacer
    text "degraded" color=orange size=13
  }
  hstack spacing=12 {
    circle color=gray frame=14x14
    text "ap-south" size=14
    spacer
    text "draining" color=gray size=13
  }

  divider
  text "Rollout" size=12 color=secondary
  progress 0.62
  spacer
  button "Promote"
}
"#,
    ),
    (
        "card",
        r#"
// A card, to show layering and a hex colour.
zstack align=center frame=320x200 {
  color #1d2b53 frame=300x180 corner=16
  vstack spacing=10 pad=24 align=leading {
    text "Boarding pass" style=title2 bold color=white
    text "LHR to SFO" color=white size=14 opacity=0.8
    spacer
    hstack spacing=16 {
      vstack spacing=2 align=leading {
        text "GATE" size=10 color=white opacity=0.6
        text "B14" style=title3 bold color=white
      }
      vstack spacing=2 align=leading {
        text "SEAT" size=10 color=white opacity=0.6
        text "22A" style=title3 bold color=white
      }
      spacer
      image "airplane"
    }
  }
}
"#,
    ),
    (
        "gauges",
        r#"
// Three gauges and a grouped block, from data alone.
vstack spacing=16 pad=20 align=leading {
  text "Capacity" style=title bold
  gauge 0.34 label="CPU"
  gauge 0.71 label="Memory"
  gauge 0.12 label="Disk"
  divider
  group_box {
    vstack spacing=6 align=leading {
      text "Notes" style=headline
      text "Every view on this screen was named by a string." size=12 color=secondary
      text "The binary has no VStack<...> type baked in for it." size=12 color=secondary
    }
  }
  spacer
}
"#,
    ),
];

fn demo(name: &str) -> Option<&'static str> {
    DEMOS.iter().find(|(n, _)| *n == name).map(|(_, s)| *s)
}

/// Read a spec from a path, a demo name, or stdin for `-`.
fn load(arg: &str) -> Result<String, String> {
    if arg == "-" {
        use std::io::Read;
        let mut s = String::new();
        std::io::stdin().read_to_string(&mut s).map_err(|e| e.to_string())?;
        return Ok(s);
    }
    if let Some(d) = demo(arg) {
        return Ok(d.to_string());
    }
    std::fs::read_to_string(arg).map_err(|e| format!("{arg}: {e}"))
}

/// Read specs from stdin and render each as it arrives.
///
/// A blank line submits. This is the shortest demonstration there is: the same
/// process, still running, drawing a screen typed a moment ago.
fn repl() {
    use std::io::{BufRead, Write};
    eprintln!("Type a screen. Blank line renders it; Ctrl-D quits.");
    eprintln!("Names: {}", spec::NAMES.join(" "));
    let stdin = std::io::stdin();
    let mut buf = String::new();
    let mut n = 0usize;
    loop {
        eprint!("> ");
        let _ = std::io::stderr().flush();
        let mut line = String::new();
        match stdin.lock().read_line(&mut line) {
            Ok(0) => break,
            Ok(_) => {}
            Err(e) => {
                eprintln!("{e}");
                break;
            }
        }
        if line.trim().is_empty() {
            if buf.trim().is_empty() {
                continue;
            }
            match spec::parse(&buf).and_then(|nodes| spec::build(&nodes)) {
                Ok(v) => {
                    n += 1;
                    let out = format!(
                        "{}/../../target/dynamic_ui_repl_{n}.png",
                        env!("CARGO_MANIFEST_DIR")
                    );
                    let handle = v.into_handle();
                    eprintln!("  {}", swiftui_native::diag::type_name(
                        swiftui_native::views::meta_of(&handle)
                    ));
                    if swiftui_native::render::render(&handle, 420, 420).write_png(&out) {
                        eprintln!("  wrote {out}");
                    }
                }
                Err(e) => eprintln!("  {e}"),
            }
            buf.clear();
        } else {
            buf.push_str(&line);
        }
    }
}

/// Print the Swift type a spec builds.
///
/// The strongest short proof that this is real SwiftUI and not a lookalike: the
/// name comes from the Swift runtime's own metadata, and it is a different name
/// for every different spec.
fn print_type(arg: &str) -> i32 {
    match load(arg).and_then(|s| {
        spec::parse(&s).and_then(|nodes| spec::build(&nodes)).map(|v| v.into_handle())
    }) {
        Ok(h) => {
            println!(
                "{}",
                swiftui_native::diag::type_name(swiftui_native::views::meta_of(&h))
            );
            0
        }
        Err(e) => {
            eprintln!("{e}");
            1
        }
    }
}

#[cfg(target_os = "macos")]
fn main() {
    let args: Vec<String> = std::env::args().skip(1).collect();
    let flag = |f: &str| args.iter().any(|a| a == f);

    if flag("--names") {
        println!("{}", spec::NAMES.join("\n"));
        return;
    }
    if flag("--repl") {
        return repl();
    }
    if let Some(i) = args.iter().position(|a| a == "--type") {
        let Some(arg) = args.get(i + 1) else {
            eprintln!("--type wants a spec: a path, `-`, or a demo name");
            std::process::exit(2);
        };
        std::process::exit(print_type(arg));
    }
    if flag("--render") {
        // One PNG per built-in spec. Different text, different Swift type,
        // different picture — from one binary, in one process.
        for (name, src) in DEMOS {
            set_spec(src);
            // The spec's own view, for the type name: `content_view` wraps it
            // in the runtime-synthesised `RustView` that carries `@State`, and
            // that wrapper is the same whatever the spec says. What differs —
            // and what makes the point — is the type underneath it.
            let built = body().into_handle();
            let out = format!("{}/../../target/dynamic_ui_{name}.png", env!("CARGO_MANIFEST_DIR"));
            let ok = swiftui_native::render::render(&content_view(), 380, 420).write_png(&out);
            println!(
                "{name:10} {}\n           {}",
                swiftui_native::diag::type_name(swiftui_native::views::meta_of(&built)),
                if ok { out } else { "render failed".to_string() }
            );
        }
        return;
    }
    if flag("--sim") {
        return simulator::main(&simulator::Bundle {
            example: "dynamic_ui",
            bundle_id: "dev.rswift.DynamicUI",
            executable: "DynamicUI",
            display_name: "Dynamic UI",
            launch_args: &[],
        });
    }

    // A path or a demo name, else the first demo.
    if let Some(arg) = args.iter().find(|a| !a.starts_with("--")) {
        match load(arg) {
            Ok(s) => set_spec(&s),
            Err(e) => {
                eprintln!("{e}");
                std::process::exit(1);
            }
        }
    }
    show_window(&content_view(), "Dynamic UI", 380.0, 460.0);
}

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