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
20pub enum CalendarEvent {
22 Selected(Date),
24}
25
26#[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 pub fn is_some(&self) -> bool {
61 match self {
62 Self::Single(Some(_)) | Self::Range(Some(_), _) => true,
63 _ => false,
64 }
65 }
66
67 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 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 pub fn end(&self) -> Option<NaiveDate> {
87 match self {
88 Self::Range(_, Some(end)) => Some(*end),
89 _ => None,
90 }
91 }
92
93 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
156pub struct IntervalMatcher {
158 before: Option<NaiveDate>,
159 after: Option<NaiveDate>,
160}
161
162pub struct RangeMatcher {
164 from: Option<NaiveDate>,
165 to: Option<NaiveDate>,
166}
167
168pub enum Matcher {
170 DayOfWeek(Vec<u32>),
175 Interval(IntervalMatcher),
183 Range(RangeMatcher),
191 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 pub fn interval(before: Option<NaiveDate>, after: Option<NaiveDate>) -> Self {
218 Matcher::Interval(IntervalMatcher { before, after })
219 }
220
221 pub fn range(from: Option<NaiveDate>, to: Option<NaiveDate>) -> Self {
223 Matcher::Range(RangeMatcher { from, to })
224 }
225
226 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 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_months: usize,
269}
270
271pub 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_months: usize,
283 pub(crate) disabled_matcher: Option<Rc<Matcher>>,
284}
285
286impl CalendarState {
287 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 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 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 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 pub fn date(&self) -> Date {
356 self.date
357 }
358
359 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 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 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 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 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 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 }) .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}