use super::{Widget, column::Column};
use alloc::{boxed::Box, rc::Rc, vec::Vec};
use core::marker::PhantomData;
use embedded_graphics::{
pixelcolor::PixelColor, prelude::*, primitives::Rectangle, text::Alignment,
};
use zest_core::{
Constraints, Length, RenderError, Renderer, ScrollDirection, ScrollMsg, ScrollState,
ScrollbarMode, SnapMode, TouchPhase, UiAction, WidgetId,
};
use zest_theme::Theme;
pub const TABLE_ROW_HEIGHT: u32 = 32;
pub const CELL_PADDING_X: u32 = 6;
pub struct TableRow<'a, C: PixelColor, M: Clone> {
rect: Rectangle,
row: usize,
base_id: Option<WidgetId>,
cells: &'a [&'a str],
columns: usize,
on_select: Option<Rc<dyn Fn(usize, usize) -> M + 'a>>,
alternate: bool,
selected_col: Option<usize>,
focused_col: Option<usize>,
pressed_col: Option<usize>,
width: Length,
height: Length,
_color: PhantomData<C>,
}
impl<'a, C: PixelColor, M: Clone> TableRow<'a, C, M> {
fn new(row: usize, cells: &'a [&'a str], columns: usize) -> Self {
Self {
rect: Rectangle::zero(),
row,
base_id: None,
cells,
columns,
on_select: None,
alternate: false,
selected_col: None,
focused_col: None,
pressed_col: None,
width: Length::Fill,
height: Length::Fixed(TABLE_ROW_HEIGHT),
_color: PhantomData,
}
}
fn is_enabled(&self) -> bool {
self.on_select.is_some()
}
fn col_width(&self) -> u32 {
let cols = self.columns.max(1) as u32;
self.rect.size.width / cols
}
fn col_at(&self, point: Point) -> Option<usize> {
let tl = self.rect.top_left;
let br = tl + Point::new(self.rect.size.width as i32, self.rect.size.height as i32);
if point.x < tl.x || point.x >= br.x || point.y < tl.y || point.y >= br.y {
return None;
}
let col_w = self.col_width().max(1) as i32;
let col = ((point.x - tl.x) / col_w) as usize;
Some(col.min(self.columns.saturating_sub(1)))
}
fn cell_id(&self, col: usize) -> Option<WidgetId> {
self.base_id
.map(|base| WidgetId::new(base.raw().wrapping_add(col as u64)))
}
}
impl<'a, C: PixelColor, M: Clone> Widget<C, M> for TableRow<'a, C, M> {
fn measure(&mut self, constraints: Constraints) -> Size {
let w = self
.width
.resolve(constraints.max.width, constraints.max.width);
let h = self
.height
.resolve(TABLE_ROW_HEIGHT, constraints.max.height);
constraints.clamp(Size::new(w, h))
}
fn preferred_size(&self) -> (Length, Length) {
(self.width, self.height)
}
fn arrange(&mut self, rect: Rectangle) {
self.rect = rect;
}
fn rect(&self) -> Rectangle {
self.rect
}
fn handle_touch(&mut self, point: Point, phase: TouchPhase) -> Option<M> {
if !self.is_enabled() {
return None;
}
match phase {
TouchPhase::Down => {
self.pressed_col = self.col_at(point);
None
}
TouchPhase::Up => {
let hit = self.col_at(point);
let fired = match (self.pressed_col, hit) {
(Some(p), Some(h)) if p == h => {
self.on_select.as_ref().map(|cb| cb(self.row, h))
}
_ => None,
};
self.pressed_col = None;
fired
}
TouchPhase::Moved => {
if self.col_at(point) != self.pressed_col {
self.pressed_col = None;
}
None
}
}
}
fn mark_pressed(&mut self, point: Point) {
if self.is_enabled() {
self.pressed_col = self.col_at(point);
}
}
fn collect_focusable(&self, out: &mut Vec<WidgetId>) {
if !self.is_enabled() {
return;
}
for col in 0..self.columns {
if let Some(id) = self.cell_id(col) {
out.push(id);
}
}
}
fn sync_focus(&mut self, focused: Option<WidgetId>) {
self.focused_col = focused
.and_then(|target| (0..self.columns).find(|col| self.cell_id(*col) == Some(target)));
}
fn route_action(&mut self, target: WidgetId, action: UiAction) -> Option<M> {
let col = (0..self.columns).find(|candidate| self.cell_id(*candidate) == Some(target))?;
match action {
UiAction::Activate => self.on_select.as_ref().map(|cb| cb(self.row, col)),
_ => None,
}
}
fn focus_rect(&self, target: WidgetId) -> Option<Rectangle> {
let col = (0..self.columns).find(|candidate| self.cell_id(*candidate) == Some(target))?;
let col_w = self.col_width();
Some(Rectangle::new(
Point::new(
self.rect.top_left.x + (col_w * col as u32) as i32,
self.rect.top_left.y,
),
Size::new(col_w, self.rect.size.height),
))
}
fn focus_at(&self, point: Point) -> Option<WidgetId> {
self.col_at(point).and_then(|col| self.cell_id(col))
}
fn draw<'t>(
&self,
renderer: &mut dyn Renderer<C>,
theme: &Theme<'t, C>,
) -> Result<(), RenderError> {
let font = theme.default_font();
let bg = if self.alternate {
theme.secondary.base
} else {
theme.primary.base
};
renderer.fill_rect(self.rect, bg)?;
let col_w = self.col_width();
let glyph_h = font.character_size.height as i32;
let baseline_y = self.rect.top_left.y + self.rect.size.height as i32 / 2 + glyph_h / 3;
for col in 0..self.columns {
let cell_x = self.rect.top_left.x + (col_w * col as u32) as i32;
let cell_rect = Rectangle::new(
Point::new(cell_x, self.rect.top_left.y),
Size::new(col_w, self.rect.size.height),
);
let highlighted = self.pressed_col == Some(col) || self.selected_col == Some(col);
let text_color = if highlighted {
renderer.fill_rect(cell_rect, theme.accent.pressed)?;
theme.accent.on_base
} else {
theme.primary.on_base
};
if self.focused_col == Some(col) {
renderer.stroke_rect(cell_rect, theme.accent.base)?;
}
if let Some(text) = self.cells.get(col) {
renderer.draw_text(
text,
Point::new(cell_x + CELL_PADDING_X as i32, baseline_y),
font,
text_color,
Alignment::Left,
)?;
}
if col > 0 {
let line = Rectangle::new(
Point::new(cell_x, self.rect.top_left.y),
Size::new(1, self.rect.size.height),
);
renderer.fill_rect(line, theme.primary.divider)?;
}
}
let y = self.rect.top_left.y + self.rect.size.height as i32 - 1;
let divider = Rectangle::new(
Point::new(self.rect.top_left.x, y),
Size::new(self.rect.size.width, 1),
);
renderer.fill_rect(divider, theme.primary.divider)?;
Ok(())
}
}
pub struct Table<'a, C: PixelColor, M: Clone> {
id: Option<WidgetId>,
header: Option<&'a [&'a str]>,
body: Vec<&'a [&'a str]>,
columns: usize,
on_select: Option<Rc<dyn Fn(usize, usize) -> M + 'a>>,
selected: Option<(usize, usize)>,
striped: bool,
width: Length,
height: Length,
scroll_dir: Option<ScrollDirection>,
scroll_state: Option<ScrollState>,
scrollbar: Option<ScrollbarMode>,
snap: Option<SnapMode>,
on_scroll: Option<Box<dyn Fn(ScrollMsg) -> M + 'a>>,
rect: Rectangle,
inner: Option<Column<'a, C, M>>,
header_rect: Rectangle,
}
impl<'a, C: PixelColor + 'a, M: Clone + 'a> Table<'a, C, M> {
pub fn new() -> Self {
Self {
header: None,
body: Vec::new(),
columns: 0,
id: None,
on_select: None,
selected: None,
striped: true,
width: Length::Fill,
height: Length::Fill,
scroll_dir: None,
scroll_state: None,
scrollbar: None,
snap: None,
on_scroll: None,
rect: Rectangle::zero(),
inner: None,
header_rect: Rectangle::zero(),
}
}
#[must_use]
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
#[must_use]
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
#[must_use]
pub fn id(mut self, id: WidgetId) -> Self {
self.id = Some(id);
self
}
#[must_use]
pub fn header(mut self, cells: &'a [&'a str]) -> Self {
self.columns = self.columns.max(cells.len());
self.header = Some(cells);
self
}
#[must_use]
pub fn rows(mut self, rows: &'a [&'a [&'a str]]) -> Self {
self.body.clear();
for r in rows {
self.columns = self.columns.max(r.len());
self.body.push(r);
}
self
}
#[must_use]
pub fn row(mut self, cells: &'a [&'a str]) -> Self {
self.columns = self.columns.max(cells.len());
self.body.push(cells);
self
}
#[must_use]
pub fn columns(mut self, columns: usize) -> Self {
self.columns = columns;
self
}
#[must_use]
pub fn striped(mut self, on: bool) -> Self {
self.striped = on;
self
}
#[must_use]
pub fn selected(mut self, row: usize, col: usize) -> Self {
self.selected = Some((row, col));
self
}
#[must_use]
pub fn on_select<F>(mut self, f: F) -> Self
where
F: Fn(usize, usize) -> M + 'a,
{
self.on_select = Some(Rc::new(f));
self
}
#[must_use]
pub fn scrollable(mut self, dir: ScrollDirection) -> Self {
self.scroll_dir = Some(dir);
self
}
#[must_use]
pub fn scroll_state(mut self, state: &ScrollState) -> Self {
self.scroll_state = Some(*state);
if self.scroll_dir.is_none() {
self.scroll_dir = Some(ScrollDirection::Vertical);
}
self
}
#[must_use]
pub fn scrollbar(mut self, mode: ScrollbarMode) -> Self {
self.scrollbar = Some(mode);
if self.scroll_dir.is_none() {
self.scroll_dir = Some(ScrollDirection::Vertical);
}
self
}
#[must_use]
pub fn snap(mut self, mode: SnapMode) -> Self {
self.snap = Some(mode);
if self.scroll_dir.is_none() {
self.scroll_dir = Some(ScrollDirection::Vertical);
}
self
}
#[must_use]
pub fn on_scroll<F>(mut self, f: F) -> Self
where
F: Fn(ScrollMsg) -> M + 'a,
{
self.on_scroll = Some(Box::new(f));
if self.scroll_dir.is_none() {
self.scroll_dir = Some(ScrollDirection::Vertical);
}
self
}
fn build_body(&mut self) -> Column<'a, C, M> {
let mut col = Column::new()
.width(self.width)
.height(Length::Fill)
.spacing(0);
if let Some(dir) = self.scroll_dir {
col = col.scrollable(dir);
if let Some(state) = self.scroll_state.as_ref() {
col = col.scroll_state(state);
}
if let Some(bar) = self.scrollbar {
col = col.scrollbar(bar);
}
if let Some(snap) = self.snap {
col = col.snap(snap);
}
if let Some(on_scroll) = self.on_scroll.take() {
col = col.on_scroll(move |sm| on_scroll(sm));
}
}
let columns = self.columns.max(1);
let striped = self.striped;
let selected = self.selected;
let on_select = self.on_select.clone();
for (i, cells) in self.body.iter().copied().enumerate() {
let mut row = TableRow::new(i, cells, columns);
row.base_id = self.row_base_id(i);
row.alternate = striped && (i % 2 == 1);
row.selected_col = match selected {
Some((r, c)) if r == i => Some(c),
_ => None,
};
row.on_select = on_select.clone();
col = col.push(row);
}
col
}
fn row_base_id(&self, row: usize) -> Option<WidgetId> {
let columns = self.columns.max(1) as u64;
self.id.map(|base| {
WidgetId::new(
base.raw()
.wrapping_add(1)
.wrapping_add(row as u64 * columns),
)
})
}
fn cell_id(&self, row: usize, col: usize) -> Option<WidgetId> {
self.row_base_id(row)
.map(|base| WidgetId::new(base.raw().wrapping_add(col as u64)))
}
fn coords_for(&self, target: WidgetId) -> Option<(usize, usize)> {
let columns = self.columns.max(1);
for row in 0..self.body.len() {
for col in 0..columns {
if self.cell_id(row, col) == Some(target) {
return Some((row, col));
}
}
}
None
}
}
impl<'a, C: PixelColor + 'a, M: Clone + 'a> Default for Table<'a, C, M> {
fn default() -> Self {
Self::new()
}
}
impl<'a, C: PixelColor + 'a, M: Clone + 'a> Widget<C, M> for Table<'a, C, M> {
fn measure(&mut self, constraints: Constraints) -> Size {
let w = self
.width
.resolve(constraints.max.width, constraints.max.width);
let h = self
.height
.resolve(constraints.max.height, constraints.max.height);
constraints.clamp(Size::new(w, h))
}
fn preferred_size(&self) -> (Length, Length) {
(self.width, self.height)
}
fn arrange(&mut self, rect: Rectangle) {
self.rect = rect;
let header_h = if self.header.is_some() {
TABLE_ROW_HEIGHT.min(rect.size.height)
} else {
0
};
self.header_rect = Rectangle::new(rect.top_left, Size::new(rect.size.width, header_h));
let body_rect = Rectangle::new(
Point::new(rect.top_left.x, rect.top_left.y + header_h as i32),
Size::new(rect.size.width, rect.size.height.saturating_sub(header_h)),
);
let mut body = self.build_body();
body.arrange(body_rect);
self.inner = Some(body);
}
fn rect(&self) -> Rectangle {
self.rect
}
fn handle_touch(&mut self, point: Point, phase: TouchPhase) -> Option<M> {
self.inner
.as_mut()
.and_then(|body| body.handle_touch(point, phase))
}
fn mark_pressed(&mut self, point: Point) {
if let Some(body) = self.inner.as_mut() {
body.mark_pressed(point);
}
}
fn collect_focusable(&self, out: &mut Vec<WidgetId>) {
if let Some(body) = self.inner.as_ref() {
body.collect_focusable(out);
}
}
fn sync_focus(&mut self, focused: Option<WidgetId>) {
if let Some(body) = self.inner.as_mut() {
body.sync_focus(focused);
}
}
fn route_action(&mut self, target: WidgetId, action: UiAction) -> Option<M> {
self.inner
.as_mut()
.and_then(|body| body.route_action(target, action))
}
fn navigate_focus(&self, target: WidgetId, action: UiAction) -> Option<WidgetId> {
let (row, col) = self.coords_for(target)?;
let columns = self.columns.max(1);
let (next_row, next_col) = match action {
UiAction::NavigateLeft => (row, col.saturating_sub(1)),
UiAction::NavigateRight => (row, (col + 1).min(columns.saturating_sub(1))),
UiAction::NavigateUp => (row.saturating_sub(1), col),
UiAction::NavigateDown => ((row + 1).min(self.body.len().saturating_sub(1)), col),
_ => return None,
};
self.cell_id(next_row, next_col)
}
fn focus_rect(&self, target: WidgetId) -> Option<Rectangle> {
self.inner.as_ref().and_then(|body| body.focus_rect(target))
}
fn focus_at(&self, point: Point) -> Option<WidgetId> {
self.inner.as_ref().and_then(|body| body.focus_at(point))
}
fn draw<'t>(
&self,
renderer: &mut dyn Renderer<C>,
theme: &Theme<'t, C>,
) -> Result<(), RenderError> {
if let Some(body) = self.inner.as_ref() {
body.draw(renderer, theme)?;
}
if let Some(cells) = self.header {
let r = self.header_rect;
renderer.fill_rect(r, theme.accent.base)?;
let font = theme.default_font();
let cols = self.columns.max(1) as u32;
let col_w = r.size.width / cols;
let glyph_h = font.character_size.height as i32;
let baseline_y = r.top_left.y + r.size.height as i32 / 2 + glyph_h / 3;
for (col, text) in cells.iter().enumerate() {
let cell_x = r.top_left.x + (col_w * col as u32) as i32;
renderer.draw_text(
text,
Point::new(cell_x + CELL_PADDING_X as i32, baseline_y),
font,
theme.accent.on_base,
Alignment::Left,
)?;
if col > 0 {
let line = Rectangle::new(
Point::new(cell_x, r.top_left.y),
Size::new(1, r.size.height),
);
renderer.fill_rect(line, theme.accent.border)?;
}
}
let y = r.top_left.y + r.size.height as i32 - 1;
let edge = Rectangle::new(Point::new(r.top_left.x, y), Size::new(r.size.width, 1));
renderer.fill_rect(edge, theme.accent.border)?;
}
Ok(())
}
}