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}