1use ratatui::layout::{Constraint, Rect};
2use ratatui::style::Style;
3use ratatui::widgets::{Cell, Row};
4use smallvec::SmallVec;
5
6use crate::model::TreeModel;
7
8pub trait TreeColumns<T: TreeModel> {
9 fn label_constraint(&self) -> Constraint;
10 fn other_constraints(&self) -> &[Constraint];
11 fn header(&self) -> Option<Row<'_>> {
12 None
13 }
14 fn cells<'a>(&'a self, model: &'a T, id: T::Id) -> SmallVec<[Cell<'a>; 8]>;
15 fn constraints_for_area(&self, _area: Rect) -> SmallVec<[Constraint; 8]> {
16 let mut constraints = SmallVec::<[Constraint; 8]>::new();
17 constraints.push(self.label_constraint());
18 constraints.extend_from_slice(self.other_constraints());
19 constraints
20 }
21}
22
23pub struct TreeColumnsLayout<const N: usize> {
24 label: Constraint,
25 other: [Constraint; N],
26}
27
28impl<const N: usize> TreeColumnsLayout<N> {
29 pub const fn new(label: Constraint, other: [Constraint; N]) -> Self {
30 Self { label, other }
31 }
32
33 pub const fn label(&self) -> Constraint {
34 self.label
35 }
36
37 pub const fn other(&self) -> &[Constraint] {
38 &self.other
39 }
40}
41
42pub type ColumnFn<T> = for<'a> fn(&'a T, <T as TreeModel>::Id) -> Cell<'a>;
43
44#[derive(Clone, Copy)]
45pub struct ColumnDef<T: TreeModel> {
46 pub header: &'static str,
47 pub constraint: Constraint,
48 pub cell: ColumnFn<T>,
49}
50
51impl<T: TreeModel> ColumnDef<T> {
52 pub const fn new(header: &'static str, constraint: Constraint, cell: ColumnFn<T>) -> Self {
53 Self {
54 header,
55 constraint,
56 cell,
57 }
58 }
59}
60
61pub struct SimpleColumns<const N: usize, T: TreeModel> {
62 label_constraint: Constraint,
63 label_header: &'static str,
64 columns: [ColumnDef<T>; N],
65 constraints: [Constraint; N],
66 header_style: Style,
67 show_header: bool,
68}
69
70impl<const N: usize, T: TreeModel> SimpleColumns<N, T> {
71 pub fn new(
72 label_constraint: Constraint,
73 label_header: &'static str,
74 columns: [ColumnDef<T>; N],
75 ) -> Self {
76 let constraints = std::array::from_fn(|idx| columns[idx].constraint);
77 Self {
78 label_constraint,
79 label_header,
80 columns,
81 constraints,
82 header_style: Style::default(),
83 show_header: true,
84 }
85 }
86
87 pub const fn header_style(mut self, style: Style) -> Self {
88 self.header_style = style;
89 self
90 }
91
92 pub const fn without_header(mut self) -> Self {
93 self.show_header = false;
94 self
95 }
96}
97
98impl<const N: usize, T: TreeModel> TreeColumns<T> for SimpleColumns<N, T> {
99 fn label_constraint(&self) -> Constraint {
100 self.label_constraint
101 }
102
103 fn other_constraints(&self) -> &[Constraint] {
104 &self.constraints
105 }
106
107 fn header(&self) -> Option<Row<'_>> {
108 if !self.show_header {
109 return None;
110 }
111
112 let mut cells = SmallVec::<[Cell; 8]>::new();
113 cells.push(Cell::from(self.label_header));
114 for column in &self.columns {
115 cells.push(Cell::from(column.header));
116 }
117
118 Some(Row::new(cells).style(self.header_style))
119 }
120
121 fn cells<'a>(&'a self, model: &'a T, id: T::Id) -> SmallVec<[Cell<'a>; 8]> {
122 let mut cells = SmallVec::<[Cell<'a>; 8]>::new();
123 for column in &self.columns {
124 cells.push((column.cell)(model, id));
125 }
126 cells
127 }
128}
129
130#[derive(Clone, Copy, Debug)]
131pub struct ColumnWidth {
132 pub min: u16,
133 pub ideal: u16,
134 pub max: u16,
135}
136
137impl ColumnWidth {
138 pub const fn fixed(width: u16) -> Self {
139 Self {
140 min: width,
141 ideal: width,
142 max: width,
143 }
144 }
145}
146
147pub fn distribute_widths(total: u16, columns: &[ColumnWidth]) -> SmallVec<[u16; 8]> {
148 let mut widths = SmallVec::<[u16; 8]>::with_capacity(columns.len());
149 let mut min_sum: u16 = 0;
150 for col in columns {
151 min_sum = min_sum.saturating_add(col.min);
152 widths.push(col.min);
153 }
154
155 let mut remaining = total.saturating_sub(min_sum);
156 if remaining == 0 {
157 return widths;
158 }
159
160 for (idx, col) in columns.iter().enumerate() {
161 if remaining == 0 {
162 break;
163 }
164 let target = col.ideal.max(col.min);
165 let add = target.saturating_sub(widths[idx]).min(remaining);
166 widths[idx] = widths[idx].saturating_add(add);
167 remaining = remaining.saturating_sub(add);
168 }
169
170 for (idx, col) in columns.iter().enumerate() {
171 if remaining == 0 {
172 break;
173 }
174 let add = col.max.saturating_sub(widths[idx]).min(remaining);
175 widths[idx] = widths[idx].saturating_add(add);
176 remaining = remaining.saturating_sub(add);
177 }
178
179 widths
180}
181
182pub struct AdaptiveColumns<const N: usize, T: TreeModel> {
183 label_header: &'static str,
184 label_width: ColumnWidth,
185 columns: [ColumnDef<T>; N],
186 column_widths: [ColumnWidth; N],
187 fallback_constraints: [Constraint; N],
188 header_style: Style,
189 show_header: bool,
190}
191
192impl<const N: usize, T: TreeModel> AdaptiveColumns<N, T> {
193 pub fn new(
194 label_width: ColumnWidth,
195 label_header: &'static str,
196 columns: [ColumnDef<T>; N],
197 column_widths: [ColumnWidth; N],
198 ) -> Self {
199 let fallback_constraints =
200 std::array::from_fn(|idx| Constraint::Length(column_widths[idx].ideal));
201 Self {
202 label_header,
203 label_width,
204 columns,
205 column_widths,
206 fallback_constraints,
207 header_style: Style::default(),
208 show_header: true,
209 }
210 }
211
212 pub const fn header_style(mut self, style: Style) -> Self {
213 self.header_style = style;
214 self
215 }
216
217 pub const fn without_header(mut self) -> Self {
218 self.show_header = false;
219 self
220 }
221}
222
223impl<const N: usize, T: TreeModel> TreeColumns<T> for AdaptiveColumns<N, T> {
224 fn label_constraint(&self) -> Constraint {
225 Constraint::Length(self.label_width.ideal)
226 }
227
228 fn other_constraints(&self) -> &[Constraint] {
229 &self.fallback_constraints
230 }
231
232 fn header(&self) -> Option<Row<'_>> {
233 if !self.show_header {
234 return None;
235 }
236
237 let mut cells = SmallVec::<[Cell; 8]>::new();
238 cells.push(Cell::from(self.label_header));
239 for column in &self.columns {
240 cells.push(Cell::from(column.header));
241 }
242
243 Some(Row::new(cells).style(self.header_style))
244 }
245
246 fn cells<'a>(&'a self, model: &'a T, id: T::Id) -> SmallVec<[Cell<'a>; 8]> {
247 let mut cells = SmallVec::<[Cell<'a>; 8]>::new();
248 for column in &self.columns {
249 cells.push((column.cell)(model, id));
250 }
251 cells
252 }
253
254 fn constraints_for_area(&self, area: Rect) -> SmallVec<[Constraint; 8]> {
255 let mut widths = SmallVec::<[ColumnWidth; 8]>::new();
256 widths.push(self.label_width);
257 widths.extend_from_slice(&self.column_widths);
258
259 let raw_widths = distribute_widths(area.width, &widths);
260 let mut constraints = SmallVec::<[Constraint; 8]>::new();
261 for width in raw_widths {
262 constraints.push(Constraint::Length(width));
263 }
264 constraints
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271 use ratatui::layout::{Constraint, Rect};
272
273 #[test]
274 fn distribute_widths_respects_min_ideal_max() {
275 let columns = [
276 ColumnWidth {
277 min: 4,
278 ideal: 6,
279 max: 8,
280 },
281 ColumnWidth {
282 min: 4,
283 ideal: 4,
284 max: 6,
285 },
286 ];
287 let widths = distribute_widths(12, &columns);
288 assert_eq!(widths.as_slice(), &[8, 4]);
289 }
290
291 #[test]
292 fn adaptive_columns_sum_to_area_width() {
293 fn cell_stub(_: &TestModel, _: usize) -> Cell<'_> {
294 Cell::from("")
295 }
296
297 struct TestModel;
298 impl TreeModel for TestModel {
299 type Id = usize;
300
301 fn root(&self) -> Option<Self::Id> {
302 None
303 }
304
305 fn children(&self, _id: Self::Id) -> &[Self::Id] {
306 &[]
307 }
308
309 fn contains(&self, _id: Self::Id) -> bool {
310 false
311 }
312 }
313
314 let columns = [
315 ColumnDef::new("A", Constraint::Length(4), cell_stub),
316 ColumnDef::new("B", Constraint::Length(4), cell_stub),
317 ];
318 let widths = [
319 ColumnWidth {
320 min: 4,
321 ideal: 6,
322 max: 8,
323 },
324 ColumnWidth {
325 min: 4,
326 ideal: 6,
327 max: 8,
328 },
329 ];
330 let layout = AdaptiveColumns::new(
331 ColumnWidth {
332 min: 6,
333 ideal: 8,
334 max: 10,
335 },
336 "Name",
337 columns,
338 widths,
339 );
340 let constraints = layout.constraints_for_area(Rect::new(0, 0, 20, 1));
341
342 let total: u16 = constraints
343 .iter()
344 .map(|c| match c {
345 Constraint::Length(len) => *len,
346 _ => 0,
347 })
348 .sum();
349 assert_eq!(total, 20);
350 }
351}