use std::cell::RefCell; use std::collections::HashMap; use cassowary::strength::{REQUIRED, WEAK}; use cassowary::WeightedRelation::*; use cassowary::{Constraint as CassowaryConstraint, Expression, Solver, Variable}; use helix_view::graphics::{Margin, Rect}; #[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)] pub enum Corner { TopLeft, TopRight, BottomRight, BottomLeft, } #[derive(Debug, Hash, Clone, PartialEq, Eq)] pub enum Direction { Horizontal, Vertical, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Constraint { // TODO: enforce range 0 - 100 Percentage(u16), Ratio(u32, u32), Length(u16), Max(u16), Min(u16), } impl Constraint { pub fn apply(&self, length: u16) -> u16 { match *self { Constraint::Percentage(p) => length * p / 100, Constraint::Ratio(num, den) => { let r = num * u32::from(length) / den; r as u16 } Constraint::Length(l) => length.min(l), Constraint::Max(m) => length.min(m), Constraint::Min(m) => length.max(m), } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Alignment { Left, Center, Right, } #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Layout { direction: Direction, margin: Margin, constraints: Vec, } thread_local! { static LAYOUT_CACHE: RefCell>> = RefCell::new(HashMap::new()); } impl Default for Layout { fn default() -> Layout { Layout { direction: Direction::Vertical, margin: Margin::none(), constraints: Vec::new(), } } } impl Layout { pub fn constraints(mut self, constraints: C) -> Layout where C: Into>, { self.constraints = constraints.into(); self } pub const fn margin(mut self, margin: u16) -> Layout { self.margin = Margin::all(margin); self } pub const fn horizontal_margin(mut self, horizontal: u16) -> Layout { self.margin.horizontal = horizontal; self } pub const fn vertical_margin(mut self, vertical: u16) -> Layout { self.margin.vertical = vertical; self } pub const fn direction(mut self, direction: Direction) -> Layout { self.direction = direction; self } /// Wrapper function around the cassowary-rs solver to be able to split a given /// area into smaller ones based on the preferred widths or heights and the direction. /// /// # Examples /// ``` /// # use helix_tui::layout::{Constraint, Direction, Layout}; /// # use helix_view::graphics::Rect; /// let chunks = Layout::default() /// .direction(Direction::Vertical) /// .constraints([Constraint::Length(5), Constraint::Min(0)].as_ref()) /// .split(Rect { /// x: 2, /// y: 2, /// width: 10, /// height: 10, /// }); /// assert_eq!( /// chunks, /// vec![ /// Rect { /// x: 2, /// y: 2, /// width: 10, /// height: 5 /// }, /// Rect { /// x: 2, /// y: 7, /// width: 10, /// height: 5 /// } /// ] /// ); /// /// let chunks = Layout::default() /// .direction(Direction::Horizontal) /// .constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)].as_ref()) /// .split(Rect { /// x: 0, /// y: 0, /// width: 9, /// height: 2, /// }); /// assert_eq!( /// chunks, /// vec![ /// Rect { /// x: 0, /// y: 0, /// width: 3, /// height: 2 /// }, /// Rect { /// x: 3, /// y: 0, /// width: 6, /// height: 2 /// } /// ] /// ); /// ``` pub fn split(&self, area: Rect) -> Vec { // TODO: Maybe use a fixed size cache ? LAYOUT_CACHE.with(|c| { c.borrow_mut() .entry((area, self.clone())) .or_insert_with(|| split(area, self)) .clone() }) } } fn split(area: Rect, layout: &Layout) -> Vec { let mut solver = Solver::new(); let mut vars: HashMap = HashMap::new(); let elements = layout .constraints .iter() .map(|_| Element::new()) .collect::>(); let mut results = layout .constraints .iter() .map(|_| Rect::default()) .collect::>(); let dest_area = area.inner(layout.margin); for (i, e) in elements.iter().enumerate() { vars.insert(e.x, (i, 0)); vars.insert(e.y, (i, 1)); vars.insert(e.width, (i, 2)); vars.insert(e.height, (i, 3)); } let mut ccs: Vec = Vec::with_capacity(elements.len() * 4 + layout.constraints.len() * 6); for elt in &elements { ccs.push(elt.width | GE(REQUIRED) | 0f64); ccs.push(elt.height | GE(REQUIRED) | 0f64); ccs.push(elt.left() | GE(REQUIRED) | f64::from(dest_area.left())); ccs.push(elt.top() | GE(REQUIRED) | f64::from(dest_area.top())); ccs.push(elt.right() | LE(REQUIRED) | f64::from(dest_area.right())); ccs.push(elt.bottom() | LE(REQUIRED) | f64::from(dest_area.bottom())); } if let Some(first) = elements.first() { ccs.push(match layout.direction { Direction::Horizontal => first.left() | EQ(REQUIRED) | f64::from(dest_area.left()), Direction::Vertical => first.top() | EQ(REQUIRED) | f64::from(dest_area.top()), }); } if let Some(last) = elements.last() { ccs.push(match layout.direction { Direction::Horizontal => last.right() | EQ(REQUIRED) | f64::from(dest_area.right()), Direction::Vertical => last.bottom() | EQ(REQUIRED) | f64::from(dest_area.bottom()), }); } match layout.direction { Direction::Horizontal => { for pair in elements.windows(2) { ccs.push((pair[0].x + pair[0].width) | EQ(REQUIRED) | pair[1].x); } for (i, size) in layout.constraints.iter().enumerate() { ccs.push(elements[i].y | EQ(REQUIRED) | f64::from(dest_area.y)); ccs.push(elements[i].height | EQ(REQUIRED) | f64::from(dest_area.height)); ccs.push(match *size { Constraint::Length(v) => elements[i].width | EQ(WEAK) | f64::from(v), Constraint::Percentage(v) => { elements[i].width | EQ(WEAK) | (f64::from(v * dest_area.width) / 100.0) } Constraint::Ratio(n, d) => { elements[i].width | EQ(WEAK) | (f64::from(dest_area.width) * f64::from(n) / f64::from(d)) } Constraint::Min(v) => elements[i].width | GE(WEAK) | f64::from(v), Constraint::Max(v) => elements[i].width | LE(WEAK) | f64::from(v), }); } } Direction::Vertical => { for pair in elements.windows(2) { ccs.push((pair[0].y + pair[0].height) | EQ(REQUIRED) | pair[1].y); } for (i, size) in layout.constraints.iter().enumerate() { ccs.push(elements[i].x | EQ(REQUIRED) | f64::from(dest_area.x)); ccs.push(elements[i].width | EQ(REQUIRED) | f64::from(dest_area.width)); ccs.push(match *size { Constraint::Length(v) => elements[i].height | EQ(WEAK) | f64::from(v), Constraint::Percentage(v) => { elements[i].height | EQ(WEAK) | (f64::from(v * dest_area.height) / 100.0) } Constraint::Ratio(n, d) => { elements[i].height | EQ(WEAK) | (f64::from(dest_area.height) * f64::from(n) / f64::from(d)) } Constraint::Min(v) => elements[i].height | GE(WEAK) | f64::from(v), Constraint::Max(v) => elements[i].height | LE(WEAK) | f64::from(v), }); } } } solver.add_constraints(&ccs).unwrap(); for &(var, value) in solver.fetch_changes() { let (index, attr) = vars[&var]; let value = if value.is_sign_negative() { 0 } else { value as u16 }; match attr { 0 => { results[index].x = value; } 1 => { results[index].y = value; } 2 => { results[index].width = value; } 3 => { results[index].height = value; } _ => {} } } // Fix imprecision by extending the last item a bit if necessary if let Some(last) = results.last_mut() { match layout.direction { Direction::Vertical => { last.height = dest_area.bottom() - last.y; } Direction::Horizontal => { last.width = dest_area.right() - last.x; } } } results } /// A container used by the solver inside split struct Element { x: Variable, y: Variable, width: Variable, height: Variable, } impl Element { fn new() -> Element { Element { x: Variable::new(), y: Variable::new(), width: Variable::new(), height: Variable::new(), } } fn left(&self) -> Variable { self.x } fn top(&self) -> Variable { self.y } fn right(&self) -> Expression { self.x + self.width } fn bottom(&self) -> Expression { self.y + self.height } } #[cfg(test)] mod tests { use super::*; #[test] fn test_vertical_split_by_height() { let target = Rect { x: 2, y: 2, width: 10, height: 10, }; let chunks = Layout::default() .direction(Direction::Vertical) .constraints( [ Constraint::Percentage(10), Constraint::Max(5), Constraint::Min(1), ] .as_ref(), ) .split(target); assert_eq!(target.height, chunks.iter().map(|r| r.height).sum::()); chunks.windows(2).for_each(|w| assert!(w[0].y <= w[1].y)); } }