1//! The module contains a [`Grid`] structure.
2
3use std::{
4 borrow::{Borrow, Cow},
5 cmp,
6 collections::BTreeMap,
7 fmt::{self, Write},
8};
9
10use crate::{
11 color::{AnsiColor, Color},
12 colors::Colors,
13 config::{AlignmentHorizontal, AlignmentVertical, Indent, Position, Sides},
14 dimension::Dimension,
15 records::Records,
16 util::string::{count_lines, get_lines, string_width, string_width_multiline, Lines},
17};
18
19use crate::config::spanned::{Formatting, Offset, SpannedConfig};
20
21/// Grid provides a set of methods for building a text-based table.
22#[derive(Debug, Clone)]
23pub struct Grid<R, D, G, C> {
24 records: R,
25 config: G,
26 dimension: D,
27 colors: C,
28}
29
30impl<R, D, G, C> Grid<R, D, G, C> {
31 /// The new method creates a grid instance with default styles.
32 pub fn new(records: R, dimension: D, config: G, colors: C) -> Self {
33 Grid {
34 records,
35 config,
36 dimension,
37 colors,
38 }
39 }
40}
41
42impl<R, D, G, C> Grid<R, D, G, C> {
43 /// Builds a table.
44 pub fn build<F>(self, mut f: F) -> fmt::Result
45 where
46 R: Records,
47 D: Dimension,
48 C: Colors,
49 G: Borrow<SpannedConfig>,
50 F: Write,
51 {
52 if self.records.count_columns() == 0 || self.records.hint_count_rows() == Some(0) {
53 return Ok(());
54 }
55
56 let config = self.config.borrow();
57 print_grid(&mut f, self.records, config, &self.dimension, &self.colors)
58 }
59
60 /// Builds a table into string.
61 ///
62 /// Notice that it consumes self.
63 #[allow(clippy::inherent_to_string)]
64 pub fn to_string(self) -> String
65 where
66 R: Records,
67 D: Dimension,
68 G: Borrow<SpannedConfig>,
69 C: Colors,
70 {
71 let mut buf = String::new();
72 self.build(&mut buf).expect("It's guaranteed to never happen otherwise it's considered an stdlib error or impl error");
73 buf
74 }
75}
76
77fn print_grid<F: Write, R: Records, D: Dimension, C: Colors>(
78 f: &mut F,
79 records: R,
80 cfg: &SpannedConfig,
81 dimension: &D,
82 colors: &C,
83) -> fmt::Result {
84 // spanned version is a bit more complex and 'supposedly' slower,
85 // because spans are considered to be not a general case we are having 2 versions
86 let grid_has_spans: bool = cfg.has_column_spans() || cfg.has_row_spans();
87 if grid_has_spans {
88 print_grid_spanned(f, records, cfg, dims:dimension, colors)
89 } else {
90 print_grid_general(f, records, cfg, dims:dimension, colors)
91 }
92}
93
94fn print_grid_general<F: Write, R: Records, D: Dimension, C: Colors>(
95 f: &mut F,
96 records: R,
97 cfg: &SpannedConfig,
98 dims: &D,
99 colors: &C,
100) -> fmt::Result {
101 let count_columns = records.count_columns();
102
103 let mut totalw = None;
104 let totalh = records
105 .hint_count_rows()
106 .map(|count_rows| total_height(cfg, dims, count_rows));
107
108 let mut records_iter = records.iter_rows().into_iter();
109 let mut next_columns = records_iter.next();
110
111 if next_columns.is_none() {
112 return Ok(());
113 }
114
115 if cfg.get_margin().top.size > 0 {
116 totalw = Some(output_width(cfg, dims, count_columns));
117
118 print_margin_top(f, cfg, totalw.unwrap())?;
119 f.write_char('\n')?;
120 }
121
122 let mut row = 0;
123 let mut line = 0;
124 let mut is_prev_row_skipped = false;
125 let mut buf = None;
126 while let Some(columns) = next_columns {
127 let columns = columns.into_iter();
128 next_columns = records_iter.next();
129 let is_last_row = next_columns.is_none();
130
131 let height = dims.get_height(row);
132 let count_rows = convert_count_rows(row, is_last_row);
133 let has_horizontal = cfg.has_horizontal(row, count_rows);
134 let shape = (count_rows, count_columns);
135
136 if row > 0 && !is_prev_row_skipped && (has_horizontal || height > 0) {
137 f.write_char('\n')?;
138 }
139
140 if has_horizontal {
141 print_horizontal_line(f, cfg, line, totalh, dims, row, shape)?;
142
143 line += 1;
144
145 if height > 0 {
146 f.write_char('\n')?;
147 }
148 }
149
150 if height == 1 {
151 print_single_line_columns(f, columns, cfg, colors, dims, row, line, totalh, shape)?
152 } else if height > 0 {
153 if buf.is_none() {
154 buf = Some(Vec::with_capacity(count_columns));
155 }
156
157 let buf = buf.as_mut().unwrap();
158 print_multiline_columns(
159 f, columns, cfg, colors, dims, height, row, line, totalh, shape, buf,
160 )?;
161
162 buf.clear();
163 }
164
165 if height == 0 && !has_horizontal {
166 is_prev_row_skipped = true;
167 } else {
168 is_prev_row_skipped = false;
169 }
170
171 line += height;
172 row += 1;
173 }
174
175 if cfg.has_horizontal(row, row) {
176 f.write_char('\n')?;
177 let shape = (row, count_columns);
178 print_horizontal_line(f, cfg, line, totalh, dims, row, shape)?;
179 }
180
181 {
182 let margin = cfg.get_margin();
183 if margin.bottom.size > 0 {
184 let totalw = totalw.unwrap_or_else(|| output_width(cfg, dims, count_columns));
185
186 f.write_char('\n')?;
187 print_margin_bottom(f, cfg, totalw)?;
188 }
189 }
190
191 Ok(())
192}
193
194fn output_width<D: Dimension>(cfg: &SpannedConfig, d: D, count_columns: usize) -> usize {
195 let margin: Sides = cfg.get_margin();
196 total_width(cfg, &d, count_columns) + margin.left.size + margin.right.size
197}
198
199#[allow(clippy::too_many_arguments)]
200fn print_horizontal_line<F: Write, D: Dimension>(
201 f: &mut F,
202 cfg: &SpannedConfig,
203 line: usize,
204 totalh: Option<usize>,
205 dimension: &D,
206 row: usize,
207 shape: (usize, usize),
208) -> fmt::Result {
209 print_margin_left(f, cfg, line, height:totalh)?;
210 print_split_line(f, cfg, dimension, row, shape)?;
211 print_margin_right(f, cfg, line, height:totalh)?;
212 Ok(())
213}
214
215#[allow(clippy::too_many_arguments)]
216fn print_multiline_columns<'a, F, I, D, C>(
217 f: &mut F,
218 columns: I,
219 cfg: &'a SpannedConfig,
220 colors: &'a C,
221 dimension: &D,
222 height: usize,
223 row: usize,
224 line: usize,
225 totalh: Option<usize>,
226 shape: (usize, usize),
227 buf: &mut Vec<Cell<I::Item, &'a C::Color>>,
228) -> fmt::Result
229where
230 F: Write,
231 I: Iterator,
232 I::Item: AsRef<str>,
233 D: Dimension,
234 C: Colors,
235{
236 collect_columns(buf, iter:columns, cfg, colors, dimension, height, row);
237 print_columns_lines(f, buf, height, cfg, line, row, totalh, shape)?;
238 Ok(())
239}
240
241#[allow(clippy::too_many_arguments)]
242fn print_single_line_columns<F, I, D, C>(
243 f: &mut F,
244 columns: I,
245 cfg: &SpannedConfig,
246 colors: &C,
247 dims: &D,
248 row: usize,
249 line: usize,
250 totalh: Option<usize>,
251 shape: (usize, usize),
252) -> fmt::Result
253where
254 F: Write,
255 I: Iterator,
256 I::Item: AsRef<str>,
257 D: Dimension,
258 C: Colors,
259{
260 print_margin_left(f, cfg, line, height:totalh)?;
261
262 for (col: usize, cell: ::Item) in columns.enumerate() {
263 let pos: (usize, usize) = (row, col);
264 let width: usize = dims.get_width(column:col);
265 let color: Option<&::Color> = colors.get_color(pos);
266 print_vertical_char(f, cfg, pos, line:0, count_lines:1, count_columns:shape.1)?;
267 print_single_line_column(f, text:cell.as_ref(), cfg, width, color, pos)?;
268 }
269
270 print_vertical_char(f, cfg, (row, shape.1), line:0, count_lines:1, count_columns:shape.1)?;
271
272 print_margin_right(f, cfg, line, height:totalh)?;
273
274 Ok(())
275}
276
277fn print_single_line_column<F: Write, C: Color>(
278 f: &mut F,
279 text: &str,
280 cfg: &SpannedConfig,
281 width: usize,
282 color: Option<&C>,
283 pos: Position,
284) -> fmt::Result {
285 let pos = pos.into();
286 let pad = cfg.get_padding(pos);
287 let pad_color = cfg.get_padding_color(pos);
288 let fmt = cfg.get_formatting(pos);
289 let space = cfg.get_justification(pos);
290 let space_color = cfg.get_justification_color(pos);
291
292 let (text, text_width) = if fmt.horizontal_trim && !text.is_empty() {
293 let text = string_trim(text);
294 let width = string_width(&text);
295
296 (text, width)
297 } else {
298 let text = Cow::Borrowed(text);
299 let width = string_width_multiline(&text);
300
301 (text, width)
302 };
303
304 let alignment = *cfg.get_alignment_horizontal(pos);
305 let available_width = width - pad.left.size - pad.right.size;
306 let (left, right) = calculate_indent(alignment, text_width, available_width);
307
308 print_padding(f, &pad.left, pad_color.left.as_ref())?;
309
310 print_indent(f, space, left, space_color)?;
311 print_text(f, &text, color)?;
312 print_indent(f, space, right, space_color)?;
313
314 print_padding(f, &pad.right, pad_color.right.as_ref())?;
315
316 Ok(())
317}
318
319#[allow(clippy::too_many_arguments)]
320fn print_columns_lines<T, F: Write, C: Color>(
321 f: &mut F,
322 buf: &mut [Cell<T, C>],
323 height: usize,
324 cfg: &SpannedConfig,
325 line: usize,
326 row: usize,
327 totalh: Option<usize>,
328 shape: (usize, usize),
329) -> fmt::Result {
330 for i: usize in 0..height {
331 let exact_line: usize = line + i;
332
333 print_margin_left(f, cfg, exact_line, height:totalh)?;
334
335 for (col: usize, cell: &mut Cell) in buf.iter_mut().enumerate() {
336 print_vertical_char(f, cfg, (row, col), line:i, count_lines:height, count_columns:shape.1)?;
337 cell.display(f)?;
338 }
339
340 print_vertical_char(f, cfg, (row, shape.1), line:i, count_lines:height, count_columns:shape.1)?;
341
342 print_margin_right(f, cfg, exact_line, height:totalh)?;
343
344 if i + 1 != height {
345 f.write_char('\n')?;
346 }
347 }
348
349 Ok(())
350}
351
352fn collect_columns<'a, I, D, C>(
353 buf: &mut Vec<Cell<I::Item, &'a C::Color>>,
354 iter: I,
355 cfg: &SpannedConfig,
356 colors: &'a C,
357 dimension: &D,
358 height: usize,
359 row: usize,
360) where
361 I: Iterator,
362 I::Item: AsRef<str>,
363 C: Colors,
364 D: Dimension,
365{
366 let iter: impl Iterator::Item, …>> = iter.enumerate().map(|(col: usize, cell: ::Item)| {
367 let pos: (usize, usize) = (row, col);
368 let width: usize = dimension.get_width(column:col);
369 let color: Option<&::Color> = colors.get_color(pos);
370 Cell::new(text:cell, width, height, cfg, color, pos)
371 });
372
373 buf.extend(iter);
374}
375
376fn print_split_line<F: Write, D: Dimension>(
377 f: &mut F,
378 cfg: &SpannedConfig,
379 dimension: &D,
380 row: usize,
381 shape: (usize, usize),
382) -> fmt::Result {
383 let mut used_color = None;
384 print_vertical_intersection(f, cfg, (row, 0), shape, &mut used_color)?;
385
386 for col in 0..shape.1 {
387 let width = dimension.get_width(col);
388
389 // general case
390 if width > 0 {
391 let pos = (row, col);
392 let main = cfg.get_horizontal(pos, shape.0);
393 match main {
394 Some(c) => {
395 let clr = cfg.get_horizontal_color(pos, shape.0);
396 prepare_coloring(f, clr, &mut used_color)?;
397 print_horizontal_border(f, cfg, pos, width, c, &used_color)?;
398 }
399 None => repeat_char(f, ' ', width)?,
400 }
401 }
402
403 print_vertical_intersection(f, cfg, (row, col + 1), shape, &mut used_color)?;
404 }
405
406 if let Some(clr) = used_color.take() {
407 clr.fmt_suffix(f)?;
408 }
409
410 Ok(())
411}
412
413fn print_grid_spanned<F: Write, R: Records, D: Dimension, C: Colors>(
414 f: &mut F,
415 records: R,
416 cfg: &SpannedConfig,
417 dims: &D,
418 colors: &C,
419) -> fmt::Result {
420 let count_columns = records.count_columns();
421
422 let total_width = total_width(cfg, dims, count_columns);
423 let margin = cfg.get_margin();
424 let total_width_with_margin = total_width + margin.left.size + margin.right.size;
425
426 let totalh = records
427 .hint_count_rows()
428 .map(|rows| total_height(cfg, dims, rows));
429
430 if margin.top.size > 0 {
431 print_margin_top(f, cfg, total_width_with_margin)?;
432 f.write_char('\n')?;
433 }
434
435 let mut buf = BTreeMap::new();
436
437 let mut records_iter = records.iter_rows().into_iter();
438 let mut next_columns = records_iter.next();
439
440 let mut need_new_line = false;
441 let mut line = 0;
442 let mut row = 0;
443 while let Some(columns) = next_columns {
444 let columns = columns.into_iter();
445 next_columns = records_iter.next();
446 let is_last_row = next_columns.is_none();
447
448 let height = dims.get_height(row);
449 let count_rows = convert_count_rows(row, is_last_row);
450 let shape = (count_rows, count_columns);
451
452 let has_horizontal = cfg.has_horizontal(row, count_rows);
453 if need_new_line && (has_horizontal || height > 0) {
454 f.write_char('\n')?;
455 need_new_line = false;
456 }
457
458 if has_horizontal {
459 print_margin_left(f, cfg, line, totalh)?;
460 print_split_line_spanned(f, &mut buf, cfg, dims, row, shape)?;
461 print_margin_right(f, cfg, line, totalh)?;
462
463 line += 1;
464
465 if height > 0 {
466 f.write_char('\n')?;
467 }
468 }
469
470 print_spanned_columns(
471 f, &mut buf, columns, cfg, colors, dims, height, row, line, totalh, shape,
472 )?;
473
474 if has_horizontal || height > 0 {
475 need_new_line = true;
476 }
477
478 line += height;
479 row += 1;
480 }
481
482 if row > 0 {
483 if cfg.has_horizontal(row, row) {
484 f.write_char('\n')?;
485 let shape = (row, count_columns);
486 print_horizontal_line(f, cfg, line, totalh, dims, row, shape)?;
487 }
488
489 if margin.bottom.size > 0 {
490 f.write_char('\n')?;
491 print_margin_bottom(f, cfg, total_width_with_margin)?;
492 }
493 }
494
495 Ok(())
496}
497
498fn print_split_line_spanned<S, F: Write, D: Dimension, C: Color>(
499 f: &mut F,
500 buf: &mut BTreeMap<usize, (Cell<S, C>, usize, usize)>,
501 cfg: &SpannedConfig,
502 dimension: &D,
503 row: usize,
504 shape: (usize, usize),
505) -> fmt::Result {
506 let mut used_color = None;
507 print_vertical_intersection(f, cfg, (row, 0), shape, &mut used_color)?;
508
509 for col in 0..shape.1 {
510 let pos = (row, col);
511 if cfg.is_cell_covered_by_both_spans(pos) {
512 continue;
513 }
514
515 let width = dimension.get_width(col);
516 let mut col = col;
517 if cfg.is_cell_covered_by_row_span(pos) {
518 // means it's part of other a spanned cell
519 // so. we just need to use line from other cell.
520
521 let (cell, _, _) = buf.get_mut(&col).unwrap();
522 cell.display(f)?;
523
524 // We need to use a correct right split char.
525 let original_row = closest_visible_row(cfg, pos).unwrap();
526 if let Some(span) = cfg.get_column_span((original_row, col)) {
527 col += span - 1;
528 }
529 } else if width > 0 {
530 // general case
531 let main = cfg.get_horizontal(pos, shape.0);
532 match main {
533 Some(c) => {
534 let clr = cfg.get_horizontal_color(pos, shape.0);
535 prepare_coloring(f, clr, &mut used_color)?;
536 print_horizontal_border(f, cfg, pos, width, c, &used_color)?;
537 }
538 None => repeat_char(f, ' ', width)?,
539 }
540 }
541
542 print_vertical_intersection(f, cfg, (row, col + 1), shape, &mut used_color)?;
543 }
544
545 if let Some(clr) = used_color.take() {
546 clr.fmt_suffix(f)?;
547 }
548
549 Ok(())
550}
551
552fn print_vertical_intersection<'a, F: fmt::Write>(
553 f: &mut F,
554 cfg: &'a SpannedConfig,
555 pos: Position,
556 shape: (usize, usize),
557 used_color: &mut Option<&'a AnsiColor<'static>>,
558) -> fmt::Result {
559 match cfg.get_intersection(pos, shape) {
560 Some(c: char) => {
561 let clr: Option<&AnsiColor<'_>> = cfg.get_intersection_color(pos, shape);
562 prepare_coloring(f, clr, used_color)?;
563 f.write_char(c)
564 }
565 None => Ok(()),
566 }
567}
568
569#[allow(clippy::too_many_arguments, clippy::type_complexity)]
570fn print_spanned_columns<'a, F, I, D, C>(
571 f: &mut F,
572 buf: &mut BTreeMap<usize, (Cell<I::Item, &'a C::Color>, usize, usize)>,
573 iter: I,
574 cfg: &SpannedConfig,
575 colors: &'a C,
576 dimension: &D,
577 this_height: usize,
578 row: usize,
579 line: usize,
580 totalh: Option<usize>,
581 shape: (usize, usize),
582) -> fmt::Result
583where
584 F: Write,
585 I: Iterator,
586 I::Item: AsRef<str>,
587 D: Dimension,
588 C: Colors,
589{
590 if this_height == 0 {
591 // it's possible that we dont show row but it contains an actual cell which will be
592 // rendered after all cause it's a rowspanned
593
594 let mut skip = 0;
595 for (col, cell) in iter.enumerate() {
596 if skip > 0 {
597 skip -= 1;
598 continue;
599 }
600
601 if let Some((_, _, colspan)) = buf.get(&col) {
602 skip = *colspan - 1;
603 continue;
604 }
605
606 let pos = (row, col);
607 let rowspan = cfg.get_row_span(pos).unwrap_or(1);
608 if rowspan < 2 {
609 continue;
610 }
611
612 let height = if rowspan > 1 {
613 range_height(cfg, dimension, row, row + rowspan, shape.0)
614 } else {
615 this_height
616 };
617
618 let colspan = cfg.get_column_span(pos).unwrap_or(1);
619 skip = colspan - 1;
620 let width = if colspan > 1 {
621 range_width(cfg, dimension, col, col + colspan, shape.1)
622 } else {
623 dimension.get_width(col)
624 };
625
626 let color = colors.get_color(pos);
627 let cell = Cell::new(cell, width, height, cfg, color, pos);
628
629 buf.insert(col, (cell, rowspan, colspan));
630 }
631
632 buf.retain(|_, (_, rowspan, _)| {
633 *rowspan -= 1;
634 *rowspan != 0
635 });
636
637 return Ok(());
638 }
639
640 let mut skip = 0;
641 for (col, cell) in iter.enumerate() {
642 if skip > 0 {
643 skip -= 1;
644 continue;
645 }
646
647 if let Some((_, _, colspan)) = buf.get(&col) {
648 skip = *colspan - 1;
649 continue;
650 }
651
652 let pos = (row, col);
653 let colspan = cfg.get_column_span(pos).unwrap_or(1);
654 skip = colspan - 1;
655
656 let width = if colspan > 1 {
657 range_width(cfg, dimension, col, col + colspan, shape.1)
658 } else {
659 dimension.get_width(col)
660 };
661
662 let rowspan = cfg.get_row_span(pos).unwrap_or(1);
663 let height = if rowspan > 1 {
664 range_height(cfg, dimension, row, row + rowspan, shape.0)
665 } else {
666 this_height
667 };
668
669 let color = colors.get_color(pos);
670 let cell = Cell::new(cell, width, height, cfg, color, pos);
671
672 buf.insert(col, (cell, rowspan, colspan));
673 }
674
675 for i in 0..this_height {
676 let exact_line = line + i;
677 let cell_line = i;
678
679 print_margin_left(f, cfg, exact_line, totalh)?;
680
681 for (&col, (cell, _, _)) in buf.iter_mut() {
682 print_vertical_char(f, cfg, (row, col), cell_line, this_height, shape.1)?;
683 cell.display(f)?;
684 }
685
686 print_vertical_char(f, cfg, (row, shape.1), cell_line, this_height, shape.1)?;
687
688 print_margin_right(f, cfg, exact_line, totalh)?;
689
690 if i + 1 != this_height {
691 f.write_char('\n')?;
692 }
693 }
694
695 buf.retain(|_, (_, rowspan, _)| {
696 *rowspan -= 1;
697 *rowspan != 0
698 });
699
700 Ok(())
701}
702
703fn print_horizontal_border<F: Write>(
704 f: &mut F,
705 cfg: &SpannedConfig,
706 pos: Position,
707 width: usize,
708 c: char,
709 used_color: &Option<&AnsiColor<'static>>,
710) -> fmt::Result {
711 if !cfg.is_overridden_horizontal(pos) {
712 return repeat_char(f, c, width);
713 }
714
715 for i in 0..width {
716 let c = cfg.lookup_horizontal_char(pos, i, width).unwrap_or(c);
717 match cfg.lookup_horizontal_color(pos, i, width) {
718 Some(color) => match used_color {
719 Some(clr) => {
720 clr.fmt_suffix(f)?;
721 color.fmt_prefix(f)?;
722 f.write_char(c)?;
723 color.fmt_suffix(f)?;
724 clr.fmt_prefix(f)?;
725 }
726 None => {
727 color.fmt_prefix(f)?;
728 f.write_char(c)?;
729 color.fmt_suffix(f)?;
730 }
731 },
732 _ => f.write_char(c)?,
733 }
734 }
735
736 Ok(())
737}
738
739struct Cell<T, C> {
740 lines: LinesIter<T>,
741 width: usize,
742 indent_top: usize,
743 indent_left: Option<usize>,
744 alignh: AlignmentHorizontal,
745 fmt: Formatting,
746 pad: Sides<Indent>,
747 pad_color: Sides<Option<AnsiColor<'static>>>,
748 color: Option<C>,
749 justification: (char, Option<AnsiColor<'static>>),
750}
751
752impl<T, C> Cell<T, C>
753where
754 T: AsRef<str>,
755{
756 fn new(
757 text: T,
758 width: usize,
759 height: usize,
760 cfg: &SpannedConfig,
761 color: Option<C>,
762 pos: Position,
763 ) -> Cell<T, C> {
764 let fmt = *cfg.get_formatting(pos.into());
765 let pad = cfg.get_padding(pos.into());
766 let pad_color = cfg.get_padding_color(pos.into()).clone();
767 let alignh = *cfg.get_alignment_horizontal(pos.into());
768 let alignv = *cfg.get_alignment_vertical(pos.into());
769 let justification = (
770 cfg.get_justification(pos.into()),
771 cfg.get_justification_color(pos.into()).cloned(),
772 );
773
774 let (count_lines, skip) = if fmt.vertical_trim {
775 let (len, top, _) = count_empty_lines(text.as_ref());
776 (len, top)
777 } else {
778 (count_lines(text.as_ref()), 0)
779 };
780
781 let indent_top = top_indent(&pad, alignv, count_lines, height);
782
783 let mut indent_left = None;
784 if !fmt.allow_lines_alignment {
785 let text_width = get_text_width(text.as_ref(), fmt.horizontal_trim);
786 let available = width - pad.left.size - pad.right.size;
787 indent_left = Some(calculate_indent(alignh, text_width, available).0);
788 }
789
790 let mut lines = LinesIter::new(text);
791 for _ in 0..skip {
792 let _ = lines.lines.next();
793 }
794
795 Self {
796 lines,
797 indent_left,
798 indent_top,
799 width,
800 alignh,
801 fmt,
802 pad,
803 pad_color,
804 color,
805 justification,
806 }
807 }
808}
809
810impl<T, C> Cell<T, C>
811where
812 C: Color,
813{
814 fn display<F: Write>(&mut self, f: &mut F) -> fmt::Result {
815 if self.indent_top > 0 {
816 self.indent_top -= 1;
817 print_padding_n(f, &self.pad.top, self.pad_color.top.as_ref(), self.width)?;
818 return Ok(());
819 }
820
821 let line = match self.lines.lines.next() {
822 Some(line) => line,
823 None => {
824 let color = self.pad_color.bottom.as_ref();
825 print_padding_n(f, &self.pad.bottom, color, self.width)?;
826 return Ok(());
827 }
828 };
829
830 let line = if self.fmt.horizontal_trim && !line.is_empty() {
831 string_trim(&line)
832 } else {
833 line
834 };
835
836 let line_width = string_width(&line);
837 let available_width = self.width - self.pad.left.size - self.pad.right.size;
838
839 let (left, right) = if self.fmt.allow_lines_alignment {
840 calculate_indent(self.alignh, line_width, available_width)
841 } else {
842 let left = self.indent_left.expect("must be here");
843 (left, available_width - line_width - left)
844 };
845
846 let (justification, justification_color) =
847 (self.justification.0, self.justification.1.as_ref());
848
849 print_padding(f, &self.pad.left, self.pad_color.left.as_ref())?;
850
851 print_indent(f, justification, left, justification_color)?;
852 print_text(f, &line, self.color.as_ref())?;
853 print_indent(f, justification, right, justification_color)?;
854
855 print_padding(f, &self.pad.right, self.pad_color.right.as_ref())?;
856
857 Ok(())
858 }
859}
860
861struct LinesIter<C> {
862 _cell: C,
863 /// SAFETY: IT'S NOT SAFE TO KEEP THE 'static REFERENCES AROUND AS THEY ARE NOT 'static in reality AND WILL BE DROPPED
864 _text: &'static str,
865 /// SAFETY: IT'S NOT SAFE TO KEEP THE 'static REFERENCES AROUND AS THEY ARE NOT 'static in reality AND WILL BE DROPPED
866 lines: Lines<'static>,
867}
868
869impl<C> LinesIter<C> {
870 fn new(cell: C) -> Self
871 where
872 C: AsRef<str>,
873 {
874 // We want to not allocate a String/Vec.
875 // It's currently not possible due to a lifetime issues. (It's known as self-referential struct)
876 //
877 // Here we change the lifetime of text.
878 //
879 // # Safety
880 //
881 // It must be safe because the referenced string and the references are dropped at the same time.
882 // And the referenced String is guaranteed to not be changed.
883 let text = cell.as_ref();
884 let text = unsafe {
885 std::str::from_utf8_unchecked(std::slice::from_raw_parts(text.as_ptr(), text.len()))
886 };
887
888 let lines = get_lines(text);
889
890 Self {
891 _cell: cell,
892 _text: text,
893 lines,
894 }
895 }
896}
897
898fn print_text<F: Write>(f: &mut F, text: &str, clr: Option<impl Color>) -> fmt::Result {
899 match clr {
900 Some(color: impl Color) => {
901 color.fmt_prefix(f)?;
902 f.write_str(text)?;
903 color.fmt_suffix(f)
904 }
905 None => f.write_str(text),
906 }
907}
908
909fn prepare_coloring<'a, 'b, F: Write>(
910 f: &mut F,
911 clr: Option<&'a AnsiColor<'b>>,
912 used_color: &mut Option<&'a AnsiColor<'b>>,
913) -> fmt::Result {
914 match clr {
915 Some(clr: &AnsiColor<'_>) => match used_color.as_mut() {
916 Some(used_clr: &mut &AnsiColor<'_>) => {
917 if **used_clr != *clr {
918 used_clr.fmt_suffix(f)?;
919 clr.fmt_prefix(f)?;
920 *used_clr = clr;
921 }
922 }
923 None => {
924 clr.fmt_prefix(f)?;
925 *used_color = Some(clr);
926 }
927 },
928 None => {
929 if let Some(clr: &AnsiColor<'_>) = used_color.take() {
930 clr.fmt_suffix(f)?
931 }
932 }
933 }
934
935 Ok(())
936}
937
938fn top_indent(
939 padding: &Sides<Indent>,
940 alignment: AlignmentVertical,
941 cell_height: usize,
942 available: usize,
943) -> usize {
944 let height: usize = available - padding.top.size;
945 let indent: usize = indent_from_top(alignment, available:height, real:cell_height);
946
947 indent + padding.top.size
948}
949
950fn indent_from_top(alignment: AlignmentVertical, available: usize, real: usize) -> usize {
951 match alignment {
952 AlignmentVertical::Top => 0,
953 AlignmentVertical::Bottom => available - real,
954 AlignmentVertical::Center => (available - real) / 2,
955 }
956}
957
958fn calculate_indent(
959 alignment: AlignmentHorizontal,
960 text_width: usize,
961 available: usize,
962) -> (usize, usize) {
963 let diff: usize = available - text_width;
964 match alignment {
965 AlignmentHorizontal::Left => (0, diff),
966 AlignmentHorizontal::Right => (diff, 0),
967 AlignmentHorizontal::Center => {
968 let left: usize = diff / 2;
969 let rest: usize = diff - left;
970 (left, rest)
971 }
972 }
973}
974
975fn repeat_char<F: Write>(f: &mut F, c: char, n: usize) -> fmt::Result {
976 for _ in 0..n {
977 f.write_char(c)?;
978 }
979
980 Ok(())
981}
982
983fn print_vertical_char<F: Write>(
984 f: &mut F,
985 cfg: &SpannedConfig,
986 pos: Position,
987 line: usize,
988 count_lines: usize,
989 count_columns: usize,
990) -> fmt::Result {
991 let symbol: char = match cfg.get_vertical(pos, count_columns) {
992 Some(c: char) => c,
993 None => return Ok(()),
994 };
995
996 let symbol: char = cfg
997 .is_overridden_vertical(pos)
998 .then(|| cfg.lookup_vertical_char(pos, line, count_lines))
999 .flatten()
1000 .unwrap_or(default:symbol);
1001
1002 match cfg.get_vertical_color(pos, count_columns) {
1003 Some(clr: &AnsiColor<'_>) => {
1004 clr.fmt_prefix(f)?;
1005 f.write_char(symbol)?;
1006 clr.fmt_suffix(f)?;
1007 }
1008 None => f.write_char(symbol)?,
1009 }
1010
1011 Ok(())
1012}
1013
1014fn print_margin_top<F: Write>(f: &mut F, cfg: &SpannedConfig, width: usize) -> fmt::Result {
1015 let indent: Indent = cfg.get_margin().top;
1016 let offset: Offset = cfg.get_margin_offset().top;
1017 let color: Sides>> = cfg.get_margin_color();
1018 let color: Option<&AnsiColor<'_>> = color.top.as_ref();
1019 print_indent_lines(f, &indent, &offset, color, width)
1020}
1021
1022fn print_margin_bottom<F: Write>(f: &mut F, cfg: &SpannedConfig, width: usize) -> fmt::Result {
1023 let indent: Indent = cfg.get_margin().bottom;
1024 let offset: Offset = cfg.get_margin_offset().bottom;
1025 let color: Sides>> = cfg.get_margin_color();
1026 let color: Option<&AnsiColor<'_>> = color.bottom.as_ref();
1027 print_indent_lines(f, &indent, &offset, color, width)
1028}
1029
1030fn print_margin_left<F: Write>(
1031 f: &mut F,
1032 cfg: &SpannedConfig,
1033 line: usize,
1034 height: Option<usize>,
1035) -> fmt::Result {
1036 let indent: Indent = cfg.get_margin().left;
1037 let offset: Offset = cfg.get_margin_offset().left;
1038 let color: Sides>> = cfg.get_margin_color();
1039 let color: Option<&AnsiColor<'_>> = color.left.as_ref();
1040 print_margin_vertical(f, indent, offset, color, line, height)
1041}
1042
1043fn print_margin_right<F: Write>(
1044 f: &mut F,
1045 cfg: &SpannedConfig,
1046 line: usize,
1047 height: Option<usize>,
1048) -> fmt::Result {
1049 let indent: Indent = cfg.get_margin().right;
1050 let offset: Offset = cfg.get_margin_offset().right;
1051 let color: Sides>> = cfg.get_margin_color();
1052 let color: Option<&AnsiColor<'_>> = color.right.as_ref();
1053 print_margin_vertical(f, indent, offset, color, line, height)
1054}
1055
1056fn print_margin_vertical<F: Write>(
1057 f: &mut F,
1058 indent: Indent,
1059 offset: Offset,
1060 color: Option<&AnsiColor<'_>>,
1061 line: usize,
1062 height: Option<usize>,
1063) -> fmt::Result {
1064 if indent.size == 0 {
1065 return Ok(());
1066 }
1067
1068 match offset {
1069 Offset::Begin(mut offset) => {
1070 if let Some(max) = height {
1071 offset = cmp::min(offset, max);
1072 }
1073
1074 if line >= offset {
1075 print_indent(f, indent.fill, indent.size, color)?;
1076 } else {
1077 repeat_char(f, ' ', indent.size)?;
1078 }
1079 }
1080 Offset::End(mut offset) => {
1081 if let Some(max) = height {
1082 offset = cmp::min(offset, max);
1083 let pos = max - offset;
1084
1085 if line >= pos {
1086 repeat_char(f, ' ', indent.size)?;
1087 } else {
1088 print_indent(f, indent.fill, indent.size, color)?;
1089 }
1090 } else {
1091 print_indent(f, indent.fill, indent.size, color)?;
1092 }
1093 }
1094 }
1095
1096 Ok(())
1097}
1098
1099fn print_indent_lines<F: Write>(
1100 f: &mut F,
1101 indent: &Indent,
1102 offset: &Offset,
1103 color: Option<&AnsiColor<'_>>,
1104 width: usize,
1105) -> fmt::Result {
1106 if indent.size == 0 {
1107 return Ok(());
1108 }
1109
1110 let (start_offset, end_offset) = match offset {
1111 Offset::Begin(start) => (*start, 0),
1112 Offset::End(end) => (0, *end),
1113 };
1114
1115 let start_offset = std::cmp::min(start_offset, width);
1116 let end_offset = std::cmp::min(end_offset, width);
1117 let indent_size = width - start_offset - end_offset;
1118
1119 for i in 0..indent.size {
1120 if start_offset > 0 {
1121 repeat_char(f, ' ', start_offset)?;
1122 }
1123
1124 if indent_size > 0 {
1125 print_indent(f, indent.fill, indent_size, color)?;
1126 }
1127
1128 if end_offset > 0 {
1129 repeat_char(f, ' ', end_offset)?;
1130 }
1131
1132 if i + 1 != indent.size {
1133 f.write_char('\n')?;
1134 }
1135 }
1136
1137 Ok(())
1138}
1139
1140fn print_padding<F: Write>(f: &mut F, pad: &Indent, color: Option<&AnsiColor<'_>>) -> fmt::Result {
1141 print_indent(f, c:pad.fill, n:pad.size, color)
1142}
1143
1144fn print_padding_n<F: Write>(
1145 f: &mut F,
1146 pad: &Indent,
1147 color: Option<&AnsiColor<'_>>,
1148 n: usize,
1149) -> fmt::Result {
1150 print_indent(f, c:pad.fill, n, color)
1151}
1152
1153fn print_indent<F: Write>(
1154 f: &mut F,
1155 c: char,
1156 n: usize,
1157 color: Option<&AnsiColor<'_>>,
1158) -> fmt::Result {
1159 if n == 0 {
1160 return Ok(());
1161 }
1162
1163 match color {
1164 Some(color: &AnsiColor<'_>) => {
1165 color.fmt_prefix(f)?;
1166 repeat_char(f, c, n)?;
1167 color.fmt_suffix(f)
1168 }
1169 None => repeat_char(f, c, n),
1170 }
1171}
1172
1173fn range_width(
1174 cfg: &SpannedConfig,
1175 d: impl Dimension,
1176 start: usize,
1177 end: usize,
1178 max: usize,
1179) -> usize {
1180 let count_borders: usize = count_verticals_in_range(cfg, start, end, max);
1181 let range_width: usize = (start..end).map(|col: usize| d.get_width(column:col)).sum::<usize>();
1182
1183 count_borders + range_width
1184}
1185
1186fn range_height(
1187 cfg: &SpannedConfig,
1188 d: impl Dimension,
1189 from: usize,
1190 end: usize,
1191 max: usize,
1192) -> usize {
1193 let count_borders: usize = count_horizontals_in_range(cfg, from, end, max);
1194 let range_width: usize = (from..end).map(|col: usize| d.get_height(row:col)).sum::<usize>();
1195
1196 count_borders + range_width
1197}
1198
1199fn count_horizontals_in_range(cfg: &SpannedConfig, from: usize, end: usize, max: usize) -> usize {
1200 (from + 1..end)
1201 .map(|i: usize| cfg.has_horizontal(row:i, count_rows:max) as usize)
1202 .sum()
1203}
1204
1205fn count_verticals_in_range(cfg: &SpannedConfig, start: usize, end: usize, max: usize) -> usize {
1206 (start..end)
1207 .skip(1)
1208 .map(|i: usize| cfg.has_vertical(col:i, count_columns:max) as usize)
1209 .sum()
1210}
1211
1212fn closest_visible_row(cfg: &SpannedConfig, mut pos: Position) -> Option<usize> {
1213 loop {
1214 if cfg.is_cell_visible(pos) {
1215 return Some(pos.0);
1216 }
1217
1218 if pos.0 == 0 {
1219 return None;
1220 }
1221
1222 pos.0 -= 1;
1223 }
1224}
1225
1226fn convert_count_rows(row: usize, is_last: bool) -> usize {
1227 if is_last {
1228 row + 1
1229 } else {
1230 row + 2
1231 }
1232}
1233
1234/// Trims a string.
1235fn string_trim(text: &str) -> Cow<'_, str> {
1236 #[cfg(feature = "color")]
1237 {
1238 ansi_str::AnsiStr::ansi_trim(text)
1239 }
1240
1241 #[cfg(not(feature = "color"))]
1242 {
1243 text.trim().into()
1244 }
1245}
1246
1247fn total_width<D: Dimension>(cfg: &SpannedConfig, dimension: &D, count_columns: usize) -> usize {
1248 (0..count_columns)
1249 .map(|i: usize| dimension.get_width(column:i))
1250 .sum::<usize>()
1251 + cfg.count_vertical(count_columns)
1252}
1253
1254fn total_height<D: Dimension>(cfg: &SpannedConfig, dimension: &D, count_rows: usize) -> usize {
1255 (0..count_rows)
1256 .map(|i: usize| dimension.get_height(row:i))
1257 .sum::<usize>()
1258 + cfg.count_horizontal(count_rows)
1259}
1260
1261fn count_empty_lines(cell: &str) -> (usize, usize, usize) {
1262 let mut len = 0;
1263 let mut top = 0;
1264 let mut bottom = 0;
1265 let mut top_check = true;
1266
1267 for line in get_lines(cell) {
1268 let is_empty = line.trim().is_empty();
1269 if top_check {
1270 if is_empty {
1271 top += 1;
1272 } else {
1273 len = 1;
1274 top_check = false;
1275 }
1276
1277 continue;
1278 }
1279
1280 if is_empty {
1281 bottom += 1;
1282 } else {
1283 len += bottom + 1;
1284 bottom = 0;
1285 }
1286 }
1287
1288 (len, top, bottom)
1289}
1290
1291fn get_text_width(text: &str, trim: bool) -> usize {
1292 if trim {
1293 get_lines(text)
1294 .map(|line| string_width(line.trim()))
1295 .max()
1296 .unwrap_or(default:0)
1297 } else {
1298 string_width_multiline(text)
1299 }
1300}
1301
1302#[cfg(test)]
1303mod tests {
1304 // use crate::util::string_width;
1305
1306 use super::*;
1307
1308 // #[test]
1309 // fn horizontal_alignment_test() {
1310 // use std::fmt;
1311
1312 // struct F<'a>(&'a str, AlignmentHorizontal, usize);
1313
1314 // impl fmt::Display for F<'_> {
1315 // fn fmt(&self, f: &mut impl fmt::Write) -> fmt::Result {
1316 // let (left, right) = calculate_indent(self.1, string_width(self.0), self.2);
1317 // print_text_formatted(f, &self.0, 4, Option::<&AnsiColor<'_>>::None)
1318 // }
1319 // }
1320
1321 // assert_eq!(F("AAA", AlignmentHorizontal::Right, 4).to_string(), " AAA");
1322 // assert_eq!(F("AAA", AlignmentHorizontal::Left, 4).to_string(), "AAA ");
1323 // assert_eq!(F("AAA", AlignmentHorizontal::Center, 4).to_string(), "AAA ");
1324 // assert_eq!(F("🎩", AlignmentHorizontal::Center, 4).to_string(), " 🎩 ");
1325 // assert_eq!(F("🎩", AlignmentHorizontal::Center, 3).to_string(), "🎩 ");
1326
1327 // #[cfg(feature = "color")]
1328 // {
1329 // use owo_colors::OwoColorize;
1330 // let text = "Colored Text".red().to_string();
1331 // assert_eq!(
1332 // F(&text, AlignmentHorizontal::Center, 15).to_string(),
1333 // format!(" {} ", text)
1334 // );
1335 // }
1336 // }
1337
1338 #[test]
1339 fn vertical_alignment_test() {
1340 use AlignmentVertical::*;
1341
1342 assert_eq!(indent_from_top(Bottom, 1, 1), 0);
1343 assert_eq!(indent_from_top(Top, 1, 1), 0);
1344 assert_eq!(indent_from_top(Center, 1, 1), 0);
1345 assert_eq!(indent_from_top(Bottom, 3, 1), 2);
1346 assert_eq!(indent_from_top(Top, 3, 1), 0);
1347 assert_eq!(indent_from_top(Center, 3, 1), 1);
1348 assert_eq!(indent_from_top(Center, 4, 1), 1);
1349 }
1350
1351 #[test]
1352 fn count_empty_lines_test() {
1353 assert_eq!(count_empty_lines("\n\nsome text\n\n\n"), (1, 2, 3));
1354 assert_eq!(count_empty_lines("\n\nsome\ntext\n\n\n"), (2, 2, 3));
1355 assert_eq!(count_empty_lines("\n\nsome\nsome\ntext\n\n\n"), (3, 2, 3));
1356 assert_eq!(count_empty_lines("\n\n\n\n"), (0, 5, 0));
1357 }
1358}
1359