1use 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#[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 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 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 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 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 let original_row = closest_visible_row(cfg, (row, col), shape).unwrap();
395
396 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 let original_row = closest_visible_row(cfg, (row, col), shape).unwrap();
524
525 let mut skip_lines = (original_row..row)
527 .map(|i| height_ctrl.get(i).unwrap())
528 .sum::<usize>();
529
530 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 if let Some(span) = cfg.get_column_span((original_row, col), shape) {
543 col += span - 1;
544 }
545 } else if width > 0 {
546 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 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 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 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 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
970fn 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}