1use anyhow::Result;
32use fission_diagnostics::prelude as diag;
33use fission_ir::op::TextRun;
34use fission_ir::{FlexDirection as IrFlexDirection, FlexWrap as IrFlexWrap, NodeId};
35use serde::{Deserialize, Serialize};
36use std::collections::{HashMap, HashSet};
37use std::sync::Arc;
38
39pub use fission_ir::{FlexDirection, GridPlacement, GridTrack, LayoutOp};
40
41pub trait ScrollDataSource {
58 fn get_offset(&self, node_id: NodeId) -> f32;
60}
61
62impl<F> ScrollDataSource for F
63where
64 F: Fn(NodeId) -> f32,
65{
66 fn get_offset(&self, node_id: NodeId) -> f32 {
67 self(node_id)
68 }
69}
70
71pub type LayoutUnit = f32;
75
76fn finite_or(value: LayoutUnit, fallback: LayoutUnit) -> LayoutUnit {
78 if value.is_finite() {
79 value
80 } else {
81 fallback
82 }
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
90pub struct LayoutPoint {
91 pub x: LayoutUnit,
93 pub y: LayoutUnit,
95}
96
97impl LayoutPoint {
98 pub const ZERO: Self = Self { x: 0.0, y: 0.0 };
100
101 pub fn new(x: LayoutUnit, y: LayoutUnit) -> Self {
103 Self { x, y }
104 }
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
112pub struct LayoutSize {
113 pub width: LayoutUnit,
115 pub height: LayoutUnit,
117}
118
119impl LayoutSize {
120 pub const ZERO: Self = Self {
122 width: 0.0,
123 height: 0.0,
124 };
125
126 pub fn new(width: LayoutUnit, height: LayoutUnit) -> Self {
128 Self { width, height }
129 }
130}
131
132#[derive(Debug, Clone, Copy, PartialEq)]
158pub struct BoxConstraints {
159 pub min_w: LayoutUnit,
161 pub max_w: LayoutUnit,
163 pub min_h: LayoutUnit,
165 pub max_h: LayoutUnit,
167}
168
169impl BoxConstraints {
170 pub fn tight(size: LayoutSize) -> Self {
174 Self {
175 min_w: size.width,
176 max_w: size.width,
177 min_h: size.height,
178 max_h: size.height,
179 }
180 }
181
182 pub fn loose(max_w: LayoutUnit, max_h: LayoutUnit) -> Self {
186 Self {
187 min_w: 0.0,
188 max_w,
189 min_h: 0.0,
190 max_h,
191 }
192 }
193
194 pub fn is_width_bounded(&self) -> bool {
196 self.max_w.is_finite()
197 }
198
199 pub fn is_height_bounded(&self) -> bool {
201 self.max_h.is_finite()
202 }
203
204 pub fn constrain(&self, size: LayoutSize) -> LayoutSize {
209 LayoutSize {
210 width: size.width.max(self.min_w).min(self.max_w),
211 height: size.height.max(self.min_h).min(self.max_h),
212 }
213 }
214
215 pub fn smallest(&self) -> LayoutSize {
217 LayoutSize::new(self.min_w, self.min_h)
218 }
219
220 pub fn deflate(&self, padding: [LayoutUnit; 4]) -> Self {
226 let horiz = padding[0] + padding[1];
227 let vert = padding[2] + padding[3];
228 let max_w = (self.max_w - horiz).max(0.0);
229 let max_h = (self.max_h - vert).max(0.0);
230 let min_w = (self.min_w - horiz).max(0.0).min(max_w);
231 let min_h = (self.min_h - vert).max(0.0).min(max_h);
232 Self {
233 min_w,
234 max_w,
235 min_h,
236 max_h,
237 }
238 }
239
240 pub fn tighten(&self, width: Option<LayoutUnit>, height: Option<LayoutUnit>) -> Self {
245 let mut out = *self;
246 if let Some(w) = width {
247 let clamped = w.min(out.max_w).max(out.min_w);
248 out.min_w = clamped;
249 out.max_w = clamped;
250 }
251 if let Some(h) = height {
252 let clamped = h.min(out.max_h).max(out.min_h);
253 out.min_h = clamped;
254 out.max_h = clamped;
255 }
256 if out.max_w < out.min_w {
257 out.max_w = out.min_w;
258 }
259 if out.max_h < out.min_h {
260 out.max_h = out.min_h;
261 }
262 out
263 }
264
265 pub fn apply_min_max(
271 &self,
272 min_w: Option<LayoutUnit>,
273 max_w: Option<LayoutUnit>,
274 min_h: Option<LayoutUnit>,
275 max_h: Option<LayoutUnit>,
276 ) -> Self {
277 let mut out = *self;
278 if let Some(w) = min_w {
279 out.min_w = out.min_w.max(w);
280 }
281 if let Some(h) = min_h {
282 out.min_h = out.min_h.max(h);
283 }
284 if let Some(w) = max_w {
285 out.max_w = out.max_w.min(w);
286 }
287 if let Some(h) = max_h {
288 out.max_h = out.max_h.min(h);
289 }
290 if out.max_w < out.min_w {
291 out.max_w = out.min_w;
292 }
293 if out.max_h < out.min_h {
294 out.max_h = out.min_h;
295 }
296 out
297 }
298
299 pub fn loosen(&self) -> Self {
304 Self {
305 min_w: 0.0,
306 max_w: self.max_w,
307 min_h: 0.0,
308 max_h: self.max_h,
309 }
310 }
311}
312
313#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
328pub struct LayoutRect {
329 pub origin: LayoutPoint,
331 pub size: LayoutSize,
333}
334
335impl LayoutRect {
336 pub fn new(x: LayoutUnit, y: LayoutUnit, width: LayoutUnit, height: LayoutUnit) -> Self {
338 Self {
339 origin: LayoutPoint { x, y },
340 size: LayoutSize { width, height },
341 }
342 }
343
344 pub fn x(&self) -> LayoutUnit {
346 self.origin.x
347 }
348 pub fn y(&self) -> LayoutUnit {
350 self.origin.y
351 }
352 pub fn width(&self) -> LayoutUnit {
354 self.size.width
355 }
356 pub fn height(&self) -> LayoutUnit {
358 self.size.height
359 }
360
361 pub fn right(&self) -> LayoutUnit {
363 self.origin.x + self.size.width
364 }
365 pub fn bottom(&self) -> LayoutUnit {
367 self.origin.y + self.size.height
368 }
369
370 pub fn contains(&self, p: LayoutPoint) -> bool {
373 p.x >= self.x() && p.x < self.right() && p.y >= self.y() && p.y < self.bottom()
374 }
375}
376
377#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
383pub struct LayoutNodeGeometry {
384 pub rect: LayoutRect,
386 pub content_size: LayoutSize,
389}
390
391#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
407pub struct LayoutSnapshot {
408 pub nodes: HashMap<NodeId, LayoutNodeGeometry>,
410 #[serde(skip)]
413 pub constraints: HashMap<NodeId, BoxConstraints>,
414 pub viewport_size: LayoutSize,
416}
417
418impl LayoutSnapshot {
419 pub fn new(viewport_size: LayoutSize) -> Self {
421 Self {
422 nodes: HashMap::new(),
423 constraints: HashMap::new(),
424 viewport_size,
425 }
426 }
427
428 pub fn get_node_geometry(&self, node_id: NodeId) -> Option<&LayoutNodeGeometry> {
431 self.nodes.get(&node_id)
432 }
433
434 pub fn get_node_rect(&self, node_id: NodeId) -> Option<LayoutRect> {
436 self.nodes.get(&node_id).map(|g| g.rect)
437 }
438
439 pub fn get_node_constraints(&self, node_id: NodeId) -> Option<BoxConstraints> {
442 self.constraints.get(&node_id).copied()
443 }
444}
445
446#[derive(Debug, Clone)]
455pub struct LayoutInputNode {
456 pub id: NodeId,
458 pub parent_id: Option<NodeId>,
460 pub op: LayoutOp,
462 pub children_ids: Vec<NodeId>,
464 pub debug_name: String,
466 pub width: Option<LayoutUnit>,
468 pub height: Option<LayoutUnit>,
470 pub flex_grow: LayoutUnit,
472 pub flex_shrink: LayoutUnit,
474 pub rich_text: Option<Vec<TextRun>>,
477}
478
479pub struct LineMetric {
485 pub start_index: usize,
487 pub end_index: usize,
489 pub baseline: f32,
491 pub height: f32,
493 pub width: f32,
495}
496
497pub trait TextMeasurer: Send + Sync {
517 fn measure(&self, text: &str, font_size: f32, available_width: Option<f32>) -> (f32, f32);
522
523 fn hit_test(
528 &self,
529 _text: &str,
530 _font_size: f32,
531 _available_width: Option<f32>,
532 _x: f32,
533 _y: f32,
534 ) -> usize {
535 0
536 }
537
538 fn get_line_metrics(
543 &self,
544 text: &str,
545 font_size: f32,
546 available_width: Option<f32>,
547 ) -> Vec<LineMetric> {
548 vec![]
549 }
550
551 fn get_caret_position(
556 &self,
557 _text: &str,
558 _font_size: f32,
559 _available_width: Option<f32>,
560 _caret_index: usize,
561 ) -> (f32, f32) {
562 (0.0, 0.0)
563 }
564
565 fn measure_rich_text(&self, _runs: &[TextRun], _available_width: Option<f32>) -> (f32, f32) {
569 (0.0, 0.0)
570 }
571}
572
573pub struct LayoutEngine {
596 measurer: Option<Arc<dyn TextMeasurer>>,
597}
598
599impl LayoutEngine {
600 pub fn new() -> Self {
605 Self { measurer: None }
606 }
607
608 pub fn with_measurer(mut self, measurer: Arc<dyn TextMeasurer>) -> Self {
612 self.measurer = Some(measurer);
613 self
614 }
615
616 pub fn update(&mut self, input_nodes: &[LayoutInputNode], _dirty_set: &HashSet<NodeId>) {
620 let _ = input_nodes;
621 }
622
623 pub fn rebuild(&mut self, input_nodes: &[LayoutInputNode]) -> Result<()> {
627 let _ = input_nodes;
628 Ok(())
629 }
630
631 pub fn verify_post_update(&self, input_nodes: &[LayoutInputNode], root: NodeId) -> Result<()> {
636 let node_map: HashMap<NodeId, &LayoutInputNode> =
637 input_nodes.iter().map(|n| (n.id, n)).collect();
638 for n in input_nodes {
640 for child in &n.children_ids {
641 let child_node = node_map
642 .get(child)
643 .ok_or_else(|| anyhow::anyhow!("[verify] child {:?} not found", child))?;
644 if child_node.parent_id != Some(n.id) {
645 anyhow::bail!("[verify] parent/child mismatch parent={:?} child={:?} child.parent_id={:?}", n.id, child, child_node.parent_id);
646 }
647 }
648 }
649 fn dfs(
651 id: NodeId,
652 map: &HashMap<NodeId, &LayoutInputNode>,
653 visited: &mut HashSet<NodeId>,
654 stack: &mut HashSet<NodeId>,
655 ) -> Result<()> {
656 if !visited.insert(id) {
657 return Ok(());
658 }
659 stack.insert(id);
660 let node = map
661 .get(&id)
662 .ok_or_else(|| anyhow::anyhow!("[verify] missing node {:?}", id))?;
663 for child in &node.children_ids {
664 if stack.contains(child) {
665 anyhow::bail!("[verify] cycle detected at {:?} -> {:?}", id, child);
666 }
667 dfs(*child, map, visited, stack)?;
668 }
669 stack.remove(&id);
670 Ok(())
671 }
672 let mut visited = HashSet::new();
673 let mut stack = HashSet::new();
674 dfs(root, &node_map, &mut visited, &mut stack)?;
675 Ok(())
676 }
677
678 pub fn compute_layout(
696 &mut self,
697 input_nodes: &[LayoutInputNode],
698 root_node_id: NodeId,
699 viewport_size: LayoutSize,
700 scroll_source: &impl ScrollDataSource,
701 ) -> Result<LayoutSnapshot> {
702 let snapshot = self.compute_layout_constraints(
703 input_nodes,
704 root_node_id,
705 viewport_size,
706 scroll_source,
707 )?;
708 self.emit_scroll_diagnostics(input_nodes, &snapshot);
709 Ok(snapshot)
710 }
711
712 pub fn compute_layout_constraints(
717 &self,
718 input_nodes: &[LayoutInputNode],
719 root_node_id: NodeId,
720 viewport_size: LayoutSize,
721 scroll_source: &impl ScrollDataSource,
722 ) -> Result<LayoutSnapshot> {
723 let node_map: HashMap<NodeId, &LayoutInputNode> =
724 input_nodes.iter().map(|n| (n.id, n)).collect();
725
726 let mut constraints = BoxConstraints::tight(viewport_size);
728 if let Some(root) = node_map.get(&root_node_id) {
729 if root.width.is_some() || root.height.is_some() {
731 constraints = BoxConstraints::loose(viewport_size.width, viewport_size.height).tighten(root.width, root.height);
732 }
733 }
734
735 let mut snapshot = LayoutSnapshot::new(viewport_size);
736 self.layout_node_constraints(
737 root_node_id,
738 constraints,
739 LayoutPoint::ZERO,
740 &node_map,
741 &mut snapshot.nodes,
742 &mut snapshot.constraints,
743 scroll_source,
744 true,
745 0,
746 );
747
748 let visual_location = |node_id: NodeId| -> Option<LayoutPoint> {
749 let mut pos = snapshot.nodes.get(&node_id)?.rect.origin;
750 let mut current = node_map.get(&node_id).and_then(|n| n.parent_id);
751 while let Some(parent_id) = current {
752 if let Some(parent) = node_map.get(&parent_id) {
753 if let LayoutOp::Scroll { direction, .. } = &parent.op {
754 let offset = scroll_source.get_offset(parent_id);
755 match direction {
756 FlexDirection::Row => pos.x -= offset,
757 FlexDirection::Column => pos.y -= offset,
758 }
759 }
760 current = parent.parent_id;
761 } else {
762 break;
763 }
764 }
765 Some(pos)
766 };
767
768 let mut flyout_abs_overrides: HashMap<NodeId, (f32, f32)> = HashMap::new();
769 for node in input_nodes {
770 if let LayoutOp::Flyout { anchor, content } = node.op {
771 if let (Some(anchor_geom), Some(_content_geom)) =
772 (snapshot.nodes.get(&anchor), snapshot.nodes.get(&content))
773 {
774 if let Some(anchor_abs) = visual_location(anchor) {
775 let anchor_w = anchor_geom.rect.width();
776 let anchor_h = anchor_geom.rect.height();
777 let left_rel = anchor_abs.x;
778 let top_rel = anchor_abs.y + anchor_h;
779 flyout_abs_overrides.insert(content, (left_rel, top_rel));
780 }
781 }
782 }
783 }
784
785 if !flyout_abs_overrides.is_empty() {
786 fn apply_offset_recursive(
787 id: NodeId,
788 dx: f32,
789 dy: f32,
790 node_map: &HashMap<NodeId, &LayoutInputNode>,
791 geometries: &mut HashMap<NodeId, LayoutNodeGeometry>,
792 ) {
793 if let Some(g) = geometries.get_mut(&id) {
794 g.rect.origin.x += dx;
795 g.rect.origin.y += dy;
796 }
797 if let Some(n) = node_map.get(&id) {
798 for child in &n.children_ids {
799 apply_offset_recursive(*child, dx, dy, node_map, geometries);
800 }
801 }
802 }
803
804 for (nid, (abs_x, abs_y)) in flyout_abs_overrides {
805 if let Some(current) = snapshot.nodes.get(&nid) {
806 let dx = abs_x - current.rect.origin.x;
807 let dy = abs_y - current.rect.origin.y;
808 apply_offset_recursive(nid, dx, dy, &node_map, &mut snapshot.nodes);
809 }
810 }
811 }
812
813 Ok(snapshot)
814 }
815
816 fn emit_scroll_diagnostics(&self, input_nodes: &[LayoutInputNode], snapshot: &LayoutSnapshot) {
817 use fission_diagnostics::prelude as diag;
818 let trace_scroll = std::env::var("FISSION_SCROLL_TRACE").ok().as_deref() == Some("1");
819 let node_map: HashMap<NodeId, &LayoutInputNode> =
820 input_nodes.iter().map(|n| (n.id, n)).collect();
821 for n in input_nodes {
822 if let LayoutOp::Scroll { .. } = n.op {
823 if let Some(g) = snapshot.nodes.get(&n.id) {
824 let note = if g.rect.height() <= 0.0 {
825 let parent_op = n
826 .parent_id
827 .and_then(|pid| node_map.get(&pid))
828 .map(|p| format!("{:?}", p.op));
829 let parent_constraints = n
830 .parent_id
831 .and_then(|pid| snapshot.constraints.get(&pid))
832 .copied();
833 snapshot
834 .constraints
835 .get(&n.id)
836 .map(|c| {
837 format!(
838 "op={:?} parent={:?} parent_op={:?} parent_constraints={:?} constraints={:?}",
839 n.op,
840 n.parent_id,
841 parent_op,
842 parent_constraints,
843 c
844 )
845 })
846 } else {
847 None
848 };
849 diag::emit(
850 diag::DiagCategory::Layout,
851 diag::DiagLevel::Debug,
852 diag::DiagEventKind::ScrollExtent {
853 node: n.id.as_u128(),
854 viewport_w: g.rect.width(),
855 viewport_h: g.rect.height(),
856 content_w: g.content_size.width,
857 content_h: g.content_size.height,
858 note,
859 },
860 );
861 if trace_scroll {
862 eprintln!(
863 "[scroll-trace] node={} viewport=({:.1},{:.1}) content=({:.1},{:.1})",
864 n.id.as_u128(),
865 g.rect.width(),
866 g.rect.height(),
867 g.content_size.width,
868 g.content_size.height
869 );
870 }
871 }
872 }
873 }
874 }
875
876 fn layout_node_constraints(
877 &self,
878 node_id: NodeId,
879 constraints: BoxConstraints,
880 origin: LayoutPoint,
881 node_map: &HashMap<NodeId, &LayoutInputNode>,
882 out: &mut HashMap<NodeId, LayoutNodeGeometry>,
883 constraints_out: &mut HashMap<NodeId, BoxConstraints>,
884 scroll_source: &impl ScrollDataSource,
885 record: bool,
886 depth: usize,
887 ) -> LayoutSize {
888 if depth > 100 {
889 panic!("Stack overflow safeguard: depth > 100 at node {:?}", node_id);
890 }
891 let node = match node_map.get(&node_id) {
892 Some(n) => *n,
893 None => return LayoutSize::ZERO,
894 };
895
896 if record {
897 constraints_out.insert(node_id, constraints);
898 }
899
900 let mut flow_children: Vec<NodeId> = Vec::new();
901 let mut abs_children: Vec<NodeId> = Vec::new();
902 for child_id in &node.children_ids {
903 let is_absolute = matches!(
904 node_map.get(child_id).map(|n| &n.op),
905 Some(LayoutOp::AbsoluteFill) | Some(LayoutOp::Positioned { .. })
906 );
907 if is_absolute {
908 abs_children.push(*child_id);
909 } else {
910 flow_children.push(*child_id);
911 }
912 }
913
914 let mut content_size = LayoutSize::ZERO;
915 let size = match &node.op {
916 LayoutOp::Box {
917 width,
918 height,
919 min_width,
920 max_width,
921 min_height,
922 max_height,
923 padding,
924 aspect_ratio,
925 ..
926 } => {
927 let mut local =
928 constraints.apply_min_max(*min_width, *max_width, *min_height, *max_height);
929 local = local.tighten(*width, *height);
930 if let Some(ratio) = aspect_ratio.filter(|r| *r > 0.0) {
931 let mut target_w = *width;
932 let mut target_h = *height;
933
934 if target_w.is_some() && target_h.is_none() {
935 target_h = Some(target_w.unwrap() / ratio);
936 } else if target_h.is_some() && target_w.is_none() {
937 target_w = Some(target_h.unwrap() * ratio);
938 } else if target_w.is_none() && target_h.is_none() {
939 if local.is_width_bounded() || local.is_height_bounded() {
940 let (mut w, mut h) = if local.is_width_bounded() {
941 let w = local.max_w;
942 let h = w / ratio;
943 (w, h)
944 } else {
945 let h = local.max_h;
946 let w = h * ratio;
947 (w, h)
948 };
949 if local.is_width_bounded()
950 && local.is_height_bounded()
951 && h > local.max_h
952 {
953 h = local.max_h;
954 w = h * ratio;
955 }
956 target_w = Some(w);
957 target_h = Some(h);
958 }
959 }
960
961 if target_w.is_some() || target_h.is_some() {
962 local = local.tighten(target_w, target_h);
963 }
964 }
965 let base_child_constraints = local.deflate(*padding);
966 let mut max_child = LayoutSize::ZERO;
967 let mut measured_children: Vec<(NodeId, BoxConstraints, LayoutSize)> = Vec::new();
968 for child_id in &flow_children {
969 let (child_width, child_height, child_max_width, child_max_height) = node_map
970 .get(child_id)
971 .map(|child| match &child.op {
972 LayoutOp::Box {
973 width,
974 height,
975 max_width,
976 max_height,
977 ..
978 } => (*width, *height, *max_width, *max_height),
979 LayoutOp::Scroll {
980 width,
981 height,
982 max_width,
983 max_height,
984 ..
985 } => (*width, *height, *max_width, *max_height),
986 LayoutOp::Embed { width, height, .. } => (*width, *height, None, None),
987 _ => (None, None, None, None),
988 })
989 .unwrap_or((None, None, None, None));
990 let mut child_constraints = base_child_constraints;
991 let stretch_width = child_constraints.min_w == child_constraints.max_w
992 && child_width.is_none()
993 && child_max_width.is_none();
994 if stretch_width {
995 child_constraints.min_w = child_constraints.max_w;
996 } else {
997 child_constraints.min_w = 0.0;
998 }
999 let stretch_height = child_constraints.min_h == child_constraints.max_h
1000 && child_height.is_none()
1001 && child_max_height.is_none();
1002 if stretch_height {
1003 child_constraints.min_h = child_constraints.max_h;
1004 } else {
1005 child_constraints.min_h = 0.0;
1006 }
1007 let child_size = self.layout_node_constraints(
1008 *child_id,
1009 child_constraints,
1010 LayoutPoint::ZERO,
1011 node_map,
1012 out,
1013 constraints_out,
1014 scroll_source,
1015 false,
1016 depth + 1,
1017 );
1018 max_child.width = max_child.width.max(child_size.width);
1019 max_child.height = max_child.height.max(child_size.height);
1020 measured_children.push((*child_id, child_constraints, child_size));
1021 }
1022 let padded = LayoutSize::new(
1023 max_child.width + padding[0] + padding[1],
1024 max_child.height + padding[2] + padding[3],
1025 );
1026 let size = local.constrain(padded);
1027 if record {
1028 for (child_id, child_constraints, _child_size) in measured_children {
1029 self.layout_node_constraints(
1030 child_id,
1031 child_constraints,
1032 LayoutPoint::new(origin.x + padding[0], origin.y + padding[2]),
1033 node_map,
1034 out,
1035 constraints_out,
1036 scroll_source,
1037 record,
1038 depth + 1,
1039 );
1040 }
1041 if !abs_children.is_empty() {
1042 let abs_constraints = BoxConstraints::loose(size.width, size.height);
1043 for child_id in abs_children {
1044 self.layout_node_constraints(
1045 child_id,
1046 abs_constraints,
1047 origin,
1048 node_map,
1049 out,
1050 constraints_out,
1051 scroll_source,
1052 record,
1053 depth + 1,
1054 );
1055 }
1056 }
1057 }
1058 content_size = padded;
1059 size
1060 }
1061 LayoutOp::Flex {
1062 direction,
1063 wrap,
1064 padding,
1065 gap,
1066 align_items,
1067 justify_content,
1068 flex_grow,
1069 ..
1070 } => {
1071 let gap = gap.unwrap_or(0.0);
1072 let mut local = constraints.tighten(node.width, node.height);
1073 let inner = local.deflate(*padding);
1074 let is_row = matches!(direction, IrFlexDirection::Row);
1075
1076 let max_main = if is_row { inner.max_w } else { inner.max_h };
1077 let max_cross = if is_row { inner.max_h } else { inner.max_w };
1078 let min_main = if is_row { inner.min_w } else { inner.min_h };
1079 let min_cross = if is_row { inner.min_h } else { inner.min_w };
1080 let main_bounded = if is_row {
1081 inner.is_width_bounded()
1082 } else {
1083 inner.is_height_bounded()
1084 };
1085 let cross_bounded = if is_row {
1086 inner.is_height_bounded()
1087 } else {
1088 inner.is_width_bounded()
1089 };
1090
1091 if matches!(wrap, IrFlexWrap::Wrap | IrFlexWrap::WrapReverse) {
1092 let mut lines: Vec<(Vec<(NodeId, LayoutSize, BoxConstraints)>, f32, f32)> =
1093 Vec::new();
1094 let mut line_children: Vec<(NodeId, LayoutSize, BoxConstraints)> = Vec::new();
1095 let mut line_main = 0.0f32;
1096 let mut line_cross = 0.0f32;
1097 let mut max_line_main = 0.0f32;
1098
1099 for child_id in &flow_children {
1100 let child_constraints = if is_row {
1101 BoxConstraints {
1102 min_w: 0.0,
1103 max_w: max_main,
1104 min_h: 0.0,
1105 max_h: max_cross,
1106 }
1107 } else {
1108 BoxConstraints {
1109 min_w: 0.0,
1110 max_w: max_cross,
1111 min_h: 0.0,
1112 max_h: max_main,
1113 }
1114 };
1115 let child_size = self.layout_node_constraints(
1116 *child_id,
1117 child_constraints,
1118 LayoutPoint::ZERO,
1119 node_map,
1120 out,
1121 constraints_out,
1122 scroll_source,
1123 false,
1124 depth + 1,
1125 );
1126 let child_main = if is_row {
1127 child_size.width
1128 } else {
1129 child_size.height
1130 };
1131 let child_cross = if is_row {
1132 child_size.height
1133 } else {
1134 child_size.width
1135 };
1136 let next_main = if line_children.is_empty() {
1137 child_main
1138 } else {
1139 line_main + gap + child_main
1140 };
1141
1142 if main_bounded && !line_children.is_empty() && next_main > max_main {
1143 max_line_main = max_line_main.max(line_main);
1144 lines.push((line_children, line_main, line_cross));
1145 line_children = Vec::new();
1146 line_main = 0.0;
1147 line_cross = 0.0;
1148 }
1149
1150 if !line_children.is_empty() {
1151 line_main += gap;
1152 }
1153 line_main += child_main;
1154 line_cross = line_cross.max(child_cross);
1155 line_children.push((*child_id, child_size, child_constraints));
1156 }
1157
1158 if !line_children.is_empty() {
1159 max_line_main = max_line_main.max(line_main);
1160 lines.push((line_children, line_main, line_cross));
1161 }
1162
1163 let mut container_main = if main_bounded && *flex_grow > 0.0 {
1164 max_main
1165 } else {
1166 max_line_main
1167 };
1168 container_main = container_main.max(min_main);
1169 let total_lines_cross: f32 =
1170 lines.iter().map(|(_, _, cross)| *cross).sum::<f32>()
1171 + gap * lines.len().saturating_sub(1) as f32;
1172 let mut container_cross = total_lines_cross.max(min_cross);
1173 let size = if is_row {
1174 local.constrain(LayoutSize::new(
1175 container_main + padding[0] + padding[1],
1176 container_cross + padding[2] + padding[3],
1177 ))
1178 } else {
1179 local.constrain(LayoutSize::new(
1180 container_cross + padding[0] + padding[1],
1181 container_main + padding[2] + padding[3],
1182 ))
1183 };
1184
1185 let inner_main = if is_row {
1186 size.width - padding[0] - padding[1]
1187 } else {
1188 size.height - padding[2] - padding[3]
1189 };
1190 let inner_cross = if is_row {
1191 size.height - padding[2] - padding[3]
1192 } else {
1193 size.width - padding[0] - padding[1]
1194 };
1195
1196 let mut ordered_lines = lines;
1197 if matches!(wrap, IrFlexWrap::WrapReverse) {
1198 ordered_lines.reverse();
1199 }
1200
1201 let mut line_cursor = if matches!(wrap, IrFlexWrap::WrapReverse) {
1202 (inner_cross - total_lines_cross).max(0.0)
1203 } else {
1204 0.0
1205 };
1206
1207 for (line_children, line_main, line_cross) in ordered_lines {
1208 let mut remaining_space = (inner_main - line_main).max(0.0);
1209 let mut extra_gap = 0.0;
1210 let mut offset_main = 0.0;
1211 match justify_content {
1212 fission_ir::op::JustifyContent::Start => {}
1213 fission_ir::op::JustifyContent::End => offset_main = remaining_space,
1214 fission_ir::op::JustifyContent::Center => {
1215 offset_main = remaining_space / 2.0
1216 }
1217 fission_ir::op::JustifyContent::SpaceBetween => {
1218 if line_children.len() > 1 {
1219 extra_gap =
1220 remaining_space / (line_children.len() as f32 - 1.0);
1221 }
1222 }
1223 fission_ir::op::JustifyContent::SpaceAround => {
1224 if !line_children.is_empty() {
1225 extra_gap = remaining_space / line_children.len() as f32;
1226 offset_main = extra_gap / 2.0;
1227 }
1228 }
1229 fission_ir::op::JustifyContent::SpaceEvenly => {
1230 if !line_children.is_empty() {
1231 extra_gap =
1232 remaining_space / (line_children.len() as f32 + 1.0);
1233 offset_main = extra_gap;
1234 }
1235 }
1236 }
1237
1238 let mut cursor = offset_main;
1239 for (child_id, child_size, mut child_constraints) in line_children {
1240 let child_main = if is_row {
1241 child_size.width
1242 } else {
1243 child_size.height
1244 };
1245 let child_cross = if is_row {
1246 child_size.height
1247 } else {
1248 child_size.width
1249 };
1250 if matches!(align_items, fission_ir::op::AlignItems::Stretch) {
1251 if is_row {
1252 child_constraints.min_h = line_cross;
1253 child_constraints.max_h = line_cross;
1254 } else {
1255 child_constraints.min_w = line_cross;
1256 child_constraints.max_w = line_cross;
1257 }
1258 }
1259 let cross_offset = match align_items {
1260 fission_ir::op::AlignItems::Start
1261 | fission_ir::op::AlignItems::Stretch => 0.0,
1262 fission_ir::op::AlignItems::End => {
1263 (line_cross - child_cross).max(0.0)
1264 }
1265 fission_ir::op::AlignItems::Center => {
1266 ((line_cross - child_cross) / 2.0).max(0.0)
1267 }
1268 fission_ir::op::AlignItems::Baseline => 0.0,
1269 };
1270 let child_origin = if is_row {
1271 LayoutPoint::new(
1272 origin.x + padding[0] + cursor,
1273 origin.y + padding[2] + line_cursor + cross_offset,
1274 )
1275 } else {
1276 LayoutPoint::new(
1277 origin.x + padding[0] + line_cursor + cross_offset,
1278 origin.y + padding[2] + cursor,
1279 )
1280 };
1281 self.layout_node_constraints(
1282 child_id,
1283 child_constraints,
1284 child_origin,
1285 node_map,
1286 out,
1287 constraints_out,
1288 scroll_source,
1289 record,
1290 depth + 1,
1291 );
1292 cursor += child_main + gap + extra_gap;
1293 }
1294
1295 line_cursor += line_cross + gap;
1296 }
1297
1298 if record && !abs_children.is_empty() {
1299 let abs_constraints = BoxConstraints::loose(size.width, size.height);
1300 for child_id in abs_children {
1301 self.layout_node_constraints(
1302 child_id,
1303 abs_constraints,
1304 origin,
1305 node_map,
1306 out,
1307 constraints_out,
1308 scroll_source,
1309 record,
1310 depth + 1,
1311 );
1312 }
1313 }
1314 content_size = size;
1315 size
1316 } else {
1317 struct FlexChildEntry {
1318 id: NodeId,
1319 flex: f32,
1320 size: LayoutSize,
1321 constraints: BoxConstraints,
1322 is_flex: bool,
1323 }
1324 let mut measured: Vec<FlexChildEntry> = Vec::new();
1325 let mut total_flex = 0.0f32;
1326 let mut nonflex_main = 0.0f32;
1327 let mut max_child_cross = 0.0f32;
1328 let treat_flex_as_nonflex = !main_bounded;
1329
1330 for child_id in &flow_children {
1331 let child = match node_map.get(child_id) {
1332 Some(c) => *c,
1333 None => continue,
1334 };
1335 let flex = child.flex_grow;
1336 if flex > 0.0 && !treat_flex_as_nonflex {
1337 total_flex += flex;
1338 measured.push(FlexChildEntry {
1339 id: *child_id,
1340 flex,
1341 size: LayoutSize::ZERO,
1342 constraints: BoxConstraints::loose(0.0, 0.0),
1343 is_flex: true,
1344 });
1345 continue;
1346 }
1347 let child_constraints = if is_row {
1348 let cross =
1349 if matches!(align_items, fission_ir::op::AlignItems::Stretch)
1350 && cross_bounded
1351 {
1352 BoxConstraints {
1353 min_w: 0.0,
1354 max_w: f32::INFINITY,
1355 min_h: max_cross,
1356 max_h: max_cross,
1357 }
1358 } else {
1359 BoxConstraints {
1360 min_w: 0.0,
1361 max_w: f32::INFINITY,
1362 min_h: 0.0,
1363 max_h: max_cross,
1364 }
1365 };
1366 cross
1367 } else {
1368 let cross =
1369 if matches!(align_items, fission_ir::op::AlignItems::Stretch)
1370 && cross_bounded
1371 {
1372 BoxConstraints {
1373 min_w: max_cross,
1374 max_w: max_cross,
1375 min_h: 0.0,
1376 max_h: f32::INFINITY,
1377 }
1378 } else {
1379 BoxConstraints {
1380 min_w: 0.0,
1381 max_w: max_cross,
1382 min_h: 0.0,
1383 max_h: f32::INFINITY,
1384 }
1385 };
1386 cross
1387 };
1388 let child_size = self.layout_node_constraints(
1389 *child_id,
1390 child_constraints,
1391 LayoutPoint::ZERO,
1392 node_map,
1393 out,
1394 constraints_out,
1395 scroll_source,
1396 false,
1397 depth + 1,
1398 );
1399 let child_main = if is_row {
1400 child_size.width
1401 } else {
1402 child_size.height
1403 };
1404 let child_cross = if is_row {
1405 child_size.height
1406 } else {
1407 child_size.width
1408 };
1409 nonflex_main += child_main;
1410 max_child_cross = max_child_cross.max(child_cross);
1411 measured.push(FlexChildEntry {
1412 id: *child_id,
1413 flex,
1414 size: child_size,
1415 constraints: child_constraints,
1416 is_flex: false,
1417 });
1418 }
1419
1420 let gap_total = gap * flow_children.len().saturating_sub(1) as f32;
1421 let remaining = if main_bounded {
1422 (max_main - nonflex_main - gap_total).max(0.0)
1423 } else {
1424 0.0
1425 };
1426
1427 for entry in measured.iter_mut().filter(|e| e.is_flex) {
1428 let flex = entry.flex;
1429 let allocated = if main_bounded && total_flex > 0.0 {
1430 remaining * (flex / total_flex)
1431 } else {
1432 0.0
1433 };
1434 let child_constraints = if is_row {
1435 let cross =
1436 if matches!(align_items, fission_ir::op::AlignItems::Stretch)
1437 && cross_bounded
1438 {
1439 BoxConstraints {
1440 min_w: allocated,
1441 max_w: allocated,
1442 min_h: max_cross,
1443 max_h: max_cross,
1444 }
1445 } else {
1446 BoxConstraints {
1447 min_w: allocated,
1448 max_w: allocated,
1449 min_h: 0.0,
1450 max_h: max_cross,
1451 }
1452 };
1453 cross
1454 } else {
1455 let cross =
1456 if matches!(align_items, fission_ir::op::AlignItems::Stretch)
1457 && cross_bounded
1458 {
1459 BoxConstraints {
1460 min_w: max_cross,
1461 max_w: max_cross,
1462 min_h: allocated,
1463 max_h: allocated,
1464 }
1465 } else {
1466 BoxConstraints {
1467 min_w: 0.0,
1468 max_w: max_cross,
1469 min_h: allocated,
1470 max_h: allocated,
1471 }
1472 };
1473 cross
1474 };
1475 let child_size = self.layout_node_constraints(
1476 entry.id,
1477 child_constraints,
1478 LayoutPoint::ZERO,
1479 node_map,
1480 out,
1481 constraints_out,
1482 scroll_source,
1483 false,
1484 depth + 1,
1485 );
1486 let child_cross = if is_row {
1487 child_size.height
1488 } else {
1489 child_size.width
1490 };
1491 max_child_cross = max_child_cross.max(child_cross);
1492 entry.size = child_size;
1493 entry.constraints = child_constraints;
1494 }
1495
1496 let final_children_main: f32 = measured
1497 .iter()
1498 .map(|entry| {
1499 if is_row {
1500 entry.size.width
1501 } else {
1502 entry.size.height
1503 }
1504 })
1505 .sum();
1506
1507 let mut container_main = if main_bounded && *flex_grow > 0.0 {
1508 max_main
1509 } else {
1510 final_children_main + gap_total
1511 };
1512 container_main = container_main.max(min_main);
1513
1514 if main_bounded && final_children_main + gap_total > max_main {
1515 let mut total_shrink_scaled = 0.0f32;
1517 for entry in &measured {
1518 let child = node_map.get(&entry.id).unwrap();
1519 let main_size = if is_row { entry.size.width } else { entry.size.height };
1520 total_shrink_scaled += main_size * child.flex_shrink;
1521 }
1522
1523 if total_shrink_scaled > 0.0 {
1524 let overflow = (final_children_main + gap_total) - max_main;
1525 for entry in &mut measured {
1526 let child = node_map.get(&entry.id).unwrap();
1527 let main_size = if is_row { entry.size.width } else { entry.size.height };
1528 let shrink_amount = (main_size * child.flex_shrink / total_shrink_scaled) * overflow;
1529 let floor = if child.flex_shrink > 0.0 {
1533 let explicit_min = match &child.op {
1535 LayoutOp::Box { min_width, min_height, height, width, .. } => {
1536 if is_row {
1537 min_width.or(*width).unwrap_or(0.0)
1538 } else {
1539 min_height.or(*height).unwrap_or(0.0)
1540 }
1541 }
1542 _ => 0.0,
1543 };
1544 explicit_min
1545 } else {
1546 main_size };
1548 let new_main = (main_size - shrink_amount).max(floor);
1549
1550 let mut child_constraints = entry.constraints;
1551 if is_row {
1552 child_constraints.min_w = new_main;
1553 child_constraints.max_w = new_main;
1554 } else {
1555 child_constraints.min_h = new_main;
1556 child_constraints.max_h = new_main;
1557 }
1558 let new_size = self.layout_node_constraints(
1559 entry.id,
1560 child_constraints,
1561 LayoutPoint::ZERO,
1562 node_map,
1563 out,
1564 constraints_out,
1565 scroll_source,
1566 false,
1567 depth + 1,
1568 );
1569 entry.size = new_size;
1570 entry.constraints = child_constraints;
1571 }
1572 }
1573 }
1574
1575 let mut container_cross = max_child_cross.max(min_cross);
1576 let size = if is_row {
1577 local.constrain(LayoutSize::new(
1578 container_main + padding[0] + padding[1],
1579 container_cross + padding[2] + padding[3],
1580 ))
1581 } else {
1582 local.constrain(LayoutSize::new(
1583 container_cross + padding[0] + padding[1],
1584 container_main + padding[2] + padding[3],
1585 ))
1586 };
1587
1588 let inner_main = if is_row {
1589 size.width - padding[0] - padding[1]
1590 } else {
1591 size.height - padding[2] - padding[3]
1592 };
1593 let inner_cross = if is_row {
1594 size.height - padding[2] - padding[3]
1595 } else {
1596 size.width - padding[0] - padding[1]
1597 };
1598
1599 let final_children_main: f32 = measured
1600 .iter()
1601 .map(|entry| {
1602 if is_row {
1603 entry.size.width
1604 } else {
1605 entry.size.height
1606 }
1607 })
1608 .sum();
1609
1610 let mut remaining_space =
1611 (inner_main - final_children_main - gap_total).max(0.0);
1612 let mut extra_gap = 0.0;
1613 let mut offset_main = 0.0;
1614 match justify_content {
1615 fission_ir::op::JustifyContent::Start => {}
1616 fission_ir::op::JustifyContent::End => offset_main = remaining_space,
1617 fission_ir::op::JustifyContent::Center => {
1618 offset_main = remaining_space / 2.0
1619 }
1620 fission_ir::op::JustifyContent::SpaceBetween => {
1621 if measured.len() > 1 {
1622 extra_gap = remaining_space / (measured.len() as f32 - 1.0);
1623 }
1624 }
1625 fission_ir::op::JustifyContent::SpaceAround => {
1626 if !measured.is_empty() {
1627 extra_gap = remaining_space / measured.len() as f32;
1628 offset_main = extra_gap / 2.0;
1629 }
1630 }
1631 fission_ir::op::JustifyContent::SpaceEvenly => {
1632 if !measured.is_empty() {
1633 extra_gap = remaining_space / (measured.len() as f32 + 1.0);
1634 offset_main = extra_gap;
1635 }
1636 }
1637 }
1638
1639 let mut cursor = offset_main;
1640 for entry in measured {
1641 let child_main = if is_row {
1642 entry.size.width
1643 } else {
1644 entry.size.height
1645 };
1646 let child_cross = if is_row {
1647 entry.size.height
1648 } else {
1649 entry.size.width
1650 };
1651 let cross_offset = match align_items {
1652 fission_ir::op::AlignItems::Start
1653 | fission_ir::op::AlignItems::Stretch => 0.0,
1654 fission_ir::op::AlignItems::End => (inner_cross - child_cross).max(0.0),
1655 fission_ir::op::AlignItems::Center => {
1656 ((inner_cross - child_cross) / 2.0).max(0.0)
1657 }
1658 fission_ir::op::AlignItems::Baseline => 0.0,
1659 };
1660 let child_origin = if is_row {
1661 LayoutPoint::new(
1662 origin.x + padding[0] + cursor,
1663 origin.y + padding[2] + cross_offset,
1664 )
1665 } else {
1666 LayoutPoint::new(
1667 origin.x + padding[0] + cross_offset,
1668 origin.y + padding[2] + cursor,
1669 )
1670 };
1671
1672 let mut child_constraints = entry.constraints;
1673 if matches!(align_items, fission_ir::op::AlignItems::Stretch) {
1674 let child_node = node_map.get(&entry.id);
1676 let has_explicit_cross = child_node.map(|n| match &n.op {
1677 LayoutOp::Box { width, height, .. } => {
1678 if is_row { height.is_some() } else { width.is_some() }
1679 }
1680 _ => false,
1681 }).unwrap_or(false);
1682 if !has_explicit_cross {
1683 if is_row {
1684 child_constraints.min_h = inner_cross;
1685 child_constraints.max_h = inner_cross;
1686 } else {
1687 child_constraints.min_w = inner_cross;
1688 child_constraints.max_w = inner_cross;
1689 }
1690 }
1691 }
1692
1693 self.layout_node_constraints(
1694 entry.id,
1695 child_constraints,
1696 child_origin,
1697 node_map,
1698 out,
1699 constraints_out,
1700 scroll_source,
1701 record,
1702 depth + 1,
1703 );
1704 cursor += child_main + gap + extra_gap;
1705 }
1706
1707 if record && !abs_children.is_empty() {
1708 let abs_constraints = BoxConstraints::loose(size.width, size.height);
1709 for child_id in abs_children {
1710 self.layout_node_constraints(
1711 child_id,
1712 abs_constraints,
1713 origin,
1714 node_map,
1715 out,
1716 constraints_out,
1717 scroll_source,
1718 record,
1719 depth + 1,
1720 );
1721 }
1722 }
1723 content_size = size;
1724 size
1725 }
1726 }
1727 LayoutOp::Grid {
1728 columns,
1729 rows,
1730 column_gap,
1731 row_gap,
1732 padding,
1733 } => {
1734 let gap_x = column_gap.unwrap_or(0.0);
1735 let gap_y = row_gap.unwrap_or(0.0);
1736 let inner = constraints.deflate(*padding);
1737 let bounded_w = inner.is_width_bounded();
1738 let bounded_h = inner.is_height_bounded();
1739 let available_w = if bounded_w { inner.max_w } else { 0.0 };
1740 let available_h = if bounded_h { inner.max_h } else { 0.0 };
1741
1742 let col_count = columns.len().max(1);
1743 let mut col_widths = vec![0.0f32; col_count];
1744 let mut fr_total = 0.0f32;
1745 let mut fixed_total = 0.0f32;
1746 for (i, track) in columns.iter().enumerate() {
1747 match track {
1748 GridTrack::Points(p) => {
1749 col_widths[i] = *p;
1750 fixed_total += *p;
1751 }
1752 GridTrack::Percent(p) => {
1753 let w = if bounded_w {
1754 available_w * (*p / 100.0)
1755 } else {
1756 0.0
1757 };
1758 col_widths[i] = w;
1759 fixed_total += w;
1760 }
1761 GridTrack::Fr(f) => fr_total += *f,
1762 _ => {}
1763 }
1764 }
1765 if fr_total > 0.0 && bounded_w {
1766 let remaining = (available_w - fixed_total - gap_x * (col_count.saturating_sub(1) as f32)).max(0.0);
1767 for (i, track) in columns.iter().enumerate() {
1768 if let GridTrack::Fr(f) = track {
1769 col_widths[i] = remaining * (*f / fr_total);
1770 }
1771 }
1772 }
1773
1774 let child_count = flow_children.len();
1775 let row_count = if rows.is_empty() {
1776 (child_count + col_count - 1) / col_count
1777 } else {
1778 rows.len()
1779 };
1780 let mut row_heights = vec![0.0f32; row_count.max(1)];
1781
1782 if !rows.is_empty() {
1783 let mut row_fr_total = 0.0f32;
1784 let mut row_fixed_total = 0.0f32;
1785 for (i, track) in rows.iter().enumerate() {
1786 if i >= row_heights.len() { break; }
1787 match track {
1788 GridTrack::Points(p) => {
1789 row_heights[i] = *p;
1790 row_fixed_total += *p;
1791 }
1792 GridTrack::Percent(p) => {
1793 let h = if bounded_h { available_h * (*p / 100.0) } else { 0.0 };
1794 row_heights[i] = h;
1795 row_fixed_total += h;
1796 }
1797 GridTrack::Fr(f) => row_fr_total += *f,
1798 _ => {}
1799 }
1800 }
1801 if row_fr_total > 0.0 && bounded_h {
1802 let remaining = (available_h - row_fixed_total - gap_y * (row_heights.len().saturating_sub(1) as f32)).max(0.0);
1803 for (i, track) in rows.iter().enumerate() {
1804 if let GridTrack::Fr(f) = track {
1805 row_heights[i] = remaining * (*f / row_fr_total);
1806 }
1807 }
1808 }
1809 }
1810
1811 let mut cell_assignments = Vec::new();
1812 let mut auto_row = 0;
1813 let mut auto_col = 0;
1814
1815 for child_id in &flow_children {
1816 let child = node_map.get(child_id).unwrap();
1817 let (row, col) = if let LayoutOp::GridItem { row_start, col_start, .. } = &child.op {
1818 let r = match row_start {
1819 fission_ir::op::GridPlacement::Line(l) => (*l as usize).saturating_sub(1),
1820 _ => auto_row,
1821 };
1822 let c = match col_start {
1823 fission_ir::op::GridPlacement::Line(l) => (*l as usize).saturating_sub(1),
1824 _ => auto_col,
1825 };
1826 (r, c)
1827 } else {
1828 let res = (auto_row, auto_col);
1829 auto_col += 1;
1830 if auto_col >= col_count {
1831 auto_col = 0;
1832 auto_row += 1;
1833 }
1834 res
1835 };
1836 cell_assignments.push((*child_id, row, col));
1837 }
1838
1839 for (child_id, row, col) in &cell_assignments {
1840 if *row >= row_heights.len() || *col >= col_widths.len() { continue; }
1841 let cell_w = col_widths[*col];
1842 let cell_constraints = BoxConstraints {
1843 min_w: cell_w,
1844 max_w: cell_w,
1845 min_h: 0.0,
1846 max_h: if row_heights[*row] > 0.0 { row_heights[*row] } else { f32::INFINITY },
1847 };
1848 let child_size = self.layout_node_constraints(*child_id, cell_constraints, LayoutPoint::ZERO, node_map, out, constraints_out, scroll_source, false, depth + 1);
1849 if row_heights[*row] == 0.0 {
1850 row_heights[*row] = child_size.height;
1851 } else {
1852 row_heights[*row] = row_heights[*row].max(child_size.height);
1853 }
1854 }
1855
1856 let grid_w: f32 = col_widths.iter().sum::<f32>() + gap_x * (col_count.saturating_sub(1) as f32);
1857 let grid_h: f32 = row_heights.iter().sum::<f32>() + gap_y * (row_heights.len().saturating_sub(1) as f32);
1858 let size = constraints.constrain(LayoutSize::new(grid_w + padding[0] + padding[1], grid_h + padding[2] + padding[3]));
1859
1860 if record {
1861 let padding_origin_x = origin.x + padding[0];
1862 let padding_origin_y = origin.y + padding[2];
1863 for (child_id, row, col) in &cell_assignments {
1864 if *row >= row_heights.len() || *col >= col_widths.len() { continue; }
1865 let mut cell_x = padding_origin_x;
1866 for i in 0..*col { cell_x += col_widths[i] + gap_x; }
1867 let mut cell_y = padding_origin_y;
1868 for i in 0..*row { cell_y += row_heights[i] + gap_y; }
1869 let cell_w = col_widths[*col];
1870 let cell_h = row_heights[*row];
1871 let child_constraints = BoxConstraints { min_w: cell_w, max_w: cell_w, min_h: cell_h, max_h: cell_h };
1872 self.layout_node_constraints(*child_id, child_constraints, LayoutPoint::new(cell_x, cell_y), node_map, out, constraints_out, scroll_source, record, depth + 1);
1873 }
1874 }
1875
1876 if record && !abs_children.is_empty() {
1877 let abs_constraints = BoxConstraints::loose(size.width, size.height);
1878 for child_id in abs_children {
1879 self.layout_node_constraints(child_id, abs_constraints, origin, node_map, out, constraints_out, scroll_source, record, depth + 1);
1880 }
1881 }
1882 content_size = size;
1883 size
1884 }
1885 LayoutOp::GridItem { .. } => {
1886 let mut child_size = LayoutSize::ZERO;
1887 if let Some(child_id) = node.children_ids.first() {
1888 child_size = self.layout_node_constraints(*child_id, constraints, origin, node_map, out, constraints_out, scroll_source, record, depth + 1);
1889 }
1890 content_size = child_size;
1891 constraints.constrain(child_size)
1892 }
1893 LayoutOp::Scroll { direction, width, height, min_width, max_width, min_height, max_height, padding, .. } => {
1894 let mut local = constraints.apply_min_max(*min_width, *max_width, *min_height, *max_height);
1895 local = local.tighten(*width, *height);
1896 let is_horizontal = matches!(direction, FlexDirection::Row);
1897 let mut child_constraints = local.deflate(*padding).loosen();
1898 if is_horizontal { child_constraints.max_w = f32::INFINITY; } else { child_constraints.max_h = f32::INFINITY; }
1899 let mut child_size = LayoutSize::ZERO;
1900 if let Some(child_id) = flow_children.first() {
1901 child_size = self.layout_node_constraints(*child_id, child_constraints, LayoutPoint::ZERO, node_map, out, constraints_out, scroll_source, false, depth + 1);
1902 }
1903 let size = local.constrain(LayoutSize::new(child_size.width + padding[0] + padding[1], child_size.height + padding[2] + padding[3]));
1904 if record {
1905 if let Some(child_id) = flow_children.first() {
1906 self.layout_node_constraints(*child_id, child_constraints, LayoutPoint::new(origin.x + padding[0], origin.y + padding[2]), node_map, out, constraints_out, scroll_source, record, depth + 1);
1907 }
1908 if !abs_children.is_empty() {
1909 let abs_constraints = BoxConstraints::loose(size.width, size.height);
1910 for child_id in abs_children {
1911 self.layout_node_constraints(child_id, abs_constraints, origin, node_map, out, constraints_out, scroll_source, record, depth + 1);
1912 }
1913 }
1914 }
1915 content_size = child_size;
1916 size
1917 }
1918 LayoutOp::Align => {
1919 let child_constraints = BoxConstraints::loose(constraints.max_w, constraints.max_h);
1920 let mut child_size = LayoutSize::ZERO;
1921 if let Some(child_id) = flow_children.first() {
1922 child_size = self.layout_node_constraints(*child_id, child_constraints, LayoutPoint::ZERO, node_map, out, constraints_out, scroll_source, false, depth + 1);
1923 }
1924 let size = if constraints.is_width_bounded() || constraints.is_height_bounded() {
1925 constraints.constrain(LayoutSize::new(if constraints.is_width_bounded() { constraints.max_w } else { child_size.width }, if constraints.is_height_bounded() { constraints.max_h } else { child_size.height }))
1926 } else { child_size };
1927 if let Some(child_id) = flow_children.first() {
1928 let dx = ((size.width - child_size.width) / 2.0).max(0.0);
1929 let dy = ((size.height - child_size.height) / 2.0).max(0.0);
1930 self.layout_node_constraints(*child_id, child_constraints, LayoutPoint::new(origin.x + dx, origin.y + dy), node_map, out, constraints_out, scroll_source, record, depth + 1);
1931 }
1932 if record && !abs_children.is_empty() {
1933 let abs_constraints = BoxConstraints::loose(size.width, size.height);
1934 for child_id in abs_children {
1935 self.layout_node_constraints(child_id, abs_constraints, origin, node_map, out, constraints_out, scroll_source, record, depth + 1);
1936 }
1937 }
1938 content_size = child_size;
1939 size
1940 }
1941 LayoutOp::ZStack => {
1942 let mut max_child = LayoutSize::ZERO;
1943 for child_id in &flow_children {
1944 let child_size = self.layout_node_constraints(*child_id, BoxConstraints::loose(constraints.max_w, constraints.max_h), LayoutPoint::ZERO, node_map, out, constraints_out, scroll_source, false, depth + 1);
1945 max_child.width = max_child.width.max(child_size.width);
1946 max_child.height = max_child.height.max(child_size.height);
1947 }
1948 let size = if constraints.is_width_bounded() || constraints.is_height_bounded() {
1949 constraints.constrain(LayoutSize::new(if constraints.is_width_bounded() { constraints.max_w } else { max_child.width }, if constraints.is_height_bounded() { constraints.max_h } else { max_child.height }))
1950 } else { max_child };
1951 for child_id in &flow_children {
1952 let child_constraints = BoxConstraints::loose(size.width, size.height);
1953 let child_origin = LayoutPoint::new(origin.x, origin.y);
1954 self.layout_node_constraints(*child_id, child_constraints, child_origin, node_map, out, constraints_out, scroll_source, record, depth + 1);
1955 }
1956 if record && !abs_children.is_empty() {
1957 let abs_constraints = BoxConstraints::loose(size.width, size.height);
1958 for child_id in abs_children {
1959 self.layout_node_constraints(child_id, abs_constraints, origin, node_map, out, constraints_out, scroll_source, record, depth + 1);
1960 }
1961 }
1962 content_size = size;
1963 size
1964 }
1965 LayoutOp::Positioned { top, left, bottom, right, width, height } => {
1966 let target_w = finite_or(constraints.max_w, finite_or(constraints.min_w, 0.0));
1967 let target_h = finite_or(constraints.max_h, finite_or(constraints.min_h, 0.0));
1968 let size = constraints.constrain(LayoutSize::new(target_w, target_h));
1969 let mut child_constraints = BoxConstraints::loose(size.width, size.height);
1970 if let (Some(l), Some(r)) = (left, right) {
1971 let w = (size.width - l - r).max(0.0);
1972 child_constraints = child_constraints.tighten(Some(w), None);
1973 }
1974 if let (Some(t), Some(b)) = (top, bottom) {
1975 let h = (size.height - t - b).max(0.0);
1976 child_constraints = child_constraints.tighten(None, Some(h));
1977 }
1978 child_constraints = child_constraints.tighten(*width, *height);
1979 if let Some(child_id) = node.children_ids.first() {
1980 let child_size = self.layout_node_constraints(*child_id, child_constraints, LayoutPoint::ZERO, node_map, out, constraints_out, scroll_source, false, depth + 1);
1981 let x = left.unwrap_or_else(|| { right.map(|r| (size.width - r - child_size.width).max(0.0)).unwrap_or(0.0) });
1982 let y = top.unwrap_or_else(|| { bottom.map(|b| (size.height - b - child_size.height).max(0.0)).unwrap_or(0.0) });
1983 self.layout_node_constraints(*child_id, child_constraints, LayoutPoint::new(origin.x + x, origin.y + y), node_map, out, constraints_out, scroll_source, record, depth + 1);
1984 }
1985 content_size = size;
1986 size
1987 }
1988 LayoutOp::Embed { width, height, .. } => {
1989 let local = constraints.tighten(*width, *height);
1990 let w = if local.is_width_bounded() { local.max_w } else { local.min_w };
1991 let h = if local.is_height_bounded() { local.max_h } else { local.min_h };
1992 let size = local.constrain(LayoutSize::new(w, h));
1993 content_size = size;
1994 size
1995 }
1996 LayoutOp::AbsoluteFill => {
1997 let target_w = finite_or(constraints.max_w, finite_or(constraints.min_w, 0.0));
1998 let target_h = finite_or(constraints.max_h, finite_or(constraints.min_h, 0.0));
1999 let size = constraints.constrain(LayoutSize::new(target_w, target_h));
2000 for child_id in &node.children_ids {
2001 self.layout_node_constraints(*child_id, BoxConstraints::tight(size), origin, node_map, out, constraints_out, scroll_source, record, depth + 1);
2002 }
2003 content_size = size;
2004 size
2005 }
2006 LayoutOp::Transform { .. } | LayoutOp::Clip { .. } => {
2007 let mut child_size = LayoutSize::ZERO;
2008 if let Some(child_id) = node.children_ids.first() {
2009 child_size = self.layout_node_constraints(*child_id, constraints, origin, node_map, out, constraints_out, scroll_source, record, depth + 1);
2010 }
2011 content_size = child_size;
2012 constraints.constrain(child_size)
2013 }
2014 LayoutOp::Flyout { anchor, content } => {
2015 let loose = BoxConstraints::loose(
2016 if constraints.is_width_bounded() { constraints.max_w } else { f32::INFINITY },
2017 if constraints.is_height_bounded() { constraints.max_h } else { f32::INFINITY },
2018 );
2019 let mut child_size = LayoutSize::ZERO;
2020 for child_id in &node.children_ids {
2021 child_size = self.layout_node_constraints(*child_id, loose, origin, node_map, out, constraints_out, scroll_source, false, depth + 1);
2022 }
2023 if record {
2024 let anchor_rect = out.get(anchor).map(|g| g.rect);
2025 let place_x = anchor_rect.map(|r| r.x()).unwrap_or(origin.x);
2026 let place_y = anchor_rect.map(|r| r.y() + r.height()).unwrap_or(origin.y);
2027 for child_id in &node.children_ids {
2028 self.layout_node_constraints(*child_id, loose, LayoutPoint::new(place_x, place_y), node_map, out, constraints_out, scroll_source, record, depth + 1);
2029 }
2030 }
2031 content_size = child_size;
2032 child_size
2033 }
2034 _ => {
2035 let mut child_size = LayoutSize::ZERO;
2036 if !node.children_ids.is_empty() {
2037 for child_id in &node.children_ids {
2038 child_size = self.layout_node_constraints(*child_id, constraints, origin, node_map, out, constraints_out, scroll_source, record, depth + 1);
2039 }
2040 }
2041 content_size = child_size;
2042 constraints.constrain(child_size)
2043 }
2044 };
2045
2046 if let Some(runs) = &node.rich_text {
2047 if let Some(measurer) = &self.measurer {
2048 let node_max_w = match &node.op {
2049 LayoutOp::Box { max_width, .. } => *max_width,
2050 _ => None,
2051 };
2052 let avail_w = {
2053 let from_constraints = if constraints.is_width_bounded() {
2054 Some(constraints.max_w)
2055 } else {
2056 None
2057 };
2058 match (from_constraints, node_max_w) {
2059 (Some(c), Some(m)) => Some(c.min(m)),
2060 (Some(c), None) => Some(c),
2061 (None, Some(m)) => Some(m),
2062 (None, None) => None,
2063 }
2064 };
2065 let (mw, mh) = if runs.len() == 1 {
2066 let run = &runs[0];
2067 measurer.measure(&run.text, run.style.font_size, avail_w)
2068 } else {
2069 measurer.measure_rich_text(runs, avail_w)
2070 };
2071 let text_content = LayoutSize::new(mw, mh);
2072 let measured = constraints.constrain(text_content);
2073 if node.children_ids.is_empty() {
2074 content_size = text_content;
2075 return self.record_geometry(node_id, origin, measured, text_content, out, record);
2076 }
2077 content_size.width = content_size.width.max(text_content.width);
2078 content_size.height = content_size.height.max(text_content.height);
2079 }
2080 }
2081
2082 self.record_geometry(node_id, origin, size, content_size, out, record)
2083 }
2084
2085 fn record_geometry(
2086 &self,
2087 node_id: NodeId,
2088 origin: LayoutPoint,
2089 size: LayoutSize,
2090 content_size: LayoutSize,
2091 out: &mut HashMap<NodeId, LayoutNodeGeometry>,
2092 record: bool,
2093 ) -> LayoutSize {
2094 let mut rect_origin = origin;
2095 let mut rect_size = size;
2096 let mut rect_content = content_size;
2097 let mut had_non_finite = false;
2098
2099 if !rect_origin.x.is_finite() { rect_origin.x = 0.0; had_non_finite = true; }
2100 if !rect_origin.y.is_finite() { rect_origin.y = 0.0; had_non_finite = true; }
2101 if !rect_size.width.is_finite() { rect_size.width = 0.0; had_non_finite = true; }
2102 if !rect_size.height.is_finite() { rect_size.height = 0.0; had_non_finite = true; }
2103 if !rect_content.width.is_finite() { rect_content.width = 0.0; had_non_finite = true; }
2104 if !rect_content.height.is_finite() { rect_content.height = 0.0; had_non_finite = true; }
2105
2106 if had_non_finite {
2107 diag::emit(diag::DiagCategory::Invariants, diag::DiagLevel::Error, diag::DiagEventKind::InvariantViolation {
2108 kind: "non_finite_layout".into(),
2109 node: Some(node_id.as_u128()),
2110 details: format!("origin=({:.2},{:.2}) size=({:.2},{:.2}) content=({:.2},{:.2})", origin.x, origin.y, size.width, size.height, content_size.width, content_size.height),
2111 dump_ref: None,
2112 });
2113 }
2114
2115 if record {
2116 let rect = LayoutRect::new(rect_origin.x, rect_origin.y, rect_size.width, rect_size.height);
2117 out.insert(node_id, LayoutNodeGeometry { rect, content_size: rect_content });
2118 }
2119 rect_size
2120 }
2121}