Skip to main content

retroglyph_ui/widget/
table.rs

1//! [`Table`]: a fixed-column, scrollable table with a highlighted row.
2use retroglyph_core::color::{Color, Style};
3use retroglyph_core::grid::Rect;
4
5use super::window::visible_window;
6use super::{InteractiveWidget, Measure, StatefulWidget};
7use crate::Surface;
8use crate::align::Align;
9use crate::draw::fill_rect;
10use crate::interact::Response;
11use crate::interact::Sense;
12use crate::state::ListState;
13use crate::text::draw_clipped;
14use crate::theme::Theme;
15
16/// A fixed-column, scrollable table with a [`ListState`]-driven highlighted
17/// row.
18///
19/// `headers` render on the first row of the area it's rendered into;
20/// `rows` follow, one per line, clipped to that area. `widths` gives each
21/// column's cell width; columns are space-separated and truncated to fit.
22///
23/// `state.offset()` is the index of the first row drawn below the header --
24/// rendering draws whatever window `offset` names and does not clamp or
25/// auto-scroll it, matching [`ListState`]'s existing "only the caller knows
26/// the viewport height" design. Call
27/// [`state.ensure_visible(visible_row_count)`](ListState::ensure_visible)
28/// before rendering to keep `state.selected()` on-screen. If `selected()` is
29/// `Some` and its row falls within the visible window, that row is drawn
30/// with an inverted highlight background; if it has scrolled out of view,
31/// no row is highlighted.
32///
33/// `header_style`, `row_style`, and `selected_style` default to [`Theme::DARK`] (as if
34/// [`Table::theme`] had been called); set them with [`Table::header_style`],
35/// [`Table::row_style`], and [`Table::selected_style`]. `column_spacing` defaults to `1` (a
36/// single blank column between cells); set it with [`Table::column_spacing`].
37///
38/// As an [`InteractiveWidget`], a single id covers the whole table: a click selects the row
39/// under [`Response::pointer_pos`], resolved from this table's own row geometry (the header row
40/// plus `state.offset()`) the same way [`super::List`] resolves its own rows, and the wheel
41/// scrolls it via [`Response::scroll_delta`]/[`ListState::scroll_by`]. A click on the header row
42/// itself selects nothing.
43///
44/// # Examples
45///
46/// ```
47/// use retroglyph_core::grid::{Grid, Rect};
48/// use retroglyph_ui::state::ListState;
49/// use retroglyph_ui::widget::{StatefulWidget, Table};
50/// use retroglyph_ui::Surface;
51///
52/// let headers = ["Name", "Score"];
53/// let widths = [10u16, 6];
54/// let rows: [&[&str]; 2] = [&["Alpha", "10"], &["Bravo", "20"]];
55///
56/// let mut state = ListState::new();
57/// state.select(Some(1));
58///
59/// let area = Rect::new(0, 0, 20, 3);
60/// let mut grid = Grid::new(20, 3);
61/// Table::new(&headers, &widths, &rows).render(&mut Surface::new(&mut grid, area, 0), &mut state);
62/// ```
63#[derive(Clone, Copy, Debug)]
64pub struct Table<'a> {
65    headers: &'a [&'a str],
66    widths: &'a [u16],
67    rows: &'a [&'a [&'a str]],
68    header_style: Style,
69    row_style: Style,
70    selected_style: Style,
71    column_spacing: u16,
72}
73
74impl<'a> Table<'a> {
75    /// A table with the given header labels, column widths, and rows, styled from
76    /// [`Theme::DARK`] (as if [`Table::theme`] had been called).
77    #[must_use]
78    pub fn new(headers: &'a [&'a str], widths: &'a [u16], rows: &'a [&'a [&'a str]]) -> Self {
79        Self {
80            headers,
81            widths,
82            rows,
83            header_style: Style::new(),
84            row_style: Style::new(),
85            selected_style: Style::new(),
86            column_spacing: 1,
87        }
88        .theme(Theme::DARK)
89    }
90
91    /// Set the header row's style.
92    #[must_use]
93    pub const fn header_style(mut self, style: Style) -> Self {
94        self.header_style = style;
95        self
96    }
97
98    /// Set the style of unselected rows.
99    #[must_use]
100    pub const fn row_style(mut self, style: Style) -> Self {
101        self.row_style = style;
102        self
103    }
104
105    /// Set the style of the selected row, including its background fill.
106    #[must_use]
107    pub const fn selected_style(mut self, style: Style) -> Self {
108        self.selected_style = style;
109        self
110    }
111
112    /// Set the number of blank columns between cells.
113    #[must_use]
114    pub const fn column_spacing(mut self, spacing: u16) -> Self {
115        self.column_spacing = spacing;
116        self
117    }
118
119    /// Applies `theme`'s named roles to this table's row styles: `header_style` becomes
120    /// `theme.fg` (brighter, matching the header's original brighter-than-row default) on
121    /// `theme.panel_bg`, `row_style` becomes `theme.dim` (the same de-emphasized role a plain
122    /// body row already reads as) on `theme.panel_bg`, and `selected_style` becomes `theme.bg`
123    /// on `theme.accent`: the same bright-on-accent highlight [`super::List::theme`] and
124    /// [`super::Button::theme`] use.
125    ///
126    /// `header_style`/`row_style` always set an explicit background for the same reason, and with
127    /// the same caveat, as [`super::Gauge::theme`]; see its doc comment for the full explanation.
128    /// Drawing this table directly on the raw screen background instead of inside a themed panel
129    /// needs a manual `.header_style(...)`/`.row_style(...)` override afterwards.
130    ///
131    /// Call before any manual [`Table::header_style`]/[`Table::row_style`]/
132    /// [`Table::selected_style`] override you want to keep.
133    #[must_use]
134    pub fn theme(self, theme: Theme) -> Self {
135        self.theme_on(theme, theme.panel_bg)
136    }
137
138    /// Same as [`Table::theme`], but `header_style`/`row_style` are drawn on `bg` instead of
139    /// `theme.panel_bg`: for a table drawn directly on a backdrop other than a themed
140    /// [`super::Panel`]/[`super::Modal`]'s fill, e.g. the raw screen background or a different
141    /// panel's fill color. [`Table::theme`] is exactly `theme_on(theme, theme.panel_bg)`.
142    #[must_use]
143    pub fn theme_on(mut self, theme: Theme, bg: Color) -> Self {
144        self.header_style = Style::new().fg(theme.fg).bg(bg);
145        self.row_style = Style::new().fg(theme.dim).bg(bg);
146        self.selected_style = Style::new().fg(theme.bg).bg(theme.accent);
147        self
148    }
149}
150
151impl Table<'_> {
152    /// The shared drawing routine both [`StatefulWidget::render`] and
153    /// [`InteractiveWidget::render`] use.
154    fn draw(&self, surface: &mut Surface<'_>, state: &ListState) {
155        let (width, height) = (surface.width(), surface.height());
156        if width == 0 || height == 0 {
157            return;
158        }
159        draw_row(
160            surface,
161            width,
162            0,
163            self.headers,
164            self.widths,
165            RowStyle {
166                style: self.header_style,
167                bg: None,
168                column_spacing: self.column_spacing,
169            },
170        );
171
172        let visible_rows = usize::from(height).saturating_sub(1);
173        let selected = state.selected();
174        for (row_index, row) in visible_window(self.rows, state.offset(), visible_rows) {
175            // `row_index - state.offset()` is a row within the visible window, so it never
176            // exceeds `visible_rows` (this surface's own `u16` height).
177            #[allow(clippy::cast_possible_truncation)]
178            let row_offset = (row_index - state.offset()) as u16;
179            let y = 1 + row_offset;
180            let (style, bg) = if Some(row_index) == selected {
181                (self.selected_style, Some(self.selected_style.background()))
182            } else {
183                (self.row_style, None)
184            };
185            draw_row(
186                surface,
187                width,
188                y,
189                row,
190                self.widths,
191                RowStyle {
192                    style,
193                    bg,
194                    column_spacing: self.column_spacing,
195                },
196            );
197        }
198    }
199
200    /// The row index at `pos`, given `state`'s current scroll offset, or `None` if `pos` falls
201    /// on the header row or past the last row (not clamped to the last row).
202    fn index_at(
203        &self,
204        area: Rect,
205        state: &ListState,
206        pos: retroglyph_core::grid::Pos,
207    ) -> Option<usize> {
208        let row = pos.y.checked_sub(area.top())?.checked_sub(1)?; // row 0 is the header
209        let index = state.offset() + usize::from(row);
210        (index < self.rows.len()).then_some(index)
211    }
212}
213
214impl StatefulWidget for Table<'_> {
215    type State = ListState;
216
217    fn render(&self, surface: &mut Surface<'_>, state: &mut Self::State) {
218        self.draw(surface, state);
219    }
220}
221
222impl<Id> InteractiveWidget<Id> for Table<'_> {
223    type State = ListState;
224
225    /// A single id covers the whole table: clicking resolves which row via
226    /// [`Response::pointer_pos`] and this table's own row geometry, rather than each row
227    /// registering its own id.
228    fn sense(&self) -> Sense {
229        Sense::click() | Sense::SCROLL | Sense::HOVER
230    }
231
232    fn render(&self, surface: &mut Surface<'_>, state: &mut Self::State, response: Response<Id>) {
233        let area = surface.area();
234
235        // A click past the last row (or on the header row) selects nothing: it's neither
236        // clamped to the last row nor left as whatever was selected before.
237        if response.clicked()
238            && let Some(pos) = response.pointer_pos()
239            && let Some(index) = self.index_at(area, state, pos)
240        {
241            state.select(Some(index));
242        }
243        let scroll_delta = response.scroll_delta();
244        if scroll_delta != 0 {
245            state.scroll_by(scroll_delta);
246        }
247
248        self.draw(surface, state);
249    }
250}
251
252impl Measure for Table<'_> {
253    /// One row per data row, plus the always-drawn header row; `width` is ignored, since cells
254    /// are truncated per column rather than wrapped.
255    fn height_for(&self, _width: u16) -> u16 {
256        #[allow(clippy::cast_possible_truncation)]
257        let rows = self.rows.len().min(usize::from(u16::MAX)) as u16;
258        rows.saturating_add(1)
259    }
260}
261
262/// The style and layout options for drawing one [`Table`] row, grouped to keep [`draw_row`]'s
263/// argument count within clippy's limit.
264#[derive(Clone, Copy)]
265struct RowStyle {
266    /// The text (and, for the selected row, background) style.
267    style: Style,
268    /// When set, the whole row width is filled with this background first.
269    bg: Option<Color>,
270    /// The number of blank columns between cells.
271    column_spacing: u16,
272}
273
274/// Draw one table row of `column_spacing`-separated, per-column-clipped cells at row `y`, in
275/// this surface's own local coordinates (`width` columns starting at `0`).
276fn draw_row(
277    surface: &mut Surface<'_>,
278    width: u16,
279    y: u16,
280    cells: &[&str],
281    widths: &[u16],
282    row_style: RowStyle,
283) {
284    let RowStyle {
285        style,
286        bg,
287        column_spacing,
288    } = row_style;
289    if let Some(bg) = bg {
290        fill_rect(surface, Rect::new(0, y, width, 1), ' ', Style::new().bg(bg));
291    }
292    let mut x = 0u16;
293    for (cell, &w) in cells.iter().zip(widths) {
294        if x >= width {
295            break;
296        }
297        let avail = (width - x).min(w);
298        let _ = draw_clipped(surface, (x, y), avail, cell, Align::Left, style);
299        x = x.saturating_add(w.saturating_add(column_spacing));
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use alloc::vec;
306    use alloc::vec::Vec;
307
308    use retroglyph_core::grid::{Grid, Pos};
309
310    use super::*;
311
312    #[test]
313    fn table_widget_highlights_the_selected_row() {
314        let area = Rect::new(0, 0, 20, 3);
315        let headers = ["Name"];
316        let widths = [10u16];
317        let rows: [&[&str]; 2] = [&["Alpha"], &["Bravo"]];
318        let table = Table::new(&headers, &widths, &rows);
319
320        let mut grid = Grid::new(20, 3);
321        let mut state = ListState::new();
322        state.select(Some(1));
323        StatefulWidget::render(&table, &mut Surface::new(&mut grid, area, 0), &mut state);
324
325        // Row 1 ("Bravo") is highlighted; row 0 ("Alpha") is not.
326        let highlighted_bg = grid[Pos::new(0, 2)].style().background();
327        let plain_bg = grid[Pos::new(0, 1)].style().background();
328        assert_ne!(highlighted_bg, plain_bg);
329    }
330
331    #[test]
332    fn table_widget_highlights_nothing_when_unselected() {
333        let area = Rect::new(0, 0, 20, 3);
334        let headers = ["Name"];
335        let widths = [10u16];
336        let rows: [&[&str]; 2] = [&["Alpha"], &["Bravo"]];
337        let table = Table::new(&headers, &widths, &rows);
338
339        let mut grid = Grid::new(20, 3);
340        let mut state = ListState::new(); // nothing selected
341        StatefulWidget::render(&table, &mut Surface::new(&mut grid, area, 0), &mut state);
342
343        let row0_bg = grid[Pos::new(0, 1)].style().background();
344        let row1_bg = grid[Pos::new(0, 2)].style().background();
345        assert_eq!(row0_bg, row1_bg);
346    }
347
348    fn rows<'a>(names: &[&'a str]) -> Vec<[&'a str; 1]> {
349        names.iter().map(|n| [*n]).collect()
350    }
351
352    fn row_refs<'a>(rows: &'a [[&'a str; 1]]) -> Vec<&'a [&'a str]> {
353        rows.iter().map(<[&str; 1]>::as_slice).collect()
354    }
355
356    #[test]
357    fn scroll_offset_renders_the_window_starting_at_offset() {
358        // 2 visible rows (area height 3, minus the header row).
359        let area = Rect::new(0, 0, 20, 3);
360        let headers = ["Name"];
361        let widths = [10u16];
362        let rows = rows(&["Alpha", "Bravo", "Charlie", "Delta"]);
363        let rows = row_refs(&rows);
364        let table = Table::new(&headers, &widths, &rows);
365
366        let mut grid = Grid::new(20, 3);
367        let mut state = ListState::new();
368        state.set_offset(2); // window is [Charlie, Delta]
369        StatefulWidget::render(&table, &mut Surface::new(&mut grid, area, 0), &mut state);
370
371        // Row 1 is "Charlie", row 2 is "Delta"; neither "Alpha" nor "Bravo"
372        // (offset 0/1) are drawn anywhere.
373        assert_eq!(grid[Pos::new(0, 1)].glyph(), 'C');
374        assert_eq!(grid[Pos::new(0, 2)].glyph(), 'D');
375    }
376
377    #[test]
378    fn selection_scrolled_out_of_view_highlights_nothing() {
379        let area = Rect::new(0, 0, 20, 3);
380        let headers = ["Name"];
381        let widths = [10u16];
382        let rows = rows(&["Alpha", "Bravo", "Charlie", "Delta"]);
383        let rows = row_refs(&rows);
384        let table = Table::new(&headers, &widths, &rows);
385
386        let mut grid = Grid::new(20, 3);
387        let mut state = ListState::new();
388        state.select(Some(0)); // "Alpha"
389        state.set_offset(2); // but the window starts at "Charlie"
390        StatefulWidget::render(&table, &mut Surface::new(&mut grid, area, 0), &mut state);
391
392        let row0_bg = grid[Pos::new(0, 1)].style().background();
393        let row1_bg = grid[Pos::new(0, 2)].style().background();
394        assert_eq!(row0_bg, row1_bg); // neither visible row is highlighted
395    }
396
397    #[test]
398    fn height_for_is_row_count_plus_the_header_row() {
399        let headers = ["Name"];
400        let widths = [10u16];
401        let rows = rows(&["Alpha", "Bravo", "Charlie"]);
402        let rows = row_refs(&rows);
403        let table = Table::new(&headers, &widths, &rows);
404
405        assert_eq!(table.height_for(80), 4); // 3 rows + 1 header
406    }
407
408    #[test]
409    fn default_header_style_matches_theme_dark() {
410        let area = Rect::new(0, 0, 20, 2);
411        let headers = ["Name"];
412        let widths = [10u16];
413        let rows: Vec<&[&str]> = vec![];
414        let table = Table::new(&headers, &widths, &rows);
415
416        let mut grid = Grid::new(20, 2);
417        let mut state = ListState::new();
418        StatefulWidget::render(&table, &mut Surface::new(&mut grid, area, 0), &mut state);
419
420        assert_eq!(grid[Pos::new(0, 0)].style().foreground(), Theme::DARK.fg);
421        assert_eq!(
422            grid[Pos::new(0, 0)].style().background(),
423            Theme::DARK.panel_bg
424        );
425    }
426
427    #[test]
428    fn header_style_can_be_overridden() {
429        let area = Rect::new(0, 0, 20, 2);
430        let headers = ["Name"];
431        let widths = [10u16];
432        let rows: Vec<&[&str]> = vec![];
433        let custom = Style::new().fg(Color::RED);
434        let table = Table::new(&headers, &widths, &rows).header_style(custom);
435
436        let mut grid = Grid::new(20, 2);
437        let mut state = ListState::new();
438        StatefulWidget::render(&table, &mut Surface::new(&mut grid, area, 0), &mut state);
439
440        assert_eq!(grid[Pos::new(0, 0)].style().foreground(), Color::RED);
441    }
442
443    #[test]
444    fn selected_style_can_be_overridden() {
445        let area = Rect::new(0, 0, 20, 3);
446        let headers = ["Name"];
447        let widths = [10u16];
448        let rows: [&[&str]; 2] = [&["Alpha"], &["Bravo"]];
449        let custom = Style::new().fg(Color::GREEN).bg(Color::BLUE);
450        let table = Table::new(&headers, &widths, &rows).selected_style(custom);
451
452        let mut grid = Grid::new(20, 3);
453        let mut state = ListState::new();
454        state.select(Some(1));
455        StatefulWidget::render(&table, &mut Surface::new(&mut grid, area, 0), &mut state);
456
457        assert_eq!(grid[Pos::new(0, 2)].style().foreground(), Color::GREEN);
458        assert_eq!(grid[Pos::new(0, 2)].style().background(), Color::BLUE);
459    }
460
461    #[test]
462    fn theme_maps_named_roles_onto_header_row_and_selected_styles() {
463        let area = Rect::new(0, 0, 20, 3);
464        let headers = ["Name"];
465        let widths = [10u16];
466        let rows: [&[&str]; 2] = [&["Alpha"], &["Bravo"]];
467        let table = Table::new(&headers, &widths, &rows).theme(Theme::DARK);
468
469        let mut grid = Grid::new(20, 3);
470        let mut state = ListState::new();
471        state.select(Some(1));
472        StatefulWidget::render(&table, &mut Surface::new(&mut grid, area, 0), &mut state);
473
474        assert_eq!(grid[Pos::new(0, 0)].style().foreground(), Theme::DARK.fg);
475        assert_eq!(
476            grid[Pos::new(0, 0)].style().background(),
477            Theme::DARK.panel_bg
478        );
479        assert_eq!(grid[Pos::new(0, 1)].style().foreground(), Theme::DARK.dim);
480        assert_eq!(
481            grid[Pos::new(0, 1)].style().background(),
482            Theme::DARK.panel_bg
483        );
484        assert_eq!(grid[Pos::new(0, 2)].style().foreground(), Theme::DARK.bg);
485        assert_eq!(
486            grid[Pos::new(0, 2)].style().background(),
487            Theme::DARK.accent
488        );
489    }
490
491    #[test]
492    fn theme_on_uses_the_given_backdrop_instead_of_panel_bg() {
493        let area = Rect::new(0, 0, 20, 2);
494        let headers = ["Name"];
495        let widths = [10u16];
496        let rows: [&[&str]; 1] = [&["Alpha"]];
497        let table = Table::new(&headers, &widths, &rows).theme_on(Theme::DARK, Color::Default);
498
499        let mut grid = Grid::new(20, 2);
500        let mut state = ListState::new();
501        StatefulWidget::render(&table, &mut Surface::new(&mut grid, area, 0), &mut state);
502
503        assert_eq!(grid[Pos::new(0, 0)].style().foreground(), Theme::DARK.fg);
504        assert_eq!(grid[Pos::new(0, 0)].style().background(), Color::Default);
505        assert_eq!(grid[Pos::new(0, 1)].style().foreground(), Theme::DARK.dim);
506        assert_eq!(grid[Pos::new(0, 1)].style().background(), Color::Default);
507    }
508
509    #[test]
510    fn column_spacing_can_be_overridden() {
511        let area = Rect::new(0, 0, 20, 1);
512        let headers = ["A", "B"];
513        let widths = [1u16, 1u16];
514        let rows: Vec<&[&str]> = vec![];
515        let table = Table::new(&headers, &widths, &rows).column_spacing(3);
516
517        let mut grid = Grid::new(20, 1);
518        let mut state = ListState::new();
519        StatefulWidget::render(&table, &mut Surface::new(&mut grid, area, 0), &mut state);
520
521        // Default spacing (1) would put "B" at column 2; spacing 3 pushes
522        // it out to column 4.
523        assert_eq!(grid[Pos::new(0, 0)].glyph(), 'A');
524        assert_eq!(grid[Pos::new(4, 0)].glyph(), 'B');
525    }
526
527    #[test]
528    fn draw_row_column_width_plus_spacing_saturates_instead_of_overflowing() {
529        // A column width near `u16::MAX` combined with a nonzero `column_spacing` must not
530        // overflow the intermediate `w + column_spacing` addition (see issue #315); the whole
531        // expression should saturate to `u16::MAX` instead of panicking (debug) or wrapping
532        // (release).
533        let area = Rect::new(0, 0, 20, 1);
534        let cells: [&str; 2] = ["A", "B"];
535        let widths = [u16::MAX - 1, 1];
536        let row_style = RowStyle {
537            style: Style::new(),
538            bg: None,
539            column_spacing: 3,
540        };
541
542        let mut grid = Grid::new(20, 1);
543        draw_row(
544            &mut Surface::new(&mut grid, area, 0),
545            area.width(),
546            0,
547            &cells,
548            &widths,
549            row_style,
550        );
551
552        assert_eq!(grid[Pos::new(0, 0)].glyph(), 'A');
553    }
554
555    #[test]
556    fn ensure_visible_before_render_keeps_selection_on_screen() {
557        let area = Rect::new(0, 0, 20, 3); // 2 visible rows
558        let headers = ["Name"];
559        let widths = [10u16];
560        let rows = rows(&["Alpha", "Bravo", "Charlie", "Delta"]);
561        let rows = row_refs(&rows);
562        let table = Table::new(&headers, &widths, &rows);
563
564        let mut grid = Grid::new(20, 3);
565        let mut state = ListState::new();
566        state.select(Some(3)); // "Delta", off the front of the default window
567        state.ensure_visible(2);
568        StatefulWidget::render(&table, &mut Surface::new(&mut grid, area, 0), &mut state);
569
570        // ensure_visible moved the window to [2, 4): "Charlie" then "Delta",
571        // with "Delta" (the selection) highlighted on the last visible row.
572        assert_eq!(grid[Pos::new(0, 2)].glyph(), 'D');
573        let highlighted_bg = grid[Pos::new(0, 2)].style().background();
574        let plain_bg = grid[Pos::new(0, 1)].style().background();
575        assert_ne!(highlighted_bg, plain_bg);
576    }
577
578    #[test]
579    fn click_selects_the_row_under_the_pointer() {
580        let area = Rect::new(0, 0, 20, 3);
581        let headers = ["Name"];
582        let widths = [10u16];
583        let rows = rows(&["Alpha", "Bravo"]);
584        let rows = row_refs(&rows);
585        let table = Table::new(&headers, &widths, &rows);
586        let mut state = ListState::new();
587
588        let response: Response<()> = Response {
589            hovered: true,
590            clicked: true,
591            pointer_pos: Some(Pos::new(2, 2)), // row 1 of the table (below the header) -> "Bravo"
592            ..Response::default()
593        };
594        let mut grid = Grid::new(20, 3);
595        InteractiveWidget::render(
596            &table,
597            &mut Surface::new(&mut grid, area, 0),
598            &mut state,
599            response,
600        );
601        assert_eq!(state.selected(), Some(1));
602    }
603
604    #[test]
605    fn click_selects_the_row_under_the_pointer_with_a_scroll_offset() {
606        let area = Rect::new(0, 0, 20, 3); // 2 visible rows below the header
607        let headers = ["Name"];
608        let widths = [10u16];
609        let rows = rows(&["Alpha", "Bravo", "Charlie", "Delta"]);
610        let rows = row_refs(&rows);
611        let table = Table::new(&headers, &widths, &rows);
612        let mut state = ListState::new();
613        state.set_offset(2); // window is [Charlie, Delta]
614
615        let response: Response<()> = Response {
616            hovered: true,
617            clicked: true,
618            pointer_pos: Some(Pos::new(2, 2)), // row 1 of the window -> "Delta" (index 3)
619            ..Response::default()
620        };
621        let mut grid = Grid::new(20, 3);
622        InteractiveWidget::render(
623            &table,
624            &mut Surface::new(&mut grid, area, 0),
625            &mut state,
626            response,
627        );
628        assert_eq!(state.selected(), Some(3));
629    }
630
631    #[test]
632    fn click_on_the_header_row_selects_nothing() {
633        let area = Rect::new(0, 0, 20, 3);
634        let headers = ["Name"];
635        let widths = [10u16];
636        let rows = rows(&["Alpha", "Bravo"]);
637        let rows = row_refs(&rows);
638        let table = Table::new(&headers, &widths, &rows);
639        let mut state = ListState::new();
640        state.select(Some(0));
641
642        let response: Response<()> = Response {
643            hovered: true,
644            clicked: true,
645            pointer_pos: Some(Pos::new(2, 0)), // the header row itself
646            ..Response::default()
647        };
648        let mut grid = Grid::new(20, 3);
649        InteractiveWidget::render(
650            &table,
651            &mut Surface::new(&mut grid, area, 0),
652            &mut state,
653            response,
654        );
655        assert_eq!(state.selected(), Some(0)); // unchanged
656    }
657
658    #[test]
659    fn wheel_scroll_moves_the_offset() {
660        let area = Rect::new(0, 0, 20, 3);
661        let headers = ["Name"];
662        let widths = [10u16];
663        let rows = rows(&["Alpha", "Bravo", "Charlie", "Delta"]);
664        let rows = row_refs(&rows);
665        let table = Table::new(&headers, &widths, &rows);
666        let mut state = ListState::new();
667
668        let response: Response<()> = Response {
669            scroll_delta: 2,
670            ..Response::default()
671        };
672        let mut grid = Grid::new(20, 3);
673        InteractiveWidget::render(
674            &table,
675            &mut Surface::new(&mut grid, area, 0),
676            &mut state,
677            response,
678        );
679        assert_eq!(state.offset(), 2);
680    }
681}