datepicker: Add Year, Month select mode. (#119)
https://github.com/user-attachments/assets/b6232d8e-0832-449d-a982-49e015232cfa
This commit is contained in:
parent
ebdcf244e2
commit
27d094c590
3 changed files with 305 additions and 93 deletions
|
|
@ -20,11 +20,13 @@ impl CalendarStory {
|
|||
}
|
||||
|
||||
fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
let now = chrono::Local::now().naive_local().date();
|
||||
let date_picker = cx.new_view(|cx| {
|
||||
DatePicker::new("date_picker_medium", cx)
|
||||
.default_now()
|
||||
let mut picker = DatePicker::new("date_picker_medium", cx)
|
||||
.cleanable(true)
|
||||
.width(px(220.))
|
||||
.width(px(220.));
|
||||
picker.set_date(Some(now), cx);
|
||||
picker
|
||||
});
|
||||
let date_picker_large = cx.new_view(|cx| {
|
||||
DatePicker::new("date_picker_large", cx)
|
||||
|
|
@ -33,10 +35,11 @@ impl CalendarStory {
|
|||
.width(px(300.))
|
||||
});
|
||||
let date_picker_small = cx.new_view(|cx| {
|
||||
DatePicker::new("date_picker_small", cx)
|
||||
.default_now()
|
||||
let mut picker = DatePicker::new("date_picker_small", cx)
|
||||
.small()
|
||||
.width(px(180.))
|
||||
.width(px(180.));
|
||||
picker.set_date(Some(now), cx);
|
||||
picker
|
||||
});
|
||||
|
||||
cx.subscribe(&date_picker, |this, _, ev, _| match ev {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@ use std::borrow::Cow;
|
|||
|
||||
use chrono::{Datelike, Local, NaiveDate};
|
||||
use gpui::{
|
||||
prelude::FluentBuilder as _, ClickEvent, EventEmitter, InteractiveElement as _, IntoElement,
|
||||
ParentElement as _, Render, SharedString, StatefulInteractiveElement, Styled as _, ViewContext,
|
||||
prelude::FluentBuilder as _, relative, ClickEvent, ElementId, EventEmitter, FocusHandle,
|
||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString,
|
||||
StatefulInteractiveElement, Styled, ViewContext,
|
||||
};
|
||||
use rust_i18n::t;
|
||||
|
||||
|
|
@ -11,7 +12,7 @@ use crate::{
|
|||
button::Button,
|
||||
h_flex,
|
||||
theme::{ActiveTheme, Colorize},
|
||||
v_flex, Clickable, IconName, StyledExt,
|
||||
v_flex, Clickable, Disableable, IconName, Selectable,
|
||||
};
|
||||
|
||||
use super::utils::days_in_month;
|
||||
|
|
@ -21,20 +22,36 @@ pub enum CalendarEvent {
|
|||
Selected(NaiveDate),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum Mode {
|
||||
Day,
|
||||
Month,
|
||||
Year,
|
||||
}
|
||||
|
||||
pub struct Calendar {
|
||||
focus_handle: FocusHandle,
|
||||
date: Option<NaiveDate>,
|
||||
mode: Mode,
|
||||
current_year: i32,
|
||||
current_month: u8,
|
||||
years: Vec<Vec<i32>>,
|
||||
year_page: i32,
|
||||
}
|
||||
|
||||
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();
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
mode: Mode::Day,
|
||||
date: None,
|
||||
current_month: today.month() as u8,
|
||||
current_year: today.year(),
|
||||
years: vec![],
|
||||
year_page: 0,
|
||||
}
|
||||
.year_range((today.year() - 50, today.year() + 50))
|
||||
}
|
||||
|
||||
/// Set the date of the calendar.
|
||||
|
|
@ -52,11 +69,54 @@ impl Calendar {
|
|||
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.
|
||||
fn days(&self) -> Vec<Vec<NaiveDate>> {
|
||||
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>) {
|
||||
self.current_month = if self.current_month == 1 {
|
||||
12
|
||||
|
|
@ -119,6 +179,37 @@ impl Calendar {
|
|||
.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(
|
||||
&self,
|
||||
ix: usize,
|
||||
|
|
@ -134,31 +225,192 @@ impl Calendar {
|
|||
|
||||
let date = *d;
|
||||
|
||||
h_flex()
|
||||
.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)
|
||||
})
|
||||
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))
|
||||
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 {
|
||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl gpui::IntoElement {
|
||||
let weeks: [SharedString; 7] = [
|
||||
t!("Calendar.week.0").into(),
|
||||
t!("Calendar.week.1").into(),
|
||||
t!("Calendar.week.2").into(),
|
||||
t!("Calendar.week.3").into(),
|
||||
t!("Calendar.week.4").into(),
|
||||
t!("Calendar.week.5").into(),
|
||||
t!("Calendar.week.6").into(),
|
||||
];
|
||||
|
||||
v_flex().gap_0p5().text_sm().child(
|
||||
v_flex()
|
||||
.child(
|
||||
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)),
|
||||
)
|
||||
})),
|
||||
)
|
||||
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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use chrono::{Local, NaiveDate};
|
||||
use chrono::NaiveDate;
|
||||
use gpui::{
|
||||
deferred, div, prelude::FluentBuilder as _, px, AppContext, ElementId, EventEmitter,
|
||||
FocusHandle, FocusableView, InteractiveElement as _, KeyBinding, Length, MouseButton,
|
||||
|
|
@ -86,12 +86,6 @@ impl DatePicker {
|
|||
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.
|
||||
pub fn date(&self) -> Option<NaiveDate> {
|
||||
self.date
|
||||
|
|
@ -208,8 +202,9 @@ impl Render for DatePicker {
|
|||
deferred(
|
||||
div()
|
||||
.track_focus(&self.focus_handle)
|
||||
.occlude()
|
||||
.absolute()
|
||||
.mt_1()
|
||||
.mt_2()
|
||||
.overflow_hidden()
|
||||
.rounded_lg()
|
||||
.p_3()
|
||||
|
|
@ -221,7 +216,7 @@ impl Render for DatePicker {
|
|||
)
|
||||
.child(self.calendar.clone()),
|
||||
)
|
||||
.with_priority(1),
|
||||
.with_priority(2),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue