bevy_ui_navigation/
resolve.rs

1//! The resolution algorithm for the navigation system.
2//!
3//! # Overview
4//!
5//! This module defines two systems:
6//! 1. [`listen_nav_requests`]: the system gathering [`NavRequest`] and running
7//!    the [`resolve`] algorithm on them, updating the [`Focusable`] states and
8//!    sending [`NavEvent`] as result.
9//! 2. [`insert_tree_menus`]: system responsible to convert [`MenuBuilder`] defined
10//!    in [`crate::menu`] into [`TreeMenu`], which is the component used by
11//!    the resolution algorithm.
12//!
13//! The module also defines the [`Focusable`] component (also used in the
14//! resolution algorithm) and its fields.
15//!
16//! The bulk of the resolution algorithm is implemented in [`resolve`],
17//! delegating some abstract tasks to helper functions, of which:
18//! * [`parent_menu`]
19//! * [`ChildQueries::focusables_of`]
20//! * [`child_menu`]
21//! * [`focus_deep`]
22//! * [`MenuNavigationStrategy::resolve_2d`]
23//! * [`resolve_scope`]
24//!
25//! A trait [`MenuNavigationStrategy`] allows user-defined movements
26//! through a custom system parameter by implementing `resolve_2d`.
27//!
28//! We define some `SystemParam`:
29//! * [`ChildQueries`]: queries used to find the focusable children of a given entity.
30//! * [`NavQueries`]: All **immutable** queries used by the resolution algorithm.
31//! * [`MutQueries`]: Queries with mutable access to [`Focusable`] and [`TreeMenu`]
32//!   for updating them in [`listen_nav_requests`].
33//! * [`UiProjectionQuery`]: A default implementation of [`MenuNavigationStrategy`]
34//!   for `bevy_ui`.
35//!
36//! [`listen_nav_requests`] uses a `ParamSet` to access the focusables immutably for
37//! navigation resolution and mutably for updating them with the new navigation state.
38use std::num::NonZeroUsize;
39
40#[cfg(feature = "bevy_reflect")]
41use bevy::ecs::reflect::{ReflectComponent, ReflectResource};
42use bevy::hierarchy::{Children, Parent};
43use bevy::log::{debug, warn};
44use bevy::prelude::{Changed, FromWorld};
45#[cfg(feature = "bevy_reflect")]
46use bevy::reflect::Reflect;
47use bevy::{
48    ecs::{
49        event::{EventReader, EventWriter},
50        prelude::{Commands, Component, Entity, ParamSet, Query, ResMut, With, Without},
51        system::{Resource, StaticSystemParam, SystemParam, SystemParamItem},
52    },
53    math::Vec2,
54};
55#[cfg(feature = "bevy_ui")]
56use bevy::{
57    math::Vec3Swizzles,
58    prelude::{GlobalTransform, Res},
59    utils::FloatOrd,
60};
61
62use non_empty_vec::NonEmpty;
63
64use crate::{
65    commands::set_focus_state,
66    events::{self, NavEvent, NavRequest},
67    menu::{MenuBuilder, MenuSetting},
68};
69
70/// System parameter used to resolve movement and cycling focus updates.
71///
72/// This is useful if you don't want to depend
73/// on bevy's `GlobalTransform` for your UI,
74/// or want to implement your own navigation algorithm.
75/// For example, if you want your ui to be 3d elements in the world.
76pub trait MenuNavigationStrategy {
77    /// Which [`Entity`] in `siblings` can be reached
78    /// from `focused` in `direction` if any, otherwise `None`.
79    ///
80    /// * `focused`: The currently focused entity in the menu
81    /// * `direction`: The direction in which the focus should move
82    /// * `cycles`: Whether the navigation should loop
83    /// * `sibligns`: All the other focusable entities in this menu
84    ///
85    /// Note that `focused` appears once in `siblings`.
86    fn resolve_2d<'a>(
87        &self,
88        focused: Entity,
89        direction: events::Direction,
90        cycles: bool,
91        siblings: &'a [Entity],
92    ) -> Option<&'a Entity>;
93}
94
95/// A rectangle to specify the [`ScreenBoundaries`],
96/// useful for 2d navigation wrapping.
97#[derive(Default, Debug, Clone, Copy)]
98#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
99pub struct Rect {
100    /// The higher `x,y` coordinate of the `Rect`.
101    pub max: Vec2,
102    /// The lower `x,y` coordinate of the `Rect`.
103    pub min: Vec2,
104}
105/// Specify the boundaries of the screen when using 2d wrapping navigation.
106///
107/// This will be used in the default [`MenuNavigationStrategy`].
108///
109/// **NOTE**: This is deprecated since `bevy_ui` doesn't support moving
110/// the UI camera anymore.
111#[derive(Default, Debug, Clone, Copy, Resource)]
112#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Resource))]
113pub struct ScreenBoundaries {
114    /// Position of the camera.
115    pub position: Vec2,
116    /// The borders of the camera.
117    pub screen_edge: Rect,
118    /// The zoom level of the camera.
119    pub scale: f32,
120}
121
122#[derive(SystemParam)]
123pub(crate) struct ChildQueries<'w, 's> {
124    children: Query<'w, 's, &'static Children>,
125    is_focusable: Query<'w, 's, &'static Focusable>,
126    is_menu: Query<'w, 's, With<MenuSetting>>,
127}
128
129/// System parameter for the default cursor navigation system.
130///
131/// It uses the bevy [`GlobalTransform`] to compute relative positions
132/// and change focus to the correct entity.
133/// It uses the [`ScreenBoundaries`] resource to compute screen boundaries
134/// and move the cursor accordingly when it reaches a screen border
135/// in a cycling menu.
136#[cfg(feature = "bevy_ui")]
137#[derive(SystemParam)]
138pub struct UiProjectionQuery<'w, 's> {
139    boundaries: Option<Res<'w, ScreenBoundaries>>,
140    transforms: Query<'w, 's, &'static GlobalTransform>,
141}
142
143/// Collection of queries to manage the navigation tree.
144#[allow(clippy::type_complexity)]
145#[derive(SystemParam)]
146pub(crate) struct NavQueries<'w, 's> {
147    pub(crate) children: ChildQueries<'w, 's>,
148    parents: Query<'w, 's, &'static Parent>,
149    focusables: Query<'w, 's, (Entity, &'static Focusable), Without<TreeMenu>>,
150    menus: Query<'w, 's, (Entity, &'static TreeMenu, &'static MenuSetting), Without<Focusable>>,
151}
152impl<'w, 's> NavQueries<'w, 's> {
153    fn active_menu(
154        &self,
155        mut entity: Entity,
156        mut active_child: Entity,
157    ) -> Option<(Entity, Entity)> {
158        let mut repeated = false;
159        loop {
160            let mut go_down_one_menu = || {
161                let (_, focus) = self.focusables.get(active_child).ok()?;
162                if focus.state() != FocusState::Active {
163                    return None;
164                }
165                let (new_menu_entity, child_menu, _) = child_menu(active_child, self)?;
166                repeated = true;
167                entity = new_menu_entity;
168                active_child = child_menu.active_child;
169                Some(())
170            };
171            match go_down_one_menu() {
172                Some(()) => {}
173                None if !repeated => return None,
174                None => return Some((entity, active_child)),
175            }
176        }
177    }
178
179    /// The [`TreeMenu`] containing `focusable`, if any.
180    pub(crate) fn parent_menu(&self, focusable: Entity) -> Option<(Entity, TreeMenu, MenuSetting)> {
181        let parent = self.parents.get(focusable).ok()?.get();
182        match self.menus.get(parent) {
183            Ok((_, tree, setting)) => Some((parent, tree.clone(), *setting)),
184            Err(_) => self.parent_menu(parent),
185        }
186    }
187
188    // TODO: worst case this iterates 3 times through list of focusables and once menus.
189    // Could be improved to a single pass.
190    fn pick_first_focused(&self) -> Option<Entity> {
191        use FocusState::{Blocked, Focused, Inert};
192        let iter_focused = || self.focusables.iter().filter(|f| f.1.state() != Blocked);
193        let root_menu = || {
194            self.menus
195                .iter()
196                .find(|(_, menu, _)| menu.focus_parent.is_none())
197        };
198        let any_in_menu = |entity, active_child| {
199            match self.focusables.get(active_child) {
200                Ok((entity, _)) => Some(entity),
201                // TODO: non-Inert non-active_child
202                Err(_) => self.children.focusables_of(entity).first().copied(),
203            }
204        };
205        let any_in_active = || {
206            let (root_menu_entity, menu, _) = root_menu()?;
207            let (active_menu_entity, active) =
208                self.active_menu(root_menu_entity, menu.active_child)?;
209            any_in_menu(active_menu_entity, active)
210        };
211        let any_in_root = || {
212            let (root_menu_entity, menu, _) = root_menu()?;
213            any_in_menu(root_menu_entity, menu.active_child)
214        };
215        let any_prioritized =
216            || iter_focused().find_map(|(e, focus)| (focus.state != Inert).then(|| e));
217        let fallback = || iter_focused().next().map(|(fo, _)| fo);
218        let focused = iter_focused().find_map(|(fo, focus)| (focus.state == Focused).then(|| fo));
219
220        focused
221            .or_else(any_in_active)
222            .or_else(any_prioritized)
223            .or_else(any_in_root)
224            .or_else(fallback)
225    }
226
227    fn root_path(&self, mut from: Entity) -> NonEmpty<Entity> {
228        let mut ret = NonEmpty::new(from);
229        loop {
230            from = match self.parent_menu(from) {
231                // purely personal preference over deeply nested pattern match
232                Some((_, menu, _)) if menu.focus_parent.is_some() => menu.focus_parent.unwrap(),
233                _ => return ret,
234            };
235            assert!(
236                !ret.contains(&from),
237                "Navigation graph cycle detected! This panic has prevented a stack \
238                overflow, please check usages of `MenuBuilder::Entity/NamedParent`"
239            );
240            ret.push(from);
241        }
242    }
243}
244
245/// Queries [`Focusable`] and [`TreeMenu`] in a mutable way.
246#[derive(SystemParam)]
247pub(crate) struct MutQueries<'w, 's> {
248    commands: Commands<'w, 's>,
249    parents: Query<'w, 's, &'static Parent>,
250    focusables: Query<'w, 's, &'static mut Focusable, Without<TreeMenu>>,
251    menus: Query<'w, 's, &'static mut TreeMenu, Without<Focusable>>,
252}
253impl<'w, 's> MutQueries<'w, 's> {
254    /// Set the [`active_child`](TreeMenu::active_child) field of the enclosing
255    /// [`TreeMenu`] and disables the previous one.
256    fn set_active_child(&mut self, child: Entity) {
257        let mut focusable = child;
258        let mut nav_menu = loop {
259            // Find the enclosing parent menu.
260            if let Ok(parent) = self.parents.get(focusable) {
261                let parent = parent.get();
262                focusable = parent;
263                if let Ok(menu) = self.menus.get_mut(parent) {
264                    break menu;
265                }
266            } else {
267                return;
268            }
269        };
270        let entity = nav_menu.active_child;
271        nav_menu.active_child = child;
272        self.set_entity_focus(entity, FocusState::Inert);
273    }
274
275    fn set_entity_focus(&mut self, entity: Entity, state: FocusState) {
276        if let Ok(mut focusable) = self.focusables.get_mut(entity) {
277            focusable.state = state;
278            self.commands.add(set_focus_state(entity, state));
279        }
280    }
281
282    /// Change focus state of relevant entities.
283    fn update_focus(&mut self, from: &[Entity], to: &NonEmpty<Entity>) -> Entity {
284        use FocusState as Fs;
285
286        if to.as_slice() == from {
287            return *to.first();
288        }
289        let (disable, put_to_sleep) = from
290            .split_last()
291            .map_or((None, from), |(tail, heads)| (Some(tail), heads));
292        if let Some(disable) = disable {
293            self.set_entity_focus(*disable, Fs::Inert);
294        }
295        for &entity in put_to_sleep {
296            self.set_entity_focus(entity, Fs::Prioritized);
297        }
298        let (&focus, activate) = to.split_first();
299        self.set_active_child(focus);
300        self.set_entity_focus(focus, Fs::Focused);
301        for &entity in activate {
302            self.set_active_child(entity);
303            self.set_entity_focus(entity, Fs::Active);
304        }
305        focus
306    }
307}
308
309/// State of a [`Focusable`].
310#[derive(Copy, Clone, PartialEq, Eq, Debug)]
311#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
312pub enum FocusState {
313    /// An entity that was previously [`FocusState::Active`]
314    /// from a branch of the menu tree that is currently not _focused_.
315    /// When focus comes back to the [`MenuSetting`] containing this [`Focusable`],
316    /// the `Prioritized` element will be the [`FocusState::Focused`] entity.
317    Prioritized,
318
319    /// The currently highlighted/used entity,
320    /// there is only a signle _focused_ entity.
321    ///
322    /// All navigation requests start from it.
323    ///
324    /// To set an arbitrary [`Focusable`] to _focused_, you should send a
325    /// [`NavRequest::FocusOn`] request.
326    Focused,
327
328    /// This [`Focusable`] is on the path in the menu tree
329    /// to the current [`FocusState::Focused`] entity.
330    ///
331    /// [`FocusState::Active`] focusables are the [`Focusable`]s
332    /// from previous menus that were activated
333    /// in order to reach the [`MenuSetting`] containing
334    /// the currently _focused_ element.
335    ///
336    /// It's the "breadcrumb" of buttons to activate to reach
337    /// the currently focused element from the root menu.
338    Active,
339
340    /// Prevents all interactions with this [`Focusable`].
341    ///
342    /// This is equivalent to removing the `Focusable` component
343    /// from the entity, but without the latency.
344    Blocked,
345
346    /// None of the above:
347    /// This [`Focusable`] is neither `Prioritized`, `Focused` or `Active`.
348    Inert,
349}
350
351/// The reason why the navigation system is locked.
352#[derive(Debug, PartialEq, Eq, Clone, Copy)]
353#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
354pub enum LockReason {
355    /// Navigation was locked by activating a [lock focusable].
356    ///
357    /// [lock focusable] Focusable::lock
358    Focusable(Entity),
359
360    /// Navigation was locked by sending a [`NavRequest::Lock`].
361    NavRequest,
362}
363
364/// The navigation system's lock.
365///
366/// When locked, the navigation system doesn't process any [`NavRequest`].
367/// It only waits on a [`NavRequest::Unlock`] event. It will then continue
368/// processing new requests.
369#[derive(Resource, Debug)]
370#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Resource))]
371pub struct NavLock {
372    lock_reason: Option<LockReason>,
373}
374impl FromWorld for NavLock {
375    // PLEASE DO NOT USE THIS.
376    //
377    // This only exists to satisfy `bevy_reflect`'s `ReflectResource` requirement.
378    fn from_world(_: &mut bevy::prelude::World) -> Self {
379        Self::new()
380    }
381}
382impl NavLock {
383    pub(crate) fn new() -> Self {
384        Self { lock_reason: None }
385    }
386    /// The reason why navigation is locked, `None` if currently unlocked.
387    pub fn reason(&self) -> Option<LockReason> {
388        self.lock_reason
389    }
390    /// Whether the navigation system is locked.
391    pub fn is_locked(&self) -> bool {
392        self.lock_reason.is_some()
393    }
394}
395
396/// A menu that isolate children [`Focusable`]s from other focusables
397/// and specify navigation method within itself.
398///
399/// The user can't create a `TreeMenu`,
400/// they will use the [`MenuSetting`] API
401/// and the `TreeMenu` component will be inserted
402/// by the [`insert_tree_menus`] system.
403#[derive(Debug, Component, Clone)]
404#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component))]
405pub(crate) struct TreeMenu {
406    /// The [`Focusable`] that sends to this `MenuSetting`
407    /// when receiving [`NavRequest::Action`].
408    pub(crate) focus_parent: Option<Entity>,
409    /// The currently prioritized or active focusable in this menu.
410    pub(crate) active_child: Entity,
411}
412impl FromWorld for TreeMenu {
413    // PLEASE DO NOT USE THIS.
414    //
415    // This only exists to satisfy `bevy_reflect`'s `ReflectResource` requirement.
416    fn from_world(_: &mut bevy::prelude::World) -> Self {
417        TreeMenu {
418            focus_parent: None,
419            active_child: Entity::PLACEHOLDER,
420        }
421    }
422}
423
424/// The actions triggered by a [`Focusable`].
425#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
426#[non_exhaustive]
427#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
428pub enum FocusAction {
429    /// Acts like a standard navigation node.
430    ///
431    /// Goes into relevant menu if any [`MenuSetting`] is
432    /// [_reachable from_](MenuBuilder::from_named) this [`Focusable`].
433    #[default]
434    Normal,
435
436    /// If we receive [`NavRequest::Action`]
437    /// while this [`Focusable`] is focused,
438    /// it will act as a [`NavRequest::Cancel`]
439    /// (leaving submenu to enter the parent one).
440    Cancel,
441
442    /// If we receive [`NavRequest::Action`]
443    /// while this [`Focusable`] is focused,
444    /// the navigation system will freeze
445    /// until [`NavRequest::Unlock`] is received,
446    /// sending a [`NavEvent::Unlocked`].
447    ///
448    /// This is useful to implement widgets with complex controls
449    /// you don't want to accidentally unfocus,
450    /// or suspending the navigation system while in-game.
451    Lock,
452}
453
454/// An [`Entity`] that can be navigated to, using the cursor navigation system.
455///
456/// It is in one of multiple [`FocusState`],
457/// you can check its state with the [`Focusable::state`] method.
458///
459/// A `Focusable` can execute a variety of [`FocusAction`]
460/// when receiving [`NavRequest::Action`],
461/// the default one is [`FocusAction::Normal`].
462///
463/// **Note**: You should avoid updating manually the state of [`Focusable`]s.
464/// You should instead use [`NavRequest`] to manipulate and change focus.
465#[derive(Component, Clone, Debug)]
466#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component))]
467pub struct Focusable {
468    pub(crate) state: FocusState,
469    action: FocusAction,
470}
471impl Default for Focusable {
472    fn default() -> Self {
473        Focusable {
474            state: FocusState::Inert,
475            action: FocusAction::Normal,
476        }
477    }
478}
479impl Focusable {
480    /// Default Focusable.
481    pub fn new() -> Self {
482        Self::default()
483    }
484
485    /// The [`FocusState`] of this `Focusable`.
486    pub fn state(&self) -> FocusState {
487        self.state
488    }
489    /// The [`FocusAction`] of this `Focusable`.
490    pub fn action(&self) -> FocusAction {
491        self.action
492    }
493
494    /// A "cancel" focusable, see [`FocusAction::Cancel`].
495    pub fn cancel() -> Self {
496        Focusable {
497            state: FocusState::Inert,
498            action: FocusAction::Cancel,
499        }
500    }
501    /// A "lock" focusable, see [`FocusAction::Lock`].
502    pub fn lock() -> Self {
503        Focusable {
504            state: FocusState::Inert,
505            action: FocusAction::Lock,
506        }
507    }
508    /// A focusable that will get highlighted in priority when none are set yet.
509    ///
510    /// **WARNING**: Only use this when creating the UI.
511    /// Any of the following state is unspecified
512    /// and will likely result in broken behavior:
513    /// * Having multiple prioritized `Focusable`s in the same menu.
514    /// * Updating an already existing `Focusable` with this.
515    ///
516    /// # Example
517    ///
518    /// ```rust
519    /// # use bevy_ui_navigation::prelude::Focusable;
520    /// # use bevy_ui_navigation::components::FocusableButtonBundle;
521    /// # use bevy::prelude::*;
522    /// fn setup(mut commands: Commands) {
523    ///     commands.spawn(FocusableButtonBundle {
524    ///         focus: Focusable::new().prioritized(),
525    ///         ..default()
526    ///     });
527    /// }
528    /// ```
529    pub fn prioritized(self) -> Self {
530        Self {
531            state: FocusState::Prioritized,
532            ..self
533        }
534    }
535
536    /// A [`FocusState::Blocked`] focusable.
537    ///
538    /// This focusable will not be able to take focus until
539    /// [`Focusable::unblock`] is called on it.
540    ///
541    /// # Example
542    ///
543    /// ```rust
544    /// # use bevy_ui_navigation::prelude::Focusable;
545    /// # use bevy_ui_navigation::components::FocusableButtonBundle;
546    /// # use bevy::prelude::*;
547    /// fn setup(mut commands: Commands) {
548    ///     commands.spawn(FocusableButtonBundle {
549    ///         focus: Focusable::new().blocked(),
550    ///         ..default()
551    ///     });
552    /// }
553    /// ```
554    pub fn blocked(self) -> Self {
555        Self {
556            state: FocusState::Blocked,
557            ..self
558        }
559    }
560
561    /// Prevent this [`Focusable`] from gaining focus until it is unblocked.
562    ///
563    /// **Note**: Due to the way focus is handled, this does nothing
564    /// when the [`Focusable::state`] is [`FocusState::Active`]
565    /// or [`FocusState::Focused`].
566    ///
567    /// Returns `true` if `self` has succesfully been blocked
568    /// (its [`Focusable::state`] was either `Inert` or `Prioritized`).
569    ///
570    /// # Limitations
571    ///
572    /// - If all the children of a menu are blocked, when activating the menu's
573    ///   parent, the block state of the last active focusable will be ignored.
574    /// - When `FocusOn` to a focusable in a menu reachable from a blocked
575    ///   focusable, its block state will be ignored.
576    pub fn block(&mut self) -> bool {
577        use FocusState::{Blocked, Inert, Prioritized};
578        let blockable = matches!(self.state(), Inert | Prioritized);
579        if blockable {
580            self.state = Blocked;
581        }
582        blockable
583    }
584
585    /// Allow this [`Focusable`] to gain focus again,
586    /// setting it to [`FocusState::Inert`].
587    ///
588    /// Returns `true` if `self`'s state was [`FocusState::Blocked`].
589    pub fn unblock(&mut self) -> bool {
590        if self.state() == FocusState::Blocked {
591            self.state = FocusState::Inert;
592            true
593        } else {
594            false
595        }
596    }
597}
598
599/// The currently _focused_ [`Focusable`].
600///
601/// You cannot edit it or create new `Focused` component.
602/// To set an arbitrary [`Focusable`] to _focused_,
603/// you should send [`NavRequest::FocusOn`].
604///
605/// This [`Component`] is useful
606/// if you needto query for the _currently focused_ element,
607/// using `Query<Entity, With<Focused>>` for example.
608///
609/// If a [`Focusable`] is focused,
610/// its [`Focusable::state()`] will be [`FocusState::Focused`],
611///
612/// # Notes
613///
614/// The `Focused` marker component is only updated
615/// at the end of the `CoreStage::Update` stage.
616/// This means it might lead to a single frame of latency
617/// compared to using [`Focusable::state()`].
618#[derive(Component)]
619#[component(storage = "SparseSet")]
620#[non_exhaustive]
621pub struct Focused;
622
623#[cfg(feature = "bevy_ui")]
624impl<'w, 's> MenuNavigationStrategy for UiProjectionQuery<'w, 's> {
625    fn resolve_2d<'a>(
626        &self,
627        focused: Entity,
628        direction: events::Direction,
629        cycles: bool,
630        siblings: &'a [Entity],
631    ) -> Option<&'a Entity> {
632        use events::Direction::*;
633
634        let pos_of = |entity: Entity| {
635            self.transforms
636                .get(entity)
637                .expect("Focusable entities must have a GlobalTransform component")
638                .translation()
639                .xy()
640        };
641        let focused_pos = pos_of(focused);
642        let closest = siblings
643            .iter()
644            .filter(|sibling| {
645                direction.is_in(focused_pos, pos_of(**sibling)) && **sibling != focused
646            })
647            .max_by_key(|s| FloatOrd(-focused_pos.distance_squared(pos_of(**s))));
648        match (closest, self.boundaries.as_ref()) {
649            (None, None) if cycles => {
650                warn!(
651                    "Tried to move in {direction:?} from Focusable {focused:?} while no other \
652                 Focusables were there. There were no `Res<ScreenBoundaries>`, so we couldn't \
653                 compute the screen edges for cycling. Make sure you either add the \
654                 bevy_ui_navigation::systems::update_boundaries system to your app or implement \
655                 your own routine to manage a `Res<ScreenBoundaries>`."
656                );
657                None
658            }
659            (None, Some(boundaries)) if cycles => {
660                let (x, y) = (boundaries.position.x, boundaries.position.y);
661                let edge = boundaries.screen_edge;
662                let scale = boundaries.scale;
663                let focused_pos = match direction {
664                    // NOTE: up/down axises are inverted in bevy
665                    South => Vec2::new(focused_pos.x, y - scale * edge.min.y),
666                    North => Vec2::new(focused_pos.x, y + scale * edge.max.y),
667                    East => Vec2::new(x - edge.min.x * scale, focused_pos.y),
668                    West => Vec2::new(x + edge.max.x * scale, focused_pos.y),
669                };
670                siblings
671                    .iter()
672                    .max_by_key(|s| FloatOrd(-focused_pos.distance_squared(pos_of(**s))))
673            }
674            (anyelse, _) => anyelse,
675        }
676    }
677}
678
679/// Returns the next or previous entity based on `direction`.
680fn resolve_scope(
681    focused: Entity,
682    direction: events::ScopeDirection,
683    cycles: bool,
684    siblings: &[Entity],
685) -> Option<&Entity> {
686    let focused_index = siblings.iter().position(|e| *e == focused)?;
687    let new_index = resolve_index(focused_index, cycles, direction, siblings.len() - 1);
688    new_index.and_then(|i| siblings.get(i))
689}
690
691/// Find the event created by `request` where the focused element is `focused`.
692fn resolve<STGY: MenuNavigationStrategy>(
693    focused: Entity,
694    request: NavRequest,
695    queries: &NavQueries,
696    // this is to avoid triggering change detection if not updated.
697    lock: &mut ResMut<NavLock>,
698    from: Vec<Entity>,
699    strategy: &STGY,
700) -> NavEvent {
701    use FocusState::Blocked;
702    use NavRequest::*;
703
704    assert!(
705        queries.focusables.get(focused).is_ok(),
706        "The resolution algorithm MUST go from a focusable element"
707    );
708    assert!(
709        !from.contains(&focused),
710        "Navigation graph cycle detected! This panic has prevented a stack overflow, \
711        please check usages of `MenuSetting::reachable_from`"
712    );
713
714    let mut from = (from, focused).into();
715
716    // Early exit with a `NoChanges` event.
717    macro_rules! or_none {
718        ($to_match:expr) => {
719            match $to_match {
720                Some(x) => x,
721                None => return NavEvent::NoChanges { from, request },
722            }
723        };
724    }
725    match request {
726        Lock => {
727            if lock.is_locked() {
728                return NavEvent::NoChanges { from, request };
729            }
730            let reason = LockReason::NavRequest;
731            lock.lock_reason = Some(reason);
732            NavEvent::Locked(reason)
733        }
734        Move(direction) => {
735            let (parent, cycles) = match queries.parent_menu(focused) {
736                Some(val) if !val.2.is_2d() => return NavEvent::NoChanges { from, request },
737                Some(val) => (Some(val.0), !val.2.bound()),
738                None => (None, true),
739            };
740            let unblocked = |(e, focus): (_, &Focusable)| (focus.state != Blocked).then(|| e);
741            let siblings = match parent {
742                Some(parent) => queries.children.focusables_of(parent),
743                None => queries.focusables.iter().filter_map(unblocked).collect(),
744            };
745            let to = strategy.resolve_2d(focused, direction, cycles, &siblings);
746            NavEvent::focus_changed(*or_none!(to), from)
747        }
748        Cancel => {
749            let to = or_none!(queries.parent_menu(focused));
750            let to = or_none!(to.1.focus_parent);
751            from.push(to);
752            NavEvent::focus_changed(to, from)
753        }
754        Action => {
755            match queries.focusables.get(focused).map(|e| e.1.action) {
756                Ok(FocusAction::Cancel) => {
757                    let mut from = from.to_vec();
758                    from.truncate(from.len() - 1);
759                    return resolve(focused, NavRequest::Cancel, queries, lock, from, strategy);
760                }
761                Ok(FocusAction::Lock) => {
762                    let reason = LockReason::Focusable(focused);
763                    lock.lock_reason = Some(reason);
764                    return NavEvent::Locked(reason);
765                }
766                Err(_) | Ok(FocusAction::Normal) => {}
767            }
768            let child_menu = child_menu(focused, queries);
769            let (_, menu, _) = or_none!(child_menu);
770            let to = (menu.active_child, from.clone().into()).into();
771            NavEvent::FocusChanged { to, from }
772        }
773        // "Tab move" nested movement
774        ScopeMove(scope_dir) => {
775            let (parent, menu, setting) = or_none!(queries.parent_menu(focused));
776            let siblings = queries.children.focusables_of(parent);
777            if !setting.is_scope() {
778                let focused = or_none!(menu.focus_parent);
779                resolve(focused, request, queries, lock, from.into(), strategy)
780            } else {
781                let cycles = !setting.bound();
782                let to = or_none!(resolve_scope(focused, scope_dir, cycles, &siblings));
783                let extra = match child_menu(*to, queries) {
784                    Some((_, menu, _)) => focus_deep(menu, queries),
785                    None => Vec::new(),
786                };
787                let to = (extra, *to).into();
788                NavEvent::FocusChanged { to, from }
789            }
790        }
791        FocusOn(new_to_focus) => {
792            let focusable = queries.focusables.get(new_to_focus);
793            if matches!(focusable, Ok((_, f)) if f.state() == Blocked) {
794                return NavEvent::NoChanges { from, request };
795            }
796            // assumption here is that there is a common ancestor
797            // though nothing really breaks if there isn't
798            let mut from = queries.root_path(focused);
799            let mut to = queries.root_path(new_to_focus);
800            trim_common_tail(&mut from, &mut to);
801            if from == to {
802                NavEvent::NoChanges { from, request }
803            } else {
804                NavEvent::FocusChanged { from, to }
805            }
806        }
807        Unlock => {
808            if let Some(lock_entity) = lock.lock_reason.take() {
809                NavEvent::Unlocked(lock_entity)
810            } else {
811                warn!("Received a NavRequest::Unlock while not locked");
812                NavEvent::NoChanges { from, request }
813            }
814        }
815    }
816}
817
818/// Replaces [`MenuBuilder`]s with proper [`TreeMenu`]s.
819pub(crate) fn insert_tree_menus(
820    mut commands: Commands,
821    builders: Query<(Entity, &MenuBuilder), With<MenuSetting>>,
822    queries: NavQueries,
823) {
824    use FocusState::{Active, Focused, Prioritized};
825    let mut inserts = Vec::new();
826    let no_focus_msg = "Within a menu built with MenuBuilder, there must be at least one entity \
827         with the Focusable component, none were found";
828    for (entity, builder) in &builders {
829        let children = queries.children.focusables_of(entity);
830        let child = children
831            .iter()
832            .find_map(|e| {
833                let (_, focusable) = queries.focusables.get(*e).ok()?;
834                matches!(focusable.state, Prioritized | Active | Focused).then_some(e)
835            })
836            .unwrap_or_else(|| children.first().expect(no_focus_msg));
837        if let Ok(focus_parent) = builder.try_into() {
838            let menu = TreeMenu {
839                focus_parent,
840                active_child: *child,
841            };
842            inserts.push((entity, menu));
843            commands.entity(entity).remove::<MenuBuilder>();
844            debug!("Associated {entity:?} with a parent focusable.");
845        }
846    }
847    commands.insert_or_spawn_batch(inserts);
848}
849
850/// System to set the first [`Focusable`] to [`FocusState::Focused`]
851/// when no navigation has been done yet.
852///
853/// This also sets `Active` state and `active_child` of menus leading
854/// to the current focusable.
855pub(crate) fn set_first_focused(
856    has_focused: Query<(), With<Focused>>,
857    mut queries: ParamSet<(NavQueries, MutQueries)>,
858    mut events: EventWriter<NavEvent>,
859) {
860    if has_focused.is_empty() {
861        if let Some(to_focus) = queries.p0().pick_first_focused() {
862            let breadcrumb = queries.p0().root_path(to_focus);
863            queries.p1().update_focus(&[], &breadcrumb);
864            events.send(NavEvent::InitiallyFocused(to_focus));
865        }
866    }
867}
868
869pub(crate) fn consistent_menu(
870    updated_focusables: Query<(Entity, &Focusable), Changed<Focusable>>,
871    children: ChildQueries,
872    mut menus: Query<(Entity, &mut TreeMenu)>,
873) {
874    for (entity, updated) in &updated_focusables {
875        if updated.state() != FocusState::Blocked {
876            continue;
877        }
878        for (menu_entity, mut menu) in &mut menus {
879            if menu.active_child != entity {
880                continue;
881            }
882            if let Some(new_active) = children.focusables_of(menu_entity).first().copied() {
883                menu.active_child = new_active;
884            }
885            // We found the unique menu that leads to the changed entity
886            // continue to check for next changed focusable.
887            break;
888        }
889    }
890}
891
892/// Listen to [`NavRequest`] and update the state of [`Focusable`] entities
893/// when relevant.
894pub(crate) fn listen_nav_requests<STGY: SystemParam>(
895    mut queries: ParamSet<(NavQueries, MutQueries)>,
896    mquery: StaticSystemParam<STGY>,
897    mut lock: ResMut<NavLock>,
898    mut requests: EventReader<NavRequest>,
899    mut events: EventWriter<NavEvent>,
900) where
901    for<'w, 's> SystemParamItem<'w, 's, STGY>: MenuNavigationStrategy,
902{
903    let no_focused = "Tried to execute a NavRequest \
904            when no focusables exist, \
905            NavRequest does nothing if \
906            there isn't any navigation to do.";
907
908    // Cache focus result from previous iteration to avoid re-running costly `pick_first_focused`
909    let mut computed_focused = None;
910    for request in requests.read() {
911        if lock.is_locked() && *request != NavRequest::Unlock {
912            continue;
913        }
914        // We use `pick_first_focused` instead of `Focused` component for first
915        // iteration because `set_first_focused` just before `listen_nav_request`
916        // without a command flush in-between.
917        let picked = || queries.p0().pick_first_focused();
918        let focused = match computed_focused.or_else(picked) {
919            Some(focused) => focused,
920            None => {
921                warn!(no_focused);
922                return;
923            }
924        };
925        let from = Vec::new();
926        let event = resolve(focused, *request, &queries.p0(), &mut lock, from, &*mquery);
927        if let NavEvent::FocusChanged { to, from } = &event {
928            computed_focused = Some(queries.p1().update_focus(from, to));
929        };
930        events.send(event);
931    }
932}
933
934/// The child [`TreeMenu`] of `focusable`.
935fn child_menu<'a>(
936    focusable: Entity,
937    queries: &'a NavQueries,
938) -> Option<(Entity, &'a TreeMenu, &'a MenuSetting)> {
939    queries
940        .menus
941        .iter()
942        .find(|e| e.1.focus_parent == Some(focusable))
943}
944
945/// The [`TreeMenu`] containing `focusable`, if any.
946pub(crate) fn parent_menu(
947    focusable: Entity,
948    queries: &NavQueries,
949) -> Option<(Entity, TreeMenu, MenuSetting)> {
950    let parent = queries.parents.get(focusable).ok()?.get();
951    match queries.menus.get(parent) {
952        Ok((_, tree, setting)) => Some((parent, tree.clone(), *setting)),
953        Err(_) => parent_menu(parent, queries),
954    }
955}
956
957impl<'w, 's> ChildQueries<'w, 's> {
958    /// All sibling [`Focusable`]s within a single [`TreeMenu`].
959    pub(crate) fn focusables_of(&self, menu: Entity) -> Vec<Entity> {
960        use FocusState::Blocked;
961        let is_focusable = |e: &&_| {
962            self.is_focusable
963                .get(**e)
964                .map_or(false, |f| f.state != Blocked)
965        };
966        match self.children.get(menu) {
967            Ok(direct_children) => {
968                let focusables = direct_children.iter().filter(is_focusable).cloned();
969                let transitive_focusables = direct_children
970                    .iter()
971                    .filter(|e| !self.is_focusable.contains(**e))
972                    .filter(|e| !self.is_menu.contains(**e))
973                    .flat_map(|e| self.focusables_of(*e));
974                focusables.chain(transitive_focusables).collect()
975            }
976            Err(_) => Vec::new(),
977        }
978    }
979}
980
981/// Remove all mutually identical elements at the end of `v1` and `v2`.
982fn trim_common_tail<T: PartialEq>(v1: &mut NonEmpty<T>, v2: &mut NonEmpty<T>) {
983    let mut i1 = v1.len().get() - 1;
984    let mut i2 = v2.len().get() - 1;
985    loop {
986        if v1[i1] != v2[i2] {
987            // unwraps: any usize + 1 (saturating) is NonZero
988            let l1 = NonZeroUsize::new(i1.saturating_add(1)).unwrap();
989            let l2 = NonZeroUsize::new(i2.saturating_add(1)).unwrap();
990            v1.truncate(l1);
991            v2.truncate(l2);
992            return;
993        } else if i1 != 0 && i2 != 0 {
994            i1 -= 1;
995            i2 -= 1;
996        } else {
997            // There is no changes to be made to the input vectors
998            return;
999        }
1000    }
1001}
1002
1003/// Navigate downward the menu hierarchy, traversing all prioritized children.
1004fn focus_deep<'a>(mut menu: &'a TreeMenu, queries: &'a NavQueries) -> Vec<Entity> {
1005    let mut ret = Vec::with_capacity(4);
1006    loop {
1007        let last = menu.active_child;
1008        ret.insert(0, last);
1009        menu = match child_menu(last, queries) {
1010            Some((_, menu, _)) => menu,
1011            None => return ret,
1012        };
1013    }
1014}
1015
1016/// Cycle through a [scoped menu](MenuSetting::scope) according to menu settings.
1017///
1018/// Returns the index of the element to focus according to `direction`.
1019/// Cycles if `cycles` and goes over `max_value` or goes bellow 0.
1020/// `None` if the direction is a dead end.
1021fn resolve_index(
1022    from: usize,
1023    cycles: bool,
1024    direction: events::ScopeDirection,
1025    max_value: usize,
1026) -> Option<usize> {
1027    use events::ScopeDirection::*;
1028    match (direction, from) {
1029        (Previous, 0) => cycles.then_some(max_value),
1030        (Previous, from) => Some(from - 1),
1031        (Next, from) if from == max_value => cycles.then_some(0),
1032        (Next, from) => Some(from + 1),
1033    }
1034}
1035
1036#[cfg(test)]
1037mod tests {
1038    use super::trim_common_tail;
1039    #[test]
1040    fn test_trim_common_tail() {
1041        use non_empty_vec::ne_vec;
1042        let mut v1 = ne_vec![1, 2, 3, 4, 5, 6, 7];
1043        let mut v2 = ne_vec![3, 2, 1, 4, 5, 6, 7];
1044        trim_common_tail(&mut v1, &mut v2);
1045        assert_eq!(v1, ne_vec![1, 2, 3]);
1046        assert_eq!(v2, ne_vec![3, 2, 1]);
1047    }
1048}