Skip to main content

swiftui_native/views/
interop.rs

1//! Shapes, images, URLs, defaults.
2
3use super::*;
4use crate::abi;
5use crate::resolve;
6use core::ffi::c_void;
7use crate::mangle::swift_type;
8
9// ── Shapes, images, URLs, defaults ──
10
11swift_cases! {
12    /// `SwiftUI.AccessibilityLabeledPairRole`.
13    pub enum LabeledPairRole {
14        Label = 0,
15        Content = 1,
16    }
17}
18
19/// Apply `.accessibilityLabeledPair(role:id:in:)`.
20pub fn accessibility_labeled_pair(
21    view: &ViewHandle,
22    role: LabeledPairRole,
23    id: HashableValue<'_>,
24) -> ViewHandle {
25    unsafe {
26        let meta = abi::metadata0(resolve::a11y_labeled_pair_role_ma());
27        let r = abi::enum_argument(meta, role.tag());
28        let (bytes, id_meta, wt) = id.parts();
29        generic1_modifier_ints(
30            resolve::a11y_labeled_pair_fn(),
31            resolve::a11y_labeled_pair_oret(),
32            view,
33            id_meta,
34            wt,
35            &[r, bytes.as_ptr() as u64, namespace_id()],
36            [0.0; 4],
37        )
38    }
39}
40
41/// Apply `.glassEffectID(_:in:)`.
42pub fn glass_effect_id(view: &ViewHandle, id: Option<HashableValue<'_>>) -> ViewHandle {
43    unsafe {
44        let probe = id.unwrap_or(HashableValue::Int(0));
45        let (bytes, meta, wt) = probe.parts();
46        let opt = abi::generic_metadata1(resolve::optional_ma(), meta);
47        let mut buf = vec![0u8; abi::type_bytes(opt)];
48        if id.is_some() {
49            buf[..bytes.len()].copy_from_slice(&bytes);
50        }
51        abi::store_optional_tag(&mut buf, meta, id.is_some());
52        // `Sendable` is a marker protocol: it constrains the signature but
53        // emits no witness table, so `Hashable`'s is the only one to pass.
54        generic1_modifier_ints(
55            resolve::glass_effect_id_fn(),
56            resolve::glass_effect_id_oret(),
57            view,
58            meta,
59            wt,
60            &[buf.as_ptr() as u64, namespace_id()],
61            [0.0; 4],
62        )
63    }
64}
65
66/// The one word a `SwiftUI.Image` occupies.
67///
68/// `Image` is frozen and wraps a single provider reference, so its initialiser
69/// returns it in x0 rather than through x8.
70pub(crate) unsafe fn image_word(name: &str) -> u64 {
71    unsafe {
72        let s = abi::swift_string(name);
73        abi::call_regs(
74            resolve::image_system_init(),
75            abi::ints(&[
76                u64::from_le_bytes(s[..8].try_into().expect("lo")),
77                u64::from_le_bytes(s[8..].try_into().expect("hi")),
78            ]),
79            [0.0; 4],
80            resolve::image_meta() as u64,
81        )[0]
82    }
83}
84
85/// The one word an `Image` built from a named asset occupies.
86///
87/// `Image(_ name: String, bundle: NSBundle?)`: the string in x0/x1, the bundle
88/// in x2 with nil as a null reference, and the metatype in x20.
89///
90/// The name is looked up in the bundle's compiled asset catalogue —
91/// `Assets.car` beside the executable for a plain binary. `NSImage.setName(_:)`
92/// puts an image in AppKit's named cache and `NSImage(named:)` finds it there,
93/// but SwiftUI does not consult that cache: an image registered that way
94/// renders as nothing at all.
95pub(crate) unsafe fn image_named_word(name: &str) -> u64 {
96    unsafe {
97        let s = abi::swift_string(name);
98        abi::call_regs(
99            resolve::image_named_init(),
100            abi::ints(&[
101                u64::from_le_bytes(s[..8].try_into().expect("lo")),
102                u64::from_le_bytes(s[8..].try_into().expect("hi")),
103                0, // bundle: nil — Bundle.main
104            ]),
105            [0.0; 4],
106            resolve::image_meta() as u64,
107        )[0]
108    }
109}
110
111/// Build a `SwiftUI.Image` from a named asset in the main bundle.
112pub fn image_named(name: &str) -> ViewHandle {
113    unsafe {
114        let word = image_named_word(name);
115        ViewHandle::wrap(
116            &word as *const u64 as *const c_void,
117            resolve::image_meta(),
118            resolve::image_wt(),
119        )
120    }
121}
122
123/// Build a `SwiftUI.Image` from an SF Symbol name.
124pub fn image_system(name: &str) -> ViewHandle {
125    unsafe {
126        let word = image_word(name);
127        ViewHandle::wrap(
128            &word as *const u64 as *const c_void,
129            resolve::image_meta(),
130            resolve::image_wt(),
131        )
132    }
133}
134
135/// The bytes of an `Image` built from an SF Symbol name.
136pub(crate) unsafe fn image_bytes(name: &str) -> Vec<u8> {
137    unsafe { image_word(name).to_le_bytes().to_vec() }
138}
139
140/// Apply `.dialogIcon(_:)`.
141pub fn dialog_icon(view: &ViewHandle, system_name: Option<&str>) -> ViewHandle {
142    unsafe {
143        // `Image` is one frozen word wrapping a provider reference, so
144        // `Image?` is the same word and `nil` is a null one — a register, not
145        // an address.
146        let word = system_name.map_or(0, |n| {
147            u64::from_le_bytes(image_bytes(n)[..8].try_into().expect("image word"))
148        });
149        opaque_modifier(
150            resolve::dialog_icon_fn(),
151            resolve::dialog_icon_oret(),
152            view,
153            &[word],
154            [0.0; 4],
155        )
156    }
157}
158
159/// Apply `.defaultAppStorage(_:)` with `UserDefaults.standard`.
160pub fn default_app_storage_standard(view: &ViewHandle) -> ViewHandle {
161    unsafe {
162        // `UserDefaults.standard` is an Objective-C class method, so it is a
163        // message send rather than a Swift symbol.
164        let cls = crate::objc::class_named(c"NSUserDefaults");
165        type Send0 = unsafe extern "C" fn(*mut c_void, *const c_void) -> *mut c_void;
166        let send: Send0 = core::mem::transmute(crate::objc::msg_send_addr());
167        let defaults = send(cls, crate::objc::sel(c"standardUserDefaults"));
168        assert!(!defaults.is_null(), "UserDefaults.standard");
169        opaque_modifier(
170            resolve::default_app_storage_fn(),
171            resolve::default_app_storage_oret(),
172            view,
173            &[defaults as u64],
174            [0.0; 4],
175        )
176    }
177}
178
179/// Apply `.defaultAppStorage(_:)` with a named suite.
180///
181/// The store every `@AppStorage` below this point reads and writes, when it
182/// should not be the app's own defaults — a shared container between an app
183/// and its extension is the usual reason.
184///
185/// Returns `None` if the suite cannot be opened. `UserDefaults(suiteName:)`
186/// fails for a name that is the app's own bundle identifier or the global
187/// domain, and returning nil is how it says so; a binding that unwrapped it
188/// would hand SwiftUI a null store.
189pub fn default_app_storage_suite(view: &ViewHandle, suite: &str) -> Option<ViewHandle> {
190    unsafe {
191        let cls = crate::objc::class_named(c"NSUserDefaults");
192        type Alloc = unsafe extern "C" fn(*mut c_void, *const c_void) -> *mut c_void;
193        type InitSuite =
194            unsafe extern "C" fn(*mut c_void, *const c_void, *mut c_void) -> *mut c_void;
195        let send_alloc: Alloc = core::mem::transmute(crate::objc::msg_send_addr());
196        let send_init: InitSuite = core::mem::transmute(crate::objc::msg_send_addr());
197        let raw = send_alloc(cls, crate::objc::sel(c"alloc"));
198        let name = crate::objc::nsstring(suite);
199        let defaults = send_init(raw, crate::objc::sel(c"initWithSuiteName:"), name);
200        if defaults.is_null() {
201            return None;
202        }
203        Some(opaque_modifier(
204            resolve::default_app_storage_fn(),
205            resolve::default_app_storage_oret(),
206            view,
207            &[defaults as u64],
208            [0.0; 4],
209        ))
210    }
211}
212
213/// A `Foundation.URL` for a file path.
214///
215/// None of `URL`'s initialisers are exported — they are all emitted into the
216/// client — so the value comes from an `NSURL` through Foundation's bridging
217/// entry point, which is exported.
218///
219/// `NSURL` stats the path: give it an existing directory and it appends a
220/// trailing slash where Swift's own initialiser does not, and the two URLs
221/// stop comparing equal.
222pub(crate) unsafe fn file_url(path: &str) -> Vec<u8> {
223    unsafe {
224        let ns_string = crate::objc::class_named(c"NSString");
225        type Send1<T> = unsafe extern "C" fn(*mut c_void, *const c_void, T) -> *mut c_void;
226        let send: Send1<*const i8> = core::mem::transmute(crate::objc::msg_send_addr());
227        let cpath = std::ffi::CString::new(path).expect("path");
228        let s = send(ns_string, crate::objc::sel(c"stringWithUTF8String:"), cpath.as_ptr());
229        let ns_url = crate::objc::class_named(c"NSURL");
230        let send_obj: Send1<*mut c_void> = core::mem::transmute(crate::objc::msg_send_addr());
231        let url = send_obj(ns_url, crate::objc::sel(c"fileURLWithPath:"), s);
232        assert!(!url.is_null(), "NSURL for {path}");
233
234        let meta = crate::type_by_name!(swift_type!(Foundation.URL).as_bytes());
235        let mut out = vec![0u8; abi::type_bytes(meta)];
236        abi::call_out(
237            resolve::url_bridge_from_objc(),
238            abi::ints(&[url as u64]),
239            [0.0; 4],
240            meta as u64,
241            out.as_mut_ptr(),
242        );
243        out
244    }
245}
246
247/// Apply `.fileDialogDefaultDirectory(_:)`.
248pub fn file_dialog_default_directory(view: &ViewHandle, path: Option<&str>) -> ViewHandle {
249    unsafe {
250        let payload = crate::type_by_name!(swift_type!(Foundation.URL).as_bytes());
251        let v = path.map(|p| file_url(p));
252        let b = opt_resilient_bytes(payload, swift_type!(opt Foundation.URL).as_bytes(), v.as_deref());
253        modifier_indirect(
254            resolve::file_dialog_default_directory_fn(),
255            resolve::file_dialog_default_directory_oret(),
256            view,
257            &b,
258        )
259    }
260}
261
262/// Apply `.previewDevice(_:)`.
263pub fn preview_device(view: &ViewHandle, name: Option<&str>) -> ViewHandle {
264    unsafe {
265        let meta = abi::metadata0(resolve::preview_device_ma());
266        let mut buf = vec![0u8; abi::type_bytes(meta)];
267        if let Some(n) = name {
268            let s = abi::swift_string(n);
269            abi::call_out(
270                resolve::preview_device_init(),
271                abi::ints(&[
272                    u64::from_le_bytes(s[..8].try_into().expect("lo")),
273                    u64::from_le_bytes(s[8..].try_into().expect("hi")),
274                ]),
275                [0.0; 4],
276                meta as u64,
277                buf.as_mut_ptr(),
278            );
279        }
280        let b = opt_resilient_bytes(meta, swift_type!(opt SwiftUI.PreviewDevice).as_bytes(), name.map(|_| &buf[..]));
281        modifier_indirect(resolve::preview_device_fn(), resolve::preview_device_oret(), view, &b)
282    }
283}
284
285#[cfg(test)]
286mod url_tests {
287    use super::*;
288
289    /// A bridged `URL` carries the right path even though its storage differs
290    /// from the one Swift source builds — `NSURL`-backed rather than
291    /// `_SwiftURL`-backed — which is why the parity suite only covers the
292    /// `nil` case of `fileDialogDefaultDirectory`.
293    #[test]
294    fn bridged_file_url_round_trips() {
295        unsafe {
296            let meta = crate::type_by_name!(swift_type!(Foundation.URL).as_bytes());
297            let mut bytes = file_url("/tmp/rswift-parity");
298            let d = crate::diag::dump_any(bytes.as_mut_ptr().cast(), meta, 1);
299            assert!(d.contains("file:///tmp/rswift-parity"), "bridged URL was {d}");
300        }
301    }
302
303    #[test]
304    fn file_dialog_default_directory_builds_either_way() {
305        let _ = file_dialog_default_directory(&text("Fx"), Some("/tmp/rswift-parity"));
306        let _ = file_dialog_default_directory(&text("Fx"), None);
307    }
308}
309
310/// A member of `AppKit`'s `NSTextContentType`.
311///
312/// These are `NSString` constants rather than Swift statics, so each is a
313/// *variable* the loader exports: `dlsym` gives its address and the string is
314/// one dereference further in.
315#[derive(Clone, Copy, Debug, PartialEq, Eq)]
316pub enum TextContentKind {
317    Username,
318    Password,
319    OneTimeCode,
320}
321
322impl TextContentKind {
323    fn constant(self) -> &'static core::ffi::CStr {
324        match self {
325            Self::Username => c"NSTextContentTypeUsername",
326            Self::Password => c"NSTextContentTypePassword",
327            Self::OneTimeCode => c"NSTextContentTypeOneTimeCode",
328        }
329    }
330}
331
332/// Apply `.textContentType(_:)`.
333pub fn text_content_type(view: &ViewHandle, kind: Option<TextContentKind>) -> ViewHandle {
334    unsafe {
335        // `NSTextContentType` is a typed `NSString`, so the optional is one
336        // reference and `nil` is a null word.
337        let word = kind.map_or(0, |k| {
338            let slot = resolve::sym(k.constant());
339            assert!(!slot.is_null(), "missing constant: {:?}", k.constant());
340            *(slot as *const u64)
341        });
342        opaque_modifier(
343            resolve::text_content_type_fn(),
344            resolve::text_content_type_oret(),
345            view,
346            &[word],
347            [0.0; 4],
348        )
349    }
350}
351
352// ── Images kept as images ───────────────────────────────────────────────────
353
354/// An `Image` value, not yet erased to a view.
355///
356/// The same reason [`super::Shape`] exists: `Image.resizable(_:_:)` and
357/// `.renderingMode(_:)` are methods on `Image` that *return* an `Image`, so
358/// they are not view modifiers and an `AnyView` cannot be their receiver. An
359/// `Image` is one word, so it travels in a register both ways.
360#[derive(Clone, Copy)]
361pub struct Image(u64);
362
363/// `Image.ResizingMode`.
364#[derive(Clone, Copy, Debug, PartialEq, Eq)]
365pub enum ResizingMode {
366    Tile,
367    Stretch,
368}
369
370impl ResizingMode {
371    /// The case descriptor the framework exports, which holds the tag.
372    fn descriptor(self) -> &'static core::ffi::CStr {
373        match self {
374            Self::Tile => c"$s7SwiftUI5ImageV12ResizingModeO4tileyA2EmFWC",
375            Self::Stretch => c"$s7SwiftUI5ImageV12ResizingModeO7stretchyA2EmFWC",
376        }
377    }
378}
379
380/// `Image.Interpolation` — how a scaled image is resampled.
381///
382/// Resilient, so the case numbering is not something a caller may write down:
383/// each variant names the `…WC` global SwiftUI exports for it, which holds the
384/// tag, and the binding reads it from there.
385#[derive(Clone, Copy, Debug, PartialEq, Eq)]
386pub enum Interpolation {
387    /// No smoothing — nearest neighbour, so pixels stay square.
388    None,
389    /// The cheapest smoothing.
390    Low,
391    /// Between `Low` and `High`.
392    Medium,
393    /// The best smoothing, and the default.
394    High,
395}
396
397impl Interpolation {
398    /// The case descriptor the framework exports, which holds the tag.
399    fn descriptor(self) -> &'static core::ffi::CStr {
400        match self {
401            Self::None => c"$s7SwiftUI5ImageV13InterpolationO4noneyA2EmFWC",
402            Self::Low => c"$s7SwiftUI5ImageV13InterpolationO3lowyA2EmFWC",
403            Self::Medium => c"$s7SwiftUI5ImageV13InterpolationO6mediumyA2EmFWC",
404            Self::High => c"$s7SwiftUI5ImageV13InterpolationO4highyA2EmFWC",
405        }
406    }
407}
408
409/// `Image.TemplateRenderingMode`.
410#[derive(Clone, Copy, Debug, PartialEq, Eq)]
411pub enum TemplateRenderingMode {
412    Template,
413    Original,
414}
415
416impl TemplateRenderingMode {
417    fn descriptor(self) -> &'static core::ffi::CStr {
418        match self {
419            Self::Template => c"$s7SwiftUI5ImageV21TemplateRenderingModeO8templateyA2EmFWC",
420            Self::Original => c"$s7SwiftUI5ImageV21TemplateRenderingModeO8originalyA2EmFWC",
421        }
422    }
423}
424
425impl Image {
426    /// `Image(systemName:)`
427    pub fn system(name: &str) -> Self {
428        Self(unsafe { image_word(name) })
429    }
430
431    /// `Image(systemName:variableValue:)` — a symbol drawn at a fill level.
432    ///
433    /// The `Double?` does not travel in a float register. It goes as its raw
434    /// bits in an integer register with the tag in the next one, which is what
435    /// the IR spells: `(i64, ptr, i64, i8)` for a `String` and an
436    /// `Optional<Double>`.
437    pub fn system_variable(name: &str, value: Option<f64>) -> Self {
438        unsafe {
439            let s = abi::swift_string(name);
440            let (bits, tag) = match value {
441                Some(v) => (v.to_bits(), 0u64),
442                None => (0, 1),
443            };
444            Self(
445                abi::call_regs(
446                    resolve::image_sysname_variable_init(),
447                    abi::ints(&[
448                        u64::from_le_bytes(s[..8].try_into().expect("lo")),
449                        u64::from_le_bytes(s[8..].try_into().expect("hi")),
450                        bits,
451                        tag,
452                    ]),
453                    [0.0; 4],
454                    resolve::image_meta() as u64,
455                )[0],
456            )
457        }
458    }
459
460    /// `.allowedDynamicRange(_:)` with `.high`.
461    ///
462    /// `Image.DynamicRange` is a resilient *struct*, so its size is a run-time
463    /// question and `.high` is handed back through x8 rather than in a
464    /// register. The `Optional` wrapper is the struct's own bytes in a buffer
465    /// the optional's size — zeroed, which is `.some` for a payload with spare
466    /// inhabitants — and it travels by address with the image beside it.
467    pub fn allowed_dynamic_range_high(self) -> Self {
468        unsafe {
469            let inner = abi::metadata0(resolve::image_dynamic_range_ma());
470            assert!(!inner.is_null(), "Image.DynamicRange metadata");
471            let value = super::static_getter_indirect(
472                resolve::image_dynamic_range_high(),
473                inner,
474                abi::type_bytes(inner),
475            );
476
477            let mut opt = vec![0u8; abi::type_bytes(inner) + 8];
478            opt[..value.len()].copy_from_slice(&value);
479            let out: u64;
480            core::arch::asm!("blr {f}", f = in(reg) resolve::image_dynamic_range_fn(),
481                in("x0") opt.as_mut_ptr(),
482                in("x1") self.0,
483                lateout("x0") out,
484                lateout("x2") _, lateout("x3") _, lateout("x4") _, lateout("x5") _,
485                lateout("x6") _, lateout("x7") _, lateout("x8") _, lateout("x9") _,
486                lateout("x10") _, lateout("x11") _, lateout("x12") _, lateout("x13") _,
487                lateout("x14") _, lateout("x15") _, lateout("x16") _, lateout("x17") _,
488                lateout("lr") _, lateout("x20") _, lateout("x21") _, clobber_abi("C"),
489            );
490            Self(out)
491        }
492    }
493
494    /// `Image(_:)` — a named asset from the main bundle's catalogue.
495    pub fn named(name: &str) -> Self {
496        Self(unsafe { image_named_word(name) })
497    }
498
499    /// `.resizable(capInsets:resizingMode:)`, with the zero cap insets Swift
500    /// source gets from the bare `.resizable()`.
501    ///
502    /// `EdgeInsets` is four `CGFloat`s and nothing else, which makes it a
503    /// homogeneous float aggregate: it travels in d0–d3 rather than by address.
504    /// The resizing mode does *not* travel in a register, though — see
505    /// [`Image::rendering_mode`].
506    pub fn resizable(self, mode: ResizingMode) -> Self {
507        unsafe {
508            let mut buf = resilient_case(
509                RESIZING_MODE,
510                mode.descriptor(),
511            );
512            let out: u64;
513            core::arch::asm!("blr {f}", f = in(reg) resolve::image_resizable_fn(),
514                in("d0") 0.0f64, in("d1") 0.0f64, in("d2") 0.0f64, in("d3") 0.0f64,
515                in("x0") buf.as_mut_ptr(),
516                in("x1") self.0,
517                lateout("x0") out,
518                lateout("x2") _, lateout("x3") _, lateout("x4") _, lateout("x5") _,
519                lateout("x6") _, lateout("x7") _, lateout("x8") _, lateout("x9") _,
520                lateout("x10") _, lateout("x11") _, lateout("x12") _, lateout("x13") _,
521                lateout("x14") _, lateout("x15") _, lateout("x16") _, lateout("x17") _,
522                lateout("lr") _, lateout("x20") _, lateout("x21") _, clobber_abi("C"),
523            );
524            Self(out)
525        }
526    }
527
528    /// `.interpolation(_:)` — how the image is resampled when scaled.
529    ///
530    /// The same argument convention as [`Image::resizable`]: the mode by address
531    /// in x0, the image itself as a plain second argument in x1 rather than in
532    /// x20, and the new image back in x0. Not what a method on a struct usually
533    /// looks like, which is why it is read out of the IR rather than assumed.
534    pub fn interpolation(self, mode: Interpolation) -> Self {
535        unsafe {
536            let mut buf = resilient_case(INTERPOLATION, mode.descriptor());
537            let out: u64;
538            core::arch::asm!("blr {f}", f = in(reg) resolve::image_interpolation_fn(),
539                in("x0") buf.as_mut_ptr(),
540                in("x1") self.0,
541                lateout("x0") out,
542                lateout("x2") _, lateout("x3") _, lateout("x4") _, lateout("x5") _,
543                lateout("x6") _, lateout("x7") _, lateout("x8") _, lateout("x9") _,
544                lateout("x10") _, lateout("x11") _, lateout("x12") _, lateout("x13") _,
545                lateout("x14") _, lateout("x15") _, lateout("x16") _, lateout("x17") _,
546                lateout("lr") _, lateout("x20") _, lateout("x21") _, clobber_abi("C"),
547            );
548            Self(out)
549        }
550    }
551
552    /// `.symbolRenderingMode(_:)` — the overload on `Image`, which returns
553    /// `Image`.
554    ///
555    /// Its `View` neighbour writes the environment and wraps the image in a
556    /// `ModifiedContent`; Apple's own listings reach this one, so a chain that
557    /// went through the environment rendered the same and typed differently.
558    /// Same shape as [`Image::rendering_mode`]: the mode is address-only and
559    /// `self` is an ordinary second argument, not a `swiftself`.
560    pub fn symbol_rendering_mode(self, mode: Option<super::SymbolRenderingMode>) -> Self {
561        unsafe {
562            let meta = abi::metadata0(resolve::symbol_rendering_mode_ma());
563            let v = mode.map(|m| super::resilient_static(meta, m.getter()));
564            let mut buf = super::opt_resilient_bytes(
565                meta,
566                swift_type!(opt SwiftUI.SymbolRenderingMode).as_bytes(),
567                v.as_deref(),
568            );
569            let out: u64;
570            core::arch::asm!("blr {f}", f = in(reg) resolve::image_symbol_rendering_mode_fn(),
571                in("x0") buf.as_mut_ptr(),
572                in("x1") self.0,
573                lateout("x0") out,
574                lateout("x2") _, lateout("x3") _, lateout("x4") _, lateout("x5") _,
575                lateout("x6") _, lateout("x7") _, lateout("x8") _, lateout("x9") _,
576                lateout("x10") _, lateout("x11") _, lateout("x12") _, lateout("x13") _,
577                lateout("x14") _, lateout("x15") _, lateout("x16") _, lateout("x17") _,
578                lateout("lr") _, lateout("x20") _, lateout("x21") _, clobber_abi("C"),
579            );
580            Self(out)
581        }
582    }
583
584    /// `.antialiased(_:)`
585    pub fn antialiased(self, on: bool) -> Self {
586        unsafe {
587            let out: u64;
588            core::arch::asm!("blr {f}", f = in(reg) resolve::image_antialiased_fn(),
589                in("x0") u64::from(on),
590                in("x1") self.0,
591                lateout("x0") out,
592                lateout("x2") _, lateout("x3") _, lateout("x4") _, lateout("x5") _,
593                lateout("x6") _, lateout("x7") _, lateout("x8") _, lateout("x9") _,
594                lateout("x10") _, lateout("x11") _, lateout("x12") _, lateout("x13") _,
595                lateout("x14") _, lateout("x15") _, lateout("x16") _, lateout("x17") _,
596                lateout("lr") _, lateout("x20") _, lateout("x21") _, clobber_abi("C"),
597            );
598            Self(out)
599        }
600    }
601
602    /// `.renderingMode(_:)`
603    ///
604    /// Neither of these two takes its enum in a register, and neither takes
605    /// `self` in x20. `Image.TemplateRenderingMode` is not `@frozen`, so its
606    /// size is only known at run time and the argument is address-only; and the
607    /// IR shows plain pointers rather than a `swiftself`, so the receiver is an
608    /// ordinary second argument. Passing the case number in x0 and the image in
609    /// x20 — which is what every *view* modifier in this crate wants — faults
610    /// inside the runtime.
611    pub fn rendering_mode(self, mode: Option<TemplateRenderingMode>) -> Self {
612        unsafe {
613            let mangled = TEMPLATE_RENDERING_MODE;
614            let mut buf = match mode {
615                Some(m) => resilient_case(mangled, m.descriptor()),
616                None => {
617                    // `nil` is an extra inhabitant of the payload, so it is
618                    // written by the *optional's* witness rather than injected.
619                    let mut opt = mangled.to_vec();
620                    opt.extend_from_slice(b"Sg");
621                    let meta = abi::resolve_type_by_mangled_name(&opt);
622                    assert!(!meta.is_null(), "Optional<TemplateRenderingMode>");
623                    let mut buf = vec![0u8; abi::type_bytes(meta).max(1)];
624                    abi::inject_enum_tag(&mut buf, meta, 1);
625                    buf
626                }
627            };
628            let out: u64;
629            core::arch::asm!("blr {f}", f = in(reg) resolve::image_rendering_mode_fn(),
630                in("x0") buf.as_mut_ptr(),
631                in("x1") self.0,
632                lateout("x0") out,
633                lateout("x2") _, lateout("x3") _, lateout("x4") _, lateout("x5") _,
634                lateout("x6") _, lateout("x7") _, lateout("x8") _, lateout("x9") _,
635                lateout("x10") _, lateout("x11") _, lateout("x12") _, lateout("x13") _,
636                lateout("x14") _, lateout("x15") _, lateout("x16") _, lateout("x17") _,
637                lateout("lr") _, lateout("x20") _, lateout("x21") _, clobber_abi("C"),
638            );
639            Self(out)
640        }
641    }
642
643    /// The image as a view.
644    pub fn view(self) -> ViewHandle {
645        unsafe {
646            ViewHandle::wrap(
647                &self.0 as *const u64 as *const c_void,
648                resolve::image_meta(),
649                resolve::image_wt(),
650            )
651        }
652    }
653}
654
655/// `SwiftUI.Image.ResizingMode` and `SwiftUI.Image.TemplateRenderingMode`.
656///
657/// Written out rather than built, because both are *enums* nested in a struct
658/// and the `O` suffix is what says so — `swift_type!` spells a nominal type as
659/// a struct, and `...ModeV` resolves to nothing at all.
660const RESIZING_MODE: &[u8] = b"7SwiftUI5ImageV12ResizingModeO";
661const INTERPOLATION: &[u8] = b"7SwiftUI5ImageV13InterpolationO";
662const TEMPLATE_RENDERING_MODE: &[u8] = b"7SwiftUI5ImageV21TemplateRenderingModeO";
663
664/// A buffer holding one case of a resilient enum.
665///
666/// The case *number* is not the declaration order and is not knowable here:
667/// the compiler loads it from a per-case descriptor the framework exports
668/// (`...WC`), then hands it to the enum's own `destructiveInjectEnumTag`. Both
669/// steps are needed — the size comes from the value witness table too.
670unsafe fn resilient_case(mangled: &[u8], case_descriptor: &'static core::ffi::CStr) -> Vec<u8> {
671    unsafe {
672        let meta = abi::resolve_type_by_mangled_name(mangled);
673        assert!(!meta.is_null(), "{}", crate::diag::demangle_type(mangled));
674        let tag = resolve::sym(case_descriptor) as *const u32;
675        assert!(!tag.is_null(), "enum case descriptor: {case_descriptor:?}");
676        let mut buf = vec![0u8; abi::type_bytes(meta).max(1)];
677        abi::inject_enum_tag(&mut buf, meta, tag.read());
678        buf
679    }
680}
681
682/// `AsyncImage(url:scale:)` — the `Content == Image` form.
683///
684/// The `Optional<URL>` travels by address in x0 and the scale in d0, with the
685/// metatype in x20. A URL that does not resolve leaves the default placeholder
686/// on screen, which is what both sides show in a test with no network.
687pub fn async_image(spec: Option<&str>, scale: f64) -> ViewHandle {
688    unsafe {
689        let ty = crate::mangle::ty("AsyncImage").of(vec![crate::mangle::ty("Image")]).mangle();
690        let meta = abi::resolve_type_by_mangled_name(ty.as_bytes());
691        assert!(!meta.is_null(), "AsyncImage<Image> metadata");
692        let mut url = super::optional_url(spec);
693        let mut value = vec![0u8; abi::type_bytes(meta)];
694        core::arch::asm!("blr {f}", f = in(reg) resolve::async_image_init(),
695            in("x0") url.as_mut_ptr(),
696            in("d0") scale,
697            in("x8") value.as_mut_ptr(),
698            in("x20") meta,
699            lateout("x1") _, lateout("x2") _, lateout("x3") _, lateout("x4") _,
700            lateout("x5") _, lateout("x6") _, lateout("x7") _, lateout("x9") _,
701            lateout("x10") _, lateout("x11") _, lateout("x12") _, lateout("x13") _,
702            lateout("x14") _, lateout("x15") _, lateout("x16") _, lateout("x17") _,
703            lateout("lr") _, lateout("x21") _, clobber_abi("C"),
704        );
705        let wt = abi::get_view_wt(meta);
706        assert!(!wt.is_null(), "AsyncImage !: View");
707        wrap_buf(&value, meta, wt)
708    }
709}
710
711/// `ShareLink(item:)` for a URL.
712///
713/// The initialiser is `ShareLink(item:subject:message:)` with both trailing
714/// arguments defaulted to nil, and a nil `Text?` is simply four zero words —
715/// eight in total for the pair, of which seven fit in x1 through x7 and one
716/// spills. The item itself goes by address in x0: a `CollectionOfOne<URL>` is
717/// laid out as the `URL` it holds, so the payload of the optional this builds
718/// serves directly.
719pub fn share_link_url(spec: &str) -> ViewHandle {
720    unsafe {
721        let ty = crate::mangle::ty("ShareLink")
722            .of(vec![
723                crate::mangle::Type::Verbatim("s15CollectionOfOneVy10Foundation3URLVG".into()),
724                crate::mangle::Type::Verbatim("s5NeverO".into()),
725                crate::mangle::Type::Verbatim("s5NeverO".into()),
726                crate::mangle::ty("DefaultShareLinkLabel"),
727            ])
728            .mangle();
729        let meta = abi::resolve_type_by_mangled_name(ty.as_bytes());
730        assert!(!meta.is_null(), "ShareLink metadata: {ty}");
731
732        let mut url = super::optional_url(Some(spec));
733        let mut value = vec![0u8; abi::type_bytes(meta)];
734        core::arch::asm!(
735            "str xzr, [sp, #-16]!",
736            "blr {f}",
737            "add sp, sp, #16",
738            f = in(reg) resolve::share_link_url_init(),
739            in("x0") url.as_mut_ptr(),
740            in("x1") 0u64, in("x2") 0u64, in("x3") 0u64, in("x4") 0u64,
741            in("x5") 0u64, in("x6") 0u64, in("x7") 0u64,
742            in("x8") value.as_mut_ptr(),
743            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
744            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
745            lateout("x17") _, lateout("lr") _, lateout("x20") _, lateout("x21") _,
746            clobber_abi("C"),
747        );
748        let wt = abi::get_view_wt(meta);
749        assert!(!wt.is_null(), "ShareLink !: View");
750        wrap_buf(&value, meta, wt)
751    }
752}