tui_temp_fork/
layout.rs

1use std::cell::RefCell;
2use std::cmp::{max, min};
3use std::collections::HashMap;
4
5use cassowary::strength::{REQUIRED, WEAK};
6use cassowary::WeightedRelation::*;
7use cassowary::{Constraint as CassowaryConstraint, Expression, Solver, Variable};
8
9#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)]
10pub enum Corner {
11    TopLeft,
12    TopRight,
13    BottomRight,
14    BottomLeft,
15}
16
17#[derive(Debug, Hash, Clone, PartialEq, Eq)]
18pub enum Direction {
19    Horizontal,
20    Vertical,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub enum Constraint {
25    // TODO: enforce range 0 - 100
26    Percentage(u16),
27    Ratio(u32, u32),
28    Length(u16),
29    Max(u16),
30    Min(u16),
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Hash)]
34pub struct Margin {
35    vertical: u16,
36    horizontal: u16,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq)]
40pub enum Alignment {
41    Left,
42    Center,
43    Right,
44}
45
46// TODO: enforce constraints size once const generics has landed
47#[derive(Debug, Clone, PartialEq, Eq, Hash)]
48pub struct Layout {
49    direction: Direction,
50    margin: Margin,
51    constraints: Vec<Constraint>,
52}
53
54thread_local! {
55    static LAYOUT_CACHE: RefCell<HashMap<(Rect, Layout), Vec<Rect>>> = RefCell::new(HashMap::new());
56}
57
58impl Default for Layout {
59    fn default() -> Layout {
60        Layout {
61            direction: Direction::Vertical,
62            margin: Margin {
63                horizontal: 0,
64                vertical: 0,
65            },
66            constraints: Vec::new(),
67        }
68    }
69}
70
71impl Layout {
72    pub fn constraints<C>(mut self, constraints: C) -> Layout
73    where
74        C: Into<Vec<Constraint>>,
75    {
76        self.constraints = constraints.into();
77        self
78    }
79
80    pub fn margin(mut self, margin: u16) -> Layout {
81        self.margin = Margin {
82            horizontal: margin,
83            vertical: margin,
84        };
85        self
86    }
87
88    pub fn horizontal_margin(mut self, horizontal: u16) -> Layout {
89        self.margin.horizontal = horizontal;
90        self
91    }
92
93    pub fn vertical_margin(mut self, vertical: u16) -> Layout {
94        self.margin.vertical = vertical;
95        self
96    }
97
98    pub fn direction(mut self, direction: Direction) -> Layout {
99        self.direction = direction;
100        self
101    }
102
103    /// Wrapper function around the cassowary-rs solver to be able to split a given
104    /// area into smaller ones based on the preferred widths or heights and the direction.
105    ///
106    /// # Examples
107    /// ```
108    /// # use tui_temp_fork::layout::{Rect, Constraint, Direction, Layout};
109    ///
110    /// # fn main() {
111    ///       let chunks = Layout::default()
112    ///           .direction(Direction::Vertical)
113    ///           .constraints([Constraint::Length(5), Constraint::Min(0)].as_ref())
114    ///           .split(Rect {
115    ///               x: 2,
116    ///               y: 2,
117    ///               width: 10,
118    ///               height: 10,
119    ///           });
120    ///       assert_eq!(
121    ///           chunks,
122    ///           vec![
123    ///               Rect {
124    ///                   x: 2,
125    ///                   y: 2,
126    ///                   width: 10,
127    ///                   height: 5
128    ///               },
129    ///               Rect {
130    ///                   x: 2,
131    ///                   y: 7,
132    ///                   width: 10,
133    ///                   height: 5
134    ///               }
135    ///           ]
136    ///       );
137    ///
138    ///       let chunks = Layout::default()
139    ///           .direction(Direction::Horizontal)
140    ///           .constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)].as_ref())
141    ///           .split(Rect {
142    ///               x: 0,
143    ///               y: 0,
144    ///               width: 9,
145    ///               height: 2,
146    ///           });
147    ///       assert_eq!(
148    ///           chunks,
149    ///           vec![
150    ///               Rect {
151    ///                   x: 0,
152    ///                   y: 0,
153    ///                   width: 3,
154    ///                   height: 2
155    ///               },
156    ///               Rect {
157    ///                   x: 3,
158    ///                   y: 0,
159    ///                   width: 6,
160    ///                   height: 2
161    ///               }
162    ///           ]
163    ///       );
164    /// # }
165    ///
166    /// ```
167    pub fn split(self, area: Rect) -> Vec<Rect> {
168        // TODO: Maybe use a fixed size cache ?
169        LAYOUT_CACHE.with(|c| {
170            c.borrow_mut()
171                .entry((area, self.clone()))
172                .or_insert_with(|| split(area, &self))
173                .clone()
174        })
175    }
176}
177
178fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
179    let mut solver = Solver::new();
180    let mut vars: HashMap<Variable, (usize, usize)> = HashMap::new();
181    let elements = layout
182        .constraints
183        .iter()
184        .map(|_| Element::new())
185        .collect::<Vec<Element>>();
186    let mut results = layout
187        .constraints
188        .iter()
189        .map(|_| Rect::default())
190        .collect::<Vec<Rect>>();
191
192    let dest_area = area.inner(&layout.margin);
193    for (i, e) in elements.iter().enumerate() {
194        vars.insert(e.x, (i, 0));
195        vars.insert(e.y, (i, 1));
196        vars.insert(e.width, (i, 2));
197        vars.insert(e.height, (i, 3));
198    }
199    let mut ccs: Vec<CassowaryConstraint> =
200        Vec::with_capacity(elements.len() * 4 + layout.constraints.len() * 6);
201    for elt in &elements {
202        ccs.push(elt.left() | GE(REQUIRED) | f64::from(dest_area.left()));
203        ccs.push(elt.top() | GE(REQUIRED) | f64::from(dest_area.top()));
204        ccs.push(elt.right() | LE(REQUIRED) | f64::from(dest_area.right()));
205        ccs.push(elt.bottom() | LE(REQUIRED) | f64::from(dest_area.bottom()));
206    }
207    if let Some(first) = elements.first() {
208        ccs.push(match layout.direction {
209            Direction::Horizontal => first.left() | EQ(REQUIRED) | f64::from(dest_area.left()),
210            Direction::Vertical => first.top() | EQ(REQUIRED) | f64::from(dest_area.top()),
211        });
212    }
213    if let Some(last) = elements.last() {
214        ccs.push(match layout.direction {
215            Direction::Horizontal => last.right() | EQ(REQUIRED) | f64::from(dest_area.right()),
216            Direction::Vertical => last.bottom() | EQ(REQUIRED) | f64::from(dest_area.bottom()),
217        });
218    }
219    match layout.direction {
220        Direction::Horizontal => {
221            for pair in elements.windows(2) {
222                ccs.push((pair[0].x + pair[0].width) | EQ(REQUIRED) | pair[1].x);
223            }
224            for (i, size) in layout.constraints.iter().enumerate() {
225                ccs.push(elements[i].y | EQ(REQUIRED) | f64::from(dest_area.y));
226                ccs.push(elements[i].height | EQ(REQUIRED) | f64::from(dest_area.height));
227                ccs.push(match *size {
228                    Constraint::Length(v) => elements[i].width | EQ(WEAK) | f64::from(v),
229                    Constraint::Percentage(v) => {
230                        elements[i].width | EQ(WEAK) | (f64::from(v * dest_area.width) / 100.0)
231                    }
232                    Constraint::Ratio(n, d) => {
233                        elements[i].width
234                            | EQ(WEAK)
235                            | (f64::from(dest_area.width) * f64::from(n) / f64::from(d))
236                    }
237                    Constraint::Min(v) => elements[i].width | GE(WEAK) | f64::from(v),
238                    Constraint::Max(v) => elements[i].width | LE(WEAK) | f64::from(v),
239                });
240            }
241        }
242        Direction::Vertical => {
243            for pair in elements.windows(2) {
244                ccs.push((pair[0].y + pair[0].height) | EQ(REQUIRED) | pair[1].y);
245            }
246            for (i, size) in layout.constraints.iter().enumerate() {
247                ccs.push(elements[i].x | EQ(REQUIRED) | f64::from(dest_area.x));
248                ccs.push(elements[i].width | EQ(REQUIRED) | f64::from(dest_area.width));
249                ccs.push(match *size {
250                    Constraint::Length(v) => elements[i].height | EQ(WEAK) | f64::from(v),
251                    Constraint::Percentage(v) => {
252                        elements[i].height | EQ(WEAK) | (f64::from(v * dest_area.height) / 100.0)
253                    }
254                    Constraint::Ratio(n, d) => {
255                        elements[i].height
256                            | EQ(WEAK)
257                            | (f64::from(dest_area.height) * f64::from(n) / f64::from(d))
258                    }
259                    Constraint::Min(v) => elements[i].height | GE(WEAK) | f64::from(v),
260                    Constraint::Max(v) => elements[i].height | LE(WEAK) | f64::from(v),
261                });
262            }
263        }
264    }
265    solver.add_constraints(&ccs).unwrap();
266    for &(var, value) in solver.fetch_changes() {
267        let (index, attr) = vars[&var];
268        let value = if value.is_sign_negative() {
269            0
270        } else {
271            value as u16
272        };
273        match attr {
274            0 => {
275                results[index].x = value;
276            }
277            1 => {
278                results[index].y = value;
279            }
280            2 => {
281                results[index].width = value;
282            }
283            3 => {
284                results[index].height = value;
285            }
286            _ => {}
287        }
288    }
289
290    // Fix imprecision by extending the last item a bit if necessary
291    if let Some(last) = results.last_mut() {
292        match layout.direction {
293            Direction::Vertical => {
294                last.height = dest_area.bottom() - last.y;
295            }
296            Direction::Horizontal => {
297                last.width = dest_area.right() - last.x;
298            }
299        }
300    }
301    results
302}
303
304/// A container used by the solver inside split
305struct Element {
306    x: Variable,
307    y: Variable,
308    width: Variable,
309    height: Variable,
310}
311
312impl Element {
313    fn new() -> Element {
314        Element {
315            x: Variable::new(),
316            y: Variable::new(),
317            width: Variable::new(),
318            height: Variable::new(),
319        }
320    }
321
322    fn left(&self) -> Variable {
323        self.x
324    }
325
326    fn top(&self) -> Variable {
327        self.y
328    }
329
330    fn right(&self) -> Expression {
331        self.x + self.width
332    }
333
334    fn bottom(&self) -> Expression {
335        self.y + self.height
336    }
337}
338
339/// A simple rectangle used in the computation of the layout and to give widgets an hint about the
340/// area they are supposed to render to.
341#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
342pub struct Rect {
343    pub x: u16,
344    pub y: u16,
345    pub width: u16,
346    pub height: u16,
347}
348
349impl Default for Rect {
350    fn default() -> Rect {
351        Rect {
352            x: 0,
353            y: 0,
354            width: 0,
355            height: 0,
356        }
357    }
358}
359
360impl Rect {
361    /// Creates a new rect, with width and height limited to keep the area under max u16.
362    /// If clipped, aspect ratio will be preserved.
363    pub fn new(x: u16, y: u16, width: u16, height: u16) -> Rect {
364        let max_area = u16::max_value();
365        let (clipped_width, clipped_height) =
366            if u32::from(width) * u32::from(height) > u32::from(max_area) {
367                let aspect_ratio = f64::from(width) / f64::from(height);
368                let max_area_f = f64::from(max_area);
369                let height_f = (max_area_f / aspect_ratio).sqrt();
370                let width_f = height_f * aspect_ratio;
371                (width_f as u16, height_f as u16)
372            } else {
373                (width, height)
374            };
375        Rect {
376            x,
377            y,
378            width: clipped_width,
379            height: clipped_height,
380        }
381    }
382
383    pub fn area(self) -> u16 {
384        self.width * self.height
385    }
386
387    pub fn left(self) -> u16 {
388        self.x
389    }
390
391    pub fn right(self) -> u16 {
392        self.x + self.width
393    }
394
395    pub fn top(self) -> u16 {
396        self.y
397    }
398
399    pub fn bottom(self) -> u16 {
400        self.y + self.height
401    }
402
403    pub fn inner(self, margin: &Margin) -> Rect {
404        if self.width < 2 * margin.horizontal || self.height < 2 * margin.vertical {
405            Rect::default()
406        } else {
407            Rect {
408                x: self.x + margin.horizontal,
409                y: self.y + margin.vertical,
410                width: self.width - 2 * margin.horizontal,
411                height: self.height - 2 * margin.vertical,
412            }
413        }
414    }
415
416    pub fn union(self, other: Rect) -> Rect {
417        let x1 = min(self.x, other.x);
418        let y1 = min(self.y, other.y);
419        let x2 = max(self.x + self.width, other.x + other.width);
420        let y2 = max(self.y + self.height, other.y + other.height);
421        Rect {
422            x: x1,
423            y: y1,
424            width: x2 - x1,
425            height: y2 - y1,
426        }
427    }
428
429    pub fn intersection(self, other: Rect) -> Rect {
430        let x1 = max(self.x, other.x);
431        let y1 = max(self.y, other.y);
432        let x2 = min(self.x + self.width, other.x + other.width);
433        let y2 = min(self.y + self.height, other.y + other.height);
434        Rect {
435            x: x1,
436            y: y1,
437            width: x2 - x1,
438            height: y2 - y1,
439        }
440    }
441
442    pub fn intersects(self, other: Rect) -> bool {
443        self.x < other.x + other.width
444            && self.x + self.width > other.x
445            && self.y < other.y + other.height
446            && self.y + self.height > other.y
447    }
448}
449
450#[test]
451fn test_rect_size_truncation() {
452    for width in 256u16..300u16 {
453        for height in 256u16..300u16 {
454            let rect = Rect::new(0, 0, width, height);
455            rect.area(); // Should not panic.
456            assert!(rect.width < width || rect.height < height);
457            // The target dimensions are rounded down so the math will not be too precise
458            // but let's make sure the ratios don't diverge crazily.
459            assert!(
460                (f64::from(rect.width) / f64::from(rect.height)
461                    - f64::from(width) / f64::from(height))
462                .abs()
463                    < 1.0
464            )
465        }
466    }
467
468    // One dimension below 255, one above. Area above max u16.
469    let width = 900;
470    let height = 100;
471    let rect = Rect::new(0, 0, width, height);
472    assert_ne!(rect.width, 900);
473    assert_ne!(rect.height, 100);
474    assert!(rect.width < width || rect.height < height);
475}
476
477#[test]
478fn test_rect_size_preservation() {
479    for width in 0..256u16 {
480        for height in 0..256u16 {
481            let rect = Rect::new(0, 0, width, height);
482            rect.area(); // Should not panic.
483            assert_eq!(rect.width, width);
484            assert_eq!(rect.height, height);
485        }
486    }
487
488    // One dimension below 255, one above. Area below max u16.
489    let rect = Rect::new(0, 0, 300, 100);
490    assert_eq!(rect.width, 300);
491    assert_eq!(rect.height, 100);
492}