datepicker: Add Year, Month select mode. (#119)

https://github.com/user-attachments/assets/b6232d8e-0832-449d-a982-49e015232cfa
This commit is contained in:
Jason Lee 2024-08-07 23:09:42 +08:00 committed by GitHub
parent ebdcf244e2
commit 27d094c590
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 305 additions and 93 deletions

View file

@ -20,11 +20,13 @@ impl CalendarStory {
} }
fn new(cx: &mut ViewContext<Self>) -> Self { fn new(cx: &mut ViewContext<Self>) -> Self {
let now = chrono::Local::now().naive_local().date();
let date_picker = cx.new_view(|cx| { let date_picker = cx.new_view(|cx| {
DatePicker::new("date_picker_medium", cx) let mut picker = DatePicker::new("date_picker_medium", cx)
.default_now()
.cleanable(true) .cleanable(true)
.width(px(220.)) .width(px(220.));
picker.set_date(Some(now), cx);
picker
}); });
let date_picker_large = cx.new_view(|cx| { let date_picker_large = cx.new_view(|cx| {
DatePicker::new("date_picker_large", cx) DatePicker::new("date_picker_large", cx)
@ -33,10 +35,11 @@ impl CalendarStory {
.width(px(300.)) .width(px(300.))
}); });
let date_picker_small = cx.new_view(|cx| { let date_picker_small = cx.new_view(|cx| {
DatePicker::new("date_picker_small", cx) let mut picker = DatePicker::new("date_picker_small", cx)
.default_now()
.small() .small()
.width(px(180.)) .width(px(180.));
picker.set_date(Some(now), cx);
picker
}); });
cx.subscribe(&date_picker, |this, _, ev, _| match ev { cx.subscribe(&date_picker, |this, _, ev, _| match ev {

View file

@ -2,8 +2,9 @@ use std::borrow::Cow;
use chrono::{Datelike, Local, NaiveDate}; use chrono::{Datelike, Local, NaiveDate};
use gpui::{ use gpui::{
prelude::FluentBuilder as _, ClickEvent, EventEmitter, InteractiveElement as _, IntoElement, prelude::FluentBuilder as _, relative, ClickEvent, ElementId, EventEmitter, FocusHandle,
ParentElement as _, Render, SharedString, StatefulInteractiveElement, Styled as _, ViewContext, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
StatefulInteractiveElement, Styled, ViewContext,
}; };
use rust_i18n::t; use rust_i18n::t;
@ -11,7 +12,7 @@ use crate::{
button::Button, button::Button,
h_flex, h_flex,
theme::{ActiveTheme, Colorize}, theme::{ActiveTheme, Colorize},
v_flex, Clickable, IconName, StyledExt, v_flex, Clickable, Disableable, IconName, Selectable,
}; };
use super::utils::days_in_month; use super::utils::days_in_month;
@ -21,20 +22,36 @@ pub enum CalendarEvent {
Selected(NaiveDate), Selected(NaiveDate),
} }
#[derive(Debug, PartialEq, Eq)]
enum Mode {
Day,
Month,
Year,
}
pub struct Calendar { pub struct Calendar {
focus_handle: FocusHandle,
date: Option<NaiveDate>, date: Option<NaiveDate>,
mode: Mode,
current_year: i32, current_year: i32,
current_month: u8, current_month: u8,
years: Vec<Vec<i32>>,
year_page: i32,
} }
impl Calendar { impl Calendar {
pub fn new(_cx: &mut ViewContext<Self>) -> Self { pub fn new(cx: &mut ViewContext<Self>) -> Self {
let today = Local::now().naive_local().date(); let today = Local::now().naive_local().date();
Self { Self {
focus_handle: cx.focus_handle(),
mode: Mode::Day,
date: None, date: None,
current_month: today.month() as u8, current_month: today.month() as u8,
current_year: today.year(), current_year: today.year(),
years: vec![],
year_page: 0,
} }
.year_range((today.year() - 50, today.year() + 50))
} }
/// Set the date of the calendar. /// Set the date of the calendar.
@ -52,11 +69,54 @@ impl Calendar {
self.date self.date
} }
/// 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.
pub fn year_range(mut self, range: (i32, i32)) -> Self {
self.years = (range.0..range.1)
.collect::<Vec<_>>()
.chunks(20)
.map(|chunk| chunk.to_vec())
.collect::<Vec<_>>();
self.year_page = self
.years
.iter()
.position(|years| years.contains(&self.current_year))
.unwrap_or(0) as i32;
self
}
/// 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)
} }
fn has_prev_year_page(&self) -> bool {
self.year_page > 0
}
fn has_next_year_page(&self) -> bool {
self.year_page < self.years.len() as i32 - 1
}
fn prev_year_page(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
if !self.has_prev_year_page() {
return;
}
self.year_page -= 1;
cx.notify()
}
fn next_year_page(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
if !self.has_next_year_page() {
return;
}
self.year_page += 1;
cx.notify()
}
fn prev_month(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) { fn prev_month(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
self.current_month = if self.current_month == 1 { self.current_month = if self.current_month == 1 {
12 12
@ -119,6 +179,37 @@ impl Calendar {
.child(week.into()) .child(week.into())
} }
fn item_button(
&self,
id: impl Into<ElementId>,
label: impl Into<SharedString>,
active: bool,
muted: bool,
cx: &mut ViewContext<Self>,
) -> impl IntoElement + Styled + StatefulInteractiveElement {
h_flex()
.id(id.into())
.w_9()
.h_9()
.rounded_lg()
.justify_center()
.cursor_pointer()
.when(muted, |this| {
this.text_color(cx.theme().muted_foreground.opacity(0.3))
})
.when(!active, |this| {
this.hover(|this| {
this.bg(cx.theme().accent)
.text_color(cx.theme().accent_foreground)
})
})
.when(active, |this| {
this.bg(cx.theme().primary)
.text_color(cx.theme().primary_foreground)
})
.child(label.into())
}
fn render_item( fn render_item(
&self, &self,
ix: usize, ix: usize,
@ -134,31 +225,192 @@ impl Calendar {
let date = *d; let date = *d;
h_flex() self.item_button(ix, day.to_string(), is_active, !is_current_month, cx)
.id(ix)
.w_9()
.h_9()
.rounded_lg()
.justify_center()
.cursor_pointer()
.when(!is_current_month, |this| {
this.text_color(cx.theme().muted_foreground.opacity(0.3))
})
.when(!is_active, |this| {
this.hover(|this| {
this.bg(cx.theme().accent)
.text_color(cx.theme().accent_foreground)
})
})
.when(is_active, |this| {
this.bg(cx.theme().primary)
.text_color(cx.theme().primary_foreground)
})
.on_click(cx.listener(move |view, _: &ClickEvent, cx| { .on_click(cx.listener(move |view, _: &ClickEvent, cx| {
view.set_date(Some(date), cx); view.set_date(Some(date), cx);
cx.emit(CalendarEvent::Selected(date)) cx.emit(CalendarEvent::Selected(date));
})) }))
.child(day.to_string()) }
fn set_mode(&mut self, mode: Mode, cx: &mut ViewContext<Self>) {
self.mode = mode;
cx.notify();
}
fn months(&self) -> Vec<SharedString> {
[
t!("Calendar.month.January"),
t!("Calendar.month.February"),
t!("Calendar.month.March"),
t!("Calendar.month.April"),
t!("Calendar.month.May"),
t!("Calendar.month.June"),
t!("Calendar.month.July"),
t!("Calendar.month.August"),
t!("Calendar.month.September"),
t!("Calendar.month.October"),
t!("Calendar.month.November"),
t!("Calendar.month.December"),
]
.iter()
.map(|s| s.clone().into())
.collect()
}
fn render_header(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let current_year = self.current_year;
let disabled = self.mode == Mode::Month;
h_flex()
.gap_0p5()
.justify_between()
.items_center()
.child(
Button::new("prev", cx)
.icon(IconName::ArrowLeft)
.ghost()
.disabled(disabled)
.when(self.mode == Mode::Day, |this| {
this.on_click(cx.listener(Self::prev_month))
})
.when(self.mode == Mode::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();
})),
),
)
.child(
Button::new("next", cx)
.icon(IconName::ArrowRight)
.ghost()
.disabled(disabled)
.when(self.mode == Mode::Day, |this| {
this.on_click(cx.listener(Self::next_month))
})
.when(self.mode == Mode::Year, |this| {
this.when(!self.has_next_year_page(), |this| this.disabled(true))
.on_click(cx.listener(Self::next_year_page))
}),
)
}
fn render_days(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let weeks = [
t!("Calendar.week.0"),
t!("Calendar.week.1"),
t!("Calendar.week.2"),
t!("Calendar.week.3"),
t!("Calendar.week.4"),
t!("Calendar.week.5"),
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)),
)
}))
}
fn render_months(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let months = self.months();
h_flex()
.mt_3()
.gap_0p5()
.gap_y_3()
.justify_between()
.flex_wrap()
.children(
months
.iter()
.enumerate()
.map(|(ix, month)| {
let active = (ix + 1) as u8 == self.current_month;
self.item_button(ix, month.to_string(), active, 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);
cx.notify();
}))
})
.collect::<Vec<_>>(),
)
}
fn render_years(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let current_page_years = &self.years[self.year_page as usize];
h_flex()
.id("years")
.mt_3()
.gap_0p5()
.gap_y_3()
.justify_between()
.flex_wrap()
.children(
current_page_years
.iter()
.enumerate()
.map(|(ix, year)| {
let year = *year;
let active = year == self.current_year;
self.item_button(ix, year.to_string(), active, false, cx)
.w(relative(0.2))
.on_click(cx.listener(move |view, _, cx| {
view.current_year = year;
view.set_mode(Mode::Day, cx);
cx.notify();
}))
})
.collect::<Vec<_>>(),
)
} }
} }
@ -166,57 +418,19 @@ impl EventEmitter<CalendarEvent> for Calendar {}
impl Render for Calendar { impl Render for Calendar {
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl gpui::IntoElement { fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl gpui::IntoElement {
let weeks: [SharedString; 7] = [ v_flex()
t!("Calendar.week.0").into(), .track_focus(&self.focus_handle)
t!("Calendar.week.1").into(), .gap_0p5()
t!("Calendar.week.2").into(), .text_sm()
t!("Calendar.week.3").into(), .child(self.render_header(cx))
t!("Calendar.week.4").into(), .when(self.mode == Mode::Day, |this| {
t!("Calendar.week.5").into(), this.child(self.render_days(cx))
t!("Calendar.week.6").into(), })
]; .when(self.mode == Mode::Month, |this| {
this.child(self.render_months(cx))
v_flex().gap_0p5().text_sm().child( })
v_flex() .when(self.mode == Mode::Year, |this| {
.child( this.child(self.render_years(cx))
h_flex() })
.gap_0p5()
.justify_between()
.items_center()
.child(
Button::new("prev", cx)
.icon(IconName::ArrowLeft)
.ghost()
.on_click(cx.listener(Self::prev_month)),
)
.child(
h_flex()
.justify_center()
.font_semibold()
.gap_3()
// TODO: Add menu to select month and year
.child(self.current_year.to_string())
.child(self.month_name()),
)
.child(
Button::new("next", cx)
.icon(IconName::ArrowRight)
.ghost()
.on_click(cx.listener(Self::next_month)),
),
)
.child(
h_flex()
.gap_0p5()
.children(weeks.iter().map(|week| self.render_week(week.clone(), cx))),
)
.children(self.days().iter().map(|week| {
h_flex().gap_0p5().children(
week.iter()
.enumerate()
.map(|(ix, d)| self.render_item(ix, d, cx)),
)
})),
)
} }
} }

View file

@ -1,4 +1,4 @@
use chrono::{Local, NaiveDate}; 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,
@ -86,12 +86,6 @@ impl DatePicker {
self self
} }
/// Set the default date of the date picker.
pub fn default_now(mut self) -> Self {
self.date = Some(Local::now().naive_local().date());
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) -> Option<NaiveDate> {
self.date self.date
@ -208,8 +202,9 @@ impl Render for DatePicker {
deferred( deferred(
div() div()
.track_focus(&self.focus_handle) .track_focus(&self.focus_handle)
.occlude()
.absolute() .absolute()
.mt_1() .mt_2()
.overflow_hidden() .overflow_hidden()
.rounded_lg() .rounded_lg()
.p_3() .p_3()
@ -221,7 +216,7 @@ impl Render for DatePicker {
) )
.child(self.calendar.clone()), .child(self.calendar.clone()),
) )
.with_priority(1), .with_priority(2),
) )
}) })
} }