Skip to content

Conversation

@snk-git-hub
Copy link
Contributor

Objective

Fixes #22234
The directional navigation system was ignoring UiTransform rotation and scaling when calculating which node to navigate to. This caused navigation to select incorrect nodes when they were rotated or scaled, because the system used unrotated layout positions instead of the visual positions seen by users.

Solution

  • Added get_rotated_bounds() helper function to calculate the axis-aligned bounding box of a rotated rectangle
  • Updated get_navigable_nodes() to apply both rotation and scale transforms from UiGlobalTransform when creating FocusableArea structs
  • Updated entity_to_camera_and_focusable_area() to apply rotation and scale transforms
  • Used bevy_math::ops instead of f32 methods for deterministic trigonometry

The fix ensures that directional navigation uses the visual position of nodes (after transforms) rather than just the layout position, so users navigate to the button they actually see on screen.

Testing

  • Tested with auto_directional_navigation example - navigation works correctly with scattered button layouts

Screencast From 2026-01-06 11-37-04

@alice-i-cecile alice-i-cecile added this to the 0.18 milestone Jan 6, 2026
@alice-i-cecile alice-i-cecile added C-Bug An unexpected or incorrect behavior A-UI Graphical user interfaces, styles, layouts, and widgets D-Straightforward Simple bug fixes and API improvements, docs, test and examples S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Jan 6, 2026
}
}

fn get_rotated_bounds(size: Vec2, rotation: f32) -> Vec2 {
Copy link
Member

@alice-i-cecile alice-i-cecile Jan 6, 2026

Choose a reason for hiding this comment

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

This might be better as a public helper method on UiTransform TBH :)

border_radius: BorderRadius::all(px(12)),
..default()
},
// This is the key: just add this component for automatic navigation!
Copy link
Member

Choose a reason for hiding this comment

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

This comment is now misplaced.

Copy link
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

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

The misplaced comment is blocking; everything else is bonus.

@alice-i-cecile alice-i-cecile added S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Jan 6, 2026
@snk-git-hub
Copy link
Contributor Author

snk-git-hub commented Jan 6, 2026

The misplaced comment is blocking; everything else is bonus.

Fixed the comment misplacing and added a comment for the Transform line

@alice-i-cecile alice-i-cecile added S-Needs-Review Needs reviewer attention (from anyone!) to move forward and removed S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged labels Jan 6, 2026
@kfc35
Copy link
Contributor

kfc35 commented Jan 6, 2026

The merging of #22340 caused a conflict with your branch

To resolve the conflict, I believe you just have to move the logic you edited in get_navigable_nodes and entity_to_camera_and_focusable_area into main’s bevy_ui/src/auto_directional_navigation.rs, since I moved the logic to that file

@alice-i-cecile alice-i-cecile added S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Jan 6, 2026
..default()
},
// Apply a scale transform to demonstrate that navigation handles transforms
Transform::from_scale(Vec3::splat(2.0)),
Copy link
Contributor

Choose a reason for hiding this comment

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

You should be using UiTransform here

Copy link
Contributor

@kfc35 kfc35 left a comment

Choose a reason for hiding this comment

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

I tested your logic as follows (basically copying ickshonpe’s test case in the issue):

I replaced some code in the auto_directional_navigation example (between let button_positions… and the end of the for loop`). The main things changed are the UiTransform that I only apply to the center button, and the four buttons.

// Spawn buttons in a scattered/irregular pattern
    // The auto-navigation system will figure out the connections!
    let button_positions_and_dims = [
        // Top left
        (350.0, 100.0, 140, 80),
        // Center. Will be scaled x2 and rotated PI/2 Radians
        (550.0, 300.0, 240, 70),
        // Bottom Left
        (350.0, 500.0, 140, 80),
        // Bottom Right
        (750.0, 500.0, 140, 80),
    ];

    let mut first_button = None;
    for (i, (x, y, width, height)) in button_positions_and_dims().enumerate() {
        let transform = if i == 1 {
            UiTransform {
                scale: Vec2::splat(2.0),
                rotation: Rot2::FRAC_PI_2,
                ..default()
            }
        } else {
            UiTransform::IDENTITY
        };
        let button_entity = commands
            .spawn((
                Button,
                Node {
                    position_type: PositionType::Absolute,
                    left: px(*x),
                    top: px(*y),
                    width: px(*width),
                    height: px(*height),
                    border: UiRect::all(px(4)),
                    justify_content: JustifyContent::Center,
                    align_items: AlignItems::Center,
                    border_radius: BorderRadius::all(px(12)),
                    ..default()
                },
                // Apply the appropriate transform
                transform,
                // This is the key: just add this component for automatic navigation!
                AutoDirectionalNavigation::default(),
                ResetTimer::default(),
                BackgroundColor::from(NORMAL_BUTTON),
                Name::new(format!("Button {}", i + 1)),
            ))
            .with_child((
                Text::new(format!("Button {}", i + 1)),
                TextLayout {
                    justify: Justify::Center,
                    ..default()
                },
            ))
            .id();

        if first_button.is_none() {
            first_button = Some(button_entity);
        }
    }

    commands.entity(root_node).add_children(&[instructions]);

    // Set initial focus
    if let Some(button) = first_button {
        input_focus.set(button);
    }
}

Without the transform, it looks like this:
Screenshot 2026-01-07 at 3 23 37 PM
And navigation between the three non-center buttons works as expected.

With the transformation, it looks like this:
Screenshot 2026-01-07 at 3 24 25 PM
And navigation now works as I expect (the center button can now be navigated to by going left or right along the bottom row).

As far as editing the example for auto_directional_navigation.rs to show that this works, I personally think it can be a follow up, but you can use what I wrote as a guide to include something in this PR if you’d like. I’m not gonna click approve yet because the example now looks like this with the scaling!

Screenshot 2026-01-07 at 3 34 22 PM

@snk-git-hub
Copy link
Contributor Author

snk-git-hub commented Jan 7, 2026

I tested your logic as follows (basically copying ickshonpe’s test case in the issue):

I replaced some code in the auto_directional_navigation example (between let button_positions… and the end of the for loop`). The main things changed are the UiTransform that I only apply to the center button, and the four buttons.

// Spawn buttons in a scattered/irregular pattern
    // The auto-navigation system will figure out the connections!
    let button_positions_and_dims = [
        // Top left
        (350.0, 100.0, 140, 80),
        // Center. Will be scaled x2 and rotated PI/2 Radians
        (550.0, 300.0, 240, 70),
        // Bottom Left
        (350.0, 500.0, 140, 80),
        // Bottom Right
        (750.0, 500.0, 140, 80),
    ];

    let mut first_button = None;
    for (i, (x, y, width, height)) in button_positions_and_dims().enumerate() {
        let transform = if i == 1 {
            UiTransform {
                scale: Vec2::splat(2.0),
                rotation: Rot2::FRAC_PI_2,
                ..default()
            }
        } else {
            UiTransform::IDENTITY
        };
        let button_entity = commands
            .spawn((
                Button,
                Node {
                    position_type: PositionType::Absolute,
                    left: px(*x),
                    top: px(*y),
                    width: px(*width),
                    height: px(*height),
                    border: UiRect::all(px(4)),
                    justify_content: JustifyContent::Center,
                    align_items: AlignItems::Center,
                    border_radius: BorderRadius::all(px(12)),
                    ..default()
                },
                // Apply the appropriate transform
                transform,
                // This is the key: just add this component for automatic navigation!
                AutoDirectionalNavigation::default(),
                ResetTimer::default(),
                BackgroundColor::from(NORMAL_BUTTON),
                Name::new(format!("Button {}", i + 1)),
            ))
            .with_child((
                Text::new(format!("Button {}", i + 1)),
                TextLayout {
                    justify: Justify::Center,
                    ..default()
                },
            ))
            .id();

        if first_button.is_none() {
            first_button = Some(button_entity);
        }
    }

    commands.entity(root_node).add_children(&[instructions]);

    // Set initial focus
    if let Some(button) = first_button {
        input_focus.set(button);
    }
}

Without the transform, it looks like this: Screenshot 2026-01-07 at 3 23 37 PM And navigation between the three non-center buttons works as expected.

With the transformation, it looks like this: Screenshot 2026-01-07 at 3 24 25 PM And navigation now works as I expect (the center button can now be navigated to by going left or right along the bottom row).

As far as editing the example for auto_directional_navigation.rs to show that this works, I personally think it can be a follow up, but you can use what I wrote as a guide to include something in this PR if you’d like. I’m not gonna click approve yet because the example now looks like this with the scaling!

Screenshot 2026-01-07 at 3 34 22 PM

Thanks for the detailed testing and suggestion! I've updated the example to apply UiTransform (2x scale + 90° rotation) to Button 5, demonstrating that navigation correctly handles transforms while keeping the UI usable. The transformed button is now clearly visible and navigable. : )
Screenshot From 2026-01-08 03-07-31

@cart cart enabled auto-merge January 7, 2026 21:59
@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-Waiting-on-Author The author needs to make changes or address concerns before this can be merged labels Jan 7, 2026
@cart cart added this pull request to the merge queue Jan 7, 2026
Merged via the queue into bevyengine:main with commit f572ad9 Jan 7, 2026
40 checks passed
cart added a commit that referenced this pull request Jan 8, 2026
# Objective
Fixes #22234
The directional navigation system was ignoring `UiTransform` rotation
and scaling when calculating which node to navigate to. This caused
navigation to select incorrect nodes when they were rotated or scaled,
because the system used unrotated layout positions instead of the visual
positions seen by users.

## Solution
- Added `get_rotated_bounds()` helper function to calculate the
axis-aligned bounding box of a rotated rectangle
- Updated `get_navigable_nodes()` to apply both rotation and scale
transforms from `UiGlobalTransform` when creating `FocusableArea`
structs
- Updated `entity_to_camera_and_focusable_area()` to apply rotation and
scale transforms
- Used `bevy_math::ops` instead of `f32` methods for deterministic
trigonometry

The fix ensures that directional navigation uses the visual position of
nodes (after transforms) rather than just the layout position, so users
navigate to the button they actually see on screen.

## Testing
- Tested with `auto_directional_navigation` example - navigation works
correctly with scattered button layouts

![Screencast From 2026-01-06
11-37-04](https://github.com/user-attachments/assets/fcaf5d24-f337-4f2d-b1e9-c08651bd9d97)

---------

Co-authored-by: Carter Anderson <[email protected]>
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-Bug An unexpected or incorrect behavior D-Straightforward Simple bug fixes and API improvements, docs, test and examples 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.

bevy_input_focus: Directional navigation ignores UiTransform

4 participants