term_data_table/
cell.rs

1use itertools::Itertools;
2use lazy_static;
3use regex::Regex;
4use std::{borrow::Cow, cell::RefCell, fmt, iter};
5use unicode_width::UnicodeWidthChar;
6
7use unicode_linebreak::{linebreaks, BreakOpportunity};
8use unicode_width::UnicodeWidthStr;
9
10/// Represents the horizontal alignment of content within a cell.
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub enum Alignment {
13    Left,
14    Right,
15    Center,
16}
17
18///A table cell containing some str content.
19///
20///A cell may span multiple columns by setting the value of `col_span`.
21///
22///`pad_content` will add a space to either side of the cell's content.
23#[derive(Debug, Clone)]
24pub struct Cell<'txt> {
25    pub(crate) content: Cow<'txt, str>,
26    pub(crate) col_span: usize,
27    pub(crate) alignment: Alignment,
28    pub(crate) pad_content: bool,
29
30    /// Positions we should split the text into multiple lines, if any.
31    ///
32    /// Is rebuild as needed.
33    layout_newlines: RefCell<Option<Vec<usize>>>,
34
35    content_without_ansi_esc: Option<String>,
36}
37
38impl<'txt> Default for Cell<'txt> {
39    fn default() -> Self {
40        Self {
41            content: Cow::Borrowed(""),
42            col_span: 1,
43            alignment: Alignment::Left,
44            pad_content: true,
45
46            layout_newlines: RefCell::new(None),
47            content_without_ansi_esc: None,
48        }
49    }
50}
51
52impl<'txt> Cell<'txt> {
53    fn owned(content: String) -> Cell<'txt> {
54        let mut this = Self {
55            content: Cow::Owned(content),
56            ..Default::default()
57        };
58        this.update_without_ansi_esc();
59        this
60    }
61
62    /// Special builder that is slightly more efficient than using `From<String>`.
63    fn borrowed(content: &'txt str) -> Self {
64        let mut this = Self {
65            content: Cow::Borrowed(content.as_ref()),
66            ..Default::default()
67        };
68        this.update_without_ansi_esc();
69        this
70    }
71
72    pub fn with_content(mut self, content: impl Into<Cow<'txt, str>>) -> Self {
73        self.set_content(content);
74        self
75    }
76
77    pub fn set_content(&mut self, content: impl Into<Cow<'txt, str>>) -> &mut Self {
78        self.content = content.into();
79        self.update_without_ansi_esc();
80        self
81    }
82
83    fn content_for_layout(&self) -> &str {
84        self.content_without_ansi_esc
85            .as_ref()
86            .map(|s| s.as_str())
87            .unwrap_or(&self.content)
88    }
89
90    fn update_without_ansi_esc(&mut self) {
91        self.content_without_ansi_esc = if ANSI_ESC_RE.is_match(&self.content) {
92            Some(ANSI_ESC_RE.split(&self.content).collect())
93        } else {
94            None
95        };
96    }
97
98    /// Set the number of columns this cell spans.
99    ///
100    /// # Panics
101    ///
102    /// Will panic if `col_span == 0`.
103    pub fn with_col_span(mut self, col_span: usize) -> Self {
104        self.set_col_span(col_span);
105        self
106    }
107
108    /// Set the number of columns this cell spans.
109    ///
110    /// # Panics
111    ///
112    /// Will panic if `col_span == 0`.
113    pub fn set_col_span(&mut self, col_span: usize) -> &mut Self {
114        assert!(col_span > 0, "cannot have a col_span of 0");
115        self.col_span = col_span;
116        *self.layout_newlines.borrow_mut() = None;
117        self
118    }
119
120    pub fn with_alignment(mut self, alignment: Alignment) -> Self {
121        self.set_alignment(alignment);
122        self
123    }
124
125    pub fn set_alignment(&mut self, alignment: Alignment) -> &mut Self {
126        self.alignment = alignment;
127        *self.layout_newlines.borrow_mut() = None;
128        self
129    }
130
131    pub fn with_padding(mut self, padding: bool) -> Self {
132        self.set_padding(padding);
133        self
134    }
135
136    pub fn set_padding(&mut self, padding: bool) -> &mut Self {
137        self.pad_content = padding;
138        *self.layout_newlines.borrow_mut() = None;
139        self
140    }
141
142    /// Calculate positions of newlines.
143    ///
144    /// Passed width includes padding spaces (if Some).
145    ///
146    /// Returns the total number of lines to be drawn.
147    // The meaining of the parameter option None (means unbounded) is different from layout_width =
148    // None (means cache is stale)
149    pub(crate) fn layout(&self, width: Option<usize>) -> usize {
150        // We can just pretend we have loads of space - we only calculate linebreaks here.
151        let width = width.unwrap_or(usize::MAX);
152        if width < 1 || (self.pad_content && width < 3) {
153            panic!("cell too small to show anything");
154        }
155        let content_width = if self.pad_content {
156            width.saturating_sub(2)
157        } else {
158            width
159        };
160        let mut ln = self.layout_newlines.borrow_mut();
161        let ln = ln.get_or_insert(vec![]);
162        ln.clear();
163        ln.push(0);
164
165        let mut s = self.content_for_layout();
166        // Go through potential linebreak locations to find where we should break.
167        let mut acc = 0;
168        while let Some(idx) = next_linebreak(s, content_width) {
169            s = &s[idx..];
170            ln.push(idx + acc);
171            acc += idx;
172        }
173        // the above method always ends the text with a newline, so pop it.
174        ln.pop();
175        // return number of lines
176        ln.len()
177    }
178
179    /// The minium width required to display the cell correctly.
180    ///
181    /// If `only_mandatory` is passed, then only mandatory newlines will be considered, meaning the
182    /// width will be larger.
183    pub(crate) fn min_width(&self, only_mandatory: bool) -> usize {
184        let content = self.content_for_layout();
185        let max_newline_gap = linebreaks(content).filter_map(|(idx, ty)| {
186            if only_mandatory && !matches!(ty, BreakOpportunity::Mandatory) {
187                None
188            } else {
189                Some(idx)
190            }
191        });
192        let max_newline_gap = iter::once(0)
193            .chain(max_newline_gap)
194            .chain(iter::once(content.len()))
195            .tuple_windows()
196            .map(|(start, end)| content[start..end].width())
197            .max()
198            .unwrap_or(0);
199
200        // We need space for the padding if the user specified to use it.
201        max_newline_gap + if self.pad_content { 2 } else { 0 }
202    }
203
204    /// Get the width of this cell, given the cell widths.
205    ///
206    /// Assumes slice starts at current cell, and returns slice starting at next cell.
207    pub(crate) fn width<'s>(
208        &self,
209        border_width: usize,
210        cell_widths: &'s [usize],
211    ) -> (usize, &'s [usize]) {
212        (
213            cell_widths[..self.col_span].iter().copied().sum::<usize>()
214                + border_width * self.col_span.saturating_sub(1),
215            &cell_widths[self.col_span..],
216        )
217    }
218
219    /// Write out the given line to the formatter.
220    ///
221    /// You must call `layout` (which lays out the text)  before calling this method, otherwise
222    /// you may get panics or garbage.
223    pub(crate) fn render_line(
224        &self,
225        line_idx: usize,
226        width: usize,
227        f: &mut fmt::Formatter,
228    ) -> fmt::Result {
229        let newlines = self.layout_newlines.borrow();
230        let newlines = newlines.as_ref().expect("missed call to `layout`");
231        let line = match newlines.get(line_idx) {
232            Some(&start_idx) => match newlines.get(line_idx + 1) {
233                Some(&end_idx) => &self.content[start_idx..end_idx],
234                None => &self.content[start_idx..],
235            },
236            // This will be the case if we already drew all the lines.
237            None => "",
238        };
239
240        let (front_pad, back_pad) = self.get_padding(width, line.width());
241        let edge = self.edge_char();
242        f.write_str(edge)?;
243        for _ in 0..front_pad {
244            f.write_str(" ")?;
245        }
246        f.write_str(line)?;
247        for _ in 0..back_pad {
248            f.write_str(" ")?;
249        }
250        f.write_str(edge)
251    }
252
253    /// Returns the number of spaces that should be placed before and after the text (excluding the
254    /// single padding char)
255    ///
256    /// line_width includes padding spaces
257    fn get_padding(&self, width: usize, line_width: usize) -> (usize, usize) {
258        let padding = if self.pad_content { 2 } else { 0 };
259        let gap = (width - line_width).saturating_sub(padding);
260        match self.alignment {
261            Alignment::Left => (0, gap),
262            Alignment::Center => (gap / 2, gap - gap / 2),
263            Alignment::Right => (gap, 0),
264        }
265    }
266
267    fn edge_char(&self) -> &'static str {
268        if self.pad_content {
269            " "
270        } else {
271            "\0"
272        }
273    }
274}
275
276impl<'txt> From<String> for Cell<'txt> {
277    fn from(other: String) -> Self {
278        Cell::owned(other)
279    }
280}
281
282impl<'txt> From<&'txt String> for Cell<'txt> {
283    fn from(other: &'txt String) -> Self {
284        Cell::borrowed(other)
285    }
286}
287
288impl<'txt> From<&'txt str> for Cell<'txt> {
289    fn from(other: &'txt str) -> Self {
290        Cell::borrowed(other)
291    }
292}
293
294// Will match any ansi escape sequence.
295// Taken from https://github.com/mitsuhiko/console
296lazy_static! {
297    static ref ANSI_ESC_RE: Regex =
298        Regex::new(r"[\x1b\x9b][\[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PRZcf-nqry=><]")
299            .unwrap();
300}
301
302/// Find where to put the next linebreak, or return None if we don't need any more.
303fn next_linebreak(text: &str, max_width: usize) -> Option<usize> {
304    let mut prev = None;
305    for (idx, ty) in linebreaks(text) {
306        if text[..idx].width() > max_width {
307            // first use the previous linebreak if there is one
308            if let Some(prev) = prev {
309                return Some(prev);
310            };
311            // next, find a character break
312            if let Some(linebreak) = next_linebreak_midword(text, max_width) {
313                return Some(linebreak);
314            }
315            // finally, do 1 char per line to be deterministic (we have a very narrow cell)
316            return text.chars().next().map(|ch| ch.width()).flatten();
317        } else if matches!(ty, BreakOpportunity::Mandatory) {
318            // we must insert a linebreak here
319            return Some(idx);
320        } else {
321            prev = Some(idx);
322        }
323    }
324    None
325}
326
327// TODO use midpoint-based search
328fn next_linebreak_midword(text: &str, max_width: usize) -> Option<usize> {
329    let mut prev = None;
330    for (idx, _) in text.char_indices() {
331        if text[..idx].width() > max_width {
332            return prev;
333        } else {
334            prev = Some(idx);
335        }
336    }
337    // we should not reach here, because we already found a potential breakpoint that was too big
338    // for the line.
339    unreachable!()
340}