repose_material/material3/
date_picker.rs1#![allow(non_snake_case)]
2
3use std::rc::Rc;
4
5use repose_core::*;
6use repose_ui::{Box, Column, Row, Spacer, Text, TextStyle, ViewExt};
7
8use super::*;
9
10pub struct DatePickerState {
12 pub year: Signal<i32>,
13 pub month: Signal<u32>, pub day: Signal<u32>,
15}
16
17impl DatePickerState {
18 pub fn new(year: i32, month: u32, day: u32) -> Self {
19 Self {
20 year: signal(year),
21 month: signal(month.clamp(1, 12)),
22 day: signal(day.clamp(1, 31)),
23 }
24 }
25
26 pub fn selected_date(&self) -> (i32, u32, u32) {
27 (self.year.get(), self.month.get(), self.day.get())
28 }
29}
30
31fn days_in_month(year: i32, month: u32) -> u32 {
32 match month {
33 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
34 4 | 6 | 9 | 11 => 30,
35 2 => {
36 if (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) {
37 29
38 } else {
39 28
40 }
41 }
42 _ => 30,
43 }
44}
45
46fn first_day_of_month(year: i32, month: u32) -> u32 {
49 let m = month as i32;
50 let (y, adj_m) = if m <= 2 {
51 (year - 1, m + 12)
52 } else {
53 (year, m)
54 };
55 let k = y % 100;
56 let j = y / 100;
57 let h = (1 + (13 * (adj_m + 1)) / 5 + k + k / 4 + j / 4 + 5 * j) % 7;
58 ((h + 5) % 7) as u32
60}
61
62struct ReposeDate {
64 year: i32,
65 month: u32,
66 day: u32,
67}
68
69impl ReposeDate {
70 fn now() -> Self {
72 let duration = web_time::SystemTime::now()
73 .duration_since(web_time::UNIX_EPOCH)
74 .unwrap_or_default();
75 let days = (duration.as_secs() / 86_400) as i64;
76 let z = days + 719468;
78 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
79 let doe = (z - era * 146_097) as u64;
80 let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
81 let y = (yoe as i64) + era * 400;
82 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
83 let mp = (5 * doy + 2) / 153;
84 let d = doy - (153 * mp + 2) / 5 + 1;
85 let m = if mp < 10 { mp + 3 } else { mp - 9 };
86 let y = if m <= 2 { y + 1 } else { y };
87 Self {
88 year: y as i32,
89 month: m as u32,
90 day: d as u32,
91 }
92 }
93}
94
95const MONTH_NAMES: [&str; 12] = [
96 "January",
97 "February",
98 "March",
99 "April",
100 "May",
101 "June",
102 "July",
103 "August",
104 "September",
105 "October",
106 "November",
107 "December",
108];
109
110const DOW_HEADERS: [&str; 7] = ["M", "T", "W", "T", "F", "S", "S"];
111
112#[derive(Clone)]
114pub struct DatePickerColors {
115 pub container_color: Color,
116 pub header_color: Color,
117 pub weekday_color: Color,
118 pub day_color: Color,
119 pub selected_day_color: Color,
120 pub selected_day_container_color: Color,
121 pub today_content_color: Color,
122 pub today_border_color: Color,
123 pub navigation_color: Color,
124 pub year_selected_container_color: Color,
125 pub year_selected_content_color: Color,
126 pub year_unselected_content_color: Color,
127}
128
129impl Default for DatePickerColors {
130 fn default() -> Self {
131 Self {
132 container_color: DatePickerDefaults::container_color(),
133 header_color: DatePickerDefaults::header_color(),
134 weekday_color: DatePickerDefaults::weekday_color(),
135 day_color: DatePickerDefaults::day_color(),
136 selected_day_color: DatePickerDefaults::selected_day_color(),
137 selected_day_container_color: DatePickerDefaults::selected_day_container_color(),
138 today_content_color: DatePickerDefaults::today_content_color(),
139 today_border_color: DatePickerDefaults::today_border_color(),
140 navigation_color: DatePickerDefaults::header_color(),
141 year_selected_container_color: DatePickerDefaults::year_selected_container_color(),
142 year_selected_content_color: DatePickerDefaults::year_selected_content_color(),
143 year_unselected_content_color: DatePickerDefaults::year_unselected_content_color(),
144 }
145 }
146}
147
148#[derive(Clone)]
150pub struct DatePickerConfig {
151 pub modifier: Modifier,
152 pub colors: DatePickerColors,
153 pub show_mode_toggle: bool,
154}
155
156impl Default for DatePickerConfig {
157 fn default() -> Self {
158 Self {
159 modifier: Modifier::new(),
160 colors: DatePickerColors::default(),
161 show_mode_toggle: true,
162 }
163 }
164}
165
166pub fn DatePicker(
169 state: Rc<DatePickerState>,
170 on_confirm: Rc<dyn Fn(i32, u32, u32)>,
171 on_dismiss: Rc<dyn Fn()>,
172 config: DatePickerConfig,
173) -> View {
174 let th = theme();
175 let (year, month, day) = state.selected_date();
176 let dim = days_in_month(year, month);
177 let start_dow = first_day_of_month(year, month);
178
179 let prev_year = {
181 let s = state.clone();
182 move || {
183 s.year.set(s.year.get() - 1);
184 let d = days_in_month(s.year.get(), s.month.get());
185 if s.day.get() > d {
186 s.day.set(d);
187 }
188 }
189 };
190 let next_year = {
191 let s = state.clone();
192 move || {
193 s.year.set(s.year.get() + 1);
194 let d = days_in_month(s.year.get(), s.month.get());
195 if s.day.get() > d {
196 s.day.set(d);
197 }
198 }
199 };
200
201 let prev_month = {
202 let s = state.clone();
203 move || {
204 if s.month.get() == 1 {
205 s.year.set(s.year.get() - 1);
206 s.month.set(12);
207 } else {
208 s.month.set(s.month.get() - 1);
209 }
210 let d = days_in_month(s.year.get(), s.month.get());
211 if s.day.get() > d {
212 s.day.set(d);
213 }
214 }
215 };
216
217 let next_month = {
218 let s = state.clone();
219 move || {
220 if s.month.get() == 12 {
221 s.year.set(s.year.get() + 1);
222 s.month.set(1);
223 } else {
224 s.month.set(s.month.get() + 1);
225 }
226 let d = days_in_month(s.year.get(), s.month.get());
227 if s.day.get() > d {
228 s.day.set(d);
229 }
230 }
231 };
232
233 let now = ReposeDate::now();
235 let today = (now.year, now.month, now.day);
236
237 Column(config.modifier.padding(16.0)).child((
238 Row(Modifier::new()
240 .fill_max_width()
241 .align_items(AlignItems::CENTER))
242 .child((
243 IconButton(
244 Box(Modifier::new())
245 .child(Text("◀").color(config.colors.navigation_color).size(16.0)),
246 prev_month,
247 IconButtonConfig::default(),
248 ),
249 Spacer(),
250 Column(Modifier::new().align_items(AlignItems::CENTER)).child((
251 Text(MONTH_NAMES[(month - 1) as usize].to_string())
252 .size(th.typography.title_medium)
253 .color(config.colors.header_color),
254 Row(Modifier::new().gap(8.0).align_items(AlignItems::CENTER)).child((
255 IconButton(
256 Box(Modifier::new())
257 .child(Text("‹").color(config.colors.navigation_color).size(14.0)),
258 prev_year,
259 IconButtonConfig::default(),
260 ),
261 Text(year.to_string())
262 .size(th.typography.body_small)
263 .color(th.on_surface_variant),
264 IconButton(
265 Box(Modifier::new())
266 .child(Text("›").color(config.colors.navigation_color).size(14.0)),
267 next_year,
268 IconButtonConfig::default(),
269 ),
270 )),
271 )),
272 Spacer(),
273 IconButton(
274 Box(Modifier::new())
275 .child(Text("▶").color(config.colors.navigation_color).size(16.0)),
276 next_month,
277 IconButtonConfig::default(),
278 ),
279 )),
280 Box(Modifier::new().fill_max_width().height(12.0)),
281 Column(Modifier::new()).child({
283 let mut rows: Vec<View> = Vec::new();
284 let dow_headers: Vec<View> = DOW_HEADERS
286 .iter()
287 .map(|d| {
288 Box(Modifier::new()
289 .width(40.0)
290 .height(40.0)
291 .align_items(AlignItems::CENTER)
292 .justify_content(JustifyContent::CENTER))
293 .child(
294 Text(d.to_string())
295 .size(th.typography.label_small)
296 .color(config.colors.weekday_color),
297 )
298 })
299 .collect();
300 rows.push(Row(Modifier::new()).with_children(dow_headers));
301
302 let total_cells = start_dow + dim;
304 let num_rows = total_cells.div_ceil(7).min(6);
305 for w in 0..num_rows {
306 let mut week: Vec<View> = Vec::new();
307 for d in 0..7 {
308 let cell_idx = w * 7 + d;
309 if cell_idx < start_dow {
310 week.push(Box(Modifier::new().width(40.0).height(40.0)));
311 } else {
312 let day_num = (cell_idx - start_dow + 1) as i32;
313 if day_num <= dim as i32 {
314 let is_selected = day_num == day as i32;
315 let is_today =
316 today.0 == year && today.1 == month && today.2 == day_num as u32;
317 let s = state.clone();
318 week.push(
319 Box(Modifier::new()
320 .width(40.0)
321 .height(40.0)
322 .background(if is_selected {
323 config.colors.selected_day_container_color
324 } else {
325 Color::TRANSPARENT
326 })
327 .clip_rounded(20.0)
328 .align_items(AlignItems::CENTER)
329 .justify_content(JustifyContent::CENTER)
330 .clickable()
331 .on_click(move || {
332 s.day.set(day_num as u32);
333 }))
334 .child({
335 let mut t = Text(day_num.to_string())
336 .size(th.typography.body_medium)
337 .color(if is_selected {
338 config.colors.selected_day_color
339 } else {
340 config.colors.day_color
341 });
342 if is_today && !is_selected {
343 t = t.modifier(Modifier::new().border(
344 1.0,
345 config.colors.today_border_color,
346 10.0,
347 ));
348 }
349 t
350 }),
351 );
352 } else {
353 week.push(Box(Modifier::new().width(40.0).height(40.0)));
354 }
355 }
356 }
357 rows.push(Row(Modifier::new()).with_children(week));
358 }
359 rows
360 }),
361 Box(Modifier::new().fill_max_width().height(12.0)),
362 Row(Modifier::new()
364 .fill_max_width()
365 .justify_content(JustifyContent::END)
366 .gap(8.0))
367 .child((
368 TextButton(
369 Modifier::new(),
370 {
371 let on_dismiss = on_dismiss.clone();
372 move || (on_dismiss)()
373 },
374 ButtonConfig::default(),
375 || Text("Cancel").size(14.0),
376 ),
377 Button(
378 Modifier::new(),
379 {
380 let on_confirm = on_confirm.clone();
381 let s = state.clone();
382 move || {
383 let (y, m, d) = s.selected_date();
384 on_confirm(y, m, d);
385 }
386 },
387 ButtonConfig::default(),
388 || Text("OK").size(14.0),
389 ),
390 )),
391 ))
392}