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