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:
parent
ddf4d9ea93
commit
bfc2a88f94
@ -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.);
|
||||||
}
|
}
|
||||||
|
@ -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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user