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