1 | //! The module contains a [`CompactGrid`] structure, |
2 | //! which is a relatively strict grid. |
3 | |
4 | use core::{ |
5 | borrow::Borrow, |
6 | fmt::{self, Display, Write}, |
7 | }; |
8 | |
9 | use 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 | |
18 | use crate::config::compact::CompactConfig; |
19 | |
20 | /// Grid provides a set of methods for building a text-based table. |
21 | #[derive (Debug, Clone)] |
22 | pub struct CompactGrid<R, D, G, C> { |
23 | records: R, |
24 | config: G, |
25 | dimension: D, |
26 | colors: C, |
27 | } |
28 | |
29 | impl<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 | |
41 | impl<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 | |
87 | impl<R, D, G, C> Display for CompactGrid<R, D, G, C> |
88 | where |
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 | |
102 | fn 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 | |
258 | type ColoredIndent = (Indent, StaticColor); |
259 | |
260 | #[allow (clippy::too_many_arguments)] |
261 | fn 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 |
270 | where |
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 | |
308 | fn 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 | |
319 | fn 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 | |
323 | fn 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 | |
332 | fn 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 | |
341 | fn 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 | |
352 | fn 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 | |
363 | fn 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 | |
374 | fn 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 | |
381 | fn 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 | |
385 | fn 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 | |
395 | type BorderChar = Option<(char, Option<StaticColor>)>; |
396 | |
397 | fn 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> |
404 | where |
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 | |
437 | fn 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 | |
470 | fn 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 | |
498 | fn 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 | |
542 | fn 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 | |
575 | fn 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 | |
586 | fn 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 | |
596 | fn 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 | |
613 | fn 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 | |
621 | fn 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 | |
632 | fn 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 | |
649 | fn 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 | |
657 | fn 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 | |