Slint Kit Documentation

Virtualized data

Virtual text view for Slint

KitVirtualTextView · Slint Kit 0.3.0

Slint document view with selection and native editing of one line.

Slint document view with selection and native editing of one line.
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.

Select a line, edit it in the native field, then choose Save line. Rust updates that line while its key remains stable.

ui/app.slint

export { KitFormValidation, HtmlInputKind, NumberInputFormat, KitInputConstraints, KitValidationResult, KitValidityState } from "@kit/kit.slint";
// SPDX-License-Identifier: GPL-3.0-or-later
import { KitVirtualTextView, KitThemes, TextLine, TextViewAccess, NavigationStep } from "@kit/kit.slint";
export { TextLine, NavigationStep }
export component App inherits Window {
    title: "Slint virtual text view";
    preferred-width: 640px; preferred-height: 340px;
    max-width: 100000px; max-height: 100000px;
    background: KitThemes.light.canvas;
    in property <[TextLine]> lines;
    in property <int> active-index: -1;
    in-out property <string> draft;
    in property <string> status: "Select a line to edit";
    in property <string> error;
    callback choose(int, int);
    callback move(NavigationStep);
    callback save(int, int, string);
    callback copied(int, int);
    VerticalLayout {
        padding: 24px; spacing: 16px;
        document := KitVirtualTextView { height: 240px; lines: root.lines; generation: 1;
            active-index: root.active-index; access: TextViewAccess.edit-line;
            line-draft <=> root.draft; error: root.error;
            select-line(token, generation) => { root.choose(token, generation); }
            navigate(step) => { root.move(step); }
            commit-line(token, generation, text) => { root.save(token, generation, text); }
            copy-requested(token, generation) => { root.copied(token, generation); }
        }
        Text { text: root.status; 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 LineKey {
    Heading,
    Body,
    Closing,
}
impl LineKey {
    fn token(self) -> i32 {
        match self {
            Self::Heading => 11,
            Self::Body => 23,
            Self::Closing => 37,
        }
    }
}
struct Line {
    key: LineKey,
    text: String,
}
struct State {
    lines: Vec<Line>,
    selected: Option<LineKey>,
}
fn select(state: &State, app: &ui::App) {
    let index = state
        .lines
        .iter()
        .position(|line| Some(line.key) == state.selected);
    let draft = index.map_or("", |i| state.lines[i].text.as_str());
    app.set_active_index(index.map_or(-1, |i| i as i32));
    app.set_draft(draft.into());
    app.set_error("".into());
}
fn publish(state: &RefCell<State>, app: &ui::App) {
    let state = state.borrow();
    let lines = state
        .lines
        .iter()
        .enumerate()
        .map(|(index, line)| ui::TextLine {
            token: line.key.token(),
            number: index as i32 + 1,
            text: line.text.as_str().into(),
            selected: state.selected == Some(line.key),
        })
        .collect::<Vec<_>>();
    drop(state);
    app.set_lines(ModelRc::new(VecModel::from(lines)));
}
fn main() -> Result<(), slint::PlatformError> {
    let app = ui::App::new()?;
    slint_kit::init_form_validation!(ui, app);
    let state = Rc::new(RefCell::new(State {
        lines: vec![
            Line {
                key: LineKey::Heading,
                text: "# Project notes".into(),
            },
            Line {
                key: LineKey::Body,
                text: "Select and edit this line.".into(),
            },
            Line {
                key: LineKey::Closing,
                text: "Rust owns the saved text.".into(),
            },
        ],
        selected: None,
    }));
    publish(&state, &app);
    let (model, weak) = (state.clone(), app.as_weak());
    app.on_choose(move |token, generation| {
        if generation != 1 {
            return;
        }
        {
            let mut state = model.borrow_mut();
            let Some(key) = state
                .lines
                .iter()
                .find(|line| line.key.token() == token)
                .map(|line| line.key)
            else {
                return;
            };
            state.selected = Some(key);
        }
        if let Some(app) = weak.upgrade() {
            select(&model.borrow(), &app);
            publish(&model, &app);
        }
    });
    let (model, weak) = (state.clone(), app.as_weak());
    app.on_move(move |step| {
        {
            let mut state = model.borrow_mut();
            let current = state
                .lines
                .iter()
                .position(|line| Some(line.key) == state.selected);
            let index = next_index(current, state.lines.len(), step);
            state.selected = Some(state.lines[index].key);
        }
        if let Some(app) = weak.upgrade() {
            select(&model.borrow(), &app);
            publish(&model, &app);
        }
    });
    let (model, weak) = (state.clone(), app.as_weak());
    app.on_save(move |token, generation, text| {
        if generation != 1 {
            return;
        }
        let Some(app) = weak.upgrade() else {
            return;
        };
        if text.contains(['\n', '\r']) {
            app.set_error("Enter one line only".into());
            return;
        }
        {
            let mut state = model.borrow_mut();
            if state.selected.map(LineKey::token) != Some(token) {
                return;
            }
            let Some(line) = state
                .lines
                .iter_mut()
                .find(|line| line.key.token() == token)
            else {
                return;
            };
            line.text = text.to_string();
        }
        app.set_error("".into());
        app.set_status("Line saved in Rust".into());
        publish(&model, &app);
    });
    let weak = app.as_weak();
    app.on_copied(move |_token, generation| {
        if generation == 1 {
            if let Some(app) = weak.upgrade() {
                app.set_status("Selected line copied".into());
            }
        }
    });
    app.run()
}

Cargo.toml and build.rs

Cargo.toml

[package]
name = "slint-kit-virtual-text-view-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

Supply lines, generation and active-index. Set access: TextViewAccess.edit-line, bind line-draft and save the accepted text in Rust through commit-line(token, generation, text).

Handle select-line to load the selected draft and navigate to move between lines. copy-requested reports a copy action. Use KitTextArea for a complete editable multiline buffer.

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 document view with selection and native editing of one line.
Native screenshot of the same example, rendered with Skia.
// SPDX-License-Identifier: GPL-3.0-or-later
import { KitTheme, KitThemes, KitVirtualTextView } from "@kit/kit.slint";

export component Example inherits Window {
    title: "KitVirtualTextView example";
    preferred-width: 620px;
    preferred-height: 360px;
    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: "Virtual Text View"; color: root.theme.text-primary; font-size: 20px * root.theme.text-scale; font-weight: 600; }
        KitVirtualTextView { theme: root.theme; height: 230px; generation: 1; lines: [{ token: 101, number: 1, text: "# Field notes", selected: false }, { token: 205, number: 2, text: "A shared space for observations.", selected: true }, { token: 309, number: 3, text: "Keep identities stable as rows move.", selected: false }, { token: 413, number: 4, text: "Use native editing for selected lines.", selected: false }]; }
        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
theme KitTheme in
lines [TextLine] in
generation int in
active-index int in
access TextViewAccess in
line-draft string in-out
error string in
label string in
scroll-y length out

Types and variants

Type Declaration
enum NavigationStep next, previous, first, last, page-next, page-previous
struct TextLine token: int, number: int, text: string, selected: bool
enum TextViewAccess read-only, edit-line

Callbacks

Functions

See also