mirror of
https://github.com/servo/servo.git
synced 2025-08-06 06:00:15 +01:00
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:
parent
71b414f9dc
commit
3e14422788
2 changed files with 366 additions and 10 deletions
|
@ -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
|
||||
}
|
||||
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(¢er) {
|
||||
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(¢er) {
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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>(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue