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