style/values/specified/
angle.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 angles.
6
7use crate::derives::*;
8use crate::parser::{Parse, ParserContext};
9use crate::values::computed::angle::Angle as ComputedAngle;
10use crate::values::computed::{Context, ToComputedValue};
11use crate::values::specified::calc::CalcNode;
12use crate::values::CSSFloat;
13use crate::Zero;
14use cssparser::{match_ignore_ascii_case, Parser, Token};
15use std::f32::consts::PI;
16use std::fmt::{self, Write};
17use std::ops::Neg;
18use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, ToCss};
19
20/// A specified angle dimension.
21#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
22#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, PartialOrd, ToCss, ToShmem)]
23pub enum AngleDimension {
24    /// An angle with degree unit.
25    #[css(dimension)]
26    Deg(CSSFloat),
27    /// An angle with gradian unit.
28    #[css(dimension)]
29    Grad(CSSFloat),
30    /// An angle with radian unit.
31    #[css(dimension)]
32    Rad(CSSFloat),
33    /// An angle with turn unit.
34    #[css(dimension)]
35    Turn(CSSFloat),
36}
37
38impl Zero for AngleDimension {
39    fn zero() -> Self {
40        AngleDimension::Deg(0.)
41    }
42
43    fn is_zero(&self) -> bool {
44        self.unitless_value() == 0.0
45    }
46}
47
48impl AngleDimension {
49    /// Returns the amount of degrees this angle represents.
50    #[inline]
51    fn degrees(&self) -> CSSFloat {
52        const DEG_PER_RAD: f32 = 180.0 / PI;
53        const DEG_PER_TURN: f32 = 360.0;
54        const DEG_PER_GRAD: f32 = 180.0 / 200.0;
55
56        match *self {
57            AngleDimension::Deg(d) => d,
58            AngleDimension::Rad(rad) => rad * DEG_PER_RAD,
59            AngleDimension::Turn(turns) => turns * DEG_PER_TURN,
60            AngleDimension::Grad(gradians) => gradians * DEG_PER_GRAD,
61        }
62    }
63
64    fn unitless_value(&self) -> CSSFloat {
65        match *self {
66            AngleDimension::Deg(v)
67            | AngleDimension::Rad(v)
68            | AngleDimension::Turn(v)
69            | AngleDimension::Grad(v) => v,
70        }
71    }
72
73    fn unit(&self) -> &'static str {
74        match *self {
75            AngleDimension::Deg(_) => "deg",
76            AngleDimension::Rad(_) => "rad",
77            AngleDimension::Turn(_) => "turn",
78            AngleDimension::Grad(_) => "grad",
79        }
80    }
81}
82
83/// A specified Angle value, which is just the angle dimension, plus whether it
84/// was specified as `calc()` or not.
85#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
86#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToShmem)]
87pub struct Angle {
88    value: AngleDimension,
89    was_calc: bool,
90}
91
92impl Zero for Angle {
93    fn zero() -> Self {
94        Self {
95            value: Zero::zero(),
96            was_calc: false,
97        }
98    }
99
100    fn is_zero(&self) -> bool {
101        self.value.is_zero()
102    }
103}
104
105impl ToCss for Angle {
106    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
107    where
108        W: Write,
109    {
110        crate::values::serialize_specified_dimension(
111            self.value.unitless_value(),
112            self.value.unit(),
113            self.was_calc,
114            dest,
115        )
116    }
117}
118
119impl ToComputedValue for Angle {
120    type ComputedValue = ComputedAngle;
121
122    #[inline]
123    fn to_computed_value(&self, _context: &Context) -> Self::ComputedValue {
124        let degrees = self.degrees();
125
126        // NaN and +-infinity should degenerate to 0: https://github.com/w3c/csswg-drafts/issues/6105
127        ComputedAngle::from_degrees(if degrees.is_finite() { degrees } else { 0.0 })
128    }
129
130    #[inline]
131    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
132        Angle {
133            value: AngleDimension::Deg(computed.degrees()),
134            was_calc: false,
135        }
136    }
137}
138
139impl Angle {
140    /// Creates an angle with the given value in degrees.
141    #[inline]
142    pub fn from_degrees(value: CSSFloat, was_calc: bool) -> Self {
143        Angle {
144            value: AngleDimension::Deg(value),
145            was_calc,
146        }
147    }
148
149    /// Creates an angle with the given value in radians.
150    #[inline]
151    pub fn from_radians(value: CSSFloat) -> Self {
152        Angle {
153            value: AngleDimension::Rad(value),
154            was_calc: false,
155        }
156    }
157
158    /// Return `0deg`.
159    pub fn zero() -> Self {
160        Self::from_degrees(0.0, false)
161    }
162
163    /// Returns the value of the angle in degrees, mostly for `calc()`.
164    #[inline]
165    pub fn degrees(&self) -> CSSFloat {
166        self.value.degrees()
167    }
168
169    /// Returns the value of the angle in radians.
170    #[inline]
171    pub fn radians(&self) -> CSSFloat {
172        const RAD_PER_DEG: f32 = PI / 180.0;
173        self.value.degrees() * RAD_PER_DEG
174    }
175
176    /// Whether this specified angle came from a `calc()` expression.
177    #[inline]
178    pub fn was_calc(&self) -> bool {
179        self.was_calc
180    }
181
182    /// Returns an `Angle` parsed from a `calc()` expression.
183    pub fn from_calc(degrees: CSSFloat) -> Self {
184        Angle {
185            value: AngleDimension::Deg(degrees),
186            was_calc: true,
187        }
188    }
189
190    /// Returns the unit of the angle.
191    #[inline]
192    pub fn unit(&self) -> &'static str {
193        self.value.unit()
194    }
195}
196
197/// Whether to allow parsing an unitless zero as a valid angle.
198///
199/// This should always be `No`, except for exceptions like:
200///
201///   https://github.com/w3c/fxtf-drafts/issues/228
202///
203/// See also: https://github.com/w3c/csswg-drafts/issues/1162.
204#[allow(missing_docs)]
205pub enum AllowUnitlessZeroAngle {
206    Yes,
207    No,
208}
209
210impl Parse for Angle {
211    /// Parses an angle according to CSS-VALUES ยง 6.1.
212    fn parse<'i, 't>(
213        context: &ParserContext,
214        input: &mut Parser<'i, 't>,
215    ) -> Result<Self, ParseError<'i>> {
216        Self::parse_internal(context, input, AllowUnitlessZeroAngle::No)
217    }
218}
219
220impl Angle {
221    /// Parse an `<angle>` value given a value and an unit.
222    pub fn parse_dimension(value: CSSFloat, unit: &str, was_calc: bool) -> Result<Angle, ()> {
223        let value = match_ignore_ascii_case! { unit,
224            "deg" => AngleDimension::Deg(value),
225            "grad" => AngleDimension::Grad(value),
226            "turn" => AngleDimension::Turn(value),
227            "rad" => AngleDimension::Rad(value),
228             _ => return Err(())
229        };
230
231        Ok(Self { value, was_calc })
232    }
233
234    /// Parse an `<angle>` allowing unitless zero to represent a zero angle.
235    ///
236    /// See the comment in `AllowUnitlessZeroAngle` for why.
237    #[inline]
238    pub fn parse_with_unitless<'i, 't>(
239        context: &ParserContext,
240        input: &mut Parser<'i, 't>,
241    ) -> Result<Self, ParseError<'i>> {
242        Self::parse_internal(context, input, AllowUnitlessZeroAngle::Yes)
243    }
244
245    pub(super) fn parse_internal<'i, 't>(
246        context: &ParserContext,
247        input: &mut Parser<'i, 't>,
248        allow_unitless_zero: AllowUnitlessZeroAngle,
249    ) -> Result<Self, ParseError<'i>> {
250        let location = input.current_source_location();
251        let t = input.next()?;
252        let allow_unitless_zero = matches!(allow_unitless_zero, AllowUnitlessZeroAngle::Yes);
253        match *t {
254            Token::Dimension {
255                value, ref unit, ..
256            } => {
257                match Angle::parse_dimension(value, unit, /* from_calc = */ false) {
258                    Ok(angle) => Ok(angle),
259                    Err(()) => {
260                        let t = t.clone();
261                        Err(input.new_unexpected_token_error(t))
262                    },
263                }
264            },
265            Token::Function(ref name) => {
266                let function = CalcNode::math_function(context, name, location)?;
267                CalcNode::parse_angle(context, input, function)
268            },
269            Token::Number { value, .. } if value == 0. && allow_unitless_zero => Ok(Angle::zero()),
270            ref t => {
271                let t = t.clone();
272                Err(input.new_unexpected_token_error(t))
273            },
274        }
275    }
276}
277
278impl SpecifiedValueInfo for Angle {}
279
280impl Neg for Angle {
281    type Output = Angle;
282
283    #[inline]
284    fn neg(self) -> Angle {
285        let value = match self.value {
286            AngleDimension::Deg(v) => AngleDimension::Deg(-v),
287            AngleDimension::Rad(v) => AngleDimension::Rad(-v),
288            AngleDimension::Turn(v) => AngleDimension::Turn(-v),
289            AngleDimension::Grad(v) => AngleDimension::Grad(-v),
290        };
291        Angle {
292            value,
293            was_calc: self.was_calc,
294        }
295    }
296}