Slint Kit Documentation

Application avec contrôleurs typés

Une application Rust complète avec un champ natif validé et une sélection par clé stable. Tous les signaux de ces deux contrôles sont raccordés à leurs contrôleurs.

Créez une application avec les paquets publiés sur crates.io. Les quatre fichiers complets ci-dessous s’utilisent dans un projet neuf, sans copier le dépôt du kit :

cargo new slint-kit-typed-example
cd slint-kit-typed-example
Fenêtre native Project settings avec un nom de projet et une sélection Workspace
Capture native du même exemple, moteur Skia.

Cargo.toml

Remplacez le manifeste créé par Cargo. Télécharger 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

Ce script compile l’interface et enregistre les imports publics @kit. Télécharger build.rs.

// SPDX-License-Identifier: GPL-3.0-or-later
fn main() {
    slint_kit_build::compile("ui/app.slint").expect("compile typed documentation example");
}

Essayer le comportement

  1. Modifiez le nom et appuyez sur Entrée, cliquez sur « Accepter le nom » ou quittez le champ : le contrôleur accepte le brouillon valide.
  2. Entrez plus de 40 graphèmes : la validation affiche un message et conserve le dernier nom accepté.
  3. Ouvrez Espace de travail, utilisez les flèches puis Entrée, ou cliquez sur un choix. La ligne Archives désactivée est ignorée.
  4. Fermez la liste avec Échap ; Tab revient au champ. Le nom choisi provient de WorkspaceKey, jamais d’un index de ligne.

L’exemple choisit Ownership::Local. Pour une persistance contrôlée, passez à Ownership::Controlled et accusez réception après la décision métier avec acknowledge pour le champ et set_selected pour la sélection. Gardez les callbacks de publication faibles et libérez tout emprunt d’état avant une mise à jour de fenêtre.

Les surfaces modales demandent aussi une pile de superpositions et un registre de focus. L’exemple complet de navigation et sa composition Slint montrent le tiroir modal avec restauration de l’ouvreur exact, trap de focus et cibles réelles enregistrées.

Composition Slint

Télécharger ui/app.slint.

// 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; }
    }
}

Contrôleurs Rust

Le parseur construit ProjectName ; le validateur compte les graphèmes. Le signal d’édition alimente le brouillon sans remplacer l’éditeur natif. bind_select! projette les options avec leur génération et raccorde tous les signaux de sélection. Télécharger 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()
}

Exécuter l’application

Enregistrez les quatre fichiers aux emplacements indiqués dans le projet créé au début de ce guide. Cargo installe slint-kit et slint-kit-build depuis crates.io ; aucun chemin local vers le kit n’est nécessaire. Lancez la version française :

cargo run -- --fr

Cet exemple utilise une fenêtre native winit/Skia. Les captures présentent cette même interface ; elles ne qualifient pas IME ou lecteur d’écran.