Skip to main content

cranpose_ui/layout/
policies.rs

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