Skip to main content

swiftui_native/views/
presentation.rs

1//! Presentation.
2
3use super::*;
4use crate::abi;
5use crate::resolve;
6use core::ffi::c_void;
7use crate::mangle::{self, swift_type};
8
9// ── Presentation ──
10
11/// Apply `.alert(_:isPresented:actions:)`.
12///
13/// Both this and `sheet` push their generic arguments onto the stack: eight
14/// registers are already taken by the title, the binding and the closure.
15/// Shared tail of `alert` and `alert_bound`.
16///
17/// # Safety
18/// `binding` must be a `Binding<Bool>`.
19pub(crate) unsafe fn alert_with_binding(
20    view: &ViewHandle,
21    title: &str, actions: &ViewHandle,
22    binding: &[u8],
23) -> ViewHandle {
24    unsafe {
25        let ss = abi::swift_string(title);
26        let s0 = u64::from_le_bytes(ss[..8].try_into().unwrap());
27        let s1 = u64::from_le_bytes(ss[8..].try_into().unwrap());
28        let (k0, k1, k2, k3) = localized_string_key(s0, s1);
29        let b0 = u64::from_le_bytes(binding[0..8].try_into().unwrap());
30        let b1 = u64::from_le_bytes(binding[8..16].try_into().unwrap());
31        let b2 = binding[16] as u64;
32
33        // The actions builder escapes, like a sheet's content: it runs when the
34        // alert is presented, not now.
35        let ctx = Box::leak(Box::new(abi::ClosureCtx::new(
36            emit_tuple,
37            Child::leak(actions, Content::Concrete),
38        ))) as *mut abi::ClosureCtx;
39
40        let subst = [
41            view.meta,                        // Self.Type — the concrete receiver
42            actions.meta,          // content — the closure emits AnyView
43            view.wt,                          // Self: View
44            actions.wt,            // content: View
45        ];
46        let (meta, wt, size) = opaque_result_type(resolve::alert_oret(), &subst);
47        let mut result = vec![0u8; size];
48        let this = view.value_ptr();
49        // alert(_:isPresented:actions:)
50        //   x0-x3 = key, x4-x6 = Binding<Bool>, x7 = actions fn,
51        //   [sp] = actions ctx, [sp+8] = Self.Type, [sp+16] = A.Type,
52        //   [sp+24] = Self: View WT, [sp+32] = A: View WT
53        core::arch::asm!(
54            "sub sp, sp, #48",
55            "stp {cx}, {sm}, [sp]",
56            "stp {am}, {sw}, [sp, #16]",
57            "str {aw}, [sp, #32]",
58            "blr {f}",
59            "add sp, sp, #48",
60            f = in(reg) resolve::alert_fn(),
61            cx = in(reg) ctx,
62            sm = in(reg) view.meta,
63            am = in(reg) actions.meta,
64            sw = in(reg) view.wt,
65            aw = in(reg) actions.wt,
66            in("x0") k0, in("x1") k1, in("x2") k2, in("x3") k3,
67            in("x4") b0, in("x5") b1, in("x6") b2,
68            in("x7") abi::swift_closure_thunk as *const c_void,
69            in("x8") result.as_mut_ptr(),
70            in("x20") this,
71            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
72            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
73            lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
74        );
75        wrap_owned(result, meta, wt)
76    }
77}
78
79/// `alert(_:isPresented:actions:message:)` — an alert that explains itself.
80///
81/// The `message:` closure is the line under the title: what the buttons are
82/// about, and the difference between "Delete" and "Delete — this cannot be
83/// undone". Only the title-and-buttons overload was bound, so an alert could
84/// ask but not say why.
85///
86/// # Safety
87/// `binding` must be a live `Binding<Bool>`.
88pub(crate) unsafe fn alert_message_with_binding(
89    view: &ViewHandle,
90    title: &str,
91    actions: &ViewHandle,
92    message: &ViewHandle,
93    binding: &[u8],
94) -> ViewHandle {
95    unsafe {
96        let ss = abi::swift_string(title);
97        let s0 = u64::from_le_bytes(ss[..8].try_into().unwrap());
98        let s1 = u64::from_le_bytes(ss[8..].try_into().unwrap());
99        let (k0, k1, k2, k3) = localized_string_key(s0, s1);
100        let b0 = u64::from_le_bytes(binding[0..8].try_into().unwrap());
101        let b1 = u64::from_le_bytes(binding[8..16].try_into().unwrap());
102        let b2 = binding[16] as u64;
103
104        // Both builders escape: they run when the alert is presented.
105        let actions_ctx = Box::leak(Box::new(abi::ClosureCtx::new(
106            emit_tuple,
107            Child::leak(actions, Content::Concrete),
108        ))) as *mut abi::ClosureCtx;
109        let message_ctx = Box::leak(Box::new(abi::ClosureCtx::new(
110            emit_tuple,
111            Child::leak(message, Content::Concrete),
112        ))) as *mut abi::ClosureCtx;
113
114        let subst = [
115            view.meta,
116            actions.meta,
117            message.meta,
118            view.wt,
119            actions.wt,
120            message.wt,
121        ];
122        let (meta, wt, size) = opaque_result_type(resolve::alert_msg_oret(), &subst);
123        let mut result = vec![0u8; size];
124        let this = view.value_ptr();
125        // alert(_:isPresented:actions:message:)
126        //   x0-x3 = key, x4-x6 = Binding<Bool>, x7 = actions fn,
127        //   [sp]    = actions ctx,  [sp+8]  = message fn,
128        //   [sp+16] = message ctx,  [sp+24] = Self.Type,
129        //   [sp+32] = A.Type,       [sp+40] = B.Type,
130        //   [sp+48] = Self: View,   [sp+56] = A: View,   [sp+64] = B: View
131        core::arch::asm!(
132            "sub sp, sp, #80",
133            "stp {ac}, {mf}, [sp]",
134            "stp {mc}, {sm}, [sp, #16]",
135            "stp {am}, {mm}, [sp, #32]",
136            "stp {sw}, {aw}, [sp, #48]",
137            "str {mw}, [sp, #64]",
138            "blr {f}",
139            "add sp, sp, #80",
140            f = in(reg) resolve::alert_msg_fn(),
141            ac = in(reg) actions_ctx,
142            mf = in(reg) abi::swift_closure_thunk as *const c_void,
143            mc = in(reg) message_ctx,
144            sm = in(reg) subst[0],
145            am = in(reg) subst[1],
146            mm = in(reg) subst[2],
147            sw = in(reg) subst[3],
148            aw = in(reg) subst[4],
149            mw = in(reg) subst[5],
150            in("x0") k0, in("x1") k1, in("x2") k2, in("x3") k3,
151            in("x4") b0, in("x5") b1, in("x6") b2,
152            in("x7") abi::swift_closure_thunk as *const c_void,
153            in("x8") result.as_mut_ptr(),
154            in("x20") this,
155            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
156            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
157            lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
158        );
159        wrap_owned(result, meta, wt)
160    }
161}
162
163/// `alert(_:isPresented:actions:message:)` with a fixed flag, which cannot be
164/// dismissed — see [`alert`].
165pub fn alert_message(
166    view: &ViewHandle,
167    title: &str,
168    actions: &ViewHandle,
169    message: &ViewHandle,
170    presented: bool,
171) -> ViewHandle {
172    unsafe {
173        let binding = constant_binding(
174            &[u8::from(presented)],
175            resolve::bool_meta(),
176            mangle::bound(&swift_type!(SwiftUI.Binding), &[mangle::BOOL]).as_bytes(),
177        );
178        alert_message_with_binding(view, title, actions, message, &binding)
179    }
180}
181
182/// `alert(_:isPresented:actions:message:)` bound to Rust state.
183pub fn alert_message_bound(
184    view: &ViewHandle,
185    title: &str,
186    actions: &ViewHandle,
187    message: &ViewHandle,
188    state: &BoundState<bool>,
189) -> ViewHandle {
190    unsafe {
191        let binding = bool_binding_of(state);
192        alert_message_with_binding(view, title, actions, message, &binding)
193    }
194}
195
196/// `alert` with a fixed flag. It cannot be dismissed: SwiftUI treats a
197/// `Binding.constant` as read-only, so the presentation has no way to write
198/// `false` back. Use [`alert_bound`] for one the user can close.
199pub fn alert(view: &ViewHandle, title: &str, actions: &ViewHandle, presented: bool) -> ViewHandle {
200    unsafe {
201        let binding = constant_binding(
202            &[u8::from(presented)],
203            resolve::bool_meta(),
204            mangle::bound(&swift_type!(SwiftUI.Binding), &[mangle::BOOL]).as_bytes(),
205        );
206        alert_with_binding(view, title, actions, &binding)
207    }
208}
209
210/// `alert` bound to Rust state, so dismissing it writes `false` back.
211pub fn alert_bound(view: &ViewHandle, title: &str, actions: &ViewHandle, state: &BoundState<bool>) -> ViewHandle {
212    unsafe {
213        let binding = bool_binding_of(state);
214        alert_with_binding(view, title, actions, &binding)
215    }
216}
217
218
219/// Apply `.sheet(isPresented:content:)`.
220/// Shared tail of `sheet` and `sheet_bound`.
221///
222/// # Safety
223/// `binding` must be a `Binding<Bool>`.
224pub(crate) unsafe fn sheet_with_binding(
225    view: &ViewHandle,
226    content: &ViewHandle,
227    binding: &[u8],
228) -> ViewHandle {
229    unsafe { sheet_with_dismiss(view, content, binding, [0, 0]) }
230}
231
232/// The same, with the `onDismiss:` closure spelled out.
233///
234/// `(() -> ())?` is two words and `nil` is both of them null, which is what
235/// [`sheet_with_binding`] passes — so a sheet could be opened and closed and
236/// nothing on the Rust side would hear about the closing.
237///
238/// # Safety
239/// `binding` must be a live `Binding<Bool>`, and `dismiss` either `[0, 0]` or a
240/// live Swift closure's two words.
241pub(crate) unsafe fn sheet_with_dismiss(
242    view: &ViewHandle,
243    content: &ViewHandle,
244    binding: &[u8],
245    dismiss: [u64; 2],
246) -> ViewHandle {
247    unsafe {
248        presenting_with_dismiss(
249            view,
250            content,
251            binding,
252            dismiss,
253            resolve::sheet_fn(),
254            resolve::sheet_oret(),
255        )
256    }
257}
258
259/// `sheet(isPresented:onDismiss:content:)` and `fullScreenCover(…)`, which take
260/// the same arguments in the same registers and differ only in which symbol is
261/// called — so one body serves both rather than two copies of this assembly.
262///
263/// # Safety
264/// As [`sheet_with_dismiss`], and `f`/`oret` must be a matching modifier and
265/// opaque-result descriptor for a presentation of that shape.
266pub(crate) unsafe fn presenting_with_dismiss(
267    view: &ViewHandle,
268    content: &ViewHandle,
269    binding: &[u8],
270    dismiss: [u64; 2],
271    f: *const c_void,
272    oret: *const c_void,
273) -> ViewHandle {
274    unsafe {
275        let b0 = u64::from_le_bytes(binding[0..8].try_into().unwrap());
276        let b1 = u64::from_le_bytes(binding[8..16].try_into().unwrap());
277        let b2 = binding[16] as u64;
278
279        // A sheet's content closure *escapes*: SwiftUI stores it and calls it
280        // when the sheet is presented, long after this function returns. Both
281        // the context and the child handles it reads must therefore outlive the
282        // call, so they are leaked rather than left on the stack.
283        let ctx = Box::leak(Box::new(abi::ClosureCtx::new(
284            emit_tuple,
285            Child::leak(content, Content::Concrete),
286        ))) as *mut abi::ClosureCtx;
287
288        let subst = [
289            view.meta,                        // Self.Type — the concrete receiver
290            content.meta,          // content — the closure emits AnyView
291            view.wt,                          // Self: View
292            content.wt,            // content: View
293        ];
294        let (meta, wt, size) = opaque_result_type(oret, &subst);
295        let mut result = vec![0u8; size];
296        let this = view.value_ptr();
297        // sheet(isPresented:onDismiss:content:)
298        //   x0-x2 = Binding<Bool>, x3/x4 = onDismiss (nil = null fn, null ctx),
299        //   x5 = content fn, x6 = content ctx, x7 = Self.Type,
300        //   [sp] = C.Type, [sp+8] = Self: View WT, [sp+16] = C: View WT
301        core::arch::asm!(
302            "sub sp, sp, #32",
303            "stp {cm}, {sw}, [sp]",
304            "str {cw}, [sp, #16]",
305            "blr {f}",
306            "add sp, sp, #32",
307            f = in(reg) f,
308            cm = in(reg) content.meta,
309            sw = in(reg) view.wt,
310            cw = in(reg) content.wt,
311            in("x0") b0, in("x1") b1, in("x2") b2,
312            in("x3") dismiss[0], in("x4") dismiss[1],
313            in("x5") abi::swift_closure_thunk as *const c_void,
314            in("x6") ctx,
315            in("x7") view.meta,
316            in("x8") result.as_mut_ptr(),
317            in("x20") this,
318            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
319            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
320            lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
321        );
322        wrap_owned(result, meta, wt)
323    }
324}
325
326/// `sheet(isPresented:onDismiss:content:)` bound to Rust state, with a
327/// callback for the closing.
328///
329/// A sheet that saves what was edited has to know when it was dismissed, and
330/// `onDismiss` was hardwired to `nil` — so the sheet could be opened and closed
331/// and nothing on the Rust side would hear about it.
332pub fn sheet_bound_dismiss(
333    view: &ViewHandle,
334    content: &ViewHandle,
335    state: &BoundState<bool>,
336    on_dismiss: impl Fn() + 'static,
337) -> ViewHandle {
338    unsafe {
339        let binding = bool_binding_of(state);
340        let words = super::closures::action_words(on_dismiss);
341        sheet_with_dismiss(view, content, &binding, words)
342    }
343}
344
345/// `sheet` with a fixed flag. It cannot be dismissed: SwiftUI treats a
346/// `Binding.constant` as read-only, so the presentation has no way to write
347/// `false` back. Use [`sheet_bound`] for one the user can close.
348pub fn sheet(view: &ViewHandle, content: &ViewHandle, presented: bool) -> ViewHandle {
349    unsafe {
350        let binding = constant_binding(
351            &[u8::from(presented)],
352            resolve::bool_meta(),
353            mangle::bound(&swift_type!(SwiftUI.Binding), &[mangle::BOOL]).as_bytes(),
354        );
355        sheet_with_binding(view, content, &binding)
356    }
357}
358
359/// `sheet` bound to Rust state, so dismissing it writes `false` back.
360pub fn sheet_bound(view: &ViewHandle, content: &ViewHandle, state: &BoundState<bool>) -> ViewHandle {
361    unsafe {
362        let binding = bool_binding_of(state);
363        sheet_with_binding(view, content, &binding)
364    }
365}
366