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 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347
//! [`NavMenu`] builders to convert into [`TreeMenu`]
//!
//! This module defines a bunch of "seed" bundles. Systems in [`crate::named`],
//! [`crate::marker`] and [`crate::resolve`] will take the components
//! defined in those seeds and replace them by [`NavMenu`]s. It is necessary
//! for a few things:
//! * The [`active_child`](TreeMenu::active_child) field of `NavMenu`, which
//! cannot be inferred without the [`Focusable`](crate::Focusable)s children of that menu
//! * Finding the [`Focusable`](crate::Focusable) specified in [`ParentName`]
//!
//! # Seed bundles
//!
//! Seed bundles are collections of components that will trigger various
//! pre-processing to create a [`TreeMenu`]. They are a combination of those
//! components:
//! * [`TreeMenuSeed`]: the base seed, which will be converted into a [`TreeMenu`]
//! in [`crate::resolve::insert_tree_menus`].
//! * [`ParentName`], the *by-name* marker: marks a [`TreeMenuSeed`] as needing
//! its [`focus_parent`](TreeMenuSeed::focus_parent) to be updated by
//! [`crate::named::resolve_named_menus`] with the [`Focusable`](crate::Focusable) which
//! [`Name`](https://docs.rs/bevy/0.6.0/bevy/core/struct.Name.html) matches
//! the one in [`ParentName`]. If for whatever reason that update doesn't
//! happen, [`crate::resolve::insert_tree_menus`] will panic.
//! * [`NavMarker<T>`], the *automarking* marker: marks a [`NavMenu`] as
//! inserting a `T` component to all it's enclosed [`Focusable`](crate::Focusable)s. The
//! [`NavMarker<T>`] will then be used by
//! [`NavMarkerPropagationPlugin<T>`](crate::NavMarkerPropagationPlugin) to
//! automatically add the marker to the children [`Focusable`](crate::Focusable)s.
//!
//! Those components are combined in the seed bundles. Which processing step is
//! applied to the [`TreeMenuSeed`] depends on which components it was inserted
//! with. The bundles are:
//! * [`MenuSeed`]: Creates a [`NavMenu`].
//! * [`NamedMenuSeed`]: Creates a [`NavMenu`] "reachable from" the
//! [`Focusable`](crate::Focusable) named in the [`ParentName`].
//! * [`MarkingMenuSeed`]: Creates a [`NavMenu`] that will insert the
//! marker component specified in [`NavMarker<T>`] in all it's [`Focusable`](crate::Focusable)
//! children.
//! * [`NamedMarkingMenuSeed`]: Combination of [`NamedMenuSeed`] and
//! [`MarkingMenuSeed`].
//!
//! # Ordering
//!
//! In order to correctly create the [`TreeMenu`] specified with the bundles
//! declared int this module, the systems need to be ran in this order:
//!
//! ```text
//! named::resolve_named_menus → resolve::insert_tree_menus → marker::mark_new_menus
//! ```
//! The insert/mark relationship is not necessary, it will eventually correctly
//! mark focusables. However, if you want to avoid a 1 frame latency on marking
//! entities with the specified component, you need to have a stage boundary
//! between `insert_tree_menus` and `mark_new_menus`.
//!
//! The resolve_named/insert relationship should be upheld. Otherwise, root
//! NavMenus will spawn instead of NavMenus with parent with given name.
#![allow(unused_parens)]
use std::borrow::Cow;
use bevy::prelude::*;
use crate::resolve::TreeMenu;
/// Option of an option.
///
/// It's really just `Option<Option<T>>` with some semantic sparkled on top.
#[derive(Clone)]
pub(crate) enum FailableOption<T> {
Uninit,
None,
Some(T),
}
impl<T> FailableOption<T> {
fn into_opt(self) -> Option<Option<T>> {
match self {
Self::Some(t) => Some(Some(t)),
Self::None => Some(None),
Self::Uninit => None,
}
}
}
impl<T> From<Option<T>> for FailableOption<T> {
fn from(option: Option<T>) -> Self {
option.map_or(Self::None, Self::Some)
}
}
/// An uninitialized [`TreeMenu`].
///
/// It is added through one of the bundles defined in this crate by the user,
/// and picked up by the [`crate::resolve::insert_tree_menus`] system to create the
/// actual [`TreeMenu`] handled by the resolution algorithm.
#[derive(Component, Clone)]
pub(crate) struct TreeMenuSeed {
pub(crate) focus_parent: FailableOption<Entity>,
menu: NavMenu,
}
impl TreeMenuSeed {
/// Initialize a [`TreeMenu`] with given active child.
///
/// (Menus without focusables are a programming error)
pub(crate) fn with_child(self, active_child: Entity) -> TreeMenu {
let TreeMenuSeed { focus_parent, menu } = self;
let msg = "An initialized parent value";
TreeMenu {
focus_parent: focus_parent.into_opt().expect(msg),
setting: menu,
active_child,
}
}
}
/// Component to specify creation of a [`TreeMenu`] refering to their parent
/// focusable by [`Name`](https://docs.rs/bevy/0.6.0/bevy/core/struct.Name.html)
///
/// It is used in [`crate::named::resolve_named_menus`] to figure out the
/// `Entity` id of the named parent of the [`TreeMenuSeed`] and set its
/// `focus_parent` field.
#[derive(Component, Clone)]
pub(crate) struct ParentName(pub(crate) Name);
/// Component to add to [`NavMenu`] entities to propagate `T` to all
/// [`Focusable`](crate::Focusable) children of that menu.
#[derive(Component, Clone)]
pub(crate) struct NavMarker<T>(pub(crate) T);
/// A menu that isolate children [`Focusable`](crate::Focusable)s from other
/// focusables and specify navigation method within itself.
///
/// # Usage
///
/// A [`NavMenu`] can be used to:
/// * Prevent navigation from one specific submenu to another
/// * Specify if 2d navigation wraps around the screen, see
/// [`NavMenu::Wrapping2d`].
/// * Specify "scope menus" such that a
/// [`NavRequest::ScopeMove`](crate::NavRequest::ScopeMove) emitted when
/// the focused element is a [`Focusable`](crate::Focusable) nested within this `NavMenu`
/// will navigate this menu. See [`NavMenu::BoundScope`] and
/// [`NavMenu::WrappingScope`].
/// * Specify _submenus_ and specify from where those submenus are reachable.
/// * Add a specific component to all [`Focusable`](crate::Focusable)s in this menu. You must
/// first create a "seed" bundle with any of the [`NavMenu`] methods and then
/// call [`marking`](MenuSeed::marking) on it.
/// * Specify which entity will be the parents of this [`NavMenu`], see
/// [`NavMenu::reachable_from`] or [`NavMenu::reachable_from_named`] if you don't
/// have access to the [`Entity`](https://docs.rs/bevy/0.6.0/bevy/ecs/entity/struct.Entity.html)
/// for the parent [`Focusable`](crate::Focusable)
///
/// ## Example
///
/// See the example in this [crate]'s root level documentation page.
///
/// # Invariants
///
/// **You need to follow those rules (invariants) to avoid panics**:
/// 1. A `Menu` must have **at least one** [`Focusable`](crate::Focusable) child in the UI
/// hierarchy.
/// 2. There must not be a menu loop. Ie: a way to go from menu A to menu B and
/// then from menu B to menu A while never going back.
/// 3. Focusables in 2d menus must have a `GlobalTransform`.
///
/// # Panics
///
/// Thankfully, programming errors are caught early and you'll probably get a
/// panic fairly quickly if you don't follow the invariants.
/// * Invariant (1) panics as soon as you add the menu without focusable
/// children.
/// * Invariant (2) panics if the focus goes into a menu loop.
#[derive(Clone, Debug, Copy, PartialEq)]
#[non_exhaustive]
pub enum NavMenu {
/// Non-wrapping menu with 2d navigation.
///
/// It is possible to move around this menu in all cardinal directions, the
/// focus changes according to the physical position of the
/// [`Focusable`](crate::Focusable) in it.
///
/// If the player moves to a direction where there aren't any focusables,
/// nothing will happen.
Bound2d,
/// Wrapping menu with 2d navigation.
///
/// It is possible to move around this menu in all cardinal directions, the
/// focus changes according to the physical position of the
/// [`Focusable`](crate::Focusable) in it.
///
/// If the player moves to a direction where there aren't any focusables,
/// the focus will "wrap" to the other direction of the screen.
Wrapping2d,
/// Non-wrapping scope menu
///
/// Controlled with [`NavRequest::ScopeMove`](crate::NavRequest::ScopeMove)
/// even when the focused element is not in this menu, but in a submenu
/// reachable from this one.
BoundScope,
/// Wrapping scope menu
///
/// Controlled with [`NavRequest::ScopeMove`](crate::NavRequest::ScopeMove) even
/// when the focused element is not in this menu, but in a submenu reachable from this one.
WrappingScope,
}
impl NavMenu {
pub(crate) fn bound(&self) -> bool {
matches!(self, NavMenu::BoundScope | NavMenu::Bound2d)
}
pub(crate) fn is_2d(&self) -> bool {
!self.is_scope()
}
pub(crate) fn is_scope(&self) -> bool {
matches!(self, NavMenu::BoundScope | NavMenu::WrappingScope)
}
}
/// A "seed" for creation of a [`NavMenu`].
///
/// Internally, `bevy_ui_navigation` uses a special component to mark UI nodes
/// as "menus", this tells the navigation algorithm to add that component to
/// this `Entity`.
#[derive(Bundle, Clone)]
pub struct MenuSeed {
seed: TreeMenuSeed,
}
impl MenuSeed {
// FIXME: consider having a `Markable` trait with the `marking` method, so
// that I don't have to copy/paste this description between `MenuSeed` and
// `NamedMenuSeed`
/// Create a [`NavMenu`] that will automatically mark all it's contained
/// [`Focusable`](crate::Focusable)s with `T`.
///
/// `marker` is the component that will be added to all [`Focusable`](crate::Focusable)
/// entities contained within this menu.
pub fn marking<T: Component>(self, marker: T) -> MarkingMenuSeed<T> {
let Self { seed } = self;
let marker = NavMarker(marker);
MarkingMenuSeed { seed, marker }
}
}
/// Bundle to specify creation of a [`NavMenu`] refering to their parent
/// focusable by [`Name`](https://docs.rs/bevy/0.6.0/bevy/core/struct.Name.html)
///
/// 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.
#[derive(Bundle, Clone)]
pub struct NamedMenuSeed {
seed: TreeMenuSeed,
parent_name: ParentName,
}
impl NamedMenuSeed {
/// Create a [`NavMenu`] that will automatically mark all it's contained
/// [`Focusable`](crate::Focusable)s with `T`, on top of referring to its parent by name.
///
/// `marker` is the component that will be added to all [`Focusable`](crate::Focusable)
/// entities contained within this menu.
pub fn marking<T: Component>(self, marker: T) -> NamedMarkingMenuSeed<T> {
let Self { seed, parent_name } = self;
NamedMarkingMenuSeed {
seed,
marker: NavMarker(marker),
parent_name,
}
}
}
/// A [`NavMenu`] with automatic `T` marker propagation
///
/// A `NavMenu` created from this bundle will automatically mark all
/// [`Focusable`](crate::Focusable)s within that menu with the `T` component.
///
/// In order for `T` to propagate to the children of this menu, you need to add
/// the [`NavMarkerPropagationPlugin<T>`](crate::NavMarkerPropagationPlugin) to
/// your bevy app.
#[derive(Bundle, Clone)]
pub struct MarkingMenuSeed<T: Send + Sync + 'static> {
seed: TreeMenuSeed,
marker: NavMarker<T>,
}
/// This is a combination of [`MarkingMenuSeed`] and [`NamedMenuSeed`], see the
/// documentation of those items for details.
#[derive(Bundle, Clone)]
pub struct NamedMarkingMenuSeed<T: Send + Sync + 'static> {
seed: TreeMenuSeed,
parent_name: ParentName,
marker: NavMarker<T>,
}
impl NavMenu {
fn seed(self, focus_parent: FailableOption<Entity>) -> TreeMenuSeed {
TreeMenuSeed {
focus_parent,
menu: self,
}
}
/// Spawn a [`NavMenu`] seed with provided parent entity (or root if
/// `None`).
///
/// Prefer [`Self::reachable_from`] and [`Self::root`] to this if you don't
/// already have an `Option<Entity>`.
pub fn with_parent(self, focus_parent: Option<Entity>) -> MenuSeed {
let seed = self.seed(focus_parent.into());
MenuSeed { seed }
}
/// Spawn this menu with no parents.
///
/// No [`Focusable`](crate::Focusable) will "lead to" this menu. You either need to
/// programmatically give focus to this menu tree with
/// [`NavRequest::FocusOn`](crate::NavRequest::FocusOn) or have only one root menu.
pub fn root(self) -> MenuSeed {
self.with_parent(None)
}
/// Spawn this menu as reachable from a given [`Focusable`](crate::Focusable)
///
/// When requesting [`NavRequest::Action`](crate::NavRequest::Action)
/// when `focusable` 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 `NavMenu` X from `Focusable` Y if there is a path from
/// `NavMenu` X to `Focusable` Y.
pub fn reachable_from(self, focusable: Entity) -> MenuSeed {
self.with_parent(Some(focusable))
}
/// Spawn this menu as reachable from a [`Focusable`](crate::Focusable) with a
/// [`Name`](https://docs.rs/bevy/0.6.0/bevy/core/struct.Name.html)
/// 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.
pub fn reachable_from_named(self, parent_label: impl Into<Cow<'static, str>>) -> NamedMenuSeed {
NamedMenuSeed {
parent_name: ParentName(Name::new(parent_label)),
seed: self.seed(FailableOption::Uninit),
}
}
}