Skip to main content

repose_material/material3/
time_picker.rs

1#![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
10/// State for `TimePicker` - manages selected hour and minute.
11pub struct TimePickerState {
12    pub hour: Signal<u32>,
13    pub minute: Signal<u32>,
14    pub is_am: Signal<bool>,
15}
16
17impl TimePickerState {
18    pub fn new(hour: u32, minute: u32) -> Self {
19        let h = hour % 12;
20        let am = hour < 12;
21        Self {
22            hour: signal(if h == 0 { 12 } else { h }),
23            minute: signal(minute.min(59)),
24            is_am: signal(am),
25        }
26    }
27
28    pub fn selected_time(&self) -> (u32, u32) {
29        let mut h = self.hour.get();
30        if !self.is_am.get() {
31            h = (h % 12) + 12;
32        } else if h == 12 {
33            h = 0;
34        }
35        (h, self.minute.get())
36    }
37}
38
39/// Layout types for [`TimePicker`].
40#[derive(Clone, Copy, PartialEq, Debug)]
41pub enum TimePickerLayoutType {
42    Horizontal,
43    Vertical,
44}
45
46/// Colors for [`TimePicker`].
47#[derive(Clone)]
48pub struct TimePickerColors {
49    pub clock_dial_color: Color,
50    pub clock_dial_selected_content_color: Color,
51    pub clock_dial_unselected_content_color: Color,
52    pub selector_color: Color,
53    pub container_color: Color,
54    pub period_selector_border_color: Color,
55    pub period_selector_selected_container_color: Color,
56    pub period_selector_unselected_container_color: Color,
57    pub period_selector_selected_content_color: Color,
58    pub period_selector_unselected_content_color: Color,
59    pub time_selector_selected_container_color: Color,
60    pub time_selector_unselected_container_color: Color,
61    pub time_selector_selected_content_color: Color,
62    pub time_selector_unselected_content_color: Color,
63}
64
65impl Default for TimePickerColors {
66    fn default() -> Self {
67        Self {
68            clock_dial_color: TimePickerDefaults::clock_dial_color(),
69            clock_dial_selected_content_color:
70                TimePickerDefaults::clock_dial_selected_content_color(),
71            clock_dial_unselected_content_color:
72                TimePickerDefaults::clock_dial_unselected_content_color(),
73            selector_color: TimePickerDefaults::selector_color(),
74            container_color: TimePickerDefaults::container_color(),
75            period_selector_border_color: TimePickerDefaults::period_selector_border_color(),
76            period_selector_selected_container_color:
77                TimePickerDefaults::period_selector_selected_container_color(),
78            period_selector_unselected_container_color:
79                TimePickerDefaults::period_selector_unselected_container_color(),
80            period_selector_selected_content_color:
81                TimePickerDefaults::period_selector_selected_content_color(),
82            period_selector_unselected_content_color:
83                TimePickerDefaults::period_selector_unselected_content_color(),
84            time_selector_selected_container_color:
85                TimePickerDefaults::time_selector_selected_container_color(),
86            time_selector_unselected_container_color:
87                TimePickerDefaults::time_selector_unselected_container_color(),
88            time_selector_selected_content_color:
89                TimePickerDefaults::time_selector_selected_content_color(),
90            time_selector_unselected_content_color:
91                TimePickerDefaults::time_selector_unselected_content_color(),
92        }
93    }
94}
95
96/// Configuration for [`TimePicker`].
97#[derive(Clone)]
98pub struct TimePickerConfig {
99    pub modifier: Modifier,
100    pub colors: TimePickerColors,
101    pub layout_type: TimePickerLayoutType,
102}
103
104impl Default for TimePickerConfig {
105    fn default() -> Self {
106        Self {
107            modifier: Modifier::new(),
108            colors: TimePickerColors::default(),
109            layout_type: TimePickerLayoutType::Vertical,
110        }
111    }
112}
113
114/// M3 Time Picker - a simple time picker with hour/minute fields and AM/PM toggle.
115pub fn TimePicker(
116    state: Rc<TimePickerState>,
117    on_confirm: Rc<dyn Fn(u32, u32)>,
118    on_dismiss: Rc<dyn Fn()>,
119    config: TimePickerConfig,
120) -> View {
121    let th = theme();
122    let hour = state.hour.get();
123    let minute = state.minute.get();
124    let is_am = state.is_am.get();
125
126    let hour_str = format!("{:02}", hour);
127    let min_str = format!("{:02}", minute);
128
129    Column(
130        config
131            .modifier
132            .width(256.0)
133            .padding(24.0)
134            .align_items(AlignItems::CENTER),
135    )
136    .child((
137        // Time display
138        Row(Modifier::new().align_items(AlignItems::CENTER)).child((
139            Box(Modifier::new()
140                .padding(8.0)
141                .indication(crate::ripple::ripple(crate::ripple::RippleConfig {
142                    color: Some(th.on_surface),
143                    bounded: true,
144                    ..Default::default()
145                }))
146                .clickable()
147                .on_click({
148                    let s = state.clone();
149                    move || s.hour.set((s.hour.get() % 12) + 1)
150                }))
151            .child(
152                Text(hour_str)
153                    .size(48.0)
154                    .color(config.colors.clock_dial_unselected_content_color)
155                    .single_line(),
156            ),
157            Text(":")
158                .size(48.0)
159                .color(config.colors.clock_dial_unselected_content_color)
160                .single_line(),
161            Box(Modifier::new()
162                .padding(8.0)
163                .indication(crate::ripple::ripple(crate::ripple::RippleConfig {
164                    color: Some(th.on_surface),
165                    bounded: true,
166                    ..Default::default()
167                }))
168                .clickable()
169                .on_click({
170                    let s = state.clone();
171                    move || s.minute.set((s.minute.get() + 1) % 60)
172                }))
173            .child(
174                Text(min_str)
175                    .size(48.0)
176                    .color(config.colors.clock_dial_unselected_content_color)
177                    .single_line(),
178            ),
179        )),
180        Box(Modifier::new().fill_max_width().height(16.0)),
181        // AM/PM toggle
182        Row(Modifier::new().align_items(AlignItems::CENTER)).child((
183            Box(Modifier::new()
184                .padding_values(PaddingValues {
185                    left: 12.0,
186                    right: 12.0,
187                    top: 4.0,
188                    bottom: 4.0,
189                })
190                .background(if is_am {
191                    config.colors.period_selector_selected_container_color
192                } else {
193                    Color::TRANSPARENT
194                })
195                .clip_rounded(8.0)
196                .indication(crate::ripple::ripple(crate::ripple::RippleConfig {
197                    color: Some(th.on_surface),
198                    bounded: true,
199                    ..Default::default()
200                }))
201                .clickable()
202                .on_click({
203                    let s = state.clone();
204                    move || {
205                        if !s.is_am.get() {
206                            s.is_am.set(true);
207                            let h = s.hour.get();
208                            s.hour.set(if h == 12 { 12 } else { (h + 12) % 24 });
209                            if s.hour.get() == 0 {
210                                s.hour.set(12);
211                            }
212                        }
213                    }
214                }))
215            .child(Text("AM").size(th.typography.label_large).color(if is_am {
216                config.colors.period_selector_selected_content_color
217            } else {
218                config.colors.period_selector_unselected_content_color
219            })),
220            Box(Modifier::new().width(8.0).height(1.0)),
221            Box(Modifier::new()
222                .padding_values(PaddingValues {
223                    left: 12.0,
224                    right: 12.0,
225                    top: 4.0,
226                    bottom: 4.0,
227                })
228                .background(if !is_am {
229                    config.colors.period_selector_selected_container_color
230                } else {
231                    Color::TRANSPARENT
232                })
233                .clip_rounded(8.0)
234                .indication(crate::ripple::ripple(crate::ripple::RippleConfig {
235                    color: Some(th.on_surface),
236                    bounded: true,
237                    ..Default::default()
238                }))
239                .clickable()
240                .on_click({
241                    let s = state.clone();
242                    move || {
243                        if s.is_am.get() {
244                            s.is_am.set(false);
245                            let h = s.hour.get();
246                            s.hour.set(if h == 12 { 12 } else { (h + 12) % 24 });
247                            if s.hour.get() == 0 {
248                                s.hour.set(12);
249                            }
250                        }
251                    }
252                }))
253            .child(Text("PM").size(th.typography.label_large).color(if !is_am {
254                config.colors.period_selector_selected_content_color
255            } else {
256                config.colors.period_selector_unselected_content_color
257            })),
258        )),
259        Box(Modifier::new().fill_max_width().height(16.0)),
260        Row(Modifier::new().fill_max_width()).child((
261            Spacer(),
262            Box(Modifier::new()
263                .padding(8.0)
264                .indication(crate::ripple::ripple(crate::ripple::RippleConfig {
265                    color: Some(th.on_surface),
266                    bounded: true,
267                    ..Default::default()
268                }))
269                .clickable()
270                .on_click({
271                    let on_dismiss = on_dismiss.clone();
272                    move || on_dismiss()
273                }))
274            .child(
275                Text("Cancel")
276                    .color(config.colors.selector_color)
277                    .size(th.typography.label_large)
278                    .single_line(),
279            ),
280            Box(Modifier::new().width(8.0).height(1.0)),
281            Box(Modifier::new()
282                .padding(8.0)
283                .indication(crate::ripple::ripple(crate::ripple::RippleConfig {
284                    color: Some(th.on_surface),
285                    bounded: true,
286                    ..Default::default()
287                }))
288                .clickable()
289                .on_click({
290                let on_confirm = on_confirm.clone();
291                let state = state.clone();
292                move || {
293                    let (h, m) = state.selected_time();
294                    on_confirm(h, m);
295                }
296            }))
297            .child(
298                Text("OK")
299                    .color(config.colors.selector_color)
300                    .size(th.typography.label_large)
301                    .single_line(),
302            ),
303        )),
304    ))
305}