Skip to main content

material_ui_rs/widget/component/
picker.rs

1//! Material 3 date and time picker constructors.
2
3use std::cell::Cell;
4use std::f32::consts::{FRAC_PI_2, TAU};
5use std::fmt;
6use std::ops::{Range, RangeInclusive};
7use std::sync::Arc;
8
9use iced_widget::button::{Status, Style};
10use iced_widget::canvas::{self, Canvas, Frame, Geometry, Path, Stroke, Text as CanvasText};
11use iced_widget::core::text as core_text;
12use iced_widget::core::time::Instant;
13use iced_widget::core::widget::{self, Tree, tree};
14use iced_widget::core::{
15    Background, Border, Clipboard, Color, Element, Event, Layout, Length, Padding, Point,
16    Rectangle, Shell, Size, Vector, Widget, alignment, border, event, layout, mouse, overlay,
17    renderer, touch, window,
18};
19use iced_widget::graphics::geometry;
20use iced_widget::renderer::wgpu::primitive;
21use iced_widget::text::{self, LineHeight};
22use iced_widget::{Column, Container, Row, Scrollable, Space, Stack, Text};
23
24use super::absolute_line_height;
25use super::button::Button;
26use super::support::{AnimatedScalar, alpha_color, duration_ms, lerp};
27use super::text_input;
28use super::viewport;
29use crate::style::button as button_style;
30use crate::utils::{disabled_container, disabled_text, mix, shadow_from_level};
31use crate::{Theme, fonts, tokens};
32
33const DEFAULT_START_YEAR: i32 = 1900;
34const DEFAULT_END_YEAR: i32 = 2100;
35const MILLIS_PER_DAY: i64 = 86_400_000;
36const WEEKDAYS_SHORT: [&str; 7] = ["S", "M", "T", "W", "T", "F", "S"];
37const MONTHS: [&str; 12] = [
38    "January",
39    "February",
40    "March",
41    "April",
42    "May",
43    "June",
44    "July",
45    "August",
46    "September",
47    "October",
48    "November",
49    "December",
50];
51const MONTHS_ABBR: [&str; 12] = [
52    "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
53];
54const HOURS: [u8; 12] = [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
55const EXTRA_HOURS: [u8; 12] = [0, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23];
56const MINUTES: [u8; 12] = [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55];
57const CLOCK_ANGLE_SCALE: f32 = 10_000.0;
58const CLOCK_LABEL_CLIP_STRIPS: usize = 40;
59const DATE_DISPLAY_PARALLAX_OFFSET: f32 = -48.0;
60const RANGE_PICKER_RENDER_MONTHS_BEFORE: i32 = 2;
61const RANGE_PICKER_RENDER_MONTHS_AFTER: i32 = 24;
62const TIME_SCROLL_TOUCH_SLOP: f32 = 8.0;
63const TIME_SCROLL_FLING_MIN_VELOCITY: f32 = 120.0;
64const TIME_SCROLL_FLING_MAX_VELOCITY: f32 = 2800.0;
65const TIME_SCROLL_FLING_DECELERATION: f32 = 3200.0;
66
67/// A day of week used for date picker layout and formatting.
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
69pub enum Weekday {
70    Sunday,
71    Monday,
72    Tuesday,
73    Wednesday,
74    Thursday,
75    Friday,
76    Saturday,
77}
78
79impl Weekday {
80    const ALL: [Self; 7] = [
81        Self::Sunday,
82        Self::Monday,
83        Self::Tuesday,
84        Self::Wednesday,
85        Self::Thursday,
86        Self::Friday,
87        Self::Saturday,
88    ];
89
90    fn sunday_first_index(self) -> usize {
91        match self {
92            Self::Sunday => 0,
93            Self::Monday => 1,
94            Self::Tuesday => 2,
95            Self::Wednesday => 3,
96            Self::Thursday => 4,
97            Self::Friday => 5,
98            Self::Saturday => 6,
99        }
100    }
101
102    pub fn short_label(self) -> &'static str {
103        WEEKDAYS_SHORT[self.sunday_first_index()]
104    }
105
106    pub fn name(self) -> &'static str {
107        match self {
108            Self::Sunday => "Sunday",
109            Self::Monday => "Monday",
110            Self::Tuesday => "Tuesday",
111            Self::Wednesday => "Wednesday",
112            Self::Thursday => "Thursday",
113            Self::Friday => "Friday",
114            Self::Saturday => "Saturday",
115        }
116    }
117}
118
119/// A proleptic Gregorian calendar date.
120#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
121pub struct Date {
122    pub year: i32,
123    pub month: u8,
124    pub day: u8,
125}
126
127impl Date {
128    /// Creates a date when the year/month/day combination is valid.
129    pub fn new(year: i32, month: u8, day: u8) -> Option<Self> {
130        if !(1..=12).contains(&month) {
131            return None;
132        }
133
134        if !(1..=days_in_month(year, month)).contains(&day) {
135            return None;
136        }
137
138        Some(Self { year, month, day })
139    }
140
141    /// Returns today's UTC date.
142    pub fn today_utc() -> Self {
143        Self::from_utc_millis(current_utc_millis())
144    }
145
146    /// Converts a UTC timestamp to the date at the start of that UTC day.
147    pub fn from_utc_millis(millis: i64) -> Self {
148        Self::from_days_since_epoch(millis.div_euclid(MILLIS_PER_DAY))
149    }
150
151    /// Converts this date to UTC milliseconds at the start of the day.
152    pub fn to_utc_millis(self) -> i64 {
153        self.days_since_epoch() * MILLIS_PER_DAY
154    }
155
156    fn from_days_since_epoch(days: i64) -> Self {
157        let (year, month, day) = civil_from_days(days);
158
159        Self { year, month, day }
160    }
161
162    fn days_since_epoch(self) -> i64 {
163        days_from_civil(self.year, self.month, self.day)
164    }
165
166    pub fn weekday(self) -> Weekday {
167        Weekday::ALL[(self.days_since_epoch() + 4).rem_euclid(7) as usize]
168    }
169
170    fn weekday_index_from(self, first_day_of_week: Weekday) -> usize {
171        (self.weekday().sunday_first_index() + 7 - first_day_of_week.sunday_first_index()) % 7
172    }
173
174    fn month_start(self) -> YearMonth {
175        YearMonth {
176            year: self.year,
177            month: self.month,
178        }
179    }
180
181    fn format_headline(self) -> String {
182        format!(
183            "{}, {} {}, {}",
184            self.weekday().name(),
185            MONTHS_ABBR[usize::from(self.month - 1)],
186            self.day,
187            self.year
188        )
189    }
190
191    fn format_input(self) -> String {
192        format!("{:02}/{:02}/{:04}", self.month, self.day, self.year)
193    }
194
195    fn format_range_label(self) -> String {
196        format!(
197            "{} {}, {}",
198            MONTHS_ABBR[usize::from(self.month - 1)],
199            self.day,
200            self.year
201        )
202    }
203}
204
205#[cfg(target_arch = "wasm32")]
206fn current_utc_millis() -> i64 {
207    js_sys::Date::now() as i64
208}
209
210#[cfg(not(target_arch = "wasm32"))]
211fn current_utc_millis() -> i64 {
212    std::time::SystemTime::now()
213        .duration_since(std::time::UNIX_EPOCH)
214        .map(|duration| duration.as_millis() as i64)
215        .unwrap_or(0)
216}
217
218/// A year and month pair used by the date picker.
219#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
220pub struct YearMonth {
221    pub year: i32,
222    pub month: u8,
223}
224
225impl YearMonth {
226    pub fn new(year: i32, month: u8) -> Option<Self> {
227        (1..=12).contains(&month).then_some(Self { year, month })
228    }
229
230    pub fn current_utc() -> Self {
231        Date::today_utc().month_start()
232    }
233
234    pub fn start_date(self) -> Date {
235        Date {
236            year: self.year,
237            month: self.month,
238            day: 1,
239        }
240    }
241
242    pub fn add_months(self, delta: i32) -> Self {
243        let month_index = self.year * 12 + i32::from(self.month - 1) + delta;
244        let year = month_index.div_euclid(12);
245        let month = month_index.rem_euclid(12) as u8 + 1;
246
247        Self { year, month }
248    }
249
250    pub fn format(self) -> String {
251        format!("{} {}", MONTHS[usize::from(self.month - 1)], self.year)
252    }
253}
254
255/// Date picker display mode.
256#[derive(Debug, Clone, Copy, PartialEq, Eq)]
257pub enum DateDisplayMode {
258    Picker,
259    Input,
260}
261
262/// Date and year enablement used by Material date pickers.
263#[derive(Clone)]
264pub struct SelectableDates {
265    is_selectable_date: Arc<dyn Fn(Date) -> bool + Send + Sync + 'static>,
266    is_selectable_year: Arc<dyn Fn(i32) -> bool + Send + Sync + 'static>,
267}
268
269impl fmt::Debug for SelectableDates {
270    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
271        f.debug_struct("SelectableDates").finish_non_exhaustive()
272    }
273}
274
275impl Default for SelectableDates {
276    fn default() -> Self {
277        Self::all()
278    }
279}
280
281impl SelectableDates {
282    /// Allows every date and year, matching the official default.
283    pub fn all() -> Self {
284        Self {
285            is_selectable_date: Arc::new(|_| true),
286            is_selectable_year: Arc::new(|_| true),
287        }
288    }
289
290    /// Creates selectable-date predicates.
291    pub fn new(
292        is_selectable_date: impl Fn(Date) -> bool + Send + Sync + 'static,
293        is_selectable_year: impl Fn(i32) -> bool + Send + Sync + 'static,
294    ) -> Self {
295        Self {
296            is_selectable_date: Arc::new(is_selectable_date),
297            is_selectable_year: Arc::new(is_selectable_year),
298        }
299    }
300
301    pub fn is_selectable_date(&self, date: Date) -> bool {
302        (self.is_selectable_date)(date)
303    }
304
305    pub fn is_selectable_year(&self, year: i32) -> bool {
306        (self.is_selectable_year)(year)
307    }
308}
309
310/// Formatter hooks used by Material date and date range pickers.
311#[derive(Clone)]
312pub struct DatePickerFormatter {
313    format_month_year: Arc<dyn Fn(YearMonth) -> String + Send + Sync + 'static>,
314    format_date: Arc<dyn Fn(Date, bool) -> String + Send + Sync + 'static>,
315}
316
317impl fmt::Debug for DatePickerFormatter {
318    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
319        f.debug_struct("DatePickerFormatter")
320            .finish_non_exhaustive()
321    }
322}
323
324impl Default for DatePickerFormatter {
325    fn default() -> Self {
326        Self::english()
327    }
328}
329
330impl DatePickerFormatter {
331    /// Creates an English formatter matching the default Material skeletons.
332    pub fn english() -> Self {
333        Self::new(YearMonth::format, |date, for_content_description| {
334            if for_content_description {
335                date.format_headline()
336            } else {
337                date.format_range_label()
338            }
339        })
340    }
341
342    /// Creates a formatter from month/year and date closures.
343    pub fn new(
344        format_month_year: impl Fn(YearMonth) -> String + Send + Sync + 'static,
345        format_date: impl Fn(Date, bool) -> String + Send + Sync + 'static,
346    ) -> Self {
347        Self {
348            format_month_year: Arc::new(format_month_year),
349            format_date: Arc::new(format_date),
350        }
351    }
352
353    pub fn format_month_year(&self, month: YearMonth) -> String {
354        (self.format_month_year)(month)
355    }
356
357    pub fn format_date(&self, date: Date, for_content_description: bool) -> String {
358        (self.format_date)(date, for_content_description)
359    }
360}
361
362#[derive(Debug, Clone)]
363struct DateDisplayAnimation {
364    progress: AnimatedScalar,
365    from: DateDisplayMode,
366    to: DateDisplayMode,
367}
368
369impl DateDisplayAnimation {
370    fn new(mode: DateDisplayMode) -> Self {
371        Self {
372            progress: AnimatedScalar::new(1.0),
373            from: mode,
374            to: mode,
375        }
376    }
377
378    fn start(&mut self, from: DateDisplayMode, to: DateDisplayMode, now: Instant) {
379        if from == to {
380            return;
381        }
382
383        self.from = from;
384        self.to = to;
385        self.progress = AnimatedScalar::new(0.0);
386        self.progress.set_target(
387            1.0,
388            now,
389            duration_ms(tokens::motion::DURATION_MEDIUM2_MS),
390            tokens::motion::EASING_EMPHASIZED,
391        );
392    }
393
394    fn advance(&mut self, now: Instant) -> bool {
395        self.progress.advance(now)
396    }
397
398    fn is_animating(&self) -> bool {
399        self.progress.is_animating()
400    }
401
402    fn progress(&self) -> f32 {
403        self.progress.value.clamp(0.0, 1.0)
404    }
405
406    #[cfg(test)]
407    fn offset(&self, input_height: f32) -> f32 {
408        self.mode_offset(self.to, input_height)
409    }
410
411    fn source_mode(&self) -> DateDisplayMode {
412        self.from
413    }
414
415    fn target_mode(&self) -> DateDisplayMode {
416        self.to
417    }
418
419    fn mode_offset(&self, mode: DateDisplayMode, input_height: f32) -> f32 {
420        if self.from == self.to {
421            return 0.0;
422        }
423
424        let progress = self.progress();
425        let full_slide_offset = input_height;
426
427        match (self.from, self.to, mode) {
428            (DateDisplayMode::Picker, DateDisplayMode::Input, DateDisplayMode::Picker) => {
429                DATE_DISPLAY_PARALLAX_OFFSET * progress
430            }
431            (DateDisplayMode::Picker, DateDisplayMode::Input, DateDisplayMode::Input) => {
432                full_slide_offset * (1.0 - progress)
433            }
434            (DateDisplayMode::Input, DateDisplayMode::Picker, DateDisplayMode::Input) => {
435                full_slide_offset * progress
436            }
437            (DateDisplayMode::Input, DateDisplayMode::Picker, DateDisplayMode::Picker) => {
438                DATE_DISPLAY_PARALLAX_OFFSET * (1.0 - progress)
439            }
440            _ => 0.0,
441        }
442    }
443
444    fn mode_alpha(&self, mode: DateDisplayMode) -> f32 {
445        if self.from == self.to {
446            return 1.0;
447        }
448
449        if mode == self.to {
450            self.progress()
451        } else if mode == self.from {
452            1.0 - self.progress()
453        } else {
454            0.0
455        }
456    }
457
458    fn content_height(
459        &self,
460        picker_height: f32,
461        input_height: f32,
462        display_mode: DateDisplayMode,
463    ) -> f32 {
464        let height_for = |mode| match mode {
465            DateDisplayMode::Picker => picker_height,
466            DateDisplayMode::Input => input_height,
467        };
468
469        if self.from == self.to {
470            return height_for(display_mode);
471        }
472
473        lerp(height_for(self.from), height_for(self.to), self.progress())
474    }
475
476    fn content_layout_height(&self, picker_height: f32, input_height: f32) -> f32 {
477        if self.from == self.to {
478            return picker_height.max(input_height);
479        }
480
481        picker_height.max(input_height)
482    }
483}
484
485#[derive(Debug, Clone)]
486struct PickerAnimation {
487    display: DateDisplayAnimation,
488    month: MonthAnimation,
489    year_picker: AnimatedScalar,
490    selection: AnimatedScalar,
491    selected_date: Option<Date>,
492}
493
494#[derive(Debug, Clone)]
495struct MonthAnimation {
496    progress: AnimatedScalar,
497    from: YearMonth,
498    to: YearMonth,
499}
500
501impl MonthAnimation {
502    fn new(month: YearMonth) -> Self {
503        Self {
504            progress: AnimatedScalar::new(1.0),
505            from: month,
506            to: month,
507        }
508    }
509
510    fn start(&mut self, from: YearMonth, to: YearMonth, now: Instant) {
511        if from == to {
512            return;
513        }
514
515        self.from = from;
516        self.to = to;
517        self.progress = AnimatedScalar::new(0.0);
518        self.progress.set_target(
519            1.0,
520            now,
521            duration_ms(tokens::motion::DURATION_MEDIUM2_MS),
522            tokens::motion::EASING_EMPHASIZED,
523        );
524    }
525
526    fn advance(&mut self, now: Instant) -> bool {
527        self.progress.advance(now)
528    }
529
530    fn is_animating(&self) -> bool {
531        self.progress.is_animating()
532    }
533
534    fn progress(&self) -> f32 {
535        self.progress.value.clamp(0.0, 1.0)
536    }
537
538    fn visible_months(&self, current: YearMonth) -> (YearMonth, YearMonth) {
539        if self.is_animating() {
540            (self.from, self.to)
541        } else {
542            (current, current)
543        }
544    }
545
546    fn month_offset(&self, month: YearMonth) -> f32 {
547        if self.from == self.to {
548            return 0.0;
549        }
550
551        let direction = month_delta(self.from, self.to).signum() as f32;
552        let width = month_grid_slide_width();
553        let progress = self.progress();
554
555        if month == self.from {
556            -direction * width * progress
557        } else if month == self.to {
558            direction * width * (1.0 - progress)
559        } else {
560            0.0
561        }
562    }
563}
564
565impl PickerAnimation {
566    fn new(
567        display_mode: DateDisplayMode,
568        year_picker_visible: bool,
569        selected_date: Option<Date>,
570        displayed_month: YearMonth,
571    ) -> Self {
572        Self {
573            display: DateDisplayAnimation::new(display_mode),
574            month: MonthAnimation::new(displayed_month),
575            year_picker: AnimatedScalar::new(if year_picker_visible { 1.0 } else { 0.0 }),
576            selection: AnimatedScalar::new(1.0),
577            selected_date,
578        }
579    }
580
581    fn set_year_picker_visible(&mut self, visible: bool, now: Instant) {
582        self.year_picker.set_target(
583            if visible { 1.0 } else { 0.0 },
584            now,
585            duration_ms(tokens::motion::DURATION_MEDIUM2_MS),
586            tokens::motion::EASING_EMPHASIZED,
587        );
588    }
589
590    fn select_date(&mut self, date: Date, now: Instant) {
591        self.selected_date = Some(date);
592        self.selection = AnimatedScalar::new(0.0);
593        self.selection.set_target(
594            1.0,
595            now,
596            duration_ms(tokens::motion::DURATION_SHORT4_MS),
597            tokens::motion::EASING_EMPHASIZED,
598        );
599    }
600
601    fn set_displayed_month(&mut self, from: YearMonth, to: YearMonth, now: Instant) {
602        self.month.start(from, to, now);
603    }
604
605    fn advance(&mut self, now: Instant) -> bool {
606        self.display.advance(now)
607            | self.month.advance(now)
608            | self.year_picker.advance(now)
609            | self.selection.advance(now)
610    }
611
612    fn is_animating(&self) -> bool {
613        self.display.is_animating()
614            || self.month.is_animating()
615            || self.year_picker.is_animating()
616            || self.selection.is_animating()
617    }
618
619    fn year_picker_progress(&self) -> f32 {
620        self.year_picker.value.clamp(0.0, 1.0)
621    }
622
623    fn selected_date_progress(&self, date: Date, selected: bool) -> f32 {
624        if selected && self.selected_date == Some(date) {
625            self.selection.value.clamp(0.0, 1.0)
626        } else if selected {
627            1.0
628        } else {
629            0.0
630        }
631    }
632
633    fn range_background_progress(&self, range_position: DateRangePosition) -> f32 {
634        if range_position.draws_range_background() && self.selection.is_animating() {
635            self.selection.value.clamp(0.0, 1.0)
636        } else if range_position.draws_range_background() {
637            1.0
638        } else {
639            0.0
640        }
641    }
642}
643
644/// State held by a Material date picker.
645#[derive(Debug, Clone)]
646pub struct DatePickerState {
647    selected_date: Option<Date>,
648    displayed_month: YearMonth,
649    display_mode: DateDisplayMode,
650    year_range: RangeInclusive<i32>,
651    selectable_dates: SelectableDates,
652    formatter: DatePickerFormatter,
653    first_day_of_week: Weekday,
654    year_picker_visible: bool,
655    input_value: String,
656    year_picker_scroll_id: widget::Id,
657    animation: PickerAnimation,
658}
659
660impl Default for DatePickerState {
661    fn default() -> Self {
662        Self::new(None)
663    }
664}
665
666impl DatePickerState {
667    /// Creates date picker state with the default Material year range (1900..=2100).
668    pub fn new(selected_date: Option<Date>) -> Self {
669        let displayed_month = selected_date
670            .map(Date::month_start)
671            .unwrap_or_else(YearMonth::current_utc);
672        let input_value = selected_date.map(Date::format_input).unwrap_or_default();
673
674        Self {
675            selected_date,
676            displayed_month,
677            display_mode: DateDisplayMode::Picker,
678            year_range: DEFAULT_START_YEAR..=DEFAULT_END_YEAR,
679            selectable_dates: SelectableDates::all(),
680            formatter: DatePickerFormatter::default(),
681            first_day_of_week: Weekday::Sunday,
682            year_picker_visible: false,
683            input_value,
684            year_picker_scroll_id: widget::Id::unique(),
685            animation: PickerAnimation::new(
686                DateDisplayMode::Picker,
687                false,
688                selected_date,
689                displayed_month,
690            ),
691        }
692    }
693
694    /// Sets the supported year range, clamping the displayed month to it.
695    pub fn year_range(mut self, year_range: RangeInclusive<i32>) -> Self {
696        self.year_range = year_range;
697        self.clamp_displayed_month();
698        self.animation.month = MonthAnimation::new(self.displayed_month);
699        self
700    }
701
702    /// Sets the initially displayed month independently from the selected date.
703    ///
704    /// Months outside the configured year range are ignored, matching Material state semantics.
705    pub fn initial_displayed_month(mut self, displayed_month: YearMonth) -> Self {
706        if self.year_range.contains(&displayed_month.year) {
707            self.displayed_month = displayed_month;
708            self.animation.month = MonthAnimation::new(displayed_month);
709        }
710
711        self
712    }
713
714    /// Sets date/year enablement predicates.
715    pub fn selectable_dates(mut self, selectable_dates: SelectableDates) -> Self {
716        self.selectable_dates = selectable_dates;
717        if self
718            .selected_date
719            .is_some_and(|date| !self.date_is_selectable(date))
720        {
721            self.selected_date = None;
722        }
723        self
724    }
725
726    /// Sets the first day of week used by the calendar grid.
727    pub fn first_day_of_week(mut self, first_day_of_week: Weekday) -> Self {
728        self.first_day_of_week = first_day_of_week;
729        self
730    }
731
732    /// Sets date display formatting hooks.
733    pub fn formatter(mut self, formatter: DatePickerFormatter) -> Self {
734        self.formatter = formatter;
735        self
736    }
737
738    pub fn selected_date(&self) -> Option<Date> {
739        self.selected_date
740    }
741
742    pub fn displayed_month(&self) -> YearMonth {
743        self.displayed_month
744    }
745
746    pub fn display_mode(&self) -> DateDisplayMode {
747        self.display_mode
748    }
749
750    pub fn year_picker_visible(&self) -> bool {
751        self.year_picker_visible
752    }
753
754    pub fn input_value(&self) -> &str {
755        &self.input_value
756    }
757
758    pub fn calendar_first_day_of_week(&self) -> Weekday {
759        self.first_day_of_week
760    }
761
762    pub fn date_formatter(&self) -> &DatePickerFormatter {
763        &self.formatter
764    }
765
766    /// Returns a task that positions the year picker near the displayed year.
767    pub fn scroll_to_year<Message>(&self) -> iced::Task<Message>
768    where
769        Message: Send + 'static,
770    {
771        iced::widget::operation::scroll_to(
772            self.year_picker_scroll_id.clone(),
773            widget::operation::scrollable::AbsoluteOffset {
774                x: None,
775                y: Some(year_picker_displayed_year_scroll_offset(
776                    &self.year_range,
777                    self.displayed_month.year,
778                )),
779            },
780        )
781    }
782
783    pub fn is_input_valid(&self) -> bool {
784        self.input_error().is_none()
785    }
786
787    pub fn input_error(&self) -> Option<String> {
788        date_input_error(
789            self.input_value(),
790            self.year_range.clone(),
791            &self.selectable_dates,
792            &self.formatter,
793            None,
794        )
795    }
796
797    /// Applies a date picker action.
798    pub fn update(&mut self, action: DatePickerAction) {
799        self.update_at(action, Instant::now());
800    }
801
802    /// Applies a date picker action and returns the follow-up scroll task used
803    /// when the year picker becomes visible.
804    pub fn update_and_scroll<Message>(&mut self, action: DatePickerAction) -> iced::Task<Message>
805    where
806        Message: Send + 'static,
807    {
808        self.update_and_scroll_at(action, Instant::now())
809    }
810
811    /// Applies a date picker action at `now` and returns the follow-up scroll
812    /// task used when the year picker becomes visible.
813    pub fn update_and_scroll_at<Message>(
814        &mut self,
815        action: DatePickerAction,
816        now: Instant,
817    ) -> iced::Task<Message>
818    where
819        Message: Send + 'static,
820    {
821        let was_year_picker_visible = self.year_picker_visible();
822
823        self.update_at(action, now);
824
825        if !was_year_picker_visible && self.year_picker_visible() {
826            self.scroll_to_year()
827        } else {
828            iced::Task::none()
829        }
830    }
831
832    /// Applies a date picker action, using `now` as the transition start time.
833    pub fn update_at(&mut self, action: DatePickerAction, now: Instant) {
834        match action {
835            DatePickerAction::SelectDate(date) => {
836                if self.date_is_selectable(date) {
837                    self.selected_date = Some(date);
838                    self.displayed_month = date.month_start();
839                    self.input_value = date.format_input();
840                    self.animation.select_date(date, now);
841                }
842            }
843            DatePickerAction::PreviousMonth => {
844                let previous = self.displayed_month.add_months(-1);
845                if self.year_range.contains(&previous.year) {
846                    let from = self.displayed_month;
847                    self.displayed_month = previous;
848                    self.animation.set_displayed_month(from, previous, now);
849                }
850            }
851            DatePickerAction::NextMonth => {
852                let next = self.displayed_month.add_months(1);
853                if self.year_range.contains(&next.year) {
854                    let from = self.displayed_month;
855                    self.displayed_month = next;
856                    self.animation.set_displayed_month(from, next, now);
857                }
858            }
859            DatePickerAction::ToggleYearPicker => {
860                self.year_picker_visible = !self.year_picker_visible;
861                self.animation
862                    .set_year_picker_visible(self.year_picker_visible, now);
863            }
864            DatePickerAction::SelectYear(year) => {
865                if self.year_range.contains(&year) {
866                    self.displayed_month.year = year;
867                    self.year_picker_visible = false;
868                    self.animation.set_year_picker_visible(false, now);
869                }
870            }
871            DatePickerAction::SetDisplayMode(display_mode) => {
872                if self.display_mode != display_mode {
873                    self.animation
874                        .display
875                        .start(self.display_mode, display_mode, now);
876
877                    if display_mode == DateDisplayMode::Input {
878                        self.input_value = self
879                            .selected_date
880                            .map(Date::format_input)
881                            .unwrap_or_default();
882                    } else if let Some(date) = self.selected_date {
883                        self.displayed_month = date.month_start();
884                    }
885
886                    self.display_mode = display_mode;
887                    self.year_picker_visible = false;
888                    self.animation.set_year_picker_visible(false, now);
889                }
890            }
891            DatePickerAction::InputChanged(value) => {
892                self.input_value = normalize_date_input(value);
893
894                if date_input_digit_count(&self.input_value) < 8 {
895                    self.selected_date = None;
896                } else if self.input_error().is_none()
897                    && let Some(date) = parse_date_input(&self.input_value)
898                {
899                    self.selected_date = Some(date);
900                    self.displayed_month = date.month_start();
901                    self.animation.select_date(date, now);
902                } else {
903                    self.selected_date = None;
904                }
905            }
906        }
907    }
908
909    /// Advances any running Material picker transitions.
910    pub fn advance(&mut self, now: Instant) -> bool {
911        self.animation.advance(now)
912    }
913
914    /// Returns whether this picker needs frame ticks for animations.
915    pub fn is_animating(&self) -> bool {
916        self.animation.is_animating()
917    }
918
919    /// Returns a frame subscription while picker animations are running.
920    pub fn subscription<Message, F>(&self, on_frame: F) -> iced::Subscription<Message>
921    where
922        Message: 'static,
923        F: Fn(Instant) -> Message + Send + Clone + 'static,
924    {
925        if self.is_animating() {
926            iced::window::frames().map(on_frame)
927        } else {
928            iced::Subscription::none()
929        }
930    }
931
932    fn date_is_in_range(&self, date: Date) -> bool {
933        self.year_range.contains(&date.year)
934    }
935
936    fn date_is_selectable(&self, date: Date) -> bool {
937        self.date_is_in_range(date)
938            && self.selectable_dates.is_selectable_year(date.year)
939            && self.selectable_dates.is_selectable_date(date)
940    }
941
942    fn clamp_displayed_month(&mut self) {
943        if self.displayed_month.year < *self.year_range.start() {
944            self.displayed_month.year = *self.year_range.start();
945            self.displayed_month.month = 1;
946        } else if self.displayed_month.year > *self.year_range.end() {
947            self.displayed_month.year = *self.year_range.end();
948            self.displayed_month.month = 12;
949        }
950
951        if self
952            .selected_date
953            .is_some_and(|date| !self.date_is_selectable(date))
954        {
955            self.selected_date = None;
956        }
957    }
958}
959
960/// Date picker state transitions.
961#[derive(Debug, Clone, PartialEq, Eq)]
962pub enum DatePickerAction {
963    SelectDate(Date),
964    PreviousMonth,
965    NextMonth,
966    ToggleYearPicker,
967    SelectYear(i32),
968    SetDisplayMode(DateDisplayMode),
969    InputChanged(String),
970}
971
972/// State held by a Material date range picker.
973#[derive(Debug, Clone)]
974pub struct DateRangePickerState {
975    selected_start_date: Option<Date>,
976    selected_end_date: Option<Date>,
977    displayed_month: YearMonth,
978    display_mode: DateDisplayMode,
979    year_range: RangeInclusive<i32>,
980    selectable_dates: SelectableDates,
981    formatter: DatePickerFormatter,
982    first_day_of_week: Weekday,
983    year_picker_visible: bool,
984    start_input_value: String,
985    end_input_value: String,
986    months_scroll_id: widget::Id,
987    year_picker_scroll_id: widget::Id,
988    animation: PickerAnimation,
989}
990
991impl Default for DateRangePickerState {
992    fn default() -> Self {
993        Self::new(None, None)
994    }
995}
996
997impl DateRangePickerState {
998    /// Creates date range picker state with the default Material year range (1900..=2100).
999    pub fn new(selected_start_date: Option<Date>, selected_end_date: Option<Date>) -> Self {
1000        let (selected_start_date, selected_end_date) =
1001            normalize_range_selection(selected_start_date, selected_end_date);
1002        let displayed_month = selected_start_date
1003            .or(selected_end_date)
1004            .map(Date::month_start)
1005            .unwrap_or_else(YearMonth::current_utc);
1006        let start_input_value = selected_start_date
1007            .map(Date::format_input)
1008            .unwrap_or_default();
1009        let end_input_value = selected_end_date
1010            .map(Date::format_input)
1011            .unwrap_or_default();
1012
1013        Self {
1014            selected_start_date,
1015            selected_end_date,
1016            displayed_month,
1017            display_mode: DateDisplayMode::Picker,
1018            year_range: DEFAULT_START_YEAR..=DEFAULT_END_YEAR,
1019            selectable_dates: SelectableDates::all(),
1020            formatter: DatePickerFormatter::default(),
1021            first_day_of_week: Weekday::Sunday,
1022            year_picker_visible: false,
1023            start_input_value,
1024            end_input_value,
1025            months_scroll_id: widget::Id::unique(),
1026            year_picker_scroll_id: widget::Id::unique(),
1027            animation: PickerAnimation::new(
1028                DateDisplayMode::Picker,
1029                false,
1030                selected_start_date.or(selected_end_date),
1031                displayed_month,
1032            ),
1033        }
1034    }
1035
1036    pub fn year_range(mut self, year_range: RangeInclusive<i32>) -> Self {
1037        self.year_range = year_range;
1038        self.clamp_displayed_month();
1039        self.animation.month = MonthAnimation::new(self.displayed_month);
1040        self.sync_input_values_from_selection();
1041        self
1042    }
1043
1044    /// Sets the initially displayed month independently from the selected range.
1045    ///
1046    /// Months outside the configured year range are ignored, matching Material state semantics.
1047    pub fn initial_displayed_month(mut self, displayed_month: YearMonth) -> Self {
1048        if self.year_range.contains(&displayed_month.year) {
1049            self.displayed_month = displayed_month;
1050            self.animation.month = MonthAnimation::new(displayed_month);
1051        }
1052
1053        self
1054    }
1055
1056    pub fn selectable_dates(mut self, selectable_dates: SelectableDates) -> Self {
1057        self.selectable_dates = selectable_dates;
1058        if self
1059            .selected_start_date
1060            .is_some_and(|date| !self.date_is_selectable(date))
1061        {
1062            self.selected_start_date = None;
1063            self.selected_end_date = None;
1064        } else if self
1065            .selected_end_date
1066            .is_some_and(|date| !self.date_is_selectable(date))
1067        {
1068            self.selected_end_date = None;
1069        }
1070        self.sync_input_values_from_selection();
1071        self
1072    }
1073
1074    /// Sets the first day of week used by the calendar grid.
1075    pub fn first_day_of_week(mut self, first_day_of_week: Weekday) -> Self {
1076        self.first_day_of_week = first_day_of_week;
1077        self
1078    }
1079
1080    /// Sets date display formatting hooks.
1081    pub fn formatter(mut self, formatter: DatePickerFormatter) -> Self {
1082        self.formatter = formatter;
1083        self
1084    }
1085
1086    pub fn selected_start_date(&self) -> Option<Date> {
1087        self.selected_start_date
1088    }
1089
1090    pub fn selected_end_date(&self) -> Option<Date> {
1091        self.selected_end_date
1092    }
1093
1094    pub fn displayed_month(&self) -> YearMonth {
1095        self.displayed_month
1096    }
1097
1098    pub fn year_picker_visible(&self) -> bool {
1099        self.year_picker_visible
1100    }
1101
1102    pub fn display_mode(&self) -> DateDisplayMode {
1103        self.display_mode
1104    }
1105
1106    pub fn calendar_first_day_of_week(&self) -> Weekday {
1107        self.first_day_of_week
1108    }
1109
1110    pub fn date_formatter(&self) -> &DatePickerFormatter {
1111        &self.formatter
1112    }
1113
1114    pub fn start_input_value(&self) -> &str {
1115        &self.start_input_value
1116    }
1117
1118    pub fn end_input_value(&self) -> &str {
1119        &self.end_input_value
1120    }
1121
1122    /// Returns a task that positions the range month list at the displayed month.
1123    pub fn scroll_to_displayed_month<Message>(&self) -> iced::Task<Message>
1124    where
1125        Message: Send + 'static,
1126    {
1127        iced::widget::operation::scroll_to(
1128            self.months_scroll_id.clone(),
1129            widget::operation::scrollable::AbsoluteOffset {
1130                x: None,
1131                y: Some(range_displayed_month_scroll_offset(self)),
1132            },
1133        )
1134    }
1135
1136    /// Returns a task that positions the year picker near the displayed year.
1137    pub fn scroll_to_year<Message>(&self) -> iced::Task<Message>
1138    where
1139        Message: Send + 'static,
1140    {
1141        iced::widget::operation::scroll_to(
1142            self.year_picker_scroll_id.clone(),
1143            widget::operation::scrollable::AbsoluteOffset {
1144                x: None,
1145                y: Some(year_picker_displayed_year_scroll_offset(
1146                    &self.year_range,
1147                    self.displayed_month.year,
1148                )),
1149            },
1150        )
1151    }
1152
1153    pub fn is_start_input_valid(&self) -> bool {
1154        self.start_input_error().is_none()
1155    }
1156
1157    pub fn is_end_input_valid(&self) -> bool {
1158        self.end_input_error().is_none()
1159    }
1160
1161    pub fn start_input_error(&self) -> Option<String> {
1162        let end = parse_date_input(self.end_input_value());
1163        date_input_error(
1164            self.start_input_value(),
1165            self.year_range.clone(),
1166            &self.selectable_dates,
1167            &self.formatter,
1168            end.map(DateInputRangeBound::StartBeforeOrEqual),
1169        )
1170    }
1171
1172    pub fn end_input_error(&self) -> Option<String> {
1173        let start = parse_date_input(self.start_input_value());
1174        date_input_error(
1175            self.end_input_value(),
1176            self.year_range.clone(),
1177            &self.selectable_dates,
1178            &self.formatter,
1179            start.map(DateInputRangeBound::EndAfterOrEqual),
1180        )
1181    }
1182
1183    /// Applies a date range picker action.
1184    pub fn update(&mut self, action: DateRangePickerAction) {
1185        self.update_at(action, Instant::now());
1186    }
1187
1188    /// Applies a date range picker action and returns the follow-up scroll task
1189    /// used when the year picker becomes visible.
1190    pub fn update_and_scroll<Message>(
1191        &mut self,
1192        action: DateRangePickerAction,
1193    ) -> iced::Task<Message>
1194    where
1195        Message: Send + 'static,
1196    {
1197        self.update_and_scroll_at(action, Instant::now())
1198    }
1199
1200    /// Applies a date range picker action at `now` and returns the follow-up
1201    /// scroll task used when the year picker becomes visible.
1202    pub fn update_and_scroll_at<Message>(
1203        &mut self,
1204        action: DateRangePickerAction,
1205        now: Instant,
1206    ) -> iced::Task<Message>
1207    where
1208        Message: Send + 'static,
1209    {
1210        let was_year_picker_visible = self.year_picker_visible();
1211
1212        self.update_at(action, now);
1213
1214        if !was_year_picker_visible && self.year_picker_visible() {
1215            self.scroll_to_year()
1216        } else {
1217            iced::Task::none()
1218        }
1219    }
1220
1221    /// Applies a date range picker action, using `now` as the transition start time.
1222    pub fn update_at(&mut self, action: DateRangePickerAction, now: Instant) {
1223        match action {
1224            DateRangePickerAction::SelectDate(date) => {
1225                if !self.date_is_selectable(date) {
1226                    return;
1227                }
1228
1229                match (self.selected_start_date, self.selected_end_date) {
1230                    (None, _) | (Some(_), Some(_)) => {
1231                        self.selected_start_date = Some(date);
1232                        self.selected_end_date = None;
1233                    }
1234                    (Some(start), None) if date < start => {
1235                        self.selected_start_date = Some(date);
1236                    }
1237                    (Some(_), None) => {
1238                        self.selected_end_date = Some(date);
1239                    }
1240                }
1241
1242                self.animation.select_date(date, now);
1243                self.sync_input_values_from_selection();
1244            }
1245            DateRangePickerAction::PreviousMonth => {
1246                let previous = self.displayed_month.add_months(-1);
1247                if self.year_range.contains(&previous.year) {
1248                    self.displayed_month = previous;
1249                }
1250            }
1251            DateRangePickerAction::NextMonth => {
1252                let next = self.displayed_month.add_months(1);
1253                if self.year_range.contains(&next.year) {
1254                    self.displayed_month = next;
1255                }
1256            }
1257            DateRangePickerAction::ToggleYearPicker => {
1258                self.year_picker_visible = !self.year_picker_visible;
1259                self.animation
1260                    .set_year_picker_visible(self.year_picker_visible, now);
1261            }
1262            DateRangePickerAction::SelectYear(year) => {
1263                if self.year_range.contains(&year) {
1264                    self.displayed_month.year = year;
1265                    self.year_picker_visible = false;
1266                    self.animation.set_year_picker_visible(false, now);
1267                }
1268            }
1269            DateRangePickerAction::SetDisplayMode(display_mode) => {
1270                if self.display_mode != display_mode {
1271                    self.animation
1272                        .display
1273                        .start(self.display_mode, display_mode, now);
1274
1275                    if display_mode == DateDisplayMode::Input {
1276                        self.sync_input_values_from_selection();
1277                    } else if let Some(start) = self.selected_start_date {
1278                        self.displayed_month = start.month_start();
1279                    }
1280
1281                    self.display_mode = display_mode;
1282                    self.year_picker_visible = false;
1283                    self.animation.set_year_picker_visible(false, now);
1284                }
1285            }
1286            DateRangePickerAction::StartInputChanged(value) => {
1287                self.start_input_value = normalize_date_input(value);
1288                let start = self.range_input_date(self.start_input_value());
1289
1290                if date_input_digit_count(&self.start_input_value) < 8 {
1291                    self.selected_start_date = None;
1292                    self.selected_end_date = None;
1293                } else if self.start_input_error().is_none()
1294                    && let Some(start) = start
1295                {
1296                    if self.selected_end_date.is_none_or(|end| start <= end) {
1297                        self.selected_start_date = Some(start);
1298                        self.displayed_month = start.month_start();
1299                        self.animation.select_date(start, now);
1300                    }
1301                } else {
1302                    self.selected_start_date = None;
1303                }
1304            }
1305            DateRangePickerAction::EndInputChanged(value) => {
1306                self.end_input_value = normalize_date_input(value);
1307                let end = self.range_input_date(self.end_input_value());
1308
1309                if date_input_digit_count(&self.end_input_value) < 8 {
1310                    self.selected_end_date = None;
1311                } else if self.end_input_error().is_none()
1312                    && let Some(end) = end
1313                {
1314                    if self.selected_start_date.is_some_and(|start| start <= end) {
1315                        self.selected_end_date = Some(end);
1316                        self.displayed_month = self
1317                            .selected_start_date
1318                            .map(Date::month_start)
1319                            .unwrap_or_else(|| end.month_start());
1320                        self.animation.select_date(end, now);
1321                    }
1322                } else {
1323                    self.selected_end_date = None;
1324                }
1325            }
1326        }
1327    }
1328
1329    /// Advances any running Material range picker transitions.
1330    pub fn advance(&mut self, now: Instant) -> bool {
1331        self.animation.advance(now)
1332    }
1333
1334    /// Returns whether this range picker needs frame ticks for animations.
1335    pub fn is_animating(&self) -> bool {
1336        self.animation.is_animating()
1337    }
1338
1339    /// Returns a frame subscription while range picker animations are running.
1340    pub fn subscription<Message, F>(&self, on_frame: F) -> iced::Subscription<Message>
1341    where
1342        Message: 'static,
1343        F: Fn(Instant) -> Message + Send + Clone + 'static,
1344    {
1345        if self.is_animating() {
1346            iced::window::frames().map(on_frame)
1347        } else {
1348            iced::Subscription::none()
1349        }
1350    }
1351
1352    fn date_is_in_range(&self, date: Date) -> bool {
1353        self.year_range.contains(&date.year)
1354    }
1355
1356    fn date_is_selectable(&self, date: Date) -> bool {
1357        self.date_is_in_range(date)
1358            && self.selectable_dates.is_selectable_year(date.year)
1359            && self.selectable_dates.is_selectable_date(date)
1360    }
1361
1362    fn date_range_position(&self, date: Date) -> DateRangePosition {
1363        match (self.selected_start_date, self.selected_end_date) {
1364            (Some(start), Some(end)) if start == end && date == start => DateRangePosition::Single,
1365            (Some(start), Some(_)) if date == start => DateRangePosition::Start,
1366            (Some(_), Some(end)) if date == end => DateRangePosition::End,
1367            (Some(start), Some(end)) if start < date && date < end => DateRangePosition::Middle,
1368            (Some(start), None) if date == start => DateRangePosition::Single,
1369            _ => DateRangePosition::None,
1370        }
1371    }
1372
1373    fn range_input_date(&self, input: &str) -> Option<Date> {
1374        let date = parse_date_input(input)?;
1375
1376        self.date_is_selectable(date).then_some(date)
1377    }
1378
1379    fn sync_input_values_from_selection(&mut self) {
1380        self.start_input_value = self
1381            .selected_start_date
1382            .map(Date::format_input)
1383            .unwrap_or_default();
1384        self.end_input_value = self
1385            .selected_end_date
1386            .map(Date::format_input)
1387            .unwrap_or_default();
1388    }
1389
1390    fn clamp_displayed_month(&mut self) {
1391        if self.displayed_month.year < *self.year_range.start() {
1392            self.displayed_month.year = *self.year_range.start();
1393            self.displayed_month.month = 1;
1394        } else if self.displayed_month.year > *self.year_range.end() {
1395            self.displayed_month.year = *self.year_range.end();
1396            self.displayed_month.month = 12;
1397        }
1398
1399        if self
1400            .selected_start_date
1401            .is_some_and(|date| !self.date_is_selectable(date))
1402        {
1403            self.selected_start_date = None;
1404            self.selected_end_date = None;
1405        } else if self
1406            .selected_end_date
1407            .is_some_and(|date| !self.date_is_selectable(date))
1408        {
1409            self.selected_end_date = None;
1410        }
1411    }
1412}
1413
1414/// Date range picker state transitions.
1415#[derive(Debug, Clone, PartialEq, Eq)]
1416pub enum DateRangePickerAction {
1417    SelectDate(Date),
1418    PreviousMonth,
1419    NextMonth,
1420    ToggleYearPicker,
1421    SelectYear(i32),
1422    SetDisplayMode(DateDisplayMode),
1423    StartInputChanged(String),
1424    EndInputChanged(String),
1425}
1426
1427#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1428enum DateRangePosition {
1429    None,
1430    Single,
1431    Start,
1432    Middle,
1433    End,
1434}
1435
1436impl DateRangePosition {
1437    fn draws_range_background(self) -> bool {
1438        matches!(self, Self::Start | Self::Middle | Self::End)
1439    }
1440
1441    fn is_middle(self) -> bool {
1442        self == Self::Middle
1443    }
1444}
1445
1446#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1447struct RangeMonthSelectionInfo {
1448    start_column: usize,
1449    start_row: usize,
1450    end_column: usize,
1451    end_row: usize,
1452    first_is_selection_start: bool,
1453    last_is_selection_end: bool,
1454}
1455
1456#[derive(Debug, Clone, Copy, PartialEq)]
1457struct RangeBackgroundRect {
1458    x: f32,
1459    y: f32,
1460    width: f32,
1461    height: f32,
1462}
1463
1464impl RangeBackgroundRect {
1465    fn corner_radius(self) -> f32 {
1466        self.height / 2.0
1467    }
1468}
1469
1470#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1471enum DateInputRangeBound {
1472    StartBeforeOrEqual(Date),
1473    EndAfterOrEqual(Date),
1474}
1475
1476/// Time picker selection mode.
1477#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1478pub enum TimePickerSelectionMode {
1479    Hour,
1480    Minute,
1481}
1482
1483/// AM/PM period.
1484#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1485pub enum Period {
1486    Am,
1487    Pm,
1488}
1489
1490/// Layout used by the analog time picker.
1491#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1492pub enum TimePickerLayout {
1493    Vertical,
1494    Horizontal,
1495}
1496
1497#[derive(Debug, Clone)]
1498struct TimePickerAnimation {
1499    clock_angle: AnimatedScalar,
1500    selection: AnimatedScalar,
1501    period: AnimatedScalar,
1502    previous_selection: TimePickerSelectionMode,
1503    selected_selection: TimePickerSelectionMode,
1504    previous_period: Period,
1505    selected_period: Period,
1506}
1507
1508#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1509enum ClockUpdate {
1510    Animate,
1511    Drag,
1512    RawAngle(i32),
1513    Settle,
1514}
1515
1516impl TimePickerAnimation {
1517    fn new(hour: u8, minute: u8, selection: TimePickerSelectionMode) -> Self {
1518        let period = period_for_hour(hour);
1519
1520        Self {
1521            clock_angle: AnimatedScalar::new(time_selector_angle(hour, minute, selection)),
1522            selection: AnimatedScalar::new(1.0),
1523            period: AnimatedScalar::new(1.0),
1524            previous_selection: selection,
1525            selected_selection: selection,
1526            previous_period: period,
1527            selected_period: period,
1528        }
1529    }
1530
1531    fn set_selection(
1532        &mut self,
1533        from: TimePickerSelectionMode,
1534        to: TimePickerSelectionMode,
1535        now: Instant,
1536    ) {
1537        if from == to {
1538            return;
1539        }
1540
1541        self.previous_selection = from;
1542        self.selected_selection = to;
1543        self.selection = AnimatedScalar::new(0.0);
1544        self.selection.set_target(
1545            1.0,
1546            now,
1547            duration_ms(tokens::motion::DURATION_MEDIUM1_MS),
1548            tokens::motion::EASING_EMPHASIZED,
1549        );
1550    }
1551
1552    fn set_period(&mut self, from: Period, to: Period, now: Instant) {
1553        if from == to {
1554            return;
1555        }
1556
1557        let start = self.period_progress(to);
1558        self.previous_period = from;
1559        self.selected_period = to;
1560        self.period = AnimatedScalar::new(start);
1561        self.period.set_target(
1562            1.0,
1563            now,
1564            duration_ms(tokens::motion::DURATION_SHORT4_MS),
1565            tokens::motion::EASING_EMPHASIZED,
1566        );
1567    }
1568
1569    fn set_clock_target(
1570        &mut self,
1571        hour: u8,
1572        minute: u8,
1573        selection: TimePickerSelectionMode,
1574        now: Instant,
1575    ) {
1576        let target = nearest_angle(
1577            self.clock_angle.value,
1578            time_selector_angle(hour, minute, selection),
1579        );
1580        if (target - self.clock_angle.value).abs() <= 1.0 / CLOCK_ANGLE_SCALE {
1581            self.clock_angle = AnimatedScalar::new(target);
1582            return;
1583        }
1584
1585        self.clock_angle.set_target(
1586            target,
1587            now,
1588            duration_ms(tokens::motion::DURATION_SHORT4_MS),
1589            tokens::motion::EASING_EMPHASIZED,
1590        );
1591    }
1592
1593    fn snap_clock_angle(&mut self, angle: f32) {
1594        let target = nearest_angle(self.clock_angle.value, angle);
1595        self.clock_angle = AnimatedScalar::new(target);
1596    }
1597
1598    fn advance(&mut self, now: Instant) -> bool {
1599        self.clock_angle.advance(now) | self.selection.advance(now) | self.period.advance(now)
1600    }
1601
1602    fn is_animating(&self) -> bool {
1603        self.clock_angle.is_animating()
1604            || self.selection.is_animating()
1605            || self.period.is_animating()
1606    }
1607
1608    fn clock_angle(&self) -> f32 {
1609        self.clock_angle.value
1610    }
1611
1612    fn selection_progress(&self, selection: TimePickerSelectionMode) -> f32 {
1613        let progress = self.selection.value.clamp(0.0, 1.0);
1614
1615        if selection == self.selected_selection {
1616            progress
1617        } else if selection == self.previous_selection {
1618            1.0 - progress
1619        } else {
1620            0.0
1621        }
1622    }
1623
1624    fn period_progress(&self, period: Period) -> f32 {
1625        if self.previous_period == self.selected_period {
1626            return if period == self.selected_period {
1627                1.0
1628            } else {
1629                0.0
1630            };
1631        }
1632
1633        let progress = self.period.value.clamp(0.0, 1.0);
1634
1635        if period == self.selected_period {
1636            progress
1637        } else if period == self.previous_period {
1638            1.0 - progress
1639        } else {
1640            0.0
1641        }
1642    }
1643}
1644
1645/// Display mode used by a time picker dialog.
1646#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1647pub enum TimePickerDisplayMode {
1648    Picker,
1649    Input,
1650    Scroll,
1651}
1652
1653impl TimePickerDisplayMode {
1654    /// Returns the alternate official dialog display mode.
1655    pub fn toggled(self) -> Self {
1656        match self {
1657            Self::Picker => Self::Input,
1658            Self::Input | Self::Scroll => Self::Picker,
1659        }
1660    }
1661
1662    /// Returns the alternate official scroll dialog display mode.
1663    pub fn scroll_toggled(self) -> Self {
1664        match self {
1665            Self::Scroll => Self::Input,
1666            Self::Picker | Self::Input => Self::Scroll,
1667        }
1668    }
1669
1670    fn title(self) -> &'static str {
1671        match self {
1672            Self::Picker => "Select Time",
1673            Self::Input => "Enter Time",
1674            Self::Scroll => "Select Time",
1675        }
1676    }
1677
1678    fn toggle_icon(self) -> &'static str {
1679        match self {
1680            Self::Picker => "keyboard",
1681            Self::Input => "schedule",
1682            Self::Scroll => "keyboard",
1683        }
1684    }
1685
1686    pub fn scroll_toggle_icon(self) -> &'static str {
1687        match self {
1688            Self::Scroll => "keyboard",
1689            Self::Picker | Self::Input => "swipe_vertical",
1690        }
1691    }
1692}
1693
1694/// State held by a Material time picker.
1695#[derive(Debug, Clone)]
1696pub struct TimePickerState {
1697    hour: u8,
1698    minute: u8,
1699    is_24_hour: bool,
1700    selection: TimePickerSelectionMode,
1701    auto_switch_to_minute: bool,
1702    hour_input: String,
1703    minute_input: String,
1704    scroll_hour_anchor: u8,
1705    scroll_minute_anchor: u8,
1706    animation: TimePickerAnimation,
1707}
1708
1709impl TimePickerState {
1710    /// Creates time picker state. Invalid initial values are clamped to official ranges.
1711    pub fn new(hour: u8, minute: u8, is_24_hour: bool) -> Self {
1712        let hour = hour.min(23);
1713        let minute = minute.min(59);
1714
1715        Self {
1716            hour,
1717            minute,
1718            is_24_hour,
1719            selection: TimePickerSelectionMode::Hour,
1720            auto_switch_to_minute: true,
1721            hour_input: two_digit(hour_for_display(hour, is_24_hour)),
1722            minute_input: two_digit(minute),
1723            scroll_hour_anchor: if is_24_hour {
1724                hour
1725            } else {
1726                hour_for_display(hour, is_24_hour)
1727            },
1728            scroll_minute_anchor: minute,
1729            animation: TimePickerAnimation::new(hour, minute, TimePickerSelectionMode::Hour),
1730        }
1731    }
1732
1733    /// Sets whether analog hour taps automatically move focus to minute selection.
1734    pub fn auto_switch_to_minute(mut self, auto_switch_to_minute: bool) -> Self {
1735        self.auto_switch_to_minute = auto_switch_to_minute;
1736        self
1737    }
1738
1739    pub fn hour(&self) -> u8 {
1740        self.hour
1741    }
1742
1743    pub fn minute(&self) -> u8 {
1744        self.minute
1745    }
1746
1747    pub fn is_24_hour(&self) -> bool {
1748        self.is_24_hour
1749    }
1750
1751    pub fn selection(&self) -> TimePickerSelectionMode {
1752        self.selection
1753    }
1754
1755    pub fn auto_switches_to_minute(&self) -> bool {
1756        self.auto_switch_to_minute
1757    }
1758
1759    pub fn hour_input(&self) -> &str {
1760        &self.hour_input
1761    }
1762
1763    pub fn minute_input(&self) -> &str {
1764        &self.minute_input
1765    }
1766
1767    pub fn period(&self) -> Period {
1768        period_for_hour(self.hour)
1769    }
1770
1771    pub fn hour_for_display(&self) -> u8 {
1772        hour_for_display(self.hour, self.is_24_hour)
1773    }
1774
1775    pub fn formatted_time(&self) -> String {
1776        if self.is_24_hour {
1777            format!("{:02}:{:02}", self.hour, self.minute)
1778        } else {
1779            let suffix = if self.period() == Period::Pm {
1780                "PM"
1781            } else {
1782                "AM"
1783            };
1784            format!("{:02}:{:02} {suffix}", self.hour_for_display(), self.minute)
1785        }
1786    }
1787
1788    pub fn is_hour_input_valid(&self) -> bool {
1789        let Ok(value) = self.hour_input.parse::<u8>() else {
1790            return false;
1791        };
1792        let min = if self.is_24_hour { 0 } else { 1 };
1793        let max = if self.is_24_hour { 23 } else { 12 };
1794
1795        (min..=max).contains(&value)
1796    }
1797
1798    pub fn is_minute_input_valid(&self) -> bool {
1799        self.minute_input
1800            .parse::<u8>()
1801            .is_ok_and(|value| value <= 59)
1802    }
1803
1804    pub fn is_input_valid(&self) -> bool {
1805        self.is_hour_input_valid() && self.is_minute_input_valid()
1806    }
1807
1808    /// Applies a time picker action.
1809    pub fn update(&mut self, action: TimePickerAction) {
1810        self.update_at(action, Instant::now());
1811    }
1812
1813    /// Applies a time picker action, using `now` as the transition start time.
1814    pub fn update_at(&mut self, action: TimePickerAction, now: Instant) {
1815        let previous_selection = self.selection;
1816        let previous_period = self.period();
1817        let clock_update = match &action {
1818            TimePickerAction::DragHour(_) | TimePickerAction::DragMinute(_) => ClockUpdate::Drag,
1819            TimePickerAction::DragHourAngle(_, angle)
1820            | TimePickerAction::DragMinuteAngle(_, angle) => ClockUpdate::RawAngle(*angle),
1821            TimePickerAction::FinishDrag => ClockUpdate::Settle,
1822            _ => ClockUpdate::Animate,
1823        };
1824
1825        match action {
1826            TimePickerAction::SetSelection(selection) => self.selection = selection,
1827            TimePickerAction::SelectHour(hour)
1828            | TimePickerAction::DragHour(hour)
1829            | TimePickerAction::DragHourAngle(hour, _) => {
1830                self.hour = if self.is_24_hour {
1831                    hour.min(23)
1832                } else {
1833                    hour_to_24(hour, self.period())
1834                };
1835                self.hour_input = two_digit(self.hour_for_display());
1836                self.sync_hour_scroll_anchor();
1837            }
1838            TimePickerAction::SelectMinute(minute) => {
1839                self.minute = minute.min(59);
1840                self.minute_input = two_digit(self.minute);
1841                self.sync_minute_scroll_anchor();
1842            }
1843            TimePickerAction::DragMinute(minute) => {
1844                self.minute = minute.min(59);
1845                self.minute_input = two_digit(self.minute);
1846                self.sync_minute_scroll_anchor();
1847            }
1848            TimePickerAction::DragMinuteAngle(minute, _) => {
1849                self.minute = minute.min(59);
1850                self.minute_input = two_digit(self.minute);
1851                self.sync_minute_scroll_anchor();
1852            }
1853            TimePickerAction::ScrollHour(hour) => {
1854                self.hour = if self.is_24_hour {
1855                    hour.min(23)
1856                } else {
1857                    hour_to_24(hour, self.period())
1858                };
1859                self.hour_input = two_digit(self.hour_for_display());
1860            }
1861            TimePickerAction::ScrollMinute(minute) => {
1862                self.minute = minute.min(59);
1863                self.minute_input = two_digit(self.minute);
1864            }
1865            TimePickerAction::SetPeriod(period) => {
1866                let display = self.hour_for_display();
1867                self.hour = hour_to_24(display, period);
1868                self.sync_hour_scroll_anchor();
1869            }
1870            TimePickerAction::TogglePeriod => {
1871                self.hour = (self.hour + 12) % 24;
1872                self.sync_hour_scroll_anchor();
1873            }
1874            TimePickerAction::HourInputChanged(input) => {
1875                self.hour_input = keep_digits(input, 2);
1876                if let Ok(value) = self.hour_input.parse::<u8>() {
1877                    let max = if self.is_24_hour { 23 } else { 12 };
1878                    let min = if self.is_24_hour { 0 } else { 1 };
1879                    if (min..=max).contains(&value) {
1880                        self.hour = if self.is_24_hour {
1881                            value
1882                        } else {
1883                            hour_to_24(value, self.period())
1884                        };
1885                        self.sync_hour_scroll_anchor();
1886                        if self.auto_switch_to_minute && !self.is_24_hour && value > 1 {
1887                            self.selection = TimePickerSelectionMode::Minute;
1888                        }
1889                    }
1890                }
1891            }
1892            TimePickerAction::MinuteInputChanged(input) => {
1893                self.minute_input = keep_digits(input, 2);
1894                if let Ok(value) = self.minute_input.parse::<u8>()
1895                    && value <= 59
1896                {
1897                    self.minute = value;
1898                    self.sync_minute_scroll_anchor();
1899                }
1900            }
1901            TimePickerAction::FinishDrag => {}
1902        }
1903
1904        self.animation
1905            .set_selection(previous_selection, self.selection, now);
1906        self.animation
1907            .set_period(previous_period, self.period(), now);
1908        match clock_update {
1909            ClockUpdate::Drag => {
1910                self.animation.snap_clock_angle(time_selector_angle(
1911                    self.hour,
1912                    self.minute,
1913                    self.selection,
1914                ));
1915            }
1916            ClockUpdate::RawAngle(angle) => {
1917                self.animation.snap_clock_angle(unpack_angle(angle));
1918            }
1919            ClockUpdate::Settle | ClockUpdate::Animate => {
1920                self.animation
1921                    .set_clock_target(self.hour, self.minute, self.selection, now);
1922            }
1923        }
1924    }
1925
1926    /// Advances any running Material time picker transitions.
1927    pub fn advance(&mut self, now: Instant) -> bool {
1928        self.animation.advance(now)
1929    }
1930
1931    /// Returns whether this picker needs frame ticks for animations.
1932    pub fn is_animating(&self) -> bool {
1933        self.animation.is_animating()
1934    }
1935
1936    /// Returns a frame subscription while picker animations are running.
1937    pub fn subscription<Message, F>(&self, on_frame: F) -> iced::Subscription<Message>
1938    where
1939        Message: 'static,
1940        F: Fn(Instant) -> Message + Send + Clone + 'static,
1941    {
1942        if self.is_animating() {
1943            iced::window::frames().map(on_frame)
1944        } else {
1945            iced::Subscription::none()
1946        }
1947    }
1948
1949    fn sync_hour_scroll_anchor(&mut self) {
1950        self.scroll_hour_anchor = if self.is_24_hour {
1951            self.hour
1952        } else {
1953            self.hour_for_display()
1954        };
1955    }
1956
1957    fn sync_minute_scroll_anchor(&mut self) {
1958        self.scroll_minute_anchor = self.minute;
1959    }
1960}
1961
1962impl Default for TimePickerState {
1963    fn default() -> Self {
1964        Self::new(12, 0, false)
1965    }
1966}
1967
1968/// Time picker state transitions.
1969#[derive(Debug, Clone, PartialEq, Eq)]
1970pub enum TimePickerAction {
1971    SetSelection(TimePickerSelectionMode),
1972    SelectHour(u8),
1973    SelectMinute(u8),
1974    DragHour(u8),
1975    DragMinute(u8),
1976    #[doc(hidden)]
1977    DragHourAngle(u8, i32),
1978    #[doc(hidden)]
1979    DragMinuteAngle(u8, i32),
1980    #[doc(hidden)]
1981    FinishDrag,
1982    ScrollHour(u8),
1983    ScrollMinute(u8),
1984    SetPeriod(Period),
1985    TogglePeriod,
1986    HourInputChanged(String),
1987    MinuteInputChanged(String),
1988}
1989
1990/// Visual options shared by date picker surfaces.
1991#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1992pub struct DatePickerOptions {
1993    pub show_mode_toggle: bool,
1994}
1995
1996impl Default for DatePickerOptions {
1997    fn default() -> Self {
1998        Self {
1999            show_mode_toggle: true,
2000        }
2001    }
2002}
2003
2004impl DatePickerOptions {
2005    /// Sets whether the picker/input mode toggle is shown in the header.
2006    pub const fn show_mode_toggle(mut self, show_mode_toggle: bool) -> Self {
2007        self.show_mode_toggle = show_mode_toggle;
2008        self
2009    }
2010}
2011
2012/// Visual options for the analog time picker.
2013#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2014pub struct TimePickerOptions {
2015    pub layout: TimePickerLayout,
2016}
2017
2018impl Default for TimePickerOptions {
2019    fn default() -> Self {
2020        Self {
2021            layout: TimePickerLayout::Vertical,
2022        }
2023    }
2024}
2025
2026impl TimePickerOptions {
2027    /// Sets the analog time picker layout.
2028    pub const fn layout(mut self, layout: TimePickerLayout) -> Self {
2029        self.layout = layout;
2030        self
2031    }
2032}
2033
2034/// Creates a Material 3 date picker.
2035pub fn date_picker<'a, Message, Renderer>(
2036    state: &'a DatePickerState,
2037    on_action: impl Fn(DatePickerAction) -> Message + Clone + 'a,
2038) -> Element<'a, Message, Theme, Renderer>
2039where
2040    Message: Clone + 'a,
2041    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2042    iced_widget::core::Font: Into<Renderer::Font>,
2043{
2044    date_picker_with(state, on_action, DatePickerOptions::default())
2045}
2046
2047/// Creates a Material 3 date picker with custom visual options.
2048pub fn date_picker_with<'a, Message, Renderer>(
2049    state: &'a DatePickerState,
2050    on_action: impl Fn(DatePickerAction) -> Message + Clone + 'a,
2051    options: DatePickerOptions,
2052) -> Element<'a, Message, Theme, Renderer>
2053where
2054    Message: Clone + 'a,
2055    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2056    iced_widget::core::Font: Into<Renderer::Font>,
2057{
2058    Container::new(date_picker_body(state, on_action, options.show_mode_toggle))
2059        .width(Length::Fixed(
2060            tokens::component::date_picker::CONTAINER_WIDTH,
2061        ))
2062        .height(Length::Fixed(
2063            tokens::component::date_picker::CONTAINER_HEIGHT,
2064        ))
2065        .style(date_picker_container_style)
2066        .into()
2067}
2068
2069/// Creates a Material 3 date range picker.
2070pub fn date_range_picker<'a, Message, Renderer>(
2071    state: &'a DateRangePickerState,
2072    on_action: impl Fn(DateRangePickerAction) -> Message + Clone + 'a,
2073) -> Element<'a, Message, Theme, Renderer>
2074where
2075    Message: Clone + 'a,
2076    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2077    iced_widget::core::Font: Into<Renderer::Font>,
2078{
2079    date_range_picker_with(state, on_action, DatePickerOptions::default())
2080}
2081
2082/// Creates a Material 3 date range picker with custom visual options.
2083pub fn date_range_picker_with<'a, Message, Renderer>(
2084    state: &'a DateRangePickerState,
2085    on_action: impl Fn(DateRangePickerAction) -> Message + Clone + 'a,
2086    options: DatePickerOptions,
2087) -> Element<'a, Message, Theme, Renderer>
2088where
2089    Message: Clone + 'a,
2090    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2091    iced_widget::core::Font: Into<Renderer::Font>,
2092{
2093    Container::new(date_range_picker_body(
2094        state,
2095        on_action,
2096        options.show_mode_toggle,
2097        tokens::component::date_picker::CONTAINER_HEIGHT,
2098    ))
2099    .width(Length::Fixed(
2100        tokens::component::date_picker::CONTAINER_WIDTH,
2101    ))
2102    .height(Length::Fixed(
2103        tokens::component::date_picker::CONTAINER_HEIGHT,
2104    ))
2105    .style(date_picker_container_style)
2106    .into()
2107}
2108
2109/// Creates a Material 3 date picker dialog surface.
2110pub fn date_picker_dialog<'a, Message, Renderer>(
2111    state: &'a DatePickerState,
2112    on_action: impl Fn(DatePickerAction) -> Message + Clone + 'a,
2113    actions: impl Into<Element<'a, Message, Theme, Renderer>>,
2114) -> Element<'a, Message, Theme, Renderer>
2115where
2116    Message: Clone + 'a,
2117    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2118    iced_widget::core::Font: Into<Renderer::Font>,
2119{
2120    date_picker_dialog_with(state, on_action, actions, DatePickerOptions::default())
2121}
2122
2123/// Creates a Material 3 date picker dialog surface with custom visual options.
2124pub fn date_picker_dialog_with<'a, Message, Renderer>(
2125    state: &'a DatePickerState,
2126    on_action: impl Fn(DatePickerAction) -> Message + Clone + 'a,
2127    actions: impl Into<Element<'a, Message, Theme, Renderer>>,
2128    options: DatePickerOptions,
2129) -> Element<'a, Message, Theme, Renderer>
2130where
2131    Message: Clone + 'a,
2132    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2133    iced_widget::core::Font: Into<Renderer::Font>,
2134{
2135    date_picker_dialog_surface(
2136        date_picker_body(state, on_action, options.show_mode_toggle),
2137        actions,
2138    )
2139}
2140
2141/// Creates a Material 3 date range picker dialog surface.
2142pub fn date_range_dialog<'a, Message, Renderer>(
2143    state: &'a DateRangePickerState,
2144    on_action: impl Fn(DateRangePickerAction) -> Message + Clone + 'a,
2145    actions: impl Into<Element<'a, Message, Theme, Renderer>>,
2146) -> Element<'a, Message, Theme, Renderer>
2147where
2148    Message: Clone + 'a,
2149    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2150    iced_widget::core::Font: Into<Renderer::Font>,
2151{
2152    date_range_dialog_with(state, on_action, actions, DatePickerOptions::default())
2153}
2154
2155/// Creates a Material 3 date range picker dialog surface with custom visual options.
2156pub fn date_range_dialog_with<'a, Message, Renderer>(
2157    state: &'a DateRangePickerState,
2158    on_action: impl Fn(DateRangePickerAction) -> Message + Clone + 'a,
2159    actions: impl Into<Element<'a, Message, Theme, Renderer>>,
2160    options: DatePickerOptions,
2161) -> Element<'a, Message, Theme, Renderer>
2162where
2163    Message: Clone + 'a,
2164    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2165    iced_widget::core::Font: Into<Renderer::Font>,
2166{
2167    date_picker_dialog_surface(
2168        date_range_picker_body(
2169            state,
2170            on_action,
2171            options.show_mode_toggle,
2172            date_picker_dialog_content_height(),
2173        ),
2174        actions,
2175    )
2176}
2177
2178/// Creates a right-aligned Material 3 date picker dialog actions row.
2179pub fn date_picker_dialog_actions<'a, Message, Renderer>(
2180    buttons: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
2181) -> Container<'a, Message, Theme, Renderer>
2182where
2183    Message: 'a,
2184    Renderer: iced_widget::core::Renderer + 'a,
2185{
2186    Container::new(
2187        Row::with_children(buttons)
2188            .spacing(tokens::component::date_picker::DIALOG_ACTIONS_MAIN_AXIS_SPACE)
2189            .align_y(alignment::Vertical::Center),
2190    )
2191    .width(Length::Fill)
2192    .align_x(alignment::Horizontal::Right)
2193}
2194
2195/// Creates a Material 3 analog time picker.
2196pub fn time_picker<'a, Message, Renderer>(
2197    state: &'a TimePickerState,
2198    on_action: impl Fn(TimePickerAction) -> Message + Clone + 'a,
2199) -> Element<'a, Message, Theme, Renderer>
2200where
2201    Message: Clone + 'a,
2202    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'static + 'a,
2203    iced_widget::core::Font: Into<Renderer::Font>,
2204{
2205    time_picker_with(state, on_action, TimePickerOptions::default())
2206}
2207
2208/// Creates a Material 3 analog time picker with custom visual options.
2209pub fn time_picker_with<'a, Message, Renderer>(
2210    state: &'a TimePickerState,
2211    on_action: impl Fn(TimePickerAction) -> Message + Clone + 'a,
2212    options: TimePickerOptions,
2213) -> Element<'a, Message, Theme, Renderer>
2214where
2215    Message: Clone + 'a,
2216    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'static + 'a,
2217    iced_widget::core::Font: Into<Renderer::Font>,
2218{
2219    Container::new(time_picker_body(state, on_action, options.layout))
2220        .padding(24.0)
2221        .style(time_picker_container_style)
2222        .into()
2223}
2224
2225/// Creates a Material 3 time picker dialog surface.
2226pub fn time_picker_dialog<'a, Message, Renderer>(
2227    state: &'a TimePickerState,
2228    display_mode: TimePickerDisplayMode,
2229    on_action: impl Fn(TimePickerAction) -> Message + Clone + 'a,
2230    on_display_mode_toggle: Option<Message>,
2231    actions: impl Into<Element<'a, Message, Theme, Renderer>>,
2232) -> Element<'a, Message, Theme, Renderer>
2233where
2234    Message: Clone + 'a,
2235    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'static + 'a,
2236    iced_widget::core::Font: Into<Renderer::Font>,
2237{
2238    let content = match display_mode {
2239        TimePickerDisplayMode::Picker => {
2240            time_picker_body(state, on_action, TimePickerLayout::Vertical)
2241        }
2242        TimePickerDisplayMode::Input => time_input_body(state, on_action),
2243        TimePickerDisplayMode::Scroll => time_scroll_body(state, on_action),
2244    };
2245
2246    let mut actions_row = Row::new()
2247        .height(Length::Fixed(tokens::component::button::CONTAINER_HEIGHT))
2248        .spacing(tokens::component::time_picker_dialog::ACTIONS_HORIZONTAL_SPACE)
2249        .align_y(alignment::Vertical::Center);
2250
2251    if let Some(message) = on_display_mode_toggle {
2252        actions_row = actions_row.push(time_picker_display_mode_toggle(display_mode, message));
2253    }
2254
2255    actions_row = actions_row
2256        .push(Space::new().width(Length::Fill))
2257        .push(actions.into());
2258
2259    Container::new(
2260        Column::new()
2261            .push(time_picker_dialog_title::<Message, Renderer>(display_mode))
2262            .push(
2263                Container::new(content)
2264                    .width(Length::Fill)
2265                    .height(time_picker_dialog_content_height(display_mode))
2266                    .align_x(alignment::Horizontal::Center)
2267                    .align_y(alignment::Vertical::Center),
2268            )
2269            .push(
2270                Container::new(actions_row)
2271                    .width(Length::Fill)
2272                    .padding(Padding {
2273                        top: 0.0,
2274                        right: tokens::component::time_picker_dialog::CONTENT_PADDING,
2275                        bottom: tokens::component::time_picker_dialog::ACTIONS_BOTTOM_SPACE,
2276                        left: tokens::component::time_picker_dialog::CONTENT_PADDING,
2277                    }),
2278            ),
2279    )
2280    .style(time_picker_dialog_container_style)
2281    .into()
2282}
2283
2284fn time_picker_dialog_content_height(display_mode: TimePickerDisplayMode) -> Length {
2285    match display_mode {
2286        TimePickerDisplayMode::Picker => Length::Shrink,
2287        TimePickerDisplayMode::Input | TimePickerDisplayMode::Scroll => {
2288            Length::Fixed(tokens::component::time_picker_dialog::MIN_HEIGHT_FOR_TIME_PICKER)
2289        }
2290    }
2291}
2292
2293/// Creates a Material 3 rich time picker dialog surface.
2294pub fn rich_time_picker_dialog<'a, Message, Renderer>(
2295    state: &'a TimePickerState,
2296    display_mode: TimePickerDisplayMode,
2297    on_action: impl Fn(TimePickerAction) -> Message + Clone + 'a,
2298    on_display_mode_toggle: Option<Message>,
2299    actions: impl Into<Element<'a, Message, Theme, Renderer>>,
2300) -> Element<'a, Message, Theme, Renderer>
2301where
2302    Message: Clone + 'a,
2303    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'static + 'a,
2304    iced_widget::core::Font: Into<Renderer::Font>,
2305{
2306    let content = match display_mode {
2307        TimePickerDisplayMode::Picker => {
2308            time_picker_body(state, on_action, TimePickerLayout::Vertical)
2309        }
2310        TimePickerDisplayMode::Input => time_input_body(state, on_action),
2311        TimePickerDisplayMode::Scroll => time_scroll_body(state, on_action),
2312    };
2313
2314    let mut actions_row = Row::new()
2315        .height(Length::Fixed(tokens::component::button::CONTAINER_HEIGHT))
2316        .spacing(tokens::component::time_picker_dialog::ACTIONS_HORIZONTAL_SPACE)
2317        .align_y(alignment::Vertical::Center);
2318
2319    if let Some(message) = on_display_mode_toggle {
2320        actions_row = actions_row.push(time_picker_display_mode_toggle(display_mode, message));
2321    }
2322
2323    actions_row = actions_row
2324        .push(Space::new().width(Length::Fill))
2325        .push(actions.into());
2326
2327    Container::new(
2328        Column::new()
2329            .push(
2330                Container::new(content)
2331                    .width(Length::Fill)
2332                    .padding(Padding {
2333                        top: tokens::component::time_picker_dialog::RICH_CONTENT_TOP_SPACE,
2334                        right: tokens::component::time_picker_dialog::RICH_CONTENT_PADDING,
2335                        bottom: tokens::component::time_picker_dialog::RICH_CONTENT_ACTIONS_SPACE,
2336                        left: tokens::component::time_picker_dialog::RICH_CONTENT_PADDING,
2337                    })
2338                    .align_x(alignment::Horizontal::Center),
2339            )
2340            .push(
2341                Container::new(actions_row)
2342                    .width(Length::Fill)
2343                    .padding(Padding {
2344                        top: 0.0,
2345                        right: tokens::component::time_picker_dialog::RICH_CONTENT_PADDING,
2346                        bottom: tokens::component::time_picker_dialog::RICH_ACTIONS_BOTTOM_SPACE,
2347                        left: tokens::component::time_picker_dialog::RICH_CONTENT_PADDING,
2348                    }),
2349            ),
2350    )
2351    .style(rich_time_picker_dialog_container_style)
2352    .into()
2353}
2354
2355fn date_picker_body<'a, Message, Renderer>(
2356    state: &'a DatePickerState,
2357    on_action: impl Fn(DatePickerAction) -> Message + Clone + 'a,
2358    show_mode_toggle: bool,
2359) -> Element<'a, Message, Theme, Renderer>
2360where
2361    Message: Clone + 'a,
2362    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2363    iced_widget::core::Font: Into<Renderer::Font>,
2364{
2365    let picker_alpha = state.animation.display.mode_alpha(DateDisplayMode::Picker);
2366    let input_alpha = state.animation.display.mode_alpha(DateDisplayMode::Input);
2367
2368    Column::new()
2369        .push(date_header(state, on_action.clone(), show_mode_toggle))
2370        .push(animated_date_display_content(
2371            date_picker_content(state, on_action.clone(), picker_alpha),
2372            date_input_content(state, on_action, input_alpha),
2373            state.display_mode,
2374            &state.animation.display,
2375            date_display_picker_height(),
2376            date_display_input_height(),
2377        ))
2378        .into()
2379}
2380
2381fn date_range_picker_body<'a, Message, Renderer>(
2382    state: &'a DateRangePickerState,
2383    on_action: impl Fn(DateRangePickerAction) -> Message + Clone + 'a,
2384    show_mode_toggle: bool,
2385    container_height: f32,
2386) -> Element<'a, Message, Theme, Renderer>
2387where
2388    Message: Clone + 'a,
2389    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2390    iced_widget::core::Font: Into<Renderer::Font>,
2391{
2392    let picker_alpha = state.animation.display.mode_alpha(DateDisplayMode::Picker);
2393    let input_alpha = state.animation.display.mode_alpha(DateDisplayMode::Input);
2394
2395    Column::new()
2396        .push(date_range_header(
2397            state,
2398            on_action.clone(),
2399            show_mode_toggle,
2400        ))
2401        .push(animated_date_display_content(
2402            date_range_picker_content(state, on_action.clone(), container_height, picker_alpha),
2403            date_range_input_content(state, on_action, input_alpha),
2404            state.display_mode,
2405            &state.animation.display,
2406            container_height - tokens::component::date_picker::RANGE_HEADER_CONTAINER_HEIGHT,
2407            date_display_input_height(),
2408        ))
2409        .into()
2410}
2411
2412fn animated_date_display_content<'a, Message, Renderer>(
2413    picker: Element<'a, Message, Theme, Renderer>,
2414    input: Element<'a, Message, Theme, Renderer>,
2415    display_mode: DateDisplayMode,
2416    animation: &DateDisplayAnimation,
2417    picker_height: f32,
2418    input_height: f32,
2419) -> Element<'a, Message, Theme, Renderer>
2420where
2421    Message: 'a,
2422    Renderer: iced_widget::core::Renderer + 'a,
2423{
2424    let height = animation.content_height(picker_height, input_height, display_mode);
2425
2426    if !animation.is_animating() {
2427        let content = match display_mode {
2428            DateDisplayMode::Picker => picker,
2429            DateDisplayMode::Input => input,
2430        };
2431
2432        return Container::new(content)
2433            .height(Length::Fixed(height))
2434            .clip(true)
2435            .into();
2436    }
2437
2438    let picker = translated(
2439        picker,
2440        Vector::new(
2441            0.0,
2442            animation.mode_offset(DateDisplayMode::Picker, input_height),
2443        ),
2444    );
2445    let input = translated(
2446        input,
2447        Vector::new(
2448            0.0,
2449            animation.mode_offset(DateDisplayMode::Input, input_height),
2450        ),
2451    );
2452
2453    let stack = match (animation.source_mode(), animation.target_mode()) {
2454        (DateDisplayMode::Picker, DateDisplayMode::Input) => Stack::new().push(picker).push(input),
2455        (DateDisplayMode::Input, DateDisplayMode::Picker) => Stack::new().push(input).push(picker),
2456        _ => match display_mode {
2457            DateDisplayMode::Picker => Stack::new().push(picker),
2458            DateDisplayMode::Input => Stack::new().push(input),
2459        },
2460    };
2461
2462    viewport::fixed_height(
2463        stack.width(Length::Fill).height(Length::Fixed(
2464            animation.content_layout_height(picker_height, input_height),
2465        )),
2466        height,
2467        animation.content_layout_height(picker_height, input_height),
2468    )
2469}
2470
2471fn date_display_picker_height() -> f32 {
2472    tokens::component::date_picker::MONTH_YEAR_CONTAINER_HEIGHT
2473        + tokens::component::date_picker::WEEKDAY_CONTAINER_HEIGHT
2474        + tokens::component::date_picker::CALENDAR_CELL_SIZE
2475            * tokens::component::date_picker::MAX_CALENDAR_ROWS as f32
2476}
2477
2478fn date_display_input_height() -> f32 {
2479    10.0 + tokens::component::text_field::CONTAINER_HEIGHT
2480        + 4.0
2481        + tokens::typography::BODY_SMALL.line_height
2482}
2483
2484fn date_picker_dialog_surface<'a, Message, Renderer>(
2485    content: impl Into<Element<'a, Message, Theme, Renderer>>,
2486    actions: impl Into<Element<'a, Message, Theme, Renderer>>,
2487) -> Element<'a, Message, Theme, Renderer>
2488where
2489    Message: 'a,
2490    Renderer: iced_widget::core::Renderer + 'a,
2491{
2492    Container::new(
2493        Column::new()
2494            .push(content)
2495            .push(date_picker_dialog_actions_container(actions)),
2496    )
2497    .width(Length::Fixed(
2498        tokens::component::date_picker::CONTAINER_WIDTH,
2499    ))
2500    .max_height(tokens::component::date_picker::CONTAINER_HEIGHT)
2501    .style(date_picker_container_style)
2502    .into()
2503}
2504
2505fn date_picker_dialog_actions_container<'a, Message, Renderer>(
2506    actions: impl Into<Element<'a, Message, Theme, Renderer>>,
2507) -> Element<'a, Message, Theme, Renderer>
2508where
2509    Message: 'a,
2510    Renderer: iced_widget::core::Renderer + 'a,
2511{
2512    Container::new(actions)
2513        .width(Length::Fill)
2514        .height(Length::Fixed(tokens::component::button::CONTAINER_HEIGHT))
2515        .align_x(alignment::Horizontal::Right)
2516        .padding(Padding {
2517            top: 0.0,
2518            right: tokens::component::date_picker::DIALOG_ACTIONS_END_SPACE,
2519            bottom: tokens::component::date_picker::DIALOG_ACTIONS_BOTTOM_SPACE,
2520            left: 0.0,
2521        })
2522        .into()
2523}
2524
2525fn date_picker_dialog_content_height() -> f32 {
2526    tokens::component::date_picker::CONTAINER_HEIGHT
2527        - tokens::component::button::CONTAINER_HEIGHT
2528        - tokens::component::date_picker::DIALOG_ACTIONS_BOTTOM_SPACE
2529}
2530
2531fn time_picker_body<'a, Message, Renderer>(
2532    state: &'a TimePickerState,
2533    on_action: impl Fn(TimePickerAction) -> Message + Clone + 'a,
2534    layout: TimePickerLayout,
2535) -> Element<'a, Message, Theme, Renderer>
2536where
2537    Message: Clone + 'a,
2538    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'static + 'a,
2539    iced_widget::core::Font: Into<Renderer::Font>,
2540{
2541    let clock = clock_face(state, on_action.clone());
2542
2543    match layout {
2544        TimePickerLayout::Vertical => Column::new()
2545            .push(clock_display(state, on_action.clone(), true))
2546            .push(Space::new().height(Length::Fixed(
2547                tokens::component::time_picker::CLOCK_DISPLAY_BOTTOM_SPACE,
2548            )))
2549            .push(clock)
2550            .push(Space::new().height(Length::Fixed(
2551                tokens::component::time_picker::CLOCK_FACE_BOTTOM_SPACE,
2552            )))
2553            .align_x(alignment::Horizontal::Center)
2554            .into(),
2555        TimePickerLayout::Horizontal => Row::new()
2556            .push(clock_display(state, on_action.clone(), false))
2557            .push(Space::new().width(Length::Fixed(
2558                tokens::component::time_picker::CLOCK_DISPLAY_BOTTOM_SPACE,
2559            )))
2560            .push(clock)
2561            .align_y(alignment::Vertical::Center)
2562            .into(),
2563    }
2564}
2565
2566fn time_input_body<'a, Message, Renderer>(
2567    state: &'a TimePickerState,
2568    on_action: impl Fn(TimePickerAction) -> Message + Clone + 'a,
2569) -> Element<'a, Message, Theme, Renderer>
2570where
2571    Message: Clone + 'a,
2572    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2573    iced_widget::core::Font: Into<Renderer::Font>,
2574{
2575    let hour = time_input_field(state, on_action.clone(), TimePickerSelectionMode::Hour);
2576    let minute = time_input_field(state, on_action.clone(), TimePickerSelectionMode::Minute);
2577    let mut content = Row::new()
2578        .push(hour)
2579        .push(display_separator_with_height::<Message, Renderer>(
2580            tokens::component::time_input::PERIOD_SELECTOR_CONTAINER_HEIGHT,
2581        ))
2582        .push(minute)
2583        .spacing(0.0)
2584        .align_y(alignment::Vertical::Top);
2585
2586    if !state.is_24_hour {
2587        content = content
2588            .push(Space::new().width(Length::Fixed(
2589                tokens::component::time_picker::PERIOD_TOGGLE_MARGIN,
2590            )))
2591            .push(period_toggle_sized(
2592                state,
2593                on_action,
2594                true,
2595                tokens::component::time_input::PERIOD_SELECTOR_CONTAINER_WIDTH,
2596                tokens::component::time_input::PERIOD_SELECTOR_CONTAINER_HEIGHT,
2597            ));
2598    }
2599
2600    content.into()
2601}
2602
2603include!("picker/time_scroll.rs");
2604
2605fn time_picker_dialog_title<'a, Message, Renderer>(
2606    display_mode: TimePickerDisplayMode,
2607) -> Element<'a, Message, Theme, Renderer>
2608where
2609    Message: 'a,
2610    Renderer: core_text::Renderer + 'a,
2611    iced_widget::core::Font: Into<Renderer::Font>,
2612{
2613    let scale = tokens::component::time_picker_dialog::TITLE_TEXT;
2614
2615    Container::new(
2616        Text::new(display_mode.title())
2617            .size(scale.size)
2618            .line_height(absolute_line_height(scale.line_height))
2619            .font(fonts::roboto_for_type_scale(scale)),
2620    )
2621    .padding(Padding {
2622        top: tokens::component::time_picker_dialog::TITLE_TOP_SPACE,
2623        right: tokens::component::time_picker_dialog::CONTENT_PADDING,
2624        bottom: tokens::component::time_picker_dialog::TITLE_BOTTOM_SPACE,
2625        left: tokens::component::time_picker_dialog::CONTENT_PADDING,
2626    })
2627    .width(Length::Fill)
2628    .into()
2629}
2630
2631fn time_picker_display_mode_toggle<'a, Message, Renderer>(
2632    display_mode: TimePickerDisplayMode,
2633    on_press: Message,
2634) -> Button<'a, Message, Renderer>
2635where
2636    Message: Clone + 'a,
2637    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2638    iced_widget::core::Font: Into<Renderer::Font>,
2639{
2640    super::button::icon_button(
2641        display_mode.toggle_icon(),
2642        super::button::IconButtonVariant::Standard,
2643    )
2644    .on_press(on_press)
2645}
2646
2647fn date_header<'a, Message, Renderer>(
2648    state: &'a DatePickerState,
2649    on_action: impl Fn(DatePickerAction) -> Message + Clone + 'a,
2650    show_mode_toggle: bool,
2651) -> Element<'a, Message, Theme, Renderer>
2652where
2653    Message: Clone + 'a,
2654    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2655    iced_widget::core::Font: Into<Renderer::Font>,
2656{
2657    let title = match state.display_mode {
2658        DateDisplayMode::Picker => "Select date",
2659        DateDisplayMode::Input => "Enter date",
2660    };
2661    let headline = state
2662        .selected_date
2663        .map(|date| state.formatter.format_date(date, false))
2664        .unwrap_or_else(|| "Select date".to_owned());
2665    let title_scale = tokens::component::date_picker::TITLE_TEXT;
2666    let headline_scale = tokens::component::date_picker::HEADLINE_TEXT;
2667    let mut headline_row = Row::new()
2668        .push(
2669            Text::new(headline)
2670                .size(headline_scale.size)
2671                .line_height(absolute_line_height(headline_scale.line_height))
2672                .font(fonts::roboto_for_type_scale(headline_scale))
2673                .width(Length::Fill),
2674        )
2675        .align_y(alignment::Vertical::Center);
2676
2677    if show_mode_toggle {
2678        let toggle_mode = if state.display_mode == DateDisplayMode::Picker {
2679            DateDisplayMode::Input
2680        } else {
2681            DateDisplayMode::Picker
2682        };
2683        let toggle_icon = if state.display_mode == DateDisplayMode::Picker {
2684            "edit"
2685        } else {
2686            "calendar_today"
2687        };
2688        let toggle =
2689            super::button::icon_button(toggle_icon, super::button::IconButtonVariant::Standard)
2690                .on_press(on_action(DatePickerAction::SetDisplayMode(toggle_mode)));
2691        headline_row = headline_row.push(toggle);
2692    }
2693
2694    Container::new(
2695        Column::new()
2696            .push(
2697                Container::new(
2698                    Text::new(title)
2699                        .size(title_scale.size)
2700                        .line_height(absolute_line_height(title_scale.line_height))
2701                        .font(fonts::roboto_for_type_scale(title_scale)),
2702                )
2703                .padding(Padding {
2704                    top: tokens::component::date_picker::HEADER_TITLE_TOP_SPACE,
2705                    right: tokens::component::date_picker::HEADER_TITLE_END_SPACE,
2706                    bottom: 0.0,
2707                    left: tokens::component::date_picker::HEADER_TITLE_START_SPACE,
2708                }),
2709            )
2710            .push(Space::new().height(Length::Fill))
2711            .push(
2712                Container::new(headline_row)
2713                    .padding(Padding {
2714                        top: 0.0,
2715                        right: tokens::component::date_picker::HEADER_HEADLINE_END_SPACE,
2716                        bottom: tokens::component::date_picker::HEADER_HEADLINE_BOTTOM_SPACE,
2717                        left: tokens::component::date_picker::HEADER_HEADLINE_START_SPACE,
2718                    })
2719                    .width(Length::Fill),
2720            ),
2721    )
2722    .height(Length::Fixed(
2723        tokens::component::date_picker::HEADER_CONTAINER_HEIGHT,
2724    ))
2725    .style(date_header_style)
2726    .into()
2727}
2728
2729fn date_range_header<'a, Message, Renderer>(
2730    state: &'a DateRangePickerState,
2731    on_action: impl Fn(DateRangePickerAction) -> Message + Clone + 'a,
2732    show_mode_toggle: bool,
2733) -> Element<'a, Message, Theme, Renderer>
2734where
2735    Message: Clone + 'a,
2736    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2737    iced_widget::core::Font: Into<Renderer::Font>,
2738{
2739    let title = match state.display_mode {
2740        DateDisplayMode::Picker => {
2741            if state.selected_start_date.is_none() || state.selected_end_date.is_some() {
2742                "Select start date"
2743            } else {
2744                "Select end date"
2745            }
2746        }
2747        DateDisplayMode::Input => "Enter dates",
2748    };
2749    let start = state
2750        .selected_start_date
2751        .map(|date| state.formatter.format_date(date, false))
2752        .unwrap_or_else(|| "Start date".to_owned());
2753    let end = state
2754        .selected_end_date
2755        .map(|date| state.formatter.format_date(date, false))
2756        .unwrap_or_else(|| "End date".to_owned());
2757    let title_scale = tokens::component::date_picker::TITLE_TEXT;
2758    let headline_scale = tokens::component::date_picker::RANGE_HEADLINE_TEXT;
2759    let mut headline_row = Row::new()
2760        .push(
2761            Text::new(format!("{start} - {end}"))
2762                .size(headline_scale.size)
2763                .line_height(absolute_line_height(headline_scale.line_height))
2764                .font(fonts::roboto_for_type_scale(headline_scale))
2765                .width(Length::Fill),
2766        )
2767        .align_y(alignment::Vertical::Center);
2768
2769    if show_mode_toggle {
2770        let toggle_mode = if state.display_mode == DateDisplayMode::Picker {
2771            DateDisplayMode::Input
2772        } else {
2773            DateDisplayMode::Picker
2774        };
2775        let toggle_icon = if state.display_mode == DateDisplayMode::Picker {
2776            "edit"
2777        } else {
2778            "calendar_today"
2779        };
2780        let toggle =
2781            super::button::icon_button(toggle_icon, super::button::IconButtonVariant::Standard)
2782                .on_press(on_action(DateRangePickerAction::SetDisplayMode(
2783                    toggle_mode,
2784                )));
2785        headline_row = headline_row.push(toggle);
2786    }
2787
2788    Container::new(
2789        Column::new()
2790            .push(
2791                Container::new(
2792                    Text::new(title)
2793                        .size(title_scale.size)
2794                        .line_height(absolute_line_height(title_scale.line_height))
2795                        .font(fonts::roboto_for_type_scale(title_scale)),
2796                )
2797                .padding(Padding {
2798                    top: tokens::component::date_picker::HEADER_TITLE_TOP_SPACE,
2799                    right: tokens::component::date_picker::HEADER_TITLE_END_SPACE,
2800                    bottom: 0.0,
2801                    left: tokens::component::date_picker::HEADER_TITLE_START_SPACE,
2802                }),
2803            )
2804            .push(Space::new().height(Length::Fill))
2805            .push(
2806                Container::new(headline_row)
2807                    .padding(Padding {
2808                        top: 0.0,
2809                        right: tokens::component::date_picker::HEADER_HEADLINE_END_SPACE,
2810                        bottom: tokens::component::date_picker::HEADER_HEADLINE_BOTTOM_SPACE,
2811                        left: tokens::component::date_picker::HEADER_HEADLINE_START_SPACE,
2812                    })
2813                    .width(Length::Fill),
2814            ),
2815    )
2816    .height(Length::Fixed(
2817        tokens::component::date_picker::RANGE_HEADER_CONTAINER_HEIGHT,
2818    ))
2819    .style(date_header_style)
2820    .into()
2821}
2822
2823fn date_picker_content<'a, Message, Renderer>(
2824    state: &'a DatePickerState,
2825    on_action: impl Fn(DatePickerAction) -> Message + Clone + 'a,
2826    content_alpha: f32,
2827) -> Element<'a, Message, Theme, Renderer>
2828where
2829    Message: Clone + 'a,
2830    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2831    iced_widget::core::Font: Into<Renderer::Font>,
2832{
2833    let year_picker_progress = state.animation.year_picker_progress();
2834    let year_picker_alpha =
2835        content_alpha * year_picker_content_alpha(year_picker_progress, state.year_picker_visible);
2836    let calendar = Column::new()
2837        .push(weekdays_row(state.first_day_of_week, content_alpha))
2838        .push(animated_month_grid(state, on_action.clone(), content_alpha));
2839    let content: Element<'a, Message, Theme, Renderer> =
2840        if year_picker_should_render(year_picker_progress, state.year_picker_visible) {
2841            Stack::new()
2842                .push(calendar)
2843                .push(year_grid(
2844                    state,
2845                    on_action.clone(),
2846                    year_picker_progress,
2847                    year_picker_alpha,
2848                ))
2849                .into()
2850        } else {
2851            calendar.into()
2852        };
2853
2854    Column::new()
2855        .push(month_navigation(state, on_action, content_alpha))
2856        .push(content)
2857        .into()
2858}
2859
2860fn date_range_picker_content<'a, Message, Renderer>(
2861    state: &'a DateRangePickerState,
2862    on_action: impl Fn(DateRangePickerAction) -> Message + Clone + 'a,
2863    container_height: f32,
2864    content_alpha: f32,
2865) -> Element<'a, Message, Theme, Renderer>
2866where
2867    Message: Clone + 'a,
2868    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2869    iced_widget::core::Font: Into<Renderer::Font>,
2870{
2871    let year_picker_progress = state.animation.year_picker_progress();
2872    let year_picker_alpha =
2873        content_alpha * year_picker_content_alpha(year_picker_progress, state.year_picker_visible);
2874    let calendar = Column::new()
2875        .push(weekdays_row(state.first_day_of_week, content_alpha))
2876        .push(range_months_list(
2877            state,
2878            on_action.clone(),
2879            container_height,
2880            content_alpha,
2881        ));
2882
2883    if year_picker_should_render(year_picker_progress, state.year_picker_visible) {
2884        Stack::new()
2885            .push(calendar)
2886            .push(range_year_grid(
2887                state,
2888                on_action,
2889                year_picker_progress,
2890                year_picker_alpha,
2891            ))
2892            .into()
2893    } else {
2894        calendar.into()
2895    }
2896}
2897
2898fn year_picker_content_alpha(progress: f32, visible: bool) -> f32 {
2899    let progress = progress.clamp(0.0, 1.0);
2900
2901    if visible {
2902        lerp(0.6, 1.0, progress)
2903    } else {
2904        progress
2905    }
2906}
2907
2908fn year_picker_should_render(progress: f32, visible: bool) -> bool {
2909    visible || progress > 0.0
2910}
2911
2912fn date_input_content<'a, Message, Renderer>(
2913    state: &'a DatePickerState,
2914    on_action: impl Fn(DatePickerAction) -> Message + Clone + 'a,
2915    content_alpha: f32,
2916) -> Element<'a, Message, Theme, Renderer>
2917where
2918    Message: Clone + 'a,
2919    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2920    iced_widget::core::Font: Into<Renderer::Font>,
2921{
2922    let input = text_input::outlined("MM/DD/YYYY", state.input_value())
2923        .error(!state.is_input_valid())
2924        .alpha(content_alpha)
2925        .on_input(move |value| on_action(DatePickerAction::InputChanged(value)));
2926    let mut content = Column::new().push(input);
2927
2928    if let Some(error) = state.input_error() {
2929        content = content.push(date_input_error_label(error, content_alpha));
2930    }
2931
2932    Container::new(content)
2933        .width(Length::Fill)
2934        .padding(Padding {
2935            top: 10.0,
2936            right: 24.0,
2937            bottom: 0.0,
2938            left: 24.0,
2939        })
2940        .style(move |theme| date_input_panel_style(theme, content_alpha))
2941        .into()
2942}
2943
2944fn date_range_input_content<'a, Message, Renderer>(
2945    state: &'a DateRangePickerState,
2946    on_action: impl Fn(DateRangePickerAction) -> Message + Clone + 'a,
2947    content_alpha: f32,
2948) -> Element<'a, Message, Theme, Renderer>
2949where
2950    Message: Clone + 'a,
2951    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
2952    iced_widget::core::Font: Into<Renderer::Font>,
2953{
2954    let start = text_input::outlined("Start date", state.start_input_value())
2955        .error(!state.is_start_input_valid())
2956        .alpha(content_alpha)
2957        .on_input({
2958            let on_action = on_action.clone();
2959            move |value| on_action(DateRangePickerAction::StartInputChanged(value))
2960        })
2961        .width(Length::Fill);
2962    let end = text_input::outlined("End date", state.end_input_value())
2963        .error(!state.is_end_input_valid())
2964        .alpha(content_alpha)
2965        .on_input(move |value| on_action(DateRangePickerAction::EndInputChanged(value)))
2966        .width(Length::Fill);
2967    let mut start = Column::new().push(start);
2968    let mut end = Column::new().push(end);
2969
2970    if let Some(error) = state.start_input_error() {
2971        start = start.push(date_input_error_label(error, content_alpha));
2972    }
2973
2974    if let Some(error) = state.end_input_error() {
2975        end = end.push(date_input_error_label(error, content_alpha));
2976    }
2977
2978    Container::new(
2979        Row::new()
2980            .push(Container::new(start).width(Length::Fill))
2981            .push(Container::new(end).width(Length::Fill))
2982            .spacing(8.0)
2983            .align_y(alignment::Vertical::Top),
2984    )
2985    .width(Length::Fill)
2986    .padding(Padding {
2987        top: 10.0,
2988        right: 24.0,
2989        bottom: 0.0,
2990        left: 24.0,
2991    })
2992    .style(move |theme| date_input_panel_style(theme, content_alpha))
2993    .into()
2994}
2995
2996fn date_input_error_label<'a, Message, Renderer>(
2997    error: String,
2998    content_alpha: f32,
2999) -> Element<'a, Message, Theme, Renderer>
3000where
3001    Message: 'a,
3002    Renderer: core_text::Renderer + 'a,
3003    iced_widget::core::Font: Into<Renderer::Font>,
3004{
3005    let scale = tokens::typography::BODY_SMALL;
3006
3007    Container::new(
3008        Text::new(error)
3009            .size(scale.size)
3010            .line_height(absolute_line_height(scale.line_height))
3011            .font(fonts::roboto_for_type_scale(scale))
3012            .style(move |theme: &Theme| text::Style {
3013                color: Some(alpha_color(theme.colors().error.color, content_alpha)),
3014            }),
3015    )
3016    .padding(Padding {
3017        top: 4.0,
3018        right: 16.0,
3019        bottom: 0.0,
3020        left: 16.0,
3021    })
3022    .width(Length::Fill)
3023    .into()
3024}
3025
3026fn month_navigation<'a, Message, Renderer>(
3027    state: &'a DatePickerState,
3028    on_action: impl Fn(DatePickerAction) -> Message + Clone + 'a,
3029    content_alpha: f32,
3030) -> Element<'a, Message, Theme, Renderer>
3031where
3032    Message: Clone + 'a,
3033    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
3034    iced_widget::core::Font: Into<Renderer::Font>,
3035{
3036    let month = state.displayed_month;
3037    let previous = month.add_months(-1);
3038    let next = month.add_months(1);
3039    let can_previous = state.year_range.contains(&previous.year);
3040    let can_next = state.year_range.contains(&next.year);
3041    let arrow = if state.year_picker_visible {
3042        "arrow_drop_up"
3043    } else {
3044        "arrow_drop_down"
3045    };
3046    let year_button_content = Row::<Message, Theme, Renderer>::new()
3047        .push(
3048            Text::new(state.formatter.format_month_year(month))
3049                .size(tokens::typography::LABEL_LARGE.size)
3050                .line_height(absolute_line_height(
3051                    tokens::typography::LABEL_LARGE.line_height,
3052                ))
3053                .font(fonts::roboto_for_type_scale(
3054                    tokens::typography::LABEL_LARGE,
3055                ))
3056                .style(move |theme: &Theme| text::Style {
3057                    color: Some(alpha_color(theme.colors().surface.text, content_alpha)),
3058                }),
3059        )
3060        .push(
3061            fonts::filled_icon(arrow, 18.0).style(move |theme: &Theme| text::Style {
3062                color: Some(alpha_color(theme.colors().surface.text, content_alpha)),
3063            }),
3064        )
3065        .spacing(8.0)
3066        .align_y(alignment::Vertical::Center);
3067    let year_button = Button::new(
3068        Container::new(year_button_content)
3069            .height(Length::Fixed(tokens::component::button::CONTAINER_HEIGHT))
3070            .padding(Padding::from([0.0, 16.0]))
3071            .align_y(alignment::Vertical::Center),
3072    )
3073    .height(Length::Fixed(tokens::component::button::CONTAINER_HEIGHT))
3074    .padding(Padding::ZERO)
3075    .style(button_style::text)
3076    .on_press(on_action.clone()(DatePickerAction::ToggleYearPicker));
3077
3078    let mut row = Row::new()
3079        .push(year_button)
3080        .push(Space::new().width(Length::Fill))
3081        .align_y(alignment::Vertical::Center);
3082
3083    if !state.year_picker_visible {
3084        row = row
3085            .push(
3086                super::button::icon_button(
3087                    "keyboard_arrow_left",
3088                    super::button::IconButtonVariant::Standard,
3089                )
3090                .on_press_maybe(
3091                    can_previous.then(|| on_action.clone()(DatePickerAction::PreviousMonth)),
3092                ),
3093            )
3094            .push(
3095                super::button::icon_button(
3096                    "keyboard_arrow_right",
3097                    super::button::IconButtonVariant::Standard,
3098                )
3099                .on_press_maybe(can_next.then(|| on_action(DatePickerAction::NextMonth))),
3100            );
3101    }
3102
3103    Container::new(row)
3104        .height(Length::Fixed(
3105            tokens::component::date_picker::MONTH_YEAR_CONTAINER_HEIGHT,
3106        ))
3107        .padding(Padding {
3108            top: 0.0,
3109            right: tokens::component::date_picker::HORIZONTAL_SPACE,
3110            bottom: 0.0,
3111            left: tokens::component::date_picker::HORIZONTAL_SPACE,
3112        })
3113        .into()
3114}
3115
3116fn weekdays_row<'a, Message, Renderer>(
3117    first_day_of_week: Weekday,
3118    content_alpha: f32,
3119) -> Element<'a, Message, Theme, Renderer>
3120where
3121    Message: 'a,
3122    Renderer: core_text::Renderer + 'a,
3123    iced_widget::core::Font: Into<Renderer::Font>,
3124{
3125    let scale = tokens::component::date_picker::WEEKDAY_LABEL_TEXT;
3126    let mut row = Row::new()
3127        .height(Length::Fixed(
3128            tokens::component::date_picker::WEEKDAY_CONTAINER_HEIGHT,
3129        ))
3130        .align_y(alignment::Vertical::Center);
3131
3132    for offset in 0..7 {
3133        let weekday = Weekday::ALL[(first_day_of_week.sunday_first_index() + offset) % 7];
3134        row = row.push(
3135            Container::new(
3136                Text::new(weekday.short_label())
3137                    .size(scale.size)
3138                    .line_height(absolute_line_height(scale.line_height))
3139                    .font(fonts::roboto_for_type_scale(scale))
3140                    .style(move |theme: &Theme| text::Style {
3141                        color: Some(alpha_color(theme.colors().surface.text, content_alpha)),
3142                    }),
3143            )
3144            .center_x(Length::Fixed(
3145                tokens::component::date_picker::CALENDAR_CELL_SIZE,
3146            ))
3147            .center_y(Length::Fixed(
3148                tokens::component::date_picker::CALENDAR_CELL_SIZE,
3149            )),
3150        );
3151    }
3152
3153    Container::new(row)
3154        .padding(Padding {
3155            top: 0.0,
3156            right: tokens::component::date_picker::HORIZONTAL_SPACE,
3157            bottom: 0.0,
3158            left: tokens::component::date_picker::HORIZONTAL_SPACE,
3159        })
3160        .into()
3161}
3162
3163fn animated_month_grid<'a, Message, Renderer>(
3164    state: &'a DatePickerState,
3165    on_action: impl Fn(DatePickerAction) -> Message + Clone + 'a,
3166    content_alpha: f32,
3167) -> Element<'a, Message, Theme, Renderer>
3168where
3169    Message: Clone + 'a,
3170    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
3171    iced_widget::core::Font: Into<Renderer::Font>,
3172{
3173    if !state.animation.month.is_animating() {
3174        return month_grid_for_month(state, on_action, state.displayed_month, content_alpha);
3175    }
3176
3177    let (from, to) = state.animation.month.visible_months(state.displayed_month);
3178    let from_grid = translated(
3179        month_grid_for_month(state, on_action.clone(), from, content_alpha),
3180        Vector::new(state.animation.month.month_offset(from), 0.0),
3181    );
3182    let to_grid = translated(
3183        month_grid_for_month(state, on_action, to, content_alpha),
3184        Vector::new(state.animation.month.month_offset(to), 0.0),
3185    );
3186
3187    Stack::new()
3188        .push(from_grid)
3189        .push(to_grid)
3190        .width(Length::Fill)
3191        .clip(true)
3192        .into()
3193}
3194
3195fn month_grid_for_month<'a, Message, Renderer>(
3196    state: &'a DatePickerState,
3197    on_action: impl Fn(DatePickerAction) -> Message + Clone + 'a,
3198    month: YearMonth,
3199    content_alpha: f32,
3200) -> Element<'a, Message, Theme, Renderer>
3201where
3202    Message: Clone + 'a,
3203    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
3204    iced_widget::core::Font: Into<Renderer::Font>,
3205{
3206    let first = month.start_date();
3207    let first_weekday = first.weekday_index_from(state.first_day_of_week);
3208    let days = days_in_month(month.year, month.month);
3209    let today = Date::today_utc();
3210    let mut day = 1u8;
3211    let mut column = Column::new();
3212
3213    for week in 0..tokens::component::date_picker::MAX_CALENDAR_ROWS {
3214        let mut row = Row::new().align_y(alignment::Vertical::Center);
3215
3216        for weekday in 0..7 {
3217            let cell = week * 7 + weekday;
3218
3219            if cell < first_weekday || day > days {
3220                row = row.push(
3221                    Space::new()
3222                        .width(Length::Fixed(
3223                            tokens::component::date_picker::CALENDAR_CELL_SIZE,
3224                        ))
3225                        .height(Length::Fixed(
3226                            tokens::component::date_picker::CALENDAR_CELL_SIZE,
3227                        )),
3228                );
3229            } else {
3230                let date = Date {
3231                    year: month.year,
3232                    month: month.month,
3233                    day,
3234                };
3235                let selected = state.selected_date == Some(date);
3236                let selected_progress = state.animation.selected_date_progress(date, selected);
3237                let is_today = date == today;
3238                let enabled = state.date_is_selectable(date);
3239                let action = on_action.clone()(DatePickerAction::SelectDate(date));
3240
3241                row = row.push(
3242                    day_button(DayCell {
3243                        day,
3244                        selected,
3245                        selected_progress,
3246                        today: is_today,
3247                        enabled,
3248                        weekday,
3249                        range_position: DateRangePosition::None,
3250                        range_background_progress: 0.0,
3251                        content_alpha,
3252                    })
3253                    .on_press_maybe(enabled.then_some(action)),
3254                );
3255                day += 1;
3256            }
3257        }
3258
3259        column = column.push(row);
3260    }
3261
3262    Container::new(column)
3263        .padding(Padding {
3264            top: 0.0,
3265            right: tokens::component::date_picker::HORIZONTAL_SPACE,
3266            bottom: 0.0,
3267            left: tokens::component::date_picker::HORIZONTAL_SPACE,
3268        })
3269        .into()
3270}
3271
3272fn year_grid<'a, Message, Renderer>(
3273    state: &'a DatePickerState,
3274    on_action: impl Fn(DatePickerAction) -> Message + Clone + 'a,
3275    progress: f32,
3276    content_alpha: f32,
3277) -> Element<'a, Message, Theme, Renderer>
3278where
3279    Message: Clone + 'a,
3280    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
3281    iced_widget::core::Font: Into<Renderer::Font>,
3282{
3283    let current_year = Date::today_utc().year;
3284    let selected_year = state.displayed_month.year;
3285    let mut column = Column::new().spacing(tokens::component::date_picker::YEAR_VERTICAL_SPACE);
3286    let mut years = state.year_range.clone().peekable();
3287
3288    while years.peek().is_some() {
3289        let mut row = year_grid_row::<Message, Renderer>();
3290
3291        for _ in 0..tokens::component::date_picker::YEARS_IN_ROW {
3292            if let Some(year) = years.next() {
3293                let enabled = state.selectable_dates.is_selectable_year(year);
3294                let action = on_action.clone()(DatePickerAction::SelectYear(year));
3295                row = row.push(Space::new().width(Length::Fill)).push(
3296                    year_button(
3297                        year,
3298                        year == selected_year,
3299                        year == current_year,
3300                        enabled,
3301                        content_alpha,
3302                    )
3303                    .on_press_maybe(enabled.then_some(action)),
3304                );
3305            } else {
3306                row = row
3307                    .push(Space::new().width(Length::Fill))
3308                    .push(year_placeholder());
3309            }
3310        }
3311
3312        column = column.push(row.push(Space::new().width(Length::Fill)));
3313    }
3314
3315    let full_height = tokens::component::date_picker::CALENDAR_CELL_SIZE
3316        * (tokens::component::date_picker::MAX_CALENDAR_ROWS as f32 + 1.0)
3317        - tokens::component::divider::THICKNESS;
3318    let height = (full_height * progress.clamp(0.0, 1.0)).max(1.0);
3319
3320    Container::new(
3321        Scrollable::new(column)
3322            .id(state.year_picker_scroll_id.clone())
3323            .height(Length::Fixed(full_height))
3324            .width(Length::Fill),
3325    )
3326    .height(Length::Fixed(height))
3327    .clip(true)
3328    .padding(Padding {
3329        top: 0.0,
3330        right: tokens::component::date_picker::HORIZONTAL_SPACE,
3331        bottom: 0.0,
3332        left: tokens::component::date_picker::HORIZONTAL_SPACE,
3333    })
3334    .style(move |theme| year_picker_panel_style(theme, content_alpha))
3335    .into()
3336}
3337
3338fn year_grid_row<'a, Message, Renderer>() -> Row<'a, Message, Theme, Renderer>
3339where
3340    Message: 'a,
3341    Renderer: iced_widget::core::Renderer + 'a,
3342{
3343    Row::new()
3344        .width(Length::Fill)
3345        .spacing(0.0)
3346        .align_y(alignment::Vertical::Center)
3347}
3348
3349fn year_placeholder() -> Space {
3350    Space::new()
3351        .width(Length::Fixed(
3352            tokens::component::date_picker::YEAR_CONTAINER_WIDTH,
3353        ))
3354        .height(Length::Fixed(
3355            tokens::component::date_picker::YEAR_CONTAINER_HEIGHT,
3356        ))
3357}
3358
3359fn range_months_list<'a, Message, Renderer>(
3360    state: &'a DateRangePickerState,
3361    on_action: impl Fn(DateRangePickerAction) -> Message + Clone + 'a,
3362    container_height: f32,
3363    content_alpha: f32,
3364) -> Element<'a, Message, Theme, Renderer>
3365where
3366    Message: Clone + 'a,
3367    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
3368    iced_widget::core::Font: Into<Renderer::Font>,
3369{
3370    let months = range_rendered_months(state);
3371    let mut column = Column::new();
3372
3373    for month in months {
3374        column = column
3375            .push(range_month_subhead::<Message, Renderer>(
3376                &state.formatter,
3377                month,
3378                content_alpha,
3379            ))
3380            .push(range_month_grid_for_month(
3381                state,
3382                on_action.clone(),
3383                month,
3384                content_alpha,
3385            ));
3386    }
3387
3388    let height = container_height
3389        - tokens::component::date_picker::RANGE_HEADER_CONTAINER_HEIGHT
3390        - tokens::component::date_picker::WEEKDAY_CONTAINER_HEIGHT;
3391
3392    Container::new(
3393        Scrollable::new(column)
3394            .id(state.months_scroll_id.clone())
3395            .height(Length::Fixed(height))
3396            .width(Length::Fill),
3397    )
3398    .height(Length::Fixed(height))
3399    .into()
3400}
3401
3402fn range_month_subhead<'a, Message, Renderer>(
3403    formatter: &DatePickerFormatter,
3404    month: YearMonth,
3405    content_alpha: f32,
3406) -> Element<'a, Message, Theme, Renderer>
3407where
3408    Message: 'a,
3409    Renderer: core_text::Renderer + 'a,
3410    iced_widget::core::Font: Into<Renderer::Font>,
3411{
3412    let scale = tokens::component::date_picker::RANGE_MONTH_SUBHEAD_TEXT;
3413
3414    Container::new(
3415        Text::new(formatter.format_month_year(month))
3416            .size(scale.size)
3417            .line_height(absolute_line_height(scale.line_height))
3418            .font(fonts::roboto_for_type_scale(scale))
3419            .style(move |theme: &Theme| text::Style {
3420                color: Some(alpha_color(
3421                    theme.colors().surface.text_variant,
3422                    content_alpha,
3423                )),
3424            }),
3425    )
3426    .padding(Padding {
3427        top: tokens::component::date_picker::RANGE_MONTH_SUBHEAD_TOP_SPACE,
3428        right: 0.0,
3429        bottom: tokens::component::date_picker::RANGE_MONTH_SUBHEAD_BOTTOM_SPACE,
3430        left: tokens::component::date_picker::RANGE_MONTH_SUBHEAD_START_SPACE,
3431    })
3432    .width(Length::Fill)
3433    .into()
3434}
3435
3436fn range_month_selection_info(
3437    state: &DateRangePickerState,
3438    month: YearMonth,
3439) -> Option<RangeMonthSelectionInfo> {
3440    let start = state.selected_start_date?;
3441    let end = state.selected_end_date?;
3442    let month_start = month.start_date();
3443    let month_days = days_in_month(month.year, month.month);
3444    let month_end = Date {
3445        year: month.year,
3446        month: month.month,
3447        day: month_days,
3448    };
3449
3450    if start > month_end || end < month_start {
3451        return None;
3452    }
3453
3454    let first_weekday = month_start.weekday_index_from(state.first_day_of_week);
3455    let first_is_selection_start = start >= month_start;
3456    let last_is_selection_end = end <= month_end;
3457    let start_day = if first_is_selection_start {
3458        start.day
3459    } else {
3460        1
3461    };
3462    let end_day = if last_is_selection_end {
3463        end.day
3464    } else {
3465        month_days
3466    };
3467    let start_cell = first_weekday + usize::from(start_day - 1);
3468    let end_cell = first_weekday + usize::from(end_day - 1);
3469
3470    Some(RangeMonthSelectionInfo {
3471        start_column: start_cell % 7,
3472        start_row: start_cell / 7,
3473        end_column: end_cell % 7,
3474        end_row: end_cell / 7,
3475        first_is_selection_start,
3476        last_is_selection_end,
3477    })
3478}
3479
3480#[derive(Debug, Clone, Copy)]
3481struct RangeBackground {
3482    info: RangeMonthSelectionInfo,
3483    width: f32,
3484}
3485
3486impl RangeBackground {
3487    fn rects(self) -> Vec<RangeBackgroundRect> {
3488        let item_container_width = tokens::component::date_picker::CALENDAR_CELL_SIZE;
3489        let item_container_height = tokens::component::date_picker::CALENDAR_CELL_SIZE;
3490        let item_state_layer_height = tokens::component::date_picker::DATE_CONTAINER_HEIGHT;
3491        let state_layer_vertical_padding = (item_container_height - item_state_layer_height) / 2.0;
3492        let horizontal_space_between_items = (self.width - 7.0 * item_container_width) / 7.0;
3493        let item_step = item_container_width + horizontal_space_between_items;
3494        let start_x = self.info.start_column as f32 * item_step
3495            + if self.info.first_is_selection_start {
3496                item_container_width / 2.0
3497            } else {
3498                0.0
3499            }
3500            + horizontal_space_between_items / 2.0;
3501        let start_y =
3502            self.info.start_row as f32 * item_container_height + state_layer_vertical_padding;
3503        let end_x = self.info.end_column as f32 * item_step
3504            + if self.info.last_is_selection_end {
3505                item_container_width / 2.0
3506            } else {
3507                item_container_width
3508            }
3509            + horizontal_space_between_items / 2.0;
3510        let end_y = self.info.end_row as f32 * item_container_height + state_layer_vertical_padding;
3511        let mut rects = Vec::with_capacity(3);
3512
3513        rects.push(RangeBackgroundRect {
3514            x: start_x,
3515            y: start_y,
3516            width: if self.info.start_row == self.info.end_row {
3517                end_x - start_x
3518            } else {
3519                self.width - start_x
3520            },
3521            height: item_state_layer_height,
3522        });
3523
3524        if self.info.start_row != self.info.end_row {
3525            for row_offset in (1..=(self.info.end_row - self.info.start_row - 1)).rev() {
3526                rects.push(RangeBackgroundRect {
3527                    x: 0.0,
3528                    y: start_y + row_offset as f32 * item_container_height,
3529                    width: self.width,
3530                    height: item_state_layer_height,
3531                });
3532            }
3533
3534            rects.push(RangeBackgroundRect {
3535                x: 0.0,
3536                y: end_y,
3537                width: end_x,
3538                height: item_state_layer_height,
3539            });
3540        }
3541
3542        rects
3543    }
3544
3545    fn reveal(self, progress: f32) -> Vec<RangeBackgroundRect> {
3546        let progress = progress.clamp(0.0, 1.0);
3547        let rects = self.rects();
3548        let total_width: f32 = rects.iter().map(|rect| rect.width.max(0.0)).sum();
3549        let mut remaining = total_width * progress;
3550        let mut clipped = Vec::with_capacity(rects.len());
3551
3552        for mut rect in rects {
3553            if remaining <= 0.0 {
3554                break;
3555            }
3556
3557            let width = rect.width.min(remaining);
3558
3559            if width > f32::EPSILON {
3560                rect.width = width;
3561                clipped.push(rect);
3562            }
3563
3564            remaining -= width;
3565        }
3566
3567        clipped
3568    }
3569}
3570
3571#[derive(Debug, Clone, Copy)]
3572struct RangeConnector {
3573    position: DateRangePosition,
3574    weekday: usize,
3575}
3576
3577impl RangeConnector {
3578    fn rect(self) -> Option<RangeBackgroundRect> {
3579        let cell_size = tokens::component::date_picker::CALENDAR_CELL_SIZE;
3580        let layer_height = tokens::component::date_picker::DATE_STATE_LAYER_HEIGHT;
3581        let y = (cell_size - layer_height) / 2.0;
3582        let half_width = cell_size / 2.0;
3583
3584        match self.position {
3585            DateRangePosition::Start if self.weekday < 6 => Some(RangeBackgroundRect {
3586                x: half_width,
3587                y,
3588                width: half_width,
3589                height: layer_height,
3590            }),
3591            DateRangePosition::End if self.weekday > 0 => Some(RangeBackgroundRect {
3592                x: 0.0,
3593                y,
3594                width: half_width,
3595                height: layer_height,
3596            }),
3597            DateRangePosition::None
3598            | DateRangePosition::Single
3599            | DateRangePosition::Middle
3600            | DateRangePosition::Start
3601            | DateRangePosition::End => None,
3602        }
3603    }
3604
3605    fn reveal_progress(range_background_progress: f32, selected_progress: f32) -> f32 {
3606        let range_background_progress = range_background_progress.clamp(0.0, 1.0);
3607
3608        if selected_progress >= 1.0 {
3609            range_background_progress
3610        } else {
3611            0.0
3612        }
3613    }
3614}
3615
3616fn range_month_grid_for_month<'a, Message, Renderer>(
3617    state: &'a DateRangePickerState,
3618    on_action: impl Fn(DateRangePickerAction) -> Message + Clone + 'a,
3619    month: YearMonth,
3620    content_alpha: f32,
3621) -> Element<'a, Message, Theme, Renderer>
3622where
3623    Message: Clone + 'a,
3624    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
3625    iced_widget::core::Font: Into<Renderer::Font>,
3626{
3627    let first = month.start_date();
3628    let first_weekday = first.weekday_index_from(state.first_day_of_week);
3629    let days = days_in_month(month.year, month.month);
3630    let today = Date::today_utc();
3631    let mut day = 1u8;
3632    let mut column = Column::new();
3633    let grid_width = tokens::component::date_picker::CALENDAR_CELL_SIZE * 7.0;
3634    let grid_height = tokens::component::date_picker::CALENDAR_CELL_SIZE
3635        * tokens::component::date_picker::MAX_CALENDAR_ROWS as f32;
3636
3637    for week in 0..tokens::component::date_picker::MAX_CALENDAR_ROWS {
3638        let mut row = Row::new().align_y(alignment::Vertical::Center);
3639
3640        for weekday in 0..7 {
3641            let cell = week * 7 + weekday;
3642
3643            if cell < first_weekday || day > days {
3644                row = row.push(
3645                    Space::new()
3646                        .width(Length::Fixed(
3647                            tokens::component::date_picker::CALENDAR_CELL_SIZE,
3648                        ))
3649                        .height(Length::Fixed(
3650                            tokens::component::date_picker::CALENDAR_CELL_SIZE,
3651                        )),
3652                );
3653            } else {
3654                let date = Date {
3655                    year: month.year,
3656                    month: month.month,
3657                    day,
3658                };
3659                let selected = state.selected_start_date == Some(date)
3660                    || state.selected_end_date == Some(date);
3661                let selected_progress = state.animation.selected_date_progress(date, selected);
3662                let range_position = state.date_range_position(date);
3663                let range_background_progress =
3664                    state.animation.range_background_progress(range_position);
3665                let is_today = date == today;
3666                let enabled = state.date_is_selectable(date);
3667                let action = on_action.clone()(DateRangePickerAction::SelectDate(date));
3668
3669                row = row.push(
3670                    day_button(DayCell {
3671                        day,
3672                        selected,
3673                        selected_progress,
3674                        today: is_today,
3675                        enabled,
3676                        weekday,
3677                        range_position,
3678                        range_background_progress,
3679                        content_alpha,
3680                    })
3681                    .on_press_maybe(enabled.then_some(action)),
3682                );
3683                day += 1;
3684            }
3685        }
3686
3687        column = column.push(row);
3688    }
3689
3690    let mut grid = Stack::new()
3691        .width(Length::Fixed(grid_width))
3692        .height(Length::Fixed(grid_height));
3693
3694    if let Some(info) = range_month_selection_info(state, month) {
3695        grid = grid.push(
3696            Canvas::new(RangeMonthBackground {
3697                info,
3698                progress: state
3699                    .animation
3700                    .range_background_progress(DateRangePosition::Middle),
3701                content_alpha,
3702            })
3703            .width(Length::Fixed(grid_width))
3704            .height(Length::Fixed(grid_height)),
3705        );
3706    }
3707
3708    grid = grid.push(
3709        Container::new(column)
3710            .width(Length::Fixed(grid_width))
3711            .height(Length::Fixed(grid_height)),
3712    );
3713
3714    Container::new(grid)
3715        .padding(Padding {
3716            top: 0.0,
3717            right: tokens::component::date_picker::HORIZONTAL_SPACE,
3718            bottom: 0.0,
3719            left: tokens::component::date_picker::HORIZONTAL_SPACE,
3720        })
3721        .into()
3722}
3723
3724fn range_year_grid<'a, Message, Renderer>(
3725    state: &'a DateRangePickerState,
3726    on_action: impl Fn(DateRangePickerAction) -> Message + Clone + 'a,
3727    progress: f32,
3728    content_alpha: f32,
3729) -> Element<'a, Message, Theme, Renderer>
3730where
3731    Message: Clone + 'a,
3732    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
3733    iced_widget::core::Font: Into<Renderer::Font>,
3734{
3735    let current_year = Date::today_utc().year;
3736    let selected_year = state.displayed_month.year;
3737    let mut column = Column::new().spacing(tokens::component::date_picker::YEAR_VERTICAL_SPACE);
3738    let mut years = state.year_range.clone().peekable();
3739
3740    while years.peek().is_some() {
3741        let mut row = year_grid_row::<Message, Renderer>();
3742
3743        for _ in 0..tokens::component::date_picker::YEARS_IN_ROW {
3744            if let Some(year) = years.next() {
3745                let enabled = state.selectable_dates.is_selectable_year(year);
3746                let action = on_action.clone()(DateRangePickerAction::SelectYear(year));
3747                row = row.push(Space::new().width(Length::Fill)).push(
3748                    year_button(
3749                        year,
3750                        year == selected_year,
3751                        year == current_year,
3752                        enabled,
3753                        content_alpha,
3754                    )
3755                    .on_press_maybe(enabled.then_some(action)),
3756                );
3757            } else {
3758                row = row
3759                    .push(Space::new().width(Length::Fill))
3760                    .push(year_placeholder());
3761            }
3762        }
3763
3764        column = column.push(row.push(Space::new().width(Length::Fill)));
3765    }
3766
3767    let full_height = tokens::component::date_picker::CALENDAR_CELL_SIZE
3768        * (tokens::component::date_picker::MAX_CALENDAR_ROWS as f32 + 1.0)
3769        - tokens::component::divider::THICKNESS;
3770    let height = (full_height * progress.clamp(0.0, 1.0)).max(1.0);
3771
3772    Container::new(
3773        Scrollable::new(column)
3774            .id(state.year_picker_scroll_id.clone())
3775            .height(Length::Fixed(full_height))
3776            .width(Length::Fill),
3777    )
3778    .height(Length::Fixed(height))
3779    .clip(true)
3780    .padding(Padding {
3781        top: 0.0,
3782        right: tokens::component::date_picker::HORIZONTAL_SPACE,
3783        bottom: 0.0,
3784        left: tokens::component::date_picker::HORIZONTAL_SPACE,
3785    })
3786    .style(move |theme| year_picker_panel_style(theme, content_alpha))
3787    .into()
3788}
3789
3790fn day_button<'a, Message, Renderer>(cell: DayCell) -> Button<'a, Message, Renderer>
3791where
3792    Message: Clone + 'a,
3793    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
3794    iced_widget::core::Font: Into<Renderer::Font>,
3795{
3796    let label = Canvas::new(cell)
3797        .width(Length::Fixed(
3798            tokens::component::date_picker::CALENDAR_CELL_SIZE,
3799        ))
3800        .height(Length::Fixed(
3801            tokens::component::date_picker::CALENDAR_CELL_SIZE,
3802        ));
3803
3804    Button::new(label)
3805        .width(Length::Fixed(
3806            tokens::component::date_picker::CALENDAR_CELL_SIZE,
3807        ))
3808        .height(Length::Fixed(
3809            tokens::component::date_picker::CALENDAR_CELL_SIZE,
3810        ))
3811        .padding(Padding::ZERO)
3812        .style(move |theme, status| {
3813            day_button_style(
3814                theme,
3815                status,
3816                cell.selected,
3817                cell.selected_progress,
3818                cell.enabled,
3819                cell.range_position,
3820                cell.content_alpha,
3821            )
3822        })
3823}
3824
3825#[derive(Debug, Clone, Copy)]
3826struct RangeMonthBackground {
3827    info: RangeMonthSelectionInfo,
3828    progress: f32,
3829    content_alpha: f32,
3830}
3831
3832impl<Message, Renderer> canvas::Program<Message, Theme, Renderer> for RangeMonthBackground
3833where
3834    Renderer: geometry::Renderer,
3835{
3836    type State = ();
3837
3838    fn draw(
3839        &self,
3840        _state: &Self::State,
3841        renderer: &Renderer,
3842        theme: &Theme,
3843        bounds: Rectangle,
3844        _cursor: mouse::Cursor,
3845    ) -> Vec<Geometry<Renderer>> {
3846        let mut frame = Frame::new(renderer, bounds.size());
3847        let progress = self.progress.clamp(0.0, 1.0);
3848
3849        if progress > 0.0 {
3850            let color = alpha_color(
3851                theme.colors().secondary.container,
3852                progress * self.content_alpha,
3853            );
3854            let rects = RangeBackground {
3855                info: self.info,
3856                width: frame.width(),
3857            }
3858            .reveal(progress);
3859
3860            for rect in rects {
3861                let size = Size::new(rect.width, rect.height);
3862                let top_left = Point::new(rect.x, rect.y);
3863                let path = Path::rounded_rectangle(
3864                    top_left,
3865                    size,
3866                    border::Radius::from(rect.corner_radius()),
3867                );
3868
3869                frame.fill(&path, color);
3870            }
3871        }
3872
3873        vec![frame.into_geometry()]
3874    }
3875}
3876
3877#[derive(Debug, Clone, Copy)]
3878struct DayCell {
3879    day: u8,
3880    selected: bool,
3881    selected_progress: f32,
3882    today: bool,
3883    enabled: bool,
3884    weekday: usize,
3885    range_position: DateRangePosition,
3886    range_background_progress: f32,
3887    content_alpha: f32,
3888}
3889
3890impl<Message, Renderer> canvas::Program<Message, Theme, Renderer> for DayCell
3891where
3892    Renderer: geometry::Renderer,
3893{
3894    type State = ();
3895
3896    fn draw(
3897        &self,
3898        _state: &Self::State,
3899        renderer: &Renderer,
3900        theme: &Theme,
3901        bounds: Rectangle,
3902        _cursor: mouse::Cursor,
3903    ) -> Vec<Geometry<Renderer>> {
3904        let colors = theme.colors();
3905        let mut frame = Frame::new(renderer, bounds.size());
3906        let center = frame.center();
3907        let indicator_width = tokens::component::date_picker::DATE_CONTAINER_WIDTH;
3908        let indicator_height = tokens::component::date_picker::DATE_CONTAINER_HEIGHT;
3909        let selected_progress = if self.selected {
3910            self.selected_progress.clamp(0.0, 1.0)
3911        } else {
3912            0.0
3913        };
3914        let connector_progress =
3915            RangeConnector::reveal_progress(self.range_background_progress, selected_progress);
3916
3917        if connector_progress > 0.0
3918            && let Some(rect) = (RangeConnector {
3919                position: self.range_position,
3920                weekday: self.weekday,
3921            }
3922            .rect())
3923        {
3924            let path = Path::rounded_rectangle(
3925                Point::new(rect.x, rect.y),
3926                Size::new(rect.width, rect.height),
3927                border::Radius::from(rect.corner_radius()),
3928            );
3929
3930            frame.fill(
3931                &path,
3932                alpha_color(
3933                    colors.secondary.container,
3934                    connector_progress * self.content_alpha,
3935                ),
3936            );
3937        }
3938
3939        let indicator_radius =
3940            indicator_width.min(indicator_height) / 2.0 * lerp(0.78, 1.0, selected_progress);
3941        let indicator = Path::circle(center, indicator_radius);
3942
3943        if selected_progress > 0.0 {
3944            let selected_container = if self.enabled {
3945                colors.primary.color
3946            } else {
3947                disabled_container(colors.surface.text)
3948            };
3949            frame.fill(
3950                &indicator,
3951                alpha_color(selected_container, selected_progress * self.content_alpha),
3952            );
3953        }
3954
3955        if self.today && selected_progress < 1.0 {
3956            frame.stroke(
3957                &indicator,
3958                Stroke::default()
3959                    .with_width(tokens::component::date_picker::DATE_TODAY_OUTLINE_WIDTH)
3960                    .with_color(alpha_color(
3961                        colors.primary.color,
3962                        (1.0 - selected_progress) * self.content_alpha,
3963                    )),
3964            );
3965        }
3966
3967        let scale = tokens::component::date_picker::DATE_LABEL_TEXT;
3968        let unselected_text_color = if self.range_position.is_middle() {
3969            colors.secondary.container_text
3970        } else if self.today {
3971            colors.primary.color
3972        } else {
3973            colors.surface.text
3974        };
3975        let text_color = if !self.enabled {
3976            disabled_text(colors.surface.text)
3977        } else if selected_progress > 0.0 {
3978            mix(
3979                unselected_text_color,
3980                colors.primary.text,
3981                selected_progress,
3982            )
3983        } else {
3984            unselected_text_color
3985        };
3986
3987        frame.fill_text(CanvasText {
3988            content: self.day.to_string(),
3989            position: center,
3990            max_width: indicator_width,
3991            color: alpha_color(text_color, self.content_alpha),
3992            size: scale.size.into(),
3993            line_height: absolute_line_height(scale.line_height),
3994            font: fonts::roboto_for_type_scale(scale),
3995            align_x: core_text::Alignment::Center,
3996            align_y: alignment::Vertical::Center,
3997            shaping: text::Shaping::Advanced,
3998        });
3999
4000        vec![frame.into_geometry()]
4001    }
4002}
4003
4004fn year_button<'a, Message, Renderer>(
4005    year: i32,
4006    selected: bool,
4007    current_year: bool,
4008    enabled: bool,
4009    content_alpha: f32,
4010) -> Button<'a, Message, Renderer>
4011where
4012    Message: Clone + 'a,
4013    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
4014    iced_widget::core::Font: Into<Renderer::Font>,
4015{
4016    let scale = tokens::component::date_picker::YEAR_LABEL_TEXT;
4017    let label = Container::new(
4018        Text::new(year.to_string())
4019            .size(scale.size)
4020            .line_height(absolute_line_height(scale.line_height))
4021            .font(fonts::roboto_for_type_scale(scale)),
4022    )
4023    .center_x(Length::Fixed(
4024        tokens::component::date_picker::YEAR_CONTAINER_WIDTH,
4025    ))
4026    .center_y(Length::Fixed(
4027        tokens::component::date_picker::YEAR_CONTAINER_HEIGHT,
4028    ));
4029
4030    Button::new(label)
4031        .width(Length::Fixed(
4032            tokens::component::date_picker::YEAR_CONTAINER_WIDTH,
4033        ))
4034        .height(Length::Fixed(
4035            tokens::component::date_picker::YEAR_CONTAINER_HEIGHT,
4036        ))
4037        .padding(Padding::ZERO)
4038        .style(move |theme, status| {
4039            year_button_style(
4040                theme,
4041                status,
4042                selected,
4043                current_year,
4044                enabled,
4045                content_alpha,
4046            )
4047        })
4048}
4049
4050fn time_input_field<'a, Message, Renderer>(
4051    state: &'a TimePickerState,
4052    on_action: impl Fn(TimePickerAction) -> Message + Clone + 'a,
4053    selection: TimePickerSelectionMode,
4054) -> Element<'a, Message, Theme, Renderer>
4055where
4056    Message: Clone + 'a,
4057    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
4058    iced_widget::core::Font: Into<Renderer::Font>,
4059{
4060    let selected = state.selection == selection;
4061    let valid = match selection {
4062        TimePickerSelectionMode::Hour => state.is_hour_input_valid(),
4063        TimePickerSelectionMode::Minute => state.is_minute_input_valid(),
4064    };
4065    let value = time_input_value(state, selection);
4066
4067    let field: Element<'a, Message, Theme, Renderer> = if selected {
4068        let input = text_input::outlined_placeholder(time_input_label(selection), &value)
4069            .error(!valid)
4070            .on_input({
4071                let on_action = on_action.clone();
4072                move |value| match selection {
4073                    TimePickerSelectionMode::Hour => {
4074                        on_action(TimePickerAction::HourInputChanged(value))
4075                    }
4076                    TimePickerSelectionMode::Minute => {
4077                        on_action(TimePickerAction::MinuteInputChanged(value))
4078                    }
4079                }
4080            })
4081            .width(Length::Fixed(
4082                tokens::component::time_input::TIME_FIELD_CONTAINER_WIDTH,
4083            ));
4084
4085        Container::new(input)
4086            .width(Length::Fixed(
4087                tokens::component::time_input::TIME_FIELD_CONTAINER_WIDTH,
4088            ))
4089            .center_y(Length::Fixed(
4090                tokens::component::time_input::TIME_FIELD_CONTAINER_HEIGHT,
4091            ))
4092            .into()
4093    } else {
4094        time_input_selector(value, valid)
4095            .on_press(on_action(TimePickerAction::SetSelection(selection)))
4096            .into()
4097    };
4098
4099    Column::new()
4100        .push(field)
4101        .push(time_input_supporting_text(state, selection, valid))
4102        .width(Length::Fixed(
4103            tokens::component::time_input::TIME_FIELD_CONTAINER_WIDTH,
4104        ))
4105        .into()
4106}
4107
4108fn time_input_selector<'a, Message, Renderer>(
4109    label: String,
4110    valid: bool,
4111) -> Button<'a, Message, Renderer>
4112where
4113    Message: Clone + 'a,
4114    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
4115    iced_widget::core::Font: Into<Renderer::Font>,
4116{
4117    let scale = tokens::component::time_input::TIME_FIELD_LABEL_TEXT;
4118    let label = Container::new(
4119        Text::new(label)
4120            .size(scale.size)
4121            .line_height(LineHeight::Absolute(scale.size.into()))
4122            .font(fonts::roboto_for_type_scale(scale)),
4123    )
4124    .center_x(Length::Fixed(
4125        tokens::component::time_input::TIME_FIELD_CONTAINER_WIDTH,
4126    ))
4127    .center_y(Length::Fixed(
4128        tokens::component::time_input::TIME_FIELD_CONTAINER_HEIGHT,
4129    ));
4130
4131    Button::new(label)
4132        .width(Length::Fixed(
4133            tokens::component::time_input::TIME_FIELD_CONTAINER_WIDTH,
4134        ))
4135        .height(Length::Fixed(
4136            tokens::component::time_input::TIME_FIELD_CONTAINER_HEIGHT,
4137        ))
4138        .padding(Padding::ZERO)
4139        .style(move |theme, status| time_input_selector_style(theme, status, valid))
4140}
4141
4142fn time_input_supporting_text<'a, Message, Renderer>(
4143    state: &TimePickerState,
4144    selection: TimePickerSelectionMode,
4145    valid: bool,
4146) -> Element<'a, Message, Theme, Renderer>
4147where
4148    Message: 'a,
4149    Renderer: core_text::Renderer + 'a,
4150    iced_widget::core::Font: Into<Renderer::Font>,
4151{
4152    let scale = tokens::component::time_input::TIME_FIELD_SUPPORTING_TEXT;
4153    let color_is_error = !valid;
4154
4155    Container::new(
4156        Text::new(time_input_supporting_label(state, selection, valid))
4157            .size(scale.size)
4158            .line_height(absolute_line_height(scale.line_height))
4159            .font(fonts::roboto_for_type_scale(scale))
4160            .style(move |theme: &Theme| {
4161                let colors = theme.colors();
4162
4163                text::Style {
4164                    color: Some(if color_is_error {
4165                        colors.error.color
4166                    } else {
4167                        colors.surface.text_variant
4168                    }),
4169                }
4170            }),
4171    )
4172    .padding(Padding {
4173        top: tokens::component::time_input::TIME_FIELD_SUPPORTING_TEXT_TOP_SPACE,
4174        right: 0.0,
4175        bottom: 0.0,
4176        left: 0.0,
4177    })
4178    .height(Length::Fixed(
4179        tokens::component::time_input::TIME_FIELD_SUPPORTING_TEXT_TOP_SPACE
4180            + tokens::component::time_input::TIME_FIELD_SUPPORTING_TEXT.line_height
4181                * tokens::component::time_input::TIME_FIELD_SUPPORTING_TEXT_LINES,
4182    ))
4183    .width(Length::Fill)
4184    .into()
4185}
4186
4187fn clock_display<'a, Message, Renderer>(
4188    state: &'a TimePickerState,
4189    on_action: impl Fn(TimePickerAction) -> Message + Clone + 'a,
4190    vertical_period: bool,
4191) -> Element<'a, Message, Theme, Renderer>
4192where
4193    Message: Clone + 'a,
4194    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
4195    iced_widget::core::Font: Into<Renderer::Font>,
4196{
4197    let hour = time_selector(
4198        two_digit(state.hour_for_display()),
4199        state
4200            .animation
4201            .selection_progress(TimePickerSelectionMode::Hour),
4202    )
4203    .on_press(on_action.clone()(TimePickerAction::SetSelection(
4204        TimePickerSelectionMode::Hour,
4205    )));
4206    let minute = time_selector(
4207        two_digit(state.minute),
4208        state
4209            .animation
4210            .selection_progress(TimePickerSelectionMode::Minute),
4211    )
4212    .on_press(on_action.clone()(TimePickerAction::SetSelection(
4213        TimePickerSelectionMode::Minute,
4214    )));
4215    let display = Row::new()
4216        .push(hour)
4217        .push(display_separator())
4218        .push(minute)
4219        .spacing(0.0)
4220        .align_y(alignment::Vertical::Center);
4221
4222    if state.is_24_hour {
4223        return display.into();
4224    }
4225
4226    if vertical_period {
4227        return display
4228            .push(Space::new().width(Length::Fixed(
4229                tokens::component::time_picker::PERIOD_TOGGLE_MARGIN,
4230            )))
4231            .push(period_toggle(state, on_action, true))
4232            .into();
4233    }
4234
4235    Column::new()
4236        .push(display)
4237        .push(Space::new().height(Length::Fixed(
4238            tokens::component::time_picker::PERIOD_TOGGLE_MARGIN,
4239        )))
4240        .push(period_toggle(state, on_action, false))
4241        .align_x(alignment::Horizontal::Center)
4242        .into()
4243}
4244
4245fn display_separator<'a, Message, Renderer>() -> Element<'a, Message, Theme, Renderer>
4246where
4247    Message: 'a,
4248    Renderer: core_text::Renderer + 'a,
4249    iced_widget::core::Font: Into<Renderer::Font>,
4250{
4251    display_separator_with_height(tokens::component::time_picker::TIME_SELECTOR_HEIGHT)
4252}
4253
4254fn display_separator_with_height<'a, Message, Renderer>(
4255    height: f32,
4256) -> Element<'a, Message, Theme, Renderer>
4257where
4258    Message: 'a,
4259    Renderer: core_text::Renderer + 'a,
4260    iced_widget::core::Font: Into<Renderer::Font>,
4261{
4262    display_separator_with_size(
4263        tokens::component::time_picker::DISPLAY_SEPARATOR_WIDTH,
4264        height,
4265    )
4266}
4267
4268fn display_separator_with_size<'a, Message, Renderer>(
4269    width: f32,
4270    height: f32,
4271) -> Element<'a, Message, Theme, Renderer>
4272where
4273    Message: 'a,
4274    Renderer: core_text::Renderer + 'a,
4275    iced_widget::core::Font: Into<Renderer::Font>,
4276{
4277    let scale = tokens::component::time_picker::TIME_SELECTOR_LABEL_TEXT;
4278
4279    Container::new(
4280        Text::new(":")
4281            .size(scale.size)
4282            .line_height(LineHeight::Absolute(scale.size.into()))
4283            .font(fonts::roboto_for_type_scale(scale)),
4284    )
4285    .center_x(Length::Fixed(width))
4286    .center_y(Length::Fixed(height))
4287    .into()
4288}
4289
4290fn time_selector<'a, Message, Renderer>(
4291    label: String,
4292    selected_progress: f32,
4293) -> Button<'a, Message, Renderer>
4294where
4295    Message: Clone + 'a,
4296    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
4297    iced_widget::core::Font: Into<Renderer::Font>,
4298{
4299    let scale = tokens::component::time_picker::TIME_SELECTOR_LABEL_TEXT;
4300    let label = Container::new(
4301        Text::new(label)
4302            .size(scale.size)
4303            .line_height(LineHeight::Absolute(scale.size.into()))
4304            .font(fonts::roboto_for_type_scale(scale)),
4305    )
4306    .center_x(Length::Fixed(
4307        tokens::component::time_picker::TIME_SELECTOR_WIDTH,
4308    ))
4309    .center_y(Length::Fixed(
4310        tokens::component::time_picker::TIME_SELECTOR_HEIGHT,
4311    ));
4312
4313    Button::new(label)
4314        .width(Length::Fixed(
4315            tokens::component::time_picker::TIME_SELECTOR_WIDTH,
4316        ))
4317        .height(Length::Fixed(
4318            tokens::component::time_picker::TIME_SELECTOR_HEIGHT,
4319        ))
4320        .padding(Padding::ZERO)
4321        .style(move |theme, status| time_selector_style(theme, status, selected_progress))
4322}
4323
4324fn period_toggle<'a, Message, Renderer>(
4325    state: &'a TimePickerState,
4326    on_action: impl Fn(TimePickerAction) -> Message + Clone + 'a,
4327    vertical: bool,
4328) -> Element<'a, Message, Theme, Renderer>
4329where
4330    Message: Clone + 'a,
4331    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
4332    iced_widget::core::Font: Into<Renderer::Font>,
4333{
4334    let width = if vertical {
4335        tokens::component::time_picker::PERIOD_SELECTOR_VERTICAL_WIDTH
4336    } else {
4337        tokens::component::time_picker::PERIOD_SELECTOR_HORIZONTAL_WIDTH
4338    };
4339    let height = if vertical {
4340        tokens::component::time_picker::PERIOD_SELECTOR_VERTICAL_HEIGHT
4341    } else {
4342        tokens::component::time_picker::PERIOD_SELECTOR_HORIZONTAL_HEIGHT
4343    };
4344
4345    period_toggle_sized(state, on_action, vertical, width, height)
4346}
4347
4348fn period_toggle_sized<'a, Message, Renderer>(
4349    state: &'a TimePickerState,
4350    on_action: impl Fn(TimePickerAction) -> Message + Clone + 'a,
4351    vertical: bool,
4352    width: f32,
4353    height: f32,
4354) -> Element<'a, Message, Theme, Renderer>
4355where
4356    Message: Clone + 'a,
4357    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
4358    iced_widget::core::Font: Into<Renderer::Font>,
4359{
4360    let (button_width, button_height) = period_toggle_item_size(vertical, width, height);
4361    let am = period_button(
4362        "AM",
4363        state.animation.period_progress(Period::Am),
4364        period_toggle_item_radius(vertical, true),
4365        button_width,
4366        button_height,
4367    )
4368    .on_press(on_action.clone()(TimePickerAction::SetPeriod(Period::Am)));
4369    let pm = period_button(
4370        "PM",
4371        state.animation.period_progress(Period::Pm),
4372        period_toggle_item_radius(vertical, false),
4373        button_width,
4374        button_height,
4375    )
4376    .on_press(on_action(TimePickerAction::SetPeriod(Period::Pm)));
4377
4378    let items: Element<'a, Message, Theme, Renderer> = if vertical {
4379        Column::new().spacing(0.0).push(am).push(pm).into()
4380    } else {
4381        Row::new().spacing(0.0).push(am).push(pm).into()
4382    };
4383
4384    Container::new(
4385        Stack::new()
4386            .push(items)
4387            .push(period_toggle_separator::<Message, Renderer>(
4388                vertical, width, height,
4389            ))
4390            .width(Length::Fixed(width))
4391            .height(Length::Fixed(height))
4392            .clip(true),
4393    )
4394    .width(Length::Fixed(width))
4395    .height(Length::Fixed(height))
4396    .style(period_toggle_container_style)
4397    .into()
4398}
4399
4400fn period_toggle_item_size(vertical: bool, width: f32, height: f32) -> (f32, f32) {
4401    if vertical {
4402        (width, height / 2.0)
4403    } else {
4404        (width / 2.0, height)
4405    }
4406}
4407
4408fn period_toggle_item_radius(vertical: bool, first: bool) -> border::Radius {
4409    let radius = tokens::component::time_picker::PERIOD_SELECTOR_SHAPE;
4410
4411    match (vertical, first) {
4412        (true, true) => border::Radius {
4413            top_left: radius,
4414            top_right: radius,
4415            bottom_right: 0.0,
4416            bottom_left: 0.0,
4417        },
4418        (true, false) => border::Radius {
4419            top_left: 0.0,
4420            top_right: 0.0,
4421            bottom_right: radius,
4422            bottom_left: radius,
4423        },
4424        (false, true) => border::Radius {
4425            top_left: radius,
4426            top_right: 0.0,
4427            bottom_right: 0.0,
4428            bottom_left: radius,
4429        },
4430        (false, false) => border::Radius {
4431            top_left: 0.0,
4432            top_right: radius,
4433            bottom_right: radius,
4434            bottom_left: 0.0,
4435        },
4436    }
4437}
4438
4439fn period_toggle_separator<'a, Message, Renderer>(
4440    vertical: bool,
4441    width: f32,
4442    height: f32,
4443) -> Element<'a, Message, Theme, Renderer>
4444where
4445    Message: 'a,
4446    Renderer: iced_widget::core::Renderer + 'a,
4447{
4448    let line = if vertical {
4449        Container::new(Space::new().width(Length::Fill).height(Length::Fixed(
4450            tokens::component::time_picker::PERIOD_SELECTOR_OUTLINE_WIDTH,
4451        )))
4452        .width(Length::Fixed(width))
4453        .height(Length::Fixed(
4454            tokens::component::time_picker::PERIOD_SELECTOR_OUTLINE_WIDTH,
4455        ))
4456    } else {
4457        Container::new(Space::new().width(Length::Fixed(
4458            tokens::component::time_picker::PERIOD_SELECTOR_OUTLINE_WIDTH,
4459        )))
4460        .width(Length::Fixed(
4461            tokens::component::time_picker::PERIOD_SELECTOR_OUTLINE_WIDTH,
4462        ))
4463        .height(Length::Fixed(height))
4464    }
4465    .style(period_toggle_separator_style);
4466
4467    if vertical {
4468        Container::new(line)
4469            .width(Length::Fixed(width))
4470            .height(Length::Fixed(height))
4471            .center_y(Length::Fixed(height))
4472            .into()
4473    } else {
4474        Container::new(line)
4475            .width(Length::Fixed(width))
4476            .height(Length::Fixed(height))
4477            .center_x(Length::Fixed(width))
4478            .into()
4479    }
4480}
4481
4482fn period_button<'a, Message, Renderer>(
4483    label: &'static str,
4484    selected_progress: f32,
4485    radius: border::Radius,
4486    width: f32,
4487    height: f32,
4488) -> Button<'a, Message, Renderer>
4489where
4490    Message: Clone + 'a,
4491    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
4492    iced_widget::core::Font: Into<Renderer::Font>,
4493{
4494    let scale = tokens::component::time_picker::PERIOD_SELECTOR_LABEL_TEXT;
4495    let label = Container::new(
4496        Text::new(label)
4497            .size(scale.size)
4498            .line_height(absolute_line_height(scale.line_height))
4499            .font(fonts::roboto_for_type_scale(scale)),
4500    )
4501    .center_x(Length::Fixed(width))
4502    .center_y(Length::Fixed(height));
4503
4504    Button::new(label)
4505        .width(Length::Fixed(width))
4506        .height(Length::Fixed(height))
4507        .padding(Padding::ZERO)
4508        .style(move |theme, status| period_button_style(theme, status, selected_progress, radius))
4509}
4510
4511include!("picker/clock.rs");
4512
4513include!("picker/translated.rs");
4514
4515include!("picker/style.rs");
4516
4517include!("picker/helpers.rs");
4518
4519#[cfg(test)]
4520#[path = "../../../tests/widget/component/picker.rs"]
4521mod tests;