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#[derive(Clone)]
35pub enum DatePickerEvent {
36 Change(Date),
37}
38
39#[derive(Clone)]
41pub enum DateRangePresetValue {
42 Single(NaiveDate),
43 Range(NaiveDate, NaiveDate),
44}
45
46#[derive(Clone)]
48pub struct DateRangePreset {
49 label: SharedString,
50 value: DateRangePresetValue,
51}
52
53impl DateRangePreset {
54 pub fn single(label: impl Into<SharedString>, date: NaiveDate) -> Self {
56 DateRangePreset {
57 label: label.into(),
58 value: DateRangePresetValue::Single(date),
59 }
60 }
61 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
70pub 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 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
92 Self::new_with_range(false, window, cx)
93 }
94
95 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 pub fn date_format(mut self, format: impl Into<SharedString>) -> Self {
138 self.date_format = format.into();
139 self
140 }
141
142 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 pub fn date(&self) -> Date {
150 self.date
151 }
152
153 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 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 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 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#[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 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 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
322 self.placeholder = Some(placeholder.into());
323 self
324 }
325
326 pub fn cleanable(mut self, cleanable: bool) -> Self {
328 self.cleanable = cleanable;
329 self
330 }
331
332 pub fn presets(mut self, presets: Vec<DateRangePreset>) -> Self {
334 self.presets = Some(presets);
335 self
336 }
337
338 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 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 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}