Skip to main content

vizia_core/views/
calendar.rs

1use chrono::{Datelike, Duration, NaiveDate, Weekday};
2
3use crate::{
4    icons::{ICON_CHEVRON_LEFT, ICON_CHEVRON_RIGHT},
5    prelude::*,
6};
7
8/// A control used to select a date.
9pub struct Calendar {
10    view_date: Signal<NaiveDate>,
11    focused_date: Signal<NaiveDate>,
12    week_starts_on: Signal<Weekday>,
13    keyboard_help: Signal<String>,
14    keyboard_help_announced: bool,
15    day_cells: Vec<Entity>,
16    on_select: Option<Box<dyn Fn(&mut EventContext, NaiveDate)>>,
17}
18
19const MONTHS: [&str; 12] =
20    ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec"];
21
22pub enum CalendarEvent {
23    IncrementMonth,
24    DecrementMonth,
25    SelectMonth(usize),
26
27    IncrementYear,
28    DecrementYear,
29    SelectYear(i32),
30
31    MoveFocusDays(i64),
32    MoveFocusLeft,
33    MoveFocusRight,
34    MoveFocusWeeks(i64),
35    MoveFocusWeekStart,
36    MoveFocusWeekEnd,
37    MoveFocusMonth(i32),
38    MoveFocusYear(i32),
39    ActivateFocusedDate,
40    RegisterDayCell(usize, usize, Entity),
41    AnnounceKeyboardHelp,
42    RefreshLocalization,
43
44    SelectDate(NaiveDate),
45}
46
47impl Calendar {
48    fn weekday_from_monday_index(index: u32) -> Weekday {
49        match index % 7 {
50            0 => Weekday::Mon,
51            1 => Weekday::Tue,
52            2 => Weekday::Wed,
53            3 => Weekday::Thu,
54            4 => Weekday::Fri,
55            5 => Weekday::Sat,
56            _ => Weekday::Sun,
57        }
58    }
59
60    fn weekday_key(weekday: Weekday) -> &'static str {
61        match weekday {
62            Weekday::Mon => "Monday",
63            Weekday::Tue => "Tuesday",
64            Weekday::Wed => "Wednesday",
65            Weekday::Thu => "Thursday",
66            Weekday::Fri => "Friday",
67            Weekday::Sat => "Saturday",
68            Weekday::Sun => "Sunday",
69        }
70    }
71
72    fn weekday_short_key(weekday: Weekday) -> &'static str {
73        match weekday {
74            Weekday::Mon => "Monday-short",
75            Weekday::Tue => "Tuesday-short",
76            Weekday::Wed => "Wednesday-short",
77            Weekday::Thu => "Thursday-short",
78            Weekday::Fri => "Friday-short",
79            Weekday::Sat => "Saturday-short",
80            Weekday::Sun => "Sunday-short",
81        }
82    }
83
84    fn first_day_column(first_day: Weekday, week_starts_on: Weekday) -> u32 {
85        let first = first_day.num_days_from_monday() as i32;
86        let start = week_starts_on.num_days_from_monday() as i32;
87        ((7 + first - start) % 7) as u32
88    }
89
90    fn weekday_from_token(token: &str) -> Option<Weekday> {
91        match token.trim().to_ascii_lowercase().as_str() {
92            "monday" => Some(Weekday::Mon),
93            "tuesday" => Some(Weekday::Tue),
94            "wednesday" => Some(Weekday::Wed),
95            "thursday" => Some(Weekday::Thu),
96            "friday" => Some(Weekday::Fri),
97            "saturday" => Some(Weekday::Sat),
98            "sunday" => Some(Weekday::Sun),
99            _ => None,
100        }
101    }
102
103    fn week_start_from_localization(cx: &impl DataContext) -> Weekday {
104        let val = Localized::new("calendar-week-start").to_string_local(cx);
105        Self::weekday_from_token(&val).unwrap_or(Weekday::Mon)
106    }
107
108    fn set_view_date(&mut self, year: i32, month: u32, day: u32) {
109        let clamped_day = day.min(Self::last_day_of_month(year, month).unwrap());
110        let date = NaiveDate::from_ymd_opt(year, month, clamped_day).unwrap();
111        self.view_date.set(date);
112        self.focused_date.set(date);
113    }
114
115    fn shift_month(&mut self, delta: i32) {
116        let focused_date = self.focused_date.get();
117        let (year, month) =
118            Self::shift_year_month(focused_date.year(), focused_date.month(), delta);
119        self.set_view_date(year, month, focused_date.day());
120    }
121
122    fn shift_year(&mut self, delta: i32) {
123        let focused_date = self.focused_date.get();
124        self.set_view_date(focused_date.year() + delta, focused_date.month(), focused_date.day());
125    }
126
127    fn shift_year_month(year: i32, month: u32, delta: i32) -> (i32, u32) {
128        let mut year = year;
129        let mut month = month as i32 + delta;
130
131        while month < 1 {
132            year -= 1;
133            month += 12;
134        }
135
136        while month > 12 {
137            year += 1;
138            month -= 12;
139        }
140
141        (year, month as u32)
142    }
143
144    fn first_day_of_month(year: i32, month: u32) -> Option<Weekday> {
145        NaiveDate::from_ymd_opt(year, month, 1).map(|date| date.weekday())
146    }
147
148    fn last_day_of_month(year: i32, month: u32) -> Option<u32> {
149        if month == 12 {
150            NaiveDate::from_ymd_opt(year + 1, 1, 1)
151        } else {
152            NaiveDate::from_ymd_opt(year, month + 1, 1)
153        }
154        .map(|date| {
155            date.signed_duration_since(NaiveDate::from_ymd_opt(year, month, 1).unwrap()).num_days()
156                as u32
157        })
158    }
159
160    // Given a date and a month offset, returns the first day of the month and the number of days in the month
161    fn view_month_info(view_date: &NaiveDate, month_offset: i32) -> (Weekday, u32) {
162        let month = view_date.month();
163        let mut year = view_date.year();
164
165        let mut month = month as i32 + month_offset;
166
167        if month < 1 {
168            year -= 1;
169            month += 12;
170        } else if month > 12 {
171            year += 1;
172            month -= 12;
173        }
174
175        let month = month as u32;
176
177        (
178            Self::first_day_of_month(year, month).unwrap(),
179            Self::last_day_of_month(year, month).unwrap(),
180        )
181    }
182
183    fn get_day_number(
184        y: u32,
185        x: u32,
186        view_date: &NaiveDate,
187        week_starts_on: Weekday,
188    ) -> (u32, bool) {
189        let (_, days_prev_month) = Self::view_month_info(view_date, -1);
190        let (first_day_this_month, days_this_month) = Self::view_month_info(view_date, 0);
191
192        let fdtm_i = Self::first_day_column(first_day_this_month, week_starts_on);
193
194        if y == 0 {
195            if x < fdtm_i {
196                (days_prev_month - (fdtm_i - x - 1), true)
197            } else {
198                (x - fdtm_i + 1, false)
199            }
200        } else {
201            let day_number = y * 7 + x - fdtm_i + 1;
202            if day_number > days_this_month {
203                (day_number - days_this_month, true)
204            } else {
205                (day_number, false)
206            }
207        }
208    }
209
210    fn get_cell_date(
211        y: u32,
212        x: u32,
213        view_date: &NaiveDate,
214        week_starts_on: Weekday,
215    ) -> (NaiveDate, bool) {
216        let (day_number, disabled) = Self::get_day_number(y, x, view_date, week_starts_on);
217
218        if !disabled {
219            return (
220                NaiveDate::from_ymd_opt(view_date.year(), view_date.month(), day_number).unwrap(),
221                false,
222            );
223        }
224
225        if y == 0 {
226            let (year, month) = Self::shift_year_month(view_date.year(), view_date.month(), -1);
227            (NaiveDate::from_ymd_opt(year, month, day_number).unwrap(), true)
228        } else {
229            let (year, month) = Self::shift_year_month(view_date.year(), view_date.month(), 1);
230            (NaiveDate::from_ymd_opt(year, month, day_number).unwrap(), true)
231        }
232    }
233
234    fn day_cell_index(y: usize, x: usize) -> usize {
235        y * 7 + x
236    }
237
238    fn focused_day_cell_index(&self) -> Option<usize> {
239        let focused_date = self.focused_date.get();
240        let view_date = self.view_date.get();
241
242        if focused_date.year() != view_date.year() || focused_date.month() != view_date.month() {
243            return None;
244        }
245
246        let first_day = Self::first_day_of_month(view_date.year(), view_date.month()).unwrap();
247        let first_day_col = Self::first_day_column(first_day, self.week_starts_on.get()) as usize;
248        Some(first_day_col + focused_date.day() as usize - 1)
249    }
250
251    fn focus_focused_day(&self, cx: &mut EventContext) {
252        if let Some(index) = self.focused_day_cell_index()
253            && let Some(entity) = self.day_cells.get(index).copied()
254            && !entity.is_null()
255        {
256            cx.with_current(entity, |cx| cx.focus());
257        }
258    }
259
260    fn focus_focused_day_with_visibility(&self, cx: &mut EventContext, focus_visible: bool) {
261        if let Some(index) = self.focused_day_cell_index()
262            && let Some(entity) = self.day_cells.get(index).copied()
263            && !entity.is_null()
264        {
265            cx.with_current(entity, |cx| cx.focus_with_visibility(focus_visible));
266        }
267    }
268
269    fn move_focused_by_days(&mut self, delta_days: i64) {
270        let focused_date = self.focused_date.get();
271        let next_date = focused_date.checked_add_signed(Duration::days(delta_days)).unwrap();
272        self.view_date.set(next_date);
273        self.focused_date.set(next_date);
274    }
275
276    fn move_focused_by_months(&mut self, delta_months: i32) {
277        let focused_date = self.focused_date.get();
278        let (year, month) =
279            Self::shift_year_month(focused_date.year(), focused_date.month(), delta_months);
280        self.set_view_date(year, month, focused_date.day());
281    }
282
283    fn move_focused_by_years(&mut self, delta_years: i32) {
284        let focused_date = self.focused_date.get();
285        self.set_view_date(
286            focused_date.year() + delta_years,
287            focused_date.month(),
288            focused_date.day(),
289        );
290    }
291
292    fn focus_is_on_calendar_day(&self, cx: &EventContext) -> bool {
293        let focused = cx.focused();
294        self.day_cells.iter().any(|cell| {
295            !cell.is_null() && (focused == *cell || focused.is_descendant_of(cx.tree, *cell))
296        })
297    }
298
299    /// Create a new [Calendar] view.
300    pub fn new<R, D>(cx: &mut Context, date: R) -> Handle<Self>
301    where
302        R: Res<D> + Clone + 'static,
303        D: Datelike + Clone + 'static,
304    {
305        let selected_date = date.get_value(cx);
306        let selected_date_signal = date.to_signal(cx);
307        let initial_view_date =
308            NaiveDate::from_ymd_opt(selected_date.year(), selected_date.month(), 1).unwrap();
309        let view_date = Signal::new(initial_view_date);
310        let focused_date = Signal::new(
311            NaiveDate::from_ymd_opt(
312                selected_date.year(),
313                selected_date.month(),
314                selected_date.day(),
315            )
316            .unwrap(),
317        );
318        let month_options =
319            Signal::new(MONTHS.iter().map(|m| Localized::new(m)).collect::<Vec<_>>());
320        let month_year_heading_date =
321            view_date.map(|date| date.and_hms_opt(0, 0, 0).unwrap().and_utc().timestamp_millis());
322        let week_starts_on = Signal::new(Self::week_start_from_localization(cx));
323        let keyboard_help = Signal::new(String::new());
324        let selected_month = view_date.map(|date| Some(date.month() as usize - 1));
325        let year_start = selected_date.year() - 50;
326        let year_end = selected_date.year() + 50;
327        let year_options =
328            Signal::new((year_start..=year_end).map(|year| year.to_string()).collect::<Vec<_>>());
329        let selected_year = view_date.map(move |date| {
330            let index = date.year() - year_start;
331            if (0..=(year_end - year_start)).contains(&index) { Some(index as usize) } else { None }
332        });
333
334        Self {
335            view_date,
336            focused_date,
337            week_starts_on,
338            keyboard_help,
339            keyboard_help_announced: false,
340            day_cells: vec![Entity::null(); 42],
341            on_select: None,
342        }
343        .build(cx, move |cx| {
344            let calendar_entity = cx.current();
345            let locale = cx.environment().locale;
346            locale.set_or_bind(cx, move |cx, _| {
347                cx.emit_to(calendar_entity, CalendarEvent::RefreshLocalization);
348            });
349
350            Keymap::from(vec![
351                (
352                    KeyChord::new(Modifiers::empty(), Code::ArrowUp),
353                    KeymapEntry::new("Calendar Focus Previous Week", |cx| {
354                        cx.emit(CalendarEvent::MoveFocusWeeks(-1))
355                    }),
356                ),
357                (
358                    KeyChord::new(Modifiers::empty(), Code::ArrowDown),
359                    KeymapEntry::new("Calendar Focus Next Week", |cx| {
360                        cx.emit(CalendarEvent::MoveFocusWeeks(1))
361                    }),
362                ),
363                (
364                    KeyChord::new(Modifiers::empty(), Code::ArrowLeft),
365                    KeymapEntry::new("Calendar Focus Left Day", |cx| {
366                        cx.emit(CalendarEvent::MoveFocusLeft)
367                    }),
368                ),
369                (
370                    KeyChord::new(Modifiers::empty(), Code::ArrowRight),
371                    KeymapEntry::new("Calendar Focus Right Day", |cx| {
372                        cx.emit(CalendarEvent::MoveFocusRight)
373                    }),
374                ),
375                (
376                    KeyChord::new(Modifiers::empty(), Code::Home),
377                    KeymapEntry::new("Calendar Focus Week Start", |cx| {
378                        cx.emit(CalendarEvent::MoveFocusWeekStart)
379                    }),
380                ),
381                (
382                    KeyChord::new(Modifiers::empty(), Code::End),
383                    KeymapEntry::new("Calendar Focus Week End", |cx| {
384                        cx.emit(CalendarEvent::MoveFocusWeekEnd)
385                    }),
386                ),
387                (
388                    KeyChord::new(Modifiers::empty(), Code::PageUp),
389                    KeymapEntry::new("Calendar Previous Month", |cx| {
390                        cx.emit(CalendarEvent::MoveFocusMonth(-1))
391                    }),
392                ),
393                (
394                    KeyChord::new(Modifiers::empty(), Code::PageDown),
395                    KeymapEntry::new("Calendar Next Month", |cx| {
396                        cx.emit(CalendarEvent::MoveFocusMonth(1))
397                    }),
398                ),
399                (
400                    KeyChord::new(Modifiers::SHIFT, Code::PageUp),
401                    KeymapEntry::new("Calendar Previous Year", |cx| {
402                        cx.emit(CalendarEvent::MoveFocusYear(-1))
403                    }),
404                ),
405                (
406                    KeyChord::new(Modifiers::SHIFT, Code::PageDown),
407                    KeymapEntry::new("Calendar Next Year", |cx| {
408                        cx.emit(CalendarEvent::MoveFocusYear(1))
409                    }),
410                ),
411                (
412                    KeyChord::new(Modifiers::empty(), Code::Space),
413                    KeymapEntry::new("Calendar Activate Date", |cx| {
414                        cx.emit(CalendarEvent::ActivateFocusedDate)
415                    }),
416                ),
417                (
418                    KeyChord::new(Modifiers::empty(), Code::Enter),
419                    KeymapEntry::new("Calendar Activate Date", |cx| {
420                        cx.emit(CalendarEvent::ActivateFocusedDate)
421                    }),
422                ),
423            ])
424            .build(cx);
425
426            HStack::new(cx, move |cx| {
427                Button::new(cx, |cx| Svg::new(cx, ICON_CHEVRON_LEFT))
428                    .on_press(|ex| ex.emit(CalendarEvent::DecrementMonth))
429                    .variant(ButtonVariant::Text)
430                    .name(Localized::new("calendar-previous-month"))
431                    .class("month-nav");
432                HStack::new(cx, move |cx| {
433                    Select::new(cx, month_options, selected_month, true)
434                        .on_select(|ex, index| ex.emit(CalendarEvent::SelectMonth(index)));
435                    Select::new(cx, year_options, selected_year, true).on_select(
436                        move |ex, index| {
437                            ex.emit(CalendarEvent::SelectYear(year_start + index as i32));
438                        },
439                    );
440                })
441                .class("calendar-controls-select");
442
443                Button::new(cx, |cx| Svg::new(cx, ICON_CHEVRON_RIGHT))
444                    .on_press(|ex| ex.emit(CalendarEvent::IncrementMonth))
445                    .variant(ButtonVariant::Text)
446                    .name(Localized::new("calendar-next-month"))
447                    .class("month-nav");
448            })
449            .class("calendar-controls");
450
451            VStack::new(cx, move |cx| {
452                Label::new(
453                    cx,
454                    Localized::new("calendar-month-year-heading")
455                        .arg("date", month_year_heading_date),
456                )
457                .class("calendar-month-year-heading")
458                .role(Role::Label)
459                .display(Display::None)
460                .live(Live::Polite);
461
462                Label::new(cx, keyboard_help)
463                    .class("calendar-keyboard-help")
464                    .role(Role::Label)
465                    .display(Display::None)
466                    .live(Live::Polite);
467
468                // Days of the week
469                HStack::new(cx, |cx| {
470                    for x in 0..7 {
471                        let week_starts_on = cx.data::<Calendar>().week_starts_on;
472                        Label::new(cx, "")
473                            .bind(week_starts_on, move |handle| {
474                                let week_starts_on = week_starts_on.get();
475                                let weekday = Self::weekday_from_monday_index(
476                                    week_starts_on.num_days_from_monday() + x,
477                                );
478                                handle
479                                    .text(Localized::new(Self::weekday_short_key(weekday)))
480                                    .name(Localized::new(Self::weekday_key(weekday)));
481                            })
482                            .class("calendar-dow")
483                            .role(Role::ColumnHeader);
484                    }
485                })
486                .class("calendar-header")
487                .role(Role::Row);
488
489                // Numbered days in a grid
490                VStack::new(cx, move |cx| {
491                    for y in 0..6 {
492                        HStack::new(cx, |cx| {
493                            for x in 0..7 {
494                                let selected_date = selected_date_signal;
495                                let view_date = cx.data::<Calendar>().view_date;
496                                let focused_date = cx.data::<Calendar>().focused_date;
497                                let week_starts_on = cx.data::<Calendar>().week_starts_on;
498                                Label::new(cx, "").bind(view_date, move |handle| {
499                                    let view_date = view_date.get();
500                                    let selected_date = selected_date;
501                                    let focused_date = focused_date;
502                                    handle.bind(week_starts_on, move |handle| {
503                                        let week_starts_on = week_starts_on.get();
504                                        let (cell_date, disabled) =
505                                            Self::get_cell_date(y, x, &view_date, week_starts_on);
506                                        let day_number = cell_date.day();
507
508                                        handle.bind(selected_date, move |handle| {
509                                            let selected_date = selected_date.get();
510                                            handle.bind(focused_date, move |handle| {
511                                                let focused_date = focused_date.get();
512                                                let is_focused = focused_date == cell_date;
513
514                                                handle
515                                                    .text(day_number.to_string())
516                                                    .class("calendar-day")
517                                                    .role(Role::GridCell)
518                                                    .name(
519                                                        Localized::new("calendar-day-cell-name")
520                                                            .arg(
521                                                                "date",
522                                                                cell_date
523                                                                    .and_hms_opt(0, 0, 0)
524                                                                    .unwrap()
525                                                                    .and_utc()
526                                                                    .timestamp_millis(),
527                                                            ),
528                                                    )
529                                                    .disabled(disabled)
530                                                    .navigable(!disabled && is_focused)
531                                                    .toggle_class("calendar-day-disabled", disabled)
532                                                    .toggle_class(
533                                                        "calendar-day-focused",
534                                                        is_focused,
535                                                    )
536                                                    .on_build(move |cx| {
537                                                        cx.emit(CalendarEvent::RegisterDayCell(
538                                                            y as usize,
539                                                            x as usize,
540                                                            cx.current(),
541                                                        ));
542                                                    })
543                                                    .on_focus_in(|ex| {
544                                                        ex.emit(CalendarEvent::AnnounceKeyboardHelp)
545                                                    })
546                                                    .on_press(move |ex| {
547                                                        if !disabled {
548                                                            ex.emit(CalendarEvent::SelectDate(
549                                                                cell_date,
550                                                            ));
551                                                        }
552                                                    })
553                                                    .toggle_class(
554                                                        "calendar-day-selected",
555                                                        !disabled
556                                                            && selected_date.day()
557                                                                == cell_date.day()
558                                                            && selected_date.month()
559                                                                == cell_date.month()
560                                                            && selected_date.year()
561                                                                == cell_date.year(),
562                                                    )
563                                                    .selected(
564                                                        !disabled
565                                                            && selected_date.day()
566                                                                == cell_date.day()
567                                                            && selected_date.month()
568                                                                == cell_date.month()
569                                                            && selected_date.year()
570                                                                == cell_date.year(),
571                                                    );
572                                            });
573                                        });
574                                    });
575                                });
576                            }
577                        })
578                        .role(Role::Row);
579                    }
580                })
581                // This shouldn't be needed but apparently grid size isn't propagated up the tree during layout
582                .width(Pixels(32.0 * 7.0))
583                .height(Pixels(32.0 * 6.0));
584            })
585            .class("calendar-body")
586            .role(Role::Grid)
587            .name(
588                Localized::new("calendar-month-year-heading").arg("date", month_year_heading_date),
589            );
590        })
591    }
592}
593
594impl View for Calendar {
595    fn element(&self) -> Option<&'static str> {
596        Some("calendar")
597    }
598
599    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
600        event.map(|e, _| match e {
601            CalendarEvent::IncrementMonth => {
602                self.shift_month(1);
603            }
604
605            CalendarEvent::DecrementMonth => {
606                self.shift_month(-1);
607            }
608
609            CalendarEvent::SelectMonth(month) => {
610                let focused_date = self.focused_date.get();
611                self.set_view_date(focused_date.year(), *month as u32 + 1, focused_date.day());
612            }
613
614            CalendarEvent::IncrementYear => {
615                self.shift_year(1);
616            }
617
618            CalendarEvent::DecrementYear => {
619                self.shift_year(-1);
620            }
621
622            CalendarEvent::SelectYear(year) => {
623                let focused_date = self.focused_date.get();
624                self.set_view_date(*year, focused_date.month(), focused_date.day());
625            }
626
627            CalendarEvent::MoveFocusDays(delta) => {
628                if self.focus_is_on_calendar_day(cx) {
629                    self.move_focused_by_days(*delta);
630                    self.focus_focused_day_with_visibility(cx, true);
631                }
632            }
633
634            CalendarEvent::MoveFocusLeft => {
635                if self.focus_is_on_calendar_day(cx) {
636                    let delta = if cx.environment().direction.get() == Direction::RightToLeft {
637                        1
638                    } else {
639                        -1
640                    };
641                    self.move_focused_by_days(delta);
642                    self.focus_focused_day_with_visibility(cx, true);
643                }
644            }
645
646            CalendarEvent::MoveFocusRight => {
647                if self.focus_is_on_calendar_day(cx) {
648                    let delta = if cx.environment().direction.get() == Direction::RightToLeft {
649                        -1
650                    } else {
651                        1
652                    };
653                    self.move_focused_by_days(delta);
654                    self.focus_focused_day_with_visibility(cx, true);
655                }
656            }
657
658            CalendarEvent::MoveFocusWeeks(delta) => {
659                if self.focus_is_on_calendar_day(cx) {
660                    self.move_focused_by_days(*delta * 7);
661                    self.focus_focused_day_with_visibility(cx, true);
662                }
663            }
664
665            CalendarEvent::MoveFocusWeekStart => {
666                if self.focus_is_on_calendar_day(cx) {
667                    let focused_date = self.focused_date.get();
668                    let offset = (7 + focused_date.weekday().num_days_from_monday() as i64
669                        - self.week_starts_on.get().num_days_from_monday() as i64)
670                        % 7;
671                    self.move_focused_by_days(-offset);
672                    self.focus_focused_day_with_visibility(cx, true);
673                }
674            }
675
676            CalendarEvent::MoveFocusWeekEnd => {
677                if self.focus_is_on_calendar_day(cx) {
678                    let focused_date = self.focused_date.get();
679                    let offset = 6
680                        - ((7 + focused_date.weekday().num_days_from_monday() as i64
681                            - self.week_starts_on.get().num_days_from_monday() as i64)
682                            % 7);
683                    self.move_focused_by_days(offset);
684                    self.focus_focused_day_with_visibility(cx, true);
685                }
686            }
687
688            CalendarEvent::MoveFocusMonth(delta) => {
689                if self.focus_is_on_calendar_day(cx) {
690                    self.move_focused_by_months(*delta);
691                    self.focus_focused_day_with_visibility(cx, true);
692                }
693            }
694
695            CalendarEvent::MoveFocusYear(delta) => {
696                if self.focus_is_on_calendar_day(cx) {
697                    self.move_focused_by_years(*delta);
698                    self.focus_focused_day_with_visibility(cx, true);
699                }
700            }
701
702            CalendarEvent::ActivateFocusedDate => {
703                if self.focus_is_on_calendar_day(cx) {
704                    let focused_date = self.focused_date.get();
705                    if focused_date.month() == self.view_date.get().month()
706                        && focused_date.year() == self.view_date.get().year()
707                    {
708                        if let Some(callback) = &self.on_select {
709                            (callback)(cx, focused_date);
710                        }
711                    }
712                }
713            }
714
715            CalendarEvent::RegisterDayCell(y, x, entity) => {
716                let index = Self::day_cell_index(*y, *x);
717                if let Some(day_cell) = self.day_cells.get_mut(index) {
718                    *day_cell = *entity;
719                }
720            }
721
722            CalendarEvent::AnnounceKeyboardHelp => {
723                if !self.keyboard_help_announced {
724                    self.keyboard_help
725                        .set(Localized::new("calendar-keyboard-help").to_string_local(cx));
726                    self.keyboard_help_announced = true;
727                }
728            }
729
730            CalendarEvent::RefreshLocalization => {
731                self.week_starts_on.set(Self::week_start_from_localization(cx));
732                self.keyboard_help.set(String::new());
733                self.keyboard_help_announced = false;
734            }
735
736            CalendarEvent::SelectDate(date) => {
737                self.focused_date.set(*date);
738                self.view_date.set(*date);
739                self.focus_focused_day(cx);
740                if let Some(callback) = &self.on_select {
741                    (callback)(cx, *date);
742                }
743            }
744        });
745
746        event.map(|window_event, meta| match window_event {
747            WindowEvent::KeyDown(code, _)
748                if self.focus_is_on_calendar_day(cx)
749                    && matches!(
750                        code,
751                        Code::ArrowUp
752                            | Code::ArrowDown
753                            | Code::ArrowLeft
754                            | Code::ArrowRight
755                            | Code::Home
756                            | Code::End
757                            | Code::PageUp
758                            | Code::PageDown
759                            | Code::Space
760                            | Code::Enter
761                    ) =>
762            {
763                meta.consume();
764            }
765
766            _ => {}
767        });
768    }
769}
770
771impl Handle<'_, Calendar> {
772    /// Set the callback triggered when a date is selected from the [Calendar] view.
773    pub fn on_select<F: 'static + Fn(&mut EventContext, NaiveDate)>(self, callback: F) -> Self {
774        self.modify(|calendar: &mut Calendar| calendar.on_select = Some(Box::new(callback)))
775    }
776}