swiftui-native 0.0.1

SwiftUI from pure Rust — no Swift bridge, no shim, only swift-runtime calls
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
//! Keys, pointers, the Touch Bar, and haptics.
//!
//! Input that arrives as a value rather than as a gesture. The recurring
//! difficulty is enum tags: `CoordinateSpace` and `HoverPhase` both carry
//! payload cases that are not in the public API, which moves every tag after
//! them.

use super::*;
use super::closures::*;
use crate::abi;
use crate::resolve;
use core::ffi::c_void;

/// Apply `.onModifierKeysChanged(mask:initial:_:)`.
///
/// The callback receives the old and new masks as raw `EventModifiers` bits.
pub fn on_modifier_keys_changed(
    view: &ViewHandle,
    mask: EventModifier,
    initial: bool,
    action: impl Fn(u64, u64) + 'static,
) -> ViewHandle {
    unsafe {
        let meta = abi::metadata0(resolve::event_modifiers_ma());
        // `EventModifiers` is frozen and one word, so it goes in a register.
        let m = frozen_static(meta, mask.getter());
        let c = pair_words(invoke_word_pair, boxed_ctx(Box::new(action) as Box<dyn Fn(u64, u64)>));
        opaque_modifier(
            resolve::on_modifier_keys_fn(),
            resolve::on_modifier_keys_oret(),
            view,
            &[m, u64::from(initial), c[0], c[1]],
            [0.0; 4],
        )
    }
}

/// Apply `.onPencilDoubleTap(perform:)`.
pub fn on_pencil_double_tap(view: &ViewHandle, action: impl Fn() + 'static) -> ViewHandle {
    unsafe {
        opaque_modifier(
            resolve::on_pencil_double_tap_fn(),
            resolve::on_pencil_double_tap_oret(),
            view,
            &ignoring_value_words(action),
            [0.0; 4],
        )
    }
}

/// Apply `.onPencilSqueeze(perform:)`.
pub fn on_pencil_squeeze(view: &ViewHandle, action: impl Fn() + 'static) -> ViewHandle {
    unsafe {
        opaque_modifier(
            resolve::on_pencil_squeeze_fn(),
            resolve::on_pencil_squeeze_oret(),
            view,
            &ignoring_value_words(action),
            [0.0; 4],
        )
    }
}

/// Apply `.touchBar(content:)`.
pub fn touch_bar(view: &ViewHandle, content: &ViewHandle) -> ViewHandle {
    unsafe {
        content_modifier(resolve::touch_bar_fn(), resolve::touch_bar_oret(), view, &[], content)
    }
}

/// Apply `.modifierKeyAlternate(_:_:)`.
pub fn modifier_key_alternate(
    view: &ViewHandle,
    modifiers: EventModifier,
    content: &ViewHandle,
) -> ViewHandle {
    unsafe {
        let meta = abi::metadata0(resolve::event_modifiers_ma());
        let m = frozen_static(meta, modifiers.getter());
        content_modifier(
            resolve::modifier_key_alternate_fn(),
            resolve::modifier_key_alternate_oret(),
            view,
            &[m],
            content,
        )
    }
}

/// Apply `.onCommand(_:perform:)`.
///
/// The selector is an Objective-C one, registered by name.
pub fn on_command(view: &ViewHandle, selector: &core::ffi::CStr, action: impl Fn() + 'static) -> ViewHandle {
    unsafe {
        let sel = crate::objc::sel(selector);
        let c = action_words(action);
        opaque_modifier(
            resolve::on_command_fn(),
            resolve::on_command_oret(),
            view,
            &[sel as u64, c[0], c[1]],
            [0.0; 4],
        )
    }
}

pub(crate) unsafe extern "C" fn ask_condition(payload: *mut c_void, arg: u64) -> u64 {
    unsafe {
        let f = boxed_ref::<dyn Fn(i64, i64) -> bool>(payload);
        // `(A, A) -> Bool` with `A` concrete and one word: old in x0, new in x1.
        // The wide thunk is not needed, but only x0 reaches here, so the new
        // value is what the condition sees.
        u64::from(f(arg as i64, arg as i64))
    }
}

/// Apply `.sensoryFeedback(_:trigger:condition:)`.
///
/// `condition` decides, from the trigger's old and new values, whether the
/// feedback plays.
pub fn sensory_feedback(
    view: &ViewHandle,
    feedback: SensoryFeedback,
    trigger: i64,
    condition: impl Fn(i64, i64) -> bool + 'static,
) -> ViewHandle {
    unsafe {
        let meta = abi::metadata0(resolve::sensory_feedback_ma());
        let f = resilient_static(meta, feedback.getter());
        let mut t = trigger;
        let c = word_closure(ask_condition, boxed_ctx(Box::new(condition) as Box<dyn Fn(i64, i64) -> bool>));
        generic1_modifier_ints(
            resolve::sensory_feedback_fn(),
            resolve::sensory_feedback_oret(),
            view,
            resolve::int_meta(),
            resolve::int_equatable_wt(),
            &[f.as_ptr() as u64, &mut t as *mut i64 as u64, c[0], c[1]],
            [0.0; 4],
        )
    }
}

/// Where a hover's location is measured.
///
/// `.named` carries an `AnyHashable`, which makes the enum address-only — so
/// unlike the small SwiftUI enums this one is built in a buffer and passed by
/// pointer. Payload cases are numbered first, so `.named` is 0 and the two
/// cases spelled here follow *both* of them: `.named(AnyHashable)` is 0 and the
/// undocumented `.id(CoordinateSpace.ID)` is 1, so `.global` is 2 and `.local`
/// is 3. Guessing from the public cases alone lands on `.id`, which renders
/// without complaint against a coordinate space that was never named.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CoordinateSpace {
    Global,
    Local,
}

impl CoordinateSpace {
    fn tag(self) -> u32 {
        match self {
            CoordinateSpace::Global => 2,
            CoordinateSpace::Local => 3,
        }
    }
}

/// Which phase of a continuous hover SwiftUI is reporting.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HoverPhase {
    /// The pointer is inside the view; the location is not surfaced here.
    Active,
    /// The pointer has left.
    Ended,
}

/// `(HoverPhase) -> ()`.
///
/// `.active` carries a `CGPoint`, so the enum is address-only and x0 is the
/// address of the value; the tag comes from the enum's own witness rather than
/// from reading a byte, because a payload case stores its discriminator
/// wherever the payload leaves room.
unsafe extern "C" fn saw_hover(value: *const u8, payload: *mut c_void) {
    unsafe {
        let f = boxed_ref::<dyn Fn(HoverPhase)>(payload);
        let meta = abi::metadata0(resolve::hover_phase_ma());
        let phase = match abi::read_enum_tag(value, meta) {
            0 => HoverPhase::Active,
            _ => HoverPhase::Ended,
        };
        f(phase);
    }
}

/// Apply `.onContinuousHover(coordinateSpace:perform:)`.
pub fn on_continuous_hover(
    view: &ViewHandle,
    space: CoordinateSpace,
    perform: impl Fn(HoverPhase) + 'static,
) -> ViewHandle {
    unsafe {
        let meta = abi::metadata0(resolve::coordinate_space_ma());
        let mut buf = vec![0u8; abi::type_bytes(meta)];
        abi::inject_enum_tag(&mut buf, meta, space.tag());
        let ctx = Box::leak(Box::new(abi::ValueClosureCtx::new(
            saw_hover,
            boxed_ctx(Box::new(perform) as Box<dyn Fn(HoverPhase)>),
        ))) as *mut abi::ValueClosureCtx;
        let subst = [view.meta, view.wt];
        let (m, wt, size) = opaque_result_type(resolve::on_continuous_hover_oret(), &subst);
        let r = abi::call_modifier(resolve::on_continuous_hover_fn(), view.value_ptr(), view.meta, view.wt,
            &[
                buf.as_ptr() as u64,
                abi::value_closure_thunk as *const () as u64,
                ctx as u64,
            ],
            [0.0; 4],
            size,
        );
        wrap_owned(r, m, wt)
    }
}

/// Apply `.touchBarItemPresence(_:)`.
///
/// The enum holds a `String`, which makes it larger than four words and so
/// address-only: the argument is a pointer to a buffer this builds, with the
/// string's two words written first and the tag injected by the enum's own
/// witness afterwards.
#[cfg(target_os = "macos")]
pub fn touch_bar_item_presence(
    view: &ViewHandle,
    presence: TouchBarItemPresence,
    identifier: &str,
) -> ViewHandle {
    unsafe {
        let meta = abi::metadata0(resolve::touch_bar_item_presence_ma());
        let mut buf = vec![0u8; abi::type_bytes(meta)];
        buf[..16].copy_from_slice(&abi::swift_string(identifier));
        abi::inject_enum_tag(&mut buf, meta, presence as u32);
        let subst = [view.meta, view.wt];
        let (m, wt, size) = opaque_result_type(resolve::touch_bar_item_presence_oret(), &subst);
        let r = abi::call_modifier(resolve::touch_bar_item_presence_fn(), view.value_ptr(), view.meta, view.wt,
            &[buf.as_ptr() as u64],
            [0.0; 4],
            size,
        );
        wrap_owned(r, m, wt)
    }
}

/// Whether a key press was consumed.
///
/// Two cases, neither with a payload, so the tags are the declaration order and
/// the whole value is one byte in a register.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum KeyPressResult {
    Handled = 0,
    Ignored = 1,
}

/// Which key was pressed, and with what held down.
///
/// The handler used to be told only that *a* key had been pressed — the
/// `KeyPress` arrived by address and was ignored — so a key handler could not
/// branch on the key, which is the only thing a key handler does.
///
/// ```text
///   +0   phase       KeyPress.Phases   (an option set)
///   +8   key         KeyEquivalent
///   +24  characters  String
///   +40  modifiers   EventModifiers    (rawValue: Int)
/// ```
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct KeyPress {
    /// What was typed — `"a"`, `"\u{1b}"` for escape, and so on. This is
    /// `KeyPress.characters`, taken from the stored `String`.
    pub characters: String,
    /// The held modifiers, as raw `EventModifiers` bits. Test them with
    /// [`KeyPress::holding`] rather than against numbers of your own.
    pub modifiers: u64,
    /// Which phase of the press this is, as raw `KeyPress.Phases` bits — down,
    /// repeat, up.
    pub phase: u64,
}

impl KeyPress {
    /// Whether `modifier` was held.
    ///
    /// The bit is read from SwiftUI's own static rather than written out here:
    /// `EventModifiers.shift` is a value with a `rawValue`, and asking for it
    /// is both shorter than a table and correct if the table would have been
    /// wrong.
    pub fn holding(&self, modifier: EventModifier) -> bool {
        let bit = unsafe {
            let meta = abi::metadata0(resolve::event_modifiers_ma());
            frozen_static(meta, modifier.getter())
        };
        bit != 0 && self.modifiers & bit == bit
    }
}

/// The offsets of `KeyPress`'s stored fields.
fn key_press_offsets() -> Option<&'static [usize; 4]> {
    static CACHE: std::sync::OnceLock<Option<[usize; 4]>> = std::sync::OnceLock::new();
    CACHE
        .get_or_init(|| unsafe {
            let offs = abi::struct_field_offsets(abi::metadata0(resolve::key_press_ma()))?;
            // phase, key, characters, modifiers.
            let &[a, b, c, d] = offs.as_slice() else { return None };
            Some([a, b, c, d])
        })
        .as_ref()
}

/// Read the press at `p`, which SwiftUI passed by address.
///
/// # Safety
/// `p` must point at a live `KeyPress`.
pub(crate) unsafe fn read_key_press(p: *const u8) -> KeyPress {
    let Some(&[phase, _key, characters, modifiers]) = key_press_offsets() else {
        return KeyPress::default();
    };
    unsafe {
        let mut s = [0u8; 16];
        s.copy_from_slice(core::slice::from_raw_parts(p.add(characters), 16));
        KeyPress {
            // A borrowed read: the string belongs to the `KeyPress`, which
            // SwiftUI owns for the duration of the call, so this copies out
            // rather than taking the reference with it.
            characters: abi::read_swift_string(s),
            modifiers: *(p.add(modifiers) as *const u64),
            phase: *(p.add(phase) as *const u64),
        }
    }
}

/// `(KeyPress) -> KeyPress.Result`.
///
/// `KeyPress` is forty-eight bytes, so it arrives by address rather than in
/// registers; the result is one byte and goes back in x0.
pub(crate) unsafe extern "C" fn invoke_key_press(payload: *mut c_void, arg: u64) -> u64 {
    unsafe {
        let f = boxed_ref::<dyn Fn(KeyPress) -> KeyPressResult>(payload);
        let meta = abi::metadata0(resolve::key_press_result_ma());
        abi::enum_argument(meta, f(read_key_press(arg as *const u8)) as u32)
    }
}

/// Apply `.onKeyPress(characters:phases:action:)`.
///
/// Both `CharacterSet` and `KeyPress.Phases` are one word wide and *resilient*,
/// so each travels by address — their static getters write through x8, which is
/// the same question asked of the getter rather than the parameter.
pub fn on_key_press(
    view: &ViewHandle,
    characters: CharacterSetKind,
    phases: KeyPressPhases,
    action: impl Fn(KeyPress) -> KeyPressResult + 'static,
) -> ViewHandle {
    unsafe {
        let cm = abi::metadata0(resolve::character_set_ma());
        let pm = abi::metadata0(resolve::key_press_phases_ma());
        let c = resilient_static(cm, characters.getter());
        let p = resilient_static(pm, phases.getter());
        let k = word_closure(invoke_key_press, boxed_ctx(Box::new(action) as Box<dyn Fn(KeyPress) -> KeyPressResult>));
        opaque_modifier(
            resolve::on_key_press_fn(),
            resolve::on_key_press_oret(),
            view,
            &[c.as_ptr() as u64, p.as_ptr() as u64, k[0], k[1]],
            [0.0; 4],
        )
    }
}


swift_statics! {
    /// A named key, as `SwiftUI.KeyEquivalent`'s statics spell it.
    ///
    /// These are the keys a shortcut is usually hung on. A `KeyEquivalent` also
    /// wraps any `Character`, but its initialiser is inlinable and exports
    /// nothing, so the named ones are what can be reached.
    pub enum Key {
        Escape = c"$s7SwiftUI13KeyEquivalentV6escapeACvgZ",
        Return = c"$s7SwiftUI13KeyEquivalentV6returnACvgZ",
        Tab = c"$s7SwiftUI13KeyEquivalentV3tabACvgZ",
        Space = c"$s7SwiftUI13KeyEquivalentV5spaceACvgZ",
        Delete = c"$s7SwiftUI13KeyEquivalentV6deleteACvgZ",
        DeleteForward = c"$s7SwiftUI13KeyEquivalentV13deleteForwardACvgZ",
        Clear = c"$s7SwiftUI13KeyEquivalentV5clearACvgZ",
        UpArrow = c"$s7SwiftUI13KeyEquivalentV7upArrowACvgZ",
        DownArrow = c"$s7SwiftUI13KeyEquivalentV9downArrowACvgZ",
        LeftArrow = c"$s7SwiftUI13KeyEquivalentV9leftArrowACvgZ",
        RightArrow = c"$s7SwiftUI13KeyEquivalentV10rightArrowACvgZ",
        Home = c"$s7SwiftUI13KeyEquivalentV4homeACvgZ",
        End = c"$s7SwiftUI13KeyEquivalentV3endACvgZ",
        PageUp = c"$s7SwiftUI13KeyEquivalentV6pageUpACvgZ",
        PageDown = c"$s7SwiftUI13KeyEquivalentV8pageDownACvgZ",
    }
}

/// `.onKeyPress(_:action:)` — one named key, with no `KeyPress` to inspect.
///
/// [`on_key_press`] watches a whole character set and hands the action the
/// press; this watches *one* key, which is how a shortcut is usually written —
/// "Escape closes this". Only the character-set form was bound, so a view that
/// cared about Escape had to accept every key and filter.
pub fn on_key(
    view: &ViewHandle,
    key: Key,
    action: impl Fn() -> KeyPressResult + 'static,
) -> ViewHandle {
    unsafe {
        // `KeyEquivalent` travels by *address*. It is sixteen bytes — one
        // `Character` — which would fit two registers, but it is resilient, so
        // its size is not something the caller is allowed to assume; reading
        // the static already goes through `resilient_static` for the same
        // reason.
        let meta = abi::metadata0(resolve::key_equivalent_ma());
        let bytes = resilient_static(meta, key.getter());

        let c = word_closure(
            invoke_key_result,
            boxed_ctx(Box::new(action) as Box<dyn Fn() -> KeyPressResult>),
        );
        opaque_modifier(
            resolve::on_key_single_fn(),
            resolve::on_key_single_oret(),
            view,
            &[bytes.as_ptr() as u64, c[0], c[1]],
            [0.0; 4],
        )
    }
}

/// `() -> KeyPress.Result` — no press to read, just the answer.
pub(crate) unsafe extern "C" fn invoke_key_result(payload: *mut c_void, _arg: u64) -> u64 {
    unsafe {
        let f = boxed_ref::<dyn Fn() -> KeyPressResult>(payload);
        let meta = abi::metadata0(resolve::key_press_result_ma());
        abi::enum_argument(meta, f() as u32)
    }
}