Skip to content

Commit 2d5ef75

Browse files
authored
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.
1 parent c809779 commit 2d5ef75

File tree

3 files changed

+171
-102
lines changed

3 files changed

+171
-102
lines changed

crates/bevy_ui/src/flex/convert.rs

Lines changed: 95 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,74 @@
1-
use taffy::style::LengthPercentageAuto;
2-
31
use crate::{
42
AlignContent, AlignItems, AlignSelf, Display, FlexDirection, FlexWrap, JustifyContent,
53
PositionType, Size, Style, UiRect, Val,
64
};
75

8-
impl Val {
9-
fn scaled(self, scale_factor: f64) -> Self {
10-
match self {
11-
Val::Auto => Val::Auto,
12-
Val::Percent(value) => Val::Percent(value),
13-
Val::Px(value) => Val::Px((scale_factor * value as f64) as f32),
14-
}
15-
}
6+
use super::LayoutContext;
167

17-
fn to_inset(self) -> LengthPercentageAuto {
8+
impl Val {
9+
fn into_length_percentage_auto(
10+
self,
11+
context: &LayoutContext,
12+
) -> taffy::style::LengthPercentageAuto {
1813
match self {
1914
Val::Auto => taffy::style::LengthPercentageAuto::Auto,
20-
Val::Percent(value) => taffy::style::LengthPercentageAuto::Percent(value / 100.0),
21-
Val::Px(value) => taffy::style::LengthPercentageAuto::Points(value),
15+
Val::Percent(value) => taffy::style::LengthPercentageAuto::Percent(value / 100.),
16+
Val::Px(value) => taffy::style::LengthPercentageAuto::Points(
17+
(context.scale_factor * value as f64) as f32,
18+
),
19+
Val::VMin(value) => {
20+
taffy::style::LengthPercentageAuto::Points(context.min_size * value / 100.)
21+
}
22+
Val::VMax(value) => {
23+
taffy::style::LengthPercentageAuto::Points(context.max_size * value / 100.)
24+
}
25+
Val::Vw(value) => {
26+
taffy::style::LengthPercentageAuto::Points(context.physical_size.x * value / 100.)
27+
}
28+
Val::Vh(value) => {
29+
taffy::style::LengthPercentageAuto::Points(context.physical_size.y * value / 100.)
30+
}
2231
}
2332
}
24-
}
2533

26-
impl UiRect {
27-
fn scaled(self, scale_factor: f64) -> Self {
28-
Self {
29-
left: self.left.scaled(scale_factor),
30-
right: self.right.scaled(scale_factor),
31-
top: self.top.scaled(scale_factor),
32-
bottom: self.bottom.scaled(scale_factor),
34+
fn into_length_percentage(self, context: &LayoutContext) -> taffy::style::LengthPercentage {
35+
match self.into_length_percentage_auto(context) {
36+
taffy::style::LengthPercentageAuto::Auto => taffy::style::LengthPercentage::Points(0.0),
37+
taffy::style::LengthPercentageAuto::Percent(value) => {
38+
taffy::style::LengthPercentage::Percent(value)
39+
}
40+
taffy::style::LengthPercentageAuto::Points(value) => {
41+
taffy::style::LengthPercentage::Points(value)
42+
}
3343
}
3444
}
35-
}
3645

37-
impl Size {
38-
fn scaled(self, scale_factor: f64) -> Self {
39-
Self {
40-
width: self.width.scaled(scale_factor),
41-
height: self.height.scaled(scale_factor),
42-
}
43-
}
44-
}
45-
46-
impl<T: From<Val>> From<UiRect> for taffy::prelude::Rect<T> {
47-
fn from(value: UiRect) -> Self {
48-
Self {
49-
left: value.left.into(),
50-
right: value.right.into(),
51-
top: value.top.into(),
52-
bottom: value.bottom.into(),
53-
}
46+
fn into_dimension(self, context: &LayoutContext) -> taffy::style::Dimension {
47+
self.into_length_percentage_auto(context).into()
5448
}
5549
}
5650

57-
impl<T: From<Val>> From<Size> for taffy::prelude::Size<T> {
58-
fn from(value: Size) -> Self {
59-
Self {
60-
width: value.width.into(),
61-
height: value.height.into(),
62-
}
63-
}
64-
}
65-
66-
impl From<Val> for taffy::style::Dimension {
67-
fn from(value: Val) -> Self {
68-
match value {
69-
Val::Auto => taffy::style::Dimension::Auto,
70-
Val::Percent(value) => taffy::style::Dimension::Percent(value / 100.0),
71-
Val::Px(value) => taffy::style::Dimension::Points(value),
72-
}
73-
}
74-
}
75-
76-
impl From<Val> for taffy::style::LengthPercentage {
77-
fn from(value: Val) -> Self {
78-
match value {
79-
Val::Auto => taffy::style::LengthPercentage::Points(0.0),
80-
Val::Percent(value) => taffy::style::LengthPercentage::Percent(value / 100.0),
81-
Val::Px(value) => taffy::style::LengthPercentage::Points(value),
51+
impl UiRect {
52+
fn map_to_taffy_rect<T>(self, map_fn: impl Fn(Val) -> T) -> taffy::geometry::Rect<T> {
53+
taffy::geometry::Rect {
54+
left: map_fn(self.left),
55+
right: map_fn(self.right),
56+
top: map_fn(self.top),
57+
bottom: map_fn(self.bottom),
8258
}
8359
}
8460
}
8561

86-
impl From<Val> for taffy::style::LengthPercentageAuto {
87-
fn from(value: Val) -> Self {
88-
match value {
89-
Val::Auto => taffy::style::LengthPercentageAuto::Auto,
90-
Val::Percent(value) => taffy::style::LengthPercentageAuto::Percent(value / 100.0),
91-
Val::Px(value) => taffy::style::LengthPercentageAuto::Points(value),
62+
impl Size {
63+
fn map_to_taffy_size<T>(self, map_fn: impl Fn(Val) -> T) -> taffy::geometry::Size<T> {
64+
taffy::geometry::Size {
65+
width: map_fn(self.width),
66+
height: map_fn(self.height),
9267
}
9368
}
9469
}
9570

96-
pub fn from_style(scale_factor: f64, style: &Style) -> taffy::style::Style {
71+
pub fn from_style(context: &LayoutContext, style: &Style) -> taffy::style::Style {
9772
taffy::style::Style {
9873
display: style.display.into(),
9974
position: style.position_type.into(),
@@ -104,22 +79,34 @@ pub fn from_style(scale_factor: f64, style: &Style) -> taffy::style::Style {
10479
align_content: Some(style.align_content.into()),
10580
justify_content: Some(style.justify_content.into()),
10681
inset: taffy::prelude::Rect {
107-
left: style.left.scaled(scale_factor).to_inset(),
108-
right: style.right.scaled(scale_factor).to_inset(),
109-
top: style.top.scaled(scale_factor).to_inset(),
110-
bottom: style.bottom.scaled(scale_factor).to_inset(),
82+
left: style.left.into_length_percentage_auto(context),
83+
right: style.right.into_length_percentage_auto(context),
84+
top: style.top.into_length_percentage_auto(context),
85+
bottom: style.bottom.into_length_percentage_auto(context),
11186
},
112-
margin: style.margin.scaled(scale_factor).into(),
113-
padding: style.padding.scaled(scale_factor).into(),
114-
border: style.border.scaled(scale_factor).into(),
87+
margin: style
88+
.margin
89+
.map_to_taffy_rect(|m| m.into_length_percentage_auto(context)),
90+
padding: style
91+
.padding
92+
.map_to_taffy_rect(|m| m.into_length_percentage(context)),
93+
border: style
94+
.border
95+
.map_to_taffy_rect(|m| m.into_length_percentage(context)),
11596
flex_grow: style.flex_grow,
11697
flex_shrink: style.flex_shrink,
117-
flex_basis: style.flex_basis.scaled(scale_factor).into(),
118-
size: style.size.scaled(scale_factor).into(),
119-
min_size: style.min_size.scaled(scale_factor).into(),
120-
max_size: style.max_size.scaled(scale_factor).into(),
98+
flex_basis: style.flex_basis.into_dimension(context),
99+
size: style.size.map_to_taffy_size(|s| s.into_dimension(context)),
100+
min_size: style
101+
.min_size
102+
.map_to_taffy_size(|s| s.into_dimension(context)),
103+
max_size: style
104+
.max_size
105+
.map_to_taffy_size(|s| s.into_dimension(context)),
121106
aspect_ratio: style.aspect_ratio,
122-
gap: style.gap.scaled(scale_factor).into(),
107+
gap: style
108+
.gap
109+
.map_to_taffy_size(|s| s.into_length_percentage(context)),
123110
justify_self: None,
124111
}
125112
}
@@ -283,7 +270,8 @@ mod tests {
283270
height: Val::Percent(0.),
284271
},
285272
};
286-
let taffy_style = from_style(1.0, &bevy_style);
273+
let viewport_values = LayoutContext::new(1.0, bevy_math::Vec2::new(800., 600.));
274+
let taffy_style = from_style(&viewport_values, &bevy_style);
287275
assert_eq!(taffy_style.display, taffy::style::Display::Flex);
288276
assert_eq!(taffy_style.position, taffy::style::Position::Absolute);
289277
assert!(matches!(
@@ -408,4 +396,27 @@ mod tests {
408396
taffy::style::LengthPercentage::Percent(0.)
409397
);
410398
}
399+
400+
#[test]
401+
fn test_into_length_percentage() {
402+
use taffy::style::LengthPercentage;
403+
let context = LayoutContext::new(2.0, bevy_math::Vec2::new(800., 600.));
404+
let cases = [
405+
(Val::Auto, LengthPercentage::Points(0.)),
406+
(Val::Percent(1.), LengthPercentage::Percent(0.01)),
407+
(Val::Px(1.), LengthPercentage::Points(2.)),
408+
(Val::Vw(1.), LengthPercentage::Points(8.)),
409+
(Val::Vh(1.), LengthPercentage::Points(6.)),
410+
(Val::VMin(2.), LengthPercentage::Points(12.)),
411+
(Val::VMax(2.), LengthPercentage::Points(16.)),
412+
];
413+
for (val, length) in cases {
414+
assert!(match (val.into_length_percentage(&context), length) {
415+
(LengthPercentage::Points(a), LengthPercentage::Points(b))
416+
| (LengthPercentage::Percent(a), LengthPercentage::Percent(b)) =>
417+
(a - b).abs() < 0.0001,
418+
_ => false,
419+
},);
420+
}
421+
}
411422
}

crates/bevy_ui/src/flex/mod.rs

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,25 @@ use taffy::{
2222
Taffy,
2323
};
2424

25+
pub struct LayoutContext {
26+
pub scale_factor: f64,
27+
pub physical_size: Vec2,
28+
pub min_size: f32,
29+
pub max_size: f32,
30+
}
31+
32+
impl LayoutContext {
33+
/// create new a [`LayoutContext`] from the window's physical size and scale factor
34+
fn new(scale_factor: f64, physical_size: Vec2) -> Self {
35+
Self {
36+
scale_factor,
37+
physical_size,
38+
min_size: physical_size.x.min(physical_size.y),
39+
max_size: physical_size.x.max(physical_size.y),
40+
}
41+
}
42+
}
43+
2544
#[derive(Resource)]
2645
pub struct FlexSurface {
2746
entity_to_taffy: HashMap<Entity, taffy::node::Node>,
@@ -59,19 +78,17 @@ impl Default for FlexSurface {
5978
}
6079

6180
impl FlexSurface {
62-
pub fn upsert_node(&mut self, entity: Entity, style: &Style, scale_factor: f64) {
81+
pub fn upsert_node(&mut self, entity: Entity, style: &Style, context: &LayoutContext) {
6382
let mut added = false;
6483
let taffy = &mut self.taffy;
6584
let taffy_node = self.entity_to_taffy.entry(entity).or_insert_with(|| {
6685
added = true;
67-
taffy
68-
.new_leaf(convert::from_style(scale_factor, style))
69-
.unwrap()
86+
taffy.new_leaf(convert::from_style(context, style)).unwrap()
7087
});
7188

7289
if !added {
7390
self.taffy
74-
.set_style(*taffy_node, convert::from_style(scale_factor, style))
91+
.set_style(*taffy_node, convert::from_style(context, style))
7592
.unwrap();
7693
}
7794
}
@@ -81,10 +98,11 @@ impl FlexSurface {
8198
entity: Entity,
8299
style: &Style,
83100
calculated_size: CalculatedSize,
84-
scale_factor: f64,
101+
context: &LayoutContext,
85102
) {
86103
let taffy = &mut self.taffy;
87-
let taffy_style = convert::from_style(scale_factor, style);
104+
let taffy_style = convert::from_style(context, style);
105+
let scale_factor = context.scale_factor;
88106
let measure = taffy::node::MeasureFunc::Boxed(Box::new(
89107
move |constraints: Size<Option<f32>>, _available: Size<AvailableSpace>| {
90108
let mut size = Size {
@@ -229,6 +247,7 @@ pub fn flex_node_system(
229247
windows: Query<(Entity, &Window)>,
230248
ui_scale: Res<UiScale>,
231249
mut scale_factor_events: EventReader<WindowScaleFactorChanged>,
250+
mut resize_events: EventReader<bevy_window::WindowResized>,
232251
mut flex_surface: ResMut<FlexSurface>,
233252
root_node_query: Query<Entity, (With<Node>, Without<Parent>)>,
234253
node_query: Query<(Entity, &Style, Option<&CalculatedSize>), (With<Node>, Changed<Style>)>,
@@ -244,45 +263,58 @@ pub fn flex_node_system(
244263
) {
245264
// assume one window for time being...
246265
// TODO: Support window-independent scaling: https://github.com/bevyengine/bevy/issues/5621
247-
let (primary_window_entity, logical_to_physical_factor) =
266+
let (primary_window_entity, logical_to_physical_factor, physical_size) =
248267
if let Ok((entity, primary_window)) = primary_window.get_single() {
249-
(entity, primary_window.resolution.scale_factor())
268+
(
269+
entity,
270+
primary_window.resolution.scale_factor(),
271+
Vec2::new(
272+
primary_window.resolution.physical_width() as f32,
273+
primary_window.resolution.physical_height() as f32,
274+
),
275+
)
250276
} else {
251277
return;
252278
};
253279

280+
let resized = resize_events
281+
.iter()
282+
.any(|resized_window| resized_window.window == primary_window_entity);
283+
254284
// update window root nodes
255285
for (entity, window) in windows.iter() {
256286
flex_surface.update_window(entity, &window.resolution);
257287
}
258288

259289
let scale_factor = logical_to_physical_factor * ui_scale.scale;
260290

291+
let viewport_values = LayoutContext::new(scale_factor, physical_size);
292+
261293
fn update_changed<F: ReadOnlyWorldQuery>(
262294
flex_surface: &mut FlexSurface,
263-
scaling_factor: f64,
295+
viewport_values: &LayoutContext,
264296
query: Query<(Entity, &Style, Option<&CalculatedSize>), F>,
265297
) {
266298
// update changed nodes
267299
for (entity, style, calculated_size) in &query {
268300
// TODO: remove node from old hierarchy if its root has changed
269301
if let Some(calculated_size) = calculated_size {
270-
flex_surface.upsert_leaf(entity, style, *calculated_size, scaling_factor);
302+
flex_surface.upsert_leaf(entity, style, *calculated_size, viewport_values);
271303
} else {
272-
flex_surface.upsert_node(entity, style, scaling_factor);
304+
flex_surface.upsert_node(entity, style, viewport_values);
273305
}
274306
}
275307
}
276308

277-
if !scale_factor_events.is_empty() || ui_scale.is_changed() {
309+
if !scale_factor_events.is_empty() || ui_scale.is_changed() || resized {
278310
scale_factor_events.clear();
279-
update_changed(&mut flex_surface, scale_factor, full_node_query);
311+
update_changed(&mut flex_surface, &viewport_values, full_node_query);
280312
} else {
281-
update_changed(&mut flex_surface, scale_factor, node_query);
313+
update_changed(&mut flex_surface, &viewport_values, node_query);
282314
}
283315

284316
for (entity, style, calculated_size) in &changed_size_query {
285-
flex_surface.upsert_leaf(entity, style, *calculated_size, scale_factor);
317+
flex_surface.upsert_leaf(entity, style, *calculated_size, &viewport_values);
286318
}
287319

288320
// clean up removed nodes

0 commit comments

Comments
 (0)