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#[derive(Debug, Default, PartialEq, Clone, Copy)]
85pub enum CalendarDayStatus {
86 #[default]
87 Idle,
88 Hovering,
89}
90
91#[cfg_attr(feature = "docs", doc = embed_doc_image::embed_image!("gallery_calendar", "images/gallery_calendar.png"))]
117#[derive(Clone, PartialEq)]
118pub struct Calendar {
119 pub(crate) theme: Option<CalendarThemePartial>,
120 selected: Option<CalendarDate>,
121 view_date: CalendarDate,
122 week_start: WeekStart,
123 on_change: Option<EventHandler<CalendarDate>>,
124 on_view_change: Option<EventHandler<CalendarDate>>,
125 key: DiffKey,
126}
127
128impl Default for Calendar {
129 fn default() -> Self {
130 Self::new()
131 }
132}
133
134impl Calendar {
135 pub fn new() -> Self {
136 Self {
137 theme: None,
138 selected: None,
139 view_date: CalendarDate::now(),
140 week_start: WeekStart::Monday,
141 on_change: None,
142 on_view_change: None,
143 key: DiffKey::None,
144 }
145 }
146
147 pub fn selected(mut self, selected: Option<CalendarDate>) -> Self {
148 self.selected = selected;
149 self
150 }
151
152 pub fn view_date(mut self, view_date: CalendarDate) -> Self {
153 self.view_date = view_date;
154 self
155 }
156
157 pub fn week_start(mut self, week_start: WeekStart) -> Self {
159 self.week_start = week_start;
160 self
161 }
162
163 pub fn on_change(mut self, on_change: impl Into<EventHandler<CalendarDate>>) -> Self {
164 self.on_change = Some(on_change.into());
165 self
166 }
167
168 pub fn on_view_change(mut self, on_view_change: impl Into<EventHandler<CalendarDate>>) -> Self {
169 self.on_view_change = Some(on_view_change.into());
170 self
171 }
172}
173
174impl KeyExt for Calendar {
175 fn write_key(&mut self) -> &mut DiffKey {
176 &mut self.key
177 }
178}
179
180impl Render for Calendar {
181 fn render(&self) -> impl IntoElement {
182 let theme = get_theme!(&self.theme, calendar);
183
184 let CalendarTheme {
185 background,
186 day_background,
187 day_hover_background,
188 day_selected_background,
189 color,
190 day_other_month_color,
191 header_color,
192 corner_radius,
193 padding,
194 day_corner_radius,
195 nav_button_hover_background,
196 } = theme;
197
198 let view_year = self.view_date.year;
199 let view_month = self.view_date.month;
200
201 let days_in_month = CalendarDate::days_in_month(view_year, view_month);
202 let first_day = CalendarDate::first_day_of_month(view_year, view_month, self.week_start);
203 let month_name = CalendarDate::month_name(view_month);
204
205 let prev_month = if view_month == 1 { 12 } else { view_month - 1 };
207 let prev_year = if view_month == 1 {
208 view_year - 1
209 } else {
210 view_year
211 };
212 let days_in_prev_month = CalendarDate::days_in_month(prev_year, prev_month);
213
214 let on_change = self.on_change.clone();
215 let on_view_change = self.on_view_change.clone();
216 let selected = self.selected;
217
218 let on_prev = {
220 let on_view_change = on_view_change.clone();
221 move |_: Event<PressEventData>| {
222 if let Some(handler) = &on_view_change {
223 let new_month = if view_month == 1 { 12 } else { view_month - 1 };
224 let new_year = if view_month == 1 {
225 view_year - 1
226 } else {
227 view_year
228 };
229 handler.call(CalendarDate::new(new_year, new_month, 1));
230 }
231 }
232 };
233
234 let on_next = {
235 let on_view_change = on_view_change.clone();
236 move |_: Event<PressEventData>| {
237 if let Some(handler) = &on_view_change {
238 let new_month = if view_month == 12 { 1 } else { view_month + 1 };
239 let new_year = if view_month == 12 {
240 view_year + 1
241 } else {
242 view_year
243 };
244 handler.call(CalendarDate::new(new_year, new_month, 1));
245 }
246 }
247 };
248
249 let mut rows: Vec<Element> = Vec::new();
250
251 let weekday_names = match self.week_start {
252 WeekStart::Sunday => ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"],
253 WeekStart::Monday => ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"],
254 };
255 let header_cells = weekday_names.iter().map(|day_name| {
256 rect()
257 .width(Size::px(36.))
258 .height(Size::px(36.))
259 .center()
260 .child(label().text(*day_name).color(header_color).font_size(12.))
261 .into()
262 });
263 rows.push(rect().horizontal().children_iter(header_cells).into());
264
265 let mut current_day: i32 = 1 - first_day as i32;
266 let total_days = first_day + days_in_month;
267 let total_weeks = total_days.div_ceil(7);
268
269 for _ in 0..total_weeks {
270 let mut week_cells: Vec<Element> = Vec::new();
271
272 for _ in 0..7 {
273 if current_day < 1 {
274 let day = (days_in_prev_month as i32 + current_day) as u32;
276 week_cells.push(
277 CalendarDay::new()
278 .key(day)
279 .day(day)
280 .color(day_other_month_color)
281 .corner_radius(day_corner_radius)
282 .enabled(false)
283 .into(),
284 );
285 } else if current_day as u32 > days_in_month {
286 let day = current_day as u32 - days_in_month;
288 week_cells.push(
289 CalendarDay::new()
290 .key(day)
291 .day(day)
292 .color(day_other_month_color)
293 .corner_radius(day_corner_radius)
294 .enabled(false)
295 .into(),
296 );
297 } else {
298 let day = current_day as u32;
300 let date = CalendarDate::new(view_year, view_month, day);
301 let is_selected = selected == Some(date);
302
303 let on_change = on_change.clone();
304
305 let (background, hover_background) = if is_selected {
306 (day_selected_background, day_selected_background)
307 } else {
308 (day_background, day_hover_background)
309 };
310
311 week_cells.push(
312 CalendarDay::new()
313 .key(day)
314 .day(day)
315 .background(background)
316 .hover_background(hover_background)
317 .color(color)
318 .corner_radius(day_corner_radius)
319 .map(on_change, |el, on_change| {
320 el.on_press(move |_| on_change.call(date))
321 })
322 .into(),
323 );
324 }
325
326 current_day += 1;
327 }
328
329 rows.push(rect().horizontal().children(week_cells).into());
330 }
331
332 rect()
333 .background(background)
334 .corner_radius(corner_radius)
335 .padding(padding)
336 .width(Size::px(280.))
337 .child(
338 rect()
339 .horizontal()
340 .width(Size::fill())
341 .padding((0., 0., 8., 0.))
342 .cross_align(Alignment::center())
343 .content(Content::flex())
344 .child(
345 NavButton::new()
346 .hover_background(nav_button_hover_background)
347 .on_press(on_prev)
348 .child(
349 ArrowIcon::new()
350 .fill(color)
351 .width(Size::px(16.))
352 .height(Size::px(16.))
353 .rotate(90.),
354 ),
355 )
356 .child(
357 label()
358 .width(Size::flex(1.))
359 .text_align(TextAlign::Center)
360 .text(format!("{} {}", month_name, view_year))
361 .color(header_color)
362 .max_lines(1)
363 .font_size(16.),
364 )
365 .child(
366 NavButton::new()
367 .hover_background(nav_button_hover_background)
368 .on_press(on_next)
369 .child(
370 ArrowIcon::new()
371 .fill(color)
372 .width(Size::px(16.))
373 .height(Size::px(16.))
374 .rotate(-90.),
375 ),
376 ),
377 )
378 .children(rows)
379 }
380
381 fn render_key(&self) -> DiffKey {
382 self.key.clone().or(self.default_key())
383 }
384}
385
386#[derive(Clone, PartialEq)]
387struct NavButton {
388 hover_background: Color,
389 children: Vec<Element>,
390 on_press: Option<EventHandler<Event<PressEventData>>>,
391}
392
393impl NavButton {
394 fn new() -> Self {
395 Self {
396 hover_background: Color::TRANSPARENT,
397 children: Vec::new(),
398 on_press: None,
399 }
400 }
401
402 fn hover_background(mut self, hover_background: Color) -> Self {
403 self.hover_background = hover_background;
404 self
405 }
406
407 fn on_press(mut self, on_press: impl Into<EventHandler<Event<PressEventData>>>) -> Self {
408 self.on_press = Some(on_press.into());
409 self
410 }
411}
412
413impl ChildrenExt for NavButton {
414 fn get_children(&mut self) -> &mut Vec<Element> {
415 &mut self.children
416 }
417}
418
419impl Render for NavButton {
420 fn render(&self) -> impl IntoElement {
421 Button::new()
422 .flat()
423 .width(Size::px(32.))
424 .height(Size::px(32.))
425 .hover_background(self.hover_background)
426 .map(self.on_press.clone(), |el, on_press| el.on_press(on_press))
427 .children(self.children.clone())
428 }
429}
430
431#[derive(Clone, PartialEq)]
432struct CalendarDay {
433 day: u32,
434 background: Color,
435 hover_background: Color,
436 color: Color,
437 corner_radius: CornerRadius,
438 on_press: Option<EventHandler<Event<PressEventData>>>,
439 enabled: bool,
440 key: DiffKey,
441}
442
443impl CalendarDay {
444 fn new() -> Self {
445 Self {
446 day: 1,
447 background: Color::TRANSPARENT,
448 hover_background: Color::TRANSPARENT,
449 color: Color::BLACK,
450 corner_radius: CornerRadius::default(),
451 on_press: None,
452 enabled: true,
453 key: DiffKey::None,
454 }
455 }
456
457 fn day(mut self, day: u32) -> Self {
458 self.day = day;
459 self
460 }
461
462 fn background(mut self, background: Color) -> Self {
463 self.background = background;
464 self
465 }
466
467 fn hover_background(mut self, hover_background: Color) -> Self {
468 self.hover_background = hover_background;
469 self
470 }
471
472 fn color(mut self, color: Color) -> Self {
473 self.color = color;
474 self
475 }
476
477 fn corner_radius(mut self, corner_radius: CornerRadius) -> Self {
478 self.corner_radius = corner_radius;
479 self
480 }
481
482 fn on_press(mut self, on_press: impl Into<EventHandler<Event<PressEventData>>>) -> Self {
483 self.on_press = Some(on_press.into());
484 self
485 }
486
487 fn enabled(mut self, enabled: bool) -> Self {
488 self.enabled = enabled;
489 self
490 }
491}
492
493impl KeyExt for CalendarDay {
494 fn write_key(&mut self) -> &mut DiffKey {
495 &mut self.key
496 }
497}
498
499impl Render for CalendarDay {
500 fn render(&self) -> impl IntoElement {
501 Button::new()
502 .flat()
503 .padding(0.)
504 .enabled(self.enabled)
505 .width(Size::px(36.))
506 .height(Size::px(36.))
507 .background(self.background)
508 .hover_background(self.hover_background)
509 .maybe(self.enabled, |el| {
510 el.map(self.on_press.clone(), |el, on_press| el.on_press(on_press))
511 })
512 .child(
513 label()
514 .text(self.day.to_string())
515 .color(self.color)
516 .font_size(14.),
517 )
518 }
519
520 fn render_key(&self) -> DiffKey {
521 self.key.clone().or(self.default_key())
522 }
523}