papergrid/grid/
peekable.rs

1//! The module contains a [`PeekableGrid`] structure.
2
3use core::borrow::Borrow;
4use std::{
5    borrow::Cow,
6    cmp,
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::{ExactRecords, PeekableRecords, Records},
19    util::string::get_line_width,
20};
21
22// TODO: Do we actually need PeekableRecords?
23//       Maybe Cloned iterator will be faster?
24//       Even better &referenced Records
25
26/// Grid provides a set of methods for building a text-based table.
27#[derive(Debug, Clone)]
28pub struct PeekableGrid<R, G, D, C> {
29    records: R,
30    config: G,
31    dimension: D,
32    colors: C,
33}
34
35impl<R, G, D, C> PeekableGrid<R, G, D, C> {
36    /// The new method creates a grid instance with default styles.
37    pub fn new(records: R, config: G, dimension: D, colors: C) -> Self {
38        PeekableGrid {
39            records,
40            config,
41            dimension,
42            colors,
43        }
44    }
45}
46
47impl<R, G, D, C> PeekableGrid<R, G, D, C> {
48    /// Builds a table.
49    pub fn build<F>(&self, mut f: F) -> fmt::Result
50    where
51        R: Records + PeekableRecords + ExactRecords,
52        D: Dimension,
53        C: Colors,
54        G: Borrow<SpannedConfig>,
55        F: Write,
56    {
57        if self.records.count_columns() == 0 || self.records.hint_count_rows() == Some(0) {
58            return Ok(());
59        }
60
61        let ctx = PrintCtx {
62            cfg: self.config.borrow(),
63            colors: &self.colors,
64            dims: &self.dimension,
65            records: &self.records,
66        };
67
68        print_grid(&mut f, ctx)
69    }
70}
71
72impl<R, G, D, C> fmt::Display for PeekableGrid<R, G, D, C>
73where
74    R: Records + PeekableRecords + ExactRecords,
75    D: Dimension,
76    C: Colors,
77    G: Borrow<SpannedConfig>,
78{
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        self.build(f)
81    }
82}
83
84#[derive(Debug, Copy, Clone)]
85struct PrintCtx<'a, R, D, C> {
86    records: &'a R,
87    cfg: &'a SpannedConfig,
88    dims: &'a D,
89    colors: &'a C,
90}
91
92fn print_grid<F, R, D, C>(f: &mut F, ctx: PrintCtx<'_, R, D, C>) -> fmt::Result
93where
94    F: Write,
95    R: Records + PeekableRecords + ExactRecords,
96    D: Dimension,
97    C: Colors,
98{
99    let has_spans = ctx.cfg.has_column_spans() || ctx.cfg.has_row_spans();
100    if has_spans {
101        return grid_spanned::build_grid(f, ctx);
102    }
103
104    let is_basic = !ctx.cfg.has_border_colors()
105        && !ctx.cfg.has_padding_color()
106        && !ctx.cfg.has_justification()
107        && !ctx.cfg.has_offset_chars()
108        && !has_margin(ctx.cfg)
109        && ctx.colors.is_empty();
110
111    if is_basic {
112        grid_basic::build_grid(f, ctx)
113    } else {
114        grid_not_spanned::build_grid(f, ctx)
115    }
116}
117
118fn has_margin(cfg: &SpannedConfig) -> bool {
119    let margin = cfg.get_margin();
120    margin.left.size > 0 || margin.right.size > 0 || margin.top.size > 0 || margin.bottom.size > 0
121}
122
123mod grid_basic {
124    use super::*;
125
126    struct TextCfg {
127        alignment: AlignmentHorizontal,
128        formatting: Formatting,
129        justification: char,
130    }
131
132    #[derive(Debug, Clone, Copy)]
133    struct Shape {
134        count_rows: usize,
135        count_columns: usize,
136    }
137
138    struct HIndent {
139        left: usize,
140        right: usize,
141    }
142
143    pub(super) fn build_grid<F, R, D, C>(f: &mut F, ctx: PrintCtx<'_, R, D, C>) -> fmt::Result
144    where
145        F: Write,
146        R: Records + PeekableRecords + ExactRecords,
147        D: Dimension,
148    {
149        let shape = Shape {
150            count_rows: ctx.records.count_rows(),
151            count_columns: ctx.records.count_columns(),
152        };
153
154        let mut new_line = false;
155
156        for row in 0..shape.count_rows {
157            let height = ctx.dims.get_height(row);
158
159            let has_horizontal = ctx.cfg.has_horizontal(row, shape.count_rows);
160
161            if new_line && (has_horizontal || height > 0) {
162                f.write_char('\n')?;
163                new_line = false;
164            }
165
166            if has_horizontal {
167                print_split_line(f, ctx.cfg, ctx.dims, row, shape)?;
168
169                if height > 0 {
170                    f.write_char('\n')?;
171                } else {
172                    new_line = true;
173                }
174            }
175
176            if height > 0 {
177                print_grid_line(f, &ctx, shape, height, row, 0)?;
178
179                for i in 1..height {
180                    f.write_char('\n')?;
181
182                    print_grid_line(f, &ctx, shape, height, row, i)?;
183                }
184
185                new_line = true;
186            }
187        }
188
189        if ctx.cfg.has_horizontal(shape.count_rows, shape.count_rows) {
190            f.write_char('\n')?;
191            print_split_line(f, ctx.cfg, ctx.dims, shape.count_rows, shape)?;
192        }
193
194        Ok(())
195    }
196
197    fn print_grid_line<F, R, D, C>(
198        f: &mut F,
199        ctx: &PrintCtx<'_, R, D, C>,
200        shape: Shape,
201        height: usize,
202        row: usize,
203        line: usize,
204    ) -> fmt::Result
205    where
206        F: Write,
207        R: Records + PeekableRecords + ExactRecords,
208        D: Dimension,
209    {
210        for col in 0..shape.count_columns {
211            let pos = (row, col).into();
212            print_vertical_char(f, ctx.cfg, pos, shape.count_columns)?;
213            print_cell_line(f, ctx, height, pos, line)?;
214        }
215
216        let pos = (row, shape.count_columns).into();
217        print_vertical_char(f, ctx.cfg, pos, shape.count_columns)?;
218
219        Ok(())
220    }
221
222    fn print_split_line<F, D>(
223        f: &mut F,
224        cfg: &SpannedConfig,
225        dimension: &D,
226        row: usize,
227        shape: Shape,
228    ) -> fmt::Result
229    where
230        F: Write,
231        D: Dimension,
232    {
233        print_vertical_intersection(f, cfg, (row, 0).into(), shape)?;
234
235        for col in 0..shape.count_columns {
236            let width = dimension.get_width(col);
237
238            // general case
239            if width > 0 {
240                let pos = (row, col).into();
241                let main = cfg.get_horizontal(pos, shape.count_rows);
242                match main {
243                    Some(c) => repeat_char(f, c, width)?,
244                    None => repeat_char(f, ' ', width)?,
245                }
246            }
247
248            let pos = (row, col + 1).into();
249            print_vertical_intersection(f, cfg, pos, shape)?;
250        }
251
252        Ok(())
253    }
254
255    fn print_vertical_intersection<F>(
256        f: &mut F,
257        cfg: &SpannedConfig,
258        pos: Position,
259        shape: Shape,
260    ) -> fmt::Result
261    where
262        F: fmt::Write,
263    {
264        let intersection = cfg.get_intersection(pos, (shape.count_rows, shape.count_columns));
265        let intersection = match intersection {
266            Some(c) => c,
267            None => return Ok(()),
268        };
269
270        // We need to make sure that we need to print it.
271        // Specifically for cases where we have a limited amount of verticals.
272        //
273        // todo: Yes... this check very likely degrages performance a bit,
274        //       Surely we need to rethink it.
275        if !cfg.has_vertical(pos.col, shape.count_columns) {
276            return Ok(());
277        }
278
279        f.write_char(intersection)?;
280
281        Ok(())
282    }
283
284    fn print_vertical_char<F>(
285        f: &mut F,
286        cfg: &SpannedConfig,
287        pos: Position,
288        count_columns: usize,
289    ) -> fmt::Result
290    where
291        F: Write,
292    {
293        let symbol = match cfg.get_vertical(pos, count_columns) {
294            Some(c) => c,
295            None => return Ok(()),
296        };
297
298        f.write_char(symbol)?;
299
300        Ok(())
301    }
302
303    fn print_cell_line<F, R, D, C>(
304        f: &mut F,
305        ctx: &PrintCtx<'_, R, D, C>,
306        height: usize,
307        pos: Position,
308        line: usize,
309    ) -> fmt::Result
310    where
311        F: Write,
312        R: Records + PeekableRecords + ExactRecords,
313        D: Dimension,
314    {
315        let width = ctx.dims.get_width(pos.col);
316
317        let pad = ctx.cfg.get_padding(pos);
318        let valignment = *ctx.cfg.get_alignment_vertical(pos);
319        let text_cfg = TextCfg {
320            alignment: *ctx.cfg.get_alignment_horizontal(pos),
321            formatting: ctx.cfg.get_formatting(pos),
322            justification: ctx.cfg.get_justification(pos),
323        };
324
325        let mut cell_height = ctx.records.count_lines(pos);
326        if text_cfg.formatting.vertical_trim {
327            cell_height -= count_empty_lines_at_start(ctx.records, pos)
328                + count_empty_lines_at_end(ctx.records, pos);
329        }
330
331        if cell_height > height {
332            // it may happen if the height estimation decide so
333            cell_height = height;
334        }
335
336        let indent = top_indent(pad, valignment, cell_height, height);
337        if indent > line {
338            return repeat_char(f, pad.top.fill, width);
339        }
340
341        let mut index = line - indent;
342        let cell_has_this_line = cell_height > index;
343        if !cell_has_this_line {
344            // happens when other cells have bigger height
345            return repeat_char(f, pad.bottom.fill, width);
346        }
347
348        if text_cfg.formatting.vertical_trim {
349            let empty_lines = count_empty_lines_at_start(ctx.records, pos);
350            index += empty_lines;
351
352            if index > ctx.records.count_lines(pos) {
353                return repeat_char(f, pad.top.fill, width);
354            }
355        }
356
357        let width = width - pad.left.size - pad.right.size;
358
359        repeat_char(f, pad.left.fill, pad.left.size)?;
360        print_line(f, ctx.records, pos, index, width, text_cfg)?;
361        repeat_char(f, pad.right.fill, pad.right.size)?;
362
363        Ok(())
364    }
365
366    fn print_line<F, R>(
367        f: &mut F,
368        records: &R,
369        pos: Position,
370        index: usize,
371        available: usize,
372        cfg: TextCfg,
373    ) -> fmt::Result
374    where
375        F: Write,
376        R: Records + PeekableRecords,
377    {
378        let line = records.get_line(pos, index);
379        let (line, line_width) = if cfg.formatting.horizontal_trim {
380            let line = string_trim(line);
381            let width = get_line_width(&line);
382            (line, width)
383        } else {
384            let width = records.get_line_width(pos, index);
385            (Cow::Borrowed(line), width)
386        };
387
388        if cfg.formatting.allow_lines_alignment {
389            let indent = calculate_indent(cfg.alignment, line_width, available);
390            return print_text_padded(f, &line, cfg.justification, indent);
391        }
392
393        let cell_width = if cfg.formatting.horizontal_trim {
394            (0..records.count_lines(pos))
395                .map(|i| records.get_line(pos, i))
396                .map(|line| get_line_width(line.trim()))
397                .max()
398                .unwrap_or_default()
399        } else {
400            records.get_width(pos)
401        };
402
403        let indent = calculate_indent(cfg.alignment, cell_width, available);
404        print_text_padded(f, &line, cfg.justification, indent)?;
405
406        let rest_width = cell_width - line_width;
407        repeat_char(f, cfg.justification, rest_width)?;
408
409        Ok(())
410    }
411
412    fn print_text_padded<F>(f: &mut F, text: &str, space: char, indent: HIndent) -> fmt::Result
413    where
414        F: Write,
415    {
416        repeat_char(f, space, indent.left)?;
417        f.write_str(text)?;
418        repeat_char(f, space, indent.right)?;
419
420        Ok(())
421    }
422
423    fn top_indent(
424        pad: &Sides<Indent>,
425        alignment: AlignmentVertical,
426        height: usize,
427        available: usize,
428    ) -> usize {
429        let available = available - pad.top.size;
430        let indent = indent_from_top(alignment, available, height);
431
432        indent + pad.top.size
433    }
434
435    fn indent_from_top(alignment: AlignmentVertical, available: usize, real: usize) -> usize {
436        match alignment {
437            AlignmentVertical::Top => 0,
438            AlignmentVertical::Bottom => available - real,
439            AlignmentVertical::Center => (available - real) / 2,
440        }
441    }
442
443    fn calculate_indent(alignment: AlignmentHorizontal, width: usize, available: usize) -> HIndent {
444        let diff = available - width;
445
446        let (left, right) = match alignment {
447            AlignmentHorizontal::Left => (0, diff),
448            AlignmentHorizontal::Right => (diff, 0),
449            AlignmentHorizontal::Center => {
450                let left = diff / 2;
451                let rest = diff - left;
452                (left, rest)
453            }
454        };
455
456        HIndent { left, right }
457    }
458
459    fn repeat_char<F>(f: &mut F, c: char, n: usize) -> fmt::Result
460    where
461        F: Write,
462    {
463        for _ in 0..n {
464            f.write_char(c)?;
465        }
466
467        Ok(())
468    }
469
470    fn count_empty_lines_at_end<R>(records: &R, pos: Position) -> usize
471    where
472        R: Records + PeekableRecords,
473    {
474        (0..records.count_lines(pos))
475            .map(|i| records.get_line(pos, i))
476            .rev()
477            .take_while(|l| l.trim().is_empty())
478            .count()
479    }
480
481    fn count_empty_lines_at_start<R>(records: &R, pos: Position) -> usize
482    where
483        R: Records + PeekableRecords,
484    {
485        (0..records.count_lines(pos))
486            .map(|i| records.get_line(pos, i))
487            .take_while(|s| s.trim().is_empty())
488            .count()
489    }
490
491    /// Trims a string.
492    fn string_trim(text: &str) -> Cow<'_, str> {
493        #[cfg(feature = "ansi")]
494        {
495            ansi_str::AnsiStr::ansi_trim(text)
496        }
497
498        #[cfg(not(feature = "ansi"))]
499        {
500            text.trim().into()
501        }
502    }
503}
504
505mod grid_not_spanned {
506    use super::*;
507
508    struct TextCfg<C, C1> {
509        alignment: AlignmentHorizontal,
510        formatting: Formatting,
511        color: Option<C>,
512        justification: Colored<char, C1>,
513    }
514
515    struct Colored<T, C> {
516        data: T,
517        color: Option<C>,
518    }
519
520    impl<T, C> Colored<T, C> {
521        fn new(data: T, color: Option<C>) -> Self {
522            Self { data, color }
523        }
524    }
525
526    #[derive(Debug, Clone, Copy)]
527    struct Shape {
528        count_rows: usize,
529        count_columns: usize,
530    }
531
532    struct HIndent {
533        left: usize,
534        right: usize,
535    }
536
537    pub(super) fn build_grid<F, R, D, C>(f: &mut F, ctx: PrintCtx<'_, R, D, C>) -> fmt::Result
538    where
539        F: Write,
540        R: Records + PeekableRecords + ExactRecords,
541        D: Dimension,
542        C: Colors,
543    {
544        let shape = Shape {
545            count_rows: ctx.records.count_rows(),
546            count_columns: ctx.records.count_columns(),
547        };
548
549        let total_width = total_width(ctx.cfg, ctx.dims, shape.count_columns);
550
551        let margin = ctx.cfg.get_margin();
552        let total_width_with_margin = total_width + margin.left.size + margin.right.size;
553
554        let total_height = total_height(ctx.cfg, ctx.dims, shape.count_rows);
555
556        if margin.top.size > 0 {
557            print_margin_top(f, ctx.cfg, total_width_with_margin)?;
558            f.write_char('\n')?;
559        }
560
561        let mut table_line = 0;
562        let mut prev_empty_horizontal = false;
563        for row in 0..shape.count_rows {
564            let height = ctx.dims.get_height(row);
565
566            if ctx.cfg.has_horizontal(row, shape.count_rows) {
567                if prev_empty_horizontal {
568                    f.write_char('\n')?;
569                }
570
571                print_margin_left(f, ctx.cfg, table_line, total_height)?;
572                print_split_line(f, ctx.cfg, ctx.dims, row, shape)?;
573                print_margin_right(f, ctx.cfg, table_line, total_height)?;
574
575                if height > 0 {
576                    f.write_char('\n')?;
577                    prev_empty_horizontal = false;
578                } else {
579                    prev_empty_horizontal = true;
580                }
581
582                table_line += 1;
583            } else if height > 0 && prev_empty_horizontal {
584                f.write_char('\n')?;
585                prev_empty_horizontal = false;
586            }
587
588            for i in 0..height {
589                print_margin_left(f, ctx.cfg, table_line, total_height)?;
590
591                for col in 0..shape.count_columns {
592                    let pos = (row, col).into();
593                    print_vertical_char(f, ctx.cfg, pos, i, height, shape.count_columns)?;
594                    print_cell_line(f, &ctx, height, pos, i)?;
595
596                    let is_last_column = col + 1 == shape.count_columns;
597                    if is_last_column {
598                        let pos = (row, col + 1).into();
599                        print_vertical_char(f, ctx.cfg, pos, i, height, shape.count_columns)?;
600                    }
601                }
602
603                print_margin_right(f, ctx.cfg, table_line, total_height)?;
604
605                let is_last_line = i + 1 == height;
606                let is_last_row = row + 1 == shape.count_rows;
607                if !(is_last_line && is_last_row) {
608                    f.write_char('\n')?;
609                }
610
611                table_line += 1;
612            }
613        }
614
615        if ctx.cfg.has_horizontal(shape.count_rows, shape.count_rows) {
616            f.write_char('\n')?;
617            print_margin_left(f, ctx.cfg, table_line, total_height)?;
618            print_split_line(f, ctx.cfg, ctx.dims, shape.count_rows, shape)?;
619            print_margin_right(f, ctx.cfg, table_line, total_height)?;
620        }
621
622        if margin.bottom.size > 0 {
623            f.write_char('\n')?;
624            print_margin_bottom(f, ctx.cfg, total_width_with_margin)?;
625        }
626
627        Ok(())
628    }
629
630    fn print_split_line<F, D>(
631        f: &mut F,
632        cfg: &SpannedConfig,
633        dimension: &D,
634        row: usize,
635        shape: Shape,
636    ) -> fmt::Result
637    where
638        F: Write,
639        D: Dimension,
640    {
641        let mut used_color = None;
642        print_vertical_intersection(f, cfg, (row, 0).into(), shape, &mut used_color)?;
643
644        for col in 0..shape.count_columns {
645            let width = dimension.get_width(col);
646
647            // general case
648            if width > 0 {
649                let pos = (row, col).into();
650                let main = cfg.get_horizontal(pos, shape.count_rows);
651                match main {
652                    Some(c) => {
653                        let clr = cfg.get_horizontal_color(pos, shape.count_rows);
654                        prepare_coloring(f, clr, &mut used_color)?;
655                        print_horizontal_border(f, cfg, pos, width, c, &used_color)?;
656                    }
657                    None => repeat_char(f, ' ', width)?,
658                }
659            }
660
661            let pos = (row, col + 1).into();
662            print_vertical_intersection(f, cfg, pos, shape, &mut used_color)?;
663        }
664
665        if let Some(clr) = used_color.take() {
666            clr.fmt_ansi_suffix(f)?;
667        }
668
669        Ok(())
670    }
671
672    fn print_vertical_intersection<'a, F>(
673        f: &mut F,
674        cfg: &'a SpannedConfig,
675        pos: Position,
676        shape: Shape,
677        used_color: &mut Option<&'a ANSIBuf>,
678    ) -> fmt::Result
679    where
680        F: fmt::Write,
681    {
682        let intersection = match cfg.get_intersection(pos, (shape.count_rows, shape.count_columns))
683        {
684            Some(c) => c,
685            None => return Ok(()),
686        };
687
688        // We need to make sure that we need to print it.
689        // Specifically for cases where we have a limited amount of verticals.
690        //
691        // todo: Yes... this check very likely degrages performance a bit,
692        //       Surely we need to rethink it.
693        if !cfg.has_vertical(pos.col, shape.count_columns) {
694            return Ok(());
695        }
696
697        let color = cfg.get_intersection_color(pos, (shape.count_rows, shape.count_columns));
698        prepare_coloring(f, color, used_color)?;
699        f.write_char(intersection)?;
700
701        Ok(())
702    }
703
704    fn prepare_coloring<'a, F>(
705        f: &mut F,
706        clr: Option<&'a ANSIBuf>,
707        used_color: &mut Option<&'a ANSIBuf>,
708    ) -> fmt::Result
709    where
710        F: Write,
711    {
712        match clr {
713            Some(clr) => match used_color.as_mut() {
714                Some(used_clr) => {
715                    if **used_clr != *clr {
716                        used_clr.fmt_ansi_suffix(f)?;
717                        clr.fmt_ansi_prefix(f)?;
718                        *used_clr = clr;
719                    }
720                }
721                None => {
722                    clr.fmt_ansi_prefix(f)?;
723                    *used_color = Some(clr);
724                }
725            },
726            None => {
727                if let Some(clr) = used_color.take() {
728                    clr.fmt_ansi_suffix(f)?
729                }
730            }
731        }
732
733        Ok(())
734    }
735
736    fn print_vertical_char<F>(
737        f: &mut F,
738        cfg: &SpannedConfig,
739        pos: Position,
740        line: usize,
741        count_lines: usize,
742        count_columns: usize,
743    ) -> fmt::Result
744    where
745        F: Write,
746    {
747        let symbol = match cfg.get_vertical(pos, count_columns) {
748            Some(c) => c,
749            None => return Ok(()),
750        };
751
752        let symbol = cfg
753            .lookup_vertical_char(pos, line, count_lines)
754            .unwrap_or(symbol);
755
756        let color = cfg
757            .get_vertical_color(pos, count_columns)
758            .or_else(|| cfg.lookup_vertical_color(pos, line, count_lines));
759
760        match color {
761            Some(clr) => {
762                clr.fmt_ansi_prefix(f)?;
763                f.write_char(symbol)?;
764                clr.fmt_ansi_suffix(f)?;
765            }
766            None => f.write_char(symbol)?,
767        }
768
769        Ok(())
770    }
771
772    fn print_horizontal_border<F>(
773        f: &mut F,
774        cfg: &SpannedConfig,
775        pos: Position,
776        width: usize,
777        c: char,
778        used_color: &Option<&ANSIBuf>,
779    ) -> fmt::Result
780    where
781        F: Write,
782    {
783        if !cfg.is_overridden_horizontal(pos) {
784            return repeat_char(f, c, width);
785        }
786
787        for i in 0..width {
788            let c = cfg.lookup_horizontal_char(pos, i, width).unwrap_or(c);
789            match cfg.lookup_horizontal_color(pos, i, width) {
790                Some(color) => match used_color {
791                    Some(clr) => {
792                        clr.fmt_ansi_suffix(f)?;
793                        color.fmt_ansi_prefix(f)?;
794                        f.write_char(c)?;
795                        color.fmt_ansi_suffix(f)?;
796                        clr.fmt_ansi_prefix(f)?;
797                    }
798                    None => {
799                        color.fmt_ansi_prefix(f)?;
800                        f.write_char(c)?;
801                        color.fmt_ansi_suffix(f)?;
802                    }
803                },
804                _ => f.write_char(c)?,
805            }
806        }
807
808        Ok(())
809    }
810
811    fn print_cell_line<F, R, D, C>(
812        f: &mut F,
813        ctx: &PrintCtx<'_, R, D, C>,
814        height: usize,
815        pos: Position,
816        line: usize,
817    ) -> fmt::Result
818    where
819        F: Write,
820        R: Records + PeekableRecords + ExactRecords,
821        C: Colors,
822        D: Dimension,
823    {
824        let width = ctx.dims.get_width(pos.col);
825
826        let formatting = ctx.cfg.get_formatting(pos);
827        let text_cfg = TextCfg {
828            alignment: *ctx.cfg.get_alignment_horizontal(pos),
829            color: ctx.colors.get_color(pos),
830            justification: Colored::new(
831                ctx.cfg.get_justification(pos),
832                ctx.cfg.get_justification_color(pos),
833            ),
834            formatting,
835        };
836
837        let pad = ctx.cfg.get_padding(pos);
838        let pad_color = ctx.cfg.get_padding_color(pos);
839        let valignment = *ctx.cfg.get_alignment_vertical(pos);
840
841        let mut cell_height = ctx.records.count_lines(pos);
842        if formatting.vertical_trim {
843            cell_height -= count_empty_lines_at_start(ctx.records, pos)
844                + count_empty_lines_at_end(ctx.records, pos);
845        }
846
847        if cell_height > height {
848            // it may happen if the height estimation decide so
849            cell_height = height;
850        }
851
852        let indent = top_indent(pad, valignment, cell_height, height);
853        if indent > line {
854            return print_indent(f, pad.top.fill, width, pad_color.top.as_ref());
855        }
856
857        let mut index = line - indent;
858        let cell_has_this_line = cell_height > index;
859        if !cell_has_this_line {
860            // happens when other cells have bigger height
861            return print_indent(f, pad.bottom.fill, width, pad_color.bottom.as_ref());
862        }
863
864        if formatting.vertical_trim {
865            let empty_lines = count_empty_lines_at_start(ctx.records, pos);
866            index += empty_lines;
867
868            if index > ctx.records.count_lines(pos) {
869                return print_indent(f, pad.top.fill, width, pad_color.top.as_ref());
870            }
871        }
872
873        let width = width - pad.left.size - pad.right.size;
874
875        print_indent(f, pad.left.fill, pad.left.size, pad_color.left.as_ref())?;
876        print_line(f, ctx.records, pos, index, width, text_cfg)?;
877        print_indent(f, pad.right.fill, pad.right.size, pad_color.right.as_ref())?;
878
879        Ok(())
880    }
881
882    fn print_line<F, R, C>(
883        f: &mut F,
884        records: &R,
885        pos: Position,
886        index: usize,
887        available: usize,
888        cfg: TextCfg<C, &'_ ANSIBuf>,
889    ) -> fmt::Result
890    where
891        F: Write,
892        R: Records + PeekableRecords,
893        C: ANSIFmt,
894    {
895        let line = records.get_line(pos, index);
896        let (line, line_width) = if cfg.formatting.horizontal_trim {
897            let line = string_trim(line);
898            let width = get_line_width(&line);
899            (line, width)
900        } else {
901            let width = records.get_line_width(pos, index);
902            (Cow::Borrowed(line), width)
903        };
904
905        if cfg.formatting.allow_lines_alignment {
906            let indent = calculate_indent(cfg.alignment, line_width, available);
907            let text = Colored::new(line.as_ref(), cfg.color);
908            return print_text_padded(f, &text, &cfg.justification, indent);
909        }
910
911        let cell_width = if cfg.formatting.horizontal_trim {
912            (0..records.count_lines(pos))
913                .map(|i| records.get_line(pos, i))
914                .map(|line| get_line_width(line.trim()))
915                .max()
916                .unwrap_or_default()
917        } else {
918            records.get_width(pos)
919        };
920
921        let indent = calculate_indent(cfg.alignment, cell_width, available);
922        let text = Colored::new(line.as_ref(), cfg.color);
923        print_text_padded(f, &text, &cfg.justification, indent)?;
924
925        let rest_width = cell_width - line_width;
926        print_indent2(f, &cfg.justification, rest_width)?;
927
928        Ok(())
929    }
930
931    fn print_text_padded<F, C, C1>(
932        f: &mut F,
933        text: &Colored<&str, C>,
934        justification: &Colored<char, C1>,
935        indent: HIndent,
936    ) -> fmt::Result
937    where
938        F: Write,
939        C: ANSIFmt,
940        C1: ANSIFmt,
941    {
942        print_indent2(f, justification, indent.left)?;
943        print_text2(f, text)?;
944        print_indent2(f, justification, indent.right)?;
945
946        Ok(())
947    }
948
949    fn print_text2<F, C>(f: &mut F, text: &Colored<&str, C>) -> fmt::Result
950    where
951        F: Write,
952        C: ANSIFmt,
953    {
954        match &text.color {
955            Some(color) => {
956                color.fmt_ansi_prefix(f)?;
957                f.write_str(text.data)?;
958                color.fmt_ansi_suffix(f)
959            }
960            None => f.write_str(text.data),
961        }
962    }
963
964    fn top_indent(
965        pad: &Sides<Indent>,
966        alignment: AlignmentVertical,
967        height: usize,
968        available: usize,
969    ) -> usize {
970        let available = available - pad.top.size;
971        let indent = indent_from_top(alignment, available, height);
972
973        indent + pad.top.size
974    }
975
976    fn indent_from_top(alignment: AlignmentVertical, available: usize, real: usize) -> usize {
977        match alignment {
978            AlignmentVertical::Top => 0,
979            AlignmentVertical::Bottom => available - real,
980            AlignmentVertical::Center => (available - real) / 2,
981        }
982    }
983
984    fn calculate_indent(alignment: AlignmentHorizontal, width: usize, available: usize) -> HIndent {
985        let diff = available - width;
986
987        let (left, right) = match alignment {
988            AlignmentHorizontal::Left => (0, diff),
989            AlignmentHorizontal::Right => (diff, 0),
990            AlignmentHorizontal::Center => {
991                let left = diff / 2;
992                let rest = diff - left;
993                (left, rest)
994            }
995        };
996
997        HIndent { left, right }
998    }
999
1000    fn repeat_char<F>(f: &mut F, c: char, n: usize) -> fmt::Result
1001    where
1002        F: Write,
1003    {
1004        for _ in 0..n {
1005            f.write_char(c)?;
1006        }
1007
1008        Ok(())
1009    }
1010
1011    fn count_empty_lines_at_end<R>(records: &R, pos: Position) -> usize
1012    where
1013        R: Records + PeekableRecords,
1014    {
1015        (0..records.count_lines(pos))
1016            .map(|i| records.get_line(pos, i))
1017            .rev()
1018            .take_while(|l| l.trim().is_empty())
1019            .count()
1020    }
1021
1022    fn count_empty_lines_at_start<R>(records: &R, pos: Position) -> usize
1023    where
1024        R: Records + PeekableRecords,
1025    {
1026        (0..records.count_lines(pos))
1027            .map(|i| records.get_line(pos, i))
1028            .take_while(|s| s.trim().is_empty())
1029            .count()
1030    }
1031
1032    fn total_width<D>(cfg: &SpannedConfig, dimension: &D, count_columns: usize) -> usize
1033    where
1034        D: Dimension,
1035    {
1036        (0..count_columns)
1037            .map(|i| dimension.get_width(i))
1038            .sum::<usize>()
1039            + cfg.count_vertical(count_columns)
1040    }
1041
1042    fn total_height<D>(cfg: &SpannedConfig, dimension: &D, count_rows: usize) -> usize
1043    where
1044        D: Dimension,
1045    {
1046        (0..count_rows)
1047            .map(|i| dimension.get_height(i))
1048            .sum::<usize>()
1049            + cfg.count_horizontal(count_rows)
1050    }
1051
1052    fn print_margin_top<F>(f: &mut F, cfg: &SpannedConfig, width: usize) -> fmt::Result
1053    where
1054        F: Write,
1055    {
1056        let indent = cfg.get_margin().top;
1057        let offset = cfg.get_margin_offset().top;
1058        let color = cfg.get_margin_color();
1059        let color = color.top;
1060        print_indent_lines(f, indent, offset, color, width)
1061    }
1062
1063    fn print_margin_bottom<F>(f: &mut F, cfg: &SpannedConfig, width: usize) -> fmt::Result
1064    where
1065        F: Write,
1066    {
1067        let indent = cfg.get_margin().bottom;
1068        let offset = cfg.get_margin_offset().bottom;
1069        let color = cfg.get_margin_color();
1070        let color = color.bottom;
1071        print_indent_lines(f, indent, offset, color, width)
1072    }
1073
1074    fn print_margin_left<F>(
1075        f: &mut F,
1076        cfg: &SpannedConfig,
1077        line: usize,
1078        height: usize,
1079    ) -> fmt::Result
1080    where
1081        F: Write,
1082    {
1083        let indent = cfg.get_margin().left;
1084        let offset = cfg.get_margin_offset().left;
1085        let color = cfg.get_margin_color();
1086        let color = color.left;
1087        print_margin_vertical(f, indent, offset, color, line, height)
1088    }
1089
1090    fn print_margin_right<F>(
1091        f: &mut F,
1092        cfg: &SpannedConfig,
1093        line: usize,
1094        height: usize,
1095    ) -> fmt::Result
1096    where
1097        F: Write,
1098    {
1099        let indent = cfg.get_margin().right;
1100        let offset = cfg.get_margin_offset().right;
1101        let color = cfg.get_margin_color();
1102        let color = color.right;
1103        print_margin_vertical(f, indent, offset, color, line, height)
1104    }
1105
1106    fn print_margin_vertical<F>(
1107        f: &mut F,
1108        indent: Indent,
1109        offset: Offset,
1110        color: Option<&ANSIBuf>,
1111        line: usize,
1112        height: usize,
1113    ) -> fmt::Result
1114    where
1115        F: Write,
1116    {
1117        if indent.size == 0 {
1118            return Ok(());
1119        }
1120
1121        match offset {
1122            Offset::Start(offset) => {
1123                let offset = cmp::min(offset, height);
1124                if line >= offset {
1125                    print_indent(f, indent.fill, indent.size, color)?;
1126                } else {
1127                    repeat_char(f, ' ', indent.size)?;
1128                }
1129            }
1130            Offset::End(offset) => {
1131                let offset = cmp::min(offset, height);
1132                let pos = height - offset;
1133
1134                if line >= pos {
1135                    repeat_char(f, ' ', indent.size)?;
1136                } else {
1137                    print_indent(f, indent.fill, indent.size, color)?;
1138                }
1139            }
1140        }
1141
1142        Ok(())
1143    }
1144
1145    fn print_indent_lines<F>(
1146        f: &mut F,
1147        indent: Indent,
1148        offset: Offset,
1149        color: Option<&ANSIBuf>,
1150        width: usize,
1151    ) -> fmt::Result
1152    where
1153        F: Write,
1154    {
1155        if indent.size == 0 {
1156            return Ok(());
1157        }
1158
1159        let (start_offset, end_offset) = match offset {
1160            Offset::Start(start) => (start, 0),
1161            Offset::End(end) => (0, end),
1162        };
1163
1164        let start_offset = std::cmp::min(start_offset, width);
1165        let end_offset = std::cmp::min(end_offset, width);
1166        let indent_size = width - start_offset - end_offset;
1167
1168        for i in 0..indent.size {
1169            if start_offset > 0 {
1170                repeat_char(f, ' ', start_offset)?;
1171            }
1172
1173            if indent_size > 0 {
1174                print_indent(f, indent.fill, indent_size, color)?;
1175            }
1176
1177            if end_offset > 0 {
1178                repeat_char(f, ' ', end_offset)?;
1179            }
1180
1181            if i + 1 != indent.size {
1182                f.write_char('\n')?;
1183            }
1184        }
1185
1186        Ok(())
1187    }
1188
1189    fn print_indent<F, C>(f: &mut F, c: char, n: usize, color: Option<C>) -> fmt::Result
1190    where
1191        F: Write,
1192        C: ANSIFmt,
1193    {
1194        if n == 0 {
1195            return Ok(());
1196        }
1197
1198        match color {
1199            Some(color) => {
1200                color.fmt_ansi_prefix(f)?;
1201                repeat_char(f, c, n)?;
1202                color.fmt_ansi_suffix(f)
1203            }
1204            None => repeat_char(f, c, n),
1205        }
1206    }
1207
1208    fn print_indent2<F, C>(f: &mut F, c: &Colored<char, C>, n: usize) -> fmt::Result
1209    where
1210        F: Write,
1211        C: ANSIFmt,
1212    {
1213        if n == 0 {
1214            return Ok(());
1215        }
1216
1217        match &c.color {
1218            Some(color) => {
1219                color.fmt_ansi_prefix(f)?;
1220                repeat_char(f, c.data, n)?;
1221                color.fmt_ansi_suffix(f)
1222            }
1223            None => repeat_char(f, c.data, n),
1224        }
1225    }
1226
1227    /// Trims a string.
1228    fn string_trim(text: &str) -> Cow<'_, str> {
1229        #[cfg(feature = "ansi")]
1230        {
1231            ansi_str::AnsiStr::ansi_trim(text)
1232        }
1233
1234        #[cfg(not(feature = "ansi"))]
1235        {
1236            text.trim().into()
1237        }
1238    }
1239}
1240
1241mod grid_spanned {
1242    use super::*;
1243
1244    struct TextCfg<C, C1> {
1245        alignment: AlignmentHorizontal,
1246        formatting: Formatting,
1247        color: Option<C>,
1248        justification: Colored<char, C1>,
1249    }
1250
1251    struct Colored<T, C> {
1252        data: T,
1253        color: Option<C>,
1254    }
1255
1256    impl<T, C> Colored<T, C> {
1257        fn new(data: T, color: Option<C>) -> Self {
1258            Self { data, color }
1259        }
1260    }
1261
1262    #[derive(Debug, Copy, Clone)]
1263    struct Shape {
1264        count_rows: usize,
1265        count_columns: usize,
1266    }
1267
1268    struct HIndent {
1269        left: usize,
1270        right: usize,
1271    }
1272
1273    pub(super) fn build_grid<F, R, D, C>(f: &mut F, ctx: PrintCtx<'_, R, D, C>) -> fmt::Result
1274    where
1275        F: Write,
1276        R: Records + PeekableRecords + ExactRecords,
1277        D: Dimension,
1278        C: Colors,
1279    {
1280        let shape = Shape {
1281            count_rows: ctx.records.count_rows(),
1282            count_columns: ctx.records.count_columns(),
1283        };
1284
1285        let total_width = total_width(ctx.cfg, ctx.dims, shape.count_columns);
1286
1287        let margin = ctx.cfg.get_margin();
1288        let total_width_with_margin = total_width + margin.left.size + margin.right.size;
1289
1290        let total_height = total_height(ctx.cfg, ctx.dims, shape.count_rows);
1291
1292        if margin.top.size > 0 {
1293            print_margin_top(f, ctx.cfg, total_width_with_margin)?;
1294            f.write_char('\n')?;
1295        }
1296
1297        let mut table_line = 0;
1298        let mut prev_empty_horizontal = false;
1299        for row in 0..shape.count_rows {
1300            let count_lines = ctx.dims.get_height(row);
1301
1302            if ctx.cfg.has_horizontal(row, shape.count_rows) {
1303                if prev_empty_horizontal {
1304                    f.write_char('\n')?;
1305                }
1306
1307                print_margin_left(f, ctx.cfg, table_line, total_height)?;
1308                print_split_line_spanned(f, &ctx, row, shape)?;
1309                print_margin_right(f, ctx.cfg, table_line, total_height)?;
1310
1311                if count_lines > 0 {
1312                    f.write_char('\n')?;
1313                    prev_empty_horizontal = false;
1314                } else {
1315                    prev_empty_horizontal = true;
1316                }
1317
1318                table_line += 1;
1319            } else if count_lines > 0 && prev_empty_horizontal {
1320                f.write_char('\n')?;
1321                prev_empty_horizontal = false;
1322            }
1323
1324            for i in 0..count_lines {
1325                print_margin_left(f, ctx.cfg, table_line, total_height)?;
1326
1327                for col in 0..shape.count_columns {
1328                    let pos = (row, col).into();
1329
1330                    if ctx.cfg.is_cell_covered_by_both_spans(pos) {
1331                        continue;
1332                    }
1333
1334                    if ctx.cfg.is_cell_covered_by_column_span(pos) {
1335                        let is_last_column = col + 1 == shape.count_columns;
1336                        if is_last_column {
1337                            let pos = (row, col + 1).into();
1338                            let count_columns = shape.count_columns;
1339                            print_vertical_char(f, ctx.cfg, pos, i, count_lines, count_columns)?;
1340                        }
1341
1342                        continue;
1343                    }
1344
1345                    print_vertical_char(f, ctx.cfg, pos, i, count_lines, shape.count_columns)?;
1346
1347                    if ctx.cfg.is_cell_covered_by_row_span(pos) {
1348                        // means it's part of other a spanned cell
1349                        // so. we just need to use line from other cell.
1350                        let original_row = closest_visible_row(ctx.cfg, pos).unwrap();
1351
1352                        // considering that the content will be printed instead horizontal lines so we can skip some lines.
1353                        let mut skip_lines = (original_row..row)
1354                            .map(|i| ctx.dims.get_height(i))
1355                            .sum::<usize>();
1356
1357                        skip_lines += (original_row + 1..=row)
1358                            .map(|row| ctx.cfg.has_horizontal(row, shape.count_rows) as usize)
1359                            .sum::<usize>();
1360
1361                        let line = i + skip_lines;
1362                        let pos = (original_row, col).into();
1363
1364                        let width = get_cell_width(ctx.cfg, ctx.dims, pos, shape.count_columns);
1365                        let height = get_cell_height(ctx.cfg, ctx.dims, pos, shape.count_rows);
1366
1367                        print_cell_line(f, &ctx, width, height, pos, line)?;
1368                    } else {
1369                        let width = get_cell_width(ctx.cfg, ctx.dims, pos, shape.count_columns);
1370                        let height = get_cell_height(ctx.cfg, ctx.dims, pos, shape.count_rows);
1371                        print_cell_line(f, &ctx, width, height, pos, i)?;
1372                    }
1373
1374                    let is_last_column = col + 1 == shape.count_columns;
1375                    if is_last_column {
1376                        let pos = (row, col + 1).into();
1377                        print_vertical_char(f, ctx.cfg, pos, i, count_lines, shape.count_columns)?;
1378                    }
1379                }
1380
1381                print_margin_right(f, ctx.cfg, table_line, total_height)?;
1382
1383                let is_last_line = i + 1 == count_lines;
1384                let is_last_row = row + 1 == shape.count_rows;
1385                if !(is_last_line && is_last_row) {
1386                    f.write_char('\n')?;
1387                }
1388
1389                table_line += 1;
1390            }
1391        }
1392
1393        if ctx.cfg.has_horizontal(shape.count_rows, shape.count_rows) {
1394            f.write_char('\n')?;
1395            print_margin_left(f, ctx.cfg, table_line, total_height)?;
1396            print_split_line(f, ctx.cfg, ctx.dims, shape.count_rows, shape)?;
1397            print_margin_right(f, ctx.cfg, table_line, total_height)?;
1398        }
1399
1400        if margin.bottom.size > 0 {
1401            f.write_char('\n')?;
1402            print_margin_bottom(f, ctx.cfg, total_width_with_margin)?;
1403        }
1404
1405        Ok(())
1406    }
1407
1408    fn print_split_line_spanned<F, R, D, C>(
1409        f: &mut F,
1410        ctx: &PrintCtx<'_, R, D, C>,
1411        row: usize,
1412        shape: Shape,
1413    ) -> fmt::Result
1414    where
1415        F: Write,
1416        R: Records + ExactRecords + PeekableRecords,
1417        D: Dimension,
1418        C: Colors,
1419    {
1420        let mut used_color = None;
1421
1422        let pos = (row, 0).into();
1423        print_vertical_intersection(f, ctx.cfg, pos, shape, &mut used_color)?;
1424
1425        for col in 0..shape.count_columns {
1426            let pos = (row, col).into();
1427            if ctx.cfg.is_cell_covered_by_both_spans(pos) {
1428                continue;
1429            }
1430
1431            if ctx.cfg.is_cell_covered_by_row_span(pos) {
1432                // means it's part of other a spanned cell
1433                // so. we just need to use line from other cell.
1434
1435                prepare_coloring(f, None, &mut used_color)?;
1436
1437                let original_row = closest_visible_row(ctx.cfg, pos).unwrap();
1438
1439                // considering that the content will be printed instead horizontal lines so we can skip some lines.
1440                let mut skip_lines = (original_row..row)
1441                    .map(|i| ctx.dims.get_height(i))
1442                    .sum::<usize>();
1443
1444                // skip horizontal lines
1445                if row > 0 {
1446                    skip_lines += (original_row..row - 1)
1447                        .map(|row| ctx.cfg.has_horizontal(row + 1, shape.count_rows) as usize)
1448                        .sum::<usize>();
1449                }
1450
1451                let pos = (original_row, col).into();
1452                let height = get_cell_height(ctx.cfg, ctx.dims, pos, shape.count_rows);
1453                let width = get_cell_width(ctx.cfg, ctx.dims, pos, shape.count_columns);
1454                let line = skip_lines;
1455
1456                print_cell_line(f, ctx, width, height, pos, line)?;
1457
1458                // We need to use a correct right split char.
1459                let mut col = col;
1460                if let Some(span) = ctx.cfg.get_column_span(pos) {
1461                    col += span - 1;
1462                }
1463
1464                let pos = (row, col + 1).into();
1465                print_vertical_intersection(f, ctx.cfg, pos, shape, &mut used_color)?;
1466
1467                continue;
1468            }
1469
1470            let width = ctx.dims.get_width(col);
1471            if width > 0 {
1472                // general case
1473                let main = ctx.cfg.get_horizontal(pos, shape.count_rows);
1474                match main {
1475                    Some(c) => {
1476                        let clr = ctx.cfg.get_horizontal_color(pos, shape.count_rows);
1477                        prepare_coloring(f, clr, &mut used_color)?;
1478                        print_horizontal_border(f, ctx.cfg, pos, width, c, &used_color)?;
1479                    }
1480                    None => repeat_char(f, ' ', width)?,
1481                }
1482            }
1483
1484            let pos = (row, col + 1).into();
1485            print_vertical_intersection(f, ctx.cfg, pos, shape, &mut used_color)?;
1486        }
1487
1488        if let Some(clr) = used_color {
1489            clr.fmt_ansi_suffix(f)?;
1490        }
1491
1492        Ok(())
1493    }
1494
1495    fn print_vertical_char<F>(
1496        f: &mut F,
1497        cfg: &SpannedConfig,
1498        pos: Position,
1499        line: usize,
1500        count_lines: usize,
1501        count_columns: usize,
1502    ) -> fmt::Result
1503    where
1504        F: Write,
1505    {
1506        let symbol = match cfg.get_vertical(pos, count_columns) {
1507            Some(c) => c,
1508            None => return Ok(()),
1509        };
1510
1511        let symbol = cfg
1512            .lookup_vertical_char(pos, line, count_lines)
1513            .unwrap_or(symbol);
1514
1515        let color = cfg
1516            .get_vertical_color(pos, count_columns)
1517            .or_else(|| cfg.lookup_vertical_color(pos, line, count_lines));
1518
1519        match color {
1520            Some(clr) => {
1521                clr.fmt_ansi_prefix(f)?;
1522                f.write_char(symbol)?;
1523                clr.fmt_ansi_suffix(f)?;
1524            }
1525            None => f.write_char(symbol)?,
1526        }
1527
1528        Ok(())
1529    }
1530
1531    fn print_vertical_intersection<'a, F>(
1532        f: &mut F,
1533        cfg: &'a SpannedConfig,
1534        pos: Position,
1535        shape: Shape,
1536        used_color: &mut Option<&'a ANSIBuf>,
1537    ) -> fmt::Result
1538    where
1539        F: fmt::Write,
1540    {
1541        let intersection = match cfg.get_intersection(pos, (shape.count_rows, shape.count_columns))
1542        {
1543            Some(c) => c,
1544            None => return Ok(()),
1545        };
1546
1547        // We need to make sure that we need to print it.
1548        // Specifically for cases where we have a limited amount of verticals.
1549        //
1550        // todo: Yes... this check very likely degrages performance a bit,
1551        //       Surely we need to rethink it.
1552        if !cfg.has_vertical(pos.col, shape.count_columns) {
1553            return Ok(());
1554        }
1555
1556        let color = cfg.get_intersection_color(pos, (shape.count_rows, shape.count_columns));
1557        prepare_coloring(f, color, used_color)?;
1558        f.write_char(intersection)?;
1559
1560        Ok(())
1561    }
1562
1563    fn prepare_coloring<'a, F>(
1564        f: &mut F,
1565        clr: Option<&'a ANSIBuf>,
1566        used_color: &mut Option<&'a ANSIBuf>,
1567    ) -> fmt::Result
1568    where
1569        F: Write,
1570    {
1571        match clr {
1572            Some(clr) => match used_color.as_mut() {
1573                Some(used_clr) => {
1574                    if **used_clr != *clr {
1575                        used_clr.fmt_ansi_suffix(f)?;
1576                        clr.fmt_ansi_prefix(f)?;
1577                        *used_clr = clr;
1578                    }
1579                }
1580                None => {
1581                    clr.fmt_ansi_prefix(f)?;
1582                    *used_color = Some(clr);
1583                }
1584            },
1585            None => {
1586                if let Some(clr) = used_color.take() {
1587                    clr.fmt_ansi_suffix(f)?
1588                }
1589            }
1590        }
1591
1592        Ok(())
1593    }
1594
1595    fn print_split_line<F, D>(
1596        f: &mut F,
1597        cfg: &SpannedConfig,
1598        dimension: &D,
1599        row: usize,
1600        shape: Shape,
1601    ) -> fmt::Result
1602    where
1603        F: Write,
1604        D: Dimension,
1605    {
1606        let mut used_color = None;
1607        print_vertical_intersection(f, cfg, (row, 0).into(), shape, &mut used_color)?;
1608
1609        for col in 0..shape.count_columns {
1610            let width = dimension.get_width(col);
1611
1612            // general case
1613            if width > 0 {
1614                let pos = (row, col).into();
1615                let main = cfg.get_horizontal(pos, shape.count_rows);
1616                match main {
1617                    Some(c) => {
1618                        let clr = cfg.get_horizontal_color(pos, shape.count_rows);
1619                        prepare_coloring(f, clr, &mut used_color)?;
1620                        print_horizontal_border(f, cfg, pos, width, c, &used_color)?;
1621                    }
1622                    None => repeat_char(f, ' ', width)?,
1623                }
1624            }
1625
1626            let pos = (row, col + 1).into();
1627            print_vertical_intersection(f, cfg, pos, shape, &mut used_color)?;
1628        }
1629
1630        if let Some(clr) = used_color.take() {
1631            clr.fmt_ansi_suffix(f)?;
1632        }
1633
1634        Ok(())
1635    }
1636
1637    fn print_horizontal_border<F>(
1638        f: &mut F,
1639        cfg: &SpannedConfig,
1640        pos: Position,
1641        width: usize,
1642        c: char,
1643        used_color: &Option<&ANSIBuf>,
1644    ) -> fmt::Result
1645    where
1646        F: Write,
1647    {
1648        if !cfg.is_overridden_horizontal(pos) {
1649            return repeat_char(f, c, width);
1650        }
1651
1652        for i in 0..width {
1653            let c = cfg.lookup_horizontal_char(pos, i, width).unwrap_or(c);
1654            match cfg.lookup_horizontal_color(pos, i, width) {
1655                Some(color) => match used_color {
1656                    Some(clr) => {
1657                        clr.fmt_ansi_suffix(f)?;
1658                        color.fmt_ansi_prefix(f)?;
1659                        f.write_char(c)?;
1660                        color.fmt_ansi_suffix(f)?;
1661                        clr.fmt_ansi_prefix(f)?;
1662                    }
1663                    None => {
1664                        color.fmt_ansi_prefix(f)?;
1665                        f.write_char(c)?;
1666                        color.fmt_ansi_suffix(f)?;
1667                    }
1668                },
1669                _ => f.write_char(c)?,
1670            }
1671        }
1672
1673        Ok(())
1674    }
1675
1676    fn print_cell_line<F, R, D, C>(
1677        f: &mut F,
1678        ctx: &PrintCtx<'_, R, D, C>,
1679        width: usize,
1680        height: usize,
1681        pos: Position,
1682        line: usize,
1683    ) -> fmt::Result
1684    where
1685        F: Write,
1686        R: Records + PeekableRecords + ExactRecords,
1687        C: Colors,
1688    {
1689        let mut cell_height = ctx.records.count_lines(pos);
1690        let formatting = ctx.cfg.get_formatting(pos);
1691        if formatting.vertical_trim {
1692            cell_height -= count_empty_lines_at_start(ctx.records, pos)
1693                + count_empty_lines_at_end(ctx.records, pos);
1694        }
1695
1696        if cell_height > height {
1697            // it may happen if the height estimation decide so
1698            cell_height = height;
1699        }
1700
1701        let pad = ctx.cfg.get_padding(pos);
1702        let pad_color = ctx.cfg.get_padding_color(pos);
1703        let alignment = ctx.cfg.get_alignment_vertical(pos);
1704        let indent = top_indent(pad, *alignment, cell_height, height);
1705        if indent > line {
1706            return print_indent(f, pad.top.fill, width, pad_color.top.as_ref());
1707        }
1708
1709        let mut index = line - indent;
1710        let cell_has_this_line = cell_height > index;
1711        if !cell_has_this_line {
1712            // happens when other cells have bigger height
1713            return print_indent(f, pad.bottom.fill, width, pad_color.bottom.as_ref());
1714        }
1715
1716        if formatting.vertical_trim {
1717            let empty_lines = count_empty_lines_at_start(ctx.records, pos);
1718            index += empty_lines;
1719
1720            if index > ctx.records.count_lines(pos) {
1721                return print_indent(f, pad.top.fill, width, pad_color.top.as_ref());
1722            }
1723        }
1724
1725        print_indent(f, pad.left.fill, pad.left.size, pad_color.left.as_ref())?;
1726
1727        let width = width - pad.left.size - pad.right.size;
1728
1729        let line_cfg = TextCfg {
1730            alignment: *ctx.cfg.get_alignment_horizontal(pos),
1731            color: ctx.colors.get_color(pos),
1732            justification: Colored::new(
1733                ctx.cfg.get_justification(pos),
1734                ctx.cfg.get_justification_color(pos),
1735            ),
1736            formatting,
1737        };
1738
1739        print_line(f, ctx.records, pos, index, width, line_cfg)?;
1740
1741        print_indent(f, pad.right.fill, pad.right.size, pad_color.right.as_ref())?;
1742
1743        Ok(())
1744    }
1745
1746    fn print_line<F, R, C, C1>(
1747        f: &mut F,
1748        records: &R,
1749        pos: Position,
1750        index: usize,
1751        available: usize,
1752        text_cfg: TextCfg<C, C1>,
1753    ) -> fmt::Result
1754    where
1755        F: Write,
1756        R: Records + PeekableRecords,
1757        C: ANSIFmt,
1758        C1: ANSIFmt,
1759    {
1760        let line = records.get_line(pos, index);
1761        let (line, line_width) = if text_cfg.formatting.horizontal_trim {
1762            let line = string_trim(line);
1763            let width = get_line_width(&line);
1764            (line, width)
1765        } else {
1766            let width = records.get_line_width(pos, index);
1767            (Cow::Borrowed(line), width)
1768        };
1769
1770        if text_cfg.formatting.allow_lines_alignment {
1771            let indent = calculate_indent(text_cfg.alignment, line_width, available);
1772            let text = Colored::new(line.as_ref(), text_cfg.color);
1773            return print_text_with_pad(f, &text, &text_cfg.justification, indent);
1774        }
1775
1776        let cell_width = if text_cfg.formatting.horizontal_trim {
1777            (0..records.count_lines(pos))
1778                .map(|i| records.get_line(pos, i))
1779                .map(|line| get_line_width(line.trim()))
1780                .max()
1781                .unwrap_or_default()
1782        } else {
1783            records.get_width(pos)
1784        };
1785
1786        let indent = calculate_indent(text_cfg.alignment, cell_width, available);
1787        let text = Colored::new(line.as_ref(), text_cfg.color.as_ref());
1788        print_text_with_pad(f, &text, &text_cfg.justification, indent)?;
1789
1790        let rest_width = cell_width - line_width;
1791        print_indent(
1792            f,
1793            text_cfg.justification.data,
1794            rest_width,
1795            text_cfg.justification.color.as_ref(),
1796        )?;
1797
1798        Ok(())
1799    }
1800
1801    fn print_text_with_pad<F, C, C1>(
1802        f: &mut F,
1803        text: &Colored<&str, C>,
1804        space: &Colored<char, C1>,
1805        indent: HIndent,
1806    ) -> fmt::Result
1807    where
1808        F: Write,
1809        C: ANSIFmt,
1810        C1: ANSIFmt,
1811    {
1812        print_indent(f, space.data, indent.left, space.color.as_ref())?;
1813        print_text(f, text.data, text.color.as_ref())?;
1814        print_indent(f, space.data, indent.right, space.color.as_ref())?;
1815        Ok(())
1816    }
1817
1818    fn print_text<F, C>(f: &mut F, text: &str, clr: Option<C>) -> fmt::Result
1819    where
1820        F: Write,
1821        C: ANSIFmt,
1822    {
1823        match clr {
1824            Some(color) => {
1825                color.fmt_ansi_prefix(f)?;
1826                f.write_str(text)?;
1827                color.fmt_ansi_suffix(f)
1828            }
1829            None => f.write_str(text),
1830        }
1831    }
1832
1833    fn top_indent(
1834        pad: &Sides<Indent>,
1835        alignment: AlignmentVertical,
1836        cell_height: usize,
1837        available: usize,
1838    ) -> usize {
1839        let height = available - pad.top.size;
1840        let indent = indent_from_top(alignment, height, cell_height);
1841
1842        indent + pad.top.size
1843    }
1844
1845    fn indent_from_top(alignment: AlignmentVertical, available: usize, real: usize) -> usize {
1846        match alignment {
1847            AlignmentVertical::Top => 0,
1848            AlignmentVertical::Bottom => available - real,
1849            AlignmentVertical::Center => (available - real) / 2,
1850        }
1851    }
1852
1853    fn calculate_indent(alignment: AlignmentHorizontal, width: usize, available: usize) -> HIndent {
1854        let diff = available - width;
1855
1856        let (left, right) = match alignment {
1857            AlignmentHorizontal::Left => (0, diff),
1858            AlignmentHorizontal::Right => (diff, 0),
1859            AlignmentHorizontal::Center => {
1860                let left = diff / 2;
1861                let rest = diff - left;
1862                (left, rest)
1863            }
1864        };
1865
1866        HIndent { left, right }
1867    }
1868
1869    fn repeat_char<F>(f: &mut F, c: char, n: usize) -> fmt::Result
1870    where
1871        F: Write,
1872    {
1873        for _ in 0..n {
1874            f.write_char(c)?;
1875        }
1876
1877        Ok(())
1878    }
1879
1880    fn count_empty_lines_at_end<R>(records: &R, pos: Position) -> usize
1881    where
1882        R: Records + PeekableRecords,
1883    {
1884        (0..records.count_lines(pos))
1885            .map(|i| records.get_line(pos, i))
1886            .rev()
1887            .take_while(|l| l.trim().is_empty())
1888            .count()
1889    }
1890
1891    fn count_empty_lines_at_start<R>(records: &R, pos: Position) -> usize
1892    where
1893        R: Records + PeekableRecords,
1894    {
1895        (0..records.count_lines(pos))
1896            .map(|i| records.get_line(pos, i))
1897            .take_while(|s| s.trim().is_empty())
1898            .count()
1899    }
1900
1901    fn total_width<D>(cfg: &SpannedConfig, dimension: &D, count_columns: usize) -> usize
1902    where
1903        D: Dimension,
1904    {
1905        (0..count_columns)
1906            .map(|i| dimension.get_width(i))
1907            .sum::<usize>()
1908            + cfg.count_vertical(count_columns)
1909    }
1910
1911    fn total_height<D>(cfg: &SpannedConfig, dimension: &D, count_rows: usize) -> usize
1912    where
1913        D: Dimension,
1914    {
1915        (0..count_rows)
1916            .map(|i| dimension.get_height(i))
1917            .sum::<usize>()
1918            + cfg.count_horizontal(count_rows)
1919    }
1920
1921    fn print_margin_top<F>(f: &mut F, cfg: &SpannedConfig, width: usize) -> fmt::Result
1922    where
1923        F: Write,
1924    {
1925        let indent = cfg.get_margin().top;
1926        let offset = cfg.get_margin_offset().top;
1927        let color = cfg.get_margin_color();
1928        let color = color.top;
1929        print_indent_lines(f, &indent, &offset, color, width)
1930    }
1931
1932    fn print_margin_bottom<F>(f: &mut F, cfg: &SpannedConfig, width: usize) -> fmt::Result
1933    where
1934        F: Write,
1935    {
1936        let indent = cfg.get_margin().bottom;
1937        let offset = cfg.get_margin_offset().bottom;
1938        let color = cfg.get_margin_color();
1939        let color = color.bottom;
1940        print_indent_lines(f, &indent, &offset, color, width)
1941    }
1942
1943    fn print_margin_left<F>(
1944        f: &mut F,
1945        cfg: &SpannedConfig,
1946        line: usize,
1947        height: usize,
1948    ) -> fmt::Result
1949    where
1950        F: Write,
1951    {
1952        let indent = cfg.get_margin().left;
1953        let offset = cfg.get_margin_offset().left;
1954        let color = cfg.get_margin_color();
1955        let color = color.left;
1956        print_margin_vertical(f, indent, offset, color, line, height)
1957    }
1958
1959    fn print_margin_right<F>(
1960        f: &mut F,
1961        cfg: &SpannedConfig,
1962        line: usize,
1963        height: usize,
1964    ) -> fmt::Result
1965    where
1966        F: Write,
1967    {
1968        let indent = cfg.get_margin().right;
1969        let offset = cfg.get_margin_offset().right;
1970        let color = cfg.get_margin_color();
1971        let color = color.right;
1972        print_margin_vertical(f, indent, offset, color, line, height)
1973    }
1974
1975    fn print_margin_vertical<F>(
1976        f: &mut F,
1977        indent: Indent,
1978        offset: Offset,
1979        color: Option<&ANSIBuf>,
1980        line: usize,
1981        height: usize,
1982    ) -> fmt::Result
1983    where
1984        F: Write,
1985    {
1986        if indent.size == 0 {
1987            return Ok(());
1988        }
1989
1990        match offset {
1991            Offset::Start(offset) => {
1992                let offset = cmp::min(offset, height);
1993                if line >= offset {
1994                    print_indent(f, indent.fill, indent.size, color)?;
1995                } else {
1996                    repeat_char(f, ' ', indent.size)?;
1997                }
1998            }
1999            Offset::End(offset) => {
2000                let offset = cmp::min(offset, height);
2001                let pos = height - offset;
2002
2003                if line >= pos {
2004                    repeat_char(f, ' ', indent.size)?;
2005                } else {
2006                    print_indent(f, indent.fill, indent.size, color)?;
2007                }
2008            }
2009        }
2010
2011        Ok(())
2012    }
2013
2014    fn print_indent_lines<F>(
2015        f: &mut F,
2016        indent: &Indent,
2017        offset: &Offset,
2018        color: Option<&ANSIBuf>,
2019        width: usize,
2020    ) -> fmt::Result
2021    where
2022        F: Write,
2023    {
2024        if indent.size == 0 {
2025            return Ok(());
2026        }
2027
2028        let (start_offset, end_offset) = match offset {
2029            Offset::Start(start) => (*start, 0),
2030            Offset::End(end) => (0, *end),
2031        };
2032
2033        let start_offset = std::cmp::min(start_offset, width);
2034        let end_offset = std::cmp::min(end_offset, width);
2035        let indent_size = width - start_offset - end_offset;
2036
2037        for i in 0..indent.size {
2038            if start_offset > 0 {
2039                repeat_char(f, ' ', start_offset)?;
2040            }
2041
2042            if indent_size > 0 {
2043                print_indent(f, indent.fill, indent_size, color)?;
2044            }
2045
2046            if end_offset > 0 {
2047                repeat_char(f, ' ', end_offset)?;
2048            }
2049
2050            if i + 1 != indent.size {
2051                f.write_char('\n')?;
2052            }
2053        }
2054
2055        Ok(())
2056    }
2057
2058    fn print_indent<F, C>(f: &mut F, c: char, n: usize, color: Option<C>) -> fmt::Result
2059    where
2060        F: Write,
2061        C: ANSIFmt,
2062    {
2063        if n == 0 {
2064            return Ok(());
2065        }
2066
2067        match color {
2068            Some(color) => {
2069                color.fmt_ansi_prefix(f)?;
2070                repeat_char(f, c, n)?;
2071                color.fmt_ansi_suffix(f)
2072            }
2073            None => repeat_char(f, c, n),
2074        }
2075    }
2076
2077    fn get_cell_width<D>(cfg: &SpannedConfig, dims: &D, pos: Position, max: usize) -> usize
2078    where
2079        D: Dimension,
2080    {
2081        match cfg.get_column_span(pos) {
2082            Some(span) => {
2083                let start = pos.col;
2084                let end = start + span;
2085                range_width(dims, start, end) + count_verticals_range(cfg, start, end, max)
2086            }
2087            None => dims.get_width(pos.col),
2088        }
2089    }
2090
2091    fn range_width<D>(dims: &D, start: usize, end: usize) -> usize
2092    where
2093        D: Dimension,
2094    {
2095        (start..end).map(|col| dims.get_width(col)).sum::<usize>()
2096    }
2097
2098    fn count_verticals_range(cfg: &SpannedConfig, start: usize, end: usize, max: usize) -> usize {
2099        (start + 1..end)
2100            .map(|i| cfg.has_vertical(i, max) as usize)
2101            .sum()
2102    }
2103
2104    fn get_cell_height<D>(cfg: &SpannedConfig, dims: &D, pos: Position, max: usize) -> usize
2105    where
2106        D: Dimension,
2107    {
2108        match cfg.get_row_span(pos) {
2109            Some(span) => {
2110                let start = pos.row;
2111                let end = pos.row + span;
2112                range_height(dims, start, end) + count_horizontals_range(cfg, start, end, max)
2113            }
2114            None => dims.get_height(pos.row),
2115        }
2116    }
2117
2118    fn range_height<D>(dims: &D, start: usize, end: usize) -> usize
2119    where
2120        D: Dimension,
2121    {
2122        (start..end).map(|col| dims.get_height(col)).sum::<usize>()
2123    }
2124
2125    fn count_horizontals_range(cfg: &SpannedConfig, start: usize, end: usize, max: usize) -> usize {
2126        (start + 1..end)
2127            .map(|i| cfg.has_horizontal(i, max) as usize)
2128            .sum()
2129    }
2130
2131    fn closest_visible_row(cfg: &SpannedConfig, mut pos: Position) -> Option<usize> {
2132        loop {
2133            if cfg.is_cell_visible(pos) {
2134                return Some(pos.row);
2135            }
2136
2137            if pos.row == 0 {
2138                return None;
2139            }
2140
2141            pos -= (1, 0);
2142        }
2143    }
2144
2145    /// Trims a string.
2146    fn string_trim(text: &str) -> Cow<'_, str> {
2147        #[cfg(feature = "ansi")]
2148        {
2149            ansi_str::AnsiStr::ansi_trim(text)
2150        }
2151
2152        #[cfg(not(feature = "ansi"))]
2153        {
2154            text.trim().into()
2155        }
2156    }
2157}