Skip to main content

cranpose_app_shell/
shell_input.rs

1use super::*;
2
3impl<R> AppShell<R>
4where
5    R: Renderer,
6    R::Error: Debug,
7{
8    fn pointer_event(
9        &self,
10        kind: PointerEventKind,
11        position: Point,
12        global_position: Point,
13        event_time: PointerEventTime,
14    ) -> PointerEvent {
15        let mut event = PointerEvent::new(kind, position, global_position)
16            .with_time_ms(event_time.platform_time_ms)
17            .with_animation_time_nanos(event_time.animation_time_nanos);
18        event.modifiers = self.modifiers;
19        event
20    }
21
22    fn resolve_gesture_targets(
23        &self,
24        pointer: PointerId,
25    ) -> Vec<<<R as Renderer>::Scene as RenderScene>::HitTarget> {
26        self.resolve_hit_path(pointer)
27    }
28
29    fn resolve_hit_path(
30        &self,
31        pointer: PointerId,
32    ) -> Vec<<<R as Renderer>::Scene as RenderScene>::HitTarget> {
33        let Some(node_ids) = self.hit_path_tracker.dispatch_order(pointer) else {
34            return Vec::new();
35        };
36
37        let scene = self.renderer.scene();
38        let targets: Vec<_> = node_ids
39            .iter()
40            .filter_map(|&id| scene.find_target(id))
41            .collect();
42        log::trace!(
43            target: "cranpose::input",
44            "resolve_hit_path pointer={pointer:?} cached={node_ids:?} resolved_count={}",
45            targets.len()
46        );
47        targets
48    }
49
50    fn dispatch_targets<I>(&mut self, targets: I, event: PointerEvent, stop_on_consume: bool)
51    where
52        I: IntoIterator<Item = <<R as Renderer>::Scene as RenderScene>::HitTarget>,
53    {
54        let mut applier = self.composition.applier_mut();
55        for target in targets {
56            let node_id = target.node_id();
57            target.dispatch_with_applier(&mut applier, event.clone());
58            log::trace!(
59                target: "cranpose::input",
60                "dispatch {:?} node={} consumed={} stop_on_consume={}",
61                event.kind,
62                node_id,
63                event.is_consumed(),
64                stop_on_consume,
65            );
66            if stop_on_consume && event.is_consumed() {
67                break;
68            }
69        }
70        event.finish_post_dispatch();
71    }
72
73    /// Sets the device source (touch/mouse/stylus) of the pointer sample that
74    /// the platform is about to dispatch. Call this before `set_cursor` /
75    /// `pointer_pressed` / `pointer_released` so the resulting `PointerEvent`s
76    /// carry the source so consumers can preserve device-specific gesture
77    /// details without changing shared pointer UI.
78    pub fn set_pointer_source(&mut self, source: PointerSource) {
79        self.pointer_source = source;
80    }
81
82    /// The device source of the most recent pointer sample.
83    pub fn pointer_source(&self) -> PointerSource {
84        self.pointer_source
85    }
86
87    /// Sets the keyboard modifiers held right now, so the platform's live
88    /// modifier state (winit's `ModifiersChanged`, a DOM event's
89    /// `shiftKey`/`ctrlKey`/`altKey`/`metaKey`) reaches every `PointerEvent`
90    /// the shell dispatches from here on -- the same state the wheel path
91    /// already carries via [`WheelScroll::with_modifiers`](crate::WheelScroll::with_modifiers).
92    /// A platform that never calls this leaves pointer events reporting
93    /// `None` (see [`PointerEvent::modifiers`]) rather than a silently wrong
94    /// "nothing held".
95    pub fn set_modifiers(&mut self, modifiers: Modifiers) {
96        self.modifiers = Some(modifiers);
97    }
98
99    /// The keyboard modifiers most recently set via
100    /// [`set_modifiers`](Self::set_modifiers), or `None` if the platform has
101    /// never reported them.
102    pub fn modifiers(&self) -> Option<Modifiers> {
103        self.modifiers
104    }
105
106    pub fn set_cursor(&mut self, x: f32, y: f32) -> bool {
107        self.set_cursor_at_time(x, y, None)
108    }
109
110    /// Like [`set_cursor`](Self::set_cursor), but carries the platform input
111    /// timestamp (milliseconds, platform-specific time base) of the sample.
112    ///
113    /// Platforms that deliver input batched/frame-aligned (Android) must use
114    /// this so gesture velocity is computed from real event times instead of
115    /// delivery times.
116    pub fn set_cursor_at_time(&mut self, x: f32, y: f32, time_ms: Option<i64>) -> bool {
117        let event_time = self.realtime_pointer_event_time(time_ms);
118        self.set_cursor_at_event_time(x, y, event_time)
119    }
120
121    /// Set the cursor using a timestamp already resolved into both clock domains.
122    pub fn set_cursor_at_event_time(
123        &mut self,
124        x: f32,
125        y: f32,
126        event_time: PointerEventTime,
127    ) -> bool {
128        let _event_handler = enter_event_handler_scope();
129        let app_context = Rc::clone(&self.app_context);
130        let result = app_context.enter(|| {
131            run_in_mutable_snapshot(|| self.set_cursor_inner(x, y, event_time)).unwrap_or(false)
132        });
133        if result {
134            self.mark_dirty();
135        }
136        log::trace!(
137            target: "cranpose::input",
138            "set_cursor ({x:.2},{y:.2}) time_ms={:?} animation_time_nanos={} -> {result}",
139            event_time.platform_time_ms,
140            event_time.animation_time_nanos,
141        );
142        result
143    }
144
145    fn set_cursor_inner(&mut self, x: f32, y: f32, event_time: PointerEventTime) -> bool {
146        self.cursor = (x, y);
147
148        if self.buttons_pressed != PointerButtons::NONE {
149            if self.hit_path_tracker.has_path(PointerId::PRIMARY) {
150                let targets = self.resolve_gesture_targets(PointerId::PRIMARY);
151                if !targets.is_empty() {
152                    let event = self
153                        .pointer_event(
154                            PointerEventKind::Move,
155                            Point { x, y },
156                            Point { x, y },
157                            event_time,
158                        )
159                        .with_buttons(self.buttons_pressed)
160                        .with_source(self.pointer_source);
161                    self.dispatch_targets(targets, event, false);
162                    return true;
163                }
164
165                return false;
166            }
167
168            return false;
169        }
170
171        let hits = self.renderer.scene().hit_test(x, y);
172        let new_ids: Vec<NodeId> = hits.iter().map(|h| h.node_id()).collect();
173
174        let pos = Point { x, y };
175        let previously_hovered = self.hovered_nodes.clone();
176        for old_id in previously_hovered {
177            if !new_ids.contains(&old_id)
178                && let Some(target) = self.renderer.scene().find_target(old_id)
179            {
180                let exit_event = self
181                    .pointer_event(PointerEventKind::Exit, pos, pos, event_time)
182                    .with_buttons(self.buttons_pressed)
183                    .with_source(self.pointer_source);
184                self.dispatch_targets(std::iter::once(target), exit_event, false);
185            }
186        }
187
188        for hit in &hits {
189            if !self.hovered_nodes.contains(&hit.node_id()) {
190                let enter_event = self
191                    .pointer_event(PointerEventKind::Enter, pos, pos, event_time)
192                    .with_buttons(self.buttons_pressed)
193                    .with_source(self.pointer_source);
194                self.dispatch_targets(std::iter::once(hit.clone()), enter_event, false);
195            }
196        }
197
198        self.hovered_nodes = new_ids;
199
200        if !hits.is_empty() {
201            let event = self
202                .pointer_event(PointerEventKind::Move, pos, pos, event_time)
203                .with_buttons(self.buttons_pressed)
204                .with_source(self.pointer_source);
205            self.dispatch_targets(hits, event, true);
206            true
207        } else {
208            false
209        }
210    }
211
212    pub fn pointer_pressed(&mut self) -> bool {
213        self.pointer_pressed_at_time(None)
214    }
215
216    /// Like [`pointer_pressed`](Self::pointer_pressed), but carries the
217    /// platform input timestamp (milliseconds) of the press sample.
218    pub fn pointer_pressed_at_time(&mut self, time_ms: Option<i64>) -> bool {
219        let event_time = self.realtime_pointer_event_time(time_ms);
220        self.pointer_pressed_at_event_time(event_time)
221    }
222
223    /// Dispatch primary-button down with an already resolved event timestamp.
224    pub fn pointer_pressed_at_event_time(&mut self, event_time: PointerEventTime) -> bool {
225        if self.dev_overlay_press(self.cursor.0, self.cursor.1) {
226            return true;
227        }
228        let _event_handler = enter_event_handler_scope();
229        let app_context = Rc::clone(&self.app_context);
230        let result = app_context.enter(|| {
231            run_in_mutable_snapshot(|| self.pointer_pressed_inner(event_time)).unwrap_or(false)
232        });
233        if result {
234            self.mark_dirty();
235        }
236        log::trace!(
237            target: "cranpose::input",
238            "pointer_pressed time_ms={:?} animation_time_nanos={} -> {result}",
239            event_time.platform_time_ms,
240            event_time.animation_time_nanos,
241        );
242        result
243    }
244
245    fn pointer_pressed_inner(&mut self, event_time: PointerEventTime) -> bool {
246        self.buttons_pressed.insert(PointerButton::Primary);
247
248        let hits = self.renderer.scene().hit_test(self.cursor.0, self.cursor.1);
249        if hits.is_empty() {
250            self.hit_path_tracker.remove_path(PointerId::PRIMARY);
251            false
252        } else {
253            let event = self
254                .pointer_event(
255                    PointerEventKind::Down,
256                    Point {
257                        x: self.cursor.0,
258                        y: self.cursor.1,
259                    },
260                    Point {
261                        x: self.cursor.0,
262                        y: self.cursor.1,
263                    },
264                    event_time,
265                )
266                .with_buttons(self.buttons_pressed)
267                .with_source(self.pointer_source);
268
269            let mut delivered_capture_paths = Vec::new();
270            let mut applier = self.composition.applier_mut();
271            for hit in hits {
272                let node_id = hit.node_id();
273                delivered_capture_paths.push(hit.capture_path());
274                hit.dispatch_with_applier(&mut applier, event.clone());
275                log::trace!(
276                    target: "cranpose::input",
277                    "dispatch {:?} node={} consumed={} stop_on_consume=true",
278                    event.kind,
279                    node_id,
280                    event.is_consumed(),
281                );
282                if event.is_consumed() {
283                    break;
284                }
285            }
286
287            self.hit_path_tracker
288                .add_hit_path(PointerId::PRIMARY, delivered_capture_paths);
289            log::trace!(
290                target: "cranpose::input",
291                "pointer_pressed_inner cached_hit_path={:?}",
292                self.hit_path_tracker.get_path(PointerId::PRIMARY),
293            );
294
295            true
296        }
297    }
298
299    pub fn pointer_released(&mut self) -> bool {
300        self.pointer_released_at_time(None)
301    }
302
303    /// Releases the pointer at the position carried by the platform's release
304    /// sample (Android `ACTION_UP`, web `pointerup`/`touchend`).
305    ///
306    /// The cursor is moved to `(x, y)` WITHOUT dispatching a Move event, then
307    /// the Up event is dispatched at that position. Platforms whose release
308    /// events carry their own coordinates must use this instead of
309    /// `set_cursor* + pointer_released*`: lift-off samples routinely roll back
310    /// a few dp against the travel direction as the finger peels off, and
311    /// feeding that jitter into gesture velocity trackers as a final Move
312    /// sample can flip the sign of the computed fling velocity (flings that
313    /// suddenly go the opposite way). Jetpack Compose likewise never feeds the
314    /// up sample into velocity tracking.
315    pub fn pointer_released_at_position(&mut self, x: f32, y: f32) -> bool {
316        self.pointer_released_at_position_time(x, y, None)
317    }
318
319    /// Like [`pointer_released_at_position`](Self::pointer_released_at_position),
320    /// but carries the platform input timestamp (milliseconds) of the release
321    /// sample.
322    pub fn pointer_released_at_position_time(
323        &mut self,
324        x: f32,
325        y: f32,
326        time_ms: Option<i64>,
327    ) -> bool {
328        let event_time = self.realtime_pointer_event_time(time_ms);
329        self.pointer_released_at_position_event_time(x, y, event_time)
330    }
331
332    /// Release at a position with an already resolved event timestamp.
333    pub fn pointer_released_at_position_event_time(
334        &mut self,
335        x: f32,
336        y: f32,
337        event_time: PointerEventTime,
338    ) -> bool {
339        let _event_handler = enter_event_handler_scope();
340        let app_context = Rc::clone(&self.app_context);
341        let result = app_context.enter(|| {
342            run_in_mutable_snapshot(|| {
343                self.cursor = (x, y);
344                self.pointer_released_inner(event_time)
345            })
346            .unwrap_or(false)
347        });
348        if result {
349            self.mark_dirty();
350        }
351        log::trace!(
352            target: "cranpose::input",
353            "pointer_released_at_position ({x:.2},{y:.2}) time_ms={:?} animation_time_nanos={} -> {result}",
354            event_time.platform_time_ms,
355            event_time.animation_time_nanos,
356        );
357        result
358    }
359
360    /// Like [`pointer_released`](Self::pointer_released), but carries the
361    /// platform input timestamp (milliseconds) of the release sample.
362    pub fn pointer_released_at_time(&mut self, time_ms: Option<i64>) -> bool {
363        let event_time = self.realtime_pointer_event_time(time_ms);
364        self.pointer_released_at_event_time(event_time)
365    }
366
367    /// Dispatch primary-button up with an already resolved event timestamp.
368    pub fn pointer_released_at_event_time(&mut self, event_time: PointerEventTime) -> bool {
369        let _event_handler = enter_event_handler_scope();
370        let app_context = Rc::clone(&self.app_context);
371        let result = app_context.enter(|| {
372            run_in_mutable_snapshot(|| self.pointer_released_inner(event_time)).unwrap_or(false)
373        });
374        if result {
375            self.mark_dirty();
376        }
377        log::trace!(
378            target: "cranpose::input",
379            "pointer_released time_ms={:?} animation_time_nanos={} -> {result}",
380            event_time.platform_time_ms,
381            event_time.animation_time_nanos,
382        );
383        result
384    }
385
386    fn pointer_released_inner(&mut self, event_time: PointerEventTime) -> bool {
387        self.buttons_pressed.remove(PointerButton::Primary);
388        let corrected_buttons = self.buttons_pressed;
389        let targets = self.resolve_gesture_targets(PointerId::PRIMARY);
390
391        self.hit_path_tracker.remove_path(PointerId::PRIMARY);
392
393        if !targets.is_empty() {
394            let event = self
395                .pointer_event(
396                    PointerEventKind::Up,
397                    Point {
398                        x: self.cursor.0,
399                        y: self.cursor.1,
400                    },
401                    Point {
402                        x: self.cursor.0,
403                        y: self.cursor.1,
404                    },
405                    event_time,
406                )
407                .with_buttons(corrected_buttons)
408                .with_source(self.pointer_source);
409
410            self.dispatch_targets(targets, event, false);
411            true
412        } else {
413            false
414        }
415    }
416
417    /// Dispatches an event for a secondary pointer (`pointer_id != 0`).
418    ///
419    /// Multi-touch gestures act on the element the first finger grabbed, so
420    /// secondary pointers are routed to the hit path captured by the primary
421    /// pointer's Down. They carry no hover/click semantics and are ignored
422    /// when no primary gesture is in progress.
423    ///
424    /// Returns `true` when the event was dispatched to at least one target.
425    pub fn secondary_pointer_pressed(
426        &mut self,
427        pointer_id: u64,
428        x: f32,
429        y: f32,
430        time_ms: Option<i64>,
431    ) -> bool {
432        let event_time = self.realtime_pointer_event_time(time_ms);
433        self.dispatch_secondary_pointer(PointerEventKind::Down, pointer_id, x, y, event_time)
434    }
435
436    /// Move counterpart of [`secondary_pointer_pressed`](Self::secondary_pointer_pressed).
437    pub fn secondary_pointer_moved(
438        &mut self,
439        pointer_id: u64,
440        x: f32,
441        y: f32,
442        time_ms: Option<i64>,
443    ) -> bool {
444        let event_time = self.realtime_pointer_event_time(time_ms);
445        self.dispatch_secondary_pointer(PointerEventKind::Move, pointer_id, x, y, event_time)
446    }
447
448    /// Release counterpart of [`secondary_pointer_pressed`](Self::secondary_pointer_pressed).
449    pub fn secondary_pointer_released(
450        &mut self,
451        pointer_id: u64,
452        x: f32,
453        y: f32,
454        time_ms: Option<i64>,
455    ) -> bool {
456        let event_time = self.realtime_pointer_event_time(time_ms);
457        self.dispatch_secondary_pointer(PointerEventKind::Up, pointer_id, x, y, event_time)
458    }
459
460    fn dispatch_secondary_pointer(
461        &mut self,
462        kind: PointerEventKind,
463        pointer_id: u64,
464        x: f32,
465        y: f32,
466        event_time: PointerEventTime,
467    ) -> bool {
468        if pointer_id == 0 {
469            log::warn!(
470                target: "cranpose::input",
471                "secondary pointer dispatch called with the primary pointer id"
472            );
473            return false;
474        }
475
476        let _event_handler = enter_event_handler_scope();
477        let app_context = Rc::clone(&self.app_context);
478        let result = app_context.enter(|| {
479            run_in_mutable_snapshot(|| {
480                if !self.hit_path_tracker.has_path(PointerId::PRIMARY) {
481                    return false;
482                }
483                let targets = self.resolve_gesture_targets(PointerId::PRIMARY);
484                if targets.is_empty() {
485                    return false;
486                }
487                let pos = Point { x, y };
488                let event = self
489                    .pointer_event(kind, pos, pos, event_time)
490                    .with_buttons(self.buttons_pressed)
491                    .with_id(pointer_id)
492                    .with_source(self.pointer_source);
493                self.dispatch_targets(targets, event, false);
494                true
495            })
496            .unwrap_or(false)
497        });
498        if result {
499            self.mark_dirty();
500        }
501        log::trace!(
502            target: "cranpose::input",
503            "secondary_pointer {kind:?} id={pointer_id} ({x:.2},{y:.2}) time_ms={:?} animation_time_nanos={} -> {result}",
504            event_time.platform_time_ms,
505            event_time.animation_time_nanos,
506        );
507        result
508    }
509
510    /// Dispatches a discrete zoom step (desktop ctrl+wheel, browser pinch)
511    /// to the pointer handlers under the cursor.
512    ///
513    /// `zoom_factor` is multiplicative: `> 1.0` zooms in, `< 1.0` zooms out.
514    /// Returns `true` if a handler consumed the event.
515    pub fn pointer_zoomed(&mut self, zoom_factor: f32) -> bool {
516        let event_time = self.realtime_pointer_event_time(None);
517        let _event_handler = enter_event_handler_scope();
518        let app_context = Rc::clone(&self.app_context);
519        let result = app_context.enter(|| {
520            run_in_mutable_snapshot(|| self.pointer_zoomed_inner(zoom_factor, event_time))
521                .unwrap_or(false)
522        });
523        if result {
524            self.mark_dirty();
525        }
526        log::trace!(
527            target: "cranpose::input",
528            "pointer_zoomed factor={zoom_factor:.4} -> {result}"
529        );
530        result
531    }
532
533    fn pointer_zoomed_inner(&mut self, zoom_factor: f32, event_time: PointerEventTime) -> bool {
534        if !zoom_factor.is_finite() || zoom_factor <= 0.0 || zoom_factor == 1.0 {
535            return false;
536        }
537
538        let hits = self.renderer.scene().hit_test(self.cursor.0, self.cursor.1);
539        if hits.is_empty() {
540            return false;
541        }
542
543        let pos = Point {
544            x: self.cursor.0,
545            y: self.cursor.1,
546        };
547        let event = self
548            .pointer_event(PointerEventKind::Zoom, pos, pos, event_time)
549            .with_buttons(self.buttons_pressed)
550            .with_zoom_delta(zoom_factor)
551            .with_source(self.pointer_source);
552
553        let capture_paths = hits
554            .iter()
555            .map(|hit| hit.capture_path())
556            .collect::<Vec<_>>();
557        let targets = crate::hit_path_tracker::dispatch_order_for_paths(&capture_paths)
558            .into_iter()
559            .filter_map(|node_id| self.renderer.scene().find_target(node_id))
560            .collect::<Vec<_>>();
561
562        self.dispatch_targets(targets, event.clone(), true);
563
564        event.is_consumed()
565    }
566
567    /// Dispatches one mouse-wheel / trackpad sample through the whole wheel
568    /// policy, and returns `true` when something consumed it.
569    ///
570    /// This is the single entry point every host with a wheel calls, after
571    /// placing the cursor. A wheel sample is not just a scroll — it is whichever
572    /// of four things the modifiers and the tree make it, in this order:
573    ///
574    /// 1. **Zoom** when ctrl is held. That is the desktop convention and the
575    ///    way browsers deliver a trackpad pinch, so both arrive here as the
576    ///    same gesture.
577    /// 2. **Rotary**, offered to [`rotary_scrolled`](Self::rotary_scrolled)
578    ///    before anything else can take it, so the Wear OS crown stack is
579    ///    developable on a machine with a wheel. Nothing consumes rotary unless
580    ///    the app opts in via `Modifier::on_rotary_scroll_event` or
581    ///    [`set_on_rotary_scroll`](Self::set_on_rotary_scroll), so ordinary
582    ///    scrolling is unaffected.
583    /// 3. **Horizontal scroll** when alt is held on a wheel that only reports a
584    ///    vertical axis.
585    /// 4. **Scroll**, to the hovered scrollable.
586    ///
587    /// Hosts must not re-implement this order. Doing so is how the browser
588    /// ended up scrolling backwards and never delivering rotary at all: the
589    /// policy lived in the desktop event loop, and the second host that grew a
590    /// wheel reimplemented the parts of it that were obvious from the outside.
591    pub fn wheel_scrolled(&mut self, wheel: crate::WheelScroll) -> bool {
592        if wheel.is_zoom() {
593            let zoom_factor = wheel.zoom_factor();
594            log::trace!(
595                target: "cranpose::input",
596                "wheel zoom factor={zoom_factor:.4}"
597            );
598            return self.pointer_zoomed(zoom_factor);
599        }
600
601        let rotary =
602            RotaryScrollEvent::from_wheel_pixels(wheel.delta.y, wheel.delta.x, wheel.uptime_millis);
603        if self.rotary_scrolled(rotary) {
604            return true;
605        }
606
607        let delta = wheel.scroll_delta();
608        log::trace!(
609            target: "cranpose::input",
610            "wheel delta ({:.2},{:.2}) alt={}",
611            delta.x,
612            delta.y,
613            wheel.modifiers.alt
614        );
615        self.pointer_scrolled(delta.x, delta.y)
616    }
617
618    /// Dispatches a mouse wheel / trackpad scroll event to hovered pointer handlers.
619    ///
620    /// Returns `true` if a handler consumed the event.
621    ///
622    /// This is the last step of the wheel policy, not its entry point: hosts
623    /// call [`wheel_scrolled`](Self::wheel_scrolled), which reaches here once
624    /// zoom and rotary have declined the sample.
625    pub fn pointer_scrolled(&mut self, delta_x: f32, delta_y: f32) -> bool {
626        let event_time = self.realtime_pointer_event_time(None);
627        let _event_handler = enter_event_handler_scope();
628        let app_context = Rc::clone(&self.app_context);
629        let result = app_context.enter(|| {
630            run_in_mutable_snapshot(|| self.pointer_scrolled_inner(delta_x, delta_y, event_time))
631                .unwrap_or(false)
632        });
633        if result {
634            self.mark_dirty();
635        }
636        log::trace!(
637            target: "cranpose::input",
638            "pointer_scrolled ({delta_x:.2},{delta_y:.2}) -> {result}"
639        );
640        result
641    }
642
643    fn pointer_scrolled_inner(
644        &mut self,
645        delta_x: f32,
646        delta_y: f32,
647        event_time: PointerEventTime,
648    ) -> bool {
649        if delta_x.abs() <= f32::EPSILON && delta_y.abs() <= f32::EPSILON {
650            return false;
651        }
652
653        let hits = self.renderer.scene().hit_test(self.cursor.0, self.cursor.1);
654        if hits.is_empty() {
655            return false;
656        }
657
658        let event = self
659            .pointer_event(
660                PointerEventKind::Scroll,
661                Point {
662                    x: self.cursor.0,
663                    y: self.cursor.1,
664                },
665                Point {
666                    x: self.cursor.0,
667                    y: self.cursor.1,
668                },
669                event_time,
670            )
671            .with_buttons(self.buttons_pressed)
672            .with_scroll_delta(Point {
673                x: delta_x,
674                y: delta_y,
675            })
676            .with_source(self.pointer_source);
677
678        let capture_paths = hits
679            .iter()
680            .map(|hit| hit.capture_path())
681            .collect::<Vec<_>>();
682        let targets = crate::hit_path_tracker::dispatch_order_for_paths(&capture_paths)
683            .into_iter()
684            .filter_map(|node_id| self.renderer.scene().find_target(node_id))
685            .collect::<Vec<_>>();
686
687        self.dispatch_targets(targets, event.clone(), true);
688
689        event.is_consumed()
690    }
691
692    /// Installs the window-level rotary (Wear OS crown / rotating bezel)
693    /// handler — the low-level escape hatch.
694    ///
695    /// The handler runs only after the routed modifier chain has declined the
696    /// event (see [`rotary_scrolled`](Self::rotary_scrolled)), so an app that
697    /// draws everything into a single canvas receives every rotary delta
698    /// without registering a focus target or a modifier. Returning `true`
699    /// reports the event as consumed to the platform.
700    ///
701    /// Passing a new handler replaces the previous one.
702    pub fn set_on_rotary_scroll<F>(&mut self, handler: F)
703    where
704        F: Fn(RotaryScrollEvent) -> bool + 'static,
705    {
706        self.on_rotary_scroll = Some(Rc::new(handler));
707    }
708
709    /// Removes the window-level rotary handler, if one is installed.
710    pub fn clear_on_rotary_scroll(&mut self) {
711        self.on_rotary_scroll = None;
712    }
713
714    /// Pixels per rotary detent used by
715    /// [`rotary_scrolled_by_detents`](Self::rotary_scrolled_by_detents).
716    pub fn rotary_scroll_factor(&self) -> f32 {
717        self.rotary_scroll_factor
718    }
719
720    /// Sets the pixels-per-detent factor for rotary input.
721    ///
722    /// On Wear OS this must be `ViewConfiguration.getScaledVerticalScrollFactor()`
723    /// for pixel-exact parity with Compose. The host activity can read it over
724    /// JNI once at startup and push it here; when it does not, the shell falls
725    /// back to [`DEFAULT_ROTARY_SCROLL_FACTOR_DP`] scaled by display density.
726    ///
727    /// Non-finite or non-positive values are ignored.
728    pub fn set_rotary_scroll_factor(&mut self, factor: f32) {
729        if factor.is_finite() && factor > 0.0 {
730            self.rotary_scroll_factor = factor;
731        }
732    }
733
734    /// Dispatches a rotary scroll expressed in raw detents (Android
735    /// `AXIS_SCROLL`), converting to pixels with the configured scroll factor.
736    ///
737    /// Applies Compose's sign convention: a positive detent value (crown turned
738    /// up/away) produces a negative `vertical_scroll_pixels`.
739    pub fn rotary_scrolled_by_detents(&mut self, detents: f32, uptime_millis: u64) -> bool {
740        let factor = self.rotary_scroll_factor;
741        self.rotary_scrolled(RotaryScrollEvent::from_detents(
742            detents,
743            factor,
744            factor,
745            uptime_millis,
746        ))
747    }
748
749    /// Dispatches a rotary scroll event (Wear OS crown, Galaxy Watch bezel, or
750    /// a desktop mouse wheel standing in for one during development).
751    ///
752    /// Routing mirrors Compose's `RotaryInputModifierNode` contract:
753    ///
754    /// 1. Resolve the target chain. When a focus target is registered
755    ///    (`cranpose_ui::focus_dispatch::active_focus_target`) and still
756    ///    exists in the current scene, its capture path is used, so rotary goes
757    ///    to the focused node exactly as on Wear OS. Cranpose does not yet wire
758    ///    focus automatically, so in practice this falls back to the chain
759    ///    under the current cursor position.
760    /// 2. **Capture pass**, root to leaf, invoking `on_pre_rotary_scroll_event`
761    ///    handlers.
762    /// 3. **Bubble pass**, leaf to root, invoking `on_rotary_scroll_event`
763    ///    handlers.
764    /// 4. If still unconsumed, the window-level handler installed by
765    ///    [`set_on_rotary_scroll`](Self::set_on_rotary_scroll).
766    ///
767    /// The first handler returning `true` consumes the event and stops every
768    /// remaining step. Returns `true` when the event was consumed.
769    pub fn rotary_scrolled(&mut self, event: RotaryScrollEvent) -> bool {
770        let _event_handler = enter_event_handler_scope();
771        let app_context = Rc::clone(&self.app_context);
772        let result = app_context.enter(|| {
773            run_in_mutable_snapshot(|| self.rotary_scrolled_inner(event)).unwrap_or(false)
774        });
775        if result {
776            self.mark_dirty();
777        }
778        log::trace!(
779            target: "cranpose::input",
780            "rotary_scrolled v={:.2} h={:.2} uptime={} -> {result}",
781            event.vertical_scroll_pixels,
782            event.horizontal_scroll_pixels,
783            event.uptime_millis,
784        );
785        result
786    }
787
788    fn rotary_scrolled_inner(&mut self, rotary: RotaryScrollEvent) -> bool {
789        if rotary.is_empty() {
790            return false;
791        }
792
793        let bubble_order = self.rotary_dispatch_order();
794        let position = Point {
795            x: self.cursor.0,
796            y: self.cursor.1,
797        };
798
799        if !bubble_order.is_empty() {
800            let capture_targets = bubble_order
801                .iter()
802                .rev()
803                .filter_map(|&node_id| self.renderer.scene().find_target(node_id))
804                .collect::<Vec<_>>();
805            let mut capture_event =
806                PointerEvent::rotary(PointerEventKind::RotaryScrollPre, rotary, position);
807            capture_event.modifiers = self.modifiers;
808            self.dispatch_targets(capture_targets, capture_event.clone(), true);
809            if capture_event.is_consumed() {
810                return true;
811            }
812
813            let bubble_targets = bubble_order
814                .iter()
815                .filter_map(|&node_id| self.renderer.scene().find_target(node_id))
816                .collect::<Vec<_>>();
817            let mut bubble_event =
818                PointerEvent::rotary(PointerEventKind::RotaryScroll, rotary, position);
819            bubble_event.modifiers = self.modifiers;
820            self.dispatch_targets(bubble_targets, bubble_event.clone(), true);
821            if bubble_event.is_consumed() {
822                return true;
823            }
824        }
825
826        if let Some(handler) = self.on_rotary_scroll.clone() {
827            return handler(rotary);
828        }
829
830        false
831    }
832
833    fn rotary_dispatch_order(&self) -> Vec<NodeId> {
834        if let Some(focused) = cranpose_ui::active_focus_target()
835            && let Some(target) = self.renderer.scene().find_target(focused)
836        {
837            let path = target.capture_path();
838            if !path.is_empty() {
839                return crate::hit_path_tracker::dispatch_order_for_paths(&[path]);
840            }
841        }
842
843        let hits = self.renderer.scene().hit_test(self.cursor.0, self.cursor.1);
844        if hits.is_empty() {
845            return Vec::new();
846        }
847        let capture_paths = hits
848            .iter()
849            .map(|hit| hit.capture_path())
850            .collect::<Vec<_>>();
851        crate::hit_path_tracker::dispatch_order_for_paths(&capture_paths)
852    }
853
854    /// Cancels any active gesture, dispatching Cancel events to cached targets.
855    /// Call this when:
856    /// - Window loses focus
857    /// - Mouse leaves window while button pressed
858    /// - Any other gesture abort scenario
859    pub fn cancel_gesture(&mut self) {
860        let event_time = self.realtime_pointer_event_time(None);
861        let _event_handler = enter_event_handler_scope();
862        let app_context = Rc::clone(&self.app_context);
863        let _ = app_context.enter(|| {
864            run_in_mutable_snapshot(|| {
865                self.cancel_gesture_inner(event_time);
866            })
867        });
868    }
869
870    fn cancel_gesture_inner(&mut self, event_time: PointerEventTime) {
871        let targets = self.resolve_gesture_targets(PointerId::PRIMARY);
872
873        self.hit_path_tracker.clear();
874        self.buttons_pressed = PointerButtons::NONE;
875
876        if !targets.is_empty() {
877            let event = self
878                .pointer_event(
879                    PointerEventKind::Cancel,
880                    Point {
881                        x: self.cursor.0,
882                        y: self.cursor.1,
883                    },
884                    Point {
885                        x: self.cursor.0,
886                        y: self.cursor.1,
887                    },
888                    event_time,
889                )
890                .with_source(self.pointer_source);
891
892            self.dispatch_targets(targets, event, false);
893        }
894
895        let pos = Point {
896            x: self.cursor.0,
897            y: self.cursor.1,
898        };
899        let hovered_nodes = self.hovered_nodes.clone();
900        for node_id in hovered_nodes {
901            if let Some(target) = self.renderer.scene().find_target(node_id) {
902                let exit_event = self
903                    .pointer_event(PointerEventKind::Exit, pos, pos, event_time)
904                    .with_source(self.pointer_source);
905                self.dispatch_targets(std::iter::once(target), exit_event, false);
906            }
907        }
908        self.hovered_nodes.clear();
909    }
910
911    /// Installs the platform soft-keyboard handler for this shell's app context.
912    ///
913    /// The handler is invoked when a text field gains focus (`show_keyboard`)
914    /// or when text-field focus is cleared or goes stale (`hide_keyboard`).
915    /// Platform runtimes with an on-screen keyboard (Android, iOS) call this
916    /// once after creating the shell.
917    pub fn set_platform_text_input(
918        &mut self,
919        handler: Rc<dyn cranpose_ui::PlatformTextInputHandler>,
920    ) {
921        let app_context = Rc::clone(&self.app_context);
922        app_context
923            .enter(|| cranpose_ui::text_input_session::set_platform_text_input_handler(handler));
924    }
925
926    /// Notifies the framework that the host app was paused/backgrounded.
927    ///
928    /// Withdraws any outstanding soft-keyboard request (and hides the keyboard)
929    /// so the "keyboard shown" state does not survive across the pause and get
930    /// restored on resume with no focused field. Platform runtimes call this
931    /// from their pause lifecycle event.
932    pub fn notify_app_paused(&mut self) {
933        let app_context = Rc::clone(&self.app_context);
934        app_context.enter(cranpose_ui::text_input_session::notify_app_paused);
935    }
936
937    /// Notifies the framework that the host app resumed/foregrounded.
938    ///
939    /// Never auto-shows the soft keyboard, even for a still-focused field: a
940    /// warm resume keeps the caret but must not resurrect the keyboard (the user
941    /// taps the field to bring it back). Always returns `false` so the platform
942    /// runtime force-hides the OS-restored keyboard. Platform runtimes call this
943    /// from their resume lifecycle event.
944    pub fn notify_app_resumed(&mut self) -> bool {
945        let app_context = Rc::clone(&self.app_context);
946        app_context.enter(cranpose_ui::text_input_session::notify_app_resumed)
947    }
948
949    /// Routes a keyboard event to the focused text field, if any.
950    ///
951    /// Returns `true` if the event was consumed by a text field.
952    ///
953    /// On desktop, Ctrl+C/X/V are handled here when native clipboard support is enabled.
954    /// On web, these keys are NOT handled here - they bubble to browser for native copy/paste events.
955    pub fn on_key_event(&mut self, event: &KeyEvent) -> bool {
956        let _event_handler = enter_event_handler_scope();
957        let app_context = Rc::clone(&self.app_context);
958        app_context.enter(|| self.on_key_event_inner(event))
959    }
960
961    fn on_key_event_inner(&mut self, event: &KeyEvent) -> bool {
962        use KeyEventType::KeyDown;
963
964        if event.event_type == KeyDown && event.modifiers.command_or_ctrl() {
965            #[cfg(all(
966                feature = "clipboard-native",
967                not(target_arch = "wasm32"),
968                not(target_os = "android"),
969                not(target_os = "ios")
970            ))]
971            {
972                match event.key_code {
973                    KeyCode::C => {
974                        if let Some(text) = self.on_copy_inner() {
975                            cranpose_ui::clipboard_session::clipboard_write_text(&text);
976                            return true;
977                        }
978                    }
979                    KeyCode::X => {
980                        if let Some(text) = self.on_cut_inner() {
981                            cranpose_ui::clipboard_session::clipboard_write_text(&text);
982                            self.mark_dirty();
983                            self.request_layout_pass();
984                            return true;
985                        }
986                    }
987                    KeyCode::V => {
988                        if let Some(text) = cranpose_ui::clipboard_session::clipboard_read_text()
989                            && self.on_paste_inner(&text)
990                        {
991                            return true;
992                        }
993                    }
994                    _ => {}
995                }
996            }
997        }
998
999        if !cranpose_ui::text_field_focus::has_focused_field() {
1000            return false;
1001        }
1002
1003        let handled =
1004            run_in_mutable_snapshot(|| cranpose_ui::text_field_focus::dispatch_key_event(event))
1005                .unwrap_or(false);
1006
1007        if handled {
1008            self.mark_dirty();
1009            self.request_layout_pass();
1010        }
1011
1012        handled
1013    }
1014
1015    /// Handles paste event from platform clipboard.
1016    /// Returns `true` if the paste was consumed by a focused text field.
1017    /// O(1) operation using stored handler.
1018    pub fn on_paste(&mut self, text: &str) -> bool {
1019        let _event_handler = enter_event_handler_scope();
1020        let app_context = Rc::clone(&self.app_context);
1021        app_context.enter(|| self.on_paste_inner(text))
1022    }
1023
1024    fn on_paste_inner(&mut self, text: &str) -> bool {
1025        let handled =
1026            run_in_mutable_snapshot(|| cranpose_ui::text_field_focus::dispatch_paste(text))
1027                .unwrap_or(false);
1028
1029        if handled {
1030            self.mark_dirty();
1031            self.request_layout_pass();
1032        }
1033
1034        handled
1035    }
1036
1037    /// Handles copy request from platform.
1038    /// Returns the selected text from focused text field, or None.
1039    /// O(1) operation using stored handler.
1040    pub fn on_copy(&mut self) -> Option<String> {
1041        let app_context = Rc::clone(&self.app_context);
1042        app_context.enter(|| self.on_copy_inner())
1043    }
1044
1045    fn on_copy_inner(&mut self) -> Option<String> {
1046        cranpose_ui::text_field_focus::dispatch_copy()
1047    }
1048
1049    /// Handles cut request from platform.
1050    /// Returns the cut text from focused text field, or None.
1051    /// O(1) operation using stored handler.
1052    pub fn on_cut(&mut self) -> Option<String> {
1053        let _event_handler = enter_event_handler_scope();
1054        let app_context = Rc::clone(&self.app_context);
1055        app_context.enter(|| self.on_cut_inner())
1056    }
1057
1058    fn on_cut_inner(&mut self) -> Option<String> {
1059        let text =
1060            run_in_mutable_snapshot(cranpose_ui::text_field_focus::dispatch_cut).unwrap_or(None);
1061
1062        if text.is_some() {
1063            self.mark_dirty();
1064            self.request_layout_pass();
1065        }
1066
1067        text
1068    }
1069
1070    /// Sets the Linux primary selection (for middle-click paste).
1071    /// This is called when text is selected in a text field.
1072    /// On non-Linux platforms, this is a no-op.
1073    #[cfg(all(
1074        feature = "clipboard-native",
1075        target_os = "linux",
1076        not(target_arch = "wasm32")
1077    ))]
1078    pub fn set_primary_selection(&mut self, text: &str) {
1079        use arboard::{LinuxClipboardKind, SetExtLinux};
1080        if let Some(ref mut clipboard) = self.clipboard {
1081            let result = clipboard
1082                .set()
1083                .clipboard(LinuxClipboardKind::Primary)
1084                .text(text.to_string());
1085            if let Err(e) = result {
1086                log::debug!("Primary selection set failed: {:?}", e);
1087            }
1088        }
1089    }
1090
1091    #[cfg(not(all(
1092        feature = "clipboard-native",
1093        target_os = "linux",
1094        not(target_arch = "wasm32")
1095    )))]
1096    pub fn set_primary_selection(&mut self, _text: &str) {}
1097
1098    /// Gets text from the Linux primary selection (for middle-click paste).
1099    /// On non-Linux platforms, returns None.
1100    #[cfg(all(
1101        feature = "clipboard-native",
1102        target_os = "linux",
1103        not(target_arch = "wasm32")
1104    ))]
1105    pub fn get_primary_selection(&mut self) -> Option<String> {
1106        use arboard::{GetExtLinux, LinuxClipboardKind};
1107        if let Some(ref mut clipboard) = self.clipboard {
1108            clipboard
1109                .get()
1110                .clipboard(LinuxClipboardKind::Primary)
1111                .text()
1112                .ok()
1113        } else {
1114            None
1115        }
1116    }
1117
1118    #[cfg(not(all(
1119        feature = "clipboard-native",
1120        target_os = "linux",
1121        not(target_arch = "wasm32")
1122    )))]
1123    pub fn get_primary_selection(&mut self) -> Option<String> {
1124        None
1125    }
1126
1127    /// Syncs the current text field selection to PRIMARY (Linux X11).
1128    /// Call this when selection changes in a text field.
1129    pub fn sync_selection_to_primary(&mut self) {
1130        #[cfg(all(target_os = "linux", not(target_arch = "wasm32")))]
1131        {
1132            if let Some(text) = self.on_copy() {
1133                self.set_primary_selection(&text);
1134            }
1135        }
1136    }
1137
1138    /// Handles IME preedit (composition) events.
1139    /// Called when the input method is composing text (e.g., typing CJK characters).
1140    ///
1141    /// - `text`: The current preedit text (empty to clear composition state)
1142    /// - `cursor`: Optional cursor position within the preedit text (start, end)
1143    ///
1144    /// Returns `true` if a text field consumed the event.
1145    pub fn on_ime_preedit(&mut self, text: &str, cursor: Option<(usize, usize)>) -> bool {
1146        let _event_handler = enter_event_handler_scope();
1147        let app_context = Rc::clone(&self.app_context);
1148        app_context.enter(|| self.on_ime_preedit_inner(text, cursor))
1149    }
1150
1151    fn on_ime_preedit_inner(&mut self, text: &str, cursor: Option<(usize, usize)>) -> bool {
1152        let handled = run_in_mutable_snapshot(|| {
1153            cranpose_ui::text_field_focus::dispatch_ime_preedit(text, cursor)
1154        })
1155        .unwrap_or(false);
1156
1157        if handled {
1158            self.mark_dirty();
1159            self.request_layout_pass();
1160        }
1161
1162        handled
1163    }
1164
1165    /// Finishes the active IME composition, keeping the composed text as
1166    /// committed text (Android `finishComposingText` semantics).
1167    /// Returns `true` if a text field consumed the event.
1168    pub fn on_ime_finish_composing(&mut self) -> bool {
1169        let _event_handler = enter_event_handler_scope();
1170        let app_context = Rc::clone(&self.app_context);
1171        app_context.enter(|| self.on_ime_finish_composing_inner())
1172    }
1173
1174    fn on_ime_finish_composing_inner(&mut self) -> bool {
1175        let handled =
1176            run_in_mutable_snapshot(cranpose_ui::text_field_focus::dispatch_ime_finish_composing)
1177                .unwrap_or(false);
1178
1179        if handled {
1180            self.mark_dirty();
1181            self.request_layout_pass();
1182        }
1183
1184        handled
1185    }
1186
1187    /// Marks existing text in the focused field as the composing region
1188    /// without changing it (Android `setComposingRegion` semantics). Offsets
1189    /// are UTF-8 bytes. Returns `true` if a text field consumed the event.
1190    pub fn on_ime_set_composing_region(&mut self, start_bytes: usize, end_bytes: usize) -> bool {
1191        let _event_handler = enter_event_handler_scope();
1192        let app_context = Rc::clone(&self.app_context);
1193        app_context.enter(|| {
1194            let handled = run_in_mutable_snapshot(|| {
1195                cranpose_ui::text_field_focus::dispatch_ime_set_composing_region(
1196                    start_bytes,
1197                    end_bytes,
1198                )
1199            })
1200            .unwrap_or(false);
1201
1202            if handled {
1203                self.mark_dirty();
1204                self.request_layout_pass();
1205            }
1206
1207            handled
1208        })
1209    }
1210
1211    /// Moves the focused field's selection/caret to `[start_bytes, end_bytes)`
1212    /// without editing text (Android `InputConnection.setSelection`; the path
1213    /// Gboard's spacebar-swipe uses to scrub the cursor). Offsets are UTF-8
1214    /// bytes. Returns `true` if a text field consumed the event.
1215    pub fn on_ime_set_selection(&mut self, start_bytes: usize, end_bytes: usize) -> bool {
1216        let _event_handler = enter_event_handler_scope();
1217        let app_context = Rc::clone(&self.app_context);
1218        app_context.enter(|| {
1219            let handled = run_in_mutable_snapshot(|| {
1220                cranpose_ui::text_field_focus::dispatch_ime_set_selection(start_bytes, end_bytes)
1221            })
1222            .unwrap_or(false);
1223
1224            if handled {
1225                self.mark_dirty();
1226            }
1227
1228            handled
1229        })
1230    }
1231
1232    /// Returns a snapshot of the focused text field's editable state for
1233    /// platform IMEs (text, selection and composition in UTF-8 bytes), or
1234    /// `None` when no text field is focused.
1235    pub fn ime_editor_state(&mut self) -> Option<cranpose_ui::text_field_focus::ImeEditorState> {
1236        let app_context = Rc::clone(&self.app_context);
1237        app_context.enter(cranpose_ui::text_field_focus::focused_editor_state)
1238    }
1239
1240    /// Window-space caret geometry of the focused field for coordinate-based
1241    /// platform text input (iOS trackpad cursor + tap-to-position), or `None`
1242    /// when no text field is focused.
1243    pub fn ime_caret_geometry(
1244        &mut self,
1245    ) -> Option<cranpose_ui::text_field_focus::ImeCaretGeometry> {
1246        let app_context = Rc::clone(&self.app_context);
1247        app_context.enter(cranpose_ui::text_field_focus::focused_caret_geometry)
1248    }
1249
1250    /// Clears text-field focus (used by platform IME actions such as
1251    /// Android's Done). The focus-loss notification hides the soft keyboard.
1252    pub fn clear_text_field_focus(&mut self) {
1253        let _event_handler = enter_event_handler_scope();
1254        let app_context = Rc::clone(&self.app_context);
1255        app_context.enter(cranpose_ui::text_field_focus::clear_focus);
1256        self.mark_dirty();
1257        self.request_layout_pass();
1258    }
1259
1260    /// Handles IME delete-surrounding events.
1261    /// Returns `true` if a text field consumed the event.
1262    pub fn on_ime_delete_surrounding(&mut self, before_bytes: usize, after_bytes: usize) -> bool {
1263        let _event_handler = enter_event_handler_scope();
1264        let app_context = Rc::clone(&self.app_context);
1265        app_context.enter(|| self.on_ime_delete_surrounding_inner(before_bytes, after_bytes))
1266    }
1267
1268    fn on_ime_delete_surrounding_inner(&mut self, before_bytes: usize, after_bytes: usize) -> bool {
1269        let handled = run_in_mutable_snapshot(|| {
1270            cranpose_ui::text_field_focus::dispatch_delete_surrounding(before_bytes, after_bytes)
1271        })
1272        .unwrap_or(false);
1273
1274        if handled {
1275            self.mark_dirty();
1276            self.request_layout_pass();
1277        }
1278
1279        handled
1280    }
1281}