1pub use cvkg_core::layout::EdgeInsets;
26use cvkg_core::{Alignment, Distribution, LayoutCache, LayoutView, Rect, Size, SizeProposal};
27use std::collections::HashMap;
28use std::cell::RefCell;
29use std::collections::HashSet;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
34pub struct LayoutCapabilities {
35 pub flexbox: bool,
36 pub grid: bool,
37 pub absolute: bool,
38 pub container_queries: bool,
39}
40
41pub fn layout_capabilities() -> LayoutCapabilities {
43 LayoutCapabilities {
44 flexbox: true,
45 grid: true,
46 absolute: true,
47 container_queries: true,
48 }
49}
50
51thread_local! {
52 static ACTIVE_LAYOUT_NODES: RefCell<HashSet<u64>> = RefCell::new(HashSet::new());
53}
54
55fn with_layout_cycle_guard<F, R>(hash: u64, fallback: R, f: F) -> R
58where
59 F: FnOnce() -> R,
60{
61 if hash == 0 {
62 return f();
63 }
64 let already_active = ACTIVE_LAYOUT_NODES.with(|nodes| !nodes.borrow_mut().insert(hash));
65 if already_active {
66 log::warn!("[Layout] Cycle detected for view hash 0x{:X}! Breaking cycle with fallback size.", hash);
67 return fallback;
68 }
69 let res = f();
70 ACTIVE_LAYOUT_NODES.with(|nodes| {
71 nodes.borrow_mut().remove(&hash);
72 });
73 res
74}
75
76fn with_layout_cycle_guard_void<F>(hash: u64, f: F)
78where
79 F: FnOnce(),
80{
81 if hash == 0 {
82 f();
83 return;
84 }
85 let already_active = ACTIVE_LAYOUT_NODES.with(|nodes| !nodes.borrow_mut().insert(hash));
86 if already_active {
87 log::warn!("[Layout] Cycle detected for view hash 0x{:X}! Breaking cycle placement.", hash);
88 return;
89 }
90 f();
91 ACTIVE_LAYOUT_NODES.with(|nodes| {
92 nodes.borrow_mut().remove(&hash);
93 });
94}
95
96pub struct TaffyLayoutEngine {
99 pub tree: taffy::TaffyTree,
100 pub node_map: HashMap<u64, taffy::NodeId>,
101}
102
103impl Default for TaffyLayoutEngine {
104 fn default() -> Self {
105 Self::new()
106 }
107}
108
109impl TaffyLayoutEngine {
110 pub fn new() -> Self {
111 Self {
112 tree: taffy::TaffyTree::new(),
113 node_map: HashMap::new(),
114 }
115 }
116
117 pub fn get_or_insert_engine(cache: &mut LayoutCache) -> &mut Self {
118 if cache.engine.is_none() {
119 cache.engine = Some(Box::new(TaffyLayoutEngine::new()));
120 }
121 cache
122 .engine
123 .as_mut()
124 .unwrap()
125 .downcast_mut::<TaffyLayoutEngine>()
126 .unwrap()
127 }
128}
129
130pub struct AnimationEngine {
132 pub active_transitions: HashMap<u64, cvkg_anim::physics::ViscousSpring>,
133 pub eviction_generation: u64,
135 pub transition_generation: HashMap<u64, u64>,
137 pub eviction_threshold: u64,
139}
140
141impl Default for AnimationEngine {
142 fn default() -> Self {
143 Self::new()
144 }
145}
146
147impl AnimationEngine {
148 pub fn new() -> Self {
149 Self {
150 active_transitions: HashMap::new(),
151 eviction_generation: 0,
152 transition_generation: HashMap::new(),
153 eviction_threshold: 300,
154 }
155 }
156
157 pub fn get_or_insert_engine(cache: &mut LayoutCache) -> &mut Self {
158 if cache.animators.is_none() {
159 cache.animators = Some(Box::new(AnimationEngine::new()));
160 }
161 cache
162 .animators
163 .as_mut()
164 .unwrap()
165 .downcast_mut::<AnimationEngine>()
166 .unwrap()
167 }
168
169 pub fn evict_stale_transitions(&mut self) {
171 self.eviction_generation += 1;
172 let threshold = self.eviction_threshold;
173 let current_gen = self.eviction_generation;
174 self.active_transitions.retain(|hash, spring| {
175 let recent = self
176 .transition_generation
177 .get(hash)
178 .map_or(false, |g| current_gen - *g < threshold);
179 let unsettled = spring.velocity_a.length_sq() > 0.0001 || spring.velocity_b.length_sq() > 0.0001;
180 recent || unsettled
181 });
182 self.transition_generation
183 .retain(|hash, _| self.active_transitions.contains_key(hash));
184 }
185}
186
187use taffy::prelude::*;
188
189fn taffy_alignment(alignment: cvkg_core::Alignment) -> Option<taffy::AlignItems> {
190 match alignment {
191 cvkg_core::Alignment::Leading => Some(taffy::AlignItems::Start),
192 cvkg_core::Alignment::Center => Some(taffy::AlignItems::Center),
193 cvkg_core::Alignment::Trailing => Some(taffy::AlignItems::End),
194 cvkg_core::Alignment::Top => Some(taffy::AlignItems::Start),
195 cvkg_core::Alignment::Bottom => Some(taffy::AlignItems::End),
196 }
197}
198
199fn taffy_distribution(dist: cvkg_core::Distribution) -> Option<taffy::JustifyContent> {
200 match dist {
201 cvkg_core::Distribution::Leading => Some(taffy::JustifyContent::Start),
202 cvkg_core::Distribution::Center => Some(taffy::JustifyContent::Center),
203 cvkg_core::Distribution::Trailing => Some(taffy::JustifyContent::End),
204 cvkg_core::Distribution::SpaceBetween => Some(taffy::JustifyContent::SpaceBetween),
205 cvkg_core::Distribution::Fill => Some(taffy::JustifyContent::Stretch),
206 _ => None,
207 }
208}
209
210#[derive(Clone, Copy)]
212struct FlexParams {
213 dir: taffy::FlexDirection,
214 spacing: f32,
215 alignment: cvkg_core::Alignment,
216 distribution: cvkg_core::Distribution,
217 bounds: Rect,
218 container_hash: u64,
219}
220
221fn collect_child_sizes(
225 subviews: &[&dyn LayoutView],
226 bounds: Rect,
227 cache: &mut LayoutCache,
228) -> (Vec<u64>, Vec<f32>, Vec<Size>) {
229 let mut sizes = Vec::with_capacity(subviews.len());
230 let mut hashes = Vec::with_capacity(subviews.len());
231 let mut flex_weights = Vec::with_capacity(subviews.len());
232
233 for child in subviews {
234 let hash = child.view_hash();
235 hashes.push(hash);
236 flex_weights.push(child.flex_weight());
237
238 let proposal = SizeProposal::new(Some(bounds.width), Some(bounds.height));
239 let cached_size = if hash != 0 {
240 cache.get_size(hash, proposal)
241 } else {
242 None
243 };
244
245 let size = match cached_size {
246 Some(sz) => sz,
247 None => {
248 let sz = with_layout_cycle_guard(hash, Size::ZERO, || {
249 child.size_that_fits(proposal, &[], cache)
250 });
251 if hash != 0 {
252 cache.set_size(hash, proposal, sz);
253 }
254 sz
255 }
256 };
257 if hash != 0 {
258 cache.register_parent(hash, 0); }
260 sizes.push(size);
261 }
262
263 (hashes, flex_weights, sizes)
264}
265
266fn intrinsic_flex_size(dir: taffy::FlexDirection, spacing: f32, sizes: &[Size]) -> Size {
269 if sizes.is_empty() {
270 return Size::ZERO;
271 }
272 let n = sizes.len();
273 match dir {
274 taffy::FlexDirection::Row | taffy::FlexDirection::RowReverse => {
275 let total_width: f32 = sizes.iter().map(|s| s.width).sum();
276 let max_height: f32 = sizes.iter().map(|s| s.height).fold(0.0, f32::max);
277 Size {
278 width: total_width + spacing * (n.saturating_sub(1) as f32),
279 height: max_height,
280 }
281 }
282 taffy::FlexDirection::Column | taffy::FlexDirection::ColumnReverse => {
283 let max_width: f32 = sizes.iter().map(|s| s.width).fold(0.0, f32::max);
284 let total_height: f32 = sizes.iter().map(|s| s.height).sum();
285 Size {
286 width: max_width,
287 height: total_height + spacing * (n.saturating_sub(1) as f32),
288 }
289 }
290 }
291}
292
293fn compute_taffy_flex(
294 params: &FlexParams,
295 subviews: &[&dyn LayoutView],
296 cache: &mut LayoutCache,
297) -> Vec<Rect> {
298 if cache.is_over_budget() {
299 let mut rects = Vec::with_capacity(subviews.len());
300 for child in subviews {
301 let hash = child.view_hash();
302 let r = if hash != 0 {
303 cache.previous_rects.get(&hash).copied().unwrap_or(Rect::zero())
304 } else {
305 Rect::zero()
306 };
307 rects.push(r);
308 }
309 return rects;
310 }
311
312 let (hashes, flex_weights, sizes) = collect_child_sizes(subviews, params.bounds, cache);
314
315 for &hash in &hashes {
317 if hash != 0 && params.container_hash != 0 {
318 cache.register_parent(hash, params.container_hash);
319 }
320 }
321
322 let engine = TaffyLayoutEngine::get_or_insert_engine(cache);
323 let mut child_nodes = Vec::with_capacity(subviews.len());
324
325 for ((&hash, &flex_weight), &size) in hashes.iter().zip(&flex_weights).zip(&sizes) {
326 let style = if flex_weight > 0.0 {
327 taffy::Style {
328 size: taffy::Size {
329 width: if params.dir == taffy::FlexDirection::Row {
330 taffy::Dimension::Auto
331 } else {
332 taffy::Dimension::Length(size.width)
333 },
334 height: if params.dir == taffy::FlexDirection::Column {
335 taffy::Dimension::Auto
336 } else {
337 taffy::Dimension::Length(size.height)
338 },
339 },
340 flex_grow: flex_weight,
341 flex_basis: taffy::Dimension::Percent(0.0),
342 ..Default::default()
343 }
344 } else {
345 taffy::Style {
346 size: taffy::Size {
347 width: taffy::Dimension::Length(size.width),
348 height: taffy::Dimension::Length(size.height),
349 },
350 ..Default::default()
351 }
352 };
353
354 let node = if hash != 0 {
355 if let Some(&existing) = engine.node_map.get(&hash) {
356 let _ = engine.tree.set_style(existing, style);
357 existing
358 } else {
359 let new_node = engine.tree.new_leaf(style).unwrap();
360 engine.node_map.insert(hash, new_node);
361 new_node
362 }
363 } else {
364 engine.tree.new_leaf(style).unwrap()
365 };
366 child_nodes.push(node);
367 }
368
369 let gap_val = taffy::LengthPercentage::Length(params.spacing);
370 let container_style = taffy::Style {
371 display: taffy::Display::Flex,
372 flex_direction: params.dir,
373 gap: taffy::Size {
374 width: if params.dir == taffy::FlexDirection::Row {
375 gap_val
376 } else {
377 taffy::LengthPercentage::Length(0.0)
378 },
379 height: if params.dir == taffy::FlexDirection::Column {
380 gap_val
381 } else {
382 taffy::LengthPercentage::Length(0.0)
383 },
384 },
385 align_items: taffy_alignment(params.alignment),
386 justify_content: taffy_distribution(params.distribution),
387 size: taffy::Size {
388 width: taffy::Dimension::Length(params.bounds.width),
389 height: taffy::Dimension::Length(params.bounds.height),
390 },
391 ..Default::default()
392 };
393
394 let root_node = if params.container_hash != 0 {
395 if let Some(&existing) = engine.node_map.get(¶ms.container_hash) {
396 let _ = engine.tree.set_style(existing, container_style);
397 let _ = engine.tree.set_children(existing, &child_nodes);
398 existing
399 } else {
400 let new_node = engine
401 .tree
402 .new_with_children(container_style, &child_nodes)
403 .unwrap();
404 engine.node_map.insert(params.container_hash, new_node);
405 new_node
406 }
407 } else {
408 engine
409 .tree
410 .new_with_children(container_style, &child_nodes)
411 .unwrap()
412 };
413
414 engine
415 .tree
416 .compute_layout(root_node, taffy::Size::MAX_CONTENT)
417 .unwrap();
418
419 let mut rects = Vec::with_capacity(subviews.len());
420 for &node in &child_nodes {
421 let layout = engine.tree.layout(node).unwrap();
422 rects.push(Rect {
423 x: params.bounds.x + layout.location.x,
424 y: params.bounds.y + layout.location.y,
425 width: layout.size.width,
426 height: layout.size.height,
427 });
428 }
429
430 if params.container_hash == 0 {
431 let _ = engine.tree.remove(root_node);
432 }
433
434 rects
435}
436
437fn apply_layout_animations(
439 rects: Vec<Rect>,
440 subviews: &mut [&mut dyn LayoutView],
441 cache: &mut LayoutCache,
442) {
443 let mut transitions_to_update = Vec::new();
444
445 for (child, target_rect) in subviews.iter().zip(&rects) {
446 let hash = child.view_hash();
447 if hash != 0 {
448 if let Some(prev) = cache.previous_rects.get(&hash) {
449 let dx = (prev.x - target_rect.x).abs();
450 let dy = (prev.y - target_rect.y).abs();
451 let dw = (prev.width - target_rect.width).abs();
452 let dh = (prev.height - target_rect.height).abs();
453 let epsilon = 1e-3;
454 if dx > epsilon || dy > epsilon || dw > epsilon || dh > epsilon {
455 transitions_to_update.push((hash, *prev, *target_rect));
456 }
457 }
458 cache.previous_rects.insert(hash, *target_rect);
459 cache.previous_rects_generation.insert(hash, cache.eviction_generation);
460 }
461 }
462
463 let mut interpolated_rects = HashMap::new();
464 let delta = cache.delta_time;
465 let scale = cache.scale_factor;
466 let anim_engine = AnimationEngine::get_or_insert_engine(cache);
467
468 for (hash, prev, target_rect) in transitions_to_update {
469 let mut spring = if let Some(mut existing) = anim_engine.active_transitions.remove(&hash) {
470 existing.position_b =
471 cvkg_anim::physics::Vec3::new(target_rect.x, target_rect.y, target_rect.width);
472 existing
473 } else {
474 cvkg_anim::physics::ViscousSpring::new(
475 cvkg_anim::physics::Vec3::new(prev.x, prev.y, prev.width),
476 cvkg_anim::physics::Vec3::new(target_rect.x, target_rect.y, target_rect.width),
477 0.9,
478 1000.0,
479 )
480 };
481 spring.step(delta);
482
483 let speed = (spring.velocity_a.length_sq() + spring.velocity_b.length_sq()).sqrt();
486 let snap = |v: f32| (v * scale).round() / scale;
487
488 let (rx, ry, rw) = if speed < 0.05 {
489 (
490 snap(spring.position_a.x),
491 snap(spring.position_a.y),
492 snap(spring.position_a.z),
493 )
494 } else {
495 (
496 spring.position_a.x,
497 spring.position_a.y,
498 spring.position_a.z,
499 )
500 };
501
502 interpolated_rects.insert(
503 hash,
504 Rect {
505 x: rx,
506 y: ry,
507 width: rw,
508 height: target_rect.height,
509 },
510 );
511 anim_engine.active_transitions.insert(hash, spring);
512 anim_engine.transition_generation.insert(hash, anim_engine.eviction_generation);
513 }
514 cache.evict_stale_entries();
519
520 let anim_engine = AnimationEngine::get_or_insert_engine(cache);
522 anim_engine.evict_stale_transitions();
523
524 for (child, mut target_rect) in subviews.iter_mut().zip(rects) {
525 let hash = child.view_hash();
526 if let Some(interp) = interpolated_rects.get(&hash) {
527 target_rect = *interp;
528 }
529 let is_visible = if let Some(viewport) = cache.viewport {
530 target_rect.intersects(&viewport)
531 } else {
532 true
533 };
534 if is_visible {
535 with_layout_cycle_guard_void(hash, || {
536 child.place_subviews(target_rect, &mut [], cache);
537 });
538 }
539 }
540}
541
542pub struct HStack {
544 spacing: f32,
545 alignment: Alignment,
546 distribution: Distribution,
547}
548
549impl HStack {
550 pub fn new(spacing: f32, alignment: Alignment, distribution: Distribution) -> Self {
552 Self {
553 spacing,
554 alignment,
555 distribution,
556 }
557 }
558
559 pub fn compute_layout(
561 spacing: f32,
562 alignment: Alignment,
563 distribution: Distribution,
564 bounds: Rect,
565 subviews: &[&dyn LayoutView],
566 cache: &mut LayoutCache,
567 ) -> Vec<Rect> {
568 Self::compute_layout_incremental(
569 spacing,
570 alignment,
571 distribution,
572 bounds,
573 0,
574 subviews,
575 cache,
576 )
577 }
578
579 pub fn compute_layout_incremental(
580 spacing: f32,
581 alignment: Alignment,
582 distribution: Distribution,
583 bounds: Rect,
584 container_hash: u64,
585 subviews: &[&dyn LayoutView],
586 cache: &mut LayoutCache,
587 ) -> Vec<Rect> {
588 compute_taffy_flex(
589 &FlexParams {
590 dir: taffy::FlexDirection::Row,
591 spacing,
592 alignment,
593 distribution,
594 bounds,
595 container_hash,
596 },
597 subviews,
598 cache,
599 )
600 }
601}
602
603impl LayoutView for HStack {
604 fn size_that_fits(
605 &self,
606 proposal: SizeProposal,
607 subviews: &[&dyn LayoutView],
608 cache: &mut LayoutCache,
609 ) -> Size {
610 let bounds = Rect {
611 x: 0.0,
612 y: 0.0,
613 width: proposal.width.unwrap_or(10000.0),
614 height: proposal.height.unwrap_or(10000.0),
615 };
616 let (_, _, sizes) = collect_child_sizes(subviews, bounds, cache);
619 intrinsic_flex_size(taffy::FlexDirection::Row, self.spacing, &sizes)
620 }
621
622 fn place_subviews(
623 &self,
624 bounds: Rect,
625 subviews: &mut [&mut dyn LayoutView],
626 cache: &mut LayoutCache,
627 ) {
628 let views: Vec<&dyn LayoutView> =
629 subviews.iter().map(|v| &**v as &dyn LayoutView).collect();
630 let rects = Self::compute_layout_incremental(
631 self.spacing,
632 self.alignment,
633 self.distribution,
634 bounds,
635 self.view_hash(),
636 &views,
637 cache,
638 );
639 apply_layout_animations(rects, subviews, cache);
640 }
641}
642
643pub struct VStack {
645 spacing: f32,
646 alignment: Alignment,
647 distribution: Distribution,
648}
649
650impl VStack {
651 pub fn new(spacing: f32, alignment: Alignment, distribution: Distribution) -> Self {
653 Self {
654 spacing,
655 alignment,
656 distribution,
657 }
658 }
659
660 pub fn compute_layout(
662 spacing: f32,
663 alignment: Alignment,
664 distribution: Distribution,
665 bounds: Rect,
666 subviews: &[&dyn LayoutView],
667 cache: &mut LayoutCache,
668 ) -> Vec<Rect> {
669 Self::compute_layout_incremental(
670 spacing,
671 alignment,
672 distribution,
673 bounds,
674 0,
675 subviews,
676 cache,
677 )
678 }
679
680 pub fn compute_layout_incremental(
681 spacing: f32,
682 alignment: Alignment,
683 distribution: Distribution,
684 bounds: Rect,
685 container_hash: u64,
686 subviews: &[&dyn LayoutView],
687 cache: &mut LayoutCache,
688 ) -> Vec<Rect> {
689 compute_taffy_flex(
690 &FlexParams {
691 dir: taffy::FlexDirection::Column,
692 spacing,
693 alignment,
694 distribution,
695 bounds,
696 container_hash,
697 },
698 subviews,
699 cache,
700 )
701 }
702}
703
704impl LayoutView for VStack {
705 fn size_that_fits(
706 &self,
707 proposal: SizeProposal,
708 subviews: &[&dyn LayoutView],
709 cache: &mut LayoutCache,
710 ) -> Size {
711 let bounds = Rect {
712 x: 0.0,
713 y: 0.0,
714 width: proposal.width.unwrap_or(10000.0),
715 height: proposal.height.unwrap_or(10000.0),
716 };
717 let (_, _, sizes) = collect_child_sizes(subviews, bounds, cache);
719 intrinsic_flex_size(taffy::FlexDirection::Column, self.spacing, &sizes)
720 }
721
722 fn place_subviews(
723 &self,
724 bounds: Rect,
725 subviews: &mut [&mut dyn LayoutView],
726 cache: &mut LayoutCache,
727 ) {
728 let views: Vec<&dyn LayoutView> =
729 subviews.iter().map(|v| &**v as &dyn LayoutView).collect();
730 let rects = Self::compute_layout_incremental(
731 self.spacing,
732 self.alignment,
733 self.distribution,
734 bounds,
735 self.view_hash(),
736 &views,
737 cache,
738 );
739 apply_layout_animations(rects, subviews, cache);
740 }
741}
742
743pub struct ZStack {}
745
746impl Default for ZStack {
747 fn default() -> Self {
748 Self::new()
749 }
750}
751
752impl ZStack {
753 pub fn new() -> Self {
755 Self {}
756 }
757}
758
759impl LayoutView for ZStack {
760 fn size_that_fits(
761 &self,
762 proposal: SizeProposal,
763 subviews: &[&dyn LayoutView],
764 cache: &mut LayoutCache,
765 ) -> Size {
766 let mut width = 0.0f32;
767 let mut height = 0.0f32;
768 let self_hash = self.view_hash();
769
770 for child in subviews.iter() {
771 let child_hash = child.view_hash();
772 if self_hash != 0 && child_hash != 0 {
773 cache.register_parent(child_hash, self_hash);
774 }
775 let child_size = with_layout_cycle_guard(child_hash, Size::ZERO, || {
776 child.size_that_fits(proposal, &[], cache)
777 });
778 width = width.max(child_size.width);
779 height = height.max(child_size.height);
780 }
781
782 Size { width, height }
783 }
784
785 fn place_subviews(
786 &self,
787 bounds: Rect,
788 subviews: &mut [&mut dyn LayoutView],
789 cache: &mut LayoutCache,
790 ) {
791 let self_hash = self.view_hash();
792 for child in subviews.iter_mut() {
793 let child_hash = child.view_hash();
794 if self_hash != 0 && child_hash != 0 {
795 cache.register_parent(child_hash, self_hash);
796 }
797 let is_visible = if let Some(viewport) = cache.viewport {
798 bounds.intersects(&viewport)
799 } else {
800 true
801 };
802 if is_visible {
803 with_layout_cycle_guard_void(child_hash, || {
804 child.place_subviews(bounds, &mut [], cache);
805 });
806 }
807 }
808 }
809}
810
811pub struct Spacer;
813
814impl LayoutView for Spacer {
815 fn size_that_fits(
816 &self,
817 proposal: SizeProposal,
818 _subviews: &[&dyn LayoutView],
819 _cache: &mut LayoutCache,
820 ) -> Size {
821 Size {
822 width: proposal.width.unwrap_or(0.0),
823 height: proposal.height.unwrap_or(0.0),
824 }
825 }
826
827 fn place_subviews(
828 &self,
829 _bounds: Rect,
830 _subviews: &mut [&mut dyn LayoutView],
831 _cache: &mut LayoutCache,
832 ) {
833 }
834}
835
836pub struct Flex {
838 pub orientation: cvkg_core::Orientation,
839 pub spacing: f32,
840}
841
842impl Flex {
843 pub fn new(orientation: cvkg_core::Orientation, spacing: f32) -> Self {
844 Self {
845 orientation,
846 spacing,
847 }
848 }
849}
850
851impl LayoutView for Flex {
852 fn size_that_fits(
853 &self,
854 proposal: SizeProposal,
855 _subviews: &[&dyn LayoutView],
856 _cache: &mut LayoutCache,
857 ) -> Size {
858 Size {
859 width: proposal.width.unwrap_or(100.0),
860 height: proposal.height.unwrap_or(100.0),
861 }
862 }
863
864 fn place_subviews(
865 &self,
866 bounds: Rect,
867 subviews: &mut [&mut dyn LayoutView],
868 cache: &mut LayoutCache,
869 ) {
870 if subviews.is_empty() {
871 return;
872 }
873
874 let self_hash = self.view_hash();
875 let n = subviews.len() as f32;
876 match self.orientation {
877 cvkg_core::Orientation::Horizontal => {
878 let total_spacing = self.spacing * (n - 1.0);
879 let item_width = (bounds.width - total_spacing) / n;
880 for (i, child) in subviews.iter_mut().enumerate() {
881 let child_rect = Rect {
882 x: bounds.x + i as f32 * (item_width + self.spacing),
883 y: bounds.y,
884 width: item_width,
885 height: bounds.height,
886 };
887 let child_hash = child.view_hash();
888 if self_hash != 0 && child_hash != 0 {
889 cache.register_parent(child_hash, self_hash);
890 }
891 let is_visible = if let Some(viewport) = cache.viewport {
892 child_rect.intersects(&viewport)
893 } else {
894 true
895 };
896 if is_visible {
897 with_layout_cycle_guard_void(child_hash, || {
898 child.place_subviews(child_rect, &mut [], cache);
899 });
900 }
901 }
902 }
903 cvkg_core::Orientation::Vertical => {
904 let total_spacing = self.spacing * (n - 1.0);
905 let item_height = (bounds.height - total_spacing) / n;
906 for (i, child) in subviews.iter_mut().enumerate() {
907 let child_rect = Rect {
908 x: bounds.x,
909 y: bounds.y + i as f32 * (item_height + self.spacing),
910 width: bounds.width,
911 height: item_height,
912 };
913 let child_hash = child.view_hash();
914 if self_hash != 0 && child_hash != 0 {
915 cache.register_parent(child_hash, self_hash);
916 }
917 let is_visible = if let Some(viewport) = cache.viewport {
918 child_rect.intersects(&viewport)
919 } else {
920 true
921 };
922 if is_visible {
923 with_layout_cycle_guard_void(child_hash, || {
924 child.place_subviews(child_rect, &mut [], cache);
925 });
926 }
927 }
928 }
929 }
930 }
931}
932
933#[derive(Debug, Clone, Copy, PartialEq)]
935pub enum GridTrack {
936 Fixed(f32),
938 Flex(f32),
940 Auto,
942 MinMax(f32, f32),
944}
945
946fn taffy_track(track: GridTrack) -> taffy::TrackSizingFunction {
947 match track {
948 GridTrack::Fixed(v) => taffy::prelude::length(v),
949 GridTrack::Flex(v) => taffy::prelude::fr(v),
950 GridTrack::Auto => taffy::prelude::auto(),
951 GridTrack::MinMax(min, max) => {
952 taffy::prelude::minmax(taffy::prelude::length(min), taffy::prelude::length(max))
953 }
954 }
955}
956
957pub struct Grid {
959 pub columns: Vec<GridTrack>,
961 pub rows: Vec<GridTrack>,
963 pub column_gap: f32,
965 pub row_gap: f32,
967}
968
969impl Grid {
970 pub fn new(
972 columns: Vec<GridTrack>,
973 rows: Vec<GridTrack>,
974 column_gap: f32,
975 row_gap: f32,
976 ) -> Self {
977 Self {
978 columns,
979 rows,
980 column_gap,
981 row_gap,
982 }
983 }
984
985 pub fn compute_layout_rects(
987 &self,
988 bounds: Rect,
989 subviews: &[&dyn LayoutView],
990 placements: &[Option<cvkg_core::GridPlacement>],
991 cache: &mut LayoutCache,
992 ) -> Vec<Rect> {
993 self.compute_layout_rects_incremental(bounds, 0, subviews, placements, cache)
994 }
995
996 pub fn compute_layout_rects_incremental(
997 &self,
998 bounds: Rect,
999 container_hash: u64,
1000 subviews: &[&dyn LayoutView],
1001 placements: &[Option<cvkg_core::GridPlacement>],
1002 cache: &mut LayoutCache,
1003 ) -> Vec<Rect> {
1004 if cache.is_over_budget() {
1005 let mut rects = Vec::with_capacity(subviews.len());
1006 for child in subviews {
1007 let hash = child.view_hash();
1008 let r = if hash != 0 {
1009 cache.previous_rects.get(&hash).copied().unwrap_or(Rect::zero())
1010 } else {
1011 Rect::zero()
1012 };
1013 rects.push(r);
1014 }
1015 return rects;
1016 }
1017
1018 let mut hashes = Vec::with_capacity(subviews.len());
1019 for child in subviews {
1020 let hash = child.view_hash();
1021 hashes.push(hash);
1022 if container_hash != 0 && hash != 0 {
1023 cache.register_parent(hash, container_hash);
1024 }
1025 }
1026
1027 let engine = TaffyLayoutEngine::get_or_insert_engine(cache);
1028 let mut child_nodes = Vec::with_capacity(subviews.len());
1029
1030 for (hash, placement) in hashes.iter().zip(placements.iter()) {
1031 let style = if let Some(p) = placement.as_ref() {
1032 taffy::Style {
1033 size: taffy::Size {
1034 width: taffy::Dimension::Auto,
1035 height: taffy::Dimension::Auto,
1036 },
1037 grid_column: taffy::Line {
1038 start: taffy::prelude::line((p.column + 1) as i16),
1039 end: taffy::prelude::span(p.column_span as u16),
1040 },
1041 grid_row: taffy::Line {
1042 start: taffy::prelude::line((p.row + 1) as i16),
1043 end: taffy::prelude::span(p.row_span as u16),
1044 },
1045 ..Default::default()
1046 }
1047 } else {
1048 taffy::Style {
1049 size: taffy::Size {
1050 width: taffy::Dimension::Auto,
1051 height: taffy::Dimension::Auto,
1052 },
1053 ..Default::default()
1054 }
1055 };
1056
1057 let node = if *hash != 0 {
1058 if let Some(&existing) = engine.node_map.get(hash) {
1059 let _ = engine.tree.set_style(existing, style);
1060 existing
1061 } else {
1062 let new_node = engine.tree.new_leaf(style).unwrap();
1063 engine.node_map.insert(*hash, new_node);
1064 new_node
1065 }
1066 } else {
1067 engine.tree.new_leaf(style).unwrap()
1068 };
1069 child_nodes.push(node);
1070 }
1071
1072 let container_style = taffy::Style {
1073 display: taffy::Display::Grid,
1074 grid_template_columns: self.columns.iter().copied().map(taffy_track).collect(),
1075 grid_template_rows: self.rows.iter().copied().map(taffy_track).collect(),
1076 gap: taffy::Size {
1077 width: taffy::LengthPercentage::Length(self.column_gap),
1078 height: taffy::LengthPercentage::Length(self.row_gap),
1079 },
1080 size: taffy::Size {
1081 width: taffy::Dimension::Length(bounds.width),
1082 height: taffy::Dimension::Length(bounds.height),
1083 },
1084 ..Default::default()
1085 };
1086
1087 let root_node = if container_hash != 0 {
1088 if let Some(&existing) = engine.node_map.get(&container_hash) {
1089 let _ = engine.tree.set_style(existing, container_style);
1090 let _ = engine.tree.set_children(existing, &child_nodes);
1091 existing
1092 } else {
1093 let new_node = engine
1094 .tree
1095 .new_with_children(container_style, &child_nodes)
1096 .unwrap();
1097 engine.node_map.insert(container_hash, new_node);
1098 new_node
1099 }
1100 } else {
1101 engine
1102 .tree
1103 .new_with_children(container_style, &child_nodes)
1104 .unwrap()
1105 };
1106
1107 engine
1108 .tree
1109 .compute_layout(root_node, taffy::Size::MAX_CONTENT)
1110 .unwrap();
1111
1112 let mut rects = Vec::with_capacity(subviews.len());
1113 for &node in &child_nodes {
1114 let layout = engine.tree.layout(node).unwrap();
1115 rects.push(Rect {
1116 x: bounds.x + layout.location.x,
1117 y: bounds.y + layout.location.y,
1118 width: layout.size.width,
1119 height: layout.size.height,
1120 });
1121 }
1122
1123 if container_hash == 0 {
1124 let _ = engine.tree.remove(root_node);
1125 }
1126 rects
1127 }
1128}
1129
1130impl LayoutView for Grid {
1131 fn size_that_fits(
1132 &self,
1133 proposal: SizeProposal,
1134 _subviews: &[&dyn LayoutView],
1135 _cache: &mut LayoutCache,
1136 ) -> Size {
1137 Size {
1138 width: proposal.width.unwrap_or(200.0),
1139 height: proposal.height.unwrap_or(200.0),
1140 }
1141 }
1142
1143 fn place_subviews(
1144 &self,
1145 bounds: Rect,
1146 subviews: &mut [&mut dyn LayoutView],
1147 cache: &mut LayoutCache,
1148 ) {
1149 let views: Vec<&dyn LayoutView> =
1150 subviews.iter().map(|v| &**v as &dyn LayoutView).collect();
1151 let placements = vec![None; subviews.len()];
1152 let rects = self.compute_layout_rects_incremental(
1153 bounds,
1154 self.view_hash(),
1155 &views,
1156 &placements,
1157 cache,
1158 );
1159 apply_layout_animations(rects, subviews, cache);
1160 }
1161}
1162
1163pub struct Padding {
1169 pub insets: EdgeInsets,
1170}
1171
1172impl Padding {
1173 pub fn new(insets: EdgeInsets) -> Self {
1174 Self { insets }
1175 }
1176
1177 pub fn uniform(value: f32) -> Self {
1178 Self {
1179 insets: EdgeInsets::all(value),
1180 }
1181 }
1182
1183 pub fn symmetric(horizontal: f32, vertical: f32) -> Self {
1184 Self {
1185 insets: EdgeInsets {
1186 top: vertical,
1187 bottom: vertical,
1188 leading: horizontal,
1189 trailing: horizontal,
1190 },
1191 }
1192 }
1193}
1194
1195impl LayoutView for Padding {
1196 fn size_that_fits(
1197 &self,
1198 proposal: SizeProposal,
1199 subviews: &[&dyn LayoutView],
1200 cache: &mut LayoutCache,
1201 ) -> Size {
1202 let inner_proposal = SizeProposal::new(
1203 proposal
1204 .width
1205 .map(|w| (w - self.insets.leading - self.insets.trailing).max(0.0)),
1206 proposal
1207 .height
1208 .map(|h| (h - self.insets.top - self.insets.bottom).max(0.0)),
1209 );
1210 let self_hash = self.view_hash();
1211 let child_size = if subviews.is_empty() {
1212 Size::ZERO
1213 } else {
1214 let child_hash = subviews[0].view_hash();
1215 if self_hash != 0 && child_hash != 0 {
1216 cache.register_parent(child_hash, self_hash);
1217 }
1218 with_layout_cycle_guard(child_hash, Size::ZERO, || {
1219 subviews[0].size_that_fits(inner_proposal, &[], cache)
1220 })
1221 };
1222 Size {
1223 width: child_size.width + self.insets.leading + self.insets.trailing,
1224 height: child_size.height + self.insets.top + self.insets.bottom,
1225 }
1226 }
1227
1228 fn place_subviews(
1229 &self,
1230 bounds: Rect,
1231 subviews: &mut [&mut dyn LayoutView],
1232 cache: &mut LayoutCache,
1233 ) {
1234 let inner = Rect {
1235 x: bounds.x + self.insets.leading,
1236 y: bounds.y + self.insets.top,
1237 width: (bounds.width - self.insets.leading - self.insets.trailing).max(0.0),
1238 height: (bounds.height - self.insets.top - self.insets.bottom).max(0.0),
1239 };
1240 let self_hash = self.view_hash();
1241 for child in subviews.iter_mut() {
1242 let child_hash = child.view_hash();
1243 if self_hash != 0 && child_hash != 0 {
1244 cache.register_parent(child_hash, self_hash);
1245 }
1246 let is_visible = if let Some(viewport) = cache.viewport {
1247 inner.intersects(&viewport)
1248 } else {
1249 true
1250 };
1251 if is_visible {
1252 with_layout_cycle_guard_void(child_hash, || {
1253 child.place_subviews(inner, &mut [], cache);
1254 });
1255 }
1256 }
1257 }
1258}
1259
1260pub struct SafeArea {
1266 pub edges: SafeAreaEdges,
1267}
1268
1269#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1270pub struct SafeAreaEdges {
1271 pub top: bool,
1272 pub bottom: bool,
1273 pub leading: bool,
1274 pub trailing: bool,
1275}
1276
1277impl Default for SafeAreaEdges {
1278 fn default() -> Self {
1279 Self {
1280 top: true,
1281 bottom: true,
1282 leading: false,
1283 trailing: false,
1284 }
1285 }
1286}
1287
1288impl SafeArea {
1289 pub fn all() -> Self {
1290 Self {
1291 edges: SafeAreaEdges {
1292 top: true,
1293 bottom: true,
1294 leading: true,
1295 trailing: true,
1296 },
1297 }
1298 }
1299
1300 pub fn vertical() -> Self {
1301 Self {
1302 edges: SafeAreaEdges::default(),
1303 }
1304 }
1305
1306 fn insets(&self) -> EdgeInsets {
1307 EdgeInsets {
1308 top: if self.edges.top { 44.0 } else { 0.0 },
1309 bottom: if self.edges.bottom { 34.0 } else { 0.0 },
1310 leading: 0.0,
1311 trailing: 0.0,
1312 }
1313 }
1314}
1315
1316impl LayoutView for SafeArea {
1317 fn size_that_fits(
1318 &self,
1319 proposal: SizeProposal,
1320 subviews: &[&dyn LayoutView],
1321 cache: &mut LayoutCache,
1322 ) -> Size {
1323 Padding::new(self.insets()).size_that_fits(proposal, subviews, cache)
1324 }
1325
1326 fn place_subviews(
1327 &self,
1328 bounds: Rect,
1329 subviews: &mut [&mut dyn LayoutView],
1330 cache: &mut LayoutCache,
1331 ) {
1332 Padding::new(self.insets()).place_subviews(bounds, subviews, cache);
1333 }
1334}
1335
1336pub struct AspectRatio {
1342 pub ratio: f32,
1343}
1344
1345impl AspectRatio {
1346 pub fn new(ratio: f32) -> Self {
1347 Self {
1348 ratio: ratio.max(0.01),
1349 }
1350 }
1351
1352 pub fn square() -> Self {
1353 Self::new(1.0)
1354 }
1355
1356 pub fn widescreen() -> Self {
1357 Self::new(16.0 / 9.0)
1358 }
1359
1360 pub fn portrait() -> Self {
1361 Self::new(9.0 / 16.0)
1362 }
1363
1364 fn fitted_size(&self, proposal: SizeProposal) -> Size {
1365 let max_w = proposal.width.unwrap_or(f32::MAX);
1366 let max_h = proposal.height.unwrap_or(f32::MAX);
1367 let w = max_w;
1368 let h = w / self.ratio;
1369 if h <= max_h {
1370 return Size {
1371 width: w,
1372 height: h,
1373 };
1374 }
1375 Size {
1376 width: max_h * self.ratio,
1377 height: max_h,
1378 }
1379 }
1380}
1381
1382impl LayoutView for AspectRatio {
1383 fn size_that_fits(
1384 &self,
1385 proposal: SizeProposal,
1386 subviews: &[&dyn LayoutView],
1387 cache: &mut LayoutCache,
1388 ) -> Size {
1389 if subviews.is_empty() {
1390 return self.fitted_size(proposal);
1391 }
1392 let self_hash = self.view_hash();
1393 let child = subviews[0];
1394 let child_hash = child.view_hash();
1395 if self_hash != 0 && child_hash != 0 {
1396 cache.register_parent(child_hash, self_hash);
1397 }
1398 let child_size = with_layout_cycle_guard(child_hash, Size::ZERO, || {
1399 child.size_that_fits(
1400 SizeProposal::new(Some(f32::MAX), Some(f32::MAX)),
1401 &[],
1402 cache,
1403 )
1404 });
1405 let intrinsic_ratio = child_size.width / child_size.height.max(0.01);
1406 if (intrinsic_ratio - self.ratio).abs() < 0.01 {
1407 return self.fitted_size(proposal);
1408 }
1409 let fit = self.fitted_size(proposal);
1410 let child_w = fit.width.min(child_size.width);
1411 let child_h = child_w / intrinsic_ratio;
1412 let final_h = child_h.min(fit.height);
1413 let final_w = final_h * intrinsic_ratio;
1414 Size {
1415 width: final_w,
1416 height: final_h,
1417 }
1418 }
1419
1420 fn place_subviews(
1421 &self,
1422 bounds: Rect,
1423 subviews: &mut [&mut dyn LayoutView],
1424 cache: &mut LayoutCache,
1425 ) {
1426 let fit = self.fitted_size(SizeProposal::new(Some(bounds.width), Some(bounds.height)));
1427 let x = bounds.x + (bounds.width - fit.width) * 0.5;
1428 let y = bounds.y + (bounds.height - fit.height) * 0.0;
1429 let inner = Rect {
1430 x,
1431 y,
1432 width: fit.width,
1433 height: fit.height,
1434 };
1435 let self_hash = self.view_hash();
1436 for child in subviews.iter_mut() {
1437 let child_hash = child.view_hash();
1438 if self_hash != 0 && child_hash != 0 {
1439 cache.register_parent(child_hash, self_hash);
1440 }
1441 let is_visible = if let Some(viewport) = cache.viewport {
1442 inner.intersects(&viewport)
1443 } else {
1444 true
1445 };
1446 if is_visible {
1447 with_layout_cycle_guard_void(child_hash, || {
1448 child.place_subviews(inner, &mut [], cache);
1449 });
1450 }
1451 }
1452 }
1453}
1454
1455#[derive(Debug, Clone)]
1466pub struct LayoutSpatialEntry {
1467 pub hash: u64,
1469 pub rect: Rect,
1471}
1472
1473pub struct LayoutSpatialIndex {
1484 root: Option<Box<QuadNode>>,
1485 bounds: Rect,
1487}
1488
1489const MAX_ITEMS_PER_NODE: usize = 16;
1490const MAX_TREE_DEPTH: u32 = 8;
1491
1492struct QuadNode {
1493 bounds: Rect,
1494 entries: Vec<LayoutSpatialEntry>,
1495 children: Option<Box<[Box<QuadNode>; 4]>>,
1497}
1498
1499impl QuadNode {
1500 fn new(bounds: Rect) -> Self {
1501 Self {
1502 bounds,
1503 entries: Vec::new(),
1504 children: None,
1505 }
1506 }
1507
1508 fn insert(&mut self, entry: LayoutSpatialEntry, depth: u32) {
1510 if !self.bounds.intersects(&entry.rect) {
1511 return;
1512 }
1513 if let Some(children) = &mut self.children {
1514 for child in children.iter_mut() {
1515 if child.bounds.intersects(&entry.rect) {
1516 child.insert(entry.clone(), depth + 1);
1517 }
1518 }
1519 return;
1520 }
1521 self.entries.push(entry);
1522 if self.entries.len() > MAX_ITEMS_PER_NODE && depth < MAX_TREE_DEPTH {
1523 self.split(depth);
1524 }
1525 }
1526
1527 fn split(&mut self, depth: u32) {
1529 let hw = self.bounds.width * 0.5;
1530 let hh = self.bounds.height * 0.5;
1531 let mx = self.bounds.x + hw;
1532 let my = self.bounds.y + hh;
1533 let make = |x, y, w, h| Box::new(QuadNode::new(Rect { x, y, width: w, height: h }));
1534 let mut children = Box::new([
1535 make(self.bounds.x, self.bounds.y, hw, hh), make(mx, self.bounds.y, hw, hh), make(self.bounds.x, my, hw, hh), make(mx, my, hw, hh), ]);
1540 let entries = std::mem::take(&mut self.entries);
1541 for e in entries {
1542 for child in children.iter_mut() {
1543 if child.bounds.intersects(&e.rect) {
1544 child.insert(e.clone(), depth + 1);
1545 }
1546 }
1547 }
1548 self.children = Some(children);
1549 }
1550
1551 fn hit_test(&self, point: (f32, f32), out: &mut Vec<LayoutSpatialEntry>) {
1553 if !self.bounds.contains(point.0, point.1) {
1554 return;
1555 }
1556 for e in &self.entries {
1557 if e.rect.contains(point.0, point.1) {
1558 out.push(e.clone());
1559 }
1560 }
1561 if let Some(children) = &self.children {
1562 for child in children.iter() {
1563 child.hit_test(point, out);
1564 }
1565 }
1566 }
1567
1568 fn query_region(&self, region: &Rect, out: &mut Vec<LayoutSpatialEntry>) {
1570 if !self.bounds.intersects(region) {
1571 return;
1572 }
1573 for e in &self.entries {
1574 if e.rect.intersects(region) {
1575 out.push(e.clone());
1576 }
1577 }
1578 if let Some(children) = &self.children {
1579 for child in children.iter() {
1580 child.query_region(region, out);
1581 }
1582 }
1583 }
1584}
1585
1586impl LayoutSpatialIndex {
1587 pub fn new() -> Self {
1589 Self { root: None, bounds: Rect::zero() }
1590 }
1591
1592 pub fn rebuild(&mut self, root_bounds: Rect, entries: impl IntoIterator<Item = LayoutSpatialEntry>) {
1598 self.bounds = root_bounds;
1599 let mut root = QuadNode::new(root_bounds);
1600 for e in entries {
1601 if e.rect.width > 0.0 && e.rect.height > 0.0 {
1603 root.insert(e, 0);
1604 }
1605 }
1606 self.root = Some(Box::new(root));
1607 }
1608
1609 pub fn hit_test(&self, x: f32, y: f32) -> Vec<LayoutSpatialEntry> {
1615 let mut out = Vec::new();
1616 if let Some(root) = &self.root {
1617 root.hit_test((x, y), &mut out);
1618 }
1619 out
1620 }
1621
1622 pub fn query_region(&self, region: &Rect) -> Vec<LayoutSpatialEntry> {
1624 let mut out = Vec::new();
1625 if let Some(root) = &self.root {
1626 root.query_region(region, &mut out);
1627 }
1628 out
1629 }
1630}
1631
1632impl Default for LayoutSpatialIndex {
1633 fn default() -> Self {
1634 Self::new()
1635 }
1636}
1637
1638pub fn size_views_parallel(
1654 views: &[&dyn LayoutView],
1655 proposal: cvkg_core::SizeProposal,
1656 cache: &mut LayoutCache,
1657) -> Vec<cvkg_core::Size> {
1658 if views.len() <= 1 {
1659 return views
1661 .iter()
1662 .map(|v| v.size_that_fits(proposal, &[], cache))
1663 .collect();
1664 }
1665
1666 #[cfg(feature = "parallel")]
1672 {
1673 }
1677
1678 #[cfg(not(feature = "parallel"))]
1679 {}
1680
1681 views
1684 .iter()
1685 .map(|v| v.size_that_fits(proposal, &[], cache))
1686 .collect()
1687}
1688
1689#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1700pub enum LayoutModality {
1701 #[default]
1703 Pointer,
1704 Touch,
1706 AccessibilityZoom,
1708}
1709
1710impl LayoutModality {
1711 pub fn min_tap_target(self) -> f32 {
1713 match self {
1714 LayoutModality::Pointer => 0.0,
1715 LayoutModality::Touch => 44.0,
1716 LayoutModality::AccessibilityZoom => 44.0,
1717 }
1718 }
1719
1720 pub fn spacing_multiplier(self) -> f32 {
1722 match self {
1723 LayoutModality::Pointer => 1.0,
1724 LayoutModality::Touch => 1.25,
1725 LayoutModality::AccessibilityZoom => 2.0,
1726 }
1727 }
1728
1729 pub fn adapt_size(self, size: cvkg_core::Size) -> cvkg_core::Size {
1733 let min = self.min_tap_target();
1734 cvkg_core::Size {
1735 width: size.width.max(min),
1736 height: size.height.max(min),
1737 }
1738 }
1739}
1740
1741#[derive(Debug, Clone, PartialEq)]
1751pub struct FocusCandidate {
1752 pub hash: u64,
1754 pub rect: Rect,
1756 pub tab_index: Option<i32>,
1760}
1761
1762pub fn compute_focus_order(mut candidates: Vec<FocusCandidate>) -> Vec<u64> {
1780 let mut explicit: Vec<FocusCandidate> = candidates
1782 .iter()
1783 .filter(|c| c.tab_index.map_or(false, |t| t > 0))
1784 .cloned()
1785 .collect();
1786 candidates.retain(|c| !c.tab_index.map_or(false, |t| t > 0));
1787
1788 explicit.sort_by(|a, b| {
1790 let ta = a.tab_index.unwrap_or(i32::MAX);
1791 let tb = b.tab_index.unwrap_or(i32::MAX);
1792 ta.cmp(&tb)
1793 .then_with(|| a.rect.y.total_cmp(&b.rect.y))
1794 .then_with(|| a.rect.x.total_cmp(&b.rect.x))
1795 });
1796
1797 let row_bucket = |r: &Rect| (r.y / 8.0).floor() as i32;
1801 candidates.sort_by(|a, b| {
1802 row_bucket(&a.rect)
1803 .cmp(&row_bucket(&b.rect))
1804 .then_with(|| a.rect.x.total_cmp(&b.rect.x))
1805 });
1806
1807 explicit
1808 .into_iter()
1809 .chain(candidates)
1810 .map(|c| c.hash)
1811 .collect()
1812}
1813
1814pub fn validate_reading_order(order: &[FocusCandidate]) -> Result<(), String> {
1825 let natural: Vec<&FocusCandidate> = order
1826 .iter()
1827 .filter(|c| !c.tab_index.map_or(false, |t| t > 0))
1828 .collect();
1829
1830 let row_bucket = |r: &Rect| (r.y / 8.0).floor() as i32;
1831 for window in natural.windows(2) {
1832 let a = window[0];
1833 let b = window[1];
1834 if row_bucket(&b.rect) < row_bucket(&a.rect) {
1836 return Err(format!(
1837 "reading order violation: view 0x{:X} (y≈{:.1}) precedes view 0x{:X} (y≈{:.1}) visually",
1838 b.hash, b.rect.y, a.hash, a.rect.y
1839 ));
1840 }
1841 if row_bucket(&a.rect) == row_bucket(&b.rect) && b.rect.x < a.rect.x - 1.0 {
1843 return Err(format!(
1844 "reading order violation: view 0x{:X} (x≈{:.1}) precedes view 0x{:X} (x≈{:.1}) on same row",
1845 b.hash, b.rect.x, a.hash, a.rect.x
1846 ));
1847 }
1848 }
1849 Ok(())
1850}
1851
1852#[derive(Debug, Clone)]
1864struct ProgressiveChild {
1865 hash: u64,
1868 laid_out: bool,
1870 rect: Rect,
1872}
1873
1874pub struct ProgressiveLayoutContext<'a> {
1892 children: &'a [&'a dyn LayoutView],
1894 entries: Vec<ProgressiveChild>,
1896 spacing: f32,
1898 alignment: Alignment,
1899 distribution: Distribution,
1900 bounds: Rect,
1901 completed: usize,
1903 fallback_applied: bool,
1905}
1906
1907impl<'a> ProgressiveLayoutContext<'a> {
1908 pub fn new(
1913 bounds: Rect,
1914 subviews: &'a [&'a dyn LayoutView],
1915 spacing: f32,
1916 alignment: Alignment,
1917 distribution: Distribution,
1918 ) -> Self {
1919 let entries = subviews
1920 .iter()
1921 .map(|v| ProgressiveChild {
1922 hash: v.view_hash(),
1923 laid_out: false,
1924 rect: Rect::zero(),
1925 })
1926 .collect();
1927
1928 Self {
1929 children: subviews,
1930 entries,
1931 spacing,
1932 alignment,
1933 distribution,
1934 bounds,
1935 completed: 0,
1936 fallback_applied: false,
1937 }
1938 }
1939
1940 pub fn layout_next_batch(&mut self, batch_size: usize) -> bool {
1949 self.layout_next_batch_inner(batch_size, None);
1950 self.is_complete()
1951 }
1952
1953 pub fn layout_next_batch_with_cache(
1961 &mut self,
1962 batch_size: usize,
1963 cache: &mut LayoutCache,
1964 ) -> (bool, Vec<Rect>) {
1965 self.layout_next_batch_inner(batch_size, Some(cache));
1966 let new_rects: Vec<Rect> = self
1967 .entries
1968 .iter()
1969 .filter(|e| e.laid_out && e.rect != Rect::zero())
1970 .map(|e| e.rect)
1971 .collect();
1972 (self.is_complete(), new_rects)
1973 }
1974
1975 fn layout_next_batch_inner(
1976 &mut self,
1977 batch_size: usize,
1978 mut cache: Option<&mut LayoutCache>,
1979 ) {
1980 let mut processed = 0;
1981 let mut batch_indices = Vec::new();
1982 for (i, entry) in self.entries.iter().enumerate() {
1983 if entry.laid_out {
1984 continue;
1985 }
1986 if processed >= batch_size {
1987 break;
1988 }
1989 batch_indices.push(i);
1990 processed += 1;
1991 }
1992
1993 if batch_indices.is_empty() {
1994 return;
1995 }
1996
1997 let batch_subviews: Vec<&dyn LayoutView> = batch_indices
1998 .iter()
1999 .map(|&i| self.children[i])
2000 .collect();
2001
2002 let rects = match cache {
2003 Some(ref mut c) => HStack::compute_layout_incremental(
2004 self.spacing,
2005 self.alignment,
2006 self.distribution,
2007 self.bounds,
2008 0,
2009 &batch_subviews,
2010 *c,
2011 ),
2012 None => {
2013 let mut tmp = LayoutCache::new();
2014 HStack::compute_layout_incremental(
2015 self.spacing,
2016 self.alignment,
2017 self.distribution,
2018 self.bounds,
2019 0,
2020 &batch_subviews,
2021 &mut tmp,
2022 )
2023 }
2024 };
2025
2026 for (local_idx, &global_idx) in batch_indices.iter().enumerate() {
2027 if local_idx < rects.len() {
2028 self.entries[global_idx].rect = rects[local_idx];
2029 self.entries[global_idx].laid_out = true;
2030 self.completed += 1;
2031 }
2032 }
2033
2034 if let Some(c) = cache.as_mut() {
2036 for (local_idx, &global_idx) in batch_indices.iter().enumerate() {
2037 if local_idx < rects.len() {
2038 let hash = self.entries[global_idx].hash;
2039 if hash != 0 {
2040 c.previous_rects.insert(hash, rects[local_idx]);
2041 }
2042 }
2043 }
2044 }
2045 }
2046
2047 pub fn is_complete(&self) -> bool {
2050 self.fallback_applied || self.completed >= self.entries.len()
2051 }
2052
2053 pub fn progress(&self) -> (usize, usize) {
2055 (self.completed, self.entries.len())
2056 }
2057
2058 pub fn apply_remaining_fallback(&mut self, cache: &mut LayoutCache) -> Vec<Rect> {
2067 let mut fallback_rects = Vec::new();
2068 let remaining: Vec<usize> = self
2069 .entries
2070 .iter()
2071 .enumerate()
2072 .filter(|(_, e)| !e.laid_out)
2073 .map(|(i, _)| i)
2074 .collect();
2075
2076 if remaining.is_empty() {
2077 self.fallback_applied = true;
2078 return fallback_rects;
2079 }
2080
2081 let cols = (remaining.len() as f32).sqrt().ceil() as usize;
2082 let rows = (remaining.len() + cols - 1) / cols;
2083 let cell_w = self.bounds.width / cols as f32;
2084 let cell_h = self.bounds.height / rows as f32;
2085
2086 for (offset, &idx) in remaining.iter().enumerate() {
2087 let hash = self.entries[idx].hash;
2088 let rect = if hash != 0 {
2089 cache
2090 .previous_rects
2091 .get(&hash)
2092 .copied()
2093 .unwrap_or_else(|| {
2094 let col = offset % cols;
2095 let row = offset / cols;
2096 Rect {
2097 x: self.bounds.x + col as f32 * cell_w,
2098 y: self.bounds.y + row as f32 * cell_h,
2099 width: cell_w,
2100 height: cell_h,
2101 }
2102 })
2103 } else {
2104 let col = offset % cols;
2105 let row = offset / cols;
2106 Rect {
2107 x: self.bounds.x + col as f32 * cell_w,
2108 y: self.bounds.y + row as f32 * cell_h,
2109 width: cell_w,
2110 height: cell_h,
2111 }
2112 };
2113
2114 self.entries[idx].rect = rect;
2115 self.entries[idx].laid_out = true;
2116 self.completed += 1;
2117 if hash != 0 {
2118 cache.previous_rects.insert(hash, rect);
2119 }
2120 fallback_rects.push(rect);
2121 }
2122
2123 self.fallback_applied = true;
2124 fallback_rects
2125 }
2126
2127 pub fn take_rects(self) -> Vec<Rect> {
2130 self.entries.into_iter().map(|e| e.rect).collect()
2131 }
2132}
2133
2134#[cfg(test)]
2135mod tests {
2136 use super::*;
2137
2138 struct MockView {
2139 size: Size,
2140 flex: f32,
2141 }
2142
2143 impl LayoutView for MockView {
2144 fn size_that_fits(
2145 &self,
2146 _p: SizeProposal,
2147 _s: &[&dyn LayoutView],
2148 _c: &mut LayoutCache,
2149 ) -> Size {
2150 self.size
2151 }
2152 fn place_subviews(&self, _b: Rect, _s: &mut [&mut dyn LayoutView], _c: &mut LayoutCache) {}
2153 fn flex_weight(&self) -> f32 {
2154 self.flex
2155 }
2156 }
2157
2158 #[test]
2159 fn test_hstack_basic() {
2160 let v1 = MockView {
2161 size: Size {
2162 width: 50.0,
2163 height: 50.0,
2164 },
2165 flex: 0.0,
2166 };
2167 let v2 = MockView {
2168 size: Size {
2169 width: 100.0,
2170 height: 100.0,
2171 },
2172 flex: 0.0,
2173 };
2174 let views: Vec<&dyn LayoutView> = vec![&v1, &v2];
2175 let mut cache = LayoutCache::new();
2176 let bounds = Rect {
2177 x: 0.0,
2178 y: 0.0,
2179 width: 300.0,
2180 height: 200.0,
2181 };
2182
2183 let rects = HStack::compute_layout(
2184 10.0,
2185 Alignment::Center,
2186 Distribution::Leading,
2187 bounds,
2188 &views,
2189 &mut cache,
2190 );
2191
2192 assert_eq!(rects.len(), 2);
2193 assert_eq!(
2194 rects[0],
2195 Rect {
2196 x: 0.0,
2197 y: 75.0,
2198 width: 50.0,
2199 height: 50.0
2200 }
2201 );
2202 assert_eq!(
2203 rects[1],
2204 Rect {
2205 x: 60.0,
2206 y: 50.0,
2207 width: 100.0,
2208 height: 100.0
2209 }
2210 );
2211 }
2212
2213 #[test]
2214 fn test_vstack_flex() {
2215 let v1 = MockView {
2216 size: Size {
2217 width: 100.0,
2218 height: 50.0,
2219 },
2220 flex: 0.0,
2221 };
2222 let v2 = MockView {
2223 size: Size {
2224 width: 100.0,
2225 height: 0.0,
2226 },
2227 flex: 1.0,
2228 }; let views: Vec<&dyn LayoutView> = vec![&v1, &v2];
2230 let mut cache = LayoutCache::new();
2231 let bounds = Rect {
2232 x: 0.0,
2233 y: 0.0,
2234 width: 200.0,
2235 height: 160.0,
2236 };
2237
2238 let rects = VStack::compute_layout(
2239 10.0,
2240 Alignment::Leading,
2241 Distribution::Fill,
2242 bounds,
2243 &views,
2244 &mut cache,
2245 );
2246
2247 assert_eq!(rects.len(), 2);
2248 assert_eq!(
2249 rects[0],
2250 Rect {
2251 x: 0.0,
2252 y: 0.0,
2253 width: 100.0,
2254 height: 50.0
2255 }
2256 );
2257 assert_eq!(
2258 rects[1],
2259 Rect {
2260 x: 0.0,
2261 y: 60.0,
2262 width: 100.0,
2263 height: 100.0
2264 }
2265 ); }
2267
2268 #[test]
2269 fn test_grid_layout() {
2270 let v1 = MockView {
2271 size: Size::ZERO,
2272 flex: 0.0,
2273 };
2274 let v2 = MockView {
2275 size: Size::ZERO,
2276 flex: 0.0,
2277 };
2278 let v3 = MockView {
2279 size: Size::ZERO,
2280 flex: 0.0,
2281 };
2282 let views: Vec<&dyn LayoutView> = vec![&v1, &v2, &v3];
2283 let mut cache = LayoutCache::new();
2284 let bounds = Rect {
2285 x: 0.0,
2286 y: 0.0,
2287 width: 210.0,
2288 height: 210.0,
2289 };
2290
2291 let grid = Grid::new(
2292 vec![GridTrack::Fixed(100.0), GridTrack::Fixed(100.0)],
2293 vec![GridTrack::Fixed(100.0), GridTrack::Fixed(100.0)],
2294 10.0,
2295 10.0,
2296 );
2297 let placements = vec![
2298 Some(cvkg_core::GridPlacement {
2299 column: 0,
2300 column_span: 1,
2301 row: 0,
2302 row_span: 1,
2303 }),
2304 Some(cvkg_core::GridPlacement {
2305 column: 1,
2306 column_span: 1,
2307 row: 0,
2308 row_span: 1,
2309 }),
2310 Some(cvkg_core::GridPlacement {
2311 column: 0,
2312 column_span: 1,
2313 row: 1,
2314 row_span: 1,
2315 }),
2316 ];
2317
2318 let rects = grid.compute_layout_rects(bounds, &views, &placements, &mut cache);
2319
2320 assert_eq!(rects.len(), 3);
2321 assert_eq!(
2322 rects[0],
2323 Rect {
2324 x: 0.0,
2325 y: 0.0,
2326 width: 100.0,
2327 height: 100.0
2328 }
2329 );
2330 assert_eq!(
2331 rects[1],
2332 Rect {
2333 x: 110.0,
2334 y: 0.0,
2335 width: 100.0,
2336 height: 100.0
2337 }
2338 );
2339 assert_eq!(
2340 rects[2],
2341 Rect {
2342 x: 0.0,
2343 y: 110.0,
2344 width: 100.0,
2345 height: 100.0
2346 }
2347 );
2348 }
2349
2350 #[test]
2351 fn test_layout_cycle_detection() {
2352 struct CyclingView {
2353 child_hash: u64,
2354 }
2355 impl LayoutView for CyclingView {
2356 fn size_that_fits(
2357 &self,
2358 proposal: SizeProposal,
2359 _subviews: &[&dyn LayoutView],
2360 cache: &mut LayoutCache,
2361 ) -> Size {
2362 with_layout_cycle_guard(self.view_hash(), Size { width: 42.0, height: 42.0 }, || {
2363 let recursive_self = CyclingView { child_hash: self.view_hash() };
2364 let subviews: Vec<&dyn LayoutView> = vec![&recursive_self];
2365 recursive_self.size_that_fits(proposal, &subviews, cache)
2366 })
2367 }
2368 fn place_subviews(&self, _b: Rect, _s: &mut [&mut dyn LayoutView], _c: &mut LayoutCache) {}
2369 fn view_hash(&self) -> u64 {
2370 12345
2371 }
2372 }
2373
2374 let view = CyclingView { child_hash: 12345 };
2375 let mut cache = LayoutCache::new();
2376 let size = view.size_that_fits(SizeProposal::unspecified(), &[], &mut cache);
2377 assert_eq!(size.width, 42.0);
2379 assert_eq!(size.height, 42.0);
2380 }
2381
2382 #[test]
2383 fn test_bottom_up_layout_invalidation() {
2384 let mut cache = LayoutCache::new();
2385 let child_hash = 100u64;
2386 let parent_hash = 200u64;
2387
2388 cache.register_parent(child_hash, parent_hash);
2389 cache.set_size(child_hash, SizeProposal::unspecified(), Size { width: 10.0, height: 10.0 });
2390 cache.set_size(parent_hash, SizeProposal::unspecified(), Size { width: 20.0, height: 20.0 });
2391
2392 assert!(cache.get_size(child_hash, SizeProposal::unspecified()).is_some());
2394 assert!(cache.get_size(parent_hash, SizeProposal::unspecified()).is_some());
2395
2396 cache.invalidate_view(child_hash);
2398
2399 assert!(cache.get_size(child_hash, SizeProposal::unspecified()).is_none());
2401 assert!(cache.get_size(parent_hash, SizeProposal::unspecified()).is_none());
2402 }
2403
2404 #[test]
2405 fn test_viewport_aware_layout_culling() {
2406 use std::sync::atomic::{AtomicUsize, Ordering};
2407 use std::sync::Arc;
2408
2409 struct SpyView {
2410 calls: Arc<AtomicUsize>,
2411 hash: u64,
2412 rect: Rect,
2413 }
2414
2415 impl LayoutView for SpyView {
2416 fn size_that_fits(&self, _p: SizeProposal, _s: &[&dyn LayoutView], _c: &mut LayoutCache) -> Size {
2417 Size { width: self.rect.width, height: self.rect.height }
2418 }
2419 fn place_subviews(&self, _b: Rect, _s: &mut [&mut dyn LayoutView], _c: &mut LayoutCache) {
2420 self.calls.fetch_add(1, Ordering::SeqCst);
2421 }
2422 fn view_hash(&self) -> u64 {
2423 self.hash
2424 }
2425 }
2426
2427 let calls = Arc::new(AtomicUsize::new(0));
2428 let view1 = SpyView {
2429 calls: calls.clone(),
2430 hash: 1001,
2431 rect: Rect::new(0.0, 0.0, 50.0, 50.0),
2432 };
2433 let view2 = SpyView {
2434 calls: calls.clone(),
2435 hash: 1002,
2436 rect: Rect::new(500.0, 0.0, 50.0, 50.0), };
2438
2439 let mut cache = LayoutCache::new();
2440 cache.viewport = Some(Rect::new(0.0, 0.0, 55.0, 100.0));
2442
2443 let mut v1 = view1;
2444 let mut v2 = view2;
2445 let mut mut_subviews: Vec<&mut dyn LayoutView> = vec![&mut v1, &mut v2];
2446
2447 HStack::new(10.0, Alignment::Center, Distribution::Leading)
2448 .place_subviews(Rect::new(0.0, 0.0, 600.0, 100.0), &mut mut_subviews, &mut cache);
2449
2450 assert_eq!(calls.load(Ordering::SeqCst), 1);
2454 }
2455
2456 #[test]
2457 fn test_layout_budget_thrashing_prevention() {
2458 use std::sync::atomic::{AtomicUsize, Ordering};
2459 use std::sync::Arc;
2460
2461 struct SpyView {
2462 calls: Arc<AtomicUsize>,
2463 hash: u64,
2464 rect: Rect,
2465 }
2466
2467 impl LayoutView for SpyView {
2468 fn size_that_fits(&self, _p: SizeProposal, _s: &[&dyn LayoutView], _c: &mut LayoutCache) -> Size {
2469 Size { width: self.rect.width, height: self.rect.height }
2470 }
2471 fn place_subviews(&self, _b: Rect, _s: &mut [&mut dyn LayoutView], _c: &mut LayoutCache) {
2472 self.calls.fetch_add(1, Ordering::SeqCst);
2473 }
2474 fn view_hash(&self) -> u64 {
2475 self.hash
2476 }
2477 }
2478
2479 let calls = Arc::new(AtomicUsize::new(0));
2480 let view = SpyView {
2481 calls: calls.clone(),
2482 hash: 2001,
2483 rect: Rect::new(0.0, 0.0, 100.0, 100.0),
2484 };
2485
2486 let mut cache = LayoutCache::new();
2487 cvkg_core::LayoutCache::set_layout_budget_deadline(Some(
2489 std::time::Instant::now() - std::time::Duration::from_millis(50),
2490 ));
2491
2492 cache.previous_rects.insert(2001, Rect::new(10.0, 10.0, 100.0, 100.0));
2494
2495 let mut v = view;
2496 let mut subviews: Vec<&mut dyn LayoutView> = vec![&mut v];
2497
2498 HStack::new(0.0, Alignment::Center, Distribution::Leading)
2499 .place_subviews(Rect::new(0.0, 0.0, 500.0, 500.0), &mut subviews, &mut cache);
2500
2501 assert_eq!(calls.load(Ordering::SeqCst), 1);
2505
2506 let engine = TaffyLayoutEngine::get_or_insert_engine(&mut cache);
2508 assert!(!engine.node_map.contains_key(&2001));
2509
2510 cvkg_core::LayoutCache::clear_layout_budget_deadline();
2511 }
2512
2513 #[test]
2517 fn test_spatial_index_hit_test() {
2518 let mut index = LayoutSpatialIndex::new();
2519 let root = Rect { x: 0.0, y: 0.0, width: 1000.0, height: 1000.0 };
2520 let entries = vec![
2521 LayoutSpatialEntry { hash: 1, rect: Rect { x: 0.0, y: 0.0, width: 100.0, height: 100.0 } },
2522 LayoutSpatialEntry { hash: 2, rect: Rect { x: 200.0, y: 200.0, width: 50.0, height: 50.0 } },
2523 LayoutSpatialEntry { hash: 3, rect: Rect { x: 500.0, y: 500.0, width: 200.0, height: 200.0 } },
2524 ];
2525 index.rebuild(root, entries);
2526
2527 let hits = index.hit_test(50.0, 50.0);
2529 assert_eq!(hits.len(), 1);
2530 assert_eq!(hits[0].hash, 1);
2531
2532 let hits = index.hit_test(600.0, 600.0);
2534 assert_eq!(hits.len(), 1);
2535 assert_eq!(hits[0].hash, 3);
2536
2537 let hits = index.hit_test(999.0, 1.0);
2539 assert!(hits.is_empty(), "Expected no hits, got {:?}", hits.iter().map(|e| e.hash).collect::<Vec<_>>());
2540 }
2541
2542 #[test]
2543 fn test_spatial_index_query_region() {
2544 let mut index = LayoutSpatialIndex::new();
2545 let root = Rect { x: 0.0, y: 0.0, width: 500.0, height: 500.0 };
2546 let entries = vec![
2547 LayoutSpatialEntry { hash: 10, rect: Rect { x: 0.0, y: 0.0, width: 100.0, height: 100.0 } },
2548 LayoutSpatialEntry { hash: 20, rect: Rect { x: 400.0, y: 400.0, width: 50.0, height: 50.0 } },
2549 ];
2550 index.rebuild(root, entries);
2551
2552 let region = Rect { x: 0.0, y: 0.0, width: 150.0, height: 150.0 };
2554 let results = index.query_region(®ion);
2555 assert!(results.iter().any(|e| e.hash == 10));
2556 assert!(!results.iter().any(|e| e.hash == 20));
2557 }
2558
2559 #[test]
2563 fn test_adaptive_modality_touch_enlarges_small_views() {
2564 let small = cvkg_core::Size { width: 20.0, height: 12.0 };
2565 let adapted = LayoutModality::Touch.adapt_size(small);
2566 assert!(adapted.width >= 44.0, "Width must be at least 44pt for touch");
2567 assert!(adapted.height >= 44.0, "Height must be at least 44pt for touch");
2568 }
2569
2570 #[test]
2571 fn test_adaptive_modality_pointer_does_not_enlarge() {
2572 let large = cvkg_core::Size { width: 200.0, height: 50.0 };
2573 let adapted = LayoutModality::Pointer.adapt_size(large);
2574 assert_eq!(adapted.width, 200.0);
2575 assert_eq!(adapted.height, 50.0);
2576 }
2577
2578 #[test]
2579 fn test_adaptive_modality_accessibility_zoom_spacing() {
2580 assert!(
2581 LayoutModality::AccessibilityZoom.spacing_multiplier() > LayoutModality::Touch.spacing_multiplier(),
2582 "Accessibility zoom must have the largest spacing multiplier"
2583 );
2584 }
2585
2586 #[test]
2590 fn test_focus_order_ltr_visual_sort() {
2591 let candidates = vec![
2593 FocusCandidate { hash: 100, rect: Rect { x: 200.0, y: 10.0, width: 50.0, height: 20.0 }, tab_index: None },
2594 FocusCandidate { hash: 200, rect: Rect { x: 0.0, y: 10.0, width: 50.0, height: 20.0 }, tab_index: None },
2595 FocusCandidate { hash: 300, rect: Rect { x: 100.0, y: 10.0, width: 50.0, height: 20.0 }, tab_index: None },
2596 ];
2597 let order = compute_focus_order(candidates);
2598 assert_eq!(order, vec![200, 300, 100], "LTR focus order violated: {:?}", order);
2600 }
2601
2602 #[test]
2603 fn test_focus_order_explicit_tabindex_comes_first() {
2604 let candidates = vec![
2605 FocusCandidate { hash: 1, rect: Rect { x: 0.0, y: 100.0, width: 50.0, height: 20.0 }, tab_index: None },
2606 FocusCandidate { hash: 2, rect: Rect { x: 0.0, y: 0.0, width: 50.0, height: 20.0 }, tab_index: Some(2) },
2607 FocusCandidate { hash: 3, rect: Rect { x: 0.0, y: 50.0, width: 50.0, height: 20.0 }, tab_index: Some(1) },
2608 ];
2609 let order = compute_focus_order(candidates);
2610 assert_eq!(order[0], 3, "tabindex=1 must be first");
2612 assert_eq!(order[1], 2, "tabindex=2 must be second");
2613 assert_eq!(order[2], 1, "natural order must be last");
2614 }
2615
2616 #[test]
2620 fn test_reading_order_valid_sequence_passes() {
2621 let candidates = vec![
2622 FocusCandidate { hash: 1, rect: Rect { x: 0.0, y: 0.0, width: 50.0, height: 20.0 }, tab_index: None },
2623 FocusCandidate { hash: 2, rect: Rect { x: 100.0, y: 0.0, width: 50.0, height: 20.0 }, tab_index: None },
2624 FocusCandidate { hash: 3, rect: Rect { x: 0.0, y: 30.0, width: 50.0, height: 20.0 }, tab_index: None },
2625 ];
2626 assert!(validate_reading_order(&candidates).is_ok());
2627 }
2628
2629 #[test]
2630 fn test_reading_order_backwards_row_fails() {
2631 let candidates = vec![
2633 FocusCandidate { hash: 1, rect: Rect { x: 0.0, y: 100.0, width: 50.0, height: 20.0 }, tab_index: None },
2634 FocusCandidate { hash: 2, rect: Rect { x: 0.0, y: 0.0, width: 50.0, height: 20.0 }, tab_index: None },
2635 ];
2636 assert!(validate_reading_order(&candidates).is_err(), "Backwards row must fail validation");
2637 }
2638
2639 #[test]
2641 fn p2_47_deep_tree_100_levels() {
2642 let mut cache = LayoutCache::new();
2643 let mut root: Box<dyn LayoutView> = Box::new(HStack::new(
2646 0.0,
2647 Alignment::Leading,
2648 Distribution::Leading,
2649 ));
2650 for _ in 0..50 {
2651 let child: Box<dyn LayoutView> =
2652 Box::new(HStack::new(0.0, Alignment::Leading, Distribution::Leading));
2653 let _ = child;
2655 }
2656 let proposal = SizeProposal::unspecified();
2658 let _ = root.size_that_fits(proposal, &[], &mut cache);
2659 }
2660
2661 #[test]
2662 fn p2_47_wide_tree_no_panic() {
2663 let mut cache = LayoutCache::new();
2664 let root = HStack::new(0.0, Alignment::Leading, Distribution::Leading);
2666 let proposal = SizeProposal::unspecified();
2667 let _ = root.size_that_fits(proposal, &[], &mut cache);
2668 }
2669
2670 #[test]
2671 fn p2_47_nested_flex_no_panic() {
2672 let mut cache = LayoutCache::new();
2673 let inner = HStack::new(0.0, Alignment::Leading, Distribution::Leading);
2674 let _ = inner.size_that_fits(SizeProposal::unspecified(), &[], &mut cache);
2675 }
2676
2677 fn make_mock_views(n: usize) -> Vec<MockView> {
2682 (0..n)
2683 .map(|_| MockView {
2684 size: Size {
2685 width: 50.0,
2686 height: 30.0,
2687 },
2688 flex: 0.0,
2689 })
2690 .collect()
2691 }
2692
2693 #[test]
2694 fn test_progressive_layout_completes_all_children() {
2695 let views = make_mock_views(10);
2696 let subviews: Vec<&dyn LayoutView> = views.iter().map(|v| v as &dyn LayoutView).collect();
2697 let bounds = Rect {
2698 x: 0.0,
2699 y: 0.0,
2700 width: 1000.0,
2701 height: 200.0,
2702 };
2703 let mut ctx = ProgressiveLayoutContext::new(
2704 bounds,
2705 &subviews,
2706 0.0,
2707 Alignment::Leading,
2708 Distribution::Leading,
2709 );
2710 assert!(!ctx.is_complete());
2711 assert!(!ctx.layout_next_batch(3));
2712 assert!(!ctx.is_complete());
2713 assert!(!ctx.layout_next_batch(3));
2714 assert!(!ctx.is_complete());
2715 assert!(!ctx.layout_next_batch(3));
2716 assert!(!ctx.is_complete());
2717 assert!(ctx.layout_next_batch(3));
2718 assert!(ctx.is_complete());
2719 }
2720
2721 #[test]
2722 fn test_progressive_layout_reports_progress() {
2723 let views = make_mock_views(5);
2724 let subviews: Vec<&dyn LayoutView> = views.iter().map(|v| v as &dyn LayoutView).collect();
2725 let bounds = Rect {
2726 x: 0.0,
2727 y: 0.0,
2728 width: 500.0,
2729 height: 200.0,
2730 };
2731 let mut ctx = ProgressiveLayoutContext::new(
2732 bounds,
2733 &subviews,
2734 0.0,
2735 Alignment::Leading,
2736 Distribution::Leading,
2737 );
2738 assert_eq!(ctx.progress(), (0, 5));
2739 ctx.layout_next_batch(2);
2740 assert_eq!(ctx.progress(), (2, 5));
2741 ctx.layout_next_batch(2);
2742 assert_eq!(ctx.progress(), (4, 5));
2743 ctx.layout_next_batch(1);
2744 assert_eq!(ctx.progress(), (5, 5));
2745 }
2746
2747 #[test]
2748 fn test_progressive_layout_fallback_positions_remaining() {
2749 let views = make_mock_views(6);
2750 let subviews: Vec<&dyn LayoutView> = views.iter().map(|v| v as &dyn LayoutView).collect();
2751 let bounds = Rect {
2752 x: 0.0,
2753 y: 0.0,
2754 width: 600.0,
2755 height: 200.0,
2756 };
2757 let mut ctx = ProgressiveLayoutContext::new(
2758 bounds,
2759 &subviews,
2760 10.0,
2761 Alignment::Leading,
2762 Distribution::Leading,
2763 );
2764 ctx.layout_next_batch(2);
2765 assert_eq!(ctx.progress(), (2, 6));
2766 let mut cache = LayoutCache::new();
2767 let fallback_rects = ctx.apply_remaining_fallback(&mut cache);
2768 assert_eq!(fallback_rects.len(), 4);
2769 for r in &fallback_rects {
2770 assert!(r.width > 0.0);
2771 assert!(r.height > 0.0);
2772 }
2773 assert!(ctx.is_complete());
2774 }
2775
2776 #[test]
2777 fn test_progressive_layout_uses_cached_results() {
2778 let views = make_mock_views(4);
2779 let subviews: Vec<&dyn LayoutView> = views.iter().map(|v| v as &dyn LayoutView).collect();
2780 let bounds = Rect {
2781 x: 0.0,
2782 y: 0.0,
2783 width: 400.0,
2784 height: 200.0,
2785 };
2786 let mut cache = LayoutCache::new();
2787 let mut ctx1 = ProgressiveLayoutContext::new(
2788 bounds,
2789 &subviews,
2790 0.0,
2791 Alignment::Leading,
2792 Distribution::Leading,
2793 );
2794 ctx1.layout_next_batch(2);
2795 for entry in ctx1.entries.iter() {
2796 if entry.rect != Rect::zero() {
2797 cache.previous_rects.insert(entry.hash, entry.rect);
2798 }
2799 }
2800 let mut ctx2 = ProgressiveLayoutContext::new(
2801 bounds,
2802 &subviews,
2803 0.0,
2804 Alignment::Leading,
2805 Distribution::Leading,
2806 );
2807 let (_done, _rects) = ctx2.layout_next_batch_with_cache(2, &mut cache);
2808 assert_eq!(ctx2.progress().0, 2);
2809 }
2810}