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