Skip to content

Fix UI surface rounding - #26026

Merged
alice-i-cecile merged 3 commits into
bevyengine:mainfrom
jorgeandrecastro:fix-ui-surface-rounding
Oct 6, 2026
Merged

alice-i-cecile merged 3 commits into
bevyengine:mainfrom
jorgeandrecastro:fix-ui-surface-rounding

Conversation

@jorgeandrecastro

@jorgeandrecastro jorgeandrecastro commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Objective

Fix a logic bug in UiSurface::get_layout where the global Taffy rounding state was unconditionally re-enabled at the end of the function, regardless of the use_rounding parameter passed by the caller.

Solution

Modified UiSurface::get_layout to conditionally restore the rounding state (self.taffy.enable_rounding()) only if use_rounding was set to true, preventing unintended state overrides.

Testing
Ran unit tests specifically for the UI module using

 cargo test -p bevy_ui --lib

ensuring all tests pass successfully.
Tested locally on Linux.

@jorgeandrecastro
jorgeandrecastro force-pushed the fix-ui-surface-rounding branch 5 times, most recently from ac73c7e to 6e29eef Compare October 4, 2026 15:54
@JMS55
JMS55 requested a review from ickshonpe October 4, 2026 17:52
@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 Oct 4, 2026
@alice-i-cecile alice-i-cecile added the X-Uncontroversial This work is generally agreed upon label Oct 4, 2026
};
let layout = self.taffy.layout(taffy_node.id).cloned();

self.taffy.disable_rounding();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This needs a comment; why are we temporarily disabling rounding?

@ickshonpe ickshonpe Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Taffy's API is a bit confusing here, which is why this bug crept in. enable/disable_rounding does two things. It enables or disables whether the rounded final_layout is updated during layout. But also after the layout is generated it controls whether the per node layout getter function returns rounded or unrounded geometry:

pub fn layout(&self, node: NodeId) -> TaffyResult<&Layout> {
        if self.config.use_rounding {
            Ok(&self.nodes[node.into()].final_layout)
        } else {
            Ok(&self.nodes[node.into()].unrounded_layout)
        }
    }

@jorgeandrecastro jorgeandrecastro Oct 4, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This needs a comment; why are we temporarily disabling rounding?

Hello alice-i-cecille 😅 thanks for the suggestion I've added a comment to document this behavior as requested, well why we need to temporarily disable rounding? It's because Taffy's enable/disable_rounding setting controls both how geometry is generated and what the per-node layout getter returns. We temporarily disable it to fetch the unrounded size, and then restore the caller's original use_rounding preference.

I found this "issue " , cause a was working on another one before so i saw this and i pull request, Hope its helps bevy engine 😀

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Taffy's API is a bit confusing here, which is why this bug crept in. enable/disable_rounding does two things. It enables or disables whether rounded node geometry is created during layout. But also after the layout is generated it controls whether the per node layout getter function returns rounded or unrounded geometry:

pub fn layout(&self, node: NodeId) -> TaffyResult<&Layout> {
        if self.config.use_rounding {
            Ok(&self.nodes[node.into()].final_layout)
        } else {
            Ok(&self.nodes[node.into()].unrounded_layout)
        }
    }

Hello ickshonpe 😀Thank you for the detailed breakdown! That explains why the API behaves that way, and it makes complete sense why we need to explicitly restore the rounding state after getting the unrounded size.

@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 D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward D-Straightforward Simple bug fixes and API improvements, docs, test and examples labels Oct 4, 2026
@jorgeandrecastro
jorgeandrecastro force-pushed the fix-ui-surface-rounding branch from b0875d0 to eb780d9 Compare October 4, 2026 21:13

@ickshonpe ickshonpe left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm really confused now, does this actually introduce the bug that it's meant to fix? This regression test passes on main but fails with this PR:

    #[test]
    fn rounding_test() {
        let mut app = setup_ui_test_app();

        let root = app
            .world_mut()
            .spawn(Node {
                width: px(100.),
                height: px(100.),
                ..default()
            })
            .with_child((
                Node::default(),
                LayoutConfig {
                    use_rounding: false,
                },
            ))
            .id();

        app.update();

        assert_eq!(
            app.world().get::<ComputedNode>(root).unwrap().size(),
            Vec2::splat(100.)
        );

        app.world_mut().get_mut::<Node>(root).unwrap().width = px(200.);
        app.update();

        assert_eq!(
            app.world().get::<ComputedNode>(root).unwrap().size(),
            Vec2::new(200., 100.)
        );
    }

@ickshonpe ickshonpe added S-Wontfix This issue is the result of a deliberate design decision, and will not be fixed and removed C-Bug An unexpected or incorrect behavior X-Uncontroversial This work is generally agreed upon labels Oct 4, 2026
@jorgeandrecastro

jorgeandrecastro commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor Author

I'm really confused now, does this actually introduce the bug that it's meant to fix? This regression test passes on main but fails with this PR:

    #[test]
    fn rounding_test() {
        let mut app = setup_ui_test_app();

        let root = app
            .world_mut()
            .spawn(Node {
                width: px(100.),
                height: px(100.),
                ..default()
            })
            .with_child((
                Node::default(),
                LayoutConfig {
                    use_rounding: false,
                },
            ))
            .id();

        app.update();

        assert_eq!(
            app.world().get::<ComputedNode>(root).unwrap().size(),
            Vec2::splat(100.)
        );

        app.world_mut().get_mut::<Node>(root).unwrap().width = px(200.);
        app.update();

        assert_eq!(
            app.world().get::<ComputedNode>(root).unwrap().size(),
            Vec2::new(200., 100.)
        );
    }

Ah, good catch! 😅 You're completely right, looking back at the diff I missed the actual size fetching and state restoration part. Thanks for writing a regression test for this I'll properly fix it tomorrow! 🤵

pub fn get_layout(
       &mut self,
       entity: Entity,
       use_rounding: bool,
   ) -> Result<(taffy::Layout, Vec2), UiSurfaceError> {
       let Some(taffy_node) = self.entity_to_taffy.get(&entity) else {
           return Err(UiSurfaceError::NoAssociatedTaffyNode);
       };

       if use_rounding {
           self.taffy.enable_rounding();
       } else {
           self.taffy.disable_rounding();
       }

       let layout = self.taffy.layout(taffy_node.id).cloned();

       // Temporarily disable rounding to retrieve the unrounded layout size
       self.taffy.disable_rounding();
       let taffy_size = self.taffy.layout(taffy_node.id).unwrap().size;
       let unrounded_size = Vec2::new(taffy_size.width, taffy_size.height);

       // Restore the initial rounding state instead of unconditionally enabling it
       if use_rounding {
           self.taffy.enable_rounding();
       }

       match layout {
           Ok(l) => Ok((l, unrounded_size)),
           Err(e) => Err(UiSurfaceError::TaffyError(e)),
       }
   }

With this version ig should work , i will fixit tomorrow morning 😀

@ickshonpe ickshonpe left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Right okay I remember everything now, it's not the fault of the author here. It's my terrible design compounded by the terrible Taffy API and my lazyness in not adding any tests 😅 .

There isn't a bug. We always generate a rounded final_layout. It is bad though. What I think needs to be changed:

  1. We shouldn't be setting rounding at the end of the get_layout function at all. Instead, we should call enable_rounding before before we call Taffy's compute_layout_with_measure in the UiSurface::compute_layout function.
  2. Add some regression tests.,
  3. Add some comments to get_layout and compute_layout explaining enable/disable_rounding's dual purpose.

@alice-i-cecile alice-i-cecile added X-Contentious There are nontrivial implications that should be thought through and removed S-Wontfix This issue is the result of a deliberate design decision, and will not be fixed labels Oct 4, 2026
@jorgeandrecastro

Copy link
Copy Markdown
Contributor Author

Right okay I remember everything now, it's not the fault of the author here. It's my terrible design compounded by the terrible Taffy API and my lazyness in not adding any tests 😅 .

There isn't a bug. We always generate a rounded final_layout. It is bad though. What I think needs to be changed:

  1. We shouldn't be setting rounding at the end of the get_layout function at all. Instead, we should call enable_rounding before before we call Taffy's compute_layout_with_measure in the UiSurface::compute_layout function.
  2. Add some regression tests.,
  3. Add some comments to get_layout and compute_layout explaining enable/disable_rounding's dual purpose.

Since you mentioned wanting to handle this further upstream in compute_layout, let me know how you'd like to proceed. Should I adjust my PR to follow your suggested approach, or are you planning to take care of the refactoring from your side? I'm happy to adapt or wait for your changes!.
It wasn't a "bug " ,cause its work's but is a Logic issue cause de never consider user choice 🤵

@alice-i-cecile

Copy link
Copy Markdown
Member

@ickshonpe's plan should be followed here, but please let us know if you don't fully understand it, and we'll have someone else take this over to make sure it doesn't get dropped <3

@jorgeandrecastro

Copy link
Copy Markdown
Contributor Author

plan should be followed here, but please let us know if you don't fully understand it, and we'll have someone else take this over to make sure it doesn't get dropped <3

Got it completely :) I've got it handled. I followed @ickshonpe's plan, and the regression test is passing successfully. Everything is formatted, pushed, and ready to go!, i hope

@ickshonpe ickshonpe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good.

@jorgeandrecastro

Copy link
Copy Markdown
Contributor Author

Looks good.

I tester the rétrocompatible test its green and the other ones , so Hope its helps 😀

@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 Oct 5, 2026
@alice-i-cecile
alice-i-cecile added this pull request to the merge queue Oct 5, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 5, 2026
@alice-i-cecile
alice-i-cecile added this pull request to the merge queue Oct 5, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 5, 2026
@alice-i-cecile
alice-i-cecile added this pull request to the merge queue Oct 6, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 6, 2026
@ickshonpe ickshonpe added C-Docs An addition or correction to our documentation C-Code-Quality A section of code that is hard to understand or change and removed X-Contentious There are nontrivial implications that should be thought through labels Oct 6, 2026
@alice-i-cecile
alice-i-cecile added this pull request to the merge queue Oct 6, 2026
Merged via the queue into bevyengine:main with commit d08bd60 Oct 6, 2026
45 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-Code-Quality A section of code that is hard to understand or change C-Docs An addition or correction to our documentation D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants