swiftui_native/view.rs
1//! `View` — chainable modifier API for ergonomic view construction.
2//!
3//! ```ignore
4//! use swiftui_native::prelude::*;
5//!
6//! text("Hello").bold().padding(16.0).bg(Color::BLUE).opacity(0.9)
7//! ```
8
9use crate::color::Color;
10use crate::views::{self, ViewHandle};
11
12/// A SwiftUI view with chainable modifiers.
13pub struct View {
14 pub(crate) handle: ViewHandle,
15}
16
17impl View {
18 /// Wrap a handle so the chainable modifiers can be used on it.
19 pub fn new(handle: ViewHandle) -> Self {
20 Self { handle }
21 }
22
23 /// Get the underlying handle.
24 pub fn handle(&self) -> &ViewHandle {
25 &self.handle
26 }
27
28 /// Consume and return the handle.
29 pub fn into_handle(self) -> ViewHandle {
30 self.handle
31 }
32
33}
34
35// `View`'s chainable methods, by subject. Rust lets a type's inherent methods
36// live in several modules, and four hundred of them in one file was a file
37// nobody could find anything in — each round's section had been inserted
38// wherever the last edit happened to land, so "layout" appeared five times and
39// "navigation" three.
40mod accessibility;
41mod generated;
42pub mod style;
43mod appearance;
44mod controls;
45mod interaction;
46mod layout;
47mod navigation;
48mod system;
49mod text;
50
51
52impl std::fmt::Debug for View {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 write!(f, "View({:?})", self.handle)
55 }
56}
57
58/// Anything that can become a `View`.
59pub trait IntoView {
60 fn into_view(self) -> View;
61}
62
63impl IntoView for View {
64 fn into_view(self) -> View {
65 self
66 }
67}
68
69impl IntoView for ViewHandle {
70 fn into_view(self) -> View {
71 View::new(self)
72 }
73}
74
75impl IntoView for &str {
76 fn into_view(self) -> View {
77 text(self)
78 }
79}
80
81impl IntoView for String {
82 fn into_view(self) -> View {
83 text(&self)
84 }
85}
86
87impl IntoView for &String {
88 fn into_view(self) -> View {
89 text(self)
90 }
91}
92
93macro_rules! into_view_via_display {
94 ($($t:ty),*) => {
95 $(
96 impl IntoView for $t {
97 fn into_view(self) -> View {
98 text(&self.to_string())
99 }
100 }
101 )*
102 };
103}
104
105// Numbers are common enough in a UI that requiring `format!` at every call site
106// is just noise.
107into_view_via_display!(i32, i64, u32, u64, usize, f32, f64, bool);
108
109/// `Optional` is a view in SwiftUI: `nil` renders as nothing.
110///
111/// So `Some(v)` is `v` and `None` is an `EmptyView`, which is what lets a
112/// `children.iter().map(...)` that sometimes yields nothing be passed straight
113/// into a container.
114impl<T: IntoView> IntoView for Option<T> {
115 fn into_view(self) -> View {
116 match self {
117 Some(v) => v.into_view(),
118 None => empty(),
119 }
120 }
121}
122
123// ═══════════════════════════════════════════════════════════════════════════
124// Free functions — the ergonomic DSL
125// ═══════════════════════════════════════════════════════════════════════════
126
127/// Create a `Text` view.
128pub fn text(s: &str) -> View {
129 View::new(views::text(s))
130}
131
132/// Create a `Color` view.
133pub fn color(c: Color) -> View {
134 View::new(views::color(c.r, c.g, c.b, c.a))
135}
136
137/// Create a `Spacer`.
138pub fn spacer() -> View {
139 View::new(views::spacer())
140}
141
142/// Create a `Divider`.
143pub fn divider() -> View {
144 View::new(views::divider())
145}
146
147/// Create an `EmptyView`.
148pub fn empty() -> View {
149 View::new(views::empty_view())
150}
151
152// The constructors for the things this crate *synthesises*. They are free
153// functions rather than chainable methods because they start a view rather than
154// modify one — `shape` and `ns_view` are to `custom_shape` and
155// `ns_view_representable` what `text` is to `views::text`.
156
157/// A `Shape` written in Rust, drawn with the foreground style.
158///
159/// ```no_run
160/// # use swiftui_native::prelude::*;
161/// # use swiftui_native::views;
162/// let chevron = views::shape_fn(|p: &mut views::ShapePath, r: views::Rect| {
163/// p.move_to(r.x, r.y + r.height);
164/// p.line_to(r.x + r.width * 0.5, r.y);
165/// p.line_to(r.x + r.width, r.y + r.height);
166/// });
167/// let v = shape(&chevron).frame(80.0, 80.0);
168/// ```
169pub fn shape(s: &views::CustomShape) -> View {
170 View::new(views::custom_shape(s))
171}
172
173/// An AppKit view hosted in a SwiftUI tree.
174///
175/// See [`views::ns_view_fn`] for making one.
176#[cfg(target_os = "macos")]
177pub fn ns_view(rep: &views::NSViewRep) -> View {
178 View::new(views::ns_view_representable(rep))
179}
180
181/// A `ScrollViewReader`, whose content is handed a proxy it can scroll with.
182pub fn scroll_reader<V: IntoView>(
183 content: impl Fn(views::ScrollProxy) -> V + 'static,
184) -> View {
185 View::new(views::scroll_view_reader(move |proxy| {
186 content(proxy).into_view().into_handle()
187 }))
188}
189
190/// `ContentUnavailableView` — the placeholder for a screen with nothing on it.
191pub fn content_unavailable(title: &str, system_image: &str, description: Option<&str>) -> View {
192 View::new(views::content_unavailable(title, system_image, description))
193}
194
195/// Create an SF Symbol `Image`.
196pub fn image(system_name: &str) -> View {
197 View::new(views::system_image(system_name))
198}
199
200/// Create a `VStack`.
201pub fn vstack(children: &[View]) -> View {
202 let handles: Vec<ViewHandle> = children.iter().map(|v| v.handle.clone()).collect();
203 View::new(views::vstack(&handles))
204}
205
206/// Create an `HStack`.
207/// The handles behind a slice of views, for the stack constructors.
208fn handles(children: &[View]) -> Vec<ViewHandle> {
209 children.iter().map(|v| v.handle.clone()).collect()
210}
211
212/// `AsyncImage(url:scale:)` — an image fetched from a URL.
213///
214/// `None` is the empty case, which is what SwiftUI shows before a URL resolves.
215pub fn async_image(url: Option<&str>, scale: f64) -> View {
216 View::new(views::async_image(url, scale))
217}
218
219/// `Canvas { … }` — immediate-mode drawing.
220pub fn canvas() -> View {
221 View::new(views::canvas())
222}
223
224/// `ProgressView()` with no value — the indeterminate spinner, as distinct from
225/// [`progress`], which shows a fraction.
226pub fn progress_indeterminate() -> View {
227 View::new(views::progress_indeterminate())
228}
229
230/// `Text(verbatim:)` — shown exactly as given.
231///
232/// [`text()`] goes through `LocalizedStringKey`, so a string that looks like a key
233/// is looked up. This one never is, which is what you want for a name, a number
234/// or anything already in the user's own words.
235pub fn text_verbatim(s: &str) -> View {
236 View::new(views::text_verbatim(s))
237}
238
239/// `EquatableView(content:)` — let SwiftUI skip redrawing when the value has not
240/// changed.
241pub fn equatable_text(text: &str) -> View {
242 View::new(views::equatable_text(text))
243}
244
245/// `Table(of:)` over interface orientations, with a single column.
246pub fn table(rows: &[views::Orientation], title: &str, cell: &str) -> View {
247 View::new(views::table(rows, title, cell))
248}
249
250/// `TupleView<(A, B, …)>` — several views with no container around them.
251///
252/// This is what a `@ViewBuilder` block evaluates to, and it is why
253/// `List { Text("a"); Text("b") }` is two rows: a `List` renders each element of
254/// its content. Wrapping them in a stack instead gives one row holding a stack,
255/// which is a different screen.
256///
257/// Usually reached through the container macros — [`crate::list!`],
258/// [`crate::form!`], [`crate::section!`] — rather than called directly.
259pub fn tuple_view(children: &[View]) -> View {
260 View::new(views::tuple_view(&handles(children)))
261}
262
263/// `VStack(alignment:) { … }` — how children line up across the stack's width.
264pub fn vstack_aligned(children: &[View], alignment: views::HorizontalAlignment) -> View {
265 View::new(views::vstack_aligned(&handles(children), alignment))
266}
267
268/// `VStack(alignment:spacing:) { … }` — `None` keeps SwiftUI's own spacing.
269pub fn vstack_aligned_spacing(
270 children: &[View],
271 alignment: views::HorizontalAlignment,
272 spacing: Option<f64>,
273) -> View {
274 View::new(views::vstack_aligned_spacing(&handles(children), alignment, spacing))
275}
276
277/// `HStack(alignment:) { … }` — how children line up across the stack's
278/// height. The baseline cases align on the children's text rather than on
279/// their boxes.
280pub fn hstack_aligned(children: &[View], alignment: views::VerticalAlignment) -> View {
281 View::new(views::hstack_aligned(&handles(children), alignment))
282}
283
284/// `HStack(alignment:spacing:) { … }` — `None` keeps SwiftUI's own spacing.
285pub fn hstack_aligned_spacing(
286 children: &[View],
287 alignment: views::VerticalAlignment,
288 spacing: Option<f64>,
289) -> View {
290 View::new(views::hstack_aligned_spacing(&handles(children), alignment, spacing))
291}
292
293/// `Section { … }` with no header.
294pub fn section(content: View) -> View {
295 View::new(views::section_content(content.handle()))
296}
297
298/// `Section { … } header: { … }`
299pub fn section_titled(header: View, content: View) -> View {
300 View::new(views::section(header.handle(), content.handle()))
301}
302
303/// `Section { … } footer: { … }`
304///
305/// A footer is where a settings group's explanatory line goes — the small text
306/// under a block of toggles saying what they do. Only the no-header and
307/// header-only initialisers were bound, so there was nowhere to put one.
308pub fn section_footer(footer: View, content: View) -> View {
309 View::new(views::section_footer(footer.handle(), content.handle()))
310}
311
312/// `Section { … } header: { … } footer: { … }`
313pub fn section_titled_footer(header: View, footer: View, content: View) -> View {
314 View::new(views::section_header_footer(
315 header.handle(),
316 footer.handle(),
317 content.handle(),
318 ))
319}
320
321/// `Section(isExpanded:) { … } header: { … }` — a section that collapses.
322///
323/// The disclosure state is bound to Rust state rather than given as an initial
324/// value, so the section opens and closes by itself *and* whatever else reads
325/// that state sees it.
326///
327/// ```no_run
328/// # use swiftui_native::prelude::*;
329/// # use swiftui_native::views::BoundState;
330/// let open = BoundState::new(true);
331/// let s = section_expanded(&open, text("Advanced"), text("the details"));
332/// ```
333pub fn section_expanded(
334 is_expanded: &views::BoundState<bool>,
335 header: View,
336 content: View,
337) -> View {
338 View::new(views::section_expanded(
339 is_expanded,
340 header.handle(),
341 content.handle(),
342 ))
343}
344
345/// `Tab("Home", systemImage: "house") { … }` — one tab of a modern `TabView`.
346///
347/// The older way to build a tab is `.tabItem { Label(…) }` on the page itself;
348/// `Tab` is the replacement, where the tab and its label are one value rather
349/// than a modifier on the content.
350///
351/// A `Tab` is not a view — it is a `TabContent`, and only [`tab_view_tabs`]
352/// accepts one.
353pub fn tab(title: &str, system_image: &str, content: View) -> View {
354 View::new(views::tab(title, system_image, content.handle()))
355}
356
357/// `TabView { Tab(…); Tab(…); … }` — the modern tab view.
358///
359/// The older way is [`tab_view`](crate::tab_view) with `.tab_item` on each
360/// page; this takes [`tab`] values, where the tab and its label are one value
361/// rather than a modifier on the content.
362///
363/// ```no_run
364/// # use swiftui_native::prelude::*;
365/// let view = tab_view_tabs(vec![
366/// tab("Home", "house", text("home page")),
367/// tab("Settings", "gear", text("settings page")),
368/// ]);
369/// ```
370pub fn tab_view_tabs(tabs: Vec<View>) -> View {
371 let handles: Vec<_> = tabs.into_iter().map(|t| t.into_handle()).collect();
372 View::new(views::tab_view_tabs(&handles))
373}
374
375/// `Stepper(_:onIncrement:onDecrement:)` — a stepper driven by callbacks.
376///
377/// [`stepper_bound`] binds a number and a range, which is right when there is a
378/// number to bind. A stepper that adds and removes rows, or nudges something
379/// the Rust side computes, has no such number — and there was no way to ask for
380/// one.
381///
382/// `None` for either half greys that side out, which is how SwiftUI says "you
383/// cannot go further this way".
384///
385/// ```no_run
386/// # use swiftui_native::prelude::*;
387/// let rows = stepper_actions(
388/// "Rows",
389/// Some(Box::new(|| println!("more"))),
390/// Some(Box::new(|| println!("fewer"))),
391/// );
392/// ```
393pub fn stepper_actions(
394 label: &str,
395 on_increment: Option<Box<dyn Fn()>>,
396 on_decrement: Option<Box<dyn Fn()>>,
397) -> View {
398 View::new(views::stepper_actions(label, on_increment, on_decrement))
399}
400
401/// `Toggle(isOn:) { label }` — a toggle whose label is any view.
402///
403/// [`toggle_bound`] takes a string, which becomes a `Text`. The convenience
404/// that takes a symbol name — `Toggle(_:systemImage:isOn:)` — is inlinable and
405/// exports nothing, so it cannot be called; but it only builds a `Label`, and
406/// this takes any view at all, so the same row is reachable and any other label
407/// with it.
408///
409/// ```no_run
410/// # use swiftui_native::prelude::*;
411/// # use swiftui_native::views::BoundState;
412/// let wifi = BoundState::new(true);
413/// let row = toggle_labelled(label("Wi-Fi", "wifi"), &wifi);
414/// ```
415pub fn toggle_labelled(label: View, state: &views::BoundState<bool>) -> View {
416 View::new(views::toggle_labelled(label.handle(), state))
417}
418
419/// `DatePicker(_:selection:in:displayedComponents:)` — a date picker with
420/// bounds.
421///
422/// "Pick a date after today" is a constraint a date picker exists to express,
423/// and only the unbounded initialiser was bound — so every picker offered every
424/// date and the check had to happen afterwards.
425///
426/// The bounds are seconds from Apple's reference date, the same scale
427/// [`date_picker_bound`] takes.
428///
429/// ```no_run
430/// # use swiftui_native::prelude::*;
431/// # use swiftui_native::views::BoundState;
432/// let when = BoundState::new(500_000.0);
433/// let picker = date_picker_range("When", &when, 0.0, 1_000_000.0);
434/// ```
435pub fn date_picker_range(
436 title: &str,
437 state: &views::BoundState<f64>,
438 lower: f64,
439 upper: f64,
440) -> View {
441 View::new(views::date_picker_range(title, state, lower, upper))
442}
443
444/// `Toggle(_:sources:isOn:)` — one toggle over several bindings.
445///
446/// The "select all" row at the top of a settings group: it reads *mixed* when
447/// the sources disagree, and setting it sets every one of them. There was no
448/// way to express that — a toggle took exactly one binding, so the mixed state
449/// had to be faked with a third value that nothing wrote back to.
450///
451/// ```no_run
452/// # use swiftui_native::prelude::*;
453/// # use swiftui_native::views::BoundState;
454/// let wifi = BoundState::new(true);
455/// let bluetooth = BoundState::new(false);
456/// let all = toggle_sources("All", &[&wifi, &bluetooth]);
457/// ```
458pub fn toggle_sources(label: &str, states: &[&views::BoundState<bool>]) -> View {
459 View::new(views::toggle_sources(label, states))
460}
461
462/// `Group { … }` — several views where one is wanted, with no layout of its own.
463pub fn group(content: View) -> View {
464 View::new(views::group(content.handle()))
465}
466
467/// `ControlGroup { … }`
468pub fn control_group(content: View) -> View {
469 View::new(views::control_group(content.handle()))
470}
471
472/// `Menu(_:) { … }`
473pub fn menu(label: &str, content: View) -> View {
474 View::new(views::menu(label, content.handle()))
475}
476
477/// `Link(_:destination:)` — opens a URL.
478pub fn link(title: &str, destination: &str) -> View {
479 View::new(views::link(title, destination))
480}
481
482/// `ShareLink(item:)` over a URL.
483pub fn share_link(url: &str) -> View {
484 View::new(views::share_link_url(url))
485}
486
487/// `Gauge(value:in:) { label }`
488pub fn gauge(value: f64, lower: f64, upper: f64, label: View) -> View {
489 View::new(views::gauge(value, lower, upper, label.handle()))
490}
491
492/// `ViewThatFits(in:) { … }` — the first child that fits, along the given axes.
493pub fn view_that_fits(content: View, axes: &[views::AxisSet]) -> View {
494 View::new(views::view_that_fits(content.handle(), axes))
495}
496
497/// `NavigationSplitView { sidebar } detail: { detail }`
498pub fn navigation_split_view(sidebar: View, detail: View) -> View {
499 View::new(views::navigation_split_view(sidebar.handle(), detail.handle()))
500}
501
502/// `NavigationSplitView { sidebar } content: { content } detail: { detail }`
503///
504/// The three-column shape — a sidebar, a list, and the selected item — which is
505/// what a Mail or Notes window is. [`navigation_split_view`] pins the middle
506/// column to `EmptyView`, so it could not be filled at all.
507pub fn navigation_split_view3(sidebar: View, content: View, detail: View) -> View {
508 View::new(views::navigation_split_view3(
509 sidebar.handle(),
510 content.handle(),
511 detail.handle(),
512 ))
513}
514
515/// `OutlineGroup(_:children:)` over an `Int` root.
516pub fn outline_group(root: i64, leaf: View) -> View {
517 View::new(views::outline_group(root, leaf.handle()))
518}
519
520/// `TextEditor(text:)` showing a fixed string, which the user cannot edit.
521///
522/// As with [`textfield`], the string becomes a `Binding.constant` and SwiftUI
523/// treats that as read-only.
524pub fn text_editor(value: &str) -> View {
525 View::new(views::text_editor(value))
526}
527
528/// `EditButton()` — iOS only; on macOS SwiftUI traps if it is reached.
529#[cfg(not(target_os = "macos"))]
530pub fn edit_button() -> View {
531 View::new(views::edit_button())
532}
533
534/// `Button { action } label: { … }` — a button whose label is a view rather
535/// than a string.
536pub fn button_labelled(label: View, action: impl Fn() + 'static) -> View {
537 View::new(views::button_labelled(label.handle(), action))
538}
539
540/// `VStack(spacing:) { … }`.
541pub fn vstack_spacing(children: &[View], spacing: f64) -> View {
542 View::new(views::vstack_spacing(&handles(children), spacing))
543}
544
545/// `HStack(spacing:) { … }`.
546pub fn hstack_spacing(children: &[View], spacing: f64) -> View {
547 View::new(views::hstack_spacing(&handles(children), spacing))
548}
549
550/// `ZStack(alignment:) { … }`.
551pub fn zstack_aligned(children: &[View], alignment: views::Alignment) -> View {
552 View::new(views::zstack_aligned(&handles(children), alignment))
553}
554
555pub fn hstack(children: &[View]) -> View {
556 let handles: Vec<ViewHandle> = children.iter().map(|v| v.handle.clone()).collect();
557 View::new(views::hstack(&handles))
558}
559
560/// Create a `ZStack`.
561pub fn zstack(children: &[View]) -> View {
562 let handles: Vec<ViewHandle> = children.iter().map(|v| v.handle.clone()).collect();
563 View::new(views::zstack(&handles))
564}
565
566/// Create a `Button`.
567pub fn button(label: &str, action: impl Fn() + 'static) -> View {
568 View::new(views::button(label, action))
569}
570
571/// Create a `Toggle`.
572pub fn toggle(label: &str, is_on: bool) -> View {
573 View::new(views::toggle(label, is_on))
574}
575
576/// Create a `Toggle` wired to Rust state, readable and writable from both sides.
577pub fn toggle_bound(label: &str, state: &views::BoundState<bool>) -> View {
578 View::new(views::toggle_bound(label, state))
579}
580
581/// Create a `TextField` showing a fixed string, which the user cannot edit.
582///
583/// The string becomes a `Binding.constant`, so keystrokes have nowhere to go
584/// and the field ignores them. [`textfield_bound`] is the editable one.
585pub fn textfield(placeholder: &str, value: &str) -> View {
586 View::new(views::textfield(placeholder, value))
587}
588
589/// Create a `TextField` wired to Rust state — every keystroke writes into it.
590pub fn textfield_bound(placeholder: &str, state: &views::BoundState<String>) -> View {
591 View::new(views::textfield_bound(placeholder, state))
592}
593
594/// `TextField(_:text:axis:)` — a field that grows along `axis` as it is typed
595/// into.
596///
597/// `Axis::Vertical` is the multi-line one: it starts a line high and adds rows
598/// as the text wraps, which is what a comment box or a note is. Only the
599/// deprecated `onEditingChanged:onCommit:` initialiser was bound, so every
600/// field was one line whatever went in it.
601///
602/// ```no_run
603/// # use swiftui_native::prelude::*;
604/// # use swiftui_native::views::{Axis, BoundState};
605/// let note = BoundState::new(String::new());
606/// let field = textfield_axis("Note", ¬e, Axis::Vertical);
607/// ```
608pub fn textfield_axis(
609 placeholder: &str,
610 state: &views::BoundState<String>,
611 axis: views::Axis,
612) -> View {
613 View::new(views::textfield_axis(placeholder, state, axis))
614}
615
616/// Create a `SecureField` showing a fixed string, which the user cannot edit.
617/// See [`securefield_bound`].
618pub fn securefield(placeholder: &str, value: &str) -> View {
619 View::new(views::securefield(placeholder, value))
620}
621
622/// Create a `SecureField` wired to Rust state — every keystroke writes into it.
623pub fn securefield_bound(placeholder: &str, state: &views::BoundState<String>) -> View {
624 View::new(views::securefield_bound(placeholder, state))
625}
626
627/// Create a `Slider` showing a fixed value, which the user cannot move.
628///
629/// The value becomes a `Binding.constant` and SwiftUI treats that as read-only.
630/// [`slider_bound`] is the one that can be dragged.
631pub fn slider(value: f64, lower: f64, upper: f64) -> View {
632 View::new(views::slider(value, lower, upper))
633}
634
635/// Create a `Slider` wired to Rust state, readable and writable from both sides.
636pub fn slider_bound(state: &views::BoundState<f64>, lower: f64, upper: f64) -> View {
637 View::new(views::slider_bound(state, lower, upper))
638}
639
640/// `Slider(value:in:step:)` — a slider that snaps to multiples of `step`.
641///
642/// A continuous slider is right for a volume and wrong for a rating out of
643/// five; only the continuous one was bound.
644///
645/// ```no_run
646/// # use swiftui_native::prelude::*;
647/// # use swiftui_native::views::BoundState;
648/// let rating = BoundState::new(3.0);
649/// let stars = slider_step_bound(&rating, 0.0, 5.0, 1.0);
650/// ```
651pub fn slider_step_bound(
652 state: &views::BoundState<f64>,
653 lower: f64,
654 upper: f64,
655 step: f64,
656) -> View {
657 View::new(views::slider_step_bound(state, lower, upper, step))
658}
659
660/// Create a `Picker` over `content`, showing a fixed selection the user cannot
661/// change. See [`picker_bound()`].
662pub fn picker(title: &str, selection: i64, content: View) -> View {
663 View::new(views::picker(title, selection, content.handle()))
664}
665
666/// Create a `Picker` wired to Rust state — choosing a row writes its tag into
667/// `state`.
668///
669/// The rows are tagged with [`View::tag`]; the tag is what lands in `state`.
670pub fn picker_bound(title: &str, state: &views::BoundState<i64>, content: View) -> View {
671 View::new(views::picker_bound(title, state, content.handle()))
672}
673
674/// Create a `DatePicker` showing a fixed date the user cannot change, as
675/// seconds since the reference date. See [`date_picker_bound`].
676pub fn date_picker(title: &str, interval: f64) -> View {
677 View::new(views::date_picker(title, interval))
678}
679
680/// Create a `DatePicker` wired to Rust state, in seconds since the reference date.
681pub fn date_picker_bound(title: &str, state: &views::BoundState<f64>) -> View {
682 View::new(views::date_picker_bound(title, state))
683}
684
685/// Create a `ColorPicker` showing a fixed colour the user cannot change.
686/// See [`color_picker_bound`].
687pub fn color_picker(title: &str, color: Color, supports_opacity: bool) -> View {
688 View::new(views::color_picker(title, color, supports_opacity))
689}
690
691/// Create a `ColorPicker` wired to Rust state.
692///
693/// The state holds the colour as the single reference `Color` is — what
694/// [`views::color_value_of`] returns — because that is what the binding reads
695/// and writes. `Color` is a class instance, not a struct of components, so
696/// there is no triple to store.
697pub fn color_picker_bound(
698 title: &str,
699 state: &views::BoundState<u64>,
700 supports_opacity: bool,
701) -> View {
702 View::new(views::color_picker_bound(title, state, supports_opacity))
703}
704
705/// Create a `ProgressView`.
706pub fn progress(value: f64, total: f64) -> View {
707 View::new(views::progress(value, total))
708}
709
710/// Create a `Label(_:systemImage:)`.
711pub fn label(title: &str, system_image: &str) -> View {
712 View::new(views::label(title, system_image))
713}
714
715/// `Rectangle()`
716pub fn rectangle() -> View {
717 View::new(views::rectangle())
718}
719
720/// Which transition a view uses when it appears or leaves.
721// `Eq` is gone from the derive: two of these carry an `f64`, and floats have no
722// total equality to derive one from.
723#[derive(Clone, Copy, PartialEq, Debug)]
724pub enum Transition {
725 /// `.opacity` — fade.
726 Opacity,
727 /// `.slide` — in from the leading edge, out to the trailing.
728 Slide,
729 /// `.scale(scale:anchor:)` — grow from a unit point.
730 Scale { scale: f64, anchor: (f64, f64) },
731 /// `.scale(…).combined(with: .opacity)` — the pair SwiftUI's own
732 /// `.scale` transitions are usually written with, since scaling from
733 /// nothing without fading looks like a pop.
734 ScaleWithOpacity { scale: f64, anchor: (f64, f64) },
735 /// `.scale` — the bare static property, which is `Scale` with SwiftUI's
736 /// own factor and anchor rather than ones given here.
737 ScaleDefault,
738 /// `.identity` — no transition, and not the same as no modifier: this
739 /// overrides one a container would otherwise hand down.
740 Identity,
741}
742
743/// A shape, before it is given something to draw with.
744///
745/// `Circle()` is a `Shape`, not yet a `View`: SwiftUI gives it a default fill
746/// when one is used as a view, and `.fill()` says what to use instead. Keeping
747/// that distinction here is what lets `circle().fill(.red)` build the type the
748/// same line of Swift builds, rather than a `Circle` with a foreground style
749/// over it.
750pub struct Shape {
751 inner: views::Shape,
752}
753
754impl Shape {
755 /// `.fill(_)` — draw the shape in one colour.
756 pub fn fill(self, c: Color) -> View {
757 View::new(self.inner.fill(views::color_value_of(c)))
758 }
759
760 /// `.fill(_)` with one of SwiftUI's own colours.
761 ///
762 /// `.blue` is not a fixed triple: it is `(0, 0.569, 1)` on the Mac and
763 /// `#007AFF` on iOS, and it changes again in dark appearance. Naming the
764 /// colour asks SwiftUI for whichever one is right, where spelling out
765 /// components picks one platform's answer and freezes it.
766 pub fn fill_system(self, c: views::SystemColor) -> View {
767 View::new(self.inner.fill(views::system_color_value(c)))
768 }
769
770 /// The shape as a view, with SwiftUI's default fill.
771 pub fn view(self) -> View {
772 View::new(self.inner.fill(views::color_value_of(Color::BLACK)))
773 }
774
775 /// `.stroke(_:lineWidth:)` — the outline instead of the fill.
776 pub fn stroke(self, c: Color, line_width: f64) -> View {
777 View::new(self.inner.stroke(views::color_value_of(c), line_width))
778 }
779
780 /// `.stroke(_:lineWidth:)` with one of SwiftUI's own colours.
781 pub fn stroke_system(self, c: views::SystemColor, line_width: f64) -> View {
782 View::new(self.inner.stroke(views::system_color_value(c), line_width))
783 }
784
785 /// `.fill(_).stroke(_, lineWidth:)` — both at once, which a shape cannot
786 /// express by chaining because `.fill` already produces a view.
787 ///
788 /// Works on transformed shapes too: `.inset(2)` gives a `Circle._Inset`,
789 /// which this now spells correctly. It used to assert, because
790 /// `inset(by:)` returns `some InsettableShape` and the binding kept the
791 /// shape it started as — so the result was named `StrokeShapeView<Circle,
792 /// …>`, a type that exists, is the wrong one, and is laid out differently.
793 ///
794 /// The field offsets are still computed by padding each to eight rather
795 /// than read from the metadata, and still checked against the type's own
796 /// size before anything is written.
797 pub fn fill_then_stroke(self, fill: Color, stroke: Color, line_width: f64) -> View {
798 // The result is `StrokeShapeView<S, Color, _ShapeView<S, Color>>`, which
799 // has to be spelled with whatever this shape actually is — after
800 // `.inset` or `.rotation` that is no longer what it started as, which is
801 // why the shape carries its own `mangle::Type`.
802 use crate::mangle;
803 let shape = self.inner.ty().clone();
804 let color = mangle::ty("Color");
805 let filled = mangle::ty("_ShapeView").of(vec![shape.clone(), color.clone()]);
806 let result = mangle::ty("StrokeShapeView").of(vec![shape, color, filled]).mangle();
807 View::new(views::fill_then_stroke(
808 &self.inner,
809 views::color_value_of(fill),
810 views::color_value_of(stroke),
811 line_width,
812 result.as_bytes(),
813 ))
814 }
815
816 /// `.fill(_).strokeBorder(_, lineWidth:)` — filled, then bordered *inside*
817 /// its own edge.
818 ///
819 /// The difference from [`fill_then_stroke`](Self::fill_then_stroke) is
820 /// where the line sits: a stroke straddles the edge and a border is drawn
821 /// within it, so a bordered shape never paints outside its bounds.
822 pub fn fill_then_stroke_border(self, fill: Color, border: Color, line_width: f64) -> View {
823 use crate::mangle;
824 // `StrokeBorderShapeView<Shape, Style, Background>` — the third
825 // parameter is what goes *behind*, which here is the filled shape, the
826 // same way `strokeBorder` alone puts an `EmptyView` there.
827 let shape = self.inner.ty().clone();
828 let color = mangle::ty("Color");
829 let filled = mangle::ty("_ShapeView").of(vec![shape.clone(), color.clone()]);
830 let result = mangle::ty("StrokeBorderShapeView")
831 .of(vec![shape, color, filled])
832 .mangle();
833 View::new(views::fill_then_stroke_border(
834 &self.inner,
835 views::color_value_of(fill),
836 views::color_value_of(border),
837 line_width,
838 result.as_bytes(),
839 ))
840 }
841
842 /// `.stroke(lineWidth:)` — outlined in the inherited foreground style,
843 /// which is what picks up an enclosing `.foregroundStyle`.
844 pub fn stroke_default(self, line_width: f64) -> View {
845 View::new(self.inner.stroke_default(line_width))
846 }
847
848 /// `.trim(from:to:)` — keep a fraction of the path, as unit fractions.
849 ///
850 /// How a progress ring is drawn: a stroked circle trimmed to the fraction
851 /// complete, usually with `.rotate(-90.0)` after it so the arc starts at the
852 /// top rather than at three o'clock. Without it a "ring showing 70%" is a
853 /// full circle.
854 pub fn trim(self, from: f64, to: f64) -> Self {
855 Self { inner: self.inner.trim(from, to) }
856 }
857
858 /// `.inset(by:)` — still a shape, so it can be filled or stroked after.
859 ///
860 /// Insetting before stroking is how you keep a `lineWidth` inside the
861 /// frame; stroking alone centres the line on the edge and spills half of it.
862 pub fn inset(self, amount: f64) -> Self {
863 Self { inner: self.inner.inset(amount) }
864 }
865
866 /// `.offset(x:y:)` on the shape itself, before it is filled.
867 pub fn offset(self, x: f64, y: f64) -> Self {
868 Self { inner: self.inner.offset(x, y) }
869 }
870
871 /// `.rotation(.degrees(_))` on the shape's path.
872 pub fn rotation(self, degrees: f64) -> Self {
873 Self { inner: self.inner.rotation(degrees) }
874 }
875}
876
877impl IntoView for Shape {
878 fn into_view(self) -> View {
879 self.view()
880 }
881}
882
883/// A SwiftUI `Image`, before it becomes a view.
884///
885/// The same reason [`Shape`] exists: `Image` has modifiers of its own that
886/// return `Image` — `.resizable()`, `.renderingMode(_:)`,
887/// `.symbolRenderingMode(_:)` — and going through the `View` extensions instead
888/// wraps it in a `ModifiedContent` and changes the type. Reaching them means
889/// keeping the `Image` around rather than turning it into a view straight away.
890pub struct Image {
891 inner: views::Image,
892}
893
894impl Image {
895 /// `.resizable()`, with the zero cap insets Swift source gets from the bare
896 /// call.
897 pub fn resizable(self) -> Self {
898 Self { inner: self.inner.resizable(views::ResizingMode::Stretch) }
899 }
900
901 /// `.resizable(resizingMode:)`
902 pub fn resizable_mode(self, mode: views::ResizingMode) -> Self {
903 Self { inner: self.inner.resizable(mode) }
904 }
905
906 /// `.renderingMode(_:)` — `None` restores the default.
907 pub fn rendering_mode(self, mode: Option<views::TemplateRenderingMode>) -> Self {
908 Self { inner: self.inner.rendering_mode(mode) }
909 }
910
911 /// `.symbolRenderingMode(_:)`
912 pub fn symbol_rendering_mode(self, mode: Option<views::SymbolRenderingMode>) -> Self {
913 Self { inner: self.inner.symbol_rendering_mode(mode) }
914 }
915
916 /// `.antialiased(_:)`
917 pub fn antialiased(self, on: bool) -> Self {
918 Self { inner: self.inner.antialiased(on) }
919 }
920
921 /// The image as a view, so the `View` modifiers can be chained after.
922 pub fn view(self) -> View {
923 View::new(self.inner.view())
924 }
925}
926
927impl IntoView for Image {
928 fn into_view(self) -> View {
929 self.view()
930 }
931}
932
933/// `Image(systemName:)`, kept as an `Image` so its own modifiers are reachable.
934pub fn system_image(name: &str) -> Image {
935 Image { inner: views::Image::system(name) }
936}
937
938/// `Image(_:)` — a named asset, kept as an `Image`.
939pub fn named_image(name: &str) -> Image {
940 Image { inner: views::Image::named(name) }
941}
942
943/// `Circle()`, ready to be filled.
944pub fn circle_shape() -> Shape {
945 Shape { inner: views::Shape::circle() }
946}
947
948/// `Capsule()`, ready to be filled.
949pub fn capsule_shape() -> Shape {
950 Shape { inner: views::Shape::capsule() }
951}
952
953/// `Rectangle()`, ready to be filled.
954pub fn rectangle_shape() -> Shape {
955 Shape { inner: views::Shape::rectangle() }
956}
957
958/// `RoundedRectangle(cornerRadius:)`, ready to be filled.
959pub fn rounded_rectangle_shape(radius: f64) -> Shape {
960 Shape { inner: views::Shape::rounded_rectangle(radius, true) }
961}
962
963/// `RoundedRectangle(cornerRadius:style:)` — `false` gives the circular arc
964/// rather than Swift's default squircle.
965pub fn rounded_rectangle_shape_styled(radius: f64, continuous: bool) -> Shape {
966 Shape { inner: views::Shape::rounded_rectangle(radius, continuous) }
967}
968
969/// `Ellipse()`, ready to be filled.
970pub fn ellipse_shape() -> Shape {
971 Shape { inner: views::Shape::ellipse() }
972}
973
974/// `Circle()`
975pub fn circle() -> View {
976 View::new(views::circle())
977}
978
979/// `Ellipse()`
980pub fn ellipse() -> View {
981 View::new(views::ellipse())
982}
983
984/// `Capsule()`
985pub fn capsule() -> View {
986 View::new(views::capsule())
987}
988
989/// `RoundedRectangle(cornerRadius:)`
990pub fn rounded_rectangle(radius: f64) -> View {
991 View::new(views::rounded_rectangle(radius))
992}
993
994/// `ForEach(0..<count) { … }`.
995pub fn for_each<R: views::ForEachRange, V: IntoView>(
996 range: R,
997 builder: impl Fn(i64) -> V + 'static,
998) -> View {
999 View::new(views::for_each(range, move |i| builder(i).into_view().into_handle()))
1000}
1001
1002/// `ScrollView { … }` — a vertical scroll view with indicators.
1003///
1004/// Named for what the macro derives from `ScrollView`, so the spelling in a
1005/// `swiftui!` body and the one here are the same word.
1006pub fn scroll_view(content: View) -> View {
1007 View::new(views::scroll(content.handle()))
1008}
1009
1010/// `GeometryReader { … }`
1011/// `ForEach` over a Rust collection.
1012///
1013/// The common case, and the one `for_each` makes awkward: with only a range, a
1014/// caller writes `for_each(0..items.len() as i64, …)` and indexes back into the
1015/// slice, which is the loop SwiftUI was supposed to be writing.
1016///
1017/// Identity is still positional — this builds on `ForEach(0..<count)`, so
1018/// inserting in the middle makes SwiftUI treat every later row as changed rather
1019/// than moved, exactly as the Swift spelling would.
1020pub fn for_each_items<T: Clone + 'static, V: IntoView>(
1021 items: &[T],
1022 builder: impl Fn(&T) -> V + 'static,
1023) -> View {
1024 View::new(views::for_each_items(items, move |t| builder(t).into_view().into_handle()))
1025}
1026
1027/// A view whose content is a function of the environment.
1028///
1029/// What `@Environment` is for, without having to be a view *type*: the closure is
1030/// handed the live `EnvironmentValues` and returns whatever it wants to build from
1031/// them. Reading a key declared here is
1032/// [`value_for`](views::EnvironmentValues::value_for), which is typed by the key.
1033///
1034/// ```no_run
1035/// # use swiftui_native::{environment_keys, prelude::*};
1036/// environment_keys! { depth: i64 = 0 }
1037///
1038/// # fn body() -> View {
1039/// environment_reader(|env| text(&format!("{} deep", env.value_for(depth()))))
1040/// # }
1041/// ```
1042pub fn environment_reader<V: IntoView>(
1043 builder: impl Fn(views::EnvironmentValues) -> V + 'static,
1044) -> View {
1045 View::new(views::environment_reader(move |env| {
1046 builder(env).into_view().into_handle()
1047 }))
1048}
1049
1050pub fn geometry_reader<V: IntoView>(builder: impl Fn(f64, f64) -> V + 'static) -> View {
1051 View::new(views::geometry_reader(move |w, h| builder(w, h).into_view().into_handle()))
1052}
1053
1054/// `NavigationLink(_:destination:)`
1055/// `NavigationLink(destination:) { label }` — a link whose label is a view.
1056///
1057/// What a list row is: a whole subtree, not a string. See [`navigation_link`]
1058/// for the string form.
1059pub fn navigation_link_labelled(label: View, destination: View) -> View {
1060 View::new(views::navigation_link_labelled(label.handle(), destination.handle()))
1061}
1062
1063pub fn navigation_link(label: &str, destination: View) -> View {
1064 View::new(views::navigation_link(label, destination.handle()))
1065}
1066
1067/// A `ViewBuilder` `if`/`else`, preserving branch identity.
1068pub fn conditional(flag: bool, if_true: View, if_false: View) -> View {
1069 View::new(views::conditional(flag, if_true.handle(), if_false.handle()))
1070}
1071
1072/// `TimelineView(.animation) { … }` — rebuild on SwiftUI's clock so that Rust
1073/// state changes appear without an explicit redisplay.
1074pub fn timeline<V: IntoView>(builder: impl Fn() -> V + 'static) -> View {
1075 View::new(views::timeline(move || builder().into_view().into_handle()))
1076}
1077
1078/// `List(ids, id: \.self) { id in … }` — rows identified by value.
1079///
1080/// [`list`](fn@list) and the [`list!`](crate::list) macro identify rows by
1081/// *position*,
1082/// because everything under them is built on `ForEach(0..<n)`. Insert a row at
1083/// the front and SwiftUI sees every later row as changed rather than moved: no
1084/// move animation, and per-row state follows the index instead of the row. That
1085/// is the same semantics as writing `ForEach(0..<n)` in Swift, and it is the
1086/// wrong semantics for a list of things.
1087///
1088/// Here the identifiers are the data. Moving an id moves its row; removing one
1089/// removes that row and not the last.
1090///
1091/// ```no_run
1092/// # use swiftui_native::prelude::*;
1093/// let ids = [10, 20, 30];
1094/// let rows = list_identified(&ids, |id| text(&format!("row {id}")));
1095/// ```
1096pub fn list_identified<V: IntoView>(ids: &[i64], builder: impl Fn(i64) -> V + 'static) -> View {
1097 View::new(views::list_identified(ids, move |id| builder(id).into_view().into_handle()))
1098}
1099
1100/// `List(names, id: \.self) { name in … }` — the same, keyed by `String`.
1101///
1102/// Which is what most lists key on: an identifier is usually a name or a UUID
1103/// string, and turning one into an integer to get identity back would defeat
1104/// the point.
1105///
1106/// ```no_run
1107/// # use swiftui_native::prelude::*;
1108/// let rows = list_identified_str(&["alpha", "beta"], |id| text(id));
1109/// ```
1110pub fn list_identified_str<V: IntoView>(
1111 ids: &[&str],
1112 builder: impl Fn(&str) -> V + 'static,
1113) -> View {
1114 View::new(views::list_identified_str(ids, move |id| {
1115 builder(id).into_view().into_handle()
1116 }))
1117}
1118
1119/// `List { … }`
1120pub fn list(child: View) -> View {
1121 View::new(views::list(child.handle()))
1122}
1123
1124/// `Form { … }`
1125pub fn form(child: View) -> View {
1126 View::new(views::form(child.handle()))
1127}
1128
1129/// `GroupBox { … }`
1130pub fn group_box(child: View) -> View {
1131 View::new(views::group_box(child.handle()))
1132}
1133
1134/// `TabView { … }`
1135pub fn tab_view(child: View) -> View {
1136 View::new(views::tab_view(child.handle()))
1137}
1138
1139/// `NavigationStack { … }`
1140pub fn navigation_stack(child: View) -> View {
1141 View::new(views::navigation_stack(child.handle()))
1142}
1143
1144/// `DisclosureGroup(_:content:)`
1145pub fn disclosure_group(label: &str, child: View) -> View {
1146 View::new(views::disclosure_group(label, child.handle()))
1147}
1148
1149/// A layout written in Rust, given children — `MyLayout { … }`.
1150///
1151/// ```no_run
1152/// # use swiftui_native::prelude::*;
1153/// # use swiftui_native::views::{Proposal, Rect, Subviews};
1154/// let row = layout(
1155/// |s: &Subviews<'_>, _| (s.len() as f64 * 44.0, 44.0),
1156/// |s: &Subviews<'_>, b: Rect, _| {
1157/// for i in 0..s.len() {
1158/// s.place(i, b.x + i as f64 * 44.0, b.y);
1159/// }
1160/// },
1161/// );
1162/// laid_out(&row, &[text("a"), text("b")]);
1163/// ```
1164pub fn laid_out(layout: &views::CustomLayout, children: &[View]) -> View {
1165 View::new(views::custom_layout(layout, &handles(children)))
1166}
1167
1168/// Build a layout from a sizing pass and a placement pass.
1169///
1170/// For a layout with parameters, implement [`views::LayoutAlgorithm`] on a
1171/// type and hand it to [`views::register`] instead.
1172pub fn layout(
1173 size: impl Fn(&views::Subviews<'_>, views::Proposal) -> (f64, f64) + Send + Sync + 'static,
1174 place: impl Fn(&views::Subviews<'_>, views::Rect, views::Proposal) + Send + Sync + 'static,
1175) -> views::CustomLayout {
1176 views::layout(size, place)
1177}
1178
1179/// `Grid { … }` over rows.
1180pub fn grid(rows: &[View]) -> View {
1181 let handles: Vec<ViewHandle> = rows.iter().map(|v| v.handle.clone()).collect();
1182 View::new(views::grid(&handles))
1183}
1184
1185/// `GridRow { … }`
1186pub fn grid_row(children: &[View]) -> View {
1187 let handles: Vec<ViewHandle> = children.iter().map(|v| v.handle.clone()).collect();
1188 View::new(views::grid_row(&handles))
1189}
1190
1191/// `GridRow(alignment:) { … }` — this row's own vertical alignment, which
1192/// overrides the grid's for the row alone.
1193pub fn grid_row_aligned(children: &[View], alignment: views::VerticalAlignment) -> View {
1194 let handles: Vec<ViewHandle> = children.iter().map(|v| v.handle.clone()).collect();
1195 View::new(views::grid_row_aligned(&handles, alignment))
1196}
1197
1198/// Create a `Stepper`.
1199pub fn stepper(label: &str, value: f64, lower: f64, upper: f64, step: f64) -> View {
1200 View::new(views::stepper(label, value, lower, upper, step))
1201}
1202
1203/// Create a `Stepper` wired to Rust state — the `+`/`−` buttons write back.
1204pub fn stepper_bound(
1205 label: &str,
1206 state: &views::BoundState<f64>,
1207 lower: f64,
1208 upper: f64,
1209 step: f64,
1210) -> View {
1211 View::new(views::stepper_bound(label, state, lower, upper, step))
1212}
1213
1214/// Create a `LazyVStack`.
1215pub fn lazy_vstack(children: &[View]) -> View {
1216 let handles: Vec<ViewHandle> = children.iter().map(|v| v.handle.clone()).collect();
1217 View::new(views::lazy_vstack(&handles))
1218}
1219
1220/// `LazyVStack(alignment:)`.
1221pub fn lazy_vstack_aligned(children: &[View], alignment: views::HorizontalAlignment) -> View {
1222 let handles: Vec<ViewHandle> = children.iter().map(|v| v.handle.clone()).collect();
1223 View::new(views::lazy_vstack_aligned(&handles, alignment))
1224}
1225
1226/// Create a `LazyHStack`.
1227pub fn lazy_hstack(children: &[View]) -> View {
1228 let handles: Vec<ViewHandle> = children.iter().map(|v| v.handle.clone()).collect();
1229 View::new(views::lazy_hstack(&handles))
1230}
1231
1232/// `LazyHStack(alignment:)`.
1233pub fn lazy_hstack_aligned(children: &[View], alignment: views::VerticalAlignment) -> View {
1234 let handles: Vec<ViewHandle> = children.iter().map(|v| v.handle.clone()).collect();
1235 View::new(views::lazy_hstack_aligned(&handles, alignment))
1236}