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