Skip to main content

herogpui_components/date_picker/
field.rs

1//! DateField.
2
3use super::*;
4
5// DateField (segmented)
6// ---------------------------------------------------------------------------
7
8/// One editable part of a [`DateField`].
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum DateSegment {
11    /// The month segment.
12    Month,
13    /// The day segment.
14    Day,
15    /// The year segment.
16    Year,
17}
18
19impl DateSegment {
20    /// All date segments in canonical month/day/year order.
21    pub const ALL: [DateSegment; 3] = [DateSegment::Month, DateSegment::Day, DateSegment::Year];
22
23    /// The lowercase segment name (`month`, `day` or `year`).
24    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    /// The segment's accessible name in the active locale (`year`, `month`,
33    /// `day` in en-US; React Aria's datepicker dictionary).
34    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    /// The placeholder this segment shows with no value, sized like its digits.
47    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    /// How many digits this segment holds — the point at which typing moves on.
56    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    /// `date` with this segment set to `value`, clamped to what the calendar
72    /// allows (February 31st becomes the 28th or 29th).
73    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    /// `date` with this segment moved by `delta`, keeping the result a real
98    /// calendar date (31 January + 1 month is the end of February, not the 31st).
99    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/// `granularity` — the smallest unit a date field shows.
259///
260/// v3 defaults a date to `day`; anything smaller adds the time segments, which
261/// is why its own example switches `defaultValue` from `parseDate` to
262/// `parseZonedDateTime` when the granularity drops below a day.
263#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
264pub enum Granularity {
265    /// Day precision, with no time segments.
266    #[default]
267    Day,
268    /// Hour precision.
269    Hour,
270    /// Minute precision.
271    Minute,
272    /// Second precision.
273    Second,
274}
275
276impl Granularity {
277    /// Every granularity, from coarsest to finest.
278    pub const ALL: [Granularity; 4] = [
279        Granularity::Day,
280        Granularity::Hour,
281        Granularity::Minute,
282        Granularity::Second,
283    ];
284
285    /// The human-readable name of this granularity.
286    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    /// The time granularity this asks for, or `None` for a plain date.
296    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/// One editable slot of a date field: a date part, or -- below `day`
308/// granularity -- a time part.
309#[derive(Clone, Copy, Debug, PartialEq, Eq)]
310pub enum FieldSegment {
311    /// A date segment (month, day or year).
312    Date(DateSegment),
313    /// A time segment, present below `day` granularity.
314    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/// State supplied to v3's DateField children render function.
407#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
408#[non_exhaustive]
409pub struct DateFieldRenderState {
410    /// Whether the field is disabled.
411    pub is_disabled: bool,
412    /// Whether controlled, server, custom or constraint validation is invalid.
413    pub is_invalid: bool,
414    /// Whether segments can be focused but not edited.
415    pub is_read_only: bool,
416    /// Whether the field is required.
417    pub is_required: bool,
418    /// Whether the field's input owns focus.
419    pub is_focused: bool,
420    /// Whether focus is inside the field.
421    pub is_focus_within: bool,
422    /// Whether keyboard-visible focus chrome should be shown.
423    pub is_focus_visible: bool,
424}
425
426/// v3's DateField: three editable segments (month / day / year), with the ISO
427/// text kept in the bound `InputState` so the form and `onChange` still see a
428/// plain date string.
429#[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    /// See [`DateField::content`].
433    content: Option<std::sync::Arc<dyn Fn(DateFieldRenderState) -> gpui::AnyElement + 'static>>,
434    /// `segment` — v3's render prop for one editable date or time segment,
435    /// handed which segment it is and the text the field would show.
436    segment: Option<FieldSegmentRender>,
437    /// `validationBehavior` — written into the text state on render.
438    validation_behavior: Option<crate::form::ValidationBehavior>,
439    /// `defaultValue` — seeds the text state on the first render only.
440    default_value: Option<Date>,
441    /// `value` — v3's controlled date, stored for the first render only and
442    /// seeded into the text state as ISO. The outer `Option` distinguishes an
443    /// unset builder from `value(null)`, the explicitly controlled empty date.
444    value: Option<Option<Date>>,
445    full_width: bool,
446    is_required: bool,
447    /// `validate` — run by the component, not the caller.
448    validate: Option<crate::validation::Validator<Option<Date>>>,
449    /// `validationErrors` — messages from a server round-trip.
450    validation_errors: Vec<SharedString>,
451    is_invalid: bool,
452    variant: herogpui_core::FieldVariant,
453    constraints: DateConstraints,
454    placeholder_value: Option<Date>,
455    /// `granularity` — `day`, or a time unit, in which case the field grows the
456    /// segments for it and the bound state holds an ISO date-and-time.
457    granularity: Granularity,
458    /// `hourCycle` — 12- or 24-hour, for the hour segment `granularity` adds.
459    hour_cycle: crate::time_field::HourCycle,
460    /// `DateField.Prefix` — content before the segments, drawn in the
461    /// placeholder colour and inert (`pointer-events-none`).
462    prefix: Option<gpui::AnyElement>,
463    /// `DateField.Suffix` — content after the segments.
464    suffix: Option<gpui::AnyElement>,
465    state: Entity<crate::input::InputState>,
466    label: Option<SharedString>,
467    /// `Description` — composed inside the field in v3's own example.
468    description: Option<SharedString>,
469    /// `name` — the name this field submits under.
470    name: Option<SharedString>,
471    /// `autoFocus` — take focus on the first render.
472    auto_focus: bool,
473    /// `shouldForceLeadingZeros` — force month, day and hour to two digits
474    /// instead of using the system regional format.
475    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    /// Public chrome-only bare mode: keeps the box geometry, drops the paint.
482    is_bare: bool,
483    /// Whether focused state paints the visual focus ring.
484    focus_ring: bool,
485    /// The family the segments are drawn with; unset keeps the mono token.
486    font_family: Option<SharedString>,
487    /// The corner radius, in place of the owning `field_radius` helper.
488    radius: Option<Pixels>,
489    /// Explicit single-line box height; `None` is the 36px stock box.
490    height: Option<Pixels>,
491    /// Explicit horizontal box padding; `None` is `px-3`.
492    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    /// The locale whose date order, separators and padding the segments use.
496    /// Absent means the OS regional format (`system_date_format`).
497    locale: Option<SharedString>,
498    /// The `sx` slot, refined over the root style at the end of render.
499    sx: Option<Box<gpui::StyleRefinement>>,
500}
501
502impl DateField {
503    /// `fullWidth`
504    pub fn full_width(mut self, v: bool) -> Self {
505        self.full_width = v;
506        self
507    }
508
509    /// `isRequired`
510    /// `Description` — help text under the field.
511    pub fn description(mut self, text: impl Into<SharedString>) -> Self {
512        self.description = Some(text.into());
513        self
514    }
515
516    /// `name` — the name this field submits under.
517    pub fn name(mut self, name: impl Into<SharedString>) -> Self {
518        self.name = Some(name.into());
519        self
520    }
521
522    /// The `Form` field this control submits, when it has a `name`.
523    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    /// `autoFocus` — take focus on the first render.
543    pub fn auto_focus(mut self, v: bool) -> Self {
544        self.auto_focus = v;
545        self
546    }
547
548    /// `shouldForceLeadingZeros` — force month, day and hour to two digits.
549    /// Without this flag those segments follow the system regional format;
550    /// minute and second segments are always two digits.
551    pub fn should_force_leading_zeros(mut self, v: bool) -> Self {
552        self.should_force_leading_zeros = v;
553        self
554    }
555
556    /// `isDisabled` — greys the field out and stops it answering keys.
557    pub fn is_disabled(mut self, v: bool) -> Self {
558        self.is_disabled = v;
559        self
560    }
561
562    /// `isReadOnly` — shows the value but refuses edits.
563    pub fn is_read_only(mut self, v: bool) -> Self {
564        self.is_read_only = v;
565        self
566    }
567
568    /// Sets whether the field is required (`isRequired`).
569    pub fn is_required(mut self, v: bool) -> Self {
570        self.is_required = v;
571        self
572    }
573
574    /// `placeholderValue` — the date the empty field formats its hint from.
575    pub fn placeholder_value(mut self, date: Date) -> Self {
576        self.placeholder_value = Some(date);
577        self
578    }
579
580    /// `granularity` — the smallest unit the field shows.
581    ///
582    /// Below `day` the field grows the time segments and the bound state holds
583    /// an ISO date-and-time (`2025-02-03T08:45`), which is the value a form
584    /// submits; `on_change` still reports the date part.
585    pub fn granularity(mut self, granularity: Granularity) -> Self {
586        self.granularity = granularity;
587        self
588    }
589
590    /// `hourCycle` — whether the hour segment `granularity` adds is 12- or
591    /// 24-hour.
592    pub fn hour_cycle(mut self, cycle: crate::time_field::HourCycle) -> Self {
593        self.hour_cycle = cycle;
594        self
595    }
596
597    /// `DateField.Prefix` — content before the segments.
598    pub fn prefix(mut self, el: impl IntoElement) -> Self {
599        self.prefix = Some(el.into_any_element());
600        self
601    }
602
603    /// `DateField.Suffix` — content after the segments.
604    pub fn suffix(mut self, el: impl IntoElement) -> Self {
605        self.suffix = Some(el.into_any_element());
606        self
607    }
608
609    /// `validate` — returns the message to show, or `None` when the date is fine.
610    ///
611    /// The component runs it and surfaces the result.
612    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    /// `validationErrors` — messages produced elsewhere, shown ahead of
618    /// whatever `validate` returns.
619    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    /// `isInvalid` — forces the danger treatment regardless of the text.
628    pub fn is_invalid(mut self, v: bool) -> Self {
629        self.is_invalid = v;
630        self
631    }
632
633    /// `variant` — `Secondary` drops the field shadow.
634    pub fn variant(mut self, variant: herogpui_core::FieldVariant) -> Self {
635        self.variant = variant;
636        self
637    }
638
639    /// Replaces the 36px box height. Only the single-line box changes: the
640    /// segments keep their 14px type and 20px line and stay centred.
641    pub fn height(mut self, h: impl Into<Pixels>) -> Self {
642        self.height = Some(h.into());
643        self
644    }
645
646    /// Replaces the box's `px-3` horizontal padding.
647    pub fn padding_x(mut self, p: impl Into<Pixels>) -> Self {
648        self.padding_x = Some(p.into());
649        self
650    }
651
652    /// Renders the box with no background, border, field shadow or focus ring,
653    /// for a caller painting around it. The field keeps its box geometry and
654    /// stays editable and focusable.
655    pub fn is_bare(mut self, v: bool) -> Self {
656        self.is_bare = v;
657        self
658    }
659
660    /// Shows or hides only the segmented field's visual focus ring. The field
661    /// remains focusable and editable when set to `false`.
662    pub fn focus_ring(mut self, v: bool) -> Self {
663        self.focus_ring = v;
664        self
665    }
666
667    /// The family the segments are drawn with; unset keeps the mono token.
668    pub fn font_family(mut self, family: impl Into<SharedString>) -> Self {
669        self.font_family = Some(family.into());
670        self
671    }
672
673    /// The corner radius, in place of the owning `field_radius` helper. Not a
674    /// v3 prop; the removed v2 `radius` prop is prohibited and this is a
675    /// per-component repository extension.
676    ///
677    /// The box and shared field chrome use the same resolved radius. A bare
678    /// field retains it without painting chrome.
679    pub fn radius(mut self, radius: impl Into<Pixels>) -> Self {
680        self.radius = Some(radius.into());
681        self
682    }
683
684    /// `value` — v3's controlled-date spelling, as a pure builder.
685    ///
686    /// The bound [`crate::InputState`] owns the ISO text once the field
687    /// renders, so this seeds the state on the first render only, winning
688    /// over [`DateField::default_value`] the way v3's controlled prop
689    /// outranks the uncontrolled seed; calling `.value(..)` twice keeps the
690    /// last call, like every other builder here. `None` is v3's `null` — an
691    /// explicitly controlled empty date, seeded as empty text, which still
692    /// outranks `default_value`. A later date is an imperative update rather
693    /// than a builder: `state.update(cx, |s, _| s.set_value(..))`.
694    pub fn value(mut self, date: Option<Date>) -> Self {
695        self.value = Some(date);
696        self
697    }
698
699    /// `minValue` — the earliest date the field accepts.
700    pub fn min_value(mut self, date: Date) -> Self {
701        self.constraints.min_value = Some(date);
702        self
703    }
704
705    /// `maxValue` — the latest date the field accepts.
706    pub fn max_value(mut self, date: Date) -> Self {
707        self.constraints.max_value = Some(date);
708        self
709    }
710
711    /// `isDateUnavailable` — rejects individual dates inside the range.
712    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    /// All the date constraints at once, for callers that already hold a set.
718    pub fn constraints(mut self, constraints: DateConstraints) -> Self {
719        self.constraints = constraints;
720        self
721    }
722
723    /// v3's field `children`-as-a-function, handed the complete
724    /// [`DateFieldRenderState`].
725    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    /// Creates a date field backed by the input `state`.
734    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    /// The locale whose date order, separators and padding the segments use.
778    ///
779    /// v3 sets this through `<I18nProvider locale=...>`; gpui has no subtree
780    /// context, so the tag is a builder on the field the provider would have
781    /// wrapped. Absent, the OS regional format wins.
782    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    /// `segment` — replaces the contents of each editable segment.
804    ///
805    /// The closure receives which [`FieldSegment`] it is drawing and the text
806    /// the field would have shown, including time segments below day
807    /// granularity.
808    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    /// `validationBehavior` — see [`crate::input::Input::validation_behavior`].
817    pub fn validation_behavior(mut self, behavior: crate::form::ValidationBehavior) -> Self {
818        self.validation_behavior = Some(behavior);
819        self
820    }
821
822    /// `defaultValue` — the uncontrolled initial date.
823    ///
824    /// Written into the state on the first render only, so it seeds the
825    /// component without fighting the user afterwards.
826    pub fn default_value(mut self, value: Date) -> Self {
827        self.default_value = Some(value);
828        self
829    }
830
831    /// Sets the label shown above the field.
832    pub fn label(mut self, l: impl Into<SharedString>) -> Self {
833        self.label = Some(l.into());
834        self
835    }
836
837    /// Sets the handler called when the date changes (`onChange`).
838    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    /// The one slot for caller-owned low-level styling: GPUI's styling methods
844    /// (`bg`, `text_color`, `w`, `h`, `p`, `rounded`, `border_color`, …)
845    /// applied to the field's root element after every value the variant and
846    /// the active theme chose, so they win.
847    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
867/// The regional format a field of this granularity accepts, which is the
868/// description v3 shows when the caller supplies none of their own.
869pub(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(&regional_time.hint());
877    }
878    hint
879}
880
881/// A date field's value: the date, and the time when `granularity` asks for
882/// one. `T` or a space separates them, as ISO 8601 allows both.
883pub(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
892/// `HH`, `HH:MM` or `HH:MM:SS`. A missing part is zero, which is what makes an
893/// hour-granularity value round-trip.
894pub(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
911/// The text the state holds, which is also what a form submits: an ISO date,
912/// widened by exactly as much time as the granularity shows.
913pub(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        // Every keyed slot below hangs off this one base, so no two of them can
937        // flatten into a shared key. The segments keep their own base, whose
938        // name has always differed from this one.
939        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        // `validationBehavior` travels with the name, on the text state.
958        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        // `value` / `defaultValue` seed the state once, before anything reads
965        // it. `value` is v3's controlled spelling, so it outranks the
966        // uncontrolled seed; the state owns the text afterwards, and
967        // `InputState::set_value` is the imperative update.
968        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        // Which segment the arrows and typing act on. `use_keyed_state` takes
1012        // `cx` mutably, so this precedes the theme tokens.
1013        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        // Digits typed into the focused segment but not yet complete, so `1` in
1020        // the month segment can still become `12`. Cleared whenever focus moves.
1021        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        // A narrower granularity can leave the caret on a slot that is gone.
1046        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        // The three ways a date can be wrong are reported separately, as the
1061        // calendar grids distinguish them too.
1062        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        // v3 order: the controlled flag, then server errors, then `validate`,
1089        // then whichever constraint the date breaks.
1090        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        // An empty field seeds from `placeholderValue`, the way v3 does, so the
1217        // first arrow press lands on a sensible date instead of jumping a step
1218        // from nothing.
1219        let seed = self.placeholder_value.unwrap_or_else(Date::today);
1220        // The box and shared field chrome use the same resolved radius.
1221        let radius = self.radius.unwrap_or_else(|| crate::util::field_radius(cx));
1222        // `useDateField` is `role: 'group'` on the field box. The hidden
1223        // native input is `role: 'presentation'` and has no counterpart
1224        // element here.
1225        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                // `.date-input-group` is `h-9 items-center overflow-hidden`
1244                // with the segments inside it.
1245                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        // v3 drives a date field from the keyboard: the arrows step the focused
1253        // segment and walk between segments, and digits type into it. Without
1254        // this the steppers were the only way to change a value at all.
1255        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                            // The first press on an empty field takes the seed
1345                            // itself rather than stepping past it.
1346                            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                        // The meridiem answers its own letters, as v3's does.
1396                        "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                            // A full segment hands the caret on, which is what
1471                            // makes `12252025` type a whole date.
1472                            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        // `.date-input-group` is `overflow-hidden` around its segments, so a
1490        // ring drawn in its margin would be cut: the group takes the chrome
1491        // without the ring and `util::field_ring_carrier` below hangs the ring
1492        // outside the clip.
1493        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        // `.date-input-group__prefix` is `ms-3 me-0`; the shell's own `px-3`
1541        // already provides that inset, so the slot only needs to sit inline and
1542        // inherit the placeholder colour.
1543        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                // `.date-input-group__segment` is `rounded-md px-0.5`.
1578                .px(px(2.))
1579                .py(px(1.))
1580                .rounded(cx.layout().radius_md())
1581                // `segment` is v3's render prop on `DateField.Segment`: the
1582                // closure is handed which segment it is drawing.
1583                .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            // `useDateSegment` is `role: 'spinbutton'` then rewritten to
1610            // `role: 'textbox'` (`useDateSegment.mjs`). Named by the
1611            // segment type, the same string `displayNames.of` gives.
1612            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        // -- label / description / error wrapper ------------------------------
1676        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        // The format hint is the description v3 shows when the caller supplies
1692        // none of their own.
1693        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        // The time category comes first in the system chain (`system_locale`).
1774        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            // Only as much of the time as the granularity wrote comes back.
1847            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        // A 12-hour clock adds the meridiem, wherever the granularity stops.
1889        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);