Skip to main content

swiftui_native/view/
interaction.rs

1//! Gestures, drag and drop, commands, scrolling and focus.
2//!
3//! Part of [`crate::view::View`]'s chainable API; Rust allows a type's
4//! inherent methods to be split across modules, and 400 of them in one file
5//! was a file nobody could find anything in.
6
7use super::{View};
8use crate::views;
9
10impl View {
11    // ── Drag and drop ──
12    //
13    // Distinct from the drag *gesture*, which is a finger moving on screen:
14    // this is handing an item to another view, another window, or another app.
15
16    /// `.dragConfiguration(within:outside:)` — which operations a drag permits
17    /// inside the app and outside it, which need not be the same.
18    pub fn drag_configuration(
19        self,
20        within_app: views::DragOperations,
21        outside_app: views::DragOperations,
22    ) -> Self {
23        Self::new(views::drag_configuration(&self.handle, within_app, outside_app))
24    }
25
26    /// `.dragContainer(for:)` — the items a multi-item drag carries.
27    pub fn drag_container(self, items: &[&str]) -> Self {
28        Self::new(views::drag_container(&self.handle, items))
29    }
30
31    /// `.dragContainerSelection(_:)` — which of them are selected, so dragging
32    /// one drags the selection rather than just the item under the finger.
33    pub fn drag_container_selection(self, selected: &[&str]) -> Self {
34        Self::new(views::drag_container_selection(&self.handle, selected))
35    }
36
37    /// `.draggableContainerItem(id:)` — mark this view as one of the container's
38    /// items.
39    pub fn draggable_container_item(self, id: views::HashableValue<'_>) -> Self {
40        Self::new(views::draggable_container_item(&self.handle, id))
41    }
42
43    /// `.dragPreviewsFormation(_:)` — how several dragged previews stack up.
44    pub fn drag_previews_formation(self, value: views::DragDropPreviewsFormation) -> Self {
45        Self::new(views::drag_previews_formation(&self.handle, value))
46    }
47
48    /// `.dropPreviewsFormation(_:)`
49    pub fn drop_previews_formation(self, value: views::DragDropPreviewsFormation) -> Self {
50        Self::new(views::drop_previews_formation(&self.handle, value))
51    }
52
53    /// `.dropConfiguration(_:)` — decide what a drop over this view would do,
54    /// which is what sets the cursor before anything is released.
55    pub fn drop_configuration(self, decide: impl Fn() -> views::DropOperation + 'static) -> Self {
56        Self::new(views::drop_configuration(&self.handle, decide))
57    }
58
59    /// `.dropDestination(for:action:isTargeted:)` — `accept` returns whether the
60    /// drop was taken; `targeted` fires as the drag enters and leaves.
61    pub fn drop_destination(
62        self,
63        accept: impl Fn(&[String]) -> bool + 'static,
64        targeted: impl Fn(bool) + 'static,
65    ) -> Self {
66        Self::new(views::drop_destination(&self.handle, accept, targeted))
67    }
68
69    /// `.onDragSessionUpdated(_:)`
70    pub fn on_drag_session_updated(self, action: impl Fn() + 'static) -> Self {
71        Self::new(views::on_drag_session_updated(&self.handle, action))
72    }
73
74    /// `.onDropSessionUpdated(_:)`
75    pub fn on_drop_session_updated(self, action: impl Fn() + 'static) -> Self {
76        Self::new(views::on_drop_session_updated(&self.handle, action))
77    }
78
79    // ── Editing commands ──
80    //
81    // What the standard Copy, Cut, Paste and Delete menu items do when this
82    // view has focus — the keyboard and menu half of an app, which a
83    // touch-first API leaves out.
84
85    /// `.onCopyCommand(perform:)` — what Copy takes from this view.
86    pub fn on_copy_command(self, items: &[&str]) -> Self {
87        Self::new(views::on_copy_command(&self.handle, items))
88    }
89
90    /// `.onCutCommand(perform:)`
91    pub fn on_cut_command(self, items: &[&str]) -> Self {
92        Self::new(views::on_cut_command(&self.handle, items))
93    }
94
95    /// `.onPasteCommand(of:perform:)`
96    pub fn on_paste_command(
97        self,
98        kinds: &[views::ContentType],
99        paste: impl Fn(&[views::ItemProvider]) + 'static,
100    ) -> Self {
101        Self::new(views::on_paste_command(&self.handle, kinds, paste))
102    }
103
104    /// `.onDeleteCommand(perform:)`
105    pub fn on_delete_command(self, action: impl Fn() + 'static) -> Self {
106        Self::new(views::on_delete_command(&self.handle, action))
107    }
108
109    /// `.onMoveCommand(perform:)` — arrow keys, with the direction as an `Int`.
110    pub fn on_move_command(self, action: impl Fn(i64) + 'static) -> Self {
111        Self::new(views::on_move_command(&self.handle, action))
112    }
113
114    /// `.onExitCommand(perform:)` — escape.
115    pub fn on_exit_command(self, action: impl Fn() + 'static) -> Self {
116        Self::new(views::on_exit_command(&self.handle, action))
117    }
118
119    /// `.onCommand(_:perform:)` — any other selector by name, which is the way
120    /// out when a command has no modifier of its own.
121    pub fn on_command(self, selector: &'static core::ffi::CStr, action: impl Fn() + 'static) -> Self {
122        Self::new(views::on_command(&self.handle, selector, action))
123    }
124
125    /// `.cuttable(for:action:)` — what Cut takes, as distinct from
126    /// [`View::copyable`], which Copy takes.
127    pub fn cuttable(self, items: &[&str]) -> Self {
128        Self::new(views::cuttable(&self.handle, items))
129    }
130
131    /// `.pasteDestination(for:action:)`
132    pub fn paste_destination(self, paste: impl Fn(&[String]) + 'static) -> Self {
133        Self::new(views::paste_destination(&self.handle, paste))
134    }
135
136    /// `.renameAction(_:)` — what the Rename command does, which a list row
137    /// needs for in-place renaming to work at all.
138    pub fn rename_action(self, action: impl Fn() + 'static) -> Self {
139        Self::new(views::rename_action(&self.handle, action))
140    }
141
142    // ── Pointer and keyboard ──
143
144    /// `.pointerStyle(_:)` — `None` restores the default cursor.
145    pub fn pointer_style(self, style: Option<views::PointerStyle>) -> Self {
146        Self::new(views::pointer_style(&self.handle, style))
147    }
148
149    /// `.pointerVisibility(_:)`
150    pub fn pointer_visibility(self, value: views::Visibility) -> Self {
151        Self::new(views::pointer_visibility(&self.handle, value))
152    }
153
154    /// `.onModifierKeysChanged(mask:initial:_:)`
155    pub fn on_modifier_keys_changed(
156        self,
157        mask: views::EventModifier,
158        initial: bool,
159        action: impl Fn(u64, u64) + 'static,
160    ) -> Self {
161        Self::new(views::on_modifier_keys_changed(&self.handle, mask, initial, action))
162    }
163
164    /// `.modifierKeyAlternate(_:_:)` — a different view while a key is held.
165    pub fn modifier_key_alternate(self, modifiers: views::EventModifier, content: View) -> Self {
166        Self::new(views::modifier_key_alternate(&self.handle, modifiers, content.handle()))
167    }
168
169    /// `.onInteractiveResizeChange(_:)`
170    pub fn on_interactive_resize_change(self, action: impl Fn(bool) + 'static) -> Self {
171        Self::new(views::on_interactive_resize_change(&self.handle, action))
172    }
173
174    /// `.userActivity(_:isActive:_:)` — advertise what this view is doing, for
175    /// Handoff and Spotlight.
176    pub fn user_activity(self, kind: &str, active: bool, action: impl Fn() + 'static) -> Self {
177        Self::new(views::user_activity(&self.handle, kind, active, action))
178    }
179
180    /// `.onContinueUserActivity(_:perform:)` — the other end of Handoff.
181    pub fn on_continue_user_activity(self, kind: &str, action: impl Fn() + 'static) -> Self {
182        Self::new(views::on_continue_user_activity(&self.handle, kind, action))
183    }
184
185    /// `.handlesExternalEvents(preferring:allowing:)`
186    pub fn handles_external_events(self, preferring: &[&str], allowing: &[&str]) -> Self {
187        Self::new(views::handles_external_events(&self.handle, preferring, allowing))
188    }
189
190    /// `.allowedDynamicRange(.high)`
191    pub fn allowed_dynamic_range_high(self) -> Self {
192        Self::new(views::allowed_dynamic_range_high(&self.handle))
193    }
194
195    // ── Gestures ──
196
197    /// `.tabItem { … }` — the label a `TabView` shows for this page.
198    pub fn tab_item(self, label: View) -> Self {
199        Self::new(views::tab_item(&self.handle, label.handle()))
200    }
201
202    /// `.highPriorityGesture(_:)` — take the gesture ahead of the children,
203    /// where `.gesture` would lose to them.
204    pub fn high_priority_gesture(self, minimum_distance: f64, action: impl Fn() + 'static) -> Self {
205        Self::new(views::high_priority_gesture(&self.handle, minimum_distance, action))
206    }
207
208    /// `.simultaneousGesture(_:)` — recognise alongside the children rather than
209    /// instead of them.
210    pub fn simultaneous_gesture(self, minimum_distance: f64, action: impl Fn() + 'static) -> Self {
211        Self::new(views::simultaneous_gesture(&self.handle, minimum_distance, action))
212    }
213
214    /// `.onOpenURL(perform:)` — deep links.
215    pub fn on_open_url(self, action: impl Fn(&str) + 'static) -> Self {
216        Self::new(views::on_open_url(&self.handle, action))
217    }
218
219    /// `.copyable(_:)` — what a copy command takes from this view.
220    pub fn copyable(self, items: &[&str]) -> Self {
221        Self::new(views::copyable(&self.handle, items))
222    }
223
224    // ── Scrolling ──
225
226    /// `.scrollPosition(id:anchor:)` bound to Rust state — the id of the item
227    /// at the anchor lands in `state`, and writing it scrolls there.
228    pub fn scroll_position_id(
229        self,
230        state: &views::BoundState<String>,
231        anchor: Option<(f64, f64)>,
232    ) -> Self {
233        Self::new(views::scroll_position_id(&self.handle, state, anchor))
234    }
235
236    /// `.defaultScrollAnchor(_:for:)` — `None` restores the default.
237    pub fn default_scroll_anchor(
238        self,
239        anchor: Option<(f64, f64)>,
240        role: views::ScrollAnchorRole,
241    ) -> Self {
242        Self::new(views::default_scroll_anchor(&self.handle, anchor, role))
243    }
244
245    /// `.scrollTargetLayout()` — mark the stack whose children are the targets
246    /// a paging scroll snaps to.
247    pub fn scroll_target_layout(self, enabled: bool) -> Self {
248        Self::new(views::scroll_target_layout(&self.handle, enabled))
249    }
250
251    /// `.scrollBounceBehavior(_:axes:)`
252    pub fn scroll_bounce_behavior(
253        self,
254        behavior: views::ScrollBounceBehavior,
255        axes: views::AxisSet,
256    ) -> Self {
257        Self::new(views::scroll_bounce_behavior(&self.handle, behavior, axes))
258    }
259
260    /// `.scrollIndicatorsFlash(onAppear:)`
261    pub fn scroll_indicators_flash(self, on_appear: bool) -> Self {
262        Self::new(views::scroll_indicators_flash(&self.handle, on_appear))
263    }
264
265    /// `.scrollEdgeEffectHidden(_:for:)`
266    pub fn scroll_edge_effect_hidden(self, hidden: bool, edges: views::EdgeSet) -> Self {
267        Self::new(views::scroll_edge_effect_hidden(&self.handle, hidden, edges))
268    }
269
270    /// `.scrollEdgeEffectStyle(_:for:)` — `None` restores the default.
271    pub fn scroll_edge_effect_style(
272        self,
273        style: Option<views::ScrollEdgeEffectStyle>,
274        edges: &[views::EdgeSetMember],
275    ) -> Self {
276        Self::new(views::scroll_edge_effect_style(&self.handle, style, edges))
277    }
278
279    /// `.scrollTransition(_:axis:)` — how a child changes as it crosses the
280    /// viewport.
281    pub fn scroll_transition(
282        self,
283        config: views::ScrollTransitionConfiguration,
284        axis: Option<views::Axis>,
285    ) -> Self {
286        Self::new(views::scroll_transition(&self.handle, config, axis))
287    }
288
289    /// `.scrollTransition(topLeading:bottomTrailing:axis:)` — the two ends of
290    /// the scroll configured separately.
291    ///
292    /// The single-configuration [`scroll_transition`](Self::scroll_transition)
293    /// applies the same one at both ends; this is what lets a view animate on
294    /// the way in and do nothing on the way out.
295    pub fn scroll_transition_ends(
296        self,
297        top_leading: views::ScrollTransitionConfiguration,
298        bottom_trailing: views::ScrollTransitionConfiguration,
299        axis: Option<views::Axis>,
300    ) -> Self {
301        Self::new(views::scroll_transition_ends(
302            &self.handle,
303            top_leading,
304            bottom_trailing,
305            axis,
306        ))
307    }
308
309    /// `.onScrollPhaseChange(_:)` — the handler sees the old and new phase.
310    pub fn on_scroll_phase_change(self, action: impl Fn(i64, i64) + 'static) -> Self {
311        Self::new(views::on_scroll_phase_change(&self.handle, action))
312    }
313
314    /// `.onScrollVisibilityChange(threshold:_:)`
315    pub fn on_scroll_visibility_change(self, threshold: f64, action: impl Fn(bool) + 'static) -> Self {
316        Self::new(views::on_scroll_visibility_change(&self.handle, threshold, action))
317    }
318
319    // ── Focus ──
320
321    /// `.defaultFocus(_:_:)` — which control starts focused.
322    pub fn default_focus(self, value: bool, priority: views::DefaultFocusPriority) -> Self {
323        Self::new(views::default_focus(&self.handle, value, priority))
324    }
325
326    /// `.prefersDefaultFocus(_:in:)`
327    pub fn prefers_default_focus(self, prefers: bool) -> Self {
328        Self::new(views::prefers_default_focus(&self.handle, prefers))
329    }
330
331    /// `.focusScope(_:)` — a boundary focus moves within.
332    pub fn focus_scope(self) -> Self {
333        Self::new(views::focus_scope(&self.handle))
334    }
335
336    /// `.accessibilityFocused(_:)`
337    pub fn a11y_focused(self) -> Self {
338        Self::new(views::accessibility_focused(&self.handle))
339    }
340
341    /// `.accessibilityDefaultFocus(_:_:)`
342    pub fn a11y_default_focus(self, value: bool) -> Self {
343        Self::new(views::accessibility_default_focus(&self.handle, value))
344    }
345
346    /// `.focused(_:)`
347    pub fn focused(self) -> Self {
348        Self::new(views::focused(&self.handle))
349    }
350
351    /// `.matchedGeometryEffect(id:in:properties:anchor:isSource:)` — the pair of
352    /// views that share an id are interpolated between rather than crossfaded.
353    pub fn matched_geometry_effect(
354        self,
355        id: views::HashableValue<'_>,
356        properties: &[views::GeometryProperties],
357        anchor: (f64, f64),
358        is_source: bool,
359    ) -> Self {
360        Self::new(views::matched_geometry_effect(
361            &self.handle,
362            id,
363            properties,
364            anchor,
365            is_source,
366        ))
367    }
368
369    /// `.matchedGeometryEffect(id:in:properties:anchor:isSource:)` in a
370    /// *shared* namespace.
371    ///
372    /// The pairing spelling. [`matched_geometry_effect`](Self::matched_geometry_effect)
373    /// mints a namespace per call, so the views it makes cannot pair with
374    /// anything.
375    pub fn matched_geometry_effect_in(
376        self,
377        id: views::HashableValue<'_>,
378        namespace: views::Namespace,
379        properties: &[views::GeometryProperties],
380        anchor: (f64, f64),
381        is_source: bool,
382    ) -> Self {
383        Self::new(views::matched_geometry_effect_in(
384            &self.handle,
385            id,
386            namespace,
387            properties,
388            anchor,
389            is_source,
390        ))
391    }
392
393    /// `.onDrag { NSItemProvider(object: item) }` with a custom preview.
394    ///
395    /// Named apart from [`View::on_drag`], which is the drag *gesture* — a
396    /// finger moving on screen — where this is drag and drop, which hands an
397    /// item to another view or another app.
398    pub fn on_drag_item(self, item: &str, preview: View) -> Self {
399        Self::new(views::on_drag(&self.handle, item, preview.handle()))
400    }
401
402    /// `.onDrop(of:isTargeted:perform:)` — the handler returns whether it took
403    /// the drop.
404    pub fn on_drop(
405        self,
406        kinds: &[views::ContentType],
407        accept: impl Fn(&[views::ItemProvider]) -> bool + 'static,
408    ) -> Self {
409        Self::new(views::on_drop(&self.handle, kinds, accept))
410    }
411
412    /// `.onKeyPress(_:phases:action:)`
413    ///
414    /// The action is handed the [`views::KeyPress`] — what was typed, which
415    /// modifiers were held, and which phase of the press it is — and answers
416    /// whether it handled it. It used to receive nothing at all, which left a
417    /// key handler unable to branch on the key.
418    ///
419    /// ```no_run
420    /// # use swiftui_native::prelude::*;
421    /// # use swiftui_native::views::{CharacterSetKind, EventModifier, KeyPressPhases, KeyPressResult};
422    /// let v = text("editor").on_key_press(
423    ///     CharacterSetKind::Alphanumerics,
424    ///     KeyPressPhases::Down,
425    ///     |press| {
426    ///         if press.characters == "s" && press.holding(EventModifier::Command) {
427    ///             KeyPressResult::Handled
428    ///         } else {
429    ///             KeyPressResult::Ignored
430    ///         }
431    ///     },
432    /// );
433    /// ```
434    pub fn on_key_press(
435        self,
436        characters: views::CharacterSetKind,
437        phases: views::KeyPressPhases,
438        action: impl Fn(views::KeyPress) -> views::KeyPressResult + 'static,
439    ) -> Self {
440        Self::new(views::on_key_press(&self.handle, characters, phases, action))
441    }
442
443    /// `.onKeyPress(_:action:)` — one named key.
444    ///
445    /// [`on_key_press`](Self::on_key_press) watches a whole character set and
446    /// hands the action the press; this watches *one* key, which is how a
447    /// shortcut is usually written — "Escape closes this". Only the
448    /// character-set form was bound, so a view that cared about Escape had to
449    /// accept every key and filter.
450    ///
451    /// ```no_run
452    /// # use swiftui_native::prelude::*;
453    /// # use swiftui_native::views::{Key, KeyPressResult};
454    /// let sheet = text("editor").on_key(Key::Escape, || KeyPressResult::Handled);
455    /// ```
456    pub fn on_key(
457        self,
458        key: views::Key,
459        action: impl Fn() -> views::KeyPressResult + 'static,
460    ) -> Self {
461        Self::new(views::on_key(&self.handle, key, action))
462    }
463
464    /// `.sensoryFeedback(_:trigger:condition:)` — haptics, fired when the
465    /// condition says the trigger's change deserves one.
466    pub fn sensory_feedback(
467        self,
468        feedback: views::SensoryFeedback,
469        trigger: i64,
470        condition: impl Fn(i64, i64) -> bool + 'static,
471    ) -> Self {
472        Self::new(views::sensory_feedback(&self.handle, feedback, trigger, condition))
473    }
474
475    // ── visionOS: hover effects ──
476
477    /// Apply `.hoverEffect(.lift)` — visionOS native gaze-driven lift.
478    ///
479    /// Constructs the `LiftHoverEffect` value, resolves witness tables, and
480    /// calls the modifier entirely at runtime via arm64 asm. No Swift bridge.
481    /// No-op on platforms where the symbol is absent.
482    #[cfg(not(target_os = "macos"))]
483    pub fn hover_effect_lift(self) -> Self {
484        Self::new(views::hover_effect_lift(&self.handle))
485    }
486
487    /// Apply `.hoverEffect(.highlight)` — visionOS native gaze-driven highlight.
488    ///
489    /// Same runtime mechanism as `hover_effect_lift`.
490    #[cfg(not(target_os = "macos"))]
491    pub fn hover_effect_highlight(self) -> Self {
492        Self::new(views::hover_effect_highlight(&self.handle))
493    }
494
495    /// Apply `.onHover(perform:)` — fires callback on gaze enter/exit (visionOS)
496    /// or mouse enter/exit (macOS).
497    pub fn on_hover(self, callback: impl Fn(bool) + 'static) -> Self {
498        Self::new(views::on_hover(&self.handle, callback))
499    }
500}