Source code
Revision control
Copy as Markdown
Other Tools
/* 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
//! 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 `<number>` 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<Number, ParseError<'i>> {
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 `<integer>` 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<Integer, ParseError<'i>> {
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 `<number>` 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<Self, ()> {
if !unit.eq_ignore_ascii_case("number") {
return Err(());
}
Ok(self.clone())
}
}
impl PartialEq<NoCalcNumber> for NoCalcNumber {
fn eq(&self, other: &NoCalcNumber) -> bool {
self.0 == other.0 || (self.0.is_nan() && other.0.is_nan())
}
}
impl PartialOrd<NoCalcNumber> for NoCalcNumber {
fn partial_cmp(&self, other: &NoCalcNumber) -> Option<std::cmp::Ordering> {
self.get().partial_cmp(&other.get())
}
}
impl ToCss for NoCalcNumber {
fn to_css<W>(&self, dest: &mut CssWriter<W>) -> 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 `<number>` specified value.
///
#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
pub struct Number(NumericUnion<(), f32, CalcNumeric>);
impl ToCss for Number {
fn to_css<W>(&self, dest: &mut CssWriter<W>) -> 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<TypedValue>) -> 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<Self, ParseError<'i>> {
parse_number_with_clamping_mode(context, input, AllowedNumericType::All)
}
}
impl PartialOrd<Number> for Number {
fn partial_cmp(&self, other: &Number) -> Option<std::cmp::Ordering> {
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<CalcNumeric>) -> Self {
Self(NumericUnion::boxed(val))
}
/// Returns this number as a percentage.
pub fn to_percentage(&self) -> Option<Percentage> {
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<f32> {
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<f32> {
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<Number, ParseError<'i>> {
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<Number, ParseError<'i>> {
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<Number>;
impl Parse for NonNegativeNumber {
fn parse<'i, 't>(
context: &ParserContext,
input: &mut Parser<'i, 't>,
) -> Result<Self, ParseError<'i>> {
parse_number_with_clamping_mode(context, input, AllowedNumericType::NonNegative)
.map(NonNegative::<Number>)
}
}
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<f32> {
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<Integer>;
impl Parse for NonNegativeInteger {
fn parse<'i, 't>(
context: &ParserContext,
input: &mut Parser<'i, 't>,
) -> Result<Self, ParseError<'i>> {
Ok(NonNegative(Integer::parse_non_negative(context, input)?))
}
}
/// A Number which is >= 1.0.
pub type GreaterThanOrEqualToOneNumber = GreaterThanOrEqualToOne<Number>;
impl Parse for GreaterThanOrEqualToOneNumber {
fn parse<'i, 't>(
context: &ParserContext,
input: &mut Parser<'i, 't>,
) -> Result<Self, ParseError<'i>> {
parse_number_with_clamping_mode(context, input, AllowedNumericType::AtLeastOne)
.map(GreaterThanOrEqualToOne::<Number>)
}
}
/// A specified `<integer>`, 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<i32> 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<CSSInteger> {
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<CSSInteger> {
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<Self, ParseError<'i>> {
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<Integer, ParseError<'i>> {
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<Integer, ParseError<'i>> {
parse_integer_with_clamping_mode(context, input, AllowedNumericType::AtLeastOne)
}
}
impl ToCss for Integer {
fn to_css<W>(&self, dest: &mut CssWriter<W>) -> 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<TypedValue>) -> 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<Integer>;
impl Parse for PositiveInteger {
fn parse<'i, 't>(
context: &ParserContext,
input: &mut Parser<'i, 't>,
) -> Result<Self, ParseError<'i>> {
Integer::parse_positive(context, input).map(GreaterThanOrEqualToOne)
}
}