1use chrono::{Datelike, Days, Months, NaiveDate};
2use datepicker::events::EventData;
3use appcui_proc_macro::CustomControl;
4
5const MINSPACE_FOR_SHORT_DATE: u32 = 15;
6const MINSPACE_FOR_LONG_DATE: u32 = 18;
7const MINSPACE_FOR_DROPBUTTON_DRAWING: u32 = 3;
8const MINSPACE_FOR_DATE_DRAWING: u32 = 5;
9const MIN_WIDTH_FOR_DATE_NAME: u32 = 6;
10const CALENDAR_WIDTH: u32 = 30;
11const CALENDAR_HEIGHT: u32 = 12;
12enum DateSize {
13 Large,
14 Small,
15 VerySmall,
16}
17#[derive(PartialEq, Eq)]
18enum HoveredDate {
19 DoubleLeftArrow,
20 LeftArrowYear,
21 RightArrowYear,
22 DoubleRightArrow,
23 LeftArrowMonth,
24 RightArrowMonth,
25 Day(u32),
26 None,
27}
28enum CharOrSpecialChar {
29 Regular(char),
30 Special(SpecialChar),
31}
32
33#[CustomControl(overwrite=OnPaint+OnDefaultAction+OnExpand+OnMouseEvent+OnKeyPressed, internal=true)]
34pub struct DatePicker {
35 header_y_ofs: i32,
36 expanded_panel_y: i32,
37 selected_date: NaiveDate,
38 date_string: String,
39 hover_date: HoveredDate,
40 virtual_date: NaiveDate,
41}
42
43impl DatePicker {
44 const DAYS: [&'static str; 7] = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
45 const MONTHS: [&'static str; 12] = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
46
47 pub fn with_date(date: NaiveDate, layout: Layout) -> Self {
57 let mut dp = DatePicker {
58 base: ControlBase::with_status_flags(layout, StatusFlags::Enabled | StatusFlags::Visible | StatusFlags::AcceptInput),
59 header_y_ofs: 0,
60 expanded_panel_y: 1,
61 selected_date: date,
62 date_string: Self::format_long_date(date),
63 hover_date: HoveredDate::None,
64 virtual_date: date,
65 };
66 dp.set_size_bounds(6, 1, u16::MAX, 1);
67 let date_len = dp.get_date_size();
68 match date_len {
69 DateSize::Large => {}
70 DateSize::Small => {
71 dp.date_string = Self::format_short_date(date);
72 }
73 DateSize::VerySmall => {
74 dp.date_string = Self::format_very_short_date(date);
75 }
76 }
77 dp
78 }
79
80 pub fn new(date_str: &str, layout: Layout) -> Self {
90 let date = date_str.parse::<NaiveDate>().unwrap();
91 Self::with_date(date, layout)
92 }
93
94 pub fn set_date_str(&mut self, date_str: &str) {
96 self.selected_date = date_str.parse::<NaiveDate>().unwrap();
97 self.date_string = Self::format_long_date(self.selected_date);
98 }
99
100 pub fn set_date(&mut self, date: NaiveDate) {
102 self.selected_date = date;
103 self.date_string = Self::format_long_date(date);
104 }
105
106 #[inline(always)]
108 pub fn date(&self) -> NaiveDate {
109 self.selected_date
110 }
111
112 fn update_date(&mut self, date: NaiveDate) {
113 if date != self.selected_date {
114 self.selected_date = date;
115 self.date_string = Self::format_long_date(date);
116
117 self.raise_event(ControlEvent {
118 emitter: self.handle,
119 receiver: self.event_processor,
120 data: ControlEventData::DatePicker(EventData { date: self.selected_date }),
121 });
122 }
123 }
124
125 fn jump_to_month(date: NaiveDate, target_month: u32) -> NaiveDate {
126 let year = date.year();
127 let day = date.day();
128
129 let mut new_date = NaiveDate::from_ymd_opt(year, target_month, 1).unwrap();
130
131 let last_day_of_month = (1..=31)
133 .rev()
134 .find(|&d| NaiveDate::from_ymd_opt(year, target_month, d).is_some())
135 .unwrap();
136
137 new_date = new_date.with_day(day.min(last_day_of_month)).unwrap();
139
140 new_date
141 }
142 fn mouse_over_calendar(&mut self, x: i32, y: i32) -> HoveredDate {
143 if !self.is_expanded() {
144 return HoveredDate::None;
145 }
146 if y == 1 + self.expanded_panel_y {
147 if x == 5 {
148 return HoveredDate::LeftArrowYear;
149 }
150 if x == 12 {
151 return HoveredDate::RightArrowYear;
152 }
153 if x == 2 || x == 3 {
154 return HoveredDate::DoubleLeftArrow;
155 }
156 if x == 14 || x == 15 {
157 return HoveredDate::DoubleRightArrow;
158 }
159
160 if x == 20 {
161 return HoveredDate::LeftArrowMonth;
162 }
163 if x == 26 {
164 return HoveredDate::RightArrowMonth;
165 }
166 }
167 let mut col = self.get_first_day_index() * 4 + 3;
168 let mut row = 4 + self.expanded_panel_y;
169 let last_day = self.days_in_month() as i32;
170
171 for i in 0..last_day {
172 let day = i + 1;
173
174 if (y == row) && (x == col || x == (col - 1)) {
175 self.virtual_date = self.virtual_date.with_day(day as u32).unwrap();
176 return HoveredDate::Day(day as u32);
177 }
178
179 col += 4;
180 if col >= 30 {
181 col = 3;
182 row += 1;
183 }
184 }
185 HoveredDate::None
186 }
187
188 fn format_very_short_date(selected_date: NaiveDate) -> String {
189 selected_date.format("%d.%m.%y").to_string()
190 }
191 fn format_short_date(selected_date: NaiveDate) -> String {
192 selected_date.format("%d.%m.%Y").to_string()
193 }
194
195 fn format_long_date(selected_date: NaiveDate) -> String {
196 selected_date.format("%Y, %b, %d").to_string()
197 }
198
199 fn get_date_size(&self) -> DateSize {
200 let width: u32 = self.size().width;
201 if width > MINSPACE_FOR_LONG_DATE {
202 DateSize::Large
203 } else if width > MINSPACE_FOR_SHORT_DATE {
204 DateSize::Small
205 } else {
206 DateSize::VerySmall
207 }
208 }
209 fn get_printed_chars(&self) -> u32 {
210 let wdth = self.size().width - MIN_WIDTH_FOR_DATE_NAME;
211 if (self.date_string.len() as u32) > wdth {
212 wdth
213 } else {
214 self.date_string.len() as u32
215 }
216 }
217
218 fn days_in_month(&self) -> u32 {
219 let month = self.virtual_date.month();
220 let year = self.virtual_date.year();
221 let next_month = if month == 12 { 1 } else { month + 1 };
222 let next_month_year = if month == 12 { year + 1 } else { year };
223
224 let first_of_next_month = NaiveDate::from_ymd_opt(next_month_year, next_month, 1).unwrap();
225 let first_of_current_month = NaiveDate::from_ymd_opt(year, month, 1).unwrap();
226
227 first_of_next_month.signed_duration_since(first_of_current_month).num_days() as u32
228 }
229
230 fn get_first_day_index(&self) -> i32 {
231 let first_day = self.virtual_date.with_day(1).unwrap().format("%a").to_string();
232 for i in 0..DatePicker::DAYS.len() {
233 if first_day.starts_with(DatePicker::DAYS[i]) {
234 return i as i32;
235 }
236 }
237 0
238 }
239}
240
241impl OnPaint for DatePicker {
242 fn on_paint(&self, surface: &mut Surface, theme: &Theme) {
243 let size = self.size();
244 let col_text = match () {
245 _ if !self.is_enabled() => theme.button.regular.text.inactive,
246 _ if self.has_focus() => theme.button.regular.text.focused,
247 _ if self.is_mouse_over() => theme.button.regular.text.hovered,
248 _ => theme.button.regular.text.normal,
249 };
250
251 let space_char = Character::with_attributes(' ', col_text);
252
253 let date_size = self.get_date_size();
255 let mut format = TextFormatBuilder::new()
256 .position(1, self.header_y_ofs)
257 .attribute(col_text)
258 .align(TextAlignment::Left)
259 .wrap_type(WrapType::SingleLineWrap((size.width - MIN_WIDTH_FOR_DATE_NAME) as u16))
260 .build();
261 surface.fill_horizontal_line(0, self.header_y_ofs, (size.width - MINSPACE_FOR_DATE_DRAWING) as i32, space_char);
262 let mut buf: [u8; 16] = [0; 16];
263 match date_size {
264 DateSize::Large => {
265 if let Some(txt) = crate::utils::FormatDate::normal(&self.selected_date, &mut buf) {
266 format.set_chars_count(txt.len() as u16);
267 surface.write_text(txt, &format);
268 }
269 }
270 DateSize::Small => {
271 if let Some(txt) = crate::utils::FormatDate::dmy(&self.selected_date, &mut buf, b'.') {
272 format.set_chars_count(txt.len() as u16);
273 surface.write_text(txt, &format);
274 }
275 }
276 DateSize::VerySmall => {
277 if let Some(txt) = crate::utils::FormatDate::short(&self.selected_date, &mut buf, b'.') {
278 format.set_chars_count(txt.len() as u16);
279 surface.write_text(txt, &format);
280 }
281 }
282 }
283
284 if size.width >= MINSPACE_FOR_DROPBUTTON_DRAWING {
285 let px = (size.width - MINSPACE_FOR_DROPBUTTON_DRAWING) as i32;
286 surface.fill_horizontal_line_with_size(px, self.header_y_ofs, 3, space_char);
287 surface.write_char(px + 1, self.header_y_ofs, Character::with_attributes(SpecialChar::TriangleDown, col_text));
288 }
289
290 if self.is_expanded() {
292 let size = self.expanded_size();
293 let col = theme.menu.text.normal;
294 let space_char = Character::with_attributes(' ', col);
295 surface.fill_rect(
296 Rect::with_size(0, self.expanded_panel_y, size.width as u16, (size.height - 1) as u16),
297 space_char,
298 );
299 surface.draw_rect(
300 Rect::with_size(0, self.expanded_panel_y, size.width as u16, (size.height - 1) as u16),
301 LineType::Single,
302 col,
303 );
304 surface.draw_horizontal_line(1, 2 + self.expanded_panel_y, (CALENDAR_WIDTH - 1) as i32, LineType::SingleRound, col);
305 surface.write_char(0, 2 + self.expanded_panel_y, Character::with_attributes(SpecialChar::BoxMidleLeft, col));
306 surface.write_char(
307 (CALENDAR_WIDTH - 1) as i32,
308 2 + self.expanded_panel_y,
309 Character::with_attributes(SpecialChar::BoxMidleRight, col),
310 );
311
312 let year = self.virtual_date.year();
313 let p_y = 1 + self.expanded_panel_y;
314 surface.write_char(7, p_y, Character::with_attributes(((year / 1000) + 48) as u8, col));
315 surface.write_char(8, p_y, Character::with_attributes(((year / 100) % 10 + 48) as u8, col));
316 surface.write_char(9, p_y, Character::with_attributes(((year / 10) % 10 + 48) as u8, col));
317 surface.write_char(10, p_y, Character::with_attributes((year % 10 + 48) as u8, col));
318
319 fn set_char(surface: &mut Surface, x: i32, y: i32, char_or_special: CharOrSpecialChar, condition: bool, theme: &Theme) {
320 let attr = if condition { theme.menu.text.hovered } else { theme.menu.text.normal };
321 let character = match char_or_special {
322 CharOrSpecialChar::Regular(c) => Character::with_attributes(c, attr),
323 CharOrSpecialChar::Special(sc) => Character::with_attributes(sc, attr),
324 };
325 surface.write_char(x, y, character);
326 }
327 let y_pos = 1 + self.expanded_panel_y;
328
329 set_char(
330 surface,
331 5,
332 y_pos,
333 CharOrSpecialChar::Special(SpecialChar::TriangleLeft),
334 self.hover_date == HoveredDate::LeftArrowYear,
335 theme,
336 );
337 set_char(
338 surface,
339 12,
340 y_pos,
341 CharOrSpecialChar::Special(SpecialChar::TriangleRight),
342 self.hover_date == HoveredDate::RightArrowYear,
343 theme,
344 );
345 set_char(
346 surface,
347 2,
348 y_pos,
349 CharOrSpecialChar::Regular('<'),
350 self.hover_date == HoveredDate::DoubleLeftArrow,
351 theme,
352 );
353 set_char(
354 surface,
355 3,
356 y_pos,
357 CharOrSpecialChar::Regular('<'),
358 self.hover_date == HoveredDate::DoubleLeftArrow,
359 theme,
360 );
361 set_char(
362 surface,
363 14,
364 y_pos,
365 CharOrSpecialChar::Regular('>'),
366 self.hover_date == HoveredDate::DoubleRightArrow,
367 theme,
368 );
369 set_char(
370 surface,
371 15,
372 y_pos,
373 CharOrSpecialChar::Regular('>'),
374 self.hover_date == HoveredDate::DoubleRightArrow,
375 theme,
376 );
377 set_char(
378 surface,
379 20,
380 y_pos,
381 CharOrSpecialChar::Special(SpecialChar::TriangleLeft),
382 self.hover_date == HoveredDate::LeftArrowMonth,
383 theme,
384 );
385 set_char(
386 surface,
387 26,
388 y_pos,
389 CharOrSpecialChar::Special(SpecialChar::TriangleRight),
390 self.hover_date == HoveredDate::RightArrowMonth,
391 theme,
392 );
393
394 let month = Self::MONTHS[self.virtual_date.month0() as usize];
395 surface.write_ascii(22, 1 + self.expanded_panel_y, month.as_bytes(), col, false);
396
397 let mut x = 2;
398 for i in 0..7 {
399 surface.write_ascii(
400 x,
401 3 + self.expanded_panel_y,
402 DatePicker::DAYS[i].as_bytes(),
403 theme.menu.text.inactive,
404 false,
405 );
406 x += 4;
407 }
408
409 let mut day_row = 4 + self.expanded_panel_y;
410 let mut day_col = self.get_first_day_index() * 4 + 3;
411
412 let last_day = self.days_in_month();
413
414 for i in 0..last_day {
415 let day = i + 1;
416 surface.write_char(day_col, day_row, Character::with_attributes((day % 10 + 48) as u8 as char, col));
417 if day > 9 {
418 surface.write_char(day_col - 1, day_row, Character::with_attributes((day / 10 + 48) as u8 as char, col));
419 }
420 if day == self.selected_date.day()
421 && self.selected_date.month() == self.virtual_date.month()
422 && self.selected_date.year() == self.virtual_date.year()
423 {
424 surface.fill_horizontal_line_with_size(
425 day_col - 2,
426 day_row,
427 4,
428 Character::with_attributes(0, theme.menu.text.pressed_or_selected),
429 );
430 } else if self.virtual_date.day() == day {
431 surface.fill_horizontal_line_with_size(day_col - 2, day_row, 4, Character::with_attributes(0, theme.menu.text.hovered));
432 }
433 day_col += 4;
434 if day_col >= 30 {
435 day_col = 3;
436 day_row += 1;
437 }
438 }
439 }
440 }
441}
442
443impl OnDefaultAction for DatePicker {
444 fn on_default_action(&mut self) {
445 if self.is_expanded() {
446 self.pack();
447 } else {
448 self.expand(Size::new(CALENDAR_WIDTH, CALENDAR_HEIGHT), Size::new(CALENDAR_WIDTH, CALENDAR_HEIGHT));
449 }
450 }
451}
452impl OnExpand for DatePicker {
453 fn on_expand(&mut self, direction: ExpandedDirection) {
454 self.virtual_date = self.selected_date;
455 match direction {
456 ExpandedDirection::OnTop => {
457 self.expanded_panel_y = 0;
458 self.header_y_ofs = (self.expanded_size().height as i32) - 1;
459 }
460 ExpandedDirection::OnBottom => {
461 self.expanded_panel_y = 1;
462 self.header_y_ofs = 0;
463 }
464 }
465 self.hover_date = HoveredDate::None;
466 }
467 fn on_pack(&mut self) {
468 self.expanded_panel_y = 1;
469 self.header_y_ofs = 0;
470 }
471}
472
473impl OnMouseEvent for DatePicker {
474 fn on_mouse_event(&mut self, event: &MouseEvent) -> EventProcessStatus {
475 match event {
476 MouseEvent::Enter => {
477 if !self.is_expanded() && (self.date_string.len() as i32) > (self.get_printed_chars() as i32) {
478 self.show_tooltip(self.date_string.as_str())
479 }
480 EventProcessStatus::Processed
481 }
482
483 MouseEvent::Leave => {
484 self.hide_tooltip();
485 EventProcessStatus::Processed
486 }
487 MouseEvent::Over(p) => {
488 let hd = self.mouse_over_calendar(p.x, p.y);
489 if hd != self.hover_date {
490 self.hover_date = hd;
491 }
492 EventProcessStatus::Processed
493 }
494 MouseEvent::Pressed(data) => {
495 let hd = self.mouse_over_calendar(data.x, data.y);
496 if hd != HoveredDate::None {
497 match hd {
498 HoveredDate::DoubleLeftArrow => {
499 self.virtual_date = self.virtual_date - Months::new(120);
500 }
501 HoveredDate::LeftArrowYear => {
502 self.virtual_date = self.virtual_date - Months::new(12);
503 }
504 HoveredDate::RightArrowYear => {
505 self.virtual_date = self.virtual_date + Months::new(12);
506 }
507 HoveredDate::DoubleRightArrow => {
508 self.virtual_date = self.virtual_date + Months::new(120);
509 }
510 HoveredDate::LeftArrowMonth => {
511 self.virtual_date = self.virtual_date - Months::new(1);
512 }
513 HoveredDate::RightArrowMonth => {
514 self.virtual_date = self.virtual_date + Months::new(1);
515 }
516 HoveredDate::Day(day) => {
517 self.update_date(self.virtual_date.with_day(day).unwrap());
518 self.on_default_action();
519 }
520
521 _ => {}
522 }
523
524 return EventProcessStatus::Processed;
525 }
526 self.on_default_action();
527 EventProcessStatus::Processed
528 }
529 _ => EventProcessStatus::Ignored,
530 }
531 }
532}
533
534impl OnKeyPressed for DatePicker {
535 fn on_key_pressed(&mut self, key: Key, _character: char) -> EventProcessStatus {
536 let expanded = self.is_expanded();
537
538 match key.value() {
539 key!("F") | key!("S") | key!("O") | key!("N") | key!("D") => {
540 let month = match key.value() {
541 key!("F") => 2,
542 key!("S") => 9,
543 key!("O") => 10,
544 key!("N") => 11,
545 key!("D") => 12,
546 _ => unreachable!(),
547 };
548 if expanded {
549 self.virtual_date = Self::jump_to_month(self.virtual_date, month);
550 } else {
551 self.update_date(Self::jump_to_month(self.selected_date, month));
552 }
553 return EventProcessStatus::Processed;
554 }
555
556 key!("J") | key!("A") | key!("M") | key!("Shift+J") | key!("Shift+A") | key!("Shift+M") => {
557 let mut val = 1i32;
558 let month_char = match key.value() {
559 key!("J") => "J",
560 key!("A") => "A",
561 key!("M") => "M",
562 key!("Shift+J") => {
563 val = -1;
564 "J"
565 }
566 key!("Shift+A") => {
567 val = -1;
568 "A"
569 }
570 key!("Shift+M") => {
571 val = -1;
572 "M"
573 }
574 _ => unreachable!(),
575 };
576 let target_month: &mut NaiveDate = if expanded { &mut self.virtual_date } else { &mut self.selected_date };
577
578 let month = {
579 let mut current_month = target_month.month() as i32 + val;
580 if current_month > 12 {
581 current_month = 1;
582 }
583 if current_month < 1 {
584 current_month = 12;
585 }
586 for _ in 0..Self::MONTHS.len() {
587 if Self::MONTHS[(current_month - 1) as usize].starts_with(month_char) {
588 break;
589 } else {
590 current_month += val;
591 if current_month > 12 {
592 current_month = 1;
593 }
594 if current_month < 1 {
595 current_month = 12;
596 }
597 }
598 }
599 current_month
600 };
601 if expanded {
602 self.virtual_date = Self::jump_to_month(self.virtual_date, month as u32);
603 } else {
604 self.update_date(Self::jump_to_month(self.selected_date, month as u32));
605 }
606 return EventProcessStatus::Processed;
607 }
608
609 _ => {}
610 }
611
612 if !expanded {
613 match key.value() {
614 key!("Escape") => {
615 return EventProcessStatus::Ignored;
616 }
617 key!("Up") => {
618 self.update_date(self.selected_date + Days::new(1));
619 return EventProcessStatus::Processed;
620 }
621
622 key!("Down") => {
623 self.update_date(self.selected_date - Days::new(1));
624 return EventProcessStatus::Processed;
625 }
626
627 key!("Shift+Up") => {
628 self.update_date(self.selected_date + Months::new(1));
629 return EventProcessStatus::Processed;
630 }
631
632 key!("Shift+Down") => {
633 self.update_date(self.selected_date - Months::new(1));
634 return EventProcessStatus::Processed;
635 }
636
637 key!("Ctrl+Up") => {
638 self.update_date(self.selected_date + Months::new(12));
639 return EventProcessStatus::Processed;
640 }
641
642 key!("Ctrl+Down") => {
643 self.update_date(self.selected_date - Months::new(12));
644 return EventProcessStatus::Processed;
645 }
646
647 key!("Ctrl+Shift+Up") => {
648 self.update_date(self.selected_date + Months::new(120));
649 return EventProcessStatus::Processed;
650 }
651
652 key!("Ctrl+Shift+Down") => {
653 self.update_date(self.selected_date - Months::new(120));
654 return EventProcessStatus::Processed;
655 }
656
657 key!("Enter") | key!("Space") => {
658 self.on_default_action();
659 return EventProcessStatus::Processed;
660 }
661 _ => {}
662 }
663 EventProcessStatus::Ignored
664 } else {
665 match key.value() {
666 key!("Escape") => {
667 self.pack();
668 return EventProcessStatus::Processed;
669 }
670
671 key!("Up") => {
672 self.virtual_date = self.virtual_date - Days::new(7);
673 return EventProcessStatus::Processed;
674 }
675
676 key!("Down") => {
677 self.virtual_date = self.virtual_date + Days::new(7);
678 return EventProcessStatus::Processed;
679 }
680
681 key!("Left") => {
682 self.virtual_date = self.virtual_date - Days::new(1);
683 return EventProcessStatus::Processed;
684 }
685
686 key!("Right") => {
687 self.virtual_date = self.virtual_date + Days::new(1);
688 return EventProcessStatus::Processed;
689 }
690
691 key!("Shift+Left") => {
692 self.virtual_date = self.virtual_date - Months::new(1);
693 return EventProcessStatus::Processed;
694 }
695
696 key!("Shift+Right") => {
697 self.virtual_date = self.virtual_date + Months::new(1);
698 return EventProcessStatus::Processed;
699 }
700
701 key!("Ctrl+Left") => {
702 self.virtual_date = self.virtual_date - Months::new(12);
703 return EventProcessStatus::Processed;
704 }
705
706 key!("Ctrl+Right") => {
707 self.virtual_date = self.virtual_date + Months::new(12);
708 return EventProcessStatus::Processed;
709 }
710
711 key!("Ctrl+Shift+Left") => {
712 self.virtual_date = self.virtual_date - Months::new(120);
713 return EventProcessStatus::Processed;
714 }
715
716 key!("Ctrl+Shift+Right") => {
717 self.virtual_date = self.virtual_date + Months::new(120);
718 return EventProcessStatus::Processed;
719 }
720
721 key!("Enter") => {
722 self.update_date(self.virtual_date);
723 self.on_default_action();
724 return EventProcessStatus::Processed;
725 }
726 _ => {}
727 }
728
729 EventProcessStatus::Ignored
730 }
731 }
732}