Skip to main content

herogpui_components/date_picker/
range.rs

1//! DateRangePicker.
2
3use super::*;
4
5// DateRangePicker
6// ---------------------------------------------------------------------------
7
8/// State entity for [`DateRangePicker`].
9pub struct DateRangeState {
10    /// Year of the month currently shown.
11    pub view_year: i32,
12    /// Month (1-12) currently shown.
13    pub view_month: u32,
14    /// Anchor day for the week and day views; the month view ignores it.
15    pub view_day: u32,
16    /// Start of the selected range, if any.
17    pub start: Option<Date>,
18    /// End of the selected range, if any.
19    pub end: Option<Date>,
20    /// Live cell under the cursor — drives the hover preview range.
21    pub hovered: Option<Date>,
22    /// Set once the user pages or picks, after which `selectionAlignment`
23    /// stops re-deriving the visible range.
24    pub user_navigated: bool,
25}
26
27impl DateRangeState {
28    /// Creates a state showing the current month with no range selected.
29    pub fn new(_cx: &mut App) -> Self {
30        let t = Date::today();
31        Self {
32            view_year: t.year,
33            view_month: t.month,
34            view_day: t.day,
35            start: None,
36            end: None,
37            hovered: None,
38            user_navigated: false,
39        }
40    }
41
42    /// `defaultValue` — a state seeded with an initial range.
43    pub fn with_range(cx: &mut App, start: Option<Date>, end: Option<Date>) -> Self {
44        let mut state = Self::new(cx);
45        state.start = start;
46        state.end = end;
47        state
48    }
49
50    /// The date the visible range starts from.
51    pub fn anchor(&self) -> Date {
52        Date::new(self.view_year, self.view_month, self.view_day.max(1))
53    }
54
55    /// Moves the visible range, recording that the user drove it.
56    pub fn set_anchor(&mut self, date: Date) {
57        self.view_year = date.year;
58        self.view_month = date.month;
59        self.view_day = date.day;
60        self.user_navigated = true;
61    }
62
63    /// The range's moving edge while the user hovers before picking the end.
64    pub fn preview_end(&self) -> Option<Date> {
65        if self.end.is_some() {
66            self.end
67        } else if self.start.is_some() {
68            self.hovered
69        } else {
70            None
71        }
72    }
73
74    /// Click logic: first click sets start; second click sets end (or restarts
75    /// when earlier than start).
76    pub fn pick(&mut self, d: Date) {
77        self.user_navigated = true;
78        match (self.start, self.end) {
79            (_, Some(_)) | (None, _) => {
80                self.start = Some(d);
81                self.end = None;
82            }
83            (Some(s), None) => {
84                if days_from_civil(&d) < days_from_civil(&s) {
85                    self.start = Some(d);
86                } else {
87                    self.end = Some(d);
88                }
89            }
90        }
91    }
92}
93
94/// State passed to DateRangePicker's v3 children render function.
95#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
96#[non_exhaustive]
97pub struct DateRangePickerRenderState {
98    /// The whole picker is unavailable.
99    pub is_disabled: bool,
100    /// Either range end currently fails validation.
101    pub is_invalid: bool,
102    /// The range can be focused but not edited.
103    pub is_read_only: bool,
104    /// Both range ends must contain dates before native submission.
105    pub is_required: bool,
106    /// A composed child currently owns focus.
107    pub is_focus_within: bool,
108    /// Focus within the picker was reached through keyboard navigation.
109    pub is_focus_visible: bool,
110    /// The range calendar popover is currently open.
111    pub is_open: bool,
112}
113
114/// HeroUI DateRangePicker.
115#[must_use = "a component does nothing until it is rendered: add it as a child or return it from `render`"]
116#[derive(IntoElement)]
117pub struct DateRangePicker {
118    /// The locale whose calendar system the popover's grid is drawn in, when
119    /// the caller names one. Forwarded to the embedded [`RangeCalendar`].
120    locale: Option<SharedString>,
121    content:
122        Option<std::sync::Arc<dyn Fn(DateRangePickerRenderState) -> gpui::AnyElement + 'static>>,
123    /// `startName` / `endName` — read back by
124    /// [`DateRangePicker::form_fields`].
125    start_name: Option<SharedString>,
126    end_name: Option<SharedString>,
127    /// `defaultValue` — seeds the state on the first render only.
128    default_value: Option<(Date, Date)>,
129    /// `value` — v3's controlled range, stored for the first render only.
130    /// Each end is its own `Option` so a half-open controlled range keeps its
131    /// null end, exactly as the builder spelled it.
132    value: Option<(Option<Date>, Option<Date>)>,
133    state: Entity<DateRangeState>,
134    /// `isOpen` — `None` leaves the picker holding the flag, seeded from
135    /// `defaultOpen`.
136    is_open: Option<bool>,
137    default_open: bool,
138    should_close_on_select: bool,
139    /// Popover placement, matching `DateRangePicker.Popover`'s placement prop.
140    placement: herogpui_core::Placement,
141    label: Option<SharedString>,
142    trigger_indicator: Option<gpui::AnyElement>,
143    range_separator: Option<gpui::AnyElement>,
144    /// The fill the trigger takes on hover, in place of `--field-hover`.
145    trigger_hover_bg: Option<gpui::Hsla>,
146    is_disabled: bool,
147    is_read_only: bool,
148    is_required: bool,
149    validation_behavior: crate::form::ValidationBehavior,
150    validate: Option<crate::validation::Validator<Option<(Date, Date)>>>,
151    validation_errors: Vec<SharedString>,
152    is_invalid: bool,
153    auto_focus: bool,
154    constraints: DateConstraints,
155    on_open_change: Option<std::sync::Arc<dyn Fn(&bool, &mut Window, &mut App) + 'static>>,
156    on_change: Option<std::sync::Arc<dyn Fn(&mut Window, &mut App) + 'static>>,
157    start_form_state: Rc<RefCell<crate::form::LiveFormFieldState>>,
158    end_form_state: Rc<RefCell<crate::form::LiveFormFieldState>>,
159    form_is_disabled: Rc<Cell<bool>>,
160    form_default: Rc<RefCell<Option<(Date, Date)>>>,
161    form_on_change: Rc<RefCell<Option<std::sync::Arc<dyn Fn(&mut Window, &mut App) + 'static>>>>,
162    start_field_state: Rc<RefCell<Option<Entity<crate::input::InputState>>>>,
163    end_field_state: Rc<RefCell<Option<Entity<crate::input::InputState>>>>,
164    form_restore: std::sync::Arc<dyn Fn(&mut Window, &mut App) + 'static>,
165    /// The `sx` slot, refined over the root style at the end of render.
166    sx: Option<Box<gpui::StyleRefinement>>,
167}
168
169impl DateRangePicker {
170    /// The locale whose calendar system the popover's grid is drawn in.
171    ///
172    /// Forwarded to the embedded calendar; see [`crate::Calendar::locale`] for
173    /// why this is a builder rather than v3's `I18nProvider`.
174    pub fn locale(mut self, tag: impl Into<SharedString>) -> Self {
175        self.locale = Some(tag.into());
176        self
177    }
178
179    /// Creates a date range picker bound to `state`.
180    pub fn new(state: Entity<DateRangeState>) -> Self {
181        let entity_id = state.entity_id().as_u64();
182        let start_form_state = date_range_picker_form_state(entity_id, false);
183        let end_form_state = date_range_picker_form_state(entity_id, true);
184        let form_is_disabled = Rc::new(Cell::new(false));
185        let form_default = Rc::new(RefCell::new(None));
186        let form_on_change: Rc<RefCell<Option<std::sync::Arc<dyn Fn(&mut Window, &mut App)>>>> =
187            Rc::new(RefCell::new(None));
188        let start_field_state = Rc::new(RefCell::new(None::<Entity<crate::input::InputState>>));
189        let end_field_state = Rc::new(RefCell::new(None::<Entity<crate::input::InputState>>));
190        let restore_start = Rc::downgrade(&start_form_state);
191        let restore_end = Rc::downgrade(&end_form_state);
192        let restore_is_disabled = form_is_disabled.clone();
193        let restore_default = form_default.clone();
194        let restore_callback = form_on_change.clone();
195        let restore_state = state.clone();
196        let restore_start_field = start_field_state.clone();
197        let restore_end_field = end_field_state.clone();
198        // FormField's restore slot is an Arc, but this callback is intentionally
199        // confined to the single-threaded GPUI app and captures its live state.
200        #[allow(clippy::arc_with_non_send_sync)]
201        let restore: std::sync::Arc<dyn Fn(&mut Window, &mut App)> =
202            std::sync::Arc::new(move |window, cx| {
203                let (start, end) = restore_default
204                    .borrow()
205                    .map(|range: (Date, Date)| (Some(range.0), Some(range.1)))
206                    .unwrap_or((None, None));
207                restore_state.update(&mut *cx, |state, cx| {
208                    state.start = start;
209                    state.end = end;
210                    if let Some(date) = start {
211                        state.view_year = date.year;
212                        state.view_month = date.month;
213                        state.view_day = date.day;
214                    }
215                    cx.notify();
216                });
217                if let Some(field_state) = restore_start_field.borrow().clone() {
218                    let text = start.map(|date| date.format_iso()).unwrap_or_default();
219                    field_state.update(cx, |state, cx| {
220                        state.set_value(text);
221                        cx.notify();
222                    });
223                }
224                if let Some(field_state) = restore_end_field.borrow().clone() {
225                    let text = end.map(|date| date.format_iso()).unwrap_or_default();
226                    field_state.update(cx, |state, cx| {
227                        state.set_value(text);
228                        cx.notify();
229                    });
230                }
231                if let Some(state) = restore_start.upgrade() {
232                    let mut state = state.borrow_mut();
233                    state.value = crate::form::FormValue::Text(
234                        start
235                            .map(|date| date.format_iso())
236                            .unwrap_or_default()
237                            .into(),
238                    );
239                    state.is_invalid = false;
240                    state.is_successful = !restore_is_disabled.get();
241                }
242                if let Some(state) = restore_end.upgrade() {
243                    let mut state = state.borrow_mut();
244                    state.value = crate::form::FormValue::Text(
245                        end.map(|date| date.format_iso()).unwrap_or_default().into(),
246                    );
247                    state.is_invalid = false;
248                    state.is_successful = !restore_is_disabled.get();
249                }
250                if let Some(callback) = restore_callback.borrow().as_ref() {
251                    callback(window, cx);
252                }
253            });
254        start_form_state.borrow_mut().restore = Some(restore.clone());
255        Self {
256            locale: None,
257            content: None,
258            start_name: None,
259            end_name: None,
260            default_value: None,
261            value: None,
262            state,
263            is_open: None,
264            default_open: false,
265            should_close_on_select: true,
266            placement: herogpui_core::Placement::Bottom,
267            label: None,
268            trigger_indicator: None,
269            range_separator: None,
270            trigger_hover_bg: None,
271            is_disabled: false,
272            is_read_only: false,
273            is_required: false,
274            validation_behavior: crate::form::ValidationBehavior::Native,
275            validate: None,
276            validation_errors: Vec::new(),
277            is_invalid: false,
278            auto_focus: false,
279            constraints: DateConstraints::new(),
280            on_open_change: None,
281            on_change: None,
282            start_form_state,
283            end_form_state,
284            form_is_disabled,
285            form_default,
286            form_on_change,
287            start_field_state,
288            end_field_state,
289            form_restore: restore,
290            sx: None,
291        }
292    }
293
294    /// `startName` — the name the range's start submits under.
295    pub fn start_name(mut self, name: impl Into<SharedString>) -> Self {
296        self.start_name = Some(name.into());
297        self
298    }
299
300    /// `endName` — the name the range's end submits under.
301    pub fn end_name(mut self, name: impl Into<SharedString>) -> Self {
302        self.end_name = Some(name.into());
303        self
304    }
305
306    /// The `Form` fields this picker submits: one per named end of the range,
307    /// each written ISO-8601.
308    pub fn form_fields(&self, cx: &App) -> Vec<crate::form::FormField> {
309        self.form_is_disabled.set(self.is_disabled);
310        self.start_form_state.borrow_mut().restore =
311            self.start_name.as_ref().map(|_| self.form_restore.clone());
312        self.end_form_state.borrow_mut().restore = (self.start_name.is_none()
313            && self.end_name.is_some())
314        .then(|| self.form_restore.clone());
315        let state = self.state.read(cx);
316        if self.form_default.borrow().is_none() {
317            if let (Some(start), Some(end)) = (state.start, state.end) {
318                *self.form_default.borrow_mut() = Some((start, end));
319            }
320        }
321        let range = state.start.zip(state.end);
322        let custom_error = self.validate.as_ref().and_then(|validate| validate(&range));
323        let mut out = Vec::new();
324        if let Some(name) = self.start_name.clone() {
325            let text = state.start.map(|d| d.format_iso()).unwrap_or_default();
326            let mut form_state = self.start_form_state.borrow_mut();
327            form_state.value = crate::form::FormValue::Text(text.into());
328            form_state.is_successful = !self.is_disabled;
329            let constraint_error = state
330                .start
331                .is_some_and(|date| !self.constraints.allows(date))
332                .then(|| SharedString::from("That date is unavailable."));
333            form_state.is_invalid = crate::validation::resolve(
334                self.is_invalid,
335                &self.validation_errors,
336                custom_error.clone(),
337                constraint_error,
338            )
339            .is_invalid;
340            out.push(
341                crate::form::FormField::live(name, self.start_form_state.clone())
342                    .is_required(self.is_required)
343                    .validation_behavior(self.validation_behavior),
344            );
345        }
346        if let Some(name) = self.end_name.clone() {
347            let text = state.end.map(|d| d.format_iso()).unwrap_or_default();
348            let mut form_state = self.end_form_state.borrow_mut();
349            form_state.value = crate::form::FormValue::Text(text.into());
350            form_state.is_successful = !self.is_disabled;
351            let constraint_error = state
352                .end
353                .is_some_and(|date| !self.constraints.allows(date))
354                .then(|| SharedString::from("That date is unavailable."));
355            form_state.is_invalid = crate::validation::resolve(
356                self.is_invalid,
357                &self.validation_errors,
358                custom_error,
359                constraint_error,
360            )
361            .is_invalid;
362            out.push(
363                crate::form::FormField::live(name, self.end_form_state.clone())
364                    .is_required(self.is_required)
365                    .validation_behavior(self.validation_behavior),
366            );
367        }
368        out
369    }
370
371    /// `defaultValue` — the uncontrolled initial range.
372    ///
373    /// Written into the state on the first render only, so it seeds the
374    /// component without fighting the user afterwards.
375    pub fn default_value(mut self, value: (Date, Date)) -> Self {
376        self.default_value = Some(value);
377        *self.form_default.borrow_mut() = Some(value);
378        self
379    }
380
381    /// Sets the label shown above the picker.
382    pub fn label(mut self, label: impl Into<SharedString>) -> Self {
383        self.label = Some(label.into());
384        self
385    }
386
387    /// Sets whether the picker is disabled (`isDisabled`).
388    pub fn is_disabled(mut self, v: bool) -> Self {
389        self.is_disabled = v;
390        self
391    }
392
393    /// The fill the trigger takes on hover, in place of `--field-hover`.
394    pub fn trigger_hover_bg(mut self, color: impl Into<gpui::Hsla>) -> Self {
395        self.trigger_hover_bg = Some(color.into());
396        self
397    }
398
399    /// `DateRangePicker.Popover.placement` — positions the range calendar
400    /// relative to the trigger. HeroUI defaults to `bottom`.
401    pub fn placement(mut self, placement: herogpui_core::Placement) -> Self {
402        self.placement = placement;
403        self
404    }
405
406    /// `isReadOnly` — keeps both fields focusable but blocks edits, selection and opening.
407    pub fn is_read_only(mut self, v: bool) -> Self {
408        self.is_read_only = v;
409        self
410    }
411
412    /// Sets whether the picker is required (`isRequired`).
413    pub fn is_required(mut self, v: bool) -> Self {
414        self.is_required = v;
415        self
416    }
417
418    /// `validationBehavior` — native errors block submission; ARIA-style errors do not.
419    pub fn validation_behavior(mut self, behavior: crate::form::ValidationBehavior) -> Self {
420        self.validation_behavior = behavior;
421        self
422    }
423
424    /// `validate` — returns one custom message for the complete selected range.
425    pub fn validate(
426        mut self,
427        f: impl Fn(&Option<(Date, Date)>) -> Option<SharedString> + 'static,
428    ) -> Self {
429        self.validate = Some(std::sync::Arc::new(f));
430        self
431    }
432
433    /// `validationErrors` — server messages take precedence over custom validation.
434    pub fn validation_errors(
435        mut self,
436        errors: impl IntoIterator<Item = impl Into<SharedString>>,
437    ) -> Self {
438        self.validation_errors = errors.into_iter().map(Into::into).collect();
439        self
440    }
441
442    /// Sets whether the picker is invalid (`isInvalid`).
443    pub fn is_invalid(mut self, v: bool) -> Self {
444        self.is_invalid = v;
445        self
446    }
447
448    /// `autoFocus` — focuses the editable start field on its first render.
449    pub fn auto_focus(mut self, v: bool) -> Self {
450        self.auto_focus = v;
451        self
452    }
453
454    /// `value` — v3's controlled-range spelling, as a pure builder.
455    ///
456    /// The bound [`DateRangeState`] owns the range once the picker renders,
457    /// so this seeds the state on the first render only, winning over
458    /// [`DateRangePicker::default_value`] the way v3's controlled prop
459    /// outranks the uncontrolled seed; calling `.value(..)` twice keeps the
460    /// last call, like every other builder here. A later range is an
461    /// imperative update rather than a builder: write the caller-owned state
462    /// entity.
463    pub fn value(mut self, start: Option<Date>, end: Option<Date>) -> Self {
464        self.value = Some((start, end));
465        self
466    }
467
468    /// `minValue` — the earliest selectable date.
469    pub fn min_value(mut self, date: Date) -> Self {
470        self.constraints.min_value = Some(date);
471        self
472    }
473
474    /// `maxValue` — the latest selectable date.
475    pub fn max_value(mut self, date: Date) -> Self {
476        self.constraints.max_value = Some(date);
477        self
478    }
479
480    /// `isDateUnavailable` — blocks individual dates inside the range.
481    pub fn is_date_unavailable(mut self, f: impl Fn(Date) -> bool + 'static) -> Self {
482        self.constraints.is_date_unavailable = Some(std::sync::Arc::new(f));
483        self
484    }
485
486    /// `firstDayOfWeek` — overrides the range calendar's first weekday.
487    pub fn first_day_of_week(mut self, day: Weekday) -> Self {
488        self.constraints.first_day_of_week = day;
489        self
490    }
491
492    /// All the date constraints at once.
493    pub fn constraints(mut self, constraints: DateConstraints) -> Self {
494        self.constraints = constraints;
495        self
496    }
497
498    /// `onOpenChange` — reports the popover toggling, including trigger clicks.
499    pub fn on_open_change(mut self, f: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
500        self.on_open_change = Some(std::sync::Arc::new(f));
501        self
502    }
503
504    /// Sets the controlled open state of the popover (`isOpen`).
505    pub fn is_open(mut self, v: bool) -> Self {
506        self.is_open = Some(v);
507        self
508    }
509    /// `defaultOpen` — the uncontrolled initial popover state.
510    ///
511    /// Only consulted when `is_open` is not supplied; the picker then owns the
512    /// flag and its trigger toggles it.
513    pub fn default_open(mut self, v: bool) -> Self {
514        self.default_open = v;
515        self
516    }
517
518    /// Sets whether the popover closes after a selection completes (`shouldCloseOnSelect`).
519    pub fn should_close_on_select(mut self, v: bool) -> Self {
520        self.should_close_on_select = v;
521        self
522    }
523
524    /// Replaces the default trigger indicator.
525    pub fn trigger_indicator(mut self, indicator: impl IntoElement) -> Self {
526        self.trigger_indicator = Some(indicator.into_any_element());
527        self
528    }
529
530    /// Replaces the separator drawn between the start and end fields.
531    pub fn range_separator(mut self, separator: impl IntoElement) -> Self {
532        self.range_separator = Some(separator.into_any_element());
533        self
534    }
535
536    /// Sets a render function for the popover content, given the picker's render state.
537    pub fn content(
538        mut self,
539        render: impl Fn(DateRangePickerRenderState) -> gpui::AnyElement + 'static,
540    ) -> Self {
541        self.content = Some(std::sync::Arc::new(render));
542        self
543    }
544
545    /// Fired after any pick (read `start`/`end` from the entity).
546    pub fn on_change(mut self, f: impl Fn(&mut Window, &mut App) + 'static) -> Self {
547        let callback = std::sync::Arc::new(f);
548        *self.form_on_change.borrow_mut() = Some(callback.clone());
549        self.on_change = Some(callback);
550        self
551    }
552
553    /// The one slot for caller-owned low-level styling: GPUI's styling methods
554    /// (`bg`, `text_color`, `w`, `h`, `p`, `rounded`, `border_color`, …)
555    /// applied to the picker's root element after every value the component
556    /// and the active theme chose, so they win.
557    pub fn sx(mut self, style: impl FnOnce(gpui::Div) -> gpui::Div) -> Self {
558        crate::util::refine_sx(&mut self.sx, style);
559        self
560    }
561}
562
563impl RenderOnce for DateRangePicker {
564    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
565        self.form_is_disabled.set(self.is_disabled);
566        // Every keyed slot and focus handle below hangs off this one base, so
567        // no two of them can flatten into a shared key.
568        let base_id = gpui::ElementId::named_usize("drp", self.state.entity_id().as_u64() as usize);
569        // `value` / `defaultValue` seed the state once, before anything reads
570        // it. `value` is v3's controlled spelling, so it outranks the
571        // uncontrolled seed; the state owns the range afterwards, and a write
572        // to the caller-owned entity is the imperative update.
573        if let Some((start, end)) = self.value {
574            let state = self.state.clone();
575            crate::util::seed_once(
576                window,
577                cx,
578                gpui::ElementId::named_usize(
579                    "daterangepicker-default",
580                    self.state.entity_id().as_u64() as usize,
581                ),
582                move |cx| {
583                    state.update(cx, |s, cx| {
584                        s.start = start;
585                        s.end = end;
586                        if let Some(start) = start {
587                            s.view_year = start.year;
588                            s.view_month = start.month;
589                            s.view_day = start.day;
590                        }
591                        cx.notify();
592                    });
593                },
594            );
595        } else if let Some(value) = self.default_value {
596            let state = self.state.clone();
597            crate::util::seed_once(
598                window,
599                cx,
600                gpui::ElementId::named_usize(
601                    "daterangepicker-default",
602                    self.state.entity_id().as_u64() as usize,
603                ),
604                move |cx| {
605                    state.update(cx, |s, cx| {
606                        s.start = Some(value.0);
607                        s.end = Some(value.1);
608                        s.view_year = value.0.year;
609                        s.view_month = value.0.month;
610                        s.view_day = value.0.day;
611                        cx.notify();
612                    });
613                },
614            );
615        }
616
617        // `controlled` takes `cx` mutably, so it precedes the theme tokens.
618        let (is_open, open_own) = crate::util::controlled(
619            window,
620            cx,
621            element_id::scoped(&base_id, "open"),
622            self.is_open,
623            self.default_open,
624        );
625        let open = is_open && !self.is_disabled;
626        let (start, end) = {
627            let st = self.state.read(cx);
628            (st.start, st.end)
629        };
630        let start_text = start.map(|date| date.format_iso()).unwrap_or_default();
631        let end_text = end.map(|date| date.format_iso()).unwrap_or_default();
632        let start_initial = start_text.clone();
633        let start_field_state = window
634            .use_keyed_state(
635                element_id::scoped(&base_id, "start-field"),
636                cx,
637                move |_, cx| cx.new(|cx| crate::input::InputState::with_value(cx, start_initial)),
638            )
639            .read(cx)
640            .clone();
641        let end_initial = end_text.clone();
642        let end_field_state = window
643            .use_keyed_state(
644                element_id::scoped(&base_id, "end-field"),
645                cx,
646                move |_, cx| cx.new(|cx| crate::input::InputState::with_value(cx, end_initial)),
647            )
648            .read(cx)
649            .clone();
650        *self.start_field_state.borrow_mut() = Some(start_field_state.clone());
651        *self.end_field_state.borrow_mut() = Some(end_field_state.clone());
652        let start_sync = window.use_keyed_state(element_id::scoped(&base_id, "start-sync"), cx, {
653            let start_text = start_text.clone();
654            move |_, _| start_text
655        });
656        let end_sync = window.use_keyed_state(element_id::scoped(&base_id, "end-sync"), cx, {
657            let end_text = end_text.clone();
658            move |_, _| end_text
659        });
660        let start_value = start_field_state.read(cx).value().to_owned();
661        let end_value = end_field_state.read(cx).value().to_owned();
662        let last_start = start_sync.read(cx).clone();
663        let last_end = end_sync.read(cx).clone();
664        let start_follows_selection = start_value == last_start && start_value != start_text;
665        let end_follows_selection = end_value == last_end && end_value != end_text;
666        if start_follows_selection {
667            start_field_state.update(cx, |state, _| state.set_value(start_text.clone()));
668        }
669        if end_follows_selection {
670            end_field_state.update(cx, |state, _| state.set_value(end_text.clone()));
671        }
672        let live_start_value = if start_follows_selection {
673            start_text.clone()
674        } else {
675            start_value
676        };
677        let live_end_value = if end_follows_selection {
678            end_text.clone()
679        } else {
680            end_value
681        };
682        if live_start_value == start_text {
683            start_sync.update(cx, |value, _| *value = start_text.clone());
684        }
685        if live_end_value == end_text {
686            end_sync.update(cx, |value, _| *value = end_text.clone());
687        }
688        let parse_live_value = |text: &str| {
689            if text.trim().is_empty() {
690                (None, None)
691            } else {
692                let (parsed, _) = parse_value(text);
693                let error = match parsed {
694                    None => Some(SharedString::from("Enter a valid date.")),
695                    Some(date) if !self.constraints.allows(date) => {
696                        Some(SharedString::from("That date is unavailable."))
697                    }
698                    Some(_) => None,
699                };
700                (parsed, error)
701            }
702        };
703        let (parsed_start, start_constraint_error) = parse_live_value(&live_start_value);
704        let (parsed_end, end_constraint_error) = parse_live_value(&live_end_value);
705        let range = parsed_start.zip(parsed_end);
706        let custom_error = self.validate.as_ref().and_then(|validate| validate(&range));
707        let start_invalid = crate::validation::resolve(
708            self.is_invalid,
709            &self.validation_errors,
710            custom_error.clone(),
711            start_constraint_error,
712        )
713        .is_invalid;
714        let end_invalid = crate::validation::resolve(
715            self.is_invalid,
716            &self.validation_errors,
717            custom_error,
718            end_constraint_error,
719        )
720        .is_invalid;
721        {
722            let mut state = self.start_form_state.borrow_mut();
723            state.value = crate::form::FormValue::Text(live_start_value.into());
724            state.is_successful = !self.is_disabled;
725            state.is_invalid = start_invalid;
726        }
727        {
728            let mut state = self.end_form_state.borrow_mut();
729            state.value = crate::form::FormValue::Text(live_end_value.into());
730            state.is_successful = !self.is_disabled;
731            state.is_invalid = end_invalid;
732        }
733        let start_focus = start_field_state.read(cx).focus_handle(cx);
734        let end_focus = end_field_state.read(cx).focus_handle(cx);
735        {
736            let mut state = self.start_form_state.borrow_mut();
737            state.focus = Some(start_focus.clone());
738        }
739        {
740            let mut state = self.end_form_state.borrow_mut();
741            state.focus = Some(end_focus.clone());
742        }
743        if let Some(render) = self.content.clone() {
744            let content_scope = window
745                .use_keyed_state(
746                    element_id::scoped(&base_id, "content-focus"),
747                    cx,
748                    |_, cx| cx.focus_handle().tab_stop(false),
749                )
750                .read(cx)
751                .clone();
752            let is_focus_within = content_scope.contains_focused(window, cx);
753            let content_root = gpui::div()
754                .relative()
755                .max_w(px(320.))
756                .track_focus(&content_scope)
757                .child(render(DateRangePickerRenderState {
758                    is_disabled: self.is_disabled,
759                    is_invalid: start_invalid || end_invalid,
760                    is_read_only: self.is_read_only,
761                    is_required: self.is_required,
762                    is_focus_within,
763                    is_focus_visible: is_focus_within && crate::util::focus_visible(cx),
764                    is_open: open,
765                }));
766            return crate::util::apply_sx(content_root, &self.sx).into_any_element();
767        }
768        let (overlay_phase, dismissal_token) = crate::util::overlay_scope(
769            window,
770            cx,
771            element_id::scoped(&base_id, "overlay"),
772            open,
773            // HeroUI keeps the popover alive for its 100ms fade/zoom exit.
774            // The calendar callback below is gated by `panel_open` so a
775            // retained cell cannot receive the same press again.
776            true,
777        );
778        let panel_visible = overlay_phase != crate::util::OverlayPhase::Closed;
779        let panel_open = overlay_phase == crate::util::OverlayPhase::Open;
780        // The positioner discovers a viewport flip during prepaint. Keep the
781        // requested and resolved sides in keyed state so the next render's
782        // entry animation follows the physical side that is actually painted.
783        let requested_placement = window.use_keyed_state(
784            element_id::scoped(&base_id, "requested-placement"),
785            cx,
786            |_, _| self.placement,
787        );
788        let resolved_placement = window.use_keyed_state(
789            element_id::scoped(&base_id, "resolved-placement"),
790            cx,
791            |_, _| Rc::new(Cell::new(None::<crate::popover::PopoverPlacement>)),
792        );
793        if *requested_placement.read(cx) != self.placement {
794            requested_placement.update(cx, |placement, _| *placement = self.placement);
795            resolved_placement.read(cx).set(None);
796        }
797        let resolved_placement = resolved_placement.read(cx).clone();
798        let entry_placement = resolved_placement.get().unwrap_or(self.placement);
799        let blur_open_own = open_own.clone();
800        let blur_open_change = self.on_open_change.clone();
801        let blur_scope =
802            crate::util::close_on_blur(window, cx, &base_id, open, move |window, cx| {
803                if let Some(held) = &blur_open_own {
804                    held.update(cx, |value, cx| {
805                        *value = false;
806                        cx.notify();
807                    });
808                }
809                if let Some(callback) = &blur_open_change {
810                    callback(&false, window, cx);
811                }
812            });
813        let initiator =
814            window.use_keyed_state(element_id::scoped(&base_id, "initiator"), cx, |_, _| 0usize);
815        let trigger_focus =
816            crate::util::tab_stop_handle(element_id::scoped(&base_id, "trigger-focus"), window, cx);
817        let colors = cx.colors();
818        let trigger_pressed = Rc::new(Cell::new(false));
819
820        let open_from_start_own = open_own.clone();
821        let open_from_start_cb = self.on_open_change.clone();
822        let start_initiator = initiator.clone();
823        let open_from_start = crate::util::shared(move |window: &mut Window, cx: &mut App| {
824            start_initiator.update(cx, |part, _| *part = 0);
825            if !is_open {
826                if let Some(held) = &open_from_start_own {
827                    held.update(cx, |open, cx| {
828                        *open = true;
829                        cx.notify();
830                    });
831                }
832                if let Some(cb) = &open_from_start_cb {
833                    cb(&true, window, cx);
834                }
835            }
836        });
837        let open_from_end_own = open_own.clone();
838        let open_from_end_cb = self.on_open_change.clone();
839        let end_initiator = initiator.clone();
840        let open_from_end = crate::util::shared(move |window: &mut Window, cx: &mut App| {
841            end_initiator.update(cx, |part, _| *part = 1);
842            if !is_open {
843                if let Some(held) = &open_from_end_own {
844                    held.update(cx, |open, cx| {
845                        *open = true;
846                        cx.notify();
847                    });
848                }
849                if let Some(cb) = &open_from_end_cb {
850                    cb(&true, window, cx);
851                }
852            }
853        });
854
855        let start_state = self.state.clone();
856        let start_change = self.on_change.clone();
857        let start_open = open_from_start.clone();
858        let start_constraints = self.constraints.clone();
859        let start_forced_invalid = self.is_invalid;
860        let start_form_state = self.start_form_state.clone();
861        let start_other_form_state = self.end_form_state.clone();
862        let start_validate = self.validate.clone();
863        let start_validation_errors = self.validation_errors.clone();
864        let start_field = DateField::new(start_field_state)
865            .embedded(true)
866            .is_disabled(self.is_disabled)
867            .is_required(self.is_required)
868            .validation_behavior(self.validation_behavior)
869            .is_invalid(start_invalid)
870            .auto_focus(self.auto_focus)
871            .constraints(self.constraints.clone())
872            .is_read_only(self.is_read_only)
873            .report_invalid_changes()
874            .when_some(self.locale.clone(), |field, tag| field.locale(tag))
875            .on_picker_open(move |window, cx| start_open(window, cx))
876            .on_change(move |date, window, cx| {
877                let valid_date = date.filter(|date| start_constraints.allows(*date));
878                let end = start_state.read(cx).end;
879                let range = valid_date.zip(end);
880                let custom_error = start_validate
881                    .as_ref()
882                    .and_then(|validate| validate(&range));
883                let constraint_error = (date.is_some() && valid_date.is_none())
884                    .then(|| SharedString::from("That date is unavailable."));
885                start_form_state.borrow_mut().is_invalid = crate::validation::resolve(
886                    start_forced_invalid,
887                    &start_validation_errors,
888                    custom_error.clone(),
889                    constraint_error,
890                )
891                .is_invalid;
892                let end_constraint_error = end
893                    .is_some_and(|date| !start_constraints.allows(date))
894                    .then(|| SharedString::from("That date is unavailable."));
895                start_other_form_state.borrow_mut().is_invalid = crate::validation::resolve(
896                    start_forced_invalid,
897                    &start_validation_errors,
898                    custom_error,
899                    end_constraint_error,
900                )
901                .is_invalid;
902                start_state.update(cx, |state, cx| {
903                    if date.is_none() || valid_date.is_some() {
904                        state.start = valid_date;
905                    }
906                    cx.notify();
907                });
908                if date.is_none() || valid_date.is_some() {
909                    start_form_state.borrow_mut().value = crate::form::FormValue::Text(
910                        valid_date
911                            .map(|date| date.format_iso())
912                            .unwrap_or_default()
913                            .into(),
914                    );
915                }
916                if date.is_none() || valid_date.is_some() {
917                    if let Some(cb) = &start_change {
918                        cb(window, cx);
919                    }
920                }
921            });
922        let end_state = self.state.clone();
923        let end_change = self.on_change.clone();
924        let end_open = open_from_end.clone();
925        let end_constraints = self.constraints.clone();
926        let end_forced_invalid = self.is_invalid;
927        let end_form_state = self.end_form_state.clone();
928        let end_other_form_state = self.start_form_state.clone();
929        let end_validate = self.validate.clone();
930        let end_validation_errors = self.validation_errors.clone();
931        let end_field = DateField::new(end_field_state)
932            .embedded(true)
933            .is_disabled(self.is_disabled)
934            .is_required(self.is_required)
935            .validation_behavior(self.validation_behavior)
936            .is_invalid(end_invalid)
937            .constraints(self.constraints.clone())
938            .is_read_only(self.is_read_only)
939            .report_invalid_changes()
940            .when_some(self.locale.clone(), |field, tag| field.locale(tag))
941            .on_picker_open(move |window, cx| end_open(window, cx))
942            .on_change(move |date, window, cx| {
943                let valid_date = date.filter(|date| end_constraints.allows(*date));
944                let start = end_state.read(cx).start;
945                let range = start.zip(valid_date);
946                let custom_error = end_validate.as_ref().and_then(|validate| validate(&range));
947                let constraint_error = (date.is_some() && valid_date.is_none())
948                    .then(|| SharedString::from("That date is unavailable."));
949                end_form_state.borrow_mut().is_invalid = crate::validation::resolve(
950                    end_forced_invalid,
951                    &end_validation_errors,
952                    custom_error.clone(),
953                    constraint_error,
954                )
955                .is_invalid;
956                let start_constraint_error = start
957                    .is_some_and(|date| !end_constraints.allows(date))
958                    .then(|| SharedString::from("That date is unavailable."));
959                end_other_form_state.borrow_mut().is_invalid = crate::validation::resolve(
960                    end_forced_invalid,
961                    &end_validation_errors,
962                    custom_error,
963                    start_constraint_error,
964                )
965                .is_invalid;
966                end_state.update(cx, |state, cx| {
967                    if date.is_none() || valid_date.is_some() {
968                        state.end = valid_date;
969                    }
970                    cx.notify();
971                });
972                if date.is_none() || valid_date.is_some() {
973                    end_form_state.borrow_mut().value = crate::form::FormValue::Text(
974                        valid_date
975                            .map(|date| date.format_iso())
976                            .unwrap_or_default()
977                            .into(),
978                    );
979                }
980                if date.is_none() || valid_date.is_some() {
981                    if let Some(cb) = &end_change {
982                        cb(window, cx);
983                    }
984                }
985            });
986
987        let mut field = gpui::div()
988            .id(base_id.clone())
989            .flex()
990            .items_center()
991            .gap(px(4.))
992            .w_full()
993            .h(crate::util::FIELD_HEIGHT)
994            .px(px(12.))
995            .text_size(crate::util::FIELD_TEXT);
996
997        // The range wrapper clips nothing, so its focused ring -- and the
998        // focused-invalid danger ring, the same geometry in `danger` -- paints
999        // as an overlay child: crisp and concentric where the spread shadow was
1000        // blurred and stuck at the element's own radius.
1001        field = crate::util::apply_field_chrome_overlay(
1002            field,
1003            herogpui_core::FieldVariant::Primary,
1004            start_invalid || end_invalid,
1005            start_focus.is_focused(window)
1006                || end_focus.is_focused(window)
1007                || trigger_focus.is_focused(window),
1008            true,
1009            None,
1010            cx,
1011        );
1012
1013        if !self.is_disabled && !self.is_read_only {
1014            let hover_bg = self.trigger_hover_bg.unwrap_or(colors.field.hover());
1015            if !is_open {
1016                field = field.hover(move |s| s.bg(hover_bg));
1017            }
1018        }
1019
1020        let trigger_indicator = self.trigger_indicator.unwrap_or_else(|| {
1021            gpui::svg()
1022                .size(px(16.))
1023                .path(icons::CALENDAR)
1024                .text_color(colors.field.placeholder)
1025                .into_any_element()
1026        });
1027        let mut trigger = gpui::div()
1028            .id(element_id::scoped(&base_id, "trigger"))
1029            .flex()
1030            .items_center()
1031            .justify_center()
1032            .size(px(24.))
1033            .child(
1034                gpui::div()
1035                    // `.date-range-picker__trigger-indicator` is `size-4`.
1036                    .flex()
1037                    .items_center()
1038                    .justify_center()
1039                    .size(px(16.))
1040                    .child(trigger_indicator),
1041            );
1042        if !self.is_disabled && !self.is_read_only {
1043            let focus_on_press = trigger_focus.clone();
1044            let trigger_initiator = initiator.clone();
1045            let open_own_trigger = open_own.clone();
1046            let open_cb_trigger = self.on_open_change.clone();
1047            let trigger_pressed_for_capture = trigger_pressed.clone();
1048            let was_open = is_open;
1049            trigger = trigger
1050                .capture_any_mouse_down(move |_, _, cx| {
1051                    trigger_pressed_for_capture.set(true);
1052                    let pressed = trigger_pressed_for_capture.clone();
1053                    cx.defer(move |_| pressed.set(false));
1054                })
1055                .track_focus(&trigger_focus)
1056                .cursor(crate::util::interactive_cursor(cx))
1057                .on_mouse_down(gpui::MouseButton::Left, move |_, window, cx| {
1058                    window.focus(&focus_on_press, cx);
1059                    cx.stop_propagation();
1060                })
1061                .on_click(move |_, window, cx| {
1062                    trigger_initiator.update(cx, |part, _| *part = 2);
1063                    if let Some(held) = &open_own_trigger {
1064                        held.update(cx, |open, cx| {
1065                            *open = !was_open;
1066                            cx.notify();
1067                        });
1068                    }
1069                    if let Some(cb) = &open_cb_trigger {
1070                        cb(&!was_open, window, cx);
1071                    }
1072                });
1073        }
1074        trigger = trigger
1075            .a11y_named(
1076                a11y::Role::Button,
1077                &a11y::Name::labelled(crate::i18n::ui_string(crate::i18n::UiString::Calendar, cx)),
1078            )
1079            .a11y_expanded(is_open);
1080        let separator_color = colors.field.placeholder;
1081        if !self.is_disabled && !self.is_read_only {
1082            trigger = crate::util::record_focus_bounds(trigger, &trigger_focus, window, cx);
1083        }
1084
1085        field = field
1086            .a11y_named(a11y::Role::Group, &a11y::Name::maybe(self.label.clone()))
1087            .child(start_field)
1088            .child(
1089                // `.date-range-picker__range-separator` is `px-1` in
1090                // `--field-placeholder`.
1091                gpui::div().px(px(4.)).text_color(separator_color).child(
1092                    self.range_separator
1093                        .unwrap_or_else(|| gpui::div().child(" - ").into_any_element()),
1094                ),
1095            )
1096            .child(end_field)
1097            .child(trigger);
1098        let anchor_bounds = window
1099            .use_keyed_state(element_id::scoped(&base_id, "anchor-bounds"), cx, |_, _| {
1100                Rc::new(Cell::new(None::<gpui::Bounds<Pixels>>))
1101            })
1102            .read(cx)
1103            .clone();
1104        let field = crate::popover::PopoverTriggerMeasure::new(field, anchor_bounds.clone());
1105
1106        let mut root = gpui::div()
1107            .relative()
1108            .w_full()
1109            .max_w(px(320.))
1110            .flex()
1111            .flex_col()
1112            .gap(px(4.));
1113        if let Some(label) = &self.label {
1114            root = root.child(
1115                crate::field::Label::new(label.clone())
1116                    .is_required(self.is_required)
1117                    .is_invalid(start_invalid || end_invalid),
1118            );
1119        }
1120        root = root.child(field);
1121
1122        if panel_visible {
1123            // A calendar has its own intrinsic width, so the panel must be
1124            // content-sized; `placed_field_panel` would clamp it to the
1125            // trigger and the grid would spill outside the surface.
1126            // Escape on the root, the outside press on the panel: see
1127            // `DatePicker` above.
1128            let close_own = open_own;
1129            let close_cb = self.on_open_change.clone();
1130            let restore_start = start_focus;
1131            let restore_end = end_focus;
1132            let restore_trigger = trigger_focus;
1133            let restore_part = initiator;
1134            let close = crate::util::shared(move |window: &mut Window, cx: &mut App| {
1135                if let Some(held) = &close_own {
1136                    held.update(cx, |v, cx| {
1137                        *v = false;
1138                        cx.notify();
1139                    });
1140                }
1141                if let Some(cb) = &close_cb {
1142                    cb(&false, window, cx);
1143                }
1144                match *restore_part.read(cx) {
1145                    1 => window.focus(&restore_end, cx),
1146                    2 => window.focus(&restore_trigger, cx),
1147                    _ => window.focus(&restore_start, cx),
1148                }
1149            });
1150
1151            // Driving RangeCalendar keeps the hover preview, the constraints
1152            // and the year picker in one place instead of a second grid. The
1153            // picker closes only once the range is complete: the first pick
1154            // leaves the panel open to choose the end, exactly as React Aria
1155            // does. RangeCalendar reports only that completed range.
1156            let pick_close = close.clone();
1157            let user_change = self.on_change.clone();
1158            let start_form_state = self.start_form_state.clone();
1159            let end_form_state = self.end_form_state.clone();
1160            let calendar_forced_invalid = self.is_invalid;
1161            let calendar_validate = self.validate.clone();
1162            let calendar_validation_errors = self.validation_errors.clone();
1163            let should_close_on_select = self.should_close_on_select;
1164            let panel_open_for_change = panel_open;
1165            let range_state = self.state.clone();
1166            let mut calendar = crate::range_calendar::RangeCalendar::new(self.state.clone())
1167                .constraints(self.constraints.clone())
1168                .when_some(self.locale.clone(), |cal, tag| cal.locale(tag))
1169                .autofocus_grid(panel_open)
1170                .inert(!panel_open)
1171                .is_read_only(self.is_read_only)
1172                .is_invalid(start_invalid || end_invalid);
1173            calendar = calendar.on_change(move |_start, _end, window, cx| {
1174                if !panel_open_for_change {
1175                    return;
1176                }
1177                let state = range_state.read(cx);
1178                let range = state.start.zip(state.end);
1179                let custom_error = calendar_validate
1180                    .as_ref()
1181                    .and_then(|validate| validate(&range));
1182                let mut start_form_state = start_form_state.borrow_mut();
1183                start_form_state.value = crate::form::FormValue::Text(
1184                    state
1185                        .start
1186                        .map(|date| date.format_iso())
1187                        .unwrap_or_default()
1188                        .into(),
1189                );
1190                start_form_state.is_invalid = crate::validation::resolve(
1191                    calendar_forced_invalid,
1192                    &calendar_validation_errors,
1193                    custom_error.clone(),
1194                    None,
1195                )
1196                .is_invalid;
1197                drop(start_form_state);
1198                let mut end_form_state = end_form_state.borrow_mut();
1199                end_form_state.value = crate::form::FormValue::Text(
1200                    state
1201                        .end
1202                        .map(|date| date.format_iso())
1203                        .unwrap_or_default()
1204                        .into(),
1205                );
1206                end_form_state.is_invalid = crate::validation::resolve(
1207                    calendar_forced_invalid,
1208                    &calendar_validation_errors,
1209                    custom_error,
1210                    None,
1211                )
1212                .is_invalid;
1213                if let Some(cb) = &user_change {
1214                    cb(window, cx);
1215                }
1216                if should_close_on_select {
1217                    pick_close(window, cx);
1218                }
1219            });
1220            let esc = close.clone();
1221            root = crate::util::dismiss_on_escape_with_token(
1222                root,
1223                dismissal_token.clone(),
1224                move |window, cx| {
1225                    esc(window, cx);
1226                    crate::util::DismissResult::Handled
1227                },
1228            );
1229            let outside_close = close.clone();
1230            let panel_open_for_dismissal = panel_open;
1231            let panel = crate::util::dismiss_on_press_outside_with_token(
1232                picker_panel(
1233                    cx,
1234                    "date-range-picker-popover",
1235                    element_id::scoped(&base_id, "popover"),
1236                )
1237                .child(calendar),
1238                dismissal_token,
1239                move |window, cx| {
1240                    if !panel_open_for_dismissal {
1241                        return crate::util::DismissResult::Declined;
1242                    }
1243                    if trigger_pressed.get() {
1244                        return crate::util::DismissResult::Declined;
1245                    }
1246                    outside_close(window, cx);
1247                    crate::util::DismissResult::Handled
1248                },
1249            );
1250            let (slide_x, slide_y) = placement_entry_offset(entry_placement);
1251            let zoom = crate::anim::ZoomBox::panel(px(12.), px(20.)).padding_x(px(12.));
1252            let zoom = crate::anim::ZoomBox {
1253                slide_x: (slide_x != 0.0).then(|| px(slide_x)),
1254                slide_y: (slide_y != 0.0).then(|| px(slide_y)),
1255                ..zoom
1256            };
1257            let panel = if overlay_phase == crate::util::OverlayPhase::Exiting {
1258                crate::anim::exiting(
1259                    panel,
1260                    element_id::scoped(&base_id, "panel-out"),
1261                    zoom,
1262                    crate::anim::Motion::LIST_OUT,
1263                    cx,
1264                )
1265            } else {
1266                crate::anim::entering_zoom(
1267                    panel,
1268                    element_id::scoped(&base_id, "panel"),
1269                    zoom,
1270                    crate::anim::Motion::LIST_IN,
1271                    cx,
1272                )
1273            };
1274            root = root.child(crate::util::floating(
1275                crate::popover::popover_with_resolved_placement(
1276                    anchor_bounds,
1277                    self.placement,
1278                    px(PICKER_POPOVER_OFFSET),
1279                    Some(resolved_placement),
1280                    panel,
1281                ),
1282            ));
1283        }
1284
1285        if self.is_disabled {
1286            root = root.opacity(cx.layout().disabled_opacity);
1287        }
1288
1289        crate::util::apply_sx(root.track_focus(&blur_scope), &self.sx).into_any_element()
1290    }
1291}
1292
1293// ---------------------------------------------------------------------------
1294
1295crate::util::impl_component_styled!(DateRangePicker);