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/// A calendar component for date selection.
85///
86/// # Example
87///
88/// ```rust
89/// # use freya::prelude::*;
90/// fn app() -> impl IntoElement {
91///     let mut selected = use_state(|| None::<CalendarDate>);
92///     let mut view_date = use_state(|| CalendarDate::new(2025, 1, 1));
93///
94///     Calendar::new()
95///         .selected(selected())
96///         .view_date(view_date())
97///         .on_change(move |date| selected.set(Some(date)))
98///         .on_view_change(move |date| view_date.set(date))
99/// }
100/// # use freya_testing::prelude::*;
101/// # launch_doc(|| {
102/// #   rect().center().expanded().child(app())
103/// # }, "./images/gallery_calendar.png").with_hook(|_| {}).with_scale_factor(0.8).render();
104/// ```
105///
106/// # Preview
107///
108/// ![Calendar Preview][gallery_calendar]
109#[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    /// Set which day the week starts on (Sunday or Monday)
151    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}