Skip to content

Commit

Permalink
Toggleable UI layout rounding (#16841)
Browse files Browse the repository at this point in the history
# 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 <[email protected]>
Co-authored-by: UkoeHB <[email protected]>
  • Loading branch information
3 people authored Dec 24, 2024
1 parent ddf4d9e commit bfc2a88
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 19 deletions.
27 changes: 19 additions & 8 deletions crates/bevy_ui/src/layout/mod.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand Down Expand Up @@ -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>,
) {
Expand Down Expand Up @@ -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,
Expand All @@ -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>,
Expand All @@ -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;
};

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);

Expand All @@ -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.);
}
Expand Down
32 changes: 21 additions & 11 deletions crates/bevy_ui/src/layout/ui_surface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

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();
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)),
};

Ok((layout, unrounded_size))
self.taffy.enable_rounding();
out
}
}

Expand Down
22 changes: 22 additions & 0 deletions crates/bevy_ui/src/ui_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit bfc2a88

Please sign in to comment.