use crate::motion::pop_in;
use gpui::{
App, Bounds, Context, Element, ElementId, Entity, GlobalElementId, Hsla, InspectorElementId,
IntoElement, LayoutId, MouseButton, Pixels, Render, SharedString, Window, actions, div,
prelude::*, px,
};
use liora_core::{Config, push_portal};
use liora_icons::Icon;
use liora_icons_lucide::IconName;
actions!(date_picker, [DatePickerClose]);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct DateValue {
pub year: i32,
pub month: u32,
pub day: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DatePickerType {
#[default]
Date,
DateRange,
Month,
MonthRange,
Year,
YearRange,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DatePickerSelection {
Single(Option<DateValue>),
Range {
start: Option<DateValue>,
end: Option<DateValue>,
},
}
pub struct DatePicker {
id: SharedString,
picker_type: DatePickerType,
value: Option<DateValue>,
range_start: Option<DateValue>,
range_end: Option<DateValue>,
view_year: i32,
view_month: u32,
is_open: bool,
placeholder: SharedString,
display_format: Option<SharedString>,
range_separator: SharedString,
width: Option<Pixels>,
disabled: bool,
last_bounds: Option<Bounds<Pixels>>,
close_on_click_outside: bool,
close_on_escape: bool,
on_change: Option<Box<dyn Fn(Option<DateValue>, &mut Window, &mut App) + 'static>>,
on_range_change:
Option<Box<dyn Fn(Option<DateValue>, Option<DateValue>, &mut Window, &mut App) + 'static>>,
on_selection_change: Option<Box<dyn Fn(DatePickerSelection, &mut Window, &mut App) + 'static>>,
}
impl DateValue {
pub fn new(year: i32, month: u32, day: u32) -> Option<Self> {
if !(1..=12).contains(&month) || day == 0 || day > days_in_month(year, month) {
return None;
}
Some(Self { year, month, day })
}
pub fn format(&self) -> String {
format!("{:04}-{:02}-{:02}", self.year, self.month, self.day)
}
}
impl DatePicker {
pub fn new() -> Self {
Self {
id: liora_core::unique_id("date-picker"),
picker_type: DatePickerType::Date,
value: None,
range_start: None,
range_end: None,
view_year: 2026,
view_month: 5,
is_open: false,
placeholder: "请选择日期".into(),
display_format: None,
range_separator: " 至 ".into(),
width: None,
disabled: false,
last_bounds: None,
close_on_click_outside: true,
close_on_escape: true,
on_change: None,
on_range_change: None,
on_selection_change: None,
}
}
pub fn id(mut self, id: impl Into<SharedString>) -> Self {
self.id = id.into();
self
}
pub fn picker_type(mut self, picker_type: DatePickerType) -> Self {
self.picker_type = picker_type;
if self.placeholder == SharedString::from("请选择日期") {
self.placeholder = default_placeholder(picker_type).into();
}
self
}
pub fn date(self) -> Self {
self.picker_type(DatePickerType::Date)
}
pub fn date_range(self) -> Self {
self.picker_type(DatePickerType::DateRange)
}
pub fn month(self) -> Self {
self.picker_type(DatePickerType::Month)
}
pub fn month_range(self) -> Self {
self.picker_type(DatePickerType::MonthRange)
}
pub fn year(self) -> Self {
self.picker_type(DatePickerType::Year)
}
pub fn year_range(self) -> Self {
self.picker_type(DatePickerType::YearRange)
}
pub fn value(mut self, value: DateValue) -> Self {
self.view_year = value.year;
self.view_month = value.month;
self.value = Some(normalize_value(value, self.picker_type));
self
}
pub fn range(mut self, start: DateValue, end: DateValue) -> Self {
let (start, end) = ordered_pair(
normalize_value(start, self.picker_type),
normalize_value(end, self.picker_type),
);
self.view_year = start.year;
self.view_month = start.month;
self.range_start = Some(start);
self.range_end = Some(end);
self
}
pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
self.placeholder = placeholder.into();
self
}
pub fn format(mut self, format: impl Into<SharedString>) -> Self {
self.display_format = Some(format.into());
self
}
pub fn range_separator(mut self, separator: impl Into<SharedString>) -> Self {
self.range_separator = separator.into();
self
}
pub fn width(mut self, width: impl Into<Pixels>) -> Self {
self.width = Some(width.into());
self
}
pub fn width_md(self) -> Self {
self.width(px(260.0))
}
pub fn width_lg(self) -> Self {
self.width(px(320.0))
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn close_on_escape(mut self, close: bool) -> Self {
self.close_on_escape = close;
self
}
pub fn close_on_click_outside(mut self, close: bool) -> Self {
self.close_on_click_outside = close;
self
}
pub fn register_key_bindings(cx: &mut App) {
cx.bind_keys([gpui::KeyBinding::new("escape", DatePickerClose, None)]);
}
fn close_on_escape_action(
&mut self,
_: &DatePickerClose,
_: &mut Window,
cx: &mut Context<Self>,
) {
if self.close_on_escape && self.is_open {
self.close(cx);
}
}
pub fn on_change(
mut self,
f: impl Fn(Option<DateValue>, &mut Window, &mut App) + 'static,
) -> Self {
self.on_change = Some(Box::new(f));
self
}
pub fn on_range_change(
mut self,
f: impl Fn(Option<DateValue>, Option<DateValue>, &mut Window, &mut App) + 'static,
) -> Self {
self.on_range_change = Some(Box::new(f));
self
}
pub fn on_selection_change(
mut self,
f: impl Fn(DatePickerSelection, &mut Window, &mut App) + 'static,
) -> Self {
self.on_selection_change = Some(Box::new(f));
self
}
pub fn set_on_change(
&mut self,
f: impl Fn(Option<DateValue>, &mut Window, &mut App) + 'static,
_cx: &mut Context<Self>,
) {
self.on_change = Some(Box::new(f));
}
pub fn set_on_range_change(
&mut self,
f: impl Fn(Option<DateValue>, Option<DateValue>, &mut Window, &mut App) + 'static,
_cx: &mut Context<Self>,
) {
self.on_range_change = Some(Box::new(f));
}
pub fn set_on_selection_change(
&mut self,
f: impl Fn(DatePickerSelection, &mut Window, &mut App) + 'static,
_cx: &mut Context<Self>,
) {
self.on_selection_change = Some(Box::new(f));
}
pub fn set_value(&mut self, value: Option<DateValue>, cx: &mut Context<Self>) {
self.value = value.map(|value| normalize_value(value, self.picker_type));
if let Some(value) = self.value {
self.view_year = value.year;
self.view_month = value.month;
}
cx.notify();
}
pub fn set_range(
&mut self,
start: Option<DateValue>,
end: Option<DateValue>,
cx: &mut Context<Self>,
) {
match (start, end) {
(Some(start), Some(end)) => {
let (start, end) = ordered_pair(
normalize_value(start, self.picker_type),
normalize_value(end, self.picker_type),
);
self.range_start = Some(start);
self.range_end = Some(end);
self.view_year = start.year;
self.view_month = start.month;
}
(start, end) => {
self.range_start = start.map(|value| normalize_value(value, self.picker_type));
self.range_end = end.map(|value| normalize_value(value, self.picker_type));
}
}
cx.notify();
}
pub fn value_ref(&self) -> Option<DateValue> {
self.value
}
pub fn range_ref(&self) -> (Option<DateValue>, Option<DateValue>) {
(self.range_start, self.range_end)
}
fn display_text(&self) -> String {
if self.picker_type.is_range() {
match (self.range_start, self.range_end) {
(Some(start), Some(end)) => format!(
"{}{}{}",
self.format_value(start),
self.range_separator,
self.format_value(end)
),
(Some(start), None) => {
format!("{}{}", self.format_value(start), self.range_separator)
}
_ => self.placeholder.to_string(),
}
} else {
self.value
.map(|value| self.format_value(value))
.unwrap_or_else(|| self.placeholder.to_string())
}
}
fn format_value(&self, value: DateValue) -> String {
let format = self
.display_format
.as_ref()
.map(|format| format.as_ref())
.unwrap_or_else(|| default_format(self.picker_type));
format_date_value(value, format)
}
fn has_display_value(&self) -> bool {
if self.picker_type.is_range() {
self.range_start.is_some()
} else {
self.value.is_some()
}
}
fn toggle_open(&mut self, cx: &mut Context<Self>) {
if self.disabled {
return;
}
self.is_open = !self.is_open;
cx.notify();
}
fn close(&mut self, cx: &mut Context<Self>) {
if self.is_open {
self.is_open = false;
cx.notify();
}
}
fn select_value(&mut self, value: DateValue, window: &mut Window, cx: &mut Context<Self>) {
let value = normalize_value(value, self.picker_type);
if self.picker_type.is_range() {
match (self.range_start, self.range_end) {
(None, _) | (Some(_), Some(_)) => {
self.range_start = Some(value);
self.range_end = None;
}
(Some(start), None) => {
let (start, end) = ordered_pair(start, value);
self.range_start = Some(start);
self.range_end = Some(end);
self.is_open = false;
}
}
} else {
self.value = Some(value);
self.is_open = false;
}
self.view_year = value.year;
self.view_month = value.month;
self.emit_change(window, cx);
cx.notify();
}
fn emit_change(&self, window: &mut Window, cx: &mut App) {
if self.picker_type.is_range() {
if let Some(ref on_range_change) = self.on_range_change {
on_range_change(self.range_start, self.range_end, window, cx);
}
if let Some(ref on_selection_change) = self.on_selection_change {
on_selection_change(
DatePickerSelection::Range {
start: self.range_start,
end: self.range_end,
},
window,
cx,
);
}
} else {
if let Some(ref on_change) = self.on_change {
on_change(self.value, window, cx);
}
if let Some(ref on_selection_change) = self.on_selection_change {
on_selection_change(DatePickerSelection::Single(self.value), window, cx);
}
}
}
fn shift_month(&mut self, delta: i32, cx: &mut Context<Self>) {
let month_index = self.view_year * 12 + self.view_month as i32 - 1 + delta;
self.view_year = month_index.div_euclid(12);
self.view_month = month_index.rem_euclid(12) as u32 + 1;
cx.notify();
}
fn shift_year(&mut self, delta: i32, cx: &mut Context<Self>) {
self.view_year += delta;
cx.notify();
}
}
impl DatePickerType {
fn is_range(self) -> bool {
matches!(
self,
DatePickerType::DateRange | DatePickerType::MonthRange | DatePickerType::YearRange
)
}
}
impl Render for DatePicker {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = cx.global::<Config>().theme.clone();
let entity = cx.entity().clone();
let display = self.display_text();
let range_start_text = self.range_start.map(|value| self.format_value(value));
let range_end_text = self.range_end.map(|value| self.format_value(value));
let range_separator = self.range_separator.clone();
let is_range = self.picker_type.is_range();
let has_value = self.has_display_value();
let border_color = if self.is_open {
theme.primary.base
} else {
theme.neutral.border
};
if self.is_open {
let entity = entity.clone();
let picker_id = self.id.clone();
let bounds = self.last_bounds;
let close_on_click_outside = self.close_on_click_outside;
push_portal(
move |_window, _cx| {
let (top, left, width) = if let Some(bounds) = bounds {
(bounds.bottom() + px(4.0), bounds.left(), bounds.size.width)
} else {
(px(100.0), px(100.0), px(240.0))
};
let close_entity = entity.clone();
div()
.absolute()
.top_0()
.left_0()
.size_full()
.bg(gpui::transparent_black())
.when(close_on_click_outside, |s| {
s.on_mouse_down(MouseButton::Left, move |_, _, cx| {
close_entity.update(cx, |picker, cx| picker.close(cx));
})
})
.child(pop_in(
format!("{}-panel-motion", picker_id),
div()
.absolute()
.top(top)
.left(left)
.w(width.max(px(300.0)))
.child(render_picker_panel(picker_id, entity, _cx)),
))
.into_any_element()
},
cx,
);
}
div()
.relative()
.when_some(self.width, |s, width| s.w(width))
.when(self.width.is_none(), |s| s.w(px(220.0)))
.h(px(34.0))
.id(format!("{}-trigger", self.id))
.flex()
.items_center()
.gap_2()
.px_3()
.bg(if self.disabled {
theme.neutral.hover
} else {
theme.neutral.card
})
.border_1()
.border_color(border_color)
.rounded(px(theme.radius.md))
.cursor_pointer()
.hover(|s| s.cursor_pointer().border_color(theme.primary.base))
.child(div().flex_1().min_w(px(0.0)).child(if is_range {
render_range_trigger_text(
range_start_text,
range_end_text,
range_separator,
self.placeholder.clone(),
has_value,
&theme,
)
} else {
div()
.text_size(px(theme.font_size.md))
.text_color(if has_value {
theme.neutral.text_1
} else {
theme.neutral.placeholder
})
.child(display)
.into_any_element()
}))
.child(
Icon::new(IconName::CalendarDays)
.size(px(16.0))
.color(theme.neutral.icon),
)
.child(
div()
.absolute()
.top_0()
.left_0()
.size_full()
.child(DatePickerBoundsCapturer { picker: entity }),
)
.on_mouse_down(
MouseButton::Left,
cx.listener(|this, _, _, cx| {
this.toggle_open(cx);
}),
)
.on_action(cx.listener(Self::close_on_escape_action))
}
}
fn render_range_trigger_text(
start: Option<String>,
end: Option<String>,
separator: SharedString,
placeholder: SharedString,
has_value: bool,
theme: &liora_theme::Theme,
) -> gpui::AnyElement {
if !has_value {
return div()
.text_size(px(theme.font_size.md))
.text_color(theme.neutral.placeholder)
.child(placeholder)
.into_any_element();
}
let start = start.unwrap_or_default();
let end = end.unwrap_or_else(|| "请选择结束".to_string());
div()
.flex()
.items_center()
.gap_2()
.w_full()
.child(range_value_text(start, true, theme))
.child(
div()
.flex_shrink_0()
.px_2()
.py_1()
.rounded(px(theme.radius.sm))
.bg(theme.neutral.hover)
.text_xs()
.text_color(theme.neutral.text_3)
.child(separator),
)
.child(range_value_text(end, false, theme))
.into_any_element()
}
fn range_value_text(
text: impl Into<SharedString>,
filled: bool,
theme: &liora_theme::Theme,
) -> impl IntoElement {
div()
.flex_1()
.min_w(px(0.0))
.px_1()
.text_size(px(theme.font_size.md))
.text_color(if filled {
theme.neutral.text_1
} else {
theme.neutral.text_3
})
.child(text.into())
}
fn render_picker_panel(
id: SharedString,
picker: Entity<DatePicker>,
cx: &mut App,
) -> gpui::AnyElement {
let picker_type = picker.update(cx, |picker, _| picker.picker_type);
match picker_type {
DatePickerType::Date | DatePickerType::DateRange => render_date_panel(id, picker, cx),
DatePickerType::Month | DatePickerType::MonthRange => render_month_panel(id, picker, cx),
DatePickerType::Year | DatePickerType::YearRange => render_year_panel(id, picker, cx),
}
}
fn panel_shell(id: &SharedString, theme: &liora_theme::Theme) -> gpui::Stateful<gpui::Div> {
div()
.id(format!("{}-panel", id))
.cursor_default()
.occlude()
.on_mouse_down(MouseButton::Left, |_, _, cx| {
cx.stop_propagation();
})
.flex()
.flex_col()
.p_3()
.gap_3()
.bg(theme.neutral.card)
.border_1()
.border_color(theme.neutral.border)
.rounded(px(theme.radius.md))
.shadow_lg()
}
fn render_date_panel(
id: SharedString,
picker: Entity<DatePicker>,
cx: &mut App,
) -> gpui::AnyElement {
let theme = cx.global::<Config>().theme.clone();
let (view_year, view_month, selected, range_start, range_end) =
picker.update(cx, |picker, _| {
(
picker.view_year,
picker.view_month,
picker.value,
picker.range_start,
picker.range_end,
)
});
let days = calendar_cells(view_year, view_month);
let picker_prev_year = picker.clone();
let picker_prev_month = picker.clone();
let picker_next_month = picker.clone();
let picker_next_year = picker.clone();
let weekdays = ["一", "二", "三", "四", "五", "六", "日"];
panel_shell(&id, &theme)
.child(
div()
.flex()
.items_center()
.justify_between()
.child(
div()
.flex()
.items_center()
.gap_1()
.child(nav_button(
format!("{}-prev-year", id),
IconName::ChevronsLeft,
theme.neutral.icon,
picker_prev_year,
|picker, cx| picker.shift_year(-1, cx),
))
.child(nav_button(
format!("{}-prev-month", id),
IconName::ChevronLeft,
theme.neutral.icon,
picker_prev_month,
|picker, cx| picker.shift_month(-1, cx),
)),
)
.child(
div()
.text_sm()
.font_weight(gpui::FontWeight::BOLD)
.text_color(theme.neutral.text_1)
.child(format!("{} 年 {:02} 月", view_year, view_month)),
)
.child(
div()
.flex()
.items_center()
.gap_1()
.child(nav_button(
format!("{}-next-month", id),
IconName::ChevronRight,
theme.neutral.icon,
picker_next_month,
|picker, cx| picker.shift_month(1, cx),
))
.child(nav_button(
format!("{}-next-year", id),
IconName::ChevronsRight,
theme.neutral.icon,
picker_next_year,
|picker, cx| picker.shift_year(1, cx),
)),
),
)
.child(
div()
.flex()
.flex_row()
.children(weekdays.into_iter().map(|day| {
div()
.flex_1()
.h(px(28.0))
.flex()
.items_center()
.justify_center()
.text_xs()
.text_color(theme.neutral.text_3)
.child(day)
})),
)
.child(
div()
.flex()
.flex_col()
.children(days.chunks(7).enumerate().map(|(week_idx, week)| {
let id = id.clone();
let week_picker = picker.clone();
let week_theme = theme.clone();
div()
.flex()
.flex_row()
.children(week.iter().enumerate().map(move |(day_idx, cell)| {
let is_current_month = cell.month == view_month;
let is_selected = selected == Some(*cell)
|| range_start == Some(*cell)
|| range_end == Some(*cell);
let in_range = is_between(*cell, range_start, range_end);
let picker = week_picker.clone();
let date = *cell;
selectable_cell(
format!("{}-day-{}-{}", id, week_idx, day_idx),
cell.day.to_string(),
is_selected,
in_range,
is_current_month,
week_theme.clone(),
picker,
move |picker, window, cx| picker.select_value(date, window, cx),
)
}))
})),
)
.into_any_element()
}
fn render_month_panel(
id: SharedString,
picker: Entity<DatePicker>,
cx: &mut App,
) -> gpui::AnyElement {
let theme = cx.global::<Config>().theme.clone();
let (view_year, selected, range_start, range_end) = picker.update(cx, |picker, _| {
(
picker.view_year,
picker.value,
picker.range_start,
picker.range_end,
)
});
let picker_prev_year = picker.clone();
let picker_next_year = picker.clone();
let labels = [
"一月",
"二月",
"三月",
"四月",
"五月",
"六月",
"七月",
"八月",
"九月",
"十月",
"十一月",
"十二月",
];
panel_shell(&id, &theme)
.child(
div()
.flex()
.items_center()
.justify_between()
.child(nav_button(
format!("{}-prev-year", id),
IconName::ChevronsLeft,
theme.neutral.icon,
picker_prev_year,
|picker, cx| picker.shift_year(-1, cx),
))
.child(
div()
.text_sm()
.font_weight(gpui::FontWeight::BOLD)
.text_color(theme.neutral.text_1)
.child(format!("{} 年", view_year)),
)
.child(nav_button(
format!("{}-next-year", id),
IconName::ChevronsRight,
theme.neutral.icon,
picker_next_year,
|picker, cx| picker.shift_year(1, cx),
)),
)
.child(
div()
.flex()
.flex_col()
.gap_2()
.children(labels.chunks(3).enumerate().map(|(row_idx, row)| {
let id = id.clone();
let row_picker = picker.clone();
let row_theme = theme.clone();
div()
.flex()
.flex_row()
.gap_2()
.children(row.iter().enumerate().map(move |(col_idx, label)| {
let month = (row_idx * 3 + col_idx + 1) as u32;
let value = DateValue {
year: view_year,
month,
day: 1,
};
let is_selected = selected == Some(value)
|| range_start == Some(value)
|| range_end == Some(value);
let in_range = is_between(value, range_start, range_end);
let picker = row_picker.clone();
selectable_cell(
format!("{}-month-{}", id, month),
*label,
is_selected,
in_range,
true,
row_theme.clone(),
picker,
move |picker, window, cx| picker.select_value(value, window, cx),
)
}))
})),
)
.into_any_element()
}
fn render_year_panel(
id: SharedString,
picker: Entity<DatePicker>,
cx: &mut App,
) -> gpui::AnyElement {
let theme = cx.global::<Config>().theme.clone();
let (view_year, selected, range_start, range_end) = picker.update(cx, |picker, _| {
(
picker.view_year,
picker.value,
picker.range_start,
picker.range_end,
)
});
let start_year = view_year.div_euclid(12) * 12;
let picker_prev = picker.clone();
let picker_next = picker.clone();
let years: Vec<i32> = (start_year..start_year + 12).collect();
panel_shell(&id, &theme)
.child(
div()
.flex()
.items_center()
.justify_between()
.child(nav_button(
format!("{}-prev-years", id),
IconName::ChevronsLeft,
theme.neutral.icon,
picker_prev,
|picker, cx| picker.shift_year(-12, cx),
))
.child(
div()
.text_sm()
.font_weight(gpui::FontWeight::BOLD)
.text_color(theme.neutral.text_1)
.child(format!("{} - {}", start_year, start_year + 11)),
)
.child(nav_button(
format!("{}-next-years", id),
IconName::ChevronsRight,
theme.neutral.icon,
picker_next,
|picker, cx| picker.shift_year(12, cx),
)),
)
.child(
div()
.flex()
.flex_col()
.gap_2()
.children(years.chunks(4).enumerate().map(|(row_idx, row)| {
let id = id.clone();
let row_picker = picker.clone();
let row_theme = theme.clone();
div()
.flex()
.flex_row()
.gap_2()
.children(row.iter().enumerate().map(move |(col_idx, year)| {
let value = DateValue {
year: *year,
month: 1,
day: 1,
};
let is_selected = selected == Some(value)
|| range_start == Some(value)
|| range_end == Some(value);
let in_range = is_between(value, range_start, range_end);
let picker = row_picker.clone();
selectable_cell(
format!("{}-year-{}-{}", id, row_idx, col_idx),
year.to_string(),
is_selected,
in_range,
true,
row_theme.clone(),
picker,
move |picker, window, cx| picker.select_value(value, window, cx),
)
}))
})),
)
.into_any_element()
}
fn selectable_cell(
id: impl Into<SharedString>,
label: impl Into<SharedString>,
is_selected: bool,
in_range: bool,
is_current_scope: bool,
theme: liora_theme::Theme,
picker: Entity<DatePicker>,
action: impl Fn(&mut DatePicker, &mut Window, &mut Context<DatePicker>) + 'static,
) -> impl IntoElement {
div()
.id(id.into())
.flex_1()
.h(px(34.0))
.flex()
.items_center()
.justify_center()
.cursor_pointer()
.rounded(px(theme.radius.sm))
.bg(if is_selected {
theme.primary.base
} else if in_range {
theme.primary.light_9
} else {
theme.neutral.card
})
.text_color(if is_selected {
theme.neutral.card
} else if is_current_scope {
theme.neutral.text_1
} else {
theme.neutral.text_3.opacity(0.55)
})
.hover(|s| {
if is_selected {
s.cursor_pointer()
} else {
s.cursor_pointer().bg(theme.neutral.hover)
}
})
.on_mouse_down(MouseButton::Left, move |_, window, cx| {
picker.update(cx, |picker, cx| action(picker, window, cx));
})
.child(div().text_sm().child(label.into()))
}
fn nav_button(
id: impl Into<SharedString>,
icon: IconName,
icon_color: Hsla,
picker: Entity<DatePicker>,
action: impl Fn(&mut DatePicker, &mut Context<DatePicker>) + 'static,
) -> impl IntoElement {
div()
.id(id.into())
.cursor_pointer()
.p_1()
.rounded(px(4.0))
.hover(|s| s.cursor_pointer().bg(gpui::black().opacity(0.04)))
.on_mouse_down(MouseButton::Left, move |_, _, cx| {
picker.update(cx, |picker, cx| action(picker, cx));
})
.child(Icon::new(icon).size(px(18.0)).color(icon_color))
}
struct DatePickerBoundsCapturer {
picker: Entity<DatePicker>,
}
impl IntoElement for DatePickerBoundsCapturer {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for DatePickerBoundsCapturer {
type RequestLayoutState = ();
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
None
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
_id2: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let mut style = gpui::Style::default();
style.size.width = gpui::relative(1.0).into();
style.size.height = gpui::relative(1.0).into();
(window.request_layout(style, [], cx), ())
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_id2: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_rl: &mut Self::RequestLayoutState,
_window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState {
self.picker.update(cx, |picker, _| {
picker.last_bounds = Some(bounds);
});
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_id2: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_rl: &mut Self::RequestLayoutState,
_ps: &mut Self::PrepaintState,
_window: &mut Window,
_cx: &mut App,
) {
}
}
fn default_placeholder(picker_type: DatePickerType) -> &'static str {
match picker_type {
DatePickerType::Date => "请选择日期",
DatePickerType::DateRange => "请选择日期范围",
DatePickerType::Month => "请选择月份",
DatePickerType::MonthRange => "请选择月份范围",
DatePickerType::Year => "请选择年份",
DatePickerType::YearRange => "请选择年份范围",
}
}
fn default_format(picker_type: DatePickerType) -> &'static str {
match picker_type {
DatePickerType::Date | DatePickerType::DateRange => "YYYY-MM-DD",
DatePickerType::Month | DatePickerType::MonthRange => "YYYY-MM",
DatePickerType::Year | DatePickerType::YearRange => "YYYY",
}
}
fn format_date_value(value: DateValue, format: &str) -> String {
format
.replace("YYYY", &format!("{:04}", value.year))
.replace("YY", &format!("{:02}", value.year.rem_euclid(100)))
.replace("MM", &format!("{:02}", value.month))
.replace("M", &value.month.to_string())
.replace("DD", &format!("{:02}", value.day))
.replace("D", &value.day.to_string())
}
fn normalize_value(value: DateValue, picker_type: DatePickerType) -> DateValue {
match picker_type {
DatePickerType::Date | DatePickerType::DateRange => value,
DatePickerType::Month | DatePickerType::MonthRange => DateValue {
year: value.year,
month: value.month,
day: 1,
},
DatePickerType::Year | DatePickerType::YearRange => DateValue {
year: value.year,
month: 1,
day: 1,
},
}
}
fn ordered_pair(a: DateValue, b: DateValue) -> (DateValue, DateValue) {
if a <= b { (a, b) } else { (b, a) }
}
fn is_between(value: DateValue, start: Option<DateValue>, end: Option<DateValue>) -> bool {
matches!((start, end), (Some(start), Some(end)) if value > start && value < end)
}
fn calendar_cells(year: i32, month: u32) -> Vec<DateValue> {
let first_weekday = weekday_monday_based(year, month, 1);
let prev_month_index = year * 12 + month as i32 - 2;
let prev_year = prev_month_index.div_euclid(12);
let prev_month = prev_month_index.rem_euclid(12) as u32 + 1;
let current_days = days_in_month(year, month);
let prev_days = days_in_month(prev_year, prev_month);
let mut cells = Vec::with_capacity(42);
for i in (0..first_weekday).rev() {
cells.push(DateValue {
year: prev_year,
month: prev_month,
day: prev_days - i,
});
}
for day in 1..=current_days {
cells.push(DateValue { year, month, day });
}
let next_month_index = year * 12 + month as i32;
let next_year = next_month_index.div_euclid(12);
let next_month = next_month_index.rem_euclid(12) as u32 + 1;
let mut next_day = 1;
while cells.len() < 42 {
cells.push(DateValue {
year: next_year,
month: next_month,
day: next_day,
});
next_day += 1;
}
cells
}
fn days_in_month(year: i32, month: u32) -> u32 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 if is_leap_year(year) => 29,
2 => 28,
_ => 30,
}
}
fn is_leap_year(year: i32) -> bool {
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}
fn weekday_monday_based(year: i32, month: u32, day: u32) -> u32 {
let mut y = year;
let mut m = month as i32;
if m < 3 {
m += 12;
y -= 1;
}
let k = y % 100;
let j = y / 100;
let h = (day as i32 + (13 * (m + 1)) / 5 + k + k / 4 + j / 4 + 5 * j).rem_euclid(7);
((h + 5) % 7) as u32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn date_picker_width_helpers_set_demo_widths() {
assert_eq!(DatePicker::new().width_md().width, Some(px(260.0)));
assert_eq!(DatePicker::new().width_lg().width, Some(px(320.0)));
}
}