1use chrono::{Datelike, NaiveDate, Weekday};
2
3use crate::prelude::*;
4
5#[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 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 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 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 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 .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 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}