Virtual text view for Slint
KitVirtualTextView · Slint Kit 0.3.0
Slint document view with selection and native editing of one line.

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
label— built-in property; custom composition exampleerror— built-in property; custom composition example
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

// 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; }
}
}
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
navigate(NavigationStep)select-line(int,int)commit-line(int,int,string)copy-requested(int,int)
Functions
take-focus()