1use std::{
7 cell::RefCell,
8 collections::HashMap,
9 rc::Rc,
10 sync::{Arc, OnceLock},
11};
12
13use gpui::{
14 div, prelude::*, px, App, ElementId, Entity, InteractiveElement, IntoElement, Pixels,
15 RenderOnce, SharedString, Styled, Window,
16};
17use herogpui_core::{element_id, FieldVariant};
18use herogpui_theme::ActiveTheme;
19
20use crate::{
21 a11y::{self, A11y as _},
22 icons, util,
23};
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub enum HourCycle {
28 H12,
30 H24,
32}
33
34impl Default for HourCycle {
35 fn default() -> Self {
36 system_time_format().hour_cycle
37 }
38}
39
40impl HourCycle {
41 pub const ALL: [HourCycle; 2] = [HourCycle::H12, HourCycle::H24];
43
44 pub fn label(self) -> &'static str {
46 match self {
47 HourCycle::H12 => "12-hour",
48 HourCycle::H24 => "24-hour",
49 }
50 }
51}
52
53#[derive(Clone, Debug, PartialEq, Eq)]
54struct RegionalTimeFormat {
55 hour_cycle: HourCycle,
56 minute_pattern: RegionalTimePattern,
57 second_pattern: RegionalTimePattern,
58 am: String,
59 pm: String,
60}
61
62#[derive(Clone, Debug, PartialEq, Eq)]
63pub(crate) struct RegionalTimePattern {
64 pub(crate) order: Vec<TimeSegment>,
65 pub(crate) literals: Vec<String>,
66 pub(crate) hour_has_leading_zero: bool,
67 pub(crate) hour_zero_based: bool,
68 pub(crate) minute_has_leading_zero: bool,
69 pub(crate) second_has_leading_zero: bool,
70 pub(crate) am: String,
71 pub(crate) pm: String,
72}
73
74impl RegionalTimeFormat {
75 fn for_locale(locale: &str) -> Option<Self> {
76 Self::for_locale_with_cycle(locale, None)
77 }
78
79 fn for_locale_with_cycle(locale: &str, hour_cycle: Option<HourCycle>) -> Option<Self> {
80 use icu_datetime::{
81 fieldsets,
82 input::Time as IcuTime,
83 provider::{
84 fields::{FieldLength, FieldSymbol, Hour as IcuHour},
85 pattern::{reference, runtime, PatternItem},
86 },
87 DateTimeFormatterPreferences, NoCalendarFormatter,
88 };
89 use icu_locale_core::{
90 preferences::extensions::unicode::keywords::HourCycle as IcuHourCycle,
91 Locale as IcuLocale,
92 };
93
94 let locale = locale.parse::<IcuLocale>().ok()?;
95 let mut preferences = DateTimeFormatterPreferences::from(locale);
96 if let Some(hour_cycle) = hour_cycle {
97 preferences.hour_cycle = Some(match hour_cycle {
98 HourCycle::H12 => IcuHourCycle::Clock12,
99 HourCycle::H24 => IcuHourCycle::Clock24,
100 });
101 }
102 let parse_pattern = |precision| {
103 let formatter = NoCalendarFormatter::try_new(preferences, precision).ok()?;
104 let formatted = formatter.format(&IcuTime::start_of_day());
105 let pattern: runtime::Pattern<'_> = formatted.pattern().into();
106 let pattern = reference::Pattern::from(&pattern);
107 let mut order = Vec::with_capacity(4);
108 let mut literals = Vec::with_capacity(5);
109 let mut literal = String::new();
110 let mut hour_has_leading_zero = None;
111 let mut hour_zero_based = None;
112 let mut hour_cycle = None;
113 let mut minute_has_leading_zero = None;
114 let mut second_has_leading_zero = None;
115 for item in pattern.into_items() {
116 let field = match item {
117 PatternItem::Literal(ch) => {
118 literal.push(ch);
119 continue;
120 }
121 PatternItem::Field(field) => field,
122 };
123 let segment = match field.symbol {
124 FieldSymbol::Hour(hour) => {
125 hour_has_leading_zero = Some(field.length == FieldLength::Two);
126 hour_zero_based = Some(hour == IcuHour::H11);
127 hour_cycle = Some(match hour {
128 IcuHour::H11 | IcuHour::H12 => HourCycle::H12,
129 IcuHour::H23 => HourCycle::H24,
130 });
131 TimeSegment::Hour
132 }
133 FieldSymbol::Minute => {
134 minute_has_leading_zero = Some(field.length == FieldLength::Two);
135 TimeSegment::Minute
136 }
137 FieldSymbol::Second(_) => {
138 second_has_leading_zero = Some(field.length == FieldLength::Two);
139 TimeSegment::Second
140 }
141 FieldSymbol::DayPeriod(_) => TimeSegment::Meridiem,
142 _ => return None,
143 };
144 if order.contains(&segment) {
145 return None;
146 }
147 literals.push(std::mem::take(&mut literal));
148 order.push(segment);
149 }
150 literals.push(literal);
151 Some((
152 RegionalTimePattern {
153 order,
154 literals,
155 hour_has_leading_zero: hour_has_leading_zero?,
156 hour_zero_based: hour_zero_based?,
157 minute_has_leading_zero: minute_has_leading_zero?,
158 second_has_leading_zero: second_has_leading_zero.unwrap_or(false),
159 am: String::new(),
160 pm: String::new(),
161 },
162 hour_cycle?,
163 ))
164 };
165
166 let (minute_pattern, hour_cycle) = parse_pattern(fieldsets::T::hm())?;
167 let (second_pattern, second_hour_cycle) = parse_pattern(fieldsets::T::hms())?;
168 if hour_cycle != second_hour_cycle {
169 return None;
170 }
171 let (am, pm) = if hour_cycle == HourCycle::H12 {
172 let formatter = NoCalendarFormatter::try_new(preferences, fieldsets::T::hm()).ok()?;
173 (
174 minute_pattern
175 .day_period_from_formatted(
176 &formatter
177 .format(&IcuTime::try_new(1, 5, 0, 0).ok()?)
178 .to_string(),
179 )
180 .unwrap_or_else(|| "AM".to_owned()),
181 minute_pattern
182 .day_period_from_formatted(
183 &formatter
184 .format(&IcuTime::try_new(13, 5, 0, 0).ok()?)
185 .to_string(),
186 )
187 .unwrap_or_else(|| "PM".to_owned()),
188 )
189 } else {
190 ("AM".to_owned(), "PM".to_owned())
191 };
192 Some(Self {
193 hour_cycle,
194 minute_pattern,
195 second_pattern,
196 am,
197 pm,
198 })
199 }
200
201 fn for_preferences(tags: &[String]) -> Option<Self> {
202 tags.iter().find_map(|tag| Self::for_locale(tag))
203 }
204}
205
206fn system_time_format() -> &'static RegionalTimeFormat {
207 static SYSTEM_TIME_FORMAT: OnceLock<RegionalTimeFormat> = OnceLock::new();
208 SYSTEM_TIME_FORMAT.get_or_init(|| {
209 RegionalTimeFormat::for_preferences(&crate::date_constraints::system_locale_tags())
210 .unwrap_or(RegionalTimeFormat {
211 hour_cycle: HourCycle::H24,
212 minute_pattern: RegionalTimePattern::fallback(TimeGranularity::Minute, false),
213 second_pattern: RegionalTimePattern::fallback(TimeGranularity::Second, false),
214 am: "AM".to_owned(),
215 pm: "PM".to_owned(),
216 })
217 })
218}
219
220fn system_time_format_for_cycle(hour_cycle: HourCycle) -> &'static RegionalTimeFormat {
221 static SYSTEM_H12_FORMAT: OnceLock<RegionalTimeFormat> = OnceLock::new();
222 static SYSTEM_H24_FORMAT: OnceLock<RegionalTimeFormat> = OnceLock::new();
223 let cache = match hour_cycle {
224 HourCycle::H12 => &SYSTEM_H12_FORMAT,
225 HourCycle::H24 => &SYSTEM_H24_FORMAT,
226 };
227 cache.get_or_init(|| {
228 crate::date_constraints::system_locale_tags()
229 .iter()
230 .find_map(|tag| RegionalTimeFormat::for_locale_with_cycle(tag, Some(hour_cycle)))
231 .unwrap_or(RegionalTimeFormat {
232 hour_cycle,
233 minute_pattern: RegionalTimePattern::fallback(
234 TimeGranularity::Minute,
235 hour_cycle == HourCycle::H12,
236 ),
237 second_pattern: RegionalTimePattern::fallback(
238 TimeGranularity::Second,
239 hour_cycle == HourCycle::H12,
240 ),
241 am: "AM".to_owned(),
242 pm: "PM".to_owned(),
243 })
244 })
245}
246
247impl RegionalTimeFormat {
248 fn pattern(&self, granularity: TimeGranularity) -> RegionalTimePattern {
249 let mut pattern = match granularity {
250 TimeGranularity::Hour | TimeGranularity::Minute => self.minute_pattern.clone(),
251 TimeGranularity::Second => self.second_pattern.clone(),
252 };
253 if granularity == TimeGranularity::Hour {
254 let mut order = Vec::with_capacity(2);
255 let mut literals = Vec::with_capacity(3);
256 for (index, segment) in pattern.order.iter().copied().enumerate() {
257 if matches!(segment, TimeSegment::Hour | TimeSegment::Meridiem) {
258 literals.push(pattern.literals[index].clone());
259 order.push(segment);
260 }
261 }
262 literals.push(String::new());
263 pattern.order = order;
264 pattern.literals = literals;
265 }
266 pattern.am.clone_from(&self.am);
267 pattern.pm.clone_from(&self.pm);
268 pattern
269 }
270}
271
272impl RegionalTimePattern {
273 fn fallback(granularity: TimeGranularity, twelve_hour: bool) -> Self {
274 let order = TimeSegment::order(granularity, twelve_hour);
275 let literals = order
276 .iter()
277 .enumerate()
278 .map(|(index, segment)| {
279 if index == 0 {
280 String::new()
281 } else if *segment == TimeSegment::Meridiem {
282 " ".to_owned()
283 } else {
284 ":".to_owned()
285 }
286 })
287 .chain(std::iter::once(String::new()))
288 .collect();
289 Self {
290 order,
291 literals,
292 hour_has_leading_zero: !twelve_hour,
293 hour_zero_based: false,
294 minute_has_leading_zero: true,
295 second_has_leading_zero: true,
296 am: "AM".to_owned(),
297 pm: "PM".to_owned(),
298 }
299 }
300
301 fn day_period_from_formatted(&self, formatted: &str) -> Option<String> {
302 let index = self
303 .order
304 .iter()
305 .position(|segment| *segment == TimeSegment::Meridiem)?;
306 let name = if index == 0 {
307 let formatted = formatted.strip_prefix(self.literals.first()?.as_str())?;
308 let numeric_start = formatted
309 .char_indices()
310 .find_map(|(index, ch)| ch.is_numeric().then_some(index))?;
311 formatted[..numeric_start].strip_suffix(self.literals.get(1)?.as_str())?
312 } else if index + 1 == self.order.len() {
313 let numeric_end = formatted
314 .char_indices()
315 .filter_map(|(index, ch)| ch.is_numeric().then_some(index + ch.len_utf8()))
316 .next_back()?;
317 formatted[numeric_end..]
318 .strip_prefix(self.literals.get(index)?.as_str())?
319 .strip_suffix(self.literals.get(index + 1)?.as_str())?
320 } else {
321 return None;
322 };
323 (!name.is_empty()).then(|| name.to_owned())
324 }
325
326 pub(crate) fn hint(&self) -> String {
327 let mut hint = String::new();
328 for (index, segment) in self.order.iter().copied().enumerate() {
329 hint.push_str(&self.literals[index]);
330 hint.push_str(match segment {
331 TimeSegment::Hour => "HH",
332 TimeSegment::Minute => "MM",
333 TimeSegment::Second => "SS",
334 TimeSegment::Meridiem => self.am.as_str(),
335 });
336 }
337 hint.push_str(self.literals.last().map_or("", String::as_str));
338 hint
339 }
340}
341
342pub(crate) fn regional_time_pattern(
343 granularity: TimeGranularity,
344 hour_cycle: HourCycle,
345) -> RegionalTimePattern {
346 system_time_format_for_cycle(hour_cycle).pattern(granularity)
347}
348
349#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
351pub struct Time {
352 pub hour: u32,
354 pub minute: u32,
356 pub second: u32,
358}
359
360impl Time {
361 pub fn new(hour: u32, minute: u32) -> Self {
363 Self {
364 hour: hour.min(23),
365 minute: minute.min(59),
366 second: 0,
367 }
368 }
369
370 pub fn with_second(mut self, second: u32) -> Self {
372 self.second = second.min(59);
373 self
374 }
375
376 pub fn bump(self, segment: TimeSegment, delta: i32) -> Self {
378 let wrap =
379 |value: u32, len: i32| -> u32 { (((value as i32 + delta) % len + len) % len) as u32 };
380 match segment {
381 TimeSegment::Hour => Self {
382 hour: wrap(self.hour, 24),
383 ..self
384 },
385 TimeSegment::Minute => Self {
386 minute: wrap(self.minute, 60),
387 ..self
388 },
389 TimeSegment::Second => Self {
390 second: wrap(self.second, 60),
391 ..self
392 },
393 TimeSegment::Meridiem => Self {
395 hour: (self.hour + 12) % 24,
396 ..self
397 },
398 }
399 }
400
401 pub fn twelve_hour(self) -> (u32, &'static str) {
403 let suffix = if self.hour < 12 { "AM" } else { "PM" };
404 let hour = match self.hour % 12 {
405 0 => 12,
406 h => h,
407 };
408 (hour, suffix)
409 }
410}
411
412fn pad2(value: u32, pad: bool) -> String {
414 if pad {
415 format!("{value:02}")
416 } else {
417 value.to_string()
418 }
419}
420
421#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
424pub enum TimeGranularity {
425 Hour,
427 #[default]
429 Minute,
430 Second,
432}
433
434#[derive(Clone, Copy, Debug, PartialEq, Eq)]
436pub enum TimeSegment {
437 Hour,
439 Minute,
441 Second,
443 Meridiem,
445}
446
447impl TimeSegment {
448 pub(crate) fn order(granularity: TimeGranularity, twelve_hour: bool) -> Vec<TimeSegment> {
454 let mut out = vec![TimeSegment::Hour];
455 if granularity != TimeGranularity::Hour {
456 out.push(TimeSegment::Minute);
457 }
458 if granularity == TimeGranularity::Second {
459 out.push(TimeSegment::Second);
460 }
461 if twelve_hour {
462 out.push(TimeSegment::Meridiem);
463 }
464 out
465 }
466
467 pub(crate) fn digits(self) -> usize {
469 match self {
470 TimeSegment::Meridiem => 0,
471 _ => 2,
472 }
473 }
474
475 pub(crate) fn a11y_label(self, cx: &App) -> SharedString {
483 use crate::i18n::{ui_string, UiString};
484 ui_string(
485 match self {
486 TimeSegment::Hour => UiString::Hour,
487 TimeSegment::Minute => UiString::Minute,
488 TimeSegment::Second => UiString::Second,
489 TimeSegment::Meridiem => UiString::DayPeriod,
490 },
491 cx,
492 )
493 }
494
495 pub(crate) fn with_value(
497 self,
498 time: Time,
499 value: u32,
500 twelve_hour: bool,
501 zero_based_twelve_hour: bool,
502 ) -> Time {
503 match self {
504 TimeSegment::Hour => {
505 let hour = if twelve_hour {
506 let pm = time.hour >= 12;
508 let base = if zero_based_twelve_hour {
509 value.min(11)
510 } else {
511 value.clamp(1, 12) % 12
512 };
513 if pm {
514 base + 12
515 } else {
516 base
517 }
518 } else {
519 value.min(23)
520 };
521 Time::new(hour, time.minute).with_second(time.second)
522 }
523 TimeSegment::Minute => Time::new(time.hour, value.min(59)).with_second(time.second),
524 TimeSegment::Second => Time::new(time.hour, time.minute).with_second(value.min(59)),
525 TimeSegment::Meridiem => time,
526 }
527 }
528}
529
530pub struct TimeState {
532 pub value: Option<Time>,
534 pub focused: TimeSegment,
536 display_value: Option<Time>,
538 cleared: Vec<TimeSegment>,
542 segment_order: Vec<TimeSegment>,
543 last_controlled: Option<Option<Time>>,
546 pub(crate) focus_handle: gpui::FocusHandle,
551}
552
553impl TimeState {
554 pub fn new(cx: &mut App) -> Self {
556 Self {
557 value: None,
558 focused: TimeSegment::Hour,
559 display_value: None,
560 cleared: Vec::new(),
561 segment_order: Vec::new(),
562 last_controlled: None,
563 focus_handle: cx.focus_handle().tab_stop(true),
565 }
566 }
567
568 pub fn with_value(cx: &mut App, value: Time) -> Self {
570 Self {
571 value: Some(value),
572 focused: TimeSegment::Hour,
573 display_value: Some(value),
574 cleared: Vec::new(),
575 segment_order: Vec::new(),
576 last_controlled: None,
577 focus_handle: cx.focus_handle().tab_stop(true),
578 }
579 }
580
581 pub fn bump_focused_from(&mut self, delta: i32, seed: Time) {
583 let base = self.display_value.unwrap_or(seed);
584 let value = base.bump(self.focused, delta);
585 self.value = Some(value);
586 self.display_value = Some(value);
587 self.cleared.retain(|segment| *segment != self.focused);
588 }
589
590 pub fn bump_focused(&mut self, delta: i32) {
592 self.bump_focused_from(delta, Time::new(0, 0));
593 }
594
595 fn visible_is_complete(&self, segments: &[TimeSegment]) -> bool {
596 !segments
597 .iter()
598 .any(|segment| self.cleared.contains(segment))
599 }
600
601 fn set_uncontrolled_value(&mut self, value: Option<Time>) {
602 self.value = value;
603 self.display_value = value;
604 self.cleared.clear();
605 }
606
607 fn sync_controlled(&mut self, value: Option<Time>) {
608 if self.last_controlled != Some(value) {
609 self.last_controlled = Some(value);
610 self.display_value = value;
611 self.cleared.clear();
612 }
613 self.value = value;
616 }
617
618 fn sync_external_value(&mut self) {
619 if self.last_controlled.is_none()
620 && self.cleared.is_empty()
621 && self.display_value != self.value
622 {
623 self.display_value = self.value;
624 }
625 }
626
627 fn sync_segment_order(&mut self, order: &[TimeSegment]) {
628 if self.segment_order != order {
629 if self.segment_order.is_empty() || !order.contains(&self.focused) {
630 self.focused = order[0];
631 }
632 self.segment_order.clear();
633 self.segment_order.extend_from_slice(order);
634 }
635 }
636
637 fn edit_focused(&mut self, value: Time, segments: &[TimeSegment]) -> bool {
641 if self.display_value.is_none() {
642 self.cleared = segments.to_vec();
643 }
644 self.display_value = Some(value);
645 self.cleared.retain(|segment| *segment != self.focused);
646 if !self.visible_is_complete(segments) {
647 return false;
648 }
649
650 match self.last_controlled {
651 Some(controlled) => {
652 self.value = controlled;
653 self.display_value = controlled;
654 self.cleared.clear();
655 }
656 None => self.set_uncontrolled_value(Some(value)),
657 }
658 true
659 }
660
661 fn clear_focused(&mut self, segments: &[TimeSegment]) -> bool {
665 if self.display_value.is_none() {
666 return false;
667 }
668 if !self.cleared.contains(&self.focused) {
669 self.cleared.push(self.focused);
670 }
671 if segments
672 .iter()
673 .all(|segment| self.cleared.contains(segment))
674 {
675 match self.last_controlled {
676 Some(Some(value)) => {
677 self.value = Some(value);
678 self.display_value = Some(value);
679 self.cleared.clear();
680 }
681 Some(None) | None => self.set_uncontrolled_value(None),
682 }
683 return true;
684 }
685 false
686 }
687}
688
689type Segment = Arc<dyn Fn(TimeSegment, SharedString) -> gpui::AnyElement + 'static>;
690
691type OnTimeChange = Arc<dyn Fn(&Option<Time>, &mut Window, &mut App) + 'static>;
692
693type TimeFieldFormState = Rc<RefCell<crate::form::LiveFormFieldState>>;
694
695thread_local! {
696 static TIME_FIELD_FORM_STATES: RefCell<HashMap<u64, std::rc::Weak<RefCell<crate::form::LiveFormFieldState>>>> =
697 RefCell::new(HashMap::new());
698}
699
700fn registered_time_field_form_state(entity_id: u64) -> Option<TimeFieldFormState> {
701 TIME_FIELD_FORM_STATES.with(|states| {
702 states
703 .borrow()
704 .get(&entity_id)
705 .and_then(|state| state.upgrade())
706 })
707}
708
709fn time_field_form_state(entity_id: u64) -> TimeFieldFormState {
710 TIME_FIELD_FORM_STATES.with(|states| {
711 let mut states = states.borrow_mut();
712 if let Some(state) = states.get(&entity_id).and_then(|state| state.upgrade()) {
713 return state;
714 }
715 let state = Rc::new(RefCell::new(crate::form::LiveFormFieldState {
716 value: crate::form::FormValue::Text(SharedString::default()),
717 is_invalid: false,
718 is_successful: true,
719 focus: None,
720 restore: None,
721 }));
722 states.insert(entity_id, Rc::downgrade(&state));
723 state
724 })
725}
726
727fn time_form_text(value: Option<Time>, granularity: TimeGranularity) -> SharedString {
729 value
730 .map(|time| match granularity {
731 TimeGranularity::Hour | TimeGranularity::Minute => {
732 format!("{:02}:{:02}", time.hour, time.minute)
733 }
734 TimeGranularity::Second => {
735 format!("{:02}:{:02}:{:02}", time.hour, time.minute, time.second)
736 }
737 })
738 .unwrap_or_default()
739 .into()
740}
741
742fn sync_time_field_form(
743 form_state: &TimeFieldFormState,
744 time_state: &Entity<TimeState>,
745 is_disabled: bool,
746 is_invalid: bool,
747 granularity: TimeGranularity,
748 cx: &App,
749) {
750 let mut state = form_state.borrow_mut();
751 state.value =
752 crate::form::FormValue::Text(time_form_text(time_state.read(cx).value, granularity));
753 state.is_successful = !is_disabled;
754 state.is_invalid = is_invalid;
755 state.focus = Some(time_state.read(cx).focus_handle.clone());
756}
757
758fn install_time_field_restore(
759 form_state: &TimeFieldFormState,
760 time_state: Entity<TimeState>,
761 default: Option<Time>,
762 granularity: TimeGranularity,
763 controlled: bool,
764 on_change: Option<OnTimeChange>,
765) {
766 if default.is_none() && !controlled {
767 return;
768 }
769 let restore_form = Rc::downgrade(form_state);
770 form_state.borrow_mut().restore = Some(util::shared(move |window: &mut Window, cx: &mut App| {
771 time_state.update(&mut *cx, |state, cx| {
772 if controlled {
773 state.sync_controlled(default);
774 } else {
775 state.set_uncontrolled_value(default);
776 }
777 cx.notify();
778 });
779 if let Some(state) = restore_form.upgrade() {
780 let mut state = state.borrow_mut();
781 state.value = crate::form::FormValue::Text(time_form_text(default, granularity));
782 state.is_invalid = false;
783 }
784 if controlled {
785 if let Some(callback) = &on_change {
786 callback(&default, window, cx);
787 }
788 }
789 }) as Arc<dyn Fn(&mut Window, &mut App)>);
790}
791
792#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
794#[non_exhaustive]
795pub struct TimeFieldRenderState {
796 pub is_disabled: bool,
798 pub is_invalid: bool,
800 pub is_read_only: bool,
802 pub is_required: bool,
804 pub is_focused: bool,
806 pub is_focus_within: bool,
808 pub is_focus_visible: bool,
810}
811
812#[must_use = "a component does nothing until it is rendered: add it as a child or return it from `render`"]
814#[derive(IntoElement)]
815pub struct TimeField {
816 segment: Option<Segment>,
819 name: Option<SharedString>,
821 validation_behavior: crate::form::ValidationBehavior,
823 default_value: Option<Time>,
825 controlled_value: Option<Option<Time>>,
827 state: Entity<TimeState>,
828 label: Option<SharedString>,
829 description: Option<SharedString>,
830 error_message: Option<SharedString>,
831 validate: Option<crate::validation::Validator<Option<Time>>>,
833 validation_errors: Vec<SharedString>,
835 prefix: Option<gpui::AnyElement>,
838 content: Option<Arc<dyn Fn(TimeFieldRenderState) -> gpui::AnyElement + 'static>>,
840 suffix: Option<gpui::AnyElement>,
843 variant: FieldVariant,
844 field: util::FieldBox,
846 stepper_hover_bg: Option<gpui::Hsla>,
848 font_family: Option<SharedString>,
850 radius: Option<Pixels>,
852 hour_cycle: HourCycle,
853 granularity: TimeGranularity,
855 auto_focus: bool,
857 should_force_leading_zeros: bool,
860 full_width: bool,
861 is_disabled: bool,
862 is_read_only: bool,
863 is_required: bool,
864 is_invalid: bool,
865 min_value: Option<Time>,
866 max_value: Option<Time>,
867 placeholder_value: Option<Time>,
869 on_change: Option<OnTimeChange>,
870 sx: Option<Box<gpui::StyleRefinement>>,
872}
873
874impl TimeField {
875 pub fn value(mut self, time: Option<Time>) -> Self {
884 self.controlled_value = Some(time);
885 self
886 }
887
888 pub fn new(state: Entity<TimeState>) -> Self {
890 Self {
891 segment: None,
892 name: None,
893 validation_behavior: crate::form::ValidationBehavior::Native,
894 default_value: None,
895 controlled_value: None,
896 state,
897 label: None,
898 description: None,
899 error_message: None,
900 content: None,
901 prefix: None,
902 suffix: None,
903 variant: FieldVariant::Primary,
904 hour_cycle: HourCycle::default(),
905 granularity: TimeGranularity::default(),
906 auto_focus: false,
907 should_force_leading_zeros: false,
908 full_width: false,
909 is_disabled: false,
910 is_read_only: false,
911 is_required: false,
912 validate: None,
913 validation_errors: Vec::new(),
914 is_invalid: false,
915 min_value: None,
916 max_value: None,
917 placeholder_value: None,
918 on_change: None,
919 sx: None,
920 field: util::FieldBox::default(),
921 stepper_hover_bg: None,
922 font_family: None,
923 radius: None,
924 }
925 }
926
927 pub fn segment(
933 mut self,
934 render: impl Fn(TimeSegment, SharedString) -> gpui::AnyElement + 'static,
935 ) -> Self {
936 self.segment = Some(Arc::new(render));
937 self
938 }
939
940 pub fn name(mut self, name: impl Into<SharedString>) -> Self {
942 self.name = Some(name.into());
943 self
944 }
945
946 pub fn validation_behavior(mut self, behavior: crate::form::ValidationBehavior) -> Self {
949 self.validation_behavior = behavior;
950 self
951 }
952
953 pub fn form_field(&self, cx: &App) -> Option<crate::form::FormField> {
962 let name = self.name.clone()?;
963 let form_state = time_field_form_state(self.state.entity_id().as_u64());
964 sync_time_field_form(
965 &form_state,
966 &self.state,
967 self.is_disabled,
968 self.is_invalid,
969 self.granularity,
970 cx,
971 );
972 install_time_field_restore(
973 &form_state,
974 self.state.clone(),
975 self.default_value,
976 self.granularity,
977 self.controlled_value.is_some(),
978 self.on_change.clone(),
979 );
980 Some(
981 crate::form::FormField::live(name, form_state)
982 .is_required(self.is_required)
983 .validation_behavior(self.validation_behavior),
984 )
985 }
986
987 pub fn default_value(mut self, value: Time) -> Self {
992 self.default_value = Some(value);
993 self
994 }
995
996 pub fn label(mut self, text: impl Into<SharedString>) -> Self {
998 self.label = Some(text.into());
999 self
1000 }
1001
1002 pub fn description(mut self, text: impl Into<SharedString>) -> Self {
1004 self.description = Some(text.into());
1005 self
1006 }
1007
1008 pub fn error_message(mut self, text: impl Into<SharedString>) -> Self {
1010 self.error_message = Some(text.into());
1011 self
1012 }
1013
1014 pub fn prefix(mut self, el: impl IntoElement) -> Self {
1016 self.prefix = Some(el.into_any_element());
1017 self
1018 }
1019
1020 pub fn content(
1024 mut self,
1025 render: impl Fn(TimeFieldRenderState) -> gpui::AnyElement + 'static,
1026 ) -> Self {
1027 self.content = Some(Arc::new(render));
1028 self
1029 }
1030
1031 pub fn suffix(mut self, el: impl IntoElement) -> Self {
1033 self.suffix = Some(el.into_any_element());
1034 self
1035 }
1036
1037 pub fn variant(mut self, variant: FieldVariant) -> Self {
1039 self.variant = variant;
1040 self
1041 }
1042
1043 pub fn height(mut self, h: impl Into<Pixels>) -> Self {
1046 self.field.height = Some(h.into());
1047 self
1048 }
1049
1050 pub fn padding_x(mut self, p: impl Into<Pixels>) -> Self {
1052 self.field.padding_x = Some(p.into());
1053 self
1054 }
1055
1056 pub fn is_bare(mut self, v: bool) -> Self {
1059 self.field.is_bare = v;
1060 self.field.is_bare_is_set = true;
1061 self
1062 }
1063
1064 pub fn focus_ring(mut self, v: bool) -> Self {
1067 self.field.focus_ring = Some(v);
1068 self
1069 }
1070
1071 pub fn stepper_hover_bg(mut self, color: impl Into<gpui::Hsla>) -> Self {
1073 self.stepper_hover_bg = Some(color.into());
1074 self
1075 }
1076
1077 pub fn font_family(mut self, family: impl Into<SharedString>) -> Self {
1079 self.font_family = Some(family.into());
1080 self
1081 }
1082
1083 pub fn radius(mut self, radius: impl Into<Pixels>) -> Self {
1090 self.radius = Some(radius.into());
1091 self
1092 }
1093
1094 pub fn hour_cycle(mut self, cycle: HourCycle) -> Self {
1096 self.hour_cycle = cycle;
1097 self
1098 }
1099
1100 pub fn granularity(mut self, granularity: TimeGranularity) -> Self {
1102 self.granularity = granularity;
1103 self
1104 }
1105
1106 pub fn auto_focus(mut self, v: bool) -> Self {
1108 self.auto_focus = v;
1109 self
1110 }
1111
1112 pub fn should_force_leading_zeros(mut self, v: bool) -> Self {
1115 self.should_force_leading_zeros = v;
1116 self
1117 }
1118
1119 pub fn show_seconds(mut self, v: bool) -> Self {
1122 self.granularity = if v {
1123 TimeGranularity::Second
1124 } else {
1125 TimeGranularity::Minute
1126 };
1127 self
1128 }
1129
1130 pub fn full_width(mut self, v: bool) -> Self {
1132 self.full_width = v;
1133 self
1134 }
1135
1136 pub fn sx(mut self, style: impl FnOnce(gpui::Div) -> gpui::Div) -> Self {
1143 util::refine_sx(&mut self.sx, style);
1144 self
1145 }
1146
1147 pub fn is_disabled(mut self, v: bool) -> Self {
1149 self.is_disabled = v;
1150 self
1151 }
1152
1153 pub fn is_read_only(mut self, v: bool) -> Self {
1155 self.is_read_only = v;
1156 self
1157 }
1158
1159 pub fn is_required(mut self, v: bool) -> Self {
1161 self.is_required = v;
1162 self
1163 }
1164
1165 pub fn validate(mut self, f: impl Fn(&Option<Time>) -> Option<SharedString> + 'static) -> Self {
1170 self.validate = Some(Arc::new(f));
1171 self
1172 }
1173
1174 pub fn validation_errors(
1177 mut self,
1178 errors: impl IntoIterator<Item = impl Into<SharedString>>,
1179 ) -> Self {
1180 self.validation_errors = errors.into_iter().map(Into::into).collect();
1181 self
1182 }
1183
1184 pub fn is_invalid(mut self, v: bool) -> Self {
1186 self.is_invalid = v;
1187 self
1188 }
1189
1190 pub fn min_value(mut self, time: Time) -> Self {
1192 self.min_value = Some(time);
1193 self
1194 }
1195
1196 pub fn max_value(mut self, time: Time) -> Self {
1198 self.max_value = Some(time);
1199 self
1200 }
1201
1202 pub fn placeholder_value(mut self, time: Time) -> Self {
1204 self.placeholder_value = Some(time);
1205 self
1206 }
1207
1208 pub fn on_change(
1210 mut self,
1211 handler: impl Fn(&Option<Time>, &mut Window, &mut App) + 'static,
1212 ) -> Self {
1213 self.on_change = Some(Arc::new(handler));
1214 self
1215 }
1216}
1217
1218impl RenderOnce for TimeField {
1219 fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1220 let entity_id = self.state.entity_id().as_u64();
1224 let base_id = ElementId::named_usize("timefield", entity_id as usize);
1225
1226 if self.controlled_value.is_none() {
1228 if let Some(value) = self.default_value {
1229 let state = self.state.clone();
1230 util::seed_once(
1231 window,
1232 cx,
1233 element_id::scoped(&base_id, "default"),
1234 move |cx| {
1235 state.update(cx, |s, cx| {
1236 s.set_uncontrolled_value(Some(value));
1237 cx.notify();
1238 });
1239 },
1240 );
1241 }
1242 }
1243 if let Some(value) = self.controlled_value {
1244 self.state.update(cx, |state, _| {
1245 state.sync_controlled(value);
1246 });
1247 } else {
1248 self.state.update(cx, |state, _| {
1249 state.sync_external_value();
1250 });
1251 }
1252
1253 if let Some(form_state) = registered_time_field_form_state(entity_id) {
1254 sync_time_field_form(
1255 &form_state,
1256 &self.state,
1257 self.is_disabled,
1258 self.is_invalid,
1259 self.granularity,
1260 cx,
1261 );
1262 install_time_field_restore(
1263 &form_state,
1264 self.state.clone(),
1265 self.default_value,
1266 self.granularity,
1267 self.controlled_value.is_some(),
1268 self.on_change.clone(),
1269 );
1270 }
1271 let focus_handle = self.state.read(cx).focus_handle.clone();
1278 if self.auto_focus {
1279 util::focus_once(
1280 window,
1281 cx,
1282 element_id::scoped(&base_id, "autofocus"),
1283 &focus_handle,
1284 );
1285 }
1286 let typing = window.use_keyed_state(element_id::scoped(&base_id, "typing"), cx, |_, _| {
1289 String::new()
1290 });
1291
1292 let regional_time = regional_time_pattern(self.granularity, self.hour_cycle);
1293 self.state.update(cx, |state, _| {
1294 state.sync_segment_order(®ional_time.order);
1295 });
1296
1297 let colors = cx.colors().clone();
1298 let layout = cx.layout().clone();
1299 let navigable = !self.is_disabled;
1300 let editable = navigable && !self.is_read_only;
1301
1302 let (value, display_value, focused, cleared) = {
1303 let st = self.state.read(cx);
1304 (st.value, st.display_value, st.focused, st.cleared.clone())
1305 };
1306
1307 let validity = crate::validation::resolve(
1310 self.is_invalid,
1311 &self.validation_errors,
1312 self.validate.as_ref().and_then(|f| f(&value)),
1313 self.error_message.clone(),
1314 );
1315 let is_invalid = validity.is_invalid;
1316 if let Some(form_state) = registered_time_field_form_state(entity_id) {
1317 sync_time_field_form(
1318 &form_state,
1319 &self.state,
1320 self.is_disabled,
1321 is_invalid,
1322 self.granularity,
1323 cx,
1324 );
1325 }
1326 if let Some(render) = self.content.clone() {
1327 let focused = focus_handle.is_focused(window);
1328 return render(TimeFieldRenderState {
1329 is_disabled: self.is_disabled,
1330 is_invalid,
1331 is_read_only: self.is_read_only,
1332 is_required: self.is_required,
1333 is_focused: focused,
1334 is_focus_within: focus_handle.contains_focused(window, cx),
1335 is_focus_visible: focused && util::focus_visible(cx),
1336 })
1337 .into_any_element();
1338 }
1339
1340 let segments = regional_time.order.clone();
1341
1342 let hour_cycle = self.hour_cycle;
1343 let pad_hour = self.should_force_leading_zeros || regional_time.hour_has_leading_zero;
1344 let pad_minute = regional_time.minute_has_leading_zero;
1345 let pad_second = regional_time.second_has_leading_zero;
1346 let zero_based_twelve_hour = regional_time.hour_zero_based;
1347 let am = regional_time.am.clone();
1348 let pm = regional_time.pm.clone();
1349 let segment_text = move |segment: TimeSegment| -> String {
1350 if cleared.contains(&segment) {
1351 return if segment == TimeSegment::Meridiem {
1352 am.clone()
1353 } else {
1354 "--".to_owned()
1355 };
1356 }
1357 let Some(t) = display_value else {
1358 return "--".to_owned();
1359 };
1360 match segment {
1361 TimeSegment::Hour => {
1362 if hour_cycle == HourCycle::H12 {
1363 let hour = if zero_based_twelve_hour {
1364 t.hour % 12
1365 } else {
1366 t.twelve_hour().0
1367 };
1368 pad2(hour, pad_hour)
1369 } else {
1370 pad2(t.hour, pad_hour)
1371 }
1372 }
1373 TimeSegment::Minute => pad2(t.minute, pad_minute),
1374 TimeSegment::Second => pad2(t.second, pad_second),
1375 TimeSegment::Meridiem => {
1376 if t.hour < 12 {
1377 am.clone()
1378 } else {
1379 pm.clone()
1380 }
1381 }
1382 }
1383 };
1384
1385 let field_box = self.field;
1390 let radius = self.radius.unwrap_or_else(|| util::field_radius(cx));
1392 let mut group = div()
1393 .id(base_id.clone())
1394 .a11y_named(
1395 a11y::Role::Group,
1396 &a11y::Name::field(self.label.as_ref(), self.description.as_ref(), &validity),
1397 )
1398 .flex()
1399 .flex_row()
1400 .items_center()
1401 .gap(px(2.))
1402 .px(field_box.resolved_padding_x())
1403 .h(field_box.resolved_height())
1404 .rounded(radius)
1405 .text_size(util::FIELD_TEXT)
1406 .line_height(px(20.))
1407 .font_family(util::MONO_FONT)
1408 .when_some(self.font_family.clone(), |group, family| {
1409 group.font_family(family)
1410 })
1411 .text_color(colors.field.foreground);
1412
1413 if !field_box.is_bare {
1414 group = util::apply_field_chrome_overlay(
1418 group,
1419 self.variant,
1420 is_invalid,
1421 focus_handle.is_focused(window),
1422 field_box.focus_ring.unwrap_or(true),
1423 Some(radius),
1424 cx,
1425 );
1426
1427 let focused = focus_handle.is_focused(window);
1428 if !self.is_disabled && !is_invalid && !focused {
1429 let idle_bg = match self.variant {
1430 FieldVariant::Primary => colors.field.background,
1431 FieldVariant::Secondary => colors.default.color,
1432 };
1433 let hover_bg = match self.variant {
1434 FieldVariant::Primary => colors.field.hover(),
1435 FieldVariant::Secondary => colors.default.hover(),
1436 };
1437 let hover_border = colors.field.border_hover();
1438 group = crate::anim::hover_fade_with_duration_and_easing(
1439 group,
1440 element_id::scoped(&base_id, "hover-fade"),
1441 (idle_bg, hover_bg),
1442 None,
1443 Some(hover_border),
1444 move |fill| fill.rounded(radius),
1445 Some(150),
1446 crate::anim::HoverFadeEasing::EaseSmooth,
1447 window,
1448 cx,
1449 );
1450 }
1451 }
1452
1453 if navigable {
1456 let state = self.state.clone();
1457 let on_change = self.on_change.clone();
1458 let buffer = typing;
1459 let fh = focus_handle.clone();
1460 let order = segments.clone();
1461 let twelve_hour = self.hour_cycle == HourCycle::H12;
1462 let seed = self.placeholder_value.unwrap_or(Time::new(0, 0));
1463 let min_value = self.min_value;
1464 let max_value = self.max_value;
1465 let is_read_only = self.is_read_only;
1466 group = group
1467 .track_focus(&focus_handle)
1468 .key_context("TimeField")
1469 .on_mouse_down(gpui::MouseButton::Left, move |_, window, cx| {
1470 window.focus(&fh, cx);
1471 })
1472 .on_key_down(move |event, window, cx| {
1473 let key = event.keystroke.key.as_str();
1474 let here = order.iter().position(|s| *s == focused).unwrap_or(0);
1475 let commit = |time: Time, window: &mut Window, cx: &mut App| {
1476 let complete = state.update(cx, |s, cx| {
1477 let complete = s.edit_focused(time, &order);
1478 cx.notify();
1479 complete
1480 });
1481 if complete {
1482 if let Some(cb) = &on_change {
1483 cb(&Some(time), window, cx);
1484 }
1485 }
1486 };
1487 match key {
1488 "left" | "right" => {
1489 let delta: i32 = if key == "right" { 1 } else { -1 };
1490 let next =
1491 (here as i32 + delta).clamp(0, order.len() as i32 - 1) as usize;
1492 buffer.update(cx, |b, _| b.clear());
1493 let segment = order[next];
1494 state.update(cx, |s, cx| {
1495 s.focused = segment;
1496 cx.notify();
1497 });
1498 }
1499 _ if is_read_only => {}
1500 "up" | "down" => {
1501 let delta = if key == "up" { 1 } else { -1 };
1502 buffer.update(cx, |b, _| b.clear());
1503 let base = state.read(cx).display_value.unwrap_or(seed);
1504 let time = clamp_time(base.bump(focused, delta), min_value, max_value);
1505 commit(time, window, cx);
1506 }
1507 "backspace" | "delete" => {
1508 buffer.update(cx, |b, _| b.clear());
1509 let emptied = state.update(cx, |s, cx| {
1510 let emptied = s.clear_focused(&order);
1511 cx.notify();
1512 emptied
1513 });
1514 if emptied {
1515 if let Some(cb) = &on_change {
1516 cb(&None, window, cx);
1517 }
1518 }
1519 }
1520 "a" | "p" if focused == TimeSegment::Meridiem => {
1523 let base = state.read(cx).display_value.unwrap_or(seed);
1524 let hour = base.hour % 12 + if key == "p" { 12 } else { 0 };
1525 commit(
1526 Time::new(hour, base.minute).with_second(base.second),
1527 window,
1528 cx,
1529 );
1530 }
1531 digit if digit.len() == 1 && digit.chars().all(|c| c.is_ascii_digit()) => {
1532 let width = focused.digits();
1533 if width == 0 {
1534 return;
1535 }
1536 let text = buffer.update(cx, |b, _| {
1537 if b.len() >= width {
1538 b.clear();
1539 }
1540 b.push_str(digit);
1541 b.clone()
1542 });
1543 let Ok(value) = text.parse::<u32>() else {
1544 return;
1545 };
1546 let base = state.read(cx).display_value.unwrap_or(seed);
1547 commit(
1548 focused.with_value(
1549 base,
1550 value,
1551 twelve_hour,
1552 zero_based_twelve_hour,
1553 ),
1554 window,
1555 cx,
1556 );
1557 if text.len() >= width {
1558 buffer.update(cx, |b, _| b.clear());
1559 if let Some(segment) = order.get(here + 1).copied() {
1560 state.update(cx, |s, cx| {
1561 s.focused = segment;
1562 cx.notify();
1563 });
1564 }
1565 }
1566 }
1567 _ => {}
1568 }
1569 });
1570 }
1571 if self.is_disabled {
1572 group = group.opacity(layout.disabled_opacity);
1573 }
1574 if self.full_width {
1575 group = group.w_full();
1576 }
1577
1578 if let Some(prefix) = self.prefix.take() {
1581 group = group.child(
1582 div()
1583 .flex()
1584 .items_center()
1585 .flex_shrink_0()
1586 .mr(px(4.))
1587 .text_color(colors.field.placeholder)
1588 .child(prefix),
1589 );
1590 }
1591
1592 for (index, segment) in segments.iter().copied().enumerate() {
1593 if let Some(literal) = regional_time
1594 .literals
1595 .get(index)
1596 .filter(|literal| !literal.is_empty())
1597 {
1598 group = group.child(div().text_color(colors.muted).child(literal.clone()));
1599 }
1600
1601 let mut seg = div()
1602 .id(element_id::indexed(&base_id, "seg", index))
1603 .px(px(2.))
1605 .py(px(1.))
1606 .rounded(cx.layout().radius_md())
1607 .child(match &self.segment {
1610 Some(render) => render(segment, segment_text(segment).into()),
1611 None => segment_text(segment).into_any_element(),
1612 });
1613
1614 if focused == segment && navigable {
1615 seg = seg
1616 .bg(colors.accent.soft())
1617 .text_color(colors.accent.soft_foreground(colors.foreground));
1618 }
1619
1620 if navigable {
1621 let state = self.state.clone();
1622 seg = seg
1623 .cursor(util::interactive_cursor(cx))
1624 .on_click(move |_, _, cx| {
1625 state.update(cx, |s, cx| {
1626 s.focused = segment;
1627 cx.notify();
1628 });
1629 });
1630 }
1631
1632 let seg_text = segment_text(segment);
1633 seg = seg
1634 .a11y_named(
1635 a11y::Role::TextInput,
1636 &a11y::Name::labelled(segment.a11y_label(cx)),
1637 )
1638 .a11y_text(&seg_text, None);
1639
1640 group = group.child(seg);
1641 }
1642 if let Some(literal) = regional_time
1643 .literals
1644 .last()
1645 .filter(|literal| !literal.is_empty())
1646 {
1647 group = group.child(div().text_color(colors.muted).child(literal.clone()));
1648 }
1649
1650 if editable {
1652 let seed = self.placeholder_value.unwrap_or(Time::new(0, 0));
1653 let min_value = self.min_value;
1654 let max_value = self.max_value;
1655 let visible_segments = segments;
1656 let mut steppers = div().flex().flex_col().ml(px(8.)).flex_shrink_0();
1657 for (icon, delta, key) in [
1658 (icons::CHEVRON_UP, 1i32, "up"),
1659 (icons::CHEVRON_DOWN, -1i32, "down"),
1660 ] {
1661 let state = self.state.clone();
1662 let on_change = self.on_change.clone();
1663 let visible_segments = visible_segments.clone();
1664 let hover_bg = self.stepper_hover_bg.unwrap_or(colors.default.color);
1665 let stepper_name = crate::i18n::ui_string_with(
1666 if key == "up" {
1667 crate::i18n::UiString::Increase
1668 } else {
1669 crate::i18n::UiString::Decrease
1670 },
1671 "",
1672 cx,
1673 );
1674 steppers = steppers.child(
1675 div()
1676 .id(element_id::scoped(&base_id, key))
1677 .flex()
1678 .items_center()
1679 .justify_center()
1680 .w(px(18.))
1681 .h(px(14.))
1682 .rounded(px(4.))
1683 .cursor(util::interactive_cursor(cx))
1684 .text_color(colors.muted)
1685 .hover(move |s| s.bg(hover_bg))
1686 .child(
1687 gpui::svg()
1688 .size(px(10.))
1689 .path(icon)
1690 .text_color(colors.muted),
1691 )
1692 .a11y_named(a11y::Role::Button, &a11y::Name::labelled(stepper_name))
1693 .on_click(move |_, window, cx| {
1694 let (next, complete) = state.update(cx, |s, cx| {
1695 let base = s.display_value.unwrap_or(seed);
1696 let mut next = base.bump(s.focused, delta);
1697 next = clamp_time(next, min_value, max_value);
1699 let complete = s.edit_focused(next, &visible_segments);
1700 cx.notify();
1701 (next, complete)
1702 });
1703 if complete {
1704 if let Some(cb) = &on_change {
1705 cb(&Some(next), window, cx);
1706 }
1707 }
1708 }),
1709 );
1710 }
1711 group = group.child(steppers);
1712 }
1713
1714 if let Some(suffix) = self.suffix.take() {
1715 group = group.child(
1716 div()
1717 .flex()
1718 .items_center()
1719 .flex_shrink_0()
1720 .ml(px(4.))
1721 .text_color(colors.field.placeholder)
1722 .child(suffix),
1723 );
1724 }
1725 if navigable {
1726 group = util::record_focus_bounds(group, &focus_handle, window, cx);
1727 }
1728
1729 let mut root = div()
1731 .flex()
1732 .flex_col()
1733 .gap(px(4.))
1734 .when(self.full_width, |root| root.w_full());
1735 if let Some(label) = self.label {
1736 root = root.child(
1737 crate::field::Label::new(label)
1738 .is_required(self.is_required)
1739 .is_disabled(self.is_disabled)
1740 .is_invalid(is_invalid),
1741 );
1742 }
1743 root = root.child(group);
1744
1745 if is_invalid {
1746 if let Some(message) = validity.first() {
1747 root = root.child(crate::field::ErrorMessage::new(message));
1748 }
1749 } else if let Some(description) = self.description {
1750 root = root.child(crate::field::Description::new(description));
1751 }
1752
1753 root = util::apply_sx(root, &self.sx);
1754 root.into_any_element()
1755 }
1756}
1757
1758#[cfg(test)]
1759mod tests {
1760 use super::*;
1761
1762 #[test]
1763 fn enabled_time_field_hover_uses_the_pinned_smooth_fill_transition() {
1764 let source = include_str!("time_field.rs")
1765 .split("#[cfg(test)]")
1766 .next()
1767 .expect("the implementation section is always present");
1768 assert!(
1769 source.contains("hover_fade_with_duration_and_easing(")
1770 && source.contains("Some(150)")
1771 && source.contains("HoverFadeEasing::EaseSmooth")
1772 );
1773 assert!(source.contains("!self.is_disabled && !is_invalid && !focused"));
1774 }
1775
1776 #[test]
1777 fn hour_wraps_at_midnight() {
1778 assert_eq!(Time::new(23, 0).bump(TimeSegment::Hour, 1).hour, 0);
1779 assert_eq!(Time::new(0, 0).bump(TimeSegment::Hour, -1).hour, 23);
1780 }
1781
1782 #[test]
1783 fn minute_wraps_without_touching_the_hour() {
1784 let t = Time::new(10, 59).bump(TimeSegment::Minute, 1);
1785 assert_eq!((t.hour, t.minute), (10, 0));
1786 }
1787
1788 #[test]
1789 fn meridiem_flips_half_a_day() {
1790 assert_eq!(Time::new(9, 30).bump(TimeSegment::Meridiem, 1).hour, 21);
1791 assert_eq!(Time::new(21, 30).bump(TimeSegment::Meridiem, 1).hour, 9);
1792 }
1793
1794 #[test]
1795 fn twelve_hour_display() {
1796 assert_eq!(Time::new(0, 0).twelve_hour(), (12, "AM"));
1797 assert_eq!(Time::new(12, 0).twelve_hour(), (12, "PM"));
1798 assert_eq!(Time::new(13, 0).twelve_hour(), (1, "PM"));
1799 assert_eq!(Time::new(11, 0).twelve_hour(), (11, "AM"));
1800 }
1801
1802 #[test]
1803 fn zero_based_twelve_hour_entry_preserves_the_half_day() {
1804 assert_eq!(
1805 TimeSegment::Hour
1806 .with_value(Time::new(1, 0), 0, true, true)
1807 .hour,
1808 0
1809 );
1810 assert_eq!(
1811 TimeSegment::Hour
1812 .with_value(Time::new(13, 0), 0, true, true)
1813 .hour,
1814 12
1815 );
1816 assert_eq!(
1817 TimeSegment::Hour
1818 .with_value(Time::new(13, 0), 12, true, false)
1819 .hour,
1820 12
1821 );
1822 }
1823
1824 #[test]
1825 fn hour_cycle_follows_locale_time_data_and_overrides() {
1826 assert_eq!(
1827 RegionalTimeFormat::for_locale("en-US").map(|format| format.hour_cycle),
1828 Some(HourCycle::H12)
1829 );
1830 assert_eq!(
1831 RegionalTimeFormat::for_locale("de-DE").map(|format| format.hour_cycle),
1832 Some(HourCycle::H24)
1833 );
1834 assert_eq!(
1835 RegionalTimeFormat::for_locale("en-US-u-hc-h23").map(|format| format.hour_cycle),
1836 Some(HourCycle::H24)
1837 );
1838 assert_eq!(
1839 RegionalTimeFormat::for_locale("de-DE-u-hc-h12").map(|format| format.hour_cycle),
1840 Some(HourCycle::H12)
1841 );
1842 assert_eq!(RegionalTimeFormat::for_locale("not_a_locale"), None);
1843 }
1844
1845 #[test]
1846 fn hour_cycle_prefers_the_system_time_category() {
1847 let tags = ["en-US".to_owned(), "de-DE".to_owned()];
1849 assert_eq!(
1850 RegionalTimeFormat::for_preferences(&tags).map(|format| format.hour_cycle),
1851 Some(HourCycle::H12)
1852 );
1853 }
1854
1855 #[test]
1856 fn hour_padding_follows_locale_time_patterns() {
1857 assert_eq!(
1858 RegionalTimeFormat::for_locale("en-US")
1859 .map(|format| format.minute_pattern.hour_has_leading_zero),
1860 Some(false)
1861 );
1862 assert_eq!(
1863 RegionalTimeFormat::for_locale("de-DE")
1864 .map(|format| format.minute_pattern.hour_has_leading_zero),
1865 Some(true)
1866 );
1867 assert_eq!(
1868 RegionalTimeFormat::for_locale("en-US-u-hc-h23")
1869 .map(|format| format.minute_pattern.hour_has_leading_zero),
1870 Some(true)
1871 );
1872 let explicit_twelve =
1873 RegionalTimeFormat::for_locale_with_cycle("en-US-u-hc-h23", Some(HourCycle::H12))
1874 .unwrap();
1875 assert_eq!(explicit_twelve.hour_cycle, HourCycle::H12);
1876 assert!(!explicit_twelve.minute_pattern.hour_has_leading_zero);
1877 }
1878
1879 #[test]
1880 fn segment_order_literals_padding_and_day_periods_follow_locale_patterns() {
1881 let us = RegionalTimeFormat::for_locale_with_cycle("en-US", Some(HourCycle::H12))
1882 .unwrap()
1883 .pattern(TimeGranularity::Second);
1884 assert_eq!(
1885 us.order,
1886 [
1887 TimeSegment::Hour,
1888 TimeSegment::Minute,
1889 TimeSegment::Second,
1890 TimeSegment::Meridiem,
1891 ]
1892 );
1893 assert_eq!(us.literals, ["", ":", ":", "\u{202f}", ""]);
1894 assert!(!us.hour_has_leading_zero);
1895 assert!(!us.hour_zero_based);
1896 assert!(us.minute_has_leading_zero);
1897 assert!(us.second_has_leading_zero);
1898 assert_eq!((us.am.as_str(), us.pm.as_str()), ("AM", "PM"));
1899 assert_eq!(us.hint(), "HH:MM:SS\u{202f}AM");
1900
1901 let japanese = RegionalTimeFormat::for_locale_with_cycle("ja-JP", Some(HourCycle::H12))
1902 .unwrap()
1903 .pattern(TimeGranularity::Minute);
1904 assert_eq!(
1905 japanese.order,
1906 [
1907 TimeSegment::Meridiem,
1908 TimeSegment::Hour,
1909 TimeSegment::Minute,
1910 ]
1911 );
1912 assert_eq!(japanese.literals, ["", "", ":", ""]);
1913 assert!(japanese.hour_zero_based);
1914 assert_eq!(
1915 (japanese.am.as_str(), japanese.pm.as_str()),
1916 ("午前", "午後")
1917 );
1918 assert_eq!(japanese.hint(), "午前HH:MM");
1919
1920 let arabic = RegionalTimeFormat::for_locale_with_cycle("ar-EG", Some(HourCycle::H12))
1921 .unwrap()
1922 .pattern(TimeGranularity::Minute);
1923 assert_eq!((arabic.am.as_str(), arabic.pm.as_str()), ("ص", "م"));
1924
1925 let korean = RegionalTimeFormat::for_locale_with_cycle("ko-KR", Some(HourCycle::H24))
1926 .unwrap()
1927 .pattern(TimeGranularity::Second);
1928 assert_eq!(
1929 korean.order,
1930 [TimeSegment::Hour, TimeSegment::Minute, TimeSegment::Second,]
1931 );
1932 assert_eq!(korean.literals, ["", ":", ":", ""]);
1933 assert!(korean.hour_has_leading_zero);
1934 assert!(korean.minute_has_leading_zero);
1935 assert!(korean.second_has_leading_zero);
1936 assert_eq!(korean.hint(), "HH:MM:SS");
1937 }
1938
1939 #[test]
1940 fn hour_granularity_keeps_day_period_order_without_minute_punctuation() {
1941 let us = RegionalTimeFormat::for_locale_with_cycle("en-US", Some(HourCycle::H12))
1942 .unwrap()
1943 .pattern(TimeGranularity::Hour);
1944 assert_eq!(us.order, [TimeSegment::Hour, TimeSegment::Meridiem]);
1945 assert_eq!(us.literals, ["", "\u{202f}", ""]);
1946
1947 let japanese = RegionalTimeFormat::for_locale_with_cycle("ja-JP", Some(HourCycle::H12))
1948 .unwrap()
1949 .pattern(TimeGranularity::Hour);
1950 assert_eq!(japanese.order, [TimeSegment::Meridiem, TimeSegment::Hour]);
1951 assert_eq!(japanese.literals, ["", "", ""]);
1952 }
1953
1954 #[test]
1955 fn constructors_clamp() {
1956 assert_eq!(Time::new(99, 99).hour, 23);
1957 assert_eq!(Time::new(99, 99).minute, 59);
1958 assert_eq!(Time::new(1, 1).with_second(99).second, 59);
1959 }
1960}
1961
1962fn clamp_time(t: Time, min: Option<Time>, max: Option<Time>) -> Time {
1964 let secs = |x: Time| x.hour * 3600 + x.minute * 60 + x.second;
1965 let mut out = t;
1966 if let Some(lo) = min {
1967 if secs(out) < secs(lo) {
1968 out = lo;
1969 }
1970 }
1971 if let Some(hi) = max {
1972 if secs(out) > secs(hi) {
1973 out = hi;
1974 }
1975 }
1976 out
1977}
1978
1979#[cfg(test)]
1980mod clamp_tests {
1981 use super::*;
1982
1983 #[test]
1984 fn no_bounds_is_identity() {
1985 let t = Time::new(9, 30);
1986 assert_eq!(clamp_time(t, None, None), t);
1987 }
1988
1989 #[test]
1990 fn clamps_up_to_min() {
1991 let min = Time::new(9, 0);
1992 assert_eq!(clamp_time(Time::new(7, 30), Some(min), None), min);
1993 assert_eq!(
1995 clamp_time(Time::new(10, 0), Some(min), None),
1996 Time::new(10, 0)
1997 );
1998 }
1999
2000 #[test]
2001 fn clamps_down_to_max() {
2002 let max = Time::new(17, 0);
2003 assert_eq!(clamp_time(Time::new(23, 30), None, Some(max)), max);
2004 assert_eq!(
2005 clamp_time(Time::new(12, 0), None, Some(max)),
2006 Time::new(12, 0)
2007 );
2008 }
2009
2010 #[test]
2011 fn bounds_are_inclusive() {
2012 let min = Time::new(9, 0);
2013 let max = Time::new(17, 0);
2014 assert_eq!(clamp_time(min, Some(min), Some(max)), min);
2015 assert_eq!(clamp_time(max, Some(min), Some(max)), max);
2016 }
2017
2018 #[test]
2019 fn compares_by_total_seconds_not_by_field() {
2020 let min = Time::new(10, 0);
2022 assert_eq!(clamp_time(Time::new(9, 59), Some(min), None), min);
2023 }
2024
2025 #[test]
2026 fn seconds_participate_in_the_comparison() {
2027 let min = Time::new(9, 0).with_second(30);
2028 assert_eq!(
2029 clamp_time(Time::new(9, 0).with_second(10), Some(min), None),
2030 min
2031 );
2032 let inside = Time::new(9, 0).with_second(45);
2033 assert_eq!(clamp_time(inside, Some(min), None), inside);
2034 }
2035}
2036
2037crate::util::impl_component_styled!(TimeField);