gpui-kit DatePicker 组件完全指南:构建跨平台日期选择与日期范围选择界面
【免费下载链接】gpui-kitRust GUI components for building fantastic cross-platform desktop application by using GPUI.项目地址: https://gitcode.com/GitHub_Trending/gp/gpui-kit
导读
DatePicker 是 gpui-kit 中基于日历交互界面的日期选择组件,支持单日期选择、日期范围选择、自定义日期格式化、日期禁用规则以及预设范围等完整能力。本指南以 website/component/date-picker.md 为骨架,结合 crates/component/src/time/date_picker.rs、crates/component/src/time/calendar.rs、crates/base/src/date_picker.rs 与 crates/base/src/calendar.rs 的源码实现,系统讲解从基础使用到业务场景(事件预约、酒店预订、财务周期选择)的完整实战方案。读完本文,你将能熟练驾驭 DatePicker 的全部配置项、禁用规则匹配器与事件订阅机制,并理解其背后的组件分层原理。
组件架构概览
从源码结构看,gpui-kit 的 DatePicker 遵循"基础行为与样式分离"的分层设计:
DatePickerState(状态实体):持有日期值、日历实体、日期格式、月份数、禁用匹配器等状态,负责状态更新、事件发射与键盘行为(见 crates/component/src/time/date_picker.rs)。DatePicker(样式化元素):接收&Entity<DatePickerState>,负责输入框外观、清空按钮、日历弹层与预设按钮的渲染,实现Sizable、Styled、Disableable、FocusableExt等 trait。CalendarState/Calendar(日历内核):真正的月视图/年视图网格、导航与日期激活逻辑,由 base 层提供无样式结构,component 层做主题化装饰(见 crates/base/src/calendar.rs 与 crates/component/src/time/calendar.rs)。Matcher:禁用日期规则抽象,支持星期、区间、间隔、自定义闭包四种形式(见 crates/base/src/calendar.rs)。
日期值使用Date枚举表示,即Date::Single(Option<NaiveDate>)与Date::Range(Option<NaiveDate>, Option<NaiveDate>),并实现了From<NaiveDate>与From<(NaiveDate, NaiveDate)>转换(见 crates/base/src/calendar.rs),因此set_date可以直接接收日期或日期元组。
引入与基础用法
导入模块
use gpui_kit::component::{ date_picker::{DatePicker, DatePickerState, DateRangePreset, DatePickerEvent}, calendar::{Date, Matcher}, };date_picker与calendar两个模块均从 crates/component/src/lib.rs 统一导出,并在组件初始化时通过date_picker::init(cx)注册enter、escape、delete、backspace等键盘快捷键(见 crates/component/src/time/date_picker.rs)。
基础日期选择器
let date_picker = cx.new(|cx| DatePickerState::new(window, cx)); DatePicker::new(&date_picker)DatePickerState::new内部调用new_with_range(false, ...),即单日期模式;DatePickerState::range则对应new_with_range(true, ...)的范围模式(见 crates/component/src/time/date_picker.rs)。
带初始日期
use chrono::Local; let date_picker = cx.new(|cx| { let mut picker = DatePickerState::new(window, cx); picker.set_date(Local::now().naive_local().date(), window, cx); picker }); DatePicker::new(&date_picker)set_date内部通过update_date(date, false, ...)同步更新内部日历状态并保持弹层关闭(emit 参数为false,不会触发Change事件,见 crates/component/src/time/date_picker.rs)。story 示例中同样以当前日期初始化选择器并禁用周末(crates/story/src/stories/date_picker_story.rs)。
日期范围选择器
use chrono::{Local, Days}; // 范围模式选择器 let range_picker = cx.new(|cx| DatePickerState::range(window, cx)); DatePicker::new(&range_picker) .number_of_months(2) // 展示 2 个月便于范围选择 // 带初始范围 let range_picker = cx.new(|cx| { let now = Local::now().naive_local().date(); let mut picker = DatePickerState::new(window, cx); picker.set_date( (now, now.checked_add_days(Days::new(7)).unwrap()), window, cx, ); picker }); DatePicker::new(&range_picker) .number_of_months(2)范围模式下的选择逻辑位于 base 层CalendarState::select_date:第一次点击记录起点(Range(Some(value), None)),第二次点击若日期晚于或等于起点则补全终点,否则重置为新起点(见 crates/base/src/calendar.rs)。
核心配置项
自定义日期格式
let date_picker = cx.new(|cx| { DatePickerState::new(window, cx) .date_format("%Y-%m-%d") // ISO 格式 }); DatePicker::new(&date_picker) // 其他格式示例: // "%m/%d/%Y" -> 12/25/2023 // "%B %d, %Y" -> December 25, 2023 // "%d %b %Y" -> 25 Dec 2023默认格式为"%Y/%m/%d"(如2026/09/15),格式串直接透传给 chrono 的NaiveDate::format;范围模式下会格式化为"{start} - {end}"形式(见 crates/base/src/calendar.rs)。kit 测试断言选中日期后输入框 value 为"2026/09/15"(crates/kit/tests/date_picker.rs)。
占位符
DatePicker::new(&date_picker) .placeholder("Select a date...")未设置时使用 i18n 默认文案t!("DatePicker.placeholder")(见 crates/component/src/time/date_picker.rs)。当未选中日期时,占位符以muted_foreground颜色显示。
可清空(Cleanable)
DatePicker::new(&date_picker) .cleanable(true) // 选中日期后显示清空按钮当cleanable为true且状态中日期非空时,输入框右侧显示清空按钮(clear_button),点击后调用clean将日期重置为Single(None)或Range(None, None)并发射Change事件(见 crates/component/src/time/date_picker.rs)。kit 测试验证了"选中后出现 clean 按钮 → 点击清空 → 按钮消失、value 为空"的完整链路(crates/kit/tests/date_picker.rs)。
不同尺寸
DatePicker::new(&date_picker).large() DatePicker::new(&date_picker) // medium(默认) DatePicker::new(&date_picker).small()尺寸通过Sizabletrait 实现,并同时作用于输入框与日历弹层宽度:Small 为224px × 月份数、Medium 为280px × 月份数、Large 为340px × 月份数(日历面板实际宽度见 crates/component/src/time/date_picker.rs)。
禁用状态
DatePicker::new(&date_picker).disabled(true)禁用后组件不可点击、不可打开弹层,输入框半透明(opacity(0.5)),且不再显示清空按钮与日历图标;同时 tab 焦点被移除(tab_stop(!disabled))。kit 测试disabled_date_picker_does_not_open验证了禁用组件点击后弹层不展开(crates/kit/tests/date_picker.rs)。
自定义外观
// 关闭默认样式 DatePicker::new(&date_picker).appearance(false) // 放入自定义容器 div() .border_b_2() .px_6() .py_3() .border_color(cx.theme().border) .bg(cx.theme().secondary) .child(DatePicker::new(&date_picker).appearance(false))appearance(false)去除输入框的背景、边框、圆角与聚焦环,只保留内容与交互,适合嵌入工具栏、搜索框等自定义容器(见 crates/component/src/time/date_picker.rs)。
日期限制(禁用规则)
禁用规则统一通过disabled_matcher传入,其参数实现了Into<Matcher>。Matcher的四种形态在 base 层定义(crates/base/src/calendar.rs),并在点击时由CalendarState::select_date校验——命中的日期直接拒绝选择(见 crates/base/src/calendar.rs)。
禁用周末
use gpui_kit::component::calendar; let date_picker = cx.new(|cx| { DatePickerState::new(window, cx) .disabled_matcher(vec![0, 6]) // Sunday=0, Saturday=6 }); DatePicker::new(&date_picker)Vec<u32>会转换为Matcher::DayOfWeek,匹配规则为date.weekday().num_days_from_sunday()是否落在列表中(见 crates/base/src/calendar.rs 与 crates/base/src/calendar.rs)。
禁用日期范围
use chrono::{Local, Days}; let now = Local::now().naive_local().date(); let date_picker = cx.new(|cx| { DatePickerState::new(window, cx) .disabled_matcher(calendar::Matcher::range( Some(now), now.checked_add_days(Days::new(7)), )) }); DatePicker::new(&date_picker)Matcher::range(from, to)表示闭区间:date >= from && date <= to的日期被禁用,from/to 均可为None表示单侧开放(见 crates/base/src/calendar.rs)。
禁用日期间隔
let date_picker = cx.new(|cx| { DatePickerState::new(window, cx) .disabled_matcher(calendar::Matcher::interval( Some(now), now.checked_add_days(Days::new(5)) )) }); DatePicker::new(&date_picker)Matcher::interval(before, after)与 range 相反,表示开区间之外的日期被禁用:date < before || date > after(即只允许[before, after]区间内的日期可选),语义适合"仅允许某段时间"的场景(见 crates/base/src/calendar.rs)。两者语义对比:
| Matcher | 禁用逻辑 | 典型场景 |
|---|---|---|
DayOfWeek(vec![0, 6]) | 命中指定星期 | 禁用周末 |
Range(from, to) | 区间内的日期禁用 | 禁用未来 7 天 |
Interval(before, after) | 区间外的日期禁用 | 只允许本周可选 |
Custom(fn) | 闭包返回 true 即禁用 | 任意业务规则 |
自定义禁用日期
// 禁用每月前 5 天 let date_picker = cx.new(|cx| { DatePickerState::new(window, cx) .disabled_matcher(calendar::Matcher::custom(|date| { date.day0() < 5 })) }); DatePicker::new(&date_picker) // 禁用所有周一 let date_picker = cx.new(|cx| { DatePickerState::new(window, cx) .disabled_matcher(calendar::Matcher::custom(|date| { date.weekday() == chrono::Weekday::Mon })) });Matcher::custom接受Fn(&NaiveDate) -> bool + Send + Sync + 'static闭包,返回true表示禁用。任何基于日期字段的规则(如day0()、weekday()、与某个日期比较)都可以实现,例如 story 中"禁用过去日期"的写法*date < Local::now().naive_local().date()(crates/story/src/stories/date_picker_story.rs)。
自定义年份范围
默认情况下,年视图展示当前年份前后各 50 年(即(当前年 - 50, 当前年 + 50),见 crates/base/src/calendar.rs)。通过set_year_range可以配置不同范围——例如生日选择器回溯到 1900 年:
use chrono::Datelike; // 生日选择器:允许 1900 到当前年份(含)的年份 let birthday_picker = cx.new(|cx| { let current_year = chrono::Local::now().year(); let mut picker = DatePickerState::new(window, cx) .date_format("%Y-%m-%d"); picker.set_year_range((1900, current_year + 1), window, cx); picker }); DatePicker::new(&birthday_picker) .cleanable(true) .placeholder("Select birthday")range参数使用半开区间(start, end),end不包含。因此要包含current_year,需传入(1900, current_year + 1)。内部实现将区间按每页 20 年切块(chunks(20)),并定位到当前年份所在页(见 crates/base/src/calendar.rs)。
set_year_range同时适用于单日期与范围模式。story 中的生日选择器以(1927, now.year() + 1)为例(crates/story/src/stories/date_picker_story.rs)。
预设范围(Presets)
预设通过DateRangePreset构造,分为单日期与范围两种。DateRangePreset::single(label, date)与DateRangePreset::range(label, start, end)(见 crates/component/src/time/date_picker.rs)。预设按钮渲染在日历左侧纵向排列(v_flex().gap_2().justify_end()),点击后调用select_preset直接更新日期并发射Change事件(见 crates/component/src/time/date_picker.rs)。
单日期预设
use chrono::{Utc, Duration}; let presets = vec![ DateRangePreset::single( "Yesterday", (Utc::now() - Duration::days(1)).naive_local().date(), ), DateRangePreset::single( "Last Week", (Utc::now() - Duration::weeks(1)).naive_local().date(), ), DateRangePreset::single( "Last Month", (Utc::now() - Duration::days(30)).naive_local().date(), ), ]; DatePicker::new(&date_picker) .presets(presets)日期范围预设
let range_presets = vec![ DateRangePreset::range( "Last 7 Days", (Utc::now() - Duration::days(7)).naive_local().date(), Utc::now().naive_local().date(), ), DateRangePreset::range( "Last 30 Days", (Utc::now() - Duration::days(30)).naive_local().date(), Utc::now().naive_local().date(), ), DateRangePreset::range( "Last 90 Days", (Utc::now() - Duration::days(90)).naive_local().date(), Utc::now().naive_local().date(), ), ]; DatePicker::new(&date_picker) .number_of_months(2) .presets(range_presets)kit 测试演示了预设的完整交互:点击预设按钮后弹层关闭、输入框值更新为对应日期("2026/09/15"),并断言预设按钮位于选择器输入框下方(crates/kit/tests/date_picker.rs)。
处理日期选择事件
let date_picker = cx.new(|cx| DatePickerState::new(window, cx)); cx.subscribe(&date_picker, |view, _, event, _| { match event { DatePickerEvent::Change(date) => { match date { Date::Single(Some(selected_date)) => { println!("Single date selected: {}", selected_date); } Date::Range(Some(start), Some(end)) => { println!("Date range selected: {} to {}", start, end); } Date::Range(Some(start), None) => { println!("Range start selected: {}", start); } _ => { println!("Date cleared"); } } } } });DatePickerEvent::Change携带完整的Date值,覆盖三种情况:单日期完成、范围起点已选但未完成(Range(Some(start), None))、范围完成与清空。范围模式下,第一次点击只发射起点,第二次点击补全终点后发射完整范围。注意set_date编程式设置不会触发事件,只有用户交互(日历点击、预设点击、清空)才会通过update_date(date, true, ...)发射Change(见 crates/component/src/time/date_picker.rs)。
多月份并排显示
// 并排显示 2 个月(适合日期范围选择) DatePicker::new(&date_picker) .number_of_months(2) // 显示 3 个月 DatePicker::new(&date_picker) .number_of_months(3)number_of_months同时存在于DatePickerState(默认 1)与DatePicker元素上,元素层的设置会传入内部Calendar。多月份模式下,头部为每个月份渲染独立的"年月"标签(text_sm().font_medium()),各月按顺序向右排开(见 crates/base/src/calendar.rs)。
高级实战示例
仅工作日可选
use chrono::Weekday; let business_days_picker = cx.new(|cx| { DatePickerState::new(window, cx) .disabled_matcher(calendar::Matcher::custom(|date| { matches!(date.weekday(), Weekday::Sat | Weekday::Sun) })) }); DatePicker::new(&business_days_picker) .placeholder("Select business day")限制最大范围时长
use chrono::Days; let max_30_days_picker = cx.new(|cx| DatePickerState::range(window, cx)); cx.subscribe(&max_30_days_picker, |view, picker, event, _| { match event { DatePickerEvent::Change(Date::Range(Some(start), Some(end))) => { let duration = end.signed_duration_since(*start).num_days(); if duration > 30 { // 超过 30 天则重置为只保留起点 picker.update(cx, |state, cx| { state.set_date(Date::Range(Some(*start), None), window, cx); }); } } _ => {} } }); DatePicker::new(&max_30_days_picker) .number_of_months(2) .placeholder("Select up to 30 days")此示例利用Change事件对用户选择进行二次校验:当范围跨度超过 30 天时,通过set_date将状态回退为仅含起点的未完成范围,用户可重新选择终点。
季度预设
use chrono::{NaiveDate, Datelike}; fn quarter_start(year: i32, quarter: u32) -> NaiveDate { let month = (quarter - 1) * 3 + 1; NaiveDate::from_ymd_opt(year, month, 1).unwrap() } fn quarter_end(year: i32, quarter: u32) -> NaiveDate { let month = quarter * 3; let start = NaiveDate::from_ymd_opt(year, month, 1).unwrap(); NaiveDate::from_ymd_opt(year, month, start.days_in_month()).unwrap() } let year = Local::now().year(); let quarterly_presets = vec![ DateRangePreset::range("Q1", quarter_start(year, 1), quarter_end(year, 1)), DateRangePreset::range("Q2", quarter_start(year, 2), quarter_end(year, 2)), DateRangePreset::range("Q3", quarter_start(year, 3), quarter_end(year, 3)), DateRangePreset::range("Q4", quarter_start(year, 4), quarter_end(year, 4)), ]; DatePicker::new(&date_picker) .presets(quarterly_presets)事件日期选择器(禁用过去日期)
let event_date = cx.new(|cx| { let mut picker = DatePickerState::new(window, cx) .date_format("%B %d, %Y") .disabled_matcher(calendar::Matcher::custom(|date| { // 禁用过去日期 *date < Local::now().naive_local().date() })); picker }); DatePicker::new(&event_date) .placeholder("Choose event date") .cleanable(true)预订系统日期范围
let booking_range = cx.new(|cx| DatePickerState::range(window, cx)); let booking_presets = vec![ DateRangePreset::range("This Weekend", /* weekend dates */), DateRangePreset::range("Next Week", /* next week dates */), DateRangePreset::range("This Month", /* this month dates */), ]; DatePicker::new(&booking_range) .number_of_months(2) .presets(booking_presets) .placeholder("Select check-in and check-out dates")财务周期选择器
let financial_period = cx.new(|cx| { DatePickerState::range(window, cx) .date_format("%Y-%m-%d") }); DatePicker::new(&financial_period) .number_of_months(3) .presets(quarterly_presets) .placeholder("Select reporting period")键盘交互与无障碍支持
DatePicker 内置完整的键盘支持,快捷键通过KeyBinding绑定到"DatePicker"上下文(见 crates/component/src/time/date_picker.rs):
| 按键 | 行为 |
|---|---|
Enter | 确认并打开/关闭弹层(Confirm) |
Escape | 关闭弹层并归还焦点(Cancel) |
Delete/Backspace | 清空当前选中日期 |
弹层关闭时焦点会归还给输入框(focus_back_if_need)。无障碍方面,base 层根元素声明了Role::ComboBox与aria_expanded状态(见 crates/base/src/date_picker.rs),选中日期通过aria_value暴露给辅助技术。
测试验证与更多参考
gpui-kit 为 DatePicker 提供了完整的自动化测试,覆盖以下关键路径(crates/kit/tests/date_picker.rs):
- 点击打开/关闭弹层(
expanded状态断言) - 点击预设并验证输入框 value(
"2026/09/15") - 月份前后翻页(
calendar-prev/calendar-next元素定位) - 日期选择后的 value 断言与清空按钮可见性
Escape关闭弹层- 禁用状态不可打开弹层
此外,crates/story/src/stories/date_picker_story.rs 提供了含单日期、范围、大中小尺寸、自定义格式、禁用规则、生日年份范围、无外观模式等完整 story 示例,可直接运行 story 应用(见 crates/story/src/main.rs)交互体验各配置项的实际效果。
【免费下载链接】gpui-kitRust GUI components for building fantastic cross-platform desktop application by using GPUI.项目地址: https://gitcode.com/GitHub_Trending/gp/gpui-kit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考