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 color::{AnsiColor, Color},
12 colors::Colors,
13 config::spanned::{Formatting, Offset, SpannedConfig},
14 config::{AlignmentHorizontal, AlignmentVertical, Indent, Position, Sides},
15 dimension::Dimension,
16 records::{ExactRecords, PeekableRecords, Records},
17 util::string::string_width,
18};
19
20/// Grid provides a set of methods for building a text-based table.
21#[derive(Debug, Clone)]
22pub struct PeekableGrid<R, G, D, C> {
23 records: R,
24 config: G,
25 dimension: D,
26 colors: C,
27}
28
29impl<R, G, D, C> PeekableGrid<R, G, D, C> {
30 /// The new method creates a grid instance with default styles.
31 pub fn new(records: R, config: G, dimension: D, colors: C) -> Self {
32 PeekableGrid {
33 records,
34 config,
35 dimension,
36 colors,
37 }
38 }
39}
40
41impl<R, G, D, C> PeekableGrid<R, G, D, C> {
42 /// Builds a table.
43 pub fn build<F>(self, mut f: F) -> fmt::Result
44 where
45 R: Records + PeekableRecords + ExactRecords,
46 D: Dimension,
47 C: Colors,
48 G: Borrow<SpannedConfig>,
49 F: Write,
50 {
51 if self.records.count_columns() == 0 || self.records.hint_count_rows() == Some(0) {
52 return Ok(());
53 }
54
55 let config = self.config.borrow();
56 print_grid(&mut f, self.records, config, &self.dimension, &self.colors)
57 }
58
59 /// Builds a table into string.
60 ///
61 /// Notice that it consumes self.
62 #[allow(clippy::inherent_to_string)]
63 pub fn to_string(self) -> String
64 where
65 R: Records + PeekableRecords + ExactRecords,
66 D: Dimension,
67 G: Borrow<SpannedConfig>,
68 C: Colors,
69 {
70 let mut buf = String::new();
71 self.build(&mut buf).expect("It's guaranteed to never happen otherwise it's considered an stdlib error or impl error");
72 buf
73 }
74}
75
76fn print_grid<F: Write, R: Records + PeekableRecords + ExactRecords, D: Dimension, C: Colors>(
77 f: &mut F,
78 records: R,
79 cfg: &SpannedConfig,
80 dimension: &D,
81 colors: &C,
82) -> fmt::Result {
83 if cfg.has_column_spans() || cfg.has_row_spans() {
84 build_grid_spanned(f, &records, cfg, dims:dimension, colors)
85 } else {
86 build_grid(f, &records, cfg, dimension, colors)
87 }
88}
89
90fn build_grid<F: Write, R: Records + PeekableRecords + ExactRecords, D: Dimension, C: Colors>(
91 f: &mut F,
92 records: &R,
93 cfg: &SpannedConfig,
94 dimension: &D,
95 colors: &C,
96) -> fmt::Result {
97 let shape = (records.count_rows(), records.count_columns());
98
99 let total_width = total_width(cfg, dimension, shape.1);
100 let total_width_with_margin =
101 total_width + cfg.get_margin().left.size + cfg.get_margin().right.size;
102
103 let total_height = total_height(cfg, dimension, shape.0);
104
105 if cfg.get_margin().top.size > 0 {
106 print_margin_top(f, cfg, total_width_with_margin)?;
107 f.write_char('\n')?;
108 }
109
110 let mut table_line = 0;
111 let mut prev_empty_horizontal = false;
112 for row in 0..shape.0 {
113 let height = dimension.get_height(row);
114
115 if cfg.has_horizontal(row, shape.0) {
116 if prev_empty_horizontal {
117 f.write_char('\n')?;
118 }
119
120 print_margin_left(f, cfg, table_line, total_height)?;
121 print_split_line(f, cfg, dimension, row, shape)?;
122 print_margin_right(f, cfg, table_line, total_height)?;
123
124 if height > 0 {
125 f.write_char('\n')?;
126 prev_empty_horizontal = false;
127 } else {
128 prev_empty_horizontal = true;
129 }
130
131 table_line += 1;
132 } else if height > 0 && prev_empty_horizontal {
133 f.write_char('\n')?;
134 prev_empty_horizontal = false;
135 }
136
137 for i in 0..height {
138 print_margin_left(f, cfg, table_line, total_height)?;
139
140 for col in 0..records.count_columns() {
141 print_vertical_char(f, cfg, (row, col), i, height, shape.1)?;
142
143 let width = dimension.get_width(col);
144 print_cell_line(f, records, cfg, colors, width, height, (row, col), i)?;
145
146 let is_last_column = col + 1 == records.count_columns();
147 if is_last_column {
148 print_vertical_char(f, cfg, (row, col + 1), i, height, shape.1)?;
149 }
150 }
151
152 print_margin_right(f, cfg, table_line, total_height)?;
153
154 let is_last_line = i + 1 == height;
155 let is_last_row = row + 1 == records.count_rows();
156 if !(is_last_line && is_last_row) {
157 f.write_char('\n')?;
158 }
159
160 table_line += 1;
161 }
162 }
163
164 if cfg.has_horizontal(shape.0, shape.0) {
165 f.write_char('\n')?;
166 print_margin_left(f, cfg, table_line, total_height)?;
167 print_split_line(f, cfg, dimension, records.count_rows(), shape)?;
168 print_margin_right(f, cfg, table_line, total_height)?;
169 }
170
171 if cfg.get_margin().bottom.size > 0 {
172 f.write_char('\n')?;
173 print_margin_bottom(f, cfg, total_width_with_margin)?;
174 }
175
176 Ok(())
177}
178
179fn print_split_line<F: Write, D: Dimension>(
180 f: &mut F,
181 cfg: &SpannedConfig,
182 dimension: &D,
183 row: usize,
184 shape: (usize, usize),
185) -> fmt::Result {
186 let mut used_color = None;
187 print_vertical_intersection(f, cfg, (row, 0), shape, &mut used_color)?;
188
189 for col in 0..shape.1 {
190 let width = dimension.get_width(col);
191
192 // general case
193 if width > 0 {
194 let pos = (row, col);
195 let main = cfg.get_horizontal(pos, shape.0);
196 match main {
197 Some(c) => {
198 let clr = cfg.get_horizontal_color(pos, shape.0);
199 prepare_coloring(f, clr, &mut used_color)?;
200 print_horizontal_border(f, cfg, pos, width, c, &used_color)?;
201 }
202 None => repeat_char(f, ' ', width)?,
203 }
204 }
205
206 print_vertical_intersection(f, cfg, (row, col + 1), shape, &mut used_color)?;
207 }
208
209 if let Some(clr) = used_color.take() {
210 clr.fmt_suffix(f)?;
211 }
212
213 Ok(())
214}
215
216fn print_vertical_intersection<'a, F: fmt::Write>(
217 f: &mut F,
218 cfg: &'a SpannedConfig,
219 pos: Position,
220 shape: (usize, usize),
221 used_color: &mut Option<&'a AnsiColor<'static>>,
222) -> fmt::Result {
223 match cfg.get_intersection(pos, shape) {
224 Some(c: char) => {
225 let clr: Option<&AnsiColor<'_>> = cfg.get_intersection_color(pos, shape);
226 prepare_coloring(f, clr, used_color)?;
227 f.write_char(c)
228 }
229 None => Ok(()),
230 }
231}
232
233fn prepare_coloring<'a, 'b, F: Write>(
234 f: &mut F,
235 clr: Option<&'a AnsiColor<'b>>,
236 used_color: &mut Option<&'a AnsiColor<'b>>,
237) -> fmt::Result {
238 match clr {
239 Some(clr: &AnsiColor<'_>) => match used_color.as_mut() {
240 Some(used_clr: &mut &AnsiColor<'_>) => {
241 if **used_clr != *clr {
242 used_clr.fmt_suffix(f)?;
243 clr.fmt_prefix(f)?;
244 *used_clr = clr;
245 }
246 }
247 None => {
248 clr.fmt_prefix(f)?;
249 *used_color = Some(clr);
250 }
251 },
252 None => {
253 if let Some(clr: &AnsiColor<'_>) = used_color.take() {
254 clr.fmt_suffix(f)?
255 }
256 }
257 }
258
259 Ok(())
260}
261
262fn print_vertical_char<F: Write>(
263 f: &mut F,
264 cfg: &SpannedConfig,
265 pos: Position,
266 line: usize,
267 count_lines: usize,
268 count_columns: usize,
269) -> fmt::Result {
270 let symbol: char = match cfg.get_vertical(pos, count_columns) {
271 Some(c: char) => c,
272 None => return Ok(()),
273 };
274
275 let symbol: char = cfg
276 .is_overridden_vertical(pos)
277 .then(|| cfg.lookup_vertical_char(pos, line, count_lines))
278 .flatten()
279 .unwrap_or(default:symbol);
280
281 match cfg.get_vertical_color(pos, count_columns) {
282 Some(clr: &AnsiColor<'_>) => {
283 clr.fmt_prefix(f)?;
284 f.write_char(symbol)?;
285 clr.fmt_suffix(f)?;
286 }
287 None => f.write_char(symbol)?,
288 }
289
290 Ok(())
291}
292
293fn build_grid_spanned<
294 F: Write,
295 R: Records + PeekableRecords + ExactRecords,
296 D: Dimension,
297 C: Colors,
298>(
299 f: &mut F,
300 records: &R,
301 cfg: &SpannedConfig,
302 dims: &D,
303 colors: &C,
304) -> fmt::Result {
305 let shape = (records.count_rows(), records.count_columns());
306
307 let total_width = total_width(cfg, dims, shape.1);
308 let total_width_with_margin =
309 total_width + cfg.get_margin().left.size + cfg.get_margin().right.size;
310
311 let total_height = total_height(cfg, dims, shape.0);
312
313 if cfg.get_margin().top.size > 0 {
314 print_margin_top(f, cfg, total_width_with_margin)?;
315 f.write_char('\n')?;
316 }
317
318 let mut table_line = 0;
319 let mut prev_empty_horizontal = false;
320 for row in 0..records.count_rows() {
321 let count_lines = dims.get_height(row);
322
323 if cfg.has_horizontal(row, shape.0) {
324 if prev_empty_horizontal {
325 f.write_char('\n')?;
326 }
327
328 print_margin_left(f, cfg, table_line, total_height)?;
329 print_split_line_spanned(f, records, cfg, dims, colors, row, shape)?;
330 print_margin_right(f, cfg, table_line, total_height)?;
331
332 if count_lines > 0 {
333 f.write_char('\n')?;
334 prev_empty_horizontal = false;
335 } else {
336 prev_empty_horizontal = true;
337 }
338
339 table_line += 1;
340 } else if count_lines > 0 && prev_empty_horizontal {
341 f.write_char('\n')?;
342 prev_empty_horizontal = false;
343 }
344
345 for i in 0..count_lines {
346 print_margin_left(f, cfg, table_line, total_height)?;
347
348 for col in 0..records.count_columns() {
349 if cfg.is_cell_covered_by_both_spans((row, col)) {
350 continue;
351 }
352
353 if cfg.is_cell_covered_by_column_span((row, col)) {
354 let is_last_column = col + 1 == records.count_columns();
355 if is_last_column {
356 print_vertical_char(f, cfg, (row, col + 1), i, count_lines, shape.1)?;
357 }
358
359 continue;
360 }
361
362 print_vertical_char(f, cfg, (row, col), i, count_lines, shape.1)?;
363
364 if cfg.is_cell_covered_by_row_span((row, col)) {
365 // means it's part of other a spanned cell
366 // so. we just need to use line from other cell.
367 let original_row = closest_visible_row(cfg, (row, col)).unwrap();
368
369 // considering that the content will be printed instead horizontal lines so we can skip some lines.
370 let mut skip_lines = (original_row..row)
371 .map(|i| dims.get_height(i))
372 .sum::<usize>();
373
374 skip_lines += (original_row + 1..=row)
375 .map(|row| cfg.has_horizontal(row, shape.0) as usize)
376 .sum::<usize>();
377
378 let line = i + skip_lines;
379 let pos = (original_row, col);
380
381 let width = get_cell_width(cfg, dims, pos, shape.1);
382 let height = get_cell_height(cfg, dims, pos, shape.0);
383
384 print_cell_line(f, records, cfg, colors, width, height, pos, line)?;
385 } else {
386 let width = get_cell_width(cfg, dims, (row, col), shape.1);
387 let height = get_cell_height(cfg, dims, (row, col), shape.0);
388 print_cell_line(f, records, cfg, colors, width, height, (row, col), i)?;
389 }
390
391 let is_last_column = col + 1 == records.count_columns();
392 if is_last_column {
393 print_vertical_char(f, cfg, (row, col + 1), i, count_lines, shape.1)?;
394 }
395 }
396
397 print_margin_right(f, cfg, table_line, total_height)?;
398
399 let is_last_line = i + 1 == count_lines;
400 let is_last_row = row + 1 == records.count_rows();
401 if !(is_last_line && is_last_row) {
402 f.write_char('\n')?;
403 }
404
405 table_line += 1;
406 }
407 }
408
409 if cfg.has_horizontal(shape.0, shape.0) {
410 f.write_char('\n')?;
411 print_margin_left(f, cfg, table_line, total_height)?;
412 print_split_line(f, cfg, dims, records.count_rows(), shape)?;
413 print_margin_right(f, cfg, table_line, total_height)?;
414 }
415
416 if cfg.get_margin().bottom.size > 0 {
417 f.write_char('\n')?;
418 print_margin_bottom(f, cfg, total_width_with_margin)?;
419 }
420
421 Ok(())
422}
423
424fn print_split_line_spanned<
425 F: Write,
426 R: Records + ExactRecords + PeekableRecords,
427 D: Dimension,
428 C: Colors,
429>(
430 f: &mut F,
431 records: &R,
432 cfg: &SpannedConfig,
433 dims: &D,
434 colors: &C,
435 row: usize,
436 shape: (usize, usize),
437) -> fmt::Result {
438 let mut used_color = None;
439 print_vertical_intersection(f, cfg, (row, 0), shape, &mut used_color)?;
440
441 for col in 0..shape.1 {
442 let pos = (row, col);
443 if cfg.is_cell_covered_by_both_spans(pos) {
444 continue;
445 }
446
447 if cfg.is_cell_covered_by_row_span(pos) {
448 // means it's part of other a spanned cell
449 // so. we just need to use line from other cell.
450
451 let original_row = closest_visible_row(cfg, (row, col)).unwrap();
452
453 // considering that the content will be printed instead horizontal lines so we can skip some lines.
454 let mut skip_lines = (original_row..row)
455 .map(|i| dims.get_height(i))
456 .sum::<usize>();
457
458 // skip horizontal lines
459 if row > 0 {
460 skip_lines += (original_row..row - 1)
461 .map(|row| cfg.has_horizontal(row + 1, shape.0) as usize)
462 .sum::<usize>();
463 }
464
465 let pos = (original_row, col);
466 let height = get_cell_height(cfg, dims, pos, shape.0);
467 let width = get_cell_width(cfg, dims, pos, shape.1);
468 let line = skip_lines;
469
470 print_cell_line(f, records, cfg, colors, width, height, pos, line)?;
471
472 // We need to use a correct right split char.
473 let mut col = col;
474 if let Some(span) = cfg.get_column_span(pos) {
475 col += span - 1;
476 }
477
478 print_vertical_intersection(f, cfg, (row, col + 1), shape, &mut used_color)?;
479
480 continue;
481 }
482
483 let width = dims.get_width(col);
484 if width > 0 {
485 // general case
486 let main = cfg.get_horizontal(pos, shape.0);
487 match main {
488 Some(c) => {
489 let clr = cfg.get_horizontal_color(pos, shape.0);
490 prepare_coloring(f, clr, &mut used_color)?;
491 print_horizontal_border(f, cfg, pos, width, c, &used_color)?;
492 }
493 None => repeat_char(f, ' ', width)?,
494 }
495 }
496
497 print_vertical_intersection(f, cfg, (row, col + 1), shape, &mut used_color)?;
498 }
499
500 if let Some(clr) = used_color {
501 clr.fmt_suffix(f)?;
502 }
503
504 Ok(())
505}
506
507fn print_horizontal_border<F: Write>(
508 f: &mut F,
509 cfg: &SpannedConfig,
510 pos: Position,
511 width: usize,
512 c: char,
513 used_color: &Option<&AnsiColor<'static>>,
514) -> fmt::Result {
515 if !cfg.is_overridden_horizontal(pos) {
516 return repeat_char(f, c, width);
517 }
518
519 for i in 0..width {
520 let c = cfg.lookup_horizontal_char(pos, i, width).unwrap_or(c);
521 match cfg.lookup_horizontal_color(pos, i, width) {
522 Some(color) => match used_color {
523 Some(clr) => {
524 clr.fmt_suffix(f)?;
525 color.fmt_prefix(f)?;
526 f.write_char(c)?;
527 color.fmt_suffix(f)?;
528 clr.fmt_prefix(f)?;
529 }
530 None => {
531 color.fmt_prefix(f)?;
532 f.write_char(c)?;
533 color.fmt_suffix(f)?;
534 }
535 },
536 _ => f.write_char(c)?,
537 }
538 }
539
540 Ok(())
541}
542
543#[allow(clippy::too_many_arguments)]
544fn print_cell_line<F: Write, R: Records + PeekableRecords + ExactRecords, C: Colors>(
545 f: &mut F,
546 records: &R,
547 cfg: &SpannedConfig,
548 colors: &C,
549 width: usize,
550 height: usize,
551 pos: Position,
552 line: usize,
553) -> fmt::Result {
554 let entity = pos.into();
555
556 let mut cell_height = records.count_lines(pos);
557 let formatting = *cfg.get_formatting(entity);
558 if formatting.vertical_trim {
559 cell_height -=
560 count_empty_lines_at_start(records, pos) + count_empty_lines_at_end(records, pos);
561 }
562
563 if cell_height > height {
564 // it may happen if the height estimation decide so
565 cell_height = height;
566 }
567
568 let pad = cfg.get_padding(entity);
569 let pad_color = cfg.get_padding_color(entity);
570 let alignment = cfg.get_alignment_vertical(entity);
571 let indent = top_indent(&pad, *alignment, cell_height, height);
572 if indent > line {
573 return print_indent(f, pad.top.fill, width, pad_color.top.as_ref());
574 }
575
576 let mut index = line - indent;
577 let cell_has_this_line = cell_height > index;
578 if !cell_has_this_line {
579 // happens when other cells have bigger height
580 return print_indent(f, pad.bottom.fill, width, pad_color.bottom.as_ref());
581 }
582
583 if formatting.vertical_trim {
584 let empty_lines = count_empty_lines_at_start(records, pos);
585 index += empty_lines;
586
587 if index > records.count_lines(pos) {
588 return print_indent(f, pad.top.fill, width, pad_color.top.as_ref());
589 }
590 }
591
592 print_indent(f, pad.left.fill, pad.left.size, pad_color.left.as_ref())?;
593
594 let width = width - pad.left.size - pad.right.size;
595 let alignment = *cfg.get_alignment_horizontal(entity);
596 let justification = (
597 cfg.get_justification(entity),
598 cfg.get_justification_color(entity),
599 );
600 let color = colors.get_color(pos);
601 print_line(
602 f,
603 records,
604 pos,
605 index,
606 alignment,
607 formatting,
608 color,
609 justification,
610 width,
611 )?;
612
613 print_indent(f, pad.right.fill, pad.right.size, pad_color.right.as_ref())?;
614
615 Ok(())
616}
617
618#[allow(clippy::too_many_arguments)]
619fn print_line<F: Write, R: Records + PeekableRecords, C: Color>(
620 f: &mut F,
621 records: &R,
622 pos: Position,
623 index: usize,
624 alignment: AlignmentHorizontal,
625 formatting: Formatting,
626 color: Option<C>,
627 justification: (char, Option<&AnsiColor<'_>>),
628 available: usize,
629) -> fmt::Result {
630 let line = records.get_line(pos, index);
631 let (line, line_width) = if formatting.horizontal_trim {
632 let line = string_trim(line);
633 let width = string_width(&line);
634 (line, width)
635 } else {
636 let width = records.get_line_width(pos, index);
637 (Cow::Borrowed(line), width)
638 };
639
640 if formatting.allow_lines_alignment {
641 let (left, right) = calculate_indent(alignment, line_width, available);
642 return print_text_with_pad(f, &line, color, justification, left, right);
643 }
644
645 let cell_width = if formatting.horizontal_trim {
646 (0..records.count_lines(pos))
647 .map(|i| records.get_line(pos, i))
648 .map(|line| string_width(line.trim()))
649 .max()
650 .unwrap_or_default()
651 } else {
652 records.get_width(pos)
653 };
654
655 let (left, right) = calculate_indent(alignment, cell_width, available);
656 print_text_with_pad(f, &line, color, justification, left, right)?;
657
658 // todo: remove me
659 let rest_width = cell_width - line_width;
660 repeat_char(f, ' ', rest_width)?;
661
662 Ok(())
663}
664
665fn print_text_with_pad<F: Write, C: Color>(
666 f: &mut F,
667 text: &str,
668 color: Option<C>,
669 justification: (char, Option<&AnsiColor<'_>>),
670 left: usize,
671 right: usize,
672) -> fmt::Result {
673 print_indent(f, c:justification.0, n:left, color:justification.1)?;
674 print_text(f, text, clr:color)?;
675 print_indent(f, c:justification.0, n:right, color:justification.1)?;
676 Ok(())
677}
678
679fn print_text<F: Write, C: Color>(f: &mut F, text: &str, clr: Option<C>) -> fmt::Result {
680 match clr {
681 Some(color: C) => {
682 color.fmt_prefix(f)?;
683 f.write_str(text)?;
684 color.fmt_suffix(f)
685 }
686 None => f.write_str(text),
687 }
688}
689
690fn top_indent(
691 pad: &Sides<Indent>,
692 alignment: AlignmentVertical,
693 cell_height: usize,
694 available: usize,
695) -> usize {
696 let height: usize = available - pad.top.size;
697 let indent: usize = indent_from_top(alignment, available:height, real:cell_height);
698
699 indent + pad.top.size
700}
701
702fn indent_from_top(alignment: AlignmentVertical, available: usize, real: usize) -> usize {
703 match alignment {
704 AlignmentVertical::Top => 0,
705 AlignmentVertical::Bottom => available - real,
706 AlignmentVertical::Center => (available - real) / 2,
707 }
708}
709
710fn calculate_indent(
711 alignment: AlignmentHorizontal,
712 text_width: usize,
713 available: usize,
714) -> (usize, usize) {
715 let diff: usize = available - text_width;
716 match alignment {
717 AlignmentHorizontal::Left => (0, diff),
718 AlignmentHorizontal::Right => (diff, 0),
719 AlignmentHorizontal::Center => {
720 let left: usize = diff / 2;
721 let rest: usize = diff - left;
722 (left, rest)
723 }
724 }
725}
726
727fn repeat_char<F: Write>(f: &mut F, c: char, n: usize) -> fmt::Result {
728 for _ in 0..n {
729 f.write_char(c)?;
730 }
731
732 Ok(())
733}
734
735fn count_empty_lines_at_end<R>(records: &R, pos: Position) -> usize
736where
737 R: Records + PeekableRecords,
738{
739 (0..records.count_lines(pos))
740 .map(|i: usize| records.get_line(pos, line:i))
741 .rev()
742 .take_while(|l: &{unknown}| l.trim().is_empty())
743 .count()
744}
745
746fn count_empty_lines_at_start<R>(records: &R, pos: Position) -> usize
747where
748 R: Records + PeekableRecords,
749{
750 (0..records.count_lines(pos))
751 .map(|i: usize| records.get_line(pos, line:i))
752 .take_while(|s: &{unknown}| s.trim().is_empty())
753 .count()
754}
755
756fn total_width<D: Dimension>(cfg: &SpannedConfig, dimension: &D, count_columns: usize) -> usize {
757 (0..count_columns)
758 .map(|i: usize| dimension.get_width(column:i))
759 .sum::<usize>()
760 + cfg.count_vertical(count_columns)
761}
762
763fn total_height<D: Dimension>(cfg: &SpannedConfig, dimension: &D, count_rows: usize) -> usize {
764 (0..count_rows)
765 .map(|i: usize| dimension.get_height(row:i))
766 .sum::<usize>()
767 + cfg.count_horizontal(count_rows)
768}
769
770fn print_margin_top<F: Write>(f: &mut F, cfg: &SpannedConfig, width: usize) -> fmt::Result {
771 let indent: Indent = cfg.get_margin().top;
772 let offset: Offset = cfg.get_margin_offset().top;
773 let color: Sides>> = cfg.get_margin_color();
774 let color: Option<&AnsiColor<'_>> = color.top.as_ref();
775 print_indent_lines(f, &indent, &offset, color, width)
776}
777
778fn print_margin_bottom<F: Write>(f: &mut F, cfg: &SpannedConfig, width: usize) -> fmt::Result {
779 let indent: Indent = cfg.get_margin().bottom;
780 let offset: Offset = cfg.get_margin_offset().bottom;
781 let color: Sides>> = cfg.get_margin_color();
782 let color: Option<&AnsiColor<'_>> = color.bottom.as_ref();
783 print_indent_lines(f, &indent, &offset, color, width)
784}
785
786fn print_margin_left<F: Write>(
787 f: &mut F,
788 cfg: &SpannedConfig,
789 line: usize,
790 height: usize,
791) -> fmt::Result {
792 let indent: Indent = cfg.get_margin().left;
793 let offset: Offset = cfg.get_margin_offset().left;
794 let color: Sides>> = cfg.get_margin_color();
795 let color: Option<&AnsiColor<'_>> = color.left.as_ref();
796 print_margin_vertical(f, indent, offset, color, line, height)
797}
798
799fn print_margin_right<F: Write>(
800 f: &mut F,
801 cfg: &SpannedConfig,
802 line: usize,
803 height: usize,
804) -> fmt::Result {
805 let indent: Indent = cfg.get_margin().right;
806 let offset: Offset = cfg.get_margin_offset().right;
807 let color: Sides>> = cfg.get_margin_color();
808 let color: Option<&AnsiColor<'_>> = color.right.as_ref();
809 print_margin_vertical(f, indent, offset, color, line, height)
810}
811
812fn print_margin_vertical<F: Write>(
813 f: &mut F,
814 indent: Indent,
815 offset: Offset,
816 color: Option<&AnsiColor<'_>>,
817 line: usize,
818 height: usize,
819) -> fmt::Result {
820 if indent.size == 0 {
821 return Ok(());
822 }
823
824 match offset {
825 Offset::Begin(offset) => {
826 let offset = cmp::min(offset, height);
827 if line >= offset {
828 print_indent(f, indent.fill, indent.size, color)?;
829 } else {
830 repeat_char(f, ' ', indent.size)?;
831 }
832 }
833 Offset::End(offset) => {
834 let offset = cmp::min(offset, height);
835 let pos = height - offset;
836
837 if line >= pos {
838 repeat_char(f, ' ', indent.size)?;
839 } else {
840 print_indent(f, indent.fill, indent.size, color)?;
841 }
842 }
843 }
844
845 Ok(())
846}
847
848fn print_indent_lines<F: Write>(
849 f: &mut F,
850 indent: &Indent,
851 offset: &Offset,
852 color: Option<&AnsiColor<'_>>,
853 width: usize,
854) -> fmt::Result {
855 if indent.size == 0 {
856 return Ok(());
857 }
858
859 let (start_offset, end_offset) = match offset {
860 Offset::Begin(start) => (*start, 0),
861 Offset::End(end) => (0, *end),
862 };
863
864 let start_offset = std::cmp::min(start_offset, width);
865 let end_offset = std::cmp::min(end_offset, width);
866 let indent_size = width - start_offset - end_offset;
867
868 for i in 0..indent.size {
869 if start_offset > 0 {
870 repeat_char(f, ' ', start_offset)?;
871 }
872
873 if indent_size > 0 {
874 print_indent(f, indent.fill, indent_size, color)?;
875 }
876
877 if end_offset > 0 {
878 repeat_char(f, ' ', end_offset)?;
879 }
880
881 if i + 1 != indent.size {
882 f.write_char('\n')?;
883 }
884 }
885
886 Ok(())
887}
888
889fn print_indent<F: Write, C: Color>(f: &mut F, c: char, n: usize, color: Option<C>) -> fmt::Result {
890 if n == 0 {
891 return Ok(());
892 }
893
894 match color {
895 Some(color: C) => {
896 color.fmt_prefix(f)?;
897 repeat_char(f, c, n)?;
898 color.fmt_suffix(f)
899 }
900 None => repeat_char(f, c, n),
901 }
902}
903
904fn get_cell_width<D: Dimension>(cfg: &SpannedConfig, dims: &D, pos: Position, max: usize) -> usize {
905 match cfg.get_column_span(pos) {
906 Some(span: usize) => {
907 let start: usize = pos.1;
908 let end: usize = pos.1 + span;
909 range_width(dims, start, end) + count_verticals_range(cfg, start, end, max)
910 }
911 None => dims.get_width(column:pos.1),
912 }
913}
914
915fn range_width<D: Dimension>(dims: &D, start: usize, end: usize) -> usize {
916 (start..end).map(|col: usize| dims.get_width(column:col)).sum::<usize>()
917}
918
919fn count_verticals_range(cfg: &SpannedConfig, start: usize, end: usize, max: usize) -> usize {
920 (start + 1..end)
921 .map(|i: usize| cfg.has_vertical(col:i, count_columns:max) as usize)
922 .sum()
923}
924
925fn get_cell_height<D: Dimension>(
926 cfg: &SpannedConfig,
927 dims: &D,
928 pos: Position,
929 max: usize,
930) -> usize {
931 match cfg.get_row_span(pos) {
932 Some(span: usize) => {
933 let start: usize = pos.0;
934 let end: usize = pos.0 + span;
935 range_height(dims, start, end) + count_horizontals_range(cfg, start, end, max)
936 }
937 None => dims.get_height(row:pos.0),
938 }
939}
940
941fn range_height<D: Dimension>(dims: &D, start: usize, end: usize) -> usize {
942 (start..end).map(|col: usize| dims.get_height(row:col)).sum::<usize>()
943}
944
945fn count_horizontals_range(cfg: &SpannedConfig, start: usize, end: usize, max: usize) -> usize {
946 (start + 1..end)
947 .map(|i: usize| cfg.has_horizontal(row:i, count_rows:max) as usize)
948 .sum()
949}
950
951fn closest_visible_row(cfg: &SpannedConfig, mut pos: Position) -> Option<usize> {
952 loop {
953 if cfg.is_cell_visible(pos) {
954 return Some(pos.0);
955 }
956
957 if pos.0 == 0 {
958 return None;
959 }
960
961 pos.0 -= 1;
962 }
963}
964
965/// Trims a string.
966fn string_trim(text: &str) -> Cow<'_, str> {
967 #[cfg(feature = "color")]
968 {
969 ansi_str::AnsiStr::ansi_trim(text)
970 }
971
972 #[cfg(not(feature = "color"))]
973 {
974 text.trim().into()
975 }
976}
977