bevy_ui_navigation/
menu.rs

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