diff options
Diffstat (limited to 'servo/components/style/values/generics/calc.rs')
-rw-r--r-- | servo/components/style/values/generics/calc.rs | 1820 |
1 files changed, 1820 insertions, 0 deletions
diff --git a/servo/components/style/values/generics/calc.rs b/servo/components/style/values/generics/calc.rs new file mode 100644 index 0000000000..abcb5fe6eb --- /dev/null +++ b/servo/components/style/values/generics/calc.rs @@ -0,0 +1,1820 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! [Calc expressions][calc]. +//! +//! [calc]: https://drafts.csswg.org/css-values/#calc-notation + +use num_traits::Zero; +use smallvec::SmallVec; +use std::fmt::{self, Write}; +use std::ops::{Add, Mul, Neg, Rem, Sub}; +use std::{cmp, mem}; +use style_traits::{CssWriter, ToCss}; + +/// Whether we're a `min` or `max` function. +#[derive( + Clone, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + ToAnimatedZero, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum MinMaxOp { + /// `min()` + Min, + /// `max()` + Max, +} + +/// Whether we're a `mod` or `rem` function. +#[derive( + Clone, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + ToAnimatedZero, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum ModRemOp { + /// `mod()` + Mod, + /// `rem()` + Rem, +} + +impl ModRemOp { + fn apply(self, dividend: f32, divisor: f32) -> f32 { + // In mod(A, B) only, if B is infinite and A has opposite sign to B + // (including an oppositely-signed zero), the result is NaN. + // https://drafts.csswg.org/css-values/#round-infinities + if matches!(self, Self::Mod) && + divisor.is_infinite() && + dividend.is_sign_negative() != divisor.is_sign_negative() + { + return f32::NAN; + } + + let (r, same_sign_as) = match self { + Self::Mod => (dividend - divisor * (dividend / divisor).floor(), divisor), + Self::Rem => (dividend - divisor * (dividend / divisor).trunc(), dividend), + }; + if r == 0.0 && same_sign_as.is_sign_negative() { + -0.0 + } else { + r + } + } +} + +/// The strategy used in `round()` +#[derive( + Clone, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + ToAnimatedZero, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum RoundingStrategy { + /// `round(nearest, a, b)` + /// round a to the nearest multiple of b + Nearest, + /// `round(up, a, b)` + /// round a up to the nearest multiple of b + Up, + /// `round(down, a, b)` + /// round a down to the nearest multiple of b + Down, + /// `round(to-zero, a, b)` + /// round a to the nearest multiple of b that is towards zero + ToZero, +} + +/// This determines the order in which we serialize members of a calc() sum. +/// +/// See https://drafts.csswg.org/css-values-4/#sort-a-calculations-children +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +#[allow(missing_docs)] +pub enum SortKey { + Number, + Percentage, + Cap, + Ch, + Cqb, + Cqh, + Cqi, + Cqmax, + Cqmin, + Cqw, + Deg, + Dppx, + Dvb, + Dvh, + Dvi, + Dvmax, + Dvmin, + Dvw, + Em, + Ex, + Ic, + Lh, + Lvb, + Lvh, + Lvi, + Lvmax, + Lvmin, + Lvw, + Px, + Rem, + Rlh, + Sec, + Svb, + Svh, + Svi, + Svmax, + Svmin, + Svw, + Vb, + Vh, + Vi, + Vmax, + Vmin, + Vw, + Other, +} + +/// A generic node in a calc expression. +/// +/// FIXME: This would be much more elegant if we used `Self` in the types below, +/// but we can't because of https://github.com/serde-rs/serde/issues/1565. +/// +/// FIXME: The following annotations are to workaround an LLVM inlining bug, see +/// bug 1631929. +/// +/// cbindgen:destructor-attributes=MOZ_NEVER_INLINE +/// cbindgen:copy-constructor-attributes=MOZ_NEVER_INLINE +/// cbindgen:eq-attributes=MOZ_NEVER_INLINE +#[repr(u8)] +#[derive( + Clone, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + ToAnimatedZero, + ToResolvedValue, + ToShmem, +)] +pub enum GenericCalcNode<L> { + /// A leaf node. + Leaf(L), + /// A node that negates its child, e.g. Negate(1) == -1. + Negate(Box<GenericCalcNode<L>>), + /// A node that inverts its child, e.g. Invert(10) == 1 / 10 == 0.1. The child must always + /// resolve to a number unit. + Invert(Box<GenericCalcNode<L>>), + /// A sum node, representing `a + b + c` where a, b, and c are the + /// arguments. + Sum(crate::OwnedSlice<GenericCalcNode<L>>), + /// A product node, representing `a * b * c` where a, b, and c are the + /// arguments. + Product(crate::OwnedSlice<GenericCalcNode<L>>), + /// A `min` or `max` function. + MinMax(crate::OwnedSlice<GenericCalcNode<L>>, MinMaxOp), + /// A `clamp()` function. + Clamp { + /// The minimum value. + min: Box<GenericCalcNode<L>>, + /// The central value. + center: Box<GenericCalcNode<L>>, + /// The maximum value. + max: Box<GenericCalcNode<L>>, + }, + /// A `round()` function. + Round { + /// The rounding strategy. + strategy: RoundingStrategy, + /// The value to round. + value: Box<GenericCalcNode<L>>, + /// The step value. + step: Box<GenericCalcNode<L>>, + }, + /// A `mod()` or `rem()` function. + ModRem { + /// The dividend calculation. + dividend: Box<GenericCalcNode<L>>, + /// The divisor calculation. + divisor: Box<GenericCalcNode<L>>, + /// Is the function mod or rem? + op: ModRemOp, + }, + /// A `hypot()` function + Hypot(crate::OwnedSlice<GenericCalcNode<L>>), + /// An `abs()` function. + Abs(Box<GenericCalcNode<L>>), + /// A `sign()` function. + Sign(Box<GenericCalcNode<L>>), +} + +pub use self::GenericCalcNode as CalcNode; + +bitflags! { + /// Expected units we allow parsing within a `calc()` expression. + /// + /// This is used as a hint for the parser to fast-reject invalid + /// expressions. Numbers are always allowed because they multiply other + /// units. + #[derive(Clone, Copy, PartialEq, Eq)] + pub struct CalcUnits: u8 { + /// <length> + const LENGTH = 1 << 0; + /// <percentage> + const PERCENTAGE = 1 << 1; + /// <angle> + const ANGLE = 1 << 2; + /// <time> + const TIME = 1 << 3; + /// <resolution> + const RESOLUTION = 1 << 4; + + /// <length-percentage> + const LENGTH_PERCENTAGE = Self::LENGTH.bits() | Self::PERCENTAGE.bits(); + // NOTE: When you add to this, make sure to make Atan2 deal with these. + /// Allow all units. + const ALL = Self::LENGTH.bits() | Self::PERCENTAGE.bits() | Self::ANGLE.bits() | Self::TIME.bits() | Self::RESOLUTION.bits(); + } +} + +impl CalcUnits { + /// Returns whether the flags only represent a single unit. This will return true for 0, which + /// is a "number" this is also fine. + #[inline] + fn is_single_unit(&self) -> bool { + self.bits() == 0 || self.bits() & (self.bits() - 1) == 0 + } + + /// Returns true if this unit is allowed to be summed with the given unit, otherwise false. + #[inline] + fn can_sum_with(&self, other: Self) -> bool { + match *self { + Self::LENGTH => other.intersects(Self::LENGTH | Self::PERCENTAGE), + Self::PERCENTAGE => other.intersects(Self::LENGTH | Self::PERCENTAGE), + Self::LENGTH_PERCENTAGE => other.intersects(Self::LENGTH | Self::PERCENTAGE), + u => u.is_single_unit() && other == u, + } + } +} + +/// For percentage resolution, sometimes we can't assume that the percentage basis is positive (so +/// we don't know whether a percentage is larger than another). +pub enum PositivePercentageBasis { + /// The percent basis is not known-positive, we can't compare percentages. + Unknown, + /// The percent basis is known-positive, we assume larger percentages are larger. + Yes, +} + +macro_rules! compare_helpers { + () => { + /// Return whether a leaf is greater than another. + #[allow(unused)] + fn gt(&self, other: &Self, basis_positive: PositivePercentageBasis) -> bool { + self.compare(other, basis_positive) == Some(cmp::Ordering::Greater) + } + + /// Return whether a leaf is less than another. + fn lt(&self, other: &Self, basis_positive: PositivePercentageBasis) -> bool { + self.compare(other, basis_positive) == Some(cmp::Ordering::Less) + } + + /// Return whether a leaf is smaller or equal than another. + fn lte(&self, other: &Self, basis_positive: PositivePercentageBasis) -> bool { + match self.compare(other, basis_positive) { + Some(cmp::Ordering::Less) => true, + Some(cmp::Ordering::Equal) => true, + Some(cmp::Ordering::Greater) => false, + None => false, + } + } + }; +} + +/// A trait that represents all the stuff a valid leaf of a calc expression. +pub trait CalcNodeLeaf: Clone + Sized + PartialEq + ToCss { + /// Returns the unit of the leaf. + fn unit(&self) -> CalcUnits; + + /// Returns the unitless value of this leaf. + fn unitless_value(&self) -> f32; + + /// Return true if the units of both leaves are equal. (NOTE: Does not take + /// the values into account) + fn is_same_unit_as(&self, other: &Self) -> bool { + std::mem::discriminant(self) == std::mem::discriminant(other) + } + + /// Do a partial comparison of these values. + fn compare( + &self, + other: &Self, + base_is_positive: PositivePercentageBasis, + ) -> Option<cmp::Ordering>; + compare_helpers!(); + + /// Create a new leaf with a number value. + fn new_number(value: f32) -> Self; + + /// Returns a float value if the leaf is a number. + fn as_number(&self) -> Option<f32>; + + /// Whether this value is known-negative. + fn is_negative(&self) -> bool { + self.unitless_value().is_sign_negative() + } + + /// Whether this value is infinite. + fn is_infinite(&self) -> bool { + self.unitless_value().is_infinite() + } + + /// Whether this value is zero. + fn is_zero(&self) -> bool { + self.unitless_value().is_zero() + } + + /// Whether this value is NaN. + fn is_nan(&self) -> bool { + self.unitless_value().is_nan() + } + + /// Tries to merge one leaf into another using the sum, that is, perform `x` + `y`. + fn try_sum_in_place(&mut self, other: &Self) -> Result<(), ()>; + + /// Try to merge the right leaf into the left by using a multiplication. Return true if the + /// merge was successful, otherwise false. + fn try_product_in_place(&mut self, other: &mut Self) -> bool; + + /// Tries a generic arithmetic operation. + fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()> + where + O: Fn(f32, f32) -> f32; + + /// Map the value of this node with the given operation. + fn map(&mut self, op: impl FnMut(f32) -> f32); + + /// Negates the leaf. + fn negate(&mut self) { + self.map(std::ops::Neg::neg); + } + + /// Canonicalizes the expression if necessary. + fn simplify(&mut self); + + /// Returns the sort key for simplification. + fn sort_key(&self) -> SortKey; + + /// Create a new leaf containing the sign() result of the given leaf. + fn sign_from(leaf: &impl CalcNodeLeaf) -> Self { + Self::new_number(if leaf.is_nan() { + f32::NAN + } else if leaf.is_zero() { + leaf.unitless_value() + } else if leaf.is_negative() { + -1.0 + } else { + 1.0 + }) + } +} + +/// The level of any argument being serialized in `to_css_impl`. +enum ArgumentLevel { + /// The root of a calculation tree. + CalculationRoot, + /// The root of an operand node's argument, e.g. `min(10, 20)`, `10` and `20` will have this + /// level, but min in this case will have `TopMost`. + ArgumentRoot, + /// Any other values serialized in the tree. + Nested, +} + +impl<L: CalcNodeLeaf> CalcNode<L> { + /// Create a dummy CalcNode that can be used to do replacements of other nodes. + fn dummy() -> Self { + Self::MinMax(Default::default(), MinMaxOp::Max) + } + + /// Change all the leaf nodes to have the given value. This is useful when + /// you have `calc(1px * nan)` and you want to replace the product node with + /// `calc(nan)`, in which case the unit will be retained. + fn coerce_to_value(&mut self, value: f32) { + self.map(|_| value); + } + + /// Return true if a product is distributive over this node. + /// Is distributive: (2 + 3) * 4 = 8 + 12 + /// Not distributive: sign(2 + 3) * 4 != sign(8 + 12) + #[inline] + pub fn is_product_distributive(&self) -> bool { + match self { + Self::Leaf(_) => true, + Self::Sum(children) => children.iter().all(|c| c.is_product_distributive()), + _ => false, + } + } + + /// If the node has a valid unit outcome, then return it, otherwise fail. + pub fn unit(&self) -> Result<CalcUnits, ()> { + Ok(match self { + CalcNode::Leaf(l) => l.unit(), + CalcNode::Negate(child) | CalcNode::Invert(child) | CalcNode::Abs(child) => { + child.unit()? + }, + CalcNode::Sum(children) => { + let mut unit = children.first().unwrap().unit()?; + for child in children.iter().skip(1) { + let child_unit = child.unit()?; + if !child_unit.can_sum_with(unit) { + return Err(()); + } + unit |= child_unit; + } + unit + }, + CalcNode::Product(children) => { + // Only one node is allowed to have a unit, the rest must be numbers. + let mut unit = None; + for child in children.iter() { + let child_unit = child.unit()?; + if child_unit.is_empty() { + // Numbers are always allowed in a product, so continue with the next. + continue; + } + + if unit.is_some() { + // We already have a unit for the node, so another unit node is invalid. + return Err(()); + } + + // We have the unit for the node. + unit = Some(child_unit); + } + // We only keep track of specified units, so if we end up with a None and no failure + // so far, then we have a number. + unit.unwrap_or(CalcUnits::empty()) + }, + CalcNode::MinMax(children, _) | CalcNode::Hypot(children) => { + let mut unit = children.first().unwrap().unit()?; + for child in children.iter().skip(1) { + let child_unit = child.unit()?; + if !child_unit.can_sum_with(unit) { + return Err(()); + } + unit |= child_unit; + } + unit + }, + CalcNode::Clamp { min, center, max } => { + let min_unit = min.unit()?; + let center_unit = center.unit()?; + + if !min_unit.can_sum_with(center_unit) { + return Err(()); + } + + let max_unit = max.unit()?; + + if !center_unit.can_sum_with(max_unit) { + return Err(()); + } + + min_unit | center_unit | max_unit + }, + CalcNode::Round { value, step, .. } => { + let value_unit = value.unit()?; + let step_unit = step.unit()?; + if !step_unit.can_sum_with(value_unit) { + return Err(()); + } + value_unit | step_unit + }, + CalcNode::ModRem { + dividend, divisor, .. + } => { + let dividend_unit = dividend.unit()?; + let divisor_unit = divisor.unit()?; + if !divisor_unit.can_sum_with(dividend_unit) { + return Err(()); + } + dividend_unit | divisor_unit + }, + CalcNode::Sign(ref child) => { + // sign() always resolves to a number, but we still need to make sure that the + // child units make sense. + let _ = child.unit()?; + CalcUnits::empty() + }, + }) + } + + /// Negate the node inline. If the node is distributive, it is replaced by the result, + /// otherwise the node is wrapped in a [`Negate`] node. + pub fn negate(&mut self) { + /// Node(params) -> Negate(Node(params)) + fn wrap_self_in_negate<L: CalcNodeLeaf>(s: &mut CalcNode<L>) { + let result = mem::replace(s, CalcNode::dummy()); + *s = CalcNode::Negate(Box::new(result)); + } + + match *self { + CalcNode::Leaf(ref mut leaf) => leaf.negate(), + CalcNode::Negate(ref mut value) => { + // Don't negate the value here. Replace `self` with it's child. + let result = mem::replace(value.as_mut(), Self::dummy()); + *self = result; + }, + CalcNode::Invert(_) => { + // -(1 / -10) == -(-0.1) == 0.1 + wrap_self_in_negate(self) + }, + CalcNode::Sum(ref mut children) => { + for child in children.iter_mut() { + child.negate(); + } + }, + CalcNode::Product(_) => { + // -(2 * 3 / 4) == -(1.5) + wrap_self_in_negate(self); + }, + CalcNode::MinMax(ref mut children, ref mut op) => { + for child in children.iter_mut() { + child.negate(); + } + + // Negating min-max means the operation is swapped. + *op = match *op { + MinMaxOp::Min => MinMaxOp::Max, + MinMaxOp::Max => MinMaxOp::Min, + }; + }, + CalcNode::Clamp { + ref mut min, + ref mut center, + ref mut max, + } => { + if min.lte(max, PositivePercentageBasis::Unknown) { + min.negate(); + center.negate(); + max.negate(); + + mem::swap(min, max); + } else { + wrap_self_in_negate(self); + } + }, + CalcNode::Round { + ref mut strategy, + ref mut value, + ref mut step, + } => { + match *strategy { + RoundingStrategy::Nearest => { + // Nearest is tricky because we'd have to swap the + // behavior at the half-way point from using the upper + // to lower bound. + // Simpler to just wrap self in a negate node. + wrap_self_in_negate(self); + return; + }, + RoundingStrategy::Up => *strategy = RoundingStrategy::Down, + RoundingStrategy::Down => *strategy = RoundingStrategy::Up, + RoundingStrategy::ToZero => (), + } + value.negate(); + step.negate(); + }, + CalcNode::ModRem { + ref mut dividend, + ref mut divisor, + .. + } => { + dividend.negate(); + divisor.negate(); + }, + CalcNode::Hypot(ref mut children) => { + for child in children.iter_mut() { + child.negate(); + } + }, + CalcNode::Abs(_) => { + wrap_self_in_negate(self); + }, + CalcNode::Sign(ref mut child) => { + child.negate(); + }, + } + } + + fn sort_key(&self) -> SortKey { + match *self { + Self::Leaf(ref l) => l.sort_key(), + _ => SortKey::Other, + } + } + + /// Returns the leaf if we can (if simplification has allowed it). + pub fn as_leaf(&self) -> Option<&L> { + match *self { + Self::Leaf(ref l) => Some(l), + _ => None, + } + } + + /// Tries to merge one node into another using the sum, that is, perform `x` + `y`. + fn try_sum_in_place(&mut self, other: &Self) -> Result<(), ()> { + match (self, other) { + (&mut CalcNode::Leaf(ref mut one), &CalcNode::Leaf(ref other)) => { + one.try_sum_in_place(other) + }, + _ => Err(()), + } + } + + /// Tries to merge one node into another using the product, that is, perform `x` * `y`. + pub fn try_product_in_place(&mut self, other: &mut Self) -> bool { + if let Ok(resolved) = other.resolve() { + if let Some(number) = resolved.as_number() { + if number == 1.0 { + return true; + } + + if self.is_product_distributive() { + self.map(|v| v * number); + return true; + } + } + } + + if let Ok(resolved) = self.resolve() { + if let Some(number) = resolved.as_number() { + if number == 1.0 { + std::mem::swap(self, other); + return true; + } + + if other.is_product_distributive() { + other.map(|v| v * number); + std::mem::swap(self, other); + return true; + } + } + } + + false + } + + /// Tries to apply a generic arithmetic operator + fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()> + where + O: Fn(f32, f32) -> f32, + { + match (self, other) { + (&CalcNode::Leaf(ref one), &CalcNode::Leaf(ref other)) => { + Ok(CalcNode::Leaf(one.try_op(other, op)?)) + }, + _ => Err(()), + } + } + + /// Map the value of this node with the given operation. + pub fn map(&mut self, mut op: impl FnMut(f32) -> f32) { + fn map_internal<L: CalcNodeLeaf>(node: &mut CalcNode<L>, op: &mut impl FnMut(f32) -> f32) { + match node { + CalcNode::Leaf(l) => l.map(op), + CalcNode::Negate(v) | CalcNode::Invert(v) => map_internal(v, op), + CalcNode::Sum(children) | CalcNode::Product(children) => { + for node in &mut **children { + map_internal(node, op); + } + }, + CalcNode::MinMax(children, _) => { + for node in &mut **children { + map_internal(node, op); + } + }, + CalcNode::Clamp { min, center, max } => { + map_internal(min, op); + map_internal(center, op); + map_internal(max, op); + }, + CalcNode::Round { value, step, .. } => { + map_internal(value, op); + map_internal(step, op); + }, + CalcNode::ModRem { + dividend, divisor, .. + } => { + map_internal(dividend, op); + map_internal(divisor, op); + }, + CalcNode::Hypot(children) => { + for node in &mut **children { + map_internal(node, op); + } + }, + CalcNode::Abs(child) | CalcNode::Sign(child) => { + map_internal(child, op); + }, + } + } + + map_internal(self, &mut op); + } + + /// Convert this `CalcNode` into a `CalcNode` with a different leaf kind. + pub fn map_leaves<O, F>(&self, mut map: F) -> CalcNode<O> + where + O: CalcNodeLeaf, + F: FnMut(&L) -> O, + { + self.map_leaves_internal(&mut map) + } + + fn map_leaves_internal<O, F>(&self, map: &mut F) -> CalcNode<O> + where + O: CalcNodeLeaf, + F: FnMut(&L) -> O, + { + fn map_children<L, O, F>( + children: &[CalcNode<L>], + map: &mut F, + ) -> crate::OwnedSlice<CalcNode<O>> + where + L: CalcNodeLeaf, + O: CalcNodeLeaf, + F: FnMut(&L) -> O, + { + children + .iter() + .map(|c| c.map_leaves_internal(map)) + .collect() + } + + match *self { + Self::Leaf(ref l) => CalcNode::Leaf(map(l)), + Self::Negate(ref c) => CalcNode::Negate(Box::new(c.map_leaves_internal(map))), + Self::Invert(ref c) => CalcNode::Invert(Box::new(c.map_leaves_internal(map))), + Self::Sum(ref c) => CalcNode::Sum(map_children(c, map)), + Self::Product(ref c) => CalcNode::Product(map_children(c, map)), + Self::MinMax(ref c, op) => CalcNode::MinMax(map_children(c, map), op), + Self::Clamp { + ref min, + ref center, + ref max, + } => { + let min = Box::new(min.map_leaves_internal(map)); + let center = Box::new(center.map_leaves_internal(map)); + let max = Box::new(max.map_leaves_internal(map)); + CalcNode::Clamp { min, center, max } + }, + Self::Round { + strategy, + ref value, + ref step, + } => { + let value = Box::new(value.map_leaves_internal(map)); + let step = Box::new(step.map_leaves_internal(map)); + CalcNode::Round { + strategy, + value, + step, + } + }, + Self::ModRem { + ref dividend, + ref divisor, + op, + } => { + let dividend = Box::new(dividend.map_leaves_internal(map)); + let divisor = Box::new(divisor.map_leaves_internal(map)); + CalcNode::ModRem { + dividend, + divisor, + op, + } + }, + Self::Hypot(ref c) => CalcNode::Hypot(map_children(c, map)), + Self::Abs(ref c) => CalcNode::Abs(Box::new(c.map_leaves_internal(map))), + Self::Sign(ref c) => CalcNode::Sign(Box::new(c.map_leaves_internal(map))), + } + } + + /// Resolve this node into a value. + pub fn resolve(&self) -> Result<L, ()> { + self.resolve_map(|l| Ok(l.clone())) + } + + /// Resolve this node into a value, given a function that maps the leaf values. + pub fn resolve_map<F>(&self, mut leaf_to_output_fn: F) -> Result<L, ()> + where + F: FnMut(&L) -> Result<L, ()>, + { + self.resolve_internal(&mut leaf_to_output_fn) + } + + fn resolve_internal<F>(&self, leaf_to_output_fn: &mut F) -> Result<L, ()> + where + F: FnMut(&L) -> Result<L, ()>, + { + match self { + Self::Leaf(l) => leaf_to_output_fn(l), + Self::Negate(child) => { + let mut result = child.resolve_internal(leaf_to_output_fn)?; + result.map(|v| v.neg()); + Ok(result) + }, + Self::Invert(child) => { + let mut result = child.resolve_internal(leaf_to_output_fn)?; + result.map(|v| 1.0 / v); + Ok(result) + }, + Self::Sum(children) => { + let mut result = children[0].resolve_internal(leaf_to_output_fn)?; + + for child in children.iter().skip(1) { + let right = child.resolve_internal(leaf_to_output_fn)?; + // try_op will make sure we only sum leaves with the same type. + result = result.try_op(&right, |left, right| left + right)?; + } + + Ok(result) + }, + Self::Product(children) => { + let mut result = children[0].resolve_internal(leaf_to_output_fn)?; + + for child in children.iter().skip(1) { + let right = child.resolve_internal(leaf_to_output_fn)?; + // Mutliply only allowed when either side is a number. + match result.as_number() { + Some(left) => { + // Left side is a number, so we use the right node as the result. + result = right; + result.map(|v| v * left); + }, + None => { + // Left side is not a number, so check if the right side is. + match right.as_number() { + Some(right) => { + result.map(|v| v * right); + }, + None => { + // Multiplying with both sides having units. + return Err(()); + }, + } + }, + } + } + + Ok(result) + }, + Self::MinMax(children, op) => { + let mut result = children[0].resolve_internal(leaf_to_output_fn)?; + + if result.is_nan() { + return Ok(result); + } + + for child in children.iter().skip(1) { + let candidate = child.resolve_internal(leaf_to_output_fn)?; + + // Leave types must match for each child. + if !result.is_same_unit_as(&candidate) { + return Err(()); + } + + if candidate.is_nan() { + result = candidate; + break; + } + + let candidate_wins = match op { + MinMaxOp::Min => candidate.lt(&result, PositivePercentageBasis::Yes), + MinMaxOp::Max => candidate.gt(&result, PositivePercentageBasis::Yes), + }; + + if candidate_wins { + result = candidate; + } + } + + Ok(result) + }, + Self::Clamp { min, center, max } => { + let min = min.resolve_internal(leaf_to_output_fn)?; + let center = center.resolve_internal(leaf_to_output_fn)?; + let max = max.resolve_internal(leaf_to_output_fn)?; + + if !min.is_same_unit_as(¢er) || !max.is_same_unit_as(¢er) { + return Err(()); + } + + if min.is_nan() { + return Ok(min); + } + + if center.is_nan() { + return Ok(center); + } + + if max.is_nan() { + return Ok(max); + } + + let mut result = center; + if result.gt(&max, PositivePercentageBasis::Yes) { + result = max; + } + if result.lt(&min, PositivePercentageBasis::Yes) { + result = min + } + + Ok(result) + }, + Self::Round { + strategy, + value, + step, + } => { + let mut value = value.resolve_internal(leaf_to_output_fn)?; + let step = step.resolve_internal(leaf_to_output_fn)?; + + if !value.is_same_unit_as(&step) { + return Err(()); + } + + let step = step.unitless_value().abs(); + + value.map(|value| { + // TODO(emilio): Seems like at least a few of these + // special-cases could be removed if we do the math in a + // particular order. + if step.is_zero() { + return f32::NAN; + } + + if value.is_infinite() { + if step.is_infinite() { + return f32::NAN; + } + return value; + } + + if step.is_infinite() { + match strategy { + RoundingStrategy::Nearest | RoundingStrategy::ToZero => { + return if value.is_sign_negative() { -0.0 } else { 0.0 } + }, + RoundingStrategy::Up => { + return if !value.is_sign_negative() && !value.is_zero() { + f32::INFINITY + } else if !value.is_sign_negative() && value.is_zero() { + value + } else { + -0.0 + } + }, + RoundingStrategy::Down => { + return if value.is_sign_negative() && !value.is_zero() { + -f32::INFINITY + } else if value.is_sign_negative() && value.is_zero() { + value + } else { + 0.0 + } + }, + } + } + + let div = value / step; + let lower_bound = div.floor() * step; + let upper_bound = div.ceil() * step; + + match strategy { + RoundingStrategy::Nearest => { + // In case of a tie, use the upper bound + if value - lower_bound < upper_bound - value { + lower_bound + } else { + upper_bound + } + }, + RoundingStrategy::Up => upper_bound, + RoundingStrategy::Down => lower_bound, + RoundingStrategy::ToZero => { + // In case of a tie, use the upper bound + if lower_bound.abs() < upper_bound.abs() { + lower_bound + } else { + upper_bound + } + }, + } + }); + + Ok(value) + }, + Self::ModRem { + dividend, + divisor, + op, + } => { + let mut dividend = dividend.resolve_internal(leaf_to_output_fn)?; + let divisor = divisor.resolve_internal(leaf_to_output_fn)?; + + if !dividend.is_same_unit_as(&divisor) { + return Err(()); + } + + let divisor = divisor.unitless_value(); + dividend.map(|dividend| op.apply(dividend, divisor)); + Ok(dividend) + }, + Self::Hypot(children) => { + let mut result = children[0].resolve_internal(leaf_to_output_fn)?; + result.map(|v| v.powi(2)); + + for child in children.iter().skip(1) { + let child_value = child.resolve_internal(leaf_to_output_fn)?; + + if !result.is_same_unit_as(&child_value) { + return Err(()); + } + + result.map(|v| v + child_value.unitless_value().powi(2)); + } + + result.map(|v| v.sqrt()); + Ok(result) + }, + Self::Abs(ref c) => { + let mut result = c.resolve_internal(leaf_to_output_fn)?; + + result.map(|v| v.abs()); + + Ok(result) + }, + Self::Sign(ref c) => { + let result = c.resolve_internal(leaf_to_output_fn)?; + Ok(L::sign_from(&result)) + }, + } + } + + fn is_negative_leaf(&self) -> bool { + match *self { + Self::Leaf(ref l) => l.is_negative(), + _ => false, + } + } + + fn is_zero_leaf(&self) -> bool { + match *self { + Self::Leaf(ref l) => l.is_zero(), + _ => false, + } + } + + fn is_infinite_leaf(&self) -> bool { + match *self { + Self::Leaf(ref l) => l.is_infinite(), + _ => false, + } + } + + /// Visits all the nodes in this calculation tree recursively, starting by + /// the leaves and bubbling all the way up. + /// + /// This is useful for simplification, but can also be used for validation + /// and such. + pub fn visit_depth_first(&mut self, mut f: impl FnMut(&mut Self)) { + self.visit_depth_first_internal(&mut f) + } + + fn visit_depth_first_internal(&mut self, f: &mut impl FnMut(&mut Self)) { + match *self { + Self::Clamp { + ref mut min, + ref mut center, + ref mut max, + } => { + min.visit_depth_first_internal(f); + center.visit_depth_first_internal(f); + max.visit_depth_first_internal(f); + }, + Self::Round { + ref mut value, + ref mut step, + .. + } => { + value.visit_depth_first_internal(f); + step.visit_depth_first_internal(f); + }, + Self::ModRem { + ref mut dividend, + ref mut divisor, + .. + } => { + dividend.visit_depth_first_internal(f); + divisor.visit_depth_first_internal(f); + }, + Self::Sum(ref mut children) | + Self::Product(ref mut children) | + Self::MinMax(ref mut children, _) | + Self::Hypot(ref mut children) => { + for child in &mut **children { + child.visit_depth_first_internal(f); + } + }, + Self::Negate(ref mut value) | Self::Invert(ref mut value) => { + value.visit_depth_first_internal(f); + }, + Self::Abs(ref mut value) | Self::Sign(ref mut value) => { + value.visit_depth_first_internal(f); + }, + Self::Leaf(..) => {}, + } + f(self); + } + + /// This function simplifies and sorts the calculation of the specified node. It simplifies + /// directly nested nodes while assuming that all nodes below it have already been simplified. + /// It is recommended to use this function in combination with `visit_depth_first()`. + /// + /// This function is necessary only if the node needs to be preserved after parsing, + /// specifically for `<length-percentage>` cases where the calculation contains percentages or + /// relative units. Otherwise, the node can be evaluated using `resolve()`, which will + /// automatically provide a simplified value. + /// + /// <https://drafts.csswg.org/css-values-4/#calc-simplification> + pub fn simplify_and_sort_direct_children(&mut self) { + macro_rules! replace_self_with { + ($slot:expr) => {{ + let result = mem::replace($slot, Self::dummy()); + *self = result; + }}; + } + + macro_rules! value_or_stop { + ($op:expr) => {{ + match $op { + Ok(value) => value, + Err(_) => return, + } + }}; + } + + match *self { + Self::Clamp { + ref mut min, + ref mut center, + ref mut max, + } => { + // NOTE: clamp() is max(min, min(center, max)) + let min_cmp_center = match min.compare(¢er, PositivePercentageBasis::Unknown) { + Some(o) => o, + None => return, + }; + + // So if we can prove that min is more than center, then we won, + // as that's what we should always return. + if matches!(min_cmp_center, cmp::Ordering::Greater) { + replace_self_with!(&mut **min); + return; + } + + // Otherwise try with max. + let max_cmp_center = match max.compare(¢er, PositivePercentageBasis::Unknown) { + Some(o) => o, + None => return, + }; + + if matches!(max_cmp_center, cmp::Ordering::Less) { + // max is less than center, so we need to return effectively + // `max(min, max)`. + let max_cmp_min = match max.compare(&min, PositivePercentageBasis::Unknown) { + Some(o) => o, + None => { + debug_assert!( + false, + "We compared center with min and max, how are \ + min / max not comparable with each other?" + ); + return; + }, + }; + + if matches!(max_cmp_min, cmp::Ordering::Less) { + replace_self_with!(&mut **min); + return; + } + + replace_self_with!(&mut **max); + return; + } + + // Otherwise we're the center node. + replace_self_with!(&mut **center); + }, + Self::Round { + strategy, + ref mut value, + ref mut step, + } => { + if step.is_zero_leaf() { + value.coerce_to_value(f32::NAN); + replace_self_with!(&mut **value); + return; + } + + if value.is_infinite_leaf() && step.is_infinite_leaf() { + value.coerce_to_value(f32::NAN); + replace_self_with!(&mut **value); + return; + } + + if value.is_infinite_leaf() { + replace_self_with!(&mut **value); + return; + } + + if step.is_infinite_leaf() { + match strategy { + RoundingStrategy::Nearest | RoundingStrategy::ToZero => { + value.coerce_to_value(0.0); + replace_self_with!(&mut **value); + return; + }, + RoundingStrategy::Up => { + if !value.is_negative_leaf() && !value.is_zero_leaf() { + value.coerce_to_value(f32::INFINITY); + replace_self_with!(&mut **value); + return; + } else if !value.is_negative_leaf() && value.is_zero_leaf() { + replace_self_with!(&mut **value); + return; + } else { + value.coerce_to_value(0.0); + replace_self_with!(&mut **value); + return; + } + }, + RoundingStrategy::Down => { + if value.is_negative_leaf() && !value.is_zero_leaf() { + value.coerce_to_value(f32::INFINITY); + replace_self_with!(&mut **value); + return; + } else if value.is_negative_leaf() && value.is_zero_leaf() { + replace_self_with!(&mut **value); + return; + } else { + value.coerce_to_value(0.0); + replace_self_with!(&mut **value); + return; + } + }, + } + } + + if step.is_negative_leaf() { + step.negate(); + } + + let remainder = value_or_stop!(value.try_op(step, Rem::rem)); + if remainder.is_zero_leaf() { + replace_self_with!(&mut **value); + return; + } + + let (mut lower_bound, mut upper_bound) = if value.is_negative_leaf() { + let upper_bound = value_or_stop!(value.try_op(&remainder, Sub::sub)); + let lower_bound = value_or_stop!(upper_bound.try_op(&step, Sub::sub)); + + (lower_bound, upper_bound) + } else { + let lower_bound = value_or_stop!(value.try_op(&remainder, Sub::sub)); + let upper_bound = value_or_stop!(lower_bound.try_op(&step, Add::add)); + + (lower_bound, upper_bound) + }; + + match strategy { + RoundingStrategy::Nearest => { + let lower_diff = value_or_stop!(value.try_op(&lower_bound, Sub::sub)); + let upper_diff = value_or_stop!(upper_bound.try_op(value, Sub::sub)); + // In case of a tie, use the upper bound + if lower_diff.lt(&upper_diff, PositivePercentageBasis::Unknown) { + replace_self_with!(&mut lower_bound); + } else { + replace_self_with!(&mut upper_bound); + } + }, + RoundingStrategy::Up => { + replace_self_with!(&mut upper_bound); + }, + RoundingStrategy::Down => { + replace_self_with!(&mut lower_bound); + }, + RoundingStrategy::ToZero => { + let mut lower_diff = lower_bound.clone(); + let mut upper_diff = upper_bound.clone(); + + if lower_diff.is_negative_leaf() { + lower_diff.negate(); + } + + if upper_diff.is_negative_leaf() { + upper_diff.negate(); + } + + // In case of a tie, use the upper bound + if lower_diff.lt(&upper_diff, PositivePercentageBasis::Unknown) { + replace_self_with!(&mut lower_bound); + } else { + replace_self_with!(&mut upper_bound); + } + }, + }; + }, + Self::ModRem { + ref dividend, + ref divisor, + op, + } => { + let mut result = value_or_stop!(dividend.try_op(divisor, |a, b| op.apply(a, b))); + replace_self_with!(&mut result); + }, + Self::MinMax(ref mut children, op) => { + let winning_order = match op { + MinMaxOp::Min => cmp::Ordering::Less, + MinMaxOp::Max => cmp::Ordering::Greater, + }; + + let mut result = 0; + for i in 1..children.len() { + let o = match children[i] + .compare(&children[result], PositivePercentageBasis::Unknown) + { + // We can't compare all the children, so we can't + // know which one will actually win. Bail out and + // keep ourselves as a min / max function. + // + // TODO: Maybe we could simplify compatible children, + // see https://github.com/w3c/csswg-drafts/issues/4756 + None => return, + Some(o) => o, + }; + + if o == winning_order { + result = i; + } + } + + replace_self_with!(&mut children[result]); + }, + Self::Sum(ref mut children_slot) => { + let mut sums_to_merge = SmallVec::<[_; 3]>::new(); + let mut extra_kids = 0; + for (i, child) in children_slot.iter().enumerate() { + if let Self::Sum(ref children) = *child { + extra_kids += children.len(); + sums_to_merge.push(i); + } + } + + // If we only have one kid, we've already simplified it, and it + // doesn't really matter whether it's a sum already or not, so + // lift it up and continue. + if children_slot.len() == 1 { + replace_self_with!(&mut children_slot[0]); + return; + } + + let mut children = mem::take(children_slot).into_vec(); + + if !sums_to_merge.is_empty() { + children.reserve(extra_kids - sums_to_merge.len()); + // Merge all our nested sums, in reverse order so that the + // list indices are not invalidated. + for i in sums_to_merge.drain(..).rev() { + let kid_children = match children.swap_remove(i) { + Self::Sum(c) => c, + _ => unreachable!(), + }; + + // This would be nicer with + // https://github.com/rust-lang/rust/issues/59878 fixed. + children.extend(kid_children.into_vec()); + } + } + + debug_assert!(children.len() >= 2, "Should still have multiple kids!"); + + // Sort by spec order. + children.sort_unstable_by_key(|c| c.sort_key()); + + // NOTE: if the function returns true, by the docs of dedup_by, + // a is removed. + children.dedup_by(|a, b| b.try_sum_in_place(a).is_ok()); + + if children.len() == 1 { + // If only one children remains, lift it up, and carry on. + replace_self_with!(&mut children[0]); + } else { + // Else put our simplified children back. + *children_slot = children.into_boxed_slice().into(); + } + }, + Self::Product(ref mut children_slot) => { + let mut products_to_merge = SmallVec::<[_; 3]>::new(); + let mut extra_kids = 0; + for (i, child) in children_slot.iter().enumerate() { + if let Self::Product(ref children) = *child { + extra_kids += children.len(); + products_to_merge.push(i); + } + } + + // If we only have one kid, we've already simplified it, and it + // doesn't really matter whether it's a product already or not, + // so lift it up and continue. + if children_slot.len() == 1 { + replace_self_with!(&mut children_slot[0]); + return; + } + + let mut children = mem::take(children_slot).into_vec(); + + if !products_to_merge.is_empty() { + children.reserve(extra_kids - products_to_merge.len()); + // Merge all our nested sums, in reverse order so that the + // list indices are not invalidated. + for i in products_to_merge.drain(..).rev() { + let kid_children = match children.swap_remove(i) { + Self::Product(c) => c, + _ => unreachable!(), + }; + + // This would be nicer with + // https://github.com/rust-lang/rust/issues/59878 fixed. + children.extend(kid_children.into_vec()); + } + } + + debug_assert!(children.len() >= 2, "Should still have multiple kids!"); + + // NOTE: if the function returns true, by the docs of dedup_by, + // a is removed. + children.dedup_by(|right, left| left.try_product_in_place(right)); + + if children.len() == 1 { + // If only one children remains, lift it up, and carry on. + replace_self_with!(&mut children[0]); + } else { + // Else put our simplified children back. + *children_slot = children.into_boxed_slice().into(); + } + }, + Self::Hypot(ref children) => { + let mut result = value_or_stop!(children[0].try_op(&children[0], Mul::mul)); + + for child in children.iter().skip(1) { + let square = value_or_stop!(child.try_op(&child, Mul::mul)); + result = value_or_stop!(result.try_op(&square, Add::add)); + } + + result = value_or_stop!(result.try_op(&result, |a, _| a.sqrt())); + + replace_self_with!(&mut result); + }, + Self::Abs(ref mut child) => { + if let CalcNode::Leaf(leaf) = child.as_mut() { + leaf.map(|v| v.abs()); + replace_self_with!(&mut **child); + } + }, + Self::Sign(ref mut child) => { + if let CalcNode::Leaf(leaf) = child.as_mut() { + let mut result = Self::Leaf(L::sign_from(leaf)); + replace_self_with!(&mut result); + } + }, + Self::Negate(ref mut child) => { + // Step 6. + match &mut **child { + CalcNode::Leaf(_) => { + // 1. If root’s child is a numeric value, return an equivalent numeric value, but + // with the value negated (0 - value). + child.negate(); + replace_self_with!(&mut **child); + }, + CalcNode::Negate(value) => { + // 2. If root’s child is a Negate node, return the child’s child. + replace_self_with!(&mut **value); + }, + _ => { + // 3. Return root. + }, + } + }, + Self::Invert(ref mut child) => { + // Step 7. + match &mut **child { + CalcNode::Leaf(leaf) => { + // 1. If root’s child is a number (not a percentage or dimension) return the + // reciprocal of the child’s value. + if leaf.unit().is_empty() { + child.map(|v| 1.0 / v); + replace_self_with!(&mut **child); + } + }, + CalcNode::Invert(value) => { + // 2. If root’s child is an Invert node, return the child’s child. + replace_self_with!(&mut **value); + }, + _ => { + // 3. Return root. + }, + } + }, + Self::Leaf(ref mut l) => { + l.simplify(); + }, + } + } + + /// Simplifies and sorts the kids in the whole calculation subtree. + pub fn simplify_and_sort(&mut self) { + self.visit_depth_first(|node| node.simplify_and_sort_direct_children()) + } + + fn to_css_impl<W>(&self, dest: &mut CssWriter<W>, level: ArgumentLevel) -> fmt::Result + where + W: Write, + { + let write_closing_paren = match *self { + Self::MinMax(_, op) => { + dest.write_str(match op { + MinMaxOp::Max => "max(", + MinMaxOp::Min => "min(", + })?; + true + }, + Self::Clamp { .. } => { + dest.write_str("clamp(")?; + true + }, + Self::Round { strategy, .. } => { + match strategy { + RoundingStrategy::Nearest => dest.write_str("round("), + RoundingStrategy::Up => dest.write_str("round(up, "), + RoundingStrategy::Down => dest.write_str("round(down, "), + RoundingStrategy::ToZero => dest.write_str("round(to-zero, "), + }?; + + true + }, + Self::ModRem { op, .. } => { + dest.write_str(match op { + ModRemOp::Mod => "mod(", + ModRemOp::Rem => "rem(", + })?; + + true + }, + Self::Hypot(_) => { + dest.write_str("hypot(")?; + true + }, + Self::Abs(_) => { + dest.write_str("abs(")?; + true + }, + Self::Sign(_) => { + dest.write_str("sign(")?; + true + }, + Self::Negate(_) => { + // We never generate a [`Negate`] node as the root of a calculation, only inside + // [`Sum`] nodes as a child. Because negate nodes are handled by the [`Sum`] node + // directly (see below), this node will never be serialized. + debug_assert!( + false, + "We never serialize Negate nodes as they are handled inside Sum nodes." + ); + dest.write_str("(-1 * ")?; + true + }, + Self::Invert(_) => { + dest.write_str("(1 / ")?; + true + }, + Self::Sum(_) | Self::Product(_) => match level { + ArgumentLevel::CalculationRoot => { + dest.write_str("calc(")?; + true + }, + ArgumentLevel::ArgumentRoot => false, + ArgumentLevel::Nested => { + dest.write_str("(")?; + true + }, + }, + Self::Leaf(_) => match level { + ArgumentLevel::CalculationRoot => { + dest.write_str("calc(")?; + true + }, + ArgumentLevel::ArgumentRoot | ArgumentLevel::Nested => false, + }, + }; + + match *self { + Self::MinMax(ref children, _) | Self::Hypot(ref children) => { + let mut first = true; + for child in &**children { + if !first { + dest.write_str(", ")?; + } + first = false; + child.to_css_impl(dest, ArgumentLevel::ArgumentRoot)?; + } + }, + Self::Negate(ref value) | Self::Invert(ref value) => { + value.to_css_impl(dest, ArgumentLevel::Nested)? + }, + Self::Sum(ref children) => { + let mut first = true; + for child in &**children { + if !first { + match child { + Self::Leaf(l) => { + if l.is_negative() { + dest.write_str(" - ")?; + let mut negated = l.clone(); + negated.negate(); + negated.to_css(dest)?; + } else { + dest.write_str(" + ")?; + l.to_css(dest)?; + } + }, + Self::Negate(n) => { + dest.write_str(" - ")?; + n.to_css_impl(dest, ArgumentLevel::Nested)?; + }, + _ => { + dest.write_str(" + ")?; + child.to_css_impl(dest, ArgumentLevel::Nested)?; + }, + } + } else { + first = false; + child.to_css_impl(dest, ArgumentLevel::Nested)?; + } + } + }, + Self::Product(ref children) => { + let mut first = true; + for child in &**children { + if !first { + match child { + Self::Invert(n) => { + dest.write_str(" / ")?; + n.to_css_impl(dest, ArgumentLevel::Nested)?; + }, + _ => { + dest.write_str(" * ")?; + child.to_css_impl(dest, ArgumentLevel::Nested)?; + }, + } + } else { + first = false; + child.to_css_impl(dest, ArgumentLevel::Nested)?; + } + } + }, + Self::Clamp { + ref min, + ref center, + ref max, + } => { + min.to_css_impl(dest, ArgumentLevel::ArgumentRoot)?; + dest.write_str(", ")?; + center.to_css_impl(dest, ArgumentLevel::ArgumentRoot)?; + dest.write_str(", ")?; + max.to_css_impl(dest, ArgumentLevel::ArgumentRoot)?; + }, + Self::Round { + ref value, + ref step, + .. + } => { + value.to_css_impl(dest, ArgumentLevel::ArgumentRoot)?; + dest.write_str(", ")?; + step.to_css_impl(dest, ArgumentLevel::ArgumentRoot)?; + }, + Self::ModRem { + ref dividend, + ref divisor, + .. + } => { + dividend.to_css_impl(dest, ArgumentLevel::ArgumentRoot)?; + dest.write_str(", ")?; + divisor.to_css_impl(dest, ArgumentLevel::ArgumentRoot)?; + }, + Self::Abs(ref v) | Self::Sign(ref v) => { + v.to_css_impl(dest, ArgumentLevel::ArgumentRoot)? + }, + Self::Leaf(ref l) => l.to_css(dest)?, + } + + if write_closing_paren { + dest.write_char(')')?; + } + Ok(()) + } + + fn compare( + &self, + other: &Self, + basis_positive: PositivePercentageBasis, + ) -> Option<cmp::Ordering> { + match (self, other) { + (&CalcNode::Leaf(ref one), &CalcNode::Leaf(ref other)) => { + one.compare(other, basis_positive) + }, + _ => None, + } + } + + compare_helpers!(); +} + +impl<L: CalcNodeLeaf> ToCss for CalcNode<L> { + /// <https://drafts.csswg.org/css-values/#calc-serialize> + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.to_css_impl(dest, ArgumentLevel::CalculationRoot) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn can_sum_with_checks() { + assert!(CalcUnits::LENGTH.can_sum_with(CalcUnits::LENGTH)); + assert!(CalcUnits::LENGTH.can_sum_with(CalcUnits::PERCENTAGE)); + assert!(CalcUnits::LENGTH.can_sum_with(CalcUnits::LENGTH_PERCENTAGE)); + + assert!(CalcUnits::PERCENTAGE.can_sum_with(CalcUnits::LENGTH)); + assert!(CalcUnits::PERCENTAGE.can_sum_with(CalcUnits::PERCENTAGE)); + assert!(CalcUnits::PERCENTAGE.can_sum_with(CalcUnits::LENGTH_PERCENTAGE)); + + assert!(CalcUnits::LENGTH_PERCENTAGE.can_sum_with(CalcUnits::LENGTH)); + assert!(CalcUnits::LENGTH_PERCENTAGE.can_sum_with(CalcUnits::PERCENTAGE)); + assert!(CalcUnits::LENGTH_PERCENTAGE.can_sum_with(CalcUnits::LENGTH_PERCENTAGE)); + + assert!(!CalcUnits::ANGLE.can_sum_with(CalcUnits::TIME)); + assert!(CalcUnits::ANGLE.can_sum_with(CalcUnits::ANGLE)); + + assert!(!(CalcUnits::ANGLE | CalcUnits::TIME).can_sum_with(CalcUnits::ANGLE)); + assert!(!CalcUnits::ANGLE.can_sum_with(CalcUnits::ANGLE | CalcUnits::TIME)); + assert!( + !(CalcUnits::ANGLE | CalcUnits::TIME).can_sum_with(CalcUnits::ANGLE | CalcUnits::TIME) + ); + } +} |