Toggleable UI layout rounding (#16841)

# Objective

Allow users to enable or disable layout rounding for specific UI nodes
and their descendants.

Fixes #16731

## Solution

New component `LayoutConfig` that can be added to any UiNode entity.
Setting the `use_rounding` field of `LayoutConfig` determines if the
Node and its descendants should be given rounded or unrounded
coordinates.

## Testing

Not tested this extensively but it seems to work and it's not very
complicated.
This really basic test app returns fractional coords:

```rust
use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, setup)
        .add_systems(Update, report)
        .run();
}

fn setup(mut commands: Commands) {
    commands.spawn(Camera2d);
    commands.spawn((
        Node {
            left: Val::Px(0.1),
            width: Val::Px(100.1),
            height: Val::Px(100.1),
            ..Default::default()
        },
        LayoutConfig { use_rounding: false },
    ));
}

fn report(node: Query<(Ref<ComputedNode>, &GlobalTransform)>) {
    for (c, g) in node.iter() {
        if c.is_changed() {
            println!("{:#?}", c);
            println!("position = {:?}", g.to_scale_rotation_translation().2);
        }
    }
}
```

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: UkoeHB <37489173+UkoeHB@users.noreply.github.com>
This commit is contained in:
ickshonpe 2024-12-24 02:41:46 +00:00 committed by GitHub
parent ddf4d9ea93
commit bfc2a88f94
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 62 additions and 19 deletions

View File

@ -1,7 +1,7 @@
use crate::{
experimental::{UiChildren, UiRootNodes},
BorderRadius, ComputedNode, ContentSize, DefaultUiCamera, Display, Node, Outline, OverflowAxis,
ScrollPosition, TargetCamera, UiScale, Val,
BorderRadius, ComputedNode, ContentSize, DefaultUiCamera, Display, LayoutConfig, Node, Outline,
OverflowAxis, ScrollPosition, TargetCamera, UiScale, Val,
};
use bevy_ecs::{
change_detection::{DetectChanges, DetectChangesMut},
@ -120,10 +120,12 @@ pub fn ui_layout_system(
&mut ComputedNode,
&mut Transform,
&Node,
Option<&LayoutConfig>,
Option<&BorderRadius>,
Option<&Outline>,
Option<&ScrollPosition>,
)>,
mut buffer_query: Query<&mut ComputedTextBlock>,
mut font_system: ResMut<CosmicFontSystem>,
) {
@ -294,6 +296,7 @@ with UI components as a child of an entity without UI components, your UI layout
&mut commands,
*root,
&mut ui_surface,
true,
None,
&mut node_transform_query,
&ui_children,
@ -312,11 +315,13 @@ with UI components as a child of an entity without UI components, your UI layout
commands: &mut Commands,
entity: Entity,
ui_surface: &mut UiSurface,
inherited_use_rounding: bool,
root_size: Option<Vec2>,
node_transform_query: &mut Query<(
&mut ComputedNode,
&mut Transform,
&Node,
Option<&LayoutConfig>,
Option<&BorderRadius>,
Option<&Outline>,
Option<&ScrollPosition>,
@ -330,12 +335,17 @@ with UI components as a child of an entity without UI components, your UI layout
mut node,
mut transform,
style,
maybe_layout_config,
maybe_border_radius,
maybe_outline,
maybe_scroll_position,
)) = node_transform_query.get_mut(entity)
{
let Ok((layout, unrounded_size)) = ui_surface.get_layout(entity) else {
let use_rounding = maybe_layout_config
.map(|layout_config| layout_config.use_rounding)
.unwrap_or(inherited_use_rounding);
let Ok((layout, unrounded_size)) = ui_surface.get_layout(entity, use_rounding) else {
return;
};
@ -446,6 +456,7 @@ with UI components as a child of an entity without UI components, your UI layout
commands,
child_uinode,
ui_surface,
use_rounding,
Some(viewport_size),
node_transform_query,
ui_children,
@ -573,7 +584,7 @@ mod tests {
let mut ui_surface = world.resource_mut::<UiSurface>();
for ui_entity in [ui_root, ui_child] {
let layout = ui_surface.get_layout(ui_entity).unwrap().0;
let layout = ui_surface.get_layout(ui_entity, true).unwrap().0;
assert_eq!(layout.size.width, WINDOW_WIDTH);
assert_eq!(layout.size.height, WINDOW_HEIGHT);
}
@ -962,7 +973,7 @@ mod tests {
let mut ui_surface = world.resource_mut::<UiSurface>();
let layout = ui_surface
.get_layout(ui_node_entity)
.get_layout(ui_node_entity, true)
.expect("failed to get layout")
.0;
@ -1049,7 +1060,7 @@ mod tests {
ui_schedule.run(&mut world);
let mut ui_surface = world.resource_mut::<UiSurface>();
let layout = ui_surface.get_layout(ui_entity).unwrap().0;
let layout = ui_surface.get_layout(ui_entity, true).unwrap().0;
// the node should takes its size from the fixed size measure func
assert_eq!(layout.size.width, content_size.x);
@ -1078,7 +1089,7 @@ mod tests {
// a node with a content size should have taffy context
assert!(ui_surface.taffy.get_node_context(ui_node).is_some());
let layout = ui_surface.get_layout(ui_entity).unwrap().0;
let layout = ui_surface.get_layout(ui_entity, true).unwrap().0;
assert_eq!(layout.size.width, content_size.x);
assert_eq!(layout.size.height, content_size.y);
@ -1091,7 +1102,7 @@ mod tests {
assert!(ui_surface.taffy.get_node_context(ui_node).is_none());
// Without a content size, the node has no width or height constraints so the length of both dimensions is 0.
let layout = ui_surface.get_layout(ui_entity).unwrap().0;
let layout = ui_surface.get_layout(ui_entity, true).unwrap().0;
assert_eq!(layout.size.width, 0.);
assert_eq!(layout.size.height, 0.);
}

View File

@ -277,23 +277,33 @@ impl UiSurface {
/// Does not compute the layout geometry, `compute_window_layouts` should be run before using this function.
/// On success returns a pair consisting of the final resolved layout values after rounding
/// and the size of the node after layout resolution but before rounding.
pub fn get_layout(&mut self, entity: Entity) -> Result<(taffy::Layout, Vec2), LayoutError> {
pub fn get_layout(
&mut self,
entity: Entity,
use_rounding: bool,
) -> Result<(taffy::Layout, Vec2), LayoutError> {
let Some(taffy_node) = self.entity_to_taffy.get(&entity) else {
return Err(LayoutError::InvalidHierarchy);
};
let layout = self
.taffy
.layout(*taffy_node)
.cloned()
.map_err(LayoutError::TaffyError)?;
if use_rounding {
self.taffy.enable_rounding();
} else {
self.taffy.disable_rounding();
}
let out = match self.taffy.layout(*taffy_node).cloned() {
Ok(layout) => {
self.taffy.disable_rounding();
let taffy_size = self.taffy.layout(*taffy_node).unwrap().size;
let unrounded_size = Vec2::new(taffy_size.width, taffy_size.height);
Ok((layout, unrounded_size))
}
Err(taffy_error) => Err(LayoutError::TaffyError(taffy_error)),
};
self.taffy.disable_rounding();
let taffy_size = self.taffy.layout(*taffy_node).unwrap().size;
let unrounded_size = Vec2::new(taffy_size.width, taffy_size.height);
self.taffy.enable_rounding();
Ok((layout, unrounded_size))
out
}
}

View File

@ -2550,6 +2550,28 @@ impl Default for ShadowStyle {
}
}
#[derive(Component, Copy, Clone, Debug, PartialEq, Reflect)]
#[reflect(Component, Debug, PartialEq, Default)]
#[cfg_attr(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
/// This component can be added to any UI node to modify its layout behavior.
pub struct LayoutConfig {
/// If set to true the coordinates for this node and its descendents will be rounded to the nearest physical pixel.
/// This can help prevent visual artifacts like blurry images or semi-transparent edges that can occur with sub-pixel positioning.
///
/// Defaults to true.
pub use_rounding: bool,
}
impl Default for LayoutConfig {
fn default() -> Self {
Self { use_rounding: true }
}
}
#[cfg(test)]
mod tests {
use crate::GridPlacement;