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 resolve_length, LayoutEngine, LayoutGraphState, LayoutInputNode, LayoutSize, TextMeasurer,
774 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 grid_repeat_and_spans_are_applied_by_the_layout_engine() {
1088 let root = WidgetId::from_u128(200);
1089 let first = WidgetId::from_u128(201);
1090 let second = WidgetId::from_u128(202);
1091 let nodes = vec![
1092 node(
1093 root,
1094 None,
1095 vec![first, second],
1096 LayoutOp::Grid {
1097 columns: vec![GridTrack::repeat(2, vec![GridTrack::Points(50.0)])],
1098 rows: vec![GridTrack::Points(20.0)],
1099 column_gap: Some(10.0),
1100 row_gap: None,
1101 padding: [0.0; 4],
1102 },
1103 ),
1104 box_node(first, Some(root), vec![]),
1105 box_node(second, Some(root), vec![]),
1106 ];
1107 let mut engine = LayoutEngine::new();
1108 let snapshot = engine
1109 .compute_layout(&nodes, root, LayoutSize::new(110.0, 20.0), &|_| 0.0)
1110 .expect("grid layout");
1111
1112 assert_eq!(snapshot.nodes[&first].rect.x(), 0.0);
1113 assert_eq!(snapshot.nodes[&second].rect.x(), 60.0);
1114 }
1115
1116 #[test]
1117 fn auto_grid_items_advance_past_occupied_spans() {
1118 let root = WidgetId::from_u128(250);
1119 let first = WidgetId::from_u128(251);
1120 let first_child = WidgetId::from_u128(252);
1121 let second = WidgetId::from_u128(253);
1122 let second_child = WidgetId::from_u128(254);
1123 let nodes = vec![
1124 node(
1125 root,
1126 None,
1127 vec![first, second],
1128 LayoutOp::Grid {
1129 columns: vec![GridTrack::Points(50.0), GridTrack::Points(50.0)],
1130 rows: vec![],
1131 column_gap: None,
1132 row_gap: None,
1133 padding: [0.0; 4],
1134 },
1135 ),
1136 node(
1137 first,
1138 Some(root),
1139 vec![first_child],
1140 LayoutOp::GridItem {
1141 row_start: GridPlacement::Auto,
1142 row_end: GridPlacement::Auto,
1143 col_start: GridPlacement::Auto,
1144 col_end: GridPlacement::Span(2),
1145 },
1146 ),
1147 box_node(first_child, Some(first), vec![]),
1148 node(
1149 second,
1150 Some(root),
1151 vec![second_child],
1152 LayoutOp::GridItem {
1153 row_start: GridPlacement::Auto,
1154 row_end: GridPlacement::Auto,
1155 col_start: GridPlacement::Auto,
1156 col_end: GridPlacement::Auto,
1157 },
1158 ),
1159 box_node(second_child, Some(second), vec![]),
1160 ];
1161 let mut engine = LayoutEngine::new();
1162 let snapshot = engine
1163 .compute_layout(&nodes, root, LayoutSize::new(100.0, 100.0), &|_| 0.0)
1164 .expect("auto grid layout");
1165
1166 assert_eq!(snapshot.nodes[&first].rect.x(), 0.0);
1167 assert_eq!(snapshot.nodes[&first].rect.width(), 100.0);
1168 assert_eq!(snapshot.nodes[&second].rect.x(), 0.0);
1169 assert_eq!(snapshot.nodes[&second].rect.y(), 20.0);
1170 }
1171
1172 #[test]
1173 fn fixed_text_box_retains_natural_size_for_overflow_inspection() {
1174 let root = WidgetId::from_u128(300);
1175 let mut text = node(
1176 root,
1177 None,
1178 vec![],
1179 LayoutOp::StyledBox {
1180 style: BoxStyle {
1181 width: Some(Length::Points(40.0)),
1182 height: Some(Length::Points(10.0)),
1183 ..Default::default()
1184 },
1185 flex_grow: 0.0,
1186 flex_shrink: 1.0,
1187 },
1188 );
1189 text.rich_text = Some(vec![text_run("overflowing text")]);
1190 let nodes = vec![text];
1191 let mut engine = LayoutEngine::new().with_measurer(Arc::new(WrappingMeasurer));
1192 let snapshot = engine
1193 .compute_layout(&nodes, root, LayoutSize::new(100.0, 100.0), &|_| 0.0)
1194 .expect("text layout");
1195 let inspection = engine
1196 .inspect_node(&snapshot, root)
1197 .expect("layout inspection");
1198
1199 assert_eq!(inspection.laid_out.width(), 40.0);
1200 assert_eq!(inspection.laid_out.height(), 10.0);
1201 assert!(inspection.measured.height() > inspection.laid_out.height());
1202 assert!(inspection.overflow_y);
1203 assert_eq!(
1204 inspection.constrained, inspection.laid_out,
1205 "fixed constraints should match final bounds"
1206 );
1207 }
1208
1209 #[test]
1210 fn max_content_box_propagates_unwrapped_text_width() {
1211 let root = WidgetId::from_u128(400);
1212 let text_id = WidgetId::from_u128(401);
1213 let mut text = node(
1214 text_id,
1215 Some(root),
1216 vec![],
1217 LayoutOp::Box {
1218 width: None,
1219 height: None,
1220 min_width: None,
1221 max_width: None,
1222 min_height: None,
1223 max_height: None,
1224 padding: [0.0; 4],
1225 flex_grow: 0.0,
1226 flex_shrink: 1.0,
1227 aspect_ratio: None,
1228 },
1229 );
1230 text.rich_text = Some(vec![text_run("hello world")]);
1231 let nodes = vec![
1232 node(
1233 root,
1234 None,
1235 vec![text_id],
1236 LayoutOp::StyledBox {
1237 style: BoxStyle {
1238 width: Some(Length::MaxContent),
1239 ..Default::default()
1240 },
1241 flex_grow: 0.0,
1242 flex_shrink: 1.0,
1243 },
1244 ),
1245 text,
1246 ];
1247 let mut engine = LayoutEngine::new().with_measurer(Arc::new(WrappingMeasurer));
1248 let snapshot = engine
1249 .compute_layout(&nodes, root, LayoutSize::new(300.0, 100.0), &|_| 0.0)
1250 .expect("max-content layout");
1251
1252 assert_eq!(snapshot.nodes[&root].rect.width(), 110.0);
1253 assert_eq!(snapshot.nodes[&text_id].rect.width(), 110.0);
1254 }
1255
1256 #[test]
1257 fn intrinsic_lengths_participate_in_clamp_expressions() {
1258 let root = WidgetId::from_u128(450);
1259 let mut text = node(
1260 root,
1261 None,
1262 vec![],
1263 LayoutOp::StyledBox {
1264 style: BoxStyle {
1265 width: Some(Length::clamp(
1266 Length::points(50.0),
1267 Length::MaxContent,
1268 Length::points(80.0),
1269 )),
1270 ..Default::default()
1271 },
1272 flex_grow: 0.0,
1273 flex_shrink: 1.0,
1274 },
1275 );
1276 text.rich_text = Some(vec![text_run("hello world")]);
1277 let mut engine = LayoutEngine::new().with_measurer(Arc::new(WrappingMeasurer));
1278 let snapshot = engine
1279 .compute_layout(&[text], root, LayoutSize::new(300.0, 100.0), &|_| 0.0)
1280 .expect("intrinsic clamp layout");
1281
1282 assert_eq!(snapshot.nodes[&root].rect.width(), 80.0);
1283 assert_eq!(snapshot.nodes[&root].rect.height(), 40.0);
1284 }
1285
1286 #[test]
1287 fn margin_wrapper_keeps_percentage_width_relative_to_the_containing_box() {
1288 let outer = WidgetId::from_u128(455);
1289 let inner = WidgetId::from_u128(456);
1290 let nodes = vec![
1291 node(
1292 outer,
1293 None,
1294 vec![inner],
1295 LayoutOp::StyledBox {
1296 style: BoxStyle {
1297 width: Some(
1298 Length::percent(50.0) + Length::points(12.0) + Length::points(12.0),
1299 ),
1300 padding: Some(Length::all(Length::points(12.0))),
1301 alignment: fission_ir::op::BoxAlignment::Stretch,
1302 ..Default::default()
1303 },
1304 flex_grow: 0.0,
1305 flex_shrink: 1.0,
1306 },
1307 ),
1308 node(
1309 inner,
1310 Some(outer),
1311 vec![],
1312 LayoutOp::StyledBox {
1313 style: BoxStyle {
1314 width: Some(Length::percent(100.0)),
1315 height: Some(Length::points(20.0)),
1316 ..Default::default()
1317 },
1318 flex_grow: 0.0,
1319 flex_shrink: 1.0,
1320 },
1321 ),
1322 ];
1323 let mut engine = LayoutEngine::new();
1324 let snapshot = engine
1325 .compute_layout(&nodes, outer, LayoutSize::new(200.0, 100.0), &|_| 0.0)
1326 .expect("margin layout");
1327
1328 assert_eq!(snapshot.nodes[&outer].rect.width(), 124.0);
1329 assert_eq!(snapshot.nodes[&inner].rect.width(), 100.0);
1330 assert_eq!(snapshot.nodes[&inner].rect.x(), 12.0);
1331 }
1332
1333 #[test]
1334 fn fit_content_height_preserves_wrapped_text_height() {
1335 let root = WidgetId::from_u128(460);
1336 let mut text = node(
1337 root,
1338 None,
1339 vec![],
1340 LayoutOp::StyledBox {
1341 style: BoxStyle {
1342 width: Some(Length::points(40.0)),
1343 height: Some(Length::fit_content(None)),
1344 ..Default::default()
1345 },
1346 flex_grow: 0.0,
1347 flex_shrink: 1.0,
1348 },
1349 );
1350 text.rich_text = Some(vec![text_run("abcdefgh")]);
1351 let mut engine = LayoutEngine::new().with_measurer(Arc::new(WrappingMeasurer));
1352 let snapshot = engine
1353 .compute_layout(&[text], root, LayoutSize::new(300.0, 100.0), &|_| 0.0)
1354 .expect("fit-content height layout");
1355
1356 assert_eq!(snapshot.nodes[&root].rect.width(), 40.0);
1357 assert_eq!(snapshot.nodes[&root].rect.height(), 40.0);
1358 }
1359
1360 #[test]
1361 fn typed_position_offsets_resolve_against_the_parent_box() {
1362 let root = WidgetId::from_u128(500);
1363 let positioned = WidgetId::from_u128(501);
1364 let child = WidgetId::from_u128(502);
1365 let nodes = vec![
1366 node(root, None, vec![positioned], LayoutOp::ZStack),
1367 node(
1368 positioned,
1369 Some(root),
1370 vec![child],
1371 LayoutOp::PositionedLengths {
1372 left: Some(Length::Percent(25.0)),
1373 top: Some(Length::Percent(10.0)),
1374 right: None,
1375 bottom: None,
1376 width: Some(Length::Points(50.0)),
1377 height: Some(Length::Points(20.0)),
1378 },
1379 ),
1380 node(
1381 child,
1382 Some(positioned),
1383 vec![],
1384 LayoutOp::Box {
1385 width: None,
1386 height: None,
1387 min_width: None,
1388 max_width: None,
1389 min_height: None,
1390 max_height: None,
1391 padding: [0.0; 4],
1392 flex_grow: 0.0,
1393 flex_shrink: 1.0,
1394 aspect_ratio: None,
1395 },
1396 ),
1397 ];
1398 let mut engine = LayoutEngine::new();
1399 let snapshot = engine
1400 .compute_layout(&nodes, root, LayoutSize::new(200.0, 100.0), &|_| 0.0)
1401 .expect("typed positioned layout");
1402
1403 assert_eq!(snapshot.nodes[&child].rect.x(), 50.0);
1404 assert_eq!(snapshot.nodes[&child].rect.y(), 10.0);
1405 assert_eq!(snapshot.nodes[&child].rect.width(), 50.0);
1406 assert_eq!(snapshot.nodes[&child].rect.height(), 20.0);
1407 }
1408}
1409
1410fn layout_input_fingerprint(node: &LayoutInputNode) -> u64 {
1411 let mut hasher = DefaultHasher::new();
1412 format!("{node:?}").hash(&mut hasher);
1413 hasher.finish()
1414}
1415
1416fn intersect_rect(left: LayoutRect, right: LayoutRect) -> LayoutRect {
1417 let x = left.x().max(right.x());
1418 let y = left.y().max(right.y());
1419 let right_edge = left.right().min(right.right());
1420 let bottom_edge = left.bottom().min(right.bottom());
1421 LayoutRect::new(x, y, (right_edge - x).max(0.0), (bottom_edge - y).max(0.0))
1422}
1423
1424fn union_rect(left: LayoutRect, right: LayoutRect) -> LayoutRect {
1425 let x = left.x().min(right.x());
1426 let y = left.y().min(right.y());
1427 let right_edge = left.right().max(right.right());
1428 let bottom_edge = left.bottom().max(right.bottom());
1429 LayoutRect::new(x, y, right_edge - x, bottom_edge - y)
1430}
1431
1432#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1447pub struct LayoutRect {
1448 pub origin: LayoutPoint,
1450 pub size: LayoutSize,
1452}
1453
1454impl LayoutRect {
1455 pub fn new(x: LayoutUnit, y: LayoutUnit, width: LayoutUnit, height: LayoutUnit) -> Self {
1457 Self {
1458 origin: LayoutPoint { x, y },
1459 size: LayoutSize { width, height },
1460 }
1461 }
1462
1463 pub fn x(&self) -> LayoutUnit {
1465 self.origin.x
1466 }
1467 pub fn y(&self) -> LayoutUnit {
1469 self.origin.y
1470 }
1471 pub fn width(&self) -> LayoutUnit {
1473 self.size.width
1474 }
1475 pub fn height(&self) -> LayoutUnit {
1477 self.size.height
1478 }
1479
1480 pub fn right(&self) -> LayoutUnit {
1482 self.origin.x + self.size.width
1483 }
1484 pub fn bottom(&self) -> LayoutUnit {
1486 self.origin.y + self.size.height
1487 }
1488
1489 pub fn contains(&self, p: LayoutPoint) -> bool {
1492 p.x >= self.x() && p.x < self.right() && p.y >= self.y() && p.y < self.bottom()
1493 }
1494}
1495
1496#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1502pub struct LayoutNodeGeometry {
1503 pub rect: LayoutRect,
1505 pub content_size: LayoutSize,
1508}
1509
1510#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1512pub struct LayoutInspection {
1513 pub node: WidgetId,
1515 pub measured: LayoutRect,
1517 pub constraints: BoxConstraints,
1519 pub constrained: LayoutRect,
1521 pub laid_out: LayoutRect,
1523 pub clipped: LayoutRect,
1525 pub painted: LayoutRect,
1527 pub overflow_x: bool,
1529 pub overflow_y: bool,
1531}
1532
1533#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1549pub struct LayoutSnapshot {
1550 pub nodes: HashMap<WidgetId, LayoutNodeGeometry>,
1552 #[serde(skip)]
1555 pub constraints: HashMap<WidgetId, BoxConstraints>,
1556 pub viewport_size: LayoutSize,
1558}
1559
1560impl LayoutSnapshot {
1561 pub fn new(viewport_size: LayoutSize) -> Self {
1563 Self {
1564 nodes: HashMap::new(),
1565 constraints: HashMap::new(),
1566 viewport_size,
1567 }
1568 }
1569
1570 pub fn get_node_geometry(&self, node_id: WidgetId) -> Option<&LayoutNodeGeometry> {
1573 self.nodes.get(&node_id)
1574 }
1575
1576 pub fn get_node_rect(&self, node_id: WidgetId) -> Option<LayoutRect> {
1578 self.nodes.get(&node_id).map(|g| g.rect)
1579 }
1580
1581 pub fn get_node_constraints(&self, node_id: WidgetId) -> Option<BoxConstraints> {
1584 self.constraints.get(&node_id).copied()
1585 }
1586}
1587
1588#[derive(Debug, Clone)]
1597pub struct LayoutInputNode {
1598 pub id: WidgetId,
1600 pub parent_id: Option<WidgetId>,
1602 pub op: LayoutOp,
1604 pub children_ids: Vec<WidgetId>,
1606 pub debug_name: String,
1608 pub width: Option<LayoutUnit>,
1610 pub height: Option<LayoutUnit>,
1612 pub flex_grow: LayoutUnit,
1614 pub flex_shrink: LayoutUnit,
1616 pub rich_text: Option<Vec<TextRun>>,
1619}
1620
1621pub struct LineMetric {
1627 pub start_index: usize,
1629 pub end_index: usize,
1631 pub baseline: f32,
1633 pub height: f32,
1635 pub width: f32,
1637}
1638
1639#[derive(Debug, Clone, Copy, PartialEq)]
1640pub struct RichTextInlineBox {
1641 pub id: u64,
1642 pub x: f32,
1643 pub y: f32,
1644 pub width: f32,
1645 pub height: f32,
1646}
1647
1648#[derive(Debug, Clone, PartialEq)]
1649pub struct RichTextLayoutInfo {
1650 pub width: f32,
1651 pub height: f32,
1652 pub inline_boxes: Vec<RichTextInlineBox>,
1653}
1654
1655const DEFAULT_RICH_TEXT_HIT_TEST_FONT_SIZE: f32 = 14.0;
1675
1676pub trait TextMeasurer: Send + Sync {
1677 fn measure(&self, text: &str, font_size: f32, available_width: Option<f32>) -> (f32, f32);
1682
1683 fn hit_test(
1688 &self,
1689 _text: &str,
1690 _font_size: f32,
1691 _available_width: Option<f32>,
1692 _x: f32,
1693 _y: f32,
1694 ) -> usize {
1695 0
1696 }
1697
1698 fn get_line_metrics(
1703 &self,
1704 _text: &str,
1705 _font_size: f32,
1706 _available_width: Option<f32>,
1707 ) -> Vec<LineMetric> {
1708 vec![]
1709 }
1710
1711 fn get_caret_position(
1716 &self,
1717 _text: &str,
1718 _font_size: f32,
1719 _available_width: Option<f32>,
1720 _caret_index: usize,
1721 ) -> (f32, f32) {
1722 (0.0, 0.0)
1723 }
1724
1725 fn measure_rich_text(&self, _runs: &[TextRun], _available_width: Option<f32>) -> (f32, f32) {
1729 (0.0, 0.0)
1730 }
1731
1732 fn layout_rich_text(
1738 &self,
1739 runs: &[TextRun],
1740 available_width: Option<f32>,
1741 ) -> RichTextLayoutInfo {
1742 let (width, height) = if runs.len() == 1 {
1743 let run = &runs[0];
1744 self.measure(&run.text, run.style.font_size, available_width)
1745 } else {
1746 self.measure_rich_text(runs, available_width)
1747 };
1748 RichTextLayoutInfo {
1749 width,
1750 height,
1751 inline_boxes: Vec::new(),
1752 }
1753 }
1754
1755 fn hit_test_rich(
1759 &self,
1760 runs: &[TextRun],
1761 _available_width: Option<f32>,
1762 x: f32,
1763 y: f32,
1764 ) -> usize {
1765 let text: String = runs.iter().map(|r| r.text.as_str()).collect();
1768 let font_size = runs
1769 .first()
1770 .map(|r| r.style.font_size)
1771 .unwrap_or(DEFAULT_RICH_TEXT_HIT_TEST_FONT_SIZE);
1772 self.hit_test(&text, font_size, None, x, y)
1773 }
1774
1775 fn resolve_rich_text_annotation_at_point(
1780 &self,
1781 _runs: &[TextRun],
1782 _available_width: Option<f32>,
1783 _x: f32,
1784 _y: f32,
1785 _paragraph_style: TextParagraphStyle,
1786 _annotations: &[RichTextAnnotation],
1787 ) -> Option<RichTextAnnotation> {
1788 None
1789 }
1790}
1791
1792pub struct LayoutEngine {
1815 measurer: Option<Arc<dyn TextMeasurer>>,
1816 graph_state: LayoutGraphState,
1817 next_graph_version: u64,
1818 incremental_reuse: Option<IncrementalLayoutReuseState>,
1819 active_viewport: LayoutSize,
1820}
1821
1822impl LayoutEngine {
1823 const MAX_LAYOUT_RECURSION_DEPTH: usize = 100;
1824
1825 pub fn new() -> Self {
1830 Self {
1831 measurer: None,
1832 graph_state: LayoutGraphState::default(),
1833 next_graph_version: 1,
1834 incremental_reuse: None,
1835 active_viewport: LayoutSize::ZERO,
1836 }
1837 }
1838
1839 pub fn with_measurer(mut self, measurer: Arc<dyn TextMeasurer>) -> Self {
1843 self.measurer = Some(measurer);
1844 self
1845 }
1846
1847 fn allocate_graph_version(&mut self) -> u64 {
1848 let version = self.next_graph_version;
1849 self.next_graph_version = self.next_graph_version.saturating_add(1);
1850 version
1851 }
1852
1853 fn refresh_graph_state(&mut self, input_nodes: &[LayoutInputNode]) {
1854 let version = self.allocate_graph_version();
1855 self.graph_state = LayoutGraphState::from_input_nodes(input_nodes, version);
1856 }
1857
1858 fn ensure_graph_state(&mut self, input_nodes: &[LayoutInputNode]) {
1859 if self.graph_state.is_empty() || !self.graph_state.matches_input_nodes(input_nodes) {
1860 self.refresh_graph_state(input_nodes);
1861 }
1862 }
1863
1864 fn validate_graph_state(&self, root: WidgetId) -> Result<()> {
1865 if let Some(err) = self.graph_state.validation.first_error() {
1866 return Err(err);
1867 }
1868 if !self.graph_state.nodes.contains_key(&root) {
1869 anyhow::bail!("[verify] missing node {:?}", root);
1870 }
1871 if !self.graph_state.roots.contains(&root)
1872 && self
1873 .graph_state
1874 .parents
1875 .get(&root)
1876 .copied()
1877 .flatten()
1878 .is_some()
1879 {
1880 anyhow::bail!("[verify] root {:?} is not a graph root", root);
1881 }
1882 if let Some(last_layout_version) = self.graph_state.last_layout_version {
1883 if last_layout_version > self.graph_state.graph_version {
1884 anyhow::bail!(
1885 "[verify] cached layout version {} exceeds graph version {}",
1886 last_layout_version,
1887 self.graph_state.graph_version
1888 );
1889 }
1890 }
1891 Ok(())
1892 }
1893
1894 pub fn update(&mut self, input_nodes: &[LayoutInputNode]) {
1899 if self.graph_state.is_empty() {
1900 self.refresh_graph_state(input_nodes);
1901 return;
1902 }
1903
1904 if self.graph_state.matches_input_nodes(input_nodes) {
1905 return;
1906 }
1907
1908 let version = self.allocate_graph_version();
1909 self.graph_state.graph_version = version;
1910 self.graph_state.update_nodes(input_nodes);
1911 }
1912
1913 pub fn rebuild(&mut self, input_nodes: &[LayoutInputNode]) -> Result<()> {
1915 self.refresh_graph_state(input_nodes);
1916 if let Some(err) = self.graph_state.validation.first_error() {
1917 return Err(err);
1918 }
1919 Ok(())
1920 }
1921
1922 pub fn verify_post_update(
1927 &self,
1928 input_nodes: &[LayoutInputNode],
1929 root: WidgetId,
1930 ) -> Result<()> {
1931 if self.graph_state.matches_input_nodes(input_nodes) {
1932 return self.validate_graph_state(root);
1933 }
1934
1935 let node_map: HashMap<WidgetId, &LayoutInputNode> =
1936 input_nodes.iter().map(|n| (n.id, n)).collect();
1937 for n in input_nodes {
1939 for child in &n.children_ids {
1940 let child_node = node_map
1941 .get(child)
1942 .ok_or_else(|| anyhow::anyhow!("[verify] child {:?} not found", child))?;
1943 if child_node.parent_id != Some(n.id) {
1944 anyhow::bail!("[verify] parent/child mismatch parent={:?} child={:?} child.parent_id={:?}", n.id, child, child_node.parent_id);
1945 }
1946 }
1947 }
1948 fn dfs(
1950 id: WidgetId,
1951 map: &HashMap<WidgetId, &LayoutInputNode>,
1952 visited: &mut HashSet<WidgetId>,
1953 stack: &mut HashSet<WidgetId>,
1954 ) -> Result<()> {
1955 if !visited.insert(id) {
1956 return Ok(());
1957 }
1958 stack.insert(id);
1959 let node = map
1960 .get(&id)
1961 .ok_or_else(|| anyhow::anyhow!("[verify] missing node {:?}", id))?;
1962 for child in &node.children_ids {
1963 if stack.contains(child) {
1964 anyhow::bail!("[verify] cycle detected at {:?} -> {:?}", id, child);
1965 }
1966 dfs(*child, map, visited, stack)?;
1967 }
1968 stack.remove(&id);
1969 Ok(())
1970 }
1971 let mut visited = HashSet::new();
1972 let mut stack = HashSet::new();
1973 dfs(root, &node_map, &mut visited, &mut stack)?;
1974 Ok(())
1975 }
1976
1977 pub fn compute_layout(
1995 &mut self,
1996 input_nodes: &[LayoutInputNode],
1997 root_node_id: WidgetId,
1998 viewport_size: LayoutSize,
1999 scroll_source: &impl ScrollDataSource,
2000 ) -> Result<LayoutSnapshot> {
2001 self.ensure_graph_state(input_nodes);
2002 self.validate_graph_state(root_node_id)?;
2003 let snapshot = self.compute_layout_constraints(
2004 input_nodes,
2005 root_node_id,
2006 viewport_size,
2007 scroll_source,
2008 )?;
2009 self.emit_scroll_diagnostics(&snapshot);
2010 self.emit_overflow_diagnostics(&snapshot);
2011 Ok(snapshot)
2012 }
2013
2014 pub fn compute_layout_constraints(
2019 &mut self,
2020 input_nodes: &[LayoutInputNode],
2021 root_node_id: WidgetId,
2022 viewport_size: LayoutSize,
2023 scroll_source: &impl ScrollDataSource,
2024 ) -> Result<LayoutSnapshot> {
2025 self.active_viewport = viewport_size;
2026 self.ensure_graph_state(input_nodes);
2027 self.validate_graph_state(root_node_id)?;
2028
2029 let mut constraints = BoxConstraints::tight(viewport_size);
2031 if let Some(root) = self.graph_state.node(root_node_id) {
2032 let styled_dimension = matches!(
2034 &root.op,
2035 LayoutOp::StyledBox { style, .. }
2036 if style.width.is_some() || style.height.is_some()
2037 );
2038 if root.width.is_some() || root.height.is_some() || styled_dimension {
2039 constraints = BoxConstraints::loose(viewport_size.width, viewport_size.height)
2040 .tighten(root.width, root.height);
2041 }
2042 }
2043
2044 let mut snapshot = LayoutSnapshot::new(viewport_size);
2045 let mut measure_cache = HashMap::new();
2046 self.layout_node_constraints(
2047 root_node_id,
2048 constraints,
2049 LayoutPoint::ZERO,
2050 &mut snapshot.nodes,
2051 &mut snapshot.constraints,
2052 &mut measure_cache,
2053 scroll_source,
2054 true,
2055 0,
2056 )?;
2057
2058 let visual_location = |node_id: WidgetId| -> Option<LayoutPoint> {
2059 let mut pos = snapshot.nodes.get(&node_id)?.rect.origin;
2060 let mut current = self.graph_state.parent_of(node_id);
2061 while let Some(parent_id) = current {
2062 if let Some(parent) = self.graph_state.node(parent_id) {
2063 if let LayoutOp::Scroll { direction, .. } = &parent.op {
2064 let offset = scroll_source.get_offset(parent_id);
2065 match direction {
2066 FlexDirection::Row => pos.x -= offset,
2067 FlexDirection::Column => pos.y -= offset,
2068 }
2069 }
2070 current = self.graph_state.parent_of(parent_id);
2071 } else {
2072 break;
2073 }
2074 }
2075 Some(pos)
2076 };
2077
2078 let mut flyout_abs_overrides: HashMap<WidgetId, (f32, f32)> = HashMap::new();
2079 for node in self.graph_state.ordered_nodes() {
2080 if let LayoutOp::Flyout { anchor, content } = node.op {
2081 if let (Some(anchor_geom), Some(content_geom)) =
2082 (snapshot.nodes.get(&anchor), snapshot.nodes.get(&content))
2083 {
2084 if let Some(anchor_abs) = visual_location(anchor) {
2085 let content_w = content_geom.rect.width();
2086 let content_h = content_geom.rect.height();
2087 let anchor_h = anchor_geom.rect.height();
2088 let max_left = (snapshot.viewport_size.width - content_w).max(0.0);
2089 let left_rel = anchor_abs.x.clamp(0.0, max_left);
2090
2091 let below_top = anchor_abs.y + anchor_h;
2092 let max_top = (snapshot.viewport_size.height - content_h).max(0.0);
2093 let top_rel = if below_top + content_h <= snapshot.viewport_size.height {
2094 below_top
2095 } else {
2096 let above_top = anchor_abs.y - content_h;
2097 if above_top >= 0.0 {
2098 above_top
2099 } else {
2100 below_top.clamp(0.0, max_top)
2101 }
2102 };
2103 flyout_abs_overrides.insert(content, (left_rel, top_rel));
2104 }
2105 }
2106 }
2107 }
2108
2109 if !flyout_abs_overrides.is_empty() {
2110 for (nid, (abs_x, abs_y)) in flyout_abs_overrides {
2111 if let Some(current) = snapshot.nodes.get(&nid) {
2112 let dx = abs_x - current.rect.origin.x;
2113 let dy = abs_y - current.rect.origin.y;
2114 let mut stack = vec![(nid, 0usize)];
2115 while let Some((current_id, depth)) = stack.pop() {
2116 if depth > Self::MAX_LAYOUT_RECURSION_DEPTH {
2117 return Err(self.layout_depth_overflow(current_id, depth));
2118 }
2119 if let Some(geometry) = snapshot.nodes.get_mut(¤t_id) {
2120 geometry.rect.origin.x += dx;
2121 geometry.rect.origin.y += dy;
2122 }
2123 for child_id in self.graph_state.children_of(current_id).iter().rev() {
2124 stack.push((*child_id, depth + 1));
2125 }
2126 }
2127 }
2128 }
2129 }
2130
2131 self.graph_state.mark_layout_complete();
2132 self.incremental_reuse = None;
2133
2134 Ok(snapshot)
2135 }
2136
2137 pub fn compute_layout_incremental(
2138 &mut self,
2139 input_nodes: &[LayoutInputNode],
2140 root_node_id: WidgetId,
2141 viewport_size: LayoutSize,
2142 scroll_source: &impl ScrollDataSource,
2143 previous_snapshot: &LayoutSnapshot,
2144 dirty_nodes: &HashSet<WidgetId>,
2145 ) -> Result<LayoutSnapshot> {
2146 self.ensure_graph_state(input_nodes);
2147 self.validate_graph_state(root_node_id)?;
2148
2149 let mut dirty_ancestors = HashSet::new();
2150 for node_id in dirty_nodes {
2151 let mut current = Some(*node_id);
2152 while let Some(id) = current {
2153 if !dirty_ancestors.insert(id) {
2154 break;
2155 }
2156 current = self.graph_state.parent_of(id);
2157 }
2158 }
2159 dirty_ancestors.insert(root_node_id);
2160
2161 self.incremental_reuse = Some(IncrementalLayoutReuseState {
2162 previous_snapshot: previous_snapshot.clone(),
2163 dirty_ancestors,
2164 });
2165 let result = self.compute_layout_constraints(
2166 input_nodes,
2167 root_node_id,
2168 viewport_size,
2169 scroll_source,
2170 );
2171 self.incremental_reuse = None;
2172 result
2173 }
2174
2175 fn emit_scroll_diagnostics(&self, snapshot: &LayoutSnapshot) {
2176 use fission_diagnostics::prelude as diag;
2177 let trace_scroll = std::env::var("FISSION_SCROLL_TRACE").ok().as_deref() == Some("1");
2178 for n in self.graph_state.ordered_nodes() {
2179 if let LayoutOp::Scroll { .. } = n.op {
2180 if let Some(g) = snapshot.nodes.get(&n.id) {
2181 let note = if g.rect.height() <= 0.0 {
2182 let parent_op = n
2183 .parent_id
2184 .and_then(|pid| self.graph_state.node(pid))
2185 .map(|p| format!("{:?}", p.op));
2186 let parent_constraints = n
2187 .parent_id
2188 .and_then(|pid| snapshot.constraints.get(&pid))
2189 .copied();
2190 snapshot
2191 .constraints
2192 .get(&n.id)
2193 .map(|c| {
2194 format!(
2195 "op={:?} parent={:?} parent_op={:?} parent_constraints={:?} constraints={:?}",
2196 n.op,
2197 n.parent_id,
2198 parent_op,
2199 parent_constraints,
2200 c
2201 )
2202 })
2203 } else {
2204 None
2205 };
2206 diag::emit(
2207 diag::DiagCategory::Layout,
2208 diag::DiagLevel::Debug,
2209 diag::DiagEventKind::ScrollExtent {
2210 node: n.id.as_u128(),
2211 viewport_w: g.rect.width(),
2212 viewport_h: g.rect.height(),
2213 content_w: g.content_size.width,
2214 content_h: g.content_size.height,
2215 note,
2216 },
2217 );
2218 if trace_scroll {
2219 eprintln!(
2220 "[scroll-trace] node={} viewport=({:.1},{:.1}) content=({:.1},{:.1})",
2221 n.id.as_u128(),
2222 g.rect.width(),
2223 g.rect.height(),
2224 g.content_size.width,
2225 g.content_size.height
2226 );
2227 }
2228 }
2229 }
2230 }
2231 }
2232
2233 fn emit_overflow_diagnostics(&self, snapshot: &LayoutSnapshot) {
2234 for node in self.graph_state.ordered_nodes() {
2235 let Some(geometry) = snapshot.nodes.get(&node.id) else {
2236 continue;
2237 };
2238 let overflow_x = geometry.content_size.width > geometry.rect.width() + 0.5;
2239 let overflow_y = geometry.content_size.height > geometry.rect.height() + 0.5;
2240 if !overflow_x && !overflow_y {
2241 continue;
2242 }
2243 let text = node.rich_text.is_some();
2244 diag::emit(
2245 diag::DiagCategory::Layout,
2246 if text {
2247 diag::DiagLevel::Warn
2248 } else {
2249 diag::DiagLevel::Debug
2250 },
2251 diag::DiagEventKind::LayoutOverflow {
2252 node: node.id.as_u128(),
2253 debug_name: node.debug_name.clone(),
2254 parent: node.parent_id.map(|parent| parent.as_u128()),
2255 parent_debug_name: node
2256 .parent_id
2257 .and_then(|parent| self.graph_state.node(parent))
2258 .map(|parent| parent.debug_name.clone()),
2259 parent_layout: node
2260 .parent_id
2261 .and_then(|parent| self.graph_state.node(parent))
2262 .map(|parent| format!("{:?}", parent.op)),
2263 text,
2264 min_w: snapshot
2265 .constraints
2266 .get(&node.id)
2267 .map_or(0.0, |constraints| constraints.min_w),
2268 max_w: snapshot
2269 .constraints
2270 .get(&node.id)
2271 .map(|constraints| constraints.max_w)
2272 .filter(|value| value.is_finite()),
2273 min_h: snapshot
2274 .constraints
2275 .get(&node.id)
2276 .map_or(0.0, |constraints| constraints.min_h),
2277 max_h: snapshot
2278 .constraints
2279 .get(&node.id)
2280 .map(|constraints| constraints.max_h)
2281 .filter(|value| value.is_finite()),
2282 laid_out_w: geometry.rect.width(),
2283 laid_out_h: geometry.rect.height(),
2284 content_w: geometry.content_size.width,
2285 content_h: geometry.content_size.height,
2286 },
2287 );
2288 }
2289 }
2290
2291 pub fn inspect_node(
2293 &self,
2294 snapshot: &LayoutSnapshot,
2295 node_id: WidgetId,
2296 ) -> Option<LayoutInspection> {
2297 let geometry = snapshot.nodes.get(&node_id)?;
2298 let constraints = snapshot.constraints.get(&node_id).copied()?;
2299 let measured = LayoutRect::new(
2300 geometry.rect.x(),
2301 geometry.rect.y(),
2302 geometry.content_size.width,
2303 geometry.content_size.height,
2304 );
2305 let effective_constraints = self
2306 .graph_state
2307 .node(node_id)
2308 .map(|node| {
2309 let resolved_style;
2310 let op = match &node.op {
2311 LayoutOp::StyledBox { style, .. } => {
2312 resolved_style =
2313 resolve_box_style(style, constraints, snapshot.viewport_size);
2314 &resolved_style
2315 }
2316 op => op,
2317 };
2318 match op {
2319 LayoutOp::Box {
2320 width,
2321 height,
2322 min_width,
2323 max_width,
2324 min_height,
2325 max_height,
2326 ..
2327 } => constraints
2328 .apply_min_max(*min_width, *max_width, *min_height, *max_height)
2329 .tighten(*width, *height),
2330 _ => constraints,
2331 }
2332 })
2333 .unwrap_or(constraints);
2334 let constrained_size = effective_constraints.constrain(geometry.content_size);
2335 let constrained = LayoutRect::new(
2336 geometry.rect.x(),
2337 geometry.rect.y(),
2338 constrained_size.width,
2339 constrained_size.height,
2340 );
2341 let mut clipped = geometry.rect;
2342 let mut ancestor = self.graph_state.parent_of(node_id);
2343 while let Some(ancestor_id) = ancestor {
2344 let clips = self
2345 .graph_state
2346 .node(ancestor_id)
2347 .is_some_and(|node| match &node.op {
2348 LayoutOp::Scroll { .. } | LayoutOp::Clip { .. } => true,
2349 LayoutOp::StyledBox { style, .. } => {
2350 style.overflow == fission_ir::op::Overflow::Clip
2351 }
2352 _ => false,
2353 });
2354 if clips {
2355 if let Some(ancestor_geometry) = snapshot.nodes.get(&ancestor_id) {
2356 clipped = intersect_rect(clipped, ancestor_geometry.rect);
2357 }
2358 }
2359 ancestor = self.graph_state.parent_of(ancestor_id);
2360 }
2361
2362 let mut painted = geometry.rect;
2363 let mut descendants = self.graph_state.children_of(node_id).to_vec();
2364 while let Some(descendant) = descendants.pop() {
2365 if let Some(descendant_geometry) = snapshot.nodes.get(&descendant) {
2366 painted = union_rect(painted, descendant_geometry.rect);
2367 }
2368 descendants.extend_from_slice(self.graph_state.children_of(descendant));
2369 }
2370 Some(LayoutInspection {
2371 node: node_id,
2372 measured,
2373 constraints,
2374 constrained,
2375 laid_out: geometry.rect,
2376 clipped,
2377 painted,
2378 overflow_x: geometry.content_size.width > geometry.rect.width() + 0.5,
2379 overflow_y: geometry.content_size.height > geometry.rect.height() + 0.5,
2380 })
2381 }
2382
2383 fn layout_depth_overflow(&self, node_id: WidgetId, depth: usize) -> anyhow::Error {
2384 let details = format!(
2385 "layout recursion depth {} exceeded max {} at node {}",
2386 depth,
2387 Self::MAX_LAYOUT_RECURSION_DEPTH,
2388 node_id.as_u128()
2389 );
2390 diag::emit(
2391 diag::DiagCategory::Invariants,
2392 diag::DiagLevel::Error,
2393 diag::DiagEventKind::InvariantViolation {
2394 kind: "layout_recursion_depth".into(),
2395 node: Some(node_id.as_u128()),
2396 details: details.clone(),
2397 dump_ref: None,
2398 },
2399 );
2400 anyhow::anyhow!(details)
2401 }
2402
2403 fn copy_cached_subtree(
2404 &self,
2405 node_id: WidgetId,
2406 origin: LayoutPoint,
2407 current_constraints: BoxConstraints,
2408 out: &mut HashMap<WidgetId, LayoutNodeGeometry>,
2409 constraints_out: &mut HashMap<WidgetId, BoxConstraints>,
2410 ) -> Result<Option<LayoutSize>> {
2411 let Some(reuse) = self.incremental_reuse.as_ref() else {
2412 return Ok(None);
2413 };
2414 if reuse.dirty_ancestors.contains(&node_id) {
2415 return Ok(None);
2416 }
2417
2418 let Some(previous_geometry) = reuse.previous_snapshot.nodes.get(&node_id) else {
2419 return Ok(None);
2420 };
2421 let Some(previous_constraints) = reuse.previous_snapshot.constraints.get(&node_id).copied()
2422 else {
2423 return Ok(None);
2424 };
2425 if previous_constraints != current_constraints {
2426 return Ok(None);
2427 }
2428
2429 let dx = origin.x - previous_geometry.rect.origin.x;
2430 let dy = origin.y - previous_geometry.rect.origin.y;
2431 let mut stack = vec![(node_id, 0usize)];
2432 while let Some((current_id, depth)) = stack.pop() {
2433 if depth > Self::MAX_LAYOUT_RECURSION_DEPTH {
2434 return Err(self.layout_depth_overflow(current_id, depth));
2435 }
2436 let Some(previous_geometry) = reuse.previous_snapshot.nodes.get(¤t_id) else {
2437 return Ok(None);
2438 };
2439 let Some(previous_constraints) = reuse
2440 .previous_snapshot
2441 .constraints
2442 .get(¤t_id)
2443 .copied()
2444 else {
2445 return Ok(None);
2446 };
2447
2448 let mut geometry = previous_geometry.clone();
2449 geometry.rect.origin.x += dx;
2450 geometry.rect.origin.y += dy;
2451 out.insert(current_id, geometry);
2452 constraints_out.insert(current_id, previous_constraints);
2453
2454 let children = self.graph_state.children_of(current_id);
2455 for child_id in children.iter().rev() {
2456 stack.push((*child_id, depth + 1));
2457 }
2458 }
2459
2460 Ok(Some(previous_geometry.content_size))
2461 }
2462
2463 #[allow(clippy::too_many_arguments)]
2464 fn measure_grid_intrinsic_width(
2465 &self,
2466 node_id: WidgetId,
2467 intrinsic: IntrinsicAxis,
2468 max_height: f32,
2469 out: &mut HashMap<WidgetId, LayoutNodeGeometry>,
2470 constraints_out: &mut HashMap<WidgetId, BoxConstraints>,
2471 measure_cache: &mut HashMap<MeasureCacheKey, LayoutSize>,
2472 scroll_source: &impl ScrollDataSource,
2473 depth: usize,
2474 ) -> Result<f32> {
2475 let Some(node) = self.graph_state.node(node_id) else {
2476 return Ok(0.0);
2477 };
2478 if let (Some(runs), Some(measurer)) = (&node.rich_text, &self.measurer) {
2479 return Ok(match intrinsic {
2480 IntrinsicAxis::Max => measurer.layout_rich_text(runs, None).width,
2481 IntrinsicAxis::Min => runs
2482 .iter()
2483 .flat_map(|run| {
2484 run.text.split_whitespace().map(move |word| {
2485 measurer.measure(word, run.style.font_size, None).0
2486 + run.style.letter_spacing
2487 * word.chars().count().saturating_sub(1) as f32
2488 })
2489 })
2490 .fold(0.0, f32::max),
2491 });
2492 }
2493
2494 if matches!(node.op, LayoutOp::GridItem { .. } | LayoutOp::Align)
2495 && node.children_ids.len() == 1
2496 {
2497 return self.measure_grid_intrinsic_width(
2498 node.children_ids[0],
2499 intrinsic,
2500 max_height,
2501 out,
2502 constraints_out,
2503 measure_cache,
2504 scroll_source,
2505 depth + 1,
2506 );
2507 }
2508
2509 let constraints = BoxConstraints {
2510 min_w: 0.0,
2511 max_w: f32::INFINITY,
2512 min_h: 0.0,
2513 max_h: if max_height.is_finite() {
2514 max_height
2515 } else {
2516 f32::INFINITY
2517 },
2518 };
2519 Ok(self
2520 .layout_node_constraints(
2521 node_id,
2522 constraints,
2523 LayoutPoint::ZERO,
2524 out,
2525 constraints_out,
2526 measure_cache,
2527 scroll_source,
2528 false,
2529 depth + 1,
2530 )?
2531 .width)
2532 }
2533
2534 fn layout_node_constraints(
2535 &self,
2536 node_id: WidgetId,
2537 constraints: BoxConstraints,
2538 origin: LayoutPoint,
2539 out: &mut HashMap<WidgetId, LayoutNodeGeometry>,
2540 constraints_out: &mut HashMap<WidgetId, BoxConstraints>,
2541 measure_cache: &mut HashMap<MeasureCacheKey, LayoutSize>,
2542 scroll_source: &impl ScrollDataSource,
2543 record: bool,
2544 depth: usize,
2545 ) -> Result<LayoutSize> {
2546 if depth > Self::MAX_LAYOUT_RECURSION_DEPTH {
2547 return Err(self.layout_depth_overflow(node_id, depth));
2548 }
2549 if !record {
2550 let cache_key = MeasureCacheKey::new(node_id, constraints);
2551 if let Some(cached) = measure_cache.get(&cache_key).copied() {
2552 return Ok(cached);
2553 }
2554 }
2555 let node = match self.graph_state.node(node_id) {
2556 Some(node) => node,
2557 None => return Ok(LayoutSize::ZERO),
2558 };
2559
2560 if record {
2561 constraints_out.insert(node_id, constraints);
2562 }
2563
2564 if record {
2565 if let Some(reused) =
2566 self.copy_cached_subtree(node_id, origin, constraints, out, constraints_out)?
2567 {
2568 return Ok(reused);
2569 }
2570 }
2571
2572 let mut flow_children: Vec<WidgetId> = Vec::new();
2573 let mut abs_children: Vec<WidgetId> = Vec::new();
2574 for child_id in self.graph_state.children_of(node_id) {
2575 let is_absolute = matches!(
2576 self.graph_state.node(*child_id).map(|n| &n.op),
2577 Some(LayoutOp::AbsoluteFill)
2578 | Some(LayoutOp::Positioned { .. })
2579 | Some(LayoutOp::PositionedLengths { .. })
2580 );
2581 if is_absolute {
2582 abs_children.push(*child_id);
2583 } else {
2584 flow_children.push(*child_id);
2585 }
2586 }
2587 let rich_text_inline_children = node.rich_text.is_some() && !flow_children.is_empty();
2588
2589 let mut resolved_style_op = match &node.op {
2590 LayoutOp::StyledBox {
2591 style,
2592 flex_grow,
2593 flex_shrink,
2594 } => {
2595 let mut op = resolve_box_style(style, constraints, self.active_viewport);
2596 if let LayoutOp::Box {
2597 flex_grow: resolved_grow,
2598 flex_shrink: resolved_shrink,
2599 ..
2600 } = &mut op
2601 {
2602 *resolved_grow = *flex_grow;
2603 *resolved_shrink = *flex_shrink;
2604 }
2605 Some(op)
2606 }
2607 _ => None,
2608 };
2609 if let (
2610 LayoutOp::StyledBox { style, .. },
2611 Some(LayoutOp::Box {
2612 width,
2613 min_width,
2614 max_width,
2615 padding,
2616 ..
2617 }),
2618 ) = (&node.op, &mut resolved_style_op)
2619 {
2620 let needs_intrinsic_width = [
2621 style.width.as_ref(),
2622 style.min_width.as_ref(),
2623 style.max_width.as_ref(),
2624 ]
2625 .into_iter()
2626 .flatten()
2627 .any(length_requires_measurement);
2628 if needs_intrinsic_width {
2629 let mut min_content = 0.0f32;
2630 let mut max_content = 0.0f32;
2631 if let (Some(runs), Some(measurer)) = (&node.rich_text, &self.measurer) {
2632 min_content = runs
2633 .iter()
2634 .flat_map(|run| {
2635 run.text.split_whitespace().map(move |word| {
2636 measurer.measure(word, run.style.font_size, None).0
2637 + run.style.letter_spacing
2638 * word.chars().count().saturating_sub(1) as f32
2639 })
2640 })
2641 .fold(0.0, f32::max);
2642 max_content = measurer.layout_rich_text(runs, None).width;
2643 }
2644 for child_id in &flow_children {
2645 min_content = min_content.max(self.measure_grid_intrinsic_width(
2646 *child_id,
2647 IntrinsicAxis::Min,
2648 constraints.max_h,
2649 out,
2650 constraints_out,
2651 measure_cache,
2652 scroll_source,
2653 depth + 1,
2654 )?);
2655 max_content = max_content.max(self.measure_grid_intrinsic_width(
2656 *child_id,
2657 IntrinsicAxis::Max,
2658 constraints.max_h,
2659 out,
2660 constraints_out,
2661 measure_cache,
2662 scroll_source,
2663 depth + 1,
2664 )?);
2665 }
2666 let horizontal_padding = padding[0] + padding[1];
2667 min_content += horizontal_padding;
2668 max_content =
2669 max_content.max(min_content - horizontal_padding) + horizontal_padding;
2670 let available = if constraints.max_w.is_finite() {
2671 constraints.max_w
2672 } else {
2673 max_content
2674 };
2675 let resolve = |length: &Option<Length>| {
2676 length.as_ref().and_then(|length| {
2677 resolve_measured_length(
2678 length,
2679 available,
2680 self.active_viewport,
2681 min_content,
2682 max_content,
2683 )
2684 })
2685 };
2686 *width = resolve(&style.width);
2687 *min_width = resolve(&style.min_width);
2688 *max_width = resolve(&style.max_width);
2689 }
2690 }
2691 let layout_op = resolved_style_op.as_ref().unwrap_or(&node.op);
2692 let box_alignment = match &node.op {
2693 LayoutOp::StyledBox { style, .. } => style.alignment,
2694 LayoutOp::Box { .. }
2698 if node.rich_text.is_some()
2699 || node.parent_id.is_some_and(|parent_id| {
2700 matches!(
2701 self.graph_state.node(parent_id).map(|parent| &parent.op),
2702 Some(LayoutOp::Flex { .. })
2703 | Some(LayoutOp::StyledBox { flex_grow: 0.0, .. })
2704 )
2705 }) =>
2706 {
2707 fission_ir::op::BoxAlignment::Start
2708 }
2709 LayoutOp::Box { .. } => fission_ir::op::BoxAlignment::Stretch,
2710 _ => fission_ir::op::BoxAlignment::Start,
2711 };
2712 let intrinsic_box_width = match &node.op {
2713 LayoutOp::StyledBox { style, .. } => style.width.as_ref(),
2714 _ => None,
2715 };
2716
2717 let mut content_size;
2718 let size = match layout_op {
2719 LayoutOp::Box {
2720 width,
2721 height,
2722 min_width,
2723 max_width,
2724 min_height,
2725 max_height,
2726 padding,
2727 aspect_ratio,
2728 ..
2729 } => {
2730 let mut local =
2731 constraints.apply_min_max(*min_width, *max_width, *min_height, *max_height);
2732 local = local.tighten(*width, *height);
2733 if node.rich_text.is_some() && height.is_none() {
2738 local.min_h = 0.0;
2739 local.max_h = f32::INFINITY;
2740 }
2741 if let Some(ratio) = aspect_ratio.filter(|r| *r > 0.0) {
2742 let mut target_w = *width;
2743 let mut target_h = *height;
2744
2745 if target_w.is_some() && target_h.is_none() {
2746 target_h = target_w.map(|w| w / ratio);
2747 } else if target_h.is_some() && target_w.is_none() {
2748 target_w = target_h.map(|h| h * ratio);
2749 } else if target_w.is_none() && target_h.is_none() {
2750 if local.is_width_bounded() || local.is_height_bounded() {
2751 let (mut w, mut h) = if local.is_width_bounded() {
2752 let w = local.max_w;
2753 let h = w / ratio;
2754 (w, h)
2755 } else {
2756 let h = local.max_h;
2757 let w = h * ratio;
2758 (w, h)
2759 };
2760 if local.is_width_bounded()
2761 && local.is_height_bounded()
2762 && h > local.max_h
2763 {
2764 h = local.max_h;
2765 w = h * ratio;
2766 }
2767 target_w = Some(w);
2768 target_h = Some(h);
2769 }
2770 }
2771
2772 if target_w.is_some() || target_h.is_some() {
2773 local = local.tighten(target_w, target_h);
2774 }
2775 }
2776 let mut base_child_constraints = local.deflate(*padding);
2777 if matches!(intrinsic_box_width, Some(Length::MaxContent)) {
2778 base_child_constraints.min_w = 0.0;
2779 base_child_constraints.max_w = f32::INFINITY;
2780 }
2781 if box_alignment != fission_ir::op::BoxAlignment::Stretch {
2782 base_child_constraints.min_w = 0.0;
2783 base_child_constraints.min_h = 0.0;
2784 }
2785 let mut max_child = LayoutSize::ZERO;
2786 let mut measured_children: Vec<(WidgetId, BoxConstraints, LayoutSize)> = Vec::new();
2787 if !rich_text_inline_children {
2788 for child_id in &flow_children {
2789 let (child_width, child_height, child_max_width, child_max_height) = self
2790 .graph_state
2791 .node(*child_id)
2792 .map(|child| match &child.op {
2793 LayoutOp::Box {
2794 width,
2795 height,
2796 max_width,
2797 max_height,
2798 ..
2799 } => (*width, *height, *max_width, *max_height),
2800 LayoutOp::Scroll {
2801 width,
2802 height,
2803 max_width,
2804 max_height,
2805 ..
2806 } => (*width, *height, *max_width, *max_height),
2807 LayoutOp::Embed { width, height, .. } => {
2808 (*width, *height, None, None)
2809 }
2810 LayoutOp::StyledBox { style, .. } => {
2811 let resolved = resolve_box_style(
2812 style,
2813 base_child_constraints,
2814 self.active_viewport,
2815 );
2816 match resolved {
2817 LayoutOp::Box {
2818 width,
2819 height,
2820 max_width,
2821 max_height,
2822 ..
2823 } => (width, height, max_width, max_height),
2824 _ => unreachable!(),
2825 }
2826 }
2827 _ => (None, None, None, None),
2828 })
2829 .unwrap_or((None, None, None, None));
2830 let mut child_constraints = base_child_constraints;
2831 if matches!(intrinsic_box_width, Some(Length::MinContent)) {
2832 let intrinsic_width = self.measure_grid_intrinsic_width(
2833 *child_id,
2834 IntrinsicAxis::Min,
2835 base_child_constraints.max_h,
2836 out,
2837 constraints_out,
2838 measure_cache,
2839 scroll_source,
2840 depth + 1,
2841 )?;
2842 child_constraints.min_w = intrinsic_width;
2843 child_constraints.max_w = intrinsic_width;
2844 }
2845 let tight_width = child_constraints.min_w == child_constraints.max_w;
2846 let stretch_width =
2847 tight_width && child_width.is_none() && child_max_width.is_none();
2848 if matches!(box_alignment, fission_ir::op::BoxAlignment::Stretch)
2849 && child_width.is_none()
2850 && child_max_width.is_none()
2851 && child_constraints.max_w.is_finite()
2852 {
2853 child_constraints.min_w = child_constraints.max_w;
2854 } else if stretch_width {
2855 child_constraints.min_w = child_constraints.max_w;
2856 } else if tight_width
2857 && (child_width.is_some() || child_max_width.is_some())
2858 {
2859 child_constraints.min_w = 0.0;
2860 }
2861 let tight_height = child_constraints.min_h == child_constraints.max_h;
2862 let stretch_height =
2863 tight_height && child_height.is_none() && child_max_height.is_none();
2864 if matches!(box_alignment, fission_ir::op::BoxAlignment::Stretch)
2865 && child_height.is_none()
2866 && child_max_height.is_none()
2867 && child_constraints.max_h.is_finite()
2868 {
2869 child_constraints.min_h = child_constraints.max_h;
2870 } else if stretch_height {
2871 child_constraints.min_h = child_constraints.max_h;
2872 } else if tight_height
2873 && (child_height.is_some() || child_max_height.is_some())
2874 {
2875 child_constraints.min_h = 0.0;
2876 }
2877 let child_size = self.layout_node_constraints(
2878 *child_id,
2879 child_constraints,
2880 LayoutPoint::ZERO,
2881 out,
2882 constraints_out,
2883 measure_cache,
2884 scroll_source,
2885 false,
2886 depth + 1,
2887 )?;
2888 max_child.width = max_child.width.max(child_size.width);
2889 max_child.height = max_child.height.max(child_size.height);
2890 measured_children.push((*child_id, child_constraints, child_size));
2891 }
2892 }
2893 let padded = LayoutSize::new(
2894 max_child.width + padding[0] + padding[1],
2895 max_child.height + padding[2] + padding[3],
2896 );
2897 if let LayoutOp::StyledBox { style, .. } = &node.op {
2898 let available = if constraints.max_h.is_finite() {
2899 constraints.max_h
2900 } else {
2901 padded.height
2902 };
2903 let resolve_intrinsic_height = |length: &Option<Length>| {
2904 length
2905 .as_ref()
2906 .filter(|length| length_requires_measurement(length))
2907 .and_then(|length| {
2908 resolve_measured_length(
2909 length,
2910 available,
2911 self.active_viewport,
2912 padded.height,
2913 padded.height,
2914 )
2915 })
2916 };
2917 local = local.apply_min_max(
2918 None,
2919 None,
2920 resolve_intrinsic_height(&style.min_height),
2921 resolve_intrinsic_height(&style.max_height),
2922 );
2923 local = local.tighten(None, resolve_intrinsic_height(&style.height));
2924 }
2925 let size = local.constrain(padded);
2926 if record {
2927 for (child_id, child_constraints, child_size) in measured_children {
2928 let inner_width = (size.width - padding[0] - padding[1]).max(0.0);
2929 let inner_height = (size.height - padding[2] - padding[3]).max(0.0);
2930 let offset = |available: f32, child: f32| match box_alignment {
2931 fission_ir::op::BoxAlignment::Start
2932 | fission_ir::op::BoxAlignment::Stretch => 0.0,
2933 fission_ir::op::BoxAlignment::Center => {
2934 ((available - child) / 2.0).max(0.0)
2935 }
2936 fission_ir::op::BoxAlignment::End => (available - child).max(0.0),
2937 };
2938 self.layout_node_constraints(
2939 child_id,
2940 child_constraints,
2941 LayoutPoint::new(
2942 origin.x + padding[0] + offset(inner_width, child_size.width),
2943 origin.y + padding[2] + offset(inner_height, child_size.height),
2944 ),
2945 out,
2946 constraints_out,
2947 measure_cache,
2948 scroll_source,
2949 record,
2950 depth + 1,
2951 )?;
2952 }
2953 if !abs_children.is_empty() {
2954 let abs_constraints = BoxConstraints::loose(size.width, size.height);
2955 for child_id in abs_children {
2956 self.layout_node_constraints(
2957 child_id,
2958 abs_constraints,
2959 origin,
2960 out,
2961 constraints_out,
2962 measure_cache,
2963 scroll_source,
2964 record,
2965 depth + 1,
2966 )?;
2967 }
2968 }
2969 }
2970 content_size = padded;
2971 size
2972 }
2973 LayoutOp::Flex {
2974 direction,
2975 wrap,
2976 padding,
2977 gap,
2978 align_items,
2979 justify_content,
2980 flex_grow,
2981 ..
2982 } => {
2983 let gap = gap.unwrap_or(0.0);
2984 let local = constraints.tighten(node.width, node.height);
2985 let inner = local.deflate(*padding);
2986 let is_row = matches!(direction, IrFlexDirection::Row);
2987
2988 let max_main = if is_row { inner.max_w } else { inner.max_h };
2989 let max_cross = if is_row { inner.max_h } else { inner.max_w };
2990 let min_main = if is_row { inner.min_w } else { inner.min_h };
2991 let min_cross = if is_row { inner.min_h } else { inner.min_w };
2992 let main_bounded = if is_row {
2993 inner.is_width_bounded()
2994 } else {
2995 inner.is_height_bounded()
2996 };
2997 let cross_bounded = if is_row {
2998 inner.is_height_bounded()
2999 } else {
3000 inner.is_width_bounded()
3001 };
3002
3003 if matches!(wrap, IrFlexWrap::Wrap | IrFlexWrap::WrapReverse) {
3004 let mut lines: Vec<(Vec<(WidgetId, LayoutSize, BoxConstraints)>, f32, f32)> =
3005 Vec::new();
3006 let mut line_children: Vec<(WidgetId, LayoutSize, BoxConstraints)> = Vec::new();
3007 let mut line_main = 0.0f32;
3008 let mut line_cross = 0.0f32;
3009 let mut max_line_main = 0.0f32;
3010
3011 for child_id in &flow_children {
3012 let child_constraints = if is_row {
3013 BoxConstraints {
3014 min_w: 0.0,
3015 max_w: max_main,
3016 min_h: 0.0,
3017 max_h: max_cross,
3018 }
3019 } else {
3020 BoxConstraints {
3021 min_w: 0.0,
3022 max_w: max_cross,
3023 min_h: 0.0,
3024 max_h: max_main,
3025 }
3026 };
3027 let child_size = self.layout_node_constraints(
3028 *child_id,
3029 child_constraints,
3030 LayoutPoint::ZERO,
3031 out,
3032 constraints_out,
3033 measure_cache,
3034 scroll_source,
3035 false,
3036 depth + 1,
3037 )?;
3038 let child_main = if is_row {
3039 child_size.width
3040 } else {
3041 child_size.height
3042 };
3043 let child_cross = if is_row {
3044 child_size.height
3045 } else {
3046 child_size.width
3047 };
3048 let next_main = if line_children.is_empty() {
3049 child_main
3050 } else {
3051 line_main + gap + child_main
3052 };
3053
3054 if main_bounded && !line_children.is_empty() && next_main > max_main {
3055 max_line_main = max_line_main.max(line_main);
3056 lines.push((line_children, line_main, line_cross));
3057 line_children = Vec::new();
3058 line_main = 0.0;
3059 line_cross = 0.0;
3060 }
3061
3062 if !line_children.is_empty() {
3063 line_main += gap;
3064 }
3065 line_main += child_main;
3066 line_cross = line_cross.max(child_cross);
3067 line_children.push((*child_id, child_size, child_constraints));
3068 }
3069
3070 if !line_children.is_empty() {
3071 max_line_main = max_line_main.max(line_main);
3072 lines.push((line_children, line_main, line_cross));
3073 }
3074
3075 let mut container_main = if main_bounded && *flex_grow > 0.0 {
3076 max_main
3077 } else {
3078 max_line_main
3079 };
3080 container_main = container_main.max(min_main);
3081 let total_lines_cross: f32 =
3082 lines.iter().map(|(_, _, cross)| *cross).sum::<f32>()
3083 + gap * lines.len().saturating_sub(1) as f32;
3084 let container_cross = total_lines_cross.max(min_cross);
3085 let size = if is_row {
3086 local.constrain(LayoutSize::new(
3087 container_main + padding[0] + padding[1],
3088 container_cross + padding[2] + padding[3],
3089 ))
3090 } else {
3091 local.constrain(LayoutSize::new(
3092 container_cross + padding[0] + padding[1],
3093 container_main + padding[2] + padding[3],
3094 ))
3095 };
3096
3097 let inner_main = if is_row {
3098 size.width - padding[0] - padding[1]
3099 } else {
3100 size.height - padding[2] - padding[3]
3101 };
3102 let inner_cross = if is_row {
3103 size.height - padding[2] - padding[3]
3104 } else {
3105 size.width - padding[0] - padding[1]
3106 };
3107
3108 let mut ordered_lines = lines;
3109 if matches!(wrap, IrFlexWrap::WrapReverse) {
3110 ordered_lines.reverse();
3111 }
3112
3113 let mut line_cursor = if matches!(wrap, IrFlexWrap::WrapReverse) {
3114 (inner_cross - total_lines_cross).max(0.0)
3115 } else {
3116 0.0
3117 };
3118
3119 for (line_children, line_main, line_cross) in ordered_lines {
3120 let remaining_space = (inner_main - line_main).max(0.0);
3121 let mut extra_gap = 0.0;
3122 let mut offset_main = 0.0;
3123 match justify_content {
3124 fission_ir::op::JustifyContent::Start => {}
3125 fission_ir::op::JustifyContent::End => offset_main = remaining_space,
3126 fission_ir::op::JustifyContent::Center => {
3127 offset_main = remaining_space / 2.0
3128 }
3129 fission_ir::op::JustifyContent::SpaceBetween => {
3130 if line_children.len() > 1 {
3131 extra_gap =
3132 remaining_space / (line_children.len() as f32 - 1.0);
3133 }
3134 }
3135 fission_ir::op::JustifyContent::SpaceAround => {
3136 if !line_children.is_empty() {
3137 extra_gap = remaining_space / line_children.len() as f32;
3138 offset_main = extra_gap / 2.0;
3139 }
3140 }
3141 fission_ir::op::JustifyContent::SpaceEvenly => {
3142 if !line_children.is_empty() {
3143 extra_gap =
3144 remaining_space / (line_children.len() as f32 + 1.0);
3145 offset_main = extra_gap;
3146 }
3147 }
3148 }
3149
3150 let mut cursor = offset_main;
3151 for (child_id, child_size, mut child_constraints) in line_children {
3152 let child_main = if is_row {
3153 child_size.width
3154 } else {
3155 child_size.height
3156 };
3157 let child_cross = if is_row {
3158 child_size.height
3159 } else {
3160 child_size.width
3161 };
3162 if matches!(align_items, fission_ir::op::AlignItems::Stretch) {
3163 if is_row {
3164 child_constraints.min_h = line_cross;
3165 child_constraints.max_h = line_cross;
3166 } else {
3167 child_constraints.min_w = line_cross;
3168 child_constraints.max_w = line_cross;
3169 }
3170 }
3171 let cross_offset = match align_items {
3172 fission_ir::op::AlignItems::Start
3173 | fission_ir::op::AlignItems::Stretch => 0.0,
3174 fission_ir::op::AlignItems::End => {
3175 (line_cross - child_cross).max(0.0)
3176 }
3177 fission_ir::op::AlignItems::Center => {
3178 ((line_cross - child_cross) / 2.0).max(0.0)
3179 }
3180 fission_ir::op::AlignItems::Baseline => 0.0,
3181 };
3182 let child_origin = if is_row {
3183 LayoutPoint::new(
3184 origin.x + padding[0] + cursor,
3185 origin.y + padding[2] + line_cursor + cross_offset,
3186 )
3187 } else {
3188 LayoutPoint::new(
3189 origin.x + padding[0] + line_cursor + cross_offset,
3190 origin.y + padding[2] + cursor,
3191 )
3192 };
3193 self.layout_node_constraints(
3194 child_id,
3195 child_constraints,
3196 child_origin,
3197 out,
3198 constraints_out,
3199 measure_cache,
3200 scroll_source,
3201 record,
3202 depth + 1,
3203 )?;
3204 cursor += child_main + gap + extra_gap;
3205 }
3206
3207 line_cursor += line_cross + gap;
3208 }
3209
3210 if record && !abs_children.is_empty() {
3211 let abs_constraints = BoxConstraints::loose(size.width, size.height);
3212 for child_id in abs_children {
3213 self.layout_node_constraints(
3214 child_id,
3215 abs_constraints,
3216 origin,
3217 out,
3218 constraints_out,
3219 measure_cache,
3220 scroll_source,
3221 record,
3222 depth + 1,
3223 )?;
3224 }
3225 }
3226 content_size = size;
3227 size
3228 } else {
3229 struct FlexChildEntry {
3230 id: WidgetId,
3231 flex: f32,
3232 size: LayoutSize,
3233 constraints: BoxConstraints,
3234 is_flex: bool,
3235 }
3236 let mut measured: Vec<FlexChildEntry> = Vec::new();
3237 let mut total_flex = 0.0f32;
3238 let mut nonflex_main = 0.0f32;
3239 let mut max_child_cross = 0.0f32;
3240 let treat_flex_as_nonflex = !main_bounded;
3241
3242 for child_id in &flow_children {
3243 let child = match self.graph_state.node(*child_id) {
3244 Some(child) => child,
3245 None => continue,
3246 };
3247 let flex = child.flex_grow;
3248 if flex > 0.0 && !treat_flex_as_nonflex {
3249 total_flex += flex;
3250 measured.push(FlexChildEntry {
3251 id: *child_id,
3252 flex,
3253 size: LayoutSize::ZERO,
3254 constraints: BoxConstraints::loose(0.0, 0.0),
3255 is_flex: true,
3256 });
3257 continue;
3258 }
3259 let child_constraints = if is_row {
3260 let cross =
3261 if matches!(align_items, fission_ir::op::AlignItems::Stretch)
3262 && cross_bounded
3263 && child.rich_text.is_none()
3264 && !matches!(
3265 child.op,
3266 LayoutOp::Box {
3267 width: None,
3268 height: None,
3269 ..
3270 }
3271 )
3272 {
3273 BoxConstraints {
3274 min_w: 0.0,
3275 max_w: f32::INFINITY,
3276 min_h: max_cross,
3277 max_h: max_cross,
3278 }
3279 } else {
3280 BoxConstraints {
3281 min_w: 0.0,
3282 max_w: f32::INFINITY,
3283 min_h: 0.0,
3284 max_h: max_cross,
3285 }
3286 };
3287 cross
3288 } else {
3289 let cross =
3290 if matches!(align_items, fission_ir::op::AlignItems::Stretch)
3291 && cross_bounded
3292 && child.rich_text.is_none()
3293 && !matches!(
3294 child.op,
3295 LayoutOp::Box {
3296 width: None,
3297 height: None,
3298 ..
3299 }
3300 )
3301 {
3302 BoxConstraints {
3303 min_w: max_cross,
3304 max_w: max_cross,
3305 min_h: 0.0,
3306 max_h: f32::INFINITY,
3307 }
3308 } else {
3309 BoxConstraints {
3310 min_w: 0.0,
3311 max_w: max_cross,
3312 min_h: 0.0,
3313 max_h: f32::INFINITY,
3314 }
3315 };
3316 cross
3317 };
3318 let child_size = self.layout_node_constraints(
3319 *child_id,
3320 child_constraints,
3321 LayoutPoint::ZERO,
3322 out,
3323 constraints_out,
3324 measure_cache,
3325 scroll_source,
3326 false,
3327 depth + 1,
3328 )?;
3329 let child_main = if is_row {
3330 child_size.width
3331 } else {
3332 child_size.height
3333 };
3334 let child_cross = if is_row {
3335 child_size.height
3336 } else {
3337 child_size.width
3338 };
3339 nonflex_main += child_main;
3340 max_child_cross = max_child_cross.max(child_cross);
3341 measured.push(FlexChildEntry {
3342 id: *child_id,
3343 flex,
3344 size: child_size,
3345 constraints: child_constraints,
3346 is_flex: false,
3347 });
3348 }
3349
3350 let gap_total = gap * flow_children.len().saturating_sub(1) as f32;
3351 let remaining = if main_bounded {
3352 (max_main - nonflex_main - gap_total).max(0.0)
3353 } else {
3354 0.0
3355 };
3356
3357 for entry in measured.iter_mut().filter(|e| e.is_flex) {
3358 let flex = entry.flex;
3359 let allocated = if main_bounded && total_flex > 0.0 {
3360 remaining * (flex / total_flex)
3361 } else {
3362 0.0
3363 };
3364 let child_constraints = if is_row {
3365 let cross =
3366 if matches!(align_items, fission_ir::op::AlignItems::Stretch)
3367 && cross_bounded
3368 {
3369 BoxConstraints {
3370 min_w: allocated,
3371 max_w: allocated,
3372 min_h: max_cross,
3373 max_h: max_cross,
3374 }
3375 } else {
3376 BoxConstraints {
3377 min_w: allocated,
3378 max_w: allocated,
3379 min_h: 0.0,
3380 max_h: max_cross,
3381 }
3382 };
3383 cross
3384 } else {
3385 let cross =
3386 if matches!(align_items, fission_ir::op::AlignItems::Stretch)
3387 && cross_bounded
3388 {
3389 BoxConstraints {
3390 min_w: max_cross,
3391 max_w: max_cross,
3392 min_h: allocated,
3393 max_h: allocated,
3394 }
3395 } else {
3396 BoxConstraints {
3397 min_w: 0.0,
3398 max_w: max_cross,
3399 min_h: allocated,
3400 max_h: allocated,
3401 }
3402 };
3403 cross
3404 };
3405 let child_size = self.layout_node_constraints(
3406 entry.id,
3407 child_constraints,
3408 LayoutPoint::ZERO,
3409 out,
3410 constraints_out,
3411 measure_cache,
3412 scroll_source,
3413 false,
3414 depth + 1,
3415 )?;
3416 let child_cross = if is_row {
3417 child_size.height
3418 } else {
3419 child_size.width
3420 };
3421 max_child_cross = max_child_cross.max(child_cross);
3422 entry.size = child_size;
3423 entry.constraints = child_constraints;
3424 }
3425
3426 let final_children_main: f32 = measured
3427 .iter()
3428 .map(|entry| {
3429 if is_row {
3430 entry.size.width
3431 } else {
3432 entry.size.height
3433 }
3434 })
3435 .sum();
3436
3437 let mut container_main = if main_bounded && *flex_grow > 0.0 {
3438 max_main
3439 } else {
3440 final_children_main + gap_total
3441 };
3442 container_main = container_main.max(min_main);
3443
3444 if main_bounded && final_children_main + gap_total > max_main {
3445 let mut total_shrink_scaled = 0.0f32;
3447 for entry in &measured {
3448 let Some(child) = self.graph_state.node(entry.id) else {
3449 continue;
3450 };
3451 let main_size = if is_row {
3452 entry.size.width
3453 } else {
3454 entry.size.height
3455 };
3456 total_shrink_scaled += main_size * child.flex_shrink;
3457 }
3458
3459 if total_shrink_scaled > 0.0 {
3460 let overflow = (final_children_main + gap_total) - max_main;
3461 for entry in &mut measured {
3462 let Some(child) = self.graph_state.node(entry.id) else {
3463 continue;
3464 };
3465 let main_size = if is_row {
3466 entry.size.width
3467 } else {
3468 entry.size.height
3469 };
3470 let shrink_amount = (main_size * child.flex_shrink
3471 / total_shrink_scaled)
3472 * overflow;
3473 let floor = if child.flex_shrink > 0.0 {
3477 let explicit_min = match &child.op {
3479 LayoutOp::Box {
3480 min_width,
3481 min_height,
3482 height,
3483 width,
3484 ..
3485 } => {
3486 if is_row {
3487 min_width.or(*width).unwrap_or(0.0)
3488 } else {
3489 min_height.or(*height).unwrap_or(0.0)
3490 }
3491 }
3492 _ => 0.0,
3493 };
3494 explicit_min
3495 } else {
3496 main_size };
3498 let new_main = (main_size - shrink_amount).max(floor);
3499
3500 let mut child_constraints = entry.constraints;
3501 if is_row {
3502 child_constraints.min_w = new_main;
3503 child_constraints.max_w = new_main;
3504 } else {
3505 child_constraints.min_h = new_main;
3506 child_constraints.max_h = new_main;
3507 }
3508 let new_size = self.layout_node_constraints(
3509 entry.id,
3510 child_constraints,
3511 LayoutPoint::ZERO,
3512 out,
3513 constraints_out,
3514 measure_cache,
3515 scroll_source,
3516 false,
3517 depth + 1,
3518 )?;
3519 entry.size = new_size;
3520 entry.constraints = child_constraints;
3521 }
3522 }
3523 }
3524
3525 let container_cross = max_child_cross.max(min_cross);
3526 let size = if is_row {
3527 local.constrain(LayoutSize::new(
3528 container_main + padding[0] + padding[1],
3529 container_cross + padding[2] + padding[3],
3530 ))
3531 } else {
3532 local.constrain(LayoutSize::new(
3533 container_cross + padding[0] + padding[1],
3534 container_main + padding[2] + padding[3],
3535 ))
3536 };
3537
3538 let inner_main = if is_row {
3539 size.width - padding[0] - padding[1]
3540 } else {
3541 size.height - padding[2] - padding[3]
3542 };
3543 let inner_cross = if is_row {
3544 size.height - padding[2] - padding[3]
3545 } else {
3546 size.width - padding[0] - padding[1]
3547 };
3548
3549 let final_children_main: f32 = measured
3550 .iter()
3551 .map(|entry| {
3552 if is_row {
3553 entry.size.width
3554 } else {
3555 entry.size.height
3556 }
3557 })
3558 .sum();
3559
3560 let remaining_space = (inner_main - final_children_main - gap_total).max(0.0);
3561 let mut extra_gap = 0.0;
3562 let mut offset_main = 0.0;
3563 match justify_content {
3564 fission_ir::op::JustifyContent::Start => {}
3565 fission_ir::op::JustifyContent::End => offset_main = remaining_space,
3566 fission_ir::op::JustifyContent::Center => {
3567 offset_main = remaining_space / 2.0
3568 }
3569 fission_ir::op::JustifyContent::SpaceBetween => {
3570 if measured.len() > 1 {
3571 extra_gap = remaining_space / (measured.len() as f32 - 1.0);
3572 }
3573 }
3574 fission_ir::op::JustifyContent::SpaceAround => {
3575 if !measured.is_empty() {
3576 extra_gap = remaining_space / measured.len() as f32;
3577 offset_main = extra_gap / 2.0;
3578 }
3579 }
3580 fission_ir::op::JustifyContent::SpaceEvenly => {
3581 if !measured.is_empty() {
3582 extra_gap = remaining_space / (measured.len() as f32 + 1.0);
3583 offset_main = extra_gap;
3584 }
3585 }
3586 }
3587
3588 let mut cursor = offset_main;
3589 for entry in measured {
3590 let child_main = if is_row {
3591 entry.size.width
3592 } else {
3593 entry.size.height
3594 };
3595 let child_cross = if is_row {
3596 entry.size.height
3597 } else {
3598 entry.size.width
3599 };
3600 let cross_offset = match align_items {
3601 fission_ir::op::AlignItems::Start
3602 | fission_ir::op::AlignItems::Stretch => 0.0,
3603 fission_ir::op::AlignItems::End => (inner_cross - child_cross).max(0.0),
3604 fission_ir::op::AlignItems::Center => {
3605 ((inner_cross - child_cross) / 2.0).max(0.0)
3606 }
3607 fission_ir::op::AlignItems::Baseline => 0.0,
3608 };
3609 let child_origin = if is_row {
3610 LayoutPoint::new(
3611 origin.x + padding[0] + cursor,
3612 origin.y + padding[2] + cross_offset,
3613 )
3614 } else {
3615 LayoutPoint::new(
3616 origin.x + padding[0] + cross_offset,
3617 origin.y + padding[2] + cursor,
3618 )
3619 };
3620
3621 let mut child_constraints = entry.constraints;
3622 if matches!(align_items, fission_ir::op::AlignItems::Stretch) {
3623 let child_node = self.graph_state.node(entry.id);
3625 let has_explicit_cross = child_node
3626 .map(|n| match &n.op {
3627 LayoutOp::Box { width, height, .. } => {
3628 if is_row {
3629 height.is_some()
3630 } else {
3631 width.is_some()
3632 }
3633 }
3634 _ => false,
3635 })
3636 .unwrap_or(false);
3637 let is_measured_text = child_node.is_some_and(|node| {
3641 node.rich_text.is_some()
3642 || matches!(
3643 node.op,
3644 LayoutOp::Box {
3645 width: None,
3646 height: None,
3647 ..
3648 }
3649 )
3650 });
3651 if !has_explicit_cross && !is_measured_text {
3652 if is_row {
3653 child_constraints.min_h = inner_cross;
3654 child_constraints.max_h = inner_cross;
3655 } else {
3656 child_constraints.min_w = inner_cross;
3657 child_constraints.max_w = inner_cross;
3658 }
3659 }
3660 }
3661
3662 self.layout_node_constraints(
3663 entry.id,
3664 child_constraints,
3665 child_origin,
3666 out,
3667 constraints_out,
3668 measure_cache,
3669 scroll_source,
3670 record,
3671 depth + 1,
3672 )?;
3673 cursor += child_main + gap + extra_gap;
3674 }
3675
3676 if record && !abs_children.is_empty() {
3677 let abs_constraints = BoxConstraints::loose(size.width, size.height);
3678 for child_id in abs_children {
3679 self.layout_node_constraints(
3680 child_id,
3681 abs_constraints,
3682 origin,
3683 out,
3684 constraints_out,
3685 measure_cache,
3686 scroll_source,
3687 record,
3688 depth + 1,
3689 )?;
3690 }
3691 }
3692 content_size = size;
3693 size
3694 }
3695 }
3696 LayoutOp::Grid {
3697 columns,
3698 rows,
3699 column_gap,
3700 row_gap,
3701 padding,
3702 } => {
3703 let gap_x = column_gap.unwrap_or(0.0);
3704 let gap_y = row_gap.unwrap_or(0.0);
3705 let inner = constraints.deflate(*padding);
3706 let bounded_w = inner.is_width_bounded();
3707 let bounded_h = inner.is_height_bounded();
3708 let child_count = flow_children.len();
3709 let available_w = bounded_w.then_some(inner.max_w);
3710 let available_h = bounded_h.then_some(inner.max_h);
3711 let mut expanded_columns = expand_tracks(columns, available_w, gap_x, child_count);
3712 if expanded_columns.is_empty() {
3713 expanded_columns.push(GridTrack::Auto);
3714 }
3715 let mut col_count = expanded_columns.len();
3716
3717 #[derive(Clone, Copy)]
3718 struct GridCell {
3719 id: WidgetId,
3720 row: usize,
3721 col: usize,
3722 row_span: usize,
3723 col_span: usize,
3724 }
3725
3726 let mut cell_assignments: Vec<GridCell> = Vec::new();
3727 let mut auto_row = 0;
3728 let mut auto_col = 0;
3729 let mut occupied = HashSet::<(usize, usize)>::new();
3730
3731 for child_id in &flow_children {
3732 let Some(child) = self.graph_state.node(*child_id) else {
3733 continue;
3734 };
3735 let (row_start, row_end, col_start, col_end) = if let LayoutOp::GridItem {
3736 row_start,
3737 row_end,
3738 col_start,
3739 col_end,
3740 ..
3741 } = &child.op
3742 {
3743 (*row_start, *row_end, *col_start, *col_end)
3744 } else {
3745 (
3746 GridPlacement::Auto,
3747 GridPlacement::Auto,
3748 GridPlacement::Auto,
3749 GridPlacement::Auto,
3750 )
3751 };
3752 let explicit_row = match row_start {
3753 GridPlacement::Line(line) => Some(line.max(1) as usize - 1),
3754 _ => None,
3755 };
3756 let explicit_col = match col_start {
3757 GridPlacement::Line(line) => Some(line.max(1) as usize - 1),
3758 _ => None,
3759 };
3760 let row_span = match row_end {
3761 GridPlacement::Span(span) => usize::from(span).max(1),
3762 GridPlacement::Line(line) => {
3763 let end = line.max(1) as usize - 1;
3764 end.saturating_sub(explicit_row.unwrap_or_default()).max(1)
3765 }
3766 GridPlacement::Auto => 1,
3767 };
3768 let col_span = match col_end {
3769 GridPlacement::Span(span) => usize::from(span).max(1),
3770 GridPlacement::Line(line) => {
3771 let end = line.max(1) as usize - 1;
3772 end.saturating_sub(explicit_col.unwrap_or_default()).max(1)
3773 }
3774 GridPlacement::Auto => 1,
3775 };
3776 let fits = |row: usize, col: usize, occupied: &HashSet<(usize, usize)>| {
3777 (row..row + row_span).all(|row| {
3778 (col..col + col_span).all(|col| !occupied.contains(&(row, col)))
3779 })
3780 };
3781 let (row, col) = match (explicit_row, explicit_col) {
3782 (Some(row), Some(col)) => (row, col),
3783 (Some(row), None) => {
3784 let mut col = 0;
3785 while !fits(row, col, &occupied) {
3786 col += 1;
3787 }
3788 (row, col)
3789 }
3790 (None, Some(col)) => {
3791 let mut row = 0;
3792 while !fits(row, col, &occupied) {
3793 row += 1;
3794 }
3795 (row, col)
3796 }
3797 (None, None) => {
3798 while (col_span <= col_count && auto_col + col_span > col_count)
3799 || !fits(auto_row, auto_col, &occupied)
3800 {
3801 auto_col += 1;
3802 if auto_col >= col_count {
3803 auto_col = 0;
3804 auto_row += 1;
3805 }
3806 }
3807 let placement = (auto_row, auto_col);
3808 if col_span >= col_count {
3809 auto_col = 0;
3810 auto_row += 1;
3811 } else {
3812 auto_col += col_span;
3813 if auto_col >= col_count {
3814 auto_col = 0;
3815 auto_row += 1;
3816 }
3817 }
3818 placement
3819 }
3820 };
3821 for occupied_row in row..row + row_span {
3822 for occupied_col in col..col + col_span {
3823 occupied.insert((occupied_row, occupied_col));
3824 }
3825 }
3826 cell_assignments.push(GridCell {
3827 id: *child_id,
3828 row,
3829 col,
3830 row_span,
3831 col_span,
3832 });
3833 }
3834
3835 let required_columns = cell_assignments
3836 .iter()
3837 .map(|cell| cell.col + cell.col_span)
3838 .max()
3839 .unwrap_or(1);
3840 if required_columns > col_count {
3841 expanded_columns.resize(required_columns, GridTrack::Auto);
3842 col_count = expanded_columns.len();
3843 }
3844
3845 let mut column_sizing = expanded_columns
3846 .iter()
3847 .map(|track| TrackSizing::from_track(track, available_w))
3848 .collect::<Vec<_>>();
3849
3850 for cell in &cell_assignments {
3851 let intrinsic = column_sizing[cell.col..cell.col + cell.col_span]
3852 .iter()
3853 .filter_map(|track| track.intrinsic)
3854 .fold(None, |current, axis| match (current, axis) {
3855 (Some(IntrinsicAxis::Max), _) | (_, IntrinsicAxis::Max) => {
3856 Some(IntrinsicAxis::Max)
3857 }
3858 _ => Some(IntrinsicAxis::Min),
3859 });
3860 let Some(intrinsic) = intrinsic else {
3861 continue;
3862 };
3863 let width = self.measure_grid_intrinsic_width(
3864 cell.id,
3865 intrinsic,
3866 inner.max_h,
3867 out,
3868 constraints_out,
3869 measure_cache,
3870 scroll_source,
3871 depth + 1,
3872 )?;
3873 distribute_deficit(
3874 &mut column_sizing,
3875 cell.col,
3876 cell.col_span,
3877 (width - gap_x * cell.col_span.saturating_sub(1) as f32).max(0.0),
3878 );
3879 }
3880 if let Some(available_w) = available_w {
3881 distribute_flex(&mut column_sizing, available_w, gap_x);
3882 }
3883 let col_widths = column_sizing
3884 .iter()
3885 .map(|track| track.base)
3886 .collect::<Vec<_>>();
3887
3888 let minimum_rows = cell_assignments
3889 .iter()
3890 .map(|cell| cell.row + cell.row_span)
3891 .max()
3892 .unwrap_or_else(|| (child_count + col_count - 1) / col_count)
3893 .max(1);
3894 let mut expanded_rows = expand_tracks(rows, available_h, gap_y, minimum_rows);
3895 if expanded_rows.is_empty() {
3896 expanded_rows.resize(minimum_rows, GridTrack::Auto);
3897 } else if expanded_rows.len() < minimum_rows {
3898 expanded_rows.resize(minimum_rows, GridTrack::Auto);
3899 }
3900 let mut row_sizing = expanded_rows
3901 .iter()
3902 .map(|track| TrackSizing::from_track(track, available_h))
3903 .collect::<Vec<_>>();
3904
3905 for cell in &cell_assignments {
3906 if cell.row >= row_sizing.len() || cell.col >= col_widths.len() {
3907 continue;
3908 }
3909 let col_end = (cell.col + cell.col_span).min(col_widths.len());
3910 let cell_w = col_widths[cell.col..col_end].iter().sum::<f32>()
3911 + gap_x * col_end.saturating_sub(cell.col + 1) as f32;
3912 let cell_constraints = BoxConstraints {
3913 min_w: 0.0,
3914 max_w: cell_w,
3915 min_h: 0.0,
3916 max_h: f32::INFINITY,
3917 };
3918 let child_size = self.layout_node_constraints(
3919 cell.id,
3920 cell_constraints,
3921 LayoutPoint::ZERO,
3922 out,
3923 constraints_out,
3924 measure_cache,
3925 scroll_source,
3926 false,
3927 depth + 1,
3928 )?;
3929 distribute_deficit(
3930 &mut row_sizing,
3931 cell.row,
3932 cell.row_span,
3933 (child_size.height - gap_y * cell.row_span.saturating_sub(1) as f32)
3934 .max(0.0),
3935 );
3936 }
3937 if let Some(available_h) = available_h {
3938 distribute_flex(&mut row_sizing, available_h, gap_y);
3939 }
3940 let row_heights = row_sizing
3941 .iter()
3942 .map(|track| track.base)
3943 .collect::<Vec<_>>();
3944
3945 let grid_w: f32 =
3946 col_widths.iter().sum::<f32>() + gap_x * (col_count.saturating_sub(1) as f32);
3947 let grid_h: f32 = row_heights.iter().sum::<f32>()
3948 + gap_y * (row_heights.len().saturating_sub(1) as f32);
3949 let size = constraints.constrain(LayoutSize::new(
3950 grid_w + padding[0] + padding[1],
3951 grid_h + padding[2] + padding[3],
3952 ));
3953
3954 if record {
3955 let padding_origin_x = origin.x + padding[0];
3956 let padding_origin_y = origin.y + padding[2];
3957 for cell in &cell_assignments {
3958 if cell.row >= row_heights.len() || cell.col >= col_widths.len() {
3959 continue;
3960 }
3961 let cell_x = padding_origin_x
3962 + col_widths[..cell.col].iter().sum::<f32>()
3963 + gap_x * cell.col as f32;
3964 let cell_y = padding_origin_y
3965 + row_heights[..cell.row].iter().sum::<f32>()
3966 + gap_y * cell.row as f32;
3967 let col_end = (cell.col + cell.col_span).min(col_widths.len());
3968 let row_end = (cell.row + cell.row_span).min(row_heights.len());
3969 let cell_w = col_widths[cell.col..col_end].iter().sum::<f32>()
3970 + gap_x * col_end.saturating_sub(cell.col + 1) as f32;
3971 let cell_h = row_heights[cell.row..row_end].iter().sum::<f32>()
3972 + gap_y * row_end.saturating_sub(cell.row + 1) as f32;
3973 let child_constraints = BoxConstraints {
3974 min_w: cell_w,
3975 max_w: cell_w,
3976 min_h: cell_h,
3977 max_h: cell_h,
3978 };
3979 self.layout_node_constraints(
3980 cell.id,
3981 child_constraints,
3982 LayoutPoint::new(cell_x, cell_y),
3983 out,
3984 constraints_out,
3985 measure_cache,
3986 scroll_source,
3987 record,
3988 depth + 1,
3989 )?;
3990 }
3991 }
3992
3993 if record && !abs_children.is_empty() {
3994 let abs_constraints = BoxConstraints::loose(size.width, size.height);
3995 for child_id in abs_children {
3996 self.layout_node_constraints(
3997 child_id,
3998 abs_constraints,
3999 origin,
4000 out,
4001 constraints_out,
4002 measure_cache,
4003 scroll_source,
4004 record,
4005 depth + 1,
4006 )?;
4007 }
4008 }
4009 content_size = size;
4010 size
4011 }
4012 LayoutOp::GridItem { .. } => {
4013 let mut child_size = LayoutSize::ZERO;
4014 if let Some(child_id) = node.children_ids.first() {
4015 child_size = self.layout_node_constraints(
4016 *child_id,
4017 constraints,
4018 origin,
4019 out,
4020 constraints_out,
4021 measure_cache,
4022 scroll_source,
4023 record,
4024 depth + 1,
4025 )?;
4026 }
4027 content_size = child_size;
4028 constraints.constrain(child_size)
4029 }
4030 LayoutOp::Responsive { query, cases } => {
4031 let query_width = match query {
4032 fission_ir::op::ResponsiveQuery::Viewport => self.active_viewport.width,
4033 fission_ir::op::ResponsiveQuery::Container => {
4034 if constraints.is_width_bounded() {
4035 constraints.max_w
4036 } else {
4037 self.active_viewport.width
4038 }
4039 }
4040 };
4041 let selected_index = cases
4042 .iter()
4043 .enumerate()
4044 .rev()
4045 .find_map(|(index, condition)| condition.matches(query_width).then_some(index))
4046 .unwrap_or(cases.len());
4047 let child_size = node
4048 .children_ids
4049 .get(selected_index)
4050 .map(|child_id| {
4051 self.layout_node_constraints(
4052 *child_id,
4053 constraints,
4054 origin,
4055 out,
4056 constraints_out,
4057 measure_cache,
4058 scroll_source,
4059 record,
4060 depth + 1,
4061 )
4062 })
4063 .transpose()?
4064 .unwrap_or(LayoutSize::ZERO);
4065 content_size = child_size;
4066 constraints.constrain(child_size)
4067 }
4068 LayoutOp::Scroll {
4069 direction,
4070 width,
4071 height,
4072 min_width,
4073 max_width,
4074 min_height,
4075 max_height,
4076 padding,
4077 ..
4078 } => {
4079 let mut local =
4080 constraints.apply_min_max(*min_width, *max_width, *min_height, *max_height);
4081 local = local.tighten(*width, *height);
4082 let is_horizontal = matches!(direction, FlexDirection::Row);
4083 let mut child_constraints = local.deflate(*padding);
4084 if is_horizontal {
4085 child_constraints.min_w = 0.0;
4086 child_constraints.max_w = f32::INFINITY;
4087 } else {
4088 child_constraints.min_h = 0.0;
4089 child_constraints.max_h = f32::INFINITY;
4090 }
4091 let mut child_size = LayoutSize::ZERO;
4092 if let Some(child_id) = flow_children.first() {
4093 child_size = self.layout_node_constraints(
4094 *child_id,
4095 child_constraints,
4096 LayoutPoint::ZERO,
4097 out,
4098 constraints_out,
4099 measure_cache,
4100 scroll_source,
4101 false,
4102 depth + 1,
4103 )?;
4104 }
4105 let size = local.constrain(LayoutSize::new(
4106 child_size.width + padding[0] + padding[1],
4107 child_size.height + padding[2] + padding[3],
4108 ));
4109 if record {
4110 if let Some(child_id) = flow_children.first() {
4111 self.layout_node_constraints(
4112 *child_id,
4113 child_constraints,
4114 LayoutPoint::new(origin.x + padding[0], origin.y + padding[2]),
4115 out,
4116 constraints_out,
4117 measure_cache,
4118 scroll_source,
4119 record,
4120 depth + 1,
4121 )?;
4122 }
4123 if !abs_children.is_empty() {
4124 let abs_constraints = BoxConstraints::loose(size.width, size.height);
4125 for child_id in abs_children {
4126 self.layout_node_constraints(
4127 child_id,
4128 abs_constraints,
4129 origin,
4130 out,
4131 constraints_out,
4132 measure_cache,
4133 scroll_source,
4134 record,
4135 depth + 1,
4136 )?;
4137 }
4138 }
4139 }
4140 content_size = child_size;
4141 size
4142 }
4143 LayoutOp::Align => {
4144 let child_constraints = BoxConstraints::loose(constraints.max_w, constraints.max_h);
4145 let mut child_size = LayoutSize::ZERO;
4146 if let Some(child_id) = flow_children.first() {
4147 child_size = self.layout_node_constraints(
4148 *child_id,
4149 child_constraints,
4150 LayoutPoint::ZERO,
4151 out,
4152 constraints_out,
4153 measure_cache,
4154 scroll_source,
4155 false,
4156 depth + 1,
4157 )?;
4158 }
4159 let size = if constraints.is_width_bounded() || constraints.is_height_bounded() {
4160 constraints.constrain(LayoutSize::new(
4161 if constraints.is_width_bounded() {
4162 constraints.max_w
4163 } else {
4164 child_size.width
4165 },
4166 if constraints.is_height_bounded() {
4167 constraints.max_h
4168 } else {
4169 child_size.height
4170 },
4171 ))
4172 } else {
4173 child_size
4174 };
4175 if let Some(child_id) = flow_children.first() {
4176 let dx = ((size.width - child_size.width) / 2.0).max(0.0);
4177 let dy = ((size.height - child_size.height) / 2.0).max(0.0);
4178 self.layout_node_constraints(
4179 *child_id,
4180 child_constraints,
4181 LayoutPoint::new(origin.x + dx, origin.y + dy),
4182 out,
4183 constraints_out,
4184 measure_cache,
4185 scroll_source,
4186 record,
4187 depth + 1,
4188 )?;
4189 }
4190 if record && !abs_children.is_empty() {
4191 let abs_constraints = BoxConstraints::loose(size.width, size.height);
4192 for child_id in abs_children {
4193 self.layout_node_constraints(
4194 child_id,
4195 abs_constraints,
4196 origin,
4197 out,
4198 constraints_out,
4199 measure_cache,
4200 scroll_source,
4201 record,
4202 depth + 1,
4203 )?;
4204 }
4205 }
4206 content_size = child_size;
4207 size
4208 }
4209 LayoutOp::ZStack => {
4210 let mut max_child = LayoutSize::ZERO;
4211 for child_id in &flow_children {
4212 let child_size = self.layout_node_constraints(
4213 *child_id,
4214 BoxConstraints::loose(constraints.max_w, constraints.max_h),
4215 LayoutPoint::ZERO,
4216 out,
4217 constraints_out,
4218 measure_cache,
4219 scroll_source,
4220 false,
4221 depth + 1,
4222 )?;
4223 max_child.width = max_child.width.max(child_size.width);
4224 max_child.height = max_child.height.max(child_size.height);
4225 }
4226 let size = if constraints.is_width_bounded() || constraints.is_height_bounded() {
4227 constraints.constrain(LayoutSize::new(
4228 if constraints.is_width_bounded() {
4229 constraints.max_w
4230 } else {
4231 max_child.width
4232 },
4233 if constraints.is_height_bounded() {
4234 constraints.max_h
4235 } else {
4236 max_child.height
4237 },
4238 ))
4239 } else {
4240 max_child
4241 };
4242 for child_id in &flow_children {
4243 let child_constraints = BoxConstraints::loose(size.width, size.height);
4244 let child_origin = LayoutPoint::new(origin.x, origin.y);
4245 self.layout_node_constraints(
4246 *child_id,
4247 child_constraints,
4248 child_origin,
4249 out,
4250 constraints_out,
4251 measure_cache,
4252 scroll_source,
4253 record,
4254 depth + 1,
4255 )?;
4256 }
4257 if record && !abs_children.is_empty() {
4258 let abs_constraints = BoxConstraints::loose(size.width, size.height);
4259 for child_id in abs_children {
4260 self.layout_node_constraints(
4261 child_id,
4262 abs_constraints,
4263 origin,
4264 out,
4265 constraints_out,
4266 measure_cache,
4267 scroll_source,
4268 record,
4269 depth + 1,
4270 )?;
4271 }
4272 }
4273 content_size = size;
4274 size
4275 }
4276 LayoutOp::Positioned {
4277 top,
4278 left,
4279 bottom,
4280 right,
4281 width,
4282 height,
4283 } => {
4284 let target_w = finite_or(constraints.max_w, finite_or(constraints.min_w, 0.0));
4285 let target_h = finite_or(constraints.max_h, finite_or(constraints.min_h, 0.0));
4286 let size = constraints.constrain(LayoutSize::new(target_w, target_h));
4287 let mut child_constraints = BoxConstraints::loose(size.width, size.height);
4288 if let (Some(l), Some(r)) = (left, right) {
4289 let w = (size.width - l - r).max(0.0);
4290 child_constraints = child_constraints.tighten(Some(w), None);
4291 }
4292 if let (Some(t), Some(b)) = (top, bottom) {
4293 let h = (size.height - t - b).max(0.0);
4294 child_constraints = child_constraints.tighten(None, Some(h));
4295 }
4296 child_constraints = child_constraints.tighten(*width, *height);
4297 if let Some(child_id) = node.children_ids.first() {
4298 let child_size = self.layout_node_constraints(
4299 *child_id,
4300 child_constraints,
4301 LayoutPoint::ZERO,
4302 out,
4303 constraints_out,
4304 measure_cache,
4305 scroll_source,
4306 false,
4307 depth + 1,
4308 )?;
4309 let x = left.unwrap_or_else(|| {
4310 right
4311 .map(|r| (size.width - r - child_size.width).max(0.0))
4312 .unwrap_or(0.0)
4313 });
4314 let y = top.unwrap_or_else(|| {
4315 bottom
4316 .map(|b| (size.height - b - child_size.height).max(0.0))
4317 .unwrap_or(0.0)
4318 });
4319 self.layout_node_constraints(
4320 *child_id,
4321 child_constraints,
4322 LayoutPoint::new(origin.x + x, origin.y + y),
4323 out,
4324 constraints_out,
4325 measure_cache,
4326 scroll_source,
4327 record,
4328 depth + 1,
4329 )?;
4330 }
4331 content_size = size;
4332 size
4333 }
4334 LayoutOp::PositionedLengths {
4335 top,
4336 left,
4337 bottom,
4338 right,
4339 width,
4340 height,
4341 } => {
4342 let target_w = finite_or(constraints.max_w, finite_or(constraints.min_w, 0.0));
4343 let target_h = finite_or(constraints.max_h, finite_or(constraints.min_h, 0.0));
4344 let size = constraints.constrain(LayoutSize::new(target_w, target_h));
4345 let resolve_horizontal = |length: &Option<Length>| {
4346 length
4347 .as_ref()
4348 .and_then(|length| resolve_length(length, size.width, self.active_viewport))
4349 };
4350 let resolve_vertical = |length: &Option<Length>| {
4351 length.as_ref().and_then(|length| {
4352 resolve_length(length, size.height, self.active_viewport)
4353 })
4354 };
4355 let left = resolve_horizontal(left);
4356 let top = resolve_vertical(top);
4357 let right = resolve_horizontal(right);
4358 let bottom = resolve_vertical(bottom);
4359 let width = resolve_horizontal(width);
4360 let height = resolve_vertical(height);
4361 let mut child_constraints = BoxConstraints::loose(size.width, size.height);
4362 if let (Some(left), Some(right)) = (left, right) {
4363 child_constraints =
4364 child_constraints.tighten(Some((size.width - left - right).max(0.0)), None);
4365 }
4366 if let (Some(top), Some(bottom)) = (top, bottom) {
4367 child_constraints = child_constraints
4368 .tighten(None, Some((size.height - top - bottom).max(0.0)));
4369 }
4370 child_constraints = child_constraints.tighten(width, height);
4371 if let Some(child_id) = node.children_ids.first() {
4372 let child_size = self.layout_node_constraints(
4373 *child_id,
4374 child_constraints,
4375 LayoutPoint::ZERO,
4376 out,
4377 constraints_out,
4378 measure_cache,
4379 scroll_source,
4380 false,
4381 depth + 1,
4382 )?;
4383 let x = left.unwrap_or_else(|| {
4384 right
4385 .map(|right| (size.width - right - child_size.width).max(0.0))
4386 .unwrap_or(0.0)
4387 });
4388 let y = top.unwrap_or_else(|| {
4389 bottom
4390 .map(|bottom| (size.height - bottom - child_size.height).max(0.0))
4391 .unwrap_or(0.0)
4392 });
4393 self.layout_node_constraints(
4394 *child_id,
4395 child_constraints,
4396 LayoutPoint::new(origin.x + x, origin.y + y),
4397 out,
4398 constraints_out,
4399 measure_cache,
4400 scroll_source,
4401 record,
4402 depth + 1,
4403 )?;
4404 }
4405 content_size = size;
4406 size
4407 }
4408 LayoutOp::Embed { width, height, .. } => {
4409 let local = constraints.tighten(*width, *height);
4410 let w = if local.is_width_bounded() {
4411 local.max_w
4412 } else {
4413 local.min_w
4414 };
4415 let h = if local.is_height_bounded() {
4416 local.max_h
4417 } else {
4418 local.min_h
4419 };
4420 let size = local.constrain(LayoutSize::new(w, h));
4421 content_size = size;
4422 size
4423 }
4424 LayoutOp::AbsoluteFill => {
4425 let target_w = finite_or(constraints.max_w, finite_or(constraints.min_w, 0.0));
4426 let target_h = finite_or(constraints.max_h, finite_or(constraints.min_h, 0.0));
4427 let size = constraints.constrain(LayoutSize::new(target_w, target_h));
4428 for child_id in self.graph_state.children_of(node_id) {
4429 self.layout_node_constraints(
4430 *child_id,
4431 BoxConstraints::tight(size),
4432 origin,
4433 out,
4434 constraints_out,
4435 measure_cache,
4436 scroll_source,
4437 record,
4438 depth + 1,
4439 )?;
4440 }
4441 content_size = size;
4442 size
4443 }
4444 LayoutOp::Transform { .. } | LayoutOp::Clip { .. } => {
4445 let mut child_size = LayoutSize::ZERO;
4446 if let Some(child_id) = node.children_ids.first() {
4447 child_size = self.layout_node_constraints(
4448 *child_id,
4449 constraints,
4450 origin,
4451 out,
4452 constraints_out,
4453 measure_cache,
4454 scroll_source,
4455 record,
4456 depth + 1,
4457 )?;
4458 }
4459 content_size = child_size;
4460 constraints.constrain(child_size)
4461 }
4462 LayoutOp::Flyout { anchor, content: _ } => {
4463 let loose = BoxConstraints::loose(
4464 if constraints.is_width_bounded() {
4465 constraints.max_w
4466 } else {
4467 f32::INFINITY
4468 },
4469 if constraints.is_height_bounded() {
4470 constraints.max_h
4471 } else {
4472 f32::INFINITY
4473 },
4474 );
4475 let mut child_size = LayoutSize::ZERO;
4476 for child_id in self.graph_state.children_of(node_id) {
4477 child_size = self.layout_node_constraints(
4478 *child_id,
4479 loose,
4480 origin,
4481 out,
4482 constraints_out,
4483 measure_cache,
4484 scroll_source,
4485 false,
4486 depth + 1,
4487 )?;
4488 }
4489 if record {
4490 let anchor_rect = out.get(anchor).map(|g| g.rect);
4491 let place_x = anchor_rect.map(|r| r.x()).unwrap_or(origin.x);
4492 let place_y = anchor_rect.map(|r| r.y() + r.height()).unwrap_or(origin.y);
4493 for child_id in self.graph_state.children_of(node_id) {
4494 self.layout_node_constraints(
4495 *child_id,
4496 loose,
4497 LayoutPoint::new(place_x, place_y),
4498 out,
4499 constraints_out,
4500 measure_cache,
4501 scroll_source,
4502 record,
4503 depth + 1,
4504 )?;
4505 }
4506 }
4507 content_size = child_size;
4508 child_size
4509 }
4510 LayoutOp::StyledBox { .. } => unreachable!("styled boxes are resolved before layout"),
4511 };
4512
4513 if let Some(runs) = &node.rich_text {
4514 if let Some(measurer) = &self.measurer {
4515 let (mut text_constraints, text_padding) = match layout_op {
4516 LayoutOp::Box {
4517 width,
4518 height,
4519 min_width,
4520 max_width,
4521 min_height,
4522 max_height,
4523 padding,
4524 ..
4525 } => (
4526 constraints
4527 .apply_min_max(*min_width, *max_width, *min_height, *max_height)
4528 .tighten(*width, *height),
4529 *padding,
4530 ),
4531 _ => (constraints, [0.0; 4]),
4532 };
4533 let text_inner_constraints = text_constraints.deflate(text_padding);
4534 let intrinsic_width = match &node.op {
4535 LayoutOp::StyledBox { style, .. } => style.width.as_ref(),
4536 _ => None,
4537 };
4538 let avail_w = match intrinsic_width {
4539 Some(Length::MaxContent) => None,
4540 Some(Length::MinContent) => Some(
4541 runs.iter()
4542 .flat_map(|run| {
4543 run.text.split_whitespace().map(move |word| {
4544 measurer.measure(word, run.style.font_size, None).0
4545 + run.style.letter_spacing
4546 * word.chars().count().saturating_sub(1) as f32
4547 })
4548 })
4549 .fold(0.0, f32::max),
4550 ),
4551 _ => text_inner_constraints
4552 .is_width_bounded()
4553 .then_some(text_inner_constraints.max_w),
4554 };
4555 let rich_layout = measurer.layout_rich_text(runs, avail_w);
4556 let text_content = LayoutSize::new(
4557 rich_layout.width + text_padding[0] + text_padding[1],
4558 rich_layout.height + text_padding[2] + text_padding[3],
4559 );
4560 if let LayoutOp::StyledBox { style, .. } = &node.op {
4561 let available = if constraints.max_h.is_finite() {
4562 constraints.max_h
4563 } else {
4564 text_content.height
4565 };
4566 let resolve_intrinsic_height = |length: &Option<Length>| {
4567 length
4568 .as_ref()
4569 .filter(|length| length_requires_measurement(length))
4570 .and_then(|length| {
4571 resolve_measured_length(
4572 length,
4573 available,
4574 self.active_viewport,
4575 text_content.height,
4576 text_content.height,
4577 )
4578 })
4579 };
4580 text_constraints = text_constraints.apply_min_max(
4581 None,
4582 None,
4583 resolve_intrinsic_height(&style.min_height),
4584 resolve_intrinsic_height(&style.max_height),
4585 );
4586 text_constraints =
4587 text_constraints.tighten(None, resolve_intrinsic_height(&style.height));
4588 }
4589 let measured = text_constraints.constrain(text_content);
4590 if rich_text_inline_children
4591 && rich_layout.inline_boxes.len() == flow_children.len()
4592 {
4593 let result =
4594 self.record_geometry(node_id, origin, measured, text_content, out, record);
4595 if record {
4596 let mut inline_boxes = rich_layout.inline_boxes;
4597 inline_boxes.sort_by_key(|inline_box| inline_box.id);
4598 for (child_id, inline_box) in flow_children.iter().zip(inline_boxes.iter())
4599 {
4600 self.layout_node_constraints(
4601 *child_id,
4602 BoxConstraints::tight(LayoutSize::new(
4603 inline_box.width,
4604 inline_box.height,
4605 )),
4606 LayoutPoint::new(
4607 origin.x + text_padding[0] + inline_box.x,
4608 origin.y + text_padding[2] + inline_box.y,
4609 ),
4610 out,
4611 constraints_out,
4612 measure_cache,
4613 scroll_source,
4614 record,
4615 depth + 1,
4616 )?;
4617 }
4618 }
4619 if !record {
4620 measure_cache.insert(MeasureCacheKey::new(node_id, constraints), result);
4621 }
4622 return Ok(result);
4623 }
4624 if node.children_ids.is_empty() {
4625 let result =
4626 self.record_geometry(node_id, origin, measured, text_content, out, record);
4627 if !record {
4628 measure_cache.insert(MeasureCacheKey::new(node_id, constraints), result);
4629 }
4630 return Ok(result);
4631 }
4632 content_size.width = content_size.width.max(text_content.width);
4633 content_size.height = content_size.height.max(text_content.height);
4634 }
4635 }
4636
4637 let result = self.record_geometry(node_id, origin, size, content_size, out, record);
4638 if !record {
4639 measure_cache.insert(MeasureCacheKey::new(node_id, constraints), result);
4640 }
4641 Ok(result)
4642 }
4643
4644 fn record_geometry(
4645 &self,
4646 node_id: WidgetId,
4647 origin: LayoutPoint,
4648 size: LayoutSize,
4649 content_size: LayoutSize,
4650 out: &mut HashMap<WidgetId, LayoutNodeGeometry>,
4651 record: bool,
4652 ) -> LayoutSize {
4653 let mut rect_origin = origin;
4654 let mut rect_size = size;
4655 let mut rect_content = content_size;
4656 let mut had_non_finite = false;
4657
4658 if !rect_origin.x.is_finite() {
4659 rect_origin.x = 0.0;
4660 had_non_finite = true;
4661 }
4662 if !rect_origin.y.is_finite() {
4663 rect_origin.y = 0.0;
4664 had_non_finite = true;
4665 }
4666 if !rect_size.width.is_finite() {
4667 rect_size.width = 0.0;
4668 had_non_finite = true;
4669 }
4670 if !rect_size.height.is_finite() {
4671 rect_size.height = 0.0;
4672 had_non_finite = true;
4673 }
4674 if !rect_content.width.is_finite() {
4675 rect_content.width = 0.0;
4676 had_non_finite = true;
4677 }
4678 if !rect_content.height.is_finite() {
4679 rect_content.height = 0.0;
4680 had_non_finite = true;
4681 }
4682
4683 if had_non_finite {
4684 diag::emit(
4685 diag::DiagCategory::Invariants,
4686 diag::DiagLevel::Error,
4687 diag::DiagEventKind::InvariantViolation {
4688 kind: "non_finite_layout".into(),
4689 node: Some(node_id.as_u128()),
4690 details: format!(
4691 "origin=({:.2},{:.2}) size=({:.2},{:.2}) content=({:.2},{:.2})",
4692 origin.x,
4693 origin.y,
4694 size.width,
4695 size.height,
4696 content_size.width,
4697 content_size.height
4698 ),
4699 dump_ref: None,
4700 },
4701 );
4702 }
4703
4704 if record {
4705 let rect = LayoutRect::new(
4706 rect_origin.x,
4707 rect_origin.y,
4708 rect_size.width,
4709 rect_size.height,
4710 );
4711 out.insert(
4712 node_id,
4713 LayoutNodeGeometry {
4714 rect,
4715 content_size: rect_content,
4716 },
4717 );
4718 }
4719 rect_size
4720 }
4721}