Skip to main content

sequoia_layout/
container.rs

1//! Container axes, alignment, justification, and style builders.
2
3use crate::dock::sanitize_gap;
4
5/// Main layout axis of a container.
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7pub enum Axis {
8    /// Children are placed left to right.
9    Horizontal,
10    /// Children are placed top to bottom.
11    Vertical,
12}
13
14impl Axis {
15    /// Returns `true` for [`Axis::Horizontal`].
16    pub const fn is_horizontal(self) -> bool {
17        matches!(self, Self::Horizontal)
18    }
19
20    /// Returns `true` for [`Axis::Vertical`].
21    pub const fn is_vertical(self) -> bool {
22        matches!(self, Self::Vertical)
23    }
24
25    /// Splits `width`/`height` into main and cross sizes for this axis.
26    pub fn split_main_cross(self, width: f32, height: f32) -> (f32, f32) {
27        match self {
28            Self::Horizontal => (width, height),
29            Self::Vertical => (height, width),
30        }
31    }
32
33    /// Combines main and cross sizes back into `width`/`height`.
34    pub fn merge_main_cross(self, main: f32, cross: f32) -> (f32, f32) {
35        match self {
36            Self::Horizontal => (main, cross),
37            Self::Vertical => (cross, main),
38        }
39    }
40}
41
42/// Distribution of free space along a container's main axis.
43#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
44#[non_exhaustive]
45pub enum Justify {
46    /// Pack items at the start of the line.
47    #[default]
48    Start,
49    /// Center items as a group.
50    Center,
51    /// Pack items at the end of the line.
52    End,
53    /// Spread items so the first and last touch the edges.
54    SpaceBetween,
55    /// Give every item equal space on both sides.
56    SpaceAround,
57    /// Give every item equal space, including the outer edges.
58    SpaceEvenly,
59}
60
61impl Justify {
62    /// Computes the leading offset and the gap added between items.
63    ///
64    /// `extra` is the free space to distribute and is clamped to zero for
65    /// invalid values.
66    pub fn distribute(self, count: usize, _base_gap: f32, extra: f32) -> (f32, f32) {
67        let extra = if extra.is_finite() {
68            extra.max(0.0)
69        } else {
70            0.0
71        };
72        match self {
73            Self::Start => (0.0, 0.0),
74            Self::Center => (extra * 0.5, 0.0),
75            Self::End => (extra, 0.0),
76            Self::SpaceBetween => {
77                if count > 1 {
78                    (0.0, extra / (count - 1) as f32)
79                } else if count == 1 {
80                    (extra * 0.5, 0.0)
81                } else {
82                    (0.0, 0.0)
83                }
84            }
85            Self::SpaceAround => {
86                if count > 0 {
87                    let step = extra / count as f32;
88                    (step * 0.5, step)
89                } else {
90                    (0.0, 0.0)
91                }
92            }
93            Self::SpaceEvenly => {
94                if count > 0 {
95                    let step = extra / (count + 1) as f32;
96                    (step, step)
97                } else {
98                    (0.0, 0.0)
99                }
100            }
101        }
102    }
103
104    /// Returns only the leading offset produced by [`Justify::distribute`].
105    pub fn start_offset(self, extra: f32) -> f32 {
106        let (start, _) = self.distribute(0, 0.0, extra);
107        match self {
108            Self::Start | Self::Center | Self::End => start,
109            _ => start,
110        }
111    }
112}
113
114/// Alignment of children on a container's cross axis.
115#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
116pub enum Align {
117    /// Align children at the start of the cross axis.
118    #[default]
119    Start,
120    /// Center children on the cross axis.
121    Center,
122    /// Align children at the end of the cross axis.
123    End,
124    /// Stretch children to fill the cross axis.
125    Stretch,
126    /// Align children on their first text baseline (horizontal rows only).
127    Baseline,
128}
129
130impl Align {
131    /// Returns `true` for [`Align::Stretch`].
132    pub const fn is_stretch(self) -> bool {
133        matches!(self, Self::Stretch)
134    }
135
136    /// Returns the cross-axis offset of a `child` inside `avail`.
137    ///
138    /// Stretch and start both produce zero; non-finite space produces zero.
139    pub fn offset(self, avail: f32, child: f32) -> f32 {
140        if !avail.is_finite() {
141            return 0.0;
142        }
143        let slack = (avail - child).max(0.0);
144        match self {
145            Self::Start | Self::Stretch | Self::Baseline => 0.0,
146            Self::Center => slack * 0.5,
147            Self::End => slack,
148        }
149    }
150
151    /// Uses the child alignment when set, otherwise the container alignment.
152    pub fn resolve(child: Option<Align>, container: Align) -> Align {
153        child.unwrap_or(container)
154    }
155}
156
157/// Layout style of a container that arranges its children on an axis.
158#[derive(Clone, Copy, Debug, PartialEq)]
159pub struct ContainerStyle {
160    /// Main axis along which children are placed.
161    pub axis: Axis,
162    /// Gap between children along the main axis.
163    pub gap: f32,
164    /// Gap between wrapped lines or cross-axis tracks.
165    pub cross_gap: f32,
166    /// Default cross-axis alignment for children.
167    pub align: Align,
168    /// Main-axis distribution of free space.
169    pub justify: Justify,
170    /// Whether children wrap onto multiple lines.
171    pub wrap: bool,
172    /// Cross-axis alignment of wrapped lines within the container.
173    pub align_content: Align,
174}
175
176impl Default for ContainerStyle {
177    fn default() -> Self {
178        Self {
179            axis: Axis::Horizontal,
180            gap: 0.0,
181            cross_gap: 0.0,
182            align: Align::Start,
183            justify: Justify::Start,
184            wrap: false,
185            align_content: Align::Start,
186        }
187    }
188}
189
190impl ContainerStyle {
191    /// Creates a style for `axis` with equal main and cross gaps.
192    pub fn new(axis: Axis, gap: f32) -> Self {
193        let gap = sanitize_gap(gap);
194        Self {
195            axis,
196            gap,
197            cross_gap: gap,
198            ..Self::default()
199        }
200    }
201
202    /// Sets both the main and cross gaps.
203    pub fn gap(mut self, gap: f32) -> Self {
204        let gap = sanitize_gap(gap);
205        self.gap = gap;
206        self.cross_gap = gap;
207        self
208    }
209
210    /// Sets the main-axis gap.
211    pub fn main_gap(mut self, gap: f32) -> Self {
212        self.gap = sanitize_gap(gap);
213        self
214    }
215
216    /// Sets the cross-axis gap.
217    pub fn cross_gap(mut self, gap: f32) -> Self {
218        self.cross_gap = sanitize_gap(gap);
219        self
220    }
221
222    /// Sets the main and cross gaps independently.
223    pub fn gaps(mut self, main: f32, cross: f32) -> Self {
224        self.gap = sanitize_gap(main);
225        self.cross_gap = sanitize_gap(cross);
226        self
227    }
228
229    /// Sets the default cross-axis alignment of children.
230    pub fn align_items(mut self, align: Align) -> Self {
231        self.align = align;
232        self
233    }
234
235    /// Sets the main-axis distribution of free space.
236    pub fn justify_content(mut self, justify: Justify) -> Self {
237        self.justify = justify;
238        self
239    }
240
241    /// Enables or disables wrapping.
242    pub fn wrap(mut self, wrap: bool) -> Self {
243        self.wrap = wrap;
244        self
245    }
246
247    /// Enables wrapping.
248    pub fn wrapped(mut self) -> Self {
249        self.wrap = true;
250        self
251    }
252
253    /// Sets the cross-axis alignment of wrapped lines.
254    pub fn align_content(mut self, align: Align) -> Self {
255        self.align_content = align;
256        self
257    }
258
259    /// Sets the main axis.
260    pub fn axis(mut self, axis: Axis) -> Self {
261        self.axis = axis;
262        self
263    }
264
265    /// Returns the total main-axis gap for `count` children.
266    pub fn main_gaps_total(self, count: usize) -> f32 {
267        if count > 1 {
268            self.gap * (count - 1) as f32
269        } else {
270            0.0
271        }
272    }
273}