Skip to main content

gpui_kit_material/components/
date_picker.rs

1//! M3 date pickers: docked, modal, modal input and full-screen, for a single
2//! date or a date range.
3//!
4//! Reference: <https://m3.material.io/components/date-pickers/specs> and the
5//! Material 3 Design Kit "Date & time pickers" page (inspected tokens:
6//! surface-container-high containers with 28dp corners for the modal and
7//! 16dp for the docked menu; a 120dp modal header with label-large
8//! supporting text and a headline-large date over an outline-variant
9//! divider; a 56dp month row with the month/year menu button and
10//! previous/next icon buttons; a 48dp weekday row of body-large letters; a
11//! 60dp action row of text buttons; the 128dp input header with the 52dp
12//! wide "today" icon button and outlined text fields; the docked menu's 64dp
13//! month/year selection row and 48dp month and year lists).
14//!
15//! Behavior: the value types and disabled-date matcher are GPUI Base's
16//! calendar types ([`DateSelection`] = `gpui_base::Date`,
17//! [`DateMatcher`] = `gpui_base::Matcher`) and range selection follows
18//! `gpui_base::CalendarState::select_date`. Base's calendar has no keyboard
19//! grid and fixes "today" to the system clock, so the picker keeps its own
20//! state entity: a single tab stop on the day grid (or year grid/list) with
21//! an active-descendant cursor that the arrow keys move by a day or week,
22//! PageUp/PageDown by a month, Shift+PageUp/PageDown by a year and
23//! Home/End to the week's ends; Enter or Space selects. Typed dates use
24//! Kit's input through [`TextField`]. Every string — month and weekday
25//! names, headlines, button labels and the input format — comes from the
26//! caller's [`DatePickerLabels`].
27//!
28//! Motion: a month change slides the outgoing grid out and the incoming
29//! grid in by the grid's width on the default spatial spring (both fade on
30//! the way); selection fills crossfade on the fast effects spring; the menu
31//! arrow turns on the fast spatial spring. Reduced motion shows the new
32//! month at once.
33use 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/// Every caller-supplied string and formatter of a date picker. Nothing
98/// user-facing is hard-coded in the component; galleries and apps build
99/// this from their locale.
100#[derive(Clone)]
101pub struct DatePickerLabels {
102    /// The modal header's supporting text, e.g. "Select date".
103    pub title: SharedString,
104    /// The headline while nothing is selected in calendar mode.
105    pub no_selection: SharedString,
106    /// The input-mode headline, e.g. "Enter date" (or "Enter dates").
107    pub input_headline: SharedString,
108    pub confirm: SharedString,
109    pub cancel: SharedString,
110    pub clear: SharedString,
111    /// The full-screen picker's confirm action, e.g. "Save".
112    pub save: SharedString,
113    /// Accessible names of icon-only controls.
114    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    /// Accessible name of the year grid / month and year lists.
122    pub choose_year: SharedString,
123    pub choose_month: SharedString,
124    /// Input-mode field labels and the format shown as placeholder.
125    pub date_field: SharedString,
126    pub start_field: SharedString,
127    pub end_field: SharedString,
128    pub input_placeholder: SharedString,
129    /// Shown under a field whose text does not parse.
130    pub invalid_input: SharedString,
131    /// A narrow weekday letter for the weekday row ("S", "M", …).
132    pub weekday: Format<Weekday>,
133    /// A full month name for the docked month list (1-based month).
134    pub month: Format<u32>,
135    /// A short month name for the docked month menu button.
136    pub month_short: Format<u32>,
137    /// "August 2025": the modal month button and full-screen month headers.
138    pub month_year: Rc<dyn Fn(i32, u32) -> SharedString>,
139    /// The single-date headline, e.g. "Mon, Aug 17".
140    pub headline: Format<NaiveDate>,
141    /// The range headline, e.g. "Aug 17 – Aug 23" (either end may be open).
142    pub range_headline: Rc<dyn Fn(Option<NaiveDate>, Option<NaiveDate>) -> SharedString>,
143    /// The full date announced for a day cell.
144    pub accessible_date: Format<NaiveDate>,
145    /// A day-of-month or year number.
146    pub number: Format<i32>,
147    /// The text-input representation of a date, and its parser.
148    pub format_input: Format<NaiveDate>,
149    pub parse_input: Parse,
150}
151
152/// Calendar or text entry.
153#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
154pub enum DateInputMode {
155    #[default]
156    Calendar,
157    Input,
158}
159
160/// What the picker body shows.
161#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
162pub enum DatePickerView {
163    /// The day grid.
164    #[default]
165    Day,
166    /// The modal year grid, or the docked year list.
167    Year,
168    /// The docked month list.
169    Month,
170}
171
172/// The M3 date picker presentations.
173#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
174pub enum DatePickerVariant {
175    /// A dialog with a header, used on mobile and for text input.
176    #[default]
177    Modal,
178    /// A menu below a text field, used on desktop.
179    Docked,
180    /// A full-screen dialog with a scrolling month list, for ranges.
181    FullScreen,
182}
183
184/// A month transition in flight: the month it leaves and its direction.
185#[derive(Clone, Copy, Debug, PartialEq)]
186struct Slide {
187    from: (i32, u32),
188    direction: f32,
189    generation: u64,
190}
191
192/// A date picker's state: selection, displayed month, view, mode and the
193/// keyboard cursor. Create it once per picker with `cx.new` and hand it to
194/// [`DatePicker`].
195pub 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/// Events a picker emits as the person edits it.
219#[derive(Clone, Copy, Debug, PartialEq, Eq)]
220pub enum DatePickerEvent {
221    /// The selection changed (possibly to an incomplete range).
222    Change(DateSelection),
223}
224
225impl EventEmitter<DatePickerEvent> for DatePickerState {}
226
227impl DatePickerState {
228    /// A single-date picker showing `today`'s month, with nothing selected.
229    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    /// Select a range instead of a single date.
256    pub fn range(mut self) -> Self {
257        self.selection = DateSelection::Range(None, None);
258        self
259    }
260    /// Start with `selection`, showing its (first) month.
261    pub fn selection(mut self, selection: impl Into<DateSelection>) -> Self {
262        self.apply(selection.into());
263        self.reveal_cursor();
264        self
265    }
266    /// Dates matching `matcher` cannot be selected.
267    pub fn disabled_dates(mut self, matcher: impl Into<DateMatcher>) -> Self {
268        self.disabled = Some(Rc::new(matcher.into()));
269        self
270    }
271    /// The years the year grid and lists offer.
272    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    /// The weekday each grid row starts on (default Sunday).
283    pub fn first_day_of_week(mut self, day: Weekday) -> Self {
284        self.first_day = day;
285        self
286    }
287    /// Start in `view` (e.g. a specimen of the year grid).
288    pub fn view(mut self, view: DatePickerView) -> Self {
289        self.view = view;
290        self.reveal_cursor();
291        self
292    }
293    /// Start in text-input mode.
294    pub fn input_mode(mut self) -> Self {
295        self.mode = DateInputMode::Input;
296        self
297    }
298    /// Edit typed dates in these Kit inputs (the second for a range's end).
299    /// The picker parses them with [`DatePickerLabels::parse_input`].
300    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    /// The displayed `(year, month)`.
337    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    /// The keyboard cursor's date.
347    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            // A date outside `years` stays selected, but the grid shows the
361            // nearest month the picker offers.
362            (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    /// Replace the selection without emitting.
372    pub fn set_value(&mut self, selection: DateSelection, cx: &mut Context<Self>) {
373        self.apply(selection);
374        cx.notify();
375    }
376
377    /// Select `date` as a person would: a single picker replaces its date; a
378    /// range starts, completes (when `date` is not before the start) or
379    /// restarts, as in `gpui_base::CalendarState::select_date`. Disabled
380    /// dates are ignored. Returns whether the selection changed.
381    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    /// Clear the selection (keeping single or range).
398    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    /// Show `year`/`month`, sliding from the current month.
410    /// Months outside `years` show the nearest month inside them.
411    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        // Drop the outgoing grid once its slide has finished.
432        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    /// Switch the body between the day grid, the year grid/list and the
465    /// docked month list.
466    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    /// Toggle `view` against the day grid.
480    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    /// Choose a year from the year grid or list and return to the days.
490    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    /// Choose a month from the docked list and return to the days.
497    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    /// Switch between calendar and text input. Entering input mode writes
505    /// the selection into the inputs; leaving it parses them.
506    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    /// Parse the text inputs into the selection. Invalid or disabled text
537    /// marks its field invalid and leaves the selection unchanged.
538    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    /// Move the keyboard cursor and keep it visible.
582    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        // Never past the lists' ends: a year outside `years` reveals the
603        // nearest one.
604        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
645/// `year`/`month` (normalized, so month 13 is January next year) kept
646/// inside the January of the first and the December of the last of `years`.
647fn 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
656/// The full-screen month list's row for `year`/`month0`, never past its
657/// last row (`years` holds twelve rows per year).
658fn 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
663/// Whether `year`/`month` lies inside `years`.
664fn 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
682/// The range rule of `gpui_base::CalendarState::select_date`.
683pub(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/// One keyboard movement.
695#[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
711/// Where `step` moves the cursor in `view`: days and weeks in the day grid,
712/// years in rows of three in the year grid, and one row in the docked lists.
713pub(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/// An M3 date picker over a [`DatePickerState`].
761#[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
774/// Widths of the presentations, in dp.
775pub 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    /// A static specimen: no tab stops, no motion, no pointer input.
805    pub fn specimen(mut self) -> Self {
806        self.specimen = true;
807        self
808    }
809    /// Show the Clear action (default on).
810    pub fn show_clear(mut self, show: bool) -> Self {
811        self.show_clear = show;
812        self
813    }
814    /// Show the action row (default on; a docked picker that commits on
815    /// selection may hide it).
816    pub fn show_actions(mut self, show: bool) -> Self {
817        self.show_actions = show;
818        self
819    }
820    /// The full-screen picker's height in dp (default 696).
821    pub fn height(mut self, height: f32) -> Self {
822        self.height = Some(height);
823        self
824    }
825    /// OK / Save: receives the selection.
826    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    /// Cancel / Close.
834    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
840/// A snapshot of the state a render needs.
841struct 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    /// Whether the month `delta` months from the shown one is offered.
865    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    /// Run `f` on the state from a listener.
999    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    /// The keyboard host around a grid or list: one tab stop, the picker's
1220    /// key context and actions.
1221    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    /// The weeks of one month: `adjacent` shows the neighbouring months'
1252    /// days (docked) instead of empty cells (modal).
1253    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    /// The day grid with its month slide.
1308    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    /// The modal year grid: three years per 52dp row, virtualized.
1365    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    /// The docked menu's selection row: month and year steppers with their
1551    /// menu buttons; the inactive one dims while the other's list is open.
1552    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            // Steppers stop at the ends of `years`.
1587            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    /// The docked month or year list.
1655    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    /// The full-screen range picker: close and save, the header, a fixed
1740    /// weekday row and a vertically scrolling, virtualized month list.
1741    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
1884/// Today's date on the local clock, for a picker's today marker.
1885pub fn today() -> NaiveDate {
1886    chrono::Local::now().date_naive()
1887}
1888
1889/// A modal host for pickers: a scrim and a centered dialog over the
1890/// window, built on GPUI Base's dialog (focus trap, Escape and outside
1891/// press dismissal). The surface enters on the default spatial spring.
1892#[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    /// Escape or a press on the scrim.
1910    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;