Skip to main content

rlvgl_widgets/
table.rs

1//! LVGL-parity table widget with shaped-text cells (LPAR-14c).
2//!
3//! [`Table`] displays a grid of text cells with configurable column widths,
4//! per-cell alignment, optional cell spanning, and a keyboard-navigable
5//! selected-cell cursor.
6//!
7//! Text measurement and row-height computation use the LPAR-08 shared
8//! measurement primitives (`wrap_greedy_ltr`, `shape_text_ltr`) — no parallel
9//! text measurement is introduced.
10//!
11//! # Navigation
12//!
13//! Wire the navigate helpers to `ObjectEvent::Key` in a node handler:
14//!
15//! ```ignore
16//! table.navigate_next();      // Key::ArrowRight / Key::Tab
17//! table.navigate_prev();      // Key::ArrowLeft  / Key::BackTab
18//! table.navigate_up();        // Key::ArrowUp
19//! table.navigate_down();      // Key::ArrowDown
20//! table.activate_selected();  // Key::Enter
21//! ```
22
23use 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// ---------------------------------------------------------------------------
34// CellCtrl
35// ---------------------------------------------------------------------------
36
37/// Bit-flags controlling individual cell appearance and layout.
38///
39/// Registration policy: **Specification Required**.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
41pub struct CellCtrl(pub u8);
42
43impl CellCtrl {
44    /// No special behavior.
45    pub const NONE: Self = Self(0);
46    /// The cell visually spans into the next column (both widths are consumed).
47    pub const MERGE_RIGHT: Self = Self(1 << 0);
48    /// Suppress wrapping; crop text to the column width instead.
49    pub const TEXT_CROP: Self = Self(1 << 1);
50
51    /// Return `true` when `flag` is set.
52    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// ---------------------------------------------------------------------------
65// CellAlign
66// ---------------------------------------------------------------------------
67
68/// Horizontal text alignment within a table cell.
69///
70/// Registration policy: **Specification Required**.
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
72pub enum CellAlign {
73    /// Align text to the left edge of the cell.
74    #[default]
75    Left,
76    /// Center text horizontally in the cell.
77    Center,
78    /// Align text to the right edge of the cell.
79    Right,
80}
81
82// ---------------------------------------------------------------------------
83// Internal cell storage
84// ---------------------------------------------------------------------------
85
86#[derive(Default)]
87struct Cell {
88    text: Option<String>,
89    ctrl: CellCtrl,
90    align: CellAlign,
91}
92
93// ---------------------------------------------------------------------------
94// Table
95// ---------------------------------------------------------------------------
96
97/// LVGL-parity table widget.
98///
99/// Stores cells as a flat row-major `Vec`; column widths as a `Vec<i32>`.
100/// Row heights are computed dynamically from the cell content using the
101/// LPAR-08 shared `wrap_greedy_ltr` measurement.
102pub struct Table {
103    bounds: Rect,
104    rows: usize,
105    cols: usize,
106    cells: Vec<Cell>,
107    col_widths: Vec<i32>,
108    /// Row heights computed from content (cached; recomputed on set_bounds).
109    row_heights: Vec<i32>,
110    selected_row: Option<usize>,
111    selected_col: Option<usize>,
112    /// Last activated cell poll slot.
113    last_activated: Option<(usize, usize)>,
114    /// Background and outer border style.
115    pub style: Style,
116    /// Color for cell background and grid lines.
117    pub cell_bg_color: Color,
118    /// Color for the selected cell background.
119    pub selected_color: Color,
120    /// Text color.
121    pub text_color: Color,
122    /// Grid line color.
123    pub grid_color: Color,
124    /// Font assignment for this widget (FONT-00 §5); resolves to `FONT_6X10`
125    /// when unset.
126    font: WidgetFont,
127}
128
129impl Table {
130    /// Create an empty table at `bounds` with `0` rows and `0` columns.
131    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    /// Assign the font used to render this widget (FONT-00 §5); resolves to
152    /// `FONT_6X10` when unset.
153    pub fn set_font(&mut self, font: &'static dyn FontMetrics) {
154        self.font.set(font);
155    }
156
157    // ── Dimensions ────────────────────────────────────────────────────────
158
159    /// Return the number of rows.
160    pub fn row_count(&self) -> usize {
161        self.rows
162    }
163
164    /// Return the number of columns.
165    pub fn column_count(&self) -> usize {
166        self.cols
167    }
168
169    /// Set the number of rows, extending or truncating the cell vector.
170    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    /// Set the number of columns, extending or truncating; preserves existing
177    /// cell content where indices overlap.
178    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    // ── Cell accessors ────────────────────────────────────────────────────
206
207    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    /// Set the text value of a cell.
216    ///
217    /// Out-of-bounds indices are silently ignored (matching LVGL behavior).
218    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    /// Return the text value of a cell, or `None` if empty or out of bounds.
226    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    /// Set control flags for a cell.
232    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    /// Return control flags for a cell.
239    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    /// Set horizontal alignment for a cell.
246    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    // ── Column widths ─────────────────────────────────────────────────────
253
254    /// Set the pixel width of column `col`.
255    ///
256    /// A width of `0` means "auto" — the column participates in the remaining
257    /// width distribution.
258    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    /// Return the stored column width (0 = auto).
265    pub fn column_width(&self, col: usize) -> i32 {
266        self.col_widths.get(col).copied().unwrap_or(0)
267    }
268
269    /// Resolve effective column widths, distributing remaining space evenly
270    /// among auto columns.
271    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    // ── Row height computation ─────────────────────────────────────────────
296
297    /// Recompute row heights using the LPAR-08 shared `wrap_greedy_ltr`.
298    ///
299    /// Each row's height is the maximum wrapped height across all cells in that
300    /// row. This is the load-bearing use of the shared font measurement.
301    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                // Use the LPAR-08 shared greedy-wrap — not per-cell advance arithmetic.
317                let wrap_w = if cell.ctrl.contains(CellCtrl::TEXT_CROP) {
318                    i32::MAX // no wrapping; measure single line
319                } 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    // ── Selection ─────────────────────────────────────────────────────────
333
334    /// Set the selected cell. Out-of-bounds is silently ignored.
335    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    /// Return the selected cell `(row, col)`, or `None` if nothing is selected.
343    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    /// Move selection to the next cell (right then down; wraps at the last cell
351    /// to the first).
352    ///
353    /// Wire to `ObjectEvent::Key(Key::ArrowRight)` or `Key::Tab` in a node handler.
354    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    /// Move selection to the previous cell (left then up; wraps at the first
371    /// cell to the last).
372    ///
373    /// Wire to `ObjectEvent::Key(Key::ArrowLeft)` or `Key::BackTab`.
374    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    /// Move selection one row up.
391    ///
392    /// Wire to `ObjectEvent::Key(Key::ArrowUp)` in a node handler.
393    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    /// Move selection one row down.
408    ///
409    /// Wire to `ObjectEvent::Key(Key::ArrowDown)` in a node handler.
410    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    /// Activate the selected cell and record it in the poll slot.
425    ///
426    /// Wire to `ObjectEvent::Key(Key::Enter)` in a node handler.
427    pub fn activate_selected(&mut self) {
428        if let Some(cell) = self.selected_cell() {
429            self.last_activated = Some(cell);
430        }
431    }
432
433    /// Drain the activation poll slot.
434    pub fn last_activated(&mut self) -> Option<(usize, usize)> {
435        self.last_activated.take()
436    }
437
438    // ── Internal draw helpers ─────────────────────────────────────────────
439
440    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                // Span: consume the next column's width too.
460                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                // Background
472                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                // Grid border (right + bottom edges)
481                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                // Text
501                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                        // Single-line crop: shape once and draw with clip.
510                        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                        // Wrapped text via the SHARED `wrap_greedy_ltr` measurement.
515                        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                // Advance x; if MERGE_RIGHT skip the next column.
540                x += cell_w;
541                if cell.ctrl.contains(CellCtrl::MERGE_RIGHT) {
542                    // We already consumed c+1's width above; skip that column's
543                    // own draw by bumping x past it (it will just draw the
544                    // already-covered area — the simpler approach is to handle
545                    // it in the outer loop via a skip flag, but for v1 we draw
546                    // it invisibly: its rect is zero-width because x consumed
547                    // its width).
548                }
549            }
550            y += row_h + 1; // +1 for grid line
551        }
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        // Part::MAIN — background
571        draw_widget_bg(renderer, self.bounds, &self.style);
572        // Part::ITEMS + Part::SELECTED — cells
573        self.draw_cells(renderer);
574    }
575
576    fn handle_event(&mut self, _event: &Event) -> bool {
577        false
578    }
579}
580
581// ---------------------------------------------------------------------------
582// Tests
583// ---------------------------------------------------------------------------
584
585#[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"); // no-op
617        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); // auto
627    }
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; // row 0, col 1 in a 2-col table
636        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(); // end of row 0 → row 1, col 0
659        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(); // wrap to start
663        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(); // wrap to last cell
673        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); // drained
697    }
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        // Row heights are recomputed — just ensure they are non-zero.
708        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}