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