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-10-27.142557.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.

///
/// For example, when navigating East/West, nodes must have some vertical overlap.
/// A value of 0.0 means any overlap is acceptable, while 1.0 means perfect alignment is required.
pub min_alignment_factor: f32,
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this an angle or a distance? What are the units?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great question - it's neither an angle nor distance, it's actually a ratio - it represents the fraction of overlap between 2 nodes on the perpendicular axis:

  • For N/S navigation: it measures horizontal overlap
  • For E/W navigation: it measures vertical overlap
  • For diagonal directions(NE/NW/SE/SW): it's ignored (always considered 1.0)

And the calculation computes this as: overlap_ratio = actual_overlap / min(origin_size, candidate_size)
For example:

  • Origin node: width 100px, centered at x=150 => spans [100, 200]
  • Candidate node: width 100px, centered at x=180 => spans [130, 230]
  • Overlap region: [130, 200] = 70px
  • Overlap ratio: 70 / 100 = 0.7

If min_alignment_factor = 0.5, this node would be reachable (as 0.7 >= 0.5). However, if min_alignment_factor = 0.8, it would not be.

I could modify the name of this to be more descriptive - something like min_overlap_ratio? Or perhaps better improve the docs to reflect?

@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 being 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
// Extract center position from transform
let (_scale, _rotation, translation) = transform.to_scale_angle_translation();
let size = computed.size();
(entity, translation, size)

Choose a reason for hiding this comment

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

May I suggest using a struct with named fields here?

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-Needs-Review Needs reviewer attention (from anyone!) to move forward

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Directional navigation is too onerous to set up

6 participants