1use ferroui_base::layout::Orientation;
8use ferroui_base::{Point, Rect, Size};
9use std::f64::consts::{FRAC_PI_2, PI, TAU};
10
11#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
12pub enum MaterialTimeSelection {
13 #[default]
14 Hour,
15 Minute,
16}
17ferroui_base::ferro_markup_enum!(MaterialTimeSelection { Hour, Minute }, { namespace: "FerroUI.Themes.Material" });
18
19#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
20pub enum MaterialTimeDisplayMode {
21 #[default]
22 Dial,
23 Input,
24}
25ferroui_base::ferro_markup_enum!(MaterialTimeDisplayMode { Dial, Input }, { namespace: "FerroUI.Themes.Material" });
26
27pub(crate) fn display_hour(hour: i32, is_24_hour: bool) -> i32 {
28 if is_24_hour {
29 hour
30 } else if hour % 12 == 0 {
31 12
32 } else {
33 hour % 12
34 }
35}
36
37pub(crate) fn angle_for(hour: i32, minute: i32, selection: MaterialTimeSelection) -> f64 {
38 TAU * match selection {
39 MaterialTimeSelection::Hour => (hour % 12) as f64 / 12.0,
40 MaterialTimeSelection::Minute => minute as f64 / 60.0,
41 } - FRAC_PI_2
42}
43
44pub(crate) fn shortest_target(current: f64, target: f64) -> f64 {
46 let difference = (current - target).rem_euclid(TAU);
47 current
48 - if difference > PI {
49 difference - TAU
50 } else {
51 difference
52 }
53}
54
55#[derive(Clone, Copy, Debug)]
56pub(crate) struct DialGeometry {
57 pub diameter: f64,
58 pub center: Point,
59 pub scale: f64,
60}
61impl DialGeometry {
62 pub fn new(size: Size) -> Self {
63 let diameter = size.width.min(size.height).max(0.0);
64 Self {
65 diameter,
66 center: Point::new(size.width / 2.0, size.height / 2.0),
67 scale: diameter / 256.0,
68 }
69 }
70 pub fn center_at(self, angle: f64, inner: bool) -> Point {
71 let radius = if inner { 69.0 } else { 101.0 } * self.scale;
72 Point::new(
73 self.center.x + radius * angle.cos(),
74 self.center.y + radius * angle.sin(),
75 )
76 }
77 pub fn number_rect(self, index: usize) -> Rect {
78 let p = self.center_at(index as f64 * TAU / 12.0 - FRAC_PI_2, index >= 12);
79 Rect::new(p.x - 24.0, p.y - 24.0, 48.0, 48.0)
80 }
81 pub fn value_at(
82 self,
83 position: Point,
84 selection: MaterialTimeSelection,
85 is_24_hour: bool,
86 is_pm: bool,
87 tapping: bool,
88 ) -> (i32, f64) {
89 let dx = position.x - self.center.x;
90 let dy = position.y - self.center.y;
91 let angle = dy.atan2(dx);
92 let fraction = (angle + FRAC_PI_2).rem_euclid(TAU) / TAU;
93 match selection {
94 MaterialTimeSelection::Hour => {
95 let hour = (fraction * 12.0).round() as i32 % 12;
96 let pm = if is_24_hour {
97 dx.hypot(dy) < 74.0 * self.scale
98 } else {
99 is_pm
100 };
101 (hour + if pm { 12 } else { 0 }, angle)
102 }
103 MaterialTimeSelection::Minute => {
104 let minute = if tapping {
105 ((fraction * 12.0).round() as i32 * 5) % 60
106 } else {
107 (fraction * 60.0).round() as i32 % 60
108 };
109 (minute, angle)
110 }
111 }
112 }
113}
114
115pub(crate) fn parse_input(
118 text: &str,
119 selection: MaterialTimeSelection,
120 is_24_hour: bool,
121 is_pm: bool,
122) -> Result<Option<i32>, ()> {
123 if text.len() > 2 || !text.bytes().all(|c| c.is_ascii_digit()) {
124 return Err(());
125 }
126 let value = if text.is_empty() {
127 0
128 } else {
129 text.parse::<i32>().map_err(|_| ())?
130 };
131 Ok(match selection {
132 MaterialTimeSelection::Minute => (value <= 59).then_some(value),
133 MaterialTimeSelection::Hour if is_24_hour => (value <= 23).then_some(value),
134 MaterialTimeSelection::Hour => {
135 (value <= 12).then_some(value % 12 + if is_pm { 12 } else { 0 })
136 }
137 })
138}
139
140#[derive(Clone, Copy, Debug)]
141pub(crate) struct TimePickerMetrics {
142 pub field_width: f64,
143 pub field_height: f64,
144 pub separator: f64,
145 pub period_width: f64,
146 pub period_gap: f64,
147 pub period_inner_gap: f64,
148 pub dial_size: f64,
149 pub dial_gap: f64,
150 pub bottom: f64,
151 pub support_height: f64,
152 pub horizontal: bool,
153 pub input: bool,
154}
155impl TimePickerMetrics {
156 pub fn new(
157 vibrant: bool,
158 is_24_hour: bool,
159 mode: MaterialTimeDisplayMode,
160 orientation: Orientation,
161 available_height: f64,
162 ) -> Self {
163 let input = mode == MaterialTimeDisplayMode::Input;
164 let horizontal = !input && orientation == Orientation::Horizontal;
165 Self {
166 field_width: if vibrant {
169 if is_24_hour && !input && !horizontal {
170 132.0
171 } else {
172 100.0
173 }
174 } else {
175 96.0
176 },
177 field_height: if vibrant {
178 120.0
179 } else if input {
180 72.0
181 } else {
182 80.0
183 },
184 separator: if vibrant { 16.0 } else { 24.0 },
185 period_width: if vibrant { 56.0 } else { 52.0 },
186 period_gap: if horizontal {
187 16.0
188 } else if vibrant {
189 if input { 8.0 } else { 16.0 }
190 } else {
191 4.0
192 },
193 period_inner_gap: if vibrant {
194 if horizontal { 16.0 } else { 8.0 }
195 } else {
196 4.0
197 },
198 dial_size: if !horizontal || vibrant || available_height >= 384.0 {
199 256.0
200 } else if available_height >= 330.0 {
201 238.0
202 } else {
203 200.0
204 },
205 dial_gap: if vibrant {
206 if horizontal { 52.0 } else { 12.0 }
207 } else {
208 36.0
209 },
210 bottom: if !vibrant && !horizontal && !input {
211 24.0
212 } else {
213 0.0
214 },
215 support_height: if input && !vibrant { 39.0 } else { 0.0 },
216 horizontal,
217 input,
218 }
219 }
220 pub fn fields_width(self) -> f64 {
221 2.0 * self.field_width + self.separator
222 }
223 pub fn display_size(self, is_24_hour: bool, vibrant: bool) -> Size {
224 let mut size = Size::new(self.fields_width(), self.field_height + self.support_height);
225 if !is_24_hour {
226 if self.horizontal {
227 size.height += self.period_gap + if vibrant { 40.0 } else { 38.0 };
228 } else {
229 size.width += self.period_gap + self.period_width;
230 }
231 }
232 size
233 }
234 pub fn desired_size(self, is_24_hour: bool, vibrant: bool) -> Size {
235 let display = self.display_size(is_24_hour, vibrant);
236 if self.input {
237 display
238 } else if self.horizontal {
239 Size::new(
240 display.width + self.dial_gap + self.dial_size,
241 display.height.max(self.dial_size),
242 )
243 } else {
244 Size::new(
245 display.width.max(self.dial_size),
246 display.height + self.dial_gap + self.dial_size + self.bottom,
247 )
248 }
249 }
250}