1use katui_scene::placement::CellRect;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum Constraint {
13 Length(u16),
15 Percentage(u8),
17 Fill,
19}
20
21#[must_use]
23pub fn split_horizontal(area: CellRect, constraints: &[Constraint]) -> Vec<CellRect> {
24 let sizes = resolve(u32::from(area.columns), constraints);
25 let mut x = area.x;
26 sizes
27 .into_iter()
28 .map(|width| {
29 let rect = CellRect {
30 x,
31 y: area.y,
32 columns: width,
33 rows: area.rows,
34 };
35 x = x.saturating_add(width);
36 rect
37 })
38 .collect()
39}
40
41#[must_use]
43pub fn split_vertical(area: CellRect, constraints: &[Constraint]) -> Vec<CellRect> {
44 let sizes = resolve(u32::from(area.rows), constraints);
45 let mut y = area.y;
46 sizes
47 .into_iter()
48 .map(|height| {
49 let rect = CellRect {
50 x: area.x,
51 y,
52 columns: area.columns,
53 rows: height,
54 };
55 y = y.saturating_add(height);
56 rect
57 })
58 .collect()
59}
60
61fn resolve(whole: u32, constraints: &[Constraint]) -> Vec<u16> {
62 let whole = whole.min(u32::from(u16::MAX));
63 let mut sizes = Vec::with_capacity(constraints.len());
64 let mut fill_indexes = Vec::new();
65 let mut used = 0u32;
66 for (index, constraint) in constraints.iter().enumerate() {
67 match constraint {
68 Constraint::Length(cells) => {
69 used = used.saturating_add(u32::from(*cells));
70 sizes.push(*cells);
71 }
72 Constraint::Percentage(percent) => {
73 let size = whole * u32::from(*percent) / 100;
74 used = used.saturating_add(size);
75 sizes.push(u16::try_from(size).unwrap_or(u16::MAX));
76 }
77 Constraint::Fill => {
78 fill_indexes.push(index);
79 sizes.push(0);
80 }
81 }
82 }
83 if !fill_indexes.is_empty() {
84 let mut rest = whole.saturating_sub(used.min(whole));
85 let share = u32::try_from(fill_indexes.len()).unwrap_or(1);
86 let each = rest / share;
87 for &index in &fill_indexes {
88 if let Some(slot) = sizes.get_mut(index) {
89 *slot = u16::try_from(each).unwrap_or(u16::MAX);
90 }
91 rest = rest.saturating_sub(each);
92 }
93 for &index in &fill_indexes {
95 if rest == 0 {
96 break;
97 }
98 if let Some(slot) = sizes.get_mut(index) {
99 *slot = (*slot).saturating_add(1);
100 }
101 rest -= 1;
102 }
103 }
104 sizes
105}
106
107#[cfg(test)]
108mod tests {
109 use super::{Constraint, split_horizontal, split_vertical};
110 use katui_scene::placement::CellRect;
111
112 fn area(columns: u16, rows: u16) -> CellRect {
113 CellRect {
114 x: 0,
115 y: 0,
116 columns,
117 rows,
118 }
119 }
120
121 fn widths(rects: &[CellRect]) -> Vec<u16> {
122 rects.iter().map(|rect| rect.columns).collect()
123 }
124
125 fn heights(rects: &[CellRect]) -> Vec<u16> {
126 rects.iter().map(|rect| rect.rows).collect()
127 }
128
129 #[test]
130 fn lengths_hold_their_cells() {
131 let rects = split_horizontal(
132 area(20, 5),
133 &[
134 Constraint::Length(5),
135 Constraint::Fill,
136 Constraint::Length(3),
137 ],
138 );
139 assert_eq!(widths(&rects), vec![5, 12, 3]);
140 assert_eq!(
141 rects[2].x + rects[2].columns,
142 20,
143 "the split reaches the edge"
144 );
145 }
146
147 #[test]
148 fn percentages_round_down_and_fills_take_the_rest() {
149 let rects = split_horizontal(
150 area(13, 4),
151 &[
152 Constraint::Percentage(50),
153 Constraint::Fill,
154 Constraint::Fill,
155 ],
156 );
157 assert_eq!(widths(&rects), vec![6, 4, 3]);
158 }
159
160 #[test]
161 fn fills_share_a_leftover_cell_to_the_earliest() {
162 let rects = split_horizontal(
163 area(11, 4),
164 &[Constraint::Fill, Constraint::Fill, Constraint::Length(4)],
165 );
166 assert_eq!(widths(&rects), vec![4, 3, 4]);
167 }
168
169 #[test]
170 fn vertical_splits_stack_rows() {
171 let rects = split_vertical(
172 area(30, 10),
173 &[
174 Constraint::Length(1),
175 Constraint::Fill,
176 Constraint::Length(2),
177 ],
178 );
179 assert_eq!(heights(&rects), vec![1, 7, 2]);
180 assert_eq!(rects[1].y, 1);
181 assert_eq!(rects[2].y, 8);
182 }
183
184 #[test]
185 fn splits_start_at_the_areas_origin() {
186 let rects = split_horizontal(
187 CellRect {
188 x: 5,
189 y: 7,
190 columns: 10,
191 rows: 3,
192 },
193 &[Constraint::Fill, Constraint::Fill],
194 );
195 assert_eq!(rects[0].x, 5);
196 assert_eq!(rects[1].x, 10);
197 assert_eq!(rects[0].rows, 3);
198 }
199
200 #[test]
201 fn an_overlong_claim_saturates_instead_of_wrapping() {
202 let rects = split_horizontal(area(5, 2), &[Constraint::Length(9), Constraint::Fill]);
203 assert_eq!(widths(&rects), vec![9, 0]);
204 }
205}