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#[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 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#[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 pub fn minute_increment_property() -> StyledProperty<i32> {
280 TimePicker::minute_increment_property().add_owner::<TimePickerPresenter>()
281 }
282
283 pub fn second_increment_property() -> StyledProperty<i32> {
285 TimePicker::second_increment_property().add_owner::<TimePickerPresenter>()
286 }
287
288 pub fn clock_identifier_property() -> StyledProperty<String> {
290 TimePicker::clock_identifier_property().add_owner::<TimePickerPresenter>()
291 }
292
293 pub fn use_seconds_property() -> StyledProperty<bool> {
295 TimePicker::use_seconds_property().add_owner::<TimePickerPresenter>()
296 }
297
298 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 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 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 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 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 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 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}