/* 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/. */ //! Specified numbers and integers. use crate::derives::*; use crate::parser::{Parse, ParserContext}; use crate::values::computed::transform::DirectionVector; use crate::values::computed::{Context, ToComputedValue}; use crate::values::generics::transform::IsParallelTo; use crate::values::generics::{GreaterThanOrEqualToOne, NonNegative}; use crate::values::specified::calc::{CalcNode, CalcNumeric, Leaf}; use crate::values::specified::{NoCalcPercentage, Percentage}; use crate::values::tagged_numeric::{NumericUnion, Unpacked, UnpackedMut}; use crate::values::{serialize_number, CSSFloat, CSSInteger}; use crate::{One, Zero}; use cssparser::{Parser, Token}; use std::fmt::{self, Write}; use style_traits::values::specified::AllowedNumericType; use style_traits::{ CssWriter, ParseError, ParsingMode, SpecifiedValueInfo, ToCss, ToTyped, TypedValue, }; use thin_vec::ThinVec; /// Parse a `` value, with a given clamping mode. pub fn parse_number_with_clamping_mode<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, clamping_mode: AllowedNumericType, ) -> Result> { let location = input.current_source_location(); Ok(Number(match *input.next()? { Token::Number { value, .. } if clamping_mode.is_ok(context.parsing_mode, value) => { NumericUnion::inline((), value) }, Token::Function(ref name) => { let function = CalcNode::math_function(context, name, location)?; let number = CalcNode::parse_number(context, input, clamping_mode, function)?; NumericUnion::boxed(Box::new(number)) }, ref t => return Err(location.new_unexpected_token_error(t.clone())), })) } /// Parse an `` value, with a given clamping mode. pub fn parse_integer_with_clamping_mode<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, clamping_mode: AllowedNumericType, ) -> Result> { let location = input.current_source_location(); Ok(Integer(match *input.next()? { Token::Number { int_value: Some(v), .. } if clamping_mode.is_ok(context.parsing_mode, v as f32) => NumericUnion::inline((), v), Token::Function(ref name) => { let function = CalcNode::math_function(context, name, location)?; let calc = CalcNode::parse_number(context, input, clamping_mode, function)?; NumericUnion::boxed(Box::new(calc)) }, ref t => return Err(location.new_unexpected_token_error(t.clone())), })) } /// A non-calc `` value. #[derive(Clone, Copy, Debug, MallocSizeOf, ToShmem, ToTyped)] #[repr(C)] pub struct NoCalcNumber(CSSFloat); impl NoCalcNumber { /// Returns a new literal number with the value `val`. #[inline] pub fn new(val: CSSFloat) -> Self { Self(val) } /// Returns the raw, underlying value of this number. #[inline] pub fn value(&self) -> f32 { self.0 } /// Returns the numeric value, clamped if needed. #[inline] pub fn get(&self) -> f32 { crate::values::normalize(self.0).min(f32::MAX).max(f32::MIN) } /// Returns the unit string for a number value. pub fn unit(&self) -> &'static str { "number" } /// Returns the canonical unit for a number value (none). pub fn canonical_unit(&self) -> Option<&'static str> { None } /// Converts to the given unit, only succeeding if the unit is "number". pub fn to(&self, unit: &str) -> Result { if !unit.eq_ignore_ascii_case("number") { return Err(()); } Ok(self.clone()) } } impl PartialEq for NoCalcNumber { fn eq(&self, other: &NoCalcNumber) -> bool { self.0 == other.0 || (self.0.is_nan() && other.0.is_nan()) } } impl PartialOrd for NoCalcNumber { fn partial_cmp(&self, other: &NoCalcNumber) -> Option { self.get().partial_cmp(&other.get()) } } impl ToCss for NoCalcNumber { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: Write, { serialize_number(self.0, dest) } } impl ToComputedValue for NoCalcNumber { type ComputedValue = CSSFloat; fn to_computed_value(&self, _: &Context) -> Self::ComputedValue { self.get() } fn from_computed_value(computed: &Self::ComputedValue) -> Self { Self::new(*computed) } } /// A CSS `` specified value. /// /// https://drafts.csswg.org/css-values-3/#number-value #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] pub struct Number(NumericUnion<(), f32, CalcNumeric>); impl ToCss for Number { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: Write, { match self.0.unpack() { Unpacked::Inline(_, v) => NoCalcNumber(v).to_css(dest), Unpacked::Boxed(calc) => calc.to_css(dest), } } } impl ToTyped for Number { fn to_typed(&self, dest: &mut ThinVec) -> Result<(), ()> { match self.0.unpack() { Unpacked::Inline((), v) => NoCalcNumber(v).to_typed(dest), Unpacked::Boxed(ref calc) => calc.to_typed(dest), } } } impl Parse for Number { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { parse_number_with_clamping_mode(context, input, AllowedNumericType::All) } } impl PartialOrd for Number { fn partial_cmp(&self, other: &Number) -> Option { self.get().partial_cmp(&other.get()) } } impl Number { /// Returns a new number with the value `val`. #[inline] pub fn new(val: CSSFloat) -> Self { Self(NumericUnion::inline((), val)) } /// Returns a new number with the value `val`. #[inline] pub fn new_calc(val: Box) -> Self { Self(NumericUnion::boxed(val)) } /// Returns this number as a percentage. pub fn to_percentage(&self) -> Option { Some(match self.0.unpack() { Unpacked::Inline((), n) => Percentage::new(n), Unpacked::Boxed(ref calc) => { let n = calc.as_number()?.get(); Percentage::new_calc(Box::new( calc.with_leaf_node(Leaf::Percentage(NoCalcPercentage::new(n))), )) }, }) } /// Returns the value if this is a plain (non-calc) number, or None otherwise. /// Use `resolve()` to also handle resolvable calc expressions, or `to_computed_value()` /// when computed context is available. #[inline] pub fn get(&self) -> Option { match self.0.unpack() { Unpacked::Inline((), f) => Some(NoCalcNumber(f).get()), Unpacked::Boxed(..) => None, } } /// Returns the value if it can be resolved at parse time, including resolvable calc /// expressions. Returns None for calc expressions that require computed-value context. pub fn resolve(&self) -> Option { match self.0.unpack() { Unpacked::Inline((), f) => Some(NoCalcNumber(f).get()), Unpacked::Boxed(ref calc) => calc.as_number().map(|n| n.get()), } } #[allow(missing_docs)] pub fn parse_non_negative<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { parse_number_with_clamping_mode(context, input, AllowedNumericType::NonNegative) } #[allow(missing_docs)] pub fn parse_at_least_one<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { parse_number_with_clamping_mode(context, input, AllowedNumericType::AtLeastOne) } /// Clamp to 1.0 if the value is over 1.0. #[inline] pub fn clamp_to_one(&mut self) { match self.0.unpack_mut() { UnpackedMut::Inline(_, ref mut v) => **v = v.min(1.), UnpackedMut::Boxed(ref mut calc) => { calc.clamping_mode = AllowedNumericType::ZeroToOne; }, } } } impl ToComputedValue for Number { type ComputedValue = CSSFloat; #[inline] fn to_computed_value(&self, context: &Context) -> CSSFloat { match self.0.unpack() { Unpacked::Inline((), n) => NoCalcNumber(n).to_computed_value(context), Unpacked::Boxed(ref calc) => { let value = calc.resolve(context, |result| match result { Ok(Leaf::Number(n)) => n.get(), _ => { debug_assert!(false, "Unexpected Number::Calc without resolved number"); f32::NAN }, }); crate::values::normalize(value).min(f32::MAX).max(f32::MIN) }, } } #[inline] fn from_computed_value(computed: &CSSFloat) -> Self { Number::new(*computed) } } impl IsParallelTo for (Number, Number, Number) { fn is_parallel_to(&self, vector: &DirectionVector) -> bool { use euclid::approxeq::ApproxEq; // If a and b is parallel, the angle between them is 0deg, so // a x b = |a|*|b|*sin(0)*n = 0 * n, |a x b| == 0. match (self.0.get(), self.1.get(), self.2.get()) { (Some(x), Some(y), Some(z)) => DirectionVector::new(x, y, z) .cross(*vector) .square_length() .approx_eq(&0.0f32), _ => false, } } } impl SpecifiedValueInfo for Number {} impl Zero for Number { #[inline] fn zero() -> Self { Self::new(0.) } // Returns true if this number was a non-calc 0. #[inline] fn is_zero(&self) -> bool { self.get() == Some(0.) } } /// A Number which is >= 0.0. pub type NonNegativeNumber = NonNegative; impl Parse for NonNegativeNumber { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { parse_number_with_clamping_mode(context, input, AllowedNumericType::NonNegative) .map(NonNegative::) } } impl One for NonNegativeNumber { #[inline] fn one() -> Self { NonNegativeNumber::new(1.0) } // Returns true if this number was a non-calc 1. #[inline] fn is_one(&self) -> bool { self.get() == Some(1.) } } impl NonNegativeNumber { /// Returns a new non-negative number with the value `val`. pub fn new(val: CSSFloat) -> Self { NonNegative(Number::new(val.max(0.))) } /// Returns the numeric value. #[inline] pub fn get(&self) -> Option { self.0.get() } } /// An Integer which is >= 0. For calc expressions that couldn't be resolved at parse time, /// this value is clamped to 0 at computed-value time. pub type NonNegativeInteger = NonNegative; impl Parse for NonNegativeInteger { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { Ok(NonNegative(Integer::parse_non_negative(context, input)?)) } } /// A Number which is >= 1.0. pub type GreaterThanOrEqualToOneNumber = GreaterThanOrEqualToOne; impl Parse for GreaterThanOrEqualToOneNumber { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { parse_number_with_clamping_mode(context, input, AllowedNumericType::AtLeastOne) .map(GreaterThanOrEqualToOne::) } } /// A specified ``, either a simple integer value, a resolved calc expression, /// or a full calc expression tree that cannot be computed at parse time. /// Note that a calc expression may not actually be an integer; it will be rounded /// at computed-value time. /// /// #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] pub struct Integer(NumericUnion<(), i32, CalcNumeric>); impl Zero for Integer { #[inline] fn zero() -> Self { Self::new(0) } // Returns true if this integer was a non-calc 0. #[inline] fn is_zero(&self) -> bool { self.get() == Some(0) } } impl One for Integer { #[inline] fn one() -> Self { Self::new(1) } // Returns true if this integer was a non-calc 1. #[inline] fn is_one(&self) -> bool { self.get() == Some(1) } } impl PartialEq for Integer { fn eq(&self, value: &i32) -> bool { self.get().is_some_and(|v| v == *value) } } impl Integer { /// Trivially constructs a new `Integer` value. pub fn new(val: CSSInteger) -> Self { Self(NumericUnion::inline((), val)) } /// Returns the value if this is a plain (non-calc) integer, or None otherwise. /// Use `resolve()` to also handle resolvable calc expressions, or `to_computed_value()` /// when computed context is available. pub fn get(&self) -> Option { match self.0.unpack() { Unpacked::Inline((), v) => Some(v), Unpacked::Boxed(..) => None, } } /// Returns the value if it can be resolved at parse time, including resolvable calc /// expressions. Returns None for calc expressions that require computed-value context. pub fn resolve(&self) -> Option { Some(match self.0.unpack() { Unpacked::Inline((), v) => v, Unpacked::Boxed(ref calc) => { let value = calc.as_number()?.get(); (value + 0.5).floor() as CSSInteger }, }) } /// Makes sure this number matches the clamping, or errors otherwise. pub fn ensure_clamping_mode(&mut self, clamping_mode: AllowedNumericType) -> Result<(), ()> { match self.0.unpack_mut() { UnpackedMut::Inline(_, i) => { if !clamping_mode.is_ok(ParsingMode::DEFAULT, *i as f32) { return Err(()); } }, UnpackedMut::Boxed(ref mut calc) => { calc.clamping_mode = clamping_mode; }, } Ok(()) } } impl Parse for Integer { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { parse_integer_with_clamping_mode(context, input, AllowedNumericType::All) } } impl Integer { /// Parse a non-negative integer. pub fn parse_non_negative<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { parse_integer_with_clamping_mode(context, input, AllowedNumericType::NonNegative) } /// Parse a positive integer (>= 1). pub fn parse_positive<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { parse_integer_with_clamping_mode(context, input, AllowedNumericType::AtLeastOne) } } impl ToCss for Integer { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: Write, { match self.0.unpack() { Unpacked::Inline(_, v) => v.to_css(dest), Unpacked::Boxed(calc) => calc.to_css(dest), } } } impl ToTyped for Integer { fn to_typed(&self, dest: &mut ThinVec) -> Result<(), ()> { match self.0.unpack() { Unpacked::Inline((), n) => n.to_typed(dest), Unpacked::Boxed(ref calc) => calc.to_typed(dest), } } } impl ToComputedValue for Integer { type ComputedValue = i32; #[inline] fn to_computed_value(&self, context: &Context) -> i32 { match self.0.unpack() { Unpacked::Inline((), i) => i, Unpacked::Boxed(ref calc) => { let value = calc.resolve(context, |result| match result { Ok(Leaf::Number(n)) => n.get(), _ => { debug_assert!(false, "Unexpected Integer::Calc without resolved number"); f32::NAN }, }); let clamped = crate::values::normalize(value).min(f32::MAX).max(f32::MIN); (clamped + 0.5).floor() as i32 }, } } #[inline] fn from_computed_value(computed: &i32) -> Self { Self::new(*computed) } } impl SpecifiedValueInfo for Integer {} /// An Integer which is >= 1. For calc expressions that couldn't be resolved at parse time, /// this value is clamped to 1 at computed-value time. pub type PositiveInteger = GreaterThanOrEqualToOne; impl Parse for PositiveInteger { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { Integer::parse_positive(context, input).map(GreaterThanOrEqualToOne) } }