#[derive(Debug, Clone, PartialEq, Default)]
pub enum Constraint {
Px(f32),
Percent(f32),
#[default]
Auto,
Vw(f32),
Vh(f32),
Vmin(f32),
Vmax(f32),
Calc(Box<CalcExpr>),
Min(Vec<Constraint>),
Max(Vec<Constraint>),
Clamp {
min: Box<Constraint>,
val: Box<Constraint>,
max: Box<Constraint>,
},
}
impl Constraint {
#[inline]
pub fn px(value: f32) -> Self {
Self::Px(value)
}
#[inline]
pub fn percent(value: f32) -> Self {
Self::Percent(value)
}
#[inline]
pub fn vw(value: f32) -> Self {
Self::Vw(value)
}
#[inline]
pub fn vh(value: f32) -> Self {
Self::Vh(value)
}
#[inline]
pub fn vmin(value: f32) -> Self {
Self::Vmin(value)
}
#[inline]
pub fn vmax(value: f32) -> Self {
Self::Vmax(value)
}
pub fn calc(expr: CalcExpr) -> Self {
Self::Calc(Box::new(expr.simplify()))
}
pub fn min(values: Vec<Constraint>) -> Self {
debug_assert!(!values.is_empty(), "min() requires at least one value");
Self::Min(values)
}
pub fn max(values: Vec<Constraint>) -> Self {
debug_assert!(!values.is_empty(), "max() requires at least one value");
Self::Max(values)
}
pub fn clamp(min: Constraint, val: Constraint, max: Constraint) -> Self {
Self::Clamp {
min: Box::new(min),
val: Box::new(val),
max: Box::new(max),
}
}
pub fn is_simple(&self) -> bool {
matches!(
self,
Self::Px(_)
| Self::Percent(_)
| Self::Auto
| Self::Vw(_)
| Self::Vh(_)
| Self::Vmin(_)
| Self::Vmax(_)
)
}
pub fn has_viewport_units(&self) -> bool {
match self {
Self::Vw(_) | Self::Vh(_) | Self::Vmin(_) | Self::Vmax(_) => true,
Self::Calc(expr) => expr.has_viewport_units(),
Self::Min(values) | Self::Max(values) => values.iter().any(|c| c.has_viewport_units()),
Self::Clamp { min, val, max } => {
min.has_viewport_units() || val.has_viewport_units() || max.has_viewport_units()
}
_ => false,
}
}
pub fn has_percentages(&self) -> bool {
match self {
Self::Percent(_) => true,
Self::Calc(expr) => expr.has_percentages(),
Self::Min(values) | Self::Max(values) => values.iter().any(|c| c.has_percentages()),
Self::Clamp { min, val, max } => {
min.has_percentages() || val.has_percentages() || max.has_percentages()
}
_ => false,
}
}
}
impl From<f32> for Constraint {
fn from(value: f32) -> Self {
Self::Px(value)
}
}
impl From<crate::length::Length> for Constraint {
fn from(length: crate::length::Length) -> Self {
match length {
crate::length::Length::Px(v) => Self::Px(v),
crate::length::Length::Percent(v) => Self::Percent(v),
crate::length::Length::Auto => Self::Auto,
crate::length::Length::Vw(v) => Self::Vw(v),
crate::length::Length::Vh(v) => Self::Vh(v),
crate::length::Length::Vmin(v) => Self::Vmin(v),
crate::length::Length::Vmax(v) => Self::Vmax(v),
}
}
}
impl From<crate::length::LengthAuto> for Constraint {
fn from(length: crate::length::LengthAuto) -> Self {
match length {
crate::length::LengthAuto::Px(v) => Self::Px(v),
crate::length::LengthAuto::Percent(v) => Self::Percent(v),
crate::length::LengthAuto::Auto => Self::Auto,
crate::length::LengthAuto::Vw(v) => Self::Vw(v),
crate::length::LengthAuto::Vh(v) => Self::Vh(v),
crate::length::LengthAuto::Vmin(v) => Self::Vmin(v),
crate::length::LengthAuto::Vmax(v) => Self::Vmax(v),
}
}
}
impl From<crate::length::LengthPercentage> for Constraint {
fn from(length: crate::length::LengthPercentage) -> Self {
match length {
crate::length::LengthPercentage::Px(v) => Self::Px(v),
crate::length::LengthPercentage::Percent(v) => Self::Percent(v),
crate::length::LengthPercentage::Vw(v) => Self::Vw(v),
crate::length::LengthPercentage::Vh(v) => Self::Vh(v),
crate::length::LengthPercentage::Vmin(v) => Self::Vmin(v),
crate::length::LengthPercentage::Vmax(v) => Self::Vmax(v),
}
}
}
impl Constraint {
pub fn to_dimension(&self) -> taffy::Dimension {
match self {
Constraint::Px(v) => taffy::Dimension::Length(*v),
Constraint::Percent(v) => taffy::Dimension::Percent(*v / 100.0),
Constraint::Auto => taffy::Dimension::Auto,
Constraint::Vw(_) | Constraint::Vh(_) | Constraint::Vmin(_) | Constraint::Vmax(_) => {
panic!(
"Viewport-relative constraints must be resolved to pixels before converting to Taffy dimension. \
Use ConstraintResolver::resolve() first."
);
}
Constraint::Calc(_)
| Constraint::Min(_)
| Constraint::Max(_)
| Constraint::Clamp { .. } => {
panic!(
"Complex constraints (calc/min/max/clamp) must be resolved to pixels before converting to Taffy dimension. \
Use ConstraintResolver::resolve() first."
);
}
}
}
pub fn try_to_dimension(&self) -> Option<taffy::Dimension> {
match self {
Constraint::Px(v) => Some(taffy::Dimension::Length(*v)),
Constraint::Percent(v) => Some(taffy::Dimension::Percent(*v / 100.0)),
Constraint::Auto => Some(taffy::Dimension::Auto),
_ => None,
}
}
pub fn try_to_length_percentage_auto(&self) -> Option<taffy::LengthPercentageAuto> {
match self {
Constraint::Px(v) => Some(taffy::LengthPercentageAuto::Length(*v)),
Constraint::Percent(v) => Some(taffy::LengthPercentageAuto::Percent(*v / 100.0)),
Constraint::Auto => Some(taffy::LengthPercentageAuto::Auto),
_ => None,
}
}
pub fn try_to_length_percentage(&self) -> Option<taffy::LengthPercentage> {
match self {
Constraint::Px(v) => Some(taffy::LengthPercentage::Length(*v)),
Constraint::Percent(v) => Some(taffy::LengthPercentage::Percent(*v / 100.0)),
_ => None,
}
}
pub fn to_length_percentage_auto(&self) -> taffy::LengthPercentageAuto {
match self {
Constraint::Px(v) => taffy::LengthPercentageAuto::Length(*v),
Constraint::Percent(v) => taffy::LengthPercentageAuto::Percent(*v / 100.0),
Constraint::Auto => taffy::LengthPercentageAuto::Auto,
Constraint::Vw(_) | Constraint::Vh(_) | Constraint::Vmin(_) | Constraint::Vmax(_) => {
panic!("Viewport-relative constraints must be resolved to pixels first.");
}
Constraint::Calc(_)
| Constraint::Min(_)
| Constraint::Max(_)
| Constraint::Clamp { .. } => {
panic!("Complex constraints must be resolved to pixels first.");
}
}
}
pub fn to_length_percentage(&self) -> taffy::LengthPercentage {
match self {
Constraint::Px(v) => taffy::LengthPercentage::Length(*v),
Constraint::Percent(v) => taffy::LengthPercentage::Percent(*v / 100.0),
Constraint::Auto => panic!("Auto is not valid for LengthPercentage"),
Constraint::Vw(_) | Constraint::Vh(_) | Constraint::Vmin(_) | Constraint::Vmax(_) => {
panic!("Viewport-relative constraints must be resolved to pixels first.");
}
Constraint::Calc(_)
| Constraint::Min(_)
| Constraint::Max(_)
| Constraint::Clamp { .. } => {
panic!("Complex constraints must be resolved to pixels first.");
}
}
}
pub fn needs_resolution(&self) -> bool {
self.try_to_dimension().is_none()
}
}
impl From<Constraint> for taffy::Dimension {
fn from(constraint: Constraint) -> Self {
constraint.to_dimension()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum CalcExpr {
Value(Constraint),
Add(Box<CalcExpr>, Box<CalcExpr>),
Sub(Box<CalcExpr>, Box<CalcExpr>),
Mul(Box<CalcExpr>, f32),
Div(Box<CalcExpr>, f32),
}
impl CalcExpr {
pub fn value(constraint: Constraint) -> Self {
Self::Value(constraint)
}
pub fn simplify(self) -> Self {
match self {
Self::Add(lhs, rhs) => {
let lhs = lhs.simplify();
let rhs = rhs.simplify();
match (&lhs, &rhs) {
(Self::Value(Constraint::Px(a)), Self::Value(Constraint::Px(b))) => {
Self::Value(Constraint::Px(a + b))
}
(Self::Value(Constraint::Px(0.0)), _) => rhs,
(_, Self::Value(Constraint::Px(0.0))) => lhs,
_ => Self::Add(Box::new(lhs), Box::new(rhs)),
}
}
Self::Sub(lhs, rhs) => {
let lhs = lhs.simplify();
let rhs = rhs.simplify();
match (&lhs, &rhs) {
(Self::Value(Constraint::Px(a)), Self::Value(Constraint::Px(b))) => {
Self::Value(Constraint::Px(a - b))
}
(_, Self::Value(Constraint::Px(0.0))) => lhs,
_ => Self::Sub(Box::new(lhs), Box::new(rhs)),
}
}
Self::Mul(expr, scalar) => {
let expr = expr.simplify();
match &expr {
Self::Value(Constraint::Px(v)) => Self::Value(Constraint::Px(v * scalar)),
_ if (scalar - 1.0).abs() < f32::EPSILON => expr,
_ if scalar.abs() < f32::EPSILON => Self::Value(Constraint::Px(0.0)),
_ => Self::Mul(Box::new(expr), scalar),
}
}
Self::Div(expr, scalar) => {
let expr = expr.simplify();
match &expr {
Self::Value(Constraint::Px(v)) => Self::Value(Constraint::Px(v / scalar)),
_ if (scalar - 1.0).abs() < f32::EPSILON => expr,
_ => Self::Div(Box::new(expr), scalar),
}
}
Self::Value(c) => Self::Value(c),
}
}
pub fn has_viewport_units(&self) -> bool {
match self {
Self::Value(c) => c.has_viewport_units(),
Self::Add(lhs, rhs) | Self::Sub(lhs, rhs) => {
lhs.has_viewport_units() || rhs.has_viewport_units()
}
Self::Mul(expr, _) | Self::Div(expr, _) => expr.has_viewport_units(),
}
}
pub fn has_percentages(&self) -> bool {
match self {
Self::Value(c) => c.has_percentages(),
Self::Add(lhs, rhs) | Self::Sub(lhs, rhs) => {
lhs.has_percentages() || rhs.has_percentages()
}
Self::Mul(expr, _) | Self::Div(expr, _) => expr.has_percentages(),
}
}
}
impl From<Constraint> for CalcExpr {
fn from(constraint: Constraint) -> Self {
Self::Value(constraint)
}
}
impl From<f32> for CalcExpr {
fn from(value: f32) -> Self {
Self::Value(Constraint::Px(value))
}
}
impl std::ops::Add for CalcExpr {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self::Add(Box::new(self), Box::new(rhs))
}
}
impl std::ops::Sub for CalcExpr {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
Self::Sub(Box::new(self), Box::new(rhs))
}
}
impl std::ops::Mul<f32> for CalcExpr {
type Output = Self;
fn mul(self, rhs: f32) -> Self::Output {
Self::Mul(Box::new(self), rhs)
}
}
impl std::ops::Div<f32> for CalcExpr {
type Output = Self;
fn div(self, rhs: f32) -> Self::Output {
Self::Div(Box::new(self), rhs)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_constraint_constructors() {
assert_eq!(Constraint::px(100.0), Constraint::Px(100.0));
assert_eq!(Constraint::percent(50.0), Constraint::Percent(50.0));
assert_eq!(Constraint::vw(80.0), Constraint::Vw(80.0));
assert_eq!(Constraint::vh(60.0), Constraint::Vh(60.0));
}
#[test]
fn test_constraint_is_simple() {
assert!(Constraint::Px(100.0).is_simple());
assert!(Constraint::Percent(50.0).is_simple());
assert!(Constraint::Auto.is_simple());
assert!(Constraint::Vw(50.0).is_simple());
let calc = Constraint::calc(CalcExpr::Value(Constraint::Px(100.0)));
assert!(!calc.is_simple());
let min = Constraint::min(vec![Constraint::Px(100.0)]);
assert!(!min.is_simple());
}
#[test]
fn test_constraint_has_viewport_units() {
assert!(!Constraint::Px(100.0).has_viewport_units());
assert!(!Constraint::Percent(50.0).has_viewport_units());
assert!(Constraint::Vw(50.0).has_viewport_units());
assert!(Constraint::Vh(50.0).has_viewport_units());
assert!(Constraint::Vmin(50.0).has_viewport_units());
assert!(Constraint::Vmax(50.0).has_viewport_units());
let calc = Constraint::calc(CalcExpr::Value(Constraint::Vw(50.0)));
assert!(calc.has_viewport_units());
let min = Constraint::min(vec![Constraint::Px(100.0), Constraint::Vw(50.0)]);
assert!(min.has_viewport_units());
}
#[test]
fn test_calc_expr_simplify_add() {
let expr = CalcExpr::Add(
Box::new(CalcExpr::Value(Constraint::Px(10.0))),
Box::new(CalcExpr::Value(Constraint::Px(20.0))),
);
assert_eq!(expr.simplify(), CalcExpr::Value(Constraint::Px(30.0)));
let expr = CalcExpr::Add(
Box::new(CalcExpr::Value(Constraint::Px(0.0))),
Box::new(CalcExpr::Value(Constraint::Percent(50.0))),
);
assert_eq!(expr.simplify(), CalcExpr::Value(Constraint::Percent(50.0)));
}
#[test]
fn test_calc_expr_simplify_sub() {
let expr = CalcExpr::Sub(
Box::new(CalcExpr::Value(Constraint::Px(30.0))),
Box::new(CalcExpr::Value(Constraint::Px(10.0))),
);
assert_eq!(expr.simplify(), CalcExpr::Value(Constraint::Px(20.0)));
}
#[test]
fn test_calc_expr_simplify_mul() {
let expr = CalcExpr::Mul(Box::new(CalcExpr::Value(Constraint::Px(10.0))), 3.0);
assert_eq!(expr.simplify(), CalcExpr::Value(Constraint::Px(30.0)));
let expr = CalcExpr::Mul(Box::new(CalcExpr::Value(Constraint::Percent(50.0))), 1.0);
assert_eq!(expr.simplify(), CalcExpr::Value(Constraint::Percent(50.0)));
}
#[test]
fn test_calc_expr_operators() {
let a = CalcExpr::from(Constraint::Percent(100.0));
let b = CalcExpr::from(Constraint::Px(40.0));
let result = (a - b).simplify();
match result {
CalcExpr::Sub(lhs, rhs) => {
assert_eq!(*lhs, CalcExpr::Value(Constraint::Percent(100.0)));
assert_eq!(*rhs, CalcExpr::Value(Constraint::Px(40.0)));
}
_ => panic!("Expected Sub expression"),
}
}
}