Skip to main content

swiftui_native/views/
stacks.rs

1//! Stacks.
2//!
3//! * Label and lazy stacks
4
5use super::*;
6use crate::abi;
7use crate::resolve;
8use core::ffi::c_void;
9use crate::mangle::{self, swift_type};
10
11// ── Stacks ──
12
13#[derive(Clone, Copy, PartialEq, Eq)]
14pub(crate) enum StackKind {
15    V,
16    H,
17    Z,
18}
19
20/// `VStack` from children.
21pub fn vstack(children: &[ViewHandle]) -> ViewHandle {
22    stack_impl(children, StackKind::V)
23}
24
25/// `HStack` from children.
26pub fn hstack(children: &[ViewHandle]) -> ViewHandle {
27    stack_impl(children, StackKind::H)
28}
29
30/// `ZStack` from children, centred.
31pub fn zstack(children: &[ViewHandle]) -> ViewHandle {
32    stack_impl(children, StackKind::Z)
33}
34
35/// `ZStack(alignment:)` from children.
36pub fn zstack_aligned(children: &[ViewHandle], alignment: super::Alignment) -> ViewHandle {
37    match children.len() {
38        0 => empty_view(),
39        _ => unsafe { build_stack_aligned(children, StackKind::Z, None, alignment) },
40    }
41}
42
43pub(crate) fn stack_impl(children: &[ViewHandle], axis: StackKind) -> ViewHandle {
44    stack_spaced(children, axis, None)
45}
46
47pub(crate) fn stack_spaced(children: &[ViewHandle], axis: StackKind, spacing: Option<f64>) -> ViewHandle {
48    match children.len() {
49        0 => empty_view(),
50        // A lone child is passed through rather than wrapped. Note this is not
51        // identical to Swift's `VStack { x }`: a `ForEach` that is the *only*
52        // child needs a real stack around it, or its rows overlay each other.
53        // Give such a stack a second child (a `spacer`, say).
54        1 if spacing.is_none() => children[0].clone(),
55        _ => unsafe { build_stack(children, axis, spacing) },
56    }
57}
58
59/// `VStack(alignment:)` from children.
60pub fn vstack_aligned(
61    children: &[ViewHandle],
62    alignment: super::HorizontalAlignment,
63) -> ViewHandle {
64    vstack_aligned_spacing(children, alignment, None)
65}
66
67/// `VStack(alignment:spacing:)` from children.
68pub fn vstack_aligned_spacing(
69    children: &[ViewHandle],
70    alignment: super::HorizontalAlignment,
71    spacing: Option<f64>,
72) -> ViewHandle {
73    unsafe {
74        build_stack_aligned_h(
75            children,
76            StackKind::V,
77            spacing,
78            super::Alignment::Center,
79            Some(alignment),
80            None,
81        )
82    }
83}
84
85/// `HStack(alignment:)` from children.
86pub fn hstack_aligned(
87    children: &[ViewHandle],
88    alignment: super::VerticalAlignment,
89) -> ViewHandle {
90    hstack_aligned_spacing(children, alignment, None)
91}
92
93/// `HStack(alignment:spacing:)` from children.
94pub fn hstack_aligned_spacing(
95    children: &[ViewHandle],
96    alignment: super::VerticalAlignment,
97    spacing: Option<f64>,
98) -> ViewHandle {
99    unsafe {
100        build_stack_aligned_h(
101            children,
102            StackKind::H,
103            spacing,
104            super::Alignment::Center,
105            None,
106            Some(alignment),
107        )
108    }
109}
110
111/// `VStack(spacing:)` from children.
112pub fn vstack_spacing(children: &[ViewHandle], spacing: f64) -> ViewHandle {
113    stack_spaced(children, StackKind::V, Some(spacing))
114}
115
116/// `HStack(spacing:)` from children.
117pub fn hstack_spacing(children: &[ViewHandle], spacing: f64) -> ViewHandle {
118    stack_spaced(children, StackKind::H, Some(spacing))
119}
120
121/// One child of a `@ViewBuilder` body: its bytes, its type, and where in the
122/// tuple the runtime decided to put it.
123#[repr(C)]
124pub(crate) struct TupleChild {
125    pub(crate) value: *const c_void,
126    pub(crate) meta: *const c_void,
127    pub(crate) offset: usize,
128}
129
130/// What the content closure needs to emit the tuple.
131#[repr(C)]
132pub(crate) struct TuplePayload {
133    pub(crate) children: *const TupleChild,
134    pub(crate) count: usize,
135}
136
137/// `@ViewBuilder content` body — writes `TupleView<(A, B, ...)>` to `out`.
138///
139/// One `initialize_with_copy` per child, at the offset the tuple's own metadata
140/// gives. That is the general form of the `swift_retain`-per-word this used to
141/// do: a child is a value of its own type now, not a reference to an `AnyView`
142/// box, so copying it means asking its value witness rather than assuming one
143/// word and one reference count.
144///
145/// The stack takes ownership of what the closure returns, so each child is
146/// copied on the way out and the caller keeps its own.
147pub(crate) unsafe extern "C" fn emit_tuple(out: *mut u8, payload: *mut c_void) {
148    let p = unsafe { &*(payload as *const TuplePayload) };
149    for i in 0..p.count {
150        let c = unsafe { &*p.children.add(i) };
151        let vwt = unsafe { &*swift_runtime_sys::SwiftABI::get_value_witness_table(c.meta) };
152        if vwt.size > 0 {
153            unsafe {
154                (vwt.initialize_with_copy)(
155                    out.add(c.offset) as *mut c_void,
156                    c.value as *mut c_void,
157                    c.meta,
158                );
159            }
160        }
161    }
162}
163
164/// How a container names the type of its content.
165///
166/// This is not a style choice — it follows from how the container's own type is
167/// obtained. A container composed from its content's metadata (`VStack`,
168/// `ScrollView`, the content-carrying modifiers) can hold the child at its own
169/// type. One named by a fixed mangled string — `Grid<AnyView>`, `Menu<AnyView>`
170/// — has `AnyView` written into that name, so the child has to be boxed to
171/// match it. Passing a concrete child to such a container describes a value the
172/// callee was never given, and it reads whatever is there.
173///
174/// Choosing wrongly is not a compile error, so the two cases are named here
175/// rather than left to whoever is reading the call site.
176#[derive(Clone, Copy, PartialEq, Eq)]
177pub(crate) enum Content {
178    /// The container composes its type from the child's; keep the child's type.
179    Concrete,
180    /// The container's type names `AnyView`; box the child to match.
181    Erased,
182}
183
184/// One child of a `@ViewBuilder` body, held for as long as the closure needs it.
185///
186/// A non-escaping `content:` — the kind an initialiser calls before it returns —
187/// could borrow, but an escaping one is called long after the binding returned.
188/// Holding the value here covers both, and the boxed word for [`Content::Erased`]
189/// needs an owner regardless.
190pub(crate) struct Child {
191    /// Kept alive for `Concrete`: the closure copies out of these bytes.
192    handle: ViewHandle,
193    /// Kept alive for `Erased`: the `AnyView` the closure copies.
194    word: u64,
195    kind: Content,
196}
197
198impl Child {
199    pub(crate) fn new(view: &ViewHandle, kind: Content) -> Self {
200        Child {
201            handle: view.clone(),
202            word: if kind == Content::Erased { view.to_anyview_word() } else { 0 },
203            kind,
204        }
205    }
206
207    /// The tuple element describing this child.
208    pub(crate) fn element(&self) -> [TupleChild; 1] {
209        match self.kind {
210            Content::Concrete => [TupleChild {
211                value: self.handle.value_ptr(),
212                meta: self.handle.meta,
213                offset: 0,
214            }],
215            Content::Erased => [TupleChild {
216                value: &self.word as *const u64 as *const c_void,
217                meta: resolve::anyview_meta(),
218                offset: 0,
219            }],
220        }
221    }
222
223    /// Leak a payload for a closure SwiftUI stores, and return the pointer a
224    /// `ClosureCtx` wants.
225    pub(crate) fn leak(view: &ViewHandle, kind: Content) -> *mut c_void {
226        let kept: &'static Child = Box::leak(Box::new(Child::new(view, kind)));
227        let kids = Box::leak(Box::new(kept.element()));
228        let payload = Box::leak(Box::new(TuplePayload { children: kids.as_ptr(), count: 1 }));
229        payload as *mut TuplePayload as *mut c_void
230    }
231}
232
233/// Read a Swift static property getter that returns a `Double` (in d0).
234pub(crate) unsafe fn static_getter_f64(getter: *const c_void, meta: *const c_void) -> f64 {
235    unsafe { abi::call_f64(getter, [0; 8], [0.0; 4], meta as u64) }
236}
237
238/// Read a Swift static property getter that returns up to two words.
239pub(crate) unsafe fn static_getter(getter: *const c_void, meta: *const c_void) -> (u64, u64) {
240    let r = unsafe { abi::static_regs(getter, meta) };
241    (r[0], r[1])
242}
243
244/// What a `@ViewBuilder` body of `children` *is*, as a type.
245///
246/// `ViewBuilder` collapses a single element to the element itself, and two or
247/// more to a `TupleView` of them — so `VStack { Text("a") }` is a
248/// `VStack<Text>` and `VStack { Text("a"); Text("b") }` a
249/// `VStack<TupleView<(Text, Text)>>`. Returns the content's metadata, its `View`
250/// conformance, and where in a tuple value each child goes.
251///
252/// The elements are the children's own types. Spelling them `AnyView` here built
253/// a real tuple of real boxes — it rendered, but it described a
254/// `TupleView<(AnyView, AnyView)>` where the same Swift source builds
255/// `TupleView<(Text, Image)>`, and no amount of care further up recovers a type
256/// thrown away at the leaves.
257///
258/// The offsets are asked for rather than packed by hand: the runtime orders and
259/// pads tuple elements by alignment, and children of their own types are not
260/// uniformly one word.
261///
262/// # Panics
263/// With no children at all — an empty `@ViewBuilder` body is `EmptyView` and has
264/// no tuple, which is a different construction and a caller's decision.
265pub(crate) unsafe fn tuple_content(
266    children: &[ViewHandle],
267) -> (*const c_void, *const c_void, Vec<usize>) {
268    assert!(!children.is_empty(), "a `@ViewBuilder` body with no children is `EmptyView`");
269    if children.len() == 1 {
270        return (children[0].meta, children[0].wt, vec![0usize]);
271    }
272    unsafe {
273        let elements: Vec<*const c_void> = children.iter().map(|c| c.meta).collect();
274        let tuple_meta = abi::tuple_metadata(&elements);
275        assert!(!tuple_meta.is_null(), "tuple metadata instantiation failed");
276        let offsets = abi::tuple_element_offsets(tuple_meta, children.len());
277        let m = abi::generic_metadata1(resolve::tupleview_ma(), tuple_meta);
278        assert!(!m.is_null(), "TupleView metadata instantiation failed");
279        let w = abi::get_view_wt(m);
280        assert!(!w.is_null(), "{} !: View", crate::diag::type_name(m));
281        (m, w, offsets)
282    }
283}
284
285/// The tuple elements describing `children`, laid out at `offsets`.
286///
287/// Borrowed rather than leaked, so the caller must keep the children alive until
288/// the closure that reads this has run. For a non-escaping `content:` — the kind
289/// an initialiser calls before it returns — that is the enclosing scope.
290pub(crate) fn tuple_children(children: &[ViewHandle], offsets: &[usize]) -> Vec<TupleChild> {
291    children
292        .iter()
293        .zip(offsets)
294        .map(|(c, &offset)| TupleChild { value: c.value_ptr(), meta: c.meta, offset })
295        .collect()
296}
297
298/// Build a real `VStack`/`HStack`/`ZStack` over N children.
299///
300/// The content is a genuine `TupleView` with one element per child, so the stack
301/// is flat — not a chain of nested two-child stacks.
302pub(crate) unsafe fn build_stack(
303    children: &[ViewHandle],
304    axis: StackKind,
305    spacing: Option<f64>,
306) -> ViewHandle {
307    unsafe { build_stack_aligned(children, axis, spacing, super::Alignment::Center) }
308}
309
310/// As [`build_stack`], with the alignment spelled out.
311///
312/// Only `ZStack` reads it: `VStack` and `HStack` take a `HorizontalAlignment`
313/// and a `VerticalAlignment` respectively, which is a different argument from
314/// the two-word `Alignment` a `ZStack` wants.
315pub(crate) unsafe fn build_stack_aligned(
316    children: &[ViewHandle],
317    axis: StackKind,
318    spacing: Option<f64>,
319    alignment: super::Alignment,
320) -> ViewHandle {
321    unsafe { build_stack_aligned_h(children, axis, spacing, alignment, None, None) }
322}
323
324/// As [`build_stack_aligned`], and for a `VStack` also its `HorizontalAlignment`.
325///
326/// A `VStack` takes a `HorizontalAlignment` and an `HStack` a
327/// `VerticalAlignment` — one word each, and a different argument from the
328/// two-word `Alignment` a `ZStack` wants, which is why this is separate.
329pub(crate) unsafe fn build_stack_aligned_h(
330    children: &[ViewHandle],
331    axis: StackKind,
332    spacing: Option<f64>,
333    alignment: super::Alignment,
334    horizontal: Option<super::HorizontalAlignment>,
335    vertical: Option<super::VerticalAlignment>,
336) -> ViewHandle {
337    let (content_meta, content_wt, offsets) = unsafe { tuple_content(children) };
338
339    // Stack type: VStack<Content> — needs both Content.Type and Content: View.
340    let stack_ma = match axis {
341        StackKind::V => resolve::vstack_ma(),
342        StackKind::H => resolve::hstack_ma(),
343        StackKind::Z => resolve::zstack_ma(),
344    };
345    let stack_meta = unsafe { abi::generic_metadata2(stack_ma, content_meta, content_wt) };
346    assert!(!stack_meta.is_null(), "stack metadata instantiation failed");
347    let stack_wt = unsafe { abi::get_view_wt(stack_meta) };
348    assert!(!stack_wt.is_null(), "{} !: View", crate::diag::type_name(stack_meta));
349
350    // Borrowed, not leaked: a stack's `content` closure is called by the
351    // initialiser before it returns, so the children outlive the call.
352    let kids = tuple_children(children, &offsets);
353    let payload = TuplePayload { children: kids.as_ptr(), count: kids.len() };
354    let mut ctx = abi::ClosureCtx::new(emit_tuple, &payload as *const TuplePayload as *mut c_void);
355    let thunk = abi::swift_closure_thunk as *const c_void;
356
357    let mut result = vec![0u8; unsafe { abi::type_bytes(stack_meta) }];
358
359    match axis {
360        StackKind::V | StackKind::H => {
361            // init(alignment:spacing:content:)
362            //   x0 = alignment, x1 = spacing payload, x2 = spacing tag (1 = nil),
363            //   x3 = closure fn, x4 = closure ctx,
364            //   x5 = Content.Type, x6 = Content: View WT, x8 = @out
365            let (align, _) = unsafe {
366                match axis {
367                    StackKind::V => static_getter(
368                        horizontal.map_or_else(resolve::halign_center, |h| h.resolved()),
369                        resolve::halign_meta(),
370                    ),
371                    _ => static_getter(
372                        vertical.map_or_else(resolve::valign_center, |v| v.resolved()),
373                        resolve::valign_meta(),
374                    ),
375                }
376            };
377            let init = if axis == StackKind::V { resolve::vstack_init() } else { resolve::hstack_init() };
378            // Optional<CGFloat>: raw bits in one word, tag in the next (1 == nil).
379            let (spacing_bits, spacing_tag) = match spacing {
380                Some(v) => (v.to_bits(), 0u64),
381                None => (0u64, 1u64),
382            };
383            unsafe {
384                core::arch::asm!("blr {f}", f = in(reg) init,
385                    in("x0") align,
386                    in("x1") spacing_bits,
387                    in("x2") spacing_tag,
388                    in("x3") thunk,
389                    in("x4") &mut ctx as *mut abi::ClosureCtx,
390                    in("x5") content_meta,
391                    in("x6") content_wt,
392                    in("x8") result.as_mut_ptr(),
393                    lateout("x0") _, lateout("x1") _, lateout("x2") _, lateout("x3") _,
394                    lateout("x4") _, lateout("x5") _, lateout("x6") _, lateout("x7") _,
395                    lateout("x9") _, lateout("x10") _, lateout("x11") _,
396                    lateout("x12") _, lateout("x13") _, lateout("x14") _, lateout("x15") _,
397                    lateout("x16") _, lateout("x17") _, lateout("lr") _,
398                    lateout("x20") _, lateout("x21") _,
399                    clobber_abi("C"),
400                );
401            }
402        }
403        StackKind::Z => {
404            // init(alignment:content:)
405            //   x0 = alignment.horizontal, x1 = alignment.vertical,
406            //   x2 = closure fn, x3 = closure ctx,
407            //   x4 = Content.Type, x5 = Content: View WT, x8 = @out
408            let [h, v] = unsafe { alignment.words() };
409            unsafe {
410                core::arch::asm!("blr {f}", f = in(reg) resolve::zstack_init(),
411                    in("x0") h,
412                    in("x1") v,
413                    in("x2") thunk,
414                    in("x3") &mut ctx as *mut abi::ClosureCtx,
415                    in("x4") content_meta,
416                    in("x5") content_wt,
417                    in("x8") result.as_mut_ptr(),
418                    lateout("x6") _, lateout("x7") _,
419                    lateout("x9") _, lateout("x10") _, lateout("x11") _,
420                    lateout("x12") _, lateout("x13") _, lateout("x14") _, lateout("x15") _,
421                    lateout("x16") _, lateout("x17") _, lateout("lr") _,
422                    lateout("x20") _, lateout("x21") _,
423                    clobber_abi("C"),
424                );
425            }
426        }
427    }
428
429    unsafe { wrap_owned(result, stack_meta, stack_wt) }
430}
431
432// ── Label and lazy stacks ──
433
434mangle::result_types! {
435    /// `SwiftUI.Label<SwiftUI.Text, SwiftUI.Image>`
436    /// `Label<Text, Image>`
437    pub(crate) LABEL_T = ty("Label").of(vec![ty("Text"), ty("Image")]);
438}
439
440/// Create a `Label(_:systemImage:)`.
441pub fn label(title: &str, system_image: &str) -> ViewHandle {
442    unsafe {
443        let ss = abi::swift_string(title);
444        let s0 = u64::from_le_bytes(ss[..8].try_into().unwrap());
445        let s1 = u64::from_le_bytes(ss[8..].try_into().unwrap());
446        let (k0, k1, k2, k3) = localized_string_key(s0, s1);
447
448        let img = abi::swift_string(system_image);
449        let i0 = u64::from_le_bytes(img[..8].try_into().unwrap());
450        let i1 = u64::from_le_bytes(img[8..].try_into().unwrap());
451
452        let (meta, wt, size) = result_type(&LABEL_T);
453        let mut result = vec![0u8; size];
454        // Label.init(_: LocalizedStringKey, systemImage: String)
455        //   x0-x3 = key, x4/x5 = image name, x8 = @out
456        core::arch::asm!("blr {f}", f = in(reg) resolve::label_init(),
457            in("x0") k0, in("x1") k1, in("x2") k2, in("x3") k3,
458            in("x4") i0, in("x5") i1,
459            in("x8") result.as_mut_ptr(),
460            lateout("x6") _, lateout("x7") _,
461            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
462            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
463            lateout("x17") _, lateout("lr") _, lateout("x20") _, lateout("x21") _,
464            clobber_abi("C"),
465        );
466        wrap_owned(result, meta, wt)
467    }
468}
469
470/// Build a `LazyVStack` or `LazyHStack` over N children.
471pub(crate) fn lazy_stack_impl(children: &[ViewHandle], vertical: bool) -> ViewHandle {
472    lazy_stack_aligned_impl(children, vertical, None, None)
473}
474
475/// As [`lazy_stack_impl`], with the cross-axis alignment named. Only the one
476/// matching the stack's axis is read; the other is ignored.
477pub(crate) fn lazy_stack_aligned_impl(
478    children: &[ViewHandle],
479    vertical: bool,
480    halign: Option<super::HorizontalAlignment>,
481    valign: Option<super::VerticalAlignment>,
482) -> ViewHandle {
483    if children.is_empty() {
484        return empty_view();
485    }
486    unsafe {
487        // Same rule as `build_stack_aligned_h`: the children's own types, and a
488        // single child is the content rather than a one-element tuple.
489        let (content_meta, content_wt, offsets) = if children.len() == 1 {
490            (children[0].meta, children[0].wt, vec![0usize])
491        } else {
492            let elements: Vec<*const c_void> = children.iter().map(|c| c.meta).collect();
493            let tuple_meta = abi::tuple_metadata(&elements);
494            let offsets = abi::tuple_element_offsets(tuple_meta, children.len());
495            let m = abi::generic_metadata1(resolve::tupleview_ma(), tuple_meta);
496            let w = abi::get_view_wt(m);
497            assert!(!w.is_null(), "TupleView !: View");
498            (m, w, offsets)
499        };
500
501        let ma = if vertical { resolve::lazy_vstack_ma() } else { resolve::lazy_hstack_ma() };
502        let stack_meta = abi::generic_metadata2(ma, content_meta, content_wt);
503        let stack_wt = abi::get_view_wt(stack_meta);
504        assert!(!stack_wt.is_null(), "lazy stack !: View");
505
506        let kids: Vec<TupleChild> = children
507            .iter()
508            .zip(&offsets)
509            .map(|(c, &offset)| TupleChild { value: c.value_ptr(), meta: c.meta, offset })
510            .collect();
511        let payload = TuplePayload { children: kids.as_ptr(), count: kids.len() };
512        let mut ctx =
513            abi::ClosureCtx::new(emit_tuple, &payload as *const TuplePayload as *mut c_void);
514
515        let (align, _) = if vertical {
516            static_getter(
517                halign.map_or_else(resolve::halign_center, |h| h.resolved()),
518                resolve::halign_meta(),
519            )
520        } else {
521            static_getter(
522                valign.map_or_else(resolve::valign_center, |v| v.resolved()),
523                resolve::valign_meta(),
524            )
525        };
526        let init = if vertical { resolve::lazy_vstack_init() } else { resolve::lazy_hstack_init() };
527
528        // `PinnedScrollableViews` is a resilient OptionSet, so it is
529        // address-only and passed by pointer; all-zero bytes mean "empty".
530        let pinned_meta = crate::type_by_name!(swift_type!(SwiftUI.PinnedScrollableViews).as_bytes());
531        assert!(!pinned_meta.is_null(), "PinnedScrollableViews metadata");
532        let mut pinned = vec![0u8; abi::type_bytes(pinned_meta)];
533
534        let mut result = vec![0u8; abi::type_bytes(stack_meta)];
535        // init(alignment:spacing:pinnedViews:content:)
536        //   x0 = alignment, x1 = spacing payload, x2 = spacing tag (1 = nil),
537        //   x3 = @in pinnedViews, x4 = closure fn, x5 = closure ctx,
538        //   x6 = Content.Type, x7 = Content: View WT, x8 = @out
539        core::arch::asm!("blr {f}", f = in(reg) init,
540            in("x0") align,
541            in("x1") 0u64,
542            in("x2") 1u64,
543            in("x3") pinned.as_mut_ptr(),
544            in("x4") abi::swift_closure_thunk as *const c_void,
545            in("x5") &mut ctx as *mut abi::ClosureCtx,
546            in("x6") content_meta,
547            in("x7") content_wt,
548            in("x8") result.as_mut_ptr(),
549            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
550            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
551            lateout("x17") _, lateout("lr") _, lateout("x20") _, lateout("x21") _,
552            clobber_abi("C"),
553        );
554        wrap_owned(result, stack_meta, stack_wt)
555    }
556}
557
558/// `LazyVStack` from children.
559pub fn lazy_vstack(children: &[ViewHandle]) -> ViewHandle {
560    lazy_stack_impl(children, true)
561}
562
563/// `LazyVStack(alignment:)` from children.
564pub fn lazy_vstack_aligned(
565    children: &[ViewHandle],
566    alignment: super::HorizontalAlignment,
567) -> ViewHandle {
568    lazy_stack_aligned_impl(children, true, Some(alignment), None)
569}
570
571/// `LazyHStack` from children.
572pub fn lazy_hstack(children: &[ViewHandle]) -> ViewHandle {
573    lazy_stack_impl(children, false)
574}
575
576/// `LazyHStack(alignment:)` from children.
577pub fn lazy_hstack_aligned(
578    children: &[ViewHandle],
579    alignment: super::VerticalAlignment,
580) -> ViewHandle {
581    lazy_stack_aligned_impl(children, false, None, Some(alignment))
582}
583
584/// `TupleView<(A, B, …)>` — what a `@ViewBuilder` makes of several views, with
585/// no container around them.
586///
587/// A `List` renders each element of its content as a row, so
588/// `List { Text("a"); Text("b") }` is two rows. Without this the nearest the
589/// bindings could get was `list(&vstack(&[a, b]))` — one row holding a stack —
590/// and the parity corpus records that: its `List` case spells the *Swift* as
591/// `List { AnyView(VStack { … }) }` so the two sides agree. They agreed about
592/// the wrong thing.
593///
594/// A single child collapses to itself, as `ViewBuilder` does: `TupleView` of one
595/// is not a type Swift ever builds.
596pub fn tuple_view(children: &[ViewHandle]) -> ViewHandle {
597    assert!(!children.is_empty(), "a TupleView needs at least one child");
598    if children.len() == 1 {
599        return children[0].clone();
600    }
601    unsafe {
602        let elements: Vec<*const c_void> = children.iter().map(|c| c.meta).collect();
603        let tuple_meta = abi::tuple_metadata(&elements);
604        assert!(!tuple_meta.is_null(), "tuple metadata instantiation failed");
605        let offsets = abi::tuple_element_offsets(tuple_meta, children.len());
606        let meta = abi::generic_metadata1(resolve::tupleview_ma(), tuple_meta);
607        assert!(!meta.is_null(), "TupleView metadata instantiation failed");
608        let wt = abi::get_view_wt(meta);
609        assert!(!wt.is_null(), "{} !: View", crate::diag::type_name(meta));
610
611        let kids: Vec<TupleChild> = children
612            .iter()
613            .zip(&offsets)
614            .map(|(c, &offset)| TupleChild { value: c.value_ptr(), meta: c.meta, offset })
615            .collect();
616        let payload = TuplePayload { children: kids.as_ptr(), count: kids.len() };
617        // `emit_tuple` copies each child through its own value witness, so the
618        // caller keeps its originals and the tuple owns copies.
619        let mut out = vec![0u8; abi::type_bytes(meta)];
620        emit_tuple(out.as_mut_ptr(), &payload as *const TuplePayload as *mut c_void);
621        super::wrap_owned(out, meta, wt)
622    }
623}
624
625/// One track of a `LazyVGrid`'s columns or a `LazyHGrid`'s rows.
626///
627/// Only `.fixed` is here. `.flexible` and `.adaptive` are the other two cases
628/// of `GridItem.Size`, and each takes a *pair* of `CGFloat`s with defaults
629/// that Swift supplies at the call site rather than in the enum — so a binding
630/// for them is a different shape, not another arm of this one.
631#[derive(Clone, Copy, Debug, PartialEq)]
632pub enum GridTrack {
633    /// A track exactly this many points across.
634    Fixed(f64),
635}
636
637/// Build one `GridItem`, with SwiftUI's own defaults for spacing and alignment.
638///
639/// # Safety
640/// Calls into SwiftUI.
641unsafe fn grid_item(track: GridTrack) -> Vec<u8> {
642    unsafe {
643        let size_meta = abi::metadata0(resolve::grid_item_size_ma());
644        assert!(!size_meta.is_null(), "GridItem.Size metadata");
645        let mut size = vec![0u8; abi::type_bytes(size_meta)];
646        let GridTrack::Fixed(points) = track;
647        size[..8].copy_from_slice(&points.to_le_bytes());
648        // The tag is read, not written down: `GridItem.Size` is resilient, so
649        // its case numbering is not something a caller may assume. SwiftUI
650        // exports the tag as an i32 global (`…FWC`), and reflection agrees —
651        // this asks reflection, which needs no extra symbol.
652        let tag = abi::enum_case_index(size_meta, "fixed")
653            .expect("GridItem.Size has a `fixed` case");
654        abi::inject_enum_tag(&mut size, size_meta, tag as u32);
655
656        let item_meta = abi::metadata0(resolve::grid_item_ma());
657        assert!(!item_meta.is_null(), "GridItem metadata");
658        let mut item = vec![0u8; abi::type_bytes(item_meta)];
659        // `init(_:spacing:alignment:)`: x0 = @in Size, x1/x2 = `CGFloat?`
660        // (payload, tag 1 = nil), x3/x4/x5 = `Alignment?` (two words and a
661        // tag), x8 = @out. Both optionals are left nil, which is what Swift's
662        // defaulted arguments pass.
663        core::arch::asm!("blr {f}", f = in(reg) resolve::grid_item_init(),
664            in("x0") size.as_mut_ptr(),
665            in("x1") 0u64, in("x2") 1u64,
666            in("x3") 0u64, in("x4") 0u64, in("x5") 1u64,
667            in("x8") item.as_mut_ptr(),
668            lateout("x6") _, lateout("x7") _,
669            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
670            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
671            lateout("x17") _, lateout("lr") _, lateout("x20") _, lateout("x21") _,
672            clobber_abi("C"),
673        );
674        item
675    }
676}
677
678/// A Swift `Array<GridItem>` from the tracks.
679///
680/// # Safety
681/// Calls into SwiftUI.
682unsafe fn grid_item_array(tracks: &[GridTrack]) -> u64 {
683    unsafe {
684        let item_meta = abi::metadata0(resolve::grid_item_ma());
685        let stride = abi::type_bytes(item_meta);
686        let (array, buf): (u64, u64);
687        core::arch::asm!("blr {f}", f = in(reg) resolve::array_alloc_uninit(),
688            in("x0") tracks.len() as u64,
689            in("x1") item_meta,
690            lateout("x0") array, lateout("x1") buf,
691            lateout("x2") _, lateout("x3") _, lateout("x4") _, lateout("x5") _,
692            lateout("x6") _, lateout("x7") _, lateout("x8") _, lateout("x9") _,
693            lateout("x10") _, lateout("x11") _, lateout("x12") _, lateout("x13") _,
694            lateout("x14") _, lateout("x15") _, lateout("x16") _, lateout("x17") _,
695            lateout("lr") _, lateout("x20") _, lateout("x21") _, clobber_abi("C"),
696        );
697        // By stride, not by size: the buffer is an array, so each element
698        // starts a whole stride along even when the type's size is smaller.
699        for (i, t) in tracks.iter().enumerate() {
700            let item = grid_item(*t);
701            core::ptr::copy_nonoverlapping(
702                item.as_ptr(),
703                (buf as *mut u8).add(i * stride),
704                item.len(),
705            );
706        }
707        array
708    }
709}
710
711/// `LazyVGrid(columns:) { … }` — children flowed down the named columns.
712pub fn lazy_vgrid(tracks: &[GridTrack], children: &[ViewHandle]) -> ViewHandle {
713    unsafe { lazy_grid(tracks, children, true) }
714}
715
716/// `LazyHGrid(rows:) { … }` — children flowed across the named rows.
717pub fn lazy_hgrid(tracks: &[GridTrack], children: &[ViewHandle]) -> ViewHandle {
718    unsafe { lazy_grid(tracks, children, false) }
719}
720
721/// Shared body. The two initialisers differ only in the alignment's axis and
722/// the name of their first argument.
723///
724/// # Safety
725/// Calls into SwiftUI.
726unsafe fn lazy_grid(tracks: &[GridTrack], children: &[ViewHandle], vertical: bool) -> ViewHandle {
727    unsafe {
728        // The children's own types, as everywhere else in this file: a grid of
729        // `AnyView` would render but would describe a different Swift type,
730        // and with one `AnyView` child the grid would have nothing to flow.
731        let (content_meta, content_wt, offsets) = if children.len() == 1 {
732            (children[0].meta, children[0].wt, vec![0usize])
733        } else {
734            let elements: Vec<*const c_void> = children.iter().map(|c| c.meta).collect();
735            let tuple_meta = abi::tuple_metadata(&elements);
736            assert!(!tuple_meta.is_null(), "tuple metadata instantiation failed");
737            let offsets = abi::tuple_element_offsets(tuple_meta, children.len());
738            let m = abi::generic_metadata1(resolve::tupleview_ma(), tuple_meta);
739            assert!(!m.is_null(), "TupleView metadata instantiation failed");
740            let w = abi::get_view_wt(m);
741            assert!(!w.is_null(), "{} !: View", crate::diag::type_name(m));
742            (m, w, offsets)
743        };
744
745        let ma = if vertical { resolve::lazy_vgrid_ma() } else { resolve::lazy_hgrid_ma() };
746        let meta = abi::generic_metadata2(ma, content_meta, content_wt);
747        assert!(!meta.is_null(), "lazy grid metadata");
748        let wt = abi::get_view_wt(meta);
749        assert!(!wt.is_null(), "lazy grid !: View");
750
751        let array = grid_item_array(tracks);
752        let (align, _) = if vertical {
753            static_getter(resolve::halign_center(), resolve::halign_meta())
754        } else {
755            static_getter(resolve::valign_center(), resolve::valign_meta())
756        };
757
758        // The content closure is called before the initialiser returns, so the
759        // children may be borrowed rather than leaked.
760        let kids: Vec<TupleChild> = children
761            .iter()
762            .zip(&offsets)
763            .map(|(c, &offset)| TupleChild { value: c.value_ptr(), meta: c.meta, offset })
764            .collect();
765        let payload = TuplePayload { children: kids.as_ptr(), count: kids.len() };
766        let mut ctx =
767            abi::ClosureCtx::new(emit_tuple, &payload as *const TuplePayload as *mut c_void);
768
769        let pinned_meta = crate::type_by_name!(swift_type!(SwiftUI.PinnedScrollableViews).as_bytes());
770        assert!(!pinned_meta.is_null(), "PinnedScrollableViews metadata");
771        let mut pinned = vec![0u8; abi::type_bytes(pinned_meta)];
772
773        let mut result = vec![0u8; abi::type_bytes(meta)];
774        // init(columns:alignment:spacing:pinnedViews:content:)
775        //   x0 = Array<GridItem>, x1 = alignment, x2/x3 = `CGFloat?`,
776        //   x4 = @in pinnedViews, x5 = content fn, x6 = content ctx,
777        //   x7 = Content.Type, [sp] = Content: View WT, x8 = @out.
778        //
779        // Nine arguments after the indirect result, so the last one spills.
780        core::arch::asm!(
781            "sub sp, sp, #16",
782            "str {vw}, [sp]",
783            "blr {f}",
784            "add sp, sp, #16",
785            f = in(reg) if vertical { resolve::lazy_vgrid_init() } else { resolve::lazy_hgrid_init() },
786            vw = in(reg) content_wt,
787            in("x0") array,
788            in("x1") align,
789            in("x2") 0u64, in("x3") 1u64,
790            in("x4") pinned.as_mut_ptr(),
791            in("x5") abi::swift_closure_thunk as *const c_void,
792            in("x6") &mut ctx as *mut abi::ClosureCtx,
793            in("x7") content_meta,
794            in("x8") result.as_mut_ptr(),
795            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
796            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
797            lateout("x17") _, lateout("lr") _, lateout("x20") _, lateout("x21") _,
798            clobber_abi("C"),
799        );
800        wrap_owned(result, meta, wt)
801    }
802}