1use anyhow::Result;
32use fission_diagnostics::prelude as diag;
33use fission_ir::op::{RichTextAnnotation, TextParagraphStyle, TextRun};
34use fission_ir::{FlexDirection as IrFlexDirection, FlexWrap as IrFlexWrap, NodeId};
35use serde::{Deserialize, Serialize};
36use std::collections::hash_map::DefaultHasher;
37use std::collections::{HashMap, HashSet};
38use std::hash::{Hash, Hasher};
39use std::sync::Arc;
40
41pub use fission_ir::{FlexDirection, GridPlacement, GridTrack, LayoutOp};
42
43pub trait ScrollDataSource {
60 fn get_offset(&self, node_id: NodeId) -> f32;
62}
63
64impl<F> ScrollDataSource for F
65where
66 F: Fn(NodeId) -> f32,
67{
68 fn get_offset(&self, node_id: NodeId) -> f32 {
69 self(node_id)
70 }
71}
72
73pub type LayoutUnit = f32;
77
78fn finite_or(value: LayoutUnit, fallback: LayoutUnit) -> LayoutUnit {
80 if value.is_finite() {
81 value
82 } else {
83 fallback
84 }
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
92pub struct LayoutPoint {
93 pub x: LayoutUnit,
95 pub y: LayoutUnit,
97}
98
99impl LayoutPoint {
100 pub const ZERO: Self = Self { x: 0.0, y: 0.0 };
102
103 pub fn new(x: LayoutUnit, y: LayoutUnit) -> Self {
105 Self { x, y }
106 }
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
114pub struct LayoutSize {
115 pub width: LayoutUnit,
117 pub height: LayoutUnit,
119}
120
121impl LayoutSize {
122 pub const ZERO: Self = Self {
124 width: 0.0,
125 height: 0.0,
126 };
127
128 pub fn new(width: LayoutUnit, height: LayoutUnit) -> Self {
130 Self { width, height }
131 }
132}
133
134#[derive(Debug, Clone, Copy, PartialEq)]
160pub struct BoxConstraints {
161 pub min_w: LayoutUnit,
163 pub max_w: LayoutUnit,
165 pub min_h: LayoutUnit,
167 pub max_h: LayoutUnit,
169}
170
171impl BoxConstraints {
172 pub fn tight(size: LayoutSize) -> Self {
176 Self {
177 min_w: size.width,
178 max_w: size.width,
179 min_h: size.height,
180 max_h: size.height,
181 }
182 }
183
184 pub fn loose(max_w: LayoutUnit, max_h: LayoutUnit) -> Self {
188 Self {
189 min_w: 0.0,
190 max_w,
191 min_h: 0.0,
192 max_h,
193 }
194 }
195
196 pub fn is_width_bounded(&self) -> bool {
198 self.max_w.is_finite()
199 }
200
201 pub fn is_height_bounded(&self) -> bool {
203 self.max_h.is_finite()
204 }
205
206 pub fn constrain(&self, size: LayoutSize) -> LayoutSize {
211 LayoutSize {
212 width: size.width.max(self.min_w).min(self.max_w),
213 height: size.height.max(self.min_h).min(self.max_h),
214 }
215 }
216
217 pub fn smallest(&self) -> LayoutSize {
219 LayoutSize::new(self.min_w, self.min_h)
220 }
221
222 pub fn deflate(&self, padding: [LayoutUnit; 4]) -> Self {
228 let horiz = padding[0] + padding[1];
229 let vert = padding[2] + padding[3];
230 let max_w = (self.max_w - horiz).max(0.0);
231 let max_h = (self.max_h - vert).max(0.0);
232 let min_w = (self.min_w - horiz).max(0.0).min(max_w);
233 let min_h = (self.min_h - vert).max(0.0).min(max_h);
234 Self {
235 min_w,
236 max_w,
237 min_h,
238 max_h,
239 }
240 }
241
242 pub fn tighten(&self, width: Option<LayoutUnit>, height: Option<LayoutUnit>) -> Self {
247 let mut out = *self;
248 if let Some(w) = width {
249 let clamped = w.min(out.max_w).max(out.min_w);
250 out.min_w = clamped;
251 out.max_w = clamped;
252 }
253 if let Some(h) = height {
254 let clamped = h.min(out.max_h).max(out.min_h);
255 out.min_h = clamped;
256 out.max_h = clamped;
257 }
258 if out.max_w < out.min_w {
259 out.max_w = out.min_w;
260 }
261 if out.max_h < out.min_h {
262 out.max_h = out.min_h;
263 }
264 out
265 }
266
267 pub fn apply_min_max(
273 &self,
274 min_w: Option<LayoutUnit>,
275 max_w: Option<LayoutUnit>,
276 min_h: Option<LayoutUnit>,
277 max_h: Option<LayoutUnit>,
278 ) -> Self {
279 let mut out = *self;
280 if let Some(w) = min_w {
281 out.min_w = out.min_w.max(w);
282 }
283 if let Some(h) = min_h {
284 out.min_h = out.min_h.max(h);
285 }
286 if let Some(w) = max_w {
287 out.max_w = out.max_w.min(w);
288 }
289 if let Some(h) = max_h {
290 out.max_h = out.max_h.min(h);
291 }
292 if out.max_w < out.min_w {
293 out.max_w = out.min_w;
294 }
295 if out.max_h < out.min_h {
296 out.max_h = out.min_h;
297 }
298 out
299 }
300
301 pub fn loosen(&self) -> Self {
306 Self {
307 min_w: 0.0,
308 max_w: self.max_w,
309 min_h: 0.0,
310 max_h: self.max_h,
311 }
312 }
313}
314
315#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
316struct MeasureCacheKey {
317 node_id: u128,
318 min_w: u32,
319 max_w: u32,
320 min_h: u32,
321 max_h: u32,
322}
323
324impl MeasureCacheKey {
325 fn new(node_id: NodeId, constraints: BoxConstraints) -> Self {
326 Self {
327 node_id: node_id.as_u128(),
328 min_w: constraints.min_w.to_bits(),
329 max_w: constraints.max_w.to_bits(),
330 min_h: constraints.min_h.to_bits(),
331 max_h: constraints.max_h.to_bits(),
332 }
333 }
334}
335
336#[derive(Debug, Clone, Default)]
337struct LayoutGraphValidationState {
338 duplicate_nodes: Vec<NodeId>,
339 missing_parent_refs: Vec<(NodeId, NodeId)>,
340 missing_child_refs: Vec<(NodeId, NodeId)>,
341 parent_child_mismatches: Vec<(NodeId, NodeId, Option<NodeId>)>,
342 cycle_nodes: Vec<NodeId>,
343 root_nodes: Vec<NodeId>,
344}
345
346impl LayoutGraphValidationState {
347 fn first_error(&self) -> Option<anyhow::Error> {
348 if let Some(node_id) = self.duplicate_nodes.first() {
349 return Some(anyhow::anyhow!(
350 "[layout] duplicate node id encountered during graph build: {:?}",
351 node_id
352 ));
353 }
354 if let Some((node_id, parent_id)) = self.missing_parent_refs.first() {
355 return Some(anyhow::anyhow!(
356 "[layout] node {:?} references missing parent {:?}",
357 node_id,
358 parent_id
359 ));
360 }
361 if let Some((node_id, child_id)) = self.missing_child_refs.first() {
362 return Some(anyhow::anyhow!(
363 "[layout] node {:?} references missing child {:?}",
364 node_id,
365 child_id
366 ));
367 }
368 if let Some((parent_id, child_id, actual_parent)) = self.parent_child_mismatches.first() {
369 return Some(anyhow::anyhow!(
370 "[layout] parent/child mismatch parent={:?} child={:?} child.parent_id={:?}",
371 parent_id,
372 child_id,
373 actual_parent
374 ));
375 }
376 if let Some(node_id) = self.cycle_nodes.first() {
377 return Some(anyhow::anyhow!(
378 "[layout] cycle detected while rebuilding graph at {:?}",
379 node_id
380 ));
381 }
382 None
383 }
384}
385
386#[derive(Debug, Clone, Default)]
387struct LayoutGraphState {
388 graph_version: u64,
389 last_layout_version: Option<u64>,
390 node_order: Vec<NodeId>,
391 node_fingerprints: HashMap<NodeId, u64>,
392 nodes: HashMap<NodeId, LayoutInputNode>,
393 parents: HashMap<NodeId, Option<NodeId>>,
394 children: HashMap<NodeId, Vec<NodeId>>,
395 roots: Vec<NodeId>,
396 validation: LayoutGraphValidationState,
397}
398
399#[derive(Debug, Clone, Default)]
400struct IncrementalLayoutReuseState {
401 previous_snapshot: LayoutSnapshot,
402 dirty_ancestors: HashSet<NodeId>,
403}
404
405impl LayoutGraphState {
406 fn is_empty(&self) -> bool {
407 self.nodes.is_empty()
408 }
409
410 fn mark_layout_complete(&mut self) {
411 self.last_layout_version = Some(self.graph_version);
412 }
413
414 fn matches_input_nodes(&self, input_nodes: &[LayoutInputNode]) -> bool {
415 if self.nodes.len() != input_nodes.len() || self.node_order.len() != input_nodes.len() {
416 return false;
417 }
418
419 for (expected_id, node) in self.node_order.iter().zip(input_nodes.iter()) {
420 if *expected_id != node.id {
421 return false;
422 }
423 let Some(existing) = self.node_fingerprints.get(&node.id) else {
424 return false;
425 };
426 if *existing != layout_input_fingerprint(node) {
427 return false;
428 }
429 }
430
431 true
432 }
433
434 fn from_input_nodes(input_nodes: &[LayoutInputNode], version: u64) -> Self {
435 let mut state = Self {
436 graph_version: version,
437 ..Self::default()
438 };
439 state.replace_all_nodes(input_nodes);
440 state
441 }
442
443 fn replace_all_nodes(&mut self, input_nodes: &[LayoutInputNode]) {
444 self.node_order.clear();
445 self.node_fingerprints.clear();
446 self.nodes.clear();
447 self.last_layout_version = None;
448
449 let mut validation = LayoutGraphValidationState::default();
450 let mut seen = HashSet::new();
451 for node in input_nodes {
452 if !seen.insert(node.id) {
453 validation.duplicate_nodes.push(node.id);
454 } else {
455 self.node_order.push(node.id);
456 }
457 self.node_fingerprints
458 .insert(node.id, layout_input_fingerprint(node));
459 self.nodes.insert(node.id, node.clone());
460 }
461
462 self.rebuild_topology(validation);
463 }
464
465 fn update_nodes(&mut self, input_nodes: &[LayoutInputNode]) {
466 let mut validation = LayoutGraphValidationState::default();
467 let mut seen = HashSet::new();
468 let mut next_order = Vec::with_capacity(input_nodes.len());
469 let mut next_fingerprints = HashMap::with_capacity(input_nodes.len());
470 let mut next_nodes = HashMap::with_capacity(input_nodes.len());
471
472 for node in input_nodes {
473 if !seen.insert(node.id) {
474 validation.duplicate_nodes.push(node.id);
475 continue;
476 }
477 next_order.push(node.id);
478 next_fingerprints.insert(node.id, layout_input_fingerprint(node));
479 next_nodes.insert(node.id, node.clone());
480 }
481
482 self.node_order = next_order;
483 self.node_fingerprints = next_fingerprints;
484 self.nodes = next_nodes;
485 self.last_layout_version = None;
486 self.rebuild_topology(validation);
487 }
488
489 fn rebuild_topology(&mut self, mut validation: LayoutGraphValidationState) {
490 self.parents.clear();
491 self.children.clear();
492 self.roots.clear();
493
494 for node_id in &self.node_order {
495 let Some(node) = self.nodes.get(node_id) else {
496 continue;
497 };
498 self.parents.insert(*node_id, node.parent_id);
499 self.children.insert(*node_id, node.children_ids.clone());
500 if node.parent_id.is_none() {
501 self.roots.push(*node_id);
502 } else if let Some(parent_id) = node.parent_id {
503 if !self.nodes.contains_key(&parent_id) {
504 validation.missing_parent_refs.push((*node_id, parent_id));
505 }
506 }
507 }
508
509 for node_id in &self.node_order {
510 let Some(node) = self.nodes.get(node_id) else {
511 continue;
512 };
513 for child_id in &node.children_ids {
514 let Some(child) = self.nodes.get(child_id) else {
515 validation.missing_child_refs.push((*node_id, *child_id));
516 continue;
517 };
518 if child.parent_id != Some(*node_id) {
519 validation
520 .parent_child_mismatches
521 .push((*node_id, *child_id, child.parent_id));
522 }
523 }
524 }
525
526 validation.root_nodes = self.roots.clone();
527 validation.cycle_nodes = self.detect_cycle_nodes();
528 self.validation = validation;
529 }
530
531 fn node(&self, node_id: NodeId) -> Option<&LayoutInputNode> {
532 self.nodes.get(&node_id)
533 }
534
535 fn children_of(&self, node_id: NodeId) -> &[NodeId] {
536 self.children
537 .get(&node_id)
538 .map(Vec::as_slice)
539 .unwrap_or(&[])
540 }
541
542 fn parent_of(&self, node_id: NodeId) -> Option<NodeId> {
543 self.parents.get(&node_id).copied().flatten()
544 }
545
546 fn ordered_nodes(&self) -> impl Iterator<Item = &LayoutInputNode> {
547 self.node_order
548 .iter()
549 .filter_map(|node_id| self.nodes.get(node_id))
550 }
551
552 fn detect_cycle_nodes(&self) -> Vec<NodeId> {
553 fn dfs(
554 node_id: NodeId,
555 children: &HashMap<NodeId, Vec<NodeId>>,
556 visited: &mut HashSet<NodeId>,
557 stack: &mut HashSet<NodeId>,
558 cycle_nodes: &mut Vec<NodeId>,
559 ) {
560 if stack.contains(&node_id) {
561 cycle_nodes.push(node_id);
562 return;
563 }
564 if !visited.insert(node_id) {
565 return;
566 }
567
568 stack.insert(node_id);
569 if let Some(child_nodes) = children.get(&node_id) {
570 for child_id in child_nodes {
571 dfs(*child_id, children, visited, stack, cycle_nodes);
572 }
573 }
574 stack.remove(&node_id);
575 }
576
577 let mut visited = HashSet::new();
578 let mut stack = HashSet::new();
579 let mut cycle_nodes = Vec::new();
580 for node_id in &self.node_order {
581 dfs(
582 *node_id,
583 &self.children,
584 &mut visited,
585 &mut stack,
586 &mut cycle_nodes,
587 );
588 }
589 cycle_nodes.sort_by_key(|node_id| node_id.as_u128());
590 cycle_nodes.dedup();
591 cycle_nodes
592 }
593}
594
595#[cfg(test)]
596mod tests {
597 use super::{LayoutEngine, LayoutGraphState, LayoutInputNode};
598 use fission_ir::{LayoutOp, NodeId};
599
600 fn box_node(
601 id: NodeId,
602 parent_id: Option<NodeId>,
603 children_ids: Vec<NodeId>,
604 ) -> LayoutInputNode {
605 LayoutInputNode {
606 id,
607 parent_id,
608 op: LayoutOp::Box {
609 width: Some(40.0),
610 height: Some(20.0),
611 min_width: None,
612 max_width: None,
613 min_height: None,
614 max_height: None,
615 padding: [0.0; 4],
616 flex_grow: 0.0,
617 flex_shrink: 0.0,
618 aspect_ratio: None,
619 },
620 children_ids,
621 debug_name: format!("node-{}", id.as_u128()),
622 width: Some(40.0),
623 height: Some(20.0),
624 flex_grow: 0.0,
625 flex_shrink: 0.0,
626 rich_text: None,
627 }
628 }
629
630 #[test]
631 fn matches_input_nodes_rejects_reordered_flattened_inputs() {
632 let root = NodeId::from_u128(1);
633 let first = NodeId::from_u128(2);
634 let second = NodeId::from_u128(3);
635 let canonical = vec![
636 box_node(root, None, vec![first, second]),
637 box_node(first, Some(root), vec![]),
638 box_node(second, Some(root), vec![]),
639 ];
640 let reordered = vec![
641 box_node(root, None, vec![first, second]),
642 box_node(second, Some(root), vec![]),
643 box_node(first, Some(root), vec![]),
644 ];
645
646 let state = LayoutGraphState::from_input_nodes(&canonical, 1);
647 assert!(!state.matches_input_nodes(&reordered));
648 }
649
650 #[test]
651 fn update_refreshes_node_order_for_reordered_flattened_inputs() {
652 let root = NodeId::from_u128(10);
653 let first = NodeId::from_u128(11);
654 let second = NodeId::from_u128(12);
655 let canonical = vec![
656 box_node(root, None, vec![first, second]),
657 box_node(first, Some(root), vec![]),
658 box_node(second, Some(root), vec![]),
659 ];
660 let reordered = vec![
661 box_node(root, None, vec![first, second]),
662 box_node(second, Some(root), vec![]),
663 box_node(first, Some(root), vec![]),
664 ];
665
666 let mut engine = LayoutEngine::new();
667 engine.update(&canonical);
668 engine.update(&reordered);
669
670 let ordered = engine
671 .graph_state
672 .ordered_nodes()
673 .map(|node| node.id)
674 .collect::<Vec<_>>();
675 assert_eq!(ordered, vec![root, second, first]);
676 }
677}
678
679fn layout_input_fingerprint(node: &LayoutInputNode) -> u64 {
680 let mut hasher = DefaultHasher::new();
681 format!("{node:?}").hash(&mut hasher);
682 hasher.finish()
683}
684
685#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
700pub struct LayoutRect {
701 pub origin: LayoutPoint,
703 pub size: LayoutSize,
705}
706
707impl LayoutRect {
708 pub fn new(x: LayoutUnit, y: LayoutUnit, width: LayoutUnit, height: LayoutUnit) -> Self {
710 Self {
711 origin: LayoutPoint { x, y },
712 size: LayoutSize { width, height },
713 }
714 }
715
716 pub fn x(&self) -> LayoutUnit {
718 self.origin.x
719 }
720 pub fn y(&self) -> LayoutUnit {
722 self.origin.y
723 }
724 pub fn width(&self) -> LayoutUnit {
726 self.size.width
727 }
728 pub fn height(&self) -> LayoutUnit {
730 self.size.height
731 }
732
733 pub fn right(&self) -> LayoutUnit {
735 self.origin.x + self.size.width
736 }
737 pub fn bottom(&self) -> LayoutUnit {
739 self.origin.y + self.size.height
740 }
741
742 pub fn contains(&self, p: LayoutPoint) -> bool {
745 p.x >= self.x() && p.x < self.right() && p.y >= self.y() && p.y < self.bottom()
746 }
747}
748
749#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
755pub struct LayoutNodeGeometry {
756 pub rect: LayoutRect,
758 pub content_size: LayoutSize,
761}
762
763#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
779pub struct LayoutSnapshot {
780 pub nodes: HashMap<NodeId, LayoutNodeGeometry>,
782 #[serde(skip)]
785 pub constraints: HashMap<NodeId, BoxConstraints>,
786 pub viewport_size: LayoutSize,
788}
789
790impl LayoutSnapshot {
791 pub fn new(viewport_size: LayoutSize) -> Self {
793 Self {
794 nodes: HashMap::new(),
795 constraints: HashMap::new(),
796 viewport_size,
797 }
798 }
799
800 pub fn get_node_geometry(&self, node_id: NodeId) -> Option<&LayoutNodeGeometry> {
803 self.nodes.get(&node_id)
804 }
805
806 pub fn get_node_rect(&self, node_id: NodeId) -> Option<LayoutRect> {
808 self.nodes.get(&node_id).map(|g| g.rect)
809 }
810
811 pub fn get_node_constraints(&self, node_id: NodeId) -> Option<BoxConstraints> {
814 self.constraints.get(&node_id).copied()
815 }
816}
817
818#[derive(Debug, Clone)]
827pub struct LayoutInputNode {
828 pub id: NodeId,
830 pub parent_id: Option<NodeId>,
832 pub op: LayoutOp,
834 pub children_ids: Vec<NodeId>,
836 pub debug_name: String,
838 pub width: Option<LayoutUnit>,
840 pub height: Option<LayoutUnit>,
842 pub flex_grow: LayoutUnit,
844 pub flex_shrink: LayoutUnit,
846 pub rich_text: Option<Vec<TextRun>>,
849}
850
851pub struct LineMetric {
857 pub start_index: usize,
859 pub end_index: usize,
861 pub baseline: f32,
863 pub height: f32,
865 pub width: f32,
867}
868
869#[derive(Debug, Clone, Copy, PartialEq)]
870pub struct RichTextInlineBox {
871 pub id: u64,
872 pub x: f32,
873 pub y: f32,
874 pub width: f32,
875 pub height: f32,
876}
877
878#[derive(Debug, Clone, PartialEq)]
879pub struct RichTextLayoutInfo {
880 pub width: f32,
881 pub height: f32,
882 pub inline_boxes: Vec<RichTextInlineBox>,
883}
884
885pub trait TextMeasurer: Send + Sync {
905 fn measure(&self, text: &str, font_size: f32, available_width: Option<f32>) -> (f32, f32);
910
911 fn hit_test(
916 &self,
917 _text: &str,
918 _font_size: f32,
919 _available_width: Option<f32>,
920 _x: f32,
921 _y: f32,
922 ) -> usize {
923 0
924 }
925
926 fn get_line_metrics(
931 &self,
932 _text: &str,
933 _font_size: f32,
934 _available_width: Option<f32>,
935 ) -> Vec<LineMetric> {
936 vec![]
937 }
938
939 fn get_caret_position(
944 &self,
945 _text: &str,
946 _font_size: f32,
947 _available_width: Option<f32>,
948 _caret_index: usize,
949 ) -> (f32, f32) {
950 (0.0, 0.0)
951 }
952
953 fn measure_rich_text(&self, _runs: &[TextRun], _available_width: Option<f32>) -> (f32, f32) {
957 (0.0, 0.0)
958 }
959
960 fn layout_rich_text(
966 &self,
967 runs: &[TextRun],
968 available_width: Option<f32>,
969 ) -> RichTextLayoutInfo {
970 let (width, height) = if runs.len() == 1 {
971 let run = &runs[0];
972 self.measure(&run.text, run.style.font_size, available_width)
973 } else {
974 self.measure_rich_text(runs, available_width)
975 };
976 RichTextLayoutInfo {
977 width,
978 height,
979 inline_boxes: Vec::new(),
980 }
981 }
982
983 fn hit_test_rich(
987 &self,
988 runs: &[TextRun],
989 _available_width: Option<f32>,
990 x: f32,
991 y: f32,
992 ) -> usize {
993 let text: String = runs.iter().map(|r| r.text.as_str()).collect();
995 let font_size = runs.first().map(|r| r.style.font_size).unwrap_or(13.0);
996 self.hit_test(&text, font_size, None, x, y)
997 }
998
999 fn resolve_rich_text_annotation_at_point(
1004 &self,
1005 _runs: &[TextRun],
1006 _available_width: Option<f32>,
1007 _x: f32,
1008 _y: f32,
1009 _paragraph_style: TextParagraphStyle,
1010 _annotations: &[RichTextAnnotation],
1011 ) -> Option<RichTextAnnotation> {
1012 None
1013 }
1014}
1015
1016pub struct LayoutEngine {
1039 measurer: Option<Arc<dyn TextMeasurer>>,
1040 graph_state: LayoutGraphState,
1041 next_graph_version: u64,
1042 incremental_reuse: Option<IncrementalLayoutReuseState>,
1043}
1044
1045impl LayoutEngine {
1046 const MAX_LAYOUT_RECURSION_DEPTH: usize = 100;
1047
1048 pub fn new() -> Self {
1053 Self {
1054 measurer: None,
1055 graph_state: LayoutGraphState::default(),
1056 next_graph_version: 1,
1057 incremental_reuse: None,
1058 }
1059 }
1060
1061 pub fn with_measurer(mut self, measurer: Arc<dyn TextMeasurer>) -> Self {
1065 self.measurer = Some(measurer);
1066 self
1067 }
1068
1069 fn allocate_graph_version(&mut self) -> u64 {
1070 let version = self.next_graph_version;
1071 self.next_graph_version = self.next_graph_version.saturating_add(1);
1072 version
1073 }
1074
1075 fn refresh_graph_state(&mut self, input_nodes: &[LayoutInputNode]) {
1076 let version = self.allocate_graph_version();
1077 self.graph_state = LayoutGraphState::from_input_nodes(input_nodes, version);
1078 }
1079
1080 fn ensure_graph_state(&mut self, input_nodes: &[LayoutInputNode]) {
1081 if self.graph_state.is_empty() || !self.graph_state.matches_input_nodes(input_nodes) {
1082 self.refresh_graph_state(input_nodes);
1083 }
1084 }
1085
1086 fn validate_graph_state(&self, root: NodeId) -> Result<()> {
1087 if let Some(err) = self.graph_state.validation.first_error() {
1088 return Err(err);
1089 }
1090 if !self.graph_state.nodes.contains_key(&root) {
1091 anyhow::bail!("[verify] missing node {:?}", root);
1092 }
1093 if !self.graph_state.roots.contains(&root)
1094 && self
1095 .graph_state
1096 .parents
1097 .get(&root)
1098 .copied()
1099 .flatten()
1100 .is_some()
1101 {
1102 anyhow::bail!("[verify] root {:?} is not a graph root", root);
1103 }
1104 if let Some(last_layout_version) = self.graph_state.last_layout_version {
1105 if last_layout_version > self.graph_state.graph_version {
1106 anyhow::bail!(
1107 "[verify] cached layout version {} exceeds graph version {}",
1108 last_layout_version,
1109 self.graph_state.graph_version
1110 );
1111 }
1112 }
1113 Ok(())
1114 }
1115
1116 pub fn update(&mut self, input_nodes: &[LayoutInputNode]) {
1121 if self.graph_state.is_empty() {
1122 self.refresh_graph_state(input_nodes);
1123 return;
1124 }
1125
1126 if self.graph_state.matches_input_nodes(input_nodes) {
1127 return;
1128 }
1129
1130 let version = self.allocate_graph_version();
1131 self.graph_state.graph_version = version;
1132 self.graph_state.update_nodes(input_nodes);
1133 }
1134
1135 pub fn rebuild(&mut self, input_nodes: &[LayoutInputNode]) -> Result<()> {
1137 self.refresh_graph_state(input_nodes);
1138 if let Some(err) = self.graph_state.validation.first_error() {
1139 return Err(err);
1140 }
1141 Ok(())
1142 }
1143
1144 pub fn verify_post_update(&self, input_nodes: &[LayoutInputNode], root: NodeId) -> Result<()> {
1149 if self.graph_state.matches_input_nodes(input_nodes) {
1150 return self.validate_graph_state(root);
1151 }
1152
1153 let node_map: HashMap<NodeId, &LayoutInputNode> =
1154 input_nodes.iter().map(|n| (n.id, n)).collect();
1155 for n in input_nodes {
1157 for child in &n.children_ids {
1158 let child_node = node_map
1159 .get(child)
1160 .ok_or_else(|| anyhow::anyhow!("[verify] child {:?} not found", child))?;
1161 if child_node.parent_id != Some(n.id) {
1162 anyhow::bail!("[verify] parent/child mismatch parent={:?} child={:?} child.parent_id={:?}", n.id, child, child_node.parent_id);
1163 }
1164 }
1165 }
1166 fn dfs(
1168 id: NodeId,
1169 map: &HashMap<NodeId, &LayoutInputNode>,
1170 visited: &mut HashSet<NodeId>,
1171 stack: &mut HashSet<NodeId>,
1172 ) -> Result<()> {
1173 if !visited.insert(id) {
1174 return Ok(());
1175 }
1176 stack.insert(id);
1177 let node = map
1178 .get(&id)
1179 .ok_or_else(|| anyhow::anyhow!("[verify] missing node {:?}", id))?;
1180 for child in &node.children_ids {
1181 if stack.contains(child) {
1182 anyhow::bail!("[verify] cycle detected at {:?} -> {:?}", id, child);
1183 }
1184 dfs(*child, map, visited, stack)?;
1185 }
1186 stack.remove(&id);
1187 Ok(())
1188 }
1189 let mut visited = HashSet::new();
1190 let mut stack = HashSet::new();
1191 dfs(root, &node_map, &mut visited, &mut stack)?;
1192 Ok(())
1193 }
1194
1195 pub fn compute_layout(
1213 &mut self,
1214 input_nodes: &[LayoutInputNode],
1215 root_node_id: NodeId,
1216 viewport_size: LayoutSize,
1217 scroll_source: &impl ScrollDataSource,
1218 ) -> Result<LayoutSnapshot> {
1219 self.ensure_graph_state(input_nodes);
1220 self.validate_graph_state(root_node_id)?;
1221 let snapshot = self.compute_layout_constraints(
1222 input_nodes,
1223 root_node_id,
1224 viewport_size,
1225 scroll_source,
1226 )?;
1227 self.emit_scroll_diagnostics(&snapshot);
1228 Ok(snapshot)
1229 }
1230
1231 pub fn compute_layout_constraints(
1236 &mut self,
1237 input_nodes: &[LayoutInputNode],
1238 root_node_id: NodeId,
1239 viewport_size: LayoutSize,
1240 scroll_source: &impl ScrollDataSource,
1241 ) -> Result<LayoutSnapshot> {
1242 self.ensure_graph_state(input_nodes);
1243 self.validate_graph_state(root_node_id)?;
1244
1245 let mut constraints = BoxConstraints::tight(viewport_size);
1247 if let Some(root) = self.graph_state.node(root_node_id) {
1248 if root.width.is_some() || root.height.is_some() {
1250 constraints = BoxConstraints::loose(viewport_size.width, viewport_size.height)
1251 .tighten(root.width, root.height);
1252 }
1253 }
1254
1255 let mut snapshot = LayoutSnapshot::new(viewport_size);
1256 let mut measure_cache = HashMap::new();
1257 self.layout_node_constraints(
1258 root_node_id,
1259 constraints,
1260 LayoutPoint::ZERO,
1261 &mut snapshot.nodes,
1262 &mut snapshot.constraints,
1263 &mut measure_cache,
1264 scroll_source,
1265 true,
1266 0,
1267 )?;
1268
1269 let visual_location = |node_id: NodeId| -> Option<LayoutPoint> {
1270 let mut pos = snapshot.nodes.get(&node_id)?.rect.origin;
1271 let mut current = self.graph_state.parent_of(node_id);
1272 while let Some(parent_id) = current {
1273 if let Some(parent) = self.graph_state.node(parent_id) {
1274 if let LayoutOp::Scroll { direction, .. } = &parent.op {
1275 let offset = scroll_source.get_offset(parent_id);
1276 match direction {
1277 FlexDirection::Row => pos.x -= offset,
1278 FlexDirection::Column => pos.y -= offset,
1279 }
1280 }
1281 current = self.graph_state.parent_of(parent_id);
1282 } else {
1283 break;
1284 }
1285 }
1286 Some(pos)
1287 };
1288
1289 let mut flyout_abs_overrides: HashMap<NodeId, (f32, f32)> = HashMap::new();
1290 for node in self.graph_state.ordered_nodes() {
1291 if let LayoutOp::Flyout { anchor, content } = node.op {
1292 if let (Some(anchor_geom), Some(content_geom)) =
1293 (snapshot.nodes.get(&anchor), snapshot.nodes.get(&content))
1294 {
1295 if let Some(anchor_abs) = visual_location(anchor) {
1296 let content_w = content_geom.rect.width();
1297 let content_h = content_geom.rect.height();
1298 let anchor_h = anchor_geom.rect.height();
1299 let max_left = (snapshot.viewport_size.width - content_w).max(0.0);
1300 let left_rel = anchor_abs.x.clamp(0.0, max_left);
1301
1302 let below_top = anchor_abs.y + anchor_h;
1303 let max_top = (snapshot.viewport_size.height - content_h).max(0.0);
1304 let top_rel = if below_top + content_h <= snapshot.viewport_size.height {
1305 below_top
1306 } else {
1307 let above_top = anchor_abs.y - content_h;
1308 if above_top >= 0.0 {
1309 above_top
1310 } else {
1311 below_top.clamp(0.0, max_top)
1312 }
1313 };
1314 flyout_abs_overrides.insert(content, (left_rel, top_rel));
1315 }
1316 }
1317 }
1318 }
1319
1320 if !flyout_abs_overrides.is_empty() {
1321 for (nid, (abs_x, abs_y)) in flyout_abs_overrides {
1322 if let Some(current) = snapshot.nodes.get(&nid) {
1323 let dx = abs_x - current.rect.origin.x;
1324 let dy = abs_y - current.rect.origin.y;
1325 let mut stack = vec![(nid, 0usize)];
1326 while let Some((current_id, depth)) = stack.pop() {
1327 if depth > Self::MAX_LAYOUT_RECURSION_DEPTH {
1328 return Err(self.layout_depth_overflow(current_id, depth));
1329 }
1330 if let Some(geometry) = snapshot.nodes.get_mut(¤t_id) {
1331 geometry.rect.origin.x += dx;
1332 geometry.rect.origin.y += dy;
1333 }
1334 for child_id in self.graph_state.children_of(current_id).iter().rev() {
1335 stack.push((*child_id, depth + 1));
1336 }
1337 }
1338 }
1339 }
1340 }
1341
1342 self.graph_state.mark_layout_complete();
1343 self.incremental_reuse = None;
1344
1345 Ok(snapshot)
1346 }
1347
1348 pub fn compute_layout_incremental(
1349 &mut self,
1350 input_nodes: &[LayoutInputNode],
1351 root_node_id: NodeId,
1352 viewport_size: LayoutSize,
1353 scroll_source: &impl ScrollDataSource,
1354 previous_snapshot: &LayoutSnapshot,
1355 dirty_nodes: &HashSet<NodeId>,
1356 ) -> Result<LayoutSnapshot> {
1357 self.ensure_graph_state(input_nodes);
1358 self.validate_graph_state(root_node_id)?;
1359
1360 let mut dirty_ancestors = HashSet::new();
1361 for node_id in dirty_nodes {
1362 let mut current = Some(*node_id);
1363 while let Some(id) = current {
1364 if !dirty_ancestors.insert(id) {
1365 break;
1366 }
1367 current = self.graph_state.parent_of(id);
1368 }
1369 }
1370 dirty_ancestors.insert(root_node_id);
1371
1372 self.incremental_reuse = Some(IncrementalLayoutReuseState {
1373 previous_snapshot: previous_snapshot.clone(),
1374 dirty_ancestors,
1375 });
1376 let result = self.compute_layout_constraints(
1377 input_nodes,
1378 root_node_id,
1379 viewport_size,
1380 scroll_source,
1381 );
1382 self.incremental_reuse = None;
1383 result
1384 }
1385
1386 fn emit_scroll_diagnostics(&self, snapshot: &LayoutSnapshot) {
1387 use fission_diagnostics::prelude as diag;
1388 let trace_scroll = std::env::var("FISSION_SCROLL_TRACE").ok().as_deref() == Some("1");
1389 for n in self.graph_state.ordered_nodes() {
1390 if let LayoutOp::Scroll { .. } = n.op {
1391 if let Some(g) = snapshot.nodes.get(&n.id) {
1392 let note = if g.rect.height() <= 0.0 {
1393 let parent_op = n
1394 .parent_id
1395 .and_then(|pid| self.graph_state.node(pid))
1396 .map(|p| format!("{:?}", p.op));
1397 let parent_constraints = n
1398 .parent_id
1399 .and_then(|pid| snapshot.constraints.get(&pid))
1400 .copied();
1401 snapshot
1402 .constraints
1403 .get(&n.id)
1404 .map(|c| {
1405 format!(
1406 "op={:?} parent={:?} parent_op={:?} parent_constraints={:?} constraints={:?}",
1407 n.op,
1408 n.parent_id,
1409 parent_op,
1410 parent_constraints,
1411 c
1412 )
1413 })
1414 } else {
1415 None
1416 };
1417 diag::emit(
1418 diag::DiagCategory::Layout,
1419 diag::DiagLevel::Debug,
1420 diag::DiagEventKind::ScrollExtent {
1421 node: n.id.as_u128(),
1422 viewport_w: g.rect.width(),
1423 viewport_h: g.rect.height(),
1424 content_w: g.content_size.width,
1425 content_h: g.content_size.height,
1426 note,
1427 },
1428 );
1429 if trace_scroll {
1430 eprintln!(
1431 "[scroll-trace] node={} viewport=({:.1},{:.1}) content=({:.1},{:.1})",
1432 n.id.as_u128(),
1433 g.rect.width(),
1434 g.rect.height(),
1435 g.content_size.width,
1436 g.content_size.height
1437 );
1438 }
1439 }
1440 }
1441 }
1442 }
1443
1444 fn layout_depth_overflow(&self, node_id: NodeId, depth: usize) -> anyhow::Error {
1445 let details = format!(
1446 "layout recursion depth {} exceeded max {} at node {}",
1447 depth,
1448 Self::MAX_LAYOUT_RECURSION_DEPTH,
1449 node_id.as_u128()
1450 );
1451 diag::emit(
1452 diag::DiagCategory::Invariants,
1453 diag::DiagLevel::Error,
1454 diag::DiagEventKind::InvariantViolation {
1455 kind: "layout_recursion_depth".into(),
1456 node: Some(node_id.as_u128()),
1457 details: details.clone(),
1458 dump_ref: None,
1459 },
1460 );
1461 anyhow::anyhow!(details)
1462 }
1463
1464 fn copy_cached_subtree(
1465 &self,
1466 node_id: NodeId,
1467 origin: LayoutPoint,
1468 current_constraints: BoxConstraints,
1469 out: &mut HashMap<NodeId, LayoutNodeGeometry>,
1470 constraints_out: &mut HashMap<NodeId, BoxConstraints>,
1471 ) -> Result<Option<LayoutSize>> {
1472 let Some(reuse) = self.incremental_reuse.as_ref() else {
1473 return Ok(None);
1474 };
1475 if reuse.dirty_ancestors.contains(&node_id) {
1476 return Ok(None);
1477 }
1478
1479 let Some(previous_geometry) = reuse.previous_snapshot.nodes.get(&node_id) else {
1480 return Ok(None);
1481 };
1482 let Some(previous_constraints) = reuse.previous_snapshot.constraints.get(&node_id).copied()
1483 else {
1484 return Ok(None);
1485 };
1486 if previous_constraints != current_constraints {
1487 return Ok(None);
1488 }
1489
1490 let dx = origin.x - previous_geometry.rect.origin.x;
1491 let dy = origin.y - previous_geometry.rect.origin.y;
1492 let mut stack = vec![(node_id, 0usize)];
1493 while let Some((current_id, depth)) = stack.pop() {
1494 if depth > Self::MAX_LAYOUT_RECURSION_DEPTH {
1495 return Err(self.layout_depth_overflow(current_id, depth));
1496 }
1497 let Some(previous_geometry) = reuse.previous_snapshot.nodes.get(¤t_id) else {
1498 return Ok(None);
1499 };
1500 let Some(previous_constraints) = reuse
1501 .previous_snapshot
1502 .constraints
1503 .get(¤t_id)
1504 .copied()
1505 else {
1506 return Ok(None);
1507 };
1508
1509 let mut geometry = previous_geometry.clone();
1510 geometry.rect.origin.x += dx;
1511 geometry.rect.origin.y += dy;
1512 out.insert(current_id, geometry);
1513 constraints_out.insert(current_id, previous_constraints);
1514
1515 let children = self.graph_state.children_of(current_id);
1516 for child_id in children.iter().rev() {
1517 stack.push((*child_id, depth + 1));
1518 }
1519 }
1520
1521 Ok(Some(previous_geometry.content_size))
1522 }
1523
1524 fn layout_node_constraints(
1525 &self,
1526 node_id: NodeId,
1527 constraints: BoxConstraints,
1528 origin: LayoutPoint,
1529 out: &mut HashMap<NodeId, LayoutNodeGeometry>,
1530 constraints_out: &mut HashMap<NodeId, BoxConstraints>,
1531 measure_cache: &mut HashMap<MeasureCacheKey, LayoutSize>,
1532 scroll_source: &impl ScrollDataSource,
1533 record: bool,
1534 depth: usize,
1535 ) -> Result<LayoutSize> {
1536 if depth > Self::MAX_LAYOUT_RECURSION_DEPTH {
1537 return Err(self.layout_depth_overflow(node_id, depth));
1538 }
1539 if !record {
1540 let cache_key = MeasureCacheKey::new(node_id, constraints);
1541 if let Some(cached) = measure_cache.get(&cache_key).copied() {
1542 return Ok(cached);
1543 }
1544 }
1545 let node = match self.graph_state.node(node_id) {
1546 Some(node) => node,
1547 None => return Ok(LayoutSize::ZERO),
1548 };
1549
1550 if record {
1551 constraints_out.insert(node_id, constraints);
1552 }
1553
1554 if record {
1555 if let Some(reused) =
1556 self.copy_cached_subtree(node_id, origin, constraints, out, constraints_out)?
1557 {
1558 return Ok(reused);
1559 }
1560 }
1561
1562 let mut flow_children: Vec<NodeId> = Vec::new();
1563 let mut abs_children: Vec<NodeId> = Vec::new();
1564 for child_id in self.graph_state.children_of(node_id) {
1565 let is_absolute = matches!(
1566 self.graph_state.node(*child_id).map(|n| &n.op),
1567 Some(LayoutOp::AbsoluteFill) | Some(LayoutOp::Positioned { .. })
1568 );
1569 if is_absolute {
1570 abs_children.push(*child_id);
1571 } else {
1572 flow_children.push(*child_id);
1573 }
1574 }
1575 let rich_text_inline_children = node.rich_text.is_some() && !flow_children.is_empty();
1576
1577 let mut content_size;
1578 let size = match &node.op {
1579 LayoutOp::Box {
1580 width,
1581 height,
1582 min_width,
1583 max_width,
1584 min_height,
1585 max_height,
1586 padding,
1587 aspect_ratio,
1588 ..
1589 } => {
1590 let mut local =
1591 constraints.apply_min_max(*min_width, *max_width, *min_height, *max_height);
1592 local = local.tighten(*width, *height);
1593 if let Some(ratio) = aspect_ratio.filter(|r| *r > 0.0) {
1594 let mut target_w = *width;
1595 let mut target_h = *height;
1596
1597 if target_w.is_some() && target_h.is_none() {
1598 target_h = Some(target_w.unwrap() / ratio);
1599 } else if target_h.is_some() && target_w.is_none() {
1600 target_w = Some(target_h.unwrap() * ratio);
1601 } else if target_w.is_none() && target_h.is_none() {
1602 if local.is_width_bounded() || local.is_height_bounded() {
1603 let (mut w, mut h) = if local.is_width_bounded() {
1604 let w = local.max_w;
1605 let h = w / ratio;
1606 (w, h)
1607 } else {
1608 let h = local.max_h;
1609 let w = h * ratio;
1610 (w, h)
1611 };
1612 if local.is_width_bounded()
1613 && local.is_height_bounded()
1614 && h > local.max_h
1615 {
1616 h = local.max_h;
1617 w = h * ratio;
1618 }
1619 target_w = Some(w);
1620 target_h = Some(h);
1621 }
1622 }
1623
1624 if target_w.is_some() || target_h.is_some() {
1625 local = local.tighten(target_w, target_h);
1626 }
1627 }
1628 let base_child_constraints = local.deflate(*padding);
1629 let mut max_child = LayoutSize::ZERO;
1630 let mut measured_children: Vec<(NodeId, BoxConstraints, LayoutSize)> = Vec::new();
1631 if !rich_text_inline_children {
1632 for child_id in &flow_children {
1633 let (child_width, child_height, child_max_width, child_max_height) = self
1634 .graph_state
1635 .node(*child_id)
1636 .map(|child| match &child.op {
1637 LayoutOp::Box {
1638 width,
1639 height,
1640 max_width,
1641 max_height,
1642 ..
1643 } => (*width, *height, *max_width, *max_height),
1644 LayoutOp::Scroll {
1645 width,
1646 height,
1647 max_width,
1648 max_height,
1649 ..
1650 } => (*width, *height, *max_width, *max_height),
1651 LayoutOp::Embed { width, height, .. } => {
1652 (*width, *height, None, None)
1653 }
1654 _ => (None, None, None, None),
1655 })
1656 .unwrap_or((None, None, None, None));
1657 let mut child_constraints = base_child_constraints;
1658 let stretch_width = child_constraints.min_w == child_constraints.max_w
1659 && child_width.is_none()
1660 && child_max_width.is_none();
1661 if stretch_width {
1662 child_constraints.min_w = child_constraints.max_w;
1663 } else {
1664 child_constraints.min_w = 0.0;
1665 }
1666 let stretch_height = child_constraints.min_h == child_constraints.max_h
1667 && child_height.is_none()
1668 && child_max_height.is_none();
1669 if stretch_height {
1670 child_constraints.min_h = child_constraints.max_h;
1671 } else {
1672 child_constraints.min_h = 0.0;
1673 }
1674 let child_size = self.layout_node_constraints(
1675 *child_id,
1676 child_constraints,
1677 LayoutPoint::ZERO,
1678 out,
1679 constraints_out,
1680 measure_cache,
1681 scroll_source,
1682 false,
1683 depth + 1,
1684 )?;
1685 max_child.width = max_child.width.max(child_size.width);
1686 max_child.height = max_child.height.max(child_size.height);
1687 measured_children.push((*child_id, child_constraints, child_size));
1688 }
1689 }
1690 let padded = LayoutSize::new(
1691 max_child.width + padding[0] + padding[1],
1692 max_child.height + padding[2] + padding[3],
1693 );
1694 let size = local.constrain(padded);
1695 if record {
1696 for (child_id, child_constraints, _child_size) in measured_children {
1697 self.layout_node_constraints(
1698 child_id,
1699 child_constraints,
1700 LayoutPoint::new(origin.x + padding[0], origin.y + padding[2]),
1701 out,
1702 constraints_out,
1703 measure_cache,
1704 scroll_source,
1705 record,
1706 depth + 1,
1707 )?;
1708 }
1709 if !abs_children.is_empty() {
1710 let abs_constraints = BoxConstraints::loose(size.width, size.height);
1711 for child_id in abs_children {
1712 self.layout_node_constraints(
1713 child_id,
1714 abs_constraints,
1715 origin,
1716 out,
1717 constraints_out,
1718 measure_cache,
1719 scroll_source,
1720 record,
1721 depth + 1,
1722 )?;
1723 }
1724 }
1725 }
1726 content_size = padded;
1727 size
1728 }
1729 LayoutOp::Flex {
1730 direction,
1731 wrap,
1732 padding,
1733 gap,
1734 align_items,
1735 justify_content,
1736 flex_grow,
1737 ..
1738 } => {
1739 let gap = gap.unwrap_or(0.0);
1740 let local = constraints.tighten(node.width, node.height);
1741 let inner = local.deflate(*padding);
1742 let is_row = matches!(direction, IrFlexDirection::Row);
1743
1744 let max_main = if is_row { inner.max_w } else { inner.max_h };
1745 let max_cross = if is_row { inner.max_h } else { inner.max_w };
1746 let min_main = if is_row { inner.min_w } else { inner.min_h };
1747 let min_cross = if is_row { inner.min_h } else { inner.min_w };
1748 let main_bounded = if is_row {
1749 inner.is_width_bounded()
1750 } else {
1751 inner.is_height_bounded()
1752 };
1753 let cross_bounded = if is_row {
1754 inner.is_height_bounded()
1755 } else {
1756 inner.is_width_bounded()
1757 };
1758
1759 if matches!(wrap, IrFlexWrap::Wrap | IrFlexWrap::WrapReverse) {
1760 let mut lines: Vec<(Vec<(NodeId, LayoutSize, BoxConstraints)>, f32, f32)> =
1761 Vec::new();
1762 let mut line_children: Vec<(NodeId, LayoutSize, BoxConstraints)> = Vec::new();
1763 let mut line_main = 0.0f32;
1764 let mut line_cross = 0.0f32;
1765 let mut max_line_main = 0.0f32;
1766
1767 for child_id in &flow_children {
1768 let child_constraints = if is_row {
1769 BoxConstraints {
1770 min_w: 0.0,
1771 max_w: max_main,
1772 min_h: 0.0,
1773 max_h: max_cross,
1774 }
1775 } else {
1776 BoxConstraints {
1777 min_w: 0.0,
1778 max_w: max_cross,
1779 min_h: 0.0,
1780 max_h: max_main,
1781 }
1782 };
1783 let child_size = self.layout_node_constraints(
1784 *child_id,
1785 child_constraints,
1786 LayoutPoint::ZERO,
1787 out,
1788 constraints_out,
1789 measure_cache,
1790 scroll_source,
1791 false,
1792 depth + 1,
1793 )?;
1794 let child_main = if is_row {
1795 child_size.width
1796 } else {
1797 child_size.height
1798 };
1799 let child_cross = if is_row {
1800 child_size.height
1801 } else {
1802 child_size.width
1803 };
1804 let next_main = if line_children.is_empty() {
1805 child_main
1806 } else {
1807 line_main + gap + child_main
1808 };
1809
1810 if main_bounded && !line_children.is_empty() && next_main > max_main {
1811 max_line_main = max_line_main.max(line_main);
1812 lines.push((line_children, line_main, line_cross));
1813 line_children = Vec::new();
1814 line_main = 0.0;
1815 line_cross = 0.0;
1816 }
1817
1818 if !line_children.is_empty() {
1819 line_main += gap;
1820 }
1821 line_main += child_main;
1822 line_cross = line_cross.max(child_cross);
1823 line_children.push((*child_id, child_size, child_constraints));
1824 }
1825
1826 if !line_children.is_empty() {
1827 max_line_main = max_line_main.max(line_main);
1828 lines.push((line_children, line_main, line_cross));
1829 }
1830
1831 let mut container_main = if main_bounded && *flex_grow > 0.0 {
1832 max_main
1833 } else {
1834 max_line_main
1835 };
1836 container_main = container_main.max(min_main);
1837 let total_lines_cross: f32 =
1838 lines.iter().map(|(_, _, cross)| *cross).sum::<f32>()
1839 + gap * lines.len().saturating_sub(1) as f32;
1840 let container_cross = total_lines_cross.max(min_cross);
1841 let size = if is_row {
1842 local.constrain(LayoutSize::new(
1843 container_main + padding[0] + padding[1],
1844 container_cross + padding[2] + padding[3],
1845 ))
1846 } else {
1847 local.constrain(LayoutSize::new(
1848 container_cross + padding[0] + padding[1],
1849 container_main + padding[2] + padding[3],
1850 ))
1851 };
1852
1853 let inner_main = if is_row {
1854 size.width - padding[0] - padding[1]
1855 } else {
1856 size.height - padding[2] - padding[3]
1857 };
1858 let inner_cross = if is_row {
1859 size.height - padding[2] - padding[3]
1860 } else {
1861 size.width - padding[0] - padding[1]
1862 };
1863
1864 let mut ordered_lines = lines;
1865 if matches!(wrap, IrFlexWrap::WrapReverse) {
1866 ordered_lines.reverse();
1867 }
1868
1869 let mut line_cursor = if matches!(wrap, IrFlexWrap::WrapReverse) {
1870 (inner_cross - total_lines_cross).max(0.0)
1871 } else {
1872 0.0
1873 };
1874
1875 for (line_children, line_main, line_cross) in ordered_lines {
1876 let remaining_space = (inner_main - line_main).max(0.0);
1877 let mut extra_gap = 0.0;
1878 let mut offset_main = 0.0;
1879 match justify_content {
1880 fission_ir::op::JustifyContent::Start => {}
1881 fission_ir::op::JustifyContent::End => offset_main = remaining_space,
1882 fission_ir::op::JustifyContent::Center => {
1883 offset_main = remaining_space / 2.0
1884 }
1885 fission_ir::op::JustifyContent::SpaceBetween => {
1886 if line_children.len() > 1 {
1887 extra_gap =
1888 remaining_space / (line_children.len() as f32 - 1.0);
1889 }
1890 }
1891 fission_ir::op::JustifyContent::SpaceAround => {
1892 if !line_children.is_empty() {
1893 extra_gap = remaining_space / line_children.len() as f32;
1894 offset_main = extra_gap / 2.0;
1895 }
1896 }
1897 fission_ir::op::JustifyContent::SpaceEvenly => {
1898 if !line_children.is_empty() {
1899 extra_gap =
1900 remaining_space / (line_children.len() as f32 + 1.0);
1901 offset_main = extra_gap;
1902 }
1903 }
1904 }
1905
1906 let mut cursor = offset_main;
1907 for (child_id, child_size, mut child_constraints) in line_children {
1908 let child_main = if is_row {
1909 child_size.width
1910 } else {
1911 child_size.height
1912 };
1913 let child_cross = if is_row {
1914 child_size.height
1915 } else {
1916 child_size.width
1917 };
1918 if matches!(align_items, fission_ir::op::AlignItems::Stretch) {
1919 if is_row {
1920 child_constraints.min_h = line_cross;
1921 child_constraints.max_h = line_cross;
1922 } else {
1923 child_constraints.min_w = line_cross;
1924 child_constraints.max_w = line_cross;
1925 }
1926 }
1927 let cross_offset = match align_items {
1928 fission_ir::op::AlignItems::Start
1929 | fission_ir::op::AlignItems::Stretch => 0.0,
1930 fission_ir::op::AlignItems::End => {
1931 (line_cross - child_cross).max(0.0)
1932 }
1933 fission_ir::op::AlignItems::Center => {
1934 ((line_cross - child_cross) / 2.0).max(0.0)
1935 }
1936 fission_ir::op::AlignItems::Baseline => 0.0,
1937 };
1938 let child_origin = if is_row {
1939 LayoutPoint::new(
1940 origin.x + padding[0] + cursor,
1941 origin.y + padding[2] + line_cursor + cross_offset,
1942 )
1943 } else {
1944 LayoutPoint::new(
1945 origin.x + padding[0] + line_cursor + cross_offset,
1946 origin.y + padding[2] + cursor,
1947 )
1948 };
1949 self.layout_node_constraints(
1950 child_id,
1951 child_constraints,
1952 child_origin,
1953 out,
1954 constraints_out,
1955 measure_cache,
1956 scroll_source,
1957 record,
1958 depth + 1,
1959 )?;
1960 cursor += child_main + gap + extra_gap;
1961 }
1962
1963 line_cursor += line_cross + gap;
1964 }
1965
1966 if record && !abs_children.is_empty() {
1967 let abs_constraints = BoxConstraints::loose(size.width, size.height);
1968 for child_id in abs_children {
1969 self.layout_node_constraints(
1970 child_id,
1971 abs_constraints,
1972 origin,
1973 out,
1974 constraints_out,
1975 measure_cache,
1976 scroll_source,
1977 record,
1978 depth + 1,
1979 )?;
1980 }
1981 }
1982 content_size = size;
1983 size
1984 } else {
1985 struct FlexChildEntry {
1986 id: NodeId,
1987 flex: f32,
1988 size: LayoutSize,
1989 constraints: BoxConstraints,
1990 is_flex: bool,
1991 }
1992 let mut measured: Vec<FlexChildEntry> = Vec::new();
1993 let mut total_flex = 0.0f32;
1994 let mut nonflex_main = 0.0f32;
1995 let mut max_child_cross = 0.0f32;
1996 let treat_flex_as_nonflex = !main_bounded;
1997
1998 for child_id in &flow_children {
1999 let child = match self.graph_state.node(*child_id) {
2000 Some(child) => child,
2001 None => continue,
2002 };
2003 let flex = child.flex_grow;
2004 if flex > 0.0 && !treat_flex_as_nonflex {
2005 total_flex += flex;
2006 measured.push(FlexChildEntry {
2007 id: *child_id,
2008 flex,
2009 size: LayoutSize::ZERO,
2010 constraints: BoxConstraints::loose(0.0, 0.0),
2011 is_flex: true,
2012 });
2013 continue;
2014 }
2015 let child_constraints = if is_row {
2016 let cross =
2017 if matches!(align_items, fission_ir::op::AlignItems::Stretch)
2018 && cross_bounded
2019 {
2020 BoxConstraints {
2021 min_w: 0.0,
2022 max_w: f32::INFINITY,
2023 min_h: max_cross,
2024 max_h: max_cross,
2025 }
2026 } else {
2027 BoxConstraints {
2028 min_w: 0.0,
2029 max_w: f32::INFINITY,
2030 min_h: 0.0,
2031 max_h: max_cross,
2032 }
2033 };
2034 cross
2035 } else {
2036 let cross =
2037 if matches!(align_items, fission_ir::op::AlignItems::Stretch)
2038 && cross_bounded
2039 {
2040 BoxConstraints {
2041 min_w: max_cross,
2042 max_w: max_cross,
2043 min_h: 0.0,
2044 max_h: f32::INFINITY,
2045 }
2046 } else {
2047 BoxConstraints {
2048 min_w: 0.0,
2049 max_w: max_cross,
2050 min_h: 0.0,
2051 max_h: f32::INFINITY,
2052 }
2053 };
2054 cross
2055 };
2056 let child_size = self.layout_node_constraints(
2057 *child_id,
2058 child_constraints,
2059 LayoutPoint::ZERO,
2060 out,
2061 constraints_out,
2062 measure_cache,
2063 scroll_source,
2064 false,
2065 depth + 1,
2066 )?;
2067 let child_main = if is_row {
2068 child_size.width
2069 } else {
2070 child_size.height
2071 };
2072 let child_cross = if is_row {
2073 child_size.height
2074 } else {
2075 child_size.width
2076 };
2077 nonflex_main += child_main;
2078 max_child_cross = max_child_cross.max(child_cross);
2079 measured.push(FlexChildEntry {
2080 id: *child_id,
2081 flex,
2082 size: child_size,
2083 constraints: child_constraints,
2084 is_flex: false,
2085 });
2086 }
2087
2088 let gap_total = gap * flow_children.len().saturating_sub(1) as f32;
2089 let remaining = if main_bounded {
2090 (max_main - nonflex_main - gap_total).max(0.0)
2091 } else {
2092 0.0
2093 };
2094
2095 for entry in measured.iter_mut().filter(|e| e.is_flex) {
2096 let flex = entry.flex;
2097 let allocated = if main_bounded && total_flex > 0.0 {
2098 remaining * (flex / total_flex)
2099 } else {
2100 0.0
2101 };
2102 let child_constraints = if is_row {
2103 let cross =
2104 if matches!(align_items, fission_ir::op::AlignItems::Stretch)
2105 && cross_bounded
2106 {
2107 BoxConstraints {
2108 min_w: allocated,
2109 max_w: allocated,
2110 min_h: max_cross,
2111 max_h: max_cross,
2112 }
2113 } else {
2114 BoxConstraints {
2115 min_w: allocated,
2116 max_w: allocated,
2117 min_h: 0.0,
2118 max_h: max_cross,
2119 }
2120 };
2121 cross
2122 } else {
2123 let cross =
2124 if matches!(align_items, fission_ir::op::AlignItems::Stretch)
2125 && cross_bounded
2126 {
2127 BoxConstraints {
2128 min_w: max_cross,
2129 max_w: max_cross,
2130 min_h: allocated,
2131 max_h: allocated,
2132 }
2133 } else {
2134 BoxConstraints {
2135 min_w: 0.0,
2136 max_w: max_cross,
2137 min_h: allocated,
2138 max_h: allocated,
2139 }
2140 };
2141 cross
2142 };
2143 let child_size = self.layout_node_constraints(
2144 entry.id,
2145 child_constraints,
2146 LayoutPoint::ZERO,
2147 out,
2148 constraints_out,
2149 measure_cache,
2150 scroll_source,
2151 false,
2152 depth + 1,
2153 )?;
2154 let child_cross = if is_row {
2155 child_size.height
2156 } else {
2157 child_size.width
2158 };
2159 max_child_cross = max_child_cross.max(child_cross);
2160 entry.size = child_size;
2161 entry.constraints = child_constraints;
2162 }
2163
2164 let final_children_main: f32 = measured
2165 .iter()
2166 .map(|entry| {
2167 if is_row {
2168 entry.size.width
2169 } else {
2170 entry.size.height
2171 }
2172 })
2173 .sum();
2174
2175 let mut container_main = if main_bounded && *flex_grow > 0.0 {
2176 max_main
2177 } else {
2178 final_children_main + gap_total
2179 };
2180 container_main = container_main.max(min_main);
2181
2182 if main_bounded && final_children_main + gap_total > max_main {
2183 let mut total_shrink_scaled = 0.0f32;
2185 for entry in &measured {
2186 let child = self.graph_state.node(entry.id).unwrap();
2187 let main_size = if is_row {
2188 entry.size.width
2189 } else {
2190 entry.size.height
2191 };
2192 total_shrink_scaled += main_size * child.flex_shrink;
2193 }
2194
2195 if total_shrink_scaled > 0.0 {
2196 let overflow = (final_children_main + gap_total) - max_main;
2197 for entry in &mut measured {
2198 let child = self.graph_state.node(entry.id).unwrap();
2199 let main_size = if is_row {
2200 entry.size.width
2201 } else {
2202 entry.size.height
2203 };
2204 let shrink_amount = (main_size * child.flex_shrink
2205 / total_shrink_scaled)
2206 * overflow;
2207 let floor = if child.flex_shrink > 0.0 {
2211 let explicit_min = match &child.op {
2213 LayoutOp::Box {
2214 min_width,
2215 min_height,
2216 height,
2217 width,
2218 ..
2219 } => {
2220 if is_row {
2221 min_width.or(*width).unwrap_or(0.0)
2222 } else {
2223 min_height.or(*height).unwrap_or(0.0)
2224 }
2225 }
2226 _ => 0.0,
2227 };
2228 explicit_min
2229 } else {
2230 main_size };
2232 let new_main = (main_size - shrink_amount).max(floor);
2233
2234 let mut child_constraints = entry.constraints;
2235 if is_row {
2236 child_constraints.min_w = new_main;
2237 child_constraints.max_w = new_main;
2238 } else {
2239 child_constraints.min_h = new_main;
2240 child_constraints.max_h = new_main;
2241 }
2242 let new_size = self.layout_node_constraints(
2243 entry.id,
2244 child_constraints,
2245 LayoutPoint::ZERO,
2246 out,
2247 constraints_out,
2248 measure_cache,
2249 scroll_source,
2250 false,
2251 depth + 1,
2252 )?;
2253 entry.size = new_size;
2254 entry.constraints = child_constraints;
2255 }
2256 }
2257 }
2258
2259 let container_cross = max_child_cross.max(min_cross);
2260 let size = if is_row {
2261 local.constrain(LayoutSize::new(
2262 container_main + padding[0] + padding[1],
2263 container_cross + padding[2] + padding[3],
2264 ))
2265 } else {
2266 local.constrain(LayoutSize::new(
2267 container_cross + padding[0] + padding[1],
2268 container_main + padding[2] + padding[3],
2269 ))
2270 };
2271
2272 let inner_main = if is_row {
2273 size.width - padding[0] - padding[1]
2274 } else {
2275 size.height - padding[2] - padding[3]
2276 };
2277 let inner_cross = if is_row {
2278 size.height - padding[2] - padding[3]
2279 } else {
2280 size.width - padding[0] - padding[1]
2281 };
2282
2283 let final_children_main: f32 = measured
2284 .iter()
2285 .map(|entry| {
2286 if is_row {
2287 entry.size.width
2288 } else {
2289 entry.size.height
2290 }
2291 })
2292 .sum();
2293
2294 let remaining_space = (inner_main - final_children_main - gap_total).max(0.0);
2295 let mut extra_gap = 0.0;
2296 let mut offset_main = 0.0;
2297 match justify_content {
2298 fission_ir::op::JustifyContent::Start => {}
2299 fission_ir::op::JustifyContent::End => offset_main = remaining_space,
2300 fission_ir::op::JustifyContent::Center => {
2301 offset_main = remaining_space / 2.0
2302 }
2303 fission_ir::op::JustifyContent::SpaceBetween => {
2304 if measured.len() > 1 {
2305 extra_gap = remaining_space / (measured.len() as f32 - 1.0);
2306 }
2307 }
2308 fission_ir::op::JustifyContent::SpaceAround => {
2309 if !measured.is_empty() {
2310 extra_gap = remaining_space / measured.len() as f32;
2311 offset_main = extra_gap / 2.0;
2312 }
2313 }
2314 fission_ir::op::JustifyContent::SpaceEvenly => {
2315 if !measured.is_empty() {
2316 extra_gap = remaining_space / (measured.len() as f32 + 1.0);
2317 offset_main = extra_gap;
2318 }
2319 }
2320 }
2321
2322 let mut cursor = offset_main;
2323 for entry in measured {
2324 let child_main = if is_row {
2325 entry.size.width
2326 } else {
2327 entry.size.height
2328 };
2329 let child_cross = if is_row {
2330 entry.size.height
2331 } else {
2332 entry.size.width
2333 };
2334 let cross_offset = match align_items {
2335 fission_ir::op::AlignItems::Start
2336 | fission_ir::op::AlignItems::Stretch => 0.0,
2337 fission_ir::op::AlignItems::End => (inner_cross - child_cross).max(0.0),
2338 fission_ir::op::AlignItems::Center => {
2339 ((inner_cross - child_cross) / 2.0).max(0.0)
2340 }
2341 fission_ir::op::AlignItems::Baseline => 0.0,
2342 };
2343 let child_origin = if is_row {
2344 LayoutPoint::new(
2345 origin.x + padding[0] + cursor,
2346 origin.y + padding[2] + cross_offset,
2347 )
2348 } else {
2349 LayoutPoint::new(
2350 origin.x + padding[0] + cross_offset,
2351 origin.y + padding[2] + cursor,
2352 )
2353 };
2354
2355 let mut child_constraints = entry.constraints;
2356 if matches!(align_items, fission_ir::op::AlignItems::Stretch) {
2357 let child_node = self.graph_state.node(entry.id);
2359 let has_explicit_cross = child_node
2360 .map(|n| match &n.op {
2361 LayoutOp::Box { width, height, .. } => {
2362 if is_row {
2363 height.is_some()
2364 } else {
2365 width.is_some()
2366 }
2367 }
2368 _ => false,
2369 })
2370 .unwrap_or(false);
2371 if !has_explicit_cross {
2372 if is_row {
2373 child_constraints.min_h = inner_cross;
2374 child_constraints.max_h = inner_cross;
2375 } else {
2376 child_constraints.min_w = inner_cross;
2377 child_constraints.max_w = inner_cross;
2378 }
2379 }
2380 }
2381
2382 self.layout_node_constraints(
2383 entry.id,
2384 child_constraints,
2385 child_origin,
2386 out,
2387 constraints_out,
2388 measure_cache,
2389 scroll_source,
2390 record,
2391 depth + 1,
2392 )?;
2393 cursor += child_main + gap + extra_gap;
2394 }
2395
2396 if record && !abs_children.is_empty() {
2397 let abs_constraints = BoxConstraints::loose(size.width, size.height);
2398 for child_id in abs_children {
2399 self.layout_node_constraints(
2400 child_id,
2401 abs_constraints,
2402 origin,
2403 out,
2404 constraints_out,
2405 measure_cache,
2406 scroll_source,
2407 record,
2408 depth + 1,
2409 )?;
2410 }
2411 }
2412 content_size = size;
2413 size
2414 }
2415 }
2416 LayoutOp::Grid {
2417 columns,
2418 rows,
2419 column_gap,
2420 row_gap,
2421 padding,
2422 } => {
2423 let gap_x = column_gap.unwrap_or(0.0);
2424 let gap_y = row_gap.unwrap_or(0.0);
2425 let inner = constraints.deflate(*padding);
2426 let bounded_w = inner.is_width_bounded();
2427 let bounded_h = inner.is_height_bounded();
2428 let available_w = if bounded_w { inner.max_w } else { 0.0 };
2429 let available_h = if bounded_h { inner.max_h } else { 0.0 };
2430
2431 let col_count = columns.len().max(1);
2432 let mut col_widths = vec![0.0f32; col_count];
2433 let mut fr_total = 0.0f32;
2434 let mut fixed_total = 0.0f32;
2435 for (i, track) in columns.iter().enumerate() {
2436 match track {
2437 GridTrack::Points(p) => {
2438 col_widths[i] = *p;
2439 fixed_total += *p;
2440 }
2441 GridTrack::Percent(p) => {
2442 let w = if bounded_w {
2443 available_w * (*p / 100.0)
2444 } else {
2445 0.0
2446 };
2447 col_widths[i] = w;
2448 fixed_total += w;
2449 }
2450 GridTrack::Fr(f) => fr_total += *f,
2451 _ => {}
2452 }
2453 }
2454 if fr_total > 0.0 && bounded_w {
2455 let remaining =
2456 (available_w - fixed_total - gap_x * (col_count.saturating_sub(1) as f32))
2457 .max(0.0);
2458 for (i, track) in columns.iter().enumerate() {
2459 if let GridTrack::Fr(f) = track {
2460 col_widths[i] = remaining * (*f / fr_total);
2461 }
2462 }
2463 }
2464
2465 let child_count = flow_children.len();
2466 let row_count = if rows.is_empty() {
2467 (child_count + col_count - 1) / col_count
2468 } else {
2469 rows.len()
2470 };
2471 let mut row_heights = vec![0.0f32; row_count.max(1)];
2472
2473 if !rows.is_empty() {
2474 let mut row_fr_total = 0.0f32;
2475 let mut row_fixed_total = 0.0f32;
2476 for (i, track) in rows.iter().enumerate() {
2477 if i >= row_heights.len() {
2478 break;
2479 }
2480 match track {
2481 GridTrack::Points(p) => {
2482 row_heights[i] = *p;
2483 row_fixed_total += *p;
2484 }
2485 GridTrack::Percent(p) => {
2486 let h = if bounded_h {
2487 available_h * (*p / 100.0)
2488 } else {
2489 0.0
2490 };
2491 row_heights[i] = h;
2492 row_fixed_total += h;
2493 }
2494 GridTrack::Fr(f) => row_fr_total += *f,
2495 _ => {}
2496 }
2497 }
2498 if row_fr_total > 0.0 && bounded_h {
2499 let remaining = (available_h
2500 - row_fixed_total
2501 - gap_y * (row_heights.len().saturating_sub(1) as f32))
2502 .max(0.0);
2503 for (i, track) in rows.iter().enumerate() {
2504 if let GridTrack::Fr(f) = track {
2505 row_heights[i] = remaining * (*f / row_fr_total);
2506 }
2507 }
2508 }
2509 }
2510
2511 let mut cell_assignments = Vec::new();
2512 let mut auto_row = 0;
2513 let mut auto_col = 0;
2514
2515 for child_id in &flow_children {
2516 let child = self.graph_state.node(*child_id).unwrap();
2517 let (row, col) = if let LayoutOp::GridItem {
2518 row_start,
2519 col_start,
2520 ..
2521 } = &child.op
2522 {
2523 let r = match row_start {
2524 fission_ir::op::GridPlacement::Line(l) => {
2525 (*l as usize).saturating_sub(1)
2526 }
2527 _ => auto_row,
2528 };
2529 let c = match col_start {
2530 fission_ir::op::GridPlacement::Line(l) => {
2531 (*l as usize).saturating_sub(1)
2532 }
2533 _ => auto_col,
2534 };
2535 (r, c)
2536 } else {
2537 let res = (auto_row, auto_col);
2538 auto_col += 1;
2539 if auto_col >= col_count {
2540 auto_col = 0;
2541 auto_row += 1;
2542 }
2543 res
2544 };
2545 cell_assignments.push((*child_id, row, col));
2546 }
2547
2548 for (child_id, row, col) in &cell_assignments {
2549 if *row >= row_heights.len() || *col >= col_widths.len() {
2550 continue;
2551 }
2552 let cell_w = col_widths[*col];
2553 let cell_constraints = BoxConstraints {
2554 min_w: cell_w,
2555 max_w: cell_w,
2556 min_h: 0.0,
2557 max_h: if row_heights[*row] > 0.0 {
2558 row_heights[*row]
2559 } else {
2560 f32::INFINITY
2561 },
2562 };
2563 let child_size = self.layout_node_constraints(
2564 *child_id,
2565 cell_constraints,
2566 LayoutPoint::ZERO,
2567 out,
2568 constraints_out,
2569 measure_cache,
2570 scroll_source,
2571 false,
2572 depth + 1,
2573 )?;
2574 if row_heights[*row] == 0.0 {
2575 row_heights[*row] = child_size.height;
2576 } else {
2577 row_heights[*row] = row_heights[*row].max(child_size.height);
2578 }
2579 }
2580
2581 let grid_w: f32 =
2582 col_widths.iter().sum::<f32>() + gap_x * (col_count.saturating_sub(1) as f32);
2583 let grid_h: f32 = row_heights.iter().sum::<f32>()
2584 + gap_y * (row_heights.len().saturating_sub(1) as f32);
2585 let size = constraints.constrain(LayoutSize::new(
2586 grid_w + padding[0] + padding[1],
2587 grid_h + padding[2] + padding[3],
2588 ));
2589
2590 if record {
2591 let padding_origin_x = origin.x + padding[0];
2592 let padding_origin_y = origin.y + padding[2];
2593 for (child_id, row, col) in &cell_assignments {
2594 if *row >= row_heights.len() || *col >= col_widths.len() {
2595 continue;
2596 }
2597 let mut cell_x = padding_origin_x;
2598 for i in 0..*col {
2599 cell_x += col_widths[i] + gap_x;
2600 }
2601 let mut cell_y = padding_origin_y;
2602 for i in 0..*row {
2603 cell_y += row_heights[i] + gap_y;
2604 }
2605 let cell_w = col_widths[*col];
2606 let cell_h = row_heights[*row];
2607 let child_constraints = BoxConstraints {
2608 min_w: cell_w,
2609 max_w: cell_w,
2610 min_h: cell_h,
2611 max_h: cell_h,
2612 };
2613 self.layout_node_constraints(
2614 *child_id,
2615 child_constraints,
2616 LayoutPoint::new(cell_x, cell_y),
2617 out,
2618 constraints_out,
2619 measure_cache,
2620 scroll_source,
2621 record,
2622 depth + 1,
2623 )?;
2624 }
2625 }
2626
2627 if record && !abs_children.is_empty() {
2628 let abs_constraints = BoxConstraints::loose(size.width, size.height);
2629 for child_id in abs_children {
2630 self.layout_node_constraints(
2631 child_id,
2632 abs_constraints,
2633 origin,
2634 out,
2635 constraints_out,
2636 measure_cache,
2637 scroll_source,
2638 record,
2639 depth + 1,
2640 )?;
2641 }
2642 }
2643 content_size = size;
2644 size
2645 }
2646 LayoutOp::GridItem { .. } => {
2647 let mut child_size = LayoutSize::ZERO;
2648 if let Some(child_id) = node.children_ids.first() {
2649 child_size = self.layout_node_constraints(
2650 *child_id,
2651 constraints,
2652 origin,
2653 out,
2654 constraints_out,
2655 measure_cache,
2656 scroll_source,
2657 record,
2658 depth + 1,
2659 )?;
2660 }
2661 content_size = child_size;
2662 constraints.constrain(child_size)
2663 }
2664 LayoutOp::Scroll {
2665 direction,
2666 width,
2667 height,
2668 min_width,
2669 max_width,
2670 min_height,
2671 max_height,
2672 padding,
2673 ..
2674 } => {
2675 let mut local =
2676 constraints.apply_min_max(*min_width, *max_width, *min_height, *max_height);
2677 local = local.tighten(*width, *height);
2678 let is_horizontal = matches!(direction, FlexDirection::Row);
2679 let mut child_constraints = local.deflate(*padding);
2680 if is_horizontal {
2681 child_constraints.min_w = 0.0;
2682 child_constraints.max_w = f32::INFINITY;
2683 } else {
2684 child_constraints.min_h = 0.0;
2685 child_constraints.max_h = f32::INFINITY;
2686 }
2687 let mut child_size = LayoutSize::ZERO;
2688 if let Some(child_id) = flow_children.first() {
2689 child_size = self.layout_node_constraints(
2690 *child_id,
2691 child_constraints,
2692 LayoutPoint::ZERO,
2693 out,
2694 constraints_out,
2695 measure_cache,
2696 scroll_source,
2697 false,
2698 depth + 1,
2699 )?;
2700 }
2701 let size = local.constrain(LayoutSize::new(
2702 child_size.width + padding[0] + padding[1],
2703 child_size.height + padding[2] + padding[3],
2704 ));
2705 if record {
2706 if let Some(child_id) = flow_children.first() {
2707 self.layout_node_constraints(
2708 *child_id,
2709 child_constraints,
2710 LayoutPoint::new(origin.x + padding[0], origin.y + padding[2]),
2711 out,
2712 constraints_out,
2713 measure_cache,
2714 scroll_source,
2715 record,
2716 depth + 1,
2717 )?;
2718 }
2719 if !abs_children.is_empty() {
2720 let abs_constraints = BoxConstraints::loose(size.width, size.height);
2721 for child_id in abs_children {
2722 self.layout_node_constraints(
2723 child_id,
2724 abs_constraints,
2725 origin,
2726 out,
2727 constraints_out,
2728 measure_cache,
2729 scroll_source,
2730 record,
2731 depth + 1,
2732 )?;
2733 }
2734 }
2735 }
2736 content_size = child_size;
2737 size
2738 }
2739 LayoutOp::Align => {
2740 let child_constraints = BoxConstraints::loose(constraints.max_w, constraints.max_h);
2741 let mut child_size = LayoutSize::ZERO;
2742 if let Some(child_id) = flow_children.first() {
2743 child_size = self.layout_node_constraints(
2744 *child_id,
2745 child_constraints,
2746 LayoutPoint::ZERO,
2747 out,
2748 constraints_out,
2749 measure_cache,
2750 scroll_source,
2751 false,
2752 depth + 1,
2753 )?;
2754 }
2755 let size = if constraints.is_width_bounded() || constraints.is_height_bounded() {
2756 constraints.constrain(LayoutSize::new(
2757 if constraints.is_width_bounded() {
2758 constraints.max_w
2759 } else {
2760 child_size.width
2761 },
2762 if constraints.is_height_bounded() {
2763 constraints.max_h
2764 } else {
2765 child_size.height
2766 },
2767 ))
2768 } else {
2769 child_size
2770 };
2771 if let Some(child_id) = flow_children.first() {
2772 let dx = ((size.width - child_size.width) / 2.0).max(0.0);
2773 let dy = ((size.height - child_size.height) / 2.0).max(0.0);
2774 self.layout_node_constraints(
2775 *child_id,
2776 child_constraints,
2777 LayoutPoint::new(origin.x + dx, origin.y + dy),
2778 out,
2779 constraints_out,
2780 measure_cache,
2781 scroll_source,
2782 record,
2783 depth + 1,
2784 )?;
2785 }
2786 if record && !abs_children.is_empty() {
2787 let abs_constraints = BoxConstraints::loose(size.width, size.height);
2788 for child_id in abs_children {
2789 self.layout_node_constraints(
2790 child_id,
2791 abs_constraints,
2792 origin,
2793 out,
2794 constraints_out,
2795 measure_cache,
2796 scroll_source,
2797 record,
2798 depth + 1,
2799 )?;
2800 }
2801 }
2802 content_size = child_size;
2803 size
2804 }
2805 LayoutOp::ZStack => {
2806 let mut max_child = LayoutSize::ZERO;
2807 for child_id in &flow_children {
2808 let child_size = self.layout_node_constraints(
2809 *child_id,
2810 BoxConstraints::loose(constraints.max_w, constraints.max_h),
2811 LayoutPoint::ZERO,
2812 out,
2813 constraints_out,
2814 measure_cache,
2815 scroll_source,
2816 false,
2817 depth + 1,
2818 )?;
2819 max_child.width = max_child.width.max(child_size.width);
2820 max_child.height = max_child.height.max(child_size.height);
2821 }
2822 let size = if constraints.is_width_bounded() || constraints.is_height_bounded() {
2823 constraints.constrain(LayoutSize::new(
2824 if constraints.is_width_bounded() {
2825 constraints.max_w
2826 } else {
2827 max_child.width
2828 },
2829 if constraints.is_height_bounded() {
2830 constraints.max_h
2831 } else {
2832 max_child.height
2833 },
2834 ))
2835 } else {
2836 max_child
2837 };
2838 for child_id in &flow_children {
2839 let child_constraints = BoxConstraints::loose(size.width, size.height);
2840 let child_origin = LayoutPoint::new(origin.x, origin.y);
2841 self.layout_node_constraints(
2842 *child_id,
2843 child_constraints,
2844 child_origin,
2845 out,
2846 constraints_out,
2847 measure_cache,
2848 scroll_source,
2849 record,
2850 depth + 1,
2851 )?;
2852 }
2853 if record && !abs_children.is_empty() {
2854 let abs_constraints = BoxConstraints::loose(size.width, size.height);
2855 for child_id in abs_children {
2856 self.layout_node_constraints(
2857 child_id,
2858 abs_constraints,
2859 origin,
2860 out,
2861 constraints_out,
2862 measure_cache,
2863 scroll_source,
2864 record,
2865 depth + 1,
2866 )?;
2867 }
2868 }
2869 content_size = size;
2870 size
2871 }
2872 LayoutOp::Positioned {
2873 top,
2874 left,
2875 bottom,
2876 right,
2877 width,
2878 height,
2879 } => {
2880 let target_w = finite_or(constraints.max_w, finite_or(constraints.min_w, 0.0));
2881 let target_h = finite_or(constraints.max_h, finite_or(constraints.min_h, 0.0));
2882 let size = constraints.constrain(LayoutSize::new(target_w, target_h));
2883 let mut child_constraints = BoxConstraints::loose(size.width, size.height);
2884 if let (Some(l), Some(r)) = (left, right) {
2885 let w = (size.width - l - r).max(0.0);
2886 child_constraints = child_constraints.tighten(Some(w), None);
2887 }
2888 if let (Some(t), Some(b)) = (top, bottom) {
2889 let h = (size.height - t - b).max(0.0);
2890 child_constraints = child_constraints.tighten(None, Some(h));
2891 }
2892 child_constraints = child_constraints.tighten(*width, *height);
2893 if let Some(child_id) = node.children_ids.first() {
2894 let child_size = self.layout_node_constraints(
2895 *child_id,
2896 child_constraints,
2897 LayoutPoint::ZERO,
2898 out,
2899 constraints_out,
2900 measure_cache,
2901 scroll_source,
2902 false,
2903 depth + 1,
2904 )?;
2905 let x = left.unwrap_or_else(|| {
2906 right
2907 .map(|r| (size.width - r - child_size.width).max(0.0))
2908 .unwrap_or(0.0)
2909 });
2910 let y = top.unwrap_or_else(|| {
2911 bottom
2912 .map(|b| (size.height - b - child_size.height).max(0.0))
2913 .unwrap_or(0.0)
2914 });
2915 self.layout_node_constraints(
2916 *child_id,
2917 child_constraints,
2918 LayoutPoint::new(origin.x + x, origin.y + y),
2919 out,
2920 constraints_out,
2921 measure_cache,
2922 scroll_source,
2923 record,
2924 depth + 1,
2925 )?;
2926 }
2927 content_size = size;
2928 size
2929 }
2930 LayoutOp::Embed { width, height, .. } => {
2931 let local = constraints.tighten(*width, *height);
2932 let w = if local.is_width_bounded() {
2933 local.max_w
2934 } else {
2935 local.min_w
2936 };
2937 let h = if local.is_height_bounded() {
2938 local.max_h
2939 } else {
2940 local.min_h
2941 };
2942 let size = local.constrain(LayoutSize::new(w, h));
2943 content_size = size;
2944 size
2945 }
2946 LayoutOp::AbsoluteFill => {
2947 let target_w = finite_or(constraints.max_w, finite_or(constraints.min_w, 0.0));
2948 let target_h = finite_or(constraints.max_h, finite_or(constraints.min_h, 0.0));
2949 let size = constraints.constrain(LayoutSize::new(target_w, target_h));
2950 for child_id in self.graph_state.children_of(node_id) {
2951 self.layout_node_constraints(
2952 *child_id,
2953 BoxConstraints::tight(size),
2954 origin,
2955 out,
2956 constraints_out,
2957 measure_cache,
2958 scroll_source,
2959 record,
2960 depth + 1,
2961 )?;
2962 }
2963 content_size = size;
2964 size
2965 }
2966 LayoutOp::Transform { .. } | LayoutOp::Clip { .. } => {
2967 let mut child_size = LayoutSize::ZERO;
2968 if let Some(child_id) = node.children_ids.first() {
2969 child_size = self.layout_node_constraints(
2970 *child_id,
2971 constraints,
2972 origin,
2973 out,
2974 constraints_out,
2975 measure_cache,
2976 scroll_source,
2977 record,
2978 depth + 1,
2979 )?;
2980 }
2981 content_size = child_size;
2982 constraints.constrain(child_size)
2983 }
2984 LayoutOp::Flyout { anchor, content: _ } => {
2985 let loose = BoxConstraints::loose(
2986 if constraints.is_width_bounded() {
2987 constraints.max_w
2988 } else {
2989 f32::INFINITY
2990 },
2991 if constraints.is_height_bounded() {
2992 constraints.max_h
2993 } else {
2994 f32::INFINITY
2995 },
2996 );
2997 let mut child_size = LayoutSize::ZERO;
2998 for child_id in self.graph_state.children_of(node_id) {
2999 child_size = self.layout_node_constraints(
3000 *child_id,
3001 loose,
3002 origin,
3003 out,
3004 constraints_out,
3005 measure_cache,
3006 scroll_source,
3007 false,
3008 depth + 1,
3009 )?;
3010 }
3011 if record {
3012 let anchor_rect = out.get(anchor).map(|g| g.rect);
3013 let place_x = anchor_rect.map(|r| r.x()).unwrap_or(origin.x);
3014 let place_y = anchor_rect.map(|r| r.y() + r.height()).unwrap_or(origin.y);
3015 for child_id in self.graph_state.children_of(node_id) {
3016 self.layout_node_constraints(
3017 *child_id,
3018 loose,
3019 LayoutPoint::new(place_x, place_y),
3020 out,
3021 constraints_out,
3022 measure_cache,
3023 scroll_source,
3024 record,
3025 depth + 1,
3026 )?;
3027 }
3028 }
3029 content_size = child_size;
3030 child_size
3031 }
3032 };
3033
3034 if let Some(runs) = &node.rich_text {
3035 if let Some(measurer) = &self.measurer {
3036 let node_max_w = match &node.op {
3037 LayoutOp::Box { max_width, .. } => *max_width,
3038 _ => None,
3039 };
3040 let avail_w = {
3041 let from_constraints = if constraints.is_width_bounded() {
3042 Some(constraints.max_w)
3043 } else {
3044 None
3045 };
3046 match (from_constraints, node_max_w) {
3047 (Some(c), Some(m)) => Some(c.min(m)),
3048 (Some(c), None) => Some(c),
3049 (None, Some(m)) => Some(m),
3050 (None, None) => None,
3051 }
3052 };
3053 let rich_layout = measurer.layout_rich_text(runs, avail_w);
3054 let text_content = LayoutSize::new(rich_layout.width, rich_layout.height);
3055 let measured = constraints.constrain(text_content);
3056 if rich_text_inline_children
3057 && rich_layout.inline_boxes.len() == flow_children.len()
3058 {
3059 let result =
3060 self.record_geometry(node_id, origin, measured, text_content, out, record);
3061 if record {
3062 let mut inline_boxes = rich_layout.inline_boxes;
3063 inline_boxes.sort_by_key(|inline_box| inline_box.id);
3064 for (child_id, inline_box) in flow_children.iter().zip(inline_boxes.iter())
3065 {
3066 self.layout_node_constraints(
3067 *child_id,
3068 BoxConstraints::tight(LayoutSize::new(
3069 inline_box.width,
3070 inline_box.height,
3071 )),
3072 LayoutPoint::new(origin.x + inline_box.x, origin.y + inline_box.y),
3073 out,
3074 constraints_out,
3075 measure_cache,
3076 scroll_source,
3077 record,
3078 depth + 1,
3079 )?;
3080 }
3081 }
3082 if !record {
3083 measure_cache.insert(MeasureCacheKey::new(node_id, constraints), result);
3084 }
3085 return Ok(result);
3086 }
3087 if node.children_ids.is_empty() {
3088 let result =
3089 self.record_geometry(node_id, origin, measured, text_content, out, record);
3090 if !record {
3091 measure_cache.insert(MeasureCacheKey::new(node_id, constraints), result);
3092 }
3093 return Ok(result);
3094 }
3095 content_size.width = content_size.width.max(text_content.width);
3096 content_size.height = content_size.height.max(text_content.height);
3097 }
3098 }
3099
3100 let result = self.record_geometry(node_id, origin, size, content_size, out, record);
3101 if !record {
3102 measure_cache.insert(MeasureCacheKey::new(node_id, constraints), result);
3103 }
3104 Ok(result)
3105 }
3106
3107 fn record_geometry(
3108 &self,
3109 node_id: NodeId,
3110 origin: LayoutPoint,
3111 size: LayoutSize,
3112 content_size: LayoutSize,
3113 out: &mut HashMap<NodeId, LayoutNodeGeometry>,
3114 record: bool,
3115 ) -> LayoutSize {
3116 let mut rect_origin = origin;
3117 let mut rect_size = size;
3118 let mut rect_content = content_size;
3119 let mut had_non_finite = false;
3120
3121 if !rect_origin.x.is_finite() {
3122 rect_origin.x = 0.0;
3123 had_non_finite = true;
3124 }
3125 if !rect_origin.y.is_finite() {
3126 rect_origin.y = 0.0;
3127 had_non_finite = true;
3128 }
3129 if !rect_size.width.is_finite() {
3130 rect_size.width = 0.0;
3131 had_non_finite = true;
3132 }
3133 if !rect_size.height.is_finite() {
3134 rect_size.height = 0.0;
3135 had_non_finite = true;
3136 }
3137 if !rect_content.width.is_finite() {
3138 rect_content.width = 0.0;
3139 had_non_finite = true;
3140 }
3141 if !rect_content.height.is_finite() {
3142 rect_content.height = 0.0;
3143 had_non_finite = true;
3144 }
3145
3146 if had_non_finite {
3147 diag::emit(
3148 diag::DiagCategory::Invariants,
3149 diag::DiagLevel::Error,
3150 diag::DiagEventKind::InvariantViolation {
3151 kind: "non_finite_layout".into(),
3152 node: Some(node_id.as_u128()),
3153 details: format!(
3154 "origin=({:.2},{:.2}) size=({:.2},{:.2}) content=({:.2},{:.2})",
3155 origin.x,
3156 origin.y,
3157 size.width,
3158 size.height,
3159 content_size.width,
3160 content_size.height
3161 ),
3162 dump_ref: None,
3163 },
3164 );
3165 }
3166
3167 if record {
3168 let rect = LayoutRect::new(
3169 rect_origin.x,
3170 rect_origin.y,
3171 rect_size.width,
3172 rect_size.height,
3173 );
3174 out.insert(
3175 node_id,
3176 LayoutNodeGeometry {
3177 rect,
3178 content_size: rect_content,
3179 },
3180 );
3181 }
3182 rect_size
3183 }
3184}