Skip to main content

vizia_core/views/
datepicker.rs

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