1use 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#[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#[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 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 pub fn today_utc() -> Self {
143 Self::from_utc_millis(current_utc_millis())
144 }
145
146 pub fn from_utc_millis(millis: i64) -> Self {
148 Self::from_days_since_epoch(millis.div_euclid(MILLIS_PER_DAY))
149 }
150
151 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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
257pub enum DateDisplayMode {
258 Picker,
259 Input,
260}
261
262#[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 pub fn all() -> Self {
284 Self {
285 is_selectable_date: Arc::new(|_| true),
286 is_selectable_year: Arc::new(|_| true),
287 }
288 }
289
290 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#[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 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 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#[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 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 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 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 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 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 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 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 pub fn update(&mut self, action: DatePickerAction) {
799 self.update_at(action, Instant::now());
800 }
801
802 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 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 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 pub fn advance(&mut self, now: Instant) -> bool {
911 self.animation.advance(now)
912 }
913
914 pub fn is_animating(&self) -> bool {
916 self.animation.is_animating()
917 }
918
919 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#[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#[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 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 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 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 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 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 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 pub fn update(&mut self, action: DateRangePickerAction) {
1185 self.update_at(action, Instant::now());
1186 }
1187
1188 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 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 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 pub fn advance(&mut self, now: Instant) -> bool {
1331 self.animation.advance(now)
1332 }
1333
1334 pub fn is_animating(&self) -> bool {
1336 self.animation.is_animating()
1337 }
1338
1339 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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1478pub enum TimePickerSelectionMode {
1479 Hour,
1480 Minute,
1481}
1482
1483#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1485pub enum Period {
1486 Am,
1487 Pm,
1488}
1489
1490#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1647pub enum TimePickerDisplayMode {
1648 Picker,
1649 Input,
1650 Scroll,
1651}
1652
1653impl TimePickerDisplayMode {
1654 pub fn toggled(self) -> Self {
1656 match self {
1657 Self::Picker => Self::Input,
1658 Self::Input | Self::Scroll => Self::Picker,
1659 }
1660 }
1661
1662 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#[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 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 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 pub fn update(&mut self, action: TimePickerAction) {
1810 self.update_at(action, Instant::now());
1811 }
1812
1813 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 pub fn advance(&mut self, now: Instant) -> bool {
1928 self.animation.advance(now)
1929 }
1930
1931 pub fn is_animating(&self) -> bool {
1933 self.animation.is_animating()
1934 }
1935
1936 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#[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#[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 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#[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 pub const fn layout(mut self, layout: TimePickerLayout) -> Self {
2029 self.layout = layout;
2030 self
2031 }
2032}
2033
2034pub 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
2047pub 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
2069pub 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
2082pub 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
2109pub 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
2123pub 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
2141pub 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
2155pub 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
2178pub 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
2195pub 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
2208pub 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
2225pub 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
2293pub 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;