1use crate::motion::MaterialMotionScheme;
9use crate::time_picker_dial::MaterialTimePickerDial;
10use crate::time_picker_state::{
11 MaterialTimeDisplayMode, MaterialTimeSelection, TimePickerMetrics, display_hour, parse_input,
12};
13use ferroui_base::animation::TimeSpan;
14use ferroui_base::data::BindingMode;
15use ferroui_base::input::platform::{ClipboardErrorKind, ClipboardExtensions};
16use ferroui_base::input::text_input::{
17 TextInputContentType, TextInputOptions, TextInputReturnKeyType,
18};
19use ferroui_base::input::{
20 FocusChangedEventArgs, InputElement, InputElementImpl, InputElementImplExt, Key, KeyEventArgs,
21 KeyModifiers, KeyboardNavigationHandler, NavigationDirection, NavigationMethod,
22 TextInputEventArgs,
23};
24use ferroui_base::interactivity::{
25 InteractiveImpl, RoutedEvent, RoutedEventArgs, RoutingStrategies,
26};
27use ferroui_base::layout::{LayoutableImpl, Orientation};
28use ferroui_base::media::FlowDirection;
29use ferroui_base::reactive::IDisposable;
30use ferroui_base::threading::Dispatcher;
31use ferroui_base::{
32 DirectProperty, FerroObjectImpl, FerroObjectImplExt, FerroProperty,
33 FerroPropertyChangedEventArgs, Rect, Ref, Size, StyledElementImpl, StyledProperty,
34 StyledPropertyOptions, Visual, VisualImpl, VisualImplExt, VisualTreeAttachmentEventArgs,
35 WeakRef, ferro_class, ferro_class_info, ferro_impl_classes, ferro_properties,
36 ferro_routed_event, ferro_routed_event_args, instantiate,
37};
38use ferroui_controls::automation::peers::{
39 AutomationControlType, AutomationPeer, AutomationPeerImpl, ControlAutomationPeer,
40 ControlAutomationPeerImpl,
41};
42use ferroui_controls::automation::provider::{IValueProvider, ProviderError};
43use ferroui_controls::automation::{
44 AutomationLiveSetting, AutomationProperties, ElementNotEnabledException,
45};
46use ferroui_controls::primitives::{
47 TemplateAppliedEventArgs, TemplatedControl, TemplatedControlImpl, TemplatedControlImplExt,
48 ToggleButton,
49};
50use ferroui_controls::{
51 ControlImpl, Panel, PanelImpl, PastingFromClipboardEventArgs, RadioButton, TextBlock, TextBox,
52 TextBoxImpl, TextBoxImplExt,
53};
54use std::cell::{Cell, RefCell};
55use std::rc::Rc;
56use std::sync::atomic::{AtomicU64, Ordering};
57
58static GROUP_ID: AtomicU64 = AtomicU64::new(1);
59
60#[derive(Clone, Default)]
61pub struct MaterialTimeChangedEventArgs {
62 base: RoutedEventArgs,
63 old_time: TimeSpan,
64 new_time: TimeSpan,
65}
66ferro_routed_event_args!(MaterialTimeChangedEventArgs: RoutedEventArgs);
67impl MaterialTimeChangedEventArgs {
68 pub fn old_time(&self) -> TimeSpan {
69 self.old_time
70 }
71 pub fn new_time(&self) -> TimeSpan {
72 self.new_time
73 }
74}
75
76#[derive(Clone, Default)]
77struct Parts {
78 layout: Option<Ref<MaterialTimePickerLayout>>,
79 dial: Option<Ref<MaterialTimePickerDial>>,
80 hour: Option<Ref<RadioButton>>,
81 minute: Option<Ref<RadioButton>>,
82 am: Option<Ref<RadioButton>>,
83 pm: Option<Ref<RadioButton>>,
84 hour_field: Option<Ref<MaterialTimePickerField>>,
85 minute_field: Option<Ref<MaterialTimePickerField>>,
86 hour_label: Option<Ref<TextBlock>>,
87 minute_label: Option<Ref<TextBlock>>,
88 separator: Option<Ref<TextBlock>>,
89}
90
91#[repr(C)]
95pub struct MaterialTimePicker {
96 base: TemplatedControl,
97 parts: RefCell<Parts>,
98 subscriptions: RefCell<Vec<Rc<dyn IDisposable>>>,
99 synchronizing: Cell<bool>,
100 editing: Cell<bool>,
101 selected_time: Cell<TimeSpan>,
102 hour_valid: Cell<bool>,
103 minute_valid: Cell<bool>,
104 input_valid: Cell<bool>,
105 hour_draft: RefCell<String>,
106 minute_draft: RefCell<String>,
107 group_id: u64,
108}
109ferro_class!(MaterialTimePicker: TemplatedControl);
110ferro_class_info!(MaterialTimePicker {
111 new: MaterialTimePicker::new
112});
113ferro_impl_classes!(MaterialTimePicker: StyledElementImpl, InteractiveImpl, InputElementImpl, LayoutableImpl);
114impl ControlImpl for MaterialTimePicker {
115 fn on_create_automation_peer(this: &Self) -> Ref<AutomationPeer> {
116 MaterialTimePickerAutomationPeer::new(this).upcast()
117 }
118}
119impl FerroObjectImpl for MaterialTimePicker {
120 fn constructed(this: &Self) {
121 Self::parent_constructed(this);
122 this.synchronize(true);
123 }
124 fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
125 Self::parent_on_property_changed(this, change);
126 if this.synchronizing.get() {
127 return;
128 }
129 let p = change.property();
130 let value_changed =
131 p == Self::hour_property().as_property() || p == Self::minute_property().as_property();
132 let reset_draft = value_changed || p == Self::is_24_hour_property().as_property();
133 if value_changed
134 || p == Self::is_24_hour_property().as_property()
135 || p == Self::selection_property().as_property()
136 || p == Self::display_mode_property().as_property()
137 || p == Self::orientation_property().as_property()
138 || p == Self::is_vibrant_property().as_property()
139 || p == Self::is_read_only_property().as_property()
140 || p == Self::auto_advance_property().as_property()
141 || p == Self::reduced_motion_property().as_property()
142 || p == Self::motion_scheme_property().as_property()
143 || p == Self::hour_label_property().as_property()
144 || p == Self::minute_label_property().as_property()
145 || p == Self::am_label_property().as_property()
146 || p == Self::pm_label_property().as_property()
147 || p == Self::hour_error_text_property().as_property()
148 || p == Self::minute_error_text_property().as_property()
149 || p == Visual::flow_direction_property().as_property()
150 || p == InputElement::is_effectively_enabled_property().as_property()
151 {
152 this.synchronize(reset_draft && !this.editing.get());
153 }
154 if value_changed {
155 this.publish_time();
156 }
157 if (p == Self::display_mode_property().as_property()
158 || p == Self::selection_property().as_property())
159 && this.display_mode() == MaterialTimeDisplayMode::Input
160 && this.is_attached_to_visual_tree()
161 && this.can_edit()
162 {
163 this.focus_field(this.selection());
164 }
165 }
166}
167impl VisualImpl for MaterialTimePicker {
168 fn on_attached_to_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
169 Self::parent_on_attached_to_visual_tree(this, e);
170 this.assign_radio_groups();
171 this.synchronize(false);
172 }
173 fn on_detached_from_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
174 if let Some(dial) = this.parts.borrow().dial.clone() {
175 dial.cancel_interaction();
176 }
177 Self::parent_on_detached_from_visual_tree(this, e);
178 }
179}
180impl TemplatedControlImpl for MaterialTimePicker {
181 fn on_apply_template(this: &Self, e: &TemplateAppliedEventArgs) {
182 this.release_parts();
183 Self::parent_on_apply_template(this, e);
184 let scope = e.name_scope();
185 let parts = Parts {
186 layout: scope.find_as("PART_Layout"),
187 dial: scope.find_as("PART_Dial"),
188 hour: scope.find_as("PART_Hour"),
189 minute: scope.find_as("PART_Minute"),
190 am: scope.find_as("PART_Am"),
191 pm: scope.find_as("PART_Pm"),
192 hour_field: scope.find_as("PART_HourField"),
193 minute_field: scope.find_as("PART_MinuteField"),
194 hour_label: scope.find_as("PART_HourLabel"),
195 minute_label: scope.find_as("PART_MinuteLabel"),
196 separator: scope.find_as("PART_Separator"),
197 };
198 let weak = this.to_ref().downgrade();
199 if let Some(layout) = &parts.layout {
200 *layout.owner.borrow_mut() = Some(weak.clone());
201 }
202 if let Some(dial) = &parts.dial {
203 dial.set_owner(Some(weak.clone()));
204 }
205 for (button, role) in [
206 (&parts.hour, 0),
207 (&parts.minute, 1),
208 (&parts.am, 2),
209 (&parts.pm, 3),
210 ] {
211 if let Some(button) = button {
212 if this.is_attached_to_visual_tree() {
215 button.set_group_name(Some(format!(
216 "MaterialTimePicker{}-{}",
217 this.group_id,
218 role / 2
219 )));
220 }
221 let owner = weak.clone();
222 let weak_button = button.downgrade();
223 let subscription = button.property_changed(move |e| {
224 if e.property() != ToggleButton::is_checked_property().as_property() {
225 return;
226 }
227 if let (Some(owner), Some(button)) = (owner.upgrade(), weak_button.upgrade()) {
228 if !owner.synchronizing.get()
229 && button.is_checked() == Some(true)
230 && owner.can_edit()
231 {
232 match role {
233 0 => {
234 owner.set_selection(MaterialTimeSelection::Hour);
235 owner.focus_selection();
236 }
237 1 => {
238 owner.set_selection(MaterialTimeSelection::Minute);
239 owner.focus_selection();
240 }
241 2 => owner.set_period(false),
242 _ => owner.set_period(true),
243 }
244 }
245 }
246 });
247 this.subscriptions.borrow_mut().push(subscription);
248 if role < 2 {
249 let owner = weak.clone();
250 let subscription = button.add_disposable_handler_with(
251 ferroui_controls::Button::click_event(),
252 move |_, _| {
253 if let Some(owner) = owner.upgrade().filter(|o| o.can_edit()) {
254 owner.focus_selection();
255 }
256 },
257 RoutingStrategies::BUBBLE,
258 false,
259 );
260 this.subscriptions.borrow_mut().push(subscription);
261 }
262 }
263 }
264 for (field, selection) in [
265 (&parts.hour_field, MaterialTimeSelection::Hour),
266 (&parts.minute_field, MaterialTimeSelection::Minute),
267 ] {
268 if let Some(field) = field {
269 *field.owner.borrow_mut() = Some(weak.clone());
270 field.selection.set(selection);
271 TextInputOptions::set_content_type(field, TextInputContentType::Digits);
272 TextInputOptions::set_return_key_type(
273 field,
274 if selection == MaterialTimeSelection::Hour {
275 TextInputReturnKeyType::Next
276 } else {
277 TextInputReturnKeyType::Done
278 },
279 );
280 }
281 }
282 for label in [&parts.hour_label, &parts.minute_label]
283 .into_iter()
284 .flatten()
285 {
286 AutomationProperties::set_live_setting(label, AutomationLiveSetting::Polite);
287 }
288 *this.parts.borrow_mut() = parts;
289 this.synchronize(false);
290 if this.display_mode() == MaterialTimeDisplayMode::Input
291 && this.is_attached_to_visual_tree()
292 && this.can_edit()
293 {
294 this.focus_field(this.selection());
295 }
296 }
297}
298
299ferro_properties! { impl MaterialTimePicker {
300 pub fn hour_property() -> StyledProperty<i32> { FerroProperty::register_with::<Self,_>("Hour", StyledPropertyOptions::new(0).validate(|v: &i32| (0..24).contains(v)).default_binding_mode(BindingMode::TwoWay)) }
301 pub fn minute_property() -> StyledProperty<i32> { FerroProperty::register_with::<Self,_>("Minute", StyledPropertyOptions::new(0).validate(|v: &i32| (0..60).contains(v)).default_binding_mode(BindingMode::TwoWay)) }
302 pub fn is_24_hour_property() -> StyledProperty<bool> { FerroProperty::register::<Self,_>("Is24Hour", false) }
303 pub fn selection_property() -> StyledProperty<MaterialTimeSelection> { FerroProperty::register::<Self,_>("Selection", MaterialTimeSelection::Hour) }
304 pub fn display_mode_property() -> StyledProperty<MaterialTimeDisplayMode> { FerroProperty::register::<Self,_>("DisplayMode", MaterialTimeDisplayMode::Dial) }
305 pub fn orientation_property() -> StyledProperty<Orientation> { FerroProperty::register::<Self,_>("Orientation", Orientation::Vertical) }
306 pub fn is_vibrant_property() -> StyledProperty<bool> { FerroProperty::register::<Self,_>("IsVibrant", false) }
307 pub fn is_read_only_property() -> StyledProperty<bool> { FerroProperty::register::<Self,_>("IsReadOnly", false) }
308 pub fn auto_advance_property() -> StyledProperty<bool> { FerroProperty::register::<Self,_>("AutoAdvance", true) }
309 pub fn reduced_motion_property() -> StyledProperty<bool> { FerroProperty::register::<Self,_>("ReducedMotion", false) }
310 pub fn motion_scheme_property() -> StyledProperty<MaterialMotionScheme> { FerroProperty::register::<Self,_>("MotionScheme", MaterialMotionScheme::Expressive) }
311 pub fn hour_label_property() -> StyledProperty<String> { FerroProperty::register::<Self,_>("HourLabel", "Hour".to_owned()) }
312 pub fn minute_label_property() -> StyledProperty<String> { FerroProperty::register::<Self,_>("MinuteLabel", "Minute".to_owned()) }
313 pub fn am_label_property() -> StyledProperty<String> { FerroProperty::register::<Self,_>("AmLabel", "AM".to_owned()) }
314 pub fn pm_label_property() -> StyledProperty<String> { FerroProperty::register::<Self,_>("PmLabel", "PM".to_owned()) }
315 pub fn hour_error_text_property() -> StyledProperty<String> { FerroProperty::register::<Self,_>("HourErrorText", "Enter a valid hour".to_owned()) }
316 pub fn minute_error_text_property() -> StyledProperty<String> { FerroProperty::register::<Self,_>("MinuteErrorText", "Enter a minute from 0 to 59".to_owned()) }
317 pub fn selected_time_property() -> DirectProperty<MaterialTimePicker, TimeSpan> { FerroProperty::register_direct::<Self,_>("SelectedTime", |o| o.selected_time(), Some(|o,v| o.set_selected_time(v)), TimeSpan::ZERO) }
318 pub fn is_hour_input_valid_property() -> DirectProperty<MaterialTimePicker,bool> { FerroProperty::register_direct::<Self,_>("IsHourInputValid", |o| o.is_hour_input_valid(), None, true) }
319 pub fn is_minute_input_valid_property() -> DirectProperty<MaterialTimePicker,bool> { FerroProperty::register_direct::<Self,_>("IsMinuteInputValid", |o| o.is_minute_input_valid(), None, true) }
320 pub fn is_input_valid_property() -> DirectProperty<MaterialTimePicker,bool> { FerroProperty::register_direct::<Self,_>("IsInputValid", |o| o.is_input_valid(), None, true) }
321}}
322
323macro_rules! accessors {
324 ($get:ident, $set:ident, $property:ident, $ty:ty) => {
325 pub fn $get(&self) -> $ty {
326 self.get_value(Self::$property())
327 }
328 pub fn $set(&self, value: $ty) {
329 self.set_value(Self::$property(), value);
330 }
331 };
332}
333impl MaterialTimePicker {
334 ferro_routed_event!(
335 pub fn time_changed_event() -> RoutedEvent<MaterialTimeChangedEventArgs> {
336 RoutedEvent::register::<Self, _>("TimeChanged", RoutingStrategies::BUBBLE)
337 }
338 );
339 ferro_routed_event!(
340 pub fn time_change_finished_event() -> RoutedEvent<RoutedEventArgs> {
341 RoutedEvent::register::<Self, _>("TimeChangeFinished", RoutingStrategies::BUBBLE)
342 }
343 );
344 pub fn construct() -> Self {
345 Self {
346 base: TemplatedControl::construct(),
347 parts: RefCell::new(Parts::default()),
348 subscriptions: RefCell::new(Vec::new()),
349 synchronizing: Cell::new(false),
350 editing: Cell::new(false),
351 selected_time: Cell::new(TimeSpan::ZERO),
352 hour_valid: Cell::new(true),
353 minute_valid: Cell::new(true),
354 input_valid: Cell::new(true),
355 hour_draft: RefCell::new("12".into()),
356 minute_draft: RefCell::new("00".into()),
357 group_id: GROUP_ID.fetch_add(1, Ordering::Relaxed),
358 }
359 }
360 pub fn new() -> Ref<Self> {
361 instantiate(Self::construct())
362 }
363 accessors!(hour, set_hour, hour_property, i32);
364 accessors!(minute, set_minute, minute_property, i32);
365 accessors!(is_24_hour, set_is_24_hour, is_24_hour_property, bool);
366 accessors!(
367 selection,
368 set_selection,
369 selection_property,
370 MaterialTimeSelection
371 );
372 accessors!(
373 display_mode,
374 set_display_mode,
375 display_mode_property,
376 MaterialTimeDisplayMode
377 );
378 accessors!(
379 orientation,
380 set_orientation,
381 orientation_property,
382 Orientation
383 );
384 accessors!(is_vibrant, set_is_vibrant, is_vibrant_property, bool);
385 accessors!(is_read_only, set_is_read_only, is_read_only_property, bool);
386 pub fn auto_advance(&self) -> bool {
390 self.get_value(Self::auto_advance_property())
391 }
392 pub fn set_auto_advance(&self, value: bool) {
393 self.set_value(Self::auto_advance_property(), value);
394 }
395 accessors!(
396 reduced_motion,
397 set_reduced_motion,
398 reduced_motion_property,
399 bool
400 );
401 accessors!(
402 motion_scheme,
403 set_motion_scheme,
404 motion_scheme_property,
405 MaterialMotionScheme
406 );
407 accessors!(hour_label, set_hour_label, hour_label_property, String);
408 accessors!(
409 minute_label,
410 set_minute_label,
411 minute_label_property,
412 String
413 );
414 accessors!(am_label, set_am_label, am_label_property, String);
415 accessors!(pm_label, set_pm_label, pm_label_property, String);
416 accessors!(
417 hour_error_text,
418 set_hour_error_text,
419 hour_error_text_property,
420 String
421 );
422 accessors!(
423 minute_error_text,
424 set_minute_error_text,
425 minute_error_text_property,
426 String
427 );
428 pub fn selected_time(&self) -> TimeSpan {
429 self.selected_time.get()
430 }
431 pub fn is_hour_input_valid(&self) -> bool {
432 self.hour_valid.get()
433 }
434 pub fn is_minute_input_valid(&self) -> bool {
435 self.minute_valid.get()
436 }
437 pub fn is_input_valid(&self) -> bool {
438 self.input_valid.get()
439 }
440 pub fn hour_input(&self) -> String {
441 self.hour_draft.borrow().clone()
442 }
443 pub fn minute_input(&self) -> String {
444 self.minute_draft.borrow().clone()
445 }
446 pub fn set_time(&self, hour: i32, minute: i32) {
448 assert!(
449 (0..24).contains(&hour) && (0..60).contains(&minute),
450 "time must be within 00:00..23:59"
451 );
452 self.synchronizing.set(true);
453 self.set_current_value(Self::hour_property(), hour);
454 self.set_current_value(Self::minute_property(), minute);
455 self.synchronizing.set(false);
456 self.synchronize(!self.editing.get());
457 self.publish_time();
458 }
459 pub fn set_selected_time(&self, value: TimeSpan) {
460 let minutes = value.total_minutes();
461 assert!(
462 minutes.is_finite() && (0.0..1440.0).contains(&minutes),
463 "time must be within one day"
464 );
465 let minutes = minutes.floor() as i32;
466 self.set_time(minutes / 60, minutes % 60);
467 }
468 pub(crate) fn can_edit(&self) -> bool {
469 self.is_effectively_enabled() && !self.is_read_only()
470 }
471 pub(crate) fn set_period(&self, pm: bool) {
472 if self.can_edit() && self.is_hour_input_valid() && (self.hour() >= 12) != pm {
473 self.set_time(self.hour() % 12 + if pm { 12 } else { 0 }, self.minute());
474 self.finish_edit();
475 }
476 }
477 pub(crate) fn select_number(&self, value: i32) {
478 if !self.can_edit() {
479 return;
480 }
481 if self.selection() == MaterialTimeSelection::Minute {
482 self.set_time(self.hour(), value);
483 } else {
484 self.set_time(
485 if self.is_24_hour() {
486 value
487 } else {
488 value % 12 + if self.hour() >= 12 { 12 } else { 0 }
489 },
490 self.minute(),
491 );
492 }
493 }
494 pub(crate) fn finish_edit(&self) {
495 if self.is_input_valid() {
496 self.raise_event(&RoutedEventArgs::with_event(
497 Self::time_change_finished_event(),
498 ));
499 }
500 }
501 fn publish_time(&self) {
502 let time = TimeSpan::from_minutes((self.hour() * 60 + self.minute()) as f64);
503 let old = self.selected_time();
504 if old == time {
505 return;
506 }
507 self.set_and_raise_cell(Self::selected_time_property(), &self.selected_time, time);
508 let e = MaterialTimeChangedEventArgs {
509 base: RoutedEventArgs::with_event(Self::time_changed_event()),
510 old_time: old,
511 new_time: time,
512 };
513 self.raise_event(&e);
514 }
515 fn release_parts(&self) {
516 for subscription in self.subscriptions.borrow_mut().drain(..) {
517 subscription.dispose();
518 }
519 let old = self.parts.replace(Parts::default());
520 if let Some(layout) = old.layout {
521 layout.owner.borrow_mut().take();
522 }
523 if let Some(dial) = old.dial {
524 dial.cancel_interaction();
525 dial.set_owner(None);
526 }
527 for field in [old.hour_field, old.minute_field].into_iter().flatten() {
528 field.owner.borrow_mut().take();
529 }
530 }
531 fn assign_radio_groups(&self) {
532 let parts = self.parts.borrow().clone();
533 let was_syncing = self.synchronizing.replace(true);
534 for (button, group) in [
535 (&parts.hour, 0),
536 (&parts.minute, 0),
537 (&parts.am, 1),
538 (&parts.pm, 1),
539 ] {
540 if let Some(button) = button {
541 button.set_group_name(Some(format!("MaterialTimePicker{}-{group}", self.group_id)));
542 }
543 }
544 self.synchronizing.set(was_syncing);
545 }
546 fn synchronize(&self, reset_drafts: bool) {
547 if self.synchronizing.replace(true) {
548 return;
549 }
550 if reset_drafts {
551 *self.hour_draft.borrow_mut() =
552 format!("{:02}", display_hour(self.hour(), self.is_24_hour()));
553 *self.minute_draft.borrow_mut() = format!("{:02}", self.minute());
554 self.set_validity(true, true);
555 }
556 let parts = self.parts.borrow().clone();
557 let input = self.display_mode() == MaterialTimeDisplayMode::Input;
558 self.pseudo_classes().set(":input", input);
559 self.pseudo_classes().set(":vibrant", self.is_vibrant());
560 self.pseudo_classes()
561 .set(":invalid", !self.is_input_valid());
562 for (button, selected, text, label) in [
563 (
564 &parts.hour,
565 self.selection() == MaterialTimeSelection::Hour,
566 if self.is_hour_input_valid() {
567 format!("{:02}", display_hour(self.hour(), self.is_24_hour()))
568 } else {
569 self.hour_input()
570 },
571 self.hour_label(),
572 ),
573 (
574 &parts.minute,
575 self.selection() == MaterialTimeSelection::Minute,
576 if self.is_minute_input_valid() {
577 format!("{:02}", self.minute())
578 } else {
579 self.minute_input()
580 },
581 self.minute_label(),
582 ),
583 (
584 &parts.am,
585 self.hour() < 12,
586 self.am_label(),
587 self.am_label(),
588 ),
589 (
590 &parts.pm,
591 self.hour() >= 12,
592 self.pm_label(),
593 self.pm_label(),
594 ),
595 ] {
596 if let Some(button) = button {
597 button.set_content(Some(Rc::new(text)));
598 button.set_is_checked(Some(selected));
599 button.set_is_enabled(self.can_edit());
600 button.pseudo_classes().set(":vibrant", self.is_vibrant());
601 button.pseudo_classes().set(":input", input);
602 button.pseudo_classes().set(
603 ":horizontal",
604 !input && self.orientation() == Orientation::Horizontal,
605 );
606 AutomationProperties::set_name(button, Some(&label));
607 }
608 }
609 for (button, selection, valid) in [
610 (
611 &parts.hour,
612 MaterialTimeSelection::Hour,
613 self.is_hour_input_valid(),
614 ),
615 (
616 &parts.minute,
617 MaterialTimeSelection::Minute,
618 self.is_minute_input_valid(),
619 ),
620 ] {
621 if let Some(button) = button {
622 button.set_is_visible(!input || self.selection() != selection);
623 button.pseudo_classes().set(":invalid", !valid);
624 }
625 }
626 for button in [&parts.am, &parts.pm].into_iter().flatten() {
627 button.set_is_visible(!self.is_24_hour());
628 button.set_is_enabled(self.can_edit() && self.is_hour_input_valid());
629 }
630 for (field, draft, valid, label, error) in [
631 (
632 &parts.hour_field,
633 self.hour_input(),
634 self.is_hour_input_valid(),
635 self.hour_label(),
636 self.hour_error_text(),
637 ),
638 (
639 &parts.minute_field,
640 self.minute_input(),
641 self.is_minute_input_valid(),
642 self.minute_label(),
643 self.minute_error_text(),
644 ),
645 ] {
646 if let Some(field) = field {
647 field.set_is_visible(input && self.selection() == field.selection.get());
648 field.set_is_read_only(self.is_read_only());
649 field.pseudo_classes().set(":vibrant", self.is_vibrant());
650 field.pseudo_classes().set(":invalid", !valid);
651 if field.text().as_deref() != Some(&draft) {
652 field.set_text(Some(&draft));
653 }
654 AutomationProperties::set_name(field, Some(&label));
655 AutomationProperties::set_help_text(
656 field,
657 Some(if valid { &label } else { &error }),
658 );
659 }
660 }
661 for (label, valid, text, error) in [
662 (
663 &parts.hour_label,
664 self.is_hour_input_valid(),
665 self.hour_label(),
666 self.hour_error_text(),
667 ),
668 (
669 &parts.minute_label,
670 self.is_minute_input_valid(),
671 self.minute_label(),
672 self.minute_error_text(),
673 ),
674 ] {
675 if let Some(label) = label {
676 label.set_is_visible(input && (!self.is_vibrant() || !valid));
677 label.set_text(Some(if valid { &text } else { &error }));
678 label.pseudo_classes().set(":invalid", !valid);
679 }
680 }
681 if let Some(dial) = &parts.dial {
682 dial.set_is_visible(!input);
683 dial.set_is_enabled(self.can_edit());
684 dial.synchronize();
685 }
686 if let Some(layout) = &parts.layout {
687 layout.invalidate_measure();
688 }
689 self.synchronizing.set(false);
690 }
691 fn set_validity(&self, hour: bool, minute: bool) {
692 self.set_and_raise_cell(Self::is_hour_input_valid_property(), &self.hour_valid, hour);
693 self.set_and_raise_cell(
694 Self::is_minute_input_valid_property(),
695 &self.minute_valid,
696 minute,
697 );
698 self.set_and_raise_cell(
699 Self::is_input_valid_property(),
700 &self.input_valid,
701 hour && minute,
702 );
703 }
704 pub fn set_input(&self, selection: MaterialTimeSelection, text: &str) -> bool {
707 if !self.can_edit() {
708 return false;
709 }
710 let Ok(value) = parse_input(text, selection, self.is_24_hour(), self.hour() >= 12) else {
711 return false;
712 };
713 self.editing.set(true);
714 match selection {
715 MaterialTimeSelection::Hour => {
716 *self.hour_draft.borrow_mut() = text.into();
717 self.set_validity(value.is_some(), self.is_minute_input_valid());
718 }
719 MaterialTimeSelection::Minute => {
720 *self.minute_draft.borrow_mut() = text.into();
721 self.set_validity(self.is_hour_input_valid(), value.is_some());
722 }
723 }
724 if let Some(value) = value {
725 if selection == MaterialTimeSelection::Hour {
726 self.set_time(value, self.minute());
727 } else {
728 self.set_time(self.hour(), value);
729 }
730 } else {
731 self.synchronize(false);
732 }
733 self.editing.set(false);
734 true
735 }
736 pub(crate) fn focus_field(&self, selection: MaterialTimeSelection) {
737 self.set_selection(selection);
738 let parts = self.parts.borrow().clone();
739 let field = if selection == MaterialTimeSelection::Hour {
740 parts.hour_field
741 } else {
742 parts.minute_field
743 };
744 if let Some(field) = field {
745 field.focus();
746 field.select_all();
747 }
748 }
749 pub(crate) fn focus_dial(&self) {
750 if let Some(dial) = self.parts.borrow().dial.clone() {
751 dial.focus_selected();
752 }
753 }
754 fn focus_selection(&self) {
755 if self.display_mode() == MaterialTimeDisplayMode::Input {
756 self.focus_field(self.selection());
757 } else {
758 self.focus_dial();
759 }
760 }
761 pub(crate) fn exit_dial(&self, backwards: bool) {
762 let parts = self.parts.borrow().clone();
763 let target = if backwards {
764 if self.selection() == MaterialTimeSelection::Hour {
765 parts.hour.clone()
766 } else {
767 parts.minute.clone()
768 }
769 } else if self.selection() == MaterialTimeSelection::Hour {
770 parts.minute.clone()
771 } else if !self.is_24_hour() {
772 parts.am
773 } else {
774 None
775 };
776 if let Some(target) = target {
777 target.focus_with(NavigationMethod::Tab, KeyModifiers::empty());
778 } else if let Some(minute) = parts.minute {
779 if let Some(next) =
780 KeyboardNavigationHandler::get_next(&minute.upcast(), NavigationDirection::Next)
781 {
782 next.focus_with(NavigationMethod::Tab, KeyModifiers::empty());
783 }
784 }
785 }
786}
787impl Drop for MaterialTimePicker {
788 fn drop(&mut self) {
789 for subscription in self.subscriptions.get_mut().drain(..) {
790 subscription.dispose();
791 }
792 }
793}
794
795#[repr(C)]
797pub struct MaterialTimePickerField {
798 base: TextBox,
799 owner: RefCell<Option<WeakRef<MaterialTimePicker>>>,
800 selection: Cell<MaterialTimeSelection>,
801}
802ferro_class!(MaterialTimePickerField: TextBox);
803ferro_class_info!(MaterialTimePickerField {
804 new: MaterialTimePickerField::new
805});
806ferro_impl_classes!(MaterialTimePickerField: StyledElementImpl, VisualImpl, LayoutableImpl, InteractiveImpl, ControlImpl, TemplatedControlImpl);
807impl FerroObjectImpl for MaterialTimePickerField {
808 fn constructed(this: &Self) {
809 Self::parent_constructed(this);
810 let weak = this.to_ref().downgrade();
811 this.pasting_from_clipboard(move |_, event| {
812 let Some(args) = event.downcast_ref::<PastingFromClipboardEventArgs>() else {
813 return;
814 };
815 let Some(clipboard) = args.clipboard() else {
816 return;
817 };
818 event.set_handled(true);
819 let weak = weak.clone();
820 drop(
824 Dispatcher::ui_thread()
825 .to_task_scheduler()
826 .start_local(async move {
827 match clipboard.try_get_text_async().await {
828 Ok(Some(text)) => {
829 if let Some(field) =
830 weak.upgrade().filter(|f| f.is_attached_to_visual_tree())
831 {
832 let mut input = TextInputEventArgs::new();
833 input.text = Some(text);
834 input.set_routed_event(Some(InputElement::text_input_event()));
835 field.raise_event(&input);
836 }
837 }
838 Ok(None) => {}
839 Err(error) => {
840 if !matches!(
841 error.kind(),
842 ClipboardErrorKind::Timeout
843 | ClipboardErrorKind::Canceled
844 | ClipboardErrorKind::AccessDenied
845 | ClipboardErrorKind::Platform
846 ) {
847 panic!("Unhandled time-input clipboard error: {error}");
848 }
849 }
850 }
851 }),
852 );
853 });
854 }
855 fn on_property_changed(this: &Self, e: &FerroPropertyChangedEventArgs<'_>) {
856 Self::parent_on_property_changed(this, e);
857 if e.property() == TextBox::text_property().as_property() {
858 if let Some(owner) = this.owner() {
859 if !owner.synchronizing.get() {
860 if !owner.set_input(this.selection.get(), this.text().as_deref().unwrap_or(""))
861 {
862 owner.synchronize(false);
863 }
864 }
865 }
866 }
867 }
868}
869impl TextBoxImpl for MaterialTimePickerField {
870 fn coerce_text(this: &Self, text: Option<String>) -> Option<String> {
871 if let Some(owner) = this.owner() {
872 if !owner.synchronizing.get()
873 && parse_input(
874 text.as_deref().unwrap_or(""),
875 this.selection.get(),
876 owner.is_24_hour(),
877 owner.hour() >= 12,
878 )
879 .is_err()
880 {
881 return this.text();
882 }
883 }
884 Self::parent_coerce_text(this, text)
885 }
886}
887impl InputElementImpl for MaterialTimePickerField {
888 fn on_got_focus(this: &Self, e: &FocusChangedEventArgs) {
889 Self::parent_on_got_focus(this, e);
890 if let Some(owner) = this.owner() {
891 owner.set_selection(this.selection.get());
892 }
893 }
894 fn on_text_input(this: &Self, e: &TextInputEventArgs) {
895 let Some(owner) = this.owner() else {
896 Self::parent_on_text_input(this, e);
897 return;
898 };
899 if !owner.can_edit() {
900 e.set_handled(true);
901 return;
902 }
903 let text = e.text.as_deref().unwrap_or("");
904 if !text.bytes().all(|c| c.is_ascii_digit()) {
905 e.set_handled(true);
906 return;
907 }
908 let current = this.text().unwrap_or_default();
909 if text.len() == 1 && current.len() == 2 && this.selection_start() == this.selection_end() {
910 this.select_all();
911 } else {
912 let retained = current.len().saturating_sub(
913 (this.selection_start() - this.selection_end()).unsigned_abs() as usize,
914 );
915 if retained + text.len() > 2 {
916 e.set_handled(true);
917 return;
918 }
919 }
920 Self::parent_on_text_input(this, e);
921 let value = this.text().unwrap_or_default();
922 if this.selection.get() == MaterialTimeSelection::Hour
923 && owner.auto_advance()
924 && owner.is_hour_input_valid()
925 && (value.len() == 2
926 || (!owner.is_24_hour()
927 && value.parse::<i32>().is_ok_and(|v| (2..=9).contains(&v))))
928 {
929 owner.focus_field(MaterialTimeSelection::Minute);
930 }
931 }
932 fn on_key_down(this: &Self, e: &KeyEventArgs) {
933 if e.key == Key::Enter {
934 if let Some(owner) = this.owner() {
935 if owner.is_input_valid() {
936 if this.selection.get() == MaterialTimeSelection::Hour {
937 owner.focus_field(MaterialTimeSelection::Minute);
938 } else {
939 owner.finish_edit();
940 }
941 }
942 e.set_handled(true);
943 return;
944 }
945 }
946 Self::parent_on_key_down(this, e);
947 }
948}
949impl MaterialTimePickerField {
950 pub fn construct() -> Self {
951 Self {
952 base: TextBox::construct(),
953 owner: RefCell::new(None),
954 selection: Cell::new(MaterialTimeSelection::Hour),
955 }
956 }
957 pub fn new() -> Ref<Self> {
958 instantiate(Self::construct())
959 }
960 fn owner(&self) -> Option<Ref<MaterialTimePicker>> {
961 self.owner.borrow().as_ref().and_then(WeakRef::upgrade)
962 }
963}
964
965#[repr(C)]
967pub struct MaterialTimePickerLayout {
968 base: Panel,
969 owner: RefCell<Option<WeakRef<MaterialTimePicker>>>,
970 metrics: Cell<TimePickerMetrics>,
971}
972ferro_class!(MaterialTimePickerLayout: Panel);
973ferro_class_info!(MaterialTimePickerLayout {
974 new: MaterialTimePickerLayout::new
975});
976ferro_impl_classes!(MaterialTimePickerLayout: FerroObjectImpl, StyledElementImpl, InputElementImpl, InteractiveImpl, ControlImpl, PanelImpl);
977impl VisualImpl for MaterialTimePickerLayout {
978 fn bypass_flow_direction_policies(_: &Self) -> bool {
979 true
980 }
981}
982impl MaterialTimePickerLayout {
983 pub fn construct() -> Self {
984 Self {
985 base: Panel::construct(),
986 owner: RefCell::new(None),
987 metrics: Cell::new(TimePickerMetrics::new(
988 false,
989 false,
990 MaterialTimeDisplayMode::Dial,
991 Orientation::Vertical,
992 f64::INFINITY,
993 )),
994 }
995 }
996 pub fn new() -> Ref<Self> {
997 instantiate(Self::construct())
998 }
999 fn owner(&self) -> Option<Ref<MaterialTimePicker>> {
1000 self.owner.borrow().as_ref().and_then(WeakRef::upgrade)
1001 }
1002}
1003impl LayoutableImpl for MaterialTimePickerLayout {
1004 fn measure_override(this: &Self, available: Size) -> Size {
1005 let Some(owner) = this.owner() else {
1006 return Size::default();
1007 };
1008 let mut metrics = TimePickerMetrics::new(
1009 owner.is_vibrant(),
1010 owner.is_24_hour(),
1011 owner.display_mode(),
1012 owner.orientation(),
1013 available.height,
1014 );
1015 if metrics.input && !owner.is_input_valid() {
1016 metrics.support_height = 39.0;
1017 }
1018 this.metrics.set(metrics);
1019 let parts = owner.parts.borrow().clone();
1020 for field in [parts.hour_field, parts.minute_field].into_iter().flatten() {
1021 field.measure(Size::new(metrics.field_width, metrics.field_height));
1022 }
1023 for button in [parts.hour, parts.minute].into_iter().flatten() {
1024 button.measure(Size::new(metrics.field_width, metrics.field_height));
1025 }
1026 for label in [parts.hour_label, parts.minute_label].into_iter().flatten() {
1027 label.measure(Size::new(metrics.field_width, metrics.support_height));
1028 }
1029 if let Some(separator) = parts.separator {
1030 separator.measure(Size::new(metrics.separator, metrics.field_height));
1031 }
1032 for button in [parts.am, parts.pm].into_iter().flatten() {
1033 button.measure(if metrics.horizontal {
1034 Size::new(
1035 (metrics.fields_width() - metrics.period_inner_gap) / 2.0,
1036 if owner.is_vibrant() { 40.0 } else { 38.0 },
1037 )
1038 } else {
1039 Size::new(
1040 metrics.period_width,
1041 (metrics.field_height - metrics.period_inner_gap) / 2.0,
1042 )
1043 });
1044 }
1045 if let Some(dial) = parts.dial {
1046 dial.measure(Size::new(metrics.dial_size, metrics.dial_size));
1047 }
1048 metrics.desired_size(owner.is_24_hour(), owner.is_vibrant())
1049 }
1050 fn arrange_override(this: &Self, size: Size) -> Size {
1051 let Some(owner) = this.owner() else {
1052 return size;
1053 };
1054 let m = this.metrics.get();
1055 let parts = owner.parts.borrow().clone();
1056 let display = m.display_size(owner.is_24_hour(), owner.is_vibrant());
1057 let desired = m.desired_size(owner.is_24_hour(), owner.is_vibrant());
1058 let rtl = owner.flow_direction() == FlowDirection::RightToLeft;
1059 let ox = (size.width - desired.width).max(0.0) / 2.0;
1060 let oy = (size.height - desired.height).max(0.0) / 2.0;
1061 let dx = if m.horizontal {
1062 if rtl {
1063 ox + m.dial_size + m.dial_gap
1064 } else {
1065 ox
1066 }
1067 } else {
1068 ox + (desired.width - display.width) / 2.0
1069 };
1070 let dy = if m.horizontal {
1071 oy + (desired.height - display.height) / 2.0
1072 } else {
1073 oy
1074 };
1075 let fx = dx
1076 + if rtl && !m.horizontal && !owner.is_24_hour() {
1077 m.period_width + m.period_gap
1078 } else {
1079 0.0
1080 };
1081 let hour = Rect::new(fx, dy, m.field_width, m.field_height);
1082 let minute = Rect::new(
1083 fx + m.field_width + m.separator,
1084 dy,
1085 m.field_width,
1086 m.field_height,
1087 );
1088 if let Some(c) = parts.hour {
1089 c.arrange(hour);
1090 }
1091 if let Some(c) = parts.hour_field {
1092 c.arrange(hour);
1093 }
1094 if let Some(c) = parts.minute {
1095 c.arrange(minute);
1096 }
1097 if let Some(c) = parts.minute_field {
1098 c.arrange(minute);
1099 }
1100 if let Some(c) = parts.separator {
1101 c.arrange(Rect::new(
1102 fx + m.field_width,
1103 dy - 4.0,
1104 m.separator,
1105 m.field_height,
1106 ));
1107 }
1108 if let Some(c) = parts.hour_label {
1109 c.arrange(Rect::new(
1110 hour.x,
1111 dy + m.field_height + 7.0,
1112 m.field_width,
1113 32.0,
1114 ));
1115 }
1116 if let Some(c) = parts.minute_label {
1117 c.arrange(Rect::new(
1118 minute.x,
1119 dy + m.field_height + 7.0,
1120 m.field_width,
1121 32.0,
1122 ));
1123 }
1124 for (button, index) in [(parts.am, 0), (parts.pm, 1)] {
1125 if let Some(button) = button {
1126 let rect = if m.horizontal {
1127 let w = (m.fields_width() - m.period_inner_gap) / 2.0;
1128 Rect::new(
1131 dx + index as f64 * (w + m.period_inner_gap),
1132 dy + m.field_height + m.period_gap,
1133 w,
1134 if owner.is_vibrant() { 40.0 } else { 38.0 },
1135 )
1136 } else {
1137 let h = (m.field_height - m.period_inner_gap) / 2.0;
1138 Rect::new(
1139 if rtl {
1140 dx
1141 } else {
1142 dx + m.fields_width() + m.period_gap
1143 },
1144 dy + index as f64 * (h + m.period_inner_gap),
1145 m.period_width,
1146 h,
1147 )
1148 };
1149 button.arrange(rect);
1150 }
1151 }
1152 if let Some(dial) = parts.dial {
1153 let (x, y) = if m.horizontal {
1154 (
1155 if rtl {
1156 ox
1157 } else {
1158 ox + display.width + m.dial_gap
1159 },
1160 oy + (desired.height - m.dial_size) / 2.0,
1161 )
1162 } else {
1163 (
1164 ox + (desired.width - m.dial_size) / 2.0,
1165 oy + display.height + m.dial_gap,
1166 )
1167 };
1168 dial.arrange(Rect::new(x, y, m.dial_size, m.dial_size));
1169 }
1170 size
1171 }
1172}
1173
1174#[repr(C)]
1175pub struct MaterialTimePickerAutomationPeer {
1176 base: ControlAutomationPeer,
1177}
1178ferro_class!(MaterialTimePickerAutomationPeer: ControlAutomationPeer);
1179ferro_class_info!(MaterialTimePickerAutomationPeer { interfaces: [Rc<dyn IValueProvider> => MaterialTimePickerAutomationPeer::provider] });
1180ferro_impl_classes!(MaterialTimePickerAutomationPeer: FerroObjectImpl, ControlAutomationPeerImpl);
1181impl AutomationPeerImpl for MaterialTimePickerAutomationPeer {
1182 fn get_class_name_core(_: &Self) -> String {
1183 "MaterialTimePicker".into()
1184 }
1185 fn get_automation_control_type_core(_: &Self) -> AutomationControlType {
1186 AutomationControlType::Custom
1187 }
1188}
1189impl MaterialTimePickerAutomationPeer {
1190 pub fn new(owner: &MaterialTimePicker) -> Ref<Self> {
1191 instantiate(Self {
1192 base: ControlAutomationPeer::construct(owner),
1193 })
1194 }
1195 fn provider(peer: Ref<Self>) -> Rc<dyn IValueProvider> {
1196 Rc::new(TimeValueProvider(peer))
1197 }
1198 fn picker(&self) -> Ref<MaterialTimePicker> {
1199 ControlAutomationPeer::owner(self)
1200 .cast()
1201 .expect("time picker owner")
1202 }
1203}
1204struct TimeValueProvider(Ref<MaterialTimePickerAutomationPeer>);
1205impl IValueProvider for TimeValueProvider {
1206 fn peer(&self) -> Ref<AutomationPeer> {
1207 self.0.clone().upcast()
1208 }
1209 fn is_read_only(&self) -> bool {
1210 self.0.picker().is_read_only()
1211 }
1212 fn value(&self) -> Option<String> {
1213 let p = self.0.picker();
1214 Some(format!("{:02}:{:02}", p.hour(), p.minute()))
1215 }
1216 fn set_value(&self, value: Option<&str>) -> Result<(), ProviderError> {
1217 let owner = self.0.picker();
1218 if !owner.can_edit() {
1219 return Err(Box::new(ElementNotEnabledException::with_message(
1220 "Time picker is disabled or read-only.",
1221 )));
1222 }
1223 let time = value
1224 .and_then(|s| s.split_once(':'))
1225 .and_then(|(h, m)| Some((h.parse::<i32>().ok()?, m.parse::<i32>().ok()?)))
1226 .filter(|(h, m)| (0..24).contains(h) && (0..60).contains(m));
1227 let Some((hour, minute)) = time else {
1228 return Err(std::io::Error::new(
1229 std::io::ErrorKind::InvalidInput,
1230 "Expected a time between 00:00 and 23:59.",
1231 )
1232 .into());
1233 };
1234 owner.set_time(hour, minute);
1235 owner.finish_edit();
1236 Ok(())
1237 }
1238}