Skip to main content

photon_ui/layout/
engine.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
24macro_rules! try_add {
25    ($expr:expr) => {
26        if $expr.is_err() {
27            return None;
28        }
29    };
30}
31
32/// A layout configuration that splits a [`Rect`] into sub-rects.
33#[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    /// Create a new layout with the given direction and constraints.
44    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    /// Shorthand for [`Layout::new`] with [`Direction::Vertical`].
58    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    /// Shorthand for [`Layout::new`] with [`Direction::Horizontal`].
66    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    /// Set the layout direction.
74    pub fn direction(mut self, direction: Direction) -> Self {
75        self.direction = direction;
76        self
77    }
78
79    /// Replace the current constraints.
80    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    /// Set uniform margin on all sides.
89    pub fn margin(mut self, margin: u16) -> Self {
90        self.margin = Margin::new(margin, margin);
91        self
92    }
93
94    /// Set horizontal margin (left and right).
95    pub fn horizontal_margin(mut self, margin: u16) -> Self {
96        self.margin.horizontal = margin;
97        self
98    }
99
100    /// Set vertical margin (top and bottom).
101    pub fn vertical_margin(mut self, margin: u16) -> Self {
102        self.margin.vertical = margin;
103        self
104    }
105
106    /// Set how excess space is distributed.
107    pub fn flex(mut self, flex: Flex) -> Self {
108        self.flex = flex;
109        self
110    }
111
112    /// Set the spacing between layout segments.
113    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    /// Split `area` into sub-rects according to this layout's constraints.
131    pub fn split(&self, area: Rect) -> Vec<Rect> {
132        self.try_split(area).unwrap_or_default()
133    }
134
135    /// Like [`split`](Layout::split), but returns a fixed-size array.
136    ///
137    /// Missing segments are filled with [`Rect::ZERO`].
138    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        // All segment variables must be non-negative.
167        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        // Sum of all segments and spacers equals the total available space.
174        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        // Apply per-segment constraints.
183        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                    // Fill is handled by the flex/grow constraints below.
218                },
219            }
220        }
221
222        // Configure spacers based on flex and spacing.
223        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                // In legacy mode all spacers are zero; excess space stays in segments.
231                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        // Grow constraints for Fill and Min segments.
358        // Weak EQ(total) pushes Cassowary to expand these segments to fill available
359        // space.
360        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        // Weak grow for all segments so non-fixed ones can expand.
378        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        // Solve and extract values.
387        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}