vizia_core/views/
datepicker.rs

1use chrono::{Datelike, NaiveDate, Weekday};
2
3use crate::prelude::*;
4
5/// A control used to select a date.
6#[derive(Lens)]
7pub struct Datepicker {
8    view_date: NaiveDate,
9    months: Vec<Localized>,
10    selected_month: usize,
11
12    #[lens(ignore)]
13    on_select: Option<Box<dyn Fn(&mut EventContext, NaiveDate)>>,
14}
15
16const MONTHS: [&str; 12] = [
17    "January",
18    "February",
19    "March",
20    "April",
21    "May",
22    "June",
23    "July",
24    "August",
25    "September",
26    "October",
27    "November",
28    "December",
29];
30
31const DAYS_HEADER: [&str; 7] =
32    ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
33
34pub(crate) enum DatepickerEvent {
35    IncrementMonth,
36    DecrementMonth,
37    SelectMonth(usize),
38
39    IncrementYear,
40    DecrementYear,
41    SelectYear(String),
42
43    SelectDate(NaiveDate),
44}
45
46impl Datepicker {
47    fn first_day_of_month(year: i32, month: u32) -> Option<Weekday> {
48        NaiveDate::from_ymd_opt(year, month, 1).map(|date| date.weekday())
49    }
50
51    fn last_day_of_month(year: i32, month: u32) -> Option<u32> {
52        if month == 12 {
53            NaiveDate::from_ymd_opt(year + 1, 1, 1)
54        } else {
55            NaiveDate::from_ymd_opt(year, month + 1, 1)
56        }
57        .map(|date| {
58            date.signed_duration_since(NaiveDate::from_ymd_opt(year, month, 1).unwrap()).num_days()
59                as u32
60        })
61    }
62
63    // Given a date and a month offset, returns the first day of the month and the number of days in the month
64    fn view_month_info(view_date: &NaiveDate, month_offset: i32) -> (Weekday, u32) {
65        let month = view_date.month();
66        let mut year = view_date.year();
67
68        let mut month = month as i32 + month_offset;
69
70        if month < 1 {
71            year -= 1;
72            month += 12;
73        } else if month > 12 {
74            year += 1;
75            month -= 12;
76        }
77
78        let month = month as u32;
79
80        (
81            Self::first_day_of_month(year, month).unwrap(),
82            Self::last_day_of_month(year, month).unwrap(),
83        )
84    }
85
86    fn get_day_number(y: u32, x: u32, view_date: &NaiveDate) -> (u32, bool) {
87        let (_, days_prev_month) = Self::view_month_info(view_date, -1);
88        let (first_day_this_month, days_this_month) = Self::view_month_info(view_date, 0);
89
90        let mut fdtm_i = first_day_this_month as usize as u32;
91        if fdtm_i == 0 {
92            fdtm_i = 7;
93        }
94
95        if y == 0 {
96            if x < fdtm_i {
97                (days_prev_month - (fdtm_i - x - 1), true)
98            } else {
99                (x - fdtm_i + 1, false)
100            }
101        } else {
102            let day_number = y * 7 + x - fdtm_i + 1;
103            if day_number > days_this_month {
104                (day_number - days_this_month, true)
105            } else {
106                (day_number, false)
107            }
108        }
109    }
110
111    /// Create a new [Datepicker] view.
112    pub fn new<L, D>(cx: &mut Context, lens: L) -> Handle<Self>
113    where
114        L: Lens<Target = D>,
115        D: Datelike + Data,
116    {
117        let view_date = lens.get(cx);
118
119        Self {
120            months: MONTHS.iter().map(|m| Localized::new(m)).collect::<Vec<_>>(),
121            selected_month: view_date.month() as usize - 1,
122            view_date: NaiveDate::from_ymd_opt(view_date.year(), view_date.month(), 1).unwrap(),
123            on_select: None,
124        }
125        .build(cx, move |cx| {
126            HStack::new(cx, |cx| {
127                Spinbox::custom(cx, |cx| {
128                    PickList::new(cx, Datepicker::months, Datepicker::selected_month, false)
129                        .on_select(|ex, index| ex.emit(DatepickerEvent::SelectMonth(index)))
130                        .width(Stretch(1.0))
131                })
132                .width(Pixels(140.0))
133                .on_increment(|ex| ex.emit(DatepickerEvent::IncrementMonth))
134                .on_decrement(|ex| ex.emit(DatepickerEvent::DecrementMonth));
135                Spinbox::custom(cx, |cx| {
136                    Textbox::new(cx, Datepicker::view_date.map(|date| date.year()))
137                        .width(Stretch(1.0))
138                        .padding(Pixels(1.0))
139                        .on_edit(|ex, v| ex.emit(DatepickerEvent::SelectYear(v)))
140                })
141                .width(Pixels(100.0))
142                .icons(SpinboxIcons::PlusMinus)
143                .on_increment(|ex| ex.emit(DatepickerEvent::IncrementYear))
144                .on_decrement(|ex| ex.emit(DatepickerEvent::DecrementYear));
145            })
146            .class("datepicker-header");
147
148            Divider::new(cx);
149
150            VStack::new(cx, move |cx| {
151                // Days of the week
152                HStack::new(cx, |cx| {
153                    for h in DAYS_HEADER {
154                        Label::new(cx, Localized::new(h).map(|day| day[0..2].to_string()))
155                            .class("datepicker-calendar-header");
156                    }
157                })
158                .class("datepicker-calendar-headers");
159
160                // Numbered days in a grid
161                VStack::new(cx, move |cx| {
162                    for y in 0..6 {
163                        HStack::new(cx, |cx| {
164                            for x in 0..7 {
165                                Label::new(cx, "").bind(
166                                    Datepicker::view_date,
167                                    move |handle, view_date| {
168                                        let view_date = view_date.get(&handle);
169
170                                        let (day_number, disabled) =
171                                            Self::get_day_number(y, x, &view_date);
172
173                                        handle.bind(lens, move |handle, selected_date| {
174                                            let selected_date = selected_date.get(&handle);
175
176                                            handle
177                                                .text(&day_number.to_string())
178                                                .class("datepicker-calendar-day")
179                                                .navigable(!disabled)
180                                                .toggle_class(
181                                                    "datepicker-calendar-day-disabled",
182                                                    disabled,
183                                                )
184                                                .on_press(move |ex| {
185                                                    if !disabled {
186                                                        ex.emit(DatepickerEvent::SelectDate(
187                                                            NaiveDate::from_ymd_opt(
188                                                                view_date.year(),
189                                                                view_date.month(),
190                                                                day_number,
191                                                            )
192                                                            .unwrap(),
193                                                        ))
194                                                    }
195                                                })
196                                                .checked(
197                                                    !disabled
198                                                        && selected_date.day() == day_number
199                                                        && selected_date.month()
200                                                            == view_date.month()
201                                                        && selected_date.year() == view_date.year(),
202                                                );
203                                        });
204                                    },
205                                );
206                            }
207                        });
208                    }
209                })
210                // This shouldn't be needed but apparently grid size isn't propagated up the tree during layout
211                .width(Pixels(32.0 * 7.0))
212                .height(Pixels(32.0 * 6.0));
213            })
214            .class("datepicker-calendar");
215        })
216    }
217}
218
219impl View for Datepicker {
220    fn element(&self) -> Option<&'static str> {
221        Some("datepicker")
222    }
223
224    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
225        event.map(|e, _| match e {
226            DatepickerEvent::IncrementMonth => {
227                if self.view_date.month() == 12 {
228                    self.view_date =
229                        NaiveDate::from_ymd_opt(self.view_date.year() + 1, 1, self.view_date.day())
230                            .unwrap();
231                } else {
232                    self.view_date = NaiveDate::from_ymd_opt(
233                        self.view_date.year(),
234                        self.view_date.month() + 1,
235                        self.view_date.day(),
236                    )
237                    .unwrap();
238                }
239                self.selected_month = self.view_date.month() as usize - 1;
240            }
241
242            DatepickerEvent::DecrementMonth => {
243                if self.view_date.month() == 1 {
244                    self.view_date = NaiveDate::from_ymd_opt(
245                        self.view_date.year() - 1,
246                        12,
247                        self.view_date.day(),
248                    )
249                    .unwrap();
250                } else {
251                    self.view_date = NaiveDate::from_ymd_opt(
252                        self.view_date.year(),
253                        self.view_date.month() - 1,
254                        self.view_date.day(),
255                    )
256                    .unwrap();
257                }
258                self.selected_month = self.view_date.month() as usize - 1;
259            }
260
261            DatepickerEvent::SelectMonth(month) => {
262                self.view_date = NaiveDate::from_ymd_opt(
263                    self.view_date.year(),
264                    *month as u32 + 1,
265                    self.view_date.day(),
266                )
267                .unwrap();
268                self.selected_month = *month;
269            }
270
271            DatepickerEvent::IncrementYear => {
272                self.view_date = NaiveDate::from_ymd_opt(
273                    self.view_date.year() + 1,
274                    self.view_date.month(),
275                    self.view_date.day(),
276                )
277                .unwrap();
278            }
279
280            DatepickerEvent::DecrementYear => {
281                self.view_date = NaiveDate::from_ymd_opt(
282                    self.view_date.year() - 1,
283                    self.view_date.month(),
284                    self.view_date.day(),
285                )
286                .unwrap();
287            }
288
289            DatepickerEvent::SelectYear(year) => {
290                if let Ok(year) = year.parse::<i32>() {
291                    self.view_date =
292                        NaiveDate::from_ymd_opt(year, self.view_date.month(), self.view_date.day())
293                            .unwrap();
294                }
295            }
296
297            DatepickerEvent::SelectDate(date) => {
298                if let Some(callback) = &self.on_select {
299                    (callback)(cx, *date);
300                }
301            }
302        })
303    }
304}
305
306impl Handle<'_, Datepicker> {
307    /// Set the callback triggered when a date is selected from the [Datepicker] view.
308    pub fn on_select<F: 'static + Fn(&mut EventContext, NaiveDate)>(self, callback: F) -> Self {
309        self.modify(|datepicker: &mut Datepicker| datepicker.on_select = Some(Box::new(callback)))
310    }
311}