1use super::{DatePickerPresenter, DatePickerSelectedValueChangedEventArgs, PickerPresenterBase};
2use crate::primitives::popup_positioning::{PopupAnchor, PopupGravity, PopupPositionerConstraintAdjustment};
3use crate::primitives::{Popup, TemplateAppliedEventArgs, TemplatedControl, TemplatedControlImpl, TemplatedControlImplExt};
4use crate::shapes::Rectangle;
5use crate::{Button, ColumnDefinition, ContentControl, ControlImpl, Grid, GridUnitType, PlacementMode, TextBlock};
6use ferroui_base::data::BindingMode;
7use ferroui_base::input::InputElementImpl;
8use ferroui_base::interactivity::{InteractiveImpl, RoutedEventHandlerToken};
9use ferroui_base::layout::{LayoutableImpl, VerticalAlignment};
10use ferroui_base::reactive::{Disposable, IDisposable};
11use ferroui_base::utilities::{CultureInfo, DateTimeOffset, HandlerList};
12use ferroui_base::{
13 ferro_class, ferro_class_info, ferro_impl_classes, ferro_properties, instantiate, FerroObject, FerroObjectImpl, FerroObjectImplExt,
14 FerroProperty, FerroPropertyChangedEventArgs, Ref, StyledElementImpl, StyledProperty, StyledPropertyOptions,
15 VisualImpl,
16};
17use std::cell::{Cell, RefCell};
18use std::rc::Rc;
19
20const PC_HAS_NO_DATE: &str = ":hasnodate";
21
22pub(super) fn index_of_ignore_case(pattern: &str, letter: char) -> i32 {
25 pattern
26 .encode_utf16()
27 .position(|unit| char::from_u32(u32::from(unit)).is_some_and(|c| c.eq_ignore_ascii_case(&letter)))
28 .map_or(-1, |index| index as i32)
29}
30
31#[repr(C)]
33pub struct DatePicker {
34 base: TemplatedControl,
35
36 flyout_button: RefCell<Option<Ref<Button>>>,
38 day_text: RefCell<Option<Ref<TextBlock>>>,
39 month_text: RefCell<Option<Ref<TextBlock>>>,
40 year_text: RefCell<Option<Ref<TextBlock>>>,
41 container: RefCell<Option<Ref<Grid>>>,
42 spacer1: RefCell<Option<Ref<Rectangle>>>,
43 spacer2: RefCell<Option<Ref<Rectangle>>>,
44 popup: RefCell<Option<Ref<Popup>>>,
45 presenter: RefCell<Option<Ref<DatePickerPresenter>>>,
46
47 flyout_button_click: Cell<Option<RoutedEventHandlerToken>>,
49 presenter_subscriptions: RefCell<Option<(Rc<dyn IDisposable>, Rc<dyn IDisposable>)>>,
52
53 are_controls_available: Cell<bool>,
54
55 selected_date_changed: HandlerList<dyn Fn(&DatePickerSelectedValueChangedEventArgs)>,
56}
57
58ferro_class! {
59 DatePicker: TemplatedControl, virtuals DatePickerImpl: TemplatedControlImpl {
60 fn on_selected_date_changed(this, e: &DatePickerSelectedValueChangedEventArgs);
62 }
63}
64
65ferro_class_info!(DatePicker {
66 new: DatePicker::new,
67 markup: {
68 attributes: [
69 TemplatePart("PART_ButtonContentGrid", type(Ref<Grid>)),
70 TemplatePart("PART_DayTextBlock", type(Ref<TextBlock>)),
71 TemplatePart("PART_FirstSpacer", type(Ref<Rectangle>)),
72 TemplatePart("PART_FlyoutButton", type(Ref<Button>)),
73 TemplatePart("PART_MonthTextBlock", type(Ref<TextBlock>)),
74 TemplatePart("PART_PickerPresenter", type(Ref<DatePickerPresenter>)),
75 TemplatePart("PART_Popup", type(Ref<Popup>)),
76 TemplatePart("PART_SecondSpacer", type(Ref<Rectangle>)),
77 TemplatePart("PART_YearTextBlock", type(Ref<TextBlock>)),
78 PseudoClasses(":hasnodate"),
79 ],
80 },
81});
82
83ferro_impl_classes!(
84 DatePicker: StyledElementImpl,
85 VisualImpl,
86 LayoutableImpl,
87 InteractiveImpl,
88 InputElementImpl
89);
90
91impl FerroObjectImpl for DatePicker {
92 fn constructed(this: &Self) {
93 Self::parent_constructed(this);
94
95 this.pseudo_classes().set(PC_HAS_NO_DATE, true);
96 let now = DateTimeOffset::now();
97 this.set_current_value(
98 Self::min_year_property(),
99 DateTimeOffset::new(now.date().year() - 100, 1, 1, 0, 0, 0, now.offset()),
100 );
101 this.set_current_value(
102 Self::max_year_property(),
103 DateTimeOffset::new(now.date().year() + 100, 12, 31, 0, 0, 0, now.offset()),
104 );
105 }
106
107 fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
108 Self::parent_on_property_changed(this, change);
109
110 let property = change.property();
111 if property == Self::day_visible_property().as_property()
112 || property == Self::month_visible_property().as_property()
113 || property == Self::year_visible_property().as_property()
114 {
115 this.set_grid();
116 } else if property == Self::max_year_property().as_property() {
117 this.on_max_year_changed(change.get_new_value::<DateTimeOffset>());
118 } else if property == Self::min_year_property().as_property() {
119 this.on_min_year_changed(change.get_new_value::<DateTimeOffset>());
120 } else if property == Self::selected_date_property().as_property() {
121 this.set_selected_date_text();
122
123 let (old_value, new_value) = change.get_old_and_new_value::<Option<DateTimeOffset>>();
124 this.on_selected_date_changed(&DatePickerSelectedValueChangedEventArgs::new(old_value, new_value));
125 } else if property == Self::month_format_property().as_property()
126 || property == Self::year_format_property().as_property()
127 || property == Self::day_format_property().as_property()
128 {
129 this.set_selected_date_text();
130 }
131 }
132}
133
134impl TemplatedControlImpl for DatePicker {
135 fn on_apply_template(this: &Self, e: &TemplateAppliedEventArgs) {
136 this.are_controls_available.set(false);
137 let flyout_button = this.flyout_button.borrow().clone();
138 if let (Some(flyout_button), Some(token)) = (flyout_button, this.flyout_button_click.take()) {
139 flyout_button.remove_handler(Button::click_event(), token);
140 }
141 if let Some((confirmed, dismissed)) = this.presenter_subscriptions.take() {
142 confirmed.dispose();
143 dismissed.dispose();
144 }
145
146 Self::parent_on_apply_template(this, e);
147 let name_scope = e.name_scope();
148 let flyout_button = name_scope.find_as::<Button>("PART_FlyoutButton");
149 *this.flyout_button.borrow_mut() = flyout_button.clone();
150 *this.day_text.borrow_mut() = name_scope.find_as::<TextBlock>("PART_DayTextBlock");
151 *this.month_text.borrow_mut() = name_scope.find_as::<TextBlock>("PART_MonthTextBlock");
152 *this.year_text.borrow_mut() = name_scope.find_as::<TextBlock>("PART_YearTextBlock");
153 *this.container.borrow_mut() = name_scope.find_as::<Grid>("PART_ButtonContentGrid");
154 *this.spacer1.borrow_mut() = name_scope.find_as::<Rectangle>("PART_FirstSpacer");
155 *this.spacer2.borrow_mut() = name_scope.find_as::<Rectangle>("PART_SecondSpacer");
156 *this.popup.borrow_mut() = name_scope.find_as::<Popup>("PART_Popup");
157 let presenter = name_scope.find_as::<DatePickerPresenter>("PART_PickerPresenter");
158 *this.presenter.borrow_mut() = presenter.clone();
159
160 this.are_controls_available.set(true);
161
162 this.set_grid();
163 this.set_selected_date_text();
164
165 if let Some(flyout_button) = flyout_button {
166 let weak = this.to_ref().downgrade();
167 let token = flyout_button.click(move |_, _| {
168 if let Some(this) = weak.upgrade() {
169 this.on_flyout_button_clicked();
170 }
171 });
172 this.flyout_button_click.set(Some(token));
173 }
174
175 if let Some(presenter) = presenter {
176 let weak = this.to_ref().downgrade();
177 let confirmed = PickerPresenterBase::confirmed(&presenter, move || {
178 if let Some(this) = weak.upgrade() {
179 this.on_confirmed();
180 }
181 });
182 let weak = this.to_ref().downgrade();
183 let dismissed = PickerPresenterBase::dismissed(&presenter, move || {
184 if let Some(this) = weak.upgrade() {
185 this.on_dismiss_picker();
186 }
187 });
188 *this.presenter_subscriptions.borrow_mut() = Some((confirmed, dismissed));
189
190 let bind = |target: &'static FerroProperty, source: &'static FerroProperty| {
191 presenter.bind_indexer(&target.bind(), &this.indexer(&source.bind()));
192 };
193
194 bind(DatePickerPresenter::max_year_property().as_property(), Self::max_year_property().as_property());
195 bind(DatePickerPresenter::min_year_property().as_property(), Self::min_year_property().as_property());
196
197 bind(
198 DatePickerPresenter::month_visible_property().as_property(),
199 Self::month_visible_property().as_property(),
200 );
201 bind(
202 DatePickerPresenter::month_format_property().as_property(),
203 Self::month_format_property().as_property(),
204 );
205
206 bind(DatePickerPresenter::day_visible_property().as_property(), Self::day_visible_property().as_property());
207 bind(DatePickerPresenter::day_format_property().as_property(), Self::day_format_property().as_property());
208
209 bind(
210 DatePickerPresenter::year_visible_property().as_property(),
211 Self::year_visible_property().as_property(),
212 );
213 bind(DatePickerPresenter::year_format_property().as_property(), Self::year_format_property().as_property());
214 }
215 }
216}
217
218impl ControlImpl for DatePicker {
219 fn on_create_automation_peer(this: &Self) -> Ref<crate::automation::peers::AutomationPeer> {
220 crate::automation::peers::DatePickerAutomationPeer::new(this).upcast()
221 }
222}
223
224impl DatePickerImpl for DatePicker {
225 fn on_selected_date_changed(this: &Self, e: &DatePickerSelectedValueChangedEventArgs) {
226 for (_, handler) in this.selected_date_changed.snapshot().iter() {
227 handler(e);
228 }
229 }
230}
231
232ferro_properties! {
233 impl DatePicker {
234 pub fn day_format_property() -> StyledProperty<String> {
236 FerroProperty::register::<DatePicker, _>("DayFormat", "%d".to_string())
237 }
238
239 pub fn day_visible_property() -> StyledProperty<bool> {
241 FerroProperty::register::<DatePicker, _>("DayVisible", true)
242 }
243
244 pub fn max_year_property() -> StyledProperty<DateTimeOffset> {
246 FerroProperty::register_with::<DatePicker, _>(
247 "MaxYear",
248 StyledPropertyOptions::new(DateTimeOffset::MAX_VALUE).coerce(DatePicker::coerce_max_year),
249 )
250 }
251
252 pub fn min_year_property() -> StyledProperty<DateTimeOffset> {
254 FerroProperty::register_with::<DatePicker, _>(
255 "MinYear",
256 StyledPropertyOptions::new(DateTimeOffset::MIN_VALUE).coerce(DatePicker::coerce_min_year),
257 )
258 }
259
260 pub fn month_format_property() -> StyledProperty<String> {
262 FerroProperty::register::<DatePicker, _>("MonthFormat", "MMMM".to_string())
263 }
264
265 pub fn month_visible_property() -> StyledProperty<bool> {
267 FerroProperty::register::<DatePicker, _>("MonthVisible", true)
268 }
269
270 pub fn year_format_property() -> StyledProperty<String> {
272 FerroProperty::register::<DatePicker, _>("YearFormat", "yyyy".to_string())
273 }
274
275 pub fn year_visible_property() -> StyledProperty<bool> {
277 FerroProperty::register::<DatePicker, _>("YearVisible", true)
278 }
279
280 pub fn vertical_content_alignment_property() -> StyledProperty<VerticalAlignment> {
282 ContentControl::vertical_content_alignment_property().add_owner::<DatePicker>()
283 }
284
285 pub fn selected_date_property() -> StyledProperty<Option<DateTimeOffset>> {
287 FerroProperty::register_with::<DatePicker, _>(
288 "SelectedDate",
289 StyledPropertyOptions::new(None)
290 .default_binding_mode(BindingMode::TwoWay)
291 .enable_data_validation(true),
292 )
293 }
294 }
295}
296
297impl DatePicker {
298 pub fn construct() -> Self {
300 Self {
301 base: TemplatedControl::construct(),
302 flyout_button: RefCell::new(None),
303 day_text: RefCell::new(None),
304 month_text: RefCell::new(None),
305 year_text: RefCell::new(None),
306 container: RefCell::new(None),
307 spacer1: RefCell::new(None),
308 spacer2: RefCell::new(None),
309 popup: RefCell::new(None),
310 presenter: RefCell::new(None),
311 flyout_button_click: Cell::new(None),
312 presenter_subscriptions: RefCell::new(None),
313 are_controls_available: Cell::new(false),
314 selected_date_changed: HandlerList::new(),
315 }
316 }
317
318 pub fn new() -> Ref<Self> {
319 instantiate(Self::construct())
320 }
321
322 pub fn vertical_content_alignment(&self) -> VerticalAlignment {
324 self.get_value(Self::vertical_content_alignment_property())
325 }
326
327 pub fn set_vertical_content_alignment(&self, value: VerticalAlignment) {
328 self.set_value(Self::vertical_content_alignment_property(), value)
329 }
330
331 pub fn day_format(&self) -> String {
333 self.get_value(Self::day_format_property())
334 }
335
336 pub fn set_day_format(&self, value: &str) {
337 self.set_value(Self::day_format_property(), value.to_string())
338 }
339
340 pub fn day_visible(&self) -> bool {
342 self.get_value(Self::day_visible_property())
343 }
344
345 pub fn set_day_visible(&self, value: bool) {
346 self.set_value(Self::day_visible_property(), value)
347 }
348
349 pub fn max_year(&self) -> DateTimeOffset {
351 self.get_value(Self::max_year_property())
352 }
353
354 pub fn set_max_year(&self, value: DateTimeOffset) {
355 self.set_value(Self::max_year_property(), value)
356 }
357
358 fn coerce_max_year(sender: &FerroObject, value: DateTimeOffset) -> DateTimeOffset {
359 if value < sender.get_value(Self::min_year_property()) {
360 panic!("MaxYear cannot be less than MinYear");
361 }
362
363 value
364 }
365
366 fn on_max_year_changed(&self, value: DateTimeOffset) {
367 if self.selected_date().is_some_and(|selected_date| selected_date > value) {
368 self.set_current_value(Self::selected_date_property(), Some(value));
369 }
370 }
371
372 pub fn min_year(&self) -> DateTimeOffset {
374 self.get_value(Self::min_year_property())
375 }
376
377 pub fn set_min_year(&self, value: DateTimeOffset) {
378 self.set_value(Self::min_year_property(), value)
379 }
380
381 fn coerce_min_year(sender: &FerroObject, value: DateTimeOffset) -> DateTimeOffset {
382 if value > sender.get_value(Self::max_year_property()) {
383 panic!("MinYear cannot be greater than MaxYear");
384 }
385
386 value
387 }
388
389 fn on_min_year_changed(&self, value: DateTimeOffset) {
390 if self.selected_date().is_some_and(|selected_date| selected_date < value) {
391 self.set_current_value(Self::selected_date_property(), Some(value));
392 }
393 }
394
395 pub fn month_format(&self) -> String {
397 self.get_value(Self::month_format_property())
398 }
399
400 pub fn set_month_format(&self, value: &str) {
401 self.set_value(Self::month_format_property(), value.to_string())
402 }
403
404 pub fn month_visible(&self) -> bool {
406 self.get_value(Self::month_visible_property())
407 }
408
409 pub fn set_month_visible(&self, value: bool) {
410 self.set_value(Self::month_visible_property(), value)
411 }
412
413 pub fn year_format(&self) -> String {
415 self.get_value(Self::year_format_property())
416 }
417
418 pub fn set_year_format(&self, value: &str) {
419 self.set_value(Self::year_format_property(), value.to_string())
420 }
421
422 pub fn year_visible(&self) -> bool {
424 self.get_value(Self::year_visible_property())
425 }
426
427 pub fn set_year_visible(&self, value: bool) {
428 self.set_value(Self::year_visible_property(), value)
429 }
430
431 pub fn selected_date(&self) -> Option<DateTimeOffset> {
433 self.get_value(Self::selected_date_property())
434 }
435
436 pub fn set_selected_date(&self, value: Option<DateTimeOffset>) {
437 self.set_value(Self::selected_date_property(), value)
438 }
439
440 pub fn selected_date_changed(
443 &self,
444 handler: impl Fn(&DatePickerSelectedValueChangedEventArgs) + 'static,
445 ) -> Rc<dyn IDisposable> {
446 let token = self.selected_date_changed.add(Rc::new(handler));
447 let weak = self.to_ref().downgrade();
448 Disposable::create(move || {
449 if let Some(this) = weak.upgrade() {
450 this.selected_date_changed.remove(token);
451 }
452 })
453 }
454
455 fn on_dismiss_picker(&self) {
456 let popup = self.popup.borrow().clone().expect("the template has a popup");
457 popup.close();
458 self.focus();
459 }
460
461 fn on_confirmed(&self) {
462 let popup = self.popup.borrow().clone().expect("the template has a popup");
463 popup.close();
464 let presenter = self.presenter.borrow().clone().expect("the template has a presenter");
465 self.set_current_value(Self::selected_date_property(), Some(presenter.date()));
466 }
467
468 fn set_grid(&self) {
469 let Some(container) = self.container.borrow().clone() else {
470 return;
471 };
472
473 let month_text = self.month_text.borrow().clone();
474 let year_text = self.year_text.borrow().clone();
475 let day_text = self.day_text.borrow().clone();
476
477 let date_time_format = CultureInfo::current_culture().date_time_format();
478 let fmt = date_time_format.short_date_pattern();
479 let mut columns = [
480 (month_text.clone(), if self.month_visible() { index_of_ignore_case(fmt, 'm') } else { -1 }),
481 (year_text, if self.year_visible() { index_of_ignore_case(fmt, 'y') } else { -1 }),
482 (day_text, if self.day_visible() { index_of_ignore_case(fmt, 'd') } else { -1 }),
483 ];
484
485 columns.sort_by_key(|column| column.1);
486 container.column_definitions().clear();
487
488 let mut column_index = 0;
489
490 for (text, index) in &columns {
491 let Some(text) = text else {
492 continue;
493 };
494
495 text.set_is_visible(*index != -1);
496
497 if *index != -1 {
498 if column_index > 0 {
499 container.column_definitions().add(ColumnDefinition::with_value(0.0, GridUnitType::Auto));
500 }
501
502 let is_month = month_text.as_ref().is_some_and(|month_text| month_text.ptr_eq(text));
503 container
504 .column_definitions()
505 .add(ColumnDefinition::with_value(if is_month { 138.0 } else { 78.0 }, GridUnitType::Star));
506
507 if text.parent().is_none() {
508 container.children().add(text.clone());
509 }
510
511 Grid::set_column(text, column_index * 2);
512 column_index += 1;
513 }
514 }
515
516 let is_spacer1_visible = column_index > 1;
517 let is_spacer2_visible = column_index > 2;
518 let spacer1 = self.spacer1.borrow().clone().expect("the template has a first spacer");
519 let spacer2 = self.spacer2.borrow().clone().expect("the template has a second spacer");
520 Grid::set_column(&spacer1, if is_spacer1_visible { 1 } else { 0 });
522 Grid::set_column(&spacer2, if is_spacer2_visible { 3 } else { 0 });
523
524 spacer1.set_is_visible(is_spacer1_visible);
525 spacer2.set_is_visible(is_spacer2_visible);
526 }
527
528 fn set_selected_date_text(&self) {
529 if !self.are_controls_available.get() {
530 return;
531 }
532
533 let month_text = self.month_text.borrow().clone().expect("the template has a month text block");
534 let year_text = self.year_text.borrow().clone().expect("the template has a year text block");
535 let day_text = self.day_text.borrow().clone().expect("the template has a day text block");
536
537 if let Some(sel_date) = self.selected_date() {
538 self.pseudo_classes().set(PC_HAS_NO_DATE, false);
539 month_text.set_text(Some(&sel_date.to_string_with(&self.month_format())));
540 year_text.set_text(Some(&sel_date.to_string_with(&self.year_format())));
541 day_text.set_text(Some(&sel_date.to_string_with(&self.day_format())));
542 } else {
543 month_text.clear_value(TextBlock::text_property());
545 year_text.clear_value(TextBlock::text_property());
546 day_text.clear_value(TextBlock::text_property());
547 self.pseudo_classes().set(PC_HAS_NO_DATE, true);
548 }
549 }
550
551 fn on_flyout_button_clicked(&self) {
552 let Some(presenter) = self.presenter.borrow().clone() else {
553 panic!("No DatePickerPresenter found.");
554 };
555 let Some(popup) = self.popup.borrow().clone() else {
556 panic!("No Popup found.");
557 };
558
559 presenter.set_date(self.selected_date().unwrap_or_else(DateTimeOffset::now));
560
561 popup.set_placement(PlacementMode::AnchorAndGravity);
562 popup.set_placement_anchor(PopupAnchor::BOTTOM);
563 popup.set_placement_gravity(PopupGravity::BOTTOM);
564 popup.set_placement_constraint_adjustment(PopupPositionerConstraintAdjustment::SLIDE_Y);
565 popup.set_is_open(true);
566
567 if !presenter.is_measure_valid() {
570 if let Some(layout_manager) = self.get_layout_manager() {
571 layout_manager.execute_initial_layout_pass();
572 }
573 }
574
575 let delta_y = presenter.get_offset_for_popup();
576
577 popup.set_vertical_offset(delta_y + 5.0);
579 }
580
581 pub fn clear(&self) {
583 self.set_current_value(Self::selected_date_property(), None);
584 }
585}