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