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#[derive(Clone)]
37pub enum DatePickerEvent {
38 Change(Date),
39}
40
41#[derive(Clone)]
43pub enum DateRangePresetValue {
44 Single(NaiveDate),
45 Range(NaiveDate, NaiveDate),
46}
47
48#[derive(Clone)]
50pub struct DateRangePreset {
51 label: SharedString,
52 value: DateRangePresetValue,
53}
54
55impl DateRangePreset {
56 pub fn single(label: impl Into<SharedString>, date: NaiveDate) -> Self {
58 DateRangePreset {
59 label: label.into(),
60 value: DateRangePresetValue::Single(date),
61 }
62 }
63 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
72pub 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 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 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
97 Self::new_with_range(false, window, cx)
98 }
99
100 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 pub fn date_format(mut self, format: impl Into<SharedString>) -> Self {
145 self.date_format = format.into();
146 self
147 }
148
149 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 pub fn first_day_of_week(mut self, day: Weekday) -> Self {
157 self.first_day_of_week = day;
158 self
159 }
160
161 pub fn date(&self) -> Date {
163 self.date
164 }
165
166 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 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 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 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 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#[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 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 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
352 self.placeholder = Some(placeholder.into());
353 self
354 }
355
356 pub fn cleanable(mut self, cleanable: bool) -> Self {
358 self.cleanable = cleanable;
359 self
360 }
361
362 pub fn presets(mut self, presets: Vec<DateRangePreset>) -> Self {
364 self.presets = Some(presets);
365 self
366 }
367
368 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 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 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}