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<cranpose_ui_layout::Placeable>; 8]> = SmallVec::new();
338        placeables.resize_with(measurables.len(), || None);
339        let mut fixed_main_size = 0.0_f32;
340        let mut max_cross_size = 0.0_f32;
341
342        for &idx in &fixed_children {
343            let measurable = &measurables[idx];
344            let placeable = measurable.measure(child_constraints);
345            let main_size = self.get_main_axis_size(placeable.width(), placeable.height());
346            let cross_size = self.get_cross_axis_size(placeable.width(), placeable.height());
347
348            fixed_main_size += main_size;
349            max_cross_size = max_cross_size.max(cross_size);
350            placeables[idx] = Some(placeable);
351        }
352
353        // Calculate spacing
354        let num_children = measurables.len();
355        let total_spacing = if num_children > 1 {
356            spacing * (num_children - 1) as f32
357        } else {
358            0.0
359        };
360
361        // Measure weighted children
362        if !weighted_children.is_empty() {
363            if main_axis_bounded {
364                // Calculate remaining space for weighted children
365                let used_main = fixed_main_size + total_spacing;
366                let remaining_main = (max_main - used_main).max(0.0);
367
368                // Calculate total weight
369                let total_weight: f32 = weighted_children.iter().map(|(_, data)| data.weight).sum();
370
371                // Measure each weighted child with its allocated space
372                for &(idx, parent_data) in &weighted_children {
373                    let measurable = &measurables[idx];
374                    let allocated = if total_weight > 0.0 {
375                        remaining_main * (parent_data.weight / total_weight)
376                    } else {
377                        0.0
378                    };
379
380                    let weighted_constraints = if parent_data.fill {
381                        // fill=true: child gets tight constraints on main axis
382                        self.make_constraints(allocated, allocated, 0.0, max_cross)
383                    } else {
384                        // fill=false: child gets loose constraints on main axis
385                        self.make_constraints(0.0, allocated, 0.0, max_cross)
386                    };
387
388                    let placeable = measurable.measure(weighted_constraints);
389                    let cross_size =
390                        self.get_cross_axis_size(placeable.width(), placeable.height());
391                    max_cross_size = max_cross_size.max(cross_size);
392                    placeables[idx] = Some(placeable);
393                }
394            } else {
395                // Main axis unbounded: ignore weights, measure like fixed children
396                for &(idx, _) in &weighted_children {
397                    let measurable = &measurables[idx];
398                    let placeable = measurable.measure(child_constraints);
399                    let cross_size =
400                        self.get_cross_axis_size(placeable.width(), placeable.height());
401                    max_cross_size = max_cross_size.max(cross_size);
402                    placeables[idx] = Some(placeable);
403                }
404            }
405        }
406
407        // Unwrap all placeables
408        let placeables: SmallVec<[cranpose_ui_layout::Placeable; 8]> = placeables
409            .into_iter()
410            .map(|p| p.expect("placeable missing"))
411            .collect();
412
413        // Calculate total main size
414        let total_main: f32 = placeables
415            .iter()
416            .map(|p| self.get_main_axis_size(p.width(), p.height()))
417            .sum::<f32>()
418            + total_spacing;
419
420        // Container size
421        let container_main = total_main.clamp(min_main, max_main);
422        let container_cross = max_cross_size.clamp(min_cross, max_cross);
423
424        // Arrange children along main axis
425        let child_main_sizes: SmallVec<[f32; 8]> = placeables
426            .iter()
427            .map(|p| self.get_main_axis_size(p.width(), p.height()))
428            .collect();
429
430        let mut main_positions: SmallVec<[f32; 8]> =
431            SmallVec::with_capacity(child_main_sizes.len());
432        main_positions.resize(child_main_sizes.len(), 0.0);
433
434        // If we overflow, use Start arrangement to avoid negative spacing
435        let arrangement = if total_main > container_main {
436            LinearArrangement::Start
437        } else {
438            self.main_axis_arrangement
439        };
440        arrangement.arrange(container_main, &child_main_sizes, &mut main_positions);
441
442        // Place children
443        let mut placements: SmallVec<[Placement; 8]> = SmallVec::with_capacity(placeables.len());
444        for (placeable, main_pos) in placeables.into_iter().zip(main_positions.into_iter()) {
445            let child_cross = self.get_cross_axis_size(placeable.width(), placeable.height());
446            let cross_pos = self
447                .cross_axis_alignment
448                .align(container_cross, child_cross);
449
450            let (x, y) = match self.axis {
451                Axis::Horizontal => (main_pos, cross_pos),
452                Axis::Vertical => (cross_pos, main_pos),
453            };
454
455            placeable.place(x, y);
456            placements.push(Placement::new(placeable.node_id(), x, y, 0));
457        }
458
459        // Create final size
460        let (width, height) = match self.axis {
461            Axis::Horizontal => (container_main, container_cross),
462            Axis::Vertical => (container_cross, container_main),
463        };
464
465        MeasureResult::new(
466            crate::modifier::Size { width, height },
467            placements.into_vec(),
468        )
469    }
470
471    fn min_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32 {
472        let spacing = self.get_spacing();
473        let total_spacing = if measurables.len() > 1 {
474            spacing * (measurables.len() - 1) as f32
475        } else {
476            0.0
477        };
478
479        match self.axis {
480            Axis::Horizontal => {
481                // Row: sum of children's min intrinsic widths + spacing
482                measurables
483                    .iter()
484                    .map(|m| m.min_intrinsic_width(height))
485                    .sum::<f32>()
486                    + total_spacing
487            }
488            Axis::Vertical => {
489                // Column: max of children's min intrinsic widths
490                measurables
491                    .iter()
492                    .map(|m| m.min_intrinsic_width(height))
493                    .fold(0.0, f32::max)
494            }
495        }
496    }
497
498    fn max_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32 {
499        let spacing = self.get_spacing();
500        let total_spacing = if measurables.len() > 1 {
501            spacing * (measurables.len() - 1) as f32
502        } else {
503            0.0
504        };
505
506        match self.axis {
507            Axis::Horizontal => {
508                // Row: sum of children's max intrinsic widths + spacing
509                measurables
510                    .iter()
511                    .map(|m| m.max_intrinsic_width(height))
512                    .sum::<f32>()
513                    + total_spacing
514            }
515            Axis::Vertical => {
516                // Column: max of children's max intrinsic widths
517                measurables
518                    .iter()
519                    .map(|m| m.max_intrinsic_width(height))
520                    .fold(0.0, f32::max)
521            }
522        }
523    }
524
525    fn min_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32 {
526        let spacing = self.get_spacing();
527        let total_spacing = if measurables.len() > 1 {
528            spacing * (measurables.len() - 1) as f32
529        } else {
530            0.0
531        };
532
533        match self.axis {
534            Axis::Horizontal => {
535                // Row: max of children's min intrinsic heights
536                measurables
537                    .iter()
538                    .map(|m| m.min_intrinsic_height(width))
539                    .fold(0.0, f32::max)
540            }
541            Axis::Vertical => {
542                // Column: sum of children's min intrinsic heights + spacing
543                measurables
544                    .iter()
545                    .map(|m| m.min_intrinsic_height(width))
546                    .sum::<f32>()
547                    + total_spacing
548            }
549        }
550    }
551
552    fn max_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32 {
553        let spacing = self.get_spacing();
554        let total_spacing = if measurables.len() > 1 {
555            spacing * (measurables.len() - 1) as f32
556        } else {
557            0.0
558        };
559
560        match self.axis {
561            Axis::Horizontal => {
562                // Row: max of children's max intrinsic heights
563                measurables
564                    .iter()
565                    .map(|m| m.max_intrinsic_height(width))
566                    .fold(0.0, f32::max)
567            }
568            Axis::Vertical => {
569                // Column: sum of children's max intrinsic heights + spacing
570                measurables
571                    .iter()
572                    .map(|m| m.max_intrinsic_height(width))
573                    .sum::<f32>()
574                    + total_spacing
575            }
576        }
577    }
578}
579
580/// MeasurePolicy for leaf nodes with fixed intrinsic size (like Spacer).
581/// This policy respects the provided constraints but has a preferred intrinsic size.
582#[derive(Clone, Debug, PartialEq)]
583pub struct LeafMeasurePolicy {
584    pub intrinsic_size: crate::modifier::Size,
585}
586
587impl LeafMeasurePolicy {
588    pub fn new(intrinsic_size: crate::modifier::Size) -> Self {
589        Self { intrinsic_size }
590    }
591}
592
593impl MeasurePolicy for LeafMeasurePolicy {
594    fn measure(
595        &self,
596        _measurables: &[Box<dyn Measurable>],
597        constraints: Constraints,
598    ) -> MeasureResult {
599        // Use intrinsic size but constrain to provided constraints
600        let (width, height) =
601            constraints.constrain(self.intrinsic_size.width, self.intrinsic_size.height);
602
603        MeasureResult::new(
604            crate::modifier::Size { width, height },
605            vec![], // Leaf nodes have no children
606        )
607    }
608
609    fn min_intrinsic_width(&self, _measurables: &[Box<dyn Measurable>], _height: f32) -> f32 {
610        self.intrinsic_size.width
611    }
612
613    fn max_intrinsic_width(&self, _measurables: &[Box<dyn Measurable>], _height: f32) -> f32 {
614        self.intrinsic_size.width
615    }
616
617    fn min_intrinsic_height(&self, _measurables: &[Box<dyn Measurable>], _width: f32) -> f32 {
618        self.intrinsic_size.height
619    }
620
621    fn max_intrinsic_height(&self, _measurables: &[Box<dyn Measurable>], _width: f32) -> f32 {
622        self.intrinsic_size.height
623    }
624}
625
626/// EmptyMeasurePolicy that delegates all measurement to modifier nodes.
627///
628/// This is used when a Layout has no child layout logic - all measurement
629/// is handled by modifier nodes (e.g., TextModifierNode for Text widgets).
630/// Matches Jetpack Compose's EmptyMeasurePolicy pattern used in BasicText.
631#[derive(Clone, Debug, PartialEq)]
632pub struct EmptyMeasurePolicy;
633
634impl EmptyMeasurePolicy {
635    pub fn new() -> Self {
636        Self
637    }
638}
639
640impl Default for EmptyMeasurePolicy {
641    fn default() -> Self {
642        Self::new()
643    }
644}
645
646impl MeasurePolicy for EmptyMeasurePolicy {
647    fn measure(
648        &self,
649        _measurables: &[Box<dyn Measurable>],
650        constraints: Constraints,
651    ) -> MeasureResult {
652        // Empty policy returns the maximum available space
653        // The actual measurement is handled by modifier nodes in the chain
654        let (width, height) = constraints.constrain(0.0, 0.0);
655
656        MeasureResult::new(
657            crate::modifier::Size { width, height },
658            vec![], // No children
659        )
660    }
661
662    fn min_intrinsic_width(&self, _measurables: &[Box<dyn Measurable>], _height: f32) -> f32 {
663        0.0
664    }
665
666    fn max_intrinsic_width(&self, _measurables: &[Box<dyn Measurable>], _height: f32) -> f32 {
667        0.0
668    }
669
670    fn min_intrinsic_height(&self, _measurables: &[Box<dyn Measurable>], _width: f32) -> f32 {
671        0.0
672    }
673
674    fn max_intrinsic_height(&self, _measurables: &[Box<dyn Measurable>], _width: f32) -> f32 {
675        0.0
676    }
677}
678
679#[cfg(test)]
680#[path = "tests/policies_tests.rs"]
681mod tests;