use std::{
borrow::{Borrow, Cow},
cmp,
collections::BTreeMap,
fmt::{self, Write},
};
use crate::{
ansi::{ANSIBuf, ANSIFmt},
colors::Colors,
config::spanned::{Offset, SpannedConfig},
config::{AlignmentHorizontal, AlignmentVertical, Formatting, Indent, Position, Sides},
dimension::Dimension,
records::{IntoRecords, Records},
util::string::{count_lines, get_line_width, get_lines, get_text_width, Lines},
};
#[derive(Debug, Clone)]
pub struct Grid<R, D, G, C> {
records: R,
config: G,
dimension: D,
colors: C,
}
impl<R, D, G, C> Grid<R, D, G, C> {
pub fn new(records: R, dimension: D, config: G, colors: C) -> Self {
Grid {
records,
config,
dimension,
colors,
}
}
}
impl<R, D, G, C> Grid<R, D, G, C> {
pub fn build<F>(self, mut f: F) -> fmt::Result
where
R: Records,
<R::Iter as IntoRecords>::Cell: AsRef<str>,
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)
}
#[allow(clippy::inherent_to_string)]
pub fn to_string(self) -> String
where
R: Records,
<R::Iter as IntoRecords>::Cell: AsRef<str>,
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, R, D, C>(
f: &mut F,
records: R,
cfg: &SpannedConfig,
dimension: &D,
colors: &C,
) -> fmt::Result
where
F: Write,
R: Records,
<R::Iter as IntoRecords>::Cell: AsRef<str>,
D: Dimension,
C: Colors,
{
let grid_has_spans = cfg.has_column_spans() || cfg.has_row_spans();
if grid_has_spans {
print_grid_spanned(f, records, cfg, dimension, colors)
} else {
print_grid_general(f, records, cfg, dimension, colors)
}
}
fn print_grid_general<F, R, D, C>(
f: &mut F,
records: R,
cfg: &SpannedConfig,
dims: &D,
colors: &C,
) -> fmt::Result
where
F: Write,
R: Records,
<R::Iter as IntoRecords>::Cell: AsRef<str>,
D: Dimension,
C: Colors,
{
let count_columns = records.count_columns();
let mut totalw = None;
let totalh = records
.hint_count_rows()
.map(|count_rows| total_height(cfg, dims, count_rows));
let mut records_iter = records.iter_rows().into_iter();
let mut next_columns = records_iter.next();
if next_columns.is_none() {
return Ok(());
}
if cfg.get_margin().top.size > 0 {
totalw = Some(output_width(cfg, dims, count_columns));
print_margin_top(f, cfg, totalw.unwrap())?;
f.write_char('\n')?;
}
let mut row = 0;
let mut line = 0;
let mut is_prev_row_skipped = false;
let mut buf = None;
while let Some(columns) = next_columns {
let columns = columns.into_iter();
next_columns = records_iter.next();
let is_last_row = next_columns.is_none();
let height = dims.get_height(row);
let count_rows = convert_count_rows(row, is_last_row);
let has_horizontal = cfg.has_horizontal(row, count_rows);
let shape = (count_rows, count_columns);
if row > 0 && !is_prev_row_skipped && (has_horizontal || height > 0) {
f.write_char('\n')?;
}
if has_horizontal {
print_horizontal_line(f, cfg, line, totalh, dims, row, shape)?;
line += 1;
if height > 0 {
f.write_char('\n')?;
}
}
if height == 1 {
print_single_line_columns(f, columns, cfg, colors, dims, row, line, totalh, shape)?
} else if height > 0 {
if buf.is_none() {
buf = Some(Vec::with_capacity(count_columns));
}
let buf = buf.as_mut().unwrap();
print_multiline_columns(
f, columns, cfg, colors, dims, height, row, line, totalh, shape, buf,
)?;
buf.clear();
}
is_prev_row_skipped = height == 0 && !has_horizontal;
line += height;
row += 1;
}
if cfg.has_horizontal(row, row) {
f.write_char('\n')?;
let shape = (row, count_columns);
print_horizontal_line(f, cfg, line, totalh, dims, row, shape)?;
}
{
let margin = cfg.get_margin();
if margin.bottom.size > 0 {
let totalw = totalw.unwrap_or_else(|| output_width(cfg, dims, count_columns));
f.write_char('\n')?;
print_margin_bottom(f, cfg, totalw)?;
}
}
Ok(())
}
fn output_width<D: Dimension>(cfg: &SpannedConfig, d: D, count_columns: usize) -> usize {
let margin = cfg.get_margin();
total_width(cfg, &d, count_columns) + margin.left.size + margin.right.size
}
#[allow(clippy::too_many_arguments)]
fn print_horizontal_line<F: Write, D: Dimension>(
f: &mut F,
cfg: &SpannedConfig,
line: usize,
totalh: Option<usize>,
dimension: &D,
row: usize,
shape: (usize, usize),
) -> fmt::Result {
print_margin_left(f, cfg, line, totalh)?;
print_split_line(f, cfg, dimension, row, shape)?;
print_margin_right(f, cfg, line, totalh)?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn print_multiline_columns<'a, F, I, D, C>(
f: &mut F,
columns: I,
cfg: &'a SpannedConfig,
colors: &'a C,
dimension: &D,
height: usize,
row: usize,
line: usize,
totalh: Option<usize>,
shape: (usize, usize),
buf: &mut Vec<Cell<I::Item, &'a C::Color>>,
) -> fmt::Result
where
F: Write,
I: Iterator,
I::Item: AsRef<str>,
D: Dimension,
C: Colors,
{
collect_columns(buf, columns, cfg, colors, dimension, height, row);
print_columns_lines(f, buf, height, cfg, line, row, totalh, shape)?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn print_single_line_columns<F, I, D, C>(
f: &mut F,
columns: I,
cfg: &SpannedConfig,
colors: &C,
dims: &D,
row: usize,
line: usize,
totalh: Option<usize>,
shape: (usize, usize),
) -> fmt::Result
where
F: Write,
I: Iterator,
I::Item: AsRef<str>,
D: Dimension,
C: Colors,
{
print_margin_left(f, cfg, line, totalh)?;
for (col, cell) in columns.enumerate() {
let pos = (row, col);
let width = dims.get_width(col);
let color = colors.get_color(pos);
print_vertical_char(f, cfg, pos, 0, 1, shape.1)?;
print_single_line_column(f, cell.as_ref(), cfg, width, color, pos)?;
}
print_vertical_char(f, cfg, (row, shape.1), 0, 1, shape.1)?;
print_margin_right(f, cfg, line, totalh)?;
Ok(())
}
fn print_single_line_column<F: Write, C: ANSIFmt>(
f: &mut F,
text: &str,
cfg: &SpannedConfig,
width: usize,
color: Option<&C>,
pos: Position,
) -> fmt::Result {
let pos = pos.into();
let pad = cfg.get_padding(pos);
let pad_color = cfg.get_padding_color(pos);
let fmt = cfg.get_formatting(pos);
let space = cfg.get_justification(pos);
let space_color = cfg.get_justification_color(pos);
let (text, text_width) = if fmt.horizontal_trim && !text.is_empty() {
let text = string_trim(text);
let width = get_line_width(&text);
(text, width)
} else {
let text = Cow::Borrowed(text);
let width = get_text_width(&text);
(text, width)
};
let alignment = *cfg.get_alignment_horizontal(pos);
let available_width = width - pad.left.size - pad.right.size;
let (left, right) = calculate_indent(alignment, text_width, available_width);
print_padding(f, &pad.left, pad_color.left.as_ref())?;
print_indent(f, space, left, space_color)?;
print_text(f, &text, color)?;
print_indent(f, space, right, space_color)?;
print_padding(f, &pad.right, pad_color.right.as_ref())?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn print_columns_lines<T, F: Write, C: ANSIFmt>(
f: &mut F,
buf: &mut [Cell<T, C>],
height: usize,
cfg: &SpannedConfig,
line: usize,
row: usize,
totalh: Option<usize>,
shape: (usize, usize),
) -> fmt::Result {
for i in 0..height {
let exact_line = line + i;
print_margin_left(f, cfg, exact_line, totalh)?;
for (col, cell) in buf.iter_mut().enumerate() {
print_vertical_char(f, cfg, (row, col), i, height, shape.1)?;
cell.display(f)?;
}
print_vertical_char(f, cfg, (row, shape.1), i, height, shape.1)?;
print_margin_right(f, cfg, exact_line, totalh)?;
if i + 1 != height {
f.write_char('\n')?;
}
}
Ok(())
}
fn collect_columns<'a, I, D, C>(
buf: &mut Vec<Cell<I::Item, &'a C::Color>>,
iter: I,
cfg: &SpannedConfig,
colors: &'a C,
dimension: &D,
height: usize,
row: usize,
) where
I: Iterator,
I::Item: AsRef<str>,
C: Colors,
D: Dimension,
{
let iter = iter.enumerate().map(|(col, cell)| {
let pos = (row, col);
let width = dimension.get_width(col);
let color = colors.get_color(pos);
Cell::new(cell, width, height, cfg, color, pos)
});
buf.extend(iter);
}
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);
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_ansi_suffix(f)?;
}
Ok(())
}
fn print_grid_spanned<F, R, D, C>(
f: &mut F,
records: R,
cfg: &SpannedConfig,
dims: &D,
colors: &C,
) -> fmt::Result
where
F: Write,
R: Records,
<R::Iter as IntoRecords>::Cell: AsRef<str>,
D: Dimension,
C: Colors,
{
let count_columns = records.count_columns();
let total_width = total_width(cfg, dims, count_columns);
let margin = cfg.get_margin();
let total_width_with_margin = total_width + margin.left.size + margin.right.size;
let totalh = records
.hint_count_rows()
.map(|rows| total_height(cfg, dims, rows));
if margin.top.size > 0 {
print_margin_top(f, cfg, total_width_with_margin)?;
f.write_char('\n')?;
}
let mut buf = BTreeMap::new();
let mut records_iter = records.iter_rows().into_iter();
let mut next_columns = records_iter.next();
let mut need_new_line = false;
let mut line = 0;
let mut row = 0;
while let Some(columns) = next_columns {
let columns = columns.into_iter();
next_columns = records_iter.next();
let is_last_row = next_columns.is_none();
let height = dims.get_height(row);
let count_rows = convert_count_rows(row, is_last_row);
let shape = (count_rows, count_columns);
let has_horizontal = cfg.has_horizontal(row, count_rows);
if need_new_line && (has_horizontal || height > 0) {
f.write_char('\n')?;
need_new_line = false;
}
if has_horizontal {
print_margin_left(f, cfg, line, totalh)?;
print_split_line_spanned(f, &mut buf, cfg, dims, row, shape)?;
print_margin_right(f, cfg, line, totalh)?;
line += 1;
if height > 0 {
f.write_char('\n')?;
}
}
print_spanned_columns(
f, &mut buf, columns, cfg, colors, dims, height, row, line, totalh, shape,
)?;
if has_horizontal || height > 0 {
need_new_line = true;
}
line += height;
row += 1;
}
if row > 0 {
if cfg.has_horizontal(row, row) {
f.write_char('\n')?;
let shape = (row, count_columns);
print_horizontal_line(f, cfg, line, totalh, dims, row, shape)?;
}
if margin.bottom.size > 0 {
f.write_char('\n')?;
print_margin_bottom(f, cfg, total_width_with_margin)?;
}
}
Ok(())
}
fn print_split_line_spanned<S, F: Write, D: Dimension, C: ANSIFmt>(
f: &mut F,
buf: &mut BTreeMap<usize, (Cell<S, C>, usize, usize)>,
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 pos = (row, col);
if cfg.is_cell_covered_by_both_spans(pos) {
continue;
}
let width = dimension.get_width(col);
let mut col = col;
if cfg.is_cell_covered_by_row_span(pos) {
let (cell, _, _) = buf.get_mut(&col).unwrap();
cell.display(f)?;
let original_row = closest_visible_row(cfg, pos).unwrap();
if let Some(span) = cfg.get_column_span((original_row, col)) {
col += span - 1;
}
} else if width > 0 {
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_ansi_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 ANSIBuf>,
) -> fmt::Result {
if !cfg.has_vertical(pos.1, shape.1) {
return Ok(());
}
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(()),
}
}
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
fn print_spanned_columns<'a, F, I, D, C>(
f: &mut F,
buf: &mut BTreeMap<usize, (Cell<I::Item, &'a C::Color>, usize, usize)>,
iter: I,
cfg: &SpannedConfig,
colors: &'a C,
dimension: &D,
this_height: usize,
row: usize,
line: usize,
totalh: Option<usize>,
shape: (usize, usize),
) -> fmt::Result
where
F: Write,
I: Iterator,
I::Item: AsRef<str>,
D: Dimension,
C: Colors,
{
if this_height == 0 {
let mut skip = 0;
for (col, cell) in iter.enumerate() {
if skip > 0 {
skip -= 1;
continue;
}
if let Some((_, _, colspan)) = buf.get(&col) {
skip = *colspan - 1;
continue;
}
let pos = (row, col);
let rowspan = cfg.get_row_span(pos).unwrap_or(1);
if rowspan < 2 {
continue;
}
let height = if rowspan > 1 {
range_height(cfg, dimension, row, row + rowspan, shape.0)
} else {
this_height
};
let colspan = cfg.get_column_span(pos).unwrap_or(1);
skip = colspan - 1;
let width = if colspan > 1 {
range_width(cfg, dimension, col, col + colspan, shape.1)
} else {
dimension.get_width(col)
};
let color = colors.get_color(pos);
let cell = Cell::new(cell, width, height, cfg, color, pos);
buf.insert(col, (cell, rowspan, colspan));
}
buf.retain(|_, (_, rowspan, _)| {
*rowspan -= 1;
*rowspan != 0
});
return Ok(());
}
let mut skip = 0;
for (col, cell) in iter.enumerate() {
if skip > 0 {
skip -= 1;
continue;
}
if let Some((_, _, colspan)) = buf.get(&col) {
skip = *colspan - 1;
continue;
}
let pos = (row, col);
let colspan = cfg.get_column_span(pos).unwrap_or(1);
skip = colspan - 1;
let width = if colspan > 1 {
range_width(cfg, dimension, col, col + colspan, shape.1)
} else {
dimension.get_width(col)
};
let rowspan = cfg.get_row_span(pos).unwrap_or(1);
let height = if rowspan > 1 {
range_height(cfg, dimension, row, row + rowspan, shape.0)
} else {
this_height
};
let color = colors.get_color(pos);
let cell = Cell::new(cell, width, height, cfg, color, pos);
buf.insert(col, (cell, rowspan, colspan));
}
for i in 0..this_height {
let exact_line = line + i;
let cell_line = i;
print_margin_left(f, cfg, exact_line, totalh)?;
for (&col, (cell, _, _)) in buf.iter_mut() {
print_vertical_char(f, cfg, (row, col), cell_line, this_height, shape.1)?;
cell.display(f)?;
}
print_vertical_char(f, cfg, (row, shape.1), cell_line, this_height, shape.1)?;
print_margin_right(f, cfg, exact_line, totalh)?;
if i + 1 != this_height {
f.write_char('\n')?;
}
}
buf.retain(|_, (_, rowspan, _)| {
*rowspan -= 1;
*rowspan != 0
});
Ok(())
}
fn print_horizontal_border<F: Write>(
f: &mut F,
cfg: &SpannedConfig,
pos: Position,
width: usize,
c: char,
used_color: &Option<&ANSIBuf>,
) -> 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_ansi_suffix(f)?;
color.fmt_ansi_prefix(f)?;
f.write_char(c)?;
color.fmt_ansi_suffix(f)?;
clr.fmt_ansi_prefix(f)?;
}
None => {
color.fmt_ansi_prefix(f)?;
f.write_char(c)?;
color.fmt_ansi_suffix(f)?;
}
},
_ => f.write_char(c)?,
}
}
Ok(())
}
struct Cell<T, C> {
lines: LinesIter<T>,
width: usize,
indent_top: usize,
indent_left: Option<usize>,
alignh: AlignmentHorizontal,
fmt: Formatting,
pad: Sides<Indent>,
pad_color: Sides<Option<ANSIBuf>>,
color: Option<C>,
justification: (char, Option<ANSIBuf>),
}
impl<T, C> Cell<T, C>
where
T: AsRef<str>,
{
fn new(
text: T,
width: usize,
height: usize,
cfg: &SpannedConfig,
color: Option<C>,
pos: Position,
) -> Cell<T, C> {
let fmt = *cfg.get_formatting(pos.into());
let pad = cfg.get_padding(pos.into());
let pad_color = cfg.get_padding_color(pos.into()).clone();
let alignh = *cfg.get_alignment_horizontal(pos.into());
let alignv = *cfg.get_alignment_vertical(pos.into());
let justification = (
cfg.get_justification(pos.into()),
cfg.get_justification_color(pos.into()).cloned(),
);
let (count_lines, skip) = if fmt.vertical_trim {
let (len, top, _) = count_empty_lines(text.as_ref());
(len, top)
} else {
(count_lines(text.as_ref()), 0)
};
let indent_top = top_indent(&pad, alignv, count_lines, height);
let mut indent_left = None;
if !fmt.allow_lines_alignment {
let text_width = text_width(text.as_ref(), fmt.horizontal_trim);
let available = width - pad.left.size - pad.right.size;
indent_left = Some(calculate_indent(alignh, text_width, available).0);
}
let mut lines = LinesIter::new(text);
for _ in 0..skip {
let _ = lines.lines.next();
}
Self {
lines,
indent_left,
indent_top,
width,
alignh,
fmt,
pad,
pad_color,
color,
justification,
}
}
}
impl<T, C> Cell<T, C>
where
C: ANSIFmt,
{
fn display<F: Write>(&mut self, f: &mut F) -> fmt::Result {
if self.indent_top > 0 {
self.indent_top -= 1;
print_padding_n(f, &self.pad.top, self.pad_color.top.as_ref(), self.width)?;
return Ok(());
}
let line = match self.lines.lines.next() {
Some(line) => line,
None => {
let color = self.pad_color.bottom.as_ref();
print_padding_n(f, &self.pad.bottom, color, self.width)?;
return Ok(());
}
};
let line = if self.fmt.horizontal_trim && !line.is_empty() {
string_trim(&line)
} else {
line
};
let line_width = get_line_width(&line);
let available_width = self.width - self.pad.left.size - self.pad.right.size;
let (left, right) = if self.fmt.allow_lines_alignment {
calculate_indent(self.alignh, line_width, available_width)
} else {
let left = self.indent_left.expect("must be here");
(left, available_width - line_width - left)
};
let (justification, justification_color) =
(self.justification.0, self.justification.1.as_ref());
print_padding(f, &self.pad.left, self.pad_color.left.as_ref())?;
print_indent(f, justification, left, justification_color)?;
print_text(f, &line, self.color.as_ref())?;
print_indent(f, justification, right, justification_color)?;
print_padding(f, &self.pad.right, self.pad_color.right.as_ref())?;
Ok(())
}
}
struct LinesIter<C> {
_cell: C,
_text: &'static str,
lines: Lines<'static>,
}
impl<C> LinesIter<C> {
fn new(cell: C) -> Self
where
C: AsRef<str>,
{
let text = cell.as_ref();
let text = unsafe {
std::str::from_utf8_unchecked(std::slice::from_raw_parts(text.as_ptr(), text.len()))
};
let lines = get_lines(text);
Self {
_cell: cell,
_text: text,
lines,
}
}
}
fn print_text<F: Write>(f: &mut F, text: &str, clr: Option<impl ANSIFmt>) -> fmt::Result {
match clr {
Some(color) => {
color.fmt_ansi_prefix(f)?;
f.write_str(text)?;
color.fmt_ansi_suffix(f)
}
None => f.write_str(text),
}
}
fn prepare_coloring<'a, F: Write>(
f: &mut F,
clr: Option<&'a ANSIBuf>,
used_color: &mut Option<&'a ANSIBuf>,
) -> fmt::Result {
match clr {
Some(clr) => match used_color.as_mut() {
Some(used_clr) => {
if **used_clr != *clr {
used_clr.fmt_ansi_suffix(f)?;
clr.fmt_ansi_prefix(f)?;
*used_clr = clr;
}
}
None => {
clr.fmt_ansi_prefix(f)?;
*used_color = Some(clr);
}
},
None => {
if let Some(clr) = used_color.take() {
clr.fmt_ansi_suffix(f)?
}
}
}
Ok(())
}
fn top_indent(
padding: &Sides<Indent>,
alignment: AlignmentVertical,
cell_height: usize,
available: usize,
) -> usize {
let height = available - padding.top.size;
let indent = indent_from_top(alignment, height, cell_height);
indent + padding.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 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
.lookup_vertical_char(pos, line, count_lines)
.unwrap_or(symbol);
let color = cfg
.get_vertical_color(pos, count_columns)
.or_else(|| cfg.lookup_vertical_color(pos, line, count_lines));
match color {
Some(clr) => {
clr.fmt_ansi_prefix(f)?;
f.write_char(symbol)?;
clr.fmt_ansi_suffix(f)?;
}
None => f.write_char(symbol)?,
}
Ok(())
}
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: Option<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: Option<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<&ANSIBuf>,
line: usize,
height: Option<usize>,
) -> fmt::Result {
if indent.size == 0 {
return Ok(());
}
match offset {
Offset::Begin(mut offset) => {
if let Some(max) = height {
offset = cmp::min(offset, max);
}
if line >= offset {
print_indent(f, indent.fill, indent.size, color)?;
} else {
repeat_char(f, ' ', indent.size)?;
}
}
Offset::End(mut offset) => {
if let Some(max) = height {
offset = cmp::min(offset, max);
let pos = max - offset;
if line >= pos {
repeat_char(f, ' ', indent.size)?;
} else {
print_indent(f, indent.fill, indent.size, color)?;
}
} 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<&ANSIBuf>,
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_padding<F: Write>(f: &mut F, pad: &Indent, color: Option<&ANSIBuf>) -> fmt::Result {
print_indent(f, pad.fill, pad.size, color)
}
fn print_padding_n<F: Write>(
f: &mut F,
pad: &Indent,
color: Option<&ANSIBuf>,
n: usize,
) -> fmt::Result {
print_indent(f, pad.fill, n, color)
}
fn print_indent<F: Write>(f: &mut F, c: char, n: usize, color: Option<&ANSIBuf>) -> fmt::Result {
if n == 0 {
return Ok(());
}
match color {
Some(color) => {
color.fmt_ansi_prefix(f)?;
repeat_char(f, c, n)?;
color.fmt_ansi_suffix(f)
}
None => repeat_char(f, c, n),
}
}
fn range_width<D>(cfg: &SpannedConfig, dims: D, start: usize, end: usize, max: usize) -> usize
where
D: Dimension,
{
let count_borders = count_verticals_in_range(cfg, start, end, max);
let range_width = (start..end).map(|col| dims.get_width(col)).sum::<usize>();
count_borders + range_width
}
fn range_height<D>(cfg: &SpannedConfig, dims: D, from: usize, end: usize, max: usize) -> usize
where
D: Dimension,
{
let count_borders = count_horizontals_in_range(cfg, from, end, max);
let range_width = (from..end).map(|col| dims.get_height(col)).sum::<usize>();
count_borders + range_width
}
fn count_horizontals_in_range(cfg: &SpannedConfig, from: usize, end: usize, max: usize) -> usize {
(from + 1..end)
.map(|i| cfg.has_horizontal(i, max) as usize)
.sum()
}
fn count_verticals_in_range(cfg: &SpannedConfig, start: usize, end: usize, max: usize) -> usize {
(start..end)
.skip(1)
.map(|i| cfg.has_vertical(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;
}
}
fn convert_count_rows(row: usize, is_last: bool) -> usize {
if is_last {
row + 1
} else {
row + 2
}
}
fn string_trim(text: &str) -> Cow<'_, str> {
#[cfg(feature = "ansi")]
{
ansi_str::AnsiStr::ansi_trim(text)
}
#[cfg(not(feature = "ansi"))]
{
text.trim().into()
}
}
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 count_empty_lines(cell: &str) -> (usize, usize, usize) {
let mut len = 0;
let mut top = 0;
let mut bottom = 0;
let mut top_check = true;
for line in get_lines(cell) {
let is_empty = line.trim().is_empty();
if top_check {
if is_empty {
top += 1;
} else {
len = 1;
top_check = false;
}
continue;
}
if is_empty {
bottom += 1;
} else {
len += bottom + 1;
bottom = 0;
}
}
(len, top, bottom)
}
fn text_width(text: &str, trim: bool) -> usize {
if trim {
get_lines(text)
.map(|line| get_line_width(line.trim()))
.max()
.unwrap_or(0)
} else {
get_text_width(text)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn vertical_alignment_test() {
use AlignmentVertical::*;
assert_eq!(indent_from_top(Bottom, 1, 1), 0);
assert_eq!(indent_from_top(Top, 1, 1), 0);
assert_eq!(indent_from_top(Center, 1, 1), 0);
assert_eq!(indent_from_top(Bottom, 3, 1), 2);
assert_eq!(indent_from_top(Top, 3, 1), 0);
assert_eq!(indent_from_top(Center, 3, 1), 1);
assert_eq!(indent_from_top(Center, 4, 1), 1);
}
#[test]
fn count_empty_lines_test() {
assert_eq!(count_empty_lines("\n\nsome text\n\n\n"), (1, 2, 3));
assert_eq!(count_empty_lines("\n\nsome\ntext\n\n\n"), (2, 2, 3));
assert_eq!(count_empty_lines("\n\nsome\nsome\ntext\n\n\n"), (3, 2, 3));
assert_eq!(count_empty_lines("\n\n\n\n"), (0, 5, 0));
}
}