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