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 .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 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}