From 6c8ca568ad999f03c8bb98ec5eff707575dc95ee Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sun, 1 Nov 2026 08:30:39 +0000 Subject: [PATCH 01/50] Draft the structure for the calc impl --- crates/bevy_ui/src/calc.rs | 10 ++++++++++ crates/bevy_ui/src/geometry.rs | 3 +++ crates/bevy_ui/src/lib.rs | 1 + 3 files changed, 14 insertions(+) create mode 100644 crates/bevy_ui/src/calc.rs diff --git a/crates/bevy_ui/src/calc.rs b/crates/bevy_ui/src/calc.rs new file mode 100644 index 0000000000000..0a4ebe7fa8c2e --- /dev/null +++ b/crates/bevy_ui/src/calc.rs @@ -0,0 +1,10 @@ +pub struct CalcId(u16); + +pub enum CalcOps { + Add(CalcId, CalcId), + Sub(CalcId, CalcId), + Scale(CalcId, f32), + Min(CalcId, CalcId), + Max(CalcId, CalcId), + Clamp(CalcId, CalcId), +} diff --git a/crates/bevy_ui/src/geometry.rs b/crates/bevy_ui/src/geometry.rs index 32eb12bcca7a5..0c6ed030d93e4 100644 --- a/crates/bevy_ui/src/geometry.rs +++ b/crates/bevy_ui/src/geometry.rs @@ -8,6 +8,8 @@ use thiserror::Error; #[cfg(feature = "serialize")] use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; +use crate::calc::CalcId; + /// Represents the possible value types for layout properties. /// /// This enum allows specifying values for various [`Node`](crate::Node) properties in different units, @@ -65,6 +67,7 @@ pub enum Val { /// Unlike [`Val::Em`] this ignores the node's own font size, so it means the same /// length anywhere in the hierarchy. Resolved from the [`RemSize`] resource. Rem(f32), + Calc(CalcId), } #[derive(Debug, Error, PartialEq, Eq)] diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 65a04cf96d49d..d0ac5aaa753bb 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -18,6 +18,7 @@ pub mod measurement; pub mod update; pub mod widget; +pub mod calc; pub mod gradients; #[cfg(feature = "bevy_picking")] pub mod picking_backend; From 251a64ea003e1d85748146243b7eaee12bf07979 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sun, 1 Nov 2026 08:34:21 +0000 Subject: [PATCH 02/50] calc module --- crates/bevy_ui/src/calc.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/crates/bevy_ui/src/calc.rs b/crates/bevy_ui/src/calc.rs index 0a4ebe7fa8c2e..54bc9ad301f6f 100644 --- a/crates/bevy_ui/src/calc.rs +++ b/crates/bevy_ui/src/calc.rs @@ -1,10 +1,12 @@ -pub struct CalcId(u16); +use crate::Val; + +pub struct CalcId(u32); pub enum CalcOps { - Add(CalcId, CalcId), - Sub(CalcId, CalcId), - Scale(CalcId, f32), - Min(CalcId, CalcId), - Max(CalcId, CalcId), - Clamp(CalcId, CalcId), + Add(Val, Val), + Sub(Val, Val), + Scale(Val, f32), + Min(Val, Val), + Max(Val, Val), + Clamp(Val, Val), } From a873e94bd352b694dbe435a581ad4fad7c067bbb Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sun, 1 Nov 2026 09:08:04 +0000 Subject: [PATCH 03/50] Calc as a tree --- .../bevy_dev_tools/src/diagnostics_overlay.rs | 2 +- .../bevy_feathers/src/controls/color_plane.rs | 2 +- .../src/controls/color_slider.rs | 2 +- .../bevy_feathers/src/controls/color_wheel.rs | 2 +- .../bevy_feathers/src/controls/scrollbar.rs | 6 +- crates/bevy_feathers/src/controls/tabs.rs | 12 +- .../bevy_feathers/src/controls/tree_view.rs | 2 +- crates/bevy_feathers/src/rounded_corners.rs | 32 +-- crates/bevy_ui/Cargo.toml | 1 + crates/bevy_ui/src/calc.rs | 147 ++++++++++++- crates/bevy_ui/src/geometry.rs | 178 +++++++++------ crates/bevy_ui/src/gradients.rs | 8 +- crates/bevy_ui/src/layout/convert.rs | 208 ++++++++++++------ crates/bevy_ui/src/layout/layout_tree.rs | 188 ++++++++++------ crates/bevy_ui/src/layout/tests.rs | 123 +++++++++++ crates/bevy_ui/src/measurement.rs | 5 +- crates/bevy_ui/src/ui_node.rs | 28 +-- crates/bevy_ui/src/ui_transform.rs | 8 +- crates/bevy_ui_render/src/box_shadow.rs | 19 +- crates/bevy_ui_widgets/src/dialog.rs | 3 +- crates/bevy_ui_widgets/src/scrollbar.rs | 10 +- examples/scene/bsn.rs | 2 +- examples/showcase/stepping.rs | 8 +- examples/stress_tests/many_buttons.rs | 8 +- examples/stress_tests/many_text.rs | 2 +- examples/testbed/full_ui.rs | 10 +- examples/testbed/ui.rs | 90 +++++--- examples/ui/layout/ghost_nodes.rs | 2 +- examples/ui/layout/size_constraints.rs | 10 +- examples/ui/styling/borders.rs | 101 ++++----- examples/ui/text/inline_image.rs | 8 +- examples/ui/ui_transform.rs | 2 +- examples/ui/widgets/viewport_node.rs | 2 +- 33 files changed, 858 insertions(+), 373 deletions(-) diff --git a/crates/bevy_dev_tools/src/diagnostics_overlay.rs b/crates/bevy_dev_tools/src/diagnostics_overlay.rs index 43234d16b55e5..d11bb30c80e9a 100644 --- a/crates/bevy_dev_tools/src/diagnostics_overlay.rs +++ b/crates/bevy_dev_tools/src/diagnostics_overlay.rs @@ -24,7 +24,7 @@ const BACKGROUND_COLOR_ALPHA: f32 = 0.75; /// Row and column gap for the diagnostics overlay const ROW_COLUMN_GAP: Val = Val::Px(4.); /// Padding for cels of the diagnostics overlay -const DEFAULT_PADDING: UiRect = UiRect::all(Val::Px(4.)); +const DEFAULT_PADDING: UiRect = UiRect::px(4., 4., 4., 4.); /// Initial Z-index for the [`DiagnosticsOverlayPlane`] pub const INITIAL_DIAGNOSTICS_OVERLAY_PLANE_Z_INDEX: GlobalZIndex = GlobalZIndex(1_000_000); /// Alias to shorten the name diff --git a/crates/bevy_feathers/src/controls/color_plane.rs b/crates/bevy_feathers/src/controls/color_plane.rs index a12cc9eca5a09..4adc7d6d2c585 100644 --- a/crates/bevy_feathers/src/controls/color_plane.rs +++ b/crates/bevy_feathers/src/controls/color_plane.rs @@ -253,7 +253,7 @@ fn update_plane_thumb_position( }; let inner_size = inner_node.size() * inner_node.inverse_scale_factor; if inner_size.x > 0.0 && inner_size.y > 0.0 { - let mut updated_transform = *thumb_transform; + let mut updated_transform = thumb_transform.clone(); // `ColorPlaneValue` is in channel space, where y increases upward, while // the transform is in screen space (+y down), so the y component is inverted. updated_transform.translation = Val2::new( diff --git a/crates/bevy_feathers/src/controls/color_slider.rs b/crates/bevy_feathers/src/controls/color_slider.rs index 7454763430a43..864b00f1acce0 100644 --- a/crates/bevy_feathers/src/controls/color_slider.rs +++ b/crates/bevy_feathers/src/controls/color_slider.rs @@ -338,7 +338,7 @@ fn update_slider_pos( { let track_width = track_node.size().x * track_node.inverse_scale_factor; let thumb_offset = range.thumb_position(value.0) * track_width - THUMB_SIZE * 0.5; - let mut updated_transform = *thumb_transform; + let mut updated_transform = thumb_transform.clone(); updated_transform.translation.x = px(thumb_offset); thumb_transform.set_if_neq(updated_transform); } diff --git a/crates/bevy_feathers/src/controls/color_wheel.rs b/crates/bevy_feathers/src/controls/color_wheel.rs index e82428e298fe4..d9b460d7f3a5d 100644 --- a/crates/bevy_feathers/src/controls/color_wheel.rs +++ b/crates/bevy_feathers/src/controls/color_wheel.rs @@ -340,7 +340,7 @@ fn position_thumb(q_transform: &mut Query<&mut UiTransform>, thumb_ent: Entity, let Ok(mut thumb_transform) = q_transform.get_mut(thumb_ent) else { return; }; - let mut updated_transform = *thumb_transform; + let mut updated_transform = thumb_transform.clone(); updated_transform.translation = Val2::px(position.x - THUMB_SIZE * 0.5, position.y - THUMB_SIZE * 0.5); thumb_transform.set_if_neq(updated_transform); diff --git a/crates/bevy_feathers/src/controls/scrollbar.rs b/crates/bevy_feathers/src/controls/scrollbar.rs index 0c6eeee1ca91f..87faeb517feb1 100644 --- a/crates/bevy_feathers/src/controls/scrollbar.rs +++ b/crates/bevy_feathers/src/controls/scrollbar.rs @@ -143,7 +143,11 @@ fn update_scrollbar_visibility( if let Ok(child_of) = q_parents.get(scrollbar_ent) && let Ok((gutter, mut node)) = q_gutters.get_mut(child_of.parent()) { - let padding = if overflows { gutter.0 } else { Val::ZERO }; + let padding = if overflows { + gutter.0.clone() + } else { + Val::ZERO + }; match scrollbar.orientation { ControlOrientation::Vertical => { if node.padding.right != padding { diff --git a/crates/bevy_feathers/src/controls/tabs.rs b/crates/bevy_feathers/src/controls/tabs.rs index 601a7cf8c1e57..a2134a1fc290b 100644 --- a/crates/bevy_feathers/src/controls/tabs.rs +++ b/crates/bevy_feathers/src/controls/tabs.rs @@ -526,12 +526,12 @@ fn arrange_tab_lists( if let Ok(mut node) = start.get_mut(child) && node.margin != start_margin { - node.margin = start_margin; + node.margin = start_margin.clone(); } if let Ok(mut node) = end.get_mut(child) && node.margin != end_margin { - node.margin = end_margin; + node.margin = end_margin.clone(); } } let rank = |child: &Entity| { @@ -684,8 +684,8 @@ fn fillet_node(visible: bool, edge: PhysicalEdge) -> Node { true => Display::Flex, false => Display::None, }, - width: size, - height: size, + width: size.clone(), + height: size.clone(), ..Default::default() }; match edge { @@ -942,8 +942,8 @@ fn spawn_tab_drag_proxy( align_items: AlignItems::Center, padding: UiRect::horizontal(px(TAB_PADDING)), column_gap: px(TAB_GAP), - border: tab_node.border, - border_radius: tab_node.border_radius, + border: tab_node.border.clone(), + border_radius: tab_node.border_radius.clone(), ..Default::default() }, GlobalZIndex(DRAG_PROXY_Z), diff --git a/crates/bevy_feathers/src/controls/tree_view.rs b/crates/bevy_feathers/src/controls/tree_view.rs index 3df72140bda1d..411102a49c746 100644 --- a/crates/bevy_feathers/src/controls/tree_view.rs +++ b/crates/bevy_feathers/src/controls/tree_view.rs @@ -499,7 +499,7 @@ fn update_row_indent( if let Ok(mut node) = headers.get_mut(child) && node.padding.left != left { - node.padding.left = left; + node.padding.left = left.clone(); } } } diff --git a/crates/bevy_feathers/src/rounded_corners.rs b/crates/bevy_feathers/src/rounded_corners.rs index 8d5780e8e8ae7..71216d88bf25f 100644 --- a/crates/bevy_feathers/src/rounded_corners.rs +++ b/crates/bevy_feathers/src/rounded_corners.rs @@ -45,49 +45,49 @@ impl RoundedCorners { RoundedCorners::All => BorderRadius::all(radius), RoundedCorners::TopLeft => BorderRadius { top_left: radius, - top_right: zero, - bottom_right: zero, + top_right: zero.clone(), + bottom_right: zero.clone(), bottom_left: zero, }, RoundedCorners::TopRight => BorderRadius { - top_left: zero, + top_left: zero.clone(), top_right: radius, - bottom_right: zero, + bottom_right: zero.clone(), bottom_left: zero, }, RoundedCorners::BottomRight => BorderRadius { - top_left: zero, - top_right: zero, + top_left: zero.clone(), + top_right: zero.clone(), bottom_right: radius, bottom_left: zero, }, RoundedCorners::BottomLeft => BorderRadius { - top_left: zero, - top_right: zero, + top_left: zero.clone(), + top_right: zero.clone(), bottom_right: zero, bottom_left: radius, }, RoundedCorners::Top => BorderRadius { - top_left: radius, + top_left: radius.clone(), top_right: radius, - bottom_right: zero, + bottom_right: zero.clone(), bottom_left: zero, }, RoundedCorners::Right => BorderRadius { - top_left: zero, - top_right: radius, + top_left: zero.clone(), + top_right: radius.clone(), bottom_right: radius, bottom_left: zero, }, RoundedCorners::Bottom => BorderRadius { - top_left: zero, + top_left: zero.clone(), top_right: zero, - bottom_right: radius, + bottom_right: radius.clone(), bottom_left: radius, }, RoundedCorners::Left => BorderRadius { - top_left: radius, - top_right: zero, + top_left: radius.clone(), + top_right: zero.clone(), bottom_right: zero, bottom_left: radius, }, diff --git a/crates/bevy_ui/Cargo.toml b/crates/bevy_ui/Cargo.toml index 0b83db396f0c7..82f08b9dfc89d 100644 --- a/crates/bevy_ui/Cargo.toml +++ b/crates/bevy_ui/Cargo.toml @@ -43,6 +43,7 @@ taffy = { version = "0.15", default-features = false, features = [ "flexbox", "grid", "content_size", + "calc", ] } serde = { version = "1", features = ["derive"], optional = true } uuid = { version = "1.21.0", features = ["v4"], optional = true } diff --git a/crates/bevy_ui/src/calc.rs b/crates/bevy_ui/src/calc.rs index 54bc9ad301f6f..adc2b2da6da9a 100644 --- a/crates/bevy_ui/src/calc.rs +++ b/crates/bevy_ui/src/calc.rs @@ -1,12 +1,153 @@ -use crate::Val; +use bevy_math::Vec2; +use bevy_reflect::Reflect; +use bevy_text::{EmSize, RemSize}; -pub struct CalcId(u32); +#[cfg(feature = "serialize")] +use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; +use crate::{Val, ValArithmeticError}; + +#[derive(Clone, Debug, PartialEq, Reflect)] +#[reflect(opaque, PartialEq, Debug, Clone)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub struct Calc(pub Box); + +#[derive(Clone, Debug, PartialEq, Reflect)] +#[reflect(PartialEq, Debug, Clone)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] pub enum CalcOps { Add(Val, Val), Sub(Val, Val), Scale(Val, f32), Min(Val, Val), Max(Val, Val), - Clamp(Val, Val), + Clamp(Val, Val, Val), +} + +impl CalcOps { + pub(crate) fn uses_percent(&self) -> bool { + let uses_percent = |val: &Val| match val { + Val::Percent(_) => true, + Val::Calc(calc) => calc.0.uses_percent(), + _ => false, + }; + match self { + Self::Add(a, b) | Self::Sub(a, b) | Self::Min(a, b) | Self::Max(a, b) => { + uses_percent(a) || uses_percent(b) + } + Self::Scale(value, _) => uses_percent(value), + Self::Clamp(min, value, max) => { + uses_percent(min) || uses_percent(value) || uses_percent(max) + } + } + } + + pub fn resolve( + &self, + scale_factor: f32, + physical_base_value: f32, + physical_target_size: Vec2, + em_size: EmSize, + rem_size: RemSize, + ) -> Result { + let resolve = |val: &Val| { + val.resolve( + scale_factor, + physical_base_value, + physical_target_size, + em_size, + rem_size, + ) + }; + Ok(match self { + Self::Add(a, b) => resolve(a)? + resolve(b)?, + Self::Sub(a, b) => resolve(a)? - resolve(b)?, + Self::Scale(value, scale) => resolve(value)? * scale, + Self::Min(a, b) => resolve(a)?.min(resolve(b)?), + Self::Max(a, b) => resolve(a)?.max(resolve(b)?), + Self::Clamp(min, value, max) => resolve(min)?.max(resolve(value)?.min(resolve(max)?)), + }) + } +} + +impl From for Val { + fn from(op: CalcOps) -> Self { + Self::Calc(Calc(Box::new(op))) + } +} + +#[cfg(te let value = + Val::Percent(50.0) - Val::Px(10.0) + Val::Em(2.0) + Val::Rem(1.0) + Val::Vw(1.0); + let resolve = |value: &Val| { + value.resolve( + 2.0, + 400.0, + Vec2::new(1000.0, 500.0), + EmSize(12.0), + RemSize(20.0), + ) + }; + assert_eq!(resolve(&value), Ok(278.0)); + assert_eq!(resolve(&(value.clone() * 2.0)), Ok(556.0)); + assert_eq!(resolve(&(value.clone() / 2.0)), Ok(139.0)); + assert_eq!(resolve(&-value.clone()), Ok(-278.0)); + assert_eq!( + resolve(&CalcOps::Min(value.clone(), Val::Px(100.0)).into()), + Ok(200.0) + ); + assert_eq!( + resolve(&CalcOps::Max(value.clone(), Val::Px(200.0)).into()), + Ok(400.0) + ); + assert_eq!( + resolve(&CalcOps::Clamp(Val::Px(100.0), value.clone(), Val::Px(125.0)).into()), + Ok(250.0) + ); + assert_eq!( + resolve(&CalcOps::Clamp(Val::Px(200.0), value.clone(), Val::Px(100.0)).into()), + Ok(400.0) + ); + let mut scaled = value; + scaled *= 2.0; + scaled /= 4.0; + assert_eq!(resolve(&scaled), Ok(139.0)); + assert_eq!( + resolve(&(Val::Auto + Val::Px(1.0))), + Err(ValArithmeticError::NonEvaluable) + ); +k(139.0)); + assert_eq!(resolve(&(Val::Auto + Val::Px(1.0))), Err(ValArithmeticError::NonEvaluable)); + } + + #[test] + fn clone_and_reflection_preserve_expressions() { + use bevy_reflect::{FromReflect, PartialReflect, TypeRegistry}; + + let value = Val::Percent(100.0) - Val::Px(24.0); + let mut cloned = value.clone(); + assert_eq!(value, cloned); + cloned *= 2.0; + assert_ne!(value, cloned); + assert_eq!(value, Val::Percent(100 assert_eq!( + bevy_text::FontSize::try_from(Val::Rem(2.0)), + Ok(bevy_text::FontSize::Rem(2.0)) + ); + assert_eq!( + bevy_text::FontSize::try_from(value.clone()), + Err(ValArithmeticError::NonEvaluable) + ); +.0) - Val::Px(24.0)); + let mut registry = TypeRegistry::default(); + registry.register::(); + let dynamic = value.to_dynamic().unwrap(); + assert_eq!(Val::from_reflect(dynamic.as_ref()), Some(value)); + } } diff --git a/crates/bevy_ui/src/geometry.rs b/crates/bevy_ui/src/geometry.rs index 0c6ed030d93e4..c7c2bc8f8090d 100644 --- a/crates/bevy_ui/src/geometry.rs +++ b/crates/bevy_ui/src/geometry.rs @@ -2,13 +2,13 @@ use bevy_math::{MismatchedUnitsError, StableInterpolate as _, TryStableInterpola use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_text::{EmSize, FontSize, RemSize}; use bevy_utils::default; -use core::ops::{Div, DivAssign, Mul, MulAssign, Neg}; +use core::ops::{Add, Div, DivAssign, Mul, MulAssign, Neg, Sub}; use thiserror::Error; #[cfg(feature = "serialize")] use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; -use crate::calc::CalcId; +use crate::calc::{Calc}; /// Represents the possible value types for layout properties. /// @@ -24,7 +24,7 @@ use crate::calc::CalcId; /// * `vmax`: percentage of the viewport's larger dimension /// /// Additionally, `auto` will be parsed as [`Val::Auto`]. -#[derive(Copy, Clone, Debug, Reflect)] +#[derive(Clone, Debug, Reflect)] #[reflect(Default, PartialEq, Debug, Clone)] #[cfg_attr( feature = "serialize", @@ -67,7 +67,7 @@ pub enum Val { /// Unlike [`Val::Em`] this ignores the node's own font size, so it means the same /// length anywhere in the hierarchy. Resolved from the [`RemSize`] resource. Rem(f32), - Calc(CalcId), + Calc(Calc), } #[derive(Debug, Error, PartialEq, Eq)] @@ -140,6 +140,9 @@ impl core::str::FromStr for Val { impl PartialEq for Val { fn eq(&self, other: &Self) -> bool { + if let (Self::Calc(a), Self::Calc(b)) = (self, other) { + return a == b; + } let same_unit = matches!( (self, other), (Self::Auto, Self::Auto) @@ -176,13 +179,13 @@ impl Val { /// The largest finite [`Val::Px`] value. pub const MAX: Self = Self::Px(f32::MAX); - /// Returns the number this `Val` wraps, whatever its unit, or `None` for [`Val::Auto`]. + /// Returns the number this `Val` wraps, whatever its unit, or `None` for [`Val::Auto`] and [`Val::Calc`]. /// /// The number is meaningless without the variant it came from — `Val::Px(4.0)` and /// `Val::Rem(4.0)` both return `4.0`. Use [`Val::resolve`] for a length in pixels. - const fn inner(self) -> Option { + const fn inner(&self) -> Option { match self { - Self::Auto => None, + Self::Auto | Self::Calc(_) => None, Self::Px(v) | Self::Percent(v) | Self::Vw(v) @@ -190,7 +193,7 @@ impl Val { | Self::VMin(v) | Self::VMax(v) | Self::Em(v) - | Self::Rem(v) => Some(v), + | Self::Rem(v) => Some(*v), } } @@ -210,7 +213,7 @@ impl Val { /// assert_eq!(ui_rect.top, Val::ZERO); /// assert_eq!(ui_rect.bottom, Val::ZERO); /// ``` - pub const fn left(self) -> UiRect { + pub fn left(self) -> UiRect { UiRect::left(self) } @@ -230,7 +233,7 @@ impl Val { /// assert_eq!(ui_rect.top, Val::ZERO); /// assert_eq!(ui_rect.bottom, Val::ZERO); /// ``` - pub const fn right(self) -> UiRect { + pub fn right(self) -> UiRect { UiRect::right(self) } @@ -250,7 +253,7 @@ impl Val { /// assert_eq!(ui_rect.top, Val::Px(1.)); /// assert_eq!(ui_rect.bottom, Val::ZERO); /// ``` - pub const fn top(self) -> UiRect { + pub fn top(self) -> UiRect { UiRect::top(self) } @@ -270,7 +273,7 @@ impl Val { /// assert_eq!(ui_rect.top, Val::ZERO); /// assert_eq!(ui_rect.bottom, Val::Px(1.)); /// ``` - pub const fn bottom(self) -> UiRect { + pub fn bottom(self) -> UiRect { UiRect::bottom(self) } @@ -288,7 +291,7 @@ impl Val { /// assert_eq!(ui_rect.top, Val::Px(1.)); /// assert_eq!(ui_rect.bottom, Val::Px(1.)); /// ``` - pub const fn all(self) -> UiRect { + pub fn all(self) -> UiRect { UiRect::all(self) } @@ -307,7 +310,7 @@ impl Val { /// assert_eq!(ui_rect.top, Val::ZERO); /// assert_eq!(ui_rect.bottom, Val::ZERO); /// ``` - pub const fn horizontal(self) -> UiRect { + pub fn horizontal(self) -> UiRect { UiRect::horizontal(self) } @@ -326,7 +329,7 @@ impl Val { /// assert_eq!(ui_rect.top, Val::Px(1.)); /// assert_eq!(ui_rect.bottom, Val::Px(1.)); /// ``` - pub const fn vertical(self) -> UiRect { + pub fn vertical(self) -> UiRect { UiRect::vertical(self) } @@ -342,7 +345,7 @@ impl Val { /// Err(ValArithmeticError::IncompatibleUnits) /// ); /// ``` - pub const fn try_add(self, val: Val) -> Result { + pub fn try_add(self, val: Val) -> Result { match (self, val) { (Val::Px(u), Val::Px(v)) => Ok(Val::Px(u + v)), (Val::Percent(u), Val::Percent(v)) => Ok(Val::Percent(u + v)), @@ -368,7 +371,7 @@ impl Val { /// Err(ValArithmeticError::IncompatibleUnits) /// ); /// ``` - pub const fn try_sub(self, val: Val) -> Result { + pub fn try_sub(self, val: Val) -> Result { match (self, val) { (Val::Px(u), Val::Px(v)) => Ok(Val::Px(u - v)), (Val::Percent(u), Val::Percent(v)) => Ok(Val::Percent(u - v)), @@ -389,6 +392,22 @@ impl Default for Val { } } +impl Add for Val { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + Calc::Add(self, rhs).into() + } +} + +impl Sub for Val { + type Output = Self; + + fn sub(self, rhs: Self) -> Self { + Calc::Sub(self, rhs).into() + } +} + impl Mul for Val { type Output = Val; @@ -403,6 +422,7 @@ impl Mul for Val { Val::VMax(value) => Val::VMax(value * rhs), Val::Em(value) => Val::Em(value * rhs), Val::Rem(value) => Val::Rem(value * rhs), + Val::Calc(op) => Calc::Scale(Val::Calc(op), rhs).into(), } } } @@ -419,6 +439,7 @@ impl MulAssign for Val { | Val::VMax(value) | Val::Em(value) | Val::Rem(value) => *value *= rhs, + Val::Calc(_) => *self = core::mem::take(self) * rhs, } } } @@ -437,6 +458,7 @@ impl Div for Val { Val::VMax(value) => Val::VMax(value / rhs), Val::Em(value) => Val::Em(value / rhs), Val::Rem(value) => Val::Rem(value / rhs), + Val::Calc(op) => Calc::Scale(Val::Calc(op), rhs.recip()).into(), } } } @@ -453,6 +475,7 @@ impl DivAssign for Val { | Val::VMax(value) | Val::Em(value) | Val::Rem(value) => *value /= rhs, + Val::Calc(_) => *self = core::mem::take(self) / rhs, } } } @@ -471,6 +494,7 @@ impl Neg for Val { Val::VMax(value) => Val::VMax(-value), Val::Em(value) => Val::Em(-value), Val::Rem(value) => Val::Rem(-value), + Val::Calc(op) => Calc::Scale(Val::Calc(op), -1.0).into(), } } } @@ -488,8 +512,8 @@ impl Val { /// `physical_target_size`, `em_size`, and `rem_size` context values. /// /// Returns a [`ValArithmeticError::NonEvaluable`] if the [`Val`] is impossible to resolve into a concrete value. - pub const fn resolve( - self, + pub fn resolve( + &self, scale_factor: f32, physical_base_value: f32, physical_target_size: Vec2, @@ -510,18 +534,27 @@ impl Val { Val::Em(value) => Ok(value * em_size.0 * scale_factor), Val::Rem(value) => Ok(value * rem_size.0 * scale_factor), Val::Auto => Err(ValArithmeticError::NonEvaluable), + Val::Calc(op) => op.0.resolve( + scale_factor, + physical_base_value, + physical_target_size, + em_size, + rem_size, + ), } } } -impl From for FontSize { +impl TryFrom for FontSize { + type Error = ValArithmeticError; + /// Converts a [`Val`] into a [`FontSize`]. /// /// [`Val::Em`] becomes [`FontSize::Rem`]: em is relative to the font size of the node /// itself, which is the very thing being defined here, so it resolves against the root /// size instead to avoid the circularity. - fn from(val: Val) -> Self { - match val { + fn try_from(val: Val) -> Result { + Ok(match val { Val::Auto => FontSize::Rem(1.), Val::Px(px) => FontSize::Px(px), Val::Percent(percent) => FontSize::Rem(percent / 100.), @@ -531,7 +564,8 @@ impl From for FontSize { Val::VMax(vmax) => FontSize::VMax(vmax), Val::Rem(rem) => FontSize::Rem(rem), Val::Em(em) => FontSize::Rem(em), // see comment above - } + Val::Calc(_) => return Err(ValArithmeticError::NonEvaluable), + }) } } @@ -686,7 +720,7 @@ pub fn vmax(value: T) -> Val { /// bottom: Val::Px(40.0), /// }; /// ``` -#[derive(Copy, Clone, PartialEq, Debug, Reflect)] +#[derive(Clone, PartialEq, Debug, Reflect)] #[reflect(Default, PartialEq, Debug, Clone)] #[cfg_attr( feature = "serialize", @@ -705,9 +739,9 @@ pub struct UiRect { } impl UiRect { - pub const DEFAULT: Self = Self::all(Val::ZERO); - pub const ZERO: Self = Self::all(Val::ZERO); - pub const AUTO: Self = Self::all(Val::Auto); + pub const DEFAULT: Self = Self::ZERO; + pub const ZERO: Self = Self::new(Val::ZERO, Val::ZERO, Val::ZERO, Val::ZERO); + pub const AUTO: Self = Self::new(Val::Auto, Val::Auto, Val::Auto, Val::Auto); /// Creates a new [`UiRect`] from the values specified. /// @@ -751,11 +785,11 @@ impl UiRect { /// assert_eq!(ui_rect.top, Val::Px(10.0)); /// assert_eq!(ui_rect.bottom, Val::Px(10.0)); /// ``` - pub const fn all(value: Val) -> Self { + pub fn all(value: Val) -> Self { UiRect { - left: value, - right: value, - top: value, + left: value.clone(), + right: value.clone(), + top: value.clone(), bottom: value, } } @@ -823,9 +857,9 @@ impl UiRect { /// assert_eq!(ui_rect.top, Val::ZERO); /// assert_eq!(ui_rect.bottom, Val::ZERO); /// ``` - pub const fn horizontal(value: Val) -> Self { + pub fn horizontal(value: Val) -> Self { Self { - left: value, + left: value.clone(), right: value, ..Self::DEFAULT } @@ -846,9 +880,9 @@ impl UiRect { /// assert_eq!(ui_rect.top, Val::Px(10.0)); /// assert_eq!(ui_rect.bottom, Val::Px(10.0)); /// ``` - pub const fn vertical(value: Val) -> Self { + pub fn vertical(value: Val) -> Self { Self { - top: value, + top: value.clone(), bottom: value, ..Self::DEFAULT } @@ -868,11 +902,11 @@ impl UiRect { /// assert_eq!(ui_rect.top, Val::Percent(15.0)); /// assert_eq!(ui_rect.bottom, Val::Percent(15.0)); /// ``` - pub const fn axes(horizontal: Val, vertical: Val) -> Self { + pub fn axes(horizontal: Val, vertical: Val) -> Self { Self { - left: horizontal, + left: horizontal.clone(), right: horizontal, - top: vertical, + top: vertical.clone(), bottom: vertical, } } @@ -892,7 +926,7 @@ impl UiRect { /// assert_eq!(ui_rect.top, Val::ZERO); /// assert_eq!(ui_rect.bottom, Val::ZERO); /// ``` - pub const fn left(left: Val) -> Self { + pub fn left(left: Val) -> Self { Self { left, ..Self::DEFAULT @@ -914,7 +948,7 @@ impl UiRect { /// assert_eq!(ui_rect.top, Val::ZERO); /// assert_eq!(ui_rect.bottom, Val::ZERO); /// ``` - pub const fn right(right: Val) -> Self { + pub fn right(right: Val) -> Self { Self { right, ..Self::DEFAULT @@ -936,7 +970,7 @@ impl UiRect { /// assert_eq!(ui_rect.top, Val::Px(10.0)); /// assert_eq!(ui_rect.bottom, Val::ZERO); /// ``` - pub const fn top(top: Val) -> Self { + pub fn top(top: Val) -> Self { Self { top, ..Self::DEFAULT @@ -958,7 +992,7 @@ impl UiRect { /// assert_eq!(ui_rect.top, Val::ZERO); /// assert_eq!(ui_rect.bottom, Val::Px(10.0)); /// ``` - pub const fn bottom(bottom: Val) -> Self { + pub fn bottom(bottom: Val) -> Self { Self { bottom, ..Self::DEFAULT @@ -979,7 +1013,7 @@ impl UiRect { /// assert_eq!(ui_rect.bottom, Val::Px(20.0)); /// ``` #[inline] - pub const fn with_left(mut self, left: Val) -> Self { + pub fn with_left(mut self, left: Val) -> Self { self.left = left; self } @@ -998,7 +1032,7 @@ impl UiRect { /// assert_eq!(ui_rect.bottom, Val::Px(20.0)); /// ``` #[inline] - pub const fn with_right(mut self, right: Val) -> Self { + pub fn with_right(mut self, right: Val) -> Self { self.right = right; self } @@ -1017,7 +1051,7 @@ impl UiRect { /// assert_eq!(ui_rect.bottom, Val::Px(20.0)); /// ``` #[inline] - pub const fn with_top(mut self, top: Val) -> Self { + pub fn with_top(mut self, top: Val) -> Self { self.top = top; self } @@ -1036,7 +1070,7 @@ impl UiRect { /// assert_eq!(ui_rect.bottom, Val::Px(10.0)); /// ``` #[inline] - pub const fn with_bottom(mut self, bottom: Val) -> Self { + pub fn with_bottom(mut self, bottom: Val) -> Self { self.bottom = bottom; self } @@ -1054,7 +1088,7 @@ impl From for UiRect { } } -#[derive(Debug, Clone, Copy, PartialEq, Reflect)] +#[derive(Debug, Clone, PartialEq, Reflect)] #[reflect(Default, Debug, PartialEq)] #[cfg_attr( feature = "serialize", @@ -1120,83 +1154,83 @@ impl UiPosition { } /// Creates a position from self with the given `x` and `y` coordinates - pub const fn at(self, x: Val, y: Val) -> Self { + pub fn at(self, x: Val, y: Val) -> Self { Self { x, y, ..self } } /// Creates a position from self with the given `x` coordinate - pub const fn at_x(self, x: Val) -> Self { + pub fn at_x(self, x: Val) -> Self { Self { x, ..self } } /// Creates a position from self with the given `y` coordinate - pub const fn at_y(self, y: Val) -> Self { + pub fn at_y(self, y: Val) -> Self { Self { y, ..self } } /// Creates a position in logical pixels from self with the given `x` and `y` coordinates - pub const fn at_px(self, x: f32, y: f32) -> Self { + pub fn at_px(self, x: f32, y: f32) -> Self { self.at(Val::Px(x), Val::Px(y)) } /// Creates a percentage position from self with the given `x` and `y` coordinates - pub const fn at_percent(self, x: f32, y: f32) -> Self { + pub fn at_percent(self, x: f32, y: f32) -> Self { self.at(Val::Percent(x), Val::Percent(y)) } /// Creates a position from self with the given `anchor` point - pub const fn with_anchor(self, anchor: Vec2) -> Self { + pub fn with_anchor(self, anchor: Vec2) -> Self { Self { anchor, ..self } } /// Position relative to the top-left corner - pub const fn top_left(x: Val, y: Val) -> Self { + pub fn top_left(x: Val, y: Val) -> Self { Self::TOP_LEFT.at(x, y) } /// Position relative to the left edge - pub const fn left(x: Val, y: Val) -> Self { + pub fn left(x: Val, y: Val) -> Self { Self::LEFT.at(x, y) } /// Position relative to the bottom-left corner - pub const fn bottom_left(x: Val, y: Val) -> Self { + pub fn bottom_left(x: Val, y: Val) -> Self { Self::BOTTOM_LEFT.at(x, y) } /// Position relative to the top edge - pub const fn top(x: Val, y: Val) -> Self { + pub fn top(x: Val, y: Val) -> Self { Self::TOP.at(x, y) } /// Position relative to the center - pub const fn center(x: Val, y: Val) -> Self { + pub fn center(x: Val, y: Val) -> Self { Self::CENTER.at(x, y) } /// Position relative to the bottom edge - pub const fn bottom(x: Val, y: Val) -> Self { + pub fn bottom(x: Val, y: Val) -> Self { Self::BOTTOM.at(x, y) } /// Position relative to the top-right corner - pub const fn top_right(x: Val, y: Val) -> Self { + pub fn top_right(x: Val, y: Val) -> Self { Self::TOP_RIGHT.at(x, y) } /// Position relative to the right edge - pub const fn right(x: Val, y: Val) -> Self { + pub fn right(x: Val, y: Val) -> Self { Self::RIGHT.at(x, y) } /// Position relative to the bottom-right corner - pub const fn bottom_right(x: Val, y: Val) -> Self { + pub fn bottom_right(x: Val, y: Val) -> Self { Self::BOTTOM_RIGHT.at(x, y) } /// Resolves the `Position` into physical coordinates. pub fn resolve( - self, + &self, scale_factor: f32, physical_size: Vec2, physical_target_size: Vec2, @@ -1258,7 +1292,7 @@ impl From<(Val, Val)> for UiPosition { /// let rem_size = RemSize(20.0); /// /// let c1 = CornerRadius { -/// x: radius, +/// x: radius.clone(), /// y: Val::ZERO, /// }; /// let c2 = CornerRadius { @@ -1275,7 +1309,7 @@ impl From<(Val, Val)> for UiPosition { /// Vec2::new(10.0, 0.0), /// ); /// ``` -#[derive(Debug, Clone, Copy, Reflect)] +#[derive(Debug, Clone, Reflect)] #[reflect(Default, PartialEq, Debug, Clone)] #[cfg_attr( feature = "serialize", @@ -1322,7 +1356,7 @@ impl CornerRadius { /// let em_size = EmSize(20.0); /// let rem_size = RemSize(20.0); /// - /// let c1 = CornerRadius::circular(radius); + /// let c1 = CornerRadius::circular(radius.clone()); /// let c2 = CornerRadius { /// x: Val::Auto, /// y: radius, @@ -1362,9 +1396,9 @@ impl CornerRadius { /// let em_size = EmSize(20.0); /// let rem_size = RemSize(20.0); /// - /// let c1 = CornerRadius::all(radius); + /// let c1 = CornerRadius::all(radius.clone()); /// let c2 = CornerRadius { - /// x: radius, + /// x: radius.clone(), /// y: radius, /// }; /// @@ -1379,9 +1413,9 @@ impl CornerRadius { /// ); /// ``` #[inline] - pub const fn all(radius: Val) -> Self { + pub fn all(radius: Val) -> Self { Self { - x: radius, + x: radius.clone(), y: radius, } } @@ -1394,7 +1428,7 @@ impl CornerRadius { /// Resolves this corner radius into horizontal and vertical radii in physical pixels. pub fn resolve( - self, + &self, scale_factor: f32, size: Vec2, viewport_size: Vec2, @@ -1784,7 +1818,7 @@ mod tests { fn test_uirect_axes() { let x = Val::Px(1.); let y = Val::Vw(4.); - let r = UiRect::axes(x, y); + let r = UiRect::axes(x.clone(), y.clone()); let h = UiRect::horizontal(x); let v = UiRect::vertical(y); diff --git a/crates/bevy_ui/src/gradients.rs b/crates/bevy_ui/src/gradients.rs index ba4c504cdec7f..9384cf91a0873 100644 --- a/crates/bevy_ui/src/gradients.rs +++ b/crates/bevy_ui/src/gradients.rs @@ -8,7 +8,7 @@ use bevy_utils::default; use core::{f32, f32::consts::TAU}; /// A color stop for a gradient -#[derive(Debug, Copy, Clone, PartialEq, Reflect)] +#[derive(Debug, Clone, PartialEq, Reflect)] #[reflect(Default, PartialEq, Debug)] #[cfg_attr( feature = "serialize", @@ -444,7 +444,7 @@ impl ConicGradient { } /// Sets the position of the gradient - pub const fn with_position(mut self, position: UiPosition) -> Self { + pub fn with_position(mut self, position: UiPosition) -> Self { self.position = position; self } @@ -565,7 +565,7 @@ impl> From for BorderGradient { } } -#[derive(Default, Copy, Clone, PartialEq, Debug, Reflect)] +#[derive(Default, Clone, PartialEq, Debug, Reflect)] #[reflect(PartialEq, Default)] #[cfg_attr( feature = "serialize", @@ -607,7 +607,7 @@ const fn far_side2(p: Vec2, h: Vec2) -> f32 { impl RadialGradientShape { /// Resolve the physical dimensions of the end shape of the radial gradient pub fn resolve( - self, + &self, position: Vec2, scale_factor: f32, physical_size: Vec2, diff --git a/crates/bevy_ui/src/layout/convert.rs b/crates/bevy_ui/src/layout/convert.rs index b79dfae85a591..0d95bf674d5de 100644 --- a/crates/bevy_ui/src/layout/convert.rs +++ b/crates/bevy_ui/src/layout/convert.rs @@ -1,3 +1,4 @@ +use bevy_platform::sync::Arc; use taffy::style_helpers; use crate::{ @@ -7,15 +8,41 @@ use crate::{ RepeatedGridTrack, UiRect, Val, }; -use super::LayoutContext; +use super::{ + layout_tree::{LayoutCalc, TaffyStyle}, + LayoutContext, +}; impl Val { pub(super) fn into_length_percentage_auto( - self, + &self, context: &LayoutContext, + calcs: &mut Vec>, ) -> taffy::style::LengthPercentageAuto { match self { Val::Auto => style_helpers::auto(), + Val::Calc(calc) => { + if !calc.0.uses_percent() { + return style_helpers::length( + self.resolve( + context.scale_factor, + 0.0, + context.physical_size, + bevy_text::EmSize(context.em_size), + bevy_text::RemSize(context.rem_size), + ) + .unwrap_or(0.0), + ); + } + let calc = Arc::new(LayoutCalc { + calc: calc.clone(), + context: *context, + }); + let value = taffy::style::LengthPercentageAuto::calc(Arc::as_ptr(&calc).cast()); + calcs.push(calc); + value + } + Val::Percent(value) => style_helpers::percent(value / 100.), Val::Px(value) => style_helpers::length(context.scale_factor * value), Val::VMin(value) => { @@ -34,11 +61,34 @@ impl Val { } pub(super) fn into_length_percentage( - self, + &self, context: &LayoutContext, + calcs: &mut Vec>, ) -> taffy::style::LengthPercentage { match self { Val::Auto => style_helpers::length(0.0_f32), + Val::Calc(calc) => { + if !calc.0.uses_percent() { + return style_helpers::length( + self.resolve( + context.scale_factor, + 0.0, + context.physical_size, + bevy_text::EmSize(context.em_size), + bevy_text::RemSize(context.rem_size), + ) + .unwrap_or(0.0), + ); + } + let calc = Arc::new(LayoutCalc { + calc: calc.clone(), + context: *context, + }); + let value = taffy::style::LengthPercentage::calc(Arc::as_ptr(&calc).cast()); + calcs.push(calc); + value + } + Val::Percent(value) => style_helpers::percent(value / 100.), Val::Px(value) => style_helpers::length(context.scale_factor * value), Val::VMin(value) => { @@ -64,10 +114,11 @@ impl Val { // * A non-zero border smaller than one physical pixel is rounded up to one pixel. // * A border larger than one physical pixel is rounded down to a whole pixel. fn into_length_percentage_for_border( - self, + &self, context: &LayoutContext, + calcs: &mut Vec>, ) -> taffy::style::LengthPercentage { - let length = self.into_length_percentage(context); + let length = self.into_length_percentage(context, calcs); let raw = length.into_raw(); // we're only snapping if sized in non-zero px if raw.tag() == taffy::style::CompactLength::LENGTH_TAG && raw.value() > 0.0 { @@ -77,21 +128,25 @@ impl Val { } } - fn into_dimension(self, context: &LayoutContext) -> taffy::style::Dimension { - self.into_length_percentage_auto(context).into() + fn into_dimension( + &self, + context: &LayoutContext, + calcs: &mut Vec>, + ) -> taffy::style::Dimension { + self.into_length_percentage_auto(context, calcs).into() } } impl UiRect { pub(super) fn map_to_taffy_rect( - self, - map_fn: impl Fn(Val) -> T, + &self, + mut map_fn: impl FnMut(&Val) -> T, ) -> taffy::geometry::Rect { taffy::geometry::Rect { - left: map_fn(self.left), - right: map_fn(self.right), - top: map_fn(self.top), - bottom: map_fn(self.bottom), + left: map_fn(&self.left), + right: map_fn(&self.right), + top: map_fn(&self.top), + bottom: map_fn(&self.bottom), } } } @@ -100,8 +155,10 @@ impl UiRect { pub fn update_taffy_style_from_node( node: &Node, context: &LayoutContext, - style: &mut taffy::Style, + taffy_style: &mut TaffyStyle, ) { + let TaffyStyle(style, calcs) = taffy_style; + calcs.clear(); style.display = node.display.into(); style.box_sizing = node.box_sizing.into(); style.item_is_table = false; @@ -122,40 +179,40 @@ pub fn update_taffy_style_from_node( style.justify_content = node.justify_content.into(); style.direction = node.direction.into(); style.inset = taffy::Rect { - left: node.left.into_length_percentage_auto(context), - right: node.right.into_length_percentage_auto(context), - top: node.top.into_length_percentage_auto(context), - bottom: node.bottom.into_length_percentage_auto(context), + left: node.left.into_length_percentage_auto(context, calcs), + right: node.right.into_length_percentage_auto(context, calcs), + top: node.top.into_length_percentage_auto(context, calcs), + bottom: node.bottom.into_length_percentage_auto(context, calcs), }; style.margin = node .margin - .map_to_taffy_rect(|m| m.into_length_percentage_auto(context)); + .map_to_taffy_rect(|m| m.into_length_percentage_auto(context, calcs)); style.padding = node .padding - .map_to_taffy_rect(|m| m.into_length_percentage(context)); + .map_to_taffy_rect(|m| m.into_length_percentage(context, calcs)); // border floors >=1px sized to nearest (>=1) real pixel size, see `into_length_percentage_for_border`. style.border = node .border - .map_to_taffy_rect(|m| m.into_length_percentage_for_border(context)); + .map_to_taffy_rect(|m| m.into_length_percentage_for_border(context, calcs)); style.flex_grow = node.flex_grow; style.flex_shrink = node.flex_shrink; - style.flex_basis = node.flex_basis.into_dimension(context); + style.flex_basis = node.flex_basis.into_dimension(context, calcs); style.size = taffy::Size { - width: node.width.into_dimension(context), - height: node.height.into_dimension(context), + width: node.width.into_dimension(context, calcs), + height: node.height.into_dimension(context, calcs), }; style.min_size = taffy::Size { - width: node.min_width.into_length_percentage_auto(context), - height: node.min_height.into_length_percentage_auto(context), + width: node.min_width.into_length_percentage_auto(context, calcs), + height: node.min_height.into_length_percentage_auto(context, calcs), }; style.max_size = taffy::Size { - width: node.max_width.into_length_percentage_auto(context), - height: node.max_height.into_length_percentage_auto(context), + width: node.max_width.into_length_percentage_auto(context, calcs), + height: node.max_height.into_length_percentage_auto(context, calcs), }; style.aspect_ratio = node.aspect_ratio; style.gap = taffy::Size { - width: node.column_gap.into_length_percentage(context), - height: node.row_gap.into_length_percentage(context), + width: node.column_gap.into_length_percentage(context, calcs), + height: node.row_gap.into_length_percentage(context, calcs), }; style.grid_auto_flow = node.grid_auto_flow.into(); style.grid_template_rows.clear(); @@ -407,14 +464,18 @@ impl From for taffy::geometry::Line taffy::style::MinTrackSizingFunction { match self { - MinTrackSizingFunction::Px(val) => Val::Px(val).into_length_percentage(context).into(), - MinTrackSizingFunction::Percent(val) => { - Val::Percent(val).into_length_percentage(context).into() - } - MinTrackSizingFunction::Em(val) => Val::Em(val).into_length_percentage(context).into(), - MinTrackSizingFunction::Rem(val) => { - Val::Rem(val).into_length_percentage(context).into() - } + MinTrackSizingFunction::Px(val) => Val::Px(val) + .into_length_percentage(context, &mut Vec::new()) + .into(), + MinTrackSizingFunction::Percent(val) => Val::Percent(val) + .into_length_percentage(context, &mut Vec::new()) + .into(), + MinTrackSizingFunction::Em(val) => Val::Em(val) + .into_length_percentage(context, &mut Vec::new()) + .into(), + MinTrackSizingFunction::Rem(val) => Val::Rem(val) + .into_length_percentage(context, &mut Vec::new()) + .into(), MinTrackSizingFunction::Auto => taffy::style::MinTrackSizingFunction::auto(), MinTrackSizingFunction::MinContent => { taffy::style::MinTrackSizingFunction::min_content() @@ -422,14 +483,18 @@ impl MinTrackSizingFunction { MinTrackSizingFunction::MaxContent => { taffy::style::MinTrackSizingFunction::max_content() } - MinTrackSizingFunction::VMin(val) => { - Val::VMin(val).into_length_percentage(context).into() - } - MinTrackSizingFunction::VMax(val) => { - Val::VMax(val).into_length_percentage(context).into() - } - MinTrackSizingFunction::Vh(val) => Val::Vh(val).into_length_percentage(context).into(), - MinTrackSizingFunction::Vw(val) => Val::Vw(val).into_length_percentage(context).into(), + MinTrackSizingFunction::VMin(val) => Val::VMin(val) + .into_length_percentage(context, &mut Vec::new()) + .into(), + MinTrackSizingFunction::VMax(val) => Val::VMax(val) + .into_length_percentage(context, &mut Vec::new()) + .into(), + MinTrackSizingFunction::Vh(val) => Val::Vh(val) + .into_length_percentage(context, &mut Vec::new()) + .into(), + MinTrackSizingFunction::Vw(val) => Val::Vw(val) + .into_length_percentage(context, &mut Vec::new()) + .into(), } } } @@ -437,14 +502,18 @@ impl MinTrackSizingFunction { impl MaxTrackSizingFunction { fn into_taffy(self, context: &LayoutContext) -> taffy::style::MaxTrackSizingFunction { match self { - MaxTrackSizingFunction::Px(val) => Val::Px(val).into_length_percentage(context).into(), - MaxTrackSizingFunction::Percent(val) => { - Val::Percent(val).into_length_percentage(context).into() - } - MaxTrackSizingFunction::Em(val) => Val::Em(val).into_length_percentage(context).into(), - MaxTrackSizingFunction::Rem(val) => { - Val::Rem(val).into_length_percentage(context).into() - } + MaxTrackSizingFunction::Px(val) => Val::Px(val) + .into_length_percentage(context, &mut Vec::new()) + .into(), + MaxTrackSizingFunction::Percent(val) => Val::Percent(val) + .into_length_percentage(context, &mut Vec::new()) + .into(), + MaxTrackSizingFunction::Em(val) => Val::Em(val) + .into_length_percentage(context, &mut Vec::new()) + .into(), + MaxTrackSizingFunction::Rem(val) => Val::Rem(val) + .into_length_percentage(context, &mut Vec::new()) + .into(), MaxTrackSizingFunction::Auto => taffy::style::MaxTrackSizingFunction::auto(), MaxTrackSizingFunction::MinContent => { taffy::style::MaxTrackSizingFunction::min_content() @@ -455,7 +524,7 @@ impl MaxTrackSizingFunction { MaxTrackSizingFunction::FitContentPx(val) => { taffy::style::MaxTrackSizingFunction::fit_content_px( Val::Px(val) - .into_length_percentage(context) + .into_length_percentage(context, &mut Vec::new()) .into_raw() .value(), ) @@ -463,7 +532,7 @@ impl MaxTrackSizingFunction { MaxTrackSizingFunction::FitContentPercent(val) => { taffy::style::MaxTrackSizingFunction::fit_content_percent( Val::Percent(val) - .into_length_percentage(context) + .into_length_percentage(context, &mut Vec::new()) .into_raw() .value(), ) @@ -471,14 +540,18 @@ impl MaxTrackSizingFunction { MaxTrackSizingFunction::Fraction(fraction) => { taffy::style::MaxTrackSizingFunction::fr(fraction) } - MaxTrackSizingFunction::VMin(val) => { - Val::VMin(val).into_length_percentage(context).into() - } - MaxTrackSizingFunction::VMax(val) => { - Val::VMax(val).into_length_percentage(context).into() - } - MaxTrackSizingFunction::Vh(val) => Val::Vh(val).into_length_percentage(context).into(), - MaxTrackSizingFunction::Vw(val) => Val::Vw(val).into_length_percentage(context).into(), + MaxTrackSizingFunction::VMin(val) => Val::VMin(val) + .into_length_percentage(context, &mut Vec::new()) + .into(), + MaxTrackSizingFunction::VMax(val) => Val::VMax(val) + .into_length_percentage(context, &mut Vec::new()) + .into(), + MaxTrackSizingFunction::Vh(val) => Val::Vh(val) + .into_length_percentage(context, &mut Vec::new()) + .into(), + MaxTrackSizingFunction::Vw(val) => Val::Vw(val) + .into_length_percentage(context, &mut Vec::new()) + .into(), } } } @@ -619,7 +692,7 @@ mod tests { EmSize(DEFAULT_REM_SIZE_PX), RemSize(DEFAULT_REM_SIZE_PX), ); - let mut taffy_style = taffy::Style::default(); + let mut taffy_style = TaffyStyle::default(); update_taffy_style_from_node(&node, &viewport_values, &mut taffy_style); assert_eq!(taffy_style.display, taffy::style::Display::Flex); assert_eq!(taffy_style.box_sizing, taffy::style::BoxSizing::ContentBox); @@ -801,7 +874,10 @@ mod tests { ]; for (val, length) in cases { assert!({ - let lhs = val.into_length_percentage(&context).into_raw().value(); + let lhs = val + .into_length_percentage(&context, &mut Vec::new()) + .into_raw() + .value(); let rhs = length.into_raw().value(); (lhs - rhs).abs() < 0.0001 }); diff --git a/crates/bevy_ui/src/layout/layout_tree.rs b/crates/bevy_ui/src/layout/layout_tree.rs index 33c2b16c0ecb7..b18dbd7f8da09 100644 --- a/crates/bevy_ui/src/layout/layout_tree.rs +++ b/crates/bevy_ui/src/layout/layout_tree.rs @@ -9,7 +9,8 @@ use bevy_ecs::{ world::Ref, }; use bevy_math::{UVec2, Vec2}; -use bevy_text::{ComputedTextBlock, FontCx}; +use bevy_platform::{collections::HashMap, sync::Arc}; +use bevy_text::{ComputedTextBlock, EmSize, FontCx, RemSize}; use bevy_utils::default; use taffy::{ compute_block_layout, compute_cached_layout, compute_flexbox_layout, compute_grid_layout, @@ -22,84 +23,109 @@ use taffy::{ }; use crate::{ - layout::UiTreeDirty, ContentSize, FixedNode, GhostNode, IgnoreScroll, LayoutConfig, - LayoutError, Measure, MeasureArgs, Node, NodeMeasure, Outline, OverrideClip, ScrollPosition, - UiTransform, + calc::Calc, + layout::{LayoutContext, UiTreeDirty}, + ContentSize, FixedNode, GhostNode, IgnoreScroll, LayoutConfig, LayoutError, Measure, + MeasureArgs, Node, NodeMeasure, Outline, OverrideClip, ScrollPosition, UiTransform, }; #[expect( unsafe_code, - reason = "taffy::Style is only thread-unsafe with the calc feature" + reason = "calc pointers are opaque lookup keys and are never dereferenced" )] -// SAFETY: taffy::Style is only thread-unsafe with the calc feature +// SAFETY: Calc pointers are only used as lookup keys, never dereferenced. The owned expressions are Send + Sync. unsafe impl Send for TaffyStyle {} #[expect( unsafe_code, - reason = "taffy::Style is only thread-unsafe with the calc feature" + reason = "calc pointers are opaque lookup keys and are never dereferenced" )] -// SAFETY: taffy::Style is only thread-unsafe with the calc feature +// SAFETY: Calc pointers are only used as lookup keys, never dereferenced. The owned expressions are Send + Sync. unsafe impl Sync for TaffyStyle {} #[derive(Default, Component, Clone, Deref, DerefMut)] -pub struct TaffyStyle(pub Style); - -pub static VIEWPORT_NODE_TAFFY_STYLE: TaffyStyle = TaffyStyle(Style { - dummy: core::marker::PhantomData, - display: Display::Grid, - item_is_table: false, - item_is_replaced: false, - item_is_compressible_replaced: false, - box_sizing: taffy::BoxSizing::BorderBox, - direction: taffy::Direction::Ltr, - overflow: taffy::Point { - x: taffy::Overflow::Visible, - y: taffy::Overflow::Visible, - }, - scrollbar_width: 0.0, - contain: taffy::Contain::NONE, - position: taffy::Position::Relative, - inset: taffy::Rect::auto(), - size: taffy::Size { - width: taffy::Dimension::percent(1.0), - height: taffy::Dimension::percent(1.0), - }, - min_size: taffy::Size::auto(), - max_size: taffy::Size::auto(), - aspect_ratio: None, - margin: taffy::Rect::zero(), - padding: taffy::Rect::zero(), - border: taffy::Rect::zero(), - align_items: taffy::AlignItems::START, - align_self: None, - justify_items: taffy::JustifyItems::START, - justify_self: None, - align_content: taffy::AlignContent::NORMAL, - justify_content: taffy::AlignContent::NORMAL, - gap: taffy::Size::zero(), - text_align: taffy::TextAlign::Auto, - flex_direction: taffy::FlexDirection::Row, - flex_wrap: taffy::FlexWrap::NoWrap, - flex_basis: taffy::Dimension::auto(), - flex_grow: 0.0, - flex_shrink: 1.0, - grid_template_rows: Vec::new(), - grid_template_columns: Vec::new(), - grid_auto_rows: Vec::new(), - grid_auto_columns: Vec::new(), - grid_auto_flow: taffy::GridAutoFlow::Row, - grid_template_areas: None, - grid_template_column_names: Vec::new(), - grid_template_row_names: Vec::new(), - grid_row: taffy::Line { - start: taffy::GridPlacement::Span(1), - end: taffy::GridPlacement::Auto, - }, - grid_column: taffy::Line { - start: taffy::GridPlacement::Span(1), - end: taffy::GridPlacement::Auto, +pub struct TaffyStyle(#[deref] pub Style, pub(crate) Vec>); + +#[repr(align(8))] +pub(crate) struct LayoutCalc { + pub calc: Calc, + pub context: LayoutContext, +} + +impl LayoutCalc { + pub fn resolve(&self, basis: f32) -> f32 { + self.calc + .0 + .resolve( + self.context.scale_factor, + basis, + self.context.physical_size, + EmSize(self.context.em_size), + RemSize(self.context.rem_size), + ) + .unwrap_or(0.0) + } +} + +pub static VIEWPORT_NODE_TAFFY_STYLE: TaffyStyle = TaffyStyle( + Style { + dummy: core::marker::PhantomData, + display: Display::Grid, + item_is_table: false, + item_is_replaced: false, + item_is_compressible_replaced: false, + box_sizing: taffy::BoxSizing::BorderBox, + direction: taffy::Direction::Ltr, + overflow: taffy::Point { + x: taffy::Overflow::Visible, + y: taffy::Overflow::Visible, + }, + scrollbar_width: 0.0, + contain: taffy::Contain::NONE, + position: taffy::Position::Relative, + inset: taffy::Rect::auto(), + size: taffy::Size { + width: taffy::Dimension::percent(1.0), + height: taffy::Dimension::percent(1.0), + }, + min_size: taffy::Size::auto(), + max_size: taffy::Size::auto(), + aspect_ratio: None, + margin: taffy::Rect::zero(), + padding: taffy::Rect::zero(), + border: taffy::Rect::zero(), + align_items: taffy::AlignItems::START, + align_self: None, + justify_items: taffy::JustifyItems::START, + justify_self: None, + align_content: taffy::AlignContent::NORMAL, + justify_content: taffy::AlignContent::NORMAL, + gap: taffy::Size::zero(), + text_align: taffy::TextAlign::Auto, + flex_direction: taffy::FlexDirection::Row, + flex_wrap: taffy::FlexWrap::NoWrap, + flex_basis: taffy::Dimension::auto(), + flex_grow: 0.0, + flex_shrink: 1.0, + grid_template_rows: Vec::new(), + grid_template_columns: Vec::new(), + grid_auto_rows: Vec::new(), + grid_auto_columns: Vec::new(), + grid_auto_flow: taffy::GridAutoFlow::Row, + grid_template_areas: None, + grid_template_column_names: Vec::new(), + grid_template_row_names: Vec::new(), + grid_row: taffy::Line { + start: taffy::GridPlacement::Span(1), + end: taffy::GridPlacement::Auto, + }, + grid_column: taffy::Line { + start: taffy::GridPlacement::Span(1), + end: taffy::GridPlacement::Auto, + }, }, -}); + Vec::new(), +); /// `NodeId`s wrap a `u64` that can be directly converted to and from an `Entity`. /// We create and assign the `NodeId`s ourselves in order that each `NodeId` corresponds to an `Entity`. @@ -359,6 +385,29 @@ pub fn compute_layout<'w, 's>( }; { + let mut calcs: HashMap = HashMap::default(); + child_stack.push(root_node_id); + while let Some(node_id) = child_stack.pop() { + let entity = entity_from_node_id(node_id).expect("Viewport node should not be a child"); + let style = style_query + .get(entity) + .expect("Missing TaffyStyle on UI node."); + calcs.extend( + style + .1 + .iter() + .map(|calc| (Arc::as_ptr(calc) as usize, calc.as_ref())), + ); + let layout = computed_layout_query + .get(entity) + .expect("Missing ComputedLayout on UI node."); + child_stack.extend_from_slice(&layout.ui_children); + } + let resolve_calc = |ptr: *const (), basis: f32| { + calcs + .get(&(ptr as usize)) + .map_or(0.0, |calc| calc.resolve(basis)) + }; let mut measure_function = |known_dimensions: taffy::Size>, available_space: taffy::Size, entity: Entity, @@ -377,6 +426,7 @@ pub fn compute_layout<'w, 's>( font_system, buffer: None, style, + resolve_calc: &resolve_calc, }; let buffer = get_text_buffer( crate::widget::TextMeasure::needs_buffer( @@ -403,6 +453,7 @@ pub fn compute_layout<'w, 's>( ..default() }, measure_function: &mut measure_function, + resolve_calc: &resolve_calc, layout_changed: false, }; @@ -603,6 +654,7 @@ struct UiLayoutTree<'a, 'w, 's> { &Style, ) -> taffy::Size, layout_changed: bool, + resolve_calc: &'a dyn Fn(*const (), f32) -> f32, } impl<'a> UiLayoutTree<'a, '_, '_> { @@ -660,7 +712,7 @@ impl<'a> UiLayoutTree<'a, '_, '_> { (_, false) => compute_leaf_layout( inputs, style, - |_, _| 0.0, + tree.resolve_calc, |known_dimensions, available_space| { (tree.measure_function)( known_dimensions, @@ -704,6 +756,10 @@ impl TraversePartialTree for UiLayoutTree<'_, '_, '_> { impl TraverseTree for UiLayoutTree<'_, '_, '_> {} impl<'tree, 'w, 's> LayoutPartialTree for UiLayoutTree<'tree, 'w, 's> { + fn resolve_calc_value(&self, val: *const (), basis: f32) -> f32 { + (self.resolve_calc)(val, basis) + } + type CoreContainerStyle<'a> = &'a Style where diff --git a/crates/bevy_ui/src/layout/tests.rs b/crates/bevy_ui/src/layout/tests.rs index e78c0723bfcb6..37e7e44a39871 100644 --- a/crates/bevy_ui/src/layout/tests.rs +++ b/crates/bevy_ui/src/layout/tests.rs @@ -91,6 +91,129 @@ fn setup_ui_test_app() -> App { app } +#[test] +fn calc_layout_resolves_percentages_and_updates_context() { + let mut app = setup_ui_test_app(); + let root = app + .world_mut() + .spawn(Node { + width: Val::Px(400.0), + height: Val::Px(100.0), + ..default() + }) + .id(); + let width = Val::Percent(50.0) - Val::Em(1.0) - Val::Rem(1.0); + let child = app + .world_mut() + .spawn(( + Node { + width: width.clone(), + height: Val::Px(10.0) + Val::Px(10.0), + ..default() + }, + EmSize(10.0), + )) + .id(); + app.world_mut().entity_mut(root).add_child(child); + app.update(); + assert_eq!( + app.world().get::(child).unwrap().size(), + Vec2::new(170.0, 20.0) + ); + + app.world_mut().get_mut::(root).unwrap().width = Val::Px(600.0); + app.world_mut().get_mut::(child).unwrap().0 = 30.0; + app.world_mut().resource_mut::().0 = 40.0; + app.update(); + assert_eq!( + app.world().get::(child).unwrap().size(), + Vec2::new(230.0, 20.0) + ); + + let clone = app.world().get::(child).unwrap().clone(); + let mut font_system = bevy_text::FontCx::default(); + let resolve_calc = |_, basis| clone.1[0].resolve(basis); + let measure_args = crate::MeasureArgs { + known_width: None, + known_height: None, + available_width: crate::AvailableSpace::Definite(600.0), + available_height: crate::AvailableSpace::Definite(100.0), + font_system: &mut font_system, + buffer: None, + style: &clone, + resolve_calc: &resolve_calc, + }; + assert_eq!(measure_args.resolve_width().preferred, Some(230.0)); + app.world_mut().get_mut::(child).unwrap().width = Val::Px(90.0); + app.update(); + assert_eq!( + app.world().get::(child).unwrap().size().x, + 90.0 + ); + assert_eq!(clone.1[0].resolve(600.0), 230.0); + app.world_mut().get_mut::(child).unwrap().width = width; + app.update(); + assert_eq!( + app.world().get::(child).unwrap().size().x, + 230.0 + ); +} + +#[test] +fn calc_layout_handles_containers_and_parent_width_based_padding() { + let mut app = setup_ui_test_app(); + let root = app + .world_mut() + .spawn(Node { + width: Val::Px(400.0), + height: Val::Px(200.0), + ..default() + }) + .id(); + let child = app + .world_mut() + .spawn(Node { + width: crate::calc::Calc::Clamp(Val::Px(100.0), Val::Percent(80.0), Val::Px(300.0)) + .into(), + height: Val::Percent(50.0) - Val::Px(10.0), + padding: UiRect::all(Val::Percent(5.0) + Val::Px(2.0)), + ..default() + }) + .id(); + let grandchild = app + .world_mut() + .spawn(Node { + width: Val::Percent(100.0) - Val::Px(6.0), + height: Val::Px(10.0), + ..default() + }) + .id(); + app.world_mut().entity_mut(root).add_child(child); + app.world_mut().entity_mut(child).add_child(grandchild); + app.update(); + assert_eq!( + app.world().get::(child).unwrap().size(), + Vec2::new(300.0, 90.0) + ); + assert_eq!( + app.world() + .get::(child) + .unwrap() + .padding() + .min_inset + .y, + 22.0 + ); + assert_eq!( + app.world() + .get::(grandchild) + .unwrap() + .size() + .x, + 250.0 + ); +} + #[test] fn ui_nodes_with_percent_100_dimensions_should_fill_their_parent() { let mut app = setup_ui_test_app(); diff --git a/crates/bevy_ui/src/measurement.rs b/crates/bevy_ui/src/measurement.rs index d2b958c292d00..56fa34f1d31a0 100644 --- a/crates/bevy_ui/src/measurement.rs +++ b/crates/bevy_ui/src/measurement.rs @@ -33,6 +33,7 @@ pub struct MeasureArgs<'a> { pub buffer: Option<&'a mut bevy_text::ComputedTextBlock>, /// Resolved style for this node in physical pixels. pub style: &'a taffy::Style, + pub resolve_calc: &'a dyn Fn(*const (), f32) -> f32, } #[derive(Copy, Clone)] @@ -54,8 +55,8 @@ fn resolve_axis( min_dim: taffy::style::LengthPercentageAuto, size_dim: taffy::style::Dimension, max_dim: taffy::style::LengthPercentageAuto, + calc: &dyn Fn(*const (), f32) -> f32, ) -> ResolvedAxis { - let calc = |_, _| 0.; let available = available_space.into_option(); let min = min_dim.maybe_resolve(available, calc); let preferred = size_dim.maybe_resolve(available, calc); @@ -77,6 +78,7 @@ impl MeasureArgs<'_> { self.style.min_size.width, self.style.size.width, self.style.max_size.width, + self.resolve_calc, ) } @@ -88,6 +90,7 @@ impl MeasureArgs<'_> { self.style.min_size.height, self.style.size.height, self.style.max_size.height, + self.resolve_calc, ) } } diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 13a7da48a3ce8..c9b7e036a4f2b 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -2665,7 +2665,7 @@ impl Default for BorderColor { } } -#[derive(Component, Copy, Clone, Debug, PartialEq, Reflect)] +#[derive(Component, Clone, Debug, PartialEq, Reflect)] #[reflect(Component, Default, Debug, PartialEq, Clone)] #[cfg_attr( feature = "serialize", @@ -2955,7 +2955,7 @@ impl> From for OuterColor { /// ``` /// /// -#[derive(Copy, Clone, Debug, PartialEq, Reflect)] +#[derive(Clone, Debug, PartialEq, Reflect)] #[reflect(PartialEq, Default, Debug, Clone)] #[cfg_attr( feature = "serialize", @@ -3011,9 +3011,9 @@ impl BorderRadius { pub fn all(radius: impl Into) -> Self { let radius = radius.into(); Self { - top_left: radius, - top_right: radius, - bottom_left: radius, + top_left: radius.clone(), + top_right: radius.clone(), + bottom_left: radius.clone(), bottom_right: radius, } } @@ -3106,7 +3106,7 @@ impl BorderRadius { pub fn left(radius: impl Into) -> Self { let radius = radius.into(); Self { - top_left: radius, + top_left: radius.clone(), bottom_left: radius, ..Self::DEFAULT } @@ -3118,7 +3118,7 @@ impl BorderRadius { pub fn right(radius: impl Into) -> Self { let radius = radius.into(); Self { - top_right: radius, + top_right: radius.clone(), bottom_right: radius, ..Self::DEFAULT } @@ -3130,7 +3130,7 @@ impl BorderRadius { pub fn top(radius: impl Into) -> Self { let radius = radius.into(); Self { - top_left: radius, + top_left: radius.clone(), top_right: radius, ..Self::DEFAULT } @@ -3142,7 +3142,7 @@ impl BorderRadius { pub fn bottom(radius: impl Into) -> Self { let radius = radius.into(); Self { - bottom_left: radius, + bottom_left: radius.clone(), bottom_right: radius, ..Self::DEFAULT } @@ -3180,7 +3180,7 @@ impl BorderRadius { #[inline] pub fn with_left(mut self, radius: impl Into) -> Self { let radius = radius.into(); - self.top_left = radius; + self.top_left = radius.clone(); self.bottom_left = radius; self } @@ -3189,7 +3189,7 @@ impl BorderRadius { #[inline] pub fn with_right(mut self, radius: impl Into) -> Self { let radius = radius.into(); - self.top_right = radius; + self.top_right = radius.clone(); self.bottom_right = radius; self } @@ -3198,7 +3198,7 @@ impl BorderRadius { #[inline] pub fn with_top(mut self, radius: impl Into) -> Self { let radius = radius.into(); - self.top_left = radius; + self.top_left = radius.clone(); self.top_right = radius; self } @@ -3207,7 +3207,7 @@ impl BorderRadius { #[inline] pub fn with_bottom(mut self, radius: impl Into) -> Self { let radius = radius.into(); - self.bottom_left = radius; + self.bottom_left = radius.clone(); self.bottom_right = radius; self } @@ -3368,7 +3368,7 @@ impl From for BoxShadow { } } -#[derive(Copy, Clone, Debug, PartialEq, Reflect)] +#[derive(Clone, Debug, PartialEq, Reflect)] #[reflect(PartialEq, Default, Clone)] #[cfg_attr( feature = "serialize", diff --git a/crates/bevy_ui/src/ui_transform.rs b/crates/bevy_ui/src/ui_transform.rs index 8c9b4264bd0c5..0c9775137e0fb 100644 --- a/crates/bevy_ui/src/ui_transform.rs +++ b/crates/bevy_ui/src/ui_transform.rs @@ -13,7 +13,7 @@ use bevy_text::{EmSize, RemSize}; use core::ops::Mul; /// A pair of [`Val`]s used to represent a 2-dimensional size or offset. -#[derive(Debug, PartialEq, Clone, Copy, Reflect)] +#[derive(Debug, PartialEq, Clone, Reflect)] #[reflect(Default, PartialEq, Debug, Clone)] #[cfg_attr( feature = "serialize", @@ -43,8 +43,8 @@ impl Val2 { } /// Creates a new [`Val2`] where both components are the same value - pub const fn all(val: Val) -> Self { - Self::new(val, val) + pub fn all(val: Val) -> Self { + Self::new(val.clone(), val) } /// Creates a new [`Val2`] where both components are in logical pixels @@ -133,7 +133,7 @@ impl From for Val2 { /// Relative 2D transform for UI nodes /// /// [`UiGlobalTransform`] is automatically inserted whenever [`UiTransform`] is inserted. -#[derive(Component, Debug, PartialEq, Clone, Copy, Reflect)] +#[derive(Component, Debug, PartialEq, Clone, Reflect)] #[reflect(Component, Default, PartialEq, Debug, Clone)] #[cfg_attr( feature = "serialize", diff --git a/crates/bevy_ui_render/src/box_shadow.rs b/crates/bevy_ui_render/src/box_shadow.rs index 9dce85e9900ab..f8c8384b5a7ff 100644 --- a/crates/bevy_ui_render/src/box_shadow.rs +++ b/crates/bevy_ui_render/src/box_shadow.rs @@ -313,8 +313,17 @@ pub fn extract_shadows( continue; } - let resolve_val = |val, base, scale_factor| match val { + let resolve_val = |val: &Val, base, scale_factor| match val { Val::Auto => 0., + Val::Calc(_) => val + .resolve( + scale_factor, + base, + ui_physical_viewport_size, + uinode.em_size, + uinode.rem_size, + ) + .unwrap_or(0.), Val::Px(px) => px * scale_factor, Val::Percent(percent) => percent / 100. * base, Val::Vw(percent) => percent / 100. * ui_physical_viewport_size.x, @@ -325,15 +334,15 @@ pub fn extract_shadows( Val::Rem(rem) => rem * uinode.rem_size.0 * scale_factor, }; - let spread_x = resolve_val(drop_shadow.spread_radius, uinode.size().x, scale_factor); + let spread_x = resolve_val(&drop_shadow.spread_radius, uinode.size().x, scale_factor); let spread_ratio = (spread_x + uinode.size().x) / uinode.size().x; let spread = vec2(spread_x, uinode.size().y * spread_ratio - uinode.size().y); - let blur_radius = resolve_val(drop_shadow.blur_radius, uinode.size().x, scale_factor); + let blur_radius = resolve_val(&drop_shadow.blur_radius, uinode.size().x, scale_factor); let offset = vec2( - resolve_val(drop_shadow.x_offset, uinode.size().x, scale_factor), - resolve_val(drop_shadow.y_offset, uinode.size().y, scale_factor), + resolve_val(&drop_shadow.x_offset, uinode.size().x, scale_factor), + resolve_val(&drop_shadow.y_offset, uinode.size().y, scale_factor), ); let shadow_size = uinode.size() + spread; diff --git a/crates/bevy_ui_widgets/src/dialog.rs b/crates/bevy_ui_widgets/src/dialog.rs index 5e9ccf8d782be..51afcae5a0775 100644 --- a/crates/bevy_ui_widgets/src/dialog.rs +++ b/crates/bevy_ui_widgets/src/dialog.rs @@ -163,7 +163,7 @@ fn dialog_drag_start( return; }; - if let Ok(translation) = q_transform.get(dialog).map(|t| t.translation) { + if let Ok(translation) = q_transform.get(dialog).map(|t| t.translation.clone()) { state.start_dialog_translation = translation; } else { warn!("Cannot get translation from dialog for dragging"); @@ -210,6 +210,7 @@ fn dialog_drag( if let Ok(mut transform) = q_transform.get_mut(dialog) && let Ok(translation) = state .start_dialog_translation + .clone() .try_add(Val2::px(clamped_offset.x, clamped_offset.y)) { transform.translation = translation; diff --git a/crates/bevy_ui_widgets/src/scrollbar.rs b/crates/bevy_ui_widgets/src/scrollbar.rs index 06fd2e2f37a5e..6152fd6411e9e 100644 --- a/crates/bevy_ui_widgets/src/scrollbar.rs +++ b/crates/bevy_ui_widgets/src/scrollbar.rs @@ -413,7 +413,7 @@ pub(crate) fn update_scrollbar_thumb( thumb_node.border_radius = border_radius; } - let resolve_border_val = |val: Val| { + let resolve_border_val = |val: &Val| { val.resolve( target_info.scale_factor(), thumb_physical_size.x, @@ -426,12 +426,12 @@ pub(crate) fn update_scrollbar_thumb( let mut resolved_border = BorderRect { min_inset: Vec2::new( - resolve_border_val(thumb.border.left), - resolve_border_val(thumb.border.top), + resolve_border_val(&thumb.border.left), + resolve_border_val(&thumb.border.top), ), max_inset: Vec2::new( - resolve_border_val(thumb.border.right), - resolve_border_val(thumb.border.bottom), + resolve_border_val(&thumb.border.right), + resolve_border_val(&thumb.border.bottom), ), }; diff --git a/examples/scene/bsn.rs b/examples/scene/bsn.rs index b5243d198b2d7..736549836a01a 100644 --- a/examples/scene/bsn.rs +++ b/examples/scene/bsn.rs @@ -69,7 +69,7 @@ fn button(label: &str, root_entity: EntityTemplate) -> impl Scene { Text(label) TextFont { font: FontSourceTemplate::Handle("fonts/FiraSans-Bold.ttf"), - font_size: px(33.0), + font_size: FontSize::Px(33.0), } TextColor(Color::srgb(0.9, 0.9, 0.9)) TextShadow diff --git a/examples/showcase/stepping.rs b/examples/showcase/stepping.rs index a59a5b6cbc217..29b47b084ae1f 100644 --- a/examples/showcase/stepping.rs +++ b/examples/showcase/stepping.rs @@ -53,8 +53,8 @@ impl Plugin for SteppingPlugin { // add our startup & stepping systems app.insert_resource(State { - ui_top: self.top, - ui_left: self.left, + ui_top: self.top.clone(), + ui_left: self.left.clone(), systems: Vec::new(), }) .add_systems( @@ -172,8 +172,8 @@ fn build_ui( SteppingUi, Node { position_type: PositionType::Absolute, - top: state.ui_top, - left: state.ui_left, + top: state.ui_top.clone(), + left: state.ui_left.clone(), padding: UiRect::all(px(10)), ..default() }, diff --git a/examples/stress_tests/many_buttons.rs b/examples/stress_tests/many_buttons.rs index 53dfa7815ed4d..96264f36d0259 100644 --- a/examples/stress_tests/many_buttons.rs +++ b/examples/stress_tests/many_buttons.rs @@ -197,7 +197,7 @@ fn setup_flex(mut commands: Commands, asset_server: Res, args: Res< column, row, args.text, - border, + border.clone(), border_color, images.as_ref().map(|images| { images[((column + row) / args.image_freq) % images.len()].clone() @@ -253,7 +253,7 @@ fn setup_grid(mut commands: Commands, asset_server: Res, args: Res< column, row, args.text, - border, + border.clone(), border_color, images.as_ref().map(|images| { images[((column + row) / args.image_freq) % images.len()].clone() @@ -279,7 +279,7 @@ fn spawn_button( ) { let width = vw(90.0 / buttons); let height = vh(90.0 / buttons); - let margin = UiRect::axes(width * 0.05, height * 0.05); + let margin = UiRect::axes(width.clone() * 0.05, height.clone() * 0.05); let mut builder = commands.spawn(( Button, Hovered(false), @@ -399,7 +399,7 @@ fn setup_many_cameras(mut commands: Commands, asset_server: Res, ar column, row, args.text, - border, + border.clone(), border_color, images.as_ref().map(|images| { images[((column + row) / args.image_freq) % images.len()] diff --git a/examples/stress_tests/many_text.rs b/examples/stress_tests/many_text.rs index d9ed8b69b409f..856dcd1a311b9 100644 --- a/examples/stress_tests/many_text.rs +++ b/examples/stress_tests/many_text.rs @@ -117,7 +117,7 @@ fn setup_text(mut commands: Commands, asset_server: Res) { let font = asset_server.load(font_path); let text_font = TextFont { font: font.into(), - font_size: px(10).into(), + font_size: FontSize::Px(10.), ..default() }; parent diff --git a/examples/testbed/full_ui.rs b/examples/testbed/full_ui.rs index 092f2723c98a6..1b8fb617d3b47 100644 --- a/examples/testbed/full_ui.rs +++ b/examples/testbed/full_ui.rs @@ -275,7 +275,7 @@ fn setup(mut commands: Commands, asset_server: Res) { ..default() }, BackgroundColor(Color::srgb(1.0, 0.0, 0.)), - BoxShadow::from(shadow_style), + BoxShadow::from(shadow_style.clone()), )) .with_children(|parent| { parent.spawn(( @@ -289,7 +289,7 @@ fn setup(mut commands: Commands, asset_server: Res) { ..default() }, BackgroundColor(Color::srgb(1.0, 0.3, 0.3)), - BoxShadow::from(shadow_style), + BoxShadow::from(shadow_style.clone()), )); parent.spawn(( Node { @@ -301,7 +301,7 @@ fn setup(mut commands: Commands, asset_server: Res) { ..default() }, BackgroundColor(Color::srgb(1.0, 0.5, 0.5)), - BoxShadow::from(shadow_style), + BoxShadow::from(shadow_style.clone()), )); parent.spawn(( Node { @@ -313,7 +313,7 @@ fn setup(mut commands: Commands, asset_server: Res) { ..default() }, BackgroundColor(Color::srgb(0.0, 0.7, 0.7)), - BoxShadow::from(shadow_style), + BoxShadow::from(shadow_style.clone()), )); // alpha test parent.spawn(( @@ -328,7 +328,7 @@ fn setup(mut commands: Commands, asset_server: Res) { BackgroundColor(Color::srgba(1.0, 0.9, 0.9, 0.4)), BoxShadow::from(ShadowStyle { color: Color::BLACK.with_alpha(0.3), - ..shadow_style + ..shadow_style.clone() }), )); }); diff --git a/examples/testbed/ui.rs b/examples/testbed/ui.rs index 775127771b141..6c414d6b6a9a2 100644 --- a/examples/testbed/ui.rs +++ b/examples/testbed/ui.rs @@ -1075,7 +1075,10 @@ mod text_measurement { BackgroundColor(Color::WHITE), ), ( - Node { width, ..default() }, + Node { + width: width.clone(), + ..default() + }, children![( Text::new("+300 Some Long Item Title"), TextFont { @@ -1109,7 +1112,10 @@ mod text_measurement { BackgroundColor(Color::WHITE), ), ( - Node { width, ..default() }, + Node { + width: width.clone(), + ..default() + }, Text::new("+300 Some Long Item Title"), TextFont { font_size: FontSize::Px(10.), @@ -1240,8 +1246,8 @@ mod borders { }, ]; - let non_zero = |x, y| x != px(0) && y != px(0); - let border_size = |x, y| if non_zero(x, y) { f32::MAX } else { 0. }; + let non_zero = |x: &Val, y: &Val| x != &px(0) && y != &px(0); + let border_size = |x: &Val, y: &Val| if non_zero(x, y) { f32::MAX } else { 0. }; for border in borders { for rounded in [true, false] { @@ -1250,16 +1256,16 @@ mod borders { Node { width: px(100), height: px(100), - border, + border: border.clone(), margin: UiRect::all(px(30)), align_items: AlignItems::Center, justify_content: JustifyContent::Center, border_radius: if rounded { BorderRadius::px( - border_size(border.left, border.top), - border_size(border.right, border.top), - border_size(border.right, border.bottom), - border_size(border.left, border.bottom), + border_size(&border.left, &border.top), + border_size(&border.right, &border.top), + border_size(&border.right, &border.bottom), + border_size(&border.left, &border.bottom), ) } else { BorderRadius::ZERO @@ -2332,8 +2338,8 @@ mod radial_gradient { }, BackgroundGradient::from(RadialGradient { stops: color_stops.clone(), - position, - shape, + position: position.clone(), + shape: shape.clone(), ..default() }), )); @@ -2846,14 +2852,42 @@ mod outer_color { )) .with_children(|builder| { for (border, border_radius, invert) in [ - (UiRect::ZERO, BorderRadius::bottom_right(radius), true), - (UiRect::top(width), BorderRadius::top(radius), false), - (UiRect::ZERO, BorderRadius::bottom_left(radius), true), - (UiRect::left(width), BorderRadius::left(radius), false), - (UiRect::all(width), BorderRadius::all(radius.x), true), - (UiRect::right(width), BorderRadius::right(radius), false), - (UiRect::ZERO, BorderRadius::top_right(radius), true), - (UiRect::bottom(width), BorderRadius::bottom(radius), false), + ( + UiRect::ZERO, + BorderRadius::bottom_right(radius.clone()), + true, + ), + ( + UiRect::top(width.clone()), + BorderRadius::top(radius.clone()), + false, + ), + ( + UiRect::ZERO, + BorderRadius::bottom_left(radius.clone()), + true, + ), + ( + UiRect::left(width.clone()), + BorderRadius::left(radius.clone()), + false, + ), + ( + UiRect::all(width.clone()), + BorderRadius::all(radius.x.clone()), + true, + ), + ( + UiRect::right(width.clone()), + BorderRadius::right(radius.clone()), + false, + ), + (UiRect::ZERO, BorderRadius::top_right(radius.clone()), true), + ( + UiRect::bottom(width), + BorderRadius::bottom(radius.clone()), + false, + ), (UiRect::ZERO, BorderRadius::top_left(radius), true), ] { builder @@ -3624,7 +3658,7 @@ mod node_material { border_radius: BorderRadius::all(px(20)), ..default() }; - let material_label = |text: &str| (Text::new(text), TextFont::from_font_size(px(10.))); + let material_label = |text: &str| (Text::new(text), TextFont::from_font_size(10.)); commands.spawn((Camera2d, DespawnOnExit(super::Scene::NodeMaterial))); commands.spawn(( @@ -3645,7 +3679,7 @@ mod node_material { children![ ( Text::new("MaterialNode"), - TextFont::from_font_size(px(30.)), + TextFont::from_font_size(30.), TextLayout::justify(Justify::Center), Node { grid_column: GridPlacement::span(2), @@ -3675,8 +3709,8 @@ mod node_material { material_label("'custom_ui_material.wgsl' clipped vertically"), ( Node { - width: material_node.width, - height: material_node.height, + width: material_node.width.clone(), + height: material_node.height.clone(), ..default() }, Outline { @@ -3698,8 +3732,8 @@ mod node_material { material_label("'custom_ui_material.wgsl' end clipped on both axis"), ( Node { - width: material_node.width, - height: material_node.height, + width: material_node.width.clone(), + height: material_node.height.clone(), ..default() }, Outline { @@ -3731,8 +3765,8 @@ mod node_material { material_label("'custom_ui_material.wgsl' start clipped on both axis"), ( Node { - width: material_node.width, - height: material_node.height, + width: material_node.width.clone(), + height: material_node.height.clone(), align_items: AlignItems::End, justify_content: JustifyContent::End, ..default() @@ -3885,7 +3919,7 @@ mod font_relative_units { ..default() }, Text::new("px sizing"), - TextFont::from_font_size(px(16)), + TextFont::from_font_size(16.), TextColor(LAVENDER.into()), ), ( diff --git a/examples/ui/layout/ghost_nodes.rs b/examples/ui/layout/ghost_nodes.rs index 7fa578de1b530..eb5bc17a3bfa9 100644 --- a/examples/ui/layout/ghost_nodes.rs +++ b/examples/ui/layout/ghost_nodes.rs @@ -119,7 +119,7 @@ fn label(text: &str) -> impl Scene { Text(text) TextFont { font: FontSourceTemplate::Handle("fonts/FiraSans-Bold.ttf"), - font_size: px(33.0), + font_size: FontSize::Px(33.0), } TextColor(Color::srgb(0.9, 0.9, 0.9)) } diff --git a/examples/ui/layout/size_constraints.rs b/examples/ui/layout/size_constraints.rs index d70f14ede09e3..cc1e3866799d0 100644 --- a/examples/ui/layout/size_constraints.rs +++ b/examples/ui/layout/size_constraints.rs @@ -39,7 +39,7 @@ enum Constraint { MaxWidth, } -#[derive(Copy, Clone, Component, Default, PartialEq)] +#[derive(Clone, Component, Default, PartialEq)] struct RadioButtonValue(Val); fn setup(mut commands: Commands) { @@ -320,16 +320,16 @@ fn on_value_change_constraints( match constraint { Constraint::FlexBasis => { - bar_node.flex_basis = value.0; + bar_node.flex_basis = value.0.clone(); } Constraint::Width => { - bar_node.width = value.0; + bar_node.width = value.0.clone(); } Constraint::MinWidth => { - bar_node.min_width = value.0; + bar_node.min_width = value.0.clone(); } Constraint::MaxWidth => { - bar_node.max_width = value.0; + bar_node.max_width = value.0.clone(); } } } diff --git a/examples/ui/styling/borders.rs b/examples/ui/styling/borders.rs index 30f277d067b6c..5d6cc9f685219 100644 --- a/examples/ui/styling/borders.rs +++ b/examples/ui/styling/borders.rs @@ -95,55 +95,58 @@ fn setup(mut commands: Commands) { flex_wrap: FlexWrap::Wrap, ..default() }, - Children::spawn(SpawnIter(border_labels.into_iter().zip(borders).map( - |(label, border)| { - ( - Node { - flex_direction: FlexDirection::Column, - align_items: AlignItems::Center, - ..default() - }, - children![ - ( - Node { - width: px(50), - height: px(50), - border, - margin: px(20).all(), - align_items: AlignItems::Center, - justify_content: JustifyContent::Center, - ..default() - }, - BackgroundColor(MAROON.into()), - BorderColor { - top: RED.into(), - bottom: YELLOW.into(), - left: GREEN.into(), - right: BLUE.into(), - }, - Outline { - width: px(6), - offset: px(6), - color: Color::WHITE, - }, - children![( + Children::spawn(SpawnIter( + border_labels + .into_iter() + .zip(borders.clone()) + .map(|(label, border)| { + ( + Node { + flex_direction: FlexDirection::Column, + align_items: AlignItems::Center, + ..default() + }, + children![ + ( Node { - width: px(10), - height: px(10), + width: px(50), + height: px(50), + border, + margin: px(20).all(), + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, ..default() }, - BackgroundColor(YELLOW.into()), - )] - ), - (Text::new(label), TextFont::from_font_size(9.0)) - ], - ) - }, - ))), + BackgroundColor(MAROON.into()), + BorderColor { + top: RED.into(), + bottom: YELLOW.into(), + left: GREEN.into(), + right: BLUE.into(), + }, + Outline { + width: px(6), + offset: px(6), + color: Color::WHITE, + }, + children![( + Node { + width: px(10), + height: px(10), + ..default() + }, + BackgroundColor(YELLOW.into()), + )] + ), + (Text::new(label), TextFont::from_font_size(9.0)) + ], + ) + }), + )), ); - let non_zero = |x, y| x != px(0) && y != px(0); - let border_size = move |x, y| { + let non_zero = |x: &Val, y: &Val| x != &px(0) && y != &px(0); + let border_size = move |x: &Val, y: &Val| { if non_zero(x, y) { f32::MAX } else { @@ -151,12 +154,12 @@ fn setup(mut commands: Commands) { } }; - let rounded_border_radii = borders.map(|border| { + let rounded_border_radii = borders.clone().map(|border| { BorderRadius::px( - border_size(border.left, border.top), - border_size(border.right, border.top), - border_size(border.right, border.bottom), - border_size(border.left, border.bottom), + border_size(&border.left, &border.top), + border_size(&border.right, &border.top), + border_size(&border.right, &border.bottom), + border_size(&border.left, &border.bottom), ) }); diff --git a/examples/ui/text/inline_image.rs b/examples/ui/text/inline_image.rs index 71b82d44a759c..bfbec0a8cc091 100644 --- a/examples/ui/text/inline_image.rs +++ b/examples/ui/text/inline_image.rs @@ -12,7 +12,7 @@ fn setup(mut commands: Commands, asset_server: Res) { commands.spawn(Camera2d); commands.spawn(( Text::new("[Text]"), - TextFont::from_font_size(px(40.)), + TextFont::from_font_size(40.), Node { margin: px(25).all(), justify_self: JustifySelf::Center, @@ -27,7 +27,7 @@ fn setup(mut commands: Commands, asset_server: Res) { children![ ( TextSpan::new("[span before image]"), - TextFont::from_font_size(px(40)), + TextFont::from_font_size(40.), ), (InlineImage { color: bevy::color::palettes::css::RED.into(), @@ -37,7 +37,7 @@ fn setup(mut commands: Commands, asset_server: Res) { },), ( TextSpan::new("[span between images]"), - TextFont::from_font_size(px(40.)), + TextFont::from_font_size(40.), ), (InlineImage { color: bevy::color::palettes::css::YELLOW.into(), @@ -47,7 +47,7 @@ fn setup(mut commands: Commands, asset_server: Res) { },), ( TextSpan::new("[span after image]"), - TextFont::from_font_size(px(40.)), + TextFont::from_font_size(40.), ), ], )); diff --git a/examples/ui/ui_transform.rs b/examples/ui/ui_transform.rs index 5def7a53e1e10..b08c21da6fccf 100644 --- a/examples/ui/ui_transform.rs +++ b/examples/ui/ui_transform.rs @@ -100,7 +100,7 @@ fn translation_system( if input.pressed(key_code) { for mut transform in translation_query.iter_mut() { let d = direction * 50.0 * time.delta_secs(); - let (Val::Px(x), Val::Px(y)) = (transform.translation.x, transform.translation.y) + let (Val::Px(x), Val::Px(y)) = (&transform.translation.x, &transform.translation.y) else { continue; }; diff --git a/examples/ui/widgets/viewport_node.rs b/examples/ui/widgets/viewport_node.rs index 459505fd63a1d..b9d044cfbc6d7 100644 --- a/examples/ui/widgets/viewport_node.rs +++ b/examples/ui/widgets/viewport_node.rs @@ -80,7 +80,7 @@ fn on_drag_viewport(drag: On, mut node_query: Query<&mut Node>) { if matches!(drag.button, PointerButton::Secondary) { let mut node = node_query.get_mut(drag.entity).unwrap(); - if let (Val::Px(top), Val::Px(left)) = (node.top, node.left) { + if let (&Val::Px(top), &Val::Px(left)) = (&node.top, &node.left) { node.left = px(left + drag.delta.x); node.top = px(top + drag.delta.y); }; From b58e0f14647f839c662cc48ee7ffd95f7d8a1874 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sun, 1 Nov 2026 09:27:24 +0000 Subject: [PATCH 04/50] Apply calc scaling to leaves and fix compile errors Remove CalcOps::Scale and push scales down to the leaves immediately, so repeated *= and /= no longer deepen the tree. Negative scales swap Min and Max, and turn Clamp into Min/Max to preserve the crossed bounds behavior. Replace stale Calc:: names with CalcOps::, rebuild the corrupted calc tests, and clone Vals in the crates that still relied on Val being Copy. Co-Authored-By: Claude Opus 5.5 --- benches/benches/bevy_ui/layout.rs | 4 +- crates/bevy_feathers/src/controls/tabs.rs | 6 +- crates/bevy_inspector/src/column_split.rs | 14 ++-- crates/bevy_ui/src/calc.rs | 90 +++++++++++++++++++---- crates/bevy_ui/src/geometry.rs | 25 +++++-- crates/bevy_ui/src/layout/tests.rs | 2 +- 6 files changed, 108 insertions(+), 33 deletions(-) diff --git a/benches/benches/bevy_ui/layout.rs b/benches/benches/bevy_ui/layout.rs index 214617169dc0f..1c37f0e3ffc4e 100644 --- a/benches/benches/bevy_ui/layout.rs +++ b/benches/benches/bevy_ui/layout.rs @@ -231,7 +231,7 @@ fn layout(c: &mut Criterion) { for &leaf in &leaves { let mut node = app.world_mut().get_mut::(leaf).unwrap(); let length = if node.width == px(1) { px(2) } else { px(1) }; - node.width = length; + node.width = length.clone(); node.height = length; } app.update(); @@ -254,7 +254,7 @@ fn layout(c: &mut Criterion) { b.iter(|| { let mut node = app.world_mut().get_mut::(leaf).unwrap(); let length = if node.width == px(1) { px(2) } else { px(1) }; - node.width = length; + node.width = length.clone(); node.height = length; app.update(); }); diff --git a/crates/bevy_feathers/src/controls/tabs.rs b/crates/bevy_feathers/src/controls/tabs.rs index a2134a1fc290b..5305840588e2c 100644 --- a/crates/bevy_feathers/src/controls/tabs.rs +++ b/crates/bevy_feathers/src/controls/tabs.rs @@ -1320,7 +1320,7 @@ mod tests { assert_eq!(drag_proxy.pointer_id, PointerId::Touch(4)); let ghost = app.world().entity(proxy); assert_eq!( - ghost.get::().map(|outline| outline.width), + ghost.get::().map(|outline| outline.width.clone()), Some(px(1)) ); assert!(ghost.contains::()); @@ -1774,7 +1774,9 @@ mod tests { let world = app.world(); let list = world.entity(list).get::().unwrap(); let tab = world.entity(tab).get::().unwrap(); - let (Val::Px(tab_height), Val::Px(min_height)) = (tab.min_height, list.min_height) else { + let (Val::Px(tab_height), Val::Px(min_height)) = + (tab.min_height.clone(), list.min_height.clone()) + else { panic!("expected pixel heights"); }; let Val::Px(top) = list.padding.top else { diff --git a/crates/bevy_inspector/src/column_split.rs b/crates/bevy_inspector/src/column_split.rs index 68540cf242966..b267a2de21c04 100644 --- a/crates/bevy_inspector/src/column_split.rs +++ b/crates/bevy_inspector/src/column_split.rs @@ -149,8 +149,8 @@ fn border_box(entity: Entity, nodes: &Query, scale: f32) -> Option<[f3 let [left, right] = inner_box(parent?.parent(), nodes, scale, true)?; let margin = &node.margin; Some([ - left + px_value(margin.left) * scale, - right - px_value(margin.right) * scale, + left + px_value(margin.left.clone()) * scale, + right - px_value(margin.right.clone()) * scale, ]) } @@ -171,8 +171,8 @@ fn inner_box( }; let [inset_left, inset_right] = match computed.is_added() { true => [ - side(node.border.left, node.padding.left), - side(node.border.right, node.padding.right), + side(node.border.left.clone(), node.padding.left.clone()), + side(node.border.right.clone(), node.padding.right.clone()), ], false => [inset.min_inset.x, inset.max_inset.x], }; @@ -237,7 +237,7 @@ fn update_column_splits( if is_handle && node.left != value { node.left = value; } else if !is_handle && node.width != value { - node.width = value; + node.width = value.clone(); node.min_width = value; } } @@ -509,7 +509,7 @@ mod tests { assert_eq!(app.world().get::(split).unwrap().len(), 1); let node = app.world().get::(handle).unwrap(); assert_eq!(node.position_type, PositionType::Absolute); - assert_eq!((node.top, node.bottom), (px(0), px(0))); + assert_eq!((node.top.clone(), node.bottom.clone()), (px(0), px(0))); assert_eq!( app.world().get::(handle), Some(&EntityCursor::System(SystemCursorIcon::ColResize)) @@ -701,7 +701,7 @@ mod tests { fn widths(app: &App, nodes: &[Entity]) -> Vec { nodes .iter() - .map(|&entity| app.world().get::(entity).unwrap().width) + .map(|&entity| app.world().get::(entity).unwrap().width.clone()) .collect() } diff --git a/crates/bevy_ui/src/calc.rs b/crates/bevy_ui/src/calc.rs index adc2b2da6da9a..46035f1aba565 100644 --- a/crates/bevy_ui/src/calc.rs +++ b/crates/bevy_ui/src/calc.rs @@ -26,7 +26,6 @@ pub struct Calc(pub Box); pub enum CalcOps { Add(Val, Val), Sub(Val, Val), - Scale(Val, f32), Min(Val, Val), Max(Val, Val), Clamp(Val, Val, Val), @@ -43,7 +42,6 @@ impl CalcOps { Self::Add(a, b) | Self::Sub(a, b) | Self::Min(a, b) | Self::Max(a, b) => { uses_percent(a) || uses_percent(b) } - Self::Scale(value, _) => uses_percent(value), Self::Clamp(min, value, max) => { uses_percent(min) || uses_percent(value) || uses_percent(max) } @@ -70,12 +68,34 @@ impl CalcOps { Ok(match self { Self::Add(a, b) => resolve(a)? + resolve(b)?, Self::Sub(a, b) => resolve(a)? - resolve(b)?, - Self::Scale(value, scale) => resolve(value)? * scale, Self::Min(a, b) => resolve(a)?.min(resolve(b)?), Self::Max(a, b) => resolve(a)?.max(resolve(b)?), Self::Clamp(min, value, max) => resolve(min)?.max(resolve(value)?.min(resolve(max)?)), }) } + + pub(crate) fn scale(&mut self, k: f32) { + use core::mem::take; + match self { + Self::Min(a, b) if k < 0.0 => *self = Self::Max(take(a) * k, take(b) * k), + Self::Max(a, b) if k < 0.0 => *self = Self::Min(take(a) * k, take(b) * k), + Self::Clamp(min, value, max) if k < 0.0 => { + *self = Self::Min( + take(min) * k, + Self::Max(take(value) * k, take(max) * k).into(), + ); + } + Self::Add(a, b) | Self::Sub(a, b) | Self::Min(a, b) | Self::Max(a, b) => { + *a *= k; + *b *= k; + } + Self::Clamp(min, value, max) => { + *min *= k; + *value *= k; + *max *= k; + } + } + } } impl From for Val { @@ -84,7 +104,13 @@ impl From for Val { } } -#[cfg(te let value = +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn resolve_expressions() { + let value = Val::Percent(50.0) - Val::Px(10.0) + Val::Em(2.0) + Val::Rem(1.0) + Val::Vw(1.0); let resolve = |value: &Val| { value.resolve( @@ -123,8 +149,42 @@ impl From for Val { resolve(&(Val::Auto + Val::Px(1.0))), Err(ValArithmeticError::NonEvaluable) ); -k(139.0)); - assert_eq!(resolve(&(Val::Auto + Val::Px(1.0))), Err(ValArithmeticError::NonEvaluable)); + } + + #[test] + fn negative_scale_swaps_min_and_max() { + let resolve = |value: Val| value.resolve(1.0, 0.0, Vec2::ZERO, EmSize(0.0), RemSize(0.0)); + let (a, b) = (Val::Px(10.0), Val::Px(20.0)); + assert_eq!( + resolve(-Val::from(CalcOps::Min(a.clone(), b.clone()))), + Ok(-10.0) + ); + assert_eq!( + resolve(-Val::from(CalcOps::Max(a.clone(), b.clone()))), + Ok(-20.0) + ); + assert_eq!( + resolve(-Val::from(CalcOps::Clamp( + a.clone(), + Val::Px(15.0), + b.clone() + ))), + Ok(-15.0) + ); + assert_eq!( + resolve(-Val::from(CalcOps::Clamp(b, Val::Px(15.0), a))), + Ok(-20.0) + ); + } + + #[test] + fn repeated_scaling_does_not_grow_the_tree() { + let mut value = Val::Px(1.0) + Val::Px(2.0); + for _ in 0..100_000 { + value *= 2.0; + value /= 2.0; + } + assert_eq!(value, Val::Px(1.0) + Val::Px(2.0)); } #[test] @@ -136,18 +196,22 @@ k(139.0)); assert_eq!(value, cloned); cloned *= 2.0; assert_ne!(value, cloned); - assert_eq!(value, Val::Percent(100 assert_eq!( + assert_eq!(value, Val::Percent(100.0) - Val::Px(24.0)); + let mut registry = TypeRegistry::default(); + registry.register::(); + let dynamic = value.to_dynamic().unwrap(); + assert_eq!(Val::from_reflect(dynamic.as_ref()), Some(value)); + } + + #[test] + fn font_size_rejects_expressions() { + assert_eq!( bevy_text::FontSize::try_from(Val::Rem(2.0)), Ok(bevy_text::FontSize::Rem(2.0)) ); assert_eq!( - bevy_text::FontSize::try_from(value.clone()), + bevy_text::FontSize::try_from(Val::Percent(100.0) - Val::Px(24.0)), Err(ValArithmeticError::NonEvaluable) ); -.0) - Val::Px(24.0)); - let mut registry = TypeRegistry::default(); - registry.register::(); - let dynamic = value.to_dynamic().unwrap(); - assert_eq!(Val::from_reflect(dynamic.as_ref()), Some(value)); } } diff --git a/crates/bevy_ui/src/geometry.rs b/crates/bevy_ui/src/geometry.rs index c7c2bc8f8090d..8afc0525b3c14 100644 --- a/crates/bevy_ui/src/geometry.rs +++ b/crates/bevy_ui/src/geometry.rs @@ -8,7 +8,7 @@ use thiserror::Error; #[cfg(feature = "serialize")] use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; -use crate::calc::{Calc}; +use crate::calc::{Calc, CalcOps}; /// Represents the possible value types for layout properties. /// @@ -396,7 +396,7 @@ impl Add for Val { type Output = Self; fn add(self, rhs: Self) -> Self { - Calc::Add(self, rhs).into() + CalcOps::Add(self, rhs).into() } } @@ -404,7 +404,7 @@ impl Sub for Val { type Output = Self; fn sub(self, rhs: Self) -> Self { - Calc::Sub(self, rhs).into() + CalcOps::Sub(self, rhs).into() } } @@ -422,7 +422,10 @@ impl Mul for Val { Val::VMax(value) => Val::VMax(value * rhs), Val::Em(value) => Val::Em(value * rhs), Val::Rem(value) => Val::Rem(value * rhs), - Val::Calc(op) => Calc::Scale(Val::Calc(op), rhs).into(), + Val::Calc(mut calc) => { + calc.0.scale(rhs); + Val::Calc(calc) + } } } } @@ -439,7 +442,7 @@ impl MulAssign for Val { | Val::VMax(value) | Val::Em(value) | Val::Rem(value) => *value *= rhs, - Val::Calc(_) => *self = core::mem::take(self) * rhs, + Val::Calc(calc) => calc.0.scale(rhs), } } } @@ -458,7 +461,10 @@ impl Div for Val { Val::VMax(value) => Val::VMax(value / rhs), Val::Em(value) => Val::Em(value / rhs), Val::Rem(value) => Val::Rem(value / rhs), - Val::Calc(op) => Calc::Scale(Val::Calc(op), rhs.recip()).into(), + Val::Calc(mut calc) => { + calc.0.scale(rhs.recip()); + Val::Calc(calc) + } } } } @@ -475,7 +481,7 @@ impl DivAssign for Val { | Val::VMax(value) | Val::Em(value) | Val::Rem(value) => *value /= rhs, - Val::Calc(_) => *self = core::mem::take(self) / rhs, + Val::Calc(calc) => calc.0.scale(rhs.recip()), } } } @@ -494,7 +500,10 @@ impl Neg for Val { Val::VMax(value) => Val::VMax(-value), Val::Em(value) => Val::Em(-value), Val::Rem(value) => Val::Rem(-value), - Val::Calc(op) => Calc::Scale(Val::Calc(op), -1.0).into(), + Val::Calc(mut calc) => { + calc.0.scale(-1.0); + Val::Calc(calc) + } } } } diff --git a/crates/bevy_ui/src/layout/tests.rs b/crates/bevy_ui/src/layout/tests.rs index 37e7e44a39871..70231e73ec981 100644 --- a/crates/bevy_ui/src/layout/tests.rs +++ b/crates/bevy_ui/src/layout/tests.rs @@ -173,7 +173,7 @@ fn calc_layout_handles_containers_and_parent_width_based_padding() { let child = app .world_mut() .spawn(Node { - width: crate::calc::Calc::Clamp(Val::Px(100.0), Val::Percent(80.0), Val::Px(300.0)) + width: crate::calc::CalcOps::Clamp(Val::Px(100.0), Val::Percent(80.0), Val::Px(300.0)) .into(), height: Val::Percent(50.0) - Val::Px(10.0), padding: UiRect::all(Val::Percent(5.0) + Val::Px(2.0)), From aa127257dc439e50dc598ed7f731d105c7036e2d Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sun, 1 Nov 2026 09:49:18 +0000 Subject: [PATCH 05/50] Renamed `CalOps` to `CalcOp` --- crates/bevy_ui/src/calc.rs | 26 +++++++++++++------------- crates/bevy_ui/src/geometry.rs | 6 +++--- crates/bevy_ui/src/layout/tests.rs | 2 +- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/crates/bevy_ui/src/calc.rs b/crates/bevy_ui/src/calc.rs index 46035f1aba565..9c26265b112ab 100644 --- a/crates/bevy_ui/src/calc.rs +++ b/crates/bevy_ui/src/calc.rs @@ -14,7 +14,7 @@ use crate::{Val, ValArithmeticError}; derive(serde::Serialize, serde::Deserialize), reflect(Serialize, Deserialize) )] -pub struct Calc(pub Box); +pub struct Calc(pub Box); #[derive(Clone, Debug, PartialEq, Reflect)] #[reflect(PartialEq, Debug, Clone)] @@ -23,7 +23,7 @@ pub struct Calc(pub Box); derive(serde::Serialize, serde::Deserialize), reflect(Serialize, Deserialize) )] -pub enum CalcOps { +pub enum CalcOp { Add(Val, Val), Sub(Val, Val), Min(Val, Val), @@ -31,7 +31,7 @@ pub enum CalcOps { Clamp(Val, Val, Val), } -impl CalcOps { +impl CalcOp { pub(crate) fn uses_percent(&self) -> bool { let uses_percent = |val: &Val| match val { Val::Percent(_) => true, @@ -98,8 +98,8 @@ impl CalcOps { } } -impl From for Val { - fn from(op: CalcOps) -> Self { +impl From for Val { + fn from(op: CalcOp) -> Self { Self::Calc(Calc(Box::new(op))) } } @@ -126,19 +126,19 @@ mod tests { assert_eq!(resolve(&(value.clone() / 2.0)), Ok(139.0)); assert_eq!(resolve(&-value.clone()), Ok(-278.0)); assert_eq!( - resolve(&CalcOps::Min(value.clone(), Val::Px(100.0)).into()), + resolve(&CalcOp::Min(value.clone(), Val::Px(100.0)).into()), Ok(200.0) ); assert_eq!( - resolve(&CalcOps::Max(value.clone(), Val::Px(200.0)).into()), + resolve(&CalcOp::Max(value.clone(), Val::Px(200.0)).into()), Ok(400.0) ); assert_eq!( - resolve(&CalcOps::Clamp(Val::Px(100.0), value.clone(), Val::Px(125.0)).into()), + resolve(&CalcOp::Clamp(Val::Px(100.0), value.clone(), Val::Px(125.0)).into()), Ok(250.0) ); assert_eq!( - resolve(&CalcOps::Clamp(Val::Px(200.0), value.clone(), Val::Px(100.0)).into()), + resolve(&CalcOp::Clamp(Val::Px(200.0), value.clone(), Val::Px(100.0)).into()), Ok(400.0) ); let mut scaled = value; @@ -156,15 +156,15 @@ mod tests { let resolve = |value: Val| value.resolve(1.0, 0.0, Vec2::ZERO, EmSize(0.0), RemSize(0.0)); let (a, b) = (Val::Px(10.0), Val::Px(20.0)); assert_eq!( - resolve(-Val::from(CalcOps::Min(a.clone(), b.clone()))), + resolve(-Val::from(CalcOp::Min(a.clone(), b.clone()))), Ok(-10.0) ); assert_eq!( - resolve(-Val::from(CalcOps::Max(a.clone(), b.clone()))), + resolve(-Val::from(CalcOp::Max(a.clone(), b.clone()))), Ok(-20.0) ); assert_eq!( - resolve(-Val::from(CalcOps::Clamp( + resolve(-Val::from(CalcOp::Clamp( a.clone(), Val::Px(15.0), b.clone() @@ -172,7 +172,7 @@ mod tests { Ok(-15.0) ); assert_eq!( - resolve(-Val::from(CalcOps::Clamp(b, Val::Px(15.0), a))), + resolve(-Val::from(CalcOp::Clamp(b, Val::Px(15.0), a))), Ok(-20.0) ); } diff --git a/crates/bevy_ui/src/geometry.rs b/crates/bevy_ui/src/geometry.rs index 8afc0525b3c14..8726dfe044f4a 100644 --- a/crates/bevy_ui/src/geometry.rs +++ b/crates/bevy_ui/src/geometry.rs @@ -8,7 +8,7 @@ use thiserror::Error; #[cfg(feature = "serialize")] use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; -use crate::calc::{Calc, CalcOps}; +use crate::calc::{Calc, CalcOp}; /// Represents the possible value types for layout properties. /// @@ -396,7 +396,7 @@ impl Add for Val { type Output = Self; fn add(self, rhs: Self) -> Self { - CalcOps::Add(self, rhs).into() + CalcOp::Add(self, rhs).into() } } @@ -404,7 +404,7 @@ impl Sub for Val { type Output = Self; fn sub(self, rhs: Self) -> Self { - CalcOps::Sub(self, rhs).into() + CalcOp::Sub(self, rhs).into() } } diff --git a/crates/bevy_ui/src/layout/tests.rs b/crates/bevy_ui/src/layout/tests.rs index 70231e73ec981..ed7968cf7977b 100644 --- a/crates/bevy_ui/src/layout/tests.rs +++ b/crates/bevy_ui/src/layout/tests.rs @@ -173,7 +173,7 @@ fn calc_layout_handles_containers_and_parent_width_based_padding() { let child = app .world_mut() .spawn(Node { - width: crate::calc::CalcOps::Clamp(Val::Px(100.0), Val::Percent(80.0), Val::Px(300.0)) + width: crate::calc::CalcOp::Clamp(Val::Px(100.0), Val::Percent(80.0), Val::Px(300.0)) .into(), height: Val::Percent(50.0) - Val::Px(10.0), padding: UiRect::all(Val::Percent(5.0) + Val::Px(2.0)), From b6ba44d819b51eeb7fae7faed1dc7674102fed4e Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sun, 1 Nov 2026 10:36:21 +0000 Subject: [PATCH 06/50] Added min, max and clamp heleprs to Val. --- crates/bevy_ui/src/geometry.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/bevy_ui/src/geometry.rs b/crates/bevy_ui/src/geometry.rs index 8726dfe044f4a..5dbce58b5bea7 100644 --- a/crates/bevy_ui/src/geometry.rs +++ b/crates/bevy_ui/src/geometry.rs @@ -384,6 +384,18 @@ impl Val { _ => Err(ValArithmeticError::IncompatibleUnits), } } + + pub fn min(self, min: Val) -> Val { + CalcOp::Min(self, min).into() + } + + pub fn max(self, max: Val) -> Val { + CalcOp::Max(self, max).into() + } + + pub fn clamp(self, min: Val, max: Val) -> Val { + CalcOp::Clamp(min, self, max).into() + } } impl Default for Val { From 406a3ee99c5d3e4602302871c5fbea80b1eccb55 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sun, 1 Nov 2026 10:56:37 +0000 Subject: [PATCH 07/50] Changed Calc to use an Arc instead of a Box --- crates/bevy_ui/src/calc.rs | 6 ++++-- crates/bevy_ui/src/geometry.rs | 11 ++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/crates/bevy_ui/src/calc.rs b/crates/bevy_ui/src/calc.rs index 9c26265b112ab..b435f91f347e0 100644 --- a/crates/bevy_ui/src/calc.rs +++ b/crates/bevy_ui/src/calc.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use bevy_math::Vec2; use bevy_reflect::Reflect; use bevy_text::{EmSize, RemSize}; @@ -14,7 +16,7 @@ use crate::{Val, ValArithmeticError}; derive(serde::Serialize, serde::Deserialize), reflect(Serialize, Deserialize) )] -pub struct Calc(pub Box); +pub struct Calc(pub Arc); #[derive(Clone, Debug, PartialEq, Reflect)] #[reflect(PartialEq, Debug, Clone)] @@ -100,7 +102,7 @@ impl CalcOp { impl From for Val { fn from(op: CalcOp) -> Self { - Self::Calc(Calc(Box::new(op))) + Self::Calc(Calc(Arc::new(op))) } } diff --git a/crates/bevy_ui/src/geometry.rs b/crates/bevy_ui/src/geometry.rs index 5dbce58b5bea7..0f27f1763cc58 100644 --- a/crates/bevy_ui/src/geometry.rs +++ b/crates/bevy_ui/src/geometry.rs @@ -3,6 +3,7 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_text::{EmSize, FontSize, RemSize}; use bevy_utils::default; use core::ops::{Add, Div, DivAssign, Mul, MulAssign, Neg, Sub}; +use std::sync::Arc; use thiserror::Error; #[cfg(feature = "serialize")] @@ -435,7 +436,7 @@ impl Mul for Val { Val::Em(value) => Val::Em(value * rhs), Val::Rem(value) => Val::Rem(value * rhs), Val::Calc(mut calc) => { - calc.0.scale(rhs); + Arc::make_mut(&mut calc.0).scale(rhs); Val::Calc(calc) } } @@ -454,7 +455,7 @@ impl MulAssign for Val { | Val::VMax(value) | Val::Em(value) | Val::Rem(value) => *value *= rhs, - Val::Calc(calc) => calc.0.scale(rhs), + Val::Calc(calc) => Arc::make_mut(&mut calc.0).scale(rhs), } } } @@ -474,7 +475,7 @@ impl Div for Val { Val::Em(value) => Val::Em(value / rhs), Val::Rem(value) => Val::Rem(value / rhs), Val::Calc(mut calc) => { - calc.0.scale(rhs.recip()); + Arc::make_mut(&mut calc.0).scale(rhs.recip()); Val::Calc(calc) } } @@ -493,7 +494,7 @@ impl DivAssign for Val { | Val::VMax(value) | Val::Em(value) | Val::Rem(value) => *value /= rhs, - Val::Calc(calc) => calc.0.scale(rhs.recip()), + Val::Calc(calc) => Arc::make_mut(&mut calc.0).scale(rhs.recip()), } } } @@ -513,7 +514,7 @@ impl Neg for Val { Val::Em(value) => Val::Em(-value), Val::Rem(value) => Val::Rem(-value), Val::Calc(mut calc) => { - calc.0.scale(-1.0); + Arc::make_mut(&mut calc.0).scale(-1.0); Val::Calc(calc) } } From cba4c355ece93bf63933b650f14a33b337d86728 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sun, 1 Nov 2026 11:14:15 +0000 Subject: [PATCH 08/50] Added some minimal doc comments, make Calc inner value pub (crate). --- crates/bevy_ui/src/calc.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ui/src/calc.rs b/crates/bevy_ui/src/calc.rs index b435f91f347e0..4202573730b7f 100644 --- a/crates/bevy_ui/src/calc.rs +++ b/crates/bevy_ui/src/calc.rs @@ -9,6 +9,7 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; use crate::{Val, ValArithmeticError}; +/// Wrapper around an opteration on `Val`s. #[derive(Clone, Debug, PartialEq, Reflect)] #[reflect(opaque, PartialEq, Debug, Clone)] #[cfg_attr( @@ -16,8 +17,9 @@ use crate::{Val, ValArithmeticError}; derive(serde::Serialize, serde::Deserialize), reflect(Serialize, Deserialize) )] -pub struct Calc(pub Arc); +pub struct Calc(pub(crate) Arc); +/// An operation performed on `Val`s #[derive(Clone, Debug, PartialEq, Reflect)] #[reflect(PartialEq, Debug, Clone)] #[cfg_attr( @@ -26,10 +28,15 @@ pub struct Calc(pub Arc); reflect(Serialize, Deserialize) )] pub enum CalcOp { + /// Add two vals. Add(Val, Val), + /// Subtract the second `Val` from the first. Sub(Val, Val), + /// Return the minimum of the two `Val`s. Min(Val, Val), + /// Return the maximum of the two `Val`s. Max(Val, Val), + /// Clamp the second `Val` between the first and third. Clamp(Val, Val, Val), } From c45d57178eef70f92b4ee14007932945aadca165 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sun, 1 Nov 2026 12:19:57 +0000 Subject: [PATCH 09/50] Added `ValUnit` discriminator enum, to be used for representing Val unit types when packing Vals. --- crates/bevy_ui/src/calc.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/crates/bevy_ui/src/calc.rs b/crates/bevy_ui/src/calc.rs index 4202573730b7f..bd4093390d627 100644 --- a/crates/bevy_ui/src/calc.rs +++ b/crates/bevy_ui/src/calc.rs @@ -113,6 +113,23 @@ impl From for Val { } } +/// Discriminator enum, should match `Val`'s unit types. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Reflect)] +#[repr(u8)] +pub enum ValUnit { + /// Use `Px` units + Px, + /// Use `Percent` units + Percent, + /// Use `Vw` units + Vw, + Vh, + VMin, + VMax, + Em, + Rem, +} + #[cfg(test)] mod tests { use super::*; From 981dc19f4e16f471956a4c8d2e1d9cd09be2c3c6 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sun, 1 Nov 2026 20:35:51 +0000 Subject: [PATCH 10/50] Add inline Val::Sum, Val::Min and Val::Max variants Up to three terms with distinct units are stored inline as a `ValUnitSet` bitmask plus `[f32; 3]`, keeping `Val` at 16 bytes and making `px + percent`, `min` and `max` of leaves allocation-free. `+`, `-`, `min` and `max` fold same-unit terms, keep terms in unit order, and fall back to `Calc` for anything longer or nested. `Sum` equality ignores zero terms, `Min`/`Max` swap under negation, and all three interpolate term-wise. `LayoutCalc` now holds a `Val` so the layout path resolves them like `Calc`. Co-Authored-By: Claude Fable 5.1 --- crates/bevy_ui/src/calc.rs | 356 ++++++++++++++++++++++- crates/bevy_ui/src/geometry.rs | 133 ++++++++- crates/bevy_ui/src/layout/convert.rs | 12 +- crates/bevy_ui/src/layout/layout_tree.rs | 8 +- crates/bevy_ui_render/src/box_shadow.rs | 2 +- 5 files changed, 478 insertions(+), 33 deletions(-) diff --git a/crates/bevy_ui/src/calc.rs b/crates/bevy_ui/src/calc.rs index bd4093390d627..8df968ccf25c3 100644 --- a/crates/bevy_ui/src/calc.rs +++ b/crates/bevy_ui/src/calc.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use bevy_math::Vec2; +use bevy_math::{StableInterpolate as _, Vec2}; use bevy_reflect::Reflect; use bevy_text::{EmSize, RemSize}; @@ -42,17 +42,12 @@ pub enum CalcOp { impl CalcOp { pub(crate) fn uses_percent(&self) -> bool { - let uses_percent = |val: &Val| match val { - Val::Percent(_) => true, - Val::Calc(calc) => calc.0.uses_percent(), - _ => false, - }; match self { Self::Add(a, b) | Self::Sub(a, b) | Self::Min(a, b) | Self::Max(a, b) => { - uses_percent(a) || uses_percent(b) + a.uses_percent() || b.uses_percent() } Self::Clamp(min, value, max) => { - uses_percent(min) || uses_percent(value) || uses_percent(max) + min.uses_percent() || value.uses_percent() || max.uses_percent() } } } @@ -130,10 +125,355 @@ pub enum ValUnit { Rem, } +impl ValUnit { + /// Every unit, in discriminant order. + pub const ALL: [Self; 8] = [ + Self::Px, + Self::Percent, + Self::Vw, + Self::Vh, + Self::VMin, + Self::VMax, + Self::Em, + Self::Rem, + ]; + + const fn bit(self) -> u8 { + 1 << self as u8 + } +} + +/// A set of [`ValUnit`]s. +/// +/// [`Val::Sum`], [`Val::Min`] and [`Val::Max`] pair one of these with an array of values: +/// the `n`th value belongs to the `n`th unit in [`ValUnitSet::iter`] order, and the +/// slots after the last unit are unused. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Default, Reflect)] +#[reflect(PartialEq, Debug, Clone)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub struct ValUnitSet(u8); + +impl ValUnitSet { + /// The empty set. + pub const EMPTY: Self = Self(0); + + /// The number of units in the set. + pub const fn len(&self) -> usize { + self.0.count_ones() as usize + } + + /// Whether the set has no units. + pub const fn is_empty(&self) -> bool { + self.0 == 0 + } + + /// Whether `unit` is in the set. + pub const fn contains(&self, unit: ValUnit) -> bool { + self.0 & unit.bit() != 0 + } + + /// The position `unit` has, or would have, among the set's units in ascending order. + pub const fn slot(&self, unit: ValUnit) -> usize { + (self.0 & (unit.bit() - 1)).count_ones() as usize + } + + /// The units in ascending order. + pub fn iter(self) -> impl Iterator { + ValUnit::ALL + .into_iter() + .filter(move |unit| self.contains(*unit)) + } + + const fn insert(&mut self, unit: ValUnit) { + self.0 |= unit.bit(); + } +} + +impl core::fmt::Debug for ValUnitSet { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_set().entries(self.iter()).finish() + } +} + +/// Working copy of the terms of a [`Val::Sum`], [`Val::Min`] or [`Val::Max`]. +#[derive(Clone, Copy)] +pub(crate) struct ValTerms { + units: ValUnitSet, + values: [f32; 3], +} + +impl ValTerms { + /// The maximum number of terms. + pub(crate) const CAPACITY: usize = 3; + + pub(crate) const fn new(units: ValUnitSet, values: [f32; 3]) -> Self { + Self { units, values } + } + + pub(crate) const fn into_parts(self) -> (ValUnitSet, [f32; 3]) { + (self.units, self.values) + } + + const fn single(unit: ValUnit, value: f32) -> Self { + let mut units = ValUnitSet::EMPTY; + units.insert(unit); + Self::new(units, [value, 0.0, 0.0]) + } + + /// The terms in ascending unit order. + pub(crate) fn terms(self) -> impl Iterator { + self.units.iter().zip(self.values) + } + + /// Folds `value` into the existing term for `unit` with `op`, or inserts it as a new term. + /// + /// Returns `false` and leaves `self` unchanged when a new term would exceed [`Self::CAPACITY`]. + fn fold(&mut self, unit: ValUnit, value: f32, op: fn(f32, f32) -> f32) -> bool { + let slot = self.units.slot(unit); + if self.units.contains(unit) { + self.values[slot] = op(self.values[slot], value); + } else if self.units.len() < Self::CAPACITY { + self.values[slot..].rotate_right(1); + self.values[slot] = value; + self.units.insert(unit); + } else { + return false; + } + true + } + + /// Removes the terms whose value is zero. + fn retain_nonzero(&mut self) { + let mut kept = Self::new(ValUnitSet::EMPTY, [0.0; 3]); + for (unit, value) in self.terms().filter(|(_, value)| *value != 0.0) { + kept.values[kept.units.len()] = value; + kept.units.insert(unit); + } + *self = kept; + } + + pub(crate) fn resolve( + &self, + op: fn(f32, f32) -> f32, + scale_factor: f32, + physical_base_value: f32, + physical_target_size: Vec2, + em_size: EmSize, + rem_size: RemSize, + ) -> f32 { + self.terms() + .map(|(unit, value)| { + Val::leaf(unit, value) + .resolve( + scale_factor, + physical_base_value, + physical_target_size, + em_size, + rem_size, + ) + .unwrap_or(0.0) + }) + .reduce(op) + .unwrap_or(0.0) + } + + /// Interpolates term-wise, or `None` when the units differ. + pub(crate) fn interpolate_stable(&self, other: &Self, t: f32) -> Option { + (self.units == other.units).then(|| { + let mut result = *self; + for (value, other) in result.values.iter_mut().zip(other.values) { + *value = value.interpolate_stable(&other, t); + } + result + }) + } +} + +impl PartialEq for ValTerms { + fn eq(&self, other: &Self) -> bool { + self.units == other.units && self.terms().eq(other.terms()) + } +} + +/// The operation a [`Val::Sum`], [`Val::Min`] or [`Val::Max`] applies to its terms. +#[derive(Clone, Copy)] +pub(crate) enum TermOp { + Sum, + Min, + Max, +} + +impl TermOp { + pub(crate) fn apply(self) -> fn(f32, f32) -> f32 { + match self { + Self::Sum => |a, b| a + b, + Self::Min => f32::min, + Self::Max => f32::max, + } + } + + /// The operation that gives the same result after every term is negated. + const fn reversed(self) -> Self { + match self { + Self::Sum => Self::Sum, + Self::Min => Self::Max, + Self::Max => Self::Min, + } + } + + /// The [`Val`] for `terms` under this operation, as a single unit value when there is one term. + fn wrap(self, terms: ValTerms) -> Val { + if terms.units.len() == 1 + && let Some((unit, value)) = terms.terms().next() + { + return Val::leaf(unit, value); + } + match self { + Self::Sum => Val::Sum(terms.units, terms.values), + Self::Min => Val::Min(terms.units, terms.values), + Self::Max => Val::Max(terms.units, terms.values), + } + } + + fn calc(self, a: Val, b: Val) -> Val { + match self { + Self::Sum => CalcOp::Add(a, b), + Self::Min => CalcOp::Min(a, b), + Self::Max => CalcOp::Max(a, b), + } + .into() + } +} + +impl Val { + /// The terms of a [`Val::Sum`], [`Val::Min`] or [`Val::Max`], or the single term of a + /// unit value. Empty for [`Val::Auto`] and [`Val::Calc`]. + pub fn terms(&self) -> impl Iterator { + let terms = match self { + Val::Sum(units, values) | Val::Min(units, values) | Val::Max(units, values) => { + ValTerms::new(*units, *values) + } + _ => match (self.unit(), self.inner()) { + (Some(unit), Some(value)) => ValTerms::single(unit, value), + _ => ValTerms::new(ValUnitSet::EMPTY, [0.0; 3]), + }, + }; + terms.terms() + } + + /// Combines two values with `op`, folding them into one set of terms when both sides are + /// unit values or terms of the same operation, and building a [`Calc`] otherwise. + pub(crate) fn combine(self, rhs: Val, op: TermOp) -> Val { + if let (Some(mut terms), Some(rhs_terms)) = (self.terms_for(op), rhs.terms_for(op)) + && rhs_terms + .terms() + .all(|(unit, value)| terms.fold(unit, value, op.apply())) + { + return op.wrap(terms); + } + op.calc(self, rhs) + } + + fn terms_for(&self, op: TermOp) -> Option { + match (self, op) { + (Val::Sum(units, values), TermOp::Sum) + | (Val::Min(units, values), TermOp::Min) + | (Val::Max(units, values), TermOp::Max) => Some(ValTerms::new(*units, *values)), + _ => Some(ValTerms::single(self.unit()?, self.inner()?)), + } + } + + /// The terms of a [`Val::Sum`] or unit value with zero terms removed, so that sums + /// compare equal whatever zero terms they carry. `None` for other values. + pub(crate) fn sum_terms(&self) -> Option { + let mut terms = match self { + Val::Sum(units, values) => ValTerms::new(*units, *values), + _ => ValTerms::single(self.unit()?, self.inner()?), + }; + terms.retain_nonzero(); + Some(terms) + } + + /// Applies `f` to every term of a [`Val::Sum`], [`Val::Min`] or [`Val::Max`]. + /// `reverses` swaps `Min` and `Max`, for an `f` that reverses ordering. + pub(crate) fn map_terms(self, f: impl Fn(f32) -> f32, reverses: bool) -> Self { + let (op, units, mut values) = match self { + Val::Sum(units, values) => (TermOp::Sum, units, values), + Val::Min(units, values) => (TermOp::Min, units, values), + Val::Max(units, values) => (TermOp::Max, units, values), + other => return other, + }; + for value in &mut values[..units.len()] { + *value = f(*value); + } + let op = if reverses { op.reversed() } else { op }; + op.wrap(ValTerms::new(units, values)) + } +} + #[cfg(test)] mod tests { use super::*; + #[test] + fn val_fits_in_two_words() { + assert!(size_of::() <= 16); + } + + #[test] + fn sums_fold_inline() { + let sum = Val::Px(10.0) + Val::Percent(50.0); + assert!(matches!(sum, Val::Sum(..)), "got {sum:?}"); + assert_eq!( + sum.terms().collect::>(), + [(ValUnit::Px, 10.0), (ValUnit::Percent, 50.0)] + ); + assert_eq!(Val::Percent(50.0) + Val::Px(10.0), sum); + assert_eq!(Val::Px(1.0) + Val::Px(2.0), Val::Px(3.0)); + assert_eq!(sum.clone() - Val::Px(10.0), Val::Percent(50.0)); + assert_eq!(Val::Px(0.0) + Val::Percent(0.0), Val::ZERO); + assert_ne!(sum, Val::Px(10.0)); + assert_eq!( + (Val::Px(1.0) + Val::Percent(2.0)) + (Val::Px(3.0) + Val::Em(4.0)), + Val::Em(4.0) + Val::Px(4.0) + Val::Percent(2.0) + ); + assert!(matches!(sum + Val::Em(1.0) + Val::Rem(1.0), Val::Calc(_))); + } + + #[test] + fn min_and_max_fold_inline() { + assert_eq!(Val::Px(10.0).min(Val::Px(20.0)), Val::Px(10.0)); + assert_eq!(Val::Px(10.0).max(Val::Px(20.0)), Val::Px(20.0)); + let min = Val::Px(10.0).min(Val::Percent(50.0)); + assert!(matches!(min, Val::Min(..))); + assert_eq!(Val::Percent(50.0).min(Val::Px(10.0)), min); + assert_eq!(-min.clone(), Val::Px(-10.0).max(Val::Percent(-50.0))); + assert!(matches!(min.clone().max(Val::Px(5.0)), Val::Calc(_))); + let resolve = + |value: &Val| value.resolve(1.0, 100.0, Vec2::ZERO, EmSize(0.0), RemSize(0.0)); + assert_eq!(resolve(&min), Ok(10.0)); + assert_eq!(resolve(&Val::Px(10.0).max(Val::Percent(50.0))), Ok(50.0)); + assert_eq!(resolve(&(min * -2.0)), Ok(-20.0)); + } + + #[test] + fn terms_interpolate_term_wise() { + use bevy_math::TryStableInterpolate; + let a = Val::Px(0.0) + Val::Percent(0.0); + let b = Val::Px(10.0) + Val::Percent(100.0); + assert_eq!( + a.try_interpolate_stable(&b, 0.5).ok(), + Some(Val::Px(5.0) + Val::Percent(50.0)) + ); + assert!(a + .try_interpolate_stable(&(Val::Px(1.0) + Val::Em(1.0)), 0.5) + .is_err()); + } + #[test] fn resolve_expressions() { let value = diff --git a/crates/bevy_ui/src/geometry.rs b/crates/bevy_ui/src/geometry.rs index 0f27f1763cc58..bfc8ef0846bdc 100644 --- a/crates/bevy_ui/src/geometry.rs +++ b/crates/bevy_ui/src/geometry.rs @@ -9,7 +9,7 @@ use thiserror::Error; #[cfg(feature = "serialize")] use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; -use crate::calc::{Calc, CalcOp}; +use crate::calc::{Calc, CalcOp, TermOp, ValTerms, ValUnit, ValUnitSet}; /// Represents the possible value types for layout properties. /// @@ -68,6 +68,17 @@ pub enum Val { /// Unlike [`Val::Em`] this ignores the node's own font size, so it means the same /// length anywhere in the hierarchy. Resolved from the [`RemSize`] resource. Rem(f32), + /// The sum of up to three terms with distinct units, such as `Val::Percent(50.0) - Val::Px(10.0)`. + /// + /// The `n`th value belongs to the `n`th unit of the set, see [`ValUnitSet`]. + /// Built by `+` and `-`; longer expressions fall back to [`Val::Calc`]. + Sum(ValUnitSet, [f32; 3]), + /// The smallest of up to three terms with distinct units, laid out like [`Val::Sum`]. + /// Built by [`Val::min`]. + Min(ValUnitSet, [f32; 3]), + /// The largest of up to three terms with distinct units, laid out like [`Val::Sum`]. + /// Built by [`Val::max`]. + Max(ValUnitSet, [f32; 3]), Calc(Calc), } @@ -141,8 +152,15 @@ impl core::str::FromStr for Val { impl PartialEq for Val { fn eq(&self, other: &Self) -> bool { - if let (Self::Calc(a), Self::Calc(b)) = (self, other) { - return a == b; + match (self, other) { + (Self::Calc(a), Self::Calc(b)) => return a == b, + (Self::Min(a, x), Self::Min(b, y)) | (Self::Max(a, x), Self::Max(b, y)) => { + return ValTerms::new(*a, *x) == ValTerms::new(*b, *y); + } + (Self::Sum(..), _) | (_, Self::Sum(..)) => { + return self.sum_terms() == other.sum_terms(); + } + _ => {} } let same_unit = matches!( (self, other), @@ -180,13 +198,13 @@ impl Val { /// The largest finite [`Val::Px`] value. pub const MAX: Self = Self::Px(f32::MAX); - /// Returns the number this `Val` wraps, whatever its unit, or `None` for [`Val::Auto`] and [`Val::Calc`]. + /// Returns the number this `Val` wraps, whatever its unit, or `None` for [`Val::Auto`] and the expression variants. /// /// The number is meaningless without the variant it came from — `Val::Px(4.0)` and /// `Val::Rem(4.0)` both return `4.0`. Use [`Val::resolve`] for a length in pixels. - const fn inner(&self) -> Option { + pub(crate) const fn inner(&self) -> Option { match self { - Self::Auto | Self::Calc(_) => None, + Self::Auto | Self::Sum(..) | Self::Min(..) | Self::Max(..) | Self::Calc(_) => None, Self::Px(v) | Self::Percent(v) | Self::Vw(v) @@ -198,6 +216,48 @@ impl Val { } } + /// The unit of a single-term value, or `None` for [`Val::Auto`] and the expression variants. + pub const fn unit(&self) -> Option { + Some(match self { + Self::Px(_) => ValUnit::Px, + Self::Percent(_) => ValUnit::Percent, + Self::Vw(_) => ValUnit::Vw, + Self::Vh(_) => ValUnit::Vh, + Self::VMin(_) => ValUnit::VMin, + Self::VMax(_) => ValUnit::VMax, + Self::Em(_) => ValUnit::Em, + Self::Rem(_) => ValUnit::Rem, + Self::Auto | Self::Sum(..) | Self::Min(..) | Self::Max(..) | Self::Calc(_) => { + return None; + } + }) + } + + /// A single-term value of the given `unit`. + pub const fn leaf(unit: ValUnit, value: f32) -> Self { + match unit { + ValUnit::Px => Self::Px(value), + ValUnit::Percent => Self::Percent(value), + ValUnit::Vw => Self::Vw(value), + ValUnit::Vh => Self::Vh(value), + ValUnit::VMin => Self::VMin(value), + ValUnit::VMax => Self::VMax(value), + ValUnit::Em => Self::Em(value), + ValUnit::Rem => Self::Rem(value), + } + } + + pub(crate) fn uses_percent(&self) -> bool { + match self { + Self::Percent(_) => true, + Self::Sum(units, _) | Self::Min(units, _) | Self::Max(units, _) => { + units.contains(ValUnit::Percent) + } + Self::Calc(calc) => calc.0.uses_percent(), + _ => false, + } + } + /// Returns a [`UiRect`] with its `left` equal to this value, /// and all other fields set to `Val::ZERO`. /// @@ -386,12 +446,14 @@ impl Val { } } - pub fn min(self, min: Val) -> Val { - CalcOp::Min(self, min).into() + /// The smaller of `self` and `other` once resolved. + pub fn min(self, other: Val) -> Val { + self.combine(other, TermOp::Min) } - pub fn max(self, max: Val) -> Val { - CalcOp::Max(self, max).into() + /// The larger of `self` and `other` once resolved. + pub fn max(self, other: Val) -> Val { + self.combine(other, TermOp::Max) } pub fn clamp(self, min: Val, max: Val) -> Val { @@ -409,7 +471,7 @@ impl Add for Val { type Output = Self; fn add(self, rhs: Self) -> Self { - CalcOp::Add(self, rhs).into() + self.combine(rhs, TermOp::Sum) } } @@ -417,7 +479,7 @@ impl Sub for Val { type Output = Self; fn sub(self, rhs: Self) -> Self { - CalcOp::Sub(self, rhs).into() + self.combine(-rhs, TermOp::Sum) } } @@ -435,6 +497,9 @@ impl Mul for Val { Val::VMax(value) => Val::VMax(value * rhs), Val::Em(value) => Val::Em(value * rhs), Val::Rem(value) => Val::Rem(value * rhs), + terms @ (Val::Sum(..) | Val::Min(..) | Val::Max(..)) => { + terms.map_terms(|value| value * rhs, rhs < 0.0) + } Val::Calc(mut calc) => { Arc::make_mut(&mut calc.0).scale(rhs); Val::Calc(calc) @@ -455,6 +520,9 @@ impl MulAssign for Val { | Val::VMax(value) | Val::Em(value) | Val::Rem(value) => *value *= rhs, + Val::Sum(..) | Val::Min(..) | Val::Max(..) => { + *self = core::mem::take(self).map_terms(|value| value * rhs, rhs < 0.0); + } Val::Calc(calc) => Arc::make_mut(&mut calc.0).scale(rhs), } } @@ -474,6 +542,9 @@ impl Div for Val { Val::VMax(value) => Val::VMax(value / rhs), Val::Em(value) => Val::Em(value / rhs), Val::Rem(value) => Val::Rem(value / rhs), + terms @ (Val::Sum(..) | Val::Min(..) | Val::Max(..)) => { + terms.map_terms(|value| value / rhs, rhs < 0.0) + } Val::Calc(mut calc) => { Arc::make_mut(&mut calc.0).scale(rhs.recip()); Val::Calc(calc) @@ -494,6 +565,9 @@ impl DivAssign for Val { | Val::VMax(value) | Val::Em(value) | Val::Rem(value) => *value /= rhs, + Val::Sum(..) | Val::Min(..) | Val::Max(..) => { + *self = core::mem::take(self).map_terms(|value| value / rhs, rhs < 0.0); + } Val::Calc(calc) => Arc::make_mut(&mut calc.0).scale(rhs.recip()), } } @@ -513,6 +587,9 @@ impl Neg for Val { Val::VMax(value) => Val::VMax(-value), Val::Em(value) => Val::Em(-value), Val::Rem(value) => Val::Rem(-value), + terms @ (Val::Sum(..) | Val::Min(..) | Val::Max(..)) => { + terms.map_terms(|value| -value, true) + } Val::Calc(mut calc) => { Arc::make_mut(&mut calc.0).scale(-1.0); Val::Calc(calc) @@ -556,6 +633,21 @@ impl Val { Val::Em(value) => Ok(value * em_size.0 * scale_factor), Val::Rem(value) => Ok(value * rem_size.0 * scale_factor), Val::Auto => Err(ValArithmeticError::NonEvaluable), + Val::Sum(units, values) | Val::Min(units, values) | Val::Max(units, values) => { + let op = match self { + Val::Min(..) => TermOp::Min, + Val::Max(..) => TermOp::Max, + _ => TermOp::Sum, + }; + Ok(ValTerms::new(*units, *values).resolve( + op.apply(), + scale_factor, + physical_base_value, + physical_target_size, + em_size, + rem_size, + )) + } Val::Calc(op) => op.0.resolve( scale_factor, physical_base_value, @@ -586,7 +678,9 @@ impl TryFrom for FontSize { Val::VMax(vmax) => FontSize::VMax(vmax), Val::Rem(rem) => FontSize::Rem(rem), Val::Em(em) => FontSize::Rem(em), // see comment above - Val::Calc(_) => return Err(ValArithmeticError::NonEvaluable), + Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Calc(_) => { + return Err(ValArithmeticError::NonEvaluable); + } }) } } @@ -611,6 +705,19 @@ impl TryStableInterpolate for Val { (Val::VMax(a), Val::VMax(b)) => Ok(Val::VMax(a.interpolate_stable(b, t))), (Val::Em(a), Val::Em(b)) => Ok(Val::Em(a.interpolate_stable(b, t))), (Val::Rem(a), Val::Rem(b)) => Ok(Val::Rem(a.interpolate_stable(b, t))), + (Val::Sum(a, x), Val::Sum(b, y)) + | (Val::Min(a, x), Val::Min(b, y)) + | (Val::Max(a, x), Val::Max(b, y)) => { + let (units, values) = ValTerms::new(*a, *x) + .interpolate_stable(&ValTerms::new(*b, *y), t) + .ok_or(MismatchedUnitsError)? + .into_parts(); + Ok(match self { + Val::Min(..) => Val::Min(units, values), + Val::Max(..) => Val::Max(units, values), + _ => Val::Sum(units, values), + }) + } (Val::Auto, Val::Auto) => Ok(Val::Auto), _ => Err(MismatchedUnitsError), } diff --git a/crates/bevy_ui/src/layout/convert.rs b/crates/bevy_ui/src/layout/convert.rs index 0d95bf674d5de..eaa186edc8601 100644 --- a/crates/bevy_ui/src/layout/convert.rs +++ b/crates/bevy_ui/src/layout/convert.rs @@ -21,8 +21,8 @@ impl Val { ) -> taffy::style::LengthPercentageAuto { match self { Val::Auto => style_helpers::auto(), - Val::Calc(calc) => { - if !calc.0.uses_percent() { + Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Calc(_) => { + if !self.uses_percent() { return style_helpers::length( self.resolve( context.scale_factor, @@ -35,7 +35,7 @@ impl Val { ); } let calc = Arc::new(LayoutCalc { - calc: calc.clone(), + val: self.clone(), context: *context, }); let value = taffy::style::LengthPercentageAuto::calc(Arc::as_ptr(&calc).cast()); @@ -67,8 +67,8 @@ impl Val { ) -> taffy::style::LengthPercentage { match self { Val::Auto => style_helpers::length(0.0_f32), - Val::Calc(calc) => { - if !calc.0.uses_percent() { + Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Calc(_) => { + if !self.uses_percent() { return style_helpers::length( self.resolve( context.scale_factor, @@ -81,7 +81,7 @@ impl Val { ); } let calc = Arc::new(LayoutCalc { - calc: calc.clone(), + val: self.clone(), context: *context, }); let value = taffy::style::LengthPercentage::calc(Arc::as_ptr(&calc).cast()); diff --git a/crates/bevy_ui/src/layout/layout_tree.rs b/crates/bevy_ui/src/layout/layout_tree.rs index b18dbd7f8da09..975d07a64272b 100644 --- a/crates/bevy_ui/src/layout/layout_tree.rs +++ b/crates/bevy_ui/src/layout/layout_tree.rs @@ -23,10 +23,9 @@ use taffy::{ }; use crate::{ - calc::Calc, layout::{LayoutContext, UiTreeDirty}, ContentSize, FixedNode, GhostNode, IgnoreScroll, LayoutConfig, LayoutError, Measure, - MeasureArgs, Node, NodeMeasure, Outline, OverrideClip, ScrollPosition, UiTransform, + MeasureArgs, Node, NodeMeasure, Outline, OverrideClip, ScrollPosition, UiTransform, Val, }; #[expect( @@ -48,14 +47,13 @@ pub struct TaffyStyle(#[deref] pub Style, pub(crate) Vec>); #[repr(align(8))] pub(crate) struct LayoutCalc { - pub calc: Calc, + pub val: Val, pub context: LayoutContext, } impl LayoutCalc { pub fn resolve(&self, basis: f32) -> f32 { - self.calc - .0 + self.val .resolve( self.context.scale_factor, basis, diff --git a/crates/bevy_ui_render/src/box_shadow.rs b/crates/bevy_ui_render/src/box_shadow.rs index f8c8384b5a7ff..771c6c0c5589f 100644 --- a/crates/bevy_ui_render/src/box_shadow.rs +++ b/crates/bevy_ui_render/src/box_shadow.rs @@ -315,7 +315,7 @@ pub fn extract_shadows( let resolve_val = |val: &Val, base, scale_factor| match val { Val::Auto => 0., - Val::Calc(_) => val + Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Calc(_) => val .resolve( scale_factor, base, From 55d73a954da2ad5c3e68db73d97893d8bb605e6f Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sun, 1 Nov 2026 20:53:02 +0000 Subject: [PATCH 11/50] Add inline Val::Clamp variant Stores `[min, value, max]` as `[ValUnit; 3]` and `[f32; 3]`, keeping `Val` at 16 bytes. `Val::clamp` builds it when all three operands are unit values and falls back to `CalcOp::Clamp` otherwise. Negation swaps the bounds so it stays a `Clamp`, and it interpolates slot-wise. Co-Authored-By: Claude Fable 5.1 --- crates/bevy_ui/src/calc.rs | 77 ++++++++++++++++++++++++- crates/bevy_ui/src/geometry.rs | 48 ++++++++++++--- crates/bevy_ui/src/layout/convert.rs | 4 +- crates/bevy_ui_render/src/box_shadow.rs | 2 +- 4 files changed, 116 insertions(+), 15 deletions(-) diff --git a/crates/bevy_ui/src/calc.rs b/crates/bevy_ui/src/calc.rs index 8df968ccf25c3..fb9aba81bd1e7 100644 --- a/crates/bevy_ui/src/calc.rs +++ b/crates/bevy_ui/src/calc.rs @@ -110,6 +110,7 @@ impl From for Val { /// Discriminator enum, should match `Val`'s unit types. #[derive(Clone, Copy, PartialEq, Eq, Debug, Reflect)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[repr(u8)] pub enum ValUnit { /// Use `Px` units @@ -351,7 +352,7 @@ impl TermOp { impl Val { /// The terms of a [`Val::Sum`], [`Val::Min`] or [`Val::Max`], or the single term of a - /// unit value. Empty for [`Val::Auto`] and [`Val::Calc`]. + /// unit value. Empty for [`Val::Auto`], [`Val::Clamp`] and [`Val::Calc`]. pub fn terms(&self) -> impl Iterator { let terms = match self { Val::Sum(units, values) | Val::Min(units, values) | Val::Max(units, values) => { @@ -398,13 +399,51 @@ impl Val { Some(terms) } - /// Applies `f` to every term of a [`Val::Sum`], [`Val::Min`] or [`Val::Max`]. - /// `reverses` swaps `Min` and `Max`, for an `f` that reverses ordering. + /// The inline clamp of `value` between `min` and `max`, when all three are unit values. + pub(crate) fn clamp_inline(min: &Val, value: &Val, max: &Val) -> Option { + let term = |val: &Val| Some((val.unit()?, val.inner()?)); + let [lo, v, hi] = [term(min)?, term(value)?, term(max)?]; + Some(Val::Clamp([lo.0, v.0, hi.0], [lo.1, v.1, hi.1])) + } + + pub(crate) fn resolve_clamp( + units: [ValUnit; 3], + values: [f32; 3], + scale_factor: f32, + physical_base_value: f32, + physical_target_size: Vec2, + em_size: EmSize, + rem_size: RemSize, + ) -> f32 { + let [lo, v, hi] = core::array::from_fn(|i| { + Val::leaf(units[i], values[i]) + .resolve( + scale_factor, + physical_base_value, + physical_target_size, + em_size, + rem_size, + ) + .unwrap_or(0.0) + }); + lo.max(v.min(hi)) + } + + /// Applies `f` to every term of a [`Val::Sum`], [`Val::Min`], [`Val::Max`] or [`Val::Clamp`]. + /// `reverses` swaps `Min` and `Max` and the bounds of `Clamp`, for an `f` that reverses ordering. pub(crate) fn map_terms(self, f: impl Fn(f32) -> f32, reverses: bool) -> Self { let (op, units, mut values) = match self { Val::Sum(units, values) => (TermOp::Sum, units, values), Val::Min(units, values) => (TermOp::Min, units, values), Val::Max(units, values) => (TermOp::Max, units, values), + Val::Clamp(mut units, values) => { + let mut values = values.map(&f); + if reverses { + units.swap(0, 2); + values.swap(0, 2); + } + return Val::Clamp(units, values); + } other => return other, }; for value in &mut values[..units.len()] { @@ -460,6 +499,38 @@ mod tests { assert_eq!(resolve(&(min * -2.0)), Ok(-20.0)); } + #[test] + fn clamp_folds_inline() { + use bevy_math::TryStableInterpolate; + let clamp = Val::Percent(80.0).clamp(Val::Px(100.0), Val::Px(300.0)); + assert!(matches!(clamp, Val::Clamp(..)), "got {clamp:?}"); + let resolve = |value: &Val, base: f32| { + value.resolve(1.0, base, Vec2::ZERO, EmSize(0.0), RemSize(0.0)) + }; + assert_eq!(resolve(&clamp, 100.0), Ok(100.0)); + assert_eq!(resolve(&clamp, 250.0), Ok(200.0)); + assert_eq!(resolve(&clamp, 1000.0), Ok(300.0)); + assert_eq!( + -clamp.clone(), + Val::Percent(-80.0).clamp(Val::Px(-300.0), Val::Px(-100.0)) + ); + assert_eq!(resolve(&(clamp.clone() * -2.0), 250.0), Ok(-400.0)); + assert_eq!( + clamp.clone() * 2.0, + Val::Percent(160.0).clamp(Val::Px(200.0), Val::Px(600.0)) + ); + assert_eq!( + clamp + .try_interpolate_stable(&Val::Percent(0.0).clamp(Val::Px(0.0), Val::Px(100.0)), 0.5) + .ok(), + Some(Val::Percent(40.0).clamp(Val::Px(50.0), Val::Px(200.0))) + ); + assert!(matches!( + (Val::Percent(80.0) + Val::Px(1.0)).clamp(Val::Px(100.0), Val::Px(300.0)), + Val::Calc(_) + )); + } + #[test] fn terms_interpolate_term_wise() { use bevy_math::TryStableInterpolate; diff --git a/crates/bevy_ui/src/geometry.rs b/crates/bevy_ui/src/geometry.rs index bfc8ef0846bdc..d55a074a00b56 100644 --- a/crates/bevy_ui/src/geometry.rs +++ b/crates/bevy_ui/src/geometry.rs @@ -79,6 +79,10 @@ pub enum Val { /// The largest of up to three terms with distinct units, laid out like [`Val::Sum`]. /// Built by [`Val::max`]. Max(ValUnitSet, [f32; 3]), + /// A value clamped between a minimum and a maximum, each a unit value, stored as the + /// `[min, value, max]` units and values. Built by [`Val::clamp`]; when an operand is itself + /// an expression it falls back to [`Val::Calc`]. + Clamp([ValUnit; 3], [f32; 3]), Calc(Calc), } @@ -157,6 +161,7 @@ impl PartialEq for Val { (Self::Min(a, x), Self::Min(b, y)) | (Self::Max(a, x), Self::Max(b, y)) => { return ValTerms::new(*a, *x) == ValTerms::new(*b, *y); } + (Self::Clamp(a, x), Self::Clamp(b, y)) => return a == b && x == y, (Self::Sum(..), _) | (_, Self::Sum(..)) => { return self.sum_terms() == other.sum_terms(); } @@ -204,7 +209,12 @@ impl Val { /// `Val::Rem(4.0)` both return `4.0`. Use [`Val::resolve`] for a length in pixels. pub(crate) const fn inner(&self) -> Option { match self { - Self::Auto | Self::Sum(..) | Self::Min(..) | Self::Max(..) | Self::Calc(_) => None, + Self::Auto + | Self::Sum(..) + | Self::Min(..) + | Self::Max(..) + | Self::Clamp(..) + | Self::Calc(_) => None, Self::Px(v) | Self::Percent(v) | Self::Vw(v) @@ -227,7 +237,12 @@ impl Val { Self::VMax(_) => ValUnit::VMax, Self::Em(_) => ValUnit::Em, Self::Rem(_) => ValUnit::Rem, - Self::Auto | Self::Sum(..) | Self::Min(..) | Self::Max(..) | Self::Calc(_) => { + Self::Auto + | Self::Sum(..) + | Self::Min(..) + | Self::Max(..) + | Self::Clamp(..) + | Self::Calc(_) => { return None; } }) @@ -253,6 +268,7 @@ impl Val { Self::Sum(units, _) | Self::Min(units, _) | Self::Max(units, _) => { units.contains(ValUnit::Percent) } + Self::Clamp(units, _) => units.contains(&ValUnit::Percent), Self::Calc(calc) => calc.0.uses_percent(), _ => false, } @@ -456,8 +472,9 @@ impl Val { self.combine(other, TermOp::Max) } + /// `self` clamped between `min` and `max` once resolved. pub fn clamp(self, min: Val, max: Val) -> Val { - CalcOp::Clamp(min, self, max).into() + Val::clamp_inline(&min, &self, &max).unwrap_or_else(|| CalcOp::Clamp(min, self, max).into()) } } @@ -497,7 +514,7 @@ impl Mul for Val { Val::VMax(value) => Val::VMax(value * rhs), Val::Em(value) => Val::Em(value * rhs), Val::Rem(value) => Val::Rem(value * rhs), - terms @ (Val::Sum(..) | Val::Min(..) | Val::Max(..)) => { + terms @ (Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Clamp(..)) => { terms.map_terms(|value| value * rhs, rhs < 0.0) } Val::Calc(mut calc) => { @@ -520,7 +537,7 @@ impl MulAssign for Val { | Val::VMax(value) | Val::Em(value) | Val::Rem(value) => *value *= rhs, - Val::Sum(..) | Val::Min(..) | Val::Max(..) => { + Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Clamp(..) => { *self = core::mem::take(self).map_terms(|value| value * rhs, rhs < 0.0); } Val::Calc(calc) => Arc::make_mut(&mut calc.0).scale(rhs), @@ -542,7 +559,7 @@ impl Div for Val { Val::VMax(value) => Val::VMax(value / rhs), Val::Em(value) => Val::Em(value / rhs), Val::Rem(value) => Val::Rem(value / rhs), - terms @ (Val::Sum(..) | Val::Min(..) | Val::Max(..)) => { + terms @ (Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Clamp(..)) => { terms.map_terms(|value| value / rhs, rhs < 0.0) } Val::Calc(mut calc) => { @@ -565,7 +582,7 @@ impl DivAssign for Val { | Val::VMax(value) | Val::Em(value) | Val::Rem(value) => *value /= rhs, - Val::Sum(..) | Val::Min(..) | Val::Max(..) => { + Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Clamp(..) => { *self = core::mem::take(self).map_terms(|value| value / rhs, rhs < 0.0); } Val::Calc(calc) => Arc::make_mut(&mut calc.0).scale(rhs.recip()), @@ -587,7 +604,7 @@ impl Neg for Val { Val::VMax(value) => Val::VMax(-value), Val::Em(value) => Val::Em(-value), Val::Rem(value) => Val::Rem(-value), - terms @ (Val::Sum(..) | Val::Min(..) | Val::Max(..)) => { + terms @ (Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Clamp(..)) => { terms.map_terms(|value| -value, true) } Val::Calc(mut calc) => { @@ -648,6 +665,15 @@ impl Val { rem_size, )) } + Val::Clamp(units, values) => Ok(Val::resolve_clamp( + *units, + *values, + scale_factor, + physical_base_value, + physical_target_size, + em_size, + rem_size, + )), Val::Calc(op) => op.0.resolve( scale_factor, physical_base_value, @@ -678,7 +704,7 @@ impl TryFrom for FontSize { Val::VMax(vmax) => FontSize::VMax(vmax), Val::Rem(rem) => FontSize::Rem(rem), Val::Em(em) => FontSize::Rem(em), // see comment above - Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Calc(_) => { + Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Clamp(..) | Val::Calc(_) => { return Err(ValArithmeticError::NonEvaluable); } }) @@ -718,6 +744,10 @@ impl TryStableInterpolate for Val { _ => Val::Sum(units, values), }) } + (Val::Clamp(a, x), Val::Clamp(b, y)) if a == b => Ok(Val::Clamp( + *a, + core::array::from_fn(|i| x[i].interpolate_stable(&y[i], t)), + )), (Val::Auto, Val::Auto) => Ok(Val::Auto), _ => Err(MismatchedUnitsError), } diff --git a/crates/bevy_ui/src/layout/convert.rs b/crates/bevy_ui/src/layout/convert.rs index eaa186edc8601..e2992747afb2e 100644 --- a/crates/bevy_ui/src/layout/convert.rs +++ b/crates/bevy_ui/src/layout/convert.rs @@ -21,7 +21,7 @@ impl Val { ) -> taffy::style::LengthPercentageAuto { match self { Val::Auto => style_helpers::auto(), - Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Calc(_) => { + Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Clamp(..) | Val::Calc(_) => { if !self.uses_percent() { return style_helpers::length( self.resolve( @@ -67,7 +67,7 @@ impl Val { ) -> taffy::style::LengthPercentage { match self { Val::Auto => style_helpers::length(0.0_f32), - Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Calc(_) => { + Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Clamp(..) | Val::Calc(_) => { if !self.uses_percent() { return style_helpers::length( self.resolve( diff --git a/crates/bevy_ui_render/src/box_shadow.rs b/crates/bevy_ui_render/src/box_shadow.rs index 771c6c0c5589f..5ecd979ad0dc3 100644 --- a/crates/bevy_ui_render/src/box_shadow.rs +++ b/crates/bevy_ui_render/src/box_shadow.rs @@ -315,7 +315,7 @@ pub fn extract_shadows( let resolve_val = |val: &Val, base, scale_factor| match val { Val::Auto => 0., - Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Calc(_) => val + Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Clamp(..) | Val::Calc(_) => val .resolve( scale_factor, base, From 345a088721791fca571f405e8c55d6d810b5d72b Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sun, 1 Nov 2026 22:06:59 +0000 Subject: [PATCH 12/50] Added a check that val units count is <= 8. --- crates/bevy_ui/src/calc.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/bevy_ui/src/calc.rs b/crates/bevy_ui/src/calc.rs index fb9aba81bd1e7..9ec7edf777b68 100644 --- a/crates/bevy_ui/src/calc.rs +++ b/crates/bevy_ui/src/calc.rs @@ -144,6 +144,9 @@ impl ValUnit { } } +/// If more unit types are added to `Val`, a `u8` discriminator won't be sufficient. +const _: () = assert!(ValUnit::ALL.len() <= u8::BITS as usize); + /// A set of [`ValUnit`]s. /// /// [`Val::Sum`], [`Val::Min`] and [`Val::Max`] pair one of these with an array of values: From 021e1b562f5ada4df9d56710b0f1eac4cdb3c624 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sun, 1 Nov 2026 22:11:59 +0000 Subject: [PATCH 13/50] Renamed `Val` methods in convert. Since they now take &self, the convention is to use a `to_` prefix. --- crates/bevy_ui/src/layout/convert.rs | 82 ++++++++++++++-------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/crates/bevy_ui/src/layout/convert.rs b/crates/bevy_ui/src/layout/convert.rs index e2992747afb2e..0b2e48fedc459 100644 --- a/crates/bevy_ui/src/layout/convert.rs +++ b/crates/bevy_ui/src/layout/convert.rs @@ -14,7 +14,7 @@ use super::{ }; impl Val { - pub(super) fn into_length_percentage_auto( + pub(super) fn to_length_percentage_auto( &self, context: &LayoutContext, calcs: &mut Vec>, @@ -60,7 +60,7 @@ impl Val { } } - pub(super) fn into_length_percentage( + pub(super) fn to_length_percentage( &self, context: &LayoutContext, calcs: &mut Vec>, @@ -113,12 +113,12 @@ impl Val { // // * A non-zero border smaller than one physical pixel is rounded up to one pixel. // * A border larger than one physical pixel is rounded down to a whole pixel. - fn into_length_percentage_for_border( + fn to_length_percentage_for_border( &self, context: &LayoutContext, calcs: &mut Vec>, ) -> taffy::style::LengthPercentage { - let length = self.into_length_percentage(context, calcs); + let length = self.to_length_percentage(context, calcs); let raw = length.into_raw(); // we're only snapping if sized in non-zero px if raw.tag() == taffy::style::CompactLength::LENGTH_TAG && raw.value() > 0.0 { @@ -128,12 +128,12 @@ impl Val { } } - fn into_dimension( + fn to_dimension( &self, context: &LayoutContext, calcs: &mut Vec>, ) -> taffy::style::Dimension { - self.into_length_percentage_auto(context, calcs).into() + self.to_length_percentage_auto(context, calcs).into() } } @@ -179,40 +179,40 @@ pub fn update_taffy_style_from_node( style.justify_content = node.justify_content.into(); style.direction = node.direction.into(); style.inset = taffy::Rect { - left: node.left.into_length_percentage_auto(context, calcs), - right: node.right.into_length_percentage_auto(context, calcs), - top: node.top.into_length_percentage_auto(context, calcs), - bottom: node.bottom.into_length_percentage_auto(context, calcs), + left: node.left.to_length_percentage_auto(context, calcs), + right: node.right.to_length_percentage_auto(context, calcs), + top: node.top.to_length_percentage_auto(context, calcs), + bottom: node.bottom.to_length_percentage_auto(context, calcs), }; style.margin = node .margin - .map_to_taffy_rect(|m| m.into_length_percentage_auto(context, calcs)); + .map_to_taffy_rect(|m| m.to_length_percentage_auto(context, calcs)); style.padding = node .padding - .map_to_taffy_rect(|m| m.into_length_percentage(context, calcs)); + .map_to_taffy_rect(|m| m.to_length_percentage(context, calcs)); // border floors >=1px sized to nearest (>=1) real pixel size, see `into_length_percentage_for_border`. style.border = node .border - .map_to_taffy_rect(|m| m.into_length_percentage_for_border(context, calcs)); + .map_to_taffy_rect(|m| m.to_length_percentage_for_border(context, calcs)); style.flex_grow = node.flex_grow; style.flex_shrink = node.flex_shrink; - style.flex_basis = node.flex_basis.into_dimension(context, calcs); + style.flex_basis = node.flex_basis.to_dimension(context, calcs); style.size = taffy::Size { - width: node.width.into_dimension(context, calcs), - height: node.height.into_dimension(context, calcs), + width: node.width.to_dimension(context, calcs), + height: node.height.to_dimension(context, calcs), }; style.min_size = taffy::Size { - width: node.min_width.into_length_percentage_auto(context, calcs), - height: node.min_height.into_length_percentage_auto(context, calcs), + width: node.min_width.to_length_percentage_auto(context, calcs), + height: node.min_height.to_length_percentage_auto(context, calcs), }; style.max_size = taffy::Size { - width: node.max_width.into_length_percentage_auto(context, calcs), - height: node.max_height.into_length_percentage_auto(context, calcs), + width: node.max_width.to_length_percentage_auto(context, calcs), + height: node.max_height.to_length_percentage_auto(context, calcs), }; style.aspect_ratio = node.aspect_ratio; style.gap = taffy::Size { - width: node.column_gap.into_length_percentage(context, calcs), - height: node.row_gap.into_length_percentage(context, calcs), + width: node.column_gap.to_length_percentage(context, calcs), + height: node.row_gap.to_length_percentage(context, calcs), }; style.grid_auto_flow = node.grid_auto_flow.into(); style.grid_template_rows.clear(); @@ -465,16 +465,16 @@ impl MinTrackSizingFunction { fn into_taffy(self, context: &LayoutContext) -> taffy::style::MinTrackSizingFunction { match self { MinTrackSizingFunction::Px(val) => Val::Px(val) - .into_length_percentage(context, &mut Vec::new()) + .to_length_percentage(context, &mut Vec::new()) .into(), MinTrackSizingFunction::Percent(val) => Val::Percent(val) - .into_length_percentage(context, &mut Vec::new()) + .to_length_percentage(context, &mut Vec::new()) .into(), MinTrackSizingFunction::Em(val) => Val::Em(val) - .into_length_percentage(context, &mut Vec::new()) + .to_length_percentage(context, &mut Vec::new()) .into(), MinTrackSizingFunction::Rem(val) => Val::Rem(val) - .into_length_percentage(context, &mut Vec::new()) + .to_length_percentage(context, &mut Vec::new()) .into(), MinTrackSizingFunction::Auto => taffy::style::MinTrackSizingFunction::auto(), MinTrackSizingFunction::MinContent => { @@ -484,16 +484,16 @@ impl MinTrackSizingFunction { taffy::style::MinTrackSizingFunction::max_content() } MinTrackSizingFunction::VMin(val) => Val::VMin(val) - .into_length_percentage(context, &mut Vec::new()) + .to_length_percentage(context, &mut Vec::new()) .into(), MinTrackSizingFunction::VMax(val) => Val::VMax(val) - .into_length_percentage(context, &mut Vec::new()) + .to_length_percentage(context, &mut Vec::new()) .into(), MinTrackSizingFunction::Vh(val) => Val::Vh(val) - .into_length_percentage(context, &mut Vec::new()) + .to_length_percentage(context, &mut Vec::new()) .into(), MinTrackSizingFunction::Vw(val) => Val::Vw(val) - .into_length_percentage(context, &mut Vec::new()) + .to_length_percentage(context, &mut Vec::new()) .into(), } } @@ -503,16 +503,16 @@ impl MaxTrackSizingFunction { fn into_taffy(self, context: &LayoutContext) -> taffy::style::MaxTrackSizingFunction { match self { MaxTrackSizingFunction::Px(val) => Val::Px(val) - .into_length_percentage(context, &mut Vec::new()) + .to_length_percentage(context, &mut Vec::new()) .into(), MaxTrackSizingFunction::Percent(val) => Val::Percent(val) - .into_length_percentage(context, &mut Vec::new()) + .to_length_percentage(context, &mut Vec::new()) .into(), MaxTrackSizingFunction::Em(val) => Val::Em(val) - .into_length_percentage(context, &mut Vec::new()) + .to_length_percentage(context, &mut Vec::new()) .into(), MaxTrackSizingFunction::Rem(val) => Val::Rem(val) - .into_length_percentage(context, &mut Vec::new()) + .to_length_percentage(context, &mut Vec::new()) .into(), MaxTrackSizingFunction::Auto => taffy::style::MaxTrackSizingFunction::auto(), MaxTrackSizingFunction::MinContent => { @@ -524,7 +524,7 @@ impl MaxTrackSizingFunction { MaxTrackSizingFunction::FitContentPx(val) => { taffy::style::MaxTrackSizingFunction::fit_content_px( Val::Px(val) - .into_length_percentage(context, &mut Vec::new()) + .to_length_percentage(context, &mut Vec::new()) .into_raw() .value(), ) @@ -532,7 +532,7 @@ impl MaxTrackSizingFunction { MaxTrackSizingFunction::FitContentPercent(val) => { taffy::style::MaxTrackSizingFunction::fit_content_percent( Val::Percent(val) - .into_length_percentage(context, &mut Vec::new()) + .to_length_percentage(context, &mut Vec::new()) .into_raw() .value(), ) @@ -541,16 +541,16 @@ impl MaxTrackSizingFunction { taffy::style::MaxTrackSizingFunction::fr(fraction) } MaxTrackSizingFunction::VMin(val) => Val::VMin(val) - .into_length_percentage(context, &mut Vec::new()) + .to_length_percentage(context, &mut Vec::new()) .into(), MaxTrackSizingFunction::VMax(val) => Val::VMax(val) - .into_length_percentage(context, &mut Vec::new()) + .to_length_percentage(context, &mut Vec::new()) .into(), MaxTrackSizingFunction::Vh(val) => Val::Vh(val) - .into_length_percentage(context, &mut Vec::new()) + .to_length_percentage(context, &mut Vec::new()) .into(), MaxTrackSizingFunction::Vw(val) => Val::Vw(val) - .into_length_percentage(context, &mut Vec::new()) + .to_length_percentage(context, &mut Vec::new()) .into(), } } @@ -875,7 +875,7 @@ mod tests { for (val, length) in cases { assert!({ let lhs = val - .into_length_percentage(&context, &mut Vec::new()) + .to_length_percentage(&context, &mut Vec::new()) .into_raw() .value(); let rhs = length.into_raw().value(); From 5d3136b69ab558e4f24f399d294c5299895ddd61 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sun, 1 Nov 2026 22:14:41 +0000 Subject: [PATCH 14/50] Use `bevy_platform::sync::Arc` instead of `std::sync::Arc`. --- crates/bevy_ui/src/calc.rs | 3 +-- crates/bevy_ui/src/geometry.rs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ui/src/calc.rs b/crates/bevy_ui/src/calc.rs index 9ec7edf777b68..c5591ae4918ce 100644 --- a/crates/bevy_ui/src/calc.rs +++ b/crates/bevy_ui/src/calc.rs @@ -1,6 +1,5 @@ -use std::sync::Arc; - use bevy_math::{StableInterpolate as _, Vec2}; +use bevy_platform::sync::Arc; use bevy_reflect::Reflect; use bevy_text::{EmSize, RemSize}; diff --git a/crates/bevy_ui/src/geometry.rs b/crates/bevy_ui/src/geometry.rs index d55a074a00b56..2a0944eb309c0 100644 --- a/crates/bevy_ui/src/geometry.rs +++ b/crates/bevy_ui/src/geometry.rs @@ -1,9 +1,9 @@ use bevy_math::{MismatchedUnitsError, StableInterpolate as _, TryStableInterpolate, Vec2}; +use bevy_platform::sync::Arc; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_text::{EmSize, FontSize, RemSize}; use bevy_utils::default; use core::ops::{Add, Div, DivAssign, Mul, MulAssign, Neg, Sub}; -use std::sync::Arc; use thiserror::Error; #[cfg(feature = "serialize")] From fe6ff8245c930cb2175558d9586a4417de1bee80 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sun, 1 Nov 2026 22:22:24 +0000 Subject: [PATCH 15/50] Resolve calc expressions to physical pixels before layout `Val::to_physical` maps every leaf except percent to physical `Px` when the taffy style is rebuilt, folding through the `Val` operators so most expressions collapse to a single length or an inline sum of a length and a percentage. `LayoutCalc` then holds only that `Val` and resolves against taffy's basis alone, so it no longer stores a `LayoutContext`. `Val::clamp` now folds three same-unit operands. Co-Authored-By: Claude Fable 5.1 --- crates/bevy_ui/src/calc.rs | 3 + crates/bevy_ui/src/layout/convert.rs | 113 +++++++++++++++++------ crates/bevy_ui/src/layout/layout_tree.rs | 23 ++--- 3 files changed, 94 insertions(+), 45 deletions(-) diff --git a/crates/bevy_ui/src/calc.rs b/crates/bevy_ui/src/calc.rs index c5591ae4918ce..2a2e2726234e6 100644 --- a/crates/bevy_ui/src/calc.rs +++ b/crates/bevy_ui/src/calc.rs @@ -405,6 +405,9 @@ impl Val { pub(crate) fn clamp_inline(min: &Val, value: &Val, max: &Val) -> Option { let term = |val: &Val| Some((val.unit()?, val.inner()?)); let [lo, v, hi] = [term(min)?, term(value)?, term(max)?]; + if lo.0 == v.0 && v.0 == hi.0 { + return Some(Val::leaf(v.0, lo.1.max(v.1.min(hi.1)))); + } Some(Val::Clamp([lo.0, v.0, hi.0], [lo.1, v.1, hi.1])) } diff --git a/crates/bevy_ui/src/layout/convert.rs b/crates/bevy_ui/src/layout/convert.rs index 0b2e48fedc459..79f01c17e808b 100644 --- a/crates/bevy_ui/src/layout/convert.rs +++ b/crates/bevy_ui/src/layout/convert.rs @@ -1,7 +1,9 @@ use bevy_platform::sync::Arc; +use bevy_text::{EmSize, RemSize}; use taffy::style_helpers; use crate::{ + calc::{CalcOp, ValUnit}, AlignContent, AlignItems, AlignSelf, BoxSizing, Display, FlexDirection, FlexWrap, GridAutoFlow, GridPlacement, GridTrack, GridTrackRepetition, InlineDirection, JustifyContent, JustifyItems, JustifySelf, MaxTrackSizingFunction, MinTrackSizingFunction, Node, OverflowAxis, PositionType, @@ -14,6 +16,49 @@ use super::{ }; impl Val { + /// This value with every unit other than percent resolved to physical pixels, so that only + /// the percentage basis is needed to finish resolving it during layout. + /// + /// Rebuilding through the `Val` operators folds the resolved terms, so most expressions + /// collapse to a single `Px` or an inline sum of a length and a percentage. + fn to_physical(&self, context: &LayoutContext) -> Val { + let leaf = |(unit, value): (ValUnit, f32)| match unit { + ValUnit::Percent => Val::Percent(value), + _ => Val::Px( + Val::leaf(unit, value) + .resolve( + context.scale_factor, + 0.0, + context.physical_size, + EmSize(context.em_size), + RemSize(context.rem_size), + ) + .unwrap_or(0.0), + ), + }; + match self { + Val::Auto => Val::Auto, + Val::Min(..) => self.terms().map(leaf).reduce(Val::min).unwrap_or(Val::ZERO), + Val::Max(..) => self.terms().map(leaf).reduce(Val::max).unwrap_or(Val::ZERO), + Val::Clamp(units, values) => leaf((units[1], values[1])) + .clamp(leaf((units[0], values[0])), leaf((units[2], values[2]))), + Val::Calc(calc) => match &*calc.0 { + CalcOp::Add(a, b) => a.to_physical(context) + b.to_physical(context), + CalcOp::Sub(a, b) => a.to_physical(context) - b.to_physical(context), + CalcOp::Min(a, b) => a.to_physical(context).min(b.to_physical(context)), + CalcOp::Max(a, b) => a.to_physical(context).max(b.to_physical(context)), + CalcOp::Clamp(min, value, max) => value + .to_physical(context) + .clamp(min.to_physical(context), max.to_physical(context)), + }, + _ => self + .terms() + .map(leaf) + .reduce(|a, b| a + b) + .unwrap_or(Val::ZERO), + } + } + pub(super) fn to_length_percentage_auto( &self, context: &LayoutContext, @@ -22,22 +67,11 @@ impl Val { match self { Val::Auto => style_helpers::auto(), Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Clamp(..) | Val::Calc(_) => { - if !self.uses_percent() { - return style_helpers::length( - self.resolve( - context.scale_factor, - 0.0, - context.physical_size, - bevy_text::EmSize(context.em_size), - bevy_text::RemSize(context.rem_size), - ) - .unwrap_or(0.0), - ); + let physical = self.to_physical(context); + if !physical.uses_percent() { + return style_helpers::length(LayoutCalc(physical).resolve(0.0)); } - let calc = Arc::new(LayoutCalc { - val: self.clone(), - context: *context, - }); + let calc = Arc::new(LayoutCalc(physical)); let value = taffy::style::LengthPercentageAuto::calc(Arc::as_ptr(&calc).cast()); calcs.push(calc); value @@ -68,22 +102,11 @@ impl Val { match self { Val::Auto => style_helpers::length(0.0_f32), Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Clamp(..) | Val::Calc(_) => { - if !self.uses_percent() { - return style_helpers::length( - self.resolve( - context.scale_factor, - 0.0, - context.physical_size, - bevy_text::EmSize(context.em_size), - bevy_text::RemSize(context.rem_size), - ) - .unwrap_or(0.0), - ); + let physical = self.to_physical(context); + if !physical.uses_percent() { + return style_helpers::length(LayoutCalc(physical).resolve(0.0)); } - let calc = Arc::new(LayoutCalc { - val: self.clone(), - context: *context, - }); + let calc = Arc::new(LayoutCalc(physical)); let value = taffy::style::LengthPercentage::calc(Arc::as_ptr(&calc).cast()); calcs.push(calc); value @@ -610,6 +633,36 @@ mod tests { use super::*; + #[test] + fn expressions_resolve_to_physical_before_layout() { + let context = + LayoutContext::new(2.0, Vec2::new(1000.0, 500.0), EmSize(12.0), RemSize(20.0)); + let value = + Val::Percent(50.0) - Val::Px(10.0) + Val::Em(2.0) + Val::Rem(1.0) + Val::Vw(1.0); + assert_eq!( + value.to_physical(&context), + Val::Percent(50.0) + Val::Px(78.0) + ); + assert_eq!( + (Val::Px(1.0) + Val::Em(1.0)).to_physical(&context), + Val::Px(26.0) + ); + assert_eq!( + Val::Percent(80.0) + .clamp(Val::Px(100.0), Val::Px(300.0)) + .to_physical(&context), + Val::Percent(80.0).clamp(Val::Px(200.0), Val::Px(600.0)) + ); + let nested: Val = CalcOp::Min(Val::Em(1.0) + Val::Percent(10.0), Val::Rem(1.0)).into(); + let physical = nested.to_physical(&context); + assert!(matches!(physical, Val::Calc(_)), "got {physical:?}"); + assert_eq!(LayoutCalc(physical).resolve(100.0), 34.0); + assert_eq!( + LayoutCalc((Val::Px(1.0) + Val::Vh(10.0)).to_physical(&context)).resolve(0.0), + 52.0 + ); + } + #[test] fn test_convert_from() { use sh::TaffyZero; diff --git a/crates/bevy_ui/src/layout/layout_tree.rs b/crates/bevy_ui/src/layout/layout_tree.rs index 975d07a64272b..466d704399cf3 100644 --- a/crates/bevy_ui/src/layout/layout_tree.rs +++ b/crates/bevy_ui/src/layout/layout_tree.rs @@ -23,9 +23,9 @@ use taffy::{ }; use crate::{ - layout::{LayoutContext, UiTreeDirty}, - ContentSize, FixedNode, GhostNode, IgnoreScroll, LayoutConfig, LayoutError, Measure, - MeasureArgs, Node, NodeMeasure, Outline, OverrideClip, ScrollPosition, UiTransform, Val, + layout::UiTreeDirty, ContentSize, FixedNode, GhostNode, IgnoreScroll, LayoutConfig, + LayoutError, Measure, MeasureArgs, Node, NodeMeasure, Outline, OverrideClip, ScrollPosition, + UiTransform, Val, }; #[expect( @@ -45,22 +45,15 @@ unsafe impl Sync for TaffyStyle {} #[derive(Default, Component, Clone, Deref, DerefMut)] pub struct TaffyStyle(#[deref] pub Style, pub(crate) Vec>); +/// An expression with every unit other than percent already resolved to physical pixels, +/// so only the percentage basis taffy supplies is needed to resolve it. #[repr(align(8))] -pub(crate) struct LayoutCalc { - pub val: Val, - pub context: LayoutContext, -} +pub(crate) struct LayoutCalc(pub Val); impl LayoutCalc { pub fn resolve(&self, basis: f32) -> f32 { - self.val - .resolve( - self.context.scale_factor, - basis, - self.context.physical_size, - EmSize(self.context.em_size), - RemSize(self.context.rem_size), - ) + self.0 + .resolve(1.0, basis, Vec2::ZERO, EmSize(0.0), RemSize(0.0)) .unwrap_or(0.0) } } From 77c63290e8ae746bdb503e40faffdf39362e5fcb Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sun, 1 Nov 2026 22:45:28 +0000 Subject: [PATCH 16/50] Move LayoutContext into the style conversion module Nothing in the layout pass reads it any more, since calc expressions are resolved to physical pixels when the taffy style is rebuilt. It is re-exported from `layout` so `bevy_ui::LayoutContext` is unchanged. Co-Authored-By: Claude Fable 5.1 --- crates/bevy_ui/src/layout/convert.rs | 55 +++++++++++++++++++++++++--- crates/bevy_ui/src/layout/mod.rs | 48 +----------------------- 2 files changed, 52 insertions(+), 51 deletions(-) diff --git a/crates/bevy_ui/src/layout/convert.rs b/crates/bevy_ui/src/layout/convert.rs index 79f01c17e808b..6948ae943ed24 100644 --- a/crates/bevy_ui/src/layout/convert.rs +++ b/crates/bevy_ui/src/layout/convert.rs @@ -1,5 +1,6 @@ +use bevy_math::Vec2; use bevy_platform::sync::Arc; -use bevy_text::{EmSize, RemSize}; +use bevy_text::{EmSize, RemSize, DEFAULT_REM_SIZE_PX}; use taffy::style_helpers; use crate::{ @@ -10,10 +11,54 @@ use crate::{ RepeatedGridTrack, UiRect, Val, }; -use super::{ - layout_tree::{LayoutCalc, TaffyStyle}, - LayoutContext, -}; +use super::layout_tree::{LayoutCalc, TaffyStyle}; + +/// The values a [`Node`]'s [`Val`]s are converted against when its taffy style is rebuilt. +#[derive(Copy, Clone)] +pub struct LayoutContext { + pub scale_factor: f32, + pub physical_size: Vec2, + pub em_size: f32, + pub rem_size: f32, +} + +impl LayoutContext { + pub const DEFAULT: Self = Self { + scale_factor: 1.0, + physical_size: Vec2::ZERO, + em_size: DEFAULT_REM_SIZE_PX, + rem_size: DEFAULT_REM_SIZE_PX, + }; + /// Create a new [`LayoutContext`] from the window's physical size and scale factor + #[inline] + pub(super) const fn new( + scale_factor: f32, + physical_size: Vec2, + em_size: EmSize, + rem_size: RemSize, + ) -> Self { + Self { + scale_factor, + physical_size, + em_size: em_size.0, + rem_size: rem_size.0, + } + } +} + +#[cfg(test)] +impl LayoutContext { + pub const TEST_CONTEXT: Self = Self { + physical_size: Vec2::new(1000.0, 1000.0), + ..Self::DEFAULT + }; +} + +impl Default for LayoutContext { + fn default() -> Self { + Self::DEFAULT + } +} impl Val { /// This value with every unit other than percent resolved to physical pixels, so that only diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index b2f7d97231e1f..dea9ae6ec0097 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -22,7 +22,7 @@ use bevy_sprite::BorderRect; use layout_tree::ComputedLayout; use thiserror::Error; -use bevy_text::{ComputedTextBlock, EmSize, FontCx, RemSize, TextFont, DEFAULT_REM_SIZE_PX}; +use bevy_text::{ComputedTextBlock, EmSize, FontCx, RemSize, TextFont}; pub mod clipping; mod convert; @@ -89,51 +89,7 @@ impl UiRoots { } } -#[derive(Copy, Clone)] -pub struct LayoutContext { - pub scale_factor: f32, - pub physical_size: Vec2, - pub em_size: f32, - pub rem_size: f32, -} - -impl LayoutContext { - pub const DEFAULT: Self = Self { - scale_factor: 1.0, - physical_size: Vec2::ZERO, - em_size: DEFAULT_REM_SIZE_PX, - rem_size: DEFAULT_REM_SIZE_PX, - }; - /// Create a new [`LayoutContext`] from the window's physical size and scale factor - #[inline] - const fn new( - scale_factor: f32, - physical_size: Vec2, - em_size: EmSize, - rem_size: RemSize, - ) -> Self { - Self { - scale_factor, - physical_size, - em_size: em_size.0, - rem_size: rem_size.0, - } - } -} - -#[cfg(test)] -impl LayoutContext { - pub const TEST_CONTEXT: Self = Self { - physical_size: Vec2::new(1000.0, 1000.0), - ..Self::DEFAULT - }; -} - -impl Default for LayoutContext { - fn default() -> Self { - Self::DEFAULT - } -} +pub use convert::LayoutContext; #[derive(Debug, Error)] pub enum LayoutError { From ace3da002d4b50d7dc995e5baa4cba5b191aad4d Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sun, 1 Nov 2026 23:09:13 +0000 Subject: [PATCH 17/50] Make negating a clamp negate its result `-clamp(lo, v, hi)` is `min(-lo, max(-v, -hi))`, which is the clamp with its bounds swapped only when `lo <= hi`. Same-unit bounds are ordered by construction, since inverted ones fold to the minimum, so they swap; mixed-unit bounds can invert at some bases, so they fall back to the exact `Calc` form as `CalcOp::scale` already does. Co-Authored-By: Claude Fable 5.1 --- crates/bevy_ui/src/calc.rs | 52 +++++++++++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/crates/bevy_ui/src/calc.rs b/crates/bevy_ui/src/calc.rs index 2a2e2726234e6..8bd60b5d26edc 100644 --- a/crates/bevy_ui/src/calc.rs +++ b/crates/bevy_ui/src/calc.rs @@ -408,6 +408,11 @@ impl Val { if lo.0 == v.0 && v.0 == hi.0 { return Some(Val::leaf(v.0, lo.1.max(v.1.min(hi.1)))); } + // Inverted bounds always resolve to the minimum, so an inline clamp with same-unit + // bounds is guaranteed to have `min <= max`. + if lo.0 == hi.0 && lo.1 > hi.1 { + return Some(Val::leaf(lo.0, lo.1)); + } Some(Val::Clamp([lo.0, v.0, hi.0], [lo.1, v.1, hi.1])) } @@ -441,13 +446,22 @@ impl Val { Val::Sum(units, values) => (TermOp::Sum, units, values), Val::Min(units, values) => (TermOp::Min, units, values), Val::Max(units, values) => (TermOp::Max, units, values), - Val::Clamp(mut units, values) => { - let mut values = values.map(&f); - if reverses { - units.swap(0, 2); - values.swap(0, 2); + Val::Clamp(units, values) => { + let values = values.map(&f); + if !reverses { + return Val::Clamp(units, values); + } + // `f` reverses order, so `f(max(lo, min(v, hi)))` is `min(f(lo), max(f(v), f(hi)))`. + // With same-unit bounds `lo <= hi` holds, so that is the clamp with the bounds + // swapped; mixed-unit bounds may invert at some bases, so keep the exact form. + let [lo, v, hi] = core::array::from_fn(|i| Val::leaf(units[i], values[i])); + if units[0] == units[2] { + return Val::Clamp( + [units[2], units[1], units[0]], + [values[2], values[1], values[0]], + ); } - return Val::Clamp(units, values); + return lo.min(v.max(hi)); } other => return other, }; @@ -504,11 +518,37 @@ mod tests { assert_eq!(resolve(&(min * -2.0)), Ok(-20.0)); } + #[test] + fn negating_a_clamp_negates_its_result() { + let clamp = Val::Percent(50.0).clamp(Val::Px(100.0), Val::Percent(20.0)); + let resolve = |value: &Val, base: f32| { + value.resolve(1.0, base, Vec2::ZERO, EmSize(0.0), RemSize(0.0)) + }; + for base in [50.0, 100.0, 150.0, 300.0, 1000.0] { + let expected = resolve(&clamp, base).unwrap(); + assert_eq!(resolve(&-clamp.clone(), base), Ok(-expected), "base {base}"); + assert_eq!( + resolve(&(clamp.clone() * -2.0), base), + Ok(-2.0 * expected), + "base {base}" + ); + assert_eq!( + resolve(&(Val::ZERO - clamp.clone()), base), + Ok(-expected), + "base {base}" + ); + } + } + #[test] fn clamp_folds_inline() { use bevy_math::TryStableInterpolate; let clamp = Val::Percent(80.0).clamp(Val::Px(100.0), Val::Px(300.0)); assert!(matches!(clamp, Val::Clamp(..)), "got {clamp:?}"); + assert_eq!( + Val::Percent(80.0).clamp(Val::Px(300.0), Val::Px(100.0)), + Val::Px(300.0) + ); let resolve = |value: &Val, base: f32| { value.resolve(1.0, base, Vec2::ZERO, EmSize(0.0), RemSize(0.0)) }; From d573e856e9b33898f275b52c91934162ed7b4289 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sun, 1 Nov 2026 23:14:27 +0000 Subject: [PATCH 18/50] Test that negating min and max negates their results Co-Authored-By: Claude Fable 5.1 --- crates/bevy_ui/src/calc.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/crates/bevy_ui/src/calc.rs b/crates/bevy_ui/src/calc.rs index 8bd60b5d26edc..d23767601382c 100644 --- a/crates/bevy_ui/src/calc.rs +++ b/crates/bevy_ui/src/calc.rs @@ -518,6 +518,33 @@ mod tests { assert_eq!(resolve(&(min * -2.0)), Ok(-20.0)); } + #[test] + fn negating_min_and_max_negates_their_results() { + assert_eq!(-Val::Px(10.0).max(Val::Px(20.0)), Val::Px(-20.0)); + assert_eq!(-Val::Px(10.0).min(Val::Px(20.0)), Val::Px(-10.0)); + let resolve = |value: &Val, base: f32| { + value.resolve(1.0, base, Vec2::ZERO, EmSize(0.0), RemSize(0.0)) + }; + for expr in [ + Val::Px(10.0).max(Val::Percent(50.0)), + Val::Px(10.0).min(Val::Percent(50.0)), + ] { + for base in [0.0, 20.0, 100.0] { + let expected = resolve(&expr, base).unwrap(); + assert_eq!( + resolve(&-expr.clone(), base), + Ok(-expected), + "{expr:?} at {base}" + ); + assert_eq!( + resolve(&(expr.clone() * -2.0), base), + Ok(-2.0 * expected), + "{expr:?} at {base}" + ); + } + } + } + #[test] fn negating_a_clamp_negates_its_result() { let clamp = Val::Percent(50.0).clamp(Val::Px(100.0), Val::Percent(20.0)); From 869f9826f44e47884eff1736aeed19e0bb8472ac Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sun, 1 Nov 2026 23:32:34 +0000 Subject: [PATCH 19/50] Serialize Calc as its CalcOp serde only implements `Serialize` and `Deserialize` for `Arc` behind its `rc` feature, which nothing in the graph enables, so the derived impls on `Calc` did not compile with the `serialize` feature. Delegating to the inner `CalcOp` fixes that and keeps the sharing out of the serialized form. Adds a ron round-trip test for expressions. Co-Authored-By: Claude Fable 5.1 --- crates/bevy_ui/Cargo.toml | 1 + crates/bevy_ui/src/calc.rs | 37 ++++++++++++++++++++++++++++++++----- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/crates/bevy_ui/Cargo.toml b/crates/bevy_ui/Cargo.toml index 82f08b9dfc89d..b5531a320d6c4 100644 --- a/crates/bevy_ui/Cargo.toml +++ b/crates/bevy_ui/Cargo.toml @@ -57,6 +57,7 @@ tracing = { version = "0.1", default-features = false, features = ["std"] } [dev-dependencies] bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.20.0-dev" } +ron = "0.12" [features] default = [] diff --git a/crates/bevy_ui/src/calc.rs b/crates/bevy_ui/src/calc.rs index d23767601382c..e85dd861de488 100644 --- a/crates/bevy_ui/src/calc.rs +++ b/crates/bevy_ui/src/calc.rs @@ -11,13 +11,25 @@ use crate::{Val, ValArithmeticError}; /// Wrapper around an opteration on `Val`s. #[derive(Clone, Debug, PartialEq, Reflect)] #[reflect(opaque, PartialEq, Debug, Clone)] -#[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), - reflect(Serialize, Deserialize) -)] +#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))] pub struct Calc(pub(crate) Arc); +// Serialized as the `CalcOp` itself: serde only implements `Arc` behind its `rc` feature, +// and sharing is an implementation detail that should not leak into the format. +#[cfg(feature = "serialize")] +impl serde::Serialize for Calc { + fn serialize(&self, serializer: S) -> Result { + self.0.serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for Calc { + fn deserialize>(deserializer: D) -> Result { + CalcOp::deserialize(deserializer).map(|op| Self(Arc::new(op))) + } +} + /// An operation performed on `Val`s #[derive(Clone, Debug, PartialEq, Reflect)] #[reflect(PartialEq, Debug, Clone)] @@ -712,6 +724,21 @@ mod tests { assert_eq!(Val::from_reflect(dynamic.as_ref()), Some(value)); } + #[cfg(feature = "serialize")] + #[test] + fn expressions_round_trip_through_serde() { + let values = [ + Val::Percent(50.0) - Val::Px(10.0), + Val::Px(10.0).min(Val::Percent(50.0)), + Val::Percent(80.0).clamp(Val::Px(100.0), Val::Px(300.0)), + CalcOp::Min(Val::Em(1.0) + Val::Percent(10.0), Val::Rem(1.0)).into(), + ]; + for value in values { + let ron = ron::to_string(&value).unwrap(); + assert_eq!(ron::from_str::(&ron).unwrap(), value, "{ron}"); + } + } + #[test] fn font_size_rejects_expressions() { assert_eq!( From faf842cc93bb956181c30170f1b94a618a077ab9 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sun, 1 Nov 2026 23:51:52 +0000 Subject: [PATCH 20/50] Added failing clamp tests --- crates/bevy_ui/src/calc.rs | 41 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/crates/bevy_ui/src/calc.rs b/crates/bevy_ui/src/calc.rs index e85dd861de488..648185bc5be61 100644 --- a/crates/bevy_ui/src/calc.rs +++ b/crates/bevy_ui/src/calc.rs @@ -750,4 +750,45 @@ mod tests { Err(ValArithmeticError::NonEvaluable) ); } + + #[test] + fn direct_inline_clamp_negates_its_resolved_value() { + let value = Val::Clamp( + [ValUnit::Px, ValUnit::Percent, ValUnit::Px], + [100.0, 50.0, 20.0], + ); + let resolve = |value: Val| { + value + .resolve( + 1.0, + 100.0, + Default::default(), + Default::default(), + Default::default(), + ) + .unwrap() + }; + assert_eq!(resolve(value.clone()), 100.0); + assert_eq!(resolve(-value), -100.0); + } + + #[test] + fn extrapolated_inline_clamp_negates_its_resolved_value() { + let a = Val::Percent(50.0).clamp(Val::Px(0.0), Val::Px(100.0)); + let b = Val::Percent(50.0).clamp(Val::Px(100.0), Val::Px(100.0)); + let value = bevy_math::TryStableInterpolate::try_interpolate_stable(&a, &b, 2.0).unwrap(); + let resolve = |value: Val| { + value + .resolve( + 1.0, + 100.0, + Default::default(), + Default::default(), + Default::default(), + ) + .unwrap() + }; + assert_eq!(resolve(value.clone()), 200.0); + assert_eq!(resolve(-value), -200.0); + } } From 6b96f93c7b4ac4254911d21bdb1aeab1fe376012 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sun, 1 Nov 2026 23:53:26 +0000 Subject: [PATCH 21/50] Negate directly built clamps with inverted bounds correctly The swapped-bounds shortcut assumed same-unit bounds are ordered, which only holds for clamps built through `Val::clamp`. A `Val::Clamp` built directly with `min > max` resolves to its minimum, so negating it must give the negated minimum; check the ordering instead of assuming it. Co-Authored-By: Claude Fable 5.1 --- crates/bevy_ui/src/calc.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ui/src/calc.rs b/crates/bevy_ui/src/calc.rs index 648185bc5be61..fbafa4a9f7b95 100644 --- a/crates/bevy_ui/src/calc.rs +++ b/crates/bevy_ui/src/calc.rs @@ -459,13 +459,18 @@ impl Val { Val::Min(units, values) => (TermOp::Min, units, values), Val::Max(units, values) => (TermOp::Max, units, values), Val::Clamp(units, values) => { + // Inverted same-unit bounds make the clamp its minimum whatever the value is. + if units[0] == units[2] && values[0] > values[2] { + return Val::leaf(units[0], f(values[0])); + } let values = values.map(&f); if !reverses { return Val::Clamp(units, values); } // `f` reverses order, so `f(max(lo, min(v, hi)))` is `min(f(lo), max(f(v), f(hi)))`. - // With same-unit bounds `lo <= hi` holds, so that is the clamp with the bounds - // swapped; mixed-unit bounds may invert at some bases, so keep the exact form. + // Same-unit bounds are now known to satisfy `lo <= hi`, so that is the clamp with + // the bounds swapped; mixed-unit bounds may invert at some bases, so keep the + // exact form. let [lo, v, hi] = core::array::from_fn(|i| Val::leaf(units[i], values[i])); if units[0] == units[2] { return Val::Clamp( From 067eedbc0b0c33da4cb7ec95fe16f873118fcd08 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 2 Nov 2026 10:03:13 +0000 Subject: [PATCH 22/50] Renamed LayoutCalc to PercentageCalc --- crates/bevy_ui/src/layout/convert.rs | 22 +++++++++++----------- crates/bevy_ui/src/layout/layout_tree.rs | 8 ++++---- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/crates/bevy_ui/src/layout/convert.rs b/crates/bevy_ui/src/layout/convert.rs index 6948ae943ed24..6c576805f7c14 100644 --- a/crates/bevy_ui/src/layout/convert.rs +++ b/crates/bevy_ui/src/layout/convert.rs @@ -11,7 +11,7 @@ use crate::{ RepeatedGridTrack, UiRect, Val, }; -use super::layout_tree::{LayoutCalc, TaffyStyle}; +use super::layout_tree::{PercentageCalc, TaffyStyle}; /// The values a [`Node`]'s [`Val`]s are converted against when its taffy style is rebuilt. #[derive(Copy, Clone)] @@ -107,16 +107,16 @@ impl Val { pub(super) fn to_length_percentage_auto( &self, context: &LayoutContext, - calcs: &mut Vec>, + calcs: &mut Vec>, ) -> taffy::style::LengthPercentageAuto { match self { Val::Auto => style_helpers::auto(), Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Clamp(..) | Val::Calc(_) => { let physical = self.to_physical(context); if !physical.uses_percent() { - return style_helpers::length(LayoutCalc(physical).resolve(0.0)); + return style_helpers::length(PercentageCalc(physical).resolve(0.0)); } - let calc = Arc::new(LayoutCalc(physical)); + let calc = Arc::new(PercentageCalc(physical)); let value = taffy::style::LengthPercentageAuto::calc(Arc::as_ptr(&calc).cast()); calcs.push(calc); value @@ -142,16 +142,16 @@ impl Val { pub(super) fn to_length_percentage( &self, context: &LayoutContext, - calcs: &mut Vec>, + calcs: &mut Vec>, ) -> taffy::style::LengthPercentage { match self { Val::Auto => style_helpers::length(0.0_f32), Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Clamp(..) | Val::Calc(_) => { let physical = self.to_physical(context); if !physical.uses_percent() { - return style_helpers::length(LayoutCalc(physical).resolve(0.0)); + return style_helpers::length(PercentageCalc(physical).resolve(0.0)); } - let calc = Arc::new(LayoutCalc(physical)); + let calc = Arc::new(PercentageCalc(physical)); let value = taffy::style::LengthPercentage::calc(Arc::as_ptr(&calc).cast()); calcs.push(calc); value @@ -184,7 +184,7 @@ impl Val { fn to_length_percentage_for_border( &self, context: &LayoutContext, - calcs: &mut Vec>, + calcs: &mut Vec>, ) -> taffy::style::LengthPercentage { let length = self.to_length_percentage(context, calcs); let raw = length.into_raw(); @@ -199,7 +199,7 @@ impl Val { fn to_dimension( &self, context: &LayoutContext, - calcs: &mut Vec>, + calcs: &mut Vec>, ) -> taffy::style::Dimension { self.to_length_percentage_auto(context, calcs).into() } @@ -701,9 +701,9 @@ mod tests { let nested: Val = CalcOp::Min(Val::Em(1.0) + Val::Percent(10.0), Val::Rem(1.0)).into(); let physical = nested.to_physical(&context); assert!(matches!(physical, Val::Calc(_)), "got {physical:?}"); - assert_eq!(LayoutCalc(physical).resolve(100.0), 34.0); + assert_eq!(PercentageCalc(physical).resolve(100.0), 34.0); assert_eq!( - LayoutCalc((Val::Px(1.0) + Val::Vh(10.0)).to_physical(&context)).resolve(0.0), + PercentageCalc((Val::Px(1.0) + Val::Vh(10.0)).to_physical(&context)).resolve(0.0), 52.0 ); } diff --git a/crates/bevy_ui/src/layout/layout_tree.rs b/crates/bevy_ui/src/layout/layout_tree.rs index 466d704399cf3..680a6a41a4d61 100644 --- a/crates/bevy_ui/src/layout/layout_tree.rs +++ b/crates/bevy_ui/src/layout/layout_tree.rs @@ -43,14 +43,14 @@ unsafe impl Send for TaffyStyle {} unsafe impl Sync for TaffyStyle {} #[derive(Default, Component, Clone, Deref, DerefMut)] -pub struct TaffyStyle(#[deref] pub Style, pub(crate) Vec>); +pub struct TaffyStyle(#[deref] pub Style, pub(crate) Vec>); /// An expression with every unit other than percent already resolved to physical pixels, /// so only the percentage basis taffy supplies is needed to resolve it. #[repr(align(8))] -pub(crate) struct LayoutCalc(pub Val); +pub(crate) struct PercentageCalc(pub Val); -impl LayoutCalc { +impl PercentageCalc { pub fn resolve(&self, basis: f32) -> f32 { self.0 .resolve(1.0, basis, Vec2::ZERO, EmSize(0.0), RemSize(0.0)) @@ -376,7 +376,7 @@ pub fn compute_layout<'w, 's>( }; { - let mut calcs: HashMap = HashMap::default(); + let mut calcs: HashMap = HashMap::default(); child_stack.push(root_node_id); while let Some(node_id) = child_stack.pop() { let entity = entity_from_node_id(node_id).expect("Viewport node should not be a child"); From 1d4afb0b0d00c556cedd683a37d5647d92ca0978 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 2 Nov 2026 10:57:39 +0000 Subject: [PATCH 23/50] Updated doc comments for convert --- crates/bevy_ui/src/layout/convert.rs | 42 +++++++++++++++++++++------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/crates/bevy_ui/src/layout/convert.rs b/crates/bevy_ui/src/layout/convert.rs index 6c576805f7c14..46ee3ac40ae5d 100644 --- a/crates/bevy_ui/src/layout/convert.rs +++ b/crates/bevy_ui/src/layout/convert.rs @@ -29,6 +29,7 @@ impl LayoutContext { em_size: DEFAULT_REM_SIZE_PX, rem_size: DEFAULT_REM_SIZE_PX, }; + /// Create a new [`LayoutContext`] from the window's physical size and scale factor #[inline] pub(super) const fn new( @@ -61,13 +62,14 @@ impl Default for LayoutContext { } impl Val { - /// This value with every unit other than percent resolved to physical pixels, so that only - /// the percentage basis is needed to finish resolving it during layout. + /// Taffy only supports lengths in pixels and percentages. So we convert `Val`s to pixels + /// before handing them over to Taffy for layout, which provides the percentage basis to finish + /// finish resolving them during layout. /// - /// Rebuilding through the `Val` operators folds the resolved terms, so most expressions - /// collapse to a single `Px` or an inline sum of a length and a percentage. + /// Rebuilding through the `Val` operators folds the resolved terms. Most expressions + /// collapse to a single `Px` or a `Px` and `Percent` sum`. fn to_physical(&self, context: &LayoutContext) -> Val { - let leaf = |(unit, value): (ValUnit, f32)| match unit { + let resolve_leaf_val = |(unit, value): (ValUnit, f32)| match unit { ValUnit::Percent => Val::Percent(value), _ => Val::Px( Val::leaf(unit, value) @@ -83,10 +85,20 @@ impl Val { }; match self { Val::Auto => Val::Auto, - Val::Min(..) => self.terms().map(leaf).reduce(Val::min).unwrap_or(Val::ZERO), - Val::Max(..) => self.terms().map(leaf).reduce(Val::max).unwrap_or(Val::ZERO), - Val::Clamp(units, values) => leaf((units[1], values[1])) - .clamp(leaf((units[0], values[0])), leaf((units[2], values[2]))), + Val::Min(..) => self + .terms() + .map(resolve_leaf_val) + .reduce(Val::min) + .unwrap_or(Val::ZERO), + Val::Max(..) => self + .terms() + .map(resolve_leaf_val) + .reduce(Val::max) + .unwrap_or(Val::ZERO), + Val::Clamp(units, values) => resolve_leaf_val((units[1], values[1])).clamp( + resolve_leaf_val((units[0], values[0])), + resolve_leaf_val((units[2], values[2])), + ), Val::Calc(calc) => match &*calc.0 { CalcOp::Add(a, b) => a.to_physical(context) + b.to_physical(context), CalcOp::Sub(a, b) => a.to_physical(context) - b.to_physical(context), @@ -98,12 +110,15 @@ impl Val { }, _ => self .terms() - .map(leaf) + .map(resolve_leaf_val) .reduce(|a, b| a + b) .unwrap_or(Val::ZERO), } } + /// Convert a `Val` to Taffy's packed `LengthPercentageAuto`. + /// Every leaf that isn't a percentage is resolved using `to_physical`. + /// Only store a calc length if a percent term remains, otherwise precalculate before layout. pub(super) fn to_length_percentage_auto( &self, context: &LayoutContext, @@ -139,6 +154,10 @@ impl Val { } } + /// Convert a `Val` to Taffy's packed `LengthPercentage`. + /// `Val::Auto` is mapped to a zero length. + /// Every leaf that isn't a percentage is resolved using `to_physical`. + /// Only store a calc length if a percent term remains, otherwise precalculate before layout. pub(super) fn to_length_percentage( &self, context: &LayoutContext, @@ -196,6 +215,9 @@ impl Val { } } + /// Convert a `Val` to Taffy's packed `Dimension`. + /// `Dimension` is a superset of `LengthPercentageAuto`, it also supports "min-content", "max-content", "fit-content", "stretch" and "content" values. + /// `bevy_ui` doesn't expose any of these additional values yet. fn to_dimension( &self, context: &LayoutContext, From 084ca8a98b3cef546d924b335a0d28f89cd50242 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 2 Nov 2026 11:10:33 +0000 Subject: [PATCH 24/50] Renamed `LayoutContext` to `ValContext`. --- crates/bevy_ui/src/layout/convert.rs | 35 ++++++++++++++-------------- crates/bevy_ui/src/layout/mod.rs | 4 ++-- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/crates/bevy_ui/src/layout/convert.rs b/crates/bevy_ui/src/layout/convert.rs index 46ee3ac40ae5d..cfe068bcc7ccd 100644 --- a/crates/bevy_ui/src/layout/convert.rs +++ b/crates/bevy_ui/src/layout/convert.rs @@ -15,14 +15,14 @@ use super::layout_tree::{PercentageCalc, TaffyStyle}; /// The values a [`Node`]'s [`Val`]s are converted against when its taffy style is rebuilt. #[derive(Copy, Clone)] -pub struct LayoutContext { +pub struct ValContext { pub scale_factor: f32, pub physical_size: Vec2, pub em_size: f32, pub rem_size: f32, } -impl LayoutContext { +impl ValContext { pub const DEFAULT: Self = Self { scale_factor: 1.0, physical_size: Vec2::ZERO, @@ -48,14 +48,14 @@ impl LayoutContext { } #[cfg(test)] -impl LayoutContext { +impl ValContext { pub const TEST_CONTEXT: Self = Self { physical_size: Vec2::new(1000.0, 1000.0), ..Self::DEFAULT }; } -impl Default for LayoutContext { +impl Default for ValContext { fn default() -> Self { Self::DEFAULT } @@ -68,7 +68,7 @@ impl Val { /// /// Rebuilding through the `Val` operators folds the resolved terms. Most expressions /// collapse to a single `Px` or a `Px` and `Percent` sum`. - fn to_physical(&self, context: &LayoutContext) -> Val { + fn to_physical(&self, context: &ValContext) -> Val { let resolve_leaf_val = |(unit, value): (ValUnit, f32)| match unit { ValUnit::Percent => Val::Percent(value), _ => Val::Px( @@ -121,7 +121,7 @@ impl Val { /// Only store a calc length if a percent term remains, otherwise precalculate before layout. pub(super) fn to_length_percentage_auto( &self, - context: &LayoutContext, + context: &ValContext, calcs: &mut Vec>, ) -> taffy::style::LengthPercentageAuto { match self { @@ -160,7 +160,7 @@ impl Val { /// Only store a calc length if a percent term remains, otherwise precalculate before layout. pub(super) fn to_length_percentage( &self, - context: &LayoutContext, + context: &ValContext, calcs: &mut Vec>, ) -> taffy::style::LengthPercentage { match self { @@ -202,7 +202,7 @@ impl Val { // * A border larger than one physical pixel is rounded down to a whole pixel. fn to_length_percentage_for_border( &self, - context: &LayoutContext, + context: &ValContext, calcs: &mut Vec>, ) -> taffy::style::LengthPercentage { let length = self.to_length_percentage(context, calcs); @@ -220,7 +220,7 @@ impl Val { /// `bevy_ui` doesn't expose any of these additional values yet. fn to_dimension( &self, - context: &LayoutContext, + context: &ValContext, calcs: &mut Vec>, ) -> taffy::style::Dimension { self.to_length_percentage_auto(context, calcs).into() @@ -244,7 +244,7 @@ impl UiRect { // Update taffy style in place from the given `Node` and context pub fn update_taffy_style_from_node( node: &Node, - context: &LayoutContext, + context: &ValContext, taffy_style: &mut TaffyStyle, ) { let TaffyStyle(style, calcs) = taffy_style; @@ -552,7 +552,7 @@ impl From for taffy::geometry::Line taffy::style::MinTrackSizingFunction { + fn into_taffy(self, context: &ValContext) -> taffy::style::MinTrackSizingFunction { match self { MinTrackSizingFunction::Px(val) => Val::Px(val) .to_length_percentage(context, &mut Vec::new()) @@ -590,7 +590,7 @@ impl MinTrackSizingFunction { } impl MaxTrackSizingFunction { - fn into_taffy(self, context: &LayoutContext) -> taffy::style::MaxTrackSizingFunction { + fn into_taffy(self, context: &ValContext) -> taffy::style::MaxTrackSizingFunction { match self { MaxTrackSizingFunction::Px(val) => Val::Px(val) .to_length_percentage(context, &mut Vec::new()) @@ -649,7 +649,7 @@ impl MaxTrackSizingFunction { impl GridTrack { pub(super) fn into_taffy_track( self, - context: &LayoutContext, + context: &ValContext, ) -> taffy::style::TrackSizingFunction { let min = self.min_sizing_function.into_taffy(context); let max = self.max_sizing_function.into_taffy(context); @@ -660,7 +660,7 @@ impl GridTrack { impl RepeatedGridTrack { pub(super) fn clone_into_repeated_taffy_track( &self, - context: &LayoutContext, + context: &ValContext, ) -> taffy::style::GridTemplateComponent { if self.tracks.len() == 1 && self.repetition == GridTrackRepetition::Count(1) { let min = self.tracks[0].min_sizing_function.into_taffy(context); @@ -702,8 +702,7 @@ mod tests { #[test] fn expressions_resolve_to_physical_before_layout() { - let context = - LayoutContext::new(2.0, Vec2::new(1000.0, 500.0), EmSize(12.0), RemSize(20.0)); + let context = ValContext::new(2.0, Vec2::new(1000.0, 500.0), EmSize(12.0), RemSize(20.0)); let value = Val::Percent(50.0) - Val::Px(10.0) + Val::Em(2.0) + Val::Rem(1.0) + Val::Vw(1.0); assert_eq!( @@ -806,7 +805,7 @@ mod tests { grid_column: GridPlacement::start(4), grid_row: GridPlacement::span(3), }; - let viewport_values = LayoutContext::new( + let viewport_values = ValContext::new( 1.0, Vec2::new(800., 600.), EmSize(DEFAULT_REM_SIZE_PX), @@ -980,7 +979,7 @@ mod tests { #[test] fn test_into_length_percentage() { use taffy::style::LengthPercentage; - let context = LayoutContext::new(2.0, Vec2::new(800., 600.), EmSize(10.), RemSize(20.)); + let context = ValContext::new(2.0, Vec2::new(800., 600.), EmSize(10.), RemSize(20.)); let cases = [ (Val::Auto, LengthPercentage::length(0.)), (Val::Percent(1.), LengthPercentage::percent(0.01)), diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index dea9ae6ec0097..8844c195027f3 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -89,7 +89,7 @@ impl UiRoots { } } -pub use convert::LayoutContext; +pub use convert::ValContext; #[derive(Debug, Error)] pub enum LayoutError { @@ -212,7 +212,7 @@ pub fn sync_taffy_styles_with_nodes( { convert::update_taffy_style_from_node( &node, - &LayoutContext::new( + &ValContext::new( target.scale_factor(), target.physical_size().as_vec2(), *em_size, From a2aaed67f3af12f5a7d9eb6f0ecf8239d152c93e Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 2 Nov 2026 11:26:32 +0000 Subject: [PATCH 25/50] Moved `ValContext` into the `calc` module and added doc comments. --- crates/bevy_ui/src/calc.rs | 58 +++++++++++++++++++++++++++- crates/bevy_ui/src/layout/convert.rs | 53 +------------------------ 2 files changed, 58 insertions(+), 53 deletions(-) diff --git a/crates/bevy_ui/src/calc.rs b/crates/bevy_ui/src/calc.rs index fbafa4a9f7b95..601e31abf1115 100644 --- a/crates/bevy_ui/src/calc.rs +++ b/crates/bevy_ui/src/calc.rs @@ -1,13 +1,67 @@ use bevy_math::{StableInterpolate as _, Vec2}; use bevy_platform::sync::Arc; use bevy_reflect::Reflect; -use bevy_text::{EmSize, RemSize}; +use bevy_text::{EmSize, RemSize, DEFAULT_REM_SIZE_PX}; #[cfg(feature = "serialize")] use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; use crate::{Val, ValArithmeticError}; +/// All the context needed to resolve a [`Val`] into a length in physical pixels, except the percentage basis. +#[derive(Copy, Clone)] +pub struct ValContext { + /// Logical pixel lengths are multiplied by the scale factor to get a physical length. + pub scale_factor: f32, + /// Size of the target surface in physical pixels. + pub physical_size: Vec2, + /// The local font size in logical pixels. + /// `Val::Em` values are normalized, so `em(1)` is `em_size * scale_factor` in physical pixels. + pub em_size: f32, + /// The root font size in logical pixels. + /// `Val::Em` values are normalized, so `rem(1)` is `rem_size * scale_factor` in physical pixels. + pub rem_size: f32, +} + +impl ValContext { + pub const DEFAULT: Self = Self { + scale_factor: 1.0, + physical_size: Vec2::ZERO, + em_size: DEFAULT_REM_SIZE_PX, + rem_size: DEFAULT_REM_SIZE_PX, + }; + + /// Create a new [`LayoutContext`] from the window's physical size and scale factor + #[inline] + pub(super) const fn new( + scale_factor: f32, + physical_size: Vec2, + em_size: EmSize, + rem_size: RemSize, + ) -> Self { + Self { + scale_factor, + physical_size, + em_size: em_size.0, + rem_size: rem_size.0, + } + } +} + +#[cfg(test)] +impl ValContext { + pub const TEST_CONTEXT: Self = Self { + physical_size: Vec2::new(1000.0, 1000.0), + ..Self::DEFAULT + }; +} + +impl Default for ValContext { + fn default() -> Self { + Self::DEFAULT + } +} + /// Wrapper around an opteration on `Val`s. #[derive(Clone, Debug, PartialEq, Reflect)] #[reflect(opaque, PartialEq, Debug, Clone)] @@ -30,7 +84,7 @@ impl<'de> serde::Deserialize<'de> for Calc { } } -/// An operation performed on `Val`s +/// An operation performed on `Val`s. #[derive(Clone, Debug, PartialEq, Reflect)] #[reflect(PartialEq, Debug, Clone)] #[cfg_attr( diff --git a/crates/bevy_ui/src/layout/convert.rs b/crates/bevy_ui/src/layout/convert.rs index cfe068bcc7ccd..6df3bfb422aa8 100644 --- a/crates/bevy_ui/src/layout/convert.rs +++ b/crates/bevy_ui/src/layout/convert.rs @@ -1,10 +1,9 @@ -use bevy_math::Vec2; use bevy_platform::sync::Arc; -use bevy_text::{EmSize, RemSize, DEFAULT_REM_SIZE_PX}; +use bevy_text::{EmSize, RemSize}; use taffy::style_helpers; use crate::{ - calc::{CalcOp, ValUnit}, + calc::{CalcOp, ValContext, ValUnit}, AlignContent, AlignItems, AlignSelf, BoxSizing, Display, FlexDirection, FlexWrap, GridAutoFlow, GridPlacement, GridTrack, GridTrackRepetition, InlineDirection, JustifyContent, JustifyItems, JustifySelf, MaxTrackSizingFunction, MinTrackSizingFunction, Node, OverflowAxis, PositionType, @@ -13,54 +12,6 @@ use crate::{ use super::layout_tree::{PercentageCalc, TaffyStyle}; -/// The values a [`Node`]'s [`Val`]s are converted against when its taffy style is rebuilt. -#[derive(Copy, Clone)] -pub struct ValContext { - pub scale_factor: f32, - pub physical_size: Vec2, - pub em_size: f32, - pub rem_size: f32, -} - -impl ValContext { - pub const DEFAULT: Self = Self { - scale_factor: 1.0, - physical_size: Vec2::ZERO, - em_size: DEFAULT_REM_SIZE_PX, - rem_size: DEFAULT_REM_SIZE_PX, - }; - - /// Create a new [`LayoutContext`] from the window's physical size and scale factor - #[inline] - pub(super) const fn new( - scale_factor: f32, - physical_size: Vec2, - em_size: EmSize, - rem_size: RemSize, - ) -> Self { - Self { - scale_factor, - physical_size, - em_size: em_size.0, - rem_size: rem_size.0, - } - } -} - -#[cfg(test)] -impl ValContext { - pub const TEST_CONTEXT: Self = Self { - physical_size: Vec2::new(1000.0, 1000.0), - ..Self::DEFAULT - }; -} - -impl Default for ValContext { - fn default() -> Self { - Self::DEFAULT - } -} - impl Val { /// Taffy only supports lengths in pixels and percentages. So we convert `Val`s to pixels /// before handing them over to Taffy for layout, which provides the percentage basis to finish From 41c25fb7347b211dcdde3f4dee371c8745b32e69 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 2 Nov 2026 11:52:41 +0000 Subject: [PATCH 26/50] Renamed `calc` module to `val`. Moved the `Val` types and implementation from `geometry` into `val`. --- crates/bevy_ui/src/calc.rs | 853 ----------- crates/bevy_ui/src/geometry.rs | 1107 +-------------- crates/bevy_ui/src/layout/convert.rs | 2 +- crates/bevy_ui/src/layout/mod.rs | 4 +- crates/bevy_ui/src/layout/tests.rs | 3 +- crates/bevy_ui/src/lib.rs | 4 +- crates/bevy_ui/src/val.rs | 1949 ++++++++++++++++++++++++++ 7 files changed, 1959 insertions(+), 1963 deletions(-) delete mode 100644 crates/bevy_ui/src/calc.rs create mode 100644 crates/bevy_ui/src/val.rs diff --git a/crates/bevy_ui/src/calc.rs b/crates/bevy_ui/src/calc.rs deleted file mode 100644 index 601e31abf1115..0000000000000 --- a/crates/bevy_ui/src/calc.rs +++ /dev/null @@ -1,853 +0,0 @@ -use bevy_math::{StableInterpolate as _, Vec2}; -use bevy_platform::sync::Arc; -use bevy_reflect::Reflect; -use bevy_text::{EmSize, RemSize, DEFAULT_REM_SIZE_PX}; - -#[cfg(feature = "serialize")] -use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; - -use crate::{Val, ValArithmeticError}; - -/// All the context needed to resolve a [`Val`] into a length in physical pixels, except the percentage basis. -#[derive(Copy, Clone)] -pub struct ValContext { - /// Logical pixel lengths are multiplied by the scale factor to get a physical length. - pub scale_factor: f32, - /// Size of the target surface in physical pixels. - pub physical_size: Vec2, - /// The local font size in logical pixels. - /// `Val::Em` values are normalized, so `em(1)` is `em_size * scale_factor` in physical pixels. - pub em_size: f32, - /// The root font size in logical pixels. - /// `Val::Em` values are normalized, so `rem(1)` is `rem_size * scale_factor` in physical pixels. - pub rem_size: f32, -} - -impl ValContext { - pub const DEFAULT: Self = Self { - scale_factor: 1.0, - physical_size: Vec2::ZERO, - em_size: DEFAULT_REM_SIZE_PX, - rem_size: DEFAULT_REM_SIZE_PX, - }; - - /// Create a new [`LayoutContext`] from the window's physical size and scale factor - #[inline] - pub(super) const fn new( - scale_factor: f32, - physical_size: Vec2, - em_size: EmSize, - rem_size: RemSize, - ) -> Self { - Self { - scale_factor, - physical_size, - em_size: em_size.0, - rem_size: rem_size.0, - } - } -} - -#[cfg(test)] -impl ValContext { - pub const TEST_CONTEXT: Self = Self { - physical_size: Vec2::new(1000.0, 1000.0), - ..Self::DEFAULT - }; -} - -impl Default for ValContext { - fn default() -> Self { - Self::DEFAULT - } -} - -/// Wrapper around an opteration on `Val`s. -#[derive(Clone, Debug, PartialEq, Reflect)] -#[reflect(opaque, PartialEq, Debug, Clone)] -#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))] -pub struct Calc(pub(crate) Arc); - -// Serialized as the `CalcOp` itself: serde only implements `Arc` behind its `rc` feature, -// and sharing is an implementation detail that should not leak into the format. -#[cfg(feature = "serialize")] -impl serde::Serialize for Calc { - fn serialize(&self, serializer: S) -> Result { - self.0.serialize(serializer) - } -} - -#[cfg(feature = "serialize")] -impl<'de> serde::Deserialize<'de> for Calc { - fn deserialize>(deserializer: D) -> Result { - CalcOp::deserialize(deserializer).map(|op| Self(Arc::new(op))) - } -} - -/// An operation performed on `Val`s. -#[derive(Clone, Debug, PartialEq, Reflect)] -#[reflect(PartialEq, Debug, Clone)] -#[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), - reflect(Serialize, Deserialize) -)] -pub enum CalcOp { - /// Add two vals. - Add(Val, Val), - /// Subtract the second `Val` from the first. - Sub(Val, Val), - /// Return the minimum of the two `Val`s. - Min(Val, Val), - /// Return the maximum of the two `Val`s. - Max(Val, Val), - /// Clamp the second `Val` between the first and third. - Clamp(Val, Val, Val), -} - -impl CalcOp { - pub(crate) fn uses_percent(&self) -> bool { - match self { - Self::Add(a, b) | Self::Sub(a, b) | Self::Min(a, b) | Self::Max(a, b) => { - a.uses_percent() || b.uses_percent() - } - Self::Clamp(min, value, max) => { - min.uses_percent() || value.uses_percent() || max.uses_percent() - } - } - } - - pub fn resolve( - &self, - scale_factor: f32, - physical_base_value: f32, - physical_target_size: Vec2, - em_size: EmSize, - rem_size: RemSize, - ) -> Result { - let resolve = |val: &Val| { - val.resolve( - scale_factor, - physical_base_value, - physical_target_size, - em_size, - rem_size, - ) - }; - Ok(match self { - Self::Add(a, b) => resolve(a)? + resolve(b)?, - Self::Sub(a, b) => resolve(a)? - resolve(b)?, - Self::Min(a, b) => resolve(a)?.min(resolve(b)?), - Self::Max(a, b) => resolve(a)?.max(resolve(b)?), - Self::Clamp(min, value, max) => resolve(min)?.max(resolve(value)?.min(resolve(max)?)), - }) - } - - pub(crate) fn scale(&mut self, k: f32) { - use core::mem::take; - match self { - Self::Min(a, b) if k < 0.0 => *self = Self::Max(take(a) * k, take(b) * k), - Self::Max(a, b) if k < 0.0 => *self = Self::Min(take(a) * k, take(b) * k), - Self::Clamp(min, value, max) if k < 0.0 => { - *self = Self::Min( - take(min) * k, - Self::Max(take(value) * k, take(max) * k).into(), - ); - } - Self::Add(a, b) | Self::Sub(a, b) | Self::Min(a, b) | Self::Max(a, b) => { - *a *= k; - *b *= k; - } - Self::Clamp(min, value, max) => { - *min *= k; - *value *= k; - *max *= k; - } - } - } -} - -impl From for Val { - fn from(op: CalcOp) -> Self { - Self::Calc(Calc(Arc::new(op))) - } -} - -/// Discriminator enum, should match `Val`'s unit types. -#[derive(Clone, Copy, PartialEq, Eq, Debug, Reflect)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[repr(u8)] -pub enum ValUnit { - /// Use `Px` units - Px, - /// Use `Percent` units - Percent, - /// Use `Vw` units - Vw, - Vh, - VMin, - VMax, - Em, - Rem, -} - -impl ValUnit { - /// Every unit, in discriminant order. - pub const ALL: [Self; 8] = [ - Self::Px, - Self::Percent, - Self::Vw, - Self::Vh, - Self::VMin, - Self::VMax, - Self::Em, - Self::Rem, - ]; - - const fn bit(self) -> u8 { - 1 << self as u8 - } -} - -/// If more unit types are added to `Val`, a `u8` discriminator won't be sufficient. -const _: () = assert!(ValUnit::ALL.len() <= u8::BITS as usize); - -/// A set of [`ValUnit`]s. -/// -/// [`Val::Sum`], [`Val::Min`] and [`Val::Max`] pair one of these with an array of values: -/// the `n`th value belongs to the `n`th unit in [`ValUnitSet::iter`] order, and the -/// slots after the last unit are unused. -#[derive(Clone, Copy, PartialEq, Eq, Hash, Default, Reflect)] -#[reflect(PartialEq, Debug, Clone)] -#[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), - reflect(Serialize, Deserialize) -)] -pub struct ValUnitSet(u8); - -impl ValUnitSet { - /// The empty set. - pub const EMPTY: Self = Self(0); - - /// The number of units in the set. - pub const fn len(&self) -> usize { - self.0.count_ones() as usize - } - - /// Whether the set has no units. - pub const fn is_empty(&self) -> bool { - self.0 == 0 - } - - /// Whether `unit` is in the set. - pub const fn contains(&self, unit: ValUnit) -> bool { - self.0 & unit.bit() != 0 - } - - /// The position `unit` has, or would have, among the set's units in ascending order. - pub const fn slot(&self, unit: ValUnit) -> usize { - (self.0 & (unit.bit() - 1)).count_ones() as usize - } - - /// The units in ascending order. - pub fn iter(self) -> impl Iterator { - ValUnit::ALL - .into_iter() - .filter(move |unit| self.contains(*unit)) - } - - const fn insert(&mut self, unit: ValUnit) { - self.0 |= unit.bit(); - } -} - -impl core::fmt::Debug for ValUnitSet { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_set().entries(self.iter()).finish() - } -} - -/// Working copy of the terms of a [`Val::Sum`], [`Val::Min`] or [`Val::Max`]. -#[derive(Clone, Copy)] -pub(crate) struct ValTerms { - units: ValUnitSet, - values: [f32; 3], -} - -impl ValTerms { - /// The maximum number of terms. - pub(crate) const CAPACITY: usize = 3; - - pub(crate) const fn new(units: ValUnitSet, values: [f32; 3]) -> Self { - Self { units, values } - } - - pub(crate) const fn into_parts(self) -> (ValUnitSet, [f32; 3]) { - (self.units, self.values) - } - - const fn single(unit: ValUnit, value: f32) -> Self { - let mut units = ValUnitSet::EMPTY; - units.insert(unit); - Self::new(units, [value, 0.0, 0.0]) - } - - /// The terms in ascending unit order. - pub(crate) fn terms(self) -> impl Iterator { - self.units.iter().zip(self.values) - } - - /// Folds `value` into the existing term for `unit` with `op`, or inserts it as a new term. - /// - /// Returns `false` and leaves `self` unchanged when a new term would exceed [`Self::CAPACITY`]. - fn fold(&mut self, unit: ValUnit, value: f32, op: fn(f32, f32) -> f32) -> bool { - let slot = self.units.slot(unit); - if self.units.contains(unit) { - self.values[slot] = op(self.values[slot], value); - } else if self.units.len() < Self::CAPACITY { - self.values[slot..].rotate_right(1); - self.values[slot] = value; - self.units.insert(unit); - } else { - return false; - } - true - } - - /// Removes the terms whose value is zero. - fn retain_nonzero(&mut self) { - let mut kept = Self::new(ValUnitSet::EMPTY, [0.0; 3]); - for (unit, value) in self.terms().filter(|(_, value)| *value != 0.0) { - kept.values[kept.units.len()] = value; - kept.units.insert(unit); - } - *self = kept; - } - - pub(crate) fn resolve( - &self, - op: fn(f32, f32) -> f32, - scale_factor: f32, - physical_base_value: f32, - physical_target_size: Vec2, - em_size: EmSize, - rem_size: RemSize, - ) -> f32 { - self.terms() - .map(|(unit, value)| { - Val::leaf(unit, value) - .resolve( - scale_factor, - physical_base_value, - physical_target_size, - em_size, - rem_size, - ) - .unwrap_or(0.0) - }) - .reduce(op) - .unwrap_or(0.0) - } - - /// Interpolates term-wise, or `None` when the units differ. - pub(crate) fn interpolate_stable(&self, other: &Self, t: f32) -> Option { - (self.units == other.units).then(|| { - let mut result = *self; - for (value, other) in result.values.iter_mut().zip(other.values) { - *value = value.interpolate_stable(&other, t); - } - result - }) - } -} - -impl PartialEq for ValTerms { - fn eq(&self, other: &Self) -> bool { - self.units == other.units && self.terms().eq(other.terms()) - } -} - -/// The operation a [`Val::Sum`], [`Val::Min`] or [`Val::Max`] applies to its terms. -#[derive(Clone, Copy)] -pub(crate) enum TermOp { - Sum, - Min, - Max, -} - -impl TermOp { - pub(crate) fn apply(self) -> fn(f32, f32) -> f32 { - match self { - Self::Sum => |a, b| a + b, - Self::Min => f32::min, - Self::Max => f32::max, - } - } - - /// The operation that gives the same result after every term is negated. - const fn reversed(self) -> Self { - match self { - Self::Sum => Self::Sum, - Self::Min => Self::Max, - Self::Max => Self::Min, - } - } - - /// The [`Val`] for `terms` under this operation, as a single unit value when there is one term. - fn wrap(self, terms: ValTerms) -> Val { - if terms.units.len() == 1 - && let Some((unit, value)) = terms.terms().next() - { - return Val::leaf(unit, value); - } - match self { - Self::Sum => Val::Sum(terms.units, terms.values), - Self::Min => Val::Min(terms.units, terms.values), - Self::Max => Val::Max(terms.units, terms.values), - } - } - - fn calc(self, a: Val, b: Val) -> Val { - match self { - Self::Sum => CalcOp::Add(a, b), - Self::Min => CalcOp::Min(a, b), - Self::Max => CalcOp::Max(a, b), - } - .into() - } -} - -impl Val { - /// The terms of a [`Val::Sum`], [`Val::Min`] or [`Val::Max`], or the single term of a - /// unit value. Empty for [`Val::Auto`], [`Val::Clamp`] and [`Val::Calc`]. - pub fn terms(&self) -> impl Iterator { - let terms = match self { - Val::Sum(units, values) | Val::Min(units, values) | Val::Max(units, values) => { - ValTerms::new(*units, *values) - } - _ => match (self.unit(), self.inner()) { - (Some(unit), Some(value)) => ValTerms::single(unit, value), - _ => ValTerms::new(ValUnitSet::EMPTY, [0.0; 3]), - }, - }; - terms.terms() - } - - /// Combines two values with `op`, folding them into one set of terms when both sides are - /// unit values or terms of the same operation, and building a [`Calc`] otherwise. - pub(crate) fn combine(self, rhs: Val, op: TermOp) -> Val { - if let (Some(mut terms), Some(rhs_terms)) = (self.terms_for(op), rhs.terms_for(op)) - && rhs_terms - .terms() - .all(|(unit, value)| terms.fold(unit, value, op.apply())) - { - return op.wrap(terms); - } - op.calc(self, rhs) - } - - fn terms_for(&self, op: TermOp) -> Option { - match (self, op) { - (Val::Sum(units, values), TermOp::Sum) - | (Val::Min(units, values), TermOp::Min) - | (Val::Max(units, values), TermOp::Max) => Some(ValTerms::new(*units, *values)), - _ => Some(ValTerms::single(self.unit()?, self.inner()?)), - } - } - - /// The terms of a [`Val::Sum`] or unit value with zero terms removed, so that sums - /// compare equal whatever zero terms they carry. `None` for other values. - pub(crate) fn sum_terms(&self) -> Option { - let mut terms = match self { - Val::Sum(units, values) => ValTerms::new(*units, *values), - _ => ValTerms::single(self.unit()?, self.inner()?), - }; - terms.retain_nonzero(); - Some(terms) - } - - /// The inline clamp of `value` between `min` and `max`, when all three are unit values. - pub(crate) fn clamp_inline(min: &Val, value: &Val, max: &Val) -> Option { - let term = |val: &Val| Some((val.unit()?, val.inner()?)); - let [lo, v, hi] = [term(min)?, term(value)?, term(max)?]; - if lo.0 == v.0 && v.0 == hi.0 { - return Some(Val::leaf(v.0, lo.1.max(v.1.min(hi.1)))); - } - // Inverted bounds always resolve to the minimum, so an inline clamp with same-unit - // bounds is guaranteed to have `min <= max`. - if lo.0 == hi.0 && lo.1 > hi.1 { - return Some(Val::leaf(lo.0, lo.1)); - } - Some(Val::Clamp([lo.0, v.0, hi.0], [lo.1, v.1, hi.1])) - } - - pub(crate) fn resolve_clamp( - units: [ValUnit; 3], - values: [f32; 3], - scale_factor: f32, - physical_base_value: f32, - physical_target_size: Vec2, - em_size: EmSize, - rem_size: RemSize, - ) -> f32 { - let [lo, v, hi] = core::array::from_fn(|i| { - Val::leaf(units[i], values[i]) - .resolve( - scale_factor, - physical_base_value, - physical_target_size, - em_size, - rem_size, - ) - .unwrap_or(0.0) - }); - lo.max(v.min(hi)) - } - - /// Applies `f` to every term of a [`Val::Sum`], [`Val::Min`], [`Val::Max`] or [`Val::Clamp`]. - /// `reverses` swaps `Min` and `Max` and the bounds of `Clamp`, for an `f` that reverses ordering. - pub(crate) fn map_terms(self, f: impl Fn(f32) -> f32, reverses: bool) -> Self { - let (op, units, mut values) = match self { - Val::Sum(units, values) => (TermOp::Sum, units, values), - Val::Min(units, values) => (TermOp::Min, units, values), - Val::Max(units, values) => (TermOp::Max, units, values), - Val::Clamp(units, values) => { - // Inverted same-unit bounds make the clamp its minimum whatever the value is. - if units[0] == units[2] && values[0] > values[2] { - return Val::leaf(units[0], f(values[0])); - } - let values = values.map(&f); - if !reverses { - return Val::Clamp(units, values); - } - // `f` reverses order, so `f(max(lo, min(v, hi)))` is `min(f(lo), max(f(v), f(hi)))`. - // Same-unit bounds are now known to satisfy `lo <= hi`, so that is the clamp with - // the bounds swapped; mixed-unit bounds may invert at some bases, so keep the - // exact form. - let [lo, v, hi] = core::array::from_fn(|i| Val::leaf(units[i], values[i])); - if units[0] == units[2] { - return Val::Clamp( - [units[2], units[1], units[0]], - [values[2], values[1], values[0]], - ); - } - return lo.min(v.max(hi)); - } - other => return other, - }; - for value in &mut values[..units.len()] { - *value = f(*value); - } - let op = if reverses { op.reversed() } else { op }; - op.wrap(ValTerms::new(units, values)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn val_fits_in_two_words() { - assert!(size_of::() <= 16); - } - - #[test] - fn sums_fold_inline() { - let sum = Val::Px(10.0) + Val::Percent(50.0); - assert!(matches!(sum, Val::Sum(..)), "got {sum:?}"); - assert_eq!( - sum.terms().collect::>(), - [(ValUnit::Px, 10.0), (ValUnit::Percent, 50.0)] - ); - assert_eq!(Val::Percent(50.0) + Val::Px(10.0), sum); - assert_eq!(Val::Px(1.0) + Val::Px(2.0), Val::Px(3.0)); - assert_eq!(sum.clone() - Val::Px(10.0), Val::Percent(50.0)); - assert_eq!(Val::Px(0.0) + Val::Percent(0.0), Val::ZERO); - assert_ne!(sum, Val::Px(10.0)); - assert_eq!( - (Val::Px(1.0) + Val::Percent(2.0)) + (Val::Px(3.0) + Val::Em(4.0)), - Val::Em(4.0) + Val::Px(4.0) + Val::Percent(2.0) - ); - assert!(matches!(sum + Val::Em(1.0) + Val::Rem(1.0), Val::Calc(_))); - } - - #[test] - fn min_and_max_fold_inline() { - assert_eq!(Val::Px(10.0).min(Val::Px(20.0)), Val::Px(10.0)); - assert_eq!(Val::Px(10.0).max(Val::Px(20.0)), Val::Px(20.0)); - let min = Val::Px(10.0).min(Val::Percent(50.0)); - assert!(matches!(min, Val::Min(..))); - assert_eq!(Val::Percent(50.0).min(Val::Px(10.0)), min); - assert_eq!(-min.clone(), Val::Px(-10.0).max(Val::Percent(-50.0))); - assert!(matches!(min.clone().max(Val::Px(5.0)), Val::Calc(_))); - let resolve = - |value: &Val| value.resolve(1.0, 100.0, Vec2::ZERO, EmSize(0.0), RemSize(0.0)); - assert_eq!(resolve(&min), Ok(10.0)); - assert_eq!(resolve(&Val::Px(10.0).max(Val::Percent(50.0))), Ok(50.0)); - assert_eq!(resolve(&(min * -2.0)), Ok(-20.0)); - } - - #[test] - fn negating_min_and_max_negates_their_results() { - assert_eq!(-Val::Px(10.0).max(Val::Px(20.0)), Val::Px(-20.0)); - assert_eq!(-Val::Px(10.0).min(Val::Px(20.0)), Val::Px(-10.0)); - let resolve = |value: &Val, base: f32| { - value.resolve(1.0, base, Vec2::ZERO, EmSize(0.0), RemSize(0.0)) - }; - for expr in [ - Val::Px(10.0).max(Val::Percent(50.0)), - Val::Px(10.0).min(Val::Percent(50.0)), - ] { - for base in [0.0, 20.0, 100.0] { - let expected = resolve(&expr, base).unwrap(); - assert_eq!( - resolve(&-expr.clone(), base), - Ok(-expected), - "{expr:?} at {base}" - ); - assert_eq!( - resolve(&(expr.clone() * -2.0), base), - Ok(-2.0 * expected), - "{expr:?} at {base}" - ); - } - } - } - - #[test] - fn negating_a_clamp_negates_its_result() { - let clamp = Val::Percent(50.0).clamp(Val::Px(100.0), Val::Percent(20.0)); - let resolve = |value: &Val, base: f32| { - value.resolve(1.0, base, Vec2::ZERO, EmSize(0.0), RemSize(0.0)) - }; - for base in [50.0, 100.0, 150.0, 300.0, 1000.0] { - let expected = resolve(&clamp, base).unwrap(); - assert_eq!(resolve(&-clamp.clone(), base), Ok(-expected), "base {base}"); - assert_eq!( - resolve(&(clamp.clone() * -2.0), base), - Ok(-2.0 * expected), - "base {base}" - ); - assert_eq!( - resolve(&(Val::ZERO - clamp.clone()), base), - Ok(-expected), - "base {base}" - ); - } - } - - #[test] - fn clamp_folds_inline() { - use bevy_math::TryStableInterpolate; - let clamp = Val::Percent(80.0).clamp(Val::Px(100.0), Val::Px(300.0)); - assert!(matches!(clamp, Val::Clamp(..)), "got {clamp:?}"); - assert_eq!( - Val::Percent(80.0).clamp(Val::Px(300.0), Val::Px(100.0)), - Val::Px(300.0) - ); - let resolve = |value: &Val, base: f32| { - value.resolve(1.0, base, Vec2::ZERO, EmSize(0.0), RemSize(0.0)) - }; - assert_eq!(resolve(&clamp, 100.0), Ok(100.0)); - assert_eq!(resolve(&clamp, 250.0), Ok(200.0)); - assert_eq!(resolve(&clamp, 1000.0), Ok(300.0)); - assert_eq!( - -clamp.clone(), - Val::Percent(-80.0).clamp(Val::Px(-300.0), Val::Px(-100.0)) - ); - assert_eq!(resolve(&(clamp.clone() * -2.0), 250.0), Ok(-400.0)); - assert_eq!( - clamp.clone() * 2.0, - Val::Percent(160.0).clamp(Val::Px(200.0), Val::Px(600.0)) - ); - assert_eq!( - clamp - .try_interpolate_stable(&Val::Percent(0.0).clamp(Val::Px(0.0), Val::Px(100.0)), 0.5) - .ok(), - Some(Val::Percent(40.0).clamp(Val::Px(50.0), Val::Px(200.0))) - ); - assert!(matches!( - (Val::Percent(80.0) + Val::Px(1.0)).clamp(Val::Px(100.0), Val::Px(300.0)), - Val::Calc(_) - )); - } - - #[test] - fn terms_interpolate_term_wise() { - use bevy_math::TryStableInterpolate; - let a = Val::Px(0.0) + Val::Percent(0.0); - let b = Val::Px(10.0) + Val::Percent(100.0); - assert_eq!( - a.try_interpolate_stable(&b, 0.5).ok(), - Some(Val::Px(5.0) + Val::Percent(50.0)) - ); - assert!(a - .try_interpolate_stable(&(Val::Px(1.0) + Val::Em(1.0)), 0.5) - .is_err()); - } - - #[test] - fn resolve_expressions() { - let value = - Val::Percent(50.0) - Val::Px(10.0) + Val::Em(2.0) + Val::Rem(1.0) + Val::Vw(1.0); - let resolve = |value: &Val| { - value.resolve( - 2.0, - 400.0, - Vec2::new(1000.0, 500.0), - EmSize(12.0), - RemSize(20.0), - ) - }; - assert_eq!(resolve(&value), Ok(278.0)); - assert_eq!(resolve(&(value.clone() * 2.0)), Ok(556.0)); - assert_eq!(resolve(&(value.clone() / 2.0)), Ok(139.0)); - assert_eq!(resolve(&-value.clone()), Ok(-278.0)); - assert_eq!( - resolve(&CalcOp::Min(value.clone(), Val::Px(100.0)).into()), - Ok(200.0) - ); - assert_eq!( - resolve(&CalcOp::Max(value.clone(), Val::Px(200.0)).into()), - Ok(400.0) - ); - assert_eq!( - resolve(&CalcOp::Clamp(Val::Px(100.0), value.clone(), Val::Px(125.0)).into()), - Ok(250.0) - ); - assert_eq!( - resolve(&CalcOp::Clamp(Val::Px(200.0), value.clone(), Val::Px(100.0)).into()), - Ok(400.0) - ); - let mut scaled = value; - scaled *= 2.0; - scaled /= 4.0; - assert_eq!(resolve(&scaled), Ok(139.0)); - assert_eq!( - resolve(&(Val::Auto + Val::Px(1.0))), - Err(ValArithmeticError::NonEvaluable) - ); - } - - #[test] - fn negative_scale_swaps_min_and_max() { - let resolve = |value: Val| value.resolve(1.0, 0.0, Vec2::ZERO, EmSize(0.0), RemSize(0.0)); - let (a, b) = (Val::Px(10.0), Val::Px(20.0)); - assert_eq!( - resolve(-Val::from(CalcOp::Min(a.clone(), b.clone()))), - Ok(-10.0) - ); - assert_eq!( - resolve(-Val::from(CalcOp::Max(a.clone(), b.clone()))), - Ok(-20.0) - ); - assert_eq!( - resolve(-Val::from(CalcOp::Clamp( - a.clone(), - Val::Px(15.0), - b.clone() - ))), - Ok(-15.0) - ); - assert_eq!( - resolve(-Val::from(CalcOp::Clamp(b, Val::Px(15.0), a))), - Ok(-20.0) - ); - } - - #[test] - fn repeated_scaling_does_not_grow_the_tree() { - let mut value = Val::Px(1.0) + Val::Px(2.0); - for _ in 0..100_000 { - value *= 2.0; - value /= 2.0; - } - assert_eq!(value, Val::Px(1.0) + Val::Px(2.0)); - } - - #[test] - fn clone_and_reflection_preserve_expressions() { - use bevy_reflect::{FromReflect, PartialReflect, TypeRegistry}; - - let value = Val::Percent(100.0) - Val::Px(24.0); - let mut cloned = value.clone(); - assert_eq!(value, cloned); - cloned *= 2.0; - assert_ne!(value, cloned); - assert_eq!(value, Val::Percent(100.0) - Val::Px(24.0)); - let mut registry = TypeRegistry::default(); - registry.register::(); - let dynamic = value.to_dynamic().unwrap(); - assert_eq!(Val::from_reflect(dynamic.as_ref()), Some(value)); - } - - #[cfg(feature = "serialize")] - #[test] - fn expressions_round_trip_through_serde() { - let values = [ - Val::Percent(50.0) - Val::Px(10.0), - Val::Px(10.0).min(Val::Percent(50.0)), - Val::Percent(80.0).clamp(Val::Px(100.0), Val::Px(300.0)), - CalcOp::Min(Val::Em(1.0) + Val::Percent(10.0), Val::Rem(1.0)).into(), - ]; - for value in values { - let ron = ron::to_string(&value).unwrap(); - assert_eq!(ron::from_str::(&ron).unwrap(), value, "{ron}"); - } - } - - #[test] - fn font_size_rejects_expressions() { - assert_eq!( - bevy_text::FontSize::try_from(Val::Rem(2.0)), - Ok(bevy_text::FontSize::Rem(2.0)) - ); - assert_eq!( - bevy_text::FontSize::try_from(Val::Percent(100.0) - Val::Px(24.0)), - Err(ValArithmeticError::NonEvaluable) - ); - } - - #[test] - fn direct_inline_clamp_negates_its_resolved_value() { - let value = Val::Clamp( - [ValUnit::Px, ValUnit::Percent, ValUnit::Px], - [100.0, 50.0, 20.0], - ); - let resolve = |value: Val| { - value - .resolve( - 1.0, - 100.0, - Default::default(), - Default::default(), - Default::default(), - ) - .unwrap() - }; - assert_eq!(resolve(value.clone()), 100.0); - assert_eq!(resolve(-value), -100.0); - } - - #[test] - fn extrapolated_inline_clamp_negates_its_resolved_value() { - let a = Val::Percent(50.0).clamp(Val::Px(0.0), Val::Px(100.0)); - let b = Val::Percent(50.0).clamp(Val::Px(100.0), Val::Px(100.0)); - let value = bevy_math::TryStableInterpolate::try_interpolate_stable(&a, &b, 2.0).unwrap(); - let resolve = |value: Val| { - value - .resolve( - 1.0, - 100.0, - Default::default(), - Default::default(), - Default::default(), - ) - .unwrap() - }; - assert_eq!(resolve(value.clone()), 200.0); - assert_eq!(resolve(-value), -200.0); - } -} diff --git a/crates/bevy_ui/src/geometry.rs b/crates/bevy_ui/src/geometry.rs index 2a0944eb309c0..df25cb492b424 100644 --- a/crates/bevy_ui/src/geometry.rs +++ b/crates/bevy_ui/src/geometry.rs @@ -1,841 +1,12 @@ -use bevy_math::{MismatchedUnitsError, StableInterpolate as _, TryStableInterpolate, Vec2}; -use bevy_platform::sync::Arc; +use crate::val::*; +use bevy_math::Vec2; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use bevy_text::{EmSize, FontSize, RemSize}; +use bevy_text::{EmSize, RemSize}; use bevy_utils::default; -use core::ops::{Add, Div, DivAssign, Mul, MulAssign, Neg, Sub}; -use thiserror::Error; #[cfg(feature = "serialize")] use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; -use crate::calc::{Calc, CalcOp, TermOp, ValTerms, ValUnit, ValUnitSet}; - -/// Represents the possible value types for layout properties. -/// -/// This enum allows specifying values for various [`Node`](crate::Node) properties in different units, -/// such as logical pixels, percentages, or automatically determined values. -/// -/// `Val` also implements [`core::str::FromStr`] to allow parsing values from strings in the format `#.#px`. Whitespaces between the value and unit is allowed. The following units are supported: -/// * `px`: logical pixels -/// * `%`: percentage -/// * `vw`: percentage of the viewport width -/// * `vh`: percentage of the viewport height -/// * `vmin`: percentage of the viewport's smaller dimension -/// * `vmax`: percentage of the viewport's larger dimension -/// -/// Additionally, `auto` will be parsed as [`Val::Auto`]. -#[derive(Clone, Debug, Reflect)] -#[reflect(Default, PartialEq, Debug, Clone)] -#[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), - reflect(Serialize, Deserialize) -)] -pub enum Val { - /// Automatically determine the value based on the context and other [`Node`](crate::Node) properties. - Auto, - /// Set this value in logical pixels. - Px(f32), - /// Set the value as a percentage of its parent node's length along a specific axis. - /// - /// If the UI node has no parent, the percentage is calculated based on the window's length - /// along the corresponding axis. - /// - /// The chosen axis depends on the [`Node`](crate::Node) field set: - /// * For `flex_basis`, the percentage is relative to the main-axis length determined by the `flex_direction`. - /// * For `gap`, `min_size`, `size`, and `max_size`: - /// - `width` is relative to the parent's width. - /// - `height` is relative to the parent's height. - /// * 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), - /// Set this value as a multiple of the node's own font size. - /// - /// `1em` is the node's font size, so `Val::Em(2.0)` is twice that. - /// Resolved from the node's [`EmSize`] component. - Em(f32), - /// Set this value as a multiple of the root font size. - /// - /// Unlike [`Val::Em`] this ignores the node's own font size, so it means the same - /// length anywhere in the hierarchy. Resolved from the [`RemSize`] resource. - Rem(f32), - /// The sum of up to three terms with distinct units, such as `Val::Percent(50.0) - Val::Px(10.0)`. - /// - /// The `n`th value belongs to the `n`th unit of the set, see [`ValUnitSet`]. - /// Built by `+` and `-`; longer expressions fall back to [`Val::Calc`]. - Sum(ValUnitSet, [f32; 3]), - /// The smallest of up to three terms with distinct units, laid out like [`Val::Sum`]. - /// Built by [`Val::min`]. - Min(ValUnitSet, [f32; 3]), - /// The largest of up to three terms with distinct units, laid out like [`Val::Sum`]. - /// Built by [`Val::max`]. - Max(ValUnitSet, [f32; 3]), - /// A value clamped between a minimum and a maximum, each a unit value, stored as the - /// `[min, value, max]` units and values. Built by [`Val::clamp`]; when an operand is itself - /// an expression it falls back to [`Val::Calc`]. - Clamp([ValUnit; 3], [f32; 3]), - Calc(Calc), -} - -#[derive(Debug, Error, PartialEq, Eq)] -pub enum ValParseError { - UnitMissing, - ValueMissing, - InvalidValue, - InvalidUnit, -} - -impl core::fmt::Display for ValParseError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - ValParseError::UnitMissing => write!(f, "unit missing"), - ValParseError::ValueMissing => write!(f, "value missing"), - ValParseError::InvalidValue => write!(f, "invalid value"), - ValParseError::InvalidUnit => write!(f, "invalid unit"), - } - } -} - -impl core::str::FromStr for Val { - type Err = ValParseError; - - fn from_str(s: &str) -> Result { - let s = s.trim(); - - if s.eq_ignore_ascii_case("auto") { - return Ok(Val::Auto); - } - - let Some(end_of_number) = s - .bytes() - .position(|c| !(c.is_ascii_digit() || c == b'.' || c == b'-' || c == b'+')) - else { - return Err(ValParseError::UnitMissing); - }; - - if end_of_number == 0 { - return Err(ValParseError::ValueMissing); - } - - let (value, unit) = s.split_at(end_of_number); - - let value: f32 = value.parse().map_err(|_| ValParseError::InvalidValue)?; - - let unit = unit.trim(); - - if unit.eq_ignore_ascii_case("px") { - Ok(Val::Px(value)) - } else if unit.eq_ignore_ascii_case("%") { - Ok(Val::Percent(value)) - } else if unit.eq_ignore_ascii_case("vw") { - Ok(Val::Vw(value)) - } else if unit.eq_ignore_ascii_case("vh") { - Ok(Val::Vh(value)) - } else if unit.eq_ignore_ascii_case("vmin") { - Ok(Val::VMin(value)) - } else if unit.eq_ignore_ascii_case("vmax") { - Ok(Val::VMax(value)) - } else if unit.eq_ignore_ascii_case("rem") { - Ok(Val::Rem(value)) - } else if unit.eq_ignore_ascii_case("em") { - Ok(Val::Em(value)) - } else { - Err(ValParseError::InvalidUnit) - } - } -} - -impl PartialEq for Val { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Self::Calc(a), Self::Calc(b)) => return a == b, - (Self::Min(a, x), Self::Min(b, y)) | (Self::Max(a, x), Self::Max(b, y)) => { - return ValTerms::new(*a, *x) == ValTerms::new(*b, *y); - } - (Self::Clamp(a, x), Self::Clamp(b, y)) => return a == b && x == y, - (Self::Sum(..), _) | (_, Self::Sum(..)) => { - return self.sum_terms() == other.sum_terms(); - } - _ => {} - } - let same_unit = matches!( - (self, other), - (Self::Auto, Self::Auto) - | (Self::Px(_), Self::Px(_)) - | (Self::Percent(_), Self::Percent(_)) - | (Self::Vw(_), Self::Vw(_)) - | (Self::Vh(_), Self::Vh(_)) - | (Self::VMin(_), Self::VMin(_)) - | (Self::VMax(_), Self::VMax(_)) - | (Self::Em(_), Self::Em(_)) - | (Self::Rem(_), Self::Rem(_)) - ); - - // as long as the units are the same comparing `inner` - // is valid. - let left = self.inner(); - let right = other.inner(); - - match (same_unit, left, right) { - (true, a, b) => a == b, - // All zero-value variants are considered equal. - (false, Some(a), Some(b)) => a == 0. && b == 0., - _ => false, - } - } -} - -impl Val { - pub const DEFAULT: Self = Self::Auto; - - /// Zero. In [`Val::Px`], but zero is the same in any unit. - pub const ZERO: Self = Self::Px(0.0); - - /// The largest finite [`Val::Px`] value. - pub const MAX: Self = Self::Px(f32::MAX); - - /// Returns the number this `Val` wraps, whatever its unit, or `None` for [`Val::Auto`] and the expression variants. - /// - /// The number is meaningless without the variant it came from — `Val::Px(4.0)` and - /// `Val::Rem(4.0)` both return `4.0`. Use [`Val::resolve`] for a length in pixels. - pub(crate) const fn inner(&self) -> Option { - match self { - Self::Auto - | Self::Sum(..) - | Self::Min(..) - | Self::Max(..) - | Self::Clamp(..) - | Self::Calc(_) => None, - Self::Px(v) - | Self::Percent(v) - | Self::Vw(v) - | Self::Vh(v) - | Self::VMin(v) - | Self::VMax(v) - | Self::Em(v) - | Self::Rem(v) => Some(*v), - } - } - - /// The unit of a single-term value, or `None` for [`Val::Auto`] and the expression variants. - pub const fn unit(&self) -> Option { - Some(match self { - Self::Px(_) => ValUnit::Px, - Self::Percent(_) => ValUnit::Percent, - Self::Vw(_) => ValUnit::Vw, - Self::Vh(_) => ValUnit::Vh, - Self::VMin(_) => ValUnit::VMin, - Self::VMax(_) => ValUnit::VMax, - Self::Em(_) => ValUnit::Em, - Self::Rem(_) => ValUnit::Rem, - Self::Auto - | Self::Sum(..) - | Self::Min(..) - | Self::Max(..) - | Self::Clamp(..) - | Self::Calc(_) => { - return None; - } - }) - } - - /// A single-term value of the given `unit`. - pub const fn leaf(unit: ValUnit, value: f32) -> Self { - match unit { - ValUnit::Px => Self::Px(value), - ValUnit::Percent => Self::Percent(value), - ValUnit::Vw => Self::Vw(value), - ValUnit::Vh => Self::Vh(value), - ValUnit::VMin => Self::VMin(value), - ValUnit::VMax => Self::VMax(value), - ValUnit::Em => Self::Em(value), - ValUnit::Rem => Self::Rem(value), - } - } - - pub(crate) fn uses_percent(&self) -> bool { - match self { - Self::Percent(_) => true, - Self::Sum(units, _) | Self::Min(units, _) | Self::Max(units, _) => { - units.contains(ValUnit::Percent) - } - Self::Clamp(units, _) => units.contains(&ValUnit::Percent), - Self::Calc(calc) => calc.0.uses_percent(), - _ => false, - } - } - - /// Returns a [`UiRect`] with its `left` equal to this value, - /// and all other fields set to `Val::ZERO`. - /// - /// - /// # Example - /// - /// ``` - /// # use bevy_ui::{UiRect, Val}; - /// # - /// let ui_rect = Val::Px(1.).left(); - /// - /// assert_eq!(ui_rect.left, Val::Px(1.)); - /// assert_eq!(ui_rect.right, Val::ZERO); - /// assert_eq!(ui_rect.top, Val::ZERO); - /// assert_eq!(ui_rect.bottom, Val::ZERO); - /// ``` - pub fn left(self) -> UiRect { - UiRect::left(self) - } - - /// Returns a [`UiRect`] with its `right` equal to this value, - /// and all other fields set to `Val::ZERO`. - /// - /// - /// # Example - /// - /// ``` - /// # use bevy_ui::{UiRect, Val}; - /// # - /// let ui_rect = Val::Px(1.).right(); - /// - /// assert_eq!(ui_rect.left, Val::ZERO); - /// assert_eq!(ui_rect.right, Val::Px(1.)); - /// assert_eq!(ui_rect.top, Val::ZERO); - /// assert_eq!(ui_rect.bottom, Val::ZERO); - /// ``` - pub fn right(self) -> UiRect { - UiRect::right(self) - } - - /// Returns a [`UiRect`] with its `top` equal to this value, - /// and all other fields set to `Val::ZERO`. - /// - /// - /// # Example - /// - /// ``` - /// # use bevy_ui::{UiRect, Val}; - /// # - /// let ui_rect = Val::Px(1.).top(); - /// - /// assert_eq!(ui_rect.left, Val::ZERO); - /// assert_eq!(ui_rect.right, Val::ZERO); - /// assert_eq!(ui_rect.top, Val::Px(1.)); - /// assert_eq!(ui_rect.bottom, Val::ZERO); - /// ``` - pub fn top(self) -> UiRect { - UiRect::top(self) - } - - /// Returns a [`UiRect`] with its `bottom` equal to this value, - /// and all other fields set to `Val::ZERO`. - /// - /// - /// # Example - /// - /// ``` - /// # use bevy_ui::{UiRect, Val}; - /// # - /// let ui_rect = Val::Px(1.).bottom(); - /// - /// assert_eq!(ui_rect.left, Val::ZERO); - /// assert_eq!(ui_rect.right, Val::ZERO); - /// assert_eq!(ui_rect.top, Val::ZERO); - /// assert_eq!(ui_rect.bottom, Val::Px(1.)); - /// ``` - pub fn bottom(self) -> UiRect { - UiRect::bottom(self) - } - - /// Returns a [`UiRect`] with all its fields equal to this value. - /// - /// # Example - /// - /// ``` - /// # use bevy_ui::{UiRect, Val}; - /// # - /// let ui_rect = Val::Px(1.).all(); - /// - /// assert_eq!(ui_rect.left, Val::Px(1.)); - /// assert_eq!(ui_rect.right, Val::Px(1.)); - /// assert_eq!(ui_rect.top, Val::Px(1.)); - /// assert_eq!(ui_rect.bottom, Val::Px(1.)); - /// ``` - pub fn all(self) -> UiRect { - UiRect::all(self) - } - - /// Returns a [`UiRect`] with all its `left` and `right` equal to this value, - /// and its `top` and `bottom` set to `Val::ZERO`. - /// - /// # Example - /// - /// ``` - /// # use bevy_ui::{UiRect, Val}; - /// # - /// let ui_rect = Val::Px(1.).horizontal(); - /// - /// assert_eq!(ui_rect.left, Val::Px(1.)); - /// assert_eq!(ui_rect.right, Val::Px(1.)); - /// assert_eq!(ui_rect.top, Val::ZERO); - /// assert_eq!(ui_rect.bottom, Val::ZERO); - /// ``` - pub fn horizontal(self) -> UiRect { - UiRect::horizontal(self) - } - - /// Returns a [`UiRect`] with all its `top` and `bottom` equal to this value, - /// and its `left` and `right` set to `Val::ZERO`. - /// - /// # Example - /// - /// ``` - /// # use bevy_ui::{UiRect, Val}; - /// # - /// let ui_rect = Val::Px(1.).vertical(); - /// - /// assert_eq!(ui_rect.left, Val::ZERO); - /// assert_eq!(ui_rect.right, Val::ZERO); - /// assert_eq!(ui_rect.top, Val::Px(1.)); - /// assert_eq!(ui_rect.bottom, Val::Px(1.)); - /// ``` - pub fn vertical(self) -> UiRect { - UiRect::vertical(self) - } - - /// Try to add two `Val`s. - /// - /// Returns [`ValArithmeticError::IncompatibleUnits`] if the units differ or both are `Val::Auto`. - /// - /// ``` - /// # use bevy_ui::{Val, ValArithmeticError}; - /// assert_eq!(Val::Px(1.).try_add(Val::Px(2.)), Ok(Val::Px(3.))); - /// assert_eq!( - /// Val::Px(1.).try_add(Val::Percent(2.)), - /// Err(ValArithmeticError::IncompatibleUnits) - /// ); - /// ``` - pub fn try_add(self, val: Val) -> Result { - match (self, val) { - (Val::Px(u), Val::Px(v)) => Ok(Val::Px(u + v)), - (Val::Percent(u), Val::Percent(v)) => Ok(Val::Percent(u + v)), - (Val::Vw(u), Val::Vw(v)) => Ok(Val::Vw(u + v)), - (Val::Vh(u), Val::Vh(v)) => Ok(Val::Vh(u + v)), - (Val::VMin(u), Val::VMin(v)) => Ok(Val::VMin(u + v)), - (Val::VMax(u), Val::VMax(v)) => Ok(Val::VMax(u + v)), - (Val::Em(u), Val::Em(v)) => Ok(Val::Em(u + v)), - (Val::Rem(u), Val::Rem(v)) => Ok(Val::Rem(u + v)), - _ => Err(ValArithmeticError::IncompatibleUnits), - } - } - - /// Try to subtract one `Val` from another. - /// - /// Returns [`ValArithmeticError::IncompatibleUnits`] if the units differ or both are `Val::Auto`. - /// - /// ``` - /// # use bevy_ui::{Val, ValArithmeticError}; - /// assert_eq!(Val::Px(3.).try_sub(Val::Px(2.)), Ok(Val::Px(1.))); - /// assert_eq!( - /// Val::Px(1.).try_sub(Val::Percent(2.)), - /// Err(ValArithmeticError::IncompatibleUnits) - /// ); - /// ``` - pub fn try_sub(self, val: Val) -> Result { - match (self, val) { - (Val::Px(u), Val::Px(v)) => Ok(Val::Px(u - v)), - (Val::Percent(u), Val::Percent(v)) => Ok(Val::Percent(u - v)), - (Val::Vw(u), Val::Vw(v)) => Ok(Val::Vw(u - v)), - (Val::Vh(u), Val::Vh(v)) => Ok(Val::Vh(u - v)), - (Val::VMin(u), Val::VMin(v)) => Ok(Val::VMin(u - v)), - (Val::VMax(u), Val::VMax(v)) => Ok(Val::VMax(u - v)), - (Val::Em(u), Val::Em(v)) => Ok(Val::Em(u - v)), - (Val::Rem(u), Val::Rem(v)) => Ok(Val::Rem(u - v)), - _ => Err(ValArithmeticError::IncompatibleUnits), - } - } - - /// The smaller of `self` and `other` once resolved. - pub fn min(self, other: Val) -> Val { - self.combine(other, TermOp::Min) - } - - /// The larger of `self` and `other` once resolved. - pub fn max(self, other: Val) -> Val { - self.combine(other, TermOp::Max) - } - - /// `self` clamped between `min` and `max` once resolved. - pub fn clamp(self, min: Val, max: Val) -> Val { - Val::clamp_inline(&min, &self, &max).unwrap_or_else(|| CalcOp::Clamp(min, self, max).into()) - } -} - -impl Default for Val { - fn default() -> Self { - Self::DEFAULT - } -} - -impl Add for Val { - type Output = Self; - - fn add(self, rhs: Self) -> Self { - self.combine(rhs, TermOp::Sum) - } -} - -impl Sub for Val { - type Output = Self; - - fn sub(self, rhs: Self) -> Self { - self.combine(-rhs, TermOp::Sum) - } -} - -impl Mul for Val { - type Output = Val; - - fn mul(self, rhs: f32) -> Self::Output { - match self { - 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), - Val::Em(value) => Val::Em(value * rhs), - Val::Rem(value) => Val::Rem(value * rhs), - terms @ (Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Clamp(..)) => { - terms.map_terms(|value| value * rhs, rhs < 0.0) - } - Val::Calc(mut calc) => { - Arc::make_mut(&mut calc.0).scale(rhs); - Val::Calc(calc) - } - } - } -} - -impl MulAssign for Val { - fn mul_assign(&mut self, rhs: f32) { - match self { - Val::Auto => {} - Val::Px(value) - | Val::Percent(value) - | Val::Vw(value) - | Val::Vh(value) - | Val::VMin(value) - | Val::VMax(value) - | Val::Em(value) - | Val::Rem(value) => *value *= rhs, - Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Clamp(..) => { - *self = core::mem::take(self).map_terms(|value| value * rhs, rhs < 0.0); - } - Val::Calc(calc) => Arc::make_mut(&mut calc.0).scale(rhs), - } - } -} - -impl Div for Val { - type Output = Val; - - fn div(self, rhs: f32) -> Self::Output { - match self { - 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), - Val::Em(value) => Val::Em(value / rhs), - Val::Rem(value) => Val::Rem(value / rhs), - terms @ (Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Clamp(..)) => { - terms.map_terms(|value| value / rhs, rhs < 0.0) - } - Val::Calc(mut calc) => { - Arc::make_mut(&mut calc.0).scale(rhs.recip()); - Val::Calc(calc) - } - } - } -} - -impl DivAssign for Val { - fn div_assign(&mut self, rhs: f32) { - match self { - Val::Auto => {} - Val::Px(value) - | Val::Percent(value) - | Val::Vw(value) - | Val::Vh(value) - | Val::VMin(value) - | Val::VMax(value) - | Val::Em(value) - | Val::Rem(value) => *value /= rhs, - Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Clamp(..) => { - *self = core::mem::take(self).map_terms(|value| value / rhs, rhs < 0.0); - } - Val::Calc(calc) => Arc::make_mut(&mut calc.0).scale(rhs.recip()), - } - } -} - -impl Neg for Val { - type Output = Val; - - fn neg(self) -> Self::Output { - match self { - Val::Auto => Val::Auto, - Val::Px(value) => Val::Px(-value), - Val::Percent(value) => Val::Percent(-value), - Val::Vw(value) => Val::Vw(-value), - Val::Vh(value) => Val::Vh(-value), - Val::VMin(value) => Val::VMin(-value), - Val::VMax(value) => Val::VMax(-value), - Val::Em(value) => Val::Em(-value), - Val::Rem(value) => Val::Rem(-value), - terms @ (Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Clamp(..)) => { - terms.map_terms(|value| -value, true) - } - Val::Calc(mut calc) => { - Arc::make_mut(&mut calc.0).scale(-1.0); - Val::Calc(calc) - } - } - } -} - -#[derive(Debug, Eq, PartialEq, Clone, Copy, Error)] -pub enum ValArithmeticError { - #[error("the given variant of Val is not evaluable (non-numeric)")] - NonEvaluable, - #[error("the given variants of Val have incompatible units")] - IncompatibleUnits, -} - -impl Val { - /// Resolves this [`Val`] to a value in physical pixels from the given `scale_factor`, `physical_base_value`, - /// `physical_target_size`, `em_size`, and `rem_size` context values. - /// - /// Returns a [`ValArithmeticError::NonEvaluable`] if the [`Val`] is impossible to resolve into a concrete value. - pub fn resolve( - &self, - scale_factor: f32, - physical_base_value: f32, - physical_target_size: Vec2, - em_size: EmSize, - rem_size: RemSize, - ) -> Result { - match self { - Val::Percent(value) => Ok(physical_base_value * value / 100.0), - Val::Px(value) => Ok(value * scale_factor), - Val::Vw(value) => Ok(physical_target_size.x * value / 100.0), - Val::Vh(value) => Ok(physical_target_size.y * value / 100.0), - Val::VMin(value) => { - Ok(physical_target_size.x.min(physical_target_size.y) * value / 100.0) - } - Val::VMax(value) => { - Ok(physical_target_size.x.max(physical_target_size.y) * value / 100.0) - } - Val::Em(value) => Ok(value * em_size.0 * scale_factor), - Val::Rem(value) => Ok(value * rem_size.0 * scale_factor), - Val::Auto => Err(ValArithmeticError::NonEvaluable), - Val::Sum(units, values) | Val::Min(units, values) | Val::Max(units, values) => { - let op = match self { - Val::Min(..) => TermOp::Min, - Val::Max(..) => TermOp::Max, - _ => TermOp::Sum, - }; - Ok(ValTerms::new(*units, *values).resolve( - op.apply(), - scale_factor, - physical_base_value, - physical_target_size, - em_size, - rem_size, - )) - } - Val::Clamp(units, values) => Ok(Val::resolve_clamp( - *units, - *values, - scale_factor, - physical_base_value, - physical_target_size, - em_size, - rem_size, - )), - Val::Calc(op) => op.0.resolve( - scale_factor, - physical_base_value, - physical_target_size, - em_size, - rem_size, - ), - } - } -} - -impl TryFrom for FontSize { - type Error = ValArithmeticError; - - /// Converts a [`Val`] into a [`FontSize`]. - /// - /// [`Val::Em`] becomes [`FontSize::Rem`]: em is relative to the font size of the node - /// itself, which is the very thing being defined here, so it resolves against the root - /// size instead to avoid the circularity. - fn try_from(val: Val) -> Result { - Ok(match val { - Val::Auto => FontSize::Rem(1.), - Val::Px(px) => FontSize::Px(px), - Val::Percent(percent) => FontSize::Rem(percent / 100.), - Val::Vw(vw) => FontSize::Vw(vw), - Val::Vh(vh) => FontSize::Vh(vh), - Val::VMin(vmin) => FontSize::VMin(vmin), - Val::VMax(vmax) => FontSize::VMax(vmax), - Val::Rem(rem) => FontSize::Rem(rem), - Val::Em(em) => FontSize::Rem(em), // see comment above - Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Clamp(..) | Val::Calc(_) => { - return Err(ValArithmeticError::NonEvaluable); - } - }) - } -} - -impl TryStableInterpolate for Val { - type Error = MismatchedUnitsError; - - /// # Example - /// - /// ``` - /// # use bevy_ui::Val; - /// # use bevy_math::TryStableInterpolate; - /// assert!(matches!(Val::Px(0.0).try_interpolate_stable(&Val::Px(10.0), 0.5), Ok(Val::Px(5.0)))); - /// ``` - fn try_interpolate_stable(&self, other: &Self, t: f32) -> Result { - match (self, other) { - (Val::Px(a), Val::Px(b)) => Ok(Val::Px(a.interpolate_stable(b, t))), - (Val::Percent(a), Val::Percent(b)) => Ok(Val::Percent(a.interpolate_stable(b, t))), - (Val::Vw(a), Val::Vw(b)) => Ok(Val::Vw(a.interpolate_stable(b, t))), - (Val::Vh(a), Val::Vh(b)) => Ok(Val::Vh(a.interpolate_stable(b, t))), - (Val::VMin(a), Val::VMin(b)) => Ok(Val::VMin(a.interpolate_stable(b, t))), - (Val::VMax(a), Val::VMax(b)) => Ok(Val::VMax(a.interpolate_stable(b, t))), - (Val::Em(a), Val::Em(b)) => Ok(Val::Em(a.interpolate_stable(b, t))), - (Val::Rem(a), Val::Rem(b)) => Ok(Val::Rem(a.interpolate_stable(b, t))), - (Val::Sum(a, x), Val::Sum(b, y)) - | (Val::Min(a, x), Val::Min(b, y)) - | (Val::Max(a, x), Val::Max(b, y)) => { - let (units, values) = ValTerms::new(*a, *x) - .interpolate_stable(&ValTerms::new(*b, *y), t) - .ok_or(MismatchedUnitsError)? - .into_parts(); - Ok(match self { - Val::Min(..) => Val::Min(units, values), - Val::Max(..) => Val::Max(units, values), - _ => Val::Sum(units, values), - }) - } - (Val::Clamp(a, x), Val::Clamp(b, y)) if a == b => Ok(Val::Clamp( - *a, - core::array::from_fn(|i| x[i].interpolate_stable(&y[i], t)), - )), - (Val::Auto, Val::Auto) => Ok(Val::Auto), - _ => Err(MismatchedUnitsError), - } - } -} - -/// All the types that should be able to be used in the [`Val`] enum should implement this trait. -/// -/// Instead of just implementing `Into` a custom trait is added. -/// This is done in order to prevent having to define a default unit, which could lead to confusion especially for newcomers. -pub trait ValNum { - /// Called by the [`Val`] helper functions to convert the implementing type to an `f32` that can - /// be used by [`Val`]. - fn val_num_f32(self) -> f32; -} - -macro_rules! impl_to_val_num { - ($($impl_type:ty),*$(,)?) => { - $( - impl ValNum for $impl_type { - fn val_num_f32(self) -> f32 { - self as f32 - } - } - )* - }; -} - -impl_to_val_num!(f32, f64, i8, i16, i32, i64, u8, u16, u32, u64, usize, isize); - -/// Returns a [`Val::Auto`] where the value is automatically determined -/// based on the context and other [`Node`](crate::Node) properties. -pub const fn auto() -> Val { - Val::Auto -} - -/// Returns a [`Val::Px`] representing a value in logical pixels. -pub fn px(value: T) -> Val { - Val::Px(value.val_num_f32()) -} - -/// Returns a [`Val::Em`] representing a value proportional to the node's own font size, represented by an [`EmSize`] component. -pub fn em(value: T) -> Val { - Val::Em(value.val_num_f32()) -} - -/// Returns a [`Val::Rem`] representing a value proportional to the root font size. -pub fn rem(value: T) -> Val { - Val::Rem(value.val_num_f32()) -} - -/// Returns a [`Val::Percent`] representing a percentage of the parent node's length -/// along a specific axis. -/// -/// If the UI node has no parent, the percentage is based on the window's length -/// along that axis. -/// -/// Axis rules: -/// * For `flex_basis`, the percentage is relative to the main-axis length determined by the `flex_direction`. -/// * For `gap`, `min_size`, `size`, and `max_size`: -/// - `width` is relative to the parent's width. -/// - `height` is relative to the parent's height. -/// * For `margin`, `padding`, and `border` values: the percentage is relative to the parent'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. -pub fn percent(value: T) -> Val { - Val::Percent(value.val_num_f32()) -} - -/// Returns a [`Val::Vw`] representing a percentage of the viewport width. -pub fn vw(value: T) -> Val { - Val::Vw(value.val_num_f32()) -} - -/// Returns a [`Val::Vh`] representing a percentage of the viewport height. -pub fn vh(value: T) -> Val { - Val::Vh(value.val_num_f32()) -} - -/// Returns a [`Val::VMin`] representing a percentage of the viewport's smaller dimension. -pub fn vmin(value: T) -> Val { - Val::VMin(value.val_num_f32()) -} - -/// Returns a [`Val::VMax`] representing a percentage of the viewport's larger dimension. -pub fn vmax(value: T) -> Val { - Val::VMax(value.val_num_f32()) -} - /// A type which is commonly used to define margins, paddings and borders. /// /// # Examples @@ -1670,79 +841,9 @@ impl PartialEq for CornerRadius { #[cfg(test)] mod tests { - use crate::geometry::*; + use super::*; use bevy_math::vec2; - #[test] - fn val_evaluate() { - let size = 250.; - let viewport_size = vec2(1000., 500.); - let em_size = EmSize(10.); - let rem_size = RemSize(20.); - - assert_eq!( - Val::Percent(80.) - .resolve(1., size, viewport_size, em_size, rem_size) - .unwrap(), - size * 0.8 - ); - // Check em and rem - assert_eq!( - Val::Em(2.) - .resolve(1., size, viewport_size, em_size, rem_size) - .unwrap(), - em_size.0 * 2. - ); - assert_eq!( - Val::Rem(5.) - .resolve(1., size, viewport_size, em_size, rem_size) - .unwrap(), - rem_size.0 * 5. - ); - // Check scale_factor is considered - assert_eq!( - Val::Em(2.) - .resolve(2., size, viewport_size, em_size, rem_size) - .unwrap(), - em_size.0 * 2. * 2. - ); - assert_eq!( - Val::Rem(5.) - .resolve(2., size, viewport_size, em_size, rem_size) - .unwrap(), - rem_size.0 * 5. * 2. - ); - } - - #[test] - fn val_resolve_px() { - let size = 250.; - let viewport_size = vec2(1000., 500.); - - let result = Val::Px(10.) - .resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) - .unwrap(); - assert_eq!(result, 10.); - - let result = Val::Px(10.) - .resolve(3., size, viewport_size, EmSize(20.0), RemSize(20.0)) - .unwrap(); - assert_eq!(result, 30.); - let result = Val::Px(10.) - .resolve(0.25, size, viewport_size, EmSize(20.0), RemSize(20.0)) - .unwrap(); - assert_eq!(result, 2.5); - - let result = Val::Em(1.) - .resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) - .unwrap(); - assert_eq!(result, 20.0); - let result = Val::Rem(1.) - .resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) - .unwrap(); - assert_eq!(result, 20.0); - } - #[test] fn corner_radius_resolve() { let size = vec2(100., 50.); @@ -1778,195 +879,6 @@ mod tests { ); } - #[test] - fn val_resolve_viewport_coords() { - let size = 250.; - let viewport_size = vec2(500., 500.); - - for value in (-10..10).map(|value| value as f32) { - // for a square viewport there should be no difference between `Vw` and `Vh` and between `Vmin` and `Vmax`. - assert_eq!( - Val::Vw(value).resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)), - Val::Vh(value).resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) - ); - assert_eq!( - Val::VMin(value).resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)), - Val::VMax(value).resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) - ); - assert_eq!( - Val::VMin(value).resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)), - Val::Vw(value).resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) - ); - } - - let viewport_size = vec2(1000., 500.); - assert_eq!( - Val::Vw(100.) - .resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) - .unwrap(), - 1000. - ); - assert_eq!( - Val::Vh(100.) - .resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) - .unwrap(), - 500. - ); - assert_eq!( - Val::Vw(60.) - .resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) - .unwrap(), - 600. - ); - assert_eq!( - Val::Vh(40.) - .resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) - .unwrap(), - 200. - ); - assert_eq!( - Val::VMin(50.) - .resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) - .unwrap(), - 250. - ); - assert_eq!( - Val::VMax(75.) - .resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) - .unwrap(), - 750. - ); - } - - #[test] - fn val_auto_is_non_evaluable() { - let size = 250.; - let viewport_size = vec2(1000., 500.); - let resolve_auto = Val::Auto.resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)); - - assert_eq!(resolve_auto, Err(ValArithmeticError::NonEvaluable)); - } - - #[test] - fn val_try_add_compatible_variants() { - assert_eq!(Val::Px(1.).try_add(Val::Px(2.)), Ok(Val::Px(3.))); - assert_eq!( - Val::Percent(1.).try_add(Val::Percent(2.)), - Ok(Val::Percent(3.)) - ); - assert_eq!(Val::Vw(1.).try_add(Val::Vw(2.)), Ok(Val::Vw(3.))); - assert_eq!(Val::Vh(1.).try_add(Val::Vh(2.)), Ok(Val::Vh(3.))); - assert_eq!(Val::VMin(1.).try_add(Val::VMin(2.)), Ok(Val::VMin(3.))); - assert_eq!(Val::VMax(1.).try_add(Val::VMax(2.)), Ok(Val::VMax(3.))); - } - - #[test] - fn val_try_add_incompatible_variants() { - assert_eq!( - Val::Auto.try_add(Val::Auto), - Err(ValArithmeticError::IncompatibleUnits) - ); - assert_eq!( - Val::Px(1.).try_add(Val::Percent(2.)), - Err(ValArithmeticError::IncompatibleUnits) - ); - assert_eq!( - Val::Auto.try_add(Val::Px(1.)), - Err(ValArithmeticError::IncompatibleUnits) - ); - } - - #[test] - fn val_try_sub_compatible_variants() { - assert_eq!(Val::Px(3.).try_sub(Val::Px(2.)), Ok(Val::Px(1.))); - assert_eq!( - Val::Percent(3.).try_sub(Val::Percent(2.)), - Ok(Val::Percent(1.)) - ); - assert_eq!(Val::Vw(3.).try_sub(Val::Vw(2.)), Ok(Val::Vw(1.))); - assert_eq!(Val::Vh(3.).try_sub(Val::Vh(2.)), Ok(Val::Vh(1.))); - assert_eq!(Val::VMin(3.).try_sub(Val::VMin(2.)), Ok(Val::VMin(1.))); - assert_eq!(Val::VMax(3.).try_sub(Val::VMax(2.)), Ok(Val::VMax(1.))); - } - - #[test] - fn val_try_sub_incompatible_variants() { - assert_eq!( - Val::Auto.try_sub(Val::Auto), - Err(ValArithmeticError::IncompatibleUnits) - ); - assert_eq!( - Val::Px(1.).try_sub(Val::Percent(2.)), - Err(ValArithmeticError::IncompatibleUnits) - ); - assert_eq!( - Val::Auto.try_sub(Val::Px(1.)), - Err(ValArithmeticError::IncompatibleUnits) - ); - } - - #[test] - fn val_str_parse() { - assert_eq!("auto".parse::(), Ok(Val::Auto)); - assert_eq!("Auto".parse::(), Ok(Val::Auto)); - assert_eq!("AUTO".parse::(), Ok(Val::Auto)); - - assert_eq!("3px".parse::(), Ok(Val::Px(3.))); - assert_eq!("3 px".parse::(), Ok(Val::Px(3.))); - assert_eq!("3.5px".parse::(), Ok(Val::Px(3.5))); - assert_eq!("-3px".parse::(), Ok(Val::Px(-3.))); - assert_eq!("3.5 PX".parse::(), Ok(Val::Px(3.5))); - - assert_eq!("3%".parse::(), Ok(Val::Percent(3.))); - assert_eq!("3 %".parse::(), Ok(Val::Percent(3.))); - assert_eq!("3.5%".parse::(), Ok(Val::Percent(3.5))); - assert_eq!("-3%".parse::(), Ok(Val::Percent(-3.))); - - assert_eq!("3vw".parse::(), Ok(Val::Vw(3.))); - assert_eq!("3 vw".parse::(), Ok(Val::Vw(3.))); - assert_eq!("3.5vw".parse::(), Ok(Val::Vw(3.5))); - assert_eq!("-3vw".parse::(), Ok(Val::Vw(-3.))); - assert_eq!("3.5 VW".parse::(), Ok(Val::Vw(3.5))); - - assert_eq!("3vh".parse::(), Ok(Val::Vh(3.))); - assert_eq!("3 vh".parse::(), Ok(Val::Vh(3.))); - assert_eq!("3.5vh".parse::(), Ok(Val::Vh(3.5))); - assert_eq!("-3vh".parse::(), Ok(Val::Vh(-3.))); - assert_eq!("3.5 VH".parse::(), Ok(Val::Vh(3.5))); - - assert_eq!("3vmin".parse::(), Ok(Val::VMin(3.))); - assert_eq!("3 vmin".parse::(), Ok(Val::VMin(3.))); - assert_eq!("3.5vmin".parse::(), Ok(Val::VMin(3.5))); - assert_eq!("-3vmin".parse::(), Ok(Val::VMin(-3.))); - assert_eq!("3.5 VMIN".parse::(), Ok(Val::VMin(3.5))); - - assert_eq!("3vmax".parse::(), Ok(Val::VMax(3.))); - assert_eq!("3 vmax".parse::(), Ok(Val::VMax(3.))); - assert_eq!("3.5vmax".parse::(), Ok(Val::VMax(3.5))); - assert_eq!("-3vmax".parse::(), Ok(Val::VMax(-3.))); - assert_eq!("3.5 VMAX".parse::(), Ok(Val::VMax(3.5))); - - assert_eq!("3em".parse::(), Ok(Val::Em(3.))); - assert_eq!("3rem".parse::(), Ok(Val::Rem(3.))); - - assert_eq!("".parse::(), Err(ValParseError::UnitMissing)); - assert_eq!( - "hello world".parse::(), - Err(ValParseError::ValueMissing) - ); - assert_eq!("3".parse::(), Err(ValParseError::UnitMissing)); - assert_eq!("3.5".parse::(), Err(ValParseError::UnitMissing)); - assert_eq!("3pxx".parse::(), Err(ValParseError::InvalidUnit)); - assert_eq!("3.5pxx".parse::(), Err(ValParseError::InvalidUnit)); - assert_eq!("3-3px".parse::(), Err(ValParseError::InvalidValue)); - assert_eq!("3.5-3px".parse::(), Err(ValParseError::InvalidValue)); - } - - #[test] - fn default_val_equals_const_default_val() { - assert_eq!(Val::default(), Val::DEFAULT); - } - #[test] fn uirect_default_equals_const_default() { assert_eq!(UiRect::default(), UiRect::all(Val::ZERO)); @@ -2005,17 +917,6 @@ mod tests { assert_eq!(r.bottom, Val::Percent(99.)); } - #[test] - fn val_constructor_fns_return_correct_val_variant() { - assert_eq!(auto(), Val::Auto); - assert_eq!(px(0.0), Val::Px(0.0)); - assert_eq!(percent(0.0), Val::Percent(0.0)); - assert_eq!(vw(0.0), Val::Vw(0.0)); - assert_eq!(vh(0.0), Val::Vh(0.0)); - assert_eq!(vmin(0.0), Val::VMin(0.0)); - assert_eq!(vmax(0.0), Val::VMax(0.0)); - } - #[test] fn corner_radius_partial_eq_with_auto() { assert_eq!( diff --git a/crates/bevy_ui/src/layout/convert.rs b/crates/bevy_ui/src/layout/convert.rs index 6df3bfb422aa8..84c882f839ac7 100644 --- a/crates/bevy_ui/src/layout/convert.rs +++ b/crates/bevy_ui/src/layout/convert.rs @@ -3,7 +3,7 @@ use bevy_text::{EmSize, RemSize}; use taffy::style_helpers; use crate::{ - calc::{CalcOp, ValContext, ValUnit}, + val::{CalcOp, ValContext, ValUnit}, AlignContent, AlignItems, AlignSelf, BoxSizing, Display, FlexDirection, FlexWrap, GridAutoFlow, GridPlacement, GridTrack, GridTrackRepetition, InlineDirection, JustifyContent, JustifyItems, JustifySelf, MaxTrackSizingFunction, MinTrackSizingFunction, Node, OverflowAxis, PositionType, diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index 8844c195027f3..ac2001f53338e 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -1,6 +1,7 @@ use crate::{ layout_tree::{compute_layout, TaffyStyle}, ui_transform::{UiGlobalTransform, UiTransform}, + val::ValContext, ComputedNode, ComputedUiRenderTargetInfo, ContentSize, Display, FixedNode, GhostNode, IgnoreScroll, LayoutConfig, Node, Outline, OverflowAxis, OverrideClip, ScrollPosition, }; @@ -15,7 +16,6 @@ use bevy_ecs::{ system::{Local, ParamSet, Query, Res, ResMut}, world::Ref, }; - use bevy_log::warn_once; use bevy_math::{Affine2, Vec2}; use bevy_sprite::BorderRect; @@ -89,8 +89,6 @@ impl UiRoots { } } -pub use convert::ValContext; - #[derive(Debug, Error)] pub enum LayoutError { #[error("UI root entity is missing or doesn't have the components needed for layout.")] diff --git a/crates/bevy_ui/src/layout/tests.rs b/crates/bevy_ui/src/layout/tests.rs index ed7968cf7977b..37d8a3ae09c86 100644 --- a/crates/bevy_ui/src/layout/tests.rs +++ b/crates/bevy_ui/src/layout/tests.rs @@ -173,8 +173,7 @@ fn calc_layout_handles_containers_and_parent_width_based_padding() { let child = app .world_mut() .spawn(Node { - width: crate::calc::CalcOp::Clamp(Val::Px(100.0), Val::Percent(80.0), Val::Px(300.0)) - .into(), + width: CalcOp::Clamp(Val::Px(100.0), Val::Percent(80.0), Val::Px(300.0)).into(), height: Val::Percent(50.0) - Val::Px(10.0), padding: UiRect::all(Val::Percent(5.0) + Val::Px(2.0)), ..default() diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index d0ac5aaa753bb..56d3bf453c944 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -18,11 +18,11 @@ pub mod measurement; pub mod update; pub mod widget; -pub mod calc; pub mod gradients; #[cfg(feature = "bevy_picking")] pub mod picking_backend; pub mod ui_transform; +pub mod val; use bevy_derive::{Deref, DerefMut}; #[cfg(feature = "bevy_picking")] @@ -49,6 +49,7 @@ pub use measurement::*; pub use scroll_into_view::{ScrollIntoView, ScrollIntoViewContainer}; pub use ui_node::*; pub use ui_transform::*; +pub use val::*; pub use widget::TextNodeFlags; /// The UI prelude. @@ -74,6 +75,7 @@ pub mod prelude { gradients::*, ui_node::*, ui_transform::*, + val::*, widget::{ImageNode, InlineImage, Label, NodeImageMode, ViewportNode}, UiScale, }, diff --git a/crates/bevy_ui/src/val.rs b/crates/bevy_ui/src/val.rs new file mode 100644 index 0000000000000..9cffc38f7cbbc --- /dev/null +++ b/crates/bevy_ui/src/val.rs @@ -0,0 +1,1949 @@ +use bevy_math::{MismatchedUnitsError, StableInterpolate as _, TryStableInterpolate, Vec2}; +use bevy_platform::sync::Arc; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_text::{EmSize, FontSize, RemSize, DEFAULT_REM_SIZE_PX}; +use core::ops::{Add, Div, DivAssign, Mul, MulAssign, Neg, Sub}; +use thiserror::Error; + +#[cfg(feature = "serialize")] +use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; + +use crate::UiRect; + +/// All the context needed to resolve a [`Val`] into a length in physical pixels, except the percentage basis. +#[derive(Copy, Clone)] +pub struct ValContext { + /// Logical pixel lengths are multiplied by the scale factor to get a physical length. + pub scale_factor: f32, + /// Size of the target surface in physical pixels. + pub physical_size: Vec2, + /// The local font size in logical pixels. + /// `Val::Em` values are normalized, so `em(1)` is `em_size * scale_factor` in physical pixels. + pub em_size: f32, + /// The root font size in logical pixels. + /// `Val::Em` values are normalized, so `rem(1)` is `rem_size * scale_factor` in physical pixels. + pub rem_size: f32, +} + +impl ValContext { + pub const DEFAULT: Self = Self { + scale_factor: 1.0, + physical_size: Vec2::ZERO, + em_size: DEFAULT_REM_SIZE_PX, + rem_size: DEFAULT_REM_SIZE_PX, + }; + + /// Create a new [`LayoutContext`] from the window's physical size and scale factor + #[inline] + pub(super) const fn new( + scale_factor: f32, + physical_size: Vec2, + em_size: EmSize, + rem_size: RemSize, + ) -> Self { + Self { + scale_factor, + physical_size, + em_size: em_size.0, + rem_size: rem_size.0, + } + } +} + +#[cfg(test)] +impl ValContext { + pub const TEST_CONTEXT: Self = Self { + physical_size: Vec2::new(1000.0, 1000.0), + ..Self::DEFAULT + }; +} + +impl Default for ValContext { + fn default() -> Self { + Self::DEFAULT + } +} + +/// Represents the possible value types for layout properties. +/// +/// This enum allows specifying values for various [`Node`](crate::Node) properties in different units, +/// such as logical pixels, percentages, or automatically determined values. +/// +/// `Val` also implements [`core::str::FromStr`] to allow parsing values from strings in the format `#.#px`. Whitespaces between the value and unit is allowed. The following units are supported: +/// * `px`: logical pixels +/// * `%`: percentage +/// * `vw`: percentage of the viewport width +/// * `vh`: percentage of the viewport height +/// * `vmin`: percentage of the viewport's smaller dimension +/// * `vmax`: percentage of the viewport's larger dimension +/// +/// Additionally, `auto` will be parsed as [`Val::Auto`]. +#[derive(Clone, Debug, Reflect)] +#[reflect(Default, PartialEq, Debug, Clone)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub enum Val { + /// Automatically determine the value based on the context and other [`Node`](crate::Node) properties. + Auto, + /// Set this value in logical pixels. + Px(f32), + /// Set the value as a percentage of its parent node's length along a specific axis. + /// + /// If the UI node has no parent, the percentage is calculated based on the window's length + /// along the corresponding axis. + /// + /// The chosen axis depends on the [`Node`](crate::Node) field set: + /// * For `flex_basis`, the percentage is relative to the main-axis length determined by the `flex_direction`. + /// * For `gap`, `min_size`, `size`, and `max_size`: + /// - `width` is relative to the parent's width. + /// - `height` is relative to the parent's height. + /// * 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), + /// Set this value as a multiple of the node's own font size. + /// + /// `1em` is the node's font size, so `Val::Em(2.0)` is twice that. + /// Resolved from the node's [`EmSize`] component. + Em(f32), + /// Set this value as a multiple of the root font size. + /// + /// Unlike [`Val::Em`] this ignores the node's own font size, so it means the same + /// length anywhere in the hierarchy. Resolved from the [`RemSize`] resource. + Rem(f32), + /// The sum of up to three terms with distinct units, such as `Val::Percent(50.0) - Val::Px(10.0)`. + /// + /// The `n`th value belongs to the `n`th unit of the set, see [`ValUnitSet`]. + /// Built by `+` and `-`; longer expressions fall back to [`Val::Calc`]. + Sum(ValUnitSet, [f32; 3]), + /// The smallest of up to three terms with distinct units, laid out like [`Val::Sum`]. + /// Built by [`Val::min`]. + Min(ValUnitSet, [f32; 3]), + /// The largest of up to three terms with distinct units, laid out like [`Val::Sum`]. + /// Built by [`Val::max`]. + Max(ValUnitSet, [f32; 3]), + /// A value clamped between a minimum and a maximum, each a unit value, stored as the + /// `[min, value, max]` units and values. Built by [`Val::clamp`]; when an operand is itself + /// an expression it falls back to [`Val::Calc`]. + Clamp([ValUnit; 3], [f32; 3]), + Calc(Calc), +} + +#[derive(Debug, Error, PartialEq, Eq)] +pub enum ValParseError { + UnitMissing, + ValueMissing, + InvalidValue, + InvalidUnit, +} + +impl core::fmt::Display for ValParseError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + ValParseError::UnitMissing => write!(f, "unit missing"), + ValParseError::ValueMissing => write!(f, "value missing"), + ValParseError::InvalidValue => write!(f, "invalid value"), + ValParseError::InvalidUnit => write!(f, "invalid unit"), + } + } +} + +impl core::str::FromStr for Val { + type Err = ValParseError; + + fn from_str(s: &str) -> Result { + let s = s.trim(); + + if s.eq_ignore_ascii_case("auto") { + return Ok(Val::Auto); + } + + let Some(end_of_number) = s + .bytes() + .position(|c| !(c.is_ascii_digit() || c == b'.' || c == b'-' || c == b'+')) + else { + return Err(ValParseError::UnitMissing); + }; + + if end_of_number == 0 { + return Err(ValParseError::ValueMissing); + } + + let (value, unit) = s.split_at(end_of_number); + + let value: f32 = value.parse().map_err(|_| ValParseError::InvalidValue)?; + + let unit = unit.trim(); + + if unit.eq_ignore_ascii_case("px") { + Ok(Val::Px(value)) + } else if unit.eq_ignore_ascii_case("%") { + Ok(Val::Percent(value)) + } else if unit.eq_ignore_ascii_case("vw") { + Ok(Val::Vw(value)) + } else if unit.eq_ignore_ascii_case("vh") { + Ok(Val::Vh(value)) + } else if unit.eq_ignore_ascii_case("vmin") { + Ok(Val::VMin(value)) + } else if unit.eq_ignore_ascii_case("vmax") { + Ok(Val::VMax(value)) + } else if unit.eq_ignore_ascii_case("rem") { + Ok(Val::Rem(value)) + } else if unit.eq_ignore_ascii_case("em") { + Ok(Val::Em(value)) + } else { + Err(ValParseError::InvalidUnit) + } + } +} + +impl PartialEq for Val { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Calc(a), Self::Calc(b)) => return a == b, + (Self::Min(a, x), Self::Min(b, y)) | (Self::Max(a, x), Self::Max(b, y)) => { + return ValTerms::new(*a, *x) == ValTerms::new(*b, *y); + } + (Self::Clamp(a, x), Self::Clamp(b, y)) => return a == b && x == y, + (Self::Sum(..), _) | (_, Self::Sum(..)) => { + return self.sum_terms() == other.sum_terms(); + } + _ => {} + } + let same_unit = matches!( + (self, other), + (Self::Auto, Self::Auto) + | (Self::Px(_), Self::Px(_)) + | (Self::Percent(_), Self::Percent(_)) + | (Self::Vw(_), Self::Vw(_)) + | (Self::Vh(_), Self::Vh(_)) + | (Self::VMin(_), Self::VMin(_)) + | (Self::VMax(_), Self::VMax(_)) + | (Self::Em(_), Self::Em(_)) + | (Self::Rem(_), Self::Rem(_)) + ); + + // as long as the units are the same comparing `inner` + // is valid. + let left = self.inner(); + let right = other.inner(); + + match (same_unit, left, right) { + (true, a, b) => a == b, + // All zero-value variants are considered equal. + (false, Some(a), Some(b)) => a == 0. && b == 0., + _ => false, + } + } +} + +impl Val { + pub const DEFAULT: Self = Self::Auto; + + /// Zero. In [`Val::Px`], but zero is the same in any unit. + pub const ZERO: Self = Self::Px(0.0); + + /// The largest finite [`Val::Px`] value. + pub const MAX: Self = Self::Px(f32::MAX); + + /// Returns the number this `Val` wraps, whatever its unit, or `None` for [`Val::Auto`] and the expression variants. + /// + /// The number is meaningless without the variant it came from — `Val::Px(4.0)` and + /// `Val::Rem(4.0)` both return `4.0`. Use [`Val::resolve`] for a length in pixels. + pub(crate) const fn inner(&self) -> Option { + match self { + Self::Auto + | Self::Sum(..) + | Self::Min(..) + | Self::Max(..) + | Self::Clamp(..) + | Self::Calc(_) => None, + Self::Px(v) + | Self::Percent(v) + | Self::Vw(v) + | Self::Vh(v) + | Self::VMin(v) + | Self::VMax(v) + | Self::Em(v) + | Self::Rem(v) => Some(*v), + } + } + + /// The unit of a single-term value, or `None` for [`Val::Auto`] and the expression variants. + pub const fn unit(&self) -> Option { + Some(match self { + Self::Px(_) => ValUnit::Px, + Self::Percent(_) => ValUnit::Percent, + Self::Vw(_) => ValUnit::Vw, + Self::Vh(_) => ValUnit::Vh, + Self::VMin(_) => ValUnit::VMin, + Self::VMax(_) => ValUnit::VMax, + Self::Em(_) => ValUnit::Em, + Self::Rem(_) => ValUnit::Rem, + Self::Auto + | Self::Sum(..) + | Self::Min(..) + | Self::Max(..) + | Self::Clamp(..) + | Self::Calc(_) => { + return None; + } + }) + } + + /// A single-term value of the given `unit`. + pub const fn leaf(unit: ValUnit, value: f32) -> Self { + match unit { + ValUnit::Px => Self::Px(value), + ValUnit::Percent => Self::Percent(value), + ValUnit::Vw => Self::Vw(value), + ValUnit::Vh => Self::Vh(value), + ValUnit::VMin => Self::VMin(value), + ValUnit::VMax => Self::VMax(value), + ValUnit::Em => Self::Em(value), + ValUnit::Rem => Self::Rem(value), + } + } + + pub(crate) fn uses_percent(&self) -> bool { + match self { + Self::Percent(_) => true, + Self::Sum(units, _) | Self::Min(units, _) | Self::Max(units, _) => { + units.contains(ValUnit::Percent) + } + Self::Clamp(units, _) => units.contains(&ValUnit::Percent), + Self::Calc(calc) => calc.0.uses_percent(), + _ => false, + } + } + + /// Returns a [`UiRect`] with its `left` equal to this value, + /// and all other fields set to `Val::ZERO`. + /// + /// + /// # Example + /// + /// ``` + /// # use bevy_ui::{UiRect, Val}; + /// # + /// let ui_rect = Val::Px(1.).left(); + /// + /// assert_eq!(ui_rect.left, Val::Px(1.)); + /// assert_eq!(ui_rect.right, Val::ZERO); + /// assert_eq!(ui_rect.top, Val::ZERO); + /// assert_eq!(ui_rect.bottom, Val::ZERO); + /// ``` + pub fn left(self) -> UiRect { + UiRect::left(self) + } + + /// Returns a [`UiRect`] with its `right` equal to this value, + /// and all other fields set to `Val::ZERO`. + /// + /// + /// # Example + /// + /// ``` + /// # use bevy_ui::{UiRect, Val}; + /// # + /// let ui_rect = Val::Px(1.).right(); + /// + /// assert_eq!(ui_rect.left, Val::ZERO); + /// assert_eq!(ui_rect.right, Val::Px(1.)); + /// assert_eq!(ui_rect.top, Val::ZERO); + /// assert_eq!(ui_rect.bottom, Val::ZERO); + /// ``` + pub fn right(self) -> UiRect { + UiRect::right(self) + } + + /// Returns a [`UiRect`] with its `top` equal to this value, + /// and all other fields set to `Val::ZERO`. + /// + /// + /// # Example + /// + /// ``` + /// # use bevy_ui::{UiRect, Val}; + /// # + /// let ui_rect = Val::Px(1.).top(); + /// + /// assert_eq!(ui_rect.left, Val::ZERO); + /// assert_eq!(ui_rect.right, Val::ZERO); + /// assert_eq!(ui_rect.top, Val::Px(1.)); + /// assert_eq!(ui_rect.bottom, Val::ZERO); + /// ``` + pub fn top(self) -> UiRect { + UiRect::top(self) + } + + /// Returns a [`UiRect`] with its `bottom` equal to this value, + /// and all other fields set to `Val::ZERO`. + /// + /// + /// # Example + /// + /// ``` + /// # use bevy_ui::{UiRect, Val}; + /// # + /// let ui_rect = Val::Px(1.).bottom(); + /// + /// assert_eq!(ui_rect.left, Val::ZERO); + /// assert_eq!(ui_rect.right, Val::ZERO); + /// assert_eq!(ui_rect.top, Val::ZERO); + /// assert_eq!(ui_rect.bottom, Val::Px(1.)); + /// ``` + pub fn bottom(self) -> UiRect { + UiRect::bottom(self) + } + + /// Returns a [`UiRect`] with all its fields equal to this value. + /// + /// # Example + /// + /// ``` + /// # use bevy_ui::{UiRect, Val}; + /// # + /// let ui_rect = Val::Px(1.).all(); + /// + /// assert_eq!(ui_rect.left, Val::Px(1.)); + /// assert_eq!(ui_rect.right, Val::Px(1.)); + /// assert_eq!(ui_rect.top, Val::Px(1.)); + /// assert_eq!(ui_rect.bottom, Val::Px(1.)); + /// ``` + pub fn all(self) -> UiRect { + UiRect::all(self) + } + + /// Returns a [`UiRect`] with all its `left` and `right` equal to this value, + /// and its `top` and `bottom` set to `Val::ZERO`. + /// + /// # Example + /// + /// ``` + /// # use bevy_ui::{UiRect, Val}; + /// # + /// let ui_rect = Val::Px(1.).horizontal(); + /// + /// assert_eq!(ui_rect.left, Val::Px(1.)); + /// assert_eq!(ui_rect.right, Val::Px(1.)); + /// assert_eq!(ui_rect.top, Val::ZERO); + /// assert_eq!(ui_rect.bottom, Val::ZERO); + /// ``` + pub fn horizontal(self) -> UiRect { + UiRect::horizontal(self) + } + + /// Returns a [`UiRect`] with all its `top` and `bottom` equal to this value, + /// and its `left` and `right` set to `Val::ZERO`. + /// + /// # Example + /// + /// ``` + /// # use bevy_ui::{UiRect, Val}; + /// # + /// let ui_rect = Val::Px(1.).vertical(); + /// + /// assert_eq!(ui_rect.left, Val::ZERO); + /// assert_eq!(ui_rect.right, Val::ZERO); + /// assert_eq!(ui_rect.top, Val::Px(1.)); + /// assert_eq!(ui_rect.bottom, Val::Px(1.)); + /// ``` + pub fn vertical(self) -> UiRect { + UiRect::vertical(self) + } + + /// Try to add two `Val`s. + /// + /// Returns [`ValArithmeticError::IncompatibleUnits`] if the units differ or both are `Val::Auto`. + /// + /// ``` + /// # use bevy_ui::{Val, ValArithmeticError}; + /// assert_eq!(Val::Px(1.).try_add(Val::Px(2.)), Ok(Val::Px(3.))); + /// assert_eq!( + /// Val::Px(1.).try_add(Val::Percent(2.)), + /// Err(ValArithmeticError::IncompatibleUnits) + /// ); + /// ``` + pub fn try_add(self, val: Val) -> Result { + match (self, val) { + (Val::Px(u), Val::Px(v)) => Ok(Val::Px(u + v)), + (Val::Percent(u), Val::Percent(v)) => Ok(Val::Percent(u + v)), + (Val::Vw(u), Val::Vw(v)) => Ok(Val::Vw(u + v)), + (Val::Vh(u), Val::Vh(v)) => Ok(Val::Vh(u + v)), + (Val::VMin(u), Val::VMin(v)) => Ok(Val::VMin(u + v)), + (Val::VMax(u), Val::VMax(v)) => Ok(Val::VMax(u + v)), + (Val::Em(u), Val::Em(v)) => Ok(Val::Em(u + v)), + (Val::Rem(u), Val::Rem(v)) => Ok(Val::Rem(u + v)), + _ => Err(ValArithmeticError::IncompatibleUnits), + } + } + + /// Try to subtract one `Val` from another. + /// + /// Returns [`ValArithmeticError::IncompatibleUnits`] if the units differ or both are `Val::Auto`. + /// + /// ``` + /// # use bevy_ui::{Val, ValArithmeticError}; + /// assert_eq!(Val::Px(3.).try_sub(Val::Px(2.)), Ok(Val::Px(1.))); + /// assert_eq!( + /// Val::Px(1.).try_sub(Val::Percent(2.)), + /// Err(ValArithmeticError::IncompatibleUnits) + /// ); + /// ``` + pub fn try_sub(self, val: Val) -> Result { + match (self, val) { + (Val::Px(u), Val::Px(v)) => Ok(Val::Px(u - v)), + (Val::Percent(u), Val::Percent(v)) => Ok(Val::Percent(u - v)), + (Val::Vw(u), Val::Vw(v)) => Ok(Val::Vw(u - v)), + (Val::Vh(u), Val::Vh(v)) => Ok(Val::Vh(u - v)), + (Val::VMin(u), Val::VMin(v)) => Ok(Val::VMin(u - v)), + (Val::VMax(u), Val::VMax(v)) => Ok(Val::VMax(u - v)), + (Val::Em(u), Val::Em(v)) => Ok(Val::Em(u - v)), + (Val::Rem(u), Val::Rem(v)) => Ok(Val::Rem(u - v)), + _ => Err(ValArithmeticError::IncompatibleUnits), + } + } + + /// The smaller of `self` and `other` once resolved. + pub fn min(self, other: Val) -> Val { + self.combine(other, TermOp::Min) + } + + /// The larger of `self` and `other` once resolved. + pub fn max(self, other: Val) -> Val { + self.combine(other, TermOp::Max) + } + + /// `self` clamped between `min` and `max` once resolved. + pub fn clamp(self, min: Val, max: Val) -> Val { + Val::clamp_inline(&min, &self, &max).unwrap_or_else(|| CalcOp::Clamp(min, self, max).into()) + } +} + +impl Default for Val { + fn default() -> Self { + Self::DEFAULT + } +} + +impl Add for Val { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + self.combine(rhs, TermOp::Sum) + } +} + +impl Sub for Val { + type Output = Self; + + fn sub(self, rhs: Self) -> Self { + self.combine(-rhs, TermOp::Sum) + } +} + +impl Mul for Val { + type Output = Val; + + fn mul(self, rhs: f32) -> Self::Output { + match self { + 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), + Val::Em(value) => Val::Em(value * rhs), + Val::Rem(value) => Val::Rem(value * rhs), + terms @ (Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Clamp(..)) => { + terms.map_terms(|value| value * rhs, rhs < 0.0) + } + Val::Calc(mut calc) => { + Arc::make_mut(&mut calc.0).scale(rhs); + Val::Calc(calc) + } + } + } +} + +impl MulAssign for Val { + fn mul_assign(&mut self, rhs: f32) { + match self { + Val::Auto => {} + Val::Px(value) + | Val::Percent(value) + | Val::Vw(value) + | Val::Vh(value) + | Val::VMin(value) + | Val::VMax(value) + | Val::Em(value) + | Val::Rem(value) => *value *= rhs, + Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Clamp(..) => { + *self = core::mem::take(self).map_terms(|value| value * rhs, rhs < 0.0); + } + Val::Calc(calc) => Arc::make_mut(&mut calc.0).scale(rhs), + } + } +} + +impl Div for Val { + type Output = Val; + + fn div(self, rhs: f32) -> Self::Output { + match self { + 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), + Val::Em(value) => Val::Em(value / rhs), + Val::Rem(value) => Val::Rem(value / rhs), + terms @ (Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Clamp(..)) => { + terms.map_terms(|value| value / rhs, rhs < 0.0) + } + Val::Calc(mut calc) => { + Arc::make_mut(&mut calc.0).scale(rhs.recip()); + Val::Calc(calc) + } + } + } +} + +impl DivAssign for Val { + fn div_assign(&mut self, rhs: f32) { + match self { + Val::Auto => {} + Val::Px(value) + | Val::Percent(value) + | Val::Vw(value) + | Val::Vh(value) + | Val::VMin(value) + | Val::VMax(value) + | Val::Em(value) + | Val::Rem(value) => *value /= rhs, + Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Clamp(..) => { + *self = core::mem::take(self).map_terms(|value| value / rhs, rhs < 0.0); + } + Val::Calc(calc) => Arc::make_mut(&mut calc.0).scale(rhs.recip()), + } + } +} + +impl Neg for Val { + type Output = Val; + + fn neg(self) -> Self::Output { + match self { + Val::Auto => Val::Auto, + Val::Px(value) => Val::Px(-value), + Val::Percent(value) => Val::Percent(-value), + Val::Vw(value) => Val::Vw(-value), + Val::Vh(value) => Val::Vh(-value), + Val::VMin(value) => Val::VMin(-value), + Val::VMax(value) => Val::VMax(-value), + Val::Em(value) => Val::Em(-value), + Val::Rem(value) => Val::Rem(-value), + terms @ (Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Clamp(..)) => { + terms.map_terms(|value| -value, true) + } + Val::Calc(mut calc) => { + Arc::make_mut(&mut calc.0).scale(-1.0); + Val::Calc(calc) + } + } + } +} + +#[derive(Debug, Eq, PartialEq, Clone, Copy, Error)] +pub enum ValArithmeticError { + #[error("the given variant of Val is not evaluable (non-numeric)")] + NonEvaluable, + #[error("the given variants of Val have incompatible units")] + IncompatibleUnits, +} + +impl Val { + /// Resolves this [`Val`] to a value in physical pixels from the given `scale_factor`, `physical_base_value`, + /// `physical_target_size`, `em_size`, and `rem_size` context values. + /// + /// Returns a [`ValArithmeticError::NonEvaluable`] if the [`Val`] is impossible to resolve into a concrete value. + pub fn resolve( + &self, + scale_factor: f32, + physical_base_value: f32, + physical_target_size: Vec2, + em_size: EmSize, + rem_size: RemSize, + ) -> Result { + match self { + Val::Percent(value) => Ok(physical_base_value * value / 100.0), + Val::Px(value) => Ok(value * scale_factor), + Val::Vw(value) => Ok(physical_target_size.x * value / 100.0), + Val::Vh(value) => Ok(physical_target_size.y * value / 100.0), + Val::VMin(value) => { + Ok(physical_target_size.x.min(physical_target_size.y) * value / 100.0) + } + Val::VMax(value) => { + Ok(physical_target_size.x.max(physical_target_size.y) * value / 100.0) + } + Val::Em(value) => Ok(value * em_size.0 * scale_factor), + Val::Rem(value) => Ok(value * rem_size.0 * scale_factor), + Val::Auto => Err(ValArithmeticError::NonEvaluable), + Val::Sum(units, values) | Val::Min(units, values) | Val::Max(units, values) => { + let op = match self { + Val::Min(..) => TermOp::Min, + Val::Max(..) => TermOp::Max, + _ => TermOp::Sum, + }; + Ok(ValTerms::new(*units, *values).resolve( + op.apply(), + scale_factor, + physical_base_value, + physical_target_size, + em_size, + rem_size, + )) + } + Val::Clamp(units, values) => Ok(Val::resolve_clamp( + *units, + *values, + scale_factor, + physical_base_value, + physical_target_size, + em_size, + rem_size, + )), + Val::Calc(op) => op.0.resolve( + scale_factor, + physical_base_value, + physical_target_size, + em_size, + rem_size, + ), + } + } +} + +impl TryFrom for FontSize { + type Error = ValArithmeticError; + + /// Converts a [`Val`] into a [`FontSize`]. + /// + /// [`Val::Em`] becomes [`FontSize::Rem`]: em is relative to the font size of the node + /// itself, which is the very thing being defined here, so it resolves against the root + /// size instead to avoid the circularity. + fn try_from(val: Val) -> Result { + Ok(match val { + Val::Auto => FontSize::Rem(1.), + Val::Px(px) => FontSize::Px(px), + Val::Percent(percent) => FontSize::Rem(percent / 100.), + Val::Vw(vw) => FontSize::Vw(vw), + Val::Vh(vh) => FontSize::Vh(vh), + Val::VMin(vmin) => FontSize::VMin(vmin), + Val::VMax(vmax) => FontSize::VMax(vmax), + Val::Rem(rem) => FontSize::Rem(rem), + Val::Em(em) => FontSize::Rem(em), // see comment above + Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Clamp(..) | Val::Calc(_) => { + return Err(ValArithmeticError::NonEvaluable); + } + }) + } +} + +impl TryStableInterpolate for Val { + type Error = MismatchedUnitsError; + + /// # Example + /// + /// ``` + /// # use bevy_ui::Val; + /// # use bevy_math::TryStableInterpolate; + /// assert!(matches!(Val::Px(0.0).try_interpolate_stable(&Val::Px(10.0), 0.5), Ok(Val::Px(5.0)))); + /// ``` + fn try_interpolate_stable(&self, other: &Self, t: f32) -> Result { + match (self, other) { + (Val::Px(a), Val::Px(b)) => Ok(Val::Px(a.interpolate_stable(b, t))), + (Val::Percent(a), Val::Percent(b)) => Ok(Val::Percent(a.interpolate_stable(b, t))), + (Val::Vw(a), Val::Vw(b)) => Ok(Val::Vw(a.interpolate_stable(b, t))), + (Val::Vh(a), Val::Vh(b)) => Ok(Val::Vh(a.interpolate_stable(b, t))), + (Val::VMin(a), Val::VMin(b)) => Ok(Val::VMin(a.interpolate_stable(b, t))), + (Val::VMax(a), Val::VMax(b)) => Ok(Val::VMax(a.interpolate_stable(b, t))), + (Val::Em(a), Val::Em(b)) => Ok(Val::Em(a.interpolate_stable(b, t))), + (Val::Rem(a), Val::Rem(b)) => Ok(Val::Rem(a.interpolate_stable(b, t))), + (Val::Sum(a, x), Val::Sum(b, y)) + | (Val::Min(a, x), Val::Min(b, y)) + | (Val::Max(a, x), Val::Max(b, y)) => { + let (units, values) = ValTerms::new(*a, *x) + .interpolate_stable(&ValTerms::new(*b, *y), t) + .ok_or(MismatchedUnitsError)? + .into_parts(); + Ok(match self { + Val::Min(..) => Val::Min(units, values), + Val::Max(..) => Val::Max(units, values), + _ => Val::Sum(units, values), + }) + } + (Val::Clamp(a, x), Val::Clamp(b, y)) if a == b => Ok(Val::Clamp( + *a, + core::array::from_fn(|i| x[i].interpolate_stable(&y[i], t)), + )), + (Val::Auto, Val::Auto) => Ok(Val::Auto), + _ => Err(MismatchedUnitsError), + } + } +} + +/// All the types that should be able to be used in the [`Val`] enum should implement this trait. +/// +/// Instead of just implementing `Into` a custom trait is added. +/// This is done in order to prevent having to define a default unit, which could lead to confusion especially for newcomers. +pub trait ValNum { + /// Called by the [`Val`] helper functions to convert the implementing type to an `f32` that can + /// be used by [`Val`]. + fn val_num_f32(self) -> f32; +} + +macro_rules! impl_to_val_num { + ($($impl_type:ty),*$(,)?) => { + $( + impl ValNum for $impl_type { + fn val_num_f32(self) -> f32 { + self as f32 + } + } + )* + }; +} + +impl_to_val_num!(f32, f64, i8, i16, i32, i64, u8, u16, u32, u64, usize, isize); + +/// Returns a [`Val::Auto`] where the value is automatically determined +/// based on the context and other [`Node`](crate::Node) properties. +pub const fn auto() -> Val { + Val::Auto +} + +/// Returns a [`Val::Px`] representing a value in logical pixels. +pub fn px(value: T) -> Val { + Val::Px(value.val_num_f32()) +} + +/// Returns a [`Val::Em`] representing a value proportional to the node's own font size, represented by an [`EmSize`] component. +pub fn em(value: T) -> Val { + Val::Em(value.val_num_f32()) +} + +/// Returns a [`Val::Rem`] representing a value proportional to the root font size. +pub fn rem(value: T) -> Val { + Val::Rem(value.val_num_f32()) +} + +/// Returns a [`Val::Percent`] representing a percentage of the parent node's length +/// along a specific axis. +/// +/// If the UI node has no parent, the percentage is based on the window's length +/// along that axis. +/// +/// Axis rules: +/// * For `flex_basis`, the percentage is relative to the main-axis length determined by the `flex_direction`. +/// * For `gap`, `min_size`, `size`, and `max_size`: +/// - `width` is relative to the parent's width. +/// - `height` is relative to the parent's height. +/// * For `margin`, `padding`, and `border` values: the percentage is relative to the parent'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. +pub fn percent(value: T) -> Val { + Val::Percent(value.val_num_f32()) +} + +/// Returns a [`Val::Vw`] representing a percentage of the viewport width. +pub fn vw(value: T) -> Val { + Val::Vw(value.val_num_f32()) +} + +/// Returns a [`Val::Vh`] representing a percentage of the viewport height. +pub fn vh(value: T) -> Val { + Val::Vh(value.val_num_f32()) +} + +/// Returns a [`Val::VMin`] representing a percentage of the viewport's smaller dimension. +pub fn vmin(value: T) -> Val { + Val::VMin(value.val_num_f32()) +} + +/// Returns a [`Val::VMax`] representing a percentage of the viewport's larger dimension. +pub fn vmax(value: T) -> Val { + Val::VMax(value.val_num_f32()) +} + +/// Wrapper around an opteration on `Val`s. +#[derive(Clone, Debug, PartialEq, Reflect)] +#[reflect(opaque, PartialEq, Debug, Clone)] +#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))] +pub struct Calc(pub(crate) Arc); + +// Serialized as the `CalcOp` itself: serde only implements `Arc` behind its `rc` feature, +// and sharing is an implementation detail that should not leak into the format. +#[cfg(feature = "serialize")] +impl serde::Serialize for Calc { + fn serialize(&self, serializer: S) -> Result { + self.0.serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for Calc { + fn deserialize>(deserializer: D) -> Result { + CalcOp::deserialize(deserializer).map(|op| Self(Arc::new(op))) + } +} + +/// An operation performed on `Val`s. +#[derive(Clone, Debug, PartialEq, Reflect)] +#[reflect(PartialEq, Debug, Clone)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub enum CalcOp { + /// Add two vals. + Add(Val, Val), + /// Subtract the second `Val` from the first. + Sub(Val, Val), + /// Return the minimum of the two `Val`s. + Min(Val, Val), + /// Return the maximum of the two `Val`s. + Max(Val, Val), + /// Clamp the second `Val` between the first and third. + Clamp(Val, Val, Val), +} + +impl CalcOp { + pub(crate) fn uses_percent(&self) -> bool { + match self { + Self::Add(a, b) | Self::Sub(a, b) | Self::Min(a, b) | Self::Max(a, b) => { + a.uses_percent() || b.uses_percent() + } + Self::Clamp(min, value, max) => { + min.uses_percent() || value.uses_percent() || max.uses_percent() + } + } + } + + pub fn resolve( + &self, + scale_factor: f32, + physical_base_value: f32, + physical_target_size: Vec2, + em_size: EmSize, + rem_size: RemSize, + ) -> Result { + let resolve = |val: &Val| { + val.resolve( + scale_factor, + physical_base_value, + physical_target_size, + em_size, + rem_size, + ) + }; + Ok(match self { + Self::Add(a, b) => resolve(a)? + resolve(b)?, + Self::Sub(a, b) => resolve(a)? - resolve(b)?, + Self::Min(a, b) => resolve(a)?.min(resolve(b)?), + Self::Max(a, b) => resolve(a)?.max(resolve(b)?), + Self::Clamp(min, value, max) => resolve(min)?.max(resolve(value)?.min(resolve(max)?)), + }) + } + + pub(crate) fn scale(&mut self, k: f32) { + use core::mem::take; + match self { + Self::Min(a, b) if k < 0.0 => *self = Self::Max(take(a) * k, take(b) * k), + Self::Max(a, b) if k < 0.0 => *self = Self::Min(take(a) * k, take(b) * k), + Self::Clamp(min, value, max) if k < 0.0 => { + *self = Self::Min( + take(min) * k, + Self::Max(take(value) * k, take(max) * k).into(), + ); + } + Self::Add(a, b) | Self::Sub(a, b) | Self::Min(a, b) | Self::Max(a, b) => { + *a *= k; + *b *= k; + } + Self::Clamp(min, value, max) => { + *min *= k; + *value *= k; + *max *= k; + } + } + } +} + +impl From for Val { + fn from(op: CalcOp) -> Self { + Self::Calc(Calc(Arc::new(op))) + } +} + +/// Discriminator enum, should match `Val`'s unit types. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Reflect)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[repr(u8)] +pub enum ValUnit { + /// Use `Px` units + Px, + /// Use `Percent` units + Percent, + /// Use `Vw` units + Vw, + Vh, + VMin, + VMax, + Em, + Rem, +} + +impl ValUnit { + /// Every unit, in discriminant order. + pub const ALL: [Self; 8] = [ + Self::Px, + Self::Percent, + Self::Vw, + Self::Vh, + Self::VMin, + Self::VMax, + Self::Em, + Self::Rem, + ]; + + const fn bit(self) -> u8 { + 1 << self as u8 + } +} + +/// If more unit types are added to `Val`, a `u8` discriminator won't be sufficient. +const _: () = assert!(ValUnit::ALL.len() <= u8::BITS as usize); + +/// A set of [`ValUnit`]s. +/// +/// [`Val::Sum`], [`Val::Min`] and [`Val::Max`] pair one of these with an array of values: +/// the `n`th value belongs to the `n`th unit in [`ValUnitSet::iter`] order, and the +/// slots after the last unit are unused. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Default, Reflect)] +#[reflect(PartialEq, Debug, Clone)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub struct ValUnitSet(u8); + +impl ValUnitSet { + /// The empty set. + pub const EMPTY: Self = Self(0); + + /// The number of units in the set. + pub const fn len(&self) -> usize { + self.0.count_ones() as usize + } + + /// Whether the set has no units. + pub const fn is_empty(&self) -> bool { + self.0 == 0 + } + + /// Whether `unit` is in the set. + pub const fn contains(&self, unit: ValUnit) -> bool { + self.0 & unit.bit() != 0 + } + + /// The position `unit` has, or would have, among the set's units in ascending order. + pub const fn slot(&self, unit: ValUnit) -> usize { + (self.0 & (unit.bit() - 1)).count_ones() as usize + } + + /// The units in ascending order. + pub fn iter(self) -> impl Iterator { + ValUnit::ALL + .into_iter() + .filter(move |unit| self.contains(*unit)) + } + + const fn insert(&mut self, unit: ValUnit) { + self.0 |= unit.bit(); + } +} + +impl core::fmt::Debug for ValUnitSet { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_set().entries(self.iter()).finish() + } +} + +/// Working copy of the terms of a [`Val::Sum`], [`Val::Min`] or [`Val::Max`]. +#[derive(Clone, Copy)] +pub(crate) struct ValTerms { + units: ValUnitSet, + values: [f32; 3], +} + +impl ValTerms { + /// The maximum number of terms. + pub(crate) const CAPACITY: usize = 3; + + pub(crate) const fn new(units: ValUnitSet, values: [f32; 3]) -> Self { + Self { units, values } + } + + pub(crate) const fn into_parts(self) -> (ValUnitSet, [f32; 3]) { + (self.units, self.values) + } + + const fn single(unit: ValUnit, value: f32) -> Self { + let mut units = ValUnitSet::EMPTY; + units.insert(unit); + Self::new(units, [value, 0.0, 0.0]) + } + + /// The terms in ascending unit order. + pub(crate) fn terms(self) -> impl Iterator { + self.units.iter().zip(self.values) + } + + /// Folds `value` into the existing term for `unit` with `op`, or inserts it as a new term. + /// + /// Returns `false` and leaves `self` unchanged when a new term would exceed [`Self::CAPACITY`]. + fn fold(&mut self, unit: ValUnit, value: f32, op: fn(f32, f32) -> f32) -> bool { + let slot = self.units.slot(unit); + if self.units.contains(unit) { + self.values[slot] = op(self.values[slot], value); + } else if self.units.len() < Self::CAPACITY { + self.values[slot..].rotate_right(1); + self.values[slot] = value; + self.units.insert(unit); + } else { + return false; + } + true + } + + /// Removes the terms whose value is zero. + fn retain_nonzero(&mut self) { + let mut kept = Self::new(ValUnitSet::EMPTY, [0.0; 3]); + for (unit, value) in self.terms().filter(|(_, value)| *value != 0.0) { + kept.values[kept.units.len()] = value; + kept.units.insert(unit); + } + *self = kept; + } + + pub(crate) fn resolve( + &self, + op: fn(f32, f32) -> f32, + scale_factor: f32, + physical_base_value: f32, + physical_target_size: Vec2, + em_size: EmSize, + rem_size: RemSize, + ) -> f32 { + self.terms() + .map(|(unit, value)| { + Val::leaf(unit, value) + .resolve( + scale_factor, + physical_base_value, + physical_target_size, + em_size, + rem_size, + ) + .unwrap_or(0.0) + }) + .reduce(op) + .unwrap_or(0.0) + } + + /// Interpolates term-wise, or `None` when the units differ. + pub(crate) fn interpolate_stable(&self, other: &Self, t: f32) -> Option { + (self.units == other.units).then(|| { + let mut result = *self; + for (value, other) in result.values.iter_mut().zip(other.values) { + *value = value.interpolate_stable(&other, t); + } + result + }) + } +} + +impl PartialEq for ValTerms { + fn eq(&self, other: &Self) -> bool { + self.units == other.units && self.terms().eq(other.terms()) + } +} + +/// The operation a [`Val::Sum`], [`Val::Min`] or [`Val::Max`] applies to its terms. +#[derive(Clone, Copy)] +pub(crate) enum TermOp { + Sum, + Min, + Max, +} + +impl TermOp { + pub(crate) fn apply(self) -> fn(f32, f32) -> f32 { + match self { + Self::Sum => |a, b| a + b, + Self::Min => f32::min, + Self::Max => f32::max, + } + } + + /// The operation that gives the same result after every term is negated. + const fn reversed(self) -> Self { + match self { + Self::Sum => Self::Sum, + Self::Min => Self::Max, + Self::Max => Self::Min, + } + } + + /// The [`Val`] for `terms` under this operation, as a single unit value when there is one term. + fn wrap(self, terms: ValTerms) -> Val { + if terms.units.len() == 1 + && let Some((unit, value)) = terms.terms().next() + { + return Val::leaf(unit, value); + } + match self { + Self::Sum => Val::Sum(terms.units, terms.values), + Self::Min => Val::Min(terms.units, terms.values), + Self::Max => Val::Max(terms.units, terms.values), + } + } + + fn calc(self, a: Val, b: Val) -> Val { + match self { + Self::Sum => CalcOp::Add(a, b), + Self::Min => CalcOp::Min(a, b), + Self::Max => CalcOp::Max(a, b), + } + .into() + } +} + +impl Val { + /// The terms of a [`Val::Sum`], [`Val::Min`] or [`Val::Max`], or the single term of a + /// unit value. Empty for [`Val::Auto`], [`Val::Clamp`] and [`Val::Calc`]. + pub fn terms(&self) -> impl Iterator { + let terms = match self { + Val::Sum(units, values) | Val::Min(units, values) | Val::Max(units, values) => { + ValTerms::new(*units, *values) + } + _ => match (self.unit(), self.inner()) { + (Some(unit), Some(value)) => ValTerms::single(unit, value), + _ => ValTerms::new(ValUnitSet::EMPTY, [0.0; 3]), + }, + }; + terms.terms() + } + + /// Combines two values with `op`, folding them into one set of terms when both sides are + /// unit values or terms of the same operation, and building a [`Calc`] otherwise. + pub(crate) fn combine(self, rhs: Val, op: TermOp) -> Val { + if let (Some(mut terms), Some(rhs_terms)) = (self.terms_for(op), rhs.terms_for(op)) + && rhs_terms + .terms() + .all(|(unit, value)| terms.fold(unit, value, op.apply())) + { + return op.wrap(terms); + } + op.calc(self, rhs) + } + + fn terms_for(&self, op: TermOp) -> Option { + match (self, op) { + (Val::Sum(units, values), TermOp::Sum) + | (Val::Min(units, values), TermOp::Min) + | (Val::Max(units, values), TermOp::Max) => Some(ValTerms::new(*units, *values)), + _ => Some(ValTerms::single(self.unit()?, self.inner()?)), + } + } + + /// The terms of a [`Val::Sum`] or unit value with zero terms removed, so that sums + /// compare equal whatever zero terms they carry. `None` for other values. + pub(crate) fn sum_terms(&self) -> Option { + let mut terms = match self { + Val::Sum(units, values) => ValTerms::new(*units, *values), + _ => ValTerms::single(self.unit()?, self.inner()?), + }; + terms.retain_nonzero(); + Some(terms) + } + + /// The inline clamp of `value` between `min` and `max`, when all three are unit values. + pub(crate) fn clamp_inline(min: &Val, value: &Val, max: &Val) -> Option { + let term = |val: &Val| Some((val.unit()?, val.inner()?)); + let [lo, v, hi] = [term(min)?, term(value)?, term(max)?]; + if lo.0 == v.0 && v.0 == hi.0 { + return Some(Val::leaf(v.0, lo.1.max(v.1.min(hi.1)))); + } + // Inverted bounds always resolve to the minimum, so an inline clamp with same-unit + // bounds is guaranteed to have `min <= max`. + if lo.0 == hi.0 && lo.1 > hi.1 { + return Some(Val::leaf(lo.0, lo.1)); + } + Some(Val::Clamp([lo.0, v.0, hi.0], [lo.1, v.1, hi.1])) + } + + pub(crate) fn resolve_clamp( + units: [ValUnit; 3], + values: [f32; 3], + scale_factor: f32, + physical_base_value: f32, + physical_target_size: Vec2, + em_size: EmSize, + rem_size: RemSize, + ) -> f32 { + let [lo, v, hi] = core::array::from_fn(|i| { + Val::leaf(units[i], values[i]) + .resolve( + scale_factor, + physical_base_value, + physical_target_size, + em_size, + rem_size, + ) + .unwrap_or(0.0) + }); + lo.max(v.min(hi)) + } + + /// Applies `f` to every term of a [`Val::Sum`], [`Val::Min`], [`Val::Max`] or [`Val::Clamp`]. + /// `reverses` swaps `Min` and `Max` and the bounds of `Clamp`, for an `f` that reverses ordering. + pub(crate) fn map_terms(self, f: impl Fn(f32) -> f32, reverses: bool) -> Self { + let (op, units, mut values) = match self { + Val::Sum(units, values) => (TermOp::Sum, units, values), + Val::Min(units, values) => (TermOp::Min, units, values), + Val::Max(units, values) => (TermOp::Max, units, values), + Val::Clamp(units, values) => { + // Inverted same-unit bounds make the clamp its minimum whatever the value is. + if units[0] == units[2] && values[0] > values[2] { + return Val::leaf(units[0], f(values[0])); + } + let values = values.map(&f); + if !reverses { + return Val::Clamp(units, values); + } + // `f` reverses order, so `f(max(lo, min(v, hi)))` is `min(f(lo), max(f(v), f(hi)))`. + // Same-unit bounds are now known to satisfy `lo <= hi`, so that is the clamp with + // the bounds swapped; mixed-unit bounds may invert at some bases, so keep the + // exact form. + let [lo, v, hi] = core::array::from_fn(|i| Val::leaf(units[i], values[i])); + if units[0] == units[2] { + return Val::Clamp( + [units[2], units[1], units[0]], + [values[2], values[1], values[0]], + ); + } + return lo.min(v.max(hi)); + } + other => return other, + }; + for value in &mut values[..units.len()] { + *value = f(*value); + } + let op = if reverses { op.reversed() } else { op }; + op.wrap(ValTerms::new(units, values)) + } +} + +#[cfg(test)] +mod tests { + use bevy_math::vec2; + + use super::*; + + #[test] + fn val_evaluate() { + let size = 250.; + let viewport_size = vec2(1000., 500.); + let em_size = EmSize(10.); + let rem_size = RemSize(20.); + + assert_eq!( + Val::Percent(80.) + .resolve(1., size, viewport_size, em_size, rem_size) + .unwrap(), + size * 0.8 + ); + // Check em and rem + assert_eq!( + Val::Em(2.) + .resolve(1., size, viewport_size, em_size, rem_size) + .unwrap(), + em_size.0 * 2. + ); + assert_eq!( + Val::Rem(5.) + .resolve(1., size, viewport_size, em_size, rem_size) + .unwrap(), + rem_size.0 * 5. + ); + // Check scale_factor is considered + assert_eq!( + Val::Em(2.) + .resolve(2., size, viewport_size, em_size, rem_size) + .unwrap(), + em_size.0 * 2. * 2. + ); + assert_eq!( + Val::Rem(5.) + .resolve(2., size, viewport_size, em_size, rem_size) + .unwrap(), + rem_size.0 * 5. * 2. + ); + } + + #[test] + fn val_resolve_px() { + let size = 250.; + let viewport_size = vec2(1000., 500.); + + let result = Val::Px(10.) + .resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) + .unwrap(); + assert_eq!(result, 10.); + + let result = Val::Px(10.) + .resolve(3., size, viewport_size, EmSize(20.0), RemSize(20.0)) + .unwrap(); + assert_eq!(result, 30.); + let result = Val::Px(10.) + .resolve(0.25, size, viewport_size, EmSize(20.0), RemSize(20.0)) + .unwrap(); + assert_eq!(result, 2.5); + + let result = Val::Em(1.) + .resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) + .unwrap(); + assert_eq!(result, 20.0); + let result = Val::Rem(1.) + .resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) + .unwrap(); + assert_eq!(result, 20.0); + } + + #[test] + fn val_fits_in_two_words() { + assert!(size_of::() <= 16); + } + + #[test] + fn val_resolve_viewport_coords() { + let size = 250.; + let viewport_size = vec2(500., 500.); + + for value in (-10..10).map(|value| value as f32) { + // for a square viewport there should be no difference between `Vw` and `Vh` and between `Vmin` and `Vmax`. + assert_eq!( + Val::Vw(value).resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)), + Val::Vh(value).resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) + ); + assert_eq!( + Val::VMin(value).resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)), + Val::VMax(value).resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) + ); + assert_eq!( + Val::VMin(value).resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)), + Val::Vw(value).resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) + ); + } + + let viewport_size = vec2(1000., 500.); + assert_eq!( + Val::Vw(100.) + .resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) + .unwrap(), + 1000. + ); + assert_eq!( + Val::Vh(100.) + .resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) + .unwrap(), + 500. + ); + assert_eq!( + Val::Vw(60.) + .resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) + .unwrap(), + 600. + ); + assert_eq!( + Val::Vh(40.) + .resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) + .unwrap(), + 200. + ); + assert_eq!( + Val::VMin(50.) + .resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) + .unwrap(), + 250. + ); + assert_eq!( + Val::VMax(75.) + .resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) + .unwrap(), + 750. + ); + } + + #[test] + fn val_auto_is_non_evaluable() { + let size = 250.; + let viewport_size = vec2(1000., 500.); + let resolve_auto = Val::Auto.resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)); + + assert_eq!(resolve_auto, Err(ValArithmeticError::NonEvaluable)); + } + + #[test] + fn val_try_add_compatible_variants() { + assert_eq!(Val::Px(1.).try_add(Val::Px(2.)), Ok(Val::Px(3.))); + assert_eq!( + Val::Percent(1.).try_add(Val::Percent(2.)), + Ok(Val::Percent(3.)) + ); + assert_eq!(Val::Vw(1.).try_add(Val::Vw(2.)), Ok(Val::Vw(3.))); + assert_eq!(Val::Vh(1.).try_add(Val::Vh(2.)), Ok(Val::Vh(3.))); + assert_eq!(Val::VMin(1.).try_add(Val::VMin(2.)), Ok(Val::VMin(3.))); + assert_eq!(Val::VMax(1.).try_add(Val::VMax(2.)), Ok(Val::VMax(3.))); + } + + #[test] + fn val_try_add_incompatible_variants() { + assert_eq!( + Val::Auto.try_add(Val::Auto), + Err(ValArithmeticError::IncompatibleUnits) + ); + assert_eq!( + Val::Px(1.).try_add(Val::Percent(2.)), + Err(ValArithmeticError::IncompatibleUnits) + ); + assert_eq!( + Val::Auto.try_add(Val::Px(1.)), + Err(ValArithmeticError::IncompatibleUnits) + ); + } + + #[test] + fn val_try_sub_compatible_variants() { + assert_eq!(Val::Px(3.).try_sub(Val::Px(2.)), Ok(Val::Px(1.))); + assert_eq!( + Val::Percent(3.).try_sub(Val::Percent(2.)), + Ok(Val::Percent(1.)) + ); + assert_eq!(Val::Vw(3.).try_sub(Val::Vw(2.)), Ok(Val::Vw(1.))); + assert_eq!(Val::Vh(3.).try_sub(Val::Vh(2.)), Ok(Val::Vh(1.))); + assert_eq!(Val::VMin(3.).try_sub(Val::VMin(2.)), Ok(Val::VMin(1.))); + assert_eq!(Val::VMax(3.).try_sub(Val::VMax(2.)), Ok(Val::VMax(1.))); + } + + #[test] + fn val_try_sub_incompatible_variants() { + assert_eq!( + Val::Auto.try_sub(Val::Auto), + Err(ValArithmeticError::IncompatibleUnits) + ); + assert_eq!( + Val::Px(1.).try_sub(Val::Percent(2.)), + Err(ValArithmeticError::IncompatibleUnits) + ); + assert_eq!( + Val::Auto.try_sub(Val::Px(1.)), + Err(ValArithmeticError::IncompatibleUnits) + ); + } + + #[test] + fn val_str_parse() { + assert_eq!("auto".parse::(), Ok(Val::Auto)); + assert_eq!("Auto".parse::(), Ok(Val::Auto)); + assert_eq!("AUTO".parse::(), Ok(Val::Auto)); + + assert_eq!("3px".parse::(), Ok(Val::Px(3.))); + assert_eq!("3 px".parse::(), Ok(Val::Px(3.))); + assert_eq!("3.5px".parse::(), Ok(Val::Px(3.5))); + assert_eq!("-3px".parse::(), Ok(Val::Px(-3.))); + assert_eq!("3.5 PX".parse::(), Ok(Val::Px(3.5))); + + assert_eq!("3%".parse::(), Ok(Val::Percent(3.))); + assert_eq!("3 %".parse::(), Ok(Val::Percent(3.))); + assert_eq!("3.5%".parse::(), Ok(Val::Percent(3.5))); + assert_eq!("-3%".parse::(), Ok(Val::Percent(-3.))); + + assert_eq!("3vw".parse::(), Ok(Val::Vw(3.))); + assert_eq!("3 vw".parse::(), Ok(Val::Vw(3.))); + assert_eq!("3.5vw".parse::(), Ok(Val::Vw(3.5))); + assert_eq!("-3vw".parse::(), Ok(Val::Vw(-3.))); + assert_eq!("3.5 VW".parse::(), Ok(Val::Vw(3.5))); + + assert_eq!("3vh".parse::(), Ok(Val::Vh(3.))); + assert_eq!("3 vh".parse::(), Ok(Val::Vh(3.))); + assert_eq!("3.5vh".parse::(), Ok(Val::Vh(3.5))); + assert_eq!("-3vh".parse::(), Ok(Val::Vh(-3.))); + assert_eq!("3.5 VH".parse::(), Ok(Val::Vh(3.5))); + + assert_eq!("3vmin".parse::(), Ok(Val::VMin(3.))); + assert_eq!("3 vmin".parse::(), Ok(Val::VMin(3.))); + assert_eq!("3.5vmin".parse::(), Ok(Val::VMin(3.5))); + assert_eq!("-3vmin".parse::(), Ok(Val::VMin(-3.))); + assert_eq!("3.5 VMIN".parse::(), Ok(Val::VMin(3.5))); + + assert_eq!("3vmax".parse::(), Ok(Val::VMax(3.))); + assert_eq!("3 vmax".parse::(), Ok(Val::VMax(3.))); + assert_eq!("3.5vmax".parse::(), Ok(Val::VMax(3.5))); + assert_eq!("-3vmax".parse::(), Ok(Val::VMax(-3.))); + assert_eq!("3.5 VMAX".parse::(), Ok(Val::VMax(3.5))); + + assert_eq!("3em".parse::(), Ok(Val::Em(3.))); + assert_eq!("3rem".parse::(), Ok(Val::Rem(3.))); + + assert_eq!("".parse::(), Err(ValParseError::UnitMissing)); + assert_eq!( + "hello world".parse::(), + Err(ValParseError::ValueMissing) + ); + assert_eq!("3".parse::(), Err(ValParseError::UnitMissing)); + assert_eq!("3.5".parse::(), Err(ValParseError::UnitMissing)); + assert_eq!("3pxx".parse::(), Err(ValParseError::InvalidUnit)); + assert_eq!("3.5pxx".parse::(), Err(ValParseError::InvalidUnit)); + assert_eq!("3-3px".parse::(), Err(ValParseError::InvalidValue)); + assert_eq!("3.5-3px".parse::(), Err(ValParseError::InvalidValue)); + } + + #[test] + fn default_val_equals_const_default_val() { + assert_eq!(Val::default(), Val::DEFAULT); + } + + #[test] + fn val_constructor_fns_return_correct_val_variant() { + assert_eq!(auto(), Val::Auto); + assert_eq!(px(0.0), Val::Px(0.0)); + assert_eq!(percent(0.0), Val::Percent(0.0)); + assert_eq!(vw(0.0), Val::Vw(0.0)); + assert_eq!(vh(0.0), Val::Vh(0.0)); + assert_eq!(vmin(0.0), Val::VMin(0.0)); + assert_eq!(vmax(0.0), Val::VMax(0.0)); + } + + #[test] + fn sums_fold_inline() { + let sum = Val::Px(10.0) + Val::Percent(50.0); + assert!(matches!(sum, Val::Sum(..)), "got {sum:?}"); + assert_eq!( + sum.terms().collect::>(), + [(ValUnit::Px, 10.0), (ValUnit::Percent, 50.0)] + ); + assert_eq!(Val::Percent(50.0) + Val::Px(10.0), sum); + assert_eq!(Val::Px(1.0) + Val::Px(2.0), Val::Px(3.0)); + assert_eq!(sum.clone() - Val::Px(10.0), Val::Percent(50.0)); + assert_eq!(Val::Px(0.0) + Val::Percent(0.0), Val::ZERO); + assert_ne!(sum, Val::Px(10.0)); + assert_eq!( + (Val::Px(1.0) + Val::Percent(2.0)) + (Val::Px(3.0) + Val::Em(4.0)), + Val::Em(4.0) + Val::Px(4.0) + Val::Percent(2.0) + ); + assert!(matches!(sum + Val::Em(1.0) + Val::Rem(1.0), Val::Calc(_))); + } + + #[test] + fn min_and_max_fold_inline() { + assert_eq!(Val::Px(10.0).min(Val::Px(20.0)), Val::Px(10.0)); + assert_eq!(Val::Px(10.0).max(Val::Px(20.0)), Val::Px(20.0)); + let min = Val::Px(10.0).min(Val::Percent(50.0)); + assert!(matches!(min, Val::Min(..))); + assert_eq!(Val::Percent(50.0).min(Val::Px(10.0)), min); + assert_eq!(-min.clone(), Val::Px(-10.0).max(Val::Percent(-50.0))); + assert!(matches!(min.clone().max(Val::Px(5.0)), Val::Calc(_))); + let resolve = + |value: &Val| value.resolve(1.0, 100.0, Vec2::ZERO, EmSize(0.0), RemSize(0.0)); + assert_eq!(resolve(&min), Ok(10.0)); + assert_eq!(resolve(&Val::Px(10.0).max(Val::Percent(50.0))), Ok(50.0)); + assert_eq!(resolve(&(min * -2.0)), Ok(-20.0)); + } + + #[test] + fn negating_min_and_max_negates_their_results() { + assert_eq!(-Val::Px(10.0).max(Val::Px(20.0)), Val::Px(-20.0)); + assert_eq!(-Val::Px(10.0).min(Val::Px(20.0)), Val::Px(-10.0)); + let resolve = |value: &Val, base: f32| { + value.resolve(1.0, base, Vec2::ZERO, EmSize(0.0), RemSize(0.0)) + }; + for expr in [ + Val::Px(10.0).max(Val::Percent(50.0)), + Val::Px(10.0).min(Val::Percent(50.0)), + ] { + for base in [0.0, 20.0, 100.0] { + let expected = resolve(&expr, base).unwrap(); + assert_eq!( + resolve(&-expr.clone(), base), + Ok(-expected), + "{expr:?} at {base}" + ); + assert_eq!( + resolve(&(expr.clone() * -2.0), base), + Ok(-2.0 * expected), + "{expr:?} at {base}" + ); + } + } + } + + #[test] + fn negating_a_clamp_negates_its_result() { + let clamp = Val::Percent(50.0).clamp(Val::Px(100.0), Val::Percent(20.0)); + let resolve = |value: &Val, base: f32| { + value.resolve(1.0, base, Vec2::ZERO, EmSize(0.0), RemSize(0.0)) + }; + for base in [50.0, 100.0, 150.0, 300.0, 1000.0] { + let expected = resolve(&clamp, base).unwrap(); + assert_eq!(resolve(&-clamp.clone(), base), Ok(-expected), "base {base}"); + assert_eq!( + resolve(&(clamp.clone() * -2.0), base), + Ok(-2.0 * expected), + "base {base}" + ); + assert_eq!( + resolve(&(Val::ZERO - clamp.clone()), base), + Ok(-expected), + "base {base}" + ); + } + } + + #[test] + fn clamp_folds_inline() { + use bevy_math::TryStableInterpolate; + let clamp = Val::Percent(80.0).clamp(Val::Px(100.0), Val::Px(300.0)); + assert!(matches!(clamp, Val::Clamp(..)), "got {clamp:?}"); + assert_eq!( + Val::Percent(80.0).clamp(Val::Px(300.0), Val::Px(100.0)), + Val::Px(300.0) + ); + let resolve = |value: &Val, base: f32| { + value.resolve(1.0, base, Vec2::ZERO, EmSize(0.0), RemSize(0.0)) + }; + assert_eq!(resolve(&clamp, 100.0), Ok(100.0)); + assert_eq!(resolve(&clamp, 250.0), Ok(200.0)); + assert_eq!(resolve(&clamp, 1000.0), Ok(300.0)); + assert_eq!( + -clamp.clone(), + Val::Percent(-80.0).clamp(Val::Px(-300.0), Val::Px(-100.0)) + ); + assert_eq!(resolve(&(clamp.clone() * -2.0), 250.0), Ok(-400.0)); + assert_eq!( + clamp.clone() * 2.0, + Val::Percent(160.0).clamp(Val::Px(200.0), Val::Px(600.0)) + ); + assert_eq!( + clamp + .try_interpolate_stable(&Val::Percent(0.0).clamp(Val::Px(0.0), Val::Px(100.0)), 0.5) + .ok(), + Some(Val::Percent(40.0).clamp(Val::Px(50.0), Val::Px(200.0))) + ); + assert!(matches!( + (Val::Percent(80.0) + Val::Px(1.0)).clamp(Val::Px(100.0), Val::Px(300.0)), + Val::Calc(_) + )); + } + + #[test] + fn terms_interpolate_term_wise() { + use bevy_math::TryStableInterpolate; + let a = Val::Px(0.0) + Val::Percent(0.0); + let b = Val::Px(10.0) + Val::Percent(100.0); + assert_eq!( + a.try_interpolate_stable(&b, 0.5).ok(), + Some(Val::Px(5.0) + Val::Percent(50.0)) + ); + assert!(a + .try_interpolate_stable(&(Val::Px(1.0) + Val::Em(1.0)), 0.5) + .is_err()); + } + + #[test] + fn resolve_expressions() { + let value = + Val::Percent(50.0) - Val::Px(10.0) + Val::Em(2.0) + Val::Rem(1.0) + Val::Vw(1.0); + let resolve = |value: &Val| { + value.resolve( + 2.0, + 400.0, + Vec2::new(1000.0, 500.0), + EmSize(12.0), + RemSize(20.0), + ) + }; + assert_eq!(resolve(&value), Ok(278.0)); + assert_eq!(resolve(&(value.clone() * 2.0)), Ok(556.0)); + assert_eq!(resolve(&(value.clone() / 2.0)), Ok(139.0)); + assert_eq!(resolve(&-value.clone()), Ok(-278.0)); + assert_eq!( + resolve(&CalcOp::Min(value.clone(), Val::Px(100.0)).into()), + Ok(200.0) + ); + assert_eq!( + resolve(&CalcOp::Max(value.clone(), Val::Px(200.0)).into()), + Ok(400.0) + ); + assert_eq!( + resolve(&CalcOp::Clamp(Val::Px(100.0), value.clone(), Val::Px(125.0)).into()), + Ok(250.0) + ); + assert_eq!( + resolve(&CalcOp::Clamp(Val::Px(200.0), value.clone(), Val::Px(100.0)).into()), + Ok(400.0) + ); + let mut scaled = value; + scaled *= 2.0; + scaled /= 4.0; + assert_eq!(resolve(&scaled), Ok(139.0)); + assert_eq!( + resolve(&(Val::Auto + Val::Px(1.0))), + Err(ValArithmeticError::NonEvaluable) + ); + } + + #[test] + fn negative_scale_swaps_min_and_max() { + let resolve = |value: Val| value.resolve(1.0, 0.0, Vec2::ZERO, EmSize(0.0), RemSize(0.0)); + let (a, b) = (Val::Px(10.0), Val::Px(20.0)); + assert_eq!( + resolve(-Val::from(CalcOp::Min(a.clone(), b.clone()))), + Ok(-10.0) + ); + assert_eq!( + resolve(-Val::from(CalcOp::Max(a.clone(), b.clone()))), + Ok(-20.0) + ); + assert_eq!( + resolve(-Val::from(CalcOp::Clamp( + a.clone(), + Val::Px(15.0), + b.clone() + ))), + Ok(-15.0) + ); + assert_eq!( + resolve(-Val::from(CalcOp::Clamp(b, Val::Px(15.0), a))), + Ok(-20.0) + ); + } + + #[test] + fn repeated_scaling_does_not_grow_the_tree() { + let mut value = Val::Px(1.0) + Val::Px(2.0); + for _ in 0..100_000 { + value *= 2.0; + value /= 2.0; + } + assert_eq!(value, Val::Px(1.0) + Val::Px(2.0)); + } + + #[test] + fn clone_and_reflection_preserve_expressions() { + use bevy_reflect::{FromReflect, PartialReflect, TypeRegistry}; + + let value = Val::Percent(100.0) - Val::Px(24.0); + let mut cloned = value.clone(); + assert_eq!(value, cloned); + cloned *= 2.0; + assert_ne!(value, cloned); + assert_eq!(value, Val::Percent(100.0) - Val::Px(24.0)); + let mut registry = TypeRegistry::default(); + registry.register::(); + let dynamic = value.to_dynamic().unwrap(); + assert_eq!(Val::from_reflect(dynamic.as_ref()), Some(value)); + } + + #[cfg(feature = "serialize")] + #[test] + fn expressions_round_trip_through_serde() { + let values = [ + Val::Percent(50.0) - Val::Px(10.0), + Val::Px(10.0).min(Val::Percent(50.0)), + Val::Percent(80.0).clamp(Val::Px(100.0), Val::Px(300.0)), + CalcOp::Min(Val::Em(1.0) + Val::Percent(10.0), Val::Rem(1.0)).into(), + ]; + for value in values { + let ron = ron::to_string(&value).unwrap(); + assert_eq!(ron::from_str::(&ron).unwrap(), value, "{ron}"); + } + } + + #[test] + fn font_size_rejects_expressions() { + assert_eq!(FontSize::try_from(Val::Rem(2.0)), Ok(FontSize::Rem(2.0))); + assert_eq!( + FontSize::try_from(Val::Percent(100.0) - Val::Px(24.0)), + Err(ValArithmeticError::NonEvaluable) + ); + } + + #[test] + fn direct_inline_clamp_negates_its_resolved_value() { + let value = Val::Clamp( + [ValUnit::Px, ValUnit::Percent, ValUnit::Px], + [100.0, 50.0, 20.0], + ); + let resolve = |value: Val| { + value + .resolve( + 1.0, + 100.0, + Default::default(), + Default::default(), + Default::default(), + ) + .unwrap() + }; + assert_eq!(resolve(value.clone()), 100.0); + assert_eq!(resolve(-value), -100.0); + } + + #[test] + fn extrapolated_inline_clamp_negates_its_resolved_value() { + let a = Val::Percent(50.0).clamp(Val::Px(0.0), Val::Px(100.0)); + let b = Val::Percent(50.0).clamp(Val::Px(100.0), Val::Px(100.0)); + let value = TryStableInterpolate::try_interpolate_stable(&a, &b, 2.0).unwrap(); + let resolve = |value: Val| { + value + .resolve( + 1.0, + 100.0, + Default::default(), + Default::default(), + Default::default(), + ) + .unwrap() + }; + assert_eq!(resolve(value.clone()), 200.0); + assert_eq!(resolve(-value), -200.0); + } +} From 9cda3bd40f13a407652864fade2781646ef00953 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 2 Nov 2026 12:13:42 +0000 Subject: [PATCH 27/50] Dereference calc pointers directly instead of mapping them Every calc pointer in a `TaffyStyle` points into one of the `Arc`s stored alongside it, so `resolve_calc` can dereference the pointer taffy hands back. That removes the per-layout subtree walk, the pointer map and the `resolve_calc` closure plumbing. To keep that sound, `TaffyStyle` loses `DerefMut` and its fields become crate-private, so nothing outside the layout module can store a pointer in a style, and `MeasureArgs` is `#[non_exhaustive]` so it can only be built by the layout system. Co-Authored-By: Claude Fable 5.1 --- crates/bevy_ui/src/layout/layout_tree.rs | 74 ++++++++++++------------ crates/bevy_ui/src/layout/tests.rs | 2 - crates/bevy_ui/src/measurement.rs | 14 ++--- 3 files changed, 45 insertions(+), 45 deletions(-) diff --git a/crates/bevy_ui/src/layout/layout_tree.rs b/crates/bevy_ui/src/layout/layout_tree.rs index 680a6a41a4d61..4d5bcc8f9e56a 100644 --- a/crates/bevy_ui/src/layout/layout_tree.rs +++ b/crates/bevy_ui/src/layout/layout_tree.rs @@ -1,4 +1,4 @@ -use bevy_derive::{Deref, DerefMut}; +use bevy_derive::Deref; use bevy_ecs::{ change_detection::DetectChanges, component::Component, @@ -9,7 +9,7 @@ use bevy_ecs::{ world::Ref, }; use bevy_math::{UVec2, Vec2}; -use bevy_platform::{collections::HashMap, sync::Arc}; +use bevy_platform::sync::Arc; use bevy_text::{ComputedTextBlock, EmSize, FontCx, RemSize}; use bevy_utils::default; use taffy::{ @@ -30,20 +30,30 @@ use crate::{ #[expect( unsafe_code, - reason = "calc pointers are opaque lookup keys and are never dereferenced" + reason = "the calc pointers in the style point into the `Arc`s owned by the same `TaffyStyle`" )] -// SAFETY: Calc pointers are only used as lookup keys, never dereferenced. The owned expressions are Send + Sync. +// SAFETY: `Style` is only `!Send` because of its raw calc pointers, which point into the +// `Arc`s held alongside it. `PercentageCalc` is `Send + Sync`. unsafe impl Send for TaffyStyle {} #[expect( unsafe_code, - reason = "calc pointers are opaque lookup keys and are never dereferenced" + reason = "the calc pointers in the style point into the `Arc`s owned by the same `TaffyStyle`" )] -// SAFETY: Calc pointers are only used as lookup keys, never dereferenced. The owned expressions are Send + Sync. +// SAFETY: `Style` is only `!Sync` because of its raw calc pointers, which point into the +// `Arc`s held alongside it. `PercentageCalc` is `Send + Sync`. unsafe impl Sync for TaffyStyle {} -#[derive(Default, Component, Clone, Deref, DerefMut)] -pub struct TaffyStyle(#[deref] pub Style, pub(crate) Vec>); +/// A node's resolved taffy style, with the expressions its calc values point to. +/// +/// Every calc pointer in the style points into one of the `Arc`s stored next +/// to it, which is what `resolve_calc` relies on. The fields are crate-private so nothing +/// else can put a pointer in the style, and it can only be read through [`Deref`]. +#[derive(Default, Component, Clone, Deref)] +pub struct TaffyStyle( + #[deref] pub(crate) Style, + pub(crate) Vec>, +); /// An expression with every unit other than percent already resolved to physical pixels, /// so only the percentage basis taffy supplies is needed to resolve it. @@ -58,6 +68,24 @@ impl PercentageCalc { } } +/// Resolves a calc pointer that taffy read back out of a [`TaffyStyle`] against the +/// percentage `basis`. +#[expect( + unsafe_code, + reason = "taffy hands back the pointer it was given, which points into an `Arc` owned by the same `TaffyStyle`" +)] +pub(crate) fn resolve_calc(ptr: *const (), basis: f32) -> f32 { + // SAFETY: Calc pointers only enter a `TaffyStyle` through `update_taffy_style_from_node`, + // which creates each one with `Arc::as_ptr` on an `Arc` that it pushes + // into the same style, and rewrites every length field whenever it clears that `Vec`. + // `TaffyStyle`'s fields are crate-private and it is not `DerefMut`, so no other code can + // store a pointer in one, and `MeasureArgs` cannot be built outside the crate. Layout + // borrows styles immutably for as long as taffy can hand a pointer back, and clones share + // the `Arc`s, so the pointee is alive and not being written. + let calc = unsafe { &*ptr.cast::() }; + calc.resolve(basis) +} + pub static VIEWPORT_NODE_TAFFY_STYLE: TaffyStyle = TaffyStyle( Style { dummy: core::marker::PhantomData, @@ -376,29 +404,6 @@ pub fn compute_layout<'w, 's>( }; { - let mut calcs: HashMap = HashMap::default(); - child_stack.push(root_node_id); - while let Some(node_id) = child_stack.pop() { - let entity = entity_from_node_id(node_id).expect("Viewport node should not be a child"); - let style = style_query - .get(entity) - .expect("Missing TaffyStyle on UI node."); - calcs.extend( - style - .1 - .iter() - .map(|calc| (Arc::as_ptr(calc) as usize, calc.as_ref())), - ); - let layout = computed_layout_query - .get(entity) - .expect("Missing ComputedLayout on UI node."); - child_stack.extend_from_slice(&layout.ui_children); - } - let resolve_calc = |ptr: *const (), basis: f32| { - calcs - .get(&(ptr as usize)) - .map_or(0.0, |calc| calc.resolve(basis)) - }; let mut measure_function = |known_dimensions: taffy::Size>, available_space: taffy::Size, entity: Entity, @@ -417,7 +422,6 @@ pub fn compute_layout<'w, 's>( font_system, buffer: None, style, - resolve_calc: &resolve_calc, }; let buffer = get_text_buffer( crate::widget::TextMeasure::needs_buffer( @@ -444,7 +448,6 @@ pub fn compute_layout<'w, 's>( ..default() }, measure_function: &mut measure_function, - resolve_calc: &resolve_calc, layout_changed: false, }; @@ -645,7 +648,6 @@ struct UiLayoutTree<'a, 'w, 's> { &Style, ) -> taffy::Size, layout_changed: bool, - resolve_calc: &'a dyn Fn(*const (), f32) -> f32, } impl<'a> UiLayoutTree<'a, '_, '_> { @@ -703,7 +705,7 @@ impl<'a> UiLayoutTree<'a, '_, '_> { (_, false) => compute_leaf_layout( inputs, style, - tree.resolve_calc, + resolve_calc, |known_dimensions, available_space| { (tree.measure_function)( known_dimensions, @@ -748,7 +750,7 @@ impl TraverseTree for UiLayoutTree<'_, '_, '_> {} impl<'tree, 'w, 's> LayoutPartialTree for UiLayoutTree<'tree, 'w, 's> { fn resolve_calc_value(&self, val: *const (), basis: f32) -> f32 { - (self.resolve_calc)(val, basis) + resolve_calc(val, basis) } type CoreContainerStyle<'a> diff --git a/crates/bevy_ui/src/layout/tests.rs b/crates/bevy_ui/src/layout/tests.rs index 37d8a3ae09c86..c19d7a8b68715 100644 --- a/crates/bevy_ui/src/layout/tests.rs +++ b/crates/bevy_ui/src/layout/tests.rs @@ -132,7 +132,6 @@ fn calc_layout_resolves_percentages_and_updates_context() { let clone = app.world().get::(child).unwrap().clone(); let mut font_system = bevy_text::FontCx::default(); - let resolve_calc = |_, basis| clone.1[0].resolve(basis); let measure_args = crate::MeasureArgs { known_width: None, known_height: None, @@ -141,7 +140,6 @@ fn calc_layout_resolves_percentages_and_updates_context() { font_system: &mut font_system, buffer: None, style: &clone, - resolve_calc: &resolve_calc, }; assert_eq!(measure_args.resolve_width().preferred, Some(230.0)); app.world_mut().get_mut::(child).unwrap().width = Val::Px(90.0); diff --git a/crates/bevy_ui/src/measurement.rs b/crates/bevy_ui/src/measurement.rs index 56fa34f1d31a0..0280f31b64fc2 100644 --- a/crates/bevy_ui/src/measurement.rs +++ b/crates/bevy_ui/src/measurement.rs @@ -7,6 +7,7 @@ pub use taffy::style::AvailableSpace; use taffy::MaybeMath; use taffy::MaybeResolve; +use crate::layout::layout_tree::resolve_calc; use crate::widget::ImageMeasure; use crate::widget::TextMeasure; @@ -18,6 +19,9 @@ impl core::fmt::Debug for ContentSize { } /// Inputs provided to [`Measure::measure`]. +/// +/// Only the layout system builds these, which keeps the calc pointers in `style` valid. +#[non_exhaustive] pub struct MeasureArgs<'a> { /// The known width from Taffy if this size is definite. pub known_width: Option, @@ -33,7 +37,6 @@ pub struct MeasureArgs<'a> { pub buffer: Option<&'a mut bevy_text::ComputedTextBlock>, /// Resolved style for this node in physical pixels. pub style: &'a taffy::Style, - pub resolve_calc: &'a dyn Fn(*const (), f32) -> f32, } #[derive(Copy, Clone)] @@ -55,12 +58,11 @@ fn resolve_axis( min_dim: taffy::style::LengthPercentageAuto, size_dim: taffy::style::Dimension, max_dim: taffy::style::LengthPercentageAuto, - calc: &dyn Fn(*const (), f32) -> f32, ) -> ResolvedAxis { let available = available_space.into_option(); - let min = min_dim.maybe_resolve(available, calc); - let preferred = size_dim.maybe_resolve(available, calc); - let max = max_dim.maybe_resolve(available, calc); + let min = min_dim.maybe_resolve(available, resolve_calc); + let preferred = size_dim.maybe_resolve(available, resolve_calc); + let max = max_dim.maybe_resolve(available, resolve_calc); ResolvedAxis { min, preferred, @@ -78,7 +80,6 @@ impl MeasureArgs<'_> { self.style.min_size.width, self.style.size.width, self.style.max_size.width, - self.resolve_calc, ) } @@ -90,7 +91,6 @@ impl MeasureArgs<'_> { self.style.min_size.height, self.style.size.height, self.style.max_size.height, - self.resolve_calc, ) } } From e5b3d43566feacbc92c3f85f5a71749a94f1b7cf Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 2 Nov 2026 12:16:08 +0000 Subject: [PATCH 28/50] Fix unbalanced backtick in to_physical doc comment Co-Authored-By: Claude Fable 5.1 --- crates/bevy_ui/src/layout/convert.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ui/src/layout/convert.rs b/crates/bevy_ui/src/layout/convert.rs index 84c882f839ac7..9f6992e2253f5 100644 --- a/crates/bevy_ui/src/layout/convert.rs +++ b/crates/bevy_ui/src/layout/convert.rs @@ -18,7 +18,7 @@ impl Val { /// finish resolving them during layout. /// /// Rebuilding through the `Val` operators folds the resolved terms. Most expressions - /// collapse to a single `Px` or a `Px` and `Percent` sum`. + /// collapse to a single `Px` or a `Px` and `Percent` sum. fn to_physical(&self, context: &ValContext) -> Val { let resolve_leaf_val = |(unit, value): (ValUnit, f32)| match unit { ValUnit::Percent => Val::Percent(value), From 3b6a72e652405bdc09787c8da950cca19d8ff948 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 2 Nov 2026 12:46:28 +0000 Subject: [PATCH 29/50] Added a `resolve` method to `ValUnit`. Used it in `convert` to resolve leaf values. --- crates/bevy_ui/src/layout/convert.rs | 15 +------------- crates/bevy_ui/src/val.rs | 29 ++++++++++++++++++++++++---- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/crates/bevy_ui/src/layout/convert.rs b/crates/bevy_ui/src/layout/convert.rs index 9f6992e2253f5..193e98cf115c4 100644 --- a/crates/bevy_ui/src/layout/convert.rs +++ b/crates/bevy_ui/src/layout/convert.rs @@ -20,20 +20,7 @@ impl Val { /// Rebuilding through the `Val` operators folds the resolved terms. Most expressions /// collapse to a single `Px` or a `Px` and `Percent` sum. fn to_physical(&self, context: &ValContext) -> Val { - let resolve_leaf_val = |(unit, value): (ValUnit, f32)| match unit { - ValUnit::Percent => Val::Percent(value), - _ => Val::Px( - Val::leaf(unit, value) - .resolve( - context.scale_factor, - 0.0, - context.physical_size, - EmSize(context.em_size), - RemSize(context.rem_size), - ) - .unwrap_or(0.0), - ), - }; + let resolve_leaf_val = |(unit, value): (ValUnit, f32)| unit.resolve(value, 1., context); match self { Val::Auto => Val::Auto, Val::Min(..) => self diff --git a/crates/bevy_ui/src/val.rs b/crates/bevy_ui/src/val.rs index 9cffc38f7cbbc..8021f23f4d4e9 100644 --- a/crates/bevy_ui/src/val.rs +++ b/crates/bevy_ui/src/val.rs @@ -1000,21 +1000,26 @@ impl From for Val { } } -/// Discriminator enum, should match `Val`'s unit types. +/// Discriminator enum, used for packing. Should match `Val`'s unit types. #[derive(Clone, Copy, PartialEq, Eq, Debug, Reflect)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[repr(u8)] pub enum ValUnit { - /// Use `Px` units + /// Use `Val::Px` units. Px, - /// Use `Percent` units + /// Use `Val::Percent` units. Percent, - /// Use `Vw` units + /// Use `Val::Vw` units. Vw, + /// Use `Val::Vh` units. Vh, + /// Use `Val::VMin` units. VMin, + /// Use `Val::VMax` units. VMax, + /// Use `Val::Em` units. Em, + /// Use `Val::Rem` units. Rem, } @@ -1034,6 +1039,22 @@ impl ValUnit { const fn bit(self) -> u8 { 1 << self as u8 } + + /// Resolve a `ValUnit` and a length to a value in physical pixels from the given local physical percentage basis + /// and context values. + #[inline] + pub fn resolve(self, value: f32, basis: f32, context: &ValContext) -> Val { + match self { + ValUnit::Percent => Val::Percent(value * basis), + ValUnit::Px => Val::Px(value * context.scale_factor), + ValUnit::Vw => Val::Px(context.physical_size.x * value / 100.0), + ValUnit::Vh => Val::Px(context.physical_size.y * value / 100.0), + ValUnit::VMin => Val::Px(context.physical_size.min_element() * value / 100.0), + ValUnit::VMax => Val::Px(context.physical_size.max_element() * value / 100.0), + ValUnit::Em => Val::Px(value * context.em_size * context.scale_factor), + ValUnit::Rem => Val::Px(value * context.rem_size * context.scale_factor), + } + } } /// If more unit types are added to `Val`, a `u8` discriminator won't be sufficient. From 3b74bdc3b1f3e9e76ac58514d2555fe852ab1088 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 2 Nov 2026 13:11:54 +0000 Subject: [PATCH 30/50] Renamings for clarity: `Terms` to `Operands`, `TermOp` to `ReduceUp`. More doc comments and cleanup. --- crates/bevy_ui/src/layout/convert.rs | 50 ++++++----- crates/bevy_ui/src/val.rs | 123 ++++++++++++++------------- 2 files changed, 92 insertions(+), 81 deletions(-) diff --git a/crates/bevy_ui/src/layout/convert.rs b/crates/bevy_ui/src/layout/convert.rs index 193e98cf115c4..e587313432383 100644 --- a/crates/bevy_ui/src/layout/convert.rs +++ b/crates/bevy_ui/src/layout/convert.rs @@ -1,5 +1,4 @@ use bevy_platform::sync::Arc; -use bevy_text::{EmSize, RemSize}; use taffy::style_helpers; use crate::{ @@ -19,17 +18,18 @@ impl Val { /// /// Rebuilding through the `Val` operators folds the resolved terms. Most expressions /// collapse to a single `Px` or a `Px` and `Percent` sum. - fn to_physical(&self, context: &ValContext) -> Val { - let resolve_leaf_val = |(unit, value): (ValUnit, f32)| unit.resolve(value, 1., context); + fn to_physical_except_percent(&self, context: &ValContext) -> Val { + let resolve_leaf_val = + |(unit, value): (ValUnit, f32)| unit.resolve_physical_except_percent(value, context); match self { Val::Auto => Val::Auto, Val::Min(..) => self - .terms() + .operands() .map(resolve_leaf_val) .reduce(Val::min) .unwrap_or(Val::ZERO), Val::Max(..) => self - .terms() + .operands() .map(resolve_leaf_val) .reduce(Val::max) .unwrap_or(Val::ZERO), @@ -38,16 +38,25 @@ impl Val { resolve_leaf_val((units[2], values[2])), ), Val::Calc(calc) => match &*calc.0 { - CalcOp::Add(a, b) => a.to_physical(context) + b.to_physical(context), - CalcOp::Sub(a, b) => a.to_physical(context) - b.to_physical(context), - CalcOp::Min(a, b) => a.to_physical(context).min(b.to_physical(context)), - CalcOp::Max(a, b) => a.to_physical(context).max(b.to_physical(context)), - CalcOp::Clamp(min, value, max) => value - .to_physical(context) - .clamp(min.to_physical(context), max.to_physical(context)), + CalcOp::Add(a, b) => { + a.to_physical_except_percent(context) + b.to_physical_except_percent(context) + } + CalcOp::Sub(a, b) => { + a.to_physical_except_percent(context) - b.to_physical_except_percent(context) + } + CalcOp::Min(a, b) => a + .to_physical_except_percent(context) + .min(b.to_physical_except_percent(context)), + CalcOp::Max(a, b) => a + .to_physical_except_percent(context) + .max(b.to_physical_except_percent(context)), + CalcOp::Clamp(min, value, max) => value.to_physical_except_percent(context).clamp( + min.to_physical_except_percent(context), + max.to_physical_except_percent(context), + ), }, _ => self - .terms() + .operands() .map(resolve_leaf_val) .reduce(|a, b| a + b) .unwrap_or(Val::ZERO), @@ -65,7 +74,7 @@ impl Val { match self { Val::Auto => style_helpers::auto(), Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Clamp(..) | Val::Calc(_) => { - let physical = self.to_physical(context); + let physical = self.to_physical_except_percent(context); if !physical.uses_percent() { return style_helpers::length(PercentageCalc(physical).resolve(0.0)); } @@ -104,7 +113,7 @@ impl Val { match self { Val::Auto => style_helpers::length(0.0_f32), Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Clamp(..) | Val::Calc(_) => { - let physical = self.to_physical(context); + let physical = self.to_physical_except_percent(context); if !physical.uses_percent() { return style_helpers::length(PercentageCalc(physical).resolve(0.0)); } @@ -644,25 +653,26 @@ mod tests { let value = Val::Percent(50.0) - Val::Px(10.0) + Val::Em(2.0) + Val::Rem(1.0) + Val::Vw(1.0); assert_eq!( - value.to_physical(&context), + value.to_physical_except_percent(&context), Val::Percent(50.0) + Val::Px(78.0) ); assert_eq!( - (Val::Px(1.0) + Val::Em(1.0)).to_physical(&context), + (Val::Px(1.0) + Val::Em(1.0)).to_physical_except_percent(&context), Val::Px(26.0) ); assert_eq!( Val::Percent(80.0) .clamp(Val::Px(100.0), Val::Px(300.0)) - .to_physical(&context), + .to_physical_except_percent(&context), Val::Percent(80.0).clamp(Val::Px(200.0), Val::Px(600.0)) ); let nested: Val = CalcOp::Min(Val::Em(1.0) + Val::Percent(10.0), Val::Rem(1.0)).into(); - let physical = nested.to_physical(&context); + let physical = nested.to_physical_except_percent(&context); assert!(matches!(physical, Val::Calc(_)), "got {physical:?}"); assert_eq!(PercentageCalc(physical).resolve(100.0), 34.0); assert_eq!( - PercentageCalc((Val::Px(1.0) + Val::Vh(10.0)).to_physical(&context)).resolve(0.0), + PercentageCalc((Val::Px(1.0) + Val::Vh(10.0)).to_physical_except_percent(&context)) + .resolve(0.0), 52.0 ); } diff --git a/crates/bevy_ui/src/val.rs b/crates/bevy_ui/src/val.rs index 8021f23f4d4e9..02c8309fe2bb0 100644 --- a/crates/bevy_ui/src/val.rs +++ b/crates/bevy_ui/src/val.rs @@ -212,7 +212,7 @@ impl PartialEq for Val { match (self, other) { (Self::Calc(a), Self::Calc(b)) => return a == b, (Self::Min(a, x), Self::Min(b, y)) | (Self::Max(a, x), Self::Max(b, y)) => { - return ValTerms::new(*a, *x) == ValTerms::new(*b, *y); + return ValOperands::new(*a, *x) == ValOperands::new(*b, *y); } (Self::Clamp(a, x), Self::Clamp(b, y)) => return a == b && x == y, (Self::Sum(..), _) | (_, Self::Sum(..)) => { @@ -517,12 +517,12 @@ impl Val { /// The smaller of `self` and `other` once resolved. pub fn min(self, other: Val) -> Val { - self.combine(other, TermOp::Min) + self.combine(other, ReduceOp::Min) } /// The larger of `self` and `other` once resolved. pub fn max(self, other: Val) -> Val { - self.combine(other, TermOp::Max) + self.combine(other, ReduceOp::Max) } /// `self` clamped between `min` and `max` once resolved. @@ -541,7 +541,7 @@ impl Add for Val { type Output = Self; fn add(self, rhs: Self) -> Self { - self.combine(rhs, TermOp::Sum) + self.combine(rhs, ReduceOp::Sum) } } @@ -549,7 +549,7 @@ impl Sub for Val { type Output = Self; fn sub(self, rhs: Self) -> Self { - self.combine(-rhs, TermOp::Sum) + self.combine(-rhs, ReduceOp::Sum) } } @@ -705,11 +705,11 @@ impl Val { Val::Auto => Err(ValArithmeticError::NonEvaluable), Val::Sum(units, values) | Val::Min(units, values) | Val::Max(units, values) => { let op = match self { - Val::Min(..) => TermOp::Min, - Val::Max(..) => TermOp::Max, - _ => TermOp::Sum, + Val::Min(..) => ReduceOp::Min, + Val::Max(..) => ReduceOp::Max, + _ => ReduceOp::Sum, }; - Ok(ValTerms::new(*units, *values).resolve( + Ok(ValOperands::new(*units, *values).resolve_to_physical( op.apply(), scale_factor, physical_base_value, @@ -787,8 +787,8 @@ impl TryStableInterpolate for Val { (Val::Sum(a, x), Val::Sum(b, y)) | (Val::Min(a, x), Val::Min(b, y)) | (Val::Max(a, x), Val::Max(b, y)) => { - let (units, values) = ValTerms::new(*a, *x) - .interpolate_stable(&ValTerms::new(*b, *y), t) + let (units, values) = ValOperands::new(*a, *x) + .interpolate_stable(&ValOperands::new(*b, *y), t) .ok_or(MismatchedUnitsError)? .into_parts(); Ok(match self { @@ -1040,12 +1040,12 @@ impl ValUnit { 1 << self as u8 } - /// Resolve a `ValUnit` and a length to a value in physical pixels from the given local physical percentage basis - /// and context values. + /// Resolve a `ValUnit` and `f32` length to physical coords except for percentage values. + /// `Percentage` values depend on the size of the parent, and can't be resolved except during layout. #[inline] - pub fn resolve(self, value: f32, basis: f32, context: &ValContext) -> Val { + pub fn resolve_physical_except_percent(self, value: f32, context: &ValContext) -> Val { match self { - ValUnit::Percent => Val::Percent(value * basis), + ValUnit::Percent => Val::Percent(value), ValUnit::Px => Val::Px(value * context.scale_factor), ValUnit::Vw => Val::Px(context.physical_size.x * value / 100.0), ValUnit::Vh => Val::Px(context.physical_size.y * value / 100.0), @@ -1116,15 +1116,16 @@ impl core::fmt::Debug for ValUnitSet { } } -/// Working copy of the terms of a [`Val::Sum`], [`Val::Min`] or [`Val::Max`]. +/// Working copy of the operands of a `Sum`, `Min` or `Max` operation. +/// Operations are automatically reduced, so each unit type can only occur once. #[derive(Clone, Copy)] -pub(crate) struct ValTerms { +pub(crate) struct ValOperands { units: ValUnitSet, values: [f32; 3], } -impl ValTerms { - /// The maximum number of terms. +impl ValOperands { + /// The maximum number of operands. pub(crate) const CAPACITY: usize = 3; pub(crate) const fn new(units: ValUnitSet, values: [f32; 3]) -> Self { @@ -1141,8 +1142,8 @@ impl ValTerms { Self::new(units, [value, 0.0, 0.0]) } - /// The terms in ascending unit order. - pub(crate) fn terms(self) -> impl Iterator { + /// The operands in ascending unit order. + pub(crate) fn iter(self) -> impl Iterator { self.units.iter().zip(self.values) } @@ -1166,14 +1167,14 @@ impl ValTerms { /// Removes the terms whose value is zero. fn retain_nonzero(&mut self) { let mut kept = Self::new(ValUnitSet::EMPTY, [0.0; 3]); - for (unit, value) in self.terms().filter(|(_, value)| *value != 0.0) { + for (unit, value) in self.iter().filter(|(_, value)| *value != 0.0) { kept.values[kept.units.len()] = value; kept.units.insert(unit); } *self = kept; } - pub(crate) fn resolve( + pub(crate) fn resolve_to_physical( &self, op: fn(f32, f32) -> f32, scale_factor: f32, @@ -1182,7 +1183,7 @@ impl ValTerms { em_size: EmSize, rem_size: RemSize, ) -> f32 { - self.terms() + self.iter() .map(|(unit, value)| { Val::leaf(unit, value) .resolve( @@ -1198,7 +1199,7 @@ impl ValTerms { .unwrap_or(0.0) } - /// Interpolates term-wise, or `None` when the units differ. + /// Interpolates operand-wise, or `None` when the units differ. pub(crate) fn interpolate_stable(&self, other: &Self, t: f32) -> Option { (self.units == other.units).then(|| { let mut result = *self; @@ -1210,21 +1211,21 @@ impl ValTerms { } } -impl PartialEq for ValTerms { +impl PartialEq for ValOperands { fn eq(&self, other: &Self) -> bool { - self.units == other.units && self.terms().eq(other.terms()) + self.units == other.units && self.iter().eq(other.iter()) } } -/// The operation a [`Val::Sum`], [`Val::Min`] or [`Val::Max`] applies to its terms. +/// The operation a [`Val::Sum`], [`Val::Min`] or [`Val::Max`] applies to its operands. #[derive(Clone, Copy)] -pub(crate) enum TermOp { +pub(crate) enum ReduceOp { Sum, Min, Max, } -impl TermOp { +impl ReduceOp { pub(crate) fn apply(self) -> fn(f32, f32) -> f32 { match self { Self::Sum => |a, b| a + b, @@ -1233,7 +1234,7 @@ impl TermOp { } } - /// The operation that gives the same result after every term is negated. + /// The operation that gives the same result after every operand is negated. const fn reversed(self) -> Self { match self { Self::Sum => Self::Sum, @@ -1242,17 +1243,17 @@ impl TermOp { } } - /// The [`Val`] for `terms` under this operation, as a single unit value when there is one term. - fn wrap(self, terms: ValTerms) -> Val { - if terms.units.len() == 1 - && let Some((unit, value)) = terms.terms().next() + /// The [`Val`] for `operands` under this operation, as a single unit value when there is one operand. + fn wrap(self, operands: ValOperands) -> Val { + if operands.units.len() == 1 + && let Some((unit, value)) = operands.iter().next() { return Val::leaf(unit, value); } match self { - Self::Sum => Val::Sum(terms.units, terms.values), - Self::Min => Val::Min(terms.units, terms.values), - Self::Max => Val::Max(terms.units, terms.values), + Self::Sum => Val::Sum(operands.units, operands.values), + Self::Min => Val::Min(operands.units, operands.values), + Self::Max => Val::Max(operands.units, operands.values), } } @@ -1267,27 +1268,27 @@ impl TermOp { } impl Val { - /// The terms of a [`Val::Sum`], [`Val::Min`] or [`Val::Max`], or the single term of a + /// The operands of a [`Val::Sum`], [`Val::Min`] or [`Val::Max`], or the single operand of a /// unit value. Empty for [`Val::Auto`], [`Val::Clamp`] and [`Val::Calc`]. - pub fn terms(&self) -> impl Iterator { - let terms = match self { + pub fn operands(&self) -> impl Iterator { + let operands = match self { Val::Sum(units, values) | Val::Min(units, values) | Val::Max(units, values) => { - ValTerms::new(*units, *values) + ValOperands::new(*units, *values) } _ => match (self.unit(), self.inner()) { - (Some(unit), Some(value)) => ValTerms::single(unit, value), - _ => ValTerms::new(ValUnitSet::EMPTY, [0.0; 3]), + (Some(unit), Some(value)) => ValOperands::single(unit, value), + _ => ValOperands::new(ValUnitSet::EMPTY, [0.0; 3]), }, }; - terms.terms() + operands.iter() } /// Combines two values with `op`, folding them into one set of terms when both sides are /// unit values or terms of the same operation, and building a [`Calc`] otherwise. - pub(crate) fn combine(self, rhs: Val, op: TermOp) -> Val { - if let (Some(mut terms), Some(rhs_terms)) = (self.terms_for(op), rhs.terms_for(op)) + pub(crate) fn combine(self, rhs: Val, op: ReduceOp) -> Val { + if let (Some(mut terms), Some(rhs_terms)) = (self.operands_for(op), rhs.operands_for(op)) && rhs_terms - .terms() + .iter() .all(|(unit, value)| terms.fold(unit, value, op.apply())) { return op.wrap(terms); @@ -1295,21 +1296,21 @@ impl Val { op.calc(self, rhs) } - fn terms_for(&self, op: TermOp) -> Option { + fn operands_for(&self, op: ReduceOp) -> Option { match (self, op) { - (Val::Sum(units, values), TermOp::Sum) - | (Val::Min(units, values), TermOp::Min) - | (Val::Max(units, values), TermOp::Max) => Some(ValTerms::new(*units, *values)), - _ => Some(ValTerms::single(self.unit()?, self.inner()?)), + (Val::Sum(units, values), ReduceOp::Sum) + | (Val::Min(units, values), ReduceOp::Min) + | (Val::Max(units, values), ReduceOp::Max) => Some(ValOperands::new(*units, *values)), + _ => Some(ValOperands::single(self.unit()?, self.inner()?)), } } /// The terms of a [`Val::Sum`] or unit value with zero terms removed, so that sums /// compare equal whatever zero terms they carry. `None` for other values. - pub(crate) fn sum_terms(&self) -> Option { + pub(crate) fn sum_terms(&self) -> Option { let mut terms = match self { - Val::Sum(units, values) => ValTerms::new(*units, *values), - _ => ValTerms::single(self.unit()?, self.inner()?), + Val::Sum(units, values) => ValOperands::new(*units, *values), + _ => ValOperands::single(self.unit()?, self.inner()?), }; terms.retain_nonzero(); Some(terms) @@ -1357,9 +1358,9 @@ impl Val { /// `reverses` swaps `Min` and `Max` and the bounds of `Clamp`, for an `f` that reverses ordering. pub(crate) fn map_terms(self, f: impl Fn(f32) -> f32, reverses: bool) -> Self { let (op, units, mut values) = match self { - Val::Sum(units, values) => (TermOp::Sum, units, values), - Val::Min(units, values) => (TermOp::Min, units, values), - Val::Max(units, values) => (TermOp::Max, units, values), + Val::Sum(units, values) => (ReduceOp::Sum, units, values), + Val::Min(units, values) => (ReduceOp::Min, units, values), + Val::Max(units, values) => (ReduceOp::Max, units, values), Val::Clamp(units, values) => { // Inverted same-unit bounds make the clamp its minimum whatever the value is. if units[0] == units[2] && values[0] > values[2] { @@ -1388,7 +1389,7 @@ impl Val { *value = f(*value); } let op = if reverses { op.reversed() } else { op }; - op.wrap(ValTerms::new(units, values)) + op.wrap(ValOperands::new(units, values)) } } @@ -1678,7 +1679,7 @@ mod tests { let sum = Val::Px(10.0) + Val::Percent(50.0); assert!(matches!(sum, Val::Sum(..)), "got {sum:?}"); assert_eq!( - sum.terms().collect::>(), + sum.operands().collect::>(), [(ValUnit::Px, 10.0), (ValUnit::Percent, 50.0)] ); assert_eq!(Val::Percent(50.0) + Val::Px(10.0), sum); From 0a1054c8fac03cb1368fdd9d7fb0305b140a162c Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 2 Nov 2026 13:25:41 +0000 Subject: [PATCH 31/50] Added release note stub --- _release-content/release-notes/val_calc.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 _release-content/release-notes/val_calc.md diff --git a/_release-content/release-notes/val_calc.md b/_release-content/release-notes/val_calc.md new file mode 100644 index 0000000000000..a365bd53becc9 --- /dev/null +++ b/_release-content/release-notes/val_calc.md @@ -0,0 +1,5 @@ +--- +title: "Val calc" +authors: ["@ickshonope"] +pull_requests: [] +--- \ No newline at end of file From b197e1407ce952e38993251dd84b8050963423e3 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 2 Nov 2026 13:26:22 +0000 Subject: [PATCH 32/50] Internal renaming. --- crates/bevy_ui/src/val.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/bevy_ui/src/val.rs b/crates/bevy_ui/src/val.rs index 02c8309fe2bb0..9667f383aa57f 100644 --- a/crates/bevy_ui/src/val.rs +++ b/crates/bevy_ui/src/val.rs @@ -216,7 +216,7 @@ impl PartialEq for Val { } (Self::Clamp(a, x), Self::Clamp(b, y)) => return a == b && x == y, (Self::Sum(..), _) | (_, Self::Sum(..)) => { - return self.sum_terms() == other.sum_terms(); + return self.sum_operands() == other.sum_operands(); } _ => {} } @@ -568,7 +568,7 @@ impl Mul for Val { Val::Em(value) => Val::Em(value * rhs), Val::Rem(value) => Val::Rem(value * rhs), terms @ (Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Clamp(..)) => { - terms.map_terms(|value| value * rhs, rhs < 0.0) + terms.map_operands(|value| value * rhs, rhs < 0.0) } Val::Calc(mut calc) => { Arc::make_mut(&mut calc.0).scale(rhs); @@ -591,7 +591,7 @@ impl MulAssign for Val { | Val::Em(value) | Val::Rem(value) => *value *= rhs, Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Clamp(..) => { - *self = core::mem::take(self).map_terms(|value| value * rhs, rhs < 0.0); + *self = core::mem::take(self).map_operands(|value| value * rhs, rhs < 0.0); } Val::Calc(calc) => Arc::make_mut(&mut calc.0).scale(rhs), } @@ -613,7 +613,7 @@ impl Div for Val { Val::Em(value) => Val::Em(value / rhs), Val::Rem(value) => Val::Rem(value / rhs), terms @ (Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Clamp(..)) => { - terms.map_terms(|value| value / rhs, rhs < 0.0) + terms.map_operands(|value| value / rhs, rhs < 0.0) } Val::Calc(mut calc) => { Arc::make_mut(&mut calc.0).scale(rhs.recip()); @@ -636,7 +636,7 @@ impl DivAssign for Val { | Val::Em(value) | Val::Rem(value) => *value /= rhs, Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Clamp(..) => { - *self = core::mem::take(self).map_terms(|value| value / rhs, rhs < 0.0); + *self = core::mem::take(self).map_operands(|value| value / rhs, rhs < 0.0); } Val::Calc(calc) => Arc::make_mut(&mut calc.0).scale(rhs.recip()), } @@ -658,7 +658,7 @@ impl Neg for Val { Val::Em(value) => Val::Em(-value), Val::Rem(value) => Val::Rem(-value), terms @ (Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Clamp(..)) => { - terms.map_terms(|value| -value, true) + terms.map_operands(|value| -value, true) } Val::Calc(mut calc) => { Arc::make_mut(&mut calc.0).scale(-1.0); @@ -1307,7 +1307,7 @@ impl Val { /// The terms of a [`Val::Sum`] or unit value with zero terms removed, so that sums /// compare equal whatever zero terms they carry. `None` for other values. - pub(crate) fn sum_terms(&self) -> Option { + pub(crate) fn sum_operands(&self) -> Option { let mut terms = match self { Val::Sum(units, values) => ValOperands::new(*units, *values), _ => ValOperands::single(self.unit()?, self.inner()?), @@ -1356,7 +1356,7 @@ impl Val { /// Applies `f` to every term of a [`Val::Sum`], [`Val::Min`], [`Val::Max`] or [`Val::Clamp`]. /// `reverses` swaps `Min` and `Max` and the bounds of `Clamp`, for an `f` that reverses ordering. - pub(crate) fn map_terms(self, f: impl Fn(f32) -> f32, reverses: bool) -> Self { + pub(crate) fn map_operands(self, f: impl Fn(f32) -> f32, reverses: bool) -> Self { let (op, units, mut values) = match self { Val::Sum(units, values) => (ReduceOp::Sum, units, values), Val::Min(units, values) => (ReduceOp::Min, units, values), From 2d5575537c8dec1c837a34b3bc31696dcc4b9e57 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 2 Nov 2026 13:47:50 +0000 Subject: [PATCH 33/50] Prove MeasureArgs cannot be built outside the crate Adds a compile_fail doctest so the safety argument in resolve_calc, which relies on every MeasureArgs coming from the layout system, is checked rather than asserted. Co-Authored-By: Claude Fable 5.1 --- crates/bevy_ui/src/measurement.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/bevy_ui/src/measurement.rs b/crates/bevy_ui/src/measurement.rs index 0280f31b64fc2..dd2b5795a491a 100644 --- a/crates/bevy_ui/src/measurement.rs +++ b/crates/bevy_ui/src/measurement.rs @@ -21,6 +21,14 @@ impl core::fmt::Debug for ContentSize { /// Inputs provided to [`Measure::measure`]. /// /// Only the layout system builds these, which keeps the calc pointers in `style` valid. +/// Other crates cannot construct one, even from an existing value: +/// +/// ```compile_fail,E0639 +/// # use bevy_ui::MeasureArgs; +/// fn rebuild(args: MeasureArgs) -> MeasureArgs { +/// MeasureArgs { ..args } +/// } +/// ``` #[non_exhaustive] pub struct MeasureArgs<'a> { /// The known width from Taffy if this size is definite. From dc27709cc9dd2cd9c796f071c3328fa7bb1de2db Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 2 Nov 2026 14:00:38 +0000 Subject: [PATCH 34/50] Val resolve now has a ValContext parameter in place of the individual fields for scale factor, target size, em size and rem size. --- crates/bevy_ui/src/geometry.rs | 36 +--- crates/bevy_ui/src/gradients.rs | 35 +--- crates/bevy_ui/src/layout/layout_tree.rs | 7 +- crates/bevy_ui/src/layout/mod.rs | 22 +-- crates/bevy_ui/src/ui_transform.rs | 10 +- crates/bevy_ui/src/val.rs | 233 +++++++++++++---------- crates/bevy_ui_render/src/box_shadow.rs | 12 +- crates/bevy_ui_render/src/gradient.rs | 10 +- crates/bevy_ui_widgets/src/scrollbar.rs | 18 +- 9 files changed, 190 insertions(+), 193 deletions(-) diff --git a/crates/bevy_ui/src/geometry.rs b/crates/bevy_ui/src/geometry.rs index df25cb492b424..4c41643b86bf5 100644 --- a/crates/bevy_ui/src/geometry.rs +++ b/crates/bevy_ui/src/geometry.rs @@ -567,28 +567,13 @@ impl UiPosition { em_size: EmSize, rem_size: RemSize, ) -> Vec2 { + let context = ValContext::new(scale_factor, physical_target_size, em_size, rem_size); let d = self.anchor.map(|p| if 0. < p { -1. } else { 1. }); physical_size * self.anchor + d * Vec2::new( - self.x - .resolve( - scale_factor, - physical_size.x, - physical_target_size, - em_size, - rem_size, - ) - .unwrap_or(0.), - self.y - .resolve( - scale_factor, - physical_size.y, - physical_target_size, - em_size, - rem_size, - ) - .unwrap_or(0.), + self.x.resolve(physical_size.x, &context).unwrap_or(0.), + self.y.resolve(physical_size.y, &context).unwrap_or(0.), ) } } @@ -765,6 +750,7 @@ impl CornerRadius { em_size: EmSize, rem_size: RemSize, ) -> Vec2 { + let context = ValContext::new(scale_factor, viewport_size, em_size, rem_size); match self { Self { x: Val::Auto, @@ -779,21 +765,13 @@ impl CornerRadius { y: radius, } => Vec2::splat( radius - .resolve( - scale_factor, - size.min_element(), - viewport_size, - em_size, - rem_size, - ) + .resolve(size.min_element(), &context) .unwrap_or(0.) .clamp(0., 0.5 * size.min_element()), ), Self { x, y } => Vec2::new( - x.resolve(scale_factor, size.x, viewport_size, em_size, rem_size) - .unwrap_or(0.), - y.resolve(scale_factor, size.y, viewport_size, em_size, rem_size) - .unwrap_or(0.), + x.resolve(size.x, &context).unwrap_or(0.), + y.resolve(size.y, &context).unwrap_or(0.), ) .clamp(Vec2::ZERO, 0.5 * size), } diff --git a/crates/bevy_ui/src/gradients.rs b/crates/bevy_ui/src/gradients.rs index 9384cf91a0873..bfbaf04c7449b 100644 --- a/crates/bevy_ui/src/gradients.rs +++ b/crates/bevy_ui/src/gradients.rs @@ -1,4 +1,4 @@ -use crate::{UiPosition, Val, ValNum}; +use crate::{UiPosition, Val, ValContext, ValNum}; use bevy_color::{Color, Srgba}; use bevy_ecs::{component::Component, reflect::ReflectComponent}; use bevy_math::Vec2; @@ -615,6 +615,7 @@ impl RadialGradientShape { em_size: EmSize, rem_size: RemSize, ) -> Vec2 { + let context = ValContext::new(scale_factor, physical_target_size, em_size, rem_size); let half_size = 0.5 * physical_size; match self { RadialGradientShape::ClosestSide => Vec2::splat(close_side2(position, half_size)), @@ -627,34 +628,12 @@ impl RadialGradientShape { far_side(position.x, half_size.x), far_side(position.y, half_size.y), ), - RadialGradientShape::Circle(radius) => Vec2::splat( - radius - .resolve( - scale_factor, - physical_size.x, - physical_target_size, - em_size, - rem_size, - ) - .unwrap_or(0.), - ), + RadialGradientShape::Circle(radius) => { + Vec2::splat(radius.resolve(physical_size.x, &context).unwrap_or(0.)) + } RadialGradientShape::Ellipse(x, y) => Vec2::new( - x.resolve( - scale_factor, - physical_size.x, - physical_target_size, - em_size, - rem_size, - ) - .unwrap_or(0.), - y.resolve( - scale_factor, - physical_size.y, - physical_target_size, - em_size, - rem_size, - ) - .unwrap_or(0.), + x.resolve(physical_size.x, &context).unwrap_or(0.), + y.resolve(physical_size.y, &context).unwrap_or(0.), ), } } diff --git a/crates/bevy_ui/src/layout/layout_tree.rs b/crates/bevy_ui/src/layout/layout_tree.rs index 4d5bcc8f9e56a..c33bf7b962ab8 100644 --- a/crates/bevy_ui/src/layout/layout_tree.rs +++ b/crates/bevy_ui/src/layout/layout_tree.rs @@ -25,7 +25,7 @@ use taffy::{ use crate::{ layout::UiTreeDirty, ContentSize, FixedNode, GhostNode, IgnoreScroll, LayoutConfig, LayoutError, Measure, MeasureArgs, Node, NodeMeasure, Outline, OverrideClip, ScrollPosition, - UiTransform, Val, + UiTransform, Val, ValContext, }; #[expect( @@ -63,7 +63,10 @@ pub(crate) struct PercentageCalc(pub Val); impl PercentageCalc { pub fn resolve(&self, basis: f32) -> f32 { self.0 - .resolve(1.0, basis, Vec2::ZERO, EmSize(0.0), RemSize(0.0)) + .resolve( + basis, + &ValContext::new(1.0, Vec2::ZERO, EmSize(0.0), RemSize(0.0)), + ) .unwrap_or(0.0) } } diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index ac2001f53338e..6ea317b422ebd 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -687,17 +687,17 @@ fn update_uinode_geometry_recursive( } if let Some(outline) = maybe_outline { + let context = ValContext::new( + inverse_target_scale_factor.recip(), + target_size, + *em_size, + rem_size, + ); // don't trigger change detection unless the outline actually changed let new_outline_width = if style.display != Display::None { outline .width - .resolve( - inverse_target_scale_factor.recip(), - computed_node.size().x, - target_size, - *em_size, - rem_size, - ) + .resolve(computed_node.size().x, &context) .unwrap_or(0.) .max(0.) } else { @@ -710,13 +710,7 @@ fn update_uinode_geometry_recursive( let new_outline_offset = outline .offset - .resolve( - inverse_target_scale_factor.recip(), - computed_node.size().x, - target_size, - *em_size, - rem_size, - ) + .resolve(computed_node.size().x, &context) .unwrap_or(0.) // Clamp outline offsets to at least the length of the node's shorter side // Negative offset outlines can be useful to create thing like in-set focus indicators diff --git a/crates/bevy_ui/src/ui_transform.rs b/crates/bevy_ui/src/ui_transform.rs index 0c9775137e0fb..550c6634ebe39 100644 --- a/crates/bevy_ui/src/ui_transform.rs +++ b/crates/bevy_ui/src/ui_transform.rs @@ -1,5 +1,6 @@ use crate::Val; use crate::ValArithmeticError; +use crate::ValContext; use crate::ValNum; use bevy_derive::Deref; use bevy_ecs::component::Component; @@ -69,13 +70,10 @@ impl Val2 { em_size: EmSize, rem_size: RemSize, ) -> Vec2 { + let context = ValContext::new(scale_factor, viewport_size, em_size, rem_size); Vec2::new( - self.x - .resolve(scale_factor, base_size.x, viewport_size, em_size, rem_size) - .unwrap_or(0.), - self.y - .resolve(scale_factor, base_size.y, viewport_size, em_size, rem_size) - .unwrap_or(0.), + self.x.resolve(base_size.x, &context).unwrap_or(0.), + self.y.resolve(base_size.y, &context).unwrap_or(0.), ) } diff --git a/crates/bevy_ui/src/val.rs b/crates/bevy_ui/src/val.rs index 9667f383aa57f..d3a4d55126b67 100644 --- a/crates/bevy_ui/src/val.rs +++ b/crates/bevy_ui/src/val.rs @@ -677,31 +677,27 @@ pub enum ValArithmeticError { } impl Val { - /// Resolves this [`Val`] to a value in physical pixels from the given `scale_factor`, `physical_base_value`, - /// `physical_target_size`, `em_size`, and `rem_size` context values. + /// Resolves this [`Val`] to a value in physical pixels from the given `physical_base_value` and `context`. /// /// Returns a [`ValArithmeticError::NonEvaluable`] if the [`Val`] is impossible to resolve into a concrete value. pub fn resolve( &self, - scale_factor: f32, physical_base_value: f32, - physical_target_size: Vec2, - em_size: EmSize, - rem_size: RemSize, + context: &ValContext, ) -> Result { match self { Val::Percent(value) => Ok(physical_base_value * value / 100.0), - Val::Px(value) => Ok(value * scale_factor), - Val::Vw(value) => Ok(physical_target_size.x * value / 100.0), - Val::Vh(value) => Ok(physical_target_size.y * value / 100.0), + Val::Px(value) => Ok(value * context.scale_factor), + Val::Vw(value) => Ok(context.physical_size.x * value / 100.0), + Val::Vh(value) => Ok(context.physical_size.y * value / 100.0), Val::VMin(value) => { - Ok(physical_target_size.x.min(physical_target_size.y) * value / 100.0) + Ok(context.physical_size.x.min(context.physical_size.y) * value / 100.0) } Val::VMax(value) => { - Ok(physical_target_size.x.max(physical_target_size.y) * value / 100.0) + Ok(context.physical_size.x.max(context.physical_size.y) * value / 100.0) } - Val::Em(value) => Ok(value * em_size.0 * scale_factor), - Val::Rem(value) => Ok(value * rem_size.0 * scale_factor), + Val::Em(value) => Ok(value * context.em_size * context.scale_factor), + Val::Rem(value) => Ok(value * context.rem_size * context.scale_factor), Val::Auto => Err(ValArithmeticError::NonEvaluable), Val::Sum(units, values) | Val::Min(units, values) | Val::Max(units, values) => { let op = match self { @@ -711,28 +707,22 @@ impl Val { }; Ok(ValOperands::new(*units, *values).resolve_to_physical( op.apply(), - scale_factor, physical_base_value, - physical_target_size, - em_size, - rem_size, + context, )) } Val::Clamp(units, values) => Ok(Val::resolve_clamp( *units, *values, - scale_factor, physical_base_value, - physical_target_size, - em_size, - rem_size, + context, )), Val::Calc(op) => op.0.resolve( - scale_factor, + context.scale_factor, physical_base_value, - physical_target_size, - em_size, - rem_size, + context.physical_size, + EmSize(context.em_size), + RemSize(context.rem_size), ), } } @@ -944,6 +934,7 @@ impl CalcOp { } } + /// Resolve a `CalcOp` to a value in physical pixels. pub fn resolve( &self, scale_factor: f32, @@ -952,15 +943,8 @@ impl CalcOp { em_size: EmSize, rem_size: RemSize, ) -> Result { - let resolve = |val: &Val| { - val.resolve( - scale_factor, - physical_base_value, - physical_target_size, - em_size, - rem_size, - ) - }; + let context = ValContext::new(scale_factor, physical_target_size, em_size, rem_size); + let resolve = |val: &Val| val.resolve(physical_base_value, &context); Ok(match self { Self::Add(a, b) => resolve(a)? + resolve(b)?, Self::Sub(a, b) => resolve(a)? - resolve(b)?, @@ -1177,22 +1161,13 @@ impl ValOperands { pub(crate) fn resolve_to_physical( &self, op: fn(f32, f32) -> f32, - scale_factor: f32, physical_base_value: f32, - physical_target_size: Vec2, - em_size: EmSize, - rem_size: RemSize, + context: &ValContext, ) -> f32 { self.iter() .map(|(unit, value)| { Val::leaf(unit, value) - .resolve( - scale_factor, - physical_base_value, - physical_target_size, - em_size, - rem_size, - ) + .resolve(physical_base_value, context) .unwrap_or(0.0) }) .reduce(op) @@ -1334,21 +1309,12 @@ impl Val { pub(crate) fn resolve_clamp( units: [ValUnit; 3], values: [f32; 3], - scale_factor: f32, physical_base_value: f32, - physical_target_size: Vec2, - em_size: EmSize, - rem_size: RemSize, + context: &ValContext, ) -> f32 { let [lo, v, hi] = core::array::from_fn(|i| { Val::leaf(units[i], values[i]) - .resolve( - scale_factor, - physical_base_value, - physical_target_size, - em_size, - rem_size, - ) + .resolve(physical_base_value, context) .unwrap_or(0.0) }); lo.max(v.min(hi)) @@ -1408,33 +1374,33 @@ mod tests { assert_eq!( Val::Percent(80.) - .resolve(1., size, viewport_size, em_size, rem_size) + .resolve(size, &ValContext::new(1., viewport_size, em_size, rem_size)) .unwrap(), size * 0.8 ); // Check em and rem assert_eq!( Val::Em(2.) - .resolve(1., size, viewport_size, em_size, rem_size) + .resolve(size, &ValContext::new(1., viewport_size, em_size, rem_size)) .unwrap(), em_size.0 * 2. ); assert_eq!( Val::Rem(5.) - .resolve(1., size, viewport_size, em_size, rem_size) + .resolve(size, &ValContext::new(1., viewport_size, em_size, rem_size)) .unwrap(), rem_size.0 * 5. ); // Check scale_factor is considered assert_eq!( Val::Em(2.) - .resolve(2., size, viewport_size, em_size, rem_size) + .resolve(size, &ValContext::new(2., viewport_size, em_size, rem_size)) .unwrap(), em_size.0 * 2. * 2. ); assert_eq!( Val::Rem(5.) - .resolve(2., size, viewport_size, em_size, rem_size) + .resolve(size, &ValContext::new(2., viewport_size, em_size, rem_size)) .unwrap(), rem_size.0 * 5. * 2. ); @@ -1446,25 +1412,40 @@ mod tests { let viewport_size = vec2(1000., 500.); let result = Val::Px(10.) - .resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) + .resolve( + size, + &ValContext::new(1., viewport_size, EmSize(20.0), RemSize(20.0)), + ) .unwrap(); assert_eq!(result, 10.); let result = Val::Px(10.) - .resolve(3., size, viewport_size, EmSize(20.0), RemSize(20.0)) + .resolve( + size, + &ValContext::new(3., viewport_size, EmSize(20.0), RemSize(20.0)), + ) .unwrap(); assert_eq!(result, 30.); let result = Val::Px(10.) - .resolve(0.25, size, viewport_size, EmSize(20.0), RemSize(20.0)) + .resolve( + size, + &ValContext::new(0.25, viewport_size, EmSize(20.0), RemSize(20.0)), + ) .unwrap(); assert_eq!(result, 2.5); let result = Val::Em(1.) - .resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) + .resolve( + size, + &ValContext::new(1., viewport_size, EmSize(20.0), RemSize(20.0)), + ) .unwrap(); assert_eq!(result, 20.0); let result = Val::Rem(1.) - .resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) + .resolve( + size, + &ValContext::new(1., viewport_size, EmSize(20.0), RemSize(20.0)), + ) .unwrap(); assert_eq!(result, 20.0); } @@ -1482,53 +1463,89 @@ mod tests { for value in (-10..10).map(|value| value as f32) { // for a square viewport there should be no difference between `Vw` and `Vh` and between `Vmin` and `Vmax`. assert_eq!( - Val::Vw(value).resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)), - Val::Vh(value).resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) + Val::Vw(value).resolve( + size, + &ValContext::new(1., viewport_size, EmSize(20.0), RemSize(20.0)) + ), + Val::Vh(value).resolve( + size, + &ValContext::new(1., viewport_size, EmSize(20.0), RemSize(20.0)) + ) ); assert_eq!( - Val::VMin(value).resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)), - Val::VMax(value).resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) + Val::VMin(value).resolve( + size, + &ValContext::new(1., viewport_size, EmSize(20.0), RemSize(20.0)) + ), + Val::VMax(value).resolve( + size, + &ValContext::new(1., viewport_size, EmSize(20.0), RemSize(20.0)) + ) ); assert_eq!( - Val::VMin(value).resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)), - Val::Vw(value).resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) + Val::VMin(value).resolve( + size, + &ValContext::new(1., viewport_size, EmSize(20.0), RemSize(20.0)) + ), + Val::Vw(value).resolve( + size, + &ValContext::new(1., viewport_size, EmSize(20.0), RemSize(20.0)) + ) ); } let viewport_size = vec2(1000., 500.); assert_eq!( Val::Vw(100.) - .resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) + .resolve( + size, + &ValContext::new(1., viewport_size, EmSize(20.0), RemSize(20.0)) + ) .unwrap(), 1000. ); assert_eq!( Val::Vh(100.) - .resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) + .resolve( + size, + &ValContext::new(1., viewport_size, EmSize(20.0), RemSize(20.0)) + ) .unwrap(), 500. ); assert_eq!( Val::Vw(60.) - .resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) + .resolve( + size, + &ValContext::new(1., viewport_size, EmSize(20.0), RemSize(20.0)) + ) .unwrap(), 600. ); assert_eq!( Val::Vh(40.) - .resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) + .resolve( + size, + &ValContext::new(1., viewport_size, EmSize(20.0), RemSize(20.0)) + ) .unwrap(), 200. ); assert_eq!( Val::VMin(50.) - .resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) + .resolve( + size, + &ValContext::new(1., viewport_size, EmSize(20.0), RemSize(20.0)) + ) .unwrap(), 250. ); assert_eq!( Val::VMax(75.) - .resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)) + .resolve( + size, + &ValContext::new(1., viewport_size, EmSize(20.0), RemSize(20.0)) + ) .unwrap(), 750. ); @@ -1538,7 +1555,10 @@ mod tests { fn val_auto_is_non_evaluable() { let size = 250.; let viewport_size = vec2(1000., 500.); - let resolve_auto = Val::Auto.resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)); + let resolve_auto = Val::Auto.resolve( + size, + &ValContext::new(1., viewport_size, EmSize(20.0), RemSize(20.0)), + ); assert_eq!(resolve_auto, Err(ValArithmeticError::NonEvaluable)); } @@ -1703,8 +1723,12 @@ mod tests { assert_eq!(Val::Percent(50.0).min(Val::Px(10.0)), min); assert_eq!(-min.clone(), Val::Px(-10.0).max(Val::Percent(-50.0))); assert!(matches!(min.clone().max(Val::Px(5.0)), Val::Calc(_))); - let resolve = - |value: &Val| value.resolve(1.0, 100.0, Vec2::ZERO, EmSize(0.0), RemSize(0.0)); + let resolve = |value: &Val| { + value.resolve( + 100.0, + &ValContext::new(1.0, Vec2::ZERO, EmSize(0.0), RemSize(0.0)), + ) + }; assert_eq!(resolve(&min), Ok(10.0)); assert_eq!(resolve(&Val::Px(10.0).max(Val::Percent(50.0))), Ok(50.0)); assert_eq!(resolve(&(min * -2.0)), Ok(-20.0)); @@ -1715,7 +1739,10 @@ mod tests { assert_eq!(-Val::Px(10.0).max(Val::Px(20.0)), Val::Px(-20.0)); assert_eq!(-Val::Px(10.0).min(Val::Px(20.0)), Val::Px(-10.0)); let resolve = |value: &Val, base: f32| { - value.resolve(1.0, base, Vec2::ZERO, EmSize(0.0), RemSize(0.0)) + value.resolve( + base, + &ValContext::new(1.0, Vec2::ZERO, EmSize(0.0), RemSize(0.0)), + ) }; for expr in [ Val::Px(10.0).max(Val::Percent(50.0)), @@ -1741,7 +1768,10 @@ mod tests { fn negating_a_clamp_negates_its_result() { let clamp = Val::Percent(50.0).clamp(Val::Px(100.0), Val::Percent(20.0)); let resolve = |value: &Val, base: f32| { - value.resolve(1.0, base, Vec2::ZERO, EmSize(0.0), RemSize(0.0)) + value.resolve( + base, + &ValContext::new(1.0, Vec2::ZERO, EmSize(0.0), RemSize(0.0)), + ) }; for base in [50.0, 100.0, 150.0, 300.0, 1000.0] { let expected = resolve(&clamp, base).unwrap(); @@ -1769,7 +1799,10 @@ mod tests { Val::Px(300.0) ); let resolve = |value: &Val, base: f32| { - value.resolve(1.0, base, Vec2::ZERO, EmSize(0.0), RemSize(0.0)) + value.resolve( + base, + &ValContext::new(1.0, Vec2::ZERO, EmSize(0.0), RemSize(0.0)), + ) }; assert_eq!(resolve(&clamp, 100.0), Ok(100.0)); assert_eq!(resolve(&clamp, 250.0), Ok(200.0)); @@ -1815,11 +1848,8 @@ mod tests { Val::Percent(50.0) - Val::Px(10.0) + Val::Em(2.0) + Val::Rem(1.0) + Val::Vw(1.0); let resolve = |value: &Val| { value.resolve( - 2.0, 400.0, - Vec2::new(1000.0, 500.0), - EmSize(12.0), - RemSize(20.0), + &ValContext::new(2.0, Vec2::new(1000.0, 500.0), EmSize(12.0), RemSize(20.0)), ) }; assert_eq!(resolve(&value), Ok(278.0)); @@ -1854,7 +1884,12 @@ mod tests { #[test] fn negative_scale_swaps_min_and_max() { - let resolve = |value: Val| value.resolve(1.0, 0.0, Vec2::ZERO, EmSize(0.0), RemSize(0.0)); + let resolve = |value: Val| { + value.resolve( + 0.0, + &ValContext::new(1.0, Vec2::ZERO, EmSize(0.0), RemSize(0.0)), + ) + }; let (a, b) = (Val::Px(10.0), Val::Px(20.0)); assert_eq!( resolve(-Val::from(CalcOp::Min(a.clone(), b.clone()))), @@ -1937,11 +1972,13 @@ mod tests { let resolve = |value: Val| { value .resolve( - 1.0, 100.0, - Default::default(), - Default::default(), - Default::default(), + &ValContext::new( + 1.0, + Default::default(), + Default::default(), + Default::default(), + ), ) .unwrap() }; @@ -1957,11 +1994,13 @@ mod tests { let resolve = |value: Val| { value .resolve( - 1.0, 100.0, - Default::default(), - Default::default(), - Default::default(), + &ValContext::new( + 1.0, + Default::default(), + Default::default(), + Default::default(), + ), ) .unwrap() }; diff --git a/crates/bevy_ui_render/src/box_shadow.rs b/crates/bevy_ui_render/src/box_shadow.rs index 5ecd979ad0dc3..f380d45fd8e99 100644 --- a/crates/bevy_ui_render/src/box_shadow.rs +++ b/crates/bevy_ui_render/src/box_shadow.rs @@ -29,7 +29,7 @@ use bevy_render::{GpuResourceAppExt, RenderApp, RenderStartup}; use bevy_shader::{Shader, ShaderDefVal}; use bevy_ui::{ BoxShadow, CalculatedClip, ComputedNode, ComputedStackIndex, ComputedUiRenderTargetInfo, - ComputedUiTargetCamera, ResolvedBorderRadius, UiGlobalTransform, Val, + ComputedUiTargetCamera, ResolvedBorderRadius, UiGlobalTransform, Val, ValContext, }; use bevy_utils::default; use bytemuck::{Pod, Zeroable}; @@ -317,11 +317,13 @@ pub fn extract_shadows( Val::Auto => 0., Val::Sum(..) | Val::Min(..) | Val::Max(..) | Val::Clamp(..) | Val::Calc(_) => val .resolve( - scale_factor, base, - ui_physical_viewport_size, - uinode.em_size, - uinode.rem_size, + &ValContext { + scale_factor, + physical_size: ui_physical_viewport_size, + em_size: uinode.em_size.0, + rem_size: uinode.rem_size.0, + }, ) .unwrap_or(0.), Val::Px(px) => px * scale_factor, diff --git a/crates/bevy_ui_render/src/gradient.rs b/crates/bevy_ui_render/src/gradient.rs index 9f4ca167d3a4a..7bc60527801e3 100644 --- a/crates/bevy_ui_render/src/gradient.rs +++ b/crates/bevy_ui_render/src/gradient.rs @@ -36,7 +36,7 @@ use bevy_text::{EmSize, RemSize}; use bevy_ui::{ BackgroundGradient, BorderGradient, ColorStop, ComputedStackIndex, ComputedUiRenderTargetInfo, ConicGradient, Gradient, InterpolationColorSpace, LinearGradient, RadialGradient, - ResolvedBorderRadius, Val, + ResolvedBorderRadius, Val, ValContext, }; use bevy_utils::default; use bytemuck::{Pod, Zeroable}; @@ -301,11 +301,17 @@ fn compute_color_stops( rem_size: RemSize, ) -> Vec<(LinearRgba, f32, f32)> { let mut extracted_color_stops = vec![]; + let context = ValContext { + scale_factor, + physical_size: target_size, + em_size: em_size.0, + rem_size: rem_size.0, + }; // resolve the physical distances of explicit stops and sort them scratch.extend(stops.iter().filter_map(|stop| { stop.point - .resolve(scale_factor, length, target_size, em_size, rem_size) + .resolve(length, &context) .ok() .map(|physical_point| (stop.color.to_linear(), physical_point, stop.hint)) })); diff --git a/crates/bevy_ui_widgets/src/scrollbar.rs b/crates/bevy_ui_widgets/src/scrollbar.rs index 6152fd6411e9e..d3c9532fba204 100644 --- a/crates/bevy_ui_widgets/src/scrollbar.rs +++ b/crates/bevy_ui_widgets/src/scrollbar.rs @@ -23,7 +23,7 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; use bevy_ui::{ prelude::BorderRect, BackgroundColor, BorderColor, BorderRadius, ComputedNode, ComputedUiRenderTargetInfo, ComputedUiTargetCamera, FocusPolicy, ScrollPosition, - UiGlobalTransform, UiRect, UiScale, UiSystems, UiTransform, Val, ZIndex, + UiGlobalTransform, UiRect, UiScale, UiSystems, UiTransform, Val, ValContext, ZIndex, }; /// Used to select the orientation of a scrollbar, slider, or other oriented control. @@ -413,16 +413,14 @@ pub(crate) fn update_scrollbar_thumb( thumb_node.border_radius = border_radius; } - let resolve_border_val = |val: &Val| { - val.resolve( - target_info.scale_factor(), - thumb_physical_size.x, - target_info.physical_size().as_vec2(), - thumb_node.em_size, - thumb_node.rem_size, - ) - .unwrap_or(0.) + let context = ValContext { + scale_factor: target_info.scale_factor(), + physical_size: target_info.physical_size().as_vec2(), + em_size: thumb_node.em_size.0, + rem_size: thumb_node.rem_size.0, }; + let resolve_border_val = + |val: &Val| val.resolve(thumb_physical_size.x, &context).unwrap_or(0.); let mut resolved_border = BorderRect { min_inset: Vec2::new( From 88b10b8bf6e612e05979149655689d60873dd603 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 2 Nov 2026 14:04:38 +0000 Subject: [PATCH 35/50] Changed CalcOp resolve to take ValContext --- crates/bevy_ui/src/val.rs | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/crates/bevy_ui/src/val.rs b/crates/bevy_ui/src/val.rs index d3a4d55126b67..036dc5c17a110 100644 --- a/crates/bevy_ui/src/val.rs +++ b/crates/bevy_ui/src/val.rs @@ -717,13 +717,7 @@ impl Val { physical_base_value, context, )), - Val::Calc(op) => op.0.resolve( - context.scale_factor, - physical_base_value, - context.physical_size, - EmSize(context.em_size), - RemSize(context.rem_size), - ), + Val::Calc(op) => op.0.resolve(physical_base_value, context), } } } @@ -937,14 +931,10 @@ impl CalcOp { /// Resolve a `CalcOp` to a value in physical pixels. pub fn resolve( &self, - scale_factor: f32, physical_base_value: f32, - physical_target_size: Vec2, - em_size: EmSize, - rem_size: RemSize, + context: &ValContext, ) -> Result { - let context = ValContext::new(scale_factor, physical_target_size, em_size, rem_size); - let resolve = |val: &Val| val.resolve(physical_base_value, &context); + let resolve = |val: &Val| val.resolve(physical_base_value, context); Ok(match self { Self::Add(a, b) => resolve(a)? + resolve(b)?, Self::Sub(a, b) => resolve(a)? - resolve(b)?, From 2b176ab5c9e297dd4da29d296b51ccab566e0472 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 2 Nov 2026 14:28:25 +0000 Subject: [PATCH 36/50] Updated BorderRadius, UiPosition, Val2, UiTransform and CornerRadius to also use ValContext. --- crates/bevy_ui/src/geometry.rs | 77 +++++++++++++------------ crates/bevy_ui/src/layout/mod.rs | 30 ++++++---- crates/bevy_ui/src/ui_node.rs | 43 ++------------ crates/bevy_ui/src/ui_transform.rs | 32 +++------- crates/bevy_ui_render/src/gradient.rs | 20 ++++--- crates/bevy_ui_widgets/src/popover.rs | 12 ++-- crates/bevy_ui_widgets/src/scrollbar.rs | 24 ++------ 7 files changed, 96 insertions(+), 142 deletions(-) diff --git a/crates/bevy_ui/src/geometry.rs b/crates/bevy_ui/src/geometry.rs index 4c41643b86bf5..6cad828453dc9 100644 --- a/crates/bevy_ui/src/geometry.rs +++ b/crates/bevy_ui/src/geometry.rs @@ -1,7 +1,6 @@ use crate::val::*; use bevy_math::Vec2; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use bevy_text::{EmSize, RemSize}; use bevy_utils::default; #[cfg(feature = "serialize")] @@ -559,21 +558,13 @@ impl UiPosition { } /// Resolves the `Position` into physical coordinates. - pub fn resolve( - &self, - scale_factor: f32, - physical_size: Vec2, - physical_target_size: Vec2, - em_size: EmSize, - rem_size: RemSize, - ) -> Vec2 { - let context = ValContext::new(scale_factor, physical_target_size, em_size, rem_size); + pub fn resolve(&self, physical_size: Vec2, context: &ValContext) -> Vec2 { let d = self.anchor.map(|p| if 0. < p { -1. } else { 1. }); physical_size * self.anchor + d * Vec2::new( - self.x.resolve(physical_size.x, &context).unwrap_or(0.), - self.y.resolve(physical_size.y, &context).unwrap_or(0.), + self.x.resolve(physical_size.x, context).unwrap_or(0.), + self.y.resolve(physical_size.y, context).unwrap_or(0.), ) } } @@ -598,13 +589,19 @@ impl From<(Val, Val)> for UiPosition { /// /// ``` /// # use bevy_math::{Vec2, Vec2Swizzles}; -/// # use bevy_ui::{CornerRadius, Val}; +/// # use bevy_ui::{CornerRadius, Val, ValContext}; /// # use bevy_text::{EmSize, RemSize}; /// let radius = Val::Px(10.0); /// let size = Vec2::new(100.0, 50.0); /// let viewport_size = Vec2::new(1920.0, 1080.0); /// let em_size = EmSize(20.0); /// let rem_size = RemSize(20.0); +/// let context = ValContext { +/// scale_factor: 1.0, +/// physical_size: viewport_size, +/// em_size: em_size.0, +/// rem_size: rem_size.0, +/// }; /// /// let c1 = CornerRadius { /// x: radius.clone(), @@ -614,10 +611,10 @@ impl From<(Val, Val)> for UiPosition { /// x: Val::ZERO, /// y: radius, /// }; -/// let r = c1.resolve(1.0, size, viewport_size, em_size, rem_size); +/// let r = c1.resolve(size, &context); /// assert_eq!( /// r, -/// c2.resolve(1.0, size, viewport_size, em_size, rem_size).yx(), +/// c2.resolve(size, &context).yx(), /// ); /// assert_eq!( /// r, @@ -663,13 +660,19 @@ impl CornerRadius { /// /// ``` /// # use bevy_math::Vec2; - /// # use bevy_ui::{CornerRadius, Val}; + /// # use bevy_ui::{CornerRadius, Val, ValContext}; /// # use bevy_text::{EmSize, RemSize}; /// let radius = Val::Px(30.0); /// let size = Vec2::new(100.0, 50.0); /// let viewport_size = Vec2::new(1920.0, 1080.0); /// let em_size = EmSize(20.0); /// let rem_size = RemSize(20.0); + /// let context = ValContext { + /// scale_factor: 1.0, + /// physical_size: viewport_size, + /// em_size: em_size.0, + /// rem_size: rem_size.0, + /// }; /// /// let c1 = CornerRadius::circular(radius.clone()); /// let c2 = CornerRadius { @@ -677,10 +680,10 @@ impl CornerRadius { /// y: radius, /// }; /// - /// let r = c1.resolve(1.0, size, viewport_size, em_size, rem_size); + /// let r = c1.resolve(size, &context); /// assert_eq!( /// r, - /// c2.resolve(1.0, size, viewport_size, em_size, rem_size), + /// c2.resolve(size, &context), /// ); /// assert_eq!( /// r, @@ -703,13 +706,19 @@ impl CornerRadius { /// /// ``` /// # use bevy_math::Vec2; - /// # use bevy_ui::{CornerRadius, Val}; + /// # use bevy_ui::{CornerRadius, Val, ValContext}; /// # use bevy_text::{EmSize, RemSize}; /// let radius = Val::Px(30.0); /// let size = Vec2::new(100.0, 50.0); /// let viewport_size = Vec2::new(1920.0, 1080.0); /// let em_size = EmSize(20.0); /// let rem_size = RemSize(20.0); + /// let context = ValContext { + /// scale_factor: 1.0, + /// physical_size: viewport_size, + /// em_size: em_size.0, + /// rem_size: rem_size.0, + /// }; /// /// let c1 = CornerRadius::all(radius.clone()); /// let c2 = CornerRadius { @@ -717,10 +726,10 @@ impl CornerRadius { /// y: radius, /// }; /// - /// let r = c1.resolve(1.0, size, viewport_size, em_size, rem_size); + /// let r = c1.resolve(size, &context); /// assert_eq!( /// r, - /// c2.resolve(1.0, size, viewport_size, em_size, rem_size), + /// c2.resolve(size, &context), /// ); /// assert_eq!( /// r, @@ -742,15 +751,7 @@ impl CornerRadius { } /// Resolves this corner radius into horizontal and vertical radii in physical pixels. - pub fn resolve( - &self, - scale_factor: f32, - size: Vec2, - viewport_size: Vec2, - em_size: EmSize, - rem_size: RemSize, - ) -> Vec2 { - let context = ValContext::new(scale_factor, viewport_size, em_size, rem_size); + pub fn resolve(&self, size: Vec2, context: &ValContext) -> Vec2 { match self { Self { x: Val::Auto, @@ -765,13 +766,13 @@ impl CornerRadius { y: radius, } => Vec2::splat( radius - .resolve(size.min_element(), &context) + .resolve(size.min_element(), context) .unwrap_or(0.) .clamp(0., 0.5 * size.min_element()), ), Self { x, y } => Vec2::new( - x.resolve(size.x, &context).unwrap_or(0.), - y.resolve(size.y, &context).unwrap_or(0.), + x.resolve(size.x, context).unwrap_or(0.), + y.resolve(size.y, context).unwrap_or(0.), ) .clamp(Vec2::ZERO, 0.5 * size), } @@ -821,18 +822,20 @@ impl PartialEq for CornerRadius { mod tests { use super::*; use bevy_math::vec2; + use bevy_text::{EmSize, RemSize}; #[test] fn corner_radius_resolve() { let size = vec2(100., 50.); let viewport_size = vec2(1000., 500.); + let context = ValContext::new(1., viewport_size, EmSize(20.0), RemSize(20.0)); assert_eq!( CornerRadius { x: Val::Px(100.), y: Val::Auto, } - .resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)), + .resolve(size, &context), vec2(25., 25.) ); assert_eq!( @@ -840,7 +843,7 @@ mod tests { x: Val::Px(40.), y: Val::Px(40.), } - .resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)), + .resolve(size, &context), vec2(40., 25.) ); assert_eq!( @@ -848,11 +851,11 @@ mod tests { x: Val::ZERO, y: Val::Px(20.), } - .resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)), + .resolve(size, &context), vec2(0., 20.) ); assert_eq!( - CornerRadius::default().resolve(1., size, viewport_size, EmSize(20.0), RemSize(20.0)), + CornerRadius::default().resolve(size, &context), vec2(0., 0.) ); } diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index 6ea317b422ebd..332b199cf2815 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -569,15 +569,17 @@ fn update_uinode_geometry_recursive( }); inherited_transform *= transform.compute_affine( - inverse_target_scale_factor.recip(), // Normally percentage translations are resolved based on a node's own size // but the size of a `GhostNode` is always zero. // Instead for a `GhostNode` percentage translations are resolved using the size // of its parent. parent_size, - target_size, - *em_size, - rem_size, + &ValContext::new( + inverse_target_scale_factor.recip(), + target_size, + *em_size, + rem_size, + ), ); if inherited_transform != **global_transform { @@ -673,11 +675,13 @@ fn update_uinode_geometry_recursive( // Compute the node's new global transform let mut local_transform = transform.compute_affine( - inverse_target_scale_factor.recip(), layout_size, - target_size, - *em_size, - rem_size, + &ValContext::new( + inverse_target_scale_factor.recip(), + target_size, + *em_size, + rem_size, + ), ); local_transform.translation += local_center; inherited_transform *= local_transform; @@ -802,11 +806,13 @@ pub fn update_border_radius( // We don't trigger change detection for changes to border radius // unless the border radius actually changed let new_border_radius = style.border_radius.resolve( - node.inverse_scale_factor.recip(), node.size, - target.physical_size.as_vec2(), - node.em_size, - node.rem_size, + &ValContext::new( + node.inverse_scale_factor.recip(), + target.physical_size.as_vec2(), + node.em_size, + node.rem_size, + ), ); if node.border_radius != new_border_radius { node.border_radius = new_border_radius; diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index c9b7e036a4f2b..4084f055803f7 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -4,7 +4,7 @@ use crate::{ UiTreeDirty, }, ui_transform::{UiGlobalTransform, UiTransform}, - ComputedStackIndex, ContentSize, CornerRadius, FocusPolicy, UiRect, Val, + ComputedStackIndex, ContentSize, CornerRadius, FocusPolicy, UiRect, Val, ValContext, }; use bevy_camera::{visibility::Visibility, Camera, RenderTarget}; use bevy_color::{Alpha, Color}; @@ -3214,43 +3214,12 @@ impl BorderRadius { /// Resolve the border radii for the corners from the given context values. /// Returns the radii of the each corner in physical pixels. - pub fn resolve( - &self, - scale_factor: f32, - node_size: Vec2, - viewport_size: Vec2, - em_size: EmSize, - rem_size: RemSize, - ) -> ResolvedBorderRadius { + pub fn resolve(&self, node_size: Vec2, context: &ValContext) -> ResolvedBorderRadius { ResolvedBorderRadius { - top_left: self.top_left.resolve( - scale_factor, - node_size, - viewport_size, - em_size, - rem_size, - ), - top_right: self.top_right.resolve( - scale_factor, - node_size, - viewport_size, - em_size, - rem_size, - ), - bottom_left: self.bottom_left.resolve( - scale_factor, - node_size, - viewport_size, - em_size, - rem_size, - ), - bottom_right: self.bottom_right.resolve( - scale_factor, - node_size, - viewport_size, - em_size, - rem_size, - ), + top_left: self.top_left.resolve(node_size, context), + top_right: self.top_right.resolve(node_size, context), + bottom_left: self.bottom_left.resolve(node_size, context), + bottom_right: self.bottom_right.resolve(node_size, context), } } } diff --git a/crates/bevy_ui/src/ui_transform.rs b/crates/bevy_ui/src/ui_transform.rs index 550c6634ebe39..b556226ec8d17 100644 --- a/crates/bevy_ui/src/ui_transform.rs +++ b/crates/bevy_ui/src/ui_transform.rs @@ -10,7 +10,6 @@ use bevy_math::Mat2; use bevy_math::Rot2; use bevy_math::Vec2; use bevy_reflect::prelude::*; -use bevy_text::{EmSize, RemSize}; use core::ops::Mul; /// A pair of [`Val`]s used to represent a 2-dimensional size or offset. @@ -58,22 +57,13 @@ impl Val2 { Self::new(Val::Percent(x.val_num_f32()), Val::Percent(y.val_num_f32())) } - /// Resolves this [`Val2`] from the given `scale_factor`, `parent_size`, - /// and `viewport_size`. + /// Resolves this [`Val2`] from the given `base_size` and `context`. /// /// Component values of [`Val::Auto`] are resolved to 0. - pub fn resolve( - &self, - scale_factor: f32, - base_size: Vec2, - viewport_size: Vec2, - em_size: EmSize, - rem_size: RemSize, - ) -> Vec2 { - let context = ValContext::new(scale_factor, viewport_size, em_size, rem_size); + pub fn resolve(&self, base_size: Vec2, context: &ValContext) -> Vec2 { Vec2::new( - self.x.resolve(base_size.x, &context).unwrap_or(0.), - self.y.resolve(base_size.y, &context).unwrap_or(0.), + self.x.resolve(base_size.x, context).unwrap_or(0.), + self.y.resolve(base_size.y, context).unwrap_or(0.), ) } @@ -187,20 +177,12 @@ impl UiTransform { } } - /// Resolves the translation from the given `scale_factor`, `base_value`, and `target_size` + /// Resolves the translation from the given `base_size` and `context` /// and returns a 2d affine transform from the resolved translation, and the `UiTransform`'s rotation, and scale. - pub fn compute_affine( - &self, - scale_factor: f32, - base_size: Vec2, - target_size: Vec2, - em_size: EmSize, - rem_size: RemSize, - ) -> Affine2 { + pub fn compute_affine(&self, base_size: Vec2, context: &ValContext) -> Affine2 { Affine2::from_mat2_translation( Mat2::from(self.rotation) * Mat2::from_diagonal(self.scale), - self.translation - .resolve(scale_factor, base_size, target_size, em_size, rem_size), + self.translation.resolve(base_size, context), ) } } diff --git a/crates/bevy_ui_render/src/gradient.rs b/crates/bevy_ui_render/src/gradient.rs index 7bc60527801e3..50b300e2c9be5 100644 --- a/crates/bevy_ui_render/src/gradient.rs +++ b/crates/bevy_ui_render/src/gradient.rs @@ -558,11 +558,13 @@ pub fn extract_gradients( stops, }) => { let c = center.resolve( - target.scale_factor(), uinode.size, - target.physical_size().as_vec2(), - uinode.em_size, - uinode.rem_size, + &ValContext { + scale_factor: target.scale_factor(), + physical_size: target.physical_size().as_vec2(), + em_size: uinode.em_size.0, + rem_size: uinode.rem_size.0, + }, ); let size = shape.resolve( @@ -617,11 +619,13 @@ pub fn extract_gradients( stops, }) => { let g_start = center.resolve( - target.scale_factor(), uinode.size, - target.physical_size().as_vec2(), - uinode.em_size, - uinode.rem_size, + &ValContext { + scale_factor: target.scale_factor(), + physical_size: target.physical_size().as_vec2(), + em_size: uinode.em_size.0, + rem_size: uinode.rem_size.0, + }, ); // sort the explicit stops diff --git a/crates/bevy_ui_widgets/src/popover.rs b/crates/bevy_ui_widgets/src/popover.rs index 6342aa6e7b6a7..70ea8567f0028 100644 --- a/crates/bevy_ui_widgets/src/popover.rs +++ b/crates/bevy_ui_widgets/src/popover.rs @@ -14,7 +14,7 @@ use bevy_math::{Affine2, Rect, Vec2}; use bevy_reflect::Reflect; use bevy_ui::{ ComputedNode, ComputedUiRenderTargetInfo, Node, PositionType, UiGlobalTransform, UiSystems, - UiTransform, Val2, + UiTransform, Val2, ValContext, }; use crate::update_scrollbar_thumb; @@ -250,11 +250,13 @@ pub(crate) fn position_popover( continue; } let resolved_translation = transform.translation.resolve( - computed_target.scale_factor(), computed_node.size(), - computed_target.physical_size().as_vec2(), - computed_node.em_size, - computed_node.rem_size, + &ValContext { + scale_factor: computed_target.scale_factor(), + physical_size: computed_target.physical_size().as_vec2(), + em_size: computed_node.em_size.0, + rem_size: computed_node.rem_size.0, + }, ); let logical_translation = (resolved_translation + parent_matrix.inverse() * physical_translation) diff --git a/crates/bevy_ui_widgets/src/scrollbar.rs b/crates/bevy_ui_widgets/src/scrollbar.rs index d3c9532fba204..3b6f6f51c87b9 100644 --- a/crates/bevy_ui_widgets/src/scrollbar.rs +++ b/crates/bevy_ui_widgets/src/scrollbar.rs @@ -402,23 +402,17 @@ pub(crate) fn update_scrollbar_thumb( thumb_node.inverse_scale_factor = inverse_scale_factor; } - let border_radius = thumb.border_radius.resolve( - target_info.scale_factor(), - thumb_physical_size, - target_info.physical_size().as_vec2(), - thumb_node.em_size, - thumb_node.rem_size, - ); - if thumb_node.border_radius != border_radius { - thumb_node.border_radius = border_radius; - } - let context = ValContext { scale_factor: target_info.scale_factor(), physical_size: target_info.physical_size().as_vec2(), em_size: thumb_node.em_size.0, rem_size: thumb_node.rem_size.0, }; + let border_radius = thumb.border_radius.resolve(thumb_physical_size, &context); + if thumb_node.border_radius != border_radius { + thumb_node.border_radius = border_radius; + } + let resolve_border_val = |val: &Val| val.resolve(thumb_physical_size.x, &context).unwrap_or(0.); @@ -450,13 +444,7 @@ pub(crate) fn update_scrollbar_thumb( thumb_node.bypass_change_detection().border = resolved_border; let new_transform = scrollbar_transform.affine() - * thumb_transform.compute_affine( - target_info.scale_factor(), - thumb_physical_size, - target_info.physical_size().as_vec2(), - thumb_node.em_size, - thumb_node.rem_size, - ) + * thumb_transform.compute_affine(thumb_physical_size, &context) * Affine2::from_translation(thumb_center * target_info.scale_factor()); if thumb_global_transform.affine() != new_transform { From a684051ded27046a6edae65c11b769acd3244555 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 2 Nov 2026 15:27:10 +0000 Subject: [PATCH 37/50] Update and clean up doc comments some more. --- crates/bevy_ui/src/layout/layout_tree.rs | 25 +++++++----------------- 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/crates/bevy_ui/src/layout/layout_tree.rs b/crates/bevy_ui/src/layout/layout_tree.rs index c33bf7b962ab8..187a5027ee12d 100644 --- a/crates/bevy_ui/src/layout/layout_tree.rs +++ b/crates/bevy_ui/src/layout/layout_tree.rs @@ -30,25 +30,21 @@ use crate::{ #[expect( unsafe_code, - reason = "the calc pointers in the style point into the `Arc`s owned by the same `TaffyStyle`" + reason = "`TaffyStyle`' uses raw pointers to its `PercentageCalc` values." )] -// SAFETY: `Style` is only `!Send` because of its raw calc pointers, which point into the -// `Arc`s held alongside it. `PercentageCalc` is `Send + Sync`. +// SAFETY: `Style` is `!Send` because of its raw calc pointers. `PercentageCalc` is `Send + Sync`. unsafe impl Send for TaffyStyle {} #[expect( unsafe_code, - reason = "the calc pointers in the style point into the `Arc`s owned by the same `TaffyStyle`" + reason = "`TaffyStyle` owns the `PercentageCalc`s that its `Style` points to." )] -// SAFETY: `Style` is only `!Sync` because of its raw calc pointers, which point into the -// `Arc`s held alongside it. `PercentageCalc` is `Send + Sync`. +// SAFETY: `Style` is `!Send` because of its raw calc pointers. `PercentageCalc` is `Send + Sync`. unsafe impl Sync for TaffyStyle {} -/// A node's resolved taffy style, with the expressions its calc values point to. -/// -/// Every calc pointer in the style points into one of the `Arc`s stored next -/// to it, which is what `resolve_calc` relies on. The fields are crate-private so nothing -/// else can put a pointer in the style, and it can only be read through [`Deref`]. +/// Cached Taffy `Style`, updated from an entity's `Node` component on changes. +/// Lengths using percentages that can't be resolved until layout are stored in `Arc`s +/// and `Style` references them using raw pointers. The `Vec` is used to keep the arcs alive. #[derive(Default, Component, Clone, Deref)] pub struct TaffyStyle( #[deref] pub(crate) Style, @@ -78,13 +74,6 @@ impl PercentageCalc { reason = "taffy hands back the pointer it was given, which points into an `Arc` owned by the same `TaffyStyle`" )] pub(crate) fn resolve_calc(ptr: *const (), basis: f32) -> f32 { - // SAFETY: Calc pointers only enter a `TaffyStyle` through `update_taffy_style_from_node`, - // which creates each one with `Arc::as_ptr` on an `Arc` that it pushes - // into the same style, and rewrites every length field whenever it clears that `Vec`. - // `TaffyStyle`'s fields are crate-private and it is not `DerefMut`, so no other code can - // store a pointer in one, and `MeasureArgs` cannot be built outside the crate. Layout - // borrows styles immutably for as long as taffy can hand a pointer back, and clones share - // the `Arc`s, so the pointee is alive and not being written. let calc = unsafe { &*ptr.cast::() }; calc.resolve(basis) } From 50607207ab0c551eb3107f46244b6526404a0859 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 2 Nov 2026 16:23:58 +0000 Subject: [PATCH 38/50] More doc comments --- crates/bevy_ui/src/val.rs | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/crates/bevy_ui/src/val.rs b/crates/bevy_ui/src/val.rs index 036dc5c17a110..cae421169d9a1 100644 --- a/crates/bevy_ui/src/val.rs +++ b/crates/bevy_ui/src/val.rs @@ -122,20 +122,14 @@ pub enum Val { /// length anywhere in the hierarchy. Resolved from the [`RemSize`] resource. Rem(f32), /// The sum of up to three terms with distinct units, such as `Val::Percent(50.0) - Val::Px(10.0)`. - /// - /// The `n`th value belongs to the `n`th unit of the set, see [`ValUnitSet`]. - /// Built by `+` and `-`; longer expressions fall back to [`Val::Calc`]. Sum(ValUnitSet, [f32; 3]), - /// The smallest of up to three terms with distinct units, laid out like [`Val::Sum`]. - /// Built by [`Val::min`]. + /// The smallest of up to three terms with distinct units. Min(ValUnitSet, [f32; 3]), - /// The largest of up to three terms with distinct units, laid out like [`Val::Sum`]. - /// Built by [`Val::max`]. + /// The largest of up to three terms with distinct units. Max(ValUnitSet, [f32; 3]), - /// A value clamped between a minimum and a maximum, each a unit value, stored as the - /// `[min, value, max]` units and values. Built by [`Val::clamp`]; when an operand is itself - /// an expression it falls back to [`Val::Calc`]. + /// A value clamped between a minimum and a maximum. Stored in order `[min, value, max]`. Clamp([ValUnit; 3], [f32; 3]), + /// A wrapped operation between `Val`s. Calc(Calc), } @@ -315,6 +309,7 @@ impl Val { } } + /// Returns true if this `Val` is a percentage value or any of the operations in its calc expression tree uses percentage values. pub(crate) fn uses_percent(&self) -> bool { match self { Self::Percent(_) => true, @@ -879,8 +874,6 @@ pub fn vmax(value: T) -> Val { #[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))] pub struct Calc(pub(crate) Arc); -// Serialized as the `CalcOp` itself: serde only implements `Arc` behind its `rc` feature, -// and sharing is an implementation detail that should not leak into the format. #[cfg(feature = "serialize")] impl serde::Serialize for Calc { fn serialize(&self, serializer: S) -> Result { @@ -917,6 +910,7 @@ pub enum CalcOp { } impl CalcOp { + /// Returns true if any of the operations in the expression tree uses percentage values. pub(crate) fn uses_percent(&self) -> bool { match self { Self::Add(a, b) | Self::Sub(a, b) | Self::Min(a, b) | Self::Max(a, b) => { @@ -944,6 +938,8 @@ impl CalcOp { }) } + /// Multiply an operation by a scalar. + /// If the scale is negative, the direction of the operation is changed. pub(crate) fn scale(&mut self, k: f32) { use core::mem::take; match self { @@ -1310,33 +1306,29 @@ impl Val { lo.max(v.min(hi)) } - /// Applies `f` to every term of a [`Val::Sum`], [`Val::Min`], [`Val::Max`] or [`Val::Clamp`]. - /// `reverses` swaps `Min` and `Max` and the bounds of `Clamp`, for an `f` that reverses ordering. + /// Applies the function to every term of a [`Val::Sum`], [`Val::Min`], [`Val::Max`] or [`Val::Clamp`]. + /// If `reverses` is true, min and max are swapped. pub(crate) fn map_operands(self, f: impl Fn(f32) -> f32, reverses: bool) -> Self { let (op, units, mut values) = match self { Val::Sum(units, values) => (ReduceOp::Sum, units, values), Val::Min(units, values) => (ReduceOp::Min, units, values), Val::Max(units, values) => (ReduceOp::Max, units, values), Val::Clamp(units, values) => { - // Inverted same-unit bounds make the clamp its minimum whatever the value is. - if units[0] == units[2] && values[0] > values[2] { + // If min is greater than max, we reduce to min by CSS rules. + if units[0] == units[2] && values[2] < values[0] { return Val::leaf(units[0], f(values[0])); } let values = values.map(&f); if !reverses { return Val::Clamp(units, values); } - // `f` reverses order, so `f(max(lo, min(v, hi)))` is `min(f(lo), max(f(v), f(hi)))`. - // Same-unit bounds are now known to satisfy `lo <= hi`, so that is the clamp with - // the bounds swapped; mixed-unit bounds may invert at some bases, so keep the - // exact form. - let [lo, v, hi] = core::array::from_fn(|i| Val::leaf(units[i], values[i])); if units[0] == units[2] { return Val::Clamp( [units[2], units[1], units[0]], [values[2], values[1], values[0]], ); } + let [lo, v, hi] = core::array::from_fn(|i| Val::leaf(units[i], values[i])); return lo.min(v.max(hi)); } other => return other, From 8cd29d20d827163cbd18c8864a9c91307ffff1ca Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 2 Nov 2026 16:31:50 +0000 Subject: [PATCH 39/50] More doc comments. --- crates/bevy_ui/src/val.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ui/src/val.rs b/crates/bevy_ui/src/val.rs index cae421169d9a1..53d0097a101fa 100644 --- a/crates/bevy_ui/src/val.rs +++ b/crates/bevy_ui/src/val.rs @@ -273,7 +273,7 @@ impl Val { } } - /// The unit of a single-term value, or `None` for [`Val::Auto`] and the expression variants. + /// Returns a `Val`'s units if it isn't auto or an expression, otherwise `None`. pub const fn unit(&self) -> Option { Some(match self { Self::Px(_) => ValUnit::Px, @@ -295,7 +295,7 @@ impl Val { }) } - /// A single-term value of the given `unit`. + /// Construct a plain unit value. pub const fn leaf(unit: ValUnit, value: f32) -> Self { match unit { ValUnit::Px => Self::Px(value), From b4822dcfd1e4bb2d0fc829602896737ba71cf22b Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 2 Nov 2026 16:35:59 +0000 Subject: [PATCH 40/50] More doc comments --- crates/bevy_ui/src/val.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/bevy_ui/src/val.rs b/crates/bevy_ui/src/val.rs index 53d0097a101fa..7ffb632246f36 100644 --- a/crates/bevy_ui/src/val.rs +++ b/crates/bevy_ui/src/val.rs @@ -250,10 +250,7 @@ impl Val { /// The largest finite [`Val::Px`] value. pub const MAX: Self = Self::Px(f32::MAX); - /// Returns the number this `Val` wraps, whatever its unit, or `None` for [`Val::Auto`] and the expression variants. - /// - /// The number is meaningless without the variant it came from — `Val::Px(4.0)` and - /// `Val::Rem(4.0)` both return `4.0`. Use [`Val::resolve`] for a length in pixels. + /// Returns the `Val`'s raw inner value without its units, or `None` if it's `Auto` or an expression. pub(crate) const fn inner(&self) -> Option { match self { Self::Auto From c88416f9b30f4564b001c659218a6f444d926262 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 2 Nov 2026 16:53:53 +0000 Subject: [PATCH 41/50] More Val doc comments --- crates/bevy_ui/src/val.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ui/src/val.rs b/crates/bevy_ui/src/val.rs index 7ffb632246f36..38b8c12fb78f5 100644 --- a/crates/bevy_ui/src/val.rs +++ b/crates/bevy_ui/src/val.rs @@ -507,17 +507,20 @@ impl Val { } } - /// The smaller of `self` and `other` once resolved. + /// Creates an expression that returns the minimum of two `Val`s when resolved. + /// If both `Val`s have the same units, their minimum is returned immediately. pub fn min(self, other: Val) -> Val { self.combine(other, ReduceOp::Min) } - /// The larger of `self` and `other` once resolved. + /// Creates an expression that returns the maximum of two `Val`s when resolved. + /// If both `Val`s have the same units, their maximum is returned immediately. pub fn max(self, other: Val) -> Val { self.combine(other, ReduceOp::Max) } - /// `self` clamped between `min` and `max` once resolved. + /// Creates an expression that returns `self` clamped between `min`` and `max` when resolved. + /// If the bounds and self are all the same units, the clamped value of `self` is returned immediately. pub fn clamp(self, min: Val, max: Val) -> Val { Val::clamp_inline(&min, &self, &max).unwrap_or_else(|| CalcOp::Clamp(min, self, max).into()) } From 89b22f30ee5d976ef533eed30f39b0a85709c1ff Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 2 Nov 2026 17:02:12 +0000 Subject: [PATCH 42/50] More doc comments --- crates/bevy_ui/src/val.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/bevy_ui/src/val.rs b/crates/bevy_ui/src/val.rs index 38b8c12fb78f5..22f3e1af45a0f 100644 --- a/crates/bevy_ui/src/val.rs +++ b/crates/bevy_ui/src/val.rs @@ -725,6 +725,8 @@ impl TryFrom for FontSize { /// [`Val::Em`] becomes [`FontSize::Rem`]: em is relative to the font size of the node /// itself, which is the very thing being defined here, so it resolves against the root /// size instead to avoid the circularity. + /// + /// Expressions cannot be converted. fn try_from(val: Val) -> Result { Ok(match val { Val::Auto => FontSize::Rem(1.), @@ -1006,6 +1008,7 @@ impl ValUnit { Self::Rem, ]; + /// A bit mask with only the bit for this unit set. const fn bit(self) -> u8 { 1 << self as u8 } From 86ce06e89aa46adeabd5ee329a7bde7743d52fc4 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 2 Nov 2026 17:18:37 +0000 Subject: [PATCH 43/50] ValUnitSet doc comments --- crates/bevy_ui/src/val.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/bevy_ui/src/val.rs b/crates/bevy_ui/src/val.rs index 22f3e1af45a0f..c753321018c48 100644 --- a/crates/bevy_ui/src/val.rs +++ b/crates/bevy_ui/src/val.rs @@ -1033,11 +1033,10 @@ impl ValUnit { /// If more unit types are added to `Val`, a `u8` discriminator won't be sufficient. const _: () = assert!(ValUnit::ALL.len() <= u8::BITS as usize); -/// A set of [`ValUnit`]s. +/// A set of distinct [`ValUnit`]s. /// -/// [`Val::Sum`], [`Val::Min`] and [`Val::Max`] pair one of these with an array of values: -/// the `n`th value belongs to the `n`th unit in [`ValUnitSet::iter`] order, and the -/// slots after the last unit are unused. +/// We can immediately reduce sum, min and max expressions to a single `Val` if it has compatible units, +/// so [`Val::Sum`], [`Val::Min`] and [`Val::Max`] always store distinct units. #[derive(Clone, Copy, PartialEq, Eq, Hash, Default, Reflect)] #[reflect(PartialEq, Debug, Clone)] #[cfg_attr( From b978b6a395375fd9751430a672fa0407df5aadb6 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 2 Nov 2026 17:28:06 +0000 Subject: [PATCH 44/50] Renamed `ValUnitSet::slot` to `index`, updated doc comments. --- crates/bevy_ui/src/val.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ui/src/val.rs b/crates/bevy_ui/src/val.rs index c753321018c48..e3e33d91b1d05 100644 --- a/crates/bevy_ui/src/val.rs +++ b/crates/bevy_ui/src/val.rs @@ -1065,8 +1065,8 @@ impl ValUnitSet { self.0 & unit.bit() != 0 } - /// The position `unit` has, or would have, among the set's units in ascending order. - pub const fn slot(&self, unit: ValUnit) -> usize { + /// The index where the value is stored in an expression's values array. + pub const fn index(&self, unit: ValUnit) -> usize { (self.0 & (unit.bit() - 1)).count_ones() as usize } @@ -1123,7 +1123,7 @@ impl ValOperands { /// /// Returns `false` and leaves `self` unchanged when a new term would exceed [`Self::CAPACITY`]. fn fold(&mut self, unit: ValUnit, value: f32, op: fn(f32, f32) -> f32) -> bool { - let slot = self.units.slot(unit); + let slot = self.units.index(unit); if self.units.contains(unit) { self.values[slot] = op(self.values[slot], value); } else if self.units.len() < Self::CAPACITY { From 70ef45016654871fcc887aa088f5e1bf4098aedc Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 2 Nov 2026 17:52:28 +0000 Subject: [PATCH 45/50] Added doc test doing interpolation between two sums. --- crates/bevy_ui/src/val.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ui/src/val.rs b/crates/bevy_ui/src/val.rs index e3e33d91b1d05..3cb730c8bdf71 100644 --- a/crates/bevy_ui/src/val.rs +++ b/crates/bevy_ui/src/val.rs @@ -1146,6 +1146,7 @@ impl ValOperands { *self = kept; } + /// Resolve the operands to physical values and apply the operation. pub(crate) fn resolve_to_physical( &self, op: fn(f32, f32) -> f32, @@ -1162,7 +1163,16 @@ impl ValOperands { .unwrap_or(0.0) } - /// Interpolates operand-wise, or `None` when the units differ. + /// Interpolates between two sets of operands, when their units are compatible. + /// + /// Called by `Val::try_interpolate_stable`, for example: + /// ``` + /// # use bevy_ui::val::{px, percent}; + /// # use bevy_math::TryStableInterpolate; + /// let a = px(0) + percent(0); + /// let b = px(10) + percent(100); + /// assert_eq!(a.try_interpolate_stable(&b, 0.5).unwrap(), px(5) + percent(50)); + /// ``` pub(crate) fn interpolate_stable(&self, other: &Self, t: f32) -> Option { (self.units == other.units).then(|| { let mut result = *self; From 673091dae4b0f20a1762e1b3d5754e159bdfbafb Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 2 Nov 2026 18:42:31 +0000 Subject: [PATCH 46/50] More doc comments for new `Val` methods. --- crates/bevy_ui/src/val.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ui/src/val.rs b/crates/bevy_ui/src/val.rs index 3cb730c8bdf71..f0d4641cfe80f 100644 --- a/crates/bevy_ui/src/val.rs +++ b/crates/bevy_ui/src/val.rs @@ -1199,6 +1199,7 @@ pub(crate) enum ReduceOp { } impl ReduceOp { + /// Apply the operation to unitless values pub(crate) fn apply(self) -> fn(f32, f32) -> f32 { match self { Self::Sum => |a, b| a + b, @@ -1230,6 +1231,7 @@ impl ReduceOp { } } + /// Creates a `CalcOp` from the `ReduceOp` and the given operands. fn calc(self, a: Val, b: Val) -> Val { match self { Self::Sum => CalcOp::Add(a, b), @@ -1256,8 +1258,8 @@ impl Val { operands.iter() } - /// Combines two values with `op`, folding them into one set of terms when both sides are - /// unit values or terms of the same operation, and building a [`Calc`] otherwise. + /// Combines two `Val`s with an operation. Unit values and expressions with the same operation are combined into + /// one inline operand set, if they fit. Otherwise, a `[Calc] is built`. pub(crate) fn combine(self, rhs: Val, op: ReduceOp) -> Val { if let (Some(mut terms), Some(rhs_terms)) = (self.operands_for(op), rhs.operands_for(op)) && rhs_terms From 19277ed483e51e5041055491f9cea80bd71f3400 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 2 Nov 2026 18:53:30 +0000 Subject: [PATCH 47/50] More doc comment edits. --- crates/bevy_ui/src/val.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ui/src/val.rs b/crates/bevy_ui/src/val.rs index f0d4641cfe80f..01d021e798881 100644 --- a/crates/bevy_ui/src/val.rs +++ b/crates/bevy_ui/src/val.rs @@ -1108,10 +1108,9 @@ impl ValOperands { (self.units, self.values) } + /// Creates a `ValOperands` containing a single value. const fn single(unit: ValUnit, value: f32) -> Self { - let mut units = ValUnitSet::EMPTY; - units.insert(unit); - Self::new(units, [value, 0.0, 0.0]) + Self::new(ValUnitSet(unit.bit()), [value, 0.0, 0.0]) } /// The operands in ascending unit order. From 6ee50c9cd0c1aae4a53625e837b9acb111d4a328 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 3 Nov 2026 08:50:48 +0000 Subject: [PATCH 48/50] Added migration guide, added PR # to release note stub --- _release-content/migration-guides/val_refactor.md | 11 +++++++++++ _release-content/release-notes/val_calc.md | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 _release-content/migration-guides/val_refactor.md diff --git a/_release-content/migration-guides/val_refactor.md b/_release-content/migration-guides/val_refactor.md new file mode 100644 index 0000000000000..9261ea9c31412 --- /dev/null +++ b/_release-content/migration-guides/val_refactor.md @@ -0,0 +1,11 @@ +--- +title: "Val refactor" +pull_requests: [26409] +--- + +- `LayoutContext` has been renamed to `ValContext`. +- `Val::resolve`, `Val2::resolve`, `UiPosition::resolve`, `CornerRadius::resolve`, + `RadialGradientShape::resolve`, `BorderRadius::resolve`, and `UiTransform::compute_affine` + now take `&ValContext` instead of separate scale factor, target size, `EmSize`, and `RemSize` + arguments. The basis for percent values remains a separate argument. +- `Val` has been moved from the `bevy_ui` crate's `geometry` module to a new module `val`. diff --git a/_release-content/release-notes/val_calc.md b/_release-content/release-notes/val_calc.md index a365bd53becc9..2599c1e95238f 100644 --- a/_release-content/release-notes/val_calc.md +++ b/_release-content/release-notes/val_calc.md @@ -1,5 +1,5 @@ --- title: "Val calc" authors: ["@ickshonope"] -pull_requests: [] +pull_requests: [26409] --- \ No newline at end of file From 3f209737e9f7e3d4fa7e853e18a2c5d32e24bfde Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 3 Nov 2026 13:45:20 +0000 Subject: [PATCH 49/50] Added SAFETY comment --- crates/bevy_ui/src/layout/layout_tree.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ui/src/layout/layout_tree.rs b/crates/bevy_ui/src/layout/layout_tree.rs index 187a5027ee12d..2633eb19b9634 100644 --- a/crates/bevy_ui/src/layout/layout_tree.rs +++ b/crates/bevy_ui/src/layout/layout_tree.rs @@ -71,8 +71,9 @@ impl PercentageCalc { /// percentage `basis`. #[expect( unsafe_code, - reason = "taffy hands back the pointer it was given, which points into an `Arc` owned by the same `TaffyStyle`" + reason = "Taffy hands back the pointer it was given, which points into an `Arc` owned by the same `TaffyStyle`" )] +/// SAFETY: Taffy hands back the pointer it was given, which points into an `Arc` owned by the same `TaffyStyle` pub(crate) fn resolve_calc(ptr: *const (), basis: f32) -> f32 { let calc = unsafe { &*ptr.cast::() }; calc.resolve(basis) From 42453de2b52b617fd20aa0bb06eea9e1e88a7fc2 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 3 Nov 2026 13:46:16 +0000 Subject: [PATCH 50/50] Fixed double backtick in doc comment --- crates/bevy_ui/src/val.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ui/src/val.rs b/crates/bevy_ui/src/val.rs index 01d021e798881..5fa364328065d 100644 --- a/crates/bevy_ui/src/val.rs +++ b/crates/bevy_ui/src/val.rs @@ -519,7 +519,7 @@ impl Val { self.combine(other, ReduceOp::Max) } - /// Creates an expression that returns `self` clamped between `min`` and `max` when resolved. + /// Creates an expression that returns `self` clamped between `min` and `max` when resolved. /// If the bounds and self are all the same units, the clamped value of `self` is returned immediately. pub fn clamp(self, min: Val, max: Val) -> Val { Val::clamp_inline(&min, &self, &max).unwrap_or_else(|| CalcOp::Clamp(min, self, max).into())