repose_material/material3/
time_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 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#[derive(Clone, Copy, PartialEq, Debug)]
41pub enum TimePickerLayoutType {
42 Horizontal,
43 Vertical,
44}
45
46#[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#[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
114pub 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 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 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}