Skip to content

Commit 786d3fd

Browse files
committed
refactor(helix-tui): split layout.rs into submodules
1 parent a828b85 commit 786d3fd

File tree

7 files changed

+387
-376
lines changed

7 files changed

+387
-376
lines changed

helix-tui/src/layout.rs

Lines changed: 9 additions & 376 deletions
Original file line numberDiff line numberDiff line change
@@ -1,381 +1,14 @@
11
//! Layout engine for terminal
22
3-
use std::cell::RefCell;
4-
use std::collections::HashMap;
3+
mod direction;
4+
pub use direction::Direction;
55

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;
98

10-
use helix_view::graphics::{Margin, Rect};
9+
#[allow(clippy::module_inception)]
10+
mod layout;
11+
pub use layout::Layout;
1112

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;

helix-tui/src/layout/alignment.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/// How content should be aligned
2+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3+
pub enum Alignment {
4+
Left,
5+
Center,
6+
Right,
7+
}

0 commit comments

Comments
 (0)