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