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::{
598 LayoutEngine, LayoutGraphState, LayoutInputNode, TextMeasurer,
599 DEFAULT_RICH_TEXT_HIT_TEST_FONT_SIZE,
600 };
601 use fission_ir::op::{Color, FontStyle, TextRun, TextStyle};
602 use fission_ir::{LayoutOp, WidgetId};
603 use std::sync::atomic::{AtomicU32, Ordering};
604
605 fn box_node(
606 id: WidgetId,
607 parent_id: Option<WidgetId>,
608 children_ids: Vec<WidgetId>,
609 ) -> LayoutInputNode {
610 LayoutInputNode {
611 id,
612 parent_id,
613 op: LayoutOp::Box {
614 width: Some(40.0),
615 height: Some(20.0),
616 min_width: None,
617 max_width: None,
618 min_height: None,
619 max_height: None,
620 padding: [0.0; 4],
621 flex_grow: 0.0,
622 flex_shrink: 0.0,
623 aspect_ratio: None,
624 },
625 children_ids,
626 debug_name: format!("node-{}", id.as_u128()),
627 width: Some(40.0),
628 height: Some(20.0),
629 flex_grow: 0.0,
630 flex_shrink: 0.0,
631 rich_text: None,
632 }
633 }
634
635 struct RecordingMeasurer {
636 last_font_size_bits: AtomicU32,
637 }
638
639 impl RecordingMeasurer {
640 fn new() -> Self {
641 Self {
642 last_font_size_bits: AtomicU32::new(f32::NAN.to_bits()),
643 }
644 }
645
646 fn last_font_size(&self) -> f32 {
647 f32::from_bits(self.last_font_size_bits.load(Ordering::SeqCst))
648 }
649 }
650
651 impl TextMeasurer for RecordingMeasurer {
652 fn measure(
653 &self,
654 _text: &str,
655 _font_size: f32,
656 _available_width: Option<f32>,
657 ) -> (f32, f32) {
658 (0.0, 0.0)
659 }
660
661 fn hit_test(
662 &self,
663 _text: &str,
664 font_size: f32,
665 _available_width: Option<f32>,
666 _x: f32,
667 _y: f32,
668 ) -> usize {
669 self.last_font_size_bits
670 .store(font_size.to_bits(), Ordering::SeqCst);
671 0
672 }
673 }
674
675 #[test]
676 fn matches_input_nodes_rejects_reordered_flattened_inputs() {
677 let root = WidgetId::from_u128(1);
678 let first = WidgetId::from_u128(2);
679 let second = WidgetId::from_u128(3);
680 let canonical = vec![
681 box_node(root, None, vec![first, second]),
682 box_node(first, Some(root), vec![]),
683 box_node(second, Some(root), vec![]),
684 ];
685 let reordered = vec![
686 box_node(root, None, vec![first, second]),
687 box_node(second, Some(root), vec![]),
688 box_node(first, Some(root), vec![]),
689 ];
690
691 let state = LayoutGraphState::from_input_nodes(&canonical, 1);
692 assert!(!state.matches_input_nodes(&reordered));
693 }
694
695 #[test]
696 fn update_refreshes_node_order_for_reordered_flattened_inputs() {
697 let root = WidgetId::from_u128(10);
698 let first = WidgetId::from_u128(11);
699 let second = WidgetId::from_u128(12);
700 let canonical = vec![
701 box_node(root, None, vec![first, second]),
702 box_node(first, Some(root), vec![]),
703 box_node(second, Some(root), vec![]),
704 ];
705 let reordered = vec![
706 box_node(root, None, vec![first, second]),
707 box_node(second, Some(root), vec![]),
708 box_node(first, Some(root), vec![]),
709 ];
710
711 let mut engine = LayoutEngine::new();
712 engine.update(&canonical);
713 engine.update(&reordered);
714
715 let ordered = engine
716 .graph_state
717 .ordered_nodes()
718 .map(|node| node.id)
719 .collect::<Vec<_>>();
720 assert_eq!(ordered, vec![root, second, first]);
721 }
722
723 #[test]
724 fn rich_text_hit_test_uses_body_font_size_when_runs_are_empty() {
725 let measurer = RecordingMeasurer::new();
726
727 measurer.hit_test_rich(&[], None, 4.0, 2.0);
728
729 assert_eq!(
730 measurer.last_font_size(),
731 DEFAULT_RICH_TEXT_HIT_TEST_FONT_SIZE
732 );
733 }
734
735 #[test]
736 fn rich_text_hit_test_uses_first_run_font_size_when_present() {
737 let measurer = RecordingMeasurer::new();
738 let runs = vec![TextRun {
739 text: "Hello".to_string(),
740 style: TextStyle {
741 font_size: 18.0,
742 color: Color::BLACK,
743 underline: false,
744 font_family: None,
745 locale: None,
746 font_weight: 400,
747 font_style: FontStyle::Normal,
748 line_height: None,
749 letter_spacing: 0.0,
750 background_color: None,
751 },
752 }];
753
754 measurer.hit_test_rich(&runs, None, 4.0, 2.0);
755
756 assert_eq!(measurer.last_font_size(), 18.0);
757 }
758}
759
760fn layout_input_fingerprint(node: &LayoutInputNode) -> u64 {
761 let mut hasher = DefaultHasher::new();
762 format!("{node:?}").hash(&mut hasher);
763 hasher.finish()
764}
765
766#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
781pub struct LayoutRect {
782 pub origin: LayoutPoint,
784 pub size: LayoutSize,
786}
787
788impl LayoutRect {
789 pub fn new(x: LayoutUnit, y: LayoutUnit, width: LayoutUnit, height: LayoutUnit) -> Self {
791 Self {
792 origin: LayoutPoint { x, y },
793 size: LayoutSize { width, height },
794 }
795 }
796
797 pub fn x(&self) -> LayoutUnit {
799 self.origin.x
800 }
801 pub fn y(&self) -> LayoutUnit {
803 self.origin.y
804 }
805 pub fn width(&self) -> LayoutUnit {
807 self.size.width
808 }
809 pub fn height(&self) -> LayoutUnit {
811 self.size.height
812 }
813
814 pub fn right(&self) -> LayoutUnit {
816 self.origin.x + self.size.width
817 }
818 pub fn bottom(&self) -> LayoutUnit {
820 self.origin.y + self.size.height
821 }
822
823 pub fn contains(&self, p: LayoutPoint) -> bool {
826 p.x >= self.x() && p.x < self.right() && p.y >= self.y() && p.y < self.bottom()
827 }
828}
829
830#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
836pub struct LayoutNodeGeometry {
837 pub rect: LayoutRect,
839 pub content_size: LayoutSize,
842}
843
844#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
860pub struct LayoutSnapshot {
861 pub nodes: HashMap<WidgetId, LayoutNodeGeometry>,
863 #[serde(skip)]
866 pub constraints: HashMap<WidgetId, BoxConstraints>,
867 pub viewport_size: LayoutSize,
869}
870
871impl LayoutSnapshot {
872 pub fn new(viewport_size: LayoutSize) -> Self {
874 Self {
875 nodes: HashMap::new(),
876 constraints: HashMap::new(),
877 viewport_size,
878 }
879 }
880
881 pub fn get_node_geometry(&self, node_id: WidgetId) -> Option<&LayoutNodeGeometry> {
884 self.nodes.get(&node_id)
885 }
886
887 pub fn get_node_rect(&self, node_id: WidgetId) -> Option<LayoutRect> {
889 self.nodes.get(&node_id).map(|g| g.rect)
890 }
891
892 pub fn get_node_constraints(&self, node_id: WidgetId) -> Option<BoxConstraints> {
895 self.constraints.get(&node_id).copied()
896 }
897}
898
899#[derive(Debug, Clone)]
908pub struct LayoutInputNode {
909 pub id: WidgetId,
911 pub parent_id: Option<WidgetId>,
913 pub op: LayoutOp,
915 pub children_ids: Vec<WidgetId>,
917 pub debug_name: String,
919 pub width: Option<LayoutUnit>,
921 pub height: Option<LayoutUnit>,
923 pub flex_grow: LayoutUnit,
925 pub flex_shrink: LayoutUnit,
927 pub rich_text: Option<Vec<TextRun>>,
930}
931
932pub struct LineMetric {
938 pub start_index: usize,
940 pub end_index: usize,
942 pub baseline: f32,
944 pub height: f32,
946 pub width: f32,
948}
949
950#[derive(Debug, Clone, Copy, PartialEq)]
951pub struct RichTextInlineBox {
952 pub id: u64,
953 pub x: f32,
954 pub y: f32,
955 pub width: f32,
956 pub height: f32,
957}
958
959#[derive(Debug, Clone, PartialEq)]
960pub struct RichTextLayoutInfo {
961 pub width: f32,
962 pub height: f32,
963 pub inline_boxes: Vec<RichTextInlineBox>,
964}
965
966const DEFAULT_RICH_TEXT_HIT_TEST_FONT_SIZE: f32 = 14.0;
986
987pub trait TextMeasurer: Send + Sync {
988 fn measure(&self, text: &str, font_size: f32, available_width: Option<f32>) -> (f32, f32);
993
994 fn hit_test(
999 &self,
1000 _text: &str,
1001 _font_size: f32,
1002 _available_width: Option<f32>,
1003 _x: f32,
1004 _y: f32,
1005 ) -> usize {
1006 0
1007 }
1008
1009 fn get_line_metrics(
1014 &self,
1015 _text: &str,
1016 _font_size: f32,
1017 _available_width: Option<f32>,
1018 ) -> Vec<LineMetric> {
1019 vec![]
1020 }
1021
1022 fn get_caret_position(
1027 &self,
1028 _text: &str,
1029 _font_size: f32,
1030 _available_width: Option<f32>,
1031 _caret_index: usize,
1032 ) -> (f32, f32) {
1033 (0.0, 0.0)
1034 }
1035
1036 fn measure_rich_text(&self, _runs: &[TextRun], _available_width: Option<f32>) -> (f32, f32) {
1040 (0.0, 0.0)
1041 }
1042
1043 fn layout_rich_text(
1049 &self,
1050 runs: &[TextRun],
1051 available_width: Option<f32>,
1052 ) -> RichTextLayoutInfo {
1053 let (width, height) = if runs.len() == 1 {
1054 let run = &runs[0];
1055 self.measure(&run.text, run.style.font_size, available_width)
1056 } else {
1057 self.measure_rich_text(runs, available_width)
1058 };
1059 RichTextLayoutInfo {
1060 width,
1061 height,
1062 inline_boxes: Vec::new(),
1063 }
1064 }
1065
1066 fn hit_test_rich(
1070 &self,
1071 runs: &[TextRun],
1072 _available_width: Option<f32>,
1073 x: f32,
1074 y: f32,
1075 ) -> usize {
1076 let text: String = runs.iter().map(|r| r.text.as_str()).collect();
1079 let font_size = runs
1080 .first()
1081 .map(|r| r.style.font_size)
1082 .unwrap_or(DEFAULT_RICH_TEXT_HIT_TEST_FONT_SIZE);
1083 self.hit_test(&text, font_size, None, x, y)
1084 }
1085
1086 fn resolve_rich_text_annotation_at_point(
1091 &self,
1092 _runs: &[TextRun],
1093 _available_width: Option<f32>,
1094 _x: f32,
1095 _y: f32,
1096 _paragraph_style: TextParagraphStyle,
1097 _annotations: &[RichTextAnnotation],
1098 ) -> Option<RichTextAnnotation> {
1099 None
1100 }
1101}
1102
1103pub struct LayoutEngine {
1126 measurer: Option<Arc<dyn TextMeasurer>>,
1127 graph_state: LayoutGraphState,
1128 next_graph_version: u64,
1129 incremental_reuse: Option<IncrementalLayoutReuseState>,
1130}
1131
1132impl LayoutEngine {
1133 const MAX_LAYOUT_RECURSION_DEPTH: usize = 100;
1134
1135 pub fn new() -> Self {
1140 Self {
1141 measurer: None,
1142 graph_state: LayoutGraphState::default(),
1143 next_graph_version: 1,
1144 incremental_reuse: None,
1145 }
1146 }
1147
1148 pub fn with_measurer(mut self, measurer: Arc<dyn TextMeasurer>) -> Self {
1152 self.measurer = Some(measurer);
1153 self
1154 }
1155
1156 fn allocate_graph_version(&mut self) -> u64 {
1157 let version = self.next_graph_version;
1158 self.next_graph_version = self.next_graph_version.saturating_add(1);
1159 version
1160 }
1161
1162 fn refresh_graph_state(&mut self, input_nodes: &[LayoutInputNode]) {
1163 let version = self.allocate_graph_version();
1164 self.graph_state = LayoutGraphState::from_input_nodes(input_nodes, version);
1165 }
1166
1167 fn ensure_graph_state(&mut self, input_nodes: &[LayoutInputNode]) {
1168 if self.graph_state.is_empty() || !self.graph_state.matches_input_nodes(input_nodes) {
1169 self.refresh_graph_state(input_nodes);
1170 }
1171 }
1172
1173 fn validate_graph_state(&self, root: WidgetId) -> Result<()> {
1174 if let Some(err) = self.graph_state.validation.first_error() {
1175 return Err(err);
1176 }
1177 if !self.graph_state.nodes.contains_key(&root) {
1178 anyhow::bail!("[verify] missing node {:?}", root);
1179 }
1180 if !self.graph_state.roots.contains(&root)
1181 && self
1182 .graph_state
1183 .parents
1184 .get(&root)
1185 .copied()
1186 .flatten()
1187 .is_some()
1188 {
1189 anyhow::bail!("[verify] root {:?} is not a graph root", root);
1190 }
1191 if let Some(last_layout_version) = self.graph_state.last_layout_version {
1192 if last_layout_version > self.graph_state.graph_version {
1193 anyhow::bail!(
1194 "[verify] cached layout version {} exceeds graph version {}",
1195 last_layout_version,
1196 self.graph_state.graph_version
1197 );
1198 }
1199 }
1200 Ok(())
1201 }
1202
1203 pub fn update(&mut self, input_nodes: &[LayoutInputNode]) {
1208 if self.graph_state.is_empty() {
1209 self.refresh_graph_state(input_nodes);
1210 return;
1211 }
1212
1213 if self.graph_state.matches_input_nodes(input_nodes) {
1214 return;
1215 }
1216
1217 let version = self.allocate_graph_version();
1218 self.graph_state.graph_version = version;
1219 self.graph_state.update_nodes(input_nodes);
1220 }
1221
1222 pub fn rebuild(&mut self, input_nodes: &[LayoutInputNode]) -> Result<()> {
1224 self.refresh_graph_state(input_nodes);
1225 if let Some(err) = self.graph_state.validation.first_error() {
1226 return Err(err);
1227 }
1228 Ok(())
1229 }
1230
1231 pub fn verify_post_update(
1236 &self,
1237 input_nodes: &[LayoutInputNode],
1238 root: WidgetId,
1239 ) -> Result<()> {
1240 if self.graph_state.matches_input_nodes(input_nodes) {
1241 return self.validate_graph_state(root);
1242 }
1243
1244 let node_map: HashMap<WidgetId, &LayoutInputNode> =
1245 input_nodes.iter().map(|n| (n.id, n)).collect();
1246 for n in input_nodes {
1248 for child in &n.children_ids {
1249 let child_node = node_map
1250 .get(child)
1251 .ok_or_else(|| anyhow::anyhow!("[verify] child {:?} not found", child))?;
1252 if child_node.parent_id != Some(n.id) {
1253 anyhow::bail!("[verify] parent/child mismatch parent={:?} child={:?} child.parent_id={:?}", n.id, child, child_node.parent_id);
1254 }
1255 }
1256 }
1257 fn dfs(
1259 id: WidgetId,
1260 map: &HashMap<WidgetId, &LayoutInputNode>,
1261 visited: &mut HashSet<WidgetId>,
1262 stack: &mut HashSet<WidgetId>,
1263 ) -> Result<()> {
1264 if !visited.insert(id) {
1265 return Ok(());
1266 }
1267 stack.insert(id);
1268 let node = map
1269 .get(&id)
1270 .ok_or_else(|| anyhow::anyhow!("[verify] missing node {:?}", id))?;
1271 for child in &node.children_ids {
1272 if stack.contains(child) {
1273 anyhow::bail!("[verify] cycle detected at {:?} -> {:?}", id, child);
1274 }
1275 dfs(*child, map, visited, stack)?;
1276 }
1277 stack.remove(&id);
1278 Ok(())
1279 }
1280 let mut visited = HashSet::new();
1281 let mut stack = HashSet::new();
1282 dfs(root, &node_map, &mut visited, &mut stack)?;
1283 Ok(())
1284 }
1285
1286 pub fn compute_layout(
1304 &mut self,
1305 input_nodes: &[LayoutInputNode],
1306 root_node_id: WidgetId,
1307 viewport_size: LayoutSize,
1308 scroll_source: &impl ScrollDataSource,
1309 ) -> Result<LayoutSnapshot> {
1310 self.ensure_graph_state(input_nodes);
1311 self.validate_graph_state(root_node_id)?;
1312 let snapshot = self.compute_layout_constraints(
1313 input_nodes,
1314 root_node_id,
1315 viewport_size,
1316 scroll_source,
1317 )?;
1318 self.emit_scroll_diagnostics(&snapshot);
1319 Ok(snapshot)
1320 }
1321
1322 pub fn compute_layout_constraints(
1327 &mut self,
1328 input_nodes: &[LayoutInputNode],
1329 root_node_id: WidgetId,
1330 viewport_size: LayoutSize,
1331 scroll_source: &impl ScrollDataSource,
1332 ) -> Result<LayoutSnapshot> {
1333 self.ensure_graph_state(input_nodes);
1334 self.validate_graph_state(root_node_id)?;
1335
1336 let mut constraints = BoxConstraints::tight(viewport_size);
1338 if let Some(root) = self.graph_state.node(root_node_id) {
1339 if root.width.is_some() || root.height.is_some() {
1341 constraints = BoxConstraints::loose(viewport_size.width, viewport_size.height)
1342 .tighten(root.width, root.height);
1343 }
1344 }
1345
1346 let mut snapshot = LayoutSnapshot::new(viewport_size);
1347 let mut measure_cache = HashMap::new();
1348 self.layout_node_constraints(
1349 root_node_id,
1350 constraints,
1351 LayoutPoint::ZERO,
1352 &mut snapshot.nodes,
1353 &mut snapshot.constraints,
1354 &mut measure_cache,
1355 scroll_source,
1356 true,
1357 0,
1358 )?;
1359
1360 let visual_location = |node_id: WidgetId| -> Option<LayoutPoint> {
1361 let mut pos = snapshot.nodes.get(&node_id)?.rect.origin;
1362 let mut current = self.graph_state.parent_of(node_id);
1363 while let Some(parent_id) = current {
1364 if let Some(parent) = self.graph_state.node(parent_id) {
1365 if let LayoutOp::Scroll { direction, .. } = &parent.op {
1366 let offset = scroll_source.get_offset(parent_id);
1367 match direction {
1368 FlexDirection::Row => pos.x -= offset,
1369 FlexDirection::Column => pos.y -= offset,
1370 }
1371 }
1372 current = self.graph_state.parent_of(parent_id);
1373 } else {
1374 break;
1375 }
1376 }
1377 Some(pos)
1378 };
1379
1380 let mut flyout_abs_overrides: HashMap<WidgetId, (f32, f32)> = HashMap::new();
1381 for node in self.graph_state.ordered_nodes() {
1382 if let LayoutOp::Flyout { anchor, content } = node.op {
1383 if let (Some(anchor_geom), Some(content_geom)) =
1384 (snapshot.nodes.get(&anchor), snapshot.nodes.get(&content))
1385 {
1386 if let Some(anchor_abs) = visual_location(anchor) {
1387 let content_w = content_geom.rect.width();
1388 let content_h = content_geom.rect.height();
1389 let anchor_h = anchor_geom.rect.height();
1390 let max_left = (snapshot.viewport_size.width - content_w).max(0.0);
1391 let left_rel = anchor_abs.x.clamp(0.0, max_left);
1392
1393 let below_top = anchor_abs.y + anchor_h;
1394 let max_top = (snapshot.viewport_size.height - content_h).max(0.0);
1395 let top_rel = if below_top + content_h <= snapshot.viewport_size.height {
1396 below_top
1397 } else {
1398 let above_top = anchor_abs.y - content_h;
1399 if above_top >= 0.0 {
1400 above_top
1401 } else {
1402 below_top.clamp(0.0, max_top)
1403 }
1404 };
1405 flyout_abs_overrides.insert(content, (left_rel, top_rel));
1406 }
1407 }
1408 }
1409 }
1410
1411 if !flyout_abs_overrides.is_empty() {
1412 for (nid, (abs_x, abs_y)) in flyout_abs_overrides {
1413 if let Some(current) = snapshot.nodes.get(&nid) {
1414 let dx = abs_x - current.rect.origin.x;
1415 let dy = abs_y - current.rect.origin.y;
1416 let mut stack = vec![(nid, 0usize)];
1417 while let Some((current_id, depth)) = stack.pop() {
1418 if depth > Self::MAX_LAYOUT_RECURSION_DEPTH {
1419 return Err(self.layout_depth_overflow(current_id, depth));
1420 }
1421 if let Some(geometry) = snapshot.nodes.get_mut(¤t_id) {
1422 geometry.rect.origin.x += dx;
1423 geometry.rect.origin.y += dy;
1424 }
1425 for child_id in self.graph_state.children_of(current_id).iter().rev() {
1426 stack.push((*child_id, depth + 1));
1427 }
1428 }
1429 }
1430 }
1431 }
1432
1433 self.graph_state.mark_layout_complete();
1434 self.incremental_reuse = None;
1435
1436 Ok(snapshot)
1437 }
1438
1439 pub fn compute_layout_incremental(
1440 &mut self,
1441 input_nodes: &[LayoutInputNode],
1442 root_node_id: WidgetId,
1443 viewport_size: LayoutSize,
1444 scroll_source: &impl ScrollDataSource,
1445 previous_snapshot: &LayoutSnapshot,
1446 dirty_nodes: &HashSet<WidgetId>,
1447 ) -> Result<LayoutSnapshot> {
1448 self.ensure_graph_state(input_nodes);
1449 self.validate_graph_state(root_node_id)?;
1450
1451 let mut dirty_ancestors = HashSet::new();
1452 for node_id in dirty_nodes {
1453 let mut current = Some(*node_id);
1454 while let Some(id) = current {
1455 if !dirty_ancestors.insert(id) {
1456 break;
1457 }
1458 current = self.graph_state.parent_of(id);
1459 }
1460 }
1461 dirty_ancestors.insert(root_node_id);
1462
1463 self.incremental_reuse = Some(IncrementalLayoutReuseState {
1464 previous_snapshot: previous_snapshot.clone(),
1465 dirty_ancestors,
1466 });
1467 let result = self.compute_layout_constraints(
1468 input_nodes,
1469 root_node_id,
1470 viewport_size,
1471 scroll_source,
1472 );
1473 self.incremental_reuse = None;
1474 result
1475 }
1476
1477 fn emit_scroll_diagnostics(&self, snapshot: &LayoutSnapshot) {
1478 use fission_diagnostics::prelude as diag;
1479 let trace_scroll = std::env::var("FISSION_SCROLL_TRACE").ok().as_deref() == Some("1");
1480 for n in self.graph_state.ordered_nodes() {
1481 if let LayoutOp::Scroll { .. } = n.op {
1482 if let Some(g) = snapshot.nodes.get(&n.id) {
1483 let note = if g.rect.height() <= 0.0 {
1484 let parent_op = n
1485 .parent_id
1486 .and_then(|pid| self.graph_state.node(pid))
1487 .map(|p| format!("{:?}", p.op));
1488 let parent_constraints = n
1489 .parent_id
1490 .and_then(|pid| snapshot.constraints.get(&pid))
1491 .copied();
1492 snapshot
1493 .constraints
1494 .get(&n.id)
1495 .map(|c| {
1496 format!(
1497 "op={:?} parent={:?} parent_op={:?} parent_constraints={:?} constraints={:?}",
1498 n.op,
1499 n.parent_id,
1500 parent_op,
1501 parent_constraints,
1502 c
1503 )
1504 })
1505 } else {
1506 None
1507 };
1508 diag::emit(
1509 diag::DiagCategory::Layout,
1510 diag::DiagLevel::Debug,
1511 diag::DiagEventKind::ScrollExtent {
1512 node: n.id.as_u128(),
1513 viewport_w: g.rect.width(),
1514 viewport_h: g.rect.height(),
1515 content_w: g.content_size.width,
1516 content_h: g.content_size.height,
1517 note,
1518 },
1519 );
1520 if trace_scroll {
1521 eprintln!(
1522 "[scroll-trace] node={} viewport=({:.1},{:.1}) content=({:.1},{:.1})",
1523 n.id.as_u128(),
1524 g.rect.width(),
1525 g.rect.height(),
1526 g.content_size.width,
1527 g.content_size.height
1528 );
1529 }
1530 }
1531 }
1532 }
1533 }
1534
1535 fn layout_depth_overflow(&self, node_id: WidgetId, depth: usize) -> anyhow::Error {
1536 let details = format!(
1537 "layout recursion depth {} exceeded max {} at node {}",
1538 depth,
1539 Self::MAX_LAYOUT_RECURSION_DEPTH,
1540 node_id.as_u128()
1541 );
1542 diag::emit(
1543 diag::DiagCategory::Invariants,
1544 diag::DiagLevel::Error,
1545 diag::DiagEventKind::InvariantViolation {
1546 kind: "layout_recursion_depth".into(),
1547 node: Some(node_id.as_u128()),
1548 details: details.clone(),
1549 dump_ref: None,
1550 },
1551 );
1552 anyhow::anyhow!(details)
1553 }
1554
1555 fn copy_cached_subtree(
1556 &self,
1557 node_id: WidgetId,
1558 origin: LayoutPoint,
1559 current_constraints: BoxConstraints,
1560 out: &mut HashMap<WidgetId, LayoutNodeGeometry>,
1561 constraints_out: &mut HashMap<WidgetId, BoxConstraints>,
1562 ) -> Result<Option<LayoutSize>> {
1563 let Some(reuse) = self.incremental_reuse.as_ref() else {
1564 return Ok(None);
1565 };
1566 if reuse.dirty_ancestors.contains(&node_id) {
1567 return Ok(None);
1568 }
1569
1570 let Some(previous_geometry) = reuse.previous_snapshot.nodes.get(&node_id) else {
1571 return Ok(None);
1572 };
1573 let Some(previous_constraints) = reuse.previous_snapshot.constraints.get(&node_id).copied()
1574 else {
1575 return Ok(None);
1576 };
1577 if previous_constraints != current_constraints {
1578 return Ok(None);
1579 }
1580
1581 let dx = origin.x - previous_geometry.rect.origin.x;
1582 let dy = origin.y - previous_geometry.rect.origin.y;
1583 let mut stack = vec![(node_id, 0usize)];
1584 while let Some((current_id, depth)) = stack.pop() {
1585 if depth > Self::MAX_LAYOUT_RECURSION_DEPTH {
1586 return Err(self.layout_depth_overflow(current_id, depth));
1587 }
1588 let Some(previous_geometry) = reuse.previous_snapshot.nodes.get(¤t_id) else {
1589 return Ok(None);
1590 };
1591 let Some(previous_constraints) = reuse
1592 .previous_snapshot
1593 .constraints
1594 .get(¤t_id)
1595 .copied()
1596 else {
1597 return Ok(None);
1598 };
1599
1600 let mut geometry = previous_geometry.clone();
1601 geometry.rect.origin.x += dx;
1602 geometry.rect.origin.y += dy;
1603 out.insert(current_id, geometry);
1604 constraints_out.insert(current_id, previous_constraints);
1605
1606 let children = self.graph_state.children_of(current_id);
1607 for child_id in children.iter().rev() {
1608 stack.push((*child_id, depth + 1));
1609 }
1610 }
1611
1612 Ok(Some(previous_geometry.content_size))
1613 }
1614
1615 fn layout_node_constraints(
1616 &self,
1617 node_id: WidgetId,
1618 constraints: BoxConstraints,
1619 origin: LayoutPoint,
1620 out: &mut HashMap<WidgetId, LayoutNodeGeometry>,
1621 constraints_out: &mut HashMap<WidgetId, BoxConstraints>,
1622 measure_cache: &mut HashMap<MeasureCacheKey, LayoutSize>,
1623 scroll_source: &impl ScrollDataSource,
1624 record: bool,
1625 depth: usize,
1626 ) -> Result<LayoutSize> {
1627 if depth > Self::MAX_LAYOUT_RECURSION_DEPTH {
1628 return Err(self.layout_depth_overflow(node_id, depth));
1629 }
1630 if !record {
1631 let cache_key = MeasureCacheKey::new(node_id, constraints);
1632 if let Some(cached) = measure_cache.get(&cache_key).copied() {
1633 return Ok(cached);
1634 }
1635 }
1636 let node = match self.graph_state.node(node_id) {
1637 Some(node) => node,
1638 None => return Ok(LayoutSize::ZERO),
1639 };
1640
1641 if record {
1642 constraints_out.insert(node_id, constraints);
1643 }
1644
1645 if record {
1646 if let Some(reused) =
1647 self.copy_cached_subtree(node_id, origin, constraints, out, constraints_out)?
1648 {
1649 return Ok(reused);
1650 }
1651 }
1652
1653 let mut flow_children: Vec<WidgetId> = Vec::new();
1654 let mut abs_children: Vec<WidgetId> = Vec::new();
1655 for child_id in self.graph_state.children_of(node_id) {
1656 let is_absolute = matches!(
1657 self.graph_state.node(*child_id).map(|n| &n.op),
1658 Some(LayoutOp::AbsoluteFill) | Some(LayoutOp::Positioned { .. })
1659 );
1660 if is_absolute {
1661 abs_children.push(*child_id);
1662 } else {
1663 flow_children.push(*child_id);
1664 }
1665 }
1666 let rich_text_inline_children = node.rich_text.is_some() && !flow_children.is_empty();
1667
1668 let mut content_size;
1669 let size = match &node.op {
1670 LayoutOp::Box {
1671 width,
1672 height,
1673 min_width,
1674 max_width,
1675 min_height,
1676 max_height,
1677 padding,
1678 aspect_ratio,
1679 ..
1680 } => {
1681 let mut local =
1682 constraints.apply_min_max(*min_width, *max_width, *min_height, *max_height);
1683 local = local.tighten(*width, *height);
1684 if let Some(ratio) = aspect_ratio.filter(|r| *r > 0.0) {
1685 let mut target_w = *width;
1686 let mut target_h = *height;
1687
1688 if target_w.is_some() && target_h.is_none() {
1689 target_h = target_w.map(|w| w / ratio);
1690 } else if target_h.is_some() && target_w.is_none() {
1691 target_w = target_h.map(|h| h * ratio);
1692 } else if target_w.is_none() && target_h.is_none() {
1693 if local.is_width_bounded() || local.is_height_bounded() {
1694 let (mut w, mut h) = if local.is_width_bounded() {
1695 let w = local.max_w;
1696 let h = w / ratio;
1697 (w, h)
1698 } else {
1699 let h = local.max_h;
1700 let w = h * ratio;
1701 (w, h)
1702 };
1703 if local.is_width_bounded()
1704 && local.is_height_bounded()
1705 && h > local.max_h
1706 {
1707 h = local.max_h;
1708 w = h * ratio;
1709 }
1710 target_w = Some(w);
1711 target_h = Some(h);
1712 }
1713 }
1714
1715 if target_w.is_some() || target_h.is_some() {
1716 local = local.tighten(target_w, target_h);
1717 }
1718 }
1719 let base_child_constraints = local.deflate(*padding);
1720 let mut max_child = LayoutSize::ZERO;
1721 let mut measured_children: Vec<(WidgetId, BoxConstraints, LayoutSize)> = Vec::new();
1722 if !rich_text_inline_children {
1723 for child_id in &flow_children {
1724 let (child_width, child_height, child_max_width, child_max_height) = self
1725 .graph_state
1726 .node(*child_id)
1727 .map(|child| match &child.op {
1728 LayoutOp::Box {
1729 width,
1730 height,
1731 max_width,
1732 max_height,
1733 ..
1734 } => (*width, *height, *max_width, *max_height),
1735 LayoutOp::Scroll {
1736 width,
1737 height,
1738 max_width,
1739 max_height,
1740 ..
1741 } => (*width, *height, *max_width, *max_height),
1742 LayoutOp::Embed { width, height, .. } => {
1743 (*width, *height, None, None)
1744 }
1745 _ => (None, None, None, None),
1746 })
1747 .unwrap_or((None, None, None, None));
1748 let mut child_constraints = base_child_constraints;
1749 let tight_width = child_constraints.min_w == child_constraints.max_w;
1750 let stretch_width =
1751 tight_width && child_width.is_none() && child_max_width.is_none();
1752 if stretch_width {
1753 child_constraints.min_w = child_constraints.max_w;
1754 } else if tight_width
1755 && (child_width.is_some() || child_max_width.is_some())
1756 {
1757 child_constraints.min_w = 0.0;
1758 }
1759 let tight_height = child_constraints.min_h == child_constraints.max_h;
1760 let stretch_height =
1761 tight_height && child_height.is_none() && child_max_height.is_none();
1762 if stretch_height {
1763 child_constraints.min_h = child_constraints.max_h;
1764 } else if tight_height
1765 && (child_height.is_some() || child_max_height.is_some())
1766 {
1767 child_constraints.min_h = 0.0;
1768 }
1769 let child_size = self.layout_node_constraints(
1770 *child_id,
1771 child_constraints,
1772 LayoutPoint::ZERO,
1773 out,
1774 constraints_out,
1775 measure_cache,
1776 scroll_source,
1777 false,
1778 depth + 1,
1779 )?;
1780 max_child.width = max_child.width.max(child_size.width);
1781 max_child.height = max_child.height.max(child_size.height);
1782 measured_children.push((*child_id, child_constraints, child_size));
1783 }
1784 }
1785 let padded = LayoutSize::new(
1786 max_child.width + padding[0] + padding[1],
1787 max_child.height + padding[2] + padding[3],
1788 );
1789 let size = local.constrain(padded);
1790 if record {
1791 for (child_id, child_constraints, _child_size) in measured_children {
1792 self.layout_node_constraints(
1793 child_id,
1794 child_constraints,
1795 LayoutPoint::new(origin.x + padding[0], origin.y + padding[2]),
1796 out,
1797 constraints_out,
1798 measure_cache,
1799 scroll_source,
1800 record,
1801 depth + 1,
1802 )?;
1803 }
1804 if !abs_children.is_empty() {
1805 let abs_constraints = BoxConstraints::loose(size.width, size.height);
1806 for child_id in abs_children {
1807 self.layout_node_constraints(
1808 child_id,
1809 abs_constraints,
1810 origin,
1811 out,
1812 constraints_out,
1813 measure_cache,
1814 scroll_source,
1815 record,
1816 depth + 1,
1817 )?;
1818 }
1819 }
1820 }
1821 content_size = padded;
1822 size
1823 }
1824 LayoutOp::Flex {
1825 direction,
1826 wrap,
1827 padding,
1828 gap,
1829 align_items,
1830 justify_content,
1831 flex_grow,
1832 ..
1833 } => {
1834 let gap = gap.unwrap_or(0.0);
1835 let local = constraints.tighten(node.width, node.height);
1836 let inner = local.deflate(*padding);
1837 let is_row = matches!(direction, IrFlexDirection::Row);
1838
1839 let max_main = if is_row { inner.max_w } else { inner.max_h };
1840 let max_cross = if is_row { inner.max_h } else { inner.max_w };
1841 let min_main = if is_row { inner.min_w } else { inner.min_h };
1842 let min_cross = if is_row { inner.min_h } else { inner.min_w };
1843 let main_bounded = if is_row {
1844 inner.is_width_bounded()
1845 } else {
1846 inner.is_height_bounded()
1847 };
1848 let cross_bounded = if is_row {
1849 inner.is_height_bounded()
1850 } else {
1851 inner.is_width_bounded()
1852 };
1853
1854 if matches!(wrap, IrFlexWrap::Wrap | IrFlexWrap::WrapReverse) {
1855 let mut lines: Vec<(Vec<(WidgetId, LayoutSize, BoxConstraints)>, f32, f32)> =
1856 Vec::new();
1857 let mut line_children: Vec<(WidgetId, LayoutSize, BoxConstraints)> = Vec::new();
1858 let mut line_main = 0.0f32;
1859 let mut line_cross = 0.0f32;
1860 let mut max_line_main = 0.0f32;
1861
1862 for child_id in &flow_children {
1863 let child_constraints = if is_row {
1864 BoxConstraints {
1865 min_w: 0.0,
1866 max_w: max_main,
1867 min_h: 0.0,
1868 max_h: max_cross,
1869 }
1870 } else {
1871 BoxConstraints {
1872 min_w: 0.0,
1873 max_w: max_cross,
1874 min_h: 0.0,
1875 max_h: max_main,
1876 }
1877 };
1878 let child_size = self.layout_node_constraints(
1879 *child_id,
1880 child_constraints,
1881 LayoutPoint::ZERO,
1882 out,
1883 constraints_out,
1884 measure_cache,
1885 scroll_source,
1886 false,
1887 depth + 1,
1888 )?;
1889 let child_main = if is_row {
1890 child_size.width
1891 } else {
1892 child_size.height
1893 };
1894 let child_cross = if is_row {
1895 child_size.height
1896 } else {
1897 child_size.width
1898 };
1899 let next_main = if line_children.is_empty() {
1900 child_main
1901 } else {
1902 line_main + gap + child_main
1903 };
1904
1905 if main_bounded && !line_children.is_empty() && next_main > max_main {
1906 max_line_main = max_line_main.max(line_main);
1907 lines.push((line_children, line_main, line_cross));
1908 line_children = Vec::new();
1909 line_main = 0.0;
1910 line_cross = 0.0;
1911 }
1912
1913 if !line_children.is_empty() {
1914 line_main += gap;
1915 }
1916 line_main += child_main;
1917 line_cross = line_cross.max(child_cross);
1918 line_children.push((*child_id, child_size, child_constraints));
1919 }
1920
1921 if !line_children.is_empty() {
1922 max_line_main = max_line_main.max(line_main);
1923 lines.push((line_children, line_main, line_cross));
1924 }
1925
1926 let mut container_main = if main_bounded && *flex_grow > 0.0 {
1927 max_main
1928 } else {
1929 max_line_main
1930 };
1931 container_main = container_main.max(min_main);
1932 let total_lines_cross: f32 =
1933 lines.iter().map(|(_, _, cross)| *cross).sum::<f32>()
1934 + gap * lines.len().saturating_sub(1) as f32;
1935 let container_cross = total_lines_cross.max(min_cross);
1936 let size = if is_row {
1937 local.constrain(LayoutSize::new(
1938 container_main + padding[0] + padding[1],
1939 container_cross + padding[2] + padding[3],
1940 ))
1941 } else {
1942 local.constrain(LayoutSize::new(
1943 container_cross + padding[0] + padding[1],
1944 container_main + padding[2] + padding[3],
1945 ))
1946 };
1947
1948 let inner_main = if is_row {
1949 size.width - padding[0] - padding[1]
1950 } else {
1951 size.height - padding[2] - padding[3]
1952 };
1953 let inner_cross = if is_row {
1954 size.height - padding[2] - padding[3]
1955 } else {
1956 size.width - padding[0] - padding[1]
1957 };
1958
1959 let mut ordered_lines = lines;
1960 if matches!(wrap, IrFlexWrap::WrapReverse) {
1961 ordered_lines.reverse();
1962 }
1963
1964 let mut line_cursor = if matches!(wrap, IrFlexWrap::WrapReverse) {
1965 (inner_cross - total_lines_cross).max(0.0)
1966 } else {
1967 0.0
1968 };
1969
1970 for (line_children, line_main, line_cross) in ordered_lines {
1971 let remaining_space = (inner_main - line_main).max(0.0);
1972 let mut extra_gap = 0.0;
1973 let mut offset_main = 0.0;
1974 match justify_content {
1975 fission_ir::op::JustifyContent::Start => {}
1976 fission_ir::op::JustifyContent::End => offset_main = remaining_space,
1977 fission_ir::op::JustifyContent::Center => {
1978 offset_main = remaining_space / 2.0
1979 }
1980 fission_ir::op::JustifyContent::SpaceBetween => {
1981 if line_children.len() > 1 {
1982 extra_gap =
1983 remaining_space / (line_children.len() as f32 - 1.0);
1984 }
1985 }
1986 fission_ir::op::JustifyContent::SpaceAround => {
1987 if !line_children.is_empty() {
1988 extra_gap = remaining_space / line_children.len() as f32;
1989 offset_main = extra_gap / 2.0;
1990 }
1991 }
1992 fission_ir::op::JustifyContent::SpaceEvenly => {
1993 if !line_children.is_empty() {
1994 extra_gap =
1995 remaining_space / (line_children.len() as f32 + 1.0);
1996 offset_main = extra_gap;
1997 }
1998 }
1999 }
2000
2001 let mut cursor = offset_main;
2002 for (child_id, child_size, mut child_constraints) in line_children {
2003 let child_main = if is_row {
2004 child_size.width
2005 } else {
2006 child_size.height
2007 };
2008 let child_cross = if is_row {
2009 child_size.height
2010 } else {
2011 child_size.width
2012 };
2013 if matches!(align_items, fission_ir::op::AlignItems::Stretch) {
2014 if is_row {
2015 child_constraints.min_h = line_cross;
2016 child_constraints.max_h = line_cross;
2017 } else {
2018 child_constraints.min_w = line_cross;
2019 child_constraints.max_w = line_cross;
2020 }
2021 }
2022 let cross_offset = match align_items {
2023 fission_ir::op::AlignItems::Start
2024 | fission_ir::op::AlignItems::Stretch => 0.0,
2025 fission_ir::op::AlignItems::End => {
2026 (line_cross - child_cross).max(0.0)
2027 }
2028 fission_ir::op::AlignItems::Center => {
2029 ((line_cross - child_cross) / 2.0).max(0.0)
2030 }
2031 fission_ir::op::AlignItems::Baseline => 0.0,
2032 };
2033 let child_origin = if is_row {
2034 LayoutPoint::new(
2035 origin.x + padding[0] + cursor,
2036 origin.y + padding[2] + line_cursor + cross_offset,
2037 )
2038 } else {
2039 LayoutPoint::new(
2040 origin.x + padding[0] + line_cursor + cross_offset,
2041 origin.y + padding[2] + cursor,
2042 )
2043 };
2044 self.layout_node_constraints(
2045 child_id,
2046 child_constraints,
2047 child_origin,
2048 out,
2049 constraints_out,
2050 measure_cache,
2051 scroll_source,
2052 record,
2053 depth + 1,
2054 )?;
2055 cursor += child_main + gap + extra_gap;
2056 }
2057
2058 line_cursor += line_cross + gap;
2059 }
2060
2061 if record && !abs_children.is_empty() {
2062 let abs_constraints = BoxConstraints::loose(size.width, size.height);
2063 for child_id in abs_children {
2064 self.layout_node_constraints(
2065 child_id,
2066 abs_constraints,
2067 origin,
2068 out,
2069 constraints_out,
2070 measure_cache,
2071 scroll_source,
2072 record,
2073 depth + 1,
2074 )?;
2075 }
2076 }
2077 content_size = size;
2078 size
2079 } else {
2080 struct FlexChildEntry {
2081 id: WidgetId,
2082 flex: f32,
2083 size: LayoutSize,
2084 constraints: BoxConstraints,
2085 is_flex: bool,
2086 }
2087 let mut measured: Vec<FlexChildEntry> = Vec::new();
2088 let mut total_flex = 0.0f32;
2089 let mut nonflex_main = 0.0f32;
2090 let mut max_child_cross = 0.0f32;
2091 let treat_flex_as_nonflex = !main_bounded;
2092
2093 for child_id in &flow_children {
2094 let child = match self.graph_state.node(*child_id) {
2095 Some(child) => child,
2096 None => continue,
2097 };
2098 let flex = child.flex_grow;
2099 if flex > 0.0 && !treat_flex_as_nonflex {
2100 total_flex += flex;
2101 measured.push(FlexChildEntry {
2102 id: *child_id,
2103 flex,
2104 size: LayoutSize::ZERO,
2105 constraints: BoxConstraints::loose(0.0, 0.0),
2106 is_flex: true,
2107 });
2108 continue;
2109 }
2110 let child_constraints = if is_row {
2111 let cross =
2112 if matches!(align_items, fission_ir::op::AlignItems::Stretch)
2113 && cross_bounded
2114 {
2115 BoxConstraints {
2116 min_w: 0.0,
2117 max_w: f32::INFINITY,
2118 min_h: max_cross,
2119 max_h: max_cross,
2120 }
2121 } else {
2122 BoxConstraints {
2123 min_w: 0.0,
2124 max_w: f32::INFINITY,
2125 min_h: 0.0,
2126 max_h: max_cross,
2127 }
2128 };
2129 cross
2130 } else {
2131 let cross =
2132 if matches!(align_items, fission_ir::op::AlignItems::Stretch)
2133 && cross_bounded
2134 {
2135 BoxConstraints {
2136 min_w: max_cross,
2137 max_w: max_cross,
2138 min_h: 0.0,
2139 max_h: f32::INFINITY,
2140 }
2141 } else {
2142 BoxConstraints {
2143 min_w: 0.0,
2144 max_w: max_cross,
2145 min_h: 0.0,
2146 max_h: f32::INFINITY,
2147 }
2148 };
2149 cross
2150 };
2151 let child_size = self.layout_node_constraints(
2152 *child_id,
2153 child_constraints,
2154 LayoutPoint::ZERO,
2155 out,
2156 constraints_out,
2157 measure_cache,
2158 scroll_source,
2159 false,
2160 depth + 1,
2161 )?;
2162 let child_main = if is_row {
2163 child_size.width
2164 } else {
2165 child_size.height
2166 };
2167 let child_cross = if is_row {
2168 child_size.height
2169 } else {
2170 child_size.width
2171 };
2172 nonflex_main += child_main;
2173 max_child_cross = max_child_cross.max(child_cross);
2174 measured.push(FlexChildEntry {
2175 id: *child_id,
2176 flex,
2177 size: child_size,
2178 constraints: child_constraints,
2179 is_flex: false,
2180 });
2181 }
2182
2183 let gap_total = gap * flow_children.len().saturating_sub(1) as f32;
2184 let remaining = if main_bounded {
2185 (max_main - nonflex_main - gap_total).max(0.0)
2186 } else {
2187 0.0
2188 };
2189
2190 for entry in measured.iter_mut().filter(|e| e.is_flex) {
2191 let flex = entry.flex;
2192 let allocated = if main_bounded && total_flex > 0.0 {
2193 remaining * (flex / total_flex)
2194 } else {
2195 0.0
2196 };
2197 let child_constraints = if is_row {
2198 let cross =
2199 if matches!(align_items, fission_ir::op::AlignItems::Stretch)
2200 && cross_bounded
2201 {
2202 BoxConstraints {
2203 min_w: allocated,
2204 max_w: allocated,
2205 min_h: max_cross,
2206 max_h: max_cross,
2207 }
2208 } else {
2209 BoxConstraints {
2210 min_w: allocated,
2211 max_w: allocated,
2212 min_h: 0.0,
2213 max_h: max_cross,
2214 }
2215 };
2216 cross
2217 } else {
2218 let cross =
2219 if matches!(align_items, fission_ir::op::AlignItems::Stretch)
2220 && cross_bounded
2221 {
2222 BoxConstraints {
2223 min_w: max_cross,
2224 max_w: max_cross,
2225 min_h: allocated,
2226 max_h: allocated,
2227 }
2228 } else {
2229 BoxConstraints {
2230 min_w: 0.0,
2231 max_w: max_cross,
2232 min_h: allocated,
2233 max_h: allocated,
2234 }
2235 };
2236 cross
2237 };
2238 let child_size = self.layout_node_constraints(
2239 entry.id,
2240 child_constraints,
2241 LayoutPoint::ZERO,
2242 out,
2243 constraints_out,
2244 measure_cache,
2245 scroll_source,
2246 false,
2247 depth + 1,
2248 )?;
2249 let child_cross = if is_row {
2250 child_size.height
2251 } else {
2252 child_size.width
2253 };
2254 max_child_cross = max_child_cross.max(child_cross);
2255 entry.size = child_size;
2256 entry.constraints = child_constraints;
2257 }
2258
2259 let final_children_main: f32 = measured
2260 .iter()
2261 .map(|entry| {
2262 if is_row {
2263 entry.size.width
2264 } else {
2265 entry.size.height
2266 }
2267 })
2268 .sum();
2269
2270 let mut container_main = if main_bounded && *flex_grow > 0.0 {
2271 max_main
2272 } else {
2273 final_children_main + gap_total
2274 };
2275 container_main = container_main.max(min_main);
2276
2277 if main_bounded && final_children_main + gap_total > max_main {
2278 let mut total_shrink_scaled = 0.0f32;
2280 for entry in &measured {
2281 let Some(child) = self.graph_state.node(entry.id) else {
2282 continue;
2283 };
2284 let main_size = if is_row {
2285 entry.size.width
2286 } else {
2287 entry.size.height
2288 };
2289 total_shrink_scaled += main_size * child.flex_shrink;
2290 }
2291
2292 if total_shrink_scaled > 0.0 {
2293 let overflow = (final_children_main + gap_total) - max_main;
2294 for entry in &mut measured {
2295 let Some(child) = self.graph_state.node(entry.id) else {
2296 continue;
2297 };
2298 let main_size = if is_row {
2299 entry.size.width
2300 } else {
2301 entry.size.height
2302 };
2303 let shrink_amount = (main_size * child.flex_shrink
2304 / total_shrink_scaled)
2305 * overflow;
2306 let floor = if child.flex_shrink > 0.0 {
2310 let explicit_min = match &child.op {
2312 LayoutOp::Box {
2313 min_width,
2314 min_height,
2315 height,
2316 width,
2317 ..
2318 } => {
2319 if is_row {
2320 min_width.or(*width).unwrap_or(0.0)
2321 } else {
2322 min_height.or(*height).unwrap_or(0.0)
2323 }
2324 }
2325 _ => 0.0,
2326 };
2327 explicit_min
2328 } else {
2329 main_size };
2331 let new_main = (main_size - shrink_amount).max(floor);
2332
2333 let mut child_constraints = entry.constraints;
2334 if is_row {
2335 child_constraints.min_w = new_main;
2336 child_constraints.max_w = new_main;
2337 } else {
2338 child_constraints.min_h = new_main;
2339 child_constraints.max_h = new_main;
2340 }
2341 let new_size = self.layout_node_constraints(
2342 entry.id,
2343 child_constraints,
2344 LayoutPoint::ZERO,
2345 out,
2346 constraints_out,
2347 measure_cache,
2348 scroll_source,
2349 false,
2350 depth + 1,
2351 )?;
2352 entry.size = new_size;
2353 entry.constraints = child_constraints;
2354 }
2355 }
2356 }
2357
2358 let container_cross = max_child_cross.max(min_cross);
2359 let size = if is_row {
2360 local.constrain(LayoutSize::new(
2361 container_main + padding[0] + padding[1],
2362 container_cross + padding[2] + padding[3],
2363 ))
2364 } else {
2365 local.constrain(LayoutSize::new(
2366 container_cross + padding[0] + padding[1],
2367 container_main + padding[2] + padding[3],
2368 ))
2369 };
2370
2371 let inner_main = if is_row {
2372 size.width - padding[0] - padding[1]
2373 } else {
2374 size.height - padding[2] - padding[3]
2375 };
2376 let inner_cross = if is_row {
2377 size.height - padding[2] - padding[3]
2378 } else {
2379 size.width - padding[0] - padding[1]
2380 };
2381
2382 let final_children_main: f32 = measured
2383 .iter()
2384 .map(|entry| {
2385 if is_row {
2386 entry.size.width
2387 } else {
2388 entry.size.height
2389 }
2390 })
2391 .sum();
2392
2393 let remaining_space = (inner_main - final_children_main - gap_total).max(0.0);
2394 let mut extra_gap = 0.0;
2395 let mut offset_main = 0.0;
2396 match justify_content {
2397 fission_ir::op::JustifyContent::Start => {}
2398 fission_ir::op::JustifyContent::End => offset_main = remaining_space,
2399 fission_ir::op::JustifyContent::Center => {
2400 offset_main = remaining_space / 2.0
2401 }
2402 fission_ir::op::JustifyContent::SpaceBetween => {
2403 if measured.len() > 1 {
2404 extra_gap = remaining_space / (measured.len() as f32 - 1.0);
2405 }
2406 }
2407 fission_ir::op::JustifyContent::SpaceAround => {
2408 if !measured.is_empty() {
2409 extra_gap = remaining_space / measured.len() as f32;
2410 offset_main = extra_gap / 2.0;
2411 }
2412 }
2413 fission_ir::op::JustifyContent::SpaceEvenly => {
2414 if !measured.is_empty() {
2415 extra_gap = remaining_space / (measured.len() as f32 + 1.0);
2416 offset_main = extra_gap;
2417 }
2418 }
2419 }
2420
2421 let mut cursor = offset_main;
2422 for entry in measured {
2423 let child_main = if is_row {
2424 entry.size.width
2425 } else {
2426 entry.size.height
2427 };
2428 let child_cross = if is_row {
2429 entry.size.height
2430 } else {
2431 entry.size.width
2432 };
2433 let cross_offset = match align_items {
2434 fission_ir::op::AlignItems::Start
2435 | fission_ir::op::AlignItems::Stretch => 0.0,
2436 fission_ir::op::AlignItems::End => (inner_cross - child_cross).max(0.0),
2437 fission_ir::op::AlignItems::Center => {
2438 ((inner_cross - child_cross) / 2.0).max(0.0)
2439 }
2440 fission_ir::op::AlignItems::Baseline => 0.0,
2441 };
2442 let child_origin = if is_row {
2443 LayoutPoint::new(
2444 origin.x + padding[0] + cursor,
2445 origin.y + padding[2] + cross_offset,
2446 )
2447 } else {
2448 LayoutPoint::new(
2449 origin.x + padding[0] + cross_offset,
2450 origin.y + padding[2] + cursor,
2451 )
2452 };
2453
2454 let mut child_constraints = entry.constraints;
2455 if matches!(align_items, fission_ir::op::AlignItems::Stretch) {
2456 let child_node = self.graph_state.node(entry.id);
2458 let has_explicit_cross = child_node
2459 .map(|n| match &n.op {
2460 LayoutOp::Box { width, height, .. } => {
2461 if is_row {
2462 height.is_some()
2463 } else {
2464 width.is_some()
2465 }
2466 }
2467 _ => false,
2468 })
2469 .unwrap_or(false);
2470 if !has_explicit_cross {
2471 if is_row {
2472 child_constraints.min_h = inner_cross;
2473 child_constraints.max_h = inner_cross;
2474 } else {
2475 child_constraints.min_w = inner_cross;
2476 child_constraints.max_w = inner_cross;
2477 }
2478 }
2479 }
2480
2481 self.layout_node_constraints(
2482 entry.id,
2483 child_constraints,
2484 child_origin,
2485 out,
2486 constraints_out,
2487 measure_cache,
2488 scroll_source,
2489 record,
2490 depth + 1,
2491 )?;
2492 cursor += child_main + gap + extra_gap;
2493 }
2494
2495 if record && !abs_children.is_empty() {
2496 let abs_constraints = BoxConstraints::loose(size.width, size.height);
2497 for child_id in abs_children {
2498 self.layout_node_constraints(
2499 child_id,
2500 abs_constraints,
2501 origin,
2502 out,
2503 constraints_out,
2504 measure_cache,
2505 scroll_source,
2506 record,
2507 depth + 1,
2508 )?;
2509 }
2510 }
2511 content_size = size;
2512 size
2513 }
2514 }
2515 LayoutOp::Grid {
2516 columns,
2517 rows,
2518 column_gap,
2519 row_gap,
2520 padding,
2521 } => {
2522 let gap_x = column_gap.unwrap_or(0.0);
2523 let gap_y = row_gap.unwrap_or(0.0);
2524 let inner = constraints.deflate(*padding);
2525 let bounded_w = inner.is_width_bounded();
2526 let bounded_h = inner.is_height_bounded();
2527 let available_w = if bounded_w { inner.max_w } else { 0.0 };
2528 let available_h = if bounded_h { inner.max_h } else { 0.0 };
2529
2530 let col_count = columns.len().max(1);
2531 let mut col_widths = vec![0.0f32; col_count];
2532 let mut fr_total = 0.0f32;
2533 let mut fixed_total = 0.0f32;
2534 for (i, track) in columns.iter().enumerate() {
2535 match track {
2536 GridTrack::Points(p) => {
2537 col_widths[i] = *p;
2538 fixed_total += *p;
2539 }
2540 GridTrack::Percent(p) => {
2541 let w = if bounded_w {
2542 available_w * (*p / 100.0)
2543 } else {
2544 0.0
2545 };
2546 col_widths[i] = w;
2547 fixed_total += w;
2548 }
2549 GridTrack::Fr(f) => fr_total += *f,
2550 _ => {}
2551 }
2552 }
2553 if fr_total > 0.0 && bounded_w {
2554 let remaining =
2555 (available_w - fixed_total - gap_x * (col_count.saturating_sub(1) as f32))
2556 .max(0.0);
2557 for (i, track) in columns.iter().enumerate() {
2558 if let GridTrack::Fr(f) = track {
2559 col_widths[i] = remaining * (*f / fr_total);
2560 }
2561 }
2562 }
2563
2564 let child_count = flow_children.len();
2565 let row_count = if rows.is_empty() {
2566 (child_count + col_count - 1) / col_count
2567 } else {
2568 rows.len()
2569 };
2570 let mut row_heights = vec![0.0f32; row_count.max(1)];
2571
2572 if !rows.is_empty() {
2573 let mut row_fr_total = 0.0f32;
2574 let mut row_fixed_total = 0.0f32;
2575 for (i, track) in rows.iter().enumerate() {
2576 if i >= row_heights.len() {
2577 break;
2578 }
2579 match track {
2580 GridTrack::Points(p) => {
2581 row_heights[i] = *p;
2582 row_fixed_total += *p;
2583 }
2584 GridTrack::Percent(p) => {
2585 let h = if bounded_h {
2586 available_h * (*p / 100.0)
2587 } else {
2588 0.0
2589 };
2590 row_heights[i] = h;
2591 row_fixed_total += h;
2592 }
2593 GridTrack::Fr(f) => row_fr_total += *f,
2594 _ => {}
2595 }
2596 }
2597 if row_fr_total > 0.0 && bounded_h {
2598 let remaining = (available_h
2599 - row_fixed_total
2600 - gap_y * (row_heights.len().saturating_sub(1) as f32))
2601 .max(0.0);
2602 for (i, track) in rows.iter().enumerate() {
2603 if let GridTrack::Fr(f) = track {
2604 row_heights[i] = remaining * (*f / row_fr_total);
2605 }
2606 }
2607 }
2608 }
2609
2610 let mut cell_assignments = Vec::new();
2611 let mut auto_row = 0;
2612 let mut auto_col = 0;
2613
2614 for child_id in &flow_children {
2615 let Some(child) = self.graph_state.node(*child_id) else {
2616 continue;
2617 };
2618 let (row, col) = if let LayoutOp::GridItem {
2619 row_start,
2620 col_start,
2621 ..
2622 } = &child.op
2623 {
2624 let r = match row_start {
2625 fission_ir::op::GridPlacement::Line(l) => {
2626 (*l as usize).saturating_sub(1)
2627 }
2628 _ => auto_row,
2629 };
2630 let c = match col_start {
2631 fission_ir::op::GridPlacement::Line(l) => {
2632 (*l as usize).saturating_sub(1)
2633 }
2634 _ => auto_col,
2635 };
2636 (r, c)
2637 } else {
2638 let res = (auto_row, auto_col);
2639 auto_col += 1;
2640 if auto_col >= col_count {
2641 auto_col = 0;
2642 auto_row += 1;
2643 }
2644 res
2645 };
2646 cell_assignments.push((*child_id, row, col));
2647 }
2648
2649 for (child_id, row, col) in &cell_assignments {
2650 if *row >= row_heights.len() || *col >= col_widths.len() {
2651 continue;
2652 }
2653 let cell_w = col_widths[*col];
2654 let cell_constraints = BoxConstraints {
2655 min_w: cell_w,
2656 max_w: cell_w,
2657 min_h: 0.0,
2658 max_h: if row_heights[*row] > 0.0 {
2659 row_heights[*row]
2660 } else {
2661 f32::INFINITY
2662 },
2663 };
2664 let child_size = self.layout_node_constraints(
2665 *child_id,
2666 cell_constraints,
2667 LayoutPoint::ZERO,
2668 out,
2669 constraints_out,
2670 measure_cache,
2671 scroll_source,
2672 false,
2673 depth + 1,
2674 )?;
2675 if row_heights[*row] == 0.0 {
2676 row_heights[*row] = child_size.height;
2677 } else {
2678 row_heights[*row] = row_heights[*row].max(child_size.height);
2679 }
2680 }
2681
2682 let grid_w: f32 =
2683 col_widths.iter().sum::<f32>() + gap_x * (col_count.saturating_sub(1) as f32);
2684 let grid_h: f32 = row_heights.iter().sum::<f32>()
2685 + gap_y * (row_heights.len().saturating_sub(1) as f32);
2686 let size = constraints.constrain(LayoutSize::new(
2687 grid_w + padding[0] + padding[1],
2688 grid_h + padding[2] + padding[3],
2689 ));
2690
2691 if record {
2692 let padding_origin_x = origin.x + padding[0];
2693 let padding_origin_y = origin.y + padding[2];
2694 for (child_id, row, col) in &cell_assignments {
2695 if *row >= row_heights.len() || *col >= col_widths.len() {
2696 continue;
2697 }
2698 let mut cell_x = padding_origin_x;
2699 for i in 0..*col {
2700 cell_x += col_widths[i] + gap_x;
2701 }
2702 let mut cell_y = padding_origin_y;
2703 for i in 0..*row {
2704 cell_y += row_heights[i] + gap_y;
2705 }
2706 let cell_w = col_widths[*col];
2707 let cell_h = row_heights[*row];
2708 let child_constraints = BoxConstraints {
2709 min_w: cell_w,
2710 max_w: cell_w,
2711 min_h: cell_h,
2712 max_h: cell_h,
2713 };
2714 self.layout_node_constraints(
2715 *child_id,
2716 child_constraints,
2717 LayoutPoint::new(cell_x, cell_y),
2718 out,
2719 constraints_out,
2720 measure_cache,
2721 scroll_source,
2722 record,
2723 depth + 1,
2724 )?;
2725 }
2726 }
2727
2728 if record && !abs_children.is_empty() {
2729 let abs_constraints = BoxConstraints::loose(size.width, size.height);
2730 for child_id in abs_children {
2731 self.layout_node_constraints(
2732 child_id,
2733 abs_constraints,
2734 origin,
2735 out,
2736 constraints_out,
2737 measure_cache,
2738 scroll_source,
2739 record,
2740 depth + 1,
2741 )?;
2742 }
2743 }
2744 content_size = size;
2745 size
2746 }
2747 LayoutOp::GridItem { .. } => {
2748 let mut child_size = LayoutSize::ZERO;
2749 if let Some(child_id) = node.children_ids.first() {
2750 child_size = self.layout_node_constraints(
2751 *child_id,
2752 constraints,
2753 origin,
2754 out,
2755 constraints_out,
2756 measure_cache,
2757 scroll_source,
2758 record,
2759 depth + 1,
2760 )?;
2761 }
2762 content_size = child_size;
2763 constraints.constrain(child_size)
2764 }
2765 LayoutOp::Scroll {
2766 direction,
2767 width,
2768 height,
2769 min_width,
2770 max_width,
2771 min_height,
2772 max_height,
2773 padding,
2774 ..
2775 } => {
2776 let mut local =
2777 constraints.apply_min_max(*min_width, *max_width, *min_height, *max_height);
2778 local = local.tighten(*width, *height);
2779 let is_horizontal = matches!(direction, FlexDirection::Row);
2780 let mut child_constraints = local.deflate(*padding);
2781 if is_horizontal {
2782 child_constraints.min_w = 0.0;
2783 child_constraints.max_w = f32::INFINITY;
2784 } else {
2785 child_constraints.min_h = 0.0;
2786 child_constraints.max_h = f32::INFINITY;
2787 }
2788 let mut child_size = LayoutSize::ZERO;
2789 if let Some(child_id) = flow_children.first() {
2790 child_size = self.layout_node_constraints(
2791 *child_id,
2792 child_constraints,
2793 LayoutPoint::ZERO,
2794 out,
2795 constraints_out,
2796 measure_cache,
2797 scroll_source,
2798 false,
2799 depth + 1,
2800 )?;
2801 }
2802 let size = local.constrain(LayoutSize::new(
2803 child_size.width + padding[0] + padding[1],
2804 child_size.height + padding[2] + padding[3],
2805 ));
2806 if record {
2807 if let Some(child_id) = flow_children.first() {
2808 self.layout_node_constraints(
2809 *child_id,
2810 child_constraints,
2811 LayoutPoint::new(origin.x + padding[0], origin.y + padding[2]),
2812 out,
2813 constraints_out,
2814 measure_cache,
2815 scroll_source,
2816 record,
2817 depth + 1,
2818 )?;
2819 }
2820 if !abs_children.is_empty() {
2821 let abs_constraints = BoxConstraints::loose(size.width, size.height);
2822 for child_id in abs_children {
2823 self.layout_node_constraints(
2824 child_id,
2825 abs_constraints,
2826 origin,
2827 out,
2828 constraints_out,
2829 measure_cache,
2830 scroll_source,
2831 record,
2832 depth + 1,
2833 )?;
2834 }
2835 }
2836 }
2837 content_size = child_size;
2838 size
2839 }
2840 LayoutOp::Align => {
2841 let child_constraints = BoxConstraints::loose(constraints.max_w, constraints.max_h);
2842 let mut child_size = LayoutSize::ZERO;
2843 if let Some(child_id) = flow_children.first() {
2844 child_size = self.layout_node_constraints(
2845 *child_id,
2846 child_constraints,
2847 LayoutPoint::ZERO,
2848 out,
2849 constraints_out,
2850 measure_cache,
2851 scroll_source,
2852 false,
2853 depth + 1,
2854 )?;
2855 }
2856 let size = if constraints.is_width_bounded() || constraints.is_height_bounded() {
2857 constraints.constrain(LayoutSize::new(
2858 if constraints.is_width_bounded() {
2859 constraints.max_w
2860 } else {
2861 child_size.width
2862 },
2863 if constraints.is_height_bounded() {
2864 constraints.max_h
2865 } else {
2866 child_size.height
2867 },
2868 ))
2869 } else {
2870 child_size
2871 };
2872 if let Some(child_id) = flow_children.first() {
2873 let dx = ((size.width - child_size.width) / 2.0).max(0.0);
2874 let dy = ((size.height - child_size.height) / 2.0).max(0.0);
2875 self.layout_node_constraints(
2876 *child_id,
2877 child_constraints,
2878 LayoutPoint::new(origin.x + dx, origin.y + dy),
2879 out,
2880 constraints_out,
2881 measure_cache,
2882 scroll_source,
2883 record,
2884 depth + 1,
2885 )?;
2886 }
2887 if record && !abs_children.is_empty() {
2888 let abs_constraints = BoxConstraints::loose(size.width, size.height);
2889 for child_id in abs_children {
2890 self.layout_node_constraints(
2891 child_id,
2892 abs_constraints,
2893 origin,
2894 out,
2895 constraints_out,
2896 measure_cache,
2897 scroll_source,
2898 record,
2899 depth + 1,
2900 )?;
2901 }
2902 }
2903 content_size = child_size;
2904 size
2905 }
2906 LayoutOp::ZStack => {
2907 let mut max_child = LayoutSize::ZERO;
2908 for child_id in &flow_children {
2909 let child_size = self.layout_node_constraints(
2910 *child_id,
2911 BoxConstraints::loose(constraints.max_w, constraints.max_h),
2912 LayoutPoint::ZERO,
2913 out,
2914 constraints_out,
2915 measure_cache,
2916 scroll_source,
2917 false,
2918 depth + 1,
2919 )?;
2920 max_child.width = max_child.width.max(child_size.width);
2921 max_child.height = max_child.height.max(child_size.height);
2922 }
2923 let size = if constraints.is_width_bounded() || constraints.is_height_bounded() {
2924 constraints.constrain(LayoutSize::new(
2925 if constraints.is_width_bounded() {
2926 constraints.max_w
2927 } else {
2928 max_child.width
2929 },
2930 if constraints.is_height_bounded() {
2931 constraints.max_h
2932 } else {
2933 max_child.height
2934 },
2935 ))
2936 } else {
2937 max_child
2938 };
2939 for child_id in &flow_children {
2940 let child_constraints = BoxConstraints::loose(size.width, size.height);
2941 let child_origin = LayoutPoint::new(origin.x, origin.y);
2942 self.layout_node_constraints(
2943 *child_id,
2944 child_constraints,
2945 child_origin,
2946 out,
2947 constraints_out,
2948 measure_cache,
2949 scroll_source,
2950 record,
2951 depth + 1,
2952 )?;
2953 }
2954 if record && !abs_children.is_empty() {
2955 let abs_constraints = BoxConstraints::loose(size.width, size.height);
2956 for child_id in abs_children {
2957 self.layout_node_constraints(
2958 child_id,
2959 abs_constraints,
2960 origin,
2961 out,
2962 constraints_out,
2963 measure_cache,
2964 scroll_source,
2965 record,
2966 depth + 1,
2967 )?;
2968 }
2969 }
2970 content_size = size;
2971 size
2972 }
2973 LayoutOp::Positioned {
2974 top,
2975 left,
2976 bottom,
2977 right,
2978 width,
2979 height,
2980 } => {
2981 let target_w = finite_or(constraints.max_w, finite_or(constraints.min_w, 0.0));
2982 let target_h = finite_or(constraints.max_h, finite_or(constraints.min_h, 0.0));
2983 let size = constraints.constrain(LayoutSize::new(target_w, target_h));
2984 let mut child_constraints = BoxConstraints::loose(size.width, size.height);
2985 if let (Some(l), Some(r)) = (left, right) {
2986 let w = (size.width - l - r).max(0.0);
2987 child_constraints = child_constraints.tighten(Some(w), None);
2988 }
2989 if let (Some(t), Some(b)) = (top, bottom) {
2990 let h = (size.height - t - b).max(0.0);
2991 child_constraints = child_constraints.tighten(None, Some(h));
2992 }
2993 child_constraints = child_constraints.tighten(*width, *height);
2994 if let Some(child_id) = node.children_ids.first() {
2995 let child_size = self.layout_node_constraints(
2996 *child_id,
2997 child_constraints,
2998 LayoutPoint::ZERO,
2999 out,
3000 constraints_out,
3001 measure_cache,
3002 scroll_source,
3003 false,
3004 depth + 1,
3005 )?;
3006 let x = left.unwrap_or_else(|| {
3007 right
3008 .map(|r| (size.width - r - child_size.width).max(0.0))
3009 .unwrap_or(0.0)
3010 });
3011 let y = top.unwrap_or_else(|| {
3012 bottom
3013 .map(|b| (size.height - b - child_size.height).max(0.0))
3014 .unwrap_or(0.0)
3015 });
3016 self.layout_node_constraints(
3017 *child_id,
3018 child_constraints,
3019 LayoutPoint::new(origin.x + x, origin.y + y),
3020 out,
3021 constraints_out,
3022 measure_cache,
3023 scroll_source,
3024 record,
3025 depth + 1,
3026 )?;
3027 }
3028 content_size = size;
3029 size
3030 }
3031 LayoutOp::Embed { width, height, .. } => {
3032 let local = constraints.tighten(*width, *height);
3033 let w = if local.is_width_bounded() {
3034 local.max_w
3035 } else {
3036 local.min_w
3037 };
3038 let h = if local.is_height_bounded() {
3039 local.max_h
3040 } else {
3041 local.min_h
3042 };
3043 let size = local.constrain(LayoutSize::new(w, h));
3044 content_size = size;
3045 size
3046 }
3047 LayoutOp::AbsoluteFill => {
3048 let target_w = finite_or(constraints.max_w, finite_or(constraints.min_w, 0.0));
3049 let target_h = finite_or(constraints.max_h, finite_or(constraints.min_h, 0.0));
3050 let size = constraints.constrain(LayoutSize::new(target_w, target_h));
3051 for child_id in self.graph_state.children_of(node_id) {
3052 self.layout_node_constraints(
3053 *child_id,
3054 BoxConstraints::tight(size),
3055 origin,
3056 out,
3057 constraints_out,
3058 measure_cache,
3059 scroll_source,
3060 record,
3061 depth + 1,
3062 )?;
3063 }
3064 content_size = size;
3065 size
3066 }
3067 LayoutOp::Transform { .. } | LayoutOp::Clip { .. } => {
3068 let mut child_size = LayoutSize::ZERO;
3069 if let Some(child_id) = node.children_ids.first() {
3070 child_size = self.layout_node_constraints(
3071 *child_id,
3072 constraints,
3073 origin,
3074 out,
3075 constraints_out,
3076 measure_cache,
3077 scroll_source,
3078 record,
3079 depth + 1,
3080 )?;
3081 }
3082 content_size = child_size;
3083 constraints.constrain(child_size)
3084 }
3085 LayoutOp::Flyout { anchor, content: _ } => {
3086 let loose = BoxConstraints::loose(
3087 if constraints.is_width_bounded() {
3088 constraints.max_w
3089 } else {
3090 f32::INFINITY
3091 },
3092 if constraints.is_height_bounded() {
3093 constraints.max_h
3094 } else {
3095 f32::INFINITY
3096 },
3097 );
3098 let mut child_size = LayoutSize::ZERO;
3099 for child_id in self.graph_state.children_of(node_id) {
3100 child_size = self.layout_node_constraints(
3101 *child_id,
3102 loose,
3103 origin,
3104 out,
3105 constraints_out,
3106 measure_cache,
3107 scroll_source,
3108 false,
3109 depth + 1,
3110 )?;
3111 }
3112 if record {
3113 let anchor_rect = out.get(anchor).map(|g| g.rect);
3114 let place_x = anchor_rect.map(|r| r.x()).unwrap_or(origin.x);
3115 let place_y = anchor_rect.map(|r| r.y() + r.height()).unwrap_or(origin.y);
3116 for child_id in self.graph_state.children_of(node_id) {
3117 self.layout_node_constraints(
3118 *child_id,
3119 loose,
3120 LayoutPoint::new(place_x, place_y),
3121 out,
3122 constraints_out,
3123 measure_cache,
3124 scroll_source,
3125 record,
3126 depth + 1,
3127 )?;
3128 }
3129 }
3130 content_size = child_size;
3131 child_size
3132 }
3133 };
3134
3135 if let Some(runs) = &node.rich_text {
3136 if let Some(measurer) = &self.measurer {
3137 let node_max_w = match &node.op {
3138 LayoutOp::Box { max_width, .. } => *max_width,
3139 _ => None,
3140 };
3141 let avail_w = {
3142 let from_constraints = if constraints.is_width_bounded() {
3143 Some(constraints.max_w)
3144 } else {
3145 None
3146 };
3147 match (from_constraints, node_max_w) {
3148 (Some(c), Some(m)) => Some(c.min(m)),
3149 (Some(c), None) => Some(c),
3150 (None, Some(m)) => Some(m),
3151 (None, None) => None,
3152 }
3153 };
3154 let rich_layout = measurer.layout_rich_text(runs, avail_w);
3155 let text_content = LayoutSize::new(rich_layout.width, rich_layout.height);
3156 let measured = constraints.constrain(text_content);
3157 if rich_text_inline_children
3158 && rich_layout.inline_boxes.len() == flow_children.len()
3159 {
3160 let result =
3161 self.record_geometry(node_id, origin, measured, text_content, out, record);
3162 if record {
3163 let mut inline_boxes = rich_layout.inline_boxes;
3164 inline_boxes.sort_by_key(|inline_box| inline_box.id);
3165 for (child_id, inline_box) in flow_children.iter().zip(inline_boxes.iter())
3166 {
3167 self.layout_node_constraints(
3168 *child_id,
3169 BoxConstraints::tight(LayoutSize::new(
3170 inline_box.width,
3171 inline_box.height,
3172 )),
3173 LayoutPoint::new(origin.x + inline_box.x, origin.y + inline_box.y),
3174 out,
3175 constraints_out,
3176 measure_cache,
3177 scroll_source,
3178 record,
3179 depth + 1,
3180 )?;
3181 }
3182 }
3183 if !record {
3184 measure_cache.insert(MeasureCacheKey::new(node_id, constraints), result);
3185 }
3186 return Ok(result);
3187 }
3188 if node.children_ids.is_empty() {
3189 let result =
3190 self.record_geometry(node_id, origin, measured, text_content, out, record);
3191 if !record {
3192 measure_cache.insert(MeasureCacheKey::new(node_id, constraints), result);
3193 }
3194 return Ok(result);
3195 }
3196 content_size.width = content_size.width.max(text_content.width);
3197 content_size.height = content_size.height.max(text_content.height);
3198 }
3199 }
3200
3201 let result = self.record_geometry(node_id, origin, size, content_size, out, record);
3202 if !record {
3203 measure_cache.insert(MeasureCacheKey::new(node_id, constraints), result);
3204 }
3205 Ok(result)
3206 }
3207
3208 fn record_geometry(
3209 &self,
3210 node_id: WidgetId,
3211 origin: LayoutPoint,
3212 size: LayoutSize,
3213 content_size: LayoutSize,
3214 out: &mut HashMap<WidgetId, LayoutNodeGeometry>,
3215 record: bool,
3216 ) -> LayoutSize {
3217 let mut rect_origin = origin;
3218 let mut rect_size = size;
3219 let mut rect_content = content_size;
3220 let mut had_non_finite = false;
3221
3222 if !rect_origin.x.is_finite() {
3223 rect_origin.x = 0.0;
3224 had_non_finite = true;
3225 }
3226 if !rect_origin.y.is_finite() {
3227 rect_origin.y = 0.0;
3228 had_non_finite = true;
3229 }
3230 if !rect_size.width.is_finite() {
3231 rect_size.width = 0.0;
3232 had_non_finite = true;
3233 }
3234 if !rect_size.height.is_finite() {
3235 rect_size.height = 0.0;
3236 had_non_finite = true;
3237 }
3238 if !rect_content.width.is_finite() {
3239 rect_content.width = 0.0;
3240 had_non_finite = true;
3241 }
3242 if !rect_content.height.is_finite() {
3243 rect_content.height = 0.0;
3244 had_non_finite = true;
3245 }
3246
3247 if had_non_finite {
3248 diag::emit(
3249 diag::DiagCategory::Invariants,
3250 diag::DiagLevel::Error,
3251 diag::DiagEventKind::InvariantViolation {
3252 kind: "non_finite_layout".into(),
3253 node: Some(node_id.as_u128()),
3254 details: format!(
3255 "origin=({:.2},{:.2}) size=({:.2},{:.2}) content=({:.2},{:.2})",
3256 origin.x,
3257 origin.y,
3258 size.width,
3259 size.height,
3260 content_size.width,
3261 content_size.height
3262 ),
3263 dump_ref: None,
3264 },
3265 );
3266 }
3267
3268 if record {
3269 let rect = LayoutRect::new(
3270 rect_origin.x,
3271 rect_origin.y,
3272 rect_size.width,
3273 rect_size.height,
3274 );
3275 out.insert(
3276 node_id,
3277 LayoutNodeGeometry {
3278 rect,
3279 content_size: rect_content,
3280 },
3281 );
3282 }
3283 rect_size
3284 }
3285}