Skip to main content

lightningcss/values/
percentage.rs

1//! CSS percentage values.
2
3use super::angle::{impl_try_from_angle, Angle};
4use super::calc::{Calc, MathFunction};
5use super::number::CSSNumber;
6use crate::error::{ParserError, PrinterError};
7use crate::printer::Printer;
8use crate::traits::private::AddInternal;
9use crate::traits::{impl_op, private::TryAdd, Op, Parse, Sign, ToCss, TryMap, TryOp, TrySign, Zero};
10#[cfg(feature = "visitor")]
11use crate::visitor::Visit;
12use cssparser::*;
13
14/// A CSS [`<percentage>`](https://www.w3.org/TR/css-values-4/#percentages) value.
15///
16/// Percentages may be explicit or computed by `calc()`, but are always stored and serialized
17/// as their computed value.
18#[derive(Debug, Clone, PartialEq)]
19#[cfg_attr(feature = "visitor", derive(Visit))]
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))]
21#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
22#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
23pub struct Percentage(pub CSSNumber);
24
25impl<'i> Parse<'i> for Percentage {
26  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
27    match input.try_parse(Calc::parse) {
28      Ok(Calc::Value(v)) => return Ok(*v),
29      // A percentage calculation must resolve to a percentage, not a number.
30      Ok(_) => return Err(input.new_custom_error(ParserError::InvalidValue)),
31      _ => {}
32    }
33
34    let percent = input.expect_percentage()?;
35    Ok(Percentage(percent))
36  }
37}
38
39impl ToCss for Percentage {
40  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
41  where
42    W: std::fmt::Write,
43  {
44    use cssparser::ToCss;
45    let v = self.0 * 100.0;
46    let int_value = if (v).fract() == 0.0 { Some(v as i32) } else { None };
47    let percent = Token::Percentage {
48      has_sign: self.0 < 0.0,
49      unit_value: self.0,
50      int_value,
51    };
52    if self.0 != 0.0 && self.0.abs() < 0.01 {
53      let mut s = String::new();
54      percent.to_css(&mut s)?;
55      if self.0 < 0.0 {
56        dest.write_char('-')?;
57        dest.write_str(s.trim_start_matches("-0"))
58      } else {
59        dest.write_str(s.trim_start_matches('0'))
60      }
61    } else {
62      percent.to_css(dest)?;
63      Ok(())
64    }
65  }
66}
67
68impl std::convert::Into<Calc<Percentage>> for Percentage {
69  fn into(self) -> Calc<Percentage> {
70    Calc::Value(Box::new(self))
71  }
72}
73
74impl std::convert::TryFrom<Calc<Percentage>> for Percentage {
75  type Error = ();
76
77  fn try_from(calc: Calc<Percentage>) -> Result<Percentage, Self::Error> {
78    match calc {
79      Calc::Value(v) => Ok(*v),
80      _ => Err(()),
81    }
82  }
83}
84
85impl std::ops::Mul<CSSNumber> for Percentage {
86  type Output = Self;
87
88  fn mul(self, other: CSSNumber) -> Percentage {
89    Percentage(self.0 * other)
90  }
91}
92
93impl AddInternal for Percentage {
94  fn add(self, other: Self) -> Self {
95    self + other
96  }
97}
98
99impl std::cmp::PartialOrd<Percentage> for Percentage {
100  fn partial_cmp(&self, other: &Percentage) -> Option<std::cmp::Ordering> {
101    self.0.partial_cmp(&other.0)
102  }
103}
104
105impl Op for Percentage {
106  fn op<F: FnOnce(f32, f32) -> f32>(&self, to: &Self, op: F) -> Self {
107    Percentage(op(self.0, to.0))
108  }
109
110  fn op_to<T, F: FnOnce(f32, f32) -> T>(&self, rhs: &Self, op: F) -> T {
111    op(self.0, rhs.0)
112  }
113}
114
115impl TryMap for Percentage {
116  fn try_map<F: FnOnce(f32) -> f32>(&self, _: F) -> Option<Self> {
117    // Percentages cannot be mapped because we don't know what they will resolve to.
118    // For example, they might be positive or negative depending on what they are a
119    // percentage of, which we don't know.
120    None
121  }
122}
123
124impl Zero for Percentage {
125  fn zero() -> Self {
126    Percentage(0.0)
127  }
128
129  fn is_zero(&self) -> bool {
130    self.0.is_zero()
131  }
132}
133
134impl Sign for Percentage {
135  fn sign(&self) -> f32 {
136    self.0.sign()
137  }
138}
139
140impl_op!(Percentage, std::ops::Rem, rem);
141impl_op!(Percentage, std::ops::Add, add);
142
143impl_try_from_angle!(Percentage);
144
145/// Either a `<number>` or `<percentage>`.
146#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
147#[cfg_attr(feature = "visitor", derive(Visit))]
148#[cfg_attr(
149  feature = "serde",
150  derive(serde::Serialize, serde::Deserialize),
151  serde(tag = "type", content = "value", rename_all = "kebab-case")
152)]
153#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
154#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
155pub enum NumberOrPercentage {
156  /// A number.
157  Number(CSSNumber),
158  /// A percentage.
159  Percentage(Percentage),
160}
161
162impl std::convert::Into<CSSNumber> for &NumberOrPercentage {
163  fn into(self) -> CSSNumber {
164    match self {
165      NumberOrPercentage::Number(a) => *a,
166      NumberOrPercentage::Percentage(a) => a.0,
167    }
168  }
169}
170
171/// A generic type that allows any kind of dimension and percentage to be
172/// used standalone or mixed within a `calc()` expression.
173///
174/// <https://drafts.csswg.org/css-values-4/#mixed-percentages>
175#[derive(Debug, Clone, PartialEq)]
176#[cfg_attr(feature = "visitor", derive(Visit))]
177#[cfg_attr(
178  feature = "serde",
179  derive(serde::Serialize, serde::Deserialize),
180  serde(tag = "type", content = "value", rename_all = "kebab-case")
181)]
182#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
183#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
184pub enum DimensionPercentage<D> {
185  /// An explicit dimension value.
186  Dimension(D),
187  /// A percentage.
188  Percentage(Percentage),
189  /// A `calc()` expression.
190  #[cfg_attr(feature = "visitor", skip_type)]
191  Calc(Box<Calc<DimensionPercentage<D>>>),
192}
193
194impl<
195    'i,
196    D: Parse<'i>
197      + std::ops::Mul<CSSNumber, Output = D>
198      + TryAdd<D>
199      + Clone
200      + TryOp
201      + TryMap
202      + Zero
203      + TrySign
204      + TryFrom<Angle>
205      + TryInto<Angle>
206      + PartialOrd<D>
207      + std::fmt::Debug,
208  > Parse<'i> for DimensionPercentage<D>
209{
210  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
211    match input.try_parse(Calc::parse) {
212      Ok(Calc::Value(v)) => return Ok(*v),
213      Ok(calc) => return Ok(DimensionPercentage::Calc(Box::new(calc))),
214      _ => {}
215    }
216
217    if let Ok(length) = input.try_parse(|input| D::parse(input)) {
218      return Ok(DimensionPercentage::Dimension(length));
219    }
220
221    if let Ok(percent) = input.try_parse(|input| Percentage::parse(input)) {
222      return Ok(DimensionPercentage::Percentage(percent));
223    }
224
225    Err(input.new_error_for_next_token())
226  }
227}
228
229impl<D: std::ops::Mul<CSSNumber, Output = D>> std::ops::Mul<CSSNumber> for DimensionPercentage<D> {
230  type Output = Self;
231
232  fn mul(self, other: CSSNumber) -> DimensionPercentage<D> {
233    match self {
234      DimensionPercentage::Dimension(l) => DimensionPercentage::Dimension(l * other),
235      DimensionPercentage::Percentage(p) => DimensionPercentage::Percentage(Percentage(p.0 * other)),
236      DimensionPercentage::Calc(c) => DimensionPercentage::Calc(Box::new(*c * other)),
237    }
238  }
239}
240
241impl<D: TryAdd<D> + Clone + Zero + TrySign + std::fmt::Debug> std::ops::Add<DimensionPercentage<D>>
242  for DimensionPercentage<D>
243{
244  type Output = DimensionPercentage<D>;
245
246  fn add(self, other: DimensionPercentage<D>) -> DimensionPercentage<D> {
247    // Unwrap calc(...) functions so we can add inside.
248    // Then wrap the result in a calc(...) again if necessary.
249    let a = unwrap_calc(self);
250    let b = unwrap_calc(other);
251    let res = AddInternal::add(a, b);
252    match res {
253      DimensionPercentage::Calc(c) => match *c {
254        Calc::Value(l) => *l,
255        Calc::Function(f) if !matches!(*f, MathFunction::Calc(_)) => {
256          DimensionPercentage::Calc(Box::new(Calc::Function(f)))
257        }
258        c => DimensionPercentage::Calc(Box::new(Calc::Function(Box::new(MathFunction::Calc(c))))),
259      },
260      _ => res,
261    }
262  }
263}
264
265fn unwrap_calc<D>(v: DimensionPercentage<D>) -> DimensionPercentage<D> {
266  match v {
267    DimensionPercentage::Calc(c) => match *c {
268      Calc::Function(f) => match *f {
269        MathFunction::Calc(c) => DimensionPercentage::Calc(Box::new(c)),
270        c => DimensionPercentage::Calc(Box::new(Calc::Function(Box::new(c)))),
271      },
272      _ => DimensionPercentage::Calc(c),
273    },
274    _ => v,
275  }
276}
277
278impl<D: TryAdd<D> + Clone + Zero + TrySign + std::fmt::Debug> AddInternal for DimensionPercentage<D> {
279  fn add(self, other: Self) -> Self {
280    match self.add_recursive(&other) {
281      Some(r) => r,
282      None => self.add(other),
283    }
284  }
285}
286
287impl<D: TryAdd<D> + Clone + Zero + TrySign + std::fmt::Debug> DimensionPercentage<D> {
288  fn add_recursive(&self, other: &DimensionPercentage<D>) -> Option<DimensionPercentage<D>> {
289    match (self, other) {
290      (DimensionPercentage::Dimension(a), DimensionPercentage::Dimension(b)) => {
291        if let Some(res) = a.try_add(b) {
292          Some(DimensionPercentage::Dimension(res))
293        } else {
294          None
295        }
296      }
297      (DimensionPercentage::Percentage(a), DimensionPercentage::Percentage(b)) => {
298        Some(DimensionPercentage::Percentage(Percentage(a.0 + b.0)))
299      }
300      (DimensionPercentage::Calc(a), other) => match &**a {
301        Calc::Value(v) => v.add_recursive(other),
302        Calc::Sum(a, b) => {
303          if let Some(res) = DimensionPercentage::Calc(Box::new(*a.clone())).add_recursive(other) {
304            return Some(res.add(DimensionPercentage::from(*b.clone())));
305          }
306
307          if let Some(res) = DimensionPercentage::Calc(Box::new(*b.clone())).add_recursive(other) {
308            return Some(DimensionPercentage::from(*a.clone()).add(res));
309          }
310
311          None
312        }
313        _ => None,
314      },
315      (other, DimensionPercentage::Calc(b)) => match &**b {
316        Calc::Value(v) => other.add_recursive(&*v),
317        Calc::Sum(a, b) => {
318          if let Some(res) = other.add_recursive(&DimensionPercentage::Calc(Box::new(*a.clone()))) {
319            return Some(res.add(DimensionPercentage::from(*b.clone())));
320          }
321
322          if let Some(res) = other.add_recursive(&DimensionPercentage::Calc(Box::new(*b.clone()))) {
323            return Some(DimensionPercentage::from(*a.clone()).add(res));
324          }
325
326          None
327        }
328        _ => None,
329      },
330      _ => None,
331    }
332  }
333
334  fn add(self, other: DimensionPercentage<D>) -> DimensionPercentage<D> {
335    let mut a = self;
336    let mut b = other;
337
338    if a.is_zero() {
339      return b;
340    }
341
342    if b.is_zero() {
343      return a;
344    }
345
346    if a.is_sign_negative() && b.is_sign_positive() {
347      std::mem::swap(&mut a, &mut b);
348    }
349
350    match (a, b) {
351      (DimensionPercentage::Calc(a), DimensionPercentage::Calc(b)) => {
352        DimensionPercentage::Calc(Box::new(a.add(*b).unwrap()))
353      }
354      (DimensionPercentage::Calc(calc), b) => {
355        if let Calc::Value(a) = *calc {
356          a.add(b)
357        } else {
358          DimensionPercentage::Calc(Box::new(Calc::Sum(Box::new((*calc).into()), Box::new(b.into()))))
359        }
360      }
361      (a, DimensionPercentage::Calc(calc)) => {
362        if let Calc::Value(b) = *calc {
363          a.add(*b)
364        } else {
365          DimensionPercentage::Calc(Box::new(Calc::Sum(Box::new(a.into()), Box::new((*calc).into()))))
366        }
367      }
368      (a, b) => DimensionPercentage::Calc(Box::new(Calc::Sum(Box::new(a.into()), Box::new(b.into())))),
369    }
370  }
371}
372
373impl<D> std::convert::Into<Calc<DimensionPercentage<D>>> for DimensionPercentage<D> {
374  fn into(self) -> Calc<DimensionPercentage<D>> {
375    match self {
376      DimensionPercentage::Calc(c) => *c,
377      b => Calc::Value(Box::new(b)),
378    }
379  }
380}
381
382impl<D> std::convert::From<Calc<DimensionPercentage<D>>> for DimensionPercentage<D> {
383  fn from(calc: Calc<DimensionPercentage<D>>) -> DimensionPercentage<D> {
384    DimensionPercentage::Calc(Box::new(calc))
385  }
386}
387
388impl<D: std::cmp::PartialOrd<D>> std::cmp::PartialOrd<DimensionPercentage<D>> for DimensionPercentage<D> {
389  fn partial_cmp(&self, other: &DimensionPercentage<D>) -> Option<std::cmp::Ordering> {
390    match (self, other) {
391      (DimensionPercentage::Dimension(a), DimensionPercentage::Dimension(b)) => a.partial_cmp(b),
392      (DimensionPercentage::Percentage(a), DimensionPercentage::Percentage(b)) => a.partial_cmp(b),
393      _ => None,
394    }
395  }
396}
397
398impl<D: TryOp> TryOp for DimensionPercentage<D> {
399  fn try_op<F: FnOnce(f32, f32) -> f32>(&self, rhs: &Self, op: F) -> Option<Self> {
400    match (self, rhs) {
401      (DimensionPercentage::Dimension(a), DimensionPercentage::Dimension(b)) => {
402        a.try_op(b, op).map(DimensionPercentage::Dimension)
403      }
404      (DimensionPercentage::Percentage(a), DimensionPercentage::Percentage(b)) => {
405        Some(DimensionPercentage::Percentage(Percentage(op(a.0, b.0))))
406      }
407      _ => None,
408    }
409  }
410
411  fn try_op_to<T, F: FnOnce(f32, f32) -> T>(&self, rhs: &Self, op: F) -> Option<T> {
412    match (self, rhs) {
413      (DimensionPercentage::Dimension(a), DimensionPercentage::Dimension(b)) => a.try_op_to(b, op),
414      (DimensionPercentage::Percentage(a), DimensionPercentage::Percentage(b)) => Some(op(a.0, b.0)),
415      _ => None,
416    }
417  }
418}
419
420impl<D: TryMap> TryMap for DimensionPercentage<D> {
421  fn try_map<F: FnOnce(f32) -> f32>(&self, op: F) -> Option<Self> {
422    match self {
423      DimensionPercentage::Dimension(v) => v.try_map(op).map(DimensionPercentage::Dimension),
424      _ => None,
425    }
426  }
427}
428
429impl<E, D: TryFrom<Angle, Error = E>> TryFrom<Angle> for DimensionPercentage<D> {
430  type Error = E;
431
432  fn try_from(value: Angle) -> Result<Self, Self::Error> {
433    Ok(DimensionPercentage::Dimension(D::try_from(value)?))
434  }
435}
436
437impl<E, D: TryInto<Angle, Error = E>> TryInto<Angle> for DimensionPercentage<D> {
438  type Error = ();
439
440  fn try_into(self) -> Result<Angle, Self::Error> {
441    match self {
442      DimensionPercentage::Dimension(d) => d.try_into().map_err(|_| ()),
443      _ => Err(()),
444    }
445  }
446}
447
448impl<D: Zero> Zero for DimensionPercentage<D> {
449  fn zero() -> Self {
450    DimensionPercentage::Dimension(D::zero())
451  }
452
453  fn is_zero(&self) -> bool {
454    match self {
455      DimensionPercentage::Dimension(d) => d.is_zero(),
456      DimensionPercentage::Percentage(p) => p.is_zero(),
457      _ => false,
458    }
459  }
460}
461
462impl<D: TrySign> TrySign for DimensionPercentage<D> {
463  fn try_sign(&self) -> Option<f32> {
464    match self {
465      DimensionPercentage::Dimension(d) => d.try_sign(),
466      DimensionPercentage::Percentage(p) => p.try_sign(),
467      DimensionPercentage::Calc(c) => c.try_sign(),
468    }
469  }
470}
471
472impl<D: ToCss + std::ops::Mul<CSSNumber, Output = D> + TrySign + Clone + std::fmt::Debug> ToCss
473  for DimensionPercentage<D>
474{
475  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
476  where
477    W: std::fmt::Write,
478  {
479    match self {
480      DimensionPercentage::Dimension(length) => length.to_css(dest),
481      DimensionPercentage::Percentage(percent) => percent.to_css(dest),
482      DimensionPercentage::Calc(calc) => calc.to_css(dest),
483    }
484  }
485}