Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_input_focus/src/directional_navigation.rs
9366 views
1
//! A manual navigation framework for moving between focusable elements based on directional input.
2
//!
3
//! Note: If using `bevy_ui`, this manual navigation framework is used to provide overrides
4
//! for its automatic navigation framework based on the `AutoDirectionalNavigation` component.
5
//! Most times, the automatic navigation framework alone should be sufficient.
6
//! If not using `bevy_ui`, this manual navigation framework can still be used by itself.
7
//!
8
//! While virtual cursors are a common way to navigate UIs with a gamepad (or arrow keys!),
9
//! they are generally both slow and frustrating to use.
10
//! Instead, directional inputs should provide a direct way to snap between focusable elements.
11
//!
12
//! Like the rest of this crate, the [`InputFocus`] resource is manipulated to track
13
//! the current focus.
14
//!
15
//! This module's [`DirectionalNavigationMap`] stores a directed graph of focusable entities.
16
//! Each entity can have up to 8 neighbors, one for each [`CompassOctant`], balancing
17
//! flexibility and required precision.
18
//!
19
//! Navigating between focusable entities (commonly UI nodes) is done by
20
//! passing a [`CompassOctant`] into the [`navigate`](DirectionalNavigation::navigate) method
21
//! from the [`DirectionalNavigation`] system parameter. Under the hood, the
22
//! [`DirectionalNavigationMap`] is used to return the focusable entity in a direction
23
//! for a given entity.
24
//!
25
//! # Setting up Directional Navigation
26
//!
27
//! ## Automatic Navigation (Recommended)
28
//!
29
//! The easiest way to set up navigation is to add the `AutoDirectionalNavigation` component
30
//! to your UI entities. This component is available in the `bevy_ui` crate. If you choose to
31
//! include automatic navigation, you should also use the `AutoDirectionalNavigator` system parameter
32
//! in that crate instead of [`DirectionalNavigation`].
33
//!
34
//! ## Combining Automatic Navigation with Manual Overrides
35
//!
36
//! Following manual edges always take precedence, allowing you to use
37
//! automatic navigation for most UI elements while overriding specific connections for
38
//! special cases like wrapping menus or cross-layer navigation. If you need to override
39
//! automatic navigation behavior, use the [`DirectionalNavigationMap`] to define
40
//! overriding edges between UI entities.
41
//!
42
//! ## Manual Navigation Only
43
//!
44
//! Manually define your navigation using the [`DirectionalNavigationMap`], and use the
45
//! [`DirectionalNavigation`] system parameter to navigate between components.
46
//! You can define navigation connections using methods like
47
//! [`add_edge`](DirectionalNavigationMap::add_edge) and
48
//! [`add_looping_edges`](DirectionalNavigationMap::add_looping_edges).
49
//!
50
//! ## When to Use Manual Navigation or Manual Overrides
51
//!
52
//! While automatic navigation is recommended and satisfactory for most use cases,
53
//! using manual navigation only or integrating manual overrides to automatic navigation provide:
54
//!
55
//! - **Precise control**: Define exact navigation flow, including non-obvious connections like looping edges
56
//! - **Cross-layer navigation**: Connect elements across different UI layers or z-index levels
57
//! - **Custom behavior**: Implement domain-specific navigation patterns (e.g., spreadsheet-style wrapping)
58
59
use crate::{navigator::find_best_candidate, InputFocus};
60
use bevy_app::prelude::*;
61
use bevy_ecs::{
62
entity::{EntityHashMap, EntityHashSet},
63
prelude::*,
64
system::SystemParam,
65
};
66
use bevy_math::{CompassOctant, Vec2};
67
use thiserror::Error;
68
69
#[cfg(feature = "bevy_reflect")]
70
use bevy_reflect::{prelude::*, Reflect};
71
72
/// A plugin that sets up the directional navigation resources.
73
#[derive(Default)]
74
pub struct DirectionalNavigationPlugin;
75
76
impl Plugin for DirectionalNavigationPlugin {
77
fn build(&self, app: &mut App) {
78
app.init_resource::<DirectionalNavigationMap>()
79
.init_resource::<AutoNavigationConfig>();
80
}
81
}
82
83
/// Configuration resource for automatic directional navigation and for generating manual
84
/// navigation edges via [`auto_generate_navigation_edges`]
85
///
86
/// This resource controls how nodes should be automatically connected in each direction.
87
#[derive(Resource, Debug, Clone, PartialEq)]
88
#[cfg_attr(
89
feature = "bevy_reflect",
90
derive(Reflect),
91
reflect(Resource, Debug, PartialEq, Clone)
92
)]
93
pub struct AutoNavigationConfig {
94
/// Minimum overlap ratio (0.0-1.0) required along the perpendicular axis for cardinal directions.
95
///
96
/// This parameter controls how much two UI elements must overlap in the perpendicular direction
97
/// to be considered reachable neighbors. It only applies to cardinal directions (`North`, `South`, `East`, `West`);
98
/// diagonal directions (`NorthEast`, `SouthEast`, etc.) ignore this requirement entirely.
99
///
100
/// # Calculation
101
///
102
/// The overlap factor is calculated as:
103
/// ```text
104
/// overlap_factor = actual_overlap / min(origin_size, candidate_size)
105
/// ```
106
///
107
/// For East/West navigation, this measures vertical overlap:
108
/// - `actual_overlap` = overlapping height between the two elements
109
/// - Sizes are the heights of the origin and candidate
110
///
111
/// For North/South navigation, this measures horizontal overlap:
112
/// - `actual_overlap` = overlapping width between the two elements
113
/// - Sizes are the widths of the origin and candidate
114
///
115
/// # Examples
116
///
117
/// - `0.0` (default): Any overlap is sufficient. Even if elements barely touch, they can be neighbors.
118
/// - `0.5`: Elements must overlap by at least 50% of the smaller element's size.
119
/// - `1.0`: Perfect alignment required. The smaller element must be completely within the bounds
120
/// of the larger element along the perpendicular axis.
121
///
122
/// # Use Cases
123
///
124
/// - **Sparse/irregular layouts** (e.g., star constellations): Use `0.0` to allow navigation
125
/// between elements that don't directly align.
126
/// - **Grid layouts**: Use `0.5` or higher to ensure navigation only connects elements in
127
/// the same row or column.
128
/// - **Strict alignment**: Use `1.0` to require perfect alignment, though this may result
129
/// in disconnected navigation graphs if elements aren't precisely aligned.
130
pub min_alignment_factor: f32,
131
132
/// Maximum search distance in logical pixels.
133
///
134
/// Nodes beyond this distance won't be connected. `None` means unlimited.
135
/// The distance between two UI elements is calculated using their closest edges.
136
pub max_search_distance: Option<f32>,
137
138
/// Whether to prefer nodes that are more aligned with the exact direction.
139
///
140
/// When `true`, nodes that are more directly in line with the requested direction
141
/// will be strongly preferred over nodes at an angle.
142
pub prefer_aligned: bool,
143
}
144
145
impl Default for AutoNavigationConfig {
146
fn default() -> Self {
147
Self {
148
min_alignment_factor: 0.0, // Any overlap is acceptable
149
max_search_distance: None, // No distance limit
150
prefer_aligned: true, // Prefer well-aligned nodes
151
}
152
}
153
}
154
155
/// The up-to-eight neighbors of a focusable entity, one for each [`CompassOctant`].
156
#[derive(Default, Debug, Clone, PartialEq)]
157
#[cfg_attr(
158
feature = "bevy_reflect",
159
derive(Reflect),
160
reflect(Default, Debug, PartialEq, Clone)
161
)]
162
pub struct NavNeighbors {
163
/// The array of neighbors, one for each [`CompassOctant`].
164
/// The mapping between array elements and directions is determined by [`CompassOctant::to_index`].
165
///
166
/// If no neighbor exists in a given direction, the value will be [`None`].
167
/// In most cases, using [`NavNeighbors::set`] and [`NavNeighbors::get`]
168
/// will be more ergonomic than directly accessing this array.
169
pub neighbors: [Option<Entity>; 8],
170
}
171
172
impl NavNeighbors {
173
/// An empty set of neighbors.
174
pub const EMPTY: NavNeighbors = NavNeighbors {
175
neighbors: [None; 8],
176
};
177
178
/// Get the neighbor for a given [`CompassOctant`].
179
pub const fn get(&self, octant: CompassOctant) -> Option<Entity> {
180
self.neighbors[octant.to_index()]
181
}
182
183
/// Set the neighbor for a given [`CompassOctant`].
184
pub const fn set(&mut self, octant: CompassOctant, entity: Entity) {
185
self.neighbors[octant.to_index()] = Some(entity);
186
}
187
}
188
189
/// A resource that stores the manually specified traversable graph of focusable entities.
190
///
191
/// Each entity can have up to 8 neighbors, one for each [`CompassOctant`].
192
///
193
/// To ensure that your graph is intuitive to navigate and generally works correctly, it should be:
194
///
195
/// - **Connected**: Every focusable entity should be reachable from every other focusable entity.
196
/// - **Symmetric**: If entity A is a neighbor of entity B, then entity B should be a neighbor of entity A, ideally in the reverse direction.
197
/// - **Physical**: The direction of navigation should match the layout of the entities when possible,
198
/// although looping around the edges of the screen is also acceptable.
199
/// - **Not self-connected**: An entity should not be a neighbor of itself; use [`None`] instead.
200
///
201
/// This graph must be built and maintained manually, and the developer is responsible for ensuring that it meets the above criteria.
202
/// Notably, if the developer adds or removes the navigability of an entity, the developer should update the map as necessary.
203
///
204
/// If the automatic navigation system in `bevy_ui` is being used, this resource can be used to specify
205
/// manual navigation overrides. Any navigation edges specified in this map take precedence over automatic
206
/// navigation. For example, if navigation on one side of the window should wrap around to
207
/// the other side of the window, this navigation behavior can be specified using this map.
208
#[derive(Resource, Debug, Default, Clone, PartialEq)]
209
#[cfg_attr(
210
feature = "bevy_reflect",
211
derive(Reflect),
212
reflect(Resource, Debug, Default, PartialEq, Clone)
213
)]
214
pub struct DirectionalNavigationMap {
215
/// A directed graph of focusable entities.
216
///
217
/// Pass in the current focus as a key, and get back a collection of up to 8 neighbors,
218
/// each keyed by a [`CompassOctant`].
219
pub neighbors: EntityHashMap<NavNeighbors>,
220
}
221
222
impl DirectionalNavigationMap {
223
/// Removes an entity from the navigation map, including all connections to and from it.
224
///
225
/// Note that this is an O(n) operation, where n is the number of entities in the map,
226
/// as we must iterate over each entity to check for connections to the removed entity.
227
///
228
/// If you are removing multiple entities, consider using [`remove_multiple`](Self::remove_multiple) instead.
229
pub fn remove(&mut self, entity: Entity) {
230
self.neighbors.remove(&entity);
231
232
for node in self.neighbors.values_mut() {
233
for neighbor in node.neighbors.iter_mut() {
234
if *neighbor == Some(entity) {
235
*neighbor = None;
236
}
237
}
238
}
239
}
240
241
/// Removes a collection of entities from the navigation map.
242
///
243
/// While this is still an O(n) operation, where n is the number of entities in the map,
244
/// it is more efficient than calling [`remove`](Self::remove) multiple times,
245
/// as we can check for connections to all removed entities in a single pass.
246
///
247
/// An [`EntityHashSet`] must be provided as it is noticeably faster than the standard hasher or a [`Vec`](`alloc::vec::Vec`).
248
pub fn remove_multiple(&mut self, entities: EntityHashSet) {
249
for entity in &entities {
250
self.neighbors.remove(entity);
251
}
252
253
for node in self.neighbors.values_mut() {
254
for neighbor in node.neighbors.iter_mut() {
255
if let Some(entity) = *neighbor {
256
if entities.contains(&entity) {
257
*neighbor = None;
258
}
259
}
260
}
261
}
262
}
263
264
/// Completely clears the navigation map, removing all entities and connections.
265
pub fn clear(&mut self) {
266
self.neighbors.clear();
267
}
268
269
/// Adds an edge between two entities in the navigation map.
270
/// Any existing edge from A in the provided direction will be overwritten.
271
///
272
/// The reverse edge will not be added, so navigation will only be possible in one direction.
273
/// If you want to add a symmetrical edge, use [`add_symmetrical_edge`](Self::add_symmetrical_edge) instead.
274
pub fn add_edge(&mut self, a: Entity, b: Entity, direction: CompassOctant) {
275
self.neighbors
276
.entry(a)
277
.or_insert(NavNeighbors::EMPTY)
278
.set(direction, b);
279
}
280
281
/// Adds a symmetrical edge between two entities in the navigation map.
282
/// The A -> B path will use the provided direction, while B -> A will use the [`CompassOctant::opposite`] variant.
283
///
284
/// Any existing connections between the two entities will be overwritten.
285
pub fn add_symmetrical_edge(&mut self, a: Entity, b: Entity, direction: CompassOctant) {
286
self.add_edge(a, b, direction);
287
self.add_edge(b, a, direction.opposite());
288
}
289
290
/// Add symmetrical edges between each consecutive pair of entities in the provided slice.
291
///
292
/// Unlike [`add_looping_edges`](Self::add_looping_edges), this method does not loop back to the first entity.
293
pub fn add_edges(&mut self, entities: &[Entity], direction: CompassOctant) {
294
for pair in entities.windows(2) {
295
self.add_symmetrical_edge(pair[0], pair[1], direction);
296
}
297
}
298
299
/// Add symmetrical edges between each consecutive pair of entities in the provided slice, looping back to the first entity at the end.
300
///
301
/// This is useful for creating a circular navigation path between a set of entities, such as a menu.
302
pub fn add_looping_edges(&mut self, entities: &[Entity], direction: CompassOctant) {
303
self.add_edges(entities, direction);
304
if let Some((first_entity, rest)) = entities.split_first() {
305
if let Some(last_entity) = rest.last() {
306
self.add_symmetrical_edge(*last_entity, *first_entity, direction);
307
}
308
}
309
}
310
311
/// Gets the entity in a given direction from the current focus, if any.
312
pub fn get_neighbor(&self, focus: Entity, octant: CompassOctant) -> Option<Entity> {
313
self.neighbors
314
.get(&focus)
315
.and_then(|neighbors| neighbors.get(octant))
316
}
317
318
/// Looks up the neighbors of a given entity.
319
///
320
/// If the entity is not in the map, [`None`] will be returned.
321
/// Note that the set of neighbors is not guaranteed to be non-empty though!
322
pub fn get_neighbors(&self, entity: Entity) -> Option<&NavNeighbors> {
323
self.neighbors.get(&entity)
324
}
325
}
326
327
/// A system parameter for navigating between focusable entities in a directional way.
328
#[derive(SystemParam, Debug)]
329
pub struct DirectionalNavigation<'w> {
330
/// The currently focused entity.
331
pub focus: ResMut<'w, InputFocus>,
332
/// The directional navigation map containing manually defined connections between entities.
333
pub map: Res<'w, DirectionalNavigationMap>,
334
}
335
336
impl<'w> DirectionalNavigation<'w> {
337
/// Navigates to the neighbor in a given direction from the current focus, if any.
338
///
339
/// Returns the new focus if successful.
340
/// Returns an error if there is no focus set or if there is no neighbor in the requested direction.
341
///
342
/// If the result was `Ok`, the [`InputFocus`] resource is updated to the new focus as part of this method call.
343
pub fn navigate(
344
&mut self,
345
direction: CompassOctant,
346
) -> Result<Entity, DirectionalNavigationError> {
347
if let Some(current_focus) = self.focus.0 {
348
// Respect manual edges first
349
if let Some(new_focus) = self.map.get_neighbor(current_focus, direction) {
350
self.focus.set(new_focus);
351
Ok(new_focus)
352
} else {
353
Err(DirectionalNavigationError::NoNeighborInDirection {
354
current_focus,
355
direction,
356
})
357
}
358
} else {
359
Err(DirectionalNavigationError::NoFocus)
360
}
361
}
362
}
363
364
/// An error that can occur when navigating between focusable entities using [directional navigation](crate::directional_navigation).
365
#[derive(Debug, PartialEq, Clone, Error)]
366
pub enum DirectionalNavigationError {
367
/// No focusable entity is currently set.
368
#[error("No focusable entity is currently set.")]
369
NoFocus,
370
/// No neighbor in the requested direction.
371
#[error("No neighbor from {current_focus} in the {direction:?} direction.")]
372
NoNeighborInDirection {
373
/// The entity that was the focus when the error occurred.
374
current_focus: Entity,
375
/// The direction in which the navigation was attempted.
376
direction: CompassOctant,
377
},
378
}
379
380
/// A focusable area with position and size information.
381
///
382
/// This struct represents a UI element used during directional navigation,
383
/// containing its entity ID, center position, and size for spatial navigation calculations.
384
///
385
/// The term "focusable area" avoids confusion with UI `Node` components in `bevy_ui`.
386
#[derive(Debug, Clone, Copy, PartialEq)]
387
#[cfg_attr(
388
feature = "bevy_reflect",
389
derive(Reflect),
390
reflect(Debug, PartialEq, Clone)
391
)]
392
pub struct FocusableArea {
393
/// The entity identifier for this focusable area.
394
pub entity: Entity,
395
/// The center position in global coordinates.
396
pub position: Vec2,
397
/// The size (width, height) of the area.
398
pub size: Vec2,
399
}
400
401
/// Trait for extracting position and size from navigable UI components.
402
///
403
/// This allows the auto-navigation system to work with different UI implementations
404
/// as long as they can provide position and size information.
405
pub trait Navigable {
406
/// Returns the center position and size in global coordinates.
407
fn get_bounds(&self) -> (Vec2, Vec2);
408
}
409
410
/// Automatically generates directional navigation edges for a collection of nodes.
411
///
412
/// This function takes a slice of navigation nodes with their positions and sizes, and populates
413
/// the navigation map with edges to the nearest neighbor in each compass direction.
414
/// Manual edges already in the map are preserved and not overwritten.
415
///
416
/// # Arguments
417
///
418
/// * `nav_map` - The navigation map to populate
419
/// * `nodes` - A slice of [`FocusableArea`] structs containing entity, position, and size data
420
/// * `config` - Configuration for the auto-generation algorithm
421
///
422
/// # Example
423
///
424
/// ```rust
425
/// # use bevy_input_focus::directional_navigation::*;
426
/// # use bevy_ecs::entity::Entity;
427
/// # use bevy_math::Vec2;
428
/// let mut nav_map = DirectionalNavigationMap::default();
429
/// let config = AutoNavigationConfig::default();
430
///
431
/// let nodes = vec![
432
/// FocusableArea { entity: Entity::PLACEHOLDER, position: Vec2::new(100.0, 100.0), size: Vec2::new(50.0, 50.0) },
433
/// FocusableArea { entity: Entity::PLACEHOLDER, position: Vec2::new(200.0, 100.0), size: Vec2::new(50.0, 50.0) },
434
/// ];
435
///
436
/// auto_generate_navigation_edges(&mut nav_map, &nodes, &config);
437
/// ```
438
pub fn auto_generate_navigation_edges(
439
nav_map: &mut DirectionalNavigationMap,
440
nodes: &[FocusableArea],
441
config: &AutoNavigationConfig,
442
) {
443
// For each node, find best neighbor in each direction
444
for origin in nodes {
445
for octant in [
446
CompassOctant::North,
447
CompassOctant::NorthEast,
448
CompassOctant::East,
449
CompassOctant::SouthEast,
450
CompassOctant::South,
451
CompassOctant::SouthWest,
452
CompassOctant::West,
453
CompassOctant::NorthWest,
454
] {
455
// Skip if manual edge already exists (check inline to avoid borrow issues)
456
if nav_map
457
.get_neighbors(origin.entity)
458
.and_then(|neighbors| neighbors.get(octant))
459
.is_some()
460
{
461
continue; // Respect manual override
462
}
463
464
// Find best candidate in this direction
465
let best_candidate = find_best_candidate(origin, octant, nodes, config);
466
467
// Add edge if we found a valid candidate
468
if let Some(neighbor) = best_candidate {
469
nav_map.add_edge(origin.entity, neighbor, octant);
470
}
471
}
472
}
473
}
474
475
#[cfg(test)]
476
mod tests {
477
use alloc::vec;
478
use bevy_ecs::system::RunSystemOnce;
479
480
use super::*;
481
482
#[test]
483
fn setting_and_getting_nav_neighbors() {
484
let mut neighbors = NavNeighbors::EMPTY;
485
assert_eq!(neighbors.get(CompassOctant::SouthEast), None);
486
487
neighbors.set(CompassOctant::SouthEast, Entity::PLACEHOLDER);
488
489
for i in 0..8 {
490
if i == CompassOctant::SouthEast.to_index() {
491
assert_eq!(
492
neighbors.get(CompassOctant::SouthEast),
493
Some(Entity::PLACEHOLDER)
494
);
495
} else {
496
assert_eq!(neighbors.get(CompassOctant::from_index(i).unwrap()), None);
497
}
498
}
499
}
500
501
#[test]
502
fn simple_set_and_get_navmap() {
503
let mut world = World::new();
504
let a = world.spawn_empty().id();
505
let b = world.spawn_empty().id();
506
507
let mut map = DirectionalNavigationMap::default();
508
map.add_edge(a, b, CompassOctant::SouthEast);
509
510
assert_eq!(map.get_neighbor(a, CompassOctant::SouthEast), Some(b));
511
assert_eq!(
512
map.get_neighbor(b, CompassOctant::SouthEast.opposite()),
513
None
514
);
515
}
516
517
#[test]
518
fn symmetrical_edges() {
519
let mut world = World::new();
520
let a = world.spawn_empty().id();
521
let b = world.spawn_empty().id();
522
523
let mut map = DirectionalNavigationMap::default();
524
map.add_symmetrical_edge(a, b, CompassOctant::North);
525
526
assert_eq!(map.get_neighbor(a, CompassOctant::North), Some(b));
527
assert_eq!(map.get_neighbor(b, CompassOctant::South), Some(a));
528
}
529
530
#[test]
531
fn remove_nodes() {
532
let mut world = World::new();
533
let a = world.spawn_empty().id();
534
let b = world.spawn_empty().id();
535
536
let mut map = DirectionalNavigationMap::default();
537
map.add_edge(a, b, CompassOctant::North);
538
map.add_edge(b, a, CompassOctant::South);
539
540
assert_eq!(map.get_neighbor(a, CompassOctant::North), Some(b));
541
assert_eq!(map.get_neighbor(b, CompassOctant::South), Some(a));
542
543
map.remove(b);
544
545
assert_eq!(map.get_neighbor(a, CompassOctant::North), None);
546
assert_eq!(map.get_neighbor(b, CompassOctant::South), None);
547
}
548
549
#[test]
550
fn remove_multiple_nodes() {
551
let mut world = World::new();
552
let a = world.spawn_empty().id();
553
let b = world.spawn_empty().id();
554
let c = world.spawn_empty().id();
555
556
let mut map = DirectionalNavigationMap::default();
557
map.add_edge(a, b, CompassOctant::North);
558
map.add_edge(b, a, CompassOctant::South);
559
map.add_edge(b, c, CompassOctant::East);
560
map.add_edge(c, b, CompassOctant::West);
561
562
let mut to_remove = EntityHashSet::default();
563
to_remove.insert(b);
564
to_remove.insert(c);
565
566
map.remove_multiple(to_remove);
567
568
assert_eq!(map.get_neighbor(a, CompassOctant::North), None);
569
assert_eq!(map.get_neighbor(b, CompassOctant::South), None);
570
assert_eq!(map.get_neighbor(b, CompassOctant::East), None);
571
assert_eq!(map.get_neighbor(c, CompassOctant::West), None);
572
}
573
574
#[test]
575
fn edges() {
576
let mut world = World::new();
577
let a = world.spawn_empty().id();
578
let b = world.spawn_empty().id();
579
let c = world.spawn_empty().id();
580
581
let mut map = DirectionalNavigationMap::default();
582
map.add_edges(&[a, b, c], CompassOctant::East);
583
584
assert_eq!(map.get_neighbor(a, CompassOctant::East), Some(b));
585
assert_eq!(map.get_neighbor(b, CompassOctant::East), Some(c));
586
assert_eq!(map.get_neighbor(c, CompassOctant::East), None);
587
588
assert_eq!(map.get_neighbor(a, CompassOctant::West), None);
589
assert_eq!(map.get_neighbor(b, CompassOctant::West), Some(a));
590
assert_eq!(map.get_neighbor(c, CompassOctant::West), Some(b));
591
}
592
593
#[test]
594
fn looping_edges() {
595
let mut world = World::new();
596
let a = world.spawn_empty().id();
597
let b = world.spawn_empty().id();
598
let c = world.spawn_empty().id();
599
600
let mut map = DirectionalNavigationMap::default();
601
map.add_looping_edges(&[a, b, c], CompassOctant::East);
602
603
assert_eq!(map.get_neighbor(a, CompassOctant::East), Some(b));
604
assert_eq!(map.get_neighbor(b, CompassOctant::East), Some(c));
605
assert_eq!(map.get_neighbor(c, CompassOctant::East), Some(a));
606
607
assert_eq!(map.get_neighbor(a, CompassOctant::West), Some(c));
608
assert_eq!(map.get_neighbor(b, CompassOctant::West), Some(a));
609
assert_eq!(map.get_neighbor(c, CompassOctant::West), Some(b));
610
}
611
612
#[test]
613
fn manual_nav_with_system_param() {
614
let mut world = World::new();
615
let a = world.spawn_empty().id();
616
let b = world.spawn_empty().id();
617
let c = world.spawn_empty().id();
618
619
let mut map = DirectionalNavigationMap::default();
620
map.add_looping_edges(&[a, b, c], CompassOctant::East);
621
622
world.insert_resource(map);
623
624
let mut focus = InputFocus::default();
625
focus.set(a);
626
world.insert_resource(focus);
627
628
let config = AutoNavigationConfig::default();
629
world.insert_resource(config);
630
631
assert_eq!(world.resource::<InputFocus>().get(), Some(a));
632
633
fn navigate_east(mut nav: DirectionalNavigation) {
634
nav.navigate(CompassOctant::East).unwrap();
635
}
636
637
world.run_system_once(navigate_east).unwrap();
638
assert_eq!(world.resource::<InputFocus>().get(), Some(b));
639
640
world.run_system_once(navigate_east).unwrap();
641
assert_eq!(world.resource::<InputFocus>().get(), Some(c));
642
643
world.run_system_once(navigate_east).unwrap();
644
assert_eq!(world.resource::<InputFocus>().get(), Some(a));
645
}
646
647
#[test]
648
fn test_auto_generate_navigation_edges() {
649
let mut nav_map = DirectionalNavigationMap::default();
650
let config = AutoNavigationConfig::default();
651
652
// Create a 2x2 grid of nodes (using UI coordinates: smaller Y = higher on screen)
653
let node_a = Entity::from_bits(1); // Top-left
654
let node_b = Entity::from_bits(2); // Top-right
655
let node_c = Entity::from_bits(3); // Bottom-left
656
let node_d = Entity::from_bits(4); // Bottom-right
657
658
let nodes = vec![
659
FocusableArea {
660
entity: node_a,
661
position: Vec2::new(0.0, 0.0),
662
size: Vec2::new(50.0, 50.0),
663
}, // Top-left
664
FocusableArea {
665
entity: node_b,
666
position: Vec2::new(100.0, 0.0),
667
size: Vec2::new(50.0, 50.0),
668
}, // Top-right
669
FocusableArea {
670
entity: node_c,
671
position: Vec2::new(0.0, 100.0),
672
size: Vec2::new(50.0, 50.0),
673
}, // Bottom-left
674
FocusableArea {
675
entity: node_d,
676
position: Vec2::new(100.0, 100.0),
677
size: Vec2::new(50.0, 50.0),
678
}, // Bottom-right
679
];
680
681
auto_generate_navigation_edges(&mut nav_map, &nodes, &config);
682
683
// Test horizontal navigation
684
assert_eq!(
685
nav_map.get_neighbor(node_a, CompassOctant::East),
686
Some(node_b)
687
);
688
assert_eq!(
689
nav_map.get_neighbor(node_b, CompassOctant::West),
690
Some(node_a)
691
);
692
693
// Test vertical navigation
694
assert_eq!(
695
nav_map.get_neighbor(node_a, CompassOctant::South),
696
Some(node_c)
697
);
698
assert_eq!(
699
nav_map.get_neighbor(node_c, CompassOctant::North),
700
Some(node_a)
701
);
702
703
// Test diagonal navigation
704
assert_eq!(
705
nav_map.get_neighbor(node_a, CompassOctant::SouthEast),
706
Some(node_d)
707
);
708
}
709
710
#[test]
711
fn test_auto_generate_respects_manual_edges() {
712
let mut nav_map = DirectionalNavigationMap::default();
713
let config = AutoNavigationConfig::default();
714
715
let node_a = Entity::from_bits(1);
716
let node_b = Entity::from_bits(2);
717
let node_c = Entity::from_bits(3);
718
719
// Manually set an edge from A to C (skipping B)
720
nav_map.add_edge(node_a, node_c, CompassOctant::East);
721
722
let nodes = vec![
723
FocusableArea {
724
entity: node_a,
725
position: Vec2::new(0.0, 0.0),
726
size: Vec2::new(50.0, 50.0),
727
},
728
FocusableArea {
729
entity: node_b,
730
position: Vec2::new(50.0, 0.0),
731
size: Vec2::new(50.0, 50.0),
732
}, // Closer
733
FocusableArea {
734
entity: node_c,
735
position: Vec2::new(100.0, 0.0),
736
size: Vec2::new(50.0, 50.0),
737
},
738
];
739
740
auto_generate_navigation_edges(&mut nav_map, &nodes, &config);
741
742
// The manual edge should be preserved, even though B is closer
743
assert_eq!(
744
nav_map.get_neighbor(node_a, CompassOctant::East),
745
Some(node_c)
746
);
747
}
748
749
#[test]
750
fn test_edge_distance_vs_center_distance() {
751
let mut nav_map = DirectionalNavigationMap::default();
752
let config = AutoNavigationConfig::default();
753
754
let left = Entity::from_bits(1);
755
let wide_top = Entity::from_bits(2);
756
let bottom = Entity::from_bits(3);
757
758
let left_node = FocusableArea {
759
entity: left,
760
position: Vec2::new(100.0, 200.0),
761
size: Vec2::new(100.0, 100.0),
762
};
763
764
let wide_top_node = FocusableArea {
765
entity: wide_top,
766
position: Vec2::new(350.0, 150.0),
767
size: Vec2::new(300.0, 80.0),
768
};
769
770
let bottom_node = FocusableArea {
771
entity: bottom,
772
position: Vec2::new(270.0, 300.0),
773
size: Vec2::new(100.0, 80.0),
774
};
775
776
let nodes = vec![left_node, wide_top_node, bottom_node];
777
778
auto_generate_navigation_edges(&mut nav_map, &nodes, &config);
779
780
assert_eq!(
781
nav_map.get_neighbor(left, CompassOctant::East),
782
Some(wide_top),
783
"Should navigate to wide_top not bottom, even though bottom's center is closer."
784
);
785
}
786
}
787
788