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