Skip to main content

swiftui_native/views/
binding.rs

1//! Two-way bindings — Rust-owned state.
2
3use super::*;
4use crate::abi;
5use crate::resolve;
6use core::ffi::c_void;
7use crate::mangle::{self, swift_type};
8
9// ── Two-way bindings — Rust-owned state ──
10
11/// Where a persistent [`BoundState`] also writes on every set.
12pub type Persist<T> = std::sync::Arc<dyn Fn(&T) + Send + Sync>;
13
14/// Rust-side state a SwiftUI control can both read and write.
15///
16/// This is not `@State`. SwiftUI's own property wrappers need the framework to
17/// reach into a Swift type's storage through reflection metadata, which a Rust
18/// type does not have. What this gives instead is the half that matters from
19/// Rust: a real `Binding<Value>` backed by Rust closures, so a control renders
20/// from Rust state and writes edits straight back into it.
21///
22/// Redisplay is driven explicitly, because SwiftUI cannot observe a Rust value:
23/// a `Binding`'s getter is read only when something else causes the body to be
24/// evaluated. Register [`on_write`] to be told when a control writes, and
25/// answer it by bumping a `@State` ([`crate::state::set_state`]) or by handing
26/// the hosting controller a new tree ([`crate::HostingView::set_root`]).
27/// Without that, an edit moves the control and updates the Rust value while a
28/// `Text` showing that value stays as it was.
29pub struct BoundState<T> {
30    value: std::sync::Arc<std::sync::Mutex<T>>,
31    /// Where to also write on [`set`](Self::set), if this state is persistent.
32    ///
33    /// Held here rather than in a second type so that every control taking a
34    /// `&BoundState<T>` — `Toggle`, `Slider`, `TextField`, all of them —
35    /// persists with no change of its own.
36    persist: Option<Persist<T>>,
37}
38
39impl<T: Clone> BoundState<T> {
40    pub fn new(initial: T) -> Self {
41        Self {
42            value: std::sync::Arc::new(std::sync::Mutex::new(initial)),
43            persist: None,
44        }
45    }
46
47    pub fn get(&self) -> T {
48        self.value.lock().unwrap().clone()
49    }
50
51    pub fn set(&self, v: T) {
52        if let Some(write) = &self.persist {
53            write(&v);
54        }
55        *self.value.lock().unwrap() = v;
56    }
57
58    /// Is this state written through to `UserDefaults`?
59    pub fn is_persistent(&self) -> bool {
60        self.persist.is_some()
61    }
62}
63
64impl<T: crate::defaults::Persisted> BoundState<T> {
65    /// State backed by `UserDefaults`, which is what `@AppStorage` is for.
66    ///
67    /// The stored value wins over `default` when there is one — including a
68    /// stored `false` or `0`, which is why [`defaults::has`](crate::defaults::has)
69    /// exists rather than leaning on the typed readers.
70    ///
71    /// Writes go through on [`set`](Self::set), so a control bound to this
72    /// persists as the user changes it and comes back to the same value in the
73    /// next process. SwiftUI is not involved in the storage at all: the value
74    /// lives in Rust and is mirrored to the same `UserDefaults` that
75    /// `@AppStorage` would have used.
76    pub fn persistent(key: &str, default: T) -> Self {
77        let initial = <T as crate::defaults::Persisted>::load(key, default);
78        let owned = key.to_owned();
79        Self {
80            value: std::sync::Arc::new(std::sync::Mutex::new(initial)),
81            persist: Some(std::sync::Arc::new(move |v: &T| {
82                <T as crate::defaults::Persisted>::store(&owned, v);
83            })),
84        }
85    }
86}
87
88impl<T> Clone for BoundState<T> {
89    fn clone(&self) -> Self {
90        Self { value: self.value.clone(), persist: self.persist.clone() }
91    }
92}
93
94/// Getter/setter pair backing one binding, kept alive for SwiftUI's benefit.
95pub(crate) struct BindingHooks {
96    get: Box<dyn Fn(*mut u8)>,
97    set: Box<dyn Fn(*const u8)>,
98}
99
100pub(crate) unsafe extern "C" fn binding_get_body(out: *mut u8, payload: *mut c_void) {
101    let hooks = unsafe { &*(payload as *const BindingHooks) };
102    (hooks.get)(out);
103}
104
105pub(crate) unsafe extern "C" fn binding_set_body(value: *const u8, payload: *mut c_void) {
106    let hooks = unsafe { &*(payload as *const BindingHooks) };
107    (hooks.set)(value);
108    // A control redraws itself after an edit, but anything *else* that reads the
109    // same Rust value — a label showing the number, a view two sections away —
110    // will not, because SwiftUI never saw a change it tracks. This is where an
111    // application says "that write mattered"; see [`on_write`].
112    if let Some(f) = ON_WRITE.get() {
113        f();
114    }
115}
116
117/// Called after any bound control writes back into Rust.
118static ON_WRITE: std::sync::OnceLock<Box<dyn Fn() + Send + Sync>> =
119    std::sync::OnceLock::new();
120
121/// Register what to do when a bound control writes.
122///
123/// Rust owns the state, so Rust decides when the interface is stale. The usual
124/// implementation bumps a `@State` — see [`crate::state::set_state`] — which is
125/// the one thing SwiftUI does track, and re-runs the body against whatever Rust
126/// now holds. Without this, editing a `Slider` moves the thumb and updates the
127/// Rust value while a `Text` showing that value stays as it was.
128///
129/// The first registration wins; later calls are ignored.
130pub fn on_write(f: impl Fn() + Send + Sync + 'static) {
131    let _ = ON_WRITE.set(Box::new(f));
132}
133
134/// Build a `Binding<Value>` from Rust getter and setter closures.
135///
136/// The hooks are leaked: SwiftUI keeps the binding for as long as the view
137/// exists and calls back into them on every read and every edit.
138pub(crate) unsafe fn binding_get_set(
139    value_meta: *const c_void,
140    binding_mangled: &[u8],
141    get: Box<dyn Fn(*mut u8)>,
142    set: Box<dyn Fn(*const u8)>,
143) -> Vec<u8> {
144    unsafe {
145        let hooks = Box::leak(Box::new(BindingHooks { get, set })) as *mut BindingHooks;
146        let get_ctx = Box::leak(Box::new(abi::ClosureCtx::new(
147            binding_get_body,
148            hooks as *mut c_void,
149        ))) as *mut abi::ClosureCtx;
150        let set_ctx = Box::leak(Box::new(abi::ValueClosureCtx::new(
151            binding_set_body,
152            hooks as *mut c_void,
153        ))) as *mut abi::ValueClosureCtx;
154
155        let binding_meta = abi::resolve_type_by_mangled_name(binding_mangled);
156        assert!(!binding_meta.is_null(), "Binding metadata");
157        let n = abi::type_stride(binding_meta).max(abi::type_size(binding_meta));
158        let mut out = vec![0u8; n];
159        // Binding.init(get:set:)
160        //   x0/x1 = get closure, x2/x3 = set closure,
161        //   x4 = Value.Type, x8 = @out Binding
162        core::arch::asm!("blr {f}", f = in(reg) resolve::binding_get_set(),
163            in("x0") abi::swift_closure_thunk as *const c_void,
164            in("x1") get_ctx,
165            in("x2") abi::value_closure_thunk as *const c_void,
166            in("x3") set_ctx,
167            in("x4") value_meta,
168            in("x8") out.as_mut_ptr(),
169            lateout("x5") _, lateout("x6") _, lateout("x7") _,
170            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
171            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
172            lateout("x17") _, lateout("lr") _, lateout("x20") _, lateout("x21") _,
173            clobber_abi("C"),
174        );
175        out
176    }
177}
178
179/// A `Toggle` wired to Rust state: it renders the current value and writes
180/// changes straight back.
181pub fn toggle_bound(label: &str, state: &BoundState<bool>) -> ViewHandle {
182    unsafe {
183        let g = state.clone();
184        let sset = state.clone();
185        let binding = binding_get_set(
186            resolve::bool_meta(),
187            mangle::bound(&swift_type!(SwiftUI.Binding), &[mangle::BOOL]).as_bytes(),
188            Box::new(move |out| *out = u8::from(g.get())),
189            Box::new(move |v| sset.set(*v != 0)),
190        );
191        toggle_with_binding(label, &binding)
192    }
193}
194
195/// `Toggle(label, isOn: $flag)` — a toggle driven directly by a real
196/// `@AppStorage`.
197///
198/// No Rust state sits in between: the binding is the one the property wrapper
199/// projects, so flipping the toggle writes through `UserDefaults` and re-runs
200/// the owning view's body. [`toggle_bound`] is the counterpart for state Rust
201/// owns.
202pub fn toggle_app_storage(label: &str, stored: &crate::state::StoredView<bool>) -> ViewHandle {
203    unsafe { toggle_with_binding(label, &stored.binding()) }
204}
205
206/// `Slider(value: $volume, in: lower...upper)` over a real `@AppStorage`.
207pub fn slider_app_storage(
208    stored: &crate::state::StoredView<f64>,
209    lower: f64,
210    upper: f64,
211) -> ViewHandle {
212    unsafe {
213        let mut binding = stored.binding();
214        super::slider_with_binding(&mut binding, lower, upper)
215    }
216}
217
218/// `Stepper(label, value: $count, in: lower...upper, step:)` over a real
219/// `@AppStorage`.
220pub fn stepper_app_storage(
221    label: &str,
222    stored: &crate::state::StoredView<f64>,
223    lower: f64,
224    upper: f64,
225    step: f64,
226) -> ViewHandle {
227    unsafe {
228        let mut binding = stored.binding();
229        super::stepper_with_binding(label, &mut binding, lower, upper, step)
230    }
231}
232
233/// A `Binding<T>` over Rust-owned state, for a value whose Swift representation
234/// is a fixed run of bytes.
235///
236/// Every bound control needs the same pair of closures — read the Rust value
237/// into the out-pointer, write the incoming bytes back — differing only in how
238/// many bytes and how to convert them. This is that pair, once.
239///
240/// # Safety
241/// `meta` and `mangled` must describe the same type as `N` bytes of `T`.
242pub(crate) unsafe fn scalar_binding<T, const N: usize>(
243    state: &BoundState<T>,
244    meta: *const c_void,
245    mangled: &[u8],
246    to_bytes: fn(&T) -> [u8; N],
247    from_bytes: fn([u8; N]) -> T,
248) -> Vec<u8>
249where
250    T: Clone + Send + 'static,
251{
252    let g = state.clone();
253    let s = state.clone();
254    unsafe {
255        binding_get_set(
256            meta,
257            mangled,
258            Box::new(move |out| {
259                let v = to_bytes(&g.get());
260                core::ptr::copy_nonoverlapping(v.as_ptr(), out, N);
261            }),
262            Box::new(move |v| {
263                let mut b = [0u8; N];
264                core::ptr::copy_nonoverlapping(v, b.as_mut_ptr(), N);
265                s.set(from_bytes(b));
266            }),
267        )
268    }
269}
270
271/// The `Binding<Bool>` every presentation modifier takes for `isPresented`.
272///
273/// # Safety
274/// Builds a binding; see [`scalar_binding`].
275pub(crate) unsafe fn bool_binding_of(state: &BoundState<bool>) -> Vec<u8> {
276    unsafe {
277        scalar_binding::<bool, 1>(
278            state,
279            resolve::bool_meta(),
280            mangle::bound(&swift_type!(SwiftUI.Binding), &[mangle::BOOL]).as_bytes(),
281            |v| [u8::from(*v)],
282            |b| b[0] != 0,
283        )
284    }
285}
286
287/// A `Binding<Double>`, for sliders, steppers and dates.
288///
289/// # Safety
290/// Builds a binding; see [`scalar_binding`].
291pub(crate) unsafe fn f64_binding_of(state: &BoundState<f64>) -> Vec<u8> {
292    unsafe {
293        scalar_binding::<f64, 8>(
294            state,
295            resolve::double_meta(),
296            mangle::bound(&swift_type!(SwiftUI.Binding), &[mangle::DOUBLE]).as_bytes(),
297            |v| v.to_le_bytes(),
298            f64::from_le_bytes,
299        )
300    }
301}
302
303/// A `Binding<Int>`, for a `Picker`'s selection.
304///
305/// # Safety
306/// Builds a binding; see [`scalar_binding`].
307pub(crate) unsafe fn i64_binding_of(state: &BoundState<i64>) -> Vec<u8> {
308    unsafe {
309        scalar_binding::<i64, 8>(
310            state,
311            resolve::int_meta(),
312            mangle::bound(&swift_type!(SwiftUI.Binding), &[mangle::INT]).as_bytes(),
313            |v| v.to_le_bytes(),
314            i64::from_le_bytes,
315        )
316    }
317}
318
319/// A `Slider` wired to Rust state: it renders the current value and writes
320/// every drag straight back.
321///
322/// [`super::slider`] builds a `Binding.constant`, which SwiftUI treats as
323/// read-only — the thumb cannot move, however well the touch arrives. A slider
324/// the user can actually drag needs a binding with a setter, which is this.
325pub fn slider_bound(state: &BoundState<f64>, lower: f64, upper: f64) -> ViewHandle {
326    unsafe {
327        let mut binding = f64_binding_of(state);
328        super::slider_with_binding(&mut binding, lower, upper)
329    }
330}
331
332/// A `Slider` that snaps to multiples of `step`.
333///
334/// A continuous slider is right for a volume and wrong for a rating out of
335/// five; only the continuous one was bound.
336pub fn slider_step_bound(
337    state: &BoundState<f64>,
338    lower: f64,
339    upper: f64,
340    step: f64,
341) -> ViewHandle {
342    unsafe {
343        let mut binding = f64_binding_of(state);
344        super::slider_step_with_binding(&mut binding, lower, upper, step)
345    }
346}
347
348/// A `Stepper` wired to Rust state: `−` and `+` write straight back.
349///
350/// Same reason as [`slider_bound`] — [`super::stepper`] builds a
351/// `Binding.constant`, and SwiftUI will not let a constant be stepped.
352pub fn stepper_bound(
353    label: &str,
354    state: &BoundState<f64>,
355    lower: f64,
356    upper: f64,
357    step: f64,
358) -> ViewHandle {
359    unsafe {
360        let mut binding = f64_binding_of(state);
361        super::stepper_with_binding(label, &mut binding, lower, upper, step)
362    }
363}
364
365/// Shared tail of `toggle` and `toggle_bound`.
366pub(crate) unsafe fn toggle_with_binding(label: &str, binding: &[u8]) -> ViewHandle {
367    unsafe {
368        let ss = abi::swift_string(label);
369        let s0 = u64::from_le_bytes(ss[..8].try_into().unwrap());
370        let s1 = u64::from_le_bytes(ss[8..].try_into().unwrap());
371        let (k0, k1, k2, k3) = localized_string_key(s0, s1);
372
373        let b0 = u64::from_le_bytes(binding[0..8].try_into().unwrap());
374        let b1 = u64::from_le_bytes(binding[8..16].try_into().unwrap());
375        let b2 = binding[16] as u64;
376
377        let (meta, wt, size) = result_type(&TOGGLE_T);
378        let mut result = vec![0u8; size];
379        core::arch::asm!("blr {f}", f = in(reg) resolve::toggle_init(),
380            in("x0") k0, in("x1") k1, in("x2") k2, in("x3") k3,
381            in("x4") b0, in("x5") b1, in("x6") b2,
382            in("x8") result.as_mut_ptr(),
383            in("x20") meta,
384            lateout("x7") _,
385            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
386            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
387            lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
388        );
389        wrap_owned(result, meta, wt)
390    }
391}
392
393/// Write through a `Binding<Bool>`, exactly as a control does when the user
394/// interacts with it. Used to prove the write-back path end to end.
395/// Write through a `Binding<T>` the way a control does — SwiftUI's own
396/// `wrappedValue` setter, reached through its dispatch thunk.
397///
398/// This is what makes a bound control testable without a touch: build the
399/// control's binding, write through it, and see whether the Rust value moved.
400///
401/// # Safety
402/// `value` must be the little-endian bytes of the type `mangled` names, and
403/// `binding` must be a `Binding` of that type.
404pub unsafe fn binding_write(binding: &mut [u8], value: &mut [u8], mangled: &[u8]) {
405    unsafe {
406        let meta = abi::resolve_type_by_mangled_name(mangled);
407        assert!(!meta.is_null(), "binding type metadata");
408        core::arch::asm!("blr {f}", f = in(reg) resolve::binding_wrapped_set(),
409            in("x0") value.as_mut_ptr(),
410            in("x1") meta,
411            in("x20") binding.as_mut_ptr(),
412            lateout("x2") _, lateout("x3") _, lateout("x4") _, lateout("x5") _,
413            lateout("x6") _, lateout("x7") _, lateout("x8") _, lateout("x9") _,
414            lateout("x10") _, lateout("x11") _, lateout("x12") _, lateout("x13") _,
415            lateout("x14") _, lateout("x15") _, lateout("x16") _, lateout("x17") _,
416            lateout("lr") _, lateout("x21") _, clobber_abi("C"),
417        );
418    }
419}
420
421pub fn binding_write_bool(binding: &mut [u8], value: bool) {
422    unsafe {
423        let mut v = [u8::from(value)];
424        let meta = abi::resolve_type_by_mangled_name(mangle::bound("7SwiftUI7BindingV", &[mangle::BOOL]).as_bytes());
425        core::arch::asm!("blr {f}", f = in(reg) resolve::binding_wrapped_set(),
426            in("x0") v.as_mut_ptr(),
427            in("x1") meta,
428            in("x20") binding.as_mut_ptr(),
429            lateout("x2") _, lateout("x3") _, lateout("x4") _, lateout("x5") _,
430            lateout("x6") _, lateout("x7") _, lateout("x8") _, lateout("x9") _,
431            lateout("x10") _, lateout("x11") _, lateout("x12") _, lateout("x13") _,
432            lateout("x14") _, lateout("x15") _, lateout("x16") _, lateout("x17") _,
433            lateout("lr") _, lateout("x21") _, clobber_abi("C"),
434        );
435    }
436}
437
438/// Build a `Binding<Bool>` over Rust state, for direct write-back testing.
439/// Build a `Binding<String>` backed by Rust state.
440///
441/// Named for the binding rather than the type: `string_binding` already means
442/// something else here — the four words of a `LocalizedStringKey`.
443///
444/// The getter hands SwiftUI a freshly built Swift `String` each time it asks;
445/// the setter copies the incoming one out without touching its storage, since
446/// the value arrives borrowed.
447pub fn text_binding(state: &BoundState<String>) -> Vec<u8> {
448    let g = state.clone();
449    let s = state.clone();
450    unsafe {
451        binding_get_set(
452            resolve::string_meta(),
453            mangle::bound(&swift_type!(SwiftUI.Binding), &[mangle::STRING]).as_bytes(),
454            Box::new(move |out| {
455                let bytes = abi::swift_string(&g.get());
456                core::ptr::copy_nonoverlapping(bytes.as_ptr(), out, 16);
457            }),
458            Box::new(move |v| {
459                let mut bytes = [0u8; 16];
460                core::ptr::copy_nonoverlapping(v, bytes.as_mut_ptr(), 16);
461                s.set(abi::read_swift_string(bytes));
462            }),
463        )
464    }
465}
466
467/// Build a `Binding<Double>` backed by Rust state.
468pub fn f64_binding(state: &BoundState<f64>) -> Vec<u8> {
469    let g = state.clone();
470    let s = state.clone();
471    unsafe {
472        binding_get_set(
473            resolve::double_meta(),
474            mangle::bound(&swift_type!(SwiftUI.Binding), &[mangle::DOUBLE]).as_bytes(),
475            Box::new(move |out| {
476                let v = g.get();
477                core::ptr::copy_nonoverlapping(v.to_le_bytes().as_ptr(), out, 8);
478            }),
479            Box::new(move |v| {
480                let mut b = [0u8; 8];
481                core::ptr::copy_nonoverlapping(v, b.as_mut_ptr(), 8);
482                s.set(f64::from_le_bytes(b));
483            }),
484        )
485    }
486}
487
488/// Build a `Binding<Int>` backed by Rust state.
489/// A `Binding<Int?>` over Rust state.
490///
491/// Built for a selectable `List`, which is **not yet bound**. Four approaches
492/// have failed, and what is known now narrows it a long way:
493///
494/// Calling `List(_:id:selection:rowContent:)` crashes under three argument
495/// layouts — the selection in four registers (it measures thirty-two bytes and
496/// `Binding` is `@frozen`, so direct passing is what the ABI rules imply), the
497/// selection by address, and the selection in registers with `Self.Type` in
498/// swiftself. What that initialiser wants is still unknown.
499///
500/// It does not have to be called, though. `List<Never, C>` and
501/// `List<SelectionValue, C>` lay out identically — both `[0, 32, 80, 88]`, read
502/// from each type's metadata — and only `selection` depends on
503/// `SelectionValue`. Building the list through the initialiser that *does*
504/// work and copying its other three fields across produces a value that builds,
505/// dumps and renders correctly, **as long as the selection is left nil**.
506/// Writing a live binding into it makes the value fault the first time
507/// anything copies it.
508///
509/// Ruled out as causes of that: the optional tag (`Binding<Int?>?` uses a spare
510/// bit, so writing `.some` is a no-op and changes nothing either way); the
511/// location not being a real object (the binding comes from SwiftUI's own
512/// `Binding.init(get:set:)`); and the field offsets, which are read rather than
513/// assumed.
514///
515/// This binding itself is sound and checked: thirty-two bytes with a live
516/// location and the value in the third word.
517///
518/// "Nothing selected" has to be representable, which is why the bound value is
519/// an optional rather than a sentinel integer. `Int` has no spare bits, so
520/// `Int?` is the eight bytes plus a tag byte, and the tag is what says which it
521/// is; [`abi::store_optional_tag`] writes it rather than this guessing at the
522/// layout.
523pub fn optional_i64_binding(state: &BoundState<Option<i64>>) -> Vec<u8> {
524    let g = state.clone();
525    let s = state.clone();
526    unsafe {
527        let payload = resolve::int_meta();
528        let opt = abi::resolve_type_by_mangled_name(b"SiSg");
529        assert!(!opt.is_null(), "Optional<Int> metadata");
530        let width = abi::type_bytes(opt);
531        binding_get_set(
532            opt,
533            mangle::bound(&swift_type!(SwiftUI.Binding), &[&mangle::optional(mangle::INT)])
534                .as_bytes(),
535            Box::new(move |out| {
536                let slot = core::slice::from_raw_parts_mut(out, width);
537                slot.fill(0);
538                match g.get() {
539                    Some(v) => {
540                        slot[..8].copy_from_slice(&v.to_le_bytes());
541                        abi::store_optional_tag(slot, payload, true);
542                    }
543                    None => abi::store_optional_tag(slot, payload, false),
544                }
545            }),
546            Box::new(move |v| {
547                let slot = core::slice::from_raw_parts(v, width);
548                let present = abi::read_optional_tag(slot, payload);
549                s.set(present.then(|| {
550                    let mut b = [0u8; 8];
551                    b.copy_from_slice(&slot[..8]);
552                    i64::from_le_bytes(b)
553                }));
554            }),
555        )
556    }
557}
558
559pub fn i64_binding(state: &BoundState<i64>) -> Vec<u8> {
560    let g = state.clone();
561    let s = state.clone();
562    unsafe {
563        binding_get_set(
564            resolve::int_meta(),
565            mangle::bound(&swift_type!(SwiftUI.Binding), &[mangle::INT]).as_bytes(),
566            Box::new(move |out| {
567                let v = g.get();
568                core::ptr::copy_nonoverlapping(v.to_le_bytes().as_ptr(), out, 8);
569            }),
570            Box::new(move |v| {
571                let mut b = [0u8; 8];
572                core::ptr::copy_nonoverlapping(v, b.as_mut_ptr(), 8);
573                s.set(i64::from_le_bytes(b));
574            }),
575        )
576    }
577}
578
579pub fn bool_binding(state: &BoundState<bool>) -> Vec<u8> {
580    let g = state.clone();
581    let s = state.clone();
582    unsafe {
583        binding_get_set(
584            resolve::bool_meta(),
585            mangle::bound(&swift_type!(SwiftUI.Binding), &[mangle::BOOL]).as_bytes(),
586            Box::new(move |out| *out = u8::from(g.get())),
587            Box::new(move |v| s.set(*v != 0)),
588        )
589    }
590}
591
592swift_cases! {
593    /// `SwiftUI.HorizontalEdge`.
594    pub enum HorizontalEdge {
595        Leading = 0,
596        Trailing = 1,
597    }
598}
599
600/// Apply a modifier that takes `isPresented:` and a `@ViewBuilder` content.
601///
602/// The shape most of SwiftUI's presentation modifiers share: three words of
603/// `Binding<Bool>`, then the content closure's two, then the type arguments —
604/// nine integer registers in all, so the tail spills onto the stack.
605unsafe fn presenting_modifier(
606    func: *const c_void,
607    oret: *const c_void,
608    view: &ViewHandle,
609    before: &[u64],
610    state: &BoundState<bool>,
611    content: &ViewHandle,
612) -> ViewHandle {
613    unsafe {
614        let mut b = bool_binding(state);
615        let w = binding_words(&mut b);
616        let mut ints = before.to_vec();
617        ints.extend_from_slice(&w);
618        ints.extend_from_slice(&view_words(content));
619        generic1_modifier_ints(
620            func,
621            oret,
622            view,
623            content.meta,
624            content.wt,
625            &ints,
626            [0.0; 4],
627        )
628    }
629}
630
631/// Apply `.navigationDestination(isPresented:destination:)`.
632pub fn navigation_destination(
633    view: &ViewHandle,
634    state: &BoundState<bool>,
635    destination: &ViewHandle,
636) -> ViewHandle {
637    unsafe {
638        presenting_modifier(
639            resolve::navigation_destination_fn(),
640            resolve::navigation_destination_oret(),
641            view,
642            &[],
643            state,
644            destination,
645        )
646    }
647}
648
649/// Apply `.inspector(isPresented:content:)`.
650pub fn inspector(
651    view: &ViewHandle,
652    state: &BoundState<bool>,
653    content: &ViewHandle,
654) -> ViewHandle {
655    unsafe {
656        presenting_modifier(
657            resolve::inspector_fn(),
658            resolve::inspector_oret(),
659            view,
660            &[],
661            state,
662            content,
663        )
664    }
665}
666
667/// Apply `.swipeActions(edge:allowsFullSwipe:isPresented:content:)`.
668///
669/// `isPresented` is an *optional* binding here. A `Binding` holds a class
670/// reference, so the optional is the same seventeen bytes with `nil` as a null
671/// one — still three registers, not an address.
672pub fn swipe_actions(
673    view: &ViewHandle,
674    edge: HorizontalEdge,
675    allows_full_swipe: bool,
676    content: &ViewHandle,
677) -> ViewHandle {
678    unsafe {
679        let e = abi::enum_argument(abi::metadata0(resolve::horizontal_edge_ma()), edge.tag());
680        let payload = abi::resolve_type_by_mangled_name(mangle::bound("7SwiftUI7BindingV", &[mangle::BOOL]).as_bytes());
681        let opt = abi::resolve_type_by_mangled_name(mangle::optional(&mangle::bound("7SwiftUI7BindingV", &[mangle::BOOL])).as_bytes());
682        let mut nil = vec![0u8; abi::type_bytes(opt).max(24)];
683        abi::store_optional_tag(&mut nil, payload, false);
684        let mut ints = vec![e, u64::from(allows_full_swipe)];
685        ints.extend_from_slice(&binding_words(&mut nil));
686        ints.extend_from_slice(&view_words(content));
687        generic1_modifier_ints(
688            resolve::swipe_actions_fn(),
689            resolve::swipe_actions_oret(),
690            view,
691            content.meta,
692            content.wt,
693            &ints,
694            [0.0; 4],
695        )
696    }
697}
698
699/// Apply `.popover(isPresented:attachmentAnchor:arrowEdges:content:)`.
700///
701/// The public symbol, where [`super::popover`] goes through `popoverCore` with
702/// SwiftUI's own defaults. Everything is the caller's here: a binding rather
703/// than a fixed `Bool`, an explicit attachment point and explicit arrow edges.
704///
705/// The anchor is `PopoverAttachmentAnchor.point(UnitPoint(x:y:))`. Both of that
706/// enum's cases carry a payload, so the tags are the declaration order and
707/// `.point` is 1; `UnitPoint` is two frozen `CGFloat`s written straight into the
708/// payload, which is why this case is reachable without `Anchor<CGRect>.Source`.
709pub fn popover_anchored(
710    view: &ViewHandle,
711    state: &BoundState<bool>,
712    anchor: (f64, f64),
713    arrow_edge: Edge,
714    content: &ViewHandle,
715) -> ViewHandle {
716    unsafe {
717        let mut b = bool_binding(state);
718        let w = binding_words(&mut b);
719        let anchor_meta = abi::metadata0(resolve::popover_anchor_ma());
720        let mut buf = vec![0u8; abi::type_bytes(anchor_meta)];
721        buf[..8].copy_from_slice(&anchor.0.to_le_bytes());
722        buf[8..16].copy_from_slice(&anchor.1.to_le_bytes());
723        abi::inject_enum_tag(&mut buf, anchor_meta, 1);
724        // `Edge` is a four-case enum with no payloads, so it is one byte in a
725        // register and its tag is the declaration order.
726        let edge = abi::enum_argument(abi::metadata0(resolve::edge_ma()), arrow_edge as u32);
727        let mut ints = w.to_vec();
728        ints.push(buf.as_ptr() as u64);
729        ints.push(edge);
730        ints.extend_from_slice(&view_words(content));
731        generic1_modifier_ints(
732            resolve::popover_fn(),
733            resolve::popover_oret(),
734            view,
735            content.meta,
736            content.wt,
737            &ints,
738            [0.0; 4],
739        )
740    }
741}
742
743/// One edge of a rectangle.
744///
745/// Four cases, none with a payload, so the tags are the declaration order and
746/// the whole value fits in a register.
747#[derive(Clone, Copy, Debug, PartialEq, Eq)]
748pub enum Edge {
749    Top = 0,
750    Leading = 1,
751    Bottom = 2,
752    Trailing = 3,
753}
754
755
756/// `Toggle(isOn:) { label }` — a toggle whose label is any view.
757///
758/// [`toggle_bound`] takes a string, which becomes a `Text`. The convenience
759/// that takes a symbol name — `Toggle(_:systemImage:isOn:)` — is inlinable and
760/// exports nothing, so it cannot be called; but it only builds a `Label`, and
761/// this takes any view at all. `Toggle(isOn:) { label("Wi-Fi", "wifi") }` gets
762/// there and more.
763pub fn toggle_labelled(label: &ViewHandle, state: &BoundState<bool>) -> ViewHandle {
764    unsafe {
765        let binding = bool_binding_of(state);
766        let b0 = u64::from_le_bytes(binding[0..8].try_into().expect("binding 0"));
767        let b1 = u64::from_le_bytes(binding[8..16].try_into().expect("binding 1"));
768        let b2 = binding[16] as u64;
769
770        // `Toggle<Label>` — one parameter, so its witness is at 1.
771        let (known_meta, known_wt, known_size) = super::result_type(&super::TOGGLE_ANY_T);
772        let typed = super::substitute_args(known_meta, 2, &[(0, label.meta)], &[(1, label.wt)]);
773        let (meta, wt) = typed.unwrap_or((known_meta, known_wt));
774        let size = if typed.is_some() { abi::type_bytes(meta) } else { known_size };
775        let (label_meta, label_wt) = match typed {
776            Some(_) => (label.meta, label.wt),
777            None => (resolve::anyview_meta(), resolve::anyview_wt()),
778        };
779        let kind = if typed.is_some() { super::Content::Concrete } else { super::Content::Erased };
780
781        // The label builder escapes: SwiftUI re-runs it whenever the toggle is
782        // laid out, so the context and the child it reads are leaked.
783        let ctx = Box::leak(Box::new(abi::ClosureCtx::new(
784            super::emit_tuple,
785            super::Child::leak(label, kind),
786        ))) as *mut abi::ClosureCtx;
787
788        let mut result = vec![0u8; size];
789        // Toggle.init(isOn:label:)
790        //   x0-x2 = Binding<Bool>, x3 = label fn, x4 = label ctx, x8 = @out,
791        //   x20 = Self.Type.
792        //
793        // `Label: View` is a requirement of `Toggle` itself rather than of this
794        // initialiser, so it is recoverable from `Self` and not passed — the
795        // same reason the string overload next door passes only `x20`.
796        core::arch::asm!("blr {f}", f = in(reg) resolve::toggle_label_init(),
797            in("x0") b0, in("x1") b1, in("x2") b2,
798            in("x3") abi::swift_closure_thunk as *const c_void,
799            in("x4") ctx,
800            in("x5") label_meta,
801            in("x6") label_wt,
802            in("x8") result.as_mut_ptr(),
803            in("x20") meta,
804            lateout("x7") _,
805            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
806            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
807            lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
808        );
809        wrap_owned(result, meta, wt)
810    }
811}
812
813
814/// `Toggle(_:isOn: [Binding<Bool>])` — one toggle over several bindings.
815///
816/// The "select all" row at the top of a settings group: it reads *mixed* when
817/// the sources disagree, and setting it sets every one of them. There was no
818/// way to express that — a toggle took exactly one binding, so the mixed state
819/// had to be faked with a third value that nothing wrote back to.
820///
821/// `sources` is `@in` and generic, so the array travels by address; its
822/// elements are real `Binding<Bool>`s, each seventeen bytes in a
823/// twenty-four-byte stride, which is why they are placed by stride rather than
824/// packed.
825///
826/// Swift spells this `Toggle("All", sources: items, isOn: \.self)` — the
827/// `isOn: [Binding<Bool>]` overload exists and is what this calls, but the
828/// compiler cannot tell it from the single-binding one and rejects the array
829/// even with its type written out. Both spellings reach the same initialiser;
830/// the parity case uses the one that compiles.
831pub fn toggle_sources(label: &str, states: &[&BoundState<bool>]) -> ViewHandle {
832    assert!(!states.is_empty(), "a Toggle over sources needs at least one");
833    unsafe {
834        let ss = abi::swift_string(label);
835        let s0 = u64::from_le_bytes(ss[..8].try_into().expect("label lo"));
836        let s1 = u64::from_le_bytes(ss[8..].try_into().expect("label hi"));
837        let (k0, k1, k2, k3) = localized_string_key(s0, s1);
838
839        let element = abi::resolve_type_by_mangled_name(b"7SwiftUI7BindingVySbG");
840        assert!(!element.is_null(), "Binding<Bool> metadata");
841        let stride = abi::type_bytes(element);
842        let mut bytes = vec![0u8; stride * states.len()];
843        for (i, state) in states.iter().enumerate() {
844            let b = bool_binding_of(state);
845            bytes[i * stride..i * stride + b.len()].copy_from_slice(&b);
846        }
847        let array = abi::swift_array(element, states.len(), &bytes);
848
849        let array_meta = abi::resolve_type_by_mangled_name(b"Say7SwiftUI7BindingVySbGG");
850        assert!(!array_meta.is_null(), "Array<Binding<Bool>> metadata");
851        let rac = super::conformance_wt(array_meta, resolve::random_access_proto());
852        assert!(!rac.is_null(), "Array<Binding<Bool>>: RandomAccessCollection");
853
854        let (meta, wt, size) = super::result_type(&super::TOGGLE_T);
855        let mut result = vec![0u8; size];
856        // Toggle.init(_:isOn:)
857        //   x0-x3 = LocalizedStringKey, x4 = @in Sources — generic, so the
858        //   array travels by address — x5 = Sources.Type,
859        //   x6 = Sources: RandomAccessCollection WT, x8 = @out,
860        //   x20 = Self.Type.
861        //
862        //   `Sources.Element == Binding<Bool>` is a *same-type* requirement, so
863        //   it needs no witness of its own.
864        core::arch::asm!("blr {f}", f = in(reg) resolve::toggle_sources_init(),
865            in("x0") k0, in("x1") k1, in("x2") k2, in("x3") k3,
866            in("x4") &array as *const u64,
867            in("x5") array_meta,
868            in("x6") rac,
869            in("x8") result.as_mut_ptr(),
870            in("x20") meta,
871            lateout("x7") _,
872            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
873            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
874            lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
875        );
876        wrap_owned(result, meta, wt)
877    }
878}