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, WidgetId};
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: WidgetId) -> f32;
62}
63
64impl<F> ScrollDataSource for F
65where
66 F: Fn(WidgetId) -> f32,
67{
68 fn get_offset(&self, node_id: WidgetId) -> 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: WidgetId, 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<WidgetId>,
339 missing_parent_refs: Vec<(WidgetId, WidgetId)>,
340 missing_child_refs: Vec<(WidgetId, WidgetId)>,
341 parent_child_mismatches: Vec<(WidgetId, WidgetId, Option<WidgetId>)>,
342 cycle_nodes: Vec<WidgetId>,
343 root_nodes: Vec<WidgetId>,
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<WidgetId>,
391 node_fingerprints: HashMap<WidgetId, u64>,
392 nodes: HashMap<WidgetId, LayoutInputNode>,
393 parents: HashMap<WidgetId, Option<WidgetId>>,
394 children: HashMap<WidgetId, Vec<WidgetId>>,
395 roots: Vec<WidgetId>,
396 validation: LayoutGraphValidationState,
397}
398
399#[derive(Debug, Clone, Default)]
400struct IncrementalLayoutReuseState {
401 previous_snapshot: LayoutSnapshot,
402 dirty_ancestors: HashSet<WidgetId>,
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: WidgetId) -> Option<&LayoutInputNode> {
532 self.nodes.get(&node_id)
533 }
534
535 fn children_of(&self, node_id: WidgetId) -> &[WidgetId] {
536 self.children
537 .get(&node_id)
538 .map(Vec::as_slice)
539 .unwrap_or(&[])
540 }
541
542 fn parent_of(&self, node_id: WidgetId) -> Option<WidgetId> {
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<WidgetId> {
553 fn dfs(
554 node_id: WidgetId,
555 children: &HashMap<WidgetId, Vec<WidgetId>>,
556 visited: &mut HashSet<WidgetId>,
557 stack: &mut HashSet<WidgetId>,
558 cycle_nodes: &mut Vec<WidgetId>,
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, WidgetId};
599
600 fn box_node(
601 id: WidgetId,
602 parent_id: Option<WidgetId>,
603 children_ids: Vec<WidgetId>,
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 = WidgetId::from_u128(1);
633 let first = WidgetId::from_u128(2);
634 let second = WidgetId::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 = WidgetId::from_u128(10);
653 let first = WidgetId::from_u128(11);
654 let second = WidgetId::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<WidgetId, LayoutNodeGeometry>,
782 #[serde(skip)]
785 pub constraints: HashMap<WidgetId, 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: WidgetId) -> Option<&LayoutNodeGeometry> {
803 self.nodes.get(&node_id)
804 }
805
806 pub fn get_node_rect(&self, node_id: WidgetId) -> Option<LayoutRect> {
808 self.nodes.get(&node_id).map(|g| g.rect)
809 }
810
811 pub fn get_node_constraints(&self, node_id: WidgetId) -> Option<BoxConstraints> {
814 self.constraints.get(&node_id).copied()
815 }
816}
817
818#[derive(Debug, Clone)]
827pub struct LayoutInputNode {
828 pub id: WidgetId,
830 pub parent_id: Option<WidgetId>,
832 pub op: LayoutOp,
834 pub children_ids: Vec<WidgetId>,
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: WidgetId) -> 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(
1149 &self,
1150 input_nodes: &[LayoutInputNode],
1151 root: WidgetId,
1152 ) -> Result<()> {
1153 if self.graph_state.matches_input_nodes(input_nodes) {
1154 return self.validate_graph_state(root);
1155 }
1156
1157 let node_map: HashMap<WidgetId, &LayoutInputNode> =
1158 input_nodes.iter().map(|n| (n.id, n)).collect();
1159 for n in input_nodes {
1161 for child in &n.children_ids {
1162 let child_node = node_map
1163 .get(child)
1164 .ok_or_else(|| anyhow::anyhow!("[verify] child {:?} not found", child))?;
1165 if child_node.parent_id != Some(n.id) {
1166 anyhow::bail!("[verify] parent/child mismatch parent={:?} child={:?} child.parent_id={:?}", n.id, child, child_node.parent_id);
1167 }
1168 }
1169 }
1170 fn dfs(
1172 id: WidgetId,
1173 map: &HashMap<WidgetId, &LayoutInputNode>,
1174 visited: &mut HashSet<WidgetId>,
1175 stack: &mut HashSet<WidgetId>,
1176 ) -> Result<()> {
1177 if !visited.insert(id) {
1178 return Ok(());
1179 }
1180 stack.insert(id);
1181 let node = map
1182 .get(&id)
1183 .ok_or_else(|| anyhow::anyhow!("[verify] missing node {:?}", id))?;
1184 for child in &node.children_ids {
1185 if stack.contains(child) {
1186 anyhow::bail!("[verify] cycle detected at {:?} -> {:?}", id, child);
1187 }
1188 dfs(*child, map, visited, stack)?;
1189 }
1190 stack.remove(&id);
1191 Ok(())
1192 }
1193 let mut visited = HashSet::new();
1194 let mut stack = HashSet::new();
1195 dfs(root, &node_map, &mut visited, &mut stack)?;
1196 Ok(())
1197 }
1198
1199 pub fn compute_layout(
1217 &mut self,
1218 input_nodes: &[LayoutInputNode],
1219 root_node_id: WidgetId,
1220 viewport_size: LayoutSize,
1221 scroll_source: &impl ScrollDataSource,
1222 ) -> Result<LayoutSnapshot> {
1223 self.ensure_graph_state(input_nodes);
1224 self.validate_graph_state(root_node_id)?;
1225 let snapshot = self.compute_layout_constraints(
1226 input_nodes,
1227 root_node_id,
1228 viewport_size,
1229 scroll_source,
1230 )?;
1231 self.emit_scroll_diagnostics(&snapshot);
1232 Ok(snapshot)
1233 }
1234
1235 pub fn compute_layout_constraints(
1240 &mut self,
1241 input_nodes: &[LayoutInputNode],
1242 root_node_id: WidgetId,
1243 viewport_size: LayoutSize,
1244 scroll_source: &impl ScrollDataSource,
1245 ) -> Result<LayoutSnapshot> {
1246 self.ensure_graph_state(input_nodes);
1247 self.validate_graph_state(root_node_id)?;
1248
1249 let mut constraints = BoxConstraints::tight(viewport_size);
1251 if let Some(root) = self.graph_state.node(root_node_id) {
1252 if root.width.is_some() || root.height.is_some() {
1254 constraints = BoxConstraints::loose(viewport_size.width, viewport_size.height)
1255 .tighten(root.width, root.height);
1256 }
1257 }
1258
1259 let mut snapshot = LayoutSnapshot::new(viewport_size);
1260 let mut measure_cache = HashMap::new();
1261 self.layout_node_constraints(
1262 root_node_id,
1263 constraints,
1264 LayoutPoint::ZERO,
1265 &mut snapshot.nodes,
1266 &mut snapshot.constraints,
1267 &mut measure_cache,
1268 scroll_source,
1269 true,
1270 0,
1271 )?;
1272
1273 let visual_location = |node_id: WidgetId| -> Option<LayoutPoint> {
1274 let mut pos = snapshot.nodes.get(&node_id)?.rect.origin;
1275 let mut current = self.graph_state.parent_of(node_id);
1276 while let Some(parent_id) = current {
1277 if let Some(parent) = self.graph_state.node(parent_id) {
1278 if let LayoutOp::Scroll { direction, .. } = &parent.op {
1279 let offset = scroll_source.get_offset(parent_id);
1280 match direction {
1281 FlexDirection::Row => pos.x -= offset,
1282 FlexDirection::Column => pos.y -= offset,
1283 }
1284 }
1285 current = self.graph_state.parent_of(parent_id);
1286 } else {
1287 break;
1288 }
1289 }
1290 Some(pos)
1291 };
1292
1293 let mut flyout_abs_overrides: HashMap<WidgetId, (f32, f32)> = HashMap::new();
1294 for node in self.graph_state.ordered_nodes() {
1295 if let LayoutOp::Flyout { anchor, content } = node.op {
1296 if let (Some(anchor_geom), Some(content_geom)) =
1297 (snapshot.nodes.get(&anchor), snapshot.nodes.get(&content))
1298 {
1299 if let Some(anchor_abs) = visual_location(anchor) {
1300 let content_w = content_geom.rect.width();
1301 let content_h = content_geom.rect.height();
1302 let anchor_h = anchor_geom.rect.height();
1303 let max_left = (snapshot.viewport_size.width - content_w).max(0.0);
1304 let left_rel = anchor_abs.x.clamp(0.0, max_left);
1305
1306 let below_top = anchor_abs.y + anchor_h;
1307 let max_top = (snapshot.viewport_size.height - content_h).max(0.0);
1308 let top_rel = if below_top + content_h <= snapshot.viewport_size.height {
1309 below_top
1310 } else {
1311 let above_top = anchor_abs.y - content_h;
1312 if above_top >= 0.0 {
1313 above_top
1314 } else {
1315 below_top.clamp(0.0, max_top)
1316 }
1317 };
1318 flyout_abs_overrides.insert(content, (left_rel, top_rel));
1319 }
1320 }
1321 }
1322 }
1323
1324 if !flyout_abs_overrides.is_empty() {
1325 for (nid, (abs_x, abs_y)) in flyout_abs_overrides {
1326 if let Some(current) = snapshot.nodes.get(&nid) {
1327 let dx = abs_x - current.rect.origin.x;
1328 let dy = abs_y - current.rect.origin.y;
1329 let mut stack = vec![(nid, 0usize)];
1330 while let Some((current_id, depth)) = stack.pop() {
1331 if depth > Self::MAX_LAYOUT_RECURSION_DEPTH {
1332 return Err(self.layout_depth_overflow(current_id, depth));
1333 }
1334 if let Some(geometry) = snapshot.nodes.get_mut(¤t_id) {
1335 geometry.rect.origin.x += dx;
1336 geometry.rect.origin.y += dy;
1337 }
1338 for child_id in self.graph_state.children_of(current_id).iter().rev() {
1339 stack.push((*child_id, depth + 1));
1340 }
1341 }
1342 }
1343 }
1344 }
1345
1346 self.graph_state.mark_layout_complete();
1347 self.incremental_reuse = None;
1348
1349 Ok(snapshot)
1350 }
1351
1352 pub fn compute_layout_incremental(
1353 &mut self,
1354 input_nodes: &[LayoutInputNode],
1355 root_node_id: WidgetId,
1356 viewport_size: LayoutSize,
1357 scroll_source: &impl ScrollDataSource,
1358 previous_snapshot: &LayoutSnapshot,
1359 dirty_nodes: &HashSet<WidgetId>,
1360 ) -> Result<LayoutSnapshot> {
1361 self.ensure_graph_state(input_nodes);
1362 self.validate_graph_state(root_node_id)?;
1363
1364 let mut dirty_ancestors = HashSet::new();
1365 for node_id in dirty_nodes {
1366 let mut current = Some(*node_id);
1367 while let Some(id) = current {
1368 if !dirty_ancestors.insert(id) {
1369 break;
1370 }
1371 current = self.graph_state.parent_of(id);
1372 }
1373 }
1374 dirty_ancestors.insert(root_node_id);
1375
1376 self.incremental_reuse = Some(IncrementalLayoutReuseState {
1377 previous_snapshot: previous_snapshot.clone(),
1378 dirty_ancestors,
1379 });
1380 let result = self.compute_layout_constraints(
1381 input_nodes,
1382 root_node_id,
1383 viewport_size,
1384 scroll_source,
1385 );
1386 self.incremental_reuse = None;
1387 result
1388 }
1389
1390 fn emit_scroll_diagnostics(&self, snapshot: &LayoutSnapshot) {
1391 use fission_diagnostics::prelude as diag;
1392 let trace_scroll = std::env::var("FISSION_SCROLL_TRACE").ok().as_deref() == Some("1");
1393 for n in self.graph_state.ordered_nodes() {
1394 if let LayoutOp::Scroll { .. } = n.op {
1395 if let Some(g) = snapshot.nodes.get(&n.id) {
1396 let note = if g.rect.height() <= 0.0 {
1397 let parent_op = n
1398 .parent_id
1399 .and_then(|pid| self.graph_state.node(pid))
1400 .map(|p| format!("{:?}", p.op));
1401 let parent_constraints = n
1402 .parent_id
1403 .and_then(|pid| snapshot.constraints.get(&pid))
1404 .copied();
1405 snapshot
1406 .constraints
1407 .get(&n.id)
1408 .map(|c| {
1409 format!(
1410 "op={:?} parent={:?} parent_op={:?} parent_constraints={:?} constraints={:?}",
1411 n.op,
1412 n.parent_id,
1413 parent_op,
1414 parent_constraints,
1415 c
1416 )
1417 })
1418 } else {
1419 None
1420 };
1421 diag::emit(
1422 diag::DiagCategory::Layout,
1423 diag::DiagLevel::Debug,
1424 diag::DiagEventKind::ScrollExtent {
1425 node: n.id.as_u128(),
1426 viewport_w: g.rect.width(),
1427 viewport_h: g.rect.height(),
1428 content_w: g.content_size.width,
1429 content_h: g.content_size.height,
1430 note,
1431 },
1432 );
1433 if trace_scroll {
1434 eprintln!(
1435 "[scroll-trace] node={} viewport=({:.1},{:.1}) content=({:.1},{:.1})",
1436 n.id.as_u128(),
1437 g.rect.width(),
1438 g.rect.height(),
1439 g.content_size.width,
1440 g.content_size.height
1441 );
1442 }
1443 }
1444 }
1445 }
1446 }
1447
1448 fn layout_depth_overflow(&self, node_id: WidgetId, depth: usize) -> anyhow::Error {
1449 let details = format!(
1450 "layout recursion depth {} exceeded max {} at node {}",
1451 depth,
1452 Self::MAX_LAYOUT_RECURSION_DEPTH,
1453 node_id.as_u128()
1454 );
1455 diag::emit(
1456 diag::DiagCategory::Invariants,
1457 diag::DiagLevel::Error,
1458 diag::DiagEventKind::InvariantViolation {
1459 kind: "layout_recursion_depth".into(),
1460 node: Some(node_id.as_u128()),
1461 details: details.clone(),
1462 dump_ref: None,
1463 },
1464 );
1465 anyhow::anyhow!(details)
1466 }
1467
1468 fn copy_cached_subtree(
1469 &self,
1470 node_id: WidgetId,
1471 origin: LayoutPoint,
1472 current_constraints: BoxConstraints,
1473 out: &mut HashMap<WidgetId, LayoutNodeGeometry>,
1474 constraints_out: &mut HashMap<WidgetId, BoxConstraints>,
1475 ) -> Result<Option<LayoutSize>> {
1476 let Some(reuse) = self.incremental_reuse.as_ref() else {
1477 return Ok(None);
1478 };
1479 if reuse.dirty_ancestors.contains(&node_id) {
1480 return Ok(None);
1481 }
1482
1483 let Some(previous_geometry) = reuse.previous_snapshot.nodes.get(&node_id) else {
1484 return Ok(None);
1485 };
1486 let Some(previous_constraints) = reuse.previous_snapshot.constraints.get(&node_id).copied()
1487 else {
1488 return Ok(None);
1489 };
1490 if previous_constraints != current_constraints {
1491 return Ok(None);
1492 }
1493
1494 let dx = origin.x - previous_geometry.rect.origin.x;
1495 let dy = origin.y - previous_geometry.rect.origin.y;
1496 let mut stack = vec![(node_id, 0usize)];
1497 while let Some((current_id, depth)) = stack.pop() {
1498 if depth > Self::MAX_LAYOUT_RECURSION_DEPTH {
1499 return Err(self.layout_depth_overflow(current_id, depth));
1500 }
1501 let Some(previous_geometry) = reuse.previous_snapshot.nodes.get(¤t_id) else {
1502 return Ok(None);
1503 };
1504 let Some(previous_constraints) = reuse
1505 .previous_snapshot
1506 .constraints
1507 .get(¤t_id)
1508 .copied()
1509 else {
1510 return Ok(None);
1511 };
1512
1513 let mut geometry = previous_geometry.clone();
1514 geometry.rect.origin.x += dx;
1515 geometry.rect.origin.y += dy;
1516 out.insert(current_id, geometry);
1517 constraints_out.insert(current_id, previous_constraints);
1518
1519 let children = self.graph_state.children_of(current_id);
1520 for child_id in children.iter().rev() {
1521 stack.push((*child_id, depth + 1));
1522 }
1523 }
1524
1525 Ok(Some(previous_geometry.content_size))
1526 }
1527
1528 fn layout_node_constraints(
1529 &self,
1530 node_id: WidgetId,
1531 constraints: BoxConstraints,
1532 origin: LayoutPoint,
1533 out: &mut HashMap<WidgetId, LayoutNodeGeometry>,
1534 constraints_out: &mut HashMap<WidgetId, BoxConstraints>,
1535 measure_cache: &mut HashMap<MeasureCacheKey, LayoutSize>,
1536 scroll_source: &impl ScrollDataSource,
1537 record: bool,
1538 depth: usize,
1539 ) -> Result<LayoutSize> {
1540 if depth > Self::MAX_LAYOUT_RECURSION_DEPTH {
1541 return Err(self.layout_depth_overflow(node_id, depth));
1542 }
1543 if !record {
1544 let cache_key = MeasureCacheKey::new(node_id, constraints);
1545 if let Some(cached) = measure_cache.get(&cache_key).copied() {
1546 return Ok(cached);
1547 }
1548 }
1549 let node = match self.graph_state.node(node_id) {
1550 Some(node) => node,
1551 None => return Ok(LayoutSize::ZERO),
1552 };
1553
1554 if record {
1555 constraints_out.insert(node_id, constraints);
1556 }
1557
1558 if record {
1559 if let Some(reused) =
1560 self.copy_cached_subtree(node_id, origin, constraints, out, constraints_out)?
1561 {
1562 return Ok(reused);
1563 }
1564 }
1565
1566 let mut flow_children: Vec<WidgetId> = Vec::new();
1567 let mut abs_children: Vec<WidgetId> = Vec::new();
1568 for child_id in self.graph_state.children_of(node_id) {
1569 let is_absolute = matches!(
1570 self.graph_state.node(*child_id).map(|n| &n.op),
1571 Some(LayoutOp::AbsoluteFill) | Some(LayoutOp::Positioned { .. })
1572 );
1573 if is_absolute {
1574 abs_children.push(*child_id);
1575 } else {
1576 flow_children.push(*child_id);
1577 }
1578 }
1579 let rich_text_inline_children = node.rich_text.is_some() && !flow_children.is_empty();
1580
1581 let mut content_size;
1582 let size = match &node.op {
1583 LayoutOp::Box {
1584 width,
1585 height,
1586 min_width,
1587 max_width,
1588 min_height,
1589 max_height,
1590 padding,
1591 aspect_ratio,
1592 ..
1593 } => {
1594 let mut local =
1595 constraints.apply_min_max(*min_width, *max_width, *min_height, *max_height);
1596 local = local.tighten(*width, *height);
1597 if let Some(ratio) = aspect_ratio.filter(|r| *r > 0.0) {
1598 let mut target_w = *width;
1599 let mut target_h = *height;
1600
1601 if target_w.is_some() && target_h.is_none() {
1602 target_h = Some(target_w.unwrap() / ratio);
1603 } else if target_h.is_some() && target_w.is_none() {
1604 target_w = Some(target_h.unwrap() * ratio);
1605 } else if target_w.is_none() && target_h.is_none() {
1606 if local.is_width_bounded() || local.is_height_bounded() {
1607 let (mut w, mut h) = if local.is_width_bounded() {
1608 let w = local.max_w;
1609 let h = w / ratio;
1610 (w, h)
1611 } else {
1612 let h = local.max_h;
1613 let w = h * ratio;
1614 (w, h)
1615 };
1616 if local.is_width_bounded()
1617 && local.is_height_bounded()
1618 && h > local.max_h
1619 {
1620 h = local.max_h;
1621 w = h * ratio;
1622 }
1623 target_w = Some(w);
1624 target_h = Some(h);
1625 }
1626 }
1627
1628 if target_w.is_some() || target_h.is_some() {
1629 local = local.tighten(target_w, target_h);
1630 }
1631 }
1632 let base_child_constraints = local.deflate(*padding);
1633 let mut max_child = LayoutSize::ZERO;
1634 let mut measured_children: Vec<(WidgetId, BoxConstraints, LayoutSize)> = Vec::new();
1635 if !rich_text_inline_children {
1636 for child_id in &flow_children {
1637 let (child_width, child_height, child_max_width, child_max_height) = self
1638 .graph_state
1639 .node(*child_id)
1640 .map(|child| match &child.op {
1641 LayoutOp::Box {
1642 width,
1643 height,
1644 max_width,
1645 max_height,
1646 ..
1647 } => (*width, *height, *max_width, *max_height),
1648 LayoutOp::Scroll {
1649 width,
1650 height,
1651 max_width,
1652 max_height,
1653 ..
1654 } => (*width, *height, *max_width, *max_height),
1655 LayoutOp::Embed { width, height, .. } => {
1656 (*width, *height, None, None)
1657 }
1658 _ => (None, None, None, None),
1659 })
1660 .unwrap_or((None, None, None, None));
1661 let mut child_constraints = base_child_constraints;
1662 let stretch_width = child_constraints.min_w == child_constraints.max_w
1663 && child_width.is_none()
1664 && child_max_width.is_none();
1665 if stretch_width {
1666 child_constraints.min_w = child_constraints.max_w;
1667 } else {
1668 child_constraints.min_w = 0.0;
1669 }
1670 let stretch_height = child_constraints.min_h == child_constraints.max_h
1671 && child_height.is_none()
1672 && child_max_height.is_none();
1673 if stretch_height {
1674 child_constraints.min_h = child_constraints.max_h;
1675 } else {
1676 child_constraints.min_h = 0.0;
1677 }
1678 let child_size = self.layout_node_constraints(
1679 *child_id,
1680 child_constraints,
1681 LayoutPoint::ZERO,
1682 out,
1683 constraints_out,
1684 measure_cache,
1685 scroll_source,
1686 false,
1687 depth + 1,
1688 )?;
1689 max_child.width = max_child.width.max(child_size.width);
1690 max_child.height = max_child.height.max(child_size.height);
1691 measured_children.push((*child_id, child_constraints, child_size));
1692 }
1693 }
1694 let padded = LayoutSize::new(
1695 max_child.width + padding[0] + padding[1],
1696 max_child.height + padding[2] + padding[3],
1697 );
1698 let size = local.constrain(padded);
1699 if record {
1700 for (child_id, child_constraints, _child_size) in measured_children {
1701 self.layout_node_constraints(
1702 child_id,
1703 child_constraints,
1704 LayoutPoint::new(origin.x + padding[0], origin.y + padding[2]),
1705 out,
1706 constraints_out,
1707 measure_cache,
1708 scroll_source,
1709 record,
1710 depth + 1,
1711 )?;
1712 }
1713 if !abs_children.is_empty() {
1714 let abs_constraints = BoxConstraints::loose(size.width, size.height);
1715 for child_id in abs_children {
1716 self.layout_node_constraints(
1717 child_id,
1718 abs_constraints,
1719 origin,
1720 out,
1721 constraints_out,
1722 measure_cache,
1723 scroll_source,
1724 record,
1725 depth + 1,
1726 )?;
1727 }
1728 }
1729 }
1730 content_size = padded;
1731 size
1732 }
1733 LayoutOp::Flex {
1734 direction,
1735 wrap,
1736 padding,
1737 gap,
1738 align_items,
1739 justify_content,
1740 flex_grow,
1741 ..
1742 } => {
1743 let gap = gap.unwrap_or(0.0);
1744 let local = constraints.tighten(node.width, node.height);
1745 let inner = local.deflate(*padding);
1746 let is_row = matches!(direction, IrFlexDirection::Row);
1747
1748 let max_main = if is_row { inner.max_w } else { inner.max_h };
1749 let max_cross = if is_row { inner.max_h } else { inner.max_w };
1750 let min_main = if is_row { inner.min_w } else { inner.min_h };
1751 let min_cross = if is_row { inner.min_h } else { inner.min_w };
1752 let main_bounded = if is_row {
1753 inner.is_width_bounded()
1754 } else {
1755 inner.is_height_bounded()
1756 };
1757 let cross_bounded = if is_row {
1758 inner.is_height_bounded()
1759 } else {
1760 inner.is_width_bounded()
1761 };
1762
1763 if matches!(wrap, IrFlexWrap::Wrap | IrFlexWrap::WrapReverse) {
1764 let mut lines: Vec<(Vec<(WidgetId, LayoutSize, BoxConstraints)>, f32, f32)> =
1765 Vec::new();
1766 let mut line_children: Vec<(WidgetId, LayoutSize, BoxConstraints)> = Vec::new();
1767 let mut line_main = 0.0f32;
1768 let mut line_cross = 0.0f32;
1769 let mut max_line_main = 0.0f32;
1770
1771 for child_id in &flow_children {
1772 let child_constraints = if is_row {
1773 BoxConstraints {
1774 min_w: 0.0,
1775 max_w: max_main,
1776 min_h: 0.0,
1777 max_h: max_cross,
1778 }
1779 } else {
1780 BoxConstraints {
1781 min_w: 0.0,
1782 max_w: max_cross,
1783 min_h: 0.0,
1784 max_h: max_main,
1785 }
1786 };
1787 let child_size = self.layout_node_constraints(
1788 *child_id,
1789 child_constraints,
1790 LayoutPoint::ZERO,
1791 out,
1792 constraints_out,
1793 measure_cache,
1794 scroll_source,
1795 false,
1796 depth + 1,
1797 )?;
1798 let child_main = if is_row {
1799 child_size.width
1800 } else {
1801 child_size.height
1802 };
1803 let child_cross = if is_row {
1804 child_size.height
1805 } else {
1806 child_size.width
1807 };
1808 let next_main = if line_children.is_empty() {
1809 child_main
1810 } else {
1811 line_main + gap + child_main
1812 };
1813
1814 if main_bounded && !line_children.is_empty() && next_main > max_main {
1815 max_line_main = max_line_main.max(line_main);
1816 lines.push((line_children, line_main, line_cross));
1817 line_children = Vec::new();
1818 line_main = 0.0;
1819 line_cross = 0.0;
1820 }
1821
1822 if !line_children.is_empty() {
1823 line_main += gap;
1824 }
1825 line_main += child_main;
1826 line_cross = line_cross.max(child_cross);
1827 line_children.push((*child_id, child_size, child_constraints));
1828 }
1829
1830 if !line_children.is_empty() {
1831 max_line_main = max_line_main.max(line_main);
1832 lines.push((line_children, line_main, line_cross));
1833 }
1834
1835 let mut container_main = if main_bounded && *flex_grow > 0.0 {
1836 max_main
1837 } else {
1838 max_line_main
1839 };
1840 container_main = container_main.max(min_main);
1841 let total_lines_cross: f32 =
1842 lines.iter().map(|(_, _, cross)| *cross).sum::<f32>()
1843 + gap * lines.len().saturating_sub(1) as f32;
1844 let container_cross = total_lines_cross.max(min_cross);
1845 let size = if is_row {
1846 local.constrain(LayoutSize::new(
1847 container_main + padding[0] + padding[1],
1848 container_cross + padding[2] + padding[3],
1849 ))
1850 } else {
1851 local.constrain(LayoutSize::new(
1852 container_cross + padding[0] + padding[1],
1853 container_main + padding[2] + padding[3],
1854 ))
1855 };
1856
1857 let inner_main = if is_row {
1858 size.width - padding[0] - padding[1]
1859 } else {
1860 size.height - padding[2] - padding[3]
1861 };
1862 let inner_cross = if is_row {
1863 size.height - padding[2] - padding[3]
1864 } else {
1865 size.width - padding[0] - padding[1]
1866 };
1867
1868 let mut ordered_lines = lines;
1869 if matches!(wrap, IrFlexWrap::WrapReverse) {
1870 ordered_lines.reverse();
1871 }
1872
1873 let mut line_cursor = if matches!(wrap, IrFlexWrap::WrapReverse) {
1874 (inner_cross - total_lines_cross).max(0.0)
1875 } else {
1876 0.0
1877 };
1878
1879 for (line_children, line_main, line_cross) in ordered_lines {
1880 let remaining_space = (inner_main - line_main).max(0.0);
1881 let mut extra_gap = 0.0;
1882 let mut offset_main = 0.0;
1883 match justify_content {
1884 fission_ir::op::JustifyContent::Start => {}
1885 fission_ir::op::JustifyContent::End => offset_main = remaining_space,
1886 fission_ir::op::JustifyContent::Center => {
1887 offset_main = remaining_space / 2.0
1888 }
1889 fission_ir::op::JustifyContent::SpaceBetween => {
1890 if line_children.len() > 1 {
1891 extra_gap =
1892 remaining_space / (line_children.len() as f32 - 1.0);
1893 }
1894 }
1895 fission_ir::op::JustifyContent::SpaceAround => {
1896 if !line_children.is_empty() {
1897 extra_gap = remaining_space / line_children.len() as f32;
1898 offset_main = extra_gap / 2.0;
1899 }
1900 }
1901 fission_ir::op::JustifyContent::SpaceEvenly => {
1902 if !line_children.is_empty() {
1903 extra_gap =
1904 remaining_space / (line_children.len() as f32 + 1.0);
1905 offset_main = extra_gap;
1906 }
1907 }
1908 }
1909
1910 let mut cursor = offset_main;
1911 for (child_id, child_size, mut child_constraints) in line_children {
1912 let child_main = if is_row {
1913 child_size.width
1914 } else {
1915 child_size.height
1916 };
1917 let child_cross = if is_row {
1918 child_size.height
1919 } else {
1920 child_size.width
1921 };
1922 if matches!(align_items, fission_ir::op::AlignItems::Stretch) {
1923 if is_row {
1924 child_constraints.min_h = line_cross;
1925 child_constraints.max_h = line_cross;
1926 } else {
1927 child_constraints.min_w = line_cross;
1928 child_constraints.max_w = line_cross;
1929 }
1930 }
1931 let cross_offset = match align_items {
1932 fission_ir::op::AlignItems::Start
1933 | fission_ir::op::AlignItems::Stretch => 0.0,
1934 fission_ir::op::AlignItems::End => {
1935 (line_cross - child_cross).max(0.0)
1936 }
1937 fission_ir::op::AlignItems::Center => {
1938 ((line_cross - child_cross) / 2.0).max(0.0)
1939 }
1940 fission_ir::op::AlignItems::Baseline => 0.0,
1941 };
1942 let child_origin = if is_row {
1943 LayoutPoint::new(
1944 origin.x + padding[0] + cursor,
1945 origin.y + padding[2] + line_cursor + cross_offset,
1946 )
1947 } else {
1948 LayoutPoint::new(
1949 origin.x + padding[0] + line_cursor + cross_offset,
1950 origin.y + padding[2] + cursor,
1951 )
1952 };
1953 self.layout_node_constraints(
1954 child_id,
1955 child_constraints,
1956 child_origin,
1957 out,
1958 constraints_out,
1959 measure_cache,
1960 scroll_source,
1961 record,
1962 depth + 1,
1963 )?;
1964 cursor += child_main + gap + extra_gap;
1965 }
1966
1967 line_cursor += line_cross + gap;
1968 }
1969
1970 if record && !abs_children.is_empty() {
1971 let abs_constraints = BoxConstraints::loose(size.width, size.height);
1972 for child_id in abs_children {
1973 self.layout_node_constraints(
1974 child_id,
1975 abs_constraints,
1976 origin,
1977 out,
1978 constraints_out,
1979 measure_cache,
1980 scroll_source,
1981 record,
1982 depth + 1,
1983 )?;
1984 }
1985 }
1986 content_size = size;
1987 size
1988 } else {
1989 struct FlexChildEntry {
1990 id: WidgetId,
1991 flex: f32,
1992 size: LayoutSize,
1993 constraints: BoxConstraints,
1994 is_flex: bool,
1995 }
1996 let mut measured: Vec<FlexChildEntry> = Vec::new();
1997 let mut total_flex = 0.0f32;
1998 let mut nonflex_main = 0.0f32;
1999 let mut max_child_cross = 0.0f32;
2000 let treat_flex_as_nonflex = !main_bounded;
2001
2002 for child_id in &flow_children {
2003 let child = match self.graph_state.node(*child_id) {
2004 Some(child) => child,
2005 None => continue,
2006 };
2007 let flex = child.flex_grow;
2008 if flex > 0.0 && !treat_flex_as_nonflex {
2009 total_flex += flex;
2010 measured.push(FlexChildEntry {
2011 id: *child_id,
2012 flex,
2013 size: LayoutSize::ZERO,
2014 constraints: BoxConstraints::loose(0.0, 0.0),
2015 is_flex: true,
2016 });
2017 continue;
2018 }
2019 let child_constraints = if is_row {
2020 let cross =
2021 if matches!(align_items, fission_ir::op::AlignItems::Stretch)
2022 && cross_bounded
2023 {
2024 BoxConstraints {
2025 min_w: 0.0,
2026 max_w: f32::INFINITY,
2027 min_h: max_cross,
2028 max_h: max_cross,
2029 }
2030 } else {
2031 BoxConstraints {
2032 min_w: 0.0,
2033 max_w: f32::INFINITY,
2034 min_h: 0.0,
2035 max_h: max_cross,
2036 }
2037 };
2038 cross
2039 } else {
2040 let cross =
2041 if matches!(align_items, fission_ir::op::AlignItems::Stretch)
2042 && cross_bounded
2043 {
2044 BoxConstraints {
2045 min_w: max_cross,
2046 max_w: max_cross,
2047 min_h: 0.0,
2048 max_h: f32::INFINITY,
2049 }
2050 } else {
2051 BoxConstraints {
2052 min_w: 0.0,
2053 max_w: max_cross,
2054 min_h: 0.0,
2055 max_h: f32::INFINITY,
2056 }
2057 };
2058 cross
2059 };
2060 let child_size = self.layout_node_constraints(
2061 *child_id,
2062 child_constraints,
2063 LayoutPoint::ZERO,
2064 out,
2065 constraints_out,
2066 measure_cache,
2067 scroll_source,
2068 false,
2069 depth + 1,
2070 )?;
2071 let child_main = if is_row {
2072 child_size.width
2073 } else {
2074 child_size.height
2075 };
2076 let child_cross = if is_row {
2077 child_size.height
2078 } else {
2079 child_size.width
2080 };
2081 nonflex_main += child_main;
2082 max_child_cross = max_child_cross.max(child_cross);
2083 measured.push(FlexChildEntry {
2084 id: *child_id,
2085 flex,
2086 size: child_size,
2087 constraints: child_constraints,
2088 is_flex: false,
2089 });
2090 }
2091
2092 let gap_total = gap * flow_children.len().saturating_sub(1) as f32;
2093 let remaining = if main_bounded {
2094 (max_main - nonflex_main - gap_total).max(0.0)
2095 } else {
2096 0.0
2097 };
2098
2099 for entry in measured.iter_mut().filter(|e| e.is_flex) {
2100 let flex = entry.flex;
2101 let allocated = if main_bounded && total_flex > 0.0 {
2102 remaining * (flex / total_flex)
2103 } else {
2104 0.0
2105 };
2106 let child_constraints = if is_row {
2107 let cross =
2108 if matches!(align_items, fission_ir::op::AlignItems::Stretch)
2109 && cross_bounded
2110 {
2111 BoxConstraints {
2112 min_w: allocated,
2113 max_w: allocated,
2114 min_h: max_cross,
2115 max_h: max_cross,
2116 }
2117 } else {
2118 BoxConstraints {
2119 min_w: allocated,
2120 max_w: allocated,
2121 min_h: 0.0,
2122 max_h: max_cross,
2123 }
2124 };
2125 cross
2126 } else {
2127 let cross =
2128 if matches!(align_items, fission_ir::op::AlignItems::Stretch)
2129 && cross_bounded
2130 {
2131 BoxConstraints {
2132 min_w: max_cross,
2133 max_w: max_cross,
2134 min_h: allocated,
2135 max_h: allocated,
2136 }
2137 } else {
2138 BoxConstraints {
2139 min_w: 0.0,
2140 max_w: max_cross,
2141 min_h: allocated,
2142 max_h: allocated,
2143 }
2144 };
2145 cross
2146 };
2147 let child_size = self.layout_node_constraints(
2148 entry.id,
2149 child_constraints,
2150 LayoutPoint::ZERO,
2151 out,
2152 constraints_out,
2153 measure_cache,
2154 scroll_source,
2155 false,
2156 depth + 1,
2157 )?;
2158 let child_cross = if is_row {
2159 child_size.height
2160 } else {
2161 child_size.width
2162 };
2163 max_child_cross = max_child_cross.max(child_cross);
2164 entry.size = child_size;
2165 entry.constraints = child_constraints;
2166 }
2167
2168 let final_children_main: f32 = measured
2169 .iter()
2170 .map(|entry| {
2171 if is_row {
2172 entry.size.width
2173 } else {
2174 entry.size.height
2175 }
2176 })
2177 .sum();
2178
2179 let mut container_main = if main_bounded && *flex_grow > 0.0 {
2180 max_main
2181 } else {
2182 final_children_main + gap_total
2183 };
2184 container_main = container_main.max(min_main);
2185
2186 if main_bounded && final_children_main + gap_total > max_main {
2187 let mut total_shrink_scaled = 0.0f32;
2189 for entry in &measured {
2190 let child = self.graph_state.node(entry.id).unwrap();
2191 let main_size = if is_row {
2192 entry.size.width
2193 } else {
2194 entry.size.height
2195 };
2196 total_shrink_scaled += main_size * child.flex_shrink;
2197 }
2198
2199 if total_shrink_scaled > 0.0 {
2200 let overflow = (final_children_main + gap_total) - max_main;
2201 for entry in &mut measured {
2202 let child = self.graph_state.node(entry.id).unwrap();
2203 let main_size = if is_row {
2204 entry.size.width
2205 } else {
2206 entry.size.height
2207 };
2208 let shrink_amount = (main_size * child.flex_shrink
2209 / total_shrink_scaled)
2210 * overflow;
2211 let floor = if child.flex_shrink > 0.0 {
2215 let explicit_min = match &child.op {
2217 LayoutOp::Box {
2218 min_width,
2219 min_height,
2220 height,
2221 width,
2222 ..
2223 } => {
2224 if is_row {
2225 min_width.or(*width).unwrap_or(0.0)
2226 } else {
2227 min_height.or(*height).unwrap_or(0.0)
2228 }
2229 }
2230 _ => 0.0,
2231 };
2232 explicit_min
2233 } else {
2234 main_size };
2236 let new_main = (main_size - shrink_amount).max(floor);
2237
2238 let mut child_constraints = entry.constraints;
2239 if is_row {
2240 child_constraints.min_w = new_main;
2241 child_constraints.max_w = new_main;
2242 } else {
2243 child_constraints.min_h = new_main;
2244 child_constraints.max_h = new_main;
2245 }
2246 let new_size = self.layout_node_constraints(
2247 entry.id,
2248 child_constraints,
2249 LayoutPoint::ZERO,
2250 out,
2251 constraints_out,
2252 measure_cache,
2253 scroll_source,
2254 false,
2255 depth + 1,
2256 )?;
2257 entry.size = new_size;
2258 entry.constraints = child_constraints;
2259 }
2260 }
2261 }
2262
2263 let container_cross = max_child_cross.max(min_cross);
2264 let size = if is_row {
2265 local.constrain(LayoutSize::new(
2266 container_main + padding[0] + padding[1],
2267 container_cross + padding[2] + padding[3],
2268 ))
2269 } else {
2270 local.constrain(LayoutSize::new(
2271 container_cross + padding[0] + padding[1],
2272 container_main + padding[2] + padding[3],
2273 ))
2274 };
2275
2276 let inner_main = if is_row {
2277 size.width - padding[0] - padding[1]
2278 } else {
2279 size.height - padding[2] - padding[3]
2280 };
2281 let inner_cross = if is_row {
2282 size.height - padding[2] - padding[3]
2283 } else {
2284 size.width - padding[0] - padding[1]
2285 };
2286
2287 let final_children_main: f32 = measured
2288 .iter()
2289 .map(|entry| {
2290 if is_row {
2291 entry.size.width
2292 } else {
2293 entry.size.height
2294 }
2295 })
2296 .sum();
2297
2298 let remaining_space = (inner_main - final_children_main - gap_total).max(0.0);
2299 let mut extra_gap = 0.0;
2300 let mut offset_main = 0.0;
2301 match justify_content {
2302 fission_ir::op::JustifyContent::Start => {}
2303 fission_ir::op::JustifyContent::End => offset_main = remaining_space,
2304 fission_ir::op::JustifyContent::Center => {
2305 offset_main = remaining_space / 2.0
2306 }
2307 fission_ir::op::JustifyContent::SpaceBetween => {
2308 if measured.len() > 1 {
2309 extra_gap = remaining_space / (measured.len() as f32 - 1.0);
2310 }
2311 }
2312 fission_ir::op::JustifyContent::SpaceAround => {
2313 if !measured.is_empty() {
2314 extra_gap = remaining_space / measured.len() as f32;
2315 offset_main = extra_gap / 2.0;
2316 }
2317 }
2318 fission_ir::op::JustifyContent::SpaceEvenly => {
2319 if !measured.is_empty() {
2320 extra_gap = remaining_space / (measured.len() as f32 + 1.0);
2321 offset_main = extra_gap;
2322 }
2323 }
2324 }
2325
2326 let mut cursor = offset_main;
2327 for entry in measured {
2328 let child_main = if is_row {
2329 entry.size.width
2330 } else {
2331 entry.size.height
2332 };
2333 let child_cross = if is_row {
2334 entry.size.height
2335 } else {
2336 entry.size.width
2337 };
2338 let cross_offset = match align_items {
2339 fission_ir::op::AlignItems::Start
2340 | fission_ir::op::AlignItems::Stretch => 0.0,
2341 fission_ir::op::AlignItems::End => (inner_cross - child_cross).max(0.0),
2342 fission_ir::op::AlignItems::Center => {
2343 ((inner_cross - child_cross) / 2.0).max(0.0)
2344 }
2345 fission_ir::op::AlignItems::Baseline => 0.0,
2346 };
2347 let child_origin = if is_row {
2348 LayoutPoint::new(
2349 origin.x + padding[0] + cursor,
2350 origin.y + padding[2] + cross_offset,
2351 )
2352 } else {
2353 LayoutPoint::new(
2354 origin.x + padding[0] + cross_offset,
2355 origin.y + padding[2] + cursor,
2356 )
2357 };
2358
2359 let mut child_constraints = entry.constraints;
2360 if matches!(align_items, fission_ir::op::AlignItems::Stretch) {
2361 let child_node = self.graph_state.node(entry.id);
2363 let has_explicit_cross = child_node
2364 .map(|n| match &n.op {
2365 LayoutOp::Box { width, height, .. } => {
2366 if is_row {
2367 height.is_some()
2368 } else {
2369 width.is_some()
2370 }
2371 }
2372 _ => false,
2373 })
2374 .unwrap_or(false);
2375 if !has_explicit_cross {
2376 if is_row {
2377 child_constraints.min_h = inner_cross;
2378 child_constraints.max_h = inner_cross;
2379 } else {
2380 child_constraints.min_w = inner_cross;
2381 child_constraints.max_w = inner_cross;
2382 }
2383 }
2384 }
2385
2386 self.layout_node_constraints(
2387 entry.id,
2388 child_constraints,
2389 child_origin,
2390 out,
2391 constraints_out,
2392 measure_cache,
2393 scroll_source,
2394 record,
2395 depth + 1,
2396 )?;
2397 cursor += child_main + gap + extra_gap;
2398 }
2399
2400 if record && !abs_children.is_empty() {
2401 let abs_constraints = BoxConstraints::loose(size.width, size.height);
2402 for child_id in abs_children {
2403 self.layout_node_constraints(
2404 child_id,
2405 abs_constraints,
2406 origin,
2407 out,
2408 constraints_out,
2409 measure_cache,
2410 scroll_source,
2411 record,
2412 depth + 1,
2413 )?;
2414 }
2415 }
2416 content_size = size;
2417 size
2418 }
2419 }
2420 LayoutOp::Grid {
2421 columns,
2422 rows,
2423 column_gap,
2424 row_gap,
2425 padding,
2426 } => {
2427 let gap_x = column_gap.unwrap_or(0.0);
2428 let gap_y = row_gap.unwrap_or(0.0);
2429 let inner = constraints.deflate(*padding);
2430 let bounded_w = inner.is_width_bounded();
2431 let bounded_h = inner.is_height_bounded();
2432 let available_w = if bounded_w { inner.max_w } else { 0.0 };
2433 let available_h = if bounded_h { inner.max_h } else { 0.0 };
2434
2435 let col_count = columns.len().max(1);
2436 let mut col_widths = vec![0.0f32; col_count];
2437 let mut fr_total = 0.0f32;
2438 let mut fixed_total = 0.0f32;
2439 for (i, track) in columns.iter().enumerate() {
2440 match track {
2441 GridTrack::Points(p) => {
2442 col_widths[i] = *p;
2443 fixed_total += *p;
2444 }
2445 GridTrack::Percent(p) => {
2446 let w = if bounded_w {
2447 available_w * (*p / 100.0)
2448 } else {
2449 0.0
2450 };
2451 col_widths[i] = w;
2452 fixed_total += w;
2453 }
2454 GridTrack::Fr(f) => fr_total += *f,
2455 _ => {}
2456 }
2457 }
2458 if fr_total > 0.0 && bounded_w {
2459 let remaining =
2460 (available_w - fixed_total - gap_x * (col_count.saturating_sub(1) as f32))
2461 .max(0.0);
2462 for (i, track) in columns.iter().enumerate() {
2463 if let GridTrack::Fr(f) = track {
2464 col_widths[i] = remaining * (*f / fr_total);
2465 }
2466 }
2467 }
2468
2469 let child_count = flow_children.len();
2470 let row_count = if rows.is_empty() {
2471 (child_count + col_count - 1) / col_count
2472 } else {
2473 rows.len()
2474 };
2475 let mut row_heights = vec![0.0f32; row_count.max(1)];
2476
2477 if !rows.is_empty() {
2478 let mut row_fr_total = 0.0f32;
2479 let mut row_fixed_total = 0.0f32;
2480 for (i, track) in rows.iter().enumerate() {
2481 if i >= row_heights.len() {
2482 break;
2483 }
2484 match track {
2485 GridTrack::Points(p) => {
2486 row_heights[i] = *p;
2487 row_fixed_total += *p;
2488 }
2489 GridTrack::Percent(p) => {
2490 let h = if bounded_h {
2491 available_h * (*p / 100.0)
2492 } else {
2493 0.0
2494 };
2495 row_heights[i] = h;
2496 row_fixed_total += h;
2497 }
2498 GridTrack::Fr(f) => row_fr_total += *f,
2499 _ => {}
2500 }
2501 }
2502 if row_fr_total > 0.0 && bounded_h {
2503 let remaining = (available_h
2504 - row_fixed_total
2505 - gap_y * (row_heights.len().saturating_sub(1) as f32))
2506 .max(0.0);
2507 for (i, track) in rows.iter().enumerate() {
2508 if let GridTrack::Fr(f) = track {
2509 row_heights[i] = remaining * (*f / row_fr_total);
2510 }
2511 }
2512 }
2513 }
2514
2515 let mut cell_assignments = Vec::new();
2516 let mut auto_row = 0;
2517 let mut auto_col = 0;
2518
2519 for child_id in &flow_children {
2520 let child = self.graph_state.node(*child_id).unwrap();
2521 let (row, col) = if let LayoutOp::GridItem {
2522 row_start,
2523 col_start,
2524 ..
2525 } = &child.op
2526 {
2527 let r = match row_start {
2528 fission_ir::op::GridPlacement::Line(l) => {
2529 (*l as usize).saturating_sub(1)
2530 }
2531 _ => auto_row,
2532 };
2533 let c = match col_start {
2534 fission_ir::op::GridPlacement::Line(l) => {
2535 (*l as usize).saturating_sub(1)
2536 }
2537 _ => auto_col,
2538 };
2539 (r, c)
2540 } else {
2541 let res = (auto_row, auto_col);
2542 auto_col += 1;
2543 if auto_col >= col_count {
2544 auto_col = 0;
2545 auto_row += 1;
2546 }
2547 res
2548 };
2549 cell_assignments.push((*child_id, row, col));
2550 }
2551
2552 for (child_id, row, col) in &cell_assignments {
2553 if *row >= row_heights.len() || *col >= col_widths.len() {
2554 continue;
2555 }
2556 let cell_w = col_widths[*col];
2557 let cell_constraints = BoxConstraints {
2558 min_w: cell_w,
2559 max_w: cell_w,
2560 min_h: 0.0,
2561 max_h: if row_heights[*row] > 0.0 {
2562 row_heights[*row]
2563 } else {
2564 f32::INFINITY
2565 },
2566 };
2567 let child_size = self.layout_node_constraints(
2568 *child_id,
2569 cell_constraints,
2570 LayoutPoint::ZERO,
2571 out,
2572 constraints_out,
2573 measure_cache,
2574 scroll_source,
2575 false,
2576 depth + 1,
2577 )?;
2578 if row_heights[*row] == 0.0 {
2579 row_heights[*row] = child_size.height;
2580 } else {
2581 row_heights[*row] = row_heights[*row].max(child_size.height);
2582 }
2583 }
2584
2585 let grid_w: f32 =
2586 col_widths.iter().sum::<f32>() + gap_x * (col_count.saturating_sub(1) as f32);
2587 let grid_h: f32 = row_heights.iter().sum::<f32>()
2588 + gap_y * (row_heights.len().saturating_sub(1) as f32);
2589 let size = constraints.constrain(LayoutSize::new(
2590 grid_w + padding[0] + padding[1],
2591 grid_h + padding[2] + padding[3],
2592 ));
2593
2594 if record {
2595 let padding_origin_x = origin.x + padding[0];
2596 let padding_origin_y = origin.y + padding[2];
2597 for (child_id, row, col) in &cell_assignments {
2598 if *row >= row_heights.len() || *col >= col_widths.len() {
2599 continue;
2600 }
2601 let mut cell_x = padding_origin_x;
2602 for i in 0..*col {
2603 cell_x += col_widths[i] + gap_x;
2604 }
2605 let mut cell_y = padding_origin_y;
2606 for i in 0..*row {
2607 cell_y += row_heights[i] + gap_y;
2608 }
2609 let cell_w = col_widths[*col];
2610 let cell_h = row_heights[*row];
2611 let child_constraints = BoxConstraints {
2612 min_w: cell_w,
2613 max_w: cell_w,
2614 min_h: cell_h,
2615 max_h: cell_h,
2616 };
2617 self.layout_node_constraints(
2618 *child_id,
2619 child_constraints,
2620 LayoutPoint::new(cell_x, cell_y),
2621 out,
2622 constraints_out,
2623 measure_cache,
2624 scroll_source,
2625 record,
2626 depth + 1,
2627 )?;
2628 }
2629 }
2630
2631 if record && !abs_children.is_empty() {
2632 let abs_constraints = BoxConstraints::loose(size.width, size.height);
2633 for child_id in abs_children {
2634 self.layout_node_constraints(
2635 child_id,
2636 abs_constraints,
2637 origin,
2638 out,
2639 constraints_out,
2640 measure_cache,
2641 scroll_source,
2642 record,
2643 depth + 1,
2644 )?;
2645 }
2646 }
2647 content_size = size;
2648 size
2649 }
2650 LayoutOp::GridItem { .. } => {
2651 let mut child_size = LayoutSize::ZERO;
2652 if let Some(child_id) = node.children_ids.first() {
2653 child_size = self.layout_node_constraints(
2654 *child_id,
2655 constraints,
2656 origin,
2657 out,
2658 constraints_out,
2659 measure_cache,
2660 scroll_source,
2661 record,
2662 depth + 1,
2663 )?;
2664 }
2665 content_size = child_size;
2666 constraints.constrain(child_size)
2667 }
2668 LayoutOp::Scroll {
2669 direction,
2670 width,
2671 height,
2672 min_width,
2673 max_width,
2674 min_height,
2675 max_height,
2676 padding,
2677 ..
2678 } => {
2679 let mut local =
2680 constraints.apply_min_max(*min_width, *max_width, *min_height, *max_height);
2681 local = local.tighten(*width, *height);
2682 let is_horizontal = matches!(direction, FlexDirection::Row);
2683 let mut child_constraints = local.deflate(*padding);
2684 if is_horizontal {
2685 child_constraints.min_w = 0.0;
2686 child_constraints.max_w = f32::INFINITY;
2687 } else {
2688 child_constraints.min_h = 0.0;
2689 child_constraints.max_h = f32::INFINITY;
2690 }
2691 let mut child_size = LayoutSize::ZERO;
2692 if let Some(child_id) = flow_children.first() {
2693 child_size = self.layout_node_constraints(
2694 *child_id,
2695 child_constraints,
2696 LayoutPoint::ZERO,
2697 out,
2698 constraints_out,
2699 measure_cache,
2700 scroll_source,
2701 false,
2702 depth + 1,
2703 )?;
2704 }
2705 let size = local.constrain(LayoutSize::new(
2706 child_size.width + padding[0] + padding[1],
2707 child_size.height + padding[2] + padding[3],
2708 ));
2709 if record {
2710 if let Some(child_id) = flow_children.first() {
2711 self.layout_node_constraints(
2712 *child_id,
2713 child_constraints,
2714 LayoutPoint::new(origin.x + padding[0], origin.y + padding[2]),
2715 out,
2716 constraints_out,
2717 measure_cache,
2718 scroll_source,
2719 record,
2720 depth + 1,
2721 )?;
2722 }
2723 if !abs_children.is_empty() {
2724 let abs_constraints = BoxConstraints::loose(size.width, size.height);
2725 for child_id in abs_children {
2726 self.layout_node_constraints(
2727 child_id,
2728 abs_constraints,
2729 origin,
2730 out,
2731 constraints_out,
2732 measure_cache,
2733 scroll_source,
2734 record,
2735 depth + 1,
2736 )?;
2737 }
2738 }
2739 }
2740 content_size = child_size;
2741 size
2742 }
2743 LayoutOp::Align => {
2744 let child_constraints = BoxConstraints::loose(constraints.max_w, constraints.max_h);
2745 let mut child_size = LayoutSize::ZERO;
2746 if let Some(child_id) = flow_children.first() {
2747 child_size = self.layout_node_constraints(
2748 *child_id,
2749 child_constraints,
2750 LayoutPoint::ZERO,
2751 out,
2752 constraints_out,
2753 measure_cache,
2754 scroll_source,
2755 false,
2756 depth + 1,
2757 )?;
2758 }
2759 let size = if constraints.is_width_bounded() || constraints.is_height_bounded() {
2760 constraints.constrain(LayoutSize::new(
2761 if constraints.is_width_bounded() {
2762 constraints.max_w
2763 } else {
2764 child_size.width
2765 },
2766 if constraints.is_height_bounded() {
2767 constraints.max_h
2768 } else {
2769 child_size.height
2770 },
2771 ))
2772 } else {
2773 child_size
2774 };
2775 if let Some(child_id) = flow_children.first() {
2776 let dx = ((size.width - child_size.width) / 2.0).max(0.0);
2777 let dy = ((size.height - child_size.height) / 2.0).max(0.0);
2778 self.layout_node_constraints(
2779 *child_id,
2780 child_constraints,
2781 LayoutPoint::new(origin.x + dx, origin.y + dy),
2782 out,
2783 constraints_out,
2784 measure_cache,
2785 scroll_source,
2786 record,
2787 depth + 1,
2788 )?;
2789 }
2790 if record && !abs_children.is_empty() {
2791 let abs_constraints = BoxConstraints::loose(size.width, size.height);
2792 for child_id in abs_children {
2793 self.layout_node_constraints(
2794 child_id,
2795 abs_constraints,
2796 origin,
2797 out,
2798 constraints_out,
2799 measure_cache,
2800 scroll_source,
2801 record,
2802 depth + 1,
2803 )?;
2804 }
2805 }
2806 content_size = child_size;
2807 size
2808 }
2809 LayoutOp::ZStack => {
2810 let mut max_child = LayoutSize::ZERO;
2811 for child_id in &flow_children {
2812 let child_size = self.layout_node_constraints(
2813 *child_id,
2814 BoxConstraints::loose(constraints.max_w, constraints.max_h),
2815 LayoutPoint::ZERO,
2816 out,
2817 constraints_out,
2818 measure_cache,
2819 scroll_source,
2820 false,
2821 depth + 1,
2822 )?;
2823 max_child.width = max_child.width.max(child_size.width);
2824 max_child.height = max_child.height.max(child_size.height);
2825 }
2826 let size = if constraints.is_width_bounded() || constraints.is_height_bounded() {
2827 constraints.constrain(LayoutSize::new(
2828 if constraints.is_width_bounded() {
2829 constraints.max_w
2830 } else {
2831 max_child.width
2832 },
2833 if constraints.is_height_bounded() {
2834 constraints.max_h
2835 } else {
2836 max_child.height
2837 },
2838 ))
2839 } else {
2840 max_child
2841 };
2842 for child_id in &flow_children {
2843 let child_constraints = BoxConstraints::loose(size.width, size.height);
2844 let child_origin = LayoutPoint::new(origin.x, origin.y);
2845 self.layout_node_constraints(
2846 *child_id,
2847 child_constraints,
2848 child_origin,
2849 out,
2850 constraints_out,
2851 measure_cache,
2852 scroll_source,
2853 record,
2854 depth + 1,
2855 )?;
2856 }
2857 if record && !abs_children.is_empty() {
2858 let abs_constraints = BoxConstraints::loose(size.width, size.height);
2859 for child_id in abs_children {
2860 self.layout_node_constraints(
2861 child_id,
2862 abs_constraints,
2863 origin,
2864 out,
2865 constraints_out,
2866 measure_cache,
2867 scroll_source,
2868 record,
2869 depth + 1,
2870 )?;
2871 }
2872 }
2873 content_size = size;
2874 size
2875 }
2876 LayoutOp::Positioned {
2877 top,
2878 left,
2879 bottom,
2880 right,
2881 width,
2882 height,
2883 } => {
2884 let target_w = finite_or(constraints.max_w, finite_or(constraints.min_w, 0.0));
2885 let target_h = finite_or(constraints.max_h, finite_or(constraints.min_h, 0.0));
2886 let size = constraints.constrain(LayoutSize::new(target_w, target_h));
2887 let mut child_constraints = BoxConstraints::loose(size.width, size.height);
2888 if let (Some(l), Some(r)) = (left, right) {
2889 let w = (size.width - l - r).max(0.0);
2890 child_constraints = child_constraints.tighten(Some(w), None);
2891 }
2892 if let (Some(t), Some(b)) = (top, bottom) {
2893 let h = (size.height - t - b).max(0.0);
2894 child_constraints = child_constraints.tighten(None, Some(h));
2895 }
2896 child_constraints = child_constraints.tighten(*width, *height);
2897 if let Some(child_id) = node.children_ids.first() {
2898 let child_size = self.layout_node_constraints(
2899 *child_id,
2900 child_constraints,
2901 LayoutPoint::ZERO,
2902 out,
2903 constraints_out,
2904 measure_cache,
2905 scroll_source,
2906 false,
2907 depth + 1,
2908 )?;
2909 let x = left.unwrap_or_else(|| {
2910 right
2911 .map(|r| (size.width - r - child_size.width).max(0.0))
2912 .unwrap_or(0.0)
2913 });
2914 let y = top.unwrap_or_else(|| {
2915 bottom
2916 .map(|b| (size.height - b - child_size.height).max(0.0))
2917 .unwrap_or(0.0)
2918 });
2919 self.layout_node_constraints(
2920 *child_id,
2921 child_constraints,
2922 LayoutPoint::new(origin.x + x, origin.y + y),
2923 out,
2924 constraints_out,
2925 measure_cache,
2926 scroll_source,
2927 record,
2928 depth + 1,
2929 )?;
2930 }
2931 content_size = size;
2932 size
2933 }
2934 LayoutOp::Embed { width, height, .. } => {
2935 let local = constraints.tighten(*width, *height);
2936 let w = if local.is_width_bounded() {
2937 local.max_w
2938 } else {
2939 local.min_w
2940 };
2941 let h = if local.is_height_bounded() {
2942 local.max_h
2943 } else {
2944 local.min_h
2945 };
2946 let size = local.constrain(LayoutSize::new(w, h));
2947 content_size = size;
2948 size
2949 }
2950 LayoutOp::AbsoluteFill => {
2951 let target_w = finite_or(constraints.max_w, finite_or(constraints.min_w, 0.0));
2952 let target_h = finite_or(constraints.max_h, finite_or(constraints.min_h, 0.0));
2953 let size = constraints.constrain(LayoutSize::new(target_w, target_h));
2954 for child_id in self.graph_state.children_of(node_id) {
2955 self.layout_node_constraints(
2956 *child_id,
2957 BoxConstraints::tight(size),
2958 origin,
2959 out,
2960 constraints_out,
2961 measure_cache,
2962 scroll_source,
2963 record,
2964 depth + 1,
2965 )?;
2966 }
2967 content_size = size;
2968 size
2969 }
2970 LayoutOp::Transform { .. } | LayoutOp::Clip { .. } => {
2971 let mut child_size = LayoutSize::ZERO;
2972 if let Some(child_id) = node.children_ids.first() {
2973 child_size = self.layout_node_constraints(
2974 *child_id,
2975 constraints,
2976 origin,
2977 out,
2978 constraints_out,
2979 measure_cache,
2980 scroll_source,
2981 record,
2982 depth + 1,
2983 )?;
2984 }
2985 content_size = child_size;
2986 constraints.constrain(child_size)
2987 }
2988 LayoutOp::Flyout { anchor, content: _ } => {
2989 let loose = BoxConstraints::loose(
2990 if constraints.is_width_bounded() {
2991 constraints.max_w
2992 } else {
2993 f32::INFINITY
2994 },
2995 if constraints.is_height_bounded() {
2996 constraints.max_h
2997 } else {
2998 f32::INFINITY
2999 },
3000 );
3001 let mut child_size = LayoutSize::ZERO;
3002 for child_id in self.graph_state.children_of(node_id) {
3003 child_size = self.layout_node_constraints(
3004 *child_id,
3005 loose,
3006 origin,
3007 out,
3008 constraints_out,
3009 measure_cache,
3010 scroll_source,
3011 false,
3012 depth + 1,
3013 )?;
3014 }
3015 if record {
3016 let anchor_rect = out.get(anchor).map(|g| g.rect);
3017 let place_x = anchor_rect.map(|r| r.x()).unwrap_or(origin.x);
3018 let place_y = anchor_rect.map(|r| r.y() + r.height()).unwrap_or(origin.y);
3019 for child_id in self.graph_state.children_of(node_id) {
3020 self.layout_node_constraints(
3021 *child_id,
3022 loose,
3023 LayoutPoint::new(place_x, place_y),
3024 out,
3025 constraints_out,
3026 measure_cache,
3027 scroll_source,
3028 record,
3029 depth + 1,
3030 )?;
3031 }
3032 }
3033 content_size = child_size;
3034 child_size
3035 }
3036 };
3037
3038 if let Some(runs) = &node.rich_text {
3039 if let Some(measurer) = &self.measurer {
3040 let node_max_w = match &node.op {
3041 LayoutOp::Box { max_width, .. } => *max_width,
3042 _ => None,
3043 };
3044 let avail_w = {
3045 let from_constraints = if constraints.is_width_bounded() {
3046 Some(constraints.max_w)
3047 } else {
3048 None
3049 };
3050 match (from_constraints, node_max_w) {
3051 (Some(c), Some(m)) => Some(c.min(m)),
3052 (Some(c), None) => Some(c),
3053 (None, Some(m)) => Some(m),
3054 (None, None) => None,
3055 }
3056 };
3057 let rich_layout = measurer.layout_rich_text(runs, avail_w);
3058 let text_content = LayoutSize::new(rich_layout.width, rich_layout.height);
3059 let measured = constraints.constrain(text_content);
3060 if rich_text_inline_children
3061 && rich_layout.inline_boxes.len() == flow_children.len()
3062 {
3063 let result =
3064 self.record_geometry(node_id, origin, measured, text_content, out, record);
3065 if record {
3066 let mut inline_boxes = rich_layout.inline_boxes;
3067 inline_boxes.sort_by_key(|inline_box| inline_box.id);
3068 for (child_id, inline_box) in flow_children.iter().zip(inline_boxes.iter())
3069 {
3070 self.layout_node_constraints(
3071 *child_id,
3072 BoxConstraints::tight(LayoutSize::new(
3073 inline_box.width,
3074 inline_box.height,
3075 )),
3076 LayoutPoint::new(origin.x + inline_box.x, origin.y + inline_box.y),
3077 out,
3078 constraints_out,
3079 measure_cache,
3080 scroll_source,
3081 record,
3082 depth + 1,
3083 )?;
3084 }
3085 }
3086 if !record {
3087 measure_cache.insert(MeasureCacheKey::new(node_id, constraints), result);
3088 }
3089 return Ok(result);
3090 }
3091 if node.children_ids.is_empty() {
3092 let result =
3093 self.record_geometry(node_id, origin, measured, text_content, out, record);
3094 if !record {
3095 measure_cache.insert(MeasureCacheKey::new(node_id, constraints), result);
3096 }
3097 return Ok(result);
3098 }
3099 content_size.width = content_size.width.max(text_content.width);
3100 content_size.height = content_size.height.max(text_content.height);
3101 }
3102 }
3103
3104 let result = self.record_geometry(node_id, origin, size, content_size, out, record);
3105 if !record {
3106 measure_cache.insert(MeasureCacheKey::new(node_id, constraints), result);
3107 }
3108 Ok(result)
3109 }
3110
3111 fn record_geometry(
3112 &self,
3113 node_id: WidgetId,
3114 origin: LayoutPoint,
3115 size: LayoutSize,
3116 content_size: LayoutSize,
3117 out: &mut HashMap<WidgetId, LayoutNodeGeometry>,
3118 record: bool,
3119 ) -> LayoutSize {
3120 let mut rect_origin = origin;
3121 let mut rect_size = size;
3122 let mut rect_content = content_size;
3123 let mut had_non_finite = false;
3124
3125 if !rect_origin.x.is_finite() {
3126 rect_origin.x = 0.0;
3127 had_non_finite = true;
3128 }
3129 if !rect_origin.y.is_finite() {
3130 rect_origin.y = 0.0;
3131 had_non_finite = true;
3132 }
3133 if !rect_size.width.is_finite() {
3134 rect_size.width = 0.0;
3135 had_non_finite = true;
3136 }
3137 if !rect_size.height.is_finite() {
3138 rect_size.height = 0.0;
3139 had_non_finite = true;
3140 }
3141 if !rect_content.width.is_finite() {
3142 rect_content.width = 0.0;
3143 had_non_finite = true;
3144 }
3145 if !rect_content.height.is_finite() {
3146 rect_content.height = 0.0;
3147 had_non_finite = true;
3148 }
3149
3150 if had_non_finite {
3151 diag::emit(
3152 diag::DiagCategory::Invariants,
3153 diag::DiagLevel::Error,
3154 diag::DiagEventKind::InvariantViolation {
3155 kind: "non_finite_layout".into(),
3156 node: Some(node_id.as_u128()),
3157 details: format!(
3158 "origin=({:.2},{:.2}) size=({:.2},{:.2}) content=({:.2},{:.2})",
3159 origin.x,
3160 origin.y,
3161 size.width,
3162 size.height,
3163 content_size.width,
3164 content_size.height
3165 ),
3166 dump_ref: None,
3167 },
3168 );
3169 }
3170
3171 if record {
3172 let rect = LayoutRect::new(
3173 rect_origin.x,
3174 rect_origin.y,
3175 rect_size.width,
3176 rect_size.height,
3177 );
3178 out.insert(
3179 node_id,
3180 LayoutNodeGeometry {
3181 rect,
3182 content_size: rect_content,
3183 },
3184 );
3185 }
3186 rect_size
3187 }
3188}