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 Percentage(u16),
27 Ratio(u32, u32),
28 Length(u16),
29 Max(u16),
30 Min(u16),
31}
32
33impl Constraint {
34 pub fn apply(&self, length: u16) -> u16 {
35 match *self {
36 Constraint::Percentage(p) => length * p / 100,
37 Constraint::Ratio(num, den) => {
38 let r = num * u32::from(length) / den;
39 r as u16
40 }
41 Constraint::Length(l) => length.min(l),
42 Constraint::Max(m) => length.min(m),
43 Constraint::Min(m) => length.max(m),
44 }
45 }
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Hash)]
49pub struct Margin {
50 pub vertical: u16,
51 pub horizontal: u16,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum Alignment {
56 Left,
57 Center,
58 Right,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq, Hash)]
62pub struct Layout {
63 direction: Direction,
64 margin: Margin,
65 constraints: Vec<Constraint>,
66 expand_to_fill: bool,
69}
70
71thread_local! {
72 static LAYOUT_CACHE: RefCell<HashMap<(Rect, Layout), Vec<Rect>>> = RefCell::new(HashMap::new());
73}
74
75impl Default for Layout {
76 fn default() -> Layout {
77 Layout {
78 direction: Direction::Vertical,
79 margin: Margin {
80 horizontal: 0,
81 vertical: 0,
82 },
83 constraints: Vec::new(),
84 expand_to_fill: true,
85 }
86 }
87}
88
89impl Layout {
90 pub fn constraints<C>(mut self, constraints: C) -> Layout
91 where
92 C: Into<Vec<Constraint>>,
93 {
94 self.constraints = constraints.into();
95 self
96 }
97
98 pub fn margin(mut self, margin: u16) -> Layout {
99 self.margin = Margin {
100 horizontal: margin,
101 vertical: margin,
102 };
103 self
104 }
105
106 pub fn horizontal_margin(mut self, horizontal: u16) -> Layout {
107 self.margin.horizontal = horizontal;
108 self
109 }
110
111 pub fn vertical_margin(mut self, vertical: u16) -> Layout {
112 self.margin.vertical = vertical;
113 self
114 }
115
116 pub fn direction(mut self, direction: Direction) -> Layout {
117 self.direction = direction;
118 self
119 }
120
121 pub(crate) fn expand_to_fill(mut self, expand_to_fill: bool) -> Layout {
122 self.expand_to_fill = expand_to_fill;
123 self
124 }
125
126 pub fn split(&self, area: Rect) -> Vec<Rect> {
187 LAYOUT_CACHE.with(|c| {
189 c.borrow_mut()
190 .entry((area, self.clone()))
191 .or_insert_with(|| split(area, self))
192 .clone()
193 })
194 }
195}
196
197fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
198 let mut solver = Solver::new();
199 let mut vars: HashMap<Variable, (usize, usize)> = HashMap::new();
200 let elements = layout
201 .constraints
202 .iter()
203 .map(|_| Element::new())
204 .collect::<Vec<Element>>();
205 let mut results = layout
206 .constraints
207 .iter()
208 .map(|_| Rect::default())
209 .collect::<Vec<Rect>>();
210
211 let dest_area = area.inner(&layout.margin);
212 for (i, e) in elements.iter().enumerate() {
213 vars.insert(e.x, (i, 0));
214 vars.insert(e.y, (i, 1));
215 vars.insert(e.width, (i, 2));
216 vars.insert(e.height, (i, 3));
217 }
218 let mut ccs: Vec<CassowaryConstraint> =
219 Vec::with_capacity(elements.len() * 4 + layout.constraints.len() * 6);
220 for elt in &elements {
221 ccs.push(elt.width | GE(REQUIRED) | 0f64);
222 ccs.push(elt.height | GE(REQUIRED) | 0f64);
223 ccs.push(elt.left() | GE(REQUIRED) | f64::from(dest_area.left()));
224 ccs.push(elt.top() | GE(REQUIRED) | f64::from(dest_area.top()));
225 ccs.push(elt.right() | LE(REQUIRED) | f64::from(dest_area.right()));
226 ccs.push(elt.bottom() | LE(REQUIRED) | f64::from(dest_area.bottom()));
227 }
228 if let Some(first) = elements.first() {
229 ccs.push(match layout.direction {
230 Direction::Horizontal => first.left() | EQ(REQUIRED) | f64::from(dest_area.left()),
231 Direction::Vertical => first.top() | EQ(REQUIRED) | f64::from(dest_area.top()),
232 });
233 }
234 if layout.expand_to_fill {
235 if let Some(last) = elements.last() {
236 ccs.push(match layout.direction {
237 Direction::Horizontal => last.right() | EQ(REQUIRED) | f64::from(dest_area.right()),
238 Direction::Vertical => last.bottom() | EQ(REQUIRED) | f64::from(dest_area.bottom()),
239 });
240 }
241 }
242 match layout.direction {
243 Direction::Horizontal => {
244 for pair in elements.windows(2) {
245 ccs.push((pair[0].x + pair[0].width) | EQ(REQUIRED) | pair[1].x);
246 }
247 for (i, size) in layout.constraints.iter().enumerate() {
248 ccs.push(elements[i].y | EQ(REQUIRED) | f64::from(dest_area.y));
249 ccs.push(elements[i].height | EQ(REQUIRED) | f64::from(dest_area.height));
250 ccs.push(match *size {
251 Constraint::Length(v) => elements[i].width | EQ(WEAK) | f64::from(v),
252 Constraint::Percentage(v) => {
253 elements[i].width | EQ(WEAK) | (f64::from(v * dest_area.width) / 100.0)
254 }
255 Constraint::Ratio(n, d) => {
256 elements[i].width
257 | EQ(WEAK)
258 | (f64::from(dest_area.width) * f64::from(n) / f64::from(d))
259 }
260 Constraint::Min(v) => elements[i].width | GE(WEAK) | f64::from(v),
261 Constraint::Max(v) => elements[i].width | LE(WEAK) | f64::from(v),
262 });
263 }
264 }
265 Direction::Vertical => {
266 for pair in elements.windows(2) {
267 ccs.push((pair[0].y + pair[0].height) | EQ(REQUIRED) | pair[1].y);
268 }
269 for (i, size) in layout.constraints.iter().enumerate() {
270 ccs.push(elements[i].x | EQ(REQUIRED) | f64::from(dest_area.x));
271 ccs.push(elements[i].width | EQ(REQUIRED) | f64::from(dest_area.width));
272 ccs.push(match *size {
273 Constraint::Length(v) => elements[i].height | EQ(WEAK) | f64::from(v),
274 Constraint::Percentage(v) => {
275 elements[i].height | EQ(WEAK) | (f64::from(v * dest_area.height) / 100.0)
276 }
277 Constraint::Ratio(n, d) => {
278 elements[i].height
279 | EQ(WEAK)
280 | (f64::from(dest_area.height) * f64::from(n) / f64::from(d))
281 }
282 Constraint::Min(v) => elements[i].height | GE(WEAK) | f64::from(v),
283 Constraint::Max(v) => elements[i].height | LE(WEAK) | f64::from(v),
284 });
285 }
286 }
287 }
288 solver.add_constraints(&ccs).unwrap();
289 for &(var, value) in solver.fetch_changes() {
290 let (index, attr) = vars[&var];
291 let value = if value.is_sign_negative() {
292 0
293 } else {
294 value as u16
295 };
296 match attr {
297 0 => {
298 results[index].x = value;
299 }
300 1 => {
301 results[index].y = value;
302 }
303 2 => {
304 results[index].width = value;
305 }
306 3 => {
307 results[index].height = value;
308 }
309 _ => {}
310 }
311 }
312
313 if layout.expand_to_fill {
314 if let Some(last) = results.last_mut() {
316 match layout.direction {
317 Direction::Vertical => {
318 last.height = dest_area.bottom() - last.y;
319 }
320 Direction::Horizontal => {
321 last.width = dest_area.right() - last.x;
322 }
323 }
324 }
325 }
326 results
327}
328
329struct Element {
331 x: Variable,
332 y: Variable,
333 width: Variable,
334 height: Variable,
335}
336
337impl Element {
338 fn new() -> Element {
339 Element {
340 x: Variable::new(),
341 y: Variable::new(),
342 width: Variable::new(),
343 height: Variable::new(),
344 }
345 }
346
347 fn left(&self) -> Variable {
348 self.x
349 }
350
351 fn top(&self) -> Variable {
352 self.y
353 }
354
355 fn right(&self) -> Expression {
356 self.x + self.width
357 }
358
359 fn bottom(&self) -> Expression {
360 self.y + self.height
361 }
362}
363
364#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default)]
367pub struct Rect {
368 pub x: u16,
369 pub y: u16,
370 pub width: u16,
371 pub height: u16,
372}
373
374impl Rect {
375 pub fn new(x: u16, y: u16, width: u16, height: u16) -> Rect {
378 let max_area = u16::max_value();
379 let (clipped_width, clipped_height) =
380 if u32::from(width) * u32::from(height) > u32::from(max_area) {
381 let aspect_ratio = f64::from(width) / f64::from(height);
382 let max_area_f = f64::from(max_area);
383 let height_f = (max_area_f / aspect_ratio).sqrt();
384 let width_f = height_f * aspect_ratio;
385 (width_f as u16, height_f as u16)
386 } else {
387 (width, height)
388 };
389 Rect {
390 x,
391 y,
392 width: clipped_width,
393 height: clipped_height,
394 }
395 }
396
397 pub fn area(self) -> u16 {
398 self.width * self.height
399 }
400
401 pub fn left(self) -> u16 {
402 self.x
403 }
404
405 pub fn right(self) -> u16 {
406 self.x.saturating_add(self.width)
407 }
408
409 pub fn top(self) -> u16 {
410 self.y
411 }
412
413 pub fn bottom(self) -> u16 {
414 self.y.saturating_add(self.height)
415 }
416
417 pub fn inner(self, margin: &Margin) -> Rect {
418 if self.width < 2 * margin.horizontal || self.height < 2 * margin.vertical {
419 Rect::default()
420 } else {
421 Rect {
422 x: self.x + margin.horizontal,
423 y: self.y + margin.vertical,
424 width: self.width - 2 * margin.horizontal,
425 height: self.height - 2 * margin.vertical,
426 }
427 }
428 }
429
430 pub fn union(self, other: Rect) -> Rect {
431 let x1 = min(self.x, other.x);
432 let y1 = min(self.y, other.y);
433 let x2 = max(self.x + self.width, other.x + other.width);
434 let y2 = max(self.y + self.height, other.y + other.height);
435 Rect {
436 x: x1,
437 y: y1,
438 width: x2 - x1,
439 height: y2 - y1,
440 }
441 }
442
443 pub fn intersection(self, other: Rect) -> Rect {
444 let x1 = max(self.x, other.x);
445 let y1 = max(self.y, other.y);
446 let x2 = min(self.x + self.width, other.x + other.width);
447 let y2 = min(self.y + self.height, other.y + other.height);
448 Rect {
449 x: x1,
450 y: y1,
451 width: x2 - x1,
452 height: y2 - y1,
453 }
454 }
455
456 pub fn intersects(self, other: Rect) -> bool {
457 self.x < other.x + other.width
458 && self.x + self.width > other.x
459 && self.y < other.y + other.height
460 && self.y + self.height > other.y
461 }
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467
468 #[test]
469 fn test_vertical_split_by_height() {
470 let target = Rect {
471 x: 2,
472 y: 2,
473 width: 10,
474 height: 10,
475 };
476
477 let chunks = Layout::default()
478 .direction(Direction::Vertical)
479 .constraints(
480 [
481 Constraint::Percentage(10),
482 Constraint::Max(5),
483 Constraint::Min(1),
484 ]
485 .as_ref(),
486 )
487 .split(target);
488
489 assert_eq!(target.height, chunks.iter().map(|r| r.height).sum::<u16>());
490 chunks.windows(2).for_each(|w| assert!(w[0].y <= w[1].y));
491 }
492
493 #[test]
494 fn test_rect_size_truncation() {
495 for width in 256u16..300u16 {
496 for height in 256u16..300u16 {
497 let rect = Rect::new(0, 0, width, height);
498 rect.area(); assert!(rect.width < width || rect.height < height);
500 assert!(
503 (f64::from(rect.width) / f64::from(rect.height)
504 - f64::from(width) / f64::from(height))
505 .abs()
506 < 1.0
507 )
508 }
509 }
510
511 let width = 900;
513 let height = 100;
514 let rect = Rect::new(0, 0, width, height);
515 assert_ne!(rect.width, 900);
516 assert_ne!(rect.height, 100);
517 assert!(rect.width < width || rect.height < height);
518 }
519
520 #[test]
521 fn test_rect_size_preservation() {
522 for width in 0..256u16 {
523 for height in 0..256u16 {
524 let rect = Rect::new(0, 0, width, height);
525 rect.area(); assert_eq!(rect.width, width);
527 assert_eq!(rect.height, height);
528 }
529 }
530
531 let rect = Rect::new(0, 0, 300, 100);
533 assert_eq!(rect.width, 300);
534 assert_eq!(rect.height, 100);
535 }
536}