Skip to main content

tui_treelistview/
columns.rs

1use std::error::Error;
2use std::fmt::{self, Display, Formatter};
3
4use ratatui::style::Style;
5use ratatui::text::Line;
6use ratatui::widgets::{Cell, Row};
7use smallvec::SmallVec;
8
9use crate::context::TreeRowContext;
10use crate::model::TreeModel;
11
12/// An error produced while constructing a valid column width.
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum ColumnWidthError {
15    MinExceedsIdeal,
16    IdealExceedsMax,
17}
18
19impl Display for ColumnWidthError {
20    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
21        match self {
22            Self::MinExceedsIdeal => formatter.write_str("minimum width exceeds ideal width"),
23            Self::IdealExceedsMax => formatter.write_str("ideal width exceeds maximum width"),
24        }
25    }
26}
27
28impl Error for ColumnWidthError {}
29
30enum TreeColumnKind<'a, T: TreeModel> {
31    Tree,
32    Data(Box<dyn TreeCellRenderer<T> + 'a>),
33}
34
35/// An error produced while parsing a column set.
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum TreeColumnsError {
38    Empty,
39    MissingTreeColumn,
40    MultipleTreeColumns,
41}
42
43impl Display for TreeColumnsError {
44    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
45        match self {
46            Self::Empty => formatter.write_str("column set is empty"),
47            Self::MissingTreeColumn => formatter.write_str("tree column is missing"),
48            Self::MultipleTreeColumns => formatter.write_str("multiple tree columns are defined"),
49        }
50    }
51}
52
53impl Error for TreeColumnsError {}
54
55/// A validated width range satisfying `min <= ideal <= max`.
56#[derive(Clone, Copy, Debug, PartialEq, Eq)]
57pub struct ColumnWidth {
58    min: u16,
59    ideal: u16,
60    max: u16,
61}
62
63impl ColumnWidth {
64    /// Creates a width range after checking its invariants once.
65    ///
66    /// # Errors
67    ///
68    /// Returns [`ColumnWidthError`] unless `min <= ideal <= max`.
69    pub const fn new(min: u16, ideal: u16, max: u16) -> Result<Self, ColumnWidthError> {
70        if min > ideal {
71            return Err(ColumnWidthError::MinExceedsIdeal);
72        }
73        if ideal > max {
74            return Err(ColumnWidthError::IdealExceedsMax);
75        }
76        Ok(Self { min, ideal, max })
77    }
78
79    /// Creates a fixed-width column.
80    #[must_use]
81    pub const fn fixed(width: u16) -> Self {
82        Self {
83            min: width,
84            ideal: width,
85            max: width,
86        }
87    }
88
89    /// Creates a flexible column without an upper bound.
90    ///
91    /// # Errors
92    ///
93    /// Returns [`ColumnWidthError::MinExceedsIdeal`] when `min > ideal`.
94    pub const fn flexible(min: u16, ideal: u16) -> Result<Self, ColumnWidthError> {
95        Self::new(min, ideal, u16::MAX)
96    }
97
98    #[must_use]
99    pub const fn min(self) -> u16 {
100        self.min
101    }
102
103    #[must_use]
104    pub const fn ideal(self) -> u16 {
105        self.ideal
106    }
107
108    #[must_use]
109    pub const fn max(self) -> u16 {
110        self.max
111    }
112}
113
114struct OwnedCellRenderer<R>(R);
115
116impl<T, R> TreeCellRenderer<T> for OwnedCellRenderer<R>
117where
118    T: TreeModel,
119    R: Fn(&T, T::Id, &TreeRowContext<'_>) -> Cell<'static>,
120{
121    fn cell<'a>(&'a self, model: &'a T, id: T::Id, context: &TreeRowContext<'_>) -> Cell<'a> {
122        (self.0)(model, id, context)
123    }
124}
125
126/// A column definition. Exactly one column in a set must have the tree role.
127pub struct ColumnDef<'a, T: TreeModel> {
128    header: Line<'a>,
129    width: ColumnWidth,
130    kind: TreeColumnKind<'a, T>,
131}
132
133impl<'a, T: TreeModel> ColumnDef<'a, T> {
134    /// Creates the primary tree column.
135    #[must_use]
136    pub fn tree(header: impl Into<Line<'a>>, width: ColumnWidth) -> Self {
137        Self {
138            header: header.into(),
139            width,
140            kind: TreeColumnKind::Tree,
141        }
142    }
143
144    /// Creates an additional column whose renderer may borrow model data without allocation.
145    ///
146    /// Function items and custom [`TreeCellRenderer`] implementations can return cells tied to
147    /// the model borrow. Use [`Self::data_owned`] for an ergonomic capturing closure.
148    #[must_use]
149    pub fn data<R>(header: impl Into<Line<'a>>, width: ColumnWidth, renderer: R) -> Self
150    where
151        R: TreeCellRenderer<T> + 'a,
152    {
153        Self {
154            header: header.into(),
155            width,
156            kind: TreeColumnKind::Data(Box::new(renderer)),
157        }
158    }
159
160    /// Creates an additional column from a capturing closure that returns an owned cell.
161    ///
162    /// Use [`Self::data`] when the returned cell borrows model data. This variant is convenient
163    /// for closures that capture formatting configuration and produce owned text.
164    #[must_use]
165    pub fn data_owned<R>(header: impl Into<Line<'a>>, width: ColumnWidth, renderer: R) -> Self
166    where
167        R: Fn(&T, T::Id, &TreeRowContext<'_>) -> Cell<'static> + 'a,
168    {
169        Self::data(header, width, OwnedCellRenderer(renderer))
170    }
171}
172
173/// A dynamic column set parsed and validated once at construction.
174pub struct TreeColumnSet<'a, T: TreeModel> {
175    columns: Vec<ColumnDef<'a, T>>,
176    tree_column: usize,
177    header_style: Style,
178    show_header: bool,
179}
180
181impl<'a, T: TreeModel> TreeColumnSet<'a, T> {
182    /// Validates the set and records its single tree column.
183    ///
184    /// # Errors
185    ///
186    /// Returns [`TreeColumnsError`] when the set is empty or does not contain exactly one tree
187    /// column.
188    pub fn new(
189        columns: impl IntoIterator<Item = ColumnDef<'a, T>>,
190    ) -> Result<Self, TreeColumnsError> {
191        let columns: Vec<_> = columns.into_iter().collect();
192        if columns.is_empty() {
193            return Err(TreeColumnsError::Empty);
194        }
195
196        let mut tree_columns = columns.iter().enumerate().filter_map(|(index, column)| {
197            matches!(column.kind, TreeColumnKind::Tree).then_some(index)
198        });
199        let Some(tree_column) = tree_columns.next() else {
200            return Err(TreeColumnsError::MissingTreeColumn);
201        };
202        if tree_columns.next().is_some() {
203            return Err(TreeColumnsError::MultipleTreeColumns);
204        }
205
206        Ok(Self {
207            columns,
208            tree_column,
209            header_style: Style::default(),
210            show_header: true,
211        })
212    }
213
214    /// Sets the header style.
215    #[must_use]
216    pub const fn header_style(mut self, style: Style) -> Self {
217        self.header_style = style;
218        self
219    }
220
221    /// Hides the header.
222    #[must_use]
223    pub const fn without_header(mut self) -> Self {
224        self.show_header = false;
225        self
226    }
227
228    fn total_width(&self, width: impl Fn(ColumnWidth) -> u16) -> u16 {
229        self.columns
230            .iter()
231            .fold(0, |sum, column| sum.saturating_add(width(column.width)))
232    }
233}
234
235impl<T: TreeModel> TreeColumns<T> for TreeColumnSet<'_, T> {
236    fn column_count(&self) -> usize {
237        self.columns.len()
238    }
239
240    fn tree_column_index(&self) -> usize {
241        self.tree_column
242    }
243
244    fn minimum_width(&self) -> u16 {
245        self.total_width(ColumnWidth::min)
246    }
247
248    fn ideal_width(&self) -> u16 {
249        self.total_width(ColumnWidth::ideal)
250    }
251
252    fn widths(&self, available: u16) -> SmallVec<[u16; 8]> {
253        distribute_widths(available, self.columns.iter().map(|column| column.width))
254    }
255
256    fn header(&self) -> Option<Row<'_>> {
257        self.show_header.then(|| {
258            Row::new(self.columns.iter().map(|column| column.header.clone()))
259                .style(self.header_style)
260        })
261    }
262
263    fn header_height(&self) -> u16 {
264        u16::from(self.show_header)
265    }
266
267    fn cells<'a>(
268        &'a self,
269        model: &'a T,
270        id: T::Id,
271        context: &TreeRowContext<'_>,
272        tree_cell: Cell<'a>,
273    ) -> SmallVec<[Cell<'a>; 8]> {
274        let mut tree_cell = Some(tree_cell);
275        self.columns
276            .iter()
277            .map(|column| match &column.kind {
278                TreeColumnKind::Tree => tree_cell.take().unwrap_or_default(),
279                TreeColumnKind::Data(renderer) => renderer.cell(model, id, context),
280            })
281            .collect()
282    }
283}
284
285/// Renders an additional cell with full row context.
286pub trait TreeCellRenderer<T: TreeModel> {
287    fn cell<'a>(&'a self, model: &'a T, id: T::Id, context: &TreeRowContext<'_>) -> Cell<'a>;
288}
289
290impl<T, F> TreeCellRenderer<T> for F
291where
292    T: TreeModel,
293    F: for<'a> Fn(&'a T, T::Id, &TreeRowContext<'_>) -> Cell<'a>,
294{
295    fn cell<'a>(&'a self, model: &'a T, id: T::Id, context: &TreeRowContext<'_>) -> Cell<'a> {
296        self(model, id, context)
297    }
298}
299
300/// Lays out columns and builds every cell in a row.
301pub trait TreeColumns<T: TreeModel> {
302    fn column_count(&self) -> usize;
303    fn tree_column_index(&self) -> usize;
304    fn minimum_width(&self) -> u16;
305    fn ideal_width(&self) -> u16;
306    fn widths(&self, available: u16) -> SmallVec<[u16; 8]>;
307    fn header(&self) -> Option<Row<'_>>;
308    fn header_height(&self) -> u16 {
309        u16::from(self.header().is_some())
310    }
311    fn cells<'a>(
312        &'a self,
313        model: &'a T,
314        id: T::Id,
315        context: &TreeRowContext<'_>,
316        tree_cell: Cell<'a>,
317    ) -> SmallVec<[Cell<'a>; 8]>;
318}
319
320/// Distributes width as evenly as possible between `min`, `ideal`, and `max`.
321///
322/// A remainder smaller than the number of growable columns is assigned in column order.
323#[must_use]
324pub fn distribute_widths(
325    total: u16,
326    columns: impl IntoIterator<Item = ColumnWidth>,
327) -> SmallVec<[u16; 8]> {
328    let columns: SmallVec<[ColumnWidth; 8]> = columns.into_iter().collect();
329    let mut widths: SmallVec<[u16; 8]> = columns.iter().map(|column| column.min).collect();
330    let minimum = widths.iter().copied().fold(0_u16, u16::saturating_add);
331    let mut remaining = total.saturating_sub(minimum);
332    grow_towards(&mut widths, &columns, &mut remaining, |column| column.ideal);
333    grow_towards(&mut widths, &columns, &mut remaining, |column| column.max);
334    widths
335}
336
337fn grow_towards(
338    widths: &mut [u16],
339    columns: &[ColumnWidth],
340    remaining: &mut u16,
341    target: impl Fn(ColumnWidth) -> u16,
342) {
343    while *remaining > 0 {
344        let active = widths
345            .iter()
346            .zip(columns)
347            .filter(|(width, column)| **width < target(**column))
348            .count();
349        if active == 0 {
350            return;
351        }
352
353        let active = u16::try_from(active).unwrap_or(u16::MAX);
354        let share = (*remaining / active).max(1);
355        let mut spent = 0_u16;
356        for (width, column) in widths.iter_mut().zip(columns) {
357            if *remaining == 0 {
358                break;
359            }
360            let add = target(*column)
361                .saturating_sub(*width)
362                .min(share)
363                .min(*remaining);
364            *width = width.saturating_add(add);
365            *remaining = remaining.saturating_sub(add);
366            spent = spent.saturating_add(add);
367        }
368        if spent == 0 {
369            return;
370        }
371    }
372}
373
374#[cfg(test)]
375mod tests {
376    use super::*;
377
378    #[test]
379    fn column_width_rejects_invalid_ranges() {
380        assert_eq!(
381            ColumnWidth::new(5, 4, 6),
382            Err(ColumnWidthError::MinExceedsIdeal)
383        );
384        assert_eq!(
385            ColumnWidth::new(2, 7, 6),
386            Err(ColumnWidthError::IdealExceedsMax)
387        );
388    }
389
390    #[test]
391    fn distribution_is_balanced_and_bounded() {
392        let width = ColumnWidth::new(2, 4, 6).expect("valid width");
393        let widths = distribute_widths(10, [width, width, width]);
394        assert_eq!(widths.as_slice(), &[4, 3, 3]);
395
396        let widths = distribute_widths(30, [width, width, width]);
397        assert_eq!(widths.as_slice(), &[6, 6, 6]);
398    }
399
400    #[test]
401    fn distribution_preserves_bounds_for_every_available_width() {
402        let columns = [
403            ColumnWidth::new(1, 4, 9).expect("valid width"),
404            ColumnWidth::new(3, 5, 7).expect("valid width"),
405            ColumnWidth::new(2, 8, 12).expect("valid width"),
406        ];
407        for total in 0..=40 {
408            let widths = distribute_widths(total, columns);
409            for (width, column) in widths.iter().zip(columns) {
410                assert!(*width >= column.min());
411                assert!(*width <= column.max());
412            }
413            let actual = widths.iter().copied().sum::<u16>();
414            assert_eq!(actual, total.clamp(6, 28));
415        }
416    }
417}