Typed controller application
A complete Rust application with a validated native field and a selection identified by stable keys. Every signal on these two controls is connected to its controller.
Create an application using the packages published on crates.io. The four complete files below work in a fresh project without copying the kit repository:
cargo new slint-kit-typed-example
cd slint-kit-typed-example

Cargo.toml
Replace the manifest created by Cargo. Download Cargo.toml.
[package]
name = "slint-kit-typed-example"
version = "0.1.0"
edition = "2024"
rust-version = "1.92"
license = "GPL-3.0-or-later"
publish = false
[dependencies]
slint-kit = "0.1"
slint = { version = "=1.18.1", default-features = false, features = ["std", "compat-1-18", "backend-winit", "renderer-skia", "accessibility"] }
[build-dependencies]
slint-kit-build = "0.1"
[workspace]
build.rs
This script compiles the interface and registers the public @kit imports. Download build.rs.
// SPDX-License-Identifier: GPL-3.0-or-later
fn main() {
slint_kit_build::compile("ui/app.slint").expect("compile typed documentation example");
}
Try the behavior
- Edit the name and press Enter, click “Accept name” or leave the field: the controller accepts a valid draft.
- Enter more than 40 graphemes: validation displays an error and preserves the last accepted name.
- Open Workspace, use arrow keys then Enter, or click an option. The disabled Archive row is skipped.
- Close the list with Escape; Tab returns to the field. The accepted choice comes from
WorkspaceKey, never a row index.
The example chooses Ownership::Local. For controlled persistence, use Ownership::Controlled and acknowledge the business decision with acknowledge for the field and set_selected for the selection. Keep publisher callbacks weak and release any state borrow before updating a window.
Modal surfaces also need an overlay stack and a focus registry. The complete navigation example and its Slint composition demonstrate a modal drawer with exact opener restoration, focus trapping and registered real targets.
Slint composition
// SPDX-License-Identifier: GPL-3.0-or-later
import { KitButton, KitTextField, KitSelect, KitThemes, FieldAccess, ValidationPresentation, OptionRow, SelectState, NavigationStep, CloseReason, SelectionAvailability } from "@kit/kit.slint";
export enum ExampleLanguage { english, french }
export { OptionRow, SelectState, NavigationStep, CloseReason, SelectionAvailability, FieldAccess, ValidationPresentation }
export component App inherits Window {
in property <ExampleLanguage> language: ExampleLanguage.english;
private property <bool> french: root.language == ExampleLanguage.french;
title: root.french ? "Exemple Slint Kit typé" : "Typed Slint Kit example";
preferred-width: 560px;
preferred-height: 480px;
background: KitThemes.light.canvas;
in property <[OptionRow]> rows;
in property <SelectState> select-state;
in property <string> selected-label;
in property <string> draft;
in property <string> error;
in property <string> accepted-label;
in property <FieldAccess> field-access;
in property <ValidationPresentation> validation;
callback select-open();
callback select-navigate(NavigationStep);
callback select-activate(int, int);
callback select-dismiss(CloseReason);
callback select-query(string);
callback select-typeahead(string);
callback name-edited(string);
callback name-commit();
callback name-blur();
callback name-cancel();
public function focus-name() { name.take-focus(); }
VerticalLayout {
padding: 32px;
spacing: 20px;
alignment: start;
Text { text: root.french ? "Paramètres du projet" : "Project settings"; color: KitThemes.light.text-primary; font-size: 24px; font-weight: 600; }
name := KitTextField {
label: root.french ? "Nom du projet" : "Project name";
text: root.draft;
error: root.error;
validation: root.validation;
access: root.field-access;
edited(text) => { root.name-edited(text); }
commit-requested(text) => { root.name-commit(); }
cancelled => { root.name-cancel(); }
focus-observed(focused) => { if (!focused) { root.name-blur(); } }
}
Text { text: root.french ? "Espace de travail" : "Workspace"; color: KitThemes.light.text-primary; font-size: 14px; }
KitSelect {
rows: root.rows;
state: root.select-state;
placeholder: root.selected-label;
open-requested => { root.select-open(); }
navigate(step) => { root.select-navigate(step); }
activate(token, generation) => { root.select-activate(token, generation); }
dismiss(reason) => { root.select-dismiss(reason); }
query-edited(query) => { root.select-query(query); }
typeahead(text) => { root.select-typeahead(text); }
tab-navigation(reverse) => { root.focus-name(); return accept; }
}
KitButton { text: root.french ? "Accepter le nom" : "Accept name"; activated => { root.name-commit(); } }
Text { text: (root.french ? "Nom accepté : " : "Accepted: ") + root.accepted-label; color: KitThemes.light.text-secondary; wrap: word-wrap; }
}
}
Rust controllers
The parser constructs ProjectName; the validator counts graphemes. The edit signal updates the draft while preserving the native editor. bind_select! projects options with their generation and wires every selection signal. Download src/main.rs.
// SPDX-License-Identifier: GPL-3.0-or-later
use slint::ComponentHandle;
use slint_kit::{
AbsencePolicy, CommitPolicy, CommitTrigger, DraftValue, FieldAccess, FieldBinding, FieldIntent,
Ownership, ParseIssue, SelectOption, SelectionIntent, ValidationPresentation,
};
mod ui {
slint::include_modules!();
}
#[derive(Clone, PartialEq, Eq)]
struct ProjectName(String);
#[derive(Clone, Copy)]
enum NameIssue {
TooLong,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
enum WorkspaceKey {
Personal,
Shared,
Archive,
}
fn main() -> Result<(), slint::PlatformError> {
let french = std::env::args().any(|arg| arg == "--fr");
let app = ui::App::new()?;
app.set_language(if french {
ui::ExampleLanguage::French
} else {
ui::ExampleLanguage::English
});
app.set_accepted_label("Atlas".into());
let weak = app.as_weak();
let field = FieldBinding::<ProjectName, NameIssue>::new(
Some(ProjectName("Atlas".into())),
Ownership::Local,
CommitPolicy::OnBlur,
AbsencePolicy::Required,
|draft| {
if draft.is_empty() {
DraftValue::Empty
} else if draft.chars().any(char::is_control) {
DraftValue::Invalid(ParseIssue::InvalidSyntax)
} else {
DraftValue::Parsed(ProjectName(draft.into()))
}
},
|name| name.0.clone(),
move |issue| match issue {
NameIssue::TooLong => if french {
"Utilisez au plus 40 graphèmes"
} else {
"Use at most 40 graphemes"
}
.into(),
},
move |projection| {
if let Some(app) = weak.upgrade() {
app.set_draft(projection.draft.into());
app.set_error(projection.error.into());
app.set_field_access(match projection.access {
FieldAccess::Editable => ui::FieldAccess::Editable,
FieldAccess::ReadOnly => ui::FieldAccess::ReadOnly,
FieldAccess::Disabled => ui::FieldAccess::Disabled,
});
app.set_validation(match projection.validation {
ValidationPresentation::Pristine => ui::ValidationPresentation::Pristine,
ValidationPresentation::Checking => ui::ValidationPresentation::Checking,
ValidationPresentation::Valid => ui::ValidationPresentation::Valid,
ValidationPresentation::Invalid => ui::ValidationPresentation::Invalid,
});
}
},
);
field.set_validator(|name| {
if slint_kit::grapheme_count(&name.0) > 40 {
vec![NameIssue::TooLong]
} else {
Vec::new()
}
});
let weak = app.as_weak();
field.on_intent(move |intent| {
if let Some(app) = weak.upgrade() {
match intent {
FieldIntent::Commit(name) => app.set_accepted_label(name.0.into()),
FieldIntent::Clear => app.set_accepted_label("".into()),
FieldIntent::Cancel => {}
}
}
});
let target = field.clone();
app.on_name_edited(move |draft| {
let _ = target.edited(draft.as_str());
});
let target = field.clone();
app.on_name_commit(move || {
let _ = target.commit(CommitTrigger::Explicit);
});
let target = field.clone();
app.on_name_blur(move || {
let _ = target.commit(CommitTrigger::Blur);
});
let target = field.clone();
app.on_name_cancel(move || target.cancel());
let mut archived = SelectOption::new(
WorkspaceKey::Archive,
if french {
"Archives (lecture seule)"
} else {
"Archive (read only)"
},
);
archived.disabled = true;
let selection = slint_kit::bind_select!(
ui,
app,
vec![
SelectOption::new(
WorkspaceKey::Personal,
if french { "Personnel" } else { "Personal" }
),
SelectOption::new(
WorkspaceKey::Shared,
if french { "Partagé" } else { "Shared" }
),
archived,
],
Some(WorkspaceKey::Personal),
Ownership::Local
);
app.set_selected_label(if french { "Personnel" } else { "Personal" }.into());
let weak = app.as_weak();
selection.on_intent(move |intent| {
if let Some(app) = weak.upgrade() {
let label = match intent {
SelectionIntent::Commit(WorkspaceKey::Personal) => {
if french {
"Personnel"
} else {
"Personal"
}
}
SelectionIntent::Commit(WorkspaceKey::Shared) => {
if french {
"Partagé"
} else {
"Shared"
}
}
SelectionIntent::Commit(WorkspaceKey::Archive) => {
if french {
"Archives (lecture seule)"
} else {
"Archive (read only)"
}
}
SelectionIntent::Clear => {
if french {
"Choisir un espace"
} else {
"Choose a workspace"
}
}
};
app.set_selected_label(label.into());
}
});
// Keep both window-local bindings alive for the complete event loop.
app.run()
}
Run the application
Save the four files at the shown paths in the project created at the start of this guide. Cargo installs slint-kit and slint-kit-build from crates.io; no local path to the kit is needed. Run the English version:
cargo run
This example uses a native winit/Skia window. The screenshots show this same interface; they do not qualify IME or a screen reader.