Skip to main content

cranpose_ui/layout/
policies.rs

1use cranpose_ui_layout::{
2    Axis, Constraints, MeasurePolicy, MeasureResult, MeasureScope, ParentData, Placement,
3    bias_offset, round_to_px,
4};
5use smallvec::SmallVec;
6
7use crate::layout::core::{
8    Alignment, Arrangement, HorizontalAlignment, LinearArrangement, Measurable, VerticalAlignment,
9};
10
11/// MeasurePolicy for Box layout - overlays children according to alignment.
12#[derive(Clone, Debug, PartialEq)]
13pub struct BoxMeasurePolicy {
14    /// Where children sit inside the box when they are smaller than it.
15    pub content_alignment: Alignment,
16    /// Whether the box's minimum constraints pass on to its children. When `false`, children may
17    /// measure smaller than the box's minimum size.
18    pub propagate_min_constraints: bool,
19    /// Direction used to resolve start and end alignment.
20    pub layout_direction: crate::LayoutDirection,
21}
22
23impl BoxMeasurePolicy {
24    /// Creates a policy that places children by `content_alignment` in a left-to-right layout
25    /// direction.
26    pub fn new(content_alignment: Alignment, propagate_min_constraints: bool) -> Self {
27        Self {
28            content_alignment,
29            propagate_min_constraints,
30            layout_direction: crate::LayoutDirection::Ltr,
31        }
32    }
33}
34
35impl MeasurePolicy for BoxMeasurePolicy {
36    fn measure(
37        &self,
38        scope: &dyn MeasureScope,
39        measurables: &[Box<dyn Measurable>],
40        constraints: Constraints,
41    ) -> MeasureResult {
42        let mut placements = Vec::new();
43        let measurement = self.measure_into(scope, measurables, constraints, &mut placements);
44        MeasureResult::new(measurement, placements)
45    }
46
47    /// A box holds while every child holds under the constraints it hands
48    /// on and the largest child, which sizes the box, still fits.
49    fn measure_hold(
50        &self,
51        measurables: &[Box<dyn Measurable>],
52        constraints: Constraints,
53        size: crate::modifier::Size,
54    ) -> Option<cranpose_ui_layout::ConstraintsHold> {
55        if measurables.is_empty() {
56            return empty_layout_hold(constraints, size);
57        }
58        let mut width = cranpose_ui_layout::BoundRange::ANY;
59        let mut height = cranpose_ui_layout::BoundRange::ANY;
60        let mut held = cranpose_ui_layout::ConstraintsHold::sized(size.width, size.height);
61        let mut largest = crate::modifier::Size::default();
62        for measurable in measurables {
63            let (child, hold) = measurable.measured_hold()?;
64            if self.propagate_min_constraints {
65                held = held.intersect(hold)?;
66            } else {
67                width = width.intersect(hold.width.max)?;
68                height = height.intersect(hold.height.max)?;
69            }
70            largest.width = largest.width.max(child.width);
71            largest.height = largest.height.max(child.height);
72        }
73        // Bounds that cut or stretched the box size it from themselves.
74        if size != largest {
75            return None;
76        }
77        held.width.max = held.width.max.intersect(width)?;
78        held.height.max = held.height.max.intersect(height)?;
79        Some(held)
80    }
81
82    fn measure_into(
83        &self,
84        scope: &dyn MeasureScope,
85        measurables: &[Box<dyn Measurable>],
86        constraints: Constraints,
87        placements: &mut Vec<Placement>,
88    ) -> cranpose_ui_layout::Measurement {
89        placements.clear();
90        let child_constraints = if self.propagate_min_constraints {
91            constraints
92        } else {
93            Constraints {
94                min_width: 0.0,
95                max_width: constraints.max_width,
96                min_height: 0.0,
97                max_height: constraints.max_height,
98            }
99        };
100
101        let mut max_width = 0.0_f32;
102        let mut max_height = 0.0_f32;
103        let mut placeables: SmallVec<[(cranpose_ui_layout::Placeable, Alignment); 8]> =
104            SmallVec::new();
105
106        for measurable in measurables {
107            let placeable = measurable.measure(child_constraints);
108            max_width = max_width.max(placeable.width());
109            max_height = max_height.max(placeable.height());
110            let alignment = measurable
111                .parent_data()
112                .box_alignment
113                .unwrap_or(self.content_alignment);
114            placeables.push((placeable, alignment));
115        }
116
117        let width = max_width.clamp(constraints.min_width, constraints.max_width);
118        let height = max_height.clamp(constraints.min_height, constraints.max_height);
119
120        placements.reserve(placeables.len());
121        for (placeable, alignment) in placeables {
122            let child_width = placeable.width();
123            let child_height = placeable.height();
124
125            let x = alignment
126                .horizontal
127                .align(width, child_width, scope.density());
128            let x = self.layout_direction.place_x(x, width, child_width);
129            let y = alignment
130                .vertical
131                .align(height, child_height, scope.density());
132
133            placeable.place(x, y);
134            placements.push(Placement::new(placeable.node_id(), x, y, 0));
135        }
136
137        crate::modifier::Size { width, height }.into()
138    }
139
140    fn min_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32 {
141        measurables
142            .iter()
143            .map(|m| m.min_intrinsic_width(height))
144            .fold(0.0, f32::max)
145    }
146
147    fn max_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32 {
148        measurables
149            .iter()
150            .map(|m| m.max_intrinsic_width(height))
151            .fold(0.0, f32::max)
152    }
153
154    fn min_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32 {
155        measurables
156            .iter()
157            .map(|m| m.min_intrinsic_height(width))
158            .fold(0.0, f32::max)
159    }
160
161    fn max_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32 {
162        measurables
163            .iter()
164            .map(|m| m.max_intrinsic_height(width))
165            .fold(0.0, f32::max)
166    }
167}
168
169/// Unified Flex layout policy that powers both Row and Column.
170///
171/// This policy implements Jetpack Compose's flex layout semantics:
172/// - Measures children with proper loose constraints (min = 0 on both axes)
173/// - Supports weighted distribution of remaining space
174/// - Handles bounded/unbounded main axis correctly
175/// - Implements correct intrinsics for both axes
176///
177/// ## Overflow Behavior
178///
179/// Like Jetpack Compose, this policy **allows children to overflow** their container bounds:
180/// - Children can be positioned outside the parent's measured size
181/// - Overflowing content is rendered (unless clipped by a modifier)
182/// - When content overflows, distribution arrangements switch to `Start` to avoid negative spacing
183/// - `SpacedBy` keeps its fixed inter-child spacing even when content overflows
184///
185/// Example: A Row with 300px of content in a 200px container will:
186/// 1. Measure children at their natural sizes
187/// 2. Detect overflow (300px > 200px)
188/// 3. Switch to Start arrangement (pack children at the start)
189/// 4. Position last children beyond the 200px boundary
190///
191/// To prevent overflow:
192/// - Use weights for flexible sizing: `.weight(1.0, true)`
193/// - Use `fillMaxWidth()`/`fillMaxHeight()` modifiers
194/// - Design UI to fit within available space
195/// - Add a clip modifier to hide overflowing content
196///
197/// ## Weighted Children
198///
199/// When the main axis is bounded and children have weights:
200/// 1. Fixed children (no weight) are measured first
201/// 2. Remaining space is distributed proportionally to weights
202/// 3. Each weighted child gets: `remaining * (weight / total_weight)`
203/// 4. If `fill=true`, child gets tight constraints; if `fill=false`, loose constraints
204///
205/// When the main axis is unbounded, weights are ignored (all children wrap content).
206#[derive(Clone, Debug, PartialEq)]
207pub struct FlexMeasurePolicy {
208    /// Main axis direction (Horizontal for Row, Vertical for Column)
209    pub axis: Axis,
210    /// Arrangement along the main axis
211    pub main_axis_arrangement: LinearArrangement,
212    /// Alignment along the cross axis (used as default for children without explicit alignment)
213    pub cross_axis_alignment: CrossAxisAlignment,
214    /// The device pixel grid children are spaced and placed on, the
215    /// composition's density.
216    pub density: f32,
217    /// Direction used to resolve horizontal order and start/end alignment.
218    pub layout_direction: crate::LayoutDirection,
219}
220
221/// Compose's weight distribution in whole device pixels: each weighted
222/// child's share of the remaining space rounded half up, and the pixels
223/// that rounding gained or lost handed back one per child from the first.
224pub(crate) struct WeightShares {
225    density: f32,
226    unit_px: f32,
227    remainder_px: f32,
228}
229
230impl WeightShares {
231    pub(crate) fn new(
232        remaining: f32,
233        weights: impl Iterator<Item = f32> + Clone,
234        density: f32,
235    ) -> Self {
236        let density = if density > 0.0 && density.is_finite() {
237            density
238        } else {
239            1.0
240        };
241        let total_weight: f32 = weights.clone().sum();
242        let remaining_px = (remaining * density).round();
243        let unit_px = if total_weight > 0.0 {
244            remaining_px / total_weight
245        } else {
246            0.0
247        };
248        let rounded_px: f32 = weights.map(|weight| (unit_px * weight + 0.5).floor()).sum();
249        Self {
250            density,
251            unit_px,
252            remainder_px: remaining_px - rounded_px,
253        }
254    }
255
256    /// The main-axis size of the next weighted child, which weighs `weight`.
257    pub(crate) fn next_share(&mut self, weight: f32) -> f32 {
258        let step = if self.remainder_px > 0.0 {
259            1.0
260        } else if self.remainder_px < 0.0 {
261            -1.0
262        } else {
263            0.0
264        };
265        self.remainder_px -= step;
266        ((self.unit_px * weight + 0.5).floor() + step).max(0.0) / self.density
267    }
268}
269
270/// Cross-axis alignment for flex layouts.
271/// This is axis-agnostic and gets interpreted based on the flex axis.
272#[derive(Clone, Copy, Debug, PartialEq)]
273pub enum CrossAxisAlignment {
274    /// Align to the start of the cross axis (Top for Row, Start for Column)
275    Start,
276    /// Align to the center of the cross axis
277    Center,
278    /// Align to the end of the cross axis (Bottom for Row, End for Column)
279    End,
280}
281
282impl CrossAxisAlignment {
283    /// Where a child sits in `available` space: see [`bias_offset`].
284    fn align(&self, available: f32, child: f32, density: f32) -> f32 {
285        let bias = match self {
286            CrossAxisAlignment::Start => -1.0,
287            CrossAxisAlignment::Center => 0.0,
288            CrossAxisAlignment::End => 1.0,
289        };
290        bias_offset(bias, available, child, density)
291    }
292}
293
294impl From<HorizontalAlignment> for CrossAxisAlignment {
295    fn from(alignment: HorizontalAlignment) -> Self {
296        match alignment {
297            HorizontalAlignment::Start => CrossAxisAlignment::Start,
298            HorizontalAlignment::CenterHorizontally => CrossAxisAlignment::Center,
299            HorizontalAlignment::End => CrossAxisAlignment::End,
300        }
301    }
302}
303
304impl From<VerticalAlignment> for CrossAxisAlignment {
305    fn from(alignment: VerticalAlignment) -> Self {
306        match alignment {
307            VerticalAlignment::Top => CrossAxisAlignment::Start,
308            VerticalAlignment::CenterVertically => CrossAxisAlignment::Center,
309            VerticalAlignment::Bottom => CrossAxisAlignment::End,
310        }
311    }
312}
313
314impl FlexMeasurePolicy {
315    /// A flex layout along `axis` on the device pixel grid of `density`.
316    pub fn new(
317        axis: Axis,
318        main_axis_arrangement: LinearArrangement,
319        cross_axis_alignment: CrossAxisAlignment,
320        density: f32,
321    ) -> Self {
322        Self {
323            axis,
324            main_axis_arrangement,
325            cross_axis_alignment,
326            density,
327            layout_direction: crate::LayoutDirection::Ltr,
328        }
329    }
330
331    /// Creates a FlexMeasurePolicy for Row (horizontal main axis).
332    pub fn row(
333        horizontal_arrangement: LinearArrangement,
334        vertical_alignment: VerticalAlignment,
335        density: f32,
336    ) -> Self {
337        Self::new(
338            Axis::Horizontal,
339            horizontal_arrangement,
340            vertical_alignment.into(),
341            density,
342        )
343    }
344
345    /// Creates a FlexMeasurePolicy for Column (vertical main axis).
346    pub fn column(
347        vertical_arrangement: LinearArrangement,
348        horizontal_alignment: HorizontalAlignment,
349        density: f32,
350    ) -> Self {
351        Self::new(
352            Axis::Vertical,
353            vertical_arrangement,
354            horizontal_alignment.into(),
355            density,
356        )
357    }
358
359    fn get_axis_constraints(&self, constraints: Constraints) -> (f32, f32, f32, f32) {
360        match self.axis {
361            Axis::Horizontal => (
362                constraints.min_width,
363                constraints.max_width,
364                constraints.min_height,
365                constraints.max_height,
366            ),
367            Axis::Vertical => (
368                constraints.min_height,
369                constraints.max_height,
370                constraints.min_width,
371                constraints.max_width,
372            ),
373        }
374    }
375
376    fn make_constraints(
377        &self,
378        min_main: f32,
379        max_main: f32,
380        min_cross: f32,
381        max_cross: f32,
382    ) -> Constraints {
383        match self.axis {
384            Axis::Horizontal => Constraints {
385                min_width: min_main,
386                max_width: max_main,
387                min_height: min_cross,
388                max_height: max_cross,
389            },
390            Axis::Vertical => Constraints {
391                min_width: min_cross,
392                max_width: max_cross,
393                min_height: min_main,
394                max_height: max_main,
395            },
396        }
397    }
398
399    fn get_main_axis_size(&self, width: f32, height: f32) -> f32 {
400        match self.axis {
401            Axis::Horizontal => width,
402            Axis::Vertical => height,
403        }
404    }
405
406    fn get_cross_axis_size(&self, width: f32, height: f32) -> f32 {
407        match self.axis {
408            Axis::Horizontal => height,
409            Axis::Vertical => width,
410        }
411    }
412
413    /// Measures the unweighted children in order and returns Compose's
414    /// `fixedSpace` with the largest cross size. Each child is offered only
415    /// the main-axis space the ones before it leave, as
416    /// `RowColumnMeasurementHelper` offers it, and the spacing after a child
417    /// never runs past the main axis.
418    fn measure_fixed_children(
419        &self,
420        measurables: &[Box<dyn Measurable>],
421        fixed_children: &[usize],
422        (max_main, max_cross): (f32, f32),
423        spacing: f32,
424        placeables: &mut [Option<cranpose_ui_layout::Placeable>],
425    ) -> (f32, f32) {
426        let main_axis_bounded = max_main.is_finite();
427        let mut fixed_space = 0.0_f32;
428        let mut max_cross_size = 0.0_f32;
429        for &idx in fixed_children {
430            let available_main = if main_axis_bounded {
431                (max_main - fixed_space).max(0.0)
432            } else {
433                max_main
434            };
435            let placeable = measurables[idx].measure(self.make_constraints(
436                0.0,
437                available_main,
438                0.0,
439                max_cross,
440            ));
441            let main_size = self.get_main_axis_size(placeable.width(), placeable.height());
442            let spacing_after = if main_axis_bounded {
443                spacing.min(available_main - main_size).max(0.0)
444            } else {
445                spacing
446            };
447            fixed_space += main_size + spacing_after;
448            max_cross_size =
449                max_cross_size.max(self.get_cross_axis_size(placeable.width(), placeable.height()));
450            placeables[idx] = Some(placeable);
451        }
452        (fixed_space, max_cross_size)
453    }
454
455    /// Compose's intrinsic size along the main axis: the sizes `main_size`
456    /// gives of the unweighted children, the space in which each weighted
457    /// child gets its own size, and the spacing.
458    fn intrinsic_main_size(
459        &self,
460        measurables: &[Box<dyn Measurable>],
461        main_size: impl Fn(&dyn Measurable) -> f32,
462    ) -> f32 {
463        if measurables.is_empty() {
464            return 0.0;
465        }
466        let mut fixed_space = 0.0_f32;
467        let mut weight_unit = 0.0_f32;
468        let mut total_weight = 0.0_f32;
469        for measurable in measurables {
470            let size = main_size(measurable.as_ref());
471            match child_weight(measurable.as_ref()) {
472                Some(weight) => {
473                    total_weight += weight;
474                    weight_unit = weight_unit.max(round_to_px(size / weight, self.density));
475                }
476                None => fixed_space += size,
477            }
478        }
479        round_to_px(weight_unit * total_weight, self.density)
480            + fixed_space
481            + self.get_spacing() * (measurables.len() - 1) as f32
482    }
483
484    /// Compose's intrinsic size across the main axis, in `main_available`
485    /// main-axis space: each unweighted child asks for the main-axis size
486    /// `main_size` gives it, up to what the children before it leave, and
487    /// the weighted children share the rest. `cross_size` gives a child's
488    /// cross size in the main-axis space it gets.
489    fn intrinsic_cross_size(
490        &self,
491        measurables: &[Box<dyn Measurable>],
492        main_available: f32,
493        main_size: impl Fn(&dyn Measurable, f32) -> f32,
494        cross_size: impl Fn(&dyn Measurable, f32) -> f32,
495    ) -> f32 {
496        if measurables.is_empty() {
497            return 0.0;
498        }
499        let mut fixed_space =
500            (self.get_spacing() * (measurables.len() - 1) as f32).min(main_available);
501        let mut cross = 0.0_f32;
502        let mut total_weight = 0.0_f32;
503        for measurable in measurables {
504            let measurable = measurable.as_ref();
505            if let Some(weight) = child_weight(measurable) {
506                total_weight += weight;
507                continue;
508            }
509            let space =
510                main_size(measurable, f32::INFINITY).min((main_available - fixed_space).max(0.0));
511            fixed_space += space;
512            cross = cross.max(cross_size(measurable, space));
513        }
514        if total_weight == 0.0 {
515            return cross;
516        }
517        let weight_unit = if main_available.is_finite() {
518            round_to_px(
519                (main_available - fixed_space).max(0.0) / total_weight,
520                self.density,
521            )
522        } else {
523            f32::INFINITY
524        };
525        for measurable in measurables {
526            let measurable = measurable.as_ref();
527            if let Some(weight) = child_weight(measurable) {
528                cross = cross.max(cross_size(
529                    measurable,
530                    round_to_px(weight_unit * weight, self.density),
531                ));
532            }
533        }
534        cross
535    }
536
537    fn get_spacing(&self) -> f32 {
538        self.main_axis_arrangement.spacing(self.density)
539    }
540
541    fn measure_weighted_children(
542        &self,
543        measurables: &[Box<dyn Measurable>],
544        weighted_children: &[(usize, ParentData)],
545        (max_main, max_cross): (f32, f32),
546        (fixed_space, spacing): (f32, f32),
547        placeables: &mut [Option<cranpose_ui_layout::Placeable>],
548    ) -> f32 {
549        let remaining_main =
550            (max_main - fixed_space - spacing * weighted_children.len().saturating_sub(1) as f32)
551                .max(0.0);
552        let mut shares = WeightShares::new(
553            remaining_main,
554            weighted_children.iter().map(|(_, data)| data.weight),
555            self.density,
556        );
557        let mut max_cross_size = 0.0_f32;
558        for &(idx, data) in weighted_children {
559            let constraints = if max_main.is_finite() {
560                let allocated = shares.next_share(data.weight);
561                let minimum = if data.fill { allocated } else { 0.0 };
562                self.make_constraints(minimum, allocated, 0.0, max_cross)
563            } else {
564                self.make_constraints(0.0, max_main, 0.0, max_cross)
565            };
566            let placeable = measurables[idx].measure(constraints);
567            max_cross_size =
568                max_cross_size.max(self.get_cross_axis_size(placeable.width(), placeable.height()));
569            placeables[idx] = Some(placeable);
570        }
571        max_cross_size
572    }
573
574    fn measured_extents(
575        &self,
576        placeables: &[cranpose_ui_layout::Placeable],
577        parent_data: &[cranpose_ui_layout::ParentData],
578    ) -> (f32, f32, f32) {
579        let mut main = 0.0;
580        let mut before = 0.0_f32;
581        let mut after = 0.0_f32;
582        for (placeable, data) in placeables.iter().zip(parent_data) {
583            main += self.get_main_axis_size(placeable.width(), placeable.height());
584            if self.axis == Axis::Horizontal
585                && data.row_baseline
586                && let Some(baseline) = placeable.alignment_lines().first_baseline()
587            {
588                before = before.max(baseline);
589                after = after.max(placeable.height() - baseline);
590            }
591        }
592        (main, before, after)
593    }
594
595    fn cross_axis_position(
596        &self,
597        placeable: &cranpose_ui_layout::Placeable,
598        parent_data: ParentData,
599        container_cross: f32,
600        before_baseline: f32,
601    ) -> f32 {
602        if self.axis == Axis::Horizontal && parent_data.row_baseline {
603            return placeable
604                .alignment_lines()
605                .first_baseline()
606                .map_or(0.0, |baseline| before_baseline - baseline);
607        }
608        let alignment = match self.axis {
609            Axis::Horizontal => parent_data
610                .row_alignment
611                .map_or(self.cross_axis_alignment, Into::into),
612            Axis::Vertical => parent_data
613                .column_alignment
614                .map_or(self.cross_axis_alignment, Into::into),
615        };
616        let child_cross = self.get_cross_axis_size(placeable.width(), placeable.height());
617        alignment.align(container_cross, child_cross, self.density)
618    }
619}
620
621/// The parent main-axis max range over which a held child keeps its size
622/// after `fixed_space` taken by the children before it, with `spacing`
623/// still fitting after it.
624fn child_main_range(
625    child_max: cranpose_ui_layout::BoundRange,
626    max_main: f32,
627    fixed_space: f32,
628    child_main: f32,
629    spacing: f32,
630) -> Option<cranpose_ui_layout::BoundRange> {
631    use cranpose_ui_layout::BoundRange;
632    if !max_main.is_finite() {
633        return child_max
634            .contains(f32::INFINITY)
635            .then_some(BoundRange::exactly(f32::INFINITY));
636    }
637    if (max_main - fixed_space).max(0.0) - child_main < spacing {
638        return None;
639    }
640    child_max
641        .outset(fixed_space)
642        .intersect(BoundRange::from(fixed_space + child_main + spacing))
643}
644
645/// The weight of a child that shares the main axis by weight.
646fn child_weight(measurable: &dyn Measurable) -> Option<f32> {
647    measurable
648        .flex_parent_data()
649        .map(|data| data.weight)
650        .filter(|weight| *weight > 0.0)
651}
652
653impl MeasurePolicy for FlexMeasurePolicy {
654    fn measure(
655        &self,
656        scope: &dyn MeasureScope,
657        measurables: &[Box<dyn Measurable>],
658        constraints: Constraints,
659    ) -> MeasureResult {
660        let mut placements = Vec::new();
661        let measurement = self.measure_into(scope, measurables, constraints, &mut placements);
662        MeasureResult::new(measurement, placements)
663    }
664
665    /// A row or column of unweighted children holds while each child holds
666    /// under the main-axis space the ones before it leave, the spacing
667    /// after each keeps its full width, and the bounds still fit the sum.
668    fn measure_hold(
669        &self,
670        measurables: &[Box<dyn Measurable>],
671        constraints: Constraints,
672        size: crate::modifier::Size,
673    ) -> Option<cranpose_ui_layout::ConstraintsHold> {
674        use cranpose_ui_layout::{AxisHold, BoundRange, ConstraintsHold};
675        if measurables.is_empty() {
676            return empty_layout_hold(constraints, size);
677        }
678        if measurables
679            .iter()
680            .any(|measurable| measurable.parent_data().has_weight())
681        {
682            return None;
683        }
684        let (_, max_main, _, _) = self.get_axis_constraints(constraints);
685        let spacing = self.get_spacing();
686        let mut main = BoundRange::ANY;
687        let mut cross = BoundRange::ANY;
688        let mut fixed_space = 0.0_f32;
689        let mut total_main = 0.0_f32;
690        let mut cross_size = 0.0_f32;
691        for measurable in measurables {
692            let (child, hold) = measurable.measured_hold()?;
693            let (main_hold, cross_hold) = match self.axis {
694                Axis::Horizontal => (hold.width, hold.height),
695                Axis::Vertical => (hold.height, hold.width),
696            };
697            let child_main = self.get_main_axis_size(child.width, child.height);
698            main = main.intersect(child_main_range(
699                main_hold.max,
700                max_main,
701                fixed_space,
702                child_main,
703                spacing,
704            )?)?;
705            cross = cross.intersect(cross_hold.max)?;
706            fixed_space += child_main + spacing;
707            total_main += child_main;
708            cross_size = cross_size.max(self.get_cross_axis_size(child.width, child.height));
709        }
710        total_main += spacing * (measurables.len() - 1) as f32;
711        let size_main = self.get_main_axis_size(size.width, size.height);
712        let size_cross = self.get_cross_axis_size(size.width, size.height);
713        // Bounds that cut or stretched the layout size it from themselves.
714        if size_main != total_main || size_cross != cross_size {
715            return None;
716        }
717        let main = AxisHold {
718            min: BoundRange::up_to(size_main),
719            max: main.intersect(BoundRange::from(size_main))?,
720        };
721        let cross = AxisHold {
722            min: BoundRange::up_to(size_cross),
723            max: cross.intersect(BoundRange::from(size_cross))?,
724        };
725        Some(match self.axis {
726            Axis::Horizontal => ConstraintsHold {
727                width: main,
728                height: cross,
729            },
730            Axis::Vertical => ConstraintsHold {
731                width: cross,
732                height: main,
733            },
734        })
735    }
736
737    fn measure_into(
738        &self,
739        _scope: &dyn MeasureScope,
740        measurables: &[Box<dyn Measurable>],
741        constraints: Constraints,
742        placements: &mut Vec<Placement>,
743    ) -> cranpose_ui_layout::Measurement {
744        placements.clear();
745        if measurables.is_empty() {
746            let (width, height) = constraints.constrain(0.0, 0.0);
747            return crate::modifier::Size { width, height }.into();
748        }
749
750        let (min_main, max_main, min_cross, max_cross) = self.get_axis_constraints(constraints);
751        let spacing = self.get_spacing();
752
753        let mut fixed_children: SmallVec<[usize; 8]> = SmallVec::new();
754        let parent_data: SmallVec<[ParentData; 8]> = measurables
755            .iter()
756            .map(|child| child.parent_data())
757            .collect();
758        let mut weighted_children: SmallVec<[(usize, ParentData); 8]> = SmallVec::new();
759
760        for (idx, data) in parent_data.iter().copied().enumerate() {
761            if data.has_weight() {
762                weighted_children.push((idx, data));
763            } else {
764                fixed_children.push(idx);
765            }
766        }
767
768        let child_constraints = self.make_constraints(0.0, max_main, 0.0, max_cross);
769
770        let mut placeables: SmallVec<[Option<cranpose_ui_layout::Placeable>; 8]> = SmallVec::new();
771        placeables.resize_with(measurables.len(), || None);
772        let (fixed_space, mut max_cross_size) = self.measure_fixed_children(
773            measurables,
774            &fixed_children,
775            (max_main, max_cross),
776            spacing,
777            &mut placeables,
778        );
779
780        let num_children = measurables.len();
781        let total_spacing = if num_children > 1 {
782            spacing * (num_children - 1) as f32
783        } else {
784            0.0
785        };
786
787        max_cross_size = max_cross_size.max(self.measure_weighted_children(
788            measurables,
789            &weighted_children,
790            (max_main, max_cross),
791            (fixed_space, spacing),
792            &mut placeables,
793        ));
794
795        let placeables: SmallVec<[cranpose_ui_layout::Placeable; 8]> = placeables
796            .into_iter()
797            .enumerate()
798            .map(|(idx, placeable)| {
799                placeable.unwrap_or_else(|| measurables[idx].measure(child_constraints))
800            })
801            .collect();
802
803        let (total_main, before_baseline, after_baseline) =
804            self.measured_extents(&placeables, &parent_data);
805        let total_main = total_main + total_spacing;
806        max_cross_size = max_cross_size.max(before_baseline + after_baseline);
807
808        let container_main = total_main.clamp(min_main, max_main);
809        let container_cross = max_cross_size.clamp(min_cross, max_cross);
810
811        let child_main_sizes: SmallVec<[f32; 8]> = placeables
812            .iter()
813            .map(|p| self.get_main_axis_size(p.width(), p.height()))
814            .collect();
815
816        let mut main_positions: SmallVec<[f32; 8]> =
817            SmallVec::with_capacity(child_main_sizes.len());
818        main_positions.resize(child_main_sizes.len(), 0.0);
819
820        let arrangement = if total_main > container_main && !self.main_axis_arrangement.is_spaced()
821        {
822            LinearArrangement::Start
823        } else {
824            self.main_axis_arrangement
825        };
826        arrangement.arrange(
827            self.density,
828            container_main,
829            &child_main_sizes,
830            &mut main_positions,
831        );
832
833        placements.reserve(placeables.len());
834        for (idx, (placeable, main_pos)) in placeables.into_iter().zip(main_positions).enumerate() {
835            let cross_pos = self.cross_axis_position(
836                &placeable,
837                parent_data[idx],
838                container_cross,
839                before_baseline,
840            );
841
842            let (x, y) = match self.axis {
843                Axis::Horizontal => (main_pos, cross_pos),
844                Axis::Vertical => (cross_pos, main_pos),
845            };
846            let width = match self.axis {
847                Axis::Horizontal => container_main,
848                Axis::Vertical => container_cross,
849            };
850            let x = self.layout_direction.place_x(x, width, placeable.width());
851
852            placeable.place(x, y);
853            placements.push(Placement::new(placeable.node_id(), x, y, 0));
854        }
855
856        let (width, height) = match self.axis {
857            Axis::Horizontal => (container_main, container_cross),
858            Axis::Vertical => (container_cross, container_main),
859        };
860
861        crate::modifier::Size { width, height }.into()
862    }
863
864    fn min_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32 {
865        match self.axis {
866            Axis::Horizontal => {
867                self.intrinsic_main_size(measurables, |m| m.min_intrinsic_width(height))
868            }
869            Axis::Vertical => self.intrinsic_cross_size(
870                measurables,
871                height,
872                |m, width| m.max_intrinsic_height(width),
873                |m, main| m.min_intrinsic_width(main),
874            ),
875        }
876    }
877
878    fn max_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32 {
879        match self.axis {
880            Axis::Horizontal => {
881                self.intrinsic_main_size(measurables, |m| m.max_intrinsic_width(height))
882            }
883            Axis::Vertical => self.intrinsic_cross_size(
884                measurables,
885                height,
886                |m, width| m.max_intrinsic_height(width),
887                |m, main| m.max_intrinsic_width(main),
888            ),
889        }
890    }
891
892    fn min_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32 {
893        match self.axis {
894            Axis::Horizontal => self.intrinsic_cross_size(
895                measurables,
896                width,
897                |m, height| m.max_intrinsic_width(height),
898                |m, main| m.min_intrinsic_height(main),
899            ),
900            Axis::Vertical => {
901                self.intrinsic_main_size(measurables, |m| m.min_intrinsic_height(width))
902            }
903        }
904    }
905
906    fn max_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32 {
907        match self.axis {
908            Axis::Horizontal => self.intrinsic_cross_size(
909                measurables,
910                width,
911                |m, height| m.max_intrinsic_width(height),
912                |m, main| m.max_intrinsic_height(main),
913            ),
914            Axis::Vertical => {
915                self.intrinsic_main_size(measurables, |m| m.max_intrinsic_height(width))
916            }
917        }
918    }
919}
920
921/// MeasurePolicy for FlowRow: children flow horizontally and wrap onto the
922/// next line when the available width runs out (Jetpack Compose `FlowRow`).
923///
924/// - Children are measured with loose constraints (min = 0) capped at the
925///   incoming max width/height, then packed left-to-right.
926/// - A child that no longer fits on the current line starts a new line; a
927///   child wider than the whole line gets a line of its own (and may
928///   overflow, like Compose).
929/// - `main_axis_spacing` separates children on the same line and
930///   `cross_axis_spacing` separates lines; children are top-aligned within
931///   their line.
932/// - With an unbounded max width everything stays on one line.
933#[derive(Clone, Debug, PartialEq)]
934pub struct FlowRowMeasurePolicy {
935    /// Horizontal gap between adjacent children on the same line, in dp.
936    pub main_axis_spacing: f32,
937    /// Vertical gap between consecutive lines, in dp.
938    pub cross_axis_spacing: f32,
939    /// Direction used to place items within each wrapped row.
940    pub layout_direction: crate::LayoutDirection,
941}
942
943impl FlowRowMeasurePolicy {
944    /// Creates a policy with the given gaps in logical pixels. Negative gaps count as zero. The
945    /// layout direction is left to right.
946    pub fn new(main_axis_spacing: f32, cross_axis_spacing: f32) -> Self {
947        Self {
948            main_axis_spacing: main_axis_spacing.max(0.0),
949            cross_axis_spacing: cross_axis_spacing.max(0.0),
950            layout_direction: crate::LayoutDirection::Ltr,
951        }
952    }
953
954    fn wrapped_intrinsic_height(
955        &self,
956        measurables: &[Box<dyn Measurable>],
957        available_width: f32,
958        use_min_height: bool,
959    ) -> f32 {
960        let mut cursor_x = 0.0_f32;
961        let mut line_top = 0.0_f32;
962        let mut line_height = 0.0_f32;
963
964        for measurable in measurables {
965            let child_width = measurable.max_intrinsic_width(f32::INFINITY);
966            let child_height = if use_min_height {
967                measurable.min_intrinsic_height(child_width)
968            } else {
969                measurable.max_intrinsic_height(child_width)
970            };
971
972            if cursor_x > 0.0 && cursor_x + self.main_axis_spacing + child_width > available_width {
973                line_top += line_height + self.cross_axis_spacing;
974                cursor_x = 0.0;
975                line_height = 0.0;
976            }
977            cursor_x += if cursor_x > 0.0 {
978                self.main_axis_spacing + child_width
979            } else {
980                child_width
981            };
982            line_height = line_height.max(child_height);
983        }
984
985        line_top + line_height
986    }
987}
988
989impl MeasurePolicy for FlowRowMeasurePolicy {
990    fn measure(
991        &self,
992        scope: &dyn MeasureScope,
993        measurables: &[Box<dyn Measurable>],
994        constraints: Constraints,
995    ) -> MeasureResult {
996        let mut placements = Vec::new();
997        let measurement = self.measure_into(scope, measurables, constraints, &mut placements);
998        MeasureResult::new(measurement, placements)
999    }
1000
1001    fn measure_into(
1002        &self,
1003        _scope: &dyn MeasureScope,
1004        measurables: &[Box<dyn Measurable>],
1005        constraints: Constraints,
1006        placements: &mut Vec<Placement>,
1007    ) -> cranpose_ui_layout::Measurement {
1008        placements.clear();
1009        if measurables.is_empty() {
1010            let (width, height) = constraints.constrain(0.0, 0.0);
1011            return crate::modifier::Size { width, height }.into();
1012        }
1013
1014        let child_constraints = Constraints {
1015            min_width: 0.0,
1016            max_width: constraints.max_width,
1017            min_height: 0.0,
1018            max_height: constraints.max_height,
1019        };
1020
1021        let placeables: SmallVec<[cranpose_ui_layout::Placeable; 8]> = measurables
1022            .iter()
1023            .map(|measurable| measurable.measure(child_constraints))
1024            .collect();
1025
1026        let mut cursor_x = 0.0_f32;
1027        let mut line_top = 0.0_f32;
1028        let mut line_height = 0.0_f32;
1029        let mut max_line_width = 0.0_f32;
1030
1031        placements.reserve(placeables.len());
1032        for placeable in &placeables {
1033            let child_width = placeable.width();
1034            let child_height = placeable.height();
1035
1036            if cursor_x > 0.0
1037                && cursor_x + self.main_axis_spacing + child_width > constraints.max_width
1038            {
1039                max_line_width = max_line_width.max(cursor_x);
1040                line_top += line_height + self.cross_axis_spacing;
1041                cursor_x = 0.0;
1042                line_height = 0.0;
1043            }
1044
1045            let x = if cursor_x > 0.0 {
1046                cursor_x + self.main_axis_spacing
1047            } else {
1048                0.0
1049            };
1050            if !self.layout_direction.is_rtl() {
1051                placeable.place(x, line_top);
1052            }
1053            placements.push(Placement::new(placeable.node_id(), x, line_top, 0));
1054
1055            cursor_x = x + child_width;
1056            line_height = line_height.max(child_height);
1057        }
1058        max_line_width = max_line_width.max(cursor_x);
1059
1060        let width = max_line_width.clamp(constraints.min_width, constraints.max_width);
1061        if self.layout_direction.is_rtl() {
1062            for (placement, placeable) in placements.iter_mut().zip(&placeables) {
1063                placement.x = self
1064                    .layout_direction
1065                    .place_x(placement.x, width, placeable.width());
1066                placeable.place(placement.x, placement.y);
1067            }
1068        }
1069        let height = (line_top + line_height).clamp(constraints.min_height, constraints.max_height);
1070        crate::modifier::Size { width, height }.into()
1071    }
1072
1073    fn min_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32 {
1074        measurables
1075            .iter()
1076            .map(|m| m.min_intrinsic_width(height))
1077            .fold(0.0, f32::max)
1078    }
1079
1080    fn max_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32 {
1081        let total_spacing = if measurables.len() > 1 {
1082            self.main_axis_spacing * (measurables.len() - 1) as f32
1083        } else {
1084            0.0
1085        };
1086        measurables
1087            .iter()
1088            .map(|m| m.max_intrinsic_width(height))
1089            .sum::<f32>()
1090            + total_spacing
1091    }
1092
1093    fn min_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32 {
1094        self.wrapped_intrinsic_height(measurables, width, true)
1095    }
1096
1097    fn max_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32 {
1098        self.wrapped_intrinsic_height(measurables, width, false)
1099    }
1100}
1101
1102/// MeasurePolicy for leaf nodes with fixed intrinsic size (like Spacer).
1103/// This policy respects the provided constraints but has a preferred intrinsic size.
1104#[derive(Clone, Debug, PartialEq)]
1105pub struct LeafMeasurePolicy {
1106    /// Preferred size in logical pixels. The measured size is this size limited by the incoming
1107    /// constraints.
1108    pub intrinsic_size: crate::modifier::Size,
1109}
1110
1111impl LeafMeasurePolicy {
1112    /// Creates a policy whose preferred size is `intrinsic_size`.
1113    pub fn new(intrinsic_size: crate::modifier::Size) -> Self {
1114        Self { intrinsic_size }
1115    }
1116}
1117
1118impl MeasurePolicy for LeafMeasurePolicy {
1119    fn measure(
1120        &self,
1121        scope: &dyn MeasureScope,
1122        _measurables: &[Box<dyn Measurable>],
1123        constraints: Constraints,
1124    ) -> MeasureResult {
1125        let mut placements = Vec::new();
1126        let measurement = self.measure_into(scope, &[], constraints, &mut placements);
1127        MeasureResult::new(measurement, placements)
1128    }
1129
1130    fn measure_into(
1131        &self,
1132        _scope: &dyn MeasureScope,
1133        _measurables: &[Box<dyn Measurable>],
1134        constraints: Constraints,
1135        placements: &mut Vec<Placement>,
1136    ) -> cranpose_ui_layout::Measurement {
1137        placements.clear();
1138        let (width, height) =
1139            constraints.constrain(self.intrinsic_size.width, self.intrinsic_size.height);
1140
1141        crate::modifier::Size { width, height }.into()
1142    }
1143
1144    fn min_intrinsic_width(&self, _measurables: &[Box<dyn Measurable>], _height: f32) -> f32 {
1145        self.intrinsic_size.width
1146    }
1147
1148    fn max_intrinsic_width(&self, _measurables: &[Box<dyn Measurable>], _height: f32) -> f32 {
1149        self.intrinsic_size.width
1150    }
1151
1152    fn min_intrinsic_height(&self, _measurables: &[Box<dyn Measurable>], _width: f32) -> f32 {
1153        self.intrinsic_size.height
1154    }
1155
1156    fn max_intrinsic_height(&self, _measurables: &[Box<dyn Measurable>], _width: f32) -> f32 {
1157        self.intrinsic_size.height
1158    }
1159}
1160
1161/// EmptyMeasurePolicy that delegates all measurement to modifier nodes.
1162///
1163/// This is used when a Layout has no child layout logic - all measurement
1164/// is handled by modifier nodes (e.g., TextModifierNode for Text widgets).
1165/// Matches Jetpack Compose's EmptyMeasurePolicy pattern used in BasicText.
1166#[derive(Clone, Debug, PartialEq)]
1167pub struct EmptyMeasurePolicy;
1168
1169impl EmptyMeasurePolicy {
1170    /// Creates the policy.
1171    pub fn new() -> Self {
1172        Self
1173    }
1174}
1175
1176impl Default for EmptyMeasurePolicy {
1177    fn default() -> Self {
1178        Self::new()
1179    }
1180}
1181
1182impl MeasurePolicy for EmptyMeasurePolicy {
1183    fn measure(
1184        &self,
1185        scope: &dyn MeasureScope,
1186        _measurables: &[Box<dyn Measurable>],
1187        constraints: Constraints,
1188    ) -> MeasureResult {
1189        let mut placements = Vec::new();
1190        let measurement = self.measure_into(scope, &[], constraints, &mut placements);
1191        MeasureResult::new(measurement, placements)
1192    }
1193
1194    fn measure_into(
1195        &self,
1196        _scope: &dyn MeasureScope,
1197        _measurables: &[Box<dyn Measurable>],
1198        constraints: Constraints,
1199        placements: &mut Vec<Placement>,
1200    ) -> cranpose_ui_layout::Measurement {
1201        placements.clear();
1202        let (width, height) = constraints.constrain(0.0, 0.0);
1203
1204        crate::modifier::Size { width, height }.into()
1205    }
1206
1207    fn measure_hold(
1208        &self,
1209        _measurables: &[Box<dyn Measurable>],
1210        constraints: Constraints,
1211        size: crate::modifier::Size,
1212    ) -> Option<cranpose_ui_layout::ConstraintsHold> {
1213        empty_layout_hold(constraints, size)
1214    }
1215
1216    fn min_intrinsic_width(&self, _measurables: &[Box<dyn Measurable>], _height: f32) -> f32 {
1217        0.0
1218    }
1219
1220    fn max_intrinsic_width(&self, _measurables: &[Box<dyn Measurable>], _height: f32) -> f32 {
1221        0.0
1222    }
1223
1224    fn min_intrinsic_height(&self, _measurables: &[Box<dyn Measurable>], _width: f32) -> f32 {
1225        0.0
1226    }
1227
1228    fn max_intrinsic_height(&self, _measurables: &[Box<dyn Measurable>], _width: f32) -> f32 {
1229        0.0
1230    }
1231}
1232
1233#[cfg(test)]
1234#[path = "tests/policies_tests.rs"]
1235mod tests;
1236
1237/// What a layout with nothing in it holds for: it takes its min
1238/// constraints, so any constraints with the same mins and a max that allows
1239/// them size it the same.
1240fn empty_layout_hold(
1241    constraints: Constraints,
1242    size: crate::modifier::Size,
1243) -> Option<cranpose_ui_layout::ConstraintsHold> {
1244    (size.width == constraints.min_width && size.height == constraints.min_height)
1245        .then(|| cranpose_ui_layout::ConstraintsHold::at_min(size.width, size.height))
1246}