bevy_alt_ui_navigation_lite/
menu.rs

1//! Contains menu-related components.
2
3use std::borrow::Cow;
4
5use bevy::ecs::{entity::Entity, name::Name, prelude::Component};
6#[cfg(feature = "bevy_reflect")]
7use bevy::{ecs::reflect::ReflectComponent, reflect::Reflect};
8
9/// Add this component to a menu entity so that all [`Focusable`]s
10/// within that menus gets added the `T` component automatically.
11///
12/// [`Focusable`]: crate::prelude::Focusable
13#[derive(Component)]
14pub struct NavMarker<T: Component>(pub T);
15
16/// Tell the navigation system to turn this UI node into a menu.
17///
18/// Note that `MenuBuilder` is replaced by a private component when encoutered.
19#[doc(alias = "NavMenu")]
20#[derive(Component, Debug, Clone)]
21#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component))]
22pub enum MenuBuilder {
23    /// Create a menu as reachable from a [`Focusable`]
24    /// with a [`Name`] component.
25    ///
26    /// This is useful if, for example, you just want to spawn your UI without
27    /// keeping track of entity ids of buttons that leads to submenus.
28    ///
29    /// See [`MenuBuilder::from_named`] for an easier to use method
30    /// if you don't have a [`Name`] ready to use.
31    ///
32    /// # Important
33    ///
34    /// You must ensure this doesn't create a cycle. Eg: you shouldn't be able
35    /// to reach `MenuSetting` X from [`Focusable`] Y if there is a path from
36    /// `MenuSetting` X to `Focusable` Y.
37    ///
38    /// # Performance and edge cases
39    ///
40    /// `bevy-ui-navigation` tries to convert **each frame** every
41    /// `MenuBuilder::NamedParent` into a [`MenuBuilder::EntityParent`].
42    ///
43    /// It iterates every [`Focusable`] with a [`Name`] component until it finds
44    /// a match. And repeat the operation next frame if no match is found.
45    ///
46    /// This incurs a significant performance cost per unmatched `NamedParent`!
47    /// `bevy-ui-navigation` emits a **`WARN`** per second if it encounters
48    /// unmatched `NamedParent`s. Pay attention to this message if you don't
49    /// want to waste preciously CPU cycles.
50    ///
51    /// [`Focusable`]: crate::prelude::Focusable
52    NamedParent(Name),
53
54    /// Create a menu as reachable from a given [`Focusable`].
55    ///
56    /// When requesting [`NavRequest::Action`] when `Entity` is focused,
57    /// the focus will be changed to a focusable within this menu.
58    ///
59    /// # Important
60    ///
61    /// You must ensure this doesn't create a cycle. Eg: you shouldn't be able
62    /// to reach `MenuSetting` X from `Focusable` Y if there is a path from
63    /// `MenuSetting` X to `Focusable` Y.
64    ///
65    /// [`Focusable`]: crate::prelude::Focusable
66    /// [`NavRequest::Action`]: crate::prelude::NavRequest::Action
67    EntityParent(Entity),
68
69    /// Create a menu with no parents.
70    ///
71    /// No [`Focusable`] will "lead to" this menu.
72    /// You either need to programmatically give focus to this menu tree
73    /// with [`NavRequest::FocusOn`]
74    /// or have only one root menu.
75    ///
76    /// [`Focusable`]: crate::prelude::Focusable
77    /// [`NavRequest::FocusOn`]: crate::prelude::NavRequest::FocusOn
78    Root,
79}
80impl MenuBuilder {
81    /// Create a [`MenuBuilder::NamedParent`] directly from `String` or `&str`.
82    ///
83    /// See [`MenuBuilder::NamedParent`] for caveats and quirks.
84    pub fn from_named(parent: impl Into<Cow<'static, str>>) -> Self {
85        Self::NamedParent(Name::new(parent))
86    }
87}
88impl From<Option<Entity>> for MenuBuilder {
89    fn from(parent: Option<Entity>) -> Self {
90        match parent {
91            Some(parent) => MenuBuilder::EntityParent(parent),
92            None => MenuBuilder::Root,
93        }
94    }
95}
96impl bevy::prelude::FromWorld for MenuBuilder {
97    /// This shouldn't be considered a good "default", this only exists
98    /// to make possible `ReflectComponent` derive.
99    fn from_world(_: &mut bevy::prelude::World) -> Self {
100        Self::Root
101    }
102}
103impl TryFrom<&MenuBuilder> for Option<Entity> {
104    type Error = ();
105    fn try_from(value: &MenuBuilder) -> Result<Self, Self::Error> {
106        match value {
107            MenuBuilder::EntityParent(parent) => Ok(Some(*parent)),
108            MenuBuilder::NamedParent(_) => Err(()),
109            MenuBuilder::Root => Ok(None),
110        }
111    }
112}
113
114/// A menu that isolate children [`Focusable`]s from other focusables
115/// and specify navigation method within itself.
116///
117/// # Usage
118///
119/// A `MenuSetting` can be used to:
120/// * Prevent navigation from one specific submenu to another
121/// * Specify if 2d navigation wraps around the screen,
122///   see [`MenuSetting::wrapping`].
123/// * Specify "scope menus" such that sending a [`NavRequest::ScopeMove`]
124///   when the focused element is a [`Focusable`] nested within this `MenuSetting`
125///   will move cursor within this menu.
126///   See [`MenuSetting::scope`].
127/// * Specify _submenus_ and specify from where those submenus are reachable.
128/// * Specify which entity will be the parents of this [`MenuSetting`].
129///   See [`MenuBuilder`].
130///
131/// If you want to specify which [`Focusable`] should be focused first
132/// when entering a menu,
133/// you should mark one of the children of this menu with [`Focusable::prioritized`].
134///
135/// # Limitations
136///
137/// Menu navigation relies heavily on the bevy hierarchy being consistent.
138/// You might get inconsistent state under the folowing conditions:
139///
140/// - You despawned an entity in the [`FocusState::Active`] state
141/// - You changed the parent of a [`Focusable`] member of a menu to a new menu.
142///         
143/// The navigation system might still work as expected,
144/// however, [`Focusable::state`] may be missleading
145/// for the length of one frame.
146///
147/// # Panics
148///
149/// **Menu loops will cause a panic**.
150/// A menu loop is a way to go from menu A to menu B and
151/// then from menu B to menu A while never going back.
152///
153/// Don't worry though, menu loops are really hard to make by accident,
154/// and it will only panic if you use a `NavRequest::FocusOn(entity)`
155/// where `entity` is inside a menu loop.
156///
157/// [`NavRequest`]: crate::prelude::NavRequest
158/// [`Focusable`]: crate::prelude::Focusable
159/// [`FocusState::Active`]: crate::prelude::FocusState::Active
160/// [`Focusable::state`]: crate::prelude::Focusable::state
161/// [`Focusable::prioritized`]: crate::prelude::Focusable::prioritized
162/// [`NavRequest::ScopeMove`]: crate::prelude::NavRequest::ScopeMove
163/// [`NavRequest`]: crate::prelude::NavRequest
164#[doc(alias = "NavMenu")]
165#[derive(Clone, Default, Component, Debug, Copy, PartialEq)]
166#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component))]
167pub struct MenuSetting {
168    /// Whether to wrap navigation.
169    ///
170    /// When the player moves to a direction where there aren't any focusables,
171    /// if this is true, the focus will "wrap" to the other direction of the screen.
172    pub wrapping: bool,
173
174    /// Whether this is a scope menu.
175    ///
176    /// A scope menu is controlled with [`NavRequest::ScopeMove`]
177    /// even when the focused element is not in this menu, but in a submenu
178    /// reachable from this one.
179    ///
180    /// [`NavRequest::ScopeMove`]: crate::prelude::NavRequest::ScopeMove
181    pub scope: bool,
182}
183impl MenuSetting {
184    pub(crate) fn bound(&self) -> bool {
185        !self.wrapping
186    }
187    pub(crate) fn is_2d(&self) -> bool {
188        !self.is_scope()
189    }
190    pub(crate) fn is_scope(&self) -> bool {
191        self.scope
192    }
193    /// Create a new non-wrapping, non-scopped [`MenuSetting`],
194    /// those are the default values.
195    ///
196    /// To create a wrapping/scopped menu, you can use:
197    /// `MenuSetting::new().wrapping().scope()`.
198    pub fn new() -> Self {
199        Self::default()
200    }
201    /// Set [`wrapping`] to true.
202    ///
203    /// [`wrapping`]: Self::wrapping
204    pub fn wrapping(mut self) -> Self {
205        self.wrapping = true;
206        self
207    }
208    /// Set `scope` to true.
209    ///
210    /// [`scope`]: Self::scope
211    pub fn scope(mut self) -> Self {
212        self.scope = true;
213        self
214    }
215}