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