use alloc::string::String;
use alloc::vec::Vec;
use rlvgl_core::draw::draw_widget_bg;
use rlvgl_core::event::Event;
use rlvgl_core::font::{FontMetrics, WidgetFont, shape_text_ltr, wrap_greedy_ltr};
use rlvgl_core::renderer::{ClipRenderer, Renderer};
use rlvgl_core::style::Style;
use rlvgl_core::widget::{Color, Rect, Widget};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct CellCtrl(pub u8);
impl CellCtrl {
pub const NONE: Self = Self(0);
pub const MERGE_RIGHT: Self = Self(1 << 0);
pub const TEXT_CROP: Self = Self(1 << 1);
pub fn contains(self, flag: Self) -> bool {
self.0 & flag.0 != 0
}
}
impl core::ops::BitOr for CellCtrl {
type Output = Self;
fn bitor(self, rhs: Self) -> Self {
Self(self.0 | rhs.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CellAlign {
#[default]
Left,
Center,
Right,
}
#[derive(Default)]
struct Cell {
text: Option<String>,
ctrl: CellCtrl,
align: CellAlign,
}
pub struct Table {
bounds: Rect,
rows: usize,
cols: usize,
cells: Vec<Cell>,
col_widths: Vec<i32>,
row_heights: Vec<i32>,
selected_row: Option<usize>,
selected_col: Option<usize>,
last_activated: Option<(usize, usize)>,
pub style: Style,
pub cell_bg_color: Color,
pub selected_color: Color,
pub text_color: Color,
pub grid_color: Color,
font: WidgetFont,
}
impl Table {
pub fn new(bounds: Rect) -> Self {
Self {
bounds,
rows: 0,
cols: 0,
cells: Vec::new(),
col_widths: Vec::new(),
row_heights: Vec::new(),
selected_row: None,
selected_col: None,
last_activated: None,
style: Style::default(),
cell_bg_color: Color(240, 240, 240, 255),
selected_color: Color(100, 160, 230, 255),
text_color: Color(30, 30, 30, 255),
grid_color: Color(180, 180, 180, 255),
font: WidgetFont::new(),
}
}
pub fn set_font(&mut self, font: &'static dyn FontMetrics) {
self.font.set(font);
}
pub fn row_count(&self) -> usize {
self.rows
}
pub fn column_count(&self) -> usize {
self.cols
}
pub fn set_row_count(&mut self, rows: usize) {
self.cells.resize_with(rows * self.cols, Cell::default);
self.rows = rows;
self.recompute_row_heights();
}
pub fn set_column_count(&mut self, cols: usize) {
if cols == self.cols {
return;
}
let mut new_cells: Vec<Cell> = Vec::with_capacity(self.rows * cols);
for r in 0..self.rows {
for c in 0..cols {
let cell = if c < self.cols {
let old_idx = r * self.cols + c;
let old = &self.cells[old_idx];
Cell {
text: old.text.clone(),
ctrl: old.ctrl,
align: old.align,
}
} else {
Cell::default()
};
new_cells.push(cell);
}
}
self.cells = new_cells;
self.col_widths.resize(cols, 0);
self.cols = cols;
self.recompute_row_heights();
}
fn cell_index(&self, row: usize, col: usize) -> Option<usize> {
if row < self.rows && col < self.cols {
Some(row * self.cols + col)
} else {
None
}
}
pub fn set_cell_value(&mut self, row: usize, col: usize, text: &str) {
if let Some(idx) = self.cell_index(row, col) {
self.cells[idx].text = Some(String::from(text));
self.recompute_row_heights();
}
}
pub fn cell_value(&self, row: usize, col: usize) -> Option<&str> {
self.cell_index(row, col)
.and_then(|idx| self.cells[idx].text.as_deref())
}
pub fn set_cell_ctrl(&mut self, row: usize, col: usize, ctrl: CellCtrl) {
if let Some(idx) = self.cell_index(row, col) {
self.cells[idx].ctrl = ctrl;
}
}
pub fn cell_ctrl(&self, row: usize, col: usize) -> CellCtrl {
self.cell_index(row, col)
.map(|idx| self.cells[idx].ctrl)
.unwrap_or(CellCtrl::NONE)
}
pub fn set_cell_align(&mut self, row: usize, col: usize, align: CellAlign) {
if let Some(idx) = self.cell_index(row, col) {
self.cells[idx].align = align;
}
}
pub fn set_column_width(&mut self, col: usize, width: i32) {
if col < self.cols {
self.col_widths[col] = width;
}
}
pub fn column_width(&self, col: usize) -> i32 {
self.col_widths.get(col).copied().unwrap_or(0)
}
fn resolved_col_widths(&self) -> Vec<i32> {
if self.cols == 0 {
return Vec::new();
}
let mut widths = self.col_widths.clone();
widths.resize(self.cols, 0);
let explicit_total: i32 = widths.iter().map(|&w| w.max(0)).sum();
let auto_count = widths.iter().filter(|&&w| w == 0).count();
let remaining = (self.bounds.width - explicit_total).max(0);
let auto_w = if auto_count > 0 {
remaining / auto_count as i32
} else {
0
};
for w in &mut widths {
if *w == 0 {
*w = auto_w;
}
}
widths
}
fn recompute_row_heights(&mut self) {
let font: &dyn FontMetrics = self.font.resolve();
let metrics = font.line_metrics();
let col_widths = self.resolved_col_widths();
self.row_heights.resize(self.rows, 0);
for r in 0..self.rows {
let mut max_h = metrics.line_height as i32;
for c in 0..self.cols {
let idx = r * self.cols + c;
let cell = &self.cells[idx];
let Some(ref text) = cell.text else {
continue;
};
let col_w = col_widths.get(c).copied().unwrap_or(0).max(1);
let wrap_w = if cell.ctrl.contains(CellCtrl::TEXT_CROP) {
i32::MAX } else {
col_w
};
let wrapped = wrap_greedy_ltr(font, text, wrap_w, 0, 0);
let h = wrapped.used_height.max(metrics.line_height as i32);
if h > max_h {
max_h = h;
}
}
self.row_heights[r] = max_h;
}
}
pub fn set_selected_cell(&mut self, row: usize, col: usize) {
if row < self.rows && col < self.cols {
self.selected_row = Some(row);
self.selected_col = Some(col);
}
}
pub fn selected_cell(&self) -> Option<(usize, usize)> {
match (self.selected_row, self.selected_col) {
(Some(r), Some(c)) => Some((r, c)),
_ => None,
}
}
pub fn navigate_next(&mut self) {
if self.rows == 0 || self.cols == 0 {
return;
}
let (r, c) = self.selected_cell().unwrap_or((0, self.cols - 1));
let (nr, nc) = if c + 1 < self.cols {
(r, c + 1)
} else if r + 1 < self.rows {
(r + 1, 0)
} else {
(0, 0)
};
self.selected_row = Some(nr);
self.selected_col = Some(nc);
}
pub fn navigate_prev(&mut self) {
if self.rows == 0 || self.cols == 0 {
return;
}
let (r, c) = self.selected_cell().unwrap_or((0, 0));
let (nr, nc) = if c > 0 {
(r, c - 1)
} else if r > 0 {
(r - 1, self.cols - 1)
} else {
(self.rows - 1, self.cols - 1)
};
self.selected_row = Some(nr);
self.selected_col = Some(nc);
}
pub fn navigate_up(&mut self) {
if self.rows == 0 {
return;
}
if let Some(r) = self.selected_row {
if r > 0 {
self.selected_row = Some(r - 1);
}
} else {
self.selected_row = Some(self.rows - 1);
self.selected_col = Some(0);
}
}
pub fn navigate_down(&mut self) {
if self.rows == 0 {
return;
}
if let Some(r) = self.selected_row {
if r + 1 < self.rows {
self.selected_row = Some(r + 1);
}
} else {
self.selected_row = Some(0);
self.selected_col = Some(0);
}
}
pub fn activate_selected(&mut self) {
if let Some(cell) = self.selected_cell() {
self.last_activated = Some(cell);
}
}
pub fn last_activated(&mut self) -> Option<(usize, usize)> {
self.last_activated.take()
}
fn draw_cells(&self, renderer: &mut dyn Renderer) {
let font: &dyn FontMetrics = self.font.resolve();
let metrics = font.line_metrics();
let col_widths = self.resolved_col_widths();
let mut y = self.bounds.y;
for r in 0..self.rows {
let row_h = self
.row_heights
.get(r)
.copied()
.unwrap_or(metrics.line_height as i32);
let mut x = self.bounds.x;
for c in 0..self.cols {
let idx = r * self.cols + c;
let cell = &self.cells[idx];
let mut cell_w = col_widths.get(c).copied().unwrap_or(0);
if cell.ctrl.contains(CellCtrl::MERGE_RIGHT) && c + 1 < self.cols {
cell_w += col_widths.get(c + 1).copied().unwrap_or(0);
}
let cell_rect = Rect {
x,
y,
width: cell_w.max(0),
height: row_h,
};
let is_selected = self.selected_row == Some(r) && self.selected_col == Some(c);
let bg = if is_selected {
self.selected_color
} else {
self.cell_bg_color
};
renderer.fill_rect(cell_rect, bg.with_alpha(self.style.alpha));
renderer.fill_rect(
Rect {
x: cell_rect.x + cell_rect.width,
y: cell_rect.y,
width: 1,
height: cell_rect.height,
},
self.grid_color.with_alpha(self.style.alpha),
);
renderer.fill_rect(
Rect {
x: cell_rect.x,
y: cell_rect.y + cell_rect.height,
width: cell_rect.width + 1,
height: 1,
},
self.grid_color.with_alpha(self.style.alpha),
);
if let Some(ref text) = cell.text
&& cell_rect.width > 0
&& cell_rect.height > 0
{
let text_color = self.text_color.with_alpha(self.style.alpha);
let baseline_y = cell_rect.y + metrics.ascent as i32 + 2;
if cell.ctrl.contains(CellCtrl::TEXT_CROP) {
let shaped = shape_text_ltr(font, text, (cell_rect.x + 2, baseline_y), 0);
let mut clipped = ClipRenderer::new(renderer, cell_rect);
clipped.draw_text_shaped(&shaped, (0, 0), text_color);
} else {
let wrap_w = cell_rect.width.max(1) - 4;
let wrapped = wrap_greedy_ltr(font, text, wrap_w, 0, 0);
let mut clipped = ClipRenderer::new(renderer, cell_rect);
let mut line_y = baseline_y;
for line in &wrapped.lines {
let line_text = &text[line.start..line.end];
let line_advance_px = (line.advance_fp16 + 8) >> 4;
let x_off = match cell.align {
CellAlign::Left => cell_rect.x + 2,
CellAlign::Center => {
cell_rect.x + (cell_rect.width - line_advance_px) / 2
}
CellAlign::Right => {
cell_rect.x + cell_rect.width - line_advance_px - 2
}
};
let shaped = shape_text_ltr(font, line_text, (x_off, line_y), 0);
clipped.draw_text_shaped(&shaped, (0, 0), text_color);
line_y += metrics.line_height as i32;
}
}
}
x += cell_w;
if cell.ctrl.contains(CellCtrl::MERGE_RIGHT) {
}
}
y += row_h + 1; }
}
}
impl Widget for Table {
fn bounds(&self) -> Rect {
self.bounds
}
fn widget_font_mut(&mut self) -> Option<&mut WidgetFont> {
Some(&mut self.font)
}
fn set_bounds(&mut self, bounds: Rect) {
self.bounds = bounds;
self.recompute_row_heights();
}
fn draw(&self, renderer: &mut dyn Renderer) {
draw_widget_bg(renderer, self.bounds, &self.style);
self.draw_cells(renderer);
}
fn handle_event(&mut self, _event: &Event) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
fn r(x: i32, y: i32, w: i32, h: i32) -> Rect {
Rect {
x,
y,
width: w,
height: h,
}
}
#[test]
fn set_size_and_cell_value() {
let mut t = Table::new(r(0, 0, 200, 100));
t.set_row_count(3);
t.set_column_count(4);
assert_eq!(t.row_count(), 3);
assert_eq!(t.column_count(), 4);
t.set_cell_value(1, 2, "hello");
assert_eq!(t.cell_value(1, 2), Some("hello"));
assert_eq!(t.cell_value(0, 0), None);
}
#[test]
fn out_of_bounds_cell_is_noop() {
let mut t = Table::new(r(0, 0, 200, 100));
t.set_row_count(2);
t.set_column_count(2);
t.set_cell_value(5, 5, "oob"); assert_eq!(t.cell_value(5, 5), None);
}
#[test]
fn column_widths_stored() {
let mut t = Table::new(r(0, 0, 200, 100));
t.set_column_count(3);
t.set_column_width(1, 60);
assert_eq!(t.column_width(1), 60);
assert_eq!(t.column_width(0), 0); }
#[test]
fn cell_align_stored() {
let mut t = Table::new(r(0, 0, 200, 100));
t.set_row_count(1);
t.set_column_count(2);
t.set_cell_align(0, 1, CellAlign::Center);
let idx = 1; assert_eq!(t.cells[idx].align, CellAlign::Center);
}
#[test]
fn merge_right_ctrl_flag() {
let mut t = Table::new(r(0, 0, 200, 100));
t.set_row_count(1);
t.set_column_count(3);
t.set_cell_ctrl(0, 0, CellCtrl::MERGE_RIGHT);
assert!(t.cell_ctrl(0, 0).contains(CellCtrl::MERGE_RIGHT));
assert!(!t.cell_ctrl(0, 1).contains(CellCtrl::MERGE_RIGHT));
}
#[test]
fn navigate_next_wraps() {
let mut t = Table::new(r(0, 0, 200, 100));
t.set_row_count(2);
t.set_column_count(2);
t.set_selected_cell(0, 0);
t.navigate_next();
assert_eq!(t.selected_cell(), Some((0, 1)));
t.navigate_next(); assert_eq!(t.selected_cell(), Some((1, 0)));
t.navigate_next();
assert_eq!(t.selected_cell(), Some((1, 1)));
t.navigate_next(); assert_eq!(t.selected_cell(), Some((0, 0)));
}
#[test]
fn navigate_prev_wraps() {
let mut t = Table::new(r(0, 0, 200, 100));
t.set_row_count(2);
t.set_column_count(2);
t.set_selected_cell(0, 0);
t.navigate_prev(); assert_eq!(t.selected_cell(), Some((1, 1)));
}
#[test]
fn navigate_up_down() {
let mut t = Table::new(r(0, 0, 200, 100));
t.set_row_count(3);
t.set_column_count(2);
t.set_selected_cell(1, 0);
t.navigate_up();
assert_eq!(t.selected_cell(), Some((0, 0)));
t.navigate_down();
assert_eq!(t.selected_cell(), Some((1, 0)));
}
#[test]
fn activate_selected_sets_poll_slot() {
let mut t = Table::new(r(0, 0, 200, 100));
t.set_row_count(2);
t.set_column_count(2);
t.set_selected_cell(1, 1);
t.activate_selected();
assert_eq!(t.last_activated(), Some((1, 1)));
assert_eq!(t.last_activated(), None); }
#[test]
fn resize_recomputes_row_heights() {
let mut t = Table::new(r(0, 0, 200, 100));
t.set_row_count(2);
t.set_column_count(2);
t.set_cell_value(0, 0, "Hello world this is a long line");
t.set_bounds(r(0, 0, 300, 150));
assert_eq!(t.bounds(), r(0, 0, 300, 150));
assert!(!t.row_heights.is_empty());
assert!(t.row_heights[0] > 0);
}
#[test]
fn column_count_change_preserves_existing_cells() {
let mut t = Table::new(r(0, 0, 200, 100));
t.set_row_count(1);
t.set_column_count(3);
t.set_cell_value(0, 1, "keep");
t.set_column_count(5);
assert_eq!(t.cell_value(0, 1), Some("keep"));
assert_eq!(t.column_count(), 5);
}
}