Sélecteur de plage de dates pour Slint
KitDateRangePicker · Slint Kit 0.3.1
Calendrier pour sélectionner une date de début et une date de fin.

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.
Naviguez entre les mois puis choisissez un jour. Rust valide les dates civiles et reconstruit le calendrier ; une plage n’est acceptée qu’après Appliquer.
ui/app.slint
export { KitFormValidation, HtmlInputKind, NumberInputFormat, KitInputConstraints, KitValidationResult, KitValidityState } from "@kit/kit.slint";
import { KitDateRangePicker,KitThemes,CalendarDay,CalendarNavigation,CivilDateView,DateRangePhase } from "@kit/kit.slint";
export {CalendarDay,CalendarNavigation,CivilDateView,DateRangePhase}
export component App inherits Window {
title:"date-range-picker";preferred-width:480px;preferred-height:560px;max-width:100000px;max-height:100000px;background:KitThemes.light.canvas;
in property <[CalendarDay]> days;in property <int> active;in property <string> calendar-title;in property <string> accepted;
callback navigate(CalendarNavigation);callback choose(CalendarDay);callback cancel();in property <DateRangePhase> phase;in property <string> summary;in property <string> error;callback apply();
VerticalLayout {padding:24px;spacing:16px;alignment:start;
control:=KitDateRangePicker{days:root.days;weekdays:["M","T","W","T","F","S","S"];active-index:root.active;title:root.calendar-title;navigate(step)=>{root.navigate(step);}choose(day)=>{root.choose(day);}cancelled=>{root.cancel();}phase:root.phase;summary:root.summary;error:root.error;apply-requested=>{root.apply();}}
Text{text:"Accepted: "+root.accepted;color:KitThemes.light.text-primary;wrap:word-wrap;}
}
}
src/main.rs
use slint::ComponentHandle;
use slint_kit::{
CalendarController, CalendarMove, CivilDate, DateRangeController, RangeSelection, WeekStart,
};
use std::{cell::RefCell, rc::Rc};
mod ui {
slint::include_modules!();
}
struct State {
calendar: CalendarController,
accepted: String,
range: DateRangeController,
}
fn movement(step: ui::CalendarNavigation) -> CalendarMove {
match step {
ui::CalendarNavigation::PreviousDay => CalendarMove::PreviousDay,
ui::CalendarNavigation::NextDay => CalendarMove::NextDay,
ui::CalendarNavigation::PreviousWeek => CalendarMove::PreviousWeek,
ui::CalendarNavigation::NextWeek => CalendarMove::NextWeek,
ui::CalendarNavigation::WeekStart => CalendarMove::WeekStart,
ui::CalendarNavigation::WeekEnd => CalendarMove::WeekEnd,
ui::CalendarNavigation::PreviousMonth => CalendarMove::PreviousMonth,
ui::CalendarNavigation::NextMonth => CalendarMove::NextMonth,
ui::CalendarNavigation::PreviousYear => CalendarMove::PreviousYear,
ui::CalendarNavigation::NextYear => CalendarMove::NextYear,
}
}
fn publish(app: &ui::App, state: &State) {
let slots = state.calendar.grid_slots();
let days = slots
.iter()
.map(|slot| {
slot.map_or_else(ui::CalendarDay::default, |cell| ui::CalendarDay {
year: cell.date.year(),
month: cell.date.month() as i32,
day: cell.date.day() as i32,
label: cell.date.day().to_string().into(),
accessible_label: cell.date.iso_string().into(),
enabled: cell.enabled,
selected: cell.selected,
in_range: false,
today: false,
})
})
.collect::<Vec<_>>();
let mut days = days;
match state.range.draft() {
RangeSelection::ChoosingStart => {
app.set_phase(ui::DateRangePhase::ChoosingStart);
app.set_summary("Choose a start date".into());
}
RangeSelection::ChoosingEnd { start } => {
app.set_phase(ui::DateRangePhase::ChoosingEnd);
app.set_summary(format!("{} → choose an end date", start.iso_string()).into());
for day in &mut days {
day.selected = day.year == start.year()
&& day.month == start.month() as i32
&& day.day == start.day() as i32;
}
}
RangeSelection::Complete(range) => {
app.set_phase(ui::DateRangePhase::Complete);
app.set_summary(
format!(
"{} → {}",
range.start().iso_string(),
range.end().iso_string()
)
.into(),
);
for day in &mut days {
if let Ok(date) = CivilDate::new(day.year, day.month as u32, day.day as u32) {
day.selected = date == range.start() || date == range.end();
day.in_range = range.contains(date);
}
}
}
}
app.set_days(slint::ModelRc::new(slint::VecModel::from(days)));
app.set_active(
slots
.iter()
.position(|s| s.is_some_and(|cell| cell.focused))
.unwrap_or(0) as i32,
);
let month = state.calendar.displayed();
app.set_calendar_title(format!("{:04}-{:02}", month.year(), month.month()).into());
app.set_accepted(state.accepted.clone().into());
}
fn main() -> Result<(), slint::PlatformError> {
let app = ui::App::new()?;
slint_kit::init_form_validation!(ui, app);
let date = CivilDate::new(2026, 10, 17).unwrap();
let state = Rc::new(RefCell::new(State {
calendar: CalendarController::new(Some(date), date.year_month(), None, WeekStart::Monday),
accepted: "No range accepted".into(),
range: DateRangeController::new(None),
}));
publish(&app, &state.borrow());
let weak = app.as_weak();
let shared = state.clone();
app.on_navigate(move |step| {
if let Some(app) = weak.upgrade() {
let mut state = shared.borrow_mut();
if state.calendar.navigate(movement(step)).is_ok() {
publish(&app, &state);
}
}
});
let weak = app.as_weak();
let shared = state.clone();
app.on_choose(move |day| {
if !day.enabled {
return;
}
if let (Some(app), Ok(date)) = (
weak.upgrade(),
CivilDate::new(day.year, day.month as u32, day.day as u32),
) {
let mut state = shared.borrow_mut();
if state.calendar.choose(date).is_ok() {
let error = if state.range.choose(date).is_ok() {
""
} else {
"End must follow start"
};
app.set_error(error.into());
publish(&app, &state);
}
}
});
let weak = app.as_weak();
let shared = state.clone();
app.on_cancel(move || {
if let Some(app) = weak.upgrade() {
let mut state = shared.borrow_mut();
state.range.cancel();
app.set_error("".into());
publish(&app, &state);
}
});
let weak = app.as_weak();
let shared = state.clone();
app.on_apply(move || {
if let Some(app) = weak.upgrade() {
let mut state = shared.borrow_mut();
if let Some(range) = state.range.apply() {
state.accepted = format!(
"{} → {}",
range.start().iso_string(),
range.end().iso_string()
);
app.set_accepted(state.accepted.clone().into());
}
}
});
app.run()
}
Cargo.toml et build.rs
Cargo.toml
[package]
name = "slint-kit-date-range-picker-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
Reliez les jours à DateRangeController et publiez sa phase de sélection. Appliquer valide une plage complète ; Annuler la restaure.
L’exemple marque les deux extrémités et les dates intermédiaires. La fin doit suivre le début.
Choisir entre une option intégrée et une composition personnalisée
Options partagées
error— propriété intégrée ; exemple de composition personnalisé
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
// Display projections are fixtures. Validate accepted domain values in Rust.
import { KitDateRangePicker, KitTheme, KitThemes } from "@kit/kit.slint";
export component Example inherits Window {
title: "KitDateRangePicker — exemple";
preferred-width: 420px;
preferred-height: 520px;
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électeur de période"; color: root.theme.text-primary; font-size: 20px * root.theme.text-scale; font-weight: 600; }
KitDateRangePicker { weekdays: ["L", "M", "M", "J", "V", "S", "D"]; theme: root.theme; title: "Octobre 2026"; days: [{ year: 2026, month: 9, day: 28, label: "28", accessible-label: "28 septembre 2026", enabled: false, selected: false, today: false, in-range: false }, { year: 2026, month: 9, day: 29, label: "29", accessible-label: "29 septembre 2026", enabled: false, selected: false, today: false, in-range: false }, { year: 2026, month: 9, day: 30, label: "30", accessible-label: "30 septembre 2026", enabled: false, selected: false, today: false, in-range: false }, { year: 2026, month: 10, day: 1, label: "1", accessible-label: "1er octobre 2026", enabled: true, selected: false, today: false, in-range: false }, { year: 2026, month: 10, day: 2, label: "2", accessible-label: "2 octobre 2026", enabled: true, selected: false, today: false, in-range: false }, { year: 2026, month: 10, day: 3, label: "3", accessible-label: "3 octobre 2026", enabled: true, selected: false, today: false, in-range: false }, { year: 2026, month: 10, day: 4, label: "4", accessible-label: "4 octobre 2026", enabled: true, selected: false, today: false, in-range: false }, { year: 2026, month: 10, day: 5, label: "5", accessible-label: "5 octobre 2026", enabled: true, selected: false, today: false, in-range: false }, { year: 2026, month: 10, day: 6, label: "6", accessible-label: "6 octobre 2026", enabled: true, selected: false, today: false, in-range: false }, { year: 2026, month: 10, day: 7, label: "7", accessible-label: "7 octobre 2026", enabled: true, selected: false, today: false, in-range: false }, { year: 2026, month: 10, day: 8, label: "8", accessible-label: "8 octobre 2026", enabled: true, selected: false, today: false, in-range: false }, { year: 2026, month: 10, day: 9, label: "9", accessible-label: "9 octobre 2026", enabled: true, selected: false, today: false, in-range: false }, { year: 2026, month: 10, day: 10, label: "10", accessible-label: "10 octobre 2026", enabled: true, selected: false, today: false, in-range: false }, { year: 2026, month: 10, day: 11, label: "11", accessible-label: "11 octobre 2026", enabled: true, selected: false, today: false, in-range: false }, { year: 2026, month: 10, day: 12, label: "12", accessible-label: "12 octobre 2026", enabled: true, selected: false, today: false, in-range: false }, { year: 2026, month: 10, day: 13, label: "13", accessible-label: "13 octobre 2026", enabled: true, selected: false, today: false, in-range: false }, { year: 2026, month: 10, day: 14, label: "14", accessible-label: "14 octobre 2026", enabled: true, selected: false, today: false, in-range: false }, { year: 2026, month: 10, day: 15, label: "15", accessible-label: "15 octobre 2026", enabled: true, selected: false, today: false, in-range: false }, { year: 2026, month: 10, day: 16, label: "16", accessible-label: "16 octobre 2026", enabled: true, selected: false, today: false, in-range: false }, { year: 2026, month: 10, day: 17, label: "17", accessible-label: "17 octobre 2026", enabled: true, selected: true, today: true, in-range: false }, { year: 2026, month: 10, day: 18, label: "18", accessible-label: "18 octobre 2026", enabled: true, selected: false, today: false, in-range: false }, { year: 2026, month: 10, day: 19, label: "19", accessible-label: "19 octobre 2026", enabled: true, selected: false, today: false, in-range: false }, { year: 2026, month: 10, day: 20, label: "20", accessible-label: "20 octobre 2026", enabled: true, selected: false, today: false, in-range: false }, { year: 2026, month: 10, day: 21, label: "21", accessible-label: "21 octobre 2026", enabled: true, selected: false, today: false, in-range: false }, { year: 2026, month: 10, day: 22, label: "22", accessible-label: "22 octobre 2026", enabled: true, selected: false, today: false, in-range: false }, { year: 2026, month: 10, day: 23, label: "23", accessible-label: "23 octobre 2026", enabled: true, selected: false, today: false, in-range: false }, { year: 2026, month: 10, day: 24, label: "24", accessible-label: "24 octobre 2026", enabled: true, selected: false, today: false, in-range: false }, { year: 2026, month: 10, day: 25, label: "25", accessible-label: "25 octobre 2026", enabled: true, selected: false, today: false, in-range: false }, { year: 2026, month: 10, day: 26, label: "26", accessible-label: "26 octobre 2026", enabled: true, selected: false, today: false, in-range: false }, { year: 2026, month: 10, day: 27, label: "27", accessible-label: "27 octobre 2026", enabled: true, selected: false, today: false, in-range: false }, { year: 2026, month: 10, day: 28, label: "28", accessible-label: "28 octobre 2026", enabled: true, selected: false, today: false, in-range: false }, { year: 2026, month: 10, day: 29, label: "29", accessible-label: "29 octobre 2026", enabled: true, selected: false, today: false, in-range: false }, { year: 2026, month: 10, day: 30, label: "30", accessible-label: "30 octobre 2026", enabled: true, selected: false, today: false, in-range: false }, { year: 2026, month: 10, day: 31, label: "31", accessible-label: "31 octobre 2026", enabled: true, selected: false, today: false, in-range: false }, { year: 2026, month: 11, day: 1, label: "1", accessible-label: "1er novembre 2026", enabled: false, selected: false, today: false, in-range: false }, { year: 2026, month: 11, day: 2, label: "2", accessible-label: "2 novembre 2026", enabled: false, selected: false, today: false, in-range: false }, { year: 2026, month: 11, day: 3, label: "3", accessible-label: "3 novembre 2026", enabled: false, selected: false, today: false, in-range: false }, { year: 2026, month: 11, day: 4, label: "4", accessible-label: "4 novembre 2026", enabled: false, selected: false, today: false, in-range: false }, { year: 2026, month: 11, day: 5, label: "5", accessible-label: "5 novembre 2026", enabled: false, selected: false, today: false, in-range: false }, { year: 2026, month: 11, day: 6, label: "6", accessible-label: "6 novembre 2026", enabled: false, selected: false, today: false, in-range: false }, { year: 2026, month: 11, day: 7, label: "7", accessible-label: "7 novembre 2026", enabled: false, selected: false, today: false, in-range: false }, { year: 2026, month: 11, day: 8, label: "8", accessible-label: "8 novembre 2026", enabled: false, selected: false, today: false, in-range: false }]; summary: "Du 17 au 21 octobre 2026"; choose(day) => { root.status = "Jour demandé " + day.day; } }
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 |
|---|---|---|
theme |
KitTheme |
in |
control-size |
ControlSize |
in |
access |
FieldAccess |
in |
phase |
DateRangePhase |
in |
days |
[CalendarDay] |
in |
weekdays |
[string] |
in |
active-index |
int |
in |
title |
string |
in |
summary |
string |
in |
error |
string |
in |
Types et variantes
| Type | Déclaration |
|---|---|
struct CalendarDay |
year: int, month: int, day: int, label: string, accessible-label: string, enabled: bool, selected: bool, in-range: bool, today: bool, |
enum CalendarNavigation |
previous-day, next-day, previous-week, next-week, week-start, week-end, previous-month, next-month, previous-year, next-year |
enum ControlSize |
compact, medium, comfortable |
enum DateRangePhase |
choosing-start, choosing-end, complete |
enum FieldAccess |
editable, read-only, disabled |
Signaux
navigate(CalendarNavigation)choose(CalendarDay)apply-requested()cancelled()