1use std::rc::Rc;
2
3use crate::{h_flex, styled::StyledExt as _, v_flex};
4use chrono::{Datelike, Local, NaiveDate, Weekday};
5use gpui::{
6 AnyElement, App, Context, ElementId, Empty, Entity, EventEmitter, FocusHandle,
7 InteractiveElement, IntoElement, ParentElement, Render, RenderOnce, SharedString,
8 StatefulInteractiveElement, StyleRefinement, Styled, Window, div, px,
9};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum Date {
14 Single(Option<NaiveDate>),
15 Range(Option<NaiveDate>, Option<NaiveDate>),
16}
17
18impl std::fmt::Display for Date {
19 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20 match self {
21 Self::Single(Some(date)) => write!(f, "{date}"),
22 Self::Single(None) | Self::Range(None, None) => write!(f, "nil"),
23 Self::Range(Some(start), Some(end)) => write!(f, "{start} - {end}"),
24 Self::Range(Some(start), None) => write!(f, "{start} - nil"),
25 Self::Range(None, Some(end)) => write!(f, "nil - {end}"),
26 }
27 }
28}
29
30impl From<NaiveDate> for Date {
31 fn from(value: NaiveDate) -> Self {
32 Self::Single(Some(value))
33 }
34}
35impl From<(NaiveDate, NaiveDate)> for Date {
36 fn from((start, end): (NaiveDate, NaiveDate)) -> Self {
37 Self::Range(Some(start), Some(end))
38 }
39}
40
41impl Date {
42 pub fn is_some(&self) -> bool {
43 matches!(self, Self::Single(Some(_)) | Self::Range(Some(_), _))
44 }
45 pub fn is_complete(&self) -> bool {
46 matches!(self, Self::Single(Some(_)) | Self::Range(Some(_), Some(_)))
47 }
48 pub fn start(&self) -> Option<NaiveDate> {
49 match self {
50 Self::Single(Some(v)) | Self::Range(Some(v), _) => Some(*v),
51 _ => None,
52 }
53 }
54 pub fn end(&self) -> Option<NaiveDate> {
55 match self {
56 Self::Range(_, Some(v)) => Some(*v),
57 _ => None,
58 }
59 }
60 pub fn format(&self, format: &str) -> Option<SharedString> {
61 match self {
62 Self::Single(Some(v)) => Some(v.format(format).to_string().into()),
63 Self::Range(Some(a), Some(b)) => {
64 Some(format!("{} - {}", a.format(format), b.format(format)).into())
65 }
66 _ => None,
67 }
68 }
69 pub fn is_active(&self, value: &NaiveDate) -> bool {
70 match self {
71 Self::Single(v) => *v == Some(*value),
72 Self::Range(a, b) => *a == Some(*value) || *b == Some(*value),
73 }
74 }
75 pub fn is_single(&self) -> bool {
76 matches!(self, Self::Single(_))
77 }
78 pub fn is_in_range(&self, value: &NaiveDate) -> bool {
79 matches!(self, Self::Range(Some(a), Some(b)) if value >= a && value <= b)
80 }
81}
82
83pub struct IntervalMatcher {
84 before: Option<NaiveDate>,
85 after: Option<NaiveDate>,
86}
87pub struct RangeMatcher {
88 from: Option<NaiveDate>,
89 to: Option<NaiveDate>,
90}
91pub enum Matcher {
92 DayOfWeek(Vec<u32>),
93 Interval(IntervalMatcher),
94 Range(RangeMatcher),
95 Custom(Box<dyn Fn(&NaiveDate) -> bool + Send + Sync>),
96}
97impl From<Vec<u32>> for Matcher {
98 fn from(v: Vec<u32>) -> Self {
99 Self::DayOfWeek(v)
100 }
101}
102impl<F: Fn(&NaiveDate) -> bool + Send + Sync + 'static> From<F> for Matcher {
103 fn from(v: F) -> Self {
104 Self::Custom(Box::new(v))
105 }
106}
107impl Matcher {
108 pub fn interval(before: Option<NaiveDate>, after: Option<NaiveDate>) -> Self {
109 Self::Interval(IntervalMatcher { before, after })
110 }
111 pub fn range(from: Option<NaiveDate>, to: Option<NaiveDate>) -> Self {
112 Self::Range(RangeMatcher { from, to })
113 }
114 pub fn custom<F: Fn(&NaiveDate) -> bool + Send + Sync + 'static>(f: F) -> Self {
115 Self::Custom(Box::new(f))
116 }
117 pub fn is_match(&self, date: &Date) -> bool {
118 match date {
119 Date::Single(Some(v)) => self.matched(v),
120 Date::Range(Some(a), Some(b)) => self.matched(a) || self.matched(b),
121 _ => false,
122 }
123 }
124 pub fn matched(&self, date: &NaiveDate) -> bool {
125 match self {
126 Self::DayOfWeek(days) => days.contains(&date.weekday().num_days_from_sunday()),
127 Self::Interval(v) => {
128 v.before.is_some_and(|x| date < &x) || v.after.is_some_and(|x| date > &x)
129 }
130 Self::Range(v) => {
131 !v.from.is_some_and(|x| date < &x) && !v.to.is_some_and(|x| date > &x)
132 }
133 Self::Custom(f) => f(date),
134 }
135 }
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub enum CalendarView {
140 Day,
141 Month,
142 Year,
143}
144impl CalendarView {
145 pub fn is_day(self) -> bool {
146 self == Self::Day
147 }
148 pub fn is_month(self) -> bool {
149 self == Self::Month
150 }
151 pub fn is_year(self) -> bool {
152 self == Self::Year
153 }
154}
155
156fn picker_grid_layout(view: CalendarView) -> Option<(u16, f32)> {
157 match view {
158 CalendarView::Day => None,
159 CalendarView::Month => Some((3, 4.)),
160 CalendarView::Year => Some((5, 4.)),
161 }
162}
163
164pub enum CalendarEvent {
165 Selected(Date),
166}
167
168pub struct CalendarState {
169 pub focus_handle: FocusHandle,
170 view: CalendarView,
171 date: Date,
172 current_year: i32,
173 current_month: u8,
174 years: Vec<Vec<i32>>,
175 year_page: i32,
176 today: NaiveDate,
177 number_of_months: usize,
178 disabled_matcher: Option<Rc<Matcher>>,
179}
180
181impl CalendarState {
182 pub fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
183 let today = Local::now().date_naive();
184 Self {
185 focus_handle: cx.focus_handle(),
186 view: CalendarView::Day,
187 date: Date::Single(None),
188 current_year: today.year(),
189 current_month: today.month() as u8,
190 years: vec![],
191 year_page: 0,
192 today,
193 number_of_months: 1,
194 disabled_matcher: None,
195 }
196 .year_range((today.year() - 50, today.year() + 50))
197 }
198 pub fn disabled_matcher(mut self, matcher: impl Into<Matcher>) -> Self {
199 self.disabled_matcher = Some(Rc::new(matcher.into()));
200 self
201 }
202 pub fn set_disabled_matcher(
203 &mut self,
204 matcher: impl Into<Matcher>,
205 _: &mut Window,
206 _: &mut Context<Self>,
207 ) {
208 self.disabled_matcher = Some(Rc::new(matcher.into()));
209 }
210 pub fn set_disabled_matcher_shared(&mut self, matcher: Option<Rc<Matcher>>) {
211 self.disabled_matcher = matcher;
212 }
213 pub fn disabled_matcher_ref(&self) -> Option<&Matcher> {
214 self.disabled_matcher.as_deref()
215 }
216 pub fn set_date(&mut self, date: impl Into<Date>, _: &mut Window, cx: &mut Context<Self>) {
217 if self.apply_date(date.into()) {
218 cx.notify();
219 }
220 }
221 pub fn apply_date(&mut self, date: Date) -> bool {
222 if self
223 .disabled_matcher
224 .as_ref()
225 .is_some_and(|m| m.is_match(&date))
226 {
227 return false;
228 }
229 self.date = date;
230 if let Some(v) = date.start() {
231 self.current_month = v.month() as u8;
232 self.current_year = v.year();
233 }
234 true
235 }
236 pub fn select_date(&mut self, value: NaiveDate) -> bool {
237 if self
238 .disabled_matcher
239 .as_ref()
240 .is_some_and(|m| m.matched(&value))
241 {
242 return false;
243 }
244 let next = match self.date {
245 Date::Single(_) => Date::Single(Some(value)),
246 Date::Range(None, None) | Date::Range(None, Some(_)) => Date::Range(Some(value), None),
247 Date::Range(Some(start), None) if value >= start => {
248 Date::Range(Some(start), Some(value))
249 }
250 Date::Range(Some(_), None) | Date::Range(Some(_), Some(_)) => {
251 Date::Range(Some(value), None)
252 }
253 };
254 self.apply_date(next);
255 self.date.is_complete()
256 }
257 pub fn activate_date(&mut self, value: NaiveDate, cx: &mut Context<Self>) -> bool {
261 let complete = self.select_date(value);
262 if complete {
263 cx.emit(CalendarEvent::Selected(self.date()));
264 }
265 cx.notify();
266 complete
267 }
268 pub fn date(&self) -> Date {
269 self.date
270 }
271 pub fn set_number_of_months(&mut self, n: usize, _: &mut Window, cx: &mut Context<Self>) {
272 self.number_of_months = n;
273 cx.notify();
274 }
275 pub fn number_of_months(&self) -> usize {
276 self.number_of_months
277 }
278 pub fn year_range(mut self, range: (i32, i32)) -> Self {
279 self.apply_year_range(range);
280 self
281 }
282 pub fn set_year_range(&mut self, range: (i32, i32), cx: &mut Context<Self>) {
283 self.apply_year_range(range);
284 cx.notify();
285 }
286 fn apply_year_range(&mut self, range: (i32, i32)) {
287 self.years = (range.0..range.1)
288 .collect::<Vec<_>>()
289 .chunks(20)
290 .map(<[_]>::to_vec)
291 .collect();
292 self.year_page = self
293 .years
294 .iter()
295 .position(|v| v.contains(&self.current_year))
296 .unwrap_or(0) as i32;
297 }
298 pub fn offset_year_month(&self, offset: usize) -> (i32, u32) {
299 let n = self.current_month as i64 - 1 + offset as i64;
300 (
301 self.current_year + n.div_euclid(12) as i32,
302 n.rem_euclid(12) as u32 + 1,
303 )
304 }
305 pub fn days(&self) -> Vec<Vec<NaiveDate>> {
306 self.month_days().into_iter().flatten().collect()
307 }
308 pub fn month_days(&self) -> Vec<Vec<Vec<NaiveDate>>> {
311 (0..self.number_of_months)
312 .map(|n| {
313 days_in_month(
314 self.current_year,
315 self.current_month as u32 + n as u32,
316 Weekday::Sun,
317 )
318 })
319 .collect()
320 }
321 pub fn has_prev_year_page(&self) -> bool {
322 self.year_page > 0
323 }
324 pub fn has_next_year_page(&self) -> bool {
325 self.year_page < self.years.len() as i32 - 1
326 }
327 pub fn prev_year_page(&mut self) -> bool {
328 if !self.has_prev_year_page() {
329 false
330 } else {
331 self.year_page -= 1;
332 true
333 }
334 }
335 pub fn next_year_page(&mut self) -> bool {
336 if !self.has_next_year_page() {
337 false
338 } else {
339 self.year_page += 1;
340 true
341 }
342 }
343 pub fn prev_month(&mut self) {
344 if self.current_month == 1 {
345 self.current_year -= 1;
346 self.current_month = 12;
347 } else {
348 self.current_month -= 1;
349 }
350 }
351 pub fn next_month(&mut self) {
352 if self.current_month == 12 {
353 self.current_year += 1;
354 self.current_month = 1;
355 } else {
356 self.current_month += 1;
357 }
358 }
359 pub fn view(&self) -> CalendarView {
360 self.view
361 }
362 pub fn set_view(&mut self, view: CalendarView) {
363 self.view = view;
364 }
365 pub fn current_year(&self) -> i32 {
366 self.current_year
367 }
368 pub fn current_month(&self) -> u8 {
369 self.current_month
370 }
371 pub fn today(&self) -> NaiveDate {
372 self.today
373 }
374 pub fn years_on_page(&self) -> &[i32] {
375 self.years
376 .get(self.year_page as usize)
377 .map(Vec::as_slice)
378 .unwrap_or_default()
379 }
380 pub fn select_month(&mut self, month: u8) {
381 self.current_month = month;
382 self.view = CalendarView::Day;
383 }
384 pub fn select_year(&mut self, year: i32) {
385 self.current_year = year;
386 self.view = CalendarView::Day;
387 }
388}
389impl EventEmitter<CalendarEvent> for CalendarState {}
390impl Render for CalendarState {
391 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
392 Empty
393 }
394}
395
396#[derive(Clone, Copy, Debug, PartialEq, Eq)]
398pub enum CalendarItemKind {
399 Previous,
400 MonthToggle,
401 YearToggle,
402 Next,
403 Weekday,
404 Day,
405 Month,
406 Year,
407}
408
409#[derive(Clone, Copy, Debug)]
415pub struct CalendarItemState {
416 kind: CalendarItemKind,
417 active: bool,
418 in_range: bool,
419 muted: bool,
420 disabled: bool,
421 today: bool,
422}
423
424impl CalendarItemState {
425 pub fn new(kind: CalendarItemKind) -> Self {
427 Self {
428 kind,
429 active: false,
430 in_range: false,
431 muted: false,
432 disabled: false,
433 today: false,
434 }
435 }
436
437 pub fn active(mut self, active: bool) -> Self {
438 self.active = active;
439 self
440 }
441
442 pub fn in_range(mut self, in_range: bool) -> Self {
444 self.in_range = in_range;
445 self
446 }
447
448 pub fn muted(mut self, muted: bool) -> Self {
451 self.muted = muted;
452 self
453 }
454
455 pub fn disabled(mut self, disabled: bool) -> Self {
456 self.disabled = disabled;
457 self
458 }
459
460 pub fn today(mut self, today: bool) -> Self {
461 self.today = today;
462 self
463 }
464
465 pub fn kind(&self) -> CalendarItemKind {
466 self.kind
467 }
468
469 pub fn is_active(&self) -> bool {
470 self.active
471 }
472
473 pub fn is_in_range(&self) -> bool {
474 self.in_range
475 }
476
477 pub fn is_muted(&self) -> bool {
478 self.muted
479 }
480
481 pub fn is_disabled(&self) -> bool {
482 self.disabled
483 }
484
485 pub fn is_today(&self) -> bool {
486 self.today
487 }
488}
489
490#[derive(IntoElement)]
492pub struct CalendarItem {
493 base: gpui::Stateful<gpui::Div>,
494 state: CalendarItemState,
495 style: StyleRefinement,
496 children: Vec<AnyElement>,
497}
498
499impl CalendarItem {
500 fn new(id: impl Into<ElementId>, state: CalendarItemState) -> Self {
501 Self {
502 base: div().id(id.into()),
503 state,
504 style: StyleRefinement::default(),
505 children: vec![],
506 }
507 }
508 pub fn item_state(&self) -> CalendarItemState {
509 self.state
510 }
511
512 pub fn clear_children(mut self) -> Self {
514 self.children.clear();
515 self
516 }
517}
518impl ParentElement for CalendarItem {
519 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
520 self.children.extend(elements);
521 }
522}
523impl Styled for CalendarItem {
524 fn style(&mut self) -> &mut StyleRefinement {
525 &mut self.style
526 }
527}
528impl InteractiveElement for CalendarItem {
529 fn interactivity(&mut self) -> &mut gpui::Interactivity {
530 self.base.interactivity()
531 }
532}
533impl StatefulInteractiveElement for CalendarItem {}
534impl RenderOnce for CalendarItem {
535 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
536 self.base.children(self.children).refine_style(&self.style)
537 }
538}
539
540type ItemRenderer =
541 Rc<dyn Fn(CalendarItem, CalendarItemState, &mut Window, &mut App) -> AnyElement>;
542type Labeler = Rc<dyn Fn(CalendarItemKind, i32) -> SharedString>;
543
544#[derive(IntoElement)]
549pub struct Calendar {
550 id: ElementId,
551 state: Entity<CalendarState>,
552 number_of_months: usize,
553 first_day_of_week: Weekday,
554 style: StyleRefinement,
555 item: ItemRenderer,
556 label: Labeler,
557}
558
559impl Calendar {
560 pub fn new(id: impl Into<ElementId>, state: &Entity<CalendarState>) -> Self {
561 Self {
562 id: id.into(),
563 state: state.clone(),
564 number_of_months: 1,
565 first_day_of_week: Weekday::Sun,
566 style: StyleRefinement::default(),
567 item: Rc::new(|item, _, _, _| item.into_any_element()),
568 label: Rc::new(|kind, value| match kind {
569 CalendarItemKind::Previous => "‹".into(),
570 CalendarItemKind::Next => "›".into(),
571 CalendarItemKind::Weekday => value.to_string().into(),
572 _ => value.to_string().into(),
573 }),
574 }
575 }
576 pub fn number_of_months(mut self, count: usize) -> Self {
577 self.number_of_months = count.max(1);
578 self
579 }
580 pub fn first_day_of_week(mut self, day: Weekday) -> Self {
581 self.first_day_of_week = day;
582 self
583 }
584 pub fn item(
585 mut self,
586 render: impl Fn(CalendarItem, CalendarItemState, &mut Window, &mut App) -> AnyElement + 'static,
587 ) -> Self {
588 self.item = Rc::new(render);
589 self
590 }
591 pub fn label(
592 mut self,
593 label: impl Fn(CalendarItemKind, i32) -> SharedString + 'static,
594 ) -> Self {
595 self.label = Rc::new(label);
596 self
597 }
598
599 fn render_item(
600 &self,
601 id: impl Into<ElementId>,
602 state: CalendarItemState,
603 value: i32,
604 window: &mut Window,
605 cx: &mut App,
606 ) -> AnyElement {
607 let label = (self.label)(state.kind(), value);
608 (self.item)(CalendarItem::new(id, state).child(label), state, window, cx)
609 }
610}
611impl Styled for Calendar {
612 fn style(&mut self) -> &mut StyleRefinement {
613 &mut self.style
614 }
615}
616
617impl RenderOnce for Calendar {
618 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
619 let count = self.number_of_months;
620 self.state
621 .update(cx, |s, cx| s.set_number_of_months(count, window, cx));
622 let view = self.state.read(cx).view();
623 let mut header = h_flex().items_center().justify_between().child({
624 let st = CalendarItemState::new(CalendarItemKind::Previous).disabled(
625 view.is_month() || (view.is_year() && !self.state.read(cx).has_prev_year_page()),
626 );
627 let mut item = CalendarItem::new("calendar-prev", st).child((self.label)(st.kind(), 0));
628 if !st.is_disabled() {
629 let entity = self.state.clone();
630 item = item.on_click(move |_, _window, cx| {
631 entity.update(cx, |s, cx| {
632 if s.view().is_day() {
633 s.prev_month();
634 } else {
635 s.prev_year_page();
636 }
637 cx.notify();
638 })
639 });
640 }
641 (self.item)(item, st, window, cx)
642 });
643 if count == 1 {
644 let (month, year) = {
645 let s = self.state.read(cx);
646 (s.current_month() as i32, s.current_year())
647 };
648 for (kind, value, active) in [
649 (CalendarItemKind::MonthToggle, month, view.is_month()),
650 (CalendarItemKind::YearToggle, year, view.is_year()),
651 ] {
652 let st = CalendarItemState::new(kind).active(active);
653 let entity = self.state.clone();
654 let mut item = CalendarItem::new(format!("calendar-{kind:?}"), st)
655 .child((self.label)(kind, value));
656 item = item.on_click(move |_, _, cx| {
657 entity.update(cx, |s, cx| {
658 s.set_view(
659 if s.view()
660 == match kind {
661 CalendarItemKind::MonthToggle => CalendarView::Month,
662 _ => CalendarView::Year,
663 }
664 {
665 CalendarView::Day
666 } else {
667 match kind {
668 CalendarItemKind::MonthToggle => CalendarView::Month,
669 _ => CalendarView::Year,
670 }
671 },
672 );
673 cx.notify();
674 })
675 });
676 header = header.child((self.item)(item, st, window, cx));
677 }
678 } else {
679 for offset in 0..count {
680 let (y, m) = self.state.read(cx).offset_year_month(offset);
681 header = header.child(
682 div().text_sm().font_medium().child(
683 v_flex()
684 .items_center()
685 .child((self.label)(CalendarItemKind::MonthToggle, m as i32))
686 .child(y.to_string()),
687 ),
688 );
689 }
690 }
691 header = header.child({
692 let st = CalendarItemState::new(CalendarItemKind::Next).disabled(
693 view.is_month() || (view.is_year() && !self.state.read(cx).has_next_year_page()),
694 );
695 let mut item = CalendarItem::new("calendar-next", st).child((self.label)(st.kind(), 0));
696 if !st.is_disabled() {
697 let entity = self.state.clone();
698 item = item.on_click(move |_, _, cx| {
699 entity.update(cx, |s, cx| {
700 if s.view().is_day() {
701 s.next_month()
702 } else {
703 s.next_year_page();
704 }
705 cx.notify();
706 })
707 });
708 }
709 (self.item)(item, st, window, cx)
710 });
711
712 let mut body = match picker_grid_layout(view) {
713 None => h_flex().justify_around(),
714 Some((columns, horizontal_gap)) => {
715 div().grid().grid_cols(columns).gap_x(px(horizontal_gap))
716 }
717 };
718 if view.is_day() {
719 for offset in 0..count {
720 let (year, month_number) = self.state.read(cx).offset_year_month(offset);
721 let weeks = days_in_month(year, month_number, self.first_day_of_week);
722 let mut month = v_flex();
723 let mut header_row = h_flex();
724 for weekday in 0..7 {
725 let st = CalendarItemState::new(CalendarItemKind::Weekday)
726 .muted(true)
727 .disabled(true);
728 header_row = header_row.child(self.render_item(
729 format!("weekday-{offset}-{weekday}"),
730 st,
731 (weekday + self.first_day_of_week.num_days_from_sunday() as i32) % 7,
732 window,
733 cx,
734 ));
735 }
736 month = month.child(header_row);
737 for (week_index, week) in weeks.iter().enumerate() {
738 let mut week_row = h_flex();
739 for date in week {
740 let date = *date;
741 let st = {
742 let s = self.state.read(cx);
743 let (_, m) = s.offset_year_month(offset);
744 let disabled =
745 s.disabled_matcher_ref().is_some_and(|x| x.matched(&date));
746 CalendarItemState::new(CalendarItemKind::Day)
747 .active(s.date().is_active(&date))
748 .in_range(s.date().is_in_range(&date))
749 .muted(date.month() != m || disabled)
750 .disabled(disabled)
751 .today(date == s.today())
752 };
753 let mut item =
754 CalendarItem::new(format!("calendar-{date}-{offset}-{week_index}"), st)
755 .child((self.label)(st.kind(), date.day() as i32));
756 if !st.is_disabled() {
757 let entity = self.state.clone();
758 item = item.on_click(move |_, _, cx| {
759 entity.update(cx, |s, cx| {
760 s.activate_date(date, cx);
761 })
762 });
763 }
764 week_row = week_row.child((self.item)(item, st, window, cx));
765 }
766 month = month.child(week_row);
767 }
768 body = body.child(month);
769 }
770 } else if view.is_month() {
771 let current = self.state.read(cx).current_month();
772 for month in 1..=12u8 {
773 let st = CalendarItemState::new(CalendarItemKind::Month).active(month == current);
774 let entity = self.state.clone();
775 let item = CalendarItem::new(format!("calendar-month-{month}"), st)
776 .child((self.label)(st.kind(), month as i32))
777 .on_click(move |_, _, cx| {
778 entity.update(cx, |s, cx| {
779 s.select_month(month);
780 cx.notify();
781 })
782 });
783 body = body.child((self.item)(item, st, window, cx));
784 }
785 } else {
786 let current = self.state.read(cx).current_year();
787 let years = self.state.read(cx).years_on_page().to_vec();
788 for year in years {
789 let st = CalendarItemState::new(CalendarItemKind::Year).active(year == current);
790 let entity = self.state.clone();
791 let item = CalendarItem::new(format!("calendar-year-{year}"), st)
792 .child((self.label)(st.kind(), year))
793 .on_click(move |_, _, cx| {
794 entity.update(cx, |s, cx| {
795 s.select_year(year);
796 cx.notify();
797 })
798 });
799 body = body.child((self.item)(item, st, window, cx));
800 }
801 }
802 v_flex()
803 .id(self.id)
804 .track_focus(&self.state.read(cx).focus_handle)
805 .child(header)
806 .child(body)
807 .refine_style(&self.style)
808 }
809}
810
811fn days_in_month(year: i32, month: u32, first_day: Weekday) -> Vec<Vec<NaiveDate>> {
812 let total = year as i64 * 12 + month as i64 - 1;
813 let year = total.div_euclid(12) as i32;
814 let month = total.rem_euclid(12) as u32 + 1;
815 let first = NaiveDate::from_ymd_opt(year, month, 1).unwrap();
816 let next = if month == 12 {
817 NaiveDate::from_ymd_opt(year + 1, 1, 1).unwrap()
818 } else {
819 NaiveDate::from_ymd_opt(year, month + 1, 1).unwrap()
820 };
821 let offset =
822 (first.weekday().num_days_from_sunday() + 7 - first_day.num_days_from_sunday()) % 7;
823 let start = first - chrono::Duration::days(offset as i64);
824 let count = ((next - start).num_days() as usize).div_ceil(7) * 7;
825 (0..count)
826 .map(|n| start + chrono::Duration::days(n as i64))
827 .collect::<Vec<_>>()
828 .chunks(7)
829 .map(<[_]>::to_vec)
830 .collect()
831}
832
833#[cfg(test)]
834mod tests {
835 use std::{cell::RefCell, rc::Rc};
836
837 use gpui::{AppContext as _, Context, Entity, IntoElement, Render, Subscription, Window};
838
839 use super::*;
840
841 struct EventHarness {
842 calendar: Entity<CalendarState>,
843 events: Rc<RefCell<Vec<Date>>>,
844 _subscription: Option<Subscription>,
845 }
846 impl EventHarness {
847 fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
848 let calendar = cx.new(|cx| CalendarState::new(window, cx));
849 let events = Rc::new(RefCell::new(Vec::new()));
850 let mut this = Self {
851 calendar: calendar.clone(),
852 events: events.clone(),
853 _subscription: None,
854 };
855 this._subscription = Some(cx.subscribe(&calendar, move |_, _, event, _| {
856 let CalendarEvent::Selected(date) = event;
857 events.borrow_mut().push(*date);
858 }));
859 this
860 }
861 }
862 impl Render for EventHarness {
863 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
864 Empty
865 }
866 }
867 fn state(cx: &mut gpui::TestAppContext, date: Date) -> gpui::Entity<CalendarState> {
868 let (state, _) = cx.add_window_view(CalendarState::new);
869 state.update(cx, |state, _| {
870 state.date = date;
871 });
872 state
873 }
874 #[gpui::test]
875 fn range_selection_restarts_and_completes(cx: &mut gpui::TestAppContext) {
876 let s = state(cx, Date::Range(None, None));
877 let a = NaiveDate::from_ymd_opt(2025, 2, 10).unwrap();
878 let b = NaiveDate::from_ymd_opt(2025, 2, 12).unwrap();
879 s.update(cx, |s, _| {
880 assert!(!s.select_date(a));
881 assert!(s.select_date(b));
882 });
883 assert_eq!(
884 s.read_with(cx, |s, _| s.date()),
885 Date::Range(Some(a), Some(b))
886 );
887 s.update(cx, |s, _| assert!(!s.select_date(a)));
888 assert_eq!(s.read_with(cx, |s, _| s.date()), Date::Range(Some(a), None));
889 }
890 #[gpui::test]
891 fn disabled_date_is_rejected(cx: &mut gpui::TestAppContext) {
892 let s = state(cx, Date::Single(None));
893 s.update(cx, |s, _| {
894 s.disabled_matcher = Some(Rc::new(Matcher::range(
895 Some(NaiveDate::from_ymd_opt(2025, 1, 1).unwrap()),
896 Some(NaiveDate::from_ymd_opt(2025, 1, 31).unwrap()),
897 )))
898 });
899 s.update(cx, |s, _| {
900 assert!(!s.select_date(NaiveDate::from_ymd_opt(2025, 1, 2).unwrap()))
901 });
902 }
903 #[gpui::test]
904 fn month_navigation_crosses_year(cx: &mut gpui::TestAppContext) {
905 let s = state(
906 cx,
907 Date::Single(Some(NaiveDate::from_ymd_opt(2025, 1, 1).unwrap())),
908 );
909 s.update(cx, |s, _| {
910 s.apply_date(Date::Single(Some(
911 NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(),
912 )));
913 s.prev_month();
914 });
915 assert_eq!(
916 s.read_with(cx, |s, _| (s.current_year(), s.current_month())),
917 (2024, 12)
918 );
919 s.update(cx, |s, _| s.next_month());
920 assert_eq!(
921 s.read_with(cx, |s, _| (s.current_year(), s.current_month())),
922 (2025, 1)
923 );
924 }
925
926 #[gpui::test]
927 fn six_week_month_is_not_truncated(cx: &mut gpui::TestAppContext) {
928 let s = state(
929 cx,
930 Date::Single(Some(NaiveDate::from_ymd_opt(2025, 8, 1).unwrap())),
931 );
932 s.update(cx, |s, _| {
933 s.apply_date(Date::Single(Some(
934 NaiveDate::from_ymd_opt(2025, 8, 1).unwrap(),
935 )));
936 assert_eq!(s.month_days().len(), 1);
937 assert_eq!(s.month_days()[0].len(), 6);
938 assert_eq!(s.days().len(), 6);
939 assert_eq!(s.month_days()[0][5][0].day(), 31);
940 });
941 }
942
943 #[gpui::test]
944 fn day_month_and_year_views_have_complete_transitions(cx: &mut gpui::TestAppContext) {
945 let s = state(cx, Date::Single(None));
946 s.update(cx, |s, _| {
947 assert_eq!(s.view(), CalendarView::Day);
948 s.set_view(CalendarView::Month);
949 s.select_month(11);
950 assert_eq!((s.view(), s.current_month()), (CalendarView::Day, 11));
951 s.set_view(CalendarView::Year);
952 s.select_year(2032);
953 assert_eq!((s.view(), s.current_year()), (CalendarView::Day, 2032));
954 });
955 }
956
957 #[test]
958 fn picker_views_use_stable_grid_layouts() {
959 assert_eq!(picker_grid_layout(CalendarView::Month), Some((3, 4.)));
960 assert_eq!(picker_grid_layout(CalendarView::Year), Some((5, 4.)));
961 assert_eq!(picker_grid_layout(CalendarView::Day), None);
962 }
963
964 #[gpui::test]
965 fn year_page_navigation_respects_both_bounds(cx: &mut gpui::TestAppContext) {
966 let s = state(cx, Date::Single(None));
967 s.update(cx, |s, _| {
968 s.apply_year_range((2000, 2041));
969 while s.prev_year_page() {}
970 assert!(!s.has_prev_year_page());
971 assert!(!s.prev_year_page());
972 assert!(s.next_year_page());
973 while s.next_year_page() {}
974 assert!(!s.has_next_year_page());
975 assert!(!s.next_year_page());
976 });
977 }
978
979 #[gpui::test]
980 fn activation_emits_only_for_complete_enabled_values(cx: &mut gpui::TestAppContext) {
981 let (harness, _) = cx.add_window_view(EventHarness::new);
982 let s = harness.read_with(cx, |h, _| h.calendar.clone());
983 s.update(cx, |s, _| s.date = Date::Range(None, None));
984 let start = NaiveDate::from_ymd_opt(2025, 4, 4).unwrap();
985 let end = NaiveDate::from_ymd_opt(2025, 4, 8).unwrap();
986 s.update(cx, |s, cx| {
987 assert!(!s.activate_date(start, cx));
988 assert!(s.activate_date(end, cx));
989 assert_eq!(s.date(), Date::Range(Some(start), Some(end)));
990 s.set_disabled_matcher_shared(Some(Rc::new(Matcher::custom(move |d| *d == start))));
991 assert!(!s.activate_date(start, cx));
992 assert_eq!(s.date(), Date::Range(Some(start), Some(end)));
993 });
994 assert_eq!(
995 harness.read_with(cx, |h, _| h.events.borrow().clone()),
996 vec![Date::Range(Some(start), Some(end))]
997 );
998
999 s.update(cx, |s, cx| {
1000 s.date = Date::Single(None);
1001 assert!(s.activate_date(end, cx));
1002 assert_eq!(s.date(), Date::Single(Some(end)));
1003 });
1004 assert_eq!(
1005 harness.read_with(cx, |h, _| h.events.borrow().clone()),
1006 vec![Date::Range(Some(start), Some(end)), Date::Single(Some(end))]
1007 );
1008 }
1009}