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            .disabled(self.disabled)
408            .on_open_change(move |open, window, cx| {
409                picker_state.update(cx, |state, cx| {
410                    if !open {
411                        state.focus_back_if_need(window, cx);
412                    }
413                    state.open = open;
414                    cx.notify();
415                });
416            })
417            .key_context(CONTEXT)
418            .on_action(window.listener_for(&self.state, DatePickerState::on_delete))
419            .flex_none()
420            .w_full()
421            .relative()
422            .on_prepaint({
423                let state = self.state.clone();
424                move |bounds, _, cx| state.update(cx, |state, _| state.bounds = bounds)
425            })
426            .input_text_size(self.size)
427            .refine_style(&self.style)
428            .child(
429                div()
430                    .id("date-picker-input")
431                    .relative()
432                    .flex()
433                    .items_center()
434                    .justify_between()
435                    .when(self.appearance, |this| {
436                        this.bg(bg)
437                            .text_color(fg)
438                            .when(self.disabled, |this| this.opacity(0.5))
439                            .border_1()
440                            .border_color(cx.theme().input)
441                            .rounded(cx.theme().radius)
442                            .when(is_focused, |this| {
443                                this.border_1().border_color(cx.theme().ring)
444                            })
445                    })
446                    .when(
447                        is_focused && self.appearance && !self.disabled && self.focus_ring_enabled,
448                        |this| this.focus_ring_style(window, cx),
449                    )
450                    .input_text_size(self.size)
451                    .input_size(self.size)
452                    .when(!state.open && !self.disabled, |this| {
453                        this.on_click(
454                            window.listener_for(&self.state, DatePickerState::toggle_calendar),
455                        )
456                    })
457                    .child(
458                        h_flex()
459                            .w_full()
460                            .min_w_0()
461                            .overflow_hidden()
462                            .whitespace_nowrap()
463                            .items_center()
464                            .justify_between()
465                            .gap_1()
466                            .child(
467                                div()
468                                    .flex_1()
469                                    .min_w_0()
470                                    .overflow_hidden()
471                                    .whitespace_nowrap()
472                                    .truncate()
473                                    .when(!state.date.is_some(), |this| {
474                                        this.text_color(cx.theme().muted_foreground)
475                                    })
476                                    .child(display_title),
477                            )
478                            .when(!self.disabled, |this| {
479                                this.when(show_clean, |this| {
480                                    this.child(clear_button(cx).on_click(
481                                        window.listener_for(&self.state, DatePickerState::clean),
482                                    ))
483                                })
484                                .when(!show_clean, |this| {
485                                    this.child(
486                                        Icon::new(IconName::Calendar)
487                                            .xsmall()
488                                            .text_color(cx.theme().muted_foreground),
489                                    )
490                                })
491                            }),
492                    ),
493            )
494            .when(state.open, |this| {
495                this.child(
496                    deferred(crate::popover::dropdown_popup(
497                        ("date-picker-popup", self.state.entity_id()),
498                        state.bounds,
499                        div()
500                            .occlude()
501                            .p_3()
502                            .popover_style(cx)
503                            .on_mouse_up_out(
504                                MouseButton::Left,
505                                window.listener_for(&self.state, |view, _, window, cx| {
506                                    view.on_escape(&Cancel, window, cx);
507                                }),
508                            )
509                            .child(
510                                h_flex()
511                                    .gap_3()
512                                    .h_full()
513                                    .items_start()
514                                    .when_some(self.presets.clone(), |this, presets| {
515                                        this.child(v_flex().my_1().gap_2().justify_end().children(
516                                            presets.into_iter().enumerate().map(|(i, preset)| {
517                                                Button::new(("preset", i))
518                                                    .small()
519                                                    .ghost()
520                                                    .tab_stop(false)
521                                                    .label(preset.label.clone())
522                                                    .on_click(window.listener_for(
523                                                        &self.state,
524                                                        move |this, _, window, cx| {
525                                                            this.select_preset(&preset, window, cx);
526                                                        },
527                                                    ))
528                                            }),
529                                        ))
530                                    })
531                                    .child(
532                                        Calendar::new(&state.calendar)
533                                            .number_of_months(self.number_of_months)
534                                            .first_day_of_week(state.first_day_of_week)
535                                            .border_0()
536                                            .rounded_none()
537                                            .p_0()
538                                            .map(|this| match self.size {
539                                                Size::Small => this.w(px(196.) * month_count),
540                                                Size::Large => this.w(px(280.) * month_count),
541                                                _ => this.w(px(224.) * month_count),
542                                            })
543                                            .with_size(self.size),
544                                    ),
545                            ),
546                        cx,
547                    ))
548                    .with_priority(gpui_base::POPUP_PRIORITY),
549                )
550            })
551    }
552}