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