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
//! Contains menu-related components.

use std::borrow::Cow;

use bevy::core::Name;
use bevy::ecs::{entity::Entity, prelude::Component};

/// 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`].
///
/// # Invariants
///
/// **You need to follow those rules (invariants) to avoid panics**:
/// 1. A `MenuSetting` must have **at least one** [`Focusable`] child in the UI
///    hierarchy.
/// 2. There must **not** be a menu loop. i.e.: a way to go from menu A to menu B and
///    then from menu B to menu A while never going back.
///
/// # 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.
///
/// [`NavRequest`]: crate::prelude::NavRequest
/// [`Focusable`]: crate::prelude::Focusable
/// [`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)]
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
    }
    pub fn new() -> Self {
        Self::default()
    }
    // Set `wrapping` to true.
    pub fn wrapping(mut self) -> Self {
        self.wrapping = true;
        self
    }
    // Set `scope` to true.
    pub fn scope(mut self) -> Self {
        self.scope = true;
        self
    }
}