Add DateRangePicker (#123)
https://github.com/user-attachments/assets/cc934401-78c5-42d6-afc8-a365fea2129b
This commit is contained in:
parent
2c531fa3ef
commit
5d55c5c31e
5 changed files with 413 additions and 118 deletions
|
|
@ -52,7 +52,7 @@ A UI components for building desktop application by using [GPUI](https://gpui.rs
|
|||
- [x] DatePicker
|
||||
- [x] Calendar
|
||||
- [ ] TimePicker
|
||||
- [ ] DateRangePicker
|
||||
- [x] DateRangePicker
|
||||
- [ ] ColorPicker
|
||||
- [x] List
|
||||
- [x] A complex List example.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use chrono::Days;
|
||||
use gpui::{
|
||||
px, IntoElement, ParentElement as _, Render, Styled as _, View, ViewContext,
|
||||
VisualContext as _, WindowContext,
|
||||
|
|
@ -12,6 +13,7 @@ pub struct CalendarStory {
|
|||
date_picker_small: View<DatePicker>,
|
||||
date_picker_large: View<DatePicker>,
|
||||
date_picker_value: Option<String>,
|
||||
date_range_picker: View<DatePicker>,
|
||||
}
|
||||
|
||||
impl CalendarStory {
|
||||
|
|
@ -25,7 +27,7 @@ impl CalendarStory {
|
|||
let mut picker = DatePicker::new("date_picker_medium", cx)
|
||||
.cleanable(true)
|
||||
.width(px(220.));
|
||||
picker.set_date(Some(now), cx);
|
||||
picker.set_date(now, cx);
|
||||
picker
|
||||
});
|
||||
let date_picker_large = cx.new_view(|cx| {
|
||||
|
|
@ -38,13 +40,27 @@ impl CalendarStory {
|
|||
let mut picker = DatePicker::new("date_picker_small", cx)
|
||||
.small()
|
||||
.width(px(180.));
|
||||
picker.set_date(Some(now), cx);
|
||||
picker.set_date(now, cx);
|
||||
picker
|
||||
});
|
||||
let date_range_picker = cx.new_view(|cx| {
|
||||
let mut picker = DatePicker::new("date_range_picker", cx)
|
||||
.width(px(300.))
|
||||
.number_of_months(2)
|
||||
.cleanable(true);
|
||||
picker.set_date((now, now.checked_add_days(Days::new(4)).unwrap()), cx);
|
||||
picker
|
||||
});
|
||||
|
||||
cx.subscribe(&date_picker, |this, _, ev, _| match ev {
|
||||
DatePickerEvent::Change(date) => {
|
||||
this.date_picker_value = date.map(|d| d.to_string());
|
||||
this.date_picker_value = date.format("%Y-%m-%d").map(|s| s.to_string());
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
cx.subscribe(&date_range_picker, |this, _, ev, _| match ev {
|
||||
DatePickerEvent::Change(date) => {
|
||||
this.date_picker_value = date.format("%Y-%m-%d").map(|s| s.to_string());
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
|
@ -53,6 +69,7 @@ impl CalendarStory {
|
|||
date_picker,
|
||||
date_picker_large,
|
||||
date_picker_small,
|
||||
date_range_picker,
|
||||
date_picker_value: None,
|
||||
}
|
||||
}
|
||||
|
|
@ -65,6 +82,7 @@ impl Render for CalendarStory {
|
|||
.child(self.date_picker.clone())
|
||||
.child(self.date_picker_small.clone())
|
||||
.child(self.date_picker_large.clone())
|
||||
.child(self.date_range_picker.clone())
|
||||
.child(format!("Date picker value: {:?}", self.date_picker_value).into_element())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,24 +19,133 @@ use super::utils::days_in_month;
|
|||
|
||||
pub enum CalendarEvent {
|
||||
/// The user selected a date.
|
||||
Selected(NaiveDate),
|
||||
Selected(Date),
|
||||
}
|
||||
|
||||
/// The date of the calendar.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Date {
|
||||
Single(Option<NaiveDate>),
|
||||
Range(Option<NaiveDate>, Option<NaiveDate>),
|
||||
}
|
||||
|
||||
impl From<NaiveDate> for Date {
|
||||
fn from(date: NaiveDate) -> Self {
|
||||
Self::Single(Some(date))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(NaiveDate, NaiveDate)> for Date {
|
||||
fn from((start, end): (NaiveDate, NaiveDate)) -> Self {
|
||||
Self::Range(Some(start), Some(end))
|
||||
}
|
||||
}
|
||||
|
||||
impl Date {
|
||||
fn is_active(&self, v: &NaiveDate) -> bool {
|
||||
let v = *v;
|
||||
match self {
|
||||
Self::Single(d) => Some(v) == *d,
|
||||
Self::Range(start, end) => Some(v) == *start || Some(v) == *end,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_single(&self) -> bool {
|
||||
matches!(self, Self::Single(_))
|
||||
}
|
||||
|
||||
fn is_in_range(&self, v: &NaiveDate) -> bool {
|
||||
let v = *v;
|
||||
match self {
|
||||
Self::Range(start, end) => {
|
||||
if let Some(start) = start {
|
||||
if let Some(end) = end {
|
||||
v >= *start && v <= *end
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_some(&self) -> bool {
|
||||
match self {
|
||||
Self::Single(Some(_)) | Self::Range(Some(_), _) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the date is complete.
|
||||
pub fn is_complete(&self) -> bool {
|
||||
match self {
|
||||
Self::Range(Some(_), Some(_)) => true,
|
||||
Self::Single(Some(_)) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&self) -> Option<NaiveDate> {
|
||||
match self {
|
||||
Self::Single(Some(date)) => Some(*date),
|
||||
Self::Range(Some(start), _) => Some(*start),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end(&self) -> Option<NaiveDate> {
|
||||
match self {
|
||||
Self::Range(_, Some(end)) => Some(*end),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return formatted date string.
|
||||
pub fn format(&self, format: &str) -> Option<SharedString> {
|
||||
match self {
|
||||
Self::Single(Some(date)) => Some(date.format(format).to_string().into()),
|
||||
Self::Range(Some(start), Some(end)) => {
|
||||
Some(format!("{} - {}", start.format(format), end.format(format)).into())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum Mode {
|
||||
enum ViewMode {
|
||||
Day,
|
||||
Month,
|
||||
Year,
|
||||
}
|
||||
|
||||
impl ViewMode {
|
||||
fn is_day(&self) -> bool {
|
||||
matches!(self, Self::Day)
|
||||
}
|
||||
|
||||
fn is_month(&self) -> bool {
|
||||
matches!(self, Self::Month)
|
||||
}
|
||||
|
||||
fn is_year(&self) -> bool {
|
||||
matches!(self, Self::Year)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Calendar {
|
||||
focus_handle: FocusHandle,
|
||||
date: Option<NaiveDate>,
|
||||
mode: Mode,
|
||||
date: Date,
|
||||
view_mode: ViewMode,
|
||||
current_year: i32,
|
||||
current_month: u8,
|
||||
years: Vec<Vec<i32>>,
|
||||
year_page: i32,
|
||||
/// Number of the months view to show.
|
||||
number_of_months: usize,
|
||||
}
|
||||
|
||||
impl Calendar {
|
||||
|
|
@ -44,31 +153,54 @@ impl Calendar {
|
|||
let today = Local::now().naive_local().date();
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
mode: Mode::Day,
|
||||
date: None,
|
||||
view_mode: ViewMode::Day,
|
||||
date: Date::Single(None),
|
||||
current_month: today.month() as u8,
|
||||
current_year: today.year(),
|
||||
years: vec![],
|
||||
year_page: 0,
|
||||
number_of_months: 1,
|
||||
}
|
||||
.year_range((today.year() - 50, today.year() + 50))
|
||||
}
|
||||
|
||||
/// Set the date of the calendar.
|
||||
pub fn set_date(&mut self, date: Option<NaiveDate>, cx: &mut ViewContext<Self>) {
|
||||
self.date = date;
|
||||
if let Some(date) = date {
|
||||
self.current_month = date.month() as u8;
|
||||
self.current_year = date.year();
|
||||
///
|
||||
/// When you set a range date, the mode will be automatically set to `Mode::Range`.
|
||||
pub fn set_date(&mut self, date: impl Into<Date>, cx: &mut ViewContext<Self>) {
|
||||
self.date = date.into();
|
||||
|
||||
match self.date {
|
||||
Date::Single(Some(date)) => {
|
||||
self.current_month = date.month() as u8;
|
||||
self.current_year = date.year();
|
||||
}
|
||||
Date::Range(Some(start), _) => {
|
||||
self.current_month = start.month() as u8;
|
||||
self.current_year = start.year();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
/// Get the date of the calendar.
|
||||
pub fn date(&self) -> Option<NaiveDate> {
|
||||
pub fn date(&self) -> Date {
|
||||
self.date
|
||||
}
|
||||
|
||||
/// Set number of months to show, default is 1.
|
||||
pub fn number_of_months(mut self, number_of_months: usize) -> Self {
|
||||
self.number_of_months = number_of_months;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_number_of_months(&mut self, number_of_months: usize, cx: &mut ViewContext<Self>) {
|
||||
self.number_of_months = number_of_months;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Set the year range of the calendar, default is 50 years before and after the current year.
|
||||
///
|
||||
/// Each year page contains 20 years, so the range will be divided into chunks of 20 years is better.
|
||||
|
|
@ -86,9 +218,29 @@ impl Calendar {
|
|||
self
|
||||
}
|
||||
|
||||
/// Get year and month by offset month.
|
||||
fn offset_year_month(&self, offset_month: usize) -> (i32, u32) {
|
||||
let mut month = self.current_month as i32 + offset_month as i32;
|
||||
let mut year = self.current_year;
|
||||
while month < 1 {
|
||||
month += 12;
|
||||
year -= 1;
|
||||
}
|
||||
while month > 12 {
|
||||
month -= 12;
|
||||
year += 1;
|
||||
}
|
||||
|
||||
(year, month as u32)
|
||||
}
|
||||
|
||||
/// Returns the days of the month in a 2D vector to render on calendar.
|
||||
fn days(&self) -> Vec<Vec<NaiveDate>> {
|
||||
days_in_month(self.current_year, self.current_month as u32)
|
||||
days_in_month(
|
||||
self.current_year,
|
||||
self.current_month as u32,
|
||||
self.number_of_months as u32,
|
||||
)
|
||||
}
|
||||
|
||||
fn has_prev_year_page(&self) -> bool {
|
||||
|
|
@ -145,8 +297,9 @@ impl Calendar {
|
|||
cx.notify()
|
||||
}
|
||||
|
||||
fn month_name(&self) -> SharedString {
|
||||
match self.current_month {
|
||||
fn month_name(&self, offset_month: usize) -> SharedString {
|
||||
let (_, month) = self.offset_year_month(offset_month);
|
||||
match month {
|
||||
1 => t!("Calendar.month.January"),
|
||||
2 => t!("Calendar.month.February"),
|
||||
3 => t!("Calendar.month.March"),
|
||||
|
|
@ -184,6 +337,7 @@ impl Calendar {
|
|||
id: impl Into<ElementId>,
|
||||
label: impl Into<SharedString>,
|
||||
active: bool,
|
||||
secondary_active: bool,
|
||||
muted: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl IntoElement + Styled + StatefulInteractiveElement {
|
||||
|
|
@ -197,6 +351,14 @@ impl Calendar {
|
|||
.when(muted, |this| {
|
||||
this.text_color(cx.theme().muted_foreground.opacity(0.3))
|
||||
})
|
||||
.when(secondary_active, |this| {
|
||||
this.bg(if muted {
|
||||
cx.theme().accent.opacity(0.3)
|
||||
} else {
|
||||
cx.theme().accent
|
||||
})
|
||||
.text_color(cx.theme().accent_foreground)
|
||||
})
|
||||
.when(!active, |this| {
|
||||
this.hover(|this| {
|
||||
this.bg(cx.theme().accent)
|
||||
|
|
@ -210,30 +372,58 @@ impl Calendar {
|
|||
.child(label.into())
|
||||
}
|
||||
|
||||
fn render_item(
|
||||
fn render_day(
|
||||
&self,
|
||||
ix: usize,
|
||||
d: &NaiveDate,
|
||||
offset_month: usize,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
let (_, month) = self.offset_year_month(offset_month);
|
||||
let day = d.day();
|
||||
let is_current_month = d.month() == self.current_month as u32;
|
||||
let is_active = match self.date {
|
||||
Some(date) => date == *d,
|
||||
None => false,
|
||||
};
|
||||
let is_current_month = d.month() == month;
|
||||
let is_active = self.date.is_active(d);
|
||||
let is_in_range = self.date.is_in_range(d);
|
||||
|
||||
let date = *d;
|
||||
|
||||
self.item_button(ix, day.to_string(), is_active, !is_current_month, cx)
|
||||
.on_click(cx.listener(move |view, _: &ClickEvent, cx| {
|
||||
view.set_date(Some(date), cx);
|
||||
cx.emit(CalendarEvent::Selected(date));
|
||||
}))
|
||||
self.item_button(
|
||||
ix,
|
||||
day.to_string(),
|
||||
is_active,
|
||||
is_in_range,
|
||||
!is_current_month,
|
||||
cx,
|
||||
)
|
||||
.on_click(cx.listener(move |view, _: &ClickEvent, cx| {
|
||||
if view.date.is_single() {
|
||||
view.set_date(date, cx);
|
||||
cx.emit(CalendarEvent::Selected(view.date()));
|
||||
} else {
|
||||
let start = view.date.start();
|
||||
let end = view.date.end();
|
||||
|
||||
if start.is_none() && end.is_none() {
|
||||
view.set_date(Date::Range(Some(date), None), cx);
|
||||
} else if start.is_some() && end.is_none() {
|
||||
if date < start.unwrap() {
|
||||
view.set_date(Date::Range(Some(date), None), cx);
|
||||
} else {
|
||||
view.set_date(Date::Range(Some(start.unwrap()), Some(date)), cx);
|
||||
}
|
||||
} else {
|
||||
view.set_date(Date::Range(Some(date), None), cx);
|
||||
}
|
||||
|
||||
if view.date.is_complete() {
|
||||
cx.emit(CalendarEvent::Selected(view.date()));
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn set_mode(&mut self, mode: Mode, cx: &mut ViewContext<Self>) {
|
||||
self.mode = mode;
|
||||
fn set_view_mode(&mut self, mode: ViewMode, cx: &mut ViewContext<Self>) {
|
||||
self.view_mode = mode;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
|
@ -259,7 +449,8 @@ impl Calendar {
|
|||
|
||||
fn render_header(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let current_year = self.current_year;
|
||||
let disabled = self.mode == Mode::Month;
|
||||
let disabled = self.view_mode.is_month();
|
||||
let multiple_months = self.number_of_months > 1;
|
||||
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
|
|
@ -270,58 +461,71 @@ impl Calendar {
|
|||
.icon(IconName::ArrowLeft)
|
||||
.ghost()
|
||||
.disabled(disabled)
|
||||
.when(self.mode == Mode::Day, |this| {
|
||||
.when(self.view_mode.is_day(), |this| {
|
||||
this.on_click(cx.listener(Self::prev_month))
|
||||
})
|
||||
.when(self.mode == Mode::Year, |this| {
|
||||
.when(self.view_mode.is_year(), |this| {
|
||||
this.when(!self.has_prev_year_page(), |this| this.disabled(true))
|
||||
.on_click(cx.listener(Self::prev_year_page))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_center()
|
||||
.gap_3()
|
||||
.child(
|
||||
Button::new("month", cx)
|
||||
.ghost()
|
||||
.label(self.month_name())
|
||||
.selected(self.mode == Mode::Month)
|
||||
.compact()
|
||||
.on_click(cx.listener(|view, _, cx| {
|
||||
if view.mode == Mode::Month {
|
||||
view.set_mode(Mode::Day, cx);
|
||||
} else {
|
||||
view.set_mode(Mode::Month, cx);
|
||||
}
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("year", cx)
|
||||
.ghost()
|
||||
.label(current_year.to_string())
|
||||
.compact()
|
||||
.selected(self.mode == Mode::Year)
|
||||
.on_click(cx.listener(|view, _, cx| {
|
||||
if view.mode == Mode::Year {
|
||||
view.set_mode(Mode::Day, cx);
|
||||
} else {
|
||||
view.set_mode(Mode::Year, cx);
|
||||
}
|
||||
cx.notify();
|
||||
})),
|
||||
),
|
||||
)
|
||||
.when(!multiple_months, |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.justify_center()
|
||||
.gap_3()
|
||||
.child(
|
||||
Button::new("month", cx)
|
||||
.ghost()
|
||||
.label(self.month_name(0))
|
||||
.selected(self.view_mode.is_month())
|
||||
.compact()
|
||||
.on_click(cx.listener(|view, _, cx| {
|
||||
if view.view_mode.is_month() {
|
||||
view.set_view_mode(ViewMode::Day, cx);
|
||||
} else {
|
||||
view.set_view_mode(ViewMode::Month, cx);
|
||||
}
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("year", cx)
|
||||
.ghost()
|
||||
.label(current_year.to_string())
|
||||
.compact()
|
||||
.selected(self.view_mode.is_year())
|
||||
.on_click(cx.listener(|view, _, cx| {
|
||||
if view.view_mode.is_year() {
|
||||
view.set_view_mode(ViewMode::Day, cx);
|
||||
} else {
|
||||
view.set_view_mode(ViewMode::Year, cx);
|
||||
}
|
||||
cx.notify();
|
||||
})),
|
||||
),
|
||||
)
|
||||
})
|
||||
.when(multiple_months, |this| {
|
||||
this.child(h_flex().flex_1().justify_around().children(
|
||||
(0..self.number_of_months).into_iter().map(|n| {
|
||||
h_flex()
|
||||
.justify_center()
|
||||
.gap_3()
|
||||
.child(self.month_name(n))
|
||||
.child(current_year.to_string())
|
||||
}),
|
||||
))
|
||||
})
|
||||
.child(
|
||||
Button::new("next", cx)
|
||||
.icon(IconName::ArrowRight)
|
||||
.ghost()
|
||||
.disabled(disabled)
|
||||
.when(self.mode == Mode::Day, |this| {
|
||||
.when(self.view_mode.is_day(), |this| {
|
||||
this.on_click(cx.listener(Self::next_month))
|
||||
})
|
||||
.when(self.mode == Mode::Year, |this| {
|
||||
.when(self.view_mode.is_year(), |this| {
|
||||
this.when(!self.has_next_year_page(), |this| this.disabled(true))
|
||||
.on_click(cx.listener(Self::next_year_page))
|
||||
}),
|
||||
|
|
@ -339,20 +543,28 @@ impl Calendar {
|
|||
t!("Calendar.week.6"),
|
||||
];
|
||||
|
||||
v_flex()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.justify_between()
|
||||
.children(weeks.iter().map(|week| self.render_week(week.clone(), cx))),
|
||||
)
|
||||
.children(self.days().iter().map(|week| {
|
||||
h_flex().gap_0p5().justify_between().children(
|
||||
week.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, d)| self.render_item(ix, d, cx)),
|
||||
)
|
||||
}))
|
||||
h_flex().gap_4().justify_between().text_sm().children(
|
||||
self.days()
|
||||
.chunks(5)
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(offset_month, days)| {
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex().gap_0p5().justify_between().children(
|
||||
weeks.iter().map(|week| self.render_week(week.clone(), cx)),
|
||||
),
|
||||
)
|
||||
.children(days.iter().map(|week| {
|
||||
h_flex().gap_0p5().justify_between().children(
|
||||
week.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, d)| self.render_day(ix, d, offset_month, cx)),
|
||||
)
|
||||
}))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_months(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
|
|
@ -371,11 +583,11 @@ impl Calendar {
|
|||
.map(|(ix, month)| {
|
||||
let active = (ix + 1) as u8 == self.current_month;
|
||||
|
||||
self.item_button(ix, month.to_string(), active, false, cx)
|
||||
self.item_button(ix, month.to_string(), active, false, false, cx)
|
||||
.w(relative(0.3))
|
||||
.on_click(cx.listener(move |view, _, cx| {
|
||||
view.current_month = (ix + 1) as u8;
|
||||
view.set_mode(Mode::Day, cx);
|
||||
view.set_view_mode(ViewMode::Day, cx);
|
||||
cx.notify();
|
||||
}))
|
||||
})
|
||||
|
|
@ -401,11 +613,11 @@ impl Calendar {
|
|||
let year = *year;
|
||||
let active = year == self.current_year;
|
||||
|
||||
self.item_button(ix, year.to_string(), active, false, cx)
|
||||
self.item_button(ix, year.to_string(), active, false, false, cx)
|
||||
.w(relative(0.2))
|
||||
.on_click(cx.listener(move |view, _, cx| {
|
||||
view.current_year = year;
|
||||
view.set_mode(Mode::Day, cx);
|
||||
view.set_view_mode(ViewMode::Day, cx);
|
||||
cx.notify();
|
||||
}))
|
||||
})
|
||||
|
|
@ -421,16 +633,18 @@ impl Render for Calendar {
|
|||
v_flex()
|
||||
.track_focus(&self.focus_handle)
|
||||
.gap_0p5()
|
||||
.text_sm()
|
||||
.child(self.render_header(cx))
|
||||
.when(self.mode == Mode::Day, |this| {
|
||||
this.child(self.render_days(cx))
|
||||
})
|
||||
.when(self.mode == Mode::Month, |this| {
|
||||
this.child(self.render_months(cx))
|
||||
})
|
||||
.when(self.mode == Mode::Year, |this| {
|
||||
this.child(self.render_years(cx))
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.when(self.view_mode.is_day(), |this| {
|
||||
this.child(self.render_days(cx))
|
||||
})
|
||||
.when(self.view_mode.is_month(), |this| {
|
||||
this.child(self.render_months(cx))
|
||||
})
|
||||
.when(self.view_mode.is_year(), |this| {
|
||||
this.child(self.render_years(cx))
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
use chrono::NaiveDate;
|
||||
use gpui::{
|
||||
deferred, div, prelude::FluentBuilder as _, px, AppContext, ElementId, EventEmitter,
|
||||
FocusHandle, FocusableView, InteractiveElement as _, KeyBinding, Length, MouseButton,
|
||||
|
|
@ -12,7 +11,7 @@ use crate::{
|
|||
theme::ActiveTheme as _, Clickable, Icon, IconName, Sizable, Size, StyledExt as _,
|
||||
};
|
||||
|
||||
use super::calendar::{Calendar, CalendarEvent};
|
||||
use super::calendar::{Calendar, CalendarEvent, Date};
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
let context = Some("DatePicker");
|
||||
|
|
@ -21,13 +20,13 @@ pub fn init(cx: &mut AppContext) {
|
|||
|
||||
#[derive(Clone)]
|
||||
pub enum DatePickerEvent {
|
||||
Change(Option<NaiveDate>),
|
||||
Change(Date),
|
||||
}
|
||||
|
||||
pub struct DatePicker {
|
||||
id: ElementId,
|
||||
focus_handle: FocusHandle,
|
||||
date: Option<NaiveDate>,
|
||||
date: Date,
|
||||
cleanable: bool,
|
||||
placeholder: Option<SharedString>,
|
||||
open: bool,
|
||||
|
|
@ -35,6 +34,7 @@ pub struct DatePicker {
|
|||
width: Length,
|
||||
date_format: SharedString,
|
||||
calendar: View<Calendar>,
|
||||
number_of_months: usize,
|
||||
}
|
||||
|
||||
impl DatePicker {
|
||||
|
|
@ -43,7 +43,7 @@ impl DatePicker {
|
|||
|
||||
cx.subscribe(&calendar, |this, _, ev: &CalendarEvent, cx| match ev {
|
||||
CalendarEvent::Selected(date) => {
|
||||
this.update_date(Some(*date), cx);
|
||||
this.update_date(*date, cx);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
|
@ -51,13 +51,14 @@ impl DatePicker {
|
|||
Self {
|
||||
id: id.into(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
date: None,
|
||||
date: Date::Single(None),
|
||||
calendar,
|
||||
open: false,
|
||||
size: Size::default(),
|
||||
width: Length::Auto,
|
||||
date_format: "%Y/%m/%d".into(),
|
||||
cleanable: false,
|
||||
number_of_months: 1,
|
||||
placeholder: None,
|
||||
}
|
||||
}
|
||||
|
|
@ -86,17 +87,23 @@ impl DatePicker {
|
|||
self
|
||||
}
|
||||
|
||||
/// Set the number of months calendar view to display, default is 1.
|
||||
pub fn number_of_months(mut self, number_of_months: usize) -> Self {
|
||||
self.number_of_months = number_of_months;
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the date of the date picker.
|
||||
pub fn date(&self) -> Option<NaiveDate> {
|
||||
pub fn date(&self) -> Date {
|
||||
self.date
|
||||
}
|
||||
|
||||
/// Set the date of the date picker.
|
||||
pub fn set_date(&mut self, date: Option<NaiveDate>, cx: &mut ViewContext<Self>) {
|
||||
self.update_date(date, cx);
|
||||
pub fn set_date(&mut self, date: impl Into<Date>, cx: &mut ViewContext<Self>) {
|
||||
self.update_date(date.into(), cx);
|
||||
}
|
||||
|
||||
fn update_date(&mut self, date: Option<NaiveDate>, cx: &mut ViewContext<Self>) {
|
||||
fn update_date(&mut self, date: Date, cx: &mut ViewContext<Self>) {
|
||||
self.date = date;
|
||||
self.calendar.update(cx, |view, cx| {
|
||||
view.set_date(date, cx);
|
||||
|
|
@ -112,7 +119,7 @@ impl DatePicker {
|
|||
}
|
||||
|
||||
fn clean(&mut self, _: &gpui::ClickEvent, cx: &mut ViewContext<Self>) {
|
||||
self.update_date(None, cx);
|
||||
self.update_date(Date::Single(None), cx);
|
||||
}
|
||||
|
||||
fn toggle_calendar(&mut self, _: &gpui::ClickEvent, cx: &mut ViewContext<Self>) {
|
||||
|
|
@ -144,8 +151,15 @@ impl Render for DatePicker {
|
|||
.unwrap_or_else(|| t!("DatePicker.placeholder").into());
|
||||
let display_title = self
|
||||
.date
|
||||
.map(|date| date.format(&self.date_format).to_string())
|
||||
.unwrap_or(placeholder.to_string());
|
||||
.format(&self.date_format)
|
||||
.unwrap_or(placeholder.clone());
|
||||
|
||||
self.calendar.update(cx, |view, cx| {
|
||||
view.set_number_of_months(self.number_of_months, cx);
|
||||
});
|
||||
|
||||
let popover_width =
|
||||
285.0 * self.number_of_months as f32 + (self.number_of_months - 1) as f32 * 16.0;
|
||||
|
||||
div()
|
||||
.id(self.id.clone())
|
||||
|
|
@ -208,7 +222,7 @@ impl Render for DatePicker {
|
|||
.overflow_hidden()
|
||||
.rounded_lg()
|
||||
.p_3()
|
||||
.w(px(300.))
|
||||
.w(px(popover_width))
|
||||
.elevation_2(cx)
|
||||
.on_mouse_up_out(
|
||||
MouseButton::Left,
|
||||
|
|
|
|||
|
|
@ -28,19 +28,47 @@ impl NaiveDateExt for chrono::NaiveDate {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn days_in_month(year: i32, month: u32) -> Vec<Vec<NaiveDate>> {
|
||||
pub(crate) fn days_in_month(year: i32, month: u32, number_of_months: u32) -> Vec<Vec<NaiveDate>> {
|
||||
let mut year = year;
|
||||
let mut month = month;
|
||||
if month > 12 {
|
||||
year += 1;
|
||||
month = 1;
|
||||
}
|
||||
if month < 1 {
|
||||
year -= 1;
|
||||
month = 12;
|
||||
}
|
||||
|
||||
let date = NaiveDate::from_ymd_opt(year, month, 1).unwrap();
|
||||
let num_days = date.days_in_month();
|
||||
let start_weekday = date.weekday().num_days_from_sunday();
|
||||
|
||||
let mut total_groups = number_of_months * 5;
|
||||
if total_groups == 0 {
|
||||
total_groups = 5;
|
||||
}
|
||||
|
||||
// Get the days in the month, 2023-02 will returns
|
||||
// "29|30|31| 1| 2| 3| 4",
|
||||
// " 5| 6| 7| 8| 9|10|11",
|
||||
// "12|13|14|15|16|17|18",
|
||||
// "19|20|21|22|23|24|25",
|
||||
// "26|27|28| 1| 2| 3| 4",
|
||||
//
|
||||
// If the number_of_months is 2, then it will return
|
||||
// "29|30|31| 1| 2| 3| 4",
|
||||
// " 5| 6| 7| 8| 9|10|11",
|
||||
// "12|13|14|15|16|17|18",
|
||||
// "19|20|21|22|23|24|25",
|
||||
// "26|27|28| 1| 2| 3| 4",
|
||||
// " 5| 6| 7| 8| 9|10|11",
|
||||
// "12|13|14|15|16|17|18",
|
||||
// "19|20|21|22|23|24|25",
|
||||
// "26|27|28| 1| 2| 3| 4",
|
||||
// " 5| 6| 7| 8| 9|10|11",
|
||||
let mut days = vec![];
|
||||
for n in 0..5 {
|
||||
for n in 0..total_groups as i32 {
|
||||
let mut week_days = vec![];
|
||||
for weekday in 0..7 {
|
||||
let (mut y, mut m) = (year, month);
|
||||
|
|
@ -103,8 +131,8 @@ mod tests {
|
|||
#[test]
|
||||
fn test_days() {
|
||||
#[track_caller]
|
||||
fn assert_case(date: NaiveDate, expected: Vec<&str>) {
|
||||
let out = days_in_month(date.year(), date.month())
|
||||
fn assert_case(date: NaiveDate, number_of_months: u32, expected: Vec<&str>) {
|
||||
let out = days_in_month(date.year(), date.month(), number_of_months)
|
||||
.iter()
|
||||
.map(|week| {
|
||||
week.iter()
|
||||
|
|
@ -127,6 +155,7 @@ mod tests {
|
|||
|
||||
assert_case(
|
||||
NaiveDate::from_ymd_opt(2024, 8, 1).unwrap(),
|
||||
1,
|
||||
vec![
|
||||
"7-28|7-29|7-30|7-31| 1| 2| 3",
|
||||
" 4| 5| 6| 7| 8| 9|10",
|
||||
|
|
@ -137,6 +166,7 @@ mod tests {
|
|||
);
|
||||
assert_case(
|
||||
NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(),
|
||||
1,
|
||||
vec![
|
||||
"2024-12-29|2024-12-30|2024-12-31| 1| 2| 3| 4",
|
||||
" 5| 6| 7| 8| 9|10|11",
|
||||
|
|
@ -148,6 +178,7 @@ mod tests {
|
|||
|
||||
assert_case(
|
||||
NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
|
||||
1,
|
||||
vec![
|
||||
"1-28|1-29|1-30|1-31| 1| 2| 3",
|
||||
" 4| 5| 6| 7| 8| 9|10",
|
||||
|
|
@ -158,6 +189,7 @@ mod tests {
|
|||
);
|
||||
assert_case(
|
||||
NaiveDate::from_ymd_opt(2023, 2, 20).unwrap(),
|
||||
1,
|
||||
vec![
|
||||
"1-29|1-30|1-31| 1| 2| 3| 4",
|
||||
" 5| 6| 7| 8| 9|10|11",
|
||||
|
|
@ -166,5 +198,22 @@ mod tests {
|
|||
"26|27|28|3-1|3-2|3-3|3-4",
|
||||
],
|
||||
);
|
||||
|
||||
assert_case(
|
||||
NaiveDate::from_ymd_opt(2023, 2, 20).unwrap(),
|
||||
2,
|
||||
vec![
|
||||
"1-29|1-30|1-31| 1| 2| 3| 4",
|
||||
" 5| 6| 7| 8| 9|10|11",
|
||||
"12|13|14|15|16|17|18",
|
||||
"19|20|21|22|23|24|25",
|
||||
"26|27|28|3-1|3-2|3-3|3-4",
|
||||
" 5| 6| 7| 8| 9|10|11",
|
||||
"12|13|14|15|16|17|18",
|
||||
"19|20|21|22|23|24|25",
|
||||
"26|27|28|4-1|4-2|4-3|4-4",
|
||||
" 5| 6| 7| 8| 9|10|11",
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue