1use std::rc::Rc;
8
9use gpui::prelude::*;
10use gpui::{div, px, App, ElementId, FontWeight, IntoElement, SharedString, Window};
11
12use super::date::{month_grid, Date, Weekday};
13use crate::devtools::Probed;
14use crate::icon::{Icon, IconName};
15use crate::theme::{theme, Size};
16
17type SelectHandler = Rc<dyn Fn(Date, &mut Window, &mut App) + 'static>;
18type MonthHandler = Rc<dyn Fn(i32, u32, &mut Window, &mut App) + 'static>;
19
20fn cell_size(size: Size) -> f32 {
22 match size {
23 Size::Xs => 26.0,
24 Size::Sm => 30.0,
25 Size::Md => 34.0,
26 Size::Lg => 40.0,
27 Size::Xl => 46.0,
28 }
29}
30
31#[derive(IntoElement)]
34pub struct Calendar {
35 id: ElementId,
36 year: i32,
37 month: u32,
38 value: Option<Date>,
39 range: Option<(Date, Option<Date>)>,
40 min: Option<Date>,
41 max: Option<Date>,
42 week_start: Weekday,
43 size: Size,
44 on_select: Option<SelectHandler>,
45 on_month_change: Option<MonthHandler>,
46}
47
48impl Calendar {
49 pub fn new(id: impl Into<ElementId>) -> Self {
51 let today = Date::today();
52 Calendar {
53 id: id.into(),
54 year: today.year(),
55 month: today.month(),
56 value: None,
57 range: None,
58 min: None,
59 max: None,
60 week_start: Weekday::Sunday,
61 size: Size::Sm,
62 on_select: None,
63 on_month_change: None,
64 }
65 }
66
67 pub fn month(mut self, year: i32, month: u32) -> Self {
69 self.year = year;
70 self.month = month.clamp(1, 12);
71 self
72 }
73
74 pub fn value(mut self, value: Option<Date>) -> Self {
76 self.value = value;
77 self
78 }
79
80 pub fn range(mut self, start: Date, end: Option<Date>) -> Self {
82 self.range = Some((start, end));
83 self
84 }
85
86 pub fn min(mut self, min: Date) -> Self {
87 self.min = Some(min);
88 self
89 }
90
91 pub fn max(mut self, max: Date) -> Self {
92 self.max = Some(max);
93 self
94 }
95
96 pub fn week_start(mut self, week_start: Weekday) -> Self {
97 self.week_start = week_start;
98 self
99 }
100
101 pub fn size(mut self, size: Size) -> Self {
102 self.size = size;
103 self
104 }
105
106 pub fn on_select(mut self, handler: impl Fn(Date, &mut Window, &mut App) + 'static) -> Self {
108 self.on_select = Some(Rc::new(handler));
109 self
110 }
111
112 pub fn on_month_change(
114 mut self,
115 handler: impl Fn(i32, u32, &mut Window, &mut App) + 'static,
116 ) -> Self {
117 self.on_month_change = Some(Rc::new(handler));
118 self
119 }
120}
121
122impl RenderOnce for Calendar {
123 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
124 let t = theme(cx);
125 let cell = cell_size(self.size);
126 let font = t.font_size(self.size);
127 let radius = t.radius(Size::Xs) + 2.0;
128 let text_color = t.text().hsla();
129 let dimmed = t.dimmed().hsla();
130 let surface_hover = t.surface_hover().hsla();
131 let accent = t.primary();
132 let accent_bg = accent.hsla();
133 let accent_fg = accent.contrasting().hsla();
134 let in_range_bg = accent.alpha(0.12);
135
136 let today = Date::today();
137 let first = Date::new(self.year, self.month, 1).unwrap_or(today);
138 let title: SharedString = format!("{} {}", first.month_name(), self.year).into();
139
140 let mut header = div().flex().items_center().justify_between().child(
141 div()
142 .text_size(px(font))
143 .font_weight(FontWeight::MEDIUM)
144 .text_color(text_color)
145 .child(title),
146 );
147 let mut nav = div().flex().items_center().gap(px(4.0));
148 for (key, icon, delta) in [
149 ("guise-cal-prev", IconName::ChevronLeft, -1),
150 ("guise-cal-next", IconName::ChevronRight, 1),
151 ] {
152 let target = first.add_months(delta);
153 let handler = self.on_month_change.clone();
154 nav = nav.child(
155 div()
156 .id(key)
157 .flex()
158 .items_center()
159 .justify_center()
160 .w(px(cell * 0.8))
161 .h(px(cell * 0.8))
162 .rounded(px(radius))
163 .text_color(dimmed)
164 .hover(move |s| s.bg(surface_hover))
165 .child(Icon::new(icon).size(Size::Sm))
166 .on_click(move |_ev, window, cx| {
167 if let Some(handler) = &handler {
168 handler(target.year(), target.month(), window, cx);
169 }
170 }),
171 );
172 }
173 header = header.child(nav);
174
175 let mut weekdays = div().flex();
176 for i in 0..7 {
177 let day = Weekday::from_index(self.week_start.index() + i);
178 weekdays = weekdays.child(
179 div()
180 .w(px(cell))
181 .h(px(cell * 0.8))
182 .flex()
183 .items_center()
184 .justify_center()
185 .text_size(px(font - 3.0))
186 .text_color(dimmed)
187 .child(SharedString::new_static(day.short())),
188 );
189 }
190
191 let grid = month_grid(self.year, self.month, self.week_start);
192 let (range_start, range_end) = match self.range {
193 Some((start, end)) => (Some(start), end),
194 None => (None, None),
195 };
196
197 let mut days = div().flex().flex_col();
198 for week in grid.chunks(7) {
199 let mut row = div().flex();
200 for date in week {
201 let date = *date;
202 let outside = date.month() != self.month;
203 let disabled =
204 self.min.is_some_and(|min| date < min) || self.max.is_some_and(|max| date > max);
205 let selected =
206 self.value == Some(date) || range_start == Some(date) || range_end == Some(date);
207 let in_range = match (range_start, range_end) {
208 (Some(start), Some(end)) => date > start && date < end,
209 _ => false,
210 };
211
212 let mut day = div()
213 .id(("guise-cal-day", date.to_days() as usize))
214 .w(px(cell))
215 .h(px(cell))
216 .flex()
217 .items_center()
218 .justify_center()
219 .rounded(px(radius))
220 .text_size(px(font - 1.0))
221 .text_color(if outside { dimmed } else { text_color })
222 .child(SharedString::from(date.day().to_string()));
223
224 if selected {
225 day = day
226 .bg(accent_bg)
227 .text_color(accent_fg)
228 .font_weight(FontWeight::MEDIUM);
229 } else if in_range {
230 day = day.bg(in_range_bg);
231 } else if date == today {
232 day = day.border_1().border_color(accent_bg);
233 }
234
235 if disabled {
236 day = day.opacity(0.35);
237 } else {
238 if !selected {
239 day = day.hover(move |s| s.bg(surface_hover));
240 }
241 let handler = self.on_select.clone();
242 day = day.on_click(move |_ev, window, cx| {
243 if let Some(handler) = &handler {
244 handler(date, window, cx);
245 }
246 });
247 }
248 row = row.child(day);
249 }
250 days = days.child(row);
251 }
252
253 div()
254 .id(self.id)
255 .flex()
256 .flex_col()
257 .gap(px(6.0))
258 .child(header)
259 .child(weekdays)
260 .child(days)
261 .probe("Calendar")
262 }
263}