Sélection multiple pour Slint
KitMultiSelect · Slint Kit 0.3.1
Sélecteur multiple avec des étiquettes amovibles pour les valeurs retenues.

Essayer le composant
Télécharger le projet complet, le décompresser et lancer cargo run --locked dans son dossier. Rust 1.92+ ; Slint Kit 0.3.0 depuis crates.io. Vous pouvez aussi copier les quatre fichiers ci-dessous.
Choisissez Push pour l’ajouter sans perdre Email. Retirez une pastille ou désactivez un canal accepté.
ui/app.slint
export { KitFormValidation, HtmlInputKind, NumberInputFormat, KitInputConstraints, KitValidationResult, KitValidityState } from "@kit/kit.slint";
// SPDX-License-Identifier: GPL-3.0-or-later
import { KitMultiSelect, OptionRow, ChoiceRow, ChipRow, SelectState, NavigationStep, SelectionAvailability, KitTheme, KitThemes } from "@kit/kit.slint";
export component App inherits Window {
title: "KitMultiSelect example";
preferred-width: 520px;
preferred-height: 400px;
max-width: 100000px;
max-height: 100000px;
background: root.theme.canvas;
in-out property <KitTheme> theme: KitThemes.light;
in property <[OptionRow]> rows;
in property <[ChoiceRow]> choices;
in property <[ChipRow]> chips;
in property <SelectState> select-state;
in property <string> result;
callback toggle(int, int);
callback navigate(NavigationStep);
callback open();
callback close();
callback remove(int);
callback query(string);
callback typeahead(string);
VerticalLayout {
padding: 24px;
spacing: 16px;
alignment: start;
control := KitMultiSelect {
theme: root.theme; rows: root.rows; chips: root.chips; state: root.select-state; placeholder: "Add a channel";
open-requested => { root.open(); }
navigate(step) => { root.navigate(step); }
activate(token, generation) => { root.toggle(token, generation); }
dismiss(reason) => { root.close(); }
tab-navigation(reverse) => { return reject; }
query-edited(text) => { root.query(text); }
typeahead(text) => { root.typeahead(text); }
remove-requested(token) => { root.remove(token); }
}
result := Text { text: root.result; color: root.theme.text-primary; }
}
}
src/main.rs
// SPDX-License-Identifier: GPL-3.0-or-later
use slint::{ComponentHandle, ModelRc, VecModel};
use slint_kit::{MultiSelectController, Ownership, SelectOption, NavigationStep};
use std::{cell::RefCell, rc::Rc};
mod ui { slint::include_modules!(); }
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
enum Channel { Email, Push, Sms }
impl Channel {
fn all() -> [Self; 3] { [Self::Email, Self::Push, Self::Sms] }
fn token(self) -> i32 { match self { Self::Email => 11, Self::Push => 23, Self::Sms => 38 } }
fn from_token(token: i32) -> Option<Self> { Self::all().into_iter().find(|key| key.token() == token) }
fn label(self) -> &'static str { match self { Self::Email => "Email", Self::Push => "Push", Self::Sms => "SMS" } }
}
fn publish(app: &ui::App, controller: &MultiSelectController<Channel>, open: bool) {
let visible = controller.options().iter().map(|option| option.key).collect::<Vec<_>>();
let rows = visible.iter().copied().map(|key| ui::OptionRow { token: key.token(), generation: 1, label: key.label().into(), description: "".into(), disabled: false });
let choices = Channel::all().map(|key| ui::ChoiceRow { token: key.token(), label: key.label().into(), disabled: false, selected: controller.selected().contains(&key) });
let chips = controller.selected().iter().map(|key| ui::ChipRow { token: key.token(), label: key.label().into(), removable: true }).collect::<Vec<_>>();
let active = controller.active().copied().unwrap_or(Channel::Email);
app.set_rows(ModelRc::new(VecModel::from(rows.collect::<Vec<_>>()))); app.set_choices(ModelRc::new(VecModel::from(choices.to_vec()))); app.set_chips(ModelRc::new(VecModel::from(chips)));
app.set_select_state(ui::SelectState { open, generation: 1, active_index: visible.iter().position(|key| *key == active).map_or(-1, |index| index as i32), active: ui::SelectionRef { present: controller.active().is_some(), token: active.token() }, selected: ui::SelectionRef { present: false, token: -1 }, selection_availability: ui::SelectionAvailability::Absent });
app.set_result(format!("Selected: {}", controller.selected().iter().map(|key| key.label()).collect::<Vec<_>>().join(", ")).into());
}
fn step(value: ui::NavigationStep) -> NavigationStep { match value { ui::NavigationStep::Next => NavigationStep::Next, ui::NavigationStep::Previous => NavigationStep::Previous, ui::NavigationStep::First => NavigationStep::First, ui::NavigationStep::Last => NavigationStep::Last, ui::NavigationStep::PageNext => NavigationStep::PageNext, ui::NavigationStep::PagePrevious => NavigationStep::PagePrevious } }
fn main() -> Result<(), slint::PlatformError> {
let app = ui::App::new()?;
slint_kit::init_form_validation!(ui, app);
let controller = Rc::new(RefCell::new(MultiSelectController::new(Channel::all().into_iter().map(|key| SelectOption::new(key, key.label())).collect(), vec![Channel::Email], Ownership::Local, None).expect("unique channel keys")));
controller.borrow_mut().navigate(NavigationStep::First, 3);
publish(&app, &controller.borrow(), false);
let state = controller.clone(); let weak = app.as_weak();
app.on_open(move || { if let Some(app) = weak.upgrade() { publish(&app, &state.borrow(), true); } });
let state = controller.clone(); let weak = app.as_weak();
app.on_close(move || { if let Some(app) = weak.upgrade() { publish(&app, &state.borrow(), false); } });
let state = controller.clone(); let weak = app.as_weak();
app.on_navigate(move |direction| { state.borrow_mut().navigate(step(direction), 3); if let Some(app) = weak.upgrade() { publish(&app, &state.borrow(), app.get_select_state().open); } });
let state = controller.clone(); let weak = app.as_weak();
app.on_toggle(move |token, generation| {
if generation != 1 { return; }
if let Some(key) = Channel::from_token(token) { let _ = state.borrow_mut().toggle(&key); }
if let Some(app) = weak.upgrade() { publish(&app, &state.borrow(), true); }
});
let state = controller.clone(); let weak = app.as_weak();
let query = Rc::new(RefCell::new(String::new()));
let draft = query.clone();
app.on_query(move |text| {
*draft.borrow_mut() = text.to_string();
let needle = text.trim().to_lowercase();
let options = Channel::all().into_iter().filter(|key| key.label().to_lowercase().contains(&needle)).map(|key| SelectOption::new(key, key.label())).collect();
let mut controller = state.borrow_mut();
controller.set_options(options).expect("unique channel keys");
controller.navigate(NavigationStep::First, 3);
if let Some(app) = weak.upgrade() { publish(&app, &controller, true); }
});
let weak = app.as_weak();
app.on_typeahead(move |text| {
query.borrow_mut().push_str(&text);
let text = query.borrow().clone();
if let Some(app) = weak.upgrade() { app.invoke_query(text.into()); }
});
let weak = app.as_weak();
app.on_remove(move |token| {
if let Some(key) = Channel::from_token(token) { let accepted = controller.borrow().selected().iter().copied().filter(|selected| *selected != key).collect(); let _ = controller.borrow_mut().set_selected(accepted); }
if let Some(app) = weak.upgrade() { publish(&app, &controller.borrow(), app.get_select_state().open); }
});
app.run()
}
Cargo.toml et build.rs
Cargo.toml
[package]
name = "slint-kit-multi-select-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"); }
Fichiers : Cargo.toml · build.rs · ui/app.slint · src/main.rs
Projetez les options de MultiSelectController, les étiquettes retenues et SelectState. Traitez séparément activate et remove-requested ; reliez query-edited et typeahead au filtrage sans remplacer la sélection.
La requête ne remplace pas l’ensemble des choix acceptés. En lecture seule, les valeurs sont présentées dans des champs natifs sélectionnables et les modifications sont bloquées.
Composition visuelle
Le fichier ci-dessous produit la capture. Pour les composants pilotés par un contrôleur, il montre l’état affiché sans fournir le branchement Rust. Créer un projet.
Afficher le fichier de la capture

// Public label overrides localize this fixture; toolkit/host defaults keep their language.
// SPDX-License-Identifier: GPL-3.0-or-later
import { KitMultiSelect, KitTheme, KitThemes, SelectionAvailability } from "@kit/kit.slint";
export component Example inherits Window {
title: "KitMultiSelect — exemple";
preferred-width: 420px;
preferred-height: 220px;
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: "Sélection multiple"; color: root.theme.text-primary; font-size: 20px * root.theme.text-scale; font-weight: 600; }
KitMultiSelect { placeholder: "Ajouter une sélection"; width: 372px; theme: root.theme; rows: [{ token: 11, generation: 1, label: "Vue d’ensemble", description: "Résumé du projet", disabled: false }, { token: 23, generation: 1, label: "Activité", description: "Modifications récentes", disabled: false }, { token: 38, generation: 1, label: "Réglages", description: "Préférences du projet", disabled: false }]; chips: [{ token: 11, label: "Design", removable: true }]; state: { open: false, generation: 1, active-index: 0, active: { present: true, token: 11 }, selected: { present: true, token: 11 }, selection-availability: SelectionAvailability.available }; }
if root.status != "": Text { text: root.status; color: root.theme.text-secondary; font-size: 12px * root.theme.text-scale; wrap: word-wrap; }
}
}
API publique
Hérite de Rectangle.
Propriétés
| Nom | Type | Sens |
|---|---|---|
required |
bool |
in |
required-message |
string |
in |
custom-validity |
string |
in-out |
validity |
KitValidityState |
out |
will-validate |
bool |
out |
validation-message |
string |
out |
theme |
KitTheme |
in |
control-size |
ControlSize |
in |
access |
FieldAccess |
in |
rows |
[OptionRow] |
in |
chips |
[ChipRow] |
in |
state |
SelectState |
in |
placeholder |
string |
in |
Types et variantes
| Type | Déclaration |
|---|---|
struct ChipRow |
token: int, label: string, removable: bool |
enum CloseReason |
escape, outside-press, system-back, trigger-toggle, programmatic, host-closed |
enum ControlSize |
compact, medium, comfortable |
enum FieldAccess |
editable, read-only, disabled |
struct KitValidityState |
valid: bool, value-missing: bool, type-mismatch: bool, pattern-mismatch: bool, too-long: bool, too-short: bool, range-underflow: bool, range-overflow: bool, step-mismatch: bool, bad-input: bool, custom-error: bool, |
enum NavigationStep |
next, previous, first, last, page-next, page-previous |
struct OptionRow |
token: int, generation: int, label: string, description: string, disabled: bool |
enum SelectionAvailability |
absent, available, unavailable |
struct SelectionRef |
present: bool, token: int |
struct SelectState |
selection-availability: SelectionAvailability, selected: SelectionRef, active: SelectionRef, open: bool, active-index: int, generation: int |
Signaux
invalid()open-requested()navigate(NavigationStep)activate(int, int)dismiss(CloseReason)typeahead(string)tab-navigation(bool) → EventResultquery-edited(string)remove-requested(int)
Fonctions
check-validity() → boolreport-validity() → boolset-custom-validity(message: string)reset-validation()take-focus()