|
1 | 1 | //! Layout engine for terminal |
2 | 2 |
|
3 | | -use std::cell::RefCell; |
4 | | -use std::collections::HashMap; |
| 3 | +mod direction; |
| 4 | +pub use direction::Direction; |
5 | 5 |
|
6 | | -use cassowary::strength::{REQUIRED, WEAK}; |
7 | | -use cassowary::WeightedRelation::*; |
8 | | -use cassowary::{Constraint as CassowaryConstraint, Expression, Solver, Variable}; |
| 6 | +mod constraint; |
| 7 | +pub use constraint::Constraint; |
9 | 8 |
|
10 | | -use helix_view::graphics::{Margin, Rect}; |
| 9 | +#[allow(clippy::module_inception)] |
| 10 | +mod layout; |
| 11 | +pub use layout::Layout; |
11 | 12 |
|
12 | | -/// Enum of all corners |
13 | | -#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)] |
14 | | -pub enum Corner { |
15 | | - TopLeft, |
16 | | - TopRight, |
17 | | - BottomRight, |
18 | | - BottomLeft, |
19 | | -} |
20 | | - |
21 | | -/// Direction a [Rect] should be split |
22 | | -#[derive(Debug, Hash, Clone, PartialEq, Eq)] |
23 | | -pub enum Direction { |
24 | | - Horizontal, |
25 | | - Vertical, |
26 | | -} |
27 | | - |
28 | | -/// Describes requirements of a [Rect] to be split |
29 | | -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] |
30 | | -pub enum Constraint { |
31 | | - // TODO: enforce range 0 - 100 |
32 | | - Percentage(u16), |
33 | | - Ratio(u32, u32), |
34 | | - Length(u16), |
35 | | - Max(u16), |
36 | | - Min(u16), |
37 | | -} |
38 | | - |
39 | | -impl Constraint { |
40 | | - pub fn apply(&self, length: u16) -> u16 { |
41 | | - match *self { |
42 | | - Constraint::Percentage(p) => length * p / 100, |
43 | | - Constraint::Ratio(num, den) => { |
44 | | - let r = num * u32::from(length) / den; |
45 | | - r as u16 |
46 | | - } |
47 | | - Constraint::Length(l) => length.min(l), |
48 | | - Constraint::Max(m) => length.min(m), |
49 | | - Constraint::Min(m) => length.max(m), |
50 | | - } |
51 | | - } |
52 | | -} |
53 | | - |
54 | | -/// How content should be aligned |
55 | | -#[derive(Debug, Clone, Copy, PartialEq, Eq)] |
56 | | -pub enum Alignment { |
57 | | - Left, |
58 | | - Center, |
59 | | - Right, |
60 | | -} |
61 | | - |
62 | | -/// Description of a how a [Rect] should be split |
63 | | -#[derive(Debug, Clone, PartialEq, Eq, Hash)] |
64 | | -pub struct Layout { |
65 | | - direction: Direction, |
66 | | - margin: Margin, |
67 | | - constraints: Vec<Constraint>, |
68 | | -} |
69 | | - |
70 | | -thread_local! { |
71 | | - static LAYOUT_CACHE: RefCell<HashMap<(Rect, Layout), Vec<Rect>>> = RefCell::new(HashMap::new()); |
72 | | -} |
73 | | - |
74 | | -impl Default for Layout { |
75 | | - fn default() -> Layout { |
76 | | - Layout { |
77 | | - direction: Direction::Vertical, |
78 | | - margin: Margin::none(), |
79 | | - constraints: Vec::new(), |
80 | | - } |
81 | | - } |
82 | | -} |
83 | | - |
84 | | -impl Layout { |
85 | | - /// Returns a layout with the given [Constraint]s. |
86 | | - pub fn constraints<C>(mut self, constraints: C) -> Layout |
87 | | - where |
88 | | - C: Into<Vec<Constraint>>, |
89 | | - { |
90 | | - self.constraints = constraints.into(); |
91 | | - self |
92 | | - } |
93 | | - |
94 | | - /// Returns a layout wit the given margins on all sides. |
95 | | - pub const fn margin(mut self, margin: u16) -> Layout { |
96 | | - self.margin = Margin::all(margin); |
97 | | - self |
98 | | - } |
99 | | - |
100 | | - /// Returns a layout with the given horizontal margins. |
101 | | - pub const fn horizontal_margin(mut self, horizontal: u16) -> Layout { |
102 | | - self.margin.horizontal = horizontal; |
103 | | - self |
104 | | - } |
105 | | - |
106 | | - /// Returns a layout with the given vertical margins. |
107 | | - pub const fn vertical_margin(mut self, vertical: u16) -> Layout { |
108 | | - self.margin.vertical = vertical; |
109 | | - self |
110 | | - } |
111 | | - |
112 | | - /// Returns a layout with the given [Direction]. |
113 | | - pub const fn direction(mut self, direction: Direction) -> Layout { |
114 | | - self.direction = direction; |
115 | | - self |
116 | | - } |
117 | | - |
118 | | - /// Wrapper function around the cassowary-rs solver to be able to split a given |
119 | | - /// area into smaller ones based on the preferred widths or heights and the direction. |
120 | | - /// |
121 | | - /// # Examples |
122 | | - /// ``` |
123 | | - /// # use helix_tui::layout::{Constraint, Direction, Layout}; |
124 | | - /// # use helix_view::graphics::Rect; |
125 | | - /// let chunks = Layout::default() |
126 | | - /// .direction(Direction::Vertical) |
127 | | - /// .constraints([Constraint::Length(5), Constraint::Min(0)].as_ref()) |
128 | | - /// .split(Rect { |
129 | | - /// x: 2, |
130 | | - /// y: 2, |
131 | | - /// width: 10, |
132 | | - /// height: 10, |
133 | | - /// }); |
134 | | - /// assert_eq!( |
135 | | - /// chunks, |
136 | | - /// vec![ |
137 | | - /// Rect { |
138 | | - /// x: 2, |
139 | | - /// y: 2, |
140 | | - /// width: 10, |
141 | | - /// height: 5 |
142 | | - /// }, |
143 | | - /// Rect { |
144 | | - /// x: 2, |
145 | | - /// y: 7, |
146 | | - /// width: 10, |
147 | | - /// height: 5 |
148 | | - /// } |
149 | | - /// ] |
150 | | - /// ); |
151 | | - /// |
152 | | - /// let chunks = Layout::default() |
153 | | - /// .direction(Direction::Horizontal) |
154 | | - /// .constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)].as_ref()) |
155 | | - /// .split(Rect { |
156 | | - /// x: 0, |
157 | | - /// y: 0, |
158 | | - /// width: 9, |
159 | | - /// height: 2, |
160 | | - /// }); |
161 | | - /// assert_eq!( |
162 | | - /// chunks, |
163 | | - /// vec![ |
164 | | - /// Rect { |
165 | | - /// x: 0, |
166 | | - /// y: 0, |
167 | | - /// width: 3, |
168 | | - /// height: 2 |
169 | | - /// }, |
170 | | - /// Rect { |
171 | | - /// x: 3, |
172 | | - /// y: 0, |
173 | | - /// width: 6, |
174 | | - /// height: 2 |
175 | | - /// } |
176 | | - /// ] |
177 | | - /// ); |
178 | | - /// ``` |
179 | | - pub fn split(&self, area: Rect) -> Vec<Rect> { |
180 | | - // TODO: Maybe use a fixed size cache ? |
181 | | - LAYOUT_CACHE.with(|c| { |
182 | | - c.borrow_mut() |
183 | | - .entry((area, self.clone())) |
184 | | - .or_insert_with(|| split(area, self)) |
185 | | - .clone() |
186 | | - }) |
187 | | - } |
188 | | -} |
189 | | - |
190 | | -fn split(area: Rect, layout: &Layout) -> Vec<Rect> { |
191 | | - let mut solver = Solver::new(); |
192 | | - let mut vars: HashMap<Variable, (usize, usize)> = HashMap::new(); |
193 | | - let elements = layout |
194 | | - .constraints |
195 | | - .iter() |
196 | | - .map(|_| Element::new()) |
197 | | - .collect::<Vec<Element>>(); |
198 | | - let mut results = layout |
199 | | - .constraints |
200 | | - .iter() |
201 | | - .map(|_| Rect::default()) |
202 | | - .collect::<Vec<Rect>>(); |
203 | | - |
204 | | - let dest_area = area.inner(layout.margin); |
205 | | - for (i, e) in elements.iter().enumerate() { |
206 | | - vars.insert(e.x, (i, 0)); |
207 | | - vars.insert(e.y, (i, 1)); |
208 | | - vars.insert(e.width, (i, 2)); |
209 | | - vars.insert(e.height, (i, 3)); |
210 | | - } |
211 | | - let mut ccs: Vec<CassowaryConstraint> = |
212 | | - Vec::with_capacity(elements.len() * 4 + layout.constraints.len() * 6); |
213 | | - for elt in &elements { |
214 | | - ccs.push(elt.width | GE(REQUIRED) | 0f64); |
215 | | - ccs.push(elt.height | GE(REQUIRED) | 0f64); |
216 | | - ccs.push(elt.left() | GE(REQUIRED) | f64::from(dest_area.left())); |
217 | | - ccs.push(elt.top() | GE(REQUIRED) | f64::from(dest_area.top())); |
218 | | - ccs.push(elt.right() | LE(REQUIRED) | f64::from(dest_area.right())); |
219 | | - ccs.push(elt.bottom() | LE(REQUIRED) | f64::from(dest_area.bottom())); |
220 | | - } |
221 | | - if let Some(first) = elements.first() { |
222 | | - ccs.push(match layout.direction { |
223 | | - Direction::Horizontal => first.left() | EQ(REQUIRED) | f64::from(dest_area.left()), |
224 | | - Direction::Vertical => first.top() | EQ(REQUIRED) | f64::from(dest_area.top()), |
225 | | - }); |
226 | | - } |
227 | | - if let Some(last) = elements.last() { |
228 | | - ccs.push(match layout.direction { |
229 | | - Direction::Horizontal => last.right() | EQ(REQUIRED) | f64::from(dest_area.right()), |
230 | | - Direction::Vertical => last.bottom() | EQ(REQUIRED) | f64::from(dest_area.bottom()), |
231 | | - }); |
232 | | - } |
233 | | - match layout.direction { |
234 | | - Direction::Horizontal => { |
235 | | - for pair in elements.windows(2) { |
236 | | - ccs.push((pair[0].x + pair[0].width) | EQ(REQUIRED) | pair[1].x); |
237 | | - } |
238 | | - for (i, size) in layout.constraints.iter().enumerate() { |
239 | | - ccs.push(elements[i].y | EQ(REQUIRED) | f64::from(dest_area.y)); |
240 | | - ccs.push(elements[i].height | EQ(REQUIRED) | f64::from(dest_area.height)); |
241 | | - ccs.push(match *size { |
242 | | - Constraint::Length(v) => elements[i].width | EQ(WEAK) | f64::from(v), |
243 | | - Constraint::Percentage(v) => { |
244 | | - elements[i].width | EQ(WEAK) | (f64::from(v * dest_area.width) / 100.0) |
245 | | - } |
246 | | - Constraint::Ratio(n, d) => { |
247 | | - elements[i].width |
248 | | - | EQ(WEAK) |
249 | | - | (f64::from(dest_area.width) * f64::from(n) / f64::from(d)) |
250 | | - } |
251 | | - Constraint::Min(v) => elements[i].width | GE(WEAK) | f64::from(v), |
252 | | - Constraint::Max(v) => elements[i].width | LE(WEAK) | f64::from(v), |
253 | | - }); |
254 | | - } |
255 | | - } |
256 | | - Direction::Vertical => { |
257 | | - for pair in elements.windows(2) { |
258 | | - ccs.push((pair[0].y + pair[0].height) | EQ(REQUIRED) | pair[1].y); |
259 | | - } |
260 | | - for (i, size) in layout.constraints.iter().enumerate() { |
261 | | - ccs.push(elements[i].x | EQ(REQUIRED) | f64::from(dest_area.x)); |
262 | | - ccs.push(elements[i].width | EQ(REQUIRED) | f64::from(dest_area.width)); |
263 | | - ccs.push(match *size { |
264 | | - Constraint::Length(v) => elements[i].height | EQ(WEAK) | f64::from(v), |
265 | | - Constraint::Percentage(v) => { |
266 | | - elements[i].height | EQ(WEAK) | (f64::from(v * dest_area.height) / 100.0) |
267 | | - } |
268 | | - Constraint::Ratio(n, d) => { |
269 | | - elements[i].height |
270 | | - | EQ(WEAK) |
271 | | - | (f64::from(dest_area.height) * f64::from(n) / f64::from(d)) |
272 | | - } |
273 | | - Constraint::Min(v) => elements[i].height | GE(WEAK) | f64::from(v), |
274 | | - Constraint::Max(v) => elements[i].height | LE(WEAK) | f64::from(v), |
275 | | - }); |
276 | | - } |
277 | | - } |
278 | | - } |
279 | | - solver.add_constraints(&ccs).unwrap(); |
280 | | - for &(var, value) in solver.fetch_changes() { |
281 | | - let (index, attr) = vars[&var]; |
282 | | - let value = if value.is_sign_negative() { |
283 | | - 0 |
284 | | - } else { |
285 | | - value as u16 |
286 | | - }; |
287 | | - match attr { |
288 | | - 0 => { |
289 | | - results[index].x = value; |
290 | | - } |
291 | | - 1 => { |
292 | | - results[index].y = value; |
293 | | - } |
294 | | - 2 => { |
295 | | - results[index].width = value; |
296 | | - } |
297 | | - 3 => { |
298 | | - results[index].height = value; |
299 | | - } |
300 | | - _ => {} |
301 | | - } |
302 | | - } |
303 | | - |
304 | | - // Fix imprecision by extending the last item a bit if necessary |
305 | | - if let Some(last) = results.last_mut() { |
306 | | - match layout.direction { |
307 | | - Direction::Vertical => { |
308 | | - last.height = dest_area.bottom() - last.y; |
309 | | - } |
310 | | - Direction::Horizontal => { |
311 | | - last.width = dest_area.right() - last.x; |
312 | | - } |
313 | | - } |
314 | | - } |
315 | | - results |
316 | | -} |
317 | | - |
318 | | -/// A container used by the solver inside split |
319 | | -struct Element { |
320 | | - x: Variable, |
321 | | - y: Variable, |
322 | | - width: Variable, |
323 | | - height: Variable, |
324 | | -} |
325 | | - |
326 | | -impl Element { |
327 | | - fn new() -> Element { |
328 | | - Element { |
329 | | - x: Variable::new(), |
330 | | - y: Variable::new(), |
331 | | - width: Variable::new(), |
332 | | - height: Variable::new(), |
333 | | - } |
334 | | - } |
335 | | - |
336 | | - fn left(&self) -> Variable { |
337 | | - self.x |
338 | | - } |
339 | | - |
340 | | - fn top(&self) -> Variable { |
341 | | - self.y |
342 | | - } |
343 | | - |
344 | | - fn right(&self) -> Expression { |
345 | | - self.x + self.width |
346 | | - } |
347 | | - |
348 | | - fn bottom(&self) -> Expression { |
349 | | - self.y + self.height |
350 | | - } |
351 | | -} |
352 | | - |
353 | | -#[cfg(test)] |
354 | | -mod tests { |
355 | | - use super::*; |
356 | | - |
357 | | - #[test] |
358 | | - fn test_vertical_split_by_height() { |
359 | | - let target = Rect { |
360 | | - x: 2, |
361 | | - y: 2, |
362 | | - width: 10, |
363 | | - height: 10, |
364 | | - }; |
365 | | - |
366 | | - let chunks = Layout::default() |
367 | | - .direction(Direction::Vertical) |
368 | | - .constraints( |
369 | | - [ |
370 | | - Constraint::Percentage(10), |
371 | | - Constraint::Max(5), |
372 | | - Constraint::Min(1), |
373 | | - ] |
374 | | - .as_ref(), |
375 | | - ) |
376 | | - .split(target); |
377 | | - |
378 | | - assert_eq!(target.height, chunks.iter().map(|r| r.height).sum::<u16>()); |
379 | | - chunks.windows(2).for_each(|w| assert!(w[0].y <= w[1].y)); |
380 | | - } |
381 | | -} |
| 13 | +mod alignment; |
| 14 | +pub use alignment::Alignment; |
0 commit comments