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),
        }
    }
}