diff --git a/crates/story/src/calendar_story.rs b/crates/story/src/calendar_story.rs index 1f638d59..a07d15a2 100644 --- a/crates/story/src/calendar_story.rs +++ b/crates/story/src/calendar_story.rs @@ -20,11 +20,13 @@ impl CalendarStory { } fn new(cx: &mut ViewContext) -> 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 { diff --git a/crates/ui/src/time/calendar.rs b/crates/ui/src/time/calendar.rs index 03c88d55..5be0e132 100644 --- a/crates/ui/src/time/calendar.rs +++ b/crates/ui/src/time/calendar.rs @@ -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, + mode: Mode, current_year: i32, current_month: u8, + years: Vec>, + year_page: i32, } impl Calendar { - pub fn new(_cx: &mut ViewContext) -> Self { + pub fn new(cx: &mut ViewContext) -> 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::>() + .chunks(20) + .map(|chunk| chunk.to_vec()) + .collect::>(); + 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> { 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) { + if !self.has_prev_year_page() { + return; + } + + self.year_page -= 1; + cx.notify() + } + + fn next_year_page(&mut self, _: &ClickEvent, cx: &mut ViewContext) { + if !self.has_next_year_page() { + return; + } + + self.year_page += 1; + cx.notify() + } + fn prev_month(&mut self, _: &ClickEvent, cx: &mut ViewContext) { 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, + label: impl Into, + active: bool, + muted: bool, + cx: &mut ViewContext, + ) -> 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.mode = mode; + cx.notify(); + } + + fn months(&self) -> Vec { + [ + 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) -> 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) -> 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) -> 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::>(), + ) + } + + fn render_years(&mut self, cx: &mut ViewContext) -> 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::>(), + ) } } @@ -166,57 +418,19 @@ impl EventEmitter for Calendar {} impl Render for Calendar { fn render(&mut self, cx: &mut gpui::ViewContext) -> 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)) + }) } } diff --git a/crates/ui/src/time/date_picker.rs b/crates/ui/src/time/date_picker.rs index f0b17c0e..4ffca55c 100644 --- a/crates/ui/src/time/date_picker.rs +++ b/crates/ui/src/time/date_picker.rs @@ -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 { 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), ) }) }