Strict types for style values (#269)
1//! Helper trait to calculate dimensions during layout resolution
2
3use crate::prelude::{Dimension, LengthPercentage, LengthPercentageAuto, Rect, Size};
4use crate::style_helpers::TaffyZero;
5
6/// Trait to encapsulate behaviour where we need to resolve from a
7/// potentially context-dependent size or dimension into
8/// a context-independent size or dimension.
9///
10/// Will return a `None` if it unable to resolve.
11pub(crate) trait MaybeResolve<In, Out> {
12 /// Resolve a dimension that might be dependent on a context, with `None` as fallback value
13 fn maybe_resolve(self, context: In) -> Out;
14}
15
16/// Trait to encapsulate behaviour where we need to resolve from a
17/// potentially context-dependent size or dimension into
18/// a context-independent size or dimension.
19///
20/// Will return a default value if it unable to resolve.
21pub(crate) trait ResolveOrZero<TContext, TOutput: TaffyZero> {
22 /// Resolve a dimension that might be dependent on a context, with a default fallback value
23 fn resolve_or_zero(self, context: TContext) -> TOutput;
24}
25
26impl MaybeResolve<Option<f32>, Option<f32>> for LengthPercentage {
27 /// Converts the given [`LengthPercentage`] into a concrete value of points
28 /// Can return `None`
29 fn maybe_resolve(self, context: Option<f32>) -> Option<f32> {
30 match self {
31 LengthPercentage::Points(points) => Some(points),
32 LengthPercentage::Percent(percent) => context.map(|dim| dim * percent),
33 }
34 }
35}
36
37impl MaybeResolve<Option<f32>, Option<f32>> for LengthPercentageAuto {
38 /// Converts the given [`LengthPercentageAuto`] into a concrete value of points
39 /// Can return `None`
40 fn maybe_resolve(self, context: Option<f32>) -> Option<f32> {
41 match self {
42 LengthPercentageAuto::Points(points) => Some(points),
43 LengthPercentageAuto::Percent(percent) => context.map(|dim| dim * percent),
44 LengthPercentageAuto::Auto => None,
45 }
46 }
47}
48
49impl MaybeResolve<Option<f32>, Option<f32>> for Dimension {
50 /// Converts the given [`Dimension`] into a concrete value of points
51 ///
52 /// Can return `None`
53 fn maybe_resolve(self, context: Option<f32>) -> Option<f32> {
54 match self {
55 Dimension::Points(points) => Some(points),
56 Dimension::Percent(percent) => context.map(|dim| dim * percent),
57 Dimension::Auto => None,
58 }
59 }
60}
61
62// Generic implementation of MaybeResolve for f32 context where MaybeResolve is implemented
63// for Option<f32> context
64impl<T: MaybeResolve<Option<f32>, Option<f32>>> MaybeResolve<f32, Option<f32>> for T {
65 /// Converts the given MaybeResolve value into a concrete value of points
66 /// Can return `None`
67 fn maybe_resolve(self, context: f32) -> Option<f32> {
68 self.maybe_resolve(Some(context))
69 }
70}
71
72// Generic MaybeResolve for Size
73impl<In, Out, T: MaybeResolve<In, Out>> MaybeResolve<Size<In>, Size<Out>> for Size<T> {
74 /// Converts any `parent`-relative values for size into an absolute size
75 fn maybe_resolve(self, context: Size<In>) -> Size<Out> {
76 Size { width: self.width.maybe_resolve(context.width), height: self.height.maybe_resolve(context.height) }
77 }
78}
79
80impl ResolveOrZero<Option<f32>, f32> for LengthPercentage {
81 /// Will return a default value of result is evaluated to `None`
82 fn resolve_or_zero(self, context: Option<f32>) -> f32 {
83 self.maybe_resolve(context).unwrap_or(0.0)
84 }
85}
86
87impl ResolveOrZero<Option<f32>, f32> for LengthPercentageAuto {
88 /// Will return a default value of result is evaluated to `None`
89 fn resolve_or_zero(self, context: Option<f32>) -> f32 {
90 self.maybe_resolve(context).unwrap_or(0.0)
91 }
92}
93
94impl ResolveOrZero<Option<f32>, f32> for Dimension {
95 /// Will return a default value of result is evaluated to `None`
96 fn resolve_or_zero(self, context: Option<f32>) -> f32 {
97 self.maybe_resolve(context).unwrap_or(0.0)
98 }
99}
100
101// Generic ResolveOrZero for Size
102impl<In, Out: TaffyZero, T: ResolveOrZero<In, Out>> ResolveOrZero<Size<In>, Size<Out>> for Size<T> {
103 /// Converts any `parent`-relative values for size into an absolute size
104 fn resolve_or_zero(self, context: Size<In>) -> Size<Out> {
105 Size { width: self.width.resolve_or_zero(context.width), height: self.height.resolve_or_zero(context.height) }
106 }
107}
108
109// Generic ResolveOrZero for resolving Rect against Size
110impl<In: Copy, Out: TaffyZero, T: ResolveOrZero<In, Out>> ResolveOrZero<Size<In>, Rect<Out>> for Rect<T> {
111 /// Converts any `parent`-relative values for Rect into an absolute Rect
112 fn resolve_or_zero(self, context: Size<In>) -> Rect<Out> {
113 Rect {
114 left: self.left.resolve_or_zero(context.width),
115 right: self.right.resolve_or_zero(context.width),
116 top: self.top.resolve_or_zero(context.height),
117 bottom: self.bottom.resolve_or_zero(context.height),
118 }
119 }
120}
121
122// Generic ResolveOrZero for resolving Rect against Option
123impl<Out: TaffyZero, T: ResolveOrZero<Option<f32>, Out>> ResolveOrZero<Option<f32>, Rect<Out>> for Rect<T> {
124 /// Converts any `parent`-relative values for Rect into an absolute Rect
125 fn resolve_or_zero(self, context: Option<f32>) -> Rect<Out> {
126 Rect {
127 left: self.left.resolve_or_zero(context),
128 right: self.right.resolve_or_zero(context),
129 top: self.top.resolve_or_zero(context),
130 bottom: self.bottom.resolve_or_zero(context),
131 }
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 mod maybe_resolve_dimension {
138
139 use crate::resolve::MaybeResolve;
140 use crate::style::Dimension;
141 use rstest::rstest;
142
143 /// `Dimension::Auto` should always return `None`
144 ///
145 /// The parent / context should not affect the outcome.
146 #[rstest]
147 #[case(Dimension::Auto, None, None)]
148 #[case(Dimension::Auto, Some(5.0), None)]
149 #[case(Dimension::Auto, Some(-5.0), None)]
150 #[case(Dimension::Auto, Some(0.), None)]
151 fn resolve_auto(#[case] input: Dimension, #[case] context: Option<f32>, #[case] expected: Option<f32>) {
152 assert_eq!(input.maybe_resolve(context), expected);
153 }
154
155 /// `Dimension::Points` should always return `Some(f32)`
156 /// where the f32 value is the inner value of the points.
157 ///
158 /// The parent / context should not affect the outcome.
159 #[rstest]
160 #[case(Dimension::Points(1.0), None, Some(1.0))]
161 #[case(Dimension::Points(1.0), Some(5.0), Some(1.0))]
162 #[case(Dimension::Points(1.0), Some(-5.0), Some(1.0))]
163 #[case(Dimension::Points(1.0), Some(0.), Some(1.0))]
164 fn resolve_points(#[case] input: Dimension, #[case] context: Option<f32>, #[case] expected: Option<f32>) {
165 assert_eq!(input.maybe_resolve(context), expected);
166 }
167
168 /// `Dimension::Percent` should return `None` if context is `None`.
169 /// Otherwise it should return `Some(f32)`
170 /// where the f32 value is the inner value of the percent * context value.
171 ///
172 /// The parent / context __should__ affect the outcome.
173 #[rstest]
174 #[case(Dimension::Percent(1.0), None, None)]
175 #[case(Dimension::Percent(1.0), Some(5.0), Some(5.0))]
176 #[case(Dimension::Percent(1.0), Some(-5.0), Some(-5.0))]
177 #[case(Dimension::Percent(1.0), Some(50.0), Some(50.0))]
178 fn resolve_percent(#[case] input: Dimension, #[case] context: Option<f32>, #[case] expected: Option<f32>) {
179 assert_eq!(input.maybe_resolve(context), expected);
180 }
181 }
182
183 mod maybe_resolve_size_dimension {
184 use crate::{prelude::Size, resolve::MaybeResolve, style::Dimension};
185 use rstest::rstest;
186
187 /// Size<Dimension::Auto> should always return Size<None>
188 ///
189 /// The parent / context should not affect the outcome.
190 #[rstest]
191 #[case(Size::auto(), Size::NONE, Size::NONE)]
192 #[case(Size::auto(), Size::new(5.0, 5.0), Size::NONE)]
193 #[case(Size::auto(), Size::new(-5.0, -5.0), Size::NONE)]
194 #[case(Size::auto(), Size::new(0.0, 0.0), Size::NONE)]
195 fn maybe_resolve_auto(
196 #[case] input: Size<Dimension>,
197 #[case] context: Size<Option<f32>>,
198 #[case] expected: Size<Option<f32>>,
199 ) {
200 assert_eq!(input.maybe_resolve(context), expected);
201 }
202
203 /// Size<Dimension::Points> should always return a Size<Some(f32)>
204 /// where the f32 values are the inner value of the points.
205 ///
206 /// The parent / context should not affect the outcome.
207 #[rstest]
208 #[case(Size::from_points(5.0, 5.0), Size::NONE, Size::new(5.0, 5.0))]
209 #[case(Size::from_points(5.0, 5.0), Size::new(5.0, 5.0), Size::new(5.0, 5.0))]
210 #[case(Size::from_points(5.0, 5.0), Size::new(-5.0, -5.0), Size::new(5.0, 5.0))]
211 #[case(Size::from_points(5.0, 5.0), Size::new(0.0, 0.0), Size::new(5.0, 5.0))]
212 fn maybe_resolve_points(
213 #[case] input: Size<Dimension>,
214 #[case] context: Size<Option<f32>>,
215 #[case] expected: Size<Option<f32>>,
216 ) {
217 assert_eq!(input.maybe_resolve(context), expected);
218 }
219
220 /// `Size<Dimension::Percent>` should return `Size<None>` if context is `Size<None>`.
221 /// Otherwise it should return `Size<Some(f32)>`
222 /// where the f32 value is the inner value of the percent * context value.
223 ///
224 /// The context __should__ affect the outcome.
225 #[rstest]
226 #[case(Size::from_percent(5.0, 5.0), Size::NONE, Size::NONE)]
227 #[case(Size::from_percent(5.0, 5.0), Size::new(5.0, 5.0), Size::new(25.0, 25.0))]
228 #[case(Size::from_percent(5.0, 5.0), Size::new(-5.0, -5.0), Size::new(-25.0, -25.0))]
229 #[case(Size::from_percent(5.0, 5.0), Size::new(0.0, 0.0), Size::new(0.0, 0.0))]
230 fn maybe_resolve_percent(
231 #[case] input: Size<Dimension>,
232 #[case] context: Size<Option<f32>>,
233 #[case] expected: Size<Option<f32>>,
234 ) {
235 assert_eq!(input.maybe_resolve(context), expected);
236 }
237 }
238
239 mod resolve_or_zero_dimension_to_option_f32 {
240 use crate::resolve::ResolveOrZero;
241 use crate::style::Dimension;
242 use rstest::rstest;
243
244 #[rstest]
245 #[case(Dimension::Auto, None, 0.0)]
246 #[case(Dimension::Auto, Some(5.0), 0.0)]
247 #[case(Dimension::Auto, Some(-5.0), 0.0)]
248 #[case(Dimension::Auto, Some(0.0), 0.0)]
249 fn resolve_or_zero_auto(#[case] input: Dimension, #[case] context: Option<f32>, #[case] expected: f32) {
250 assert_eq!(input.resolve_or_zero(context), expected);
251 }
252 #[rstest]
253 #[case(Dimension::Points(5.0), None, 5.0)]
254 #[case(Dimension::Points(5.0), Some(5.0), 5.0)]
255 #[case(Dimension::Points(5.0), Some(-5.0), 5.0)]
256 #[case(Dimension::Points(5.0), Some(0.0), 5.0)]
257 fn resolve_or_zero_points(#[case] input: Dimension, #[case] context: Option<f32>, #[case] expected: f32) {
258 assert_eq!(input.resolve_or_zero(context), expected);
259 }
260 #[rstest]
261 #[case(Dimension::Percent(5.0), None, 0.0)]
262 #[case(Dimension::Percent(5.0), Some(5.0), 25.0)]
263 #[case(Dimension::Percent(5.0), Some(-5.0), -25.0)]
264 #[case(Dimension::Percent(5.0), Some(0.0), 0.0)]
265 fn resolve_or_zero_percent(#[case] input: Dimension, #[case] context: Option<f32>, #[case] expected: f32) {
266 assert_eq!(input.resolve_or_zero(context), expected);
267 }
268 }
269
270 mod resolve_or_zero_rect_dimension_to_rect {
271 use crate::geometry::{Rect, Size};
272 use crate::resolve::ResolveOrZero;
273 use crate::style::Dimension;
274 use rstest::rstest;
275
276 #[rstest]
277 #[case(Rect::auto(), Size::NONE, Rect::zero())]
278 #[case(Rect::auto(), Size::new(5.0, 5.0), Rect::zero())]
279 #[case(Rect::auto(), Size::new(-5.0, -5.0), Rect::zero())]
280 #[case(Rect::auto(), Size::new(0.0, 0.0), Rect::zero())]
281 fn resolve_or_zero_auto(
282 #[case] input: Rect<Dimension>,
283 #[case] context: Size<Option<f32>>,
284 #[case] expected: Rect<f32>,
285 ) {
286 assert_eq!(input.resolve_or_zero(context), expected);
287 }
288
289 #[rstest]
290 #[case(Rect::from_points(5.0, 5.0, 5.0, 5.0), Size::NONE, Rect::new(5.0, 5.0, 5.0, 5.0))]
291 #[case(Rect::from_points(5.0, 5.0, 5.0, 5.0), Size::new(5.0, 5.0), Rect::new(5.0, 5.0, 5.0, 5.0))]
292 #[case(Rect::from_points(5.0, 5.0, 5.0, 5.0), Size::new(-5.0, -5.0), Rect::new(5.0, 5.0, 5.0, 5.0))]
293 #[case(Rect::from_points(5.0, 5.0, 5.0, 5.0), Size::new(0.0, 0.0), Rect::new(5.0, 5.0, 5.0, 5.0))]
294 fn resolve_or_zero_points(
295 #[case] input: Rect<Dimension>,
296 #[case] context: Size<Option<f32>>,
297 #[case] expected: Rect<f32>,
298 ) {
299 assert_eq!(input.resolve_or_zero(context), expected);
300 }
301
302 #[rstest]
303 #[case(Rect::from_percent(5.0, 5.0, 5.0, 5.0), Size::NONE, Rect::zero())]
304 #[case(Rect::from_percent(5.0, 5.0, 5.0, 5.0), Size::new(5.0, 5.0), Rect::new(25.0, 25.0, 25.0, 25.0))]
305 #[case(Rect::from_percent(5.0, 5.0, 5.0, 5.0), Size::new(-5.0, -5.0), Rect::new(-25.0, -25.0, -25.0, -25.0))]
306 #[case(Rect::from_percent(5.0, 5.0, 5.0, 5.0), Size::new(0.0, 0.0), Rect::zero())]
307 fn resolve_or_zero_percent(
308 #[case] input: Rect<Dimension>,
309 #[case] context: Size<Option<f32>>,
310 #[case] expected: Rect<f32>,
311 ) {
312 assert_eq!(input.resolve_or_zero(context), expected);
313 }
314 }
315
316 mod resolve_or_zero_rect_dimension_to_rect_f32_via_option {
317 use crate::geometry::Rect;
318 use crate::resolve::ResolveOrZero;
319 use crate::style::Dimension;
320 use rstest::rstest;
321
322 #[rstest]
323 #[case(Rect::auto(), None, Rect::zero())]
324 #[case(Rect::auto(), Some(5.0), Rect::zero())]
325 #[case(Rect::auto(), Some(-5.0), Rect::zero())]
326 #[case(Rect::auto(), Some(0.0), Rect::zero())]
327 fn resolve_or_zero_auto(
328 #[case] input: Rect<Dimension>,
329 #[case] context: Option<f32>,
330 #[case] expected: Rect<f32>,
331 ) {
332 assert_eq!(input.resolve_or_zero(context), expected);
333 }
334
335 #[rstest]
336 #[case(Rect::from_points(5.0, 5.0, 5.0, 5.0), None, Rect::new(5.0, 5.0, 5.0, 5.0))]
337 #[case(Rect::from_points(5.0, 5.0, 5.0, 5.0), Some(5.0), Rect::new(5.0, 5.0, 5.0, 5.0))]
338 #[case(Rect::from_points(5.0, 5.0, 5.0, 5.0), Some(-5.0), Rect::new(5.0, 5.0, 5.0, 5.0))]
339 #[case(Rect::from_points(5.0, 5.0, 5.0, 5.0), Some(0.0), Rect::new(5.0, 5.0, 5.0, 5.0))]
340 fn resolve_or_zero_points(
341 #[case] input: Rect<Dimension>,
342 #[case] context: Option<f32>,
343 #[case] expected: Rect<f32>,
344 ) {
345 assert_eq!(input.resolve_or_zero(context), expected);
346 }
347
348 #[rstest]
349 #[case(Rect::from_percent(5.0, 5.0, 5.0, 5.0), None, Rect::zero())]
350 #[case(Rect::from_percent(5.0, 5.0, 5.0, 5.0), Some(5.0), Rect::new(25.0, 25.0, 25.0, 25.0))]
351 #[case(Rect::from_percent(5.0, 5.0, 5.0, 5.0), Some(-5.0), Rect::new(-25.0, -25.0, -25.0, -25.0))]
352 #[case(Rect::from_percent(5.0, 5.0, 5.0, 5.0), Some(0.0), Rect::zero())]
353 fn resolve_or_zero_percent(
354 #[case] input: Rect<Dimension>,
355 #[case] context: Option<f32>,
356 #[case] expected: Rect<f32>,
357 ) {
358 assert_eq!(input.resolve_or_zero(context), expected);
359 }
360 }
361}