egui-table-kit 0.1.1

An extension for `egui` that brings batteries-included, filtering, highlighting, tree structures, and an action dispatch to your tables.
Documentation

egui-table-kit

egui-table-kit is an extension library for egui and egui_extras designed to simplify implementing table state management, high-performance interactions, and custom action workflows. It extends egui_extras::TableBuilder by providing unified filtering, sorting, row color tagging, hierarchical tree structures, and multi-row selection patterns.


Features

  • Text Search & Regex Filtering: Advanced per-column text filters supporting raw literal substrings, case insensitivity, and regular expressions with integrated error feedback for invalid regex patterns.
  • Color Highlighting & Tagging: Categorize rows into up to 10 color-based highlight groups (backed by compressed bitmaps) and filter rows matching specific colors.
  • Hierarchical Tree Support: Native rendering of nested trees, custom guideline drawing with dashed line segments, parent-child expansion/collapse toggle buttons, and cached subtree flattening.
  • Optimized Selection Storage: Selection state tracking utilizing roaring bitmaps for efficient range queries and multi-gigabyte row scale memory footprint.
  • Convenience Extractors (RowSliceExt): Simple helper trait on raw row data to parse, extract, or fall back between primary cell text and optional alternate/hover text.
  • Action Dispatch Framework (TableOperations): Build toolbar button groups or context menus bound to row selection limits. Built-in support for long-running operations via poll loops, modal dialogues, progress spinners, and operation errors.
  • Default Actions: Shipped out-of-the-box with operations for select all, deselect all, filter-based selections, and clipboard-copy utilities (with and without header data).

Installation

Add the following to your Cargo.toml:

[dependencies]
egui = "0.34"
egui_extras = { version = "0.34", default-features = false }
egui-table-kit = "0.1.0"

Core Concepts

1. TableProvider

Implement the TableProvider trait to adapt your dataset for the table view. This trait handles headers, total row counts, streaming over selections, custom sorting fallback implementations, and hierarchical tree navigation.

pub trait TableProvider {
    fn headers(&self) -> &[&str];
    fn row_count(&self) -> usize;

    /// Stream selected rows sequentially to a captured closure.
    fn for_selected_rows(
        &self,
        state: &TableState,
        f: &mut RowCallback<'_>,
    ) -> Result<(), TableError>;

    /// Stream all rows sequentially to a captured closure.
    fn for_all_rows(&self, f: &mut RowCallback<'_>) -> Result<(), TableError>;

    /// Sort the active row indices by a column. Contains a default string-based fallback.
    fn sort_active_rows(
        &self,
        active_rows: &mut Vec<usize>,
        col_index: usize,
        ascending: bool,
    ) -> Result<(), TableError>;

    /// Sequential fallback filtering. Override for custom or parallel filtering (e.g., using Rayon).
    fn filter_rows(
        &self,
        state: &TableState,
        filters: &[(usize, Filter)],
    ) -> Result<Vec<usize>, TableError>;

    // --- Hierarchical Tree Table Methods ---

    /// Returns the tree structural context for a row. Defaults to None (flat table).
    fn row_hierarchy(&self, _state: &TableState, _row_index: usize) -> Option<RowHierarchy> { None }

    /// Returns whether this table displays nested tree structures.
    fn is_tree(&self) -> bool { false }

    /// Gets the immediate parent index of a row.
    fn row_parent(&self, _row_index: usize) -> Option<usize> { None }

    /// Gets the immediate child indices of a parent row.
    fn row_children(&self, _row_index: usize) -> Vec<usize> { Vec::new() }

    /// Checks whether an individual row matches active filtering requirements.
    fn row_matches(
        &self,
        _state: &TableState,
        _row_index: usize,
        _filters: &[(usize, Filter)],
        _highlight: Option<u8>,
    ) -> bool { true }
}

2. TableState

TableState stores active runtime view settings for your table. It maintains:

  • The persistent table ID.
  • Dynamic column states (hover highlights, active search criteria, and active sort orientations).
  • Row indices matching active filters (active_rows).
  • Selected and expanded nodes (powered by RoaringBitmap).
  • Tree guidelines and expansion toggle state indicators.
  • Caches for sibling sorting to minimize CPU overhead on frame redraws.

3. TableOperations

Compose context menus or top-aligned toolbar panels. By grouping actions together, TableOperations::gui handles enabling/disabling triggers, generating keyboard shortcuts, displaying state-dependent menu prompts, or spawning modal workflows.

use egui_table_kit::operations::{TableOperations, SelectAll, DeSelectAll, CopyRows};

let operations = TableOperations::new()
    .with_group(vec![
        Box::new(SelectAll::new()),
        Box::new(DeSelectAll::new()),
    ])
    .with_group(vec![
        Box::new(CopyRows::new()),
    ]);

You can define custom operations by implementing the TableOperation trait:

pub trait TableOperation: std::any::Any + std::fmt::Debug + Send + Sync {
    fn name(&self) -> Cow<'_, str>;
    fn icon(&self) -> &'static str { "X" }
    fn enabled(&self) -> TableOperationEnablement;
    fn exec(&mut self, ctx: &mut OperationContext<'_, '_>) -> Result<(), TableError>;

    // Optional modal UI and background polling hooks:
    fn pollable(&self) -> bool { false }
    fn poll(&mut self, _ui: &mut egui::Ui, _data: &mut TableState) -> Result<(), TableError> { Ok(()) }
    fn is_pending(&mut self) -> bool { false }
    fn just_completed(&mut self) -> bool { false }
    fn error(&self) -> Option<&str> { None }
    fn clear_error(&mut self) {}
}

Example Usage

The following example is a simplified version of examples/minimal.rs, illustrating how to define a data provider, construct your state, render headers using the HeaderTrait, and process filtering/sorting events.

use std::borrow::Cow;

use eframe::egui;
use egui_table_kit::{
    error::TableError,
    header::HeaderTrait,
    operations::{CopyRows, DeSelectAll, RowCallback, SelectAll, TableOperations, TableProvider},
    state::TableState,
};

struct SimpleDataset {
    headers: Vec<&'static str>,
    records: Vec<Vec<String>>,
}

impl TableProvider for SimpleDataset {
    fn headers(&self) -> &[&str] { &self.headers }
    fn row_count(&self) -> usize { self.records.len() }

    fn for_all_rows(&self, f: &mut RowCallback<'_>) -> Result<(), TableError> {
        for record in &self.records {
            let cells: Vec<(Cow<'_, str>, Option<Cow<'_, str>>)> = record
                .iter()
                .map(|val| (Cow::Borrowed(val.as_str()), None))
                .collect();
            f(&cells)?;
        }
        Ok(())
    }

    fn for_selected_rows(
        &self,
        state: &TableState,
        f: &mut RowCallback<'_>,
    ) -> Result<(), TableError> {
        for idx in &state.selected_rows {
            if let Some(record) = self.records.get(idx as usize) {
                let cells: Vec<(Cow<'_, str>, Option<Cow<'_, str>>)> = record
                    .iter()
                    .map(|val| (Cow::Borrowed(val.as_str()), None))
                    .collect();
                f(&cells)?;
            }
        }
        Ok(())
    }
}

struct DemoApp {
    provider: SimpleDataset,
    state: TableState,
    operations: TableOperations,
}

impl Default for DemoApp {
    fn default() -> Self {
        let provider = SimpleDataset {
            headers: vec!["Name", "Role", "Department"],
            records: vec![
                vec!["Alice".into(), "Engineer".into(), "Platform".into()],
                vec!["Bob".into(), "Designer".into(), "Product".into()],
            ],
        };
        let row_count = provider.row_count();
        let state = TableState::new("demo_table_unique_id", row_count);
        let operations = TableOperations::new()
            .with_group(vec![
                Box::new(SelectAll::new()),
                Box::new(DeSelectAll::new()),
                Box::new(CopyRows::new()),
            ]);

        Self { provider, state, operations }
    }
}

impl eframe::App for DemoApp {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        egui::CentralPanel::default().show(ctx, |ui| {
            // Render action toolbar
            ui.horizontal(|ui| {
                let _ = self.operations.gui(ui, &self.provider, &mut self.state, false);
            });

            ui.add_space(5.0);
            ui.label(self.state.counts_header(self.provider.row_count()));

            // Setup the egui_extras TableBuilder
            let builder = egui_extras::TableBuilder::new(ui)
                .resizable(true)
                .sense(egui::Sense::click())
                .column(egui_extras::Column::initial(150.0))
                .column(egui_extras::Column::initial(150.0))
                .column(egui_extras::Column::remainder());

            // Build table header with built-in filter and sort support
            if let Ok((responses, table)) = builder.archived_headers(
                &self.state,
                self.provider.headers().iter().copied(),
                22.0, // Header height
                &[],  // Highlight palette Row 1
                &[],  // Highlight palette Row 2
            ) {
                // Update table state filters/sorting with responses from user clicks
                let _ = self.state.process_responses(&self.provider, responses);

                // Run active column filters on flat dataset
                let filter_state = self.state.get_filter_state();
                let _ = self.state.apply_all_filters(&self.provider, &filter_state);

                // Apply active column sorting
                if let Some((sort_col, sort_up)) = self.state.get_sort_state() {
                    let _ = self.provider.sort_active_rows(
                        &mut self.state.active_rows,
                        sort_col,
                        sort_up,
                    );
                }

                let active_rows = self.state.active_rows.clone();

                table.body(|mut body| {
                    for &row_idx in &active_rows {
                        let is_selected = self.state.selected_rows.contains(row_idx as u32);
                        body.row(18.0, |mut row| {
                            for col_idx in 0..self.provider.headers().len() {
                                row.col(|ui| {
                                    let rect = ui.max_rect().expand2(0.5 * ui.spacing().item_spacing);
                                    let response = ui.interact(rect, ui.id().with((row_idx, col_idx)), egui::Sense::click());

                                    if response.clicked() {
                                        self.state.handle_row_selection(ui.input(|i| i.modifiers), row_idx);
                                    }

                                    if is_selected {
                                        ui.painter().rect_filled(rect, egui::CornerRadius::ZERO, ui.visuals().selection.bg_fill);
                                    }

                                    let cell_val = &self.provider.records[row_idx][col_idx];
                                    ui.label(cell_val);
                                });
                            }
                        });
                    }
                });
            }
        });
    }
}

Technical Details

1. Guideline dashed rendering in Tree views

When configuring hierarchical tree tables, TableState::show_tree_cell draws guidelines linking nested child entities to parents. Calculations avoid allocating extra buffers by evaluating coordinates on-the-fly and issuing direct draw calls to the active painter.

2. Double-text fallback with RowSliceExt

RowSlice<'a, 'b> holds references to TableCell<'a> containing (Cow<'a, str>, Option<Cow<'a, str>>). The first string represents the primary display value, while the second serves as an alternate or hover string. The RowSliceExt trait enables parsing either source value:

  • get_primary(col) / get_hover(col): Retrieve raw string values.
  • parse_primary::<T>(col) / parse_hover::<T>(col): Attempt to parse cellular strings into specific typed values (e.g. f32, DateTime, etc.) with generic error propagation.