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}
127
128impl Align {
129    /// Returns `true` for [`Align::Stretch`].
130    pub const fn is_stretch(self) -> bool {
131        matches!(self, Self::Stretch)
132    }
133
134    /// Returns the cross-axis offset of a `child` inside `avail`.
135    ///
136    /// Stretch and start both produce zero; non-finite space produces zero.
137    pub fn offset(self, avail: f32, child: f32) -> f32 {
138        if !avail.is_finite() {
139            return 0.0;
140        }
141        let slack = (avail - child).max(0.0);
142        match self {
143            Self::Start | Self::Stretch => 0.0,
144            Self::Center => slack * 0.5,
145            Self::End => slack,
146        }
147    }
148
149    /// Uses the child alignment when set, otherwise the container alignment.
150    pub fn resolve(child: Option<Align>, container: Align) -> Align {
151        child.unwrap_or(container)
152    }
153}
154
155/// Layout style of a container that arranges its children on an axis.
156#[derive(Clone, Copy, Debug, PartialEq)]
157pub struct ContainerStyle {
158    /// Main axis along which children are placed.
159    pub axis: Axis,
160    /// Gap between children along the main axis.
161    pub gap: f32,
162    /// Gap between wrapped lines or cross-axis tracks.
163    pub cross_gap: f32,
164    /// Default cross-axis alignment for children.
165    pub align: Align,
166    /// Main-axis distribution of free space.
167    pub justify: Justify,
168    /// Whether children wrap onto multiple lines.
169    pub wrap: bool,
170    /// Cross-axis alignment of wrapped lines within the container.
171    pub align_content: Align,
172}
173
174impl Default for ContainerStyle {
175    fn default() -> Self {
176        Self {
177            axis: Axis::Horizontal,
178            gap: 0.0,
179            cross_gap: 0.0,
180            align: Align::Start,
181            justify: Justify::Start,
182            wrap: false,
183            align_content: Align::Start,
184        }
185    }
186}
187
188impl ContainerStyle {
189    /// Creates a style for `axis` with equal main and cross gaps.
190    pub fn new(axis: Axis, gap: f32) -> Self {
191        let gap = sanitize_gap(gap);
192        Self {
193            axis,
194            gap,
195            cross_gap: gap,
196            ..Self::default()
197        }
198    }
199
200    /// Sets both the main and cross gaps.
201    pub fn gap(mut self, gap: f32) -> Self {
202        let gap = sanitize_gap(gap);
203        self.gap = gap;
204        self.cross_gap = gap;
205        self
206    }
207
208    /// Sets the main-axis gap.
209    pub fn main_gap(mut self, gap: f32) -> Self {
210        self.gap = sanitize_gap(gap);
211        self
212    }
213
214    /// Sets the cross-axis gap.
215    pub fn cross_gap(mut self, gap: f32) -> Self {
216        self.cross_gap = sanitize_gap(gap);
217        self
218    }
219
220    /// Sets the main and cross gaps independently.
221    pub fn gaps(mut self, main: f32, cross: f32) -> Self {
222        self.gap = sanitize_gap(main);
223        self.cross_gap = sanitize_gap(cross);
224        self
225    }
226
227    /// Sets the default cross-axis alignment of children.
228    pub fn align_items(mut self, align: Align) -> Self {
229        self.align = align;
230        self
231    }
232
233    /// Sets the main-axis distribution of free space.
234    pub fn justify_content(mut self, justify: Justify) -> Self {
235        self.justify = justify;
236        self
237    }
238
239    /// Enables or disables wrapping.
240    pub fn wrap(mut self, wrap: bool) -> Self {
241        self.wrap = wrap;
242        self
243    }
244
245    /// Enables wrapping.
246    pub fn wrapped(mut self) -> Self {
247        self.wrap = true;
248        self
249    }
250
251    /// Sets the cross-axis alignment of wrapped lines.
252    pub fn align_content(mut self, align: Align) -> Self {
253        self.align_content = align;
254        self
255    }
256
257    /// Sets the main axis.
258    pub fn axis(mut self, axis: Axis) -> Self {
259        self.axis = axis;
260        self
261    }
262
263    /// Returns the total main-axis gap for `count` children.
264    pub fn main_gaps_total(self, count: usize) -> f32 {
265        if count > 1 {
266            self.gap * (count - 1) as f32
267        } else {
268            0.0
269        }
270    }
271}