Skip to main content

rgpui_component/time/
date_picker.rs

1use std::rc::Rc;
2
3use chrono::NaiveDate;
4use rgpui::{
5    App, AppContext, ClickEvent, Context, ElementId, Empty, Entity, EventEmitter, FocusHandle,
6    Focusable, InteractiveElement as _, IntoElement, KeyBinding, MouseButton, ParentElement as _,
7    Render, RenderOnce, SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled,
8    Subscription, Window, anchored, deferred, div, prelude::FluentBuilder as _, px,
9};
10use rust_i18n::t;
11
12use crate::{
13    ActiveTheme, Disableable, Icon, IconName, Sizable, Size, StyleSized as _, StyledExt as _,
14    actions::{Cancel, Confirm},
15    button::{Button, ButtonVariants as _},
16    h_flex,
17    input::{Delete, clear_button, input_style},
18    v_flex,
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, cx);
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    /// Set the year range for the internal calendar.
165    ///
166    /// Default is 50 years before and after the current year.
167    /// `range` uses a half-open interval `(start, end)` where `end` is exclusive.
168    pub fn set_year_range(&mut self, range: (i32, i32), cx: &mut Context<Self>) {
169        self.calendar.update(cx, |state, cx| {
170            state.set_year_range(range, cx);
171        });
172    }
173
174    fn update_date(&mut self, date: Date, emit: bool, window: &mut Window, cx: &mut Context<Self>) {
175        self.date = date;
176        self.calendar.update(cx, |view, cx| {
177            view.set_date(date, window, cx);
178        });
179        self.open = false;
180        if emit {
181            cx.emit(DatePickerEvent::Change(date));
182        }
183        cx.notify();
184    }
185
186    /// Set the disabled matcher of the date picker.
187    fn set_canlendar_disabled_matcher(&mut self, _: &mut Window, cx: &mut Context<Self>) {
188        let matcher = self.disabled_matcher.clone();
189        self.calendar.update(cx, |state, _| {
190            state.disabled_matcher = matcher;
191        });
192    }
193
194    fn on_escape(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
195        if !self.open {
196            cx.propagate();
197        }
198
199        self.focus_back_if_need(window, cx);
200        self.open = false;
201
202        cx.notify();
203    }
204
205    fn on_enter(&mut self, _: &Confirm, _: &mut Window, cx: &mut Context<Self>) {
206        if !self.open {
207            self.open = true;
208            cx.notify();
209        }
210    }
211
212    fn on_delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
213        self.clean(&ClickEvent::default(), window, cx);
214    }
215
216    // To focus the Picker Input, if current focus in is on the container.
217    //
218    // This is because mouse down out the Calendar, GPUI will move focus to the container.
219    // So we need to move focus back to the Picker Input.
220    //
221    // But if mouse down target is some other focusable element (e.g.: [`crate::Input`]), we should not move focus.
222    fn focus_back_if_need(&mut self, window: &mut Window, cx: &mut Context<Self>) {
223        if !self.open {
224            return;
225        }
226
227        if let Some(focused) = window.focused(cx) {
228            if focused.contains(&self.focus_handle, window) {
229                self.focus_handle.focus(window, cx);
230            }
231        }
232    }
233
234    fn clean(&mut self, _: &rgpui::ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
235        cx.stop_propagation();
236        match self.date {
237            Date::Single(_) => {
238                self.update_date(Date::Single(None), true, window, cx);
239            }
240            Date::Range(_, _) => {
241                self.update_date(Date::Range(None, None), true, window, cx);
242            }
243        }
244    }
245
246    fn toggle_calendar(&mut self, _: &rgpui::ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
247        self.open = !self.open;
248        cx.notify();
249    }
250
251    fn select_preset(
252        &mut self,
253        preset: &DateRangePreset,
254        window: &mut Window,
255        cx: &mut Context<Self>,
256    ) {
257        match preset.value {
258            DateRangePresetValue::Single(single) => {
259                self.update_date(Date::Single(Some(single)), true, window, cx)
260            }
261            DateRangePresetValue::Range(start, end) => {
262                self.update_date(Date::Range(Some(start), Some(end)), true, window, cx)
263            }
264        }
265    }
266}
267
268/// A DatePicker element.
269#[derive(IntoElement)]
270pub struct DatePicker {
271    id: ElementId,
272    style: StyleRefinement,
273    state: Entity<DatePickerState>,
274    cleanable: bool,
275    placeholder: Option<SharedString>,
276    size: Size,
277    number_of_months: usize,
278    presets: Option<Vec<DateRangePreset>>,
279    appearance: bool,
280    disabled: bool,
281}
282
283impl Sizable for DatePicker {
284    fn with_size(mut self, size: impl Into<Size>) -> Self {
285        self.size = size.into();
286        self
287    }
288}
289impl Focusable for DatePicker {
290    fn focus_handle(&self, cx: &App) -> FocusHandle {
291        self.state.focus_handle(cx)
292    }
293}
294
295impl Styled for DatePicker {
296    fn style(&mut self) -> &mut StyleRefinement {
297        &mut self.style
298    }
299}
300
301impl Disableable for DatePicker {
302    fn disabled(mut self, disabled: bool) -> Self {
303        self.disabled = disabled;
304        self
305    }
306}
307
308impl Render for DatePickerState {
309    fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl rgpui::IntoElement {
310        Empty
311    }
312}
313
314impl DatePicker {
315    /// Create a new DatePicker with the given [`DatePickerState`].
316    pub fn new(state: &Entity<DatePickerState>) -> Self {
317        Self {
318            id: ("date-picker", state.entity_id()).into(),
319            state: state.clone(),
320            cleanable: false,
321            placeholder: None,
322            size: Size::default(),
323            style: StyleRefinement::default(),
324            number_of_months: 2,
325            presets: None,
326            appearance: true,
327            disabled: false,
328        }
329    }
330
331    /// Set the placeholder of the date picker, default: "".
332    pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
333        self.placeholder = Some(placeholder.into());
334        self
335    }
336
337    /// Set whether to show the clear button when the input field is not empty, default is false.
338    pub fn cleanable(mut self, cleanable: bool) -> Self {
339        self.cleanable = cleanable;
340        self
341    }
342
343    /// Set preset ranges for the date picker.
344    pub fn presets(mut self, presets: Vec<DateRangePreset>) -> Self {
345        self.presets = Some(presets);
346        self
347    }
348
349    /// Set number of months to display in the calendar, default is 2.
350    pub fn number_of_months(mut self, number_of_months: usize) -> Self {
351        self.number_of_months = number_of_months;
352        self
353    }
354
355    /// Set appearance of the date picker, if false, the date picker will be in a minimal style.
356    pub fn appearance(mut self, appearance: bool) -> Self {
357        self.appearance = appearance;
358        self
359    }
360}
361
362impl RenderOnce for DatePicker {
363    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
364        self.state.update(cx, |state, cx| {
365            state.set_canlendar_disabled_matcher(window, cx);
366        });
367
368        // This for keep focus border style, when click on the popup.
369        let is_focused = self.focus_handle(cx).contains_focused(window, cx);
370        let state = self.state.read(cx);
371        let show_clean = self.cleanable && state.date.is_some();
372        let placeholder = self
373            .placeholder
374            .clone()
375            .unwrap_or_else(|| t!("DatePicker.placeholder").into());
376        let display_title = state
377            .date
378            .format(&state.date_format)
379            .unwrap_or(placeholder.clone());
380
381        let (bg, fg) = input_style(self.disabled, cx);
382
383        div()
384            .id(self.id.clone())
385            .key_context(CONTEXT)
386            .track_focus(&self.focus_handle(cx).tab_stop(true))
387            .on_action(window.listener_for(&self.state, DatePickerState::on_enter))
388            .on_action(window.listener_for(&self.state, DatePickerState::on_delete))
389            .when(state.open, |this| {
390                this.on_action(window.listener_for(&self.state, DatePickerState::on_escape))
391            })
392            .flex_none()
393            .w_full()
394            .relative()
395            .input_text_size(self.size)
396            .refine_style(&self.style)
397            .child(
398                div()
399                    .id("date-picker-input")
400                    .relative()
401                    .flex()
402                    .items_center()
403                    .justify_between()
404                    .when(self.appearance, |this| {
405                        this.bg(bg)
406                            .text_color(fg)
407                            .when(self.disabled, |this| this.opacity(0.5))
408                            .border_1()
409                            .border_color(cx.theme().input)
410                            .rounded(cx.theme().radius)
411                            .when(cx.theme().shadow, |this| this.shadow_xs())
412                            .when(is_focused, |this| this.focused_border(cx))
413                    })
414                    .overflow_hidden()
415                    .input_text_size(self.size)
416                    .input_size(self.size)
417                    .when(!state.open && !self.disabled, |this| {
418                        this.on_click(
419                            window.listener_for(&self.state, DatePickerState::toggle_calendar),
420                        )
421                    })
422                    .child(
423                        h_flex()
424                            .w_full()
425                            .items_center()
426                            .justify_between()
427                            .gap_1()
428                            .child(
429                                div()
430                                    .w_full()
431                                    .overflow_hidden()
432                                    .when(!state.date.is_some(), |this| {
433                                        this.text_color(cx.theme().muted_foreground)
434                                    })
435                                    .child(display_title),
436                            )
437                            .when(!self.disabled, |this| {
438                                this.when(show_clean, |this| {
439                                    this.child(clear_button(cx).on_click(
440                                        window.listener_for(&self.state, DatePickerState::clean),
441                                    ))
442                                })
443                                .when(!show_clean, |this| {
444                                    this.child(
445                                        Icon::new(IconName::Calendar)
446                                            .xsmall()
447                                            .text_color(cx.theme().muted_foreground),
448                                    )
449                                })
450                            }),
451                    ),
452            )
453            .when(state.open, |this| {
454                this.child(
455                    deferred(
456                        anchored().snap_to_window_with_margin(px(8.)).child(
457                            div()
458                                .occlude()
459                                .mt_1p5()
460                                .p_3()
461                                .border_1()
462                                .border_color(cx.theme().border)
463                                .shadow_lg()
464                                .rounded((cx.theme().radius * 2.).min(px(8.)))
465                                .bg(cx.theme().popover)
466                                .text_color(cx.theme().popover_foreground)
467                                .on_mouse_up_out(
468                                    MouseButton::Left,
469                                    window.listener_for(&self.state, |view, _, window, cx| {
470                                        view.on_escape(&Cancel, window, cx);
471                                    }),
472                                )
473                                .child(
474                                    h_flex()
475                                        .gap_3()
476                                        .h_full()
477                                        .items_start()
478                                        .when_some(self.presets.clone(), |this, presets| {
479                                            this.child(
480                                                v_flex().my_1().gap_2().justify_end().children(
481                                                    presets.into_iter().enumerate().map(
482                                                        |(i, preset)| {
483                                                            Button::new(("preset", i))
484                                                                .small()
485                                                                .ghost()
486                                                                .tab_stop(false)
487                                                                .label(preset.label.clone())
488                                                                .on_click(window.listener_for(
489                                                                    &self.state,
490                                                                    move |this, _, window, cx| {
491                                                                        this.select_preset(
492                                                                            &preset, window, cx,
493                                                                        );
494                                                                    },
495                                                                ))
496                                                        },
497                                                    ),
498                                                ),
499                                            )
500                                        })
501                                        .child(
502                                            Calendar::new(&state.calendar)
503                                                .number_of_months(self.number_of_months)
504                                                .border_0()
505                                                .rounded_none()
506                                                .p_0()
507                                                .with_size(self.size),
508                                        ),
509                                ),
510                        ),
511                    )
512                    .with_priority(2),
513                )
514            })
515    }
516}