1#![forbid(unsafe_code)]
10
11use stipple_geometry::Size;
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum Axis {
16 Horizontal,
17 Vertical,
18}
19
20impl Axis {
21 #[inline]
23 pub fn main(self, size: Size) -> f64 {
24 match self {
25 Axis::Horizontal => size.width,
26 Axis::Vertical => size.height,
27 }
28 }
29
30 #[inline]
32 pub fn cross(self, size: Size) -> f64 {
33 match self {
34 Axis::Horizontal => size.height,
35 Axis::Vertical => size.width,
36 }
37 }
38
39 #[inline]
41 pub fn size(self, main: f64, cross: f64) -> Size {
42 match self {
43 Axis::Horizontal => Size::new(main, cross),
44 Axis::Vertical => Size::new(cross, main),
45 }
46 }
47}
48
49#[derive(Clone, Copy, Debug, PartialEq)]
52pub struct Constraints {
53 pub min: Size,
54 pub max: Size,
55}
56
57impl Constraints {
58 #[inline]
60 pub fn loose(max: Size) -> Self {
61 Self {
62 min: Size::ZERO,
63 max,
64 }
65 }
66
67 #[inline]
69 pub fn tight(size: Size) -> Self {
70 Self {
71 min: size,
72 max: size,
73 }
74 }
75
76 #[inline]
78 pub fn constrain(&self, size: Size) -> Size {
79 size.clamp(self.min, self.max)
80 }
81
82 pub fn deflate(&self, horizontal: f64, vertical: f64) -> Self {
85 let max = Size::new(
86 (self.max.width - horizontal).max(0.0),
87 (self.max.height - vertical).max(0.0),
88 );
89 Self {
90 min: self.min.clamp(Size::ZERO, max),
91 max,
92 }
93 }
94}
95
96#[derive(Clone, Copy, Debug, PartialEq)]
98pub struct FlexItem {
99 pub basis: f64,
101 pub grow: f64,
103}
104
105impl FlexItem {
106 #[inline]
108 pub fn fixed(basis: f64) -> Self {
109 Self { basis, grow: 0.0 }
110 }
111
112 #[inline]
114 pub fn flex(grow: f64) -> Self {
115 Self { basis: 0.0, grow }
116 }
117}
118
119#[derive(Clone, Copy, Debug, PartialEq)]
121pub struct Span {
122 pub offset: f64,
124 pub length: f64,
126}
127
128pub fn solve_main_axis(available: f64, gap: f64, items: &[FlexItem]) -> Vec<Span> {
133 if items.is_empty() {
134 return Vec::new();
135 }
136 let total_gap = gap * (items.len() as f64 - 1.0);
137 let total_basis: f64 = items.iter().map(|i| i.basis).sum();
138 let total_grow: f64 = items.iter().map(|i| i.grow).sum();
139 let free = (available - total_basis - total_gap).max(0.0);
140
141 let mut spans = Vec::with_capacity(items.len());
142 let mut cursor = 0.0;
143 for item in items {
144 let extra = if total_grow > 0.0 {
145 free * (item.grow / total_grow)
146 } else {
147 0.0
148 };
149 let length = item.basis + extra;
150 spans.push(Span {
151 offset: cursor,
152 length,
153 });
154 cursor += length + gap;
155 }
156 spans
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn fixed_items_pack_with_gap() {
165 let spans = solve_main_axis(200.0, 10.0, &[FlexItem::fixed(40.0), FlexItem::fixed(60.0)]);
166 assert_eq!(
167 spans[0],
168 Span {
169 offset: 0.0,
170 length: 40.0
171 }
172 );
173 assert_eq!(
174 spans[1],
175 Span {
176 offset: 50.0,
177 length: 60.0
178 }
179 );
180 }
181
182 #[test]
183 fn grow_distributes_free_space() {
184 let spans = solve_main_axis(
187 200.0,
188 20.0,
189 &[
190 FlexItem::fixed(40.0),
191 FlexItem::flex(1.0),
192 FlexItem::flex(1.0),
193 ],
194 );
195 assert_eq!(spans[0].length, 40.0);
196 assert_eq!(spans[1].length, 60.0);
197 assert_eq!(spans[2].length, 60.0);
198 assert_eq!(spans[1].offset, 60.0);
200 assert_eq!(spans[2].offset, 140.0);
201 }
202
203 #[test]
204 fn overflow_clamps_free_to_zero() {
205 let spans = solve_main_axis(30.0, 0.0, &[FlexItem::fixed(40.0), FlexItem::flex(1.0)]);
207 assert_eq!(spans[1].length, 0.0);
208 }
209
210 #[test]
211 fn axis_main_cross_roundtrip() {
212 let s = Axis::Vertical.size(10.0, 4.0);
213 assert_eq!(s, Size::new(4.0, 10.0));
214 assert_eq!(Axis::Vertical.main(s), 10.0);
215 assert_eq!(Axis::Vertical.cross(s), 4.0);
216 }
217}