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}