tui_treelistview/
columns.rs

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}