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