mirror of
https://github.com/bevyengine/bevy
synced 2024-11-26 06:30:19 +00:00
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:
parent
c809779b6e
commit
2d5ef75c9f
3 changed files with 172 additions and 103 deletions
|
@ -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,
|
||||
},);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue