1use alloc::string::String;
24use alloc::vec::Vec;
25
26use rlvgl_core::draw::draw_widget_bg;
27use rlvgl_core::event::Event;
28use rlvgl_core::font::{FontMetrics, WidgetFont, shape_text_ltr, wrap_greedy_ltr};
29use rlvgl_core::renderer::{ClipRenderer, Renderer};
30use rlvgl_core::style::Style;
31use rlvgl_core::widget::{Color, Rect, Widget};
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
41pub struct CellCtrl(pub u8);
42
43impl CellCtrl {
44 pub const NONE: Self = Self(0);
46 pub const MERGE_RIGHT: Self = Self(1 << 0);
48 pub const TEXT_CROP: Self = Self(1 << 1);
50
51 pub fn contains(self, flag: Self) -> bool {
53 self.0 & flag.0 != 0
54 }
55}
56
57impl core::ops::BitOr for CellCtrl {
58 type Output = Self;
59 fn bitor(self, rhs: Self) -> Self {
60 Self(self.0 | rhs.0)
61 }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
72pub enum CellAlign {
73 #[default]
75 Left,
76 Center,
78 Right,
80}
81
82#[derive(Default)]
87struct Cell {
88 text: Option<String>,
89 ctrl: CellCtrl,
90 align: CellAlign,
91}
92
93pub struct Table {
103 bounds: Rect,
104 rows: usize,
105 cols: usize,
106 cells: Vec<Cell>,
107 col_widths: Vec<i32>,
108 row_heights: Vec<i32>,
110 selected_row: Option<usize>,
111 selected_col: Option<usize>,
112 last_activated: Option<(usize, usize)>,
114 pub style: Style,
116 pub cell_bg_color: Color,
118 pub selected_color: Color,
120 pub text_color: Color,
122 pub grid_color: Color,
124 font: WidgetFont,
127}
128
129impl Table {
130 pub fn new(bounds: Rect) -> Self {
132 Self {
133 bounds,
134 rows: 0,
135 cols: 0,
136 cells: Vec::new(),
137 col_widths: Vec::new(),
138 row_heights: Vec::new(),
139 selected_row: None,
140 selected_col: None,
141 last_activated: None,
142 style: Style::default(),
143 cell_bg_color: Color(240, 240, 240, 255),
144 selected_color: Color(100, 160, 230, 255),
145 text_color: Color(30, 30, 30, 255),
146 grid_color: Color(180, 180, 180, 255),
147 font: WidgetFont::new(),
148 }
149 }
150
151 pub fn set_font(&mut self, font: &'static dyn FontMetrics) {
154 self.font.set(font);
155 }
156
157 pub fn row_count(&self) -> usize {
161 self.rows
162 }
163
164 pub fn column_count(&self) -> usize {
166 self.cols
167 }
168
169 pub fn set_row_count(&mut self, rows: usize) {
171 self.cells.resize_with(rows * self.cols, Cell::default);
172 self.rows = rows;
173 self.recompute_row_heights();
174 }
175
176 pub fn set_column_count(&mut self, cols: usize) {
179 if cols == self.cols {
180 return;
181 }
182 let mut new_cells: Vec<Cell> = Vec::with_capacity(self.rows * cols);
183 for r in 0..self.rows {
184 for c in 0..cols {
185 let cell = if c < self.cols {
186 let old_idx = r * self.cols + c;
187 let old = &self.cells[old_idx];
188 Cell {
189 text: old.text.clone(),
190 ctrl: old.ctrl,
191 align: old.align,
192 }
193 } else {
194 Cell::default()
195 };
196 new_cells.push(cell);
197 }
198 }
199 self.cells = new_cells;
200 self.col_widths.resize(cols, 0);
201 self.cols = cols;
202 self.recompute_row_heights();
203 }
204
205 fn cell_index(&self, row: usize, col: usize) -> Option<usize> {
208 if row < self.rows && col < self.cols {
209 Some(row * self.cols + col)
210 } else {
211 None
212 }
213 }
214
215 pub fn set_cell_value(&mut self, row: usize, col: usize, text: &str) {
219 if let Some(idx) = self.cell_index(row, col) {
220 self.cells[idx].text = Some(String::from(text));
221 self.recompute_row_heights();
222 }
223 }
224
225 pub fn cell_value(&self, row: usize, col: usize) -> Option<&str> {
227 self.cell_index(row, col)
228 .and_then(|idx| self.cells[idx].text.as_deref())
229 }
230
231 pub fn set_cell_ctrl(&mut self, row: usize, col: usize, ctrl: CellCtrl) {
233 if let Some(idx) = self.cell_index(row, col) {
234 self.cells[idx].ctrl = ctrl;
235 }
236 }
237
238 pub fn cell_ctrl(&self, row: usize, col: usize) -> CellCtrl {
240 self.cell_index(row, col)
241 .map(|idx| self.cells[idx].ctrl)
242 .unwrap_or(CellCtrl::NONE)
243 }
244
245 pub fn set_cell_align(&mut self, row: usize, col: usize, align: CellAlign) {
247 if let Some(idx) = self.cell_index(row, col) {
248 self.cells[idx].align = align;
249 }
250 }
251
252 pub fn set_column_width(&mut self, col: usize, width: i32) {
259 if col < self.cols {
260 self.col_widths[col] = width;
261 }
262 }
263
264 pub fn column_width(&self, col: usize) -> i32 {
266 self.col_widths.get(col).copied().unwrap_or(0)
267 }
268
269 fn resolved_col_widths(&self) -> Vec<i32> {
272 if self.cols == 0 {
273 return Vec::new();
274 }
275 let mut widths = self.col_widths.clone();
276 widths.resize(self.cols, 0);
277
278 let explicit_total: i32 = widths.iter().map(|&w| w.max(0)).sum();
279 let auto_count = widths.iter().filter(|&&w| w == 0).count();
280 let remaining = (self.bounds.width - explicit_total).max(0);
281 let auto_w = if auto_count > 0 {
282 remaining / auto_count as i32
283 } else {
284 0
285 };
286
287 for w in &mut widths {
288 if *w == 0 {
289 *w = auto_w;
290 }
291 }
292 widths
293 }
294
295 fn recompute_row_heights(&mut self) {
302 let font: &dyn FontMetrics = self.font.resolve();
303 let metrics = font.line_metrics();
304 let col_widths = self.resolved_col_widths();
305
306 self.row_heights.resize(self.rows, 0);
307 for r in 0..self.rows {
308 let mut max_h = metrics.line_height as i32;
309 for c in 0..self.cols {
310 let idx = r * self.cols + c;
311 let cell = &self.cells[idx];
312 let Some(ref text) = cell.text else {
313 continue;
314 };
315 let col_w = col_widths.get(c).copied().unwrap_or(0).max(1);
316 let wrap_w = if cell.ctrl.contains(CellCtrl::TEXT_CROP) {
318 i32::MAX } else {
320 col_w
321 };
322 let wrapped = wrap_greedy_ltr(font, text, wrap_w, 0, 0);
323 let h = wrapped.used_height.max(metrics.line_height as i32);
324 if h > max_h {
325 max_h = h;
326 }
327 }
328 self.row_heights[r] = max_h;
329 }
330 }
331
332 pub fn set_selected_cell(&mut self, row: usize, col: usize) {
336 if row < self.rows && col < self.cols {
337 self.selected_row = Some(row);
338 self.selected_col = Some(col);
339 }
340 }
341
342 pub fn selected_cell(&self) -> Option<(usize, usize)> {
344 match (self.selected_row, self.selected_col) {
345 (Some(r), Some(c)) => Some((r, c)),
346 _ => None,
347 }
348 }
349
350 pub fn navigate_next(&mut self) {
355 if self.rows == 0 || self.cols == 0 {
356 return;
357 }
358 let (r, c) = self.selected_cell().unwrap_or((0, self.cols - 1));
359 let (nr, nc) = if c + 1 < self.cols {
360 (r, c + 1)
361 } else if r + 1 < self.rows {
362 (r + 1, 0)
363 } else {
364 (0, 0)
365 };
366 self.selected_row = Some(nr);
367 self.selected_col = Some(nc);
368 }
369
370 pub fn navigate_prev(&mut self) {
375 if self.rows == 0 || self.cols == 0 {
376 return;
377 }
378 let (r, c) = self.selected_cell().unwrap_or((0, 0));
379 let (nr, nc) = if c > 0 {
380 (r, c - 1)
381 } else if r > 0 {
382 (r - 1, self.cols - 1)
383 } else {
384 (self.rows - 1, self.cols - 1)
385 };
386 self.selected_row = Some(nr);
387 self.selected_col = Some(nc);
388 }
389
390 pub fn navigate_up(&mut self) {
394 if self.rows == 0 {
395 return;
396 }
397 if let Some(r) = self.selected_row {
398 if r > 0 {
399 self.selected_row = Some(r - 1);
400 }
401 } else {
402 self.selected_row = Some(self.rows - 1);
403 self.selected_col = Some(0);
404 }
405 }
406
407 pub fn navigate_down(&mut self) {
411 if self.rows == 0 {
412 return;
413 }
414 if let Some(r) = self.selected_row {
415 if r + 1 < self.rows {
416 self.selected_row = Some(r + 1);
417 }
418 } else {
419 self.selected_row = Some(0);
420 self.selected_col = Some(0);
421 }
422 }
423
424 pub fn activate_selected(&mut self) {
428 if let Some(cell) = self.selected_cell() {
429 self.last_activated = Some(cell);
430 }
431 }
432
433 pub fn last_activated(&mut self) -> Option<(usize, usize)> {
435 self.last_activated.take()
436 }
437
438 fn draw_cells(&self, renderer: &mut dyn Renderer) {
441 let font: &dyn FontMetrics = self.font.resolve();
442 let metrics = font.line_metrics();
443 let col_widths = self.resolved_col_widths();
444
445 let mut y = self.bounds.y;
446 for r in 0..self.rows {
447 let row_h = self
448 .row_heights
449 .get(r)
450 .copied()
451 .unwrap_or(metrics.line_height as i32);
452 let mut x = self.bounds.x;
453
454 for c in 0..self.cols {
455 let idx = r * self.cols + c;
456 let cell = &self.cells[idx];
457 let mut cell_w = col_widths.get(c).copied().unwrap_or(0);
458
459 if cell.ctrl.contains(CellCtrl::MERGE_RIGHT) && c + 1 < self.cols {
461 cell_w += col_widths.get(c + 1).copied().unwrap_or(0);
462 }
463
464 let cell_rect = Rect {
465 x,
466 y,
467 width: cell_w.max(0),
468 height: row_h,
469 };
470
471 let is_selected = self.selected_row == Some(r) && self.selected_col == Some(c);
473 let bg = if is_selected {
474 self.selected_color
475 } else {
476 self.cell_bg_color
477 };
478 renderer.fill_rect(cell_rect, bg.with_alpha(self.style.alpha));
479
480 renderer.fill_rect(
482 Rect {
483 x: cell_rect.x + cell_rect.width,
484 y: cell_rect.y,
485 width: 1,
486 height: cell_rect.height,
487 },
488 self.grid_color.with_alpha(self.style.alpha),
489 );
490 renderer.fill_rect(
491 Rect {
492 x: cell_rect.x,
493 y: cell_rect.y + cell_rect.height,
494 width: cell_rect.width + 1,
495 height: 1,
496 },
497 self.grid_color.with_alpha(self.style.alpha),
498 );
499
500 if let Some(ref text) = cell.text
502 && cell_rect.width > 0
503 && cell_rect.height > 0
504 {
505 let text_color = self.text_color.with_alpha(self.style.alpha);
506 let baseline_y = cell_rect.y + metrics.ascent as i32 + 2;
507
508 if cell.ctrl.contains(CellCtrl::TEXT_CROP) {
509 let shaped = shape_text_ltr(font, text, (cell_rect.x + 2, baseline_y), 0);
511 let mut clipped = ClipRenderer::new(renderer, cell_rect);
512 clipped.draw_text_shaped(&shaped, (0, 0), text_color);
513 } else {
514 let wrap_w = cell_rect.width.max(1) - 4;
516 let wrapped = wrap_greedy_ltr(font, text, wrap_w, 0, 0);
517 let mut clipped = ClipRenderer::new(renderer, cell_rect);
518 let mut line_y = baseline_y;
519 for line in &wrapped.lines {
520 let line_text = &text[line.start..line.end];
521 let line_advance_px = (line.advance_fp16 + 8) >> 4;
522
523 let x_off = match cell.align {
524 CellAlign::Left => cell_rect.x + 2,
525 CellAlign::Center => {
526 cell_rect.x + (cell_rect.width - line_advance_px) / 2
527 }
528 CellAlign::Right => {
529 cell_rect.x + cell_rect.width - line_advance_px - 2
530 }
531 };
532 let shaped = shape_text_ltr(font, line_text, (x_off, line_y), 0);
533 clipped.draw_text_shaped(&shaped, (0, 0), text_color);
534 line_y += metrics.line_height as i32;
535 }
536 }
537 }
538
539 x += cell_w;
541 if cell.ctrl.contains(CellCtrl::MERGE_RIGHT) {
542 }
549 }
550 y += row_h + 1; }
552 }
553}
554
555impl Widget for Table {
556 fn bounds(&self) -> Rect {
557 self.bounds
558 }
559
560 fn widget_font_mut(&mut self) -> Option<&mut WidgetFont> {
561 Some(&mut self.font)
562 }
563
564 fn set_bounds(&mut self, bounds: Rect) {
565 self.bounds = bounds;
566 self.recompute_row_heights();
567 }
568
569 fn draw(&self, renderer: &mut dyn Renderer) {
570 draw_widget_bg(renderer, self.bounds, &self.style);
572 self.draw_cells(renderer);
574 }
575
576 fn handle_event(&mut self, _event: &Event) -> bool {
577 false
578 }
579}
580
581#[cfg(test)]
586mod tests {
587 use super::*;
588
589 fn r(x: i32, y: i32, w: i32, h: i32) -> Rect {
590 Rect {
591 x,
592 y,
593 width: w,
594 height: h,
595 }
596 }
597
598 #[test]
599 fn set_size_and_cell_value() {
600 let mut t = Table::new(r(0, 0, 200, 100));
601 t.set_row_count(3);
602 t.set_column_count(4);
603 assert_eq!(t.row_count(), 3);
604 assert_eq!(t.column_count(), 4);
605
606 t.set_cell_value(1, 2, "hello");
607 assert_eq!(t.cell_value(1, 2), Some("hello"));
608 assert_eq!(t.cell_value(0, 0), None);
609 }
610
611 #[test]
612 fn out_of_bounds_cell_is_noop() {
613 let mut t = Table::new(r(0, 0, 200, 100));
614 t.set_row_count(2);
615 t.set_column_count(2);
616 t.set_cell_value(5, 5, "oob"); assert_eq!(t.cell_value(5, 5), None);
618 }
619
620 #[test]
621 fn column_widths_stored() {
622 let mut t = Table::new(r(0, 0, 200, 100));
623 t.set_column_count(3);
624 t.set_column_width(1, 60);
625 assert_eq!(t.column_width(1), 60);
626 assert_eq!(t.column_width(0), 0); }
628
629 #[test]
630 fn cell_align_stored() {
631 let mut t = Table::new(r(0, 0, 200, 100));
632 t.set_row_count(1);
633 t.set_column_count(2);
634 t.set_cell_align(0, 1, CellAlign::Center);
635 let idx = 1; assert_eq!(t.cells[idx].align, CellAlign::Center);
637 }
638
639 #[test]
640 fn merge_right_ctrl_flag() {
641 let mut t = Table::new(r(0, 0, 200, 100));
642 t.set_row_count(1);
643 t.set_column_count(3);
644 t.set_cell_ctrl(0, 0, CellCtrl::MERGE_RIGHT);
645 assert!(t.cell_ctrl(0, 0).contains(CellCtrl::MERGE_RIGHT));
646 assert!(!t.cell_ctrl(0, 1).contains(CellCtrl::MERGE_RIGHT));
647 }
648
649 #[test]
650 fn navigate_next_wraps() {
651 let mut t = Table::new(r(0, 0, 200, 100));
652 t.set_row_count(2);
653 t.set_column_count(2);
654 t.set_selected_cell(0, 0);
655
656 t.navigate_next();
657 assert_eq!(t.selected_cell(), Some((0, 1)));
658 t.navigate_next(); assert_eq!(t.selected_cell(), Some((1, 0)));
660 t.navigate_next();
661 assert_eq!(t.selected_cell(), Some((1, 1)));
662 t.navigate_next(); assert_eq!(t.selected_cell(), Some((0, 0)));
664 }
665
666 #[test]
667 fn navigate_prev_wraps() {
668 let mut t = Table::new(r(0, 0, 200, 100));
669 t.set_row_count(2);
670 t.set_column_count(2);
671 t.set_selected_cell(0, 0);
672 t.navigate_prev(); assert_eq!(t.selected_cell(), Some((1, 1)));
674 }
675
676 #[test]
677 fn navigate_up_down() {
678 let mut t = Table::new(r(0, 0, 200, 100));
679 t.set_row_count(3);
680 t.set_column_count(2);
681 t.set_selected_cell(1, 0);
682 t.navigate_up();
683 assert_eq!(t.selected_cell(), Some((0, 0)));
684 t.navigate_down();
685 assert_eq!(t.selected_cell(), Some((1, 0)));
686 }
687
688 #[test]
689 fn activate_selected_sets_poll_slot() {
690 let mut t = Table::new(r(0, 0, 200, 100));
691 t.set_row_count(2);
692 t.set_column_count(2);
693 t.set_selected_cell(1, 1);
694 t.activate_selected();
695 assert_eq!(t.last_activated(), Some((1, 1)));
696 assert_eq!(t.last_activated(), None); }
698
699 #[test]
700 fn resize_recomputes_row_heights() {
701 let mut t = Table::new(r(0, 0, 200, 100));
702 t.set_row_count(2);
703 t.set_column_count(2);
704 t.set_cell_value(0, 0, "Hello world this is a long line");
705 t.set_bounds(r(0, 0, 300, 150));
706 assert_eq!(t.bounds(), r(0, 0, 300, 150));
707 assert!(!t.row_heights.is_empty());
709 assert!(t.row_heights[0] > 0);
710 }
711
712 #[test]
713 fn column_count_change_preserves_existing_cells() {
714 let mut t = Table::new(r(0, 0, 200, 100));
715 t.set_row_count(1);
716 t.set_column_count(3);
717 t.set_cell_value(0, 1, "keep");
718 t.set_column_count(5);
719 assert_eq!(t.cell_value(0, 1), Some("keep"));
720 assert_eq!(t.column_count(), 5);
721 }
722}