1use super::date_parts::{
34 Activate, CalendarDay, DAY_CELL, DayRange, GRID_WEEKS, LIST_ITEM_HEIGHT, PickerListItem,
35 PickerMenuButton, YearChip, add_months, first_of_month, month_weeks, week_days,
36};
37use super::text_field::type_style;
38use super::{
39 ButtonVariant, IconButton, IconButtonWidth, InteractionPreview, TextField, material_button,
40};
41use crate::{
42 color::ColorRole,
43 icons::{Icon, Symbol},
44 motion::{self, MotionToken},
45 theme::color,
46 tokens::{
47 elevation,
48 typography::{TypeRole, rem},
49 },
50};
51use chrono::{Datelike, Days, NaiveDate, Weekday};
52use gpui::{prelude::*, *};
53use gpui_base::input::{InputEvent, InputState};
54use std::{ops::RangeInclusive, rc::Rc, time::Duration};
55
56pub use gpui_base::{Date as DateSelection, Matcher as DateMatcher};
57
58actions!(
59 material_date_picker,
60 [
61 CursorLeft,
62 CursorRight,
63 CursorUp,
64 CursorDown,
65 CursorPreviousMonth,
66 CursorNextMonth,
67 CursorPreviousYear,
68 CursorNextYear,
69 CursorWeekStart,
70 CursorWeekEnd,
71 SelectCursor
72 ]
73);
74
75const CONTEXT: &str = "MaterialDatePicker";
76
77pub(crate) fn init(cx: &mut App) {
78 cx.bind_keys([
79 KeyBinding::new("left", CursorLeft, Some(CONTEXT)),
80 KeyBinding::new("right", CursorRight, Some(CONTEXT)),
81 KeyBinding::new("up", CursorUp, Some(CONTEXT)),
82 KeyBinding::new("down", CursorDown, Some(CONTEXT)),
83 KeyBinding::new("pageup", CursorPreviousMonth, Some(CONTEXT)),
84 KeyBinding::new("pagedown", CursorNextMonth, Some(CONTEXT)),
85 KeyBinding::new("shift-pageup", CursorPreviousYear, Some(CONTEXT)),
86 KeyBinding::new("shift-pagedown", CursorNextYear, Some(CONTEXT)),
87 KeyBinding::new("home", CursorWeekStart, Some(CONTEXT)),
88 KeyBinding::new("end", CursorWeekEnd, Some(CONTEXT)),
89 KeyBinding::new("enter", SelectCursor, Some(CONTEXT)),
90 KeyBinding::new("space", SelectCursor, Some(CONTEXT)),
91 ]);
92}
93
94type Format<T> = Rc<dyn Fn(T) -> SharedString>;
95type Parse = Rc<dyn Fn(&str) -> Option<NaiveDate>>;
96
97#[derive(Clone)]
101pub struct DatePickerLabels {
102 pub title: SharedString,
104 pub no_selection: SharedString,
106 pub input_headline: SharedString,
108 pub confirm: SharedString,
109 pub cancel: SharedString,
110 pub clear: SharedString,
111 pub save: SharedString,
113 pub close: SharedString,
115 pub previous_month: SharedString,
116 pub next_month: SharedString,
117 pub previous_year: SharedString,
118 pub next_year: SharedString,
119 pub switch_to_input: SharedString,
120 pub switch_to_calendar: SharedString,
121 pub choose_year: SharedString,
123 pub choose_month: SharedString,
124 pub date_field: SharedString,
126 pub start_field: SharedString,
127 pub end_field: SharedString,
128 pub input_placeholder: SharedString,
129 pub invalid_input: SharedString,
131 pub weekday: Format<Weekday>,
133 pub month: Format<u32>,
135 pub month_short: Format<u32>,
137 pub month_year: Rc<dyn Fn(i32, u32) -> SharedString>,
139 pub headline: Format<NaiveDate>,
141 pub range_headline: Rc<dyn Fn(Option<NaiveDate>, Option<NaiveDate>) -> SharedString>,
143 pub accessible_date: Format<NaiveDate>,
145 pub number: Format<i32>,
147 pub format_input: Format<NaiveDate>,
149 pub parse_input: Parse,
150}
151
152#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
154pub enum DateInputMode {
155 #[default]
156 Calendar,
157 Input,
158}
159
160#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
162pub enum DatePickerView {
163 #[default]
165 Day,
166 Year,
168 Month,
170}
171
172#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
174pub enum DatePickerVariant {
175 #[default]
177 Modal,
178 Docked,
180 FullScreen,
182}
183
184#[derive(Clone, Copy, Debug, PartialEq)]
186struct Slide {
187 from: (i32, u32),
188 direction: f32,
189 generation: u64,
190}
191
192pub struct DatePickerState {
196 focus: FocusHandle,
197 labels: Rc<DatePickerLabels>,
198 selection: DateSelection,
199 today: NaiveDate,
200 year: i32,
201 month: u32,
202 cursor: NaiveDate,
203 view: DatePickerView,
204 mode: DateInputMode,
205 disabled: Option<Rc<DateMatcher>>,
206 years: RangeInclusive<i32>,
207 first_day: Weekday,
208 slide: Option<Slide>,
209 generation: u64,
210 year_scroll: UniformListScrollHandle,
211 list_scroll: UniformListScrollHandle,
212 month_scroll: UniformListScrollHandle,
213 inputs: Option<(Entity<InputState>, Option<Entity<InputState>>)>,
214 invalid: (bool, bool),
215 _subscriptions: Vec<Subscription>,
216}
217
218#[derive(Clone, Copy, Debug, PartialEq, Eq)]
220pub enum DatePickerEvent {
221 Change(DateSelection),
223}
224
225impl EventEmitter<DatePickerEvent> for DatePickerState {}
226
227impl DatePickerState {
228 pub fn new(labels: DatePickerLabels, today: NaiveDate, cx: &mut Context<Self>) -> Self {
230 let state = Self {
231 focus: cx.focus_handle().tab_stop(true),
232 labels: Rc::new(labels),
233 selection: DateSelection::Single(None),
234 today,
235 year: today.year(),
236 month: today.month(),
237 cursor: today,
238 view: DatePickerView::Day,
239 mode: DateInputMode::Calendar,
240 disabled: None,
241 years: today.year() - 100..=today.year() + 100,
242 first_day: Weekday::Sun,
243 slide: None,
244 generation: 0,
245 year_scroll: UniformListScrollHandle::new(),
246 list_scroll: UniformListScrollHandle::new(),
247 month_scroll: UniformListScrollHandle::new(),
248 inputs: None,
249 invalid: (false, false),
250 _subscriptions: Vec::new(),
251 };
252 state.reveal_cursor();
253 state
254 }
255 pub fn range(mut self) -> Self {
257 self.selection = DateSelection::Range(None, None);
258 self
259 }
260 pub fn selection(mut self, selection: impl Into<DateSelection>) -> Self {
262 self.apply(selection.into());
263 self.reveal_cursor();
264 self
265 }
266 pub fn disabled_dates(mut self, matcher: impl Into<DateMatcher>) -> Self {
268 self.disabled = Some(Rc::new(matcher.into()));
269 self
270 }
271 pub fn years(mut self, years: RangeInclusive<i32>) -> Self {
273 self.years = years;
274 let shown = clamp_month(&self.years, self.year, self.month);
275 if shown != (self.year, self.month) {
276 (self.year, self.month) = shown;
277 self.cursor = first_of_month(shown.0, shown.1);
278 }
279 self.reveal_cursor();
280 self
281 }
282 pub fn first_day_of_week(mut self, day: Weekday) -> Self {
284 self.first_day = day;
285 self
286 }
287 pub fn view(mut self, view: DatePickerView) -> Self {
289 self.view = view;
290 self.reveal_cursor();
291 self
292 }
293 pub fn input_mode(mut self) -> Self {
295 self.mode = DateInputMode::Input;
296 self
297 }
298 pub fn with_inputs(
301 mut self,
302 start: Entity<InputState>,
303 end: Option<Entity<InputState>>,
304 cx: &mut Context<Self>,
305 ) -> Self {
306 self._subscriptions
307 .push(cx.subscribe(&start, |this, _, event: &InputEvent, cx| {
308 if matches!(event, InputEvent::Change) {
309 this.parse_inputs(cx);
310 }
311 }));
312 if let Some(end) = &end {
313 self._subscriptions
314 .push(cx.subscribe(end, |this, _, event: &InputEvent, cx| {
315 if matches!(event, InputEvent::Change) {
316 this.parse_inputs(cx);
317 }
318 }));
319 }
320 self.inputs = Some((start, end));
321 self
322 }
323
324 pub fn labels(&self) -> &DatePickerLabels {
325 &self.labels
326 }
327 pub fn focus_handle(&self) -> &FocusHandle {
328 &self.focus
329 }
330 pub fn value(&self) -> DateSelection {
331 self.selection
332 }
333 pub fn today(&self) -> NaiveDate {
334 self.today
335 }
336 pub fn displayed_month(&self) -> (i32, u32) {
338 (self.year, self.month)
339 }
340 pub fn current_view(&self) -> DatePickerView {
341 self.view
342 }
343 pub fn mode(&self) -> DateInputMode {
344 self.mode
345 }
346 pub fn cursor(&self) -> NaiveDate {
348 self.cursor
349 }
350 pub fn is_range(&self) -> bool {
351 !self.selection.is_single()
352 }
353 pub fn is_disabled(&self, date: NaiveDate) -> bool {
354 self.disabled.as_ref().is_some_and(|m| m.matched(&date))
355 }
356
357 fn apply(&mut self, selection: DateSelection) {
358 self.selection = selection;
359 if let Some(start) = selection.start() {
360 (self.year, self.month) = clamp_month(&self.years, start.year(), start.month());
363 self.cursor = if (start.year(), start.month()) == (self.year, self.month) {
364 start
365 } else {
366 first_of_month(self.year, self.month)
367 };
368 }
369 }
370
371 pub fn set_value(&mut self, selection: DateSelection, cx: &mut Context<Self>) {
373 self.apply(selection);
374 cx.notify();
375 }
376
377 pub fn select(&mut self, date: NaiveDate, cx: &mut Context<Self>) -> bool {
382 if self.is_disabled(date) {
383 return false;
384 }
385 self.selection = next_selection(self.selection, date);
386 self.cursor = date;
387 if (date.year(), date.month()) != (self.year, self.month)
388 && self.view == DatePickerView::Day
389 {
390 self.show_month(date.year(), date.month(), cx);
391 }
392 cx.emit(DatePickerEvent::Change(self.selection));
393 cx.notify();
394 true
395 }
396
397 pub fn clear(&mut self, cx: &mut Context<Self>) {
399 self.selection = if self.is_range() {
400 DateSelection::Range(None, None)
401 } else {
402 DateSelection::Single(None)
403 };
404 self.invalid = (false, false);
405 cx.emit(DatePickerEvent::Change(self.selection));
406 cx.notify();
407 }
408
409 pub fn show_month(&mut self, year: i32, month: u32, cx: &mut Context<Self>) {
412 let (year, month) = clamp_month(&self.years, year, month);
413 let target = first_of_month(year, month);
414 let current = (self.year, self.month);
415 let to = (target.year(), target.month());
416 if to == current {
417 return;
418 }
419 self.generation += 1;
420 let direction = if to > current { 1. } else { -1. };
421 self.slide = Some(Slide {
422 from: current,
423 direction,
424 generation: self.generation,
425 });
426 (self.year, self.month) = to;
427 if (self.cursor.year(), self.cursor.month()) != to {
428 let day = self.cursor.day().min(days_in_month(to.0, to.1));
429 self.cursor = NaiveDate::from_ymd_opt(to.0, to.1, day).unwrap_or(target);
430 }
431 let generation = self.generation;
433 let settle = motion::curve(MotionToken::DefaultSpatial).duration();
434 cx.spawn(async move |this, cx| {
435 cx.background_executor()
436 .timer(settle + Duration::from_millis(16))
437 .await;
438 this.update(cx, |this, cx| {
439 if this.slide.is_some_and(|s| s.generation == generation) {
440 this.slide = None;
441 cx.notify();
442 }
443 })
444 .ok();
445 })
446 .detach();
447 cx.notify();
448 }
449
450 pub fn next_month(&mut self, cx: &mut Context<Self>) {
451 self.show_month(self.year, self.month + 1, cx);
452 }
453 pub fn previous_month(&mut self, cx: &mut Context<Self>) {
454 let (year, month) = previous(self.year, self.month);
455 self.show_month(year, month, cx);
456 }
457 pub fn next_year(&mut self, cx: &mut Context<Self>) {
458 self.show_month(self.year + 1, self.month, cx);
459 }
460 pub fn previous_year(&mut self, cx: &mut Context<Self>) {
461 self.show_month(self.year - 1, self.month, cx);
462 }
463
464 pub fn set_view(&mut self, view: DatePickerView, cx: &mut Context<Self>) {
467 if self.view != view {
468 self.view = view;
469 self.cursor = NaiveDate::from_ymd_opt(
470 self.year,
471 self.month,
472 self.cursor.day().min(days_in_month(self.year, self.month)),
473 )
474 .unwrap_or(self.cursor);
475 self.reveal_cursor();
476 cx.notify();
477 }
478 }
479 pub fn toggle_view(&mut self, view: DatePickerView, cx: &mut Context<Self>) {
481 let next = if self.view == view {
482 DatePickerView::Day
483 } else {
484 view
485 };
486 self.set_view(next, cx);
487 }
488
489 pub fn choose_year(&mut self, year: i32, cx: &mut Context<Self>) {
491 let month = self.month;
492 self.view = DatePickerView::Day;
493 self.show_month(year, month, cx);
494 cx.notify();
495 }
496 pub fn choose_month(&mut self, month: u32, cx: &mut Context<Self>) {
498 let year = self.year;
499 self.view = DatePickerView::Day;
500 self.show_month(year, month, cx);
501 cx.notify();
502 }
503
504 pub fn toggle_mode(&mut self, window: &mut Window, cx: &mut Context<Self>) {
507 match self.mode {
508 DateInputMode::Calendar => {
509 self.mode = DateInputMode::Input;
510 self.write_inputs(window, cx);
511 }
512 DateInputMode::Input => {
513 self.parse_inputs(cx);
514 self.mode = DateInputMode::Calendar;
515 }
516 }
517 cx.notify();
518 }
519
520 fn write_inputs(&mut self, window: &mut Window, cx: &mut Context<Self>) {
521 let Some((start, end)) = self.inputs.clone() else {
522 return;
523 };
524 let format = self.labels.format_input.clone();
525 let text = |date: Option<NaiveDate>| -> SharedString {
526 date.map(|d| format(d)).unwrap_or_default()
527 };
528 let (first, last) = (self.selection.start(), self.selection.end());
529 start.update(cx, |input, cx| input.set_value(text(first), window, cx));
530 if let Some(end) = end {
531 end.update(cx, |input, cx| input.set_value(text(last), window, cx));
532 }
533 self.invalid = (false, false);
534 }
535
536 pub fn parse_inputs(&mut self, cx: &mut Context<Self>) {
539 let Some((start, end)) = self.inputs.clone() else {
540 return;
541 };
542 let parse = self.labels.parse_input.clone();
543 let read = |input: &Entity<InputState>, cx: &App| -> Result<Option<NaiveDate>, ()> {
544 let text = input.read(cx).value();
545 let text = text.trim();
546 if text.is_empty() {
547 return Ok(None);
548 }
549 parse(text).map(Some).ok_or(())
550 };
551 let first = read(&start, cx);
552 let last = end.as_ref().map(|end| read(end, cx));
553 let disabled = |d: &Result<Option<NaiveDate>, ()>| matches!(d, Ok(Some(date)) if self.is_disabled(*date));
554 self.invalid = (
555 first.is_err() || disabled(&first),
556 last.as_ref().is_some_and(|l| l.is_err() || disabled(l)),
557 );
558 if self.invalid != (false, false) {
559 cx.notify();
560 return;
561 }
562 let next = match (self.selection, first, last) {
563 (DateSelection::Single(_), Ok(date), _) => DateSelection::Single(date),
564 (DateSelection::Range(..), Ok(a), Some(Ok(b))) => match (a, b) {
565 (Some(a), Some(b)) if b < a => {
566 self.invalid.1 = true;
567 cx.notify();
568 return;
569 }
570 _ => DateSelection::Range(a, b),
571 },
572 _ => return,
573 };
574 if next != self.selection {
575 self.apply(next);
576 cx.emit(DatePickerEvent::Change(self.selection));
577 }
578 cx.notify();
579 }
580
581 fn move_cursor(&mut self, step: CursorStep, cx: &mut Context<Self>) {
583 let next = cursor_target(self.view, self.cursor, step, self.first_day);
584 let years = self.years.clone();
585 let next = if years.contains(&next.year()) {
586 next
587 } else {
588 self.cursor
589 };
590 self.cursor = next;
591 if self.view == DatePickerView::Day
592 && (next.year(), next.month()) != (self.year, self.month)
593 {
594 self.show_month(next.year(), next.month(), cx);
595 }
596 self.reveal_cursor();
597 cx.notify();
598 }
599
600 fn reveal_cursor(&self) {
601 let years_from = *self.years.start();
602 let years = (self.years.end() - years_from + 1).max(1) as usize;
605 match self.view {
606 DatePickerView::Year => {
607 let index = ((self.cursor.year() - years_from).max(0) as usize).min(years - 1);
608 self.year_scroll
609 .scroll_to_item(index / YEAR_COLUMNS, ScrollStrategy::Center);
610 self.list_scroll
611 .scroll_to_item(index, ScrollStrategy::Center);
612 }
613 DatePickerView::Month => {
614 self.list_scroll
615 .scroll_to_item(self.cursor.month0() as usize, ScrollStrategy::Center);
616 }
617 DatePickerView::Day => {
618 let index = month_list_index(&self.years, self.year, self.month0());
619 self.month_scroll.scroll_to_item(index, ScrollStrategy::Top);
620 }
621 }
622 }
623
624 fn month0(&self) -> usize {
625 self.month.saturating_sub(1) as usize
626 }
627
628 fn activate_cursor(&mut self, cx: &mut Context<Self>) {
629 match self.view {
630 DatePickerView::Day => {
631 self.select(self.cursor, cx);
632 }
633 DatePickerView::Year => self.choose_year(self.cursor.year(), cx),
634 DatePickerView::Month => self.choose_month(self.cursor.month(), cx),
635 }
636 }
637}
638
639impl Render for DatePickerState {
640 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
641 Empty
642 }
643}
644
645fn clamp_month(years: &RangeInclusive<i32>, year: i32, month: u32) -> (i32, u32) {
648 let date = first_of_month(year, month);
649 let to = (date.year(), date.month());
650 if years.start() > years.end() {
651 return to;
652 }
653 to.clamp((*years.start(), 1), (*years.end(), 12))
654}
655
656fn month_list_index(years: &RangeInclusive<i32>, year: i32, month0: usize) -> usize {
659 let count = (years.end() - years.start() + 1).max(1) as usize * 12;
660 ((year - years.start()).max(0) as usize * 12 + month0).min(count - 1)
661}
662
663fn month_in_range(years: &RangeInclusive<i32>, year: i32, month: u32) -> bool {
665 let date = first_of_month(year, month);
666 clamp_month(years, year, month) == (date.year(), date.month())
667}
668
669fn previous(year: i32, month: u32) -> (i32, u32) {
670 if month <= 1 {
671 (year - 1, 12)
672 } else {
673 (year, month - 1)
674 }
675}
676
677pub(crate) fn days_in_month(year: i32, month: u32) -> u32 {
678 let first = first_of_month(year, month);
679 (first_of_month(year, month + 1) - first).num_days() as u32
680}
681
682pub(crate) fn next_selection(current: DateSelection, value: NaiveDate) -> DateSelection {
684 match current {
685 DateSelection::Single(_) => DateSelection::Single(Some(value)),
686 DateSelection::Range(None, _) => DateSelection::Range(Some(value), None),
687 DateSelection::Range(Some(start), None) if value >= start => {
688 DateSelection::Range(Some(start), Some(value))
689 }
690 DateSelection::Range(Some(_), _) => DateSelection::Range(Some(value), None),
691 }
692}
693
694#[derive(Clone, Copy, Debug, PartialEq, Eq)]
696pub(crate) enum CursorStep {
697 Left,
698 Right,
699 Up,
700 Down,
701 PreviousMonth,
702 NextMonth,
703 PreviousYear,
704 NextYear,
705 WeekStart,
706 WeekEnd,
707}
708
709const YEAR_COLUMNS: usize = 3;
710
711pub(crate) fn cursor_target(
714 view: DatePickerView,
715 cursor: NaiveDate,
716 step: CursorStep,
717 first_day: Weekday,
718) -> NaiveDate {
719 use CursorStep::*;
720 let days = |n: i64| {
721 if n >= 0 {
722 cursor + Days::new(n as u64)
723 } else {
724 cursor - Days::new(n.unsigned_abs())
725 }
726 };
727 match (view, step) {
728 (_, PreviousMonth) => add_months(cursor, -1),
729 (_, NextMonth) => add_months(cursor, 1),
730 (_, PreviousYear) => add_months(cursor, -12),
731 (_, NextYear) => add_months(cursor, 12),
732 (DatePickerView::Day, Left) => days(-1),
733 (DatePickerView::Day, Right) => days(1),
734 (DatePickerView::Day, Up) => days(-7),
735 (DatePickerView::Day, Down) => days(7),
736 (DatePickerView::Day, WeekStart | WeekEnd) => {
737 let offset = (cursor.weekday().num_days_from_sunday() + 7
738 - first_day.num_days_from_sunday())
739 % 7;
740 if step == WeekStart {
741 days(-(offset as i64))
742 } else {
743 days(6 - offset as i64)
744 }
745 }
746 (DatePickerView::Year, Left) => add_months(cursor, -12),
747 (DatePickerView::Year, Right) => add_months(cursor, 12),
748 (DatePickerView::Year, Up) => add_months(cursor, -12 * YEAR_COLUMNS as i32),
749 (DatePickerView::Year, Down) => add_months(cursor, 12 * YEAR_COLUMNS as i32),
750 (DatePickerView::Month, Up | Left) if cursor.month() > 1 => add_months(cursor, -1),
751 (DatePickerView::Month, Down | Right) if cursor.month() < 12 => add_months(cursor, 1),
752 (DatePickerView::Month, WeekStart) => add_months(cursor, 1 - cursor.month() as i32),
753 (DatePickerView::Month, WeekEnd) => add_months(cursor, 12 - cursor.month() as i32),
754 (DatePickerView::Year, WeekStart | WeekEnd) | (DatePickerView::Month, _) => cursor,
755 }
756}
757
758type Confirm = Rc<dyn Fn(DateSelection, &mut Window, &mut App)>;
759
760#[derive(gpui::IntoElement)]
762pub struct DatePicker {
763 id: ElementId,
764 state: Entity<DatePickerState>,
765 variant: DatePickerVariant,
766 specimen: bool,
767 show_clear: bool,
768 show_actions: bool,
769 height: Option<f32>,
770 on_confirm: Option<Confirm>,
771 on_cancel: Option<Activate>,
772}
773
774pub const DATE_PICKER_WIDTH: f32 = 360.;
776pub const DATE_INPUT_PICKER_WIDTH: f32 = 328.;
777const GRID_PADDING: f32 = 12.;
778const SLIDE_DISTANCE: f32 = DAY_CELL * 7.;
779
780impl DatePicker {
781 pub fn new(id: impl Into<ElementId>, state: &Entity<DatePickerState>) -> Self {
782 Self {
783 id: id.into(),
784 state: state.clone(),
785 variant: DatePickerVariant::Modal,
786 specimen: false,
787 show_clear: true,
788 show_actions: true,
789 height: None,
790 on_confirm: None,
791 on_cancel: None,
792 }
793 }
794 pub fn variant(mut self, variant: DatePickerVariant) -> Self {
795 self.variant = variant;
796 self
797 }
798 pub fn docked(self) -> Self {
799 self.variant(DatePickerVariant::Docked)
800 }
801 pub fn full_screen(self) -> Self {
802 self.variant(DatePickerVariant::FullScreen)
803 }
804 pub fn specimen(mut self) -> Self {
806 self.specimen = true;
807 self
808 }
809 pub fn show_clear(mut self, show: bool) -> Self {
811 self.show_clear = show;
812 self
813 }
814 pub fn show_actions(mut self, show: bool) -> Self {
817 self.show_actions = show;
818 self
819 }
820 pub fn height(mut self, height: f32) -> Self {
822 self.height = Some(height);
823 self
824 }
825 pub fn on_confirm(
827 mut self,
828 f: impl Fn(DateSelection, &mut Window, &mut App) + 'static,
829 ) -> Self {
830 self.on_confirm = Some(Rc::new(f));
831 self
832 }
833 pub fn on_cancel(mut self, f: impl Fn(&mut Window, &mut App) + 'static) -> Self {
835 self.on_cancel = Some(Rc::new(f));
836 self
837 }
838}
839
840struct Snapshot {
842 labels: Rc<DatePickerLabels>,
843 selection: DateSelection,
844 today: NaiveDate,
845 year: i32,
846 month: u32,
847 cursor: NaiveDate,
848 view: DatePickerView,
849 mode: DateInputMode,
850 disabled: Option<Rc<DateMatcher>>,
851 years: RangeInclusive<i32>,
852 first_day: Weekday,
853 slide: Option<Slide>,
854 focused: bool,
855 focus: FocusHandle,
856 year_scroll: UniformListScrollHandle,
857 list_scroll: UniformListScrollHandle,
858 month_scroll: UniformListScrollHandle,
859 inputs: Option<(Entity<InputState>, Option<Entity<InputState>>)>,
860 invalid: (bool, bool),
861}
862
863impl Snapshot {
864 fn can_step_months(&self, delta: i32) -> bool {
866 let total = self.year as i64 * 12 + self.month as i64 - 1 + delta as i64;
867 month_in_range(
868 &self.years,
869 total.div_euclid(12) as i32,
870 total.rem_euclid(12) as u32 + 1,
871 )
872 }
873 fn is_disabled(&self, date: NaiveDate) -> bool {
874 self.disabled.as_ref().is_some_and(|m| m.matched(&date))
875 }
876 fn headline(&self) -> SharedString {
877 match self.selection {
878 DateSelection::Single(Some(date)) => (self.labels.headline)(date),
879 DateSelection::Single(None) | DateSelection::Range(None, None) => {
880 self.labels.no_selection.clone()
881 }
882 DateSelection::Range(start, end) => (self.labels.range_headline)(start, end),
883 }
884 }
885 fn range_of(&self, date: NaiveDate) -> (bool, DayRange) {
886 match self.selection {
887 DateSelection::Single(selected) => (selected == Some(date), DayRange::None),
888 DateSelection::Range(Some(start), Some(end)) if start != end => {
889 if date == start {
890 (true, DayRange::Start)
891 } else if date == end {
892 (true, DayRange::End)
893 } else if date > start && date < end {
894 (false, DayRange::Middle)
895 } else {
896 (false, DayRange::None)
897 }
898 }
899 DateSelection::Range(start, end) => {
900 (start == Some(date) || end == Some(date), DayRange::None)
901 }
902 }
903 }
904}
905
906fn icon(name: &str) -> Icon {
907 Icon::from(Symbol::named(name).expect("a Material Symbol in the pinned catalog"))
908}
909
910impl RenderOnce for DatePicker {
911 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
912 let s = self.state.read(cx);
913 let snap = Rc::new(Snapshot {
914 labels: s.labels.clone(),
915 selection: s.selection,
916 today: s.today,
917 year: s.year,
918 month: s.month,
919 cursor: s.cursor,
920 view: s.view,
921 mode: s.mode,
922 disabled: s.disabled.clone(),
923 years: s.years.clone(),
924 first_day: s.first_day,
925 slide: s.slide,
926 focused: s.focus.is_focused(window),
927 focus: s.focus.clone(),
928 year_scroll: s.year_scroll.clone(),
929 list_scroll: s.list_scroll.clone(),
930 month_scroll: s.month_scroll.clone(),
931 inputs: s.inputs.clone(),
932 invalid: s.invalid,
933 });
934 let ctx = PickerCx {
935 id: self.id.clone(),
936 state: self.state.clone(),
937 snap,
938 specimen: self.specimen,
939 reduce_motion: cx.reduce_motion(),
940 };
941 let actions = ActionRow {
942 show_clear: self.show_clear,
943 on_confirm: self.on_confirm.clone(),
944 on_cancel: self.on_cancel.clone(),
945 };
946 match (self.variant, ctx.snap.mode) {
947 (DatePickerVariant::Docked, _) => ctx
948 .docked(self.show_actions.then_some(actions))
949 .into_any_element(),
950 (DatePickerVariant::FullScreen, _) => ctx
951 .full_screen(actions, self.height.unwrap_or(696.))
952 .into_any_element(),
953 (DatePickerVariant::Modal, DateInputMode::Input) => {
954 ctx.modal_input(actions).into_any_element()
955 }
956 (DatePickerVariant::Modal, DateInputMode::Calendar) => {
957 ctx.modal(actions).into_any_element()
958 }
959 }
960 }
961}
962
963struct ActionRow {
964 show_clear: bool,
965 on_confirm: Option<Confirm>,
966 on_cancel: Option<Activate>,
967}
968
969struct PickerCx {
970 id: ElementId,
971 state: Entity<DatePickerState>,
972 snap: Rc<Snapshot>,
973 specimen: bool,
974 reduce_motion: bool,
975}
976
977fn label_text(
978 id: impl Into<ElementId>,
979 text: SharedString,
980 role: TypeRole,
981 ink: Rgba,
982) -> Stateful<Div> {
983 div()
984 .id(id)
985 .text_color(ink)
986 .map(|d| type_style(d, role))
987 .whitespace_nowrap()
988 .overflow_hidden()
989 .text_ellipsis()
990 .child(text)
991}
992
993impl PickerCx {
994 fn child_id(&self, name: &'static str) -> ElementId {
995 ElementId::NamedChild(self.id.clone().into(), name.into())
996 }
997
998 fn update(
1000 &self,
1001 f: impl Fn(&mut DatePickerState, &mut Window, &mut Context<DatePickerState>) + 'static,
1002 ) -> impl Fn(&mut Window, &mut App) + 'static {
1003 let state = self.state.clone();
1004 move |window, cx| state.update(cx, |s, cx| f(s, window, cx))
1005 }
1006
1007 fn preview(&self) -> Option<InteractionPreview> {
1008 self.specimen.then_some(InteractionPreview::Enabled)
1009 }
1010
1011 fn icon_button(
1012 &self,
1013 name: &'static str,
1014 symbol: &str,
1015 label: SharedString,
1016 f: impl Fn(&mut DatePickerState, &mut Window, &mut Context<DatePickerState>) + 'static,
1017 ) -> IconButton {
1018 let click = self.update(f);
1019 let mut button = IconButton::new(self.child_id(name), icon(symbol), label)
1020 .on_click(move |_, window, cx| click(window, cx));
1021 if let Some(preview) = self.preview() {
1022 button = button.preview_state(preview);
1023 }
1024 button
1025 }
1026
1027 fn text_button(
1028 &self,
1029 name: &'static str,
1030 label: SharedString,
1031 f: impl Fn(&mut Window, &mut App) + 'static,
1032 ) -> AnyElement {
1033 let mut button = material_button(self.child_id(name), ButtonVariant::Text)
1034 .child(label)
1035 .on_click(move |_, window, cx| f(window, cx));
1036 if let Some(preview) = self.preview() {
1037 button = button.preview_state(preview);
1038 }
1039 button.into_any_element()
1040 }
1041
1042 fn action_row(&self, actions: ActionRow, clear: bool) -> Div {
1043 let l = &self.snap.labels;
1044 let cancel = actions.on_cancel.clone();
1045 let confirm = actions.on_confirm.clone();
1046 let state = self.state.clone();
1047 div()
1048 .w_full()
1049 .flex()
1050 .items_start()
1051 .justify_between()
1052 .pt(rem(4.))
1053 .pr(rem(12.))
1054 .pb(rem(8.))
1055 .pl(rem(12.))
1056 .gap(rem(8.))
1057 .child(if clear && actions.show_clear {
1058 self.text_button("clear", l.clear.clone(), {
1059 let state = self.state.clone();
1060 move |_, cx| state.update(cx, |s, cx| s.clear(cx))
1061 })
1062 } else {
1063 div().into_any_element()
1064 })
1065 .child(
1066 div()
1067 .flex()
1068 .gap(rem(8.))
1069 .child(
1070 self.text_button("cancel", l.cancel.clone(), move |window, cx| {
1071 if let Some(f) = &cancel {
1072 f(window, cx);
1073 }
1074 }),
1075 )
1076 .child(
1077 self.text_button("confirm", l.confirm.clone(), move |window, cx| {
1078 if state.read(cx).mode == DateInputMode::Input {
1079 state.update(cx, |s, cx| s.parse_inputs(cx));
1080 if state.read(cx).invalid != (false, false) {
1081 return;
1082 }
1083 }
1084 let value = state.read(cx).selection;
1085 if let Some(f) = &confirm {
1086 f(value, window, cx);
1087 }
1088 }),
1089 ),
1090 )
1091 }
1092
1093 fn surface(&self, width: f32, radius: f32) -> Stateful<Div> {
1094 div()
1095 .id(self.id.clone())
1096 .w(rem(width))
1097 .flex()
1098 .flex_col()
1099 .flex_shrink_0()
1100 .overflow_hidden()
1101 .rounded(rem(radius))
1102 .bg(color(ColorRole::SURFACE_CONTAINER_HIGH))
1103 .role(Role::Dialog)
1104 }
1105
1106 fn modal_header(&self) -> Div {
1107 let l = &self.snap.labels;
1108 div()
1109 .w_full()
1110 .h(rem(120.))
1111 .flex()
1112 .items_end()
1113 .gap(rem(8.))
1114 .pt(rem(16.))
1115 .pr(rem(12.))
1116 .pb(rem(12.))
1117 .pl(rem(24.))
1118 .border_b(rem(1.))
1119 .border_color(color(ColorRole::OUTLINE_VARIANT))
1120 .child(
1121 div()
1122 .flex_1()
1123 .min_w_0()
1124 .flex()
1125 .flex_col()
1126 .gap(rem(36.))
1127 .child(label_text(
1128 "title",
1129 l.title.clone(),
1130 TypeRole::LabelLarge,
1131 color(ColorRole::ON_SURFACE_VARIANT),
1132 ))
1133 .child(
1134 label_text(
1135 "headline",
1136 self.snap.headline(),
1137 TypeRole::HeadlineLarge,
1138 color(ColorRole::ON_SURFACE),
1139 )
1140 .role(Role::Heading),
1141 ),
1142 )
1143 .child(self.icon_button(
1144 "mode",
1145 "edit",
1146 l.switch_to_input.clone(),
1147 |s, window, cx| s.toggle_mode(window, cx),
1148 ))
1149 }
1150
1151 fn month_row(&self) -> Div {
1152 let snap = &self.snap;
1153 let l = &snap.labels;
1154 let year_view = snap.view == DatePickerView::Year;
1155 let mut menu = PickerMenuButton::new(
1156 self.child_id("month-menu"),
1157 (l.month_year)(snap.year, snap.month),
1158 )
1159 .accessible_label(l.choose_year.clone())
1160 .open(year_view)
1161 .on_click(self.update(|s, _, cx| s.toggle_view(DatePickerView::Year, cx)));
1162 if let Some(preview) = self.preview() {
1163 menu = menu.preview_state(preview);
1164 }
1165 div()
1166 .w_full()
1167 .h(rem(56.))
1168 .flex()
1169 .items_center()
1170 .justify_between()
1171 .pt(rem(4.))
1172 .pr(rem(12.))
1173 .pb(rem(4.))
1174 .pl(rem(16.))
1175 .child(menu)
1176 .when(!year_view, |row| {
1177 row.child(
1178 div()
1179 .flex()
1180 .child(
1181 self.icon_button(
1182 "previous",
1183 "chevron_backward",
1184 l.previous_month.clone(),
1185 |s, _, cx| s.previous_month(cx),
1186 )
1187 .disabled(!snap.can_step_months(-1)),
1188 )
1189 .child(
1190 self.icon_button(
1191 "next",
1192 "chevron_forward",
1193 l.next_month.clone(),
1194 |s, _, cx| s.next_month(cx),
1195 )
1196 .disabled(!snap.can_step_months(1)),
1197 ),
1198 )
1199 })
1200 }
1201
1202 fn weekday_row(&self) -> Div {
1203 let l = &self.snap.labels;
1204 div()
1205 .flex()
1206 .flex_shrink_0()
1207 .children(week_days(self.snap.first_day).into_iter().map(|day| {
1208 div()
1209 .size(rem(DAY_CELL))
1210 .flex()
1211 .items_center()
1212 .justify_center()
1213 .text_color(color(ColorRole::ON_SURFACE))
1214 .map(|d| type_style(d, TypeRole::BodyLarge))
1215 .child((l.weekday)(day))
1216 }))
1217 }
1218
1219 fn key_host(&self, element: Stateful<Div>) -> Stateful<Div> {
1222 if self.specimen {
1223 return element;
1224 }
1225 fn on_step<A: Action>(
1226 element: Stateful<Div>,
1227 state: &Entity<DatePickerState>,
1228 step: CursorStep,
1229 ) -> Stateful<Div> {
1230 let state = state.clone();
1231 element.on_action(move |_: &A, _, cx| state.update(cx, |s, cx| s.move_cursor(step, cx)))
1232 }
1233 let state = &self.state;
1234 let select = self.update(|s, _, cx| s.activate_cursor(cx));
1235 let element = element
1236 .track_focus(&self.snap.focus)
1237 .key_context(CONTEXT)
1238 .on_action(move |_: &SelectCursor, window, cx| select(window, cx));
1239 let element = on_step::<CursorLeft>(element, state, CursorStep::Left);
1240 let element = on_step::<CursorRight>(element, state, CursorStep::Right);
1241 let element = on_step::<CursorUp>(element, state, CursorStep::Up);
1242 let element = on_step::<CursorDown>(element, state, CursorStep::Down);
1243 let element = on_step::<CursorPreviousMonth>(element, state, CursorStep::PreviousMonth);
1244 let element = on_step::<CursorNextMonth>(element, state, CursorStep::NextMonth);
1245 let element = on_step::<CursorPreviousYear>(element, state, CursorStep::PreviousYear);
1246 let element = on_step::<CursorNextYear>(element, state, CursorStep::NextYear);
1247 let element = on_step::<CursorWeekStart>(element, state, CursorStep::WeekStart);
1248 on_step::<CursorWeekEnd>(element, state, CursorStep::WeekEnd)
1249 }
1250
1251 fn weeks(&self, year: i32, month: u32, adjacent: bool, live: bool, key: &str) -> Div {
1254 let snap = &self.snap;
1255 let weeks = month_weeks(year, month, snap.first_day);
1256 div()
1257 .flex()
1258 .flex_col()
1259 .children(weeks.into_iter().enumerate().map(|(row, week)| {
1260 div()
1261 .id(SharedString::from(format!("{key}-week-{row}")))
1262 .flex()
1263 .role(Role::Row)
1264 .children(week.into_iter().map(|date| {
1265 let outside = date.month() != month;
1266 if outside && !adjacent {
1267 return div().size(rem(DAY_CELL)).flex_shrink_0().into_any_element();
1268 }
1269 self.day(date, outside, live, key).into_any_element()
1270 }))
1271 }))
1272 }
1273
1274 fn day(&self, date: NaiveDate, outside: bool, live: bool, key: &str) -> CalendarDay {
1275 let snap = &self.snap;
1276 let (selected, range) = if outside {
1277 (false, DayRange::None)
1278 } else {
1279 snap.range_of(date)
1280 };
1281 let id = SharedString::from(format!("{key}-{}", date.num_days_from_ce()));
1282 let mut day = CalendarDay::new(id, (snap.labels.number)(date.day() as i32))
1283 .accessible_label((snap.labels.accessible_date)(date))
1284 .selected(selected)
1285 .today(date == snap.today && !outside)
1286 .adjacent(outside)
1287 .range(range)
1288 .disabled(snap.is_disabled(date));
1289 if self.specimen || !live {
1290 day = day.preview_state(InteractionPreview::Enabled);
1291 } else {
1292 let state = self.state.clone();
1293 let focus = snap.focus.clone();
1294 day = day
1295 .animate(!self.reduce_motion)
1296 .cursor(snap.focused && date == snap.cursor)
1297 .on_click(move |window, cx| {
1298 focus.focus(window, cx);
1299 state.update(cx, |s, cx| {
1300 s.select(date, cx);
1301 });
1302 });
1303 }
1304 day
1305 }
1306
1307 fn day_grid(&self, adjacent: bool) -> Stateful<Div> {
1309 let snap = &self.snap;
1310 let incoming = self
1311 .weeks(snap.year, snap.month, adjacent, true, "in")
1312 .w(rem(SLIDE_DISTANCE));
1313 let body = div().relative().overflow_hidden().w(rem(SLIDE_DISTANCE));
1314 let body = match snap.slide.filter(|_| !self.specimen && !self.reduce_motion) {
1315 Some(slide) => {
1316 let spatial = motion::curve(MotionToken::DefaultSpatial);
1317 let ease = spatial.ease();
1318 let animation = Animation::new(spatial.duration());
1319 let dir = slide.direction;
1320 let outgoing = self
1321 .weeks(slide.from.0, slide.from.1, adjacent, false, "out")
1322 .absolute()
1323 .top_0()
1324 .left_0()
1325 .w(rem(SLIDE_DISTANCE));
1326 body.child(incoming.with_animation(
1327 ("month-in", slide.generation),
1328 animation.clone(),
1329 move |el, t| {
1330 let p = ease(t);
1331 el.relative()
1332 .left(rem(SLIDE_DISTANCE * dir * (1. - p)))
1333 .opacity((t * 1.5).clamp(0., 1.))
1334 },
1335 ))
1336 .child(outgoing.with_animation(
1337 ("month-out", slide.generation),
1338 animation,
1339 move |el, t| {
1340 let p = ease(t);
1341 el.left(rem(-SLIDE_DISTANCE * dir * p))
1342 .opacity((1. - t * 2.).clamp(0., 1.))
1343 },
1344 ))
1345 }
1346 None => body.child(incoming),
1347 };
1348 let l = &snap.labels;
1349 self.key_host(
1350 div()
1351 .id(self.child_id("grid"))
1352 .flex()
1353 .flex_col()
1354 .px(rem(GRID_PADDING))
1355 .role(Role::Grid)
1356 .aria_label((l.month_year)(snap.year, snap.month))
1357 .aria_row_count(GRID_WEEKS)
1358 .aria_column_count(7)
1359 .child(self.weekday_row())
1360 .child(body),
1361 )
1362 }
1363
1364 fn year_grid(&self, height: f32) -> Stateful<Div> {
1366 let snap = self.snap.clone();
1367 let first = *snap.years.start();
1368 let count = (snap.years.end() - first + 1).max(0) as usize;
1369 let rows = count.div_ceil(YEAR_COLUMNS);
1370 let state = self.state.clone();
1371 let specimen = self.specimen;
1372 let list = uniform_list(self.child_id("years"), rows, move |range, _, _| {
1373 range
1374 .map(|row| {
1375 div()
1376 .id(row)
1377 .w_full()
1378 .flex()
1379 .gap(rem(8.))
1380 .px(rem(24.))
1381 .role(Role::Row)
1382 .children((0..YEAR_COLUMNS).filter_map(|col| {
1383 let year = first + (row * YEAR_COLUMNS + col) as i32;
1384 (year <= *snap.years.end()).then(|| {
1385 let mut chip =
1386 YearChip::new(year as usize, (snap.labels.number)(year))
1387 .selected(year == snap.year);
1388 if specimen {
1389 chip = chip.preview_state(InteractionPreview::Enabled);
1390 } else {
1391 let state = state.clone();
1392 chip = chip
1393 .cursor(snap.focused && year == snap.cursor.year())
1394 .on_click(move |_, cx| {
1395 state.update(cx, |s, cx| s.choose_year(year, cx))
1396 });
1397 }
1398 chip
1399 })
1400 }))
1401 })
1402 .collect()
1403 })
1404 .track_scroll(&self.snap.year_scroll)
1405 .w_full()
1406 .h(rem(height));
1407 self.key_host(
1408 div()
1409 .id(self.child_id("year-grid"))
1410 .w_full()
1411 .role(Role::Grid)
1412 .aria_label(self.snap.labels.choose_year.clone())
1413 .child(list),
1414 )
1415 }
1416
1417 fn modal(self, actions: ActionRow) -> Stateful<Div> {
1418 let body_height = DAY_CELL * (GRID_WEEKS as f32 + 1.);
1419 let body = match self.snap.view {
1420 DatePickerView::Day | DatePickerView::Month => self.day_grid(false).into_any_element(),
1421 DatePickerView::Year => self.year_grid(body_height).into_any_element(),
1422 };
1423 self.surface(DATE_PICKER_WIDTH, 28.)
1424 .child(self.modal_header())
1425 .child(self.month_row())
1426 .child(div().h(rem(body_height)).child(body))
1427 .child(self.action_row(actions, true))
1428 }
1429
1430 fn field(
1431 &self,
1432 name: &'static str,
1433 input: Option<&Entity<InputState>>,
1434 label: SharedString,
1435 invalid: bool,
1436 first: bool,
1437 ) -> AnyElement {
1438 let l = &self.snap.labels;
1439 let field = match input {
1440 Some(input) if !self.specimen => TextField::new(self.child_id(name), input),
1441 _ => TextField::specimen(self.child_id(name), ""),
1442 }
1443 .outlined()
1444 .label(label)
1445 .placeholder(l.input_placeholder.clone())
1446 .error(invalid)
1447 .w_full();
1448 let field = if invalid {
1449 field.supporting_text(l.invalid_input.clone())
1450 } else {
1451 field
1452 };
1453 let field = if self.specimen && first {
1454 field.preview_state(InteractionPreview::Focused)
1455 } else if self.specimen {
1456 field.preview_state(InteractionPreview::Enabled)
1457 } else {
1458 field
1459 };
1460 div().flex_1().min_w_0().child(field).into_any_element()
1461 }
1462
1463 fn modal_input(self, actions: ActionRow) -> Stateful<Div> {
1464 let l = self.snap.labels.clone();
1465 let range = !self.snap.selection.is_single();
1466 let inputs = self.snap.inputs.clone();
1467 let (start, end) = match &inputs {
1468 Some((start, end)) => (Some(start), end.as_ref()),
1469 None => (None, None),
1470 };
1471 let invalid = self.snap.invalid;
1472 let mut button = IconButton::new(
1473 self.child_id("mode"),
1474 icon("today"),
1475 l.switch_to_calendar.clone(),
1476 )
1477 .width(IconButtonWidth::Wide)
1478 .on_click({
1479 let f = self.update(|s, window, cx| s.toggle_mode(window, cx));
1480 move |_, window, cx| f(window, cx)
1481 });
1482 if let Some(preview) = self.preview() {
1483 button = button.preview_state(preview);
1484 }
1485 let header = div()
1486 .w_full()
1487 .h(rem(128.))
1488 .flex()
1489 .flex_col()
1490 .gap(rem(36.))
1491 .pt(rem(16.))
1492 .pr(rem(12.))
1493 .pb(rem(8.))
1494 .pl(rem(24.))
1495 .border_b(rem(1.))
1496 .border_color(color(ColorRole::OUTLINE_VARIANT))
1497 .child(label_text(
1498 "title",
1499 l.title.clone(),
1500 TypeRole::LabelLarge,
1501 color(ColorRole::ON_SURFACE_VARIANT),
1502 ))
1503 .child(
1504 div()
1505 .flex()
1506 .items_center()
1507 .gap(rem(8.))
1508 .child(
1509 label_text(
1510 "headline",
1511 l.input_headline.clone(),
1512 TypeRole::HeadlineLarge,
1513 color(ColorRole::ON_SURFACE),
1514 )
1515 .flex_1()
1516 .min_w_0()
1517 .role(Role::Heading),
1518 )
1519 .child(button),
1520 );
1521 let fields = div()
1522 .w_full()
1523 .flex()
1524 .gap(rem(12.))
1525 .pt(rem(18.))
1526 .pr(rem(24.))
1527 .pb(rem(16.))
1528 .pl(rem(24.))
1529 .min_h(rem(90.))
1530 .child(self.field(
1531 "start-field",
1532 start,
1533 if range {
1534 l.start_field.clone()
1535 } else {
1536 l.date_field.clone()
1537 },
1538 invalid.0,
1539 true,
1540 ))
1541 .when(range, |row| {
1542 row.child(self.field("end-field", end, l.end_field.clone(), invalid.1, false))
1543 });
1544 self.surface(DATE_INPUT_PICKER_WIDTH, 28.)
1545 .child(header)
1546 .child(fields)
1547 .child(self.action_row(actions, false))
1548 }
1549
1550 fn docked_row(&self) -> Div {
1553 let snap = &self.snap;
1554 let l = &snap.labels;
1555 let view = snap.view;
1556 let stepper = |name: &'static str,
1557 label: SharedString,
1558 list: DatePickerView,
1559 previous: (
1560 SharedString,
1561 fn(&mut DatePickerState, &mut Context<DatePickerState>),
1562 ),
1563 next: (
1564 SharedString,
1565 fn(&mut DatePickerState, &mut Context<DatePickerState>),
1566 ),
1567 accessible: SharedString| {
1568 let open = view == list;
1569 let dimmed = view != DatePickerView::Day && !open;
1570 let mut menu = PickerMenuButton::new(self.child_id(name), label)
1571 .accessible_label(accessible)
1572 .open(open)
1573 .arrow(!dimmed)
1574 .disabled(dimmed)
1575 .on_click(self.update(move |s, _, cx| s.toggle_view(list, cx)));
1576 if let Some(preview) = self.preview() {
1577 menu = menu.preview_state(if dimmed {
1578 InteractionPreview::Disabled
1579 } else {
1580 preview
1581 });
1582 }
1583 let (prev_label, prev) = previous;
1584 let (next_label, next) = next;
1585 let show_steppers = view == DatePickerView::Day;
1586 let months = if list == DatePickerView::Month { 1 } else { 12 };
1588 let (has_prev, has_next) =
1589 (snap.can_step_months(-months), snap.can_step_months(months));
1590 div()
1591 .flex()
1592 .items_center()
1593 .when(show_steppers, |row| {
1594 row.child(
1595 self.icon_button(
1596 if list == DatePickerView::Month {
1597 "prev-month"
1598 } else {
1599 "prev-year"
1600 },
1601 "chevron_left",
1602 prev_label,
1603 move |s, _, cx| prev(s, cx),
1604 )
1605 .disabled(!has_prev),
1606 )
1607 })
1608 .child(menu)
1609 .when(show_steppers, |row| {
1610 row.child(
1611 self.icon_button(
1612 if list == DatePickerView::Month {
1613 "next-month"
1614 } else {
1615 "next-year"
1616 },
1617 "chevron_right",
1618 next_label,
1619 move |s, _, cx| next(s, cx),
1620 )
1621 .disabled(!has_next),
1622 )
1623 })
1624 };
1625 div()
1626 .w_full()
1627 .h(rem(64.))
1628 .flex()
1629 .items_center()
1630 .justify_between()
1631 .when(view != DatePickerView::Day, |row| {
1632 row.px(rem(40.))
1633 .border_b(rem(1.))
1634 .border_color(color(ColorRole::OUTLINE_VARIANT))
1635 })
1636 .child(stepper(
1637 "docked-month",
1638 (l.month_short)(snap.month),
1639 DatePickerView::Month,
1640 (l.previous_month.clone(), DatePickerState::previous_month),
1641 (l.next_month.clone(), DatePickerState::next_month),
1642 l.choose_month.clone(),
1643 ))
1644 .child(stepper(
1645 "docked-year",
1646 (l.number)(snap.year),
1647 DatePickerView::Year,
1648 (l.previous_year.clone(), DatePickerState::previous_year),
1649 (l.next_year.clone(), DatePickerState::next_year),
1650 l.choose_year.clone(),
1651 ))
1652 }
1653
1654 fn docked_list(&self, months: bool) -> Stateful<Div> {
1656 let snap = self.snap.clone();
1657 let first = *snap.years.start();
1658 let count = if months {
1659 12
1660 } else {
1661 (snap.years.end() - first + 1).max(0) as usize
1662 };
1663 let state = self.state.clone();
1664 let specimen = self.specimen;
1665 let list = uniform_list(self.child_id("docked-list"), count, move |range, _, _| {
1666 range
1667 .map(|index| {
1668 let (label, selected, cursor) = if months {
1669 let month = index as u32 + 1;
1670 (
1671 (snap.labels.month)(month),
1672 month == snap.month,
1673 month == snap.cursor.month(),
1674 )
1675 } else {
1676 let year = first + index as i32;
1677 (
1678 (snap.labels.number)(year),
1679 year == snap.year,
1680 year == snap.cursor.year(),
1681 )
1682 };
1683 let mut item = PickerListItem::new(index, label).selected(selected);
1684 if specimen {
1685 item = item.preview_state(InteractionPreview::Enabled);
1686 } else {
1687 let state = state.clone();
1688 item = item.cursor(snap.focused && cursor).on_click(move |_, cx| {
1689 state.update(cx, |s, cx| {
1690 if months {
1691 s.choose_month(index as u32 + 1, cx)
1692 } else {
1693 s.choose_year(first + index as i32, cx)
1694 }
1695 })
1696 });
1697 }
1698 item
1699 })
1700 .collect()
1701 })
1702 .track_scroll(&self.snap.list_scroll)
1703 .h(rem(LIST_ITEM_HEIGHT * 7.));
1704 let label = if months {
1705 self.snap.labels.choose_month.clone()
1706 } else {
1707 self.snap.labels.choose_year.clone()
1708 };
1709 self.key_host(
1710 div()
1711 .id(self.child_id("docked-list-host"))
1712 .w_full()
1713 .pr(rem(16.))
1714 .pt(rem(8.))
1715 .pb(rem(20.))
1716 .role(Role::ListBox)
1717 .aria_label(label)
1718 .child(list),
1719 )
1720 }
1721
1722 fn docked(self, actions: Option<ActionRow>) -> Stateful<Div> {
1723 let view = self.snap.view;
1724 let surface = self
1725 .surface(DATE_PICKER_WIDTH, 16.)
1726 .shadow(elevation::level(3))
1727 .child(self.docked_row());
1728 match view {
1729 DatePickerView::Day => surface
1730 .child(self.day_grid(true).pb(rem(4.)))
1731 .when_some(actions, |s, actions| {
1732 s.child(self.action_row(actions, false))
1733 }),
1734 DatePickerView::Month => surface.child(self.docked_list(true)),
1735 DatePickerView::Year => surface.child(self.docked_list(false)),
1736 }
1737 }
1738
1739 fn full_screen(self, actions: ActionRow, height: f32) -> Stateful<Div> {
1742 let l = self.snap.labels.clone();
1743 let cancel = actions.on_cancel.clone();
1744 let confirm = actions.on_confirm.clone();
1745 let state = self.state.clone();
1746 let mut close = IconButton::new(self.child_id("close"), icon("close"), l.close.clone())
1747 .on_click(move |_, window, cx| {
1748 if let Some(f) = &cancel {
1749 f(window, cx);
1750 }
1751 });
1752 if let Some(preview) = self.preview() {
1753 close = close.preview_state(preview);
1754 }
1755 let save = self.text_button("save", l.save.clone(), move |window, cx| {
1756 let value = state.read(cx).selection;
1757 if let Some(f) = &confirm {
1758 f(value, window, cx);
1759 }
1760 });
1761 let header = div()
1762 .w_full()
1763 .flex()
1764 .flex_col()
1765 .pt(rem(12.))
1766 .border_b(rem(1.))
1767 .border_color(color(ColorRole::OUTLINE_VARIANT))
1768 .child(
1769 div()
1770 .h(rem(48.))
1771 .flex()
1772 .items_center()
1773 .justify_between()
1774 .pr(rem(12.))
1775 .child(close)
1776 .child(save),
1777 )
1778 .child(
1779 div()
1780 .h(rem(72.))
1781 .flex()
1782 .items_end()
1783 .gap(rem(8.))
1784 .pl(rem(64.))
1785 .pr(rem(12.))
1786 .pb(rem(12.))
1787 .child(
1788 div()
1789 .flex_1()
1790 .min_w_0()
1791 .flex()
1792 .flex_col()
1793 .gap(rem(8.))
1794 .pb(rem(4.))
1795 .child(label_text(
1796 "title",
1797 l.title.clone(),
1798 TypeRole::LabelLarge,
1799 color(ColorRole::ON_SURFACE_VARIANT),
1800 ))
1801 .child(
1802 label_text(
1803 "headline",
1804 self.snap.headline(),
1805 TypeRole::TitleLarge,
1806 color(ColorRole::ON_SURFACE),
1807 )
1808 .role(Role::Heading),
1809 ),
1810 )
1811 .child(self.icon_button(
1812 "mode",
1813 "edit",
1814 l.switch_to_input.clone(),
1815 |s, window, cx| s.toggle_mode(window, cx),
1816 )),
1817 );
1818 let snap = self.snap.clone();
1819 let first = *snap.years.start();
1820 let count = ((snap.years.end() - first + 1).max(0) as usize) * 12;
1821 let me = Rc::new(PickerCx {
1822 id: self.id.clone(),
1823 state: self.state.clone(),
1824 snap: snap.clone(),
1825 specimen: self.specimen,
1826 reduce_motion: self.reduce_motion,
1827 });
1828 let months = uniform_list(self.child_id("months"), count, move |range, _, _| {
1829 range
1830 .map(|index| {
1831 let year = first + (index / 12) as i32;
1832 let month = (index % 12) as u32 + 1;
1833 div()
1834 .id(index)
1835 .flex()
1836 .flex_col()
1837 .px(rem(GRID_PADDING))
1838 .child(
1839 div()
1840 .h(rem(48.))
1841 .pt(rem(16.))
1842 .px(rem(24.))
1843 .pb(rem(12.))
1844 .child(label_text(
1845 "month",
1846 (me.snap.labels.month_year)(year, month),
1847 TypeRole::TitleSmall,
1848 color(ColorRole::ON_SURFACE_VARIANT),
1849 )),
1850 )
1851 .child(me.weeks(year, month, false, true, &format!("m{index}")))
1852 })
1853 .collect()
1854 })
1855 .track_scroll(&self.snap.month_scroll)
1856 .flex_1();
1857 let grid = self.key_host(
1858 div()
1859 .id(self.child_id("month-list"))
1860 .flex_1()
1861 .min_h_0()
1862 .flex()
1863 .flex_col()
1864 .role(Role::Grid)
1865 .aria_label(l.title.clone())
1866 .child(months),
1867 );
1868 div()
1869 .id(self.id.clone())
1870 .w(rem(DATE_PICKER_WIDTH))
1871 .h(rem(height))
1872 .flex()
1873 .flex_col()
1874 .overflow_hidden()
1875 .bg(color(ColorRole::SURFACE_CONTAINER_HIGH))
1876 .role(Role::Dialog)
1877 .child(header)
1878 .child(div().px(rem(GRID_PADDING)).child(self.weekday_row()))
1879 .child(grid)
1880 .child(self.action_row(actions, true).border_t(rem(0.)))
1881 }
1882}
1883
1884pub fn today() -> NaiveDate {
1886 chrono::Local::now().date_naive()
1887}
1888
1889#[derive(gpui::IntoElement)]
1893pub struct PickerDialog {
1894 id: ElementId,
1895 open: bool,
1896 content: AnyElement,
1897 on_dismiss: Option<Activate>,
1898}
1899
1900impl PickerDialog {
1901 pub fn new(id: impl Into<ElementId>, open: bool, content: impl IntoElement) -> Self {
1902 Self {
1903 id: id.into(),
1904 open,
1905 content: content.into_any_element(),
1906 on_dismiss: None,
1907 }
1908 }
1909 pub fn on_dismiss(mut self, f: impl Fn(&mut Window, &mut App) + 'static) -> Self {
1911 self.on_dismiss = Some(Rc::new(f));
1912 self
1913 }
1914}
1915
1916impl RenderOnce for PickerDialog {
1917 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1918 if !self.open {
1919 return div().into_any_element();
1920 }
1921 let focus = window
1922 .use_keyed_state((self.id.clone(), "dialog-focus"), cx, |_, cx| {
1923 cx.focus_handle()
1924 })
1925 .read(cx)
1926 .clone();
1927 let dismiss = self.on_dismiss.clone();
1928 let mut scrim = color(ColorRole::SCRIM);
1929 scrim.a *= 0.32;
1930 let surface = div()
1931 .rounded(rem(28.))
1932 .shadow(elevation::level(3))
1933 .child(self.content);
1934 gpui_base::Dialog::new(cx)
1935 .focus_handle(focus)
1936 .open(true)
1937 .on_open_change(move |open, _, window, cx| {
1938 if !open && let Some(f) = &dismiss {
1939 f(window, cx);
1940 }
1941 })
1942 .backdrop(div().size_full().bg(scrim).occlude())
1943 .popup(motion::state_enter((self.id, "enter"), surface))
1944 .into_any_element()
1945 }
1946}
1947
1948#[cfg(test)]
1949#[path = "date_picker_tests.rs"]
1950mod tests;