Skip to main content

material_ui_rs/widget/component/
picker.rs

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