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      // Percentages are always compatible, so they will always compute to a value.
30      Ok(_) => unreachable!(),
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 int_value = if (self.0 * 100.0).fract() == 0.0 {
46      Some(self.0 as i32)
47    } else {
48      None
49    };
50    let percent = Token::Percentage {
51      has_sign: self.0 < 0.0,
52      unit_value: self.0,
53      int_value,
54    };
55    if self.0 != 0.0 && self.0.abs() < 0.01 {
56      let mut s = String::new();
57      percent.to_css(&mut s)?;
58      if self.0 < 0.0 {
59        dest.write_char('-')?;
60        dest.write_str(s.trim_start_matches("-0"))
61      } else {
62        dest.write_str(s.trim_start_matches('0'))
63      }
64    } else {
65      percent.to_css(dest)?;
66      Ok(())
67    }
68  }
69}
70
71impl std::convert::Into<Calc<Percentage>> for Percentage {
72  fn into(self) -> Calc<Percentage> {
73    Calc::Value(Box::new(self))
74  }
75}
76
77impl std::convert::From<Calc<Percentage>> for Percentage {
78  fn from(calc: Calc<Percentage>) -> Percentage {
79    match calc {
80      Calc::Value(v) => *v,
81      _ => unreachable!(),
82    }
83  }
84}
85
86impl std::ops::Mul<CSSNumber> for Percentage {
87  type Output = Self;
88
89  fn mul(self, other: CSSNumber) -> Percentage {
90    Percentage(self.0 * other)
91  }
92}
93
94impl AddInternal for Percentage {
95  fn add(self, other: Self) -> Self {
96    self + other
97  }
98}
99
100impl std::cmp::PartialOrd<Percentage> for Percentage {
101  fn partial_cmp(&self, other: &Percentage) -> Option<std::cmp::Ordering> {
102    self.0.partial_cmp(&other.0)
103  }
104}
105
106impl Op for Percentage {
107  fn op<F: FnOnce(f32, f32) -> f32>(&self, to: &Self, op: F) -> Self {
108    Percentage(op(self.0, to.0))
109  }
110
111  fn op_to<T, F: FnOnce(f32, f32) -> T>(&self, rhs: &Self, op: F) -> T {
112    op(self.0, rhs.0)
113  }
114}
115
116impl TryMap for Percentage {
117  fn try_map<F: FnOnce(f32) -> f32>(&self, _: F) -> Option<Self> {
118    // Percentages cannot be mapped because we don't know what they will resolve to.
119    // For example, they might be positive or negative depending on what they are a
120    // percentage of, which we don't know.
121    None
122  }
123}
124
125impl Zero for Percentage {
126  fn zero() -> Self {
127    Percentage(0.0)
128  }
129
130  fn is_zero(&self) -> bool {
131    self.0.is_zero()
132  }
133}
134
135impl Sign for Percentage {
136  fn sign(&self) -> f32 {
137    self.0.sign()
138  }
139}
140
141impl_op!(Percentage, std::ops::Rem, rem);
142impl_op!(Percentage, std::ops::Add, add);
143
144impl_try_from_angle!(Percentage);
145
146/// Either a `<number>` or `<percentage>`.
147#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
148#[cfg_attr(feature = "visitor", derive(Visit))]
149#[cfg_attr(
150  feature = "serde",
151  derive(serde::Serialize, serde::Deserialize),
152  serde(tag = "type", content = "value", rename_all = "kebab-case")
153)]
154#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
155#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
156pub enum NumberOrPercentage {
157  /// A number.
158  Number(CSSNumber),
159  /// A percentage.
160  Percentage(Percentage),
161}
162
163impl std::convert::Into<CSSNumber> for &NumberOrPercentage {
164  fn into(self) -> CSSNumber {
165    match self {
166      NumberOrPercentage::Number(a) => *a,
167      NumberOrPercentage::Percentage(a) => a.0,
168    }
169  }
170}
171
172/// A generic type that allows any kind of dimension and percentage to be
173/// used standalone or mixed within a `calc()` expression.
174///
175/// <https://drafts.csswg.org/css-values-4/#mixed-percentages>
176#[derive(Debug, Clone, PartialEq)]
177#[cfg_attr(feature = "visitor", derive(Visit))]
178#[cfg_attr(
179  feature = "serde",
180  derive(serde::Serialize, serde::Deserialize),
181  serde(tag = "type", content = "value", rename_all = "kebab-case")
182)]
183#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
184#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
185pub enum DimensionPercentage<D> {
186  /// An explicit dimension value.
187  Dimension(D),
188  /// A percentage.
189  Percentage(Percentage),
190  /// A `calc()` expression.
191  #[cfg_attr(feature = "visitor", skip_type)]
192  Calc(Box<Calc<DimensionPercentage<D>>>),
193}
194
195impl<
196    'i,
197    D: Parse<'i>
198      + std::ops::Mul<CSSNumber, Output = D>
199      + TryAdd<D>
200      + Clone
201      + TryOp
202      + TryMap
203      + Zero
204      + TrySign
205      + TryFrom<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)))
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<D: Zero> Zero for DimensionPercentage<D> {
438  fn zero() -> Self {
439    DimensionPercentage::Dimension(D::zero())
440  }
441
442  fn is_zero(&self) -> bool {
443    match self {
444      DimensionPercentage::Dimension(d) => d.is_zero(),
445      DimensionPercentage::Percentage(p) => p.is_zero(),
446      _ => false,
447    }
448  }
449}
450
451impl<D: TrySign> TrySign for DimensionPercentage<D> {
452  fn try_sign(&self) -> Option<f32> {
453    match self {
454      DimensionPercentage::Dimension(d) => d.try_sign(),
455      DimensionPercentage::Percentage(p) => p.try_sign(),
456      DimensionPercentage::Calc(c) => c.try_sign(),
457    }
458  }
459}
460
461impl<D: ToCss + std::ops::Mul<CSSNumber, Output = D> + TrySign + Clone + std::fmt::Debug> ToCss
462  for DimensionPercentage<D>
463{
464  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
465  where
466    W: std::fmt::Write,
467  {
468    match self {
469      DimensionPercentage::Dimension(length) => length.to_css(dest),
470      DimensionPercentage::Percentage(percent) => percent.to_css(dest),
471      DimensionPercentage::Calc(calc) => calc.to_css(dest),
472    }
473  }
474}