Skip to main content

gpui_kit_material/components/
time_picker.rs

1//! M3 time pickers: the dial picker (vertical and horizontal, 12- and
2//! 24-hour) and the keyboard input picker.
3//!
4//! Reference: <https://m3.material.io/components/time-pickers/specs> and the
5//! Material 3 Design Kit "Date & time pickers" page (inspected tokens: a
6//! surface-container-high container with 28dp corners; a label-medium
7//! title; 96×80 display-large time selectors on primary-container
8//! (selected) or surface-container-highest; a 1dp outline period selector,
9//! 52×80 vertical or 216×38 horizontal, selected half on tertiary-container
10//! with title-medium labels; a 256dp surface-container-highest clock face
11//! with 48dp body-large numbers, the outer ring's selector 104dp and the
12//! 24-hour inner ring's 76dp from the center, a 2dp primary hand, an 8dp
13//! center dot and a 48dp primary selector; 96×72 display-medium input
14//! fields with a 2dp primary outline while editing and a body-small label).
15//!
16//! Behavior: keyboard text entry is GPUI Base's segmented
17//! [`gpui_base::TimeField`] (digits, arrows, Tab between segments, A/P for
18//! the period), decorated as Material input fields. The dial is a slider:
19//! pointer press and drag pick the nearest value (the inner ring for
20//! 13–23 and 00 on a 24-hour face), releasing on an hour moves on to the
21//! minutes, and the arrow keys step the value. Strings and number
22//! formatting come from the caller's [`TimePickerLabels`].
23//!
24//! md3.io's options beyond the kit: [`TimePickerState::seconds`] adds a
25//! seconds selector, dial unit and input segment; a minute step snaps the
26//! dial and its arrow keys to 5- or 15-minute marks; earliest/latest
27//! limits clamp every edit (dial, keys, period and typing) and fade the
28//! dial numbers that fall outside them.
29//!
30//! Motion: the hand turns along the shorter arc and changes rings on the
31//! default spatial spring, retargeting from wherever it is; reduced motion
32//! moves it at once.
33use super::InteractionPreview;
34use super::button::ButtonFocusRing;
35use super::date_parts::Activate;
36use super::text_field::type_style;
37use super::{ButtonVariant, IconButton, material_button};
38use crate::{
39    color::ColorRole,
40    icons::{Icon, Symbol},
41    motion::{self, MotionToken},
42    theme::color,
43    tokens::{
44        state,
45        typography::{TypeRole, rem},
46    },
47};
48use chrono::{NaiveTime, Timelike};
49use gpui::{prelude::*, *};
50use gpui_base::{HourCycle, TimeField, TimeFieldEvent, TimeFieldState, TimePrecision, TimeSegment};
51use std::{cell::Cell, f32::consts::PI, rc::Rc};
52
53type Format = Rc<dyn Fn(u32) -> SharedString>;
54type PeriodChange = Rc<dyn Fn(bool, &mut Window, &mut App)>;
55type TimeConfirm = Rc<dyn Fn(NaiveTime, &mut Window, &mut App)>;
56
57/// Every caller-supplied string of a time picker.
58#[derive(Clone)]
59pub struct TimePickerLabels {
60    /// The dial picker's title, e.g. "Select time".
61    pub select_title: SharedString,
62    /// The input picker's title, e.g. "Enter time".
63    pub enter_title: SharedString,
64    pub hour: SharedString,
65    pub minute: SharedString,
66    pub am: SharedString,
67    pub pm: SharedString,
68    pub confirm: SharedString,
69    pub cancel: SharedString,
70    /// Accessible names of the mode switch.
71    pub switch_to_input: SharedString,
72    pub switch_to_dial: SharedString,
73    /// A dial number ("7", "13").
74    pub number: Format,
75    /// A two-digit field value ("07", "05").
76    pub two_digits: Format,
77}
78
79/// 12-hour (with a period selector) or 24-hour.
80#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
81pub enum TimeFormat {
82    #[default]
83    TwelveHour,
84    TwentyFourHour,
85}
86
87/// The dial picker's layout.
88#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
89pub enum TimeOrientation {
90    #[default]
91    Vertical,
92    Horizontal,
93}
94
95/// Dial or keyboard input.
96#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
97pub enum TimeInputMode {
98    #[default]
99    Dial,
100    Input,
101}
102
103/// Which value the dial edits.
104#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
105pub enum TimeUnit {
106    #[default]
107    Hour,
108    Minute,
109    /// With [`TimePickerState::seconds`].
110    Second,
111}
112
113/// A time picker's state. Create it with `cx.new` and hand it to
114/// [`TimePicker`].
115pub struct TimePickerState {
116    labels: Rc<TimePickerLabels>,
117    time: NaiveTime,
118    format: TimeFormat,
119    mode: TimeInputMode,
120    unit: TimeUnit,
121    /// The hand's unwrapped target angle in turns, so a change turns along
122    /// the shorter arc.
123    hand: f32,
124    /// The minute grid the dial and its keys snap to (1 = every minute).
125    minute_step: u32,
126    /// The seconds selector's label, when seconds are shown.
127    second_label: Option<SharedString>,
128    /// Earliest and latest selectable times.
129    limits: Option<(NaiveTime, NaiveTime)>,
130    dial_focus: FocusHandle,
131    field: Entity<TimeFieldState>,
132    _subscription: Subscription,
133}
134
135/// Events a picker emits as the person edits it.
136#[derive(Clone, Copy, Debug, PartialEq, Eq)]
137pub enum TimePickerEvent {
138    Change(NaiveTime),
139}
140
141impl EventEmitter<TimePickerEvent> for TimePickerState {}
142
143impl TimePickerState {
144    pub fn new(
145        labels: TimePickerLabels,
146        time: NaiveTime,
147        window: &mut Window,
148        cx: &mut Context<Self>,
149    ) -> Self {
150        let field = cx.new(|cx| {
151            let mut field = TimeFieldState::new(window, cx).hour_cycle(HourCycle::H12);
152            field.set_time(time, window, cx);
153            field
154        });
155        let subscription = cx.subscribe_in(
156            &field,
157            window,
158            |this, field, event: &TimeFieldEvent, window, cx| {
159                let TimeFieldEvent::Change(typed) = *event;
160                // A typed time outside the limits lands on the nearest one.
161                let time = this.clamp(typed);
162                if time != typed {
163                    field.update(cx, |field, cx| field.set_time(time, window, cx));
164                }
165                this.apply(time);
166                cx.emit(TimePickerEvent::Change(time));
167                cx.notify();
168            },
169        );
170        let mut state = Self {
171            labels: Rc::new(labels),
172            time,
173            format: TimeFormat::TwelveHour,
174            mode: TimeInputMode::Dial,
175            unit: TimeUnit::Hour,
176            hand: 0.,
177            minute_step: 1,
178            second_label: None,
179            limits: None,
180            dial_focus: cx.focus_handle().tab_stop(true),
181            field,
182            _subscription: subscription,
183        };
184        state.hand = hand_turns(time, state.unit, None);
185        state
186    }
187    /// Use the 24-hour clock.
188    pub fn format(
189        mut self,
190        format: TimeFormat,
191        window: &mut Window,
192        cx: &mut Context<Self>,
193    ) -> Self {
194        self.set_format(format, window, cx);
195        self
196    }
197    pub fn input_mode(mut self) -> Self {
198        self.mode = TimeInputMode::Input;
199        self
200    }
201    /// Show seconds: a third selector (named `label`), a seconds dial unit
202    /// and a seconds input segment.
203    pub fn seconds(
204        mut self,
205        label: impl Into<SharedString>,
206        window: &mut Window,
207        cx: &mut Context<Self>,
208    ) -> Self {
209        self.second_label = Some(label.into());
210        let time = self.time;
211        self.field.update(cx, |field, cx| {
212            field.set_precision(TimePrecision::Second, window, cx);
213            field.set_time(time, window, cx);
214        });
215        self
216    }
217    /// Snap the dial and its arrow keys to every `step` minutes (e.g. 5 or
218    /// 15; 0 and 1 mean every minute).
219    pub fn minute_step(mut self, step: u32) -> Self {
220        self.minute_step = step.clamp(1, 30);
221        self
222    }
223    /// Only times from `earliest` to `latest` can be chosen; the current
224    /// time moves inside them.
225    pub fn limits(
226        mut self,
227        earliest: NaiveTime,
228        latest: NaiveTime,
229        window: &mut Window,
230        cx: &mut Context<Self>,
231    ) -> Self {
232        self.limits = Some((earliest.min(latest), earliest.max(latest)));
233        let time = self.clamp(self.time);
234        if time != self.time {
235            self.apply(time);
236            self.field
237                .update(cx, |field, cx| field.set_time(time, window, cx));
238        }
239        self
240    }
241    pub fn unit(mut self, unit: TimeUnit) -> Self {
242        self.unit = unit;
243        self.hand = hand_turns(self.time, unit, None);
244        self
245    }
246
247    pub fn labels(&self) -> &TimePickerLabels {
248        &self.labels
249    }
250    pub fn time(&self) -> NaiveTime {
251        self.time
252    }
253    pub fn current_format(&self) -> TimeFormat {
254        self.format
255    }
256    pub fn mode(&self) -> TimeInputMode {
257        self.mode
258    }
259    pub fn current_unit(&self) -> TimeUnit {
260        self.unit
261    }
262    pub fn shows_seconds(&self) -> bool {
263        self.second_label.is_some()
264    }
265    pub fn current_minute_step(&self) -> u32 {
266        self.minute_step
267    }
268    pub fn current_limits(&self) -> Option<(NaiveTime, NaiveTime)> {
269        self.limits
270    }
271    /// `time` moved inside the limits.
272    fn clamp(&self, time: NaiveTime) -> NaiveTime {
273        clamp_time(time, self.limits)
274    }
275    pub fn dial_focus(&self) -> &FocusHandle {
276        &self.dial_focus
277    }
278    /// Base's time field, for focusing input mode.
279    pub fn field(&self) -> &Entity<TimeFieldState> {
280        &self.field
281    }
282
283    fn apply(&mut self, time: NaiveTime) {
284        self.time = time;
285        self.hand = hand_turns(time, self.unit, Some(self.hand));
286    }
287
288    pub fn set_format(&mut self, format: TimeFormat, window: &mut Window, cx: &mut Context<Self>) {
289        self.format = format;
290        let cycle = match format {
291            TimeFormat::TwelveHour => HourCycle::H12,
292            TimeFormat::TwentyFourHour => HourCycle::H23,
293        };
294        self.field
295            .update(cx, |field, cx| field.set_hour_cycle(cycle, window, cx));
296        cx.notify();
297    }
298
299    /// Set the time as a person would, emitting [`TimePickerEvent::Change`].
300    pub fn set_time(&mut self, time: NaiveTime, window: &mut Window, cx: &mut Context<Self>) {
301        let time = self.clamp(time);
302        if time == self.time {
303            return;
304        }
305        self.apply(time);
306        self.field
307            .update(cx, |field, cx| field.set_time(time, window, cx));
308        cx.emit(TimePickerEvent::Change(time));
309        cx.notify();
310    }
311
312    /// Edit `unit` on the dial.
313    pub fn set_unit(&mut self, unit: TimeUnit, cx: &mut Context<Self>) {
314        if self.unit != unit {
315            self.unit = unit;
316            self.hand = hand_turns(self.time, unit, Some(self.hand));
317            cx.notify();
318        }
319    }
320
321    /// AM (`false`) or PM (`true`).
322    pub fn set_pm(&mut self, pm: bool, window: &mut Window, cx: &mut Context<Self>) {
323        let hour = self.time.hour() % 12 + if pm { 12 } else { 0 };
324        let time = self.time.with_hour(hour).unwrap_or(self.time);
325        self.set_time(time, window, cx);
326    }
327
328    pub fn toggle_mode(&mut self, window: &mut Window, cx: &mut Context<Self>) {
329        self.mode = match self.mode {
330            TimeInputMode::Dial => {
331                let field = self.field.clone();
332                window.defer(cx, move |window, cx| {
333                    let handle = field.read(cx).focus_handle(cx);
334                    handle.focus(window, cx);
335                });
336                TimeInputMode::Input
337            }
338            TimeInputMode::Input => TimeInputMode::Dial,
339        };
340        cx.notify();
341    }
342
343    /// Step the dial's value by `delta`, wrapping within the hour or day.
344    pub fn step(&mut self, delta: i32, window: &mut Window, cx: &mut Context<Self>) {
345        let time = match self.unit {
346            TimeUnit::Minute if self.minute_step > 1 => {
347                let minute = step_minutes(self.time.minute(), self.minute_step, delta);
348                self.time.with_minute(minute).unwrap_or(self.time)
349            }
350            unit => step_time(self.time, unit, self.format, delta),
351        };
352        self.set_time(time, window, cx);
353    }
354
355    /// Choose the dial value nearest a pointer at `offset` from the face's
356    /// center (in dp).
357    fn pick(&mut self, offset: Point<f32>, window: &mut Window, cx: &mut Context<Self>) {
358        let mut time = dial_pick(self.time, self.unit, self.format, offset);
359        if self.unit == TimeUnit::Minute && self.minute_step > 1 {
360            let step = self.minute_step as f32;
361            let minute = ((time.minute() as f32 / step).round() * step) as u32 % 60;
362            time = time.with_minute(minute).unwrap_or(time);
363        }
364        self.set_time(time, window, cx);
365    }
366}
367
368impl Render for TimePickerState {
369    fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
370        Empty
371    }
372}
373
374/// Radii of the dial's rings, from the face's center to a number's center.
375pub const DIAL_SIZE: f32 = 256.;
376const OUTER_RADIUS: f32 = 104.;
377const INNER_RADIUS: f32 = 76.;
378const NUMBER_CELL: f32 = 48.;
379
380/// The angle in turns (clockwise from 12 o'clock) of `time`'s `unit`.
381fn angle_of(time: NaiveTime, unit: TimeUnit) -> f32 {
382    match unit {
383        TimeUnit::Hour => (time.hour() % 12) as f32 / 12.,
384        TimeUnit::Minute => time.minute() as f32 / 60.,
385        TimeUnit::Second => time.second() as f32 / 60.,
386    }
387}
388
389/// `time` moved inside `limits` (earliest, latest).
390pub(crate) fn clamp_time(time: NaiveTime, limits: Option<(NaiveTime, NaiveTime)>) -> NaiveTime {
391    match limits {
392        Some((earliest, latest)) => time.max(earliest).min(latest),
393        None => time,
394    }
395}
396
397/// Step `minute` by `delta` marks of a `step`-minute grid: an off-grid
398/// minute first moves to the neighboring mark in the step's direction.
399pub(crate) fn step_minutes(minute: u32, step: u32, delta: i32) -> u32 {
400    let (minute, step) = (minute as i32, step.max(1) as i32);
401    let floor = minute.div_euclid(step) * step;
402    let next = if floor == minute || delta > 0 {
403        floor + step * delta
404    } else {
405        floor + step * (delta + 1)
406    };
407    next.rem_euclid(60) as u32
408}
409
410/// Whether some time in hour `hour` (or, for minutes and seconds, `time`
411/// with that value) lies inside `limits`.
412pub(crate) fn within_limits(
413    time: NaiveTime,
414    unit: TimeUnit,
415    value: u32,
416    limits: Option<(NaiveTime, NaiveTime)>,
417) -> bool {
418    let Some((earliest, latest)) = limits else {
419        return true;
420    };
421    match unit {
422        TimeUnit::Hour => {
423            let start = NaiveTime::from_hms_opt(value, 0, 0);
424            let end = NaiveTime::from_hms_opt(value, 59, 59);
425            matches!((start, end), (Some(start), Some(end)) if start <= latest && end >= earliest)
426        }
427        TimeUnit::Minute => match time.with_minute(value) {
428            Some(t) => {
429                let (start, end) = (
430                    t.with_second(0).unwrap_or(t),
431                    t.with_second(59).unwrap_or(t),
432                );
433                start <= latest && end >= earliest
434            }
435            None => false,
436        },
437        TimeUnit::Second => time
438            .with_second(value)
439            .is_some_and(|t| t >= earliest && t <= latest),
440    }
441}
442
443/// The unwrapped target turn nearest `previous`.
444pub(crate) fn hand_turns(time: NaiveTime, unit: TimeUnit, previous: Option<f32>) -> f32 {
445    let angle = angle_of(time, unit);
446    match previous {
447        None => angle,
448        Some(previous) => {
449            let delta = (angle - previous).rem_euclid(1.);
450            let delta = if delta > 0.5 { delta - 1. } else { delta };
451            previous + delta
452        }
453    }
454}
455
456/// Whether the hand sits on the 24-hour face's inner ring (00 and 13–23).
457pub(crate) fn inner_ring(time: NaiveTime, unit: TimeUnit, format: TimeFormat) -> bool {
458    format == TimeFormat::TwentyFourHour
459        && unit == TimeUnit::Hour
460        && (time.hour() == 0 || time.hour() > 12)
461}
462
463pub(crate) fn step_time(
464    time: NaiveTime,
465    unit: TimeUnit,
466    format: TimeFormat,
467    delta: i32,
468) -> NaiveTime {
469    match unit {
470        TimeUnit::Minute => {
471            let minute = (time.minute() as i32 + delta).rem_euclid(60) as u32;
472            time.with_minute(minute).unwrap_or(time)
473        }
474        TimeUnit::Second => {
475            let second = (time.second() as i32 + delta).rem_euclid(60) as u32;
476            time.with_second(second).unwrap_or(time)
477        }
478        TimeUnit::Hour => {
479            let hour = match format {
480                TimeFormat::TwentyFourHour => (time.hour() as i32 + delta).rem_euclid(24) as u32,
481                // A 12-hour dial steps within the period.
482                TimeFormat::TwelveHour => {
483                    time.hour() / 12 * 12 + (time.hour() as i32 % 12 + delta).rem_euclid(12) as u32
484                }
485            };
486            time.with_hour(hour).unwrap_or(time)
487        }
488    }
489}
490
491/// The value nearest `offset` (dp from the center, y down).
492pub(crate) fn dial_pick(
493    time: NaiveTime,
494    unit: TimeUnit,
495    format: TimeFormat,
496    offset: Point<f32>,
497) -> NaiveTime {
498    let turn = (offset.x.atan2(-offset.y) / (2. * PI)).rem_euclid(1.);
499    match unit {
500        TimeUnit::Minute => {
501            let minute = (turn * 60.).round() as u32 % 60;
502            time.with_minute(minute).unwrap_or(time)
503        }
504        TimeUnit::Second => {
505            let second = (turn * 60.).round() as u32 % 60;
506            time.with_second(second).unwrap_or(time)
507        }
508        TimeUnit::Hour => {
509            let position = (turn * 12.).round() as u32 % 12;
510            let hour = match format {
511                TimeFormat::TwelveHour => time.hour() / 12 * 12 + position,
512                TimeFormat::TwentyFourHour => {
513                    let distance = (offset.x * offset.x + offset.y * offset.y).sqrt();
514                    let inner = distance < (OUTER_RADIUS + INNER_RADIUS) / 2.;
515                    match (inner, position) {
516                        (true, 0) => 0,
517                        (true, p) => p + 12,
518                        (false, 0) => 12,
519                        (false, p) => p,
520                    }
521                }
522            };
523            time.with_hour(hour).unwrap_or(time)
524        }
525    }
526}
527
528fn point_on(turn: f32, radius: f32) -> (f32, f32) {
529    let a = turn * 2. * PI;
530    (
531        DIAL_SIZE / 2. + radius * a.sin(),
532        DIAL_SIZE / 2. - radius * a.cos(),
533    )
534}
535
536/// The 256dp clock face (`.Building Blocks/Clock face`): numbers, the
537/// hand and the selector. Interactive over a [`TimePickerState`], or a
538/// static specimen of `time`.
539#[derive(gpui::IntoElement)]
540pub struct ClockDial {
541    id: ElementId,
542    state: Option<Entity<TimePickerState>>,
543    time: NaiveTime,
544    unit: TimeUnit,
545    format: TimeFormat,
546    labels: Rc<TimePickerLabels>,
547    second_label: Option<SharedString>,
548    limits: Option<(NaiveTime, NaiveTime)>,
549}
550
551impl ClockDial {
552    /// A live dial editing `state`.
553    pub fn new(id: impl Into<ElementId>, state: &Entity<TimePickerState>, cx: &App) -> Self {
554        let s = state.read(cx);
555        Self {
556            id: id.into(),
557            state: Some(state.clone()),
558            time: s.time,
559            unit: s.unit,
560            format: s.format,
561            labels: s.labels.clone(),
562            second_label: s.second_label.clone(),
563            limits: s.limits,
564        }
565    }
566    /// A static specimen of `time`.
567    pub fn specimen(
568        id: impl Into<ElementId>,
569        labels: Rc<TimePickerLabels>,
570        time: NaiveTime,
571        unit: TimeUnit,
572        format: TimeFormat,
573    ) -> Self {
574        Self {
575            id: id.into(),
576            state: None,
577            time,
578            unit,
579            format,
580            labels,
581            second_label: None,
582            limits: None,
583        }
584    }
585}
586
587impl RenderOnce for ClockDial {
588    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
589        let (time, unit, format) = (self.time, self.unit, self.format);
590        let inner = inner_ring(time, unit, format);
591        let target_radius = if inner { INNER_RADIUS } else { OUTER_RADIUS };
592        let (turn, radius) = match &self.state {
593            Some(state) => {
594                let target = state.read(cx).hand;
595                (
596                    motion::token_value(
597                        (self.id.clone(), "hand"),
598                        target,
599                        MotionToken::DefaultSpatial,
600                        window,
601                        cx,
602                    ),
603                    motion::token_value(
604                        (self.id.clone(), "radius"),
605                        target_radius,
606                        MotionToken::DefaultSpatial,
607                        window,
608                        cx,
609                    ),
610                )
611            }
612            None => (angle_of(time, unit), target_radius),
613        };
614        let primary = color(ColorRole::PRIMARY);
615        let on_primary = color(ColorRole::ON_PRIMARY);
616        let between = match unit {
617            TimeUnit::Minute => time.minute() % 5 != 0,
618            TimeUnit::Second => time.second() % 5 != 0,
619            TimeUnit::Hour => false,
620        };
621        let bounds = Rc::new(Cell::new(Bounds::<Pixels>::default()));
622        let observed = bounds.clone();
623        let hand = canvas(
624            move |b, _, _| observed.set(b),
625            move |b, _, window, _| {
626                let scale = b.size.width / DIAL_SIZE;
627                let at = |(x, y): (f32, f32)| b.origin + point(scale * x, scale * y);
628                let center = at((DIAL_SIZE / 2., DIAL_SIZE / 2.));
629                let tip = at(point_on(turn, radius));
630                let mut line = PathBuilder::stroke(scale * 2.);
631                line.move_to(center);
632                line.line_to(tip);
633                if let Ok(path) = line.build() {
634                    window.paint_path(path, primary);
635                }
636                let circle = |c: Point<Pixels>, r: Pixels, paint: Rgba, window: &mut Window| {
637                    window.paint_quad(
638                        fill(Bounds::new(c - point(r, r), size(r * 2., r * 2.)), paint)
639                            .corner_radii(r),
640                    );
641                };
642                circle(tip, scale * (NUMBER_CELL / 2.), primary, window);
643                circle(center, scale * 4., primary, window);
644                if between {
645                    circle(tip, scale * 2., on_primary, window);
646                }
647            },
648        )
649        .absolute()
650        .inset_0();
651        let numbers: Vec<(u32, f32, f32, SharedString)> = match (unit, format) {
652            (TimeUnit::Minute | TimeUnit::Second, _) => (0..12)
653                .map(|i| {
654                    (
655                        i * 5,
656                        i as f32 / 12.,
657                        OUTER_RADIUS,
658                        (self.labels.two_digits)(i * 5),
659                    )
660                })
661                .collect(),
662            (TimeUnit::Hour, TimeFormat::TwelveHour) => (0..12)
663                .map(|i| {
664                    let label = if i == 0 { 12 } else { i };
665                    (
666                        label,
667                        i as f32 / 12.,
668                        OUTER_RADIUS,
669                        (self.labels.number)(label),
670                    )
671                })
672                .collect(),
673            (TimeUnit::Hour, TimeFormat::TwentyFourHour) => (0..12)
674                .map(|i| {
675                    let label = if i == 0 { 12 } else { i };
676                    (
677                        label,
678                        i as f32 / 12.,
679                        OUTER_RADIUS,
680                        (self.labels.number)(label),
681                    )
682                })
683                .chain((0..12).map(|i| {
684                    let hour = if i == 0 { 0 } else { i + 12 };
685                    (
686                        hour,
687                        i as f32 / 12.,
688                        INNER_RADIUS,
689                        (self.labels.two_digits)(hour),
690                    )
691                }))
692                .collect(),
693        };
694        let selected = match (unit, format) {
695            (TimeUnit::Minute, _) => time.minute(),
696            (TimeUnit::Second, _) => time.second(),
697            (TimeUnit::Hour, TimeFormat::TwelveHour) => (time.hour() + 11) % 12 + 1,
698            (TimeUnit::Hour, TimeFormat::TwentyFourHour) => time.hour(),
699        };
700        let ink = color(ColorRole::ON_SURFACE);
701        let faded = state::disabled(ink);
702        let limits = self.limits;
703        // A 12-hour face's number names an hour in the current period.
704        let hour_of = move |label: u32| match format {
705            TimeFormat::TwelveHour => label % 12 + time.hour() / 12 * 12,
706            TimeFormat::TwentyFourHour => label,
707        };
708        let mut face = div()
709            .id(self.id.clone())
710            .relative()
711            .flex_shrink_0()
712            .size(rem(DIAL_SIZE))
713            .rounded_full()
714            .bg(color(ColorRole::SURFACE_CONTAINER_HIGHEST))
715            .role(Role::Slider)
716            .aria_label(match unit {
717                TimeUnit::Hour => self.labels.hour.clone(),
718                TimeUnit::Minute => self.labels.minute.clone(),
719                TimeUnit::Second => self
720                    .second_label
721                    .clone()
722                    .unwrap_or_else(|| self.labels.minute.clone()),
723            })
724            .aria_numeric_value(selected as f64)
725            .child(hand)
726            .children(numbers.into_iter().map(|(value, turn, r, label)| {
727                let (x, y) = point_on(turn, r);
728                div()
729                    .absolute()
730                    .left(rem(x - NUMBER_CELL / 2.))
731                    .top(rem(y - NUMBER_CELL / 2.))
732                    .size(rem(NUMBER_CELL))
733                    .flex()
734                    .items_center()
735                    .justify_center()
736                    .text_color(if value == selected {
737                        on_primary
738                    } else {
739                        let value = if unit == TimeUnit::Hour {
740                            hour_of(value)
741                        } else {
742                            value
743                        };
744                        if within_limits(time, unit, value, limits) {
745                            ink
746                        } else {
747                            faded
748                        }
749                    })
750                    .map(|d| type_style(d, TypeRole::BodyLarge))
751                    .child(label)
752            }));
753        let Some(state) = self.state else {
754            return face.into_any_element();
755        };
756        let focus = state.read(cx).dial_focus.clone();
757        let pressed = window.use_keyed_state((self.id.clone(), "dragging"), cx, |_, _| false);
758        let offset = move |position: Point<Pixels>, b: Bounds<Pixels>| {
759            let scale = f32::from(b.size.width) / DIAL_SIZE;
760            let c = b.center();
761            point(
762                f32::from(position.x - c.x) / scale,
763                f32::from(position.y - c.y) / scale,
764            )
765        };
766        face = face
767            .track_focus(&focus)
768            .cursor_pointer()
769            .on_mouse_down(MouseButton::Left, {
770                let (state, bounds, pressed) = (state.clone(), bounds.clone(), pressed.clone());
771                let focus = focus.clone();
772                move |e, window, cx| {
773                    focus.focus(window, cx);
774                    pressed.update(cx, |p, _| *p = true);
775                    let o = offset(e.position, bounds.get());
776                    state.update(cx, |s, cx| s.pick(o, window, cx));
777                }
778            })
779            .on_mouse_move({
780                let (state, bounds, pressed) = (state.clone(), bounds.clone(), pressed.clone());
781                move |e, window, cx| {
782                    if *pressed.read(cx) && e.pressed_button == Some(MouseButton::Left) {
783                        let o = offset(e.position, bounds.get());
784                        state.update(cx, |s, cx| s.pick(o, window, cx));
785                    }
786                }
787            })
788            .on_mouse_up(MouseButton::Left, {
789                let (state, pressed) = (state.clone(), pressed.clone());
790                move |_, _, cx| {
791                    if *pressed.read(cx) {
792                        pressed.update(cx, |p, _| *p = false);
793                        state.update(cx, |s, cx| {
794                            // Releasing moves on to the next unit.
795                            let next = match s.unit {
796                                TimeUnit::Hour => TimeUnit::Minute,
797                                TimeUnit::Minute if s.shows_seconds() => TimeUnit::Second,
798                                unit => unit,
799                            };
800                            s.set_unit(next, cx)
801                        });
802                    }
803                }
804            })
805            .on_mouse_up_out(MouseButton::Left, move |_, _, cx| {
806                pressed.update(cx, |p, _| *p = false);
807            })
808            .on_key_down(move |e, window, cx| {
809                let delta = match e.keystroke.key.as_str() {
810                    "up" | "right" => 1,
811                    "down" | "left" => -1,
812                    _ => return,
813                };
814                cx.stop_propagation();
815                state.update(cx, |s, cx| s.step(delta, window, cx));
816            });
817        ButtonFocusRing::new(face, focus, true, rem(DIAL_SIZE / 2.)).into_any_element()
818    }
819}
820
821/// A dial picker's hour or minute selector (`.Building Blocks/Input`): a
822/// 96×80 display-large box, primary-container while its unit is on the dial.
823#[derive(gpui::IntoElement)]
824pub struct TimeSelector {
825    id: ElementId,
826    value: SharedString,
827    label: SharedString,
828    selected: bool,
829    compact: bool,
830    preview: Option<InteractionPreview>,
831    on_click: Option<Activate>,
832}
833
834/// A compact selector's width: three of them (with seconds) fit where the
835/// kit's two 96dp selectors and a period selector do.
836const COMPACT_SELECTOR: f32 = 72.;
837
838impl TimeSelector {
839    /// `value` is the formatted number; `label` names it ("Hour").
840    pub fn new(
841        id: impl Into<ElementId>,
842        value: impl Into<SharedString>,
843        label: impl Into<SharedString>,
844    ) -> Self {
845        Self {
846            id: id.into(),
847            value: value.into(),
848            label: label.into(),
849            selected: false,
850            compact: false,
851            preview: None,
852            on_click: None,
853        }
854    }
855    pub fn selected(mut self, selected: bool) -> Self {
856        self.selected = selected;
857        self
858    }
859    /// 72dp wide with display-medium digits, for hours, minutes and seconds
860    /// side by side.
861    pub fn compact(mut self, compact: bool) -> Self {
862        self.compact = compact;
863        self
864    }
865    pub fn preview_state(mut self, state: InteractionPreview) -> Self {
866        self.preview = Some(state);
867        self
868    }
869    pub fn on_click(mut self, f: impl Fn(&mut Window, &mut App) + 'static) -> Self {
870        self.on_click = Some(Rc::new(f));
871        self
872    }
873}
874
875impl RenderOnce for TimeSelector {
876    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
877        let (fill, ink) = if self.selected {
878            (
879                color(ColorRole::PRIMARY_CONTAINER),
880                color(ColorRole::ON_PRIMARY_CONTAINER),
881            )
882        } else {
883            (
884                color(ColorRole::SURFACE_CONTAINER_HIGHEST),
885                color(ColorRole::ON_SURFACE),
886            )
887        };
888        let preview = self.preview;
889        let disabled = preview.is_some_and(InteractionPreview::is_disabled);
890        let hover = state::hovered(fill, ink);
891        let press = state::pressed(fill, ink);
892        let focus = window
893            .use_keyed_state((self.id.clone(), "focus"), cx, |_, cx| cx.focus_handle())
894            .read(cx)
895            .clone();
896        let mut button = gpui_base::Button::new(self.id)
897            .accessibility_label(self.label)
898            .disabled(disabled)
899            .w(rem(if self.compact { COMPACT_SELECTOR } else { 96. }))
900            .h(rem(80.))
901            .flex()
902            .items_center()
903            .justify_center()
904            .rounded(rem(8.))
905            .bg(fill)
906            .text_color(ink)
907            .map(|b| {
908                type_style(
909                    b,
910                    if self.compact {
911                        TypeRole::DisplayMedium
912                    } else {
913                        TypeRole::DisplayLarge
914                    },
915                )
916            })
917            .when(disabled, |b| b.opacity(state::DISABLED_CONTENT))
918            .child(self.value);
919        button = match preview {
920            Some(InteractionPreview::Hovered) => button.bg(hover),
921            Some(InteractionPreview::Focused) => button.bg(state::layer(fill, ink, state::FOCUS)),
922            Some(InteractionPreview::Pressed) => button.bg(press),
923            Some(_) => button,
924            None => button
925                .cursor_pointer()
926                .hover(move |s| s.bg(hover))
927                .active(move |s| s.bg(press)),
928        };
929        if let Some(f) = self.on_click.filter(|_| preview.is_none()) {
930            button = button.on_click(move |_, window, cx| f(window, cx));
931        }
932        let button = button.track_focus(&focus).tab_stop(preview.is_none());
933        ButtonFocusRing::new(button, focus, !disabled, rem(8.))
934            .forced(!disabled && preview == Some(InteractionPreview::Focused))
935    }
936}
937
938/// The AM/PM selector (`.Building Blocks/Period Selector`), stacked
939/// (52 wide) or side by side (216 wide).
940#[derive(gpui::IntoElement)]
941pub struct PeriodSelector {
942    id: ElementId,
943    am: SharedString,
944    pm: SharedString,
945    is_pm: bool,
946    horizontal: bool,
947    height: f32,
948    preview: Option<(bool, InteractionPreview)>,
949    on_change: Option<PeriodChange>,
950}
951
952impl PeriodSelector {
953    pub fn new(
954        id: impl Into<ElementId>,
955        am: impl Into<SharedString>,
956        pm: impl Into<SharedString>,
957    ) -> Self {
958        Self {
959            id: id.into(),
960            am: am.into(),
961            pm: pm.into(),
962            is_pm: false,
963            horizontal: false,
964            height: 80.,
965            preview: None,
966            on_change: None,
967        }
968    }
969    pub fn pm(mut self, pm: bool) -> Self {
970        self.is_pm = pm;
971        self
972    }
973    pub fn horizontal(mut self, horizontal: bool) -> Self {
974        self.horizontal = horizontal;
975        self
976    }
977    /// The stacked selector's height (80 beside dial selectors, 72 beside
978    /// input fields).
979    pub fn height(mut self, height: f32) -> Self {
980        self.height = height;
981        self
982    }
983    /// Force `state` on the AM (`false`) or PM (`true`) half.
984    pub fn preview_state(mut self, pm: bool, state: InteractionPreview) -> Self {
985        self.preview = Some((pm, state));
986        self
987    }
988    pub fn on_change(mut self, f: impl Fn(bool, &mut Window, &mut App) + 'static) -> Self {
989        self.on_change = Some(Rc::new(f));
990        self
991    }
992}
993
994impl RenderOnce for PeriodSelector {
995    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
996        let outline = color(ColorRole::OUTLINE);
997        let specimen = self.preview.is_some();
998        let half = |pm: bool, label: SharedString, window: &mut Window, cx: &mut App| {
999            let selected = pm == self.is_pm;
1000            let (fill, ink) = if selected {
1001                (
1002                    color(ColorRole::TERTIARY_CONTAINER),
1003                    color(ColorRole::ON_TERTIARY_CONTAINER),
1004                )
1005            } else {
1006                let ink = color(ColorRole::ON_SURFACE_VARIANT);
1007                (Rgba { a: 0., ..ink }, ink)
1008            };
1009            let forced = self.preview.filter(|(p, _)| *p == pm).map(|(_, s)| s);
1010            let hover = state::hovered(fill, ink);
1011            let press = state::pressed(fill, ink);
1012            let focus = window
1013                .use_keyed_state(
1014                    (self.id.clone(), if pm { "pm" } else { "am" }),
1015                    cx,
1016                    |_, cx| cx.focus_handle(),
1017                )
1018                .read(cx)
1019                .clone();
1020            let mut button = gpui_base::Button::new(if pm { "pm" } else { "am" })
1021                .accessibility_label(label.clone())
1022                .role(Role::RadioButton)
1023                .aria_toggled(if selected {
1024                    accesskit::Toggled::True
1025                } else {
1026                    accesskit::Toggled::False
1027                })
1028                .flex_1()
1029                .flex()
1030                .items_center()
1031                .justify_center()
1032                .bg(fill)
1033                .text_color(ink)
1034                .map(|b| type_style(b, TypeRole::TitleMedium))
1035                .child(label);
1036            button = match forced {
1037                Some(InteractionPreview::Hovered) => button.bg(hover),
1038                Some(InteractionPreview::Focused) => {
1039                    button.bg(state::layer(fill, ink, state::FOCUS))
1040                }
1041                Some(InteractionPreview::Pressed) => button.bg(press),
1042                Some(_) => button,
1043                None if !specimen => button
1044                    .cursor_pointer()
1045                    .hover(move |s| s.bg(hover))
1046                    .active(move |s| s.bg(press)),
1047                None => button,
1048            };
1049            if let Some(f) = self.on_change.clone().filter(|_| !specimen) {
1050                button = button.on_click(move |_, window, cx| f(pm, window, cx));
1051            }
1052            let button = button.track_focus(&focus).tab_stop(!specimen);
1053            ButtonFocusRing::new(button, focus, true, rem(0.))
1054                .inward()
1055                .forced(forced == Some(InteractionPreview::Focused))
1056        };
1057        let am = half(false, self.am.clone(), window, cx);
1058        let pm = half(true, self.pm.clone(), window, cx);
1059        let divider = div().flex_shrink_0().bg(outline);
1060        div()
1061            .id(self.id.clone())
1062            .flex()
1063            .flex_shrink_0()
1064            .overflow_hidden()
1065            .rounded(rem(8.))
1066            .border(rem(1.))
1067            .border_color(outline)
1068            .role(Role::RadioGroup)
1069            .map(|d| {
1070                if self.horizontal {
1071                    d.flex_row()
1072                        .w(rem(216.))
1073                        .h(rem(38.))
1074                        .child(am)
1075                        .child(divider.w(rem(1.)).h_full())
1076                        .child(pm)
1077                } else {
1078                    d.flex_col()
1079                        .w(rem(52.))
1080                        .h(rem(self.height))
1081                        .child(am)
1082                        .child(divider.h(rem(1.)).w_full())
1083                        .child(pm)
1084                }
1085            })
1086    }
1087}
1088
1089/// An M3 time picker over a [`TimePickerState`].
1090#[derive(gpui::IntoElement)]
1091pub struct TimePicker {
1092    id: ElementId,
1093    state: Entity<TimePickerState>,
1094    orientation: TimeOrientation,
1095    specimen: bool,
1096    on_confirm: Option<TimeConfirm>,
1097    on_cancel: Option<Activate>,
1098}
1099
1100impl TimePicker {
1101    pub fn new(id: impl Into<ElementId>, state: &Entity<TimePickerState>) -> Self {
1102        Self {
1103            id: id.into(),
1104            state: state.clone(),
1105            orientation: TimeOrientation::Vertical,
1106            specimen: false,
1107            on_confirm: None,
1108            on_cancel: None,
1109        }
1110    }
1111    pub fn orientation(mut self, orientation: TimeOrientation) -> Self {
1112        self.orientation = orientation;
1113        self
1114    }
1115    /// A static specimen: no tab stops, no motion, no pointer input.
1116    pub fn specimen(mut self) -> Self {
1117        self.specimen = true;
1118        self
1119    }
1120    pub fn on_confirm(mut self, f: impl Fn(NaiveTime, &mut Window, &mut App) + 'static) -> Self {
1121        self.on_confirm = Some(Rc::new(f));
1122        self
1123    }
1124    pub fn on_cancel(mut self, f: impl Fn(&mut Window, &mut App) + 'static) -> Self {
1125        self.on_cancel = Some(Rc::new(f));
1126        self
1127    }
1128}
1129
1130fn named(id: &ElementId, name: &'static str) -> ElementId {
1131    ElementId::NamedChild(id.clone().into(), name.into())
1132}
1133
1134fn symbol(name: &str) -> Icon {
1135    Icon::from(Symbol::named(name).expect("a Material Symbol in the pinned catalog"))
1136}
1137
1138impl RenderOnce for TimePicker {
1139    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1140        let s = self.state.read(cx);
1141        let labels = s.labels.clone();
1142        let (time, format, mode, unit) = (s.time, s.format, s.mode, s.unit);
1143        let second_label = s.second_label.clone();
1144        let seconds = second_label.is_some();
1145        let field = s.field.clone();
1146        let id = self.id.clone();
1147        let specimen = self.specimen;
1148        let preview = specimen.then_some(InteractionPreview::Enabled);
1149        let twelve = format == TimeFormat::TwelveHour;
1150        let horizontal =
1151            self.orientation == TimeOrientation::Horizontal && mode == TimeInputMode::Dial;
1152        let update = |f: fn(&mut TimePickerState, &mut Window, &mut Context<TimePickerState>)| {
1153            let state = self.state.clone();
1154            move |window: &mut Window, cx: &mut App| state.update(cx, |s, cx| f(s, window, cx))
1155        };
1156        let state = self.state.clone();
1157        let period = |height: f32, horizontal: bool| {
1158            let state = state.clone();
1159            let mut period =
1160                PeriodSelector::new(named(&id, "period"), labels.am.clone(), labels.pm.clone())
1161                    .pm(time.hour() >= 12)
1162                    .horizontal(horizontal)
1163                    .height(height)
1164                    .on_change(move |pm, window, cx| {
1165                        state.update(cx, |s, cx| s.set_pm(pm, window, cx))
1166                    });
1167            if specimen {
1168                period = period.preview_state(false, InteractionPreview::Enabled);
1169            }
1170            period
1171        };
1172        let display_hour = if twelve {
1173            (time.hour() + 11) % 12 + 1
1174        } else {
1175            time.hour()
1176        };
1177        let selector = |name: &'static str, which: TimeUnit, value: u32, label: SharedString| {
1178            let state = self.state.clone();
1179            let mut selector =
1180                TimeSelector::new(named(&id, name), (labels.two_digits)(value), label)
1181                    .selected(unit == which)
1182                    .compact(seconds)
1183                    .on_click(move |_, cx| state.update(cx, |s, cx| s.set_unit(which, cx)));
1184            if let Some(p) = preview {
1185                selector = selector.preview_state(p);
1186            }
1187            selector
1188        };
1189        let separator = |height: f32| {
1190            div()
1191                .w(rem(24.))
1192                .h(rem(height))
1193                .flex()
1194                .items_center()
1195                .justify_center()
1196                .text_color(color(ColorRole::ON_SURFACE))
1197                .map(|d| type_style(d, TypeRole::DisplayLarge))
1198                .child(":")
1199        };
1200        let time_row = div()
1201            .flex()
1202            .child(selector(
1203                "hour",
1204                TimeUnit::Hour,
1205                display_hour,
1206                labels.hour.clone(),
1207            ))
1208            .child(separator(80.))
1209            .child(selector(
1210                "minute",
1211                TimeUnit::Minute,
1212                time.minute(),
1213                labels.minute.clone(),
1214            ))
1215            .when_some(second_label.clone(), |row, label| {
1216                row.child(separator(80.)).child(selector(
1217                    "second",
1218                    TimeUnit::Second,
1219                    time.second(),
1220                    label,
1221                ))
1222            });
1223        let dial = if specimen {
1224            ClockDial::specimen(named(&id, "dial"), labels.clone(), time, unit, format)
1225        } else {
1226            ClockDial::new(named(&id, "dial"), &self.state, cx)
1227        };
1228        let title = if mode == TimeInputMode::Dial {
1229            labels.select_title.clone()
1230        } else {
1231            labels.enter_title.clone()
1232        };
1233        let header = div()
1234            .h(rem(40.))
1235            .pt(rem(24.))
1236            .px(rem(24.))
1237            .text_color(color(ColorRole::ON_SURFACE_VARIANT))
1238            .map(|d| type_style(d, TypeRole::LabelMedium))
1239            .child(title);
1240        let (mode_icon, mode_label) = match mode {
1241            TimeInputMode::Dial => ("keyboard", labels.switch_to_input.clone()),
1242            TimeInputMode::Input => ("schedule", labels.switch_to_dial.clone()),
1243        };
1244        let toggle = update(TimePickerState::toggle_mode);
1245        let mut mode_button = IconButton::new(named(&id, "mode"), symbol(mode_icon), mode_label)
1246            .on_click(move |_, window, cx| toggle(window, cx));
1247        if let Some(p) = preview {
1248            mode_button = mode_button.preview_state(p);
1249        }
1250        let text_button = |name: &'static str, label: SharedString, f: Activate| {
1251            let mut b = material_button(named(&id, name), ButtonVariant::Text)
1252                .child(label)
1253                .on_click(move |_, window, cx| f(window, cx));
1254            if let Some(p) = preview {
1255                b = b.preview_state(p);
1256            }
1257            b
1258        };
1259        let cancel = self.on_cancel.clone();
1260        let confirm = self.on_confirm.clone();
1261        let read = self.state.clone();
1262        let actions = div()
1263            .flex()
1264            .items_center()
1265            .justify_between()
1266            .pr(rem(24.))
1267            .pb(rem(20.))
1268            .pl(rem(12.))
1269            .when(horizontal, |a| a.pt(rem(20.)))
1270            .child(mode_button)
1271            .child(
1272                div()
1273                    .flex()
1274                    .gap(rem(8.))
1275                    .child(text_button(
1276                        "cancel",
1277                        labels.cancel.clone(),
1278                        Rc::new(move |window, cx| {
1279                            if let Some(f) = &cancel {
1280                                f(window, cx)
1281                            }
1282                        }),
1283                    ))
1284                    .child(text_button(
1285                        "confirm",
1286                        labels.confirm.clone(),
1287                        Rc::new(move |window, cx| {
1288                            let time = read.read(cx).time;
1289                            if let Some(f) = &confirm {
1290                                f(time, window, cx)
1291                            }
1292                        }),
1293                    )),
1294            );
1295        // Three compact selectors and two separators are 48dp wider than
1296        // the kit's two selectors and separator.
1297        let extra = if seconds { 48. } else { 0. };
1298        let width = extra
1299            + match (mode, horizontal, twelve) {
1300                (TimeInputMode::Input, _, false) => 264.,
1301                (_, true, _) => 572.,
1302                _ => 328.,
1303            };
1304        let surface = div()
1305            .id(self.id.clone())
1306            .w(rem(width))
1307            .flex()
1308            .flex_col()
1309            .flex_shrink_0()
1310            .rounded(rem(28.))
1311            .bg(color(ColorRole::SURFACE_CONTAINER_HIGH))
1312            .role(Role::Dialog)
1313            .aria_label(labels.select_title.clone());
1314        match mode {
1315            TimeInputMode::Input => {
1316                let field_focused = field.read(cx).focus_handle(cx).is_focused(window);
1317                let editing = field.read(cx).selected_segment();
1318                let segment_labels = labels.clone();
1319                let period_state = self.state.clone();
1320                let period_labels = labels.clone();
1321                let pm = time.hour() >= 12;
1322                let input = TimeField::new(named(&id, "field"), &field)
1323                    .disabled(specimen)
1324                    .flex()
1325                    .items_start()
1326                    .text_color(color(ColorRole::ON_SURFACE))
1327                    .map(|d| type_style(d, TypeRole::DisplayLarge))
1328                    .line_height(rem(72.))
1329                    .render_segment(move |segment, segment_state, _, _| {
1330                        let which = segment_state.segment();
1331                        if which == TimeSegment::Period {
1332                            let state = period_state.clone();
1333                            return segment
1334                                .clear_children()
1335                                .ml(rem(12.))
1336                                .child(
1337                                    PeriodSelector::new(
1338                                        "period",
1339                                        period_labels.am.clone(),
1340                                        period_labels.pm.clone(),
1341                                    )
1342                                    .pm(pm)
1343                                    .height(72.)
1344                                    .on_change(
1345                                        move |pm, window, cx| {
1346                                            state.update(cx, |s, cx| s.set_pm(pm, window, cx))
1347                                        },
1348                                    ),
1349                                )
1350                                .into_any_element();
1351                        }
1352                        let selected = which == editing;
1353                        let (fill, ink) = if selected {
1354                            (
1355                                color(ColorRole::PRIMARY_CONTAINER),
1356                                color(ColorRole::ON_PRIMARY_CONTAINER),
1357                            )
1358                        } else {
1359                            (
1360                                color(ColorRole::SURFACE_CONTAINER_HIGHEST),
1361                                color(ColorRole::ON_SURFACE),
1362                            )
1363                        };
1364                        let label = match which {
1365                            TimeSegment::Hour => segment_labels.hour.clone(),
1366                            TimeSegment::Second => second_label
1367                                .clone()
1368                                .unwrap_or_else(|| segment_labels.minute.clone()),
1369                            _ => segment_labels.minute.clone(),
1370                        };
1371                        segment
1372                            .clear_children()
1373                            .flex()
1374                            .flex_col()
1375                            .gap(rem(7.))
1376                            .w(rem(if seconds { COMPACT_SELECTOR } else { 96. }))
1377                            .child(
1378                                div()
1379                                    .h(rem(72.))
1380                                    .w_full()
1381                                    .flex()
1382                                    .items_center()
1383                                    .justify_center()
1384                                    .gap(rem(1.))
1385                                    .rounded(rem(8.))
1386                                    .bg(fill)
1387                                    .when(selected, |b| {
1388                                        b.border(rem(2.)).border_color(color(ColorRole::PRIMARY))
1389                                    })
1390                                    .text_color(ink)
1391                                    .map(|b| type_style(b, TypeRole::DisplayMedium))
1392                                    .child((segment_labels.two_digits)(segment_state.value()))
1393                                    .when(selected && field_focused, |b| {
1394                                        b.child(
1395                                            div()
1396                                                .w(rem(2.))
1397                                                .h(rem(42.))
1398                                                .bg(color(ColorRole::PRIMARY)),
1399                                        )
1400                                    }),
1401                            )
1402                            .child(
1403                                div()
1404                                    .text_color(color(ColorRole::ON_SURFACE_VARIANT))
1405                                    .map(|d| type_style(d, TypeRole::BodySmall))
1406                                    .child(label),
1407                            )
1408                            .into_any_element()
1409                    });
1410                surface
1411                    .gap(rem(20.))
1412                    .child(header)
1413                    .child(div().px(rem(24.)).child(input))
1414                    .child(actions)
1415            }
1416            TimeInputMode::Dial if horizontal => surface
1417                .child(header)
1418                .child(
1419                    div()
1420                        .flex()
1421                        .items_center()
1422                        .gap(rem(52.))
1423                        .px(rem(24.))
1424                        .child(
1425                            div()
1426                                .w(rem(216. + extra))
1427                                .flex()
1428                                .flex_col()
1429                                .gap(rem(16.))
1430                                .child(time_row)
1431                                .when(twelve, |c| c.child(period(38., true))),
1432                        )
1433                        .child(dial),
1434                )
1435                .child(actions),
1436            TimeInputMode::Dial => surface
1437                .gap(rem(20.))
1438                .child(header)
1439                .child(
1440                    div()
1441                        .flex()
1442                        .flex_col()
1443                        .items_center()
1444                        .gap(rem(36.))
1445                        .px(rem(24.))
1446                        .child(
1447                            div()
1448                                .flex()
1449                                .gap(rem(12.))
1450                                .child(time_row)
1451                                .when(twelve, |r| r.child(period(80., false))),
1452                        )
1453                        .child(dial),
1454                )
1455                .child(actions),
1456        }
1457    }
1458}
1459
1460#[cfg(test)]
1461#[path = "time_picker_tests.rs"]
1462mod tests;