use std::cmp;
use crate::{
grid::{
config::{AlignmentVertical, ColoredConfig, Entity, Offset, Position, Sides},
dimension::{CompleteDimension, Dimension, Estimate},
records::{
vec_records::{Text, VecRecords},
ExactRecords, Records,
},
util::string::{get_char_width, get_line_width},
},
settings::{object::Columns, style::LineText, Alignment, Color, Padding, TableOption},
};
#[derive(Debug, Clone)]
pub struct RowNames {
names: Vec<String>,
colors: Option<ListValue<Color>>,
alignments: ListValue<Alignment>,
paddings: ListValue<Padding>,
line: usize,
}
impl RowNames {
pub fn new<I>(names: I) -> Self
where
I: IntoIterator,
I::Item: AsRef<str>,
{
let names = collect_first_lines(names);
Self {
names,
paddings: ListValue::Static(Padding::zero()),
alignments: ListValue::Static(Alignment::left()),
colors: Default::default(),
line: 0,
}
}
pub fn color<T>(self, color: T) -> Self
where
T: Into<ListValue<Color>>,
{
Self {
names: self.names,
line: self.line,
paddings: self.paddings,
alignments: self.alignments,
colors: Some(color.into()),
}
}
pub fn line(self, i: usize) -> Self {
Self {
names: self.names,
paddings: self.paddings,
alignments: self.alignments,
colors: self.colors,
line: i,
}
}
pub fn alignment<T>(self, alignment: T) -> Self
where
T: Into<ListValue<Alignment>>,
{
Self {
names: self.names,
paddings: self.paddings,
line: self.line,
colors: self.colors,
alignments: alignment.into(),
}
}
pub fn padding<T>(self, padding: T) -> Self
where
T: Into<ListValue<Padding>>,
{
Self {
names: self.names,
line: self.line,
colors: self.colors,
alignments: self.alignments,
paddings: padding.into(),
}
}
}
fn collect_first_lines<I>(names: I) -> Vec<String>
where
I: IntoIterator,
I::Item: AsRef<str>,
{
names
.into_iter()
.map(|name| name.as_ref().lines().next().unwrap_or("").to_string())
.collect::<Vec<_>>()
}
impl TableOption<VecRecords<Text<String>>, ColoredConfig, CompleteDimension> for RowNames {
fn change(
self,
records: &mut VecRecords<Text<String>>,
cfg: &mut ColoredConfig,
dims: &mut CompleteDimension,
) {
let count_rows = records.count_rows();
let count_columns = records.count_columns();
if count_columns == 0 || count_rows == 0 || self.line > count_rows {
return;
}
let mut names = self.names;
let size = records.count_rows();
ensure_vector_size(&mut names, size);
let alignment = convert_alignment_value(self.alignments)
.unwrap_or(ListValue::Static(AlignmentVertical::Top));
let info = RowInfo::new(names, self.colors, alignment, self.paddings, self.line);
set_row_text(records, dims, cfg, info);
}
fn hint_change(&self) -> Option<Entity> {
Some(Entity::Column(0))
}
}
#[derive(Debug, Clone)]
struct RowInfo {
names: Vec<String>,
colors: Option<ListValue<Color>>,
alignments: ListValue<AlignmentVertical>,
paddings: ListValue<Padding>,
line: usize,
}
impl RowInfo {
fn new(
names: Vec<String>,
colors: Option<ListValue<Color>>,
alignments: ListValue<AlignmentVertical>,
paddings: ListValue<Padding>,
line: usize,
) -> Self {
Self {
names,
colors,
alignments,
paddings,
line,
}
}
}
fn set_row_text(
records: &mut VecRecords<Text<String>>,
dims: &mut CompleteDimension,
cfg: &mut ColoredConfig,
info: RowInfo,
) {
dims.estimate(&*records, cfg);
let count_rows = info.names.len();
let mut heights = Vec::with_capacity(count_rows);
for (row, name) in info.names.iter().enumerate() {
let pad = Sides::from(info.paddings.get_or_else(row, Padding::zero));
let name_height = get_line_width(name) + pad.top.size + pad.bottom.size;
let row_height = dims.get_height(row);
let height = cmp::max(name_height, row_height);
heights.push(height);
}
let mut global_offset = 0;
for (row, (name, row_height)) in info.names.into_iter().zip(heights.iter()).enumerate() {
let color = get_color(&info.colors, row);
let alignment = info.alignments.get_or_else(row, || AlignmentVertical::Top);
let padding = Sides::from(info.paddings.get_or_else(row, Padding::zero));
let horizontal_pos = (row, info.line).into();
let top_horizontal = get_horizontal_width(cfg, horizontal_pos, count_rows);
let height = row_height - padding.top.size - padding.bottom.size;
let cell_indent = get_vertical_indent(&name, alignment, height);
let offset = global_offset + top_horizontal + cell_indent + padding.top.size;
dims.set_heights(heights.clone());
let linetext = create_line_text(&name, offset, color, Columns::one(info.line));
linetext.change(records, cfg, dims);
global_offset += row_height + top_horizontal;
}
dims.set_heights(heights);
}
fn ensure_vector_size(data: &mut Vec<String>, size: usize) {
match data.len().cmp(&size) {
cmp::Ordering::Equal => {}
cmp::Ordering::Less => {
let additional_size = size - data.len();
data.extend(std::iter::repeat_n(String::new(), additional_size));
}
cmp::Ordering::Greater => {
data.truncate(size);
}
}
}
fn create_line_text<T>(text: &str, offset: usize, color: Option<&Color>, line: T) -> LineText<T> {
let offset = Offset::Start(offset);
let mut btext = LineText::new(text, line).offset(offset);
if let Some(color) = color {
btext = btext.color(color.clone());
}
btext
}
fn get_color(colors: &Option<ListValue<Color>>, i: usize) -> Option<&Color> {
match colors {
Some(ListValue::List(list)) => list.get(i),
Some(ListValue::Static(color)) => Some(color),
None => None,
}
}
fn get_vertical_indent(text: &str, align: AlignmentVertical, available: usize) -> usize {
match align {
AlignmentVertical::Top => 0,
AlignmentVertical::Bottom => available - get_line_width(text),
AlignmentVertical::Center => (available - get_line_width(text)) / 2,
}
}
fn get_horizontal_width(cfg: &mut ColoredConfig, pos: Position, count_rows: usize) -> usize {
cfg.get_horizontal(pos, count_rows)
.map(get_char_width)
.unwrap_or(0)
}
fn convert_alignment_value<T>(value: ListValue<Alignment>) -> Option<ListValue<T>>
where
Option<T>: From<Alignment>,
{
match value {
ListValue::List(list) => {
let new = list
.iter()
.flat_map(|value| Option::from(*value))
.collect::<Vec<_>>();
if new.len() == list.len() {
Some(ListValue::List(new))
} else {
None
}
}
ListValue::Static(value) => Option::from(value).map(ListValue::Static),
}
}
#[derive(Debug, Clone)]
pub enum ListValue<T> {
List(Vec<T>),
Static(T),
}
impl<T> ListValue<T> {
fn get(&self, i: usize) -> Option<T>
where
T: Copy,
{
match self {
ListValue::List(list) => list.get(i).copied(),
ListValue::Static(alignment) => Some(*alignment),
}
}
fn get_or_else<F>(&self, i: usize, default: F) -> T
where
F: FnOnce() -> T,
T: Copy,
{
self.get(i).unwrap_or_else(default)
}
}
impl<T> From<T> for ListValue<T> {
fn from(value: T) -> Self {
Self::Static(value)
}
}
impl<T> From<Vec<T>> for ListValue<T> {
fn from(value: Vec<T>) -> Self {
Self::List(value)
}
}
impl<T> Default for ListValue<T>
where
T: Default,
{
fn default() -> Self {
Self::Static(T::default())
}
}