1use anyhow::Result;
32use fission_diagnostics::prelude as diag;
33use fission_ir::op::{BoxStyle, Length, 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, Mutex};
40
41mod paragraph;
42pub use paragraph::{
43 LineMetric, ParagraphCaretStop, ParagraphCluster, ParagraphGlyph, ParagraphSelectionBox,
44 ResolvedParagraphLayout, RichTextInlineBox, RichTextLayoutInfo,
45};
46
47mod grid_tracks;
48
49use grid_tracks::{distribute_deficit, distribute_flex, expand_tracks, IntrinsicAxis, TrackSizing};
50
51pub use fission_ir::{FlexDirection, GridPlacement, GridTrack, LayoutOp};
52
53pub trait ScrollDataSource {
70 fn get_offset(&self, node_id: WidgetId) -> f32;
72}
73
74impl<F> ScrollDataSource for F
75where
76 F: Fn(WidgetId) -> f32,
77{
78 fn get_offset(&self, node_id: WidgetId) -> f32 {
79 self(node_id)
80 }
81}
82
83pub type LayoutUnit = f32;
87
88fn finite_or(value: LayoutUnit, fallback: LayoutUnit) -> LayoutUnit {
90 if value.is_finite() {
91 value
92 } else {
93 fallback
94 }
95}
96
97fn resolve_length(
98 length: &Length,
99 reference: LayoutUnit,
100 viewport: LayoutSize,
101) -> Option<LayoutUnit> {
102 length
103 .resolve(reference, viewport.width, viewport.height)
104 .map(|value| value.max(0.0))
105}
106
107fn length_requires_measurement(length: &Length) -> bool {
108 match length {
109 Length::FitContent(_) | Length::MinContent | Length::MaxContent => true,
110 Length::Add(left, right) | Length::Subtract(left, right) => {
111 length_requires_measurement(left) || length_requires_measurement(right)
112 }
113 Length::Min(values) | Length::Max(values) => values.iter().any(length_requires_measurement),
114 Length::Clamp {
115 min,
116 preferred,
117 max,
118 } => {
119 length_requires_measurement(min)
120 || length_requires_measurement(preferred)
121 || length_requires_measurement(max)
122 }
123 Length::Points(_)
124 | Length::Percent(_)
125 | Length::ViewportWidth(_)
126 | Length::ViewportHeight(_)
127 | Length::Auto => false,
128 }
129}
130
131fn resolve_measured_length(
132 length: &Length,
133 reference: LayoutUnit,
134 viewport: LayoutSize,
135 min_content: LayoutUnit,
136 max_content: LayoutUnit,
137) -> Option<LayoutUnit> {
138 let resolved = match length {
139 Length::MinContent => min_content,
140 Length::MaxContent => max_content,
141 Length::FitContent(limit) => {
142 let limit = limit
143 .as_deref()
144 .and_then(|limit| {
145 resolve_measured_length(limit, reference, viewport, min_content, max_content)
146 })
147 .unwrap_or(reference);
148 max_content.min(min_content.max(limit))
149 }
150 Length::Add(left, right) => {
151 resolve_measured_length(left, reference, viewport, min_content, max_content)?
152 + resolve_measured_length(right, reference, viewport, min_content, max_content)?
153 }
154 Length::Subtract(left, right) => {
155 resolve_measured_length(left, reference, viewport, min_content, max_content)?
156 - resolve_measured_length(right, reference, viewport, min_content, max_content)?
157 }
158 Length::Min(values) => values
159 .iter()
160 .map(|value| {
161 resolve_measured_length(value, reference, viewport, min_content, max_content)
162 })
163 .collect::<Option<Vec<_>>>()?
164 .into_iter()
165 .reduce(LayoutUnit::min)?,
166 Length::Max(values) => values
167 .iter()
168 .map(|value| {
169 resolve_measured_length(value, reference, viewport, min_content, max_content)
170 })
171 .collect::<Option<Vec<_>>>()?
172 .into_iter()
173 .reduce(LayoutUnit::max)?,
174 Length::Clamp {
175 min,
176 preferred,
177 max,
178 } => {
179 let minimum =
180 resolve_measured_length(min, reference, viewport, min_content, max_content)?;
181 let maximum =
182 resolve_measured_length(max, reference, viewport, min_content, max_content)?;
183 resolve_measured_length(preferred, reference, viewport, min_content, max_content)?
184 .clamp(minimum.min(maximum), minimum.max(maximum))
185 }
186 Length::Auto => return None,
187 Length::Points(_)
188 | Length::Percent(_)
189 | Length::ViewportWidth(_)
190 | Length::ViewportHeight(_) => {
191 length.resolve(reference, viewport.width, viewport.height)?
192 }
193 };
194 resolved.is_finite().then_some(resolved.max(0.0))
195}
196
197fn resolve_box_style(
198 style: &BoxStyle,
199 constraints: BoxConstraints,
200 viewport: LayoutSize,
201) -> LayoutOp {
202 let horizontal_reference = constraints.max_w;
203 let vertical_reference = constraints.max_h;
204 let padding = style
205 .padding
206 .as_ref()
207 .map(|padding| {
208 [
209 resolve_length(&padding[0], horizontal_reference, viewport).unwrap_or(0.0),
210 resolve_length(&padding[1], horizontal_reference, viewport).unwrap_or(0.0),
211 resolve_length(&padding[2], vertical_reference, viewport).unwrap_or(0.0),
212 resolve_length(&padding[3], vertical_reference, viewport).unwrap_or(0.0),
213 ]
214 })
215 .unwrap_or([0.0; 4]);
216 let fit_content_limit = |length: &Option<Length>, reference| match length {
217 Some(Length::FitContent(Some(limit))) => resolve_length(limit, reference, viewport),
218 _ => None,
219 };
220 let resolved_max_width = style
221 .max_width
222 .as_ref()
223 .and_then(|value| resolve_length(value, horizontal_reference, viewport));
224 let resolved_max_height = style
225 .max_height
226 .as_ref()
227 .and_then(|value| resolve_length(value, vertical_reference, viewport));
228 LayoutOp::Box {
229 width: style.width.as_ref().and_then(|value| {
230 (!matches!(value, Length::FitContent(_)))
231 .then(|| resolve_length(value, horizontal_reference, viewport))
232 .flatten()
233 }),
234 height: style.height.as_ref().and_then(|value| {
235 (!matches!(value, Length::FitContent(_)))
236 .then(|| resolve_length(value, vertical_reference, viewport))
237 .flatten()
238 }),
239 min_width: style
240 .min_width
241 .as_ref()
242 .and_then(|value| resolve_length(value, horizontal_reference, viewport)),
243 max_width: match (
244 resolved_max_width,
245 fit_content_limit(&style.width, horizontal_reference),
246 ) {
247 (Some(maximum), Some(fit)) => Some(maximum.min(fit)),
248 (maximum, fit) => maximum.or(fit),
249 },
250 min_height: style
251 .min_height
252 .as_ref()
253 .and_then(|value| resolve_length(value, vertical_reference, viewport)),
254 max_height: match (
255 resolved_max_height,
256 fit_content_limit(&style.height, vertical_reference),
257 ) {
258 (Some(maximum), Some(fit)) => Some(maximum.min(fit)),
259 (maximum, fit) => maximum.or(fit),
260 },
261 padding,
262 flex_grow: style.flex_grow.map(|value| value.0).unwrap_or(0.0),
263 flex_shrink: style.flex_shrink.map(|value| value.0).unwrap_or(1.0),
264 aspect_ratio: style.aspect_ratio.map(|value| value.0),
265 }
266}
267
268#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
273pub struct LayoutPoint {
274 pub x: LayoutUnit,
276 pub y: LayoutUnit,
278}
279
280impl LayoutPoint {
281 pub const ZERO: Self = Self { x: 0.0, y: 0.0 };
283
284 pub fn new(x: LayoutUnit, y: LayoutUnit) -> Self {
286 Self { x, y }
287 }
288}
289
290#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
295pub struct LayoutSize {
296 pub width: LayoutUnit,
298 pub height: LayoutUnit,
300}
301
302impl LayoutSize {
303 pub const ZERO: Self = Self {
305 width: 0.0,
306 height: 0.0,
307 };
308
309 pub fn new(width: LayoutUnit, height: LayoutUnit) -> Self {
311 Self { width, height }
312 }
313}
314
315#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
341pub struct BoxConstraints {
342 pub min_w: LayoutUnit,
344 pub max_w: LayoutUnit,
346 pub min_h: LayoutUnit,
348 pub max_h: LayoutUnit,
350}
351
352impl BoxConstraints {
353 pub fn tight(size: LayoutSize) -> Self {
357 Self {
358 min_w: size.width,
359 max_w: size.width,
360 min_h: size.height,
361 max_h: size.height,
362 }
363 }
364
365 pub fn loose(max_w: LayoutUnit, max_h: LayoutUnit) -> Self {
369 Self {
370 min_w: 0.0,
371 max_w,
372 min_h: 0.0,
373 max_h,
374 }
375 }
376
377 pub fn is_width_bounded(&self) -> bool {
379 self.max_w.is_finite()
380 }
381
382 pub fn is_height_bounded(&self) -> bool {
384 self.max_h.is_finite()
385 }
386
387 pub fn constrain(&self, size: LayoutSize) -> LayoutSize {
392 LayoutSize {
393 width: size.width.max(self.min_w).min(self.max_w),
394 height: size.height.max(self.min_h).min(self.max_h),
395 }
396 }
397
398 pub fn smallest(&self) -> LayoutSize {
400 LayoutSize::new(self.min_w, self.min_h)
401 }
402
403 pub fn deflate(&self, padding: [LayoutUnit; 4]) -> Self {
409 let horiz = padding[0] + padding[1];
410 let vert = padding[2] + padding[3];
411 let max_w = (self.max_w - horiz).max(0.0);
412 let max_h = (self.max_h - vert).max(0.0);
413 let min_w = (self.min_w - horiz).max(0.0).min(max_w);
414 let min_h = (self.min_h - vert).max(0.0).min(max_h);
415 Self {
416 min_w,
417 max_w,
418 min_h,
419 max_h,
420 }
421 }
422
423 pub fn tighten(&self, width: Option<LayoutUnit>, height: Option<LayoutUnit>) -> Self {
428 let mut out = *self;
429 if let Some(w) = width {
430 let clamped = w.min(out.max_w).max(out.min_w);
431 out.min_w = clamped;
432 out.max_w = clamped;
433 }
434 if let Some(h) = height {
435 let clamped = h.min(out.max_h).max(out.min_h);
436 out.min_h = clamped;
437 out.max_h = clamped;
438 }
439 if out.max_w < out.min_w {
440 out.max_w = out.min_w;
441 }
442 if out.max_h < out.min_h {
443 out.max_h = out.min_h;
444 }
445 out
446 }
447
448 pub fn apply_min_max(
454 &self,
455 min_w: Option<LayoutUnit>,
456 max_w: Option<LayoutUnit>,
457 min_h: Option<LayoutUnit>,
458 max_h: Option<LayoutUnit>,
459 ) -> Self {
460 let mut out = *self;
461 if let Some(w) = min_w {
462 out.min_w = out.min_w.max(w);
463 }
464 if let Some(h) = min_h {
465 out.min_h = out.min_h.max(h);
466 }
467 if let Some(w) = max_w {
468 out.max_w = out.max_w.min(w);
469 }
470 if let Some(h) = max_h {
471 out.max_h = out.max_h.min(h);
472 }
473 if out.max_w < out.min_w {
474 out.max_w = out.min_w;
475 }
476 if out.max_h < out.min_h {
477 out.max_h = out.min_h;
478 }
479 out
480 }
481
482 pub fn loosen(&self) -> Self {
487 Self {
488 min_w: 0.0,
489 max_w: self.max_w,
490 min_h: 0.0,
491 max_h: self.max_h,
492 }
493 }
494}
495
496#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
497struct MeasureCacheKey {
498 node_id: u128,
499 min_w: u32,
500 max_w: u32,
501 min_h: u32,
502 max_h: u32,
503}
504
505impl MeasureCacheKey {
506 fn new(node_id: WidgetId, constraints: BoxConstraints) -> Self {
507 Self {
508 node_id: node_id.as_u128(),
509 min_w: constraints.min_w.to_bits(),
510 max_w: constraints.max_w.to_bits(),
511 min_h: constraints.min_h.to_bits(),
512 max_h: constraints.max_h.to_bits(),
513 }
514 }
515}
516
517#[derive(Debug, Clone, Default)]
518struct LayoutGraphValidationState {
519 duplicate_nodes: Vec<WidgetId>,
520 missing_parent_refs: Vec<(WidgetId, WidgetId)>,
521 missing_child_refs: Vec<(WidgetId, WidgetId)>,
522 parent_child_mismatches: Vec<(WidgetId, WidgetId, Option<WidgetId>)>,
523 cycle_nodes: Vec<WidgetId>,
524 root_nodes: Vec<WidgetId>,
525}
526
527impl LayoutGraphValidationState {
528 fn first_error(&self) -> Option<anyhow::Error> {
529 if let Some(node_id) = self.duplicate_nodes.first() {
530 return Some(anyhow::anyhow!(
531 "[layout] duplicate node id encountered during graph build: {:?}",
532 node_id
533 ));
534 }
535 if let Some((node_id, parent_id)) = self.missing_parent_refs.first() {
536 return Some(anyhow::anyhow!(
537 "[layout] node {:?} references missing parent {:?}",
538 node_id,
539 parent_id
540 ));
541 }
542 if let Some((node_id, child_id)) = self.missing_child_refs.first() {
543 return Some(anyhow::anyhow!(
544 "[layout] node {:?} references missing child {:?}",
545 node_id,
546 child_id
547 ));
548 }
549 if let Some((parent_id, child_id, actual_parent)) = self.parent_child_mismatches.first() {
550 return Some(anyhow::anyhow!(
551 "[layout] parent/child mismatch parent={:?} child={:?} child.parent_id={:?}",
552 parent_id,
553 child_id,
554 actual_parent
555 ));
556 }
557 if let Some(node_id) = self.cycle_nodes.first() {
558 return Some(anyhow::anyhow!(
559 "[layout] cycle detected while rebuilding graph at {:?}",
560 node_id
561 ));
562 }
563 None
564 }
565}
566
567#[derive(Debug, Clone, Default)]
568struct LayoutGraphState {
569 graph_version: u64,
570 last_layout_version: Option<u64>,
571 node_order: Vec<WidgetId>,
572 node_fingerprints: HashMap<WidgetId, u64>,
573 nodes: HashMap<WidgetId, LayoutInputNode>,
574 parents: HashMap<WidgetId, Option<WidgetId>>,
575 children: HashMap<WidgetId, Vec<WidgetId>>,
576 roots: Vec<WidgetId>,
577 validation: LayoutGraphValidationState,
578}
579
580#[derive(Debug, Clone, Default)]
581struct IncrementalLayoutReuseState {
582 previous_snapshot: LayoutSnapshot,
583 dirty_ancestors: HashSet<WidgetId>,
584}
585
586impl LayoutGraphState {
587 fn is_empty(&self) -> bool {
588 self.nodes.is_empty()
589 }
590
591 fn mark_layout_complete(&mut self) {
592 self.last_layout_version = Some(self.graph_version);
593 }
594
595 fn matches_input_nodes(&self, input_nodes: &[LayoutInputNode]) -> bool {
596 if self.nodes.len() != input_nodes.len() || self.node_order.len() != input_nodes.len() {
597 return false;
598 }
599
600 for (expected_id, node) in self.node_order.iter().zip(input_nodes.iter()) {
601 if *expected_id != node.id {
602 return false;
603 }
604 let Some(existing) = self.node_fingerprints.get(&node.id) else {
605 return false;
606 };
607 if *existing != layout_input_fingerprint(node) {
608 return false;
609 }
610 }
611
612 true
613 }
614
615 fn from_input_nodes(input_nodes: &[LayoutInputNode], version: u64) -> Self {
616 let mut state = Self {
617 graph_version: version,
618 ..Self::default()
619 };
620 state.replace_all_nodes(input_nodes);
621 state
622 }
623
624 fn replace_all_nodes(&mut self, input_nodes: &[LayoutInputNode]) {
625 self.node_order.clear();
626 self.node_fingerprints.clear();
627 self.nodes.clear();
628 self.last_layout_version = None;
629
630 let mut validation = LayoutGraphValidationState::default();
631 let mut seen = HashSet::new();
632 for node in input_nodes {
633 if !seen.insert(node.id) {
634 validation.duplicate_nodes.push(node.id);
635 } else {
636 self.node_order.push(node.id);
637 }
638 self.node_fingerprints
639 .insert(node.id, layout_input_fingerprint(node));
640 self.nodes.insert(node.id, node.clone());
641 }
642
643 self.rebuild_topology(validation);
644 }
645
646 fn update_nodes(&mut self, input_nodes: &[LayoutInputNode]) {
647 let mut validation = LayoutGraphValidationState::default();
648 let mut seen = HashSet::new();
649 let mut next_order = Vec::with_capacity(input_nodes.len());
650 let mut next_fingerprints = HashMap::with_capacity(input_nodes.len());
651 let mut next_nodes = HashMap::with_capacity(input_nodes.len());
652
653 for node in input_nodes {
654 if !seen.insert(node.id) {
655 validation.duplicate_nodes.push(node.id);
656 continue;
657 }
658 next_order.push(node.id);
659 next_fingerprints.insert(node.id, layout_input_fingerprint(node));
660 next_nodes.insert(node.id, node.clone());
661 }
662
663 self.node_order = next_order;
664 self.node_fingerprints = next_fingerprints;
665 self.nodes = next_nodes;
666 self.last_layout_version = None;
667 self.rebuild_topology(validation);
668 }
669
670 fn rebuild_topology(&mut self, mut validation: LayoutGraphValidationState) {
671 self.parents.clear();
672 self.children.clear();
673 self.roots.clear();
674
675 for node_id in &self.node_order {
676 let Some(node) = self.nodes.get(node_id) else {
677 continue;
678 };
679 self.parents.insert(*node_id, node.parent_id);
680 self.children.insert(*node_id, node.children_ids.clone());
681 if node.parent_id.is_none() {
682 self.roots.push(*node_id);
683 } else if let Some(parent_id) = node.parent_id {
684 if !self.nodes.contains_key(&parent_id) {
685 validation.missing_parent_refs.push((*node_id, parent_id));
686 }
687 }
688 }
689
690 for node_id in &self.node_order {
691 let Some(node) = self.nodes.get(node_id) else {
692 continue;
693 };
694 for child_id in &node.children_ids {
695 let Some(child) = self.nodes.get(child_id) else {
696 validation.missing_child_refs.push((*node_id, *child_id));
697 continue;
698 };
699 if child.parent_id != Some(*node_id) {
700 validation
701 .parent_child_mismatches
702 .push((*node_id, *child_id, child.parent_id));
703 }
704 }
705 }
706
707 validation.root_nodes = self.roots.clone();
708 validation.cycle_nodes = self.detect_cycle_nodes();
709 self.validation = validation;
710 }
711
712 fn node(&self, node_id: WidgetId) -> Option<&LayoutInputNode> {
713 self.nodes.get(&node_id)
714 }
715
716 fn children_of(&self, node_id: WidgetId) -> &[WidgetId] {
717 self.children
718 .get(&node_id)
719 .map(Vec::as_slice)
720 .unwrap_or(&[])
721 }
722
723 fn parent_of(&self, node_id: WidgetId) -> Option<WidgetId> {
724 self.parents.get(&node_id).copied().flatten()
725 }
726
727 fn ordered_nodes(&self) -> impl Iterator<Item = &LayoutInputNode> {
728 self.node_order
729 .iter()
730 .filter_map(|node_id| self.nodes.get(node_id))
731 }
732
733 fn detect_cycle_nodes(&self) -> Vec<WidgetId> {
734 fn dfs(
735 node_id: WidgetId,
736 children: &HashMap<WidgetId, Vec<WidgetId>>,
737 visited: &mut HashSet<WidgetId>,
738 stack: &mut HashSet<WidgetId>,
739 cycle_nodes: &mut Vec<WidgetId>,
740 ) {
741 if stack.contains(&node_id) {
742 cycle_nodes.push(node_id);
743 return;
744 }
745 if !visited.insert(node_id) {
746 return;
747 }
748
749 stack.insert(node_id);
750 if let Some(child_nodes) = children.get(&node_id) {
751 for child_id in child_nodes {
752 dfs(*child_id, children, visited, stack, cycle_nodes);
753 }
754 }
755 stack.remove(&node_id);
756 }
757
758 let mut visited = HashSet::new();
759 let mut stack = HashSet::new();
760 let mut cycle_nodes = Vec::new();
761 for node_id in &self.node_order {
762 dfs(
763 *node_id,
764 &self.children,
765 &mut visited,
766 &mut stack,
767 &mut cycle_nodes,
768 );
769 }
770 cycle_nodes.sort_by_key(|node_id| node_id.as_u128());
771 cycle_nodes.dedup();
772 cycle_nodes
773 }
774}
775
776#[cfg(test)]
777mod tests {
778 use super::{
779 flyout_root_position, resolve_length, LayoutEngine, LayoutGraphState, LayoutInputNode,
780 LayoutPoint, LayoutRect, LayoutSize, TextMeasurer, DEFAULT_RICH_TEXT_HIT_TEST_FONT_SIZE,
781 };
782 use fission_ir::op::{
783 BoxStyle, Color, FontStyle, GridTrack, Length, ResponsiveCondition, ResponsiveQuery,
784 TextRun, TextStyle,
785 };
786 use fission_ir::{GridPlacement, LayoutOp, WidgetId};
787 use std::sync::atomic::{AtomicU32, Ordering};
788 use std::sync::Arc;
789
790 fn box_node(
791 id: WidgetId,
792 parent_id: Option<WidgetId>,
793 children_ids: Vec<WidgetId>,
794 ) -> LayoutInputNode {
795 LayoutInputNode {
796 id,
797 parent_id,
798 op: LayoutOp::Box {
799 width: Some(40.0),
800 height: Some(20.0),
801 min_width: None,
802 max_width: None,
803 min_height: None,
804 max_height: None,
805 padding: [0.0; 4],
806 flex_grow: 0.0,
807 flex_shrink: 0.0,
808 aspect_ratio: None,
809 },
810 children_ids,
811 debug_name: format!("node-{}", id.as_u128()),
812 width: Some(40.0),
813 height: Some(20.0),
814 flex_grow: 0.0,
815 flex_shrink: 0.0,
816 rich_text: None,
817 }
818 }
819
820 struct RecordingMeasurer {
821 last_font_size_bits: AtomicU32,
822 }
823
824 struct WrappingMeasurer;
825
826 impl TextMeasurer for WrappingMeasurer {
827 fn measure(&self, text: &str, _font_size: f32, available_width: Option<f32>) -> (f32, f32) {
828 let natural_width = text.chars().count() as f32 * 10.0;
829 match available_width.filter(|width| *width > 0.0 && natural_width > *width) {
830 Some(width) => (width, (natural_width / width).ceil() * 20.0),
831 None => (natural_width, 20.0),
832 }
833 }
834 }
835
836 fn node(
837 id: WidgetId,
838 parent_id: Option<WidgetId>,
839 children_ids: Vec<WidgetId>,
840 op: LayoutOp,
841 ) -> LayoutInputNode {
842 let (width, height, flex_grow, flex_shrink) = match &op {
843 LayoutOp::Box {
844 width,
845 height,
846 flex_grow,
847 flex_shrink,
848 ..
849 } => (*width, *height, *flex_grow, *flex_shrink),
850 LayoutOp::StyledBox {
851 flex_grow,
852 flex_shrink,
853 ..
854 } => (None, None, *flex_grow, *flex_shrink),
855 _ => (None, None, 0.0, 1.0),
856 };
857 LayoutInputNode {
858 id,
859 parent_id,
860 op,
861 children_ids,
862 debug_name: format!("node-{}", id.as_u128()),
863 width,
864 height,
865 flex_grow,
866 flex_shrink,
867 rich_text: None,
868 }
869 }
870
871 fn text_run(text: &str) -> TextRun {
872 TextRun {
873 text: text.to_owned(),
874 style: TextStyle {
875 font_size: 16.0,
876 color: Color::BLACK,
877 underline: false,
878 font_family: None,
879 locale: None,
880 font_weight: 400,
881 font_style: FontStyle::Normal,
882 line_height: None,
883 letter_spacing: 0.0,
884 background_color: None,
885 typography: Default::default(),
886 },
887 }
888 }
889
890 impl RecordingMeasurer {
891 fn new() -> Self {
892 Self {
893 last_font_size_bits: AtomicU32::new(f32::NAN.to_bits()),
894 }
895 }
896
897 fn last_font_size(&self) -> f32 {
898 f32::from_bits(self.last_font_size_bits.load(Ordering::SeqCst))
899 }
900 }
901
902 impl TextMeasurer for RecordingMeasurer {
903 fn measure(
904 &self,
905 _text: &str,
906 _font_size: f32,
907 _available_width: Option<f32>,
908 ) -> (f32, f32) {
909 (0.0, 0.0)
910 }
911
912 fn hit_test(
913 &self,
914 _text: &str,
915 font_size: f32,
916 _available_width: Option<f32>,
917 _x: f32,
918 _y: f32,
919 ) -> usize {
920 self.last_font_size_bits
921 .store(font_size.to_bits(), Ordering::SeqCst);
922 0
923 }
924 }
925
926 #[test]
927 fn matches_input_nodes_rejects_reordered_flattened_inputs() {
928 let root = WidgetId::from_u128(1);
929 let first = WidgetId::from_u128(2);
930 let second = WidgetId::from_u128(3);
931 let canonical = vec![
932 box_node(root, None, vec![first, second]),
933 box_node(first, Some(root), vec![]),
934 box_node(second, Some(root), vec![]),
935 ];
936 let reordered = vec![
937 box_node(root, None, vec![first, second]),
938 box_node(second, Some(root), vec![]),
939 box_node(first, Some(root), vec![]),
940 ];
941
942 let state = LayoutGraphState::from_input_nodes(&canonical, 1);
943 assert!(!state.matches_input_nodes(&reordered));
944 }
945
946 #[test]
947 fn update_refreshes_node_order_for_reordered_flattened_inputs() {
948 let root = WidgetId::from_u128(10);
949 let first = WidgetId::from_u128(11);
950 let second = WidgetId::from_u128(12);
951 let canonical = vec![
952 box_node(root, None, vec![first, second]),
953 box_node(first, Some(root), vec![]),
954 box_node(second, Some(root), vec![]),
955 ];
956 let reordered = vec![
957 box_node(root, None, vec![first, second]),
958 box_node(second, Some(root), vec![]),
959 box_node(first, Some(root), vec![]),
960 ];
961
962 let mut engine = LayoutEngine::new();
963 engine.update(&canonical);
964 engine.update(&reordered);
965
966 let ordered = engine
967 .graph_state
968 .ordered_nodes()
969 .map(|node| node.id)
970 .collect::<Vec<_>>();
971 assert_eq!(ordered, vec![root, second, first]);
972 }
973
974 #[test]
975 fn rich_text_hit_test_uses_body_font_size_when_runs_are_empty() {
976 let measurer = RecordingMeasurer::new();
977
978 measurer.hit_test_rich(&[], None, 4.0, 2.0);
979
980 assert_eq!(
981 measurer.last_font_size(),
982 DEFAULT_RICH_TEXT_HIT_TEST_FONT_SIZE
983 );
984 }
985
986 #[test]
987 fn rich_text_hit_test_uses_first_run_font_size_when_present() {
988 let measurer = RecordingMeasurer::new();
989 let runs = vec![TextRun {
990 text: "Hello".to_string(),
991 style: TextStyle {
992 font_size: 18.0,
993 color: Color::BLACK,
994 underline: false,
995 font_family: None,
996 locale: None,
997 font_weight: 400,
998 font_style: FontStyle::Normal,
999 line_height: None,
1000 letter_spacing: 0.0,
1001 background_color: None,
1002 typography: Default::default(),
1003 },
1004 }];
1005
1006 measurer.hit_test_rich(&runs, None, 4.0, 2.0);
1007
1008 assert_eq!(measurer.last_font_size(), 18.0);
1009 }
1010
1011 #[test]
1012 fn typed_lengths_resolve_calc_clamp_and_viewport_units() {
1013 let viewport = LayoutSize::new(1200.0, 800.0);
1014 let calculated = Length::percent(50.0) - Length::points(24.0);
1015 let clamped = Length::clamp(Length::points(100.0), calculated, Length::vw(40.0));
1016
1017 assert_eq!(resolve_length(&clamped, 600.0, viewport), Some(276.0));
1018 assert_eq!(
1019 resolve_length(&Length::vh(25.0), 0.0, viewport),
1020 Some(200.0)
1021 );
1022 assert_eq!(
1023 Length::points(10.0).resolve(0.0, viewport.width, viewport.height),
1024 Some(10.0)
1025 );
1026 assert_eq!(
1027 (Length::points(10.0) - Length::points(24.0)).resolve(
1028 0.0,
1029 viewport.width,
1030 viewport.height
1031 ),
1032 Some(-14.0),
1033 "signed expressions remain available to typed positioning"
1034 );
1035 assert_eq!(
1036 Length::min(vec![Length::points(10.0), Length::MaxContent]).resolve(
1037 100.0,
1038 viewport.width,
1039 viewport.height
1040 ),
1041 None,
1042 "intrinsic expressions must be measured rather than partially resolved"
1043 );
1044 }
1045
1046 #[test]
1047 fn responsive_container_query_selects_from_parent_constraints() {
1048 let root = WidgetId::from_u128(100);
1049 let responsive = WidgetId::from_u128(101);
1050 let compact = WidgetId::from_u128(102);
1051 let wide = WidgetId::from_u128(103);
1052 let nodes = vec![
1053 node(
1054 root,
1055 None,
1056 vec![responsive],
1057 LayoutOp::Box {
1058 width: Some(240.0),
1059 height: Some(100.0),
1060 min_width: None,
1061 max_width: None,
1062 min_height: None,
1063 max_height: None,
1064 padding: [0.0; 4],
1065 flex_grow: 0.0,
1066 flex_shrink: 1.0,
1067 aspect_ratio: None,
1068 },
1069 ),
1070 node(
1071 responsive,
1072 Some(root),
1073 vec![compact, wide],
1074 LayoutOp::Responsive {
1075 query: ResponsiveQuery::Container,
1076 cases: vec![ResponsiveCondition {
1077 min_width: None,
1078 max_width: Some(300.0),
1079 }],
1080 },
1081 ),
1082 box_node(compact, Some(responsive), vec![]),
1083 box_node(wide, Some(responsive), vec![]),
1084 ];
1085 let mut engine = LayoutEngine::new();
1086 let snapshot = engine
1087 .compute_layout(&nodes, root, LayoutSize::new(800.0, 600.0), &|_| 0.0)
1088 .expect("responsive layout");
1089
1090 assert!(snapshot.nodes.contains_key(&compact));
1091 assert!(!snapshot.nodes.contains_key(&wide));
1092 }
1093
1094 #[test]
1095 fn responsive_cases_use_first_match_precedence() {
1096 let root = WidgetId::from_u128(110);
1097 let responsive = WidgetId::from_u128(111);
1098 let first_match = WidgetId::from_u128(112);
1099 let later_match = WidgetId::from_u128(113);
1100 let fallback = WidgetId::from_u128(114);
1101 let nodes = vec![
1102 node(
1103 root,
1104 None,
1105 vec![responsive],
1106 LayoutOp::Box {
1107 width: Some(500.0),
1108 height: Some(100.0),
1109 min_width: None,
1110 max_width: None,
1111 min_height: None,
1112 max_height: None,
1113 padding: [0.0; 4],
1114 flex_grow: 0.0,
1115 flex_shrink: 1.0,
1116 aspect_ratio: None,
1117 },
1118 ),
1119 node(
1120 responsive,
1121 Some(root),
1122 vec![first_match, later_match, fallback],
1123 LayoutOp::Responsive {
1124 query: ResponsiveQuery::Viewport,
1125 cases: vec![
1126 ResponsiveCondition {
1127 min_width: None,
1128 max_width: Some(900.0),
1129 },
1130 ResponsiveCondition {
1131 min_width: None,
1132 max_width: Some(600.0),
1133 },
1134 ],
1135 },
1136 ),
1137 box_node(first_match, Some(responsive), vec![]),
1138 box_node(later_match, Some(responsive), vec![]),
1139 box_node(fallback, Some(responsive), vec![]),
1140 ];
1141 let mut engine = LayoutEngine::new();
1142 let snapshot = engine
1143 .compute_layout(&nodes, root, LayoutSize::new(500.0, 600.0), &|_| 0.0)
1144 .expect("responsive layout");
1145
1146 assert!(snapshot.nodes.contains_key(&first_match));
1147 assert!(!snapshot.nodes.contains_key(&later_match));
1148 assert!(!snapshot.nodes.contains_key(&fallback));
1149 }
1150
1151 #[test]
1152 fn grid_repeat_and_spans_are_applied_by_the_layout_engine() {
1153 let root = WidgetId::from_u128(200);
1154 let first = WidgetId::from_u128(201);
1155 let second = WidgetId::from_u128(202);
1156 let nodes = vec![
1157 node(
1158 root,
1159 None,
1160 vec![first, second],
1161 LayoutOp::Grid {
1162 columns: vec![GridTrack::repeat(2, vec![GridTrack::Points(50.0)])],
1163 rows: vec![GridTrack::Points(20.0)],
1164 column_gap: Some(10.0),
1165 row_gap: None,
1166 padding: [0.0; 4],
1167 },
1168 ),
1169 box_node(first, Some(root), vec![]),
1170 box_node(second, Some(root), vec![]),
1171 ];
1172 let mut engine = LayoutEngine::new();
1173 let snapshot = engine
1174 .compute_layout(&nodes, root, LayoutSize::new(110.0, 20.0), &|_| 0.0)
1175 .expect("grid layout");
1176
1177 assert_eq!(snapshot.nodes[&first].rect.x(), 0.0);
1178 assert_eq!(snapshot.nodes[&second].rect.x(), 60.0);
1179 }
1180
1181 #[test]
1182 fn auto_grid_items_advance_past_occupied_spans() {
1183 let root = WidgetId::from_u128(250);
1184 let first = WidgetId::from_u128(251);
1185 let first_child = WidgetId::from_u128(252);
1186 let second = WidgetId::from_u128(253);
1187 let second_child = WidgetId::from_u128(254);
1188 let nodes = vec![
1189 node(
1190 root,
1191 None,
1192 vec![first, second],
1193 LayoutOp::Grid {
1194 columns: vec![GridTrack::Points(50.0), GridTrack::Points(50.0)],
1195 rows: vec![],
1196 column_gap: None,
1197 row_gap: None,
1198 padding: [0.0; 4],
1199 },
1200 ),
1201 node(
1202 first,
1203 Some(root),
1204 vec![first_child],
1205 LayoutOp::GridItem {
1206 row_start: GridPlacement::Auto,
1207 row_end: GridPlacement::Auto,
1208 col_start: GridPlacement::Auto,
1209 col_end: GridPlacement::Span(2),
1210 },
1211 ),
1212 box_node(first_child, Some(first), vec![]),
1213 node(
1214 second,
1215 Some(root),
1216 vec![second_child],
1217 LayoutOp::GridItem {
1218 row_start: GridPlacement::Auto,
1219 row_end: GridPlacement::Auto,
1220 col_start: GridPlacement::Auto,
1221 col_end: GridPlacement::Auto,
1222 },
1223 ),
1224 box_node(second_child, Some(second), vec![]),
1225 ];
1226 let mut engine = LayoutEngine::new();
1227 let snapshot = engine
1228 .compute_layout(&nodes, root, LayoutSize::new(100.0, 100.0), &|_| 0.0)
1229 .expect("auto grid layout");
1230
1231 assert_eq!(snapshot.nodes[&first].rect.x(), 0.0);
1232 assert_eq!(snapshot.nodes[&first].rect.width(), 100.0);
1233 assert_eq!(snapshot.nodes[&second].rect.x(), 0.0);
1234 assert_eq!(snapshot.nodes[&second].rect.y(), 20.0);
1235 }
1236
1237 #[test]
1238 fn fixed_text_box_retains_natural_size_for_overflow_inspection() {
1239 let root = WidgetId::from_u128(300);
1240 let mut text = node(
1241 root,
1242 None,
1243 vec![],
1244 LayoutOp::StyledBox {
1245 style: BoxStyle {
1246 width: Some(Length::Points(40.0)),
1247 height: Some(Length::Points(10.0)),
1248 ..Default::default()
1249 },
1250 flex_grow: 0.0,
1251 flex_shrink: 1.0,
1252 },
1253 );
1254 text.rich_text = Some(vec![text_run("overflowing text")]);
1255 let nodes = vec![text];
1256 let mut engine = LayoutEngine::new().with_measurer(Arc::new(WrappingMeasurer));
1257 let snapshot = engine
1258 .compute_layout(&nodes, root, LayoutSize::new(100.0, 100.0), &|_| 0.0)
1259 .expect("text layout");
1260 let inspection = engine
1261 .inspect_node(&snapshot, root)
1262 .expect("layout inspection");
1263
1264 assert_eq!(inspection.laid_out.width(), 40.0);
1265 assert_eq!(inspection.laid_out.height(), 10.0);
1266 assert!(inspection.measured.height() > inspection.laid_out.height());
1267 assert!(inspection.overflow_y);
1268 assert_eq!(
1269 inspection.constrained, inspection.laid_out,
1270 "fixed constraints should match final bounds"
1271 );
1272 }
1273
1274 #[test]
1275 fn max_content_box_propagates_unwrapped_text_width() {
1276 let root = WidgetId::from_u128(400);
1277 let text_id = WidgetId::from_u128(401);
1278 let mut text = node(
1279 text_id,
1280 Some(root),
1281 vec![],
1282 LayoutOp::Box {
1283 width: None,
1284 height: None,
1285 min_width: None,
1286 max_width: None,
1287 min_height: None,
1288 max_height: None,
1289 padding: [0.0; 4],
1290 flex_grow: 0.0,
1291 flex_shrink: 1.0,
1292 aspect_ratio: None,
1293 },
1294 );
1295 text.rich_text = Some(vec![text_run("hello world")]);
1296 let nodes = vec![
1297 node(
1298 root,
1299 None,
1300 vec![text_id],
1301 LayoutOp::StyledBox {
1302 style: BoxStyle {
1303 width: Some(Length::MaxContent),
1304 ..Default::default()
1305 },
1306 flex_grow: 0.0,
1307 flex_shrink: 1.0,
1308 },
1309 ),
1310 text,
1311 ];
1312 let mut engine = LayoutEngine::new().with_measurer(Arc::new(WrappingMeasurer));
1313 let snapshot = engine
1314 .compute_layout(&nodes, root, LayoutSize::new(300.0, 100.0), &|_| 0.0)
1315 .expect("max-content layout");
1316
1317 assert_eq!(snapshot.nodes[&root].rect.width(), 110.0);
1318 assert_eq!(snapshot.nodes[&text_id].rect.width(), 110.0);
1319 }
1320
1321 #[test]
1322 fn intrinsic_lengths_participate_in_clamp_expressions() {
1323 let root = WidgetId::from_u128(450);
1324 let mut text = node(
1325 root,
1326 None,
1327 vec![],
1328 LayoutOp::StyledBox {
1329 style: BoxStyle {
1330 width: Some(Length::clamp(
1331 Length::points(50.0),
1332 Length::MaxContent,
1333 Length::points(80.0),
1334 )),
1335 ..Default::default()
1336 },
1337 flex_grow: 0.0,
1338 flex_shrink: 1.0,
1339 },
1340 );
1341 text.rich_text = Some(vec![text_run("hello world")]);
1342 let mut engine = LayoutEngine::new().with_measurer(Arc::new(WrappingMeasurer));
1343 let snapshot = engine
1344 .compute_layout(&[text], root, LayoutSize::new(300.0, 100.0), &|_| 0.0)
1345 .expect("intrinsic clamp layout");
1346
1347 assert_eq!(snapshot.nodes[&root].rect.width(), 80.0);
1348 assert_eq!(snapshot.nodes[&root].rect.height(), 40.0);
1349 }
1350
1351 #[test]
1352 fn margin_wrapper_keeps_percentage_width_relative_to_the_containing_box() {
1353 let outer = WidgetId::from_u128(455);
1354 let inner = WidgetId::from_u128(456);
1355 let nodes = vec![
1356 node(
1357 outer,
1358 None,
1359 vec![inner],
1360 LayoutOp::StyledBox {
1361 style: BoxStyle {
1362 width: Some(
1363 Length::percent(50.0) + Length::points(12.0) + Length::points(12.0),
1364 ),
1365 padding: Some(Length::all(Length::points(12.0))),
1366 alignment: fission_ir::op::BoxAlignment::Stretch,
1367 ..Default::default()
1368 },
1369 flex_grow: 0.0,
1370 flex_shrink: 1.0,
1371 },
1372 ),
1373 node(
1374 inner,
1375 Some(outer),
1376 vec![],
1377 LayoutOp::StyledBox {
1378 style: BoxStyle {
1379 width: Some(Length::percent(100.0)),
1380 height: Some(Length::points(20.0)),
1381 ..Default::default()
1382 },
1383 flex_grow: 0.0,
1384 flex_shrink: 1.0,
1385 },
1386 ),
1387 ];
1388 let mut engine = LayoutEngine::new();
1389 let snapshot = engine
1390 .compute_layout(&nodes, outer, LayoutSize::new(200.0, 100.0), &|_| 0.0)
1391 .expect("margin layout");
1392
1393 assert_eq!(snapshot.nodes[&outer].rect.width(), 124.0);
1394 assert_eq!(snapshot.nodes[&inner].rect.width(), 100.0);
1395 assert_eq!(snapshot.nodes[&inner].rect.x(), 12.0);
1396 }
1397
1398 #[test]
1399 fn fit_content_height_preserves_wrapped_text_height() {
1400 let root = WidgetId::from_u128(460);
1401 let mut text = node(
1402 root,
1403 None,
1404 vec![],
1405 LayoutOp::StyledBox {
1406 style: BoxStyle {
1407 width: Some(Length::points(40.0)),
1408 height: Some(Length::fit_content(None)),
1409 ..Default::default()
1410 },
1411 flex_grow: 0.0,
1412 flex_shrink: 1.0,
1413 },
1414 );
1415 text.rich_text = Some(vec![text_run("abcdefgh")]);
1416 let mut engine = LayoutEngine::new().with_measurer(Arc::new(WrappingMeasurer));
1417 let snapshot = engine
1418 .compute_layout(&[text], root, LayoutSize::new(300.0, 100.0), &|_| 0.0)
1419 .expect("fit-content height layout");
1420
1421 assert_eq!(snapshot.nodes[&root].rect.width(), 40.0);
1422 assert_eq!(snapshot.nodes[&root].rect.height(), 40.0);
1423 }
1424
1425 #[test]
1426 fn typed_position_offsets_resolve_against_the_parent_box() {
1427 let root = WidgetId::from_u128(500);
1428 let positioned = WidgetId::from_u128(501);
1429 let child = WidgetId::from_u128(502);
1430 let nodes = vec![
1431 node(root, None, vec![positioned], LayoutOp::ZStack),
1432 node(
1433 positioned,
1434 Some(root),
1435 vec![child],
1436 LayoutOp::PositionedLengths {
1437 left: Some(Length::Percent(25.0)),
1438 top: Some(Length::Percent(10.0)),
1439 right: None,
1440 bottom: None,
1441 width: Some(Length::Points(50.0)),
1442 height: Some(Length::Points(20.0)),
1443 },
1444 ),
1445 node(
1446 child,
1447 Some(positioned),
1448 vec![],
1449 LayoutOp::Box {
1450 width: None,
1451 height: None,
1452 min_width: None,
1453 max_width: None,
1454 min_height: None,
1455 max_height: None,
1456 padding: [0.0; 4],
1457 flex_grow: 0.0,
1458 flex_shrink: 1.0,
1459 aspect_ratio: None,
1460 },
1461 ),
1462 ];
1463 let mut engine = LayoutEngine::new();
1464 let snapshot = engine
1465 .compute_layout(&nodes, root, LayoutSize::new(200.0, 100.0), &|_| 0.0)
1466 .expect("typed positioned layout");
1467
1468 assert_eq!(snapshot.nodes[&child].rect.x(), 50.0);
1469 assert_eq!(snapshot.nodes[&child].rect.y(), 10.0);
1470 assert_eq!(snapshot.nodes[&child].rect.width(), 50.0);
1471 assert_eq!(snapshot.nodes[&child].rect.height(), 20.0);
1472 }
1473
1474 #[test]
1475 fn spotlight_lays_out_inverse_overlay_around_anchor() {
1476 let root = WidgetId::from_u128(20);
1477 let positioned = WidgetId::from_u128(21);
1478 let anchor = WidgetId::from_u128(22);
1479 let spotlight = WidgetId::from_u128(23);
1480 let panels = (24..=28).map(WidgetId::from_u128).collect::<Vec<_>>();
1481
1482 let mut nodes = vec![
1483 LayoutInputNode {
1484 id: root,
1485 parent_id: None,
1486 op: LayoutOp::ZStack,
1487 children_ids: vec![positioned, spotlight],
1488 debug_name: "root".into(),
1489 width: None,
1490 height: None,
1491 flex_grow: 0.0,
1492 flex_shrink: 1.0,
1493 rich_text: None,
1494 },
1495 LayoutInputNode {
1496 id: positioned,
1497 parent_id: Some(root),
1498 op: LayoutOp::Positioned {
1499 left: Some(100.0),
1500 top: Some(100.0),
1501 right: None,
1502 bottom: None,
1503 width: Some(200.0),
1504 height: Some(80.0),
1505 },
1506 children_ids: vec![anchor],
1507 debug_name: "positioned-anchor".into(),
1508 width: Some(200.0),
1509 height: Some(80.0),
1510 flex_grow: 0.0,
1511 flex_shrink: 0.0,
1512 rich_text: None,
1513 },
1514 box_node(anchor, Some(positioned), vec![]),
1515 LayoutInputNode {
1516 id: spotlight,
1517 parent_id: Some(root),
1518 op: LayoutOp::Spotlight {
1519 anchor,
1520 padding: 12.0,
1521 },
1522 children_ids: panels.clone(),
1523 debug_name: "spotlight".into(),
1524 width: None,
1525 height: None,
1526 flex_grow: 0.0,
1527 flex_shrink: 1.0,
1528 rich_text: None,
1529 },
1530 ];
1531 nodes[2].op = LayoutOp::Box {
1532 width: Some(200.0),
1533 height: Some(80.0),
1534 min_width: None,
1535 max_width: None,
1536 min_height: None,
1537 max_height: None,
1538 padding: [0.0; 4],
1539 flex_grow: 0.0,
1540 flex_shrink: 0.0,
1541 aspect_ratio: None,
1542 };
1543 nodes[2].width = Some(200.0);
1544 nodes[2].height = Some(80.0);
1545 nodes.extend(
1546 panels
1547 .iter()
1548 .map(|id| box_node(*id, Some(spotlight), vec![])),
1549 );
1550
1551 let mut engine = LayoutEngine::new();
1552 let snapshot = engine
1553 .compute_layout(&nodes, root, LayoutSize::new(800.0, 600.0), &|_| 0.0)
1554 .expect("spotlight layout");
1555
1556 let expected = [
1557 LayoutRect::new(0.0, 0.0, 800.0, 88.0),
1558 LayoutRect::new(0.0, 192.0, 800.0, 408.0),
1559 LayoutRect::new(0.0, 88.0, 88.0, 104.0),
1560 LayoutRect::new(312.0, 88.0, 488.0, 104.0),
1561 LayoutRect::new(88.0, 88.0, 224.0, 104.0),
1562 ];
1563 for (panel, expected_rect) in panels.iter().zip(expected) {
1564 assert_eq!(snapshot.get_node_rect(*panel), Some(expected_rect));
1565 }
1566 }
1567
1568 #[test]
1569 fn flyout_placement_clamps_rendered_descendants_inside_viewport() {
1570 let position = flyout_root_position(
1571 LayoutSize::new(800.0, 600.0),
1572 LayoutRect::new(700.0, 550.0, 80.0, 32.0),
1573 LayoutRect::new(0.0, 8.0, 440.0, 220.0),
1574 );
1575
1576 assert_eq!(position, LayoutPoint::new(360.0, 322.0));
1577 }
1578
1579 #[test]
1580 fn flyout_placement_prefers_below_when_full_content_fits() {
1581 let position = flyout_root_position(
1582 LayoutSize::new(800.0, 600.0),
1583 LayoutRect::new(100.0, 100.0, 200.0, 80.0),
1584 LayoutRect::new(0.0, 8.0, 320.0, 180.0),
1585 );
1586
1587 assert_eq!(position, LayoutPoint::new(100.0, 172.0));
1588 }
1589}
1590
1591fn layout_input_fingerprint(node: &LayoutInputNode) -> u64 {
1592 let mut hasher = DefaultHasher::new();
1593 format!("{node:?}").hash(&mut hasher);
1594 hasher.finish()
1595}
1596
1597fn intersect_rect(left: LayoutRect, right: LayoutRect) -> LayoutRect {
1598 let x = left.x().max(right.x());
1599 let y = left.y().max(right.y());
1600 let right_edge = left.right().min(right.right());
1601 let bottom_edge = left.bottom().min(right.bottom());
1602 LayoutRect::new(x, y, (right_edge - x).max(0.0), (bottom_edge - y).max(0.0))
1603}
1604
1605fn union_rect(left: LayoutRect, right: LayoutRect) -> LayoutRect {
1606 let x = left.x().min(right.x());
1607 let y = left.y().min(right.y());
1608 let right_edge = left.right().max(right.right());
1609 let bottom_edge = left.bottom().max(right.bottom());
1610 LayoutRect::new(x, y, right_edge - x, bottom_edge - y)
1611}
1612
1613#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1628pub struct LayoutRect {
1629 pub origin: LayoutPoint,
1631 pub size: LayoutSize,
1633}
1634
1635impl LayoutRect {
1636 pub fn new(x: LayoutUnit, y: LayoutUnit, width: LayoutUnit, height: LayoutUnit) -> Self {
1638 Self {
1639 origin: LayoutPoint { x, y },
1640 size: LayoutSize { width, height },
1641 }
1642 }
1643
1644 pub fn x(&self) -> LayoutUnit {
1646 self.origin.x
1647 }
1648 pub fn y(&self) -> LayoutUnit {
1650 self.origin.y
1651 }
1652 pub fn width(&self) -> LayoutUnit {
1654 self.size.width
1655 }
1656 pub fn height(&self) -> LayoutUnit {
1658 self.size.height
1659 }
1660
1661 pub fn right(&self) -> LayoutUnit {
1663 self.origin.x + self.size.width
1664 }
1665 pub fn bottom(&self) -> LayoutUnit {
1667 self.origin.y + self.size.height
1668 }
1669
1670 pub fn contains(&self, p: LayoutPoint) -> bool {
1673 p.x >= self.x() && p.x < self.right() && p.y >= self.y() && p.y < self.bottom()
1674 }
1675}
1676
1677fn spotlight_regions(
1678 bounds: LayoutRect,
1679 target: Option<LayoutRect>,
1680 padding: LayoutUnit,
1681) -> [LayoutRect; 5] {
1682 let zero = LayoutRect::new(bounds.x(), bounds.y(), 0.0, 0.0);
1683 let Some(target) = target else {
1684 return [bounds, zero, zero, zero, zero];
1685 };
1686
1687 let padding = if padding.is_finite() {
1688 padding.max(0.0)
1689 } else {
1690 0.0
1691 };
1692 let left = (target.x() - padding).clamp(bounds.x(), bounds.right());
1693 let top = (target.y() - padding).clamp(bounds.y(), bounds.bottom());
1694 let right = (target.right() + padding).clamp(bounds.x(), bounds.right());
1695 let bottom = (target.bottom() + padding).clamp(bounds.y(), bounds.bottom());
1696
1697 if right <= left || bottom <= top {
1698 return [bounds, zero, zero, zero, zero];
1699 }
1700
1701 let hole_width = right - left;
1702 let hole_height = bottom - top;
1703 [
1704 LayoutRect::new(bounds.x(), bounds.y(), bounds.width(), top - bounds.y()),
1705 LayoutRect::new(bounds.x(), bottom, bounds.width(), bounds.bottom() - bottom),
1706 LayoutRect::new(bounds.x(), top, left - bounds.x(), hole_height),
1707 LayoutRect::new(left + hole_width, top, bounds.right() - right, hole_height),
1708 LayoutRect::new(left, top, hole_width, hole_height),
1709 ]
1710}
1711
1712fn flyout_root_position(
1713 viewport: LayoutSize,
1714 anchor: LayoutRect,
1715 content_extents: LayoutRect,
1716) -> LayoutPoint {
1717 let min_left = -content_extents.x();
1718 let max_left = viewport.width - content_extents.right();
1719 let desired_left = anchor.x() - content_extents.x();
1720 let left = if max_left >= min_left {
1721 desired_left.clamp(min_left, max_left)
1722 } else {
1723 min_left
1724 };
1725
1726 let below = anchor.bottom() - content_extents.y();
1727 let above = anchor.y() - content_extents.bottom();
1728 let min_top = -content_extents.y();
1729 let max_top = viewport.height - content_extents.bottom();
1730 let top = if below + content_extents.bottom() <= viewport.height {
1731 below
1732 } else if above + content_extents.y() >= 0.0 {
1733 above
1734 } else if max_top >= min_top {
1735 below.clamp(min_top, max_top)
1736 } else {
1737 min_top
1738 };
1739
1740 LayoutPoint::new(left, top)
1741}
1742
1743#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1749pub struct LayoutNodeGeometry {
1750 pub rect: LayoutRect,
1752 pub content_size: LayoutSize,
1755}
1756
1757#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1759pub struct LayoutInspection {
1760 pub node: WidgetId,
1762 pub measured: LayoutRect,
1764 pub constraints: BoxConstraints,
1766 pub constrained: LayoutRect,
1768 pub laid_out: LayoutRect,
1770 pub clipped: LayoutRect,
1772 pub painted: LayoutRect,
1774 pub overflow_x: bool,
1776 pub overflow_y: bool,
1778}
1779
1780#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1796pub struct LayoutSnapshot {
1797 pub nodes: HashMap<WidgetId, LayoutNodeGeometry>,
1799 #[serde(skip)]
1802 pub constraints: HashMap<WidgetId, BoxConstraints>,
1803 #[serde(default)]
1807 pub paragraphs: HashMap<WidgetId, ResolvedParagraphLayout>,
1808 pub viewport_size: LayoutSize,
1810}
1811
1812impl LayoutSnapshot {
1813 pub fn new(viewport_size: LayoutSize) -> Self {
1815 Self {
1816 nodes: HashMap::new(),
1817 constraints: HashMap::new(),
1818 paragraphs: HashMap::new(),
1819 viewport_size,
1820 }
1821 }
1822
1823 pub fn get_node_geometry(&self, node_id: WidgetId) -> Option<&LayoutNodeGeometry> {
1826 self.nodes.get(&node_id)
1827 }
1828
1829 pub fn get_node_rect(&self, node_id: WidgetId) -> Option<LayoutRect> {
1831 self.nodes.get(&node_id).map(|g| g.rect)
1832 }
1833
1834 pub fn get_node_constraints(&self, node_id: WidgetId) -> Option<BoxConstraints> {
1837 self.constraints.get(&node_id).copied()
1838 }
1839
1840 pub fn get_resolved_paragraph(&self, node_id: WidgetId) -> Option<&ResolvedParagraphLayout> {
1841 self.paragraphs.get(&node_id)
1842 }
1843}
1844
1845#[derive(Debug, Clone)]
1854pub struct LayoutInputNode {
1855 pub id: WidgetId,
1857 pub parent_id: Option<WidgetId>,
1859 pub op: LayoutOp,
1861 pub children_ids: Vec<WidgetId>,
1863 pub debug_name: String,
1865 pub width: Option<LayoutUnit>,
1867 pub height: Option<LayoutUnit>,
1869 pub flex_grow: LayoutUnit,
1871 pub flex_shrink: LayoutUnit,
1873 pub rich_text: Option<Vec<TextRun>>,
1876}
1877
1878fn has_explicit_axis_size(node: &LayoutInputNode, horizontal: bool) -> bool {
1879 let fixed = if horizontal { node.width } else { node.height };
1880 if fixed.is_some() {
1881 return true;
1882 }
1883
1884 let typed_length_is_explicit =
1885 |length: Option<&Length>| length.is_some_and(|length| !matches!(length, Length::Auto));
1886
1887 match &node.op {
1888 LayoutOp::Box { width, height, .. }
1889 | LayoutOp::Scroll { width, height, .. }
1890 | LayoutOp::Embed { width, height, .. }
1891 | LayoutOp::Positioned { width, height, .. } => {
1892 if horizontal {
1893 width.is_some()
1894 } else {
1895 height.is_some()
1896 }
1897 }
1898 LayoutOp::StyledBox { style, .. } => typed_length_is_explicit(if horizontal {
1899 style.width.as_ref()
1900 } else {
1901 style.height.as_ref()
1902 }),
1903 LayoutOp::PositionedLengths { width, height, .. } => {
1904 typed_length_is_explicit(if horizontal {
1905 width.as_ref()
1906 } else {
1907 height.as_ref()
1908 })
1909 }
1910 _ => false,
1911 }
1912}
1913
1914fn has_explicit_cross_axis_size(node: &LayoutInputNode, is_row: bool) -> bool {
1915 has_explicit_axis_size(node, !is_row)
1916}
1917
1918fn has_explicit_main_axis_size(node: &LayoutInputNode, is_row: bool) -> bool {
1919 has_explicit_axis_size(node, is_row)
1920}
1921
1922const DEFAULT_RICH_TEXT_HIT_TEST_FONT_SIZE: f32 = 14.0;
1942
1943pub trait TextMeasurer: Send + Sync {
1944 fn measure(&self, text: &str, font_size: f32, available_width: Option<f32>) -> (f32, f32);
1949
1950 fn hit_test(
1955 &self,
1956 _text: &str,
1957 _font_size: f32,
1958 _available_width: Option<f32>,
1959 _x: f32,
1960 _y: f32,
1961 ) -> usize {
1962 0
1963 }
1964
1965 fn get_line_metrics(
1970 &self,
1971 _text: &str,
1972 _font_size: f32,
1973 _available_width: Option<f32>,
1974 ) -> Vec<LineMetric> {
1975 vec![]
1976 }
1977
1978 fn get_caret_position(
1983 &self,
1984 _text: &str,
1985 _font_size: f32,
1986 _available_width: Option<f32>,
1987 _caret_index: usize,
1988 ) -> (f32, f32) {
1989 (0.0, 0.0)
1990 }
1991
1992 fn measure_rich_text(&self, _runs: &[TextRun], _available_width: Option<f32>) -> (f32, f32) {
1996 (0.0, 0.0)
1997 }
1998
1999 fn layout_rich_text(
2005 &self,
2006 runs: &[TextRun],
2007 available_width: Option<f32>,
2008 ) -> RichTextLayoutInfo {
2009 let (width, height) = if runs.len() == 1 {
2010 let run = &runs[0];
2011 self.measure(&run.text, run.style.font_size, available_width)
2012 } else {
2013 self.measure_rich_text(runs, available_width)
2014 };
2015 RichTextLayoutInfo {
2016 width,
2017 height,
2018 inline_boxes: Vec::new(),
2019 }
2020 }
2021
2022 fn resolve_rich_text(
2026 &self,
2027 runs: &[TextRun],
2028 available_width: Option<f32>,
2029 ) -> ResolvedParagraphLayout {
2030 let info = self.layout_rich_text(runs, available_width);
2031 ResolvedParagraphLayout {
2032 constraint_width: available_width,
2033 size: LayoutSize::new(info.width, info.height),
2034 lines: Vec::new(),
2035 inline_boxes: info.inline_boxes,
2036 clusters: Vec::new(),
2037 glyphs: Vec::new(),
2038 caret_stops: Vec::new(),
2039 selection_boxes: Vec::new(),
2040 }
2041 }
2042
2043 fn hit_test_rich(
2047 &self,
2048 runs: &[TextRun],
2049 _available_width: Option<f32>,
2050 x: f32,
2051 y: f32,
2052 ) -> usize {
2053 let text: String = runs.iter().map(|r| r.text.as_str()).collect();
2056 let font_size = runs
2057 .first()
2058 .map(|r| r.style.font_size)
2059 .unwrap_or(DEFAULT_RICH_TEXT_HIT_TEST_FONT_SIZE);
2060 self.hit_test(&text, font_size, None, x, y)
2061 }
2062
2063 fn resolve_rich_text_annotation_at_point(
2068 &self,
2069 _runs: &[TextRun],
2070 _available_width: Option<f32>,
2071 _x: f32,
2072 _y: f32,
2073 _paragraph_style: TextParagraphStyle,
2074 _annotations: &[RichTextAnnotation],
2075 ) -> Option<RichTextAnnotation> {
2076 None
2077 }
2078}
2079
2080pub struct LayoutEngine {
2103 measurer: Option<Arc<dyn TextMeasurer>>,
2104 graph_state: LayoutGraphState,
2105 next_graph_version: u64,
2106 incremental_reuse: Option<IncrementalLayoutReuseState>,
2107 active_viewport: LayoutSize,
2108 resolved_paragraphs: Mutex<HashMap<WidgetId, ResolvedParagraphLayout>>,
2109}
2110
2111impl LayoutEngine {
2112 const MAX_LAYOUT_RECURSION_DEPTH: usize = 100;
2113
2114 pub fn new() -> Self {
2119 Self {
2120 measurer: None,
2121 graph_state: LayoutGraphState::default(),
2122 next_graph_version: 1,
2123 incremental_reuse: None,
2124 active_viewport: LayoutSize::ZERO,
2125 resolved_paragraphs: Mutex::new(HashMap::new()),
2126 }
2127 }
2128
2129 pub fn with_measurer(mut self, measurer: Arc<dyn TextMeasurer>) -> Self {
2133 self.measurer = Some(measurer);
2134 self
2135 }
2136
2137 fn allocate_graph_version(&mut self) -> u64 {
2138 let version = self.next_graph_version;
2139 self.next_graph_version = self.next_graph_version.saturating_add(1);
2140 version
2141 }
2142
2143 fn refresh_graph_state(&mut self, input_nodes: &[LayoutInputNode]) {
2144 let version = self.allocate_graph_version();
2145 self.graph_state = LayoutGraphState::from_input_nodes(input_nodes, version);
2146 }
2147
2148 fn ensure_graph_state(&mut self, input_nodes: &[LayoutInputNode]) {
2149 if self.graph_state.is_empty() || !self.graph_state.matches_input_nodes(input_nodes) {
2150 self.refresh_graph_state(input_nodes);
2151 }
2152 }
2153
2154 fn validate_graph_state(&self, root: WidgetId) -> Result<()> {
2155 if let Some(err) = self.graph_state.validation.first_error() {
2156 return Err(err);
2157 }
2158 if !self.graph_state.nodes.contains_key(&root) {
2159 anyhow::bail!("[verify] missing node {:?}", root);
2160 }
2161 if !self.graph_state.roots.contains(&root)
2162 && self
2163 .graph_state
2164 .parents
2165 .get(&root)
2166 .copied()
2167 .flatten()
2168 .is_some()
2169 {
2170 anyhow::bail!("[verify] root {:?} is not a graph root", root);
2171 }
2172 if let Some(last_layout_version) = self.graph_state.last_layout_version {
2173 if last_layout_version > self.graph_state.graph_version {
2174 anyhow::bail!(
2175 "[verify] cached layout version {} exceeds graph version {}",
2176 last_layout_version,
2177 self.graph_state.graph_version
2178 );
2179 }
2180 }
2181 Ok(())
2182 }
2183
2184 pub fn update(&mut self, input_nodes: &[LayoutInputNode]) {
2189 if self.graph_state.is_empty() {
2190 self.refresh_graph_state(input_nodes);
2191 return;
2192 }
2193
2194 if self.graph_state.matches_input_nodes(input_nodes) {
2195 return;
2196 }
2197
2198 let version = self.allocate_graph_version();
2199 self.graph_state.graph_version = version;
2200 self.graph_state.update_nodes(input_nodes);
2201 }
2202
2203 pub fn rebuild(&mut self, input_nodes: &[LayoutInputNode]) -> Result<()> {
2205 self.refresh_graph_state(input_nodes);
2206 if let Some(err) = self.graph_state.validation.first_error() {
2207 return Err(err);
2208 }
2209 Ok(())
2210 }
2211
2212 pub fn verify_post_update(
2217 &self,
2218 input_nodes: &[LayoutInputNode],
2219 root: WidgetId,
2220 ) -> Result<()> {
2221 if self.graph_state.matches_input_nodes(input_nodes) {
2222 return self.validate_graph_state(root);
2223 }
2224
2225 let node_map: HashMap<WidgetId, &LayoutInputNode> =
2226 input_nodes.iter().map(|n| (n.id, n)).collect();
2227 for n in input_nodes {
2229 for child in &n.children_ids {
2230 let child_node = node_map
2231 .get(child)
2232 .ok_or_else(|| anyhow::anyhow!("[verify] child {:?} not found", child))?;
2233 if child_node.parent_id != Some(n.id) {
2234 anyhow::bail!("[verify] parent/child mismatch parent={:?} child={:?} child.parent_id={:?}", n.id, child, child_node.parent_id);
2235 }
2236 }
2237 }
2238 fn dfs(
2240 id: WidgetId,
2241 map: &HashMap<WidgetId, &LayoutInputNode>,
2242 visited: &mut HashSet<WidgetId>,
2243 stack: &mut HashSet<WidgetId>,
2244 ) -> Result<()> {
2245 if !visited.insert(id) {
2246 return Ok(());
2247 }
2248 stack.insert(id);
2249 let node = map
2250 .get(&id)
2251 .ok_or_else(|| anyhow::anyhow!("[verify] missing node {:?}", id))?;
2252 for child in &node.children_ids {
2253 if stack.contains(child) {
2254 anyhow::bail!("[verify] cycle detected at {:?} -> {:?}", id, child);
2255 }
2256 dfs(*child, map, visited, stack)?;
2257 }
2258 stack.remove(&id);
2259 Ok(())
2260 }
2261 let mut visited = HashSet::new();
2262 let mut stack = HashSet::new();
2263 dfs(root, &node_map, &mut visited, &mut stack)?;
2264 Ok(())
2265 }
2266
2267 pub fn compute_layout(
2285 &mut self,
2286 input_nodes: &[LayoutInputNode],
2287 root_node_id: WidgetId,
2288 viewport_size: LayoutSize,
2289 scroll_source: &impl ScrollDataSource,
2290 ) -> Result<LayoutSnapshot> {
2291 self.ensure_graph_state(input_nodes);
2292 self.validate_graph_state(root_node_id)?;
2293 let snapshot = self.compute_layout_constraints(
2294 input_nodes,
2295 root_node_id,
2296 viewport_size,
2297 scroll_source,
2298 )?;
2299 self.emit_scroll_diagnostics(&snapshot);
2300 self.emit_overflow_diagnostics(&snapshot);
2301 Ok(snapshot)
2302 }
2303
2304 pub fn compute_layout_constraints(
2309 &mut self,
2310 input_nodes: &[LayoutInputNode],
2311 root_node_id: WidgetId,
2312 viewport_size: LayoutSize,
2313 scroll_source: &impl ScrollDataSource,
2314 ) -> Result<LayoutSnapshot> {
2315 self.active_viewport = viewport_size;
2316 self.resolved_paragraphs.lock().unwrap().clear();
2317 self.ensure_graph_state(input_nodes);
2318 self.validate_graph_state(root_node_id)?;
2319
2320 let mut constraints = BoxConstraints::tight(viewport_size);
2322 if let Some(root) = self.graph_state.node(root_node_id) {
2323 let styled_dimension = matches!(
2325 &root.op,
2326 LayoutOp::StyledBox { style, .. }
2327 if style.width.is_some() || style.height.is_some()
2328 );
2329 if root.width.is_some() || root.height.is_some() || styled_dimension {
2330 constraints = BoxConstraints::loose(viewport_size.width, viewport_size.height)
2331 .tighten(root.width, root.height);
2332 }
2333 }
2334
2335 let mut snapshot = LayoutSnapshot::new(viewport_size);
2336 let mut measure_cache = HashMap::new();
2337 self.layout_node_constraints(
2338 root_node_id,
2339 constraints,
2340 LayoutPoint::ZERO,
2341 &mut snapshot.nodes,
2342 &mut snapshot.constraints,
2343 &mut measure_cache,
2344 scroll_source,
2345 true,
2346 0,
2347 )?;
2348
2349 let visual_location = |node_id: WidgetId| -> Option<LayoutPoint> {
2350 let mut pos = snapshot.nodes.get(&node_id)?.rect.origin;
2351 let mut current = self.graph_state.parent_of(node_id);
2352 while let Some(parent_id) = current {
2353 if let Some(parent) = self.graph_state.node(parent_id) {
2354 if let LayoutOp::Scroll { direction, .. } = &parent.op {
2355 let offset = scroll_source.get_offset(parent_id);
2356 match direction {
2357 FlexDirection::Row => pos.x -= offset,
2358 FlexDirection::Column => pos.y -= offset,
2359 }
2360 }
2361 current = self.graph_state.parent_of(parent_id);
2362 } else {
2363 break;
2364 }
2365 }
2366 Some(pos)
2367 };
2368
2369 let mut spotlight_overrides = Vec::new();
2370 for node in self.graph_state.ordered_nodes() {
2371 let LayoutOp::Spotlight { anchor, padding } = node.op else {
2372 continue;
2373 };
2374 if node.children_ids.len() != 5 {
2375 continue;
2376 }
2377
2378 let Some(bounds) = snapshot.nodes.get(&node.id).map(|geometry| geometry.rect) else {
2379 continue;
2380 };
2381 let target = snapshot.nodes.get(&anchor).and_then(|geometry| {
2382 let origin = visual_location(anchor)?;
2383 Some(LayoutRect::new(
2384 origin.x,
2385 origin.y,
2386 geometry.rect.width(),
2387 geometry.rect.height(),
2388 ))
2389 });
2390 let regions = spotlight_regions(bounds, target, padding);
2391 spotlight_overrides.push((node.children_ids.clone(), regions));
2392 }
2393
2394 let mut flyout_abs_overrides: HashMap<WidgetId, (f32, f32)> = HashMap::new();
2395 for node in self.graph_state.ordered_nodes() {
2396 if let LayoutOp::Flyout { anchor, content } = node.op {
2397 if let (Some(anchor_geom), Some(content_geom)) =
2398 (snapshot.nodes.get(&anchor), snapshot.nodes.get(&content))
2399 {
2400 if let (Some(anchor_abs), Some(content_abs)) =
2401 (visual_location(anchor), visual_location(content))
2402 {
2403 let mut min_x: f32 = 0.0;
2404 let mut min_y: f32 = 0.0;
2405 let mut max_x = content_geom.rect.width();
2406 let mut max_y = content_geom.rect.height();
2407 let mut stack = vec![content];
2408 while let Some(current) = stack.pop() {
2409 if let (Some(geometry), Some(origin)) =
2410 (snapshot.nodes.get(¤t), visual_location(current))
2411 {
2412 let relative_x = origin.x - content_abs.x;
2413 let relative_y = origin.y - content_abs.y;
2414 min_x = min_x.min(relative_x);
2415 min_y = min_y.min(relative_y);
2416 max_x = max_x.max(relative_x + geometry.rect.width());
2417 max_y = max_y.max(relative_y + geometry.rect.height());
2418 }
2419 stack.extend(self.graph_state.children_of(current).iter().copied());
2420 }
2421 let anchor_rect = LayoutRect::new(
2422 anchor_abs.x,
2423 anchor_abs.y,
2424 anchor_geom.rect.width(),
2425 anchor_geom.rect.height(),
2426 );
2427 let content_extents =
2428 LayoutRect::new(min_x, min_y, max_x - min_x, max_y - min_y);
2429 let position = flyout_root_position(
2430 snapshot.viewport_size,
2431 anchor_rect,
2432 content_extents,
2433 );
2434 flyout_abs_overrides.insert(content, (position.x, position.y));
2435 }
2436 }
2437 }
2438 }
2439
2440 for (children, regions) in spotlight_overrides {
2441 for (child_id, region) in children.into_iter().zip(regions) {
2442 self.layout_node_constraints(
2443 child_id,
2444 BoxConstraints::tight(region.size),
2445 region.origin,
2446 &mut snapshot.nodes,
2447 &mut snapshot.constraints,
2448 &mut measure_cache,
2449 scroll_source,
2450 true,
2451 0,
2452 )?;
2453 }
2454 }
2455
2456 if !flyout_abs_overrides.is_empty() {
2457 for (nid, (abs_x, abs_y)) in flyout_abs_overrides {
2458 if let Some(current) = snapshot.nodes.get(&nid) {
2459 let dx = abs_x - current.rect.origin.x;
2460 let dy = abs_y - current.rect.origin.y;
2461 let mut stack = vec![(nid, 0usize)];
2462 while let Some((current_id, depth)) = stack.pop() {
2463 if depth > Self::MAX_LAYOUT_RECURSION_DEPTH {
2464 return Err(self.layout_depth_overflow(current_id, depth));
2465 }
2466 if let Some(geometry) = snapshot.nodes.get_mut(¤t_id) {
2467 geometry.rect.origin.x += dx;
2468 geometry.rect.origin.y += dy;
2469 }
2470 for child_id in self.graph_state.children_of(current_id).iter().rev() {
2471 stack.push((*child_id, depth + 1));
2472 }
2473 }
2474 }
2475 }
2476 }
2477
2478 snapshot.paragraphs = self.resolved_paragraphs.lock().unwrap().clone();
2479 self.graph_state.mark_layout_complete();
2480 self.incremental_reuse = None;
2481
2482 Ok(snapshot)
2483 }
2484
2485 pub fn compute_layout_incremental(
2486 &mut self,
2487 input_nodes: &[LayoutInputNode],
2488 root_node_id: WidgetId,
2489 viewport_size: LayoutSize,
2490 scroll_source: &impl ScrollDataSource,
2491 previous_snapshot: &LayoutSnapshot,
2492 dirty_nodes: &HashSet<WidgetId>,
2493 ) -> Result<LayoutSnapshot> {
2494 self.ensure_graph_state(input_nodes);
2495 self.validate_graph_state(root_node_id)?;
2496
2497 let mut dirty_ancestors = HashSet::new();
2498 for node_id in dirty_nodes {
2499 let mut current = Some(*node_id);
2500 while let Some(id) = current {
2501 if !dirty_ancestors.insert(id) {
2502 break;
2503 }
2504 current = self.graph_state.parent_of(id);
2505 }
2506 }
2507 dirty_ancestors.insert(root_node_id);
2508
2509 self.incremental_reuse = Some(IncrementalLayoutReuseState {
2510 previous_snapshot: previous_snapshot.clone(),
2511 dirty_ancestors,
2512 });
2513 let result = self.compute_layout_constraints(
2514 input_nodes,
2515 root_node_id,
2516 viewport_size,
2517 scroll_source,
2518 );
2519 self.incremental_reuse = None;
2520 result
2521 }
2522
2523 fn emit_scroll_diagnostics(&self, snapshot: &LayoutSnapshot) {
2524 use fission_diagnostics::prelude as diag;
2525 let trace_scroll = std::env::var("FISSION_SCROLL_TRACE").ok().as_deref() == Some("1");
2526 for n in self.graph_state.ordered_nodes() {
2527 if let LayoutOp::Scroll { .. } = n.op {
2528 if let Some(g) = snapshot.nodes.get(&n.id) {
2529 let note = if g.rect.height() <= 0.0 {
2530 let parent_op = n
2531 .parent_id
2532 .and_then(|pid| self.graph_state.node(pid))
2533 .map(|p| format!("{:?}", p.op));
2534 let parent_constraints = n
2535 .parent_id
2536 .and_then(|pid| snapshot.constraints.get(&pid))
2537 .copied();
2538 snapshot
2539 .constraints
2540 .get(&n.id)
2541 .map(|c| {
2542 format!(
2543 "op={:?} parent={:?} parent_op={:?} parent_constraints={:?} constraints={:?}",
2544 n.op,
2545 n.parent_id,
2546 parent_op,
2547 parent_constraints,
2548 c
2549 )
2550 })
2551 } else {
2552 None
2553 };
2554 diag::emit(
2555 diag::DiagCategory::Layout,
2556 diag::DiagLevel::Debug,
2557 diag::DiagEventKind::ScrollExtent {
2558 node: n.id.as_u128(),
2559 viewport_w: g.rect.width(),
2560 viewport_h: g.rect.height(),
2561 content_w: g.content_size.width,
2562 content_h: g.content_size.height,
2563 note,
2564 },
2565 );
2566 if trace_scroll {
2567 eprintln!(
2568 "[scroll-trace] node={} viewport=({:.1},{:.1}) content=({:.1},{:.1})",
2569 n.id.as_u128(),
2570 g.rect.width(),
2571 g.rect.height(),
2572 g.content_size.width,
2573 g.content_size.height
2574 );
2575 }
2576 }
2577 }
2578 }
2579 }
2580
2581 fn emit_overflow_diagnostics(&self, snapshot: &LayoutSnapshot) {
2582 for node in self.graph_state.ordered_nodes() {
2583 let Some(geometry) = snapshot.nodes.get(&node.id) else {
2584 continue;
2585 };
2586 let overflow_x = geometry.content_size.width > geometry.rect.width() + 0.5;
2587 let overflow_y = geometry.content_size.height > geometry.rect.height() + 0.5;
2588 if !overflow_x && !overflow_y {
2589 continue;
2590 }
2591 let text = node.rich_text.is_some();
2592 diag::emit(
2593 diag::DiagCategory::Layout,
2594 if text {
2595 diag::DiagLevel::Warn
2596 } else {
2597 diag::DiagLevel::Debug
2598 },
2599 diag::DiagEventKind::LayoutOverflow {
2600 node: node.id.as_u128(),
2601 debug_name: node.debug_name.clone(),
2602 parent: node.parent_id.map(|parent| parent.as_u128()),
2603 parent_debug_name: node
2604 .parent_id
2605 .and_then(|parent| self.graph_state.node(parent))
2606 .map(|parent| parent.debug_name.clone()),
2607 parent_layout: node
2608 .parent_id
2609 .and_then(|parent| self.graph_state.node(parent))
2610 .map(|parent| format!("{:?}", parent.op)),
2611 text,
2612 min_w: snapshot
2613 .constraints
2614 .get(&node.id)
2615 .map_or(0.0, |constraints| constraints.min_w),
2616 max_w: snapshot
2617 .constraints
2618 .get(&node.id)
2619 .map(|constraints| constraints.max_w)
2620 .filter(|value| value.is_finite()),
2621 min_h: snapshot
2622 .constraints
2623 .get(&node.id)
2624 .map_or(0.0, |constraints| constraints.min_h),
2625 max_h: snapshot
2626 .constraints
2627 .get(&node.id)
2628 .map(|constraints| constraints.max_h)
2629 .filter(|value| value.is_finite()),
2630 laid_out_w: geometry.rect.width(),
2631 laid_out_h: geometry.rect.height(),
2632 content_w: geometry.content_size.width,
2633 content_h: geometry.content_size.height,
2634 },
2635 );
2636 }
2637 }
2638
2639 pub fn inspect_node(
2641 &self,
2642 snapshot: &LayoutSnapshot,
2643 node_id: WidgetId,
2644 ) -> Option<LayoutInspection> {
2645 let geometry = snapshot.nodes.get(&node_id)?;
2646 let constraints = snapshot.constraints.get(&node_id).copied()?;
2647 let measured = LayoutRect::new(
2648 geometry.rect.x(),
2649 geometry.rect.y(),
2650 geometry.content_size.width,
2651 geometry.content_size.height,
2652 );
2653 let effective_constraints = self
2654 .graph_state
2655 .node(node_id)
2656 .map(|node| {
2657 let resolved_style;
2658 let op = match &node.op {
2659 LayoutOp::StyledBox { style, .. } => {
2660 resolved_style =
2661 resolve_box_style(style, constraints, snapshot.viewport_size);
2662 &resolved_style
2663 }
2664 op => op,
2665 };
2666 match op {
2667 LayoutOp::Box {
2668 width,
2669 height,
2670 min_width,
2671 max_width,
2672 min_height,
2673 max_height,
2674 ..
2675 } => constraints
2676 .apply_min_max(*min_width, *max_width, *min_height, *max_height)
2677 .tighten(*width, *height),
2678 _ => constraints,
2679 }
2680 })
2681 .unwrap_or(constraints);
2682 let constrained_size = effective_constraints.constrain(geometry.content_size);
2683 let constrained = LayoutRect::new(
2684 geometry.rect.x(),
2685 geometry.rect.y(),
2686 constrained_size.width,
2687 constrained_size.height,
2688 );
2689 let mut clipped = geometry.rect;
2690 let mut ancestor = self.graph_state.parent_of(node_id);
2691 while let Some(ancestor_id) = ancestor {
2692 let clips = self
2693 .graph_state
2694 .node(ancestor_id)
2695 .is_some_and(|node| match &node.op {
2696 LayoutOp::Scroll { .. } | LayoutOp::Clip { .. } => true,
2697 LayoutOp::StyledBox { style, .. } => {
2698 style.overflow == fission_ir::op::Overflow::Clip
2699 }
2700 _ => false,
2701 });
2702 if clips {
2703 if let Some(ancestor_geometry) = snapshot.nodes.get(&ancestor_id) {
2704 clipped = intersect_rect(clipped, ancestor_geometry.rect);
2705 }
2706 }
2707 ancestor = self.graph_state.parent_of(ancestor_id);
2708 }
2709
2710 let mut painted = geometry.rect;
2711 let mut descendants = self.graph_state.children_of(node_id).to_vec();
2712 while let Some(descendant) = descendants.pop() {
2713 if let Some(descendant_geometry) = snapshot.nodes.get(&descendant) {
2714 painted = union_rect(painted, descendant_geometry.rect);
2715 }
2716 descendants.extend_from_slice(self.graph_state.children_of(descendant));
2717 }
2718 Some(LayoutInspection {
2719 node: node_id,
2720 measured,
2721 constraints,
2722 constrained,
2723 laid_out: geometry.rect,
2724 clipped,
2725 painted,
2726 overflow_x: geometry.content_size.width > geometry.rect.width() + 0.5,
2727 overflow_y: geometry.content_size.height > geometry.rect.height() + 0.5,
2728 })
2729 }
2730
2731 fn layout_depth_overflow(&self, node_id: WidgetId, depth: usize) -> anyhow::Error {
2732 let details = format!(
2733 "layout recursion depth {} exceeded max {} at node {}",
2734 depth,
2735 Self::MAX_LAYOUT_RECURSION_DEPTH,
2736 node_id.as_u128()
2737 );
2738 diag::emit(
2739 diag::DiagCategory::Invariants,
2740 diag::DiagLevel::Error,
2741 diag::DiagEventKind::InvariantViolation {
2742 kind: "layout_recursion_depth".into(),
2743 node: Some(node_id.as_u128()),
2744 details: details.clone(),
2745 dump_ref: None,
2746 },
2747 );
2748 anyhow::anyhow!(details)
2749 }
2750
2751 fn copy_cached_subtree(
2752 &self,
2753 node_id: WidgetId,
2754 origin: LayoutPoint,
2755 current_constraints: BoxConstraints,
2756 out: &mut HashMap<WidgetId, LayoutNodeGeometry>,
2757 constraints_out: &mut HashMap<WidgetId, BoxConstraints>,
2758 ) -> Result<Option<LayoutSize>> {
2759 let Some(reuse) = self.incremental_reuse.as_ref() else {
2760 return Ok(None);
2761 };
2762 if reuse.dirty_ancestors.contains(&node_id) {
2763 return Ok(None);
2764 }
2765
2766 let Some(previous_geometry) = reuse.previous_snapshot.nodes.get(&node_id) else {
2767 return Ok(None);
2768 };
2769 let Some(previous_constraints) = reuse.previous_snapshot.constraints.get(&node_id).copied()
2770 else {
2771 return Ok(None);
2772 };
2773 if previous_constraints != current_constraints {
2774 return Ok(None);
2775 }
2776
2777 let dx = origin.x - previous_geometry.rect.origin.x;
2778 let dy = origin.y - previous_geometry.rect.origin.y;
2779 let mut stack = vec![(node_id, 0usize)];
2780 while let Some((current_id, depth)) = stack.pop() {
2781 if depth > Self::MAX_LAYOUT_RECURSION_DEPTH {
2782 return Err(self.layout_depth_overflow(current_id, depth));
2783 }
2784 let Some(previous_geometry) = reuse.previous_snapshot.nodes.get(¤t_id) else {
2785 return Ok(None);
2786 };
2787 let Some(previous_constraints) = reuse
2788 .previous_snapshot
2789 .constraints
2790 .get(¤t_id)
2791 .copied()
2792 else {
2793 return Ok(None);
2794 };
2795
2796 let mut geometry = previous_geometry.clone();
2797 geometry.rect.origin.x += dx;
2798 geometry.rect.origin.y += dy;
2799 out.insert(current_id, geometry);
2800 constraints_out.insert(current_id, previous_constraints);
2801 if let Some(paragraph) = reuse.previous_snapshot.paragraphs.get(¤t_id) {
2802 self.resolved_paragraphs
2803 .lock()
2804 .unwrap()
2805 .insert(current_id, paragraph.clone());
2806 }
2807
2808 let children = self.graph_state.children_of(current_id);
2809 for child_id in children.iter().rev() {
2810 stack.push((*child_id, depth + 1));
2811 }
2812 }
2813
2814 Ok(Some(previous_geometry.content_size))
2815 }
2816
2817 #[allow(clippy::too_many_arguments)]
2818 fn measure_grid_intrinsic_width(
2819 &self,
2820 node_id: WidgetId,
2821 intrinsic: IntrinsicAxis,
2822 max_height: f32,
2823 out: &mut HashMap<WidgetId, LayoutNodeGeometry>,
2824 constraints_out: &mut HashMap<WidgetId, BoxConstraints>,
2825 measure_cache: &mut HashMap<MeasureCacheKey, LayoutSize>,
2826 scroll_source: &impl ScrollDataSource,
2827 depth: usize,
2828 ) -> Result<f32> {
2829 let Some(node) = self.graph_state.node(node_id) else {
2830 return Ok(0.0);
2831 };
2832 if let (Some(runs), Some(measurer)) = (&node.rich_text, &self.measurer) {
2833 return Ok(match intrinsic {
2834 IntrinsicAxis::Max => measurer.resolve_rich_text(runs, None).size.width,
2835 IntrinsicAxis::Min => runs
2836 .iter()
2837 .flat_map(|run| {
2838 run.text.split_whitespace().map(move |word| {
2839 measurer.measure(word, run.style.font_size, None).0
2840 + run.style.letter_spacing
2841 * word.chars().count().saturating_sub(1) as f32
2842 })
2843 })
2844 .fold(0.0, f32::max),
2845 });
2846 }
2847
2848 if matches!(node.op, LayoutOp::GridItem { .. } | LayoutOp::Align)
2849 && node.children_ids.len() == 1
2850 {
2851 return self.measure_grid_intrinsic_width(
2852 node.children_ids[0],
2853 intrinsic,
2854 max_height,
2855 out,
2856 constraints_out,
2857 measure_cache,
2858 scroll_source,
2859 depth + 1,
2860 );
2861 }
2862
2863 let constraints = BoxConstraints {
2864 min_w: 0.0,
2865 max_w: f32::INFINITY,
2866 min_h: 0.0,
2867 max_h: if max_height.is_finite() {
2868 max_height
2869 } else {
2870 f32::INFINITY
2871 },
2872 };
2873 Ok(self
2874 .layout_node_constraints(
2875 node_id,
2876 constraints,
2877 LayoutPoint::ZERO,
2878 out,
2879 constraints_out,
2880 measure_cache,
2881 scroll_source,
2882 false,
2883 depth + 1,
2884 )?
2885 .width)
2886 }
2887
2888 fn layout_node_constraints(
2889 &self,
2890 node_id: WidgetId,
2891 constraints: BoxConstraints,
2892 origin: LayoutPoint,
2893 out: &mut HashMap<WidgetId, LayoutNodeGeometry>,
2894 constraints_out: &mut HashMap<WidgetId, BoxConstraints>,
2895 measure_cache: &mut HashMap<MeasureCacheKey, LayoutSize>,
2896 scroll_source: &impl ScrollDataSource,
2897 record: bool,
2898 depth: usize,
2899 ) -> Result<LayoutSize> {
2900 if depth > Self::MAX_LAYOUT_RECURSION_DEPTH {
2901 return Err(self.layout_depth_overflow(node_id, depth));
2902 }
2903 if !record {
2904 let cache_key = MeasureCacheKey::new(node_id, constraints);
2905 if let Some(cached) = measure_cache.get(&cache_key).copied() {
2906 return Ok(cached);
2907 }
2908 }
2909 let node = match self.graph_state.node(node_id) {
2910 Some(node) => node,
2911 None => return Ok(LayoutSize::ZERO),
2912 };
2913
2914 if record {
2915 constraints_out.insert(node_id, constraints);
2916 }
2917
2918 if record {
2919 if let Some(reused) =
2920 self.copy_cached_subtree(node_id, origin, constraints, out, constraints_out)?
2921 {
2922 return Ok(reused);
2923 }
2924 }
2925
2926 let mut flow_children: Vec<WidgetId> = Vec::new();
2927 let mut abs_children: Vec<WidgetId> = Vec::new();
2928 for child_id in self.graph_state.children_of(node_id) {
2929 let is_absolute = matches!(
2930 self.graph_state.node(*child_id).map(|n| &n.op),
2931 Some(LayoutOp::AbsoluteFill)
2932 | Some(LayoutOp::Positioned { .. })
2933 | Some(LayoutOp::PositionedLengths { .. })
2934 );
2935 if is_absolute {
2936 abs_children.push(*child_id);
2937 } else {
2938 flow_children.push(*child_id);
2939 }
2940 }
2941 let rich_text_inline_children = node.rich_text.is_some() && !flow_children.is_empty();
2942
2943 let mut resolved_style_op = match &node.op {
2944 LayoutOp::StyledBox {
2945 style,
2946 flex_grow,
2947 flex_shrink,
2948 } => {
2949 let mut op = resolve_box_style(style, constraints, self.active_viewport);
2950 if let LayoutOp::Box {
2951 flex_grow: resolved_grow,
2952 flex_shrink: resolved_shrink,
2953 ..
2954 } = &mut op
2955 {
2956 *resolved_grow = *flex_grow;
2957 *resolved_shrink = *flex_shrink;
2958 }
2959 Some(op)
2960 }
2961 _ => None,
2962 };
2963 if let (
2964 LayoutOp::StyledBox { style, .. },
2965 Some(LayoutOp::Box {
2966 width,
2967 min_width,
2968 max_width,
2969 padding,
2970 ..
2971 }),
2972 ) = (&node.op, &mut resolved_style_op)
2973 {
2974 let needs_intrinsic_width = [
2975 style.width.as_ref(),
2976 style.min_width.as_ref(),
2977 style.max_width.as_ref(),
2978 ]
2979 .into_iter()
2980 .flatten()
2981 .any(length_requires_measurement);
2982 if needs_intrinsic_width {
2983 let mut min_content = 0.0f32;
2984 let mut max_content = 0.0f32;
2985 if let (Some(runs), Some(measurer)) = (&node.rich_text, &self.measurer) {
2986 min_content = runs
2987 .iter()
2988 .flat_map(|run| {
2989 run.text.split_whitespace().map(move |word| {
2990 measurer.measure(word, run.style.font_size, None).0
2991 + run.style.letter_spacing
2992 * word.chars().count().saturating_sub(1) as f32
2993 })
2994 })
2995 .fold(0.0, f32::max);
2996 max_content = measurer.resolve_rich_text(runs, None).size.width;
2997 }
2998 for child_id in &flow_children {
2999 min_content = min_content.max(self.measure_grid_intrinsic_width(
3000 *child_id,
3001 IntrinsicAxis::Min,
3002 constraints.max_h,
3003 out,
3004 constraints_out,
3005 measure_cache,
3006 scroll_source,
3007 depth + 1,
3008 )?);
3009 max_content = max_content.max(self.measure_grid_intrinsic_width(
3010 *child_id,
3011 IntrinsicAxis::Max,
3012 constraints.max_h,
3013 out,
3014 constraints_out,
3015 measure_cache,
3016 scroll_source,
3017 depth + 1,
3018 )?);
3019 }
3020 let horizontal_padding = padding[0] + padding[1];
3021 min_content += horizontal_padding;
3022 max_content =
3023 max_content.max(min_content - horizontal_padding) + horizontal_padding;
3024 let available = if constraints.max_w.is_finite() {
3025 constraints.max_w
3026 } else {
3027 max_content
3028 };
3029 let resolve = |length: &Option<Length>| {
3030 length.as_ref().and_then(|length| {
3031 resolve_measured_length(
3032 length,
3033 available,
3034 self.active_viewport,
3035 min_content,
3036 max_content,
3037 )
3038 })
3039 };
3040 *width = resolve(&style.width);
3041 *min_width = resolve(&style.min_width);
3042 *max_width = resolve(&style.max_width);
3043 }
3044 }
3045 let layout_op = resolved_style_op.as_ref().unwrap_or(&node.op);
3046 let box_alignment = match &node.op {
3047 LayoutOp::StyledBox { style, .. } => style.alignment,
3048 LayoutOp::Box { .. }
3052 if node.rich_text.is_some()
3053 || node.parent_id.is_some_and(|parent_id| {
3054 matches!(
3055 self.graph_state.node(parent_id).map(|parent| &parent.op),
3056 Some(LayoutOp::Flex { .. })
3057 | Some(LayoutOp::Align)
3058 | Some(LayoutOp::StyledBox { flex_grow: 0.0, .. })
3059 )
3060 }) =>
3061 {
3062 fission_ir::op::BoxAlignment::Start
3063 }
3064 LayoutOp::Box { .. } => fission_ir::op::BoxAlignment::Stretch,
3065 _ => fission_ir::op::BoxAlignment::Start,
3066 };
3067 let intrinsic_box_width = match &node.op {
3068 LayoutOp::StyledBox { style, .. } => style.width.as_ref(),
3069 _ => None,
3070 };
3071 let intrinsic_box_height = match &node.op {
3072 LayoutOp::StyledBox { style, .. } => style.height.as_ref(),
3073 _ => None,
3074 };
3075
3076 let mut content_size;
3077 let size = match layout_op {
3078 LayoutOp::Box {
3079 width,
3080 height,
3081 min_width,
3082 max_width,
3083 min_height,
3084 max_height,
3085 padding,
3086 aspect_ratio,
3087 ..
3088 } => {
3089 let mut local =
3090 constraints.apply_min_max(*min_width, *max_width, *min_height, *max_height);
3091 local = local.tighten(*width, *height);
3092 if node.rich_text.is_some() && height.is_none() {
3097 local.min_h = 0.0;
3098 local.max_h = f32::INFINITY;
3099 }
3100 if let Some(ratio) = aspect_ratio.filter(|r| *r > 0.0) {
3101 let mut target_w = *width;
3102 let mut target_h = *height;
3103
3104 if target_w.is_some() && target_h.is_none() {
3105 target_h = target_w.map(|w| w / ratio);
3106 } else if target_h.is_some() && target_w.is_none() {
3107 target_w = target_h.map(|h| h * ratio);
3108 } else if target_w.is_none() && target_h.is_none() {
3109 if local.is_width_bounded() || local.is_height_bounded() {
3110 let (mut w, mut h) = if local.is_width_bounded() {
3111 let w = local.max_w;
3112 let h = w / ratio;
3113 (w, h)
3114 } else {
3115 let h = local.max_h;
3116 let w = h * ratio;
3117 (w, h)
3118 };
3119 if local.is_width_bounded()
3120 && local.is_height_bounded()
3121 && h > local.max_h
3122 {
3123 h = local.max_h;
3124 w = h * ratio;
3125 }
3126 target_w = Some(w);
3127 target_h = Some(h);
3128 }
3129 }
3130
3131 if target_w.is_some() || target_h.is_some() {
3132 local = local.tighten(target_w, target_h);
3133 }
3134 }
3135 let mut base_child_constraints = local.deflate(*padding);
3136 if matches!(intrinsic_box_width, Some(Length::MaxContent)) {
3137 base_child_constraints.min_w = 0.0;
3138 base_child_constraints.max_w = f32::INFINITY;
3139 }
3140 if matches!(intrinsic_box_height, Some(Length::FitContent(_))) {
3146 base_child_constraints.min_h = 0.0;
3147 base_child_constraints.max_h = f32::INFINITY;
3148 }
3149 if box_alignment != fission_ir::op::BoxAlignment::Stretch {
3150 base_child_constraints.min_w = 0.0;
3151 base_child_constraints.min_h = 0.0;
3152 }
3153 let mut max_child = LayoutSize::ZERO;
3154 let mut measured_children: Vec<(WidgetId, BoxConstraints, LayoutSize)> = Vec::new();
3155 if !rich_text_inline_children {
3156 for child_id in &flow_children {
3157 let (child_width, child_height, child_max_width, child_max_height) = self
3158 .graph_state
3159 .node(*child_id)
3160 .map(|child| match &child.op {
3161 LayoutOp::Box {
3162 width,
3163 height,
3164 max_width,
3165 max_height,
3166 ..
3167 } => (*width, *height, *max_width, *max_height),
3168 LayoutOp::Scroll {
3169 width,
3170 height,
3171 max_width,
3172 max_height,
3173 ..
3174 } => (*width, *height, *max_width, *max_height),
3175 LayoutOp::Embed { width, height, .. } => {
3176 (*width, *height, None, None)
3177 }
3178 LayoutOp::StyledBox { style, .. } => {
3179 let resolved = resolve_box_style(
3180 style,
3181 base_child_constraints,
3182 self.active_viewport,
3183 );
3184 match resolved {
3185 LayoutOp::Box {
3186 width,
3187 height,
3188 max_width,
3189 max_height,
3190 ..
3191 } => (width, height, max_width, max_height),
3192 _ => unreachable!(),
3193 }
3194 }
3195 _ => (None, None, None, None),
3196 })
3197 .unwrap_or((None, None, None, None));
3198 let mut child_constraints = base_child_constraints;
3199 let child_is_align = self
3200 .graph_state
3201 .node(*child_id)
3202 .is_some_and(|child| matches!(&child.op, LayoutOp::Align));
3203 if box_alignment != fission_ir::op::BoxAlignment::Stretch && child_is_align
3210 {
3211 if width.is_none() && local.min_w < local.max_w {
3212 child_constraints.max_w = f32::INFINITY;
3213 }
3214 if height.is_none() && local.min_h < local.max_h {
3215 child_constraints.max_h = f32::INFINITY;
3216 }
3217 }
3218 if matches!(intrinsic_box_width, Some(Length::MinContent)) {
3219 let intrinsic_width = self.measure_grid_intrinsic_width(
3220 *child_id,
3221 IntrinsicAxis::Min,
3222 base_child_constraints.max_h,
3223 out,
3224 constraints_out,
3225 measure_cache,
3226 scroll_source,
3227 depth + 1,
3228 )?;
3229 child_constraints.min_w = intrinsic_width;
3230 child_constraints.max_w = intrinsic_width;
3231 }
3232 let tight_width = child_constraints.min_w == child_constraints.max_w;
3233 let stretch_width =
3237 tight_width && child_width.is_none() && child_max_width.is_none();
3238 if stretch_width {
3239 child_constraints.min_w = child_constraints.max_w;
3240 } else if tight_width
3241 && (child_width.is_some() || child_max_width.is_some())
3242 {
3243 child_constraints.min_w = 0.0;
3244 }
3245 let tight_height = child_constraints.min_h == child_constraints.max_h;
3246 let stretch_height =
3247 tight_height && child_height.is_none() && child_max_height.is_none();
3248 if stretch_height {
3249 child_constraints.min_h = child_constraints.max_h;
3250 } else if tight_height
3251 && (child_height.is_some() || child_max_height.is_some())
3252 {
3253 child_constraints.min_h = 0.0;
3254 }
3255 let child_size = self.layout_node_constraints(
3256 *child_id,
3257 child_constraints,
3258 LayoutPoint::ZERO,
3259 out,
3260 constraints_out,
3261 measure_cache,
3262 scroll_source,
3263 false,
3264 depth + 1,
3265 )?;
3266 max_child.width = max_child.width.max(child_size.width);
3267 max_child.height = max_child.height.max(child_size.height);
3268 measured_children.push((*child_id, child_constraints, child_size));
3269 }
3270 }
3271 let padded = LayoutSize::new(
3272 max_child.width + padding[0] + padding[1],
3273 max_child.height + padding[2] + padding[3],
3274 );
3275 if let LayoutOp::StyledBox { style, .. } = &node.op {
3276 let available = if constraints.max_h.is_finite() {
3277 constraints.max_h
3278 } else {
3279 padded.height
3280 };
3281 let resolve_intrinsic_height = |length: &Option<Length>| {
3282 length
3283 .as_ref()
3284 .filter(|length| length_requires_measurement(length))
3285 .and_then(|length| {
3286 resolve_measured_length(
3287 length,
3288 available,
3289 self.active_viewport,
3290 padded.height,
3291 padded.height,
3292 )
3293 })
3294 };
3295 local = local.apply_min_max(
3296 None,
3297 None,
3298 resolve_intrinsic_height(&style.min_height),
3299 resolve_intrinsic_height(&style.max_height),
3300 );
3301 local = local.tighten(None, resolve_intrinsic_height(&style.height));
3302 }
3303 let size = local.constrain(padded);
3304 if record {
3305 for (child_id, child_constraints, child_size) in measured_children {
3306 let inner_width = (size.width - padding[0] - padding[1]).max(0.0);
3307 let inner_height = (size.height - padding[2] - padding[3]).max(0.0);
3308 let offset = |available: f32, child: f32| match box_alignment {
3309 fission_ir::op::BoxAlignment::Start
3310 | fission_ir::op::BoxAlignment::Stretch => 0.0,
3311 fission_ir::op::BoxAlignment::Center => {
3312 ((available - child) / 2.0).max(0.0)
3313 }
3314 fission_ir::op::BoxAlignment::End => (available - child).max(0.0),
3315 };
3316 self.layout_node_constraints(
3317 child_id,
3318 child_constraints,
3319 LayoutPoint::new(
3320 origin.x + padding[0] + offset(inner_width, child_size.width),
3321 origin.y + padding[2] + offset(inner_height, child_size.height),
3322 ),
3323 out,
3324 constraints_out,
3325 measure_cache,
3326 scroll_source,
3327 record,
3328 depth + 1,
3329 )?;
3330 }
3331 if !abs_children.is_empty() {
3332 let abs_constraints = BoxConstraints::loose(size.width, size.height);
3333 for child_id in abs_children {
3334 self.layout_node_constraints(
3335 child_id,
3336 abs_constraints,
3337 origin,
3338 out,
3339 constraints_out,
3340 measure_cache,
3341 scroll_source,
3342 record,
3343 depth + 1,
3344 )?;
3345 }
3346 }
3347 }
3348 content_size = padded;
3349 size
3350 }
3351 LayoutOp::Flex {
3352 direction,
3353 wrap,
3354 padding,
3355 gap,
3356 align_items,
3357 justify_content,
3358 flex_grow,
3359 ..
3360 } => {
3361 let gap = gap.unwrap_or(0.0);
3362 let local = constraints.tighten(node.width, node.height);
3363 let inner = local.deflate(*padding);
3364 let is_row = matches!(direction, IrFlexDirection::Row);
3365
3366 let max_main = if is_row { inner.max_w } else { inner.max_h };
3367 let max_cross = if is_row { inner.max_h } else { inner.max_w };
3368 let min_main = if is_row { inner.min_w } else { inner.min_h };
3369 let min_cross = if is_row { inner.min_h } else { inner.min_w };
3370 let main_bounded = if is_row {
3371 inner.is_width_bounded()
3372 } else {
3373 inner.is_height_bounded()
3374 };
3375 let cross_bounded = if is_row {
3376 inner.is_height_bounded()
3377 } else {
3378 inner.is_width_bounded()
3379 };
3380
3381 if matches!(wrap, IrFlexWrap::Wrap | IrFlexWrap::WrapReverse) {
3382 let mut lines: Vec<(Vec<(WidgetId, LayoutSize, BoxConstraints)>, f32, f32)> =
3383 Vec::new();
3384 let mut line_children: Vec<(WidgetId, LayoutSize, BoxConstraints)> = Vec::new();
3385 let mut line_main = 0.0f32;
3386 let mut line_cross = 0.0f32;
3387 let mut max_line_main = 0.0f32;
3388
3389 for child_id in &flow_children {
3390 let has_explicit_main = self
3391 .graph_state
3392 .node(*child_id)
3393 .is_some_and(|child| has_explicit_main_axis_size(child, is_row));
3394 let mut child_constraints = if is_row {
3399 BoxConstraints {
3400 min_w: 0.0,
3401 max_w: if main_bounded && has_explicit_main {
3402 max_main
3403 } else {
3404 f32::INFINITY
3405 },
3406 min_h: 0.0,
3407 max_h: max_cross,
3408 }
3409 } else {
3410 BoxConstraints {
3411 min_w: 0.0,
3412 max_w: max_cross,
3413 min_h: 0.0,
3414 max_h: if main_bounded && has_explicit_main {
3415 max_main
3416 } else {
3417 f32::INFINITY
3418 },
3419 }
3420 };
3421 let mut child_size = self.layout_node_constraints(
3422 *child_id,
3423 child_constraints,
3424 LayoutPoint::ZERO,
3425 out,
3426 constraints_out,
3427 measure_cache,
3428 scroll_source,
3429 false,
3430 depth + 1,
3431 )?;
3432 let mut child_main = if is_row {
3433 child_size.width
3434 } else {
3435 child_size.height
3436 };
3437 if main_bounded && child_main > max_main {
3438 if is_row {
3439 child_constraints.max_w = max_main;
3440 } else {
3441 child_constraints.max_h = max_main;
3442 }
3443 child_size = self.layout_node_constraints(
3444 *child_id,
3445 child_constraints,
3446 LayoutPoint::ZERO,
3447 out,
3448 constraints_out,
3449 measure_cache,
3450 scroll_source,
3451 false,
3452 depth + 1,
3453 )?;
3454 child_main = if is_row {
3455 child_size.width
3456 } else {
3457 child_size.height
3458 };
3459 }
3460 let child_cross = if is_row {
3461 child_size.height
3462 } else {
3463 child_size.width
3464 };
3465 let next_main = if line_children.is_empty() {
3466 child_main
3467 } else {
3468 line_main + gap + child_main
3469 };
3470
3471 if main_bounded && !line_children.is_empty() && next_main > max_main {
3472 max_line_main = max_line_main.max(line_main);
3473 lines.push((line_children, line_main, line_cross));
3474 line_children = Vec::new();
3475 line_main = 0.0;
3476 line_cross = 0.0;
3477 }
3478
3479 if !line_children.is_empty() {
3480 line_main += gap;
3481 }
3482 line_main += child_main;
3483 line_cross = line_cross.max(child_cross);
3484 line_children.push((*child_id, child_size, child_constraints));
3485 }
3486
3487 if !line_children.is_empty() {
3488 max_line_main = max_line_main.max(line_main);
3489 lines.push((line_children, line_main, line_cross));
3490 }
3491
3492 let mut container_main = if main_bounded && *flex_grow > 0.0 {
3493 max_main
3494 } else {
3495 max_line_main
3496 };
3497 container_main = container_main.max(min_main);
3498 let total_lines_cross: f32 =
3499 lines.iter().map(|(_, _, cross)| *cross).sum::<f32>()
3500 + gap * lines.len().saturating_sub(1) as f32;
3501 let container_cross = total_lines_cross.max(min_cross);
3502 let size = if is_row {
3503 local.constrain(LayoutSize::new(
3504 container_main + padding[0] + padding[1],
3505 container_cross + padding[2] + padding[3],
3506 ))
3507 } else {
3508 local.constrain(LayoutSize::new(
3509 container_cross + padding[0] + padding[1],
3510 container_main + padding[2] + padding[3],
3511 ))
3512 };
3513
3514 let inner_main = if is_row {
3515 size.width - padding[0] - padding[1]
3516 } else {
3517 size.height - padding[2] - padding[3]
3518 };
3519 let inner_cross = if is_row {
3520 size.height - padding[2] - padding[3]
3521 } else {
3522 size.width - padding[0] - padding[1]
3523 };
3524
3525 let mut ordered_lines = lines;
3526 if matches!(wrap, IrFlexWrap::WrapReverse) {
3527 ordered_lines.reverse();
3528 }
3529
3530 let mut line_cursor = if matches!(wrap, IrFlexWrap::WrapReverse) {
3531 (inner_cross - total_lines_cross).max(0.0)
3532 } else {
3533 0.0
3534 };
3535
3536 for (line_children, line_main, line_cross) in ordered_lines {
3537 let remaining_space = (inner_main - line_main).max(0.0);
3538 let mut extra_gap = 0.0;
3539 let mut offset_main = 0.0;
3540 match justify_content {
3541 fission_ir::op::JustifyContent::Start => {}
3542 fission_ir::op::JustifyContent::End => offset_main = remaining_space,
3543 fission_ir::op::JustifyContent::Center => {
3544 offset_main = remaining_space / 2.0
3545 }
3546 fission_ir::op::JustifyContent::SpaceBetween => {
3547 if line_children.len() > 1 {
3548 extra_gap =
3549 remaining_space / (line_children.len() as f32 - 1.0);
3550 }
3551 }
3552 fission_ir::op::JustifyContent::SpaceAround => {
3553 if !line_children.is_empty() {
3554 extra_gap = remaining_space / line_children.len() as f32;
3555 offset_main = extra_gap / 2.0;
3556 }
3557 }
3558 fission_ir::op::JustifyContent::SpaceEvenly => {
3559 if !line_children.is_empty() {
3560 extra_gap =
3561 remaining_space / (line_children.len() as f32 + 1.0);
3562 offset_main = extra_gap;
3563 }
3564 }
3565 }
3566
3567 let mut cursor = offset_main;
3568 for (child_id, child_size, mut child_constraints) in line_children {
3569 let child_main = if is_row {
3570 child_size.width
3571 } else {
3572 child_size.height
3573 };
3574 let child_cross = if is_row {
3575 child_size.height
3576 } else {
3577 child_size.width
3578 };
3579 let has_explicit_cross = self
3580 .graph_state
3581 .node(child_id)
3582 .is_some_and(|child| has_explicit_cross_axis_size(child, is_row));
3583 if matches!(align_items, fission_ir::op::AlignItems::Stretch)
3584 && !has_explicit_cross
3585 {
3586 if is_row {
3587 child_constraints.min_h = line_cross;
3588 child_constraints.max_h = line_cross;
3589 } else {
3590 child_constraints.min_w = line_cross;
3591 child_constraints.max_w = line_cross;
3592 }
3593 }
3594 let cross_offset = match align_items {
3595 fission_ir::op::AlignItems::Start
3596 | fission_ir::op::AlignItems::Stretch => 0.0,
3597 fission_ir::op::AlignItems::End => {
3598 (line_cross - child_cross).max(0.0)
3599 }
3600 fission_ir::op::AlignItems::Center => {
3601 ((line_cross - child_cross) / 2.0).max(0.0)
3602 }
3603 fission_ir::op::AlignItems::Baseline => 0.0,
3604 };
3605 let child_origin = if is_row {
3606 LayoutPoint::new(
3607 origin.x + padding[0] + cursor,
3608 origin.y + padding[2] + line_cursor + cross_offset,
3609 )
3610 } else {
3611 LayoutPoint::new(
3612 origin.x + padding[0] + line_cursor + cross_offset,
3613 origin.y + padding[2] + cursor,
3614 )
3615 };
3616 self.layout_node_constraints(
3617 child_id,
3618 child_constraints,
3619 child_origin,
3620 out,
3621 constraints_out,
3622 measure_cache,
3623 scroll_source,
3624 record,
3625 depth + 1,
3626 )?;
3627 cursor += child_main + gap + extra_gap;
3628 }
3629
3630 line_cursor += line_cross + gap;
3631 }
3632
3633 if record && !abs_children.is_empty() {
3634 let abs_constraints = BoxConstraints::loose(size.width, size.height);
3635 for child_id in abs_children {
3636 self.layout_node_constraints(
3637 child_id,
3638 abs_constraints,
3639 origin,
3640 out,
3641 constraints_out,
3642 measure_cache,
3643 scroll_source,
3644 record,
3645 depth + 1,
3646 )?;
3647 }
3648 }
3649 content_size = size;
3650 size
3651 } else {
3652 struct FlexChildEntry {
3653 id: WidgetId,
3654 flex: f32,
3655 size: LayoutSize,
3656 constraints: BoxConstraints,
3657 is_flex: bool,
3658 }
3659 let mut measured: Vec<FlexChildEntry> = Vec::new();
3660 let mut total_flex = 0.0f32;
3661 let mut nonflex_main = 0.0f32;
3662 let mut max_child_cross = 0.0f32;
3663 let treat_flex_as_nonflex = !main_bounded;
3664
3665 for child_id in &flow_children {
3666 let child = match self.graph_state.node(*child_id) {
3667 Some(child) => child,
3668 None => continue,
3669 };
3670 let has_explicit_cross = has_explicit_cross_axis_size(child, is_row);
3671 let has_explicit_main = has_explicit_main_axis_size(child, is_row);
3672 let flex = child.flex_grow;
3673 if flex > 0.0 && !treat_flex_as_nonflex {
3674 total_flex += flex;
3675 measured.push(FlexChildEntry {
3676 id: *child_id,
3677 flex,
3678 size: LayoutSize::ZERO,
3679 constraints: BoxConstraints::loose(0.0, 0.0),
3680 is_flex: true,
3681 });
3682 continue;
3683 }
3684 let child_constraints = if is_row {
3685 let cross =
3686 if matches!(align_items, fission_ir::op::AlignItems::Stretch)
3687 && cross_bounded
3688 && !has_explicit_cross
3689 && child.rich_text.is_none()
3690 && !matches!(
3691 child.op,
3692 LayoutOp::Box {
3693 width: None,
3694 height: None,
3695 ..
3696 }
3697 )
3698 {
3699 BoxConstraints {
3700 min_w: 0.0,
3701 max_w: if main_bounded && has_explicit_main {
3702 max_main
3703 } else {
3704 f32::INFINITY
3705 },
3706 min_h: max_cross,
3707 max_h: max_cross,
3708 }
3709 } else {
3710 BoxConstraints {
3711 min_w: 0.0,
3712 max_w: if main_bounded && has_explicit_main {
3713 max_main
3714 } else {
3715 f32::INFINITY
3716 },
3717 min_h: 0.0,
3718 max_h: max_cross,
3719 }
3720 };
3721 cross
3722 } else {
3723 let cross =
3724 if matches!(align_items, fission_ir::op::AlignItems::Stretch)
3725 && cross_bounded
3726 && !has_explicit_cross
3727 && child.rich_text.is_none()
3728 && !matches!(
3729 child.op,
3730 LayoutOp::Box {
3731 width: None,
3732 height: None,
3733 ..
3734 }
3735 )
3736 {
3737 BoxConstraints {
3738 min_w: max_cross,
3739 max_w: max_cross,
3740 min_h: 0.0,
3741 max_h: if main_bounded && has_explicit_main {
3742 max_main
3743 } else {
3744 f32::INFINITY
3745 },
3746 }
3747 } else {
3748 BoxConstraints {
3749 min_w: 0.0,
3750 max_w: max_cross,
3751 min_h: 0.0,
3752 max_h: if main_bounded && has_explicit_main {
3753 max_main
3754 } else {
3755 f32::INFINITY
3756 },
3757 }
3758 };
3759 cross
3760 };
3761 let child_size = self.layout_node_constraints(
3762 *child_id,
3763 child_constraints,
3764 LayoutPoint::ZERO,
3765 out,
3766 constraints_out,
3767 measure_cache,
3768 scroll_source,
3769 false,
3770 depth + 1,
3771 )?;
3772 let child_main = if is_row {
3773 child_size.width
3774 } else {
3775 child_size.height
3776 };
3777 let child_cross = if is_row {
3778 child_size.height
3779 } else {
3780 child_size.width
3781 };
3782 nonflex_main += child_main;
3783 max_child_cross = max_child_cross.max(child_cross);
3784 measured.push(FlexChildEntry {
3785 id: *child_id,
3786 flex,
3787 size: child_size,
3788 constraints: child_constraints,
3789 is_flex: false,
3790 });
3791 }
3792
3793 let gap_total = gap * flow_children.len().saturating_sub(1) as f32;
3794 let remaining = if main_bounded {
3795 (max_main - nonflex_main - gap_total).max(0.0)
3796 } else {
3797 0.0
3798 };
3799
3800 for entry in measured.iter_mut().filter(|e| e.is_flex) {
3801 let flex = entry.flex;
3802 let has_explicit_cross = self
3803 .graph_state
3804 .node(entry.id)
3805 .is_some_and(|child| has_explicit_cross_axis_size(child, is_row));
3806 let allocated = if main_bounded && total_flex > 0.0 {
3807 remaining * (flex / total_flex)
3808 } else {
3809 0.0
3810 };
3811 let child_constraints = if is_row {
3812 let cross =
3813 if matches!(align_items, fission_ir::op::AlignItems::Stretch)
3814 && cross_bounded
3815 && !has_explicit_cross
3816 {
3817 BoxConstraints {
3818 min_w: allocated,
3819 max_w: allocated,
3820 min_h: max_cross,
3821 max_h: max_cross,
3822 }
3823 } else {
3824 BoxConstraints {
3825 min_w: allocated,
3826 max_w: allocated,
3827 min_h: 0.0,
3828 max_h: max_cross,
3829 }
3830 };
3831 cross
3832 } else {
3833 let cross =
3834 if matches!(align_items, fission_ir::op::AlignItems::Stretch)
3835 && cross_bounded
3836 && !has_explicit_cross
3837 {
3838 BoxConstraints {
3839 min_w: max_cross,
3840 max_w: max_cross,
3841 min_h: allocated,
3842 max_h: allocated,
3843 }
3844 } else {
3845 BoxConstraints {
3846 min_w: 0.0,
3847 max_w: max_cross,
3848 min_h: allocated,
3849 max_h: allocated,
3850 }
3851 };
3852 cross
3853 };
3854 let child_size = self.layout_node_constraints(
3855 entry.id,
3856 child_constraints,
3857 LayoutPoint::ZERO,
3858 out,
3859 constraints_out,
3860 measure_cache,
3861 scroll_source,
3862 false,
3863 depth + 1,
3864 )?;
3865 let child_cross = if is_row {
3866 child_size.height
3867 } else {
3868 child_size.width
3869 };
3870 max_child_cross = max_child_cross.max(child_cross);
3871 entry.size = child_size;
3872 entry.constraints = child_constraints;
3873 }
3874
3875 let final_children_main: f32 = measured
3876 .iter()
3877 .map(|entry| {
3878 if is_row {
3879 entry.size.width
3880 } else {
3881 entry.size.height
3882 }
3883 })
3884 .sum();
3885
3886 let mut container_main = if main_bounded && *flex_grow > 0.0 {
3887 max_main
3888 } else {
3889 final_children_main + gap_total
3890 };
3891 container_main = container_main.max(min_main);
3892
3893 if main_bounded && final_children_main + gap_total > max_main {
3894 let mut total_shrink_scaled = 0.0f32;
3896 for entry in &measured {
3897 let Some(child) = self.graph_state.node(entry.id) else {
3898 continue;
3899 };
3900 let main_size = if is_row {
3901 entry.size.width
3902 } else {
3903 entry.size.height
3904 };
3905 total_shrink_scaled += main_size * child.flex_shrink;
3906 }
3907
3908 if total_shrink_scaled > 0.0 {
3909 let overflow = (final_children_main + gap_total) - max_main;
3910 for entry in &mut measured {
3911 let Some(child) = self.graph_state.node(entry.id) else {
3912 continue;
3913 };
3914 let main_size = if is_row {
3915 entry.size.width
3916 } else {
3917 entry.size.height
3918 };
3919 let shrink_amount = (main_size * child.flex_shrink
3920 / total_shrink_scaled)
3921 * overflow;
3922 let floor = if child.flex_shrink > 0.0 {
3926 let explicit_min = match &child.op {
3928 LayoutOp::Box {
3929 min_width,
3930 min_height,
3931 height,
3932 width,
3933 ..
3934 } => {
3935 if is_row {
3936 min_width.or(*width).unwrap_or(0.0)
3937 } else {
3938 min_height.or(*height).unwrap_or(0.0)
3939 }
3940 }
3941 _ => 0.0,
3942 };
3943 explicit_min
3944 } else {
3945 main_size };
3947 let new_main = (main_size - shrink_amount).max(floor);
3948
3949 let mut child_constraints = entry.constraints;
3950 if is_row {
3951 child_constraints.min_w = new_main;
3952 child_constraints.max_w = new_main;
3953 } else {
3954 child_constraints.min_h = new_main;
3955 child_constraints.max_h = new_main;
3956 }
3957 let new_size = self.layout_node_constraints(
3958 entry.id,
3959 child_constraints,
3960 LayoutPoint::ZERO,
3961 out,
3962 constraints_out,
3963 measure_cache,
3964 scroll_source,
3965 false,
3966 depth + 1,
3967 )?;
3968 entry.size = new_size;
3969 entry.constraints = child_constraints;
3970 }
3971 }
3972 }
3973
3974 let container_cross = max_child_cross.max(min_cross);
3975 let size = if is_row {
3976 local.constrain(LayoutSize::new(
3977 container_main + padding[0] + padding[1],
3978 container_cross + padding[2] + padding[3],
3979 ))
3980 } else {
3981 local.constrain(LayoutSize::new(
3982 container_cross + padding[0] + padding[1],
3983 container_main + padding[2] + padding[3],
3984 ))
3985 };
3986
3987 let inner_main = if is_row {
3988 size.width - padding[0] - padding[1]
3989 } else {
3990 size.height - padding[2] - padding[3]
3991 };
3992 let inner_cross = if is_row {
3993 size.height - padding[2] - padding[3]
3994 } else {
3995 size.width - padding[0] - padding[1]
3996 };
3997
3998 let final_children_main: f32 = measured
3999 .iter()
4000 .map(|entry| {
4001 if is_row {
4002 entry.size.width
4003 } else {
4004 entry.size.height
4005 }
4006 })
4007 .sum();
4008
4009 let remaining_space = (inner_main - final_children_main - gap_total).max(0.0);
4010 let mut extra_gap = 0.0;
4011 let mut offset_main = 0.0;
4012 match justify_content {
4013 fission_ir::op::JustifyContent::Start => {}
4014 fission_ir::op::JustifyContent::End => offset_main = remaining_space,
4015 fission_ir::op::JustifyContent::Center => {
4016 offset_main = remaining_space / 2.0
4017 }
4018 fission_ir::op::JustifyContent::SpaceBetween => {
4019 if measured.len() > 1 {
4020 extra_gap = remaining_space / (measured.len() as f32 - 1.0);
4021 }
4022 }
4023 fission_ir::op::JustifyContent::SpaceAround => {
4024 if !measured.is_empty() {
4025 extra_gap = remaining_space / measured.len() as f32;
4026 offset_main = extra_gap / 2.0;
4027 }
4028 }
4029 fission_ir::op::JustifyContent::SpaceEvenly => {
4030 if !measured.is_empty() {
4031 extra_gap = remaining_space / (measured.len() as f32 + 1.0);
4032 offset_main = extra_gap;
4033 }
4034 }
4035 }
4036
4037 let mut cursor = offset_main;
4038 for entry in measured {
4039 let child_main = if is_row {
4040 entry.size.width
4041 } else {
4042 entry.size.height
4043 };
4044 let child_cross = if is_row {
4045 entry.size.height
4046 } else {
4047 entry.size.width
4048 };
4049 let cross_offset = match align_items {
4050 fission_ir::op::AlignItems::Start
4051 | fission_ir::op::AlignItems::Stretch => 0.0,
4052 fission_ir::op::AlignItems::End => (inner_cross - child_cross).max(0.0),
4053 fission_ir::op::AlignItems::Center => {
4054 ((inner_cross - child_cross) / 2.0).max(0.0)
4055 }
4056 fission_ir::op::AlignItems::Baseline => 0.0,
4057 };
4058 let child_origin = if is_row {
4059 LayoutPoint::new(
4060 origin.x + padding[0] + cursor,
4061 origin.y + padding[2] + cross_offset,
4062 )
4063 } else {
4064 LayoutPoint::new(
4065 origin.x + padding[0] + cross_offset,
4066 origin.y + padding[2] + cursor,
4067 )
4068 };
4069
4070 let mut child_constraints = entry.constraints;
4071 if matches!(align_items, fission_ir::op::AlignItems::Stretch) {
4072 let child_node = self.graph_state.node(entry.id);
4074 let has_explicit_cross = child_node
4075 .is_some_and(|node| has_explicit_cross_axis_size(node, is_row));
4076 let is_measured_text = child_node.is_some_and(|node| {
4080 node.rich_text.is_some()
4081 || matches!(
4082 node.op,
4083 LayoutOp::Box {
4084 width: None,
4085 height: None,
4086 ..
4087 }
4088 )
4089 });
4090 if !has_explicit_cross && !is_measured_text {
4091 if is_row {
4092 child_constraints.min_h = inner_cross;
4093 child_constraints.max_h = inner_cross;
4094 } else {
4095 child_constraints.min_w = inner_cross;
4096 child_constraints.max_w = inner_cross;
4097 }
4098 }
4099 }
4100
4101 self.layout_node_constraints(
4102 entry.id,
4103 child_constraints,
4104 child_origin,
4105 out,
4106 constraints_out,
4107 measure_cache,
4108 scroll_source,
4109 record,
4110 depth + 1,
4111 )?;
4112 cursor += child_main + gap + extra_gap;
4113 }
4114
4115 if record && !abs_children.is_empty() {
4116 let abs_constraints = BoxConstraints::loose(size.width, size.height);
4117 for child_id in abs_children {
4118 self.layout_node_constraints(
4119 child_id,
4120 abs_constraints,
4121 origin,
4122 out,
4123 constraints_out,
4124 measure_cache,
4125 scroll_source,
4126 record,
4127 depth + 1,
4128 )?;
4129 }
4130 }
4131 content_size = size;
4132 size
4133 }
4134 }
4135 LayoutOp::Grid {
4136 columns,
4137 rows,
4138 column_gap,
4139 row_gap,
4140 padding,
4141 } => {
4142 let gap_x = column_gap.unwrap_or(0.0);
4143 let gap_y = row_gap.unwrap_or(0.0);
4144 let inner = constraints.deflate(*padding);
4145 let bounded_w = inner.is_width_bounded();
4146 let bounded_h = inner.is_height_bounded();
4147 let child_count = flow_children.len();
4148 let available_w = bounded_w.then_some(inner.max_w);
4149 let available_h = bounded_h.then_some(inner.max_h);
4150 let mut expanded_columns = expand_tracks(columns, available_w, gap_x, child_count);
4151 if expanded_columns.is_empty() {
4152 expanded_columns.push(GridTrack::Auto);
4153 }
4154 let mut col_count = expanded_columns.len();
4155
4156 #[derive(Clone, Copy)]
4157 struct GridCell {
4158 id: WidgetId,
4159 row: usize,
4160 col: usize,
4161 row_span: usize,
4162 col_span: usize,
4163 }
4164
4165 let mut cell_assignments: Vec<GridCell> = Vec::new();
4166 let mut auto_row = 0;
4167 let mut auto_col = 0;
4168 let mut occupied = HashSet::<(usize, usize)>::new();
4169
4170 for child_id in &flow_children {
4171 let Some(child) = self.graph_state.node(*child_id) else {
4172 continue;
4173 };
4174 let (row_start, row_end, col_start, col_end) = if let LayoutOp::GridItem {
4175 row_start,
4176 row_end,
4177 col_start,
4178 col_end,
4179 ..
4180 } = &child.op
4181 {
4182 (*row_start, *row_end, *col_start, *col_end)
4183 } else {
4184 (
4185 GridPlacement::Auto,
4186 GridPlacement::Auto,
4187 GridPlacement::Auto,
4188 GridPlacement::Auto,
4189 )
4190 };
4191 let explicit_row = match row_start {
4192 GridPlacement::Line(line) => Some(line.max(1) as usize - 1),
4193 _ => None,
4194 };
4195 let explicit_col = match col_start {
4196 GridPlacement::Line(line) => Some(line.max(1) as usize - 1),
4197 _ => None,
4198 };
4199 let row_span = match row_end {
4200 GridPlacement::Span(span) => usize::from(span).max(1),
4201 GridPlacement::Line(line) => {
4202 let end = line.max(1) as usize - 1;
4203 end.saturating_sub(explicit_row.unwrap_or_default()).max(1)
4204 }
4205 GridPlacement::Auto => 1,
4206 };
4207 let col_span = match col_end {
4208 GridPlacement::Span(span) => usize::from(span).max(1),
4209 GridPlacement::Line(line) => {
4210 let end = line.max(1) as usize - 1;
4211 end.saturating_sub(explicit_col.unwrap_or_default()).max(1)
4212 }
4213 GridPlacement::Auto => 1,
4214 };
4215 let fits = |row: usize, col: usize, occupied: &HashSet<(usize, usize)>| {
4216 (row..row + row_span).all(|row| {
4217 (col..col + col_span).all(|col| !occupied.contains(&(row, col)))
4218 })
4219 };
4220 let (row, col) = match (explicit_row, explicit_col) {
4221 (Some(row), Some(col)) => (row, col),
4222 (Some(row), None) => {
4223 let mut col = 0;
4224 while !fits(row, col, &occupied) {
4225 col += 1;
4226 }
4227 (row, col)
4228 }
4229 (None, Some(col)) => {
4230 let mut row = 0;
4231 while !fits(row, col, &occupied) {
4232 row += 1;
4233 }
4234 (row, col)
4235 }
4236 (None, None) => {
4237 while (col_span <= col_count && auto_col + col_span > col_count)
4238 || !fits(auto_row, auto_col, &occupied)
4239 {
4240 auto_col += 1;
4241 if auto_col >= col_count {
4242 auto_col = 0;
4243 auto_row += 1;
4244 }
4245 }
4246 let placement = (auto_row, auto_col);
4247 if col_span >= col_count {
4248 auto_col = 0;
4249 auto_row += 1;
4250 } else {
4251 auto_col += col_span;
4252 if auto_col >= col_count {
4253 auto_col = 0;
4254 auto_row += 1;
4255 }
4256 }
4257 placement
4258 }
4259 };
4260 for occupied_row in row..row + row_span {
4261 for occupied_col in col..col + col_span {
4262 occupied.insert((occupied_row, occupied_col));
4263 }
4264 }
4265 cell_assignments.push(GridCell {
4266 id: *child_id,
4267 row,
4268 col,
4269 row_span,
4270 col_span,
4271 });
4272 }
4273
4274 let required_columns = cell_assignments
4275 .iter()
4276 .map(|cell| cell.col + cell.col_span)
4277 .max()
4278 .unwrap_or(1);
4279 if required_columns > col_count {
4280 expanded_columns.resize(required_columns, GridTrack::Auto);
4281 col_count = expanded_columns.len();
4282 }
4283
4284 let mut column_sizing = expanded_columns
4285 .iter()
4286 .map(|track| TrackSizing::from_track(track, available_w))
4287 .collect::<Vec<_>>();
4288
4289 for cell in &cell_assignments {
4290 let intrinsic = column_sizing[cell.col..cell.col + cell.col_span]
4291 .iter()
4292 .filter_map(|track| track.intrinsic)
4293 .fold(None, |current, axis| match (current, axis) {
4294 (Some(IntrinsicAxis::Max), _) | (_, IntrinsicAxis::Max) => {
4295 Some(IntrinsicAxis::Max)
4296 }
4297 _ => Some(IntrinsicAxis::Min),
4298 });
4299 let Some(intrinsic) = intrinsic else {
4300 continue;
4301 };
4302 let width = self.measure_grid_intrinsic_width(
4303 cell.id,
4304 intrinsic,
4305 inner.max_h,
4306 out,
4307 constraints_out,
4308 measure_cache,
4309 scroll_source,
4310 depth + 1,
4311 )?;
4312 distribute_deficit(
4313 &mut column_sizing,
4314 cell.col,
4315 cell.col_span,
4316 (width - gap_x * cell.col_span.saturating_sub(1) as f32).max(0.0),
4317 );
4318 }
4319 if let Some(available_w) = available_w {
4320 distribute_flex(&mut column_sizing, available_w, gap_x);
4321 }
4322 let col_widths = column_sizing
4323 .iter()
4324 .map(|track| track.base)
4325 .collect::<Vec<_>>();
4326
4327 let minimum_rows = cell_assignments
4328 .iter()
4329 .map(|cell| cell.row + cell.row_span)
4330 .max()
4331 .unwrap_or_else(|| (child_count + col_count - 1) / col_count)
4332 .max(1);
4333 let mut expanded_rows = expand_tracks(rows, available_h, gap_y, minimum_rows);
4334 if expanded_rows.is_empty() {
4335 expanded_rows.resize(minimum_rows, GridTrack::Auto);
4336 } else if expanded_rows.len() < minimum_rows {
4337 expanded_rows.resize(minimum_rows, GridTrack::Auto);
4338 }
4339 let mut row_sizing = expanded_rows
4340 .iter()
4341 .map(|track| TrackSizing::from_track(track, available_h))
4342 .collect::<Vec<_>>();
4343
4344 for cell in &cell_assignments {
4345 if cell.row >= row_sizing.len() || cell.col >= col_widths.len() {
4346 continue;
4347 }
4348 let col_end = (cell.col + cell.col_span).min(col_widths.len());
4349 let cell_w = col_widths[cell.col..col_end].iter().sum::<f32>()
4350 + gap_x * col_end.saturating_sub(cell.col + 1) as f32;
4351 let cell_constraints = BoxConstraints {
4352 min_w: 0.0,
4353 max_w: cell_w,
4354 min_h: 0.0,
4355 max_h: f32::INFINITY,
4356 };
4357 let child_size = self.layout_node_constraints(
4358 cell.id,
4359 cell_constraints,
4360 LayoutPoint::ZERO,
4361 out,
4362 constraints_out,
4363 measure_cache,
4364 scroll_source,
4365 false,
4366 depth + 1,
4367 )?;
4368 distribute_deficit(
4369 &mut row_sizing,
4370 cell.row,
4371 cell.row_span,
4372 (child_size.height - gap_y * cell.row_span.saturating_sub(1) as f32)
4373 .max(0.0),
4374 );
4375 }
4376 if let Some(available_h) = available_h {
4377 distribute_flex(&mut row_sizing, available_h, gap_y);
4378 }
4379 let row_heights = row_sizing
4380 .iter()
4381 .map(|track| track.base)
4382 .collect::<Vec<_>>();
4383
4384 let grid_w: f32 =
4385 col_widths.iter().sum::<f32>() + gap_x * (col_count.saturating_sub(1) as f32);
4386 let grid_h: f32 = row_heights.iter().sum::<f32>()
4387 + gap_y * (row_heights.len().saturating_sub(1) as f32);
4388 let size = constraints.constrain(LayoutSize::new(
4389 grid_w + padding[0] + padding[1],
4390 grid_h + padding[2] + padding[3],
4391 ));
4392
4393 if record {
4394 let padding_origin_x = origin.x + padding[0];
4395 let padding_origin_y = origin.y + padding[2];
4396 for cell in &cell_assignments {
4397 if cell.row >= row_heights.len() || cell.col >= col_widths.len() {
4398 continue;
4399 }
4400 let cell_x = padding_origin_x
4401 + col_widths[..cell.col].iter().sum::<f32>()
4402 + gap_x * cell.col as f32;
4403 let cell_y = padding_origin_y
4404 + row_heights[..cell.row].iter().sum::<f32>()
4405 + gap_y * cell.row as f32;
4406 let col_end = (cell.col + cell.col_span).min(col_widths.len());
4407 let row_end = (cell.row + cell.row_span).min(row_heights.len());
4408 let cell_w = col_widths[cell.col..col_end].iter().sum::<f32>()
4409 + gap_x * col_end.saturating_sub(cell.col + 1) as f32;
4410 let cell_h = row_heights[cell.row..row_end].iter().sum::<f32>()
4411 + gap_y * row_end.saturating_sub(cell.row + 1) as f32;
4412 let child_constraints = BoxConstraints {
4413 min_w: cell_w,
4414 max_w: cell_w,
4415 min_h: cell_h,
4416 max_h: cell_h,
4417 };
4418 self.layout_node_constraints(
4419 cell.id,
4420 child_constraints,
4421 LayoutPoint::new(cell_x, cell_y),
4422 out,
4423 constraints_out,
4424 measure_cache,
4425 scroll_source,
4426 record,
4427 depth + 1,
4428 )?;
4429 }
4430 }
4431
4432 if record && !abs_children.is_empty() {
4433 let abs_constraints = BoxConstraints::loose(size.width, size.height);
4434 for child_id in abs_children {
4435 self.layout_node_constraints(
4436 child_id,
4437 abs_constraints,
4438 origin,
4439 out,
4440 constraints_out,
4441 measure_cache,
4442 scroll_source,
4443 record,
4444 depth + 1,
4445 )?;
4446 }
4447 }
4448 content_size = size;
4449 size
4450 }
4451 LayoutOp::GridItem { .. } => {
4452 let mut child_size = LayoutSize::ZERO;
4453 if let Some(child_id) = node.children_ids.first() {
4454 child_size = self.layout_node_constraints(
4455 *child_id,
4456 constraints,
4457 origin,
4458 out,
4459 constraints_out,
4460 measure_cache,
4461 scroll_source,
4462 record,
4463 depth + 1,
4464 )?;
4465 }
4466 content_size = child_size;
4467 constraints.constrain(child_size)
4468 }
4469 LayoutOp::Responsive { query, cases } => {
4470 let query_width = match query {
4471 fission_ir::op::ResponsiveQuery::Viewport => self.active_viewport.width,
4472 fission_ir::op::ResponsiveQuery::Container => {
4473 if constraints.is_width_bounded() {
4474 constraints.max_w
4475 } else {
4476 self.active_viewport.width
4477 }
4478 }
4479 };
4480 let selected_index = cases
4481 .iter()
4482 .enumerate()
4483 .find_map(|(index, condition)| condition.matches(query_width).then_some(index))
4484 .unwrap_or(cases.len());
4485 let child_size = node
4486 .children_ids
4487 .get(selected_index)
4488 .map(|child_id| {
4489 self.layout_node_constraints(
4490 *child_id,
4491 constraints,
4492 origin,
4493 out,
4494 constraints_out,
4495 measure_cache,
4496 scroll_source,
4497 record,
4498 depth + 1,
4499 )
4500 })
4501 .transpose()?
4502 .unwrap_or(LayoutSize::ZERO);
4503 content_size = child_size;
4504 constraints.constrain(child_size)
4505 }
4506 LayoutOp::Scroll {
4507 direction,
4508 width,
4509 height,
4510 min_width,
4511 max_width,
4512 min_height,
4513 max_height,
4514 padding,
4515 ..
4516 } => {
4517 let mut local =
4518 constraints.apply_min_max(*min_width, *max_width, *min_height, *max_height);
4519 local = local.tighten(*width, *height);
4520 let is_horizontal = matches!(direction, FlexDirection::Row);
4521 let mut child_constraints = local.deflate(*padding);
4522 if is_horizontal {
4523 child_constraints.min_w = 0.0;
4524 child_constraints.max_w = f32::INFINITY;
4525 } else {
4526 child_constraints.min_h = 0.0;
4527 child_constraints.max_h = f32::INFINITY;
4528 }
4529 let mut child_size = LayoutSize::ZERO;
4530 if let Some(child_id) = flow_children.first() {
4531 child_size = self.layout_node_constraints(
4532 *child_id,
4533 child_constraints,
4534 LayoutPoint::ZERO,
4535 out,
4536 constraints_out,
4537 measure_cache,
4538 scroll_source,
4539 false,
4540 depth + 1,
4541 )?;
4542 }
4543 let size = local.constrain(LayoutSize::new(
4544 child_size.width + padding[0] + padding[1],
4545 child_size.height + padding[2] + padding[3],
4546 ));
4547 if record {
4548 if let Some(child_id) = flow_children.first() {
4549 self.layout_node_constraints(
4550 *child_id,
4551 child_constraints,
4552 LayoutPoint::new(origin.x + padding[0], origin.y + padding[2]),
4553 out,
4554 constraints_out,
4555 measure_cache,
4556 scroll_source,
4557 record,
4558 depth + 1,
4559 )?;
4560 }
4561 if !abs_children.is_empty() {
4562 let abs_constraints = BoxConstraints::loose(size.width, size.height);
4563 for child_id in abs_children {
4564 self.layout_node_constraints(
4565 child_id,
4566 abs_constraints,
4567 origin,
4568 out,
4569 constraints_out,
4570 measure_cache,
4571 scroll_source,
4572 record,
4573 depth + 1,
4574 )?;
4575 }
4576 }
4577 }
4578 content_size = child_size;
4579 size
4580 }
4581 LayoutOp::Align => {
4582 let child_constraints = BoxConstraints::loose(constraints.max_w, constraints.max_h);
4583 let mut child_size = LayoutSize::ZERO;
4584 if let Some(child_id) = flow_children.first() {
4585 child_size = self.layout_node_constraints(
4586 *child_id,
4587 child_constraints,
4588 LayoutPoint::ZERO,
4589 out,
4590 constraints_out,
4591 measure_cache,
4592 scroll_source,
4593 false,
4594 depth + 1,
4595 )?;
4596 }
4597 let size = if constraints.is_width_bounded() || constraints.is_height_bounded() {
4598 constraints.constrain(LayoutSize::new(
4599 if constraints.is_width_bounded() {
4600 constraints.max_w
4601 } else {
4602 child_size.width
4603 },
4604 if constraints.is_height_bounded() {
4605 constraints.max_h
4606 } else {
4607 child_size.height
4608 },
4609 ))
4610 } else {
4611 child_size
4612 };
4613 if let Some(child_id) = flow_children.first() {
4614 let dx = ((size.width - child_size.width) / 2.0).max(0.0);
4615 let dy = ((size.height - child_size.height) / 2.0).max(0.0);
4616 self.layout_node_constraints(
4617 *child_id,
4618 child_constraints,
4619 LayoutPoint::new(origin.x + dx, origin.y + dy),
4620 out,
4621 constraints_out,
4622 measure_cache,
4623 scroll_source,
4624 record,
4625 depth + 1,
4626 )?;
4627 }
4628 if record && !abs_children.is_empty() {
4629 let abs_constraints = BoxConstraints::loose(size.width, size.height);
4630 for child_id in abs_children {
4631 self.layout_node_constraints(
4632 child_id,
4633 abs_constraints,
4634 origin,
4635 out,
4636 constraints_out,
4637 measure_cache,
4638 scroll_source,
4639 record,
4640 depth + 1,
4641 )?;
4642 }
4643 }
4644 content_size = child_size;
4645 size
4646 }
4647 LayoutOp::ZStack => {
4648 let mut max_child = LayoutSize::ZERO;
4649 for child_id in &flow_children {
4650 let child_size = self.layout_node_constraints(
4651 *child_id,
4652 BoxConstraints::loose(constraints.max_w, constraints.max_h),
4653 LayoutPoint::ZERO,
4654 out,
4655 constraints_out,
4656 measure_cache,
4657 scroll_source,
4658 false,
4659 depth + 1,
4660 )?;
4661 max_child.width = max_child.width.max(child_size.width);
4662 max_child.height = max_child.height.max(child_size.height);
4663 }
4664 let size = if constraints.is_width_bounded() || constraints.is_height_bounded() {
4665 constraints.constrain(LayoutSize::new(
4666 if constraints.is_width_bounded() {
4667 constraints.max_w
4668 } else {
4669 max_child.width
4670 },
4671 if constraints.is_height_bounded() {
4672 constraints.max_h
4673 } else {
4674 max_child.height
4675 },
4676 ))
4677 } else {
4678 max_child
4679 };
4680 for child_id in &flow_children {
4681 let child_constraints = BoxConstraints::loose(size.width, size.height);
4682 let child_origin = LayoutPoint::new(origin.x, origin.y);
4683 self.layout_node_constraints(
4684 *child_id,
4685 child_constraints,
4686 child_origin,
4687 out,
4688 constraints_out,
4689 measure_cache,
4690 scroll_source,
4691 record,
4692 depth + 1,
4693 )?;
4694 }
4695 if record && !abs_children.is_empty() {
4696 let abs_constraints = BoxConstraints::loose(size.width, size.height);
4697 for child_id in abs_children {
4698 self.layout_node_constraints(
4699 child_id,
4700 abs_constraints,
4701 origin,
4702 out,
4703 constraints_out,
4704 measure_cache,
4705 scroll_source,
4706 record,
4707 depth + 1,
4708 )?;
4709 }
4710 }
4711 content_size = size;
4712 size
4713 }
4714 LayoutOp::Positioned {
4715 top,
4716 left,
4717 bottom,
4718 right,
4719 width,
4720 height,
4721 } => {
4722 let target_w = finite_or(constraints.max_w, finite_or(constraints.min_w, 0.0));
4723 let target_h = finite_or(constraints.max_h, finite_or(constraints.min_h, 0.0));
4724 let size = constraints.constrain(LayoutSize::new(target_w, target_h));
4725 let mut child_constraints = BoxConstraints::loose(size.width, size.height);
4726 if let (Some(l), Some(r)) = (left, right) {
4727 let w = (size.width - l - r).max(0.0);
4728 child_constraints = child_constraints.tighten(Some(w), None);
4729 }
4730 if let (Some(t), Some(b)) = (top, bottom) {
4731 let h = (size.height - t - b).max(0.0);
4732 child_constraints = child_constraints.tighten(None, Some(h));
4733 }
4734 child_constraints = child_constraints.tighten(*width, *height);
4735 if let Some(child_id) = node.children_ids.first() {
4736 let child_size = self.layout_node_constraints(
4737 *child_id,
4738 child_constraints,
4739 LayoutPoint::ZERO,
4740 out,
4741 constraints_out,
4742 measure_cache,
4743 scroll_source,
4744 false,
4745 depth + 1,
4746 )?;
4747 let x = left.unwrap_or_else(|| {
4748 right
4749 .map(|r| (size.width - r - child_size.width).max(0.0))
4750 .unwrap_or(0.0)
4751 });
4752 let y = top.unwrap_or_else(|| {
4753 bottom
4754 .map(|b| (size.height - b - child_size.height).max(0.0))
4755 .unwrap_or(0.0)
4756 });
4757 self.layout_node_constraints(
4758 *child_id,
4759 child_constraints,
4760 LayoutPoint::new(origin.x + x, origin.y + y),
4761 out,
4762 constraints_out,
4763 measure_cache,
4764 scroll_source,
4765 record,
4766 depth + 1,
4767 )?;
4768 }
4769 content_size = size;
4770 size
4771 }
4772 LayoutOp::PositionedLengths {
4773 top,
4774 left,
4775 bottom,
4776 right,
4777 width,
4778 height,
4779 } => {
4780 let target_w = finite_or(constraints.max_w, finite_or(constraints.min_w, 0.0));
4781 let target_h = finite_or(constraints.max_h, finite_or(constraints.min_h, 0.0));
4782 let size = constraints.constrain(LayoutSize::new(target_w, target_h));
4783 let resolve_horizontal = |length: &Option<Length>| {
4784 length
4785 .as_ref()
4786 .and_then(|length| resolve_length(length, size.width, self.active_viewport))
4787 };
4788 let resolve_vertical = |length: &Option<Length>| {
4789 length.as_ref().and_then(|length| {
4790 resolve_length(length, size.height, self.active_viewport)
4791 })
4792 };
4793 let left = resolve_horizontal(left);
4794 let top = resolve_vertical(top);
4795 let right = resolve_horizontal(right);
4796 let bottom = resolve_vertical(bottom);
4797 let width = resolve_horizontal(width);
4798 let height = resolve_vertical(height);
4799 let mut child_constraints = BoxConstraints::loose(size.width, size.height);
4800 if let (Some(left), Some(right)) = (left, right) {
4801 child_constraints =
4802 child_constraints.tighten(Some((size.width - left - right).max(0.0)), None);
4803 }
4804 if let (Some(top), Some(bottom)) = (top, bottom) {
4805 child_constraints = child_constraints
4806 .tighten(None, Some((size.height - top - bottom).max(0.0)));
4807 }
4808 child_constraints = child_constraints.tighten(width, height);
4809 if let Some(child_id) = node.children_ids.first() {
4810 let child_size = self.layout_node_constraints(
4811 *child_id,
4812 child_constraints,
4813 LayoutPoint::ZERO,
4814 out,
4815 constraints_out,
4816 measure_cache,
4817 scroll_source,
4818 false,
4819 depth + 1,
4820 )?;
4821 let x = left.unwrap_or_else(|| {
4822 right
4823 .map(|right| (size.width - right - child_size.width).max(0.0))
4824 .unwrap_or(0.0)
4825 });
4826 let y = top.unwrap_or_else(|| {
4827 bottom
4828 .map(|bottom| (size.height - bottom - child_size.height).max(0.0))
4829 .unwrap_or(0.0)
4830 });
4831 self.layout_node_constraints(
4832 *child_id,
4833 child_constraints,
4834 LayoutPoint::new(origin.x + x, origin.y + y),
4835 out,
4836 constraints_out,
4837 measure_cache,
4838 scroll_source,
4839 record,
4840 depth + 1,
4841 )?;
4842 }
4843 content_size = size;
4844 size
4845 }
4846 LayoutOp::Embed { width, height, .. } => {
4847 let local = constraints.tighten(*width, *height);
4848 let w = if local.is_width_bounded() {
4849 local.max_w
4850 } else {
4851 local.min_w
4852 };
4853 let h = if local.is_height_bounded() {
4854 local.max_h
4855 } else {
4856 local.min_h
4857 };
4858 let size = local.constrain(LayoutSize::new(w, h));
4859 content_size = size;
4860 size
4861 }
4862 LayoutOp::AbsoluteFill => {
4863 let target_w = finite_or(constraints.max_w, finite_or(constraints.min_w, 0.0));
4864 let target_h = finite_or(constraints.max_h, finite_or(constraints.min_h, 0.0));
4865 let size = constraints.constrain(LayoutSize::new(target_w, target_h));
4866 for child_id in self.graph_state.children_of(node_id) {
4867 self.layout_node_constraints(
4868 *child_id,
4869 BoxConstraints::tight(size),
4870 origin,
4871 out,
4872 constraints_out,
4873 measure_cache,
4874 scroll_source,
4875 record,
4876 depth + 1,
4877 )?;
4878 }
4879 content_size = size;
4880 size
4881 }
4882 LayoutOp::Spotlight { .. } => {
4883 let target_w = finite_or(constraints.max_w, finite_or(constraints.min_w, 0.0));
4884 let target_h = finite_or(constraints.max_h, finite_or(constraints.min_h, 0.0));
4885 let size = constraints.constrain(LayoutSize::new(target_w, target_h));
4886 for child_id in self.graph_state.children_of(node_id) {
4887 self.layout_node_constraints(
4888 *child_id,
4889 BoxConstraints::tight(LayoutSize::ZERO),
4890 origin,
4891 out,
4892 constraints_out,
4893 measure_cache,
4894 scroll_source,
4895 record,
4896 depth + 1,
4897 )?;
4898 }
4899 content_size = size;
4900 size
4901 }
4902 LayoutOp::Transform { .. }
4903 | LayoutOp::InteractiveViewport { .. }
4904 | LayoutOp::Clip { .. } => {
4905 let mut child_size = LayoutSize::ZERO;
4906 if let Some(child_id) = node.children_ids.first() {
4907 child_size = self.layout_node_constraints(
4908 *child_id,
4909 constraints,
4910 origin,
4911 out,
4912 constraints_out,
4913 measure_cache,
4914 scroll_source,
4915 record,
4916 depth + 1,
4917 )?;
4918 }
4919 content_size = child_size;
4920 constraints.constrain(child_size)
4921 }
4922 LayoutOp::Flyout { anchor, content: _ } => {
4923 let loose = BoxConstraints::loose(
4924 if constraints.is_width_bounded() {
4925 constraints.max_w
4926 } else {
4927 f32::INFINITY
4928 },
4929 if constraints.is_height_bounded() {
4930 constraints.max_h
4931 } else {
4932 f32::INFINITY
4933 },
4934 );
4935 let mut child_size = LayoutSize::ZERO;
4936 for child_id in self.graph_state.children_of(node_id) {
4937 child_size = self.layout_node_constraints(
4938 *child_id,
4939 loose,
4940 origin,
4941 out,
4942 constraints_out,
4943 measure_cache,
4944 scroll_source,
4945 false,
4946 depth + 1,
4947 )?;
4948 }
4949 if record {
4950 let anchor_rect = out.get(anchor).map(|g| g.rect);
4951 let place_x = anchor_rect.map(|r| r.x()).unwrap_or(origin.x);
4952 let place_y = anchor_rect.map(|r| r.y() + r.height()).unwrap_or(origin.y);
4953 for child_id in self.graph_state.children_of(node_id) {
4954 self.layout_node_constraints(
4955 *child_id,
4956 loose,
4957 LayoutPoint::new(place_x, place_y),
4958 out,
4959 constraints_out,
4960 measure_cache,
4961 scroll_source,
4962 record,
4963 depth + 1,
4964 )?;
4965 }
4966 }
4967 content_size = child_size;
4968 child_size
4969 }
4970 LayoutOp::StyledBox { .. } => unreachable!("styled boxes are resolved before layout"),
4971 };
4972
4973 if let Some(runs) = &node.rich_text {
4974 if let Some(measurer) = &self.measurer {
4975 let (mut text_constraints, text_padding) = match layout_op {
4976 LayoutOp::Box {
4977 width,
4978 height,
4979 min_width,
4980 max_width,
4981 min_height,
4982 max_height,
4983 padding,
4984 ..
4985 } => (
4986 constraints
4987 .apply_min_max(*min_width, *max_width, *min_height, *max_height)
4988 .tighten(*width, *height),
4989 *padding,
4990 ),
4991 _ => (constraints, [0.0; 4]),
4992 };
4993 let text_inner_constraints = text_constraints.deflate(text_padding);
4994 let intrinsic_width = match &node.op {
4995 LayoutOp::StyledBox { style, .. } => style.width.as_ref(),
4996 _ => None,
4997 };
4998 let avail_w = match intrinsic_width {
4999 Some(Length::MaxContent) => None,
5000 Some(Length::MinContent) => Some(
5001 runs.iter()
5002 .flat_map(|run| {
5003 run.text.split_whitespace().map(move |word| {
5004 measurer.measure(word, run.style.font_size, None).0
5005 + run.style.letter_spacing
5006 * word.chars().count().saturating_sub(1) as f32
5007 })
5008 })
5009 .fold(0.0, f32::max),
5010 ),
5011 _ => text_inner_constraints
5012 .is_width_bounded()
5013 .then_some(text_inner_constraints.max_w),
5014 };
5015 let rich_layout = measurer.resolve_rich_text(runs, avail_w);
5016 if record {
5017 self.resolved_paragraphs
5018 .lock()
5019 .unwrap()
5020 .insert(node_id, rich_layout.clone());
5021 }
5022 let text_content = LayoutSize::new(
5023 rich_layout.size.width + text_padding[0] + text_padding[1],
5024 rich_layout.size.height + text_padding[2] + text_padding[3],
5025 );
5026 if let LayoutOp::StyledBox { style, .. } = &node.op {
5027 let available = if constraints.max_h.is_finite() {
5028 constraints.max_h
5029 } else {
5030 text_content.height
5031 };
5032 let resolve_intrinsic_height = |length: &Option<Length>| {
5033 length
5034 .as_ref()
5035 .filter(|length| length_requires_measurement(length))
5036 .and_then(|length| {
5037 resolve_measured_length(
5038 length,
5039 available,
5040 self.active_viewport,
5041 text_content.height,
5042 text_content.height,
5043 )
5044 })
5045 };
5046 text_constraints = text_constraints.apply_min_max(
5047 None,
5048 None,
5049 resolve_intrinsic_height(&style.min_height),
5050 resolve_intrinsic_height(&style.max_height),
5051 );
5052 text_constraints =
5053 text_constraints.tighten(None, resolve_intrinsic_height(&style.height));
5054 }
5055 let measured = text_constraints.constrain(text_content);
5056 if rich_text_inline_children
5057 && rich_layout.inline_boxes.len() == flow_children.len()
5058 {
5059 let result =
5060 self.record_geometry(node_id, origin, measured, text_content, out, record);
5061 if record {
5062 let mut inline_boxes = rich_layout.inline_boxes;
5063 inline_boxes.sort_by_key(|inline_box| inline_box.id);
5064 for (child_id, inline_box) in flow_children.iter().zip(inline_boxes.iter())
5065 {
5066 self.layout_node_constraints(
5067 *child_id,
5068 BoxConstraints::tight(LayoutSize::new(
5069 inline_box.width,
5070 inline_box.height,
5071 )),
5072 LayoutPoint::new(
5073 origin.x + text_padding[0] + inline_box.x,
5074 origin.y + text_padding[2] + inline_box.y,
5075 ),
5076 out,
5077 constraints_out,
5078 measure_cache,
5079 scroll_source,
5080 record,
5081 depth + 1,
5082 )?;
5083 }
5084 }
5085 if !record {
5086 measure_cache.insert(MeasureCacheKey::new(node_id, constraints), result);
5087 }
5088 return Ok(result);
5089 }
5090 if node.children_ids.is_empty() {
5091 let result =
5092 self.record_geometry(node_id, origin, measured, text_content, out, record);
5093 if !record {
5094 measure_cache.insert(MeasureCacheKey::new(node_id, constraints), result);
5095 }
5096 return Ok(result);
5097 }
5098 content_size.width = content_size.width.max(text_content.width);
5099 content_size.height = content_size.height.max(text_content.height);
5100 }
5101 }
5102
5103 let result = self.record_geometry(node_id, origin, size, content_size, out, record);
5104 if !record {
5105 measure_cache.insert(MeasureCacheKey::new(node_id, constraints), result);
5106 }
5107 Ok(result)
5108 }
5109
5110 fn record_geometry(
5111 &self,
5112 node_id: WidgetId,
5113 origin: LayoutPoint,
5114 size: LayoutSize,
5115 content_size: LayoutSize,
5116 out: &mut HashMap<WidgetId, LayoutNodeGeometry>,
5117 record: bool,
5118 ) -> LayoutSize {
5119 let mut rect_origin = origin;
5120 let mut rect_size = size;
5121 let mut rect_content = content_size;
5122 let mut had_non_finite = false;
5123
5124 if !rect_origin.x.is_finite() {
5125 rect_origin.x = 0.0;
5126 had_non_finite = true;
5127 }
5128 if !rect_origin.y.is_finite() {
5129 rect_origin.y = 0.0;
5130 had_non_finite = true;
5131 }
5132 if !rect_size.width.is_finite() {
5133 rect_size.width = 0.0;
5134 had_non_finite = true;
5135 }
5136 if !rect_size.height.is_finite() {
5137 rect_size.height = 0.0;
5138 had_non_finite = true;
5139 }
5140 if !rect_content.width.is_finite() {
5141 rect_content.width = 0.0;
5142 had_non_finite = true;
5143 }
5144 if !rect_content.height.is_finite() {
5145 rect_content.height = 0.0;
5146 had_non_finite = true;
5147 }
5148
5149 if had_non_finite {
5150 diag::emit(
5151 diag::DiagCategory::Invariants,
5152 diag::DiagLevel::Error,
5153 diag::DiagEventKind::InvariantViolation {
5154 kind: "non_finite_layout".into(),
5155 node: Some(node_id.as_u128()),
5156 details: format!(
5157 "origin=({:.2},{:.2}) size=({:.2},{:.2}) content=({:.2},{:.2})",
5158 origin.x,
5159 origin.y,
5160 size.width,
5161 size.height,
5162 content_size.width,
5163 content_size.height
5164 ),
5165 dump_ref: None,
5166 },
5167 );
5168 }
5169
5170 if record {
5171 let rect = LayoutRect::new(
5172 rect_origin.x,
5173 rect_origin.y,
5174 rect_size.width,
5175 rect_size.height,
5176 );
5177 out.insert(
5178 node_id,
5179 LayoutNodeGeometry {
5180 rect,
5181 content_size: rect_content,
5182 },
5183 );
5184 }
5185 rect_size
5186 }
5187}