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 actions::{SelectLeft, SelectNextColumn, SelectPrevColumn, SelectRight},
15 input::Delete,
16};
17
18const CONTEXT: &str = "TimeField";
19
20pub(crate) fn init(cx: &mut App) {
21 cx.bind_keys([
22 KeyBinding::new("up", Increment, Some(CONTEXT)),
23 KeyBinding::new("down", Decrement, Some(CONTEXT)),
24 KeyBinding::new("left", SelectLeft, Some(CONTEXT)),
25 KeyBinding::new("right", SelectRight, Some(CONTEXT)),
26 KeyBinding::new("tab", SelectNextColumn, Some(CONTEXT)),
27 KeyBinding::new("shift-tab", SelectPrevColumn, Some(CONTEXT)),
28 KeyBinding::new("backspace", Delete, Some(CONTEXT)),
29 KeyBinding::new("delete", Delete, Some(CONTEXT)),
30 ]);
31}
32
33#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
35pub enum TimePrecision {
36 #[default]
38 Minute,
39 Second,
41}
42
43impl TimePrecision {
44 pub fn format(self, hour_cycle: HourCycle) -> &'static str {
46 match (self, hour_cycle) {
47 (Self::Minute, HourCycle::H23) => "%H:%M",
48 (Self::Second, HourCycle::H23) => "%H:%M:%S",
49 (Self::Minute, HourCycle::H12) => "%I:%M %p",
50 (Self::Second, HourCycle::H12) => "%I:%M:%S %p",
51 }
52 }
53
54 pub fn truncate(self, time: NaiveTime) -> NaiveTime {
56 let second = match self {
57 Self::Minute => 0,
58 Self::Second => time.second(),
59 };
60 NaiveTime::from_hms_opt(time.hour(), time.minute(), second).unwrap_or(time)
61 }
62}
63
64#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
68pub enum HourCycle {
69 #[default]
71 H23,
72 H12,
74}
75
76#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
78pub enum TimeSegment {
79 Hour,
80 Minute,
81 Second,
82 Period,
84}
85
86impl TimeSegment {
87 fn name(self) -> &'static str {
88 match self {
89 Self::Hour => "hour",
90 Self::Minute => "minute",
91 Self::Second => "second",
92 Self::Period => "period",
93 }
94 }
95}
96
97#[derive(Clone, Copy, Debug, PartialEq, Eq)]
99pub enum TimeFieldEvent {
100 Change(NaiveTime),
102}
103
104#[derive(Clone, Copy, Debug, PartialEq, Eq)]
107struct SegmentEditor {
108 time: NaiveTime,
109 precision: TimePrecision,
110 hour_cycle: HourCycle,
111 segment: TimeSegment,
112 pending_digit: Option<u32>,
114}
115
116impl SegmentEditor {
117 fn new() -> Self {
118 Self {
119 time: NaiveTime::MIN,
120 precision: TimePrecision::default(),
121 hour_cycle: HourCycle::default(),
122 segment: TimeSegment::Hour,
123 pending_digit: None,
124 }
125 }
126
127 fn segments(&self) -> &'static [TimeSegment] {
129 use TimeSegment::*;
130 match (self.precision, self.hour_cycle) {
131 (TimePrecision::Minute, HourCycle::H23) => &[Hour, Minute],
132 (TimePrecision::Second, HourCycle::H23) => &[Hour, Minute, Second],
133 (TimePrecision::Minute, HourCycle::H12) => &[Hour, Minute, Period],
134 (TimePrecision::Second, HourCycle::H12) => &[Hour, Minute, Second, Period],
135 }
136 }
137
138 fn bounds(&self, segment: TimeSegment) -> (u32, u32) {
140 match (segment, self.hour_cycle) {
141 (TimeSegment::Hour, HourCycle::H23) => (0, 23),
142 (TimeSegment::Hour, HourCycle::H12) => (1, 12),
143 (TimeSegment::Minute | TimeSegment::Second, _) => (0, 59),
144 (TimeSegment::Period, _) => (0, 1),
145 }
146 }
147
148 fn value(&self, segment: TimeSegment) -> u32 {
151 let hour = self.time.hour();
152 match (segment, self.hour_cycle) {
153 (TimeSegment::Hour, HourCycle::H23) => hour,
154 (TimeSegment::Hour, HourCycle::H12) => (hour + 11) % 12 + 1,
155 (TimeSegment::Minute, _) => self.time.minute(),
156 (TimeSegment::Second, _) => self.time.second(),
157 (TimeSegment::Period, _) => hour / 12,
158 }
159 }
160
161 fn label(&self, segment: TimeSegment) -> SharedString {
162 match segment {
163 TimeSegment::Period if self.value(segment) == 0 => "AM".into(),
164 TimeSegment::Period => "PM".into(),
165 _ => format!("{:02}", self.value(segment)).into(),
166 }
167 }
168
169 fn with_value(&self, segment: TimeSegment, value: u32) -> NaiveTime {
170 let time = self.time;
171 let pm_offset = time.hour() / 12 * 12;
172 match (segment, self.hour_cycle) {
173 (TimeSegment::Hour, HourCycle::H23) => time.with_hour(value),
174 (TimeSegment::Hour, HourCycle::H12) => time.with_hour(value % 12 + pm_offset),
176 (TimeSegment::Minute, _) => time.with_minute(value),
177 (TimeSegment::Second, _) => time.with_second(value),
178 (TimeSegment::Period, _) => time.with_hour(time.hour() % 12 + value * 12),
179 }
180 .unwrap_or(time)
181 }
182
183 fn set_precision(&mut self, precision: TimePrecision) {
184 self.precision = precision;
185 self.time = precision.truncate(self.time);
186 self.reset_segment();
187 }
188
189 fn set_hour_cycle(&mut self, hour_cycle: HourCycle) {
190 self.hour_cycle = hour_cycle;
191 self.reset_segment();
192 }
193
194 fn reset_segment(&mut self) {
195 if !self.segments().contains(&self.segment) {
196 self.segment = TimeSegment::Hour;
197 }
198 self.pending_digit = None;
199 }
200
201 fn set_time(&mut self, time: NaiveTime) -> bool {
202 let time = self.precision.truncate(time);
203 if self.time == time {
204 return false;
206 }
207 self.time = time;
208 self.pending_digit = None;
209 true
210 }
211
212 fn select_segment(&mut self, segment: TimeSegment) -> bool {
213 if !self.segments().contains(&segment) {
214 return false;
215 }
216 self.segment = segment;
217 self.pending_digit = None;
218 true
219 }
220
221 fn move_segment(&mut self, offset: isize) -> bool {
223 let segments = self.segments();
224 let ix = segments
225 .iter()
226 .position(|segment| *segment == self.segment)
227 .unwrap_or(0);
228 let Some(next) = ix
229 .checked_add_signed(offset)
230 .and_then(|ix| segments.get(ix))
231 else {
232 return false;
233 };
234 self.select_segment(*next)
235 }
236
237 fn step(&mut self, delta: i32) -> bool {
239 self.pending_digit = None;
240 let (min, max) = self.bounds(self.segment);
241 let span = (max - min + 1) as i32;
242 let value = (self.value(self.segment) as i32 - min as i32 + delta).rem_euclid(span);
243 self.replace_segment(value as u32 + min)
244 }
245
246 fn input_digit(&mut self, digit: u32) -> bool {
247 if self.segment == TimeSegment::Period {
248 return false;
249 }
250 let (min, max) = self.bounds(self.segment);
251 let (value, complete) = match self.pending_digit.take() {
252 Some(first) if (min..=max).contains(&(first * 10 + digit)) => {
253 (first * 10 + digit, true)
254 }
255 _ => (digit, digit * 10 > max),
257 };
258 if !complete {
259 self.pending_digit = Some(digit);
260 }
261 let changed = self.replace_segment(value);
262 if complete {
263 self.move_segment(1);
264 }
265 changed
266 }
267
268 fn input_period(&mut self, pm: bool) -> bool {
270 if self.segment != TimeSegment::Period {
271 return false;
272 }
273 self.replace_segment(pm as u32)
274 }
275
276 fn clear_segment(&mut self) -> bool {
279 self.pending_digit = None;
280 self.replace_segment(0)
281 }
282
283 fn replace_segment(&mut self, value: u32) -> bool {
284 let time = self.with_value(self.segment, value);
285 let changed = time != self.time;
286 self.time = time;
287 changed
288 }
289}
290
291pub struct TimeFieldState {
303 focus_handle: FocusHandle,
304 editor: SegmentEditor,
305}
306
307impl TimeFieldState {
308 pub fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
309 Self {
310 focus_handle: cx.focus_handle().tab_stop(true),
311 editor: SegmentEditor::new(),
312 }
313 }
314
315 pub fn precision(mut self, precision: TimePrecision) -> Self {
317 self.editor.set_precision(precision);
318 self
319 }
320
321 pub fn set_precision(
322 &mut self,
323 precision: TimePrecision,
324 _: &mut Window,
325 cx: &mut Context<Self>,
326 ) {
327 if self.editor.precision != precision {
328 self.editor.set_precision(precision);
329 cx.notify();
330 }
331 }
332
333 pub fn hour_cycle(mut self, hour_cycle: HourCycle) -> Self {
335 self.editor.set_hour_cycle(hour_cycle);
336 self
337 }
338
339 pub fn set_hour_cycle(
340 &mut self,
341 hour_cycle: HourCycle,
342 _: &mut Window,
343 cx: &mut Context<Self>,
344 ) {
345 if self.editor.hour_cycle != hour_cycle {
346 self.editor.set_hour_cycle(hour_cycle);
347 cx.notify();
348 }
349 }
350
351 pub fn time(&self) -> NaiveTime {
352 self.editor.time
353 }
354
355 pub fn set_time(&mut self, time: NaiveTime, _: &mut Window, cx: &mut Context<Self>) {
357 if self.editor.set_time(time) {
358 cx.notify();
359 }
360 }
361
362 pub fn selected_segment(&self) -> TimeSegment {
364 self.editor.segment
365 }
366
367 pub fn focus(&self, window: &mut Window, cx: &mut App) {
368 self.focus_handle.focus(window, cx);
369 }
370
371 fn edit(&mut self, cx: &mut Context<Self>, edit: impl FnOnce(&mut SegmentEditor) -> bool) {
372 if edit(&mut self.editor) {
373 cx.emit(TimeFieldEvent::Change(self.editor.time));
374 }
375 cx.notify();
376 }
377
378 fn on_increment(&mut self, _: &Increment, _: &mut Window, cx: &mut Context<Self>) {
379 self.edit(cx, |editor| editor.step(1));
380 }
381
382 fn on_decrement(&mut self, _: &Decrement, _: &mut Window, cx: &mut Context<Self>) {
383 self.edit(cx, |editor| editor.step(-1));
384 }
385
386 fn on_delete(&mut self, _: &Delete, _: &mut Window, cx: &mut Context<Self>) {
387 self.edit(cx, SegmentEditor::clear_segment);
388 }
389
390 fn on_select_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context<Self>) {
391 self.editor.move_segment(-1);
392 cx.notify();
393 }
394
395 fn on_select_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut Context<Self>) {
396 self.editor.move_segment(1);
397 cx.notify();
398 }
399
400 fn on_next_column(&mut self, _: &SelectNextColumn, _: &mut Window, cx: &mut Context<Self>) {
402 if self.editor.move_segment(1) {
403 cx.notify();
404 } else {
405 cx.propagate();
406 }
407 }
408
409 fn on_prev_column(&mut self, _: &SelectPrevColumn, _: &mut Window, cx: &mut Context<Self>) {
410 if self.editor.move_segment(-1) {
411 cx.notify();
412 } else {
413 cx.propagate();
414 }
415 }
416
417 fn on_key_down(&mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context<Self>) {
418 let keystroke = &event.keystroke;
419 if keystroke.modifiers.modified() || keystroke.key.chars().count() != 1 {
420 return;
421 }
422 let Some(ch) = keystroke.key.chars().next() else {
423 return;
424 };
425 let edit: Box<dyn FnOnce(&mut SegmentEditor) -> bool> = match ch {
426 '0'..='9' => Box::new(move |editor| editor.input_digit(ch as u32 - '0' as u32)),
427 'a' | 'A' => Box::new(|editor| editor.input_period(false)),
428 'p' | 'P' => Box::new(|editor| editor.input_period(true)),
429 _ => return,
430 };
431 window.prevent_default();
432 cx.stop_propagation();
433 self.edit(cx, edit);
434 }
435
436 fn select_segment(&mut self, segment: TimeSegment, cx: &mut Context<Self>) {
437 self.editor.select_segment(segment);
438 cx.notify();
439 }
440}
441
442impl Focusable for TimeFieldState {
443 fn focus_handle(&self, _: &App) -> FocusHandle {
444 self.focus_handle.clone()
445 }
446}
447impl EventEmitter<TimeFieldEvent> for TimeFieldState {}
448impl Render for TimeFieldState {
449 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
450 Empty
451 }
452}
453
454#[derive(Clone, Copy, Debug)]
456pub struct TimeFieldSegmentState {
457 segment: TimeSegment,
458 value: u32,
459 selected: bool,
460 disabled: bool,
461}
462
463impl TimeFieldSegmentState {
464 pub fn segment(&self) -> TimeSegment {
465 self.segment
466 }
467
468 pub fn value(&self) -> u32 {
471 self.value
472 }
473
474 pub fn is_selected(&self) -> bool {
477 self.selected
478 }
479
480 pub fn is_disabled(&self) -> bool {
481 self.disabled
482 }
483}
484
485type SegmentRenderer =
486 Rc<dyn Fn(TimeFieldSegment, TimeFieldSegmentState, &mut Window, &mut App) -> AnyElement>;
487
488#[derive(IntoElement)]
490pub struct TimeFieldSegment {
491 base: crate::ObservedElement<gpui::Stateful<gpui::Div>>,
492 style: StyleRefinement,
493 children: Vec<AnyElement>,
494}
495
496impl TimeFieldSegment {
497 pub fn clear_children(mut self) -> Self {
499 self.children.clear();
500 self
501 }
502}
503
504impl ParentElement for TimeFieldSegment {
505 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
506 self.children.extend(elements);
507 }
508}
509impl Styled for TimeFieldSegment {
510 fn style(&mut self) -> &mut StyleRefinement {
511 &mut self.style
512 }
513}
514impl InteractiveElement for TimeFieldSegment {
515 fn interactivity(&mut self) -> &mut gpui::Interactivity {
516 self.base.interactivity()
517 }
518}
519impl StatefulInteractiveElement for TimeFieldSegment {}
520impl RenderOnce for TimeFieldSegment {
521 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
522 self.base.children(self.children).refine_style(&self.style)
523 }
524}
525
526#[derive(IntoElement)]
533pub struct TimeField {
534 id: ElementId,
535 state: Entity<TimeFieldState>,
536 disabled: bool,
537 style: StyleRefinement,
538 segment: SegmentRenderer,
539}
540
541impl TimeField {
542 pub fn new(id: impl Into<ElementId>, state: &Entity<TimeFieldState>) -> Self {
543 Self {
544 id: id.into(),
545 state: state.clone(),
546 disabled: false,
547 style: StyleRefinement::default(),
548 segment: Rc::new(|segment, _, _, _| segment.into_any_element()),
549 }
550 }
551
552 pub fn disabled(mut self, disabled: bool) -> Self {
553 self.disabled = disabled;
554 self
555 }
556
557 pub fn render_segment(
560 mut self,
561 render: impl Fn(TimeFieldSegment, TimeFieldSegmentState, &mut Window, &mut App) -> AnyElement
562 + 'static,
563 ) -> Self {
564 self.segment = Rc::new(render);
565 self
566 }
567}
568
569impl Styled for TimeField {
570 fn style(&mut self) -> &mut StyleRefinement {
571 &mut self.style
572 }
573}
574
575impl RenderOnce for TimeField {
576 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
577 let disabled = self.disabled;
578 let state = self.state.read(cx);
579 let focus_handle = state.focus_handle.clone();
580 let focused = focus_handle.is_focused(window);
581 let editor = state.editor;
582 let format = editor.precision.format(editor.hour_cycle);
583
584 let mut root = div()
585 .id(self.id.clone())
586 .test_support()
587 .role(Role::TimeInput)
588 .aria_value(SharedString::from(editor.time.format(format).to_string()))
589 .track_focus(&focus_handle.clone().tab_stop(!disabled))
590 .when(!disabled, |this| {
591 this.key_context(CONTEXT)
592 .on_action(window.listener_for(&self.state, TimeFieldState::on_increment))
593 .on_action(window.listener_for(&self.state, TimeFieldState::on_decrement))
594 .on_action(window.listener_for(&self.state, TimeFieldState::on_delete))
595 .on_action(window.listener_for(&self.state, TimeFieldState::on_select_left))
596 .on_action(window.listener_for(&self.state, TimeFieldState::on_select_right))
597 .on_action(window.listener_for(&self.state, TimeFieldState::on_next_column))
598 .on_action(window.listener_for(&self.state, TimeFieldState::on_prev_column))
599 .on_key_down(window.listener_for(&self.state, TimeFieldState::on_key_down))
600 });
601
602 for (ix, segment) in editor.segments().iter().copied().enumerate() {
603 if ix > 0 && segment != TimeSegment::Period {
604 root = root.child(div().child(":"));
605 }
606 let segment_state = TimeFieldSegmentState {
607 segment,
608 value: editor.value(segment),
609 selected: focused && segment == editor.segment,
610 disabled,
611 };
612 let mut item = TimeFieldSegment {
613 base: div().id(segment.name()).test_support(),
614 style: StyleRefinement::default(),
615 children: vec![],
616 }
617 .child(editor.label(segment));
618 if !disabled {
619 let state = self.state.clone();
620 let focus_handle = focus_handle.clone();
621 item =
622 item.on_mouse_down(MouseButton::Left, move |_: &MouseDownEvent, window, cx| {
623 focus_handle.focus(window, cx);
624 state.update(cx, |state, cx| state.select_segment(segment, cx));
625 });
626 }
627 root = root.child((self.segment)(item, segment_state, window, cx));
628 }
629
630 root.refine_style(&self.style)
631 }
632}
633
634#[cfg(test)]
635mod tests {
636 use super::*;
637
638 fn hms(h: u32, m: u32, s: u32) -> NaiveTime {
639 NaiveTime::from_hms_opt(h, m, s).unwrap()
640 }
641
642 fn editor(precision: TimePrecision, hour_cycle: HourCycle) -> SegmentEditor {
643 let mut editor = SegmentEditor::new();
644 editor.set_precision(precision);
645 editor.set_hour_cycle(hour_cycle);
646 editor
647 }
648
649 fn type_keys(editor: &mut SegmentEditor, keys: &str) {
650 for ch in keys.chars() {
651 match ch {
652 'a' => editor.input_period(false),
653 'p' => editor.input_period(true),
654 _ => editor.input_digit(ch.to_digit(10).unwrap()),
655 };
656 }
657 }
658
659 #[test]
660 fn typing_fills_segments_and_advances() {
661 let mut editor = editor(TimePrecision::Second, HourCycle::H23);
662 type_keys(&mut editor, "093015");
663 assert_eq!(editor.time, hms(9, 30, 15));
664 assert_eq!(editor.segment, TimeSegment::Second);
666 }
667
668 #[test]
669 fn a_digit_that_cannot_start_two_digits_completes_the_segment() {
670 let mut editor = editor(TimePrecision::Minute, HourCycle::H23);
671 type_keys(&mut editor, "7");
672 assert_eq!(editor.time, hms(7, 0, 0));
673 assert_eq!(editor.segment, TimeSegment::Minute);
674 type_keys(&mut editor, "8");
675 assert_eq!(editor.time, hms(7, 8, 0));
676 }
677
678 #[test]
679 fn an_out_of_range_pair_restarts_from_the_second_digit() {
680 let mut editor = editor(TimePrecision::Minute, HourCycle::H23);
681 type_keys(&mut editor, "25");
683 assert_eq!(editor.time, hms(5, 0, 0));
684 assert_eq!(editor.segment, TimeSegment::Minute);
685 }
686
687 #[test]
688 fn stepping_wraps_without_carry() {
689 let mut editor = editor(TimePrecision::Minute, HourCycle::H23);
690 editor.set_time(hms(23, 59, 0));
691 editor.step(1);
692 assert_eq!(editor.time, hms(0, 59, 0));
693 editor.select_segment(TimeSegment::Minute);
694 editor.step(1);
695 assert_eq!(editor.time, hms(0, 0, 0));
696 editor.step(-1);
697 assert_eq!(editor.time, hms(0, 59, 0));
698 }
699
700 #[test]
701 fn segment_movement_stops_at_the_ends() {
702 let mut editor = editor(TimePrecision::Minute, HourCycle::H23);
703 assert!(!editor.move_segment(-1));
704 assert!(editor.move_segment(1));
705 assert!(!editor.move_segment(1));
706 assert!(!editor.select_segment(TimeSegment::Second));
707 assert!(!editor.select_segment(TimeSegment::Period));
708 }
709
710 #[test]
711 fn precision_truncates_seconds() {
712 let mut editor = editor(TimePrecision::Second, HourCycle::H23);
713 editor.set_time(hms(9, 30, 15));
714 editor.select_segment(TimeSegment::Second);
715 editor.set_precision(TimePrecision::Minute);
716 assert_eq!(editor.time, hms(9, 30, 0));
717 assert_eq!(editor.segment, TimeSegment::Hour);
718 }
719
720 #[test]
721 fn clearing_resets_only_the_selected_segment() {
722 let mut editor = editor(TimePrecision::Minute, HourCycle::H23);
723 editor.set_time(hms(9, 30, 0));
724 editor.select_segment(TimeSegment::Minute);
725 assert!(editor.clear_segment());
726 assert_eq!(editor.time, hms(9, 0, 0));
727 }
728
729 #[test]
730 fn twelve_hour_labels_map_midnight_and_noon_to_twelve() {
731 let mut editor = editor(TimePrecision::Minute, HourCycle::H12);
732 for (hour, label, period) in [
733 (0, "12", "AM"),
734 (9, "09", "AM"),
735 (12, "12", "PM"),
736 (23, "11", "PM"),
737 ] {
738 editor.set_time(hms(hour, 0, 0));
739 assert_eq!(editor.label(TimeSegment::Hour), label);
740 assert_eq!(editor.label(TimeSegment::Period), period);
741 }
742 }
743
744 #[test]
745 fn twelve_hour_typing_keeps_the_period_until_it_is_typed() {
746 let mut editor = editor(TimePrecision::Minute, HourCycle::H12);
747 type_keys(&mut editor, "12");
749 assert_eq!(editor.time, hms(0, 0, 0));
750 type_keys(&mut editor, "30p");
751 assert_eq!(editor.time, hms(12, 30, 0));
752 assert_eq!(editor.segment, TimeSegment::Period);
753 editor.select_segment(TimeSegment::Hour);
754 type_keys(&mut editor, "9");
755 assert_eq!(editor.time, hms(21, 30, 0));
756 editor.select_segment(TimeSegment::Hour);
758 type_keys(&mut editor, "00");
759 assert_eq!(editor.segment, TimeSegment::Hour);
760 }
761
762 #[test]
763 fn twelve_hour_stepping_wraps_within_the_period() {
764 let mut editor = editor(TimePrecision::Minute, HourCycle::H12);
765 editor.set_time(hms(11, 0, 0));
766 editor.step(1);
767 assert_eq!(editor.time, hms(0, 0, 0), "11 AM steps to 12 AM");
768 editor.select_segment(TimeSegment::Period);
769 editor.step(1);
770 assert_eq!(editor.time, hms(12, 0, 0));
771 editor.step(1);
772 assert_eq!(editor.time, hms(0, 0, 0));
773 assert!(!editor.input_digit(1), "digits do not edit the period");
774 }
775}