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 let intrinsic_box_height = match &node.op {
3057 LayoutOp::StyledBox { style, .. } => style.height.as_ref(),
3058 _ => None,
3059 };
3060
3061 let mut content_size;
3062 let size = match layout_op {
3063 LayoutOp::Box {
3064 width,
3065 height,
3066 min_width,
3067 max_width,
3068 min_height,
3069 max_height,
3070 padding,
3071 aspect_ratio,
3072 ..
3073 } => {
3074 let mut local =
3075 constraints.apply_min_max(*min_width, *max_width, *min_height, *max_height);
3076 local = local.tighten(*width, *height);
3077 if node.rich_text.is_some() && height.is_none() {
3082 local.min_h = 0.0;
3083 local.max_h = f32::INFINITY;
3084 }
3085 if let Some(ratio) = aspect_ratio.filter(|r| *r > 0.0) {
3086 let mut target_w = *width;
3087 let mut target_h = *height;
3088
3089 if target_w.is_some() && target_h.is_none() {
3090 target_h = target_w.map(|w| w / ratio);
3091 } else if target_h.is_some() && target_w.is_none() {
3092 target_w = target_h.map(|h| h * ratio);
3093 } else if target_w.is_none() && target_h.is_none() {
3094 if local.is_width_bounded() || local.is_height_bounded() {
3095 let (mut w, mut h) = if local.is_width_bounded() {
3096 let w = local.max_w;
3097 let h = w / ratio;
3098 (w, h)
3099 } else {
3100 let h = local.max_h;
3101 let w = h * ratio;
3102 (w, h)
3103 };
3104 if local.is_width_bounded()
3105 && local.is_height_bounded()
3106 && h > local.max_h
3107 {
3108 h = local.max_h;
3109 w = h * ratio;
3110 }
3111 target_w = Some(w);
3112 target_h = Some(h);
3113 }
3114 }
3115
3116 if target_w.is_some() || target_h.is_some() {
3117 local = local.tighten(target_w, target_h);
3118 }
3119 }
3120 let mut base_child_constraints = local.deflate(*padding);
3121 if matches!(intrinsic_box_width, Some(Length::MaxContent)) {
3122 base_child_constraints.min_w = 0.0;
3123 base_child_constraints.max_w = f32::INFINITY;
3124 }
3125 if matches!(intrinsic_box_height, Some(Length::FitContent(_))) {
3131 base_child_constraints.min_h = 0.0;
3132 base_child_constraints.max_h = f32::INFINITY;
3133 }
3134 if box_alignment != fission_ir::op::BoxAlignment::Stretch {
3135 base_child_constraints.min_w = 0.0;
3136 base_child_constraints.min_h = 0.0;
3137 }
3138 let mut max_child = LayoutSize::ZERO;
3139 let mut measured_children: Vec<(WidgetId, BoxConstraints, LayoutSize)> = Vec::new();
3140 if !rich_text_inline_children {
3141 for child_id in &flow_children {
3142 let (child_width, child_height, child_max_width, child_max_height) = self
3143 .graph_state
3144 .node(*child_id)
3145 .map(|child| match &child.op {
3146 LayoutOp::Box {
3147 width,
3148 height,
3149 max_width,
3150 max_height,
3151 ..
3152 } => (*width, *height, *max_width, *max_height),
3153 LayoutOp::Scroll {
3154 width,
3155 height,
3156 max_width,
3157 max_height,
3158 ..
3159 } => (*width, *height, *max_width, *max_height),
3160 LayoutOp::Embed { width, height, .. } => {
3161 (*width, *height, None, None)
3162 }
3163 LayoutOp::StyledBox { style, .. } => {
3164 let resolved = resolve_box_style(
3165 style,
3166 base_child_constraints,
3167 self.active_viewport,
3168 );
3169 match resolved {
3170 LayoutOp::Box {
3171 width,
3172 height,
3173 max_width,
3174 max_height,
3175 ..
3176 } => (width, height, max_width, max_height),
3177 _ => unreachable!(),
3178 }
3179 }
3180 _ => (None, None, None, None),
3181 })
3182 .unwrap_or((None, None, None, None));
3183 let mut child_constraints = base_child_constraints;
3184 let child_is_align = self
3185 .graph_state
3186 .node(*child_id)
3187 .is_some_and(|child| matches!(&child.op, LayoutOp::Align));
3188 if box_alignment != fission_ir::op::BoxAlignment::Stretch && child_is_align
3195 {
3196 if width.is_none() && local.min_w < local.max_w {
3197 child_constraints.max_w = f32::INFINITY;
3198 }
3199 if height.is_none() && local.min_h < local.max_h {
3200 child_constraints.max_h = f32::INFINITY;
3201 }
3202 }
3203 if matches!(intrinsic_box_width, Some(Length::MinContent)) {
3204 let intrinsic_width = self.measure_grid_intrinsic_width(
3205 *child_id,
3206 IntrinsicAxis::Min,
3207 base_child_constraints.max_h,
3208 out,
3209 constraints_out,
3210 measure_cache,
3211 scroll_source,
3212 depth + 1,
3213 )?;
3214 child_constraints.min_w = intrinsic_width;
3215 child_constraints.max_w = intrinsic_width;
3216 }
3217 let tight_width = child_constraints.min_w == child_constraints.max_w;
3218 let stretch_width =
3219 tight_width && child_width.is_none() && child_max_width.is_none();
3220 if matches!(box_alignment, fission_ir::op::BoxAlignment::Stretch)
3221 && child_width.is_none()
3222 && child_max_width.is_none()
3223 && child_constraints.max_w.is_finite()
3224 {
3225 child_constraints.min_w = child_constraints.max_w;
3226 } else if stretch_width {
3227 child_constraints.min_w = child_constraints.max_w;
3228 } else if tight_width
3229 && (child_width.is_some() || child_max_width.is_some())
3230 {
3231 child_constraints.min_w = 0.0;
3232 }
3233 let tight_height = child_constraints.min_h == child_constraints.max_h;
3234 let stretch_height =
3235 tight_height && child_height.is_none() && child_max_height.is_none();
3236 if matches!(box_alignment, fission_ir::op::BoxAlignment::Stretch)
3237 && child_height.is_none()
3238 && child_max_height.is_none()
3239 && child_constraints.max_h.is_finite()
3240 {
3241 child_constraints.min_h = child_constraints.max_h;
3242 } else if stretch_height {
3243 child_constraints.min_h = child_constraints.max_h;
3244 } else if tight_height
3245 && (child_height.is_some() || child_max_height.is_some())
3246 {
3247 child_constraints.min_h = 0.0;
3248 }
3249 let child_size = self.layout_node_constraints(
3250 *child_id,
3251 child_constraints,
3252 LayoutPoint::ZERO,
3253 out,
3254 constraints_out,
3255 measure_cache,
3256 scroll_source,
3257 false,
3258 depth + 1,
3259 )?;
3260 max_child.width = max_child.width.max(child_size.width);
3261 max_child.height = max_child.height.max(child_size.height);
3262 measured_children.push((*child_id, child_constraints, child_size));
3263 }
3264 }
3265 let padded = LayoutSize::new(
3266 max_child.width + padding[0] + padding[1],
3267 max_child.height + padding[2] + padding[3],
3268 );
3269 if let LayoutOp::StyledBox { style, .. } = &node.op {
3270 let available = if constraints.max_h.is_finite() {
3271 constraints.max_h
3272 } else {
3273 padded.height
3274 };
3275 let resolve_intrinsic_height = |length: &Option<Length>| {
3276 length
3277 .as_ref()
3278 .filter(|length| length_requires_measurement(length))
3279 .and_then(|length| {
3280 resolve_measured_length(
3281 length,
3282 available,
3283 self.active_viewport,
3284 padded.height,
3285 padded.height,
3286 )
3287 })
3288 };
3289 local = local.apply_min_max(
3290 None,
3291 None,
3292 resolve_intrinsic_height(&style.min_height),
3293 resolve_intrinsic_height(&style.max_height),
3294 );
3295 local = local.tighten(None, resolve_intrinsic_height(&style.height));
3296 }
3297 let size = local.constrain(padded);
3298 if record {
3299 for (child_id, child_constraints, child_size) in measured_children {
3300 let inner_width = (size.width - padding[0] - padding[1]).max(0.0);
3301 let inner_height = (size.height - padding[2] - padding[3]).max(0.0);
3302 let offset = |available: f32, child: f32| match box_alignment {
3303 fission_ir::op::BoxAlignment::Start
3304 | fission_ir::op::BoxAlignment::Stretch => 0.0,
3305 fission_ir::op::BoxAlignment::Center => {
3306 ((available - child) / 2.0).max(0.0)
3307 }
3308 fission_ir::op::BoxAlignment::End => (available - child).max(0.0),
3309 };
3310 self.layout_node_constraints(
3311 child_id,
3312 child_constraints,
3313 LayoutPoint::new(
3314 origin.x + padding[0] + offset(inner_width, child_size.width),
3315 origin.y + padding[2] + offset(inner_height, child_size.height),
3316 ),
3317 out,
3318 constraints_out,
3319 measure_cache,
3320 scroll_source,
3321 record,
3322 depth + 1,
3323 )?;
3324 }
3325 if !abs_children.is_empty() {
3326 let abs_constraints = BoxConstraints::loose(size.width, size.height);
3327 for child_id in abs_children {
3328 self.layout_node_constraints(
3329 child_id,
3330 abs_constraints,
3331 origin,
3332 out,
3333 constraints_out,
3334 measure_cache,
3335 scroll_source,
3336 record,
3337 depth + 1,
3338 )?;
3339 }
3340 }
3341 }
3342 content_size = padded;
3343 size
3344 }
3345 LayoutOp::Flex {
3346 direction,
3347 wrap,
3348 padding,
3349 gap,
3350 align_items,
3351 justify_content,
3352 flex_grow,
3353 ..
3354 } => {
3355 let gap = gap.unwrap_or(0.0);
3356 let local = constraints.tighten(node.width, node.height);
3357 let inner = local.deflate(*padding);
3358 let is_row = matches!(direction, IrFlexDirection::Row);
3359
3360 let max_main = if is_row { inner.max_w } else { inner.max_h };
3361 let max_cross = if is_row { inner.max_h } else { inner.max_w };
3362 let min_main = if is_row { inner.min_w } else { inner.min_h };
3363 let min_cross = if is_row { inner.min_h } else { inner.min_w };
3364 let main_bounded = if is_row {
3365 inner.is_width_bounded()
3366 } else {
3367 inner.is_height_bounded()
3368 };
3369 let cross_bounded = if is_row {
3370 inner.is_height_bounded()
3371 } else {
3372 inner.is_width_bounded()
3373 };
3374
3375 if matches!(wrap, IrFlexWrap::Wrap | IrFlexWrap::WrapReverse) {
3376 let mut lines: Vec<(Vec<(WidgetId, LayoutSize, BoxConstraints)>, f32, f32)> =
3377 Vec::new();
3378 let mut line_children: Vec<(WidgetId, LayoutSize, BoxConstraints)> = Vec::new();
3379 let mut line_main = 0.0f32;
3380 let mut line_cross = 0.0f32;
3381 let mut max_line_main = 0.0f32;
3382
3383 for child_id in &flow_children {
3384 let has_explicit_main = self
3385 .graph_state
3386 .node(*child_id)
3387 .is_some_and(|child| has_explicit_main_axis_size(child, is_row));
3388 let mut child_constraints = if is_row {
3393 BoxConstraints {
3394 min_w: 0.0,
3395 max_w: if main_bounded && has_explicit_main {
3396 max_main
3397 } else {
3398 f32::INFINITY
3399 },
3400 min_h: 0.0,
3401 max_h: max_cross,
3402 }
3403 } else {
3404 BoxConstraints {
3405 min_w: 0.0,
3406 max_w: max_cross,
3407 min_h: 0.0,
3408 max_h: if main_bounded && has_explicit_main {
3409 max_main
3410 } else {
3411 f32::INFINITY
3412 },
3413 }
3414 };
3415 let mut child_size = self.layout_node_constraints(
3416 *child_id,
3417 child_constraints,
3418 LayoutPoint::ZERO,
3419 out,
3420 constraints_out,
3421 measure_cache,
3422 scroll_source,
3423 false,
3424 depth + 1,
3425 )?;
3426 let mut child_main = if is_row {
3427 child_size.width
3428 } else {
3429 child_size.height
3430 };
3431 if main_bounded && child_main > max_main {
3432 if is_row {
3433 child_constraints.max_w = max_main;
3434 } else {
3435 child_constraints.max_h = max_main;
3436 }
3437 child_size = self.layout_node_constraints(
3438 *child_id,
3439 child_constraints,
3440 LayoutPoint::ZERO,
3441 out,
3442 constraints_out,
3443 measure_cache,
3444 scroll_source,
3445 false,
3446 depth + 1,
3447 )?;
3448 child_main = if is_row {
3449 child_size.width
3450 } else {
3451 child_size.height
3452 };
3453 }
3454 let child_cross = if is_row {
3455 child_size.height
3456 } else {
3457 child_size.width
3458 };
3459 let next_main = if line_children.is_empty() {
3460 child_main
3461 } else {
3462 line_main + gap + child_main
3463 };
3464
3465 if main_bounded && !line_children.is_empty() && next_main > max_main {
3466 max_line_main = max_line_main.max(line_main);
3467 lines.push((line_children, line_main, line_cross));
3468 line_children = Vec::new();
3469 line_main = 0.0;
3470 line_cross = 0.0;
3471 }
3472
3473 if !line_children.is_empty() {
3474 line_main += gap;
3475 }
3476 line_main += child_main;
3477 line_cross = line_cross.max(child_cross);
3478 line_children.push((*child_id, child_size, child_constraints));
3479 }
3480
3481 if !line_children.is_empty() {
3482 max_line_main = max_line_main.max(line_main);
3483 lines.push((line_children, line_main, line_cross));
3484 }
3485
3486 let mut container_main = if main_bounded && *flex_grow > 0.0 {
3487 max_main
3488 } else {
3489 max_line_main
3490 };
3491 container_main = container_main.max(min_main);
3492 let total_lines_cross: f32 =
3493 lines.iter().map(|(_, _, cross)| *cross).sum::<f32>()
3494 + gap * lines.len().saturating_sub(1) as f32;
3495 let container_cross = total_lines_cross.max(min_cross);
3496 let size = if is_row {
3497 local.constrain(LayoutSize::new(
3498 container_main + padding[0] + padding[1],
3499 container_cross + padding[2] + padding[3],
3500 ))
3501 } else {
3502 local.constrain(LayoutSize::new(
3503 container_cross + padding[0] + padding[1],
3504 container_main + padding[2] + padding[3],
3505 ))
3506 };
3507
3508 let inner_main = if is_row {
3509 size.width - padding[0] - padding[1]
3510 } else {
3511 size.height - padding[2] - padding[3]
3512 };
3513 let inner_cross = if is_row {
3514 size.height - padding[2] - padding[3]
3515 } else {
3516 size.width - padding[0] - padding[1]
3517 };
3518
3519 let mut ordered_lines = lines;
3520 if matches!(wrap, IrFlexWrap::WrapReverse) {
3521 ordered_lines.reverse();
3522 }
3523
3524 let mut line_cursor = if matches!(wrap, IrFlexWrap::WrapReverse) {
3525 (inner_cross - total_lines_cross).max(0.0)
3526 } else {
3527 0.0
3528 };
3529
3530 for (line_children, line_main, line_cross) in ordered_lines {
3531 let remaining_space = (inner_main - line_main).max(0.0);
3532 let mut extra_gap = 0.0;
3533 let mut offset_main = 0.0;
3534 match justify_content {
3535 fission_ir::op::JustifyContent::Start => {}
3536 fission_ir::op::JustifyContent::End => offset_main = remaining_space,
3537 fission_ir::op::JustifyContent::Center => {
3538 offset_main = remaining_space / 2.0
3539 }
3540 fission_ir::op::JustifyContent::SpaceBetween => {
3541 if line_children.len() > 1 {
3542 extra_gap =
3543 remaining_space / (line_children.len() as f32 - 1.0);
3544 }
3545 }
3546 fission_ir::op::JustifyContent::SpaceAround => {
3547 if !line_children.is_empty() {
3548 extra_gap = remaining_space / line_children.len() as f32;
3549 offset_main = extra_gap / 2.0;
3550 }
3551 }
3552 fission_ir::op::JustifyContent::SpaceEvenly => {
3553 if !line_children.is_empty() {
3554 extra_gap =
3555 remaining_space / (line_children.len() as f32 + 1.0);
3556 offset_main = extra_gap;
3557 }
3558 }
3559 }
3560
3561 let mut cursor = offset_main;
3562 for (child_id, child_size, mut child_constraints) in line_children {
3563 let child_main = if is_row {
3564 child_size.width
3565 } else {
3566 child_size.height
3567 };
3568 let child_cross = if is_row {
3569 child_size.height
3570 } else {
3571 child_size.width
3572 };
3573 let has_explicit_cross = self
3574 .graph_state
3575 .node(child_id)
3576 .is_some_and(|child| has_explicit_cross_axis_size(child, is_row));
3577 if matches!(align_items, fission_ir::op::AlignItems::Stretch)
3578 && !has_explicit_cross
3579 {
3580 if is_row {
3581 child_constraints.min_h = line_cross;
3582 child_constraints.max_h = line_cross;
3583 } else {
3584 child_constraints.min_w = line_cross;
3585 child_constraints.max_w = line_cross;
3586 }
3587 }
3588 let cross_offset = match align_items {
3589 fission_ir::op::AlignItems::Start
3590 | fission_ir::op::AlignItems::Stretch => 0.0,
3591 fission_ir::op::AlignItems::End => {
3592 (line_cross - child_cross).max(0.0)
3593 }
3594 fission_ir::op::AlignItems::Center => {
3595 ((line_cross - child_cross) / 2.0).max(0.0)
3596 }
3597 fission_ir::op::AlignItems::Baseline => 0.0,
3598 };
3599 let child_origin = if is_row {
3600 LayoutPoint::new(
3601 origin.x + padding[0] + cursor,
3602 origin.y + padding[2] + line_cursor + cross_offset,
3603 )
3604 } else {
3605 LayoutPoint::new(
3606 origin.x + padding[0] + line_cursor + cross_offset,
3607 origin.y + padding[2] + cursor,
3608 )
3609 };
3610 self.layout_node_constraints(
3611 child_id,
3612 child_constraints,
3613 child_origin,
3614 out,
3615 constraints_out,
3616 measure_cache,
3617 scroll_source,
3618 record,
3619 depth + 1,
3620 )?;
3621 cursor += child_main + gap + extra_gap;
3622 }
3623
3624 line_cursor += line_cross + gap;
3625 }
3626
3627 if record && !abs_children.is_empty() {
3628 let abs_constraints = BoxConstraints::loose(size.width, size.height);
3629 for child_id in abs_children {
3630 self.layout_node_constraints(
3631 child_id,
3632 abs_constraints,
3633 origin,
3634 out,
3635 constraints_out,
3636 measure_cache,
3637 scroll_source,
3638 record,
3639 depth + 1,
3640 )?;
3641 }
3642 }
3643 content_size = size;
3644 size
3645 } else {
3646 struct FlexChildEntry {
3647 id: WidgetId,
3648 flex: f32,
3649 size: LayoutSize,
3650 constraints: BoxConstraints,
3651 is_flex: bool,
3652 }
3653 let mut measured: Vec<FlexChildEntry> = Vec::new();
3654 let mut total_flex = 0.0f32;
3655 let mut nonflex_main = 0.0f32;
3656 let mut max_child_cross = 0.0f32;
3657 let treat_flex_as_nonflex = !main_bounded;
3658
3659 for child_id in &flow_children {
3660 let child = match self.graph_state.node(*child_id) {
3661 Some(child) => child,
3662 None => continue,
3663 };
3664 let has_explicit_cross = has_explicit_cross_axis_size(child, is_row);
3665 let has_explicit_main = has_explicit_main_axis_size(child, is_row);
3666 let flex = child.flex_grow;
3667 if flex > 0.0 && !treat_flex_as_nonflex {
3668 total_flex += flex;
3669 measured.push(FlexChildEntry {
3670 id: *child_id,
3671 flex,
3672 size: LayoutSize::ZERO,
3673 constraints: BoxConstraints::loose(0.0, 0.0),
3674 is_flex: true,
3675 });
3676 continue;
3677 }
3678 let child_constraints = if is_row {
3679 let cross =
3680 if matches!(align_items, fission_ir::op::AlignItems::Stretch)
3681 && cross_bounded
3682 && !has_explicit_cross
3683 && child.rich_text.is_none()
3684 && !matches!(
3685 child.op,
3686 LayoutOp::Box {
3687 width: None,
3688 height: None,
3689 ..
3690 }
3691 )
3692 {
3693 BoxConstraints {
3694 min_w: 0.0,
3695 max_w: if main_bounded && has_explicit_main {
3696 max_main
3697 } else {
3698 f32::INFINITY
3699 },
3700 min_h: max_cross,
3701 max_h: max_cross,
3702 }
3703 } else {
3704 BoxConstraints {
3705 min_w: 0.0,
3706 max_w: if main_bounded && has_explicit_main {
3707 max_main
3708 } else {
3709 f32::INFINITY
3710 },
3711 min_h: 0.0,
3712 max_h: max_cross,
3713 }
3714 };
3715 cross
3716 } else {
3717 let cross =
3718 if matches!(align_items, fission_ir::op::AlignItems::Stretch)
3719 && cross_bounded
3720 && !has_explicit_cross
3721 && child.rich_text.is_none()
3722 && !matches!(
3723 child.op,
3724 LayoutOp::Box {
3725 width: None,
3726 height: None,
3727 ..
3728 }
3729 )
3730 {
3731 BoxConstraints {
3732 min_w: max_cross,
3733 max_w: max_cross,
3734 min_h: 0.0,
3735 max_h: if main_bounded && has_explicit_main {
3736 max_main
3737 } else {
3738 f32::INFINITY
3739 },
3740 }
3741 } else {
3742 BoxConstraints {
3743 min_w: 0.0,
3744 max_w: max_cross,
3745 min_h: 0.0,
3746 max_h: if main_bounded && has_explicit_main {
3747 max_main
3748 } else {
3749 f32::INFINITY
3750 },
3751 }
3752 };
3753 cross
3754 };
3755 let child_size = self.layout_node_constraints(
3756 *child_id,
3757 child_constraints,
3758 LayoutPoint::ZERO,
3759 out,
3760 constraints_out,
3761 measure_cache,
3762 scroll_source,
3763 false,
3764 depth + 1,
3765 )?;
3766 let child_main = if is_row {
3767 child_size.width
3768 } else {
3769 child_size.height
3770 };
3771 let child_cross = if is_row {
3772 child_size.height
3773 } else {
3774 child_size.width
3775 };
3776 nonflex_main += child_main;
3777 max_child_cross = max_child_cross.max(child_cross);
3778 measured.push(FlexChildEntry {
3779 id: *child_id,
3780 flex,
3781 size: child_size,
3782 constraints: child_constraints,
3783 is_flex: false,
3784 });
3785 }
3786
3787 let gap_total = gap * flow_children.len().saturating_sub(1) as f32;
3788 let remaining = if main_bounded {
3789 (max_main - nonflex_main - gap_total).max(0.0)
3790 } else {
3791 0.0
3792 };
3793
3794 for entry in measured.iter_mut().filter(|e| e.is_flex) {
3795 let flex = entry.flex;
3796 let has_explicit_cross = self
3797 .graph_state
3798 .node(entry.id)
3799 .is_some_and(|child| has_explicit_cross_axis_size(child, is_row));
3800 let allocated = if main_bounded && total_flex > 0.0 {
3801 remaining * (flex / total_flex)
3802 } else {
3803 0.0
3804 };
3805 let child_constraints = if is_row {
3806 let cross =
3807 if matches!(align_items, fission_ir::op::AlignItems::Stretch)
3808 && cross_bounded
3809 && !has_explicit_cross
3810 {
3811 BoxConstraints {
3812 min_w: allocated,
3813 max_w: allocated,
3814 min_h: max_cross,
3815 max_h: max_cross,
3816 }
3817 } else {
3818 BoxConstraints {
3819 min_w: allocated,
3820 max_w: allocated,
3821 min_h: 0.0,
3822 max_h: max_cross,
3823 }
3824 };
3825 cross
3826 } else {
3827 let cross =
3828 if matches!(align_items, fission_ir::op::AlignItems::Stretch)
3829 && cross_bounded
3830 && !has_explicit_cross
3831 {
3832 BoxConstraints {
3833 min_w: max_cross,
3834 max_w: max_cross,
3835 min_h: allocated,
3836 max_h: allocated,
3837 }
3838 } else {
3839 BoxConstraints {
3840 min_w: 0.0,
3841 max_w: max_cross,
3842 min_h: allocated,
3843 max_h: allocated,
3844 }
3845 };
3846 cross
3847 };
3848 let child_size = self.layout_node_constraints(
3849 entry.id,
3850 child_constraints,
3851 LayoutPoint::ZERO,
3852 out,
3853 constraints_out,
3854 measure_cache,
3855 scroll_source,
3856 false,
3857 depth + 1,
3858 )?;
3859 let child_cross = if is_row {
3860 child_size.height
3861 } else {
3862 child_size.width
3863 };
3864 max_child_cross = max_child_cross.max(child_cross);
3865 entry.size = child_size;
3866 entry.constraints = child_constraints;
3867 }
3868
3869 let final_children_main: f32 = measured
3870 .iter()
3871 .map(|entry| {
3872 if is_row {
3873 entry.size.width
3874 } else {
3875 entry.size.height
3876 }
3877 })
3878 .sum();
3879
3880 let mut container_main = if main_bounded && *flex_grow > 0.0 {
3881 max_main
3882 } else {
3883 final_children_main + gap_total
3884 };
3885 container_main = container_main.max(min_main);
3886
3887 if main_bounded && final_children_main + gap_total > max_main {
3888 let mut total_shrink_scaled = 0.0f32;
3890 for entry in &measured {
3891 let Some(child) = self.graph_state.node(entry.id) else {
3892 continue;
3893 };
3894 let main_size = if is_row {
3895 entry.size.width
3896 } else {
3897 entry.size.height
3898 };
3899 total_shrink_scaled += main_size * child.flex_shrink;
3900 }
3901
3902 if total_shrink_scaled > 0.0 {
3903 let overflow = (final_children_main + gap_total) - max_main;
3904 for entry in &mut measured {
3905 let Some(child) = self.graph_state.node(entry.id) else {
3906 continue;
3907 };
3908 let main_size = if is_row {
3909 entry.size.width
3910 } else {
3911 entry.size.height
3912 };
3913 let shrink_amount = (main_size * child.flex_shrink
3914 / total_shrink_scaled)
3915 * overflow;
3916 let floor = if child.flex_shrink > 0.0 {
3920 let explicit_min = match &child.op {
3922 LayoutOp::Box {
3923 min_width,
3924 min_height,
3925 height,
3926 width,
3927 ..
3928 } => {
3929 if is_row {
3930 min_width.or(*width).unwrap_or(0.0)
3931 } else {
3932 min_height.or(*height).unwrap_or(0.0)
3933 }
3934 }
3935 _ => 0.0,
3936 };
3937 explicit_min
3938 } else {
3939 main_size };
3941 let new_main = (main_size - shrink_amount).max(floor);
3942
3943 let mut child_constraints = entry.constraints;
3944 if is_row {
3945 child_constraints.min_w = new_main;
3946 child_constraints.max_w = new_main;
3947 } else {
3948 child_constraints.min_h = new_main;
3949 child_constraints.max_h = new_main;
3950 }
3951 let new_size = self.layout_node_constraints(
3952 entry.id,
3953 child_constraints,
3954 LayoutPoint::ZERO,
3955 out,
3956 constraints_out,
3957 measure_cache,
3958 scroll_source,
3959 false,
3960 depth + 1,
3961 )?;
3962 entry.size = new_size;
3963 entry.constraints = child_constraints;
3964 }
3965 }
3966 }
3967
3968 let container_cross = max_child_cross.max(min_cross);
3969 let size = if is_row {
3970 local.constrain(LayoutSize::new(
3971 container_main + padding[0] + padding[1],
3972 container_cross + padding[2] + padding[3],
3973 ))
3974 } else {
3975 local.constrain(LayoutSize::new(
3976 container_cross + padding[0] + padding[1],
3977 container_main + padding[2] + padding[3],
3978 ))
3979 };
3980
3981 let inner_main = if is_row {
3982 size.width - padding[0] - padding[1]
3983 } else {
3984 size.height - padding[2] - padding[3]
3985 };
3986 let inner_cross = if is_row {
3987 size.height - padding[2] - padding[3]
3988 } else {
3989 size.width - padding[0] - padding[1]
3990 };
3991
3992 let final_children_main: f32 = measured
3993 .iter()
3994 .map(|entry| {
3995 if is_row {
3996 entry.size.width
3997 } else {
3998 entry.size.height
3999 }
4000 })
4001 .sum();
4002
4003 let remaining_space = (inner_main - final_children_main - gap_total).max(0.0);
4004 let mut extra_gap = 0.0;
4005 let mut offset_main = 0.0;
4006 match justify_content {
4007 fission_ir::op::JustifyContent::Start => {}
4008 fission_ir::op::JustifyContent::End => offset_main = remaining_space,
4009 fission_ir::op::JustifyContent::Center => {
4010 offset_main = remaining_space / 2.0
4011 }
4012 fission_ir::op::JustifyContent::SpaceBetween => {
4013 if measured.len() > 1 {
4014 extra_gap = remaining_space / (measured.len() as f32 - 1.0);
4015 }
4016 }
4017 fission_ir::op::JustifyContent::SpaceAround => {
4018 if !measured.is_empty() {
4019 extra_gap = remaining_space / measured.len() as f32;
4020 offset_main = extra_gap / 2.0;
4021 }
4022 }
4023 fission_ir::op::JustifyContent::SpaceEvenly => {
4024 if !measured.is_empty() {
4025 extra_gap = remaining_space / (measured.len() as f32 + 1.0);
4026 offset_main = extra_gap;
4027 }
4028 }
4029 }
4030
4031 let mut cursor = offset_main;
4032 for entry in measured {
4033 let child_main = if is_row {
4034 entry.size.width
4035 } else {
4036 entry.size.height
4037 };
4038 let child_cross = if is_row {
4039 entry.size.height
4040 } else {
4041 entry.size.width
4042 };
4043 let cross_offset = match align_items {
4044 fission_ir::op::AlignItems::Start
4045 | fission_ir::op::AlignItems::Stretch => 0.0,
4046 fission_ir::op::AlignItems::End => (inner_cross - child_cross).max(0.0),
4047 fission_ir::op::AlignItems::Center => {
4048 ((inner_cross - child_cross) / 2.0).max(0.0)
4049 }
4050 fission_ir::op::AlignItems::Baseline => 0.0,
4051 };
4052 let child_origin = if is_row {
4053 LayoutPoint::new(
4054 origin.x + padding[0] + cursor,
4055 origin.y + padding[2] + cross_offset,
4056 )
4057 } else {
4058 LayoutPoint::new(
4059 origin.x + padding[0] + cross_offset,
4060 origin.y + padding[2] + cursor,
4061 )
4062 };
4063
4064 let mut child_constraints = entry.constraints;
4065 if matches!(align_items, fission_ir::op::AlignItems::Stretch) {
4066 let child_node = self.graph_state.node(entry.id);
4068 let has_explicit_cross = child_node
4069 .is_some_and(|node| has_explicit_cross_axis_size(node, is_row));
4070 let is_measured_text = child_node.is_some_and(|node| {
4074 node.rich_text.is_some()
4075 || matches!(
4076 node.op,
4077 LayoutOp::Box {
4078 width: None,
4079 height: None,
4080 ..
4081 }
4082 )
4083 });
4084 if !has_explicit_cross && !is_measured_text {
4085 if is_row {
4086 child_constraints.min_h = inner_cross;
4087 child_constraints.max_h = inner_cross;
4088 } else {
4089 child_constraints.min_w = inner_cross;
4090 child_constraints.max_w = inner_cross;
4091 }
4092 }
4093 }
4094
4095 self.layout_node_constraints(
4096 entry.id,
4097 child_constraints,
4098 child_origin,
4099 out,
4100 constraints_out,
4101 measure_cache,
4102 scroll_source,
4103 record,
4104 depth + 1,
4105 )?;
4106 cursor += child_main + gap + extra_gap;
4107 }
4108
4109 if record && !abs_children.is_empty() {
4110 let abs_constraints = BoxConstraints::loose(size.width, size.height);
4111 for child_id in abs_children {
4112 self.layout_node_constraints(
4113 child_id,
4114 abs_constraints,
4115 origin,
4116 out,
4117 constraints_out,
4118 measure_cache,
4119 scroll_source,
4120 record,
4121 depth + 1,
4122 )?;
4123 }
4124 }
4125 content_size = size;
4126 size
4127 }
4128 }
4129 LayoutOp::Grid {
4130 columns,
4131 rows,
4132 column_gap,
4133 row_gap,
4134 padding,
4135 } => {
4136 let gap_x = column_gap.unwrap_or(0.0);
4137 let gap_y = row_gap.unwrap_or(0.0);
4138 let inner = constraints.deflate(*padding);
4139 let bounded_w = inner.is_width_bounded();
4140 let bounded_h = inner.is_height_bounded();
4141 let child_count = flow_children.len();
4142 let available_w = bounded_w.then_some(inner.max_w);
4143 let available_h = bounded_h.then_some(inner.max_h);
4144 let mut expanded_columns = expand_tracks(columns, available_w, gap_x, child_count);
4145 if expanded_columns.is_empty() {
4146 expanded_columns.push(GridTrack::Auto);
4147 }
4148 let mut col_count = expanded_columns.len();
4149
4150 #[derive(Clone, Copy)]
4151 struct GridCell {
4152 id: WidgetId,
4153 row: usize,
4154 col: usize,
4155 row_span: usize,
4156 col_span: usize,
4157 }
4158
4159 let mut cell_assignments: Vec<GridCell> = Vec::new();
4160 let mut auto_row = 0;
4161 let mut auto_col = 0;
4162 let mut occupied = HashSet::<(usize, usize)>::new();
4163
4164 for child_id in &flow_children {
4165 let Some(child) = self.graph_state.node(*child_id) else {
4166 continue;
4167 };
4168 let (row_start, row_end, col_start, col_end) = if let LayoutOp::GridItem {
4169 row_start,
4170 row_end,
4171 col_start,
4172 col_end,
4173 ..
4174 } = &child.op
4175 {
4176 (*row_start, *row_end, *col_start, *col_end)
4177 } else {
4178 (
4179 GridPlacement::Auto,
4180 GridPlacement::Auto,
4181 GridPlacement::Auto,
4182 GridPlacement::Auto,
4183 )
4184 };
4185 let explicit_row = match row_start {
4186 GridPlacement::Line(line) => Some(line.max(1) as usize - 1),
4187 _ => None,
4188 };
4189 let explicit_col = match col_start {
4190 GridPlacement::Line(line) => Some(line.max(1) as usize - 1),
4191 _ => None,
4192 };
4193 let row_span = match row_end {
4194 GridPlacement::Span(span) => usize::from(span).max(1),
4195 GridPlacement::Line(line) => {
4196 let end = line.max(1) as usize - 1;
4197 end.saturating_sub(explicit_row.unwrap_or_default()).max(1)
4198 }
4199 GridPlacement::Auto => 1,
4200 };
4201 let col_span = match col_end {
4202 GridPlacement::Span(span) => usize::from(span).max(1),
4203 GridPlacement::Line(line) => {
4204 let end = line.max(1) as usize - 1;
4205 end.saturating_sub(explicit_col.unwrap_or_default()).max(1)
4206 }
4207 GridPlacement::Auto => 1,
4208 };
4209 let fits = |row: usize, col: usize, occupied: &HashSet<(usize, usize)>| {
4210 (row..row + row_span).all(|row| {
4211 (col..col + col_span).all(|col| !occupied.contains(&(row, col)))
4212 })
4213 };
4214 let (row, col) = match (explicit_row, explicit_col) {
4215 (Some(row), Some(col)) => (row, col),
4216 (Some(row), None) => {
4217 let mut col = 0;
4218 while !fits(row, col, &occupied) {
4219 col += 1;
4220 }
4221 (row, col)
4222 }
4223 (None, Some(col)) => {
4224 let mut row = 0;
4225 while !fits(row, col, &occupied) {
4226 row += 1;
4227 }
4228 (row, col)
4229 }
4230 (None, None) => {
4231 while (col_span <= col_count && auto_col + col_span > col_count)
4232 || !fits(auto_row, auto_col, &occupied)
4233 {
4234 auto_col += 1;
4235 if auto_col >= col_count {
4236 auto_col = 0;
4237 auto_row += 1;
4238 }
4239 }
4240 let placement = (auto_row, auto_col);
4241 if col_span >= col_count {
4242 auto_col = 0;
4243 auto_row += 1;
4244 } else {
4245 auto_col += col_span;
4246 if auto_col >= col_count {
4247 auto_col = 0;
4248 auto_row += 1;
4249 }
4250 }
4251 placement
4252 }
4253 };
4254 for occupied_row in row..row + row_span {
4255 for occupied_col in col..col + col_span {
4256 occupied.insert((occupied_row, occupied_col));
4257 }
4258 }
4259 cell_assignments.push(GridCell {
4260 id: *child_id,
4261 row,
4262 col,
4263 row_span,
4264 col_span,
4265 });
4266 }
4267
4268 let required_columns = cell_assignments
4269 .iter()
4270 .map(|cell| cell.col + cell.col_span)
4271 .max()
4272 .unwrap_or(1);
4273 if required_columns > col_count {
4274 expanded_columns.resize(required_columns, GridTrack::Auto);
4275 col_count = expanded_columns.len();
4276 }
4277
4278 let mut column_sizing = expanded_columns
4279 .iter()
4280 .map(|track| TrackSizing::from_track(track, available_w))
4281 .collect::<Vec<_>>();
4282
4283 for cell in &cell_assignments {
4284 let intrinsic = column_sizing[cell.col..cell.col + cell.col_span]
4285 .iter()
4286 .filter_map(|track| track.intrinsic)
4287 .fold(None, |current, axis| match (current, axis) {
4288 (Some(IntrinsicAxis::Max), _) | (_, IntrinsicAxis::Max) => {
4289 Some(IntrinsicAxis::Max)
4290 }
4291 _ => Some(IntrinsicAxis::Min),
4292 });
4293 let Some(intrinsic) = intrinsic else {
4294 continue;
4295 };
4296 let width = self.measure_grid_intrinsic_width(
4297 cell.id,
4298 intrinsic,
4299 inner.max_h,
4300 out,
4301 constraints_out,
4302 measure_cache,
4303 scroll_source,
4304 depth + 1,
4305 )?;
4306 distribute_deficit(
4307 &mut column_sizing,
4308 cell.col,
4309 cell.col_span,
4310 (width - gap_x * cell.col_span.saturating_sub(1) as f32).max(0.0),
4311 );
4312 }
4313 if let Some(available_w) = available_w {
4314 distribute_flex(&mut column_sizing, available_w, gap_x);
4315 }
4316 let col_widths = column_sizing
4317 .iter()
4318 .map(|track| track.base)
4319 .collect::<Vec<_>>();
4320
4321 let minimum_rows = cell_assignments
4322 .iter()
4323 .map(|cell| cell.row + cell.row_span)
4324 .max()
4325 .unwrap_or_else(|| (child_count + col_count - 1) / col_count)
4326 .max(1);
4327 let mut expanded_rows = expand_tracks(rows, available_h, gap_y, minimum_rows);
4328 if expanded_rows.is_empty() {
4329 expanded_rows.resize(minimum_rows, GridTrack::Auto);
4330 } else if expanded_rows.len() < minimum_rows {
4331 expanded_rows.resize(minimum_rows, GridTrack::Auto);
4332 }
4333 let mut row_sizing = expanded_rows
4334 .iter()
4335 .map(|track| TrackSizing::from_track(track, available_h))
4336 .collect::<Vec<_>>();
4337
4338 for cell in &cell_assignments {
4339 if cell.row >= row_sizing.len() || cell.col >= col_widths.len() {
4340 continue;
4341 }
4342 let col_end = (cell.col + cell.col_span).min(col_widths.len());
4343 let cell_w = col_widths[cell.col..col_end].iter().sum::<f32>()
4344 + gap_x * col_end.saturating_sub(cell.col + 1) as f32;
4345 let cell_constraints = BoxConstraints {
4346 min_w: 0.0,
4347 max_w: cell_w,
4348 min_h: 0.0,
4349 max_h: f32::INFINITY,
4350 };
4351 let child_size = self.layout_node_constraints(
4352 cell.id,
4353 cell_constraints,
4354 LayoutPoint::ZERO,
4355 out,
4356 constraints_out,
4357 measure_cache,
4358 scroll_source,
4359 false,
4360 depth + 1,
4361 )?;
4362 distribute_deficit(
4363 &mut row_sizing,
4364 cell.row,
4365 cell.row_span,
4366 (child_size.height - gap_y * cell.row_span.saturating_sub(1) as f32)
4367 .max(0.0),
4368 );
4369 }
4370 if let Some(available_h) = available_h {
4371 distribute_flex(&mut row_sizing, available_h, gap_y);
4372 }
4373 let row_heights = row_sizing
4374 .iter()
4375 .map(|track| track.base)
4376 .collect::<Vec<_>>();
4377
4378 let grid_w: f32 =
4379 col_widths.iter().sum::<f32>() + gap_x * (col_count.saturating_sub(1) as f32);
4380 let grid_h: f32 = row_heights.iter().sum::<f32>()
4381 + gap_y * (row_heights.len().saturating_sub(1) as f32);
4382 let size = constraints.constrain(LayoutSize::new(
4383 grid_w + padding[0] + padding[1],
4384 grid_h + padding[2] + padding[3],
4385 ));
4386
4387 if record {
4388 let padding_origin_x = origin.x + padding[0];
4389 let padding_origin_y = origin.y + padding[2];
4390 for cell in &cell_assignments {
4391 if cell.row >= row_heights.len() || cell.col >= col_widths.len() {
4392 continue;
4393 }
4394 let cell_x = padding_origin_x
4395 + col_widths[..cell.col].iter().sum::<f32>()
4396 + gap_x * cell.col as f32;
4397 let cell_y = padding_origin_y
4398 + row_heights[..cell.row].iter().sum::<f32>()
4399 + gap_y * cell.row as f32;
4400 let col_end = (cell.col + cell.col_span).min(col_widths.len());
4401 let row_end = (cell.row + cell.row_span).min(row_heights.len());
4402 let cell_w = col_widths[cell.col..col_end].iter().sum::<f32>()
4403 + gap_x * col_end.saturating_sub(cell.col + 1) as f32;
4404 let cell_h = row_heights[cell.row..row_end].iter().sum::<f32>()
4405 + gap_y * row_end.saturating_sub(cell.row + 1) as f32;
4406 let child_constraints = BoxConstraints {
4407 min_w: cell_w,
4408 max_w: cell_w,
4409 min_h: cell_h,
4410 max_h: cell_h,
4411 };
4412 self.layout_node_constraints(
4413 cell.id,
4414 child_constraints,
4415 LayoutPoint::new(cell_x, cell_y),
4416 out,
4417 constraints_out,
4418 measure_cache,
4419 scroll_source,
4420 record,
4421 depth + 1,
4422 )?;
4423 }
4424 }
4425
4426 if record && !abs_children.is_empty() {
4427 let abs_constraints = BoxConstraints::loose(size.width, size.height);
4428 for child_id in abs_children {
4429 self.layout_node_constraints(
4430 child_id,
4431 abs_constraints,
4432 origin,
4433 out,
4434 constraints_out,
4435 measure_cache,
4436 scroll_source,
4437 record,
4438 depth + 1,
4439 )?;
4440 }
4441 }
4442 content_size = size;
4443 size
4444 }
4445 LayoutOp::GridItem { .. } => {
4446 let mut child_size = LayoutSize::ZERO;
4447 if let Some(child_id) = node.children_ids.first() {
4448 child_size = self.layout_node_constraints(
4449 *child_id,
4450 constraints,
4451 origin,
4452 out,
4453 constraints_out,
4454 measure_cache,
4455 scroll_source,
4456 record,
4457 depth + 1,
4458 )?;
4459 }
4460 content_size = child_size;
4461 constraints.constrain(child_size)
4462 }
4463 LayoutOp::Responsive { query, cases } => {
4464 let query_width = match query {
4465 fission_ir::op::ResponsiveQuery::Viewport => self.active_viewport.width,
4466 fission_ir::op::ResponsiveQuery::Container => {
4467 if constraints.is_width_bounded() {
4468 constraints.max_w
4469 } else {
4470 self.active_viewport.width
4471 }
4472 }
4473 };
4474 let selected_index = cases
4475 .iter()
4476 .enumerate()
4477 .find_map(|(index, condition)| condition.matches(query_width).then_some(index))
4478 .unwrap_or(cases.len());
4479 let child_size = node
4480 .children_ids
4481 .get(selected_index)
4482 .map(|child_id| {
4483 self.layout_node_constraints(
4484 *child_id,
4485 constraints,
4486 origin,
4487 out,
4488 constraints_out,
4489 measure_cache,
4490 scroll_source,
4491 record,
4492 depth + 1,
4493 )
4494 })
4495 .transpose()?
4496 .unwrap_or(LayoutSize::ZERO);
4497 content_size = child_size;
4498 constraints.constrain(child_size)
4499 }
4500 LayoutOp::Scroll {
4501 direction,
4502 width,
4503 height,
4504 min_width,
4505 max_width,
4506 min_height,
4507 max_height,
4508 padding,
4509 ..
4510 } => {
4511 let mut local =
4512 constraints.apply_min_max(*min_width, *max_width, *min_height, *max_height);
4513 local = local.tighten(*width, *height);
4514 let is_horizontal = matches!(direction, FlexDirection::Row);
4515 let mut child_constraints = local.deflate(*padding);
4516 if is_horizontal {
4517 child_constraints.min_w = 0.0;
4518 child_constraints.max_w = f32::INFINITY;
4519 } else {
4520 child_constraints.min_h = 0.0;
4521 child_constraints.max_h = f32::INFINITY;
4522 }
4523 let mut child_size = LayoutSize::ZERO;
4524 if let Some(child_id) = flow_children.first() {
4525 child_size = self.layout_node_constraints(
4526 *child_id,
4527 child_constraints,
4528 LayoutPoint::ZERO,
4529 out,
4530 constraints_out,
4531 measure_cache,
4532 scroll_source,
4533 false,
4534 depth + 1,
4535 )?;
4536 }
4537 let size = local.constrain(LayoutSize::new(
4538 child_size.width + padding[0] + padding[1],
4539 child_size.height + padding[2] + padding[3],
4540 ));
4541 if record {
4542 if let Some(child_id) = flow_children.first() {
4543 self.layout_node_constraints(
4544 *child_id,
4545 child_constraints,
4546 LayoutPoint::new(origin.x + padding[0], origin.y + padding[2]),
4547 out,
4548 constraints_out,
4549 measure_cache,
4550 scroll_source,
4551 record,
4552 depth + 1,
4553 )?;
4554 }
4555 if !abs_children.is_empty() {
4556 let abs_constraints = BoxConstraints::loose(size.width, size.height);
4557 for child_id in abs_children {
4558 self.layout_node_constraints(
4559 child_id,
4560 abs_constraints,
4561 origin,
4562 out,
4563 constraints_out,
4564 measure_cache,
4565 scroll_source,
4566 record,
4567 depth + 1,
4568 )?;
4569 }
4570 }
4571 }
4572 content_size = child_size;
4573 size
4574 }
4575 LayoutOp::Align => {
4576 let child_constraints = BoxConstraints::loose(constraints.max_w, constraints.max_h);
4577 let mut child_size = LayoutSize::ZERO;
4578 if let Some(child_id) = flow_children.first() {
4579 child_size = self.layout_node_constraints(
4580 *child_id,
4581 child_constraints,
4582 LayoutPoint::ZERO,
4583 out,
4584 constraints_out,
4585 measure_cache,
4586 scroll_source,
4587 false,
4588 depth + 1,
4589 )?;
4590 }
4591 let size = if constraints.is_width_bounded() || constraints.is_height_bounded() {
4592 constraints.constrain(LayoutSize::new(
4593 if constraints.is_width_bounded() {
4594 constraints.max_w
4595 } else {
4596 child_size.width
4597 },
4598 if constraints.is_height_bounded() {
4599 constraints.max_h
4600 } else {
4601 child_size.height
4602 },
4603 ))
4604 } else {
4605 child_size
4606 };
4607 if let Some(child_id) = flow_children.first() {
4608 let dx = ((size.width - child_size.width) / 2.0).max(0.0);
4609 let dy = ((size.height - child_size.height) / 2.0).max(0.0);
4610 self.layout_node_constraints(
4611 *child_id,
4612 child_constraints,
4613 LayoutPoint::new(origin.x + dx, origin.y + dy),
4614 out,
4615 constraints_out,
4616 measure_cache,
4617 scroll_source,
4618 record,
4619 depth + 1,
4620 )?;
4621 }
4622 if record && !abs_children.is_empty() {
4623 let abs_constraints = BoxConstraints::loose(size.width, size.height);
4624 for child_id in abs_children {
4625 self.layout_node_constraints(
4626 child_id,
4627 abs_constraints,
4628 origin,
4629 out,
4630 constraints_out,
4631 measure_cache,
4632 scroll_source,
4633 record,
4634 depth + 1,
4635 )?;
4636 }
4637 }
4638 content_size = child_size;
4639 size
4640 }
4641 LayoutOp::ZStack => {
4642 let mut max_child = LayoutSize::ZERO;
4643 for child_id in &flow_children {
4644 let child_size = self.layout_node_constraints(
4645 *child_id,
4646 BoxConstraints::loose(constraints.max_w, constraints.max_h),
4647 LayoutPoint::ZERO,
4648 out,
4649 constraints_out,
4650 measure_cache,
4651 scroll_source,
4652 false,
4653 depth + 1,
4654 )?;
4655 max_child.width = max_child.width.max(child_size.width);
4656 max_child.height = max_child.height.max(child_size.height);
4657 }
4658 let size = if constraints.is_width_bounded() || constraints.is_height_bounded() {
4659 constraints.constrain(LayoutSize::new(
4660 if constraints.is_width_bounded() {
4661 constraints.max_w
4662 } else {
4663 max_child.width
4664 },
4665 if constraints.is_height_bounded() {
4666 constraints.max_h
4667 } else {
4668 max_child.height
4669 },
4670 ))
4671 } else {
4672 max_child
4673 };
4674 for child_id in &flow_children {
4675 let child_constraints = BoxConstraints::loose(size.width, size.height);
4676 let child_origin = LayoutPoint::new(origin.x, origin.y);
4677 self.layout_node_constraints(
4678 *child_id,
4679 child_constraints,
4680 child_origin,
4681 out,
4682 constraints_out,
4683 measure_cache,
4684 scroll_source,
4685 record,
4686 depth + 1,
4687 )?;
4688 }
4689 if record && !abs_children.is_empty() {
4690 let abs_constraints = BoxConstraints::loose(size.width, size.height);
4691 for child_id in abs_children {
4692 self.layout_node_constraints(
4693 child_id,
4694 abs_constraints,
4695 origin,
4696 out,
4697 constraints_out,
4698 measure_cache,
4699 scroll_source,
4700 record,
4701 depth + 1,
4702 )?;
4703 }
4704 }
4705 content_size = size;
4706 size
4707 }
4708 LayoutOp::Positioned {
4709 top,
4710 left,
4711 bottom,
4712 right,
4713 width,
4714 height,
4715 } => {
4716 let target_w = finite_or(constraints.max_w, finite_or(constraints.min_w, 0.0));
4717 let target_h = finite_or(constraints.max_h, finite_or(constraints.min_h, 0.0));
4718 let size = constraints.constrain(LayoutSize::new(target_w, target_h));
4719 let mut child_constraints = BoxConstraints::loose(size.width, size.height);
4720 if let (Some(l), Some(r)) = (left, right) {
4721 let w = (size.width - l - r).max(0.0);
4722 child_constraints = child_constraints.tighten(Some(w), None);
4723 }
4724 if let (Some(t), Some(b)) = (top, bottom) {
4725 let h = (size.height - t - b).max(0.0);
4726 child_constraints = child_constraints.tighten(None, Some(h));
4727 }
4728 child_constraints = child_constraints.tighten(*width, *height);
4729 if let Some(child_id) = node.children_ids.first() {
4730 let child_size = self.layout_node_constraints(
4731 *child_id,
4732 child_constraints,
4733 LayoutPoint::ZERO,
4734 out,
4735 constraints_out,
4736 measure_cache,
4737 scroll_source,
4738 false,
4739 depth + 1,
4740 )?;
4741 let x = left.unwrap_or_else(|| {
4742 right
4743 .map(|r| (size.width - r - child_size.width).max(0.0))
4744 .unwrap_or(0.0)
4745 });
4746 let y = top.unwrap_or_else(|| {
4747 bottom
4748 .map(|b| (size.height - b - child_size.height).max(0.0))
4749 .unwrap_or(0.0)
4750 });
4751 self.layout_node_constraints(
4752 *child_id,
4753 child_constraints,
4754 LayoutPoint::new(origin.x + x, origin.y + y),
4755 out,
4756 constraints_out,
4757 measure_cache,
4758 scroll_source,
4759 record,
4760 depth + 1,
4761 )?;
4762 }
4763 content_size = size;
4764 size
4765 }
4766 LayoutOp::PositionedLengths {
4767 top,
4768 left,
4769 bottom,
4770 right,
4771 width,
4772 height,
4773 } => {
4774 let target_w = finite_or(constraints.max_w, finite_or(constraints.min_w, 0.0));
4775 let target_h = finite_or(constraints.max_h, finite_or(constraints.min_h, 0.0));
4776 let size = constraints.constrain(LayoutSize::new(target_w, target_h));
4777 let resolve_horizontal = |length: &Option<Length>| {
4778 length
4779 .as_ref()
4780 .and_then(|length| resolve_length(length, size.width, self.active_viewport))
4781 };
4782 let resolve_vertical = |length: &Option<Length>| {
4783 length.as_ref().and_then(|length| {
4784 resolve_length(length, size.height, self.active_viewport)
4785 })
4786 };
4787 let left = resolve_horizontal(left);
4788 let top = resolve_vertical(top);
4789 let right = resolve_horizontal(right);
4790 let bottom = resolve_vertical(bottom);
4791 let width = resolve_horizontal(width);
4792 let height = resolve_vertical(height);
4793 let mut child_constraints = BoxConstraints::loose(size.width, size.height);
4794 if let (Some(left), Some(right)) = (left, right) {
4795 child_constraints =
4796 child_constraints.tighten(Some((size.width - left - right).max(0.0)), None);
4797 }
4798 if let (Some(top), Some(bottom)) = (top, bottom) {
4799 child_constraints = child_constraints
4800 .tighten(None, Some((size.height - top - bottom).max(0.0)));
4801 }
4802 child_constraints = child_constraints.tighten(width, height);
4803 if let Some(child_id) = node.children_ids.first() {
4804 let child_size = self.layout_node_constraints(
4805 *child_id,
4806 child_constraints,
4807 LayoutPoint::ZERO,
4808 out,
4809 constraints_out,
4810 measure_cache,
4811 scroll_source,
4812 false,
4813 depth + 1,
4814 )?;
4815 let x = left.unwrap_or_else(|| {
4816 right
4817 .map(|right| (size.width - right - child_size.width).max(0.0))
4818 .unwrap_or(0.0)
4819 });
4820 let y = top.unwrap_or_else(|| {
4821 bottom
4822 .map(|bottom| (size.height - bottom - child_size.height).max(0.0))
4823 .unwrap_or(0.0)
4824 });
4825 self.layout_node_constraints(
4826 *child_id,
4827 child_constraints,
4828 LayoutPoint::new(origin.x + x, origin.y + y),
4829 out,
4830 constraints_out,
4831 measure_cache,
4832 scroll_source,
4833 record,
4834 depth + 1,
4835 )?;
4836 }
4837 content_size = size;
4838 size
4839 }
4840 LayoutOp::Embed { width, height, .. } => {
4841 let local = constraints.tighten(*width, *height);
4842 let w = if local.is_width_bounded() {
4843 local.max_w
4844 } else {
4845 local.min_w
4846 };
4847 let h = if local.is_height_bounded() {
4848 local.max_h
4849 } else {
4850 local.min_h
4851 };
4852 let size = local.constrain(LayoutSize::new(w, h));
4853 content_size = size;
4854 size
4855 }
4856 LayoutOp::AbsoluteFill => {
4857 let target_w = finite_or(constraints.max_w, finite_or(constraints.min_w, 0.0));
4858 let target_h = finite_or(constraints.max_h, finite_or(constraints.min_h, 0.0));
4859 let size = constraints.constrain(LayoutSize::new(target_w, target_h));
4860 for child_id in self.graph_state.children_of(node_id) {
4861 self.layout_node_constraints(
4862 *child_id,
4863 BoxConstraints::tight(size),
4864 origin,
4865 out,
4866 constraints_out,
4867 measure_cache,
4868 scroll_source,
4869 record,
4870 depth + 1,
4871 )?;
4872 }
4873 content_size = size;
4874 size
4875 }
4876 LayoutOp::Spotlight { .. } => {
4877 let target_w = finite_or(constraints.max_w, finite_or(constraints.min_w, 0.0));
4878 let target_h = finite_or(constraints.max_h, finite_or(constraints.min_h, 0.0));
4879 let size = constraints.constrain(LayoutSize::new(target_w, target_h));
4880 for child_id in self.graph_state.children_of(node_id) {
4881 self.layout_node_constraints(
4882 *child_id,
4883 BoxConstraints::tight(LayoutSize::ZERO),
4884 origin,
4885 out,
4886 constraints_out,
4887 measure_cache,
4888 scroll_source,
4889 record,
4890 depth + 1,
4891 )?;
4892 }
4893 content_size = size;
4894 size
4895 }
4896 LayoutOp::Transform { .. } | LayoutOp::Clip { .. } => {
4897 let mut child_size = LayoutSize::ZERO;
4898 if let Some(child_id) = node.children_ids.first() {
4899 child_size = self.layout_node_constraints(
4900 *child_id,
4901 constraints,
4902 origin,
4903 out,
4904 constraints_out,
4905 measure_cache,
4906 scroll_source,
4907 record,
4908 depth + 1,
4909 )?;
4910 }
4911 content_size = child_size;
4912 constraints.constrain(child_size)
4913 }
4914 LayoutOp::Flyout { anchor, content: _ } => {
4915 let loose = BoxConstraints::loose(
4916 if constraints.is_width_bounded() {
4917 constraints.max_w
4918 } else {
4919 f32::INFINITY
4920 },
4921 if constraints.is_height_bounded() {
4922 constraints.max_h
4923 } else {
4924 f32::INFINITY
4925 },
4926 );
4927 let mut child_size = LayoutSize::ZERO;
4928 for child_id in self.graph_state.children_of(node_id) {
4929 child_size = self.layout_node_constraints(
4930 *child_id,
4931 loose,
4932 origin,
4933 out,
4934 constraints_out,
4935 measure_cache,
4936 scroll_source,
4937 false,
4938 depth + 1,
4939 )?;
4940 }
4941 if record {
4942 let anchor_rect = out.get(anchor).map(|g| g.rect);
4943 let place_x = anchor_rect.map(|r| r.x()).unwrap_or(origin.x);
4944 let place_y = anchor_rect.map(|r| r.y() + r.height()).unwrap_or(origin.y);
4945 for child_id in self.graph_state.children_of(node_id) {
4946 self.layout_node_constraints(
4947 *child_id,
4948 loose,
4949 LayoutPoint::new(place_x, place_y),
4950 out,
4951 constraints_out,
4952 measure_cache,
4953 scroll_source,
4954 record,
4955 depth + 1,
4956 )?;
4957 }
4958 }
4959 content_size = child_size;
4960 child_size
4961 }
4962 LayoutOp::StyledBox { .. } => unreachable!("styled boxes are resolved before layout"),
4963 };
4964
4965 if let Some(runs) = &node.rich_text {
4966 if let Some(measurer) = &self.measurer {
4967 let (mut text_constraints, text_padding) = match layout_op {
4968 LayoutOp::Box {
4969 width,
4970 height,
4971 min_width,
4972 max_width,
4973 min_height,
4974 max_height,
4975 padding,
4976 ..
4977 } => (
4978 constraints
4979 .apply_min_max(*min_width, *max_width, *min_height, *max_height)
4980 .tighten(*width, *height),
4981 *padding,
4982 ),
4983 _ => (constraints, [0.0; 4]),
4984 };
4985 let text_inner_constraints = text_constraints.deflate(text_padding);
4986 let intrinsic_width = match &node.op {
4987 LayoutOp::StyledBox { style, .. } => style.width.as_ref(),
4988 _ => None,
4989 };
4990 let avail_w = match intrinsic_width {
4991 Some(Length::MaxContent) => None,
4992 Some(Length::MinContent) => Some(
4993 runs.iter()
4994 .flat_map(|run| {
4995 run.text.split_whitespace().map(move |word| {
4996 measurer.measure(word, run.style.font_size, None).0
4997 + run.style.letter_spacing
4998 * word.chars().count().saturating_sub(1) as f32
4999 })
5000 })
5001 .fold(0.0, f32::max),
5002 ),
5003 _ => text_inner_constraints
5004 .is_width_bounded()
5005 .then_some(text_inner_constraints.max_w),
5006 };
5007 let rich_layout = measurer.layout_rich_text(runs, avail_w);
5008 let text_content = LayoutSize::new(
5009 rich_layout.width + text_padding[0] + text_padding[1],
5010 rich_layout.height + text_padding[2] + text_padding[3],
5011 );
5012 if let LayoutOp::StyledBox { style, .. } = &node.op {
5013 let available = if constraints.max_h.is_finite() {
5014 constraints.max_h
5015 } else {
5016 text_content.height
5017 };
5018 let resolve_intrinsic_height = |length: &Option<Length>| {
5019 length
5020 .as_ref()
5021 .filter(|length| length_requires_measurement(length))
5022 .and_then(|length| {
5023 resolve_measured_length(
5024 length,
5025 available,
5026 self.active_viewport,
5027 text_content.height,
5028 text_content.height,
5029 )
5030 })
5031 };
5032 text_constraints = text_constraints.apply_min_max(
5033 None,
5034 None,
5035 resolve_intrinsic_height(&style.min_height),
5036 resolve_intrinsic_height(&style.max_height),
5037 );
5038 text_constraints =
5039 text_constraints.tighten(None, resolve_intrinsic_height(&style.height));
5040 }
5041 let measured = text_constraints.constrain(text_content);
5042 if rich_text_inline_children
5043 && rich_layout.inline_boxes.len() == flow_children.len()
5044 {
5045 let result =
5046 self.record_geometry(node_id, origin, measured, text_content, out, record);
5047 if record {
5048 let mut inline_boxes = rich_layout.inline_boxes;
5049 inline_boxes.sort_by_key(|inline_box| inline_box.id);
5050 for (child_id, inline_box) in flow_children.iter().zip(inline_boxes.iter())
5051 {
5052 self.layout_node_constraints(
5053 *child_id,
5054 BoxConstraints::tight(LayoutSize::new(
5055 inline_box.width,
5056 inline_box.height,
5057 )),
5058 LayoutPoint::new(
5059 origin.x + text_padding[0] + inline_box.x,
5060 origin.y + text_padding[2] + inline_box.y,
5061 ),
5062 out,
5063 constraints_out,
5064 measure_cache,
5065 scroll_source,
5066 record,
5067 depth + 1,
5068 )?;
5069 }
5070 }
5071 if !record {
5072 measure_cache.insert(MeasureCacheKey::new(node_id, constraints), result);
5073 }
5074 return Ok(result);
5075 }
5076 if node.children_ids.is_empty() {
5077 let result =
5078 self.record_geometry(node_id, origin, measured, text_content, out, record);
5079 if !record {
5080 measure_cache.insert(MeasureCacheKey::new(node_id, constraints), result);
5081 }
5082 return Ok(result);
5083 }
5084 content_size.width = content_size.width.max(text_content.width);
5085 content_size.height = content_size.height.max(text_content.height);
5086 }
5087 }
5088
5089 let result = self.record_geometry(node_id, origin, size, content_size, out, record);
5090 if !record {
5091 measure_cache.insert(MeasureCacheKey::new(node_id, constraints), result);
5092 }
5093 Ok(result)
5094 }
5095
5096 fn record_geometry(
5097 &self,
5098 node_id: WidgetId,
5099 origin: LayoutPoint,
5100 size: LayoutSize,
5101 content_size: LayoutSize,
5102 out: &mut HashMap<WidgetId, LayoutNodeGeometry>,
5103 record: bool,
5104 ) -> LayoutSize {
5105 let mut rect_origin = origin;
5106 let mut rect_size = size;
5107 let mut rect_content = content_size;
5108 let mut had_non_finite = false;
5109
5110 if !rect_origin.x.is_finite() {
5111 rect_origin.x = 0.0;
5112 had_non_finite = true;
5113 }
5114 if !rect_origin.y.is_finite() {
5115 rect_origin.y = 0.0;
5116 had_non_finite = true;
5117 }
5118 if !rect_size.width.is_finite() {
5119 rect_size.width = 0.0;
5120 had_non_finite = true;
5121 }
5122 if !rect_size.height.is_finite() {
5123 rect_size.height = 0.0;
5124 had_non_finite = true;
5125 }
5126 if !rect_content.width.is_finite() {
5127 rect_content.width = 0.0;
5128 had_non_finite = true;
5129 }
5130 if !rect_content.height.is_finite() {
5131 rect_content.height = 0.0;
5132 had_non_finite = true;
5133 }
5134
5135 if had_non_finite {
5136 diag::emit(
5137 diag::DiagCategory::Invariants,
5138 diag::DiagLevel::Error,
5139 diag::DiagEventKind::InvariantViolation {
5140 kind: "non_finite_layout".into(),
5141 node: Some(node_id.as_u128()),
5142 details: format!(
5143 "origin=({:.2},{:.2}) size=({:.2},{:.2}) content=({:.2},{:.2})",
5144 origin.x,
5145 origin.y,
5146 size.width,
5147 size.height,
5148 content_size.width,
5149 content_size.height
5150 ),
5151 dump_ref: None,
5152 },
5153 );
5154 }
5155
5156 if record {
5157 let rect = LayoutRect::new(
5158 rect_origin.x,
5159 rect_origin.y,
5160 rect_size.width,
5161 rect_size.height,
5162 );
5163 out.insert(
5164 node_id,
5165 LayoutNodeGeometry {
5166 rect,
5167 content_size: rect_content,
5168 },
5169 );
5170 }
5171 rect_size
5172 }
5173}