Skip to main content

gpui_base/
time_field.rs

1use crate::TestSupportExt as _;
2use std::rc::Rc;
3
4use chrono::{NaiveTime, Timelike as _};
5use gpui::{
6    AnyElement, App, Context, ElementId, Empty, Entity, EventEmitter, FocusHandle, Focusable,
7    InteractiveElement, IntoElement, KeyBinding, KeyDownEvent, MouseButton, MouseDownEvent,
8    ParentElement, Render, RenderOnce, Role, SharedString, StatefulInteractiveElement,
9    StyleRefinement, Styled, Window, div, prelude::FluentBuilder as _,
10};
11
12use crate::{
13    Decrement, Increment, StyledExt as _,
14    a11y_disabled::A11yDisabledExt as _,
15    actions::{SelectLeft, SelectNextColumn, SelectPrevColumn, SelectRight},
16    input::Delete,
17};
18
19const CONTEXT: &str = "TimeField";
20
21pub(crate) fn init(cx: &mut App) {
22    cx.bind_keys([
23        KeyBinding::new("up", Increment, Some(CONTEXT)),
24        KeyBinding::new("down", Decrement, Some(CONTEXT)),
25        KeyBinding::new("left", SelectLeft, Some(CONTEXT)),
26        KeyBinding::new("right", SelectRight, Some(CONTEXT)),
27        KeyBinding::new("tab", SelectNextColumn, Some(CONTEXT)),
28        KeyBinding::new("shift-tab", SelectPrevColumn, Some(CONTEXT)),
29        KeyBinding::new("backspace", Delete, Some(CONTEXT)),
30        KeyBinding::new("delete", Delete, Some(CONTEXT)),
31    ]);
32}
33
34/// The smallest unit a [`TimeField`] edits.
35#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
36pub enum TimePrecision {
37    /// Hours and minutes, e.g. `09:30`.
38    #[default]
39    Minute,
40    /// Hours, minutes and seconds, e.g. `09:30:15`.
41    Second,
42}
43
44impl TimePrecision {
45    /// The `chrono` format that displays a time at this precision and cycle.
46    pub fn format(self, hour_cycle: HourCycle) -> &'static str {
47        match (self, hour_cycle) {
48            (Self::Minute, HourCycle::H23) => "%H:%M",
49            (Self::Second, HourCycle::H23) => "%H:%M:%S",
50            (Self::Minute, HourCycle::H12) => "%I:%M %p",
51            (Self::Second, HourCycle::H12) => "%I:%M:%S %p",
52        }
53    }
54
55    /// Drop the components finer than this precision.
56    pub fn truncate(self, time: NaiveTime) -> NaiveTime {
57        let second = match self {
58            Self::Minute => 0,
59            Self::Second => time.second(),
60        };
61        NaiveTime::from_hms_opt(time.hour(), time.minute(), second).unwrap_or(time)
62    }
63}
64
65/// How a [`TimeField`] counts the hours of a day.
66///
67/// The names follow the Unicode `hourCycle` values.
68#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
69pub enum HourCycle {
70    /// A 24-hour clock from `00` to `23`.
71    #[default]
72    H23,
73    /// A 12-hour clock from `12` to `11`, with an AM/PM segment.
74    H12,
75}
76
77/// One editable component of a [`TimeField`].
78#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
79pub enum TimeSegment {
80    Hour,
81    Minute,
82    Second,
83    /// AM or PM, present with [`HourCycle::H12`].
84    Period,
85}
86
87impl TimeSegment {
88    fn name(self) -> &'static str {
89        match self {
90            Self::Hour => "hour",
91            Self::Minute => "minute",
92            Self::Second => "second",
93            Self::Period => "period",
94        }
95    }
96}
97
98/// A semantic notification from a [`TimeFieldState`].
99#[derive(Clone, Copy, Debug, PartialEq, Eq)]
100pub enum TimeFieldEvent {
101    /// Keyboard editing changed the time.
102    Change(NaiveTime),
103}
104
105/// The pure editing rules of a [`TimeFieldState`], kept apart from focus so
106/// they can be tested without a window.
107#[derive(Clone, Copy, Debug, PartialEq, Eq)]
108struct SegmentEditor {
109    time: NaiveTime,
110    precision: TimePrecision,
111    hour_cycle: HourCycle,
112    segment: TimeSegment,
113    /// The first digit typed into the selected segment, awaiting a second.
114    pending_digit: Option<u32>,
115}
116
117impl SegmentEditor {
118    fn new() -> Self {
119        Self {
120            time: NaiveTime::MIN,
121            precision: TimePrecision::default(),
122            hour_cycle: HourCycle::default(),
123            segment: TimeSegment::Hour,
124            pending_digit: None,
125        }
126    }
127
128    /// The segments shown, in reading order.
129    fn segments(&self) -> &'static [TimeSegment] {
130        use TimeSegment::*;
131        match (self.precision, self.hour_cycle) {
132            (TimePrecision::Minute, HourCycle::H23) => &[Hour, Minute],
133            (TimePrecision::Second, HourCycle::H23) => &[Hour, Minute, Second],
134            (TimePrecision::Minute, HourCycle::H12) => &[Hour, Minute, Period],
135            (TimePrecision::Second, HourCycle::H12) => &[Hour, Minute, Second, Period],
136        }
137    }
138
139    /// The inclusive range a segment's displayed value takes.
140    fn bounds(&self, segment: TimeSegment) -> (u32, u32) {
141        match (segment, self.hour_cycle) {
142            (TimeSegment::Hour, HourCycle::H23) => (0, 23),
143            (TimeSegment::Hour, HourCycle::H12) => (1, 12),
144            (TimeSegment::Minute | TimeSegment::Second, _) => (0, 59),
145            (TimeSegment::Period, _) => (0, 1),
146        }
147    }
148
149    /// The displayed value of a segment: the hour follows the cycle, and the
150    /// period is `0` for AM and `1` for PM.
151    fn value(&self, segment: TimeSegment) -> u32 {
152        let hour = self.time.hour();
153        match (segment, self.hour_cycle) {
154            (TimeSegment::Hour, HourCycle::H23) => hour,
155            (TimeSegment::Hour, HourCycle::H12) => (hour + 11) % 12 + 1,
156            (TimeSegment::Minute, _) => self.time.minute(),
157            (TimeSegment::Second, _) => self.time.second(),
158            (TimeSegment::Period, _) => hour / 12,
159        }
160    }
161
162    fn label(&self, segment: TimeSegment) -> SharedString {
163        match segment {
164            TimeSegment::Period if self.value(segment) == 0 => "AM".into(),
165            TimeSegment::Period => "PM".into(),
166            _ => format!("{:02}", self.value(segment)).into(),
167        }
168    }
169
170    fn with_value(&self, segment: TimeSegment, value: u32) -> NaiveTime {
171        let time = self.time;
172        let pm_offset = time.hour() / 12 * 12;
173        match (segment, self.hour_cycle) {
174            (TimeSegment::Hour, HourCycle::H23) => time.with_hour(value),
175            // 12 AM is midnight and 12 PM is noon.
176            (TimeSegment::Hour, HourCycle::H12) => time.with_hour(value % 12 + pm_offset),
177            (TimeSegment::Minute, _) => time.with_minute(value),
178            (TimeSegment::Second, _) => time.with_second(value),
179            (TimeSegment::Period, _) => time.with_hour(time.hour() % 12 + value * 12),
180        }
181        .unwrap_or(time)
182    }
183
184    fn set_precision(&mut self, precision: TimePrecision) {
185        self.precision = precision;
186        self.time = precision.truncate(self.time);
187        self.reset_segment();
188    }
189
190    fn set_hour_cycle(&mut self, hour_cycle: HourCycle) {
191        self.hour_cycle = hour_cycle;
192        self.reset_segment();
193    }
194
195    fn reset_segment(&mut self) {
196        if !self.segments().contains(&self.segment) {
197            self.segment = TimeSegment::Hour;
198        }
199        self.pending_digit = None;
200    }
201
202    fn set_time(&mut self, time: NaiveTime) -> bool {
203        let time = self.precision.truncate(time);
204        if self.time == time {
205            // Keep a half-typed segment when the owner echoes the same value back.
206            return false;
207        }
208        self.time = time;
209        self.pending_digit = None;
210        true
211    }
212
213    fn select_segment(&mut self, segment: TimeSegment) -> bool {
214        if !self.segments().contains(&segment) {
215            return false;
216        }
217        self.segment = segment;
218        self.pending_digit = None;
219        true
220    }
221
222    /// Move the selected segment by `offset`; returns `false` at either end.
223    fn move_segment(&mut self, offset: isize) -> bool {
224        let segments = self.segments();
225        let ix = segments
226            .iter()
227            .position(|segment| *segment == self.segment)
228            .unwrap_or(0);
229        let Some(next) = ix
230            .checked_add_signed(offset)
231            .and_then(|ix| segments.get(ix))
232        else {
233            return false;
234        };
235        self.select_segment(*next)
236    }
237
238    /// Step the selected segment by `delta`, wrapping within the segment.
239    fn step(&mut self, delta: i32) -> bool {
240        self.pending_digit = None;
241        let (min, max) = self.bounds(self.segment);
242        let span = (max - min + 1) as i32;
243        let value = (self.value(self.segment) as i32 - min as i32 + delta).rem_euclid(span);
244        self.replace_segment(value as u32 + min)
245    }
246
247    fn input_digit(&mut self, digit: u32) -> bool {
248        if self.segment == TimeSegment::Period {
249            return false;
250        }
251        let (min, max) = self.bounds(self.segment);
252        let (value, complete) = match self.pending_digit.take() {
253            Some(first) if (min..=max).contains(&(first * 10 + digit)) => {
254                (first * 10 + digit, true)
255            }
256            // The two digits cannot form a valid value, so the new digit starts over.
257            _ => (digit, digit * 10 > max),
258        };
259        if !complete {
260            self.pending_digit = Some(digit);
261        }
262        let changed = self.replace_segment(value);
263        if complete {
264            self.move_segment(1);
265        }
266        changed
267    }
268
269    /// Type `a` or `p` into the period segment.
270    fn input_period(&mut self, pm: bool) -> bool {
271        if self.segment != TimeSegment::Period {
272            return false;
273        }
274        self.replace_segment(pm as u32)
275    }
276
277    /// Reset the selected segment to its first value: zero, or `12` / AM on a
278    /// 12-hour clock.
279    fn clear_segment(&mut self) -> bool {
280        self.pending_digit = None;
281        self.replace_segment(0)
282    }
283
284    fn replace_segment(&mut self, value: u32) -> bool {
285        let time = self.with_value(self.segment, value);
286        let changed = time != self.time;
287        self.time = time;
288        changed
289    }
290}
291
292/// Retained editing behavior for a segmented time field.
293///
294/// The field is one Tab stop. Inside it, one segment is selected at a time:
295/// Up/Down step the selected segment and wrap within it (no carry into the
296/// next unit), Left/Right and Tab/Shift-Tab move between segments, digits are
297/// typed with a two-digit buffer that advances to the next segment once no
298/// further digit could fit, `a`/`p` set the AM/PM segment, and
299/// Backspace/Delete reset the segment.
300///
301/// The owner supplies the value with [`TimeFieldState::set_time`], which does
302/// not emit; user edits emit [`TimeFieldEvent::Change`].
303pub struct TimeFieldState {
304    focus_handle: FocusHandle,
305    editor: SegmentEditor,
306}
307
308impl TimeFieldState {
309    pub fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
310        Self {
311            focus_handle: cx.focus_handle().tab_stop(true),
312            editor: SegmentEditor::new(),
313        }
314    }
315
316    /// Set the precision, default: [`TimePrecision::Minute`].
317    pub fn precision(mut self, precision: TimePrecision) -> Self {
318        self.editor.set_precision(precision);
319        self
320    }
321
322    pub fn set_precision(
323        &mut self,
324        precision: TimePrecision,
325        _: &mut Window,
326        cx: &mut Context<Self>,
327    ) {
328        if self.editor.precision != precision {
329            self.editor.set_precision(precision);
330            cx.notify();
331        }
332    }
333
334    /// Set the hour cycle, default: [`HourCycle::H23`].
335    pub fn hour_cycle(mut self, hour_cycle: HourCycle) -> Self {
336        self.editor.set_hour_cycle(hour_cycle);
337        self
338    }
339
340    pub fn set_hour_cycle(
341        &mut self,
342        hour_cycle: HourCycle,
343        _: &mut Window,
344        cx: &mut Context<Self>,
345    ) {
346        if self.editor.hour_cycle != hour_cycle {
347            self.editor.set_hour_cycle(hour_cycle);
348            cx.notify();
349        }
350    }
351
352    pub fn time(&self) -> NaiveTime {
353        self.editor.time
354    }
355
356    /// Replace the time without emitting [`TimeFieldEvent::Change`].
357    pub fn set_time(&mut self, time: NaiveTime, _: &mut Window, cx: &mut Context<Self>) {
358        if self.editor.set_time(time) {
359            cx.notify();
360        }
361    }
362
363    /// The segment that keyboard editing applies to.
364    pub fn selected_segment(&self) -> TimeSegment {
365        self.editor.segment
366    }
367
368    pub fn focus(&self, window: &mut Window, cx: &mut App) {
369        self.focus_handle.focus(window, cx);
370    }
371
372    fn edit(&mut self, cx: &mut Context<Self>, edit: impl FnOnce(&mut SegmentEditor) -> bool) {
373        if edit(&mut self.editor) {
374            cx.emit(TimeFieldEvent::Change(self.editor.time));
375        }
376        cx.notify();
377    }
378
379    fn on_increment(&mut self, _: &Increment, _: &mut Window, cx: &mut Context<Self>) {
380        self.edit(cx, |editor| editor.step(1));
381    }
382
383    fn on_decrement(&mut self, _: &Decrement, _: &mut Window, cx: &mut Context<Self>) {
384        self.edit(cx, |editor| editor.step(-1));
385    }
386
387    fn on_delete(&mut self, _: &Delete, _: &mut Window, cx: &mut Context<Self>) {
388        self.edit(cx, SegmentEditor::clear_segment);
389    }
390
391    fn on_select_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context<Self>) {
392        self.editor.move_segment(-1);
393        cx.notify();
394    }
395
396    fn on_select_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut Context<Self>) {
397        self.editor.move_segment(1);
398        cx.notify();
399    }
400
401    // Tab walks the segments first and leaves the field only from the last one.
402    fn on_next_column(&mut self, _: &SelectNextColumn, _: &mut Window, cx: &mut Context<Self>) {
403        if self.editor.move_segment(1) {
404            cx.notify();
405        } else {
406            cx.propagate();
407        }
408    }
409
410    fn on_prev_column(&mut self, _: &SelectPrevColumn, _: &mut Window, cx: &mut Context<Self>) {
411        if self.editor.move_segment(-1) {
412            cx.notify();
413        } else {
414            cx.propagate();
415        }
416    }
417
418    fn on_key_down(&mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context<Self>) {
419        let keystroke = &event.keystroke;
420        if keystroke.modifiers.modified() || keystroke.key.chars().count() != 1 {
421            return;
422        }
423        let Some(ch) = keystroke.key.chars().next() else {
424            return;
425        };
426        let edit: Box<dyn FnOnce(&mut SegmentEditor) -> bool> = match ch {
427            '0'..='9' => Box::new(move |editor| editor.input_digit(ch as u32 - '0' as u32)),
428            'a' | 'A' => Box::new(|editor| editor.input_period(false)),
429            'p' | 'P' => Box::new(|editor| editor.input_period(true)),
430            _ => return,
431        };
432        window.prevent_default();
433        cx.stop_propagation();
434        self.edit(cx, edit);
435    }
436
437    fn select_segment(&mut self, segment: TimeSegment, cx: &mut Context<Self>) {
438        self.editor.select_segment(segment);
439        cx.notify();
440    }
441}
442
443impl Focusable for TimeFieldState {
444    fn focus_handle(&self, _: &App) -> FocusHandle {
445        self.focus_handle.clone()
446    }
447}
448impl EventEmitter<TimeFieldEvent> for TimeFieldState {}
449impl Render for TimeFieldState {
450    fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
451        Empty
452    }
453}
454
455/// State exposed to a [`TimeField`] segment slot for decoration.
456#[derive(Clone, Copy, Debug)]
457pub struct TimeFieldSegmentState {
458    segment: TimeSegment,
459    value: u32,
460    selected: bool,
461    disabled: bool,
462}
463
464impl TimeFieldSegmentState {
465    pub fn segment(&self) -> TimeSegment {
466        self.segment
467    }
468
469    /// The displayed value: the hour on the configured clock, the minute or
470    /// second, or `0` for AM and `1` for PM.
471    pub fn value(&self) -> u32 {
472        self.value
473    }
474
475    /// Whether keyboard editing applies to this segment: it is the selected
476    /// segment and the field has focus.
477    pub fn is_selected(&self) -> bool {
478        self.selected
479    }
480
481    pub fn is_disabled(&self) -> bool {
482        self.disabled
483    }
484}
485
486type SegmentRenderer =
487    Rc<dyn Fn(TimeFieldSegment, TimeFieldSegmentState, &mut Window, &mut App) -> AnyElement>;
488
489/// An unstyled, pre-wired segment passed to the [`TimeField`] segment slot.
490#[derive(IntoElement)]
491pub struct TimeFieldSegment {
492    base: crate::ObservedElement<gpui::Stateful<gpui::Div>>,
493    style: StyleRefinement,
494    children: Vec<AnyElement>,
495}
496
497impl TimeFieldSegment {
498    /// Remove the default label so a styled facade can provide custom content.
499    pub fn clear_children(mut self) -> Self {
500        self.children.clear();
501        self
502    }
503}
504
505impl ParentElement for TimeFieldSegment {
506    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
507        self.children.extend(elements);
508    }
509}
510impl Styled for TimeFieldSegment {
511    fn style(&mut self) -> &mut StyleRefinement {
512        &mut self.style
513    }
514}
515impl InteractiveElement for TimeFieldSegment {
516    fn interactivity(&mut self) -> &mut gpui::Interactivity {
517        self.base.interactivity()
518    }
519}
520impl StatefulInteractiveElement for TimeFieldSegment {}
521impl RenderOnce for TimeFieldSegment {
522    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
523        self.base.children(self.children).refine_style(&self.style)
524    }
525}
526
527/// Unstyled segmented time editor.
528///
529/// Base owns focus, the keyboard model and pointer segment selection; the
530/// presentation lays out and decorates the field with `Styled` and each
531/// segment through [`TimeField::render_segment`]. A `:` separates the numeric
532/// segments unless [`TimeField::separator`] says otherwise; the AM/PM segment
533/// follows without one.
534#[derive(IntoElement)]
535pub struct TimeField {
536    id: ElementId,
537    state: Entity<TimeFieldState>,
538    disabled: bool,
539    style: StyleRefinement,
540    segment: SegmentRenderer,
541    separator: SharedString,
542}
543
544impl TimeField {
545    pub fn new(id: impl Into<ElementId>, state: &Entity<TimeFieldState>) -> Self {
546        Self {
547            id: id.into(),
548            state: state.clone(),
549            disabled: false,
550            style: StyleRefinement::default(),
551            segment: Rc::new(|segment, _, _, _| segment.into_any_element()),
552            separator: SharedString::new_static(":"),
553        }
554    }
555
556    /// What stands between the field's segments (`:` by default), as a caller
557    /// showing a time in its own locale may need.
558    pub fn separator(mut self, separator: impl Into<SharedString>) -> Self {
559        self.separator = separator.into();
560        self
561    }
562
563    pub fn disabled(mut self, disabled: bool) -> Self {
564        self.disabled = disabled;
565        self
566    }
567
568    /// Decorate each segment. The segment already carries its label: two
569    /// digits, or `AM`/`PM`.
570    pub fn render_segment(
571        mut self,
572        render: impl Fn(TimeFieldSegment, TimeFieldSegmentState, &mut Window, &mut App) -> AnyElement
573        + 'static,
574    ) -> Self {
575        self.segment = Rc::new(render);
576        self
577    }
578}
579
580impl Styled for TimeField {
581    fn style(&mut self) -> &mut StyleRefinement {
582        &mut self.style
583    }
584}
585
586impl RenderOnce for TimeField {
587    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
588        let disabled = self.disabled;
589        let state = self.state.read(cx);
590        let focus_handle = state.focus_handle.clone();
591        let focused = focus_handle.is_focused(window);
592        let editor = state.editor;
593        let format = editor.precision.format(editor.hour_cycle);
594
595        let mut root = div()
596            .id(self.id.clone())
597            .test_support()
598            .role(Role::TimeInput)
599            .aria_value(SharedString::from(editor.time.format(format).to_string()))
600            .track_focus(&focus_handle.clone().tab_stop(!disabled))
601            .when(!disabled, |this| {
602                this.key_context(CONTEXT)
603                    .on_action(window.listener_for(&self.state, TimeFieldState::on_increment))
604                    .on_action(window.listener_for(&self.state, TimeFieldState::on_decrement))
605                    .on_action(window.listener_for(&self.state, TimeFieldState::on_delete))
606                    .on_action(window.listener_for(&self.state, TimeFieldState::on_select_left))
607                    .on_action(window.listener_for(&self.state, TimeFieldState::on_select_right))
608                    .on_action(window.listener_for(&self.state, TimeFieldState::on_next_column))
609                    .on_action(window.listener_for(&self.state, TimeFieldState::on_prev_column))
610                    .on_key_down(window.listener_for(&self.state, TimeFieldState::on_key_down))
611                    // A press on the field outside any segment focuses it and selects the
612                    // first segment; a press on a segment stops here and selects that one.
613                    .on_mouse_down(MouseButton::Left, {
614                        let state = self.state.clone();
615                        let focus_handle = focus_handle.clone();
616                        move |_: &MouseDownEvent, window, cx| {
617                            focus_handle.focus(window, cx);
618                            state.update(cx, |state, cx| {
619                                let first = state.editor.segments()[0];
620                                state.select_segment(first, cx);
621                            });
622                        }
623                    })
624            });
625
626        for (ix, segment) in editor.segments().iter().copied().enumerate() {
627            if ix > 0 && segment != TimeSegment::Period {
628                root = root.child(div().child(self.separator.clone()));
629            }
630            let segment_state = TimeFieldSegmentState {
631                segment,
632                value: editor.value(segment),
633                selected: focused && segment == editor.segment,
634                disabled,
635            };
636            let mut item = TimeFieldSegment {
637                base: div().id(segment.name()).test_support(),
638                style: StyleRefinement::default(),
639                children: vec![],
640            }
641            .child(editor.label(segment));
642            if !disabled {
643                let state = self.state.clone();
644                let focus_handle = focus_handle.clone();
645                item =
646                    item.on_mouse_down(MouseButton::Left, move |_: &MouseDownEvent, window, cx| {
647                        cx.stop_propagation();
648                        focus_handle.focus(window, cx);
649                        state.update(cx, |state, cx| state.select_segment(segment, cx));
650                    });
651            }
652            root = root.child((self.segment)(item, segment_state, window, cx));
653        }
654
655        root.refine_style(&self.style).a11y_disabled(disabled)
656    }
657}
658
659#[cfg(test)]
660mod tests {
661    use super::*;
662
663    fn hms(h: u32, m: u32, s: u32) -> NaiveTime {
664        NaiveTime::from_hms_opt(h, m, s).unwrap()
665    }
666
667    fn editor(precision: TimePrecision, hour_cycle: HourCycle) -> SegmentEditor {
668        let mut editor = SegmentEditor::new();
669        editor.set_precision(precision);
670        editor.set_hour_cycle(hour_cycle);
671        editor
672    }
673
674    fn type_keys(editor: &mut SegmentEditor, keys: &str) {
675        for ch in keys.chars() {
676            match ch {
677                'a' => editor.input_period(false),
678                'p' => editor.input_period(true),
679                _ => editor.input_digit(ch.to_digit(10).unwrap()),
680            };
681        }
682    }
683
684    #[test]
685    fn typing_fills_segments_and_advances() {
686        let mut editor = editor(TimePrecision::Second, HourCycle::H23);
687        type_keys(&mut editor, "093015");
688        assert_eq!(editor.time, hms(9, 30, 15));
689        // The last segment stays selected once it is complete.
690        assert_eq!(editor.segment, TimeSegment::Second);
691    }
692
693    #[test]
694    fn a_digit_that_cannot_start_two_digits_completes_the_segment() {
695        let mut editor = editor(TimePrecision::Minute, HourCycle::H23);
696        type_keys(&mut editor, "7");
697        assert_eq!(editor.time, hms(7, 0, 0));
698        assert_eq!(editor.segment, TimeSegment::Minute);
699        type_keys(&mut editor, "8");
700        assert_eq!(editor.time, hms(7, 8, 0));
701    }
702
703    #[test]
704    fn an_out_of_range_pair_restarts_from_the_second_digit() {
705        let mut editor = editor(TimePrecision::Minute, HourCycle::H23);
706        // 2 then 5 cannot form 25 hours, so 5 starts a new entry and completes it.
707        type_keys(&mut editor, "25");
708        assert_eq!(editor.time, hms(5, 0, 0));
709        assert_eq!(editor.segment, TimeSegment::Minute);
710    }
711
712    #[test]
713    fn stepping_wraps_without_carry() {
714        let mut editor = editor(TimePrecision::Minute, HourCycle::H23);
715        editor.set_time(hms(23, 59, 0));
716        editor.step(1);
717        assert_eq!(editor.time, hms(0, 59, 0));
718        editor.select_segment(TimeSegment::Minute);
719        editor.step(1);
720        assert_eq!(editor.time, hms(0, 0, 0));
721        editor.step(-1);
722        assert_eq!(editor.time, hms(0, 59, 0));
723    }
724
725    #[test]
726    fn segment_movement_stops_at_the_ends() {
727        let mut editor = editor(TimePrecision::Minute, HourCycle::H23);
728        assert!(!editor.move_segment(-1));
729        assert!(editor.move_segment(1));
730        assert!(!editor.move_segment(1));
731        assert!(!editor.select_segment(TimeSegment::Second));
732        assert!(!editor.select_segment(TimeSegment::Period));
733    }
734
735    #[test]
736    fn precision_truncates_seconds() {
737        let mut editor = editor(TimePrecision::Second, HourCycle::H23);
738        editor.set_time(hms(9, 30, 15));
739        editor.select_segment(TimeSegment::Second);
740        editor.set_precision(TimePrecision::Minute);
741        assert_eq!(editor.time, hms(9, 30, 0));
742        assert_eq!(editor.segment, TimeSegment::Hour);
743    }
744
745    #[test]
746    fn clearing_resets_only_the_selected_segment() {
747        let mut editor = editor(TimePrecision::Minute, HourCycle::H23);
748        editor.set_time(hms(9, 30, 0));
749        editor.select_segment(TimeSegment::Minute);
750        assert!(editor.clear_segment());
751        assert_eq!(editor.time, hms(9, 0, 0));
752    }
753
754    #[test]
755    fn twelve_hour_labels_map_midnight_and_noon_to_twelve() {
756        let mut editor = editor(TimePrecision::Minute, HourCycle::H12);
757        for (hour, label, period) in [
758            (0, "12", "AM"),
759            (9, "09", "AM"),
760            (12, "12", "PM"),
761            (23, "11", "PM"),
762        ] {
763            editor.set_time(hms(hour, 0, 0));
764            assert_eq!(editor.label(TimeSegment::Hour), label);
765            assert_eq!(editor.label(TimeSegment::Period), period);
766        }
767    }
768
769    #[test]
770    fn twelve_hour_typing_keeps_the_period_until_it_is_typed() {
771        let mut editor = editor(TimePrecision::Minute, HourCycle::H12);
772        // "1" may start 10–12, so it waits; "2" makes 12, which is midnight in AM.
773        type_keys(&mut editor, "12");
774        assert_eq!(editor.time, hms(0, 0, 0));
775        type_keys(&mut editor, "30p");
776        assert_eq!(editor.time, hms(12, 30, 0));
777        assert_eq!(editor.segment, TimeSegment::Period);
778        editor.select_segment(TimeSegment::Hour);
779        type_keys(&mut editor, "9");
780        assert_eq!(editor.time, hms(21, 30, 0));
781        // "00" is not an hour on a 12-hour clock.
782        editor.select_segment(TimeSegment::Hour);
783        type_keys(&mut editor, "00");
784        assert_eq!(editor.segment, TimeSegment::Hour);
785    }
786
787    #[test]
788    fn twelve_hour_stepping_wraps_within_the_period() {
789        let mut editor = editor(TimePrecision::Minute, HourCycle::H12);
790        editor.set_time(hms(11, 0, 0));
791        editor.step(1);
792        assert_eq!(editor.time, hms(0, 0, 0), "11 AM steps to 12 AM");
793        editor.select_segment(TimeSegment::Period);
794        editor.step(1);
795        assert_eq!(editor.time, hms(12, 0, 0));
796        editor.step(1);
797        assert_eq!(editor.time, hms(0, 0, 0));
798        assert!(!editor.input_digit(1), "digits do not edit the period");
799    }
800
801    #[gpui::test]
802    fn the_separator_is_a_colon_unless_set(cx: &mut gpui::TestAppContext) {
803        use gpui::AppContext as _;
804
805        let cx = cx.add_empty_window();
806        let state = cx.update(|window, cx| cx.new(|cx| TimeFieldState::new(window, cx)));
807        assert_eq!(TimeField::new("time", &state).separator.as_ref(), ":");
808        assert_eq!(
809            TimeField::new("time", &state)
810                .separator(".")
811                .separator
812                .as_ref(),
813            "."
814        );
815    }
816}