Skip to main content

swiftui_native/view/
layout.rs

1//! Size, position and how a view sits among its neighbours.
2//!
3//! Part of [`crate::view::View`]'s chainable API; Rust allows a type's
4//! inherent methods to be split across modules, and 400 of them in one file
5//! was a file nobody could find anything in.
6
7use super::{Color, Transition, View};
8use crate::views;
9
10impl View {
11    // ── Size and frame ──
12
13    /// Apply `.padding(_:)`.
14    pub fn padding(self, amount: f64) -> Self {
15        Self::new(views::padding(&self.handle, amount))
16    }
17
18    /// `.padding(_:_:)` on chosen edges — `.padding(.horizontal, 8)`.
19    ///
20    /// `None` asks for SwiftUI's own default inset rather than a number.
21    pub fn padding_edges(self, edges: views::EdgeSet, length: Option<f64>) -> Self {
22        Self::new(views::padding_set(&self.handle, edges, length))
23    }
24
25    /// `.padding()` — the system's default inset on every edge.
26    pub fn padding_default(self) -> Self {
27        Self::new(views::padding_default(&self.handle))
28    }
29
30    /// `.frame(width:height:alignment:)` with either dimension left out.
31    ///
32    /// An omitted dimension is not zero: it means "take what the parent offers",
33    /// which is a different layout from asking for nothing.
34    pub fn frame_optional(
35        self,
36        width: Option<f64>,
37        height: Option<f64>,
38        alignment: views::Alignment,
39    ) -> Self {
40        Self::new(views::frame_optional(&self.handle, width, height, alignment))
41    }
42
43    /// `.frame(maxWidth: .infinity, maxHeight: .infinity)` — fill the parent.
44    pub fn expand(self, alignment: views::Alignment) -> Self {
45        Self::new(views::frame_max_infinity(&self.handle, alignment))
46    }
47
48    /// `.frame(maxWidth: .infinity)` — fill across, keep the natural height.
49    pub fn expand_width(self, alignment: views::Alignment) -> Self {
50        Self::new(views::frame_max_width(&self.handle, alignment))
51    }
52
53    /// `.frame(width:height:alignment:)`.
54    pub fn frame_aligned(self, w: f64, h: f64, alignment: views::Alignment) -> Self {
55        Self::new(views::frame_aligned(&self.handle, w, h, alignment))
56    }
57
58    /// Apply `.frame(width:height:)`.
59    pub fn frame(self, w: f64, h: f64) -> Self {
60        Self::new(views::frame(&self.handle, w, h))
61    }
62
63    // ── Compositing ──
64
65    /// `.clipShape(Circle())`
66    pub fn clip_circle(self) -> Self {
67        Self::new(views::clip_circle(&self.handle))
68    }
69
70    /// `.clipShape(Capsule())`
71    pub fn clip_capsule(self) -> Self {
72        Self::new(views::clip_capsule(&self.handle))
73    }
74
75    /// `.clipShape(Capsule(style:))` — `false` gives the circular arc.
76    pub fn clip_capsule_styled(self, continuous: bool) -> Self {
77        Self::new(views::clip_capsule_styled(&self.handle, continuous))
78    }
79
80    /// `.clipShape(RoundedRectangle(cornerRadius:style:))` — `.continuous` is
81    /// the squircle Apple's own UI uses, and it is visibly not a circular arc.
82    pub fn clip_rounded_styled(self, radius: f64, continuous: bool) -> Self {
83        Self::new(views::clip_rounded_rect_styled(&self.handle, radius, continuous))
84    }
85
86    /// `.clipShape(RoundedRectangle(cornerRadius:))`
87    pub fn clip_rounded(self, radius: f64) -> Self {
88        Self::new(views::clip_rounded_rect(&self.handle, radius))
89    }
90
91    /// `.overlay(alignment: .center) { … }`
92    pub fn overlay(self, content: View) -> Self {
93        Self::new(views::overlay(&self.handle, content.handle()))
94    }
95
96    /// `.overlay(alignment:content:)` — the same modifier as [`View::overlay`],
97    /// which pins to the centre.
98    pub fn overlay_aligned(self, content: View, alignment: views::Alignment) -> Self {
99        Self::new(views::overlay_aligned(&self.handle, content.handle(), alignment))
100    }
101
102    /// `.mask(alignment: .center) { … }` — a `_MaskAlignmentEffect`.
103    pub fn mask(self, content: View) -> Self {
104        Self::new(views::mask(&self.handle, content.handle()))
105    }
106
107    /// `.mask(alignment:_:)` with an alignment other than the centre.
108    pub fn mask_aligned(self, content: View, alignment: views::Alignment) -> Self {
109        Self::new(views::mask_aligned(&self.handle, content.handle(), alignment))
110    }
111
112    /// `.mask(_:)` — the older, alignment-free overload, which is a
113    /// `_MaskEffect`. A different type from [`View::mask`], not a shorthand
114    /// for it.
115    pub fn mask_plain(self, content: View) -> Self {
116        Self::new(views::mask_plain(&self.handle, content.handle()))
117    }
118
119    /// `.aspectRatio(_:contentMode:)`
120    pub fn aspect_ratio(self, ratio: f64, fill: bool) -> Self {
121        Self::new(views::aspect_ratio(&self.handle, ratio, fill))
122    }
123
124    /// `.fixedSize(horizontal:vertical:)`
125    pub fn fixed_size(self, horizontal: bool, vertical: bool) -> Self {
126        Self::new(views::fixed_size(&self.handle, horizontal, vertical))
127    }
128
129    /// `.zIndex(_:)`
130    pub fn z_index(self, value: f64) -> Self {
131        Self::new(views::z_index(&self.handle, value))
132    }
133
134    /// `.disabled(_:)`
135    pub fn disabled(self, value: bool) -> Self {
136        Self::new(views::disabled(&self.handle, value))
137    }
138
139    /// `.hidden()`
140    pub fn hidden(self) -> Self {
141        Self::new(views::hidden(&self.handle))
142    }
143
144    /// `.blendMode(_:)`
145    pub fn blend_mode(self, mode: u8) -> Self {
146        Self::new(views::blend_mode(&self.handle, mode))
147    }
148
149    /// `.navigationTitle(_:)`
150    pub fn navigation_title(self, title: &str) -> Self {
151        Self::new(views::navigation_title(&self.handle, title))
152    }
153
154    /// `.navigationTitle(_:)` with a binding — an *editable* title.
155    ///
156    /// The title bar becomes a rename field: the user edits it in place and the
157    /// new name is written back into Rust. Only the fixed-string overload was
158    /// bound, so a document could be shown under a name but never renamed from
159    /// where the name appears.
160    ///
161    /// ```no_run
162    /// # use swiftui_native::prelude::*;
163    /// # use swiftui_native::views::BoundState;
164    /// let name = BoundState::new(String::from("Untitled"));
165    /// let doc = text("body").navigation_title_bound(&name);
166    /// ```
167    pub fn navigation_title_bound(self, state: &views::BoundState<String>) -> Self {
168        Self::new(views::navigation_title_bound(&self.handle, state))
169    }
170
171    /// `.navigationDestination(for: Int.self) { value in … }` — value-based
172    /// routing.
173    ///
174    /// This is how a `NavigationStack` is driven in modern SwiftUI: rows push a
175    /// *value*, and the stack asks this modifier what view that value means.
176    /// Only the `isPresented:` overload was bound, which is the older
177    /// one-destination-per-flag shape — fine for a single detail sheet, and no
178    /// use for a stack that pushes rows.
179    ///
180    /// ```no_run
181    /// # use swiftui_native::prelude::*;
182    /// let stack = navigation_stack(
183    ///     list_identified(&[1, 2, 3], |id| text(&format!("row {id}")))
184    ///         .navigation_destination_for(|id| text(&format!("detail {id}"))),
185    /// );
186    /// ```
187    pub fn navigation_destination_for<V: crate::view::IntoView>(
188        self,
189        destination: impl Fn(i64) -> V + 'static,
190    ) -> Self {
191        Self::new(views::navigation_destination_for(&self.handle, move |i| {
192            destination(i).into_view().into_handle()
193        }))
194    }
195
196    /// `.alert(_:isPresented:actions:)` with a fixed flag.
197    ///
198    /// The flag becomes a `Binding.constant`, which SwiftUI treats as
199    /// read-only, so this alert cannot be dismissed — there is nowhere to write
200    /// `false` back to. [`View::alert_bound`] is the one a user can close.
201    pub fn alert(self, title: &str, presented: bool, actions: View) -> Self {
202        Self::new(views::alert(&self.handle, title, actions.handle(), presented))
203    }
204
205    /// `.alert(_:isPresented:actions:)` bound to Rust state.
206    ///
207    /// Dismissing writes `false` into `state`, so Rust learns the alert closed
208    /// without being told.
209    pub fn alert_bound(
210        self,
211        title: &str,
212        state: &views::BoundState<bool>,
213        actions: View,
214    ) -> Self {
215        Self::new(views::alert_bound(&self.handle, title, actions.handle(), state))
216    }
217
218    /// `.alert(_:isPresented:actions:message:)` — an alert that explains itself.
219    ///
220    /// The message is the line under the title: what the buttons are about, and
221    /// the difference between "Delete" and "Delete — this cannot be undone".
222    /// Only the title-and-buttons overload was bound, so an alert could ask but
223    /// not say why.
224    ///
225    /// ```no_run
226    /// # use swiftui_native::prelude::*;
227    /// # use swiftui_native::views::BoundState;
228    /// let asking = BoundState::new(false);
229    /// let page = text("Notes").alert_message_bound(
230    ///     "Delete this?",
231    ///     &asking,
232    ///     button("Delete", || {}),
233    ///     text("This cannot be undone."),
234    /// );
235    /// ```
236    pub fn alert_message_bound(
237        self,
238        title: &str,
239        state: &views::BoundState<bool>,
240        actions: View,
241        message: View,
242    ) -> Self {
243        Self::new(views::alert_message_bound(
244            &self.handle,
245            title,
246            actions.handle(),
247            message.handle(),
248            state,
249        ))
250    }
251
252    /// `.alert(_:isPresented:actions:message:)` with a fixed flag, which cannot
253    /// be dismissed — see [`alert`](Self::alert).
254    pub fn alert_message(
255        self,
256        title: &str,
257        presented: bool,
258        actions: View,
259        message: View,
260    ) -> Self {
261        Self::new(views::alert_message(
262            &self.handle,
263            title,
264            actions.handle(),
265            message.handle(),
266            presented,
267        ))
268    }
269
270    /// `.confirmationDialog(_:isPresented:titleVisibility:actions:message:)`
271    ///
272    /// The message is the explanatory line under the title, and on a dialog it
273    /// is often the only place the consequence is stated — "This deletes 12
274    /// items" under a bare "Delete".
275    pub fn confirmation_message_bound(
276        self,
277        title: &str,
278        state: &views::BoundState<bool>,
279        actions: View,
280        message: View,
281    ) -> Self {
282        Self::new(views::confirmation_message_bound(
283            &self.handle,
284            title,
285            actions.handle(),
286            message.handle(),
287            state,
288        ))
289    }
290
291    /// `.sheet(isPresented:content:)` with a fixed flag — undismissable, as
292    /// [`View::alert`] is. See [`View::sheet_bound`].
293    pub fn sheet(self, presented: bool, content: View) -> Self {
294        Self::new(views::sheet(&self.handle, content.handle(), presented))
295    }
296
297    /// `.sheet(isPresented:content:)` bound to Rust state, so a swipe down
298    /// writes `false` back.
299    pub fn sheet_bound(self, state: &views::BoundState<bool>, content: View) -> Self {
300        Self::new(views::sheet_bound(&self.handle, content.handle(), state))
301    }
302
303    /// `.sheet(isPresented:onDismiss:content:)` — with a callback for the
304    /// closing.
305    ///
306    /// A sheet that saves what was edited has to know when it was dismissed,
307    /// and `onDismiss` was hardwired to `nil`, so the sheet could be opened and
308    /// closed and nothing on the Rust side would hear about it.
309    ///
310    /// ```no_run
311    /// # use swiftui_native::prelude::*;
312    /// # use swiftui_native::views::BoundState;
313    /// let editing = BoundState::new(false);
314    /// let page = text("Notes").sheet_bound_dismiss(
315    ///     &editing,
316    ///     text("editor"),
317    ///     || println!("saved"),
318    /// );
319    /// ```
320    pub fn sheet_bound_dismiss(
321        self,
322        state: &views::BoundState<bool>,
323        content: View,
324        on_dismiss: impl Fn() + 'static,
325    ) -> Self {
326        Self::new(views::sheet_bound_dismiss(
327            &self.handle,
328            content.handle(),
329            state,
330            on_dismiss,
331        ))
332    }
333
334    /// `.confirmationDialog(_:isPresented:actions:)` with a fixed flag —
335    /// undismissable. See [`View::confirmation_dialog_bound`].
336    pub fn confirmation_dialog(self, title: &str, presented: bool, actions: View) -> Self {
337        Self::new(views::confirmation_dialog(&self.handle, title, actions.handle(), presented))
338    }
339
340    /// `.confirmationDialog(_:isPresented:actions:)` bound to Rust state.
341    pub fn confirmation_dialog_bound(
342        self,
343        title: &str,
344        state: &views::BoundState<bool>,
345        actions: View,
346    ) -> Self {
347        Self::new(views::confirmation_dialog_bound(&self.handle, title, actions.handle(), state))
348    }
349
350    /// `.accessibilityLabel(_:)`
351    pub fn a11y_label(self, label: &str) -> Self {
352        Self::new(views::accessibility_label(&self.handle, label))
353    }
354
355    /// `.accessibilityHidden(_:)`
356    pub fn a11y_hidden(self, hidden: bool) -> Self {
357        Self::new(views::accessibility_hidden(&self.handle, hidden))
358    }
359
360    /// `.focusable(_:)`
361    pub fn focusable(self, value: bool) -> Self {
362        Self::new(views::focusable(&self.handle, value))
363    }
364
365    /// `.onDisappear(perform:)`
366    pub fn on_disappear(self, action: impl Fn() + 'static) -> Self {
367        Self::new(views::on_disappear(&self.handle, action))
368    }
369
370    /// `.gesture(LongPressGesture(...))`
371    pub fn on_long_press(self, duration: f64, max_distance: f64) -> Self {
372        Self::new(views::long_press_gesture(&self.handle, duration, max_distance))
373    }
374
375    /// `.onLongPressGesture(minimumDuration:maximumDistance:pressing:perform:)`
376    ///
377    /// Prefer this over [`on_long_press`](Self::on_long_press), which attaches
378    /// the gesture with nothing listening.
379    pub fn on_long_press_perform(self, duration: f64, max_distance: f64, action: impl Fn() + 'static) -> Self {
380        Self::new(views::on_long_press_gesture(&self.handle, duration, max_distance, action))
381    }
382
383    /// `.clipped()`
384    pub fn clipped(self) -> Self {
385        Self::new(views::clipped(&self.handle))
386    }
387
388    /// `.compositingGroup()`
389    pub fn compositing_group(self) -> Self {
390        Self::new(views::compositing_group(&self.handle))
391    }
392
393    /// `.drawingGroup()` — flatten the subtree through Metal.
394    pub fn drawing_group(self) -> Self {
395        Self::new(views::drawing_group(&self.handle))
396    }
397
398    /// `.rotation3DEffect(_:axis:)`
399    pub fn rotate_3d(self, degrees: f64, axis: (f64, f64, f64)) -> Self {
400        Self::new(views::rotation_3d(&self.handle, degrees, axis.0, axis.1, axis.2))
401    }
402
403    /// `.gesture(DragGesture(...))`
404    pub fn on_drag(self, minimum_distance: f64) -> Self {
405        Self::new(views::drag_gesture(&self.handle, minimum_distance))
406    }
407
408    /// `.gesture(DragGesture(...).onEnded(_:))`
409    ///
410    /// Prefer this over [`on_drag`](Self::on_drag), which attaches the gesture
411    /// with nothing listening.
412    pub fn on_drag_ended(self, minimum_distance: f64, action: impl Fn() + 'static) -> Self {
413        Self::new(views::on_drag_gesture(&self.handle, minimum_distance, action))
414    }
415
416    /// `.gesture(DragGesture(...).onEnded { value in … })`, with the value.
417    ///
418    /// [`on_drag_ended`](Self::on_drag_ended) tells the handler only *that* a
419    /// drag ended. This one hands over where it started, where it finished and
420    /// how fast it was moving — see [`views::DragValue`].
421    ///
422    /// ```no_run
423    /// # use swiftui_native::prelude::*;
424    /// let card = text("drag me").on_drag_value(1.0, |v| {
425    ///     let (dx, dy) = v.translation();
426    ///     println!("moved {dx} x {dy}");
427    /// });
428    /// ```
429    pub fn on_drag_value(
430        self,
431        minimum_distance: f64,
432        action: impl Fn(views::DragValue) + 'static,
433    ) -> Self {
434        Self::new(views::on_drag_ended_value(&self.handle, minimum_distance, action))
435    }
436
437    /// `.gesture(MagnifyGesture(...))`
438    pub fn on_magnify(self, minimum_scale_delta: f64) -> Self {
439        Self::new(views::magnify_gesture(&self.handle, minimum_scale_delta))
440    }
441
442    /// `.gesture(MagnifyGesture(...).onEnded { value in … })`
443    ///
444    /// [`on_magnify`](Self::on_magnify) attaches the gesture with nothing
445    /// listening — it builds the value Swift would and can never be seen to
446    /// fire. This one hands over the magnification, the velocity and where the
447    /// pinch was centred; see [`views::MagnifyValue`].
448    pub fn on_magnify_value(
449        self,
450        minimum_scale_delta: f64,
451        action: impl Fn(views::MagnifyValue) + 'static,
452    ) -> Self {
453        Self::new(views::on_magnify_ended(&self.handle, minimum_scale_delta, action))
454    }
455
456    /// `.gesture(RotateGesture(...))`
457    pub fn on_rotate(self, minimum_degrees: f64) -> Self {
458        Self::new(views::rotate_gesture(&self.handle, minimum_degrees))
459    }
460
461    /// `.gesture(RotateGesture(...).onEnded { value in … })`
462    ///
463    /// As [`on_rotate`](Self::on_rotate), but the handler is told how far.
464    /// `Angle` stores radians, so [`views::RotateValue::degrees`] converts
465    /// rather than reading a second field.
466    pub fn on_rotate_value(
467        self,
468        minimum_degrees: f64,
469        action: impl Fn(views::RotateValue) + 'static,
470    ) -> Self {
471        Self::new(views::on_rotate_ended(&self.handle, minimum_degrees, action))
472    }
473
474    /// `.popover(isPresented:content:)` with a fixed flag — undismissable.
475    /// See [`View::popover_bound`].
476    pub fn popover(self, presented: bool, content: View) -> Self {
477        Self::new(views::popover(&self.handle, content.handle(), presented))
478    }
479
480    /// `.popover(isPresented:content:)` bound to Rust state, so tapping outside
481    /// writes `false` back.
482    pub fn popover_bound(self, state: &views::BoundState<bool>, content: View) -> Self {
483        Self::new(views::popover_bound(&self.handle, content.handle(), state))
484    }
485
486    /// `.onChange(of:_:)`, watching an `Int`.
487    pub fn on_change(self, value: i64, handler: impl Fn(i64, i64) + 'static) -> Self {
488        Self::new(views::on_change(&self.handle, value, handler))
489    }
490
491    /// `.onChange(of:initial:_:)` over a `String`.
492    ///
493    /// `onChange` is one of the modifiers an application reaches for most, and
494    /// it could only watch an `Int` — a search term, a selected identifier, a
495    /// mode name all had to be turned into a number first. Swift constrains the
496    /// watched value to `Equatable` and nothing else.
497    ///
498    /// ```no_run
499    /// # use swiftui_native::prelude::*;
500    /// let v = text("results").on_change_str("query", |was, now| {
501    ///     println!("search went from {was:?} to {now:?}");
502    /// });
503    /// ```
504    pub fn on_change_str(
505        self,
506        value: &str,
507        handler: impl Fn(String, String) + 'static,
508    ) -> Self {
509        Self::new(views::on_change_str(&self.handle, value, handler))
510    }
511
512    /// `.onChange(of:initial:_:)` over a `Bool`.
513    pub fn on_change_bool(self, value: bool, handler: impl Fn(bool, bool) + 'static) -> Self {
514        Self::new(views::on_change_bool(&self.handle, value, handler))
515    }
516
517    /// `.onChange(of:initial:_:)` over a `Double`.
518    pub fn on_change_f64(self, value: f64, handler: impl Fn(f64, f64) + 'static) -> Self {
519        Self::new(views::on_change_f64(&self.handle, value, handler))
520    }
521
522    /// `.toolbar { ToolbarItem { content } }`
523    pub fn toolbar(self, content: View) -> Self {
524        Self::new(views::toolbar(&self.handle, content.handle()))
525    }
526
527    /// `.toolbar(id:) { ToolbarItem(id:showsByDefault:) { content } }` — a
528    /// toolbar the user can rearrange, remembered under `toolbar_id`.
529    ///
530    /// `shows_by_default` is whether the item is there before any
531    /// customization; `false` leaves it in the palette until it is dragged in.
532    pub fn toolbar_customizable(
533        self,
534        toolbar_id: &str,
535        item_id: &str,
536        shows_by_default: bool,
537        content: View,
538    ) -> Self {
539        Self::new(views::toolbar_customizable(
540            &self.handle,
541            toolbar_id,
542            item_id,
543            shows_by_default,
544            content.handle(),
545        ))
546    }
547
548    /// `.transition(.opacity)`
549    pub fn transition(self, kind: Transition) -> Self {
550        Self::new(match kind {
551            Transition::Opacity => views::transition_opacity(&self.handle),
552            Transition::Slide => views::transition_slide(&self.handle),
553            Transition::Scale { scale, anchor } => {
554                views::transition(&self.handle, views::transition_scale(scale, anchor))
555            }
556            Transition::ScaleDefault => {
557                views::transition(&self.handle, views::transition_scale_value())
558            }
559            Transition::Identity => {
560                views::transition(&self.handle, views::transition_identity_value())
561            }
562            Transition::ScaleWithOpacity { scale, anchor } => views::transition(
563                &self.handle,
564                views::transition_combined(
565                    views::transition_scale(scale, anchor),
566                    views::transition_opacity_value(),
567                ),
568            ),
569        })
570    }
571
572    /// `.id(_)` — the identity SwiftUI diffs against.
573    ///
574    /// Changing it replaces the view rather than updating it, which is what
575    /// makes a transition play: the old identity leaves, the new one arrives.
576    pub fn id(self, value: i64) -> Self {
577        Self::new(views::id(&self.handle, views::HashableValue::Int(value)))
578    }
579
580    /// `.animation(.default, value:)`
581    pub fn animation(self, value: i64) -> Self {
582        Self::new(views::animation(&self.handle, value))
583    }
584
585    /// `.onAppear(perform:)`
586    pub fn on_appear(self, action: impl Fn() + 'static) -> Self {
587        Self::new(views::on_appear(&self.handle, action))
588    }
589
590    /// `.onTapGesture(count:perform:)`
591    pub fn on_tap(self, action: impl Fn() + 'static) -> Self {
592        Self::new(views::on_tap_gesture(&self.handle, 1, action))
593    }
594
595    /// `.contextMenu(menuItems:)`
596    pub fn context_menu(self, items: View) -> Self {
597        Self::new(views::context_menu(&self.handle, items.handle()))
598    }
599
600    /// `.contextMenu(menuItems:preview:)` — a menu with something to look at.
601    ///
602    /// The preview is what a long press enlarges before the menu appears: the
603    /// row blown up, a photo, a card. [`context_menu`](Self::context_menu) has
604    /// no such slot, so the menu came up over the view as it already was.
605    ///
606    /// ```no_run
607    /// # use swiftui_native::prelude::*;
608    /// let row = text("Photo").context_menu_preview(
609    ///     button("Delete", || {}),
610    ///     image("photo").frame(200.0, 200.0),
611    /// );
612    /// ```
613    pub fn context_menu_preview(self, items: View, preview: View) -> Self {
614        Self::new(views::context_menu_preview(
615            &self.handle,
616            items.handle(),
617            preview.handle(),
618        ))
619    }
620
621    /// Wrap in a vertical `ScrollView`.
622    pub fn scroll(self) -> Self {
623        Self::new(views::scroll(&self.handle))
624    }
625
626    // ── Placement ──
627
628    /// `.position(x:y:)` — place the view's centre at a point in its parent,
629    /// which is not the same as `.offset(x:y:)` moving it from where it was.
630    pub fn position(self, x: f64, y: f64) -> Self {
631        Self::new(views::position(&self.handle, x, y))
632    }
633
634    /// `.hueRotation(.radians(_))`
635    pub fn hue_rotation(self, radians: f64) -> Self {
636        Self::new(views::hue_rotation(&self.handle, radians))
637    }
638
639    /// `.colorMultiply(_:)` — multiply every pixel by a colour, which tints
640    /// without covering.
641    pub fn color_multiply(self, color: Color) -> Self {
642        Self::new(views::color_multiply(&self.handle, color))
643    }
644
645    /// `.contentTransition(_:)` — how a change to the *content* animates, as
646    /// distinct from the view arriving or leaving.
647    pub fn content_transition(self, transition: views::ContentTransition) -> Self {
648        Self::new(views::content_transition(&self.handle, transition))
649    }
650
651    /// `.projectionEffect(_:)` — a 3x3 projection, row-major.
652    pub fn projection_effect(self, m: [f64; 9]) -> Self {
653        Self::new(views::projection_effect(&self.handle, m))
654    }
655
656    /// `.aspectRatio(contentMode:)` without pinning a ratio — `true` fills,
657    /// `false` fits.
658    pub fn aspect_ratio_mode(self, fill: bool) -> Self {
659        Self::new(views::aspect_ratio_mode(&self.handle, fill))
660    }
661
662    /// `.safeAreaPadding(_:)` — padding measured from the safe area rather than
663    /// from the view.
664    pub fn safe_area_padding(self, length: f64) -> Self {
665        Self::new(views::safe_area_padding(&self.handle, length))
666    }
667
668    /// `.contentMargins(_:for:)`
669    pub fn content_margins(self, length: f64, placement: views::ContentMarginPlacement) -> Self {
670        Self::new(views::content_margins(&self.handle, length, placement))
671    }
672
673    /// `.containerShape(_:)` — the shape descendants inherit for clipping.
674    pub fn container_shape(self, shape: views::InsettableShape) -> Self {
675        Self::new(views::container_shape(&self.handle, shape))
676    }
677
678    /// `.coordinateSpace(_:)` — name this view's space so a `GeometryProxy` can
679    /// report positions relative to it.
680    pub fn coordinate_space(self, name: views::HashableValue<'_>) -> Self {
681        Self::new(views::coordinate_space(&self.handle, name))
682    }
683
684    // ── Grid cells ──
685
686    /// `.gridCellUnsizedAxes(_:)` — axes this cell should not stretch along.
687    pub fn grid_cell_unsized_axes(self, axes: views::AxisSet) -> Self {
688        Self::new(views::grid_cell_unsized_axes(&self.handle, axes))
689    }
690
691    /// `.gridColumnAlignment(_:)`
692    pub fn grid_column_alignment(self, alignment: views::HorizontalAlignment) -> Self {
693        Self::new(views::grid_column_alignment(&self.handle, alignment))
694    }
695
696    // ── Guides and container-relative sizing ──
697
698    /// `.alignmentGuide(_:computeValue:)` — move where this view lines up with
699    /// its siblings without moving the view.
700    ///
701    /// `compute` is handed the view's [`views::ViewDimensions`] and answers in
702    /// terms of them, which is what an alignment guide is for: returning
703    /// `d.width / 2.0` lines the view up by its centre, `d.width` by its far
704    /// edge. It used to take no arguments, so the only guide it could express
705    /// was a constant — the one kind that does not need the modifier.
706    ///
707    /// ```no_run
708    /// # use swiftui_native::prelude::*;
709    /// # use swiftui_native::views::HorizontalAlignment;
710    /// let v = text("centred").alignment_guide(HorizontalAlignment::Leading, |d| d.width / 2.0);
711    /// ```
712    pub fn alignment_guide(
713        self,
714        alignment: views::HorizontalAlignment,
715        compute: impl Fn(views::ViewDimensions) -> f64 + 'static,
716    ) -> Self {
717        Self::new(views::alignment_guide(&self.handle, alignment, compute))
718    }
719
720    /// `.containerRelativeFrame(_:alignment:)` — size against the container
721    /// rather than the content, which is what makes a carousel page.
722    pub fn container_relative_frame(
723        self,
724        axes: &[views::AxisSet],
725        alignment: views::Alignment,
726    ) -> Self {
727        Self::new(views::container_relative_frame(&self.handle, axes, alignment))
728    }
729
730    /// `.containerRelativeFrame(_:count:span:spacing:alignment:)` at thirds.
731    pub fn container_relative_frame_thirds(
732        self,
733        axes: &[views::AxisSet],
734        alignment: views::Alignment,
735    ) -> Self {
736        Self::new(views::container_relative_frame_thirds(&self.handle, axes, alignment))
737    }
738
739    /// `.onGeometryChange(for:of:action:)` — observe the measured geometry.
740    ///
741    /// `measure` is handed the view's width and height and returns the value to
742    /// watch; `action` is called with the old and the new one whenever that
743    /// value changes. Both used to be blind — `measure` took no arguments, so
744    /// it could not read the geometry the modifier exists to report, and
745    /// `action` was told only that *something* had changed.
746    ///
747    /// ```no_run
748    /// # use swiftui_native::prelude::*;
749    /// let v = text("wide?").on_geometry_change(
750    ///     |w, _h| (w > 400.0) as i64,
751    ///     |was, now| println!("wide: {was} -> {now}"),
752    /// );
753    /// ```
754    pub fn on_geometry_change(
755        self,
756        measure: impl Fn(f64, f64) -> i64 + 'static,
757        action: impl Fn(i64, i64) + 'static,
758    ) -> Self {
759        Self::new(views::on_geometry_change_int(&self.handle, measure, action))
760    }
761
762    /// `.onGeometryChange(for: Double.self, of:action:)` — the same, watching a
763    /// `Double`.
764    ///
765    /// Which is usually what is wanted: the value being watched is nearly
766    /// always a width or a height, and `for: Int.self` meant rounding it before
767    /// it could be watched at all.
768    ///
769    /// ```no_run
770    /// # use swiftui_native::prelude::*;
771    /// let v = text("wide?").on_geometry_change_f64(|w, _h| w, |was, now| {
772    ///     println!("width {was} -> {now}");
773    /// });
774    /// ```
775    pub fn on_geometry_change_f64(
776        self,
777        measure: impl Fn(f64, f64) -> f64 + 'static,
778        action: impl Fn(f64, f64) + 'static,
779    ) -> Self {
780        Self::new(views::on_geometry_change_f64(&self.handle, measure, action))
781    }
782
783    /// `.clipShape(Ellipse())`
784    pub fn clip_ellipse(self) -> Self {
785        Self::new(views::clip_ellipse(&self.handle))
786    }
787
788    /// `.layoutDirectionBehavior(_:)` — whether this mirrors in a
789    /// right-to-left language.
790    pub fn layout_direction_behavior(self, value: views::LayoutDirectionBehavior) -> Self {
791        Self::new(views::layout_direction_behavior(&self.handle, value))
792    }
793
794    /// `.layoutValue(key:value:)`
795    pub fn layout_value(self, value: i64) -> Self {
796        Self::new(views::layout_value_int(&self.handle, value))
797    }
798
799    /// `.containerCornerOffset(_:)`
800    pub fn container_corner_offset(self, edges: views::EdgeSet, value: bool) -> Self {
801        Self::new(views::container_corner_offset(&self.handle, edges, value))
802    }
803
804    /// `.safeAreaBar(edge:alignment:spacing:content:)` — a bar that shrinks the
805    /// safe area, as [`View::safe_area_inset`] does, but styled as a bar.
806    pub fn safe_area_bar(
807        self,
808        edge: views::VerticalEdge,
809        alignment: views::HorizontalAlignment,
810        spacing: Option<f64>,
811        content: View,
812    ) -> Self {
813        Self::new(views::safe_area_bar(
814            &self.handle,
815            edge,
816            alignment,
817            spacing,
818            content.handle(),
819        ))
820    }
821
822    // ── Safe area and identity ──
823
824    /// `.ignoresSafeArea()` — let the view run under the notch, the home
825    /// indicator and the keyboard, which is what a full-bleed background wants.
826    pub fn ignores_safe_area(self) -> Self {
827        self.ignores_safe_area_edges(views::SafeAreaRegions::All, views::EdgeSet::All)
828    }
829
830    /// `.ignoresSafeArea(_:edges:)` — the same, but only the regions and edges
831    /// named. `.ignoresSafeArea(.keyboard)` alone is the common case.
832    pub fn ignores_safe_area_edges(
833        self,
834        regions: views::SafeAreaRegions,
835        edges: views::EdgeSet,
836    ) -> Self {
837        Self::new(views::ignores_safe_area(&self.handle, regions, edges))
838    }
839
840    /// `.safeAreaInset(edge:alignment:spacing:) { … }` — put a view against one
841    /// edge and shrink this view's safe area by however tall it turns out to
842    /// be. A bottom bar that scrolling content stops above, without measuring.
843    pub fn safe_area_inset(
844        self,
845        edge: views::VerticalEdge,
846        alignment: views::HorizontalAlignment,
847        spacing: Option<f64>,
848        content: View,
849    ) -> Self {
850        Self::new(views::safe_area_inset(
851            &self.handle,
852            edge,
853            alignment,
854            spacing,
855            content.handle(),
856        ))
857    }
858
859    /// `.scrollIndicators(_:axes:)`
860    ///
861    /// The axis is not optional here as it is in Swift: SwiftUI's default is
862    /// `[.horizontal, .vertical]`, and `Axis.Set` has no `.all` static to name
863    /// that pair with, so a no-axis convenience would have to quietly pick one.
864    pub fn scroll_indicators(
865        self,
866        visibility: views::ScrollIndicatorVisibility,
867        axes: views::AxisSet,
868    ) -> Self {
869        Self::new(views::scroll_indicators(&self.handle, visibility, axes))
870    }
871
872    /// `.contentShape(_:)` — the region that takes hits, which is otherwise the
873    /// view's frame. A circular button in a square frame needs this or its
874    /// corners stay tappable.
875    pub fn content_shape(self, shape: views::InsettableShape) -> Self {
876        Self::new(views::content_shape(&self.handle, shape, false))
877    }
878
879    /// `.tag(_:)` — the value a `Picker` or `TabView` selects when this is the
880    /// chosen row.
881    pub fn tag(self, value: views::HashableValue<'_>) -> Self {
882        Self::new(views::tag(&self.handle, value))
883    }
884
885    /// `.tag(_:)` with an `Int`, which is what a `Picker` bound to
886    /// [`views::BoundState<i64>`] selects by.
887    pub fn tag_int(self, value: i64) -> Self {
888        self.tag(views::HashableValue::Int(value))
889    }
890
891    /// `.tag(_:)` with a `String`.
892    pub fn tag_str(self, value: &str) -> Self {
893        self.tag(views::HashableValue::Str(value))
894    }
895
896    /// `.task { … }` — run the action once when the view appears, cancelled if
897    /// it goes away first.
898    ///
899    /// `#[track_caller]` so the task carries the call site's file and line,
900    /// which is what SwiftUI names it by in Instruments — taking `file!()` here
901    /// would name every task after this file.
902    #[track_caller]
903    pub fn task(self, action: impl Fn() + Send + Sync + 'static) -> Self {
904        let loc = core::panic::Location::caller();
905        Self::new(views::task_once(
906            &self.handle,
907            views::TaskPriority::UserInitiated,
908            loc.file(),
909            i64::from(loc.line()),
910            action,
911        ))
912    }
913
914    /// `.task(id:) { … }` — as [`task`](Self::task), but restarted whenever the
915    /// identity changes.
916    ///
917    /// SwiftUI cancels the running task and starts a new one when `id` differs
918    /// from the last body evaluation, which is how a detail view re-fetches
919    /// when the selection moves. `views::task` has always built this; it had
920    /// no chainable method, so the DSL could only ever start a task once.
921    #[track_caller]
922    pub fn task_id(self, id: i64, action: impl Fn() + Send + Sync + 'static) -> Self {
923        let loc = core::panic::Location::caller();
924        Self::new(views::task(
925            &self.handle,
926            id,
927            views::TaskPriority::UserInitiated,
928            loc.file(),
929            i64::from(loc.line()),
930            action,
931        ))
932    }
933
934    /// `.imageScale(_:)` — how large an SF Symbol draws relative to its font.
935    pub fn image_scale(self, scale: views::ImageScale) -> Self {
936        Self::new(views::image_scale(&self.handle, scale))
937    }
938
939    /// `.textCase(_:)` — `None` restores the text as written.
940    pub fn text_case(self, case: Option<views::TextCase>) -> Self {
941        Self::new(views::text_case(&self.handle, case))
942    }
943
944}