papergrid/
grid.rs

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