1use super::*;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum DateSegment {
11 Month,
13 Day,
15 Year,
17}
18
19impl DateSegment {
20 pub const ALL: [DateSegment; 3] = [DateSegment::Month, DateSegment::Day, DateSegment::Year];
22
23 pub fn label(self) -> &'static str {
25 match self {
26 DateSegment::Month => "month",
27 DateSegment::Day => "day",
28 DateSegment::Year => "year",
29 }
30 }
31
32 pub(crate) fn a11y_label(self, cx: &App) -> SharedString {
35 use crate::i18n::{ui_string, UiString};
36 ui_string(
37 match self {
38 DateSegment::Month => UiString::Month,
39 DateSegment::Day => UiString::Day,
40 DateSegment::Year => UiString::Year,
41 },
42 cx,
43 )
44 }
45
46 pub(super) fn hint(self) -> &'static str {
48 match self {
49 DateSegment::Month => "mm",
50 DateSegment::Day => "dd",
51 DateSegment::Year => "yyyy",
52 }
53 }
54
55 pub(super) fn digits(self) -> usize {
57 match self {
58 DateSegment::Year => 4,
59 _ => 2,
60 }
61 }
62
63 pub(super) fn page_step(self) -> i32 {
64 match self {
65 DateSegment::Year => 5,
66 DateSegment::Month => 2,
67 DateSegment::Day => 7,
68 }
69 }
70
71 pub(super) fn with_value(self, date: Date, value: u32) -> Date {
74 match self {
75 DateSegment::Year => {
76 let year = value as i32;
77 let day = date
78 .day
79 .min(crate::calendar::days_in_month(year, date.month));
80 Date::new(year, date.month, day)
81 }
82 DateSegment::Month => {
83 let month = value.clamp(1, 12);
84 let day = date
85 .day
86 .min(crate::calendar::days_in_month(date.year, month));
87 Date::new(date.year, month, day)
88 }
89 DateSegment::Day => Date::new(
90 date.year,
91 date.month,
92 value.clamp(1, crate::calendar::days_in_month(date.year, date.month)),
93 ),
94 }
95 }
96
97 pub(super) fn bump(self, date: Date, delta: i32) -> Date {
100 match self {
101 DateSegment::Year => {
102 let year = cycle_value(date.year, delta, 1, 9999);
103 let day = date
104 .day
105 .min(crate::calendar::days_in_month(year, date.month));
106 Date::new(year, date.month, day)
107 }
108 DateSegment::Month => {
109 let month = cycle_value(date.month as i32, delta, 1, 12) as u32;
110 let day = date
111 .day
112 .min(crate::calendar::days_in_month(date.year, month));
113 Date::new(date.year, month, day)
114 }
115 DateSegment::Day => {
116 let days = crate::calendar::days_in_month(date.year, date.month);
117 let day = cycle_value(date.day as i32, delta, 1, days as i32) as u32;
118 Date::new(date.year, date.month, day)
119 }
120 }
121 }
122
123 pub(super) fn bound(self, date: Date, maximum: bool) -> Date {
124 match (self, maximum) {
125 (DateSegment::Year, false) => {
126 let day = date.day.min(crate::calendar::days_in_month(1, date.month));
127 Date::new(1, date.month, day)
128 }
129 (DateSegment::Year, true) => {
130 let day = date
131 .day
132 .min(crate::calendar::days_in_month(9999, date.month));
133 Date::new(9999, date.month, day)
134 }
135 (DateSegment::Month, false) => {
136 let day = date.day.min(crate::calendar::days_in_month(date.year, 1));
137 Date::new(date.year, 1, day)
138 }
139 (DateSegment::Month, true) => {
140 let day = date.day.min(crate::calendar::days_in_month(date.year, 12));
141 Date::new(date.year, 12, day)
142 }
143 (DateSegment::Day, false) => Date::new(date.year, date.month, 1),
144 (DateSegment::Day, true) => Date::new(
145 date.year,
146 date.month,
147 crate::calendar::days_in_month(date.year, date.month),
148 ),
149 }
150 }
151}
152
153#[derive(Clone, Debug, PartialEq, Eq)]
154pub(super) struct RegionalDateFormat {
155 order: [DateSegment; 3],
156 literals: [String; 4],
157 month_has_leading_zero: bool,
158 day_has_leading_zero: bool,
159}
160
161impl RegionalDateFormat {
162 pub(super) fn for_locale(locale: &str) -> Option<Self> {
163 use icu_datetime::{
164 fieldsets,
165 input::Date as IcuDate,
166 options::YearStyle,
167 provider::{
168 fields::{FieldLength, FieldSymbol},
169 pattern::{reference, runtime, PatternItem},
170 },
171 DateTimeFormatter,
172 };
173 use icu_locale_core::Locale as IcuLocale;
174
175 let locale = locale.parse::<IcuLocale>().ok()?;
176 let formatter = DateTimeFormatter::try_new(
177 locale.into(),
178 fieldsets::YMD::short().with_year_style(YearStyle::Full),
179 )
180 .ok()?;
181 let formatted = formatter.format(&IcuDate::try_new_iso(2000, 1, 1).ok()?);
182 let pattern: runtime::Pattern<'_> = formatted.pattern().into();
183 let mut order = Vec::with_capacity(3);
184 let mut literals = Vec::with_capacity(4);
185 let mut literal = String::new();
186 let mut month_has_leading_zero = None;
187 let mut day_has_leading_zero = None;
188 for item in reference::Pattern::from(&pattern).into_items() {
189 let field = match item {
190 PatternItem::Literal(ch) => {
191 literal.push(ch);
192 continue;
193 }
194 PatternItem::Field(field) => field,
195 };
196 let segment = match field.symbol {
197 FieldSymbol::Month(_) => {
198 month_has_leading_zero = Some(field.length == FieldLength::Two);
199 DateSegment::Month
200 }
201 FieldSymbol::Day(_) => {
202 day_has_leading_zero = Some(field.length == FieldLength::Two);
203 DateSegment::Day
204 }
205 FieldSymbol::Year(_) => DateSegment::Year,
206 _ => return None,
207 };
208 if order.contains(&segment) {
209 return None;
210 }
211 literals.push(std::mem::take(&mut literal));
212 order.push(segment);
213 }
214 literals.push(literal);
215 Some(Self {
216 order: order.try_into().ok()?,
217 literals: literals.try_into().ok()?,
218 month_has_leading_zero: month_has_leading_zero?,
219 day_has_leading_zero: day_has_leading_zero?,
220 })
221 }
222
223 pub(super) fn for_preferences(tags: &[String]) -> Option<Self> {
224 tags.iter().find_map(|tag| Self::for_locale(tag))
225 }
226
227 pub(super) fn date_hint(&self) -> String {
228 let mut hint = self.literals[0].clone();
229 for (index, segment) in self.order.iter().enumerate() {
230 hint.push_str(match segment {
231 DateSegment::Month => "MM",
232 DateSegment::Day => "DD",
233 DateSegment::Year => "YYYY",
234 });
235 hint.push_str(&self.literals[index + 1]);
236 }
237 hint
238 }
239}
240
241pub(super) fn system_date_format() -> &'static RegionalDateFormat {
242 static SYSTEM_DATE_FORMAT: OnceLock<RegionalDateFormat> = OnceLock::new();
243 SYSTEM_DATE_FORMAT.get_or_init(|| {
244 RegionalDateFormat::for_preferences(&crate::date_constraints::system_locale_tags())
245 .unwrap_or(RegionalDateFormat {
246 order: DateSegment::ALL,
247 literals: [String::new(), "/".to_owned(), "/".to_owned(), String::new()],
248 month_has_leading_zero: false,
249 day_has_leading_zero: false,
250 })
251 })
252}
253
254pub(super) fn cycle_value(value: i32, delta: i32, min: i32, max: i32) -> i32 {
255 (value - min + delta).rem_euclid(max - min + 1) + min
256}
257
258#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
264pub enum Granularity {
265 #[default]
267 Day,
268 Hour,
270 Minute,
272 Second,
274}
275
276impl Granularity {
277 pub const ALL: [Granularity; 4] = [
279 Granularity::Day,
280 Granularity::Hour,
281 Granularity::Minute,
282 Granularity::Second,
283 ];
284
285 pub fn label(self) -> &'static str {
287 match self {
288 Granularity::Day => "Day",
289 Granularity::Hour => "Hour",
290 Granularity::Minute => "Minute",
291 Granularity::Second => "Second",
292 }
293 }
294
295 pub(super) fn time(self) -> Option<crate::time_field::TimeGranularity> {
297 use crate::time_field::TimeGranularity as T;
298 match self {
299 Granularity::Day => None,
300 Granularity::Hour => Some(T::Hour),
301 Granularity::Minute => Some(T::Minute),
302 Granularity::Second => Some(T::Second),
303 }
304 }
305}
306
307#[derive(Clone, Copy, Debug, PartialEq, Eq)]
310pub enum FieldSegment {
311 Date(DateSegment),
313 Time(crate::time_field::TimeSegment),
315}
316
317#[derive(Clone)]
318pub(super) struct DateFieldDisplay {
319 date: Option<Date>,
320 time: Option<crate::time_field::Time>,
321 cleared: Vec<FieldSegment>,
322 committed: String,
323}
324
325impl DateFieldDisplay {
326 pub(super) fn new(committed: &str, segments: &[FieldSegment]) -> Self {
327 let (date, time) = parse_value(committed);
328 let mut this = Self {
329 date,
330 time,
331 cleared: Vec::new(),
332 committed: committed.to_owned(),
333 };
334 this.sync_visible_segments(segments);
335 this
336 }
337
338 pub(super) fn sync(&mut self, committed: &str, segments: &[FieldSegment]) {
339 if self.committed != committed {
340 let (date, time) = parse_value(committed);
341 self.date = date;
342 self.time = time;
343 self.cleared.clear();
344 self.committed = committed.to_owned();
345 }
346 self.sync_visible_segments(segments);
347 }
348
349 pub(super) fn sync_visible_segments(&mut self, segments: &[FieldSegment]) {
350 self.cleared.retain(|segment| segments.contains(segment));
351 for segment in segments {
352 let missing = match segment {
353 FieldSegment::Date(_) => self.date.is_none(),
354 FieldSegment::Time(_) => self.time.is_none(),
355 };
356 if missing && !self.cleared.contains(segment) {
357 self.cleared.push(*segment);
358 }
359 }
360 }
361
362 pub(super) fn edit(
363 &mut self,
364 focused: FieldSegment,
365 date: Date,
366 time: Option<crate::time_field::Time>,
367 segments: &[FieldSegment],
368 granularity: Granularity,
369 ) -> Option<(Date, Option<crate::time_field::Time>, String)> {
370 self.date = Some(date);
371 self.time = time;
372 self.cleared.retain(|segment| *segment != focused);
373 if segments
374 .iter()
375 .any(|segment| self.cleared.contains(segment))
376 {
377 return None;
378 }
379
380 let committed = format_value(date, time, granularity);
381 self.committed.clone_from(&committed);
382 Some((date, time, committed))
383 }
384
385 pub(super) fn clear(&mut self, focused: FieldSegment, segments: &[FieldSegment]) -> bool {
386 if !self.cleared.contains(&focused) {
387 self.cleared.push(focused);
388 }
389 if !segments
390 .iter()
391 .all(|segment| self.cleared.contains(segment))
392 {
393 return false;
394 }
395
396 self.date = None;
397 self.time = None;
398 self.committed.clear();
399 true
400 }
401}
402
403pub(super) type FieldSegmentRender =
404 std::sync::Arc<dyn Fn(FieldSegment, SharedString) -> gpui::AnyElement + 'static>;
405
406#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
408#[non_exhaustive]
409pub struct DateFieldRenderState {
410 pub is_disabled: bool,
412 pub is_invalid: bool,
414 pub is_read_only: bool,
416 pub is_required: bool,
418 pub is_focused: bool,
420 pub is_focus_within: bool,
422 pub is_focus_visible: bool,
424}
425
426#[must_use = "a component does nothing until it is rendered: add it as a child or return it from `render`"]
430#[derive(IntoElement)]
431pub struct DateField {
432 content: Option<std::sync::Arc<dyn Fn(DateFieldRenderState) -> gpui::AnyElement + 'static>>,
434 segment: Option<FieldSegmentRender>,
437 validation_behavior: Option<crate::form::ValidationBehavior>,
439 default_value: Option<Date>,
441 value: Option<Option<Date>>,
445 full_width: bool,
446 is_required: bool,
447 validate: Option<crate::validation::Validator<Option<Date>>>,
449 validation_errors: Vec<SharedString>,
451 is_invalid: bool,
452 variant: herogpui_core::FieldVariant,
453 constraints: DateConstraints,
454 placeholder_value: Option<Date>,
455 granularity: Granularity,
458 hour_cycle: crate::time_field::HourCycle,
460 prefix: Option<gpui::AnyElement>,
463 suffix: Option<gpui::AnyElement>,
465 state: Entity<crate::input::InputState>,
466 label: Option<SharedString>,
467 description: Option<SharedString>,
469 name: Option<SharedString>,
471 auto_focus: bool,
473 should_force_leading_zeros: bool,
476 is_disabled: bool,
477 is_read_only: bool,
478 on_change: Option<OnChange>,
479 embedded: bool,
480 bare: bool,
481 is_bare: bool,
483 focus_ring: bool,
485 font_family: Option<SharedString>,
487 radius: Option<Pixels>,
489 height: Option<Pixels>,
491 padding_x: Option<Pixels>,
493 on_picker_open: Option<std::sync::Arc<dyn Fn(&mut Window, &mut App) + 'static>>,
494 report_invalid_changes: bool,
495 locale: Option<SharedString>,
498 sx: Option<Box<gpui::StyleRefinement>>,
500}
501
502impl DateField {
503 pub fn full_width(mut self, v: bool) -> Self {
505 self.full_width = v;
506 self
507 }
508
509 pub fn description(mut self, text: impl Into<SharedString>) -> Self {
512 self.description = Some(text.into());
513 self
514 }
515
516 pub fn name(mut self, name: impl Into<SharedString>) -> Self {
518 self.name = Some(name.into());
519 self
520 }
521
522 pub fn form_field(&self) -> Option<crate::form::FormField> {
524 let name = self.name.clone()?;
525 let form_state = date_field_form_state(self.state.entity_id().as_u64());
526 form_state.borrow_mut().is_successful = !self.is_disabled;
527 if let Some(default) = self.default_value {
528 install_date_field_restore(
529 &form_state,
530 self.state.clone(),
531 default.format_iso().into(),
532 );
533 }
534 let mut field =
535 crate::form::FormField::live(name, form_state).is_required(self.is_required);
536 if let Some(behavior) = self.validation_behavior {
537 field = field.validation_behavior(behavior);
538 }
539 Some(field)
540 }
541
542 pub fn auto_focus(mut self, v: bool) -> Self {
544 self.auto_focus = v;
545 self
546 }
547
548 pub fn should_force_leading_zeros(mut self, v: bool) -> Self {
552 self.should_force_leading_zeros = v;
553 self
554 }
555
556 pub fn is_disabled(mut self, v: bool) -> Self {
558 self.is_disabled = v;
559 self
560 }
561
562 pub fn is_read_only(mut self, v: bool) -> Self {
564 self.is_read_only = v;
565 self
566 }
567
568 pub fn is_required(mut self, v: bool) -> Self {
570 self.is_required = v;
571 self
572 }
573
574 pub fn placeholder_value(mut self, date: Date) -> Self {
576 self.placeholder_value = Some(date);
577 self
578 }
579
580 pub fn granularity(mut self, granularity: Granularity) -> Self {
586 self.granularity = granularity;
587 self
588 }
589
590 pub fn hour_cycle(mut self, cycle: crate::time_field::HourCycle) -> Self {
593 self.hour_cycle = cycle;
594 self
595 }
596
597 pub fn prefix(mut self, el: impl IntoElement) -> Self {
599 self.prefix = Some(el.into_any_element());
600 self
601 }
602
603 pub fn suffix(mut self, el: impl IntoElement) -> Self {
605 self.suffix = Some(el.into_any_element());
606 self
607 }
608
609 pub fn validate(mut self, f: impl Fn(&Option<Date>) -> Option<SharedString> + 'static) -> Self {
613 self.validate = Some(std::sync::Arc::new(f));
614 self
615 }
616
617 pub fn validation_errors(
620 mut self,
621 errors: impl IntoIterator<Item = impl Into<SharedString>>,
622 ) -> Self {
623 self.validation_errors = errors.into_iter().map(Into::into).collect();
624 self
625 }
626
627 pub fn is_invalid(mut self, v: bool) -> Self {
629 self.is_invalid = v;
630 self
631 }
632
633 pub fn variant(mut self, variant: herogpui_core::FieldVariant) -> Self {
635 self.variant = variant;
636 self
637 }
638
639 pub fn height(mut self, h: impl Into<Pixels>) -> Self {
642 self.height = Some(h.into());
643 self
644 }
645
646 pub fn padding_x(mut self, p: impl Into<Pixels>) -> Self {
648 self.padding_x = Some(p.into());
649 self
650 }
651
652 pub fn is_bare(mut self, v: bool) -> Self {
656 self.is_bare = v;
657 self
658 }
659
660 pub fn focus_ring(mut self, v: bool) -> Self {
663 self.focus_ring = v;
664 self
665 }
666
667 pub fn font_family(mut self, family: impl Into<SharedString>) -> Self {
669 self.font_family = Some(family.into());
670 self
671 }
672
673 pub fn radius(mut self, radius: impl Into<Pixels>) -> Self {
680 self.radius = Some(radius.into());
681 self
682 }
683
684 pub fn value(mut self, date: Option<Date>) -> Self {
695 self.value = Some(date);
696 self
697 }
698
699 pub fn min_value(mut self, date: Date) -> Self {
701 self.constraints.min_value = Some(date);
702 self
703 }
704
705 pub fn max_value(mut self, date: Date) -> Self {
707 self.constraints.max_value = Some(date);
708 self
709 }
710
711 pub fn is_date_unavailable(mut self, f: impl Fn(Date) -> bool + 'static) -> Self {
713 self.constraints.is_date_unavailable = Some(std::sync::Arc::new(f));
714 self
715 }
716
717 pub fn constraints(mut self, constraints: DateConstraints) -> Self {
719 self.constraints = constraints;
720 self
721 }
722
723 pub fn content(
726 mut self,
727 render: impl Fn(DateFieldRenderState) -> gpui::AnyElement + 'static,
728 ) -> Self {
729 self.content = Some(std::sync::Arc::new(render));
730 self
731 }
732
733 pub fn new(state: Entity<crate::input::InputState>) -> Self {
735 Self {
736 content: None,
737 segment: None,
738 granularity: Granularity::Day,
739 hour_cycle: crate::time_field::HourCycle::default(),
740 validation_behavior: None,
741 default_value: None,
742 value: None,
743 full_width: false,
744 is_required: false,
745 validate: None,
746 validation_errors: Vec::new(),
747 is_invalid: false,
748 variant: herogpui_core::FieldVariant::Primary,
749 constraints: DateConstraints::new(),
750 placeholder_value: None,
751 prefix: None,
752 suffix: None,
753 state,
754 label: None,
755 description: None,
756 name: None,
757 auto_focus: false,
758 should_force_leading_zeros: false,
759 is_disabled: false,
760 is_read_only: false,
761 on_change: None,
762 embedded: false,
763 bare: false,
764 is_bare: false,
765 focus_ring: true,
766 font_family: None,
767 radius: None,
768 height: None,
769 padding_x: None,
770 on_picker_open: None,
771 report_invalid_changes: false,
772 locale: None,
773 sx: None,
774 }
775 }
776
777 pub fn locale(mut self, tag: impl Into<SharedString>) -> Self {
783 self.locale = Some(tag.into());
784 self
785 }
786
787 pub(super) fn embedded(mut self, bare: bool) -> Self {
788 self.embedded = true;
789 self.bare = bare;
790 self
791 }
792
793 pub(super) fn on_picker_open(mut self, f: impl Fn(&mut Window, &mut App) + 'static) -> Self {
794 self.on_picker_open = Some(std::sync::Arc::new(f));
795 self
796 }
797
798 pub(super) fn report_invalid_changes(mut self) -> Self {
799 self.report_invalid_changes = true;
800 self
801 }
802
803 pub fn segment(
809 mut self,
810 render: impl Fn(FieldSegment, SharedString) -> gpui::AnyElement + 'static,
811 ) -> Self {
812 self.segment = Some(std::sync::Arc::new(render));
813 self
814 }
815
816 pub fn validation_behavior(mut self, behavior: crate::form::ValidationBehavior) -> Self {
818 self.validation_behavior = Some(behavior);
819 self
820 }
821
822 pub fn default_value(mut self, value: Date) -> Self {
827 self.default_value = Some(value);
828 self
829 }
830
831 pub fn label(mut self, l: impl Into<SharedString>) -> Self {
833 self.label = Some(l.into());
834 self
835 }
836
837 pub fn on_change(mut self, f: impl Fn(&Option<Date>, &mut Window, &mut App) + 'static) -> Self {
839 self.on_change = Some(std::sync::Arc::new(f));
840 self
841 }
842
843 pub fn sx(mut self, style: impl FnOnce(gpui::Div) -> gpui::Div) -> Self {
848 crate::util::refine_sx(&mut self.sx, style);
849 self
850 }
851}
852
853pub(super) fn parse_iso(text: &str) -> Option<Date> {
854 let parts: Vec<&str> = text.trim().split('-').collect();
855 if parts.len() != 3 {
856 return None;
857 }
858 let y: i32 = parts[0].parse().ok()?;
859 let m: u32 = parts[1].parse().ok()?;
860 let d: u32 = parts[2].parse().ok()?;
861 if !(1..=12).contains(&m) || d == 0 || d > crate::calendar::days_in_month(y, m) {
862 return None;
863 }
864 Some(Date::new(y, m, d))
865}
866
867pub(super) fn format_hint(
870 regional_date: &RegionalDateFormat,
871 regional_time: Option<&crate::time_field::RegionalTimePattern>,
872) -> String {
873 let mut hint = regional_date.date_hint();
874 if let Some(regional_time) = regional_time {
875 hint.push_str(", ");
876 hint.push_str(®ional_time.hint());
877 }
878 hint
879}
880
881pub(super) fn parse_value(text: &str) -> (Option<Date>, Option<crate::time_field::Time>) {
884 let text = text.trim();
885 let (date, time) = match text.split_once(['T', ' ']) {
886 Some((d, t)) => (d, Some(t)),
887 None => (text, None),
888 };
889 (parse_iso(date), time.and_then(parse_time))
890}
891
892pub(super) fn parse_time(text: &str) -> Option<crate::time_field::Time> {
895 let mut parts = text.trim().split(':');
896 let hour: u32 = parts.next()?.parse().ok()?;
897 let minute: u32 = match parts.next() {
898 Some(m) => m.parse().ok()?,
899 None => 0,
900 };
901 let second: u32 = match parts.next() {
902 Some(sec) => sec.parse().ok()?,
903 None => 0,
904 };
905 if hour > 23 || minute > 59 || second > 59 {
906 return None;
907 }
908 Some(crate::time_field::Time::new(hour, minute).with_second(second))
909}
910
911pub(super) fn format_value(
914 date: Date,
915 time: Option<crate::time_field::Time>,
916 granularity: Granularity,
917) -> String {
918 let t = time.unwrap_or_default();
919 match granularity {
920 Granularity::Day => date.format_iso(),
921 Granularity::Hour => format!("{}T{:02}", date.format_iso(), t.hour),
922 Granularity::Minute => format!("{}T{:02}:{:02}", date.format_iso(), t.hour, t.minute),
923 Granularity::Second => format!(
924 "{}T{:02}:{:02}:{:02}",
925 date.format_iso(),
926 t.hour,
927 t.minute,
928 t.second
929 ),
930 }
931}
932
933impl RenderOnce for DateField {
934 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
935 let entity_id = self.state.entity_id().as_u64();
936 let base_id = gpui::ElementId::named_usize("datefield", entity_id as usize);
940 let segment_base_id = gpui::ElementId::named_usize("date", entity_id as usize);
941 let form_state = registered_date_field_form_state(entity_id);
942 if let Some(form_state) = form_state.as_ref() {
943 let mut state = form_state.borrow_mut();
944 state.value =
945 crate::form::FormValue::Text(self.state.read(cx).value().to_owned().into());
946 state.is_successful = !self.is_disabled;
947 state.focus = Some(self.state.read(cx).focus_handle.clone());
948 if let Some(default) = self.default_value {
949 drop(state);
950 install_date_field_restore(
951 form_state,
952 self.state.clone(),
953 default.format_iso().into(),
954 );
955 }
956 }
957 if let Some(behavior) = self.validation_behavior {
959 if self.state.read(cx).validation_behavior() != behavior {
960 self.state
961 .update(cx, |s, _| s.set_validation_behavior(behavior));
962 }
963 }
964 if let Some(date) = self.value {
969 let text = date.map(|d| d.format_iso()).unwrap_or_default();
970 let state = self.state.clone();
971 crate::util::seed_once(
972 window,
973 cx,
974 gpui::ElementId::named_usize(
975 "datefield-default",
976 self.state.entity_id().as_u64() as usize,
977 ),
978 move |cx| {
979 state.update(cx, |s, cx| {
980 s.set_value(text);
981 cx.notify();
982 });
983 },
984 );
985 } else if let Some(value) = self.default_value {
986 let state = self.state.clone();
987 crate::util::seed_once(
988 window,
989 cx,
990 gpui::ElementId::named_usize(
991 "datefield-default",
992 self.state.entity_id().as_u64() as usize,
993 ),
994 move |cx| {
995 state.update(cx, |s, cx| {
996 s.set_value(value.format_iso());
997 cx.notify();
998 });
999 },
1000 );
1001 }
1002
1003 let regional_owned = self
1004 .locale
1005 .as_deref()
1006 .and_then(RegionalDateFormat::for_locale);
1007 let regional_date = regional_owned
1008 .as_ref()
1009 .unwrap_or_else(|| system_date_format());
1010
1011 let first_date_segment = regional_date.order[0];
1014 let focused_seg =
1015 window.use_keyed_state(element_id::scoped(&base_id, "seg"), cx, move |_, _| {
1016 FieldSegment::Date(first_date_segment)
1017 });
1018 let mut focused = *focused_seg.read(cx);
1019 let typing = window.use_keyed_state(element_id::scoped(&base_id, "typing"), cx, |_, _| {
1022 String::new()
1023 });
1024
1025 let colors = cx.colors().clone();
1026 let navigable = !self.is_disabled;
1027
1028 let text = self.state.read(cx).value().to_owned();
1029 let (parsed, _) = parse_value(&text);
1030 let non_empty = !text.trim().is_empty();
1031
1032 let twelve_hour = self.hour_cycle == crate::time_field::HourCycle::H12;
1033 let regional_time = self.granularity.time().map(|granularity| {
1034 crate::time_field::regional_time_pattern(granularity, self.hour_cycle)
1035 });
1036 let mut segments: Vec<FieldSegment> = regional_date
1037 .order
1038 .iter()
1039 .copied()
1040 .map(FieldSegment::Date)
1041 .collect();
1042 if let Some(regional_time) = regional_time.as_ref() {
1043 segments.extend(regional_time.order.iter().copied().map(FieldSegment::Time));
1044 }
1045 if !segments.contains(&focused) {
1047 focused = segments[0];
1048 }
1049 let granularity = self.granularity;
1050 let display =
1051 window.use_keyed_state(element_id::scoped(&base_id, "display"), cx, |_, _| {
1052 DateFieldDisplay::new(&text, &segments)
1053 });
1054 display.update(cx, |display, _| display.sync(&text, &segments));
1055 let (display_date, display_time, cleared) = {
1056 let display = display.read(cx);
1057 (display.date, display.time, display.cleared.clone())
1058 };
1059
1060 let rejection = if !non_empty {
1063 None
1064 } else if parsed.is_none() {
1065 Some("Enter a valid date.".to_owned())
1066 } else {
1067 let date = parsed.expect("checked above");
1068 if self.constraints.out_of_range(date) {
1069 Some(
1070 match (self.constraints.min_value, self.constraints.max_value) {
1071 (Some(min), Some(max)) => format!(
1072 "Pick a date between {} and {}.",
1073 min.format_iso(),
1074 max.format_iso()
1075 ),
1076 (Some(min), None) => format!("Pick {} or later.", min.format_iso()),
1077 (None, Some(max)) => format!("Pick {} or earlier.", max.format_iso()),
1078 (None, None) => "Date is out of range.".to_owned(),
1079 },
1080 )
1081 } else if self.constraints.is_unavailable(date) {
1082 Some("That date is unavailable.".to_owned())
1083 } else {
1084 None
1085 }
1086 };
1087
1088 let validity = crate::validation::resolve(
1091 self.is_invalid,
1092 &self.validation_errors,
1093 self.validate.as_ref().and_then(|f| f(&parsed)),
1094 rejection.map(Into::into),
1095 );
1096 let is_invalid = validity.is_invalid;
1097 let validity_state = self.state.clone();
1098 if validity_state.read(cx).validity() != &validity {
1099 validity_state.update(cx, |state, _| state.set_validity(validity.clone()));
1100 }
1101 if let Some(form_state) = form_state.as_ref() {
1102 let mut state = form_state.borrow_mut();
1103 state.value = crate::form::FormValue::Text(text.clone().into());
1104 state.is_invalid = is_invalid;
1105 state.is_successful = !self.is_disabled;
1106 state.focus = Some(self.state.read(cx).focus_handle.clone());
1107 }
1108
1109 let focus_handle = self.state.read(cx).focus_handle.clone();
1110 if self.auto_focus {
1111 crate::util::focus_once(
1112 window,
1113 cx,
1114 element_id::scoped(&base_id, "autofocus"),
1115 &focus_handle,
1116 );
1117 }
1118 if let Some(render) = self.content.clone() {
1119 let focused = focus_handle.is_focused(window);
1120 return render(DateFieldRenderState {
1121 is_disabled: self.is_disabled,
1122 is_invalid,
1123 is_read_only: self.is_read_only,
1124 is_required: self.is_required,
1125 is_focused: focused,
1126 is_focus_within: focus_handle.contains_focused(window, cx),
1127 is_focus_visible: focused && crate::util::focus_visible(cx),
1128 })
1129 .into_any_element();
1130 }
1131
1132 let pad_month = self.should_force_leading_zeros || regional_date.month_has_leading_zero;
1133 let pad_day = self.should_force_leading_zeros || regional_date.day_has_leading_zero;
1134 let pad_hour = self.should_force_leading_zeros
1135 || regional_time
1136 .as_ref()
1137 .is_some_and(|format| format.hour_has_leading_zero);
1138 let pad_minute = regional_time
1139 .as_ref()
1140 .is_none_or(|format| format.minute_has_leading_zero);
1141 let pad_second = regional_time
1142 .as_ref()
1143 .is_none_or(|format| format.second_has_leading_zero);
1144 let zero_based_twelve_hour = regional_time
1145 .as_ref()
1146 .is_some_and(|format| format.hour_zero_based);
1147 let am = regional_time
1148 .as_ref()
1149 .map_or_else(|| "AM".to_owned(), |format| format.am.clone());
1150 let pm = regional_time
1151 .as_ref()
1152 .map_or_else(|| "PM".to_owned(), |format| format.pm.clone());
1153 let segment_text = move |segment: FieldSegment| -> String {
1154 use crate::time_field::TimeSegment as T;
1155 match segment {
1156 FieldSegment::Date(segment) => {
1157 if cleared.contains(&FieldSegment::Date(segment)) {
1158 return segment.hint().to_owned();
1159 }
1160 let Some(d) = display_date else {
1161 return segment.hint().to_owned();
1162 };
1163 match segment {
1164 DateSegment::Month if pad_month => format!("{:02}", d.month),
1165 DateSegment::Day if pad_day => format!("{:02}", d.day),
1166 DateSegment::Month => d.month.to_string(),
1167 DateSegment::Day => d.day.to_string(),
1168 DateSegment::Year => format!("{:04}", d.year),
1169 }
1170 }
1171 FieldSegment::Time(segment) => {
1172 if cleared.contains(&FieldSegment::Time(segment)) {
1173 return if segment == T::Meridiem {
1174 am.clone()
1175 } else {
1176 "--".to_owned()
1177 };
1178 }
1179 let Some(t) = display_time else {
1180 return "--".to_owned();
1181 };
1182 match segment {
1183 T::Hour if twelve_hour && pad_hour => {
1184 let hour = if zero_based_twelve_hour {
1185 t.hour % 12
1186 } else {
1187 t.twelve_hour().0
1188 };
1189 format!("{hour:02}")
1190 }
1191 T::Hour if twelve_hour => {
1192 if zero_based_twelve_hour {
1193 (t.hour % 12).to_string()
1194 } else {
1195 t.twelve_hour().0.to_string()
1196 }
1197 }
1198 T::Hour if pad_hour => format!("{:02}", t.hour),
1199 T::Hour => t.hour.to_string(),
1200 T::Minute if pad_minute => format!("{:02}", t.minute),
1201 T::Minute => t.minute.to_string(),
1202 T::Second if pad_second => format!("{:02}", t.second),
1203 T::Second => t.second.to_string(),
1204 T::Meridiem => {
1205 if t.hour < 12 {
1206 am.clone()
1207 } else {
1208 pm.clone()
1209 }
1210 }
1211 }
1212 }
1213 }
1214 };
1215
1216 let seed = self.placeholder_value.unwrap_or_else(Date::today);
1220 let radius = self.radius.unwrap_or_else(|| crate::util::field_radius(cx));
1222 let mut group = gpui::div()
1226 .id(base_id.clone())
1227 .a11y_named(
1228 a11y::Role::Group,
1229 &a11y::Name::field(self.label.as_ref(), self.description.as_ref(), &validity),
1230 )
1231 .flex()
1232 .flex_row()
1233 .items_center()
1234 .gap(px(2.))
1235 .text_size(crate::util::FIELD_TEXT)
1236 .line_height(px(20.))
1237 .font_family(crate::util::MONO_FONT)
1238 .when_some(self.font_family.clone(), |group, family| {
1239 group.font_family(family)
1240 })
1241 .text_color(colors.field.foreground)
1242 .when(!self.bare, |el| {
1243 el.px(self.padding_x.unwrap_or(px(12.)))
1246 .h(self.height.unwrap_or(crate::util::FIELD_HEIGHT))
1247 .overflow_hidden()
1248 .rounded(radius)
1249 })
1250 .when(self.bare, |el| el.flex_1().min_w_0());
1251
1252 if navigable {
1256 let state = self.state.clone();
1257 let on_change = self.on_change.clone();
1258 let constraints = self.constraints.clone();
1259 let display = display.clone();
1260 let held = focused_seg.clone();
1261 let buffer = typing;
1262 let fh = focus_handle.clone();
1263 let slots = segments.clone();
1264 let is_read_only = self.is_read_only;
1265 let on_picker_open = self.on_picker_open.clone();
1266 let report_invalid_changes = self.report_invalid_changes;
1267 group = group
1268 .track_focus(&focus_handle)
1269 .key_context("DateField")
1270 .on_mouse_down(gpui::MouseButton::Left, move |_, window, cx| {
1271 window.focus(&fh, cx);
1272 })
1273 .on_key_down(move |event, window, cx| {
1274 let key = event.keystroke.key.as_str();
1275 if ((event.keystroke.modifiers.alt && matches!(key, "down" | "up"))
1276 || key == "space")
1277 && on_picker_open.is_some()
1278 && !is_read_only
1279 {
1280 if let Some(open) = &on_picker_open {
1281 open(window, cx);
1282 }
1283 return;
1284 }
1285 let commit = |focused: FieldSegment,
1286 date: Date,
1287 time: Option<crate::time_field::Time>,
1288 window: &mut Window,
1289 cx: &mut App| {
1290 let complete = display.update(cx, |display, cx| {
1291 let complete = display.edit(focused, date, time, &slots, granularity);
1292 cx.notify();
1293 complete
1294 });
1295 if let Some((date, _, committed)) = complete {
1296 state.update(cx, |s, cx| {
1297 s.set_value(committed);
1298 cx.notify();
1299 });
1300 if let Some(cb) = &on_change {
1301 let reported = if report_invalid_changes {
1302 Some(date)
1303 } else {
1304 Some(date).filter(|d| constraints.allows(*d))
1305 };
1306 cb(&reported, window, cx);
1307 }
1308 }
1309 };
1310 let (display_date, display_time) = {
1311 let display = display.read(cx);
1312 (display.date, display.time)
1313 };
1314 let seed_time = display_time.unwrap_or_default();
1315 match key {
1316 "left" | "right" => {
1317 let delta = if key == "right" { 1 } else { -1 };
1318 buffer.update(cx, |b, _| b.clear());
1319 let here = slots.iter().position(|s| *s == focused).unwrap_or(0) as i32;
1320 let next = (here + delta).clamp(0, slots.len() as i32 - 1) as usize;
1321 let next = slots[next];
1322 held.update(cx, |seg, cx| {
1323 *seg = next;
1324 cx.notify();
1325 });
1326 }
1327 _ if is_read_only => {}
1328 "up" | "down" | "pageup" | "pagedown" => {
1329 let direction = if matches!(key, "up" | "pageup") {
1330 1
1331 } else {
1332 -1
1333 };
1334 let delta = match focused {
1335 FieldSegment::Date(segment)
1336 if matches!(key, "pageup" | "pagedown") =>
1337 {
1338 direction * segment.page_step()
1339 }
1340 _ => direction,
1341 };
1342 let base = display_date.unwrap_or(seed);
1343 buffer.update(cx, |b, _| b.clear());
1344 match focused {
1347 FieldSegment::Date(segment) => {
1348 let next = match display_date {
1349 Some(_) => segment.bump(base, delta),
1350 None => base,
1351 };
1352 commit(focused, next, display_time, window, cx);
1353 }
1354 FieldSegment::Time(segment) => {
1355 let next = match display_time {
1356 Some(t) => t.bump(segment, delta),
1357 None => seed_time,
1358 };
1359 commit(focused, base, Some(next), window, cx);
1360 }
1361 }
1362 }
1363 "home" | "end" => {
1364 let maximum = key == "end";
1365 let base = display_date.unwrap_or(seed);
1366 buffer.update(cx, |b, _| b.clear());
1367 match focused {
1368 FieldSegment::Date(segment) => commit(
1369 focused,
1370 segment.bound(base, maximum),
1371 display_time,
1372 window,
1373 cx,
1374 ),
1375 FieldSegment::Time(segment) => {
1376 use crate::time_field::TimeSegment as T;
1377 let next = match segment {
1378 T::Meridiem => {
1379 let hour =
1380 seed_time.hour % 12 + if maximum { 12 } else { 0 };
1381 crate::time_field::Time::new(hour, seed_time.minute)
1382 .with_second(seed_time.second)
1383 }
1384 _ => segment.with_value(
1385 seed_time,
1386 if maximum { u32::MAX } else { 0 },
1387 twelve_hour,
1388 zero_based_twelve_hour,
1389 ),
1390 };
1391 commit(focused, base, Some(next), window, cx);
1392 }
1393 }
1394 }
1395 "a" | "p"
1397 if focused
1398 == FieldSegment::Time(crate::time_field::TimeSegment::Meridiem) =>
1399 {
1400 let hour = seed_time.hour % 12 + if key == "p" { 12 } else { 0 };
1401 let next = crate::time_field::Time::new(hour, seed_time.minute)
1402 .with_second(seed_time.second);
1403 commit(
1404 focused,
1405 display_date.unwrap_or(seed),
1406 Some(next),
1407 window,
1408 cx,
1409 );
1410 }
1411 "backspace" | "delete" => {
1412 buffer.update(cx, |b, _| b.clear());
1413 let emptied = display.update(cx, |display, cx| {
1414 let emptied = display.clear(focused, &slots);
1415 cx.notify();
1416 emptied
1417 });
1418 if emptied {
1419 state.update(cx, |s, cx| {
1420 s.set_value(String::new());
1421 cx.notify();
1422 });
1423 if let Some(cb) = &on_change {
1424 cb(&None, window, cx);
1425 }
1426 }
1427 }
1428 digit if digit.len() == 1 && digit.chars().all(|c| c.is_ascii_digit()) => {
1429 let digits = match focused {
1430 FieldSegment::Date(segment) => segment.digits(),
1431 FieldSegment::Time(segment) => segment.digits(),
1432 };
1433 if digits == 0 {
1434 return;
1435 }
1436 let text = buffer.update(cx, |b, _| {
1437 if b.len() >= digits {
1438 b.clear();
1439 }
1440 b.push_str(digit);
1441 b.clone()
1442 });
1443 let Ok(value) = text.parse::<u32>() else {
1444 return;
1445 };
1446 match focused {
1447 FieldSegment::Date(segment) => {
1448 let base = display_date.unwrap_or(seed);
1449 commit(
1450 focused,
1451 segment.with_value(base, value),
1452 display_time,
1453 window,
1454 cx,
1455 );
1456 }
1457 FieldSegment::Time(segment) => commit(
1458 focused,
1459 display_date.unwrap_or(seed),
1460 Some(segment.with_value(
1461 seed_time,
1462 value,
1463 twelve_hour,
1464 zero_based_twelve_hour,
1465 )),
1466 window,
1467 cx,
1468 ),
1469 }
1470 if text.len() >= digits {
1473 buffer.update(cx, |b, _| b.clear());
1474 let here =
1475 slots.iter().position(|s| *s == focused).unwrap_or(0) as i32;
1476 let next = (here + 1).clamp(0, slots.len() as i32 - 1) as usize;
1477 let next = slots[next];
1478 held.update(cx, |seg, cx| {
1479 *seg = next;
1480 cx.notify();
1481 });
1482 }
1483 }
1484 _ => {}
1485 }
1486 });
1487 }
1488
1489 let mut carrier_ring = None;
1494 if !self.bare && !self.is_bare {
1495 carrier_ring = Some(crate::util::field_ring_color(
1496 is_invalid,
1497 focus_handle.is_focused(window),
1498 self.focus_ring,
1499 cx,
1500 ));
1501 group = crate::util::apply_field_chrome_ringless(
1502 group,
1503 self.variant,
1504 is_invalid,
1505 focus_handle.is_focused(window),
1506 self.focus_ring,
1507 Some(radius),
1508 cx,
1509 );
1510
1511 let focused = focus_handle.is_focused(window);
1512 if !self.embedded && !self.is_disabled && !is_invalid && !focused {
1513 let idle_bg = match self.variant {
1514 herogpui_core::FieldVariant::Primary => colors.field.background,
1515 herogpui_core::FieldVariant::Secondary => colors.default.color,
1516 };
1517 let hover_bg = match self.variant {
1518 herogpui_core::FieldVariant::Primary => colors.field.hover(),
1519 herogpui_core::FieldVariant::Secondary => colors.default.hover(),
1520 };
1521 let hover_border = colors.field.border_hover();
1522 group = crate::anim::hover_fade_with_duration_and_easing(
1523 group,
1524 element_id::scoped(&base_id, "hover-fade"),
1525 (idle_bg, hover_bg),
1526 None,
1527 Some(hover_border),
1528 move |fill| fill.rounded(radius),
1529 Some(150),
1530 crate::anim::HoverFadeEasing::EaseSmooth,
1531 window,
1532 cx,
1533 );
1534 }
1535 }
1536 if self.full_width {
1537 group = group.w_full();
1538 }
1539
1540 if let Some(prefix) = self.prefix {
1544 group = group.child(
1545 gpui::div()
1546 .flex()
1547 .items_center()
1548 .flex_shrink_0()
1549 .mr(px(4.))
1550 .text_color(colors.field.placeholder)
1551 .child(prefix),
1552 );
1553 }
1554
1555 for (index, segment) in segments.iter().copied().enumerate() {
1556 let separator = match (index, segment) {
1557 (index, FieldSegment::Date(_)) => Some(regional_date.literals[index].clone()),
1558 (index, FieldSegment::Time(_)) => {
1559 let time_index = index - regional_date.order.len();
1560 regional_time.as_ref().and_then(|format| {
1561 format.literals.get(time_index).map(|literal| {
1562 if time_index == 0 {
1563 format!(", {literal}")
1564 } else {
1565 literal.clone()
1566 }
1567 })
1568 })
1569 }
1570 };
1571 if let Some(separator) = separator.filter(|separator| !separator.is_empty()) {
1572 group = group.child(gpui::div().text_color(colors.muted).child(separator));
1573 }
1574
1575 let mut seg = gpui::div()
1576 .id(element_id::indexed(&segment_base_id, "seg", index))
1577 .px(px(2.))
1579 .py(px(1.))
1580 .rounded(cx.layout().radius_md())
1581 .child(match &self.segment {
1584 Some(render) => render(segment, segment_text(segment).into()),
1585 _ => segment_text(segment).into_any_element(),
1586 });
1587
1588 if parsed.is_none() {
1589 seg = seg.text_color(colors.muted);
1590 }
1591 if focused == segment {
1592 seg = seg
1593 .bg(colors.accent.soft())
1594 .text_color(colors.accent.soft_foreground(colors.foreground));
1595 }
1596
1597 if navigable {
1598 let held = focused_seg.clone();
1599 seg = seg
1600 .cursor(crate::util::interactive_cursor(cx))
1601 .on_click(move |_, _, cx| {
1602 held.update(cx, |s, cx| {
1603 *s = segment;
1604 cx.notify();
1605 });
1606 });
1607 }
1608
1609 let seg_text = segment_text(segment);
1613 seg = seg
1614 .a11y_named(
1615 a11y::Role::TextInput,
1616 &a11y::Name::labelled(match segment {
1617 FieldSegment::Date(segment) => segment.a11y_label(cx),
1618 FieldSegment::Time(segment) => segment.a11y_label(cx),
1619 }),
1620 )
1621 .a11y_text(&seg_text, None);
1622
1623 group = group.child(seg);
1624 if index == regional_date.order.len() - 1 && !regional_date.literals[3].is_empty() {
1625 group = group.child(
1626 gpui::div()
1627 .text_color(colors.muted)
1628 .child(regional_date.literals[3].clone()),
1629 );
1630 }
1631 }
1632 if let Some(literal) = regional_time
1633 .as_ref()
1634 .and_then(|format| format.literals.last())
1635 .filter(|literal| !literal.is_empty())
1636 {
1637 group = group.child(gpui::div().text_color(colors.muted).child(literal.clone()));
1638 }
1639
1640 if let Some(suffix) = self.suffix {
1641 group = group.child(
1642 gpui::div()
1643 .flex()
1644 .items_center()
1645 .flex_shrink_0()
1646 .ml(px(4.))
1647 .text_color(colors.field.placeholder)
1648 .child(suffix),
1649 );
1650 }
1651 if navigable {
1652 group = crate::util::record_focus_bounds(group, &focus_handle, window, cx);
1653 }
1654
1655 if self.embedded {
1656 if self.is_disabled {
1657 group = group.opacity(cx.layout().disabled_opacity);
1658 }
1659 let group = crate::util::apply_sx(group, &self.sx);
1660 return match carrier_ring {
1661 Some(ring) => {
1662 crate::util::field_ring_carrier(group, ring, radius, cx).into_any_element()
1663 }
1664 None => group.into_any_element(),
1665 };
1666 }
1667
1668 let row: gpui::AnyElement = match carrier_ring {
1669 Some(ring) => {
1670 crate::util::field_ring_carrier(group, ring, radius, cx).into_any_element()
1671 }
1672 None => group.into_any_element(),
1673 };
1674
1675 let mut el = gpui::div().flex().flex_col().gap(px(4.));
1677 if !self.full_width {
1678 el = el.max_w(px(320.));
1679 } else {
1680 el = el.w_full();
1681 }
1682 if let Some(label) = self.label.clone() {
1683 el = el.child(
1684 crate::field::Label::new(label)
1685 .is_required(self.is_required)
1686 .is_invalid(is_invalid)
1687 .is_disabled(self.is_disabled),
1688 );
1689 }
1690 el = el.child(row);
1691 match validity.first() {
1694 Some(message) => el = el.child(crate::field::ErrorMessage::new(message)),
1695 None => {
1696 let description = self
1697 .description
1698 .clone()
1699 .unwrap_or_else(|| format_hint(regional_date, regional_time.as_ref()).into());
1700 el = el.child(crate::field::Description::new(description));
1701 }
1702 }
1703 if self.is_disabled {
1704 el = el.opacity(cx.layout().disabled_opacity);
1705 }
1706 crate::util::apply_sx(el, &self.sx).into_any_element()
1707 }
1708}
1709
1710#[cfg(test)]
1711mod tests {
1712 use std::process::Command;
1713
1714 use super::*;
1715
1716 #[test]
1717 fn enabled_date_field_hover_uses_the_pinned_smooth_fill_transition() {
1718 let source = include_str!("field.rs")
1719 .split("#[cfg(test)]")
1720 .next()
1721 .expect("the implementation section is always present");
1722 assert!(
1723 source.contains("hover_fade_with_duration_and_easing(")
1724 && source.contains("Some(150)")
1725 && source.contains("HoverFadeEasing::EaseSmooth")
1726 );
1727 assert!(source.contains("!self.embedded && !self.is_disabled && !is_invalid && !focused"));
1728 }
1729 use crate::time_field::{HourCycle, Time, TimeSegment};
1730
1731 #[test]
1732 fn a_day_value_is_a_plain_iso_date() {
1733 let date = Date::new(2025, 2, 3);
1734 assert_eq!(format_value(date, None, Granularity::Day), "2025-02-03");
1735 assert_eq!(parse_value("2025-02-03"), (Some(date), None));
1736 }
1737
1738 #[test]
1739 fn date_order_literals_and_padding_follow_locale_patterns() {
1740 let us = RegionalDateFormat::for_locale("en-US").unwrap();
1741 assert_eq!(us.order, DateSegment::ALL);
1742 assert_eq!(us.literals, ["", "/", "/", ""]);
1743 assert!(!us.month_has_leading_zero);
1744 assert!(!us.day_has_leading_zero);
1745
1746 let gb = RegionalDateFormat::for_locale("en-GB").unwrap();
1747 assert_eq!(
1748 gb.order,
1749 [DateSegment::Day, DateSegment::Month, DateSegment::Year]
1750 );
1751 assert_eq!(gb.literals, ["", "/", "/", ""]);
1752 assert!(gb.month_has_leading_zero);
1753 assert!(gb.day_has_leading_zero);
1754
1755 let german = RegionalDateFormat::for_locale("de-DE").unwrap();
1756 assert_eq!(
1757 german.order,
1758 [DateSegment::Day, DateSegment::Month, DateSegment::Year]
1759 );
1760 assert_eq!(german.literals, ["", ".", ".", ""]);
1761
1762 let japanese = RegionalDateFormat::for_locale("ja-JP").unwrap();
1763 assert_eq!(
1764 japanese.order,
1765 [DateSegment::Year, DateSegment::Month, DateSegment::Day]
1766 );
1767 assert_eq!(japanese.literals, ["", "/", "/", ""]);
1768 assert_eq!(RegionalDateFormat::for_locale("not_a_locale"), None);
1769 }
1770
1771 #[test]
1772 fn date_padding_prefers_the_system_time_category() {
1773 let tags = ["en-GB".to_owned(), "en-US".to_owned()];
1775 assert_eq!(
1776 RegionalDateFormat::for_preferences(&tags),
1777 RegionalDateFormat::for_locale("en-GB")
1778 );
1779 }
1780
1781 #[gpui::test]
1782 fn date_field_hour_cycle_follows_the_system_time_locale(cx: &mut gpui::TestAppContext) {
1783 const CHILD: &str = "HEROGPUI_DATE_FIELD_TIME_LOCALE_TEST";
1784 if std::env::var_os(CHILD).is_none() {
1785 let output = Command::new(std::env::current_exe().unwrap())
1786 .args([
1787 "--exact",
1788 "date_picker::tests::date_field_hour_cycle_follows_the_system_time_locale",
1789 "--nocapture",
1790 ])
1791 .env(CHILD, "1")
1792 .env_remove("LC_ALL")
1793 .env("LC_TIME", "en_US.UTF-8")
1794 .output()
1795 .unwrap();
1796 assert!(
1797 output.status.success(),
1798 "12-hour DateField locale child failed:\n{}\n{}",
1799 String::from_utf8_lossy(&output.stdout),
1800 String::from_utf8_lossy(&output.stderr),
1801 );
1802 return;
1803 }
1804
1805 assert_eq!(HourCycle::default(), HourCycle::H12);
1806 let state = cx.new(|cx| crate::input::InputState::new(cx));
1807 let field = DateField::new(state.clone()).granularity(Granularity::Minute);
1808 assert_eq!(field.hour_cycle, HourCycle::H12);
1809 assert_eq!(
1810 TimeSegment::order(
1811 field.granularity.time().unwrap(),
1812 field.hour_cycle == HourCycle::H12,
1813 ),
1814 [
1815 TimeSegment::Hour,
1816 TimeSegment::Minute,
1817 TimeSegment::Meridiem,
1818 ]
1819 );
1820
1821 let explicit = DateField::new(state)
1822 .granularity(Granularity::Minute)
1823 .hour_cycle(HourCycle::H24);
1824 assert_eq!(explicit.hour_cycle, HourCycle::H24);
1825 assert_eq!(
1826 TimeSegment::order(
1827 explicit.granularity.time().unwrap(),
1828 explicit.hour_cycle == HourCycle::H12,
1829 ),
1830 [TimeSegment::Hour, TimeSegment::Minute]
1831 );
1832 }
1833
1834 #[test]
1835 fn a_time_value_round_trips_at_every_granularity() {
1836 let date = Date::new(2025, 2, 3);
1837 let time = Time::new(8, 45).with_second(9);
1838 for (granularity, text) in [
1839 (Granularity::Hour, "2025-02-03T08"),
1840 (Granularity::Minute, "2025-02-03T08:45"),
1841 (Granularity::Second, "2025-02-03T08:45:09"),
1842 ] {
1843 assert_eq!(format_value(date, Some(time), granularity), text);
1844 let (d, t) = parse_value(text);
1845 assert_eq!(d, Some(date));
1846 let t = t.expect("a time");
1848 assert_eq!(t.hour, 8);
1849 assert_eq!(
1850 (t.minute, t.second),
1851 match granularity {
1852 Granularity::Hour => (0, 0),
1853 Granularity::Minute => (45, 0),
1854 _ => (45, 9),
1855 }
1856 );
1857 }
1858 }
1859
1860 #[test]
1861 fn a_space_separates_as_well_as_a_t() {
1862 assert_eq!(
1863 parse_value("2025-02-03 08:45"),
1864 parse_value("2025-02-03T08:45")
1865 );
1866 }
1867
1868 #[test]
1869 fn an_impossible_time_is_no_time() {
1870 assert_eq!(parse_value("2025-02-03T24:00").1, None);
1871 assert_eq!(parse_value("2025-02-03T08:60").1, None);
1872 assert_eq!(parse_value("2025-02-03Tzz").1, None);
1873 }
1874
1875 #[test]
1876 fn granularity_picks_the_time_slots() {
1877 let slots = |g: Granularity, twelve: bool| {
1878 g.time()
1879 .map(|t| TimeSegment::order(t, twelve))
1880 .unwrap_or_default()
1881 };
1882 assert!(slots(Granularity::Day, false).is_empty());
1883 assert_eq!(slots(Granularity::Hour, false), vec![TimeSegment::Hour]);
1884 assert_eq!(
1885 slots(Granularity::Second, false),
1886 vec![TimeSegment::Hour, TimeSegment::Minute, TimeSegment::Second]
1887 );
1888 assert_eq!(
1890 slots(Granularity::Hour, true),
1891 vec![TimeSegment::Hour, TimeSegment::Meridiem]
1892 );
1893 }
1894
1895 #[test]
1896 fn the_hint_says_what_the_field_takes() {
1897 let us = RegionalDateFormat::for_locale("en-US").unwrap();
1898 let gb = RegionalDateFormat::for_locale("en-GB").unwrap();
1899 let german = RegionalDateFormat::for_locale("de-DE").unwrap();
1900 assert_eq!(format_hint(&us, None), "MM/DD/YYYY");
1901 assert_eq!(format_hint(&gb, None), "DD/MM/YYYY");
1902 assert_eq!(format_hint(&german, None), "DD.MM.YYYY");
1903 let minute = crate::time_field::RegionalTimePattern {
1904 order: vec![TimeSegment::Hour, TimeSegment::Minute],
1905 literals: vec![String::new(), ":".to_owned(), String::new()],
1906 hour_has_leading_zero: true,
1907 hour_zero_based: false,
1908 minute_has_leading_zero: true,
1909 second_has_leading_zero: false,
1910 am: "AM".to_owned(),
1911 pm: "PM".to_owned(),
1912 };
1913 assert_eq!(format_hint(&us, Some(&minute)), "MM/DD/YYYY, HH:MM");
1914 let second_twelve = crate::time_field::RegionalTimePattern {
1915 order: vec![
1916 TimeSegment::Hour,
1917 TimeSegment::Minute,
1918 TimeSegment::Second,
1919 TimeSegment::Meridiem,
1920 ],
1921 literals: vec![
1922 String::new(),
1923 ":".to_owned(),
1924 ":".to_owned(),
1925 " ".to_owned(),
1926 String::new(),
1927 ],
1928 hour_has_leading_zero: false,
1929 hour_zero_based: false,
1930 minute_has_leading_zero: true,
1931 second_has_leading_zero: true,
1932 am: "AM".to_owned(),
1933 pm: "PM".to_owned(),
1934 };
1935 assert_eq!(
1936 format_hint(&us, Some(&second_twelve)),
1937 "MM/DD/YYYY, HH:MM:SS AM"
1938 );
1939 }
1940}
1941
1942crate::util::impl_component_styled!(DateField);