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(
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}