Skip to main content

tau_term_screen/
two_line_elision.rs

1//! Width-adaptive two-row excerpt layout.
2
3use crate::{Cell, StyledText, layout_lines};
4
5/// Width-adaptive two-row content that preserves leading and trailing excerpts.
6///
7/// Callers must bound the prefix, excerpts, candidate lists, and optional
8/// unabridged content. Layout work scales with the supplied values and does not
9/// impose its own source-size limit.
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct TwoLineElision {
12    /// Styled prefix shown before the first excerpt, such as a prompt marker.
13    pub prefix: StyledText,
14    /// Styled beginning of the first source line.
15    pub first: StyledText,
16    /// Styled end of the last source line.
17    pub last: StyledText,
18    /// Styled first-row omission markers, ordered from preferred to most
19    /// compact.
20    pub first_omissions: Vec<StyledText>,
21    /// Styled second-row omission markers, ordered from preferred to most
22    /// compact.
23    pub last_omissions: Vec<StyledText>,
24    /// Styled state labels, ordered from preferred to most compact.
25    pub labels: Vec<StyledText>,
26    /// Bounded complete presentation retained when it naturally fits two rows.
27    pub unabridged: Option<StyledText>,
28}
29
30impl TwoLineElision {
31    /// Returns whether every possible presentation is empty.
32    pub(crate) fn is_empty(&self) -> bool {
33        self.prefix.is_empty()
34            && self.first.is_empty()
35            && self.last.is_empty()
36            && self.first_omissions.iter().all(StyledText::is_empty)
37            && self.last_omissions.iter().all(StyledText::is_empty)
38            && self.labels.iter().all(StyledText::is_empty)
39            && self.unabridged.as_ref().is_none_or(StyledText::is_empty)
40    }
41
42    /// Lays out this excerpt within the current display width.
43    pub(crate) fn layout(&self, width: usize) -> Vec<Vec<Cell>> {
44        let configured_prefix = self.prefix.to_cells();
45        let prefix = if cells_width(&configured_prefix) < width {
46            configured_prefix
47        } else {
48            Vec::new()
49        };
50        if let Some(unabridged) = &self.unabridged {
51            let mut complete = self.prefix.clone();
52            for span in unabridged.spans() {
53                complete.push(span.clone());
54            }
55            let lines = layout_lines().content(&complete).width(width).call();
56            if lines.len() <= 2 {
57                return lines;
58            }
59        }
60
61        let first_marker = select(
62            &self.first_omissions,
63            width.saturating_sub(cells_width(&prefix)),
64        );
65        let label = select(&self.labels, width);
66        let last_marker = select(
67            &self.last_omissions,
68            width.saturating_sub(cells_width(&label)),
69        );
70
71        let mut first_row = prefix;
72        let first_budget = width
73            .saturating_sub(cells_width(&first_row))
74            .saturating_sub(cells_width(&first_marker));
75        first_row.extend(cell_prefix(&self.first.to_cells(), first_budget));
76        first_row.extend(first_marker);
77
78        let last_budget = width
79            .saturating_sub(cells_width(&last_marker))
80            .saturating_sub(cells_width(&label));
81        let mut last_row = last_marker;
82        last_row.extend(cell_suffix(&self.last.to_cells(), last_budget));
83        last_row.extend(label);
84        vec![first_row, last_row]
85    }
86}
87
88fn select(options: &[StyledText], width: usize) -> Vec<Cell> {
89    for option in options {
90        let cells = option.to_cells();
91        if cells_width(&cells) <= width {
92            return cells;
93        }
94    }
95    Vec::new()
96}
97
98fn cell_prefix(cells: &[Cell], width: usize) -> Vec<Cell> {
99    let mut used = 0;
100    cells
101        .iter()
102        .take_while(|cell| {
103            let next = used + cell.col_width();
104            let fits = next <= width;
105            if fits {
106                used = next;
107            }
108            fits
109        })
110        .cloned()
111        .collect()
112}
113
114fn cell_suffix(cells: &[Cell], width: usize) -> Vec<Cell> {
115    let mut used = 0;
116    let mut start = cells.len();
117    for (index, cell) in cells.iter().enumerate().rev() {
118        let next = used + cell.col_width();
119        if width < next {
120            start = index + 1;
121            while start < cells.len() && cells[start].col_width() == 0 {
122                start += 1;
123            }
124            break;
125        }
126        used = next;
127        start = index;
128    }
129    cells[start..].to_vec()
130}
131
132fn cells_width(cells: &[Cell]) -> usize {
133    cells.iter().map(Cell::col_width).sum()
134}