Skip to main content

gpui_component/time/
calendar.rs

1use chrono::Weekday;
2use gpui::{
3    App, ElementId, Entity, InteractiveElement, IntoElement, ParentElement, RenderOnce,
4    SharedString, StyleRefinement, Styled, Window, prelude::FluentBuilder as _, px,
5};
6use rust_i18n::t;
7
8use crate::{ActiveTheme, Icon, IconName, Sizable, Size, StyledExt as _};
9
10use gpui_base::{Calendar as BaseCalendar, CalendarItemKind};
11pub use gpui_base::{CalendarEvent, CalendarState, Date, Matcher};
12
13fn month_name(month: i32) -> SharedString {
14    match month {
15        1 => t!("Calendar.month.January"),
16        2 => t!("Calendar.month.February"),
17        3 => t!("Calendar.month.March"),
18        4 => t!("Calendar.month.April"),
19        5 => t!("Calendar.month.May"),
20        6 => t!("Calendar.month.June"),
21        7 => t!("Calendar.month.July"),
22        8 => t!("Calendar.month.August"),
23        9 => t!("Calendar.month.September"),
24        10 => t!("Calendar.month.October"),
25        11 => t!("Calendar.month.November"),
26        12 => t!("Calendar.month.December"),
27        _ => "".into(),
28    }
29    .into()
30}
31
32fn uses_compact_text(kind: CalendarItemKind) -> bool {
33    kind == CalendarItemKind::Month
34}
35
36/// Styled facade for the complete behavior and structure in `gpui-base`.
37#[derive(IntoElement)]
38pub struct Calendar {
39    id: ElementId,
40    size: Size,
41    state: Entity<CalendarState>,
42    style: StyleRefinement,
43    number_of_months: usize,
44    first_day_of_week: Weekday,
45}
46
47impl Calendar {
48    pub fn new(state: &Entity<CalendarState>) -> Self {
49        Self {
50            id: ("calendar", state.entity_id()).into(),
51            size: Size::default(),
52            state: state.clone(),
53            style: StyleRefinement::default(),
54            number_of_months: 1,
55            first_day_of_week: Weekday::Sun,
56        }
57    }
58    pub fn number_of_months(mut self, count: usize) -> Self {
59        self.number_of_months = count;
60        self
61    }
62    /// Set the first day of the week for the calendar.
63    pub fn first_day_of_week(mut self, day: Weekday) -> Self {
64        self.first_day_of_week = day;
65        self
66    }
67}
68impl Sizable for Calendar {
69    fn with_size(mut self, size: impl Into<Size>) -> Self {
70        self.size = size.into();
71        self
72    }
73}
74impl Styled for Calendar {
75    fn style(&mut self) -> &mut StyleRefinement {
76        &mut self.style
77    }
78}
79
80impl RenderOnce for Calendar {
81    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
82        let size = self.size;
83        let month_count = self.number_of_months.max(1) as f32;
84        BaseCalendar::new(self.id, &self.state)
85            .number_of_months(self.number_of_months)
86            .first_day_of_week(self.first_day_of_week)
87            .label(|kind, value| match kind {
88                CalendarItemKind::Previous => "‹".into(),
89                CalendarItemKind::Next => "›".into(),
90                CalendarItemKind::MonthToggle | CalendarItemKind::Month => month_name(value),
91                CalendarItemKind::Weekday => match value {
92                    0 => t!("Calendar.week.0"),
93                    1 => t!("Calendar.week.1"),
94                    2 => t!("Calendar.week.2"),
95                    3 => t!("Calendar.week.3"),
96                    4 => t!("Calendar.week.4"),
97                    5 => t!("Calendar.week.5"),
98                    _ => t!("Calendar.week.6"),
99                }
100                .into(),
101                _ => value.to_string().into(),
102            })
103            .item(move |item, state, _, cx| {
104                let item = match state.kind() {
105                    CalendarItemKind::Previous => item
106                        .clear_children()
107                        .child(Icon::new(IconName::ChevronLeft).size_4()),
108                    CalendarItemKind::Next => item
109                        .clear_children()
110                        .child(Icon::new(IconName::ChevronRight).size_4()),
111                    _ => item,
112                };
113                item.map(|this| match size {
114                    Size::Small => this.size_7().rounded(cx.theme().radius / 2.),
115                    Size::Large => this.size_10().rounded(cx.theme().radius * 2.),
116                    _ => this.size_8().rounded(cx.theme().radius),
117                })
118                .flex()
119                .items_center()
120                .justify_center()
121                .when(
122                    state.kind() != CalendarItemKind::Weekday,
123                    |this| match size {
124                        Size::Small => this.text_xs(),
125                        _ => this.text_sm(),
126                    },
127                )
128                .when(state.kind() == CalendarItemKind::Weekday, |this| {
129                    this.text_xs()
130                        .font_normal()
131                        .text_color(cx.theme().muted_foreground)
132                })
133                .when(uses_compact_text(state.kind()), |this| this.text_xs())
134                .when(
135                    matches!(
136                        state.kind(),
137                        CalendarItemKind::MonthToggle | CalendarItemKind::YearToggle
138                    ),
139                    |this| this.text_sm().font_medium(),
140                )
141                .when(
142                    matches!(
143                        state.kind(),
144                        CalendarItemKind::Month | CalendarItemKind::MonthToggle
145                    ),
146                    |this| {
147                        this.when(state.kind() == CalendarItemKind::Month, |this| {
148                            this.w_full().my_1()
149                        })
150                        .when(state.kind() == CalendarItemKind::MonthToggle, |this| {
151                            this.w_auto().px_2()
152                        })
153                    },
154                )
155                .when(
156                    matches!(
157                        state.kind(),
158                        CalendarItemKind::Year | CalendarItemKind::YearToggle
159                    ),
160                    |this| {
161                        this.when(state.kind() == CalendarItemKind::Year, |this| {
162                            this.w_full().my_1()
163                        })
164                        .when(state.kind() == CalendarItemKind::YearToggle, |this| {
165                            this.w_auto().px_2()
166                        })
167                    },
168                )
169                .when(state.is_muted(), |this| {
170                    this.text_color(cx.theme().muted_foreground)
171                        .when(state.is_disabled(), |this| this.opacity(0.5))
172                })
173                .when(state.is_in_range(), |this| {
174                    this.bg(cx.theme().accent)
175                        .text_color(cx.theme().accent_foreground)
176                })
177                .when(
178                    !state.is_active()
179                        && !state.is_disabled()
180                        && state.kind() != CalendarItemKind::Weekday,
181                    |this| {
182                        this.hover(|this| {
183                            this.bg(cx.theme().tokens.secondary_hover)
184                                .text_color(cx.theme().foreground)
185                        })
186                    },
187                )
188                .when(state.is_active(), |this| {
189                    this.bg(cx.theme().tokens.primary)
190                        .text_color(cx.theme().primary_foreground)
191                })
192                .when(state.is_today() && !state.is_active(), |this| {
193                    this.bg(cx.theme().accent)
194                        .text_color(cx.theme().accent_foreground)
195                })
196                .into_any_element()
197            })
198            .border_1()
199            .border_color(cx.theme().border)
200            .rounded(cx.theme().radius_lg)
201            .p_3()
202            .gap_0p5()
203            .map(|this| match size {
204                Size::Small => this.w(px(220.) * month_count),
205                Size::Large => this.w(px(304.) * month_count),
206                _ => this.w(px(248.) * month_count),
207            })
208            .refine_style(&self.style)
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::{CalendarItemKind, Date, uses_compact_text};
215    use chrono::NaiveDate;
216
217    #[test]
218    fn date_display_is_stable() {
219        assert_eq!(
220            Date::Single(Some(NaiveDate::from_ymd_opt(2024, 8, 3).unwrap())).to_string(),
221            "2024-08-03"
222        );
223        assert_eq!(Date::Single(None).to_string(), "nil");
224    }
225
226    #[test]
227    fn only_month_options_use_compact_text() {
228        assert!(uses_compact_text(CalendarItemKind::Month));
229        assert!(!uses_compact_text(CalendarItemKind::MonthToggle));
230        assert!(!uses_compact_text(CalendarItemKind::Day));
231    }
232}