Skip to main content

swiftui_native/views/
input.rs

1//! Keys, pointers, the Touch Bar, and haptics.
2//!
3//! Input that arrives as a value rather than as a gesture. The recurring
4//! difficulty is enum tags: `CoordinateSpace` and `HoverPhase` both carry
5//! payload cases that are not in the public API, which moves every tag after
6//! them.
7
8use super::*;
9use super::closures::*;
10use crate::abi;
11use crate::resolve;
12use core::ffi::c_void;
13
14/// Apply `.onModifierKeysChanged(mask:initial:_:)`.
15///
16/// The callback receives the old and new masks as raw `EventModifiers` bits.
17pub fn on_modifier_keys_changed(
18    view: &ViewHandle,
19    mask: EventModifier,
20    initial: bool,
21    action: impl Fn(u64, u64) + 'static,
22) -> ViewHandle {
23    unsafe {
24        let meta = abi::metadata0(resolve::event_modifiers_ma());
25        // `EventModifiers` is frozen and one word, so it goes in a register.
26        let m = frozen_static(meta, mask.getter());
27        let c = pair_words(invoke_word_pair, boxed_ctx(Box::new(action) as Box<dyn Fn(u64, u64)>));
28        opaque_modifier(
29            resolve::on_modifier_keys_fn(),
30            resolve::on_modifier_keys_oret(),
31            view,
32            &[m, u64::from(initial), c[0], c[1]],
33            [0.0; 4],
34        )
35    }
36}
37
38/// Apply `.onPencilDoubleTap(perform:)`.
39pub fn on_pencil_double_tap(view: &ViewHandle, action: impl Fn() + 'static) -> ViewHandle {
40    unsafe {
41        opaque_modifier(
42            resolve::on_pencil_double_tap_fn(),
43            resolve::on_pencil_double_tap_oret(),
44            view,
45            &ignoring_value_words(action),
46            [0.0; 4],
47        )
48    }
49}
50
51/// Apply `.onPencilSqueeze(perform:)`.
52pub fn on_pencil_squeeze(view: &ViewHandle, action: impl Fn() + 'static) -> ViewHandle {
53    unsafe {
54        opaque_modifier(
55            resolve::on_pencil_squeeze_fn(),
56            resolve::on_pencil_squeeze_oret(),
57            view,
58            &ignoring_value_words(action),
59            [0.0; 4],
60        )
61    }
62}
63
64/// Apply `.touchBar(content:)`.
65pub fn touch_bar(view: &ViewHandle, content: &ViewHandle) -> ViewHandle {
66    unsafe {
67        content_modifier(resolve::touch_bar_fn(), resolve::touch_bar_oret(), view, &[], content)
68    }
69}
70
71/// Apply `.modifierKeyAlternate(_:_:)`.
72pub fn modifier_key_alternate(
73    view: &ViewHandle,
74    modifiers: EventModifier,
75    content: &ViewHandle,
76) -> ViewHandle {
77    unsafe {
78        let meta = abi::metadata0(resolve::event_modifiers_ma());
79        let m = frozen_static(meta, modifiers.getter());
80        content_modifier(
81            resolve::modifier_key_alternate_fn(),
82            resolve::modifier_key_alternate_oret(),
83            view,
84            &[m],
85            content,
86        )
87    }
88}
89
90/// Apply `.onCommand(_:perform:)`.
91///
92/// The selector is an Objective-C one, registered by name.
93pub fn on_command(view: &ViewHandle, selector: &core::ffi::CStr, action: impl Fn() + 'static) -> ViewHandle {
94    unsafe {
95        let sel = crate::objc::sel(selector);
96        let c = action_words(action);
97        opaque_modifier(
98            resolve::on_command_fn(),
99            resolve::on_command_oret(),
100            view,
101            &[sel as u64, c[0], c[1]],
102            [0.0; 4],
103        )
104    }
105}
106
107pub(crate) unsafe extern "C" fn ask_condition(payload: *mut c_void, arg: u64) -> u64 {
108    unsafe {
109        let f = boxed_ref::<dyn Fn(i64, i64) -> bool>(payload);
110        // `(A, A) -> Bool` with `A` concrete and one word: old in x0, new in x1.
111        // The wide thunk is not needed, but only x0 reaches here, so the new
112        // value is what the condition sees.
113        u64::from(f(arg as i64, arg as i64))
114    }
115}
116
117/// Apply `.sensoryFeedback(_:trigger:condition:)`.
118///
119/// `condition` decides, from the trigger's old and new values, whether the
120/// feedback plays.
121pub fn sensory_feedback(
122    view: &ViewHandle,
123    feedback: SensoryFeedback,
124    trigger: i64,
125    condition: impl Fn(i64, i64) -> bool + 'static,
126) -> ViewHandle {
127    unsafe {
128        let meta = abi::metadata0(resolve::sensory_feedback_ma());
129        let f = resilient_static(meta, feedback.getter());
130        let mut t = trigger;
131        let c = word_closure(ask_condition, boxed_ctx(Box::new(condition) as Box<dyn Fn(i64, i64) -> bool>));
132        generic1_modifier_ints(
133            resolve::sensory_feedback_fn(),
134            resolve::sensory_feedback_oret(),
135            view,
136            resolve::int_meta(),
137            resolve::int_equatable_wt(),
138            &[f.as_ptr() as u64, &mut t as *mut i64 as u64, c[0], c[1]],
139            [0.0; 4],
140        )
141    }
142}
143
144/// Where a hover's location is measured.
145///
146/// `.named` carries an `AnyHashable`, which makes the enum address-only — so
147/// unlike the small SwiftUI enums this one is built in a buffer and passed by
148/// pointer. Payload cases are numbered first, so `.named` is 0 and the two
149/// cases spelled here follow *both* of them: `.named(AnyHashable)` is 0 and the
150/// undocumented `.id(CoordinateSpace.ID)` is 1, so `.global` is 2 and `.local`
151/// is 3. Guessing from the public cases alone lands on `.id`, which renders
152/// without complaint against a coordinate space that was never named.
153#[derive(Clone, Copy, Debug, PartialEq, Eq)]
154pub enum CoordinateSpace {
155    Global,
156    Local,
157}
158
159impl CoordinateSpace {
160    fn tag(self) -> u32 {
161        match self {
162            CoordinateSpace::Global => 2,
163            CoordinateSpace::Local => 3,
164        }
165    }
166}
167
168/// Which phase of a continuous hover SwiftUI is reporting.
169#[derive(Clone, Copy, Debug, PartialEq, Eq)]
170pub enum HoverPhase {
171    /// The pointer is inside the view; the location is not surfaced here.
172    Active,
173    /// The pointer has left.
174    Ended,
175}
176
177/// `(HoverPhase) -> ()`.
178///
179/// `.active` carries a `CGPoint`, so the enum is address-only and x0 is the
180/// address of the value; the tag comes from the enum's own witness rather than
181/// from reading a byte, because a payload case stores its discriminator
182/// wherever the payload leaves room.
183unsafe extern "C" fn saw_hover(value: *const u8, payload: *mut c_void) {
184    unsafe {
185        let f = boxed_ref::<dyn Fn(HoverPhase)>(payload);
186        let meta = abi::metadata0(resolve::hover_phase_ma());
187        let phase = match abi::read_enum_tag(value, meta) {
188            0 => HoverPhase::Active,
189            _ => HoverPhase::Ended,
190        };
191        f(phase);
192    }
193}
194
195/// Apply `.onContinuousHover(coordinateSpace:perform:)`.
196pub fn on_continuous_hover(
197    view: &ViewHandle,
198    space: CoordinateSpace,
199    perform: impl Fn(HoverPhase) + 'static,
200) -> ViewHandle {
201    unsafe {
202        let meta = abi::metadata0(resolve::coordinate_space_ma());
203        let mut buf = vec![0u8; abi::type_bytes(meta)];
204        abi::inject_enum_tag(&mut buf, meta, space.tag());
205        let ctx = Box::leak(Box::new(abi::ValueClosureCtx::new(
206            saw_hover,
207            boxed_ctx(Box::new(perform) as Box<dyn Fn(HoverPhase)>),
208        ))) as *mut abi::ValueClosureCtx;
209        let subst = [view.meta, view.wt];
210        let (m, wt, size) = opaque_result_type(resolve::on_continuous_hover_oret(), &subst);
211        let r = abi::call_modifier(resolve::on_continuous_hover_fn(), view.value_ptr(), view.meta, view.wt,
212            &[
213                buf.as_ptr() as u64,
214                abi::value_closure_thunk as *const () as u64,
215                ctx as u64,
216            ],
217            [0.0; 4],
218            size,
219        );
220        wrap_owned(r, m, wt)
221    }
222}
223
224/// Apply `.touchBarItemPresence(_:)`.
225///
226/// The enum holds a `String`, which makes it larger than four words and so
227/// address-only: the argument is a pointer to a buffer this builds, with the
228/// string's two words written first and the tag injected by the enum's own
229/// witness afterwards.
230#[cfg(target_os = "macos")]
231pub fn touch_bar_item_presence(
232    view: &ViewHandle,
233    presence: TouchBarItemPresence,
234    identifier: &str,
235) -> ViewHandle {
236    unsafe {
237        let meta = abi::metadata0(resolve::touch_bar_item_presence_ma());
238        let mut buf = vec![0u8; abi::type_bytes(meta)];
239        buf[..16].copy_from_slice(&abi::swift_string(identifier));
240        abi::inject_enum_tag(&mut buf, meta, presence as u32);
241        let subst = [view.meta, view.wt];
242        let (m, wt, size) = opaque_result_type(resolve::touch_bar_item_presence_oret(), &subst);
243        let r = abi::call_modifier(resolve::touch_bar_item_presence_fn(), view.value_ptr(), view.meta, view.wt,
244            &[buf.as_ptr() as u64],
245            [0.0; 4],
246            size,
247        );
248        wrap_owned(r, m, wt)
249    }
250}
251
252/// Whether a key press was consumed.
253///
254/// Two cases, neither with a payload, so the tags are the declaration order and
255/// the whole value is one byte in a register.
256#[derive(Clone, Copy, Debug, PartialEq, Eq)]
257pub enum KeyPressResult {
258    Handled = 0,
259    Ignored = 1,
260}
261
262/// Which key was pressed, and with what held down.
263///
264/// The handler used to be told only that *a* key had been pressed — the
265/// `KeyPress` arrived by address and was ignored — so a key handler could not
266/// branch on the key, which is the only thing a key handler does.
267///
268/// ```text
269///   +0   phase       KeyPress.Phases   (an option set)
270///   +8   key         KeyEquivalent
271///   +24  characters  String
272///   +40  modifiers   EventModifiers    (rawValue: Int)
273/// ```
274#[derive(Clone, Debug, Default, PartialEq, Eq)]
275pub struct KeyPress {
276    /// What was typed — `"a"`, `"\u{1b}"` for escape, and so on. This is
277    /// `KeyPress.characters`, taken from the stored `String`.
278    pub characters: String,
279    /// The held modifiers, as raw `EventModifiers` bits. Test them with
280    /// [`KeyPress::holding`] rather than against numbers of your own.
281    pub modifiers: u64,
282    /// Which phase of the press this is, as raw `KeyPress.Phases` bits — down,
283    /// repeat, up.
284    pub phase: u64,
285}
286
287impl KeyPress {
288    /// Whether `modifier` was held.
289    ///
290    /// The bit is read from SwiftUI's own static rather than written out here:
291    /// `EventModifiers.shift` is a value with a `rawValue`, and asking for it
292    /// is both shorter than a table and correct if the table would have been
293    /// wrong.
294    pub fn holding(&self, modifier: EventModifier) -> bool {
295        let bit = unsafe {
296            let meta = abi::metadata0(resolve::event_modifiers_ma());
297            frozen_static(meta, modifier.getter())
298        };
299        bit != 0 && self.modifiers & bit == bit
300    }
301}
302
303/// The offsets of `KeyPress`'s stored fields.
304fn key_press_offsets() -> Option<&'static [usize; 4]> {
305    static CACHE: std::sync::OnceLock<Option<[usize; 4]>> = std::sync::OnceLock::new();
306    CACHE
307        .get_or_init(|| unsafe {
308            let offs = abi::struct_field_offsets(abi::metadata0(resolve::key_press_ma()))?;
309            // phase, key, characters, modifiers.
310            let &[a, b, c, d] = offs.as_slice() else { return None };
311            Some([a, b, c, d])
312        })
313        .as_ref()
314}
315
316/// Read the press at `p`, which SwiftUI passed by address.
317///
318/// # Safety
319/// `p` must point at a live `KeyPress`.
320pub(crate) unsafe fn read_key_press(p: *const u8) -> KeyPress {
321    let Some(&[phase, _key, characters, modifiers]) = key_press_offsets() else {
322        return KeyPress::default();
323    };
324    unsafe {
325        let mut s = [0u8; 16];
326        s.copy_from_slice(core::slice::from_raw_parts(p.add(characters), 16));
327        KeyPress {
328            // A borrowed read: the string belongs to the `KeyPress`, which
329            // SwiftUI owns for the duration of the call, so this copies out
330            // rather than taking the reference with it.
331            characters: abi::read_swift_string(s),
332            modifiers: *(p.add(modifiers) as *const u64),
333            phase: *(p.add(phase) as *const u64),
334        }
335    }
336}
337
338/// `(KeyPress) -> KeyPress.Result`.
339///
340/// `KeyPress` is forty-eight bytes, so it arrives by address rather than in
341/// registers; the result is one byte and goes back in x0.
342pub(crate) unsafe extern "C" fn invoke_key_press(payload: *mut c_void, arg: u64) -> u64 {
343    unsafe {
344        let f = boxed_ref::<dyn Fn(KeyPress) -> KeyPressResult>(payload);
345        let meta = abi::metadata0(resolve::key_press_result_ma());
346        abi::enum_argument(meta, f(read_key_press(arg as *const u8)) as u32)
347    }
348}
349
350/// Apply `.onKeyPress(characters:phases:action:)`.
351///
352/// Both `CharacterSet` and `KeyPress.Phases` are one word wide and *resilient*,
353/// so each travels by address — their static getters write through x8, which is
354/// the same question asked of the getter rather than the parameter.
355pub fn on_key_press(
356    view: &ViewHandle,
357    characters: CharacterSetKind,
358    phases: KeyPressPhases,
359    action: impl Fn(KeyPress) -> KeyPressResult + 'static,
360) -> ViewHandle {
361    unsafe {
362        let cm = abi::metadata0(resolve::character_set_ma());
363        let pm = abi::metadata0(resolve::key_press_phases_ma());
364        let c = resilient_static(cm, characters.getter());
365        let p = resilient_static(pm, phases.getter());
366        let k = word_closure(invoke_key_press, boxed_ctx(Box::new(action) as Box<dyn Fn(KeyPress) -> KeyPressResult>));
367        opaque_modifier(
368            resolve::on_key_press_fn(),
369            resolve::on_key_press_oret(),
370            view,
371            &[c.as_ptr() as u64, p.as_ptr() as u64, k[0], k[1]],
372            [0.0; 4],
373        )
374    }
375}
376
377
378swift_statics! {
379    /// A named key, as `SwiftUI.KeyEquivalent`'s statics spell it.
380    ///
381    /// These are the keys a shortcut is usually hung on. A `KeyEquivalent` also
382    /// wraps any `Character`, but its initialiser is inlinable and exports
383    /// nothing, so the named ones are what can be reached.
384    pub enum Key {
385        Escape = c"$s7SwiftUI13KeyEquivalentV6escapeACvgZ",
386        Return = c"$s7SwiftUI13KeyEquivalentV6returnACvgZ",
387        Tab = c"$s7SwiftUI13KeyEquivalentV3tabACvgZ",
388        Space = c"$s7SwiftUI13KeyEquivalentV5spaceACvgZ",
389        Delete = c"$s7SwiftUI13KeyEquivalentV6deleteACvgZ",
390        DeleteForward = c"$s7SwiftUI13KeyEquivalentV13deleteForwardACvgZ",
391        Clear = c"$s7SwiftUI13KeyEquivalentV5clearACvgZ",
392        UpArrow = c"$s7SwiftUI13KeyEquivalentV7upArrowACvgZ",
393        DownArrow = c"$s7SwiftUI13KeyEquivalentV9downArrowACvgZ",
394        LeftArrow = c"$s7SwiftUI13KeyEquivalentV9leftArrowACvgZ",
395        RightArrow = c"$s7SwiftUI13KeyEquivalentV10rightArrowACvgZ",
396        Home = c"$s7SwiftUI13KeyEquivalentV4homeACvgZ",
397        End = c"$s7SwiftUI13KeyEquivalentV3endACvgZ",
398        PageUp = c"$s7SwiftUI13KeyEquivalentV6pageUpACvgZ",
399        PageDown = c"$s7SwiftUI13KeyEquivalentV8pageDownACvgZ",
400    }
401}
402
403/// `.onKeyPress(_:action:)` — one named key, with no `KeyPress` to inspect.
404///
405/// [`on_key_press`] watches a whole character set and hands the action the
406/// press; this watches *one* key, which is how a shortcut is usually written —
407/// "Escape closes this". Only the character-set form was bound, so a view that
408/// cared about Escape had to accept every key and filter.
409pub fn on_key(
410    view: &ViewHandle,
411    key: Key,
412    action: impl Fn() -> KeyPressResult + 'static,
413) -> ViewHandle {
414    unsafe {
415        // `KeyEquivalent` travels by *address*. It is sixteen bytes — one
416        // `Character` — which would fit two registers, but it is resilient, so
417        // its size is not something the caller is allowed to assume; reading
418        // the static already goes through `resilient_static` for the same
419        // reason.
420        let meta = abi::metadata0(resolve::key_equivalent_ma());
421        let bytes = resilient_static(meta, key.getter());
422
423        let c = word_closure(
424            invoke_key_result,
425            boxed_ctx(Box::new(action) as Box<dyn Fn() -> KeyPressResult>),
426        );
427        opaque_modifier(
428            resolve::on_key_single_fn(),
429            resolve::on_key_single_oret(),
430            view,
431            &[bytes.as_ptr() as u64, c[0], c[1]],
432            [0.0; 4],
433        )
434    }
435}
436
437/// `() -> KeyPress.Result` — no press to read, just the answer.
438pub(crate) unsafe extern "C" fn invoke_key_result(payload: *mut c_void, _arg: u64) -> u64 {
439    unsafe {
440        let f = boxed_ref::<dyn Fn() -> KeyPressResult>(payload);
441        let meta = abi::metadata0(resolve::key_press_result_ma());
442        abi::enum_argument(meta, f() as u32)
443    }
444}