Skip to main content

swiftui_native/views/
closures.rs

1//! Closure arguments.
2
3use super::*;
4use crate::abi;
5use crate::resolve;
6use core::ffi::c_void;
7use crate::mangle::swift_type;
8// ── Closure arguments ──
9
10//
11// A Swift closure is two words — an entry point and a context — so it drops
12// straight into the integer argument list like any other pair of registers.
13// What varies is the *shape* of the callback, and that is what the thunks in
14// `abi` are for: which registers the argument arrives in, and whether the
15// closure has to write a result through x8.
16//
17// Every context here is leaked. SwiftUI stores an escaping closure for as long
18// as the view lives, and a non-escaping one is called before the modifier
19// returns, so leaking is either necessary or harmless; freeing is neither.
20
21/// The two words of a Swift `() -> ()` closure that runs `action`.
22pub(crate) fn action_words(action: impl Fn() + 'static) -> [u64; 2] {
23    let ctx = Box::leak(Box::new(abi::ClosureCtx::new(invoke_action, boxed_ctx(Box::new(action) as Box<dyn Fn()>))))
24        as *mut abi::ClosureCtx;
25    [abi::swift_closure_thunk as *const () as u64, ctx as u64]
26}
27
28/// The two words of a `() -> ()` closure that runs an already-boxed action.
29///
30/// [`action_words`] takes the closure by value and boxes it; this takes the box
31/// straight, which is what an `Option<Box<dyn Fn()>>` argument already holds.
32pub(crate) fn boxed_action_words(action: Box<dyn Fn()>) -> [u64; 2] {
33    let ctx = Box::leak(Box::new(abi::ClosureCtx::new(
34        super::controls::run_boxed_action,
35        super::boxed_ctx(action),
36    ))) as *mut abi::ClosureCtx;
37    [abi::swift_closure_thunk as *const () as u64, ctx as u64]
38}
39
40/// The two words of a `@ViewBuilder () -> V` closure returning `view`.
41///
42/// The view is built here and handed back whenever SwiftUI asks, which is what
43/// a `ViewBuilder` closure over a fixed subtree amounts to. `V` is the view's
44/// own type — the caller has to name it in the `Content.Type` it passes, and
45/// `view.meta`/`view.wt` are what to name it with.
46pub(crate) fn view_words(view: &ViewHandle) -> [u64; 2] {
47    let ctx = Box::leak(Box::new(abi::ClosureCtx::new(
48        emit_tuple,
49        Child::leak(view, Content::Concrete),
50    ))) as *mut abi::ClosureCtx;
51    [abi::swift_closure_thunk as *const () as u64, ctx as u64]
52}
53
54/// The two words of a one-argument, no-result closure.
55pub(crate) fn wide_words(
56    body: unsafe extern "C" fn(u64, u64, u64, *mut c_void),
57    payload: *mut c_void,
58) -> [u64; 2] {
59    let ctx = Box::leak(Box::new(abi::WideClosureCtx::new(body, payload)))
60        as *mut abi::WideClosureCtx;
61    [abi::wide_closure_thunk as *const () as u64, ctx as u64]
62}
63
64/// The two words of a two-argument, no-result closure.
65pub(crate) fn pair_words(
66    body: unsafe extern "C" fn(*const u8, *const u8, *mut c_void),
67    payload: *mut c_void,
68) -> [u64; 2] {
69    let ctx = Box::leak(Box::new(abi::PairClosureCtx::new(body, payload)))
70        as *mut abi::PairClosureCtx;
71    [abi::pair_closure_thunk as *const () as u64, ctx as u64]
72}
73
74/// A callback that receives an enum's case number.
75///
76/// Whether the case arrives as a value or as an address is not the closure's
77/// choice: a `@frozen` enum is loadable and travels in x0, a resilient one of
78/// the very same single byte travels by pointer. `indirect` records which, and
79/// `getEnumTag` turns either into a case number.
80pub(crate) struct TagCallback {
81    pub(crate) f: Box<dyn Fn(i64)>,
82    pub(crate) meta: *const c_void,
83    pub(crate) indirect: bool,
84}
85
86pub(crate) unsafe extern "C" fn invoke_tag(a0: u64, _a1: u64, _a2: u64, payload: *mut c_void) {
87    unsafe {
88        let cb = &*(payload as *const TagCallback);
89        let tag = if cb.indirect {
90            abi::read_enum_tag(a0 as *const u8, cb.meta)
91        } else {
92            abi::read_enum_tag(a0.to_le_bytes().as_ptr(), cb.meta)
93        };
94        (cb.f)(i64::from(tag));
95    }
96}
97
98/// The two words of an `(E) -> ()` closure delivering `E`'s case number.
99pub(crate) unsafe fn tag_words(f: impl Fn(i64) + 'static, meta: *const c_void, indirect: bool) -> [u64; 2] {
100    let cb = Box::leak(Box::new(TagCallback { f: Box::new(f), meta, indirect }));
101    wide_words(invoke_tag, cb as *mut TagCallback as *mut c_void)
102}
103
104pub(crate) unsafe extern "C" fn invoke_bool(a0: u64, _a1: u64, _a2: u64, payload: *mut c_void) {
105    let f = unsafe { boxed_ref::<dyn Fn(bool)>(payload) };
106    f(a0 & 1 != 0);
107}
108
109/// The two words of a `(Bool) -> ()` closure.
110pub(crate) fn bool_words(f: impl Fn(bool) + 'static) -> [u64; 2] {
111    wide_words(invoke_bool, boxed_ctx(Box::new(f) as Box<dyn Fn(bool)>))
112}
113
114pub(crate) unsafe extern "C" fn invoke_url(a0: u64, _a1: u64, _a2: u64, payload: *mut c_void) {
115    unsafe {
116        let f = boxed_ref::<dyn Fn(&str)>(payload);
117        // `URL` is resilient, so it arrives by address; `absoluteString` is the
118        // one accessor that turns it back into something Rust can read.
119        let (s0, s1): (u64, u64);
120        core::arch::asm!("blr {f}", f = in(reg) resolve::url_absolute_string(),
121            in("x20") a0,
122            lateout("x0") s0, lateout("x1") s1,
123            lateout("x2") _, lateout("x3") _, lateout("x4") _, lateout("x5") _,
124            lateout("x6") _, lateout("x7") _, lateout("x8") _, lateout("x9") _,
125            lateout("x10") _, lateout("x11") _, lateout("x12") _, lateout("x13") _,
126            lateout("x14") _, lateout("x15") _, lateout("x16") _, lateout("x17") _,
127            lateout("lr") _, lateout("x21") _, clobber_abi("C"),
128        );
129        let mut raw = [0u8; 16];
130        raw[..8].copy_from_slice(&s0.to_le_bytes());
131        raw[8..].copy_from_slice(&s1.to_le_bytes());
132        f(&abi::read_swift_string(raw));
133    }
134}
135
136/// The two words of a `(URL) -> ()` closure, delivering the absolute string.
137pub(crate) fn url_words(f: impl Fn(&str) + 'static) -> [u64; 2] {
138    wide_words(invoke_url, boxed_ctx(Box::new(f) as Box<dyn Fn(&str)>))
139}
140
141/// A callback receiving two enum case numbers.
142pub(crate) struct TagPairCallback {
143    pub(crate) f: Box<dyn Fn(i64, i64)>,
144    pub(crate) meta: *const c_void,
145}
146
147pub(crate) unsafe extern "C" fn invoke_tag_pair(a: *const u8, b: *const u8, payload: *mut c_void) {
148    unsafe {
149        let cb = &*(payload as *const TagPairCallback);
150        // Both are frozen single-byte enums, so the registers hold the values.
151        let ta = abi::read_enum_tag((a as u64).to_le_bytes().as_ptr(), cb.meta);
152        let tb = abi::read_enum_tag((b as u64).to_le_bytes().as_ptr(), cb.meta);
153        (cb.f)(i64::from(ta), i64::from(tb));
154    }
155}
156
157pub(crate) unsafe extern "C" fn invoke_word_pair(a: *const u8, b: *const u8, payload: *mut c_void) {
158    let f = unsafe { boxed_ref::<dyn Fn(u64, u64)>(payload) };
159    f(a as u64, b as u64);
160}
161
162/// Apply a modifier with one generic parameter, given the whole argument list.
163///
164/// [`generic1_modifier`] leads with a pointer to the value, which is right when
165/// the generic parameter *is* the argument. A `@ViewBuilder` modifier is the
166/// other case: the generic parameter is what the closure returns, so the
167/// argument list is the closure's two words and the type arguments still have
168/// to follow.
169pub(crate) unsafe fn generic1_modifier_ints(
170    func: *const c_void,
171    oret: *const c_void,
172    view: &ViewHandle,
173    arg_meta: *const c_void,
174    arg_wt: *const c_void,
175    leading: &[u64],
176    floats: [f64; 4],
177) -> ViewHandle {
178    unsafe {
179        let subst = [view.meta, arg_meta, view.wt, arg_wt];
180        let (meta, wt, size) = opaque_result_type(oret, &subst);
181        let mut ints = Vec::with_capacity(leading.len() + 4);
182        ints.extend_from_slice(leading);
183        ints.push(view.meta as u64);
184        ints.push(arg_meta as u64);
185        ints.push(view.wt as u64);
186        ints.push(arg_wt as u64);
187        let r = abi::call_modifier_generic(func, view.value_ptr(), &ints, floats, size);
188        wrap_owned(r, meta, wt)
189    }
190}
191
192/// Apply a `@ViewBuilder content:` modifier whose content is a fixed subtree.
193pub(crate) unsafe fn content_modifier(
194    func: *const c_void,
195    oret: *const c_void,
196    view: &ViewHandle,
197    leading: &[u64],
198    content: &ViewHandle,
199) -> ViewHandle {
200    unsafe {
201        let mut ints = leading.to_vec();
202        ints.extend_from_slice(&view_words(content));
203        generic1_modifier_ints(
204            func,
205            oret,
206            view,
207            content.meta,
208            content.wt,
209            &ints,
210            [0.0; 4],
211        )
212    }
213}
214
215/// Read a `@frozen` static property that fits in one register.
216///
217/// The counterpart to [`resilient_static`]: a frozen type of eight bytes or
218/// fewer is loadable, so its getter returns the value in x0 rather than
219/// writing it through x8.
220pub(crate) unsafe fn frozen_static(meta: *const c_void, getter: &'static core::ffi::CStr) -> u64 {
221    unsafe {
222        let f = resolve::sym(getter);
223        assert!(!f.is_null(), "missing static: {}", crate::diag::symbol_name(getter));
224        abi::static_regs(f, meta)[0]
225    }
226}
227
228/// A member of `SwiftUI.AccessibilityActionKind`.
229///
230/// Three of these are platform-specific, and asking for one where it does not
231/// exist is not a missing symbol — the symbol is there on every platform and
232/// traps with "Unavailable code reached" when called. Availability is in the
233/// SDK's declarations, not in the export trie, so it is written out here.
234#[derive(Clone, Copy, Debug, PartialEq, Eq)]
235pub enum ActionKind {
236    /// Every platform.
237    Default,
238    /// Every platform.
239    Escape,
240    /// iOS, tvOS and watchOS only.
241    /// Unavailable on macOS: SwiftUI ships a trap stub there, so
242    /// reaching this aborts with "Unavailable code reached".
243    #[cfg(not(target_os = "macos"))]
244    MagicTap,
245    /// macOS only.
246    ShowMenu,
247    /// macOS only.
248    Delete,
249}
250
251impl ActionKind {
252    pub(crate) fn getter(self) -> &'static core::ffi::CStr {
253        match self {
254            Self::Default => c"$s7SwiftUI23AccessibilityActionKindV7defaultACvgZ",
255            Self::Escape => c"$s7SwiftUI23AccessibilityActionKindV6escapeACvgZ",
256            #[cfg(not(target_os = "macos"))]
257            Self::MagicTap => c"$s7SwiftUI23AccessibilityActionKindV8magicTapACvgZ",
258            Self::ShowMenu => c"$s7SwiftUI23AccessibilityActionKindV8showMenuACvgZ",
259            Self::Delete => c"$s7SwiftUI23AccessibilityActionKindV6deleteACvgZ",
260        }
261    }
262}
263
264swift_statics! {
265    /// A member of `SwiftUI.SubmitTriggers`.
266    pub enum SubmitTrigger {
267        Text = c"$s7SwiftUI14SubmitTriggersV4textACvgZ",
268        Search = c"$s7SwiftUI14SubmitTriggersV6searchACvgZ",
269        Collection = c"$s7SwiftUI14SubmitTriggersV10collectionACvgZ",
270    }
271}
272
273swift_statics! {
274    /// A member of `SwiftUI.EventModifiers`.
275    pub enum EventModifier {
276        Shift = c"$s7SwiftUI14EventModifiersV5shiftACvgZ",
277        Control = c"$s7SwiftUI14EventModifiersV7controlACvgZ",
278        Option = c"$s7SwiftUI14EventModifiersV6optionACvgZ",
279        Command = c"$s7SwiftUI14EventModifiersV7commandACvgZ",
280        CapsLock = c"$s7SwiftUI14EventModifiersV8capsLockACvgZ",
281        Function = c"$s7SwiftUI14EventModifiersV8functionACvgZ",
282        NumericPad = c"$s7SwiftUI14EventModifiersV10numericPadACvgZ",
283        All = c"$s7SwiftUI14EventModifiersV3allACvgZ",
284    }
285}
286
287/// Apply `.accessibilityAction(_:_:)`.
288pub fn accessibility_action(view: &ViewHandle, kind: ActionKind, action: impl Fn() + 'static) -> ViewHandle {
289    unsafe {
290        let meta = abi::metadata0(resolve::a11y_action_kind_ma());
291        let k = resilient_static(meta, kind.getter());
292        let c = action_words(action);
293        a11y_modifier(resolve::a11y_action_fn(), view, &[k.as_ptr() as u64, c[0], c[1]], [0.0; 4])
294    }
295}
296
297/// Apply `.accessibilityAdjustableAction(_:)`.
298///
299/// The direction reaches the callback as a case number: 0 is `.increment`,
300/// 1 is `.decrement`.
301pub fn accessibility_adjustable_action(view: &ViewHandle, action: impl Fn(i64) + 'static) -> ViewHandle {
302    unsafe {
303        let meta = abi::metadata0(resolve::a11y_adjustment_direction_ma());
304        // Resilient, so the case arrives by address even though it is one byte.
305        let c = tag_words(action, meta, true);
306        a11y_modifier(resolve::a11y_adjustable_action_fn(), view, &c, [0.0; 4])
307    }
308}
309
310/// Apply `.accessibilityScrollAction(_:)`.
311///
312/// The edge reaches the callback as a case number, in `Edge`'s declared order:
313/// top, leading, bottom, trailing.
314pub fn accessibility_scroll_action(view: &ViewHandle, action: impl Fn(i64) + 'static) -> ViewHandle {
315    unsafe {
316        let meta = abi::metadata0(resolve::edge_enum_ma());
317        // `Edge` is frozen, so the case travels in the register itself.
318        let c = tag_words(action, meta, false);
319        a11y_modifier(resolve::a11y_scroll_action_fn(), view, &c, [0.0; 4])
320    }
321}
322
323/// Apply `.renameAction(_:)`.
324pub fn rename_action(view: &ViewHandle, action: impl Fn() + 'static) -> ViewHandle {
325    unsafe {
326        opaque_modifier(
327            resolve::rename_action_fn(),
328            resolve::rename_action_oret(),
329            view,
330            &action_words(action),
331            [0.0; 4],
332        )
333    }
334}
335
336/// Apply `.onSubmit(of:_:)`.
337pub fn on_submit(view: &ViewHandle, trigger: SubmitTrigger, action: impl Fn() + 'static) -> ViewHandle {
338    unsafe {
339        let meta = abi::metadata0(resolve::submit_triggers_ma());
340        let t = resilient_static(meta, trigger.getter());
341        let c = action_words(action);
342        opaque_modifier(
343            resolve::on_submit_fn(),
344            resolve::on_submit_oret(),
345            view,
346            &[t.as_ptr() as u64, c[0], c[1]],
347            [0.0; 4],
348        )
349    }
350}
351
352/// Apply `.onDeleteCommand(perform:)`.
353pub fn on_delete_command(view: &ViewHandle, action: impl Fn() + 'static) -> ViewHandle {
354    unsafe {
355        opaque_modifier(
356            resolve::on_delete_command_fn(),
357            resolve::on_delete_command_oret(),
358            view,
359            &action_words(action),
360            [0.0; 4],
361        )
362    }
363}
364
365/// Apply `.onExitCommand(perform:)`.
366pub fn on_exit_command(view: &ViewHandle, action: impl Fn() + 'static) -> ViewHandle {
367    unsafe {
368        opaque_modifier(
369            resolve::on_exit_command_fn(),
370            resolve::on_exit_command_oret(),
371            view,
372            &action_words(action),
373            [0.0; 4],
374        )
375    }
376}
377
378/// Apply `.onMoveCommand(perform:)`.
379///
380/// The direction arrives as a case number: up, down, left, right.
381pub fn on_move_command(view: &ViewHandle, action: impl Fn(i64) + 'static) -> ViewHandle {
382    unsafe {
383        let meta = abi::metadata0(resolve::move_command_direction_ma());
384        let c = tag_words(action, meta, true);
385        opaque_modifier(
386            resolve::on_move_command_fn(),
387            resolve::on_move_command_oret(),
388            view,
389            &c,
390            [0.0; 4],
391        )
392    }
393}
394
395/// Apply `.onInteractiveResizeChange(_:)`.
396pub fn on_interactive_resize_change(view: &ViewHandle, action: impl Fn(bool) + 'static) -> ViewHandle {
397    unsafe {
398        opaque_modifier(
399            resolve::on_interactive_resize_fn(),
400            resolve::on_interactive_resize_oret(),
401            view,
402            &bool_words(action),
403            [0.0; 4],
404        )
405    }
406}
407
408/// Apply `.onScrollVisibilityChange(threshold:_:)`.
409pub fn on_scroll_visibility_change(
410    view: &ViewHandle,
411    threshold: f64,
412    action: impl Fn(bool) + 'static,
413) -> ViewHandle {
414    unsafe {
415        opaque_modifier(
416            resolve::on_scroll_visibility_fn(),
417            resolve::on_scroll_visibility_oret(),
418            view,
419            &bool_words(action),
420            [threshold, 0.0, 0.0, 0.0],
421        )
422    }
423}
424
425/// Apply `.onOpenURL(perform:)`.
426pub fn on_open_url(view: &ViewHandle, action: impl Fn(&str) + 'static) -> ViewHandle {
427    unsafe {
428        opaque_modifier(
429            resolve::on_open_url_fn(),
430            resolve::on_open_url_oret(),
431            view,
432            &url_words(action),
433            [0.0; 4],
434        )
435    }
436}
437
438/// Apply `.onScrollPhaseChange(_:)`.
439///
440/// Both phases arrive as case numbers, in `ScrollPhase`'s declared order:
441/// idle, tracking, interacting, decelerating, animating.
442pub fn on_scroll_phase_change(view: &ViewHandle, action: impl Fn(i64, i64) + 'static) -> ViewHandle {
443    unsafe {
444        let meta = abi::metadata0(resolve::scroll_phase_ma());
445        let cb = Box::leak(Box::new(TagPairCallback { f: Box::new(action), meta }));
446        let c = pair_words(invoke_tag_pair, cb as *mut TagPairCallback as *mut c_void);
447        opaque_modifier(
448            resolve::on_scroll_phase_change_fn(),
449            resolve::on_scroll_phase_change_oret(),
450            view,
451            &c,
452            [0.0; 4],
453        )
454    }
455}
456
457/// A `SwiftUI.Transaction` being handed to a `.transaction` callback.
458///
459/// Only for the duration of the call: the pointer is the one SwiftUI passed
460/// `inout`, and writing through it after the callback returns writes to a dead
461/// stack slot.
462#[derive(Clone, Copy, Debug)]
463pub struct TransactionRef(*mut c_void);
464
465impl TransactionRef {
466    /// Set `disablesAnimations`.
467    pub fn set_disables_animations(self, value: bool) {
468        unsafe {
469            abi::call_out(
470                resolve::transaction_disables_animations_set(),
471                abi::ints(&[u64::from(value)]),
472                [0.0; 4],
473                self.0 as u64,
474                core::ptr::null_mut(),
475            );
476        }
477    }
478}
479
480pub(crate) unsafe extern "C" fn invoke_transaction(a0: u64, _a1: u64, _a2: u64, payload: *mut c_void) {
481    let f = unsafe { boxed_ref::<dyn Fn(TransactionRef)>(payload) };
482    f(TransactionRef(a0 as *mut c_void));
483}
484
485/// Apply `.transaction(_:)`.
486///
487/// The transaction is `inout`, so it arrives by address whether or not it would
488/// otherwise be loadable, and the callback can write to it in place.
489pub fn transaction(view: &ViewHandle, action: impl Fn(TransactionRef) + 'static) -> ViewHandle {
490    unsafe {
491        let c = wide_words(invoke_transaction, boxed_ctx(Box::new(action) as Box<dyn Fn(TransactionRef)>));
492        opaque_modifier(resolve::transaction_fn(), resolve::transaction_oret(), view, &c, [0.0; 4])
493    }
494}
495
496/// Apply `.accessibilityChildren(children:)`.
497pub fn accessibility_children(view: &ViewHandle, children: &ViewHandle) -> ViewHandle {
498    unsafe {
499        content_modifier(
500            resolve::a11y_children_fn(),
501            resolve::a11y_children_oret(),
502            view,
503            &[],
504            children,
505        )
506    }
507}
508
509/// Apply `.accessibilityRepresentation(representation:)`.
510pub fn accessibility_representation(view: &ViewHandle, representation: &ViewHandle) -> ViewHandle {
511    unsafe {
512        content_modifier(
513            resolve::a11y_representation_fn(),
514            resolve::a11y_representation_oret(),
515            view,
516            &[],
517            representation,
518        )
519    }
520}
521
522swift_statics! {
523    /// A member of `SwiftUI.AccessibilityActionCategory`.
524    pub enum ActionCategory {
525        Default = c"$s7SwiftUI27AccessibilityActionCategoryV7defaultACvgZ",
526        Edit = c"$s7SwiftUI27AccessibilityActionCategoryV4editACvgZ",
527    }
528}
529
530/// Apply `.accessibilityActions(category:_:)`.
531pub fn accessibility_actions(
532    view: &ViewHandle,
533    category: ActionCategory,
534    content: &ViewHandle,
535) -> ViewHandle {
536    unsafe {
537        let meta = abi::metadata0(resolve::a11y_action_category_ma());
538        let c = resilient_static(meta, category.getter());
539        content_modifier(
540            resolve::a11y_actions_fn(),
541            resolve::a11y_actions_oret(),
542            view,
543            &[c.as_ptr() as u64],
544            content,
545        )
546    }
547}
548
549/// Apply `.toolbarTitleMenu(content:)`.
550pub fn toolbar_title_menu(view: &ViewHandle, content: &ViewHandle) -> ViewHandle {
551    unsafe {
552        content_modifier(
553            resolve::toolbar_title_menu_fn(),
554            resolve::toolbar_title_menu_oret(),
555            view,
556            &[],
557            content,
558        )
559    }
560}
561
562/// Apply `.sectionActions(content:)`.
563pub fn section_actions(view: &ViewHandle, content: &ViewHandle) -> ViewHandle {
564    unsafe {
565        content_modifier(
566            resolve::section_actions_fn(),
567            resolve::section_actions_oret(),
568            view,
569            &[],
570            content,
571        )
572    }
573}
574
575/// Apply `.tabItem(_:)`.
576pub fn tab_item(view: &ViewHandle, label: &ViewHandle) -> ViewHandle {
577    unsafe {
578        content_modifier(resolve::tab_item_fn(), resolve::tab_item_oret(), view, &[], label)
579    }
580}
581
582#[cfg(test)]
583mod closure_tests {
584    use super::*;
585    use std::sync::atomic::{AtomicI64, Ordering};
586
587    /// Call a closure the way Swift does: argument in x0, context in x20.
588    ///
589    /// Constructing a modifier proves only that the closure's two words were
590    /// accepted. Whether the callback runs, and whether it reads the argument
591    /// out of the right register, is not visible in the view's type or in a
592    /// dump of its value — so it is checked here directly.
593    unsafe fn call_closure(words: [u64; 2], arg: u64) {
594        unsafe {
595            core::arch::asm!("blr {f}", f = in(reg) words[0],
596                in("x0") arg, in("x20") words[1],
597                lateout("x1") _, lateout("x2") _, lateout("x3") _, lateout("x4") _,
598                lateout("x5") _, lateout("x6") _, lateout("x7") _, lateout("x8") _,
599                lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
600                lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
601                lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
602            );
603        }
604    }
605
606    static SEEN: AtomicI64 = AtomicI64::new(-1);
607
608    fn record(v: i64) {
609        SEEN.store(v, Ordering::SeqCst);
610    }
611
612    #[test]
613    fn frozen_enum_case_arrives_in_the_register() {
614        unsafe {
615            // `Edge` is frozen and one byte, so the case number *is* x0.
616            let meta = abi::metadata0(resolve::edge_enum_ma());
617            let words = tag_words(record, meta, false);
618            for case in 0..4u64 {
619                SEEN.store(-1, Ordering::SeqCst);
620                call_closure(words, case);
621                assert_eq!(SEEN.load(Ordering::SeqCst), case as i64, "Edge case {case}");
622            }
623        }
624    }
625
626    #[test]
627    fn resilient_enum_case_arrives_by_address() {
628        unsafe {
629            // `MoveCommandDirection` is the same one byte but not frozen, so
630            // x0 holds a pointer to it rather than the case itself.
631            let meta = abi::metadata0(resolve::move_command_direction_ma());
632            let words = tag_words(record, meta, true);
633            for case in 0..4u32 {
634                let mut buf = vec![0u8; abi::type_bytes(meta)];
635                abi::inject_enum_tag(&mut buf, meta, case);
636                SEEN.store(-1, Ordering::SeqCst);
637                call_closure(words, buf.as_ptr() as u64);
638                assert_eq!(SEEN.load(Ordering::SeqCst), i64::from(case), "direction {case}");
639            }
640        }
641    }
642
643    fn record_bool(v: bool) {
644        SEEN.store(i64::from(v), Ordering::SeqCst);
645    }
646
647    #[test]
648    fn bool_closure_reads_the_low_bit() {
649        let words = bool_words(record_bool);
650        unsafe {
651            SEEN.store(-1, Ordering::SeqCst);
652            call_closure(words, 1);
653            assert_eq!(SEEN.load(Ordering::SeqCst), 1);
654            call_closure(words, 0);
655            assert_eq!(SEEN.load(Ordering::SeqCst), 0);
656        }
657    }
658
659    /// A round trip through the enum witnesses: inject a case, read it back.
660    #[test]
661    fn enum_tag_round_trips() {
662        unsafe {
663            let meta = abi::metadata0(resolve::scroll_phase_ma());
664            let mut buf = vec![0u8; abi::type_bytes(meta)];
665            for case in 0..5u32 {
666                abi::inject_enum_tag(&mut buf, meta, case);
667                assert_eq!(abi::read_enum_tag(buf.as_ptr(), meta), case);
668            }
669        }
670    }
671
672    /// The context must survive the call that installed it, so the words a
673    /// closure is built from have to point at something leaked rather than at
674    /// a temporary.
675    #[test]
676    fn closure_context_outlives_construction() {
677        let words = {
678            let view = text("gone by now");
679            view_words(&view)
680        };
681        assert_ne!(words[0], 0);
682        assert_ne!(words[1], 0);
683        let mut out = 0u64;
684        unsafe {
685            core::arch::asm!("blr {f}", f = in(reg) words[0],
686                in("x8") &mut out as *mut u64, in("x20") words[1],
687                lateout("x0") _, lateout("x1") _, lateout("x2") _, lateout("x3") _,
688                lateout("x4") _, lateout("x5") _, lateout("x6") _, lateout("x7") _,
689                lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
690                lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
691                lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
692            );
693        }
694        assert_ne!(out, 0, "the ViewBuilder closure handed back nothing");
695    }
696}
697
698/// The two words of a `(V) -> ()` closure that ignores its argument.
699///
700/// SwiftUI hands these callbacks a `DragSession`, a `PencilSqueezeGesturePhase`
701/// and the like — values this crate has no reader for yet. The callback still
702/// fires; only the argument is dropped, and the wide thunk forwards x0 through
703/// x2 so it does not matter whether the value came in registers or by address.
704pub(crate) fn ignoring_value_words(action: impl Fn() + 'static) -> [u64; 2] {
705    wide_words(invoke_ignoring_value, boxed_ctx(Box::new(action) as Box<dyn Fn()>))
706}
707
708pub(crate) unsafe extern "C" fn invoke_ignoring_value(
709    _a0: u64,
710    _a1: u64,
711    _a2: u64,
712    payload: *mut c_void,
713) {
714    let f = unsafe { boxed_ref::<dyn Fn()>(payload) };
715    f();
716}
717
718/// Apply `.accessibilityZoomAction(_:)`.
719pub fn accessibility_zoom_action(view: &ViewHandle, action: impl Fn() + 'static) -> ViewHandle {
720    unsafe { a11y_modifier(resolve::a11y_zoom_action_fn(), view, &ignoring_value_words(action), [0.0; 4]) }
721}
722
723/// Apply `.onContinueUserActivity(_:perform:)`.
724pub fn on_continue_user_activity(view: &ViewHandle, kind: &str, action: impl Fn() + 'static) -> ViewHandle {
725    unsafe {
726        let s = abi::swift_string(kind);
727        let c = ignoring_value_words(action);
728        opaque_modifier(
729            resolve::on_continue_user_activity_fn(),
730            resolve::on_continue_user_activity_oret(),
731            view,
732            &[
733                u64::from_le_bytes(s[..8].try_into().expect("lo")),
734                u64::from_le_bytes(s[8..].try_into().expect("hi")),
735                c[0],
736                c[1],
737            ],
738            [0.0; 4],
739        )
740    }
741}
742
743/// Apply `.presentationBackground(alignment:content:)`.
744pub fn presentation_background(
745    view: &ViewHandle,
746    alignment: Alignment,
747    content: &ViewHandle,
748) -> ViewHandle {
749    unsafe {
750        let a = alignment.words();
751        content_modifier(
752            resolve::presentation_background_fn(),
753            resolve::presentation_background_oret(),
754            view,
755            &[a[0], a[1]],
756            content,
757        )
758    }
759}
760
761swift_statics! {
762    /// A content type, as `UTType`'s own static properties name them.
763    pub enum ContentType {
764        PlainText = c"$s22UniformTypeIdentifiers6UTTypeV9plainTextACvgZ",
765        Text = c"$s22UniformTypeIdentifiers6UTTypeV4textACvgZ",
766        Url = c"$s22UniformTypeIdentifiers6UTTypeV3urlACvgZ",
767    }
768}
769
770/// The two words of a closure returning a single word.
771pub(crate) fn word_closure(
772    body: unsafe extern "C" fn(*mut c_void, u64) -> u64,
773    payload: *mut c_void,
774) -> [u64; 2] {
775    let ctx = Box::leak(Box::new(abi::WordClosureCtx::new(body, payload)))
776        as *mut abi::WordClosureCtx;
777    [abi::word_closure_thunk as *const () as u64, ctx as u64]
778}
779
780unsafe impl Send for ItemProvider {}
781unsafe impl Sync for ItemProvider {}
782
783/// A Swift `[NSItemProvider]` built from Rust.
784pub(crate) unsafe fn provider_array(items: &[&str]) -> u64 {
785    unsafe {
786        let meta = crate::type_by_name!(swift_type!(objc NSItemProvider).as_bytes());
787        let stride = abi::type_bytes(meta);
788        let mut bytes = vec![0u8; stride * items.len()];
789        for (i, t) in items.iter().enumerate() {
790            let p = ItemProvider::plain_text(t);
791            bytes[i * stride..i * stride + 8].copy_from_slice(&(p.0 as u64).to_le_bytes());
792        }
793        abi::swift_array(meta, items.len(), &bytes)
794    }
795}
796
797/// Whether a drop should be accepted — `onDrop`'s `isTargeted`-style predicate.
798pub(crate) type DropDecide = Box<dyn Fn(&[ItemProvider]) -> bool>;
799
800/// What a drop handler is told, with no answer expected.
801pub(crate) type DropObserve = Box<dyn Fn(&[ItemProvider])>;
802
803/// What an item-provider closure carries.
804pub(crate) struct ProviderPayload {
805    array: u64,
806    decide: Option<DropDecide>,
807    observe: Option<DropObserve>,
808}
809
810pub(crate) unsafe extern "C" fn emit_providers(payload: *mut c_void, _arg: u64) -> u64 {
811    unsafe {
812        let p = &*(payload as *const ProviderPayload);
813        swift_runtime_sys::RuntimeRaw::swift_retain(p.array as *mut c_void);
814        p.array
815    }
816}
817
818pub(crate) unsafe extern "C" fn emit_first_provider(payload: *mut c_void, _arg: u64) -> u64 {
819    unsafe {
820        let p = &*(payload as *const ProviderPayload);
821        let items = read_provider_array(p.array);
822        items.first().map_or(0, |i| i.0 as u64)
823    }
824}
825
826pub(crate) unsafe extern "C" fn decide_on_providers(payload: *mut c_void, arg: u64) -> u64 {
827    unsafe {
828        let p = &*(payload as *const ProviderPayload);
829        let items = read_provider_array(arg);
830        u64::from(p.decide.as_ref().map(|f| f(&items)).unwrap_or(false))
831    }
832}
833
834pub(crate) unsafe extern "C" fn observe_providers(
835    a0: u64,
836    _a1: u64,
837    _a2: u64,
838    payload: *mut c_void,
839) {
840    unsafe {
841        let p = &*(payload as *const ProviderPayload);
842        if let Some(f) = p.observe.as_ref() {
843            f(&read_provider_array(a0));
844        }
845    }
846}
847
848pub(crate) fn provider_payload(
849    array: u64,
850    decide: Option<DropDecide>,
851    observe: Option<DropObserve>,
852) -> *mut c_void {
853    Box::leak(Box::new(ProviderPayload { array, decide, observe })) as *mut ProviderPayload
854        as *mut c_void
855}
856
857swift_statics! {
858    /// Where a container background sits.
859    /// Where a `.containerBackground` is drawn.
860    ///
861    /// **`Navigation` and `NavigationSplitView` are unavailable on macOS** —
862    /// SwiftUI ships trap stubs for them there, so reaching one aborts with
863    /// "Unavailable code reached" rather than failing to resolve. `Window` and
864    /// `Presentation` work on every platform. Measured, not assumed: each case
865    /// was called on macOS and the two named here are the ones that trap.
866    pub enum ContainerBackgroundPlacement {
867        Window = c"$s7SwiftUI28ContainerBackgroundPlacementV6windowACvgZ",
868        /// Unavailable on macOS: SwiftUI ships a trap stub there, so
869        /// reaching this aborts with "Unavailable code reached".
870        #[cfg(not(target_os = "macos"))]
871        Navigation = c"$s7SwiftUI28ContainerBackgroundPlacementV10navigationACvgZ",
872        Presentation = c"$s7SwiftUI28ContainerBackgroundPlacementV12presentationACvgZ",
873        /// Unavailable on macOS: SwiftUI ships a trap stub there, so
874        /// reaching this aborts with "Unavailable code reached".
875        #[cfg(not(target_os = "macos"))]
876        NavigationSplitView = c"$s7SwiftUI28ContainerBackgroundPlacementV19navigationSplitViewACvgZ",
877    }
878}
879
880swift_cases! {
881    /// `SwiftUI.VerticalEdge`.
882    pub enum VerticalEdge {
883        Top = 0,
884        Bottom = 1,
885    }
886}
887
888/// Apply `.containerBackground(for:alignment:content:)`.
889pub fn container_background(
890    view: &ViewHandle,
891    placement: ContainerBackgroundPlacement,
892    alignment: Alignment,
893    content: &ViewHandle,
894) -> ViewHandle {
895    unsafe {
896        let meta = abi::metadata0(resolve::container_background_placement_ma());
897        let p = resilient_static(meta, placement.getter());
898        let a = alignment.words();
899        content_modifier(
900            resolve::container_background_fn(),
901            resolve::container_background_oret(),
902            view,
903            &[p.as_ptr() as u64, a[0], a[1]],
904            content,
905        )
906    }
907}
908
909/// The edge, alignment and spacing the two safe-area modifiers share.
910unsafe fn safe_area_args(
911    edge: VerticalEdge,
912    alignment: HorizontalAlignment,
913    spacing: Option<f64>,
914) -> Vec<u64> {
915    unsafe {
916        let e = abi::enum_argument(abi::metadata0(resolve::vertical_edge_ma()), edge.tag());
917        let a = frozen_static(abi::metadata0(resolve::horizontal_alignment_ma()), alignment.getter());
918        let mut ints = vec![e, a];
919        ints.extend(opt_f64_words(spacing));
920        ints
921    }
922}
923
924/// Apply `.safeAreaInset(edge:alignment:spacing:content:)`.
925pub fn safe_area_inset(
926    view: &ViewHandle,
927    edge: VerticalEdge,
928    alignment: HorizontalAlignment,
929    spacing: Option<f64>,
930    content: &ViewHandle,
931) -> ViewHandle {
932    unsafe {
933        let args = safe_area_args(edge, alignment, spacing);
934        content_modifier(
935            resolve::safe_area_inset_fn(),
936            resolve::safe_area_inset_oret(),
937            view,
938            &args,
939            content,
940        )
941    }
942}
943
944/// Apply `.safeAreaBar(edge:alignment:spacing:content:)`.
945pub fn safe_area_bar(
946    view: &ViewHandle,
947    edge: VerticalEdge,
948    alignment: HorizontalAlignment,
949    spacing: Option<f64>,
950    content: &ViewHandle,
951) -> ViewHandle {
952    unsafe {
953        let args = safe_area_args(edge, alignment, spacing);
954        content_modifier(
955            resolve::safe_area_bar_fn(),
956            resolve::safe_area_bar_oret(),
957            view,
958            &args,
959            content,
960        )
961    }
962}
963
964/// Apply `.userActivity(_:isActive:_:)`.
965pub fn user_activity(view: &ViewHandle, kind: &str, active: bool, action: impl Fn() + 'static) -> ViewHandle {
966    unsafe {
967        let s = abi::swift_string(kind);
968        let c = ignoring_value_words(action);
969        opaque_modifier(
970            resolve::user_activity_fn(),
971            resolve::user_activity_oret(),
972            view,
973            &[
974                u64::from_le_bytes(s[..8].try_into().expect("lo")),
975                u64::from_le_bytes(s[8..].try_into().expect("hi")),
976                u64::from(active),
977                c[0],
978                c[1],
979            ],
980            [0.0; 4],
981        )
982    }
983}
984
985swift_statics! {
986    /// A haptic or audible cue.
987    pub enum SensoryFeedback {
988        Success = c"$s7SwiftUI15SensoryFeedbackV7successACvgZ",
989        Warning = c"$s7SwiftUI15SensoryFeedbackV7warningACvgZ",
990        Error = c"$s7SwiftUI15SensoryFeedbackV5errorACvgZ",
991        Selection = c"$s7SwiftUI15SensoryFeedbackV9selectionACvgZ",
992        Impact = c"$s7SwiftUI15SensoryFeedbackV6impactACvgZ",
993        Alignment = c"$s7SwiftUI15SensoryFeedbackV9alignmentACvgZ",
994        Decrease = c"$s7SwiftUI15SensoryFeedbackV8decreaseACvgZ",
995        Increase = c"$s7SwiftUI15SensoryFeedbackV8increaseACvgZ",
996        LevelChange = c"$s7SwiftUI15SensoryFeedbackV11levelChangeACvgZ",
997        PathComplete = c"$s7SwiftUI15SensoryFeedbackV12pathCompleteACvgZ",
998        Start = c"$s7SwiftUI15SensoryFeedbackV5startACvgZ",
999        Stop = c"$s7SwiftUI15SensoryFeedbackV4stopACvgZ",
1000    }
1001}
1002
1003/// A byte buffer as the argument registers it occupies.
1004pub(crate) fn words(bytes: &[u8]) -> Vec<u64> {
1005    bytes
1006        .chunks(8)
1007        .map(|c| {
1008            let mut w = [0u8; 8];
1009            w[..c.len()].copy_from_slice(c);
1010            u64::from_le_bytes(w)
1011        })
1012        .collect()
1013}
1014
1015swift_statics! {
1016    /// Where a search field is placed in the surrounding chrome.
1017    ///
1018    /// A struct rather than an enum, so each case is a static getter returning a
1019    /// value small enough to travel in a register.
1020    pub enum SearchFieldPlacement {
1021        Automatic = c"$s7SwiftUI20SearchFieldPlacementV9automaticACvgZ",
1022        ToolbarPrincipal = c"$s7SwiftUI20SearchFieldPlacementV16toolbarPrincipalACvgZ",
1023        NavigationColumn = c"$s7SwiftUI20SearchFieldPlacementV16navigationColumnACvgZ",
1024        /// Unavailable on macOS: SwiftUI ships a trap stub there, so
1025        /// reaching this aborts with "Unavailable code reached".
1026        #[cfg(not(target_os = "macos"))]
1027        NavigationBarDrawer = c"$s7SwiftUI20SearchFieldPlacementV19navigationBarDrawerACvgZ",
1028        Sidebar = c"$s7SwiftUI20SearchFieldPlacementV7sidebarACvgZ",
1029        Toolbar = c"$s7SwiftUI20SearchFieldPlacementV7toolbarACvgZ",
1030    }
1031}
1032
1033/// Apply a modifier with *two* generic `View` parameters beyond `Self`.
1034///
1035/// Distinct from `generics::generic2_modifier_ints`, which is one generic
1036/// parameter carrying two conformances.
1037///
1038/// The tail is every metadata argument in generic-parameter order and then every
1039/// witness table in requirement order — not the two interleaved, which is what
1040/// repeating [`generic1_modifier_ints`] would give.
1041pub(crate) unsafe fn two_view_modifier(
1042    func: *const c_void,
1043    oret: *const c_void,
1044    view: &ViewHandle,
1045    leading: &[u64],
1046    a: &ViewHandle,
1047    b: &ViewHandle,
1048) -> ViewHandle {
1049    unsafe {
1050        // `Self`, then the two content views — which are passed as `AnyView`
1051        // words, so only the first of each triple is the receiver's.
1052        // Three distinct types: the receiver, and each of the two contents,
1053        // which `view_words` now hands back at its own type rather than boxed.
1054        let subst = [view.meta, a.meta, b.meta, view.wt, a.wt, b.wt];
1055        let (meta, wt, size) = opaque_result_type(oret, &subst);
1056        let mut ints = leading.to_vec();
1057        ints.extend_from_slice(&view_words(a));
1058        ints.extend_from_slice(&view_words(b));
1059        ints.extend_from_slice(&[view.meta as u64, a.meta as u64, b.meta as u64]);
1060        ints.extend_from_slice(&[view.wt as u64, a.wt as u64, b.wt as u64]);
1061        let r = abi::call_modifier_generic(func, view.value_ptr(), &ints, [0.0; 4], size);
1062        wrap_owned(r, meta, wt)
1063    }
1064}
1065
1066swift_statics! {
1067    /// `SwiftUI.SearchSuggestionsPlacement.Set`, an option set.
1068    pub enum SearchSuggestionsPlacementSet {
1069        Menu = c"$s7SwiftUI26SearchSuggestionsPlacementV3SetV4menuAEvgZ",
1070        Content = c"$s7SwiftUI26SearchSuggestionsPlacementV3SetV7contentAEvgZ",
1071    }
1072}
1073
1074swift_statics! {
1075    /// `SwiftUI.TextInputFormattingControlPlacement.Set`, an option set.
1076    pub enum TextInputFormattingControlPlacementSet {
1077        All = c"$s7SwiftUI35TextInputFormattingControlPlacementV3SetV3allAEvgZ",
1078        Default = c"$s7SwiftUI35TextInputFormattingControlPlacementV3SetV7defaultAEvgZ",
1079        ContextMenu = c"$s7SwiftUI35TextInputFormattingControlPlacementV3SetV11contextMenuAEvgZ",
1080        AccessoryBar = c"$s7SwiftUI35TextInputFormattingControlPlacementV3SetV12accessoryBarAEvgZ",
1081        /// Unavailable on macOS: SwiftUI ships a trap stub there, so
1082        /// reaching this aborts with "Unavailable code reached".
1083        #[cfg(not(target_os = "macos"))]
1084        InputAssistant = c"$s7SwiftUI35TextInputFormattingControlPlacementV3SetV14inputAssistantAEvgZ",
1085        FontPanel = c"$s7SwiftUI35TextInputFormattingControlPlacementV3SetV9fontPanelAEvgZ",
1086    }
1087}
1088
1089swift_statics! {
1090    /// A Foundation `CharacterSet`, by the static that names it.
1091    ///
1092    /// `Whitespaces` and `Decimals` are the ones the parity suite compares.
1093    /// `.alphanumerics` is bound and correct, but it cannot be compared *across
1094    /// processes*: which concrete class backs it — a bitmap `NSCharacterSet` or
1095    /// a CF-bridged `__NSCFCharacterSet` — depends on what else in the process
1096    /// has touched Foundation, and `dump` prints the two differently. That is a
1097    /// property of Foundation's caching, not of the call.
1098    pub enum CharacterSetKind {
1099        Alphanumerics = c"$s10Foundation12CharacterSetV13alphanumericsACvgZ",
1100        Letters = c"$s10Foundation12CharacterSetV7lettersACvgZ",
1101        Decimals = c"$s10Foundation12CharacterSetV13decimalDigitsACvgZ",
1102        Whitespaces = c"$s10Foundation12CharacterSetV11whitespacesACvgZ",
1103    }
1104}
1105
1106swift_statics! {
1107    /// Which phases of a key press to observe.
1108    pub enum KeyPressPhases {
1109        Down = c"$s7SwiftUI8KeyPressV6PhasesV4downAEvgZ",
1110        Up = c"$s7SwiftUI8KeyPressV6PhasesV2upAEvgZ",
1111        Repeat = c"$s7SwiftUI8KeyPressV6PhasesV6repeatAEvgZ",
1112        All = c"$s7SwiftUI8KeyPressV6PhasesV3allAEvgZ",
1113    }
1114}
1115
1116/// Apply `.navigationTransition(_:)` with the automatic transition.
1117///
1118/// The static is declared on the *protocol*, in an extension constrained to
1119/// `Self == AutomaticNavigationTransition`, so reading it means naming that
1120/// concrete type — and the conformance comes from a runtime lookup rather than
1121/// an exported witness-table symbol.
1122pub fn navigation_transition(view: &ViewHandle) -> ViewHandle {
1123    unsafe {
1124        let meta = abi::metadata0(resolve::automatic_navigation_transition_ma());
1125        let wt = abi::get_view_wt_for_proto(meta, resolve::navigation_transition_proto());
1126        assert!(!wt.is_null(), "AutomaticNavigationTransition: NavigationTransition");
1127        let value = abi::static_out(
1128            resolve::navigation_transition_automatic(), meta, abi::type_bytes(meta));
1129        generic1_modifier_ints(
1130            resolve::navigation_transition_fn(),
1131            resolve::navigation_transition_oret(),
1132            view,
1133            meta,
1134            wt,
1135            &[value.as_ptr() as u64],
1136            [0.0; 4],
1137        )
1138    }
1139}
1140
1141swift_statics! {
1142    /// A line height, as `AttributedString.LineHeight`'s statics name it.
1143    ///
1144    /// The initialiser taking an `NSParagraphStyle` is SwiftUI-internal, so the
1145    /// statics are the public way in — and `exact(points:)` and
1146    /// `multiple(factor:)` are the only two that take an argument.
1147    pub enum LineHeightKind {
1148        Variable = c"$s10Foundation16AttributedStringV8CoreTextE10LineHeightV8variableAFvgZ",
1149        Normal = c"$s10Foundation16AttributedStringV8CoreTextE10LineHeightV6normalAFvgZ",
1150        Tight = c"$s10Foundation16AttributedStringV8CoreTextE10LineHeightV5tightAFvgZ",
1151        Loose = c"$s10Foundation16AttributedStringV8CoreTextE10LineHeightV5looseAFvgZ",
1152    }
1153}
1154
1155/// Apply `.lineHeight(_:)`.
1156///
1157/// `AttributedString.LineHeight` is nine bytes and resilient, so its static
1158/// getters write through x8 and the optional wrapping one goes to the modifier
1159/// by address. Passing `None` writes the optional's `.none` tag and leaves the
1160/// payload alone.
1161pub fn line_height(view: &ViewHandle, height: Option<LineHeightKind>) -> ViewHandle {
1162    unsafe {
1163        let payload = abi::metadata0(resolve::line_height_ma());
1164        let opt = abi::resolve_type_by_mangled_name(
1165            b"10Foundation16AttributedStringV8CoreTextE10LineHeightVSg");
1166        assert!(!opt.is_null(), "Optional<AttributedString.LineHeight>");
1167        let mut buf = vec![0u8; abi::type_bytes(opt)];
1168        if let Some(kind) = height {
1169            let v = resilient_static(payload, kind.getter());
1170            buf[..v.len()].copy_from_slice(&v);
1171        }
1172        abi::store_optional_tag(&mut buf, payload, height.is_some());
1173        opaque_modifier(
1174            resolve::line_height_fn(),
1175            resolve::line_height_oret(),
1176            view,
1177            &[buf.as_ptr() as u64],
1178            [0.0; 4],
1179        )
1180    }
1181}
1182
1183/// Apply `.refreshable(action:)`.
1184///
1185/// The action is a Swift `async` closure built by [`crate::asyncfn`]: it runs
1186/// `action` and resumes immediately, never suspending.
1187pub fn refreshable(view: &ViewHandle, action: impl Fn() + Send + Sync + 'static) -> ViewHandle {
1188    unsafe {
1189        static mut RECORD: crate::asyncfn::FunctionPointer =
1190            crate::asyncfn::FunctionPointer::new();
1191        let closure = crate::asyncfn::prepare(
1192            crate::static_mut(&raw mut RECORD),
1193            0,
1194            action,
1195        );
1196        opaque_modifier(
1197            resolve::refreshable_fn(),
1198            resolve::refreshable_oret(),
1199            view,
1200            &closure,
1201            [0.0; 4],
1202        )
1203    }
1204}
1205
1206/// [`generic1_modifier_ints`] for out-of-crate diagnostics.
1207#[doc(hidden)]
1208pub unsafe fn generic1_modifier_ints_for_test(
1209    func: *const c_void,
1210    oret: *const c_void,
1211    view: &ViewHandle,
1212    arg_meta: *const c_void,
1213    arg_wt: *const c_void,
1214    leading: &[u64],
1215) -> ViewHandle {
1216    unsafe { generic1_modifier_ints(func, oret, view, arg_meta, arg_wt, leading, [0.0; 4]) }
1217}
1218
1219swift_statics! {
1220    /// How urgently a `task` should run.
1221    /// `.medium` is deliberately absent. The stdlib marks it
1222    /// `@_alwaysEmitIntoClient`, so unlike its neighbours it has no symbol to
1223    /// call — `dlsym` returns null for `$sScP6mediumScPvgZ` even with
1224    /// `libswift_Concurrency` loaded, and naming it could only panic.
1225    pub enum TaskPriority {
1226        High = c"$sScP4highScPvgZ",
1227        UserInitiated = c"$sScP13userInitiatedScPvgZ",
1228        Utility = c"$sScP7utilityScPvgZ",
1229        Low = c"$sScP3lowScPvgZ",
1230        Background = c"$sScP10backgroundScPvgZ",
1231    }
1232}
1233
1234/// Apply `.task(id:priority:_:)`.
1235pub fn task(
1236    view: &ViewHandle,
1237    id: i64,
1238    priority: TaskPriority,
1239    file: &str,
1240    line: i64,
1241    action: impl Fn() + Send + Sync + 'static,
1242) -> ViewHandle {
1243    unsafe {
1244        static mut RECORD: crate::asyncfn::FunctionPointer =
1245            crate::asyncfn::FunctionPointer::new();
1246        let closure = crate::asyncfn::prepare(
1247            crate::static_mut(&raw mut RECORD),
1248            1,
1249            action,
1250        );
1251        let ctx = Box::leak(Box::new(abi::IsolatedContext::nonisolated()))
1252            as *mut abi::IsolatedContext;
1253        let pm = abi::metadata0(resolve::task_priority_ma());
1254        let p = resilient_static(pm, priority.getter());
1255        let name = abi::swift_string(&format!("View.task @ {file}:{line}"));
1256        let n0 = u64::from_le_bytes(name[..8].try_into().expect("name lo"));
1257        let n1 = u64::from_le_bytes(name[8..].try_into().expect("name hi"));
1258        let meta = resolve::int_meta();
1259        let wt = resolve::int_equatable_wt();
1260        let modifier = abi::generic_metadata2(resolve::task_modifier_ma(), meta, wt);
1261        assert!(!modifier.is_null(), "_TaskValueModifier2<Int> metadata");
1262        let mut value = vec![0u8; abi::type_bytes(modifier)];
1263        let mut boxed = id;
1264        core::arch::asm!(
1265            "sub sp, sp, #16",
1266            "stp {s0}, {s1}, [sp]",
1267            "blr {f}",
1268            "add sp, sp, #16",
1269            f = in(reg) resolve::task_modifier_init(),
1270            s0 = in(reg) meta, s1 = in(reg) wt,
1271            in("x0") &mut boxed as *mut i64,
1272            in("x1") n0, in("x2") n1,
1273            in("x3") 0u64, in("x4") 0u64,
1274            in("x5") p.as_ptr(),
1275            in("x6") closure[0], in("x7") ctx as u64,
1276            in("x8") value.as_mut_ptr(),
1277            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
1278            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
1279            lateout("x17") _, lateout("lr") _, lateout("x20") _, lateout("x21") _,
1280            clobber_abi("C"),
1281        );
1282        apply_modifier(view, modifier, &value)
1283    }
1284}
1285
1286/// Apply `.task { … }` — the spelling with no `id:`.
1287///
1288/// A different modifier from [`task`], not a default-argument version of it:
1289/// that one builds `_TaskValueModifier2<ID>` and re-runs when the id changes,
1290/// this one builds `_TaskModifier2` and runs once. `_TaskModifier2` is not
1291/// generic, so there are no generic words to spill.
1292///
1293/// The `name` carries the call site — SwiftUI records `View.task @ file:line` —
1294/// so a value comparison against a reference only agrees if the caller passes
1295/// the reference's own file and line, which is why they are parameters.
1296pub fn task_once(
1297    view: &ViewHandle,
1298    priority: TaskPriority,
1299    file: &str,
1300    line: i64,
1301    action: impl Fn() + Send + Sync + 'static,
1302) -> ViewHandle {
1303    unsafe {
1304        static mut RECORD: crate::asyncfn::FunctionPointer =
1305            crate::asyncfn::FunctionPointer::new();
1306        let closure = crate::asyncfn::prepare(
1307            crate::static_mut(&raw mut RECORD),
1308            2,
1309            action,
1310        );
1311        let ctx = Box::leak(Box::new(abi::IsolatedContext::nonisolated()))
1312            as *mut abi::IsolatedContext;
1313        let pm = abi::metadata0(resolve::task_priority_ma());
1314        let p = resilient_static(pm, priority.getter());
1315        let name = abi::swift_string(&format!("View.task @ {file}:{line}"));
1316        let n0 = u64::from_le_bytes(name[..8].try_into().expect("name lo"));
1317        let n1 = u64::from_le_bytes(name[8..].try_into().expect("name hi"));
1318
1319        let modifier = abi::metadata0(resolve::task_modifier2_ma());
1320        assert!(!modifier.is_null(), "_TaskModifier2 metadata");
1321        let mut value = vec![0u8; abi::type_bytes(modifier)];
1322        // init(name:executorPreference:priority:action:)
1323        //   x0/x1 = name, x2/x3 = TaskExecutor? existential (nil),
1324        //   x4 = &priority, x5 = action fn, x6 = action ctx, x8 = @out
1325        core::arch::asm!("blr {f}", f = in(reg) resolve::task_modifier2_init(),
1326            in("x0") n0, in("x1") n1,
1327            in("x2") 0u64, in("x3") 0u64,
1328            in("x4") p.as_ptr(),
1329            in("x5") closure[0], in("x6") ctx as u64,
1330            in("x8") value.as_mut_ptr(),
1331            lateout("x7") _, lateout("x9") _, lateout("x10") _, lateout("x11") _,
1332            lateout("x12") _, lateout("x13") _, lateout("x14") _, lateout("x15") _,
1333            lateout("x16") _, lateout("x17") _, lateout("lr") _, lateout("x20") _,
1334            lateout("x21") _, clobber_abi("C"),
1335        );
1336        apply_modifier(view, modifier, &value)
1337    }
1338}