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