Skip to main content

DataTable

Purpose​

A table that sorts, searches, pages and selects its rows for you.

Signature​

pub struct DataTable<Msg = crate::callback::Callback> { .. }

impl DataTable {
pub fn new(
headers: impl IntoIterator<Item = impl Into<String>>,
rows: impl IntoIterator<Item = Vec<String>>,
) -> Self;
pub fn lazy(
headers: impl IntoIterator<Item = impl Into<String>>,
count: usize,
row: impl Fn(usize) -> Vec<String> + 'static,
) -> Self;
}

Example​

Rendered preview of the DataTable demo

From frus-examples/widgets/src/features/display/widgets/datatable.rs:

//! `DataTable` — a table of text rows that sorts, filters, paginates and selects for
//! you, while the state it shows (sort, page, query, selection) stays in your app.

use frus::{
Align, Button, Column, DataTable, State, StateContext, StatefulWidget, Text, Variant, Widget,
};

pub const PEOPLE: [(&str, &str, u32); 12] = [
("Ada", "Research", 92),
("Alan", "Research", 88),
("Barbara", "Platform", 75),
("Dennis", "Platform", 81),
("Edsger", "Research", 67),
("Frances", "Compilers", 95),
("Grace", "Compilers", 99),
("Ken", "Platform", 72),
("Linus", "Kernel", 84),
("Margaret", "Flight", 97),
("Niklaus", "Compilers", 70),
("Radia", "Networks", 90),
];

pub const PER_PAGE: usize = 5;

pub fn example() -> Box<dyn Widget> {
Box::new(PeopleTable.into_widget())
}

/// The demo is a small `StatefulWidget`: the table's whole state lives in one struct, and
/// each handler changes it through a method — the same shape as a real page.
pub struct PeopleTable;

#[derive(Default)]
pub struct PeopleTableState {
/// The sorted column and its direction; `None` keeps the source order.
pub sort: Option<(usize, bool)>,
/// 1-based page.
page: usize,
query: String,
/// Checked rows, as indices into `PEOPLE` (the table translates through sorting).
pub checked: Vec<usize>,
}

impl PeopleTableState {
/// A click on a header: a new column sorts ascending, the same one flips.
pub fn sort_by(&mut self, column: usize) {
let ascending = match self.sort {
Some((current, ascending)) if current == column => !ascending,
_ => true,
};
self.sort = Some((column, ascending));
self.page = 1;
}

fn search(&mut self, query: String) {
self.query = query;
self.page = 1;
}

pub fn toggle(&mut self, row: usize) {
if let Some(at) = self.checked.iter().position(|&r| r == row) {
self.checked.remove(at);
} else {
self.checked.push(row);
}
}

/// The header box: everything, or nothing if everything was already checked.
pub fn toggle_all(&mut self) {
if self.checked.len() == PEOPLE.len() {
self.checked.clear();
} else {
self.checked = (0..PEOPLE.len()).collect();
}
}
}

impl StatefulWidget for PeopleTable {
type State = PeopleTableState;

fn create_state(&self) -> PeopleTableState {
PeopleTableState {
page: 1,
..PeopleTableState::default()
}
}
}

impl State for PeopleTableState {
type Widget = PeopleTable;

fn build(&self, cx: &StateContext<Self>) -> Box<dyn Widget> {
let rows = PEOPLE
.iter()
.map(|(name, team, score)| vec![name.to_string(), team.to_string(), score.to_string()]);

let mut table = DataTable::new(["Name", "Team", "Score"], rows)
.column_widths(&[110.0, 110.0, 70.0])
// Searching filters every column, before sorting and paging.
.searchable(
self.query.as_str(),
cx.handler(|s: &mut Self, q: String| s.search(q)),
)
.empty_text("Nobody matches that search")
// A header click reports the column; the app decides the direction.
.on_sort(cx.handler(|s: &mut Self, column: usize| s.sort_by(column)))
// Checkboxes for multi-selection, with a "check all" box in the header.
.checkboxes(
cx.handler(|s: &mut Self, row: usize| s.toggle(row)),
cx.callback(Self::toggle_all),
)
.selected(&self.checked)
// Shown above the table only while rows are checked.
.bulk_actions({
let clear = cx.callback(|s: &mut Self| s.checked.clear());
move || {
vec![Box::new(
Button::new("Clear")
.variant(Variant::Text)
.on_press(clear.clone()),
) as Box<dyn Widget>]
}
})
.paginated(
self.page,
PER_PAGE,
cx.handler(|s: &mut Self, page: usize| s.page = page),
);
if let Some((column, ascending)) = self.sort {
table = table.sorted(column, ascending);
}

Box::new(
Column::new()
.spacing(8.0)
.cross_axis_alignment(Align::Start)
.child(table)
.child(
Text::new(format!("{} checked", self.checked.len())).color(cx.theme().muted),
),
)
}
}

Properties​

MethodArgumentEffect
.column_widths()widths: &[f32]The fixed width of each column, in pixels (0 or less = a flexible column).
.sorted()column: usize, ascending: boolThe sorted column and its direction (true = ascending): it sorts the rows for display and shows the direction indicator.
.on_sort()on_sort: impl Fn(usize) -> R + 'staticMakes the headers clickable: on_sort(column) on click (the application then flips the direction and passes sorted(...) back).
.paginated()current: usize, per_page: usize, on_page: impl Fn(usize) -> R1 + 'staticPaginates the table: it shows only the slice of page current (1-indexed) with a size of per_page, and places a Pagination selector beneath it. on_page(page) when a page is clicked (the application updates current). The slicing follows the sort.
.page_sizes()sizes: &[usize], on_page_size: impl Fn(usize) -> R1 + 'staticAdds a page size selector (a SegmentedButton of the sizes offered) to the footer. on_page_size(size) on change (the app updates the size and, usually, returns to page 1). It has no effect if the table is not paginated.
.on_select_row()on_select: impl Fn(usize) -> R + 'staticMakes the rows clickable: on_select_row(source_row) when a row is clicked. The index passed is that of the source row (before sorting and pagination) — the DataTable translates the displayed position into the original index.
.selected()rows: &[usize]The selected rows (highlighted, or checked), designated by their source row index. The DataTable only highlights (and checks) those visible in the current slice.
.checkboxes()on_check: impl Fn(usize) -> R1 + 'static, on_check_all: MsgEnables multiple selection: a column of checkboxes capped by a "check all". on_check(source_row) toggles one row (the source row index, translated from the displayed position), and on_check_all toggles the header box. The checked state reflects selected. It combines with on_select_row: the box handles group selection, and a click on the row's body stays a row click.
.searchable()query: impl Into<String>, on_query: impl Fn(String) -> R1 + 'staticMakes the table searchable: a search field (with value query) caps the table, and the source rows are filtered (row_matches, a case-insensitive substring across every column) before sorting and pagination. on_query(text) on each keystroke (the application updates query and usually returns to page 1). The filter acts upstream of the sort, the page and the selection: boxes and highlighting stay in source indices, over the visible subset.
.bulk_actions()make: impl Fn() -> Vec<Box<dyn Widget<Msg>>> + 'staticAdds a bulk actions bar above the table, visible only when rows are selected. The make factory produces the action widgets (typically Buttons — the app chooses variants and messages); the bar precedes them with an "N selected" label. Called again on every rebuild (fresh widgets). The number shown is that of the selected rows, across all pages.
.empty_text()text: impl Into<String>Overrides the empty state's text (default "No results") — "No people match your search", say. Shown centred under the header when no row is visible (milestone 244).
.sort_with()col: usize, cmp: impl Fn(&str, &str) -> Ordering + 'staticGives column col a custom comparator: cmp(a, b) orders two of its (text) cells. It replaces the default sort (compare_cells) for that column — useful when the values sort badly as they stand: formatted dates ("Mar 2024"), amounts ("$1.2M"), priorities ("High"/"Medium"/"Low"). The direction (sorted(_, ascending)) applies on top (the comparator defines the ascending order).

Behaviour​

A text data table that sorts its own rows according to the sort state supplied, then delegates the rendering to a Table.

use frus_widgets::DataTable;
let rows = vec![vec!["Bob".to_string(), "9".to_string()], vec!["Ada".to_string(), "10".to_string()]];
let table: DataTable = DataTable::new(["Name", "Score"], rows).sorted(1, true);

See also​

Source: crates/frus-widgets/src/datatable.rs @ e6fba33f5b42