gpui_component/time/
date_picker.rs

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