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}