Skip to main content

style/values/specified/
percentage.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 percentages.
6
7use crate::derives::*;
8use crate::parser::{Parse, ParserContext};
9use crate::typed_om::{ToTyped, TypedValue};
10use crate::values::computed::percentage::Percentage as ComputedPercentage;
11use crate::values::computed::{Context, ToComputedValue};
12use crate::values::generics::{NonNegative, Optional};
13use crate::values::specified::calc::{CalcNode, CalcNumeric, CalcPercentageLeaf, Leaf};
14use crate::values::specified::{CalcLengthPercentage, LengthPercentage, NoCalcNumber, Number};
15use crate::values::tagged_numeric::{Extracted, NumericUnion, Unpacked, UnpackedMut};
16use crate::values::{CSSFloat, normalize, reify_percentage, serialize_percentage};
17use cssparser::{Parser, Token};
18use std::fmt::{self, Write};
19use style_traits::values::specified::AllowedNumericType;
20use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, ToCss};
21use thin_vec::ThinVec;
22
23/// A percentage value, where [0 .. 100%] maps to [0.0 .. 1.0]
24#[derive(
25    Clone,
26    Copy,
27    Debug,
28    Default,
29    Deserialize,
30    MallocSizeOf,
31    PartialEq,
32    PartialOrd,
33    Serialize,
34    ToAnimatedZero,
35    ToResolvedValue,
36    ToShmem,
37)]
38#[repr(C)]
39pub struct NoCalcPercentage(CSSFloat);
40
41impl SpecifiedValueInfo for NoCalcPercentage {}
42
43impl ToCss for NoCalcPercentage {
44    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
45    where
46        W: Write,
47    {
48        serialize_percentage(self.0, dest)
49    }
50}
51
52impl ToTyped for NoCalcPercentage {
53    fn to_typed(&self, dest: &mut ThinVec<TypedValue>) -> Result<(), ()> {
54        reify_percentage(self.0, dest)
55    }
56}
57
58impl NoCalcPercentage {
59    /// Creates a percentage from a numeric value.
60    pub fn new(value: CSSFloat) -> Self {
61        Self(value)
62    }
63
64    /// `0%`
65    #[inline]
66    pub fn zero() -> Self {
67        Self::new(0.)
68    }
69
70    /// `100%`
71    #[inline]
72    pub fn hundred() -> Self {
73        Self::new(1.)
74    }
75
76    /// Gets the underlying value for this float.
77    #[inline]
78    pub fn get(&self) -> CSSFloat {
79        self.0
80    }
81
82    /// Return the unit, as a string.
83    pub fn unit(&self) -> &'static str {
84        "percent"
85    }
86
87    /// Return no canonical unit (percent values do not have one).
88    pub fn canonical_unit(&self) -> Option<&'static str> {
89        None
90    }
91
92    /// Convert only if the unit is the same (conversion to other units does
93    /// not make sense).
94    pub fn to(&self, unit: &str) -> Result<Self, ()> {
95        if !unit.eq_ignore_ascii_case("percent") {
96            return Err(());
97        }
98        Ok(*self)
99    }
100}
101
102impl ToComputedValue for NoCalcPercentage {
103    type ComputedValue = ComputedPercentage;
104
105    fn to_computed_value(&self, _: &Context) -> Self::ComputedValue {
106        ComputedPercentage(normalize(self.get()))
107    }
108
109    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
110        Self::new(computed.0)
111    }
112}
113
114/// A specified percentage value, either a plain value or a `calc()` expression.
115#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
116pub struct Percentage(NumericUnion<(), f32, CalcNumeric>);
117
118impl ToCss for Percentage {
119    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
120    where
121        W: Write,
122    {
123        match self.0.unpack() {
124            Unpacked::Inline((), p) => NoCalcPercentage(p).to_css(dest),
125            Unpacked::Boxed(calc) => calc.to_css(dest),
126        }
127    }
128}
129
130impl ToTyped for Percentage {
131    fn to_typed(&self, dest: &mut ThinVec<TypedValue>) -> Result<(), ()> {
132        match self.0.unpack() {
133            Unpacked::Inline((), p) => NoCalcPercentage(p).to_typed(dest),
134            Unpacked::Boxed(calc) => calc.to_typed(dest),
135        }
136    }
137}
138
139impl Percentage {
140    /// Creates a percentage from a numeric value.
141    pub fn new(value: CSSFloat) -> Self {
142        Self(NumericUnion::inline((), value))
143    }
144
145    /// Returns a new percentage calc value with the value `val`.
146    #[inline]
147    pub fn new_calc(val: Box<CalcNumeric>) -> Self {
148        Self(NumericUnion::boxed(val))
149    }
150
151    /// `0%`
152    #[inline]
153    pub fn zero() -> Self {
154        Self::new(0.)
155    }
156
157    /// `100%`
158    #[inline]
159    pub fn hundred() -> Self {
160        Self::new(1.)
161    }
162
163    /// Returns true if it is a calc percentage.
164    #[inline]
165    pub fn is_calc(&self) -> bool {
166        self.0.is_boxed()
167    }
168
169    /// Returns the value if this is a plain (non-calc) percentage, or None otherwise.
170    /// Use `resolve()` to also handle resolvable calc expressions, or `to_computed_value()`
171    /// when computed context is available.
172    #[inline]
173    pub fn get(&self) -> Option<f32> {
174        match self.0.unpack() {
175            Unpacked::Inline((), f) => Some(f),
176            Unpacked::Boxed(..) => None,
177        }
178    }
179
180    /// Returns the value if it can be resolved at parse time, including resolvable calc
181    /// expressions. Returns None for calc expressions that require computed context
182    /// (e.g. those using relative lengths or sibling-index()).
183    #[inline]
184    pub fn resolve(&self) -> Option<CSSFloat> {
185        match self.0.unpack() {
186            Unpacked::Inline((), f) => Some(f),
187            Unpacked::Boxed(calc) => calc.as_percentage().map(|p| p.get()),
188        }
189    }
190
191    /// Returns this percentage as a number.
192    pub fn to_number(&self) -> Option<Number> {
193        Some(match self.0.unpack() {
194            Unpacked::Inline((), p) => Number::new(p),
195            Unpacked::Boxed(calc) => {
196                let p = calc.as_percentage()?.get();
197                Number::new_calc(Box::new(
198                    calc.with_leaf_node(Leaf::Number(NoCalcNumber::new(p))),
199                ))
200            },
201        })
202    }
203
204    /// Returns this percentage as a LengthPercentage.
205    pub fn to_length_percentage(self) -> LengthPercentage {
206        match self.0.extract() {
207            Extracted::Inline((), p) => LengthPercentage::Percentage(NoCalcPercentage(p)),
208            Extracted::Boxed(calc) => LengthPercentage::Calc(Box::new(CalcLengthPercentage(*calc))),
209        }
210    }
211
212    /// Reverses this percentage, preserving calc-ness.
213    ///
214    /// For example: If it was 20%, convert it into 80%.
215    pub fn reverse(&mut self) {
216        match self.0.unpack_mut() {
217            UnpackedMut::Inline(_, p) => {
218                *p = 1. - *p;
219            },
220            UnpackedMut::Boxed(calc) => {
221                let mut sum = smallvec::SmallVec::<[CalcNode; 2]>::new();
222                sum.push(CalcNode::Leaf(Leaf::Percentage(CalcPercentageLeaf::new(
223                    1.,
224                    Optional::None,
225                ))));
226                let mut node = calc.node.clone();
227                node.negate();
228                sum.push(node);
229                let mut diff = CalcNode::Sum(sum.into_boxed_slice().into());
230                diff.simplify_and_sort();
231                calc.node = diff;
232            },
233        }
234    }
235
236    /// Parses a specific kind of percentage.
237    pub fn parse_with_clamping_mode(
238        context: &ParserContext,
239        input: &mut Parser,
240        num_context: AllowedNumericType,
241    ) -> Result<Self, ParseError> {
242        Ok(Self(match *input.next()? {
243            Token::Percentage { unit_value, .. }
244                if num_context.is_ok(context.parsing_mode, unit_value) =>
245            {
246                NumericUnion::inline((), unit_value)
247            },
248            Token::Function(ref name) => {
249                let function = CalcNode::math_function(context, name)?;
250                let calc = CalcNode::parse_percentage(context, input, num_context, function)?;
251                NumericUnion::boxed(Box::new(calc))
252            },
253            _ => return Err(ParseError::unexpected_token()),
254        }))
255    }
256
257    /// Parses a percentage token, but rejects it if it's negative.
258    pub fn parse_non_negative(
259        context: &ParserContext,
260        input: &mut Parser,
261    ) -> Result<Self, ParseError> {
262        Self::parse_with_clamping_mode(context, input, AllowedNumericType::NonNegative)
263    }
264
265    /// Parses a percentage token, but rejects it if it's negative or more than
266    /// 100%.
267    pub fn parse_zero_to_a_hundred(
268        context: &ParserContext,
269        input: &mut Parser,
270    ) -> Result<Self, ParseError> {
271        Self::parse_with_clamping_mode(context, input, AllowedNumericType::ZeroToOne)
272    }
273
274    /// Clamp to 100% if the value is over 100%.
275    #[inline]
276    pub fn clamp_to_hundred(&mut self) {
277        match self.0.unpack_mut() {
278            UnpackedMut::Inline((), p) => *p = p.min(1.),
279            UnpackedMut::Boxed(calc) => {
280                calc.clamping_mode = AllowedNumericType::ZeroToOne;
281            },
282        }
283    }
284}
285
286impl Parse for Percentage {
287    #[inline]
288    fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
289        Self::parse_with_clamping_mode(context, input, AllowedNumericType::All)
290    }
291}
292
293impl ToComputedValue for Percentage {
294    type ComputedValue = ComputedPercentage;
295
296    #[inline]
297    fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
298        match self.0.unpack() {
299            Unpacked::Inline((), p) => NoCalcPercentage(p).to_computed_value(context),
300            Unpacked::Boxed(calc) => {
301                let value = calc.resolve(context, |result| match result {
302                    Ok(Leaf::Percentage(p)) => p.get(),
303                    _ => {
304                        debug_assert!(
305                            false,
306                            "Unexpected Percentage::Calc without resolved percentage"
307                        );
308                        f32::NAN
309                    },
310                });
311                ComputedPercentage(crate::values::normalize(value).min(f32::MAX).max(f32::MIN))
312            },
313        }
314    }
315
316    #[inline]
317    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
318        Percentage::new(computed.0)
319    }
320}
321
322impl SpecifiedValueInfo for Percentage {}
323
324/// Turns the percentage into a plain float.
325pub trait ToPercentage {
326    /// Returns whether this percentage used to be a calc().
327    fn is_calc(&self) -> bool {
328        false
329    }
330    /// Returns the percentage as a plain float, or None for calc expressions that require
331    /// computed context. Will always return Some if `is_calc` is false.
332    fn to_percentage(&self) -> Option<CSSFloat>;
333}
334
335impl ToPercentage for Percentage {
336    fn is_calc(&self) -> bool {
337        self.0.is_boxed()
338    }
339
340    fn to_percentage(&self) -> Option<CSSFloat> {
341        self.resolve()
342    }
343}
344
345/// A wrapper of Percentage, whose value must be >= 0.
346pub type NonNegativePercentage = NonNegative<Percentage>;
347
348impl Parse for NonNegativePercentage {
349    #[inline]
350    fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
351        Ok(NonNegative(Percentage::parse_non_negative(context, input)?))
352    }
353}
354
355impl NonNegativePercentage {
356    /// Convert to ComputedPercentage, for FontFaceRule size-adjust getter.
357    /// Returns None if the value is a calc expression that cannot be resolved at parse time.
358    #[inline]
359    pub fn compute(&self) -> Option<ComputedPercentage> {
360        self.0
361            .resolve()
362            .map(|f| AllowedNumericType::NonNegative.clamp(f))
363            .map(ComputedPercentage)
364    }
365}