1use ferroui_base::utilities::{DateTime, DateTimeFormatInfo, DayOfWeek};
9
10#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
11pub enum MaterialDatePickerMode {
12 #[default]
13 Picker,
14 Input,
15}
16ferroui_base::ferro_markup_enum!(MaterialDatePickerMode { Picker, Input },
17 { namespace: "FerroUI.Themes.Material" });
18
19#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
20pub enum MaterialDateSelectionMode {
21 #[default]
22 Single,
23 Range,
24}
25ferroui_base::ferro_markup_enum!(MaterialDateSelectionMode { Single, Range },
26 { namespace: "FerroUI.Themes.Material" });
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub struct MaterialDatePickerState {
32 pub selected_start: Option<DateTime>,
33 pub selected_end: Option<DateTime>,
34 pub displayed_month: DateTime,
35 pub mode: MaterialDatePickerMode,
36 pub selection_mode: MaterialDateSelectionMode,
37 pub first_year: i32,
38 pub last_year: i32,
39}
40impl Default for MaterialDatePickerState {
41 fn default() -> Self {
42 Self {
43 selected_start: None,
44 selected_end: None,
45 displayed_month: month_start(DateTime::today()),
46 mode: MaterialDatePickerMode::Picker,
47 selection_mode: MaterialDateSelectionMode::Single,
48 first_year: 1900,
49 last_year: 2100,
50 }
51 }
52}
53impl MaterialDatePickerState {
54 pub fn canonical_date(&self, date: Option<DateTime>) -> Option<DateTime> {
55 date.filter(|d| self.contains_year(d.year())).map(day_start)
56 }
57 pub fn contains_year(&self, year: i32) -> bool {
58 (self.first_year..=self.last_year).contains(&year)
59 }
60 pub fn set_selection(&mut self, start: Option<DateTime>, end: Option<DateTime>) {
61 let start = self.canonical_date(start);
62 let end = if self.selection_mode == MaterialDateSelectionMode::Range {
63 self.canonical_date(end)
64 } else {
65 None
66 };
67 if start.is_some() && (end.is_none() || start <= end) {
68 self.selected_start = start;
69 self.selected_end = end;
70 } else {
71 self.selected_start = None;
72 self.selected_end = None;
73 }
74 }
75 pub fn select_date(&mut self, date: DateTime) {
78 let Some(date) = self.canonical_date(Some(date)) else {
79 return;
80 };
81 if self.selection_mode == MaterialDateSelectionMode::Single {
82 self.set_selection(Some(date), None);
83 } else if self.selected_end.is_some()
84 || self.selected_start.is_none()
85 || self.selected_start.is_some_and(|start| date < start)
86 {
87 self.set_selection(Some(date), None);
88 } else {
89 self.set_selection(self.selected_start, Some(date));
90 }
91 }
92 pub fn set_displayed_month(&mut self, date: DateTime) {
93 if self.contains_year(date.year()) {
94 self.displayed_month = month_start(date);
95 }
96 }
97 pub fn set_mode(&mut self, mode: MaterialDatePickerMode) {
98 if let Some(date) = self.selected_start {
99 self.set_displayed_month(date);
100 }
101 self.mode = mode;
102 }
103 pub fn set_year_range(&mut self, first: i32, last: i32) {
104 self.first_year = first.clamp(1, 9999);
105 self.last_year = last.clamp(self.first_year, 9999);
106 self.set_selection(self.selected_start, self.selected_end);
107 if !self.contains_year(self.displayed_month.year()) {
108 self.displayed_month = DateTime::new(
109 self.displayed_month
110 .year()
111 .clamp(self.first_year, self.last_year),
112 self.displayed_month.month(),
113 1,
114 );
115 }
116 }
117 pub fn is_in_range(&self, date: DateTime) -> bool {
118 self.selected_start
119 .zip(self.selected_end)
120 .is_some_and(|(start, end)| start <= date && date <= end)
121 }
122 pub fn is_complete(&self) -> bool {
123 self.selected_start.is_some()
124 && (self.selection_mode == MaterialDateSelectionMode::Single
125 || self.selected_end.is_some())
126 }
127 pub fn month_index(&self, month: DateTime) -> i32 {
128 (month.year() - self.first_year) * 12 + month.month() - 1
129 }
130 pub fn month_count(&self) -> i32 {
131 (self.last_year - self.first_year + 1) * 12
132 }
133 pub fn month_at(&self, index: i32) -> DateTime {
134 let index = index.clamp(0, self.month_count() - 1);
135 DateTime::new(self.first_year + index / 12, index % 12 + 1, 1)
136 }
137}
138
139pub(crate) fn day_start(date: DateTime) -> DateTime {
140 DateTime::new(date.year(), date.month(), date.day())
141}
142pub(crate) fn month_start(date: DateTime) -> DateTime {
143 DateTime::new(date.year(), date.month(), 1)
144}
145pub(crate) fn month_leading_days(month: DateTime, first_day: DayOfWeek) -> i32 {
146 (month_start(month).day_of_week().to_i32() - first_day.to_i32() + 7) % 7
147}
148
149#[derive(Clone, Debug, PartialEq, Eq)]
152pub struct MaterialDateInputFormat {
153 order: [char; 3],
154 delimiter: char,
155}
156impl MaterialDateInputFormat {
157 pub fn for_culture(info: &DateTimeFormatInfo) -> Self {
158 let mut order = Vec::new();
159 let mut delimiter = None;
160 let mut quoted = false;
161 for c in info.short_date_pattern().chars() {
162 if c == '\'' || c == '"' {
163 quoted = !quoted;
164 continue;
165 }
166 if quoted {
167 continue;
168 }
169 let part = match c {
170 'M' => Some('M'),
171 'd' => Some('d'),
172 'y' => Some('y'),
173 _ => None,
174 };
175 if let Some(part) = part {
176 if !order.contains(&part) {
177 order.push(part);
178 }
179 } else if matches!(c, '/' | '.' | '-') && delimiter.is_none() {
180 delimiter = Some(c);
181 }
182 }
183 Self {
184 order: order.try_into().unwrap_or(['M', 'd', 'y']),
185 delimiter: delimiter.unwrap_or('/'),
186 }
187 }
188 pub fn pattern(&self) -> String {
189 self.order
190 .iter()
191 .map(|c| match c {
192 'y' => "YYYY",
193 'M' => "MM",
194 _ => "DD",
195 })
196 .collect::<Vec<_>>()
197 .join(&self.delimiter.to_string())
198 }
199 pub fn digits(&self, date: DateTime) -> String {
200 self.order
201 .iter()
202 .map(|c| match c {
203 'y' => format!("{:04}", date.year()),
204 'M' => format!("{:02}", date.month()),
205 _ => format!("{:02}", date.day()),
206 })
207 .collect()
208 }
209 pub fn format_digits(&self, digits: &str) -> String {
210 let mut text = String::new();
211 let first = if self.order[0] == 'y' { 4 } else { 2 };
212 let second = first + if self.order[1] == 'y' { 4 } else { 2 };
213 for (index, digit) in digits.chars().take(8).enumerate() {
214 if index == first || index == second {
215 text.push(self.delimiter);
216 }
217 text.push(digit);
218 }
219 if digits.len() == first || digits.len() == second {
221 text.push(self.delimiter);
222 }
223 text
224 }
225 pub fn extract_digits(&self, text: &str) -> Option<String> {
226 if text
227 .chars()
228 .any(|c| !c.is_ascii_digit() && c != self.delimiter)
229 {
230 return None;
231 }
232 let digits: String = text.chars().filter(char::is_ascii_digit).collect();
233 (digits.len() <= 8).then_some(digits)
234 }
235 pub fn parse_digits(&self, digits: &str) -> Option<DateTime> {
236 if digits.len() != 8 || !digits.bytes().all(|v| v.is_ascii_digit()) {
237 return None;
238 }
239 let mut index = 0;
240 let (mut year, mut month, mut day) = (0, 0, 0);
241 for part in self.order {
242 let length = if part == 'y' { 4 } else { 2 };
243 let value: i32 = digits[index..index + length].parse().ok()?;
244 match part {
245 'y' => year = value,
246 'M' => month = value,
247 _ => day = value,
248 }
249 index += length;
250 }
251 DateTime::try_new(year, month, day)
252 }
253}