Skip to main content

cranpose_app_shell/
shell_input.rs

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