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