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