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