use crate::text::{TabPolicy, clip_to_cells_cow, fit_to_cells};
use crate::widgets::{BorderChars, Widget};
use crate::{Alignment, Color, ColorPair, Result, Window};
#[derive(Debug, Clone)]
pub struct TableColumn {
header: String,
width: u16,
alignment: Alignment,
header_alignment: Option<Alignment>,
header_color: Option<ColorPair>,
cell_color: Option<ColorPair>,
}
impl TableColumn {
pub fn new(header: impl Into<String>) -> Self {
Self {
header: header.into(),
width: 10,
alignment: Alignment::Left,
header_alignment: None,
header_color: None,
cell_color: None,
}
}
pub fn width(&self) -> u16 {
self.width
}
pub fn with_width(mut self, width: u16) -> Self {
self.width = width;
self
}
pub fn with_alignment(mut self, alignment: Alignment) -> Self {
self.alignment = alignment;
self
}
pub fn with_header_alignment(mut self, alignment: Alignment) -> Self {
self.header_alignment = Some(alignment);
self
}
pub fn with_header_color(mut self, color: ColorPair) -> Self {
self.header_color = Some(color);
self
}
pub fn with_cell_color(mut self, color: ColorPair) -> Self {
self.cell_color = Some(color);
self
}
}
#[derive(Debug, Clone)]
pub struct Table {
x: u16,
y: u16,
width: u16,
height: u16,
columns: Vec<TableColumn>,
rows: Vec<Vec<String>>,
show_border: bool,
border_chars: BorderChars,
border_color: ColorPair,
show_header: bool,
show_header_separator: bool,
header_color: ColorPair,
cell_color: ColorPair,
grid_color: ColorPair,
scroll_x: u16, scroll_y: u16,
show_column_separators: bool,
column_separator: &'static str, }
impl Default for Table {
fn default() -> Self {
Self::new(0, 0, 0, 0)
}
}
impl Table {
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
Self {
x,
y,
width,
height,
columns: Vec::new(),
rows: Vec::new(),
show_border: true,
border_chars: BorderChars::single_line(),
border_color: ColorPair::new(Color::LightGray, Color::Transparent),
show_header: true,
show_header_separator: true,
header_color: ColorPair::new(Color::White, Color::Transparent),
cell_color: ColorPair::new(Color::LightGray, Color::Transparent),
grid_color: ColorPair::new(Color::DarkGray, Color::Transparent),
scroll_x: 0,
scroll_y: 0,
show_column_separators: true,
column_separator: "│",
}
}
pub fn with_position_and_size(mut self, x: u16, y: u16, width: u16, height: u16) -> Self {
self.x = x;
self.y = y;
self.width = width;
self.height = height;
self
}
pub fn with_columns(mut self, columns: Vec<TableColumn>) -> Self {
self.columns = columns;
self
}
pub fn with_rows(mut self, rows: Vec<Vec<String>>) -> Self {
self.rows = rows;
self
}
pub fn with_border(mut self, enabled: bool) -> Self {
self.show_border = enabled;
self
}
pub fn with_border_chars(mut self, chars: BorderChars) -> Self {
self.border_chars = chars;
self
}
pub fn with_border_color(mut self, color: ColorPair) -> Self {
self.border_color = color;
self
}
pub fn with_header(mut self, enabled: bool) -> Self {
self.show_header = enabled;
self
}
pub fn with_header_separator(mut self, enabled: bool) -> Self {
self.show_header_separator = enabled;
self
}
pub fn with_header_color(mut self, color: ColorPair) -> Self {
self.header_color = color;
self
}
pub fn with_cell_color(mut self, color: ColorPair) -> Self {
self.cell_color = color;
self
}
pub fn with_grid_color(mut self, color: ColorPair) -> Self {
self.grid_color = color;
self
}
pub fn with_scroll(mut self, scroll_x: u16, scroll_y: u16) -> Self {
self.scroll_x = scroll_x;
self.scroll_y = scroll_y;
self
}
pub fn with_column_separators(mut self, enabled: bool) -> Self {
self.show_column_separators = enabled;
self
}
pub fn set_scroll(&mut self, scroll_x: u16, scroll_y: u16) {
self.scroll_x = scroll_x;
self.scroll_y = scroll_y;
}
pub fn scroll_x(&self) -> u16 {
self.scroll_x
}
pub fn scroll_y(&self) -> u16 {
self.scroll_y
}
pub fn row_count(&self) -> usize {
self.rows.len()
}
pub fn column_count(&self) -> usize {
self.columns.len()
}
pub fn content_area(&self) -> (u16, u16, u16, u16) {
if !self.show_border {
return (self.x, self.y, self.width, self.height);
}
let x = self.x.saturating_add(1);
let y = self.y.saturating_add(1);
let w = self.width.saturating_sub(2);
let h = self.height.saturating_sub(2);
(x, y, w, h)
}
fn body_area(&self) -> (u16, u16, u16, u16) {
let (cx, cy, cw, ch) = self.content_area();
let mut body_y = cy;
let mut body_h = ch;
if self.show_header && ch > 0 {
body_y = body_y.saturating_add(1);
body_h = body_h.saturating_sub(1);
if self.show_header_separator && body_h > 0 {
body_y = body_y.saturating_add(1);
body_h = body_h.saturating_sub(1);
}
}
(cx, body_y, cw, body_h)
}
fn available_body_rows(&self) -> usize {
let (_, _, _, body_h) = self.body_area();
body_h as usize
}
fn total_table_content_width(&self) -> u16 {
if self.columns.is_empty() {
return 0;
}
let cols_w: u16 = self.columns.iter().map(|c| c.width).sum();
if self.show_column_separators {
cols_w.saturating_add((self.columns.len().saturating_sub(1)) as u16)
} else {
cols_w
}
}
fn draw_border(&self, window: &mut dyn Window) -> Result<()> {
if !self.show_border || self.width == 0 || self.height == 0 {
return Ok(());
}
let bc = &self.border_chars;
let x0 = self.x;
let y0 = self.y;
let x1 = self.x.saturating_add(self.width.saturating_sub(1));
let y1 = self.y.saturating_add(self.height.saturating_sub(1));
window.write_str_colored(y0, x0, &bc.top_left.to_string(), self.border_color)?;
if self.width > 2 {
let horiz = bc.horizontal.to_string().repeat((self.width - 2) as usize);
window.write_str_colored(y0, x0 + 1, &horiz, self.border_color)?;
}
window.write_str_colored(y0, x1, &bc.top_right.to_string(), self.border_color)?;
if self.height > 2 {
for yy in (y0 + 1)..y1 {
window.write_str_colored(yy, x0, &bc.vertical.to_string(), self.border_color)?;
window.write_str_colored(yy, x1, &bc.vertical.to_string(), self.border_color)?;
}
}
if self.height > 1 {
window.write_str_colored(y1, x0, &bc.bottom_left.to_string(), self.border_color)?;
if self.width > 2 {
let horiz = bc.horizontal.to_string().repeat((self.width - 2) as usize);
window.write_str_colored(y1, x0 + 1, &horiz, self.border_color)?;
}
window.write_str_colored(y1, x1, &bc.bottom_right.to_string(), self.border_color)?;
}
Ok(())
}
fn draw_header(&self, window: &mut dyn Window) -> Result<()> {
if !self.show_header {
return Ok(());
}
let (cx, cy, cw, ch) = self.content_area();
if cw == 0 || ch == 0 {
return Ok(());
}
if self.columns.is_empty() {
return Ok(());
}
self.draw_row_cells(
window,
cy,
cx,
cw,
|col_idx| Some(self.columns[col_idx].header.as_str()),
true,
)?;
if self.show_header_separator && ch >= 2 {
let sep_y = cy + 1;
self.draw_horizontal_rule(window, sep_y, cx, cw)?;
}
Ok(())
}
fn draw_horizontal_rule(&self, window: &mut dyn Window, y: u16, x: u16, w: u16) -> Result<()> {
if w == 0 {
return Ok(());
}
let bc = &self.border_chars;
let rule_ch = bc.horizontal.to_string();
let sep_ch = if self.show_column_separators {
bc.intersect.to_string()
} else {
bc.horizontal.to_string()
};
let mut buf = String::with_capacity(w as usize);
let visible_start = self.scroll_x;
let visible_end = self.scroll_x.saturating_add(w);
let mut next_separator = if self.show_column_separators {
self.visible_separator_positions(visible_start, visible_end)
} else {
Vec::new().into_iter()
}
.peekable();
for content_col in visible_start..visible_end {
if next_separator.peek().copied() == Some(content_col) {
buf.push_str(&sep_ch);
next_separator.next();
} else {
buf.push_str(&rule_ch);
}
}
window.write_str_colored(y, x, &buf, self.grid_color)?;
Ok(())
}
fn visible_separator_positions(
&self,
visible_start: u16,
visible_end: u16,
) -> std::vec::IntoIter<u16> {
let mut positions = Vec::new();
if !self.show_column_separators {
return positions.into_iter();
}
let mut col_start: u16 = 0;
for (i, col) in self.columns.iter().enumerate() {
let col_end = col_start.saturating_add(col.width);
if i + 1 >= self.columns.len() {
break;
}
if col_end >= visible_start && col_end < visible_end {
positions.push(col_end);
}
col_start = col_end.saturating_add(1);
}
positions.into_iter()
}
fn draw_body(&self, window: &mut dyn Window) -> Result<()> {
let (bx, by, bw, bh) = self.body_area();
if bw == 0 || bh == 0 {
return Ok(());
}
if self.columns.is_empty() {
return Ok(());
}
let clear_line = " ".repeat(bw as usize);
for row in 0..bh {
window.write_str(by + row, bx, &clear_line)?;
}
let start_row = self.scroll_y as usize;
let max_rows = self.available_body_rows();
for visible_idx in 0..max_rows {
let row_idx = start_row + visible_idx;
if row_idx >= self.rows.len() {
break;
}
let y = by + (visible_idx as u16);
let row_ref = &self.rows[row_idx];
self.draw_row_cells(
window,
y,
bx,
bw,
|col_idx| row_ref.get(col_idx).map(|s| s.as_str()),
false,
)?;
}
Ok(())
}
fn draw_row_cells<'a, F>(
&self,
window: &mut dyn Window,
y: u16,
content_x: u16,
content_w: u16,
cell_text: F,
is_header: bool,
) -> Result<()>
where
F: Fn(usize) -> Option<&'a str>,
{
if content_w == 0 {
return Ok(());
}
let total_w = self.total_table_content_width();
if total_w == 0 {
return Ok(());
}
let base_color = if is_header {
self.header_color
} else {
self.cell_color
};
let clear_line = " ".repeat(content_w as usize);
window.write_str_colored(y, content_x, &clear_line, base_color)?;
let visible_start = self.scroll_x;
let visible_end = self.scroll_x.saturating_add(content_w);
let mut col_start: u16 = 0;
for (i, col) in self.columns.iter().enumerate() {
let col_end = col_start.saturating_add(col.width);
if col_end > visible_start && col_start < visible_end {
let raw = cell_text(i).unwrap_or("");
let align = if is_header {
col.header_alignment.unwrap_or(col.alignment)
} else {
col.alignment
};
let s = fit_to_cells(raw, col.width, TabPolicy::SingleCell, true);
let cell = align_to_width(&s, col.width, align);
let start_skip = visible_start.saturating_sub(col_start);
let draw_x = content_x + col_start.saturating_sub(visible_start);
let draw_w = col_end
.min(visible_end)
.saturating_sub(visible_start.max(col_start));
let visible_cell = if start_skip == 0 {
clip_to_cells_cow(&cell, draw_w, TabPolicy::SingleCell)
} else {
let shifted = drop_leading_cells(&cell, start_skip);
std::borrow::Cow::Owned(
clip_to_cells_cow(&shifted, draw_w, TabPolicy::SingleCell).into_owned(),
)
};
window.write_str_colored(y, draw_x, &visible_cell, base_color)?;
}
if self.show_column_separators && i + 1 < self.columns.len() {
let sep_x_in_row = col_end;
if sep_x_in_row >= visible_start && sep_x_in_row < visible_end {
let vx = sep_x_in_row - visible_start;
window.write_str_colored(
y,
content_x + vx,
self.column_separator,
self.grid_color,
)?;
}
col_start = col_end.saturating_add(1);
} else {
col_start = col_end;
}
}
Ok(())
}
}
impl Widget for Table {
fn draw(&self, window: &mut dyn Window) -> Result<()> {
if self.width == 0 || self.height == 0 {
return Ok(());
}
self.draw_border(window)?;
self.draw_header(window)?;
self.draw_body(window)?;
Ok(())
}
fn get_size(&self) -> (u16, u16) {
(self.width, self.height)
}
fn get_position(&self) -> (u16, u16) {
(self.x, self.y)
}
}
fn align_to_width(s: &str, width: u16, align: Alignment) -> String {
let text_w = crate::text::cell_width(s, TabPolicy::SingleCell) as u16;
if text_w >= width {
return s.to_string();
}
let pad = width - text_w;
let mut out = String::with_capacity(s.len() + pad as usize);
match align {
Alignment::Left => {
out.push_str(s);
out.extend(std::iter::repeat(' ').take(pad as usize));
}
Alignment::Right => {
out.extend(std::iter::repeat(' ').take(pad as usize));
out.push_str(s);
}
Alignment::Center => {
let left = pad / 2;
let right = pad - left;
out.extend(std::iter::repeat(' ').take(left as usize));
out.push_str(s);
out.extend(std::iter::repeat(' ').take(right as usize));
}
};
out
}
fn drop_leading_cells(s: &str, cells: u16) -> String {
if cells == 0 {
return s.to_string();
}
let mut acc: u16 = 0;
let mut start_char = 0usize;
for (i, ch) in s.chars().enumerate() {
let w = crate::text::cell_width_char(ch);
if w == 0 {
continue;
}
if acc.saturating_add(w) > cells {
start_char = i;
break;
}
acc = acc.saturating_add(w);
start_char = i + 1;
}
s.chars().skip(start_char).collect::<String>()
}