swiftui-native 0.0.1

SwiftUI from pure Rust — no Swift bridge, no shim, only swift-runtime calls
Documentation
//! A list-and-detail screen — the other shape an app usually is.
//!
//! Where `settings.rs` is a form of bound controls, this is a navigation stack
//! over a list: rows built by `ForEach`, a search field writing into Rust, a
//! toolbar, and swipe actions. Between them the two examples cover most of what
//! a screen does.
//!
//! ```sh
//! cargo run -p swiftui-native --example inbox
//! cargo run -p swiftui-native --example inbox -- --render
//! ```

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

struct Message {
    from: &'static str,
    subject: &'static str,
    unread: bool,
}

const MESSAGES: &[Message] = &[
    Message { from: "Ada Lovelace", subject: "Notes on the Analytical Engine", unread: true },
    Message { from: "Alan Turing", subject: "On computable numbers", unread: true },
    Message { from: "Grace Hopper", subject: "Compiler progress", unread: false },
    Message { from: "Barbara Liskov", subject: "Substitution", unread: false },
];

struct Inbox {
    query: views::BoundState<String>,
    searching: views::BoundState<bool>,
}

fn inbox() -> &'static Inbox {
    static I: OnceLock<Inbox> = OnceLock::new();
    I.get_or_init(|| Inbox {
        query: views::BoundState::new(String::new()),
        searching: views::BoundState::new(false),
    })
}

/// Which messages match the current query — plain Rust, evaluated when SwiftUI
/// asks for the body.
fn matching() -> Vec<usize> {
    let q = inbox().query.get().to_lowercase();
    MESSAGES
        .iter()
        .enumerate()
        .filter(|(_, m)| {
            q.is_empty()
                || m.from.to_lowercase().contains(&q)
                || m.subject.to_lowercase().contains(&q)
        })
        .map(|(i, _)| i)
        .collect()
}

/// The message itself, pushed when its row is tapped.
fn detail(i: usize) -> View {
    let m = &MESSAGES[i];
    vstack_aligned_spacing(
        &[
            text(m.subject).text_style(TextStyle::Title).bold(),
            text!("From {}", m.from).fg_system(SystemColor::Gray),
            divider(),
            text("Body text would go here.").fg_system(SystemColor::Gray),
            spacer(),
        ],
        HorizontalAlignment::Leading,
        Some(12.0),
    )
    .padding(20.0)
    .navigation_title(m.subject)
}

fn row(i: usize) -> View {
    let m = &MESSAGES[i];
    hstack_spacing(
        &[
            // An unread dot, or the space where one would be.
            view_if!(
                m.unread,
                circle_shape().fill_system(SystemColor::Blue).frame(8.0, 8.0),
                empty().frame(8.0, 8.0)
            ),
            vstack_aligned(
                &[
                    text(m.from).bold(),
                    text(m.subject).font_size(12.0).fg_system(SystemColor::Gray),
                ],
                HorizontalAlignment::Leading,
            ),
            spacer(),
        ],
        8.0,
    )
    .padding_edges(EdgeSet::Vertical, Some(4.0))
    .a11y_label(&format!("{}, {}", m.from, m.subject))
    .a11y_hint(if m.unread { "Unread" } else { "Read" })
}

/// The row as it appears in the list: a link to the detail, with the swipe
/// action attached to the link rather than to its label.
fn linked_row(i: usize) -> View {
    navigation_link_labelled(row(i), detail(i))
        .swipe_actions(HorizontalEdge::Trailing, true, button("Delete", || {}))
}

fn body() -> View {
    let i = inbox();
    let rows = matching();
    navigation_stack(
        list(for_each_items(&rows, |&i| linked_row(i)))
        .list_style(views::ListStyle::InsetListStyle)
        .searchable(&i.query, &i.searching, SearchFieldPlacement::Automatic, "Search"),
    )
    .navigation_title("Inbox")
    .toolbar(button("Compose", || {}).keyboard_shortcut(KeyboardShortcut::DefaultAction))
}

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: "inbox",
            bundle_id: "dev.rswift.Inbox",
            executable: "Inbox",
            display_name: "Inbox",
            launch_args: &[],
        });
    }
    if std::env::args().any(|a| a == "--render") {
        let out = concat!(env!("CARGO_MANIFEST_DIR"), "/../../target/inbox.png");
        swiftui_native::render::render(&content_view(), 420, 520).write_png(out);
        eprintln!("wrote {out}");
        return;
    }
    show_window(&content_view(), "Inbox", 420.0, 520.0);
}

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