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;
fn for_selected_rows(
&self,
state: &TableState,
f: &mut RowCallback<'_>,
) -> Result<(), TableError>;
fn for_all_rows(&self, f: &mut RowCallback<'_>) -> Result<(), TableError>;
fn sort_active_rows(
&self,
active_rows: &mut Vec<usize>,
col_index: usize,
ascending: bool,
) -> Result<(), TableError>;
fn filter_rows(
&self,
state: &TableState,
filters: &[(usize, Filter)],
) -> Result<Vec<usize>, TableError>;
fn row_hierarchy(&self, _state: &TableState, _row_index: usize) -> Option<RowHierarchy> { None }
fn is_tree(&self) -> bool { false }
fn row_parent(&self, _row_index: usize) -> Option<usize> { None }
fn row_children(&self, _row_index: usize) -> Vec<usize> { Vec::new() }
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>;
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| {
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()));
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());
if let Ok((responses, table)) = builder.archived_headers(
&self.state,
self.provider.headers().iter().copied(),
22.0, &[], &[], ) {
let _ = self.state.process_responses(&self.provider, responses);
let filter_state = self.state.get_filter_state();
let _ = self.state.apply_all_filters(&self.provider, &filter_state);
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.