1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3pub enum WeekStart {
4 Sunday,
5 Monday,
6}
7
8use chrono::{
9 Datelike,
10 Local,
11 Month,
12 NaiveDate,
13};
14use freya_core::prelude::*;
15use torin::{
16 content::Content,
17 prelude::Alignment,
18 size::Size,
19};
20
21use crate::{
22 button::Button,
23 get_theme,
24 icons::arrow::ArrowIcon,
25 theming::component_themes::{
26 ButtonColorsThemePartialExt,
27 ButtonLayoutThemePartialExt,
28 CalendarTheme,
29 CalendarThemePartial,
30 },
31};
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub struct CalendarDate {
36 pub year: i32,
37 pub month: u32,
38 pub day: u32,
39}
40
41impl CalendarDate {
42 pub fn new(year: i32, month: u32, day: u32) -> Self {
43 Self { year, month, day }
44 }
45
46 pub fn now() -> Self {
48 let today = Local::now().date_naive();
49 Self {
50 year: today.year(),
51 month: today.month(),
52 day: today.day(),
53 }
54 }
55
56 fn days_in_month(year: i32, month: u32) -> u32 {
58 let next_month = if month == 12 { 1 } else { month + 1 };
59 let next_year = if month == 12 { year + 1 } else { year };
60 NaiveDate::from_ymd_opt(next_year, next_month, 1)
61 .and_then(|d| d.pred_opt())
62 .map(|d| d.day())
63 .unwrap_or(30)
64 }
65
66 fn first_day_of_month(year: i32, month: u32, week_start: WeekStart) -> u32 {
68 NaiveDate::from_ymd_opt(year, month, 1)
69 .map(|d| match week_start {
70 WeekStart::Sunday => d.weekday().num_days_from_sunday(),
71 WeekStart::Monday => d.weekday().num_days_from_monday(),
72 })
73 .unwrap_or(0)
74 }
75
76 fn month_name(month: u32) -> String {
78 Month::try_from(month as u8)
79 .map(|m| m.name().to_string())
80 .unwrap_or_else(|_| "Unknown".to_string())
81 }
82}
83
84#[cfg_attr(feature = "docs", doc = embed_doc_image::embed_image!("gallery_calendar", "images/gallery_calendar.png"))]
110#[derive(Clone, PartialEq)]
111pub struct Calendar {
112 pub(crate) theme: Option<CalendarThemePartial>,
113 selected: Option<CalendarDate>,
114 view_date: CalendarDate,
115 week_start: WeekStart,
116 on_change: Option<EventHandler<CalendarDate>>,
117 on_view_change: Option<EventHandler<CalendarDate>>,
118 key: DiffKey,
119}
120
121impl Default for Calendar {
122 fn default() -> Self {
123 Self::new()
124 }
125}
126
127impl Calendar {
128 pub fn new() -> Self {
129 Self {
130 theme: None,
131 selected: None,
132 view_date: CalendarDate::now(),
133 week_start: WeekStart::Monday,
134 on_change: None,
135 on_view_change: None,
136 key: DiffKey::None,
137 }
138 }
139
140 pub fn selected(mut self, selected: Option<CalendarDate>) -> Self {
141 self.selected = selected;
142 self
143 }
144
145 pub fn view_date(mut self, view_date: CalendarDate) -> Self {
146 self.view_date = view_date;
147 self
148 }
149
150 pub fn week_start(mut self, week_start: WeekStart) -> Self {
152 self.week_start = week_start;
153 self
154 }
155
156 pub fn on_change(mut self, on_change: impl Into<EventHandler<CalendarDate>>) -> Self {
157 self.on_change = Some(on_change.into());
158 self
159 }
160
161 pub fn on_view_change(mut self, on_view_change: impl Into<EventHandler<CalendarDate>>) -> Self {
162 self.on_view_change = Some(on_view_change.into());
163 self
164 }
165}
166
167impl KeyExt for Calendar {
168 fn write_key(&mut self) -> &mut DiffKey {
169 &mut self.key
170 }
171}
172
173impl Component for Calendar {
174 fn render(&self) -> impl IntoElement {
175 let theme = get_theme!(&self.theme, calendar);
176
177 let CalendarTheme {
178 background,
179 day_background,
180 day_hover_background,
181 day_selected_background,
182 color,
183 day_other_month_color,
184 header_color,
185 corner_radius,
186 padding,
187 day_corner_radius,
188 nav_button_hover_background,
189 } = theme;
190
191 let view_year = self.view_date.year;
192 let view_month = self.view_date.month;
193
194 let days_in_month = CalendarDate::days_in_month(view_year, view_month);
195 let first_day = CalendarDate::first_day_of_month(view_year, view_month, self.week_start);
196 let month_name = CalendarDate::month_name(view_month);
197
198 let prev_month = if view_month == 1 { 12 } else { view_month - 1 };
199 let prev_year = if view_month == 1 {
200 view_year - 1
201 } else {
202 view_year
203 };
204 let days_in_prev_month = CalendarDate::days_in_month(prev_year, prev_month);
205
206 let on_change = self.on_change.clone();
207 let on_view_change = self.on_view_change.clone();
208 let selected = self.selected;
209
210 let on_prev = EventHandler::from({
211 let on_view_change = on_view_change.clone();
212 move |_: Event<PressEventData>| {
213 if let Some(handler) = &on_view_change {
214 let new_month = if view_month == 1 { 12 } else { view_month - 1 };
215 let new_year = if view_month == 1 {
216 view_year - 1
217 } else {
218 view_year
219 };
220 handler.call(CalendarDate::new(new_year, new_month, 1));
221 }
222 }
223 });
224
225 let on_next = EventHandler::from({
226 let on_view_change = on_view_change.clone();
227 move |_: Event<PressEventData>| {
228 if let Some(handler) = &on_view_change {
229 let new_month = if view_month == 12 { 1 } else { view_month + 1 };
230 let new_year = if view_month == 12 {
231 view_year + 1
232 } else {
233 view_year
234 };
235 handler.call(CalendarDate::new(new_year, new_month, 1));
236 }
237 }
238 });
239
240 let nav_button = |on_press: EventHandler<Event<PressEventData>>, rotate| {
241 Button::new()
242 .flat()
243 .width(Size::px(32.))
244 .height(Size::px(32.))
245 .hover_background(nav_button_hover_background)
246 .on_press(on_press)
247 .child(
248 ArrowIcon::new()
249 .fill(color)
250 .width(Size::px(16.))
251 .height(Size::px(16.))
252 .rotate(rotate),
253 )
254 };
255
256 let weekday_names = match self.week_start {
257 WeekStart::Sunday => ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"],
258 WeekStart::Monday => ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"],
259 };
260
261 let header_cells = weekday_names.iter().map(|day_name| {
262 rect()
263 .width(Size::px(36.))
264 .height(Size::px(36.))
265 .center()
266 .child(label().text(*day_name).color(header_color).font_size(12.))
267 .into()
268 });
269
270 let total_cells = (first_day + days_in_month).div_ceil(7) * 7;
271 let day_cells = (0..total_cells).map(|i| {
272 let current_day = i as i32 - first_day as i32 + 1;
273
274 let (day, day_color, enabled) = if current_day < 1 {
275 let day = (days_in_prev_month as i32 + current_day) as u32;
276 (day, day_other_month_color, false)
277 } else if current_day as u32 > days_in_month {
278 let day = current_day as u32 - days_in_month;
279 (day, day_other_month_color, false)
280 } else {
281 (current_day as u32, color, true)
282 };
283
284 let date = CalendarDate::new(view_year, view_month, current_day as u32);
285 let is_selected = enabled && selected == Some(date);
286 let on_change = on_change.clone();
287
288 let (bg, hover_bg) = if is_selected {
289 (day_selected_background, day_selected_background)
290 } else if enabled {
291 (day_background, day_hover_background)
292 } else {
293 (Color::TRANSPARENT, Color::TRANSPARENT)
294 };
295
296 CalendarDay::new()
297 .key(day)
298 .day(day)
299 .background(bg)
300 .hover_background(hover_bg)
301 .color(day_color)
302 .corner_radius(day_corner_radius)
303 .enabled(enabled)
304 .maybe(enabled, |el| {
305 el.map(on_change, |el, on_change| {
306 el.on_press(move |_| on_change.call(date))
307 })
308 })
309 .into()
310 });
311
312 rect()
313 .background(background)
314 .corner_radius(corner_radius)
315 .padding(padding)
316 .width(Size::px(280.))
317 .child(
318 rect()
319 .horizontal()
320 .width(Size::fill())
321 .padding((0., 0., 8., 0.))
322 .cross_align(Alignment::center())
323 .content(Content::flex())
324 .child(nav_button(on_prev, 90.))
325 .child(
326 label()
327 .width(Size::flex(1.))
328 .text_align(TextAlign::Center)
329 .text(format!("{} {}", month_name, view_year))
330 .color(header_color)
331 .max_lines(1)
332 .font_size(16.),
333 )
334 .child(nav_button(on_next, -90.)),
335 )
336 .child(
337 rect()
338 .horizontal()
339 .content(Content::wrap())
340 .width(Size::fill())
341 .children(header_cells)
342 .children(day_cells),
343 )
344 }
345
346 fn render_key(&self) -> DiffKey {
347 self.key.clone().or(self.default_key())
348 }
349}
350
351#[derive(Clone, PartialEq)]
352struct CalendarDay {
353 day: u32,
354 background: Color,
355 hover_background: Color,
356 color: Color,
357 corner_radius: CornerRadius,
358 on_press: Option<EventHandler<Event<PressEventData>>>,
359 enabled: bool,
360 key: DiffKey,
361}
362
363impl CalendarDay {
364 fn new() -> Self {
365 Self {
366 day: 1,
367 background: Color::TRANSPARENT,
368 hover_background: Color::TRANSPARENT,
369 color: Color::BLACK,
370 corner_radius: CornerRadius::default(),
371 on_press: None,
372 enabled: true,
373 key: DiffKey::None,
374 }
375 }
376
377 fn day(mut self, day: u32) -> Self {
378 self.day = day;
379 self
380 }
381
382 fn background(mut self, background: Color) -> Self {
383 self.background = background;
384 self
385 }
386
387 fn hover_background(mut self, hover_background: Color) -> Self {
388 self.hover_background = hover_background;
389 self
390 }
391
392 fn color(mut self, color: Color) -> Self {
393 self.color = color;
394 self
395 }
396
397 fn corner_radius(mut self, corner_radius: CornerRadius) -> Self {
398 self.corner_radius = corner_radius;
399 self
400 }
401
402 fn on_press(mut self, on_press: impl Into<EventHandler<Event<PressEventData>>>) -> Self {
403 self.on_press = Some(on_press.into());
404 self
405 }
406
407 fn enabled(mut self, enabled: bool) -> Self {
408 self.enabled = enabled;
409 self
410 }
411}
412
413impl KeyExt for CalendarDay {
414 fn write_key(&mut self) -> &mut DiffKey {
415 &mut self.key
416 }
417}
418
419impl Component for CalendarDay {
420 fn render(&self) -> impl IntoElement {
421 Button::new()
422 .flat()
423 .padding(0.)
424 .enabled(self.enabled)
425 .width(Size::px(36.))
426 .height(Size::px(36.))
427 .background(self.background)
428 .hover_background(self.hover_background)
429 .maybe(self.enabled, |el| {
430 el.map(self.on_press.clone(), |el, on_press| el.on_press(on_press))
431 })
432 .child(
433 label()
434 .text(self.day.to_string())
435 .color(self.color)
436 .font_size(14.),
437 )
438 }
439
440 fn render_key(&self) -> DiffKey {
441 self.key.clone().or(self.default_key())
442 }
443}