Skip to main content

cranpose_ui_layout/
arrangement.rs

1//! Arrangement strategies for distributing children along an axis
2
3use crate::{alignment::bias_offset, round_to_px};
4
5/// Trait implemented by arrangement strategies that distribute children on an axis.
6pub trait Arrangement {
7    /// Computes the position for each child given the available space and
8    /// their sizes, on the device pixel grid of `density`, where Compose
9    /// places children.
10    fn arrange(&self, density: f32, total_size: f32, sizes: &[f32], out_positions: &mut [f32]);
11}
12
13/// Arrangement strategy matching Jetpack Compose's linear arrangements.
14#[derive(Clone, Copy, Debug, PartialEq)]
15pub enum LinearArrangement {
16    /// Place children consecutively starting from the leading edge.
17    Start,
18    /// Place children so the last child touches the trailing edge.
19    End,
20    /// Place children so they are centered as a block.
21    Center,
22    /// Distribute the remaining space evenly between children.
23    SpaceBetween,
24    /// Distribute the remaining space before, after, and between children.
25    SpaceAround,
26    /// Distribute the remaining space before the first child, between children, and after the last child.
27    SpaceEvenly,
28    /// Insert a fixed amount of space between children.
29    SpacedBy(f32),
30    /// Insert a fixed amount of space between children and place the block
31    /// they make at `bias` of the space left over: -1 the start, 0 the
32    /// middle, 1 the end.
33    SpacedByAligned { spacing: f32, bias: f32 },
34}
35
36/// An alignment along one axis, which places a block at a bias of the space
37/// left beside it.
38pub trait AxisAlignment {
39    /// -1 the start, 0 the middle, 1 the end.
40    fn bias(&self) -> f32;
41}
42
43impl AxisAlignment for crate::HorizontalAlignment {
44    fn bias(&self) -> f32 {
45        crate::HorizontalAlignment::bias(self)
46    }
47}
48
49impl AxisAlignment for crate::VerticalAlignment {
50    fn bias(&self) -> f32 {
51        crate::VerticalAlignment::bias(self)
52    }
53}
54
55impl LinearArrangement {
56    /// Creates an arrangement that inserts a fixed spacing between children.
57    pub fn spaced_by(spacing: f32) -> Self {
58        Self::SpacedBy(spacing)
59    }
60
61    /// Compose's `Arrangement.spacedBy(space, alignment)`: `spacing` between
62    /// children, and the block they make placed by `alignment` in the space
63    /// left over, as when the numbers of a table cell keep to its end.
64    pub fn spaced_by_aligned(spacing: f32, alignment: impl AxisAlignment) -> Self {
65        Self::SpacedByAligned {
66            spacing,
67            bias: alignment.bias(),
68        }
69    }
70
71    /// Whether the arrangement puts a fixed spacing between children and
72    /// keeps them within the container on its own when they overflow it.
73    pub fn is_spaced(&self) -> bool {
74        matches!(self, Self::SpacedBy(_) | Self::SpacedByAligned { .. })
75    }
76
77    /// The space a spaced arrangement puts between children, on the device
78    /// pixel grid of `density` as Compose's `roundToPx` puts it; none for the
79    /// others.
80    pub fn spacing(&self, density: f32) -> f32 {
81        match *self {
82            Self::SpacedBy(spacing) | Self::SpacedByAligned { spacing, .. } => {
83                round_to_px(spacing.max(0.0), density)
84            }
85            _ => 0.0,
86        }
87    }
88
89    /// Compose's `placeLeftOrTop` and its `placeCenter`, `placeSpace*` and
90    /// `placeRightOrBottom` kin: each child `gap` after the one before, the
91    /// first at `start`, every position rounded to a device pixel.
92    fn fill_positions(
93        density: f32,
94        start: f32,
95        gap: f32,
96        sizes: &[f32],
97        out_positions: &mut [f32],
98    ) {
99        debug_assert_eq!(sizes.len(), out_positions.len());
100        let mut cursor = start;
101        for (size, position) in sizes.iter().zip(out_positions.iter_mut()) {
102            *position = round_to_px(cursor, density);
103            cursor += size + gap;
104        }
105    }
106
107    /// Compose's `SpacedAligned` without an alignment: each child after the
108    /// one before and `spacing` more, but never past the end of
109    /// `total_size`, and the spacing after it only as wide as what is left.
110    fn spaced_positions(spacing: f32, total_size: f32, sizes: &[f32], out_positions: &mut [f32]) {
111        let mut occupied = 0.0_f32;
112        for (&size, position) in sizes.iter().zip(out_positions.iter_mut()) {
113            *position = occupied.min(total_size - size);
114            let space_after = spacing.min(total_size - *position - size);
115            occupied = *position + size + space_after;
116        }
117    }
118}
119
120impl Arrangement for LinearArrangement {
121    fn arrange(&self, density: f32, total_size: f32, sizes: &[f32], out_positions: &mut [f32]) {
122        debug_assert_eq!(sizes.len(), out_positions.len());
123        if sizes.is_empty() {
124            return;
125        }
126
127        let remaining = total_size - sizes.iter().sum::<f32>();
128        let count = sizes.len() as f32;
129
130        match *self {
131            LinearArrangement::Start => {
132                Self::fill_positions(density, 0.0, 0.0, sizes, out_positions);
133            }
134            LinearArrangement::End => {
135                Self::fill_positions(density, remaining, 0.0, sizes, out_positions);
136            }
137            LinearArrangement::Center => {
138                Self::fill_positions(density, remaining / 2.0, 0.0, sizes, out_positions);
139            }
140            LinearArrangement::SpaceBetween => {
141                let gap = remaining / (count - 1.0).max(1.0);
142                Self::fill_positions(density, 0.0, gap, sizes, out_positions);
143            }
144            LinearArrangement::SpaceAround => {
145                let gap = remaining / count;
146                Self::fill_positions(density, gap / 2.0, gap, sizes, out_positions);
147            }
148            LinearArrangement::SpaceEvenly => {
149                let gap = remaining / (count + 1.0);
150                Self::fill_positions(density, gap, gap, sizes, out_positions);
151            }
152            LinearArrangement::SpacedBy(_) => {
153                Self::spaced_positions(self.spacing(density), total_size, sizes, out_positions);
154            }
155            LinearArrangement::SpacedByAligned { bias, .. } => {
156                Self::spaced_positions(self.spacing(density), total_size, sizes, out_positions);
157                let (Some(&last), Some(&last_size)) = (out_positions.last(), sizes.last()) else {
158                    return;
159                };
160                let occupied = last + last_size;
161                if occupied < total_size {
162                    let shift = bias_offset(bias, total_size, occupied, density);
163                    for position in out_positions.iter_mut() {
164                        *position += shift;
165                    }
166                }
167            }
168        }
169    }
170}
171
172#[cfg(test)]
173#[path = "tests/arrangement_tests.rs"]
174mod tests;