Skip to main content

ferroui_base/input/navigation/
xy_focus.rs

1use super::{
2    XYFocusAlgorithms, XYFocusHelpers, XYFocusManifolds, XYFocusNavigationModes, XYFocusNavigationStrategy,
3    XYFocusOptions,
4};
5use crate::input::{FocusManager, InputElement, KeyDeviceType, NavigationDirection};
6use crate::media::FlowDirection;
7use crate::utilities::MathUtilities;
8use crate::{
9    ferro_property, AttachedProperty, FerroProperty, Rect, Ref, StyledPropertyOptions, Visual,
10};
11use std::cell::{Cell, RefCell};
12use std::cmp::Ordering;
13
14type Element = Ref<InputElement>;
15
16/// A candidate of a directional focus search.
17struct XYFocusParams {
18    element: Element,
19    bounds: Rect,
20    score: f64,
21}
22
23crate::ferro_thread_local! {
24    static INSTANCE: XYFocus = XYFocus::new();
25}
26
27/// Directional (XY) focus navigation: the attached properties that
28/// configure it and the search for the next element in a direction.
29pub struct XYFocus {
30    manifolds: Cell<XYFocusManifolds>,
31    pooled_candidates: RefCell<Vec<XYFocusParams>>,
32}
33
34crate::ferro_static_type!(XYFocus);
35
36macro_rules! override_property {
37    ($(#[$meta:meta])* $property:ident, $name:literal, $get:ident, $set:ident) => {
38        ferro_property!(for XYFocus;
39            $(#[$meta])*
40            pub fn $property() -> AttachedProperty<Option<Ref<InputElement>>> {
41                FerroProperty::register_attached::<XYFocus, InputElement, _>($name, None)
42            }
43        );
44
45        pub fn $set(obj: &InputElement, value: Option<Ref<InputElement>>) {
46            obj.set_value(Self::$property(), value)
47        }
48
49        pub fn $get(obj: &InputElement) -> Option<Ref<InputElement>> {
50            obj.get_value(Self::$property())
51        }
52    };
53}
54
55macro_rules! strategy_property {
56    ($(#[$meta:meta])* $property:ident, $name:literal, $get:ident, $set:ident) => {
57        ferro_property!(for XYFocus;
58            $(#[$meta])*
59            pub fn $property() -> AttachedProperty<XYFocusNavigationStrategy> {
60                FerroProperty::register_attached_with::<XYFocus, InputElement, _>(
61                    $name,
62                    StyledPropertyOptions::new(XYFocusNavigationStrategy::Auto).inherits(true),
63                )
64            }
65        );
66
67        pub fn $set(obj: &InputElement, value: XYFocusNavigationStrategy) {
68            obj.set_value(Self::$property(), value)
69        }
70
71        pub fn $get(obj: &InputElement) -> XYFocusNavigationStrategy {
72            obj.get_value(Self::$property())
73        }
74    };
75}
76
77crate::ferro_properties! {
78    impl XYFocus, also [
79        XYFocus::down_property,
80        XYFocus::left_property,
81        XYFocus::right_property,
82        XYFocus::up_property,
83        XYFocus::down_navigation_strategy_property,
84        XYFocus::up_navigation_strategy_property,
85        XYFocus::left_navigation_strategy_property,
86        XYFocus::right_navigation_strategy_property,
87        XYFocus::navigation_modes_property,
88        XYFocus::is_focus_engagement_enabled_property,
89        XYFocus::is_focus_engaged_property,
90    ] {}
91}
92
93impl XYFocus {
94    // --- properties ---------------------------------------------------------
95
96    override_property!(
97        /// The object that gets focus when a user presses the Dpad down.
98        down_property, "Down", get_down, set_down
99    );
100
101    override_property!(
102        /// The object that gets focus when a user presses the Dpad left.
103        left_property, "Left", get_left, set_left
104    );
105
106    override_property!(
107        /// The object that gets focus when a user presses the Dpad right.
108        right_property, "Right", get_right, set_right
109    );
110
111    override_property!(
112        /// The object that gets focus when a user presses the Dpad up.
113        up_property, "Up", get_up, set_up
114    );
115
116    strategy_property!(
117        /// The strategy used to determine the target element of a down
118        /// navigation.
119        down_navigation_strategy_property, "DownNavigationStrategy",
120        get_down_navigation_strategy, set_down_navigation_strategy
121    );
122
123    strategy_property!(
124        /// The strategy used to determine the target element of an up
125        /// navigation.
126        up_navigation_strategy_property, "UpNavigationStrategy",
127        get_up_navigation_strategy, set_up_navigation_strategy
128    );
129
130    strategy_property!(
131        /// The strategy used to determine the target element of a left
132        /// navigation.
133        left_navigation_strategy_property, "LeftNavigationStrategy",
134        get_left_navigation_strategy, set_left_navigation_strategy
135    );
136
137    strategy_property!(
138        /// The strategy used to determine the target element of a right
139        /// navigation.
140        right_navigation_strategy_property, "RightNavigationStrategy",
141        get_right_navigation_strategy, set_right_navigation_strategy
142    );
143
144    ferro_property!(for XYFocus;
145        /// Defines the XY focus navigation modes that are enabled for the
146        /// element.
147        pub fn navigation_modes_property() -> AttachedProperty<XYFocusNavigationModes> {
148            FerroProperty::register_attached_with::<XYFocus, InputElement, _>(
149                "NavigationModes",
150                StyledPropertyOptions::new(XYFocusNavigationModes::GAMEPAD | XYFocusNavigationModes::REMOTE)
151                    .inherits(true),
152            )
153        }
154    );
155
156    pub fn set_navigation_modes(obj: &InputElement, value: XYFocusNavigationModes) {
157        obj.set_value(Self::navigation_modes_property(), value)
158    }
159
160    pub fn get_navigation_modes(obj: &InputElement) -> XYFocusNavigationModes {
161        obj.get_value(Self::navigation_modes_property())
162    }
163
164    ferro_property!(for XYFocus;
165        pub(crate) fn is_focus_engagement_enabled_property() -> AttachedProperty<bool> {
166            FerroProperty::register_attached::<XYFocus, InputElement, _>("IsFocusEngagementEnabled", false)
167        }
168    );
169
170    #[allow(dead_code)]
171    pub(crate) fn set_is_focus_engagement_enabled(obj: &InputElement, value: bool) {
172        obj.set_value(Self::is_focus_engagement_enabled_property(), value)
173    }
174
175    pub(crate) fn get_is_focus_engagement_enabled(obj: &InputElement) -> bool {
176        obj.get_value(Self::is_focus_engagement_enabled_property())
177    }
178
179    ferro_property!(for XYFocus;
180        pub(crate) fn is_focus_engaged_property() -> AttachedProperty<bool> {
181            FerroProperty::register_attached_with::<XYFocus, Visual, _>(
182                "IsFocusEngaged",
183                StyledPropertyOptions::new(false).coerce(|sender, value| {
184                    value
185                        && sender
186                            .downcast_ref::<InputElement>()
187                            .is_some_and(XYFocus::get_is_focus_engagement_enabled)
188                }),
189            )
190        }
191    );
192
193    #[allow(dead_code)]
194    pub(crate) fn set_is_focus_engaged(obj: &Visual, value: bool) {
195        obj.set_value(Self::is_focus_engaged_property(), value)
196    }
197
198    pub(crate) fn get_is_focus_engaged(obj: &Visual) -> bool {
199        obj.get_value(Self::is_focus_engaged_property())
200    }
201
202    // --- find elements ------------------------------------------------------
203
204    fn find_elements(
205        focus_list: &mut Vec<XYFocusParams>,
206        start_root: &InputElement,
207        current_element: Option<&Element>,
208        active_scroller: Option<&Element>,
209        ignore_clipping: bool,
210        input_key_device_type: Option<KeyDeviceType>,
211    ) {
212        let is_scrolling = active_scroller.is_some();
213        let Some(collection) = start_root.visual_children_snapshot() else { return };
214
215        for child in collection.iter() {
216            let Some(child) = child.cast::<InputElement>() else { continue };
217
218            let is_engagement_enabled_but_not_engaged =
219                Self::get_is_focus_engagement_enabled(&child) && !Self::get_is_focus_engaged(&child);
220
221            if Some(&child) != current_element && Self::is_valid_candidate(&child, input_key_device_type) {
222                if let Some(bounds) = Self::get_bounds_for_ranking(&child, ignore_clipping) {
223                    if is_scrolling {
224                        if Self::is_candidate_participating_in_scroll(&child, active_scroller)
225                            || !Self::is_occluded(&child, bounds)
226                            || Self::is_candidate_child_of_ancestor_scroller(&child, active_scroller)
227                        {
228                            focus_list.push(XYFocusParams { element: child.clone(), bounds, score: 0.0 });
229                        }
230                    } else {
231                        focus_list.push(XYFocusParams { element: child.clone(), bounds, score: 0.0 });
232                    }
233                }
234            }
235
236            if Self::is_valid_focus_subtree(&child) && !is_engagement_enabled_but_not_engaged {
237                Self::find_elements(
238                    focus_list,
239                    &child,
240                    current_element,
241                    active_scroller,
242                    ignore_clipping,
243                    input_key_device_type,
244                );
245            }
246        }
247    }
248
249    fn is_valid_focus_subtree(candidate: &InputElement) -> bool {
250        // We don't need to check for effective values, as we've already
251        // checked parents of this subtree on previous steps.
252        candidate.is_visible() && candidate.is_enabled()
253    }
254
255    fn is_valid_candidate(candidate: &InputElement, input_key_device_type: Option<KeyDeviceType>) -> bool {
256        candidate.focusable()
257            && candidate.is_effectively_enabled()
258            && candidate.is_effectively_visible()
259            // Only allow candidate focus, if original key device type could
260            // focus it.
261            && XYFocusHelpers::is_allowed_xy_navigation_mode(candidate, input_key_device_type)
262    }
263
264    fn is_candidate_participating_in_scroll(candidate: &Element, active_scroller: Option<&Element>) -> bool {
265        let Some(active_scroller) = active_scroller else { return false };
266
267        let closest_scroller = candidate.find_ancestor_of_type_where::<InputElement>(true, |e| e.as_scrollable().is_some());
268        closest_scroller.as_ref() == Some(active_scroller)
269    }
270
271    fn is_candidate_child_of_ancestor_scroller(candidate: &Element, active_scroller: Option<&Element>) -> bool {
272        let Some(active_scroller) = active_scroller else { return false };
273
274        let mut parent = active_scroller.parent();
275        while let Some(current) = parent {
276            if let Some(element) = current.downcast_ref::<InputElement>() {
277                if element.as_scrollable().is_some() && element.is_visual_ancestor_of(candidate) {
278                    return true;
279                }
280            }
281            parent = current.parent();
282        }
283
284        false
285    }
286
287    fn is_occluded(element: &InputElement, element_bounds: Rect) -> bool {
288        let Some(root) = element.visual_root() else { return true };
289
290        // Check if the element is within the visible area of the window
291        let visible_bounds = Rect::new(0.0, 0.0, root.bounds().width, root.bounds().height);
292
293        !visible_bounds.intersects(element_bounds)
294    }
295
296    pub(crate) fn get_bounds_for_ranking(element: &InputElement, ignore_clipping: bool) -> Option<Rect> {
297        element.get_transformed_bounds().map(|bounds| {
298            if ignore_clipping {
299                bounds.bounds.transform_to_aabb(bounds.transform)
300            } else {
301                bounds.clip
302            }
303        })
304    }
305
306    // --- bubbling -----------------------------------------------------------
307
308    fn get_direction_override(
309        element: &InputElement,
310        search_root: Option<&Element>,
311        direction: NavigationDirection,
312        ignore_focusability: bool,
313    ) -> Option<Element> {
314        let index = Self::get_xy_focus_property_index(element, direction)?;
315        let override_element = element.get_value(index)?;
316
317        if !ignore_focusability && !FocusManager::can_focus(&override_element) {
318            return None;
319        }
320
321        // If an override was specified but it is located outside the
322        // search root, don't use it as the candidate.
323        if let Some(search_root) = search_root {
324            if !search_root.is_visual_ancestor_of(&override_element) {
325                return None;
326            }
327        }
328
329        Some(override_element)
330    }
331
332    fn try_xy_focus_bubble(
333        element: &Element,
334        candidate: Option<Element>,
335        search_root: Option<&Element>,
336        direction: NavigationDirection,
337    ) -> Option<Element> {
338        let candidate = candidate?;
339        let mut next_focusable_element = candidate.clone();
340
341        if let Some(direction_override_root) = Self::get_direction_override_root(element, search_root, direction) {
342            let is_ancestor = direction_override_root.is_visual_ancestor_of(&candidate);
343            if !is_ancestor {
344                if let Some(overridden) =
345                    Self::get_direction_override(&direction_override_root, search_root, direction, false)
346                {
347                    next_focusable_element = overridden;
348                }
349            }
350        }
351
352        Some(next_focusable_element)
353    }
354
355    fn get_direction_override_root(
356        element: &Element,
357        search_root: Option<&Element>,
358        direction: NavigationDirection,
359    ) -> Option<Element> {
360        let mut root = Some(element.clone());
361
362        while let Some(current) = &root {
363            if Self::get_direction_override(current, search_root, direction, false).is_some() {
364                break;
365            }
366            root = current.get_visual_parent_of_type::<InputElement>();
367        }
368
369        root
370    }
371
372    fn get_strategy(
373        element: &Element,
374        direction: NavigationDirection,
375        navigation_strategy_override: Option<XYFocusNavigationStrategy>,
376    ) -> XYFocusNavigationStrategy {
377        let is_auto_override = navigation_strategy_override == Some(XYFocusNavigationStrategy::Auto);
378        let mut element = element.clone();
379
380        if let Some(strategy) = navigation_strategy_override.filter(|_| !is_auto_override) {
381            // The override is converted by offsetting its value, as
382            // upstream does.
383            return match strategy as i32 - 1 {
384                1 => XYFocusNavigationStrategy::Projection,
385                2 => XYFocusNavigationStrategy::NavigationDirectionDistance,
386                3 => XYFocusNavigationStrategy::RectilinearDistance,
387                _ => XYFocusNavigationStrategy::Auto,
388            };
389        } else if is_auto_override {
390            // Skip the element if we have an auto override and look at its
391            // parent's strategy
392            if let Some(parent) = element.get_visual_parent_of_type::<InputElement>() {
393                element = parent;
394            }
395        }
396
397        if let Some(index) = Self::get_xy_focus_navigation_strategy_property_index(&element, direction) {
398            let mut current = Some(element);
399            while let Some(c) = current {
400                let mode = c.get_value(index);
401                if mode != XYFocusNavigationStrategy::Auto {
402                    return mode;
403                }
404
405                current = c.get_visual_parent_of_type::<InputElement>();
406            }
407        }
408
409        XYFocusNavigationStrategy::Projection
410    }
411
412    fn flip_for_flow_direction(element: &InputElement, direction: NavigationDirection) -> NavigationDirection {
413        if element.flow_direction() == FlowDirection::RightToLeft {
414            match direction {
415                NavigationDirection::Left => return NavigationDirection::Right,
416                NavigationDirection::Right => return NavigationDirection::Left,
417                _ => {}
418            }
419        }
420        direction
421    }
422
423    fn get_xy_focus_property_index(
424        element: &InputElement,
425        direction: NavigationDirection,
426    ) -> Option<&'static AttachedProperty<Option<Ref<InputElement>>>> {
427        match Self::flip_for_flow_direction(element, direction) {
428            NavigationDirection::Left => Some(Self::left_property()),
429            NavigationDirection::Right => Some(Self::right_property()),
430            NavigationDirection::Up => Some(Self::up_property()),
431            NavigationDirection::Down => Some(Self::down_property()),
432            _ => None,
433        }
434    }
435
436    fn get_xy_focus_navigation_strategy_property_index(
437        element: &InputElement,
438        direction: NavigationDirection,
439    ) -> Option<&'static AttachedProperty<XYFocusNavigationStrategy>> {
440        match Self::flip_for_flow_direction(element, direction) {
441            NavigationDirection::Left => Some(Self::left_navigation_strategy_property()),
442            NavigationDirection::Right => Some(Self::right_navigation_strategy_property()),
443            NavigationDirection::Up => Some(Self::up_navigation_strategy_property()),
444            NavigationDirection::Down => Some(Self::down_navigation_strategy_property()),
445            _ => None,
446        }
447    }
448
449    // --- implementation -----------------------------------------------------
450
451    pub(crate) fn new() -> Self {
452        Self { manifolds: Cell::new(XYFocusManifolds::new()), pooled_candidates: RefCell::new(Vec::new()) }
453    }
454
455    pub(crate) fn set_manifolds_from_bounds(&self, bounds: Rect) {
456        self.manifolds.set(XYFocusManifolds {
457            v_manifold: (bounds.left(), bounds.right()),
458            h_manifold: (bounds.top(), bounds.bottom()),
459        });
460    }
461
462    pub(crate) fn update_manifolds(
463        &self,
464        direction: NavigationDirection,
465        element_bounds: Rect,
466        candidate: &InputElement,
467        ignore_clipping: bool,
468    ) {
469        let candidate_bounds = Self::get_bounds_for_ranking(candidate, ignore_clipping)
470            .expect("the candidate of a completed focus change has bounds");
471        let mut manifolds = self.manifolds.get();
472        XYFocusAlgorithms::update_manifolds(direction, element_bounds, candidate_bounds, &mut manifolds);
473        self.manifolds.set(manifolds);
474    }
475
476    /// Finds the element directional navigation moves to from `element`,
477    /// always using the bounds of the current element as the manifolds.
478    pub(crate) fn try_directional_focus(
479        direction: NavigationDirection,
480        element: &Element,
481        _owner: Option<&Element>,
482        engaged_control: Option<&Element>,
483        key_device_type: Option<KeyDeviceType>,
484    ) -> Option<Element> {
485        if !XYFocusHelpers::is_allowed_xy_navigation_mode(element, key_device_type) {
486            return None;
487        }
488
489        let bounds = Self::get_bounds_for_ranking(element, true)?;
490        let search_root = XYFocusHelpers::find_xy_search_root(element, key_device_type);
491
492        INSTANCE.with(|instance| {
493            instance.set_manifolds_from_bounds(bounds);
494
495            let mut options = XYFocusOptions::new();
496            options.key_device_type = key_device_type;
497            options.focused_element_bounds = Some(bounds);
498            options.update_manifold = true;
499            options.search_root = Some(search_root);
500
501            instance.get_next_focusable_element(direction, Some(element), engaged_control, true, &options)
502        })
503    }
504
505    pub(crate) fn get_next_focusable_element(
506        &self,
507        direction: NavigationDirection,
508        element: Option<&Element>,
509        engaged_control: Option<&Element>,
510        update_manifolds: bool,
511        xy_focus_options: &XYFocusOptions,
512    ) -> Option<Element> {
513        let element = element?;
514        let root = element.visual_root().and_then(|root| root.downcast::<InputElement>().ok())?;
515
516        let is_right_to_left = element.flow_direction() == FlowDirection::RightToLeft;
517        let mode = Self::get_strategy(element, direction, xy_focus_options.navigation_strategy_override);
518
519        let mut focused_element_bounds =
520            xy_focus_options.focused_element_bounds.expect("FocusedElementBounds needs to be set");
521
522        let search_root = xy_focus_options.search_root.as_ref();
523
524        if let Some(next) = Self::get_direction_override(element, search_root, direction, true) {
525            return Some(next);
526        }
527
528        let active_scroller = Self::get_active_scroller_for_scroll(direction, element);
529        let is_processing_input_for_scroll = active_scroller.is_some();
530        let mut element = Some(element);
531
532        if let Some(hint) = xy_focus_options.focus_hint_rectangle {
533            focused_element_bounds = hint;
534            element = None;
535        }
536
537        let ignore_clipping = xy_focus_options.ignore_clipping;
538        let root_bounds = match (engaged_control, search_root) {
539            (Some(engaged), _) => Self::get_bounds_for_ranking(engaged, ignore_clipping),
540            (None, Some(search_root)) => Self::get_bounds_for_ranking(search_root, ignore_clipping),
541            (None, None) => Self::get_bounds_for_ranking(&root, ignore_clipping),
542        }
543        .unwrap_or_else(|| root.bounds());
544
545        // The candidate list is reused between searches; a nested search
546        // (from user code run by a property getter) gets its own.
547        let mut candidate_list = self.pooled_candidates.try_borrow_mut().map(|mut list| std::mem::take(&mut *list)).unwrap_or_default();
548        let mut next_focusable_element = None;
549
550        Self::get_all_valid_focusable_children(
551            &mut candidate_list,
552            &root,
553            element,
554            engaged_control,
555            search_root,
556            active_scroller.as_ref(),
557            ignore_clipping,
558            xy_focus_options.key_device_type,
559        );
560
561        if !candidate_list.is_empty() {
562            let mut max_root_bounds_distance =
563                (root_bounds.right() - root_bounds.left()).max(root_bounds.bottom() - root_bounds.top());
564            max_root_bounds_distance = max_root_bounds_distance.max(Self::get_max_root_bounds_distance(
565                &candidate_list,
566                focused_element_bounds,
567                direction,
568            ));
569
570            self.rank_elements(
571                &mut candidate_list,
572                direction,
573                focused_element_bounds,
574                max_root_bounds_distance,
575                mode,
576                xy_focus_options.exclusion_rect,
577                xy_focus_options.ignore_cone,
578            );
579
580            let ignore_occlusivity = xy_focus_options.ignore_occlusivity || is_processing_input_for_scroll;
581
582            // Choose the best candidate, after testing for occlusivity; if
583            // we're currently scrolling, the test has been done already,
584            // skip it.
585            next_focusable_element = self.choose_best_focusable_element_from_list(
586                &mut candidate_list,
587                direction,
588                focused_element_bounds,
589                ignore_clipping,
590                ignore_occlusivity,
591                is_right_to_left,
592                xy_focus_options.update_manifold && update_manifolds,
593            );
594
595            if let Some(element) = element {
596                next_focusable_element =
597                    Self::try_xy_focus_bubble(element, next_focusable_element, search_root, direction);
598            }
599        }
600
601        candidate_list.clear();
602        if let Ok(mut pooled) = self.pooled_candidates.try_borrow_mut() {
603            *pooled = candidate_list;
604        }
605
606        next_focusable_element
607    }
608
609    #[allow(clippy::too_many_arguments)]
610    fn choose_best_focusable_element_from_list(
611        &self,
612        score_list: &mut [XYFocusParams],
613        direction: NavigationDirection,
614        bounds: Rect,
615        ignore_clipping: bool,
616        ignore_occlusivity: bool,
617        is_right_to_left: bool,
618        update_manifolds: bool,
619    ) -> Option<Element> {
620        let compare = |a: f64, b: f64| a.partial_cmp(&b).unwrap_or(Ordering::Equal);
621
622        score_list.sort_by(|element_a, element_b| {
623            if element_a.element == element_b.element {
624                return Ordering::Equal;
625            }
626
627            let compared = compare(element_b.score, element_a.score);
628            if compared == Ordering::Equal {
629                let first_bounds = element_a.bounds;
630                let second_bounds = element_b.bounds;
631
632                if first_bounds == second_bounds {
633                    Ordering::Equal
634                } else if direction == NavigationDirection::Up || direction == NavigationDirection::Down {
635                    if is_right_to_left {
636                        compare(second_bounds.left(), first_bounds.left())
637                    } else {
638                        compare(first_bounds.left(), second_bounds.left())
639                    }
640                } else {
641                    compare(first_bounds.top(), second_bounds.top())
642                }
643            } else {
644                compared
645            }
646        });
647
648        for param in score_list.iter() {
649            if param.score <= 0.0 {
650                break;
651            }
652
653            let bounds_for_occ_testing = if ignore_clipping {
654                Self::get_bounds_for_ranking(&param.element, false).unwrap_or(param.bounds)
655            } else {
656                param.bounds
657            };
658
659            // Don't check for occlusivity if we've already covered
660            // occlusivity scenarios for scrollable content or have been
661            // asked to ignore occlusivity by the caller.
662            if (param.bounds.x - f64::MAX).abs() > MathUtilities::DOUBLE_EPSILON
663                && (ignore_occlusivity || !Self::is_occluded(&param.element, bounds_for_occ_testing))
664            {
665                if update_manifolds {
666                    // Update the manifolds with the newly selected focus
667                    let mut manifolds = self.manifolds.get();
668                    XYFocusAlgorithms::update_manifolds(direction, bounds, param.bounds, &mut manifolds);
669                    self.manifolds.set(manifolds);
670                }
671
672                return Some(param.element.clone());
673            }
674        }
675
676        None
677    }
678
679    #[allow(clippy::too_many_arguments)]
680    fn get_all_valid_focusable_children(
681        candidate_list: &mut Vec<XYFocusParams>,
682        start_root: &Element,
683        current_element: Option<&Element>,
684        engaged_control: Option<&Element>,
685        search_scope: Option<&Element>,
686        active_scroller: Option<&Element>,
687        ignore_clipping: bool,
688        input_key_device_type: Option<KeyDeviceType>,
689    ) {
690        // If asked to scope the search within the given container, honor it
691        // without any exceptions
692        let root_for_tree_walk = search_scope.unwrap_or(start_root);
693
694        match engaged_control {
695            None => Self::find_elements(
696                candidate_list,
697                root_for_tree_walk,
698                current_element,
699                active_scroller,
700                ignore_clipping,
701                input_key_device_type,
702            ),
703            Some(engaged_control) => {
704                // Only run through this when you are an engaged element.
705                // Being an engaged element means that you should only look
706                // at the children of the engaged element.
707                Self::find_elements(
708                    candidate_list,
709                    engaged_control,
710                    current_element,
711                    active_scroller,
712                    ignore_clipping,
713                    input_key_device_type,
714                );
715
716                if current_element != Some(engaged_control) {
717                    if let Some(bounds) = Self::get_bounds_for_ranking(engaged_control, ignore_clipping) {
718                        candidate_list.push(XYFocusParams { element: engaged_control.clone(), bounds, score: 0.0 });
719                    }
720                }
721            }
722        }
723    }
724
725    #[allow(clippy::too_many_arguments)]
726    fn rank_elements(
727        &self,
728        candidate_list: &mut [XYFocusParams],
729        direction: NavigationDirection,
730        bounds: Rect,
731        max_root_bounds_distance: f64,
732        mode: XYFocusNavigationStrategy,
733        exclusion_bounds: Rect,
734        ignore_cone: bool,
735    ) {
736        let manifolds = self.manifolds.get();
737
738        for candidate in candidate_list.iter_mut() {
739            let candidate_bounds = candidate.bounds;
740
741            if !(exclusion_bounds.intersects(candidate_bounds) || exclusion_bounds.contains_rect(candidate_bounds)) {
742                if mode == XYFocusNavigationStrategy::Projection
743                    && XYFocusAlgorithms::should_candidate_be_considered_for_ranking(
744                        bounds,
745                        candidate_bounds,
746                        max_root_bounds_distance,
747                        direction,
748                        exclusion_bounds,
749                        ignore_cone,
750                    )
751                {
752                    candidate.score = XYFocusAlgorithms::get_score_projection(
753                        direction,
754                        bounds,
755                        candidate_bounds,
756                        &manifolds,
757                        max_root_bounds_distance,
758                    );
759                } else if mode == XYFocusNavigationStrategy::NavigationDirectionDistance
760                    || mode == XYFocusNavigationStrategy::RectilinearDistance
761                {
762                    candidate.score = XYFocusAlgorithms::get_score_proximity(
763                        direction,
764                        bounds,
765                        candidate_bounds,
766                        max_root_bounds_distance,
767                        mode == XYFocusNavigationStrategy::RectilinearDistance,
768                    );
769                }
770            }
771        }
772    }
773
774    fn get_max_root_bounds_distance(list: &[XYFocusParams], bounds: Rect, direction: NavigationDirection) -> f64 {
775        let mut max_element = &list[0];
776        let mut max_value = f64::MIN;
777
778        for param in list {
779            let candidate_bounds = param.bounds;
780            let value = match direction {
781                NavigationDirection::Left => candidate_bounds.left(),
782                NavigationDirection::Right => candidate_bounds.right(),
783                NavigationDirection::Up => candidate_bounds.top(),
784                NavigationDirection::Down => candidate_bounds.bottom(),
785                _ => 0.0,
786            };
787
788            if value > max_value {
789                max_value = value;
790                max_element = param;
791            }
792        }
793
794        let max_bounds = max_element.bounds;
795        match direction {
796            NavigationDirection::Left => (max_bounds.right() - bounds.left()).abs(),
797            NavigationDirection::Right => (bounds.right() - max_bounds.left()).abs(),
798            NavigationDirection::Up => (bounds.bottom() - max_bounds.top()).abs(),
799            NavigationDirection::Down => (max_bounds.bottom() - bounds.top()).abs(),
800            _ => 0.0,
801        }
802    }
803
804    fn get_active_scroller_for_scroll(direction: NavigationDirection, focused_element: &Element) -> Option<Element> {
805        let mut parent = Some(focused_element.clone());
806
807        while let Some(element) = parent {
808            if let Some(scrollable) = element.as_scrollable() {
809                let is_horizontally_scrollable_for_direction =
810                    matches!(direction, NavigationDirection::Left | NavigationDirection::Right)
811                        && scrollable.can_horizontally_scroll();
812                let is_vertically_scrollable_for_direction =
813                    matches!(direction, NavigationDirection::Up | NavigationDirection::Down)
814                        && scrollable.can_vertically_scroll();
815
816                if is_horizontally_scrollable_for_direction || is_vertically_scrollable_for_direction {
817                    return Some(element);
818                }
819            }
820
821            parent = element.get_visual_parent_of_type::<InputElement>();
822        }
823
824        None
825    }
826}