Skip to main content

swiftui_native/views/
catalogue.rs

1//! The last of Apple's own catalogue.
2//!
3//! `tools/xcode_catalog.py` reads the list Xcode's Library panel is built from —
4//! 415 views and modifiers, each with Apple's canonical example — and reports
5//! which of them this crate can build. These are the ones that were left, and
6//! they are here together rather than scattered because what they have in common
7//! is *why* they were last: each needed something the generated bindings cannot
8//! express.
9//!
10//!   * `RenameButton` and `SettingsLink` take no arguments at all — their labels
11//!     are fixed types — so there is nothing for a generator to read.
12//!   * `HSplitView`/`VSplitView` are containers, not modifiers.
13//!   * `ContentUnavailableView` has two initialisers of very different shapes:
14//!     three view builders, or a title and an SF Symbol.
15//!   * `ScrollViewReader` hands its closure a *proxy*, which is the only place in
16//!     SwiftUI where a view builder takes an argument that is not a value the
17//!     caller supplied.
18
19use super::*;
20use crate::abi;
21use crate::mangle::{self, swift_type};
22use crate::resolve;
23use core::ffi::c_void;
24
25/// `X<Content>` for a container generic over one `Content: View`.
26///
27/// Only `HSplitView` and `VSplitView` use this, and both are macOS-only.
28///
29/// # Safety
30/// `ma` must be such a type's metadata accessor.
31#[cfg(target_os = "macos")]
32unsafe fn container_meta(
33    ma: *const c_void,
34    content: &ViewHandle,
35) -> (*const c_void, *const c_void) {
36    unsafe {
37        let meta = abi::generic_metadata2(ma, content.meta, content.wt);
38        assert!(!meta.is_null(), "container metadata instantiation failed");
39        let wt = abi::get_view_wt(meta);
40        assert!(!wt.is_null(), "{} !: View", crate::diag::type_name(meta));
41        (meta, wt)
42    }
43}
44
45/// A container built from `init(content:)` — one view builder and nothing else.
46///
47/// `HSplitView`, `VSplitView` and `Settings` all have this shape:
48/// `(sret, closure fn, closure ctx, Content.Type, Content: View)`.
49///
50/// # Safety
51/// `init` must be such an initialiser and `ma` its type's metadata accessor.
52#[cfg(target_os = "macos")]
53unsafe fn one_child_container(
54    init: *const c_void,
55    ma: *const c_void,
56    content: &ViewHandle,
57) -> ViewHandle {
58    unsafe {
59        let (meta, wt) = container_meta(ma, content);
60        // Borrowed, not leaked: `init(content:)` takes its builder `@noescape`
61        // and calls it before returning, so the child outlives the call.
62        let child = content.clone();
63        let kid = Child::new(&child, Content::Concrete);
64        let handles = kid.element();
65        let payload = TuplePayload { children: handles.as_ptr(), count: 1 };
66        let mut ctx =
67            abi::ClosureCtx::new(emit_tuple, &payload as *const TuplePayload as *mut c_void);
68
69        let mut result = vec![0u8; abi::type_bytes(meta)];
70        core::arch::asm!("blr {f}", f = in(reg) init,
71            in("x0") abi::swift_closure_thunk as *const c_void,
72            in("x1") &mut ctx as *mut abi::ClosureCtx,
73            in("x2") content.meta,
74            in("x3") content.wt,
75            in("x8") result.as_mut_ptr(),
76            lateout("x4") _, lateout("x5") _, lateout("x6") _, lateout("x7") _,
77            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
78            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
79            lateout("x17") _, lateout("lr") _, lateout("x20") _, lateout("x21") _,
80            clobber_abi("C"),
81        );
82        wrap_owned(result, meta, wt)
83    }
84}
85
86/// `HSplitView { content }` — a draggable divider between side-by-side panes.
87///
88/// macOS only. A split *view*, not a `NavigationSplitView`: the panes are
89/// resizable by the user and there is no sidebar behaviour.
90#[cfg(target_os = "macos")]
91pub fn hsplit_view(content: &ViewHandle) -> ViewHandle {
92    unsafe {
93        one_child_container(resolve::hsplit_view_init(), resolve::hsplit_view_ma(), content)
94    }
95}
96
97/// `VSplitView { content }` — the same, stacked.
98#[cfg(target_os = "macos")]
99pub fn vsplit_view(content: &ViewHandle) -> ViewHandle {
100    unsafe {
101        one_child_container(resolve::vsplit_view_init(), resolve::vsplit_view_ma(), content)
102    }
103}
104
105/// A view whose initialiser takes nothing but the result slot.
106///
107/// # Safety
108/// `init` must be a no-argument initialiser and `named` its result type's
109/// mangled name.
110unsafe fn no_argument_view(init: *const c_void, named: &str) -> ViewHandle {
111    unsafe {
112        let meta = abi::resolve_type_by_mangled_name(named.as_bytes());
113        assert!(!meta.is_null(), "{}", mangle::demangle(named));
114        let wt = abi::get_view_wt(meta);
115        assert!(!wt.is_null(), "{} !: View", crate::diag::type_name(meta));
116        let mut result = vec![0u8; abi::type_bytes(meta)];
117        core::arch::asm!("blr {f}", f = in(reg) init,
118            in("x8") result.as_mut_ptr(),
119            lateout("x0") _, lateout("x1") _, lateout("x2") _, lateout("x3") _,
120            lateout("x4") _, lateout("x5") _, lateout("x6") _, lateout("x7") _,
121            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
122            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
123            lateout("x17") _, lateout("lr") _, lateout("x20") _, lateout("x21") _,
124            clobber_abi("C"),
125        );
126        wrap_owned(result, meta, wt)
127    }
128}
129
130/// `RenameButton()` — the button a `.renameAction` puts in a context menu.
131///
132/// It takes no arguments: the label is always `Label<Text, Image>`, and what it
133/// does comes from the `renameAction` in the environment rather than from a
134/// closure here.
135pub fn rename_button() -> ViewHandle {
136    unsafe {
137        let label = mangle::bound(
138            &swift_type!(SwiftUI.Label),
139            &[&swift_type!(SwiftUI.Text), &swift_type!(SwiftUI.Image)],
140        );
141        let named = mangle::bound(&swift_type!(SwiftUI.RenameButton), &[&label]);
142        no_argument_view(resolve::rename_button_init(), &named)
143    }
144}
145
146/// `SettingsLink()` — a button that opens the app's `Settings` scene.
147///
148/// macOS only, and the counterpart to [`crate::scene::settings`]: that builds
149/// the window, this is how a view opens it without the user reaching for ⌘,.
150#[cfg(target_os = "macos")]
151pub fn settings_link() -> ViewHandle {
152    unsafe {
153        let named = mangle::bound(
154            &swift_type!(SwiftUI.SettingsLink),
155            &[&swift_type!(SwiftUI.DefaultSettingsLinkLabel)],
156        );
157        no_argument_view(resolve::settings_link_init(), &named)
158    }
159}
160
161/// `ContentUnavailableView(title, systemImage:description:)` — the placeholder
162/// for an empty list, a failed search, a screen with nothing on it yet.
163///
164/// The shorthand initialiser, whose three generic parameters are all fixed:
165/// `Label<Text, Image>` for the label, `Text?` for the description and
166/// `EmptyView` for the actions. `description` is optional — `None` is a genuine
167/// `nil`, not an empty string, and draws no second line at all.
168pub fn content_unavailable(title: &str, system_image: &str, description: Option<&str>) -> ViewHandle {
169    unsafe {
170        let label = mangle::bound(
171            &swift_type!(SwiftUI.Label),
172            &[&swift_type!(SwiftUI.Text), &swift_type!(SwiftUI.Image)],
173        );
174        let named = mangle::bound(
175            &swift_type!(SwiftUI.ContentUnavailableView),
176            &[
177                &label,
178                &mangle::optional(&swift_type!(SwiftUI.Text)),
179                &swift_type!(SwiftUI.EmptyView),
180            ],
181        );
182        let meta = abi::resolve_type_by_mangled_name(named.as_bytes());
183        assert!(!meta.is_null(), "{}", mangle::demangle(&named));
184        let wt = abi::get_view_wt(meta);
185        assert!(!wt.is_null(), "{} !: View", crate::diag::type_name(meta));
186
187        let (k0, k1, k2, k3) = lsk_words(title);
188        let img = abi::swift_string(system_image);
189        let i0 = u64::from_le_bytes(img[..8].try_into().expect("image lo"));
190        let i1 = u64::from_le_bytes(img[8..].try_into().expect("image hi"));
191        // `Text?` is four words; `nil` is all four zero, since a `Text`'s first
192        // word is a reference and a null one is what the optional's spare-bit
193        // representation uses.
194        let text = match description {
195            Some(s) => text_value(s),
196            None => [0u64; 4],
197        };
198
199        let mut result = vec![0u8; abi::type_bytes(meta)];
200        // x0-x3 = LocalizedStringKey, x4/x5 = the systemImage String,
201        // x6/x7 + two stack words = the `Text?`, x8 = @out.
202        core::arch::asm!(
203            "sub sp, sp, #16",
204            "stp {t2}, {t3}, [sp]",
205            "blr {f}",
206            "add sp, sp, #16",
207            f = in(reg) resolve::content_unavailable_system_init(),
208            t2 = in(reg) text[2],
209            t3 = in(reg) text[3],
210            in("x0") k0, in("x1") k1, in("x2") k2, in("x3") k3,
211            in("x4") i0, in("x5") i1,
212            in("x6") text[0], in("x7") text[1],
213            in("x8") result.as_mut_ptr(),
214            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
215            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
216            lateout("x17") _, lateout("lr") _, lateout("x20") _, lateout("x21") _,
217            clobber_abi("C"),
218        );
219        wrap_owned(result, meta, wt)
220    }
221}
222
223// ── ScrollViewReader ──
224
225/// The `ScrollViewProxy` a [`scroll_view_reader`]'s closure is handed.
226///
227/// An owned copy, not a borrow, because that is how SwiftUI means it to be used:
228///
229/// ```text
230/// ScrollViewReader { proxy in
231///     Button("go") { proxy.scrollTo(10) }   // captured, used later
232/// }
233/// ```
234///
235/// Calling `scroll_to` *during* the build instead traps —
236/// `ScrollViewProxy may not be accessed during view updates`, which is SwiftUI's
237/// own check and was measured by doing it. So the proxy has to outlive the
238/// closure, and a borrow would not let it.
239///
240/// `ScrollViewProxy` is resilient, so it reaches the closure by address and its
241/// size is not something a caller may assume: the bytes are copied through its
242/// own value witness rather than read as a word. It happens to be eight POD
243/// bytes today, which is exactly the kind of fact a resilient type is allowed to
244/// change.
245pub struct ScrollProxy {
246    bytes: Vec<u8>,
247    meta: *const c_void,
248}
249
250impl Clone for ScrollProxy {
251    fn clone(&self) -> Self {
252        unsafe {
253            let vwt = &*swift_runtime_sys::SwiftABI::get_value_witness_table(self.meta);
254            let mut bytes = vec![0u8; self.bytes.len()];
255            (vwt.initialize_with_copy)(
256                bytes.as_mut_ptr() as *mut c_void,
257                self.bytes.as_ptr() as *mut c_void,
258                self.meta,
259            );
260            Self { bytes, meta: self.meta }
261        }
262    }
263}
264
265impl Drop for ScrollProxy {
266    fn drop(&mut self) {
267        unsafe {
268            let vwt = &*swift_runtime_sys::SwiftABI::get_value_witness_table(self.meta);
269            if vwt.size > 0 {
270                (vwt.destroy)(self.bytes.as_mut_ptr() as *mut c_void, self.meta);
271            }
272        }
273    }
274}
275
276impl ScrollProxy {
277    /// Copy the proxy SwiftUI handed the closure.
278    ///
279    /// # Safety
280    /// `raw` must address a live `ScrollViewProxy`.
281    unsafe fn copy_from(raw: *mut c_void) -> Self {
282        unsafe {
283            let meta = abi::metadata0(resolve::scroll_view_proxy_ma());
284            assert!(!meta.is_null(), "ScrollViewProxy metadata");
285            let vwt = &*swift_runtime_sys::SwiftABI::get_value_witness_table(meta);
286            let mut bytes = vec![0u8; abi::type_bytes(meta)];
287            (vwt.initialize_with_copy)(bytes.as_mut_ptr() as *mut c_void, raw, meta);
288            Self { bytes, meta }
289        }
290    }
291
292    /// `proxy.scrollTo(id)` — bring the view with that `.id(_:)` into view.
293    ///
294    /// `Int` ids only, which is what `ForEach(0..<n)` and `.id(i)` produce. A
295    /// `String` id would need its own `Hashable` witness passed, and nothing in
296    /// this crate produces one yet.
297    pub fn scroll_to(&self, id: i64) {
298        unsafe { self.scroll(id, None) }
299    }
300
301    /// `proxy.scrollTo(id, anchor: UnitPoint(x:y:))`.
302    pub fn scroll_to_anchor(&self, id: i64, anchor: (f64, f64)) {
303        unsafe { self.scroll(id, Some(anchor)) }
304    }
305
306    /// # Safety
307    /// `self.bytes` must hold a live `ScrollViewProxy`.
308    unsafe fn scroll(&self, id: i64, anchor: Option<(f64, f64)>) {
309        unsafe {
310            let mut value = id;
311            // `UnitPoint?` is two `CGFloat`s and a tag, and it travels as *three
312            // integer registers*: an optional is an enum, and an enum with a
313            // payload is lowered to its raw storage rather than to its fields,
314            // so the two doubles arrive in x1/x2 and not in d0/d1.
315            let (x, y, tag) = match anchor {
316                Some((x, y)) => (x.to_bits(), y.to_bits(), 0u64),
317                None => (0, 0, 1),
318            };
319            core::arch::asm!("blr {f}", f = in(reg) resolve::scroll_to_fn(),
320                in("x0") &mut value as *mut i64,
321                in("x1") x, in("x2") y, in("x3") tag,
322                in("x4") resolve::int_meta(),
323                in("x5") resolve::int_hashable_wt(),
324                in("x20") self.bytes.as_ptr(),
325                lateout("x6") _, lateout("x7") _, lateout("x8") _,
326                lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
327                lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
328                lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
329            );
330        }
331    }
332}
333
334/// What the reader's content closure needs.
335struct ProxyPayload {
336    builder: Box<dyn Fn(ScrollProxy) -> ViewHandle>,
337}
338
339/// Build the content, handing the proxy through.
340///
341/// # Safety
342/// Reached through [`abi::index_closure_thunk`], which puts the proxy in the
343/// second argument; `payload` must be a live [`ProxyPayload`].
344unsafe extern "C" fn emit_with_proxy(out: *mut u8, proxy: i64, payload: *mut c_void) {
345    let p = unsafe { &*(payload as *const ProxyPayload) };
346    let view = (p.builder)(unsafe { ScrollProxy::copy_from(proxy as *mut c_void) });
347    unsafe { *(out as *mut u64) = view.into_anyview_word() };
348}
349
350/// `ScrollViewReader { proxy in content }` — a scroll view that can be scrolled
351/// from code.
352///
353/// The one view builder in SwiftUI that takes an argument the caller did not
354/// supply. It is also *escaping* — `…Vc` in the mangling, not `…VXE` — so both
355/// the closure and its context are leaked rather than borrowed: SwiftUI keeps
356/// them and calls back long after this returns.
357pub fn scroll_view_reader(
358    content: impl Fn(ScrollProxy) -> ViewHandle + 'static,
359) -> ViewHandle {
360    unsafe {
361        let meta = abi::generic_metadata2(
362            resolve::scroll_view_reader_ma(),
363            resolve::anyview_meta(),
364            resolve::anyview_wt(),
365        );
366        assert!(!meta.is_null(), "ScrollViewReader metadata");
367        let wt = abi::get_view_wt(meta);
368        assert!(!wt.is_null(), "{} !: View", crate::diag::type_name(meta));
369
370        let payload = Box::leak(Box::new(ProxyPayload { builder: Box::new(content) }))
371            as *mut ProxyPayload as *mut c_void;
372        let ctx = Box::leak(Box::new(abi::IndexClosureCtx::new(emit_with_proxy, payload)))
373            as *mut abi::IndexClosureCtx;
374
375        // The initialiser returns its two words *in registers*, not through x8:
376        // `ScrollViewReader<Content>` is loadable and small enough. The assert is
377        // the check that it still is — a third word would arrive somewhere this
378        // does not look.
379        let stride = abi::type_bytes(meta);
380        assert_eq!(stride, 16, "ScrollViewReader is no longer two words");
381        let (w0, w1): (u64, u64);
382        core::arch::asm!("blr {f}", f = in(reg) resolve::scroll_view_reader_init(),
383            in("x0") abi::index_closure_thunk as *const c_void,
384            in("x1") ctx,
385            in("x2") resolve::anyview_meta(),
386            in("x3") resolve::anyview_wt(),
387            lateout("x0") w0, lateout("x1") w1,
388            lateout("x2") _, lateout("x3") _, lateout("x4") _, lateout("x5") _,
389            lateout("x6") _, lateout("x7") _, lateout("x8") _,
390            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
391            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
392            lateout("x17") _, lateout("lr") _, lateout("x20") _, lateout("x21") _,
393            clobber_abi("C"),
394        );
395        let mut result = vec![0u8; stride];
396        result[..8].copy_from_slice(&w0.to_le_bytes());
397        result[8..16].copy_from_slice(&w1.to_le_bytes());
398        wrap_owned(result, meta, wt)
399    }
400}
401
402// ── PasteButton ──
403
404/// `PasteButton(supportedContentTypes:payloadAction:)` — a button that pastes.
405///
406/// Not generic: its label is fixed, so there is no content type to pass. What
407/// arrives in the action is the pasteboard's item providers, the same shape
408/// `on_paste_command` receives.
409pub fn paste_button(
410    kinds: &[ContentType],
411    paste: impl Fn(&[ItemProvider]) + 'static,
412) -> ViewHandle {
413    unsafe {
414        let meta = abi::metadata0(resolve::paste_button_ma());
415        assert!(!meta.is_null(), "PasteButton metadata");
416        let wt = abi::get_view_wt(meta);
417        assert!(!wt.is_null(), "{} !: View", crate::diag::type_name(meta));
418
419        let types = content_types(kinds);
420        let c = wide_words(observe_providers, provider_payload(0, None, Some(Box::new(paste))));
421        let mut result = vec![0u8; abi::type_bytes(meta)];
422        core::arch::asm!("blr {f}", f = in(reg) resolve::paste_button_init(),
423            in("x0") types,
424            in("x1") c[0],
425            in("x2") c[1],
426            in("x8") result.as_mut_ptr(),
427            lateout("x3") _, lateout("x4") _, lateout("x5") _, lateout("x6") _,
428            lateout("x7") _,
429            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
430            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
431            lateout("x17") _, lateout("lr") _, lateout("x20") _, lateout("x21") _,
432            clobber_abi("C"),
433        );
434        wrap_owned(result, meta, wt)
435    }
436}
437
438// ── onInsert ──
439
440/// `.onInsert(of:perform:)` — accept a drop into a `ForEach`'s rows.
441///
442/// A modifier on `DynamicViewContent`, not on `View`: the receiver has to be a
443/// `ForEach`, because what the action is told is *where* in the collection the
444/// drop landed. Handing it anything else fails loudly rather than rendering
445/// something that ignores drops — the conformance lookup is the check.
446///
447/// The action is `(Int, [NSItemProvider]) -> Void`: the index first, then what
448/// was dropped.
449pub fn on_insert(
450    content: &ViewHandle,
451    kinds: &[ContentType],
452    action: impl Fn(i64, &[ItemProvider]) + 'static,
453) -> ViewHandle {
454    unsafe {
455        let dvc = abi::get_view_wt_for_proto(content.meta, resolve::dynamic_view_content_proto());
456        assert!(
457            !dvc.is_null(),
458            "{} !: DynamicViewContent — `on_insert` needs a ForEach, not any view",
459            crate::diag::type_name(content.meta),
460        );
461
462        let types = content_types(kinds);
463        let c = pair_words(
464            insert_at_index,
465            Box::leak(Box::new(InsertPayload { action: Box::new(action) })) as *mut InsertPayload
466                as *mut c_void,
467        );
468
469        let (meta, _dvc_out, size) =
470            super::opaque_result_type(resolve::on_insert_oret(), &[content.meta, dvc]);
471        // The opaque type's *first* conformance is `DynamicViewContent`, since
472        // that is what the signature promises. A handle needs the `View` one,
473        // which the same type also has.
474        let wt = abi::get_view_wt(meta);
475        assert!(!wt.is_null(), "{} !: View", crate::diag::type_name(meta));
476
477        let mut result = vec![0u8; size];
478        let this = content.value_ptr();
479        // x0 = [UTType], x1/x2 = the action, x3 = Self.Type,
480        // x4 = Self: DynamicViewContent, x8 = @out, x20 = self.
481        core::arch::asm!("blr {f}", f = in(reg) resolve::on_insert_fn(),
482            in("x0") types,
483            in("x1") c[0],
484            in("x2") c[1],
485            in("x3") content.meta,
486            in("x4") dvc,
487            in("x8") result.as_mut_ptr(),
488            in("x20") this,
489            lateout("x5") _, lateout("x6") _, lateout("x7") _,
490            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
491            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
492            lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
493        );
494        wrap_owned(result, meta, wt)
495    }
496}
497
498/// `(Int, [NSItemProvider]) -> Void` as a Rust callback.
499type InsertAction = Box<dyn Fn(i64, &[ItemProvider])>;
500
501/// What [`insert_at_index`] calls.
502struct InsertPayload {
503    action: InsertAction,
504}
505
506/// `(Int, [NSItemProvider]) -> Void`, as Rust.
507///
508/// # Safety
509/// Reached through [`abi::pair_closure_thunk`]; `payload` must be a live
510/// [`InsertPayload`], `index` an `Int` and `providers` a Swift array of
511/// `NSItemProvider`.
512unsafe extern "C" fn insert_at_index(index: *const u8, providers: *const u8, payload: *mut c_void) {
513    let p = unsafe { &*(payload as *const InsertPayload) };
514    let items = unsafe { read_provider_array(providers as u64) };
515    (p.action)(index as i64, &items);
516}
517
518// ── SubscriptionView ──
519
520/// `SubscriptionView(content:publisher:action:)` — `onReceive` as a view.
521///
522/// The same thing [`super::on_receive`] does as a modifier, and with the same
523/// publisher: `Combine.Empty<Int, Never>`, which is the one publisher that can be
524/// built without a Foundation object behind it. `complete_immediately` is
525/// `Empty`'s only stored property — `true` finishes at once, `false` never
526/// publishes anything.
527///
528/// `PublisherType` comes *first* in `SubscriptionView<PublisherType, Content>`,
529/// so the metadata and witness tables go in that order too. The labels read
530/// `content:publisher:` and the arguments follow the labels, which is the
531/// opposite order from the generic parameters — worth getting from the IR rather
532/// than from the signature.
533pub fn subscription_view(
534    content: &ViewHandle,
535    complete_immediately: bool,
536    action: impl Fn(i64) + 'static,
537) -> ViewHandle {
538    unsafe {
539        let empty = mangle::bound("7Combine5EmptyV", &[mangle::INT, mangle::NEVER]);
540        let publisher = abi::resolve_type_by_mangled_name(empty.as_bytes());
541        assert!(!publisher.is_null(), "{}", mangle::demangle(&empty));
542        let pub_wt = abi::get_view_wt_for_proto(publisher, resolve::publisher_proto());
543        assert!(!pub_wt.is_null(), "Empty<Int, Never> !: Combine.Publisher");
544
545        let named = mangle::bound(
546            &swift_type!(SwiftUI.SubscriptionView),
547            &[&empty, &swift_type!(SwiftUI.AnyView)],
548        );
549        let meta = abi::resolve_type_by_mangled_name(named.as_bytes());
550        assert!(!meta.is_null(), "{}", mangle::demangle(&named));
551        let wt = abi::get_view_wt(meta);
552        assert!(!wt.is_null(), "{} !: View", crate::diag::type_name(meta));
553
554        let mut value = vec![u8::from(complete_immediately); abi::type_bytes(publisher).max(1)];
555        // The content is erased, because the closure that produced it could have
556        // returned anything; `AnyView` is one word.
557        let mut erased_word = content.to_anyview_word().to_le_bytes();
558
559        let ctx = Box::leak(Box::new(abi::ValueClosureCtx::new(
560            super::saw_published_int,
561            super::boxed_ctx(Box::new(action) as Box<dyn Fn(i64)>),
562        ))) as *mut abi::ValueClosureCtx;
563
564        let mut result = vec![0u8; abi::type_bytes(meta)];
565        // x0 = @in content, x1 = @in publisher, x2/x3 = the action,
566        // x4 = PublisherType.Type, x5 = Content.Type,
567        // x6 = PublisherType: Publisher, x7 = Content: View, x8 = @out.
568        core::arch::asm!("blr {f}", f = in(reg) resolve::subscription_view_init(),
569            in("x0") erased_word.as_mut_ptr(),
570            in("x1") value.as_mut_ptr(),
571            in("x2") abi::value_closure_thunk as *const c_void,
572            in("x3") ctx,
573            in("x4") publisher,
574            in("x5") resolve::anyview_meta(),
575            in("x6") pub_wt,
576            in("x7") resolve::anyview_wt(),
577            in("x8") result.as_mut_ptr(),
578            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
579            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
580            lateout("x17") _, lateout("lr") _, lateout("x20") _, lateout("x21") _,
581            clobber_abi("C"),
582        );
583        wrap_owned(result, meta, wt)
584    }
585}
586
587// ── Sign in with Apple ──
588//
589// The last entry in Apple's catalogue, and the only one that is not in SwiftUI:
590// `SignInWithAppleButton` lives in `_AuthenticationServices_SwiftUI`, a
591// cross-import overlay that depends on SwiftUI rather than being depended on by
592// it. Nothing in the process has loaded it, so `resolve::sym` cannot see it and
593// these go through [`resolve::sym_in`], which loads the image first.
594//
595// The button renders anywhere. *Authorising* needs the Sign in with Apple
596// entitlement, which is a signing matter and not a binding one — what is bound
597// here is the button, its two callbacks and its style.
598
599/// Which words `SignInWithAppleButton` draws.
600#[derive(Clone, Copy, Debug, PartialEq, Eq)]
601pub enum AppleButtonLabel {
602    /// "Sign in with Apple".
603    SignIn,
604    /// "Sign up with Apple".
605    SignUp,
606    /// "Continue with Apple".
607    Continue,
608}
609
610impl AppleButtonLabel {
611    fn getter(self) -> &'static core::ffi::CStr {
612        match self {
613            Self::SignIn => c"$s31_AuthenticationServices_SwiftUI21SignInWithAppleButtonV5LabelV04signF0AEvgZ",
614            Self::SignUp => c"$s31_AuthenticationServices_SwiftUI21SignInWithAppleButtonV5LabelV6signUpAEvgZ",
615            Self::Continue => c"$s31_AuthenticationServices_SwiftUI21SignInWithAppleButtonV5LabelV8continueAEvgZ",
616        }
617    }
618}
619
620/// How `SignInWithAppleButton` is painted.
621#[derive(Clone, Copy, Debug, PartialEq, Eq)]
622pub enum AppleButtonStyle {
623    /// Black on white.
624    Black,
625    /// White on black.
626    White,
627    /// White with a black outline, for light backgrounds.
628    WhiteOutline,
629}
630
631impl AppleButtonStyle {
632    fn getter(self) -> &'static core::ffi::CStr {
633        match self {
634            Self::Black => c"$s31_AuthenticationServices_SwiftUI21SignInWithAppleButtonV5StyleV5blackAEvgZ",
635            Self::White => c"$s31_AuthenticationServices_SwiftUI21SignInWithAppleButtonV5StyleV5whiteAEvgZ",
636            Self::WhiteOutline => c"$s31_AuthenticationServices_SwiftUI21SignInWithAppleButtonV5StyleV12whiteOutlineAEvgZ",
637        }
638    }
639}
640
641/// The `ASAuthorizationAppleIDRequest` the `onRequest` callback is handed.
642///
643/// A borrowed Objective-C object: configure it and return. The one thing worth
644/// configuring is what to ask for, which is why that is the method here; `raw`
645/// is for anything else, reachable through [`crate::objc`].
646pub struct AppleIDRequest<'a> {
647    raw: *mut c_void,
648    marker: core::marker::PhantomData<&'a ()>,
649}
650
651impl AppleIDRequest<'_> {
652    /// `request.requestedScopes = [.fullName, .email]`.
653    ///
654    /// The two scopes Sign in with Apple offers. Set through the Objective-C
655    /// runtime because `ASAuthorizationScope` is a string constant in the
656    /// framework's headers, not a Swift enum with a getter.
657    pub fn request_full_name_and_email(&self) {
658        unsafe {
659            let full_name = crate::objc::nsstring("fullName");
660            let email = crate::objc::nsstring("email");
661            let items = [full_name, email];
662            let array_cls = crate::objc::class(c"OBJC_CLASS_$_NSArray");
663            assert!(!array_cls.is_null(), "NSArray class");
664            type WithObjects =
665                unsafe extern "C" fn(*mut c_void, *const c_void, *const *mut c_void, u64) -> *mut c_void;
666            let f: WithObjects =
667                core::mem::transmute(crate::objc::msg_send_addr());
668            let array = f(
669                array_cls,
670                crate::objc::sel(c"arrayWithObjects:count:"),
671                items.as_ptr(),
672                2,
673            );
674            crate::objc::send_id(self.raw, crate::objc::sel(c"setRequestedScopes:"), array);
675        }
676    }
677
678    /// The object itself, for anything [`request_full_name_and_email`] does not
679    /// cover.
680    ///
681    /// [`request_full_name_and_email`]: Self::request_full_name_and_email
682    pub fn raw(&self) -> *mut c_void {
683        self.raw
684    }
685}
686
687/// What the button's two callbacks are.
688struct ApplePayload {
689    on_request: Box<dyn Fn(AppleIDRequest<'_>)>,
690    on_completion: Box<dyn Fn(bool)>,
691    result_meta: *const c_void,
692}
693
694/// `onRequest: (ASAuthorizationAppleIDRequest) -> Void`.
695///
696/// A class reference, so x0 *is* the object rather than a pointer to it.
697///
698/// # Safety
699/// Reached through [`abi::value_closure_thunk`]; `payload` must be a live
700/// [`ApplePayload`].
701unsafe extern "C" fn apple_on_request(request: *const u8, payload: *mut c_void) {
702    let p = unsafe { &*(payload as *const ApplePayload) };
703    (p.on_request)(AppleIDRequest {
704        raw: request as *mut c_void,
705        marker: core::marker::PhantomData,
706    });
707}
708
709/// `onCompletion: (Result<ASAuthorization, any Error>) -> Void`.
710///
711/// `Result` with an existential failure is address-only, so x0 is the address of
712/// the value. Which case it holds is read from the enum's own `getEnumTag`
713/// witness — case 0 is `.success` — rather than from the bytes, because a
714/// multi-payload enum's discriminator is not at a fixed offset.
715///
716/// # Safety
717/// As [`apple_on_request`].
718unsafe extern "C" fn apple_on_completion(result: *const u8, payload: *mut c_void) {
719    let p = unsafe { &*(payload as *const ApplePayload) };
720    let tag = unsafe { abi::enum_tag(result as *const c_void, p.result_meta) };
721    (p.on_completion)(tag == 0);
722}
723
724/// `SignInWithAppleButton(label, onRequest:onCompletion:)`.
725///
726/// `on_request` is called before the sheet appears, to say what to ask for;
727/// `on_completion` is told whether the authorisation succeeded.
728pub fn sign_in_with_apple_button(
729    label: AppleButtonLabel,
730    on_request: impl Fn(AppleIDRequest<'_>) + 'static,
731    on_completion: impl Fn(bool) + 'static,
732) -> ViewHandle {
733    unsafe {
734        let meta = abi::metadata0(resolve::siwa_button_ma());
735        assert!(!meta.is_null(), "SignInWithAppleButton metadata");
736        let wt = abi::get_view_wt(meta);
737        assert!(!wt.is_null(), "{} !: View", crate::diag::type_name(meta));
738
739        let label_meta = abi::metadata0(resolve::siwa_label_ma());
740        assert!(!label_meta.is_null(), "SignInWithAppleButton.Label metadata");
741        let getter = resolve::sym_in(resolve::AUTH_SWIFTUI, label.getter());
742        assert!(!getter.is_null(), "{:?}", label.getter());
743        let mut label_value = static_getter_indirect(getter, label_meta, 0);
744
745        let result_named = mangle::bound(
746            "s6ResultO",
747            &["So15ASAuthorizationC", "s5Error_p"],
748        );
749        let result_meta = abi::resolve_type_by_mangled_name(result_named.as_bytes());
750        assert!(!result_meta.is_null(), "{}", mangle::demangle(&result_named));
751
752        // Both callbacks escape — SwiftUI keeps the button and calls them when
753        // the user taps it — so the payload and both contexts are leaked.
754        let payload = Box::leak(Box::new(ApplePayload {
755            on_request: Box::new(on_request),
756            on_completion: Box::new(on_completion),
757            result_meta,
758        })) as *mut ApplePayload as *mut c_void;
759        let req_ctx = Box::leak(Box::new(abi::ValueClosureCtx::new(apple_on_request, payload)))
760            as *mut abi::ValueClosureCtx;
761        let done_ctx =
762            Box::leak(Box::new(abi::ValueClosureCtx::new(apple_on_completion, payload)))
763                as *mut abi::ValueClosureCtx;
764
765        let mut result = vec![0u8; abi::type_bytes(meta)];
766        // x0 = @in Label, x1/x2 = onRequest, x3/x4 = onCompletion, x8 = @out.
767        core::arch::asm!("blr {f}", f = in(reg) resolve::siwa_button_init(),
768            in("x0") label_value.as_mut_ptr(),
769            in("x1") abi::value_closure_thunk as *const c_void,
770            in("x2") req_ctx,
771            in("x3") abi::value_closure_thunk as *const c_void,
772            in("x4") done_ctx,
773            in("x8") result.as_mut_ptr(),
774            lateout("x5") _, lateout("x6") _, lateout("x7") _,
775            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
776            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
777            lateout("x17") _, lateout("lr") _, lateout("x20") _, lateout("x21") _,
778            clobber_abi("C"),
779        );
780        wrap_owned(result, meta, wt)
781    }
782}
783
784/// `.signInWithAppleButtonStyle(_:)`.
785///
786/// Declared on `View` in the overlay, so its mangled name says `SwiftUI.View`
787/// with the overlay as the extension's module — which is why it resolves in the
788/// overlay's image and not in SwiftUI's.
789pub fn sign_in_with_apple_button_style(
790    view: &ViewHandle,
791    style: AppleButtonStyle,
792) -> ViewHandle {
793    unsafe {
794        let style_meta = abi::metadata0(resolve::siwa_style_ma());
795        assert!(!style_meta.is_null(), "SignInWithAppleButton.Style metadata");
796        let getter = resolve::sym_in(resolve::AUTH_SWIFTUI, style.getter());
797        assert!(!getter.is_null(), "{:?}", style.getter());
798        let mut value = static_getter_indirect(getter, style_meta, 0);
799
800        let (meta, wt, size) =
801            super::opaque_result_type(resolve::siwa_style_oret(), &[view.meta, view.wt]);
802        let mut result = vec![0u8; size];
803        let this = view.value_ptr();
804        // x0 = @in Style, x1 = Self.Type, x2 = Self: View, x8 = @out, x20 = self.
805        core::arch::asm!("blr {f}", f = in(reg) resolve::siwa_style_fn(),
806            in("x0") value.as_mut_ptr(),
807            in("x1") view.meta,
808            in("x2") view.wt,
809            in("x8") result.as_mut_ptr(),
810            in("x20") this,
811            lateout("x3") _, lateout("x4") _, lateout("x5") _, lateout("x6") _,
812            lateout("x7") _,
813            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
814            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
815            lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
816        );
817        wrap_owned(result, meta, wt)
818    }
819}