gpui_component/time/
calendar.rs

1use std::{borrow::Cow, rc::Rc};
2
3use chrono::{Datelike, Local, NaiveDate};
4use gpui::{
5    prelude::FluentBuilder as _, px, relative, App, ClickEvent, Context, ElementId, Empty, Entity,
6    EventEmitter, FocusHandle, InteractiveElement, IntoElement, ParentElement, Render, RenderOnce,
7    SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Window,
8};
9use rust_i18n::t;
10
11use crate::{
12    button::{Button, ButtonVariants as _},
13    h_flex, v_flex, ActiveTheme, Disableable as _, IconName, Selectable, Sizable, Size,
14    StyledExt as _,
15};
16
17use super::utils::days_in_month;
18
19pub enum CalendarEvent {
20    /// The user selected a date.
21    Selected(Date),
22}
23
24/// The date of the calendar.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum Date {
27    Single(Option<NaiveDate>),
28    Range(Option<NaiveDate>, Option<NaiveDate>),
29}
30
31impl std::fmt::Display for Date {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        match self {
34            Self::Single(Some(date)) => write!(f, "{}", date),
35            Self::Single(None) => write!(f, "nil"),
36            Self::Range(Some(start), Some(end)) => write!(f, "{} - {}", start, end),
37            Self::Range(None, None) => write!(f, "nil"),
38            Self::Range(Some(start), None) => write!(f, "{} - nil", start),
39            Self::Range(None, Some(end)) => write!(f, "nil - {}", end),
40        }
41    }
42}
43
44impl From<NaiveDate> for Date {
45    fn from(date: NaiveDate) -> Self {
46        Self::Single(Some(date))
47    }
48}
49
50impl From<(NaiveDate, NaiveDate)> for Date {
51    fn from((start, end): (NaiveDate, NaiveDate)) -> Self {
52        Self::Range(Some(start), Some(end))
53    }
54}
55
56impl Date {
57    fn is_active(&self, v: &NaiveDate) -> bool {
58        let v = *v;
59        match self {
60            Self::Single(d) => Some(v) == *d,
61            Self::Range(start, end) => Some(v) == *start || Some(v) == *end,
62        }
63    }
64
65    fn is_single(&self) -> bool {
66        matches!(self, Self::Single(_))
67    }
68
69    fn is_in_range(&self, v: &NaiveDate) -> bool {
70        let v = *v;
71        match self {
72            Self::Range(start, end) => {
73                if let Some(start) = start {
74                    if let Some(end) = end {
75                        v >= *start && v <= *end
76                    } else {
77                        false
78                    }
79                } else {
80                    false
81                }
82            }
83            _ => false,
84        }
85    }
86
87    pub fn is_some(&self) -> bool {
88        match self {
89            Self::Single(Some(_)) | Self::Range(Some(_), _) => true,
90            _ => false,
91        }
92    }
93
94    /// Check if the date is complete.
95    pub fn is_complete(&self) -> bool {
96        match self {
97            Self::Range(Some(_), Some(_)) => true,
98            Self::Single(Some(_)) => true,
99            _ => false,
100        }
101    }
102
103    pub fn start(&self) -> Option<NaiveDate> {
104        match self {
105            Self::Single(Some(date)) => Some(*date),
106            Self::Range(Some(start), _) => Some(*start),
107            _ => None,
108        }
109    }
110
111    pub fn end(&self) -> Option<NaiveDate> {
112        match self {
113            Self::Range(_, Some(end)) => Some(*end),
114            _ => None,
115        }
116    }
117
118    /// Return formatted date string.
119    pub fn format(&self, format: &str) -> Option<SharedString> {
120        match self {
121            Self::Single(Some(date)) => Some(date.format(format).to_string().into()),
122            Self::Range(Some(start), Some(end)) => {
123                Some(format!("{} - {}", start.format(format), end.format(format)).into())
124            }
125            _ => None,
126        }
127    }
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131enum ViewMode {
132    Day,
133    Month,
134    Year,
135}
136
137impl ViewMode {
138    fn is_day(&self) -> bool {
139        matches!(self, Self::Day)
140    }
141
142    fn is_month(&self) -> bool {
143        matches!(self, Self::Month)
144    }
145
146    fn is_year(&self) -> bool {
147        matches!(self, Self::Year)
148    }
149}
150
151pub struct IntervalMatcher {
152    before: Option<NaiveDate>,
153    after: Option<NaiveDate>,
154}
155
156pub struct RangeMatcher {
157    from: Option<NaiveDate>,
158    to: Option<NaiveDate>,
159}
160
161pub enum Matcher {
162    /// Match declare days of the week.
163    ///
164    /// Matcher::DayOfWeek(vec![0, 6])
165    /// Will match the days of the week that are Sunday and Saturday.
166    DayOfWeek(Vec<u32>),
167    /// Match the included days, except for those before and after the interval.
168    ///
169    /// Matcher::Interval(IntervalMatcher {
170    ///   before: Some(NaiveDate::from_ymd(2020, 1, 2)),
171    ///   after: Some(NaiveDate::from_ymd(2020, 1, 3)),
172    /// })
173    /// Will match the days that are not between 2020-01-02 and 2020-01-03.
174    Interval(IntervalMatcher),
175    /// Match the days within the range.
176    ///
177    /// Matcher::Range(RangeMatcher {
178    ///   from: Some(NaiveDate::from_ymd(2020, 1, 1)),
179    ///   to: Some(NaiveDate::from_ymd(2020, 1, 3)),
180    /// })
181    /// Will match the days that are between 2020-01-01 and 2020-01-03.
182    Range(RangeMatcher),
183    /// Match dates using a custom function.
184    ///
185    /// let matcher = Matcher::Custom(Box::new(|date: &NaiveDate| {
186    ///     date.day0() < 5
187    /// }));
188    /// Will match first 5 days of each month
189    Custom(Box<dyn Fn(&NaiveDate) -> bool + Send + Sync>),
190}
191
192impl From<Vec<u32>> for Matcher {
193    fn from(days: Vec<u32>) -> Self {
194        Matcher::DayOfWeek(days)
195    }
196}
197
198impl<F> From<F> for Matcher
199where
200    F: Fn(&NaiveDate) -> bool + Send + Sync + 'static,
201{
202    fn from(f: F) -> Self {
203        Matcher::Custom(Box::new(f))
204    }
205}
206
207impl Matcher {
208    pub fn interval(before: Option<NaiveDate>, after: Option<NaiveDate>) -> Self {
209        Matcher::Interval(IntervalMatcher { before, after })
210    }
211
212    pub fn range(from: Option<NaiveDate>, to: Option<NaiveDate>) -> Self {
213        Matcher::Range(RangeMatcher { from, to })
214    }
215
216    fn matched(&self, date: &NaiveDate) -> bool {
217        match self {
218            Matcher::DayOfWeek(days) => days.contains(&date.weekday().num_days_from_sunday()),
219            Matcher::Interval(interval) => {
220                let before_check = interval.before.map_or(false, |before| date < &before);
221                let after_check = interval.after.map_or(false, |after| date > &after);
222                before_check || after_check
223            }
224            Matcher::Range(range) => {
225                let from_check = range.from.map_or(false, |from| date < &from);
226                let to_check = range.to.map_or(false, |to| date > &to);
227                !from_check && !to_check
228            }
229            Matcher::Custom(f) => f(date),
230        }
231    }
232
233    pub fn date_matched(&self, date: &Date) -> bool {
234        match date {
235            Date::Single(Some(date)) => self.matched(date),
236            Date::Range(Some(start), Some(end)) => self.matched(start) || self.matched(end),
237            _ => false,
238        }
239    }
240
241    pub fn custom<F>(f: F) -> Self
242    where
243        F: Fn(&NaiveDate) -> bool + Send + Sync + 'static,
244    {
245        Matcher::Custom(Box::new(f))
246    }
247}
248
249#[derive(IntoElement)]
250pub struct Calendar {
251    id: ElementId,
252    size: Size,
253    state: Entity<CalendarState>,
254    style: StyleRefinement,
255    /// Number of the months view to show.
256    number_of_months: usize,
257}
258
259/// Use to store the state of the calendar.
260pub struct CalendarState {
261    focus_handle: FocusHandle,
262    view_mode: ViewMode,
263    date: Date,
264    current_year: i32,
265    current_month: u8,
266    years: Vec<Vec<i32>>,
267    year_page: i32,
268    today: NaiveDate,
269    /// Number of the months view to show.
270    number_of_months: usize,
271    pub(crate) disabled_matcher: Option<Rc<Matcher>>,
272}
273
274impl CalendarState {
275    pub fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
276        let today = Local::now().naive_local().date();
277        Self {
278            focus_handle: cx.focus_handle(),
279            view_mode: ViewMode::Day,
280            date: Date::Single(None),
281            current_month: today.month() as u8,
282            current_year: today.year(),
283            years: vec![],
284            year_page: 0,
285            today,
286            number_of_months: 1,
287            disabled_matcher: None,
288        }
289        .year_range((today.year() - 50, today.year() + 50))
290    }
291
292    /// Set the disabled matcher of the calendar state.
293    pub fn disabled_matcher(mut self, matcher: impl Into<Matcher>) -> Self {
294        self.disabled_matcher = Some(Rc::new(matcher.into()));
295        self
296    }
297
298    /// Set the disabled matcher of the calendar.
299    ///
300    /// The disabled matcher will be used to disable the days that match the matcher.
301    pub fn set_disabled_matcher(
302        &mut self,
303        disabled: impl Into<Matcher>,
304        _: &mut Window,
305        _: &mut Context<Self>,
306    ) {
307        self.disabled_matcher = Some(Rc::new(disabled.into()));
308    }
309
310    /// Set the date of the calendar.
311    ///
312    /// When you set a range date, the mode will be automatically set to `Mode::Range`.
313    pub fn set_date(&mut self, date: impl Into<Date>, _: &mut Window, cx: &mut Context<Self>) {
314        let date = date.into();
315
316        let invalid = self
317            .disabled_matcher
318            .as_ref()
319            .map_or(false, |matcher| matcher.date_matched(&date));
320
321        if invalid {
322            return;
323        }
324
325        self.date = date;
326        match self.date {
327            Date::Single(Some(date)) => {
328                self.current_month = date.month() as u8;
329                self.current_year = date.year();
330            }
331            Date::Range(Some(start), _) => {
332                self.current_month = start.month() as u8;
333                self.current_year = start.year();
334            }
335            _ => {}
336        }
337
338        cx.notify()
339    }
340
341    /// Get the date of the calendar.
342    pub fn date(&self) -> Date {
343        self.date
344    }
345
346    // pub fn set_size(&mut self, size: Size, _: &mut Window, cx: &mut Context<Self>) {
347    //     self.size = size;
348    //     cx.notify();
349    // }
350
351    pub fn set_number_of_months(
352        &mut self,
353        number_of_months: usize,
354        _: &mut Window,
355        cx: &mut Context<Self>,
356    ) {
357        self.number_of_months = number_of_months;
358        cx.notify();
359    }
360
361    /// Set the year range of the calendar, default is 50 years before and after the current year.
362    ///
363    /// Each year page contains 20 years, so the range will be divided into chunks of 20 years is better.
364    pub fn year_range(mut self, range: (i32, i32)) -> Self {
365        self.years = (range.0..range.1)
366            .collect::<Vec<_>>()
367            .chunks(20)
368            .map(|chunk| chunk.to_vec())
369            .collect::<Vec<_>>();
370        self.year_page = self
371            .years
372            .iter()
373            .position(|years| years.contains(&self.current_year))
374            .unwrap_or(0) as i32;
375        self
376    }
377
378    /// Get year and month by offset month.
379    fn offset_year_month(&self, offset_month: usize) -> (i32, u32) {
380        let mut month = self.current_month as i32 + offset_month as i32;
381        let mut year = self.current_year;
382        while month < 1 {
383            month += 12;
384            year -= 1;
385        }
386        while month > 12 {
387            month -= 12;
388            year += 1;
389        }
390
391        (year, month as u32)
392    }
393
394    /// Returns the days of the month in a 2D vector to render on calendar.
395    fn days(&self) -> Vec<Vec<NaiveDate>> {
396        (0..self.number_of_months)
397            .flat_map(|offset| {
398                days_in_month(self.current_year, self.current_month as u32 + offset as u32)
399            })
400            .collect()
401    }
402
403    fn has_prev_year_page(&self) -> bool {
404        self.year_page > 0
405    }
406
407    fn has_next_year_page(&self) -> bool {
408        self.year_page < self.years.len() as i32 - 1
409    }
410
411    fn prev_year_page(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
412        if !self.has_prev_year_page() {
413            return;
414        }
415
416        self.year_page -= 1;
417        cx.notify()
418    }
419
420    fn next_year_page(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
421        if !self.has_next_year_page() {
422            return;
423        }
424
425        self.year_page += 1;
426        cx.notify()
427    }
428
429    fn prev_month(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
430        self.current_month = if self.current_month == 1 {
431            12
432        } else {
433            self.current_month - 1
434        };
435        self.current_year = if self.current_month == 12 {
436            self.current_year - 1
437        } else {
438            self.current_year
439        };
440        cx.notify()
441    }
442
443    fn next_month(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
444        self.current_month = if self.current_month == 12 {
445            1
446        } else {
447            self.current_month + 1
448        };
449        self.current_year = if self.current_month == 1 {
450            self.current_year + 1
451        } else {
452            self.current_year
453        };
454        cx.notify()
455    }
456
457    fn month_name(&self, offset_month: usize) -> SharedString {
458        let (_, month) = self.offset_year_month(offset_month);
459        match month {
460            1 => t!("Calendar.month.January"),
461            2 => t!("Calendar.month.February"),
462            3 => t!("Calendar.month.March"),
463            4 => t!("Calendar.month.April"),
464            5 => t!("Calendar.month.May"),
465            6 => t!("Calendar.month.June"),
466            7 => t!("Calendar.month.July"),
467            8 => t!("Calendar.month.August"),
468            9 => t!("Calendar.month.September"),
469            10 => t!("Calendar.month.October"),
470            11 => t!("Calendar.month.November"),
471            12 => t!("Calendar.month.December"),
472            _ => Cow::Borrowed(""),
473        }
474        .into()
475    }
476
477    fn year_name(&self, offset_month: usize) -> SharedString {
478        let (year, _) = self.offset_year_month(offset_month);
479        year.to_string().into()
480    }
481
482    fn set_view_mode(&mut self, mode: ViewMode, _: &mut Window, cx: &mut Context<Self>) {
483        self.view_mode = mode;
484        cx.notify();
485    }
486
487    fn months(&self) -> Vec<SharedString> {
488        [
489            t!("Calendar.month.January"),
490            t!("Calendar.month.February"),
491            t!("Calendar.month.March"),
492            t!("Calendar.month.April"),
493            t!("Calendar.month.May"),
494            t!("Calendar.month.June"),
495            t!("Calendar.month.July"),
496            t!("Calendar.month.August"),
497            t!("Calendar.month.September"),
498            t!("Calendar.month.October"),
499            t!("Calendar.month.November"),
500            t!("Calendar.month.December"),
501        ]
502        .iter()
503        .map(|s| s.clone().into())
504        .collect()
505    }
506}
507
508impl Render for CalendarState {
509    fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
510        Empty
511    }
512}
513
514impl Calendar {
515    pub fn new(state: &Entity<CalendarState>) -> Self {
516        Self {
517            id: ("calendar", state.entity_id()).into(),
518            size: Size::default(),
519            state: state.clone(),
520            style: StyleRefinement::default(),
521            number_of_months: 1,
522        }
523    }
524
525    /// Set number of months to show, default is 1.
526    pub fn number_of_months(mut self, number_of_months: usize) -> Self {
527        self.number_of_months = number_of_months;
528        self
529    }
530
531    fn render_day(
532        &self,
533        d: &NaiveDate,
534        offset_month: usize,
535        window: &mut Window,
536        cx: &mut App,
537    ) -> impl IntoElement {
538        let state = self.state.read(cx);
539        let (_, month) = state.offset_year_month(offset_month);
540        let day = d.day();
541        let is_current_month = d.month() == month;
542        let is_active = state.date.is_active(d);
543        let is_in_range = state.date.is_in_range(d);
544
545        let date = *d;
546        let is_today = *d == state.today;
547        let disabled = state
548            .disabled_matcher
549            .as_ref()
550            .map_or(false, |disabled| disabled.matched(&date));
551
552        let date_id: SharedString = format!("{}_{}", date.format("%Y-%m-%d"), offset_month).into();
553
554        self.item_button(
555            date_id,
556            day.to_string(),
557            is_active,
558            is_in_range,
559            !is_current_month || disabled,
560            disabled,
561            window,
562            cx,
563        )
564        .when(is_today && !is_active, |this| {
565            this.border_1().border_color(cx.theme().border)
566        }) // Add border for today
567        .when(!disabled, |this| {
568            this.on_click(window.listener_for(
569                &self.state,
570                move |view, _: &ClickEvent, window, cx| {
571                    if view.date.is_single() {
572                        view.set_date(date, window, cx);
573                        cx.emit(CalendarEvent::Selected(view.date()));
574                    } else {
575                        let start = view.date.start();
576                        let end = view.date.end();
577
578                        if start.is_none() && end.is_none() {
579                            view.set_date(Date::Range(Some(date), None), window, cx);
580                        } else if start.is_some() && end.is_none() {
581                            if date < start.unwrap() {
582                                view.set_date(Date::Range(Some(date), None), window, cx);
583                            } else {
584                                view.set_date(
585                                    Date::Range(Some(start.unwrap()), Some(date)),
586                                    window,
587                                    cx,
588                                );
589                            }
590                        } else {
591                            view.set_date(Date::Range(Some(date), None), window, cx);
592                        }
593
594                        if view.date.is_complete() {
595                            cx.emit(CalendarEvent::Selected(view.date()));
596                        }
597                    }
598                },
599            ))
600        })
601    }
602
603    fn render_header(&self, window: &mut Window, cx: &mut App) -> impl IntoElement {
604        let state = self.state.read(cx);
605        let current_year = state.current_year;
606        let view_mode = state.view_mode;
607        let disabled = view_mode.is_month();
608        let multiple_months = self.number_of_months > 1;
609        let icon_size = match self.size {
610            Size::Small => Size::Small,
611            Size::Large => Size::Medium,
612            _ => Size::Medium,
613        };
614
615        h_flex()
616            .gap_0p5()
617            .justify_between()
618            .items_center()
619            .child(
620                Button::new("prev")
621                    .icon(IconName::ArrowLeft)
622                    .tab_stop(false)
623                    .ghost()
624                    .disabled(disabled)
625                    .with_size(icon_size)
626                    .when(view_mode.is_day(), |this| {
627                        this.on_click(window.listener_for(&self.state, CalendarState::prev_month))
628                    })
629                    .when(view_mode.is_year(), |this| {
630                        this.when(!state.has_prev_year_page(), |this| this.disabled(true))
631                            .on_click(
632                                window.listener_for(&self.state, CalendarState::prev_year_page),
633                            )
634                    }),
635            )
636            .when(!multiple_months, |this| {
637                this.child(
638                    h_flex()
639                        .justify_center()
640                        .gap_3()
641                        .child(
642                            Button::new("month")
643                                .ghost()
644                                .label(state.month_name(0))
645                                .compact()
646                                .tab_stop(false)
647                                .with_size(self.size)
648                                .selected(view_mode.is_month())
649                                .on_click(window.listener_for(
650                                    &self.state,
651                                    move |view, _, window, cx| {
652                                        if view_mode.is_month() {
653                                            view.set_view_mode(ViewMode::Day, window, cx);
654                                        } else {
655                                            view.set_view_mode(ViewMode::Month, window, cx);
656                                        }
657                                        cx.notify();
658                                    },
659                                )),
660                        )
661                        .child(
662                            Button::new("year")
663                                .ghost()
664                                .label(current_year.to_string())
665                                .compact()
666                                .tab_stop(false)
667                                .with_size(self.size)
668                                .selected(view_mode.is_year())
669                                .on_click(window.listener_for(
670                                    &self.state,
671                                    |view, _, window, cx| {
672                                        if view.view_mode.is_year() {
673                                            view.set_view_mode(ViewMode::Day, window, cx);
674                                        } else {
675                                            view.set_view_mode(ViewMode::Year, window, cx);
676                                        }
677                                        cx.notify();
678                                    },
679                                )),
680                        ),
681                )
682            })
683            .when(multiple_months, |this| {
684                this.child(h_flex().flex_1().justify_around().children(
685                    (0..self.number_of_months).map(|n| {
686                        h_flex()
687                            .justify_center()
688                            .map(|this| match self.size {
689                                Size::Small => this.gap_2(),
690                                Size::Large => this.gap_4(),
691                                _ => this.gap_3(),
692                            })
693                            .child(state.month_name(n))
694                            .child(state.year_name(n))
695                    }),
696                ))
697            })
698            .child(
699                Button::new("next")
700                    .icon(IconName::ArrowRight)
701                    .ghost()
702                    .tab_stop(false)
703                    .disabled(disabled)
704                    .with_size(icon_size)
705                    .when(view_mode.is_day(), |this| {
706                        this.on_click(window.listener_for(&self.state, CalendarState::next_month))
707                    })
708                    .when(view_mode.is_year(), |this| {
709                        this.when(!state.has_next_year_page(), |this| this.disabled(true))
710                            .on_click(
711                                window.listener_for(&self.state, CalendarState::next_year_page),
712                            )
713                    }),
714            )
715    }
716
717    #[allow(clippy::too_many_arguments)]
718    fn item_button(
719        &self,
720        id: impl Into<ElementId>,
721        label: impl Into<SharedString>,
722        active: bool,
723        secondary_active: bool,
724        muted: bool,
725        disabled: bool,
726        _: &mut Window,
727        cx: &mut App,
728    ) -> impl IntoElement + Styled + StatefulInteractiveElement {
729        h_flex()
730            .id(id.into())
731            .map(|this| match self.size {
732                Size::Small => this.size_7().rounded(cx.theme().radius),
733                Size::Large => this.size_10().rounded(cx.theme().radius * 2.),
734                _ => this.size_9().rounded(cx.theme().radius * 2.),
735            })
736            .justify_center()
737            .when(muted, |this| {
738                this.text_color(if disabled {
739                    cx.theme().muted_foreground.opacity(0.3)
740                } else {
741                    cx.theme().muted_foreground
742                })
743            })
744            .when(secondary_active, |this| {
745                this.bg(if muted {
746                    cx.theme().accent.opacity(0.5)
747                } else {
748                    cx.theme().accent
749                })
750                .text_color(cx.theme().accent_foreground)
751            })
752            .when(!active && !disabled, |this| {
753                this.hover(|this| {
754                    this.bg(cx.theme().accent)
755                        .text_color(cx.theme().accent_foreground)
756                })
757            })
758            .when(active, |this| {
759                this.bg(cx.theme().primary)
760                    .text_color(cx.theme().primary_foreground)
761            })
762            .child(label.into())
763    }
764
765    fn render_days(&self, window: &mut Window, cx: &mut App) -> impl IntoElement {
766        let state = self.state.read(cx);
767        let weeks = [
768            t!("Calendar.week.0"),
769            t!("Calendar.week.1"),
770            t!("Calendar.week.2"),
771            t!("Calendar.week.3"),
772            t!("Calendar.week.4"),
773            t!("Calendar.week.5"),
774            t!("Calendar.week.6"),
775        ];
776
777        h_flex()
778            .map(|this| match self.size {
779                Size::Small => this.gap_3().text_sm(),
780                Size::Large => this.gap_5().text_base(),
781                _ => this.gap_4().text_sm(),
782            })
783            .justify_between()
784            .children(
785                state
786                    .days()
787                    .chunks(5)
788                    .enumerate()
789                    .map(|(offset_month, days)| {
790                        v_flex()
791                            .gap_0p5()
792                            .child(
793                                h_flex().gap_0p5().justify_between().children(
794                                    weeks
795                                        .iter()
796                                        .map(|week| self.render_week(week.clone(), window, cx)),
797                                ),
798                            )
799                            .children(days.iter().map(|week| {
800                                h_flex().gap_0p5().justify_between().children(
801                                    week.iter()
802                                        .map(|d| self.render_day(d, offset_month, window, cx)),
803                                )
804                            }))
805                    }),
806            )
807    }
808
809    fn render_week(
810        &self,
811        week: impl Into<SharedString>,
812        _: &mut Window,
813        cx: &mut App,
814    ) -> impl IntoElement {
815        h_flex()
816            .map(|this| match self.size {
817                Size::Small => this.size_7().rounded(cx.theme().radius / 2.0),
818                Size::Large => this.size_10().rounded(cx.theme().radius),
819                _ => this.size_9().rounded(cx.theme().radius),
820            })
821            .justify_center()
822            .text_color(cx.theme().muted_foreground)
823            .text_sm()
824            .child(week.into())
825    }
826
827    fn render_months(&self, window: &mut Window, cx: &mut App) -> impl IntoElement {
828        let state = self.state.read(cx);
829        let months = state.months();
830        let current_month = state.current_month;
831
832        h_flex()
833            .mt_3()
834            .gap_0p5()
835            .gap_y_3()
836            .map(|this| match self.size {
837                Size::Small => this.mt_2().gap_y_2().w(px(208.)),
838                Size::Large => this.mt_4().gap_y_4().w(px(292.)),
839                _ => this.mt_3().gap_y_3().w(px(264.)),
840            })
841            .justify_between()
842            .flex_wrap()
843            .children(
844                months
845                    .iter()
846                    .enumerate()
847                    .map(|(ix, month)| {
848                        let active = (ix + 1) as u8 == current_month;
849
850                        self.item_button(
851                            ix,
852                            month.to_string(),
853                            active,
854                            false,
855                            false,
856                            false,
857                            window,
858                            cx,
859                        )
860                        .w(relative(0.3))
861                        .text_sm()
862                        .on_click(window.listener_for(
863                            &self.state,
864                            move |view, _, window, cx| {
865                                view.current_month = (ix + 1) as u8;
866                                view.set_view_mode(ViewMode::Day, window, cx);
867                                cx.notify();
868                            },
869                        ))
870                    })
871                    .collect::<Vec<_>>(),
872            )
873    }
874
875    fn render_years(&self, window: &mut Window, cx: &mut App) -> impl IntoElement {
876        let state = self.state.read(cx);
877        let current_year = state.current_year;
878        let current_page_years = &self.state.read(cx).years[state.year_page as usize].clone();
879
880        h_flex()
881            .id("years")
882            .gap_0p5()
883            .map(|this| match self.size {
884                Size::Small => this.mt_2().gap_y_2().w(px(208.)),
885                Size::Large => this.mt_4().gap_y_4().w(px(292.)),
886                _ => this.mt_3().gap_y_3().w(px(264.)),
887            })
888            .justify_between()
889            .flex_wrap()
890            .children(
891                current_page_years
892                    .iter()
893                    .enumerate()
894                    .map(|(ix, year)| {
895                        let year = *year;
896                        let active = year == current_year;
897
898                        self.item_button(
899                            ix,
900                            year.to_string(),
901                            active,
902                            false,
903                            false,
904                            false,
905                            window,
906                            cx,
907                        )
908                        .w(relative(0.2))
909                        .on_click(window.listener_for(
910                            &self.state,
911                            move |view, _, window, cx| {
912                                view.current_year = year;
913                                view.set_view_mode(ViewMode::Day, window, cx);
914                                cx.notify();
915                            },
916                        ))
917                    })
918                    .collect::<Vec<_>>(),
919            )
920    }
921}
922
923impl Sizable for Calendar {
924    fn with_size(mut self, size: impl Into<Size>) -> Self {
925        self.size = size.into();
926        self
927    }
928}
929
930impl Styled for Calendar {
931    fn style(&mut self) -> &mut StyleRefinement {
932        &mut self.style
933    }
934}
935
936impl EventEmitter<CalendarEvent> for CalendarState {}
937impl RenderOnce for Calendar {
938    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
939        let view_mode = self.state.read(cx).view_mode;
940        let number_of_months = self.number_of_months;
941        self.state.update(cx, |state, _| {
942            state.number_of_months = number_of_months;
943        });
944
945        v_flex()
946            .id(self.id.clone())
947            .track_focus(&self.state.read(cx).focus_handle)
948            .border_1()
949            .border_color(cx.theme().border)
950            .rounded(cx.theme().radius_lg)
951            .p_3()
952            .gap_0p5()
953            .refine_style(&self.style)
954            .child(self.render_header(window, cx))
955            .child(
956                v_flex()
957                    .when(view_mode.is_day(), |this| {
958                        this.child(self.render_days(window, cx))
959                    })
960                    .when(view_mode.is_month(), |this| {
961                        this.child(self.render_months(window, cx))
962                    })
963                    .when(view_mode.is_year(), |this| {
964                        this.child(self.render_years(window, cx))
965                    }),
966            )
967    }
968}
969
970#[cfg(test)]
971mod tests {
972    use chrono::NaiveDate;
973
974    use super::Date;
975
976    #[test]
977    fn test_date_to_string() {
978        let date = Date::Single(Some(NaiveDate::from_ymd_opt(2024, 8, 3).unwrap()));
979        assert_eq!(date.to_string(), "2024-08-03");
980
981        let date = Date::Single(None);
982        assert_eq!(date.to_string(), "nil");
983
984        let date = Date::Range(
985            Some(NaiveDate::from_ymd_opt(2024, 8, 3).unwrap()),
986            Some(NaiveDate::from_ymd_opt(2024, 8, 5).unwrap()),
987        );
988        assert_eq!(date.to_string(), "2024-08-03 - 2024-08-05");
989
990        let date = Date::Range(Some(NaiveDate::from_ymd_opt(2024, 8, 3).unwrap()), None);
991        assert_eq!(date.to_string(), "2024-08-03 - nil");
992
993        let date = Date::Range(None, Some(NaiveDate::from_ymd_opt(2024, 8, 5).unwrap()));
994        assert_eq!(date.to_string(), "nil - 2024-08-05");
995
996        let date = Date::Range(None, None);
997        assert_eq!(date.to_string(), "nil");
998    }
999}