use super::theme;
use ratatui::{
buffer::Buffer,
layout::{Constraint, Rect},
style::Style,
text::Span,
widgets::Widget,
};
pub struct Column {
pub header: &'static str,
pub width: Constraint,
}
pub struct GridCell {
pub text: String,
pub style: Style,
}
pub struct GridRow {
pub cells: Vec<GridCell>,
}
pub struct GridTable<'a> {
pub columns: &'a [Column],
pub rows: &'a [GridRow],
pub selected: Option<usize>, pub header_style: Style,
pub separator_style: Style,
pub selected_style: Style,
}
impl<'a> Widget for GridTable<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.height < 3 || area.width < 4 {
return;
}
let widths = compute_widths(self.columns, area.width);
draw_box(buf, area, theme::grid_header());
let header_y = area.y + 1;
let mut x = area.x + 1;
for (col_idx, col) in self.columns.iter().enumerate() {
let w = widths[col_idx];
let text = truncate_pad(col.header, w.saturating_sub(1) as usize);
let span = Span::styled(format!(" {}", text), self.header_style);
buf.set_span(x, header_y, &span, w);
if col_idx < self.columns.len() - 1 {
let sep_x = x + w;
if sep_x < area.x + area.width - 1 {
buf.get_mut(sep_x, header_y)
.set_symbol("│")
.set_style(self.separator_style);
}
}
x += w + 1; }
let sep_y = area.y + 2;
if sep_y < area.y + area.height - 1 {
x = area.x + 1;
for col_idx in 0..self.columns.len() {
let w = widths[col_idx];
for fill_x in x..(x + w).min(area.x + area.width - 1) {
buf.get_mut(fill_x, sep_y)
.set_symbol("─")
.set_style(self.separator_style);
}
if col_idx < self.columns.len() - 1 {
let sep_x = x + w;
if sep_x < area.x + area.width - 1 {
buf.get_mut(sep_x, sep_y)
.set_symbol("┼")
.set_style(self.separator_style);
}
}
x += w + 1;
}
buf.get_mut(area.x, sep_y)
.set_symbol("├")
.set_style(self.separator_style);
if area.x + area.width > 0 {
buf.get_mut(area.x + area.width - 1, sep_y)
.set_symbol("┤")
.set_style(self.separator_style);
}
}
let data_start_y = area.y + 3; let max_rows = (area.height as usize).saturating_sub(4);
for (row_idx, row) in self.rows.iter().take(max_rows).enumerate() {
let row_y = data_start_y + row_idx as u16;
if row_y >= area.y + area.height - 1 {
break;
}
let is_selected = self.selected == Some(row_idx);
let row_bg = if is_selected {
self.selected_style
} else {
Style::default().bg(theme::bg_color())
};
for fill_x in (area.x + 1)..(area.x + area.width - 1) {
buf.get_mut(fill_x, row_y).set_style(row_bg);
}
if is_selected {
buf.get_mut(area.x, row_y)
.set_symbol("▌")
.set_style(Style::default().fg(theme::hot_pink()));
}
x = area.x + 1;
for (col_idx, _col) in self.columns.iter().enumerate() {
let w = widths[col_idx];
if let Some(cell) = row.cells.get(col_idx) {
let text = truncate_pad(&cell.text, w.saturating_sub(1) as usize);
let cell_style = if is_selected {
cell.style.bg(theme::overlay_color())
} else {
cell.style
};
let span = Span::styled(format!(" {}", text), cell_style);
buf.set_span(x, row_y, &span, w);
}
if col_idx < self.columns.len() - 1 {
let sep_x = x + w;
if sep_x < area.x + area.width - 1 {
buf.get_mut(sep_x, row_y)
.set_symbol("│")
.set_style(self.separator_style);
}
}
x += w + 1;
}
}
}
}
fn truncate_pad(text: &str, max: usize) -> String {
if max == 0 {
return String::new();
}
let mut result = String::new();
let mut width = 0usize;
for ch in text.chars() {
let ch_w = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1);
if width + ch_w > max {
if max >= 1 {
result.push('…');
}
break;
}
result.push(ch);
width += ch_w;
}
while width < max {
result.push(' ');
width += 1;
}
result
}
fn draw_box(buf: &mut Buffer, area: Rect, style: Style) {
buf.get_mut(area.x, area.y).set_symbol("┌").set_style(style);
buf.get_mut(area.x + area.width - 1, area.y)
.set_symbol("┐")
.set_style(style);
buf.get_mut(area.x, area.y + area.height - 1)
.set_symbol("└")
.set_style(style);
buf.get_mut(area.x + area.width - 1, area.y + area.height - 1)
.set_symbol("┘")
.set_style(style);
for x in (area.x + 1)..(area.x + area.width - 1) {
buf.get_mut(x, area.y).set_symbol("─").set_style(style);
buf.get_mut(x, area.y + area.height - 1)
.set_symbol("─")
.set_style(style);
}
for y in (area.y + 1)..(area.y + area.height - 1) {
buf.get_mut(area.x, y).set_symbol("│").set_style(style);
buf.get_mut(area.x + area.width - 1, y)
.set_symbol("│")
.set_style(style);
}
}
fn compute_widths(columns: &[Column], total_width: u16) -> Vec<u16> {
let separators = columns.len().saturating_sub(1) as u16;
let available = total_width.saturating_sub(2 + separators);
let mut widths = Vec::with_capacity(columns.len());
let mut used = 0u16;
let mut min_cols: Vec<(usize, u16)> = Vec::new();
for (i, col) in columns.iter().enumerate() {
match col.width {
Constraint::Length(n) => {
widths.push(n);
used += n;
min_cols.push((i, n));
}
_ => {
widths.push(0); }
}
}
let remaining = available.saturating_sub(used);
let flexible_count = columns
.iter()
.filter(|c| !matches!(c.width, Constraint::Length(_)))
.count();
let per_flex = if flexible_count > 0 {
remaining / flexible_count as u16
} else {
0
};
for (i, col) in columns.iter().enumerate() {
match col.width {
Constraint::Percentage(p) => {
widths[i] = (available * p / 100).min(remaining);
}
Constraint::Min(n) => {
widths[i] = per_flex.max(n);
}
_ => {}
}
}
widths
}