1use std::rc::Rc;
2
3use chrono::{NaiveDate, NaiveDateTime, NaiveTime, Weekday};
4use gpui::{
5 App, AppContext, Bounds, ClickEvent, Context, ElementId, Empty, Entity, EventEmitter,
6 FocusHandle, Focusable, InteractiveElement as _, IntoElement, KeyBinding, MouseButton,
7 ParentElement as _, Pixels, Render, RenderOnce, SharedString, StatefulInteractiveElement as _,
8 StyleRefinement, Styled, Subscription, Window, deferred, div, prelude::FluentBuilder as _, px,
9};
10use rust_i18n::t;
11
12use crate::ThemeStyled as _;
13use crate::{
14 ActiveTheme, Disableable, Icon, IconName, Sizable, Size, StyleSized as _, StyledExt as _,
15 actions::{Cancel, Confirm},
16 button::{Button, ButtonVariants as _},
17 h_flex,
18 input::{Delete, clear_button, input_style},
19 v_flex,
20};
21
22use super::calendar::{Calendar, CalendarEvent, CalendarState, Date, Matcher};
23use super::time_field::{
24 HourCycle, TimeField, TimeFieldEvent, TimeFieldState, TimePrecision, tabular_figures,
25};
26use gpui_base::{DatePicker as BaseDatePicker, ElementExt as _};
27
28const CONTEXT: &'static str = "DatePicker";
29pub(crate) fn init(cx: &mut App) {
30 cx.bind_keys([
31 KeyBinding::new("enter", Confirm { secondary: false }, Some(CONTEXT)),
32 KeyBinding::new("escape", Cancel, Some(CONTEXT)),
33 KeyBinding::new("delete", Delete, Some(CONTEXT)),
34 KeyBinding::new("backspace", Delete, Some(CONTEXT)),
35 ])
36}
37
38#[derive(Clone)]
40pub enum DatePickerEvent {
41 Change(DateTime),
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum DateTime {
53 Single(Option<NaiveDateTime>),
54 Range(Option<NaiveDateTime>, Option<NaiveDateTime>),
55}
56
57impl From<NaiveDateTime> for DateTime {
58 fn from(value: NaiveDateTime) -> Self {
59 Self::Single(Some(value))
60 }
61}
62
63impl From<(NaiveDateTime, NaiveDateTime)> for DateTime {
64 fn from((start, end): (NaiveDateTime, NaiveDateTime)) -> Self {
65 Self::Range(Some(start), Some(end))
66 }
67}
68
69impl DateTime {
70 pub fn is_some(&self) -> bool {
71 self.date().is_some()
72 }
73
74 pub fn is_complete(&self) -> bool {
75 self.date().is_complete()
76 }
77
78 pub fn start(&self) -> Option<NaiveDateTime> {
79 match self {
80 Self::Single(v) | Self::Range(v, _) => *v,
81 }
82 }
83
84 pub fn end(&self) -> Option<NaiveDateTime> {
85 match self {
86 Self::Range(_, v) => *v,
87 Self::Single(_) => None,
88 }
89 }
90
91 pub fn date(&self) -> Date {
93 match self {
94 Self::Single(v) => Date::Single(v.map(|v| v.date())),
95 Self::Range(a, b) => Date::Range(a.map(|v| v.date()), b.map(|v| v.date())),
96 }
97 }
98
99 pub fn format(&self, format: &str) -> Option<SharedString> {
101 match self {
102 Self::Single(Some(v)) => Some(v.format(format).to_string().into()),
103 Self::Range(Some(a), Some(b)) => {
104 Some(format!("{} - {}", a.format(format), b.format(format)).into())
105 }
106 _ => None,
107 }
108 }
109}
110
111#[derive(Clone)]
113pub enum DateRangePresetValue {
114 Single(NaiveDate),
115 Range(NaiveDate, NaiveDate),
116 DateTime(DateTime),
117}
118
119#[derive(Clone)]
121pub struct DateRangePreset {
122 label: SharedString,
123 value: DateRangePresetValue,
124}
125
126impl DateRangePreset {
127 pub fn single(label: impl Into<SharedString>, date: NaiveDate) -> Self {
129 DateRangePreset {
130 label: label.into(),
131 value: DateRangePresetValue::Single(date),
132 }
133 }
134 pub fn range(label: impl Into<SharedString>, start: NaiveDate, end: NaiveDate) -> Self {
136 DateRangePreset {
137 label: label.into(),
138 value: DateRangePresetValue::Range(start, end),
139 }
140 }
141 pub fn date_time(label: impl Into<SharedString>, value: impl Into<DateTime>) -> Self {
145 DateRangePreset {
146 label: label.into(),
147 value: DateRangePresetValue::DateTime(value.into()),
148 }
149 }
150}
151
152pub struct DatePickerState {
154 focus_handle: FocusHandle,
155 date: Date,
156 open: bool,
157 calendar: Entity<CalendarState>,
158 date_format: Option<SharedString>,
159 number_of_months: usize,
160 disabled_matcher: Option<Rc<Matcher>>,
161 time_precision: Option<TimePrecision>,
163 hour_cycle: HourCycle,
164 default_time: NaiveTime,
165 start_time: NaiveTime,
166 end_time: NaiveTime,
169 time_field: Entity<TimeFieldState>,
170 time_field_pushed: bool,
171 _subscriptions: Vec<Subscription>,
172 first_day_of_week: Weekday,
174 bounds: Bounds<Pixels>,
175}
176
177impl Focusable for DatePickerState {
178 fn focus_handle(&self, _: &App) -> FocusHandle {
179 self.focus_handle.clone()
180 }
181}
182impl EventEmitter<DatePickerEvent> for DatePickerState {}
183
184impl DatePickerState {
185 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
187 Self::new_with_range(false, window, cx)
188 }
189
190 pub fn range(window: &mut Window, cx: &mut Context<Self>) -> Self {
192 Self::new_with_range(true, window, cx)
193 }
194
195 fn new_with_range(is_range: bool, window: &mut Window, cx: &mut Context<Self>) -> Self {
196 let date = if is_range {
197 Date::Range(None, None)
198 } else {
199 Date::Single(None)
200 };
201
202 let calendar = cx.new(|cx| {
203 let mut this = CalendarState::new(window, cx);
204 this.set_date(date, window, cx);
205 this
206 });
207 let time_field = cx.new(|cx| TimeFieldState::new(window, cx));
208
209 let _subscriptions = vec![
210 cx.subscribe_in(
211 &calendar,
212 window,
213 |this, _, ev: &CalendarEvent, window, cx| match ev {
214 CalendarEvent::Selected(date) => {
215 this.select_date(*date, window, cx);
216 }
217 },
218 ),
219 cx.subscribe_in(
220 &time_field,
221 window,
222 |this, _, ev: &TimeFieldEvent, _, cx| match ev {
223 TimeFieldEvent::Change(time) => {
224 this.start_time = *time;
225 this.emit_change(cx);
226 }
227 },
228 ),
229 ];
230
231 Self {
232 focus_handle: cx.focus_handle(),
233 date,
234 calendar,
235 open: false,
236 date_format: None,
237 number_of_months: 1,
238 disabled_matcher: None,
239 time_precision: None,
240 hour_cycle: HourCycle::default(),
241 default_time: NaiveTime::MIN,
242 start_time: NaiveTime::MIN,
243 end_time: NaiveTime::MIN,
244 time_field,
245 time_field_pushed: false,
246 _subscriptions,
247 first_day_of_week: Weekday::Sun,
248 bounds: Bounds::default(),
249 }
250 }
251
252 pub fn date_format(mut self, format: impl Into<SharedString>) -> Self {
257 self.date_format = Some(format.into());
258 self
259 }
260
261 pub fn time_precision(mut self, precision: TimePrecision) -> Self {
270 self.time_precision = Some(precision);
271 self.default_time = precision.truncate(self.default_time);
272 self.start_time = precision.truncate(self.start_time);
273 self.end_time = precision.truncate(self.end_time);
274 self.time_field_pushed = false;
275 self
276 }
277
278 pub fn hour_cycle(mut self, hour_cycle: HourCycle) -> Self {
280 self.hour_cycle = hour_cycle;
281 self.time_field_pushed = false;
282 self
283 }
284
285 pub fn default_time(mut self, time: NaiveTime) -> Self {
287 let time = self.truncate_time(time);
288 self.default_time = time;
289 self.start_time = time;
290 self.end_time = time;
291 self.time_field_pushed = false;
292 self
293 }
294
295 pub fn number_of_months(mut self, number_of_months: usize) -> Self {
297 self.number_of_months = number_of_months;
298 self
299 }
300
301 pub fn first_day_of_week(mut self, day: Weekday) -> Self {
303 self.first_day_of_week = day;
304 self
305 }
306
307 pub fn date(&self) -> Date {
309 self.date
310 }
311
312 pub fn date_time(&self) -> DateTime {
314 match self.date {
315 Date::Single(date) => DateTime::Single(date.map(|d| d.and_time(self.start_time))),
316 Date::Range(start, end) => DateTime::Range(
317 start.map(|d| d.and_time(self.start_time)),
318 end.map(|d| d.and_time(self.end_time)),
319 ),
320 }
321 }
322
323 pub fn set_date(&mut self, date: impl Into<Date>, window: &mut Window, cx: &mut Context<Self>) {
325 self.update_date(date.into(), false, window, cx);
326 }
327
328 pub fn set_date_time(
330 &mut self,
331 value: impl Into<DateTime>,
332 window: &mut Window,
333 cx: &mut Context<Self>,
334 ) {
335 let value = value.into();
336 let start = value.start().map_or(self.default_time, |v| v.time());
337 let end = value.end().map_or(start, |v| v.time());
338 self.set_times(start, end, window, cx);
339 self.update_date(value.date(), false, window, cx);
340 }
341
342 pub fn disabled_matcher(mut self, disabled: impl Into<Matcher>) -> Self {
344 self.disabled_matcher = Some(Rc::new(disabled.into()));
345 self
346 }
347
348 pub fn set_year_range(&mut self, range: (i32, i32), cx: &mut Context<Self>) {
353 self.calendar.update(cx, |state, cx| {
354 state.set_year_range(range, cx);
355 });
356 }
357
358 fn edited_time_precision(&self) -> Option<TimePrecision> {
360 self.time_precision.filter(|_| self.date.is_single())
361 }
362
363 fn truncate_time(&self, time: NaiveTime) -> NaiveTime {
364 self.time_precision
365 .map_or(time, |precision| precision.truncate(time))
366 }
367
368 fn set_times(
369 &mut self,
370 start: NaiveTime,
371 end: NaiveTime,
372 window: &mut Window,
373 cx: &mut Context<Self>,
374 ) {
375 self.start_time = self.truncate_time(start);
376 self.end_time = self.truncate_time(end);
377 self.push_time_field(window, cx);
378 }
379
380 fn push_time_field(&mut self, window: &mut Window, cx: &mut Context<Self>) {
386 let precision = self.time_precision.unwrap_or_default();
387 let (hour_cycle, time) = (self.hour_cycle, self.start_time);
388 self.time_field.update(cx, |field, cx| {
389 field.set_precision(precision, window, cx);
390 field.set_hour_cycle(hour_cycle, window, cx);
391 field.set_time(time, window, cx);
392 });
393 self.time_field_pushed = true;
394 }
395
396 fn emit_change(&mut self, cx: &mut Context<Self>) {
397 if self.date.is_complete() {
398 cx.emit(DatePickerEvent::Change(self.date_time()));
399 }
400 cx.notify();
401 }
402
403 fn select_date(&mut self, date: Date, window: &mut Window, cx: &mut Context<Self>) {
404 if self.edited_time_precision().is_some() {
405 if date == self.date {
406 self.set_open(false, window, cx);
409 return;
410 }
411 self.date = date;
413 self.emit_change(cx);
414 } else {
415 self.update_date(date, true, window, cx);
416 self.focus_handle.focus(window, cx);
417 }
418 }
419
420 fn update_date(&mut self, date: Date, emit: bool, window: &mut Window, cx: &mut Context<Self>) {
421 self.date = date;
422 self.calendar.update(cx, |view, cx| {
423 view.set_date(date, window, cx);
424 });
425 self.open = false;
426 if emit {
427 cx.emit(DatePickerEvent::Change(self.date_time()));
428 }
429 cx.notify();
430 }
431
432 fn sync_children(&mut self, window: &mut Window, cx: &mut Context<Self>) {
434 let matcher = self.disabled_matcher.clone();
435 self.calendar.update(cx, |state, _| {
436 state.set_disabled_matcher_shared(matcher);
437 });
438 if !self.time_field_pushed {
440 self.push_time_field(window, cx);
441 }
442 }
443
444 fn set_open(&mut self, open: bool, window: &mut Window, cx: &mut Context<Self>) {
445 if !open {
446 self.focus_back_if_need(window, cx);
447 }
448 self.open = open;
449 cx.notify();
450 }
451
452 fn on_escape(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
453 if !self.open {
454 cx.propagate();
455 }
456 self.set_open(false, window, cx);
457 }
458
459 fn on_delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
460 self.clean(&ClickEvent::default(), window, cx);
461 }
462
463 fn focus_back_if_need(&mut self, window: &mut Window, cx: &mut Context<Self>) {
471 if !self.open {
472 return;
473 }
474
475 if let Some(focused) = window.focused(cx) {
476 if focused.contains(&self.focus_handle, window)
477 || self.focus_handle.contains_focused(window, cx)
478 {
479 self.focus_handle.focus(window, cx);
480 }
481 }
482 }
483
484 fn clean(&mut self, _: &gpui::ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
485 cx.stop_propagation();
486 let default_time = self.default_time;
487 self.set_times(default_time, default_time, window, cx);
488 match self.date {
489 Date::Single(_) => {
490 self.update_date(Date::Single(None), true, window, cx);
491 }
492 Date::Range(_, _) => {
493 self.update_date(Date::Range(None, None), true, window, cx);
494 }
495 }
496 }
497
498 fn toggle_calendar(
499 &mut self,
500 _: &gpui::ClickEvent,
501 window: &mut Window,
502 cx: &mut Context<Self>,
503 ) {
504 let open = !self.open;
505 self.set_open(open, window, cx);
506 }
507
508 fn select_preset(
509 &mut self,
510 preset: &DateRangePreset,
511 window: &mut Window,
512 cx: &mut Context<Self>,
513 ) {
514 match preset.value {
515 DateRangePresetValue::Single(single) => {
516 self.update_date(Date::Single(Some(single)), true, window, cx)
517 }
518 DateRangePresetValue::Range(start, end) => {
519 self.update_date(Date::Range(Some(start), Some(end)), true, window, cx)
520 }
521 DateRangePresetValue::DateTime(value) => {
522 self.set_date_time(value, window, cx);
523 cx.emit(DatePickerEvent::Change(self.date_time()));
524 }
525 }
526 self.focus_handle.focus(window, cx);
527 }
528
529 fn display_format(&self) -> SharedString {
530 if let Some(format) = &self.date_format {
531 return format.clone();
532 }
533 match self.edited_time_precision() {
534 Some(precision) => format!("%Y/%m/%d {}", precision.format(self.hour_cycle)).into(),
535 None => "%Y/%m/%d".into(),
536 }
537 }
538}
539
540#[derive(IntoElement)]
542pub struct DatePicker {
543 id: ElementId,
544 style: StyleRefinement,
545 state: Entity<DatePickerState>,
546 cleanable: bool,
547 placeholder: Option<SharedString>,
548 size: Size,
549 number_of_months: usize,
550 presets: Option<Vec<DateRangePreset>>,
551 appearance: bool,
552 focus_ring_enabled: bool,
553 disabled: bool,
554}
555
556impl Sizable for DatePicker {
557 fn with_size(mut self, size: impl Into<Size>) -> Self {
558 self.size = size.into();
559 self
560 }
561}
562impl Focusable for DatePicker {
563 fn focus_handle(&self, cx: &App) -> FocusHandle {
564 self.state.focus_handle(cx)
565 }
566}
567
568impl Styled for DatePicker {
569 fn style(&mut self) -> &mut StyleRefinement {
570 &mut self.style
571 }
572}
573
574impl Disableable for DatePicker {
575 fn disabled(mut self, disabled: bool) -> Self {
576 self.disabled = disabled;
577 self
578 }
579}
580
581impl crate::FocusableExt for DatePicker {
582 fn focus_ring(mut self, enabled: bool) -> Self {
583 self.focus_ring_enabled = enabled;
584 self
585 }
586
587 fn is_focus_ring_enabled(&self) -> bool {
588 self.focus_ring_enabled
589 }
590}
591
592impl Render for DatePickerState {
593 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl gpui::IntoElement {
594 Empty
595 }
596}
597
598impl DatePicker {
599 pub fn new(state: &Entity<DatePickerState>) -> Self {
601 Self {
602 id: ("date-picker", state.entity_id()).into(),
603 state: state.clone(),
604 cleanable: false,
605 placeholder: None,
606 size: Size::default(),
607 style: StyleRefinement::default(),
608 number_of_months: 1,
609 presets: None,
610 appearance: true,
611 focus_ring_enabled: true,
612 disabled: false,
613 }
614 }
615
616 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
618 self.placeholder = Some(placeholder.into());
619 self
620 }
621
622 pub fn cleanable(mut self, cleanable: bool) -> Self {
624 self.cleanable = cleanable;
625 self
626 }
627
628 pub fn presets(mut self, presets: Vec<DateRangePreset>) -> Self {
630 self.presets = Some(presets);
631 self
632 }
633
634 pub fn number_of_months(mut self, number_of_months: usize) -> Self {
636 self.number_of_months = number_of_months;
637 self
638 }
639
640 pub fn appearance(mut self, appearance: bool) -> Self {
642 self.appearance = appearance;
643 self
644 }
645
646 fn render_time_field(size: Size, state: &DatePickerState, cx: &App) -> impl IntoElement {
647 h_flex()
648 .map(|this| match size {
649 Size::Small => this.mt_2().pt_2(),
650 _ => this.mt_3().pt_3(),
651 })
652 .gap_3()
653 .justify_between()
654 .border_t_1()
655 .border_color(cx.theme().border)
656 .child(
657 div()
658 .map(|this| match size {
659 Size::Small => this.text_xs(),
660 _ => this.text_sm(),
661 })
662 .text_color(cx.theme().muted_foreground)
663 .child(SharedString::from(t!("DatePicker.time"))),
664 )
665 .child(
666 TimeField::new(&state.time_field)
667 .with_id("time")
668 .with_size(size),
669 )
670 }
671}
672
673impl RenderOnce for DatePicker {
674 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
675 self.state.update(cx, |state, cx| {
676 state.sync_children(window, cx);
677 });
678 let month_count = self.number_of_months.max(1) as f32;
679
680 let is_focused = self.focus_handle(cx).contains_focused(window, cx);
682 let state = self.state.read(cx);
683 let show_clean = self.cleanable && state.date.is_some();
684 let placeholder = self
685 .placeholder
686 .clone()
687 .unwrap_or_else(|| t!("DatePicker.placeholder").into());
688 let display_title = state
689 .date_time()
690 .format(&state.display_format())
691 .unwrap_or(placeholder.clone());
692
693 let (bg, fg) = input_style(self.disabled, cx);
694
695 let picker_state = self.state.clone();
696
697 BaseDatePicker::new(self.id, &state.focus_handle)
698 .open(state.open)
699 .when(state.date.is_some(), |this| {
700 this.aria_value(display_title.clone())
701 })
702 .disabled(self.disabled)
703 .on_open_change(move |open, window, cx| {
704 picker_state.update(cx, |state, cx| state.set_open(open, window, cx));
705 })
706 .key_context(CONTEXT)
707 .on_action(window.listener_for(&self.state, DatePickerState::on_delete))
708 .flex_none()
709 .w_full()
710 .relative()
711 .on_prepaint({
712 let state = self.state.clone();
713 move |bounds, _, cx| state.update(cx, |state, _| state.bounds = bounds)
714 })
715 .input_text_size(self.size)
716 .refine_style(&self.style)
717 .child(
718 div()
719 .id("date-picker-input")
720 .relative()
721 .flex()
722 .items_center()
723 .justify_between()
724 .when(self.appearance, |this| {
725 this.bg(bg)
726 .text_color(fg)
727 .when(self.disabled, |this| this.opacity(0.5))
728 .border_1()
729 .border_color(cx.theme().input)
730 .rounded(cx.theme().radius)
731 .when(is_focused, |this| {
732 this.border_1().border_color(cx.theme().ring)
733 })
734 })
735 .when(
736 is_focused && self.appearance && !self.disabled && self.focus_ring_enabled,
737 |this| this.focus_ring_style(window, cx),
738 )
739 .input_text_size(self.size)
740 .input_size(self.size)
741 .when(!state.open && !self.disabled, |this| {
742 this.on_click(
743 window.listener_for(&self.state, DatePickerState::toggle_calendar),
744 )
745 })
746 .child(
747 h_flex()
748 .w_full()
749 .min_w_0()
750 .overflow_hidden()
751 .whitespace_nowrap()
752 .items_center()
753 .justify_between()
754 .gap_1()
755 .child(
756 div()
757 .flex_1()
758 .min_w_0()
759 .overflow_hidden()
760 .whitespace_nowrap()
761 .truncate()
762 .when(state.edited_time_precision().is_some(), |this| {
764 this.font_features(tabular_figures())
765 })
766 .when(!state.date.is_some(), |this| {
767 this.text_color(cx.theme().muted_foreground)
768 })
769 .child(display_title),
770 )
771 .when(!self.disabled, |this| {
772 this.when(show_clean, |this| {
773 this.child(clear_button(cx).on_click(
774 window.listener_for(&self.state, DatePickerState::clean),
775 ))
776 })
777 .when(!show_clean, |this| {
778 this.child(
779 Icon::new(IconName::Calendar)
780 .xsmall()
781 .text_color(cx.theme().muted_foreground),
782 )
783 })
784 }),
785 ),
786 )
787 .when(state.open, |this| {
788 this.child(
789 deferred(crate::popover::dropdown_popup(
790 ("date-picker-popup", self.state.entity_id()),
791 state.bounds,
792 div()
793 .occlude()
794 .p_3()
795 .popover_style(cx)
796 .on_mouse_up_out(
797 MouseButton::Left,
798 window.listener_for(&self.state, |view, _, window, cx| {
799 view.on_escape(&Cancel, window, cx);
800 }),
801 )
802 .child(
803 h_flex()
804 .gap_3()
805 .h_full()
806 .items_start()
807 .when_some(self.presets.clone(), |this, presets| {
808 this.child(v_flex().my_1().gap_2().justify_end().children(
809 presets.into_iter().enumerate().map(|(i, preset)| {
810 Button::new(("preset", i))
811 .small()
812 .ghost()
813 .tab_stop(false)
814 .label(preset.label.clone())
815 .on_click(window.listener_for(
816 &self.state,
817 move |this, _, window, cx| {
818 this.select_preset(&preset, window, cx);
819 },
820 ))
821 }),
822 ))
823 })
824 .child(
825 v_flex()
826 .child(
827 Calendar::new(&state.calendar)
828 .number_of_months(self.number_of_months)
829 .first_day_of_week(state.first_day_of_week)
830 .border_0()
831 .rounded_none()
832 .p_0()
833 .map(|this| match self.size {
834 Size::Small => {
835 this.w(px(196.) * month_count)
836 }
837 Size::Large => {
838 this.w(px(280.) * month_count)
839 }
840 _ => this.w(px(224.) * month_count),
841 })
842 .with_size(self.size),
843 )
844 .when_some(state.edited_time_precision(), |this, _| {
845 this.child(Self::render_time_field(
846 self.size, state, cx,
847 ))
848 }),
849 ),
850 ),
851 cx,
852 ))
853 .with_priority(gpui_base::POPUP_PRIORITY),
854 )
855 })
856 }
857}