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