1use super::*;
4
5pub struct DateRangeState {
10 pub view_year: i32,
12 pub view_month: u32,
14 pub view_day: u32,
16 pub start: Option<Date>,
18 pub end: Option<Date>,
20 pub hovered: Option<Date>,
22 pub user_navigated: bool,
25}
26
27impl DateRangeState {
28 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 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 pub fn anchor(&self) -> Date {
52 Date::new(self.view_year, self.view_month, self.view_day.max(1))
53 }
54
55 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 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 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
96#[non_exhaustive]
97pub struct DateRangePickerRenderState {
98 pub is_disabled: bool,
100 pub is_invalid: bool,
102 pub is_read_only: bool,
104 pub is_required: bool,
106 pub is_focus_within: bool,
108 pub is_focus_visible: bool,
110 pub is_open: bool,
112}
113
114#[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 locale: Option<SharedString>,
121 content:
122 Option<std::sync::Arc<dyn Fn(DateRangePickerRenderState) -> gpui::AnyElement + 'static>>,
123 start_name: Option<SharedString>,
126 end_name: Option<SharedString>,
127 default_value: Option<(Date, Date)>,
129 value: Option<(Option<Date>, Option<Date>)>,
133 state: Entity<DateRangeState>,
134 is_open: Option<bool>,
137 default_open: bool,
138 should_close_on_select: bool,
139 placement: herogpui_core::Placement,
141 label: Option<SharedString>,
142 trigger_indicator: Option<gpui::AnyElement>,
143 range_separator: Option<gpui::AnyElement>,
144 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 sx: Option<Box<gpui::StyleRefinement>>,
167}
168
169impl DateRangePicker {
170 pub fn locale(mut self, tag: impl Into<SharedString>) -> Self {
175 self.locale = Some(tag.into());
176 self
177 }
178
179 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 #[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 pub fn start_name(mut self, name: impl Into<SharedString>) -> Self {
296 self.start_name = Some(name.into());
297 self
298 }
299
300 pub fn end_name(mut self, name: impl Into<SharedString>) -> Self {
302 self.end_name = Some(name.into());
303 self
304 }
305
306 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 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 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
383 self.label = Some(label.into());
384 self
385 }
386
387 pub fn is_disabled(mut self, v: bool) -> Self {
389 self.is_disabled = v;
390 self
391 }
392
393 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 pub fn placement(mut self, placement: herogpui_core::Placement) -> Self {
402 self.placement = placement;
403 self
404 }
405
406 pub fn is_read_only(mut self, v: bool) -> Self {
408 self.is_read_only = v;
409 self
410 }
411
412 pub fn is_required(mut self, v: bool) -> Self {
414 self.is_required = v;
415 self
416 }
417
418 pub fn validation_behavior(mut self, behavior: crate::form::ValidationBehavior) -> Self {
420 self.validation_behavior = behavior;
421 self
422 }
423
424 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 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 pub fn is_invalid(mut self, v: bool) -> Self {
444 self.is_invalid = v;
445 self
446 }
447
448 pub fn auto_focus(mut self, v: bool) -> Self {
450 self.auto_focus = v;
451 self
452 }
453
454 pub fn value(mut self, start: Option<Date>, end: Option<Date>) -> Self {
464 self.value = Some((start, end));
465 self
466 }
467
468 pub fn min_value(mut self, date: Date) -> Self {
470 self.constraints.min_value = Some(date);
471 self
472 }
473
474 pub fn max_value(mut self, date: Date) -> Self {
476 self.constraints.max_value = Some(date);
477 self
478 }
479
480 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 pub fn first_day_of_week(mut self, day: Weekday) -> Self {
488 self.constraints.first_day_of_week = day;
489 self
490 }
491
492 pub fn constraints(mut self, constraints: DateConstraints) -> Self {
494 self.constraints = constraints;
495 self
496 }
497
498 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 pub fn is_open(mut self, v: bool) -> Self {
506 self.is_open = Some(v);
507 self
508 }
509 pub fn default_open(mut self, v: bool) -> Self {
514 self.default_open = v;
515 self
516 }
517
518 pub fn should_close_on_select(mut self, v: bool) -> Self {
520 self.should_close_on_select = v;
521 self
522 }
523
524 pub fn trigger_indicator(mut self, indicator: impl IntoElement) -> Self {
526 self.trigger_indicator = Some(indicator.into_any_element());
527 self
528 }
529
530 pub fn range_separator(mut self, separator: impl IntoElement) -> Self {
532 self.range_separator = Some(separator.into_any_element());
533 self
534 }
535
536 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 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 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 let base_id = gpui::ElementId::named_usize("drp", self.state.entity_id().as_u64() as usize);
569 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 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 true,
777 );
778 let panel_visible = overlay_phase != crate::util::OverlayPhase::Closed;
779 let panel_open = overlay_phase == crate::util::OverlayPhase::Open;
780 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 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 .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 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 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 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
1293crate::util::impl_component_styled!(DateRangePicker);