Skip to main content

ferroui_controls/date_time_pickers/
time_picker_presenter.rs

1use super::{
2    DateTimePickerPanel, DateTimePickerPanelType, PickerPresenterBase, PickerPresenterBaseImpl, PickerPresenterBaseImplExt, TimePicker,
3};
4use crate::primitives::{TemplateAppliedEventArgs, TemplatedControlImpl, TemplatedControlImplExt};
5use crate::{Button, ColumnDefinition, ColumnDefinitions, Control, ControlImpl, Grid, GridLength, Panel, SpinDirection};
6use ferroui_base::animation::TimeSpan;
7use ferroui_base::input::{
8    FocusManager, InputElementImpl, InputElementImplExt, Key, KeyEventArgs, KeyModifiers, KeyboardNavigation, KeyboardNavigationHandler,
9    KeyboardNavigationMode, NavigationDirection, NavigationMethod,
10};
11use ferroui_base::interactivity::InteractiveImpl;
12use ferroui_base::layout::LayoutableImpl;
13use ferroui_base::utilities::DateTime;
14use ferroui_base::{
15    ferro_class, ferro_class_info, ferro_impl_classes, ferro_properties, instantiate, FerroObjectImpl, FerroObjectImplExt, FerroProperty,
16    FerroPropertyChangedEventArgs, Ref, StyledElementImpl, StyledProperty, VisualImpl,
17};
18use std::cell::RefCell;
19
20const PICKER_CONTAINER_NAME: &str = "PART_PickerContainer";
21const ACCEPT_BUTTON_NAME: &str = "PART_AcceptButton";
22const DISMISS_BUTTON_NAME: &str = "PART_DismissButton";
23const SECOND_SPACER_NAME: &str = "PART_SecondSpacer";
24const THIRD_SPACER_NAME: &str = "PART_ThirdSpacer";
25const SECOND_HOST_NAME: &str = "PART_SecondHost";
26const PERIOD_HOST_NAME: &str = "PART_PeriodHost";
27const HOUR_SELECTOR_NAME: &str = "PART_HourSelector";
28const MINUTE_SELECTOR_NAME: &str = "PART_MinuteSelector";
29const SECOND_SELECTOR_NAME: &str = "PART_SecondSelector";
30const PERIOD_SELECTOR_NAME: &str = "PART_PeriodSelector";
31const HOUR_UP_BUTTON_NAME: &str = "PART_HourUpButton";
32const MINUTE_UP_BUTTON_NAME: &str = "PART_MinuteUpButton";
33const SECOND_UP_BUTTON_NAME: &str = "PART_SecondUpButton";
34const PERIOD_UP_BUTTON_NAME: &str = "PART_PeriodUpButton";
35const HOUR_DOWN_BUTTON_NAME: &str = "PART_HourDownButton";
36const MINUTE_DOWN_BUTTON_NAME: &str = "PART_MinuteDownButton";
37const SECOND_DOWN_BUTTON_NAME: &str = "PART_SecondDownButton";
38const PERIOD_DOWN_BUTTON_NAME: &str = "PART_PeriodDownButton";
39
40/// The parts of the control template.
41#[derive(Clone)]
42#[allow(dead_code)]
43struct TemplateItems {
44    picker_container: Ref<Grid>,
45    accept_button: Ref<Button>,
46    dismiss_button: Option<Ref<Button>>,
47    /// The 2nd spacer, not seconds of time.
48    second_spacer: Ref<Control>,
49    third_spacer: Option<Ref<Control>>,
50    second_host: Option<Ref<Panel>>,
51    period_host: Ref<Panel>,
52    hour_selector: Ref<DateTimePickerPanel>,
53    minute_selector: Ref<DateTimePickerPanel>,
54    second_selector: Option<Ref<DateTimePickerPanel>>,
55    period_selector: Ref<DateTimePickerPanel>,
56    hour_up_button: Option<Ref<Button>>,
57    minute_up_button: Option<Ref<Button>>,
58    second_up_button: Option<Ref<Button>>,
59    period_up_button: Option<Ref<Button>>,
60    hour_down_button: Option<Ref<Button>>,
61    minute_down_button: Option<Ref<Button>>,
62    second_down_button: Option<Ref<Button>>,
63    period_down_button: Option<Ref<Button>>,
64}
65
66/// Defines the presenter used for selecting a time. Intended for use with
67/// [`TimePicker`] but can be used independently.
68#[repr(C)]
69pub struct TimePickerPresenter {
70    base: PickerPresenterBase,
71    template_items: RefCell<Option<TemplateItems>>,
72}
73
74ferro_class!(TimePickerPresenter: PickerPresenterBase);
75
76ferro_class_info!(TimePickerPresenter {
77    new: TimePickerPresenter::new,
78    markup: {
79        attributes: [
80            TemplatePart("PART_AcceptButton", type(Ref<Button>), IsRequired = true),
81            TemplatePart("PART_DismissButton", type(Ref<Button>)),
82            TemplatePart("PART_HourDownButton", type(Ref<Button>)),
83            TemplatePart("PART_HourSelector", type(Ref<DateTimePickerPanel>), IsRequired = true),
84            TemplatePart("PART_HourUpButton", type(Ref<Button>)),
85            TemplatePart("PART_MinuteDownButton", type(Ref<Button>)),
86            TemplatePart("PART_MinuteSelector", type(Ref<DateTimePickerPanel>), IsRequired = true),
87            TemplatePart("PART_MinuteUpButton", type(Ref<Button>)),
88            TemplatePart("PART_SecondDownButton", type(Ref<Button>)),
89            TemplatePart("PART_SecondHost", type(Ref<Panel>)),
90            TemplatePart("PART_SecondSelector", type(Ref<DateTimePickerPanel>)),
91            TemplatePart("PART_SecondUpButton", type(Ref<Button>)),
92            TemplatePart("PART_PeriodDownButton", type(Ref<Button>)),
93            TemplatePart("PART_PeriodHost", type(Ref<Panel>), IsRequired = true),
94            TemplatePart("PART_PeriodSelector", type(Ref<DateTimePickerPanel>), IsRequired = true),
95            TemplatePart("PART_PeriodUpButton", type(Ref<Button>)),
96            TemplatePart("PART_PickerContainer", type(Ref<Grid>), IsRequired = true),
97            TemplatePart("PART_SecondSpacer", type(Ref<Control>), IsRequired = true),
98            TemplatePart("PART_ThirdSpacer", type(Ref<Control>)),
99        ],
100    },
101});
102
103ferro_impl_classes!(TimePickerPresenter: StyledElementImpl, VisualImpl, LayoutableImpl, InteractiveImpl, ControlImpl);
104
105impl FerroObjectImpl for TimePickerPresenter {
106    fn constructed(this: &Self) {
107        Self::parent_constructed(this);
108
109        this.set_current_value(Self::time_property(), DateTime::now().time_of_day());
110    }
111
112    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
113        Self::parent_on_property_changed(this, change);
114
115        let property = change.property();
116        if property == Self::minute_increment_property().as_property()
117            || property == Self::second_increment_property().as_property()
118            || property == Self::clock_identifier_property().as_property()
119            || property == Self::use_seconds_property().as_property()
120            || property == Self::time_property().as_property()
121        {
122            this.init_picker();
123        }
124    }
125}
126
127impl TemplatedControlImpl for TimePickerPresenter {
128    fn on_apply_template(this: &Self, e: &TemplateAppliedEventArgs) {
129        Self::parent_on_apply_template(this, e);
130
131        let name_scope = e.name_scope();
132        let weak_this = this.to_ref().downgrade();
133        let selector_button = |name: &str, type_: DateTimePickerPanelType, direction: SpinDirection| {
134            let button = name_scope.find_as::<Button>(name)?;
135            let weak = weak_this.clone();
136            button.click(move |_, _| {
137                if let Some(this) = weak.upgrade() {
138                    this.on_selector_button_click(type_, direction);
139                }
140            });
141            Some(button)
142        };
143
144        let items = TemplateItems {
145            picker_container: name_scope.get_as::<Grid>(PICKER_CONTAINER_NAME),
146            period_host: name_scope.get_as::<Panel>(PERIOD_HOST_NAME),
147            second_host: name_scope.find_as::<Panel>(SECOND_HOST_NAME),
148
149            hour_selector: name_scope.get_as::<DateTimePickerPanel>(HOUR_SELECTOR_NAME),
150            minute_selector: name_scope.get_as::<DateTimePickerPanel>(MINUTE_SELECTOR_NAME),
151            second_selector: name_scope.find_as::<DateTimePickerPanel>(SECOND_SELECTOR_NAME),
152            period_selector: name_scope.get_as::<DateTimePickerPanel>(PERIOD_SELECTOR_NAME),
153
154            second_spacer: name_scope.get_as::<Control>(SECOND_SPACER_NAME),
155            third_spacer: name_scope.find_as::<Control>(THIRD_SPACER_NAME),
156
157            accept_button: name_scope.get_as::<Button>(ACCEPT_BUTTON_NAME),
158
159            hour_up_button: selector_button(HOUR_UP_BUTTON_NAME, DateTimePickerPanelType::Hour, SpinDirection::Decrease),
160            hour_down_button: selector_button(HOUR_DOWN_BUTTON_NAME, DateTimePickerPanelType::Hour, SpinDirection::Increase),
161
162            minute_up_button: selector_button(
163                MINUTE_UP_BUTTON_NAME,
164                DateTimePickerPanelType::Minute,
165                SpinDirection::Decrease,
166            ),
167            minute_down_button: selector_button(
168                MINUTE_DOWN_BUTTON_NAME,
169                DateTimePickerPanelType::Minute,
170                SpinDirection::Increase,
171            ),
172
173            second_up_button: selector_button(
174                SECOND_UP_BUTTON_NAME,
175                DateTimePickerPanelType::Second,
176                SpinDirection::Decrease,
177            ),
178            second_down_button: selector_button(
179                SECOND_DOWN_BUTTON_NAME,
180                DateTimePickerPanelType::Second,
181                SpinDirection::Increase,
182            ),
183
184            period_up_button: selector_button(
185                PERIOD_UP_BUTTON_NAME,
186                DateTimePickerPanelType::TimePeriod,
187                SpinDirection::Decrease,
188            ),
189            period_down_button: selector_button(
190                PERIOD_DOWN_BUTTON_NAME,
191                DateTimePickerPanelType::TimePeriod,
192                SpinDirection::Increase,
193            ),
194
195            dismiss_button: name_scope.find_as::<Button>(DISMISS_BUTTON_NAME),
196        };
197        *this.template_items.borrow_mut() = Some(items.clone());
198
199        let weak = weak_this.clone();
200        items.accept_button.click(move |_, _| {
201            if let Some(this) = weak.upgrade() {
202                this.on_accept_button_clicked();
203            }
204        });
205        if let Some(dismiss_button) = &items.dismiss_button {
206            let weak = weak_this.clone();
207            dismiss_button.click(move |_, _| {
208                if let Some(this) = weak.upgrade() {
209                    this.on_dismiss_button_clicked();
210                }
211            });
212        }
213
214        this.init_picker();
215    }
216}
217
218impl InputElementImpl for TimePickerPresenter {
219    fn on_key_down(this: &Self, e: &KeyEventArgs) {
220        match e.key {
221            Key::Escape => {
222                this.on_dismiss();
223                e.set_handled(true);
224            }
225            Key::Tab => {
226                let focus_manager = FocusManager::get_focus_manager(this);
227                if let Some(focus) = focus_manager.and_then(|focus_manager| focus_manager.get_focused_element()) {
228                    let next_focus = KeyboardNavigationHandler::get_next(&focus, NavigationDirection::Next);
229                    if let Some(next_focus) = next_focus {
230                        next_focus.focus_with(NavigationMethod::Tab, KeyModifiers::NONE);
231                    }
232                    e.set_handled(true);
233                }
234            }
235            Key::Enter => {
236                this.on_confirmed();
237                e.set_handled(true);
238            }
239            _ => {}
240        }
241        Self::parent_on_key_down(this, e);
242    }
243}
244
245impl PickerPresenterBaseImpl for TimePickerPresenter {
246    fn on_confirmed(this: &Self) {
247        if let Some(items) = this.items() {
248            let mut hr = items.hour_selector.selected_value();
249            let min = items.minute_selector.selected_value();
250            let sec = items.second_selector.as_ref().map_or(0, |selector| selector.selected_value());
251            let per = items.period_selector.selected_value();
252
253            if this.clock_identifier() == "12HourClock" {
254                hr = if per == 1 {
255                    if hr == 12 {
256                        12
257                    } else {
258                        hr + 12
259                    }
260                } else if per == 0 && hr == 12 {
261                    0
262                } else {
263                    hr
264                };
265            }
266
267            this.set_current_value(
268                Self::time_property(),
269                TimeSpan::from_hms(hr, min, if this.use_seconds() { sec } else { 0 }),
270            );
271        }
272        Self::parent_on_confirmed(this);
273    }
274}
275
276ferro_properties! {
277    impl TimePickerPresenter {
278        /// Defines the `MinuteIncrement` property.
279        pub fn minute_increment_property() -> StyledProperty<i32> {
280            TimePicker::minute_increment_property().add_owner::<TimePickerPresenter>()
281        }
282
283        /// Defines the `SecondIncrement` property.
284        pub fn second_increment_property() -> StyledProperty<i32> {
285            TimePicker::second_increment_property().add_owner::<TimePickerPresenter>()
286        }
287
288        /// Defines the `ClockIdentifier` property.
289        pub fn clock_identifier_property() -> StyledProperty<String> {
290            TimePicker::clock_identifier_property().add_owner::<TimePickerPresenter>()
291        }
292
293        /// Defines the `UseSeconds` property.
294        pub fn use_seconds_property() -> StyledProperty<bool> {
295            TimePicker::use_seconds_property().add_owner::<TimePickerPresenter>()
296        }
297
298        /// Defines the `Time` property.
299        pub fn time_property() -> StyledProperty<TimeSpan> {
300            FerroProperty::register::<TimePickerPresenter, _>("Time", TimeSpan::default())
301        }
302    }
303}
304
305impl TimePickerPresenter {
306    fn static_constructor() {
307        KeyboardNavigation::tab_navigation_property()
308            .override_default_value::<TimePickerPresenter>(KeyboardNavigationMode::Cycle);
309    }
310
311    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
312    pub fn construct() -> Self {
313        Self { base: PickerPresenterBase::construct(), template_items: RefCell::new(None) }
314    }
315
316    pub fn new() -> Ref<Self> {
317        instantiate(Self::construct())
318    }
319
320    /// The minute increment in the selector.
321    pub fn minute_increment(&self) -> i32 {
322        self.get_value(Self::minute_increment_property())
323    }
324
325    pub fn set_minute_increment(&self, value: i32) {
326        self.set_value(Self::minute_increment_property(), value)
327    }
328
329    /// The second increment in the selector.
330    pub fn second_increment(&self) -> i32 {
331        self.get_value(Self::second_increment_property())
332    }
333
334    pub fn set_second_increment(&self, value: i32) {
335        self.set_value(Self::second_increment_property(), value)
336    }
337
338    /// The current clock identifier, either `12HourClock` or `24HourClock`.
339    pub fn clock_identifier(&self) -> String {
340        self.get_value(Self::clock_identifier_property())
341    }
342
343    pub fn set_clock_identifier(&self, value: &str) {
344        self.set_value(Self::clock_identifier_property(), value.to_string())
345    }
346
347    /// Whether the seconds are selected too.
348    pub fn use_seconds(&self) -> bool {
349        self.get_value(Self::use_seconds_property())
350    }
351
352    pub fn set_use_seconds(&self, value: bool) {
353        self.set_value(Self::use_seconds_property(), value)
354    }
355
356    /// The current time.
357    pub fn time(&self) -> TimeSpan {
358        self.get_value(Self::time_property())
359    }
360
361    pub fn set_time(&self, value: TimeSpan) {
362        self.set_value(Self::time_property(), value)
363    }
364
365    fn items(&self) -> Option<TemplateItems> {
366        self.template_items.borrow().clone()
367    }
368
369    fn init_picker(&self) {
370        let Some(items) = self.items() else {
371            return;
372        };
373
374        let clock12 = self.clock_identifier() == "12HourClock";
375        items.hour_selector.set_maximum_value(if clock12 { 12 } else { 23 });
376        items.hour_selector.set_minimum_value(if clock12 { 1 } else { 0 });
377        items.hour_selector.set_item_format("%h");
378        let hr = self.time().hours();
379        items.hour_selector.set_selected_value(if !clock12 {
380            hr
381        } else if hr > 12 {
382            hr - 12
383        } else if hr == 0 {
384            12
385        } else {
386            hr
387        });
388
389        items.minute_selector.set_maximum_value(59);
390        items.minute_selector.set_minimum_value(0);
391        items.minute_selector.set_increment(self.minute_increment());
392        items.minute_selector.set_item_format("mm");
393        items.minute_selector.set_selected_value(self.time().minutes());
394
395        if let Some(second_selector) = &items.second_selector {
396            second_selector.set_maximum_value(59);
397            second_selector.set_minimum_value(0);
398            second_selector.set_increment(self.second_increment());
399            second_selector.set_item_format("ss");
400            second_selector.set_selected_value(self.time().seconds());
401        }
402
403        items.period_selector.set_maximum_value(1);
404        items.period_selector.set_minimum_value(0);
405        items.period_selector.set_selected_value(if hr >= 12 { 1 } else { 0 });
406
407        self.set_grid(&items);
408        items.hour_selector.focus_with(NavigationMethod::Pointer, KeyModifiers::NONE);
409    }
410
411    fn set_grid(&self, items: &TemplateItems) {
412        let use_24_hour_clock = self.clock_identifier() == "24HourClock";
413        let use_seconds = self.use_seconds();
414
415        let columns_d = ColumnDefinitions::new();
416        columns_d.add(ColumnDefinition::with_width(GridLength::STAR));
417        columns_d.add(ColumnDefinition::with_width(GridLength::AUTO));
418        columns_d.add(ColumnDefinition::with_width(GridLength::STAR));
419
420        if let (Some(second_host), Some(third_spacer)) = (&items.second_host, &items.third_spacer) {
421            if use_seconds {
422                columns_d.add(ColumnDefinition::with_width(GridLength::AUTO));
423                columns_d.add(ColumnDefinition::with_width(GridLength::STAR));
424            }
425
426            items.second_spacer.set_is_visible(use_seconds);
427            second_host.set_is_visible(use_seconds);
428            third_spacer.set_is_visible(!use_24_hour_clock);
429            items.period_host.set_is_visible(!use_24_hour_clock);
430
431            let am_pm_column = if use_seconds { 6 } else { 4 };
432
433            Grid::set_column(&items.second_spacer, if use_seconds { 3 } else { 0 });
434            Grid::set_column(second_host, if use_seconds { 4 } else { 0 });
435            Grid::set_column(third_spacer, if use_24_hour_clock { 0 } else { am_pm_column - 1 });
436            Grid::set_column(&items.period_host, if use_24_hour_clock { 0 } else { am_pm_column });
437        } else {
438            items.second_spacer.set_is_visible(!use_24_hour_clock);
439            items.period_host.set_is_visible(!use_24_hour_clock);
440            Grid::set_column(&items.second_spacer, if use_24_hour_clock { 0 } else { 3 });
441            Grid::set_column(&items.period_host, if use_24_hour_clock { 0 } else { 4 });
442        }
443
444        if !use_24_hour_clock {
445            columns_d.add(ColumnDefinition::with_width(GridLength::AUTO));
446            columns_d.add(ColumnDefinition::with_width(GridLength::STAR));
447        }
448
449        items.picker_container.set_column_definitions(columns_d);
450    }
451
452    fn on_dismiss_button_clicked(&self) {
453        self.on_dismiss();
454    }
455
456    fn on_accept_button_clicked(&self) {
457        self.on_confirmed();
458    }
459
460    fn on_selector_button_click(&self, type_: DateTimePickerPanelType, direction: SpinDirection) {
461        let items = self.items();
462        let target = match type_ {
463            DateTimePickerPanelType::Hour => items.map(|items| items.hour_selector),
464            DateTimePickerPanelType::Minute => items.map(|items| items.minute_selector),
465            DateTimePickerPanelType::Second => items.and_then(|items| items.second_selector),
466            DateTimePickerPanelType::TimePeriod => items.map(|items| items.period_selector),
467            _ => panic!("The method or operation is not implemented."),
468        };
469
470        let Some(target) = target else {
471            return;
472        };
473
474        match direction {
475            SpinDirection::Increase => target.scroll_down(1),
476            SpinDirection::Decrease => target.scroll_up(1),
477        }
478    }
479
480    pub(crate) fn get_offset_for_popup(&self) -> f64 {
481        let Some(items) = self.items() else {
482            return 0.0;
483        };
484
485        let accept_dismiss_button_height = items.accept_button.bounds().height;
486        -(self.max_height() - accept_dismiss_button_height) / 2.0 - (items.hour_selector.item_height() / 2.0)
487    }
488}