use crate::components::navigation::SelectionState;
use crate::components::{Box as TinkBox, Line, Span, Text};
use crate::core::{Color, Element, FlexDirection, Style};
#[derive(Debug, Clone)]
pub struct Cell {
pub content: Line,
pub style: Option<Style>,
}
impl Cell {
pub fn new(content: impl Into<String>) -> Self {
Self {
content: Line::raw(content),
style: None,
}
}
pub fn from_line(line: Line) -> Self {
Self {
content: line,
style: None,
}
}
pub fn from_spans(spans: Vec<Span>) -> Self {
Self {
content: Line::from_spans(spans),
style: None,
}
}
pub fn style(mut self, style: Style) -> Self {
self.style = Some(style);
self
}
pub fn color(mut self, color: Color) -> Self {
let style = self.style.get_or_insert(Style::new());
style.color = Some(color);
self
}
}
impl<T: Into<String>> From<T> for Cell {
fn from(s: T) -> Self {
Cell::new(s)
}
}
#[derive(Debug, Clone)]
pub struct Row {
pub cells: Vec<Cell>,
pub style: Option<Style>,
pub height: u16,
}
impl Row {
pub fn new<I, T>(cells: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<Cell>,
{
Self {
cells: cells.into_iter().map(|c| c.into()).collect(),
style: None,
height: 1,
}
}
pub fn style(mut self, style: Style) -> Self {
self.style = Some(style);
self
}
pub fn height(mut self, height: u16) -> Self {
self.height = height;
self
}
}
#[derive(Debug, Clone, Default)]
pub struct TableState {
pub selected: Option<usize>,
pub offset: usize,
}
impl TableState {
pub fn new() -> Self {
Self::default()
}
pub fn with_selected(selected: Option<usize>) -> Self {
Self {
selected,
offset: 0,
}
}
}
impl SelectionState for TableState {
fn selected(&self) -> Option<usize> {
self.selected
}
fn select(&mut self, index: Option<usize>) {
self.selected = index;
}
fn offset(&self) -> usize {
self.offset
}
fn set_offset(&mut self, offset: usize) {
self.offset = offset;
}
}
#[derive(Debug, Clone, Copy)]
pub enum Constraint {
Length(u16),
Min(u16),
Max(u16),
Percentage(u16),
Ratio(u16, u16),
}
impl Default for Constraint {
fn default() -> Self {
Constraint::Min(1)
}
}
#[derive(Debug, Clone)]
pub struct Table {
header: Option<Row>,
rows: Vec<Row>,
widths: Vec<Constraint>,
highlight_style: Style,
highlight_symbol: Option<String>,
column_separator: Option<String>,
key: Option<String>,
}
impl Table {
pub fn new() -> Self {
Self {
header: None,
rows: Vec::new(),
widths: Vec::new(),
highlight_style: Style::new(),
highlight_symbol: None,
column_separator: Some(" ".to_string()),
key: None,
}
}
pub fn header(mut self, header: Row) -> Self {
self.header = Some(header);
self
}
pub fn rows<I>(mut self, rows: I) -> Self
where
I: IntoIterator<Item = Row>,
{
self.rows = rows.into_iter().collect();
self
}
pub fn row(mut self, row: Row) -> Self {
self.rows.push(row);
self
}
pub fn widths<I>(mut self, widths: I) -> Self
where
I: IntoIterator<Item = Constraint>,
{
self.widths = widths.into_iter().collect();
self
}
pub fn highlight_style(mut self, style: Style) -> Self {
self.highlight_style = style;
self
}
pub fn highlight_symbol(mut self, symbol: impl Into<String>) -> Self {
self.highlight_symbol = Some(symbol.into());
self
}
pub fn column_separator(mut self, sep: impl Into<String>) -> Self {
self.column_separator = Some(sep.into());
self
}
pub fn key(mut self, key: impl Into<String>) -> Self {
self.key = Some(key.into());
self
}
pub fn len(&self) -> usize {
self.rows.len()
}
pub fn is_empty(&self) -> bool {
self.rows.is_empty()
}
pub fn render(self, state: &TableState) -> Element {
let selected = state.selected;
let separator = self.column_separator.as_deref().unwrap_or(" ");
let symbol_width = self.highlight_symbol.as_ref().map(|s| s.len()).unwrap_or(0);
let mut container = TinkBox::new().flex_direction(FlexDirection::Column);
if let Some(ref key) = self.key {
container = container.key(key.clone());
}
if let Some(header) = &self.header {
let header_element = self.render_row(header, separator, false, symbol_width);
container = container.child(header_element);
}
for (idx, row) in self.rows.iter().enumerate() {
let is_selected = selected == Some(idx);
let row_element = self.render_row(row, separator, is_selected, symbol_width);
container = container.child(row_element);
}
container.into_element()
}
fn render_row(
&self,
row: &Row,
separator: &str,
is_selected: bool,
symbol_width: usize,
) -> Element {
let mut spans = Vec::new();
if let Some(ref symbol) = self.highlight_symbol {
if is_selected {
spans.push(Span::new(symbol.clone()));
} else {
spans.push(Span::new(" ".repeat(symbol_width)));
}
}
for (i, cell) in row.cells.iter().enumerate() {
if i > 0 {
spans.push(Span::new(separator));
}
for span in &cell.content.spans {
spans.push(span.clone());
}
}
let line = Line::from_spans(spans);
let mut text = Text::line(line);
if is_selected {
if let Some(color) = self.highlight_style.color {
text = text.color(color);
}
if let Some(bg) = self.highlight_style.background_color {
text = text.background(bg);
}
if self.highlight_style.bold {
text = text.bold();
}
if self.highlight_style.inverse {
text = text.inverse();
}
}
text.into_element()
}
pub fn into_element(self) -> Element {
self.render(&TableState::new())
}
}
impl Default for Table {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cell_creation() {
let cell = Cell::new("Test");
assert_eq!(cell.content.spans[0].content, "Test");
}
#[test]
fn test_row_creation() {
let row = Row::new(vec!["A", "B", "C"]);
assert_eq!(row.cells.len(), 3);
}
#[test]
fn test_table_creation() {
let table = Table::new()
.header(Row::new(vec!["Name", "Age", "City"]))
.rows(vec![
Row::new(vec!["Alice", "30", "NYC"]),
Row::new(vec!["Bob", "25", "LA"]),
]);
assert_eq!(table.len(), 2);
assert!(table.header.is_some());
}
#[test]
fn test_table_state() {
let mut state = TableState::new();
state.select_next(5);
assert_eq!(state.selected, Some(0));
state.select_next(5);
assert_eq!(state.selected, Some(1));
state.select_previous(5);
assert_eq!(state.selected, Some(0));
}
}