on 2158366 help sign in
helix/helix-tui/src/layout.rs
375 lines 11 kB history raw
1use std::cell::RefCell;
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)]
11pub enum Corner {
12 TopLeft,
13 TopRight,
14 BottomRight,
15 BottomLeft,
18#[derive(Debug, Hash, Clone, PartialEq, Eq)]
19pub enum Direction {
20 Horizontal,
21 Vertical,
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25pub enum Constraint {
26 // TODO: enforce range 0 - 100
27 Percentage(u16),
28 Ratio(u32, u32),
29 Length(u16),
30 Max(u16),
31 Min(u16),
34impl Constraint {
35 pub fn apply(&self, length: u16) -> u16 {
36 match *self {
37 Constraint::Percentage(p) => length * p / 100,
38 Constraint::Ratio(num, den) => {
39 let r = num * u32::from(length) / den;
40 r as u16
41 }
42 Constraint::Length(l) => length.min(l),
43 Constraint::Max(m) => length.min(m),
44 Constraint::Min(m) => length.max(m),
45 }
46 }
49#[derive(Debug, Clone, Copy, PartialEq)]
50pub enum Alignment {
51 Left,
52 Center,
53 Right,
56#[derive(Debug, Clone, PartialEq, Eq, Hash)]
57pub struct Layout {
58 direction: Direction,
59 margin: Margin,
60 constraints: Vec<Constraint>,
63thread_local! {
64 static LAYOUT_CACHE: RefCell<HashMap<(Rect, Layout), Vec<Rect>>> = RefCell::new(HashMap::new());
67impl Default for Layout {
68 fn default() -> Layout {
69 Layout {
70 direction: Direction::Vertical,
71 margin: Margin {
72 horizontal: 0,
73 vertical: 0,
74 },
75 constraints: Vec::new(),
76 }
77 }
80impl Layout {
81 pub fn constraints<C>(mut self, constraints: C) -> Layout
82 where
83 C: Into<Vec<Constraint>>,
84 {
85 self.constraints = constraints.into();
86 self
87 }
89 pub fn margin(mut self, margin: u16) -> Layout {
90 self.margin = Margin {
91 horizontal: margin,
92 vertical: margin,
93 };
94 self
95 }
97 pub fn horizontal_margin(mut self, horizontal: u16) -> Layout {
98 self.margin.horizontal = horizontal;
99 self
100 }
102 pub fn vertical_margin(mut self, vertical: u16) -> Layout {
103 self.margin.vertical = vertical;
104 self
105 }
107 pub fn direction(mut self, direction: Direction) -> Layout {
108 self.direction = direction;
109 self
110 }
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.
114 ///
115 /// # Examples
116 /// ```
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())
122 /// .split(Rect {
123 /// x: 2,
124 /// y: 2,
125 /// width: 10,
126 /// height: 10,
127 /// });
128 /// assert_eq!(
129 /// chunks,
130 /// vec![
131 /// Rect {
132 /// x: 2,
133 /// y: 2,
134 /// width: 10,
135 /// height: 5
136 /// },
137 /// Rect {
138 /// x: 2,
139 /// y: 7,
140 /// width: 10,
141 /// height: 5
142 /// }
143 /// ]
144 /// );
145 ///
146 /// let chunks = Layout::default()
147 /// .direction(Direction::Horizontal)
148 /// .constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)].as_ref())
149 /// .split(Rect {
150 /// x: 0,
151 /// y: 0,
152 /// width: 9,
153 /// height: 2,
154 /// });
155 /// assert_eq!(
156 /// chunks,
157 /// vec![
158 /// Rect {
159 /// x: 0,
160 /// y: 0,
161 /// width: 3,
162 /// height: 2
163 /// },
164 /// Rect {
165 /// x: 3,
166 /// y: 0,
167 /// width: 6,
168 /// height: 2
169 /// }
170 /// ]
171 /// );
172 /// ```
173 pub fn split(&self, area: Rect) -> Vec<Rect> {
174 // TODO: Maybe use a fixed size cache ?
175 LAYOUT_CACHE.with(|c| {
176 c.borrow_mut()
177 .entry((area, self.clone()))
178 .or_insert_with(|| split(area, self))
179 .clone()
180 })
181 }
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
188 .constraints
189 .iter()
190 .map(|_| Element::new())
191 .collect::<Vec<Element>>();
192 let mut results = layout
193 .constraints
194 .iter()
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));
204 }
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()));
214 }
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()),
219 });
220 }
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()),
225 });
226 }
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);
231 }
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)
239 }
240 Constraint::Ratio(n, d) => {
241 elements[i].width
242 | EQ(WEAK)
243 | (f64::from(dest_area.width) * f64::from(n) / f64::from(d))
244 }
245 Constraint::Min(v) => elements[i].width | GE(WEAK) | f64::from(v),
246 Constraint::Max(v) => elements[i].width | LE(WEAK) | f64::from(v),
247 });
248 }
249 }
250 Direction::Vertical => {
251 for pair in elements.windows(2) {
252 ccs.push((pair[0].y + pair[0].height) | EQ(REQUIRED) | pair[1].y);
253 }
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)
261 }
262 Constraint::Ratio(n, d) => {
263 elements[i].height
264 | EQ(WEAK)
265 | (f64::from(dest_area.height) * f64::from(n) / f64::from(d))
266 }
267 Constraint::Min(v) => elements[i].height | GE(WEAK) | f64::from(v),
268 Constraint::Max(v) => elements[i].height | LE(WEAK) | f64::from(v),
269 });
270 }
271 }
272 }
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() {
277 0
278 } else {
279 value as u16
280 };
281 match attr {
282 0 => {
283 results[index].x = value;
284 }
285 1 => {
286 results[index].y = value;
287 }
288 2 => {
289 results[index].width = value;
290 }
291 3 => {
292 results[index].height = value;
293 }
294 _ => {}
295 }
296 }
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;
303 }
304 Direction::Horizontal => {
305 last.width = dest_area.right() - last.x;
306 }
307 }
308 }
309 results
312/// A container used by the solver inside split
313struct Element {
314 x: Variable,
315 y: Variable,
316 width: Variable,
317 height: Variable,
320impl Element {
321 fn new() -> Element {
322 Element {
323 x: Variable::new(),
324 y: Variable::new(),
325 width: Variable::new(),
326 height: Variable::new(),
327 }
328 }
330 fn left(&self) -> Variable {
331 self.x
332 }
334 fn top(&self) -> Variable {
335 self.y
336 }
338 fn right(&self) -> Expression {
339 self.x + self.width
340 }
342 fn bottom(&self) -> Expression {
343 self.y + self.height
344 }
347#[cfg(test)]
348mod tests {
349 use super::*;
351 #[test]
352 fn test_vertical_split_by_height() {
353 let target = Rect {
354 x: 2,
355 y: 2,
356 width: 10,
357 height: 10,
358 };
360 let chunks = Layout::default()
361 .direction(Direction::Vertical)
362 .constraints(
363 [
364 Constraint::Percentage(10),
365 Constraint::Max(5),
366 Constraint::Min(1),
367 ]
368 .as_ref(),
369 )
370 .split(target);
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));
374 }