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