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(
41 root: NodeId,
42 width: AvailableSpace,
43 height: AvailableSpace,
44) -> Result<(), LayoutError> {
45 compute_layout_root(root, width, height)
46}
47
48pub fn compute_layout_root(
53 root: NodeId,
54 width: AvailableSpace,
55 height: AvailableSpace,
56) -> Result<(), LayoutError> {
57 let direction = crate::direction::current_direction();
59 with_runtime(|rt| rt.engine.set_direction(direction));
60 let updates = with_runtime(|rt| rt.compute_layout(root, width, height))?;
61 batch(|| {
62 for (sig, rect) in updates {
63 if sig.peek() != rect {
64 sig.set(rect);
65 }
66 }
67 });
68 Ok(())
69}
70
71pub fn relayout_if_dirty() {
78 let roots: Vec<(NodeId, AvailableSpace, AvailableSpace)> = with_runtime(|rt| {
79 rt.last_space
80 .iter()
81 .map(|(&n, &(w, h))| (n, w, h))
82 .collect()
83 });
84 for (root, width, height) in roots {
85 let _ = compute_layout_root(root, width, height);
86 }
87}
88
89pub fn track_layout(node: NodeId) -> Option<RwSignal<Rect>> {
90 with_runtime(|rt| rt.track_layout(node))
91}
92
93pub fn absolute_rect(node: NodeId) -> Option<Rect> {
102 with_runtime(|rt| {
103 let &(x, y) = rt.abs_pos.get(&node)?;
104 let size = rt.registry.get(&node).map(|s| s.peek()).unwrap_or_default();
105 Some(Rect::new(x, y, size.width, size.height))
106 })
107}
108
109pub fn is_descendant_of(node: NodeId, ancestor: NodeId) -> bool {
112 with_runtime(|rt| rt.is_in_subtree(node, ancestor))
113}
114
115pub fn mark_dirty(node: NodeId) -> Result<(), LayoutError> {
116 with_runtime(|rt| rt.mark_dirty(node))
117}
118
119pub fn set_layout_style(node: NodeId, style: LayoutStyle) -> Result<(), LayoutError> {
125 with_runtime(|rt| {
126 rt.engine.set_style(node, style)?;
127 rt.mark_dirty(node)
128 })
129}
130
131pub fn set_display(node: NodeId, visible: bool) {
133 with_runtime(|rt| rt.set_display(node, visible))
134}
135
136pub fn container_is_row(node: NodeId) -> bool {
139 with_runtime(|rt| rt.engine.is_row(node))
140}
141
142pub fn set_leading_margin(node: NodeId, is_row: bool, px: f32) {
145 with_runtime(|rt| rt.engine.set_leading_margin(node, is_row, px))
146}
147
148pub fn set_min_height(node: NodeId, px: f32) {
152 with_runtime(|rt| rt.engine.set_min_height(node, Some(px)))
153}
154
155pub fn set_children(parent: NodeId, children: &[NodeId]) -> Result<(), LayoutError> {
158 with_runtime(|rt| rt.set_children(parent, children))
159}
160
161pub fn remove_node(node: NodeId) {
165 with_runtime(|rt| rt.remove_node(node))
166}
167
168pub fn set_overlay_host(node: NodeId) {
174 with_runtime(|rt| {
175 rt.overlay_host = Some(node);
176 rt.host_pinned = true;
177 });
178}
179
180pub fn attach_overlay(node: NodeId) -> bool {
186 with_runtime(|rt| {
187 let Some(host) = rt.overlay_host else {
188 return false;
189 };
190 if rt.engine.add_child(host, node).is_err() {
191 return false;
192 }
193 rt.parents.insert(node, host);
194 rt.engine.mark_dirty(host).ok();
195 true
196 })
197}
198
199pub fn detach_overlay(node: NodeId) {
202 with_runtime(|rt| {
203 if let Some(host) = rt.parents.remove(&node) {
207 rt.engine.remove_child(host, node).ok();
208 rt.engine.mark_dirty(host).ok();
209 }
210 });
211}
212
213struct LayoutRuntime {
214 engine: LayoutEngine,
215 registry: FxHashMap<NodeId, RwSignal<Rect>>,
216 parents: FxHashMap<NodeId, NodeId>,
217 boundary_nodes: FxHashMap<NodeId, (f32, f32)>,
218 last_space: FxHashMap<NodeId, (AvailableSpace, AvailableSpace)>,
220 constrained: Vec<(NodeId, LayoutStyle, Option<f32>)>,
222 root_auto: FxHashMap<NodeId, (bool, bool)>,
224 overlay_host: Option<NodeId>,
227 host_pinned: bool,
232 abs_pos: FxHashMap<NodeId, (f32, f32)>,
238 #[cfg(debug_assertions)]
240 is_computing: bool,
241}
242
243impl LayoutRuntime {
244 fn new() -> Self {
245 Self {
246 engine: LayoutEngine::new(),
247 registry: FxHashMap::default(),
248 parents: FxHashMap::default(),
249 boundary_nodes: FxHashMap::default(),
250 last_space: FxHashMap::default(),
251 constrained: Vec::new(),
252 root_auto: FxHashMap::default(),
253 overlay_host: None,
254 host_pinned: false,
255 abs_pos: FxHashMap::default(),
256 #[cfg(debug_assertions)]
257 is_computing: false,
258 }
259 }
260
261 fn track_constrained(&mut self, node: NodeId, style: &LayoutStyle) {
262 if style.max_width_px().is_some() {
263 self.constrained.push((node, style.clone(), None));
264 }
265 }
266
267 pub(crate) fn new_leaf(
268 &mut self,
269 style: LayoutStyle,
270 ) -> Result<(NodeId, RwSignal<Rect>), LayoutError> {
271 let node = self.engine.new_leaf(style.clone())?;
272 let signal = signal(Rect::default());
273 self.registry.insert(node, signal.clone());
274 if let Some(dimensions) = self.engine.is_fixed_size(node) {
275 self.boundary_nodes.insert(node, dimensions);
276 }
277 self.track_constrained(node, &style);
278 Ok((node, signal))
279 }
280
281 pub(crate) fn new_measured_leaf(
282 &mut self,
283 style: LayoutStyle,
284 measure: MeasureFn,
285 ) -> Result<(NodeId, RwSignal<Rect>), LayoutError> {
286 let node = self.engine.new_measured_leaf(style.clone(), measure)?;
287 let signal = signal(Rect::default());
288 self.registry.insert(node, signal.clone());
289 self.track_constrained(node, &style);
290 Ok((node, signal))
291 }
292
293 pub(crate) fn new_container(
294 &mut self,
295 style: LayoutStyle,
296 children: &[NodeId],
297 ) -> Result<NodeId, LayoutError> {
298 let node = self.engine.new_container(style.clone(), children)?;
299 let signal = signal(Rect::default());
300 self.registry.insert(node, signal);
301 for &child in children {
302 self.parents.insert(child, node);
303 }
304 if let Some(dimensions) = self.engine.is_fixed_size(node) {
305 self.boundary_nodes.insert(node, dimensions);
306 }
307 self.track_constrained(node, &style);
308 Ok(node)
309 }
310
311 fn compute_layout(
312 &mut self,
313 root: NodeId,
314 width: AvailableSpace,
315 height: AvailableSpace,
316 ) -> Result<Vec<(RwSignal<Rect>, Rect)>, LayoutError> {
317 if !self.host_pinned
324 && !self.parents.contains_key(&root)
325 && matches!(height, AvailableSpace::Definite(_))
326 {
327 self.overlay_host = Some(root);
328 }
329 let is_space_changed = self.last_space.get(&root) != Some(&(width, height));
331 if is_space_changed {
332 self.engine.mark_dirty(root).ok();
333 self.last_space.insert(root, (width, height));
334 } else if !self.engine.is_dirty(root) {
335 return Ok(Vec::new());
336 }
337 let (width_auto, height_auto) = match self.root_auto.get(&root).copied() {
339 Some(v) => v,
340 None => {
341 let v = self.engine.is_size_auto(root);
342 self.root_auto.insert(root, v);
343 v
344 }
345 };
346 for i in 0..self.constrained.len() {
348 let node = self.constrained[i].0;
349 let had_pin = self.constrained[i].2.is_some();
350 if !is_space_changed && !had_pin {
351 continue;
352 }
353 let style = self.constrained[i].1.clone();
354 self.engine.set_style(node, style).ok();
355 self.engine.mark_dirty(node).ok();
356 self.constrained[i].2 = None;
357 }
358 let mut did_fill_root = false;
359 if width_auto {
360 let w = match width {
361 AvailableSpace::Definite(w) => Some(w),
362 _ => None,
363 };
364 self.engine.set_width(root, w);
365 did_fill_root = true;
366 }
367 if height_auto {
368 let h = match height {
369 AvailableSpace::Definite(h) => Some(h),
370 _ => None,
371 };
372 self.engine.set_height(root, h);
373 did_fill_root = true;
374 }
375 if did_fill_root {
376 self.engine.mark_dirty(root).ok();
377 }
378 let mut dirty_nodes = Vec::new();
379 self.engine.collect_dirty_nodes(root, &mut dirty_nodes);
380 if dirty_nodes.is_empty() {
381 return Ok(Vec::new());
382 }
383 #[cfg(debug_assertions)]
384 {
385 assert!(
386 !self.is_computing,
387 "[rsx layout] cycle detected: compute_layout() called recursively. \
388 An effect is reading a layout signal and then calling compute_layout() again inside its body. \
389 This causes an infinite re-layout loop (capped by MAX_FLUSH_ITERATIONS). \
390 Move style mutations outside of layout-observing effects."
391 );
392 self.is_computing = true;
393 }
394 let (layout_root, layout_width, layout_height) =
395 self.find_boundary_root(&dirty_nodes, root, width, height);
396 self.engine
397 .compute_layout(layout_root, layout_width, layout_height)?;
398 let mut did_pin_any = false;
400 for i in 0..self.constrained.len() {
401 let node = self.constrained[i].0;
402 let style = self.constrained[i].1.clone();
403 let Some(max_w) = style.max_width_px() else {
404 continue;
405 };
406 if !self.is_in_subtree(node, layout_root) {
407 continue;
408 }
409 if let Ok(layout) = self.engine.layout(node) {
410 if layout.width > 0.0 && layout.width <= max_w + 0.5 {
411 self.engine.set_style(node, style.width(layout.width)).ok();
412 self.engine.mark_dirty(node).ok();
413 self.constrained[i].2 = Some(layout.width);
414 did_pin_any = true;
415 }
416 }
417 }
418 if did_pin_any {
419 self.engine
420 .compute_layout(layout_root, layout_width, layout_height)?;
421 }
422 let mut updates: Vec<(RwSignal<Rect>, Rect)> = Vec::new();
425 let is_window_walk = layout_root == root && !self.parents.contains_key(&root);
428 let mut abs_updates: Vec<(NodeId, f32, f32)> = Vec::new();
429 let registry = &self.registry;
430 let walk_result = self.engine.walk(layout_root, &mut |node_id, rect| {
431 if let Some(sig) = registry.get(&node_id) {
432 if sig.peek() != rect {
433 updates.push((sig.clone(), rect));
434 }
435 }
436 if is_window_walk {
437 abs_updates.push((node_id, rect.x, rect.y));
438 }
439 true
440 });
441 for (n, x, y) in abs_updates {
442 self.abs_pos.insert(n, (x, y));
443 }
444 #[cfg(debug_assertions)]
445 {
446 self.is_computing = false;
447 }
448 walk_result.map(|()| updates)
449 }
450
451 fn find_boundary_root(
452 &self,
453 dirty_nodes: &[NodeId],
454 global_root: NodeId,
455 global_width: AvailableSpace,
456 global_height: AvailableSpace,
457 ) -> (NodeId, AvailableSpace, AvailableSpace) {
458 let candidate = dirty_nodes
459 .iter()
460 .find_map(|&node| self.find_nearest_boundary(node));
461 match candidate {
462 Some((boundary, boundary_width, boundary_height))
463 if dirty_nodes.iter().all(|&n| self.is_in_subtree(n, boundary)) =>
464 {
465 (
466 boundary,
467 AvailableSpace::Definite(boundary_width),
468 AvailableSpace::Definite(boundary_height),
469 )
470 }
471 _ => (global_root, global_width, global_height),
472 }
473 }
474
475 fn find_nearest_boundary(&self, mut node: NodeId) -> Option<(NodeId, f32, f32)> {
476 loop {
477 if let Some(&(w, h)) = self.boundary_nodes.get(&node) {
478 return Some((node, w, h));
479 }
480 node = *self.parents.get(&node)?;
481 }
482 }
483
484 fn is_in_subtree(&self, mut node: NodeId, ancestor: NodeId) -> bool {
485 loop {
486 if node == ancestor {
487 return true;
488 }
489 match self.parents.get(&node) {
490 Some(&parent) => node = parent,
491 None => return false,
492 }
493 }
494 }
495
496 pub(crate) fn track_layout(&self, node: NodeId) -> Option<RwSignal<Rect>> {
497 self.registry.get(&node).cloned()
498 }
499
500 pub(crate) fn mark_dirty(&mut self, node: NodeId) -> Result<(), LayoutError> {
501 self.engine.mark_dirty(node)
502 }
503
504 pub(crate) fn set_display(&mut self, node: NodeId, visible: bool) {
505 self.engine.set_display(node, visible);
506 }
507
508 fn set_children(&mut self, parent: NodeId, children: &[NodeId]) -> Result<(), LayoutError> {
509 self.engine.set_children(parent, children)?;
510 for &child in children {
511 self.parents.insert(child, parent);
512 }
513 self.engine.mark_dirty(parent).ok();
514 Ok(())
515 }
516
517 fn remove_node(&mut self, node: NodeId) {
518 self.engine.remove(node);
519 self.registry.remove(&node);
520 self.parents.remove(&node);
521 self.boundary_nodes.remove(&node);
522 self.last_space.remove(&node);
523 self.root_auto.remove(&node);
524 self.abs_pos.remove(&node);
525 self.constrained.retain(|(n, _, _)| *n != node);
526 }
527}
528
529#[cfg(test)]
530mod tests {
531 use geometry_core::Rect;
532 use layout_core::{LayoutStyle, SizeDimension};
533
534 use super::*;
535
536 #[test]
538 fn maxwidth_box_reserves_height_for_wrapped_content() {
539 reset_layout_runtime();
540 let mut items = Vec::new();
541 for _ in 0..4 {
542 let (n, _) = new_leaf(
543 LayoutStyle::new()
544 .width(200.0)
545 .height(100.0)
546 .min_width(200.0)
547 .flex_grow(1.0),
548 )
549 .unwrap();
550 items.push(n);
551 }
552 let row =
553 new_container(LayoutStyle::new().flex_row().flex_wrap().gap(24.0), &items).unwrap();
554 let boxed = new_container(
556 LayoutStyle::new()
557 .flex_column()
558 .width(SizeDimension::Percent(1.0))
559 .max_width(500.0),
560 &[row],
561 )
562 .unwrap();
563 let page = new_container(
564 LayoutStyle::new()
565 .flex_column()
566 .width(SizeDimension::Percent(1.0)),
567 &[boxed],
568 )
569 .unwrap();
570 compute_layout(
571 page,
572 AvailableSpace::Definite(900.0),
573 AvailableSpace::MaxContent,
574 )
575 .unwrap();
576 let box_rect = track_layout(boxed).unwrap().get();
577 let row_rect = track_layout(row).unwrap().get();
578 assert!(
579 (box_rect.width - 500.0).abs() < 1.0,
580 "box not capped: {box_rect:?}"
581 );
582 assert!(
583 row_rect.height >= 200.0,
584 "row did not wrap to 2 lines: {row_rect:?}"
585 );
586 assert!(
587 box_rect.height >= row_rect.height - 0.5,
588 "box too short for wrapped content: box={box_rect:?} row={row_rect:?}"
589 );
590 }
591
592 #[test]
594 fn maxwidth_box_stable_across_recompute() {
595 reset_layout_runtime();
596 let mut items = Vec::new();
597 for _ in 0..4 {
598 let (n, _) = new_leaf(
599 LayoutStyle::new()
600 .width(200.0)
601 .height(100.0)
602 .min_width(200.0)
603 .flex_grow(1.0),
604 )
605 .unwrap();
606 items.push(n);
607 }
608 let row =
609 new_container(LayoutStyle::new().flex_row().flex_wrap().gap(24.0), &items).unwrap();
610 let boxed = new_container(
611 LayoutStyle::new()
612 .flex_column()
613 .width(SizeDimension::Percent(1.0))
614 .max_width(500.0),
615 &[row],
616 )
617 .unwrap();
618 let page = new_container(
619 LayoutStyle::new()
620 .flex_column()
621 .width(SizeDimension::Percent(1.0)),
622 &[boxed],
623 )
624 .unwrap();
625
626 let space = (AvailableSpace::Definite(900.0), AvailableSpace::MaxContent);
627 compute_layout(page, space.0, space.1).unwrap();
628 let first = track_layout(boxed).unwrap().get();
629
630 mark_dirty(page).unwrap();
632 compute_layout(page, space.0, space.1).unwrap();
633 let second = track_layout(boxed).unwrap().get();
634
635 assert!(
636 (second.width - 500.0).abs() < 1.0,
637 "box not capped on recompute: {second:?}"
638 );
639 assert!(
640 (first.width - second.width).abs() < 0.5 && (first.height - second.height).abs() < 0.5,
641 "box layout drifted across recompute: first={first:?} second={second:?}"
642 );
643 }
644
645 #[test]
647 fn auto_root_fills_definite_width() {
648 reset_layout_runtime();
649 let (child, _) = new_leaf(LayoutStyle::new().height(40.0)).unwrap();
650 let page = new_container(LayoutStyle::new().flex_column(), &[child]).unwrap();
652 compute_layout(
653 page,
654 AvailableSpace::Definite(1000.0),
655 AvailableSpace::MaxContent,
656 )
657 .unwrap();
658 let w = track_layout(page).unwrap().get().width;
659 assert!(
660 (w - 1000.0).abs() < 1.0,
661 "auto root did not fill width: {w}"
662 );
663 }
664
665 #[test]
667 fn hidden_child_collapses_to_zero_rect() {
668 reset_layout_runtime();
671 let (a, _) = new_leaf(LayoutStyle::new().width(50.0).height(30.0)).unwrap();
672 let (b, b_rect) = new_leaf(LayoutStyle::new().width(50.0).height(30.0)).unwrap();
673 let root = new_container(LayoutStyle::new().flex_column(), &[a, b]).unwrap();
674 compute_layout(
675 root,
676 AvailableSpace::Definite(200.0),
677 AvailableSpace::Definite(200.0),
678 )
679 .unwrap();
680 assert!(b_rect.get().height > 0.0, "b should start visible");
681
682 set_display(b, false);
683 mark_dirty(root).unwrap();
684 compute_layout(
685 root,
686 AvailableSpace::Definite(200.0),
687 AvailableSpace::Definite(200.0),
688 )
689 .unwrap();
690 let r = b_rect.get();
691 assert_eq!(
692 (r.width, r.height),
693 (0.0, 0.0),
694 "hidden child not collapsed: {r:?}"
695 );
696 }
697
698 #[test]
702 fn hidden_subtree_collapses_descendants() {
703 reset_layout_runtime();
704 let (grandchild, gc_rect) = new_leaf(LayoutStyle::new().width(40.0).height(20.0)).unwrap();
705 let section = new_container(LayoutStyle::new().flex_column(), &[grandchild]).unwrap();
706 let root = new_container(LayoutStyle::new().flex_column(), &[section]).unwrap();
707 compute_layout(
708 root,
709 AvailableSpace::Definite(200.0),
710 AvailableSpace::Definite(200.0),
711 )
712 .unwrap();
713 assert!(gc_rect.get().width > 0.0, "grandchild should start visible");
714
715 set_display(section, false);
716 mark_dirty(root).unwrap();
717 compute_layout(
718 root,
719 AvailableSpace::Definite(200.0),
720 AvailableSpace::Definite(200.0),
721 )
722 .unwrap();
723 let r = gc_rect.get();
724 assert_eq!(
725 (r.width, r.height),
726 (0.0, 0.0),
727 "descendant of hidden section not collapsed: {r:?}"
728 );
729 }
730
731 #[test]
732 fn auto_root_with_max_width_fills_capped() {
733 reset_layout_runtime();
734 let (child, _) = new_leaf(LayoutStyle::new().height(40.0)).unwrap();
735 let page =
736 new_container(LayoutStyle::new().flex_column().max_width(600.0), &[child]).unwrap();
737 compute_layout(
739 page,
740 AvailableSpace::Definite(1000.0),
741 AvailableSpace::MaxContent,
742 )
743 .unwrap();
744 let w = track_layout(page).unwrap().get().width;
745 assert!((w - 600.0).abs() < 1.0, "capped fill failed: {w}");
746 compute_layout(
748 page,
749 AvailableSpace::Definite(400.0),
750 AvailableSpace::MaxContent,
751 )
752 .unwrap();
753 let w = track_layout(page).unwrap().get().width;
754 assert!((w - 400.0).abs() < 1.0, "sub-cap fill failed: {w}");
755 }
756
757 #[test]
759 fn centered_capped_column_tracks_width() {
760 reset_layout_runtime();
761 let (child, _) = new_leaf(LayoutStyle::new().height(40.0)).unwrap();
762 let inner = new_container(
763 LayoutStyle::new()
764 .flex_column()
765 .width(SizeDimension::Percent(1.0))
766 .max_width(960.0),
767 &[child],
768 )
769 .unwrap();
770 let outer = new_container(
771 LayoutStyle::new()
772 .flex_column()
773 .align_items(layout_core::AlignItems::CENTER),
774 &[inner],
775 )
776 .unwrap();
777 let inner_rect = track_layout(inner).unwrap();
778 let outer_rect = track_layout(outer).unwrap();
779 compute_layout(
781 outer,
782 AvailableSpace::Definite(1400.0),
783 AvailableSpace::MaxContent,
784 )
785 .unwrap();
786 assert!(
787 (outer_rect.get().width - 1400.0).abs() < 1.0,
788 "outer fill: {}",
789 outer_rect.get().width
790 );
791 assert!(
792 (inner_rect.get().width - 960.0).abs() < 1.0,
793 "inner cap: {}",
794 inner_rect.get().width
795 );
796 assert!(
797 (inner_rect.get().x - 220.0).abs() < 1.0,
798 "inner centered: {}",
799 inner_rect.get().x
800 );
801 compute_layout(
803 outer,
804 AvailableSpace::Definite(700.0),
805 AvailableSpace::MaxContent,
806 )
807 .unwrap();
808 assert!(
809 (inner_rect.get().width - 700.0).abs() < 1.0,
810 "inner tracks narrow: {}",
811 inner_rect.get().width
812 );
813 assert!(
814 inner_rect.get().x.abs() < 1.0,
815 "no margin when full: {}",
816 inner_rect.get().x
817 );
818 }
819
820 #[test]
823 fn set_min_height_grows_short_measured_leaf() {
824 reset_layout_runtime();
825 let (leaf, rect) = new_measured_leaf(
827 LayoutStyle::new().width(SizeDimension::Percent(1.0)),
828 Box::new(|_w| (0.0, 20.0)),
829 )
830 .unwrap();
831 let root = new_container(
832 LayoutStyle::new()
833 .flex_column()
834 .width(SizeDimension::Percent(1.0)),
835 &[leaf],
836 )
837 .unwrap();
838 let space = (AvailableSpace::Definite(300.0), AvailableSpace::MaxContent);
839 compute_layout(root, space.0, space.1).unwrap();
840 assert!(
841 (rect.get().height - 20.0).abs() < 0.5,
842 "starts at its content height: {:?}",
843 rect.get()
844 );
845
846 set_min_height(leaf, 200.0);
848 compute_layout(root, space.0, space.1).unwrap();
849 assert!(
850 (rect.get().height - 200.0).abs() < 0.5,
851 "min_height fills the short leaf: {:?}",
852 rect.get()
853 );
854
855 set_min_height(leaf, 0.0);
857 compute_layout(root, space.0, space.1).unwrap();
858 assert!(
859 (rect.get().height - 20.0).abs() < 0.5,
860 "a zero floor restores the content height: {:?}",
861 rect.get()
862 );
863 }
864
865 #[test]
866 fn ctx_register_leaf_returns_ok() {
867 reset_layout_runtime();
868 let result = new_leaf(LayoutStyle::new());
869 assert!(result.is_ok());
870 }
871
872 #[test]
873 fn ctx_new_container_returns_ok() {
874 reset_layout_runtime();
875 let leaf_result = new_leaf(LayoutStyle::new());
876 assert!(leaf_result.is_ok());
877 let (leaf, _) = leaf_result.unwrap();
878 let container_result = new_container(LayoutStyle::new(), &[leaf]);
879 assert!(container_result.is_ok());
880 }
881
882 #[test]
883 fn ctx_register_leaf_returns_zero_rect() {
884 reset_layout_runtime();
885 let (_node, rect) = new_leaf(LayoutStyle::new()).unwrap();
886 assert_eq!(rect.get(), Rect::default());
887 }
888
889 #[test]
890 fn ctx_compute_updates_rect() {
891 reset_layout_runtime();
892 let (leaf, rect) = new_leaf(LayoutStyle::new().width(100.0).height(50.0)).unwrap();
893 let root = new_container(
894 LayoutStyle::new().flex_row().width(200.0).height(100.0),
895 &[leaf],
896 )
897 .unwrap();
898 compute_layout(
899 root,
900 AvailableSpace::Definite(200.0),
901 AvailableSpace::Definite(100.0),
902 )
903 .unwrap();
904 assert_eq!(rect.get().width, 100.0);
905 assert_eq!(rect.get().height, 50.0);
906 }
907
908 #[test]
909 fn setting_the_direction_signal_reaches_the_engine_on_the_next_layout_pass() {
910 reset_layout_runtime();
912 crate::set_direction(layout_core::Direction::Ltr);
913 let (first, first_rect) = new_leaf(LayoutStyle::new().width(40.0).height(10.0)).unwrap();
914 let (second, second_rect) = new_leaf(LayoutStyle::new().width(40.0).height(10.0)).unwrap();
915 let root = new_container(
916 LayoutStyle::new().flex_row().width(200.0).height(100.0),
917 &[first, second],
918 )
919 .unwrap();
920 let space = || {
921 (
922 AvailableSpace::Definite(200.0),
923 AvailableSpace::Definite(100.0),
924 )
925 };
926 let (w, h) = space();
927 compute_layout(root, w, h).unwrap();
928 assert_eq!(first_rect.get().x, 0.0);
929 assert_eq!(second_rect.get().x, 40.0);
930
931 crate::set_direction(layout_core::Direction::Rtl);
932 mark_dirty(root).unwrap();
933 let (w, h) = space();
934 compute_layout(root, w, h).unwrap();
935 assert_eq!(first_rect.get().x, 160.0, "the row now starts at the right");
936 assert_eq!(second_rect.get().x, 120.0);
937 crate::set_direction(layout_core::Direction::Ltr);
938 }
939
940 #[test]
942 fn attached_overlay_fills_host_viewport_not_its_small_parent() {
943 reset_layout_runtime();
944 let (small, _) = new_leaf(LayoutStyle::new().width(50.0).height(50.0)).unwrap();
946 let root = new_container(LayoutStyle::new().flex_column(), &[small]).unwrap();
947 compute_layout(
948 root,
949 AvailableSpace::Definite(800.0),
950 AvailableSpace::Definite(600.0),
951 )
952 .unwrap();
953
954 let (inner, inner_rect) = new_leaf(
956 LayoutStyle::new()
957 .width(SizeDimension::Percent(1.0))
958 .height(SizeDimension::Percent(1.0)),
959 )
960 .unwrap();
961 let content = new_container(LayoutStyle::new().absolute_fill(), &[inner]).unwrap();
962 assert!(
963 attach_overlay(content),
964 "the host must be set after the first compute"
965 );
966 relayout_if_dirty();
967
968 let r = inner_rect.get();
969 assert!(
970 (r.width - 800.0).abs() < 0.5 && (r.height - 600.0).abs() < 0.5,
971 "portal fills the viewport, not its 50px parent: {r:?}"
972 );
973
974 detach_overlay(content);
976 remove_node(content);
977 relayout_if_dirty();
978 }
979
980 #[test]
985 fn absolute_rect_stays_window_absolute_across_a_separate_content_root() {
986 reset_layout_runtime();
987 let (sidebar, _) = new_leaf(LayoutStyle::new().width(248.0).height(600.0)).unwrap();
988 let (trigger, trigger_sig) =
989 new_leaf(LayoutStyle::new().width(120.0).height(30.0)).unwrap();
990 let content =
991 new_container(LayoutStyle::new().flex_column().flex_grow(1.0), &[trigger]).unwrap();
992 let root = new_container(LayoutStyle::new().flex_row(), &[sidebar, content]).unwrap();
993 compute_layout(
994 root,
995 AvailableSpace::Definite(1000.0),
996 AvailableSpace::Definite(600.0),
997 )
998 .unwrap();
999 set_overlay_host(root);
1000 assert!(
1002 (absolute_rect(trigger).unwrap().x - 248.0).abs() < 1.0,
1003 "abs x should be past the 248px sidebar: {:?}",
1004 absolute_rect(trigger)
1005 );
1006
1007 mark_dirty(content).unwrap();
1009 compute_layout(
1010 content,
1011 AvailableSpace::Definite(752.0),
1012 AvailableSpace::MaxContent,
1013 )
1014 .unwrap();
1015 assert!(
1016 trigger_sig.get().x < 1.0,
1017 "the rect signal is now content-local (~0): {:?}",
1018 trigger_sig.get()
1019 );
1020 assert!(
1022 (absolute_rect(trigger).unwrap().x - 248.0).abs() < 1.0,
1023 "absolute_rect must stay window-absolute across the sub-root compute: {:?}",
1024 absolute_rect(trigger)
1025 );
1026 }
1027}