1use embedded_graphics_core::pixelcolor::{Rgb565, RgbColor};
2
3use crate::{
4 block::Block,
5 geometry::{EdgeInsets, Rect},
6 render::{Compositor, RenderCtx, TextAlign, TextStyle},
7 style::{Border, VisualState, WidgetStyle},
8 widget::{PropertyError, PropertyKey, PropertyValue, Widget},
9};
10
11#[derive(Clone, Copy, Debug, PartialEq)]
13pub struct TableWidget<'a> {
14 pub rows: &'a [&'a [&'a str]],
15 pub headers: Option<&'a [&'a str]>,
16 pub selected: Option<(usize, usize)>,
17 pub separators: bool,
18 pub cell_padding: u8,
19 pub align: TextAlign,
20}
21
22impl<'a> TableWidget<'a> {
23 pub const fn new(rows: &'a [&'a [&'a str]]) -> Self {
24 Self {
25 rows,
26 headers: None,
27 selected: None,
28 separators: true,
29 cell_padding: 4,
30 align: TextAlign::Left,
31 }
32 }
33
34 pub const fn with_headers(mut self, headers: &'a [&'a str]) -> Self {
35 self.headers = Some(headers);
36 self
37 }
38
39 pub const fn with_selection(mut self, row: usize, col: usize) -> Self {
40 self.selected = Some((row, col));
41 self
42 }
43
44 pub const fn with_separators(mut self, separators: bool) -> Self {
45 self.separators = separators;
46 self
47 }
48
49 pub const fn with_align(mut self, align: TextAlign) -> Self {
50 self.align = align;
51 self
52 }
53
54 pub fn move_cursor(&mut self, d_row: i32, d_col: i32) {
56 if self.rows.is_empty() {
57 return;
58 }
59 let max_rows = self.rows.len();
60 let max_cols = self.rows.iter().map(|r| r.len()).max().unwrap_or(1);
61
62 let (cur_r, cur_c) = self.selected.unwrap_or((0, 0));
63 let next_r = (cur_r as i32 + d_row).clamp(0, max_rows as i32 - 1) as usize;
64 let next_c = (cur_c as i32 + d_col).clamp(0, max_cols as i32 - 1) as usize;
65 self.selected = Some((next_r, next_c));
66 }
67
68 pub fn render<D, C>(
69 &self,
70 ctx: &mut RenderCtx<'_, D, C>,
71 rect: Rect,
72 style: WidgetStyle,
73 state: VisualState,
74 ) -> Result<(), D::Error>
75 where
76 D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>,
77 C: Compositor<D>,
78 {
79 let resolved = style.resolve(state);
80 let block = Block::styled(resolved);
81 block.render(rect, ctx)?;
82
83 let inner = block.inner(rect);
84 let header_rows = if self.headers.is_some() { 1 } else { 0 };
85 let total_rows = self.rows.len() + header_rows;
86 if total_rows == 0 {
87 return Ok(());
88 }
89
90 let max_cols = {
91 let data_cols = self.rows.iter().map(|r| r.len()).max().unwrap_or(1);
92 let header_cols = self.headers.map(|h| h.len()).unwrap_or(1);
93 data_cols.max(header_cols).max(1)
94 };
95
96 let row_h = (inner.h / total_rows as u32).max(1);
97 let col_w = (inner.w / max_cols as u32).max(1);
98
99 let mut cur_y = inner.y;
100
101 if let Some(headers) = self.headers {
103 for c in 0..max_cols {
104 let cell_rect = Rect::new(inner.x + (c as u32 * col_w) as i32, cur_y, col_w, row_h);
105 ctx.fill_rect(cell_rect, Rgb565::new(4, 8, 12))?;
107 if self.separators {
108 ctx.stroke_rect(cell_rect, Border::one(resolved.border.color))?;
109 }
110 let text = headers.get(c).copied().unwrap_or("");
111 ctx.draw_text_in(
112 cell_rect.inset(EdgeInsets::all(self.cell_padding as i16)),
113 text,
114 TextStyle::new(Rgb565::WHITE)
115 .with_font(resolved.font)
116 .with_align(self.align),
117 )?;
118 }
119 cur_y += row_h as i32;
120 }
121
122 for (r, cols) in self.rows.iter().enumerate() {
124 let row_y = cur_y + (r as u32 * row_h) as i32;
125 for c in 0..max_cols {
126 let cell_rect = Rect::new(inner.x + (c as u32 * col_w) as i32, row_y, col_w, row_h);
127 let is_selected = self.selected == Some((r, c));
128
129 if is_selected {
130 ctx.fill_rect(cell_rect, Rgb565::new(0, 15, 25))?;
132 ctx.stroke_rect(cell_rect, Border::one(Rgb565::new(0, 45, 31)))?;
133 } else if self.separators {
134 ctx.stroke_rect(cell_rect, Border::one(resolved.border.color))?;
135 }
136
137 let text = cols.get(c).copied().unwrap_or("");
138 let text_color = if is_selected {
139 Rgb565::WHITE
140 } else {
141 resolved.text
142 };
143
144 ctx.draw_text_in(
145 cell_rect.inset(EdgeInsets::all(self.cell_padding as i16)),
146 text,
147 TextStyle::new(text_color)
148 .with_font(resolved.font)
149 .with_align(self.align),
150 )?;
151 }
152 }
153
154 Ok(())
155 }
156}
157
158impl<'a> Widget for TableWidget<'a> {
159 fn render_widget_bounds(&self, _bounds: Rect, _style: &crate::style::Style) {}
160
161 fn get_property(&self, key: PropertyKey) -> Option<PropertyValue<'_>> {
162 match key {
163 PropertyKey::Selected => self.selected.map(|(r, _)| PropertyValue::Int(r as i32)),
164 _ => None,
165 }
166 }
167
168 fn set_property<'p>(
169 &mut self,
170 key: PropertyKey,
171 val: PropertyValue<'p>,
172 ) -> Result<(), PropertyError> {
173 match (key, val) {
174 (PropertyKey::Selected, PropertyValue::Int(r)) => {
175 let col = self.selected.map(|(_, c)| c).unwrap_or(0);
176 self.selected = Some((r.max(0) as usize, col));
177 Ok(())
178 }
179 _ => Err(PropertyError::NotFound),
180 }
181 }
182}