1pub mod taffy_engine;
26pub mod animation;
27pub mod spatial;
28pub mod focus;
29pub mod progressive;
30pub mod primitives;
31
32pub use cvkg_core::layout::EdgeInsets;
33use cvkg_core::{LayoutCache, LayoutView};
34use std::cell::RefCell;
35use std::collections::HashSet;
36
37pub use taffy_engine::{
38 taffy_alignment, taffy_distribution, taffy_track, Flex, Grid, GridTrack, HStack, Spacer,
39 TaffyLayoutEngine, VStack, ZStack,
40};
41pub use animation::AnimationEngine;
42pub use spatial::{LayoutSpatialEntry, LayoutSpatialIndex};
43pub use focus::{compute_focus_order, validate_reading_order, LayoutModality, FocusCandidate};
44pub use progressive::{ProgressiveChild, ProgressiveLayoutContext};
45pub use primitives::{AspectRatio, Padding, SafeArea, SafeAreaEdges};
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
49pub struct LayoutCapabilities {
50 pub flexbox: bool,
51 pub grid: bool,
52 pub absolute: bool,
53 pub container_queries: bool,
54}
55
56pub fn layout_capabilities() -> LayoutCapabilities {
58 LayoutCapabilities {
59 flexbox: true,
60 grid: true,
61 absolute: true,
62 container_queries: true,
63 }
64}
65
66thread_local! {
67 static ACTIVE_LAYOUT_NODES: RefCell<HashSet<u64>> = RefCell::new(HashSet::new());
68}
69
70pub struct LayoutCycleGuard {
72 hash: u64,
73}
74
75impl Drop for LayoutCycleGuard {
76 fn drop(&mut self) {
77 if self.hash != 0 {
78 ACTIVE_LAYOUT_NODES.with(|nodes| {
79 nodes.borrow_mut().remove(&self.hash);
80 });
81 }
82 }
83}
84
85pub fn with_layout_cycle_guard<F, R>(hash: u64, fallback: R, f: F) -> R
87where
88 F: FnOnce() -> R,
89{
90 if hash == 0 {
91 return f();
92 }
93 let already_active = ACTIVE_LAYOUT_NODES.with(|nodes| !nodes.borrow_mut().insert(hash));
94 if already_active {
95 log::warn!("[Layout] Cycle detected for view hash 0x{:X}! Breaking cycle with fallback size.", hash);
96 return fallback;
97 }
98 let _guard = LayoutCycleGuard { hash };
99 f()
100}
101
102pub fn with_layout_cycle_guard_void<F>(hash: u64, f: F)
104where
105 F: FnOnce(),
106{
107 if hash == 0 {
108 f();
109 return;
110 }
111 let already_active = ACTIVE_LAYOUT_NODES.with(|nodes| !nodes.borrow_mut().insert(hash));
112 if already_active {
113 log::warn!("[Layout] Cycle detected for view hash 0x{:X}! Breaking cycle placement.", hash);
114 return;
115 }
116 let _guard = LayoutCycleGuard { hash };
117 f();
118}
119
120pub fn size_views_parallel(
122 views: &[&dyn LayoutView],
123 proposal: cvkg_core::SizeProposal,
124 cache: &mut LayoutCache,
125) -> Vec<cvkg_core::Size> {
126 if views.len() <= 1 {
127 return views
128 .iter()
129 .map(|v| v.size_that_fits(proposal, &[], cache))
130 .collect();
131 }
132
133 views
134 .iter()
135 .map(|v| v.size_that_fits(proposal, &[], cache))
136 .collect()
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use cvkg_core::{Alignment, Distribution, Rect, Size, SizeProposal};
143
144 struct MockView {
145 size: Size,
146 flex: f32,
147 }
148
149 impl LayoutView for MockView {
150 fn size_that_fits(
151 &self,
152 _p: SizeProposal,
153 _s: &[&dyn LayoutView],
154 _c: &mut LayoutCache,
155 ) -> Size {
156 self.size
157 }
158 fn place_subviews(&self, _b: Rect, _s: &mut [&mut dyn LayoutView], _c: &mut LayoutCache) {}
159 fn flex_weight(&self) -> f32 {
160 self.flex
161 }
162 }
163
164 #[test]
165 fn test_hstack_basic() {
166 let v1 = MockView {
167 size: Size {
168 width: 50.0,
169 height: 50.0,
170 },
171 flex: 0.0,
172 };
173 let v2 = MockView {
174 size: Size {
175 width: 100.0,
176 height: 100.0,
177 },
178 flex: 0.0,
179 };
180 let views: Vec<&dyn LayoutView> = vec![&v1, &v2];
181 let mut cache = LayoutCache::new();
182 let bounds = Rect {
183 x: 0.0,
184 y: 0.0,
185 width: 300.0,
186 height: 200.0,
187 };
188
189 let rects = HStack::compute_layout(
190 10.0,
191 Alignment::Center,
192 Distribution::Leading,
193 bounds,
194 &views,
195 &mut cache,
196 );
197
198 assert_eq!(rects.len(), 2);
199 assert_eq!(
200 rects[0],
201 Rect {
202 x: 0.0,
203 y: 75.0,
204 width: 50.0,
205 height: 50.0
206 }
207 );
208 assert_eq!(
209 rects[1],
210 Rect {
211 x: 60.0,
212 y: 50.0,
213 width: 100.0,
214 height: 100.0
215 }
216 );
217 }
218
219 #[test]
220 fn test_vstack_flex() {
221 let v1 = MockView {
222 size: Size {
223 width: 100.0,
224 height: 50.0,
225 },
226 flex: 0.0,
227 };
228 let v2 = MockView {
229 size: Size {
230 width: 100.0,
231 height: 0.0,
232 },
233 flex: 1.0,
234 };
235 let views: Vec<&dyn LayoutView> = vec![&v1, &v2];
236 let mut cache = LayoutCache::new();
237 let bounds = Rect {
238 x: 0.0,
239 y: 0.0,
240 width: 200.0,
241 height: 160.0,
242 };
243
244 let rects = VStack::compute_layout(
245 10.0,
246 Alignment::Leading,
247 Distribution::Fill,
248 bounds,
249 &views,
250 &mut cache,
251 );
252
253 assert_eq!(rects.len(), 2);
254 assert_eq!(
255 rects[0],
256 Rect {
257 x: 0.0,
258 y: 0.0,
259 width: 100.0,
260 height: 50.0
261 }
262 );
263 assert_eq!(
264 rects[1],
265 Rect {
266 x: 0.0,
267 y: 60.0,
268 width: 100.0,
269 height: 100.0
270 }
271 );
272 }
273
274 #[test]
275 fn test_grid_layout() {
276 let v1 = MockView {
277 size: Size::ZERO,
278 flex: 0.0,
279 };
280 let v2 = MockView {
281 size: Size::ZERO,
282 flex: 0.0,
283 };
284 let v3 = MockView {
285 size: Size::ZERO,
286 flex: 0.0,
287 };
288 let views: Vec<&dyn LayoutView> = vec![&v1, &v2, &v3];
289 let mut cache = LayoutCache::new();
290 let bounds = Rect {
291 x: 0.0,
292 y: 0.0,
293 width: 210.0,
294 height: 210.0,
295 };
296
297 let grid = Grid::new(
298 vec![GridTrack::Fixed(100.0), GridTrack::Fixed(100.0)],
299 vec![GridTrack::Fixed(100.0), GridTrack::Fixed(100.0)],
300 10.0,
301 10.0,
302 );
303 let placements = vec![
304 Some(cvkg_core::GridPlacement {
305 column: 0,
306 column_span: 1,
307 row: 0,
308 row_span: 1,
309 }),
310 Some(cvkg_core::GridPlacement {
311 column: 1,
312 column_span: 1,
313 row: 0,
314 row_span: 1,
315 }),
316 Some(cvkg_core::GridPlacement {
317 column: 0,
318 column_span: 1,
319 row: 1,
320 row_span: 1,
321 }),
322 ];
323
324 let rects = grid.compute_layout_rects(bounds, &views, &placements, &mut cache);
325
326 assert_eq!(rects.len(), 3);
327 assert_eq!(
328 rects[0],
329 Rect {
330 x: 0.0,
331 y: 0.0,
332 width: 100.0,
333 height: 100.0
334 }
335 );
336 assert_eq!(
337 rects[1],
338 Rect {
339 x: 110.0,
340 y: 0.0,
341 width: 100.0,
342 height: 100.0
343 }
344 );
345 assert_eq!(
346 rects[2],
347 Rect {
348 x: 0.0,
349 y: 110.0,
350 width: 100.0,
351 height: 100.0
352 }
353 );
354 }
355
356 #[test]
357 fn test_layout_cycle_detection() {
358 struct CyclingView {
359 child_hash: u64,
360 }
361 impl LayoutView for CyclingView {
362 fn size_that_fits(
363 &self,
364 proposal: SizeProposal,
365 _subviews: &[&dyn LayoutView],
366 cache: &mut LayoutCache,
367 ) -> Size {
368 with_layout_cycle_guard(self.view_hash(), Size { width: 42.0, height: 42.0 }, || {
369 let recursive_self = CyclingView { child_hash: self.view_hash() };
370 let subviews: Vec<&dyn LayoutView> = vec![&recursive_self];
371 recursive_self.size_that_fits(proposal, &subviews, cache)
372 })
373 }
374 fn place_subviews(&self, _b: Rect, _s: &mut [&mut dyn LayoutView], _c: &mut LayoutCache) {}
375 fn view_hash(&self) -> u64 {
376 12345
377 }
378 }
379
380 let view = CyclingView { child_hash: 12345 };
381 let mut cache = LayoutCache::new();
382 let size = view.size_that_fits(SizeProposal::unspecified(), &[], &mut cache);
383 assert_eq!(size.width, 42.0);
384 assert_eq!(size.height, 42.0);
385 }
386
387 #[test]
388 fn test_bottom_up_layout_invalidation() {
389 let mut cache = LayoutCache::new();
390 let child_hash = 100u64;
391 let parent_hash = 200u64;
392
393 cache.register_parent(child_hash, parent_hash);
394 cache.set_size(child_hash, SizeProposal::unspecified(), Size { width: 10.0, height: 10.0 });
395 cache.set_size(parent_hash, SizeProposal::unspecified(), Size { width: 20.0, height: 20.0 });
396
397 assert!(cache.get_size(child_hash, SizeProposal::unspecified()).is_some());
398 assert!(cache.get_size(parent_hash, SizeProposal::unspecified()).is_some());
399
400 cache.invalidate_view(child_hash);
401
402 assert!(cache.get_size(child_hash, SizeProposal::unspecified()).is_none());
403 assert!(cache.get_size(parent_hash, SizeProposal::unspecified()).is_none());
404 }
405
406 #[test]
407 fn test_viewport_aware_layout_culling() {
408 use std::sync::atomic::{AtomicUsize, Ordering};
409 use std::sync::Arc;
410
411 struct SpyView {
412 calls: Arc<AtomicUsize>,
413 hash: u64,
414 rect: Rect,
415 }
416
417 impl LayoutView for SpyView {
418 fn size_that_fits(&self, _p: SizeProposal, _s: &[&dyn LayoutView], _c: &mut LayoutCache) -> Size {
419 Size { width: self.rect.width, height: self.rect.height }
420 }
421 fn place_subviews(&self, _b: Rect, _s: &mut [&mut dyn LayoutView], _c: &mut LayoutCache) {
422 self.calls.fetch_add(1, Ordering::SeqCst);
423 }
424 fn view_hash(&self) -> u64 {
425 self.hash
426 }
427 }
428
429 let calls = Arc::new(AtomicUsize::new(0));
430 let view1 = SpyView {
431 calls: calls.clone(),
432 hash: 1001,
433 rect: Rect::new(0.0, 0.0, 50.0, 50.0),
434 };
435 let view2 = SpyView {
436 calls: calls.clone(),
437 hash: 1002,
438 rect: Rect::new(500.0, 0.0, 50.0, 50.0),
439 };
440
441 let mut cache = LayoutCache::new();
442 cache.viewport = Some(Rect::new(0.0, 0.0, 55.0, 100.0));
443
444 let mut v1 = view1;
445 let mut v2 = view2;
446 let mut mut_subviews: Vec<&mut dyn LayoutView> = vec![&mut v1, &mut v2];
447
448 HStack::new(10.0, Alignment::Center, Distribution::Leading)
449 .place_subviews(Rect::new(0.0, 0.0, 600.0, 100.0), &mut mut_subviews, &mut cache);
450
451 assert_eq!(calls.load(Ordering::SeqCst), 1);
452 }
453
454 #[test]
455 fn test_layout_budget_thrashing_prevention() {
456 use std::sync::atomic::{AtomicUsize, Ordering};
457 use std::sync::Arc;
458
459 struct SpyView {
460 calls: Arc<AtomicUsize>,
461 hash: u64,
462 rect: Rect,
463 }
464
465 impl LayoutView for SpyView {
466 fn size_that_fits(&self, _p: SizeProposal, _s: &[&dyn LayoutView], _c: &mut LayoutCache) -> Size {
467 Size { width: self.rect.width, height: self.rect.height }
468 }
469 fn place_subviews(&self, _b: Rect, _s: &mut [&mut dyn LayoutView], _c: &mut LayoutCache) {
470 self.calls.fetch_add(1, Ordering::SeqCst);
471 }
472 fn view_hash(&self) -> u64 {
473 self.hash
474 }
475 }
476
477 let calls = Arc::new(AtomicUsize::new(0));
478 let view = SpyView {
479 calls: calls.clone(),
480 hash: 2001,
481 rect: Rect::new(0.0, 0.0, 100.0, 100.0),
482 };
483
484 let mut cache = LayoutCache::new();
485 cvkg_core::LayoutCache::set_layout_budget_deadline(Some(
486 std::time::Instant::now() - std::time::Duration::from_millis(50),
487 ));
488
489 cache.previous_rects.insert(2001, Rect::new(10.0, 10.0, 100.0, 100.0));
490
491 let mut v = view;
492 let mut subviews: Vec<&mut dyn LayoutView> = vec![&mut v];
493
494 HStack::new(0.0, Alignment::Center, Distribution::Leading)
495 .place_subviews(Rect::new(0.0, 0.0, 500.0, 500.0), &mut subviews, &mut cache);
496
497 assert_eq!(calls.load(Ordering::SeqCst), 1);
498
499 let engine = TaffyLayoutEngine::get_or_insert_engine(&mut cache);
500 assert!(!engine.node_map.contains_key(&2001));
501
502 cvkg_core::LayoutCache::clear_layout_budget_deadline();
503 }
504
505 #[test]
506 fn test_spatial_index_hit_test() {
507 let mut index = LayoutSpatialIndex::new();
508 let root = Rect { x: 0.0, y: 0.0, width: 1000.0, height: 1000.0 };
509 let entries = vec![
510 LayoutSpatialEntry { hash: 1, rect: Rect { x: 0.0, y: 0.0, width: 100.0, height: 100.0 } },
511 LayoutSpatialEntry { hash: 2, rect: Rect { x: 200.0, y: 200.0, width: 50.0, height: 50.0 } },
512 LayoutSpatialEntry { hash: 3, rect: Rect { x: 500.0, y: 500.0, width: 200.0, height: 200.0 } },
513 ];
514 index.rebuild(root, entries);
515
516 let hits = index.hit_test(50.0, 50.0);
517 assert_eq!(hits.len(), 1);
518 assert_eq!(hits[0].hash, 1);
519
520 let hits = index.hit_test(600.0, 600.0);
521 assert_eq!(hits.len(), 1);
522 assert_eq!(hits[0].hash, 3);
523
524 let hits = index.hit_test(999.0, 1.0);
525 assert!(hits.is_empty(), "Expected no hits, got {:?}", hits.iter().map(|e| e.hash).collect::<Vec<_>>());
526 }
527
528 #[test]
529 fn test_spatial_index_query_region() {
530 let mut index = LayoutSpatialIndex::new();
531 let root = Rect { x: 0.0, y: 0.0, width: 500.0, height: 500.0 };
532 let entries = vec![
533 LayoutSpatialEntry { hash: 10, rect: Rect { x: 0.0, y: 0.0, width: 100.0, height: 100.0 } },
534 LayoutSpatialEntry { hash: 20, rect: Rect { x: 400.0, y: 400.0, width: 50.0, height: 50.0 } },
535 ];
536 index.rebuild(root, entries);
537
538 let region = Rect { x: 0.0, y: 0.0, width: 150.0, height: 150.0 };
539 let results = index.query_region(®ion);
540 assert!(results.iter().any(|e| e.hash == 10));
541 assert!(!results.iter().any(|e| e.hash == 20));
542 }
543
544 #[test]
545 fn test_adaptive_modality_touch_enlarges_small_views() {
546 let small = cvkg_core::Size { width: 20.0, height: 12.0 };
547 let adapted = LayoutModality::Touch.adapt_size(small);
548 assert!(adapted.width >= 44.0, "Width must be at least 44pt for touch");
549 assert!(adapted.height >= 44.0, "Height must be at least 44pt for touch");
550 }
551
552 #[test]
553 fn test_adaptive_modality_pointer_does_not_enlarge() {
554 let large = cvkg_core::Size { width: 200.0, height: 50.0 };
555 let adapted = LayoutModality::Pointer.adapt_size(large);
556 assert_eq!(adapted.width, 200.0);
557 assert_eq!(adapted.height, 50.0);
558 }
559
560 #[test]
561 fn test_adaptive_modality_accessibility_zoom_spacing() {
562 assert!(
563 LayoutModality::AccessibilityZoom.spacing_multiplier() > LayoutModality::Touch.spacing_multiplier(),
564 "Accessibility zoom must have the largest spacing multiplier"
565 );
566 }
567
568 #[test]
569 fn test_focus_order_ltr_visual_sort() {
570 let candidates = vec![
571 FocusCandidate { hash: 100, rect: Rect { x: 200.0, y: 10.0, width: 50.0, height: 20.0 }, tab_index: None },
572 FocusCandidate { hash: 200, rect: Rect { x: 0.0, y: 10.0, width: 50.0, height: 20.0 }, tab_index: None },
573 FocusCandidate { hash: 300, rect: Rect { x: 100.0, y: 10.0, width: 50.0, height: 20.0 }, tab_index: None },
574 ];
575 let order = compute_focus_order(candidates);
576 assert_eq!(order, vec![200, 300, 100], "LTR focus order violated: {:?}", order);
577 }
578
579 #[test]
580 fn test_focus_order_explicit_tabindex_comes_first() {
581 let candidates = vec![
582 FocusCandidate { hash: 1, rect: Rect { x: 0.0, y: 100.0, width: 50.0, height: 20.0 }, tab_index: None },
583 FocusCandidate { hash: 2, rect: Rect { x: 0.0, y: 0.0, width: 50.0, height: 20.0 }, tab_index: Some(2) },
584 FocusCandidate { hash: 3, rect: Rect { x: 0.0, y: 50.0, width: 50.0, height: 20.0 }, tab_index: Some(1) },
585 ];
586 let order = compute_focus_order(candidates);
587 assert_eq!(order[0], 3, "tabindex=1 must be first");
588 assert_eq!(order[1], 2, "tabindex=2 must be second");
589 assert_eq!(order[2], 1, "natural order must be last");
590 }
591
592 #[test]
593 fn test_reading_order_valid_sequence_passes() {
594 let candidates = vec![
595 FocusCandidate { hash: 1, rect: Rect { x: 0.0, y: 0.0, width: 50.0, height: 20.0 }, tab_index: None },
596 FocusCandidate { hash: 2, rect: Rect { x: 100.0, y: 0.0, width: 50.0, height: 20.0 }, tab_index: None },
597 FocusCandidate { hash: 3, rect: Rect { x: 0.0, y: 30.0, width: 50.0, height: 20.0 }, tab_index: None },
598 ];
599 assert!(validate_reading_order(&candidates).is_ok());
600 }
601
602 #[test]
603 fn test_reading_order_backwards_row_fails() {
604 let candidates = vec![
605 FocusCandidate { hash: 1, rect: Rect { x: 0.0, y: 100.0, width: 50.0, height: 20.0 }, tab_index: None },
606 FocusCandidate { hash: 2, rect: Rect { x: 0.0, y: 0.0, width: 50.0, height: 20.0 }, tab_index: None },
607 ];
608 assert!(validate_reading_order(&candidates).is_err(), "Backwards row must fail validation");
609 }
610
611 #[test]
612 fn p2_47_deep_tree_100_levels() {
613 let mut cache = LayoutCache::new();
614 let mut root: Box<dyn LayoutView> = Box::new(HStack::new(
615 0.0,
616 Alignment::Leading,
617 Distribution::Leading,
618 ));
619 for _ in 0..50 {
620 let child: Box<dyn LayoutView> =
621 Box::new(HStack::new(0.0, Alignment::Leading, Distribution::Leading));
622 let _ = child;
623 }
624 let proposal = SizeProposal::unspecified();
625 let _ = root.size_that_fits(proposal, &[], &mut cache);
626 }
627
628 #[test]
629 fn p2_47_wide_tree_no_panic() {
630 let mut cache = LayoutCache::new();
631 let root = HStack::new(0.0, Alignment::Leading, Distribution::Leading);
632 let proposal = SizeProposal::unspecified();
633 let _ = root.size_that_fits(proposal, &[], &mut cache);
634 }
635
636 #[test]
637 fn p2_47_nested_flex_no_panic() {
638 let mut cache = LayoutCache::new();
639 let inner = HStack::new(0.0, Alignment::Leading, Distribution::Leading);
640 let _ = inner.size_that_fits(SizeProposal::unspecified(), &[], &mut cache);
641 }
642
643 fn make_mock_views(n: usize) -> Vec<MockView> {
644 (0..n)
645 .map(|_| MockView {
646 size: Size {
647 width: 50.0,
648 height: 30.0,
649 },
650 flex: 0.0,
651 })
652 .collect()
653 }
654
655 #[test]
656 fn test_progressive_layout_completes_all_children() {
657 let views = make_mock_views(10);
658 let subviews: Vec<&dyn LayoutView> = views.iter().map(|v| v as &dyn LayoutView).collect();
659 let bounds = Rect {
660 x: 0.0,
661 y: 0.0,
662 width: 1000.0,
663 height: 200.0,
664 };
665 let mut ctx = ProgressiveLayoutContext::new(
666 bounds,
667 &subviews,
668 0.0,
669 Alignment::Leading,
670 Distribution::Leading,
671 );
672 assert!(!ctx.is_complete());
673 assert!(!ctx.layout_next_batch(3));
674 assert!(!ctx.is_complete());
675 assert!(!ctx.layout_next_batch(3));
676 assert!(!ctx.is_complete());
677 assert!(!ctx.layout_next_batch(3));
678 assert!(!ctx.is_complete());
679 assert!(ctx.layout_next_batch(3));
680 assert!(ctx.is_complete());
681 }
682
683 #[test]
684 fn test_progressive_layout_reports_progress() {
685 let views = make_mock_views(5);
686 let subviews: Vec<&dyn LayoutView> = views.iter().map(|v| v as &dyn LayoutView).collect();
687 let bounds = Rect {
688 x: 0.0,
689 y: 0.0,
690 width: 500.0,
691 height: 200.0,
692 };
693 let mut ctx = ProgressiveLayoutContext::new(
694 bounds,
695 &subviews,
696 0.0,
697 Alignment::Leading,
698 Distribution::Leading,
699 );
700 assert_eq!(ctx.progress(), (0, 5));
701 ctx.layout_next_batch(2);
702 assert_eq!(ctx.progress(), (2, 5));
703 ctx.layout_next_batch(2);
704 assert_eq!(ctx.progress(), (4, 5));
705 ctx.layout_next_batch(1);
706 assert_eq!(ctx.progress(), (5, 5));
707 }
708
709 #[test]
710 fn test_progressive_layout_fallback_positions_remaining() {
711 let views = make_mock_views(6);
712 let subviews: Vec<&dyn LayoutView> = views.iter().map(|v| v as &dyn LayoutView).collect();
713 let bounds = Rect {
714 x: 0.0,
715 y: 0.0,
716 width: 600.0,
717 height: 200.0,
718 };
719 let mut ctx = ProgressiveLayoutContext::new(
720 bounds,
721 &subviews,
722 10.0,
723 Alignment::Leading,
724 Distribution::Leading,
725 );
726 ctx.layout_next_batch(2);
727 assert_eq!(ctx.progress(), (2, 6));
728 let mut cache = LayoutCache::new();
729 let fallback_rects = ctx.apply_remaining_fallback(&mut cache);
730 assert_eq!(fallback_rects.len(), 4);
731 for r in &fallback_rects {
732 assert!(r.width > 0.0);
733 assert!(r.height > 0.0);
734 }
735 assert!(ctx.is_complete());
736 }
737
738 #[test]
739 fn test_progressive_layout_uses_cached_results() {
740 let views = make_mock_views(4);
741 let subviews: Vec<&dyn LayoutView> = views.iter().map(|v| v as &dyn LayoutView).collect();
742 let bounds = Rect {
743 x: 0.0,
744 y: 0.0,
745 width: 400.0,
746 height: 200.0,
747 };
748 let mut cache = LayoutCache::new();
749 let mut ctx1 = ProgressiveLayoutContext::new(
750 bounds,
751 &subviews,
752 0.0,
753 Alignment::Leading,
754 Distribution::Leading,
755 );
756 ctx1.layout_next_batch(2);
757 for entry in ctx1.entries.iter() {
758 if entry.rect != Rect::zero() {
759 cache.previous_rects.insert(entry.hash, entry.rect);
760 }
761 }
762 let mut ctx2 = ProgressiveLayoutContext::new(
763 bounds,
764 &subviews,
765 0.0,
766 Alignment::Leading,
767 Distribution::Leading,
768 );
769 let (_done, _rects) = ctx2.layout_next_batch_with_cache(2, &mut cache);
770 assert_eq!(ctx2.progress().0, 2);
771 }
772}