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#[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#[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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
57pub struct ColumnWidth {
58 min: u16,
59 ideal: u16,
60 max: u16,
61}
62
63impl ColumnWidth {
64 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 #[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 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
126pub 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 #[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 #[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 #[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
173pub 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 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 #[must_use]
216 pub const fn header_style(mut self, style: Style) -> Self {
217 self.header_style = style;
218 self
219 }
220
221 #[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
285pub 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
300pub 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#[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}