Skip to main content

swiftui_native/views/
compositing.rs

1//! Compositing, 3D, and the rest of the gesture family.
2
3use super::*;
4use crate::abi;
5use crate::resolve;
6use core::ffi::c_void;
7use crate::mangle::{self, swift_type};
8mangle::modifier_types! {
9    /// `.onChange(of:initial:_:)` over an `Int` adds two modifiers; these
10    /// compose as `ModifiedContent<ModifiedContent<Receiver, inner>, outer>`.
11    pub(crate) ON_CHANGE_INNER_T = ty("_ValueActionModifier2").of(vec![Type::Verbatim("Si".into())]);
12    /// Watching a `String`.
13    pub(crate) ON_CHANGE_STR_T = ty("_ValueActionModifier2").of(vec![Type::Verbatim("SS".into())]);
14    /// Watching a `Bool`.
15    pub(crate) ON_CHANGE_BOOL_T = ty("_ValueActionModifier2").of(vec![Type::Verbatim("Sb".into())]);
16    /// Watching a `Double`.
17    pub(crate) ON_CHANGE_F64_T = ty("_ValueActionModifier2").of(vec![Type::Verbatim("Sd".into())]);
18    /// The outer half of `.onChange(of:initial:_:)`.
19    pub(crate) ON_CHANGE_OUTER_T = ty("_AppearanceActionModifier");
20}
21mangle::modifier_types! {
22    /// `ModifiedContent<AnyView, _ClipEffect<Rectangle>>`
23    pub(crate) CLIPPED_T = ty("_ClipEffect").of(vec![ty("Rectangle")]);
24    /// `ModifiedContent<AnyView, _CompositingGroupEffect>`
25    pub(crate) COMPOSITING_T = ty("_CompositingGroupEffect");
26    /// `ModifiedContent<AnyView, _DrawingGroupEffect>`
27    pub(crate) DRAWING_GROUP_T = ty("_DrawingGroupEffect");
28    /// `ModifiedContent<AnyView, _Rotation3DEffect>`
29    pub(crate) ROTATION3D_T = ty("_Rotation3DEffect");
30    /// `ModifiedContent<AnyView, AddGestureModifier<DragGesture, DefaultGestureCombiner>>`
31    pub(crate) DRAG_T = ty("AddGestureModifier").of(vec![ty("DragGesture"), ty("DefaultGestureCombiner")]);
32    /// `ModifiedContent<AnyView, AddGestureModifier<MagnifyGesture, DefaultGestureCombiner>>`
33    pub(crate) MAGNIFY_T = ty("AddGestureModifier").of(vec![ty("MagnifyGesture"), ty("DefaultGestureCombiner")]);
34    /// `ModifiedContent<AnyView, AddGestureModifier<RotateGesture, DefaultGestureCombiner>>`
35    pub(crate) ROTATE_T = ty("AddGestureModifier").of(vec![ty("RotateGesture"), ty("DefaultGestureCombiner")]);
36    /// `.popover(isPresented:content:)`
37    /// `ModifiedContent<AnyView, PopoverPresentationModifier<PopoverConditionalStateProvider<AnyView>>>`
38    pub(crate) POPOVER_T = ty("PopoverPresentationModifier").of(vec![ty("PopoverConditionalStateProvider").of(vec![ty("AnyView")])]);
39}
40
41
42
43
44
45
46
47/// Apply `.clipped()`.
48pub fn clipped(view: &ViewHandle) -> ViewHandle {
49    unsafe {
50        let (_, _, size) = CLIPPED_T.applied_to(view.meta);
51        let r = abi::call_modifier(resolve::clipped_fn(), view.value_ptr(), view.meta, view.wt, &[0], [0.0; 4], size);
52        wrap_modified(r, &CLIPPED_T, view)
53    }
54}
55
56/// Apply `.compositingGroup()`.
57pub fn compositing_group(view: &ViewHandle) -> ViewHandle {
58    unsafe {
59        let (_, _, size) = COMPOSITING_T.applied_to(view.meta);
60        let r = abi::call_modifier(resolve::compositing_group_fn(), view.value_ptr(), view.meta, view.wt, &[], [0.0; 4], size);
61        wrap_modified(r, &COMPOSITING_T, view)
62    }
63}
64
65/// Apply `.drawingGroup()` — render the subtree offscreen through Metal.
66pub fn drawing_group(view: &ViewHandle) -> ViewHandle {
67    unsafe {
68        let (_, _, size) = DRAWING_GROUP_T.applied_to(view.meta);
69        // `ColorRenderingMode` is a resilient enum, so it is address-only and
70        // passed by pointer rather than by value.
71        let mut mode = [0u8]; // .nonLinear
72        let r = abi::call_modifier(resolve::drawing_group_fn(), view.value_ptr(), view.meta, view.wt,
73            &[0, mode.as_mut_ptr() as u64],
74            [0.0; 4],
75            size,
76        );
77        wrap_modified(r, &DRAWING_GROUP_T, view)
78    }
79}
80
81/// Apply `.rotation3DEffect(_:axis:)`.
82pub fn rotation_3d(view: &ViewHandle, degrees: f64, ax: f64, ay: f64, az: f64) -> ViewHandle {
83    unsafe {
84        let (_, _, size) = ROTATION3D_T.applied_to(view.meta);
85        let mut result = vec![0u8; size];
86        let this = view.value_ptr();
87        let radians = degrees * std::f64::consts::PI / 180.0;
88        let (anchor_x, anchor_y) = (0.5f64, 0.5f64);
89        // rotation3DEffect(_:axis:anchor:anchorZ:perspective:)
90        //   every argument is a CGFloat or an Angle, so they all travel in
91        //   d0-d7: angle, axis x/y/z, anchor x/y, anchorZ, perspective
92        core::arch::asm!("blr {f}", f = in(reg) resolve::rotation3d_fn(),
93            in("d0") radians,
94            in("d1") ax, in("d2") ay, in("d3") az,
95            in("d4") anchor_x, in("d5") anchor_y,
96            in("d6") 0.0f64, in("d7") 1.0f64,
97            in("x0") view.meta,
98            in("x1") view.wt,
99            in("x8") result.as_mut_ptr(),
100            in("x20") this,
101            lateout("x2") _, lateout("x3") _, lateout("x4") _, lateout("x5") _,
102            lateout("x6") _, lateout("x7") _, lateout("x9") _, lateout("x10") _,
103            lateout("x11") _, lateout("x12") _, lateout("x13") _, lateout("x14") _,
104            lateout("x15") _, lateout("x16") _, lateout("x17") _,
105            lateout("lr") _, lateout("x21") _, clobber_abi("C"),
106        );
107        wrap_modified(result, &ROTATION3D_T, view)
108    }
109}
110
111/// Attach a gesture value that is already built.
112pub(crate) unsafe fn attach_gesture(
113    view: &ViewHandle,
114    gesture: &mut [u8],
115    g_meta: *const c_void,
116    modifier: &ModifierType,
117) -> ViewHandle {
118    unsafe {
119        let g_wt = abi::get_view_wt_for_proto(g_meta, resolve::gesture_proto());
120        assert!(!g_wt.is_null(), "gesture type: Gesture");
121        let (_, _, size) = modifier.applied_to(view.meta);
122        let mut result = vec![0u8; size];
123        let this = view.value_ptr();
124        core::arch::asm!("blr {f}", f = in(reg) resolve::gesture_fn(),
125            in("x0") gesture.as_mut_ptr(),
126            in("x1") 3u64, // GestureMask.all
127            in("x2") view.meta,
128            in("x3") g_meta,
129            in("x4") view.wt,
130            in("x5") g_wt,
131            in("x8") result.as_mut_ptr(),
132            in("x20") this,
133            lateout("x6") _, lateout("x7") _,
134            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
135            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
136            lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
137        );
138        wrap_modified(result, modifier, view)
139    }
140}
141
142/// Attach a `DragGesture`.
143pub fn drag_gesture(view: &ViewHandle, minimum_distance: f64) -> ViewHandle {
144    unsafe {
145        let g_meta = abi::metadata0(resolve::drag_gesture_ma());
146        let space_meta = abi::metadata0(resolve::local_space_ma());
147        let mut gesture = vec![0u8; abi::type_bytes(g_meta)];
148        // DragGesture.init<C: CoordinateSpaceProtocol>(minimumDistance:coordinateSpace:)
149        //   d0 = distance, x0 = @in C (LocalCoordinateSpace is zero-size),
150        //   x1 = C.Type, x2 = C: CoordinateSpaceProtocol WT, x8 = @out
151        let mut space = [0u8; 8];
152        core::arch::asm!("blr {f}", f = in(reg) resolve::drag_gesture_init(),
153            in("d0") minimum_distance,
154            in("x0") space.as_mut_ptr(),
155            in("x1") space_meta,
156            in("x2") resolve::local_space_wt(),
157            in("x8") gesture.as_mut_ptr(),
158            in("x20") g_meta,
159            lateout("x3") _, lateout("x4") _, lateout("x5") _, lateout("x6") _,
160            lateout("x7") _, lateout("x9") _, lateout("x10") _, lateout("x11") _,
161            lateout("x12") _, lateout("x13") _, lateout("x14") _, lateout("x15") _,
162            lateout("x16") _, lateout("x17") _, lateout("lr") _, lateout("x21") _,
163            clobber_abi("C"),
164        );
165        attach_gesture(view, &mut gesture, g_meta, &DRAG_T)
166    }
167}
168
169/// Attach a `DragGesture` with an `onEnded` handler.
170///
171/// [`drag_gesture`] attaches a bare `DragGesture` with nothing listening, which
172/// builds the same value Swift would but can never be seen to fire. This wires
173/// the action SwiftUI calls when the drag ends, so the gesture is observable.
174///
175/// The result type is taken from `.gesture`'s opaque-result-type descriptor
176/// rather than a hand-written mangling: the type is
177/// `ModifiedContent<AnyView, AddGestureModifier<_EndedGesture<DragGesture>, _>>`,
178/// and spelling that out by hand is how earlier bindings resolved to null.
179pub fn on_drag_gesture(view: &ViewHandle, minimum_distance: f64, action: impl Fn() + 'static) -> ViewHandle {
180    unsafe {
181        drag_ended(
182            view,
183            minimum_distance,
184            invoke_value_action,
185            boxed_ctx(Box::new(action) as Box<dyn Fn()>),
186        )
187    }
188}
189
190/// The same, with the value SwiftUI delivered.
191///
192/// [`on_drag_gesture`]'s handler is told only *that* a drag ended. This one is
193/// told where it started, where it finished and how fast it was going — see
194/// [`DragValue`].
195pub fn on_drag_ended_value(
196    view: &ViewHandle,
197    minimum_distance: f64,
198    action: impl Fn(DragValue) + 'static,
199) -> ViewHandle {
200    unsafe {
201        drag_ended(
202            view,
203            minimum_distance,
204            invoke_drag_value,
205            boxed_ctx(Box::new(action) as Box<dyn Fn(DragValue)>),
206        )
207    }
208}
209
210/// Build the gesture and attach it — the part both handlers share.
211///
212/// # Safety
213/// `payload` must be what `body` expects.
214unsafe fn drag_ended(
215    view: &ViewHandle,
216    minimum_distance: f64,
217    body: unsafe extern "C" fn(*const u8, *mut c_void),
218    payload: *mut c_void,
219) -> ViewHandle {
220    unsafe {
221        let g_meta = abi::metadata0(resolve::drag_gesture_ma());
222        let space_meta = abi::metadata0(resolve::local_space_ma());
223        let mut gesture = vec![0u8; abi::type_bytes(g_meta)];
224        let mut space = [0u8; 8];
225        core::arch::asm!("blr {f}", f = in(reg) resolve::drag_gesture_init(),
226            in("d0") minimum_distance,
227            in("x0") space.as_mut_ptr(),
228            in("x1") space_meta,
229            in("x2") resolve::local_space_wt(),
230            in("x8") gesture.as_mut_ptr(),
231            in("x20") g_meta,
232            lateout("x3") _, lateout("x4") _, lateout("x5") _, lateout("x6") _,
233            lateout("x7") _, lateout("x9") _, lateout("x10") _, lateout("x11") _,
234            lateout("x12") _, lateout("x13") _, lateout("x14") _, lateout("x15") _,
235            lateout("x16") _, lateout("x17") _, lateout("lr") _, lateout("x21") _,
236            clobber_abi("C"),
237        );
238        gesture_ended(view, &mut gesture, g_meta, body, payload)
239    }
240}
241
242/// Wrap any built gesture in `.onEnded(_:)` and attach it.
243///
244/// Nothing below is drag-specific: `Gesture.onEnded` is an extension on the
245/// protocol, `_EndedGesture` takes whatever gesture it is given, and the result
246/// type comes from `.gesture`'s own opaque-result-type descriptor. So magnify
247/// and rotate get handlers by handing their gesture to this — they had none at
248/// all, which made them the only gestures the DSL could attach but never hear
249/// from.
250///
251/// # Safety
252/// `gesture` must be a live value of type `g_meta`, and `payload` what `body`
253/// expects.
254pub(crate) unsafe fn gesture_ended(
255    view: &ViewHandle,
256    gesture: &mut [u8],
257    g_meta: *const c_void,
258    body: unsafe extern "C" fn(*const u8, *mut c_void),
259    payload: *mut c_void,
260) -> ViewHandle {
261    unsafe {
262        let g_wt = abi::get_view_wt_for_proto(g_meta, resolve::gesture_proto());
263        assert!(!g_wt.is_null(), "gesture type: Gesture");
264
265        // `_EndedGesture<Content: Gesture>`: one generic parameter and one
266        // conformance requirement, so the accessor takes two key arguments.
267        let ended_meta =
268            abi::generic_metadata2(resolve::ended_gesture_ma(), g_meta, g_wt);
269        let mut ended = vec![0u8; abi::type_bytes(ended_meta)];
270
271        // The handler takes `DragGesture.Value` by pointer and returns nothing,
272        // which is exactly the shape `value_closure_thunk` was written for.
273        let ctx = Box::leak(Box::new(abi::ValueClosureCtx::new(body, payload)))
274            as *mut abi::ValueClosureCtx;
275
276        // Gesture.onEnded(_:) — x0/x1 = closure, x2 = Self.Type,
277        // x3 = Self: Gesture WT, x8 = @out, x20 = @in_guaranteed self.
278        core::arch::asm!("blr {f}", f = in(reg) resolve::gesture_on_ended(),
279            in("x0") abi::value_closure_thunk as *const () as u64,
280            in("x1") ctx as u64,
281            in("x2") g_meta,
282            in("x3") g_wt,
283            in("x8") ended.as_mut_ptr(),
284            in("x20") gesture.as_mut_ptr(),
285            lateout("x4") _, lateout("x5") _, lateout("x6") _, lateout("x7") _,
286            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
287            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
288            lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
289        );
290
291        let ended_wt = abi::get_view_wt_for_proto(ended_meta, resolve::gesture_proto());
292        assert!(!ended_wt.is_null(), "_EndedGesture<G>: Gesture");
293
294        let subst = [
295            view.meta,
296            ended_meta,
297            view.wt,
298            ended_wt,
299        ];
300        let (meta, wt, size) = opaque_result_type(resolve::gesture_oret(), &subst);
301        let mut result = vec![0u8; size];
302        let this = view.value_ptr();
303        core::arch::asm!("blr {f}", f = in(reg) resolve::gesture_fn(),
304            in("x0") ended.as_mut_ptr(),
305            in("x1") 3u64, // GestureMask.all
306            in("x2") view.meta,
307            in("x3") ended_meta,
308            in("x4") view.wt,
309            in("x5") ended_wt,
310            in("x8") result.as_mut_ptr(),
311            in("x20") this,
312            lateout("x6") _, lateout("x7") _,
313            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
314            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
315            lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
316        );
317        wrap_owned(result, meta, wt)
318    }
319}
320
321/// Body for a `(Value) -> ()` handler that ignores the value.
322pub(crate) unsafe extern "C" fn invoke_value_action(_value: *const u8, payload: *mut c_void) {
323    let action = unsafe { boxed_ref::<dyn Fn()>(payload) };
324    action();
325}
326
327/// Attach a `MagnifyGesture`.
328pub fn magnify_gesture(view: &ViewHandle, minimum_scale_delta: f64) -> ViewHandle {
329    unsafe {
330        let g_meta = abi::metadata0(resolve::magnify_gesture_ma());
331        let mut gesture = vec![0u8; abi::type_bytes(g_meta)];
332        core::arch::asm!("blr {f}", f = in(reg) resolve::magnify_gesture_init(),
333            in("d0") minimum_scale_delta,
334            in("x8") gesture.as_mut_ptr(),
335            in("x20") g_meta,
336            lateout("x0") _, lateout("x1") _, lateout("x2") _, lateout("x3") _,
337            lateout("x4") _, lateout("x5") _, lateout("x6") _, lateout("x7") _,
338            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
339            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
340            lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
341        );
342        attach_gesture(view, &mut gesture, g_meta, &MAGNIFY_T)
343    }
344}
345
346/// Attach a `RotateGesture`. The minimum delta is an `Angle`, i.e. radians.
347pub fn rotate_gesture(view: &ViewHandle, minimum_degrees: f64) -> ViewHandle {
348    unsafe {
349        let g_meta = abi::metadata0(resolve::rotate_gesture_ma());
350        let mut gesture = vec![0u8; abi::type_bytes(g_meta)];
351        let radians = minimum_degrees * std::f64::consts::PI / 180.0;
352        core::arch::asm!("blr {f}", f = in(reg) resolve::rotate_gesture_init(),
353            in("d0") radians,
354            in("x8") gesture.as_mut_ptr(),
355            in("x20") g_meta,
356            lateout("x0") _, lateout("x1") _, lateout("x2") _, lateout("x3") _,
357            lateout("x4") _, lateout("x5") _, lateout("x6") _, lateout("x7") _,
358            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
359            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
360            lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
361        );
362        attach_gesture(view, &mut gesture, g_meta, &ROTATE_T)
363    }
364}
365
366
367
368/// The Rust side of an `onChange` handler.
369pub(crate) type ChangeHandler = Box<dyn Fn(i64, i64)>;
370
371pub(crate) unsafe extern "C" fn invoke_change(old: *const u8, new: *const u8, payload: *mut c_void) {
372    let handler = unsafe { &*(payload as *const ChangeHandler) };
373    unsafe { handler(*(old as *const i64), *(new as *const i64)) };
374}
375
376/// Apply `.onChange(of:initial:_:)`, watching an `Int`.
377///
378/// The handler is called with the old and new values. It is leaked, since
379/// SwiftUI holds it for the lifetime of the view.
380pub fn on_change(
381    view: &ViewHandle,
382    value: i64,
383    handler: impl Fn(i64, i64) + 'static,
384) -> ViewHandle {
385    unsafe {
386        let mut v = value;
387        on_change_of(
388            view,
389            (&raw mut v).cast(),
390            resolve::int_meta(),
391            &ON_CHANGE_INNER_T,
392            invoke_change,
393            boxed_ctx(Box::new(handler) as Box<dyn Fn(i64, i64)>),
394        )
395    }
396}
397
398/// Apply `.onChange(of:initial:_:)`, watching a `String`.
399///
400/// `onChange` is one of the modifiers an application reaches for most, and it
401/// could only ever watch an `Int` — a search field, a selected identifier, a
402/// mode name all had to be turned into a number first. `V` is constrained to
403/// `Equatable` and nothing else, so anything with a witness can be watched;
404/// these are the three the standard library exports one for besides `Int`.
405pub fn on_change_str(
406    view: &ViewHandle,
407    value: &str,
408    handler: impl Fn(String, String) + 'static,
409) -> ViewHandle {
410    unsafe {
411        // `of:` is `@in V`, which is *consumed* — the callee takes ownership,
412        // so the string is handed over and not dropped here.
413        let mut v = abi::swift_string(value);
414        on_change_of(
415            view,
416            v.as_mut_ptr(),
417            resolve::string_meta(),
418            &ON_CHANGE_STR_T,
419            invoke_change_str,
420            boxed_ctx(Box::new(handler) as Box<dyn Fn(String, String)>),
421        )
422    }
423}
424
425/// Apply `.onChange(of:initial:_:)`, watching a `Bool`.
426pub fn on_change_bool(
427    view: &ViewHandle,
428    value: bool,
429    handler: impl Fn(bool, bool) + 'static,
430) -> ViewHandle {
431    unsafe {
432        let mut v = u8::from(value);
433        on_change_of(
434            view,
435            &raw mut v,
436            resolve::bool_meta(),
437            &ON_CHANGE_BOOL_T,
438            invoke_change_bool,
439            boxed_ctx(Box::new(handler) as Box<dyn Fn(bool, bool)>),
440        )
441    }
442}
443
444/// Apply `.onChange(of:initial:_:)`, watching a `Double`.
445pub fn on_change_f64(
446    view: &ViewHandle,
447    value: f64,
448    handler: impl Fn(f64, f64) + 'static,
449) -> ViewHandle {
450    unsafe {
451        let mut v = value;
452        on_change_of(
453            view,
454            (&raw mut v).cast(),
455            resolve::double_meta(),
456            &ON_CHANGE_F64_T,
457            invoke_change_f64,
458            boxed_ctx(Box::new(handler) as Box<dyn Fn(f64, f64)>),
459        )
460    }
461}
462
463pub(crate) unsafe extern "C" fn invoke_change_str(
464    old: *const u8,
465    new: *const u8,
466    payload: *mut c_void,
467) {
468    unsafe {
469        let f = boxed_ref::<dyn Fn(String, String)>(payload);
470        let read = |p: *const u8| {
471            let mut b = [0u8; 16];
472            b.copy_from_slice(core::slice::from_raw_parts(p, 16));
473            // A borrowed read: both strings belong to SwiftUI for the duration
474            // of the call, so this copies out rather than taking ownership.
475            abi::read_swift_string(b)
476        };
477        f(read(old), read(new));
478    }
479}
480
481pub(crate) unsafe extern "C" fn invoke_change_bool(
482    old: *const u8,
483    new: *const u8,
484    payload: *mut c_void,
485) {
486    unsafe {
487        let f = boxed_ref::<dyn Fn(bool, bool)>(payload);
488        f(*old & 1 != 0, *new & 1 != 0);
489    }
490}
491
492pub(crate) unsafe extern "C" fn invoke_change_f64(
493    old: *const u8,
494    new: *const u8,
495    payload: *mut c_void,
496) {
497    unsafe {
498        let f = boxed_ref::<dyn Fn(f64, f64)>(payload);
499        f(*(old as *const f64), *(new as *const f64));
500    }
501}
502
503/// The half of `onChange` that does not depend on what is being watched.
504///
505/// `onChange<V: Equatable>` is generic, so everything type-specific is a
506/// register: the value's address, its metadata, and the mangled name of the
507/// `_ValueActionModifier2<V>` the result nests in.
508///
509/// # Safety
510/// `value` must point at a live value of type `meta`, which the call consumes;
511/// `inner` must be `_ValueActionModifier2` over that same type; and `payload`
512/// must be what `body` expects.
513unsafe fn on_change_of(
514    view: &ViewHandle,
515    value: *mut u8,
516    meta: *const c_void,
517    inner: &ModifierType,
518    body: unsafe extern "C" fn(*const u8, *const u8, *mut c_void),
519    payload: *mut c_void,
520) -> ViewHandle {
521    unsafe {
522        let ctx = Box::leak(Box::new(abi::PairClosureCtx::new(body, payload)))
523            as *mut abi::PairClosureCtx;
524
525        let eq_wt = abi::get_view_wt_for_proto(meta, resolve::equatable_proto());
526        assert!(!eq_wt.is_null(), "{}: Equatable", crate::diag::type_name(meta));
527
528        let (inner_m, _, _) = inner.applied_to(view.meta);
529        let (_, _, size) = ON_CHANGE_OUTER_T.applied_to(inner_m);
530        let mut result = vec![0u8; size];
531        let this = view.value_ptr();
532        // onChange<V: Equatable>(of: V, initial: Bool, _: (V, V) -> ())
533        //   x0 = @in V, x1 = initial, x2 = closure fn, x3 = closure ctx,
534        //   x4 = Self.Type, x5 = V.Type, x6 = Self: View WT,
535        //   x7 = V: Equatable WT, x8 = @out, x20 = self
536        core::arch::asm!("blr {f}", f = in(reg) resolve::on_change_fn(),
537            in("x0") value,
538            in("x1") 0u64,
539            in("x2") abi::pair_closure_thunk as *const c_void,
540            in("x3") ctx,
541            in("x4") view.meta,
542            in("x5") meta,
543            in("x6") view.wt,
544            in("x7") eq_wt,
545            in("x8") result.as_mut_ptr(),
546            in("x20") this,
547            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
548            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
549            lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
550        );
551        // `onChange` adds two modifiers, so the result nests twice: the
552        // value-action modifier over the receiver, then the appearance-action
553        // modifier over that.
554        let (nested, _, _) = inner.applied_to(view.meta);
555        let (meta, wt, _) = ON_CHANGE_OUTER_T.applied_to(nested);
556        wrap_owned(result, meta, wt)
557    }
558}
559
560
561
562/// Apply `.popover(isPresented:content:)`.
563///
564/// The public `popover` is fully inlined — it has a symbol but nothing ever
565/// calls it — so this goes through `popoverCore`, which is what Swift itself
566/// ends up calling.
567/// Shared tail of `popover` and `popover_bound`.
568///
569/// # Safety
570/// `binding` must be a `Binding<Bool>`.
571pub(crate) unsafe fn popover_with_binding(
572    view: &ViewHandle,
573    content: &ViewHandle,
574    binding: &[u8],
575) -> ViewHandle {
576    unsafe {
577        let b0 = u64::from_le_bytes(binding[0..8].try_into().unwrap());
578        let b1 = u64::from_le_bytes(binding[8..16].try_into().unwrap());
579        let b2 = binding[16] as u64;
580
581        // PopoverAttachmentAnchor.rect(.bounds): the Source is one word, the
582        // enum pads to sixteen and carries its tag in the seventeenth byte,
583        // where `.rect` is case 0.
584        let anchor_meta = abi::metadata0(resolve::popover_anchor_ma());
585        let mut anchor = vec![0u8; abi::type_bytes(anchor_meta)];
586        let source: u64;
587        core::arch::asm!("blr {f}", f = in(reg) resolve::anchor_source_bounds(),
588            lateout("x0") source,
589            lateout("x1") _, lateout("x2") _, lateout("x3") _, lateout("x4") _,
590            lateout("x5") _, lateout("x6") _, lateout("x7") _, lateout("x8") _,
591            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
592            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
593            lateout("x17") _, lateout("lr") _, lateout("x20") _, lateout("x21") _,
594            clobber_abi("C"),
595        );
596        anchor[..8].copy_from_slice(&source.to_le_bytes());
597
598        let ctx = Box::leak(Box::new(abi::ClosureCtx::new(
599            emit_tuple,
600            // `POPOVER_T` names
601            // `PopoverPresentationModifier<PopoverConditionalStateProvider<AnyView>>`:
602            // the content's type is part of the modifier's own name, so the
603            // child stays erased until that name is composed per content.
604            Child::leak(content, Content::Erased),
605        ))) as *mut abi::ClosureCtx;
606
607        let (_, _, size) = POPOVER_T.applied_to(view.meta);
608        let mut result = vec![0u8; size];
609        let this = view.value_ptr();
610        // popoverCore(isPresented:attachmentAnchor:arrowEdges:isDetachable:content:)
611        //   x0-x2 = Binding<Bool>, x3 = @in anchor,
612        //   x4 = Edge.Set? (nil is 0x0100 — value then tag),
613        //   x5 = isDetachable, x6 = content fn, x7 = content ctx,
614        //   [sp] = Self.Type, [sp+8] = C.Type,
615        //   [sp+16] = Self: View WT, [sp+24] = C: View WT
616        core::arch::asm!(
617            "sub sp, sp, #32",
618            "stp {sm}, {cm}, [sp]",
619            "stp {sw}, {cw}, [sp, #16]",
620            "blr {f}",
621            "add sp, sp, #32",
622            f = in(reg) resolve::popover_core_fn(),
623            sm = in(reg) view.meta,
624            cm = in(reg) resolve::anyview_meta(),
625            sw = in(reg) view.wt,
626            cw = in(reg) resolve::anyview_wt(),
627            in("x0") b0, in("x1") b1, in("x2") b2,
628            in("x3") anchor.as_mut_ptr(),
629            in("x4") 0x0100u64,
630            in("x5") 0u64,
631            in("x6") abi::swift_closure_thunk as *const c_void,
632            in("x7") ctx,
633            in("x8") result.as_mut_ptr(),
634            in("x20") this,
635            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
636            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
637            lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
638        );
639        wrap_modified(result, &POPOVER_T, view)
640    }
641}
642
643/// `popover` with a fixed flag. It cannot be dismissed: SwiftUI treats a
644/// `Binding.constant` as read-only, so the presentation has no way to write
645/// `false` back. Use [`popover_bound`] for one the user can close.
646pub fn popover(view: &ViewHandle, content: &ViewHandle, presented: bool) -> ViewHandle {
647    unsafe {
648        let binding = constant_binding(
649            &[u8::from(presented)],
650            resolve::bool_meta(),
651            mangle::bound(&swift_type!(SwiftUI.Binding), &[mangle::BOOL]).as_bytes(),
652        );
653        popover_with_binding(view, content, &binding)
654    }
655}
656
657/// `popover` bound to Rust state, so dismissing it writes `false` back.
658pub fn popover_bound(view: &ViewHandle, content: &ViewHandle, state: &BoundState<bool>) -> ViewHandle {
659    unsafe {
660        let binding = bool_binding_of(state);
661        popover_with_binding(view, content, &binding)
662    }
663}
664
665
666// ── What a drag delivered ───────────────────────────────────────────────────
667
668/// A `DragGesture.Value`, read out of the struct SwiftUI hands the handler.
669///
670/// `on_drag_gesture` used to throw this away — the handler was `() -> ()` and
671/// the value went into `invoke_value_action`, which ignores its argument. A
672/// drag handler that cannot see where the drag went is not much of a drag
673/// handler: it can tell you that something was dragged and nothing else.
674///
675/// The fields come from the struct's own layout rather than from accessors,
676/// because SwiftUI exports exactly one — `translation` — and none of the
677/// others:
678///
679/// ```text
680///   +0   time           Date            (seconds from the 2001 epoch)
681///   +8   location       CGPoint
682///   +24  startLocation  CGPoint
683///   +40  _velocity      _Velocity<CGSize>
684///   +56  platform       (empty)
685/// ```
686///
687/// That layout is read from the metadata at run time rather than trusted: a
688/// field added in front of `location` in a later SwiftUI would otherwise turn
689/// every drag into silent nonsense. If the shape is not the five fields above,
690/// the reader hands back zeroes rather than reading somewhere it should not.
691#[derive(Clone, Copy, Debug, PartialEq)]
692pub struct DragValue {
693    /// Where the drag is now, in the gesture's coordinate space.
694    pub location: (f64, f64),
695    /// Where it began.
696    pub start_location: (f64, f64),
697    /// How fast it was moving, in points per second.
698    ///
699    /// `_Velocity<CGSize>` wraps a single `CGSize` and nothing else — checked
700    /// by following the field record's symbolic reference to the descriptor,
701    /// which reports one stored field — so the two doubles at the field's own
702    /// offset are the velocity, with no wrapper to step over.
703    pub velocity: (f64, f64),
704    /// Seconds since Apple's reference date (1 January 2001), which is what a
705    /// `Date` stores. Useful for comparing two values, not as a wall clock.
706    pub time: f64,
707}
708
709impl DragValue {
710    /// `location - startLocation`, which is what Swift's `translation`
711    /// computes. Cross-checked against SwiftUI's own getter in the tests.
712    pub fn translation(&self) -> (f64, f64) {
713        (self.location.0 - self.start_location.0, self.location.1 - self.start_location.1)
714    }
715}
716
717/// The offsets of `DragGesture.Value`'s stored fields, from its metadata.
718///
719/// Read rather than hardcoded: this is a resilient struct in a system
720/// framework, and a field added in front of `location` would otherwise turn
721/// every drag into silent nonsense. Five fields at these offsets is what this
722/// build of SwiftUI has; a different shape returns `None` and the reader hands
723/// back zeroes rather than reading somewhere it should not.
724fn drag_value_offsets() -> Option<&'static [usize; 4]> {
725    static CACHE: std::sync::OnceLock<Option<[usize; 4]>> = std::sync::OnceLock::new();
726    CACHE
727        .get_or_init(|| unsafe {
728            let meta = abi::metadata0(resolve::drag_value_ma());
729            let offs = abi::struct_field_offsets(meta)?;
730            // time, location, startLocation, _velocity, platform.
731            if offs.len() != 5 {
732                return None;
733            }
734            Some([offs[0], offs[1], offs[2], offs[3]])
735        })
736        .as_ref()
737}
738
739/// Read the value at `p`, which SwiftUI passed by address.
740///
741/// # Safety
742/// `p` must point at a live `DragGesture.Value`.
743pub(crate) unsafe fn read_drag_value(p: *const u8) -> DragValue {
744    let Some(&[time, location, start, velocity]) = drag_value_offsets() else {
745        return DragValue {
746            location: (0.0, 0.0),
747            start_location: (0.0, 0.0),
748            velocity: (0.0, 0.0),
749            time: 0.0,
750        };
751    };
752    unsafe {
753        let point = |at: usize| {
754            let f = p.add(at) as *const f64;
755            (*f, *f.add(1))
756        };
757        let v = DragValue {
758            time: *(p.add(time) as *const f64),
759            location: point(location),
760            start_location: point(start),
761            velocity: point(velocity),
762        };
763        debug_assert_eq!(
764            v.translation(),
765            swift_translation(p),
766            "computed translation disagrees with SwiftUI's own getter — the \
767             field offsets have moved"
768        );
769        v
770    }
771}
772
773/// What SwiftUI's own `translation` getter says, for checking against
774/// [`DragValue::translation`].
775///
776/// Only the one accessor is exported: `velocity`, `predictedEndLocation` and
777/// `predictedEndTranslation` resolve to nothing, so they are computed here or
778/// not at all. This is the one crosscheck available, and it covers `location`
779/// and `startLocation` between them — which are the fields a wrong offset
780/// would corrupt.
781///
782/// # Safety
783/// `p` must point at a live `DragGesture.Value`.
784unsafe fn swift_translation(p: *const u8) -> (f64, f64) {
785    unsafe {
786        let (w, h): (f64, f64);
787        // A getter on a 56-byte struct: self is `@in_guaranteed` in x20, and
788        // `CGSize` comes back in d0/d1.
789        core::arch::asm!("blr {f}", f = in(reg) resolve::drag_value_translation(),
790            in("x20") p,
791            lateout("d0") w, lateout("d1") h,
792            lateout("x0") _, lateout("x1") _, lateout("x2") _, lateout("x3") _,
793            lateout("x4") _, lateout("x5") _, lateout("x6") _, lateout("x7") _,
794            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
795            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
796            lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
797        );
798        (w, h)
799    }
800}
801
802/// Body for a `(DragGesture.Value) -> ()` handler that reads the value.
803pub(crate) unsafe extern "C" fn invoke_drag_value(value: *const u8, payload: *mut c_void) {
804    let f = unsafe { boxed_ref::<dyn Fn(DragValue)>(payload) };
805    f(unsafe { read_drag_value(value) });
806}
807
808// ── What a magnify or a rotate delivered ────────────────────────────────────
809
810/// A `MagnifyGesture.Value`.
811///
812/// `magnify_gesture` attaches the gesture and listens to nothing — it was the
813/// only kind of gesture the DSL could add but never hear from. The layout is
814/// the same shape as the drag's and read the same way:
815///
816/// ```text
817///   +0   time           Date
818///   +8   magnification  CGFloat
819///   +16  velocity       CGFloat
820///   +24  startAnchor    UnitPoint
821///   +40  startLocation  CGPoint
822///   +56  platform       (empty)
823/// ```
824#[derive(Clone, Copy, Debug, PartialEq)]
825pub struct MagnifyValue {
826    /// How far the pinch has scaled, as a multiplier — 1.0 is no change.
827    pub magnification: f64,
828    /// How fast it is scaling, in multiples per second.
829    pub velocity: f64,
830    /// Where the pinch is centred, in unit space — (0.5, 0.5) is the middle.
831    pub start_anchor: (f64, f64),
832    /// Where it began, in the gesture's coordinate space.
833    pub start_location: (f64, f64),
834    /// Seconds since Apple's reference date.
835    pub time: f64,
836}
837
838/// A `RotateGesture.Value`.
839///
840/// The same layout as [`MagnifyValue`] with `rotation`/`velocity` as `Angle`,
841/// which stores radians and nothing else.
842#[derive(Clone, Copy, Debug, PartialEq)]
843pub struct RotateValue {
844    /// How far it has turned, in radians. `Angle` stores radians; the degrees
845    /// are computed, so the conversion is here rather than through an accessor.
846    pub radians: f64,
847    /// How fast it is turning, in radians per second.
848    pub velocity: f64,
849    /// Where the rotation is centred, in unit space.
850    pub start_anchor: (f64, f64),
851    /// Where it began, in the gesture's coordinate space.
852    pub start_location: (f64, f64),
853    /// Seconds since Apple's reference date.
854    pub time: f64,
855}
856
857impl RotateValue {
858    /// The rotation in degrees, which is what `Angle.degrees` reports.
859    pub fn degrees(&self) -> f64 {
860        self.radians.to_degrees()
861    }
862}
863
864/// The offsets shared by `MagnifyGesture.Value` and `RotateGesture.Value`.
865///
866/// Both are six fields at 0, 8, 16, 24, 40, 56 in this build of SwiftUI, read
867/// from the metadata for the same reason the drag's are: a field inserted in
868/// front of `magnification` would otherwise be read as the magnification.
869fn pinch_value_offsets(accessor: *const c_void) -> Option<[usize; 5]> {
870    unsafe {
871        let offs = abi::struct_field_offsets(abi::metadata0(accessor))?;
872        // time, amount, velocity, startAnchor, startLocation, platform.
873        if offs.len() != 6 {
874            return None;
875        }
876        Some([offs[0], offs[1], offs[2], offs[3], offs[4]])
877    }
878}
879
880/// Read the five readable fields, which both gestures share.
881///
882/// # Safety
883/// `p` must point at a live value of the type `accessor` names.
884unsafe fn read_pinch_value(p: *const u8, accessor: *const c_void) -> [f64; 7] {
885    let Some([time, amount, velocity, anchor, start]) = pinch_value_offsets(accessor) else {
886        return [0.0; 7];
887    };
888    unsafe {
889        let at = |o: usize| *(p.add(o) as *const f64);
890        let pair = |o: usize| (at(o), at(o + 8));
891        let (ax, ay) = pair(anchor);
892        let (sx, sy) = pair(start);
893        [at(amount), at(velocity), ax, ay, sx, sy, at(time)]
894    }
895}
896
897/// Body for a `(MagnifyGesture.Value) -> ()` handler.
898unsafe extern "C" fn invoke_magnify_value(value: *const u8, payload: *mut c_void) {
899    let f = unsafe { boxed_ref::<dyn Fn(MagnifyValue)>(payload) };
900    let v = unsafe { read_pinch_value(value, resolve::magnify_value_ma()) };
901    f(MagnifyValue {
902        magnification: v[0],
903        velocity: v[1],
904        start_anchor: (v[2], v[3]),
905        start_location: (v[4], v[5]),
906        time: v[6],
907    });
908}
909
910/// Body for a `(RotateGesture.Value) -> ()` handler.
911unsafe extern "C" fn invoke_rotate_value(value: *const u8, payload: *mut c_void) {
912    let f = unsafe { boxed_ref::<dyn Fn(RotateValue)>(payload) };
913    let v = unsafe { read_pinch_value(value, resolve::rotate_value_ma()) };
914    f(RotateValue {
915        radians: v[0],
916        velocity: v[1],
917        start_anchor: (v[2], v[3]),
918        start_location: (v[4], v[5]),
919        time: v[6],
920    });
921}
922
923/// Attach a `MagnifyGesture` with an `onEnded` handler.
924pub fn on_magnify_ended(
925    view: &ViewHandle,
926    minimum_scale_delta: f64,
927    action: impl Fn(MagnifyValue) + 'static,
928) -> ViewHandle {
929    unsafe {
930        let g_meta = abi::metadata0(resolve::magnify_gesture_ma());
931        let mut gesture = vec![0u8; abi::type_bytes(g_meta)];
932        core::arch::asm!("blr {f}", f = in(reg) resolve::magnify_gesture_init(),
933            in("d0") minimum_scale_delta,
934            in("x8") gesture.as_mut_ptr(),
935            in("x20") g_meta,
936            lateout("x0") _, lateout("x1") _, lateout("x2") _, lateout("x3") _,
937            lateout("x4") _, lateout("x5") _, lateout("x6") _, lateout("x7") _,
938            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
939            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
940            lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
941        );
942        gesture_ended(
943            view,
944            &mut gesture,
945            g_meta,
946            invoke_magnify_value,
947            boxed_ctx(Box::new(action) as Box<dyn Fn(MagnifyValue)>),
948        )
949    }
950}
951
952/// Attach a `RotateGesture` with an `onEnded` handler.
953pub fn on_rotate_ended(
954    view: &ViewHandle,
955    minimum_degrees: f64,
956    action: impl Fn(RotateValue) + 'static,
957) -> ViewHandle {
958    unsafe {
959        let g_meta = abi::metadata0(resolve::rotate_gesture_ma());
960        let mut gesture = vec![0u8; abi::type_bytes(g_meta)];
961        // `minimumAngleDelta` is an `Angle`, which stores radians — the same
962        // conversion `rotate_gesture` does, and the reason this is spelled out
963        // rather than left to look like a plain float argument.
964        let radians = minimum_degrees * std::f64::consts::PI / 180.0;
965        core::arch::asm!("blr {f}", f = in(reg) resolve::rotate_gesture_init(),
966            in("d0") radians,
967            in("x8") gesture.as_mut_ptr(),
968            in("x20") g_meta,
969            lateout("x0") _, lateout("x1") _, lateout("x2") _, lateout("x3") _,
970            lateout("x4") _, lateout("x5") _, lateout("x6") _, lateout("x7") _,
971            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
972            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
973            lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
974        );
975        gesture_ended(
976            view,
977            &mut gesture,
978            g_meta,
979            invoke_rotate_value,
980            boxed_ctx(Box::new(action) as Box<dyn Fn(RotateValue)>),
981        )
982    }
983}
984
985#[cfg(test)]
986mod tests {
987    use super::*;
988
989    /// The pinch readers pick the right field out of a real layout.
990    ///
991    /// A synthetic magnify or rotate event is not something AppKit will build —
992    /// there is no public constructor for one — so unlike the drag, these
993    /// readers cannot be driven end to end. What *can* be checked is the part
994    /// that would be wrong if the offsets moved: a buffer is filled with a
995    /// distinct value at each of the offsets the metadata reports, and the
996    /// reader has to come back with them in the right order.
997    ///
998    /// This is a weaker check than the drag's behavioural one and worth saying
999    /// so: it proves the reader agrees with the metadata, not that SwiftUI puts
1000    /// the magnification where its metadata says. The drag case covers that for
1001    /// the shared mechanism.
1002    #[test]
1003    fn pinch_reader_matches_the_live_layout() {
1004        for accessor in [resolve::magnify_value_ma(), resolve::rotate_value_ma()] {
1005            let offsets = pinch_value_offsets(accessor).expect("six stored fields");
1006            let [time, amount, velocity, anchor, start] = offsets;
1007
1008            // A distinct value per field, so a swapped pair cannot pass.
1009            let size = unsafe { abi::type_bytes(abi::metadata0(accessor)) };
1010            let mut buf = vec![0u8; size];
1011            let put = |b: &mut [u8], at: usize, v: f64| {
1012                b[at..at + 8].copy_from_slice(&v.to_le_bytes());
1013            };
1014            put(&mut buf, time, 11.0);
1015            put(&mut buf, amount, 22.0);
1016            put(&mut buf, velocity, 33.0);
1017            put(&mut buf, anchor, 44.0);
1018            put(&mut buf, anchor + 8, 55.0);
1019            put(&mut buf, start, 66.0);
1020            put(&mut buf, start + 8, 77.0);
1021
1022            let got = unsafe { read_pinch_value(buf.as_ptr(), accessor) };
1023            assert_eq!(got, [22.0, 33.0, 44.0, 55.0, 66.0, 77.0, 11.0]);
1024        }
1025    }
1026
1027    /// `Angle` stores radians, so the degrees are computed.
1028    #[test]
1029    fn rotate_value_converts_to_degrees() {
1030        let v = RotateValue {
1031            radians: std::f64::consts::FRAC_PI_2,
1032            velocity: 0.0,
1033            start_anchor: (0.5, 0.5),
1034            start_location: (0.0, 0.0),
1035            time: 0.0,
1036        };
1037        assert!((v.degrees() - 90.0).abs() < 1e-9);
1038    }
1039
1040    /// A drag's translation is the difference of its two points.
1041    #[test]
1042    fn drag_value_translation_is_the_difference() {
1043        let v = DragValue {
1044            location: (142.0, 40.0),
1045            start_location: (70.0, 40.0),
1046            velocity: (164.0, 0.0),
1047            time: 0.0,
1048        };
1049        assert_eq!(v.translation(), (72.0, 0.0));
1050    }
1051}