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