1 | //! The module contains a [`PeekableGrid`] structure. |
2 | |
3 | use core::borrow::Borrow; |
4 | use std::{ |
5 | borrow::Cow, |
6 | cmp, |
7 | fmt::{self, Write}, |
8 | }; |
9 | |
10 | use crate::{ |
11 | color::{AnsiColor, Color}, |
12 | colors::Colors, |
13 | config::spanned::{Formatting, Offset, SpannedConfig}, |
14 | config::{AlignmentHorizontal, AlignmentVertical, Indent, Position, Sides}, |
15 | dimension::Dimension, |
16 | records::{ExactRecords, PeekableRecords, Records}, |
17 | util::string::string_width, |
18 | }; |
19 | |
20 | /// Grid provides a set of methods for building a text-based table. |
21 | #[derive (Debug, Clone)] |
22 | pub struct PeekableGrid<R, G, D, C> { |
23 | records: R, |
24 | config: G, |
25 | dimension: D, |
26 | colors: C, |
27 | } |
28 | |
29 | impl<R, G, D, C> PeekableGrid<R, G, D, C> { |
30 | /// The new method creates a grid instance with default styles. |
31 | pub fn new(records: R, config: G, dimension: D, colors: C) -> Self { |
32 | PeekableGrid { |
33 | records, |
34 | config, |
35 | dimension, |
36 | colors, |
37 | } |
38 | } |
39 | } |
40 | |
41 | impl<R, G, D, C> PeekableGrid<R, G, D, C> { |
42 | /// Builds a table. |
43 | pub fn build<F>(self, mut f: F) -> fmt::Result |
44 | where |
45 | R: Records + PeekableRecords + ExactRecords, |
46 | D: Dimension, |
47 | C: Colors, |
48 | G: Borrow<SpannedConfig>, |
49 | F: Write, |
50 | { |
51 | if self.records.count_columns() == 0 || self.records.hint_count_rows() == Some(0) { |
52 | return Ok(()); |
53 | } |
54 | |
55 | let config = self.config.borrow(); |
56 | print_grid(&mut f, self.records, config, &self.dimension, &self.colors) |
57 | } |
58 | |
59 | /// Builds a table into string. |
60 | /// |
61 | /// Notice that it consumes self. |
62 | #[allow (clippy::inherent_to_string)] |
63 | pub fn to_string(self) -> String |
64 | where |
65 | R: Records + PeekableRecords + ExactRecords, |
66 | D: Dimension, |
67 | G: Borrow<SpannedConfig>, |
68 | C: Colors, |
69 | { |
70 | let mut buf = String::new(); |
71 | self.build(&mut buf).expect("It's guaranteed to never happen otherwise it's considered an stdlib error or impl error" ); |
72 | buf |
73 | } |
74 | } |
75 | |
76 | fn print_grid<F: Write, R: Records + PeekableRecords + ExactRecords, D: Dimension, C: Colors>( |
77 | f: &mut F, |
78 | records: R, |
79 | cfg: &SpannedConfig, |
80 | dimension: &D, |
81 | colors: &C, |
82 | ) -> fmt::Result { |
83 | if cfg.has_column_spans() || cfg.has_row_spans() { |
84 | build_grid_spanned(f, &records, cfg, dims:dimension, colors) |
85 | } else { |
86 | build_grid(f, &records, cfg, dimension, colors) |
87 | } |
88 | } |
89 | |
90 | fn build_grid<F: Write, R: Records + PeekableRecords + ExactRecords, D: Dimension, C: Colors>( |
91 | f: &mut F, |
92 | records: &R, |
93 | cfg: &SpannedConfig, |
94 | dimension: &D, |
95 | colors: &C, |
96 | ) -> fmt::Result { |
97 | let shape = (records.count_rows(), records.count_columns()); |
98 | |
99 | let total_width = total_width(cfg, dimension, shape.1); |
100 | let total_width_with_margin = |
101 | total_width + cfg.get_margin().left.size + cfg.get_margin().right.size; |
102 | |
103 | let total_height = total_height(cfg, dimension, shape.0); |
104 | |
105 | if cfg.get_margin().top.size > 0 { |
106 | print_margin_top(f, cfg, total_width_with_margin)?; |
107 | f.write_char(' \n' )?; |
108 | } |
109 | |
110 | let mut table_line = 0; |
111 | let mut prev_empty_horizontal = false; |
112 | for row in 0..shape.0 { |
113 | let height = dimension.get_height(row); |
114 | |
115 | if cfg.has_horizontal(row, shape.0) { |
116 | if prev_empty_horizontal { |
117 | f.write_char(' \n' )?; |
118 | } |
119 | |
120 | print_margin_left(f, cfg, table_line, total_height)?; |
121 | print_split_line(f, cfg, dimension, row, shape)?; |
122 | print_margin_right(f, cfg, table_line, total_height)?; |
123 | |
124 | if height > 0 { |
125 | f.write_char(' \n' )?; |
126 | prev_empty_horizontal = false; |
127 | } else { |
128 | prev_empty_horizontal = true; |
129 | } |
130 | |
131 | table_line += 1; |
132 | } else if height > 0 && prev_empty_horizontal { |
133 | f.write_char(' \n' )?; |
134 | prev_empty_horizontal = false; |
135 | } |
136 | |
137 | for i in 0..height { |
138 | print_margin_left(f, cfg, table_line, total_height)?; |
139 | |
140 | for col in 0..records.count_columns() { |
141 | print_vertical_char(f, cfg, (row, col), i, height, shape.1)?; |
142 | |
143 | let width = dimension.get_width(col); |
144 | print_cell_line(f, records, cfg, colors, width, height, (row, col), i)?; |
145 | |
146 | let is_last_column = col + 1 == records.count_columns(); |
147 | if is_last_column { |
148 | print_vertical_char(f, cfg, (row, col + 1), i, height, shape.1)?; |
149 | } |
150 | } |
151 | |
152 | print_margin_right(f, cfg, table_line, total_height)?; |
153 | |
154 | let is_last_line = i + 1 == height; |
155 | let is_last_row = row + 1 == records.count_rows(); |
156 | if !(is_last_line && is_last_row) { |
157 | f.write_char(' \n' )?; |
158 | } |
159 | |
160 | table_line += 1; |
161 | } |
162 | } |
163 | |
164 | if cfg.has_horizontal(shape.0, shape.0) { |
165 | f.write_char(' \n' )?; |
166 | print_margin_left(f, cfg, table_line, total_height)?; |
167 | print_split_line(f, cfg, dimension, records.count_rows(), shape)?; |
168 | print_margin_right(f, cfg, table_line, total_height)?; |
169 | } |
170 | |
171 | if cfg.get_margin().bottom.size > 0 { |
172 | f.write_char(' \n' )?; |
173 | print_margin_bottom(f, cfg, total_width_with_margin)?; |
174 | } |
175 | |
176 | Ok(()) |
177 | } |
178 | |
179 | fn print_split_line<F: Write, D: Dimension>( |
180 | f: &mut F, |
181 | cfg: &SpannedConfig, |
182 | dimension: &D, |
183 | row: usize, |
184 | shape: (usize, usize), |
185 | ) -> fmt::Result { |
186 | let mut used_color = None; |
187 | print_vertical_intersection(f, cfg, (row, 0), shape, &mut used_color)?; |
188 | |
189 | for col in 0..shape.1 { |
190 | let width = dimension.get_width(col); |
191 | |
192 | // general case |
193 | if width > 0 { |
194 | let pos = (row, col); |
195 | let main = cfg.get_horizontal(pos, shape.0); |
196 | match main { |
197 | Some(c) => { |
198 | let clr = cfg.get_horizontal_color(pos, shape.0); |
199 | prepare_coloring(f, clr, &mut used_color)?; |
200 | print_horizontal_border(f, cfg, pos, width, c, &used_color)?; |
201 | } |
202 | None => repeat_char(f, ' ' , width)?, |
203 | } |
204 | } |
205 | |
206 | print_vertical_intersection(f, cfg, (row, col + 1), shape, &mut used_color)?; |
207 | } |
208 | |
209 | if let Some(clr) = used_color.take() { |
210 | clr.fmt_suffix(f)?; |
211 | } |
212 | |
213 | Ok(()) |
214 | } |
215 | |
216 | fn print_vertical_intersection<'a, F: fmt::Write>( |
217 | f: &mut F, |
218 | cfg: &'a SpannedConfig, |
219 | pos: Position, |
220 | shape: (usize, usize), |
221 | used_color: &mut Option<&'a AnsiColor<'static>>, |
222 | ) -> fmt::Result { |
223 | match cfg.get_intersection(pos, shape) { |
224 | Some(c: char) => { |
225 | let clr: Option<&AnsiColor<'_>> = cfg.get_intersection_color(pos, shape); |
226 | prepare_coloring(f, clr, used_color)?; |
227 | f.write_char(c) |
228 | } |
229 | None => Ok(()), |
230 | } |
231 | } |
232 | |
233 | fn prepare_coloring<'a, 'b, F: Write>( |
234 | f: &mut F, |
235 | clr: Option<&'a AnsiColor<'b>>, |
236 | used_color: &mut Option<&'a AnsiColor<'b>>, |
237 | ) -> fmt::Result { |
238 | match clr { |
239 | Some(clr: &AnsiColor<'_>) => match used_color.as_mut() { |
240 | Some(used_clr: &mut &AnsiColor<'_>) => { |
241 | if **used_clr != *clr { |
242 | used_clr.fmt_suffix(f)?; |
243 | clr.fmt_prefix(f)?; |
244 | *used_clr = clr; |
245 | } |
246 | } |
247 | None => { |
248 | clr.fmt_prefix(f)?; |
249 | *used_color = Some(clr); |
250 | } |
251 | }, |
252 | None => { |
253 | if let Some(clr: &AnsiColor<'_>) = used_color.take() { |
254 | clr.fmt_suffix(f)? |
255 | } |
256 | } |
257 | } |
258 | |
259 | Ok(()) |
260 | } |
261 | |
262 | fn print_vertical_char<F: Write>( |
263 | f: &mut F, |
264 | cfg: &SpannedConfig, |
265 | pos: Position, |
266 | line: usize, |
267 | count_lines: usize, |
268 | count_columns: usize, |
269 | ) -> fmt::Result { |
270 | let symbol: char = match cfg.get_vertical(pos, count_columns) { |
271 | Some(c: char) => c, |
272 | None => return Ok(()), |
273 | }; |
274 | |
275 | let symbol: char = cfg |
276 | .is_overridden_vertical(pos) |
277 | .then(|| cfg.lookup_vertical_char(pos, line, count_lines)) |
278 | .flatten() |
279 | .unwrap_or(default:symbol); |
280 | |
281 | match cfg.get_vertical_color(pos, count_columns) { |
282 | Some(clr: &AnsiColor<'_>) => { |
283 | clr.fmt_prefix(f)?; |
284 | f.write_char(symbol)?; |
285 | clr.fmt_suffix(f)?; |
286 | } |
287 | None => f.write_char(symbol)?, |
288 | } |
289 | |
290 | Ok(()) |
291 | } |
292 | |
293 | fn build_grid_spanned< |
294 | F: Write, |
295 | R: Records + PeekableRecords + ExactRecords, |
296 | D: Dimension, |
297 | C: Colors, |
298 | >( |
299 | f: &mut F, |
300 | records: &R, |
301 | cfg: &SpannedConfig, |
302 | dims: &D, |
303 | colors: &C, |
304 | ) -> fmt::Result { |
305 | let shape = (records.count_rows(), records.count_columns()); |
306 | |
307 | let total_width = total_width(cfg, dims, shape.1); |
308 | let total_width_with_margin = |
309 | total_width + cfg.get_margin().left.size + cfg.get_margin().right.size; |
310 | |
311 | let total_height = total_height(cfg, dims, shape.0); |
312 | |
313 | if cfg.get_margin().top.size > 0 { |
314 | print_margin_top(f, cfg, total_width_with_margin)?; |
315 | f.write_char(' \n' )?; |
316 | } |
317 | |
318 | let mut table_line = 0; |
319 | let mut prev_empty_horizontal = false; |
320 | for row in 0..records.count_rows() { |
321 | let count_lines = dims.get_height(row); |
322 | |
323 | if cfg.has_horizontal(row, shape.0) { |
324 | if prev_empty_horizontal { |
325 | f.write_char(' \n' )?; |
326 | } |
327 | |
328 | print_margin_left(f, cfg, table_line, total_height)?; |
329 | print_split_line_spanned(f, records, cfg, dims, colors, row, shape)?; |
330 | print_margin_right(f, cfg, table_line, total_height)?; |
331 | |
332 | if count_lines > 0 { |
333 | f.write_char(' \n' )?; |
334 | prev_empty_horizontal = false; |
335 | } else { |
336 | prev_empty_horizontal = true; |
337 | } |
338 | |
339 | table_line += 1; |
340 | } else if count_lines > 0 && prev_empty_horizontal { |
341 | f.write_char(' \n' )?; |
342 | prev_empty_horizontal = false; |
343 | } |
344 | |
345 | for i in 0..count_lines { |
346 | print_margin_left(f, cfg, table_line, total_height)?; |
347 | |
348 | for col in 0..records.count_columns() { |
349 | if cfg.is_cell_covered_by_both_spans((row, col)) { |
350 | continue; |
351 | } |
352 | |
353 | if cfg.is_cell_covered_by_column_span((row, col)) { |
354 | let is_last_column = col + 1 == records.count_columns(); |
355 | if is_last_column { |
356 | print_vertical_char(f, cfg, (row, col + 1), i, count_lines, shape.1)?; |
357 | } |
358 | |
359 | continue; |
360 | } |
361 | |
362 | print_vertical_char(f, cfg, (row, col), i, count_lines, shape.1)?; |
363 | |
364 | if cfg.is_cell_covered_by_row_span((row, col)) { |
365 | // means it's part of other a spanned cell |
366 | // so. we just need to use line from other cell. |
367 | let original_row = closest_visible_row(cfg, (row, col)).unwrap(); |
368 | |
369 | // considering that the content will be printed instead horizontal lines so we can skip some lines. |
370 | let mut skip_lines = (original_row..row) |
371 | .map(|i| dims.get_height(i)) |
372 | .sum::<usize>(); |
373 | |
374 | skip_lines += (original_row + 1..=row) |
375 | .map(|row| cfg.has_horizontal(row, shape.0) as usize) |
376 | .sum::<usize>(); |
377 | |
378 | let line = i + skip_lines; |
379 | let pos = (original_row, col); |
380 | |
381 | let width = get_cell_width(cfg, dims, pos, shape.1); |
382 | let height = get_cell_height(cfg, dims, pos, shape.0); |
383 | |
384 | print_cell_line(f, records, cfg, colors, width, height, pos, line)?; |
385 | } else { |
386 | let width = get_cell_width(cfg, dims, (row, col), shape.1); |
387 | let height = get_cell_height(cfg, dims, (row, col), shape.0); |
388 | print_cell_line(f, records, cfg, colors, width, height, (row, col), i)?; |
389 | } |
390 | |
391 | let is_last_column = col + 1 == records.count_columns(); |
392 | if is_last_column { |
393 | print_vertical_char(f, cfg, (row, col + 1), i, count_lines, shape.1)?; |
394 | } |
395 | } |
396 | |
397 | print_margin_right(f, cfg, table_line, total_height)?; |
398 | |
399 | let is_last_line = i + 1 == count_lines; |
400 | let is_last_row = row + 1 == records.count_rows(); |
401 | if !(is_last_line && is_last_row) { |
402 | f.write_char(' \n' )?; |
403 | } |
404 | |
405 | table_line += 1; |
406 | } |
407 | } |
408 | |
409 | if cfg.has_horizontal(shape.0, shape.0) { |
410 | f.write_char(' \n' )?; |
411 | print_margin_left(f, cfg, table_line, total_height)?; |
412 | print_split_line(f, cfg, dims, records.count_rows(), shape)?; |
413 | print_margin_right(f, cfg, table_line, total_height)?; |
414 | } |
415 | |
416 | if cfg.get_margin().bottom.size > 0 { |
417 | f.write_char(' \n' )?; |
418 | print_margin_bottom(f, cfg, total_width_with_margin)?; |
419 | } |
420 | |
421 | Ok(()) |
422 | } |
423 | |
424 | fn print_split_line_spanned< |
425 | F: Write, |
426 | R: Records + ExactRecords + PeekableRecords, |
427 | D: Dimension, |
428 | C: Colors, |
429 | >( |
430 | f: &mut F, |
431 | records: &R, |
432 | cfg: &SpannedConfig, |
433 | dims: &D, |
434 | colors: &C, |
435 | row: usize, |
436 | shape: (usize, usize), |
437 | ) -> fmt::Result { |
438 | let mut used_color = None; |
439 | print_vertical_intersection(f, cfg, (row, 0), shape, &mut used_color)?; |
440 | |
441 | for col in 0..shape.1 { |
442 | let pos = (row, col); |
443 | if cfg.is_cell_covered_by_both_spans(pos) { |
444 | continue; |
445 | } |
446 | |
447 | if cfg.is_cell_covered_by_row_span(pos) { |
448 | // means it's part of other a spanned cell |
449 | // so. we just need to use line from other cell. |
450 | |
451 | let original_row = closest_visible_row(cfg, (row, col)).unwrap(); |
452 | |
453 | // considering that the content will be printed instead horizontal lines so we can skip some lines. |
454 | let mut skip_lines = (original_row..row) |
455 | .map(|i| dims.get_height(i)) |
456 | .sum::<usize>(); |
457 | |
458 | // skip horizontal lines |
459 | if row > 0 { |
460 | skip_lines += (original_row..row - 1) |
461 | .map(|row| cfg.has_horizontal(row + 1, shape.0) as usize) |
462 | .sum::<usize>(); |
463 | } |
464 | |
465 | let pos = (original_row, col); |
466 | let height = get_cell_height(cfg, dims, pos, shape.0); |
467 | let width = get_cell_width(cfg, dims, pos, shape.1); |
468 | let line = skip_lines; |
469 | |
470 | print_cell_line(f, records, cfg, colors, width, height, pos, line)?; |
471 | |
472 | // We need to use a correct right split char. |
473 | let mut col = col; |
474 | if let Some(span) = cfg.get_column_span(pos) { |
475 | col += span - 1; |
476 | } |
477 | |
478 | print_vertical_intersection(f, cfg, (row, col + 1), shape, &mut used_color)?; |
479 | |
480 | continue; |
481 | } |
482 | |
483 | let width = dims.get_width(col); |
484 | if width > 0 { |
485 | // general case |
486 | let main = cfg.get_horizontal(pos, shape.0); |
487 | match main { |
488 | Some(c) => { |
489 | let clr = cfg.get_horizontal_color(pos, shape.0); |
490 | prepare_coloring(f, clr, &mut used_color)?; |
491 | print_horizontal_border(f, cfg, pos, width, c, &used_color)?; |
492 | } |
493 | None => repeat_char(f, ' ' , width)?, |
494 | } |
495 | } |
496 | |
497 | print_vertical_intersection(f, cfg, (row, col + 1), shape, &mut used_color)?; |
498 | } |
499 | |
500 | if let Some(clr) = used_color { |
501 | clr.fmt_suffix(f)?; |
502 | } |
503 | |
504 | Ok(()) |
505 | } |
506 | |
507 | fn print_horizontal_border<F: Write>( |
508 | f: &mut F, |
509 | cfg: &SpannedConfig, |
510 | pos: Position, |
511 | width: usize, |
512 | c: char, |
513 | used_color: &Option<&AnsiColor<'static>>, |
514 | ) -> fmt::Result { |
515 | if !cfg.is_overridden_horizontal(pos) { |
516 | return repeat_char(f, c, width); |
517 | } |
518 | |
519 | for i in 0..width { |
520 | let c = cfg.lookup_horizontal_char(pos, i, width).unwrap_or(c); |
521 | match cfg.lookup_horizontal_color(pos, i, width) { |
522 | Some(color) => match used_color { |
523 | Some(clr) => { |
524 | clr.fmt_suffix(f)?; |
525 | color.fmt_prefix(f)?; |
526 | f.write_char(c)?; |
527 | color.fmt_suffix(f)?; |
528 | clr.fmt_prefix(f)?; |
529 | } |
530 | None => { |
531 | color.fmt_prefix(f)?; |
532 | f.write_char(c)?; |
533 | color.fmt_suffix(f)?; |
534 | } |
535 | }, |
536 | _ => f.write_char(c)?, |
537 | } |
538 | } |
539 | |
540 | Ok(()) |
541 | } |
542 | |
543 | #[allow (clippy::too_many_arguments)] |
544 | fn print_cell_line<F: Write, R: Records + PeekableRecords + ExactRecords, C: Colors>( |
545 | f: &mut F, |
546 | records: &R, |
547 | cfg: &SpannedConfig, |
548 | colors: &C, |
549 | width: usize, |
550 | height: usize, |
551 | pos: Position, |
552 | line: usize, |
553 | ) -> fmt::Result { |
554 | let entity = pos.into(); |
555 | |
556 | let mut cell_height = records.count_lines(pos); |
557 | let formatting = *cfg.get_formatting(entity); |
558 | if formatting.vertical_trim { |
559 | cell_height -= |
560 | count_empty_lines_at_start(records, pos) + count_empty_lines_at_end(records, pos); |
561 | } |
562 | |
563 | if cell_height > height { |
564 | // it may happen if the height estimation decide so |
565 | cell_height = height; |
566 | } |
567 | |
568 | let pad = cfg.get_padding(entity); |
569 | let pad_color = cfg.get_padding_color(entity); |
570 | let alignment = cfg.get_alignment_vertical(entity); |
571 | let indent = top_indent(&pad, *alignment, cell_height, height); |
572 | if indent > line { |
573 | return print_indent(f, pad.top.fill, width, pad_color.top.as_ref()); |
574 | } |
575 | |
576 | let mut index = line - indent; |
577 | let cell_has_this_line = cell_height > index; |
578 | if !cell_has_this_line { |
579 | // happens when other cells have bigger height |
580 | return print_indent(f, pad.bottom.fill, width, pad_color.bottom.as_ref()); |
581 | } |
582 | |
583 | if formatting.vertical_trim { |
584 | let empty_lines = count_empty_lines_at_start(records, pos); |
585 | index += empty_lines; |
586 | |
587 | if index > records.count_lines(pos) { |
588 | return print_indent(f, pad.top.fill, width, pad_color.top.as_ref()); |
589 | } |
590 | } |
591 | |
592 | print_indent(f, pad.left.fill, pad.left.size, pad_color.left.as_ref())?; |
593 | |
594 | let width = width - pad.left.size - pad.right.size; |
595 | let alignment = *cfg.get_alignment_horizontal(entity); |
596 | let justification = ( |
597 | cfg.get_justification(entity), |
598 | cfg.get_justification_color(entity), |
599 | ); |
600 | let color = colors.get_color(pos); |
601 | print_line( |
602 | f, |
603 | records, |
604 | pos, |
605 | index, |
606 | alignment, |
607 | formatting, |
608 | color, |
609 | justification, |
610 | width, |
611 | )?; |
612 | |
613 | print_indent(f, pad.right.fill, pad.right.size, pad_color.right.as_ref())?; |
614 | |
615 | Ok(()) |
616 | } |
617 | |
618 | #[allow (clippy::too_many_arguments)] |
619 | fn print_line<F: Write, R: Records + PeekableRecords, C: Color>( |
620 | f: &mut F, |
621 | records: &R, |
622 | pos: Position, |
623 | index: usize, |
624 | alignment: AlignmentHorizontal, |
625 | formatting: Formatting, |
626 | color: Option<C>, |
627 | justification: (char, Option<&AnsiColor<'_>>), |
628 | available: usize, |
629 | ) -> fmt::Result { |
630 | let line = records.get_line(pos, index); |
631 | let (line, line_width) = if formatting.horizontal_trim { |
632 | let line = string_trim(line); |
633 | let width = string_width(&line); |
634 | (line, width) |
635 | } else { |
636 | let width = records.get_line_width(pos, index); |
637 | (Cow::Borrowed(line), width) |
638 | }; |
639 | |
640 | if formatting.allow_lines_alignment { |
641 | let (left, right) = calculate_indent(alignment, line_width, available); |
642 | return print_text_with_pad(f, &line, color, justification, left, right); |
643 | } |
644 | |
645 | let cell_width = if formatting.horizontal_trim { |
646 | (0..records.count_lines(pos)) |
647 | .map(|i| records.get_line(pos, i)) |
648 | .map(|line| string_width(line.trim())) |
649 | .max() |
650 | .unwrap_or_default() |
651 | } else { |
652 | records.get_width(pos) |
653 | }; |
654 | |
655 | let (left, right) = calculate_indent(alignment, cell_width, available); |
656 | print_text_with_pad(f, &line, color, justification, left, right)?; |
657 | |
658 | // todo: remove me |
659 | let rest_width = cell_width - line_width; |
660 | repeat_char(f, ' ' , rest_width)?; |
661 | |
662 | Ok(()) |
663 | } |
664 | |
665 | fn print_text_with_pad<F: Write, C: Color>( |
666 | f: &mut F, |
667 | text: &str, |
668 | color: Option<C>, |
669 | justification: (char, Option<&AnsiColor<'_>>), |
670 | left: usize, |
671 | right: usize, |
672 | ) -> fmt::Result { |
673 | print_indent(f, c:justification.0, n:left, color:justification.1)?; |
674 | print_text(f, text, clr:color)?; |
675 | print_indent(f, c:justification.0, n:right, color:justification.1)?; |
676 | Ok(()) |
677 | } |
678 | |
679 | fn print_text<F: Write, C: Color>(f: &mut F, text: &str, clr: Option<C>) -> fmt::Result { |
680 | match clr { |
681 | Some(color: C) => { |
682 | color.fmt_prefix(f)?; |
683 | f.write_str(text)?; |
684 | color.fmt_suffix(f) |
685 | } |
686 | None => f.write_str(text), |
687 | } |
688 | } |
689 | |
690 | fn top_indent( |
691 | pad: &Sides<Indent>, |
692 | alignment: AlignmentVertical, |
693 | cell_height: usize, |
694 | available: usize, |
695 | ) -> usize { |
696 | let height: usize = available - pad.top.size; |
697 | let indent: usize = indent_from_top(alignment, available:height, real:cell_height); |
698 | |
699 | indent + pad.top.size |
700 | } |
701 | |
702 | fn indent_from_top(alignment: AlignmentVertical, available: usize, real: usize) -> usize { |
703 | match alignment { |
704 | AlignmentVertical::Top => 0, |
705 | AlignmentVertical::Bottom => available - real, |
706 | AlignmentVertical::Center => (available - real) / 2, |
707 | } |
708 | } |
709 | |
710 | fn calculate_indent( |
711 | alignment: AlignmentHorizontal, |
712 | text_width: usize, |
713 | available: usize, |
714 | ) -> (usize, usize) { |
715 | let diff: usize = available - text_width; |
716 | match alignment { |
717 | AlignmentHorizontal::Left => (0, diff), |
718 | AlignmentHorizontal::Right => (diff, 0), |
719 | AlignmentHorizontal::Center => { |
720 | let left: usize = diff / 2; |
721 | let rest: usize = diff - left; |
722 | (left, rest) |
723 | } |
724 | } |
725 | } |
726 | |
727 | fn repeat_char<F: Write>(f: &mut F, c: char, n: usize) -> fmt::Result { |
728 | for _ in 0..n { |
729 | f.write_char(c)?; |
730 | } |
731 | |
732 | Ok(()) |
733 | } |
734 | |
735 | fn count_empty_lines_at_end<R>(records: &R, pos: Position) -> usize |
736 | where |
737 | R: Records + PeekableRecords, |
738 | { |
739 | (0..records.count_lines(pos)) |
740 | .map(|i: usize| records.get_line(pos, line:i)) |
741 | .rev() |
742 | .take_while(|l: &{unknown}| l.trim().is_empty()) |
743 | .count() |
744 | } |
745 | |
746 | fn count_empty_lines_at_start<R>(records: &R, pos: Position) -> usize |
747 | where |
748 | R: Records + PeekableRecords, |
749 | { |
750 | (0..records.count_lines(pos)) |
751 | .map(|i: usize| records.get_line(pos, line:i)) |
752 | .take_while(|s: &{unknown}| s.trim().is_empty()) |
753 | .count() |
754 | } |
755 | |
756 | fn total_width<D: Dimension>(cfg: &SpannedConfig, dimension: &D, count_columns: usize) -> usize { |
757 | (0..count_columns) |
758 | .map(|i: usize| dimension.get_width(column:i)) |
759 | .sum::<usize>() |
760 | + cfg.count_vertical(count_columns) |
761 | } |
762 | |
763 | fn total_height<D: Dimension>(cfg: &SpannedConfig, dimension: &D, count_rows: usize) -> usize { |
764 | (0..count_rows) |
765 | .map(|i: usize| dimension.get_height(row:i)) |
766 | .sum::<usize>() |
767 | + cfg.count_horizontal(count_rows) |
768 | } |
769 | |
770 | fn print_margin_top<F: Write>(f: &mut F, cfg: &SpannedConfig, width: usize) -> fmt::Result { |
771 | let indent: Indent = cfg.get_margin().top; |
772 | let offset: Offset = cfg.get_margin_offset().top; |
773 | let color: Sides = cfg.get_margin_color(); |
774 | let color: Option<&AnsiColor<'_>> = color.top.as_ref(); |
775 | print_indent_lines(f, &indent, &offset, color, width) |
776 | } |
777 | |
778 | fn print_margin_bottom<F: Write>(f: &mut F, cfg: &SpannedConfig, width: usize) -> fmt::Result { |
779 | let indent: Indent = cfg.get_margin().bottom; |
780 | let offset: Offset = cfg.get_margin_offset().bottom; |
781 | let color: Sides = cfg.get_margin_color(); |
782 | let color: Option<&AnsiColor<'_>> = color.bottom.as_ref(); |
783 | print_indent_lines(f, &indent, &offset, color, width) |
784 | } |
785 | |
786 | fn print_margin_left<F: Write>( |
787 | f: &mut F, |
788 | cfg: &SpannedConfig, |
789 | line: usize, |
790 | height: usize, |
791 | ) -> fmt::Result { |
792 | let indent: Indent = cfg.get_margin().left; |
793 | let offset: Offset = cfg.get_margin_offset().left; |
794 | let color: Sides = cfg.get_margin_color(); |
795 | let color: Option<&AnsiColor<'_>> = color.left.as_ref(); |
796 | print_margin_vertical(f, indent, offset, color, line, height) |
797 | } |
798 | |
799 | fn print_margin_right<F: Write>( |
800 | f: &mut F, |
801 | cfg: &SpannedConfig, |
802 | line: usize, |
803 | height: usize, |
804 | ) -> fmt::Result { |
805 | let indent: Indent = cfg.get_margin().right; |
806 | let offset: Offset = cfg.get_margin_offset().right; |
807 | let color: Sides = cfg.get_margin_color(); |
808 | let color: Option<&AnsiColor<'_>> = color.right.as_ref(); |
809 | print_margin_vertical(f, indent, offset, color, line, height) |
810 | } |
811 | |
812 | fn print_margin_vertical<F: Write>( |
813 | f: &mut F, |
814 | indent: Indent, |
815 | offset: Offset, |
816 | color: Option<&AnsiColor<'_>>, |
817 | line: usize, |
818 | height: usize, |
819 | ) -> fmt::Result { |
820 | if indent.size == 0 { |
821 | return Ok(()); |
822 | } |
823 | |
824 | match offset { |
825 | Offset::Begin(offset) => { |
826 | let offset = cmp::min(offset, height); |
827 | if line >= offset { |
828 | print_indent(f, indent.fill, indent.size, color)?; |
829 | } else { |
830 | repeat_char(f, ' ' , indent.size)?; |
831 | } |
832 | } |
833 | Offset::End(offset) => { |
834 | let offset = cmp::min(offset, height); |
835 | let pos = height - offset; |
836 | |
837 | if line >= pos { |
838 | repeat_char(f, ' ' , indent.size)?; |
839 | } else { |
840 | print_indent(f, indent.fill, indent.size, color)?; |
841 | } |
842 | } |
843 | } |
844 | |
845 | Ok(()) |
846 | } |
847 | |
848 | fn print_indent_lines<F: Write>( |
849 | f: &mut F, |
850 | indent: &Indent, |
851 | offset: &Offset, |
852 | color: Option<&AnsiColor<'_>>, |
853 | width: usize, |
854 | ) -> fmt::Result { |
855 | if indent.size == 0 { |
856 | return Ok(()); |
857 | } |
858 | |
859 | let (start_offset, end_offset) = match offset { |
860 | Offset::Begin(start) => (*start, 0), |
861 | Offset::End(end) => (0, *end), |
862 | }; |
863 | |
864 | let start_offset = std::cmp::min(start_offset, width); |
865 | let end_offset = std::cmp::min(end_offset, width); |
866 | let indent_size = width - start_offset - end_offset; |
867 | |
868 | for i in 0..indent.size { |
869 | if start_offset > 0 { |
870 | repeat_char(f, ' ' , start_offset)?; |
871 | } |
872 | |
873 | if indent_size > 0 { |
874 | print_indent(f, indent.fill, indent_size, color)?; |
875 | } |
876 | |
877 | if end_offset > 0 { |
878 | repeat_char(f, ' ' , end_offset)?; |
879 | } |
880 | |
881 | if i + 1 != indent.size { |
882 | f.write_char(' \n' )?; |
883 | } |
884 | } |
885 | |
886 | Ok(()) |
887 | } |
888 | |
889 | fn print_indent<F: Write, C: Color>(f: &mut F, c: char, n: usize, color: Option<C>) -> fmt::Result { |
890 | if n == 0 { |
891 | return Ok(()); |
892 | } |
893 | |
894 | match color { |
895 | Some(color: C) => { |
896 | color.fmt_prefix(f)?; |
897 | repeat_char(f, c, n)?; |
898 | color.fmt_suffix(f) |
899 | } |
900 | None => repeat_char(f, c, n), |
901 | } |
902 | } |
903 | |
904 | fn get_cell_width<D: Dimension>(cfg: &SpannedConfig, dims: &D, pos: Position, max: usize) -> usize { |
905 | match cfg.get_column_span(pos) { |
906 | Some(span: usize) => { |
907 | let start: usize = pos.1; |
908 | let end: usize = pos.1 + span; |
909 | range_width(dims, start, end) + count_verticals_range(cfg, start, end, max) |
910 | } |
911 | None => dims.get_width(column:pos.1), |
912 | } |
913 | } |
914 | |
915 | fn range_width<D: Dimension>(dims: &D, start: usize, end: usize) -> usize { |
916 | (start..end).map(|col: usize| dims.get_width(column:col)).sum::<usize>() |
917 | } |
918 | |
919 | fn count_verticals_range(cfg: &SpannedConfig, start: usize, end: usize, max: usize) -> usize { |
920 | (start + 1..end) |
921 | .map(|i: usize| cfg.has_vertical(col:i, count_columns:max) as usize) |
922 | .sum() |
923 | } |
924 | |
925 | fn get_cell_height<D: Dimension>( |
926 | cfg: &SpannedConfig, |
927 | dims: &D, |
928 | pos: Position, |
929 | max: usize, |
930 | ) -> usize { |
931 | match cfg.get_row_span(pos) { |
932 | Some(span: usize) => { |
933 | let start: usize = pos.0; |
934 | let end: usize = pos.0 + span; |
935 | range_height(dims, start, end) + count_horizontals_range(cfg, start, end, max) |
936 | } |
937 | None => dims.get_height(row:pos.0), |
938 | } |
939 | } |
940 | |
941 | fn range_height<D: Dimension>(dims: &D, start: usize, end: usize) -> usize { |
942 | (start..end).map(|col: usize| dims.get_height(row:col)).sum::<usize>() |
943 | } |
944 | |
945 | fn count_horizontals_range(cfg: &SpannedConfig, start: usize, end: usize, max: usize) -> usize { |
946 | (start + 1..end) |
947 | .map(|i: usize| cfg.has_horizontal(row:i, count_rows:max) as usize) |
948 | .sum() |
949 | } |
950 | |
951 | fn closest_visible_row(cfg: &SpannedConfig, mut pos: Position) -> Option<usize> { |
952 | loop { |
953 | if cfg.is_cell_visible(pos) { |
954 | return Some(pos.0); |
955 | } |
956 | |
957 | if pos.0 == 0 { |
958 | return None; |
959 | } |
960 | |
961 | pos.0 -= 1; |
962 | } |
963 | } |
964 | |
965 | /// Trims a string. |
966 | fn string_trim(text: &str) -> Cow<'_, str> { |
967 | #[cfg (feature = "color" )] |
968 | { |
969 | ansi_str::AnsiStr::ansi_trim(text) |
970 | } |
971 | |
972 | #[cfg (not(feature = "color" ))] |
973 | { |
974 | text.trim().into() |
975 | } |
976 | } |
977 | |