Skip to main content

swiftui_native/views/
controls.rs

1//! Button.
2//!
3//! * Scroll
4//! * Window
5
6use super::*;
7use crate::abi;
8use crate::resolve;
9use core::ffi::c_void;
10use crate::mangle;
11use crate::mangle::swift_type;
12
13// ── Button ──
14
15mangle::result_types! {
16    /// `SwiftUI.Button<SwiftUI.Text>`
17    /// `Button<Text>`
18    pub(crate) BUTTON_T = ty("Button").of(vec![ty("Text")]);
19    /// `Button<AnyView>`
20    /// `Button<AnyView>`
21    pub(crate) BUTTON_LABEL_T = ty("Button").of(vec![ty("AnyView")]);
22}
23
24/// Trampoline for a Button action: a void closure ignores the `@out` slot.
25pub(crate) unsafe extern "C" fn invoke_action(_out: *mut u8, payload: *mut c_void) {
26    let f = unsafe { boxed_ref::<dyn Fn()>(payload) };
27    f();
28}
29
30swift_statics! {
31    /// `SwiftUI.ButtonRole` — what a button *is*, not how it looks.
32    ///
33    /// The style system reads it: a destructive button is red wherever red
34    /// means destructive, and a cancel button is the one a sheet's dismiss
35    /// gesture triggers. Setting a colour by hand gets the first and none of
36    /// the rest.
37    pub enum ButtonRole {
38        Destructive = c"$s7SwiftUI10ButtonRoleV11destructiveACvgZ",
39        Cancel = c"$s7SwiftUI10ButtonRoleV6cancelACvgZ",
40        Confirm = c"$s7SwiftUI10ButtonRoleV7confirmACvgZ",
41        Close = c"$s7SwiftUI10ButtonRoleV5closeACvgZ",
42    }
43}
44
45/// `Button(_:role:action:)`.
46///
47/// `None` is the plain button [`button`] builds; the roles are what Apple's
48/// `Button("Delete", role: .destructive)` is about.
49pub fn button_role(
50    label: &str,
51    role: Option<ButtonRole>,
52    callback: impl Fn() + 'static,
53) -> ViewHandle {
54    unsafe {
55        let ss = abi::swift_string(label);
56        let s0 = u64::from_le_bytes(ss[..8].try_into().expect("lo"));
57        let s1 = u64::from_le_bytes(ss[8..].try_into().expect("hi"));
58        let (k0, k1, k2, k3) = localized_string_key(s0, s1);
59
60        let boxed: Box<dyn Fn()> = Box::new(callback);
61        let payload = Box::into_raw(Box::new(boxed)) as *mut c_void;
62        let ctx = Box::leak(Box::new(abi::ClosureCtx::new(run_boxed_action, payload)))
63            as *mut abi::ClosureCtx;
64
65        // `ButtonRole` is resilient, so the optional travels by address.
66        let role_meta = abi::metadata0(resolve::button_role_ma());
67        assert!(!role_meta.is_null(), "ButtonRole metadata");
68        let value = role.map(|r| {
69            super::static_getter_indirect(r.resolved(), role_meta, abi::type_bytes(role_meta))
70        });
71        let mut arg = super::opt_resilient_bytes(
72            role_meta,
73            swift_type!(opt SwiftUI.ButtonRole).as_bytes(),
74            value.as_deref(),
75        );
76
77        let (meta, wt, size) = result_type(&BUTTON_T);
78        let mut result = vec![0u8; size];
79        // `Button<Text>.init(_:role:action:)`
80        //   x0-x3 = LocalizedStringKey, x4 = @in ButtonRole?,
81        //   x5 = action fn, x6 = action ctx, x8 = @out
82        core::arch::asm!("blr {f}", f = in(reg) resolve::button_role_init(),
83            in("x0") k0, in("x1") k1, in("x2") k2, in("x3") k3,
84            in("x4") arg.as_mut_ptr(),
85            in("x5") abi::swift_closure_thunk as *const c_void,
86            in("x6") ctx,
87            in("x8") result.as_mut_ptr(),
88            in("x20") meta,
89            lateout("x7") _,
90            lateout("x9") _, lateout("x10") _, lateout("x11") _,
91            lateout("x12") _, lateout("x13") _, lateout("x14") _, lateout("x15") _,
92            lateout("x16") _, lateout("x17") _, lateout("lr") _, lateout("x21") _,
93            clobber_abi("C"),
94        );
95        wrap_owned(result, meta, wt)
96    }
97}
98
99/// Create a `Button` with a text label and an action.
100///
101/// The action may capture. It was `fn()` — a bare pointer stored as the context
102/// — which meant a button could not know anything about where it was: a row's
103/// checkmark could not be told which row it belonged to, and every list had to
104/// route through a global. `button_labelled` next door already boxed its
105/// closure; this now does the same.
106pub fn button(label: &str, callback: impl Fn() + 'static) -> ViewHandle {
107    unsafe {
108        let ss = abi::swift_string(label);
109        let s0 = u64::from_le_bytes(ss[..8].try_into().unwrap());
110        let s1 = u64::from_le_bytes(ss[8..].try_into().unwrap());
111        let (k0, k1, k2, k3) = localized_string_key(s0, s1);
112
113        // SwiftUI keeps the action for as long as the button exists, so the
114        // context outlives this call.
115        let boxed: Box<dyn Fn()> = Box::new(callback);
116        let payload = Box::into_raw(Box::new(boxed)) as *mut c_void;
117        let ctx = Box::leak(Box::new(abi::ClosureCtx::new(run_boxed_action, payload)))
118            as *mut abi::ClosureCtx;
119
120        let (meta, wt, size) = result_type(&BUTTON_T);
121        let mut result = vec![0u8; size];
122        // Button<Text>.init(_:action:)
123        //   x0-x3 = LocalizedStringKey, x4 = action fn, x5 = action ctx, x8 = @out
124        core::arch::asm!("blr {f}", f = in(reg) resolve::button_init(),
125            in("x0") k0, in("x1") k1, in("x2") k2, in("x3") k3,
126            in("x4") abi::swift_closure_thunk as *const c_void,
127            in("x5") ctx,
128            in("x8") result.as_mut_ptr(),
129            in("x20") meta,
130            lateout("x6") _, lateout("x7") _,
131            lateout("x9") _, lateout("x10") _, lateout("x11") _,
132            lateout("x12") _, lateout("x13") _, lateout("x14") _, lateout("x15") _,
133            lateout("x16") _, lateout("x17") _, lateout("lr") _, lateout("x21") _,
134            clobber_abi("C"),
135        );
136        wrap_owned(result, meta, wt)
137    }
138}
139
140/// Create a `Button` with raw C callback.
141pub fn button_raw(
142    label: &str,
143    _callback: unsafe extern "C" fn(*mut c_void),
144    _userdata: *mut c_void,
145) -> ViewHandle {
146    text(label)
147}
148
149// ── Scroll ──
150
151/// Wrap in a vertical `ScrollView` with indicators shown.
152pub fn scroll(view: &ViewHandle) -> ViewHandle {
153    unsafe {
154        // The scroll view's content is the view itself, kept below as a copy
155        // the closure can hand back each time it is asked.
156        let content_meta = view.meta;
157        let content_wt = view.wt;
158        let sv_meta = abi::generic_metadata2(resolve::scrollview_ma(), content_meta, content_wt);
159        assert!(!sv_meta.is_null(), "ScrollView metadata");
160        let sv_wt = abi::get_view_wt(sv_meta);
161        assert!(!sv_wt.is_null(), "ScrollView !: View");
162
163        let child = view.clone();
164        let kid_handles = Child::new(&child, Content::Concrete);
165        let handles = kid_handles.element();
166        let payload = TuplePayload { children: handles.as_ptr(), count: 1 };
167        let mut ctx = abi::ClosureCtx::new(emit_tuple, &payload as *const TuplePayload as *mut c_void);
168
169        let mut result = vec![0u8; abi::type_bytes(sv_meta)];
170        // ScrollView.init(_:showsIndicators:content:)
171        //   x0 = Axis.Set (vertical == 2), x1 = showsIndicators,
172        //   x2 = closure fn, x3 = closure ctx,
173        //   x4 = Content.Type, x5 = Content: View WT, x8 = @out
174        core::arch::asm!("blr {f}", f = in(reg) resolve::scrollview_init(),
175            in("x0") 2u64,
176            in("x1") 1u64,
177            in("x2") abi::swift_closure_thunk as *const c_void,
178            in("x3") &mut ctx as *mut abi::ClosureCtx,
179            in("x4") content_meta,
180            in("x5") content_wt,
181            in("x8") result.as_mut_ptr(),
182            lateout("x6") _, lateout("x7") _,
183            lateout("x9") _, lateout("x10") _, lateout("x11") _,
184            lateout("x12") _, lateout("x13") _, lateout("x14") _, lateout("x15") _,
185            lateout("x16") _, lateout("x17") _, lateout("lr") _,
186            lateout("x20") _, lateout("x21") _,
187            clobber_abi("C"),
188        );
189        wrap_owned(result, sv_meta, sv_wt)
190    }
191}
192
193// ── Window ──
194
195/// Show a view in a native window and run the event loop.
196///
197/// On the Mac this is `NSWindow` + `NSApp.run()`. On iOS it is the UIKit entry
198/// point, which is behind the `uikit-entry` feature: `App.main()` needs none of
199/// it, and declaring `UIApplicationMain` is what puts UIKit in the binary.
200#[cfg(any(target_os = "macos", feature = "uikit-entry"))]
201pub fn show_window(view: &ViewHandle, title: &str, width: f64, height: f64) {
202    crate::window::show_window(view, title, width, height);
203}
204
205/// Which `ProgressViewStyle` to apply.
206///
207/// Each case is a distinct Swift type with its own conformance, which is why
208/// the modifier is generic in Swift and takes a value here.
209#[derive(Clone, Copy, Debug, PartialEq, Eq)]
210pub enum ProgressStyle {
211    /// `.automatic` — `DefaultProgressViewStyle`.
212    Automatic,
213    /// `.circular` — a spinner.
214    Circular,
215    /// `.linear` — a bar.
216    Linear,
217}
218
219impl ProgressStyle {
220    /// The style's metadata accessor, initialiser and `ProgressViewStyle`
221    /// witness.
222    fn parts(self) -> (*const c_void, *const c_void, *const c_void) {
223        match self {
224            Self::Automatic => (
225                resolve::default_progress_ma(),
226                resolve::default_progress_init(),
227                resolve::default_progress_wt(),
228            ),
229            Self::Circular => (
230                resolve::circular_progress_ma(),
231                resolve::circular_progress_init(),
232                resolve::circular_progress_wt(),
233            ),
234            Self::Linear => (
235                resolve::linear_progress_ma(),
236                resolve::linear_progress_init(),
237                resolve::linear_progress_wt(),
238            ),
239        }
240    }
241}
242
243/// Apply `.progressViewStyle(_:)`.
244///
245/// Not missing after all. Searching the export table for `progressViewStyle`
246/// finds nothing because the mangling compresses `ViewStyle` to
247/// `08progressC5Style` — `C` substitutes the `View` of the enclosing
248/// `SwiftUI.View` — so the spelled-out name never appears. The SIL for one line
249/// of Swift names it outright.
250///
251/// `.linear` and `.automatic` are `@_alwaysEmitIntoClient` statics with no
252/// symbol of their own; the structs they return have plain initialisers, which
253/// is what this needs.
254pub fn progress_view_style(view: &ViewHandle, style: ProgressStyle) -> ViewHandle {
255    unsafe {
256        let (ma, init, wt) = style.parts();
257        let style_meta = abi::metadata0(ma);
258        assert!(!style_meta.is_null(), "ProgressViewStyle metadata");
259        let mut style_buf = vec![0u8; abi::type_bytes(style_meta)];
260        core::arch::asm!("blr {f}", f = in(reg) init,
261            in("x8") style_buf.as_mut_ptr(),
262            in("x20") style_meta,
263            lateout("x0") _, lateout("x1") _, lateout("x2") _, lateout("x3") _,
264            lateout("x4") _, lateout("x5") _, lateout("x6") _, lateout("x7") _,
265            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
266            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
267            lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
268        );
269
270        let subst = [view.meta, style_meta, view.wt, wt];
271        let (meta, rwt, size) =
272            super::opaque_result_type(resolve::progress_view_style_oret(), &subst);
273
274        let mut result = vec![0u8; size];
275        let this = view.value_ptr();
276        // progressViewStyle<S: ProgressViewStyle>(_:)
277        //   x0 = @in_guaranteed style, then the four generic words, x20 = self.
278        core::arch::asm!("blr {f}", f = in(reg) resolve::progress_view_style_fn(),
279            in("x0") style_buf.as_mut_ptr(),
280            in("x1") subst[0], in("x2") subst[1], in("x3") subst[2], in("x4") subst[3],
281            in("x8") result.as_mut_ptr(),
282            in("x20") this,
283            lateout("x5") _, lateout("x6") _, lateout("x7") _, lateout("x9") _,
284            lateout("x10") _, lateout("x11") _, lateout("x12") _, lateout("x13") _,
285            lateout("x14") _, lateout("x15") _, lateout("x16") _, lateout("x17") _,
286            lateout("lr") _, lateout("x21") _, clobber_abi("C"),
287        );
288        wrap_owned(result, meta, rwt)
289    }
290}
291
292/// Apply `.progressViewStyle(.circular)`.
293pub fn progress_view_style_circular(view: &ViewHandle) -> ViewHandle {
294    progress_view_style(view, ProgressStyle::Circular)
295}
296
297/// Apply `.buttonStyle(.glass(.clear))`.
298///
299/// `.glass(_:)` is a static on a `PrimitiveButtonStyle` extension constrained
300/// to `GlassButtonStyle` — the shape that has been inlinable every other time
301/// in this file. This one is exported, which is only knowable by looking.
302pub fn button_style_glass_clear(view: &ViewHandle) -> ViewHandle {
303    unsafe {
304        let glass_meta = abi::metadata0(resolve::glass_ma());
305        assert!(!glass_meta.is_null(), "Glass metadata");
306        let mut glass = super::static_getter_indirect(
307            resolve::glass_clear(),
308            glass_meta,
309            abi::type_bytes(glass_meta),
310        );
311
312        let style_meta = abi::metadata0(resolve::glass_button_style_ma());
313        assert!(!style_meta.is_null(), "GlassButtonStyle metadata");
314        let mut style = vec![0u8; abi::type_bytes(style_meta)];
315        core::arch::asm!("blr {f}", f = in(reg) resolve::glass_button_style_of(),
316            in("x0") glass.as_mut_ptr(),
317            in("x8") style.as_mut_ptr(),
318            in("x20") style_meta,
319            lateout("x1") _, lateout("x2") _, lateout("x3") _, lateout("x4") _,
320            lateout("x5") _, lateout("x6") _, lateout("x7") _, lateout("x9") _,
321            lateout("x10") _, lateout("x11") _, lateout("x12") _, lateout("x13") _,
322            lateout("x14") _, lateout("x15") _, lateout("x16") _, lateout("x17") _,
323            lateout("lr") _, lateout("x21") _, clobber_abi("C"),
324        );
325
326        let style_wt = resolve::glass_button_style_wt();
327        let subst = [view.meta, style_meta, view.wt, style_wt];
328        let (meta, wt, size) = super::opaque_result_type(resolve::button_style_oret(), &subst);
329        let mut result = vec![0u8; size];
330        let this = view.value_ptr();
331        core::arch::asm!("blr {f}", f = in(reg) resolve::button_style_fn(),
332            in("x0") style.as_mut_ptr(),
333            in("x1") subst[0], in("x2") subst[1], in("x3") subst[2], in("x4") subst[3],
334            in("x8") result.as_mut_ptr(),
335            in("x20") this,
336            lateout("x5") _, lateout("x6") _, lateout("x7") _, lateout("x9") _,
337            lateout("x10") _, lateout("x11") _, lateout("x12") _, lateout("x13") _,
338            lateout("x14") _, lateout("x15") _, lateout("x16") _, lateout("x17") _,
339            lateout("lr") _, lateout("x21") _, clobber_abi("C"),
340        );
341        wrap_owned(result, meta, wt)
342    }
343}
344
345/// `Button(action:label:)` — a button whose label is a view rather than a string.
346///
347/// The action is an escaping `() -> Void`, so its context is leaked; the label
348/// closure is `@noescape` and resolved during the call, so that context can live
349/// on this frame.
350pub fn button_labelled(label: &ViewHandle, action: impl Fn() + 'static) -> ViewHandle {
351    unsafe {
352        let boxed: Box<dyn Fn()> = Box::new(action);
353        let payload = Box::into_raw(Box::new(boxed)) as *mut c_void;
354        let act = Box::leak(Box::new(abi::ClosureCtx::new(run_boxed_action, payload)))
355            as *mut abi::ClosureCtx;
356
357        let (known_meta, known_wt, known_size) = result_type(&BUTTON_LABEL_T);
358        // `Button<Label>`: one parameter, then its `View` conformance.
359        let typed = super::substitute_content(known_meta, 2, 0, 1, label.meta, label.wt);
360        let (meta, wt) = typed.unwrap_or((known_meta, known_wt));
361        let size = if typed.is_some() { abi::type_bytes(meta) } else { known_size };
362        let (label_meta, label_wt) = match typed {
363            Some(_) => (label.meta, label.wt),
364            None => (resolve::anyview_meta(), resolve::anyview_wt()),
365        };
366        let kind = if typed.is_some() { super::Content::Concrete } else { super::Content::Erased };
367
368        let kid_handles = super::Child::new(label, kind);
369        let handles = kid_handles.element();
370        let tuple = super::TuplePayload { children: handles.as_ptr(), count: 1 };
371        let mut ctx = abi::ClosureCtx::new(
372            super::emit_tuple,
373            &tuple as *const super::TuplePayload as *mut c_void,
374        );
375
376        let mut result = vec![0u8; size];
377        // Button.init(action:label:)
378        //   x0 = action fn, x1 = action ctx, x2 = label fn, x3 = label ctx,
379        //   x4 = Label.Type, x5 = Label: View WT, x8 = @out
380        core::arch::asm!("blr {f}", f = in(reg) resolve::button_label_init(),
381            in("x0") abi::swift_closure_thunk as *const c_void,
382            in("x1") act,
383            in("x2") abi::swift_closure_thunk as *const c_void,
384            in("x3") &mut ctx as *mut abi::ClosureCtx,
385            in("x4") label_meta,
386            in("x5") label_wt,
387            in("x8") result.as_mut_ptr(),
388            lateout("x6") _, lateout("x7") _, lateout("x9") _, lateout("x10") _,
389            lateout("x11") _, lateout("x12") _, lateout("x13") _, lateout("x14") _,
390            lateout("x15") _, lateout("x16") _, lateout("x17") _, lateout("lr") _,
391            lateout("x20") _, lateout("x21") _, clobber_abi("C"),
392        );
393        wrap_owned(result, meta, wt)
394    }
395}
396
397/// The `() -> Void` a `Button`'s action is.
398/// Call a boxed `Fn()` held as a closure context.
399///
400/// The counterpart of [`invoke_action`], which transmutes the context back to a
401/// bare `fn()` — which is exactly what a capturing closure cannot be.
402pub(crate) unsafe extern "C" fn run_boxed_action(_out: *mut u8, payload: *mut c_void) {
403    unsafe { (*(payload as *const Box<dyn Fn()>))() };
404}