Skip to main content

cranpose_ui/layout/
policies.rs

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