Skip to main content

gpui_base/
calendar.rs

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