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 pub fn single(label: impl Into<SharedString>, single: NaiveDate) -> Self {
53 DateRangePreset {
54 label: label.into(),
55 value: DateRangePresetValue::Single(single),
56 }
57 }
58 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
67pub 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 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
89 Self::new_with_range(false, window, cx)
90 }
91
92 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 pub fn date_format(mut self, format: impl Into<SharedString>) -> Self {
135 self.date_format = format.into();
136 self
137 }
138
139 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 pub fn date(&self) -> Date {
147 self.date
148 }
149
150 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 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 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 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 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
317 self.placeholder = Some(placeholder.into());
318 self
319 }
320
321 pub fn cleanable(mut self) -> Self {
323 self.cleanable = true;
324 self
325 }
326
327 pub fn presets(mut self, presets: Vec<DateRangePreset>) -> Self {
329 self.presets = Some(presets);
330 self
331 }
332
333 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 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 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}