Skip to content

Commit

Permalink
Val viewport unit variants (#8137)
Browse files Browse the repository at this point in the history
# 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.
  • Loading branch information
ickshonpe authored Mar 21, 2023
1 parent c809779 commit 2d5ef75
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 102 deletions.
179 changes: 95 additions & 84 deletions crates/bevy_ui/src/flex/convert.rs
Original file line number Diff line number Diff line change
@@ -1,99 +1,74 @@
use taffy::style::LengthPercentageAuto;

use crate::{
AlignContent, AlignItems, AlignSelf, Display, FlexDirection, FlexWrap, JustifyContent,
PositionType, Size, Style, UiRect, Val,
};

impl Val {
fn scaled(self, scale_factor: f64) -> Self {
match self {
Val::Auto => Val::Auto,
Val::Percent(value) => Val::Percent(value),
Val::Px(value) => Val::Px((scale_factor * value as f64) as f32),
}
}
use super::LayoutContext;

fn to_inset(self) -> LengthPercentageAuto {
impl Val {
fn into_length_percentage_auto(
self,
context: &LayoutContext,
) -> taffy::style::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),
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.)
}
}
}
}

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 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)
}
}
}
}

impl Size {
fn scaled(self, scale_factor: f64) -> Self {
Self {
width: self.width.scaled(scale_factor),
height: self.height.scaled(scale_factor),
}
}
}

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(),
}
fn into_dimension(self, context: &LayoutContext) -> taffy::style::Dimension {
self.into_length_percentage_auto(context).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 UiRect {
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 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),
impl Size {
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),
}
}
}

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(),
Expand All @@ -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,
}
}
Expand Down Expand Up @@ -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!(
Expand Down Expand Up @@ -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,
},);
}
}
}
64 changes: 48 additions & 16 deletions crates/bevy_ui/src/flex/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand Down Expand Up @@ -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();
}
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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>)>,
Expand All @@ -244,45 +263,58 @@ 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);
}

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
Expand Down
Loading

0 comments on commit 2d5ef75

Please sign in to comment.