style: Implement atan2(), and enable calc() trigonometric functions by default on nightly

We now have test coverage, so let's do this.

The remaining failures are just about infinity/nan, which is a
completely different feature.

Differential Revision: https://phabricator.services.mozilla.com/D154831
This commit is contained in:
Emilio Cobos Álvarez 2022-08-18 08:49:30 +00:00 committed by Martin Robinson
parent 03e84754cc
commit dd849de9d9
3 changed files with 180 additions and 117 deletions

View file

@ -158,6 +158,14 @@ impl<L: CalcNodeLeaf> CalcNode<L> {
}
}
/// 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 sum to another, that is, perform `x` + `y`.
fn try_sum_in_place(&mut self, other: &Self) -> Result<(), ()> {
match (self, other) {

View file

@ -43,6 +43,8 @@ pub enum MathFunction {
Acos,
/// `atan()`: https://drafts.csswg.org/css-values-4/#funcdef-atan
Atan,
/// `atan2()`: https://drafts.csswg.org/css-values-4/#funcdef-atan2
Atan2,
}
/// A leaf node inside a `Calc` expression's AST.
@ -60,6 +62,15 @@ pub enum Leaf {
Number(CSSFloat),
}
impl Leaf {
fn as_length(&self) -> Option<&NoCalcLength> {
match *self {
Self::Length(ref l) => Some(l),
_ => None,
}
}
}
impl ToCss for Leaf {
fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
where
@ -75,23 +86,22 @@ impl ToCss for Leaf {
}
}
/// An expected unit we intend to parse within a `calc()` expression.
///
/// This is used as a hint for the parser to fast-reject invalid expressions.
#[derive(Clone, Copy, PartialEq)]
enum CalcUnit {
/// `<number>`
Number,
/// `<length>`
Length,
/// `<percentage>`
Percentage,
/// `<length> | <percentage>`
LengthPercentage,
/// `<angle>`
Angle,
/// `<time>`
Time,
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.
struct CalcUnits: u8 {
const LENGTH = 1 << 0;
const PERCENTAGE = 1 << 1;
const ANGLE = 1 << 2;
const TIME = 1 << 3;
const LENGTH_PERCENTAGE = Self::LENGTH.bits | Self::PERCENTAGE.bits;
// NOTE: When you add to this, make sure to make Atan2 deal with these.
const ALL = Self::LENGTH.bits | Self::PERCENTAGE.bits | Self::ANGLE.bits | Self::TIME.bits;
}
}
/// A struct to hold a simplified `<length>` or `<percentage>` expression.
@ -108,6 +118,27 @@ pub struct CalcLengthPercentage {
pub node: CalcNode,
}
impl CalcLengthPercentage {
fn same_unit_length_as(a: &Self, b: &Self) -> Option<(CSSFloat, CSSFloat)> {
use generic::CalcNodeLeaf;
debug_assert_eq!(a.clamping_mode, b.clamping_mode);
debug_assert_eq!(a.clamping_mode, AllowedNumericType::All);
let a = a.node.as_leaf()?;
let b = b.node.as_leaf()?;
if a.sort_key() != b.sort_key() {
return None;
}
let a = a.as_length()?.unitless_value();
let b = b.as_length()?.unitless_value();
return Some((a, b))
}
}
impl SpecifiedValueInfo for CalcLengthPercentage {}
impl PartialOrd for Leaf {
@ -277,71 +308,47 @@ pub type CalcNode = generic::GenericCalcNode<Leaf>;
impl CalcNode {
/// Tries to parse a single element in the expression, that is, a
/// `<length>`, `<angle>`, `<time>`, `<percentage>`, according to
/// `expected_unit`.
/// `allowed_units`.
///
/// May return a "complex" `CalcNode`, in the presence of a parenthesized
/// expression, for example.
fn parse_one<'i, 't>(
context: &ParserContext,
input: &mut Parser<'i, 't>,
expected_unit: CalcUnit,
allowed_units: CalcUnits,
) -> Result<Self, ParseError<'i>> {
let location = input.current_source_location();
match (input.next()?, expected_unit) {
(&Token::Number { value, .. }, _) => Ok(CalcNode::Leaf(Leaf::Number(value))),
(
&Token::Dimension {
value, ref unit, ..
},
CalcUnit::Length,
) |
(
&Token::Dimension {
value, ref unit, ..
},
CalcUnit::LengthPercentage,
) => match NoCalcLength::parse_dimension(context, value, unit) {
Ok(l) => Ok(CalcNode::Leaf(Leaf::Length(l))),
Err(()) => Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
},
(
&Token::Dimension {
value, ref unit, ..
},
CalcUnit::Angle,
) => {
match Angle::parse_dimension(value, unit, /* from_calc = */ true) {
Ok(a) => Ok(CalcNode::Leaf(Leaf::Angle(a))),
Err(()) => {
Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError))
},
match input.next()? {
&Token::Number { value, .. } => Ok(CalcNode::Leaf(Leaf::Number(value))),
&Token::Dimension { value, ref unit, .. } => {
if allowed_units.intersects(CalcUnits::LENGTH) {
if let Ok(l) = NoCalcLength::parse_dimension(context, value, unit) {
return Ok(CalcNode::Leaf(Leaf::Length(l)));
}
}
},
(
&Token::Dimension {
value, ref unit, ..
},
CalcUnit::Time,
) => {
match Time::parse_dimension(value, unit, /* from_calc = */ true) {
Ok(t) => Ok(CalcNode::Leaf(Leaf::Time(t))),
Err(()) => {
Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError))
},
if allowed_units.intersects(CalcUnits::ANGLE) {
if let Ok(a) = Angle::parse_dimension(value, unit, /* from_calc = */ true) {
return Ok(CalcNode::Leaf(Leaf::Angle(a)));
}
}
if allowed_units.intersects(CalcUnits::TIME) {
if let Ok(t) = Time::parse_dimension(value, unit, /* from_calc = */ true) {
return Ok(CalcNode::Leaf(Leaf::Time(t)));
}
}
return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError))
},
(&Token::Percentage { unit_value, .. }, CalcUnit::LengthPercentage) |
(&Token::Percentage { unit_value, .. }, CalcUnit::Percentage) => {
&Token::Percentage { unit_value, .. } if allowed_units.intersects(CalcUnits::PERCENTAGE) => {
Ok(CalcNode::Leaf(Leaf::Percentage(unit_value)))
},
(&Token::ParenthesisBlock, _) => input.parse_nested_block(|input| {
CalcNode::parse_argument(context, input, expected_unit)
}
&Token::ParenthesisBlock => input.parse_nested_block(|input| {
CalcNode::parse_argument(context, input, allowed_units)
}),
(&Token::Function(ref name), _) => {
&Token::Function(ref name) => {
let function = CalcNode::math_function(name, location)?;
CalcNode::parse(context, input, function, expected_unit)
CalcNode::parse(context, input, function, allowed_units)
},
(&Token::Ident(ref ident), _) => {
&Token::Ident(ref ident) => {
if !trig_enabled() {
return Err(location.new_unexpected_token_error(Token::Ident(ident.clone())));
}
@ -352,7 +359,7 @@ impl CalcNode {
};
Ok(CalcNode::Leaf(Leaf::Number(number)))
},
(t, _) => Err(location.new_unexpected_token_error(t.clone())),
t => Err(location.new_unexpected_token_error(t.clone())),
}
}
@ -363,20 +370,20 @@ impl CalcNode {
context: &ParserContext,
input: &mut Parser<'i, 't>,
function: MathFunction,
expected_unit: CalcUnit,
allowed_units: CalcUnits,
) -> Result<Self, ParseError<'i>> {
// TODO: Do something different based on the function name. In
// particular, for non-calc function we need to take a list of
// comma-separated arguments and such.
input.parse_nested_block(|input| {
match function {
MathFunction::Calc => Self::parse_argument(context, input, expected_unit),
MathFunction::Calc => Self::parse_argument(context, input, allowed_units),
MathFunction::Clamp => {
let min = Self::parse_argument(context, input, expected_unit)?;
let min = Self::parse_argument(context, input, allowed_units)?;
input.expect_comma()?;
let center = Self::parse_argument(context, input, expected_unit)?;
let center = Self::parse_argument(context, input, allowed_units)?;
input.expect_comma()?;
let max = Self::parse_argument(context, input, expected_unit)?;
let max = Self::parse_argument(context, input, allowed_units)?;
Ok(Self::Clamp {
min: Box::new(min),
center: Box::new(center),
@ -390,7 +397,7 @@ impl CalcNode {
// Consider adding an API to cssparser to specify the
// initial vector capacity?
let arguments = input.parse_comma_separated(|input| {
Self::parse_argument(context, input, expected_unit)
Self::parse_argument(context, input, allowed_units)
})?;
let op = match function {
@ -402,7 +409,7 @@ impl CalcNode {
Ok(Self::MinMax(arguments.into(), op))
},
MathFunction::Sin | MathFunction::Cos | MathFunction::Tan => {
let argument = Self::parse_argument(context, input, CalcUnit::Angle)?;
let argument = Self::parse_argument(context, input, CalcUnits::ANGLE)?;
let radians = match argument.to_number() {
Ok(v) => v,
Err(()) => match argument.to_angle() {
@ -425,7 +432,7 @@ impl CalcNode {
Ok(Self::Leaf(Leaf::Number(number)))
},
MathFunction::Asin | MathFunction::Acos | MathFunction::Atan => {
let argument = Self::parse_argument(context, input, CalcUnit::Number)?;
let argument = Self::parse_argument(context, input, CalcUnits::empty())?;
let number = match argument.to_number() {
Ok(v) => v,
Err(()) => {
@ -444,6 +451,46 @@ impl CalcNode {
},
};
Ok(Self::Leaf(Leaf::Angle(Angle::from_radians(radians))))
},
MathFunction::Atan2 => {
let a = Self::parse_argument(context, input, CalcUnits::ALL)?;
input.expect_comma()?;
let b = Self::parse_argument(context, input, CalcUnits::ALL)?;
fn resolve_atan2(a: CalcNode, b: CalcNode) -> Result<CSSFloat, ()> {
if let Ok(a) = a.to_number() {
let b = b.to_number()?;
return Ok(a.atan2(b));
}
if let Ok(a) = a.to_percentage() {
let b = b.to_percentage()?;
return Ok(a.atan2(b));
}
if let Ok(a) = a.to_time() {
let b = b.to_time()?;
return Ok(a.seconds().atan2(b.seconds()));
}
if let Ok(a) = a.to_angle() {
let b = b.to_angle()?;
return Ok(a.radians().atan2(b.radians()));
}
let a = a.into_length_or_percentage(AllowedNumericType::All)?;
let b = b.into_length_or_percentage(AllowedNumericType::All)?;
let (a, b) = CalcLengthPercentage::same_unit_length_as(&a, &b).ok_or(())?;
return Ok(a.atan2(b));
}
let radians = match resolve_atan2(a, b) {
Ok(v) => v,
Err(()) => return Err(
input.new_custom_error(StyleParseErrorKind::UnspecifiedError)
),
};
Ok(Self::Leaf(Leaf::Angle(Angle::from_radians(radians))))
},
}
@ -453,10 +500,10 @@ impl CalcNode {
fn parse_argument<'i, 't>(
context: &ParserContext,
input: &mut Parser<'i, 't>,
expected_unit: CalcUnit,
allowed_units: CalcUnits,
) -> Result<Self, ParseError<'i>> {
let mut sum = SmallVec::<[CalcNode; 1]>::new();
sum.push(Self::parse_product(context, input, expected_unit)?);
sum.push(Self::parse_product(context, input, allowed_units)?);
loop {
let start = input.state();
@ -467,10 +514,10 @@ impl CalcNode {
}
match *input.next()? {
Token::Delim('+') => {
sum.push(Self::parse_product(context, input, expected_unit)?);
sum.push(Self::parse_product(context, input, allowed_units)?);
},
Token::Delim('-') => {
let mut rhs = Self::parse_product(context, input, expected_unit)?;
let mut rhs = Self::parse_product(context, input, allowed_units)?;
rhs.negate();
sum.push(rhs);
},
@ -506,15 +553,15 @@ impl CalcNode {
fn parse_product<'i, 't>(
context: &ParserContext,
input: &mut Parser<'i, 't>,
expected_unit: CalcUnit,
allowed_units: CalcUnits,
) -> Result<Self, ParseError<'i>> {
let mut node = Self::parse_one(context, input, expected_unit)?;
let mut node = Self::parse_one(context, input, allowed_units)?;
loop {
let start = input.state();
match input.next() {
Ok(&Token::Delim('*')) => {
let rhs = Self::parse_one(context, input, expected_unit)?;
let rhs = Self::parse_one(context, input, allowed_units)?;
if let Ok(rhs) = rhs.to_number() {
node.mul_by(rhs);
} else if let Ok(number) = node.to_number() {
@ -527,7 +574,7 @@ impl CalcNode {
}
},
Ok(&Token::Delim('/')) => {
let rhs = Self::parse_one(context, input, expected_unit)?;
let rhs = Self::parse_one(context, input, allowed_units)?;
// Dividing by units is not ok.
//
// TODO(emilio): Eventually it should be.
@ -627,7 +674,7 @@ impl CalcNode {
},
};
if matches!(function, Sin | Cos | Tan | Asin | Acos | Atan) && !trig_enabled() {
if matches!(function, Sin | Cos | Tan | Asin | Acos | Atan | Atan2) && !trig_enabled() {
return Err(location.new_unexpected_token_error(Token::Function(name.clone())));
}
@ -650,7 +697,7 @@ impl CalcNode {
clamping_mode: AllowedNumericType,
function: MathFunction,
) -> Result<CalcLengthPercentage, ParseError<'i>> {
Self::parse(context, input, function, CalcUnit::LengthPercentage)?
Self::parse(context, input, function, CalcUnits::LENGTH_PERCENTAGE)?
.into_length_or_percentage(clamping_mode)
.map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
}
@ -661,7 +708,7 @@ impl CalcNode {
input: &mut Parser<'i, 't>,
function: MathFunction,
) -> Result<CSSFloat, ParseError<'i>> {
Self::parse(context, input, function, CalcUnit::Percentage)?
Self::parse(context, input, function, CalcUnits::PERCENTAGE)?
.to_percentage()
.map(crate::values::normalize)
.map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
@ -674,7 +721,7 @@ impl CalcNode {
clamping_mode: AllowedNumericType,
function: MathFunction,
) -> Result<CalcLengthPercentage, ParseError<'i>> {
Self::parse(context, input, function, CalcUnit::Length)?
Self::parse(context, input, function, CalcUnits::LENGTH)?
.into_length_or_percentage(clamping_mode)
.map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
}
@ -685,7 +732,7 @@ impl CalcNode {
input: &mut Parser<'i, 't>,
function: MathFunction,
) -> Result<CSSFloat, ParseError<'i>> {
Self::parse(context, input, function, CalcUnit::Number)?
Self::parse(context, input, function, CalcUnits::empty())?
.to_number()
.map(crate::values::normalize)
.map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
@ -697,7 +744,7 @@ impl CalcNode {
input: &mut Parser<'i, 't>,
function: MathFunction,
) -> Result<Angle, ParseError<'i>> {
Self::parse(context, input, function, CalcUnit::Angle)?
Self::parse(context, input, function, CalcUnits::ANGLE)?
.to_angle()
.map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
}
@ -708,7 +755,7 @@ impl CalcNode {
input: &mut Parser<'i, 't>,
function: MathFunction,
) -> Result<Time, ParseError<'i>> {
Self::parse(context, input, function, CalcUnit::Time)?
Self::parse(context, input, function, CalcUnits::TIME)?
.to_time()
.map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
}
@ -719,7 +766,7 @@ impl CalcNode {
input: &mut Parser<'i, 't>,
function: MathFunction,
) -> Result<NumberOrPercentage, ParseError<'i>> {
let node = Self::parse(context, input, function, CalcUnit::Percentage)?;
let node = Self::parse(context, input, function, CalcUnits::PERCENTAGE)?;
if let Ok(value) = node.to_number() {
return Ok(NumberOrPercentage::Number { value });
@ -737,7 +784,7 @@ impl CalcNode {
input: &mut Parser<'i, 't>,
function: MathFunction,
) -> Result<AngleOrNumber, ParseError<'i>> {
let node = Self::parse(context, input, function, CalcUnit::Angle)?;
let node = Self::parse(context, input, function, CalcUnits::ANGLE)?;
if let Ok(angle) = node.to_angle() {
let degrees = angle.degrees();

View file

@ -90,25 +90,23 @@ impl FontBaseSize {
impl FontRelativeLength {
/// Return true if this is a zero value.
fn is_zero(&self) -> bool {
self.unitless_value() == 0.
}
/// Return the unitless, raw value.
fn unitless_value(&self) -> CSSFloat {
match *self {
FontRelativeLength::Em(v) |
FontRelativeLength::Ex(v) |
FontRelativeLength::Ch(v) |
FontRelativeLength::Cap(v) |
FontRelativeLength::Ic(v) |
FontRelativeLength::Rem(v) => v == 0.,
FontRelativeLength::Rem(v) => v,
}
}
fn is_negative(&self) -> bool {
match *self {
FontRelativeLength::Em(v) |
FontRelativeLength::Ex(v) |
FontRelativeLength::Ch(v) |
FontRelativeLength::Cap(v) |
FontRelativeLength::Ic(v) |
FontRelativeLength::Rem(v) => v < 0.,
}
self.unitless_value() < 0.
}
fn try_sum(&self, other: &Self) -> Result<Self, ()> {
@ -388,13 +386,16 @@ pub enum ViewportPercentageLength {
impl ViewportPercentageLength {
/// Return true if this is a zero value.
fn is_zero(&self) -> bool {
let (_, _, v) = self.unpack();
v == 0.
self.unitless_value() == 0.
}
fn is_negative(&self) -> bool {
let (_, _, v) = self.unpack();
v < 0.
self.unitless_value() < 0.
}
/// Return the unitless, raw value.
fn unitless_value(&self) -> CSSFloat {
self.unpack().2
}
fn unpack(&self) -> (ViewportVariant, ViewportUnit, CSSFloat) {
@ -642,7 +643,8 @@ pub enum AbsoluteLength {
}
impl AbsoluteLength {
fn is_zero(&self) -> bool {
/// Return the unitless, raw value.
fn unitless_value(&self) -> CSSFloat {
match *self {
AbsoluteLength::Px(v) |
AbsoluteLength::In(v) |
@ -650,20 +652,16 @@ impl AbsoluteLength {
AbsoluteLength::Mm(v) |
AbsoluteLength::Q(v) |
AbsoluteLength::Pt(v) |
AbsoluteLength::Pc(v) => v == 0.,
AbsoluteLength::Pc(v) => v,
}
}
fn is_zero(&self) -> bool {
self.unitless_value() == 0.
}
fn is_negative(&self) -> bool {
match *self {
AbsoluteLength::Px(v) |
AbsoluteLength::In(v) |
AbsoluteLength::Cm(v) |
AbsoluteLength::Mm(v) |
AbsoluteLength::Q(v) |
AbsoluteLength::Pt(v) |
AbsoluteLength::Pc(v) => v < 0.,
}
self.unitless_value() < 0.
}
/// Convert this into a pixel value.
@ -780,6 +778,16 @@ impl Mul<CSSFloat> for NoCalcLength {
}
impl NoCalcLength {
/// Return the unitless, raw value.
pub fn unitless_value(&self) -> CSSFloat {
match *self {
NoCalcLength::Absolute(v) => v.unitless_value(),
NoCalcLength::FontRelative(v) => v.unitless_value(),
NoCalcLength::ViewportPercentage(v) => v.unitless_value(),
NoCalcLength::ServoCharacterWidth(c) => c.0 as f32,
}
}
/// Returns whether the value of this length without unit is less than zero.
pub fn is_negative(&self) -> bool {
match *self {