Skip to main content

ctl_layout/
table.rs

1//! A table of rows and columns.
2
3use ctl_text::{Context, Line, Measurement, Overflow, Renderable, Text};
4
5use crate::column::Column;
6use crate::frame::{Frame, RowSeparation};
7use crate::glyphs::Glyphs;
8use crate::lines::{literal, push_cell, rule_line};
9use crate::widths::{Extent, Framing, solve};
10
11/// Cells between two columns of an unframed table.
12const GAP: u16 = 2;
13
14/// A table of rows under column headers.
15///
16/// A table measures its columns, gives each one a width, and renders every
17/// cell at its column's width:
18///
19/// 1. Each column starts at the widest cell it holds.
20/// 2. When those widths and the frame fit, the table is exactly that wide.
21/// 3. Otherwise the widest column that can still shrink narrows by 1 cell of
22///    width, again and again. A keep-words column never goes below its widest
23///    word, and a fold column never goes below its widest cluster. An ellipsis
24///    column can shrink to a width of 1 cell.
25/// 4. When the narrowest widths still do not fit, an unframed table lets its
26///    keep-words columns run past the width. A [`Frame::Grid`] table folds
27///    those columns instead, so its frame stays aligned. It folds only the
28///    columns that can fold alone to make it fit, so a short id keeps its word
29///    beside a long URL.
30/// 5. Each cell renders at its column's width. An ellipsis cell ends in `…`,
31///    and a numeric column aligns right.
32/// 6. Each column then narrows to its widest rendered line. After a column
33///    wraps, its widest line fills its width, so no blank strip runs down the
34///    whole column. A table sized to its content draws no wider than it needs.
35///    An ellipsis column that cut a cell keeps its width, since the cut cell
36///    fills it.
37///
38/// Narrowing never changes the lines: text wrapped at a width wraps the same
39/// at its own widest line.
40///
41/// A table with no rows and no header draws nothing and measures 0.
42#[derive(Clone, Debug)]
43pub struct Table {
44    columns: Vec<Column>,
45    /// The headers of the columns, moved out of them, with each column's
46    /// overflow applied. `None` when no column has a header.
47    header: Option<Vec<Text>>,
48    /// Every row has one cell per column, each with its column's overflow.
49    rows: Vec<Vec<Text>>,
50    frame: Frame,
51    separation: RowSeparation,
52}
53
54impl Table {
55    /// A table with `columns` and no rows, drawn as a [`Frame::Grid`] with no
56    /// separation between rows.
57    #[must_use]
58    pub fn new(columns: impl IntoIterator<Item = Column>) -> Self {
59        Self::from_columns(columns.into_iter().collect())
60    }
61
62    fn from_columns(mut columns: Vec<Column>) -> Self {
63        let titled = columns.iter().any(|column| column.header.is_some());
64        let header: Option<Vec<Text>> = titled.then(|| {
65            columns
66                .iter_mut()
67                .map(|column| {
68                    column
69                        .header
70                        .take()
71                        .unwrap_or_default()
72                        .overflow(column.overflow)
73                })
74                .collect()
75        });
76        Self {
77            columns,
78            header,
79            rows: Vec::new(),
80            frame: Frame::default(),
81            separation: RowSeparation::default(),
82        }
83    }
84
85    /// Sets what the table draws around and between its columns, replacing
86    /// the default [`Frame::Grid`].
87    #[must_use]
88    pub fn frame(mut self, frame: Frame) -> Self {
89        self.frame = frame;
90        self
91    }
92
93    /// Sets what separates consecutive rows, replacing the default
94    /// [`RowSeparation::None`].
95    #[must_use]
96    pub fn row_separation(mut self, separation: RowSeparation) -> Self {
97        self.separation = separation;
98        self
99    }
100
101    /// Adds a row of `&str`, `String`, [`Text`], or anything else that
102    /// converts to a [`Text`].
103    ///
104    /// The column's overflow replaces the cell's own. A row with fewer cells
105    /// than the table has columns gets empty cells, and cells past the last
106    /// column are dropped.
107    #[must_use]
108    pub fn row<C: Into<Text>>(self, cells: impl IntoIterator<Item = C>) -> Self {
109        self.push_row(cells.into_iter().map(Into::into).collect())
110    }
111
112    /// Adds every row of `rows` in order, as [`row`](Self::row) adds one.
113    #[must_use]
114    pub fn rows<C, R>(self, rows: impl IntoIterator<Item = R>) -> Self
115    where
116        R: IntoIterator<Item = C>,
117        C: Into<Text>,
118    {
119        rows.into_iter().fold(self, Table::row)
120    }
121
122    fn push_row(mut self, mut cells: Vec<Text>) -> Self {
123        cells.resize_with(self.columns.len(), Text::new);
124        let cells = cells
125            .into_iter()
126            .zip(&self.columns)
127            .map(|(cell, column)| cell.overflow(column.overflow))
128            .collect();
129        self.rows.push(cells);
130        self
131    }
132
133    /// The rows, so a [`Record`](crate::Record) can change its frame without
134    /// losing them.
135    pub(crate) fn into_rows(self) -> Vec<Vec<Text>> {
136        self.rows
137    }
138
139    /// Whether there is nothing to draw: no columns, or neither a header nor
140    /// a row. A grid would otherwise draw an empty box.
141    fn is_blank(&self) -> bool {
142        self.columns.is_empty() || (self.header.is_none() && self.rows.is_empty())
143    }
144
145    /// Cells taken by the frame and the gaps, for the columns it has.
146    fn frame_width(&self) -> u16 {
147        let count = u16::try_from(self.columns.len()).unwrap_or(u16::MAX);
148        match self.frame.framing() {
149            Framing::Framed => count.saturating_mul(3).saturating_add(1),
150            Framing::Unframed => count.saturating_sub(1).saturating_mul(GAP),
151        }
152    }
153
154    /// The header row, when there is one, then every row.
155    fn grid(&self) -> impl Iterator<Item = &[Text]> {
156        self.header.iter().chain(&self.rows).map(Vec::as_slice)
157    }
158
159    /// The overflow a cell of `column` renders with. A grid folds, so a
160    /// keep-words column folds a word wider than itself and the frame stays
161    /// aligned.
162    fn cell_overflow(&self, column: &Column) -> Overflow {
163        match self.frame.framing() {
164            Framing::Framed if column.overflow == Overflow::KeepWords => Overflow::Fold,
165            Framing::Framed | Framing::Unframed => column.overflow,
166        }
167    }
168
169    fn extents(&self, context: &Context) -> Vec<Extent> {
170        let framing = self.frame.framing();
171        self.columns
172            .iter()
173            .enumerate()
174            .map(|(index, column)| {
175                let mut extent = Extent {
176                    natural: 0,
177                    minimum: 0,
178                    floor: 0,
179                };
180                for cell in self.grid().filter_map(|row| row.get(index)) {
181                    let measured = cell.measure(context);
182                    let floor = match framing {
183                        Framing::Framed if column.overflow != Overflow::Fold => cell
184                            .measure_as(context, Overflow::Fold)
185                            .minimum()
186                            .min(measured.minimum()),
187                        Framing::Framed | Framing::Unframed => measured.minimum(),
188                    };
189                    extent.natural = extent.natural.max(measured.maximum());
190                    extent.minimum = extent.minimum.max(measured.minimum());
191                    extent.floor = extent.floor.max(floor);
192                }
193                extent
194            })
195            .collect()
196    }
197
198    /// Rows of cells rendered at `widths`.
199    fn render_cells(&self, widths: &[u16], context: &Context) -> Vec<Vec<Vec<Line>>> {
200        self.grid()
201            .map(|row| {
202                row.iter()
203                    .zip(widths)
204                    .zip(&self.columns)
205                    .map(|((cell, width), column)| {
206                        let context = context.at_width(*width);
207                        cell.render_as(&context, self.cell_overflow(column))
208                    })
209                    .collect()
210            })
211            .collect()
212    }
213
214    fn row_lines(&self, cells: &[Vec<Line>], widths: &[u16], glyphs: &Glyphs) -> Vec<Line> {
215        let height = cells.iter().map(Vec::len).max().unwrap_or(0).max(1);
216        let empty = Line::new();
217        let framing = self.frame.framing();
218        let (edge, gap, padding) = match framing {
219            Framing::Framed => (
220                glyphs.outer.to_string(),
221                glyphs.inner.to_string(),
222                " ".to_owned(),
223            ),
224            Framing::Unframed => (String::new(), " ".repeat(usize::from(GAP)), String::new()),
225        };
226        (0..height)
227            .map(|index| {
228                let mut line = Line::from(literal(&edge));
229                for (column, (width, spec)) in widths.iter().zip(&self.columns).enumerate() {
230                    if column > 0 {
231                        line.push(literal(&gap));
232                    }
233                    line.push(literal(&padding));
234                    let cell = cells
235                        .get(column)
236                        .and_then(|lines| lines.get(index))
237                        .unwrap_or(&empty);
238                    push_cell(&mut line, cell, *width, spec.align);
239                    line.push(literal(&padding));
240                }
241                match framing {
242                    Framing::Framed => line.push(literal(&edge)),
243                    Framing::Unframed => line.trim_end(),
244                }
245                line
246            })
247            .collect()
248    }
249
250    fn compose(&self, cells: &[Vec<Vec<Line>>], widths: &[u16], glyphs: &Glyphs) -> Vec<Line> {
251        let header = self.header.is_some();
252        let framing = self.frame.framing();
253        let total = widths
254            .iter()
255            .fold(self.frame_width(), |sum, width| sum.saturating_add(*width));
256        let blank_cells = vec![Vec::new(); widths.len()];
257        let mut lines = Vec::new();
258        match framing {
259            Framing::Framed => lines.push(glyphs.top.line(widths)),
260            Framing::Unframed => {}
261        }
262        for (index, row) in cells.iter().enumerate() {
263            if header && index == 1 {
264                match self.frame {
265                    Frame::Grid => lines.push(glyphs.header.line(widths)),
266                    Frame::HeaderRule => lines.push(rule_line(glyphs.rule, total)),
267                    Frame::Plain => {}
268                }
269            } else if index > 0 {
270                match (self.separation, framing) {
271                    (RowSeparation::Rule, Framing::Framed) => lines.push(glyphs.row.line(widths)),
272                    (RowSeparation::Rule, Framing::Unframed) => {
273                        lines.push(rule_line(glyphs.rule, total));
274                    }
275                    (RowSeparation::Blank, Framing::Framed | Framing::Unframed) => {
276                        lines.extend(self.row_lines(&blank_cells, widths, glyphs));
277                    }
278                    (RowSeparation::None, Framing::Framed | Framing::Unframed) => {}
279                }
280            }
281            lines.extend(self.row_lines(row, widths, glyphs));
282        }
283        match framing {
284            Framing::Framed => lines.push(glyphs.bottom.line(widths)),
285            Framing::Unframed => {}
286        }
287        lines
288    }
289}
290
291/// Each of `widths` lowered to the widest line its column rendered, step 6 of
292/// [`Table`]. A width never grows: a word kept whole past its column's width
293/// runs past it as before.
294fn narrowed(cells: &[Vec<Vec<Line>>], widths: &[u16]) -> Vec<u16> {
295    widths
296        .iter()
297        .enumerate()
298        .map(|(index, width)| {
299            cells
300                .iter()
301                .filter_map(|row| row.get(index))
302                .flatten()
303                .map(Line::width)
304                .max()
305                .unwrap_or(0)
306                .min(*width)
307        })
308        .collect()
309}
310
311impl Renderable for Table {
312    fn measure(&self, context: &Context) -> Measurement {
313        if self.is_blank() {
314            return Measurement::exact(0);
315        }
316        let extents = self.extents(context);
317        let frame = u64::from(self.frame_width());
318        let sum = |value: fn(&Extent) -> u16| {
319            extents.iter().fold(frame, |total, extent| {
320                total.saturating_add(u64::from(value(extent)))
321            })
322        };
323        let narrowest = match self.frame.framing() {
324            Framing::Framed => sum(|extent| extent.floor),
325            Framing::Unframed => sum(|extent| extent.minimum),
326        };
327        let natural = sum(|extent| extent.natural);
328        Measurement::new(
329            u16::try_from(narrowest).unwrap_or(u16::MAX),
330            u16::try_from(natural).unwrap_or(u16::MAX),
331        )
332    }
333
334    fn render(&self, context: &Context) -> Vec<Line> {
335        if self.is_blank() {
336            return Vec::new();
337        }
338        let extents = self.extents(context);
339        let available = context.width().saturating_sub(self.frame_width());
340        let widths = solve(&extents, available, self.frame.framing());
341        let cells = self.render_cells(&widths, context);
342        let widths = narrowed(&cells, &widths);
343        let glyphs = Glyphs::for_chars(context.box_chars());
344        self.compose(&cells, &widths, &glyphs)
345    }
346}