1mod types;
4mod utils;
5
6pub use types::*;
7pub(crate) use utils::*;
8
9use crate::callback::{Callback, KeyHandler};
10use crate::core::element::{Element, IntoElement};
11use crate::core::event::{KeyCode, KeyEvent, MouseEvent};
12use crate::style::{BorderStyle, Length, Padding, Style, StyleSlot};
13use crate::widgets::{BorderLabels, Button, Center, Frame, FrameLabel, HStack, Text, VStack};
14use std::sync::Arc;
15
16#[derive(Clone)]
30pub struct DatePicker {
31 pub(crate) year: i32,
32 pub(crate) month: u32,
33 pub(crate) day: u32,
34 pub(crate) title: Option<Arc<str>>,
35 pub(crate) title_style: Style,
36 pub(crate) style: Style,
37 pub(crate) header_style: Style,
38 pub(crate) weekday_style: Style,
39 pub(crate) day_style: Style,
40 pub(crate) day_hover_style: StyleSlot,
41 pub(crate) selected_style: Style,
42 pub(crate) outside_month_style: Style,
43 pub(crate) nav_style: Style,
44 pub(crate) nav_hover_style: StyleSlot,
45 pub(crate) nav_disabled_style: Style,
46 pub(crate) show_outside_days: bool,
47 pub(crate) border: bool,
48 pub(crate) border_style: BorderStyle,
49 pub(crate) padding: Padding,
50 pub(crate) width: Length,
51 pub(crate) height: Length,
52 pub(crate) disabled: bool,
53 pub(crate) disabled_style: Style,
54 pub(crate) focusable: bool,
55 pub(crate) tab_stop: bool,
56 pub(crate) focus_key: Option<Arc<str>>,
57 pub(crate) on_focus: Option<Callback<DateEvent>>,
58 pub(crate) on_blur: Option<Callback<DateEvent>>,
59 pub(crate) on_select: Option<Callback<DateEvent>>,
60 pub(crate) on_prev_month: Option<Callback<()>>,
61 pub(crate) on_next_month: Option<Callback<()>>,
62 pub(crate) on_key: Option<KeyHandler>,
63}
64
65fn derived_focus_key(title: Option<&str>) -> Arc<str> {
66 match title {
67 Some(title) if !title.is_empty() => Arc::from(format!("__tui_lipan_datepicker:{title}")),
68 _ => Arc::from("__tui_lipan_datepicker"),
69 }
70}
71
72impl DatePicker {
73 pub fn new() -> Self {
75 Self {
76 year: 2024,
77 month: 1,
78 day: 1,
79 title: Some("Select Date".into()),
80 title_style: Style::default(),
81 style: Style::default(),
82 header_style: Style::default(),
83 weekday_style: Style::default(),
84 day_style: Style::default(),
85 day_hover_style: StyleSlot::Inherit,
86 selected_style: Style::default(),
87 outside_month_style: Style::default(),
88 nav_style: Style::default(),
89 nav_hover_style: StyleSlot::Inherit,
90 nav_disabled_style: Style::default(),
91 show_outside_days: false,
92 border: true,
93 border_style: BorderStyle::Rounded,
94 padding: Padding::default(),
95 width: Length::Auto,
96 height: Length::Auto,
97 disabled: false,
98 disabled_style: Style::default(),
99 focusable: true,
100 tab_stop: true,
101 focus_key: None,
102 on_focus: None,
103 on_blur: None,
104 on_select: None,
105 on_prev_month: None,
106 on_next_month: None,
107 on_key: None,
108 }
109 }
110
111 pub fn year(mut self, year: i32) -> Self {
113 self.year = year;
114 self
115 }
116
117 pub fn month(mut self, month: u32) -> Self {
119 self.month = month;
120 self
121 }
122
123 pub fn day(mut self, day: u32) -> Self {
125 self.day = day;
126 self
127 }
128
129 pub fn title(mut self, title: Option<impl Into<Arc<str>>>) -> Self {
131 self.title = title.map(Into::into);
132 self
133 }
134
135 pub fn title_style(mut self, style: Style) -> Self {
137 self.title_style = style;
138 self
139 }
140
141 pub fn style(mut self, style: Style) -> Self {
143 self.style = style;
144 self
145 }
146
147 pub fn header_style(mut self, style: Style) -> Self {
149 self.header_style = style;
150 self
151 }
152
153 pub fn weekday_style(mut self, style: Style) -> Self {
155 self.weekday_style = style;
156 self
157 }
158
159 pub fn day_style(mut self, style: Style) -> Self {
161 self.day_style = style;
162 self
163 }
164
165 pub fn day_hover_style(mut self, style: Style) -> Self {
167 self.day_hover_style = StyleSlot::Replace(style);
168 self
169 }
170
171 pub fn extend_day_hover_style(mut self, style: Style) -> Self {
173 self.day_hover_style = StyleSlot::Extend(style);
174 self
175 }
176
177 pub fn inherit_day_hover_style(mut self) -> Self {
179 self.day_hover_style = StyleSlot::Inherit;
180 self
181 }
182
183 pub fn day_hover_style_slot(mut self, slot: StyleSlot) -> Self {
185 self.day_hover_style = slot;
186 self
187 }
188
189 pub fn selected_style(mut self, style: Style) -> Self {
191 self.selected_style = style;
192 self
193 }
194
195 pub fn outside_month_style(mut self, style: Style) -> Self {
197 self.outside_month_style = style;
198 self
199 }
200
201 pub fn nav_style(mut self, style: Style) -> Self {
203 self.nav_style = style;
204 self
205 }
206
207 pub fn nav_hover_style(mut self, style: Style) -> Self {
209 self.nav_hover_style = StyleSlot::Replace(style);
210 self
211 }
212
213 pub fn extend_nav_hover_style(mut self, style: Style) -> Self {
215 self.nav_hover_style = StyleSlot::Extend(style);
216 self
217 }
218
219 pub fn inherit_nav_hover_style(mut self) -> Self {
221 self.nav_hover_style = StyleSlot::Inherit;
222 self
223 }
224
225 pub fn nav_hover_style_slot(mut self, slot: StyleSlot) -> Self {
227 self.nav_hover_style = slot;
228 self
229 }
230
231 pub fn nav_disabled_style(mut self, style: Style) -> Self {
233 self.nav_disabled_style = style;
234 self
235 }
236
237 pub fn show_outside_days(mut self, show: bool) -> Self {
239 self.show_outside_days = show;
240 self
241 }
242
243 pub fn border(mut self, border: bool) -> Self {
245 self.border = border;
246 self
247 }
248
249 pub fn border_style(mut self, border_style: BorderStyle) -> Self {
251 self.border_style = border_style;
252 self
253 }
254
255 pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
257 self.padding = padding.into();
258 self
259 }
260
261 pub fn width(mut self, width: Length) -> Self {
263 self.width = width;
264 self
265 }
266
267 pub fn height(mut self, height: Length) -> Self {
269 self.height = height;
270 self
271 }
272
273 pub fn disabled(mut self, disabled: bool) -> Self {
275 self.disabled = disabled;
276 self
277 }
278
279 pub fn disabled_style(mut self, style: Style) -> Self {
281 self.disabled_style = style;
282 self
283 }
284
285 pub fn focusable(mut self, focusable: bool) -> Self {
287 self.focusable = focusable;
288 self
289 }
290
291 pub fn tab_stop(mut self, tab_stop: bool) -> Self {
295 self.tab_stop = tab_stop;
296 self
297 }
298
299 pub fn focus_key(mut self, key: impl Into<Arc<str>>) -> Self {
306 self.focus_key = Some(key.into());
307 self
308 }
309
310 pub fn on_focus(mut self, cb: Callback<DateEvent>) -> Self {
312 self.on_focus = Some(cb);
313 self
314 }
315
316 pub fn on_blur(mut self, cb: Callback<DateEvent>) -> Self {
318 self.on_blur = Some(cb);
319 self
320 }
321
322 pub fn on_select(mut self, cb: Callback<DateEvent>) -> Self {
324 self.on_select = Some(cb);
325 self
326 }
327
328 pub fn on_prev_month(mut self, cb: Callback<()>) -> Self {
330 self.on_prev_month = Some(cb);
331 self
332 }
333
334 pub fn on_next_month(mut self, cb: Callback<()>) -> Self {
336 self.on_next_month = Some(cb);
337 self
338 }
339
340 pub fn on_key(mut self, handler: KeyHandler) -> Self {
342 self.on_key = Some(handler);
343 self
344 }
345}
346
347impl Default for DatePicker {
348 fn default() -> Self {
349 Self::new()
350 }
351}
352
353impl From<DatePicker> for Element {
354 fn from(picker: DatePicker) -> Self {
355 let months = [
356 "January",
357 "February",
358 "March",
359 "April",
360 "May",
361 "June",
362 "July",
363 "August",
364 "September",
365 "October",
366 "November",
367 "December",
368 ];
369
370 let year = picker.year;
371 let month = picker.month.clamp(1, 12);
372 let day = picker.day.clamp(1, days_in_month(year, month));
373 let focus_key = picker
374 .focus_key
375 .clone()
376 .unwrap_or_else(|| derived_focus_key(picker.title.as_deref()));
377
378 let header_label = format!(
379 "{} {}",
380 months[(month.saturating_sub(1) % 12) as usize],
381 year
382 );
383
384 let mut prev_button = Button::filled("◀")
385 .padding(0)
386 .style(picker.nav_style)
387 .hover_style_slot(picker.nav_hover_style)
388 .width(Length::Px(2))
389 .focusable(false)
390 .tab_stop(false);
391 let mut next_button = Button::filled("▶")
392 .padding(0)
393 .style(picker.nav_style)
394 .hover_style_slot(picker.nav_hover_style)
395 .width(Length::Px(2))
396 .focusable(false)
397 .tab_stop(false);
398
399 if picker.disabled {
400 prev_button = prev_button
401 .disabled(true)
402 .disabled_style(picker.nav_disabled_style);
403 next_button = next_button
404 .disabled(true)
405 .disabled_style(picker.nav_disabled_style);
406 } else if let Some(cb) = picker.on_prev_month.clone() {
407 prev_button = prev_button.on_click(Callback::new(move |_: MouseEvent| cb.emit(())));
408 } else {
409 prev_button = prev_button
410 .disabled(true)
411 .disabled_style(picker.nav_disabled_style);
412 }
413
414 if !picker.disabled {
415 if let Some(cb) = picker.on_next_month.clone() {
416 next_button = next_button.on_click(Callback::new(move |_: MouseEvent| cb.emit(())));
417 } else {
418 next_button = next_button
419 .disabled(true)
420 .disabled_style(picker.nav_disabled_style);
421 }
422 }
423
424 let header = HStack::new()
425 .gap(1)
426 .height(Length::Px(1))
427 .child(prev_button)
428 .child(Center::new().child(Text::new(header_label).style(picker.header_style)))
429 .child(next_button);
430
431 let days_header = HStack::new()
432 .gap(1)
433 .height(Length::Px(1))
434 .child(
435 Text::new("Su")
436 .style(picker.weekday_style)
437 .width(Length::Px(2)),
438 )
439 .child(
440 Text::new("Mo")
441 .style(picker.weekday_style)
442 .width(Length::Px(2)),
443 )
444 .child(
445 Text::new("Tu")
446 .style(picker.weekday_style)
447 .width(Length::Px(2)),
448 )
449 .child(
450 Text::new("We")
451 .style(picker.weekday_style)
452 .width(Length::Px(2)),
453 )
454 .child(
455 Text::new("Th")
456 .style(picker.weekday_style)
457 .width(Length::Px(2)),
458 )
459 .child(
460 Text::new("Fr")
461 .style(picker.weekday_style)
462 .width(Length::Px(2)),
463 )
464 .child(
465 Text::new("Sa")
466 .style(picker.weekday_style)
467 .width(Length::Px(2)),
468 );
469
470 let first_weekday = weekday(year, month, 1) as usize;
471 let days_in_current_month = days_in_month(year, month);
472 let (prev_year, prev_month_val) = prev_month(year, month);
473 let days_in_prev = days_in_month(prev_year, prev_month_val);
474
475 let mut calendar = VStack::new().gap(0).height(Length::Auto);
476 let mut day_counter = 1u32;
477 let mut next_day = 1u32;
478
479 for week in 0..6 {
480 let mut row = HStack::new().gap(1).height(Length::Px(1));
481 for weekday_idx in 0..7 {
482 let cell_index = week * 7 + weekday_idx;
483
484 if cell_index < first_weekday {
485 if picker.show_outside_days {
486 let day_val = days_in_prev - (first_weekday as u32 - cell_index as u32) + 1;
487 let label = format!("{:>2}", day_val);
488 let cell = Text::new(label)
489 .style(picker.outside_month_style)
490 .width(Length::Px(2));
491 row = row.child(cell);
492 } else {
493 row = row.child(Text::new(" ").width(Length::Px(2)));
494 }
495 continue;
496 }
497
498 if day_counter <= days_in_current_month {
499 let is_selected = day_counter == day;
500 let label = format!("{:>2}", day_counter);
501 let cell_event = DateEvent {
502 year,
503 month,
504 day: day_counter,
505 };
506 let mut button = Button::filled(label)
507 .padding(0)
508 .width(Length::Px(2))
509 .style(if is_selected {
510 picker.selected_style
511 } else {
512 picker.day_style
513 })
514 .hover_style_slot(picker.day_hover_style)
515 .focusable(picker.focusable && is_selected && !picker.disabled)
518 .tab_stop(picker.tab_stop && is_selected && !picker.disabled);
519
520 if is_selected {
521 if let Some(cb) = picker.on_focus.clone() {
522 button = button.on_focus(Callback::new(move |_| cb.emit(cell_event)));
523 }
524 if let Some(cb) = picker.on_blur.clone() {
525 button = button.on_blur(Callback::new(move |_| cb.emit(cell_event)));
526 }
527 }
528
529 if picker.disabled {
530 button = button.disabled(true).disabled_style(picker.disabled_style);
531 } else if let Some(cb) = picker.on_select.clone() {
532 let event = cell_event;
533 button =
534 button.on_click(Callback::new(move |_: MouseEvent| cb.emit(event)));
535 } else {
536 button = button.disabled(true).disabled_style(picker.day_style);
537 }
538
539 if is_selected && !picker.disabled {
540 let on_select = picker.on_select.clone();
541 let on_prev = picker.on_prev_month.clone();
542 let on_next = picker.on_next_month.clone();
543 let caller_on_key = picker.on_key.clone();
544 button = button.on_key(KeyHandler::new(move |key: KeyEvent| {
545 if caller_on_key
546 .as_ref()
547 .is_some_and(|handler| handler.handle(key))
548 {
549 return true;
550 }
551 handle_datepicker_key(
552 key, year, month, day, &on_select, &on_prev, &on_next,
553 )
554 }));
555 }
556
557 let cell: Element = if is_selected {
558 button.key(focus_key.clone())
559 } else {
560 button.into()
561 };
562 row = row.child(cell);
563 day_counter = day_counter.saturating_add(1);
564 } else if picker.show_outside_days {
565 let label = format!("{:>2}", next_day);
566 let cell = Text::new(label)
567 .style(picker.outside_month_style)
568 .width(Length::Px(2));
569 row = row.child(cell);
570 next_day = next_day.saturating_add(1);
571 } else {
572 row = row.child(Text::new(" ").width(Length::Px(2)));
573 }
574 }
575 calendar = calendar.child(row);
576 }
577
578 let content = VStack::new()
579 .gap(1)
580 .child(header)
581 .child(days_header)
582 .child(calendar);
583
584 let mut frame = Frame::new()
585 .border(picker.border)
586 .border_style(picker.border_style)
587 .padding(picker.padding)
588 .style(picker.style)
589 .child(content)
590 .width(picker.width)
591 .height(picker.height);
592
593 if let Some(title) = picker.title.clone() {
594 frame = frame
595 .header(BorderLabels::new().left(FrameLabel::new(title).style(picker.title_style)));
596 }
597
598 frame.into()
599 }
600}
601
602fn handle_datepicker_key(
603 key: KeyEvent,
604 year: i32,
605 month: u32,
606 day: u32,
607 on_select: &Option<Callback<DateEvent>>,
608 on_prev: &Option<Callback<()>>,
609 on_next: &Option<Callback<()>>,
610) -> bool {
611 if key.mods.ctrl || key.mods.alt || key.mods.super_key {
612 return false;
613 }
614
615 match key.code {
616 KeyCode::Left => {
617 emit_day_delta(year, month, day, -1, on_select);
618 true
619 }
620 KeyCode::Right => {
621 emit_day_delta(year, month, day, 1, on_select);
622 true
623 }
624 KeyCode::Up => {
625 emit_day_delta(year, month, day, -7, on_select);
626 true
627 }
628 KeyCode::Down => {
629 emit_day_delta(year, month, day, 7, on_select);
630 true
631 }
632 KeyCode::PageUp => {
633 let (y, m) = prev_month(year, month);
634 let d = day.min(days_in_month(y, m));
635 if let Some(cb) = on_select {
636 cb.emit(DateEvent {
637 year: y,
638 month: m,
639 day: d,
640 });
641 } else if let Some(cb) = on_prev {
642 cb.emit(());
643 }
644 true
645 }
646 KeyCode::PageDown => {
647 let (y, m) = next_month(year, month);
648 let d = day.min(days_in_month(y, m));
649 if let Some(cb) = on_select {
650 cb.emit(DateEvent {
651 year: y,
652 month: m,
653 day: d,
654 });
655 } else if let Some(cb) = on_next {
656 cb.emit(());
657 }
658 true
659 }
660 KeyCode::Home => {
661 if let Some(cb) = on_select {
662 cb.emit(DateEvent {
663 year,
664 month,
665 day: 1,
666 });
667 }
668 true
669 }
670 KeyCode::End => {
671 if let Some(cb) = on_select {
672 cb.emit(DateEvent {
673 year,
674 month,
675 day: days_in_month(year, month),
676 });
677 }
678 true
679 }
680 _ => false,
681 }
682}
683
684fn emit_day_delta(
685 year: i32,
686 month: u32,
687 day: u32,
688 delta: i32,
689 on_select: &Option<Callback<DateEvent>>,
690) {
691 let Some(cb) = on_select else {
692 return;
693 };
694 let (y, m, d) = shift_day(year, month, day, delta);
695 if y == year && m == month && d == day {
696 return;
697 }
698 cb.emit(DateEvent {
699 year: y,
700 month: m,
701 day: d,
702 });
703}