Val viewport unit variants (#8137)

# Objective

Add viewport variants to `Val` that specify a percentage length based on
the size of the window.

## Solution

Add the variants `Vw`, `Vh`, `VMin` and `VMax` to `Val`.
Add a physical window size parameter to the `from_style` function and
use it to convert the viewport variants to Taffy Points values.

One issue: It isn't responsive to window resizes. So `flex_node_system`
has to do a full update every time the window size changes. Perhaps this
can be fixed with support from Taffy.

---

## Changelog

* Added `Val` viewport unit variants `Vw`, `Vh`, `VMin` and `VMax`.
* Modified `convert` module to support the new `Val` variants.
* Changed `flex_node_system` to support the new `Val` variants.
* Perform full layout update on screen resizing, to propagate the new
viewport size to all nodes.
This commit is contained in:
ickshonpe 2023-03-21 19:14:27 +00:00 committed by GitHub
parent c809779b6e
commit 2d5ef75c9f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 172 additions and 103 deletions

View file

@ -1,99 +1,74 @@
use taffy::style::LengthPercentageAuto;
use crate::{
AlignContent, AlignItems, AlignSelf, Display, FlexDirection, FlexWrap, JustifyContent,
PositionType, Size, Style, UiRect, Val,
};
use super::LayoutContext;
impl Val {
fn scaled(self, scale_factor: f64) -> Self {
fn into_length_percentage_auto(
self,
context: &LayoutContext,
) -> taffy::style::LengthPercentageAuto {
match self {
Val::Auto => Val::Auto,
Val::Percent(value) => Val::Percent(value),
Val::Px(value) => Val::Px((scale_factor * value as f64) as f32),
Val::Auto => taffy::style::LengthPercentageAuto::Auto,
Val::Percent(value) => taffy::style::LengthPercentageAuto::Percent(value / 100.),
Val::Px(value) => taffy::style::LengthPercentageAuto::Points(
(context.scale_factor * value as f64) as f32,
),
Val::VMin(value) => {
taffy::style::LengthPercentageAuto::Points(context.min_size * value / 100.)
}
Val::VMax(value) => {
taffy::style::LengthPercentageAuto::Points(context.max_size * value / 100.)
}
Val::Vw(value) => {
taffy::style::LengthPercentageAuto::Points(context.physical_size.x * value / 100.)
}
Val::Vh(value) => {
taffy::style::LengthPercentageAuto::Points(context.physical_size.y * value / 100.)
}
}
}
fn to_inset(self) -> LengthPercentageAuto {
match self {
Val::Auto => taffy::style::LengthPercentageAuto::Auto,
Val::Percent(value) => taffy::style::LengthPercentageAuto::Percent(value / 100.0),
Val::Px(value) => taffy::style::LengthPercentageAuto::Points(value),
fn into_length_percentage(self, context: &LayoutContext) -> taffy::style::LengthPercentage {
match self.into_length_percentage_auto(context) {
taffy::style::LengthPercentageAuto::Auto => taffy::style::LengthPercentage::Points(0.0),
taffy::style::LengthPercentageAuto::Percent(value) => {
taffy::style::LengthPercentage::Percent(value)
}
taffy::style::LengthPercentageAuto::Points(value) => {
taffy::style::LengthPercentage::Points(value)
}
}
}
fn into_dimension(self, context: &LayoutContext) -> taffy::style::Dimension {
self.into_length_percentage_auto(context).into()
}
}
impl UiRect {
fn scaled(self, scale_factor: f64) -> Self {
Self {
left: self.left.scaled(scale_factor),
right: self.right.scaled(scale_factor),
top: self.top.scaled(scale_factor),
bottom: self.bottom.scaled(scale_factor),
fn map_to_taffy_rect<T>(self, map_fn: impl Fn(Val) -> T) -> taffy::geometry::Rect<T> {
taffy::geometry::Rect {
left: map_fn(self.left),
right: map_fn(self.right),
top: map_fn(self.top),
bottom: map_fn(self.bottom),
}
}
}
impl Size {
fn scaled(self, scale_factor: f64) -> Self {
Self {
width: self.width.scaled(scale_factor),
height: self.height.scaled(scale_factor),
fn map_to_taffy_size<T>(self, map_fn: impl Fn(Val) -> T) -> taffy::geometry::Size<T> {
taffy::geometry::Size {
width: map_fn(self.width),
height: map_fn(self.height),
}
}
}
impl<T: From<Val>> From<UiRect> for taffy::prelude::Rect<T> {
fn from(value: UiRect) -> Self {
Self {
left: value.left.into(),
right: value.right.into(),
top: value.top.into(),
bottom: value.bottom.into(),
}
}
}
impl<T: From<Val>> From<Size> for taffy::prelude::Size<T> {
fn from(value: Size) -> Self {
Self {
width: value.width.into(),
height: value.height.into(),
}
}
}
impl From<Val> for taffy::style::Dimension {
fn from(value: Val) -> Self {
match value {
Val::Auto => taffy::style::Dimension::Auto,
Val::Percent(value) => taffy::style::Dimension::Percent(value / 100.0),
Val::Px(value) => taffy::style::Dimension::Points(value),
}
}
}
impl From<Val> for taffy::style::LengthPercentage {
fn from(value: Val) -> Self {
match value {
Val::Auto => taffy::style::LengthPercentage::Points(0.0),
Val::Percent(value) => taffy::style::LengthPercentage::Percent(value / 100.0),
Val::Px(value) => taffy::style::LengthPercentage::Points(value),
}
}
}
impl From<Val> for taffy::style::LengthPercentageAuto {
fn from(value: Val) -> Self {
match value {
Val::Auto => taffy::style::LengthPercentageAuto::Auto,
Val::Percent(value) => taffy::style::LengthPercentageAuto::Percent(value / 100.0),
Val::Px(value) => taffy::style::LengthPercentageAuto::Points(value),
}
}
}
pub fn from_style(scale_factor: f64, style: &Style) -> taffy::style::Style {
pub fn from_style(context: &LayoutContext, style: &Style) -> taffy::style::Style {
taffy::style::Style {
display: style.display.into(),
position: style.position_type.into(),
@ -104,22 +79,34 @@ pub fn from_style(scale_factor: f64, style: &Style) -> taffy::style::Style {
align_content: Some(style.align_content.into()),
justify_content: Some(style.justify_content.into()),
inset: taffy::prelude::Rect {
left: style.left.scaled(scale_factor).to_inset(),
right: style.right.scaled(scale_factor).to_inset(),
top: style.top.scaled(scale_factor).to_inset(),
bottom: style.bottom.scaled(scale_factor).to_inset(),
left: style.left.into_length_percentage_auto(context),
right: style.right.into_length_percentage_auto(context),
top: style.top.into_length_percentage_auto(context),
bottom: style.bottom.into_length_percentage_auto(context),
},
margin: style.margin.scaled(scale_factor).into(),
padding: style.padding.scaled(scale_factor).into(),
border: style.border.scaled(scale_factor).into(),
margin: style
.margin
.map_to_taffy_rect(|m| m.into_length_percentage_auto(context)),
padding: style
.padding
.map_to_taffy_rect(|m| m.into_length_percentage(context)),
border: style
.border
.map_to_taffy_rect(|m| m.into_length_percentage(context)),
flex_grow: style.flex_grow,
flex_shrink: style.flex_shrink,
flex_basis: style.flex_basis.scaled(scale_factor).into(),
size: style.size.scaled(scale_factor).into(),
min_size: style.min_size.scaled(scale_factor).into(),
max_size: style.max_size.scaled(scale_factor).into(),
flex_basis: style.flex_basis.into_dimension(context),
size: style.size.map_to_taffy_size(|s| s.into_dimension(context)),
min_size: style
.min_size
.map_to_taffy_size(|s| s.into_dimension(context)),
max_size: style
.max_size
.map_to_taffy_size(|s| s.into_dimension(context)),
aspect_ratio: style.aspect_ratio,
gap: style.gap.scaled(scale_factor).into(),
gap: style
.gap
.map_to_taffy_size(|s| s.into_length_percentage(context)),
justify_self: None,
}
}
@ -283,7 +270,8 @@ mod tests {
height: Val::Percent(0.),
},
};
let taffy_style = from_style(1.0, &bevy_style);
let viewport_values = LayoutContext::new(1.0, bevy_math::Vec2::new(800., 600.));
let taffy_style = from_style(&viewport_values, &bevy_style);
assert_eq!(taffy_style.display, taffy::style::Display::Flex);
assert_eq!(taffy_style.position, taffy::style::Position::Absolute);
assert!(matches!(
@ -408,4 +396,27 @@ mod tests {
taffy::style::LengthPercentage::Percent(0.)
);
}
#[test]
fn test_into_length_percentage() {
use taffy::style::LengthPercentage;
let context = LayoutContext::new(2.0, bevy_math::Vec2::new(800., 600.));
let cases = [
(Val::Auto, LengthPercentage::Points(0.)),
(Val::Percent(1.), LengthPercentage::Percent(0.01)),
(Val::Px(1.), LengthPercentage::Points(2.)),
(Val::Vw(1.), LengthPercentage::Points(8.)),
(Val::Vh(1.), LengthPercentage::Points(6.)),
(Val::VMin(2.), LengthPercentage::Points(12.)),
(Val::VMax(2.), LengthPercentage::Points(16.)),
];
for (val, length) in cases {
assert!(match (val.into_length_percentage(&context), length) {
(LengthPercentage::Points(a), LengthPercentage::Points(b))
| (LengthPercentage::Percent(a), LengthPercentage::Percent(b)) =>
(a - b).abs() < 0.0001,
_ => false,
},);
}
}
}

View file

@ -22,6 +22,25 @@ use taffy::{
Taffy,
};
pub struct LayoutContext {
pub scale_factor: f64,
pub physical_size: Vec2,
pub min_size: f32,
pub max_size: f32,
}
impl LayoutContext {
/// create new a [`LayoutContext`] from the window's physical size and scale factor
fn new(scale_factor: f64, physical_size: Vec2) -> Self {
Self {
scale_factor,
physical_size,
min_size: physical_size.x.min(physical_size.y),
max_size: physical_size.x.max(physical_size.y),
}
}
}
#[derive(Resource)]
pub struct FlexSurface {
entity_to_taffy: HashMap<Entity, taffy::node::Node>,
@ -59,19 +78,17 @@ impl Default for FlexSurface {
}
impl FlexSurface {
pub fn upsert_node(&mut self, entity: Entity, style: &Style, scale_factor: f64) {
pub fn upsert_node(&mut self, entity: Entity, style: &Style, context: &LayoutContext) {
let mut added = false;
let taffy = &mut self.taffy;
let taffy_node = self.entity_to_taffy.entry(entity).or_insert_with(|| {
added = true;
taffy
.new_leaf(convert::from_style(scale_factor, style))
.unwrap()
taffy.new_leaf(convert::from_style(context, style)).unwrap()
});
if !added {
self.taffy
.set_style(*taffy_node, convert::from_style(scale_factor, style))
.set_style(*taffy_node, convert::from_style(context, style))
.unwrap();
}
}
@ -81,10 +98,11 @@ impl FlexSurface {
entity: Entity,
style: &Style,
calculated_size: CalculatedSize,
scale_factor: f64,
context: &LayoutContext,
) {
let taffy = &mut self.taffy;
let taffy_style = convert::from_style(scale_factor, style);
let taffy_style = convert::from_style(context, style);
let scale_factor = context.scale_factor;
let measure = taffy::node::MeasureFunc::Boxed(Box::new(
move |constraints: Size<Option<f32>>, _available: Size<AvailableSpace>| {
let mut size = Size {
@ -229,6 +247,7 @@ pub fn flex_node_system(
windows: Query<(Entity, &Window)>,
ui_scale: Res<UiScale>,
mut scale_factor_events: EventReader<WindowScaleFactorChanged>,
mut resize_events: EventReader<bevy_window::WindowResized>,
mut flex_surface: ResMut<FlexSurface>,
root_node_query: Query<Entity, (With<Node>, Without<Parent>)>,
node_query: Query<(Entity, &Style, Option<&CalculatedSize>), (With<Node>, Changed<Style>)>,
@ -244,13 +263,24 @@ pub fn flex_node_system(
) {
// assume one window for time being...
// TODO: Support window-independent scaling: https://github.com/bevyengine/bevy/issues/5621
let (primary_window_entity, logical_to_physical_factor) =
let (primary_window_entity, logical_to_physical_factor, physical_size) =
if let Ok((entity, primary_window)) = primary_window.get_single() {
(entity, primary_window.resolution.scale_factor())
(
entity,
primary_window.resolution.scale_factor(),
Vec2::new(
primary_window.resolution.physical_width() as f32,
primary_window.resolution.physical_height() as f32,
),
)
} else {
return;
};
let resized = resize_events
.iter()
.any(|resized_window| resized_window.window == primary_window_entity);
// update window root nodes
for (entity, window) in windows.iter() {
flex_surface.update_window(entity, &window.resolution);
@ -258,31 +288,33 @@ pub fn flex_node_system(
let scale_factor = logical_to_physical_factor * ui_scale.scale;
let viewport_values = LayoutContext::new(scale_factor, physical_size);
fn update_changed<F: ReadOnlyWorldQuery>(
flex_surface: &mut FlexSurface,
scaling_factor: f64,
viewport_values: &LayoutContext,
query: Query<(Entity, &Style, Option<&CalculatedSize>), F>,
) {
// update changed nodes
for (entity, style, calculated_size) in &query {
// TODO: remove node from old hierarchy if its root has changed
if let Some(calculated_size) = calculated_size {
flex_surface.upsert_leaf(entity, style, *calculated_size, scaling_factor);
flex_surface.upsert_leaf(entity, style, *calculated_size, viewport_values);
} else {
flex_surface.upsert_node(entity, style, scaling_factor);
flex_surface.upsert_node(entity, style, viewport_values);
}
}
}
if !scale_factor_events.is_empty() || ui_scale.is_changed() {
if !scale_factor_events.is_empty() || ui_scale.is_changed() || resized {
scale_factor_events.clear();
update_changed(&mut flex_surface, scale_factor, full_node_query);
update_changed(&mut flex_surface, &viewport_values, full_node_query);
} else {
update_changed(&mut flex_surface, scale_factor, node_query);
update_changed(&mut flex_surface, &viewport_values, node_query);
}
for (entity, style, calculated_size) in &changed_size_query {
flex_surface.upsert_leaf(entity, style, *calculated_size, scale_factor);
flex_surface.upsert_leaf(entity, style, *calculated_size, &viewport_values);
}
// clean up removed nodes

View file

@ -81,6 +81,14 @@ pub enum Val {
/// * For `margin`, `padding`, and `border` values: the percentage is relative to the parent node's width.
/// * For positions, `left` and `right` are relative to the parent's width, while `bottom` and `top` are relative to the parent's height.
Percent(f32),
/// Set this value in percent of the viewport width
Vw(f32),
/// Set this value in percent of the viewport height
Vh(f32),
/// Set this value in percent of the viewport's smaller dimension.
VMin(f32),
/// Set this value in percent of the viewport's larger dimension.
VMax(f32),
}
impl Val {
@ -101,6 +109,10 @@ impl Mul<f32> for Val {
Val::Auto => Val::Auto,
Val::Px(value) => Val::Px(value * rhs),
Val::Percent(value) => Val::Percent(value * rhs),
Val::Vw(value) => Val::Vw(value * rhs),
Val::Vh(value) => Val::Vh(value * rhs),
Val::VMin(value) => Val::VMin(value * rhs),
Val::VMax(value) => Val::VMax(value * rhs),
}
}
}
@ -109,7 +121,12 @@ impl MulAssign<f32> for Val {
fn mul_assign(&mut self, rhs: f32) {
match self {
Val::Auto => {}
Val::Px(value) | Val::Percent(value) => *value *= rhs,
Val::Px(value)
| Val::Percent(value)
| Val::Vw(value)
| Val::Vh(value)
| Val::VMin(value)
| Val::VMax(value) => *value *= rhs,
}
}
}
@ -122,6 +139,10 @@ impl Div<f32> for Val {
Val::Auto => Val::Auto,
Val::Px(value) => Val::Px(value / rhs),
Val::Percent(value) => Val::Percent(value / rhs),
Val::Vw(value) => Val::Vw(value / rhs),
Val::Vh(value) => Val::Vh(value / rhs),
Val::VMin(value) => Val::VMin(value / rhs),
Val::VMax(value) => Val::VMax(value / rhs),
}
}
}
@ -130,7 +151,12 @@ impl DivAssign<f32> for Val {
fn div_assign(&mut self, rhs: f32) {
match self {
Val::Auto => {}
Val::Px(value) | Val::Percent(value) => *value /= rhs,
Val::Px(value)
| Val::Percent(value)
| Val::Vw(value)
| Val::Vh(value)
| Val::VMin(value)
| Val::VMax(value) => *value /= rhs,
}
}
}