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