Skip to main content

gpui_base/
calendar.rs

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/// A controlled calendar value.
12#[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    /// Activates a day item and emits [`CalendarEvent::Selected`] once the
258    /// controlled value is complete. This is the single pointer/keyboard
259    /// activation path used by the calendar root.
260    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    /// Calendar weeks grouped by visible month. This preserves six-week
309    /// months and is the preferred rendering API.
310    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/// The semantic kind of a calendar control rendered by [`Calendar`].
397#[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/// State exposed to a calendar item slot. Applications may use it solely to
410/// decorate the unstyled primitive; all interaction remains owned by base.
411///
412/// The fields are private and reached through the methods below, so that a new
413/// one can be added without breaking the item slots.
414#[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    /// Create a state for `kind`, with every flag off.
426    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    /// Set whether the item is between the two ends of a range selection.
443    pub fn in_range(mut self, in_range: bool) -> Self {
444        self.in_range = in_range;
445        self
446    }
447
448    /// Set whether the item is shown as secondary, e.g.: a weekday header or a
449    /// day that belongs to the neighboring month.
450    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/// An unstyled, pre-wired calendar item passed to the item slot.
491#[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    /// Remove the default label so a styled facade can provide custom content.
513    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/// Complete unstyled calendar structure and behavior.
545///
546/// Base owns navigation, view switching, grids, disabled/selection state and
547/// click handling. The UI crate only decorates the pre-wired item slot.
548#[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}