use crate::component::{Component, EventCx, LayoutCx, MeasureCx};
use crate::event::Event;
use crate::geom::{Rect, Size};
use crate::layout::Constraint;
use crate::render::RenderCx;
use crate::style::Style;
#[derive(Default)]
pub struct TableColumn {
pub title: String,
pub width: u16,
pub align: crate::style::TextAlign,
}
pub struct Table {
columns: Vec<TableColumn>,
rows: Vec<Vec<String>>,
selected: Option<usize>,
scroll_offset: usize,
rect: Rect,
style: Style,
header_style: Style,
select_style: Style,
}
impl Table {
pub fn new() -> Self {
Self {
columns: Vec::new(),
rows: Vec::new(),
selected: None,
scroll_offset: 0,
rect: Rect::default(),
style: Style::default(),
header_style: Style::default().bold(),
select_style: Style::default(),
}
}
pub fn columns(mut self, columns: Vec<TableColumn>) -> Self {
self.columns = columns;
self
}
pub fn rows(mut self, rows: Vec<Vec<String>>) -> Self {
self.rows = rows;
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn header_style(mut self, style: Style) -> Self {
self.header_style = style;
self
}
pub fn select_style(mut self, style: Style) -> Self {
self.select_style = style;
self
}
pub fn selected(&self) -> Option<usize> {
self.selected
}
pub fn set_selected(&mut self, index: Option<usize>, cx: &mut EventCx) {
self.selected = index;
cx.invalidate_paint();
}
pub fn row_count(&self) -> usize {
self.rows.len()
}
pub fn sort_by_column(&mut self, col: usize, cx: &mut EventCx) {
if col < self.columns.len() {
self.rows.sort_by(|a, b| {
let ca = a.get(col).map(|s| s.as_str()).unwrap_or("");
let cb = b.get(col).map(|s| s.as_str()).unwrap_or("");
ca.cmp(cb)
});
cx.invalidate_paint();
}
}
pub fn set_column_width(&mut self, col: usize, width: u16, cx: &mut EventCx) {
if col < self.columns.len() {
self.columns[col].width = width.max(3);
cx.invalidate_layout();
}
}
pub fn adjust_column_width(&mut self, col: usize, delta: i16, cx: &mut EventCx) {
if col < self.columns.len() {
let new = (self.columns[col].width as i16 + delta).max(3) as u16;
self.columns[col].width = new;
cx.invalidate_layout();
}
}
fn visible_rows(&self, height: u16) -> usize {
let usable = height.saturating_sub(2); usable as usize
}
}
impl Component for Table {
fn render(&self, cx: &mut RenderCx) {
let columns = &self.columns;
if columns.is_empty() {
return;
}
let col_count = columns.len();
let visible = self.visible_rows(cx.rect.height);
let start_row = self.scroll_offset;
let end_row = (start_row + visible).min(self.rows.len());
cx.set_style(self.header_style.clone());
for (i, col) in columns.iter().enumerate() {
let text = truncate_to_width(&col.title, col.width, col.align);
cx.text(&text);
if i < col_count - 1 {
cx.text("│");
}
}
cx.line("");
cx.set_style(self.style.clone());
for (i, col) in columns.iter().enumerate() {
let sep = "─".repeat(col.width as usize);
cx.text(&sep);
if i < col_count - 1 {
cx.text("┼");
}
}
cx.line("");
for row_idx in start_row..end_row {
let is_selected = self.selected == Some(row_idx);
if is_selected {
cx.set_style(self.select_style.clone());
} else {
cx.set_style(self.style.clone());
}
let row = &self.rows[row_idx];
for (i, col) in columns.iter().enumerate() {
let cell_text = row.get(i).map(|s| s.as_str()).unwrap_or("");
let text = truncate_to_width(cell_text, col.width, col.align);
cx.text(&text);
if i < col_count - 1 {
cx.text("│");
}
}
cx.line("");
}
}
fn measure(&self, _constraint: Constraint, _cx: &mut MeasureCx) -> Size {
let col_count = self.columns.len() as u16;
if col_count == 0 {
return Size { width: 0, height: 0 };
}
let width: u16 = self.columns.iter().map(|c| c.width).sum::<u16>()
+ col_count.saturating_sub(1);
let visible = self.rows.len().min(u16::MAX as usize) as u16;
let height = 2u16.saturating_add(visible);
Size { width, height }
}
fn event(&mut self, event: &Event, cx: &mut EventCx) {
if matches!(event, Event::Focus | Event::Blur | Event::Tick) {
return;
}
if self.rows.is_empty() {
return;
}
if let Event::Key(key_event) = event {
match &key_event.key {
crate::event::Key::Up => {
let new_idx = match self.selected {
Some(i) if i > 0 => i - 1,
_ => 0,
};
self.selected = Some(new_idx);
self.scroll_to_visible(new_idx);
cx.invalidate_paint();
}
crate::event::Key::Down => {
let max = self.rows.len() - 1;
let new_idx = match self.selected {
Some(i) if i < max => i + 1,
Some(i) => i,
None => 0,
};
self.selected = Some(new_idx);
self.scroll_to_visible(new_idx);
cx.invalidate_paint();
}
_ => {}
}
}
}
fn layout(&mut self, rect: Rect, _cx: &mut LayoutCx) {
self.rect = rect;
}
fn focusable(&self) -> bool {
false
}
fn style(&self) -> Style {
self.style.clone()
}
}
impl Table {
fn scroll_to_visible(&mut self, idx: usize) {
let visible = self.visible_rows(self.rect.height);
if visible == 0 {
return;
}
if idx < self.scroll_offset {
self.scroll_offset = idx;
} else if idx >= self.scroll_offset + visible {
self.scroll_offset = idx.saturating_sub(visible.saturating_sub(1));
}
}
}
fn truncate_to_width(text: &str, max_width: u16, align: crate::style::TextAlign) -> String {
let mut result = String::new();
let mut used: u16 = 0;
for ch in text.chars() {
let w = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0) as u16;
if used + w > max_width {
break;
}
used += w;
result.push(ch);
}
let padding = max_width.saturating_sub(used);
match align {
crate::style::TextAlign::Left => {
while used < max_width { result.push(' '); used += 1; }
}
crate::style::TextAlign::Center => {
let left = padding / 2;
let right = padding - left;
let mut s = String::new();
for _ in 0..left { s.push(' '); }
s.push_str(&result);
for _ in 0..right { s.push(' '); }
result = s;
}
crate::style::TextAlign::Right => {
let mut s = String::new();
for _ in 0..padding { s.push(' '); }
s.push_str(&result);
result = s;
}
}
result
}