1use super::{PickerPresenterBase, TimePickerPresenter, TimePickerSelectedValueChangedEventArgs};
2use crate::primitives::popup_positioning::{PopupAnchor, PopupGravity, PopupPositionerConstraintAdjustment};
3use crate::primitives::{Popup, TemplateAppliedEventArgs, TemplatedControl, TemplatedControlImpl, TemplatedControlImplExt};
4use crate::shapes::Rectangle;
5use crate::utils::TimeUtils;
6use crate::{
7 Border, Button, ColumnDefinition, ColumnDefinitions, ContentControl, ControlImpl, Grid, GridLength, PlacementMode,
8 TextBlock,
9};
10use ferroui_base::animation::TimeSpan;
11use ferroui_base::data::BindingMode;
12use ferroui_base::input::InputElementImpl;
13use ferroui_base::interactivity::{InteractiveImpl, RoutedEventHandlerToken};
14use ferroui_base::layout::{LayoutableImpl, VerticalAlignment};
15use ferroui_base::reactive::{Disposable, IDisposable};
16use ferroui_base::utilities::{CultureInfo, DateTime, HandlerList};
17use ferroui_base::{
18 ferro_class, ferro_class_info, ferro_impl_classes, ferro_properties, instantiate, FerroObject, FerroObjectImpl, FerroObjectImplExt,
19 FerroProperty, FerroPropertyChangedEventArgs, Ref, StyledElementImpl, StyledProperty, StyledPropertyOptions,
20 VisualImpl,
21};
22use std::cell::{Cell, RefCell};
23use std::rc::Rc;
24
25const PC_HAS_NO_TIME: &str = ":hasnotime";
26
27#[repr(C)]
29pub struct TimePicker {
30 base: TemplatedControl,
31
32 presenter: RefCell<Option<Ref<TimePickerPresenter>>>,
34 flyout_button: RefCell<Option<Ref<Button>>>,
35 first_picker_host: RefCell<Option<Ref<Border>>>,
36 second_picker_host: RefCell<Option<Ref<Border>>>,
37 third_picker_host: RefCell<Option<Ref<Border>>>,
38 fourth_picker_host: RefCell<Option<Ref<Border>>>,
39 hour_text: RefCell<Option<Ref<TextBlock>>>,
40 minute_text: RefCell<Option<Ref<TextBlock>>>,
41 second_text: RefCell<Option<Ref<TextBlock>>>,
42 period_text: RefCell<Option<Ref<TextBlock>>>,
43 first_splitter: RefCell<Option<Ref<Rectangle>>>,
44 second_splitter: RefCell<Option<Ref<Rectangle>>>,
45 third_splitter: RefCell<Option<Ref<Rectangle>>>,
46 content_grid: RefCell<Option<Ref<Grid>>>,
47 popup: RefCell<Option<Ref<Popup>>>,
48
49 flyout_button_click: Cell<Option<RoutedEventHandlerToken>>,
51 presenter_subscriptions: RefCell<Option<(Rc<dyn IDisposable>, Rc<dyn IDisposable>)>>,
54
55 selected_time_changed: HandlerList<dyn Fn(&TimePickerSelectedValueChangedEventArgs)>,
56}
57
58ferro_class! {
59 TimePicker: TemplatedControl, virtuals TimePickerImpl: TemplatedControlImpl {
60 fn on_selected_time_changed(this, old_time: Option<TimeSpan>, new_time: Option<TimeSpan>);
62 }
63}
64
65ferro_class_info!(TimePicker {
66 new: TimePicker::new,
67 markup: {
68 attributes: [
69 TemplatePart("PART_FirstColumnDivider", type(Ref<Rectangle>)),
70 TemplatePart("PART_FirstPickerHost", type(Ref<Border>)),
71 TemplatePart("PART_FlyoutButton", type(Ref<Button>)),
72 TemplatePart("PART_FlyoutButtonContentGrid", type(Ref<Grid>)),
73 TemplatePart("PART_HourTextBlock", type(Ref<TextBlock>)),
74 TemplatePart("PART_MinuteTextBlock", type(Ref<TextBlock>)),
75 TemplatePart("PART_SecondTextBlock", type(Ref<TextBlock>)),
76 TemplatePart("PART_PeriodTextBlock", type(Ref<TextBlock>)),
77 TemplatePart("PART_PickerPresenter", type(Ref<TimePickerPresenter>)),
78 TemplatePart("PART_Popup", type(Ref<Popup>)),
79 TemplatePart("PART_SecondColumnDivider", type(Ref<Rectangle>)),
80 TemplatePart("PART_SecondPickerHost", type(Ref<Border>)),
81 TemplatePart("PART_ThirdColumnDivider", type(Ref<Rectangle>)),
82 TemplatePart("PART_ThirdPickerHost", type(Ref<Border>)),
83 TemplatePart("PART_FourthPickerHost", type(Ref<Border>)),
84 PseudoClasses(":hasnotime"),
85 ],
86 },
87});
88
89ferro_impl_classes!(
90 TimePicker: StyledElementImpl,
91 VisualImpl,
92 LayoutableImpl,
93 InteractiveImpl,
94 InputElementImpl
95);
96
97impl FerroObjectImpl for TimePicker {
98 fn constructed(this: &Self) {
99 Self::parent_constructed(this);
100
101 this.pseudo_classes().set(PC_HAS_NO_TIME, true);
102
103 let date_time_format = CultureInfo::current_culture().date_time_format();
104 let time_pattern = date_time_format.short_time_pattern();
105 if time_pattern.contains('H') {
106 this.set_current_value(Self::clock_identifier_property(), "24HourClock".to_string());
107 }
108 }
109
110 fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
111 Self::parent_on_property_changed(this, change);
112
113 let property = change.property();
114 if property == Self::minute_increment_property().as_property() {
115 this.set_selected_time_text();
116 } else if property == Self::second_increment_property().as_property() {
117 this.set_selected_time_text();
118 } else if property == Self::clock_identifier_property().as_property() {
119 this.set_grid();
120 this.set_selected_time_text();
121 } else if property == Self::use_seconds_property().as_property() {
122 this.set_grid();
123 this.set_selected_time_text();
124 } else if property == Self::selected_time_property().as_property() {
125 let (old_value, new_value) = change.get_old_and_new_value::<Option<TimeSpan>>();
126 this.on_selected_time_changed(old_value, new_value);
127 this.set_selected_time_text();
128 }
129 }
130}
131
132impl TemplatedControlImpl for TimePicker {
133 fn on_apply_template(this: &Self, e: &TemplateAppliedEventArgs) {
134 let flyout_button = this.flyout_button.borrow().clone();
135 if let (Some(flyout_button), Some(token)) = (flyout_button, this.flyout_button_click.take()) {
136 flyout_button.remove_handler(Button::click_event(), token);
137 }
138
139 if let Some((confirmed, dismissed)) = this.presenter_subscriptions.take() {
140 confirmed.dispose();
141 dismissed.dispose();
142 }
143 Self::parent_on_apply_template(this, e);
144
145 let name_scope = e.name_scope();
146 let flyout_button = name_scope.find_as::<Button>("PART_FlyoutButton");
147 *this.flyout_button.borrow_mut() = flyout_button.clone();
148
149 *this.first_picker_host.borrow_mut() = name_scope.find_as::<Border>("PART_FirstPickerHost");
150 *this.second_picker_host.borrow_mut() = name_scope.find_as::<Border>("PART_SecondPickerHost");
151 *this.third_picker_host.borrow_mut() = name_scope.find_as::<Border>("PART_ThirdPickerHost");
152 *this.fourth_picker_host.borrow_mut() = name_scope.find_as::<Border>("PART_FourthPickerHost");
153
154 *this.hour_text.borrow_mut() = name_scope.find_as::<TextBlock>("PART_HourTextBlock");
155 *this.minute_text.borrow_mut() = name_scope.find_as::<TextBlock>("PART_MinuteTextBlock");
156 *this.second_text.borrow_mut() = name_scope.find_as::<TextBlock>("PART_SecondTextBlock");
157 *this.period_text.borrow_mut() = name_scope.find_as::<TextBlock>("PART_PeriodTextBlock");
158
159 *this.first_splitter.borrow_mut() = name_scope.find_as::<Rectangle>("PART_FirstColumnDivider");
160 *this.second_splitter.borrow_mut() = name_scope.find_as::<Rectangle>("PART_SecondColumnDivider");
161 *this.third_splitter.borrow_mut() = name_scope.find_as::<Rectangle>("PART_ThirdColumnDivider");
162
163 *this.content_grid.borrow_mut() = name_scope.find_as::<Grid>("PART_FlyoutButtonContentGrid");
164
165 *this.popup.borrow_mut() = name_scope.find_as::<Popup>("PART_Popup");
166 let presenter = name_scope.find_as::<TimePickerPresenter>("PART_PickerPresenter");
167 *this.presenter.borrow_mut() = presenter.clone();
168
169 if let Some(flyout_button) = flyout_button {
170 let weak = this.to_ref().downgrade();
171 let token = flyout_button.click(move |_, _| {
172 if let Some(this) = weak.upgrade() {
173 this.on_flyout_button_clicked();
174 }
175 });
176 this.flyout_button_click.set(Some(token));
177 }
178
179 this.set_grid();
180 this.set_selected_time_text();
181
182 if let Some(presenter) = presenter {
183 let weak = this.to_ref().downgrade();
184 let confirmed = PickerPresenterBase::confirmed(&presenter, move || {
185 if let Some(this) = weak.upgrade() {
186 this.on_confirmed();
187 }
188 });
189 let weak = this.to_ref().downgrade();
190 let dismissed = PickerPresenterBase::dismissed(&presenter, move || {
191 if let Some(this) = weak.upgrade() {
192 this.on_dismiss_picker();
193 }
194 });
195 *this.presenter_subscriptions.borrow_mut() = Some((confirmed, dismissed));
196
197 let bind = |target: &'static FerroProperty, source: &'static FerroProperty| {
198 presenter.bind_indexer(&target.bind(), &this.indexer(&source.bind()));
199 };
200
201 bind(
202 TimePickerPresenter::minute_increment_property().as_property(),
203 Self::minute_increment_property().as_property(),
204 );
205 bind(
206 TimePickerPresenter::second_increment_property().as_property(),
207 Self::second_increment_property().as_property(),
208 );
209 bind(
210 TimePickerPresenter::clock_identifier_property().as_property(),
211 Self::clock_identifier_property().as_property(),
212 );
213 bind(TimePickerPresenter::use_seconds_property().as_property(), Self::use_seconds_property().as_property());
214 }
215 }
216}
217
218impl ControlImpl for TimePicker {
219 fn on_create_automation_peer(this: &Self) -> Ref<crate::automation::peers::AutomationPeer> {
220 crate::automation::peers::TimePickerAutomationPeer::new(this).upcast()
221 }
222}
223
224impl TimePickerImpl for TimePicker {
225 fn on_selected_time_changed(this: &Self, old_time: Option<TimeSpan>, new_time: Option<TimeSpan>) {
226 let e = TimePickerSelectedValueChangedEventArgs::new(old_time, new_time);
227 for (_, handler) in this.selected_time_changed.snapshot().iter() {
228 handler(&e);
229 }
230 }
231}
232
233ferro_properties! {
234 impl TimePicker {
235 pub fn minute_increment_property() -> StyledProperty<i32> {
237 FerroProperty::register_with::<TimePicker, _>(
238 "MinuteIncrement",
239 StyledPropertyOptions::new(1).coerce(TimePicker::coerce_minute_increment),
240 )
241 }
242
243 pub fn second_increment_property() -> StyledProperty<i32> {
245 FerroProperty::register_with::<TimePicker, _>(
246 "SecondIncrement",
247 StyledPropertyOptions::new(1).coerce(TimePicker::coerce_second_increment),
248 )
249 }
250
251 pub fn clock_identifier_property() -> StyledProperty<String> {
253 FerroProperty::register_with::<TimePicker, _>(
254 "ClockIdentifier",
255 StyledPropertyOptions::new("12HourClock".to_string()).coerce(TimePicker::coerce_clock_identifier),
256 )
257 }
258
259 pub fn vertical_content_alignment_property() -> StyledProperty<VerticalAlignment> {
261 ContentControl::vertical_content_alignment_property().add_owner::<TimePicker>()
262 }
263
264 pub fn use_seconds_property() -> StyledProperty<bool> {
266 FerroProperty::register_with::<TimePicker, _>(
267 "UseSeconds",
268 StyledPropertyOptions::new(false).coerce(TimePicker::coerce_use_seconds),
269 )
270 }
271
272 pub fn selected_time_property() -> StyledProperty<Option<TimeSpan>> {
274 FerroProperty::register_with::<TimePicker, _>(
275 "SelectedTime",
276 StyledPropertyOptions::new(None)
277 .default_binding_mode(BindingMode::TwoWay)
278 .enable_data_validation(true),
279 )
280 }
281 }
282}
283
284impl TimePicker {
285 pub fn construct() -> Self {
287 Self {
288 base: TemplatedControl::construct(),
289 presenter: RefCell::new(None),
290 flyout_button: RefCell::new(None),
291 first_picker_host: RefCell::new(None),
292 second_picker_host: RefCell::new(None),
293 third_picker_host: RefCell::new(None),
294 fourth_picker_host: RefCell::new(None),
295 hour_text: RefCell::new(None),
296 minute_text: RefCell::new(None),
297 second_text: RefCell::new(None),
298 period_text: RefCell::new(None),
299 first_splitter: RefCell::new(None),
300 second_splitter: RefCell::new(None),
301 third_splitter: RefCell::new(None),
302 content_grid: RefCell::new(None),
303 popup: RefCell::new(None),
304 flyout_button_click: Cell::new(None),
305 presenter_subscriptions: RefCell::new(None),
306 selected_time_changed: HandlerList::new(),
307 }
308 }
309
310 pub fn new() -> Ref<Self> {
311 instantiate(Self::construct())
312 }
313
314 pub fn vertical_content_alignment(&self) -> VerticalAlignment {
316 self.get_value(Self::vertical_content_alignment_property())
317 }
318
319 pub fn set_vertical_content_alignment(&self, value: VerticalAlignment) {
320 self.set_value(Self::vertical_content_alignment_property(), value)
321 }
322
323 pub fn minute_increment(&self) -> i32 {
325 self.get_value(Self::minute_increment_property())
326 }
327
328 pub fn set_minute_increment(&self, value: i32) {
329 self.set_value(Self::minute_increment_property(), value)
330 }
331
332 fn coerce_minute_increment(_sender: &FerroObject, value: i32) -> i32 {
333 if !(1..=59).contains(&value) {
334 panic!("1 >= MinuteIncrement <= 59");
335 }
336
337 value
338 }
339
340 pub fn second_increment(&self) -> i32 {
342 self.get_value(Self::second_increment_property())
343 }
344
345 pub fn set_second_increment(&self, value: i32) {
346 self.set_value(Self::second_increment_property(), value)
347 }
348
349 fn coerce_second_increment(_sender: &FerroObject, value: i32) -> i32 {
350 if !(1..=59).contains(&value) {
351 panic!("1 >= SecondIncrement <= 59");
352 }
353
354 value
355 }
356
357 pub fn clock_identifier(&self) -> String {
359 self.get_value(Self::clock_identifier_property())
360 }
361
362 pub fn set_clock_identifier(&self, value: &str) {
363 self.set_value(Self::clock_identifier_property(), value.to_string())
364 }
365
366 fn coerce_clock_identifier(_sender: &FerroObject, value: String) -> String {
367 if !(value.is_empty() || value == "12HourClock" || value == "24HourClock") {
368 panic!("Invalid ClockIdentifier");
369 }
370
371 value
372 }
373
374 pub fn use_seconds(&self) -> bool {
376 self.get_value(Self::use_seconds_property())
377 }
378
379 pub fn set_use_seconds(&self, value: bool) {
380 self.set_value(Self::use_seconds_property(), value)
381 }
382
383 fn coerce_use_seconds(_sender: &FerroObject, value: bool) -> bool {
384 value
387 }
388
389 pub fn selected_time(&self) -> Option<TimeSpan> {
391 self.get_value(Self::selected_time_property())
392 }
393
394 pub fn set_selected_time(&self, value: Option<TimeSpan>) {
395 self.set_value(Self::selected_time_property(), value)
396 }
397
398 pub fn selected_time_changed(
401 &self,
402 handler: impl Fn(&TimePickerSelectedValueChangedEventArgs) + 'static,
403 ) -> Rc<dyn IDisposable> {
404 let token = self.selected_time_changed.add(Rc::new(handler));
405 let weak = self.to_ref().downgrade();
406 Disposable::create(move || {
407 if let Some(this) = weak.upgrade() {
408 this.selected_time_changed.remove(token);
409 }
410 })
411 }
412
413 fn set_grid(&self) {
414 let Some(content_grid) = self.content_grid.borrow().clone() else {
415 return;
416 };
417
418 let first_picker_host = self.first_picker_host.borrow().clone();
419 let second_picker_host = self.second_picker_host.borrow().clone();
420 let third_picker_host = self.third_picker_host.borrow().clone();
421 let fourth_picker_host = self.fourth_picker_host.borrow().clone();
422 let first_splitter = self.first_splitter.borrow().clone();
423 let second_splitter = self.second_splitter.borrow().clone();
424 let third_splitter = self.third_splitter.borrow().clone();
425
426 let use_24_hour_clock = self.clock_identifier() == "24HourClock";
427 let use_seconds = self.use_seconds();
428 let seconds_parts = match (self.second_text.borrow().is_some(), &fourth_picker_host, &third_splitter) {
429 (true, Some(fourth_picker_host), Some(third_splitter)) => Some((fourth_picker_host, third_splitter)),
430 _ => None,
431 };
432 let can_use_seconds = seconds_parts.is_some();
433
434 let columns_d = ColumnDefinitions::new();
435 columns_d.add(ColumnDefinition::with_width(GridLength::STAR));
436 columns_d.add(ColumnDefinition::with_width(GridLength::AUTO));
437 columns_d.add(ColumnDefinition::with_width(GridLength::STAR));
438 if can_use_seconds && use_seconds {
439 columns_d.add(ColumnDefinition::with_width(GridLength::AUTO));
440 columns_d.add(ColumnDefinition::with_width(GridLength::STAR));
441 }
442 if !use_24_hour_clock {
443 columns_d.add(ColumnDefinition::with_width(GridLength::AUTO));
444 columns_d.add(ColumnDefinition::with_width(GridLength::STAR));
445 }
446
447 content_grid.set_column_definitions(columns_d);
448
449 let third_picker_host = third_picker_host.expect("the template has a third picker host");
450 let second_splitter = second_splitter.expect("the template has a second column divider");
451
452 if let Some((fourth_picker_host, third_splitter)) = seconds_parts {
453 third_picker_host.set_is_visible(use_seconds);
454 second_splitter.set_is_visible(use_seconds);
455 fourth_picker_host.set_is_visible(!use_24_hour_clock);
456 third_splitter.set_is_visible(!use_24_hour_clock);
457 } else {
458 third_picker_host.set_is_visible(!use_24_hour_clock);
459 second_splitter.set_is_visible(!use_24_hour_clock);
460 }
461
462 Grid::set_column(&first_picker_host.expect("the template has a first picker host"), 0);
463 Grid::set_column(&second_picker_host.expect("the template has a second picker host"), 2);
464 let first_splitter = first_splitter.expect("the template has a first column divider");
465
466 if let Some((fourth_picker_host, third_splitter)) = seconds_parts {
467 let am_pm_column = if use_seconds { 6 } else { 4 };
468 Grid::set_column(&third_picker_host, if use_seconds { 4 } else { 0 });
469 Grid::set_column(fourth_picker_host, if use_24_hour_clock { 0 } else { am_pm_column });
470 Grid::set_column(&first_splitter, 1);
471 Grid::set_column(&second_splitter, if use_seconds { 3 } else { 0 });
472 Grid::set_column(third_splitter, if use_24_hour_clock { 0 } else { am_pm_column - 1 });
473 } else {
474 Grid::set_column(&third_picker_host, if use_24_hour_clock { 0 } else { 4 });
475 Grid::set_column(&first_splitter, 1);
476 Grid::set_column(&second_splitter, if use_24_hour_clock { 0 } else { 3 });
477 }
478 }
479
480 fn set_selected_time_text(&self) {
481 let (Some(hour_text), Some(minute_text), Some(period_text)) =
482 (self.hour_text.borrow().clone(), self.minute_text.borrow().clone(), self.period_text.borrow().clone())
483 else {
484 return;
485 };
486 let second_text = self.second_text.borrow().clone();
487
488 if let Some(time) = self.selected_time() {
489 let mut new_time = time;
490
491 if self.clock_identifier() == "12HourClock" {
492 let hr = new_time.hours();
493 let hr = if hr > 12 {
494 hr - 12
495 } else if hr == 0 {
496 12
497 } else {
498 hr
499 };
500 new_time = TimeSpan::from_hms(hr, new_time.minutes(), new_time.seconds());
501 }
502
503 hour_text.set_text(Some(&new_time.to_string_format("%h")));
504 minute_text.set_text(Some(&new_time.to_string_format("mm")));
505 if let Some(second_text) = &second_text {
506 second_text.set_text(Some(&new_time.to_string_format("ss")));
507 }
508
509 self.pseudo_classes().set(PC_HAS_NO_TIME, false);
510
511 period_text.set_text(Some(&if time.hours() >= 12 {
512 TimeUtils::get_pm_designator()
513 } else {
514 TimeUtils::get_am_designator()
515 }));
516 } else {
517 hour_text.clear_value(TextBlock::text_property());
519 minute_text.clear_value(TextBlock::text_property());
520 if let Some(second_text) = &second_text {
521 second_text.clear_value(TextBlock::text_property());
522 }
523
524 self.pseudo_classes().set(PC_HAS_NO_TIME, true);
525
526 period_text.set_text(Some(&if DateTime::now().hour() >= 12 {
527 TimeUtils::get_pm_designator()
528 } else {
529 TimeUtils::get_am_designator()
530 }));
531 }
532 }
533
534 fn on_flyout_button_clicked(&self) {
535 let Some(presenter) = self.presenter.borrow().clone() else {
536 panic!("No DatePickerPresenter found.");
537 };
538 let Some(popup) = self.popup.borrow().clone() else {
539 panic!("No Popup found.");
540 };
541
542 presenter.set_time(self.selected_time().unwrap_or_else(|| DateTime::now().time_of_day()));
543
544 popup.set_placement(PlacementMode::AnchorAndGravity);
545 popup.set_placement_anchor(PopupAnchor::BOTTOM);
546 popup.set_placement_gravity(PopupGravity::BOTTOM);
547 popup.set_placement_constraint_adjustment(PopupPositionerConstraintAdjustment::SLIDE_Y);
548 popup.set_is_open(true);
549
550 if !presenter.is_measure_valid() {
553 if let Some(layout_manager) = self.get_layout_manager() {
554 layout_manager.execute_initial_layout_pass();
555 }
556 }
557
558 let delta_y = presenter.get_offset_for_popup();
559
560 popup.set_vertical_offset(delta_y + 5.0);
562 }
563
564 fn on_dismiss_picker(&self) {
565 let popup = self.popup.borrow().clone().expect("the template has a popup");
566 popup.close();
567 self.focus();
568 }
569
570 fn on_confirmed(&self) {
571 let popup = self.popup.borrow().clone().expect("the template has a popup");
572 popup.close();
573 let presenter = self.presenter.borrow().clone().expect("the template has a presenter");
574 self.set_current_value(Self::selected_time_property(), Some(presenter.time()));
575 }
576
577 pub fn clear(&self) {
579 self.set_current_value(Self::selected_time_property(), None);
580 }
581}