1 | //! The module contains a [`Grid`] structure. |
2 | |
3 | use std::{ |
4 | borrow::{Borrow, Cow}, |
5 | cmp, |
6 | collections::BTreeMap, |
7 | fmt::{self, Write}, |
8 | }; |
9 | |
10 | use 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 | |
19 | use crate::config::spanned::{Formatting, Offset, SpannedConfig}; |
20 | |
21 | /// Grid provides a set of methods for building a text-based table. |
22 | #[derive (Debug, Clone)] |
23 | pub struct Grid<R, D, G, C> { |
24 | records: R, |
25 | config: G, |
26 | dimension: D, |
27 | colors: C, |
28 | } |
29 | |
30 | impl<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 | |
42 | impl<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 | |
77 | fn 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 | |
94 | fn 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 | |
194 | fn 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)] |
200 | fn 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)] |
216 | fn 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 |
229 | where |
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)] |
242 | fn 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 |
253 | where |
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 | |
277 | fn 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)] |
320 | fn 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 | |
352 | fn 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 | |
376 | fn 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 | |
413 | fn 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 | |
498 | fn 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 | |
552 | fn 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)] |
570 | fn 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 |
583 | where |
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 | |
703 | fn 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 | |
739 | struct 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 | |
752 | impl<T, C> Cell<T, C> |
753 | where |
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 | |
810 | impl<T, C> Cell<T, C> |
811 | where |
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 | |
861 | struct 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 | |
869 | impl<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 | |
898 | fn 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 | |
909 | fn 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 | |
938 | fn 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 | |
950 | fn 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 | |
958 | fn 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 | |
975 | fn 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 | |
983 | fn 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 | |
1014 | fn 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 | |
1022 | fn 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 | |
1030 | fn 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 | |
1043 | fn 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 | |
1056 | fn 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 | |
1099 | fn 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 | |
1140 | fn 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 | |
1144 | fn 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 | |
1153 | fn 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 | |
1173 | fn 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 | |
1186 | fn 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 | |
1199 | fn 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 | |
1205 | fn 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 | |
1212 | fn 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 | |
1226 | fn 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. |
1235 | fn 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 | |
1247 | fn 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 | |
1254 | fn 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 | |
1261 | fn 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 | |
1291 | fn 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)] |
1303 | mod 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 | |