Skip to main content

swiftui_native/views/
interaction.rs

1//! Interaction, lifecycle and animation.
2
3use super::*;
4use crate::abi;
5use crate::resolve;
6use core::ffi::c_void;
7use crate::mangle;
8
9// ── Interaction, lifecycle and animation ──
10
11mangle::modifier_types! {
12    /// `.transition(_:)`
13    /// `ModifiedContent<AnyView, _TraitWritingModifier<TransitionTraitKey>>`
14    pub(crate) TRANSITION_T = ty("_TraitWritingModifier").of(vec![ty("TransitionTraitKey")]);
15    /// `.onAppear(perform:)`
16    /// `ModifiedContent<AnyView, _AppearanceActionModifier>`
17    pub(crate) ON_APPEAR_T = ty("_AppearanceActionModifier");
18    /// `.animation(_:value:)` with an `Int` value
19    /// `ModifiedContent<AnyView, _AnimationModifier<Int>>`
20    pub(crate) ANIMATION_T = ty("_AnimationModifier").of(vec![Type::Verbatim("Si".into())]);
21}
22
23
24
25/// `.transition(_:)` for an `AnyTransition` already in hand.
26///
27/// The bindings for SwiftUI's own transitions read a static and pass its word;
28/// [`crate::views::custom_transition`] builds its own box and has the same word
29/// to pass, so the call is shared rather than written twice.
30///
31/// # Safety
32/// `boxed` must be a live `AnyTransition` — one class reference, `+1`.
33pub(crate) unsafe fn opaque_modifier_for_transition(
34    view: &ViewHandle,
35    boxed: u64,
36) -> ViewHandle {
37    unsafe {
38        let (_, _, size) = TRANSITION_T.applied_to(view.meta);
39        let r = abi::call_modifier(
40            resolve::transition_fn(),
41            view.value_ptr(),
42            view.meta,
43            view.wt,
44            &[boxed],
45            [0.0; 4],
46            size,
47        );
48        wrap_modified(r, &TRANSITION_T, view)
49    }
50}
51
52/// Apply `.transition(.opacity)`.
53///
54/// `AnyTransition` is eight bytes — a single class reference — so it is
55/// *loadable*: its static getters return it in x0 and it is passed by value.
56/// Reading it through an indirect (`x8`) return, as the larger resilient types
57/// like `Material` and `Glass` require, yields an uninitialised buffer whose
58/// contents are then released as if they were an object.
59pub fn transition_opacity(view: &ViewHandle) -> ViewHandle {
60    unsafe {
61        let meta = abi::metadata0(resolve::any_transition_ma());
62        let (value, _) = static_getter(resolve::transition_opacity(), meta);
63        let (_, _, size) = TRANSITION_T.applied_to(view.meta);
64        let r = abi::call_modifier(resolve::transition_fn(), view.value_ptr(), view.meta, view.wt, &[value], [0.0; 4], size);
65        wrap_modified(r, &TRANSITION_T, view)
66    }
67}
68
69/// Apply `.transition(.slide)` — in from the leading edge, out to the trailing.
70pub fn transition_slide(view: &ViewHandle) -> ViewHandle {
71    unsafe {
72        let meta = abi::metadata0(resolve::any_transition_ma());
73        let (value, _) = static_getter(resolve::transition_slide(), meta);
74        let (_, _, size) = TRANSITION_T.applied_to(view.meta);
75        let r = abi::call_modifier(
76            resolve::transition_fn(),
77            view.value_ptr(),
78            view.meta,
79            view.wt,
80            &[value],
81            [0.0; 4],
82            size,
83        );
84        wrap_modified(r, &TRANSITION_T, view)
85    }
86}
87
88swift_statics! {
89    /// `SwiftUI.Animation`'s built-in curves, as the `static var`s they are.
90    ///
91    /// Named `AnimationKind` rather than `Animation` because
92    /// [`crate::render::Animation`] — a captured sequence of frames — already
93    /// holds that name in the prelude, and the two are unrelated.
94    ///
95    /// The timed forms (`easeInOut(duration:)`, `spring(…)`) are *methods*,
96    /// not statics, and so are not here; [`crate::state`] calls one of them
97    /// directly.
98    pub enum AnimationKind {
99        Default = c"$s7SwiftUI9AnimationV7defaultACvgZ",
100        EaseIn = c"$s7SwiftUI9AnimationV6easeInACvgZ",
101        EaseInOut = c"$s7SwiftUI9AnimationV9easeInOutACvgZ",
102        EaseOut = c"$s7SwiftUI9AnimationV7easeOutACvgZ",
103        Linear = c"$s7SwiftUI9AnimationV6linearACvgZ",
104        // `systemOverlayAppearance` and its two neighbours are *not* here.
105        // They are exported, and the mangled names resolve — but they live in
106        // an `@available(macOS, unavailable) @available(iOS, unavailable)`
107        // extension, and calling one traps with "Unavailable code reached".
108        // visionOS only.
109    }
110}
111
112/// `.animation(_:value:)` for an `Animation` already in hand.
113///
114/// [`animation_kind`] reads a static and passes its word; a custom animation
115/// builds its own and has the same word to pass, so the call is shared.
116///
117/// # Safety
118/// `anim` must be a live `Animation` — one class reference, `+1`.
119pub(crate) unsafe fn animation_word(view: &ViewHandle, anim: u64, value: i64) -> ViewHandle {
120    unsafe {
121        let int_meta = resolve::int_meta();
122        let eq_wt = abi::get_view_wt_for_proto(int_meta, resolve::equatable_proto());
123        assert!(!eq_wt.is_null(), "Int: Equatable");
124        let (_, _, size) = ANIMATION_T.applied_to(view.meta);
125        let mut result = vec![0u8; size];
126        let this = view.value_ptr();
127        let mut v = value;
128        core::arch::asm!("blr {f}", f = in(reg) resolve::animation_fn(),
129            in("x0") anim,
130            in("x1") &mut v as *mut i64,
131            in("x2") view.meta,
132            in("x3") int_meta,
133            in("x4") view.wt,
134            in("x5") eq_wt,
135            in("x8") result.as_mut_ptr(),
136            in("x20") this,
137            lateout("x6") _, lateout("x7") _,
138            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
139            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
140            lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
141        );
142        wrap_modified(result, &ANIMATION_T, view)
143    }
144}
145
146/// Apply `.animation(.default, value:)`.
147///
148/// `Animation` is likewise eight bytes, and so is `Animation?` — the optional
149/// needs no separate tag, `nil` is simply a null reference.
150pub fn animation(view: &ViewHandle, value: i64) -> ViewHandle {
151    animation_kind(view, AnimationKind::Default, value)
152}
153
154/// `.animation(_:value:)` with the curve named.
155pub fn animation_kind(view: &ViewHandle, kind: AnimationKind, value: i64) -> ViewHandle {
156    unsafe {
157        let anim_meta = abi::metadata0(resolve::animation_ma());
158        let (anim, _) = static_getter(kind.resolved(), anim_meta);
159
160        let int_meta = resolve::int_meta();
161        let eq_wt = abi::get_view_wt_for_proto(int_meta, resolve::equatable_proto());
162        assert!(!eq_wt.is_null(), "Int: Equatable");
163
164        let (_, _, size) = ANIMATION_T.applied_to(view.meta);
165        let mut result = vec![0u8; size];
166        let this = view.value_ptr();
167        let mut v = value;
168        // animation<V: Equatable>(_: Animation?, value: V)
169        //   x0 = Animation? by value, x1 = @in V (generic, so indirect),
170        //   x2 = Self.Type, x3 = V.Type, x4 = Self: View WT,
171        //   x5 = V: Equatable WT, x8 = @out, x20 = self
172        core::arch::asm!("blr {f}", f = in(reg) resolve::animation_fn(),
173            in("x0") anim,
174            in("x1") &mut v as *mut i64,
175            in("x2") view.meta,
176            in("x3") int_meta,
177            in("x4") view.wt,
178            in("x5") eq_wt,
179            in("x8") result.as_mut_ptr(),
180            in("x20") this,
181            lateout("x6") _, lateout("x7") _,
182            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
183            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
184            lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
185        );
186        wrap_modified(result, &ANIMATION_T, view)
187    }
188}
189
190/// Apply `.onAppear(perform:)`.
191pub fn on_appear(view: &ViewHandle, action: impl Fn() + 'static) -> ViewHandle {
192    unsafe {
193        // SwiftUI stores the action, so the context is leaked on purpose.
194        let ctx = Box::leak(Box::new(abi::ClosureCtx::new(
195            invoke_action,
196            boxed_ctx(Box::new(action) as Box<dyn Fn()>),
197        ))) as *mut abi::ClosureCtx;
198        let (_, _, size) = ON_APPEAR_T.applied_to(view.meta);
199        // Optional<() -> ()> is just the (function, context) pair; nil is a null
200        // function pointer.
201        let r = abi::call_modifier(resolve::on_appear_fn(), view.value_ptr(), view.meta, view.wt,
202            &[abi::swift_closure_thunk as *const () as u64, ctx as u64],
203            [0.0; 4],
204            size,
205        );
206        wrap_modified(r, &ON_APPEAR_T, view)
207    }
208}
209
210/// Apply `.onTapGesture(count:perform:)`.
211/// The action may capture, as [`super::button`]'s may — a tap handler that
212/// cannot know which row it is on is one every list has to route around.
213pub fn on_tap_gesture(view: &ViewHandle, count: i64, action: impl Fn() + 'static) -> ViewHandle {
214    unsafe {
215        let boxed: Box<dyn Fn()> = Box::new(action);
216        let payload = Box::into_raw(Box::new(boxed)) as *mut c_void;
217        let ctx = Box::leak(Box::new(abi::ClosureCtx::new(
218            super::run_boxed_action,
219            payload,
220        ))) as *mut abi::ClosureCtx;
221        let subst = [view.meta, view.wt];
222        let (meta, wt, size) = opaque_result_type(resolve::on_tap_oret(), &subst);
223        let r = abi::call_modifier(resolve::on_tap_fn(), view.value_ptr(), view.meta, view.wt,
224            &[count as u64, abi::swift_closure_thunk as *const () as u64, ctx as u64],
225            [0.0; 4],
226            size,
227        );
228        wrap_owned(r, meta, wt)
229    }
230}
231
232/// Apply `.onLongPressGesture(minimumDuration:maximumDistance:pressing:perform:)`.
233///
234/// Unlike [`long_press_gesture`], which attaches a bare `LongPressGesture` with
235/// nothing listening, this takes the action SwiftUI runs when the press
236/// completes — so the gesture can be observed rather than only built.
237pub fn on_long_press_gesture(
238    view: &ViewHandle,
239    duration: f64,
240    max_distance: f64,
241    action: impl Fn() + 'static,
242) -> ViewHandle {
243    unsafe {
244        let ctx = Box::leak(Box::new(abi::ClosureCtx::new(
245            invoke_action,
246            boxed_ctx(Box::new(action) as Box<dyn Fn()>),
247        ))) as *mut abi::ClosureCtx;
248        let subst = [view.meta, view.wt];
249        let (meta, wt, size) = opaque_result_type(resolve::on_long_press_oret(), &subst);
250        // x0/x1 = `pressing:`, an Optional closure left nil; x2/x3 = `perform:`.
251        let r = abi::call_modifier(resolve::on_long_press_fn(), view.value_ptr(), view.meta, view.wt,
252            &[0, 0, abi::swift_closure_thunk as *const () as u64, ctx as u64],
253            [duration, max_distance, 0.0, 0.0],
254            size,
255        );
256        wrap_owned(r, meta, wt)
257    }
258}
259
260// `.animation(_:value:)` is deliberately not bound.
261//
262// `_AnimationModifier` constructs and renders correctly once, then SwiftUI
263// faults inside `_AnimationModifier.== infix` the next time it diffs the view.
264// The `Optional<Animation>` is built through the value witness table and sized
265// by stride, so this is not the buffer-overflow bug that `transition` had — the
266// remaining suspicion is the ownership of `Animation.default`, which may be
267// returned at +0 rather than +1, making the modifier's copy over-release.
268//
269// Shipping a binding that crashes on the second render would be worse than
270// leaving it out. `.transition(_:)`, `.onAppear`, `.onTapGesture` and
271// `.contextMenu` are bound and verified.
272
273/// Apply `.contextMenu(menuItems:)`.
274pub fn context_menu(view: &ViewHandle, items: &ViewHandle) -> ViewHandle {
275    unsafe {
276        // A context menu's items are built when the menu opens, so the closure
277        // escapes and its context must outlive this call.
278        let ctx = Box::leak(Box::new(abi::ClosureCtx::new(
279            emit_tuple,
280            Child::leak(items, Content::Concrete),
281        ))) as *mut abi::ClosureCtx;
282
283        // Self, then the menu's `M` — which the closure emits as an AnyView.
284        let subst = [
285            view.meta,
286            items.meta,
287            view.wt,
288            items.wt,
289        ];
290        let (meta, wt, size) = opaque_result_type(resolve::context_menu_oret(), &subst);
291        let mut result = vec![0u8; size];
292        let this = view.value_ptr();
293        // contextMenu<M: View>(menuItems: () -> M)
294        //   x0 = closure fn, x1 = closure ctx, x2 = Self.Type, x3 = M.Type,
295        //   x4 = Self: View WT, x5 = M: View WT, x8 = @out, x20 = self
296        core::arch::asm!("blr {f}", f = in(reg) resolve::context_menu_fn(),
297            in("x0") abi::swift_closure_thunk as *const c_void,
298            in("x1") ctx,
299            in("x2") view.meta,                  // Self.Type
300            in("x3") items.meta,    // M.Type
301            in("x4") view.wt,                    // Self: View
302            in("x5") items.wt,      // M: View
303            in("x8") result.as_mut_ptr(),
304            in("x20") this,
305            lateout("x6") _, lateout("x7") _,
306            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
307            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
308            lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
309        );
310        wrap_owned(result, meta, wt)
311    }
312}
313
314/// `AnyTransition.scale(scale:anchor:)`, as the single word it is.
315///
316/// Loadable for the reason [`transition_opacity`] gives: an `AnyTransition` is
317/// one class reference, so the static returns it in x0.
318pub fn transition_scale(scale: f64, anchor: (f64, f64)) -> u64 {
319    unsafe {
320        let out: u64;
321        // Three doubles and nothing else: `scale(scale:anchor:)` is a static on a
322        // struct, so there is no metatype in x20 — passing one is what the
323        // resilient statics next door need, not this.
324        core::arch::asm!("blr {f}", f = in(reg) resolve::transition_scale_anchor(),
325            in("d0") scale, in("d1") anchor.0, in("d2") anchor.1,
326            lateout("x0") out,
327            lateout("x20") _,
328            lateout("x1") _, lateout("x2") _, lateout("x3") _, lateout("x4") _,
329            lateout("x5") _, lateout("x6") _, lateout("x7") _, lateout("x8") _,
330            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
331            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
332            lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
333        );
334        out
335    }
336}
337
338/// `AnyTransition.opacity`, as one word.
339pub fn transition_opacity_value() -> u64 {
340    unsafe {
341        let meta = abi::metadata0(resolve::any_transition_ma());
342        static_getter(resolve::transition_opacity(), meta).0
343    }
344}
345
346/// `AnyTransition.identity` — a transition that does nothing.
347///
348/// Not the same as leaving the modifier off: `.identity` *replaces* the
349/// transition a container would otherwise hand down, which is how one row of
350/// an animated list is held still while the rest move.
351pub fn transition_identity_value() -> u64 {
352    unsafe {
353        let meta = abi::metadata0(resolve::any_transition_ma());
354        static_getter(resolve::transition_identity(), meta).0
355    }
356}
357
358/// `AnyTransition.scale` — the static property, which scales from nothing.
359///
360/// [`transition_scale`] is the *method* `scale(scale:anchor:)`, a different
361/// symbol that takes a factor and an anchor. This is the bare `.scale` Swift
362/// source writes when it wants the default.
363pub fn transition_scale_value() -> u64 {
364    unsafe {
365        let meta = abi::metadata0(resolve::any_transition_ma());
366        static_getter(resolve::transition_scale(), meta).0
367    }
368}
369
370/// `AnyTransition.combined(with:)` — the receiver in x20, the other in x0.
371pub fn transition_combined(lhs: u64, rhs: u64) -> u64 {
372    unsafe {
373        let out: u64;
374        // `combined(with:)` takes its receiver as an ordinary second argument in
375        // x1, *by value*, not through the self register — an `AnyTransition` is
376        // one reference, so there is nothing for swiftself to point at.
377        core::arch::asm!("blr {f}", f = in(reg) resolve::transition_combined(),
378            in("x0") rhs,
379            in("x1") lhs,
380            lateout("x0") out,
381            lateout("x20") _,
382            lateout("x2") _, lateout("x3") _, lateout("x4") _,
383            lateout("x5") _, lateout("x6") _, lateout("x7") _, lateout("x8") _,
384            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
385            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
386            lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
387        );
388        out
389    }
390}
391
392/// Apply `.transition(_:)` with an `AnyTransition` already built.
393pub fn transition(view: &ViewHandle, value: u64) -> ViewHandle {
394    unsafe {
395        let (_, _, size) = TRANSITION_T.applied_to(view.meta);
396        let r = abi::call_modifier(resolve::transition_fn(), view.value_ptr(), view.meta, view.wt, &[value], [0.0; 4], size);
397        wrap_modified(r, &TRANSITION_T, view)
398    }
399}
400
401
402/// `.contextMenu(menuItems:preview:)` — a menu with something to look at.
403///
404/// The preview is what a long press enlarges before the menu appears: the row
405/// blown up, a photo, a card. [`context_menu`] has no such slot, so the menu
406/// came up over the view as it already was.
407///
408/// The `isPresented:` overload beside it in the export trie is *not* bound. It
409/// is there, and a call to it builds — but Swift rejects
410/// `contextMenu(isPresented:menuItems:)` with "extra argument", so it is not
411/// public API on this SDK and there is no reference to check a binding
412/// against. `Toggle(isOn: [Binding<Bool>])` is exported and unwritable in the
413/// same way.
414pub fn context_menu_preview(
415    view: &ViewHandle,
416    items: &ViewHandle,
417    preview: &ViewHandle,
418) -> ViewHandle {
419    unsafe {
420        let items_ctx = Box::leak(Box::new(abi::ClosureCtx::new(
421            emit_tuple,
422            Child::leak(items, Content::Concrete),
423        ))) as *mut abi::ClosureCtx;
424        let preview_ctx = Box::leak(Box::new(abi::ClosureCtx::new(
425            emit_tuple,
426            Child::leak(preview, Content::Concrete),
427        ))) as *mut abi::ClosureCtx;
428
429        let subst = [view.meta, items.meta, preview.meta, view.wt, items.wt, preview.wt];
430        let (meta, wt, size) = opaque_result_type(resolve::context_menu_preview_oret(), &subst);
431        let mut result = vec![0u8; size];
432        let this = view.value_ptr();
433        // contextMenu<M: View, P: View>(menuItems: () -> M, preview: () -> P)
434        //   x0/x1 = menu closure, x2/x3 = preview closure, x4 = Self.Type,
435        //   x5 = M.Type, x6 = P.Type, x7 = Self: View WT,
436        //   [sp] = M: View WT, [sp+8] = P: View WT, x8 = @out, x20 = self
437        core::arch::asm!(
438            "sub sp, sp, #16",
439            "stp {mw}, {pw}, [sp]",
440            "blr {f}",
441            "add sp, sp, #16",
442            f = in(reg) resolve::context_menu_preview_fn(),
443            mw = in(reg) items.wt,
444            pw = in(reg) preview.wt,
445            in("x0") abi::swift_closure_thunk as *const c_void,
446            in("x1") items_ctx,
447            in("x2") abi::swift_closure_thunk as *const c_void,
448            in("x3") preview_ctx,
449            in("x4") view.meta,
450            in("x5") items.meta,
451            in("x6") preview.meta,
452            in("x7") view.wt,
453            in("x8") result.as_mut_ptr(),
454            in("x20") this,
455            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
456            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
457            lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
458        );
459        wrap_owned(result, meta, wt)
460    }
461}