Skip to main content

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::typed_om::{NumericType, NumericValue, ToTyped, TypedValue, UnitValue};
10use crate::values::computed::angle::Angle as ComputedAngle;
11use crate::values::computed::{Context, ToComputedValue};
12use crate::values::specified::calc::{CalcNode, CalcNumeric, Leaf};
13use crate::values::tagged_numeric::{Extracted, NumericUnion, Unpacked};
14use crate::values::CSSFloat;
15use crate::Zero;
16use cssparser::{match_ignore_ascii_case, Parser, Token};
17use std::f32::consts::PI;
18use std::fmt::{self, Write};
19use std::ops::Neg;
20use style_traits::{CssString, CssWriter, ParseError, SpecifiedValueInfo, ToCss};
21use thin_vec::ThinVec;
22
23/// Number of degrees per radian.
24const DEG_PER_RAD: f32 = 180.0 / PI;
25/// Number of degrees per turn.
26const DEG_PER_TURN: f32 = 360.0;
27/// Number of degrees per gradian.
28const DEG_PER_GRAD: f32 = 180.0 / 200.0;
29
30/// The unit of a `<angle>` value.
31#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, PartialOrd, ToShmem)]
32#[repr(u8)]
33pub enum AngleUnit {
34    /// `deg`
35    Deg,
36    /// `grad`
37    Grad,
38    /// `rad`
39    Rad,
40    /// `turn`
41    Turn,
42}
43
44impl AngleUnit {
45    /// Returns the angle unit for the given string.
46    #[inline]
47    pub fn from_str(unit: &str) -> Result<Self, ()> {
48        Ok(match_ignore_ascii_case! { unit,
49            "deg" => Self::Deg,
50            "grad" => Self::Grad,
51            "turn" => Self::Turn,
52            "rad" => Self::Rad,
53             _ => return Err(())
54        })
55    }
56
57    /// Returns this unit as a string.
58    #[inline]
59    pub fn as_str(self) -> &'static str {
60        match self {
61            Self::Deg => "deg",
62            Self::Grad => "grad",
63            Self::Rad => "rad",
64            Self::Turn => "turn",
65        }
66    }
67}
68
69/// A non-calc `<angle>` value.
70#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, PartialOrd, ToShmem)]
71#[repr(C)]
72pub struct NoCalcAngle {
73    unit: AngleUnit,
74    value: CSSFloat,
75}
76
77impl Zero for NoCalcAngle {
78    fn zero() -> Self {
79        Self::from_degrees(0.)
80    }
81
82    fn is_zero(&self) -> bool {
83        self.value == 0.0
84    }
85}
86
87impl ToCss for NoCalcAngle {
88    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
89    where
90        W: Write,
91    {
92        crate::values::serialize_specified_dimension(
93            self.value,
94            self.unit.as_str(),
95            /* was_calc = */ false,
96            dest,
97        )
98    }
99}
100
101impl ToTyped for NoCalcAngle {
102    fn to_typed(&self, dest: &mut ThinVec<TypedValue>) -> Result<(), ()> {
103        let numeric_type = NumericType::angle();
104        let value = self.unitless_value();
105        let unit = CssString::from(self.unit());
106        dest.push(TypedValue::Numeric(NumericValue::Unit(UnitValue {
107            numeric_type,
108            value,
109            unit,
110        })));
111        Ok(())
112    }
113}
114
115impl SpecifiedValueInfo for NoCalcAngle {}
116
117impl NoCalcAngle {
118    /// Creates an angle with the given unit and value.
119    #[inline]
120    pub fn new(unit: AngleUnit, value: CSSFloat) -> Self {
121        Self { unit, value }
122    }
123
124    /// Creates an angle with the given value in degrees.
125    #[inline]
126    pub fn from_degrees(value: CSSFloat) -> Self {
127        Self::new(AngleUnit::Deg, value)
128    }
129
130    /// Creates an angle with the given value in radians.
131    #[inline]
132    pub fn from_radians(value: CSSFloat) -> Self {
133        Self::new(AngleUnit::Rad, value)
134    }
135
136    /// Return `0deg`.
137    pub fn zero() -> Self {
138        Self::from_degrees(0.0)
139    }
140
141    /// Returns the value of the angle in degrees.
142    #[inline]
143    pub fn degrees(&self) -> CSSFloat {
144        match self.unit {
145            AngleUnit::Deg => self.value,
146            AngleUnit::Rad => self.value * DEG_PER_RAD,
147            AngleUnit::Turn => self.value * DEG_PER_TURN,
148            AngleUnit::Grad => self.value * DEG_PER_GRAD,
149        }
150    }
151
152    /// Returns the value of the angle in radians.
153    #[inline]
154    pub fn radians(&self) -> CSSFloat {
155        const RAD_PER_DEG: f32 = PI / 180.0;
156        self.degrees() * RAD_PER_DEG
157    }
158
159    /// Returns the unit of the angle.
160    #[inline]
161    pub fn angle_unit(&self) -> AngleUnit {
162        self.unit
163    }
164
165    /// Returns the unitless, raw value.
166    #[inline]
167    pub fn unitless_value(&self) -> CSSFloat {
168        self.value
169    }
170
171    /// Returns the unit of the angle as a string.
172    #[inline]
173    pub fn unit(&self) -> &'static str {
174        self.unit.as_str()
175    }
176
177    /// Return the canonical unit for this value.
178    pub fn canonical_unit(&self) -> Option<&'static str> {
179        Some("deg")
180    }
181
182    /// Convert this value to the specified unit, if possible.
183    pub fn to(&self, unit: &str) -> Result<Self, ()> {
184        let degrees = self.degrees();
185        let unit = AngleUnit::from_str(unit)?;
186        let divisor = match unit {
187            AngleUnit::Deg => 1.0,
188            AngleUnit::Grad => DEG_PER_GRAD,
189            AngleUnit::Turn => DEG_PER_TURN,
190            AngleUnit::Rad => DEG_PER_RAD,
191        };
192        Ok(Self::new(unit, degrees / divisor))
193    }
194
195    /// Parse an `<angle>` value given a value and a unit.
196    pub fn parse_dimension(value: CSSFloat, unit: &str) -> Result<Self, ()> {
197        let unit = AngleUnit::from_str(unit)?;
198        Ok(Self::new(unit, value))
199    }
200}
201
202impl Neg for NoCalcAngle {
203    type Output = NoCalcAngle;
204
205    #[inline]
206    fn neg(self) -> NoCalcAngle {
207        Self::new(self.unit, -self.value)
208    }
209}
210
211/// A specified `<angle>` value, either a plain value or a `calc()` expression.
212///
213/// https://drafts.csswg.org/css-values/#angle-value
214#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
215pub struct Angle(NumericUnion<AngleUnit, f32, CalcNumeric>);
216
217impl ToCss for Angle {
218    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
219    where
220        W: Write,
221    {
222        match self.0.unpack() {
223            Unpacked::Inline(unit, value) => NoCalcAngle::new(unit, value).to_css(dest),
224            Unpacked::Boxed(calc) => calc.to_css(dest),
225        }
226    }
227}
228
229impl ToTyped for Angle {
230    fn to_typed(&self, dest: &mut ThinVec<TypedValue>) -> Result<(), ()> {
231        match self.0.unpack() {
232            Unpacked::Inline(unit, value) => NoCalcAngle::new(unit, value).to_typed(dest),
233            Unpacked::Boxed(calc) => calc.to_typed(dest),
234        }
235    }
236}
237
238impl SpecifiedValueInfo for Angle {}
239
240/// Whether to allow parsing an unitless zero as a valid angle.
241///
242/// This should always be `No`, except for exceptions like:
243///
244///   https://github.com/w3c/fxtf-drafts/issues/228
245///
246/// See also: https://github.com/w3c/csswg-drafts/issues/1162.
247#[allow(missing_docs)]
248pub enum AllowUnitlessZeroAngle {
249    Yes,
250    No,
251}
252
253impl Parse for Angle {
254    /// Parses an angle according to CSS-VALUES ยง 6.1.
255    fn parse<'i, 't>(
256        context: &ParserContext,
257        input: &mut Parser<'i, 't>,
258    ) -> Result<Self, ParseError<'i>> {
259        Self::parse_internal(context, input, AllowUnitlessZeroAngle::No)
260    }
261}
262
263impl Zero for Angle {
264    fn zero() -> Self {
265        Self::new(NoCalcAngle::zero())
266    }
267
268    fn is_zero(&self) -> bool {
269        match self.0.unpack() {
270            Unpacked::Inline(_, v) => v == 0.0,
271            Unpacked::Boxed(_) => false,
272        }
273    }
274}
275
276impl ToComputedValue for Angle {
277    type ComputedValue = ComputedAngle;
278
279    #[inline]
280    fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
281        let degrees = match self.0.unpack() {
282            Unpacked::Inline(unit, value) => NoCalcAngle::new(unit, value).degrees(),
283            Unpacked::Boxed(ref calc) => calc.resolve(context, |result| match result {
284                Ok(Leaf::Angle(a)) => a.degrees(),
285                _ => {
286                    debug_assert!(false, "Unexpected Angle::Calc without resolved angle");
287                    f32::NAN
288                },
289            }),
290        };
291
292        // NaN and +-infinity should degenerate to 0: https://github.com/w3c/csswg-drafts/issues/6105
293        ComputedAngle::from_degrees(if degrees.is_finite() { degrees } else { 0.0 })
294    }
295
296    #[inline]
297    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
298        Self::new(NoCalcAngle::from_degrees(computed.degrees()))
299    }
300}
301
302impl Angle {
303    /// Creates an angle from a non-calc `NoCalcAngle`.
304    #[inline]
305    pub fn new(angle: NoCalcAngle) -> Self {
306        Self(NumericUnion::inline(angle.unit, angle.value))
307    }
308
309    /// Creates an angle from a calc() expression.
310    #[inline]
311    pub fn new_calc(calc: Box<CalcNumeric>) -> Self {
312        Self(NumericUnion::boxed(calc))
313    }
314
315    /// Creates an angle with the given value in degrees.
316    #[inline]
317    pub fn from_degrees(value: CSSFloat) -> Self {
318        Self::new(NoCalcAngle::from_degrees(value))
319    }
320
321    /// Return `0deg`.
322    pub fn zero() -> Self {
323        Self::new(NoCalcAngle::zero())
324    }
325
326    /// Returns true if this is a `calc()` expression.
327    #[inline]
328    pub fn is_calc(&self) -> bool {
329        self.0.is_boxed()
330    }
331
332    /// Returns the inner non-calc angle, if this isn't a calc expression.
333    #[inline]
334    pub fn as_no_calc(&self) -> Option<NoCalcAngle> {
335        match self.0.unpack() {
336            Unpacked::Inline(unit, value) => Some(NoCalcAngle::new(unit, value)),
337            Unpacked::Boxed(_) => None,
338        }
339    }
340
341    /// Returns the angle in degrees if it can be resolved at parse time, or None for calc
342    /// expressions that require computed context. Prefer `to_computed_value(context).degrees()`
343    /// when an element context is available.
344    #[inline]
345    pub fn degrees(&self) -> Option<CSSFloat> {
346        match self.0.unpack() {
347            Unpacked::Inline(unit, value) => Some(NoCalcAngle::new(unit, value).degrees()),
348            Unpacked::Boxed(ref calc) => calc
349                .as_angle()
350                .map(|a| calc.clamping_mode.clamp(a.degrees())),
351        }
352    }
353
354    /// Parse an `<angle>` allowing unitless zero to represent a zero angle.
355    ///
356    /// See the comment in `AllowUnitlessZeroAngle` for why.
357    #[inline]
358    pub fn parse_with_unitless<'i, 't>(
359        context: &ParserContext,
360        input: &mut Parser<'i, 't>,
361    ) -> Result<Self, ParseError<'i>> {
362        Self::parse_internal(context, input, AllowUnitlessZeroAngle::Yes)
363    }
364
365    pub(super) fn parse_internal<'i, 't>(
366        context: &ParserContext,
367        input: &mut Parser<'i, 't>,
368        allow_unitless_zero: AllowUnitlessZeroAngle,
369    ) -> Result<Self, ParseError<'i>> {
370        let location = input.current_source_location();
371        let t = input.next()?;
372        let allow_unitless_zero = matches!(allow_unitless_zero, AllowUnitlessZeroAngle::Yes);
373        match *t {
374            Token::Dimension {
375                value, ref unit, ..
376            } => match NoCalcAngle::parse_dimension(value, unit) {
377                Ok(angle) => Ok(Self::new(angle)),
378                Err(()) => {
379                    let t = t.clone();
380                    Err(input.new_unexpected_token_error(t))
381                },
382            },
383            Token::Function(ref name) => {
384                let function = CalcNode::math_function(context, name, location)?;
385                CalcNode::parse_angle(context, input, function)
386                    .map(Box::new)
387                    .map(Self::new_calc)
388            },
389            Token::Number { value, .. } if value == 0. && allow_unitless_zero => Ok(Angle::zero()),
390            ref t => {
391                let t = t.clone();
392                Err(input.new_unexpected_token_error(t))
393            },
394        }
395    }
396}
397
398impl Neg for Angle {
399    type Output = Angle;
400
401    #[inline]
402    fn neg(self) -> Angle {
403        match self.0.extract() {
404            Extracted::Inline(unit, value) => Self::new(NoCalcAngle::new(unit, -value)),
405            Extracted::Boxed(mut c) => {
406                c.node.negate();
407                Self::new_calc(c)
408            },
409        }
410    }
411}