papergrid/grid/
compact.rs

1//! The module contains a [`CompactGrid`] structure,
2//! which is a relatively strict grid.
3
4use core::{
5    borrow::Borrow,
6    fmt::{self, Display, Write},
7};
8
9use crate::{
10    ansi::{ANSIFmt, ANSIStr},
11    colors::Colors,
12    config::{AlignmentHorizontal, Borders, HorizontalLine, Indent, Sides},
13    dimension::Dimension,
14    records::{IntoRecords, Records},
15    util::string::get_line_width,
16};
17
18use crate::config::compact::CompactConfig;
19
20type ANSIString = ANSIStr<'static>;
21
22/// Grid provides a set of methods for building a text-based table.
23#[derive(Debug, Clone)]
24pub struct CompactGrid<R, D, G, C> {
25    records: R,
26    config: G,
27    dimension: D,
28    colors: C,
29}
30
31impl<R, D, G, C> CompactGrid<R, D, G, C> {
32    /// The new method creates a grid instance with default styles.
33    pub fn new(records: R, config: G, dimension: D, colors: C) -> Self {
34        CompactGrid {
35            records,
36            config,
37            dimension,
38            colors,
39        }
40    }
41}
42
43impl<R, D, G, C> CompactGrid<R, D, G, C> {
44    /// Sets colors map.
45    pub fn with_colors<Colors>(self, colors: Colors) -> CompactGrid<R, D, G, Colors> {
46        CompactGrid {
47            records: self.records,
48            config: self.config,
49            dimension: self.dimension,
50            colors,
51        }
52    }
53
54    /// Builds a table.
55    pub fn build<F>(self, mut f: F) -> fmt::Result
56    where
57        R: Records,
58        <R::Iter as IntoRecords>::Cell: AsRef<str>,
59        D: Dimension,
60        C: Colors,
61        G: Borrow<CompactConfig>,
62        F: Write,
63    {
64        if self.records.count_columns() == 0 {
65            return Ok(());
66        }
67
68        let config = self.config.borrow();
69        print_grid(&mut f, self.records, config, &self.dimension, &self.colors)
70    }
71
72    /// Builds a table into string.
73    ///
74    /// Notice that it consumes self.
75    #[cfg(feature = "std")]
76    #[allow(clippy::inherent_to_string)]
77    pub fn to_string(self) -> String
78    where
79        R: Records,
80        <R::Iter as IntoRecords>::Cell: AsRef<str>,
81        D: Dimension,
82        G: Borrow<CompactConfig>,
83        C: Colors,
84    {
85        let mut buf = String::new();
86        self.build(&mut buf).expect("It's guaranteed to never happen otherwise it's considered an stdlib error or impl error");
87        buf
88    }
89}
90
91impl<R, D, G, C> Display for CompactGrid<R, D, G, C>
92where
93    for<'a> &'a R: Records,
94    for<'a> <<&'a R as Records>::Iter as IntoRecords>::Cell: AsRef<str>,
95    D: Dimension,
96    G: Borrow<CompactConfig>,
97    C: Colors,
98{
99    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100        let records = &self.records;
101        let config = self.config.borrow();
102
103        print_grid(f, records, config, &self.dimension, &self.colors)
104    }
105}
106
107#[derive(Debug, Clone)]
108struct RowConfig<D, C> {
109    margin: Sides<ColoredIndent>,
110    pad: Sides<ColoredIndent>,
111    verticals: HorizontalLine<ColoredIndent>,
112    alignment: AlignmentHorizontal,
113    dims: D,
114    colors: C,
115    count_columns: usize,
116}
117
118impl<D, C> RowConfig<D, C> {
119    fn new(cfg: &CompactConfig, dims: D, colors: C, count_columns: usize) -> Self {
120        let borders_chars = cfg.get_borders();
121        let borders_colors = cfg.get_borders_color();
122        let verticals = create_vertical_borders(borders_chars, borders_colors);
123        let margin = create_margin(cfg);
124        let pad = create_padding(cfg);
125        let alignment = cfg.get_alignment_horizontal();
126
127        Self {
128            margin,
129            pad,
130            alignment,
131            verticals,
132            colors,
133            dims,
134            count_columns,
135        }
136    }
137}
138
139#[derive(Debug, Clone)]
140struct RowIter<I> {
141    iter: I,
142    row: usize,
143}
144
145impl<I> RowIter<I> {
146    fn new(iter: I, row: usize) -> Self {
147        Self { iter, row }
148    }
149}
150
151fn print_grid<F, R, D, C>(
152    f: &mut F,
153    records: R,
154    cfg: &CompactConfig,
155    dims: &D,
156    colors: &C,
157) -> fmt::Result
158where
159    F: Write,
160    R: Records,
161    <R::Iter as IntoRecords>::Cell: AsRef<str>,
162    D: Dimension,
163    C: Colors,
164{
165    let count_columns = records.count_columns();
166    let count_rows = records.hint_count_rows();
167
168    if count_columns == 0 || matches!(count_rows, Some(0)) {
169        return Ok(());
170    }
171
172    let mut records = records.iter_rows().into_iter();
173    let records_first = match records.next() {
174        Some(row) => row,
175        None => return Ok(()),
176    };
177
178    let wtotal = total_width(cfg, dims, count_columns);
179
180    let borders_chars = cfg.get_borders();
181    let borders_colors = cfg.get_borders_color();
182
183    let horizontal_borders = create_horizontal(borders_chars);
184    let horizontal_colors = create_horizontal_colors(borders_colors);
185
186    let margin = create_margin(cfg);
187
188    let rowcfg = RowConfig::new(cfg, dims, colors, count_columns);
189
190    let mut new_line = false;
191
192    if margin.top.space.size > 0 {
193        let width_total = wtotal + margin.left.space.size + margin.right.space.size;
194        let indent = ColoredIndent::new(width_total, margin.top.space.fill, margin.top.color);
195        print_indent_lines(f, indent)?;
196        new_line = true;
197    }
198
199    if borders_chars.has_top() {
200        if new_line {
201            f.write_char('\n')?
202        }
203
204        let borders = create_horizontal_top(borders_chars);
205        let borders_colors = create_horizontal_top_colors(borders_colors);
206        print_horizontal_line(f, dims, &borders, &borders_colors, &margin, count_columns)?;
207
208        new_line = true;
209    }
210
211    if borders_chars.has_horizontal() {
212        if new_line {
213            f.write_char('\n')?;
214        }
215
216        let cells = records_first.into_iter();
217        let iter = RowIter::new(cells, 0);
218        print_grid_row(f, iter, &rowcfg)?;
219
220        for (row, cells) in records.enumerate() {
221            f.write_char('\n')?;
222
223            print_horizontal_line(
224                f,
225                dims,
226                &horizontal_borders,
227                &horizontal_colors,
228                &margin,
229                count_columns,
230            )?;
231
232            f.write_char('\n')?;
233
234            let cells = cells.into_iter();
235            let iter = RowIter::new(cells, row + 1);
236            print_grid_row(f, iter, &rowcfg)?;
237        }
238    } else {
239        if new_line {
240            f.write_char('\n')?;
241        }
242
243        let cells = records_first.into_iter();
244        let iter = RowIter::new(cells, 0);
245        print_grid_row(f, iter, &rowcfg)?;
246
247        for (row, cells) in records.enumerate() {
248            f.write_char('\n')?;
249
250            let cells = cells.into_iter();
251            let iter = RowIter::new(cells, row + 1);
252            print_grid_row(f, iter, &rowcfg)?;
253        }
254    }
255
256    if borders_chars.has_bottom() {
257        f.write_char('\n')?;
258
259        let borders = create_horizontal_bottom(borders_chars);
260        let colors = create_horizontal_bottom_colors(borders_colors);
261        print_horizontal_line(f, dims, &borders, &colors, &margin, count_columns)?;
262    }
263
264    if cfg.get_margin().bottom.size > 0 {
265        f.write_char('\n')?;
266
267        let width_total = wtotal + margin.left.space.size + margin.right.space.size;
268        let indent = ColoredIndent::new(width_total, margin.bottom.space.fill, margin.bottom.color);
269        print_indent_lines(f, indent)?;
270    }
271
272    Ok(())
273}
274
275fn create_margin(cfg: &CompactConfig) -> Sides<ColoredIndent> {
276    let margin = cfg.get_margin();
277    let margin_color = cfg.get_margin_color();
278    Sides::new(
279        ColoredIndent::from_indent(margin.left, margin_color.left),
280        ColoredIndent::from_indent(margin.right, margin_color.right),
281        ColoredIndent::from_indent(margin.top, margin_color.top),
282        ColoredIndent::from_indent(margin.bottom, margin_color.bottom),
283    )
284}
285
286fn create_vertical_borders(
287    borders: &Borders<char>,
288    colors: &Borders<ANSIString>,
289) -> HorizontalLine<ColoredIndent> {
290    let intersect = borders
291        .vertical
292        .map(|c| ColoredIndent::new(0, c, colors.vertical));
293    let left = borders.left.map(|c| ColoredIndent::new(0, c, colors.left));
294    let right = borders
295        .right
296        .map(|c| ColoredIndent::new(0, c, colors.right));
297
298    HorizontalLine::new(None, intersect, left, right)
299}
300
301fn print_horizontal_line<F, D>(
302    f: &mut F,
303    dims: &D,
304    borders: &HorizontalLine<char>,
305    borders_colors: &HorizontalLine<ANSIString>,
306    margin: &Sides<ColoredIndent>,
307    count_columns: usize,
308) -> fmt::Result
309where
310    F: fmt::Write,
311    D: Dimension,
312{
313    let is_not_colored = borders_colors.is_empty();
314
315    print_indent(f, margin.left)?;
316
317    if is_not_colored {
318        print_split_line(f, dims, borders, count_columns)?;
319    } else {
320        print_split_line_colored(f, dims, borders, borders_colors, count_columns)?;
321    }
322
323    print_indent(f, margin.right)?;
324
325    Ok(())
326}
327
328fn print_grid_row<F, I, D, C>(f: &mut F, iter: RowIter<I>, rowcfg: &RowConfig<D, C>) -> fmt::Result
329where
330    F: Write,
331    I: Iterator,
332    I::Item: AsRef<str>,
333    D: Dimension,
334    C: Colors,
335{
336    for _ in 0..rowcfg.pad.top.space.size {
337        print_indent(f, rowcfg.margin.left)?;
338        print_row_columns_empty(f, rowcfg, rowcfg.pad.top.color)?;
339        print_indent(f, rowcfg.margin.right)?;
340
341        f.write_char('\n')?;
342    }
343
344    print_indent(f, rowcfg.margin.left)?;
345    print_row_columns(f, iter, rowcfg)?;
346    print_indent(f, rowcfg.margin.right)?;
347
348    for _ in 0..rowcfg.pad.bottom.space.size {
349        f.write_char('\n')?;
350
351        print_indent(f, rowcfg.margin.left)?;
352        print_row_columns_empty(f, rowcfg, rowcfg.pad.bottom.color)?;
353        print_indent(f, rowcfg.margin.right)?;
354    }
355
356    Ok(())
357}
358
359fn create_padding(cfg: &CompactConfig) -> Sides<ColoredIndent> {
360    let pad = cfg.get_padding();
361    let colors = cfg.get_padding_color();
362    Sides::new(
363        ColoredIndent::new(pad.left.size, pad.left.fill, create_color(colors.left)),
364        ColoredIndent::new(pad.right.size, pad.right.fill, create_color(colors.right)),
365        ColoredIndent::new(pad.top.size, pad.top.fill, create_color(colors.top)),
366        ColoredIndent::new(
367            pad.bottom.size,
368            pad.bottom.fill,
369            create_color(colors.bottom),
370        ),
371    )
372}
373
374fn create_horizontal(b: &Borders<char>) -> HorizontalLine<char> {
375    HorizontalLine::new(b.horizontal, b.intersection, b.left, b.right)
376}
377
378fn create_horizontal_top(b: &Borders<char>) -> HorizontalLine<char> {
379    HorizontalLine::new(b.top, b.top_intersection, b.top_left, b.top_right)
380}
381
382fn create_horizontal_bottom(b: &Borders<char>) -> HorizontalLine<char> {
383    HorizontalLine::new(
384        b.bottom,
385        b.bottom_intersection,
386        b.bottom_left,
387        b.bottom_right,
388    )
389}
390
391fn create_horizontal_colors(b: &Borders<ANSIString>) -> HorizontalLine<ANSIString> {
392    HorizontalLine::new(b.horizontal, b.intersection, b.left, b.right)
393}
394
395fn create_horizontal_top_colors(b: &Borders<ANSIString>) -> HorizontalLine<ANSIString> {
396    HorizontalLine::new(b.top, b.top_intersection, b.top_left, b.top_right)
397}
398
399fn create_horizontal_bottom_colors(b: &Borders<ANSIString>) -> HorizontalLine<ANSIString> {
400    HorizontalLine::new(
401        b.bottom,
402        b.bottom_intersection,
403        b.bottom_left,
404        b.bottom_right,
405    )
406}
407
408fn total_width<D>(cfg: &CompactConfig, dims: &D, count_columns: usize) -> usize
409where
410    D: Dimension,
411{
412    let content_width = total_columns_width(dims, count_columns);
413    let count_verticals = count_verticals(cfg, count_columns);
414
415    content_width + count_verticals
416}
417
418fn total_columns_width<D>(dims: &D, count_columns: usize) -> usize
419where
420    D: Dimension,
421{
422    (0..count_columns).map(|i| dims.get_width(i)).sum::<usize>()
423}
424
425fn count_verticals(cfg: &CompactConfig, count_columns: usize) -> usize {
426    assert!(count_columns > 0);
427
428    let count_verticals = count_columns - 1;
429    let borders = cfg.get_borders();
430    borders.has_vertical() as usize * count_verticals
431        + borders.has_left() as usize
432        + borders.has_right() as usize
433}
434
435fn print_row_columns<F, I, D, C>(
436    f: &mut F,
437    mut iter: RowIter<I>,
438    rowcfg: &RowConfig<D, C>,
439) -> fmt::Result
440where
441    F: Write,
442    I: Iterator,
443    I::Item: AsRef<str>,
444    D: Dimension,
445    C: Colors,
446{
447    if let Some(indent) = rowcfg.verticals.left {
448        print_char(f, indent.space.fill, indent.color)?;
449    }
450
451    let text = iter
452        .iter
453        .next()
454        .expect("we check in the beginning that size must be at least 1 column");
455    let width = rowcfg.dims.get_width(0);
456    let color = rowcfg.colors.get_color((iter.row, 0).into());
457
458    let text = text.as_ref();
459    let text = text.lines().next().unwrap_or("");
460    print_cell(f, text, color, &rowcfg.pad, rowcfg.alignment, width)?;
461
462    match rowcfg.verticals.intersection {
463        Some(indent) => {
464            for (col, text) in iter.iter.enumerate() {
465                let col = col + 1;
466
467                let width = rowcfg.dims.get_width(col);
468                let color = rowcfg.colors.get_color((iter.row, col).into());
469                let text = text.as_ref();
470                let text = text.lines().next().unwrap_or("");
471
472                print_char(f, indent.space.fill, indent.color)?;
473                print_cell(f, text, color, &rowcfg.pad, rowcfg.alignment, width)?;
474            }
475        }
476        None => {
477            for (col, text) in iter.iter.enumerate() {
478                let col = col + 1;
479
480                let width = rowcfg.dims.get_width(col);
481                let color = rowcfg.colors.get_color((iter.row, col).into());
482                let text = text.as_ref();
483                let text = text.lines().next().unwrap_or("");
484
485                print_cell(f, text, color, &rowcfg.pad, rowcfg.alignment, width)?;
486            }
487        }
488    }
489
490    if let Some(indent) = rowcfg.verticals.right {
491        print_char(f, indent.space.fill, indent.color)?;
492    }
493
494    Ok(())
495}
496
497fn print_row_columns_empty<F, D, C>(
498    f: &mut F,
499    rowcfg: &RowConfig<D, C>,
500    color: Option<ANSIString>,
501) -> fmt::Result
502where
503    F: Write,
504    D: Dimension,
505{
506    if let Some(indent) = rowcfg.verticals.left {
507        print_char(f, indent.space.fill, indent.color)?;
508    }
509
510    let width = rowcfg.dims.get_width(0);
511    print_indent(f, ColoredIndent::new(width, ' ', color))?;
512
513    match rowcfg.verticals.intersection {
514        Some(indent) => {
515            for column in 1..rowcfg.count_columns {
516                let width = rowcfg.dims.get_width(column);
517
518                print_char(f, indent.space.fill, indent.color)?;
519                print_indent(f, ColoredIndent::new(width, ' ', color))?;
520            }
521        }
522        None => {
523            for column in 1..rowcfg.count_columns {
524                let width = rowcfg.dims.get_width(column);
525                print_indent(f, ColoredIndent::new(width, ' ', color))?;
526            }
527        }
528    }
529
530    if let Some(indent) = rowcfg.verticals.right {
531        print_char(f, indent.space.fill, indent.color)?;
532    }
533
534    Ok(())
535}
536
537fn print_cell<F, C>(
538    f: &mut F,
539    text: &str,
540    color: Option<C>,
541    padding: &Sides<ColoredIndent>,
542    alignment: AlignmentHorizontal,
543    width: usize,
544) -> fmt::Result
545where
546    F: Write,
547    C: ANSIFmt,
548{
549    let available = width - (padding.left.space.size + padding.right.space.size);
550
551    let text_width = get_line_width(text);
552    let (left, right) = if available > text_width {
553        calculate_indent(alignment, text_width, available)
554    } else {
555        (0, 0)
556    };
557
558    print_indent(f, padding.left)?;
559
560    repeat_char(f, ' ', left)?;
561    print_text(f, text, color)?;
562    repeat_char(f, ' ', right)?;
563
564    print_indent(f, padding.right)?;
565
566    Ok(())
567}
568
569fn print_split_line_colored<F, D>(
570    f: &mut F,
571    dimension: &D,
572    borders: &HorizontalLine<char>,
573    borders_colors: &HorizontalLine<ANSIString>,
574    count_columns: usize,
575) -> fmt::Result
576where
577    F: Write,
578    D: Dimension,
579{
580    let mut used_color = ANSIStr::default();
581    let chars_main = borders.main.unwrap_or(' ');
582
583    if let Some(c) = borders.left {
584        if let Some(color) = &borders_colors.right {
585            prepare_coloring(f, color, &mut used_color)?;
586        }
587
588        f.write_char(c)?;
589    }
590
591    let width = dimension.get_width(0);
592    if width > 0 {
593        if let Some(color) = borders_colors.main {
594            prepare_coloring(f, &color, &mut used_color)?;
595        }
596
597        repeat_char(f, chars_main, width)?;
598    }
599
600    for col in 1..count_columns {
601        if let Some(c) = borders.intersection {
602            if let Some(color) = borders_colors.intersection {
603                prepare_coloring(f, &color, &mut used_color)?;
604            }
605
606            f.write_char(c)?;
607        }
608
609        let width = dimension.get_width(col);
610        if width > 0 {
611            if let Some(color) = borders_colors.main {
612                prepare_coloring(f, &color, &mut used_color)?;
613            }
614
615            repeat_char(f, chars_main, width)?;
616        }
617    }
618
619    if let Some(c) = borders.right {
620        if let Some(color) = &borders_colors.right {
621            prepare_coloring(f, color, &mut used_color)?;
622        }
623
624        f.write_char(c)?;
625    }
626
627    used_color.fmt_ansi_suffix(f)?;
628
629    Ok(())
630}
631
632fn print_split_line<F, D>(
633    f: &mut F,
634    dims: &D,
635    chars: &HorizontalLine<char>,
636    count_columns: usize,
637) -> fmt::Result
638where
639    F: Write,
640    D: Dimension,
641{
642    let chars_main = chars.main.unwrap_or(' ');
643
644    if let Some(c) = chars.left {
645        f.write_char(c)?;
646    }
647
648    let width = dims.get_width(0);
649    if width > 0 {
650        repeat_char(f, chars_main, width)?;
651    }
652
653    for col in 1..count_columns {
654        if let Some(c) = chars.intersection {
655            f.write_char(c)?;
656        }
657
658        let width = dims.get_width(col);
659        if width > 0 {
660            repeat_char(f, chars_main, width)?;
661        }
662    }
663
664    if let Some(c) = chars.right {
665        f.write_char(c)?;
666    }
667
668    Ok(())
669}
670
671fn print_text<F, C>(f: &mut F, text: &str, color: Option<C>) -> fmt::Result
672where
673    F: Write,
674    C: ANSIFmt,
675{
676    match color {
677        Some(color) => {
678            color.fmt_ansi_prefix(f)?;
679            f.write_str(text)?;
680            color.fmt_ansi_suffix(f)?;
681        }
682        None => {
683            f.write_str(text)?;
684        }
685    };
686
687    Ok(())
688}
689
690fn prepare_coloring<F>(f: &mut F, clr: &ANSIString, used: &mut ANSIString) -> fmt::Result
691where
692    F: Write,
693{
694    if *used != *clr {
695        used.fmt_ansi_suffix(f)?;
696        clr.fmt_ansi_prefix(f)?;
697        *used = *clr;
698    }
699
700    Ok(())
701}
702
703fn calculate_indent(
704    alignment: AlignmentHorizontal,
705    text_width: usize,
706    available: usize,
707) -> (usize, usize) {
708    let diff = available - text_width;
709    match alignment {
710        AlignmentHorizontal::Left => (0, diff),
711        AlignmentHorizontal::Right => (diff, 0),
712        AlignmentHorizontal::Center => {
713            let left = diff / 2;
714            let rest = diff - left;
715            (left, rest)
716        }
717    }
718}
719
720fn repeat_char<F>(f: &mut F, c: char, n: usize) -> fmt::Result
721where
722    F: Write,
723{
724    for _ in 0..n {
725        f.write_char(c)?;
726    }
727
728    Ok(())
729}
730
731// todo: replace Option<StaticColor> to StaticColor and check performance
732fn print_char<F>(f: &mut F, c: char, color: Option<ANSIString>) -> fmt::Result
733where
734    F: Write,
735{
736    match color {
737        Some(color) => {
738            color.fmt_ansi_prefix(f)?;
739            f.write_char(c)?;
740            color.fmt_ansi_suffix(f)
741        }
742        None => f.write_char(c),
743    }
744}
745
746fn print_indent_lines<F>(f: &mut F, indent: ColoredIndent) -> fmt::Result
747where
748    F: Write,
749{
750    print_indent(f, indent)?;
751    f.write_char('\n')?;
752
753    for _ in 1..indent.space.size {
754        f.write_char('\n')?;
755        print_indent(f, indent)?;
756    }
757
758    Ok(())
759}
760
761fn print_indent<F>(f: &mut F, indent: ColoredIndent) -> fmt::Result
762where
763    F: Write,
764{
765    match indent.color {
766        Some(color) => {
767            color.fmt_ansi_prefix(f)?;
768            repeat_char(f, indent.space.fill, indent.space.size)?;
769            color.fmt_ansi_suffix(f)?;
770        }
771        None => {
772            repeat_char(f, indent.space.fill, indent.space.size)?;
773        }
774    }
775
776    Ok(())
777}
778
779#[derive(Debug, Clone, Copy)]
780struct ColoredIndent {
781    space: Indent,
782    color: Option<ANSIString>,
783}
784
785impl ColoredIndent {
786    fn new(width: usize, c: char, color: Option<ANSIString>) -> Self {
787        Self {
788            space: Indent::new(width, c),
789            color,
790        }
791    }
792
793    fn from_indent(indent: Indent, color: ANSIString) -> Self {
794        Self {
795            space: indent,
796            color: create_color(color),
797        }
798    }
799}
800
801fn create_color(color: ANSIString) -> Option<ANSIString> {
802    if color.is_empty() {
803        None
804    } else {
805        Some(color)
806    }
807}