gpui_component/time/
calendar.rs

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