1use chrono::{Datelike, Duration, NaiveDate, Weekday};
2
3use crate::{
4 icons::{ICON_CHEVRON_LEFT, ICON_CHEVRON_RIGHT},
5 prelude::*,
6};
7
8pub 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 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 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 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 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 .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 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}