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