1use std::hash::{
2 Hash,
3 Hasher,
4};
5
6use kasuari::{
7 Solver,
8 Variable,
9 WeightedRelation::*,
10};
11
12use super::{
13 Constraint,
14 Direction,
15 Flex,
16 Margin,
17 Rect,
18 Spacing,
19 strengths,
20};
21
22const FLOAT_PRECISION_MULTIPLIER: f64 = 100.0;
23
24macro_rules! try_add {
25 ($expr:expr) => {
26 if $expr.is_err() {
27 return None;
28 }
29 };
30}
31
32#[derive(Debug, Default, Clone, PartialEq, Eq)]
34pub struct Layout {
35 direction: Direction,
36 constraints: Vec<Constraint>,
37 margin: Margin,
38 flex: Flex,
39 spacing: Spacing,
40}
41
42impl Layout {
43 pub fn new<I>(direction: Direction, constraints: I) -> Self
45 where
46 I: IntoIterator,
47 I::Item: Into<Constraint>, {
48 Self {
49 direction,
50 constraints: constraints.into_iter().map(Into::into).collect(),
51 margin: Margin::new(0, 0),
52 flex: Flex::default(),
53 spacing: Spacing::default(),
54 }
55 }
56
57 pub fn vertical<I>(constraints: I) -> Self
59 where
60 I: IntoIterator,
61 I::Item: Into<Constraint>, {
62 Self::new(Direction::Vertical, constraints)
63 }
64
65 pub fn horizontal<I>(constraints: I) -> Self
67 where
68 I: IntoIterator,
69 I::Item: Into<Constraint>, {
70 Self::new(Direction::Horizontal, constraints)
71 }
72
73 pub fn direction(mut self, direction: Direction) -> Self {
75 self.direction = direction;
76 self
77 }
78
79 pub fn constraints<I>(mut self, constraints: I) -> Self
81 where
82 I: IntoIterator,
83 I::Item: Into<Constraint>, {
84 self.constraints = constraints.into_iter().map(Into::into).collect();
85 self
86 }
87
88 pub fn margin(mut self, margin: u16) -> Self {
90 self.margin = Margin::new(margin, margin);
91 self
92 }
93
94 pub fn horizontal_margin(mut self, margin: u16) -> Self {
96 self.margin.horizontal = margin;
97 self
98 }
99
100 pub fn vertical_margin(mut self, margin: u16) -> Self {
102 self.margin.vertical = margin;
103 self
104 }
105
106 pub fn flex(mut self, flex: Flex) -> Self {
108 self.flex = flex;
109 self
110 }
111
112 pub fn spacing<T: Into<Spacing>>(mut self, spacing: T) -> Self {
114 self.spacing = spacing.into();
115 self
116 }
117}
118
119impl Hash for Layout {
120 fn hash<H: Hasher>(&self, state: &mut H) {
121 self.direction.hash(state);
122 self.constraints.hash(state);
123 self.margin.hash(state);
124 self.flex.hash(state);
125 self.spacing.hash(state);
126 }
127}
128
129impl Layout {
130 pub fn split(&self, area: Rect) -> Vec<Rect> {
132 self.try_split(area).unwrap_or_default()
133 }
134
135 pub fn areas<const N: usize>(&self, area: Rect) -> [Rect; N] {
139 let rects = self.split(area);
140 let mut iter = rects.into_iter();
141 [(); N].map(|_| match iter.next() {
142 | Some(r) => r,
143 | None => Rect::ZERO,
144 })
145 }
146
147 fn try_split(&self, area: Rect) -> Option<Vec<Rect>> {
148 let inner = area.inner(self.margin);
149 if inner.is_empty() {
150 return Some(vec![Rect::ZERO; self.constraints.len()]);
151 }
152
153 let mut solver = Solver::new();
154 let segment_count = self.constraints.len();
155 let spacer_count = segment_count.saturating_add(1);
156
157 let segment_vars: Vec<Variable> = (0..segment_count).map(|_| Variable::new()).collect();
158 let spacer_vars: Vec<Variable> = (0..spacer_count).map(|_| Variable::new()).collect();
159
160 let total_size = match self.direction {
161 | Direction::Horizontal => inner.width,
162 | Direction::Vertical => inner.height,
163 };
164 let total = (total_size as f64 * FLOAT_PRECISION_MULTIPLIER) as i64;
165
166 for &var in &segment_vars {
168 try_add!(
169 solver.add_constraint(var | GE(kasuari::Strength::new(strengths::REQUIRED)) | 0.0)
170 );
171 }
172
173 let mut sum_expr = kasuari::Expression::from_constant(0.0);
175 for &var in segment_vars.iter().chain(spacer_vars.iter()) {
176 sum_expr += var;
177 }
178 try_add!(solver.add_constraint(
179 sum_expr | EQ(kasuari::Strength::new(strengths::REQUIRED)) | total as f64,
180 ));
181
182 for (i, constraint) in self.constraints.iter().enumerate() {
184 let var = segment_vars[i];
185 match constraint {
186 | Constraint::Length(n) => {
187 let target = *n as f64 * FLOAT_PRECISION_MULTIPLIER;
188 try_add!(solver.add_constraint(
189 var | EQ(kasuari::Strength::new(strengths::LENGTH_SIZE_EQ)) | target,
190 ));
191 },
192 | Constraint::Percentage(p) => {
193 let target = total as f64 * (*p as f64) / 100.0;
194 try_add!(solver.add_constraint(
195 var | EQ(kasuari::Strength::new(strengths::PERCENTAGE_SIZE_EQ)) | target,
196 ));
197 },
198 | Constraint::Ratio(n, d) => {
199 let target = total as f64 * (*n as f64) / (*d as f64);
200 try_add!(solver.add_constraint(
201 var | EQ(kasuari::Strength::new(strengths::RATIO_SIZE_EQ)) | target,
202 ));
203 },
204 | Constraint::Min(m) => {
205 let target = *m as f64 * FLOAT_PRECISION_MULTIPLIER;
206 try_add!(solver.add_constraint(
207 var | GE(kasuari::Strength::new(strengths::MIN_SIZE_GE)) | target,
208 ));
209 },
210 | Constraint::Max(m) => {
211 let target = *m as f64 * FLOAT_PRECISION_MULTIPLIER;
212 try_add!(solver.add_constraint(
213 var | LE(kasuari::Strength::new(strengths::MAX_SIZE_LE)) | target,
214 ));
215 },
216 | Constraint::Fill(_) => {
217 },
219 }
220 }
221
222 let spacing_value = match self.spacing {
224 | Spacing::Space(v) => v as f64 * FLOAT_PRECISION_MULTIPLIER,
225 | Spacing::Overlap(v) => -(v as f64) * FLOAT_PRECISION_MULTIPLIER,
226 };
227
228 match self.flex {
229 | Flex::Legacy => {
230 for &var in &spacer_vars {
232 try_add!(solver.add_constraint(
233 var | EQ(kasuari::Strength::new(strengths::REQUIRED)) | 0.0
234 ));
235 }
236 },
237 | Flex::Start => {
238 if let Some(&first) = spacer_vars.first() {
239 try_add!(solver.add_constraint(
240 first | EQ(kasuari::Strength::new(strengths::REQUIRED)) | 0.0,
241 ));
242 }
243 for &var in spacer_vars
244 .iter()
245 .skip(1)
246 .take(spacer_count.saturating_sub(2))
247 {
248 try_add!(solver.add_constraint(
249 var | EQ(kasuari::Strength::new(strengths::SPACER_SIZE_EQ)) | spacing_value,
250 ));
251 }
252 if let Some(&last) = spacer_vars.last() {
253 try_add!(solver.add_constraint(
254 last | GE(kasuari::Strength::new(strengths::REQUIRED)) | 0.0,
255 ));
256 }
257 },
258 | Flex::End => {
259 if let Some(&last) = spacer_vars.last() {
260 try_add!(solver.add_constraint(
261 last | EQ(kasuari::Strength::new(strengths::REQUIRED)) | 0.0,
262 ));
263 }
264 for &var in spacer_vars
265 .iter()
266 .skip(1)
267 .take(spacer_count.saturating_sub(2))
268 {
269 try_add!(solver.add_constraint(
270 var | EQ(kasuari::Strength::new(strengths::SPACER_SIZE_EQ)) | spacing_value,
271 ));
272 }
273 if let Some(&first) = spacer_vars.first() {
274 try_add!(solver.add_constraint(
275 first | GE(kasuari::Strength::new(strengths::REQUIRED)) | 0.0,
276 ));
277 }
278 },
279 | Flex::Center => {
280 for &var in spacer_vars
281 .iter()
282 .skip(1)
283 .take(spacer_count.saturating_sub(2))
284 {
285 try_add!(solver.add_constraint(
286 var | EQ(kasuari::Strength::new(strengths::SPACER_SIZE_EQ)) | spacing_value,
287 ));
288 }
289 if spacer_count >= 2 {
290 let first = spacer_vars[0];
291 let last = spacer_vars[spacer_count - 1];
292 try_add!(solver.add_constraint(
293 (first - last) | EQ(kasuari::Strength::new(strengths::REQUIRED)) | 0.0,
294 ));
295 }
296 },
297 | Flex::SpaceBetween => {
298 if let Some(&first) = spacer_vars.first() {
299 try_add!(solver.add_constraint(
300 first | EQ(kasuari::Strength::new(strengths::REQUIRED)) | 0.0,
301 ));
302 }
303 if let Some(&last) = spacer_vars.last() {
304 try_add!(solver.add_constraint(
305 last | EQ(kasuari::Strength::new(strengths::REQUIRED)) | 0.0,
306 ));
307 }
308 if spacer_count >= 3 {
309 let first_internal = spacer_vars[1];
310 for &var in spacer_vars.iter().skip(2).take(spacer_count - 3) {
311 try_add!(solver.add_constraint(
312 (var - first_internal) |
313 EQ(kasuari::Strength::new(strengths::SPACER_SIZE_EQ)) |
314 0.0,
315 ));
316 }
317 }
318 },
319 | Flex::SpaceAround => {
320 if spacer_count >= 3 {
321 let first = spacer_vars[0];
322 let last = spacer_vars[spacer_count - 1];
323 let first_internal = spacer_vars[1];
324 try_add!(solver.add_constraint(
325 (first * 2.0 - first_internal) |
326 EQ(kasuari::Strength::new(strengths::SPACER_SIZE_EQ)) |
327 0.0,
328 ));
329 try_add!(solver.add_constraint(
330 (last * 2.0 - first_internal) |
331 EQ(kasuari::Strength::new(strengths::SPACER_SIZE_EQ)) |
332 0.0,
333 ));
334 for &var in spacer_vars.iter().skip(2).take(spacer_count - 3) {
335 try_add!(solver.add_constraint(
336 (var - first_internal) |
337 EQ(kasuari::Strength::new(strengths::SPACER_SIZE_EQ)) |
338 0.0,
339 ));
340 }
341 }
342 },
343 | Flex::SpaceEvenly => {
344 if spacer_count >= 2 {
345 let first = spacer_vars[0];
346 for &var in spacer_vars.iter().skip(1) {
347 try_add!(solver.add_constraint(
348 (var - first) |
349 EQ(kasuari::Strength::new(strengths::SPACER_SIZE_EQ)) |
350 0.0,
351 ));
352 }
353 }
354 },
355 }
356
357 for (i, constraint) in self.constraints.iter().enumerate() {
361 let var = segment_vars[i];
362 match constraint {
363 | Constraint::Fill(priority) => {
364 let strength =
365 kasuari::Strength::new(strengths::FILL_GROW * (*priority as f64));
366 try_add!(solver.add_constraint(var | EQ(strength) | total as f64));
367 },
368 | Constraint::Min(_) => {
369 try_add!(solver.add_constraint(
370 var | EQ(kasuari::Strength::new(strengths::GROW)) | total as f64,
371 ));
372 },
373 | _ => {},
374 }
375 }
376
377 if self.flex != Flex::Legacy {
379 for &var in &segment_vars {
380 try_add!(solver.add_constraint(
381 var | EQ(kasuari::Strength::new(strengths::ALL_SEGMENT_GROW)) | total as f64,
382 ));
383 }
384 }
385
386 solver.fetch_changes();
388
389 let mut rects = Vec::with_capacity(segment_count);
390 let mut current: u16 = 0;
391
392 for i in 0..segment_count {
393 let spacer =
394 (solver.get_value(spacer_vars[i]) / FLOAT_PRECISION_MULTIPLIER).round() as u16;
395 current = current.saturating_add(spacer);
396
397 let size =
398 (solver.get_value(segment_vars[i]) / FLOAT_PRECISION_MULTIPLIER).round() as u16;
399
400 let rect = match self.direction {
401 | Direction::Horizontal => {
402 Rect::new(inner.x.saturating_add(current), inner.y, size, inner.height)
403 },
404 | Direction::Vertical => {
405 Rect::new(inner.x, inner.y.saturating_add(current), inner.width, size)
406 },
407 };
408 rects.push(rect);
409
410 current = current.saturating_add(size);
411 }
412
413 Some(rects)
414 }
415}
416
417#[cfg(test)]
418mod tests {
419 use super::*;
420
421 #[test]
422 fn layout_vertical_split_length() {
423 let layout = Layout::vertical([Constraint::Length(5), Constraint::Length(5)]);
424 let rects = layout.split(Rect::new(0, 0, 10, 10));
425 assert_eq!(rects.len(), 2);
426 assert_eq!(rects[0].height, 5);
427 assert_eq!(rects[1].height, 5);
428 }
429
430 #[test]
431 fn layout_horizontal_split_length() {
432 let layout = Layout::horizontal([Constraint::Length(5), Constraint::Length(5)]);
433 let rects = layout.split(Rect::new(0, 0, 10, 10));
434 assert_eq!(rects.len(), 2);
435 assert_eq!(rects[0].width, 5);
436 assert_eq!(rects[1].width, 5);
437 }
438
439 #[test]
440 fn layout_split_with_margin() {
441 let layout = Layout::vertical([Constraint::Length(5), Constraint::Length(5)]).margin(1);
442 let rects = layout.split(Rect::new(0, 0, 10, 10));
443 assert_eq!(rects.len(), 2);
444 assert_eq!(rects[0].y, 1);
445 assert_eq!(rects[0].width, 8);
446 }
447
448 #[test]
449 fn layout_split_empty_area() {
450 let layout = Layout::vertical([Constraint::Length(5)]);
451 let rects = layout.split(Rect::ZERO);
452 assert_eq!(rects.len(), 1);
453 assert_eq!(rects[0], Rect::ZERO);
454 }
455
456 #[test]
457 fn layout_builder_api() {
458 let layout = Layout::default()
459 .direction(Direction::Horizontal)
460 .constraints([Constraint::Length(10)])
461 .margin(2)
462 .flex(Flex::Center)
463 .spacing(1);
464 assert_eq!(layout.direction, Direction::Horizontal);
465 assert_eq!(layout.constraints, vec![Constraint::Length(10)]);
466 assert_eq!(layout.margin, Margin::new(2, 2));
467 assert_eq!(layout.flex, Flex::Center);
468 assert_eq!(layout.spacing, Spacing::Space(1));
469 }
470
471 #[test]
472 fn layout_areas_const_generic() {
473 let layout = Layout::vertical([Constraint::Length(5), Constraint::Length(5)]);
474 let areas: [Rect; 2] = layout.areas(Rect::new(0, 0, 10, 10));
475 assert_eq!(areas[0].height, 5);
476 assert_eq!(areas[1].height, 5);
477 }
478}