freya_components/
calendar.rs

1/// Determines which day the week starts on.
2#[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/// A simple date representation for the calendar.
34#[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    /// Returns the current local date.
47    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    /// Returns the number of days in the given month.
57    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    /// Returns the day of the week for the first day of the month.
67    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    /// Returns the full name of the month.
77    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/// A calendar component for date selection.
92///
93/// # Example
94///
95/// ```rust
96/// # use freya::prelude::*;
97/// fn app() -> impl IntoElement {
98///     let mut selected = use_state(|| None::<CalendarDate>);
99///     let mut view_date = use_state(|| CalendarDate::new(2025, 1, 1));
100///
101///     Calendar::new()
102///         .selected(selected())
103///         .view_date(view_date())
104///         .on_change(move |date| selected.set(Some(date)))
105///         .on_view_change(move |date| view_date.set(date))
106/// }
107/// # use freya_testing::prelude::*;
108/// # launch_doc(|| {
109/// #   rect().center().expanded().child(app())
110/// # }, "./images/gallery_calendar.png").with_hook(|_| {}).with_scale_factor(0.8).render();
111/// ```
112///
113/// # Preview
114///
115/// ![Calendar Preview][gallery_calendar]
116#[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    /// Set which day the week starts on (Sunday or Monday)
158    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        // Previous month info for leading days
206        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        // Navigation handlers
219        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                    // Previous month
275                    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                    // Next month
287                    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                    // Current month
299                    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}