Skip to content

Conversation

@jbuehler23
Copy link
Contributor

@jbuehler23 jbuehler23 commented Oct 27, 2025

Objective

Resolves #21661

Adds automatic directional navigation graph generation based on UI node positions and sizes, eliminating the need for tedious manual graph construction in dynamic UIs.

Solution

Implements a spatial navigation algorithm that automatically computes the nearest neighbor in each compass direction for UI elements, while respecting any manually-defined edges.

Features

  • Automatic edge generation: Finds the best neighbor in each of 8 compass directions based on distance, alignment, and overlap
  • Manual override support: Manual edges always take precedence over auto-generated ones
  • Configurable: AutoNavigationConfig resource allows tuning alignment requirements, distance limits, and preference weighting
  • Opt-in: Entities must have AutoDirectionalNavigation component added to use, and therefore not a breaking change
  • Generic: Core algorithm works with any Vec2 position/size data, not just bevy_ui

Implementation

New Components & Resources (bevy_input_focus/src/directional_navigation.rs):

  • AutoDirectionalNavigation - Marker component to enable auto-navigation
  • AutoNavigationConfig - Configuration resource with settings:
    • min_alignment_factor: Minimum perpendicular overlap (0.0-1.0) required for cardinal directions
    • max_search_distance: Optional distance limit for connections
    • prefer_aligned: Whether to strongly prefer well-aligned nodes

Core Algorithm:

pub fn auto_generate_navigation_edges(
    nav_map: &mut DirectionalNavigationMap,
    nodes: &[(Entity, Vec2, Vec2)],  // (entity, center_pos, size)
    config: &AutoNavigationConfig,
)

For each node and each direction:

  1. Filter candidates that are actually in that direction (cone-based check)
  2. Calculate overlap factor for cardinal directions (horizontal overlap for N/S, vertical for E/W)
  3. Score candidates based on:
    • Distance (closer is better)
    • Alignment with direction vector (more aligned is better)
    • Overlap factor (must meet minimum threshold)
  4. Select the best-scoring candidate as the neighbor

Scoring Formula:

score = distance + alignment_penalty
where alignment_penalty = (1.0 - alignment) * distance * 2.0

This makes misaligned nodes significantly less attractive while still considering distance.

Usage

Before (manual):

// Must manually specify all connections
for row in 0..N_ROWS {
    let entities_in_row: Vec<Entity> = (0..N_COLS)
        .map(|col| button_entities.get(&(row, col)).unwrap())
        .copied()
        .collect();
    directional_nav_map.add_looping_edges(&entities_in_row, CompassOctant::East);
}

// Repeat for columns...
for col in 0..N_COLS {
    let entities_in_column: Vec<Entity> = (0..N_ROWS)
        .map(|row| button_entities.get(&(row, col)).unwrap())
        .copied()
        .collect();
    directional_nav_map.add_edges(&entities_in_column, CompassOctant::South);
}

After:

// Just add the `AutoDirectionalNavigation` component!
commands.spawn((
    Button,
    Node { /* ... */ },
    AutoDirectionalNavigation::default(),
    // ... other components
));

Testing

  • Added new example: auto_directional_navigation
  • Ran existing directional_navigation

Showcase

New Example: auto_directional_navigation

Demonstrates automatic navigation with irregularly-positioned buttons. Unlike a regular grid, these buttons are scattered, but auto-navigation figures out the correct connections - also shows currently focused button, and the last "input" pressed to show the logical flow of navigating:

cargo run --example auto_directional_navigation
Recording.2025-11-05.214742.mp4

Key differences from manual directional_navigation example:

  • No manual add_edges() or add_looping_edges() calls
  • Buttons positioned irregularly (not in a perfect grid)
  • Works with absolute positioning and dynamic layouts

Migration Guide

No breaking changes - this is a purely additive feature.

To adopt automatic navigation:

  1. Add AutoDirectionalNavigation component to focusable entities
  2. Optionally configure AutoNavigationConfig resource

…navigation system

- Introduced  example demonstrating automatic navigation with zero configuration.
- Updated  example to reference the new automatic navigation capabilities.
- Enhanced  to support automatic graph generation based on UI element positions.
- Added  component for automatic navigation graph maintenance.
@jbuehler23 jbuehler23 marked this pull request as ready for review October 27, 2025 15:24
@jbuehler23
Copy link
Contributor Author

jbuehler23 commented Oct 27, 2025

opening this up for review, now that CI has passed! @alice-i-cecile @viridia @fallible-algebra

Copy link
Contributor

@viridia viridia left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall I think this is great.

I'm a little bit concerned about the overlap restriction. Imagine a UI in which you have a constellation of stars (like the magic sklill trees in skyrim) where the individual nodes of the tree are very small and the space between them large. In such a case, nodes will hardly ever overlap along an axis, but you'd still want to navigate between nodes where the line connecting the two nodes was at an angle close to the nav direction.

@jbuehler23
Copy link
Contributor Author

jbuehler23 commented Oct 27, 2025

Overall I think this is great.

I'm a little bit concerned about the overlap restriction. Imagine a UI in which you have a constellation of stars (like the magic sklill trees in skyrim) where the individual nodes of the tree are very small and the space between them large. In such a case, nodes will hardly ever overlap along an axis, but you'd still want to navigate between nodes where the line connecting the two nodes was at an angle close to the nav direction.

This is a great point! However, I think the default configuration would actually handle this scenario:

  • The min_alignment_factor defaults to 0.0, which means any overlap is acceptable - even a single pixel. So even with small nodes spaced far apart, as long as there's any perpendicular overlap at all, the connection will be considered.

That said, I agree the overlap restriction could be problematic for extremely sparse layouts where nodes might not overlap at all on the perpendicular axis, but there are some mitigating factors in the design currently:

  1. Diagonal directions ignore overlap entirely - they always return 1.0 for overlap factor (line 450). So sparse UIs can still navigate via NE/SE/SW/NW even if cardinal directions fail.
  2. The alignment penalty in scoring naturally prefers well-aligned nodes - even among candidates that meet the overlap threshold, the scoring function (lines 495-509) uses dot product alignment to prefer nodes that are more directly in the requested direction.

What we could do for now is improve documentation around this and clarify why/when overlap matters and suggest possible workarounds for this with extremely sparse UIs?

@IQuick143
Copy link
Contributor

How is the graph connectivity with this algorithm?
It's rather suboptimal to have the graph be disconnected, imo worse than oddly skewed and somewhat unintuitive connections.

Maybe there could be some pattern where we run the solver recursively with increasingly more lenient parameters until the graph is sufficiently dense? (This could be up to the user, but we should have a clean demonstration on how to do it.)

PS: I would suggest looking at how Shift+Arrow navigation works in a browser as prior art.

/// Whether to also consider `TabIndex` for navigation order hints.
/// Currently unused but reserved for future functionality.
pub respect_tab_order: bool,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the behavior when entities with AutoDirectionalNavigation are located at the same position, but in different UI layers.

For example, separate UI entity trees that have different root GlobalZIndex, but they are top on each other. You can imagine two floating windows on top of each other, or dropdown on top of UI.

Does this algorithm handle each UI layer separately? Is it possible to configure it to recognize such layers (TabGroup, TabIndex ? ).

Copy link
Contributor Author

@jbuehler23 jbuehler23 Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, the algorithm is z-index agnostic - it treats all entities with AutoDirectionalNavigation as a flat set based purely on XY position. So overlapping windows/layers would incorrectly connect to each other.

For proper layer handling, you'd need to:

  1. Query entities per-layer manually and call auto_generate_navigation_edges() separately for each layer, OR
  2. Use manual edges to explicitly define cross-layer navigation when desired

To avoid making this PR too large, I would add automatic layer support as a future enhancement. You could:

  • Filter by TabGroup component, as suggested!
  • Add layer_id field to AutoDirectionalNavigation
  • Use parent entity hierarchy to detect separate UI trees

Would like to know if you think it's necessary for this PR to address this, or is a follow-up PR better?

Copy link
Contributor

@PPakalns PPakalns Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it can be done in future PR when bevy_ui with multiple layers is used more widely.

Good to hear that currently there is workaround where auto_generate_navigation_edges can be called manually.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you can manually run that function if necessary - I've created #21679 so we don't lose track of this @PPakalns!

@jbuehler23
Copy link
Contributor Author

Thanks for the comments @IQuick143 and @PPakalns I'll try and address these tomorrow - it's late here for me now 😀

@jbuehler23
Copy link
Contributor Author

How is the graph connectivity with this algorithm? It's rather suboptimal to have the graph be disconnected, imo worse than oddly skewed and somewhat unintuitive connections.

Maybe there could be some pattern where we run the solver recursively with increasingly more lenient parameters until the graph is sufficiently dense? (This could be up to the user, but we should have a clean demonstration on how to do it.)

PS: I would suggest looking at how Shift+Arrow navigation works in a browser as prior art.

With default config, the graph should be weakly connected in most layouts, but it's not guaranteed due to the directional cone filter + overlap requirements.

I do like your progressive relaxation idea, however. We could add a helper function:

pub fn ensure_connectivity(
    nav_map: &mut DirectionalNavigationMap,
    nodes: &[(Entity, Vec2, Vec2)],
) {
    // Try default config, then relax if disconnected
}

Or perhaps better, is to just add a config parameter for the user to define:

pub struct AutoNavigationConfig {
    // ... existing fields ...

    /// If true, ensures all nodes are reachable by relaxing constraints
    /// for nodes that would otherwise be isolated
    pub ensure_connectivity: bool,
}

Then we can demonstrate this usage in the auto_directional_navigation example.

I think either of these options give the user control of how to manage the connectivity issue you've described, but remains simple enough so as to not pollute with too many options. What do you think of this?

Re: browser shift+arrow - based on my research it appears to use projection-based selection to guarantee connectivity. It might be best to add this as an alternative mode in the future, and support other algorithms? Something like:

pub enum NavigationAlgorithm {
      OverlapBased,  // Current design
      ProjectionBased, // Like browser Shift+Arrow?
}

@alice-i-cecile alice-i-cecile added this to the 0.18 milestone Oct 29, 2025
@alice-i-cecile alice-i-cecile added C-Feature A new feature, making something new possible A-UI Graphical user interfaces, styles, layouts, and widgets M-Release-Note Work that should be called out in the blog due to impact labels Oct 29, 2025
@github-actions
Copy link
Contributor

It looks like your PR has been selected for a highlight in the next release blog post, but you didn't provide a release note.

Please review the instructions for writing release notes, then expand or revise the content in the release notes directory to showcase your changes.

@alice-i-cecile alice-i-cecile added the S-Needs-Review Needs reviewer attention (from anyone!) to move forward label Oct 29, 2025
@jbuehler23
Copy link
Contributor Author

It looks like your PR has been selected for a highlight in the next release blog post, but you didn't provide a release note.

Please review the instructions for writing release notes, then expand or revise the content in the release notes directory to showcase your changes.

added release notes @alice-i-cecile :)

Copy link
Contributor

@ickshonpe ickshonpe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the example, if you press down to navigate from button 3 to button 5 and then press up, you don't return to button 3 but instead go to button 2. Ideally there should be a history so that navigation can backtrack following the user's route.

@ickshonpe
Copy link
Contributor

Looking at the example, if you press down to navigate from button 3 to button 5 and then press up, you don't return to button 3 but instead go to button 2. Ideally there should be a history so that navigation can backtrack following the user's route.

Oh I see, button 2 can only have one edge for the upwards direction in the navigation map, so it's not that easy.

@viridia
Copy link
Contributor

viridia commented Nov 6, 2025

Looking at the example, if you press down to navigate from button 3 to button 5 and then press up, you don't return to button 3 but instead go to button 2. Ideally there should be a history so that navigation can backtrack following the user's route.

This lack of reversibility is not actually that uncommon in games. I see this in Witcher 3 inventory screen for example. It's a bit annoying, but also kind of expected.

Comment on lines 519 to 530
let origin_left = origin_pos.x - origin_size.x / 2.0;
let origin_right = origin_pos.x + origin_size.x / 2.0;
let cand_left = candidate_pos.x - candidate_size.x / 2.0;
let cand_right = candidate_pos.x + candidate_size.x / 2.0;

let overlap = (origin_right.min(cand_right) - origin_left.max(cand_left)).max(0.0);
let max_overlap = origin_size.x.min(candidate_size.x);
if max_overlap > 0.0 {
overlap / max_overlap
} else {
0.0
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The actual overlap calculations are 1-dimensional and the same on both axes? I think it would better if they were extracted to another function, that is called for each case.

}
}
// Diagonal directions don't require strict overlap
_ => 1.0,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For diagonals we could project onto the diagonal axis and the compare the overlap, or is that not necessary?

Copy link
Contributor Author

@jbuehler23 jbuehler23 Nov 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question! I went with a simpler approach (no overlap check for diagonals) because:

  1. Diagonal navigation is less common in UI (especially so on controllers, I think - happy to be corrected on this though)
  2. The distance and directional checks still filter candidates appropriately
  3. Keeps the code simpler, for initial PR

If we find real-world cases where diagonal navigation feels wrong, we could add diagonal projection as an enhancement later. Can this go in a follow-up PR?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's less common, but there are lots of games that have radial navigation, particularly for things like big skill trees that expand outwards in all directions.

But I'm fine to keep things simple for now as long. As it's behaving predictably, that's the main thing.

@ickshonpe
Copy link
Contributor

Looking at the example, if you press down to navigate from button 3 to button 5 and then press up, you don't return to button 3 but instead go to button 2. Ideally there should be a history so that navigation can backtrack following the user's route.

This lack of reversibility is not actually that uncommon in games. I see this in Witcher 3 inventory screen for example. It's a bit annoying, but also kind of expected.

Yep wouldn't block on this for an intial PR. It's fine to go with a simpler model for now.

@jbuehler23
Copy link
Contributor Author

Thanks for the review @ickshonpe! I'll come back to this tonight once the kids are asleep, been a hectic day at work.

@jbuehler23
Copy link
Contributor Author

@ickshonpe I think I've addressed all of your review comments now, would appreciate another quick check whenever you can please! Thanks again for going through it!

@ickshonpe
Copy link
Contributor

@ickshonpe I think I've addressed all of your review comments now, would appreciate another quick check whenever you can please! Thanks again for going through it!

Sorry for the delay, I'll try and find time to a have a look later today.

Copy link
Contributor

@ickshonpe ickshonpe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy with this now, looks good.

@alice-i-cecile alice-i-cecile added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Nov 10, 2025
@alice-i-cecile alice-i-cecile added this pull request to the merge queue Nov 10, 2025
Merged via the queue into bevyengine:main with commit 0c90f04 Nov 10, 2025
42 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-UI Graphical user interfaces, styles, layouts, and widgets C-Feature A new feature, making something new possible M-Release-Note Work that should be called out in the blog due to impact S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Directional navigation is too onerous to set up

9 participants