bevy_ui_navigation/
events.rs

1//! Navigation events and requests.
2//!
3//! The navigation system works through bevy's `Events` system.
4//! It is a system with one input and two outputs:
5//! * Input `EventWriter<NavRequest>`, tells the navigation system what to do.
6//!   Your app should have a system that writes to a `EventWriter<NavRequest>`
7//!   based on inputs or internal game state.
8//!   Bevy provides default systems in `bevy_ui`.
9//!   But you can add your own requests on top of the ones the default systems send.
10//!   For example to unlock the UI with [`NavRequest::Unlock`].
11//! * Output [`Focusable`] components.
12//!   The navigation system updates the focusables component
13//!   according to the focus state of the navigation system.
14//!   See `examples/cursor_navigation` directory for usage clues.
15//! * Output `EventReader<NavEvent>`,
16//!   contains specific information about what the navigation system is doing.
17//!
18//! [`Focusable`]: crate::resolve::Focusable
19use bevy::{
20    ecs::{
21        entity::Entity,
22        event::EventReader,
23        query::{ReadOnlyWorldQuery, WorldQuery},
24        system::Query,
25    },
26    math::Vec2,
27    prelude::Event,
28};
29use non_empty_vec::NonEmpty;
30
31use crate::resolve::LockReason;
32
33/// Requests to send to the navigation system to update focus.
34#[derive(Debug, PartialEq, Clone, Copy, Event)]
35pub enum NavRequest {
36    /// Move in in provided direction according to the plugin's [navigation strategy].
37    ///
38    /// Typically used by gamepads.
39    ///
40    /// [navigation strategy]: crate::resolve::MenuNavigationStrategy.
41    Move(Direction),
42
43    /// Move within the encompassing [`MenuSetting::scope`].
44    ///
45    /// [`MenuSetting::scope`]: crate::prelude::MenuSetting::scope
46    ScopeMove(ScopeDirection),
47
48    /// Activate the currently focused [`Focusable`].
49    ///
50    /// If a menu is _[reachable from]_
51    ///
52    /// [`Focusable`]: crate::prelude::Focusable
53    /// [reachable from]: crate::menu::MenuBuilder::NamedParent
54    Action,
55
56    /// Leave this submenu to enter the one it is _[reachable from]_.
57    ///
58    /// [reachable from]: crate::menu::MenuBuilder::NamedParent
59    Cancel,
60
61    /// Move the focus to any arbitrary [`Focusable`] entity.
62    ///
63    /// Note that resolving a `FocusOn` request is expensive,
64    /// make sure you do not spam `FocusOn` messages in your input systems.
65    /// Avoid sending FocusOn messages when you know the target entity is
66    /// already focused.
67    ///
68    /// [`Focusable`]: crate::resolve::Focusable
69    FocusOn(Entity),
70
71    /// Locks the navigation system.
72    ///
73    /// A [`NavEvent::Locked`] will be emitted as a response if the
74    /// navigation system was not already locked.
75    Lock,
76
77    /// Unlocks the navigation system.
78    ///
79    /// A [`NavEvent::Unlocked`] will be emitted as a response if the
80    /// navigation system was indeed locked.
81    Unlock,
82}
83
84/// Direction for movement in [`MenuSetting::scope`] menus.
85///
86/// [`MenuSetting::scope`]: crate::menu::MenuSetting
87#[derive(Debug, PartialEq, Clone, Copy)]
88pub enum ScopeDirection {
89    /// The next focusable in menu, usually goes right.
90    Next,
91
92    /// The previous focusable in menu, usually goes left.
93    Previous,
94}
95
96/// 2d direction to move in normal menus
97#[derive(Debug, PartialEq, Clone, Copy)]
98pub enum Direction {
99    /// Down.
100    South,
101    /// Up.
102    North,
103    /// Right.
104    East,
105    /// Left.
106    West,
107}
108impl Direction {
109    /// Is `other` in direction `self` from `reference`?
110    pub fn is_in(&self, reference: Vec2, other: Vec2) -> bool {
111        let coord = other - reference;
112        use Direction::*;
113        match self {
114            North => coord.y < coord.x && coord.y < -coord.x,
115            South => coord.y > coord.x && coord.y > -coord.x,
116            East => coord.y < coord.x && coord.y > -coord.x,
117            West => coord.y > coord.x && coord.y < -coord.x,
118        }
119    }
120}
121
122/// Events emitted by the navigation system.
123///
124/// Useful if you want to react to [`NavEvent::NoChanges`] event, for example
125/// when a "start game" button is focused and the [`NavRequest::Action`] is
126/// pressed.
127#[derive(Debug, Clone, Event)]
128pub enum NavEvent {
129    /// Tells the app which element is the first one to be focused.
130    ///
131    /// This will be sent whenever the number of focused elements go from 0 to 1.
132    /// Meaning: whenever you spawn a new UI with [`Focusable`] elements.
133    ///
134    /// The order of selection when no [`Focusable`] is focused yet is as follow:
135    /// - The prioritized `Focusable` of the root menu
136    /// - Any prioritized `Focusable`
137    /// - Any `Focusable` in the root menu
138    /// - Any `Focusable`
139    ///
140    /// [`Focusable`]: crate::resolve::Focusable
141    InitiallyFocused(Entity),
142
143    /// Focus changed.
144    ///
145    /// ## Notes
146    ///
147    /// Both `to` and `from` are ascending, meaning that the focused and newly
148    /// focused elements are the first of their respective vectors.
149    ///
150    /// [`NonEmpty`] enables you to safely check `to.first()` or `from.first()`
151    /// without returning an option. It is guaranteed that there is at least
152    /// one element.
153    FocusChanged {
154        /// The list of elements that has become active after the focus
155        /// change
156        to: NonEmpty<Entity>,
157        /// The list of active elements from the focused one to the last
158        /// active which is affected by the focus change
159        from: NonEmpty<Entity>,
160    },
161
162    /// The [`NavRequest`] didn't lead to any change in focus.
163    NoChanges {
164        /// The active elements from the focused one to the last
165        /// active which is affected by the focus change.
166        from: NonEmpty<Entity>,
167        /// The [`NavRequest`] that didn't do anything.
168        request: NavRequest,
169    },
170
171    /// The navigation [lock] has been enabled.
172    /// Either by a [lock focusable] or [`NavRequest::Lock`].
173    ///
174    /// Once the navigation plugin enters a locked state, the only way to exit
175    /// it is to send a [`NavRequest::Unlock`].
176    ///
177    /// [lock]: crate::resolve::NavLock
178    /// [lock focusable]: crate::resolve::Focusable::lock
179    Locked(LockReason),
180
181    /// The navigation [lock] has been released.
182    ///
183    /// The navigation system was in a locked state triggered [`Entity`],
184    /// is now unlocked, and receiving events again.
185    ///
186    /// [lock]: crate::resolve::NavLock
187    Unlocked(LockReason),
188}
189impl NavEvent {
190    /// Create a `FocusChanged` with a single `to`
191    ///
192    /// Usually the `NavEvent::FocusChanged.to` field has a unique value.
193    pub(crate) fn focus_changed(to: Entity, from: NonEmpty<Entity>) -> NavEvent {
194        NavEvent::FocusChanged {
195            from,
196            to: NonEmpty::new(to),
197        }
198    }
199
200    /// Whether this event is a [`NavEvent::NoChanges`]
201    /// triggered by a [`NavRequest::Action`]
202    /// if `entity` is the currently focused element.
203    pub fn is_activated(&self, entity: Entity) -> bool {
204        matches!(self, NavEvent::NoChanges { from,  request: NavRequest::Action } if *from.first() == entity)
205    }
206}
207
208/// Extend [`EventReader<NavEvent>`] with methods
209/// to simplify working with [`NavEvent`]s.
210///
211/// See the [`NavEventReader`] documentation for details.
212///
213/// [`EventReader<NavEvent>`]: EventReader
214pub trait NavEventReaderExt<'w, 's> {
215    /// Create a [`NavEventReader`] from this event reader.
216    fn nav_iter(&mut self) -> NavEventReader<'w, 's, '_>;
217}
218impl<'w, 's> NavEventReaderExt<'w, 's> for EventReader<'w, 's, NavEvent> {
219    fn nav_iter(&mut self) -> NavEventReader<'w, 's, '_> {
220        NavEventReader { event_reader: self }
221    }
222}
223
224/// A wrapper for `EventReader<NavEvent>` to simplify dealing with [`NavEvent`]s.
225pub struct NavEventReader<'w, 's, 'a> {
226    event_reader: &'a mut EventReader<'w, 's, NavEvent>,
227}
228
229impl<'w, 's, 'a> NavEventReader<'w, 's, 'a> {
230    /// Iterate over [`NavEvent::NoChanges`] focused entity
231    /// triggered by `request` type requests.
232    pub fn with_request(&mut self, request: NavRequest) -> impl Iterator<Item = Entity> + '_ {
233        self.event_reader
234            .read()
235            .filter_map(move |nav_event| match nav_event {
236                NavEvent::NoChanges {
237                    from,
238                    request: event_request,
239                } if *event_request == request => Some(*from.first()),
240                _ => None,
241            })
242    }
243    /// Iterate over _activated_ [`Focusable`]s.
244    ///
245    /// A [`Focusable`] is _activated_ when a [`NavRequest::Action`] is sent
246    /// while it is focused, and it doesn't lead to a new menu.
247    ///
248    /// [`Focusable`]: crate::resolve::Focusable
249    pub fn activated(&mut self) -> impl Iterator<Item = Entity> + '_ {
250        self.with_request(NavRequest::Action)
251    }
252
253    /// Iterate over [`NavEvent`]s, associating them
254    /// with the "relevant" entity of the event.
255    pub fn types(&mut self) -> impl Iterator<Item = (&NavEvent, Entity)> + '_ {
256        use NavEvent::{FocusChanged, InitiallyFocused, Locked, NoChanges, Unlocked};
257        self.event_reader.read().filter_map(|event| {
258            let entity = match event {
259                NoChanges { from, .. } => Some(*from.first()),
260                InitiallyFocused(initial) => Some(*initial),
261                FocusChanged { from, .. } => Some(*from.first()),
262                Locked(LockReason::Focusable(from)) => Some(*from),
263                Unlocked(LockReason::Focusable(from)) => Some(*from),
264                _ => None,
265            };
266            entity.map(|e| (event, e))
267        })
268    }
269
270    /// Iterate over query items of _activated_ focusables.
271    ///
272    /// See [`Self::activated`] for meaning of _"activated"_.
273    pub fn activated_in_query<'b, 'c: 'b, Q: ReadOnlyWorldQuery, F: ReadOnlyWorldQuery>(
274        &'b mut self,
275        query: &'c Query<Q, F>,
276    ) -> impl Iterator<Item = Q::Item<'c>> + 'b {
277        query.iter_many(self.activated())
278    }
279
280    /// Run `for_each` with result of `query` for each _activated_ entity.
281    ///
282    /// Unlike [`Self::activated_in_query`] this works with mutable queries.
283    /// see [`Self::activated`] for meaning of _"activated"_.
284    pub fn activated_in_query_foreach_mut<Q: WorldQuery, F: ReadOnlyWorldQuery>(
285        &mut self,
286        query: &mut Query<Q, F>,
287        mut for_each: impl FnMut(Q::Item<'_>),
288    ) {
289        let mut iter = query.iter_many_mut(self.activated());
290        while let Some(item) = iter.fetch_next() {
291            for_each(item)
292        }
293    }
294}