Skip to main content

swiftui_native/
macros.rs

1//! Declarative macros for building view trees.
2//!
3//! The function forms take slices, which means spelling out `&[...]` and often a
4//! `Vec` when children have different origins. These macros take a plain,
5//! comma-separated list instead, so a tree reads top to bottom the way the
6//! SwiftUI it mirrors does:
7//!
8//! ```ignore
9//! use swiftui_native::prelude::*;
10//!
11//! let ui = vstack![
12//!     text("Dashboard").text_style(TextStyle::Title),
13//!     divider(),
14//!     hstack![image("star.fill"), text("starred"), spacer()],
15//!     zstack![
16//!         rounded_rectangle(12.0).fg(Color::hex(0x2E86DE)),
17//!         text("badge").fg(Color::WHITE),
18//!     ],
19//! ]
20//! .padding(20.0);
21//! ```
22//!
23//! Every element goes through [`IntoView`](crate::view::IntoView), so string
24//! literals can be written directly:
25//!
26//! ```ignore
27//! vstack!["first", "second", text("third").bold()]
28//! ```
29//!
30//! A trailing comma is allowed everywhere.
31
32/// `Text("…")` with interpolation, the way Swift writes `Text("\(name)!")`.
33///
34/// The function form takes a `&str`, so an interpolated label reads
35/// `text(&format!("{}!", name))`. This says the same thing in the order Swift
36/// says it:
37///
38/// ```ignore
39/// text!("{}!", selected)
40/// ```
41#[macro_export]
42macro_rules! text {
43    ($fmt:literal $(, $arg:expr)* $(,)?) => {
44        $crate::view::text(&::std::format!($fmt $(, $arg)*))
45    };
46}
47
48/// `Image(systemName: "…")` with interpolation — see [`text!`].
49///
50/// ```ignore
51/// image!("figure.{}", activity.to_lowercase())
52/// ```
53#[macro_export]
54macro_rules! image {
55    ($fmt:literal $(, $arg:expr)* $(,)?) => {
56        $crate::view::image(&::std::format!($fmt $(, $arg)*))
57    };
58}
59
60/// Build a `VStack` from a list of children.
61///
62/// `VStack(spacing: 12) { … }` is written `vstack![spacing: 12.0; …]`: the
63/// configuration first, then a semicolon, then the children.
64#[macro_export]
65macro_rules! vstack {
66    (spacing: $spacing:expr; $($child:expr),* $(,)?) => {
67        $crate::view::vstack_spacing(
68            &[$($crate::view::IntoView::into_view($child)),*],
69            $spacing,
70        )
71    };
72    ($($child:expr),* $(,)?) => {
73        $crate::view::vstack(&[$($crate::view::IntoView::into_view($child)),*])
74    };
75}
76
77/// Build an `HStack` from a list of children — see [`vstack!`].
78#[macro_export]
79macro_rules! hstack {
80    (spacing: $spacing:expr; $($child:expr),* $(,)?) => {
81        $crate::view::hstack_spacing(
82            &[$($crate::view::IntoView::into_view($child)),*],
83            $spacing,
84        )
85    };
86    ($($child:expr),* $(,)?) => {
87        $crate::view::hstack(&[$($crate::view::IntoView::into_view($child)),*])
88    };
89}
90
91/// Build a `ZStack` from a list of children, back to front.
92///
93/// `ZStack(alignment: .topLeading) { … }` is
94/// `zstack![alignment: Alignment::TopLeading; …]`.
95#[macro_export]
96macro_rules! zstack {
97    (alignment: $alignment:expr; $($child:expr),* $(,)?) => {
98        $crate::view::zstack_aligned(
99            &[$($crate::view::IntoView::into_view($child)),*],
100            $alignment,
101        )
102    };
103    ($($child:expr),* $(,)?) => {
104        $crate::view::zstack(&[$($crate::view::IntoView::into_view($child)),*])
105    };
106}
107
108/// Build a `LazyVStack` from a list of children.
109#[macro_export]
110macro_rules! lazy_vstack {
111    ($($child:expr),* $(,)?) => {
112        $crate::view::lazy_vstack(&[$($crate::view::IntoView::into_view($child)),*])
113    };
114}
115
116/// Build a `LazyHStack` from a list of children.
117#[macro_export]
118macro_rules! lazy_hstack {
119    ($($child:expr),* $(,)?) => {
120        $crate::view::lazy_hstack(&[$($crate::view::IntoView::into_view($child)),*])
121    };
122}
123
124/// Build a `GridRow` from a list of cells.
125#[macro_export]
126macro_rules! grid_row {
127    ($($cell:expr),* $(,)?) => {
128        $crate::view::grid_row(&[$($crate::view::IntoView::into_view($cell)),*])
129    };
130}
131
132/// Build a `Grid` from a list of rows.
133#[macro_export]
134macro_rules! grid {
135    ($($row:expr),* $(,)?) => {
136        $crate::view::grid(&[$($row),*])
137    };
138}
139
140/// A Rust layout, given children — `MyLayout { … }` in Swift.
141///
142/// ```ignore
143/// laid_out!(cascade, text("a"), text("b"), text("c"))
144/// ```
145#[macro_export]
146macro_rules! laid_out {
147    ($layout:expr, $($child:expr),* $(,)?) => {
148        $crate::view::laid_out(&$layout, &[$($child),*])
149    };
150}
151
152/// `ForEach` over a range or a collection.
153///
154/// ```ignore
155/// foreach!(0..4, |i| text(&format!("row {i}")))
156/// foreach!(&["a", "b", "c"], |s| text(s))
157/// ```
158#[macro_export]
159macro_rules! foreach {
160    ($range:expr, $body:expr) => {
161        $crate::view::for_each($range, $body)
162    };
163}
164
165/// A `ViewBuilder`-style conditional that keeps each branch's identity.
166///
167/// Unlike a plain Rust `if`, which picks a view and erases the branch, this
168/// builds a `_ConditionalContent` — so SwiftUI sees a *branch* change and can
169/// run insertion and removal transitions, and each arm keeps its own state.
170///
171/// ```ignore
172/// view_if!(is_open, text("open"), text("closed"))
173/// ```
174#[macro_export]
175macro_rules! view_if {
176    // No else branch, which is the common shape: `if cond { view }`. The absent
177    // arm is an `EmptyView`, so the branch is still a branch and transitions
178    // still run, rather than the whole thing collapsing to nothing.
179    ($cond:expr, $then:expr $(,)?) => {
180        $crate::view::conditional(
181            $cond,
182            $crate::view::IntoView::into_view($then),
183            $crate::view::empty(),
184        )
185    };
186    ($cond:expr, $then:expr, $else:expr $(,)?) => {
187        $crate::view::conditional(
188            $cond,
189            $crate::view::IntoView::into_view($then),
190            $crate::view::IntoView::into_view($else),
191        )
192    };
193}
194
195/// `List { … }` over several rows.
196///
197/// `List { Text("a"); Text("b") }` is `list![text("a"), text("b")]` — two rows,
198/// because the children become a `TupleView` and a `List` renders each element
199/// of its content as a row. Passing a stack instead, which is what the free
200/// function [`crate::view::list`] takes, gives *one* row holding a stack.
201#[macro_export]
202macro_rules! list {
203    ($($child:expr),* $(,)?) => {
204        $crate::view::list($crate::view::tuple_view(
205            &[$($crate::view::IntoView::into_view($child)),*],
206        ))
207    };
208}
209
210/// `Form { … }` over several rows.
211#[macro_export]
212macro_rules! form {
213    ($($child:expr),* $(,)?) => {
214        $crate::view::form($crate::view::tuple_view(
215            &[$($crate::view::IntoView::into_view($child)),*],
216        ))
217    };
218}
219
220/// `Section { … }`, optionally with a header: `section![header: text("Title"); …]`.
221#[macro_export]
222macro_rules! section {
223    (header: $header:expr, footer: $footer:expr; $($child:expr),* $(,)?) => {
224        $crate::view::section_titled_footer(
225            $crate::view::IntoView::into_view($header),
226            $crate::view::IntoView::into_view($footer),
227            $crate::view::tuple_view(&[$($crate::view::IntoView::into_view($child)),*]),
228        )
229    };
230    (footer: $footer:expr; $($child:expr),* $(,)?) => {
231        $crate::view::section_footer(
232            $crate::view::IntoView::into_view($footer),
233            $crate::view::tuple_view(&[$($crate::view::IntoView::into_view($child)),*]),
234        )
235    };
236    (header: $header:expr; $($child:expr),* $(,)?) => {
237        $crate::view::section_titled(
238            $crate::view::IntoView::into_view($header),
239            $crate::view::tuple_view(&[$($crate::view::IntoView::into_view($child)),*]),
240        )
241    };
242    ($($child:expr),* $(,)?) => {
243        $crate::view::section($crate::view::tuple_view(
244            &[$($crate::view::IntoView::into_view($child)),*],
245        ))
246    };
247}
248
249/// `Group { … }` — several views where one is wanted, with no layout of its own.
250#[macro_export]
251macro_rules! group {
252    ($($child:expr),* $(,)?) => {
253        $crate::view::group($crate::view::tuple_view(
254            &[$($crate::view::IntoView::into_view($child)),*],
255        ))
256    };
257}
258
259/// `ControlGroup { … }`
260#[macro_export]
261macro_rules! control_group {
262    ($($child:expr),* $(,)?) => {
263        $crate::view::control_group($crate::view::tuple_view(
264            &[$($crate::view::IntoView::into_view($child)),*],
265        ))
266    };
267}
268
269/// `TabView { … }` over several tabs.
270#[macro_export]
271macro_rules! tab_view {
272    ($($child:expr),* $(,)?) => {
273        $crate::view::tab_view($crate::view::tuple_view(
274            &[$($crate::view::IntoView::into_view($child)),*],
275        ))
276    };
277}
278
279/// `TabView { Tab(…); … }` over several [`tab`](crate::view::tab)s.
280///
281/// ```no_run
282/// # use swiftui_native::prelude::*;
283/// # use swiftui_native::tabs;
284/// let view = tabs![
285///     tab("Home", "house", text("home page")),
286///     tab("Settings", "gear", text("settings page")),
287/// ];
288/// ```
289#[macro_export]
290macro_rules! tabs {
291    ($($tab:expr),* $(,)?) => {
292        $crate::view::tab_view_tabs(vec![$($tab),*])
293    };
294}
295
296/// `Menu(_:) { … }` over several items.
297#[macro_export]
298macro_rules! menu {
299    ($label:expr, $($child:expr),* $(,)?) => {
300        $crate::view::menu($label, $crate::view::tuple_view(
301            &[$($crate::view::IntoView::into_view($child)),*],
302        ))
303    };
304}
305
306/// `Picker(_:selection:)` over rows, bound to Rust state.
307///
308/// The rows are a `@ViewBuilder` block in Swift, so they are a list here rather
309/// than something the caller has to wrap in a `tuple_view` by hand.
310///
311/// ```ignore
312/// picker_bound!("Theme", &theme, text("System").tag_int(0), text("Dark").tag_int(1))
313/// ```
314#[macro_export]
315macro_rules! picker_bound {
316    ($title:expr, $state:expr, $($row:expr),* $(,)?) => {
317        $crate::view::picker_bound(
318            $title,
319            $state,
320            $crate::view::tuple_view(&[$($crate::view::IntoView::into_view($row)),*]),
321        )
322    };
323}
324
325/// `Picker(_:selection:)` over rows with a fixed selection — see
326/// [`picker_bound!`] for the one the user can change.
327#[macro_export]
328macro_rules! picker {
329    ($title:expr, $selection:expr, $($row:expr),* $(,)?) => {
330        $crate::view::picker(
331            $title,
332            $selection,
333            $crate::view::tuple_view(&[$($crate::view::IntoView::into_view($row)),*]),
334        )
335    };
336}
337
338macro_rules! swift_statics {
339    (
340        $(#[$meta:meta])*
341        $vis:vis enum $name:ident {
342            $($(#[$vmeta:meta])* $variant:ident = $symbol:expr),* $(,)?
343        }
344    ) => {
345        $(#[$meta])*
346        #[derive(Clone, Copy, Debug, PartialEq, Eq)]
347        $vis enum $name {
348            $($(#[$vmeta])* $variant),*
349        }
350
351        impl $name {
352            /// The mangled symbol of the static property this variant names.
353            ///
354            /// `allow(unused_doc_comments)`: the arms carry each variant's
355            /// attributes so that a `#[cfg]` gating a variant gates its arm
356            /// too, and a `///` comes along with it.
357            #[allow(unused_doc_comments)]
358            pub(crate) fn getter(self) -> &'static core::ffi::CStr {
359                match self {
360                    $($(#[$vmeta])* Self::$variant => $symbol),*
361                }
362            }
363
364            /// The resolved getter, cached per variant.
365            ///
366            /// `allow(dead_code)`: the macro generates this for every statics
367            /// enum, and a few of them have no caller yet.
368            ///
369            /// Each variant names one fixed symbol, so it can keep the answer
370            /// beside itself the way a `cached!` entry does and the way Swift's
371            /// own `…Ml` cache words do: a relaxed load, a compare, and a store
372            /// on the miss.
373            ///
374            /// Going through [`crate::resolve::sym`] instead took an `RwLock`
375            /// and hashed the mangled name on *every* read, and these are read
376            /// once per enum argument per view — measured at ~35 ns each, which
377            /// was more than the whole of `.padding(_:)` beside it.
378            #[allow(dead_code)]
379            pub(crate) fn resolved(self) -> *const core::ffi::c_void {
380                use std::sync::atomic::{AtomicUsize, Ordering};
381                // Counts *every* variant, including ones this platform gates
382                // out: an attribute cannot sit on an array element, and a
383                // cache longer than the enum costs a few unused words where a
384                // shorter one would index out of range.
385                const N: usize = [$(stringify!($variant)),*].len();
386                static CACHE: [AtomicUsize; N] = [const { AtomicUsize::new(0) }; N];
387                let slot = &CACHE[self as usize];
388                let v = slot.load(Ordering::Relaxed);
389                if v != 0 {
390                    // Report even on a hit, so a symbol trace sees the statics
391                    // a build used and not only the ones it resolved first.
392                    $crate::resolve::note(self.getter());
393                    return v as *const core::ffi::c_void;
394                }
395                let p = $crate::resolve::require(self.getter());
396                slot.store(p as usize, Ordering::Relaxed);
397                p
398            }
399        }
400
401    };
402}
403
404/// An app's `body`, written the way Swift writes it.
405///
406/// `scene::commands(scene::group(vec![…]), vec![…])` is the same thing inside
407/// out: the scenes read bottom-up, the commands are a second list far from what
408/// they attach to, and every body ends in `.into_handle()`. This reads in the
409/// order the app is built.
410///
411/// ```no_run
412/// # use swiftui_native::{app, prelude::*, scenes};
413/// app::run_scene(|| scenes! {
414///     window_group { text("main") }
415///     settings { text("preferences") }
416///     window("Utility", "utility") { text("a second window") }
417///     menu_bar_extra("Status", "star.fill") { text("in the menu bar") }
418///     commands {
419///         menu("Counter") { button("Reset", || {}) }
420///         replacing(NewItem) { button("New Count", || {}) }
421///         after(Help) { button("About", || {}) }
422///     }
423/// });
424/// ```
425///
426/// Each body is an ordinary block returning anything that is a view — a `View`,
427/// a `ViewHandle`, or a `&str`. `commands` is optional and goes last, which is
428/// also where Swift's `.commands { }` ends up.
429///
430/// The `window`, `settings` and `menu_bar_extra` clauses are macOS-only, as the
431/// scenes themselves are; a `scenes!` using them does not compile for iOS, which
432/// is the same answer Swift gives.
433#[macro_export]
434macro_rules! scenes {
435    ($($clause:tt)*) => {
436        $crate::scenes_build!(@scene [] $($clause)*)
437    };
438}
439
440/// The muncher behind [`scenes!`]. Not for direct use.
441#[doc(hidden)]
442#[macro_export]
443macro_rules! scenes_build {
444    // `commands { … }` ends the list and wraps everything built so far.
445    (@scene [$($s:expr),* $(,)?] commands { $($c:tt)* }) => {
446        $crate::scene::commands(
447            $crate::scene::group(::std::vec![$($s),*]),
448            $crate::scenes_build!(@commands [] $($c)*),
449        )
450    };
451
452    (@scene [$($s:expr),* $(,)?] window_group $body:block $($rest:tt)*) => {
453        $crate::scenes_build!(@scene [$($s,)* $crate::scene::window_group(move || $body)] $($rest)*)
454    };
455    (@scene [$($s:expr),* $(,)?] settings $body:block $($rest:tt)*) => {
456        $crate::scenes_build!(@scene [$($s,)* $crate::scene::settings(move || $body)] $($rest)*)
457    };
458    (@scene [$($s:expr),* $(,)?] window($title:expr, $id:expr) $body:block $($rest:tt)*) => {
459        $crate::scenes_build!(@scene [$($s,)* $crate::scene::window($title, $id, move || $body)] $($rest)*)
460    };
461    (@scene [$($s:expr),* $(,)?] menu_bar_extra($title:expr, $image:expr) $body:block $($rest:tt)*) => {
462        $crate::scenes_build!(@scene [$($s,)* $crate::scene::menu_bar_extra($title, $image, move || $body)] $($rest)*)
463    };
464
465    // No commands: the scenes are the whole body.
466    (@scene [$($s:expr),* $(,)?]) => {
467        $crate::scene::group(::std::vec![$($s),*])
468    };
469
470    (@commands [$($c:expr),* $(,)?] menu($title:expr) $body:block $($rest:tt)*) => {
471        $crate::scenes_build!(@commands [$($c,)* $crate::scene::command_menu($title, move || $body)] $($rest)*)
472    };
473    (@commands [$($c:expr),* $(,)?] replacing($place:ident) $body:block $($rest:tt)*) => {
474        $crate::scenes_build!(@commands [$($c,)* $crate::scene::command_group(
475            $crate::scene::CommandPlacement::$place,
476            $crate::scene::Relative::Replacing,
477            move || $body,
478        )] $($rest)*)
479    };
480    (@commands [$($c:expr),* $(,)?] after($place:ident) $body:block $($rest:tt)*) => {
481        $crate::scenes_build!(@commands [$($c,)* $crate::scene::command_group(
482            $crate::scene::CommandPlacement::$place,
483            $crate::scene::Relative::After,
484            move || $body,
485        )] $($rest)*)
486    };
487    (@commands [$($c:expr),* $(,)?] before($place:ident) $body:block $($rest:tt)*) => {
488        $crate::scenes_build!(@commands [$($c,)* $crate::scene::command_group(
489            $crate::scene::CommandPlacement::$place,
490            $crate::scene::Relative::Before,
491            move || $body,
492        )] $($rest)*)
493    };
494    (@commands [$($c:expr),* $(,)?]) => { ::std::vec![$($c),*] };
495}
496
497/// Declare environment keys of your own.
498///
499/// A key is inherently a global: one name is one `EnvironmentKey` conformance and
500/// one key path, minted once and shared by everything that reads or writes it. So
501/// the declaration is a `static` — and writing that by hand was the noisiest
502/// thing in this crate:
503///
504/// ```ignore
505/// static KEY: OnceLock<&'static CustomEnvKey<String>> = OnceLock::new();
506/// let key = *KEY.get_or_init(|| Box::leak(Box::new(env_key("greeting", "hello".to_string()))));
507/// ```
508///
509/// This says the same thing in one line per key:
510///
511/// ```
512/// # use swiftui_native::{environment_keys, prelude::*};
513/// environment_keys! {
514///     /// How deeply this row is nested.
515///     depth: i64 = 3,
516///     /// What to greet the reader with.
517///     greeting: String = "hello",
518/// }
519///
520/// # fn body() -> View {
521/// text("hi").environment_value(depth(), 7).environment_value(greeting(), "bonjour")
522/// # }
523/// ```
524///
525/// Each entry becomes a function returning the one key, so `depth()` in two
526/// places is one key in both. Reading one is
527/// [`EnvironmentValues::value_for`](crate::views::EnvironmentValues::value_for),
528/// which is typed by the key and so cannot be read as the wrong thing.
529///
530/// The name SwiftUI sees includes the declaring module, so two modules may each
531/// have a `depth` without colliding — a key is shared by *calling* the same
532/// function, not by agreeing on a string.
533#[macro_export]
534macro_rules! environment_keys {
535    ($(
536        $(#[$meta:meta])*
537        $name:ident : $ty:ty = $default:expr
538    ),* $(,)?) => {$(
539        $(#[$meta])*
540        pub fn $name() -> &'static $crate::views::CustomEnvKey<$ty> {
541            static KEY: ::std::sync::OnceLock<$crate::views::CustomEnvKey<$ty>> =
542                ::std::sync::OnceLock::new();
543            KEY.get_or_init(|| {
544                $crate::views::env_key(
545                    concat!(module_path!(), "::", stringify!($name)),
546                    $default,
547                )
548            })
549        }
550    )*};
551}
552
553/// Declare container-value keys of your own — as [`environment_keys!`], for
554/// `ContainerValues`.
555///
556/// What a row tells the container it is in, rather than what a container tells
557/// the views inside it.
558///
559/// ```
560/// # use swiftui_native::{container_keys, prelude::*};
561/// container_keys! {
562///     /// How much of the row's width the leading column takes.
563///     ratio: f64 = 0.5,
564/// }
565///
566/// # fn row() -> View {
567/// text("row").container_value(ratio(), 0.25)
568/// # }
569/// ```
570#[macro_export]
571macro_rules! container_keys {
572    ($(
573        $(#[$meta:meta])*
574        $name:ident : $ty:ty = $default:expr
575    ),* $(,)?) => {$(
576        $(#[$meta])*
577        pub fn $name() -> &'static $crate::views::CustomContainerKey<$ty> {
578            static KEY: ::std::sync::OnceLock<$crate::views::CustomContainerKey<$ty>> =
579                ::std::sync::OnceLock::new();
580            KEY.get_or_init(|| {
581                $crate::views::container_key(
582                    concat!(module_path!(), "::", stringify!($name)),
583                    $default,
584                )
585            })
586        }
587    )*};
588}
589
590/// Declare focused-value keys of your own — as [`environment_keys!`], for
591/// `FocusedValues`.
592///
593/// No default: a focused value is `T?` and absent until something publishes it,
594/// which is what lets a menu command know whether anything is focused at all.
595///
596/// ```
597/// # use swiftui_native::{focused_keys, prelude::*};
598/// focused_keys! {
599///     /// What the focused window has selected.
600///     selection: String,
601/// }
602///
603/// # fn body() -> View {
604/// text("row").focused_scene_value(selection(), "the third row")
605/// # }
606/// ```
607#[macro_export]
608macro_rules! focused_keys {
609    ($(
610        $(#[$meta:meta])*
611        $name:ident : $ty:ty
612    ),* $(,)?) => {$(
613        $(#[$meta])*
614        pub fn $name() -> &'static $crate::views::FocusKey<$ty> {
615            static KEY: ::std::sync::OnceLock<$crate::views::FocusKey<$ty>> =
616                ::std::sync::OnceLock::new();
617            KEY.get_or_init(|| {
618                $crate::views::focus_key(concat!(module_path!(), "::", stringify!($name)))
619            })
620        }
621    )*};
622}
623
624/// Declare an `@Observable` model — the properties, not the plumbing.
625///
626/// `@Observable class Model { var count = 0; var name = "Ada" }` in Swift. Here it
627/// is a type whose fields are the properties, so the names are checked and the
628/// types come from the declaration:
629///
630/// ```
631/// # use swiftui_native::{observable, prelude::*};
632/// observable! {
633///     /// Everything the app's windows share.
634///     pub AppModel {
635///         count: i64 = 0,
636///         name: String = "Ada",
637///     }
638/// }
639///
640/// let model = AppModel::new();
641/// model.count.set(model.count.peek() + 1);
642/// assert_eq!(model.count.peek(), 1);
643/// assert_eq!(model.name.peek(), "Ada");
644/// ```
645///
646/// Each field is a [`Property`](crate::observable::Property), with its own key
647/// path, so a view body that read `count` is rebuilt when `count` changes and left
648/// alone when `name` does. [`shared`](crate::observable::Model) is the form an app
649/// usually wants — one model for the process, reachable from every window:
650///
651/// ```no_run
652/// # use swiftui_native::{app, observable, prelude::*};
653/// # observable! { pub AppModel { count: i64 = 0 } }
654/// app::run(|| {
655///     let model = AppModel::shared();
656///     text(&format!("{}", model.count.get())).environment_model(model.as_model())
657/// });
658/// ```
659///
660/// Reading through `get` inside a body is what subscribes it; `peek` reads without
661/// subscribing, for assertions that must not.
662#[macro_export]
663macro_rules! observable {
664    ($(
665        $(#[$meta:meta])*
666        $vis:vis $name:ident {
667            $(
668                $(#[$field_meta:meta])*
669                $field:ident : $ty:ty = $default:expr
670            ),* $(,)?
671        }
672    )*) => {$(
673        $(#[$meta])*
674        $vis struct $name {
675            model: $crate::observable::Model,
676            $(
677                $(#[$field_meta])*
678                pub $field: $crate::observable::Property<$ty>,
679            )*
680        }
681
682        impl $name {
683            /// A new model, with every property at its declared value.
684            ///
685            /// Two of these are two models: a write to one is not a change to the
686            /// other, which is what makes [`shared`](Self::shared) the interesting
687            /// one for an app.
688            pub fn new() -> Self {
689                let model = $crate::observable::object();
690                $name {
691                    $($field: model.property(
692                        concat!(module_path!(), "::", stringify!($name), ".", stringify!($field)),
693                        $default,
694                    ),)*
695                    model,
696                }
697            }
698
699            /// The one model for this process, made on first use.
700            pub fn shared() -> &'static Self {
701                static SHARED: ::std::sync::OnceLock<$name> = ::std::sync::OnceLock::new();
702                SHARED.get_or_init($name::new)
703            }
704
705            /// The model itself, for `.environment_model(…)` and
706            /// `.focused_value_model(…)`.
707            pub fn as_model(&self) -> &$crate::observable::Model {
708                &self.model
709            }
710        }
711
712        impl ::core::default::Default for $name {
713            fn default() -> Self {
714                Self::new()
715            }
716        }
717    )*};
718}