Skip to main content

photon_ui/layout/
layout.rs

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
24/// A layout configuration that splits a [`Rect`] into sub-rects.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct Layout {
27    direction: Direction,
28    constraints: Vec<Constraint>,
29    margin: Margin,
30    flex: Flex,
31    spacing: Spacing,
32}
33
34impl Layout {
35    /// Create a new layout with the given direction and constraints.
36    pub fn new<I>(direction: Direction, constraints: I) -> Self
37    where
38        I: IntoIterator,
39        I::Item: Into<Constraint>, {
40        Self {
41            direction,
42            constraints: constraints.into_iter().map(Into::into).collect(),
43            margin: Margin::new(0, 0),
44            flex: Flex::default(),
45            spacing: Spacing::default(),
46        }
47    }
48
49    /// Shorthand for [`Layout::new`] with [`Direction::Vertical`].
50    pub fn vertical<I>(constraints: I) -> Self
51    where
52        I: IntoIterator,
53        I::Item: Into<Constraint>, {
54        Self::new(Direction::Vertical, constraints)
55    }
56
57    /// Shorthand for [`Layout::new`] with [`Direction::Horizontal`].
58    pub fn horizontal<I>(constraints: I) -> Self
59    where
60        I: IntoIterator,
61        I::Item: Into<Constraint>, {
62        Self::new(Direction::Horizontal, constraints)
63    }
64
65    /// Set the layout direction.
66    pub fn direction(mut self, direction: Direction) -> Self {
67        self.direction = direction;
68        self
69    }
70
71    /// Replace the current constraints.
72    pub fn constraints<I>(mut self, constraints: I) -> Self
73    where
74        I: IntoIterator,
75        I::Item: Into<Constraint>, {
76        self.constraints = constraints.into_iter().map(Into::into).collect();
77        self
78    }
79
80    /// Set uniform margin on all sides.
81    pub fn margin(mut self, margin: u16) -> Self {
82        self.margin = Margin::new(margin, margin);
83        self
84    }
85
86    /// Set horizontal margin (left and right).
87    pub fn horizontal_margin(mut self, margin: u16) -> Self {
88        self.margin.horizontal = margin;
89        self
90    }
91
92    /// Set vertical margin (top and bottom).
93    pub fn vertical_margin(mut self, margin: u16) -> Self {
94        self.margin.vertical = margin;
95        self
96    }
97
98    /// Set how excess space is distributed.
99    pub fn flex(mut self, flex: Flex) -> Self {
100        self.flex = flex;
101        self
102    }
103
104    /// Set the spacing between layout segments.
105    pub fn spacing<T: Into<Spacing>>(mut self, spacing: T) -> Self {
106        self.spacing = spacing.into();
107        self
108    }
109}
110
111impl Default for Layout {
112    fn default() -> Self {
113        Self {
114            direction: Direction::default(),
115            constraints: Vec::new(),
116            margin: Margin::default(),
117            flex: Flex::default(),
118            spacing: Spacing::default(),
119        }
120    }
121}
122
123impl Hash for Layout {
124    fn hash<H: Hasher>(&self, state: &mut H) {
125        self.direction.hash(state);
126        self.constraints.hash(state);
127        self.margin.hash(state);
128        self.flex.hash(state);
129        self.spacing.hash(state);
130    }
131}
132
133impl Layout {
134    /// Split `area` into sub-rects according to this layout's constraints.
135    pub fn split(&self, area: Rect) -> Vec<Rect> {
136        self.try_split(area).unwrap_or_default()
137    }
138
139    /// Like [`split`](Layout::split), but returns a fixed-size array.
140    ///
141    /// Missing segments are filled with [`Rect::ZERO`].
142    pub fn areas<const N: usize>(&self, area: Rect) -> [Rect; N] {
143        let rects = self.split(area);
144        let mut iter = rects.into_iter();
145        [(); N].map(|_| match iter.next() {
146            | Some(r) => r,
147            | None => Rect::ZERO,
148        })
149    }
150
151    fn try_split(&self, area: Rect) -> Option<Vec<Rect>> {
152        let inner = area.inner(self.margin);
153        if inner.is_empty() {
154            return Some(vec![Rect::ZERO; self.constraints.len()]);
155        }
156
157        let mut solver = Solver::new();
158        let segment_count = self.constraints.len();
159        let spacer_count = segment_count.saturating_add(1);
160
161        let segment_vars: Vec<Variable> = (0..segment_count).map(|_| Variable::new()).collect();
162        let spacer_vars: Vec<Variable> = (0..spacer_count).map(|_| Variable::new()).collect();
163
164        let total_size = match self.direction {
165            | Direction::Horizontal => inner.width,
166            | Direction::Vertical => inner.height,
167        };
168        let total = (total_size as f64 * FLOAT_PRECISION_MULTIPLIER) as i64;
169
170        // All segment variables must be non-negative.
171        for &var in &segment_vars {
172            solver
173                .add_constraint(var | GE(kasuari::Strength::new(strengths::REQUIRED)) | 0.0)
174                .ok()?;
175        }
176
177        // Sum of all segments and spacers equals the total available space.
178        let mut sum_expr = kasuari::Expression::from_constant(0.0);
179        for &var in segment_vars.iter().chain(spacer_vars.iter()) {
180            sum_expr += var;
181        }
182        solver
183            .add_constraint(
184                sum_expr | EQ(kasuari::Strength::new(strengths::REQUIRED)) | total as f64,
185            )
186            .ok()?;
187
188        // Apply per-segment constraints.
189        for (i, constraint) in self.constraints.iter().enumerate() {
190            let var = segment_vars[i];
191            match constraint {
192                | Constraint::Length(n) => {
193                    let target = *n as f64 * FLOAT_PRECISION_MULTIPLIER;
194                    solver
195                        .add_constraint(
196                            var | EQ(kasuari::Strength::new(strengths::LENGTH_SIZE_EQ)) | target,
197                        )
198                        .ok()?;
199                },
200                | Constraint::Percentage(p) => {
201                    let target = total as f64 * (*p as f64) / 100.0;
202                    solver
203                        .add_constraint(
204                            var | EQ(kasuari::Strength::new(strengths::PERCENTAGE_SIZE_EQ)) |
205                                target,
206                        )
207                        .ok()?;
208                },
209                | Constraint::Ratio(n, d) => {
210                    let target = total as f64 * (*n as f64) / (*d as f64);
211                    solver
212                        .add_constraint(
213                            var | EQ(kasuari::Strength::new(strengths::RATIO_SIZE_EQ)) | target,
214                        )
215                        .ok()?;
216                },
217                | Constraint::Min(m) => {
218                    let target = *m as f64 * FLOAT_PRECISION_MULTIPLIER;
219                    solver
220                        .add_constraint(
221                            var | GE(kasuari::Strength::new(strengths::MIN_SIZE_GE)) | target,
222                        )
223                        .ok()?;
224                },
225                | Constraint::Max(m) => {
226                    let target = *m as f64 * FLOAT_PRECISION_MULTIPLIER;
227                    solver
228                        .add_constraint(
229                            var | LE(kasuari::Strength::new(strengths::MAX_SIZE_LE)) | target,
230                        )
231                        .ok()?;
232                },
233                | Constraint::Fill(_) => {
234                    // Fill is handled by the flex/grow constraints below.
235                },
236            }
237        }
238
239        // Configure spacers based on flex and spacing.
240        let spacing_value = match self.spacing {
241            | Spacing::Space(v) => v as f64 * FLOAT_PRECISION_MULTIPLIER,
242            | Spacing::Overlap(v) => -(v as f64) * FLOAT_PRECISION_MULTIPLIER,
243        };
244
245        match self.flex {
246            | Flex::Legacy => {
247                // In legacy mode all spacers are zero; excess space stays in segments.
248                for &var in &spacer_vars {
249                    solver
250                        .add_constraint(var | EQ(kasuari::Strength::new(strengths::REQUIRED)) | 0.0)
251                        .ok()?;
252                }
253            },
254            | Flex::Start => {
255                if let Some(&first) = spacer_vars.first() {
256                    solver
257                        .add_constraint(
258                            first | EQ(kasuari::Strength::new(strengths::REQUIRED)) | 0.0,
259                        )
260                        .ok()?;
261                }
262                for &var in spacer_vars
263                    .iter()
264                    .skip(1)
265                    .take(spacer_count.saturating_sub(2))
266                {
267                    solver
268                        .add_constraint(
269                            var | EQ(kasuari::Strength::new(strengths::SPACER_SIZE_EQ)) |
270                                spacing_value,
271                        )
272                        .ok()?;
273                }
274                if let Some(&last) = spacer_vars.last() {
275                    solver
276                        .add_constraint(
277                            last | GE(kasuari::Strength::new(strengths::REQUIRED)) | 0.0,
278                        )
279                        .ok()?;
280                }
281            },
282            | Flex::End => {
283                if let Some(&last) = spacer_vars.last() {
284                    solver
285                        .add_constraint(
286                            last | EQ(kasuari::Strength::new(strengths::REQUIRED)) | 0.0,
287                        )
288                        .ok()?;
289                }
290                for &var in spacer_vars
291                    .iter()
292                    .skip(1)
293                    .take(spacer_count.saturating_sub(2))
294                {
295                    solver
296                        .add_constraint(
297                            var | EQ(kasuari::Strength::new(strengths::SPACER_SIZE_EQ)) |
298                                spacing_value,
299                        )
300                        .ok()?;
301                }
302                if let Some(&first) = spacer_vars.first() {
303                    solver
304                        .add_constraint(
305                            first | GE(kasuari::Strength::new(strengths::REQUIRED)) | 0.0,
306                        )
307                        .ok()?;
308                }
309            },
310            | Flex::Center => {
311                for &var in spacer_vars
312                    .iter()
313                    .skip(1)
314                    .take(spacer_count.saturating_sub(2))
315                {
316                    solver
317                        .add_constraint(
318                            var | EQ(kasuari::Strength::new(strengths::SPACER_SIZE_EQ)) |
319                                spacing_value,
320                        )
321                        .ok()?;
322                }
323                if spacer_count >= 2 {
324                    let first = spacer_vars[0];
325                    let last = spacer_vars[spacer_count - 1];
326                    solver
327                        .add_constraint(
328                            (first - last) | EQ(kasuari::Strength::new(strengths::REQUIRED)) | 0.0,
329                        )
330                        .ok()?;
331                }
332            },
333            | Flex::SpaceBetween => {
334                if let Some(&first) = spacer_vars.first() {
335                    solver
336                        .add_constraint(
337                            first | EQ(kasuari::Strength::new(strengths::REQUIRED)) | 0.0,
338                        )
339                        .ok()?;
340                }
341                if let Some(&last) = spacer_vars.last() {
342                    solver
343                        .add_constraint(
344                            last | EQ(kasuari::Strength::new(strengths::REQUIRED)) | 0.0,
345                        )
346                        .ok()?;
347                }
348                if spacer_count >= 3 {
349                    let first_internal = spacer_vars[1];
350                    for &var in spacer_vars.iter().skip(2).take(spacer_count - 3) {
351                        solver
352                            .add_constraint(
353                                (var - first_internal) |
354                                    EQ(kasuari::Strength::new(strengths::SPACER_SIZE_EQ)) |
355                                    0.0,
356                            )
357                            .ok()?;
358                    }
359                }
360            },
361            | Flex::SpaceAround => {
362                if spacer_count >= 3 {
363                    let first = spacer_vars[0];
364                    let last = spacer_vars[spacer_count - 1];
365                    let first_internal = spacer_vars[1];
366                    solver
367                        .add_constraint(
368                            (first * 2.0 - first_internal) |
369                                EQ(kasuari::Strength::new(strengths::SPACER_SIZE_EQ)) |
370                                0.0,
371                        )
372                        .ok()?;
373                    solver
374                        .add_constraint(
375                            (last * 2.0 - first_internal) |
376                                EQ(kasuari::Strength::new(strengths::SPACER_SIZE_EQ)) |
377                                0.0,
378                        )
379                        .ok()?;
380                    for &var in spacer_vars.iter().skip(2).take(spacer_count - 3) {
381                        solver
382                            .add_constraint(
383                                (var - first_internal) |
384                                    EQ(kasuari::Strength::new(strengths::SPACER_SIZE_EQ)) |
385                                    0.0,
386                            )
387                            .ok()?;
388                    }
389                }
390            },
391            | Flex::SpaceEvenly => {
392                if spacer_count >= 2 {
393                    let first = spacer_vars[0];
394                    for &var in spacer_vars.iter().skip(1) {
395                        solver
396                            .add_constraint(
397                                (var - first) |
398                                    EQ(kasuari::Strength::new(strengths::SPACER_SIZE_EQ)) |
399                                    0.0,
400                            )
401                            .ok()?;
402                    }
403                }
404            },
405        }
406
407        // Grow constraints for Fill and Min segments.
408        // Weak EQ(total) pushes Cassowary to expand these segments to fill available
409        // space.
410        for (i, constraint) in self.constraints.iter().enumerate() {
411            let var = segment_vars[i];
412            match constraint {
413                | Constraint::Fill(priority) => {
414                    let strength =
415                        kasuari::Strength::new(strengths::FILL_GROW * (*priority as f64));
416                    solver
417                        .add_constraint(var | EQ(strength) | total as f64)
418                        .ok()?;
419                },
420                | Constraint::Min(_) => {
421                    solver
422                        .add_constraint(
423                            var | EQ(kasuari::Strength::new(strengths::GROW)) | total as f64,
424                        )
425                        .ok()?;
426                },
427                | _ => {},
428            }
429        }
430
431        // Weak grow for all segments so non-fixed ones can expand.
432        if self.flex != Flex::Legacy {
433            for &var in &segment_vars {
434                solver
435                    .add_constraint(
436                        var | EQ(kasuari::Strength::new(strengths::ALL_SEGMENT_GROW)) |
437                            total as f64,
438                    )
439                    .ok()?;
440            }
441        }
442
443        // Solve and extract values.
444        solver.fetch_changes();
445
446        let mut rects = Vec::with_capacity(segment_count);
447        let mut current: u16 = 0;
448
449        for i in 0..segment_count {
450            let spacer =
451                (solver.get_value(spacer_vars[i]) / FLOAT_PRECISION_MULTIPLIER).round() as u16;
452            current = current.saturating_add(spacer);
453
454            let size =
455                (solver.get_value(segment_vars[i]) / FLOAT_PRECISION_MULTIPLIER).round() as u16;
456
457            let rect = match self.direction {
458                | Direction::Horizontal => {
459                    Rect::new(inner.x.saturating_add(current), inner.y, size, inner.height)
460                },
461                | Direction::Vertical => {
462                    Rect::new(inner.x, inner.y.saturating_add(current), inner.width, size)
463                },
464            };
465            rects.push(rect);
466
467            current = current.saturating_add(size);
468        }
469
470        Some(rects)
471    }
472}
473
474#[cfg(test)]
475mod tests {
476    use super::*;
477
478    #[test]
479    fn layout_vertical_split_length() {
480        let layout = Layout::vertical([Constraint::Length(5), Constraint::Length(5)]);
481        let rects = layout.split(Rect::new(0, 0, 10, 10));
482        assert_eq!(rects.len(), 2);
483        assert_eq!(rects[0].height, 5);
484        assert_eq!(rects[1].height, 5);
485    }
486
487    #[test]
488    fn layout_horizontal_split_length() {
489        let layout = Layout::horizontal([Constraint::Length(5), Constraint::Length(5)]);
490        let rects = layout.split(Rect::new(0, 0, 10, 10));
491        assert_eq!(rects.len(), 2);
492        assert_eq!(rects[0].width, 5);
493        assert_eq!(rects[1].width, 5);
494    }
495
496    #[test]
497    fn layout_split_with_margin() {
498        let layout = Layout::vertical([Constraint::Length(5), Constraint::Length(5)]).margin(1);
499        let rects = layout.split(Rect::new(0, 0, 10, 10));
500        assert_eq!(rects.len(), 2);
501        assert_eq!(rects[0].y, 1);
502        assert_eq!(rects[0].width, 8);
503    }
504
505    #[test]
506    fn layout_split_empty_area() {
507        let layout = Layout::vertical([Constraint::Length(5)]);
508        let rects = layout.split(Rect::ZERO);
509        assert_eq!(rects.len(), 1);
510        assert_eq!(rects[0], Rect::ZERO);
511    }
512
513    #[test]
514    fn layout_builder_api() {
515        let layout = Layout::default()
516            .direction(Direction::Horizontal)
517            .constraints([Constraint::Length(10)])
518            .margin(2)
519            .flex(Flex::Center)
520            .spacing(1);
521        assert_eq!(layout.direction, Direction::Horizontal);
522        assert_eq!(layout.constraints, vec![Constraint::Length(10)]);
523        assert_eq!(layout.margin, Margin::new(2, 2));
524        assert_eq!(layout.flex, Flex::Center);
525        assert_eq!(layout.spacing, Spacing::Space(1));
526    }
527
528    #[test]
529    fn layout_areas_const_generic() {
530        let layout = Layout::vertical([Constraint::Length(5), Constraint::Length(5)]);
531        let areas: [Rect; 2] = layout.areas(Rect::new(0, 0, 10, 10));
532        assert_eq!(areas[0].height, 5);
533        assert_eq!(areas[1].height, 5);
534    }
535}