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#[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 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(state.kind() != CalendarItemKind::Weekday, |this| {
122 this.text_sm()
123 })
124 .when(state.kind() == CalendarItemKind::Weekday, |this| {
125 this.text_xs()
126 .font_normal()
127 .text_color(cx.theme().muted_foreground)
128 })
129 .when(uses_compact_text(state.kind()), |this| this.text_xs())
130 .when(
131 matches!(
132 state.kind(),
133 CalendarItemKind::MonthToggle | CalendarItemKind::YearToggle
134 ),
135 |this| this.text_sm().font_medium(),
136 )
137 .when(
138 matches!(
139 state.kind(),
140 CalendarItemKind::Month | CalendarItemKind::MonthToggle
141 ),
142 |this| {
143 this.when(state.kind() == CalendarItemKind::Month, |this| {
144 this.w_full().my_1()
145 })
146 .when(state.kind() == CalendarItemKind::MonthToggle, |this| {
147 this.w_auto().px_2()
148 })
149 },
150 )
151 .when(
152 matches!(
153 state.kind(),
154 CalendarItemKind::Year | CalendarItemKind::YearToggle
155 ),
156 |this| {
157 this.when(state.kind() == CalendarItemKind::Year, |this| {
158 this.w_full().my_1()
159 })
160 .when(state.kind() == CalendarItemKind::YearToggle, |this| {
161 this.w_auto().px_2()
162 })
163 },
164 )
165 .when(state.is_muted(), |this| {
166 this.text_color(cx.theme().muted_foreground)
167 .when(state.is_disabled(), |this| this.opacity(0.5))
168 })
169 .when(state.is_in_range(), |this| {
170 this.bg(cx.theme().accent)
171 .text_color(cx.theme().accent_foreground)
172 })
173 .when(
174 !state.is_active()
175 && !state.is_disabled()
176 && state.kind() != CalendarItemKind::Weekday,
177 |this| {
178 this.hover(|this| {
179 this.bg(cx.theme().tokens.secondary_hover)
180 .text_color(cx.theme().foreground)
181 })
182 },
183 )
184 .when(state.is_active(), |this| {
185 this.bg(cx.theme().tokens.primary)
186 .text_color(cx.theme().primary_foreground)
187 })
188 .when(state.is_today() && !state.is_active(), |this| {
189 this.bg(cx.theme().accent)
190 .text_color(cx.theme().accent_foreground)
191 })
192 .into_any_element()
193 })
194 .border_1()
195 .border_color(cx.theme().border)
196 .rounded(cx.theme().radius_lg)
197 .p_3()
198 .gap_0p5()
199 .map(|this| match size {
200 Size::Small => this.w(px(220.) * month_count),
201 Size::Large => this.w(px(304.) * month_count),
202 _ => this.w(px(248.) * month_count),
203 })
204 .refine_style(&self.style)
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::{CalendarItemKind, Date, uses_compact_text};
211 use chrono::NaiveDate;
212
213 #[test]
214 fn date_display_is_stable() {
215 assert_eq!(
216 Date::Single(Some(NaiveDate::from_ymd_opt(2024, 8, 3).unwrap())).to_string(),
217 "2024-08-03"
218 );
219 assert_eq!(Date::Single(None).to_string(), "nil");
220 }
221
222 #[test]
223 fn only_month_options_use_compact_text() {
224 assert!(uses_compact_text(CalendarItemKind::Month));
225 assert!(!uses_compact_text(CalendarItemKind::MonthToggle));
226 assert!(!uses_compact_text(CalendarItemKind::Day));
227 }
228}