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 {
34        /// Space between two children, in logical pixels.
35        spacing: f32,
36        /// Where the block sits in the space left over, -1 to 1.
37        bias: f32,
38    },
39}
40
41/// An alignment along one axis, which places a block at a bias of the space
42/// left beside it.
43pub trait AxisAlignment {
44    /// -1 the start, 0 the middle, 1 the end.
45    fn bias(&self) -> f32;
46}
47
48impl AxisAlignment for crate::HorizontalAlignment {
49    fn bias(&self) -> f32 {
50        crate::HorizontalAlignment::bias(self)
51    }
52}
53
54impl AxisAlignment for crate::VerticalAlignment {
55    fn bias(&self) -> f32 {
56        crate::VerticalAlignment::bias(self)
57    }
58}
59
60impl LinearArrangement {
61    /// Creates an arrangement that inserts a fixed spacing between children.
62    pub fn spaced_by(spacing: f32) -> Self {
63        Self::SpacedBy(spacing)
64    }
65
66    /// Compose's `Arrangement.spacedBy(space, alignment)`: `spacing` between
67    /// children, and the block they make placed by `alignment` in the space
68    /// left over, as when the numbers of a table cell keep to its end.
69    pub fn spaced_by_aligned(spacing: f32, alignment: impl AxisAlignment) -> Self {
70        Self::SpacedByAligned {
71            spacing,
72            bias: alignment.bias(),
73        }
74    }
75
76    /// Whether the arrangement puts a fixed spacing between children and
77    /// keeps them within the container on its own when they overflow it.
78    pub fn is_spaced(&self) -> bool {
79        matches!(self, Self::SpacedBy(_) | Self::SpacedByAligned { .. })
80    }
81
82    /// The space a spaced arrangement puts between children, on the device
83    /// pixel grid of `density` as Compose's `roundToPx` puts it; none for the
84    /// others.
85    pub fn spacing(&self, density: f32) -> f32 {
86        match *self {
87            Self::SpacedBy(spacing) | Self::SpacedByAligned { spacing, .. } => {
88                round_to_px(spacing.max(0.0), density)
89            }
90            _ => 0.0,
91        }
92    }
93
94    /// Compose's `placeLeftOrTop` and its `placeCenter`, `placeSpace*` and
95    /// `placeRightOrBottom` kin: each child `gap` after the one before, the
96    /// first at `start`, every position rounded to a device pixel.
97    fn fill_positions(
98        density: f32,
99        start: f32,
100        gap: f32,
101        sizes: &[f32],
102        out_positions: &mut [f32],
103    ) {
104        debug_assert_eq!(sizes.len(), out_positions.len());
105        let mut cursor = start;
106        for (size, position) in sizes.iter().zip(out_positions.iter_mut()) {
107            *position = round_to_px(cursor, density);
108            cursor += size + gap;
109        }
110    }
111
112    /// Compose's `SpacedAligned` without an alignment: each child after the
113    /// one before and `spacing` more, but never past the end of
114    /// `total_size`, and the spacing after it only as wide as what is left.
115    fn spaced_positions(spacing: f32, total_size: f32, sizes: &[f32], out_positions: &mut [f32]) {
116        let mut occupied = 0.0_f32;
117        for (&size, position) in sizes.iter().zip(out_positions.iter_mut()) {
118            *position = occupied.min(total_size - size);
119            let space_after = spacing.min(total_size - *position - size);
120            occupied = *position + size + space_after;
121        }
122    }
123}
124
125impl Arrangement for LinearArrangement {
126    fn arrange(&self, density: f32, total_size: f32, sizes: &[f32], out_positions: &mut [f32]) {
127        debug_assert_eq!(sizes.len(), out_positions.len());
128        if sizes.is_empty() {
129            return;
130        }
131
132        let remaining = total_size - sizes.iter().sum::<f32>();
133        let count = sizes.len() as f32;
134
135        match *self {
136            LinearArrangement::Start => {
137                Self::fill_positions(density, 0.0, 0.0, sizes, out_positions);
138            }
139            LinearArrangement::End => {
140                Self::fill_positions(density, remaining, 0.0, sizes, out_positions);
141            }
142            LinearArrangement::Center => {
143                Self::fill_positions(density, remaining / 2.0, 0.0, sizes, out_positions);
144            }
145            LinearArrangement::SpaceBetween => {
146                let gap = remaining / (count - 1.0).max(1.0);
147                Self::fill_positions(density, 0.0, gap, sizes, out_positions);
148            }
149            LinearArrangement::SpaceAround => {
150                let gap = remaining / count;
151                Self::fill_positions(density, gap / 2.0, gap, sizes, out_positions);
152            }
153            LinearArrangement::SpaceEvenly => {
154                let gap = remaining / (count + 1.0);
155                Self::fill_positions(density, gap, gap, sizes, out_positions);
156            }
157            LinearArrangement::SpacedBy(_) => {
158                Self::spaced_positions(self.spacing(density), total_size, sizes, out_positions);
159            }
160            LinearArrangement::SpacedByAligned { bias, .. } => {
161                Self::spaced_positions(self.spacing(density), total_size, sizes, out_positions);
162                let (Some(&last), Some(&last_size)) = (out_positions.last(), sizes.last()) else {
163                    return;
164                };
165                let occupied = last + last_size;
166                if occupied < total_size {
167                    let shift = bias_offset(bias, total_size, occupied, density);
168                    for position in out_positions.iter_mut() {
169                        *position += shift;
170                    }
171                }
172            }
173        }
174    }
175}
176
177#[cfg(test)]
178#[path = "tests/arrangement_tests.rs"]
179mod tests;