Slint Kit Documentation

Virtualized data

Data table for Slint

KitDataTable · Slint Kit 0.3.0

Slint data table with virtualized rows, sortable headers and row selection.

Slint data table with virtualized rows, sortable headers and row selection.
Native app built from the example below.

Try the component

Download the complete project, extract it and run cargo run --locked in its folder. Rust 1.92+; Slint Kit 0.3.0 from crates.io. You can also copy the four files below.

Click Name to sort, then select Ada. Sort again: Rust keeps Ada selected by key, even when the row moves.

ui/app.slint

export { KitFormValidation, HtmlInputKind, NumberInputFormat, KitInputConstraints, KitValidationResult, KitValidityState } from "@kit/kit.slint";
// SPDX-License-Identifier: GPL-3.0-or-later
import { KitDataTable, KitThemes, TableRow, TableColumn, DataSortDirection, DataAlignment, NavigationStep } from "@kit/kit.slint";
export { TableRow, TableColumn, DataSortDirection, DataAlignment, NavigationStep }
export component App inherits Window {
    title: "Slint data table";
    preferred-width: 520px; preferred-height: 320px;
    max-width: 100000px; max-height: 100000px;
    background: KitThemes.light.canvas;
    in property <[TableRow]> rows;
    in property <[TableColumn]> columns;
    in property <int> generation;
    in property <int> active-index: -1;
    in property <string> selected-label: "No selection";
    callback sort(int);
    callback choose(int, int);
    callback move(NavigationStep);
    VerticalLayout {
        padding: 24px; spacing: 16px;
        table := KitDataTable {
            height: 220px; rows: root.rows; columns: root.columns;
            generation: root.generation; active-index: root.active-index;
            sort-requested(token) => { root.sort(token); }
            activate(token, generation) => { root.choose(token, generation); }
            navigate(step) => { root.move(step); }
        }
        Text { text: root.selected-label; color: KitThemes.light.text-primary; }
    }
}

src/main.rs

// SPDX-License-Identifier: GPL-3.0-or-later
use slint::{ComponentHandle, ModelRc, VecModel};
use std::{cell::RefCell, rc::Rc};
mod ui {
    slint::include_modules!();
}

fn next_index(current: Option<usize>, length: usize, step: ui::NavigationStep) -> usize {
    use ui::NavigationStep::*;
    let current = current.unwrap_or(0);
    match step {
        First => 0,
        Last => length.saturating_sub(1),
        Previous | PagePrevious => current.saturating_sub(1),
        Next | PageNext => (current + 1).min(length.saturating_sub(1)),
    }
}

#[derive(Clone, Copy, PartialEq, Eq)]
enum Person {
    Zoe,
    Ada,
    Lina,
}
impl Person {
    fn token(self) -> i32 {
        match self {
            Self::Zoe => 11,
            Self::Ada => 23,
            Self::Lina => 37,
        }
    }
    fn label(self) -> &'static str {
        match self {
            Self::Zoe => "Zoe",
            Self::Ada => "Ada",
            Self::Lina => "Lina",
        }
    }
}
#[derive(Clone, Copy, PartialEq, Eq)]
struct Generation(i32);
struct State {
    order: Vec<Person>,
    selected: Option<Person>,
    active: Option<Person>,
    direction: ui::DataSortDirection,
    generation: Generation,
}
fn publish(state: &RefCell<State>, app: &ui::App) {
    let state = state.borrow();
    let rows = state
        .order
        .iter()
        .map(|person| ui::TableRow {
            token: person.token(),
            cells: ModelRc::new(VecModel::from(vec![person.label().into()])),
            selected: state.selected == Some(*person),
            disabled: false,
        })
        .collect::<Vec<_>>();
    let columns = vec![ui::TableColumn {
        token: 1,
        label: "Name".into(),
        sortable: true,
        sort: state.direction,
        alignment: ui::DataAlignment::Leading,
    }];
    let active = state
        .order
        .iter()
        .position(|p| Some(*p) == state.active)
        .map_or(-1, |i| i as i32);
    let label = state.selected.map_or("No selection".to_owned(), |p| {
        format!("Selected: {}", p.label())
    });
    let generation = state.generation.0;
    drop(state);
    app.set_generation(generation);
    app.set_active_index(active);
    app.set_rows(ModelRc::new(VecModel::from(rows)));
    app.set_columns(ModelRc::new(VecModel::from(columns)));
    app.set_selected_label(label.into());
}
fn main() -> Result<(), slint::PlatformError> {
    let app = ui::App::new()?;
    slint_kit::init_form_validation!(ui, app);
    let state = Rc::new(RefCell::new(State {
        order: vec![Person::Zoe, Person::Ada, Person::Lina],
        selected: None,
        active: None,
        direction: ui::DataSortDirection::Unsorted,
        generation: Generation(1),
    }));
    publish(&state, &app);
    let (model, weak) = (state.clone(), app.as_weak());
    app.on_sort(move |column| {
        if column != 1 {
            return;
        }
        {
            let mut state = model.borrow_mut();
            let Some(next) = state.generation.0.checked_add(1) else {
                return;
            };
            state.generation = Generation(next);
            state.direction = if state.direction == ui::DataSortDirection::Ascending {
                ui::DataSortDirection::Descending
            } else {
                ui::DataSortDirection::Ascending
            };
            state.order.sort_by_key(|p| p.label());
            if state.direction == ui::DataSortDirection::Descending {
                state.order.reverse();
            }
        }
        if let Some(app) = weak.upgrade() {
            publish(&model, &app);
        }
    });
    let (model, weak) = (state.clone(), app.as_weak());
    app.on_choose(move |token, generation| {
        {
            let mut state = model.borrow_mut();
            if generation != state.generation.0 {
                return;
            }
            let Some(person) = state.order.iter().copied().find(|p| p.token() == token) else {
                return;
            };
            state.selected = Some(person);
            state.active = Some(person);
        }
        if let Some(app) = weak.upgrade() {
            publish(&model, &app);
        }
    });
    let weak = app.as_weak();
    app.on_move(move |step| {
        {
            let mut state = state.borrow_mut();
            let current = state.order.iter().position(|p| Some(*p) == state.active);
            let index = next_index(current, state.order.len(), step);
            state.active = Some(state.order[index]);
        }
        if let Some(app) = weak.upgrade() {
            publish(&state, &app);
        }
    });
    app.run()
}

Cargo.toml and build.rs

Cargo.toml

[package]
name = "slint-kit-data-table-example"
version = "0.1.0"
edition = "2024"
rust-version = "1.92"
license = "GPL-3.0-or-later"
publish = false

[dependencies]
slint-kit = "=0.3.0"
slint = { version = "=1.18.1", default-features = false, features = ["std", "compat-1-18", "backend-winit", "renderer-skia", "accessibility"] }

[build-dependencies]
slint-kit-build = "=0.3.0"

[workspace]

build.rs

fn main() {
    slint_kit_build::compile("ui/app.slint").expect("compile component example");
}

Files: Cargo.toml · build.rs · ui/app.slint · src/main.rs

Give the table a bounded height, then supply columns, rows, generation and active-index. Handle sort-requested(column-token) in Rust by sorting your model and updating the column direction. Handle navigate to update the active index and activate(row-token, generation) to act on a row.

The table renders the supplied row order; clicking a header does not sort it automatically. Changing active-index reveals that row. Set empty-label for an empty model and interactive: false while a modal blocks the table.

To connect sorting, keyboard input and selection, use the complete sortable table application.

Choose between built-in options and custom composition

Shared options

Visual composition

The file below produces the screenshot. For controller-driven components, it shows the displayed state without supplying the Rust wiring. Create a project.

Show the screenshot source
Slint data table with virtualized rows, sortable headers and row selection.
Native screenshot of the same example, rendered with Skia.
// SPDX-License-Identifier: GPL-3.0-or-later
import { DataAlignment, DataSortDirection, KitDataTable, KitTheme, KitThemes } from "@kit/kit.slint";

export component Example inherits Window {
    title: "KitDataTable example";
    preferred-width: 620px;
    preferred-height: 400px;
    max-width: 100000px;
    max-height: 100000px;
    background: root.theme.canvas;
    in-out property <KitTheme> theme: KitThemes.light;
    in-out property <string> status;
    VerticalLayout {
        padding: 24px; spacing: 16px; alignment: start;
        Text { text: "Data Table"; color: root.theme.text-primary; font-size: 20px * root.theme.text-scale; font-weight: 600; }
        KitDataTable { theme: root.theme; height: 250px; generation: 1; columns: [{ token: 11, label: "Project", sortable: true, sort: DataSortDirection.ascending, alignment: DataAlignment.leading }, { token: 23, label: "Status", sortable: true, sort: DataSortDirection.unsorted, alignment: DataAlignment.leading }, { token: 38, label: "Owner", sortable: false, sort: DataSortDirection.unsorted, alignment: DataAlignment.leading }]; rows: [{ token: 101, cells: ["Field notes", "In progress", "Alex"], selected: true, disabled: false }, { token: 205, cells: ["Design system", "Review", "Jamie"], selected: false, disabled: false }, { token: 309, cells: ["Launch plan", "Draft", "Taylor"], selected: false, disabled: false }]; activate(token, generation) => { root.status = "Project token " + token; } }
        if root.status != "": Text { text: root.status; color: root.theme.text-secondary; font-size: 12px * root.theme.text-scale; wrap: word-wrap; }
    }
}

Download this example

Public API

Inherits Rectangle.

Properties

Name Type Direction
interactive bool in
theme KitTheme in
control-size ControlSize in
columns [TableColumn] in
rows [TableRow] in
generation int in
active-index int in
label string in
empty-label string in
minimum-column-width length in
scroll-y length out

Types and variants

Type Declaration
enum ControlSize compact, medium, comfortable
enum DataAlignment leading, center, trailing
enum DataSortDirection unsorted, ascending, descending
enum NavigationStep next, previous, first, last, page-next, page-previous
struct TableColumn token: int, label: string, sortable: bool, sort: DataSortDirection, alignment: DataAlignment
struct TableRow token: int, cells: [string], selected: bool, disabled: bool

Callbacks

Functions

See also