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