Slint Kit Documentation

Choices and values

Multi Select for Slint

KitMultiSelect · Slint Kit 0.3.0

Multiple-choice selector with removable chips for accepted values.

Multiple-choice selector with removable chips for accepted values.
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.

Choose Push to add it without losing Email. Remove a chip or toggle an accepted channel to remove it.

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 and 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"); }

Files: Cargo.toml · build.rs · ui/app.slint · src/main.rs

Project MultiSelectController options, accepted chips and SelectState. Handle activate and remove-requested separately; connect query-edited and typeahead to filtering without replacing the accepted set.

The query does not replace the accepted choice set. In read-only mode values appear in selectable native fields and changes are blocked.

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
Multiple-choice selector with removable chips for accepted values.
Native screenshot of the same example, rendered with Skia.
// SPDX-License-Identifier: GPL-3.0-or-later
import { KitMultiSelect, KitTheme, KitThemes, SelectionAvailability } from "@kit/kit.slint";

export component Example inherits Window {
    title: "KitMultiSelect example";
    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: "Multi Select"; color: root.theme.text-primary; font-size: 20px * root.theme.text-scale; font-weight: 600; }
        KitMultiSelect { width: 372px; theme: root.theme; rows: [{ token: 11, generation: 1, label: "Overview", description: "Project summary", disabled: false }, { token: 23, generation: 1, label: "Activity", description: "Recent changes", disabled: false }, { token: 38, generation: 1, label: "Settings", description: "Project preferences", 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; }
    }
}

Download this example

Public API

Inherits Rectangle.

Properties

Name Type Direction
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 and variants

Type Declaration
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

Callbacks

Functions

See also