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        self.on_group_navigation_key(event)
1015    }
1016
1017    fn on_group_navigation_key(&mut self, event: &KeyEvent) -> bool {
1018        let focused = cranpose_ui::active_focus_target();
1019        let target = self.with_layout_tree(|tree| {
1020            cranpose_ui::keyboard_focus_target(
1021                tree?,
1022                focused,
1023                event.key_code,
1024                event.modifiers.shift,
1025            )
1026        });
1027        let Some(target) = target else {
1028            return false;
1029        };
1030        let moved =
1031            run_in_mutable_snapshot(|| cranpose_ui::request_focus_from_platform(target.node_id))
1032                .unwrap_or(false);
1033        if moved {
1034            self.mark_dirty();
1035            self.note_focus_moved_by_keyboard(true);
1036            if target.activate {
1037                self.on_activation_key(&KeyEvent::key_down(KeyCode::Space, " "));
1038            }
1039        }
1040        moved
1041    }
1042
1043    fn on_activation_key(&mut self, event: &KeyEvent) -> bool {
1044        if !plain_key_down(event)
1045            || !matches!(event.key_code, KeyCode::Enter | KeyCode::Space)
1046            || cranpose_ui::text_field_focus::has_focused_field()
1047        {
1048            return false;
1049        }
1050        let Some(focused) = cranpose_ui::active_focus_target() else {
1051            return false;
1052        };
1053        let center = self.with_layout_tree(|layout_tree| {
1054            layout_tree
1055                .map(cranpose_ui::collect_focus_order)
1056                .unwrap_or_default()
1057                .iter()
1058                .find(|entry| entry.node_id == focused)
1059                .map(cranpose_ui::FocusEntry::center)
1060        });
1061        let Some((x, y)) = center else {
1062            return false;
1063        };
1064        self.set_cursor(x, y);
1065        let pressed = self.pointer_pressed();
1066        let released = self.pointer_released_at_position(x, y);
1067        self.note_focus_moved_by_keyboard(true);
1068        pressed || released
1069    }
1070
1071    fn on_arrow_key(&mut self, event: &KeyEvent) -> bool {
1072        if !plain_key_down(event) || cranpose_ui::text_field_focus::has_focused_field() {
1073            return false;
1074        }
1075        if self.on_group_navigation_key(event) {
1076            return true;
1077        }
1078        let direction = match event.key_code {
1079            KeyCode::ArrowLeft => FocusDirection::Left,
1080            KeyCode::ArrowRight => FocusDirection::Right,
1081            KeyCode::ArrowUp => FocusDirection::Up,
1082            KeyCode::ArrowDown => FocusDirection::Down,
1083            _ => return false,
1084        };
1085        let Some(focused) = cranpose_ui::active_focus_target() else {
1086            return false;
1087        };
1088        let order = self.with_layout_tree(|layout_tree| {
1089            let layout_tree = layout_tree?;
1090            let group = cranpose_ui::selectable_group_of(layout_tree, focused)?;
1091            Some(cranpose_ui::collect_focus_order_under(layout_tree, group))
1092        });
1093        order.is_some_and(|order| self.move_focus_in_order(order, direction))
1094    }
1095
1096    fn note_focus_moved_by_keyboard(&mut self, keyboard: bool) {
1097        if cranpose_ui::set_keyboard_focus_visible(keyboard) {
1098            cranpose_ui::request_render_invalidation();
1099            self.mark_dirty();
1100        }
1101    }
1102
1103    fn on_escape_key(&mut self, event: &KeyEvent) -> bool {
1104        if event.event_type != KeyEventType::KeyDown || event.key_code != KeyCode::Escape {
1105            return false;
1106        }
1107        self.dismiss_top_modal_in_context()
1108    }
1109
1110    /// Asks the innermost open dialog or popup to close, the way the platform
1111    /// back gesture does. Answers whether one was open to take the request.
1112    pub fn dismiss_top_modal(&mut self) -> bool {
1113        let _event_handler = enter_event_handler_scope();
1114        let app_context = Rc::clone(&self.shell.app.app_context);
1115        app_context.enter(|| self.dismiss_top_modal_in_context())
1116    }
1117
1118    fn dismiss_top_modal_in_context(&mut self) -> bool {
1119        let closed = run_in_mutable_snapshot(|| {
1120            if cranpose_ui::modal_depth() > 0 {
1121                cranpose_ui::dispatch_modal_back()
1122            } else {
1123                cranpose_ui::dismiss_top_popup()
1124            }
1125        })
1126        .unwrap_or(false);
1127        if closed {
1128            self.mark_dirty();
1129        }
1130        closed
1131    }
1132
1133    /// Publishes the focus order layout left behind and moves focus one step.
1134    /// Answers whether focus moved.
1135    pub fn move_focus_in_context(&mut self, direction: FocusDirection) -> bool {
1136        let order = self.with_layout_tree(|layout_tree| {
1137            layout_tree
1138                .map(cranpose_ui::collect_focus_order)
1139                .unwrap_or_default()
1140        });
1141        self.move_focus_in_order(order, direction)
1142    }
1143
1144    fn move_focus_in_order(
1145        &mut self,
1146        order: Vec<cranpose_ui::FocusEntry>,
1147        direction: FocusDirection,
1148    ) -> bool {
1149        cranpose_ui::set_focus_order(order);
1150        let moved = run_in_mutable_snapshot(|| cranpose_ui::FocusManager.move_focus(direction))
1151            .unwrap_or(false);
1152        if moved {
1153            self.mark_dirty();
1154            self.note_focus_moved_by_keyboard(true);
1155        }
1156        moved
1157    }
1158
1159    pub fn on_key_event(&mut self, event: &KeyEvent) -> bool {
1160        let _event_handler = enter_event_handler_scope();
1161        let app_context = Rc::clone(&self.shell.app.app_context);
1162        app_context.enter(|| self.on_key_event_inner(event))
1163    }
1164
1165    fn on_key_event_inner(&mut self, event: &KeyEvent) -> bool {
1166        if self.inspector_key(event) {
1167            return true;
1168        }
1169        use KeyEventType::KeyDown;
1170
1171        if event.event_type == KeyDown && event.modifiers.command_or_ctrl() {
1172            #[cfg(all(
1173                feature = "clipboard-native",
1174                not(target_arch = "wasm32"),
1175                not(target_os = "android"),
1176                not(target_os = "ios")
1177            ))]
1178            {
1179                match event.key_code {
1180                    KeyCode::C => {
1181                        if let Some(text) = self.on_copy_inner() {
1182                            cranpose_ui::clipboard_session::clipboard_write_text(&text);
1183                            return true;
1184                        }
1185                    }
1186                    KeyCode::X => {
1187                        if let Some(text) = self.on_cut_inner() {
1188                            cranpose_ui::clipboard_session::clipboard_write_text(&text);
1189                            self.mark_dirty();
1190                            self.shell.app.request_layout_pass();
1191                            return true;
1192                        }
1193                    }
1194                    KeyCode::V => {
1195                        if let Some(text) = cranpose_ui::clipboard_session::clipboard_read_text()
1196                            && self.on_paste_inner(&text)
1197                        {
1198                            return true;
1199                        }
1200                    }
1201                    _ => {}
1202                }
1203            }
1204        }
1205
1206        if self.on_focus_key(event) {
1207            return true;
1208        }
1209
1210        if self.on_escape_key(event) {
1211            return true;
1212        }
1213
1214        if self.on_activation_key(event) || self.on_arrow_key(event) {
1215            return true;
1216        }
1217
1218        if !cranpose_ui::text_field_focus::has_focused_field() {
1219            return false;
1220        }
1221
1222        let handled =
1223            run_in_mutable_snapshot(|| cranpose_ui::text_field_focus::dispatch_key_event(event))
1224                .unwrap_or(false);
1225
1226        if handled {
1227            self.mark_dirty();
1228            self.shell.app.request_layout_pass();
1229        }
1230
1231        handled
1232    }
1233
1234    /// Handles paste event from platform clipboard.
1235    /// Returns `true` if the paste was consumed by a focused text field.
1236    /// O(1) operation using stored handler.
1237    pub fn on_paste(&mut self, text: &str) -> bool {
1238        if self.inspector_owns_keyboard() {
1239            return true;
1240        }
1241        let _event_handler = enter_event_handler_scope();
1242        let app_context = Rc::clone(&self.shell.app.app_context);
1243        app_context.enter(|| self.on_paste_inner(text))
1244    }
1245
1246    fn on_paste_inner(&mut self, text: &str) -> bool {
1247        let handled =
1248            run_in_mutable_snapshot(|| cranpose_ui::text_field_focus::dispatch_paste(text))
1249                .unwrap_or(false);
1250
1251        if handled {
1252            self.mark_dirty();
1253            self.shell.app.request_layout_pass();
1254        }
1255
1256        handled
1257    }
1258
1259    /// Handles copy request from platform.
1260    /// Returns the selected text from focused text field, or None.
1261    /// O(1) operation using stored handler.
1262    pub fn on_copy(&mut self) -> Option<String> {
1263        let app_context = Rc::clone(&self.shell.app.app_context);
1264        if self.inspector_owns_keyboard() {
1265            return None;
1266        }
1267        app_context.enter(|| self.on_copy_inner())
1268    }
1269
1270    fn on_copy_inner(&mut self) -> Option<String> {
1271        cranpose_ui::text_field_focus::dispatch_copy()
1272    }
1273
1274    /// Handles cut request from platform.
1275    /// Returns the cut text from focused text field, or None.
1276    /// O(1) operation using stored handler.
1277    pub fn on_cut(&mut self) -> Option<String> {
1278        let _event_handler = enter_event_handler_scope();
1279        if self.inspector_owns_keyboard() {
1280            return None;
1281        }
1282        let app_context = Rc::clone(&self.shell.app.app_context);
1283        app_context.enter(|| self.on_cut_inner())
1284    }
1285
1286    fn on_cut_inner(&mut self) -> Option<String> {
1287        let text =
1288            run_in_mutable_snapshot(cranpose_ui::text_field_focus::dispatch_cut).unwrap_or(None);
1289
1290        if text.is_some() {
1291            self.mark_dirty();
1292            self.shell.app.request_layout_pass();
1293        }
1294
1295        text
1296    }
1297
1298    /// Handles IME preedit (composition) events.
1299    /// Called when the input method is composing text (e.g., typing CJK characters).
1300    ///
1301    /// - `text`: The current preedit text (empty to clear composition state)
1302    /// - `cursor`: Optional cursor position within the preedit text (start, end)
1303    ///
1304    /// Returns `true` if a text field consumed the event.
1305    pub fn on_ime_preedit(&mut self, text: &str, cursor: Option<(usize, usize)>) -> bool {
1306        if self.inspector_owns_keyboard() {
1307            return true;
1308        }
1309        let _event_handler = enter_event_handler_scope();
1310        let app_context = Rc::clone(&self.shell.app.app_context);
1311        app_context.enter(|| self.on_ime_preedit_inner(text, cursor))
1312    }
1313
1314    fn on_ime_preedit_inner(&mut self, text: &str, cursor: Option<(usize, usize)>) -> bool {
1315        let handled = run_in_mutable_snapshot(|| {
1316            cranpose_ui::text_field_focus::dispatch_ime_preedit(text, cursor)
1317        })
1318        .unwrap_or(false);
1319
1320        if handled {
1321            self.mark_dirty();
1322            self.shell.app.request_layout_pass();
1323        }
1324
1325        handled
1326    }
1327
1328    /// Finishes the active IME composition, keeping the composed text as
1329    /// committed text (Android `finishComposingText` semantics).
1330    /// Returns `true` if a text field consumed the event.
1331    pub fn on_ime_finish_composing(&mut self) -> bool {
1332        if self.inspector_owns_keyboard() {
1333            return true;
1334        }
1335        let _event_handler = enter_event_handler_scope();
1336        let app_context = Rc::clone(&self.shell.app.app_context);
1337        app_context.enter(|| self.on_ime_finish_composing_inner())
1338    }
1339
1340    fn on_ime_finish_composing_inner(&mut self) -> bool {
1341        let handled =
1342            run_in_mutable_snapshot(cranpose_ui::text_field_focus::dispatch_ime_finish_composing)
1343                .unwrap_or(false);
1344
1345        if handled {
1346            self.mark_dirty();
1347            self.shell.app.request_layout_pass();
1348        }
1349
1350        handled
1351    }
1352
1353    /// Marks existing text in the focused field as the composing region
1354    /// without changing it (Android `setComposingRegion` semantics). Offsets
1355    /// are UTF-8 bytes. Returns `true` if a text field consumed the event.
1356    pub fn on_ime_set_composing_region(&mut self, start_bytes: usize, end_bytes: usize) -> bool {
1357        if self.inspector_owns_keyboard() {
1358            return true;
1359        }
1360        let _event_handler = enter_event_handler_scope();
1361        let app_context = Rc::clone(&self.shell.app.app_context);
1362        app_context.enter(|| {
1363            let handled = run_in_mutable_snapshot(|| {
1364                cranpose_ui::text_field_focus::dispatch_ime_set_composing_region(
1365                    start_bytes,
1366                    end_bytes,
1367                )
1368            })
1369            .unwrap_or(false);
1370
1371            if handled {
1372                self.mark_dirty();
1373                self.shell.app.request_layout_pass();
1374            }
1375
1376            handled
1377        })
1378    }
1379
1380    /// Moves the focused field's selection/caret to `[start_bytes, end_bytes)`
1381    /// without editing text (Android `InputConnection.setSelection`; the path
1382    /// Gboard's spacebar-swipe uses to scrub the cursor). Offsets are UTF-8
1383    /// bytes. Returns `true` if a text field consumed the event.
1384    pub fn on_ime_set_selection(&mut self, start_bytes: usize, end_bytes: usize) -> bool {
1385        if self.inspector_owns_keyboard() {
1386            return true;
1387        }
1388        let _event_handler = enter_event_handler_scope();
1389        let app_context = Rc::clone(&self.shell.app.app_context);
1390        app_context.enter(|| {
1391            let handled = run_in_mutable_snapshot(|| {
1392                cranpose_ui::text_field_focus::dispatch_ime_set_selection(start_bytes, end_bytes)
1393            })
1394            .unwrap_or(false);
1395
1396            if handled {
1397                self.mark_dirty();
1398            }
1399
1400            handled
1401        })
1402    }
1403
1404    /// Returns a snapshot of the focused text field's editable state for
1405    /// platform IMEs (text, selection and composition in UTF-8 bytes), or
1406    /// `None` when no text field is focused.
1407    pub fn ime_editor_state(&mut self) -> Option<cranpose_ui::text_field_focus::ImeEditorState> {
1408        let app_context = Rc::clone(&self.shell.app.app_context);
1409        app_context.enter(cranpose_ui::text_field_focus::focused_editor_state)
1410    }
1411
1412    /// Window-space caret geometry of the focused field for coordinate-based
1413    /// platform text input (iOS trackpad cursor + tap-to-position), or `None`
1414    /// when no text field is focused.
1415    pub fn ime_caret_geometry(
1416        &mut self,
1417    ) -> Option<cranpose_ui::text_field_focus::ImeCaretGeometry> {
1418        let app_context = Rc::clone(&self.shell.app.app_context);
1419        app_context.enter(cranpose_ui::text_field_focus::focused_caret_geometry)
1420    }
1421
1422    /// Clears text-field focus (used by platform IME actions such as
1423    /// Android's Done). The focus-loss notification hides the soft keyboard.
1424    pub fn clear_text_field_focus(&mut self) {
1425        let _event_handler = enter_event_handler_scope();
1426        let app_context = Rc::clone(&self.shell.app.app_context);
1427        app_context.enter(cranpose_ui::text_field_focus::clear_focus);
1428        self.mark_dirty();
1429        self.shell.app.request_layout_pass();
1430    }
1431
1432    /// Handles IME delete-surrounding events.
1433    /// Returns `true` if a text field consumed the event.
1434    pub fn on_ime_delete_surrounding(&mut self, before_bytes: usize, after_bytes: usize) -> bool {
1435        if self.inspector_owns_keyboard() {
1436            return true;
1437        }
1438        let _event_handler = enter_event_handler_scope();
1439        let app_context = Rc::clone(&self.shell.app.app_context);
1440        app_context.enter(|| self.on_ime_delete_surrounding_inner(before_bytes, after_bytes))
1441    }
1442
1443    fn on_ime_delete_surrounding_inner(&mut self, before_bytes: usize, after_bytes: usize) -> bool {
1444        let handled = run_in_mutable_snapshot(|| {
1445            cranpose_ui::text_field_focus::dispatch_delete_surrounding(before_bytes, after_bytes)
1446        })
1447        .unwrap_or(false);
1448
1449        if handled {
1450            self.mark_dirty();
1451            self.shell.app.request_layout_pass();
1452        }
1453
1454        handled
1455    }
1456}
1457
1458impl<R> AppShell<R>
1459where
1460    R: Renderer,
1461    R::Error: Debug,
1462{
1463    /// Sets the keyboard modifiers held right now, so the platform's live
1464    /// modifier state (winit's `ModifiersChanged`, a DOM event's
1465    /// `shiftKey`/`ctrlKey`/`altKey`/`metaKey`) reaches every `PointerEvent`
1466    /// the shell dispatches from here on -- the same state the wheel path
1467    /// already carries via [`WheelScroll::with_modifiers`](crate::WheelScroll::with_modifiers).
1468    /// A platform that never calls this leaves pointer events reporting
1469    /// `None` (see [`PointerEvent::modifiers`]) rather than a silently wrong
1470    /// "nothing held".
1471    pub fn set_modifiers(&mut self, modifiers: Modifiers) {
1472        self.app.modifiers = Some(modifiers);
1473    }
1474
1475    /// The keyboard modifiers most recently set via
1476    /// [`set_modifiers`](Self::set_modifiers), or `None` if the platform has
1477    /// never reported them.
1478    pub fn modifiers(&self) -> Option<Modifiers> {
1479        self.app.modifiers
1480    }
1481
1482    /// Pixels per rotary detent used by
1483    /// [`rotary_scrolled_by_detents`](Self::rotary_scrolled_by_detents).
1484    pub fn rotary_scroll_factor(&self) -> f32 {
1485        self.app.rotary_scroll_factor
1486    }
1487
1488    /// Sets the pixels-per-detent factor for rotary input.
1489    ///
1490    /// On Wear OS this must be `ViewConfiguration.getScaledVerticalScrollFactor()`
1491    /// for pixel-exact parity with Compose. The host activity can read it over
1492    /// JNI once at startup and push it here; when it does not, the shell falls
1493    /// back to [`DEFAULT_ROTARY_SCROLL_FACTOR_DP`] scaled by display density.
1494    ///
1495    /// Non-finite or non-positive values are ignored.
1496    pub fn set_rotary_scroll_factor(&mut self, factor: f32) {
1497        if factor.is_finite() && factor > 0.0 {
1498            self.app.rotary_scroll_factor = factor;
1499        }
1500    }
1501
1502    /// Notifies the framework that the host app was paused/backgrounded.
1503    ///
1504    /// Withdraws any outstanding soft-keyboard request (and hides the keyboard)
1505    /// so the "keyboard shown" state does not survive across the pause and get
1506    /// restored on resume with no focused field. Platform runtimes call this
1507    /// from their pause lifecycle event.
1508    pub fn notify_app_paused(&mut self) {
1509        let app_context = Rc::clone(&self.app.app_context);
1510        app_context.enter(cranpose_ui::text_input_session::notify_app_paused);
1511    }
1512
1513    /// Notifies the framework that the host app resumed/foregrounded.
1514    ///
1515    /// Never auto-shows the soft keyboard, even for a still-focused field: a
1516    /// warm resume keeps the caret but must not resurrect the keyboard (the user
1517    /// taps the field to bring it back). Always returns `false` so the platform
1518    /// runtime force-hides the OS-restored keyboard. Platform runtimes call this
1519    /// from their resume lifecycle event.
1520    pub fn notify_app_resumed(&mut self) -> bool {
1521        let app_context = Rc::clone(&self.app.app_context);
1522        app_context.enter(cranpose_ui::text_input_session::notify_app_resumed)
1523    }
1524
1525    #[cfg(all(
1526        feature = "clipboard-native",
1527        target_os = "linux",
1528        not(target_arch = "wasm32")
1529    ))]
1530    pub fn set_primary_selection(&mut self, text: &str) {
1531        use arboard::{LinuxClipboardKind, SetExtLinux};
1532        if let Some(ref mut clipboard) = self.app.clipboard {
1533            let result = clipboard
1534                .set()
1535                .clipboard(LinuxClipboardKind::Primary)
1536                .text(text.to_string());
1537            if let Err(e) = result {
1538                log::debug!("Primary selection set failed: {:?}", e);
1539            }
1540        }
1541    }
1542
1543    #[cfg(not(all(
1544        feature = "clipboard-native",
1545        target_os = "linux",
1546        not(target_arch = "wasm32")
1547    )))]
1548    pub fn set_primary_selection(&mut self, _text: &str) {}
1549
1550    #[cfg(all(
1551        feature = "clipboard-native",
1552        target_os = "linux",
1553        not(target_arch = "wasm32")
1554    ))]
1555    pub fn get_primary_selection(&mut self) -> Option<String> {
1556        use arboard::{GetExtLinux, LinuxClipboardKind};
1557        if let Some(ref mut clipboard) = self.app.clipboard {
1558            clipboard
1559                .get()
1560                .clipboard(LinuxClipboardKind::Primary)
1561                .text()
1562                .ok()
1563        } else {
1564            None
1565        }
1566    }
1567
1568    #[cfg(not(all(
1569        feature = "clipboard-native",
1570        target_os = "linux",
1571        not(target_arch = "wasm32")
1572    )))]
1573    pub fn get_primary_selection(&mut self) -> Option<String> {
1574        None
1575    }
1576
1577    /// Syncs the current text field selection to PRIMARY (Linux X11).
1578    /// Call this when selection changes in a text field.
1579    pub fn sync_selection_to_primary(&mut self) {
1580        #[cfg(all(target_os = "linux", not(target_arch = "wasm32")))]
1581        {
1582            if let Some(text) = self.on_copy() {
1583                self.set_primary_selection(&text);
1584            }
1585        }
1586    }
1587
1588    /// Primary-surface form of [`SurfaceMut::set_pointer_source`].
1589    pub fn set_pointer_source(&mut self, source: PointerSource) {
1590        self.primary().set_pointer_source(source);
1591    }
1592
1593    /// Primary-surface form of [`SurfaceMut::pointer_source`].
1594    pub fn pointer_source(&self) -> PointerSource {
1595        self.surfaces[0].pointer_source
1596    }
1597
1598    /// Primary-surface form of [`SurfaceMut::set_cursor`].
1599    pub fn set_cursor(&mut self, x: f32, y: f32) -> bool {
1600        self.primary().set_cursor(x, y)
1601    }
1602
1603    /// Primary-surface form of [`SurfaceMut::set_cursor_at_time`].
1604    pub fn set_cursor_at_time(&mut self, x: f32, y: f32, time_ms: Option<i64>) -> bool {
1605        self.primary().set_cursor_at_time(x, y, time_ms)
1606    }
1607
1608    /// Primary-surface form of [`SurfaceMut::set_cursor_at_event_time`].
1609    pub fn set_cursor_at_event_time(
1610        &mut self,
1611        x: f32,
1612        y: f32,
1613        event_time: PointerEventTime,
1614    ) -> bool {
1615        self.primary().set_cursor_at_event_time(x, y, event_time)
1616    }
1617
1618    /// Primary-surface form of [`SurfaceMut::pointer_pressed`].
1619    pub fn pointer_pressed(&mut self) -> bool {
1620        self.primary().pointer_pressed()
1621    }
1622
1623    /// Primary-surface form of [`SurfaceMut::accessibility_activate_at`].
1624    pub fn accessibility_activate_at(&mut self, x: f32, y: f32) -> bool {
1625        self.primary().accessibility_activate_at(x, y)
1626    }
1627
1628    /// Primary-surface form of [`SurfaceMut::pointer_pressed_at_time`].
1629    pub fn pointer_pressed_at_time(&mut self, time_ms: Option<i64>) -> bool {
1630        self.primary().pointer_pressed_at_time(time_ms)
1631    }
1632
1633    /// Primary-surface form of [`SurfaceMut::pointer_pressed_at_event_time`].
1634    pub fn pointer_pressed_at_event_time(&mut self, event_time: PointerEventTime) -> bool {
1635        self.primary().pointer_pressed_at_event_time(event_time)
1636    }
1637
1638    /// Primary-surface form of [`SurfaceMut::pointer_released`].
1639    pub fn pointer_released(&mut self) -> bool {
1640        self.primary().pointer_released()
1641    }
1642
1643    /// Primary-surface form of [`SurfaceMut::pointer_released_at_position`].
1644    pub fn pointer_released_at_position(&mut self, x: f32, y: f32) -> bool {
1645        self.primary().pointer_released_at_position(x, y)
1646    }
1647
1648    /// Primary-surface form of
1649    /// [`SurfaceMut::pointer_released_at_position_time`].
1650    pub fn pointer_released_at_position_time(
1651        &mut self,
1652        x: f32,
1653        y: f32,
1654        time_ms: Option<i64>,
1655    ) -> bool {
1656        self.primary()
1657            .pointer_released_at_position_time(x, y, time_ms)
1658    }
1659
1660    /// Primary-surface form of
1661    /// [`SurfaceMut::pointer_released_at_position_event_time`].
1662    pub fn pointer_released_at_position_event_time(
1663        &mut self,
1664        x: f32,
1665        y: f32,
1666        event_time: PointerEventTime,
1667    ) -> bool {
1668        self.primary()
1669            .pointer_released_at_position_event_time(x, y, event_time)
1670    }
1671
1672    /// Primary-surface form of [`SurfaceMut::pointer_released_at_time`].
1673    pub fn pointer_released_at_time(&mut self, time_ms: Option<i64>) -> bool {
1674        self.primary().pointer_released_at_time(time_ms)
1675    }
1676
1677    /// Primary-surface form of [`SurfaceMut::pointer_released_at_event_time`].
1678    pub fn pointer_released_at_event_time(&mut self, event_time: PointerEventTime) -> bool {
1679        self.primary().pointer_released_at_event_time(event_time)
1680    }
1681
1682    /// Primary-surface form of [`SurfaceMut::secondary_pointer_pressed`].
1683    pub fn secondary_pointer_pressed(
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_pressed(pointer_id, x, y, time_ms)
1692    }
1693
1694    /// Primary-surface form of [`SurfaceMut::secondary_pointer_moved`].
1695    pub fn secondary_pointer_moved(
1696        &mut self,
1697        pointer_id: u64,
1698        x: f32,
1699        y: f32,
1700        time_ms: Option<i64>,
1701    ) -> bool {
1702        self.primary()
1703            .secondary_pointer_moved(pointer_id, x, y, time_ms)
1704    }
1705
1706    /// Primary-surface form of [`SurfaceMut::secondary_pointer_released`].
1707    pub fn secondary_pointer_released(
1708        &mut self,
1709        pointer_id: u64,
1710        x: f32,
1711        y: f32,
1712        time_ms: Option<i64>,
1713    ) -> bool {
1714        self.primary()
1715            .secondary_pointer_released(pointer_id, x, y, time_ms)
1716    }
1717
1718    /// Primary-surface form of [`SurfaceMut::pointer_zoomed`].
1719    pub fn pointer_zoomed(&mut self, zoom_factor: f32) -> bool {
1720        self.primary().pointer_zoomed(zoom_factor)
1721    }
1722
1723    /// Primary-surface form of [`SurfaceMut::wheel_scrolled`].
1724    pub fn wheel_scrolled(&mut self, wheel: crate::WheelScroll) -> bool {
1725        self.primary().wheel_scrolled(wheel)
1726    }
1727
1728    /// Primary-surface form of [`SurfaceMut::pointer_scrolled`].
1729    pub fn pointer_scrolled(&mut self, delta_x: f32, delta_y: f32) -> bool {
1730        self.primary().pointer_scrolled(delta_x, delta_y)
1731    }
1732
1733    /// Primary-surface form of [`SurfaceMut::set_on_rotary_scroll`].
1734    pub fn set_on_rotary_scroll<F>(&mut self, handler: F)
1735    where
1736        F: Fn(RotaryScrollEvent) -> bool + 'static,
1737    {
1738        self.primary().set_on_rotary_scroll(handler);
1739    }
1740
1741    /// Primary-surface form of [`SurfaceMut::clear_on_rotary_scroll`].
1742    pub fn clear_on_rotary_scroll(&mut self) {
1743        self.primary().clear_on_rotary_scroll();
1744    }
1745
1746    /// Primary-surface form of [`SurfaceMut::rotary_scrolled_by_detents`].
1747    pub fn rotary_scrolled_by_detents(&mut self, detents: f32, uptime_millis: u64) -> bool {
1748        self.primary()
1749            .rotary_scrolled_by_detents(detents, uptime_millis)
1750    }
1751
1752    /// Primary-surface form of [`SurfaceMut::rotary_scrolled`].
1753    pub fn rotary_scrolled(&mut self, event: RotaryScrollEvent) -> bool {
1754        self.primary().rotary_scrolled(event)
1755    }
1756
1757    /// Primary-surface form of [`SurfaceMut::cancel_gesture`].
1758    pub fn cancel_gesture(&mut self) {
1759        self.primary().cancel_gesture();
1760    }
1761
1762    /// Primary-surface form of [`SurfaceMut::cancel_gesture_unless_pressed`].
1763    pub fn cancel_gesture_unless_pressed(&mut self) {
1764        self.primary().cancel_gesture_unless_pressed();
1765    }
1766
1767    /// Offers the primary window's current pointer icon to the platform
1768    /// again, for the moments a windowing system has drawn its own default
1769    /// over it.
1770    pub fn refresh_pointer_icon(&self) {
1771        self.surfaces[0].pointer_icon.refresh();
1772    }
1773
1774    /// The pointer icon the platform has not applied to the primary window
1775    /// yet, or `None` when the icon has not changed since the last call.
1776    ///
1777    /// Platform backends call this after handing the shell a batch of input and
1778    /// set the returned icon on the window they own. Platforms with no pointing
1779    /// device never call it.
1780    pub fn take_pointer_icon_change(&self) -> Option<PointerIcon> {
1781        self.surfaces[0].pointer_icon.take_change()
1782    }
1783
1784    /// Installs the platform soft-keyboard handler for the primary window.
1785    ///
1786    /// The handler is invoked when a text field gains focus (`show_keyboard`)
1787    /// or when text-field focus is cleared or goes stale (`hide_keyboard`).
1788    /// Platform runtimes with an on-screen keyboard (Android, iOS) call this
1789    /// once after creating the shell.
1790    pub fn set_platform_text_input(
1791        &mut self,
1792        handler: Rc<dyn cranpose_ui::PlatformTextInputHandler>,
1793    ) {
1794        self.primary().set_platform_text_input(handler);
1795    }
1796
1797    /// Primary-surface form of [`SurfaceMut::dismiss_top_modal`].
1798    pub fn dismiss_top_modal(&mut self) -> bool {
1799        self.primary().dismiss_top_modal()
1800    }
1801
1802    /// Primary-surface form of [`SurfaceMut::move_focus_in_context`].
1803    pub fn move_focus_in_context(&mut self, direction: FocusDirection) -> bool {
1804        self.primary().move_focus_in_context(direction)
1805    }
1806
1807    /// Primary-surface form of [`SurfaceMut::on_key_event`].
1808    pub fn on_key_event(&mut self, event: &KeyEvent) -> bool {
1809        self.primary().on_key_event(event)
1810    }
1811
1812    /// Primary-surface form of [`SurfaceMut::on_paste`].
1813    pub fn on_paste(&mut self, text: &str) -> bool {
1814        self.primary().on_paste(text)
1815    }
1816
1817    /// Primary-surface form of [`SurfaceMut::on_copy`].
1818    pub fn on_copy(&mut self) -> Option<String> {
1819        self.primary().on_copy()
1820    }
1821
1822    /// Primary-surface form of [`SurfaceMut::on_cut`].
1823    pub fn on_cut(&mut self) -> Option<String> {
1824        self.primary().on_cut()
1825    }
1826
1827    /// Primary-surface form of [`SurfaceMut::on_ime_preedit`].
1828    pub fn on_ime_preedit(&mut self, text: &str, cursor: Option<(usize, usize)>) -> bool {
1829        self.primary().on_ime_preedit(text, cursor)
1830    }
1831
1832    /// Primary-surface form of [`SurfaceMut::on_ime_finish_composing`].
1833    pub fn on_ime_finish_composing(&mut self) -> bool {
1834        self.primary().on_ime_finish_composing()
1835    }
1836
1837    /// Primary-surface form of [`SurfaceMut::on_ime_set_composing_region`].
1838    pub fn on_ime_set_composing_region(&mut self, start_bytes: usize, end_bytes: usize) -> bool {
1839        self.primary()
1840            .on_ime_set_composing_region(start_bytes, end_bytes)
1841    }
1842
1843    /// Primary-surface form of [`SurfaceMut::on_ime_set_selection`].
1844    pub fn on_ime_set_selection(&mut self, start_bytes: usize, end_bytes: usize) -> bool {
1845        self.primary().on_ime_set_selection(start_bytes, end_bytes)
1846    }
1847
1848    /// Primary-surface form of [`SurfaceMut::ime_editor_state`].
1849    pub fn ime_editor_state(&mut self) -> Option<cranpose_ui::text_field_focus::ImeEditorState> {
1850        self.primary().ime_editor_state()
1851    }
1852
1853    /// Primary-surface form of [`SurfaceMut::ime_caret_geometry`].
1854    pub fn ime_caret_geometry(
1855        &mut self,
1856    ) -> Option<cranpose_ui::text_field_focus::ImeCaretGeometry> {
1857        self.primary().ime_caret_geometry()
1858    }
1859
1860    /// Primary-surface form of [`SurfaceMut::clear_text_field_focus`].
1861    pub fn clear_text_field_focus(&mut self) {
1862        self.primary().clear_text_field_focus();
1863    }
1864
1865    /// Primary-surface form of [`SurfaceMut::on_ime_delete_surrounding`].
1866    pub fn on_ime_delete_surrounding(&mut self, before_bytes: usize, after_bytes: usize) -> bool {
1867        self.primary()
1868            .on_ime_delete_surrounding(before_bytes, after_bytes)
1869    }
1870}
1871
1872fn plain_key_down(event: &KeyEvent) -> bool {
1873    event.event_type == KeyEventType::KeyDown
1874        && !event.modifiers.ctrl
1875        && !event.modifiers.meta
1876        && !event.modifiers.alt
1877}