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
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.apply_year_range(range);
375 self
376 }
377
378 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 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 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 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 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 }) .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}