Skip to main content

embedded_gui/widgets/
table.rs

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/// 2D Table Data Grid Widget with cell selection and 2D navigation.
12#[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    /// Moves the active 2D selection cursor by `(d_row, d_col)`.
55    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        // Render header row if present
102        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                // Header cell background tint
106                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        // Render data rows
123        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                    // Highlight selected cell
131                    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}