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