1use geometry_core::Rect;
2use layout_core::{AvailableSpace, LayoutEngine, LayoutError, LayoutStyle, MeasureFn, NodeId};
3use reactive_core::{RwSignal, batch, signal};
4use rustc_hash::FxHashMap;
5
6reactive_core::surface_local! {
7 slot LAYOUT_RUNTIME: LayoutRuntime = LayoutRuntime::new();
13 access with_runtime, with_runtime_ref;
14 context LayoutContext, LayoutGuard;
15}
16
17pub fn reset_layout_runtime() {
20 with_runtime(|rt| *rt = LayoutRuntime::new());
21}
22
23pub fn new_leaf(style: LayoutStyle) -> Result<(NodeId, RwSignal<Rect>), LayoutError> {
24 with_runtime(|rt| rt.new_leaf(style))
25}
26
27pub fn new_measured_leaf(
30 style: LayoutStyle,
31 measure: MeasureFn,
32) -> Result<(NodeId, RwSignal<Rect>), LayoutError> {
33 with_runtime(|rt| rt.new_measured_leaf(style, measure))
34}
35
36pub fn new_container(style: LayoutStyle, children: &[NodeId]) -> Result<NodeId, LayoutError> {
37 with_runtime(|rt| rt.new_container(style, children))
38}
39
40pub fn compute_layout(
45 root: NodeId,
46 width: AvailableSpace,
47 height: AvailableSpace,
48) -> Result<(), LayoutError> {
49 let direction = crate::direction::current_direction();
51 with_runtime(|rt| rt.engine.set_direction(direction));
52 let updates = with_runtime(|rt| rt.compute_layout(root, width, height))?;
53 batch(|| {
54 for (sig, rect) in updates {
55 if sig.peek() != rect {
56 sig.set(rect);
57 }
58 }
59 });
60 Ok(())
61}
62
63pub fn relayout_if_dirty() {
70 let roots: Vec<(NodeId, AvailableSpace, AvailableSpace)> = with_runtime(|rt| {
71 rt.last_space
72 .iter()
73 .map(|(&n, &(w, h))| (n, w, h))
74 .collect()
75 });
76 for (root, width, height) in roots {
77 let _ = compute_layout(root, width, height);
78 }
79}
80
81pub fn track_layout(node: NodeId) -> Option<RwSignal<Rect>> {
82 with_runtime(|rt| rt.track_layout(node))
83}
84
85pub fn absolute_rect(node: NodeId) -> Option<Rect> {
94 with_runtime(|rt| {
95 let &(x, y) = rt.abs_pos.get(&node)?;
96 let size = rt.registry.get(&node).map(|s| s.peek()).unwrap_or_default();
97 Some(Rect::new(x, y, size.width, size.height))
98 })
99}
100
101pub fn is_descendant_of(node: NodeId, ancestor: NodeId) -> bool {
104 with_runtime(|rt| rt.is_in_subtree(node, ancestor))
105}
106
107pub fn is_hidden(node: NodeId) -> bool {
113 with_runtime(|rt| rt.is_hidden_by_display(node))
114}
115
116pub fn mark_dirty(node: NodeId) -> Result<(), LayoutError> {
117 with_runtime(|rt| rt.engine.mark_dirty(node))
118}
119
120pub fn set_layout_style(node: NodeId, style: LayoutStyle) -> Result<(), LayoutError> {
126 with_runtime(|rt| {
127 rt.engine.set_style(node, style)?;
128 rt.engine.mark_dirty(node)
129 })
130}
131
132pub fn set_display(node: NodeId, visible: bool) {
134 with_runtime(|rt| rt.engine.set_display(node, visible))
135}
136
137pub fn set_container_row(node: NodeId) {
141 with_runtime(|rt| rt.engine.make_flex_row(node));
142 mark_dirty(node).ok();
143}
144
145pub fn container_is_row(node: NodeId) -> bool {
148 with_runtime(|rt| rt.engine.is_row(node))
149}
150
151pub fn set_leading_margin(node: NodeId, is_row: bool, px: f32) {
154 with_runtime(|rt| rt.engine.set_leading_margin(node, is_row, px))
155}
156
157pub fn set_min_height(node: NodeId, px: f32) {
161 with_runtime(|rt| rt.engine.set_min_height(node, Some(px)))
162}
163
164pub fn set_children(parent: NodeId, children: &[NodeId]) -> Result<(), LayoutError> {
167 with_runtime(|rt| rt.set_children(parent, children))
168}
169
170pub fn remove_node(node: NodeId) {
174 with_runtime(|rt| rt.remove_node(node))
175}
176
177pub fn overlay_viewport() -> Option<geometry_core::Rect> {
189 with_runtime(|rt| {
190 let host = rt.overlay_host?;
191 rt.engine.layout(host).ok()
192 })
193}
194
195pub fn set_overlay_host(node: NodeId) {
196 with_runtime(|rt| {
197 rt.overlay_host = Some(node);
198 rt.host_pinned = true;
199 });
200}
201
202pub fn attach_overlay(node: NodeId) -> bool {
208 with_runtime(|rt| {
209 let Some(host) = rt.overlay_host else {
210 return false;
211 };
212 if rt.engine.add_child(host, node).is_err() {
213 return false;
214 }
215 rt.parents.insert(node, host);
216 rt.engine.mark_dirty(host).ok();
217 true
218 })
219}
220
221pub fn detach_overlay(node: NodeId) {
224 with_runtime(|rt| {
225 if let Some(host) = rt.parents.remove(&node) {
229 rt.engine.remove_child(host, node).ok();
230 rt.engine.mark_dirty(host).ok();
231 }
232 });
233}
234
235struct LayoutRuntime {
236 engine: LayoutEngine,
237 registry: FxHashMap<NodeId, RwSignal<Rect>>,
238 parents: FxHashMap<NodeId, NodeId>,
239 boundary_nodes: FxHashMap<NodeId, (f32, f32)>,
240 last_space: FxHashMap<NodeId, (AvailableSpace, AvailableSpace)>,
242 constrained: Vec<(NodeId, LayoutStyle, Option<f32>)>,
244 root_auto: FxHashMap<NodeId, (bool, bool)>,
246 overlay_host: Option<NodeId>,
249 host_pinned: bool,
254 abs_pos: FxHashMap<NodeId, (f32, f32)>,
260 #[cfg(debug_assertions)]
262 is_computing: bool,
263}
264
265impl LayoutRuntime {
266 fn new() -> Self {
267 Self {
268 engine: LayoutEngine::new(),
269 registry: FxHashMap::default(),
270 parents: FxHashMap::default(),
271 boundary_nodes: FxHashMap::default(),
272 last_space: FxHashMap::default(),
273 constrained: Vec::new(),
274 root_auto: FxHashMap::default(),
275 overlay_host: None,
276 host_pinned: false,
277 abs_pos: FxHashMap::default(),
278 #[cfg(debug_assertions)]
279 is_computing: false,
280 }
281 }
282
283 fn track_constrained(&mut self, node: NodeId, style: &LayoutStyle) {
284 if style.max_width_px().is_some() {
285 self.constrained.push((node, style.clone(), None));
286 }
287 }
288
289 pub(crate) fn new_leaf(
290 &mut self,
291 style: LayoutStyle,
292 ) -> Result<(NodeId, RwSignal<Rect>), LayoutError> {
293 let node = self.engine.new_leaf(style.clone())?;
294 let signal = signal(Rect::default());
295 self.registry.insert(node, signal.clone());
296 if let Some(dimensions) = self.engine.is_fixed_size(node) {
297 self.boundary_nodes.insert(node, dimensions);
298 }
299 self.track_constrained(node, &style);
300 Ok((node, signal))
301 }
302
303 pub(crate) fn new_measured_leaf(
304 &mut self,
305 style: LayoutStyle,
306 measure: MeasureFn,
307 ) -> Result<(NodeId, RwSignal<Rect>), LayoutError> {
308 let node = self.engine.new_measured_leaf(style.clone(), measure)?;
309 let signal = signal(Rect::default());
310 self.registry.insert(node, signal.clone());
311 self.track_constrained(node, &style);
312 Ok((node, signal))
313 }
314
315 pub(crate) fn new_container(
316 &mut self,
317 style: LayoutStyle,
318 children: &[NodeId],
319 ) -> Result<NodeId, LayoutError> {
320 let node = self.engine.new_container(style.clone(), children)?;
321 let signal = signal(Rect::default());
322 self.registry.insert(node, signal);
323 for &child in children {
324 self.parents.insert(child, node);
325 }
326 if let Some(dimensions) = self.engine.is_fixed_size(node) {
327 self.boundary_nodes.insert(node, dimensions);
328 }
329 self.track_constrained(node, &style);
330 Ok(node)
331 }
332
333 fn compute_layout(
334 &mut self,
335 root: NodeId,
336 width: AvailableSpace,
337 height: AvailableSpace,
338 ) -> Result<Vec<(RwSignal<Rect>, Rect)>, LayoutError> {
339 if !self.host_pinned
346 && !self.parents.contains_key(&root)
347 && matches!(height, AvailableSpace::Definite(_))
348 {
349 self.overlay_host = Some(root);
350 }
351 let is_space_changed = self.last_space.get(&root) != Some(&(width, height));
353 if is_space_changed {
354 self.engine.mark_dirty(root).ok();
355 self.last_space.insert(root, (width, height));
356 } else if !self.engine.is_dirty(root) {
357 return Ok(Vec::new());
358 }
359 let (width_auto, height_auto) = match self.root_auto.get(&root).copied() {
361 Some(v) => v,
362 None => {
363 let v = self.engine.is_size_auto(root);
364 self.root_auto.insert(root, v);
365 v
366 }
367 };
368 for i in 0..self.constrained.len() {
370 let node = self.constrained[i].0;
371 let had_pin = self.constrained[i].2.is_some();
372 if !is_space_changed && !had_pin {
373 continue;
374 }
375 let style = self.constrained[i].1.clone();
376 self.engine.set_style(node, style).ok();
377 self.engine.mark_dirty(node).ok();
378 self.constrained[i].2 = None;
379 }
380 let mut did_fill_root = false;
381 if width_auto {
382 let w = match width {
383 AvailableSpace::Definite(w) => Some(w),
384 _ => None,
385 };
386 self.engine.set_width(root, w);
387 did_fill_root = true;
388 }
389 if height_auto {
390 let h = match height {
391 AvailableSpace::Definite(h) => Some(h),
392 _ => None,
393 };
394 self.engine.set_height(root, h);
395 did_fill_root = true;
396 }
397 if did_fill_root {
398 self.engine.mark_dirty(root).ok();
399 }
400 let mut dirty_nodes = Vec::new();
401 self.engine.collect_dirty_nodes(root, &mut dirty_nodes);
402 if dirty_nodes.is_empty() {
403 return Ok(Vec::new());
404 }
405 #[cfg(debug_assertions)]
406 {
407 assert!(
408 !self.is_computing,
409 "[rsx layout] cycle detected: compute_layout() called recursively. \
410 An effect is reading a layout signal and then calling compute_layout() again inside its body. \
411 This causes an infinite re-layout loop (capped by MAX_FLUSH_ITERATIONS). \
412 Move style mutations outside of layout-observing effects."
413 );
414 self.is_computing = true;
415 }
416 let (layout_root, layout_width, layout_height) =
417 self.find_boundary_root(&dirty_nodes, root, width, height);
418 self.engine
419 .compute_layout(layout_root, layout_width, layout_height)?;
420 let mut did_pin_any = false;
422 for i in 0..self.constrained.len() {
423 let node = self.constrained[i].0;
424 let style = self.constrained[i].1.clone();
425 let Some(max_w) = style.max_width_px() else {
426 continue;
427 };
428 if !self.is_in_subtree(node, layout_root) {
429 continue;
430 }
431 if let Ok(layout) = self.engine.layout(node) {
432 if layout.width > 0.0 && layout.width <= max_w + 0.5 {
433 self.engine.set_style(node, style.width(layout.width)).ok();
434 self.engine.mark_dirty(node).ok();
435 self.constrained[i].2 = Some(layout.width);
436 did_pin_any = true;
437 }
438 }
439 }
440 if did_pin_any {
441 self.engine
442 .compute_layout(layout_root, layout_width, layout_height)?;
443 }
444 let mut updates: Vec<(RwSignal<Rect>, Rect)> = Vec::new();
447 let is_window_walk = layout_root == root && !self.parents.contains_key(&root);
450 let mut abs_updates: Vec<(NodeId, f32, f32)> = Vec::new();
451 let registry = &self.registry;
452 let walk_result = self.engine.walk(layout_root, &mut |node_id, rect| {
453 if let Some(sig) = registry.get(&node_id) {
454 if sig.peek() != rect {
455 updates.push((sig.clone(), rect));
456 }
457 }
458 if is_window_walk {
459 abs_updates.push((node_id, rect.x, rect.y));
460 }
461 true
462 });
463 for (n, x, y) in abs_updates {
464 self.abs_pos.insert(n, (x, y));
465 }
466 #[cfg(debug_assertions)]
467 {
468 self.is_computing = false;
469 }
470 walk_result.map(|()| updates)
471 }
472
473 fn find_boundary_root(
474 &self,
475 dirty_nodes: &[NodeId],
476 global_root: NodeId,
477 global_width: AvailableSpace,
478 global_height: AvailableSpace,
479 ) -> (NodeId, AvailableSpace, AvailableSpace) {
480 let candidate = dirty_nodes
481 .iter()
482 .find_map(|&node| self.find_nearest_boundary(node));
483 match candidate {
484 Some((boundary, boundary_width, boundary_height))
485 if dirty_nodes.iter().all(|&n| self.is_in_subtree(n, boundary)) =>
486 {
487 (
488 boundary,
489 AvailableSpace::Definite(boundary_width),
490 AvailableSpace::Definite(boundary_height),
491 )
492 }
493 _ => (global_root, global_width, global_height),
494 }
495 }
496
497 fn find_nearest_boundary(&self, mut node: NodeId) -> Option<(NodeId, f32, f32)> {
498 loop {
499 if let Some(&(w, h)) = self.boundary_nodes.get(&node) {
500 return Some((node, w, h));
501 }
502 node = *self.parents.get(&node)?;
503 }
504 }
505
506 fn is_hidden_by_display(&self, mut node: NodeId) -> bool {
507 loop {
508 if self.engine.is_display_none(node) {
509 return true;
510 }
511 match self.parents.get(&node) {
512 Some(&parent) => node = parent,
513 None => return false,
514 }
515 }
516 }
517
518 fn is_in_subtree(&self, mut node: NodeId, ancestor: NodeId) -> bool {
519 loop {
520 if node == ancestor {
521 return true;
522 }
523 match self.parents.get(&node) {
524 Some(&parent) => node = parent,
525 None => return false,
526 }
527 }
528 }
529
530 pub(crate) fn track_layout(&self, node: NodeId) -> Option<RwSignal<Rect>> {
531 self.registry.get(&node).cloned()
532 }
533
534 fn set_children(&mut self, parent: NodeId, children: &[NodeId]) -> Result<(), LayoutError> {
535 self.engine.set_children(parent, children)?;
536 for &child in children {
537 self.parents.insert(child, parent);
538 }
539 self.engine.mark_dirty(parent).ok();
540 Ok(())
541 }
542
543 fn remove_node(&mut self, node: NodeId) {
544 self.engine.remove(node);
545 self.registry.remove(&node);
546 self.parents.remove(&node);
547 self.boundary_nodes.remove(&node);
548 self.last_space.remove(&node);
549 self.root_auto.remove(&node);
550 self.abs_pos.remove(&node);
551 self.constrained.retain(|(n, _, _)| *n != node);
552 }
553}
554
555#[cfg(test)]
556mod tests {
557 use geometry_core::Rect;
558 use layout_core::{LayoutStyle, SizeDimension};
559
560 use super::*;
561
562 #[test]
566 fn a_maxwidth_box_measures_its_content_at_the_capped_width() {
567 reset_layout_runtime();
568 const TOTAL: f32 = 1200.0;
569 const LINE: f32 = 20.0;
570 let measure: layout_core::MeasureFn = Box::new(|available: f32| {
572 let width = if available > 0.0 { available } else { TOTAL };
573 (width, (TOTAL / width).ceil() * LINE)
574 });
575 let (text, text_rect) = new_measured_leaf(LayoutStyle::new(), measure).unwrap();
576 let box_node =
577 new_container(LayoutStyle::new().flex_column().max_width(400.0), &[text]).unwrap();
578 let root = new_container(LayoutStyle::new().flex_column(), &[box_node]).unwrap();
579 let box_rect = track_layout(box_node).unwrap();
580
581 compute_layout(
582 root,
583 AvailableSpace::Definite(1000.0),
584 AvailableSpace::Definite(1000.0),
585 )
586 .unwrap();
587
588 assert_eq!(text_rect.get().width, 400.0);
589 assert_eq!(
590 box_rect.get().height,
591 3.0 * LINE,
592 "the box has to reserve the height its content takes at the width it was capped to"
593 );
594 }
595
596 #[test]
598 fn maxwidth_box_reserves_height_for_wrapped_content() {
599 reset_layout_runtime();
600 let mut items = Vec::new();
601 for _ in 0..4 {
602 let (n, _) = new_leaf(
603 LayoutStyle::new()
604 .width(200.0)
605 .height(100.0)
606 .min_width(200.0)
607 .flex_grow(1.0),
608 )
609 .unwrap();
610 items.push(n);
611 }
612 let row =
613 new_container(LayoutStyle::new().flex_row().flex_wrap().gap(24.0), &items).unwrap();
614 let boxed = new_container(
616 LayoutStyle::new()
617 .flex_column()
618 .width(SizeDimension::Percent(1.0))
619 .max_width(500.0),
620 &[row],
621 )
622 .unwrap();
623 let page = new_container(
624 LayoutStyle::new()
625 .flex_column()
626 .width(SizeDimension::Percent(1.0)),
627 &[boxed],
628 )
629 .unwrap();
630 compute_layout(
631 page,
632 AvailableSpace::Definite(900.0),
633 AvailableSpace::MaxContent,
634 )
635 .unwrap();
636 let box_rect = track_layout(boxed).unwrap().get();
637 let row_rect = track_layout(row).unwrap().get();
638 assert!(
639 (box_rect.width - 500.0).abs() < 1.0,
640 "box not capped: {box_rect:?}"
641 );
642 assert!(
643 row_rect.height >= 200.0,
644 "row did not wrap to 2 lines: {row_rect:?}"
645 );
646 assert!(
647 box_rect.height >= row_rect.height - 0.5,
648 "box too short for wrapped content: box={box_rect:?} row={row_rect:?}"
649 );
650 }
651
652 #[test]
655 fn wrapped_flex_row_reserves_height_for_all_lines() {
656 reset_layout_runtime();
657 let mut cards = Vec::new();
658 for _ in 0..4 {
659 let (n, _) = new_leaf(
660 LayoutStyle::new()
661 .min_width(260.0)
662 .height(100.0)
663 .flex_grow(1.0),
664 )
665 .unwrap();
666 cards.push(n);
667 }
668 let row =
669 new_container(LayoutStyle::new().flex_row().flex_wrap().gap(24.0), &cards).unwrap();
670 let (marker, _) = new_leaf(LayoutStyle::new().height(50.0)).unwrap();
671 let col =
672 new_container(LayoutStyle::new().flex_column().gap(20.0), &[row, marker]).unwrap();
673 compute_layout(
675 col,
676 AvailableSpace::Definite(900.0),
677 AvailableSpace::MaxContent,
678 )
679 .unwrap();
680 let row_rect = track_layout(row).unwrap().get();
681 let marker_rect = track_layout(marker).unwrap().get();
682 assert!(
683 row_rect.height >= 220.0,
684 "wrapped row height {} should cover 2 lines (~224)",
685 row_rect.height
686 );
687 assert!(
688 marker_rect.y >= row_rect.y + row_rect.height - 0.5,
689 "marker overlaps wrapped row: row.y={} row.h={} marker.y={}",
690 row_rect.y,
691 row_rect.height,
692 marker_rect.y
693 );
694 }
695
696 #[test]
699 fn wrapped_content_sized_cards_reserve_height() {
700 reset_layout_runtime();
701 let mut cards = Vec::new();
702 for _ in 0..4 {
703 let (inner, _) = new_leaf(LayoutStyle::new().height(100.0)).unwrap();
704 let card = new_container(
705 LayoutStyle::new()
706 .flex_column()
707 .min_width(260.0)
708 .flex_grow(1.0),
709 &[inner],
710 )
711 .unwrap();
712 cards.push(card);
713 }
714 let row =
715 new_container(LayoutStyle::new().flex_row().flex_wrap().gap(24.0), &cards).unwrap();
716 let (marker, _) = new_leaf(LayoutStyle::new().height(50.0)).unwrap();
717 let col =
718 new_container(LayoutStyle::new().flex_column().gap(20.0), &[row, marker]).unwrap();
719 compute_layout(
720 col,
721 AvailableSpace::Definite(900.0),
722 AvailableSpace::MaxContent,
723 )
724 .unwrap();
725 let row_rect = track_layout(row).unwrap().get();
726 let marker_rect = track_layout(marker).unwrap().get();
727 assert!(
728 row_rect.height >= 220.0,
729 "wrapped content-sized row height {} should cover 2 lines (~224)",
730 row_rect.height
731 );
732 assert!(
733 marker_rect.y >= row_rect.y + row_rect.height - 0.5,
734 "marker overlaps row: row.y={} row.h={} marker.y={}",
735 row_rect.y,
736 row_rect.height,
737 marker_rect.y
738 );
739 }
740
741 #[test]
743 fn maxwidth_box_stable_across_recompute() {
744 reset_layout_runtime();
745 let mut items = Vec::new();
746 for _ in 0..4 {
747 let (n, _) = new_leaf(
748 LayoutStyle::new()
749 .width(200.0)
750 .height(100.0)
751 .min_width(200.0)
752 .flex_grow(1.0),
753 )
754 .unwrap();
755 items.push(n);
756 }
757 let row =
758 new_container(LayoutStyle::new().flex_row().flex_wrap().gap(24.0), &items).unwrap();
759 let boxed = new_container(
760 LayoutStyle::new()
761 .flex_column()
762 .width(SizeDimension::Percent(1.0))
763 .max_width(500.0),
764 &[row],
765 )
766 .unwrap();
767 let page = new_container(
768 LayoutStyle::new()
769 .flex_column()
770 .width(SizeDimension::Percent(1.0)),
771 &[boxed],
772 )
773 .unwrap();
774
775 let space = (AvailableSpace::Definite(900.0), AvailableSpace::MaxContent);
776 compute_layout(page, space.0, space.1).unwrap();
777 let first = track_layout(boxed).unwrap().get();
778
779 mark_dirty(page).unwrap();
781 compute_layout(page, space.0, space.1).unwrap();
782 let second = track_layout(boxed).unwrap().get();
783
784 assert!(
785 (second.width - 500.0).abs() < 1.0,
786 "box not capped on recompute: {second:?}"
787 );
788 assert!(
789 (first.width - second.width).abs() < 0.5 && (first.height - second.height).abs() < 0.5,
790 "box layout drifted across recompute: first={first:?} second={second:?}"
791 );
792 }
793
794 #[test]
796 fn hidden_maxwidth_box_stays_hidden_after_a_resize() {
797 reset_layout_runtime();
798 let mut items = Vec::new();
799 for _ in 0..4 {
800 let (n, _) = new_leaf(
801 LayoutStyle::new()
802 .width(200.0)
803 .height(100.0)
804 .min_width(200.0)
805 .flex_grow(1.0),
806 )
807 .unwrap();
808 items.push(n);
809 }
810 let row =
811 new_container(LayoutStyle::new().flex_row().flex_wrap().gap(24.0), &items).unwrap();
812 let boxed = new_container(
813 LayoutStyle::new()
814 .flex_column()
815 .width(SizeDimension::Percent(1.0))
816 .max_width(500.0),
817 &[row],
818 )
819 .unwrap();
820 let page = new_container(
821 LayoutStyle::new()
822 .flex_column()
823 .width(SizeDimension::Percent(1.0)),
824 &[boxed],
825 )
826 .unwrap();
827
828 compute_layout(
829 page,
830 AvailableSpace::Definite(900.0),
831 AvailableSpace::MaxContent,
832 )
833 .unwrap();
834
835 set_display(boxed, false);
836 mark_dirty(page).unwrap();
837 assert!(is_hidden(boxed), "hidden right after set_display");
838
839 compute_layout(
841 page,
842 AvailableSpace::Definite(700.0),
843 AvailableSpace::MaxContent,
844 )
845 .unwrap();
846 assert!(
847 is_hidden(boxed),
848 "resize must not revert the out-of-band hide"
849 );
850 }
851
852 #[test]
854 fn auto_root_fills_definite_width() {
855 reset_layout_runtime();
856 let (child, _) = new_leaf(LayoutStyle::new().height(40.0)).unwrap();
857 let page = new_container(LayoutStyle::new().flex_column(), &[child]).unwrap();
859 compute_layout(
860 page,
861 AvailableSpace::Definite(1000.0),
862 AvailableSpace::MaxContent,
863 )
864 .unwrap();
865 let w = track_layout(page).unwrap().get().width;
866 assert!(
867 (w - 1000.0).abs() < 1.0,
868 "auto root did not fill width: {w}"
869 );
870 }
871
872 #[test]
874 fn hidden_child_collapses_to_zero_rect() {
875 reset_layout_runtime();
878 let (a, _) = new_leaf(LayoutStyle::new().width(50.0).height(30.0)).unwrap();
879 let (b, b_rect) = new_leaf(LayoutStyle::new().width(50.0).height(30.0)).unwrap();
880 let root = new_container(LayoutStyle::new().flex_column(), &[a, b]).unwrap();
881 compute_layout(
882 root,
883 AvailableSpace::Definite(200.0),
884 AvailableSpace::Definite(200.0),
885 )
886 .unwrap();
887 assert!(b_rect.get().height > 0.0, "b should start visible");
888
889 set_display(b, false);
890 mark_dirty(root).unwrap();
891 compute_layout(
892 root,
893 AvailableSpace::Definite(200.0),
894 AvailableSpace::Definite(200.0),
895 )
896 .unwrap();
897 let r = b_rect.get();
898 assert_eq!(
899 (r.width, r.height),
900 (0.0, 0.0),
901 "hidden child not collapsed: {r:?}"
902 );
903 }
904
905 #[test]
909 fn hidden_subtree_collapses_descendants() {
910 reset_layout_runtime();
911 let (grandchild, gc_rect) = new_leaf(LayoutStyle::new().width(40.0).height(20.0)).unwrap();
912 let section = new_container(LayoutStyle::new().flex_column(), &[grandchild]).unwrap();
913 let root = new_container(LayoutStyle::new().flex_column(), &[section]).unwrap();
914 compute_layout(
915 root,
916 AvailableSpace::Definite(200.0),
917 AvailableSpace::Definite(200.0),
918 )
919 .unwrap();
920 assert!(gc_rect.get().width > 0.0, "grandchild should start visible");
921
922 set_display(section, false);
923 mark_dirty(root).unwrap();
924 compute_layout(
925 root,
926 AvailableSpace::Definite(200.0),
927 AvailableSpace::Definite(200.0),
928 )
929 .unwrap();
930 let r = gc_rect.get();
931 assert_eq!(
932 (r.width, r.height),
933 (0.0, 0.0),
934 "descendant of hidden section not collapsed: {r:?}"
935 );
936 }
937
938 #[test]
939 fn auto_root_with_max_width_fills_capped() {
940 reset_layout_runtime();
941 let (child, _) = new_leaf(LayoutStyle::new().height(40.0)).unwrap();
942 let page =
943 new_container(LayoutStyle::new().flex_column().max_width(600.0), &[child]).unwrap();
944 compute_layout(
946 page,
947 AvailableSpace::Definite(1000.0),
948 AvailableSpace::MaxContent,
949 )
950 .unwrap();
951 let w = track_layout(page).unwrap().get().width;
952 assert!((w - 600.0).abs() < 1.0, "capped fill failed: {w}");
953 compute_layout(
955 page,
956 AvailableSpace::Definite(400.0),
957 AvailableSpace::MaxContent,
958 )
959 .unwrap();
960 let w = track_layout(page).unwrap().get().width;
961 assert!((w - 400.0).abs() < 1.0, "sub-cap fill failed: {w}");
962 }
963
964 #[test]
966 fn centered_capped_column_tracks_width() {
967 reset_layout_runtime();
968 let (child, _) = new_leaf(LayoutStyle::new().height(40.0)).unwrap();
969 let inner = new_container(
970 LayoutStyle::new()
971 .flex_column()
972 .width(SizeDimension::Percent(1.0))
973 .max_width(960.0),
974 &[child],
975 )
976 .unwrap();
977 let outer = new_container(
978 LayoutStyle::new()
979 .flex_column()
980 .align_items(layout_core::AlignItems::CENTER),
981 &[inner],
982 )
983 .unwrap();
984 let inner_rect = track_layout(inner).unwrap();
985 let outer_rect = track_layout(outer).unwrap();
986 compute_layout(
988 outer,
989 AvailableSpace::Definite(1400.0),
990 AvailableSpace::MaxContent,
991 )
992 .unwrap();
993 assert!(
994 (outer_rect.get().width - 1400.0).abs() < 1.0,
995 "outer fill: {}",
996 outer_rect.get().width
997 );
998 assert!(
999 (inner_rect.get().width - 960.0).abs() < 1.0,
1000 "inner cap: {}",
1001 inner_rect.get().width
1002 );
1003 assert!(
1004 (inner_rect.get().x - 220.0).abs() < 1.0,
1005 "inner centered: {}",
1006 inner_rect.get().x
1007 );
1008 compute_layout(
1010 outer,
1011 AvailableSpace::Definite(700.0),
1012 AvailableSpace::MaxContent,
1013 )
1014 .unwrap();
1015 assert!(
1016 (inner_rect.get().width - 700.0).abs() < 1.0,
1017 "inner tracks narrow: {}",
1018 inner_rect.get().width
1019 );
1020 assert!(
1021 inner_rect.get().x.abs() < 1.0,
1022 "no margin when full: {}",
1023 inner_rect.get().x
1024 );
1025 }
1026
1027 #[test]
1030 fn set_min_height_grows_short_measured_leaf() {
1031 reset_layout_runtime();
1032 let (leaf, rect) = new_measured_leaf(
1034 LayoutStyle::new().width(SizeDimension::Percent(1.0)),
1035 Box::new(|_w| (0.0, 20.0)),
1036 )
1037 .unwrap();
1038 let root = new_container(
1039 LayoutStyle::new()
1040 .flex_column()
1041 .width(SizeDimension::Percent(1.0)),
1042 &[leaf],
1043 )
1044 .unwrap();
1045 let space = (AvailableSpace::Definite(300.0), AvailableSpace::MaxContent);
1046 compute_layout(root, space.0, space.1).unwrap();
1047 assert!(
1048 (rect.get().height - 20.0).abs() < 0.5,
1049 "starts at its content height: {:?}",
1050 rect.get()
1051 );
1052
1053 set_min_height(leaf, 200.0);
1055 compute_layout(root, space.0, space.1).unwrap();
1056 assert!(
1057 (rect.get().height - 200.0).abs() < 0.5,
1058 "min_height fills the short leaf: {:?}",
1059 rect.get()
1060 );
1061
1062 set_min_height(leaf, 0.0);
1064 compute_layout(root, space.0, space.1).unwrap();
1065 assert!(
1066 (rect.get().height - 20.0).abs() < 0.5,
1067 "a zero floor restores the content height: {:?}",
1068 rect.get()
1069 );
1070 }
1071
1072 #[test]
1074 fn min_height_survives_a_later_set_layout_style() {
1075 reset_layout_runtime();
1076 let (leaf, rect) = new_measured_leaf(
1077 LayoutStyle::new().width(SizeDimension::Percent(1.0)),
1078 Box::new(|_w| (0.0, 20.0)),
1079 )
1080 .unwrap();
1081 let root = new_container(
1082 LayoutStyle::new()
1083 .flex_column()
1084 .width(SizeDimension::Percent(1.0)),
1085 &[leaf],
1086 )
1087 .unwrap();
1088 let space = (AvailableSpace::Definite(300.0), AvailableSpace::MaxContent);
1089 compute_layout(root, space.0, space.1).unwrap();
1090
1091 set_min_height(leaf, 200.0);
1092 compute_layout(root, space.0, space.1).unwrap();
1093 assert!(
1094 (rect.get().height - 200.0).abs() < 0.5,
1095 "min_height applied: {:?}",
1096 rect.get()
1097 );
1098
1099 set_layout_style(leaf, LayoutStyle::new().width(SizeDimension::Percent(1.0))).unwrap();
1101 compute_layout(root, space.0, space.1).unwrap();
1102 assert!(
1103 (rect.get().height - 200.0).abs() < 0.5,
1104 "min_height survives a later set_layout_style: {:?}",
1105 rect.get()
1106 );
1107 }
1108
1109 #[test]
1110 fn ctx_register_leaf_returns_ok() {
1111 reset_layout_runtime();
1112 let result = new_leaf(LayoutStyle::new());
1113 assert!(result.is_ok());
1114 }
1115
1116 #[test]
1117 fn ctx_new_container_returns_ok() {
1118 reset_layout_runtime();
1119 let leaf_result = new_leaf(LayoutStyle::new());
1120 assert!(leaf_result.is_ok());
1121 let (leaf, _) = leaf_result.unwrap();
1122 let container_result = new_container(LayoutStyle::new(), &[leaf]);
1123 assert!(container_result.is_ok());
1124 }
1125
1126 #[test]
1127 fn ctx_register_leaf_returns_zero_rect() {
1128 reset_layout_runtime();
1129 let (_node, rect) = new_leaf(LayoutStyle::new()).unwrap();
1130 assert_eq!(rect.get(), Rect::default());
1131 }
1132
1133 #[test]
1134 fn ctx_compute_updates_rect() {
1135 reset_layout_runtime();
1136 let (leaf, rect) = new_leaf(LayoutStyle::new().width(100.0).height(50.0)).unwrap();
1137 let root = new_container(
1138 LayoutStyle::new().flex_row().width(200.0).height(100.0),
1139 &[leaf],
1140 )
1141 .unwrap();
1142 compute_layout(
1143 root,
1144 AvailableSpace::Definite(200.0),
1145 AvailableSpace::Definite(100.0),
1146 )
1147 .unwrap();
1148 assert_eq!(rect.get().width, 100.0);
1149 assert_eq!(rect.get().height, 50.0);
1150 }
1151
1152 #[test]
1153 fn setting_the_direction_signal_reaches_the_engine_on_the_next_layout_pass() {
1154 reset_layout_runtime();
1156 crate::set_direction(layout_core::Direction::Ltr);
1157 let (first, first_rect) = new_leaf(LayoutStyle::new().width(40.0).height(10.0)).unwrap();
1158 let (second, second_rect) = new_leaf(LayoutStyle::new().width(40.0).height(10.0)).unwrap();
1159 let root = new_container(
1160 LayoutStyle::new().flex_row().width(200.0).height(100.0),
1161 &[first, second],
1162 )
1163 .unwrap();
1164 let space = || {
1165 (
1166 AvailableSpace::Definite(200.0),
1167 AvailableSpace::Definite(100.0),
1168 )
1169 };
1170 let (w, h) = space();
1171 compute_layout(root, w, h).unwrap();
1172 assert_eq!(first_rect.get().x, 0.0);
1173 assert_eq!(second_rect.get().x, 40.0);
1174
1175 crate::set_direction(layout_core::Direction::Rtl);
1176 mark_dirty(root).unwrap();
1177 let (w, h) = space();
1178 compute_layout(root, w, h).unwrap();
1179 assert_eq!(first_rect.get().x, 160.0, "the row now starts at the right");
1180 assert_eq!(second_rect.get().x, 120.0);
1181 crate::set_direction(layout_core::Direction::Ltr);
1182 }
1183
1184 #[test]
1186 fn attached_overlay_fills_host_viewport_not_its_small_parent() {
1187 reset_layout_runtime();
1188 let (small, _) = new_leaf(LayoutStyle::new().width(50.0).height(50.0)).unwrap();
1190 let root = new_container(LayoutStyle::new().flex_column(), &[small]).unwrap();
1191 compute_layout(
1192 root,
1193 AvailableSpace::Definite(800.0),
1194 AvailableSpace::Definite(600.0),
1195 )
1196 .unwrap();
1197
1198 let (inner, inner_rect) = new_leaf(
1200 LayoutStyle::new()
1201 .width(SizeDimension::Percent(1.0))
1202 .height(SizeDimension::Percent(1.0)),
1203 )
1204 .unwrap();
1205 let content = new_container(LayoutStyle::new().absolute_fill(), &[inner]).unwrap();
1206 assert!(
1207 attach_overlay(content),
1208 "the host must be set after the first compute"
1209 );
1210 relayout_if_dirty();
1211
1212 let r = inner_rect.get();
1213 assert!(
1214 (r.width - 800.0).abs() < 0.5 && (r.height - 600.0).abs() < 0.5,
1215 "portal fills the viewport, not its 50px parent: {r:?}"
1216 );
1217
1218 detach_overlay(content);
1220 remove_node(content);
1221 relayout_if_dirty();
1222 }
1223
1224 #[test]
1229 fn absolute_rect_stays_window_absolute_across_a_separate_content_root() {
1230 reset_layout_runtime();
1231 let (sidebar, _) = new_leaf(LayoutStyle::new().width(248.0).height(600.0)).unwrap();
1232 let (trigger, trigger_sig) =
1233 new_leaf(LayoutStyle::new().width(120.0).height(30.0)).unwrap();
1234 let content =
1235 new_container(LayoutStyle::new().flex_column().flex_grow(1.0), &[trigger]).unwrap();
1236 let root = new_container(LayoutStyle::new().flex_row(), &[sidebar, content]).unwrap();
1237 compute_layout(
1238 root,
1239 AvailableSpace::Definite(1000.0),
1240 AvailableSpace::Definite(600.0),
1241 )
1242 .unwrap();
1243 set_overlay_host(root);
1244 assert!(
1246 (absolute_rect(trigger).unwrap().x - 248.0).abs() < 1.0,
1247 "abs x should be past the 248px sidebar: {:?}",
1248 absolute_rect(trigger)
1249 );
1250
1251 mark_dirty(content).unwrap();
1253 compute_layout(
1254 content,
1255 AvailableSpace::Definite(752.0),
1256 AvailableSpace::MaxContent,
1257 )
1258 .unwrap();
1259 assert!(
1260 trigger_sig.get().x < 1.0,
1261 "the rect signal is now content-local (~0): {:?}",
1262 trigger_sig.get()
1263 );
1264 assert!(
1266 (absolute_rect(trigger).unwrap().x - 248.0).abs() < 1.0,
1267 "absolute_rect must stay window-absolute across the sub-root compute: {:?}",
1268 absolute_rect(trigger)
1269 );
1270 }
1271}