From e55c03c8ff2e4344913bb4b43e870cc9f913d3cf Mon Sep 17 00:00:00 2001 From: Connor Pearson Date: Tue, 29 Nov 2022 13:05:39 +0000 Subject: [PATCH] style: Implement CSS mod() and rem() functions Differential Revision: https://phabricator.services.mozilla.com/D163166 --- components/style/values/generics/calc.rs | 129 ++++++++++++++++++++++ components/style/values/specified/calc.rs | 24 +++- 2 files changed, 152 insertions(+), 1 deletion(-) diff --git a/components/style/values/generics/calc.rs b/components/style/values/generics/calc.rs index 91941a6b71b..24b0cf23a32 100644 --- a/components/style/values/generics/calc.rs +++ b/components/style/values/generics/calc.rs @@ -34,6 +34,27 @@ pub enum MinMaxOp { 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, +} + /// The strategy used in `round()` #[derive( Clone, @@ -162,6 +183,15 @@ pub enum GenericCalcNode { /// The step value. step: Box>, }, + /// A `mod()` or `rem()` function. + ModRem { + /// The dividend calculation. + dividend: Box>, + /// The divisor calculation. + divisor: Box>, + /// Is the function mod or rem? + op: ModRemOp, + }, } pub use self::GenericCalcNode as CalcNode; @@ -314,6 +344,19 @@ impl CalcNode { 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, + } + }, } } @@ -469,6 +512,29 @@ impl CalcNode { }, } }, + Self::ModRem { + ref dividend, + ref divisor, + op, + } => { + let dividend = dividend.resolve_internal(leaf_to_output_fn)?; + let divisor = divisor.resolve_internal(leaf_to_output_fn)?; + + // 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!(op, ModRemOp::Mod) && + divisor.is_infinite() && + dividend.is_sign_negative() != divisor.is_sign_negative() + { + return Ok(::nan()); + } + + match op { + ModRemOp::Mod => dividend - divisor * (dividend / divisor).floor(), + ModRemOp::Rem => dividend - divisor * (dividend / divisor).trunc(), + } + }, }) } @@ -539,6 +605,14 @@ impl CalcNode { value.mul_by(scalar); step.mul_by(scalar); }, + Self::ModRem { + ref mut dividend, + ref mut divisor, + .. + } => { + dividend.mul_by(scalar); + divisor.mul_by(scalar); + }, } } @@ -570,6 +644,14 @@ impl CalcNode { 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::MinMax(ref mut children, _) => { for child in &mut **children { child.visit_depth_first_internal(f); @@ -772,6 +854,36 @@ impl CalcNode { }, }; }, + Self::ModRem { + ref dividend, + ref divisor, + op, + } => { + let mut result = dividend.clone(); + + // 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!(op, ModRemOp::Mod) && + divisor.is_infinite_leaf() && + dividend.is_negative_leaf() != divisor.is_negative_leaf() + { + result.mul_by(f32::NAN); + return replace_self_with!(&mut *result); + } + + let result = match op { + ModRemOp::Mod => dividend.try_op(divisor, |a, b| a - b * (a / b).floor()), + ModRemOp::Rem => dividend.try_op(divisor, |a, b| a - b * (a / b).trunc()), + }; + + let mut result = match result { + Ok(res) => res, + Err(..) => return, + }; + + return replace_self_with!(&mut result); + }, Self::MinMax(ref mut children, op) => { let winning_order = match op { MinMaxOp::Min => cmp::Ordering::Less, @@ -887,6 +999,14 @@ impl CalcNode { true }, + Self::ModRem { op, .. } => { + dest.write_str(match op { + ModRemOp::Mod => "mod(", + ModRemOp::Rem => "rem(", + })?; + + true + }, _ => { if is_outermost { dest.write_str("calc(")?; @@ -945,6 +1065,15 @@ impl CalcNode { dest.write_str(", ")?; step.to_css_impl(dest, false)?; }, + Self::ModRem { + ref dividend, + ref divisor, + .. + } => { + dividend.to_css_impl(dest, false)?; + dest.write_str(", ")?; + divisor.to_css_impl(dest, false)?; + }, Self::Leaf(ref l) => l.to_css(dest)?, } diff --git a/components/style/values/specified/calc.rs b/components/style/values/specified/calc.rs index dd4ab3ab4cb..583948aab22 100644 --- a/components/style/values/specified/calc.rs +++ b/components/style/values/specified/calc.rs @@ -8,7 +8,7 @@ use crate::parser::ParserContext; use crate::values::generics::calc as generic; -use crate::values::generics::calc::{MinMaxOp, RoundingStrategy, SortKey}; +use crate::values::generics::calc::{MinMaxOp, ModRemOp, RoundingStrategy, SortKey}; use crate::values::specified::length::{AbsoluteLength, FontRelativeLength, NoCalcLength}; use crate::values::specified::length::{ContainerRelativeLength, ViewportPercentageLength}; use crate::values::specified::{self, Angle, Time}; @@ -54,6 +54,10 @@ pub enum MathFunction { Clamp, /// `round()`: https://drafts.csswg.org/css-values-4/#funcdef-round Round, + /// `mod()`: https://drafts.csswg.org/css-values-4/#funcdef-mod + Mod, + /// `rem()`: https://drafts.csswg.org/css-values-4/#funcdef-rem + Rem, /// `sin()`: https://drafts.csswg.org/css-values-4/#funcdef-sin Sin, /// `cos()`: https://drafts.csswg.org/css-values-4/#funcdef-cos @@ -490,6 +494,22 @@ impl CalcNode { step: Box::new(step), }) }, + MathFunction::Mod | MathFunction::Rem => { + let dividend = Self::parse_argument(context, input, allowed_units)?; + input.expect_comma()?; + let divisor = Self::parse_argument(context, input, allowed_units)?; + + let op = match function { + MathFunction::Mod => ModRemOp::Mod, + MathFunction::Rem => ModRemOp::Rem, + _ => unreachable!(), + }; + Ok(Self::ModRem { + dividend: Box::new(dividend), + divisor: Box::new(divisor), + op, + }) + }, MathFunction::Min | MathFunction::Max => { // TODO(emilio): The common case for parse_comma_separated // is just one element, but for min / max is two, really... @@ -780,6 +800,8 @@ impl CalcNode { trig_enabled() } else if matches!(function, Round) { round_enabled() + } else if matches!(function, Mod | Rem) { + static_prefs::pref!("layout.css.mod-rem.enabled") } else { true };