Skip to main content

gpui_component/time/
date_picker.rs

1use std::rc::Rc;
2
3use chrono::{NaiveDate, 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 gpui_base::{DatePicker as BaseDatePicker, ElementExt as _};
24
25const CONTEXT: &'static str = "DatePicker";
26pub(crate) fn init(cx: &mut App) {
27    cx.bind_keys([
28        KeyBinding::new("enter", Confirm { secondary: false }, Some(CONTEXT)),
29        KeyBinding::new("escape", Cancel, Some(CONTEXT)),
30        KeyBinding::new("delete", Delete, Some(CONTEXT)),
31        KeyBinding::new("backspace", Delete, Some(CONTEXT)),
32    ])
33}
34
35/// Events emitted by the DatePicker.
36#[derive(Clone)]
37pub enum DatePickerEvent {
38    Change(Date),
39}
40
41/// Preset value for DateRangePreset.
42#[derive(Clone)]
43pub enum DateRangePresetValue {
44    Single(NaiveDate),
45    Range(NaiveDate, NaiveDate),
46}
47
48/// Preset for date range selection.
49#[derive(Clone)]
50pub struct DateRangePreset {
51    label: SharedString,
52    value: DateRangePresetValue,
53}
54
55impl DateRangePreset {
56    /// Creates a new DateRangePreset with a date.
57    pub fn single(label: impl Into<SharedString>, date: NaiveDate) -> Self {
58        DateRangePreset {
59            label: label.into(),
60            value: DateRangePresetValue::Single(date),
61        }
62    }
63    /// Creates a new DateRangePreset with a range of dates.
64    pub fn range(label: impl Into<SharedString>, start: NaiveDate, end: NaiveDate) -> Self {
65        DateRangePreset {
66            label: label.into(),
67            value: DateRangePresetValue::Range(start, end),
68        }
69    }
70}
71
72/// Use to store the state of the date picker.
73pub struct DatePickerState {
74    focus_handle: FocusHandle,
75    date: Date,
76    open: bool,
77    calendar: Entity<CalendarState>,
78    date_format: SharedString,
79    number_of_months: usize,
80    disabled_matcher: Option<Rc<Matcher>>,
81    _subscriptions: Vec<Subscription>,
82    /// The first day of the week. Defaults to Sunday.
83    first_day_of_week: Weekday,
84    bounds: Bounds<Pixels>,
85}
86
87impl Focusable for DatePickerState {
88    fn focus_handle(&self, _: &App) -> FocusHandle {
89        self.focus_handle.clone()
90    }
91}
92impl EventEmitter<DatePickerEvent> for DatePickerState {}
93
94impl DatePickerState {
95    /// Create a date state.
96    pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
97        Self::new_with_range(false, window, cx)
98    }
99
100    /// Create a date state with range mode.
101    pub fn range(window: &mut Window, cx: &mut Context<Self>) -> Self {
102        Self::new_with_range(true, window, cx)
103    }
104
105    fn new_with_range(is_range: bool, window: &mut Window, cx: &mut Context<Self>) -> Self {
106        let date = if is_range {
107            Date::Range(None, None)
108        } else {
109            Date::Single(None)
110        };
111
112        let calendar = cx.new(|cx| {
113            let mut this = CalendarState::new(window, cx);
114            this.set_date(date, window, cx);
115            this
116        });
117
118        let _subscriptions = vec![cx.subscribe_in(
119            &calendar,
120            window,
121            |this, _, ev: &CalendarEvent, window, cx| match ev {
122                CalendarEvent::Selected(date) => {
123                    this.update_date(*date, true, window, cx);
124                    this.focus_handle.focus(window, cx);
125                }
126            },
127        )];
128
129        Self {
130            focus_handle: cx.focus_handle(),
131            date,
132            calendar,
133            open: false,
134            date_format: "%Y/%m/%d".into(),
135            number_of_months: 1,
136            disabled_matcher: None,
137            _subscriptions,
138            first_day_of_week: Weekday::Sun,
139            bounds: Bounds::default(),
140        }
141    }
142
143    /// Set the date format of the date picker to display in Input, default: "%Y/%m/%d".
144    pub fn date_format(mut self, format: impl Into<SharedString>) -> Self {
145        self.date_format = format.into();
146        self
147    }
148
149    /// Set the number of months calendar view to display, default is 1.
150    pub fn number_of_months(mut self, number_of_months: usize) -> Self {
151        self.number_of_months = number_of_months;
152        self
153    }
154
155    /// Set the first day of the week.
156    pub fn first_day_of_week(mut self, day: Weekday) -> Self {
157        self.first_day_of_week = day;
158        self
159    }
160
161    /// Get the date of the date picker.
162    pub fn date(&self) -> Date {
163        self.date
164    }
165
166    /// Set the date of the date picker.
167    pub fn set_date(&mut self, date: impl Into<Date>, window: &mut Window, cx: &mut Context<Self>) {
168        self.update_date(date.into(), false, window, cx);
169    }
170
171    /// Set the disabled match for the calendar.
172    pub fn disabled_matcher(mut self, disabled: impl Into<Matcher>) -> Self {
173        self.disabled_matcher = Some(Rc::new(disabled.into()));
174        self
175    }
176
177    /// Set the year range for the internal calendar.
178    ///
179    /// Default is 50 years before and after the current year.
180    /// `range` uses a half-open interval `(start, end)` where `end` is exclusive.
181    pub fn set_year_range(&mut self, range: (i32, i32), cx: &mut Context<Self>) {
182        self.calendar.update(cx, |state, cx| {
183            state.set_year_range(range, cx);
184        });
185    }
186
187    fn update_date(&mut self, date: Date, emit: bool, window: &mut Window, cx: &mut Context<Self>) {
188        self.date = date;
189        self.calendar.update(cx, |view, cx| {
190            view.set_date(date, window, cx);
191        });
192        self.open = false;
193        if emit {
194            cx.emit(DatePickerEvent::Change(date));
195        }
196        cx.notify();
197    }
198
199    /// Set the disabled matcher of the date picker.
200    fn set_canlendar_disabled_matcher(&mut self, _: &mut Window, cx: &mut Context<Self>) {
201        let matcher = self.disabled_matcher.clone();
202        self.calendar.update(cx, |state, _| {
203            state.set_disabled_matcher_shared(matcher);
204        });
205    }
206
207    fn on_escape(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
208        if !self.open {
209            cx.propagate();
210        }
211
212        self.focus_back_if_need(window, cx);
213        self.open = false;
214
215        cx.notify();
216    }
217
218    fn on_delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
219        self.clean(&ClickEvent::default(), window, cx);
220    }
221
222    // To focus the Picker Input, if current focus in is on the container.
223    //
224    // This is because mouse down out the Calendar, GPUI will move focus to the container.
225    // So we need to move focus back to the Picker Input.
226    //
227    // But if mouse down target is some other focusable element (e.g.: [`crate::Input`]), we should not move focus.
228    fn focus_back_if_need(&mut self, window: &mut Window, cx: &mut Context<Self>) {
229        if !self.open {
230            return;
231        }
232
233        if let Some(focused) = window.focused(cx) {
234            if focused.contains(&self.focus_handle, window) {
235                self.focus_handle.focus(window, cx);
236            }
237        }
238    }
239
240    fn clean(&mut self, _: &gpui::ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
241        cx.stop_propagation();
242        match self.date {
243            Date::Single(_) => {
244                self.update_date(Date::Single(None), true, window, cx);
245            }
246            Date::Range(_, _) => {
247                self.update_date(Date::Range(None, None), true, window, cx);
248            }
249        }
250    }
251
252    fn toggle_calendar(&mut self, _: &gpui::ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
253        self.open = !self.open;
254        cx.notify();
255    }
256
257    fn select_preset(
258        &mut self,
259        preset: &DateRangePreset,
260        window: &mut Window,
261        cx: &mut Context<Self>,
262    ) {
263        match preset.value {
264            DateRangePresetValue::Single(single) => {
265                self.update_date(Date::Single(Some(single)), true, window, cx)
266            }
267            DateRangePresetValue::Range(start, end) => {
268                self.update_date(Date::Range(Some(start), Some(end)), true, window, cx)
269            }
270        }
271    }
272}
273
274/// A DatePicker element.
275#[derive(IntoElement)]
276pub struct DatePicker {
277    id: ElementId,
278    style: StyleRefinement,
279    state: Entity<DatePickerState>,
280    cleanable: bool,
281    placeholder: Option<SharedString>,
282    size: Size,
283    number_of_months: usize,
284    presets: Option<Vec<DateRangePreset>>,
285    appearance: bool,
286    focus_ring_enabled: bool,
287    disabled: bool,
288}
289
290impl Sizable for DatePicker {
291    fn with_size(mut self, size: impl Into<Size>) -> Self {
292        self.size = size.into();
293        self
294    }
295}
296impl Focusable for DatePicker {
297    fn focus_handle(&self, cx: &App) -> FocusHandle {
298        self.state.focus_handle(cx)
299    }
300}
301
302impl Styled for DatePicker {
303    fn style(&mut self) -> &mut StyleRefinement {
304        &mut self.style
305    }
306}
307
308impl Disableable for DatePicker {
309    fn disabled(mut self, disabled: bool) -> Self {
310        self.disabled = disabled;
311        self
312    }
313}
314
315impl crate::FocusableExt for DatePicker {
316    fn focus_ring(mut self, enabled: bool) -> Self {
317        self.focus_ring_enabled = enabled;
318        self
319    }
320
321    fn is_focus_ring_enabled(&self) -> bool {
322        self.focus_ring_enabled
323    }
324}
325
326impl Render for DatePickerState {
327    fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl gpui::IntoElement {
328        Empty
329    }
330}
331
332impl DatePicker {
333    /// Create a new DatePicker with the given [`DatePickerState`].
334    pub fn new(state: &Entity<DatePickerState>) -> Self {
335        Self {
336            id: ("date-picker", state.entity_id()).into(),
337            state: state.clone(),
338            cleanable: false,
339            placeholder: None,
340            size: Size::default(),
341            style: StyleRefinement::default(),
342            number_of_months: 1,
343            presets: None,
344            appearance: true,
345            focus_ring_enabled: true,
346            disabled: false,
347        }
348    }
349
350    /// Set the placeholder of the date picker, default: "".
351    pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
352        self.placeholder = Some(placeholder.into());
353        self
354    }
355
356    /// Set whether to show the clear button when the input field is not empty, default is false.
357    pub fn cleanable(mut self, cleanable: bool) -> Self {
358        self.cleanable = cleanable;
359        self
360    }
361
362    /// Set preset ranges for the date picker.
363    pub fn presets(mut self, presets: Vec<DateRangePreset>) -> Self {
364        self.presets = Some(presets);
365        self
366    }
367
368    /// Set number of months to display in the calendar, default is 1.
369    pub fn number_of_months(mut self, number_of_months: usize) -> Self {
370        self.number_of_months = number_of_months;
371        self
372    }
373
374    /// Set appearance of the date picker, if false, the date picker will be in a minimal style.
375    pub fn appearance(mut self, appearance: bool) -> Self {
376        self.appearance = appearance;
377        self
378    }
379}
380
381impl RenderOnce for DatePicker {
382    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
383        self.state.update(cx, |state, cx| {
384            state.set_canlendar_disabled_matcher(window, cx);
385        });
386        let month_count = self.number_of_months.max(1) as f32;
387
388        // This for keep focus border style, when click on the popup.
389        let is_focused = self.focus_handle(cx).contains_focused(window, cx);
390        let state = self.state.read(cx);
391        let show_clean = self.cleanable && state.date.is_some();
392        let placeholder = self
393            .placeholder
394            .clone()
395            .unwrap_or_else(|| t!("DatePicker.placeholder").into());
396        let display_title = state
397            .date
398            .format(&state.date_format)
399            .unwrap_or(placeholder.clone());
400
401        let (bg, fg) = input_style(self.disabled, cx);
402
403        let picker_state = self.state.clone();
404
405        BaseDatePicker::new(self.id, &state.focus_handle)
406            .open(state.open)
407            .when(state.date.is_some(), |this| {
408                this.aria_value(display_title.clone())
409            })
410            .disabled(self.disabled)
411            .on_open_change(move |open, window, cx| {
412                picker_state.update(cx, |state, cx| {
413                    if !open {
414                        state.focus_back_if_need(window, cx);
415                    }
416                    state.open = open;
417                    cx.notify();
418                });
419            })
420            .key_context(CONTEXT)
421            .on_action(window.listener_for(&self.state, DatePickerState::on_delete))
422            .flex_none()
423            .w_full()
424            .relative()
425            .on_prepaint({
426                let state = self.state.clone();
427                move |bounds, _, cx| state.update(cx, |state, _| state.bounds = bounds)
428            })
429            .input_text_size(self.size)
430            .refine_style(&self.style)
431            .child(
432                div()
433                    .id("date-picker-input")
434                    .relative()
435                    .flex()
436                    .items_center()
437                    .justify_between()
438                    .when(self.appearance, |this| {
439                        this.bg(bg)
440                            .text_color(fg)
441                            .when(self.disabled, |this| this.opacity(0.5))
442                            .border_1()
443                            .border_color(cx.theme().input)
444                            .rounded(cx.theme().radius)
445                            .when(is_focused, |this| {
446                                this.border_1().border_color(cx.theme().ring)
447                            })
448                    })
449                    .when(
450                        is_focused && self.appearance && !self.disabled && self.focus_ring_enabled,
451                        |this| this.focus_ring_style(window, cx),
452                    )
453                    .input_text_size(self.size)
454                    .input_size(self.size)
455                    .when(!state.open && !self.disabled, |this| {
456                        this.on_click(
457                            window.listener_for(&self.state, DatePickerState::toggle_calendar),
458                        )
459                    })
460                    .child(
461                        h_flex()
462                            .w_full()
463                            .min_w_0()
464                            .overflow_hidden()
465                            .whitespace_nowrap()
466                            .items_center()
467                            .justify_between()
468                            .gap_1()
469                            .child(
470                                div()
471                                    .flex_1()
472                                    .min_w_0()
473                                    .overflow_hidden()
474                                    .whitespace_nowrap()
475                                    .truncate()
476                                    .when(!state.date.is_some(), |this| {
477                                        this.text_color(cx.theme().muted_foreground)
478                                    })
479                                    .child(display_title),
480                            )
481                            .when(!self.disabled, |this| {
482                                this.when(show_clean, |this| {
483                                    this.child(clear_button(cx).on_click(
484                                        window.listener_for(&self.state, DatePickerState::clean),
485                                    ))
486                                })
487                                .when(!show_clean, |this| {
488                                    this.child(
489                                        Icon::new(IconName::Calendar)
490                                            .xsmall()
491                                            .text_color(cx.theme().muted_foreground),
492                                    )
493                                })
494                            }),
495                    ),
496            )
497            .when(state.open, |this| {
498                this.child(
499                    deferred(crate::popover::dropdown_popup(
500                        ("date-picker-popup", self.state.entity_id()),
501                        state.bounds,
502                        div()
503                            .occlude()
504                            .p_3()
505                            .popover_style(cx)
506                            .on_mouse_up_out(
507                                MouseButton::Left,
508                                window.listener_for(&self.state, |view, _, window, cx| {
509                                    view.on_escape(&Cancel, window, cx);
510                                }),
511                            )
512                            .child(
513                                h_flex()
514                                    .gap_3()
515                                    .h_full()
516                                    .items_start()
517                                    .when_some(self.presets.clone(), |this, presets| {
518                                        this.child(v_flex().my_1().gap_2().justify_end().children(
519                                            presets.into_iter().enumerate().map(|(i, preset)| {
520                                                Button::new(("preset", i))
521                                                    .small()
522                                                    .ghost()
523                                                    .tab_stop(false)
524                                                    .label(preset.label.clone())
525                                                    .on_click(window.listener_for(
526                                                        &self.state,
527                                                        move |this, _, window, cx| {
528                                                            this.select_preset(&preset, window, cx);
529                                                        },
530                                                    ))
531                                            }),
532                                        ))
533                                    })
534                                    .child(
535                                        Calendar::new(&state.calendar)
536                                            .number_of_months(self.number_of_months)
537                                            .first_day_of_week(state.first_day_of_week)
538                                            .border_0()
539                                            .rounded_none()
540                                            .p_0()
541                                            .map(|this| match self.size {
542                                                Size::Small => this.w(px(196.) * month_count),
543                                                Size::Large => this.w(px(280.) * month_count),
544                                                _ => this.w(px(224.) * month_count),
545                                            })
546                                            .with_size(self.size),
547                                    ),
548                            ),
549                        cx,
550                    ))
551                    .with_priority(gpui_base::POPUP_PRIORITY),
552                )
553            })
554    }
555}