Skip to main content

appcui/ui/timepicker/
timepicker.rs

1use super::{events::EventData, Flags};
2use crate::input::KeyCode;
3use crate::prelude::*;
4use crate::ui::common::{ControlEvent, ControlEventData};
5use chrono::{NaiveTime, Timelike};
6
7#[derive(Clone, Copy, PartialEq)]
8enum TimeComponent {
9    Hour,
10    Minute,
11    Second,
12    AmPm,
13}
14
15#[CustomControl(overwrite=OnPaint+OnKeyPressed+OnMouseEvent+OnFocus, internal=true)]
16pub struct TimePicker {
17    hour: u8,
18    minute: u8,
19    second: u8,
20    flags: Flags,
21    selected_component: TimeComponent,
22    editable_digit_is_first: bool,
23    hovered_component: Option<TimeComponent>,
24}
25
26impl TimePicker {
27    /// Creates a new TimePicker with the given time string (e.g. "12:34:56") and layout
28    /// 
29    /// # Arguments
30    /// 
31    /// * `time_str` - The time string to parse
32    /// * `layout` - The layout to use
33    /// * `flags` - The flags to use
34    /// 
35    /// # Example
36    /// ```rust,no_run
37    /// use appcui::prelude::*;
38    /// 
39    /// let timepicker = TimePicker::new("12:34:56", 
40    ///                                 layout!("x:1,y:1,w:10"), 
41    ///                                 timepicker::Flags::Seconds);
42    /// ```
43    pub fn new(time_str: &str, layout: Layout, flags: Flags) -> Self {
44        let time = time_str.parse::<NaiveTime>().unwrap();
45        Self::with_time(time, layout, flags)
46    }
47
48    /// Creates a new TimePicker with the given time and layout
49    /// 
50    /// # Arguments
51    /// 
52    /// * `time` - The time to use
53    /// * `layout` - The layout to use
54    /// * `flags` - The flags to use
55    /// 
56    /// # Example
57    /// ```rust,no_run
58    /// use appcui::prelude::*;
59    /// use chrono::NaiveTime;
60    /// 
61    /// let timepicker = TimePicker::with_time(
62    ///     NaiveTime::from_hms_opt(12, 34, 56).unwrap(), 
63    ///     layout!("x:1,y:1,w:10"), 
64    ///     timepicker::Flags::Seconds);
65    /// ```
66    pub fn with_time(time: NaiveTime, layout: Layout, flags: Flags) -> Self {
67        let hour = time.hour().min(23) as u8;
68        let minute = time.minute().min(59) as u8;
69        let second = time.second().min(59) as u8;
70
71        let mut obj = Self {
72            base: ControlBase::with_status_flags(layout, StatusFlags::Visible | StatusFlags::Enabled | StatusFlags::AcceptInput),
73            hour,
74            minute,
75            second,
76            flags,
77            selected_component: TimeComponent::Hour,
78            editable_digit_is_first: true,
79            hovered_component: None,
80        };
81
82        let min_width = 7 + if flags.contains(Flags::AMPM) { 3 } else { 0 } + if flags.contains(Flags::Seconds) { 3 } else { 0 };
83        obj.set_size_bounds(min_width, 1, u16::MAX, 1);
84        obj
85    }
86
87    fn raise_time_changed_event(&mut self) {
88        self.raise_event(ControlEvent {
89            emitter: self.handle,
90            receiver: self.event_processor,
91            data: ControlEventData::TimePicker(EventData {
92                time: self.time(),
93            }),
94        });
95    }
96
97    /// Gets the current time
98    #[inline(always)]
99    pub fn time(&self) -> NaiveTime {
100        NaiveTime::from_hms_opt(self.hour as u32, self.minute as u32, self.second as u32).unwrap()
101    }
102
103    /// Sets the time
104    pub fn set_time(&mut self, time: NaiveTime) {
105        self.hour = time.hour().min(23) as u8;
106        self.minute = time.minute().min(59) as u8;
107        self.second = time.second().min(59) as u8;
108    }
109
110    fn mouse_pos_to_component(&self, x: i32, y: i32) -> Option<TimeComponent> {
111        if y != 0 {
112            return None;
113        }
114        if !self.is_enabled() {
115            return None;
116        }
117        match x {
118            1 | 2 => Some(TimeComponent::Hour),
119            4 | 5 => Some(TimeComponent::Minute),
120            7 | 8 => {
121                if self.flags.contains_one(Flags::Seconds) {
122                    Some(TimeComponent::Second)
123                } else if self.flags.contains_one(Flags::AMPM) {
124                    Some(TimeComponent::AmPm)
125                } else {
126                    None
127                }
128            }
129            10 | 11 => {
130                if self.flags & (Flags::Seconds | Flags::AMPM) == (Flags::Seconds | Flags::AMPM) {
131                    Some(TimeComponent::AmPm)
132                } else {
133                    None
134                }
135            }
136            _ => None,
137        }
138    }
139    fn update_value(value: u8, max: u8, increment: bool) -> u8 {
140        let value = value % max;
141        if increment {
142            (value + 1) % max
143        } else {
144            (value + max - 1) % max
145        }
146    }
147    fn update_value_withdigit(value: u8, max: u8, digit: u8, first_digit: bool) -> Option<u8> {
148        if first_digit {
149            if digit > max / 10 {
150                None
151            } else {
152                Some((digit * 10 + (value % 10)).min(max))
153            }
154        } else {
155            let new_value = digit + ((value / 10) * 10);
156            if new_value > max {
157                None
158            } else {
159                Some(new_value)
160            }
161        }
162    }
163
164    fn update_digit(&mut self, digit: u8) {
165        let (value, max) = match self.selected_component {
166            TimeComponent::Hour => (self.hour, 23),
167            TimeComponent::Minute => (self.minute, 59),
168            TimeComponent::Second => (self.second, 59),
169            TimeComponent::AmPm => {
170                return;
171            }
172        };
173        if let Some(v) = Self::update_value_withdigit(value, max, digit, self.editable_digit_is_first) {
174            match self.selected_component {
175                TimeComponent::Hour => self.hour = v,
176                TimeComponent::Minute => self.minute = v,
177                TimeComponent::Second => self.second = v,
178                TimeComponent::AmPm => {}
179            }
180            self.editable_digit_is_first = !self.editable_digit_is_first;
181            if self.editable_digit_is_first {
182                self.move_to_next_component();
183            }
184            self.raise_time_changed_event();
185        }
186    }
187    fn increment_decrement_selected_component(&mut self, increment: bool) {
188        match self.selected_component {
189            TimeComponent::Hour => {
190                self.hour = Self::update_value(self.hour, 24, increment);
191            }
192            TimeComponent::Minute => {
193                self.minute = Self::update_value(self.minute, 60, increment);
194            }
195            TimeComponent::Second => {
196                self.second = Self::update_value(self.second, 60, increment);
197            }
198            TimeComponent::AmPm => {
199                // referse the hour to reflect AM or PM
200                if self.hour >= 12 {
201                    self.hour -= 12;
202                } else {
203                    self.hour += 12;
204                }
205                self.raise_time_changed_event();
206            }
207        }
208        self.raise_time_changed_event();
209    }
210
211    fn move_to_next_component(&mut self) {
212        self.selected_component = match self.selected_component {
213            TimeComponent::Hour => TimeComponent::Minute,
214            TimeComponent::Minute => {
215                if self.flags.contains(Flags::Seconds) {
216                    TimeComponent::Second
217                } else if self.flags.contains(Flags::AMPM) {
218                    TimeComponent::AmPm
219                } else {
220                    TimeComponent::Hour
221                }
222            }
223            TimeComponent::Second => {
224                if self.flags.contains(Flags::AMPM) {
225                    TimeComponent::AmPm
226                } else {
227                    TimeComponent::Hour
228                }
229            }
230            TimeComponent::AmPm => TimeComponent::Hour,
231        };
232        self.editable_digit_is_first = true;
233    }
234
235    fn move_to_prev_component(&mut self) {
236        self.selected_component = match self.selected_component {
237            TimeComponent::Hour => {
238                if self.flags.contains(Flags::AMPM) {
239                    TimeComponent::AmPm
240                } else if self.flags.contains(Flags::Seconds) {
241                    TimeComponent::Second
242                } else {
243                    TimeComponent::Minute
244                }
245            }
246            TimeComponent::Minute => TimeComponent::Hour,
247            TimeComponent::Second => TimeComponent::Minute,
248            TimeComponent::AmPm => {
249                if self.flags.contains(Flags::Seconds) {
250                    TimeComponent::Second
251                } else {
252                    TimeComponent::Minute
253                }
254            }
255        };
256        self.editable_digit_is_first = true;
257    }
258    #[inline(always)]
259    fn paint_number(&self, surface: &mut Surface, theme: &Theme, x: i32, comp: TimeComponent, attr: CharAttribute, has_focus: bool) {
260        let num = match comp {
261            TimeComponent::Hour => {
262                if self.flags.contains(Flags::AMPM) {
263                    match self.hour % 12 {
264                        0 => 12,
265                        n => n,
266                    }
267                } else {
268                    self.hour
269                }
270            }
271            TimeComponent::Minute => self.minute,
272            TimeComponent::Second => self.second,
273            TimeComponent::AmPm => 0,
274        };
275        let buf: [u8; 2] = [48 + num / 10, 48 + num % 10];
276        let (attr, show_cursor) = if has_focus {
277            if comp == self.selected_component {
278                (theme.editor.pressed_or_selected, true)
279            } else if Some(comp) == self.hovered_component {
280                (theme.editor.hovered, false)
281            } else {
282                (attr, false)
283            }
284        } else if Some(comp) == self.hovered_component {
285            (theme.editor.hovered, false)
286        } else {
287            (attr, false)
288        };
289        surface.write_ascii(x, 0, &buf, attr, false);
290        if show_cursor {
291            surface.set_cursor(x + if self.editable_digit_is_first { 0 } else { 1 }, 0);
292        }
293    }
294}
295
296impl OnPaint for TimePicker {
297    fn on_paint(&self, surface: &mut Surface, theme: &Theme) {
298        let attr = match () {
299            _ if !self.is_enabled() => theme.editor.inactive,
300            _ if self.has_focus() => theme.editor.focused,
301            _ => theme.editor.normal,
302        };
303        surface.clear(Character::with_attributes(' ', attr));
304
305        let has_focus = self.has_focus();
306        let sep = Character::with_attributes(':', theme.editor.inactive);
307
308        let mut x = 1;
309
310        // hour
311        self.paint_number(surface, theme, x, TimeComponent::Hour, attr, has_focus);
312        surface.write_char(x + 2, 0, sep);
313        x += 3;
314
315        // minute
316        self.paint_number(surface, theme, x, TimeComponent::Minute, attr, has_focus);
317        x += 2;
318
319        // second
320        if self.flags.contains(Flags::Seconds) {
321            surface.write_char(x, 0, sep);
322            x += 1;
323            self.paint_number(surface, theme, x, TimeComponent::Second, attr, has_focus);
324            x += 2;
325        }
326        if self.flags.contains(Flags::AMPM) {
327            x += 1;
328            let attr = if has_focus {
329                if self.selected_component == TimeComponent::AmPm {
330                    theme.editor.pressed_or_selected
331                } else if Some(TimeComponent::AmPm) == self.hovered_component {
332                    theme.editor.hovered
333                } else {
334                    attr
335                }
336            } else if Some(TimeComponent::AmPm) == self.hovered_component {
337                theme.editor.hovered
338            } else {
339                attr
340            };
341            if self.hour >= 12 {
342                surface.write_ascii(x, 0, b"PM", attr, false);
343            } else {
344                surface.write_ascii(x, 0, b"AM", attr, false);
345            }
346        }
347    }
348}
349
350impl OnKeyPressed for TimePicker {
351    fn on_key_pressed(&mut self, key: Key, character: char) -> EventProcessStatus {
352        match key.code {
353            KeyCode::Up => {
354                self.increment_decrement_selected_component(true);
355                EventProcessStatus::Processed
356            }
357            KeyCode::Down => {
358                self.increment_decrement_selected_component(false);
359                EventProcessStatus::Processed
360            }
361            KeyCode::Left => {
362                self.move_to_prev_component();
363                EventProcessStatus::Processed
364            }
365            KeyCode::Right => {
366                self.move_to_next_component();
367                EventProcessStatus::Processed
368            }
369            KeyCode::Backspace => {
370                self.editable_digit_is_first = true;
371                EventProcessStatus::Processed
372            }
373            _ => {
374                if character.is_ascii_digit() {
375                    self.update_digit((character as u8) - b'0');
376                    EventProcessStatus::Processed
377                } else {
378                    EventProcessStatus::Ignored
379                }
380            }
381        }
382    }
383}
384
385impl OnMouseEvent for TimePicker {
386    fn on_mouse_event(&mut self, mouse_event: &MouseEvent) -> EventProcessStatus {
387        if !self.is_enabled() {
388            return EventProcessStatus::Ignored;
389        }
390
391        match mouse_event {
392            MouseEvent::Pressed(data) => {
393                if let Some(new_pos) = self.mouse_pos_to_component(data.x, data.y) {
394                    self.selected_component = new_pos;
395                }
396                EventProcessStatus::Processed
397            }
398            MouseEvent::Enter | MouseEvent::Leave => {
399                self.hovered_component = None;
400                EventProcessStatus::Processed
401            }
402            MouseEvent::Over(point) => {
403                let new_pos = self.mouse_pos_to_component(point.x, point.y);
404                if new_pos != self.hovered_component {
405                    self.hovered_component = new_pos;
406                    EventProcessStatus::Processed
407                } else {
408                    EventProcessStatus::Ignored
409                }
410            }
411            MouseEvent::Wheel(dir) => {
412                match dir {
413                    MouseWheelDirection::Left => self.move_to_prev_component(),
414                    MouseWheelDirection::Right => self.move_to_next_component(),
415                    MouseWheelDirection::Up => self.increment_decrement_selected_component(true),
416                    MouseWheelDirection::Down => self.increment_decrement_selected_component(false),
417                };
418                EventProcessStatus::Processed
419            }
420            _ => EventProcessStatus::Ignored,
421        }
422    }
423}
424
425impl OnFocus for TimePicker {
426    fn on_focus(&mut self) {
427        self.selected_component = TimeComponent::Hour;
428        self.editable_digit_is_first = true;
429    }
430}