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