Skip to main content

rgpui_component/time/
calendar.rs

1use std::{borrow::Cow, rc::Rc};
2
3use chrono::{Datelike, Local, NaiveDate};
4use rgpui::{
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.apply_year_range(range);
375        self
376    }
377
378    /// Set the year range of the calendar.
379    pub fn set_year_range(&mut self, range: (i32, i32), cx: &mut Context<Self>) {
380        self.apply_year_range(range);
381        cx.notify();
382    }
383
384    fn apply_year_range(&mut self, range: (i32, i32)) {
385        self.years = (range.0..range.1)
386            .collect::<Vec<_>>()
387            .chunks(20)
388            .map(|chunk| chunk.to_vec())
389            .collect::<Vec<_>>();
390        self.year_page = self
391            .years
392            .iter()
393            .position(|years| years.contains(&self.current_year))
394            .unwrap_or(0) as i32;
395    }
396
397    /// Get year and month by offset month.
398    fn offset_year_month(&self, offset_month: usize) -> (i32, u32) {
399        let mut month = self.current_month as i32 + offset_month as i32;
400        let mut year = self.current_year;
401        while month < 1 {
402            month += 12;
403            year -= 1;
404        }
405        while month > 12 {
406            month -= 12;
407            year += 1;
408        }
409
410        (year, month as u32)
411    }
412
413    /// Returns the days of the month in a 2D vector to render on calendar.
414    fn days(&self) -> Vec<Vec<NaiveDate>> {
415        (0..self.number_of_months)
416            .flat_map(|offset| {
417                days_in_month(self.current_year, self.current_month as u32 + offset as u32)
418            })
419            .collect()
420    }
421
422    fn has_prev_year_page(&self) -> bool {
423        self.year_page > 0
424    }
425
426    fn has_next_year_page(&self) -> bool {
427        self.year_page < self.years.len() as i32 - 1
428    }
429
430    fn prev_year_page(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
431        if !self.has_prev_year_page() {
432            return;
433        }
434
435        self.year_page -= 1;
436        cx.notify()
437    }
438
439    fn next_year_page(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
440        if !self.has_next_year_page() {
441            return;
442        }
443
444        self.year_page += 1;
445        cx.notify()
446    }
447
448    fn prev_month(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
449        self.current_month = if self.current_month == 1 {
450            12
451        } else {
452            self.current_month - 1
453        };
454        self.current_year = if self.current_month == 12 {
455            self.current_year - 1
456        } else {
457            self.current_year
458        };
459        cx.notify()
460    }
461
462    fn next_month(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
463        self.current_month = if self.current_month == 12 {
464            1
465        } else {
466            self.current_month + 1
467        };
468        self.current_year = if self.current_month == 1 {
469            self.current_year + 1
470        } else {
471            self.current_year
472        };
473        cx.notify()
474    }
475
476    fn month_name(&self, offset_month: usize) -> SharedString {
477        let (_, month) = self.offset_year_month(offset_month);
478        match month {
479            1 => t!("Calendar.month.January"),
480            2 => t!("Calendar.month.February"),
481            3 => t!("Calendar.month.March"),
482            4 => t!("Calendar.month.April"),
483            5 => t!("Calendar.month.May"),
484            6 => t!("Calendar.month.June"),
485            7 => t!("Calendar.month.July"),
486            8 => t!("Calendar.month.August"),
487            9 => t!("Calendar.month.September"),
488            10 => t!("Calendar.month.October"),
489            11 => t!("Calendar.month.November"),
490            12 => t!("Calendar.month.December"),
491            _ => Cow::Borrowed(""),
492        }
493        .into()
494    }
495
496    fn year_name(&self, offset_month: usize) -> SharedString {
497        let (year, _) = self.offset_year_month(offset_month);
498        year.to_string().into()
499    }
500
501    fn set_view_mode(&mut self, mode: ViewMode, _: &mut Window, cx: &mut Context<Self>) {
502        self.view_mode = mode;
503        cx.notify();
504    }
505
506    fn months(&self) -> Vec<SharedString> {
507        [
508            t!("Calendar.month.January"),
509            t!("Calendar.month.February"),
510            t!("Calendar.month.March"),
511            t!("Calendar.month.April"),
512            t!("Calendar.month.May"),
513            t!("Calendar.month.June"),
514            t!("Calendar.month.July"),
515            t!("Calendar.month.August"),
516            t!("Calendar.month.September"),
517            t!("Calendar.month.October"),
518            t!("Calendar.month.November"),
519            t!("Calendar.month.December"),
520        ]
521        .iter()
522        .map(|s| s.clone().into())
523        .collect()
524    }
525}
526
527impl Render for CalendarState {
528    fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
529        Empty
530    }
531}
532
533impl Calendar {
534    /// Create a new calendar element with [`CalendarState`].
535    pub fn new(state: &Entity<CalendarState>) -> Self {
536        Self {
537            id: ("calendar", state.entity_id()).into(),
538            size: Size::default(),
539            state: state.clone(),
540            style: StyleRefinement::default(),
541            number_of_months: 1,
542        }
543    }
544
545    /// Set number of months to show, default is 1.
546    pub fn number_of_months(mut self, number_of_months: usize) -> Self {
547        self.number_of_months = number_of_months;
548        self
549    }
550
551    fn render_day(
552        &self,
553        d: &NaiveDate,
554        offset_month: usize,
555        window: &mut Window,
556        cx: &mut App,
557    ) -> Stateful<Div> {
558        let state = self.state.read(cx);
559        let (_, month) = state.offset_year_month(offset_month);
560        let day = d.day();
561        let is_current_month = d.month() == month;
562        let is_active = state.date.is_active(d);
563        let is_in_range = state.date.is_in_range(d);
564
565        let date = *d;
566        let is_today = *d == state.today;
567        let disabled = state
568            .disabled_matcher
569            .as_ref()
570            .map_or(false, |disabled| disabled.matched(&date));
571
572        let date_id: SharedString = format!("{}_{}", date.format("%Y-%m-%d"), offset_month).into();
573
574        self.item_button(
575            date_id.clone(),
576            day.to_string(),
577            is_active,
578            is_in_range,
579            !is_current_month || disabled,
580            disabled,
581            window,
582            cx,
583        )
584        .when(is_today && !is_active, |this| {
585            this.border_1().border_color(cx.theme().border)
586        }) // Add border for today
587        .when(!disabled, |this| {
588            this.on_click(window.listener_for(
589                &self.state,
590                move |view, _: &ClickEvent, window, cx| {
591                    if view.date.is_single() {
592                        view.set_date(date, window, cx);
593                        cx.emit(CalendarEvent::Selected(view.date()));
594                    } else {
595                        let start = view.date.start();
596                        let end = view.date.end();
597
598                        if start.is_none() && end.is_none() {
599                            view.set_date(Date::Range(Some(date), None), window, cx);
600                        } else if start.is_some() && end.is_none() {
601                            if date < start.unwrap() {
602                                view.set_date(Date::Range(Some(date), None), window, cx);
603                            } else {
604                                view.set_date(
605                                    Date::Range(Some(start.unwrap()), Some(date)),
606                                    window,
607                                    cx,
608                                );
609                            }
610                        } else {
611                            view.set_date(Date::Range(Some(date), None), window, cx);
612                        }
613
614                        if view.date.is_complete() {
615                            cx.emit(CalendarEvent::Selected(view.date()));
616                        }
617                    }
618                },
619            ))
620        })
621    }
622
623    fn render_header(&self, window: &mut Window, cx: &mut App) -> impl IntoElement {
624        let state = self.state.read(cx);
625        let current_year = state.current_year;
626        let view_mode = state.view_mode;
627        let disabled = view_mode.is_month();
628        let multiple_months = self.number_of_months > 1;
629        let icon_size = match self.size {
630            Size::Small => Size::Small,
631            Size::Large => Size::Medium,
632            _ => Size::Medium,
633        };
634
635        h_flex()
636            .gap_0p5()
637            .justify_between()
638            .items_center()
639            .child(
640                Button::new("prev")
641                    .icon(IconName::ArrowLeft)
642                    .tab_stop(false)
643                    .ghost()
644                    .disabled(disabled)
645                    .with_size(icon_size)
646                    .when(view_mode.is_day(), |this| {
647                        this.on_click(window.listener_for(&self.state, CalendarState::prev_month))
648                    })
649                    .when(view_mode.is_year(), |this| {
650                        this.when(!state.has_prev_year_page(), |this| this.disabled(true))
651                            .on_click(
652                                window.listener_for(&self.state, CalendarState::prev_year_page),
653                            )
654                    }),
655            )
656            .when(!multiple_months, |this| {
657                this.child(
658                    h_flex()
659                        .justify_center()
660                        .gap_3()
661                        .child(
662                            Button::new("month")
663                                .ghost()
664                                .label(state.month_name(0))
665                                .compact()
666                                .tab_stop(false)
667                                .with_size(self.size)
668                                .selected(view_mode.is_month())
669                                .on_click(window.listener_for(
670                                    &self.state,
671                                    move |view, _, window, cx| {
672                                        if view_mode.is_month() {
673                                            view.set_view_mode(ViewMode::Day, window, cx);
674                                        } else {
675                                            view.set_view_mode(ViewMode::Month, window, cx);
676                                        }
677                                        cx.notify();
678                                    },
679                                )),
680                        )
681                        .child(
682                            Button::new("year")
683                                .ghost()
684                                .label(current_year.to_string())
685                                .compact()
686                                .tab_stop(false)
687                                .with_size(self.size)
688                                .selected(view_mode.is_year())
689                                .on_click(window.listener_for(
690                                    &self.state,
691                                    |view, _, window, cx| {
692                                        if view.view_mode.is_year() {
693                                            view.set_view_mode(ViewMode::Day, window, cx);
694                                        } else {
695                                            view.set_view_mode(ViewMode::Year, window, cx);
696                                        }
697                                        cx.notify();
698                                    },
699                                )),
700                        ),
701                )
702            })
703            .when(multiple_months, |this| {
704                this.child(h_flex().flex_1().justify_around().children(
705                    (0..self.number_of_months).map(|n| {
706                        h_flex()
707                            .justify_center()
708                            .map(|this| match self.size {
709                                Size::Small => this.gap_2(),
710                                Size::Large => this.gap_4(),
711                                _ => this.gap_3(),
712                            })
713                            .child(state.month_name(n))
714                            .child(state.year_name(n))
715                    }),
716                ))
717            })
718            .child(
719                Button::new("next")
720                    .icon(IconName::ArrowRight)
721                    .ghost()
722                    .tab_stop(false)
723                    .disabled(disabled)
724                    .with_size(icon_size)
725                    .when(view_mode.is_day(), |this| {
726                        this.on_click(window.listener_for(&self.state, CalendarState::next_month))
727                    })
728                    .when(view_mode.is_year(), |this| {
729                        this.when(!state.has_next_year_page(), |this| this.disabled(true))
730                            .on_click(
731                                window.listener_for(&self.state, CalendarState::next_year_page),
732                            )
733                    }),
734            )
735    }
736
737    #[allow(clippy::too_many_arguments)]
738    fn item_button(
739        &self,
740        id: impl Into<ElementId>,
741        label: impl Into<SharedString>,
742        active: bool,
743        secondary_active: bool,
744        muted: bool,
745        disabled: bool,
746        _: &mut Window,
747        cx: &mut App,
748    ) -> Stateful<Div> {
749        h_flex()
750            .id(id.into())
751            .map(|this| match self.size {
752                Size::Small => this.size_7().rounded(cx.theme().radius / 2.),
753                Size::Large => this.size_10().rounded(cx.theme().radius * 2.),
754                _ => this.size_9().rounded(cx.theme().radius),
755            })
756            .justify_center()
757            .when(muted, |this| {
758                this.text_color(if disabled {
759                    cx.theme().muted_foreground.opacity(0.3)
760                } else {
761                    cx.theme().muted_foreground
762                })
763            })
764            .when(secondary_active, |this| {
765                this.bg(if muted {
766                    cx.theme().accent.opacity(0.5)
767                } else {
768                    cx.theme().accent
769                })
770                .text_color(cx.theme().accent_foreground)
771            })
772            .when(!active && !disabled, |this| {
773                this.hover(|this| {
774                    this.bg(cx.theme().accent)
775                        .text_color(cx.theme().accent_foreground)
776                })
777            })
778            .when(active, |this| {
779                this.bg(cx.theme().primary)
780                    .text_color(cx.theme().primary_foreground)
781            })
782            .child(label.into())
783    }
784
785    fn render_days(&self, window: &mut Window, cx: &mut App) -> impl IntoElement {
786        let state = self.state.read(cx);
787        let weeks = [
788            t!("Calendar.week.0"),
789            t!("Calendar.week.1"),
790            t!("Calendar.week.2"),
791            t!("Calendar.week.3"),
792            t!("Calendar.week.4"),
793            t!("Calendar.week.5"),
794            t!("Calendar.week.6"),
795        ];
796
797        h_flex()
798            .map(|this| match self.size {
799                Size::Small => this.gap_3().text_sm(),
800                Size::Large => this.gap_5().text_base(),
801                _ => this.gap_4().text_sm(),
802            })
803            .justify_between()
804            .children(
805                state
806                    .days()
807                    .chunks(5)
808                    .enumerate()
809                    .map(|(offset_month, days)| {
810                        v_flex()
811                            .gap_0p5()
812                            .child(
813                                h_flex().gap_0p5().justify_between().children(
814                                    weeks
815                                        .iter()
816                                        .map(|week| self.render_week(week.clone(), window, cx)),
817                                ),
818                            )
819                            .children(days.iter().map(|week| {
820                                h_flex().gap_0p5().justify_between().children(
821                                    week.iter()
822                                        .map(|d| self.render_day(d, offset_month, window, cx)),
823                                )
824                            }))
825                    }),
826            )
827    }
828
829    fn render_week(&self, week: impl Into<SharedString>, _: &mut Window, cx: &mut App) -> Div {
830        h_flex()
831            .map(|this| match self.size {
832                Size::Small => this.size_7().rounded(cx.theme().radius / 2.0),
833                Size::Large => this.size_10().rounded(cx.theme().radius),
834                _ => this.size_9().rounded(cx.theme().radius),
835            })
836            .justify_center()
837            .text_color(cx.theme().muted_foreground)
838            .text_sm()
839            .child(week.into())
840    }
841
842    fn render_months(&self, window: &mut Window, cx: &mut App) -> impl IntoElement {
843        let state = self.state.read(cx);
844        let months = state.months();
845        let current_month = state.current_month;
846
847        h_flex()
848            .mt_3()
849            .gap_0p5()
850            .gap_y_3()
851            .map(|this| match self.size {
852                Size::Small => this.mt_2().gap_y_2().w(px(208.)),
853                Size::Large => this.mt_4().gap_y_4().w(px(292.)),
854                _ => this.mt_3().gap_y_3().w(px(264.)),
855            })
856            .justify_between()
857            .flex_wrap()
858            .children(
859                months
860                    .iter()
861                    .enumerate()
862                    .map(|(ix, month)| {
863                        let active = (ix + 1) as u8 == current_month;
864
865                        self.item_button(
866                            ix,
867                            month.to_string(),
868                            active,
869                            false,
870                            false,
871                            false,
872                            window,
873                            cx,
874                        )
875                        .w(relative(0.3))
876                        .text_sm()
877                        .on_click(window.listener_for(
878                            &self.state,
879                            move |view, _, window, cx| {
880                                view.current_month = (ix + 1) as u8;
881                                view.set_view_mode(ViewMode::Day, window, cx);
882                                cx.notify();
883                            },
884                        ))
885                    })
886                    .collect::<Vec<_>>(),
887            )
888    }
889
890    fn render_years(&self, window: &mut Window, cx: &mut App) -> impl IntoElement {
891        let state = self.state.read(cx);
892        let current_year = state.current_year;
893        let current_page_years = &self.state.read(cx).years[state.year_page as usize].clone();
894
895        h_flex()
896            .id("years")
897            .gap_0p5()
898            .map(|this| match self.size {
899                Size::Small => this.mt_2().gap_y_2().w(px(208.)),
900                Size::Large => this.mt_4().gap_y_4().w(px(292.)),
901                _ => this.mt_3().gap_y_3().w(px(264.)),
902            })
903            .justify_between()
904            .flex_wrap()
905            .children(
906                current_page_years
907                    .iter()
908                    .enumerate()
909                    .map(|(ix, year)| {
910                        let year = *year;
911                        let active = year == current_year;
912
913                        self.item_button(
914                            ix,
915                            year.to_string(),
916                            active,
917                            false,
918                            false,
919                            false,
920                            window,
921                            cx,
922                        )
923                        .w(relative(0.2))
924                        .on_click(window.listener_for(
925                            &self.state,
926                            move |view, _, window, cx| {
927                                view.current_year = year;
928                                view.set_view_mode(ViewMode::Day, window, cx);
929                                cx.notify();
930                            },
931                        ))
932                    })
933                    .collect::<Vec<_>>(),
934            )
935    }
936}
937
938impl Sizable for Calendar {
939    fn with_size(mut self, size: impl Into<Size>) -> Self {
940        self.size = size.into();
941        self
942    }
943}
944
945impl Styled for Calendar {
946    fn style(&mut self) -> &mut StyleRefinement {
947        &mut self.style
948    }
949}
950
951impl EventEmitter<CalendarEvent> for CalendarState {}
952impl RenderOnce for Calendar {
953    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
954        let view_mode = self.state.read(cx).view_mode;
955        let number_of_months = self.number_of_months;
956        self.state.update(cx, |state, _| {
957            state.number_of_months = number_of_months;
958        });
959
960        v_flex()
961            .id(self.id.clone())
962            .track_focus(&self.state.read(cx).focus_handle)
963            .border_1()
964            .border_color(cx.theme().border)
965            .rounded(cx.theme().radius_lg)
966            .p_3()
967            .gap_0p5()
968            .refine_style(&self.style)
969            .child(self.render_header(window, cx))
970            .child(
971                v_flex()
972                    .when(view_mode.is_day(), |this| {
973                        this.child(self.render_days(window, cx))
974                    })
975                    .when(view_mode.is_month(), |this| {
976                        this.child(self.render_months(window, cx))
977                    })
978                    .when(view_mode.is_year(), |this| {
979                        this.child(self.render_years(window, cx))
980                    }),
981            )
982    }
983}
984
985#[cfg(test)]
986mod tests {
987    use chrono::NaiveDate;
988
989    use super::Date;
990
991    #[test]
992    fn test_date_to_string() {
993        let date = Date::Single(Some(NaiveDate::from_ymd_opt(2024, 8, 3).unwrap()));
994        assert_eq!(date.to_string(), "2024-08-03");
995
996        let date = Date::Single(None);
997        assert_eq!(date.to_string(), "nil");
998
999        let date = Date::Range(
1000            Some(NaiveDate::from_ymd_opt(2024, 8, 3).unwrap()),
1001            Some(NaiveDate::from_ymd_opt(2024, 8, 5).unwrap()),
1002        );
1003        assert_eq!(date.to_string(), "2024-08-03 - 2024-08-05");
1004
1005        let date = Date::Range(Some(NaiveDate::from_ymd_opt(2024, 8, 3).unwrap()), None);
1006        assert_eq!(date.to_string(), "2024-08-03 - nil");
1007
1008        let date = Date::Range(None, Some(NaiveDate::from_ymd_opt(2024, 8, 5).unwrap()));
1009        assert_eq!(date.to_string(), "nil - 2024-08-05");
1010
1011        let date = Date::Range(None, None);
1012        assert_eq!(date.to_string(), "nil");
1013    }
1014}