Skip to main content

cranpose_app_shell/
shell_input.rs

1use cranpose_ui::FocusDirection;
2
3use super::*;
4
5impl<R> SurfaceMut<'_, R>
6where
7    R: Renderer,
8    R::Error: Debug,
9{
10    fn pointer_event(
11        &self,
12        kind: PointerEventKind,
13        position: Point,
14        global_position: Point,
15        event_time: PointerEventTime,
16    ) -> PointerEvent {
17        let screen_position = self.surface().screen_origin.map(|origin| Point {
18            x: origin.x + global_position.x,
19            y: origin.y + global_position.y,
20        });
21        let mut event = PointerEvent::new(kind, position, global_position)
22            .with_time_ms(event_time.platform_time_ms)
23            .with_animation_time_nanos(event_time.animation_time_nanos)
24            .with_screen_position(screen_position);
25        event.modifiers = self.shell.app.modifiers;
26        event
27    }
28
29    fn resolve_gesture_targets(
30        &self,
31        pointer: PointerId,
32    ) -> Vec<<<R as Renderer>::Scene as RenderScene>::HitTarget> {
33        self.resolve_hit_path(pointer)
34    }
35
36    fn resolve_hit_path(
37        &self,
38        pointer: PointerId,
39    ) -> Vec<<<R as Renderer>::Scene as RenderScene>::HitTarget> {
40        let Some(node_ids) = self.surface().hit_path_tracker.dispatch_order(pointer) else {
41            return Vec::new();
42        };
43
44        let scene = self.surface().renderer.scene();
45        let targets: Vec<_> = node_ids
46            .iter()
47            .filter_map(|&id| scene.find_target(id))
48            .collect();
49        log::trace!(
50            target: "cranpose::input",
51            "resolve_hit_path pointer={pointer:?} cached={node_ids:?} resolved_count={}",
52            targets.len()
53        );
54        targets
55    }
56
57    fn dispatch_targets<I>(&mut self, targets: I, event: PointerEvent, stop_on_consume: bool)
58    where
59        I: IntoIterator<Item = <<R as Renderer>::Scene as RenderScene>::HitTarget>,
60    {
61        let mut applier = self.shell.app.composition.applier_mut();
62        for target in targets {
63            let node_id = target.node_id();
64            target.dispatch_with_applier(&mut applier, event.clone());
65            log::trace!(
66                target: "cranpose::input",
67                "dispatch {:?} node={} consumed={} stop_on_consume={}",
68                event.kind,
69                node_id,
70                event.is_consumed(),
71                stop_on_consume,
72            );
73            if stop_on_consume && event.is_consumed() {
74                break;
75            }
76        }
77        event.finish_post_dispatch();
78    }
79
80    fn dispatch_to_hits(
81        &mut self,
82        hits: &[<<R as Renderer>::Scene as RenderScene>::HitTarget],
83        event: PointerEvent,
84    ) -> bool {
85        let capture_paths = hits
86            .iter()
87            .map(|hit| hit.capture_path())
88            .collect::<Vec<_>>();
89        let targets = crate::hit_path_tracker::dispatch_order_for_paths(&capture_paths)
90            .into_iter()
91            .filter_map(|node_id| self.surface().renderer.scene().find_target(node_id))
92            .collect::<Vec<_>>();
93
94        self.dispatch_targets(targets, event.clone(), true);
95
96        event.is_consumed()
97    }
98
99    /// Sets the device source (touch/mouse/stylus) of the pointer sample that
100    /// the platform is about to dispatch. Call this before `set_cursor` /
101    /// `pointer_pressed` / `pointer_released` so the resulting `PointerEvent`s
102    /// carry the source so consumers can preserve device-specific gesture
103    /// details without changing shared pointer UI.
104    pub fn set_pointer_source(&mut self, source: PointerSource) {
105        self.surface_mut().pointer_source = source;
106    }
107
108    /// The device source of the most recent pointer sample.
109    pub fn pointer_source(&self) -> PointerSource {
110        self.surface().pointer_source
111    }
112
113    pub fn set_cursor(&mut self, x: f32, y: f32) -> bool {
114        self.set_cursor_at_time(x, y, None)
115    }
116
117    /// Like [`set_cursor`](Self::set_cursor), but carries the platform input
118    /// timestamp (milliseconds, platform-specific time base) of the sample.
119    ///
120    /// Platforms that deliver input batched/frame-aligned (Android) must use
121    /// this so gesture velocity is computed from real event times instead of
122    /// delivery times.
123    pub fn set_cursor_at_time(&mut self, x: f32, y: f32, time_ms: Option<i64>) -> bool {
124        let event_time = self.shell.app.realtime_pointer_event_time(time_ms);
125        self.set_cursor_at_event_time(x, y, event_time)
126    }
127
128    /// Set the cursor using a timestamp already resolved into both clock domains.
129    pub fn set_cursor_at_event_time(
130        &mut self,
131        x: f32,
132        y: f32,
133        event_time: PointerEventTime,
134    ) -> bool {
135        let _event_handler = enter_event_handler_scope();
136        let app_context = Rc::clone(&self.shell.app.app_context);
137        let result = app_context.enter(|| {
138            run_in_mutable_snapshot(|| self.set_cursor_inner(x, y, event_time)).unwrap_or(false)
139        });
140        self.route_drag_and_drop();
141        if result {
142            self.mark_dirty();
143        }
144        log::trace!(
145            target: "cranpose::input",
146            "set_cursor ({x:.2},{y:.2}) time_ms={:?} animation_time_nanos={} -> {result}",
147            event_time.platform_time_ms,
148            event_time.animation_time_nanos,
149        );
150        result
151    }
152
153    fn set_cursor_inner(&mut self, x: f32, y: f32, event_time: PointerEventTime) -> bool {
154        self.surface_mut().cursor = (x, y);
155        if self.inspector_move(x, y) {
156            return true;
157        }
158
159        if self.surface().buttons_pressed != PointerButtons::NONE {
160            if self.surface().hit_path_tracker.has_path(PointerId::PRIMARY) {
161                let targets = self.resolve_gesture_targets(PointerId::PRIMARY);
162                if !targets.is_empty() {
163                    let event = self
164                        .pointer_event(
165                            PointerEventKind::Move,
166                            Point { x, y },
167                            Point { x, y },
168                            event_time,
169                        )
170                        .with_buttons(self.surface().buttons_pressed)
171                        .with_source(self.surface().pointer_source);
172                    self.dispatch_targets(targets, event, false);
173                    return true;
174                }
175
176                return false;
177            }
178
179            return false;
180        }
181
182        let hits = if self.inspector_blocks_pointer(x, y) {
183            Vec::new()
184        } else {
185            self.surface().renderer.scene().hit_test(x, y)
186        };
187        let new_ids: Vec<NodeId> = hits.iter().map(|h| h.node_id()).collect();
188
189        let pos = Point { x, y };
190        let previously_hovered = self.surface().hovered_nodes.clone();
191        for old_id in previously_hovered {
192            if !new_ids.contains(&old_id)
193                && let Some(target) = self.surface().renderer.scene().find_target(old_id)
194            {
195                let exit_event = self
196                    .pointer_event(PointerEventKind::Exit, pos, pos, event_time)
197                    .with_buttons(self.surface().buttons_pressed)
198                    .with_source(self.surface().pointer_source);
199                self.dispatch_targets(std::iter::once(target), exit_event, false);
200            }
201        }
202
203        for hit in &hits {
204            if !self.surface().hovered_nodes.contains(&hit.node_id()) {
205                let enter_event = self
206                    .pointer_event(PointerEventKind::Enter, pos, pos, event_time)
207                    .with_buttons(self.surface().buttons_pressed)
208                    .with_source(self.surface().pointer_source);
209                self.dispatch_targets(std::iter::once(hit.clone()), enter_event, false);
210            }
211        }
212
213        self.surface_mut().hovered_nodes = new_ids;
214        self.apply_hovered_pointer_icon(&hits);
215
216        if !hits.is_empty() {
217            let event = self
218                .pointer_event(PointerEventKind::Move, pos, pos, event_time)
219                .with_buttons(self.surface().buttons_pressed)
220                .with_source(self.surface().pointer_source);
221            self.dispatch_targets(hits, event, true);
222            true
223        } else {
224            false
225        }
226    }
227
228    /// Activates an application control at logical coordinates for a platform reader.
229    ///
230    /// Developer overlays never receive this synthetic press and release.
231    pub fn accessibility_activate_at(&mut self, x: f32, y: f32) -> bool {
232        let event_time = self.shell.app.realtime_pointer_event_time(None);
233        let _event_handler = enter_event_handler_scope();
234        let app_context = Rc::clone(&self.shell.app.app_context);
235        let result = app_context.enter(|| {
236            run_in_mutable_snapshot(|| {
237                self.surface_mut().cursor = (x, y);
238                let pressed = self.pointer_pressed_inner(event_time);
239                self.release_app_pointer(event_time) || pressed
240            })
241            .unwrap_or(false)
242        });
243        if result {
244            self.mark_dirty();
245        }
246        result
247    }
248
249    pub fn pointer_pressed(&mut self) -> bool {
250        self.pointer_pressed_at_time(None)
251    }
252
253    /// Like [`pointer_pressed`](Self::pointer_pressed), but carries the
254    /// platform input timestamp (milliseconds) of the press sample.
255    pub fn pointer_pressed_at_time(&mut self, time_ms: Option<i64>) -> bool {
256        let event_time = self.shell.app.realtime_pointer_event_time(time_ms);
257        self.pointer_pressed_at_event_time(event_time)
258    }
259
260    /// Dispatch primary-button down with an already resolved event timestamp.
261    pub fn pointer_pressed_at_event_time(&mut self, event_time: PointerEventTime) -> bool {
262        let (cursor_x, cursor_y) = self.surface().cursor;
263        if self.inspector_press(cursor_x, cursor_y) {
264            return true;
265        }
266        if self.dev_overlay_press(cursor_x, cursor_y) {
267            return true;
268        }
269        let _event_handler = enter_event_handler_scope();
270        let app_context = Rc::clone(&self.shell.app.app_context);
271        let result = app_context.enter(|| {
272            run_in_mutable_snapshot(|| self.pointer_pressed_inner(event_time)).unwrap_or(false)
273        });
274        if result {
275            self.mark_dirty();
276        }
277        log::trace!(
278            target: "cranpose::input",
279            "pointer_pressed time_ms={:?} animation_time_nanos={} -> {result}",
280            event_time.platform_time_ms,
281            event_time.animation_time_nanos,
282        );
283        result
284    }
285
286    fn pointer_pressed_inner(&mut self, event_time: PointerEventTime) -> bool {
287        self.activate();
288        self.note_focus_moved_by_keyboard(false);
289        self.surface_mut()
290            .buttons_pressed
291            .insert(PointerButton::Primary);
292
293        let (cursor_x, cursor_y) = self.surface().cursor;
294        let mut hits = self.surface().renderer.scene().hit_test(cursor_x, cursor_y);
295        if self.surface().pointer_source.is_touch_like()
296            && let Some(near) = self
297                .surface()
298                .renderer
299                .scene()
300                .hit_test_near(cursor_x, cursor_y)
301            && hits.iter().all(|hit| {
302                hit.node_id() != near.node_id() && near.capture_path().contains(&hit.node_id())
303            })
304        {
305            hits.insert(0, near);
306        }
307        if hits.is_empty() {
308            self.surface_mut()
309                .hit_path_tracker
310                .remove_path(PointerId::PRIMARY);
311            false
312        } else {
313            let event = self
314                .pointer_event(
315                    PointerEventKind::Down,
316                    Point {
317                        x: self.surface().cursor.0,
318                        y: self.surface().cursor.1,
319                    },
320                    Point {
321                        x: self.surface().cursor.0,
322                        y: self.surface().cursor.1,
323                    },
324                    event_time,
325                )
326                .with_buttons(self.surface().buttons_pressed)
327                .with_source(self.surface().pointer_source);
328
329            let mut delivered_capture_paths = Vec::new();
330            let (app, surface) = self.parts();
331            let mut applier = app.composition.applier_mut();
332            for hit in hits {
333                let node_id = hit.node_id();
334                delivered_capture_paths.push(hit.capture_path());
335                hit.dispatch_with_applier(&mut applier, event.clone());
336                log::trace!(
337                    target: "cranpose::input",
338                    "dispatch {:?} node={} consumed={} stop_on_consume=true",
339                    event.kind,
340                    node_id,
341                    event.is_consumed(),
342                );
343                if event.is_consumed() {
344                    break;
345                }
346            }
347
348            surface
349                .hit_path_tracker
350                .add_hit_path(PointerId::PRIMARY, delivered_capture_paths);
351            log::trace!(
352                target: "cranpose::input",
353                "pointer_pressed_inner cached_hit_path={:?}",
354                surface.hit_path_tracker.get_path(PointerId::PRIMARY),
355            );
356
357            true
358        }
359    }
360
361    pub fn pointer_released(&mut self) -> bool {
362        self.pointer_released_at_time(None)
363    }
364
365    /// Releases the pointer at the position carried by the platform's release
366    /// sample (Android `ACTION_UP`, web `pointerup`/`touchend`).
367    ///
368    /// The cursor is moved to `(x, y)` WITHOUT dispatching a Move event, then
369    /// the Up event is dispatched at that position. Platforms whose release
370    /// events carry their own coordinates must use this instead of
371    /// `set_cursor* + pointer_released*`: lift-off samples routinely roll back
372    /// a few dp against the travel direction as the finger peels off, and
373    /// feeding that jitter into gesture velocity trackers as a final Move
374    /// sample can flip the sign of the computed fling velocity (flings that
375    /// suddenly go the opposite way). Jetpack Compose likewise never feeds the
376    /// up sample into velocity tracking.
377    pub fn pointer_released_at_position(&mut self, x: f32, y: f32) -> bool {
378        self.pointer_released_at_position_time(x, y, None)
379    }
380
381    /// Like [`pointer_released_at_position`](Self::pointer_released_at_position),
382    /// but carries the platform input timestamp (milliseconds) of the release
383    /// sample.
384    pub fn pointer_released_at_position_time(
385        &mut self,
386        x: f32,
387        y: f32,
388        time_ms: Option<i64>,
389    ) -> bool {
390        let event_time = self.shell.app.realtime_pointer_event_time(time_ms);
391        self.pointer_released_at_position_event_time(x, y, event_time)
392    }
393
394    /// Release at a position with an already resolved event timestamp.
395    pub fn pointer_released_at_position_event_time(
396        &mut self,
397        x: f32,
398        y: f32,
399        event_time: PointerEventTime,
400    ) -> bool {
401        let _event_handler = enter_event_handler_scope();
402        let app_context = Rc::clone(&self.shell.app.app_context);
403        let result = app_context.enter(|| {
404            run_in_mutable_snapshot(|| {
405                self.surface_mut().cursor = (x, y);
406                self.pointer_released_inner(event_time)
407            })
408            .unwrap_or(false)
409        });
410        self.route_drag_and_drop();
411        if result {
412            self.mark_dirty();
413        }
414        log::trace!(
415            target: "cranpose::input",
416            "pointer_released_at_position ({x:.2},{y:.2}) time_ms={:?} animation_time_nanos={} -> {result}",
417            event_time.platform_time_ms,
418            event_time.animation_time_nanos,
419        );
420        result
421    }
422
423    /// Like [`pointer_released`](Self::pointer_released), but carries the
424    /// platform input timestamp (milliseconds) of the release sample.
425    pub fn pointer_released_at_time(&mut self, time_ms: Option<i64>) -> bool {
426        let event_time = self.shell.app.realtime_pointer_event_time(time_ms);
427        self.pointer_released_at_event_time(event_time)
428    }
429
430    /// Dispatch primary-button up with an already resolved event timestamp.
431    pub fn pointer_released_at_event_time(&mut self, event_time: PointerEventTime) -> bool {
432        let _event_handler = enter_event_handler_scope();
433        let app_context = Rc::clone(&self.shell.app.app_context);
434        let result = app_context.enter(|| {
435            run_in_mutable_snapshot(|| self.pointer_released_inner(event_time)).unwrap_or(false)
436        });
437        self.route_drag_and_drop();
438        if result {
439            self.mark_dirty();
440        }
441        log::trace!(
442            target: "cranpose::input",
443            "pointer_released time_ms={:?} animation_time_nanos={} -> {result}",
444            event_time.platform_time_ms,
445            event_time.animation_time_nanos,
446        );
447        result
448    }
449
450    fn pointer_released_inner(&mut self, event_time: PointerEventTime) -> bool {
451        if self.surface_mut().inspector.release_pointer() {
452            return true;
453        }
454        self.release_app_pointer(event_time)
455    }
456
457    fn release_app_pointer(&mut self, event_time: PointerEventTime) -> bool {
458        self.surface_mut()
459            .buttons_pressed
460            .remove(PointerButton::Primary);
461        let corrected_buttons = self.surface().buttons_pressed;
462        let targets = self.resolve_gesture_targets(PointerId::PRIMARY);
463
464        self.surface_mut()
465            .hit_path_tracker
466            .remove_path(PointerId::PRIMARY);
467
468        if !targets.is_empty() {
469            let event = self
470                .pointer_event(
471                    PointerEventKind::Up,
472                    Point {
473                        x: self.surface().cursor.0,
474                        y: self.surface().cursor.1,
475                    },
476                    Point {
477                        x: self.surface().cursor.0,
478                        y: self.surface().cursor.1,
479                    },
480                    event_time,
481                )
482                .with_buttons(corrected_buttons)
483                .with_source(self.surface().pointer_source);
484
485            self.dispatch_targets(targets, event, false);
486            true
487        } else {
488            false
489        }
490    }
491
492    /// Dispatches an event for a secondary pointer (`pointer_id != 0`).
493    ///
494    /// Multi-touch gestures act on the element the first finger grabbed, so
495    /// secondary pointers are routed to the hit path captured by the primary
496    /// pointer's Down. They carry no hover/click semantics and are ignored
497    /// when no primary gesture is in progress.
498    ///
499    /// Returns `true` when the event was dispatched to at least one target.
500    pub fn secondary_pointer_pressed(
501        &mut self,
502        pointer_id: u64,
503        x: f32,
504        y: f32,
505        time_ms: Option<i64>,
506    ) -> bool {
507        let event_time = self.shell.app.realtime_pointer_event_time(time_ms);
508        self.dispatch_secondary_pointer(PointerEventKind::Down, pointer_id, x, y, event_time)
509    }
510
511    /// Move counterpart of [`secondary_pointer_pressed`](Self::secondary_pointer_pressed).
512    pub fn secondary_pointer_moved(
513        &mut self,
514        pointer_id: u64,
515        x: f32,
516        y: f32,
517        time_ms: Option<i64>,
518    ) -> bool {
519        let event_time = self.shell.app.realtime_pointer_event_time(time_ms);
520        self.dispatch_secondary_pointer(PointerEventKind::Move, pointer_id, x, y, event_time)
521    }
522
523    /// Release counterpart of [`secondary_pointer_pressed`](Self::secondary_pointer_pressed).
524    pub fn secondary_pointer_released(
525        &mut self,
526        pointer_id: u64,
527        x: f32,
528        y: f32,
529        time_ms: Option<i64>,
530    ) -> bool {
531        let event_time = self.shell.app.realtime_pointer_event_time(time_ms);
532        self.dispatch_secondary_pointer(PointerEventKind::Up, pointer_id, x, y, event_time)
533    }
534
535    fn dispatch_secondary_pointer(
536        &mut self,
537        kind: PointerEventKind,
538        pointer_id: u64,
539        x: f32,
540        y: f32,
541        event_time: PointerEventTime,
542    ) -> bool {
543        if pointer_id == 0 {
544            log::warn!(
545                target: "cranpose::input",
546                "secondary pointer dispatch called with the primary pointer id"
547            );
548            return false;
549        }
550
551        let _event_handler = enter_event_handler_scope();
552        let app_context = Rc::clone(&self.shell.app.app_context);
553        let result = app_context.enter(|| {
554            run_in_mutable_snapshot(|| {
555                if !self.surface().hit_path_tracker.has_path(PointerId::PRIMARY) {
556                    return false;
557                }
558                let targets = self.resolve_gesture_targets(PointerId::PRIMARY);
559                if targets.is_empty() {
560                    return false;
561                }
562                let pos = Point { x, y };
563                let event = self
564                    .pointer_event(kind, pos, pos, event_time)
565                    .with_buttons(self.surface().buttons_pressed)
566                    .with_id(pointer_id)
567                    .with_source(self.surface().pointer_source);
568                self.dispatch_targets(targets, event, false);
569                true
570            })
571            .unwrap_or(false)
572        });
573        if result {
574            self.mark_dirty();
575        }
576        log::trace!(
577            target: "cranpose::input",
578            "secondary_pointer {kind:?} id={pointer_id} ({x:.2},{y:.2}) time_ms={:?} animation_time_nanos={} -> {result}",
579            event_time.platform_time_ms,
580            event_time.animation_time_nanos,
581        );
582        result
583    }
584
585    /// Dispatches a discrete zoom step (desktop ctrl+wheel, browser pinch)
586    /// to the pointer handlers under the cursor.
587    ///
588    /// `zoom_factor` is multiplicative: `> 1.0` zooms in, `< 1.0` zooms out.
589    /// Returns `true` if a handler consumed the event.
590    pub fn pointer_zoomed(&mut self, zoom_factor: f32) -> bool {
591        let event_time = self.shell.app.realtime_pointer_event_time(None);
592        let _event_handler = enter_event_handler_scope();
593        let app_context = Rc::clone(&self.shell.app.app_context);
594        let result = app_context.enter(|| {
595            run_in_mutable_snapshot(|| self.pointer_zoomed_inner(zoom_factor, event_time))
596                .unwrap_or(false)
597        });
598        if result {
599            self.mark_dirty();
600        }
601        log::trace!(
602            target: "cranpose::input",
603            "pointer_zoomed factor={zoom_factor:.4} -> {result}"
604        );
605        result
606    }
607
608    fn pointer_zoomed_inner(&mut self, zoom_factor: f32, event_time: PointerEventTime) -> bool {
609        if !zoom_factor.is_finite() || zoom_factor <= 0.0 || zoom_factor == 1.0 {
610            return false;
611        }
612
613        let hits = self
614            .surface()
615            .renderer
616            .scene()
617            .hit_test(self.surface().cursor.0, self.surface().cursor.1);
618        if hits.is_empty() {
619            return false;
620        }
621
622        let pos = Point {
623            x: self.surface().cursor.0,
624            y: self.surface().cursor.1,
625        };
626        let event = self
627            .pointer_event(PointerEventKind::Zoom, pos, pos, event_time)
628            .with_buttons(self.surface().buttons_pressed)
629            .with_zoom_delta(zoom_factor)
630            .with_source(self.surface().pointer_source);
631
632        self.dispatch_to_hits(&hits, event)
633    }
634
635    /// Dispatches one mouse-wheel / trackpad sample through the whole wheel
636    /// policy, and returns `true` when something consumed it.
637    ///
638    /// This is the single entry point every host with a wheel calls, after
639    /// placing the cursor. A wheel sample is not just a scroll — it is whichever
640    /// of four things the modifiers and the tree make it, in this order:
641    ///
642    /// 1. **Zoom** when ctrl is held. That is the desktop convention and the
643    ///    way browsers deliver a trackpad pinch, so both arrive here as the
644    ///    same gesture.
645    /// 2. **Rotary**, offered to [`rotary_scrolled`](Self::rotary_scrolled)
646    ///    before anything else can take it, so the Wear OS crown stack is
647    ///    developable on a machine with a wheel. Nothing consumes rotary unless
648    ///    the app opts in via `Modifier::on_rotary_scroll_event` or
649    ///    [`set_on_rotary_scroll`](Self::set_on_rotary_scroll), so ordinary
650    ///    scrolling is unaffected.
651    /// 3. **Horizontal scroll** when alt is held on a wheel that only reports a
652    ///    vertical axis.
653    /// 4. **Scroll**, to the hovered scrollable.
654    ///
655    /// Hosts must not re-implement this order. Doing so is how the browser
656    /// ended up scrolling backwards and never delivering rotary at all: the
657    /// policy lived in the desktop event loop, and the second host that grew a
658    /// wheel reimplemented the parts of it that were obvious from the outside.
659    pub fn wheel_scrolled(&mut self, wheel: crate::WheelScroll) -> bool {
660        if self.inspector_scroll(wheel.delta.y) {
661            return true;
662        }
663        if wheel.is_zoom() {
664            let zoom_factor = wheel.zoom_factor();
665            log::trace!(
666                target: "cranpose::input",
667                "wheel zoom factor={zoom_factor:.4}"
668            );
669            return self.pointer_zoomed(zoom_factor);
670        }
671
672        let rotary =
673            RotaryScrollEvent::from_wheel_pixels(wheel.delta.y, wheel.delta.x, wheel.uptime_millis);
674        if self.rotary_scrolled(rotary) {
675            return true;
676        }
677
678        let delta = wheel.scroll_delta();
679        log::trace!(
680            target: "cranpose::input",
681            "wheel delta ({:.2},{:.2}) alt={}",
682            delta.x,
683            delta.y,
684            wheel.modifiers.alt
685        );
686        self.pointer_scrolled(delta.x, delta.y)
687    }
688
689    /// Dispatches a mouse wheel / trackpad scroll event to hovered pointer handlers.
690    ///
691    /// Returns `true` if a handler consumed the event.
692    ///
693    /// This is the last step of the wheel policy, not its entry point: hosts
694    /// call [`wheel_scrolled`](Self::wheel_scrolled), which reaches here once
695    /// zoom and rotary have declined the sample.
696    pub fn pointer_scrolled(&mut self, delta_x: f32, delta_y: f32) -> bool {
697        if self.inspector_scroll(delta_y) {
698            return true;
699        }
700        let event_time = self.shell.app.realtime_pointer_event_time(None);
701        let _event_handler = enter_event_handler_scope();
702        let app_context = Rc::clone(&self.shell.app.app_context);
703        let result = app_context.enter(|| {
704            run_in_mutable_snapshot(|| self.pointer_scrolled_inner(delta_x, delta_y, event_time))
705                .unwrap_or(false)
706        });
707        if result {
708            self.mark_dirty();
709        }
710        log::trace!(
711            target: "cranpose::input",
712            "pointer_scrolled ({delta_x:.2},{delta_y:.2}) -> {result}"
713        );
714        result
715    }
716
717    fn pointer_scrolled_inner(
718        &mut self,
719        delta_x: f32,
720        delta_y: f32,
721        event_time: PointerEventTime,
722    ) -> bool {
723        if delta_x.abs() <= f32::EPSILON && delta_y.abs() <= f32::EPSILON {
724            return false;
725        }
726
727        let hits = self
728            .surface()
729            .renderer
730            .scene()
731            .hit_test(self.surface().cursor.0, self.surface().cursor.1);
732        if hits.is_empty() {
733            return false;
734        }
735
736        let event = self
737            .pointer_event(
738                PointerEventKind::Scroll,
739                Point {
740                    x: self.surface().cursor.0,
741                    y: self.surface().cursor.1,
742                },
743                Point {
744                    x: self.surface().cursor.0,
745                    y: self.surface().cursor.1,
746                },
747                event_time,
748            )
749            .with_buttons(self.surface().buttons_pressed)
750            .with_scroll_delta(Point {
751                x: delta_x,
752                y: delta_y,
753            })
754            .with_source(self.surface().pointer_source);
755
756        self.dispatch_to_hits(&hits, event)
757    }
758
759    /// Installs the window-level rotary (Wear OS crown / rotating bezel)
760    /// handler — the low-level escape hatch.
761    ///
762    /// The handler runs only after the routed modifier chain has declined the
763    /// event (see [`rotary_scrolled`](Self::rotary_scrolled)), so an app that
764    /// draws everything into a single canvas receives every rotary delta
765    /// without registering a focus target or a modifier. Returning `true`
766    /// reports the event as consumed to the platform.
767    ///
768    /// Passing a new handler replaces the previous one.
769    pub fn set_on_rotary_scroll<F>(&mut self, handler: F)
770    where
771        F: Fn(RotaryScrollEvent) -> bool + 'static,
772    {
773        self.surface_mut().on_rotary_scroll = Some(Rc::new(handler));
774    }
775
776    /// Removes the window-level rotary handler, if one is installed.
777    pub fn clear_on_rotary_scroll(&mut self) {
778        self.surface_mut().on_rotary_scroll = None;
779    }
780
781    /// Dispatches a rotary scroll expressed in raw detents (Android
782    /// `AXIS_SCROLL`), converting to pixels with the configured scroll factor.
783    ///
784    /// Applies Compose's sign convention: a positive detent value (crown turned
785    /// up/away) produces a negative `vertical_scroll_pixels`.
786    pub fn rotary_scrolled_by_detents(&mut self, detents: f32, uptime_millis: u64) -> bool {
787        let factor = self.shell.app.rotary_scroll_factor;
788        self.rotary_scrolled(RotaryScrollEvent::from_detents(
789            detents,
790            factor,
791            factor,
792            uptime_millis,
793        ))
794    }
795
796    /// Dispatches a rotary scroll event (Wear OS crown, Galaxy Watch bezel, or
797    /// a desktop mouse wheel standing in for one during development).
798    ///
799    /// Routing mirrors Compose's `RotaryInputModifierNode` contract:
800    ///
801    /// 1. Resolve the target chain. When a focus target is registered
802    ///    (`cranpose_ui::focus_dispatch::active_focus_target`) and still
803    ///    exists in the current scene, its capture path is used, so rotary goes
804    ///    to the focused node exactly as on Wear OS. Cranpose does not yet wire
805    ///    focus automatically, so in practice this falls back to the chain
806    ///    under the current cursor position.
807    /// 2. **Capture pass**, root to leaf, invoking `on_pre_rotary_scroll_event`
808    ///    handlers.
809    /// 3. **Bubble pass**, leaf to root, invoking `on_rotary_scroll_event`
810    ///    handlers.
811    /// 4. If still unconsumed, the window-level handler installed by
812    ///    [`set_on_rotary_scroll`](Self::set_on_rotary_scroll).
813    ///
814    /// The first handler returning `true` consumes the event and stops every
815    /// remaining step. Returns `true` when the event was consumed.
816    pub fn rotary_scrolled(&mut self, event: RotaryScrollEvent) -> bool {
817        let _event_handler = enter_event_handler_scope();
818        let app_context = Rc::clone(&self.shell.app.app_context);
819        let result = app_context.enter(|| {
820            run_in_mutable_snapshot(|| self.rotary_scrolled_inner(event)).unwrap_or(false)
821        });
822        if result {
823            self.mark_dirty();
824        }
825        log::trace!(
826            target: "cranpose::input",
827            "rotary_scrolled v={:.2} h={:.2} uptime={} -> {result}",
828            event.vertical_scroll_pixels,
829            event.horizontal_scroll_pixels,
830            event.uptime_millis,
831        );
832        result
833    }
834
835    fn rotary_scrolled_inner(&mut self, rotary: RotaryScrollEvent) -> bool {
836        if rotary.is_empty() {
837            return false;
838        }
839
840        let bubble_order = self.rotary_dispatch_order();
841        let position = Point {
842            x: self.surface().cursor.0,
843            y: self.surface().cursor.1,
844        };
845
846        if !bubble_order.is_empty() {
847            let capture_targets = bubble_order
848                .iter()
849                .rev()
850                .filter_map(|&node_id| self.surface().renderer.scene().find_target(node_id))
851                .collect::<Vec<_>>();
852            let mut capture_event =
853                PointerEvent::rotary(PointerEventKind::RotaryScrollPre, rotary, position);
854            capture_event.modifiers = self.shell.app.modifiers;
855            self.dispatch_targets(capture_targets, capture_event.clone(), true);
856            if capture_event.is_consumed() {
857                return true;
858            }
859
860            let bubble_targets = bubble_order
861                .iter()
862                .filter_map(|&node_id| self.surface().renderer.scene().find_target(node_id))
863                .collect::<Vec<_>>();
864            let mut bubble_event =
865                PointerEvent::rotary(PointerEventKind::RotaryScroll, rotary, position);
866            bubble_event.modifiers = self.shell.app.modifiers;
867            self.dispatch_targets(bubble_targets, bubble_event.clone(), true);
868            if bubble_event.is_consumed() {
869                return true;
870            }
871        }
872
873        if let Some(handler) = self.surface().on_rotary_scroll.clone() {
874            return handler(rotary);
875        }
876
877        false
878    }
879
880    fn rotary_dispatch_order(&self) -> Vec<NodeId> {
881        if let Some(focused) = cranpose_ui::active_focus_target()
882            && let Some(target) = self.surface().renderer.scene().find_target(focused)
883        {
884            let path = target.capture_path();
885            if !path.is_empty() {
886                return crate::hit_path_tracker::dispatch_order_for_paths(&[path]);
887            }
888        }
889
890        let hits = self
891            .surface()
892            .renderer
893            .scene()
894            .hit_test(self.surface().cursor.0, self.surface().cursor.1);
895        if hits.is_empty() {
896            return Vec::new();
897        }
898        let capture_paths = hits
899            .iter()
900            .map(|hit| hit.capture_path())
901            .collect::<Vec<_>>();
902        crate::hit_path_tracker::dispatch_order_for_paths(&capture_paths)
903    }
904
905    /// Cancels any active gesture, dispatching Cancel events to cached targets.
906    /// Call this when:
907    /// - Window loses focus
908    /// - Mouse leaves window while button pressed
909    /// - Any other gesture abort scenario
910    pub fn cancel_gesture(&mut self) {
911        let event_time = self.shell.app.realtime_pointer_event_time(None);
912        let _event_handler = enter_event_handler_scope();
913        let app_context = Rc::clone(&self.shell.app.app_context);
914        let _ = app_context.enter(|| {
915            run_in_mutable_snapshot(|| {
916                self.cancel_gesture_inner(event_time);
917            })
918        });
919        self.route_drag_and_drop();
920    }
921
922    fn route_drag_and_drop(&mut self) {
923        let app_context = Rc::clone(&self.shell.app.app_context);
924        let source = self.index;
925        let shell = &mut *self.shell;
926        let routed = app_context.enter(|| {
927            run_in_mutable_snapshot(|| {
928                app_context
929                    .drag_and_drop()
930                    .route(|point| shell.drag_and_drop_target_at(point, source))
931            })
932            .unwrap_or(false)
933        });
934        if routed {
935            shell.mark_all_dirty();
936        }
937    }
938
939    /// Ends the gesture only when the pointer leaving really ended it.
940    ///
941    /// A held button belongs to the surface that received the press until
942    /// the release: a window drawn over the cursor, or a drag carried past
943    /// an edge, both leave the press where it started and deliver the
944    /// release there too. Cancelling on either would drop a gesture the
945    /// user has not finished, so a pressed pointer is left alone and only
946    /// an idle one cancels.
947    pub fn cancel_gesture_unless_pressed(&mut self) {
948        if self.surface().buttons_pressed != PointerButtons::NONE
949            || self.surface().inspector.pointer_captured
950        {
951            return;
952        }
953        self.cancel_gesture();
954    }
955
956    fn cancel_gesture_inner(&mut self, event_time: PointerEventTime) {
957        self.surface_mut().inspector.cancel_pointer();
958        let targets = self.resolve_gesture_targets(PointerId::PRIMARY);
959
960        self.surface_mut().hit_path_tracker.clear();
961        self.surface_mut().buttons_pressed = PointerButtons::NONE;
962
963        if !targets.is_empty() {
964            let event = self
965                .pointer_event(
966                    PointerEventKind::Cancel,
967                    Point {
968                        x: self.surface().cursor.0,
969                        y: self.surface().cursor.1,
970                    },
971                    Point {
972                        x: self.surface().cursor.0,
973                        y: self.surface().cursor.1,
974                    },
975                    event_time,
976                )
977                .with_source(self.surface().pointer_source);
978
979            self.dispatch_targets(targets, event, false);
980        }
981
982        let pos = Point {
983            x: self.surface().cursor.0,
984            y: self.surface().cursor.1,
985        };
986        let hovered_nodes = self.surface().hovered_nodes.clone();
987        for node_id in hovered_nodes {
988            if let Some(target) = self.surface().renderer.scene().find_target(node_id) {
989                let exit_event = self
990                    .pointer_event(PointerEventKind::Exit, pos, pos, event_time)
991                    .with_source(self.surface().pointer_source);
992                self.dispatch_targets(std::iter::once(target), exit_event, false);
993            }
994        }
995        self.surface_mut().hovered_nodes.clear();
996        self.surface().pointer_icon.set(PointerIcon::DEFAULT);
997    }
998
999    fn apply_hovered_pointer_icon(
1000        &self,
1001        hits: &[<<R as Renderer>::Scene as RenderScene>::HitTarget],
1002    ) {
1003        let icon = hits
1004            .iter()
1005            .find_map(HitTestTarget::pointer_icon)
1006            .unwrap_or(PointerIcon::DEFAULT);
1007        self.surface().pointer_icon.set(icon);
1008    }
1009
1010    fn on_focus_key(&mut self, event: &KeyEvent) -> bool {
1011        if !plain_key_down(event) || event.key_code != KeyCode::Tab {
1012            return false;
1013        }
1014        let direction = if event.modifiers.shift {
1015            FocusDirection::Previous
1016        } else {
1017            FocusDirection::Next
1018        };
1019        self.move_focus_in_context(direction)
1020    }
1021
1022    fn on_activation_key(&mut self, event: &KeyEvent) -> bool {
1023        if !plain_key_down(event)
1024            || !matches!(event.key_code, KeyCode::Enter | KeyCode::Space)
1025            || cranpose_ui::text_field_focus::has_focused_field()
1026        {
1027            return false;
1028        }
1029        let Some(focused) = cranpose_ui::active_focus_target() else {
1030            return false;
1031        };
1032        let center = self.with_layout_tree(|layout_tree| {
1033            layout_tree
1034                .map(cranpose_ui::collect_focus_order)
1035                .unwrap_or_default()
1036                .iter()
1037                .find(|entry| entry.node_id == focused)
1038                .map(cranpose_ui::FocusEntry::center)
1039        });
1040        let Some((x, y)) = center else {
1041            return false;
1042        };
1043        self.set_cursor(x, y);
1044        let pressed = self.pointer_pressed();
1045        let released = self.pointer_released_at_position(x, y);
1046        self.note_focus_moved_by_keyboard(true);
1047        pressed || released
1048    }
1049
1050    fn on_arrow_key(&mut self, event: &KeyEvent) -> bool {
1051        if !plain_key_down(event) || cranpose_ui::text_field_focus::has_focused_field() {
1052            return false;
1053        }
1054        let direction = match event.key_code {
1055            KeyCode::ArrowLeft => FocusDirection::Left,
1056            KeyCode::ArrowRight => FocusDirection::Right,
1057            KeyCode::ArrowUp => FocusDirection::Up,
1058            KeyCode::ArrowDown => FocusDirection::Down,
1059            _ => return false,
1060        };
1061        let Some(focused) = cranpose_ui::active_focus_target() else {
1062            return false;
1063        };
1064        let order = self.with_layout_tree(|layout_tree| {
1065            let layout_tree = layout_tree?;
1066            let group = cranpose_ui::selectable_group_of(layout_tree, focused)?;
1067            Some(cranpose_ui::collect_focus_order_under(layout_tree, group))
1068        });
1069        order.is_some_and(|order| self.move_focus_in_order(order, direction))
1070    }
1071
1072    fn note_focus_moved_by_keyboard(&mut self, keyboard: bool) {
1073        if cranpose_ui::set_keyboard_focus_visible(keyboard) {
1074            cranpose_ui::request_render_invalidation();
1075            self.mark_dirty();
1076        }
1077    }
1078
1079    fn on_escape_key(&mut self, event: &KeyEvent) -> bool {
1080        if event.event_type != KeyEventType::KeyDown || event.key_code != KeyCode::Escape {
1081            return false;
1082        }
1083        self.dismiss_top_modal_in_context()
1084    }
1085
1086    /// Asks the innermost open dialog or popup to close, the way the platform
1087    /// back gesture does. Answers whether one was open to take the request.
1088    pub fn dismiss_top_modal(&mut self) -> bool {
1089        let _event_handler = enter_event_handler_scope();
1090        let app_context = Rc::clone(&self.shell.app.app_context);
1091        app_context.enter(|| self.dismiss_top_modal_in_context())
1092    }
1093
1094    fn dismiss_top_modal_in_context(&mut self) -> bool {
1095        let closed = run_in_mutable_snapshot(|| {
1096            if cranpose_ui::modal_depth() > 0 {
1097                cranpose_ui::dispatch_modal_back()
1098            } else {
1099                cranpose_ui::dismiss_top_popup()
1100            }
1101        })
1102        .unwrap_or(false);
1103        if closed {
1104            self.mark_dirty();
1105        }
1106        closed
1107    }
1108
1109    /// Publishes the focus order layout left behind and moves focus one step.
1110    /// Answers whether focus moved.
1111    pub fn move_focus_in_context(&mut self, direction: FocusDirection) -> bool {
1112        let order = self.with_layout_tree(|layout_tree| {
1113            layout_tree
1114                .map(cranpose_ui::collect_focus_order)
1115                .unwrap_or_default()
1116        });
1117        self.move_focus_in_order(order, direction)
1118    }
1119
1120    fn move_focus_in_order(
1121        &mut self,
1122        order: Vec<cranpose_ui::FocusEntry>,
1123        direction: FocusDirection,
1124    ) -> bool {
1125        cranpose_ui::set_focus_order(order);
1126        let moved = run_in_mutable_snapshot(|| cranpose_ui::FocusManager.move_focus(direction))
1127            .unwrap_or(false);
1128        if moved {
1129            self.mark_dirty();
1130            self.note_focus_moved_by_keyboard(true);
1131        }
1132        moved
1133    }
1134
1135    pub fn on_key_event(&mut self, event: &KeyEvent) -> bool {
1136        let _event_handler = enter_event_handler_scope();
1137        let app_context = Rc::clone(&self.shell.app.app_context);
1138        app_context.enter(|| self.on_key_event_inner(event))
1139    }
1140
1141    fn on_key_event_inner(&mut self, event: &KeyEvent) -> bool {
1142        if self.inspector_key(event) {
1143            return true;
1144        }
1145        use KeyEventType::KeyDown;
1146
1147        if event.event_type == KeyDown && event.modifiers.command_or_ctrl() {
1148            #[cfg(all(
1149                feature = "clipboard-native",
1150                not(target_arch = "wasm32"),
1151                not(target_os = "android"),
1152                not(target_os = "ios")
1153            ))]
1154            {
1155                match event.key_code {
1156                    KeyCode::C => {
1157                        if let Some(text) = self.on_copy_inner() {
1158                            cranpose_ui::clipboard_session::clipboard_write_text(&text);
1159                            return true;
1160                        }
1161                    }
1162                    KeyCode::X => {
1163                        if let Some(text) = self.on_cut_inner() {
1164                            cranpose_ui::clipboard_session::clipboard_write_text(&text);
1165                            self.mark_dirty();
1166                            self.shell.app.request_layout_pass();
1167                            return true;
1168                        }
1169                    }
1170                    KeyCode::V => {
1171                        if let Some(text) = cranpose_ui::clipboard_session::clipboard_read_text()
1172                            && self.on_paste_inner(&text)
1173                        {
1174                            return true;
1175                        }
1176                    }
1177                    _ => {}
1178                }
1179            }
1180        }
1181
1182        if self.on_focus_key(event) {
1183            return true;
1184        }
1185
1186        if self.on_escape_key(event) {
1187            return true;
1188        }
1189
1190        if self.on_activation_key(event) || self.on_arrow_key(event) {
1191            return true;
1192        }
1193
1194        if !cranpose_ui::text_field_focus::has_focused_field() {
1195            return false;
1196        }
1197
1198        let handled =
1199            run_in_mutable_snapshot(|| cranpose_ui::text_field_focus::dispatch_key_event(event))
1200                .unwrap_or(false);
1201
1202        if handled {
1203            self.mark_dirty();
1204            self.shell.app.request_layout_pass();
1205        }
1206
1207        handled
1208    }
1209
1210    /// Handles paste event from platform clipboard.
1211    /// Returns `true` if the paste was consumed by a focused text field.
1212    /// O(1) operation using stored handler.
1213    pub fn on_paste(&mut self, text: &str) -> bool {
1214        if self.inspector_owns_keyboard() {
1215            return true;
1216        }
1217        let _event_handler = enter_event_handler_scope();
1218        let app_context = Rc::clone(&self.shell.app.app_context);
1219        app_context.enter(|| self.on_paste_inner(text))
1220    }
1221
1222    fn on_paste_inner(&mut self, text: &str) -> bool {
1223        let handled =
1224            run_in_mutable_snapshot(|| cranpose_ui::text_field_focus::dispatch_paste(text))
1225                .unwrap_or(false);
1226
1227        if handled {
1228            self.mark_dirty();
1229            self.shell.app.request_layout_pass();
1230        }
1231
1232        handled
1233    }
1234
1235    /// Handles copy request from platform.
1236    /// Returns the selected text from focused text field, or None.
1237    /// O(1) operation using stored handler.
1238    pub fn on_copy(&mut self) -> Option<String> {
1239        let app_context = Rc::clone(&self.shell.app.app_context);
1240        if self.inspector_owns_keyboard() {
1241            return None;
1242        }
1243        app_context.enter(|| self.on_copy_inner())
1244    }
1245
1246    fn on_copy_inner(&mut self) -> Option<String> {
1247        cranpose_ui::text_field_focus::dispatch_copy()
1248    }
1249
1250    /// Handles cut request from platform.
1251    /// Returns the cut text from focused text field, or None.
1252    /// O(1) operation using stored handler.
1253    pub fn on_cut(&mut self) -> Option<String> {
1254        let _event_handler = enter_event_handler_scope();
1255        if self.inspector_owns_keyboard() {
1256            return None;
1257        }
1258        let app_context = Rc::clone(&self.shell.app.app_context);
1259        app_context.enter(|| self.on_cut_inner())
1260    }
1261
1262    fn on_cut_inner(&mut self) -> Option<String> {
1263        let text =
1264            run_in_mutable_snapshot(cranpose_ui::text_field_focus::dispatch_cut).unwrap_or(None);
1265
1266        if text.is_some() {
1267            self.mark_dirty();
1268            self.shell.app.request_layout_pass();
1269        }
1270
1271        text
1272    }
1273
1274    /// Handles IME preedit (composition) events.
1275    /// Called when the input method is composing text (e.g., typing CJK characters).
1276    ///
1277    /// - `text`: The current preedit text (empty to clear composition state)
1278    /// - `cursor`: Optional cursor position within the preedit text (start, end)
1279    ///
1280    /// Returns `true` if a text field consumed the event.
1281    pub fn on_ime_preedit(&mut self, text: &str, cursor: Option<(usize, usize)>) -> bool {
1282        if self.inspector_owns_keyboard() {
1283            return true;
1284        }
1285        let _event_handler = enter_event_handler_scope();
1286        let app_context = Rc::clone(&self.shell.app.app_context);
1287        app_context.enter(|| self.on_ime_preedit_inner(text, cursor))
1288    }
1289
1290    fn on_ime_preedit_inner(&mut self, text: &str, cursor: Option<(usize, usize)>) -> bool {
1291        let handled = run_in_mutable_snapshot(|| {
1292            cranpose_ui::text_field_focus::dispatch_ime_preedit(text, cursor)
1293        })
1294        .unwrap_or(false);
1295
1296        if handled {
1297            self.mark_dirty();
1298            self.shell.app.request_layout_pass();
1299        }
1300
1301        handled
1302    }
1303
1304    /// Finishes the active IME composition, keeping the composed text as
1305    /// committed text (Android `finishComposingText` semantics).
1306    /// Returns `true` if a text field consumed the event.
1307    pub fn on_ime_finish_composing(&mut self) -> bool {
1308        if self.inspector_owns_keyboard() {
1309            return true;
1310        }
1311        let _event_handler = enter_event_handler_scope();
1312        let app_context = Rc::clone(&self.shell.app.app_context);
1313        app_context.enter(|| self.on_ime_finish_composing_inner())
1314    }
1315
1316    fn on_ime_finish_composing_inner(&mut self) -> bool {
1317        let handled =
1318            run_in_mutable_snapshot(cranpose_ui::text_field_focus::dispatch_ime_finish_composing)
1319                .unwrap_or(false);
1320
1321        if handled {
1322            self.mark_dirty();
1323            self.shell.app.request_layout_pass();
1324        }
1325
1326        handled
1327    }
1328
1329    /// Marks existing text in the focused field as the composing region
1330    /// without changing it (Android `setComposingRegion` semantics). Offsets
1331    /// are UTF-8 bytes. Returns `true` if a text field consumed the event.
1332    pub fn on_ime_set_composing_region(&mut self, start_bytes: usize, end_bytes: usize) -> bool {
1333        if self.inspector_owns_keyboard() {
1334            return true;
1335        }
1336        let _event_handler = enter_event_handler_scope();
1337        let app_context = Rc::clone(&self.shell.app.app_context);
1338        app_context.enter(|| {
1339            let handled = run_in_mutable_snapshot(|| {
1340                cranpose_ui::text_field_focus::dispatch_ime_set_composing_region(
1341                    start_bytes,
1342                    end_bytes,
1343                )
1344            })
1345            .unwrap_or(false);
1346
1347            if handled {
1348                self.mark_dirty();
1349                self.shell.app.request_layout_pass();
1350            }
1351
1352            handled
1353        })
1354    }
1355
1356    /// Moves the focused field's selection/caret to `[start_bytes, end_bytes)`
1357    /// without editing text (Android `InputConnection.setSelection`; the path
1358    /// Gboard's spacebar-swipe uses to scrub the cursor). Offsets are UTF-8
1359    /// bytes. Returns `true` if a text field consumed the event.
1360    pub fn on_ime_set_selection(&mut self, start_bytes: usize, end_bytes: usize) -> bool {
1361        if self.inspector_owns_keyboard() {
1362            return true;
1363        }
1364        let _event_handler = enter_event_handler_scope();
1365        let app_context = Rc::clone(&self.shell.app.app_context);
1366        app_context.enter(|| {
1367            let handled = run_in_mutable_snapshot(|| {
1368                cranpose_ui::text_field_focus::dispatch_ime_set_selection(start_bytes, end_bytes)
1369            })
1370            .unwrap_or(false);
1371
1372            if handled {
1373                self.mark_dirty();
1374            }
1375
1376            handled
1377        })
1378    }
1379
1380    /// Returns a snapshot of the focused text field's editable state for
1381    /// platform IMEs (text, selection and composition in UTF-8 bytes), or
1382    /// `None` when no text field is focused.
1383    pub fn ime_editor_state(&mut self) -> Option<cranpose_ui::text_field_focus::ImeEditorState> {
1384        let app_context = Rc::clone(&self.shell.app.app_context);
1385        app_context.enter(cranpose_ui::text_field_focus::focused_editor_state)
1386    }
1387
1388    /// Window-space caret geometry of the focused field for coordinate-based
1389    /// platform text input (iOS trackpad cursor + tap-to-position), or `None`
1390    /// when no text field is focused.
1391    pub fn ime_caret_geometry(
1392        &mut self,
1393    ) -> Option<cranpose_ui::text_field_focus::ImeCaretGeometry> {
1394        let app_context = Rc::clone(&self.shell.app.app_context);
1395        app_context.enter(cranpose_ui::text_field_focus::focused_caret_geometry)
1396    }
1397
1398    /// Clears text-field focus (used by platform IME actions such as
1399    /// Android's Done). The focus-loss notification hides the soft keyboard.
1400    pub fn clear_text_field_focus(&mut self) {
1401        let _event_handler = enter_event_handler_scope();
1402        let app_context = Rc::clone(&self.shell.app.app_context);
1403        app_context.enter(cranpose_ui::text_field_focus::clear_focus);
1404        self.mark_dirty();
1405        self.shell.app.request_layout_pass();
1406    }
1407
1408    /// Handles IME delete-surrounding events.
1409    /// Returns `true` if a text field consumed the event.
1410    pub fn on_ime_delete_surrounding(&mut self, before_bytes: usize, after_bytes: usize) -> bool {
1411        if self.inspector_owns_keyboard() {
1412            return true;
1413        }
1414        let _event_handler = enter_event_handler_scope();
1415        let app_context = Rc::clone(&self.shell.app.app_context);
1416        app_context.enter(|| self.on_ime_delete_surrounding_inner(before_bytes, after_bytes))
1417    }
1418
1419    fn on_ime_delete_surrounding_inner(&mut self, before_bytes: usize, after_bytes: usize) -> bool {
1420        let handled = run_in_mutable_snapshot(|| {
1421            cranpose_ui::text_field_focus::dispatch_delete_surrounding(before_bytes, after_bytes)
1422        })
1423        .unwrap_or(false);
1424
1425        if handled {
1426            self.mark_dirty();
1427            self.shell.app.request_layout_pass();
1428        }
1429
1430        handled
1431    }
1432}
1433
1434impl<R> AppShell<R>
1435where
1436    R: Renderer,
1437    R::Error: Debug,
1438{
1439    /// Sets the keyboard modifiers held right now, so the platform's live
1440    /// modifier state (winit's `ModifiersChanged`, a DOM event's
1441    /// `shiftKey`/`ctrlKey`/`altKey`/`metaKey`) reaches every `PointerEvent`
1442    /// the shell dispatches from here on -- the same state the wheel path
1443    /// already carries via [`WheelScroll::with_modifiers`](crate::WheelScroll::with_modifiers).
1444    /// A platform that never calls this leaves pointer events reporting
1445    /// `None` (see [`PointerEvent::modifiers`]) rather than a silently wrong
1446    /// "nothing held".
1447    pub fn set_modifiers(&mut self, modifiers: Modifiers) {
1448        self.app.modifiers = Some(modifiers);
1449    }
1450
1451    /// The keyboard modifiers most recently set via
1452    /// [`set_modifiers`](Self::set_modifiers), or `None` if the platform has
1453    /// never reported them.
1454    pub fn modifiers(&self) -> Option<Modifiers> {
1455        self.app.modifiers
1456    }
1457
1458    /// Pixels per rotary detent used by
1459    /// [`rotary_scrolled_by_detents`](Self::rotary_scrolled_by_detents).
1460    pub fn rotary_scroll_factor(&self) -> f32 {
1461        self.app.rotary_scroll_factor
1462    }
1463
1464    /// Sets the pixels-per-detent factor for rotary input.
1465    ///
1466    /// On Wear OS this must be `ViewConfiguration.getScaledVerticalScrollFactor()`
1467    /// for pixel-exact parity with Compose. The host activity can read it over
1468    /// JNI once at startup and push it here; when it does not, the shell falls
1469    /// back to [`DEFAULT_ROTARY_SCROLL_FACTOR_DP`] scaled by display density.
1470    ///
1471    /// Non-finite or non-positive values are ignored.
1472    pub fn set_rotary_scroll_factor(&mut self, factor: f32) {
1473        if factor.is_finite() && factor > 0.0 {
1474            self.app.rotary_scroll_factor = factor;
1475        }
1476    }
1477
1478    /// Notifies the framework that the host app was paused/backgrounded.
1479    ///
1480    /// Withdraws any outstanding soft-keyboard request (and hides the keyboard)
1481    /// so the "keyboard shown" state does not survive across the pause and get
1482    /// restored on resume with no focused field. Platform runtimes call this
1483    /// from their pause lifecycle event.
1484    pub fn notify_app_paused(&mut self) {
1485        let app_context = Rc::clone(&self.app.app_context);
1486        app_context.enter(cranpose_ui::text_input_session::notify_app_paused);
1487    }
1488
1489    /// Notifies the framework that the host app resumed/foregrounded.
1490    ///
1491    /// Never auto-shows the soft keyboard, even for a still-focused field: a
1492    /// warm resume keeps the caret but must not resurrect the keyboard (the user
1493    /// taps the field to bring it back). Always returns `false` so the platform
1494    /// runtime force-hides the OS-restored keyboard. Platform runtimes call this
1495    /// from their resume lifecycle event.
1496    pub fn notify_app_resumed(&mut self) -> bool {
1497        let app_context = Rc::clone(&self.app.app_context);
1498        app_context.enter(cranpose_ui::text_input_session::notify_app_resumed)
1499    }
1500
1501    #[cfg(all(
1502        feature = "clipboard-native",
1503        target_os = "linux",
1504        not(target_arch = "wasm32")
1505    ))]
1506    pub fn set_primary_selection(&mut self, text: &str) {
1507        use arboard::{LinuxClipboardKind, SetExtLinux};
1508        if let Some(ref mut clipboard) = self.app.clipboard {
1509            let result = clipboard
1510                .set()
1511                .clipboard(LinuxClipboardKind::Primary)
1512                .text(text.to_string());
1513            if let Err(e) = result {
1514                log::debug!("Primary selection set failed: {:?}", e);
1515            }
1516        }
1517    }
1518
1519    #[cfg(not(all(
1520        feature = "clipboard-native",
1521        target_os = "linux",
1522        not(target_arch = "wasm32")
1523    )))]
1524    pub fn set_primary_selection(&mut self, _text: &str) {}
1525
1526    #[cfg(all(
1527        feature = "clipboard-native",
1528        target_os = "linux",
1529        not(target_arch = "wasm32")
1530    ))]
1531    pub fn get_primary_selection(&mut self) -> Option<String> {
1532        use arboard::{GetExtLinux, LinuxClipboardKind};
1533        if let Some(ref mut clipboard) = self.app.clipboard {
1534            clipboard
1535                .get()
1536                .clipboard(LinuxClipboardKind::Primary)
1537                .text()
1538                .ok()
1539        } else {
1540            None
1541        }
1542    }
1543
1544    #[cfg(not(all(
1545        feature = "clipboard-native",
1546        target_os = "linux",
1547        not(target_arch = "wasm32")
1548    )))]
1549    pub fn get_primary_selection(&mut self) -> Option<String> {
1550        None
1551    }
1552
1553    /// Syncs the current text field selection to PRIMARY (Linux X11).
1554    /// Call this when selection changes in a text field.
1555    pub fn sync_selection_to_primary(&mut self) {
1556        #[cfg(all(target_os = "linux", not(target_arch = "wasm32")))]
1557        {
1558            if let Some(text) = self.on_copy() {
1559                self.set_primary_selection(&text);
1560            }
1561        }
1562    }
1563
1564    /// Primary-surface form of [`SurfaceMut::set_pointer_source`].
1565    pub fn set_pointer_source(&mut self, source: PointerSource) {
1566        self.primary().set_pointer_source(source);
1567    }
1568
1569    /// Primary-surface form of [`SurfaceMut::pointer_source`].
1570    pub fn pointer_source(&self) -> PointerSource {
1571        self.surfaces[0].pointer_source
1572    }
1573
1574    /// Primary-surface form of [`SurfaceMut::set_cursor`].
1575    pub fn set_cursor(&mut self, x: f32, y: f32) -> bool {
1576        self.primary().set_cursor(x, y)
1577    }
1578
1579    /// Primary-surface form of [`SurfaceMut::set_cursor_at_time`].
1580    pub fn set_cursor_at_time(&mut self, x: f32, y: f32, time_ms: Option<i64>) -> bool {
1581        self.primary().set_cursor_at_time(x, y, time_ms)
1582    }
1583
1584    /// Primary-surface form of [`SurfaceMut::set_cursor_at_event_time`].
1585    pub fn set_cursor_at_event_time(
1586        &mut self,
1587        x: f32,
1588        y: f32,
1589        event_time: PointerEventTime,
1590    ) -> bool {
1591        self.primary().set_cursor_at_event_time(x, y, event_time)
1592    }
1593
1594    /// Primary-surface form of [`SurfaceMut::pointer_pressed`].
1595    pub fn pointer_pressed(&mut self) -> bool {
1596        self.primary().pointer_pressed()
1597    }
1598
1599    /// Primary-surface form of [`SurfaceMut::accessibility_activate_at`].
1600    pub fn accessibility_activate_at(&mut self, x: f32, y: f32) -> bool {
1601        self.primary().accessibility_activate_at(x, y)
1602    }
1603
1604    /// Primary-surface form of [`SurfaceMut::pointer_pressed_at_time`].
1605    pub fn pointer_pressed_at_time(&mut self, time_ms: Option<i64>) -> bool {
1606        self.primary().pointer_pressed_at_time(time_ms)
1607    }
1608
1609    /// Primary-surface form of [`SurfaceMut::pointer_pressed_at_event_time`].
1610    pub fn pointer_pressed_at_event_time(&mut self, event_time: PointerEventTime) -> bool {
1611        self.primary().pointer_pressed_at_event_time(event_time)
1612    }
1613
1614    /// Primary-surface form of [`SurfaceMut::pointer_released`].
1615    pub fn pointer_released(&mut self) -> bool {
1616        self.primary().pointer_released()
1617    }
1618
1619    /// Primary-surface form of [`SurfaceMut::pointer_released_at_position`].
1620    pub fn pointer_released_at_position(&mut self, x: f32, y: f32) -> bool {
1621        self.primary().pointer_released_at_position(x, y)
1622    }
1623
1624    /// Primary-surface form of
1625    /// [`SurfaceMut::pointer_released_at_position_time`].
1626    pub fn pointer_released_at_position_time(
1627        &mut self,
1628        x: f32,
1629        y: f32,
1630        time_ms: Option<i64>,
1631    ) -> bool {
1632        self.primary()
1633            .pointer_released_at_position_time(x, y, time_ms)
1634    }
1635
1636    /// Primary-surface form of
1637    /// [`SurfaceMut::pointer_released_at_position_event_time`].
1638    pub fn pointer_released_at_position_event_time(
1639        &mut self,
1640        x: f32,
1641        y: f32,
1642        event_time: PointerEventTime,
1643    ) -> bool {
1644        self.primary()
1645            .pointer_released_at_position_event_time(x, y, event_time)
1646    }
1647
1648    /// Primary-surface form of [`SurfaceMut::pointer_released_at_time`].
1649    pub fn pointer_released_at_time(&mut self, time_ms: Option<i64>) -> bool {
1650        self.primary().pointer_released_at_time(time_ms)
1651    }
1652
1653    /// Primary-surface form of [`SurfaceMut::pointer_released_at_event_time`].
1654    pub fn pointer_released_at_event_time(&mut self, event_time: PointerEventTime) -> bool {
1655        self.primary().pointer_released_at_event_time(event_time)
1656    }
1657
1658    /// Primary-surface form of [`SurfaceMut::secondary_pointer_pressed`].
1659    pub fn secondary_pointer_pressed(
1660        &mut self,
1661        pointer_id: u64,
1662        x: f32,
1663        y: f32,
1664        time_ms: Option<i64>,
1665    ) -> bool {
1666        self.primary()
1667            .secondary_pointer_pressed(pointer_id, x, y, time_ms)
1668    }
1669
1670    /// Primary-surface form of [`SurfaceMut::secondary_pointer_moved`].
1671    pub fn secondary_pointer_moved(
1672        &mut self,
1673        pointer_id: u64,
1674        x: f32,
1675        y: f32,
1676        time_ms: Option<i64>,
1677    ) -> bool {
1678        self.primary()
1679            .secondary_pointer_moved(pointer_id, x, y, time_ms)
1680    }
1681
1682    /// Primary-surface form of [`SurfaceMut::secondary_pointer_released`].
1683    pub fn secondary_pointer_released(
1684        &mut self,
1685        pointer_id: u64,
1686        x: f32,
1687        y: f32,
1688        time_ms: Option<i64>,
1689    ) -> bool {
1690        self.primary()
1691            .secondary_pointer_released(pointer_id, x, y, time_ms)
1692    }
1693
1694    /// Primary-surface form of [`SurfaceMut::pointer_zoomed`].
1695    pub fn pointer_zoomed(&mut self, zoom_factor: f32) -> bool {
1696        self.primary().pointer_zoomed(zoom_factor)
1697    }
1698
1699    /// Primary-surface form of [`SurfaceMut::wheel_scrolled`].
1700    pub fn wheel_scrolled(&mut self, wheel: crate::WheelScroll) -> bool {
1701        self.primary().wheel_scrolled(wheel)
1702    }
1703
1704    /// Primary-surface form of [`SurfaceMut::pointer_scrolled`].
1705    pub fn pointer_scrolled(&mut self, delta_x: f32, delta_y: f32) -> bool {
1706        self.primary().pointer_scrolled(delta_x, delta_y)
1707    }
1708
1709    /// Primary-surface form of [`SurfaceMut::set_on_rotary_scroll`].
1710    pub fn set_on_rotary_scroll<F>(&mut self, handler: F)
1711    where
1712        F: Fn(RotaryScrollEvent) -> bool + 'static,
1713    {
1714        self.primary().set_on_rotary_scroll(handler);
1715    }
1716
1717    /// Primary-surface form of [`SurfaceMut::clear_on_rotary_scroll`].
1718    pub fn clear_on_rotary_scroll(&mut self) {
1719        self.primary().clear_on_rotary_scroll();
1720    }
1721
1722    /// Primary-surface form of [`SurfaceMut::rotary_scrolled_by_detents`].
1723    pub fn rotary_scrolled_by_detents(&mut self, detents: f32, uptime_millis: u64) -> bool {
1724        self.primary()
1725            .rotary_scrolled_by_detents(detents, uptime_millis)
1726    }
1727
1728    /// Primary-surface form of [`SurfaceMut::rotary_scrolled`].
1729    pub fn rotary_scrolled(&mut self, event: RotaryScrollEvent) -> bool {
1730        self.primary().rotary_scrolled(event)
1731    }
1732
1733    /// Primary-surface form of [`SurfaceMut::cancel_gesture`].
1734    pub fn cancel_gesture(&mut self) {
1735        self.primary().cancel_gesture();
1736    }
1737
1738    /// Primary-surface form of [`SurfaceMut::cancel_gesture_unless_pressed`].
1739    pub fn cancel_gesture_unless_pressed(&mut self) {
1740        self.primary().cancel_gesture_unless_pressed();
1741    }
1742
1743    /// Offers the primary window's current pointer icon to the platform
1744    /// again, for the moments a windowing system has drawn its own default
1745    /// over it.
1746    pub fn refresh_pointer_icon(&self) {
1747        self.surfaces[0].pointer_icon.refresh();
1748    }
1749
1750    /// The pointer icon the platform has not applied to the primary window
1751    /// yet, or `None` when the icon has not changed since the last call.
1752    ///
1753    /// Platform backends call this after handing the shell a batch of input and
1754    /// set the returned icon on the window they own. Platforms with no pointing
1755    /// device never call it.
1756    pub fn take_pointer_icon_change(&self) -> Option<PointerIcon> {
1757        self.surfaces[0].pointer_icon.take_change()
1758    }
1759
1760    /// Installs the platform soft-keyboard handler for the primary window.
1761    ///
1762    /// The handler is invoked when a text field gains focus (`show_keyboard`)
1763    /// or when text-field focus is cleared or goes stale (`hide_keyboard`).
1764    /// Platform runtimes with an on-screen keyboard (Android, iOS) call this
1765    /// once after creating the shell.
1766    pub fn set_platform_text_input(
1767        &mut self,
1768        handler: Rc<dyn cranpose_ui::PlatformTextInputHandler>,
1769    ) {
1770        self.primary().set_platform_text_input(handler);
1771    }
1772
1773    /// Primary-surface form of [`SurfaceMut::dismiss_top_modal`].
1774    pub fn dismiss_top_modal(&mut self) -> bool {
1775        self.primary().dismiss_top_modal()
1776    }
1777
1778    /// Primary-surface form of [`SurfaceMut::move_focus_in_context`].
1779    pub fn move_focus_in_context(&mut self, direction: FocusDirection) -> bool {
1780        self.primary().move_focus_in_context(direction)
1781    }
1782
1783    /// Primary-surface form of [`SurfaceMut::on_key_event`].
1784    pub fn on_key_event(&mut self, event: &KeyEvent) -> bool {
1785        self.primary().on_key_event(event)
1786    }
1787
1788    /// Primary-surface form of [`SurfaceMut::on_paste`].
1789    pub fn on_paste(&mut self, text: &str) -> bool {
1790        self.primary().on_paste(text)
1791    }
1792
1793    /// Primary-surface form of [`SurfaceMut::on_copy`].
1794    pub fn on_copy(&mut self) -> Option<String> {
1795        self.primary().on_copy()
1796    }
1797
1798    /// Primary-surface form of [`SurfaceMut::on_cut`].
1799    pub fn on_cut(&mut self) -> Option<String> {
1800        self.primary().on_cut()
1801    }
1802
1803    /// Primary-surface form of [`SurfaceMut::on_ime_preedit`].
1804    pub fn on_ime_preedit(&mut self, text: &str, cursor: Option<(usize, usize)>) -> bool {
1805        self.primary().on_ime_preedit(text, cursor)
1806    }
1807
1808    /// Primary-surface form of [`SurfaceMut::on_ime_finish_composing`].
1809    pub fn on_ime_finish_composing(&mut self) -> bool {
1810        self.primary().on_ime_finish_composing()
1811    }
1812
1813    /// Primary-surface form of [`SurfaceMut::on_ime_set_composing_region`].
1814    pub fn on_ime_set_composing_region(&mut self, start_bytes: usize, end_bytes: usize) -> bool {
1815        self.primary()
1816            .on_ime_set_composing_region(start_bytes, end_bytes)
1817    }
1818
1819    /// Primary-surface form of [`SurfaceMut::on_ime_set_selection`].
1820    pub fn on_ime_set_selection(&mut self, start_bytes: usize, end_bytes: usize) -> bool {
1821        self.primary().on_ime_set_selection(start_bytes, end_bytes)
1822    }
1823
1824    /// Primary-surface form of [`SurfaceMut::ime_editor_state`].
1825    pub fn ime_editor_state(&mut self) -> Option<cranpose_ui::text_field_focus::ImeEditorState> {
1826        self.primary().ime_editor_state()
1827    }
1828
1829    /// Primary-surface form of [`SurfaceMut::ime_caret_geometry`].
1830    pub fn ime_caret_geometry(
1831        &mut self,
1832    ) -> Option<cranpose_ui::text_field_focus::ImeCaretGeometry> {
1833        self.primary().ime_caret_geometry()
1834    }
1835
1836    /// Primary-surface form of [`SurfaceMut::clear_text_field_focus`].
1837    pub fn clear_text_field_focus(&mut self) {
1838        self.primary().clear_text_field_focus();
1839    }
1840
1841    /// Primary-surface form of [`SurfaceMut::on_ime_delete_surrounding`].
1842    pub fn on_ime_delete_surrounding(&mut self, before_bytes: usize, after_bytes: usize) -> bool {
1843        self.primary()
1844            .on_ime_delete_surrounding(before_bytes, after_bytes)
1845    }
1846}
1847
1848fn plain_key_down(event: &KeyEvent) -> bool {
1849    event.event_type == KeyEventType::KeyDown
1850        && !event.modifiers.ctrl
1851        && !event.modifiers.meta
1852        && !event.modifiers.alt
1853}