Skip to main content

style/values/specified/
number.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! Specified numbers and integers.
6
7use crate::derives::*;
8use crate::parser::{Parse, ParserContext};
9use crate::typed_om::{NumericBaseType, ToTyped, TypedValue};
10use crate::values::computed::transform::DirectionVector;
11use crate::values::computed::{Context, ToComputedValue};
12use crate::values::generics::transform::IsParallelTo;
13use crate::values::generics::Optional;
14use crate::values::generics::{GreaterThanOrEqualToOne, NonNegative};
15use crate::values::specified::calc::{
16    CalcNode, CalcNumeric, CalcPercentageLeaf, Leaf, PercentageContext,
17};
18use crate::values::specified::Percentage;
19use crate::values::tagged_numeric::{NumericUnion, Unpacked, UnpackedMut};
20use crate::values::{serialize_number, CSSFloat, CSSInteger};
21use crate::{One, Zero};
22use cssparser::{Parser, Token};
23use std::fmt::{self, Write};
24use style_traits::values::specified::AllowedNumericType;
25use style_traits::{CssWriter, ParseError, ParsingMode, SpecifiedValueInfo, ToCss};
26use thin_vec::ThinVec;
27
28/// Parse a `<number>` value, with a given clamping mode.
29pub fn parse_number_with_clamping_mode(
30    context: &ParserContext,
31    input: &mut Parser,
32    clamping_mode: AllowedNumericType,
33    percentage_context: PercentageContext,
34) -> Result<Number, ParseError> {
35    Ok(Number(match *input.next()? {
36        Token::Number { value, .. } if clamping_mode.is_ok(context.parsing_mode, value) => {
37            NumericUnion::inline((), value)
38        },
39        Token::Function(ref name) => {
40            let function = CalcNode::math_function(context, name)?;
41            let number = CalcNode::parse_number(
42                context,
43                input,
44                clamping_mode,
45                function,
46                percentage_context,
47            )?;
48            NumericUnion::boxed(Box::new(number))
49        },
50        _ => return Err(ParseError::unexpected_token()),
51    }))
52}
53
54/// Parse an `<integer>` value, with a given clamping mode.
55pub fn parse_integer_with_clamping_mode(
56    context: &ParserContext,
57    input: &mut Parser,
58    clamping_mode: AllowedNumericType,
59    percentage_context: PercentageContext,
60) -> Result<Integer, ParseError> {
61    Ok(Integer(match *input.next()? {
62        Token::Number {
63            int_value: Some(v), ..
64        } if clamping_mode.is_ok(context.parsing_mode, v as f32) => NumericUnion::inline((), v),
65        Token::Function(ref name) => {
66            let function = CalcNode::math_function(context, name)?;
67            let calc = CalcNode::parse_number(
68                context,
69                input,
70                clamping_mode,
71                function,
72                percentage_context,
73            )?;
74            NumericUnion::boxed(Box::new(calc))
75        },
76        _ => return Err(ParseError::unexpected_token()),
77    }))
78}
79
80/// A non-calc `<number>` value.
81#[derive(Clone, Copy, Debug, MallocSizeOf, ToShmem, ToTyped)]
82#[repr(C)]
83pub struct NoCalcNumber(CSSFloat);
84
85impl NoCalcNumber {
86    /// Returns a new literal number with the value `val`.
87    #[inline]
88    pub fn new(val: CSSFloat) -> Self {
89        Self(val)
90    }
91
92    /// Returns the raw, underlying value of this number.
93    #[inline]
94    pub fn value(&self) -> f32 {
95        self.0
96    }
97
98    /// Returns the numeric value, clamped if needed.
99    #[inline]
100    pub fn get(&self) -> f32 {
101        crate::values::normalize(self.0).min(f32::MAX).max(f32::MIN)
102    }
103
104    /// Returns the unit string for a number value.
105    pub fn unit(&self) -> &'static str {
106        "number"
107    }
108
109    /// Returns the canonical unit for a number value (none).
110    pub fn canonical_unit(&self) -> Option<&'static str> {
111        None
112    }
113
114    /// Converts to the given unit, only succeeding if the unit is "number".
115    pub fn to(&self, unit: &str) -> Result<Self, ()> {
116        if !unit.eq_ignore_ascii_case("number") {
117            return Err(());
118        }
119        Ok(*self)
120    }
121}
122
123impl PartialEq<NoCalcNumber> for NoCalcNumber {
124    fn eq(&self, other: &NoCalcNumber) -> bool {
125        self.0 == other.0 || (self.0.is_nan() && other.0.is_nan())
126    }
127}
128
129impl PartialOrd<NoCalcNumber> for NoCalcNumber {
130    fn partial_cmp(&self, other: &NoCalcNumber) -> Option<std::cmp::Ordering> {
131        self.get().partial_cmp(&other.get())
132    }
133}
134
135impl ToCss for NoCalcNumber {
136    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
137    where
138        W: Write,
139    {
140        serialize_number(self.0, dest)
141    }
142}
143
144impl ToComputedValue for NoCalcNumber {
145    type ComputedValue = CSSFloat;
146
147    fn to_computed_value(&self, _: &Context) -> Self::ComputedValue {
148        self.get()
149    }
150
151    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
152        Self::new(*computed)
153    }
154}
155
156/// A CSS `<number>` specified value.
157///
158/// https://drafts.csswg.org/css-values-3/#number-value
159#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
160pub struct Number(NumericUnion<(), f32, CalcNumeric>);
161
162impl ToCss for Number {
163    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
164    where
165        W: Write,
166    {
167        match self.0.unpack() {
168            Unpacked::Inline(_, v) => NoCalcNumber(v).to_css(dest),
169            Unpacked::Boxed(calc) => calc.to_css(dest),
170        }
171    }
172}
173
174impl ToTyped for Number {
175    fn to_typed(&self, dest: &mut ThinVec<TypedValue>) -> Result<(), ()> {
176        match self.0.unpack() {
177            Unpacked::Inline((), v) => NoCalcNumber(v).to_typed(dest),
178            Unpacked::Boxed(ref calc) => calc.to_typed(dest),
179        }
180    }
181}
182
183impl Parse for Number {
184    fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
185        parse_number_with_clamping_mode(
186            context,
187            input,
188            AllowedNumericType::All,
189            PercentageContext::not_allowed(),
190        )
191    }
192}
193
194impl PartialOrd<Number> for Number {
195    fn partial_cmp(&self, other: &Number) -> Option<std::cmp::Ordering> {
196        self.get().partial_cmp(&other.get())
197    }
198}
199
200impl Number {
201    /// Returns a new number with the value `val`.
202    #[inline]
203    pub fn new(val: CSSFloat) -> Self {
204        Self(NumericUnion::inline((), val))
205    }
206
207    /// Returns a new number with the value `val`.
208    #[inline]
209    pub fn new_calc(val: Box<CalcNumeric>) -> Self {
210        Self(NumericUnion::boxed(val))
211    }
212
213    /// Returns this number as a percentage.
214    pub fn to_percentage(&self) -> Option<Percentage> {
215        Some(match self.0.unpack() {
216            Unpacked::Inline((), n) => Percentage::new(n),
217            Unpacked::Boxed(calc) => {
218                let n = calc.as_number()?.get();
219                Percentage::new_calc(Box::new(calc.with_leaf_node(Leaf::Percentage(
220                    CalcPercentageLeaf::new(n, Optional::Some(NumericBaseType::Percent)),
221                ))))
222            },
223        })
224    }
225
226    /// Returns the value if this is a plain (non-calc) number, or None otherwise.
227    /// Use `resolve()` to also handle resolvable calc expressions, or `to_computed_value()`
228    /// when computed context is available.
229    #[inline]
230    pub fn get(&self) -> Option<f32> {
231        match self.0.unpack() {
232            Unpacked::Inline((), f) => Some(NoCalcNumber(f).get()),
233            Unpacked::Boxed(..) => None,
234        }
235    }
236
237    /// Returns the value if it can be resolved at parse time, including resolvable calc
238    /// expressions. Returns None for calc expressions that require computed-value context.
239    pub fn resolve(&self) -> Option<f32> {
240        match self.0.unpack() {
241            Unpacked::Inline((), f) => Some(NoCalcNumber(f).get()),
242            Unpacked::Boxed(calc) => calc.as_number().map(|n| n.get()),
243        }
244    }
245
246    /// Returns the calc tree if this number is a calc expression.
247    #[inline]
248    pub fn as_calc(&self) -> Option<&CalcNumeric> {
249        match self.0.unpack() {
250            Unpacked::Inline(..) => None,
251            Unpacked::Boxed(calc) => Some(calc),
252        }
253    }
254
255    #[allow(missing_docs)]
256    pub fn parse_non_negative(
257        context: &ParserContext,
258        input: &mut Parser,
259    ) -> Result<Number, ParseError> {
260        parse_number_with_clamping_mode(
261            context,
262            input,
263            AllowedNumericType::NonNegative,
264            PercentageContext::not_allowed(),
265        )
266    }
267
268    #[allow(missing_docs)]
269    pub fn parse_at_least_one(
270        context: &ParserContext,
271        input: &mut Parser,
272    ) -> Result<Number, ParseError> {
273        parse_number_with_clamping_mode(
274            context,
275            input,
276            AllowedNumericType::AtLeastOne,
277            PercentageContext::not_allowed(),
278        )
279    }
280
281    /// Clamp to 1.0 if the value is over 1.0.
282    #[inline]
283    pub fn clamp_to_one(&mut self) {
284        match self.0.unpack_mut() {
285            UnpackedMut::Inline(_, ref mut v) => **v = v.min(1.),
286            UnpackedMut::Boxed(ref mut calc) => {
287                calc.clamping_mode = AllowedNumericType::ZeroToOne;
288            },
289        }
290    }
291}
292
293impl ToComputedValue for Number {
294    type ComputedValue = CSSFloat;
295
296    #[inline]
297    fn to_computed_value(&self, context: &Context) -> CSSFloat {
298        match self.0.unpack() {
299            Unpacked::Inline((), n) => NoCalcNumber(n).to_computed_value(context),
300            Unpacked::Boxed(calc) => {
301                let value = calc.resolve(context, |result| match result {
302                    Ok(Leaf::Number(n)) => n.get(),
303                    _ => {
304                        debug_assert!(false, "Unexpected Number::Calc without resolved number");
305                        f32::NAN
306                    },
307                });
308                crate::values::normalize(value).min(f32::MAX).max(f32::MIN)
309            },
310        }
311    }
312
313    #[inline]
314    fn from_computed_value(computed: &CSSFloat) -> Self {
315        Number::new(*computed)
316    }
317}
318
319impl IsParallelTo for (Number, Number, Number) {
320    fn is_parallel_to(&self, vector: &DirectionVector) -> bool {
321        use euclid::approxeq::ApproxEq;
322        // If a and b is parallel, the angle between them is 0deg, so
323        // a x b = |a|*|b|*sin(0)*n = 0 * n, |a x b| == 0.
324        match (self.0.get(), self.1.get(), self.2.get()) {
325            (Some(x), Some(y), Some(z)) => DirectionVector::new(x, y, z)
326                .cross(*vector)
327                .square_length()
328                .approx_eq(&0.0f32),
329            _ => false,
330        }
331    }
332}
333
334impl SpecifiedValueInfo for Number {}
335
336impl Zero for Number {
337    #[inline]
338    fn zero() -> Self {
339        Self::new(0.)
340    }
341
342    // Returns true if this number was a non-calc 0.
343    #[inline]
344    fn is_zero(&self) -> bool {
345        self.get() == Some(0.)
346    }
347}
348
349/// A Number which is >= 0.0.
350pub type NonNegativeNumber = NonNegative<Number>;
351
352impl Parse for NonNegativeNumber {
353    fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
354        parse_number_with_clamping_mode(
355            context,
356            input,
357            AllowedNumericType::NonNegative,
358            PercentageContext::not_allowed(),
359        )
360        .map(NonNegative::<Number>)
361    }
362}
363
364impl One for NonNegativeNumber {
365    #[inline]
366    fn one() -> Self {
367        NonNegativeNumber::new(1.0)
368    }
369
370    // Returns true if this number was a non-calc 1.
371    #[inline]
372    fn is_one(&self) -> bool {
373        self.get() == Some(1.)
374    }
375}
376
377impl NonNegativeNumber {
378    /// Returns a new non-negative number with the value `val`.
379    pub fn new(val: CSSFloat) -> Self {
380        NonNegative(Number::new(val.max(0.)))
381    }
382
383    /// Returns the numeric value.
384    #[inline]
385    pub fn get(&self) -> Option<f32> {
386        self.0.get()
387    }
388
389    /// Returns the calc tree if this number is a calc expression.
390    #[inline]
391    pub fn as_calc(&self) -> Option<&CalcNumeric> {
392        self.0.as_calc()
393    }
394}
395
396/// An Integer which is >= 0. For calc expressions that couldn't be resolved at parse time,
397/// this value is clamped to 0 at computed-value time.
398pub type NonNegativeInteger = NonNegative<Integer>;
399
400impl Parse for NonNegativeInteger {
401    fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
402        Ok(NonNegative(Integer::parse_non_negative(context, input)?))
403    }
404}
405
406/// A Number which is >= 1.0.
407pub type GreaterThanOrEqualToOneNumber = GreaterThanOrEqualToOne<Number>;
408
409impl Parse for GreaterThanOrEqualToOneNumber {
410    fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
411        parse_number_with_clamping_mode(
412            context,
413            input,
414            AllowedNumericType::AtLeastOne,
415            PercentageContext::not_allowed(),
416        )
417        .map(GreaterThanOrEqualToOne::<Number>)
418    }
419}
420
421/// A specified `<integer>`, either a simple integer value, a resolved calc expression,
422/// or a full calc expression tree that cannot be computed at parse time.
423/// Note that a calc expression may not actually be an integer; it will be rounded
424/// at computed-value time.
425///
426/// <https://drafts.csswg.org/css-values/#integers>
427#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
428pub struct Integer(NumericUnion<(), i32, CalcNumeric>);
429
430impl Zero for Integer {
431    #[inline]
432    fn zero() -> Self {
433        Self::new(0)
434    }
435
436    // Returns true if this integer was a non-calc 0.
437    #[inline]
438    fn is_zero(&self) -> bool {
439        self.get() == Some(0)
440    }
441}
442
443impl One for Integer {
444    #[inline]
445    fn one() -> Self {
446        Self::new(1)
447    }
448
449    // Returns true if this integer was a non-calc 1.
450    #[inline]
451    fn is_one(&self) -> bool {
452        self.get() == Some(1)
453    }
454}
455
456impl PartialEq<i32> for Integer {
457    fn eq(&self, value: &i32) -> bool {
458        self.get().is_some_and(|v| v == *value)
459    }
460}
461
462impl Integer {
463    /// Trivially constructs a new `Integer` value.
464    pub fn new(val: CSSInteger) -> Self {
465        Self(NumericUnion::inline((), val))
466    }
467
468    /// Returns the value if this is a plain (non-calc) integer, or None otherwise.
469    /// Use `resolve()` to also handle resolvable calc expressions, or `to_computed_value()`
470    /// when computed context is available.
471    pub fn get(&self) -> Option<CSSInteger> {
472        match self.0.unpack() {
473            Unpacked::Inline((), v) => Some(v),
474            Unpacked::Boxed(..) => None,
475        }
476    }
477
478    /// Returns the value if it can be resolved at parse time, including resolvable calc
479    /// expressions. Returns None for calc expressions that require computed-value context.
480    pub fn resolve(&self) -> Option<CSSInteger> {
481        Some(match self.0.unpack() {
482            Unpacked::Inline((), v) => v,
483            Unpacked::Boxed(calc) => {
484                let value = calc.as_number()?.get();
485                (value + 0.5).floor() as CSSInteger
486            },
487        })
488    }
489
490    /// Makes sure this number matches the clamping, or errors otherwise.
491    pub fn ensure_clamping_mode(&mut self, clamping_mode: AllowedNumericType) -> Result<(), ()> {
492        match self.0.unpack_mut() {
493            UnpackedMut::Inline(_, i) => {
494                if !clamping_mode.is_ok(ParsingMode::DEFAULT, *i as f32) {
495                    return Err(());
496                }
497            },
498            UnpackedMut::Boxed(ref mut calc) => {
499                calc.clamping_mode = clamping_mode;
500            },
501        }
502        Ok(())
503    }
504}
505
506impl Parse for Integer {
507    fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
508        parse_integer_with_clamping_mode(
509            context,
510            input,
511            AllowedNumericType::All,
512            PercentageContext::not_allowed(),
513        )
514    }
515}
516
517impl Integer {
518    /// Parse a non-negative integer.
519    pub fn parse_non_negative(
520        context: &ParserContext,
521        input: &mut Parser,
522    ) -> Result<Integer, ParseError> {
523        parse_integer_with_clamping_mode(
524            context,
525            input,
526            AllowedNumericType::NonNegative,
527            PercentageContext::not_allowed(),
528        )
529    }
530
531    /// Parse a positive integer (>= 1).
532    pub fn parse_positive(
533        context: &ParserContext,
534        input: &mut Parser,
535    ) -> Result<Integer, ParseError> {
536        parse_integer_with_clamping_mode(
537            context,
538            input,
539            AllowedNumericType::AtLeastOne,
540            PercentageContext::not_allowed(),
541        )
542    }
543}
544
545impl ToCss for Integer {
546    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
547    where
548        W: Write,
549    {
550        match self.0.unpack() {
551            Unpacked::Inline(_, v) => v.to_css(dest),
552            Unpacked::Boxed(calc) => calc.to_css(dest),
553        }
554    }
555}
556
557impl ToTyped for Integer {
558    fn to_typed(&self, dest: &mut ThinVec<TypedValue>) -> Result<(), ()> {
559        match self.0.unpack() {
560            Unpacked::Inline((), n) => n.to_typed(dest),
561            Unpacked::Boxed(ref calc) => calc.to_typed(dest),
562        }
563    }
564}
565
566impl ToComputedValue for Integer {
567    type ComputedValue = i32;
568
569    #[inline]
570    fn to_computed_value(&self, context: &Context) -> i32 {
571        match self.0.unpack() {
572            Unpacked::Inline((), i) => i,
573            Unpacked::Boxed(calc) => {
574                let value = calc.resolve(context, |result| match result {
575                    Ok(Leaf::Number(n)) => n.get(),
576                    _ => {
577                        debug_assert!(false, "Unexpected Integer::Calc without resolved number");
578                        f32::NAN
579                    },
580                });
581                let clamped = crate::values::normalize(value).min(f32::MAX).max(f32::MIN);
582                (clamped + 0.5).floor() as i32
583            },
584        }
585    }
586
587    #[inline]
588    fn from_computed_value(computed: &i32) -> Self {
589        Self::new(*computed)
590    }
591}
592
593impl SpecifiedValueInfo for Integer {}
594
595/// An Integer which is >= 1. For calc expressions that couldn't be resolved at parse time,
596/// this value is clamped to 1 at computed-value time.
597pub type PositiveInteger = GreaterThanOrEqualToOne<Integer>;
598
599impl Parse for PositiveInteger {
600    fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
601        Integer::parse_positive(context, input).map(GreaterThanOrEqualToOne)
602    }
603}