Skip to main content

whisker_css/shorthand/
transform.rs

1//! `transform` — a sequence of transform functions.
2
3use core::fmt;
4
5use crate::css::Css;
6use crate::data_type::{Angle, Length, LengthPercentage};
7use crate::to_css::{write_number, ToCss};
8
9/// One CSS transform function. Lynx supports the 2-D and 3-D
10/// transform families except `rotate3d()` and `scale3d()`.
11#[derive(Clone, Debug, PartialEq)]
12pub enum TransformFn {
13    /// `translate(<x>, <y>)`.
14    Translate(LengthPercentage, LengthPercentage),
15    /// `translateX(<x>)`.
16    TranslateX(LengthPercentage),
17    /// `translateY(<y>)`.
18    TranslateY(LengthPercentage),
19    /// `translateZ(<z>)`.
20    TranslateZ(Length),
21    /// `translate3d(<x>, <y>, <z>)`.
22    Translate3d(LengthPercentage, LengthPercentage, Length),
23    /// `rotate(<angle>)` — alias of `rotateZ`.
24    Rotate(Angle),
25    /// `rotateX(<angle>)`.
26    RotateX(Angle),
27    /// `rotateY(<angle>)`.
28    RotateY(Angle),
29    /// `rotateZ(<angle>)`.
30    RotateZ(Angle),
31    /// `scale(<x>, <y>)`.
32    Scale(f32, f32),
33    /// `scaleX(<x>)`.
34    ScaleX(f32),
35    /// `scaleY(<y>)`.
36    ScaleY(f32),
37    /// `skew(<x-angle>, <y-angle>)`.
38    Skew(Angle, Angle),
39    /// `skewX(<angle>)`.
40    SkewX(Angle),
41    /// `skewY(<angle>)`.
42    SkewY(Angle),
43    /// `matrix(a, b, c, d, tx, ty)`.
44    Matrix([f32; 6]),
45    /// `matrix3d(...)` — 16-element column-major matrix.
46    Matrix3d([f32; 16]),
47}
48
49impl ToCss for TransformFn {
50    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
51        match self {
52            TransformFn::Translate(x, y) => {
53                dest.write_str("translate(")?;
54                x.to_css(dest)?;
55                dest.write_str(", ")?;
56                y.to_css(dest)?;
57                dest.write_char(')')
58            }
59            TransformFn::TranslateX(x) => {
60                dest.write_str("translateX(")?;
61                x.to_css(dest)?;
62                dest.write_char(')')
63            }
64            TransformFn::TranslateY(y) => {
65                dest.write_str("translateY(")?;
66                y.to_css(dest)?;
67                dest.write_char(')')
68            }
69            TransformFn::TranslateZ(z) => {
70                dest.write_str("translateZ(")?;
71                z.to_css(dest)?;
72                dest.write_char(')')
73            }
74            TransformFn::Translate3d(x, y, z) => {
75                dest.write_str("translate3d(")?;
76                x.to_css(dest)?;
77                dest.write_str(", ")?;
78                y.to_css(dest)?;
79                dest.write_str(", ")?;
80                z.to_css(dest)?;
81                dest.write_char(')')
82            }
83            TransformFn::Rotate(a) => fn_one(dest, "rotate", a),
84            TransformFn::RotateX(a) => fn_one(dest, "rotateX", a),
85            TransformFn::RotateY(a) => fn_one(dest, "rotateY", a),
86            TransformFn::RotateZ(a) => fn_one(dest, "rotateZ", a),
87            TransformFn::Scale(x, y) => {
88                dest.write_str("scale(")?;
89                write_number(dest, *x)?;
90                dest.write_str(", ")?;
91                write_number(dest, *y)?;
92                dest.write_char(')')
93            }
94            TransformFn::ScaleX(x) => {
95                dest.write_str("scaleX(")?;
96                write_number(dest, *x)?;
97                dest.write_char(')')
98            }
99            TransformFn::ScaleY(y) => {
100                dest.write_str("scaleY(")?;
101                write_number(dest, *y)?;
102                dest.write_char(')')
103            }
104            TransformFn::Skew(x, y) => {
105                dest.write_str("skew(")?;
106                x.to_css(dest)?;
107                dest.write_str(", ")?;
108                y.to_css(dest)?;
109                dest.write_char(')')
110            }
111            TransformFn::SkewX(a) => fn_one(dest, "skewX", a),
112            TransformFn::SkewY(a) => fn_one(dest, "skewY", a),
113            TransformFn::Matrix(m) => {
114                dest.write_str("matrix(")?;
115                for (i, v) in m.iter().enumerate() {
116                    if i > 0 {
117                        dest.write_str(", ")?;
118                    }
119                    write_number(dest, *v)?;
120                }
121                dest.write_char(')')
122            }
123            TransformFn::Matrix3d(m) => {
124                dest.write_str("matrix3d(")?;
125                for (i, v) in m.iter().enumerate() {
126                    if i > 0 {
127                        dest.write_str(", ")?;
128                    }
129                    write_number(dest, *v)?;
130                }
131                dest.write_char(')')
132            }
133        }
134    }
135}
136
137fn fn_one(dest: &mut dyn fmt::Write, name: &str, a: &Angle) -> fmt::Result {
138    dest.write_str(name)?;
139    dest.write_char('(')?;
140    a.to_css(dest)?;
141    dest.write_char(')')
142}
143
144/// A sequence of [`TransformFn`]s — the value of the `transform`
145/// property.
146#[derive(Clone, Debug, Default, PartialEq)]
147pub struct Transform(pub Vec<TransformFn>);
148
149impl Transform {
150    /// An empty transform list (renders as nothing).
151    pub fn new() -> Self {
152        Self::default()
153    }
154
155    /// Append a function.
156    pub fn push(mut self, f: TransformFn) -> Self {
157        self.0.push(f);
158        self
159    }
160}
161
162impl ToCss for Transform {
163    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
164        for (i, f) in self.0.iter().enumerate() {
165            if i > 0 {
166                dest.write_char(' ')?;
167            }
168            f.to_css(dest)?;
169        }
170        Ok(())
171    }
172}
173
174impl From<TransformFn> for Transform {
175    fn from(f: TransformFn) -> Self {
176        Self(vec![f])
177    }
178}
179
180impl<const N: usize> From<[TransformFn; N]> for Transform {
181    fn from(arr: [TransformFn; N]) -> Self {
182        Self(arr.into_iter().collect())
183    }
184}
185
186impl From<Vec<TransformFn>> for Transform {
187    fn from(v: Vec<TransformFn>) -> Self {
188        Self(v)
189    }
190}
191
192impl Css {
193    /// Sets `transform` to a list of [`TransformFn`]s. Functions are
194    /// applied left-to-right.
195    /// <https://lynxjs.org/api/css/properties/transform>
196    pub fn transform(self, t: impl Into<Transform>) -> Self {
197        self.push("transform", t.into())
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use crate::ext::*;
204    use crate::Css;
205
206    use super::*;
207
208    #[test]
209    fn single_transform_function() {
210        let s = Css::new().transform(TransformFn::TranslateX(px(10).into()));
211        assert_eq!(s.to_string(), "transform: translateX(10px);");
212    }
213
214    #[test]
215    fn chain_of_transforms() {
216        let s = Css::new().transform([
217            TransformFn::TranslateX(px(10).into()),
218            TransformFn::Rotate(45.deg()),
219            TransformFn::Scale(1.5, 2.0),
220        ]);
221        assert_eq!(
222            s.to_string(),
223            "transform: translateX(10px) rotate(45deg) scale(1.5, 2);"
224        );
225    }
226
227    #[test]
228    fn translate_variants() {
229        assert_eq!(
230            Css::new()
231                .transform(TransformFn::Translate(px(1).into(), px(2).into()))
232                .to_string(),
233            "transform: translate(1px, 2px);"
234        );
235        assert_eq!(
236            Css::new()
237                .transform(TransformFn::TranslateY(px(3).into()))
238                .to_string(),
239            "transform: translateY(3px);"
240        );
241        assert_eq!(
242            Css::new()
243                .transform(TransformFn::TranslateZ(px(4)))
244                .to_string(),
245            "transform: translateZ(4px);"
246        );
247        assert_eq!(
248            Css::new()
249                .transform(TransformFn::Translate3d(px(1).into(), px(2).into(), px(3),))
250                .to_string(),
251            "transform: translate3d(1px, 2px, 3px);"
252        );
253    }
254
255    #[test]
256    fn rotate_axes() {
257        let s = Css::new().transform([
258            TransformFn::RotateX(10.deg()),
259            TransformFn::RotateY(20.deg()),
260            TransformFn::RotateZ(30.deg()),
261        ]);
262        assert_eq!(
263            s.to_string(),
264            "transform: rotateX(10deg) rotateY(20deg) rotateZ(30deg);"
265        );
266    }
267
268    #[test]
269    fn scale_axes() {
270        let s = Css::new().transform([TransformFn::ScaleX(2.0), TransformFn::ScaleY(0.5)]);
271        assert_eq!(s.to_string(), "transform: scaleX(2) scaleY(0.5);");
272    }
273
274    #[test]
275    fn skew_variants() {
276        let s = Css::new().transform([
277            TransformFn::Skew(10.deg(), 20.deg()),
278            TransformFn::SkewX(5.deg()),
279            TransformFn::SkewY(15.deg()),
280        ]);
281        assert_eq!(
282            s.to_string(),
283            "transform: skew(10deg, 20deg) skewX(5deg) skewY(15deg);"
284        );
285    }
286
287    #[test]
288    fn matrix_six_values() {
289        let s = Css::new().transform(TransformFn::Matrix([1.0, 0.0, 0.0, 1.0, 10.0, 20.0]));
290        assert_eq!(s.to_string(), "transform: matrix(1, 0, 0, 1, 10, 20);");
291    }
292
293    #[test]
294    fn matrix3d_sixteen_values() {
295        let mut m = [0.0_f32; 16];
296        m[0] = 1.0;
297        m[5] = 1.0;
298        m[10] = 1.0;
299        m[15] = 1.0;
300        let s = Css::new().transform(TransformFn::Matrix3d(m));
301        assert_eq!(
302            s.to_string(),
303            "transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);"
304        );
305    }
306
307    #[test]
308    fn transform_from_vec_and_array() {
309        let from_vec = Css::new().transform(vec![TransformFn::Rotate(10.deg())]);
310        let from_arr = Css::new().transform([TransformFn::Rotate(10.deg())]);
311        assert_eq!(from_vec.to_string(), from_arr.to_string());
312    }
313
314    #[test]
315    fn transform_builder_pattern() {
316        let t = Transform::new()
317            .push(TransformFn::TranslateX(px(10).into()))
318            .push(TransformFn::Rotate(45.deg()));
319        let s = Css::new().transform(t);
320        assert_eq!(s.to_string(), "transform: translateX(10px) rotate(45deg);");
321    }
322}