Skip to main content

rich_micro/
rows.rs

1//! Assets two rows high (#711): reserving their rows in any layout.
2//!
3//! A micro asset in a [`Text`](rich::Text) is always its **first row**: its
4//! columns of placeholder cells, in the line it sits on, so it measures,
5//! wraps and crops as one unbreakable word of exactly its width. An asset
6//! two rows high (a 2×2 icon, a 3×2 animation) also needs the row below
7//! that line, and core's layout knows nothing of it. [`MicroRows`] adds it:
8//! it renders what it wraps, and after every line holding the first row of a
9//! two-row asset it inserts a **reserved row**:
10//!
11//! - under each such asset, its second row: [`PAD_CELL`] blanks at the same
12//!   columns, tagged with the asset's occurrence and row 1, so a
13//!   [`MicroView`](crate::MicroView) can draw the image over both rows;
14//! - everywhere else, blank: spaces under the text beside the asset (the
15//!   text stays on the first row and never flows into the reserved row),
16//!   keeping only the background colour of the cells above, and with no
17//!   blanks after the last asset unless they have a background.
18//!
19//! So a two-row asset never overlaps the line below it: that line moves
20//! down. The rule is the same at every width and in every container, and the
21//! output depends only on the input, so the same input renders the same way
22//! every time. [`MicroRows`] reports the extra row through its rendered
23//! height, which is what tables and panels lay out by, so a table row or a
24//! panel grows to hold it; its width is the inner renderable's.
25//!
26//! Wrap the [`Text`](rich::Text) that holds the asset (or a renderable that
27//! lays out like text), not a table or panel around it: a reserved row is
28//! blank across the whole line, borders included. A row already reserved is
29//! never reserved again, so wrapping twice is harmless.
30//! [`MicroAssetRef`](crate::MicroAssetRef) reserves its own rows.
31//!
32//! Where nothing reserves the rows (a bare `Text`, Markdown, a table or
33//! panel title), a two-row asset shows its first row only, as its fallback:
34//! the renderers draw an image over both rows only when both are there.
35
36use rich::cells::cell_len;
37use rich::measure::Measurement;
38use rich::{Console, ConsoleOptions, Renderable, Segment, Style};
39
40use crate::render::{MicroMeta, PAD_CELL};
41
42/// One run of cells tagged with one occurrence's row, found in rendered
43/// output.
44#[derive(Clone, Debug)]
45pub(crate) struct Run {
46    pub meta: MicroMeta,
47    /// The run's segments: `start..end`.
48    pub start: usize,
49    pub end: usize,
50    /// The line it is on (counting line breaks from 0) and its first column.
51    pub line: usize,
52    pub column: usize,
53    /// Its width in cells.
54    pub width: usize,
55}
56
57/// Every tagged run in `segments`, in order. A run is the segments tagged
58/// with one occurrence's row, next to each other; a control segment ends it.
59pub(crate) fn runs(segments: &[Segment]) -> Vec<Run> {
60    let mut out = Vec::new();
61    let (mut line, mut column) = (0, 0);
62    let mut index = 0;
63    while index < segments.len() {
64        let segment = &segments[index];
65        let Some(meta) = MicroMeta::from_segment(segment) else {
66            if !segment.control {
67                match segment.text.rfind('\n') {
68                    Some(last) => {
69                        line += segment.text.matches('\n').count();
70                        column = cell_len(&segment.text[last + 1..]);
71                    }
72                    None => column += segment.cell_length(),
73                }
74            }
75            index += 1;
76            continue;
77        };
78        let start = index;
79        let mut width = 0;
80        while index < segments.len()
81            && MicroMeta::from_segment(&segments[index])
82                .is_some_and(|m| m.id == meta.id && m.row == meta.row)
83        {
84            width += segments[index].cell_length();
85            index += 1;
86        }
87        out.push(Run {
88            meta,
89            start,
90            end: index,
91            line,
92            column,
93            width,
94        });
95        column += width;
96    }
97    out
98}
99
100/// `segments` split at line breaks: the lines, and the break after each but
101/// the last (its own segment, as it was styled).
102fn split(segments: Vec<Segment>) -> (Vec<Vec<Segment>>, Vec<Segment>) {
103    let mut lines = vec![Vec::new()];
104    let mut breaks = Vec::new();
105    for segment in segments {
106        if segment.control || !segment.text.contains('\n') {
107            lines.last_mut().expect("a line").push(segment);
108            continue;
109        }
110        for (index, part) in segment.text.split('\n').enumerate() {
111            if index > 0 {
112                breaks.push(Segment::new("\n", segment.style.clone()));
113                lines.push(Vec::new());
114            }
115            if !part.is_empty() {
116                lines
117                    .last_mut()
118                    .expect("a line")
119                    .push(Segment::new(part, segment.style.clone()));
120            }
121        }
122    }
123    (lines, breaks)
124}
125
126/// Only the background colour of `style`, for a blank under it.
127fn background(style: Option<&Style>) -> Option<Style> {
128    let color = style?.bgcolor()?.clone();
129    Some(Style::new().with_bgcolor(color))
130}
131
132/// Append `width` blank cells in `style`, merged into a blank before them.
133fn blank(out: &mut Vec<Segment>, width: usize, style: Option<Style>) {
134    if width == 0 {
135        return;
136    }
137    if let Some(last) = out.last_mut() {
138        let is_blank = !last.control
139            && last.style == style
140            && MicroMeta::from_segment(last).is_none()
141            && last.text.bytes().all(|b| b == b' ');
142        if is_blank {
143            last.text.push_str(&" ".repeat(width));
144            return;
145        }
146    }
147    out.push(Segment::new(" ".repeat(width), style));
148}
149
150/// Reserved row `row` (1 for the first below the line) of `line`, under the
151/// two-row assets in `pending` (runs of `line`).
152fn reserved(line: &[Segment], pending: &[&Run], row: usize) -> Vec<Segment> {
153    let mut out: Vec<Segment> = Vec::new();
154    let mut index = 0;
155    while index < line.len() {
156        if let Some(run) = pending.iter().find(|run| run.start == index) {
157            let under = background(line[run.start].style.as_ref());
158            if run.meta.rows > row {
159                let tag = run.meta.with_row(row).to_style();
160                let style = under.map_or(tag.clone(), |under| under.combine(&tag));
161                out.push(Segment::new(
162                    PAD_CELL.to_string().repeat(run.meta.cols),
163                    Some(style),
164                ));
165            } else {
166                blank(&mut out, run.width, under);
167            }
168            index = run.end;
169            continue;
170        }
171        let segment = &line[index];
172        if !segment.control {
173            blank(
174                &mut out,
175                segment.cell_length(),
176                background(segment.style.as_ref()),
177            );
178        }
179        index += 1;
180    }
181    // Nothing after the last asset but blanks with no background: drop them.
182    if out
183        .last()
184        .is_some_and(|last| last.style.is_none() && last.text.bytes().all(|b| b == b' '))
185    {
186        out.pop();
187    }
188    out
189}
190
191/// Reserve the rows of every two-row asset in `segments` (rendered output):
192/// see the [module docs](self). Output with no two-row asset is returned as
193/// it is.
194pub fn reserve_rows(segments: Vec<Segment>) -> Vec<Segment> {
195    let multi_row = segments.iter().any(|segment| {
196        MicroMeta::from_segment(segment).is_some_and(|meta| meta.row == 0 && meta.rows > 1)
197    });
198    if !multi_row {
199        return segments;
200    }
201    let (lines, breaks) = split(segments);
202    let found: Vec<Vec<Run>> = lines.iter().map(|line| runs(line)).collect();
203    let mut out = Vec::new();
204    for (index, line) in lines.iter().enumerate() {
205        // The two-row assets whole on this line, unless the line below
206        // already holds their second row.
207        let below = found.get(index + 1);
208        let pending: Vec<&Run> = found[index]
209            .iter()
210            .filter(|run| {
211                run.meta.row == 0
212                    && run.meta.rows > 1
213                    && run.width == run.meta.cols
214                    && !below.is_some_and(|below| {
215                        below.iter().any(|other| {
216                            other.meta.id == run.meta.id
217                                && other.meta.row == 1
218                                && other.column == run.column
219                        })
220                    })
221            })
222            .collect();
223        out.extend(line.iter().cloned());
224        let extra = pending.iter().map(|run| run.meta.rows - 1).max();
225        for row in 1..=extra.unwrap_or(0) {
226            out.push(Segment::line());
227            out.extend(reserved(line, &pending, row));
228        }
229        if let Some(line_break) = breaks.get(index) {
230            out.push(line_break.clone());
231        }
232    }
233    out
234}
235
236/// A renderable with the rows of its two-row micro assets reserved: see the
237/// [module docs](self).
238///
239/// ```
240/// use rich::{Console, Text};
241/// use rich_micro::{CellSize, MicroAsset, MicroExt, MicroRows};
242///
243/// let icon = MicroAsset::new("team/icon", "blue square")?
244///     .with_size(CellSize::new(2, 2)?)?
245///     .with_text("[]")?;
246/// let mut text = Text::new("Deploying ");
247/// text.append_micro_asset(&icon).append(" done", None);
248/// let console = Console::builder().width(40).build();
249/// let shown = console.render_export(&MicroRows::new(text));
250/// // The line the icon sits on, then the row it reserves below it.
251/// assert_eq!(shown, "Deploying [] done\n          \u{2800}\u{2800}\n");
252/// # Ok::<(), rich_micro::MicroError>(())
253/// ```
254#[derive(Clone, Debug)]
255pub struct MicroRows<R> {
256    inner: R,
257}
258
259impl<R> MicroRows<R> {
260    pub fn new(inner: R) -> Self {
261        MicroRows { inner }
262    }
263
264    /// What it wraps.
265    pub fn inner(&self) -> &R {
266        &self.inner
267    }
268
269    pub fn into_inner(self) -> R {
270        self.inner
271    }
272}
273
274impl<R: Renderable> Renderable for MicroRows<R> {
275    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
276        reserve_rows(self.inner.rich_render(console, options))
277    }
278
279    fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
280        self.inner.measure(console, options)
281    }
282
283    fn fit_to_measurement(&self) -> bool {
284        self.inner.fit_to_measurement()
285    }
286
287    fn vertical(&self) -> Option<rich::align::VerticalAlign> {
288        self.inner.vertical()
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295
296    fn tagged(text: &str, meta: &MicroMeta) -> Segment {
297        Segment::new(text, Some(meta.to_style()))
298    }
299
300    fn meta(rows: usize, id: u64) -> MicroMeta {
301        MicroMeta {
302            name: "a".into(),
303            cols: 2,
304            rows,
305            id,
306            row: 0,
307        }
308    }
309
310    fn plain(segments: &[Segment]) -> String {
311        segments.iter().map(|s| s.text.as_str()).collect()
312    }
313
314    #[test]
315    fn runs_know_their_line_and_column() {
316        let one = meta(1, 1);
317        let two = meta(2, 2);
318        let segments = vec![
319            Segment::new("ab ", None),
320            tagged("x", &one),
321            tagged("y", &one),
322            Segment::new(" c\nd", None),
323            tagged("zz", &two),
324            Segment::control("\x1b[0m"),
325            tagged("zz", &two),
326        ];
327        let found = runs(&segments);
328        let shape: Vec<(u64, usize, usize, usize, usize)> = found
329            .iter()
330            .map(|r| (r.meta.id, r.line, r.column, r.width, r.end - r.start))
331            .collect();
332        // A control segment ends a run.
333        assert_eq!(
334            shape,
335            [(1, 0, 3, 2, 2), (2, 1, 1, 2, 1), (2, 1, 3, 2, 1)],
336            "{found:#?}"
337        );
338    }
339
340    #[test]
341    fn a_line_with_a_two_row_asset_gets_a_row_below_it() {
342        let two = meta(2, 7);
343        let red = Style::parse("red on blue").unwrap();
344        let segments = vec![
345            Segment::new("go ", None),
346            tagged("[]", &two),
347            Segment::new(" now", Some(red)),
348            Segment::line(),
349            Segment::new("next", None),
350        ];
351        let out = reserve_rows(segments);
352        assert_eq!(plain(&out), "go [] now\n   \u{2800}\u{2800}    \nnext");
353        // The blank under " now" keeps its background, nothing else.
354        let under = out.iter().rev().nth(2).unwrap();
355        assert_eq!(under.text, "    ");
356        assert_eq!(
357            under.style.as_ref().and_then(|s| s.bgcolor()).cloned(),
358            Style::parse("on blue").unwrap().bgcolor().cloned()
359        );
360        assert!(under.style.as_ref().unwrap().color().is_none());
361        // The second row is tagged as row 1 of the same occurrence.
362        let second: Vec<MicroMeta> = out.iter().filter_map(MicroMeta::from_segment).collect();
363        assert_eq!(second.iter().map(|m| m.row).collect::<Vec<_>>(), [0, 1]);
364        assert!(second.iter().all(|m| m.id == 7));
365        // Reserving again changes nothing.
366        assert_eq!(reserve_rows(out.clone()), out);
367    }
368
369    #[test]
370    fn one_row_output_is_untouched() {
371        let one = meta(1, 3);
372        let segments = vec![Segment::new("a\n", None), tagged("xx", &one)];
373        assert_eq!(reserve_rows(segments.clone()), segments);
374    }
375
376    #[test]
377    fn a_cut_asset_reserves_nothing() {
378        let two = meta(2, 4);
379        let segments = vec![Segment::new("ab ", None), tagged("x", &two)];
380        assert_eq!(plain(&reserve_rows(segments)), "ab x");
381    }
382}