1use rich::cells::cell_len;
37use rich::measure::Measurement;
38use rich::{Console, ConsoleOptions, Renderable, Segment, Style};
39
40use crate::render::{MicroMeta, PAD_CELL};
41
42#[derive(Clone, Debug)]
45pub(crate) struct Run {
46 pub meta: MicroMeta,
47 pub start: usize,
49 pub end: usize,
50 pub line: usize,
52 pub column: usize,
53 pub width: usize,
55}
56
57pub(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
100fn 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
126fn background(style: Option<&Style>) -> Option<Style> {
128 let color = style?.bgcolor()?.clone();
129 Some(Style::new().with_bgcolor(color))
130}
131
132fn 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
150fn 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 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
191pub 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 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#[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 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 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 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 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 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}