Skip to main content

jag_ui/elements/
table.rs

1//! Simple table element with headers and data rows.
2
3use jag_draw::{Brush, ColorLinPremul, Rect};
4use jag_surface::Canvas;
5
6use crate::event::{
7    EventHandler, EventResult, KeyboardEvent, MouseClickEvent, MouseMoveEvent, ScrollEvent,
8};
9use crate::focus::FocusId;
10
11use super::Element;
12
13/// A simple table displaying headers and rows of text cells with
14/// grid lines and optional zebra striping.
15pub struct Table {
16    pub rect: Rect,
17    /// Column headers.
18    pub headers: Vec<String>,
19    /// Data rows (each row is a vec of cell strings).
20    pub rows: Vec<Vec<String>>,
21    /// Header background color.
22    pub header_bg: ColorLinPremul,
23    /// Header text color.
24    pub header_text_color: ColorLinPremul,
25    /// Cell text color.
26    pub cell_text_color: ColorLinPremul,
27    /// Grid line color.
28    pub grid_color: ColorLinPremul,
29    /// Grid line width.
30    pub grid_width: f32,
31    /// Background color.
32    pub bg: ColorLinPremul,
33    /// Font size for headers.
34    pub header_font_size: f32,
35    /// Font size for cells.
36    pub cell_font_size: f32,
37    /// Row height in logical pixels.
38    pub row_height: f32,
39    /// Horizontal cell padding.
40    pub cell_padding_x: f32,
41    /// Enable alternating row colors.
42    pub zebra_striping: bool,
43    /// Alternate row color.
44    pub zebra_color: ColorLinPremul,
45}
46
47impl Table {
48    /// Create a table with headers and default styling.
49    pub fn new(headers: Vec<String>) -> Self {
50        Self {
51            rect: Rect {
52                x: 0.0,
53                y: 0.0,
54                w: 600.0,
55                h: 300.0,
56            },
57            headers,
58            rows: Vec::new(),
59            header_bg: ColorLinPremul::from_srgba_u8([248, 248, 248, 255]),
60            header_text_color: ColorLinPremul::from_srgba_u8([60, 60, 60, 255]),
61            cell_text_color: ColorLinPremul::from_srgba_u8([80, 80, 80, 255]),
62            grid_color: ColorLinPremul::from_srgba_u8([224, 224, 224, 255]),
63            grid_width: 1.0,
64            bg: ColorLinPremul::from_srgba_u8([255, 255, 255, 255]),
65            header_font_size: 14.0,
66            cell_font_size: 14.0,
67            row_height: 40.0,
68            cell_padding_x: 12.0,
69            zebra_striping: false,
70            zebra_color: ColorLinPremul::from_srgba_u8([249, 249, 249, 255]),
71        }
72    }
73
74    /// Set data rows.
75    pub fn with_rows(mut self, rows: Vec<Vec<String>>) -> Self {
76        self.rows = rows;
77        self
78    }
79
80    /// Add a single row.
81    pub fn add_row(&mut self, row: Vec<String>) {
82        self.rows.push(row);
83    }
84
85    /// Enable or disable zebra striping.
86    pub fn with_zebra(mut self, enabled: bool) -> Self {
87        self.zebra_striping = enabled;
88        self
89    }
90
91    /// Calculate equal column widths.
92    fn column_width(&self) -> f32 {
93        let n = self.headers.len().max(1) as f32;
94        self.rect.w / n
95    }
96
97    /// Hit-test.
98    pub fn hit_test(&self, x: f32, y: f32) -> bool {
99        x >= self.rect.x
100            && x <= self.rect.x + self.rect.w
101            && y >= self.rect.y
102            && y <= self.rect.y + self.rect.h
103    }
104}
105
106// ---------------------------------------------------------------------------
107// Element trait
108// ---------------------------------------------------------------------------
109
110impl Element for Table {
111    fn rect(&self) -> Rect {
112        self.rect
113    }
114
115    fn set_rect(&mut self, rect: Rect) {
116        self.rect = rect;
117    }
118
119    fn render(&self, canvas: &mut Canvas, z: i32) {
120        let col_w = self.column_width();
121        let mut y = self.rect.y;
122
123        // Background
124        canvas.fill_rect(
125            self.rect.x,
126            self.rect.y,
127            self.rect.w,
128            self.rect.h,
129            Brush::Solid(self.bg),
130            z,
131        );
132
133        // Header row background
134        if !self.headers.is_empty() {
135            canvas.fill_rect(
136                self.rect.x,
137                y,
138                self.rect.w,
139                self.row_height,
140                Brush::Solid(self.header_bg),
141                z + 1,
142            );
143
144            // Header text
145            for (i, header) in self.headers.iter().enumerate() {
146                let tx = self.rect.x + i as f32 * col_w + self.cell_padding_x;
147                let ty = y + self.row_height * 0.5 + self.header_font_size * 0.35;
148                canvas.draw_text_run_weighted(
149                    [tx, ty],
150                    header.clone(),
151                    self.header_font_size,
152                    600.0,
153                    self.header_text_color,
154                    z + 3,
155                );
156            }
157
158            // Horizontal line under header
159            canvas.fill_rect(
160                self.rect.x,
161                y + self.row_height,
162                self.rect.w,
163                self.grid_width,
164                Brush::Solid(self.grid_color),
165                z + 2,
166            );
167
168            y += self.row_height;
169        }
170
171        // Data rows
172        for (row_idx, row) in self.rows.iter().enumerate() {
173            if y > self.rect.y + self.rect.h {
174                break;
175            }
176
177            // Zebra stripe
178            if self.zebra_striping && row_idx % 2 == 1 {
179                canvas.fill_rect(
180                    self.rect.x,
181                    y,
182                    self.rect.w,
183                    self.row_height,
184                    Brush::Solid(self.zebra_color),
185                    z + 1,
186                );
187            }
188
189            // Cell text
190            for (col_idx, cell) in row.iter().enumerate() {
191                if col_idx >= self.headers.len() {
192                    break;
193                }
194                let tx = self.rect.x + col_idx as f32 * col_w + self.cell_padding_x;
195                let ty = y + self.row_height * 0.5 + self.cell_font_size * 0.35;
196                canvas.draw_text_run_weighted(
197                    [tx, ty],
198                    cell.clone(),
199                    self.cell_font_size,
200                    400.0,
201                    self.cell_text_color,
202                    z + 3,
203                );
204            }
205
206            // Horizontal grid line
207            if row_idx < self.rows.len() - 1 {
208                canvas.fill_rect(
209                    self.rect.x,
210                    y + self.row_height,
211                    self.rect.w,
212                    self.grid_width,
213                    Brush::Solid(self.grid_color),
214                    z + 2,
215                );
216            }
217
218            y += self.row_height;
219        }
220
221        // Vertical grid lines
222        for i in 1..self.headers.len() {
223            let lx = self.rect.x + i as f32 * col_w;
224            canvas.fill_rect(
225                lx,
226                self.rect.y,
227                self.grid_width,
228                (y - self.rect.y).min(self.rect.h),
229                Brush::Solid(self.grid_color),
230                z + 2,
231            );
232        }
233
234        // Outer border
235        if self.grid_width > 0.0 {
236            let used_h = (y - self.rect.y).min(self.rect.h);
237            // Top
238            canvas.fill_rect(
239                self.rect.x,
240                self.rect.y,
241                self.rect.w,
242                self.grid_width,
243                Brush::Solid(self.grid_color),
244                z + 4,
245            );
246            // Bottom
247            canvas.fill_rect(
248                self.rect.x,
249                self.rect.y + used_h - self.grid_width,
250                self.rect.w,
251                self.grid_width,
252                Brush::Solid(self.grid_color),
253                z + 4,
254            );
255            // Left
256            canvas.fill_rect(
257                self.rect.x,
258                self.rect.y,
259                self.grid_width,
260                used_h,
261                Brush::Solid(self.grid_color),
262                z + 4,
263            );
264            // Right
265            canvas.fill_rect(
266                self.rect.x + self.rect.w - self.grid_width,
267                self.rect.y,
268                self.grid_width,
269                used_h,
270                Brush::Solid(self.grid_color),
271                z + 4,
272            );
273        }
274    }
275
276    fn focus_id(&self) -> Option<FocusId> {
277        None
278    }
279}
280
281// ---------------------------------------------------------------------------
282// EventHandler trait
283// ---------------------------------------------------------------------------
284
285impl EventHandler for Table {
286    fn handle_mouse_click(&mut self, _event: &MouseClickEvent) -> EventResult {
287        EventResult::Ignored
288    }
289
290    fn handle_keyboard(&mut self, _event: &KeyboardEvent) -> EventResult {
291        EventResult::Ignored
292    }
293
294    fn handle_mouse_move(&mut self, _event: &MouseMoveEvent) -> EventResult {
295        EventResult::Ignored
296    }
297
298    fn handle_scroll(&mut self, _event: &ScrollEvent) -> EventResult {
299        EventResult::Ignored
300    }
301
302    fn is_focused(&self) -> bool {
303        false
304    }
305
306    fn set_focused(&mut self, _focused: bool) {}
307
308    fn contains_point(&self, x: f32, y: f32) -> bool {
309        self.hit_test(x, y)
310    }
311}
312
313// ---------------------------------------------------------------------------
314// Tests
315// ---------------------------------------------------------------------------
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320
321    #[test]
322    fn table_new_defaults() {
323        let t = Table::new(vec!["Name".into(), "Age".into()]);
324        assert_eq!(t.headers.len(), 2);
325        assert!(t.rows.is_empty());
326        assert!(!t.zebra_striping);
327    }
328
329    #[test]
330    fn table_add_rows() {
331        let mut t = Table::new(vec!["A".into(), "B".into()]);
332        t.add_row(vec!["1".into(), "2".into()]);
333        t.add_row(vec!["3".into(), "4".into()]);
334        assert_eq!(t.rows.len(), 2);
335    }
336
337    #[test]
338    fn table_with_rows_builder() {
339        let t = Table::new(vec!["X".into()]).with_rows(vec![vec!["a".into()], vec!["b".into()]]);
340        assert_eq!(t.rows.len(), 2);
341    }
342
343    #[test]
344    fn table_column_width() {
345        let mut t = Table::new(vec!["A".into(), "B".into(), "C".into()]);
346        t.rect.w = 300.0;
347        let cw = t.column_width();
348        assert!((cw - 100.0).abs() < f32::EPSILON);
349    }
350
351    #[test]
352    fn table_hit_test() {
353        let t = Table::new(vec!["H".into()]);
354        assert!(t.hit_test(300.0, 150.0));
355        assert!(!t.hit_test(700.0, 0.0));
356    }
357
358    #[test]
359    fn table_not_focusable() {
360        let t = Table::new(vec!["H".into()]);
361        assert!(t.focus_id().is_none());
362    }
363}