swiftui-native 0.0.1

SwiftUI from pure Rust — no Swift bridge, no shim, only swift-runtime calls
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
//! Interaction, lifecycle and animation.

use super::*;
use crate::abi;
use crate::resolve;
use core::ffi::c_void;
use crate::mangle;

// ── Interaction, lifecycle and animation ──

mangle::modifier_types! {
    /// `.transition(_:)`
    /// `ModifiedContent<AnyView, _TraitWritingModifier<TransitionTraitKey>>`
    pub(crate) TRANSITION_T = ty("_TraitWritingModifier").of(vec![ty("TransitionTraitKey")]);
    /// `.onAppear(perform:)`
    /// `ModifiedContent<AnyView, _AppearanceActionModifier>`
    pub(crate) ON_APPEAR_T = ty("_AppearanceActionModifier");
    /// `.animation(_:value:)` with an `Int` value
    /// `ModifiedContent<AnyView, _AnimationModifier<Int>>`
    pub(crate) ANIMATION_T = ty("_AnimationModifier").of(vec![Type::Verbatim("Si".into())]);
}



/// `.transition(_:)` for an `AnyTransition` already in hand.
///
/// The bindings for SwiftUI's own transitions read a static and pass its word;
/// [`crate::views::custom_transition`] builds its own box and has the same word
/// to pass, so the call is shared rather than written twice.
///
/// # Safety
/// `boxed` must be a live `AnyTransition` — one class reference, `+1`.
pub(crate) unsafe fn opaque_modifier_for_transition(
    view: &ViewHandle,
    boxed: u64,
) -> ViewHandle {
    unsafe {
        let (_, _, size) = TRANSITION_T.applied_to(view.meta);
        let r = abi::call_modifier(
            resolve::transition_fn(),
            view.value_ptr(),
            view.meta,
            view.wt,
            &[boxed],
            [0.0; 4],
            size,
        );
        wrap_modified(r, &TRANSITION_T, view)
    }
}

/// Apply `.transition(.opacity)`.
///
/// `AnyTransition` is eight bytes — a single class reference — so it is
/// *loadable*: its static getters return it in x0 and it is passed by value.
/// Reading it through an indirect (`x8`) return, as the larger resilient types
/// like `Material` and `Glass` require, yields an uninitialised buffer whose
/// contents are then released as if they were an object.
pub fn transition_opacity(view: &ViewHandle) -> ViewHandle {
    unsafe {
        let meta = abi::metadata0(resolve::any_transition_ma());
        let (value, _) = static_getter(resolve::transition_opacity(), meta);
        let (_, _, size) = TRANSITION_T.applied_to(view.meta);
        let r = abi::call_modifier(resolve::transition_fn(), view.value_ptr(), view.meta, view.wt, &[value], [0.0; 4], size);
        wrap_modified(r, &TRANSITION_T, view)
    }
}

/// Apply `.transition(.slide)` — in from the leading edge, out to the trailing.
pub fn transition_slide(view: &ViewHandle) -> ViewHandle {
    unsafe {
        let meta = abi::metadata0(resolve::any_transition_ma());
        let (value, _) = static_getter(resolve::transition_slide(), meta);
        let (_, _, size) = TRANSITION_T.applied_to(view.meta);
        let r = abi::call_modifier(
            resolve::transition_fn(),
            view.value_ptr(),
            view.meta,
            view.wt,
            &[value],
            [0.0; 4],
            size,
        );
        wrap_modified(r, &TRANSITION_T, view)
    }
}

swift_statics! {
    /// `SwiftUI.Animation`'s built-in curves, as the `static var`s they are.
    ///
    /// Named `AnimationKind` rather than `Animation` because
    /// [`crate::render::Animation`] — a captured sequence of frames — already
    /// holds that name in the prelude, and the two are unrelated.
    ///
    /// The timed forms (`easeInOut(duration:)`, `spring(…)`) are *methods*,
    /// not statics, and so are not here; [`crate::state`] calls one of them
    /// directly.
    pub enum AnimationKind {
        Default = c"$s7SwiftUI9AnimationV7defaultACvgZ",
        EaseIn = c"$s7SwiftUI9AnimationV6easeInACvgZ",
        EaseInOut = c"$s7SwiftUI9AnimationV9easeInOutACvgZ",
        EaseOut = c"$s7SwiftUI9AnimationV7easeOutACvgZ",
        Linear = c"$s7SwiftUI9AnimationV6linearACvgZ",
        // `systemOverlayAppearance` and its two neighbours are *not* here.
        // They are exported, and the mangled names resolve — but they live in
        // an `@available(macOS, unavailable) @available(iOS, unavailable)`
        // extension, and calling one traps with "Unavailable code reached".
        // visionOS only.
    }
}

/// `.animation(_:value:)` for an `Animation` already in hand.
///
/// [`animation_kind`] reads a static and passes its word; a custom animation
/// builds its own and has the same word to pass, so the call is shared.
///
/// # Safety
/// `anim` must be a live `Animation` — one class reference, `+1`.
pub(crate) unsafe fn animation_word(view: &ViewHandle, anim: u64, value: i64) -> ViewHandle {
    unsafe {
        let int_meta = resolve::int_meta();
        let eq_wt = abi::get_view_wt_for_proto(int_meta, resolve::equatable_proto());
        assert!(!eq_wt.is_null(), "Int: Equatable");
        let (_, _, size) = ANIMATION_T.applied_to(view.meta);
        let mut result = vec![0u8; size];
        let this = view.value_ptr();
        let mut v = value;
        core::arch::asm!("blr {f}", f = in(reg) resolve::animation_fn(),
            in("x0") anim,
            in("x1") &mut v as *mut i64,
            in("x2") view.meta,
            in("x3") int_meta,
            in("x4") view.wt,
            in("x5") eq_wt,
            in("x8") result.as_mut_ptr(),
            in("x20") this,
            lateout("x6") _, lateout("x7") _,
            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
            lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
        );
        wrap_modified(result, &ANIMATION_T, view)
    }
}

/// Apply `.animation(.default, value:)`.
///
/// `Animation` is likewise eight bytes, and so is `Animation?` — the optional
/// needs no separate tag, `nil` is simply a null reference.
pub fn animation(view: &ViewHandle, value: i64) -> ViewHandle {
    animation_kind(view, AnimationKind::Default, value)
}

/// `.animation(_:value:)` with the curve named.
pub fn animation_kind(view: &ViewHandle, kind: AnimationKind, value: i64) -> ViewHandle {
    unsafe {
        let anim_meta = abi::metadata0(resolve::animation_ma());
        let (anim, _) = static_getter(kind.resolved(), anim_meta);

        let int_meta = resolve::int_meta();
        let eq_wt = abi::get_view_wt_for_proto(int_meta, resolve::equatable_proto());
        assert!(!eq_wt.is_null(), "Int: Equatable");

        let (_, _, size) = ANIMATION_T.applied_to(view.meta);
        let mut result = vec![0u8; size];
        let this = view.value_ptr();
        let mut v = value;
        // animation<V: Equatable>(_: Animation?, value: V)
        //   x0 = Animation? by value, x1 = @in V (generic, so indirect),
        //   x2 = Self.Type, x3 = V.Type, x4 = Self: View WT,
        //   x5 = V: Equatable WT, x8 = @out, x20 = self
        core::arch::asm!("blr {f}", f = in(reg) resolve::animation_fn(),
            in("x0") anim,
            in("x1") &mut v as *mut i64,
            in("x2") view.meta,
            in("x3") int_meta,
            in("x4") view.wt,
            in("x5") eq_wt,
            in("x8") result.as_mut_ptr(),
            in("x20") this,
            lateout("x6") _, lateout("x7") _,
            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
            lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
        );
        wrap_modified(result, &ANIMATION_T, view)
    }
}

/// Apply `.onAppear(perform:)`.
pub fn on_appear(view: &ViewHandle, action: impl Fn() + 'static) -> ViewHandle {
    unsafe {
        // SwiftUI stores the action, so the context is leaked on purpose.
        let ctx = Box::leak(Box::new(abi::ClosureCtx::new(
            invoke_action,
            boxed_ctx(Box::new(action) as Box<dyn Fn()>),
        ))) as *mut abi::ClosureCtx;
        let (_, _, size) = ON_APPEAR_T.applied_to(view.meta);
        // Optional<() -> ()> is just the (function, context) pair; nil is a null
        // function pointer.
        let r = abi::call_modifier(resolve::on_appear_fn(), view.value_ptr(), view.meta, view.wt,
            &[abi::swift_closure_thunk as *const () as u64, ctx as u64],
            [0.0; 4],
            size,
        );
        wrap_modified(r, &ON_APPEAR_T, view)
    }
}

/// Apply `.onTapGesture(count:perform:)`.
/// The action may capture, as [`super::button`]'s may — a tap handler that
/// cannot know which row it is on is one every list has to route around.
pub fn on_tap_gesture(view: &ViewHandle, count: i64, action: impl Fn() + 'static) -> ViewHandle {
    unsafe {
        let boxed: Box<dyn Fn()> = Box::new(action);
        let payload = Box::into_raw(Box::new(boxed)) as *mut c_void;
        let ctx = Box::leak(Box::new(abi::ClosureCtx::new(
            super::run_boxed_action,
            payload,
        ))) as *mut abi::ClosureCtx;
        let subst = [view.meta, view.wt];
        let (meta, wt, size) = opaque_result_type(resolve::on_tap_oret(), &subst);
        let r = abi::call_modifier(resolve::on_tap_fn(), view.value_ptr(), view.meta, view.wt,
            &[count as u64, abi::swift_closure_thunk as *const () as u64, ctx as u64],
            [0.0; 4],
            size,
        );
        wrap_owned(r, meta, wt)
    }
}

/// Apply `.onLongPressGesture(minimumDuration:maximumDistance:pressing:perform:)`.
///
/// Unlike [`long_press_gesture`], which attaches a bare `LongPressGesture` with
/// nothing listening, this takes the action SwiftUI runs when the press
/// completes — so the gesture can be observed rather than only built.
pub fn on_long_press_gesture(
    view: &ViewHandle,
    duration: f64,
    max_distance: f64,
    action: impl Fn() + 'static,
) -> ViewHandle {
    unsafe {
        let ctx = Box::leak(Box::new(abi::ClosureCtx::new(
            invoke_action,
            boxed_ctx(Box::new(action) as Box<dyn Fn()>),
        ))) as *mut abi::ClosureCtx;
        let subst = [view.meta, view.wt];
        let (meta, wt, size) = opaque_result_type(resolve::on_long_press_oret(), &subst);
        // x0/x1 = `pressing:`, an Optional closure left nil; x2/x3 = `perform:`.
        let r = abi::call_modifier(resolve::on_long_press_fn(), view.value_ptr(), view.meta, view.wt,
            &[0, 0, abi::swift_closure_thunk as *const () as u64, ctx as u64],
            [duration, max_distance, 0.0, 0.0],
            size,
        );
        wrap_owned(r, meta, wt)
    }
}

// `.animation(_:value:)` is deliberately not bound.
//
// `_AnimationModifier` constructs and renders correctly once, then SwiftUI
// faults inside `_AnimationModifier.== infix` the next time it diffs the view.
// The `Optional<Animation>` is built through the value witness table and sized
// by stride, so this is not the buffer-overflow bug that `transition` had — the
// remaining suspicion is the ownership of `Animation.default`, which may be
// returned at +0 rather than +1, making the modifier's copy over-release.
//
// Shipping a binding that crashes on the second render would be worse than
// leaving it out. `.transition(_:)`, `.onAppear`, `.onTapGesture` and
// `.contextMenu` are bound and verified.

/// Apply `.contextMenu(menuItems:)`.
pub fn context_menu(view: &ViewHandle, items: &ViewHandle) -> ViewHandle {
    unsafe {
        // A context menu's items are built when the menu opens, so the closure
        // escapes and its context must outlive this call.
        let ctx = Box::leak(Box::new(abi::ClosureCtx::new(
            emit_tuple,
            Child::leak(items, Content::Concrete),
        ))) as *mut abi::ClosureCtx;

        // Self, then the menu's `M` — which the closure emits as an AnyView.
        let subst = [
            view.meta,
            items.meta,
            view.wt,
            items.wt,
        ];
        let (meta, wt, size) = opaque_result_type(resolve::context_menu_oret(), &subst);
        let mut result = vec![0u8; size];
        let this = view.value_ptr();
        // contextMenu<M: View>(menuItems: () -> M)
        //   x0 = closure fn, x1 = closure ctx, x2 = Self.Type, x3 = M.Type,
        //   x4 = Self: View WT, x5 = M: View WT, x8 = @out, x20 = self
        core::arch::asm!("blr {f}", f = in(reg) resolve::context_menu_fn(),
            in("x0") abi::swift_closure_thunk as *const c_void,
            in("x1") ctx,
            in("x2") view.meta,                  // Self.Type
            in("x3") items.meta,    // M.Type
            in("x4") view.wt,                    // Self: View
            in("x5") items.wt,      // M: View
            in("x8") result.as_mut_ptr(),
            in("x20") this,
            lateout("x6") _, lateout("x7") _,
            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
            lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
        );
        wrap_owned(result, meta, wt)
    }
}

/// `AnyTransition.scale(scale:anchor:)`, as the single word it is.
///
/// Loadable for the reason [`transition_opacity`] gives: an `AnyTransition` is
/// one class reference, so the static returns it in x0.
pub fn transition_scale(scale: f64, anchor: (f64, f64)) -> u64 {
    unsafe {
        let out: u64;
        // Three doubles and nothing else: `scale(scale:anchor:)` is a static on a
        // struct, so there is no metatype in x20 — passing one is what the
        // resilient statics next door need, not this.
        core::arch::asm!("blr {f}", f = in(reg) resolve::transition_scale_anchor(),
            in("d0") scale, in("d1") anchor.0, in("d2") anchor.1,
            lateout("x0") out,
            lateout("x20") _,
            lateout("x1") _, lateout("x2") _, lateout("x3") _, lateout("x4") _,
            lateout("x5") _, lateout("x6") _, lateout("x7") _, lateout("x8") _,
            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
            lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
        );
        out
    }
}

/// `AnyTransition.opacity`, as one word.
pub fn transition_opacity_value() -> u64 {
    unsafe {
        let meta = abi::metadata0(resolve::any_transition_ma());
        static_getter(resolve::transition_opacity(), meta).0
    }
}

/// `AnyTransition.identity` — a transition that does nothing.
///
/// Not the same as leaving the modifier off: `.identity` *replaces* the
/// transition a container would otherwise hand down, which is how one row of
/// an animated list is held still while the rest move.
pub fn transition_identity_value() -> u64 {
    unsafe {
        let meta = abi::metadata0(resolve::any_transition_ma());
        static_getter(resolve::transition_identity(), meta).0
    }
}

/// `AnyTransition.scale` — the static property, which scales from nothing.
///
/// [`transition_scale`] is the *method* `scale(scale:anchor:)`, a different
/// symbol that takes a factor and an anchor. This is the bare `.scale` Swift
/// source writes when it wants the default.
pub fn transition_scale_value() -> u64 {
    unsafe {
        let meta = abi::metadata0(resolve::any_transition_ma());
        static_getter(resolve::transition_scale(), meta).0
    }
}

/// `AnyTransition.combined(with:)` — the receiver in x20, the other in x0.
pub fn transition_combined(lhs: u64, rhs: u64) -> u64 {
    unsafe {
        let out: u64;
        // `combined(with:)` takes its receiver as an ordinary second argument in
        // x1, *by value*, not through the self register — an `AnyTransition` is
        // one reference, so there is nothing for swiftself to point at.
        core::arch::asm!("blr {f}", f = in(reg) resolve::transition_combined(),
            in("x0") rhs,
            in("x1") lhs,
            lateout("x0") out,
            lateout("x20") _,
            lateout("x2") _, lateout("x3") _, lateout("x4") _,
            lateout("x5") _, lateout("x6") _, lateout("x7") _, lateout("x8") _,
            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
            lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
        );
        out
    }
}

/// Apply `.transition(_:)` with an `AnyTransition` already built.
pub fn transition(view: &ViewHandle, value: u64) -> ViewHandle {
    unsafe {
        let (_, _, size) = TRANSITION_T.applied_to(view.meta);
        let r = abi::call_modifier(resolve::transition_fn(), view.value_ptr(), view.meta, view.wt, &[value], [0.0; 4], size);
        wrap_modified(r, &TRANSITION_T, view)
    }
}


/// `.contextMenu(menuItems:preview:)` — a menu with something to look at.
///
/// The preview is what a long press enlarges before the menu appears: the row
/// blown up, a photo, a card. [`context_menu`] has no such slot, so the menu
/// came up over the view as it already was.
///
/// The `isPresented:` overload beside it in the export trie is *not* bound. It
/// is there, and a call to it builds — but Swift rejects
/// `contextMenu(isPresented:menuItems:)` with "extra argument", so it is not
/// public API on this SDK and there is no reference to check a binding
/// against. `Toggle(isOn: [Binding<Bool>])` is exported and unwritable in the
/// same way.
pub fn context_menu_preview(
    view: &ViewHandle,
    items: &ViewHandle,
    preview: &ViewHandle,
) -> ViewHandle {
    unsafe {
        let items_ctx = Box::leak(Box::new(abi::ClosureCtx::new(
            emit_tuple,
            Child::leak(items, Content::Concrete),
        ))) as *mut abi::ClosureCtx;
        let preview_ctx = Box::leak(Box::new(abi::ClosureCtx::new(
            emit_tuple,
            Child::leak(preview, Content::Concrete),
        ))) as *mut abi::ClosureCtx;

        let subst = [view.meta, items.meta, preview.meta, view.wt, items.wt, preview.wt];
        let (meta, wt, size) = opaque_result_type(resolve::context_menu_preview_oret(), &subst);
        let mut result = vec![0u8; size];
        let this = view.value_ptr();
        // contextMenu<M: View, P: View>(menuItems: () -> M, preview: () -> P)
        //   x0/x1 = menu closure, x2/x3 = preview closure, x4 = Self.Type,
        //   x5 = M.Type, x6 = P.Type, x7 = Self: View WT,
        //   [sp] = M: View WT, [sp+8] = P: View WT, x8 = @out, x20 = self
        core::arch::asm!(
            "sub sp, sp, #16",
            "stp {mw}, {pw}, [sp]",
            "blr {f}",
            "add sp, sp, #16",
            f = in(reg) resolve::context_menu_preview_fn(),
            mw = in(reg) items.wt,
            pw = in(reg) preview.wt,
            in("x0") abi::swift_closure_thunk as *const c_void,
            in("x1") items_ctx,
            in("x2") abi::swift_closure_thunk as *const c_void,
            in("x3") preview_ctx,
            in("x4") view.meta,
            in("x5") items.meta,
            in("x6") preview.meta,
            in("x7") view.wt,
            in("x8") result.as_mut_ptr(),
            in("x20") this,
            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
            lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
        );
        wrap_owned(result, meta, wt)
    }
}