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}