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

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)] #[cfg(test)]
mod tests { mod tests {
use crate::GridPlacement; use crate::GridPlacement;