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