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#[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, 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 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 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 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 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#[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 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 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
333 self.placeholder = Some(placeholder.into());
334 self
335 }
336
337 pub fn cleanable(mut self, cleanable: bool) -> Self {
339 self.cleanable = cleanable;
340 self
341 }
342
343 pub fn presets(mut self, presets: Vec<DateRangePreset>) -> Self {
345 self.presets = Some(presets);
346 self
347 }
348
349 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 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 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}