| //! The module contains a [`PeekableGrid`] structure. |
| |
| use core::borrow::Borrow; |
| use std::{ |
| borrow::Cow, |
| cmp, |
| fmt::{self, Write}, |
| }; |
| |
| use crate::{ |
| color::{AnsiColor, Color}, |
| colors::Colors, |
| config::spanned::{Formatting, Offset, SpannedConfig}, |
| config::{AlignmentHorizontal, AlignmentVertical, Indent, Position, Sides}, |
| dimension::Dimension, |
| records::{ExactRecords, PeekableRecords, Records}, |
| util::string::string_width, |
| }; |
| |
| /// Grid provides a set of methods for building a text-based table. |
| #[derive(Debug, Clone)] |
| pub struct PeekableGrid<R, G, D, C> { |
| records: R, |
| config: G, |
| dimension: D, |
| colors: C, |
| } |
| |
| impl<R, G, D, C> PeekableGrid<R, G, D, C> { |
| /// The new method creates a grid instance with default styles. |
| pub fn new(records: R, config: G, dimension: D, colors: C) -> Self { |
| PeekableGrid { |
| records, |
| config, |
| dimension, |
| colors, |
| } |
| } |
| } |
| |
| impl<R, G, D, C> PeekableGrid<R, G, D, C> { |
| /// Builds a table. |
| pub fn build<F>(self, mut f: F) -> fmt::Result |
| where |
| R: Records + PeekableRecords + ExactRecords, |
| D: Dimension, |
| C: Colors, |
| G: Borrow<SpannedConfig>, |
| F: Write, |
| { |
| if self.records.count_columns() == 0 || self.records.hint_count_rows() == Some(0) { |
| return Ok(()); |
| } |
| |
| let config = self.config.borrow(); |
| print_grid(&mut f, self.records, config, &self.dimension, &self.colors) |
| } |
| |
| /// Builds a table into string. |
| /// |
| /// Notice that it consumes self. |
| #[allow(clippy::inherent_to_string)] |
| pub fn to_string(self) -> String |
| where |
| R: Records + PeekableRecords + ExactRecords, |
| D: Dimension, |
| G: Borrow<SpannedConfig>, |
| C: Colors, |
| { |
| let mut buf = String::new(); |
| self.build(&mut buf).expect("It's guaranteed to never happen otherwise it's considered an stdlib error or impl error"); |
| buf |
| } |
| } |
| |
| fn print_grid<F: Write, R: Records + PeekableRecords + ExactRecords, D: Dimension, C: Colors>( |
| f: &mut F, |
| records: R, |
| cfg: &SpannedConfig, |
| dimension: &D, |
| colors: &C, |
| ) -> fmt::Result { |
| if cfg.has_column_spans() || cfg.has_row_spans() { |
| build_grid_spanned(f, &records, cfg, dimension, colors) |
| } else { |
| build_grid(f, &records, cfg, dimension, colors) |
| } |
| } |
| |
| fn build_grid<F: Write, R: Records + PeekableRecords + ExactRecords, D: Dimension, C: Colors>( |
| f: &mut F, |
| records: &R, |
| cfg: &SpannedConfig, |
| dimension: &D, |
| colors: &C, |
| ) -> fmt::Result { |
| let shape = (records.count_rows(), records.count_columns()); |
| |
| let total_width = total_width(cfg, dimension, shape.1); |
| let total_width_with_margin = |
| total_width + cfg.get_margin().left.size + cfg.get_margin().right.size; |
| |
| let total_height = total_height(cfg, dimension, shape.0); |
| |
| if cfg.get_margin().top.size > 0 { |
| print_margin_top(f, cfg, total_width_with_margin)?; |
| f.write_char('\n')?; |
| } |
| |
| let mut table_line = 0; |
| let mut prev_empty_horizontal = false; |
| for row in 0..shape.0 { |
| let height = dimension.get_height(row); |
| |
| if cfg.has_horizontal(row, shape.0) { |
| if prev_empty_horizontal { |
| f.write_char('\n')?; |
| } |
| |
| print_margin_left(f, cfg, table_line, total_height)?; |
| print_split_line(f, cfg, dimension, row, shape)?; |
| print_margin_right(f, cfg, table_line, total_height)?; |
| |
| if height > 0 { |
| f.write_char('\n')?; |
| prev_empty_horizontal = false; |
| } else { |
| prev_empty_horizontal = true; |
| } |
| |
| table_line += 1; |
| } else if height > 0 && prev_empty_horizontal { |
| f.write_char('\n')?; |
| prev_empty_horizontal = false; |
| } |
| |
| for i in 0..height { |
| print_margin_left(f, cfg, table_line, total_height)?; |
| |
| for col in 0..records.count_columns() { |
| print_vertical_char(f, cfg, (row, col), i, height, shape.1)?; |
| |
| let width = dimension.get_width(col); |
| print_cell_line(f, records, cfg, colors, width, height, (row, col), i)?; |
| |
| let is_last_column = col + 1 == records.count_columns(); |
| if is_last_column { |
| print_vertical_char(f, cfg, (row, col + 1), i, height, shape.1)?; |
| } |
| } |
| |
| print_margin_right(f, cfg, table_line, total_height)?; |
| |
| let is_last_line = i + 1 == height; |
| let is_last_row = row + 1 == records.count_rows(); |
| if !(is_last_line && is_last_row) { |
| f.write_char('\n')?; |
| } |
| |
| table_line += 1; |
| } |
| } |
| |
| if cfg.has_horizontal(shape.0, shape.0) { |
| f.write_char('\n')?; |
| print_margin_left(f, cfg, table_line, total_height)?; |
| print_split_line(f, cfg, dimension, records.count_rows(), shape)?; |
| print_margin_right(f, cfg, table_line, total_height)?; |
| } |
| |
| if cfg.get_margin().bottom.size > 0 { |
| f.write_char('\n')?; |
| print_margin_bottom(f, cfg, total_width_with_margin)?; |
| } |
| |
| Ok(()) |
| } |
| |
| fn print_split_line<F: Write, D: Dimension>( |
| f: &mut F, |
| cfg: &SpannedConfig, |
| dimension: &D, |
| row: usize, |
| shape: (usize, usize), |
| ) -> fmt::Result { |
| let mut used_color = None; |
| print_vertical_intersection(f, cfg, (row, 0), shape, &mut used_color)?; |
| |
| for col in 0..shape.1 { |
| let width = dimension.get_width(col); |
| |
| // general case |
| if width > 0 { |
| let pos = (row, col); |
| let main = cfg.get_horizontal(pos, shape.0); |
| match main { |
| Some(c) => { |
| let clr = cfg.get_horizontal_color(pos, shape.0); |
| prepare_coloring(f, clr, &mut used_color)?; |
| print_horizontal_border(f, cfg, pos, width, c, &used_color)?; |
| } |
| None => repeat_char(f, ' ', width)?, |
| } |
| } |
| |
| print_vertical_intersection(f, cfg, (row, col + 1), shape, &mut used_color)?; |
| } |
| |
| if let Some(clr) = used_color.take() { |
| clr.fmt_suffix(f)?; |
| } |
| |
| Ok(()) |
| } |
| |
| fn print_vertical_intersection<'a, F: fmt::Write>( |
| f: &mut F, |
| cfg: &'a SpannedConfig, |
| pos: Position, |
| shape: (usize, usize), |
| used_color: &mut Option<&'a AnsiColor<'static>>, |
| ) -> fmt::Result { |
| match cfg.get_intersection(pos, shape) { |
| Some(c) => { |
| let clr = cfg.get_intersection_color(pos, shape); |
| prepare_coloring(f, clr, used_color)?; |
| f.write_char(c) |
| } |
| None => Ok(()), |
| } |
| } |
| |
| fn prepare_coloring<'a, 'b, F: Write>( |
| f: &mut F, |
| clr: Option<&'a AnsiColor<'b>>, |
| used_color: &mut Option<&'a AnsiColor<'b>>, |
| ) -> fmt::Result { |
| match clr { |
| Some(clr) => match used_color.as_mut() { |
| Some(used_clr) => { |
| if **used_clr != *clr { |
| used_clr.fmt_suffix(f)?; |
| clr.fmt_prefix(f)?; |
| *used_clr = clr; |
| } |
| } |
| None => { |
| clr.fmt_prefix(f)?; |
| *used_color = Some(clr); |
| } |
| }, |
| None => { |
| if let Some(clr) = used_color.take() { |
| clr.fmt_suffix(f)? |
| } |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| fn print_vertical_char<F: Write>( |
| f: &mut F, |
| cfg: &SpannedConfig, |
| pos: Position, |
| line: usize, |
| count_lines: usize, |
| count_columns: usize, |
| ) -> fmt::Result { |
| let symbol = match cfg.get_vertical(pos, count_columns) { |
| Some(c) => c, |
| None => return Ok(()), |
| }; |
| |
| let symbol = cfg |
| .is_overridden_vertical(pos) |
| .then(|| cfg.lookup_vertical_char(pos, line, count_lines)) |
| .flatten() |
| .unwrap_or(symbol); |
| |
| match cfg.get_vertical_color(pos, count_columns) { |
| Some(clr) => { |
| clr.fmt_prefix(f)?; |
| f.write_char(symbol)?; |
| clr.fmt_suffix(f)?; |
| } |
| None => f.write_char(symbol)?, |
| } |
| |
| Ok(()) |
| } |
| |
| fn build_grid_spanned< |
| F: Write, |
| R: Records + PeekableRecords + ExactRecords, |
| D: Dimension, |
| C: Colors, |
| >( |
| f: &mut F, |
| records: &R, |
| cfg: &SpannedConfig, |
| dims: &D, |
| colors: &C, |
| ) -> fmt::Result { |
| let shape = (records.count_rows(), records.count_columns()); |
| |
| let total_width = total_width(cfg, dims, shape.1); |
| let total_width_with_margin = |
| total_width + cfg.get_margin().left.size + cfg.get_margin().right.size; |
| |
| let total_height = total_height(cfg, dims, shape.0); |
| |
| if cfg.get_margin().top.size > 0 { |
| print_margin_top(f, cfg, total_width_with_margin)?; |
| f.write_char('\n')?; |
| } |
| |
| let mut table_line = 0; |
| let mut prev_empty_horizontal = false; |
| for row in 0..records.count_rows() { |
| let count_lines = dims.get_height(row); |
| |
| if cfg.has_horizontal(row, shape.0) { |
| if prev_empty_horizontal { |
| f.write_char('\n')?; |
| } |
| |
| print_margin_left(f, cfg, table_line, total_height)?; |
| print_split_line_spanned(f, records, cfg, dims, colors, row, shape)?; |
| print_margin_right(f, cfg, table_line, total_height)?; |
| |
| if count_lines > 0 { |
| f.write_char('\n')?; |
| prev_empty_horizontal = false; |
| } else { |
| prev_empty_horizontal = true; |
| } |
| |
| table_line += 1; |
| } else if count_lines > 0 && prev_empty_horizontal { |
| f.write_char('\n')?; |
| prev_empty_horizontal = false; |
| } |
| |
| for i in 0..count_lines { |
| print_margin_left(f, cfg, table_line, total_height)?; |
| |
| for col in 0..records.count_columns() { |
| if cfg.is_cell_covered_by_both_spans((row, col)) { |
| continue; |
| } |
| |
| if cfg.is_cell_covered_by_column_span((row, col)) { |
| let is_last_column = col + 1 == records.count_columns(); |
| if is_last_column { |
| print_vertical_char(f, cfg, (row, col + 1), i, count_lines, shape.1)?; |
| } |
| |
| continue; |
| } |
| |
| print_vertical_char(f, cfg, (row, col), i, count_lines, shape.1)?; |
| |
| if cfg.is_cell_covered_by_row_span((row, col)) { |
| // means it's part of other a spanned cell |
| // so. we just need to use line from other cell. |
| let original_row = closest_visible_row(cfg, (row, col)).unwrap(); |
| |
| // considering that the content will be printed instead horizontal lines so we can skip some lines. |
| let mut skip_lines = (original_row..row) |
| .map(|i| dims.get_height(i)) |
| .sum::<usize>(); |
| |
| skip_lines += (original_row + 1..=row) |
| .map(|row| cfg.has_horizontal(row, shape.0) as usize) |
| .sum::<usize>(); |
| |
| let line = i + skip_lines; |
| let pos = (original_row, col); |
| |
| let width = get_cell_width(cfg, dims, pos, shape.1); |
| let height = get_cell_height(cfg, dims, pos, shape.0); |
| |
| print_cell_line(f, records, cfg, colors, width, height, pos, line)?; |
| } else { |
| let width = get_cell_width(cfg, dims, (row, col), shape.1); |
| let height = get_cell_height(cfg, dims, (row, col), shape.0); |
| print_cell_line(f, records, cfg, colors, width, height, (row, col), i)?; |
| } |
| |
| let is_last_column = col + 1 == records.count_columns(); |
| if is_last_column { |
| print_vertical_char(f, cfg, (row, col + 1), i, count_lines, shape.1)?; |
| } |
| } |
| |
| print_margin_right(f, cfg, table_line, total_height)?; |
| |
| let is_last_line = i + 1 == count_lines; |
| let is_last_row = row + 1 == records.count_rows(); |
| if !(is_last_line && is_last_row) { |
| f.write_char('\n')?; |
| } |
| |
| table_line += 1; |
| } |
| } |
| |
| if cfg.has_horizontal(shape.0, shape.0) { |
| f.write_char('\n')?; |
| print_margin_left(f, cfg, table_line, total_height)?; |
| print_split_line(f, cfg, dims, records.count_rows(), shape)?; |
| print_margin_right(f, cfg, table_line, total_height)?; |
| } |
| |
| if cfg.get_margin().bottom.size > 0 { |
| f.write_char('\n')?; |
| print_margin_bottom(f, cfg, total_width_with_margin)?; |
| } |
| |
| Ok(()) |
| } |
| |
| fn print_split_line_spanned< |
| F: Write, |
| R: Records + ExactRecords + PeekableRecords, |
| D: Dimension, |
| C: Colors, |
| >( |
| f: &mut F, |
| records: &R, |
| cfg: &SpannedConfig, |
| dims: &D, |
| colors: &C, |
| row: usize, |
| shape: (usize, usize), |
| ) -> fmt::Result { |
| let mut used_color = None; |
| print_vertical_intersection(f, cfg, (row, 0), shape, &mut used_color)?; |
| |
| for col in 0..shape.1 { |
| let pos = (row, col); |
| if cfg.is_cell_covered_by_both_spans(pos) { |
| continue; |
| } |
| |
| if cfg.is_cell_covered_by_row_span(pos) { |
| // means it's part of other a spanned cell |
| // so. we just need to use line from other cell. |
| |
| let original_row = closest_visible_row(cfg, (row, col)).unwrap(); |
| |
| // considering that the content will be printed instead horizontal lines so we can skip some lines. |
| let mut skip_lines = (original_row..row) |
| .map(|i| dims.get_height(i)) |
| .sum::<usize>(); |
| |
| // skip horizontal lines |
| if row > 0 { |
| skip_lines += (original_row..row - 1) |
| .map(|row| cfg.has_horizontal(row + 1, shape.0) as usize) |
| .sum::<usize>(); |
| } |
| |
| let pos = (original_row, col); |
| let height = get_cell_height(cfg, dims, pos, shape.0); |
| let width = get_cell_width(cfg, dims, pos, shape.1); |
| let line = skip_lines; |
| |
| print_cell_line(f, records, cfg, colors, width, height, pos, line)?; |
| |
| // We need to use a correct right split char. |
| let mut col = col; |
| if let Some(span) = cfg.get_column_span(pos) { |
| col += span - 1; |
| } |
| |
| print_vertical_intersection(f, cfg, (row, col + 1), shape, &mut used_color)?; |
| |
| continue; |
| } |
| |
| let width = dims.get_width(col); |
| if width > 0 { |
| // general case |
| let main = cfg.get_horizontal(pos, shape.0); |
| match main { |
| Some(c) => { |
| let clr = cfg.get_horizontal_color(pos, shape.0); |
| prepare_coloring(f, clr, &mut used_color)?; |
| print_horizontal_border(f, cfg, pos, width, c, &used_color)?; |
| } |
| None => repeat_char(f, ' ', width)?, |
| } |
| } |
| |
| print_vertical_intersection(f, cfg, (row, col + 1), shape, &mut used_color)?; |
| } |
| |
| if let Some(clr) = used_color { |
| clr.fmt_suffix(f)?; |
| } |
| |
| Ok(()) |
| } |
| |
| fn print_horizontal_border<F: Write>( |
| f: &mut F, |
| cfg: &SpannedConfig, |
| pos: Position, |
| width: usize, |
| c: char, |
| used_color: &Option<&AnsiColor<'static>>, |
| ) -> fmt::Result { |
| if !cfg.is_overridden_horizontal(pos) { |
| return repeat_char(f, c, width); |
| } |
| |
| for i in 0..width { |
| let c = cfg.lookup_horizontal_char(pos, i, width).unwrap_or(c); |
| match cfg.lookup_horizontal_color(pos, i, width) { |
| Some(color) => match used_color { |
| Some(clr) => { |
| clr.fmt_suffix(f)?; |
| color.fmt_prefix(f)?; |
| f.write_char(c)?; |
| color.fmt_suffix(f)?; |
| clr.fmt_prefix(f)?; |
| } |
| None => { |
| color.fmt_prefix(f)?; |
| f.write_char(c)?; |
| color.fmt_suffix(f)?; |
| } |
| }, |
| _ => f.write_char(c)?, |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| #[allow(clippy::too_many_arguments)] |
| fn print_cell_line<F: Write, R: Records + PeekableRecords + ExactRecords, C: Colors>( |
| f: &mut F, |
| records: &R, |
| cfg: &SpannedConfig, |
| colors: &C, |
| width: usize, |
| height: usize, |
| pos: Position, |
| line: usize, |
| ) -> fmt::Result { |
| let entity = pos.into(); |
| |
| let mut cell_height = records.count_lines(pos); |
| let formatting = *cfg.get_formatting(entity); |
| if formatting.vertical_trim { |
| cell_height -= |
| count_empty_lines_at_start(records, pos) + count_empty_lines_at_end(records, pos); |
| } |
| |
| if cell_height > height { |
| // it may happen if the height estimation decide so |
| cell_height = height; |
| } |
| |
| let pad = cfg.get_padding(entity); |
| let pad_color = cfg.get_padding_color(entity); |
| let alignment = cfg.get_alignment_vertical(entity); |
| let indent = top_indent(&pad, *alignment, cell_height, height); |
| if indent > line { |
| return print_indent(f, pad.top.fill, width, pad_color.top.as_ref()); |
| } |
| |
| let mut index = line - indent; |
| let cell_has_this_line = cell_height > index; |
| if !cell_has_this_line { |
| // happens when other cells have bigger height |
| return print_indent(f, pad.bottom.fill, width, pad_color.bottom.as_ref()); |
| } |
| |
| if formatting.vertical_trim { |
| let empty_lines = count_empty_lines_at_start(records, pos); |
| index += empty_lines; |
| |
| if index > records.count_lines(pos) { |
| return print_indent(f, pad.top.fill, width, pad_color.top.as_ref()); |
| } |
| } |
| |
| print_indent(f, pad.left.fill, pad.left.size, pad_color.left.as_ref())?; |
| |
| let width = width - pad.left.size - pad.right.size; |
| let alignment = *cfg.get_alignment_horizontal(entity); |
| let justification = ( |
| cfg.get_justification(entity), |
| cfg.get_justification_color(entity), |
| ); |
| let color = colors.get_color(pos); |
| print_line( |
| f, |
| records, |
| pos, |
| index, |
| alignment, |
| formatting, |
| color, |
| justification, |
| width, |
| )?; |
| |
| print_indent(f, pad.right.fill, pad.right.size, pad_color.right.as_ref())?; |
| |
| Ok(()) |
| } |
| |
| #[allow(clippy::too_many_arguments)] |
| fn print_line<F: Write, R: Records + PeekableRecords, C: Color>( |
| f: &mut F, |
| records: &R, |
| pos: Position, |
| index: usize, |
| alignment: AlignmentHorizontal, |
| formatting: Formatting, |
| color: Option<C>, |
| justification: (char, Option<&AnsiColor<'_>>), |
| available: usize, |
| ) -> fmt::Result { |
| let line = records.get_line(pos, index); |
| let (line, line_width) = if formatting.horizontal_trim { |
| let line = string_trim(line); |
| let width = string_width(&line); |
| (line, width) |
| } else { |
| let width = records.get_line_width(pos, index); |
| (Cow::Borrowed(line), width) |
| }; |
| |
| if formatting.allow_lines_alignment { |
| let (left, right) = calculate_indent(alignment, line_width, available); |
| return print_text_with_pad(f, &line, color, justification, left, right); |
| } |
| |
| let cell_width = if formatting.horizontal_trim { |
| (0..records.count_lines(pos)) |
| .map(|i| records.get_line(pos, i)) |
| .map(|line| string_width(line.trim())) |
| .max() |
| .unwrap_or_default() |
| } else { |
| records.get_width(pos) |
| }; |
| |
| let (left, right) = calculate_indent(alignment, cell_width, available); |
| print_text_with_pad(f, &line, color, justification, left, right)?; |
| |
| // todo: remove me |
| let rest_width = cell_width - line_width; |
| repeat_char(f, ' ', rest_width)?; |
| |
| Ok(()) |
| } |
| |
| fn print_text_with_pad<F: Write, C: Color>( |
| f: &mut F, |
| text: &str, |
| color: Option<C>, |
| justification: (char, Option<&AnsiColor<'_>>), |
| left: usize, |
| right: usize, |
| ) -> fmt::Result { |
| print_indent(f, justification.0, left, justification.1)?; |
| print_text(f, text, color)?; |
| print_indent(f, justification.0, right, justification.1)?; |
| Ok(()) |
| } |
| |
| fn print_text<F: Write, C: Color>(f: &mut F, text: &str, clr: Option<C>) -> fmt::Result { |
| match clr { |
| Some(color) => { |
| color.fmt_prefix(f)?; |
| f.write_str(text)?; |
| color.fmt_suffix(f) |
| } |
| None => f.write_str(text), |
| } |
| } |
| |
| fn top_indent( |
| pad: &Sides<Indent>, |
| alignment: AlignmentVertical, |
| cell_height: usize, |
| available: usize, |
| ) -> usize { |
| let height = available - pad.top.size; |
| let indent = indent_from_top(alignment, height, cell_height); |
| |
| indent + pad.top.size |
| } |
| |
| fn indent_from_top(alignment: AlignmentVertical, available: usize, real: usize) -> usize { |
| match alignment { |
| AlignmentVertical::Top => 0, |
| AlignmentVertical::Bottom => available - real, |
| AlignmentVertical::Center => (available - real) / 2, |
| } |
| } |
| |
| fn calculate_indent( |
| alignment: AlignmentHorizontal, |
| text_width: usize, |
| available: usize, |
| ) -> (usize, usize) { |
| let diff = available - text_width; |
| match alignment { |
| AlignmentHorizontal::Left => (0, diff), |
| AlignmentHorizontal::Right => (diff, 0), |
| AlignmentHorizontal::Center => { |
| let left = diff / 2; |
| let rest = diff - left; |
| (left, rest) |
| } |
| } |
| } |
| |
| fn repeat_char<F: Write>(f: &mut F, c: char, n: usize) -> fmt::Result { |
| for _ in 0..n { |
| f.write_char(c)?; |
| } |
| |
| Ok(()) |
| } |
| |
| fn count_empty_lines_at_end<R>(records: &R, pos: Position) -> usize |
| where |
| R: Records + PeekableRecords, |
| { |
| (0..records.count_lines(pos)) |
| .map(|i| records.get_line(pos, i)) |
| .rev() |
| .take_while(|l| l.trim().is_empty()) |
| .count() |
| } |
| |
| fn count_empty_lines_at_start<R>(records: &R, pos: Position) -> usize |
| where |
| R: Records + PeekableRecords, |
| { |
| (0..records.count_lines(pos)) |
| .map(|i| records.get_line(pos, i)) |
| .take_while(|s| s.trim().is_empty()) |
| .count() |
| } |
| |
| fn total_width<D: Dimension>(cfg: &SpannedConfig, dimension: &D, count_columns: usize) -> usize { |
| (0..count_columns) |
| .map(|i| dimension.get_width(i)) |
| .sum::<usize>() |
| + cfg.count_vertical(count_columns) |
| } |
| |
| fn total_height<D: Dimension>(cfg: &SpannedConfig, dimension: &D, count_rows: usize) -> usize { |
| (0..count_rows) |
| .map(|i| dimension.get_height(i)) |
| .sum::<usize>() |
| + cfg.count_horizontal(count_rows) |
| } |
| |
| fn print_margin_top<F: Write>(f: &mut F, cfg: &SpannedConfig, width: usize) -> fmt::Result { |
| let indent = cfg.get_margin().top; |
| let offset = cfg.get_margin_offset().top; |
| let color = cfg.get_margin_color(); |
| let color = color.top.as_ref(); |
| print_indent_lines(f, &indent, &offset, color, width) |
| } |
| |
| fn print_margin_bottom<F: Write>(f: &mut F, cfg: &SpannedConfig, width: usize) -> fmt::Result { |
| let indent = cfg.get_margin().bottom; |
| let offset = cfg.get_margin_offset().bottom; |
| let color = cfg.get_margin_color(); |
| let color = color.bottom.as_ref(); |
| print_indent_lines(f, &indent, &offset, color, width) |
| } |
| |
| fn print_margin_left<F: Write>( |
| f: &mut F, |
| cfg: &SpannedConfig, |
| line: usize, |
| height: usize, |
| ) -> fmt::Result { |
| let indent = cfg.get_margin().left; |
| let offset = cfg.get_margin_offset().left; |
| let color = cfg.get_margin_color(); |
| let color = color.left.as_ref(); |
| print_margin_vertical(f, indent, offset, color, line, height) |
| } |
| |
| fn print_margin_right<F: Write>( |
| f: &mut F, |
| cfg: &SpannedConfig, |
| line: usize, |
| height: usize, |
| ) -> fmt::Result { |
| let indent = cfg.get_margin().right; |
| let offset = cfg.get_margin_offset().right; |
| let color = cfg.get_margin_color(); |
| let color = color.right.as_ref(); |
| print_margin_vertical(f, indent, offset, color, line, height) |
| } |
| |
| fn print_margin_vertical<F: Write>( |
| f: &mut F, |
| indent: Indent, |
| offset: Offset, |
| color: Option<&AnsiColor<'_>>, |
| line: usize, |
| height: usize, |
| ) -> fmt::Result { |
| if indent.size == 0 { |
| return Ok(()); |
| } |
| |
| match offset { |
| Offset::Begin(offset) => { |
| let offset = cmp::min(offset, height); |
| if line >= offset { |
| print_indent(f, indent.fill, indent.size, color)?; |
| } else { |
| repeat_char(f, ' ', indent.size)?; |
| } |
| } |
| Offset::End(offset) => { |
| let offset = cmp::min(offset, height); |
| let pos = height - offset; |
| |
| if line >= pos { |
| repeat_char(f, ' ', indent.size)?; |
| } else { |
| print_indent(f, indent.fill, indent.size, color)?; |
| } |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| fn print_indent_lines<F: Write>( |
| f: &mut F, |
| indent: &Indent, |
| offset: &Offset, |
| color: Option<&AnsiColor<'_>>, |
| width: usize, |
| ) -> fmt::Result { |
| if indent.size == 0 { |
| return Ok(()); |
| } |
| |
| let (start_offset, end_offset) = match offset { |
| Offset::Begin(start) => (*start, 0), |
| Offset::End(end) => (0, *end), |
| }; |
| |
| let start_offset = std::cmp::min(start_offset, width); |
| let end_offset = std::cmp::min(end_offset, width); |
| let indent_size = width - start_offset - end_offset; |
| |
| for i in 0..indent.size { |
| if start_offset > 0 { |
| repeat_char(f, ' ', start_offset)?; |
| } |
| |
| if indent_size > 0 { |
| print_indent(f, indent.fill, indent_size, color)?; |
| } |
| |
| if end_offset > 0 { |
| repeat_char(f, ' ', end_offset)?; |
| } |
| |
| if i + 1 != indent.size { |
| f.write_char('\n')?; |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| fn print_indent<F: Write, C: Color>(f: &mut F, c: char, n: usize, color: Option<C>) -> fmt::Result { |
| if n == 0 { |
| return Ok(()); |
| } |
| |
| match color { |
| Some(color) => { |
| color.fmt_prefix(f)?; |
| repeat_char(f, c, n)?; |
| color.fmt_suffix(f) |
| } |
| None => repeat_char(f, c, n), |
| } |
| } |
| |
| fn get_cell_width<D: Dimension>(cfg: &SpannedConfig, dims: &D, pos: Position, max: usize) -> usize { |
| match cfg.get_column_span(pos) { |
| Some(span) => { |
| let start = pos.1; |
| let end = pos.1 + span; |
| range_width(dims, start, end) + count_verticals_range(cfg, start, end, max) |
| } |
| None => dims.get_width(pos.1), |
| } |
| } |
| |
| fn range_width<D: Dimension>(dims: &D, start: usize, end: usize) -> usize { |
| (start..end).map(|col| dims.get_width(col)).sum::<usize>() |
| } |
| |
| fn count_verticals_range(cfg: &SpannedConfig, start: usize, end: usize, max: usize) -> usize { |
| (start + 1..end) |
| .map(|i| cfg.has_vertical(i, max) as usize) |
| .sum() |
| } |
| |
| fn get_cell_height<D: Dimension>( |
| cfg: &SpannedConfig, |
| dims: &D, |
| pos: Position, |
| max: usize, |
| ) -> usize { |
| match cfg.get_row_span(pos) { |
| Some(span) => { |
| let start = pos.0; |
| let end = pos.0 + span; |
| range_height(dims, start, end) + count_horizontals_range(cfg, start, end, max) |
| } |
| None => dims.get_height(pos.0), |
| } |
| } |
| |
| fn range_height<D: Dimension>(dims: &D, start: usize, end: usize) -> usize { |
| (start..end).map(|col| dims.get_height(col)).sum::<usize>() |
| } |
| |
| fn count_horizontals_range(cfg: &SpannedConfig, start: usize, end: usize, max: usize) -> usize { |
| (start + 1..end) |
| .map(|i| cfg.has_horizontal(i, max) as usize) |
| .sum() |
| } |
| |
| fn closest_visible_row(cfg: &SpannedConfig, mut pos: Position) -> Option<usize> { |
| loop { |
| if cfg.is_cell_visible(pos) { |
| return Some(pos.0); |
| } |
| |
| if pos.0 == 0 { |
| return None; |
| } |
| |
| pos.0 -= 1; |
| } |
| } |
| |
| /// Trims a string. |
| fn string_trim(text: &str) -> Cow<'_, str> { |
| #[cfg(feature = "color")] |
| { |
| ansi_str::AnsiStr::ansi_trim(text) |
| } |
| |
| #[cfg(not(feature = "color"))] |
| { |
| text.trim().into() |
| } |
| } |