2use std::collections::HashMap;
4use cassowary::strength::{REQUIRED, WEAK};
5use cassowary::WeightedRelation::*;
6use cassowary::{Constraint as CassowaryConstraint, Expression, Solver, Variable};
8use helix_view::graphics::{Margin, Rect};
10#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)]
18#[derive(Debug, Hash, Clone, PartialEq, Eq)]
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26 // TODO: enforce range 0 - 100
35 pub fn apply(&self, length: u16) -> u16 {
37 Constraint::Percentage(p) => length * p / 100,
38 Constraint::Ratio(num, den) => {
39 let r = num * u32::from(length) / den;
42 Constraint::Length(l) => length.min(l),
43 Constraint::Max(m) => length.min(m),
44 Constraint::Min(m) => length.max(m),
49#[derive(Debug, Clone, Copy, PartialEq)]
56#[derive(Debug, Clone, PartialEq, Eq, Hash)]
60 constraints: Vec<Constraint>,
64 static LAYOUT_CACHE: RefCell<HashMap<(Rect, Layout), Vec<Rect>>> = RefCell::new(HashMap::new());
67impl Default for Layout {
68 fn default() -> Layout {
70 direction: Direction::Vertical,
75 constraints: Vec::new(),
81 pub fn constraints<C>(mut self, constraints: C) -> Layout
83 C: Into<Vec<Constraint>>,
85 self.constraints = constraints.into();
89 pub fn margin(mut self, margin: u16) -> Layout {
90 self.margin = Margin {
97 pub fn horizontal_margin(mut self, horizontal: u16) -> Layout {
98 self.margin.horizontal = horizontal;
102 pub fn vertical_margin(mut self, vertical: u16) -> Layout {
103 self.margin.vertical = vertical;
107 pub fn direction(mut self, direction: Direction) -> Layout {
108 self.direction = direction;
112 /// Wrapper function around the cassowary-rs solver to be able to split a given
113 /// area into smaller ones based on the preferred widths or heights and the direction.
117 /// # use helix_tui::layout::{Constraint, Direction, Layout};
118 /// # use helix_view::graphics::Rect;
119 /// let chunks = Layout::default()
120 /// .direction(Direction::Vertical)
121 /// .constraints([Constraint::Length(5), Constraint::Min(0)].as_ref())
146 /// let chunks = Layout::default()
147 /// .direction(Direction::Horizontal)
148 /// .constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)].as_ref())
173 pub fn split(&self, area: Rect) -> Vec<Rect> {
174 // TODO: Maybe use a fixed size cache ?
175 LAYOUT_CACHE.with(|c| {
177 .entry((area, self.clone()))
178 .or_insert_with(|| split(area, self))
184fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
185 let mut solver = Solver::new();
186 let mut vars: HashMap<Variable, (usize, usize)> = HashMap::new();
187 let elements = layout
190 .map(|_| Element::new())
191 .collect::<Vec<Element>>();
192 let mut results = layout
195 .map(|_| Rect::default())
196 .collect::<Vec<Rect>>();
198 let dest_area = area.inner(&layout.margin);
199 for (i, e) in elements.iter().enumerate() {
200 vars.insert(e.x, (i, 0));
201 vars.insert(e.y, (i, 1));
202 vars.insert(e.width, (i, 2));
203 vars.insert(e.height, (i, 3));
205 let mut ccs: Vec<CassowaryConstraint> =
206 Vec::with_capacity(elements.len() * 4 + layout.constraints.len() * 6);
207 for elt in &elements {
208 ccs.push(elt.width | GE(REQUIRED) | 0f64);
209 ccs.push(elt.height | GE(REQUIRED) | 0f64);
210 ccs.push(elt.left() | GE(REQUIRED) | f64::from(dest_area.left()));
211 ccs.push(elt.top() | GE(REQUIRED) | f64::from(dest_area.top()));
212 ccs.push(elt.right() | LE(REQUIRED) | f64::from(dest_area.right()));
213 ccs.push(elt.bottom() | LE(REQUIRED) | f64::from(dest_area.bottom()));
215 if let Some(first) = elements.first() {
216 ccs.push(match layout.direction {
217 Direction::Horizontal => first.left() | EQ(REQUIRED) | f64::from(dest_area.left()),
218 Direction::Vertical => first.top() | EQ(REQUIRED) | f64::from(dest_area.top()),
221 if let Some(last) = elements.last() {
222 ccs.push(match layout.direction {
223 Direction::Horizontal => last.right() | EQ(REQUIRED) | f64::from(dest_area.right()),
224 Direction::Vertical => last.bottom() | EQ(REQUIRED) | f64::from(dest_area.bottom()),
227 match layout.direction {
228 Direction::Horizontal => {
229 for pair in elements.windows(2) {
230 ccs.push((pair[0].x + pair[0].width) | EQ(REQUIRED) | pair[1].x);
232 for (i, size) in layout.constraints.iter().enumerate() {
233 ccs.push(elements[i].y | EQ(REQUIRED) | f64::from(dest_area.y));
234 ccs.push(elements[i].height | EQ(REQUIRED) | f64::from(dest_area.height));
235 ccs.push(match *size {
236 Constraint::Length(v) => elements[i].width | EQ(WEAK) | f64::from(v),
237 Constraint::Percentage(v) => {
238 elements[i].width | EQ(WEAK) | (f64::from(v * dest_area.width) / 100.0)
240 Constraint::Ratio(n, d) => {
243 | (f64::from(dest_area.width) * f64::from(n) / f64::from(d))
245 Constraint::Min(v) => elements[i].width | GE(WEAK) | f64::from(v),
246 Constraint::Max(v) => elements[i].width | LE(WEAK) | f64::from(v),
250 Direction::Vertical => {
251 for pair in elements.windows(2) {
252 ccs.push((pair[0].y + pair[0].height) | EQ(REQUIRED) | pair[1].y);
254 for (i, size) in layout.constraints.iter().enumerate() {
255 ccs.push(elements[i].x | EQ(REQUIRED) | f64::from(dest_area.x));
256 ccs.push(elements[i].width | EQ(REQUIRED) | f64::from(dest_area.width));
257 ccs.push(match *size {
258 Constraint::Length(v) => elements[i].height | EQ(WEAK) | f64::from(v),
259 Constraint::Percentage(v) => {
260 elements[i].height | EQ(WEAK) | (f64::from(v * dest_area.height) / 100.0)
262 Constraint::Ratio(n, d) => {
265 | (f64::from(dest_area.height) * f64::from(n) / f64::from(d))
267 Constraint::Min(v) => elements[i].height | GE(WEAK) | f64::from(v),
268 Constraint::Max(v) => elements[i].height | LE(WEAK) | f64::from(v),
273 solver.add_constraints(&ccs).unwrap();
274 for &(var, value) in solver.fetch_changes() {
275 let (index, attr) = vars[&var];
276 let value = if value.is_sign_negative() {
283 results[index].x = value;
286 results[index].y = value;
289 results[index].width = value;
292 results[index].height = value;
298 // Fix imprecision by extending the last item a bit if necessary
299 if let Some(last) = results.last_mut() {
300 match layout.direction {
301 Direction::Vertical => {
302 last.height = dest_area.bottom() - last.y;
304 Direction::Horizontal => {
305 last.width = dest_area.right() - last.x;
312/// A container used by the solver inside split
321 fn new() -> Element {
325 width: Variable::new(),
326 height: Variable::new(),
330 fn left(&self) -> Variable {
334 fn top(&self) -> Variable {
338 fn right(&self) -> Expression {
342 fn bottom(&self) -> Expression {
352 fn test_vertical_split_by_height() {
360 let chunks = Layout::default()
361 .direction(Direction::Vertical)
364 Constraint::Percentage(10),
372 assert_eq!(target.height, chunks.iter().map(|r| r.height).sum::<u16>());
373 chunks.windows(2).for_each(|w| assert!(w[0].y <= w[1].y));
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list