Skip to main content

inbox/
inbox.rs

1//! A list-and-detail screen — the other shape an app usually is.
2//!
3//! Where `settings.rs` is a form of bound controls, this is a navigation stack
4//! over a list: rows built by `ForEach`, a search field writing into Rust, a
5//! toolbar, and swipe actions. Between them the two examples cover most of what
6//! a screen does.
7//!
8//! ```sh
9//! cargo run -p swiftui-native --example inbox
10//! cargo run -p swiftui-native --example inbox -- --render
11//! ```
12
13use std::sync::OnceLock;
14use swiftui_native::prelude::*;
15use swiftui_native::{state, views};
16
17struct Message {
18    from: &'static str,
19    subject: &'static str,
20    unread: bool,
21}
22
23const MESSAGES: &[Message] = &[
24    Message { from: "Ada Lovelace", subject: "Notes on the Analytical Engine", unread: true },
25    Message { from: "Alan Turing", subject: "On computable numbers", unread: true },
26    Message { from: "Grace Hopper", subject: "Compiler progress", unread: false },
27    Message { from: "Barbara Liskov", subject: "Substitution", unread: false },
28];
29
30struct Inbox {
31    query: views::BoundState<String>,
32    searching: views::BoundState<bool>,
33}
34
35fn inbox() -> &'static Inbox {
36    static I: OnceLock<Inbox> = OnceLock::new();
37    I.get_or_init(|| Inbox {
38        query: views::BoundState::new(String::new()),
39        searching: views::BoundState::new(false),
40    })
41}
42
43/// Which messages match the current query — plain Rust, evaluated when SwiftUI
44/// asks for the body.
45fn matching() -> Vec<usize> {
46    let q = inbox().query.get().to_lowercase();
47    MESSAGES
48        .iter()
49        .enumerate()
50        .filter(|(_, m)| {
51            q.is_empty()
52                || m.from.to_lowercase().contains(&q)
53                || m.subject.to_lowercase().contains(&q)
54        })
55        .map(|(i, _)| i)
56        .collect()
57}
58
59/// The message itself, pushed when its row is tapped.
60fn detail(i: usize) -> View {
61    let m = &MESSAGES[i];
62    vstack_aligned_spacing(
63        &[
64            text(m.subject).text_style(TextStyle::Title).bold(),
65            text!("From {}", m.from).fg_system(SystemColor::Gray),
66            divider(),
67            text("Body text would go here.").fg_system(SystemColor::Gray),
68            spacer(),
69        ],
70        HorizontalAlignment::Leading,
71        Some(12.0),
72    )
73    .padding(20.0)
74    .navigation_title(m.subject)
75}
76
77fn row(i: usize) -> View {
78    let m = &MESSAGES[i];
79    hstack_spacing(
80        &[
81            // An unread dot, or the space where one would be.
82            view_if!(
83                m.unread,
84                circle_shape().fill_system(SystemColor::Blue).frame(8.0, 8.0),
85                empty().frame(8.0, 8.0)
86            ),
87            vstack_aligned(
88                &[
89                    text(m.from).bold(),
90                    text(m.subject).font_size(12.0).fg_system(SystemColor::Gray),
91                ],
92                HorizontalAlignment::Leading,
93            ),
94            spacer(),
95        ],
96        8.0,
97    )
98    .padding_edges(EdgeSet::Vertical, Some(4.0))
99    .a11y_label(&format!("{}, {}", m.from, m.subject))
100    .a11y_hint(if m.unread { "Unread" } else { "Read" })
101}
102
103/// The row as it appears in the list: a link to the detail, with the swipe
104/// action attached to the link rather than to its label.
105fn linked_row(i: usize) -> View {
106    navigation_link_labelled(row(i), detail(i))
107        .swipe_actions(HorizontalEdge::Trailing, true, button("Delete", || {}))
108}
109
110fn body() -> View {
111    let i = inbox();
112    let rows = matching();
113    navigation_stack(
114        list(for_each_items(&rows, |&i| linked_row(i)))
115        .list_style(views::ListStyle::InsetListStyle)
116        .searchable(&i.query, &i.searching, SearchFieldPlacement::Automatic, "Search"),
117    )
118    .navigation_title("Inbox")
119    .toolbar(button("Compose", || {}).keyboard_shortcut(KeyboardShortcut::DefaultAction))
120}
121
122fn content_view() -> swiftui_native::ViewHandle {
123    state::root_view(|_| body().into_handle())
124}
125
126#[cfg(target_os = "macos")]
127#[path = "common/simulator.rs"]
128mod simulator;
129
130#[cfg(target_os = "macos")]
131fn main() {
132    if std::env::args().any(|a| a == "--sim" || a == "device") {
133        return simulator::main(&simulator::Bundle {
134            example: "inbox",
135            bundle_id: "dev.rswift.Inbox",
136            executable: "Inbox",
137            display_name: "Inbox",
138            launch_args: &[],
139        });
140    }
141    if std::env::args().any(|a| a == "--render") {
142        let out = concat!(env!("CARGO_MANIFEST_DIR"), "/../../target/inbox.png");
143        swiftui_native::render::render(&content_view(), 420, 520).write_png(out);
144        eprintln!("wrote {out}");
145        return;
146    }
147    show_window(&content_view(), "Inbox", 420.0, 520.0);
148}
149
150#[cfg(not(target_os = "macos"))]
151fn main() {
152    swiftui_native::app::run(content_view)
153}