1use serde::Deserialize;
5
6use super::animatable::{Animatable, AnimatableField};
7use super::units::{Angle, Length};
8
9#[derive(Debug, Clone, Default, PartialEq, Deserialize)]
13#[serde(rename_all = "camelCase")]
14pub struct Transform {
15 pub translate_x: Option<Animatable<Length>>,
18 pub translate_y: Option<Animatable<Length>>,
21 pub scale: Option<Animatable<f32>>,
23 pub scale_x: Option<Animatable<f32>>,
24 pub scale_y: Option<Animatable<f32>>,
25 pub rotate: Option<Animatable<Angle>>,
28}
29
30#[derive(Debug, Clone, Default, PartialEq, Deserialize)]
36#[serde(rename_all = "camelCase")]
37pub struct Transform3d {
38 pub perspective: Option<Animatable<f32>>,
42 pub translate_x: Option<Animatable<f32>>,
44 pub translate_y: Option<Animatable<f32>>,
46 pub translate_z: Option<Animatable<f32>>,
49 pub rotate_x: Option<Animatable<Angle>>,
51 pub rotate_y: Option<Animatable<Angle>>,
53 pub rotate_z: Option<Animatable<Angle>>,
55 pub scale: Option<Animatable<f32>>,
57 pub scale_x: Option<Animatable<f32>>,
58 pub scale_y: Option<Animatable<f32>>,
59 pub origin: Option<Transform3dOrigin>,
62}
63
64impl Transform3d {
65 pub fn is_identity(&self) -> bool {
69 let Self {
70 perspective,
71 translate_x,
72 translate_y,
73 translate_z,
74 rotate_x,
75 rotate_y,
76 rotate_z,
77 scale,
78 scale_x,
79 scale_y,
80 origin: _, } = self;
82 let no_binding = |f: &Option<Animatable<f32>>| f.binding().is_none();
86 let no_angle_binding = |f: &Option<Animatable<Angle>>| f.binding().is_none();
87 perspective.is_none()
88 && [
89 translate_x,
90 translate_y,
91 translate_z,
92 scale,
93 scale_x,
94 scale_y,
95 ]
96 .iter()
97 .all(|f| no_binding(f))
98 && [rotate_x, rotate_y, rotate_z]
99 .iter()
100 .all(|f| no_angle_binding(f))
101 && translate_x.static_val().unwrap_or(0.0) == 0.0
102 && translate_y.static_val().unwrap_or(0.0) == 0.0
103 && translate_z.static_val().unwrap_or(0.0) == 0.0
104 && rotate_x.static_val().unwrap_or_default().radians() == 0.0
105 && rotate_y.static_val().unwrap_or_default().radians() == 0.0
106 && rotate_z.static_val().unwrap_or_default().radians() == 0.0
107 && scale.static_val().unwrap_or(1.0) == 1.0
108 && scale_x.static_val().unwrap_or(1.0) == 1.0
109 && scale_y.static_val().unwrap_or(1.0) == 1.0
110 }
111}
112
113#[derive(Debug, Clone, PartialEq, Deserialize)]
116pub struct Transform3dOrigin {
117 pub x: Animatable<Length>,
118 pub y: Animatable<Length>,
119}
120
121impl Default for Transform3dOrigin {
122 fn default() -> Self {
123 Self {
124 x: Animatable::Static(Length::Percent(50.0)),
125 y: Animatable::Static(Length::Percent(50.0)),
126 }
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use crate::protocol::props::{Props, props_from_json as props};
134 use crate::protocol::style::{Style, style_groups};
135
136 #[test]
139 fn deserializes_transform_opacity_and_transition() {
140 let s: Style = serde_json::from_str(
141 r#"{
142 "transform": { "scale": 0.95, "translateX": 4, "translateY": "50%" },
143 "opacity": 0.5,
144 "transition": { "transform": { "duration": 0.15, "easing": "easeOut" } }
145 }"#,
146 )
147 .expect("style decodes");
148 let t = s.transform.expect("transform present");
149 assert_eq!(t.scale.static_val(), Some(0.95));
150 assert_eq!(t.translate_x.static_val(), Some(Length::Px(4.0)));
152 assert_eq!(t.translate_y.static_val(), Some(Length::Percent(50.0)));
153 assert_eq!(t.scale_x, None);
154 assert_eq!(s.opacity.static_val(), Some(0.5));
155 let transition = s.transition.expect("transition present");
156 assert!(transition.for_transform().is_some());
157 assert!(transition.for_opacity().is_none());
158 }
159
160 #[test]
165 fn deserializes_transform3d() {
166 let s: Style = serde_json::from_str(
167 r#"{
168 "transform3d": {
169 "perspective": 800,
170 "translateZ": -20,
171 "rotateY": 45,
172 "rotateX": "1.5rad",
173 "rotateZ": "not-an-angle",
174 "scale": 1.25,
175 "origin": { "x": "50%", "y": 10 }
176 }
177 }"#,
178 )
179 .expect("style decodes");
180 let t = s.transform3d.clone().expect("transform3d present");
181 assert_eq!(t.perspective.static_val(), Some(800.0));
182 assert_eq!(t.translate_z.static_val(), Some(-20.0));
183 assert_eq!(
184 t.rotate_y.static_val().unwrap().radians(),
185 45f32.to_radians()
186 );
187 assert_eq!(t.rotate_x.static_val().unwrap().radians(), 1.5);
188 assert_eq!(t.rotate_z.static_val().unwrap().radians(), 0.0);
190 assert_eq!(t.scale.static_val(), Some(1.25));
191 let origin = t.origin.clone().expect("origin present");
192 assert_eq!(origin.x.value(), Some(&Length::Percent(50.0)));
193 assert_eq!(origin.y.value(), Some(&Length::Px(10.0)));
194 assert!(!t.is_identity());
195
196 let s: Style = serde_json::from_str(r#"{ "transform3d": {} }"#).expect("style decodes");
197 assert!(s.transform3d.expect("present").is_identity());
198
199 let mut cached = Props::default();
200 let (dirty, _) = cached.merge_delta(
201 props(serde_json::json!({ "style": { "transform3d": { "rotateY": 45 } } })),
202 &[],
203 &[],
204 );
205 assert!(dirty.style.intersects(style_groups::TRANSFORM3D));
206 assert!(dirty.style.intersects(style_groups::LAYER));
207 assert!(dirty.style.intersects(style_groups::TRANSITION));
208 }
209}