1//! The module contains a [`CompactGrid`] structure,
2//! which is a relatively strict grid.
3
4use core::{
5 borrow::Borrow,
6 fmt::{self, Display, Write},
7};
8
9use crate::{
10 color::{Color, StaticColor},
11 colors::{Colors, NoColors},
12 config::{AlignmentHorizontal, Borders, Indent, Line, Sides},
13 dimension::Dimension,
14 records::Records,
15 util::string::string_width,
16};
17
18use crate::config::compact::CompactConfig;
19
20/// Grid provides a set of methods for building a text-based table.
21#[derive(Debug, Clone)]
22pub struct CompactGrid<R, D, G, C> {
23 records: R,
24 config: G,
25 dimension: D,
26 colors: C,
27}
28
29impl<R, D, G> CompactGrid<R, D, G, NoColors> {
30 /// The new method creates a grid instance with default styles.
31 pub fn new(records: R, dimension: D, config: G) -> Self {
32 CompactGrid {
33 records,
34 config,
35 dimension,
36 colors: NoColors::default(),
37 }
38 }
39}
40
41impl<R, D, G, C> CompactGrid<R, D, G, C> {
42 /// Sets colors map.
43 pub fn with_colors<Colors>(self, colors: Colors) -> CompactGrid<R, D, G, Colors> {
44 CompactGrid {
45 records: self.records,
46 config: self.config,
47 dimension: self.dimension,
48 colors,
49 }
50 }
51
52 /// Builds a table.
53 pub fn build<F>(self, mut f: F) -> fmt::Result
54 where
55 R: Records,
56 D: Dimension,
57 C: Colors,
58 G: Borrow<CompactConfig>,
59 F: Write,
60 {
61 if self.records.count_columns() == 0 {
62 return Ok(());
63 }
64
65 let config = self.config.borrow();
66 print_grid(&mut f, self.records, config, &self.dimension, &self.colors)
67 }
68
69 /// Builds a table into string.
70 ///
71 /// Notice that it consumes self.
72 #[cfg(feature = "std")]
73 #[allow(clippy::inherent_to_string)]
74 pub fn to_string(self) -> String
75 where
76 R: Records,
77 D: Dimension,
78 G: Borrow<CompactConfig>,
79 C: Colors,
80 {
81 let mut buf = String::new();
82 self.build(&mut buf).expect("It's guaranteed to never happen otherwise it's considered an stdlib error or impl error");
83 buf
84 }
85}
86
87impl<R, D, G, C> Display for CompactGrid<R, D, G, C>
88where
89 for<'a> &'a R: Records,
90 D: Dimension,
91 G: Borrow<CompactConfig>,
92 C: Colors,
93{
94 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95 let records: &R = &self.records;
96 let config: &CompactConfig = self.config.borrow();
97
98 print_grid(f, records, cfg:config, &self.dimension, &self.colors)
99 }
100}
101
102fn print_grid<F: Write, R: Records, D: Dimension, C: Colors>(
103 f: &mut F,
104 records: R,
105 cfg: &CompactConfig,
106 dims: &D,
107 colors: &C,
108) -> fmt::Result {
109 let count_columns = records.count_columns();
110 let count_rows = records.hint_count_rows();
111
112 if count_columns == 0 || matches!(count_rows, Some(0)) {
113 return Ok(());
114 }
115
116 let mut records = records.iter_rows().into_iter();
117 let mut next_columns = records.next();
118
119 if next_columns.is_none() {
120 return Ok(());
121 }
122
123 let wtotal = total_width(cfg, dims, count_columns);
124
125 let borders = cfg.get_borders();
126 let bcolors = cfg.get_borders_color();
127
128 let h_chars = create_horizontal(borders);
129 let h_colors = create_horizontal_colors(bcolors);
130
131 let has_horizontal = borders.has_horizontal();
132 let has_horizontal_colors = bcolors.has_horizontal();
133 let has_horizontal_second = cfg.get_first_horizontal_line().is_some();
134
135 let vert = (
136 borders.left.map(|c| (c, bcolors.left)),
137 borders.vertical.map(|c| (c, bcolors.vertical)),
138 borders.right.map(|c| (c, bcolors.right)),
139 );
140
141 let margin = cfg.get_margin();
142 let margin_color = cfg.get_margin_color();
143 let pad = create_padding(cfg);
144 let align = cfg.get_alignment_horizontal();
145 let mar = (
146 (margin.left, margin_color.left),
147 (margin.right, margin_color.right),
148 );
149
150 let widths = (0..count_columns).map(|col| dims.get_width(col));
151
152 let mut new_line = false;
153
154 if margin.top.size > 0 {
155 let wtotal = wtotal + margin.left.size + margin.right.size;
156 print_indent_lines(f, wtotal, margin.top, margin_color.top)?;
157 new_line = true;
158 }
159
160 if borders.has_top() {
161 if new_line {
162 f.write_char('\n')?
163 }
164
165 print_indent2(f, margin.left, margin_color.left)?;
166
167 let chars = create_horizontal_top(borders);
168 if bcolors.has_top() {
169 let chars_color = create_horizontal_top_colors(bcolors);
170 print_split_line_colored(f, chars, chars_color, dims, count_columns)?;
171 } else {
172 print_split_line(f, chars, dims, count_columns)?;
173 }
174
175 print_indent2(f, margin.right, margin_color.right)?;
176
177 new_line = true;
178 }
179
180 let mut row = 0;
181 while let Some(columns) = next_columns {
182 let columns = columns.into_iter();
183 next_columns = records.next();
184
185 if row > 0 && has_horizontal {
186 if new_line {
187 f.write_char('\n')?;
188 }
189
190 print_indent2(f, margin.left, margin_color.left)?;
191
192 if has_horizontal_colors {
193 print_split_line_colored(f, h_chars, h_colors, dims, count_columns)?;
194 } else {
195 print_split_line(f, h_chars, dims, count_columns)?;
196 }
197
198 print_indent2(f, margin.right, margin_color.right)?;
199 } else if row == 1 && has_horizontal_second {
200 if new_line {
201 f.write_char('\n')?;
202 }
203
204 print_indent2(f, margin.left, margin_color.left)?;
205
206 let h_chars = cfg.get_first_horizontal_line().expect("must be here");
207
208 if has_horizontal_colors {
209 print_split_line_colored(f, h_chars, h_colors, dims, count_columns)?;
210 } else {
211 print_split_line(f, h_chars, dims, count_columns)?;
212 }
213
214 print_indent2(f, margin.right, margin_color.right)?;
215 }
216
217 if new_line {
218 f.write_char('\n')?;
219 }
220
221 let columns = columns
222 .enumerate()
223 .map(|(col, text)| (text, colors.get_color((row, col))));
224
225 let widths = widths.clone();
226 print_grid_row(f, columns, widths, mar, pad, vert, align)?;
227
228 new_line = true;
229 row += 1;
230 }
231
232 if borders.has_bottom() {
233 f.write_char('\n')?;
234
235 print_indent2(f, margin.left, margin_color.left)?;
236
237 let chars = create_horizontal_bottom(borders);
238 if bcolors.has_bottom() {
239 let chars_color = create_horizontal_bottom_colors(bcolors);
240 print_split_line_colored(f, chars, chars_color, dims, count_columns)?;
241 } else {
242 print_split_line(f, chars, dims, count_columns)?;
243 }
244
245 print_indent2(f, margin.right, margin_color.right)?;
246 }
247
248 if cfg.get_margin().bottom.size > 0 {
249 f.write_char('\n')?;
250
251 let wtotal = wtotal + margin.left.size + margin.right.size;
252 print_indent_lines(f, wtotal, margin.bottom, margin_color.bottom)?;
253 }
254
255 Ok(())
256}
257
258type ColoredIndent = (Indent, StaticColor);
259
260#[allow(clippy::too_many_arguments)]
261fn print_grid_row<F, I, T, C, D>(
262 f: &mut F,
263 columns: I,
264 widths: D,
265 mar: (ColoredIndent, ColoredIndent),
266 pad: Sides<ColoredIndent>,
267 vert: (BorderChar, BorderChar, BorderChar),
268 align: AlignmentHorizontal,
269) -> fmt::Result
270where
271 F: Write,
272 I: Iterator<Item = (T, Option<C>)>,
273 T: AsRef<str>,
274 C: Color,
275 D: Iterator<Item = usize> + Clone,
276{
277 if pad.top.0.size > 0 {
278 for _ in 0..pad.top.0.size {
279 print_indent2(f, mar.0 .0, mar.0 .1)?;
280 print_columns_empty_colored(f, widths.clone(), vert, pad.top.1)?;
281 print_indent2(f, mar.1 .0, mar.1 .1)?;
282
283 f.write_char('\n')?;
284 }
285 }
286
287 let mut widths1 = widths.clone();
288 let columns = columns.map(move |(text, color)| {
289 let width = widths1.next().expect("must be here");
290 (text, color, width)
291 });
292
293 print_indent2(f, mar.0 .0, mar.0 .1)?;
294 print_row_columns(f, columns, vert, pad, align)?;
295 print_indent2(f, mar.1 .0, mar.1 .1)?;
296
297 for _ in 0..pad.bottom.0.size {
298 f.write_char('\n')?;
299
300 print_indent2(f, mar.0 .0, mar.0 .1)?;
301 print_columns_empty_colored(f, widths.clone(), vert, pad.bottom.1)?;
302 print_indent2(f, mar.1 .0, mar.1 .1)?;
303 }
304
305 Ok(())
306}
307
308fn create_padding(cfg: &CompactConfig) -> Sides<ColoredIndent> {
309 let pad: &Sides = cfg.get_padding();
310 let pad_colors: Sides = cfg.get_padding_color();
311 Sides::new(
312 (pad.left, pad_colors.left),
313 (pad.right, pad_colors.right),
314 (pad.top, pad_colors.top),
315 (pad.bottom, pad_colors.bottom),
316 )
317}
318
319fn create_horizontal(b: &Borders<char>) -> Line<char> {
320 Line::new(main:b.horizontal.unwrap_or(' '), b.intersection, connect1:b.left, connect2:b.right)
321}
322
323fn create_horizontal_top(b: &Borders<char>) -> Line<char> {
324 Line::new(
325 main:b.top.unwrap_or(' '),
326 b.top_intersection,
327 connect1:b.top_left,
328 connect2:b.top_right,
329 )
330}
331
332fn create_horizontal_bottom(b: &Borders<char>) -> Line<char> {
333 Line::new(
334 main:b.bottom.unwrap_or(' '),
335 b.bottom_intersection,
336 connect1:b.bottom_left,
337 connect2:b.bottom_right,
338 )
339}
340
341fn create_horizontal_colors(
342 b: &Borders<StaticColor>,
343) -> (StaticColor, StaticColor, StaticColor, StaticColor) {
344 (
345 b.horizontal.unwrap_or(StaticColor::default()),
346 b.left.unwrap_or(StaticColor::default()),
347 b.intersection.unwrap_or(StaticColor::default()),
348 b.right.unwrap_or(StaticColor::default()),
349 )
350}
351
352fn create_horizontal_top_colors(
353 b: &Borders<StaticColor>,
354) -> (StaticColor, StaticColor, StaticColor, StaticColor) {
355 (
356 b.top.unwrap_or(StaticColor::default()),
357 b.top_left.unwrap_or(StaticColor::default()),
358 b.top_intersection.unwrap_or(StaticColor::default()),
359 b.top_right.unwrap_or(StaticColor::default()),
360 )
361}
362
363fn create_horizontal_bottom_colors(
364 b: &Borders<StaticColor>,
365) -> (StaticColor, StaticColor, StaticColor, StaticColor) {
366 (
367 b.bottom.unwrap_or(StaticColor::default()),
368 b.bottom_left.unwrap_or(StaticColor::default()),
369 b.bottom_intersection.unwrap_or(StaticColor::default()),
370 b.bottom_right.unwrap_or(StaticColor::default()),
371 )
372}
373
374fn total_width<D: Dimension>(cfg: &CompactConfig, dims: &D, count_columns: usize) -> usize {
375 let content_width: usize = total_columns_width(count_columns, dims);
376 let count_verticals: usize = count_verticals(cfg, count_columns);
377
378 content_width + count_verticals
379}
380
381fn total_columns_width<D: Dimension>(count_columns: usize, dims: &D) -> usize {
382 (0..count_columns).map(|i: usize| dims.get_width(column:i)).sum::<usize>()
383}
384
385fn count_verticals(cfg: &CompactConfig, count_columns: usize) -> usize {
386 assert!(count_columns > 0);
387
388 let count_verticals: usize = count_columns - 1;
389 let borders: &Borders = cfg.get_borders();
390 borders.has_vertical() as usize * count_verticals
391 + borders.has_left() as usize
392 + borders.has_right() as usize
393}
394
395type BorderChar = Option<(char, Option<StaticColor>)>;
396
397fn print_row_columns<F, I, T, C>(
398 f: &mut F,
399 mut columns: I,
400 borders: (BorderChar, BorderChar, BorderChar),
401 pad: Sides<ColoredIndent>,
402 align: AlignmentHorizontal,
403) -> Result<(), fmt::Error>
404where
405 F: Write,
406 I: Iterator<Item = (T, Option<C>, usize)>,
407 T: AsRef<str>,
408 C: Color,
409{
410 if let Some((c, color)) = borders.0 {
411 print_char(f, c, color)?;
412 }
413
414 if let Some((text, color, width)) = columns.next() {
415 let text = text.as_ref();
416 let text = text.lines().next().unwrap_or("");
417 print_cell(f, text, width, color, (pad.left, pad.right), align)?;
418 }
419
420 for (text, color, width) in columns {
421 if let Some((c, color)) = borders.1 {
422 print_char(f, c, color)?;
423 }
424
425 let text = text.as_ref();
426 let text = text.lines().next().unwrap_or("");
427 print_cell(f, text, width, color, (pad.left, pad.right), align)?;
428 }
429
430 if let Some((c, color)) = borders.2 {
431 print_char(f, c, color)?;
432 }
433
434 Ok(())
435}
436
437fn print_columns_empty_colored<F: Write, I: Iterator<Item = usize>>(
438 f: &mut F,
439 mut columns: I,
440 borders: (BorderChar, BorderChar, BorderChar),
441 color: StaticColor,
442) -> Result<(), fmt::Error> {
443 if let Some((c, color)) = borders.0 {
444 print_char(f, c, color)?;
445 }
446
447 if let Some(width) = columns.next() {
448 color.fmt_prefix(f)?;
449 repeat_char(f, ' ', width)?;
450 color.fmt_suffix(f)?;
451 }
452
453 for width in columns {
454 if let Some((c, color)) = borders.1 {
455 print_char(f, c, color)?;
456 }
457
458 color.fmt_prefix(f)?;
459 repeat_char(f, ' ', width)?;
460 color.fmt_suffix(f)?;
461 }
462
463 if let Some((c, color)) = borders.2 {
464 print_char(f, c, color)?;
465 }
466
467 Ok(())
468}
469
470fn print_cell<F: Write, C: Color>(
471 f: &mut F,
472 text: &str,
473 width: usize,
474 color: Option<C>,
475 (pad_l: (Indent, StaticColor), pad_r: (Indent, StaticColor)): (ColoredIndent, ColoredIndent),
476 align: AlignmentHorizontal,
477) -> fmt::Result {
478 let available: usize = width - pad_l.0.size - pad_r.0.size;
479
480 let text_width: usize = string_width(text);
481 let (left: usize, right: usize) = if available < text_width {
482 (0, 0)
483 } else {
484 calculate_indent(alignment:align, text_width, available)
485 };
486
487 print_indent(f, c:pad_l.0.fill, n:pad_l.0.size, color:pad_l.1)?;
488
489 repeat_char(f, c:' ', n:left)?;
490 print_text(f, text, clr:color)?;
491 repeat_char(f, c:' ', n:right)?;
492
493 print_indent(f, c:pad_r.0.fill, n:pad_r.0.size, color:pad_r.1)?;
494
495 Ok(())
496}
497
498fn print_split_line_colored<F: Write>(
499 f: &mut F,
500 chars: Line<char>,
501 colors: (StaticColor, StaticColor, StaticColor, StaticColor),
502 dimension: impl Dimension,
503 count_columns: usize,
504) -> fmt::Result {
505 let mut used_color = StaticColor::default();
506
507 if let Some(c) = chars.connect1 {
508 colors.1.fmt_prefix(f)?;
509 f.write_char(c)?;
510 used_color = colors.1;
511 }
512
513 let width = dimension.get_width(0);
514 if width > 0 {
515 prepare_coloring(f, &colors.0, &mut used_color)?;
516 repeat_char(f, chars.main, width)?;
517 }
518
519 for col in 1..count_columns {
520 if let Some(c) = &chars.intersection {
521 prepare_coloring(f, &colors.2, &mut used_color)?;
522 f.write_char(*c)?;
523 }
524
525 let width = dimension.get_width(col);
526 if width > 0 {
527 prepare_coloring(f, &colors.0, &mut used_color)?;
528 repeat_char(f, chars.main, width)?;
529 }
530 }
531
532 if let Some(c) = &chars.connect2 {
533 prepare_coloring(f, &colors.3, &mut used_color)?;
534 f.write_char(*c)?;
535 }
536
537 used_color.fmt_suffix(f)?;
538
539 Ok(())
540}
541
542fn print_split_line<F: Write>(
543 f: &mut F,
544 chars: Line<char>,
545 dimension: impl Dimension,
546 count_columns: usize,
547) -> fmt::Result {
548 if let Some(c) = chars.connect1 {
549 f.write_char(c)?;
550 }
551
552 let width = dimension.get_width(0);
553 if width > 0 {
554 repeat_char(f, chars.main, width)?;
555 }
556
557 for col in 1..count_columns {
558 if let Some(c) = chars.intersection {
559 f.write_char(c)?;
560 }
561
562 let width = dimension.get_width(col);
563 if width > 0 {
564 repeat_char(f, chars.main, width)?;
565 }
566 }
567
568 if let Some(c) = chars.connect2 {
569 f.write_char(c)?;
570 }
571
572 Ok(())
573}
574
575fn print_text<F: Write>(f: &mut F, text: &str, clr: Option<impl Color>) -> fmt::Result {
576 match clr {
577 Some(color: impl Color) => {
578 color.fmt_prefix(f)?;
579 f.write_str(text)?;
580 color.fmt_suffix(f)
581 }
582 None => f.write_str(text),
583 }
584}
585
586fn prepare_coloring<F: Write>(f: &mut F, clr: &StaticColor, used: &mut StaticColor) -> fmt::Result {
587 if *used != *clr {
588 used.fmt_suffix(f)?;
589 clr.fmt_prefix(f)?;
590 *used = *clr;
591 }
592
593 Ok(())
594}
595
596fn calculate_indent(
597 alignment: AlignmentHorizontal,
598 text_width: usize,
599 available: usize,
600) -> (usize, usize) {
601 let diff: usize = available - text_width;
602 match alignment {
603 AlignmentHorizontal::Left => (0, diff),
604 AlignmentHorizontal::Right => (diff, 0),
605 AlignmentHorizontal::Center => {
606 let left: usize = diff / 2;
607 let rest: usize = diff - left;
608 (left, rest)
609 }
610 }
611}
612
613fn repeat_char<F: Write>(f: &mut F, c: char, n: usize) -> fmt::Result {
614 for _ in 0..n {
615 f.write_char(c)?;
616 }
617
618 Ok(())
619}
620
621fn print_char<F: Write>(f: &mut F, c: char, color: Option<StaticColor>) -> fmt::Result {
622 match color {
623 Some(color: StaticColor) => {
624 color.fmt_prefix(f)?;
625 f.write_char(c)?;
626 color.fmt_suffix(f)
627 }
628 None => f.write_char(c),
629 }
630}
631
632fn print_indent_lines<F: Write>(
633 f: &mut F,
634 width: usize,
635 indent: Indent,
636 color: StaticColor,
637) -> fmt::Result {
638 print_indent(f, c:indent.fill, n:width, color)?;
639 f.write_char('\n')?;
640
641 for _ in 1..indent.size {
642 f.write_char('\n')?;
643 print_indent(f, c:indent.fill, n:width, color)?;
644 }
645
646 Ok(())
647}
648
649fn print_indent<F: Write>(f: &mut F, c: char, n: usize, color: StaticColor) -> fmt::Result {
650 color.fmt_prefix(f)?;
651 repeat_char(f, c, n)?;
652 color.fmt_suffix(f)?;
653
654 Ok(())
655}
656
657fn print_indent2<F: Write>(f: &mut F, indent: Indent, color: StaticColor) -> fmt::Result {
658 print_indent(f, c:indent.fill, n:indent.size, color)
659}
660