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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
36pub enum TimePrecision {
37 #[default]
39 Minute,
40 Second,
42}
43
44impl TimePrecision {
45 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 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
69pub enum HourCycle {
70 #[default]
72 H23,
73 H12,
75}
76
77#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
79pub enum TimeSegment {
80 Hour,
81 Minute,
82 Second,
83 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
100pub enum TimeFieldEvent {
101 Change(NaiveTime),
103}
104
105#[derive(Clone, Copy, Debug, PartialEq, Eq)]
108struct SegmentEditor {
109 time: NaiveTime,
110 precision: TimePrecision,
111 hour_cycle: HourCycle,
112 segment: TimeSegment,
113 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 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 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 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 (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 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 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 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 _ => (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 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 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
292pub 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 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 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 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 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 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#[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 pub fn value(&self) -> u32 {
472 self.value
473 }
474
475 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#[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 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#[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 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 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 .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 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 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 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 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}