1use 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
70pub trait MenuNavigationStrategy {
77 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#[derive(Default, Debug, Clone, Copy)]
98#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
99pub struct Rect {
100 pub max: Vec2,
102 pub min: Vec2,
104}
105#[derive(Default, Debug, Clone, Copy, Resource)]
112#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Resource))]
113pub struct ScreenBoundaries {
114 pub position: Vec2,
116 pub screen_edge: Rect,
118 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#[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#[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 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 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 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 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#[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 fn set_active_child(&mut self, child: Entity) {
257 let mut focusable = child;
258 let mut nav_menu = loop {
259 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 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#[derive(Copy, Clone, PartialEq, Eq, Debug)]
311#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
312pub enum FocusState {
313 Prioritized,
318
319 Focused,
327
328 Active,
339
340 Blocked,
345
346 Inert,
349}
350
351#[derive(Debug, PartialEq, Eq, Clone, Copy)]
353#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
354pub enum LockReason {
355 Focusable(Entity),
359
360 NavRequest,
362}
363
364#[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 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 pub fn reason(&self) -> Option<LockReason> {
388 self.lock_reason
389 }
390 pub fn is_locked(&self) -> bool {
392 self.lock_reason.is_some()
393 }
394}
395
396#[derive(Debug, Component, Clone)]
404#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component))]
405pub(crate) struct TreeMenu {
406 pub(crate) focus_parent: Option<Entity>,
409 pub(crate) active_child: Entity,
411}
412impl FromWorld for TreeMenu {
413 fn from_world(_: &mut bevy::prelude::World) -> Self {
417 TreeMenu {
418 focus_parent: None,
419 active_child: Entity::PLACEHOLDER,
420 }
421 }
422}
423
424#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
426#[non_exhaustive]
427#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
428pub enum FocusAction {
429 #[default]
434 Normal,
435
436 Cancel,
441
442 Lock,
452}
453
454#[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 pub fn new() -> Self {
482 Self::default()
483 }
484
485 pub fn state(&self) -> FocusState {
487 self.state
488 }
489 pub fn action(&self) -> FocusAction {
491 self.action
492 }
493
494 pub fn cancel() -> Self {
496 Focusable {
497 state: FocusState::Inert,
498 action: FocusAction::Cancel,
499 }
500 }
501 pub fn lock() -> Self {
503 Focusable {
504 state: FocusState::Inert,
505 action: FocusAction::Lock,
506 }
507 }
508 pub fn prioritized(self) -> Self {
530 Self {
531 state: FocusState::Prioritized,
532 ..self
533 }
534 }
535
536 pub fn blocked(self) -> Self {
555 Self {
556 state: FocusState::Blocked,
557 ..self
558 }
559 }
560
561 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 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#[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 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
679fn 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
691fn resolve<STGY: MenuNavigationStrategy>(
693 focused: Entity,
694 request: NavRequest,
695 queries: &NavQueries,
696 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 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 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 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
818pub(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
850pub(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 break;
888 }
889 }
890}
891
892pub(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 let mut computed_focused = None;
910 for request in requests.read() {
911 if lock.is_locked() && *request != NavRequest::Unlock {
912 continue;
913 }
914 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
934fn 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
945pub(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 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
981fn 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 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 return;
999 }
1000 }
1001}
1002
1003fn 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
1016fn 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}