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)]
534pub struct TimeField {
535 id: ElementId,
536 state: Entity<TimeFieldState>,
537 disabled: bool,
538 style: StyleRefinement,
539 segment: SegmentRenderer,
540}
541
542impl TimeField {
543 pub fn new(id: impl Into<ElementId>, state: &Entity<TimeFieldState>) -> Self {
544 Self {
545 id: id.into(),
546 state: state.clone(),
547 disabled: false,
548 style: StyleRefinement::default(),
549 segment: Rc::new(|segment, _, _, _| segment.into_any_element()),
550 }
551 }
552
553 pub fn disabled(mut self, disabled: bool) -> Self {
554 self.disabled = disabled;
555 self
556 }
557
558 pub fn render_segment(
561 mut self,
562 render: impl Fn(TimeFieldSegment, TimeFieldSegmentState, &mut Window, &mut App) -> AnyElement
563 + 'static,
564 ) -> Self {
565 self.segment = Rc::new(render);
566 self
567 }
568}
569
570impl Styled for TimeField {
571 fn style(&mut self) -> &mut StyleRefinement {
572 &mut self.style
573 }
574}
575
576impl RenderOnce for TimeField {
577 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
578 let disabled = self.disabled;
579 let state = self.state.read(cx);
580 let focus_handle = state.focus_handle.clone();
581 let focused = focus_handle.is_focused(window);
582 let editor = state.editor;
583 let format = editor.precision.format(editor.hour_cycle);
584
585 let mut root = div()
586 .id(self.id.clone())
587 .test_support()
588 .role(Role::TimeInput)
589 .aria_value(SharedString::from(editor.time.format(format).to_string()))
590 .track_focus(&focus_handle.clone().tab_stop(!disabled))
591 .when(!disabled, |this| {
592 this.key_context(CONTEXT)
593 .on_action(window.listener_for(&self.state, TimeFieldState::on_increment))
594 .on_action(window.listener_for(&self.state, TimeFieldState::on_decrement))
595 .on_action(window.listener_for(&self.state, TimeFieldState::on_delete))
596 .on_action(window.listener_for(&self.state, TimeFieldState::on_select_left))
597 .on_action(window.listener_for(&self.state, TimeFieldState::on_select_right))
598 .on_action(window.listener_for(&self.state, TimeFieldState::on_next_column))
599 .on_action(window.listener_for(&self.state, TimeFieldState::on_prev_column))
600 .on_key_down(window.listener_for(&self.state, TimeFieldState::on_key_down))
601 });
602
603 for (ix, segment) in editor.segments().iter().copied().enumerate() {
604 if ix > 0 && segment != TimeSegment::Period {
605 root = root.child(div().child(":"));
606 }
607 let segment_state = TimeFieldSegmentState {
608 segment,
609 value: editor.value(segment),
610 selected: focused && segment == editor.segment,
611 disabled,
612 };
613 let mut item = TimeFieldSegment {
614 base: div().id(segment.name()).test_support(),
615 style: StyleRefinement::default(),
616 children: vec![],
617 }
618 .child(editor.label(segment));
619 if !disabled {
620 let state = self.state.clone();
621 let focus_handle = focus_handle.clone();
622 item =
623 item.on_mouse_down(MouseButton::Left, move |_: &MouseDownEvent, window, cx| {
624 focus_handle.focus(window, cx);
625 state.update(cx, |state, cx| state.select_segment(segment, cx));
626 });
627 }
628 root = root.child((self.segment)(item, segment_state, window, cx));
629 }
630
631 root.refine_style(&self.style).a11y_disabled(disabled)
632 }
633}
634
635#[cfg(test)]
636mod tests {
637 use super::*;
638
639 fn hms(h: u32, m: u32, s: u32) -> NaiveTime {
640 NaiveTime::from_hms_opt(h, m, s).unwrap()
641 }
642
643 fn editor(precision: TimePrecision, hour_cycle: HourCycle) -> SegmentEditor {
644 let mut editor = SegmentEditor::new();
645 editor.set_precision(precision);
646 editor.set_hour_cycle(hour_cycle);
647 editor
648 }
649
650 fn type_keys(editor: &mut SegmentEditor, keys: &str) {
651 for ch in keys.chars() {
652 match ch {
653 'a' => editor.input_period(false),
654 'p' => editor.input_period(true),
655 _ => editor.input_digit(ch.to_digit(10).unwrap()),
656 };
657 }
658 }
659
660 #[test]
661 fn typing_fills_segments_and_advances() {
662 let mut editor = editor(TimePrecision::Second, HourCycle::H23);
663 type_keys(&mut editor, "093015");
664 assert_eq!(editor.time, hms(9, 30, 15));
665 assert_eq!(editor.segment, TimeSegment::Second);
667 }
668
669 #[test]
670 fn a_digit_that_cannot_start_two_digits_completes_the_segment() {
671 let mut editor = editor(TimePrecision::Minute, HourCycle::H23);
672 type_keys(&mut editor, "7");
673 assert_eq!(editor.time, hms(7, 0, 0));
674 assert_eq!(editor.segment, TimeSegment::Minute);
675 type_keys(&mut editor, "8");
676 assert_eq!(editor.time, hms(7, 8, 0));
677 }
678
679 #[test]
680 fn an_out_of_range_pair_restarts_from_the_second_digit() {
681 let mut editor = editor(TimePrecision::Minute, HourCycle::H23);
682 type_keys(&mut editor, "25");
684 assert_eq!(editor.time, hms(5, 0, 0));
685 assert_eq!(editor.segment, TimeSegment::Minute);
686 }
687
688 #[test]
689 fn stepping_wraps_without_carry() {
690 let mut editor = editor(TimePrecision::Minute, HourCycle::H23);
691 editor.set_time(hms(23, 59, 0));
692 editor.step(1);
693 assert_eq!(editor.time, hms(0, 59, 0));
694 editor.select_segment(TimeSegment::Minute);
695 editor.step(1);
696 assert_eq!(editor.time, hms(0, 0, 0));
697 editor.step(-1);
698 assert_eq!(editor.time, hms(0, 59, 0));
699 }
700
701 #[test]
702 fn segment_movement_stops_at_the_ends() {
703 let mut editor = editor(TimePrecision::Minute, HourCycle::H23);
704 assert!(!editor.move_segment(-1));
705 assert!(editor.move_segment(1));
706 assert!(!editor.move_segment(1));
707 assert!(!editor.select_segment(TimeSegment::Second));
708 assert!(!editor.select_segment(TimeSegment::Period));
709 }
710
711 #[test]
712 fn precision_truncates_seconds() {
713 let mut editor = editor(TimePrecision::Second, HourCycle::H23);
714 editor.set_time(hms(9, 30, 15));
715 editor.select_segment(TimeSegment::Second);
716 editor.set_precision(TimePrecision::Minute);
717 assert_eq!(editor.time, hms(9, 30, 0));
718 assert_eq!(editor.segment, TimeSegment::Hour);
719 }
720
721 #[test]
722 fn clearing_resets_only_the_selected_segment() {
723 let mut editor = editor(TimePrecision::Minute, HourCycle::H23);
724 editor.set_time(hms(9, 30, 0));
725 editor.select_segment(TimeSegment::Minute);
726 assert!(editor.clear_segment());
727 assert_eq!(editor.time, hms(9, 0, 0));
728 }
729
730 #[test]
731 fn twelve_hour_labels_map_midnight_and_noon_to_twelve() {
732 let mut editor = editor(TimePrecision::Minute, HourCycle::H12);
733 for (hour, label, period) in [
734 (0, "12", "AM"),
735 (9, "09", "AM"),
736 (12, "12", "PM"),
737 (23, "11", "PM"),
738 ] {
739 editor.set_time(hms(hour, 0, 0));
740 assert_eq!(editor.label(TimeSegment::Hour), label);
741 assert_eq!(editor.label(TimeSegment::Period), period);
742 }
743 }
744
745 #[test]
746 fn twelve_hour_typing_keeps_the_period_until_it_is_typed() {
747 let mut editor = editor(TimePrecision::Minute, HourCycle::H12);
748 type_keys(&mut editor, "12");
750 assert_eq!(editor.time, hms(0, 0, 0));
751 type_keys(&mut editor, "30p");
752 assert_eq!(editor.time, hms(12, 30, 0));
753 assert_eq!(editor.segment, TimeSegment::Period);
754 editor.select_segment(TimeSegment::Hour);
755 type_keys(&mut editor, "9");
756 assert_eq!(editor.time, hms(21, 30, 0));
757 editor.select_segment(TimeSegment::Hour);
759 type_keys(&mut editor, "00");
760 assert_eq!(editor.segment, TimeSegment::Hour);
761 }
762
763 #[test]
764 fn twelve_hour_stepping_wraps_within_the_period() {
765 let mut editor = editor(TimePrecision::Minute, HourCycle::H12);
766 editor.set_time(hms(11, 0, 0));
767 editor.step(1);
768 assert_eq!(editor.time, hms(0, 0, 0), "11 AM steps to 12 AM");
769 editor.select_segment(TimeSegment::Period);
770 editor.step(1);
771 assert_eq!(editor.time, hms(12, 0, 0));
772 editor.step(1);
773 assert_eq!(editor.time, hms(0, 0, 0));
774 assert!(!editor.input_digit(1), "digits do not edit the period");
775 }
776}