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::{
5    Axis, Constraints, FlexParentData, MeasurePolicy, MeasureResult, Placement,
6};
7use smallvec::SmallVec;
8
9/// MeasurePolicy for Box layout - overlays children according to alignment.
10#[derive(Clone, Debug, PartialEq)]
11pub struct BoxMeasurePolicy {
12    pub content_alignment: Alignment,
13    pub propagate_min_constraints: bool,
14}
15
16impl BoxMeasurePolicy {
17    pub fn new(content_alignment: Alignment, propagate_min_constraints: bool) -> Self {
18        Self {
19            content_alignment,
20            propagate_min_constraints,
21        }
22    }
23}
24
25impl MeasurePolicy for BoxMeasurePolicy {
26    fn measure(
27        &self,
28        measurables: &[Box<dyn Measurable>],
29        constraints: Constraints,
30    ) -> MeasureResult {
31        let child_constraints = if self.propagate_min_constraints {
32            constraints
33        } else {
34            Constraints {
35                min_width: 0.0,
36                max_width: constraints.max_width,
37                min_height: 0.0,
38                max_height: constraints.max_height,
39            }
40        };
41
42        let mut max_width = 0.0_f32;
43        let mut max_height = 0.0_f32;
44        let mut placeables = Vec::with_capacity(measurables.len());
45
46        for measurable in measurables {
47            let placeable = measurable.measure(child_constraints);
48            max_width = max_width.max(placeable.width());
49            max_height = max_height.max(placeable.height());
50            placeables.push(placeable);
51        }
52
53        let width = max_width.clamp(constraints.min_width, constraints.max_width);
54        let height = max_height.clamp(constraints.min_height, constraints.max_height);
55
56        let mut placements = Vec::with_capacity(placeables.len());
57        for placeable in placeables {
58            let child_width = placeable.width();
59            let child_height = placeable.height();
60
61            let x = match self.content_alignment.horizontal {
62                HorizontalAlignment::Start => 0.0,
63                HorizontalAlignment::CenterHorizontally => ((width - child_width) / 2.0).max(0.0),
64                HorizontalAlignment::End => (width - child_width).max(0.0),
65            };
66
67            let y = match self.content_alignment.vertical {
68                VerticalAlignment::Top => 0.0,
69                VerticalAlignment::CenterVertically => ((height - child_height) / 2.0).max(0.0),
70                VerticalAlignment::Bottom => (height - child_height).max(0.0),
71            };
72
73            placeable.place(x, y);
74            placements.push(Placement::new(placeable.node_id(), x, y, 0));
75        }
76
77        MeasureResult::new(crate::modifier::Size { width, height }, placements)
78    }
79
80    fn min_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32 {
81        measurables
82            .iter()
83            .map(|m| m.min_intrinsic_width(height))
84            .fold(0.0, f32::max)
85    }
86
87    fn max_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32 {
88        measurables
89            .iter()
90            .map(|m| m.max_intrinsic_width(height))
91            .fold(0.0, f32::max)
92    }
93
94    fn min_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32 {
95        measurables
96            .iter()
97            .map(|m| m.min_intrinsic_height(width))
98            .fold(0.0, f32::max)
99    }
100
101    fn max_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32 {
102        measurables
103            .iter()
104            .map(|m| m.max_intrinsic_height(width))
105            .fold(0.0, f32::max)
106    }
107}
108
109// Note: RowMeasurePolicy and ColumnMeasurePolicy have been replaced by FlexMeasurePolicy.
110// See FlexMeasurePolicy below for the unified flex layout implementation.
111
112/// Unified Flex layout policy that powers both Row and Column.
113///
114/// This policy implements Jetpack Compose's flex layout semantics:
115/// - Measures children with proper loose constraints (min = 0 on both axes)
116/// - Supports weighted distribution of remaining space
117/// - Handles bounded/unbounded main axis correctly
118/// - Implements correct intrinsics for both axes
119///
120/// ## Overflow Behavior
121///
122/// Like Jetpack Compose, this policy **allows children to overflow** their container bounds:
123/// - Children can be positioned outside the parent's measured size
124/// - Overflowing content is rendered (unless clipped by a modifier)
125/// - When content overflows, arrangement switches to `Start` to avoid negative spacing
126///
127/// Example: A Row with 300px of content in a 200px container will:
128/// 1. Measure children at their natural sizes
129/// 2. Detect overflow (300px > 200px)
130/// 3. Switch to Start arrangement (pack children at the start)
131/// 4. Position last children beyond the 200px boundary
132///
133/// To prevent overflow:
134/// - Use weights for flexible sizing: `.weight(1.0, true)`
135/// - Use `fillMaxWidth()`/`fillMaxHeight()` modifiers
136/// - Design UI to fit within available space
137/// - Add a clip modifier (when implemented) to hide overflowing content
138///
139/// ## Weighted Children
140///
141/// When the main axis is bounded and children have weights:
142/// 1. Fixed children (no weight) are measured first
143/// 2. Remaining space is distributed proportionally to weights
144/// 3. Each weighted child gets: `remaining * (weight / total_weight)`
145/// 4. If `fill=true`, child gets tight constraints; if `fill=false`, loose constraints
146///
147/// When the main axis is unbounded, weights are ignored (all children wrap content).
148#[derive(Clone, Debug, PartialEq)]
149pub struct FlexMeasurePolicy {
150    /// Main axis direction (Horizontal for Row, Vertical for Column)
151    pub axis: Axis,
152    /// Arrangement along the main axis
153    pub main_axis_arrangement: LinearArrangement,
154    /// Alignment along the cross axis (used as default for children without explicit alignment)
155    pub cross_axis_alignment: CrossAxisAlignment,
156}
157
158/// Cross-axis alignment for flex layouts.
159/// This is axis-agnostic and gets interpreted based on the flex axis.
160#[derive(Clone, Copy, Debug, PartialEq)]
161pub enum CrossAxisAlignment {
162    /// Align to the start of the cross axis (Top for Row, Start for Column)
163    Start,
164    /// Align to the center of the cross axis
165    Center,
166    /// Align to the end of the cross axis (Bottom for Row, End for Column)
167    End,
168}
169
170impl CrossAxisAlignment {
171    /// Calculate the offset for positioning a child on the cross axis.
172    fn align(&self, available: f32, child: f32) -> f32 {
173        match self {
174            CrossAxisAlignment::Start => 0.0,
175            CrossAxisAlignment::Center => ((available - child) / 2.0).max(0.0),
176            CrossAxisAlignment::End => (available - child).max(0.0),
177        }
178    }
179}
180
181impl From<HorizontalAlignment> for CrossAxisAlignment {
182    fn from(alignment: HorizontalAlignment) -> Self {
183        match alignment {
184            HorizontalAlignment::Start => CrossAxisAlignment::Start,
185            HorizontalAlignment::CenterHorizontally => CrossAxisAlignment::Center,
186            HorizontalAlignment::End => CrossAxisAlignment::End,
187        }
188    }
189}
190
191impl From<VerticalAlignment> for CrossAxisAlignment {
192    fn from(alignment: VerticalAlignment) -> Self {
193        match alignment {
194            VerticalAlignment::Top => CrossAxisAlignment::Start,
195            VerticalAlignment::CenterVertically => CrossAxisAlignment::Center,
196            VerticalAlignment::Bottom => CrossAxisAlignment::End,
197        }
198    }
199}
200
201impl FlexMeasurePolicy {
202    pub fn new(
203        axis: Axis,
204        main_axis_arrangement: LinearArrangement,
205        cross_axis_alignment: CrossAxisAlignment,
206    ) -> Self {
207        Self {
208            axis,
209            main_axis_arrangement,
210            cross_axis_alignment,
211        }
212    }
213
214    /// Creates a FlexMeasurePolicy for Row (horizontal main axis).
215    pub fn row(
216        horizontal_arrangement: LinearArrangement,
217        vertical_alignment: VerticalAlignment,
218    ) -> Self {
219        Self::new(
220            Axis::Horizontal,
221            horizontal_arrangement,
222            vertical_alignment.into(),
223        )
224    }
225
226    /// Creates a FlexMeasurePolicy for Column (vertical main axis).
227    pub fn column(
228        vertical_arrangement: LinearArrangement,
229        horizontal_alignment: HorizontalAlignment,
230    ) -> Self {
231        Self::new(
232            Axis::Vertical,
233            vertical_arrangement,
234            horizontal_alignment.into(),
235        )
236    }
237
238    /// Extract main and cross axis values from constraints.
239    fn get_axis_constraints(&self, constraints: Constraints) -> (f32, f32, f32, f32) {
240        match self.axis {
241            Axis::Horizontal => (
242                constraints.min_width,
243                constraints.max_width,
244                constraints.min_height,
245                constraints.max_height,
246            ),
247            Axis::Vertical => (
248                constraints.min_height,
249                constraints.max_height,
250                constraints.min_width,
251                constraints.max_width,
252            ),
253        }
254    }
255
256    /// Create constraints from main and cross axis values.
257    fn make_constraints(
258        &self,
259        min_main: f32,
260        max_main: f32,
261        min_cross: f32,
262        max_cross: f32,
263    ) -> Constraints {
264        match self.axis {
265            Axis::Horizontal => Constraints {
266                min_width: min_main,
267                max_width: max_main,
268                min_height: min_cross,
269                max_height: max_cross,
270            },
271            Axis::Vertical => Constraints {
272                min_width: min_cross,
273                max_width: max_cross,
274                min_height: min_main,
275                max_height: max_main,
276            },
277        }
278    }
279
280    /// Get the main axis size from width/height.
281    fn get_main_axis_size(&self, width: f32, height: f32) -> f32 {
282        match self.axis {
283            Axis::Horizontal => width,
284            Axis::Vertical => height,
285        }
286    }
287
288    /// Get the cross axis size from width/height.
289    fn get_cross_axis_size(&self, width: f32, height: f32) -> f32 {
290        match self.axis {
291            Axis::Horizontal => height,
292            Axis::Vertical => width,
293        }
294    }
295
296    /// Calculate spacing between children based on arrangement.
297    fn get_spacing(&self) -> f32 {
298        match self.main_axis_arrangement {
299            LinearArrangement::SpacedBy(value) => value.max(0.0),
300            _ => 0.0,
301        }
302    }
303}
304
305impl MeasurePolicy for FlexMeasurePolicy {
306    fn measure(
307        &self,
308        measurables: &[Box<dyn Measurable>],
309        constraints: Constraints,
310    ) -> MeasureResult {
311        if measurables.is_empty() {
312            let (width, height) = constraints.constrain(0.0, 0.0);
313            return MeasureResult::new(crate::modifier::Size { width, height }, vec![]);
314        }
315
316        let (min_main, max_main, min_cross, max_cross) = self.get_axis_constraints(constraints);
317        let main_axis_bounded = max_main.is_finite();
318        let spacing = self.get_spacing();
319
320        // Separate children into fixed and weighted
321        let mut fixed_children: SmallVec<[usize; 8]> = SmallVec::new();
322        let mut weighted_children: SmallVec<[(usize, FlexParentData); 8]> = SmallVec::new();
323
324        for (idx, measurable) in measurables.iter().enumerate() {
325            let parent_data = measurable.flex_parent_data().unwrap_or_default();
326            if parent_data.has_weight() {
327                weighted_children.push((idx, parent_data));
328            } else {
329                fixed_children.push(idx);
330            }
331        }
332
333        // Measure fixed children first
334        // Children get loose constraints on both axes (min = 0)
335        let child_constraints = self.make_constraints(0.0, max_main, 0.0, max_cross);
336
337        let mut placeables: SmallVec<[Option<Box<dyn cranpose_ui_layout::Placeable>>; 8]> =
338            SmallVec::new();
339        placeables.resize_with(measurables.len(), || None);
340        let mut fixed_main_size = 0.0_f32;
341        let mut max_cross_size = 0.0_f32;
342
343        for &idx in &fixed_children {
344            let measurable = &measurables[idx];
345            let placeable = measurable.measure(child_constraints);
346            let main_size = self.get_main_axis_size(placeable.width(), placeable.height());
347            let cross_size = self.get_cross_axis_size(placeable.width(), placeable.height());
348
349            fixed_main_size += main_size;
350            max_cross_size = max_cross_size.max(cross_size);
351            placeables[idx] = Some(placeable);
352        }
353
354        // Calculate spacing
355        let num_children = measurables.len();
356        let total_spacing = if num_children > 1 {
357            spacing * (num_children - 1) as f32
358        } else {
359            0.0
360        };
361
362        // Measure weighted children
363        if !weighted_children.is_empty() {
364            if main_axis_bounded {
365                // Calculate remaining space for weighted children
366                let used_main = fixed_main_size + total_spacing;
367                let remaining_main = (max_main - used_main).max(0.0);
368
369                // Calculate total weight
370                let total_weight: f32 = weighted_children.iter().map(|(_, data)| data.weight).sum();
371
372                // Measure each weighted child with its allocated space
373                for &(idx, parent_data) in &weighted_children {
374                    let measurable = &measurables[idx];
375                    let allocated = if total_weight > 0.0 {
376                        remaining_main * (parent_data.weight / total_weight)
377                    } else {
378                        0.0
379                    };
380
381                    let weighted_constraints = if parent_data.fill {
382                        // fill=true: child gets tight constraints on main axis
383                        self.make_constraints(allocated, allocated, 0.0, max_cross)
384                    } else {
385                        // fill=false: child gets loose constraints on main axis
386                        self.make_constraints(0.0, allocated, 0.0, max_cross)
387                    };
388
389                    let placeable = measurable.measure(weighted_constraints);
390                    let cross_size =
391                        self.get_cross_axis_size(placeable.width(), placeable.height());
392                    max_cross_size = max_cross_size.max(cross_size);
393                    placeables[idx] = Some(placeable);
394                }
395            } else {
396                // Main axis unbounded: ignore weights, measure like fixed children
397                for &(idx, _) in &weighted_children {
398                    let measurable = &measurables[idx];
399                    let placeable = measurable.measure(child_constraints);
400                    let cross_size =
401                        self.get_cross_axis_size(placeable.width(), placeable.height());
402                    max_cross_size = max_cross_size.max(cross_size);
403                    placeables[idx] = Some(placeable);
404                }
405            }
406        }
407
408        // Unwrap all placeables
409        let placeables: SmallVec<[Box<dyn cranpose_ui_layout::Placeable>; 8]> = placeables
410            .into_iter()
411            .map(|p| p.expect("placeable missing"))
412            .collect();
413
414        // Calculate total main size
415        let total_main: f32 = placeables
416            .iter()
417            .map(|p| self.get_main_axis_size(p.width(), p.height()))
418            .sum::<f32>()
419            + total_spacing;
420
421        // Container size
422        let container_main = total_main.clamp(min_main, max_main);
423        let container_cross = max_cross_size.clamp(min_cross, max_cross);
424
425        // Arrange children along main axis
426        let child_main_sizes: SmallVec<[f32; 8]> = placeables
427            .iter()
428            .map(|p| self.get_main_axis_size(p.width(), p.height()))
429            .collect();
430
431        let mut main_positions: SmallVec<[f32; 8]> =
432            SmallVec::with_capacity(child_main_sizes.len());
433        main_positions.resize(child_main_sizes.len(), 0.0);
434
435        // If we overflow, use Start arrangement to avoid negative spacing
436        let arrangement = if total_main > container_main {
437            LinearArrangement::Start
438        } else {
439            self.main_axis_arrangement
440        };
441        arrangement.arrange(container_main, &child_main_sizes, &mut main_positions);
442
443        // Place children
444        let mut placements: SmallVec<[Placement; 8]> = SmallVec::with_capacity(placeables.len());
445        for (placeable, main_pos) in placeables.into_iter().zip(main_positions.into_iter()) {
446            let child_cross = self.get_cross_axis_size(placeable.width(), placeable.height());
447            let cross_pos = self
448                .cross_axis_alignment
449                .align(container_cross, child_cross);
450
451            let (x, y) = match self.axis {
452                Axis::Horizontal => (main_pos, cross_pos),
453                Axis::Vertical => (cross_pos, main_pos),
454            };
455
456            placeable.place(x, y);
457            placements.push(Placement::new(placeable.node_id(), x, y, 0));
458        }
459
460        // Create final size
461        let (width, height) = match self.axis {
462            Axis::Horizontal => (container_main, container_cross),
463            Axis::Vertical => (container_cross, container_main),
464        };
465
466        MeasureResult::new(
467            crate::modifier::Size { width, height },
468            placements.into_vec(),
469        )
470    }
471
472    fn min_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32 {
473        let spacing = self.get_spacing();
474        let total_spacing = if measurables.len() > 1 {
475            spacing * (measurables.len() - 1) as f32
476        } else {
477            0.0
478        };
479
480        match self.axis {
481            Axis::Horizontal => {
482                // Row: sum of children's min intrinsic widths + spacing
483                measurables
484                    .iter()
485                    .map(|m| m.min_intrinsic_width(height))
486                    .sum::<f32>()
487                    + total_spacing
488            }
489            Axis::Vertical => {
490                // Column: max of children's min intrinsic widths
491                measurables
492                    .iter()
493                    .map(|m| m.min_intrinsic_width(height))
494                    .fold(0.0, f32::max)
495            }
496        }
497    }
498
499    fn max_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32 {
500        let spacing = self.get_spacing();
501        let total_spacing = if measurables.len() > 1 {
502            spacing * (measurables.len() - 1) as f32
503        } else {
504            0.0
505        };
506
507        match self.axis {
508            Axis::Horizontal => {
509                // Row: sum of children's max intrinsic widths + spacing
510                measurables
511                    .iter()
512                    .map(|m| m.max_intrinsic_width(height))
513                    .sum::<f32>()
514                    + total_spacing
515            }
516            Axis::Vertical => {
517                // Column: max of children's max intrinsic widths
518                measurables
519                    .iter()
520                    .map(|m| m.max_intrinsic_width(height))
521                    .fold(0.0, f32::max)
522            }
523        }
524    }
525
526    fn min_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32 {
527        let spacing = self.get_spacing();
528        let total_spacing = if measurables.len() > 1 {
529            spacing * (measurables.len() - 1) as f32
530        } else {
531            0.0
532        };
533
534        match self.axis {
535            Axis::Horizontal => {
536                // Row: max of children's min intrinsic heights
537                measurables
538                    .iter()
539                    .map(|m| m.min_intrinsic_height(width))
540                    .fold(0.0, f32::max)
541            }
542            Axis::Vertical => {
543                // Column: sum of children's min intrinsic heights + spacing
544                measurables
545                    .iter()
546                    .map(|m| m.min_intrinsic_height(width))
547                    .sum::<f32>()
548                    + total_spacing
549            }
550        }
551    }
552
553    fn max_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32 {
554        let spacing = self.get_spacing();
555        let total_spacing = if measurables.len() > 1 {
556            spacing * (measurables.len() - 1) as f32
557        } else {
558            0.0
559        };
560
561        match self.axis {
562            Axis::Horizontal => {
563                // Row: max of children's max intrinsic heights
564                measurables
565                    .iter()
566                    .map(|m| m.max_intrinsic_height(width))
567                    .fold(0.0, f32::max)
568            }
569            Axis::Vertical => {
570                // Column: sum of children's max intrinsic heights + spacing
571                measurables
572                    .iter()
573                    .map(|m| m.max_intrinsic_height(width))
574                    .sum::<f32>()
575                    + total_spacing
576            }
577        }
578    }
579}
580
581/// MeasurePolicy for leaf nodes with fixed intrinsic size (like Spacer).
582/// This policy respects the provided constraints but has a preferred intrinsic size.
583#[derive(Clone, Debug, PartialEq)]
584pub struct LeafMeasurePolicy {
585    pub intrinsic_size: crate::modifier::Size,
586}
587
588impl LeafMeasurePolicy {
589    pub fn new(intrinsic_size: crate::modifier::Size) -> Self {
590        Self { intrinsic_size }
591    }
592}
593
594impl MeasurePolicy for LeafMeasurePolicy {
595    fn measure(
596        &self,
597        _measurables: &[Box<dyn Measurable>],
598        constraints: Constraints,
599    ) -> MeasureResult {
600        // Use intrinsic size but constrain to provided constraints
601        let (width, height) =
602            constraints.constrain(self.intrinsic_size.width, self.intrinsic_size.height);
603
604        MeasureResult::new(
605            crate::modifier::Size { width, height },
606            vec![], // Leaf nodes have no children
607        )
608    }
609
610    fn min_intrinsic_width(&self, _measurables: &[Box<dyn Measurable>], _height: f32) -> f32 {
611        self.intrinsic_size.width
612    }
613
614    fn max_intrinsic_width(&self, _measurables: &[Box<dyn Measurable>], _height: f32) -> f32 {
615        self.intrinsic_size.width
616    }
617
618    fn min_intrinsic_height(&self, _measurables: &[Box<dyn Measurable>], _width: f32) -> f32 {
619        self.intrinsic_size.height
620    }
621
622    fn max_intrinsic_height(&self, _measurables: &[Box<dyn Measurable>], _width: f32) -> f32 {
623        self.intrinsic_size.height
624    }
625}
626
627/// EmptyMeasurePolicy that delegates all measurement to modifier nodes.
628///
629/// This is used when a Layout has no child layout logic - all measurement
630/// is handled by modifier nodes (e.g., TextModifierNode for Text widgets).
631/// Matches Jetpack Compose's EmptyMeasurePolicy pattern used in BasicText.
632#[derive(Clone, Debug, PartialEq)]
633pub struct EmptyMeasurePolicy;
634
635impl EmptyMeasurePolicy {
636    pub fn new() -> Self {
637        Self
638    }
639}
640
641impl Default for EmptyMeasurePolicy {
642    fn default() -> Self {
643        Self::new()
644    }
645}
646
647impl MeasurePolicy for EmptyMeasurePolicy {
648    fn measure(
649        &self,
650        _measurables: &[Box<dyn Measurable>],
651        constraints: Constraints,
652    ) -> MeasureResult {
653        // Empty policy returns the maximum available space
654        // The actual measurement is handled by modifier nodes in the chain
655        let (width, height) = constraints.constrain(0.0, 0.0);
656
657        MeasureResult::new(
658            crate::modifier::Size { width, height },
659            vec![], // No children
660        )
661    }
662
663    fn min_intrinsic_width(&self, _measurables: &[Box<dyn Measurable>], _height: f32) -> f32 {
664        0.0
665    }
666
667    fn max_intrinsic_width(&self, _measurables: &[Box<dyn Measurable>], _height: f32) -> f32 {
668        0.0
669    }
670
671    fn min_intrinsic_height(&self, _measurables: &[Box<dyn Measurable>], _width: f32) -> f32 {
672        0.0
673    }
674
675    fn max_intrinsic_height(&self, _measurables: &[Box<dyn Measurable>], _width: f32) -> f32 {
676        0.0
677    }
678}
679
680#[cfg(test)]
681#[path = "tests/policies_tests.rs"]
682mod tests;