1use std::{
4 borrow::{Borrow, Cow},
5 cmp,
6 collections::BTreeMap,
7 fmt::{self, Write},
8};
9
10use crate::{
11 ansi::{ANSIBuf, ANSIFmt},
12 colors::Colors,
13 config::{
14 spanned::SpannedConfig, AlignmentHorizontal, AlignmentVertical, Formatting, Indent, Offset,
15 Position, Sides,
16 },
17 dimension::Dimension,
18 records::{IntoRecords, Records},
19 util::string::{count_lines, get_line_width, get_lines, get_text_width, Lines},
20};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
24pub struct IterGrid<R, D, G, C> {
25 records: R,
26 config: G,
27 dimension: D,
28 colors: C,
29}
30
31impl<R, D, G, C> IterGrid<R, D, G, C> {
32 pub fn new(records: R, config: G, dimension: D, colors: C) -> Self {
34 IterGrid {
35 records,
36 config,
37 dimension,
38 colors,
39 }
40 }
41
42 pub fn build<F>(self, mut f: F) -> fmt::Result
44 where
45 R: Records,
46 <R::Iter as IntoRecords>::Cell: AsRef<str>,
47 D: Dimension,
48 C: Colors,
49 G: Borrow<SpannedConfig>,
50 F: Write,
51 {
52 if self.records.count_columns() == 0 || self.records.hint_count_rows() == Some(0) {
53 return Ok(());
54 }
55
56 let config = self.config.borrow();
57 let ctx = GridCtx {
58 cfg: config,
59 colors: &self.colors,
60 dims: &self.dimension,
61 };
62
63 print_grid(&mut f, self.records, &ctx)
64 }
65
66 #[allow(clippy::inherent_to_string)]
70 pub fn to_string(self) -> String
71 where
72 R: Records,
73 <R::Iter as IntoRecords>::Cell: AsRef<str>,
74 D: Dimension,
75 G: Borrow<SpannedConfig>,
76 C: Colors,
77 {
78 let mut buf = String::new();
79 self.build(&mut buf).expect("It's guaranteed to never happen otherwise it's considered an stdlib error or impl error");
80 buf
81 }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85struct GridCtx<'a, D, C> {
86 cfg: &'a SpannedConfig,
87 colors: &'a C,
88 dims: &'a D,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92struct RowLine {
93 row: usize,
94 line: usize,
95}
96
97impl RowLine {
98 fn new(row: usize, line: usize) -> Self {
99 Self { row, line }
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104struct Height {
105 value: usize,
106 total: Option<usize>,
107}
108
109impl Height {
110 fn new(value: usize, total: Option<usize>) -> Self {
111 Self { value, total }
112 }
113}
114
115fn print_grid<F, R, D, C>(f: &mut F, records: R, ctx: &GridCtx<'_, D, C>) -> fmt::Result
116where
117 F: Write,
118 R: Records,
119 <R::Iter as IntoRecords>::Cell: AsRef<str>,
120 D: Dimension,
121 C: Colors,
122{
123 let grid_has_spans = ctx.cfg.has_column_spans() || ctx.cfg.has_row_spans();
126 if grid_has_spans {
127 print_grid_spanned(f, records, ctx)
128 } else {
129 print_grid_general(f, records, ctx)
130 }
131}
132
133fn print_grid_general<F, R, D, C>(f: &mut F, records: R, ctx: &GridCtx<'_, D, C>) -> fmt::Result
134where
135 F: Write,
136 R: Records,
137 <R::Iter as IntoRecords>::Cell: AsRef<str>,
138 D: Dimension,
139 C: Colors,
140{
141 let count_columns = records.count_columns();
142
143 let mut totalw = None;
144 let totalh = records
145 .hint_count_rows()
146 .map(|count_rows| total_height(ctx.cfg, ctx.dims, count_rows));
147
148 let mut records_iter = records.iter_rows().into_iter();
149 let mut next_columns = records_iter.next();
150
151 if next_columns.is_none() {
152 return Ok(());
153 }
154
155 if ctx.cfg.get_margin().top.size > 0 {
156 totalw = Some(output_width(ctx.cfg, ctx.dims, count_columns));
157
158 print_margin_top(f, ctx.cfg, totalw.unwrap())?;
159 f.write_char('\n')?;
160 }
161
162 let mut row = 0;
163 let mut line = 0;
164 let mut is_prev_row_skipped = false;
165 let mut buf = Vec::new();
166 while let Some(columns) = next_columns {
167 let columns = columns.into_iter();
168 next_columns = records_iter.next();
169 let is_last_row = next_columns.is_none();
170
171 let height = ctx.dims.get_height(row);
172 let count_rows = convert_count_rows(row, is_last_row);
173 let has_horizontal = ctx.cfg.has_horizontal(row, count_rows);
174 let shape = Position::new(count_rows, count_columns);
175 let rline = RowLine::new(row, line);
176
177 if row > 0 && !is_prev_row_skipped && (has_horizontal || height > 0) {
178 f.write_char('\n')?;
179 }
180
181 if has_horizontal {
182 print_horizontal_line(f, ctx, rline, shape, totalh)?;
183
184 line += 1;
185
186 if height > 0 {
187 f.write_char('\n')?;
188 }
189 }
190
191 if height == 1 {
192 print_single_line_columns(f, columns, ctx, rline, totalh, shape)?
193 } else if height > 0 {
194 buf.reserve(count_columns);
195
196 collect_columns(&mut buf, columns, ctx, row, height);
197 let height = Height::new(height, totalh);
198 print_columns_lines(f, &mut buf, ctx.cfg, rline, height)?;
199
200 buf.clear();
201 }
202
203 is_prev_row_skipped = height == 0 && !has_horizontal;
204 line += height;
205 row += 1;
206 }
207
208 if ctx.cfg.has_horizontal(row, row) {
209 f.write_char('\n')?;
210 let shape = Position::new(row, count_columns);
211 let rline = RowLine::new(row, line);
212 print_horizontal_line(f, ctx, rline, shape, totalh)?;
213 }
214
215 if ctx.cfg.get_margin().bottom.size > 0 {
216 let totalw = totalw.unwrap_or_else(|| output_width(ctx.cfg, ctx.dims, count_columns));
217
218 f.write_char('\n')?;
219 print_margin_bottom(f, ctx.cfg, totalw)?;
220 }
221
222 Ok(())
223}
224
225fn output_width<D>(cfg: &SpannedConfig, d: D, count_columns: usize) -> usize
226where
227 D: Dimension,
228{
229 let margin = cfg.get_margin();
230 total_width(cfg, &d, count_columns) + margin.left.size + margin.right.size
231}
232
233fn print_horizontal_line<F, D, C>(
234 f: &mut F,
235 ctx: &GridCtx<'_, D, C>,
236 rline: RowLine,
237 shape: Position,
238 totalh: Option<usize>,
239) -> fmt::Result
240where
241 F: Write,
242 D: Dimension,
243{
244 print_margin_left(f, ctx.cfg, rline.line, totalh)?;
245 print_split_line(f, ctx.cfg, ctx.dims, rline.row, shape)?;
246 print_margin_right(f, ctx.cfg, rline.line, totalh)?;
247 Ok(())
248}
249
250fn print_single_line_columns<F, I, D, C>(
251 f: &mut F,
252 iter: I,
253 ctx: &GridCtx<'_, D, C>,
254 rline: RowLine,
255 totalheight: Option<usize>,
256 shape: Position,
257) -> fmt::Result
258where
259 F: Write,
260 I: Iterator,
261 I::Item: AsRef<str>,
262 D: Dimension,
263 C: Colors,
264{
265 print_margin_left(f, ctx.cfg, rline.line, totalheight)?;
266
267 for (col, cell) in iter.enumerate() {
268 let pos = Position::new(rline.row, col);
269 let width = ctx.dims.get_width(col);
270 let color = ctx.colors.get_color(pos);
271 let text = cell.as_ref();
272 print_vertical_char(f, ctx.cfg, pos, 0, 1, shape.col)?;
273 print_single_line_column(f, text, ctx.cfg, width, color, pos)?;
274 }
275
276 let pos = Position::new(rline.row, shape.col);
277 print_vertical_char(f, ctx.cfg, pos, 0, 1, shape.col)?;
278 print_margin_right(f, ctx.cfg, rline.line, totalheight)?;
279
280 Ok(())
281}
282
283fn print_single_line_column<F, C>(
284 f: &mut F,
285 text: &str,
286 cfg: &SpannedConfig,
287 width: usize,
288 color: Option<&C>,
289 pos: Position,
290) -> fmt::Result
291where
292 F: Write,
293 C: ANSIFmt,
294{
295 let pad = cfg.get_padding(pos);
296 let pad_color = cfg.get_padding_color(pos);
297 let fmt = cfg.get_formatting(pos);
298 let space = cfg.get_justification(pos);
299 let space_color = cfg.get_justification_color(pos);
300
301 let (text, text_width) = if fmt.horizontal_trim && !text.is_empty() {
302 let text = string_trim(text);
303 let width = get_line_width(&text);
304
305 (text, width)
306 } else {
307 let text = Cow::Borrowed(text);
308 let width = get_text_width(&text);
309
310 (text, width)
311 };
312
313 let alignment = *cfg.get_alignment_horizontal(pos);
314 let available_width = width - pad.left.size - pad.right.size;
315 let (left, right) = calculate_indent(alignment, text_width, available_width);
316
317 print_padding(f, &pad.left, pad_color.left.as_ref())?;
318
319 print_indent(f, space, left, space_color)?;
320 print_text(f, &text, color)?;
321 print_indent(f, space, right, space_color)?;
322
323 print_padding(f, &pad.right, pad_color.right.as_ref())?;
324
325 Ok(())
326}
327
328fn print_columns_lines<T, F, C>(
329 f: &mut F,
330 buf: &mut [Cell<'_, T, C>],
331 cfg: &SpannedConfig,
332 rline: RowLine,
333 height: Height,
334) -> fmt::Result
335where
336 F: Write,
337 C: ANSIFmt,
338{
339 let count_columns = buf.len();
340
341 for i in 0..height.value {
342 let exact_line = rline.line + i;
343
344 print_margin_left(f, cfg, exact_line, height.total)?;
345
346 for (col, cell) in buf.iter_mut().enumerate() {
347 let pos = Position::new(rline.row, col);
348 print_vertical_char(f, cfg, pos, i, height.value, count_columns)?;
349 cell.display(f)?;
350 }
351
352 let pos = Position::new(rline.row, count_columns);
353 print_vertical_char(f, cfg, pos, i, height.value, count_columns)?;
354
355 print_margin_right(f, cfg, exact_line, height.total)?;
356
357 if i + 1 != height.value {
358 f.write_char('\n')?;
359 }
360 }
361
362 Ok(())
363}
364
365fn collect_columns<'a, I, D, C>(
366 buf: &mut Vec<Cell<'a, I::Item, &'a C::Color>>,
367 iter: I,
368 ctx: &GridCtx<'a, D, C>,
369 row: usize,
370 height: usize,
371) where
372 I: Iterator,
373 I::Item: AsRef<str>,
374 C: Colors,
375 D: Dimension,
376{
377 for (col, cell) in iter.enumerate() {
378 let pos = Position::new(row, col);
379 let width = ctx.dims.get_width(col);
380 let color = ctx.colors.get_color(pos);
381 let cell = Cell::new(cell, width, height, ctx.cfg, color, pos);
382 buf.push(cell);
383 }
384}
385
386fn print_split_line<F, D>(
387 f: &mut F,
388 cfg: &SpannedConfig,
389 dimension: &D,
390 row: usize,
391 shape: Position,
392) -> fmt::Result
393where
394 F: Write,
395 D: Dimension,
396{
397 let mut used_color = None;
398 print_vertical_intersection(f, cfg, (row, 0).into(), shape, &mut used_color)?;
399
400 for col in 0..shape.col {
401 let width = dimension.get_width(col);
402
403 if width > 0 {
405 let pos = (row, col).into();
406 let main = cfg.get_horizontal(pos, shape.row);
407 match main {
408 Some(c) => {
409 let clr = cfg.get_horizontal_color(pos, shape.row);
410 prepare_coloring(f, clr, &mut used_color)?;
411 print_horizontal_border(f, cfg, pos, width, c, &used_color)?;
412 }
413 None => repeat_char(f, ' ', width)?,
414 }
415 }
416
417 print_vertical_intersection(f, cfg, (row, col + 1).into(), shape, &mut used_color)?;
418 }
419
420 if let Some(clr) = used_color.take() {
421 clr.fmt_ansi_suffix(f)?;
422 }
423
424 Ok(())
425}
426
427fn print_grid_spanned<F, R, D, C>(f: &mut F, records: R, ctx: &GridCtx<'_, D, C>) -> fmt::Result
428where
429 F: Write,
430 R: Records,
431 <R::Iter as IntoRecords>::Cell: AsRef<str>,
432 D: Dimension,
433 C: Colors,
434{
435 let count_columns = records.count_columns();
436
437 let total_width = total_width(ctx.cfg, ctx.dims, count_columns);
438 let margin = ctx.cfg.get_margin();
439 let total_width_with_margin = total_width + margin.left.size + margin.right.size;
440
441 let totalh = records
442 .hint_count_rows()
443 .map(|rows| total_height(ctx.cfg, ctx.dims, rows));
444
445 if margin.top.size > 0 {
446 print_margin_top(f, ctx.cfg, total_width_with_margin)?;
447 f.write_char('\n')?;
448 }
449
450 let mut buf = BTreeMap::new();
451
452 let mut records_iter = records.iter_rows().into_iter();
453 let mut next_columns = records_iter.next();
454
455 let mut need_new_line = false;
456 let mut line = 0;
457 let mut row = 0;
458 while let Some(columns) = next_columns {
459 let columns = columns.into_iter();
460 next_columns = records_iter.next();
461 let is_last_row = next_columns.is_none();
462
463 let height = ctx.dims.get_height(row);
464 let count_rows = convert_count_rows(row, is_last_row);
465 let shape = Position::new(count_rows, count_columns);
466
467 let has_horizontal = ctx.cfg.has_horizontal(row, count_rows);
468 if need_new_line && (has_horizontal || height > 0) {
469 f.write_char('\n')?;
470 need_new_line = false;
471 }
472
473 if has_horizontal {
474 print_margin_left(f, ctx.cfg, line, totalh)?;
475 print_split_line_spanned(f, &mut buf, ctx.cfg, ctx.dims, row, shape)?;
476 print_margin_right(f, ctx.cfg, line, totalh)?;
477
478 line += 1;
479
480 if height > 0 {
481 f.write_char('\n')?;
482 }
483 }
484
485 let rline = RowLine::new(row, line);
486 let height = Height::new(height, totalh);
487 print_spanned_columns(f, columns, &mut buf, ctx, rline, height, shape)?;
488
489 if has_horizontal || height.value > 0 {
490 need_new_line = true;
491 }
492
493 line += height.value;
494 row += 1;
495 }
496
497 if row > 0 {
498 if ctx.cfg.has_horizontal(row, row) {
499 f.write_char('\n')?;
500 let shape = Position::new(row, count_columns);
501 let rline = RowLine::new(row, line);
502 print_horizontal_line(f, ctx, rline, shape, totalh)?;
503 }
504
505 if margin.bottom.size > 0 {
506 f.write_char('\n')?;
507 print_margin_bottom(f, ctx.cfg, total_width_with_margin)?;
508 }
509 }
510
511 Ok(())
512}
513
514fn print_split_line_spanned<S, F, D, C>(
515 f: &mut F,
516 buf: &mut BTreeMap<usize, Cell<'_, S, C>>,
517 cfg: &SpannedConfig,
518 dimension: &D,
519 row: usize,
520 shape: Position,
521) -> fmt::Result
522where
523 F: Write,
524 D: Dimension,
525 C: ANSIFmt,
526{
527 let mut used_color = None;
528 print_vertical_intersection(f, cfg, (row, 0).into(), shape, &mut used_color)?;
529
530 for col in 0..shape.col {
531 let pos = (row, col).into();
532 if cfg.is_cell_covered_by_both_spans(pos) {
533 continue;
534 }
535
536 let width = dimension.get_width(col);
537 let mut col = col;
538 if cfg.is_cell_covered_by_row_span(pos) {
539 prepare_coloring(f, None, &mut used_color)?;
543 let cell = buf.get_mut(&col).unwrap();
544 cell.display(f)?;
545
546 let original_row = closest_visible_row(cfg, pos).unwrap();
548 if let Some(span) = cfg.get_column_span((original_row, col).into()) {
549 col += span - 1;
550 }
551 } else if width > 0 {
552 let main = cfg.get_horizontal(pos, shape.row);
554 match main {
555 Some(c) => {
556 let clr = cfg.get_horizontal_color(pos, shape.row);
557 prepare_coloring(f, clr, &mut used_color)?;
558 print_horizontal_border(f, cfg, pos, width, c, &used_color)?;
559 }
560 None => repeat_char(f, ' ', width)?,
561 }
562 }
563
564 print_vertical_intersection(f, cfg, (row, col + 1).into(), shape, &mut used_color)?;
565 }
566
567 if let Some(clr) = used_color.take() {
568 clr.fmt_ansi_suffix(f)?;
569 }
570
571 Ok(())
572}
573
574fn print_vertical_intersection<'a, F>(
575 f: &mut F,
576 cfg: &'a SpannedConfig,
577 pos: Position,
578 shape: Position,
579 used_color: &mut Option<&'a ANSIBuf>,
580) -> fmt::Result
581where
582 F: fmt::Write,
583{
584 if !cfg.has_vertical(pos.col, shape.col) {
585 return Ok(());
586 }
587
588 match cfg.get_intersection(pos, shape.into()) {
589 Some(c) => {
590 let clr = cfg.get_intersection_color(pos, shape.into());
591 prepare_coloring(f, clr, used_color)?;
592 f.write_char(c)
593 }
594 None => Ok(()),
595 }
596}
597
598fn print_spanned_columns<'a, F, I, D, C>(
599 f: &mut F,
600 iter: I,
601 buf: &mut BTreeMap<usize, Cell<'a, I::Item, &'a C::Color>>,
602 ctx: &GridCtx<'a, D, C>,
603 rline: RowLine,
604 height: Height,
605 shape: Position,
606) -> fmt::Result
607where
608 F: Write,
609 I: Iterator,
610 I::Item: AsRef<str>,
611 D: Dimension,
612 C: Colors,
613{
614 if height.value == 0 {
615 let mut skip = 0;
619 for (col, cell) in iter.enumerate() {
620 if skip > 0 {
621 skip -= 1;
622 continue;
623 }
624
625 if let Some(cell) = buf.get(&col) {
626 skip = cell.colspan - 1;
627 continue;
628 }
629
630 let pos = Position::new(rline.row, col);
631 let rowspan = ctx.cfg.get_row_span(pos).unwrap_or(1);
632 if rowspan < 2 {
633 continue;
634 }
635
636 let height = if rowspan > 1 {
639 range_height(ctx.cfg, ctx.dims, rline.row, rline.row + rowspan, shape.row)
640 } else {
641 height.value
642 };
643
644 let colspan = ctx.cfg.get_column_span(pos).unwrap_or(1);
645 skip = colspan - 1;
646 let width = if colspan > 1 {
647 range_width(ctx.cfg, ctx.dims, col, col + colspan, shape.col)
648 } else {
649 ctx.dims.get_width(col)
650 };
651
652 let color = ctx.colors.get_color(pos);
653 let mut cell = Cell::new(cell, width, height, ctx.cfg, color, pos);
654 cell.rowspan = rowspan;
655 cell.colspan = colspan;
656
657 buf.insert(col, cell);
658 }
659
660 buf.retain(|_, cell| {
661 cell.rowspan -= 1;
662 cell.rowspan != 0
663 });
664
665 return Ok(());
666 }
667
668 let mut skip = 0;
669 for (col, cell) in iter.enumerate() {
670 if skip > 0 {
671 skip -= 1;
672 continue;
673 }
674
675 if let Some(cell) = buf.get(&col) {
676 skip = cell.colspan - 1;
677 continue;
678 }
679
680 let pos = Position::new(rline.row, col);
681 let colspan = ctx.cfg.get_column_span(pos).unwrap_or(1);
682 skip = colspan - 1;
683
684 let width = if colspan > 1 {
685 range_width(ctx.cfg, ctx.dims, col, col + colspan, shape.col)
686 } else {
687 ctx.dims.get_width(col)
688 };
689
690 let rowspan = ctx.cfg.get_row_span(pos).unwrap_or(1);
691 let height = if rowspan > 1 {
692 range_height(ctx.cfg, ctx.dims, rline.row, rline.row + rowspan, shape.row)
693 } else {
694 height.value
695 };
696
697 let color = ctx.colors.get_color(pos);
698 let mut cell = Cell::new(cell, width, height, ctx.cfg, color, pos);
699 cell.rowspan = rowspan;
700 cell.colspan = colspan;
701
702 buf.insert(col, cell);
703 }
704
705 for i in 0..height.value {
706 let exact_line = rline.line + i;
707 let cell_line = i;
708
709 print_margin_left(f, ctx.cfg, exact_line, height.total)?;
710
711 for (&col, cell) in buf.iter_mut() {
712 let pos = Position::new(rline.row, col);
713 print_vertical_char(f, ctx.cfg, pos, cell_line, height.value, shape.col)?;
714 cell.display(f)?;
715 }
716
717 let pos = Position::new(rline.row, shape.col);
718 print_vertical_char(f, ctx.cfg, pos, cell_line, height.value, shape.col)?;
719
720 print_margin_right(f, ctx.cfg, exact_line, height.total)?;
721
722 if i + 1 != height.value {
723 f.write_char('\n')?;
724 }
725 }
726
727 buf.retain(|_, cell| {
728 cell.rowspan -= 1;
729 cell.rowspan != 0
730 });
731
732 Ok(())
733}
734
735fn print_horizontal_border<F>(
736 f: &mut F,
737 cfg: &SpannedConfig,
738 pos: Position,
739 width: usize,
740 c: char,
741 used_color: &Option<&ANSIBuf>,
742) -> fmt::Result
743where
744 F: Write,
745{
746 if !cfg.is_overridden_horizontal(pos) {
747 return repeat_char(f, c, width);
748 }
749
750 for i in 0..width {
751 let c = cfg.lookup_horizontal_char(pos, i, width).unwrap_or(c);
752 match cfg.lookup_horizontal_color(pos, i, width) {
753 Some(color) => match used_color {
754 Some(clr) => {
755 clr.fmt_ansi_suffix(f)?;
756 color.fmt_ansi_prefix(f)?;
757 f.write_char(c)?;
758 color.fmt_ansi_suffix(f)?;
759 clr.fmt_ansi_prefix(f)?;
760 }
761 None => {
762 color.fmt_ansi_prefix(f)?;
763 f.write_char(c)?;
764 color.fmt_ansi_suffix(f)?;
765 }
766 },
767 _ => f.write_char(c)?,
768 }
769 }
770
771 Ok(())
772}
773
774struct Cell<'a, T, C> {
775 lines: LinesIter<T>,
776 width: usize,
777 indent_top: usize,
778 indent_left: Option<usize>,
779 alignh: AlignmentHorizontal,
780 fmt: Formatting,
781 pad: &'a Sides<Indent>,
782 pad_color: &'a Sides<Option<ANSIBuf>>,
783 color: Option<C>,
784 justification: char,
785 justification_color: Option<&'a ANSIBuf>,
786 rowspan: usize,
787 colspan: usize,
788}
789
790impl<'a, T, C> Cell<'a, T, C> {
791 fn new(
792 text: T,
793 width: usize,
794 height: usize,
795 cfg: &'a SpannedConfig,
796 color: Option<C>,
797 pos: Position,
798 ) -> Cell<'a, T, C>
799 where
800 T: AsRef<str>,
801 {
802 let fmt = cfg.get_formatting(pos);
803 let pad = cfg.get_padding(pos);
804 let pad_color = cfg.get_padding_color(pos);
805 let alignh = *cfg.get_alignment_horizontal(pos);
806 let alignv = *cfg.get_alignment_vertical(pos);
807 let justification = cfg.get_justification(pos);
808 let justification_color = cfg.get_justification_color(pos);
809
810 let (count_lines, skip) = if fmt.vertical_trim {
811 let (len, top, _) = count_empty_lines(text.as_ref());
812 (len, top)
813 } else {
814 (count_lines(text.as_ref()), 0)
815 };
816
817 let indent_top = top_indent(pad, alignv, count_lines, height);
818
819 let mut indent_left = None;
820 if !fmt.allow_lines_alignment {
821 let text_width = text_width(text.as_ref(), fmt.horizontal_trim);
822 let available = width - pad.left.size - pad.right.size;
823 indent_left = Some(calculate_indent(alignh, text_width, available).0);
824 }
825
826 let mut lines = LinesIter::new(text);
827 for _ in 0..skip {
828 let _ = lines.lines.next();
829 }
830
831 Self {
832 lines,
833 indent_left,
834 indent_top,
835 width,
836 alignh,
837 fmt,
838 pad,
839 pad_color,
840 color,
841 justification,
842 justification_color,
843 colspan: 0,
844 rowspan: 0,
845 }
846 }
847
848 fn display<F>(&mut self, f: &mut F) -> fmt::Result
849 where
850 F: Write,
851 C: ANSIFmt,
852 {
853 if self.indent_top > 0 {
854 self.indent_top -= 1;
855 print_padding_n(f, &self.pad.top, self.pad_color.top.as_ref(), self.width)?;
856 return Ok(());
857 }
858
859 let line = match self.lines.lines.next() {
860 Some(line) => line,
861 None => {
862 let color = self.pad_color.bottom.as_ref();
863 print_padding_n(f, &self.pad.bottom, color, self.width)?;
864 return Ok(());
865 }
866 };
867
868 let line = if self.fmt.horizontal_trim && !line.is_empty() {
869 string_trim(&line)
870 } else {
871 line
872 };
873
874 let line_width = get_line_width(&line);
875 let available_width = self.width - self.pad.left.size - self.pad.right.size;
876
877 let (left, right) = if self.fmt.allow_lines_alignment {
878 calculate_indent(self.alignh, line_width, available_width)
879 } else {
880 let left = self.indent_left.expect("must be here");
881 (left, available_width - line_width - left)
882 };
883
884 print_padding(f, &self.pad.left, self.pad_color.left.as_ref())?;
885
886 print_indent(f, self.justification, left, self.justification_color)?;
887 print_text(f, &line, self.color.as_ref())?;
888 print_indent(f, self.justification, right, self.justification_color)?;
889
890 print_padding(f, &self.pad.right, self.pad_color.right.as_ref())?;
891
892 Ok(())
893 }
894}
895
896struct LinesIter<C> {
897 _cell: C,
898 _text: &'static str,
900 lines: Lines<'static>,
902}
903
904impl<C> LinesIter<C> {
905 fn new(cell: C) -> Self
906 where
907 C: AsRef<str>,
908 {
909 let text = cell.as_ref();
919 let text = unsafe {
920 std::str::from_utf8_unchecked(std::slice::from_raw_parts(text.as_ptr(), text.len()))
921 };
922
923 let lines = get_lines(text);
924
925 Self {
926 _cell: cell,
927 _text: text,
928 lines,
929 }
930 }
931}
932
933fn print_text<F>(f: &mut F, text: &str, clr: Option<impl ANSIFmt>) -> fmt::Result
934where
935 F: Write,
936{
937 match clr {
938 Some(color) => {
939 color.fmt_ansi_prefix(f)?;
940 f.write_str(text)?;
941 color.fmt_ansi_suffix(f)
942 }
943 None => f.write_str(text),
944 }
945}
946
947fn prepare_coloring<'a, F>(
948 f: &mut F,
949 clr: Option<&'a ANSIBuf>,
950 used_color: &mut Option<&'a ANSIBuf>,
951) -> fmt::Result
952where
953 F: Write,
954{
955 match clr {
956 Some(clr) => match used_color.as_mut() {
957 Some(used_clr) => {
958 if **used_clr != *clr {
959 used_clr.fmt_ansi_suffix(f)?;
960 clr.fmt_ansi_prefix(f)?;
961 *used_clr = clr;
962 }
963 }
964 None => {
965 clr.fmt_ansi_prefix(f)?;
966 *used_color = Some(clr);
967 }
968 },
969 None => {
970 if let Some(clr) = used_color.take() {
971 clr.fmt_ansi_suffix(f)?
972 }
973 }
974 }
975
976 Ok(())
977}
978
979fn top_indent(
980 padding: &Sides<Indent>,
981 alignment: AlignmentVertical,
982 cell_height: usize,
983 available: usize,
984) -> usize {
985 let height = available - padding.top.size;
986 let indent = indent_from_top(alignment, height, cell_height);
987
988 indent + padding.top.size
989}
990
991fn indent_from_top(alignment: AlignmentVertical, available: usize, real: usize) -> usize {
992 match alignment {
993 AlignmentVertical::Top => 0,
994 AlignmentVertical::Bottom => available - real,
995 AlignmentVertical::Center => (available - real) / 2,
996 }
997}
998
999fn calculate_indent(alignment: AlignmentHorizontal, got: usize, max: usize) -> (usize, usize) {
1000 let diff = max - got;
1001 match alignment {
1002 AlignmentHorizontal::Left => (0, diff),
1003 AlignmentHorizontal::Right => (diff, 0),
1004 AlignmentHorizontal::Center => {
1005 let left = diff / 2;
1006 let rest = diff - left;
1007 (left, rest)
1008 }
1009 }
1010}
1011
1012fn repeat_char<F>(f: &mut F, c: char, n: usize) -> fmt::Result
1013where
1014 F: Write,
1015{
1016 for _ in 0..n {
1017 f.write_char(c)?;
1018 }
1019
1020 Ok(())
1021}
1022
1023fn print_vertical_char<F>(
1024 f: &mut F,
1025 cfg: &SpannedConfig,
1026 pos: Position,
1027 line: usize,
1028 count_lines: usize,
1029 count_columns: usize,
1030) -> fmt::Result
1031where
1032 F: Write,
1033{
1034 let symbol = match cfg.get_vertical(pos, count_columns) {
1035 Some(c) => c,
1036 None => return Ok(()),
1037 };
1038
1039 let symbol = cfg
1040 .lookup_vertical_char(pos, line, count_lines)
1041 .unwrap_or(symbol);
1042
1043 let color = cfg
1044 .get_vertical_color(pos, count_columns)
1045 .or_else(|| cfg.lookup_vertical_color(pos, line, count_lines));
1046
1047 match color {
1048 Some(clr) => {
1049 clr.fmt_ansi_prefix(f)?;
1050 f.write_char(symbol)?;
1051 clr.fmt_ansi_suffix(f)?;
1052 }
1053 None => f.write_char(symbol)?,
1054 }
1055
1056 Ok(())
1057}
1058
1059fn print_margin_top<F>(f: &mut F, cfg: &SpannedConfig, width: usize) -> fmt::Result
1060where
1061 F: Write,
1062{
1063 let indent = cfg.get_margin().top;
1064 let offset = cfg.get_margin_offset().top;
1065 let color = cfg.get_margin_color();
1066 let color = color.top;
1067 print_indent_lines(f, &indent, &offset, color, width)
1068}
1069
1070fn print_margin_bottom<F>(f: &mut F, cfg: &SpannedConfig, width: usize) -> fmt::Result
1071where
1072 F: Write,
1073{
1074 let indent = cfg.get_margin().bottom;
1075 let offset = cfg.get_margin_offset().bottom;
1076 let color = cfg.get_margin_color();
1077 let color = color.bottom;
1078 print_indent_lines(f, &indent, &offset, color, width)
1079}
1080
1081fn print_margin_left<F>(
1082 f: &mut F,
1083 cfg: &SpannedConfig,
1084 line: usize,
1085 height: Option<usize>,
1086) -> fmt::Result
1087where
1088 F: Write,
1089{
1090 let indent = cfg.get_margin().left;
1091 let offset = cfg.get_margin_offset().left;
1092 let color = cfg.get_margin_color();
1093 let color = color.left;
1094 print_margin_vertical(f, indent, offset, color, line, height)
1095}
1096
1097fn print_margin_right<F>(
1098 f: &mut F,
1099 cfg: &SpannedConfig,
1100 line: usize,
1101 height: Option<usize>,
1102) -> fmt::Result
1103where
1104 F: Write,
1105{
1106 let indent = cfg.get_margin().right;
1107 let offset = cfg.get_margin_offset().right;
1108 let color = cfg.get_margin_color();
1109 let color = color.right;
1110 print_margin_vertical(f, indent, offset, color, line, height)
1111}
1112
1113fn print_margin_vertical<F>(
1114 f: &mut F,
1115 indent: Indent,
1116 offset: Offset,
1117 color: Option<&ANSIBuf>,
1118 line: usize,
1119 height: Option<usize>,
1120) -> fmt::Result
1121where
1122 F: Write,
1123{
1124 if indent.size == 0 {
1125 return Ok(());
1126 }
1127
1128 match offset {
1129 Offset::Start(mut offset) => {
1130 if let Some(max) = height {
1131 offset = cmp::min(offset, max);
1132 }
1133
1134 if line >= offset {
1135 print_indent(f, indent.fill, indent.size, color)?;
1136 } else {
1137 repeat_char(f, ' ', indent.size)?;
1138 }
1139 }
1140 Offset::End(mut offset) => {
1141 if let Some(max) = height {
1142 offset = cmp::min(offset, max);
1143 let pos = max - offset;
1144
1145 if line >= pos {
1146 repeat_char(f, ' ', indent.size)?;
1147 } else {
1148 print_indent(f, indent.fill, indent.size, color)?;
1149 }
1150 } else {
1151 print_indent(f, indent.fill, indent.size, color)?;
1152 }
1153 }
1154 }
1155
1156 Ok(())
1157}
1158
1159fn print_indent_lines<F>(
1160 f: &mut F,
1161 indent: &Indent,
1162 offset: &Offset,
1163 color: Option<&ANSIBuf>,
1164 width: usize,
1165) -> fmt::Result
1166where
1167 F: Write,
1168{
1169 if indent.size == 0 {
1170 return Ok(());
1171 }
1172
1173 let (start_offset, end_offset) = match offset {
1174 Offset::Start(start) => (*start, 0),
1175 Offset::End(end) => (0, *end),
1176 };
1177
1178 let start_offset = std::cmp::min(start_offset, width);
1179 let end_offset = std::cmp::min(end_offset, width);
1180 let indent_size = width - start_offset - end_offset;
1181
1182 for i in 0..indent.size {
1183 if start_offset > 0 {
1184 repeat_char(f, ' ', start_offset)?;
1185 }
1186
1187 if indent_size > 0 {
1188 print_indent(f, indent.fill, indent_size, color)?;
1189 }
1190
1191 if end_offset > 0 {
1192 repeat_char(f, ' ', end_offset)?;
1193 }
1194
1195 if i + 1 != indent.size {
1196 f.write_char('\n')?;
1197 }
1198 }
1199
1200 Ok(())
1201}
1202
1203fn print_padding<F>(f: &mut F, pad: &Indent, color: Option<&ANSIBuf>) -> fmt::Result
1204where
1205 F: Write,
1206{
1207 print_indent(f, pad.fill, pad.size, color)
1208}
1209
1210fn print_padding_n<F>(f: &mut F, pad: &Indent, color: Option<&ANSIBuf>, n: usize) -> fmt::Result
1211where
1212 F: Write,
1213{
1214 print_indent(f, pad.fill, n, color)
1215}
1216
1217fn print_indent<F>(f: &mut F, c: char, n: usize, color: Option<&ANSIBuf>) -> fmt::Result
1218where
1219 F: Write,
1220{
1221 if n == 0 {
1222 return Ok(());
1223 }
1224
1225 match color {
1226 Some(color) => {
1227 color.fmt_ansi_prefix(f)?;
1228 repeat_char(f, c, n)?;
1229 color.fmt_ansi_suffix(f)
1230 }
1231 None => repeat_char(f, c, n),
1232 }
1233}
1234
1235fn range_width<D>(cfg: &SpannedConfig, dims: D, start: usize, end: usize, max: usize) -> usize
1236where
1237 D: Dimension,
1238{
1239 let count_borders = count_verticals_in_range(cfg, start, end, max);
1240 let range_width = (start..end).map(|col| dims.get_width(col)).sum::<usize>();
1241
1242 count_borders + range_width
1243}
1244
1245fn range_height<D>(cfg: &SpannedConfig, dims: D, from: usize, end: usize, max: usize) -> usize
1246where
1247 D: Dimension,
1248{
1249 let count_borders = count_horizontals_in_range(cfg, from, end, max);
1250 let range_width = (from..end).map(|col| dims.get_height(col)).sum::<usize>();
1251
1252 count_borders + range_width
1253}
1254
1255fn count_horizontals_in_range(cfg: &SpannedConfig, from: usize, end: usize, max: usize) -> usize {
1256 (from + 1..end)
1257 .map(|i| cfg.has_horizontal(i, max) as usize)
1258 .sum()
1259}
1260
1261fn count_verticals_in_range(cfg: &SpannedConfig, start: usize, end: usize, max: usize) -> usize {
1262 (start..end)
1263 .skip(1)
1264 .map(|i| cfg.has_vertical(i, max) as usize)
1265 .sum()
1266}
1267
1268fn closest_visible_row(cfg: &SpannedConfig, mut pos: Position) -> Option<usize> {
1269 loop {
1270 if cfg.is_cell_visible(pos) {
1271 return Some(pos.row);
1272 }
1273
1274 if pos.row == 0 {
1275 return None;
1276 }
1277
1278 pos -= (1, 0);
1279 }
1280}
1281
1282fn convert_count_rows(row: usize, is_last: bool) -> usize {
1283 if is_last {
1284 row + 1
1285 } else {
1286 row + 2
1287 }
1288}
1289
1290fn string_trim(text: &str) -> Cow<'_, str> {
1292 #[cfg(feature = "ansi")]
1293 {
1294 ansi_str::AnsiStr::ansi_trim(text)
1295 }
1296
1297 #[cfg(not(feature = "ansi"))]
1298 {
1299 text.trim().into()
1300 }
1301}
1302
1303fn total_width<D>(cfg: &SpannedConfig, dimension: &D, count_columns: usize) -> usize
1304where
1305 D: Dimension,
1306{
1307 (0..count_columns)
1308 .map(|i| dimension.get_width(i))
1309 .sum::<usize>()
1310 + cfg.count_vertical(count_columns)
1311}
1312
1313fn total_height<D>(cfg: &SpannedConfig, dimension: &D, count_rows: usize) -> usize
1314where
1315 D: Dimension,
1316{
1317 (0..count_rows)
1318 .map(|i| dimension.get_height(i))
1319 .sum::<usize>()
1320 + cfg.count_horizontal(count_rows)
1321}
1322
1323fn count_empty_lines(cell: &str) -> (usize, usize, usize) {
1324 let mut len = 0;
1325 let mut top = 0;
1326 let mut bottom = 0;
1327 let mut top_check = true;
1328
1329 for line in get_lines(cell) {
1330 let is_empty = line.trim().is_empty();
1331 if top_check {
1332 if is_empty {
1333 top += 1;
1334 } else {
1335 len = 1;
1336 top_check = false;
1337 }
1338
1339 continue;
1340 }
1341
1342 if is_empty {
1343 bottom += 1;
1344 } else {
1345 len += bottom + 1;
1346 bottom = 0;
1347 }
1348 }
1349
1350 (len, top, bottom)
1351}
1352
1353fn text_width(text: &str, trim: bool) -> usize {
1354 if trim {
1355 get_lines(text)
1356 .map(|line| get_line_width(line.trim()))
1357 .max()
1358 .unwrap_or(0)
1359 } else {
1360 get_text_width(text)
1361 }
1362}
1363
1364#[cfg(test)]
1365mod tests {
1366 use super::*;
1367
1368 #[test]
1369 fn vertical_alignment_test() {
1370 use AlignmentVertical::*;
1371
1372 assert_eq!(indent_from_top(Bottom, 1, 1), 0);
1373 assert_eq!(indent_from_top(Top, 1, 1), 0);
1374 assert_eq!(indent_from_top(Center, 1, 1), 0);
1375 assert_eq!(indent_from_top(Bottom, 3, 1), 2);
1376 assert_eq!(indent_from_top(Top, 3, 1), 0);
1377 assert_eq!(indent_from_top(Center, 3, 1), 1);
1378 assert_eq!(indent_from_top(Center, 4, 1), 1);
1379 }
1380
1381 #[test]
1382 fn count_empty_lines_test() {
1383 assert_eq!(count_empty_lines("\n\nsome text\n\n\n"), (1, 2, 3));
1384 assert_eq!(count_empty_lines("\n\nsome\ntext\n\n\n"), (2, 2, 3));
1385 assert_eq!(count_empty_lines("\n\nsome\nsome\ntext\n\n\n"), (3, 2, 3));
1386 assert_eq!(count_empty_lines("\n\n\n\n"), (0, 5, 0));
1387 }
1388}