Skip to main content

swiftui_native/views/
search.rs

1//! Search fields, file dialogs, and what a presentation offers.
2//!
3//! Grouped because they share an argument vocabulary — a `Binding` for what is
4//! showing, a `Set` or an array of content types for what is allowed, and a
5//! completion taking a `Result` the Rust side has to open.
6
7use super::*;
8use super::closures::*;
9use super::dragdrop::*;
10use crate::abi;
11use crate::resolve;
12use core::ffi::c_void;
13use crate::mangle::{self, swift_type};
14
15/// Apply `.searchSelection(_:)`.
16///
17/// `Binding<TextSelection?>` is forty bytes — past the four registers a
18/// loadable value gets — so it travels by address. The selection itself is
19/// opaque to Rust, so the binding reports "no selection" and `changed` is told
20/// whenever SwiftUI writes one back.
21pub fn search_selection(view: &ViewHandle, changed: impl Fn() + 'static) -> ViewHandle {
22    unsafe {
23        let value_meta = crate::type_by_name!(swift_type!(opt SwiftUI.TextSelection).as_bytes());
24        let payload = crate::type_by_name!(swift_type!(SwiftUI.TextSelection).as_bytes());
25        let width = abi::type_bytes(value_meta);
26        // Zeroing is not `nil`: `TextSelection` has no spare bit pattern that
27        // reads as absent, so the discriminator has to be written explicitly.
28        let mut none = vec![0u8; width];
29        abi::store_optional_tag(&mut none, payload, false);
30        let binding = binding_get_set(
31            value_meta,
32            mangle::ty("Binding")
33                .of(vec![mangle::opt(mangle::ty("TextSelection"))])
34                .mangle()
35                .as_bytes(),
36            Box::new(move |out| {
37                core::ptr::copy_nonoverlapping(none.as_ptr(), out, width);
38            }),
39            Box::new(move |_| changed()),
40        );
41        modifier_indirect(
42            resolve::search_selection_fn(),
43            resolve::search_selection_oret(),
44            view,
45            &binding,
46        )
47    }
48}
49
50/// Which detent a sheet may rest at.
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub enum PresentationDetent {
53    Medium,
54    Large,
55}
56
57impl PresentationDetent {
58    fn getter(self) -> &'static core::ffi::CStr {
59        match self {
60            PresentationDetent::Medium => c"$s7SwiftUI18PresentationDetentV6mediumACvgZ",
61            PresentationDetent::Large => c"$s7SwiftUI18PresentationDetentV5largeACvgZ",
62        }
63    }
64}
65
66/// A `Set<String>` holding `items`.
67pub(crate) unsafe fn string_set(items: &[&str]) -> u64 {
68    unsafe {
69        let meta = resolve::string_meta();
70        let stride = abi::type_bytes(meta);
71        let mut bytes = vec![0u8; stride * items.len()];
72        for (i, s) in items.iter().enumerate() {
73            bytes[i * stride..i * stride + 16].copy_from_slice(&abi::swift_string(s));
74        }
75        abi::swift_set(meta, resolve::string_hashable_wt(), items.len(), &bytes)
76    }
77}
78
79/// Apply `.handlesExternalEvents(preferring:allowing:)`.
80///
81/// Both arguments are real `Set<String>`s built from the caller's strings.
82pub fn handles_external_events(
83    view: &ViewHandle,
84    preferring: &[&str],
85    allowing: &[&str],
86) -> ViewHandle {
87    unsafe {
88        opaque_modifier(
89            resolve::handles_external_events_fn(),
90            resolve::handles_external_events_oret(),
91            view,
92            &[string_set(preferring), string_set(allowing)],
93            [0.0; 4],
94        )
95    }
96}
97
98/// Apply `.presentationDetents(_:)`.
99///
100/// `PresentationDetent` exports no `Hashable` witness table, so the conformance
101/// is looked up at run time the same way a `View` conformance is — which is the
102/// only reason a `Set` of them can be built from outside SwiftUI at all.
103pub fn presentation_detents(view: &ViewHandle, detents: &[PresentationDetent]) -> ViewHandle {
104    unsafe {
105        let meta = abi::metadata0(resolve::presentation_detent_ma());
106        let wt = abi::get_view_wt_for_proto(meta, resolve::hashable_proto());
107        assert!(!wt.is_null(), "PresentationDetent: Hashable");
108        let stride = abi::type_bytes(meta);
109        let mut bytes = vec![0u8; stride * detents.len()];
110        for (i, d) in detents.iter().enumerate() {
111            let v = resilient_static(meta, d.getter());
112            bytes[i * stride..i * stride + v.len()].copy_from_slice(&v);
113        }
114        let set = abi::swift_set(meta, wt, detents.len(), &bytes);
115        opaque_modifier(
116            resolve::presentation_detents_fn(),
117            resolve::presentation_detents_oret(),
118            view,
119            &[set],
120            [0.0; 4],
121        )
122    }
123}
124
125/// Apply `.searchable(text:isPresented:placement:prompt:)`.
126///
127/// The widest modifier bound here: four words of `Binding<String>`, three of
128/// `Binding<Bool>`, one of placement and four of `LocalizedStringKey` — twelve
129/// argument words before the two type arguments, so eight of them spill onto
130/// the stack.
131pub fn searchable(
132    view: &ViewHandle,
133    text: &BoundState<String>,
134    presented: &BoundState<bool>,
135    placement: SearchFieldPlacement,
136    prompt: &str,
137) -> ViewHandle {
138    unsafe {
139        let t = text_binding(text);
140        let p = bool_binding(presented);
141        // One byte wide, but resilient: the getter writes it through x8 and the
142        // modifier takes it by address, so reading x0 gets a register that was
143        // never written.
144        let meta = abi::metadata0(resolve::search_field_placement_ma());
145        let place = resilient_static(meta, placement.getter());
146        let (k0, k1, k2, k3) = lsk_words(prompt);
147        let mut ints: Vec<u64> = words(&t);
148        ints.extend(words(&p));
149        ints.push(place.as_ptr() as u64);
150        ints.extend_from_slice(&[k0, k1, k2, k3]);
151        let subst = [view.meta, view.wt];
152        let (m, wt, size) = opaque_result_type(resolve::searchable_oret(), &subst);
153        let r = abi::call_modifier(resolve::searchable_fn(), view.value_ptr(), view.meta, view.wt, &ints, [0.0; 4], size);
154        wrap_owned(r, m, wt)
155    }
156}
157
158/// Apply `.searchSuggestions(_:for:)`.
159pub fn search_suggestions(
160    view: &ViewHandle,
161    visibility: Visibility,
162    placements: SearchSuggestionsPlacementSet,
163) -> ViewHandle {
164    unsafe {
165        // `Visibility` travels *in* a register; the option set does not.
166        // `Edge.Set` and friends are frozen and come back from their getters in
167        // x0, but these two are resilient — the getter writes through x8, and a
168        // getter's convention and a parameter's are the same question.
169        let vm = abi::metadata0(resolve::visibility_ma());
170        let sm = abi::metadata0(resolve::search_suggestions_placement_set_ma());
171        let p = resilient_static(sm, placements.getter());
172        opaque_modifier(
173            resolve::search_suggestions_fn(),
174            resolve::search_suggestions_oret(),
175            view,
176            &[abi::enum_argument(vm, visibility as u32), p.as_ptr() as u64],
177            [0.0; 4],
178        )
179    }
180}
181
182/// Apply `.textInputFormattingControlVisibility(_:for:)`.
183pub fn text_input_formatting_control_visibility(
184    view: &ViewHandle,
185    visibility: Visibility,
186    placements: TextInputFormattingControlPlacementSet,
187) -> ViewHandle {
188    unsafe {
189        // Resilient, so by address — see [`search_suggestions`].
190        let vm = abi::metadata0(resolve::visibility_ma());
191        let sm = abi::metadata0(resolve::text_input_formatting_set_ma());
192        let p = resilient_static(sm, placements.getter());
193        opaque_modifier(
194            resolve::text_input_formatting_fn(),
195            resolve::text_input_formatting_oret(),
196            view,
197            &[abi::enum_argument(vm, visibility as u32), p.as_ptr() as u64],
198            [0.0; 4],
199        )
200    }
201}
202
203/// Apply `.dismissalConfirmationDialog(_:shouldPresent:actions:message:)`.
204pub fn dismissal_confirmation_dialog(
205    view: &ViewHandle,
206    title: &str,
207    should_present: bool,
208    actions: &ViewHandle,
209    message: &ViewHandle,
210) -> ViewHandle {
211    unsafe {
212        let (k0, k1, k2, k3) = lsk_words(title);
213        two_view_modifier(
214            resolve::dismissal_dialog_fn(),
215            resolve::dismissal_dialog_oret(),
216            view,
217            &[k0, k1, k2, k3, u64::from(should_present)],
218            actions,
219            message,
220        )
221    }
222}
223
224/// The outcome of a file dialog, as much of it as Rust can read.
225#[derive(Clone, Debug, PartialEq, Eq)]
226pub enum FileOutcome {
227    /// The user chose this URL, given as its absolute string.
228    Picked(String),
229    /// The dialog failed. The `Error` is an existential this cannot open.
230    Failed,
231}
232
233/// `(Result<URL, Error>) -> ()`.
234///
235/// `Result` carries an `Error` existential, which puts it well past four words,
236/// so it arrives by address. Its cases are `success` then `failure` and neither
237/// is empty, so the tags are the declaration order — but the tag is read through
238/// the enum's own witness rather than off a fixed byte, because a payload case
239/// stores its discriminator wherever the payload leaves room.
240pub(crate) unsafe extern "C" fn invoke_file_result(
241    a0: u64,
242    _a1: u64,
243    _a2: u64,
244    payload: *mut c_void,
245) {
246    unsafe {
247        let f = boxed_ref::<dyn Fn(FileOutcome)>(payload);
248        let meta = abi::resolve_type_by_mangled_name(mangle::bound(&swift_type!(stdlib enum Result), &[&swift_type!(Foundation.URL), &mangle::existential("s5Error")]).as_bytes());
249        if meta.is_null() {
250            f(FileOutcome::Failed);
251            return;
252        }
253        if abi::read_enum_tag(a0 as *const u8, meta) != 0 {
254            f(FileOutcome::Failed);
255            return;
256        }
257        // The payload sits at offset zero; `absoluteString` is the one accessor
258        // that turns a resilient `URL` back into something Rust can read.
259        let (s0, s1): (u64, u64);
260        core::arch::asm!("blr {f}", f = in(reg) resolve::url_absolute_string(),
261            in("x20") a0,
262            lateout("x0") s0, lateout("x1") s1,
263            lateout("x2") _, lateout("x3") _, lateout("x4") _, lateout("x5") _,
264            lateout("x6") _, lateout("x7") _, lateout("x8") _, lateout("x9") _,
265            lateout("x10") _, lateout("x11") _, lateout("x12") _, lateout("x13") _,
266            lateout("x14") _, lateout("x15") _, lateout("x16") _, lateout("x17") _,
267            lateout("lr") _, lateout("x21") _, clobber_abi("C"),
268        );
269        let mut raw = [0u8; 16];
270        raw[..8].copy_from_slice(&s0.to_le_bytes());
271        raw[8..].copy_from_slice(&s1.to_le_bytes());
272        f(FileOutcome::Picked(abi::read_swift_string(raw)));
273    }
274}
275
276/// Apply `.fileImporter(isPresented:allowedContentTypes:onCompletion:)`.
277pub fn file_importer(
278    view: &ViewHandle,
279    state: &BoundState<bool>,
280    kinds: &[ContentType],
281    completion: impl Fn(FileOutcome) + 'static,
282) -> ViewHandle {
283    unsafe {
284        let mut b = bool_binding(state);
285        let w = binding_words(&mut b);
286        let c = wide_words(invoke_file_result, boxed_ctx(Box::new(completion) as Box<dyn Fn(FileOutcome)>));
287        opaque_modifier(
288            resolve::file_importer_fn(),
289            resolve::file_importer_oret(),
290            view,
291            &[w[0], w[1], w[2], content_types(kinds), c[0], c[1]],
292            [0.0; 4],
293        )
294    }
295}
296
297/// Apply `.searchScopes(_:scopes:)` with `A == Int`.
298///
299/// Two generic parameters carrying *different* constraints — `Hashable` on the
300/// binding's value and `View` on the builder's result — so the tail is three
301/// metadata arguments and three witness tables, in requirement order.
302pub fn search_scopes(
303    view: &ViewHandle,
304    scope: &BoundState<i64>,
305    scopes: &ViewHandle,
306) -> ViewHandle {
307    unsafe {
308        // `Binding<A>` with `A` *generic* is address-only however small the
309        // caller's choice of `A` turns out to be — the callee cannot know its
310        // layout. So a pointer, where `searchable`'s concrete `Binding<String>`
311        // travels in registers.
312        let b = i64_binding(scope);
313        // `Self` is the concrete receiver; the trailing type is the content
314        // the closure emits, which `view_words` erases to an `AnyView`.
315        let av = scopes.meta;
316        let aw = scopes.wt;
317        let im = resolve::int_meta();
318        let ih = resolve::int_hashable_wt();
319        let subst = [view.meta, im, av, view.wt, ih, aw];
320        let (meta, wt, size) = opaque_result_type(resolve::search_scopes_oret(), &subst);
321        let mut ints = vec![b.as_ptr() as u64];
322        ints.extend_from_slice(&view_words(scopes));
323        ints.extend_from_slice(&[view.meta as u64, im as u64, av as u64]);
324        ints.extend_from_slice(&[view.wt as u64, ih as u64, aw as u64]);
325        let r = abi::call_modifier_generic(resolve::search_scopes_fn(), view.value_ptr(), &ints, [0.0; 4], size);
326        wrap_owned(r, meta, wt)
327    }
328}
329
330/// `Optional<Foundation.URL>` in a buffer, ready to pass by address.
331///
332/// `URL.init?(string:)` is the only exported way in — `init(fileURLWithPath:)`
333/// has no symbol at all — and its parameter is `__shared String`, which is why
334/// the mangling ends `SSh_` rather than `SS_`. It is failable, so what comes
335/// back through x8 is already the optional the modifier wants.
336pub(crate) unsafe fn optional_url(spec: Option<&str>) -> Vec<u8> {
337    unsafe {
338        let payload = abi::metadata0(resolve::url_ma());
339        let opt = crate::type_by_name!(swift_type!(opt Foundation.URL).as_bytes());
340        assert!(!opt.is_null(), "Optional<URL>");
341        let mut buf = vec![0u8; abi::type_bytes(opt)];
342        match spec {
343            Some(s) => {
344                let ss = abi::swift_string(s);
345                let s0 = u64::from_le_bytes(ss[..8].try_into().expect("url lo"));
346                let s1 = u64::from_le_bytes(ss[8..].try_into().expect("url hi"));
347                core::arch::asm!("blr {f}", f = in(reg) resolve::url_string_init(),
348                    in("x0") s0, in("x1") s1,
349                    in("x8") buf.as_mut_ptr(), in("x20") payload,
350                    lateout("x2") _, lateout("x3") _, lateout("x4") _, lateout("x5") _,
351                    lateout("x6") _, lateout("x7") _, lateout("x9") _, lateout("x10") _,
352                    lateout("x11") _, lateout("x12") _, lateout("x13") _, lateout("x14") _,
353                    lateout("x15") _, lateout("x16") _, lateout("x17") _, lateout("lr") _,
354                    lateout("x21") _, clobber_abi("C"),
355                );
356            }
357            None => abi::store_optional_tag(&mut buf, payload, false),
358        }
359        buf
360    }
361}
362
363/// Apply `.fileMover(isPresented:file:onCompletion:)`.
364pub fn file_mover(
365    view: &ViewHandle,
366    state: &BoundState<bool>,
367    file: Option<&str>,
368    completion: impl Fn(FileOutcome) + 'static,
369) -> ViewHandle {
370    unsafe {
371        let mut b = bool_binding(state);
372        let w = binding_words(&mut b);
373        let url = optional_url(file);
374        let c = wide_words(invoke_file_result, boxed_ctx(Box::new(completion) as Box<dyn Fn(FileOutcome)>));
375        opaque_modifier(
376            resolve::file_mover_fn(),
377            resolve::file_mover_oret(),
378            view,
379            &[w[0], w[1], w[2], url.as_ptr() as u64, c[0], c[1]],
380            [0.0; 4],
381        )
382    }
383}
384
385/// Apply `.fileExporter(isPresented:item:contentTypes:defaultFilename:onCompletion:onCancellation:)`
386/// with `Item == String`.
387///
388/// `String` is the one `Transferable` reachable without declaring a type, so it
389/// is what gets exported; the item is `Optional<String>`, which is two words and
390/// a tag and so travels in registers.
391pub fn file_exporter(
392    view: &ViewHandle,
393    state: &BoundState<bool>,
394    item: Option<&str>,
395    kinds: &[ContentType],
396    default_filename: Option<&str>,
397    completion: impl Fn(FileOutcome) + 'static,
398    cancellation: impl Fn() + 'static,
399) -> ViewHandle {
400    unsafe {
401        let mut b = bool_binding(state);
402        let w = binding_words(&mut b);
403        let meta = resolve::string_meta();
404        let opt = abi::resolve_type_by_mangled_name(mangle::optional(mangle::STRING).as_bytes());
405        // `Optional<Item>` with `Item` *generic* is address-only, however small
406        // the caller's choice turns out to be — the same rule that puts a
407        // generic `Binding` behind a pointer. `defaultFilename` beside it is a
408        // concrete `Optional<String>` and does travel in registers.
409        let mut boxed = vec![0u8; abi::type_bytes(opt)];
410        if let Some(s) = item {
411            boxed[..16].copy_from_slice(&abi::swift_string(s));
412        }
413        abi::store_optional_tag(&mut boxed, meta, item.is_some());
414        let name = optional_words(
415            meta,
416            opt,
417            &default_filename.map(|s| abi::swift_string(s)).unwrap_or([0u8; 16]),
418            default_filename.is_some(),
419        );
420        let done = wide_words(invoke_file_result, boxed_ctx(Box::new(completion) as Box<dyn Fn(FileOutcome)>));
421        let cancel = action_words(cancellation);
422        let mut ints = w.to_vec();
423        ints.push(boxed.as_ptr() as u64);
424        ints.push(content_types(kinds));
425        ints.extend_from_slice(&name);
426        ints.extend_from_slice(&done);
427        ints.extend_from_slice(&cancel);
428        generic1_modifier_ints(
429            resolve::file_exporter_fn(),
430            resolve::file_exporter_oret(),
431            view,
432            meta,
433            {
434                // `String: Transferable` is not an exported witness table; the
435                // conformance is found the same way a `View` one is.
436                let wt = abi::get_view_wt_for_proto(meta, resolve::transferable_proto());
437                assert!(!wt.is_null(), "String: Transferable");
438                wt
439            },
440            &ints,
441            [0.0; 4],
442        )
443    }
444}
445
446/// Apply `.navigationDocument(_:)` with a `URL`.
447///
448/// The non-generic overload — `URL` is resilient, so it goes by address, and the
449/// optional the initialiser hands back is unwrapped by pointing past its tag:
450/// a `.some` payload starts at offset zero.
451pub fn navigation_document_url(view: &ViewHandle, spec: &str) -> ViewHandle {
452    unsafe {
453        let url = optional_url(Some(spec));
454        opaque_modifier(
455            resolve::navigation_document_url_fn(),
456            resolve::navigation_document_url_oret(),
457            view,
458            &[url.as_ptr() as u64],
459            [0.0; 4],
460        )
461    }
462}
463
464/// Apply `.navigationDocument(_:)` with a `Transferable` item.
465///
466/// `String` is the one `Transferable` reachable without declaring a type. The
467/// item is a bare generic parameter, so it travels by address.
468pub fn navigation_document(view: &ViewHandle, item: &str) -> ViewHandle {
469    unsafe {
470        let meta = resolve::string_meta();
471        let wt = abi::get_view_wt_for_proto(meta, resolve::transferable_proto());
472        assert!(!wt.is_null(), "String: Transferable");
473        let s = abi::swift_string(item);
474        generic1_modifier_ints(
475            resolve::navigation_document_item_fn(),
476            resolve::navigation_document_item_oret(),
477            view,
478            meta,
479            wt,
480            &[s.as_ptr() as u64],
481            [0.0; 4],
482        )
483    }
484}
485
486/// Apply `.fileDialogURLEnabled(_:)`.
487///
488/// The argument is a `Foundation.Predicate<Pack{URL}>`, which the `#Predicate`
489/// macro normally builds from an expression tree at compile time. `Predicate`
490/// also exposes `.true` and `.false` as statics, and those are values rather
491/// than syntax — so they are reachable from here, where an arbitrary predicate
492/// is not.
493///
494/// The type is generic over a *parameter pack*, whose mangling closes with
495/// `_QP` rather than directly; see [`crate::mangle::pack`].
496pub fn file_dialog_url_enabled(view: &ViewHandle, enabled: bool) -> ViewHandle {
497    unsafe {
498        let meta = abi::resolve_type_by_mangled_name(
499            mangle::pack(
500                mangle::Type::Nominal {
501                    module: "Foundation",
502                    path: vec!["Predicate"],
503                    kind: mangle::Kind::Struct,
504                },
505                vec![mangle::Type::Nominal {
506                    module: "Foundation",
507                    path: vec!["URL"],
508                    kind: mangle::Kind::Struct,
509                }],
510            )
511            .mangle()
512            .as_bytes(),
513        );
514        assert!(!meta.is_null(), "Predicate<Pack{{URL}}> metadata");
515        let getter = if enabled { resolve::predicate_true() } else { resolve::predicate_false() };
516        let value = abi::static_out(getter, meta, abi::type_bytes(meta));
517        opaque_modifier(
518            resolve::file_dialog_url_enabled_fn(),
519            resolve::file_dialog_url_enabled_oret(),
520            view,
521            &[value.as_ptr() as u64],
522            [0.0; 4],
523        )
524    }
525}