style: Add some calc infrastructure to deal with simplification / sorting / etc.

For now, we still bail out at the stage of getting the calc node into a
CalcLengthPercentage if we couldn't simplify the min() / max() / clamps()
involved.

After this plan is to use just CalcNode everywhere instead of
specified::CalcLengthPercentage, and then modify the computed
CalcLengthPercentage, which would look slightly different as we know all the sum
terms for those are a struct like { Length, Percentage, bool has_percentage } or
such, so all the simplification code for that becomes much simpler, ideally.

Or we could turn CalcNode generic otherwise, if it's too much code... We'll see.

Differential Revision: https://phabricator.services.mozilla.com/D61739
This commit is contained in:
Emilio Cobos Álvarez 2020-02-10 14:09:27 +00:00
parent 71b414f9dc
commit 3e14422788
No known key found for this signature in database
GPG key ID: E1152D0994E4BF8A
2 changed files with 366 additions and 10 deletions

View file

@ -10,11 +10,12 @@ use crate::parser::ParserContext;
use crate::values::computed;
use crate::values::specified::length::ViewportPercentageLength;
use crate::values::specified::length::{AbsoluteLength, FontRelativeLength, NoCalcLength};
use crate::values::specified::{Angle, Time};
use crate::values::specified::{self, Angle, Time};
use crate::values::{CSSFloat, CSSInteger};
use cssparser::{AngleOrNumber, CowRcStr, NumberOrPercentage, Parser, Token};
use smallvec::SmallVec;
use std::fmt::{self, Write};
use std::{cmp, mem};
use style_traits::values::specified::AllowedNumericType;
use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss};
@ -31,8 +32,30 @@ pub enum MathFunction {
Clamp,
}
/// 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(Debug, Copy, Clone, Eq, Ord, PartialEq, PartialOrd)]
enum SortKey {
Number,
Percentage,
Ch,
Deg,
Em,
Ex,
Px,
Rem,
Sec,
Vh,
Vmax,
Vmin,
Vw,
Other,
}
/// Whether we're a `min` or `max` function.
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum MinMaxOp {
/// `min()`
Min,
@ -41,7 +64,7 @@ pub enum MinMaxOp {
}
/// A node inside a `Calc` expression's AST.
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq)]
pub enum CalcNode {
/// `<length>`
Length(NoCalcLength),
@ -201,27 +224,37 @@ macro_rules! impl_generic_to_type {
// Equivalent to cmp::max(min, cmp::min(center, max))
//
// But preserving units when appropriate.
let center_float = center.$to_float();
let min_float = min.$to_float();
let max_float = max.$to_float();
let mut result = center;
if result.$to_float() > max.$to_float() {
let mut result_float = center_float;
if result_float > max_float {
result = max;
result_float = max_float;
}
if result.$to_float() < min.$to_float() {
result = min;
}
if result_float < min_float {
min
} else {
result
}
},
Self::MinMax(ref nodes, op) => {
let mut result = nodes[0].$to_self()?;
let mut result_float = result.$to_float();
for node in nodes.iter().skip(1) {
let candidate = node.$to_self()?;
let candidate_float = candidate.$to_float();
let result_float = result.$to_float();
let candidate_wins = match op {
MinMaxOp::Min => candidate_float < result_float,
MinMaxOp::Max => candidate_float > result_float,
};
if candidate_wins {
result = candidate;
result_float = candidate_float;
}
}
result
@ -235,6 +268,20 @@ macro_rules! impl_generic_to_type {
}}
}
impl PartialOrd for CalcNode {
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
use self::CalcNode::*;
match (self, other) {
(&Length(ref one), &Length(ref other)) => one.partial_cmp(other),
(&Percentage(ref one), &Percentage(ref other)) => one.partial_cmp(other),
(&Angle(ref one), &Angle(ref other)) => one.degrees().partial_cmp(&other.degrees()),
(&Time(ref one), &Time(ref other)) => one.seconds().partial_cmp(&other.seconds()),
(&Number(ref one), &Number(ref other)) => one.partial_cmp(other),
_ => None,
}
}
}
impl CalcNode {
fn negate(&mut self) {
self.mul_by(-1.);
@ -286,12 +333,225 @@ impl CalcNode {
max.mul_by(scalar);
// For negatives we need to swap min / max.
if scalar < 0. {
std::mem::swap(min, max);
mem::swap(min, max);
}
},
}
}
fn calc_node_sort_key(&self) -> SortKey {
match *self {
Self::Number(..) => SortKey::Number,
Self::Percentage(..) => SortKey::Percentage,
Self::Time(..) => SortKey::Sec,
Self::Angle(..) => SortKey::Deg,
Self::Length(ref l) => {
match *l {
NoCalcLength::Absolute(..) => SortKey::Px,
NoCalcLength::FontRelative(ref relative) => {
match *relative {
FontRelativeLength::Ch(..) => SortKey::Ch,
FontRelativeLength::Em(..) => SortKey::Em,
FontRelativeLength::Ex(..) => SortKey::Ex,
FontRelativeLength::Rem(..) => SortKey::Rem,
}
},
NoCalcLength::ViewportPercentage(ref vp) => {
match *vp {
ViewportPercentageLength::Vh(..) => SortKey::Vh,
ViewportPercentageLength::Vw(..) => SortKey::Vw,
ViewportPercentageLength::Vmax(..) => SortKey::Vmax,
ViewportPercentageLength::Vmin(..) => SortKey::Vmin,
}
},
NoCalcLength::ServoCharacterWidth(..) => unreachable!(),
}
},
Self::Sum(..) | Self::MinMax(..) | Self::Clamp { .. } => SortKey::Other,
}
}
/// Tries to merge one sum to another, that is, perform `x` + `y`.
///
/// Only handles leaf nodes, it's the caller's responsibility to simplify
/// them before calling this if needed.
fn try_sum_in_place(&mut self, other: &Self) -> Result<(), ()> { use
self::CalcNode::*;
match (self, other) { (&mut Number(ref mut one), &Number(ref other)) |
(&mut Percentage(ref mut one), &Percentage(ref other)) => { *one +=
*other; } (&mut Angle(ref mut one), &Angle(ref other)) => { *one
= specified::Angle::from_calc(one.degrees() +
other.degrees()); } (&mut Time(ref mut one), &Time(ref
other)) => { *one =
specified::Time::from_calc(one.seconds() +
other.seconds()); } (&mut Length(ref mut one),
&Length(ref other)) => { *one =
one.try_sum(other)?; } _ => return Err(()),
}
Ok(()) }
/// Simplifies and sorts the calculation. This is only needed if it's going
/// to be preserved after parsing (so, for `<length-percentage>`). Otherwise
/// we can just evaluate it and we'll come up with a simplified value
/// anyways.
fn simplify_and_sort_children(&mut self) {
macro_rules! replace_self_with {
($slot:expr) => {{
let result = mem::replace($slot, Self::Number(0.));
mem::replace(self, result);
}}
}
match *self {
Self::Clamp { ref mut min, ref mut center, ref mut max } => {
min.simplify_and_sort_children();
center.simplify_and_sort_children();
max.simplify_and_sort_children();
// NOTE: clamp() is max(min, min(center, max))
let min_cmp_center = match min.partial_cmp(&center) {
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) {
return replace_self_with!(&mut **min);
}
// Otherwise try with max.
let max_cmp_center = match max.partial_cmp(&center) {
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.partial_cmp(&min) {
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) {
return replace_self_with!(&mut **min);
}
return replace_self_with!(&mut **max);
}
// Otherwise we're the center node.
return replace_self_with!(&mut **center);
},
Self::MinMax(ref mut children, op) => {
for child in &mut **children {
child.simplify_and_sort_children();
}
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].partial_cmp(&children[result]) {
// 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_mut().enumerate() {
child.simplify_and_sort_children();
if let Self::Sum(ref mut 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 {
return replace_self_with!(&mut children_slot[0]);
}
let mut children = mem::replace(children_slot, Box::new([])).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.calc_node_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.
mem::replace(children_slot, children.into_boxed_slice());
}
},
Self::Length(ref mut len) => {
if let NoCalcLength::Absolute(ref mut absolute_length) = *len {
*absolute_length = AbsoluteLength::Px(absolute_length.to_px());
}
}
Self::Percentage(..) |
Self::Angle(..) |
Self::Time(..) |
Self::Number(..) => {
// These are leaves already, nothing to do.
},
}
}
/// Tries to parse a single element in the expression, that is, a
/// `<length>`, `<angle>`, `<time>`, `<percentage>`, according to
/// `expected_unit`.
@ -509,13 +769,14 @@ impl CalcNode {
/// Tries to simplify this expression into a `<length>` or `<percentage`>
/// value.
fn to_length_or_percentage(
&self,
&mut self,
clamping_mode: AllowedNumericType,
) -> Result<CalcLengthPercentage, ()> {
let mut ret = CalcLengthPercentage {
clamping_mode,
..Default::default()
};
self.simplify_and_sort_children();
self.add_length_or_percentage_to(&mut ret, 1.0)?;
Ok(ret)
}

View file

@ -96,6 +96,17 @@ impl FontRelativeLength {
}
}
fn try_sum(&self, other: &Self) -> Result<Self, ()> {
use self::FontRelativeLength::*;
Ok(match (self, other) {
(&Em(one), &Em(other)) => Em(one + other),
(&Ex(one), &Ex(other)) => Ex(one + other),
(&Ch(one), &Ch(other)) => Ch(one + other),
(&Rem(one), &Rem(other)) => Rem(one + other),
_ => return Err(()),
})
}
/// Computes the font-relative length.
pub fn to_computed_value(
&self,
@ -247,6 +258,17 @@ impl ViewportPercentageLength {
}
}
fn try_sum(&self, other: &Self) -> Result<Self, ()> {
use self::ViewportPercentageLength::*;
Ok(match (self, other) {
(&Vw(one), &Vw(other)) => Vw(one + other),
(&Vh(one), &Vh(other)) => Vh(one + other),
(&Vmin(one), &Vmin(other)) => Vmin(one + other),
(&Vmax(one), &Vmax(other)) => Vmax(one + other),
_ => return Err(()),
})
}
/// Computes the given viewport-relative length for the given viewport size.
pub fn to_computed_value(&self, viewport_size: Size2D<Au>) -> CSSPixelLength {
let (factor, length) = match *self {
@ -354,6 +376,12 @@ impl ToComputedValue for AbsoluteLength {
}
}
impl PartialOrd for AbsoluteLength {
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
self.to_px().partial_cmp(&other.to_px())
}
}
impl Mul<CSSFloat> for AbsoluteLength {
type Output = AbsoluteLength;
@ -468,6 +496,21 @@ impl NoCalcLength {
})
}
/// Try to sume two lengths if compatible into the left hand side.
pub(crate) fn try_sum(&self, other: &Self) -> Result<Self, ()> {
use self::NoCalcLength::*;
Ok(match (self, other) {
(&Absolute(ref one), &Absolute(ref other)) => Absolute(*one + *other),
(&FontRelative(ref one), &FontRelative(ref other)) => FontRelative(one.try_sum(other)?),
(&ViewportPercentage(ref one), &ViewportPercentage(ref other)) => ViewportPercentage(one.try_sum(other)?),
(&ServoCharacterWidth(ref one), &ServoCharacterWidth(ref other)) => {
ServoCharacterWidth(CharacterWidth(one.0 + other.0))
},
_ => return Err(()),
})
}
/// Get a px value without context.
#[inline]
pub fn to_computed_pixel_length_without_context(&self) -> Result<CSSFloat, ()> {
@ -486,6 +529,24 @@ impl NoCalcLength {
impl SpecifiedValueInfo for NoCalcLength {}
impl PartialOrd for NoCalcLength {
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
use self::NoCalcLength::*;
match (self, other) {
(&Absolute(ref one), &Absolute(ref other)) => one.to_px().partial_cmp(&other.to_px()),
(&FontRelative(ref one), &FontRelative(ref other)) => one.partial_cmp(other),
(&ViewportPercentage(ref one), &ViewportPercentage(ref other)) => one.partial_cmp(other),
(&ServoCharacterWidth(ref one), &ServoCharacterWidth(ref other)) => one.0.partial_cmp(&other.0),
_ => {
// This will at least catch issues some of the time... Maybe
// could be worth a derive thing?
debug_assert_ne!(self, other, "Forgot a match arm?");
None
}
}
}
}
impl Zero for NoCalcLength {
fn zero() -> Self {
NoCalcLength::Absolute(AbsoluteLength::Px(0.))
@ -534,6 +595,23 @@ impl Mul<CSSFloat> for Length {
}
}
impl PartialOrd for FontRelativeLength {
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
use self::FontRelativeLength::*;
match (self, other) {
(&Em(ref one), &Em(ref other)) => one.partial_cmp(other),
(&Ex(ref one), &Ex(ref other)) => one.partial_cmp(other),
(&Ch(ref one), &Ch(ref other)) => one.partial_cmp(other),
(&Rem(ref one), &Rem(ref other)) => one.partial_cmp(other),
_ => {
// This will at least catch issues some of the time.
debug_assert_ne!(self, other, "Forgot a match arm?");
None
}
}
}
}
impl Mul<CSSFloat> for FontRelativeLength {
type Output = FontRelativeLength;
@ -562,6 +640,23 @@ impl Mul<CSSFloat> for ViewportPercentageLength {
}
}
impl PartialOrd for ViewportPercentageLength {
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
use self::ViewportPercentageLength::*;
match (self, other) {
(&Vw(ref one), &Vw(ref other)) => one.partial_cmp(other),
(&Vh(ref one), &Vh(ref other)) => one.partial_cmp(other),
(&Vmin(ref one), &Vmin(ref other)) => one.partial_cmp(other),
(&Vmax(ref one), &Vmax(ref other)) => one.partial_cmp(other),
_ => {
// This will at least catch issues some of the time.
debug_assert_ne!(self, other, "Forgot a match arm?");
None
}
}
}
}
impl Length {
#[inline]
fn parse_internal<'i, 't>(