Skip to content

Commit

Permalink
feat: Add weak constraints to make rects closer to each other in size…
Browse files Browse the repository at this point in the history
… ✨ (#395)

Also make `Max` and `Min` constraints MEDIUM strength for higher priority over equal chunks
  • Loading branch information
kdheepak committed Aug 20, 2023
1 parent dc55211 commit 6153371
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 18 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ bitflags = "2.3"
cassowary = "0.3"
crossterm = { version = "0.27", optional = true }
indoc = "2.0"
itertools = "0.11"
paste = "1.0.2"
serde = { version = "1", optional = true, features = ["derive"] }
termion = { version = "2.0", optional = true }
Expand All @@ -56,7 +57,6 @@ cargo-husky = { version = "1.5.0", default-features = false, features = [
] }
criterion = { version = "0.5", features = ["html_reports"] }
fakeit = "1.1"
itertools = "0.10"
rand = "0.8"
pretty_assertions = "1.4.0"

Expand Down
133 changes: 118 additions & 15 deletions src/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ use std::{
};

use cassowary::{
strength::{REQUIRED, STRONG, WEAK},
strength::{MEDIUM, REQUIRED, STRONG, WEAK},
AddConstraintError, Expression, Solver, Variable,
WeightedRelation::{EQ, GE, LE},
};
use itertools::Itertools;

#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
pub enum Corner {
Expand Down Expand Up @@ -232,6 +233,14 @@ impl Rect {
}
}

#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub enum SegmentSize {
EvenDistribution,
#[default]
LastTakesRemainder,
None,
}

/// A layout is a set of constraints that can be applied to a given area to split it into smaller
/// ones.
///
Expand Down Expand Up @@ -281,9 +290,8 @@ pub struct Layout {
direction: Direction,
margin: Margin,
constraints: Vec<Constraint>,
/// Whether the last chunk of the computed layout should be expanded to fill the available
/// space.
expand_to_fill: bool,
/// option for segment size preferences
segment_size: SegmentSize,
}

impl Default for Layout {
Expand All @@ -298,7 +306,7 @@ impl Layout {
/// - direction: [Direction::Vertical]
/// - margin: 0, 0
/// - constraints: empty
/// - expand_to_fill: true
/// - segment_size: SegmentSize::LastTakesRemainder
pub const fn new() -> Layout {
Layout {
direction: Direction::Vertical,
Expand All @@ -307,7 +315,7 @@ impl Layout {
vertical: 0,
},
constraints: Vec::new(),
expand_to_fill: true,
segment_size: SegmentSize::LastTakesRemainder,
}
}

Expand Down Expand Up @@ -419,10 +427,9 @@ impl Layout {
self
}

/// Builder method to set whether the last chunk of the computed layout should be expanded to
/// fill the available space.
pub(crate) const fn expand_to_fill(mut self, expand_to_fill: bool) -> Layout {
self.expand_to_fill = expand_to_fill;
/// Builder method to set whether chunks should be of equal size.
pub(crate) const fn segment_size(mut self, segment_size: SegmentSize) -> Layout {
self.segment_size = segment_size;
self
}

Expand Down Expand Up @@ -524,7 +531,7 @@ fn try_split(area: Rect, layout: &Layout) -> Result<Rc<[Rect]>, AddConstraintErr
solver.add_constraint(first.start | EQ(REQUIRED) | area_start)?;
}
// ensure the last element touches the right/bottom edge of the area
if layout.expand_to_fill {
if layout.segment_size != SegmentSize::None {
if let Some(last) = elements.last() {
solver.add_constraint(last.end | EQ(REQUIRED) | area_end)?;
}
Expand All @@ -547,17 +554,23 @@ fn try_split(area: Rect, layout: &Layout) -> Result<Rc<[Rect]>, AddConstraintErr
Constraint::Max(m) => {
solver.add_constraints(&[
element.size() | LE(STRONG) | f64::from(m),
element.size() | EQ(WEAK) | f64::from(m),
element.size() | EQ(MEDIUM) | f64::from(m),
])?;
}
Constraint::Min(m) => {
solver.add_constraints(&[
element.size() | GE(STRONG) | f64::from(m),
element.size() | EQ(WEAK) | f64::from(m),
element.size() | EQ(MEDIUM) | f64::from(m),
])?;
}
}
}
// prefer equal chunks if other constraints are all satisfied
if layout.segment_size == SegmentSize::EvenDistribution {
for (left, right) in elements.iter().tuple_combinations() {
solver.add_constraint(left.size() | EQ(WEAK) | right.size())?;
}
}

let changes: HashMap<Variable, f64> = solver.fetch_changes().iter().copied().collect();

Expand Down Expand Up @@ -600,7 +613,97 @@ fn try_split(area: Rect, layout: &Layout) -> Result<Rc<[Rect]>, AddConstraintErr

#[cfg(test)]
mod tests {
use super::*;
use super::{SegmentSize::*, *};
use crate::prelude::Constraint::*;

fn get_x_width_with_segment_size(
segment_size: SegmentSize,
constraints: Vec<Constraint>,
target: Rect,
) -> Vec<(u16, u16)> {
let layout = Layout::default()
.direction(Direction::Horizontal)
.constraints(constraints)
.segment_size(segment_size);
let chunks = layout.split(target);
chunks.iter().map(|r| (r.x, r.width)).collect()
}

#[test]
fn test_split_equally_in_underspecified_case() {
let target = Rect::new(100, 200, 10, 10);
assert_eq!(
get_x_width_with_segment_size(LastTakesRemainder, vec![Min(2), Min(2), Min(0)], target),
[(100, 2), (102, 2), (104, 6)]
);
assert_eq!(
get_x_width_with_segment_size(EvenDistribution, vec![Min(2), Min(2), Min(0)], target),
[(100, 3), (103, 4), (107, 3)]
);
}

#[test]
fn test_split_equally_in_overconstrained_case_for_min() {
let target = Rect::new(100, 200, 100, 10);
assert_eq!(
get_x_width_with_segment_size(
LastTakesRemainder,
vec![Percentage(50), Min(10), Percentage(50)],
target
),
[(100, 50), (150, 10), (160, 40)]
);
assert_eq!(
get_x_width_with_segment_size(
EvenDistribution,
vec![Percentage(50), Min(10), Percentage(50)],
target
),
[(100, 45), (145, 10), (155, 45)]
);
}

#[test]
fn test_split_equally_in_overconstrained_case_for_max() {
let target = Rect::new(100, 200, 100, 10);
assert_eq!(
get_x_width_with_segment_size(
LastTakesRemainder,
vec![Percentage(30), Max(10), Percentage(30)],
target
),
[(100, 30), (130, 10), (140, 60)]
);
assert_eq!(
get_x_width_with_segment_size(
EvenDistribution,
vec![Percentage(30), Max(10), Percentage(30)],
target
),
[(100, 45), (145, 10), (155, 45)]
);
}

#[test]
fn test_split_equally_in_overconstrained_case_for_length() {
let target = Rect::new(100, 200, 100, 10);
assert_eq!(
get_x_width_with_segment_size(
LastTakesRemainder,
vec![Percentage(50), Length(10), Percentage(50)],
target
),
[(100, 50), (150, 10), (160, 40)]
);
assert_eq!(
get_x_width_with_segment_size(
EvenDistribution,
vec![Percentage(50), Length(10), Percentage(50)],
target
),
[(100, 45), (145, 10), (155, 45)]
);
}

#[test]
fn test_rect_size_truncation() {
Expand Down Expand Up @@ -705,7 +808,7 @@ mod tests {
const _DEFAULT_LAYOUT: Layout = Layout::new()
.direction(Direction::Horizontal)
.margin(1)
.expand_to_fill(false);
.segment_size(SegmentSize::LastTakesRemainder);
const _HORIZONTAL_LAYOUT: Layout = Layout::new().horizontal_margin(1);
const _VERTICAL_LAYOUT: Layout = Layout::new().vertical_margin(1);
}
Expand Down
4 changes: 2 additions & 2 deletions src/widgets/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use unicode_width::UnicodeWidthStr;

use crate::{
buffer::Buffer,
layout::{Alignment, Constraint, Direction, Layout, Rect},
layout::{Alignment, Constraint, Direction, Layout, Rect, SegmentSize},
style::{Style, Styled},
text::Text,
widgets::{Block, StatefulWidget, Widget},
Expand Down Expand Up @@ -350,7 +350,7 @@ impl<'a> Table<'a> {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(constraints)
.expand_to_fill(false)
.segment_size(SegmentSize::None)
.split(Rect {
x: 0,
y: 0,
Expand Down

0 comments on commit 6153371

Please sign in to comment.