Skip to main content

rat_ftable2/table/
widget.rs

1use crate::_private::NonExhaustive;
2use crate::TableStyle;
3use crate::selection::RowSelection;
4use crate::table::TableState;
5use crate::util::{fallback_select_style, revert_style, transfer_buffer};
6use crate::{LayoutWidth, TableContext, TableSelection};
7use rat_scrolled::{Scroll, ScrollArea, ScrollAreaState};
8use ratatui_core::buffer::Buffer;
9use ratatui_core::layout::{Constraint, Flex, Layout, Rect};
10use ratatui_core::style::Style;
11use ratatui_core::text::Text;
12use ratatui_core::widgets::{StatefulWidget, Widget};
13use ratatui_widgets::block::Block;
14use std::cmp::max;
15use std::marker::PhantomData;
16use std::rc::Rc;
17
18/// Table widget that renders data from an Iterator.
19///
20/// This will do minimal rendering of the visible rows/cells
21/// of the table.
22///
23/// ```rust, ignore
24/// Table::default()
25///         .data(state.table_data.iter())
26///         .widths([
27///             Constraint::Length(6),
28///             Constraint::Length(20),
29///             Constraint::Length(15),
30///             Constraint::Length(15),
31///             Constraint::Length(3),
32///         ])
33///         .render_cell(0, |_v, area, buf, ctx| {
34///             row_fmt.fmt_u(ctx.row).render(area, buf)
35///         })
36///         .render_cell(1, |v, area, buf, _ctx| v.text.render(area, buf))
37///         .render_cell(2, |v, area, buf, _ctx| {
38///             num1_fmt.fmt_u(v.num1).render(area, buf)
39///         })
40///         .render_cell(3, |v, area, buf, _ctx| {
41///             num2_fmt.fmt_u(v.num2).render(area, buf)
42///         })
43///         .render_cell(4, |v, area, buf, _ctx| {
44///             let cc = if v.check { "\u{2622}" } else { "\u{2623}" };
45///             cc.render(area, buf);
46///         })
47///         .column_spacing(1)
48///         .header(vec!["Nr", "Text", "Val1", "Val2", "State"])
49///         .footer(vec!["a", "b", "c", "d", "e"])
50///         .block(
51///             Block::bordered()
52///                 .border_type(BorderType::Rounded)
53///                 .title("tabledata-iter (no row-count)"),
54///         )
55///         .vscroll(Scroll::new())
56///         .styles(table(&ctx.theme))
57///         .render(l0[0], buf, &mut state.table);
58/// ```
59pub struct Table<'a, T: Copy, Selection = RowSelection> {
60    header: Vec<Text<'a>>,
61    footer: Vec<Text<'a>>,
62
63    data: Option<Box<dyn Iterator<Item = T> + 'a>>,
64
65    row_height_fn: Option<Box<dyn FnMut(T, &TableContext) -> u8 + 'a>>,
66    render_row_fn: Option<Box<dyn FnMut(T, Rect, &mut Buffer, &TableContext) -> bool + 'a>>,
67    render_cell_fn: Vec<Option<Box<dyn FnMut(T, Rect, &mut Buffer, &TableContext) + 'a>>>,
68
69    widths: Vec<Constraint>,
70    varying_heights: bool,
71    flex: Flex,
72    column_spacing: u16,
73    layout_width: LayoutWidth,
74
75    style: Style,
76    block: Option<Block<'a>>,
77    hscroll: Option<Scroll<'a>>,
78    vscroll: Option<Scroll<'a>>,
79    header_style: Option<Style>,
80    footer_style: Option<Style>,
81    focus_style: Option<Style>,
82
83    select_row_style: Option<Style>,
84    show_row_focus: bool,
85    select_column_style: Option<Style>,
86    show_column_focus: bool,
87    select_cell_style: Option<Style>,
88    show_cell_focus: bool,
89    select_header_style: Option<Style>,
90    show_header_focus: bool,
91    select_footer_style: Option<Style>,
92    show_footer_focus: bool,
93
94    _phantom: PhantomData<Selection>,
95}
96
97impl<'a, T: Copy, Selection> Default for Table<'a, T, Selection> {
98    fn default() -> Self {
99        Self {
100            header: Default::default(),
101            footer: Default::default(),
102            data: Default::default(),
103            row_height_fn: Default::default(),
104            render_row_fn: Default::default(),
105            render_cell_fn: Default::default(),
106            widths: Default::default(),
107            varying_heights: Default::default(),
108            flex: Default::default(),
109            column_spacing: Default::default(),
110            layout_width: Default::default(),
111            style: Default::default(),
112            block: Default::default(),
113            hscroll: Default::default(),
114            vscroll: Default::default(),
115            header_style: Default::default(),
116            footer_style: Default::default(),
117            focus_style: Default::default(),
118            select_row_style: Default::default(),
119            show_row_focus: true,
120            select_column_style: Default::default(),
121            show_column_focus: Default::default(),
122            select_cell_style: Default::default(),
123            show_cell_focus: Default::default(),
124            select_header_style: Default::default(),
125            show_header_focus: Default::default(),
126            select_footer_style: Default::default(),
127            show_footer_focus: Default::default(),
128            _phantom: Default::default(),
129        }
130    }
131}
132
133impl<'a, T: Copy, Selection> Table<'a, T, Selection> {
134    /// New, empty Table.
135    pub fn new() -> Self
136    where
137        Selection: Default,
138    {
139        Self::default()
140    }
141
142    #[inline]
143    pub fn data(mut self, data: impl IntoIterator<Item = T> + 'a) -> Self {
144        self.data = Some(Box::new(data.into_iter()));
145        self
146    }
147
148    /// Set the closure that calculates the row-height for a value.
149    #[inline]
150    pub fn row_height(mut self, row_height_fn: impl FnMut(T, &TableContext) -> u8 + 'a) -> Self {
151        self.row_height_fn = Some(Box::new(row_height_fn));
152        self
153    }
154
155    /// Set a closure for row-level rendering.
156    #[inline]
157    pub fn render_row(
158        mut self,
159        render_row_fn: impl FnMut(T, Rect, &mut Buffer, &TableContext) -> bool + 'a,
160    ) -> Self {
161        self.render_row_fn = Some(Box::new(render_row_fn));
162        self
163    }
164
165    /// Set a closure for rendering one column.
166    #[inline]
167    pub fn render_cell(
168        mut self,
169        column: usize,
170        render_cell_fn: impl FnMut(T, Rect, &mut Buffer, &TableContext) + 'a,
171    ) -> Self {
172        while self.render_cell_fn.len() <= column {
173            self.render_cell_fn.push(None);
174        }
175        self.render_cell_fn[column] = Some(Box::new(render_cell_fn));
176        self
177    }
178
179    /// Set the table-header.
180    #[inline]
181    pub fn header<I>(mut self, header: I) -> Self
182    where
183        I: IntoIterator,
184        I::Item: Into<Text<'a>>,
185    {
186        self.header = header.into_iter().map(|v| v.into()).collect();
187        self
188    }
189
190    /// Set the table-footer.
191    #[inline]
192    pub fn footer<I>(mut self, footer: I) -> Self
193    where
194        I: IntoIterator,
195        I::Item: Into<Text<'a>>,
196    {
197        self.footer = footer.into_iter().map(|v| v.into()).collect();
198        self
199    }
200
201    /// Column widths as Constraints.
202    #[inline]
203    pub fn widths<I>(mut self, widths: I) -> Self
204    where
205        I: IntoIterator,
206        I::Item: Into<Constraint>,
207    {
208        self.widths = widths.into_iter().map(|v| v.into()).collect();
209        self
210    }
211
212    /// If your table uses varying row-heights for each row
213    /// all page calculations will fail (somewhat).
214    /// If you enable this flag, the row-heights for
215    /// each row will be stored in the state to allow
216    /// accurate page-calculations.
217    pub fn varying_heights(mut self, varying: bool) -> Self {
218        self.varying_heights = varying;
219        self
220    }
221
222    /// Flex for layout.
223    #[inline]
224    pub fn flex(mut self, flex: Flex) -> Self {
225        self.flex = flex;
226        self
227    }
228
229    /// Spacing between columns.
230    #[inline]
231    pub fn column_spacing(mut self, spacing: u16) -> Self {
232        self.column_spacing = spacing;
233        self
234    }
235
236    /// Set the display width of the table.
237    /// If this is not set, the width of the rendered area is used.
238    #[inline]
239    pub fn layout_width(mut self, width: LayoutWidth) -> Self {
240        self.layout_width = width;
241        self
242    }
243
244    /// Draws a block around the table widget.
245    #[inline]
246    pub fn block(mut self, block: Block<'a>) -> Self {
247        self.block = Some(block);
248        self.block = self.block.map(|v| v.style(self.style));
249        self
250    }
251
252    /// Sets the border-style for the Block, if any.
253    pub fn border_style(mut self, style: Style) -> Self {
254        self.block = self.block.map(|v| v.border_style(style));
255        self
256    }
257
258    /// Sets the title-style for the Block, if any.
259    pub fn title_style(mut self, style: Style) -> Self {
260        self.block = self.block.map(|v| v.title_style(style));
261        self
262    }
263
264    /// Scrollbars
265    pub fn scroll(mut self, scroll: Scroll<'a>) -> Self {
266        self.hscroll = Some(scroll.clone().override_horizontal());
267        self.vscroll = Some(scroll.override_vertical());
268        self
269    }
270
271    /// Scrollbars
272    pub fn hscroll(mut self, scroll: Scroll<'a>) -> Self {
273        self.hscroll = Some(scroll.override_horizontal());
274        self
275    }
276
277    /// Scrollbars
278    pub fn vscroll(mut self, scroll: Scroll<'a>) -> Self {
279        self.vscroll = Some(scroll.override_vertical());
280        self
281    }
282
283    /// Set all styles as a bundle.
284    #[inline]
285    pub fn styles(mut self, styles: TableStyle) -> Self {
286        self.style = styles.style;
287        if styles.block.is_some() {
288            self.block = styles.block;
289        }
290        if let Some(border_style) = styles.border_style {
291            self.block = self.block.map(|v| v.border_style(border_style));
292        }
293        if let Some(title_style) = styles.title_style {
294            self.block = self.block.map(|v| v.title_style(title_style));
295        }
296        self.block = self.block.map(|v| v.style(self.style));
297
298        if let Some(styles) = styles.scroll {
299            self.hscroll = self.hscroll.map(|v| v.styles(styles.clone()));
300            self.vscroll = self.vscroll.map(|v| v.styles(styles));
301        }
302        if styles.header.is_some() {
303            self.header_style = styles.header;
304        }
305        if styles.footer.is_some() {
306            self.footer_style = styles.footer;
307        }
308        if styles.select_row.is_some() {
309            self.select_row_style = styles.select_row;
310        }
311        self.show_row_focus = styles.show_row_focus;
312        if styles.select_column.is_some() {
313            self.select_column_style = styles.select_column;
314        }
315        self.show_column_focus = styles.show_column_focus;
316        if styles.select_cell.is_some() {
317            self.select_cell_style = styles.select_cell;
318        }
319        self.show_cell_focus = styles.show_cell_focus;
320        if styles.select_header.is_some() {
321            self.select_header_style = styles.select_header;
322        }
323        self.show_header_focus = styles.show_header_focus;
324        if styles.select_footer.is_some() {
325            self.select_footer_style = styles.select_footer;
326        }
327        self.show_footer_focus = styles.show_footer_focus;
328        if styles.focus_style.is_some() {
329            self.focus_style = styles.focus_style;
330        }
331        self
332    }
333
334    /// Base style for the table.
335    #[inline]
336    pub fn style(mut self, style: Style) -> Self {
337        self.style = style;
338        self.block = self.block.map(|v| v.style(self.style));
339        self
340    }
341
342    /// Base style for the table.
343    #[inline]
344    pub fn header_style(mut self, style: Option<Style>) -> Self {
345        self.header_style = style;
346        self
347    }
348
349    /// Base style for the table.
350    #[inline]
351    pub fn footer_style(mut self, style: Option<Style>) -> Self {
352        self.footer_style = style;
353        self
354    }
355
356    /// Style for a selected row. The chosen selection must support
357    /// row-selection for this to take effect.
358    #[inline]
359    pub fn select_row_style(mut self, select_style: Option<Style>) -> Self {
360        self.select_row_style = select_style;
361        self
362    }
363
364    /// Add the focus-style to the row-style if the table is focused.
365    #[inline]
366    pub fn show_row_focus(mut self, show: bool) -> Self {
367        self.show_row_focus = show;
368        self
369    }
370
371    /// Style for a selected column. The chosen selection must support
372    /// column-selection for this to take effect.
373    #[inline]
374    pub fn select_column_style(mut self, select_style: Option<Style>) -> Self {
375        self.select_column_style = select_style;
376        self
377    }
378
379    /// Add the focus-style to the column-style if the table is focused.
380    #[inline]
381    pub fn show_column_focus(mut self, show: bool) -> Self {
382        self.show_column_focus = show;
383        self
384    }
385
386    /// Style for a selected cell. The chosen selection must support
387    /// cell-selection for this to take effect.
388    #[inline]
389    pub fn select_cell_style(mut self, select_style: Option<Style>) -> Self {
390        self.select_cell_style = select_style;
391        self
392    }
393
394    /// Add the focus-style to the cell-style if the table is focused.
395    #[inline]
396    pub fn show_cell_focus(mut self, show: bool) -> Self {
397        self.show_cell_focus = show;
398        self
399    }
400
401    /// Style for a selected header cell. The chosen selection must
402    /// support column-selection for this to take effect.
403    #[inline]
404    pub fn select_header_style(mut self, select_style: Option<Style>) -> Self {
405        self.select_header_style = select_style;
406        self
407    }
408
409    /// Add the focus-style to the header-style if the table is focused.
410    #[inline]
411    pub fn show_header_focus(mut self, show: bool) -> Self {
412        self.show_header_focus = show;
413        self
414    }
415
416    /// Style for a selected footer cell. The chosen selection must
417    /// support column-selection for this to take effect.
418    #[inline]
419    pub fn select_footer_style(mut self, select_style: Option<Style>) -> Self {
420        self.select_footer_style = select_style;
421        self
422    }
423
424    /// Add the footer-style to the table-style if the table is focused.
425    #[inline]
426    pub fn show_footer_focus(mut self, show: bool) -> Self {
427        self.show_footer_focus = show;
428        self
429    }
430
431    /// This style will be patched onto the selection to indicate that
432    /// the widget has the input focus.
433    ///
434    /// The selection must support some kind of selection for this to
435    /// be effective.
436    #[inline]
437    pub fn focus_style(mut self, focus_style: Option<Style>) -> Self {
438        self.focus_style = focus_style;
439        self
440    }
441
442    /// Inherent width.
443    pub fn width(&self) -> u16 {
444        let sa = ScrollArea::new()
445            .style(self.style)
446            .block(self.block.as_ref())
447            .h_scroll(self.hscroll.as_ref())
448            .v_scroll(self.vscroll.as_ref());
449        let padding = sa.padding();
450        let width = total_width(self, 0);
451
452        width + padding.left + padding.right
453    }
454
455    /// Inherent height.
456    pub fn height(&self) -> u16 {
457        let sa = ScrollArea::new()
458            .style(self.style)
459            .block(self.block.as_ref())
460            .h_scroll(self.hscroll.as_ref())
461            .v_scroll(self.vscroll.as_ref());
462        let padding = sa.padding();
463        let header = self
464            .header
465            .iter()
466            .map(|v| v.height() as u16)
467            .max()
468            .unwrap_or(0);
469        let footer = self
470            .footer
471            .iter()
472            .map(|v| v.height() as u16)
473            .max()
474            .unwrap_or(0);
475
476        1 + header + footer + padding.top + padding.bottom
477    }
478}
479
480fn total_width<T: Copy, Selection>(widget: &Table<'_, T, Selection>, area_width: u16) -> u16 {
481    match widget.layout_width {
482        LayoutWidth::Length(len) => len,
483        LayoutWidth::Columns => {
484            let mut width = 0;
485            for w in widget.widths.iter().copied() {
486                match w {
487                    Constraint::Min(v) => width += v + widget.column_spacing,
488                    Constraint::Max(v) => width += v + widget.column_spacing,
489                    Constraint::Length(v) => width += v + widget.column_spacing,
490                    Constraint::Percentage(p) => {
491                        width += (((area_width as u32) * (p as u32)) / 100) as u16;
492                    }
493                    Constraint::Ratio(n, d) => {
494                        width += (((area_width as u32) * n) / d) as u16;
495                    }
496                    Constraint::Fill(_) => {
497                        /* unsupported, use dummy */
498                        width += 10;
499                    }
500                }
501            }
502            max(width, area_width)
503        }
504        LayoutWidth::Area => area_width,
505    }
506}
507
508// Do the column-layout. Fill in missing columns, if necessary.
509fn layout_columns<T: Copy, Selection>(
510    widget: &Table<'_, T, Selection>,
511    area_width: u16,
512) -> (u16, Rc<[Rect]>, Rc<[Rect]>) {
513    let width = total_width(widget, area_width);
514    let area = Rect::new(0, 0, width, 0);
515
516    let (layout, spacers) = Layout::horizontal(&widget.widths)
517        .flex(widget.flex)
518        .spacing(widget.column_spacing)
519        .split_with_spacers(area);
520
521    (width, layout, spacers)
522}
523
524// Layout header/table/footer
525#[inline]
526fn layout_areas<T: Copy, Selection>(widget: &Table<'_, T, Selection>, area: Rect) -> Rc<[Rect]> {
527    let heights = vec![
528        Constraint::Length(
529            widget
530                .header
531                .iter()
532                .map(|v| v.height() as u16)
533                .max()
534                .unwrap_or(0),
535        ),
536        Constraint::Fill(1),
537        Constraint::Length(
538            widget
539                .footer
540                .iter()
541                .map(|v| v.height() as u16)
542                .max()
543                .unwrap_or(0),
544        ),
545    ];
546
547    Layout::vertical(heights).split(area)
548}
549
550impl<'a, T: Copy, Selection> StatefulWidget for Table<'a, T, Selection>
551where
552    T: 'a,
553    Selection: TableSelection + Default,
554{
555    type State = TableState<Selection>;
556
557    fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
558        let data = self.data.take();
559        if let Some(mut data) = data {
560            render_table(self, Some(data.as_mut()), area, buf, state);
561        } else {
562            render_table(self, None, area, buf, state);
563        }
564    }
565}
566
567fn render_table<'a, T: Copy, Selection: TableSelection>(
568    mut widget: Table<'a, T, Selection>,
569    mut data: Option<&mut dyn Iterator<Item = T>>,
570    area: Rect,
571    buf: &mut Buffer,
572    state: &mut TableState<Selection>,
573) {
574    state.rows = data.as_mut().map(|v| v.size_hint().0).unwrap_or(0);
575    state.columns = widget.widths.len();
576    state.area = area;
577
578    let sa = ScrollArea::new()
579        .style(widget.style)
580        .block(widget.block.as_ref())
581        .h_scroll(widget.hscroll.as_ref())
582        .v_scroll(widget.vscroll.as_ref());
583    state.inner = sa.inner(area, Some(&state.hscroll), Some(&state.vscroll));
584
585    let l_rows = layout_areas(&widget, state.inner);
586    state.header_area = l_rows[0];
587    state.table_area = l_rows[1];
588    state.footer_area = l_rows[2];
589
590    // horizontal layout
591    let (width, l_columns, l_spacers) = layout_columns(&widget, state.table_area.width);
592    calculate_column_areas(state.columns, l_columns.as_ref(), l_spacers.as_ref(), state);
593
594    // render block only
595    sa.render_block(area, buf);
596
597    // render header & footer
598    render_header(
599        &widget,
600        state.columns,
601        width,
602        l_columns.as_ref(),
603        l_spacers.as_ref(),
604        state.header_area,
605        buf,
606        state,
607    );
608    render_footer(
609        &widget,
610        state.columns,
611        width,
612        l_columns.as_ref(),
613        l_spacers.as_ref(),
614        state.footer_area,
615        buf,
616        state,
617    );
618
619    // render table
620    state.row_areas.clear();
621    state.row_heights.clear();
622    state.vscroll.set_page_len(0);
623    state.hscroll.set_page_len(area.width as usize);
624
625    let mut ctx = TableContext {
626        row: 0,
627        focus: state.focus.get(),
628        selected_cell: false,
629        selected_row: false,
630        selected_column: false,
631        style: widget.style,
632        select_style: None,
633        column_layout: &state.column_layout,
634        space_area: Default::default(),
635        row_area: Default::default(),
636        non_exhaustive: NonExhaustive,
637    };
638
639    'll: {
640        let Some(data) = data else {
641            break 'll;
642        };
643
644        // iterate to offset row.
645        let mut row = None;
646        let mut value;
647        let mut row_y = state.table_area.y;
648        let mut row_heights = Vec::new();
649        let mut row_buf = Buffer::empty(Rect::new(0, 0, width, 1));
650
651        // skip to offset.
652        if widget.varying_heights {
653            loop {
654                value = if let Some(v) = data.nth(0) {
655                    v
656                } else {
657                    break 'll;
658                };
659
660                if let Some(rr) = row {
661                    if rr + 1 < state.vscroll.offset() {
662                        ctx.row = rr;
663                        let row_height = widget
664                            .row_height_fn
665                            .as_mut()
666                            .map_or(1, |f| (*f)(value, &ctx));
667                        state.row_heights.push(row_height, true)
668                    }
669                    row = Some(rr + 1);
670                } else {
671                    if 0 < state.vscroll.offset() {
672                        ctx.row = 0;
673                        let row_height = widget
674                            .row_height_fn
675                            .as_mut()
676                            .map_or(1, |f| (*f)(value, &ctx));
677                        state.row_heights.push(row_height, true)
678                    }
679                    row = Some(0);
680                }
681
682                if row.expect("row") >= state.vscroll.offset() {
683                    break;
684                }
685            }
686        } else {
687            value = if let Some(v) = data.nth(state.vscroll.offset()) {
688                v
689            } else {
690                break 'll;
691            };
692            row = Some(state.vscroll.offset());
693        }
694
695        // render-loop
696        loop {
697            ctx.row = row.expect("row");
698            ctx.selected_cell = false;
699            ctx.selected_row = false;
700            ctx.selected_column = false;
701            ctx.select_style = None;
702            ctx.focus = false;
703            ctx.row_area = Rect::default();
704            ctx.space_area = Rect::default();
705
706            // Render each row to a temporary buffer.
707            let row_height = widget
708                .row_height_fn
709                .as_mut()
710                .map_or(1, |f| (*f)(value, &ctx)) as u16;
711            let render_row_area = Rect::new(0, 0, width, row_height);
712            ctx.row_area = render_row_area;
713            row_buf.resize(render_row_area);
714            row_heights.push(row_height);
715            state
716                .row_heights
717                .push(row_height as u8, widget.varying_heights);
718            // Target area for the finished row.
719            let visible_row_area = Rect::new(
720                state.table_area.x,
721                row_y,
722                state.table_area.width,
723                render_row_area.height,
724            )
725            .intersection(state.table_area);
726            state.row_areas.push(visible_row_area);
727            state.vscroll.page_len += 1;
728
729            if state.selection.is_selected_row(row.expect("row")) {
730                ctx.selected_row = true;
731                ctx.select_style = patch_select(
732                    widget.select_row_style,
733                    widget.focus_style,
734                    state.focus.get(),
735                    widget.show_row_focus,
736                );
737            }
738
739            let render_cells = if render_row_area.height > 0 {
740                if let Some(row_style) = ctx.select_style {
741                    row_buf.set_style(render_row_area, row_style);
742                } else {
743                    row_buf.set_style(render_row_area, widget.style);
744                }
745
746                if let Some(render_row) = widget.render_row_fn.as_mut() {
747                    render_row(value, render_row_area, &mut row_buf, &ctx)
748                } else {
749                    true
750                }
751            } else {
752                true
753            };
754
755            // can skip this entirely
756            if render_cells && render_row_area.height > 0 {
757                let mut col = 0;
758                loop {
759                    if col >= state.columns {
760                        break;
761                    }
762
763                    ctx.selected_cell = false;
764                    ctx.selected_column = false;
765                    ctx.select_style = None;
766
767                    let render_cell_area = Rect::new(
768                        l_columns[col].x,
769                        0,
770                        l_columns[col].width,
771                        render_row_area.height,
772                    );
773                    ctx.space_area = Rect::new(
774                        l_spacers[col + 1].x,
775                        0,
776                        l_spacers[col + 1].width,
777                        render_row_area.height,
778                    );
779
780                    if state.selection.is_selected_column(col) {
781                        ctx.selected_column = true;
782                        ctx.select_style = patch_select(
783                            widget.select_column_style,
784                            widget.focus_style,
785                            state.focus.get(),
786                            widget.show_column_focus,
787                        );
788                    }
789
790                    if state.selection.is_selected_cell(col, row.expect("row")) {
791                        ctx.selected_cell = true;
792                        ctx.select_style = patch_select(
793                            widget.select_cell_style,
794                            widget.focus_style,
795                            state.focus.get(),
796                            widget.show_cell_focus,
797                        );
798                    }
799
800                    // at least partially visible?
801                    if render_cell_area.right() > state.hscroll.offset as u16
802                        || render_cell_area.left() < state.hscroll.offset as u16 + area.width
803                    {
804                        if let Some(select_style) = ctx.select_style {
805                            row_buf.set_style(render_cell_area, select_style);
806                            row_buf.set_style(ctx.space_area, select_style);
807                        }
808
809                        if let Some(Some(render_cell)) = widget.render_cell_fn.get_mut(col) {
810                            render_cell(value, render_cell_area, &mut row_buf, &ctx);
811                        }
812                    }
813
814                    col += 1;
815                }
816
817                // render shifted and clipped row.
818                // this moves the cells from row_buf to the screen buf.
819                transfer_buffer(
820                    &mut row_buf,
821                    state.hscroll.offset() as u16,
822                    visible_row_area,
823                    buf,
824                );
825            }
826
827            value = if let Some(value) = data.nth(0) {
828                value
829            } else {
830                break 'll;
831            };
832
833            row = Some(row.expect("row").saturating_add(1));
834            row_y += render_row_area.height;
835
836            if visible_row_area.bottom() >= state.table_area.bottom() {
837                break;
838            }
839        }
840
841        if !widget.varying_heights {
842            // skip to a guess for the last page.
843            // the guess uses row-height is 1, which may read a few more lines than
844            // absolutely necessary.
845            let skip_rows = state
846                .rows
847                .saturating_sub(row.map_or(0, |v| v + 1))
848                .saturating_sub(state.table_area.height as usize);
849            // if we can still skip some rows, then the data so far is useless.
850            if skip_rows > 0 {
851                row_heights.clear();
852            }
853
854            value = if let Some(value) = data.nth(skip_rows) {
855                row = Some(row.map_or(skip_rows, |row| row + skip_rows + 1));
856                value
857            } else {
858                break 'll;
859            }
860        }
861
862        let mut sum_height = row_heights.iter().sum::<u16>();
863        loop {
864            let row_height = widget
865                .row_height_fn
866                .as_mut()
867                .map_or(1, |f| (*f)(value, &ctx)) as u16;
868            state
869                .row_heights
870                .push(row_height as u8, widget.varying_heights);
871            row_heights.push(row_height);
872
873            // Keep a rolling sum of the heights and drop unnecessary info.
874            // We don't need more info, and there will be a lot more otherwise.
875            sum_height += row_height;
876            if sum_height.saturating_sub(row_heights.first().copied().unwrap_or_default())
877                > state.table_area.height
878            {
879                let lost_height = row_heights.remove(0);
880                sum_height -= lost_height;
881            }
882
883            value = if let Some(value) = data.nth(0) {
884                value
885            } else {
886                break;
887            };
888
889            row = Some(row.expect("row") + 1);
890        }
891
892        state.rows = row.map_or(0, |v| v + 1);
893        // have we got a page worth of data?
894        if let Some(last_page) = calc_last_page(state.table_area.height, row_heights) {
895            state.vscroll.set_max_offset(state.rows - last_page);
896        } else {
897            // we don't have enough data to establish the last page.
898            // either there are not enough rows or the given row-count
899            // was off. make a guess.
900            state
901                .vscroll
902                .set_max_offset(state.rows.saturating_sub(state.table_area.height as usize));
903        }
904        state
905            .hscroll
906            .set_max_offset(width.saturating_sub(state.table_area.width) as usize);
907        state.selection.validate_rows(state.rows);
908        state.selection.validate_cols(state.columns);
909    }
910
911    // render only the scrollbars.
912    ScrollArea::new()
913        .style(widget.style)
914        .block(widget.block.as_ref())
915        .h_scroll(widget.hscroll.as_ref())
916        .v_scroll(widget.vscroll.as_ref())
917        .render_scrollbars(
918            area,
919            buf,
920            &mut ScrollAreaState::new()
921                .h_scroll(&mut state.hscroll)
922                .v_scroll(&mut state.vscroll),
923        );
924}
925
926#[allow(clippy::too_many_arguments)]
927fn render_footer<T: Copy, Selection: TableSelection>(
928    widget: &Table<'_, T, Selection>,
929    columns: usize,
930    width: u16,
931    l_columns: &[Rect],
932    l_spacers: &[Rect],
933    area: Rect,
934    buf: &mut Buffer,
935    state: &mut TableState<Selection>,
936) {
937    let height = widget
938        .footer
939        .iter()
940        .map(|v| v.height() as u16)
941        .max()
942        .unwrap_or(0);
943
944    if height > 0 {
945        let render_row_area = Rect::new(0, 0, width, height);
946        let mut row_buf = Buffer::empty(render_row_area);
947
948        row_buf.set_style(render_row_area, widget.style);
949        if let Some(footer_style) = widget.footer_style {
950            row_buf.set_style(render_row_area, footer_style);
951        }
952
953        for (col, text) in widget.footer.iter().enumerate() {
954            if col >= columns {
955                break;
956            }
957
958            let render_cell_area =
959                Rect::new(l_columns[col].x, 0, l_columns[col].width, area.height);
960            let render_space_area = Rect::new(
961                l_spacers[col + 1].x,
962                0,
963                l_spacers[col + 1].width,
964                area.height,
965            );
966
967            if state.selection.is_selected_column(col) {
968                if let Some(selected_style) = patch_select(
969                    widget.select_footer_style,
970                    widget.focus_style,
971                    state.focus.get(),
972                    widget.show_footer_focus,
973                ) {
974                    row_buf.set_style(render_cell_area, selected_style);
975                    row_buf.set_style(render_space_area, selected_style);
976                }
977            };
978
979            // partially visible?
980            if render_cell_area.right() > state.hscroll.offset as u16
981                || render_cell_area.left() < state.hscroll.offset as u16 + area.width
982            {
983                text.render(render_cell_area, &mut row_buf);
984            }
985        }
986
987        // render shifted and clipped row.
988        transfer_buffer(&mut row_buf, state.hscroll.offset() as u16, area, buf);
989    }
990}
991
992#[allow(clippy::too_many_arguments)]
993fn render_header<T: Copy, Selection: TableSelection>(
994    widget: &Table<'_, T, Selection>,
995    columns: usize,
996    width: u16,
997    l_columns: &[Rect],
998    l_spacers: &[Rect],
999    area: Rect,
1000    buf: &mut Buffer,
1001    state: &mut TableState<Selection>,
1002) {
1003    let height = widget
1004        .header
1005        .iter()
1006        .map(|v| v.height() as u16)
1007        .max()
1008        .unwrap_or(0);
1009
1010    if height > 0 {
1011        let render_row_area = Rect::new(0, 0, width, height);
1012        let mut row_buf = Buffer::empty(render_row_area);
1013
1014        row_buf.set_style(render_row_area, widget.style);
1015        if let Some(header_style) = widget.header_style {
1016            row_buf.set_style(render_row_area, header_style);
1017        }
1018
1019        for (col, text) in widget.header.iter().enumerate() {
1020            if col >= columns {
1021                break;
1022            }
1023
1024            let render_cell_area =
1025                Rect::new(l_columns[col].x, 0, l_columns[col].width, area.height);
1026            let render_space_area = Rect::new(
1027                l_spacers[col + 1].x,
1028                0,
1029                l_spacers[col + 1].width,
1030                area.height,
1031            );
1032
1033            if state.selection.is_selected_column(col) {
1034                if let Some(selected_style) = patch_select(
1035                    widget.select_header_style,
1036                    widget.focus_style,
1037                    state.focus.get(),
1038                    widget.show_header_focus,
1039                ) {
1040                    row_buf.set_style(render_cell_area, selected_style);
1041                    row_buf.set_style(render_space_area, selected_style);
1042                }
1043            };
1044
1045            // partially visible?
1046            if render_cell_area.right() > state.hscroll.offset as u16
1047                || render_cell_area.left() < state.hscroll.offset as u16 + area.width
1048            {
1049                text.render(render_cell_area, &mut row_buf);
1050            }
1051        }
1052
1053        // render shifted and clipped row.
1054        transfer_buffer(&mut row_buf, state.hscroll.offset() as u16, area, buf);
1055    }
1056}
1057
1058fn calculate_column_areas<Selection>(
1059    columns: usize,
1060    l_columns: &[Rect],
1061    l_spacers: &[Rect],
1062    state: &mut TableState<Selection>,
1063) {
1064    state.column_areas.clear();
1065    state.column_layout.clear();
1066
1067    let mut col = 0;
1068    let shift = state.hscroll.offset() as isize;
1069    loop {
1070        if col >= columns {
1071            break;
1072        }
1073
1074        state.column_layout.push(Rect::new(
1075            l_columns[col].x,
1076            0,
1077            l_columns[col].width + l_spacers[col + 1].width,
1078            0,
1079        ));
1080
1081        let cell_x1 = l_columns[col].x as isize;
1082        let cell_x2 = (l_columns[col].x + l_columns[col].width + l_spacers[col + 1].width) as isize;
1083
1084        let squish_x1 = cell_x1.saturating_sub(shift);
1085        let squish_x2 = cell_x2.saturating_sub(shift);
1086
1087        let abs_x1 = max(0, squish_x1) as u16;
1088        let abs_x2 = max(0, squish_x2) as u16;
1089
1090        let v_area = Rect::new(
1091            state.table_area.x + abs_x1,
1092            state.table_area.y,
1093            abs_x2 - abs_x1,
1094            state.table_area.height,
1095        );
1096        state
1097            .column_areas
1098            .push(v_area.intersection(state.table_area));
1099
1100        col += 1;
1101    }
1102}
1103
1104fn patch_select(
1105    style: Option<Style>,
1106    focus_style: Option<Style>,
1107    focus: bool,
1108    show_focus: bool,
1109) -> Option<Style> {
1110    if let Some(style) = style {
1111        if let Some(focus_style) = focus_style {
1112            if focus && show_focus {
1113                Some(style.patch(focus_style))
1114            } else {
1115                Some(fallback_select_style(style))
1116            }
1117        } else {
1118            if focus && show_focus {
1119                Some(revert_style(style))
1120            } else {
1121                Some(fallback_select_style(style))
1122            }
1123        }
1124    } else {
1125        None
1126    }
1127}
1128
1129fn calc_last_page(height: u16, mut row_heights: Vec<u16>) -> Option<usize> {
1130    let mut sum_heights = 0;
1131    let mut n_rows = 0;
1132    while let Some(h) = row_heights.pop() {
1133        sum_heights += h;
1134        n_rows += 1;
1135        if sum_heights >= height {
1136            break;
1137        }
1138    }
1139
1140    if sum_heights < height {
1141        None
1142    } else {
1143        Some(n_rows)
1144    }
1145}