Skip to main content

accesskit_android/
adapter.rs

1// Copyright 2025 The AccessKit Authors. All rights reserved.
2// Licensed under the Apache License, Version 2.0 (found in
3// the LICENSE-APACHE file) or the MIT license (found in
4// the LICENSE-MIT file), at your option.
5
6// Derived from the Flutter engine.
7// Copyright 2013 The Flutter Authors. All rights reserved.
8// Use of this source code is governed by a BSD-style license that can be
9// found in the LICENSE.chromium file.
10
11use accesskit::{
12    Action, ActionData, ActionHandler, ActionRequest, ActivationHandler, Node as NodeData,
13    NodeId as LocalNodeId, Orientation, Point, Role, ScrollUnit, TextSelection, Tree as TreeData,
14    TreeId, TreeUpdate,
15};
16use accesskit_consumer::{FilterResult, Node, TextPosition, Tree, TreeChangeHandler};
17use jni::{
18    objects::JObject,
19    sys::{jfloat, jint},
20    JNIEnv,
21};
22
23use crate::{
24    action::{PlatformAction, PlatformActionInner},
25    event::{QueuedEvent, QueuedEvents, ScrollDimension},
26    filters::filter,
27    node::{add_action, NodeWrapper},
28    util::*,
29};
30
31fn enqueue_window_content_changed(events: &mut Vec<QueuedEvent>) {
32    events.push(QueuedEvent::WindowContentChanged {
33        virtual_view_id: HOST_VIEW_ID,
34    });
35}
36
37fn enqueue_focus_event_if_applicable(
38    events: &mut Vec<QueuedEvent>,
39    node_id_map: &mut NodeIdMap,
40    node: &Node,
41) {
42    if node.is_root() && node.role() == Role::Window {
43        return;
44    }
45    let id = node_id_map.get_or_create_java_id(node);
46    events.push(QueuedEvent::Simple {
47        virtual_view_id: id,
48        event_type: EVENT_VIEW_FOCUSED,
49    });
50}
51
52struct AdapterChangeHandler<'a> {
53    events: &'a mut Vec<QueuedEvent>,
54    node_id_map: &'a mut NodeIdMap,
55    enqueued_window_content_changed: bool,
56}
57
58impl<'a> AdapterChangeHandler<'a> {
59    fn new(events: &'a mut Vec<QueuedEvent>, node_id_map: &'a mut NodeIdMap) -> Self {
60        Self {
61            events,
62            node_id_map,
63            enqueued_window_content_changed: false,
64        }
65    }
66}
67
68impl AdapterChangeHandler<'_> {
69    fn enqueue_window_content_changed_if_needed(&mut self) {
70        if self.enqueued_window_content_changed {
71            return;
72        }
73        enqueue_window_content_changed(self.events);
74        self.enqueued_window_content_changed = true;
75    }
76}
77
78impl TreeChangeHandler for AdapterChangeHandler<'_> {
79    fn node_added(&mut self, _node: &Node) {
80        self.enqueue_window_content_changed_if_needed();
81        // TODO: live regions?
82    }
83
84    fn node_updated(&mut self, old_node: &Node, new_node: &Node) {
85        self.enqueue_window_content_changed_if_needed();
86        if filter(new_node) != FilterResult::Include {
87            return;
88        }
89        let old_wrapper = NodeWrapper(old_node);
90        let new_wrapper = NodeWrapper(new_node);
91        let old_text = old_wrapper.text();
92        let new_text = new_wrapper.text();
93        if old_text != new_text {
94            let id = self.node_id_map.get_or_create_java_id(new_node);
95            self.events.push(QueuedEvent::TextChanged {
96                virtual_view_id: id,
97                old: old_text.unwrap_or_else(String::new),
98                new: new_text.clone().unwrap_or_else(String::new),
99            });
100        }
101        if old_node.raw_text_selection() != new_node.raw_text_selection()
102            || (new_node.raw_text_selection().is_some()
103                && old_node.is_focused() != new_node.is_focused())
104        {
105            if let Some((start, end)) = new_wrapper.text_selection() {
106                if let Some(text) = new_text {
107                    let id = self.node_id_map.get_or_create_java_id(new_node);
108                    self.events.push(QueuedEvent::TextSelectionChanged {
109                        virtual_view_id: id,
110                        text,
111                        start: start as jint,
112                        end: end as jint,
113                    });
114                }
115            }
116        }
117        let scroll_x = old_wrapper
118            .scroll_x()
119            .zip(new_wrapper.scroll_x())
120            .and_then(|(old, new)| {
121                (new != old).then(|| ScrollDimension {
122                    current: new,
123                    delta: new - old,
124                    max: new_wrapper.max_scroll_x(),
125                })
126            });
127        let scroll_y = old_wrapper
128            .scroll_y()
129            .zip(new_wrapper.scroll_y())
130            .and_then(|(old, new)| {
131                (new != old).then(|| ScrollDimension {
132                    current: new,
133                    delta: new - old,
134                    max: new_wrapper.max_scroll_y(),
135                })
136            });
137        if scroll_x.is_some() || scroll_y.is_some() {
138            let id = self.node_id_map.get_or_create_java_id(new_node);
139            self.events.push(QueuedEvent::Scrolled {
140                virtual_view_id: id,
141                x: scroll_x,
142                y: scroll_y,
143            });
144        }
145        // TODO: other events
146    }
147
148    fn focus_moved(&mut self, _old_node: Option<&Node>, new_node: Option<&Node>) {
149        if let Some(new_node) = new_node {
150            enqueue_focus_event_if_applicable(self.events, self.node_id_map, new_node);
151        }
152    }
153
154    fn node_removed(&mut self, _node: &Node) {
155        self.enqueue_window_content_changed_if_needed();
156        // TODO: other events?
157    }
158}
159
160const PLACEHOLDER_ROOT_ID: LocalNodeId = LocalNodeId(0);
161
162#[derive(Debug, Default)]
163enum State {
164    #[default]
165    Inactive,
166    Placeholder(Tree),
167    Active(Tree),
168}
169
170impl State {
171    fn get_or_init_tree<H: ActivationHandler + ?Sized>(
172        &mut self,
173        activation_handler: &mut H,
174    ) -> &Tree {
175        match self {
176            Self::Inactive => {
177                *self = match activation_handler.request_initial_tree() {
178                    Some(initial_state) => Self::Active(Tree::new(initial_state, true)),
179                    None => {
180                        let placeholder_update = TreeUpdate {
181                            nodes: vec![(PLACEHOLDER_ROOT_ID, NodeData::new(Role::Window))],
182                            tree: Some(TreeData::new(PLACEHOLDER_ROOT_ID)),
183                            tree_id: TreeId::ROOT,
184                            focus: PLACEHOLDER_ROOT_ID,
185                        };
186                        Self::Placeholder(Tree::new(placeholder_update, true))
187                    }
188                };
189                self.get_or_init_tree(activation_handler)
190            }
191            Self::Placeholder(tree) => tree,
192            Self::Active(tree) => tree,
193        }
194    }
195
196    fn get_full_tree(&mut self) -> Option<&mut Tree> {
197        match self {
198            Self::Inactive => None,
199            Self::Placeholder(_) => None,
200            Self::Active(tree) => Some(tree),
201        }
202    }
203}
204
205fn update_tree(
206    events: &mut Vec<QueuedEvent>,
207    node_id_map: &mut NodeIdMap,
208    tree: &mut Tree,
209    update: TreeUpdate,
210) {
211    let mut handler = AdapterChangeHandler::new(events, node_id_map);
212    tree.update_and_process_changes(update, &mut handler);
213}
214
215/// Low-level AccessKit adapter for Android.
216///
217/// This layer provides maximum flexibility in the application threading
218/// model, the interface between Java and native code, and the implementation
219/// of action callbacks, at the expense of requiring its caller to provide
220/// glue code. For a higher-level implementation built on this type, see
221/// [`InjectingAdapter`].
222///
223/// [`InjectingAdapter`]: crate::InjectingAdapter
224#[derive(Debug, Default)]
225pub struct Adapter {
226    node_id_map: NodeIdMap,
227    state: State,
228    accessibility_focus: Option<jint>,
229    hover_target: Option<jint>,
230}
231
232impl Adapter {
233    /// If and only if the tree has been initialized, call the provided function
234    /// and apply the resulting update. Note: If the caller's implementation of
235    /// [`ActivationHandler::request_initial_tree`] initially returned `None`,
236    /// the [`TreeUpdate`] returned by the provided function must contain
237    /// a full tree.
238    ///
239    /// If a [`QueuedEvents`] instance is returned, the caller must call
240    /// [`QueuedEvents::raise`] on it.
241    ///
242    /// This method may be safely called on any thread, but refer to
243    /// [`QueuedEvents::raise`] for restrictions on the context in which
244    /// it should be called.
245    pub fn update_if_active(
246        &mut self,
247        update_factory: impl FnOnce() -> TreeUpdate,
248    ) -> Option<QueuedEvents> {
249        match &mut self.state {
250            State::Inactive => None,
251            State::Placeholder(_) => {
252                let tree = Tree::new(update_factory(), true);
253                let mut events = Vec::new();
254                enqueue_window_content_changed(&mut events);
255                let state = tree.state();
256                if let Some(focus) = state.focus() {
257                    enqueue_focus_event_if_applicable(&mut events, &mut self.node_id_map, &focus);
258                }
259                self.state = State::Active(tree);
260                Some(QueuedEvents(events))
261            }
262            State::Active(tree) => {
263                let mut events = Vec::new();
264                update_tree(&mut events, &mut self.node_id_map, tree, update_factory());
265                Some(QueuedEvents(events))
266            }
267        }
268    }
269
270    /// Create an `AccessibilityNodeInfo` for the AccessKit node
271    /// corresponding to the given virtual view ID. Returns null if
272    /// there is no such node.
273    ///
274    /// The `host` parameter is the Android view for this adapter.
275    /// It must be an instance of `android.view.View` or a subclass.
276    pub fn create_accessibility_node_info<'local, H: ActivationHandler + ?Sized>(
277        &mut self,
278        activation_handler: &mut H,
279        env: &mut JNIEnv<'local>,
280        host: &JObject,
281        virtual_view_id: jint,
282    ) -> JObject<'local> {
283        let tree = self.state.get_or_init_tree(activation_handler);
284        let tree_state = tree.state();
285        let node = if virtual_view_id == HOST_VIEW_ID {
286            tree_state.root()
287        } else {
288            let Some(accesskit_id) = self.node_id_map.get_accesskit_id(virtual_view_id) else {
289                return JObject::null();
290            };
291            let Some(node) = tree_state.node_by_id(accesskit_id) else {
292                return JObject::null();
293            };
294            node
295        };
296
297        let node_info_class = env
298            .find_class("android/view/accessibility/AccessibilityNodeInfo")
299            .unwrap();
300        let node_info = env
301            .call_static_method(
302                &node_info_class,
303                "obtain",
304                "(Landroid/view/View;I)Landroid/view/accessibility/AccessibilityNodeInfo;",
305                &[host.into(), virtual_view_id.into()],
306            )
307            .unwrap()
308            .l()
309            .unwrap();
310
311        let package_name = get_package_name(env, host);
312        env.call_method(
313            &node_info,
314            "setPackageName",
315            "(Ljava/lang/CharSequence;)V",
316            &[(&package_name).into()],
317        )
318        .unwrap();
319
320        let wrapper = NodeWrapper(&node);
321        wrapper.populate_node_info(env, host, &mut self.node_id_map, &node_info);
322
323        let is_accessibility_focus = self.accessibility_focus == Some(virtual_view_id);
324        env.call_method(
325            &node_info,
326            "setAccessibilityFocused",
327            "(Z)V",
328            &[is_accessibility_focus.into()],
329        )
330        .unwrap();
331        add_action(
332            env,
333            &node_info,
334            if is_accessibility_focus {
335                ACTION_CLEAR_ACCESSIBILITY_FOCUS
336            } else {
337                ACTION_ACCESSIBILITY_FOCUS
338            },
339        );
340
341        node_info
342    }
343
344    /// Create an `AccessibilityNodeInfo` for the AccessKit node
345    /// with the given focus type. Returns null if there is no such node.
346    ///
347    /// The `host` parameter is the Android view for this adapter.
348    /// It must be an instance of `android.view.View` or a subclass.
349    pub fn find_focus<'local, H: ActivationHandler + ?Sized>(
350        &mut self,
351        activation_handler: &mut H,
352        env: &mut JNIEnv<'local>,
353        host: &JObject,
354        focus_type: jint,
355    ) -> JObject<'local> {
356        let virtual_view_id = match focus_type {
357            FOCUS_INPUT => {
358                let tree = self.state.get_or_init_tree(activation_handler);
359                let tree_state = tree.state();
360                let node = tree_state.focus_in_tree();
361                self.node_id_map.get_or_create_java_id(&node)
362            }
363            FOCUS_ACCESSIBILITY => {
364                let Some(id) = self.accessibility_focus else {
365                    return JObject::null();
366                };
367                id
368            }
369            _ => return JObject::null(),
370        };
371        self.create_accessibility_node_info(activation_handler, env, host, virtual_view_id)
372    }
373
374    fn perform_simple_action<H: ActionHandler + ?Sized>(
375        &mut self,
376        action_handler: &mut H,
377        virtual_view_id: jint,
378        action: jint,
379    ) -> Option<QueuedEvents> {
380        let tree = self.state.get_full_tree()?;
381        let tree_state = tree.state();
382        let target = if virtual_view_id == HOST_VIEW_ID {
383            tree_state.root_id()
384        } else {
385            self.node_id_map.get_accesskit_id(virtual_view_id)?
386        };
387        let (target_node, target_tree) = tree.locate_node(target)?;
388        let mut events = Vec::new();
389        let request = match action {
390            ACTION_CLICK => ActionRequest {
391                action: {
392                    let node = tree_state.node_by_id(target).unwrap();
393                    if node.is_focusable(&filter)
394                        && !node.is_focused()
395                        && !node.is_clickable(&filter)
396                    {
397                        Action::Focus
398                    } else {
399                        Action::Click
400                    }
401                },
402                target_tree,
403                target_node,
404                data: None,
405            },
406            ACTION_FOCUS => ActionRequest {
407                action: Action::Focus,
408                target_tree,
409                target_node,
410                data: None,
411            },
412            ACTION_SCROLL_BACKWARD | ACTION_SCROLL_FORWARD => ActionRequest {
413                action: {
414                    let node = tree_state.node_by_id(target).unwrap();
415                    if let Some(orientation) = node.orientation() {
416                        match orientation {
417                            Orientation::Horizontal => {
418                                if action == ACTION_SCROLL_BACKWARD {
419                                    Action::ScrollLeft
420                                } else {
421                                    Action::ScrollRight
422                                }
423                            }
424                            Orientation::Vertical => {
425                                if action == ACTION_SCROLL_BACKWARD {
426                                    Action::ScrollUp
427                                } else {
428                                    Action::ScrollDown
429                                }
430                            }
431                        }
432                    } else if action == ACTION_SCROLL_BACKWARD {
433                        if node.supports_action(Action::ScrollUp, &filter) {
434                            Action::ScrollUp
435                        } else {
436                            Action::ScrollLeft
437                        }
438                    } else if node.supports_action(Action::ScrollDown, &filter) {
439                        Action::ScrollDown
440                    } else {
441                        Action::ScrollRight
442                    }
443                },
444                target_tree,
445                target_node,
446                data: Some(ActionData::ScrollUnit(ScrollUnit::Page)),
447            },
448            ACTION_ACCESSIBILITY_FOCUS => {
449                self.accessibility_focus = Some(virtual_view_id);
450                events.push(QueuedEvent::InvalidateHost);
451                events.push(QueuedEvent::Simple {
452                    virtual_view_id,
453                    event_type: EVENT_VIEW_ACCESSIBILITY_FOCUSED,
454                });
455                return Some(QueuedEvents(events));
456            }
457            ACTION_CLEAR_ACCESSIBILITY_FOCUS => {
458                if self.accessibility_focus == Some(virtual_view_id) {
459                    self.accessibility_focus = None;
460                }
461                events.push(QueuedEvent::InvalidateHost);
462                events.push(QueuedEvent::Simple {
463                    virtual_view_id,
464                    event_type: EVENT_VIEW_ACCESSIBILITY_FOCUS_CLEARED,
465                });
466                return Some(QueuedEvents(events));
467            }
468            _ => {
469                return None;
470            }
471        };
472        action_handler.do_action(request);
473        if action == ACTION_CLICK {
474            events.push(QueuedEvent::Simple {
475                virtual_view_id,
476                event_type: EVENT_VIEW_CLICKED,
477            });
478        }
479        Some(QueuedEvents(events))
480    }
481
482    fn set_text_selection_common<H: ActionHandler + ?Sized, F, Extra>(
483        &mut self,
484        action_handler: &mut H,
485        events: &mut Vec<QueuedEvent>,
486        virtual_view_id: jint,
487        selection_factory: F,
488    ) -> Option<Extra>
489    where
490        for<'a> F: FnOnce(&'a Node<'a>) -> Option<(TextPosition<'a>, TextPosition<'a>, Extra)>,
491    {
492        let tree = self.state.get_full_tree()?;
493        let tree_state = tree.state();
494        let node = if virtual_view_id == HOST_VIEW_ID {
495            tree_state.root()
496        } else {
497            let id = self.node_id_map.get_accesskit_id(virtual_view_id)?;
498            tree_state.node_by_id(id).unwrap()
499        };
500        let (node_id, tree_id) = tree.locate_node(node.id())?;
501        let (focus_id, _) = tree.locate_node(tree_state.focus_id_in_tree())?;
502        // TalkBack expects the text selection change to take effect
503        // immediately, so we optimistically update the node.
504        // But don't be *too* optimistic.
505        if !node.supports_action(Action::SetTextSelection, &filter) {
506            return None;
507        }
508        let (anchor, focus, extra) = selection_factory(&node)?;
509        let selection = TextSelection {
510            anchor: anchor.to_raw(),
511            focus: focus.to_raw(),
512        };
513        let mut new_node = node.data().clone();
514        new_node.set_text_selection(selection);
515        let update = TreeUpdate {
516            nodes: vec![(node_id, new_node)],
517            tree: None,
518            tree_id,
519            focus: focus_id,
520        };
521        update_tree(events, &mut self.node_id_map, tree, update);
522        let request = ActionRequest {
523            action: Action::SetTextSelection,
524            target_tree: tree_id,
525            target_node: node_id,
526            data: Some(ActionData::SetTextSelection(selection)),
527        };
528        action_handler.do_action(request);
529        Some(extra)
530    }
531
532    fn set_text_selection<H: ActionHandler + ?Sized>(
533        &mut self,
534        action_handler: &mut H,
535        virtual_view_id: jint,
536        anchor: jint,
537        focus: jint,
538    ) -> Option<QueuedEvents> {
539        let mut events = Vec::new();
540        self.set_text_selection_common(action_handler, &mut events, virtual_view_id, |node| {
541            let anchor = usize::try_from(anchor).ok()?;
542            let anchor = node.text_position_from_global_utf16_index(anchor)?;
543            let focus = usize::try_from(focus).ok()?;
544            let focus = node.text_position_from_global_utf16_index(focus)?;
545            Some((anchor, focus, ()))
546        })?;
547        Some(QueuedEvents(events))
548    }
549
550    fn collapse_text_selection<H: ActionHandler + ?Sized>(
551        &mut self,
552        action_handler: &mut H,
553        virtual_view_id: jint,
554    ) -> Option<QueuedEvents> {
555        let mut events = Vec::new();
556        self.set_text_selection_common(action_handler, &mut events, virtual_view_id, |node| {
557            node.text_selection_focus().map(|pos| (pos, pos, ()))
558        })?;
559        Some(QueuedEvents(events))
560    }
561
562    fn traverse_text<H: ActionHandler + ?Sized>(
563        &mut self,
564        action_handler: &mut H,
565        virtual_view_id: jint,
566        granularity: jint,
567        forward: bool,
568        extend_selection: bool,
569    ) -> Option<QueuedEvents> {
570        let mut events = Vec::new();
571        let (segment_start, segment_end) =
572            self.set_text_selection_common(action_handler, &mut events, virtual_view_id, |node| {
573                let current = node.text_selection_focus().unwrap_or_else(|| {
574                    let range = node.document_range();
575                    if forward {
576                        range.start()
577                    } else {
578                        range.end()
579                    }
580                });
581                if (forward && current.is_document_end())
582                    || (!forward && current.is_document_start())
583                {
584                    return None;
585                }
586                let current = if forward {
587                    current.biased_to_start()
588                } else {
589                    current.biased_to_end()
590                };
591                let (segment_start, segment_end) = match granularity {
592                    MOVEMENT_GRANULARITY_CHARACTER => {
593                        if forward {
594                            (current, current.forward_to_character_end())
595                        } else {
596                            (current.backward_to_character_start(), current)
597                        }
598                    }
599                    MOVEMENT_GRANULARITY_WORD => {
600                        if forward {
601                            let start = if current.is_word_start() {
602                                current
603                            } else {
604                                let start = current.forward_to_word_start();
605                                if start.is_document_end() {
606                                    return None;
607                                }
608                                start
609                            };
610                            (start, start.forward_to_word_end())
611                        } else {
612                            let end = if current.is_line_end() || current.is_word_start() {
613                                current
614                            } else {
615                                let end = current.backward_to_word_start().biased_to_end();
616                                if end.is_document_start() {
617                                    return None;
618                                }
619                                end
620                            };
621                            (end.backward_to_word_start(), end)
622                        }
623                    }
624                    MOVEMENT_GRANULARITY_LINE => {
625                        if forward {
626                            let start = if current.is_line_start() {
627                                current
628                            } else {
629                                let start = current.forward_to_line_start();
630                                if start.is_document_end() {
631                                    return None;
632                                }
633                                start
634                            };
635                            (start, start.forward_to_line_end())
636                        } else {
637                            let end = if current.is_line_end() {
638                                current
639                            } else {
640                                let end = current.backward_to_line_start().biased_to_end();
641                                if end.is_document_start() {
642                                    return None;
643                                }
644                                end
645                            };
646                            (end.backward_to_line_start(), end)
647                        }
648                    }
649                    MOVEMENT_GRANULARITY_PARAGRAPH => {
650                        if forward {
651                            let mut start = current;
652                            while start.is_paragraph_separator() {
653                                start = start.forward_to_paragraph_start();
654                            }
655                            if start.is_document_end() {
656                                return None;
657                            }
658                            let mut end = start.forward_to_paragraph_end();
659                            let prev = end.backward_to_character_start();
660                            if prev.is_paragraph_separator() {
661                                end = prev;
662                            }
663                            (start, end)
664                        } else {
665                            let mut end = current;
666                            while !end.is_document_start()
667                                && end.backward_to_character_start().is_paragraph_separator()
668                            {
669                                end = end.backward_to_character_start();
670                            }
671                            if end.is_document_start() {
672                                return None;
673                            }
674                            (end.backward_to_paragraph_start(), end)
675                        }
676                    }
677                    _ => {
678                        return None;
679                    }
680                };
681                if segment_start == segment_end {
682                    return None;
683                }
684                let focus = if forward { segment_end } else { segment_start };
685                let anchor = if extend_selection {
686                    node.text_selection_anchor().unwrap_or({
687                        if forward {
688                            segment_start
689                        } else {
690                            segment_end
691                        }
692                    })
693                } else {
694                    focus
695                };
696                Some((
697                    anchor,
698                    focus,
699                    (
700                        segment_start.to_global_utf16_index(),
701                        segment_end.to_global_utf16_index(),
702                    ),
703                ))
704            })?;
705        events.push(QueuedEvent::TextTraversed {
706            virtual_view_id,
707            granularity,
708            forward,
709            segment_start: segment_start as jint,
710            segment_end: segment_end as jint,
711        });
712        Some(QueuedEvents(events))
713    }
714
715    /// Perform the specified accessibility action.
716    ///
717    /// If a [`QueuedEvents`] instance is returned, the caller must call
718    /// [`QueuedEvents::raise`] on it, and the Java `performAction` method
719    /// must return `true`. Otherwise, the Java `performAction` method
720    /// must either handle the action some other way or return `false`.
721    ///
722    /// This method may be safely called on any thread, but refer to
723    /// [`QueuedEvents::raise`] for restrictions on the context in which
724    /// it should be called.
725    pub fn perform_action<H: ActionHandler + ?Sized>(
726        &mut self,
727        action_handler: &mut H,
728        virtual_view_id: jint,
729        action: &PlatformAction,
730    ) -> Option<QueuedEvents> {
731        match action.0 {
732            PlatformActionInner::Simple { action } => {
733                self.perform_simple_action(action_handler, virtual_view_id, action)
734            }
735            PlatformActionInner::SetTextSelection { anchor, focus } => {
736                self.set_text_selection(action_handler, virtual_view_id, anchor, focus)
737            }
738            PlatformActionInner::CollapseTextSelection => {
739                self.collapse_text_selection(action_handler, virtual_view_id)
740            }
741            PlatformActionInner::TraverseText {
742                granularity,
743                forward,
744                extend_selection,
745            } => self.traverse_text(
746                action_handler,
747                virtual_view_id,
748                granularity,
749                forward,
750                extend_selection,
751            ),
752        }
753    }
754
755    fn virtual_view_at_point<H: ActivationHandler + ?Sized>(
756        &mut self,
757        activation_handler: &mut H,
758        x: jfloat,
759        y: jfloat,
760    ) -> Option<jint> {
761        let tree = self.state.get_or_init_tree(activation_handler);
762        let tree_state = tree.state();
763        let root = tree_state.root();
764        let point = Point::new(x.into(), y.into());
765        let point = root.transform().inverse() * point;
766        let node = root.node_at_point(point, &filter)?;
767        Some(self.node_id_map.get_or_create_java_id(&node))
768    }
769
770    /// Handle the provided hover event.
771    ///
772    /// The `action`, `x`, and `y` parameters must be retrieved from
773    /// the corresponding properties on an Android motion event. These
774    /// parameters are passed individually so you can use either a Java
775    /// or NDK event.
776    ///
777    /// If a [`QueuedEvents`] instance is returned, the caller must call
778    /// [`QueuedEvents::raise`] on it, and if using Java, the event handler
779    /// must return `true`. Otherwise, if using Java, the event handler
780    /// must either handle the event some other way or return `false`.
781    ///
782    /// This method may be safely called on any thread, but refer to
783    /// [`QueuedEvents::raise`] for restrictions on the context in which
784    /// it should be called.
785    pub fn on_hover_event<H: ActivationHandler + ?Sized>(
786        &mut self,
787        activation_handler: &mut H,
788        action: jint,
789        x: jfloat,
790        y: jfloat,
791    ) -> Option<QueuedEvents> {
792        let mut events = Vec::new();
793        match action {
794            MOTION_ACTION_HOVER_ENTER | MOTION_ACTION_HOVER_MOVE => {
795                let new_id = self.virtual_view_at_point(activation_handler, x, y);
796                if self.hover_target != new_id {
797                    if let Some(virtual_view_id) = new_id {
798                        events.push(QueuedEvent::Simple {
799                            virtual_view_id,
800                            event_type: EVENT_VIEW_HOVER_ENTER,
801                        });
802                    }
803                    if let Some(virtual_view_id) = self.hover_target {
804                        events.push(QueuedEvent::Simple {
805                            virtual_view_id,
806                            event_type: EVENT_VIEW_HOVER_EXIT,
807                        });
808                    }
809                    self.hover_target = new_id;
810                }
811            }
812            MOTION_ACTION_HOVER_EXIT => {
813                if let Some(virtual_view_id) = self.hover_target.take() {
814                    events.push(QueuedEvent::Simple {
815                        virtual_view_id,
816                        event_type: EVENT_VIEW_HOVER_EXIT,
817                    });
818                }
819            }
820            _ => return None,
821        }
822        Some(QueuedEvents(events))
823    }
824}