1use std::ops::Mul;
2
3#[allow(unused)]
4use pax_runtime_api::math::Generic;
5use pax_runtime_api::math::{Point2, Space, TransformParts};
6use pax_runtime_api::{Interpolatable, LocalProperty, Percent, Rotation, Window};
7
8use crate::api::math::{Transform2, Vector2};
9use crate::api::{Axis, LayoutRole, Size, Transform2D};
10use crate::node_interface::NodeLocal;
11use crate::ContainerFrame;
12
13pub fn compute_tab(
15 layout_properties: LocalProperty<LayoutProperties>,
16 extra_transform: LocalProperty<Option<Transform2D>>,
17 container_transform_and_bounds: LocalProperty<TransformAndBounds<NodeLocal, Window>>,
18) -> LocalProperty<TransformAndBounds<NodeLocal, Window>> {
19 let deps = [
23 layout_properties.untyped(),
24 container_transform_and_bounds.untyped(),
25 extra_transform.untyped(),
26 ];
27
28 LocalProperty::computed(
29 move || {
30 let container_t_and_b = container_transform_and_bounds.get();
31 layout_properties.read(|layout_properties| {
32 let transform_and_bounds =
33 calculate_transform_and_bounds(&layout_properties, container_t_and_b.clone());
34 let extra_transform = extra_transform.get();
35 if let Some(transform) = extra_transform {
36 transform.apply(transform_and_bounds)
37 } else {
38 transform_and_bounds
39 }
40 })
41 },
42 &deps,
43 )
44}
45
46pub fn apply_container_frame(
51 container_transform_and_bounds: TransformAndBounds<NodeLocal, Window>,
52 container_frame: Option<ContainerFrame>,
53) -> TransformAndBounds<NodeLocal, Window> {
54 match container_frame {
55 Some(frame) => TransformAndBounds {
56 transform: container_transform_and_bounds.transform * frame.transform,
57 bounds: frame.bounds,
58 },
59 None => container_transform_and_bounds,
60 }
61}
62
63pub fn apply_padding_frame(
65 container_transform_and_bounds: TransformAndBounds<NodeLocal, Window>,
66 padding_x: Option<Size>,
67 padding_y: Option<Size>,
68) -> TransformAndBounds<NodeLocal, Window> {
69 let padding_x = padding_x
70 .map(|padding| {
71 padding
72 .evaluate(container_transform_and_bounds.bounds, Axis::X)
73 .max(0.0)
74 })
75 .unwrap_or(0.0);
76 let padding_y = padding_y
77 .map(|padding| {
78 padding
79 .evaluate(container_transform_and_bounds.bounds, Axis::Y)
80 .max(0.0)
81 })
82 .unwrap_or(0.0);
83
84 if padding_x == 0.0 && padding_y == 0.0 {
85 return container_transform_and_bounds;
86 }
87
88 TransformAndBounds {
89 transform: container_transform_and_bounds.transform
90 * Transform2::translate(Vector2::new(padding_x, padding_y)),
91 bounds: (
92 (container_transform_and_bounds.bounds.0 - (2.0 * padding_x)).max(0.0),
93 (container_transform_and_bounds.bounds.1 - (2.0 * padding_y)).max(0.0),
94 ),
95 }
96}
97
98pub fn resolve_padded_autosize_axis(content_extent: f64, padding: Option<Size>) -> Option<f64> {
100 let Some(padding) = padding else {
101 return Some(content_extent.max(0.0));
102 };
103
104 let (pixel_component, percent_component) = match padding {
105 Size::Pixels(pixels) => (pixels.to_float().max(0.0), 0.0),
106 Size::Percent(percent) => (0.0, percent.to_float().max(0.0) / 100.0),
107 Size::Combined(pixels, percent) => (
108 pixels.to_float().max(0.0),
109 percent.to_float().max(0.0) / 100.0,
110 ),
111 };
112
113 let denominator = 1.0 - (2.0 * percent_component);
114 (denominator > f64::EPSILON)
115 .then_some(((content_extent.max(0.0) + (2.0 * pixel_component)) / denominator).max(0.0))
116}
117
118#[derive(Clone, Copy, Debug, Default, PartialEq)]
124pub struct LayoutHull {
125 pub min_x: f64,
126 pub max_x: f64,
127 pub min_y: f64,
128 pub max_y: f64,
129 pub valid_x: bool,
130 pub valid_y: bool,
131}
132
133impl Interpolatable for LayoutHull {}
134
135const LAYOUT_MEASUREMENT_EPSILON: f64 = 1e-9;
136
137pub(crate) fn layout_hulls_equivalent(previous: &LayoutHull, candidate: &LayoutHull) -> bool {
138 previous.valid_x == candidate.valid_x
139 && previous.valid_y == candidate.valid_y
140 && layout_measurements_equivalent(previous.min_x, candidate.min_x)
141 && layout_measurements_equivalent(previous.max_x, candidate.max_x)
142 && layout_measurements_equivalent(previous.min_y, candidate.min_y)
143 && layout_measurements_equivalent(previous.max_y, candidate.max_y)
144}
145
146fn layout_measurements_equivalent(previous: f64, candidate: f64) -> bool {
147 if previous == candidate {
148 return true;
149 }
150 if !previous.is_finite() || !candidate.is_finite() {
151 return previous.to_bits() == candidate.to_bits();
152 }
153 (previous - candidate).abs() <= LAYOUT_MEASUREMENT_EPSILON
154}
155
156impl LayoutHull {
157 pub fn from_bounds(bounds: (f64, f64)) -> Self {
158 Self::from_axis_ranges(Some((0.0, bounds.0)), Some((0.0, bounds.1)))
159 }
160
161 pub fn from_axis_ranges(x: Option<(f64, f64)>, y: Option<(f64, f64)>) -> Self {
162 let (min_x, max_x, valid_x) = match x {
163 Some((min_x, max_x)) => {
164 let (min_x, max_x) = order_pair(min_x, max_x);
165 (min_x, max_x, true)
166 }
167 None => (0.0, 0.0, false),
168 };
169 let (min_y, max_y, valid_y) = match y {
170 Some((min_y, max_y)) => {
171 let (min_y, max_y) = order_pair(min_y, max_y);
172 (min_y, max_y, true)
173 }
174 None => (0.0, 0.0, false),
175 };
176 Self {
177 min_x,
178 max_x,
179 min_y,
180 max_y,
181 valid_x,
182 valid_y,
183 }
184 }
185
186 pub fn x_range(&self) -> Option<(f64, f64)> {
187 self.valid_x.then_some((self.min_x, self.max_x))
188 }
189
190 pub fn y_range(&self) -> Option<(f64, f64)> {
191 self.valid_y.then_some((self.min_y, self.max_y))
192 }
193
194 pub fn union(self, other: Self) -> Self {
195 Self::from_axis_ranges(
196 union_axis_ranges(self.x_range(), other.x_range()),
197 union_axis_ranges(self.y_range(), other.y_range()),
198 )
199 }
200
201 pub fn forward_extent_x(&self) -> Option<f64> {
202 self.x_range().map(|(_, max_x)| max_x.max(0.0))
203 }
204
205 pub fn forward_extent_y(&self) -> Option<f64> {
206 self.y_range().map(|(_, max_y)| max_y.max(0.0))
207 }
208}
209
210pub fn project_layout_hull<F: Space, T: Space>(
213 transform: Transform2<F, T>,
214 hull: LayoutHull,
215) -> LayoutHull {
216 let mixes_axes = transform.m[1].abs() > f64::EPSILON || transform.m[2].abs() > f64::EPSILON;
217 if hull.valid_x && hull.valid_y {
218 let corners = [
219 transform * Point2::new(hull.min_x, hull.min_y),
220 transform * Point2::new(hull.min_x, hull.max_y),
221 transform * Point2::new(hull.max_x, hull.min_y),
222 transform * Point2::new(hull.max_x, hull.max_y),
223 ];
224 let (min_x, max_x) = corners.iter().fold(
225 (f64::INFINITY, f64::NEG_INFINITY),
226 |(min_x, max_x), point| (min_x.min(point.x), max_x.max(point.x)),
227 );
228 let (min_y, max_y) = corners.iter().fold(
229 (f64::INFINITY, f64::NEG_INFINITY),
230 |(min_y, max_y), point| (min_y.min(point.y), max_y.max(point.y)),
231 );
232 return LayoutHull::from_axis_ranges(Some((min_x, max_x)), Some((min_y, max_y)));
233 }
234
235 if mixes_axes {
236 return LayoutHull::default();
237 }
238
239 let x = hull
240 .x_range()
241 .map(|(min_x, max_x)| project_axis_range(min_x, max_x, transform.m[0], transform.m[4]));
242 let y = hull
243 .y_range()
244 .map(|(min_y, max_y)| project_axis_range(min_y, max_y, transform.m[3], transform.m[5]));
245 LayoutHull::from_axis_ranges(x, y)
246}
247
248pub fn project_child_layout_hull_to_parent_space(
250 parent: TransformAndBounds<NodeLocal, Window>,
251 child: TransformAndBounds<NodeLocal, Window>,
252 child_hull: LayoutHull,
253) -> LayoutHull {
254 let relative_transform = parent.transform.inverse() * child.transform;
255 let layout_transform: Transform2<NodeLocal, NodeLocal> = Transform2::translate(Vector2::new(
259 relative_transform.m[4],
260 relative_transform.m[5],
261 ));
262 project_layout_hull(layout_transform, child_hull)
263}
264
265pub(crate) fn projected_child_layout_hull_property(
270 parent: LocalProperty<TransformAndBounds<NodeLocal, Window>>,
271 padding_x: LocalProperty<Option<Size>>,
272 padding_y: LocalProperty<Option<Size>>,
273 child: LocalProperty<TransformAndBounds<NodeLocal, Window>>,
274 child_hull: LocalProperty<LayoutHull>,
275 child_layout_role: LocalProperty<Option<LayoutRole>>,
276 name: &str,
277) -> LocalProperty<LayoutHull> {
278 let deps = [
279 parent.untyped(),
280 padding_x.untyped(),
281 padding_y.untyped(),
282 child.untyped(),
283 child_hull.untyped(),
284 child_layout_role.untyped(),
285 ];
286 LocalProperty::computed_with_cutoff_and_name(
287 move || {
288 if child_layout_role.get() == Some(LayoutRole::Breakout) {
289 return LayoutHull::default();
290 }
291 let parent = apply_padding_frame(parent.get(), padding_x.get(), padding_y.get());
292 project_child_layout_hull_to_parent_space(parent, child.get(), child_hull.get())
293 },
294 &deps,
295 layout_hulls_equivalent,
296 name,
297 )
298}
299
300pub fn add_symmetric_padding_to_content_layout_hull(
307 hull: LayoutHull,
308 padding_x: Option<Size>,
309 padding_y: Option<Size>,
310) -> LayoutHull {
311 LayoutHull::from_axis_ranges(
312 hull.x_range().and_then(|(min_x, max_x)| {
313 resolve_symmetric_padded_axis_range(min_x, max_x, padding_x)
314 }),
315 hull.y_range().and_then(|(min_y, max_y)| {
316 resolve_symmetric_padded_axis_range(min_y, max_y, padding_y)
317 }),
318 )
319}
320
321fn resolve_symmetric_padded_axis_range(
322 min_extent: f64,
323 max_extent: f64,
324 padding: Option<Size>,
325) -> Option<(f64, f64)> {
326 let Some(padding) = padding else {
327 return Some((min_extent, max_extent));
328 };
329
330 let outer_extent = resolve_padded_autosize_axis(max_extent.max(0.0), Some(padding))?;
331 let leading_padding = evaluate_padding_against_resolved_extent(padding, outer_extent)?;
332 Some((
333 min_extent + leading_padding,
334 outer_extent.max(max_extent + leading_padding),
335 ))
336}
337
338fn evaluate_padding_against_resolved_extent(padding: Size, outer_extent: f64) -> Option<f64> {
339 let (pixel_component, percent_component) = padding_components(padding);
340 let padding = pixel_component + (percent_component * outer_extent.max(0.0));
341 padding.is_finite().then_some(padding.max(0.0))
342}
343
344fn padding_components(padding: Size) -> (f64, f64) {
345 match padding {
346 Size::Pixels(pixels) => (pixels.to_float().max(0.0), 0.0),
347 Size::Percent(percent) => (0.0, percent.to_float().max(0.0) / 100.0),
348 Size::Combined(pixels, percent) => (
349 pixels.to_float().max(0.0),
350 percent.to_float().max(0.0) / 100.0,
351 ),
352 }
353}
354
355fn order_pair(a: f64, b: f64) -> (f64, f64) {
356 if a <= b {
357 (a, b)
358 } else {
359 (b, a)
360 }
361}
362
363fn union_axis_ranges(lhs: Option<(f64, f64)>, rhs: Option<(f64, f64)>) -> Option<(f64, f64)> {
364 match (lhs, rhs) {
365 (Some((lhs_min, lhs_max)), Some((rhs_min, rhs_max))) => {
366 Some((lhs_min.min(rhs_min), lhs_max.max(rhs_max)))
367 }
368 (Some(range), None) | (None, Some(range)) => Some(range),
369 (None, None) => None,
370 }
371}
372
373fn project_axis_range(min: f64, max: f64, scale: f64, translate: f64) -> (f64, f64) {
374 order_pair(scale * min + translate, scale * max + translate)
375}
376
377pub fn calculate_transform_and_bounds(
379 LayoutProperties {
380 width,
381 height,
382 anchor_x,
383 anchor_y,
384 x,
385 y,
386 rotate,
387 scale_x,
388 scale_y,
389 skew_x,
390 skew_y,
391 }: &LayoutProperties,
392 TransformAndBounds {
393 transform: container_transform,
394 bounds: container_bounds,
395 }: TransformAndBounds<NodeLocal, Window>,
396) -> TransformAndBounds<NodeLocal, Window> {
397 let x = x.unwrap_or(Size::ZERO());
398 let y = y.unwrap_or(Size::ZERO());
399 let width = width
400 .map(|v| v.evaluate(container_bounds, Axis::X))
401 .unwrap_or(container_bounds.0);
402 let height = height
403 .map(|v| v.evaluate(container_bounds, Axis::Y))
404 .unwrap_or(container_bounds.1);
405 let origin = Vector2::new(
406 x.evaluate(container_bounds, Axis::X),
407 y.evaluate(container_bounds, Axis::Y),
408 );
409
410 let bounds = (width, height);
411
412 let anchor_x = anchor_x.unwrap_or_else(|| match x {
416 Size::Pixels(_) => Size::ZERO(),
417 Size::Combined(_, per) | Size::Percent(per) => {
418 if per.to_float() > 100.0 {
419 Size::default()
420 } else if per.to_float() < 0.0 {
421 Size::ZERO()
422 } else {
423 Size::Percent(per)
424 }
425 }
426 });
427
428 let anchor_y = anchor_y.unwrap_or_else(|| match y {
429 Size::Pixels(_) => Size::ZERO(),
430 Size::Combined(_, per) | Size::Percent(per) => {
431 if per.to_float() > 100.0 {
432 Size::default()
433 } else if per.to_float() < 0.0 {
434 Size::ZERO()
435 } else {
436 Size::Percent(per)
437 }
438 }
439 });
440
441 let anchor_transform = Transform2::translate(Vector2::new(
442 -anchor_x.evaluate(bounds, Axis::X),
443 -anchor_y.evaluate(bounds, Axis::Y),
444 ));
445
446 let scale = Vector2::new(
447 scale_x
448 .as_ref()
449 .map(|s| s.0.to_float() / 100.0)
450 .unwrap_or(1.0),
451 scale_y
452 .as_ref()
453 .map(|s| s.0.to_float() / 100.0)
454 .unwrap_or(1.0),
455 );
456
457 let skew = Vector2::new(
458 skew_x.map(|s| s.get_as_radians()).unwrap_or(0.0),
459 skew_y.map(|s| s.get_as_radians()).unwrap_or(0.0),
460 );
461
462 let rotation = rotate.map(|s| s.get_as_radians()).unwrap_or(0.0);
463
464 let parts = TransformParts {
465 origin,
466 scale,
467 skew,
468 rotation,
469 };
470
471 let combined_transform: Transform2<NodeLocal, NodeLocal> = parts.into();
472
473 TransformAndBounds {
474 transform: container_transform * combined_transform * anchor_transform,
475 bounds,
476 }
477}
478
479impl<F: Space, T: Space> Interpolatable for TransformAndBounds<F, T> {
480 fn interpolate(&self, other: &Self, t: f64) -> Self {
481 TransformAndBounds {
482 transform: self.transform.interpolate(&other.transform, t),
483 bounds: (
484 self.bounds.0 + (other.bounds.0 - self.bounds.0) * t,
485 self.bounds.1 + (other.bounds.1 - self.bounds.1) * t,
486 ),
487 }
488 }
489}
490
491#[derive(PartialEq)]
498pub struct TransformAndBounds<F, T = F> {
499 pub transform: Transform2<F, T>,
500 pub bounds: (f64, f64),
501}
502
503impl<F: Space, T: Space> std::fmt::Debug for TransformAndBounds<F, T> {
504 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
505 f.debug_struct("TransformAndBounds")
506 .field("transform", &self.transform)
507 .field("bounds", &self.bounds)
508 .finish()
509 }
510}
511
512impl PartialEq for TransformAndBounds<NodeLocal, Window> {
513 fn eq(&self, other: &Self) -> bool {
514 self.transform == other.transform && self.bounds == other.bounds
515 }
516}
517
518impl<F: Space, T: Space> Default for TransformAndBounds<F, T> {
519 fn default() -> Self {
520 Self {
521 transform: Default::default(),
522 bounds: (100.0, 100.0),
523 }
524 }
525}
526
527impl<F, T> Clone for TransformAndBounds<F, T> {
528 fn clone(&self) -> Self {
529 Self {
530 transform: self.transform.clone(),
531 bounds: self.bounds.clone(),
532 }
533 }
534}
535
536impl<F, T> Copy for TransformAndBounds<F, T> {}
537
538impl<W1: Space, W2: Space, W3: Space> Mul<TransformAndBounds<W1, W2>>
539 for TransformAndBounds<W2, W3>
540{
541 type Output = TransformAndBounds<W1, W3>;
542
543 fn mul(self, rhs: TransformAndBounds<W1, W2>) -> Self::Output {
552 let s_s = Transform2::scale_sep(Vector2::new(self.bounds.0, self.bounds.1));
553 let r_s = Transform2::scale_sep(Vector2::new(rhs.bounds.0, rhs.bounds.1));
554
555 let s_t = self.transform * s_s;
556 let r_t = rhs.transform * r_s;
557 let res = s_t * r_t * s_s.inverse() * r_s.inverse();
558
559 TransformAndBounds {
560 transform: res,
561 bounds: (self.bounds.0 * rhs.bounds.0, self.bounds.1 * rhs.bounds.1),
562 }
563 }
564}
565
566impl<F: Space, T: Space> TransformAndBounds<F, T> {
567 pub fn center(&self) -> Point2<T> {
569 let (o, u, v) = self.transform.decompose();
570 let u = u * self.bounds.0;
571 let v = v * self.bounds.1;
572 o + v / 2.0 + u / 2.0
573 }
574
575 pub fn corners(&self) -> [Point2<T>; 4] {
577 let (o, u, v) = self.transform.decompose();
578 let u = u * self.bounds.0;
579 let v = v * self.bounds.1;
580 [o, o + v, o + u + v, o + u]
581 }
582
583 pub fn contains_point(&self, point: Point2<T>) -> bool {
585 self.as_transform().contains_point(point)
586 }
587
588 pub fn as_pure_size(self) -> Self {
590 let mut parts: TransformParts = self.transform.into();
591 let bounds_x = std::mem::replace(&mut parts.scale.x, 1.0);
592 let bounds_y = std::mem::replace(&mut parts.scale.y, 1.0);
593 TransformAndBounds {
594 transform: parts.into(),
595 bounds: (self.bounds.0 * bounds_x, self.bounds.1 * bounds_y),
596 }
597 }
598 pub fn as_pure_scale(self) -> Self {
600 TransformAndBounds {
601 transform: self.transform
602 * Transform2::scale_sep(Vector2::new(self.bounds.0, self.bounds.1)),
603 bounds: (1.0, 1.0),
604 }
605 }
606
607 pub fn cast_spaces<A: Space, B: Space>(self) -> TransformAndBounds<A, B> {
609 TransformAndBounds {
610 transform: self.transform.cast_spaces(),
611 bounds: self.bounds,
612 }
613 }
614
615 pub fn as_transform(&self) -> Transform2<F, T> {
617 self.transform * Transform2::scale_sep(Vector2::new(self.bounds.0, self.bounds.1))
618 }
619}
620
621#[test]
622fn test_transform_and_bounds_mult() {
623 let dvx = 0.6;
624 let dvy = 0.3;
625
626 let t_and_b_with_scale = TransformAndBounds::<Generic> {
627 transform: Transform2::new([1.5, 1.1, 2.3, 3.2, 1.2, 1.0])
628 * Transform2::scale_sep(Vector2::<Generic>::new(dvx, dvy)),
629 bounds: (2.1, 1.2),
630 };
631
632 let t_and_b_with_size = TransformAndBounds::<Generic> {
633 transform: Transform2::new([1.5, 1.1, 2.3, 3.2, 1.2, 1.0]),
634 bounds: (2.1 * dvx, 1.2 * dvy),
635 };
636 let some_other_transform = TransformAndBounds::<Generic> {
637 transform: Transform2::new([1.1, 1.2, 5.3, 9.2, 1.0, 2.0]),
638 bounds: (1.9, 4.5),
639 };
640
641 let res_scale = t_and_b_with_scale * some_other_transform;
642 let res_size = t_and_b_with_size * some_other_transform;
643
644 let t_scale = res_scale.transform
645 * Transform2::<Generic>::scale_sep(Vector2::new(res_scale.bounds.0, res_scale.bounds.1));
646 let t_scale_c = t_scale.coeffs();
647 let t_size = res_size.transform
648 * Transform2::<Generic>::scale_sep(Vector2::new(res_size.bounds.0, res_size.bounds.1));
649 let t_size_c = t_size.coeffs();
650 let diff_sum = t_scale_c
651 .iter()
652 .zip(t_size_c)
653 .map(|(a, b)| (a - b).abs())
654 .sum::<f64>();
655 assert!(diff_sum < 1e-4);
656
657 let res_scale_other_way = some_other_transform * t_and_b_with_scale;
658 let res_size_other_way = some_other_transform * t_and_b_with_size;
659
660 let t_scale = res_scale_other_way.transform
661 * Transform2::<Generic>::scale_sep(Vector2::new(
662 res_scale_other_way.bounds.0,
663 res_scale_other_way.bounds.1,
664 ));
665 let t_scale_c = t_scale.coeffs();
666 let t_size = res_size_other_way.transform
667 * Transform2::<Generic>::scale_sep(Vector2::new(
668 res_size_other_way.bounds.0,
669 res_size_other_way.bounds.1,
670 ));
671 let t_size_c = t_size.coeffs();
672 let diff_sum = t_scale_c
673 .iter()
674 .zip(t_size_c)
675 .map(|(a, b)| (a - b).abs())
676 .sum::<f64>();
677 assert!(diff_sum < 1e-4);
678}
679
680#[test]
681fn test_apply_container_frame_uses_assigned_bounds() {
682 let parent = TransformAndBounds::<NodeLocal, Window> {
683 transform: Transform2::translate(Vector2::new(50.0, 60.0)),
684 bounds: (100.0, 100.0),
685 };
686 let frame = ContainerFrame {
687 transform: Transform2::translate(Vector2::new(10.0, 20.0)),
688 bounds: (30.0, 40.0),
689 };
690
691 let result = apply_container_frame(parent, Some(frame));
692
693 assert_eq!(result.bounds, (30.0, 40.0));
694 assert_eq!(result.transform.m[4], 60.0);
695 assert_eq!(result.transform.m[5], 80.0);
696}
697
698#[test]
699fn test_apply_padding_frame_shrinks_and_offsets_child_bounds() {
700 let parent = TransformAndBounds::<NodeLocal, Window> {
701 transform: Transform2::translate(Vector2::new(50.0, 60.0)),
702 bounds: (200.0, 100.0),
703 };
704
705 let result = apply_padding_frame(
706 parent,
707 Some(Size::Pixels(10.into())),
708 Some(Size::Percent(20.into())),
709 );
710
711 assert_eq!(result.bounds, (180.0, 60.0));
712 assert_eq!(result.transform.m[4], 60.0);
713 assert_eq!(result.transform.m[5], 80.0);
714}
715
716#[test]
717fn test_resolve_padded_autosize_axis_solves_percent_padding() {
718 let resolved = resolve_padded_autosize_axis(60.0, Some(Size::Percent(20.into()))).unwrap();
719
720 assert!((resolved - 100.0).abs() < 1e-9);
721}
722
723#[test]
724fn test_add_symmetric_padding_to_content_layout_hull_solves_from_content() {
725 let hull = LayoutHull::from_axis_ranges(Some((0.0, 50.0)), Some((4.0, 84.0)));
726 let padded = add_symmetric_padding_to_content_layout_hull(
727 hull,
728 Some(Size::Pixels(5.into())),
729 Some(Size::Percent(10.into())),
730 );
731
732 assert_eq!(padded.x_range(), Some((5.0, 60.0)));
733 assert!((padded.y_range().unwrap().0 - 14.5).abs() < 1e-9);
734 assert!((padded.forward_extent_y().unwrap() - 105.0).abs() < 1e-9);
735}
736
737#[test]
738fn test_project_layout_hull_with_translation_preserves_forward_extents() {
739 let hull = LayoutHull::from_axis_ranges(Some((-10.0, 40.0)), Some((0.0, 20.0)));
740 let projected = project_layout_hull(
741 Transform2::<Generic>::translate(Vector2::new(30.0, -5.0)),
742 hull,
743 );
744
745 assert_eq!(projected.x_range(), Some((20.0, 70.0)));
746 assert_eq!(projected.y_range(), Some((-5.0, 15.0)));
747 assert_eq!(projected.forward_extent_x(), Some(70.0));
748 assert_eq!(projected.forward_extent_y(), Some(15.0));
749}
750
751#[test]
752fn test_project_layout_hull_keeps_axis_independence_without_cross_axis_mixing() {
753 let hull = LayoutHull::from_axis_ranges(Some((10.0, 30.0)), None);
754 let projected = project_layout_hull(
755 Transform2::<Generic>::new([-2.0, 0.0, 0.0, 3.0, 5.0, 7.0]),
756 hull,
757 );
758
759 assert_eq!(projected.x_range(), Some((-55.0, -15.0)));
760 assert_eq!(projected.y_range(), None);
761}
762
763#[test]
764fn test_project_layout_hull_invalidates_partial_axes_when_transform_mixes_axes() {
765 let hull = LayoutHull::from_axis_ranges(Some((0.0, 20.0)), None);
766 let projected = project_layout_hull(
767 Transform2::<Generic>::rotate(std::f64::consts::FRAC_PI_4),
768 hull,
769 );
770
771 assert_eq!(projected, LayoutHull::default());
772}
773
774#[test]
775fn test_project_child_layout_hull_ignores_rotation_for_flow_measurement() {
776 let parent = TransformAndBounds::<NodeLocal, Window> {
777 transform: Transform2::identity(),
778 bounds: (100.0, 100.0),
779 };
780 let child = TransformAndBounds::<NodeLocal, Window> {
781 transform: Transform2::<NodeLocal, Window>::translate(Vector2::new(10.0, 20.0))
782 * Transform2::<NodeLocal, NodeLocal>::rotate(std::f64::consts::FRAC_PI_4),
783 bounds: (100.0, 100.0),
784 };
785 let hull = LayoutHull::from_axis_ranges(None, Some((0.0, 50.0)));
786
787 let projected = project_child_layout_hull_to_parent_space(parent, child, hull);
788
789 assert_eq!(projected.x_range(), None);
790 assert_eq!(projected.y_range(), Some((20.0, 70.0)));
791}
792
793#[test]
794fn test_layout_hull_equivalence_ignores_numeric_jitter() {
795 let previous = LayoutHull::from_axis_ranges(Some((10.0, 30.0)), Some((20.0, 40.0000000000001)));
796 let equivalent =
797 LayoutHull::from_axis_ranges(Some((10.0, 30.0000000000001)), Some((20.0, 40.0)));
798 let changed = LayoutHull::from_axis_ranges(Some((10.0, 30.001)), Some((20.0, 40.0)));
799
800 assert!(layout_hulls_equivalent(&previous, &equivalent));
801 assert!(!layout_hulls_equivalent(&previous, &changed));
802}
803
804impl Interpolatable for LayoutProperties {}
805
806#[derive(Debug, Default, Clone)]
807pub struct LayoutProperties {
809 pub x: Option<Size>,
810 pub y: Option<Size>,
811 pub width: Option<Size>,
812 pub height: Option<Size>,
813 pub rotate: Option<Rotation>,
814 pub scale_x: Option<Percent>,
815 pub scale_y: Option<Percent>,
816 pub anchor_x: Option<Size>,
817 pub anchor_y: Option<Size>,
818 pub skew_x: Option<Rotation>,
819 pub skew_y: Option<Rotation>,
820}
821
822impl LayoutProperties {
823 pub fn fill() -> Self {
825 Self {
826 x: Some(Size::ZERO()),
827 y: Some(Size::ZERO()),
828 width: Some(Size::default()),
829 height: Some(Size::default()),
830 rotate: Some(Rotation::ZERO()),
831 scale_x: Some(Percent(100.into())),
832 scale_y: Some(Percent(100.into())),
833 anchor_x: None,
834 anchor_y: None,
835 skew_x: Some(Rotation::ZERO()),
836 skew_y: Some(Rotation::ZERO()),
837 }
838 }
839}
840
841impl<F: Space, T: Space> TransformAndBounds<F, T> {
842 pub fn inverse(&self) -> TransformAndBounds<T, F> {
844 let t_inv = self.transform.inverse();
845 let b_inv = (1.0 / self.bounds.0, 1.0 / self.bounds.1);
846 TransformAndBounds {
847 transform: t_inv,
848 bounds: b_inv,
849 }
850 }
851
852 pub fn intersects(&self, other: &Self) -> bool {
854 let corners_self = self.corners();
855 let corners_other = other.corners();
856
857 for i in 0..2 {
858 let axis = (corners_self[i] - corners_self[(i + 1) % 4]).normal();
859
860 let self_projections: Vec<_> = corners_self
861 .iter()
862 .map(|&p| p.to_vector().project_onto(axis).length())
863 .collect();
864 let other_projections: Vec<_> = corners_other
865 .iter()
866 .map(|&p| p.to_vector().project_onto(axis).length())
867 .collect();
868
869 let (min_self, max_self) = min_max_projections(&self_projections);
870 let (min_other, max_other) = min_max_projections(&other_projections);
871
872 if max_self < min_other || max_other < min_self {
874 return false;
876 }
877 }
878 true
879 }
880}
881
882fn min_max_projections(projections: &[f64]) -> (f64, f64) {
883 let min_projection = *projections.iter().min_by(|a, b| a.total_cmp(b)).unwrap();
884 let max_projection = *projections.iter().max_by(|a, b| a.total_cmp(b)).unwrap();
885 (min_projection, max_projection)
886}
887
888pub trait ComputableTransform<F, T> {
889 fn apply(&self, bounds: TransformAndBounds<F, T>) -> TransformAndBounds<F, T>;
890}
891
892impl ComputableTransform<NodeLocal, Window> for Transform2D {
893 fn apply(
894 &self,
895 bounds: TransformAndBounds<NodeLocal, Window>,
896 ) -> TransformAndBounds<NodeLocal, Window> {
897 let layout_properties = LayoutProperties {
898 x: self.translate.map(|v| v[0]),
899 y: self.translate.map(|v| v[1]),
900 width: Some(Size::Pixels(bounds.bounds.0.into())),
901 height: Some(Size::Pixels(bounds.bounds.1.into())),
902 rotate: self.rotate.clone(),
903 scale_x: self
904 .scale
905 .as_ref()
906 .map(|v| Percent((100.0 * v[0].clone().expect_percent()).into())),
907 scale_y: self
908 .scale
909 .as_ref()
910 .map(|v| Percent((100.0 * v[1].clone().expect_percent()).into())),
911 anchor_x: self.anchor.map(|v| v[0]),
912 anchor_y: self.anchor.map(|v| v[1]),
913 skew_x: self.skew.as_ref().map(|v| v[0].clone()),
914 skew_y: self.skew.as_ref().map(|v| v[1].clone()),
915 };
916
917 let curr = calculate_transform_and_bounds(&layout_properties, bounds.clone());
918 match &self.previous {
919 Some(previous) => (*previous).apply(curr),
920 None => curr,
921 }
922 }
923}