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