Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Data Table

Group: Data

Code

pub struct DataTableDemo {
    columns: Vec<Column>,
    selected: Option<usize>,
    sort_col: Option<usize>,
    sort_asc: bool,
    show_footer: bool,
    hovered_idx: Option<usize>,
}

impl DataTableDemo {
    pub fn new() -> Self {
        Self {
            columns: vec![
                Column {
                    title: "Name".into(),
                    width: ColumnWidth::Flex(3.0),
                    align: ColumnAlign::Left,
                },
                Column {
                    title: "Status".into(),
                    width: ColumnWidth::Flex(1.5),
                    align: ColumnAlign::Left,
                },
                Column {
                    title: "Age".into(),
                    width: ColumnWidth::Flex(0.5),
                    align: ColumnAlign::Right,
                },
                Column {
                    title: "Restarts".into(),
                    width: ColumnWidth::Fixed(10.0),
                    align: ColumnAlign::Right,
                },
            ],
            selected: Some(0),
            sort_col: Some(0),
            sort_asc: true,
            show_footer: true,
            hovered_idx: None,
        }
    }

    fn pod_rows() -> Vec<DataRow> {
        let pods = [
            ("nginx-7d9b8c66b-x2j4k", "Running", "3d", "0"),
            ("redis-master-0", "Running", "5d", "1"),
            ("api-gateway-5f6c8d-9mn2q", "Running", "1d", "0"),
            ("worker-batch-7b9c4-kl3m8", "Pending", "2m", "0"),
            ("grafana-5f4c8d-mn2q7", "CrashLoopBackOff", "1h", "14"),
            ("prometheus-server-0", "Running", "3d", "0"),
        ];
        pods.iter()
            .map(|(name, status, age, restarts)| DataRow {
                cells: vec![
                    StyledText::plain(*name),
                    if status.starts_with("Running") {
                        StyledText::colored(*status, Color::rgb(80, 200, 80))
                    } else if status.starts_with("CrashLoopBackOff") {
                        StyledText::colored(*status, Color::rgb(220, 60, 60))
                    } else {
                        StyledText::colored(*status, Color::rgb(220, 180, 50))
                    },
                    StyledText::plain(*age),
                    StyledText::plain(*restarts),
                ],
                decoration: Default::default(),
            })
            .collect()
    }

    fn keybinding_rows() -> Vec<DataRow> {
        [
            ("Ctrl+Enter", "Stage hunk"),
            ("Ctrl+Shift+Enter", "Stage file"),
            ("Ctrl+Z", "Revert hunk"),
            ("[", "Previous change"),
            ("]", "Next change"),
            ("Esc", "Close"),
        ]
        .iter()
        .map(|(key, action)| DataRow {
            cells: vec![StyledText::plain(*key), StyledText::plain(*action)],
            decoration: Default::default(),
        })
        .collect()
    }

    fn footer_row(&self) -> DataRow {
        let rows = Self::pod_rows();
        let total_restarts: u32 = rows
            .iter()
            .filter_map(|r| r.cells.get(3))
            .filter_map(|c| {
                let text: String = c.spans.iter().map(|s| s.text.as_str()).collect();
                text.parse::<u32>().ok()
            })
            .sum();
        DataRow {
            cells: vec![
                StyledText::plain(format!("{} pods", rows.len())),
                StyledText::plain(""),
                StyledText::plain(""),
                StyledText::colored(total_restarts.to_string(), Color::rgb(220, 180, 50)),
            ],
            decoration: Default::default(),
        }
    }

    fn table(&self, variant: usize) -> DataTable {
        if variant == 0 {
            let mut rows = Self::pod_rows();
            if let Some(col) = self.sort_col {
                rows.sort_by(|a, b| {
                    let a_text: String = a
                        .cells
                        .get(col)
                        .map(|c| c.spans.iter().map(|s| s.text.as_str()).collect())
                        .unwrap_or_default();
                    let b_text: String = b
                        .cells
                        .get(col)
                        .map(|c| c.spans.iter().map(|s| s.text.as_str()).collect())
                        .unwrap_or_default();
                    let cmp = a_text.cmp(&b_text);
                    if self.sort_asc {
                        cmp
                    } else {
                        cmp.reverse()
                    }
                });
            }
            DataTable {
                id: WidgetId::new("gallery:data-table:pods"),
                columns: self.columns.clone(),
                rows,
                selected_idx: self.selected,
                scroll_offset: 0,
                sort: self.sort_col.map(|c| {
                    (
                        c,
                        if self.sort_asc {
                            SortDirection::Ascending
                        } else {
                            SortDirection::Descending
                        },
                    )
                }),
                has_focus: true,
                show_scrollbar: true,
                min_total_width: None,
                h_scroll: 0.0,
                column_overrides: Vec::new(),
                footer: if self.show_footer {
                    Some(self.footer_row())
                } else {
                    None
                },
            }
        } else {
            DataTable {
                id: WidgetId::new("gallery:data-table:keybindings"),
                columns: vec![
                    Column {
                        title: "Key".into(),
                        width: ColumnWidth::Flex(1.0),
                        align: ColumnAlign::Left,
                    },
                    Column {
                        title: "Action".into(),
                        width: ColumnWidth::Flex(2.0),
                        align: ColumnAlign::Left,
                    },
                ],
                rows: Self::keybinding_rows(),
                selected_idx: None,
                scroll_offset: 0,
                sort: None,
                has_focus: false,
                show_scrollbar: false,
                min_total_width: None,
                h_scroll: 0.0,
                column_overrides: Vec::new(),
                footer: None,
            }
        }
    }

    fn status(&self, variant: usize) -> StatusBar {
        let text = if variant == 0 {
            let sort_text = match self.sort_col {
                Some(c) => format!(
                    "sort: {} {}",
                    self.columns[c].title,
                    if self.sort_asc { "asc" } else { "desc" }
                ),
                None => "sort: none".into(),
            };
            format!(" {sort_text} — click header to sort, f=footer ")
        } else {
            " static reference table — no sort, no selection ".into()
        };
        StatusBar {
            id: WidgetId::new("gallery:data-table:status"),
            left_segments: vec![StatusBarSegment {
                text,
                fg: Color::rgb(220, 220, 220),
                bg: Color::rgb(40, 40, 60),
                bold: false,
                action_id: None,
            }],
            right_segments: vec![],
        }
    }

    fn table_rect(area: Rect, backend: &dyn Backend) -> Rect {
        let lh = backend.line_height();
        Rect::new(area.x, area.y, area.width, (area.height - lh).max(0.0))
    }

    fn status_rect(area: Rect, backend: &dyn Backend) -> Rect {
        let lh = backend.line_height();
        Rect::new(area.x, area.y + area.height - lh, area.width, lh)
    }
}

impl Default for DataTableDemo {
    fn default() -> Self {
        Self::new()
    }
}

impl Demo for DataTableDemo {
    fn name(&self) -> &'static str {
        "Data Table"
    }

    fn group(&self) -> &'static str {
        "Data"
    }

    fn variants(&self) -> &'static [&'static str] {
        &["Pods (sortable)", "Keybindings (static)"]
    }

    fn render(&self, variant: usize, backend: &mut dyn Backend, area: Rect) {
        let table_rect = Self::table_rect(area, backend);
        let hovered = if variant == 0 { self.hovered_idx } else { None };
        let _ = backend.draw_data_table(table_rect, &self.table(variant), hovered);

        let status_rect = Self::status_rect(area, backend);
        let _ = backend.draw_status_bar_interactive(
            status_rect,
            &self.status(variant),
            &InteractionState::new(),
        );
    }

    fn handle(
        &mut self,
        variant: usize,
        event: &UiEvent,
        backend: &mut dyn Backend,
        area: Rect,
    ) -> Reaction {
        if variant != 0 {
            return Reaction::Continue;
        }
        let table_rect = Self::table_rect(area, backend);
        let total = Self::pod_rows().len();
        match event {
            UiEvent::KeyPressed { key, .. } => match key {
                Key::Char('j') => {
                    let cur = self.selected.unwrap_or(0);
                    if cur + 1 < total {
                        self.selected = Some(cur + 1);
                    }
                    Reaction::Redraw
                }
                Key::Char('k') => {
                    let cur = self.selected.unwrap_or(0);
                    self.selected = Some(cur.saturating_sub(1));
                    Reaction::Redraw
                }
                Key::Char('s') => {
                    self.sort_col = match self.sort_col {
                        None => Some(0),
                        Some(c) if c + 1 < self.columns.len() => Some(c + 1),
                        Some(_) => None,
                    };
                    Reaction::Redraw
                }
                Key::Char('d') => {
                    self.sort_asc = !self.sort_asc;
                    Reaction::Redraw
                }
                Key::Char('f') => {
                    self.show_footer = !self.show_footer;
                    Reaction::Redraw
                }
                _ => Reaction::Continue,
            },
            UiEvent::MouseDown { position, .. } => {
                let table = self.table(variant);
                let layout = backend.data_table_layout(table_rect, &table);
                // `DataTableLayout::hit_test` is viewport-relative (measured
                // from the table's own left/top edge), not absolute like
                // most other primitives' layouts — localize before testing.
                let (lx, ly) = (position.x - table_rect.x, position.y - table_rect.y);
                match layout.hit_test(lx, ly, 0, total) {
                    DataTableHit::Header { col } => {
                        if self.sort_col == Some(col) {
                            self.sort_asc = !self.sort_asc;
                        } else {
                            self.sort_col = Some(col);
                            self.sort_asc = true;
                        }
                        Reaction::Redraw
                    }
                    DataTableHit::Row { idx } => {
                        self.selected = Some(idx);
                        Reaction::Redraw
                    }
                    _ => Reaction::Continue,
                }
            }
            UiEvent::MouseMoved { position, .. } => {
                let table = self.table(variant);
                let layout = backend.data_table_layout(table_rect, &table);
                let (lx, ly) = (position.x - table_rect.x, position.y - table_rect.y);
                let old = self.hovered_idx;
                self.hovered_idx = match layout.hit_test(lx, ly, 0, total) {
                    DataTableHit::Row { idx } => Some(idx),
                    _ => None,
                };
                if self.hovered_idx != old {
                    Reaction::Redraw
                } else {
                    Reaction::Continue
                }
            }
            _ => Reaction::Continue,
        }
    }

    fn source(&self) -> &'static str {
        extract_region(SOURCE)
    }

    fn data(&self, variant: usize) -> serde_json::Value {
        serde_json::to_value(self.table(variant)).unwrap_or(serde_json::Value::Null)
    }

    fn caps_note(&self, _variant: usize, _caps: &BackendCaps) -> Option<String> {
        None
    }
}

Backends

VariantTUIGTKmacOSWindows
Pods (sortable)
Keybindings (static)