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                .clickable()
141                .on_click({
142                    let s = state.clone();
143                    move || s.hour.set((s.hour.get() % 12) + 1)
144                })
145                .padding(8.0))
146            .child(
147                Text(hour_str)
148                    .size(48.0)
149                    .color(config.colors.clock_dial_unselected_content_color)
150                    .single_line(),
151            ),
152            Text(":")
153                .size(48.0)
154                .color(config.colors.clock_dial_unselected_content_color)
155                .single_line(),
156            Box(Modifier::new()
157                .clickable()
158                .on_click({
159                    let s = state.clone();
160                    move || s.minute.set((s.minute.get() + 1) % 60)
161                })
162                .padding(8.0))
163            .child(
164                Text(min_str)
165                    .size(48.0)
166                    .color(config.colors.clock_dial_unselected_content_color)
167                    .single_line(),
168            ),
169        )),
170        Box(Modifier::new().fill_max_width().height(16.0)),
171        // AM/PM toggle
172        Row(Modifier::new().align_items(AlignItems::CENTER)).child((
173            Box(Modifier::new()
174                .padding_values(PaddingValues {
175                    left: 12.0,
176                    right: 12.0,
177                    top: 4.0,
178                    bottom: 4.0,
179                })
180                .background(if is_am {
181                    config.colors.period_selector_selected_container_color
182                } else {
183                    Color::TRANSPARENT
184                })
185                .clip_rounded(8.0)
186                .clickable()
187                .on_click({
188                    let s = state.clone();
189                    move || {
190                        if !s.is_am.get() {
191                            s.is_am.set(true);
192                            let h = s.hour.get();
193                            s.hour.set(if h == 12 { 12 } else { (h + 12) % 24 });
194                            if s.hour.get() == 0 {
195                                s.hour.set(12);
196                            }
197                        }
198                    }
199                }))
200            .child(Text("AM").size(th.typography.label_large).color(if is_am {
201                config.colors.period_selector_selected_content_color
202            } else {
203                config.colors.period_selector_unselected_content_color
204            })),
205            Box(Modifier::new().width(8.0).height(1.0)),
206            Box(Modifier::new()
207                .padding_values(PaddingValues {
208                    left: 12.0,
209                    right: 12.0,
210                    top: 4.0,
211                    bottom: 4.0,
212                })
213                .background(if !is_am {
214                    config.colors.period_selector_selected_container_color
215                } else {
216                    Color::TRANSPARENT
217                })
218                .clip_rounded(8.0)
219                .clickable()
220                .on_click({
221                    let s = state.clone();
222                    move || {
223                        if s.is_am.get() {
224                            s.is_am.set(false);
225                            let h = s.hour.get();
226                            s.hour.set(if h == 12 { 12 } else { (h + 12) % 24 });
227                            if s.hour.get() == 0 {
228                                s.hour.set(12);
229                            }
230                        }
231                    }
232                }))
233            .child(Text("PM").size(th.typography.label_large).color(if !is_am {
234                config.colors.period_selector_selected_content_color
235            } else {
236                config.colors.period_selector_unselected_content_color
237            })),
238        )),
239        Box(Modifier::new().fill_max_width().height(16.0)),
240        Row(Modifier::new().fill_max_width()).child((
241            Spacer(),
242            Box(Modifier::new().padding(8.0).clickable().on_click({
243                let on_dismiss = on_dismiss.clone();
244                move || on_dismiss()
245            }))
246            .child(
247                Text("Cancel")
248                    .color(config.colors.selector_color)
249                    .size(th.typography.label_large)
250                    .single_line(),
251            ),
252            Box(Modifier::new().width(8.0).height(1.0)),
253            Box(Modifier::new().padding(8.0).clickable().on_click({
254                let on_confirm = on_confirm.clone();
255                let state = state.clone();
256                move || {
257                    let (h, m) = state.selected_time();
258                    on_confirm(h, m);
259                }
260            }))
261            .child(
262                Text("OK")
263                    .color(config.colors.selector_color)
264                    .size(th.typography.label_large)
265                    .single_line(),
266            ),
267        )),
268    ))
269}