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}