Skip to main content

ferroui_controls/date_time_pickers/
date_time_picker_panel.rs

1use crate::presenters::ScrollContentPresenter;
2use crate::primitives::{register_logical_scrollable, ILogicalScrollable};
3use crate::utils::TimeUtils;
4use crate::{Control, ControlImpl, Controls, ListBoxItem, Panel, PanelImpl};
5use ferroui_base::animation::TimeSpan;
6use ferroui_base::input::{
7    IScrollable, InputElement, InputElementImpl, InputElementImplExt, Key, KeyEventArgs, NavigationDirection, ScrollGestureEndedEventArgs,
8    TappedEventArgs,
9};
10use ferroui_base::interactivity::{InteractiveImpl, RoutedEventHandlerToken, RoutingStrategies};
11use ferroui_base::layout::{Layoutable, LayoutableImpl, LayoutableImplExt, VerticalAlignment};
12use ferroui_base::media::{Brushes, IBrush};
13use ferroui_base::reactive::{Disposable, IDisposable};
14use ferroui_base::utilities::{DateTime, HandlerList};
15use ferroui_base::{
16    ferro_class, ferro_class_info, ferro_impl_classes, ferro_properties, instantiate, AnyValue, BoxedValue,
17    FerroObjectImpl, FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs, Rect, Ref, Size, StyledElementImpl, StyledProperty,
18    Vector, Visual, VisualImpl, VisualImplExt, VisualTreeAttachmentEventArgs, WeakRef,
19};
20use std::cell::{Cell, RefCell};
21use std::rc::Rc;
22
23/// What a [`DateTimePickerPanel`] displays, in date or time units.
24#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
25#[repr(i32)]
26pub enum DateTimePickerPanelType {
27    #[default]
28    Year = 0,
29    Month = 1,
30    Day = 2,
31    Hour = 3,
32    Minute = 4,
33    /// AM or PM.
34    TimePeriod = 5,
35    Second = 6,
36}
37
38impl DateTimePickerPanelType {
39    /// The name of the member, as upstream `ToString()` gives it.
40    pub const fn name(self) -> &'static str {
41        match self {
42            DateTimePickerPanelType::Year => "Year",
43            DateTimePickerPanelType::Month => "Month",
44            DateTimePickerPanelType::Day => "Day",
45            DateTimePickerPanelType::Hour => "Hour",
46            DateTimePickerPanelType::Minute => "Minute",
47            DateTimePickerPanelType::TimePeriod => "TimePeriod",
48            DateTimePickerPanelType::Second => "Second",
49        }
50    }
51
52    /// The style class of the items of a panel of this type.
53    const fn item_class(self) -> &'static str {
54        match self {
55            DateTimePickerPanelType::Year => "YearItem",
56            DateTimePickerPanelType::Month => "MonthItem",
57            DateTimePickerPanelType::Day => "DayItem",
58            DateTimePickerPanelType::Hour => "HourItem",
59            DateTimePickerPanelType::Minute => "MinuteItem",
60            DateTimePickerPanelType::TimePeriod => "TimePeriodItem",
61            DateTimePickerPanelType::Second => "SecondItem",
62        }
63    }
64}
65
66/// The looping, virtualizing panel that shows the values of one unit of a
67/// date or time picker.
68#[repr(C)]
69pub struct DateTimePickerPanel {
70    base: Panel,
71
72    // Backing fields for properties
73    minimum_value: Cell<i32>,
74    maximum_value: Cell<i32>,
75    selected_value: Cell<i32>,
76    increment: Cell<i32>,
77
78    // Helper fields
79    selected_index: Cell<i32>,
80    total_items: Cell<i32>,
81    num_items_above_below_selected: Cell<i32>,
82    range: Cell<i32>,
83    extent_one: Cell<f64>,
84    extent: Cell<Size>,
85    offset: Cell<Vector>,
86    has_init: Cell<bool>,
87    suppress_update_offset: Cell<bool>,
88    parent_scroller: RefCell<Option<WeakRef<ScrollContentPresenter>>>,
89    /// The handler of the scroll gesture ended event of the parent scroller,
90    /// while it is added.
91    scroll_gesture_ended_handler: Cell<Option<RoutedEventHandlerToken>>,
92
93    format_date: Cell<DateTime>,
94    scroll_invalidated: Rc<HandlerList<dyn Fn()>>,
95    selection_changed: HandlerList<dyn Fn()>,
96}
97
98ferro_class!(DateTimePickerPanel: Panel);
99
100ferro_class_info!(DateTimePickerPanel {
101    new: DateTimePickerPanel::new,
102    markup: {
103        properties: [
104            MinimumValue: i32 { get: DateTimePickerPanel::minimum_value, set: DateTimePickerPanel::set_minimum_value },
105            MaximumValue: i32 { get: DateTimePickerPanel::maximum_value, set: DateTimePickerPanel::set_maximum_value },
106            SelectedValue: i32 { get: DateTimePickerPanel::selected_value, set: DateTimePickerPanel::set_selected_value },
107            Increment: i32 { get: DateTimePickerPanel::increment, set: DateTimePickerPanel::set_increment },
108            Offset: Vector { get: DateTimePickerPanel::offset, set: DateTimePickerPanel::set_offset },
109            CanHorizontallyScroll: bool {
110                get: DateTimePickerPanel::can_horizontally_scroll,
111                set: DateTimePickerPanel::set_can_horizontally_scroll
112            },
113            CanVerticallyScroll: bool {
114                get: DateTimePickerPanel::can_vertically_scroll,
115                set: DateTimePickerPanel::set_can_vertically_scroll
116            },
117            IsLogicalScrollEnabled: bool { get: DateTimePickerPanel::is_logical_scroll_enabled },
118            ScrollSize: Size { get: DateTimePickerPanel::scroll_size },
119            PageScrollSize: Size { get: DateTimePickerPanel::page_scroll_size },
120            Extent: Size { get: DateTimePickerPanel::extent },
121            Viewport: Size { get: DateTimePickerPanel::viewport },
122        ],
123    },
124});
125
126ferro_impl_classes!(DateTimePickerPanel: StyledElementImpl, InteractiveImpl, ControlImpl, PanelImpl);
127
128impl FerroObjectImpl for DateTimePickerPanel {
129    fn constructed(this: &Self) {
130        Self::parent_constructed(this);
131
132        let weak = this.to_ref().downgrade();
133        this.add_handler_with(
134            InputElement::tapped_event(),
135            move |_, e: &TappedEventArgs| {
136                if let Some(this) = weak.upgrade() {
137                    this.on_item_tapped(e);
138                }
139            },
140            RoutingStrategies::BUBBLE,
141            false,
142        );
143    }
144
145    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
146        Self::parent_on_property_changed(this, change);
147
148        if change.property() == Self::item_height_property().as_property() {
149            let item_height = this.item_height();
150            for child in this.children().snapshot().iter() {
151                child.set_height(item_height);
152            }
153
154            this.update_helper_info();
155            this.has_init.set(false);
156        }
157    }
158}
159
160impl VisualImpl for DateTimePickerPanel {
161    fn on_attached_to_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
162        Self::parent_on_attached_to_visual_tree(this, e);
163
164        let parent_scroller = this.get_visual_parent().and_then(|parent| parent.cast::<ScrollContentPresenter>());
165        if let Some(parent_scroller) = &parent_scroller {
166            let weak = this.to_ref().downgrade();
167            let token = parent_scroller.add_handler(
168                InputElement::scroll_gesture_ended_event(),
169                move |_, e: &ScrollGestureEndedEventArgs| {
170                    if let Some(this) = weak.upgrade() {
171                        this.on_scroll_gesture_ended(e);
172                    }
173                },
174            );
175            this.scroll_gesture_ended_handler.set(Some(token));
176        }
177        *this.parent_scroller.borrow_mut() = parent_scroller.as_ref().map(Ref::downgrade);
178    }
179
180    fn on_detached_from_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
181        Self::parent_on_detached_from_visual_tree(this, e);
182
183        let parent_scroller = this.parent_scroller.take().and_then(|weak| weak.upgrade());
184        let token = this.scroll_gesture_ended_handler.take();
185        if let (Some(parent_scroller), Some(token)) = (parent_scroller, token) {
186            parent_scroller.remove_handler(InputElement::scroll_gesture_ended_event(), token);
187        }
188    }
189}
190
191impl LayoutableImpl for DateTimePickerPanel {
192    fn measure_override(this: &Self, available_size: Size) -> Size {
193        if available_size.width.is_infinite() || available_size.height.is_infinite() {
194            panic!("Panel must have finite height");
195        }
196
197        if !this.has_init.get() {
198            this.update_helper_info();
199        }
200
201        let item_height = this.item_height();
202        let init_y = (available_size.height / 2.0) - (item_height / 2.0);
203        this.num_items_above_below_selected.set((init_y / item_height).ceil() as i32 + 1);
204
205        let children = this.children();
206
207        this.create_or_destroy_items(&children);
208
209        for child in children.snapshot().iter() {
210            child.measure(available_size);
211        }
212
213        if !this.has_init.get() {
214            this.update_items();
215            this.raise_scroll_invalidated();
216            this.has_init.set(true);
217        }
218
219        available_size
220    }
221
222    fn arrange_override(this: &Self, final_size: Size) -> Size {
223        let children = this.children();
224        if children.count() == 0 {
225            return Self::parent_arrange_override(this, final_size);
226        }
227
228        let item_hgt = this.item_height();
229        let offset_y = this.offset.get().y;
230        let mut init_y = (final_size.height / 2.0) - (item_hgt / 2.0);
231
232        if this.should_loop() {
233            let extent_one = this.extent_one.get();
234            let current_set = (offset_y / extent_one).trunc();
235            init_y += (extent_one * current_set)
236                + f64::from(this.selected_index.get() - this.num_items_above_below_selected.get()) * item_hgt;
237
238            for child in children.snapshot().iter() {
239                let rc = Rect::new(0.0, init_y - offset_y, final_size.width, item_hgt);
240                child.arrange(rc);
241                init_y += item_hgt;
242            }
243        } else {
244            let first = f64::from((this.selected_index.get() - this.num_items_above_below_selected.get()).max(0));
245            for child in children.snapshot().iter() {
246                let rc = Rect::new(0.0, (init_y + first * item_hgt) - offset_y, final_size.width, item_hgt);
247                child.arrange(rc);
248                init_y += item_hgt;
249            }
250        }
251
252        final_size
253    }
254}
255
256impl InputElementImpl for DateTimePickerPanel {
257    fn on_key_down(this: &Self, e: &KeyEventArgs) {
258        match e.key {
259            Key::Up => {
260                this.scroll_up(1);
261                e.set_handled(true);
262            }
263            Key::Down => {
264                this.scroll_down(1);
265                e.set_handled(true);
266            }
267            Key::PageUp => {
268                this.scroll_up(4);
269                e.set_handled(true);
270            }
271            Key::PageDown => {
272                this.scroll_down(4);
273                e.set_handled(true);
274            }
275            _ => {}
276        }
277        Self::parent_on_key_down(this, e);
278    }
279}
280
281impl IScrollable for DateTimePickerPanel {
282    fn extent(&self) -> Size {
283        DateTimePickerPanel::extent(self)
284    }
285
286    fn offset(&self) -> Vector {
287        DateTimePickerPanel::offset(self)
288    }
289
290    fn set_offset(&self, value: Vector) {
291        DateTimePickerPanel::set_offset(self, value)
292    }
293
294    fn viewport(&self) -> Size {
295        DateTimePickerPanel::viewport(self)
296    }
297
298    fn can_horizontally_scroll(&self) -> bool {
299        DateTimePickerPanel::can_horizontally_scroll(self)
300    }
301
302    fn can_vertically_scroll(&self) -> bool {
303        DateTimePickerPanel::can_vertically_scroll(self)
304    }
305}
306
307impl ILogicalScrollable for DateTimePickerPanel {
308    fn set_can_horizontally_scroll(&self, value: bool) {
309        DateTimePickerPanel::set_can_horizontally_scroll(self, value)
310    }
311
312    fn set_can_vertically_scroll(&self, value: bool) {
313        DateTimePickerPanel::set_can_vertically_scroll(self, value)
314    }
315
316    fn is_logical_scroll_enabled(&self) -> bool {
317        DateTimePickerPanel::is_logical_scroll_enabled(self)
318    }
319
320    fn scroll_size(&self) -> Size {
321        DateTimePickerPanel::scroll_size(self)
322    }
323
324    fn page_scroll_size(&self) -> Size {
325        DateTimePickerPanel::page_scroll_size(self)
326    }
327
328    fn scroll_invalidated(&self, handler: Rc<dyn Fn()>) -> Rc<dyn IDisposable> {
329        let token = self.scroll_invalidated.add(handler);
330        let handlers = Rc::downgrade(&self.scroll_invalidated);
331        Disposable::create(move || {
332            if let Some(handlers) = handlers.upgrade() {
333                handlers.remove(token);
334            }
335        })
336    }
337
338    fn bring_into_view(&self, target: &Ref<Control>, target_rect: Rect) -> bool {
339        DateTimePickerPanel::bring_into_view(self, target, target_rect)
340    }
341
342    fn get_control_in_direction(
343        &self,
344        direction: NavigationDirection,
345        from: Option<&Ref<Control>>,
346    ) -> Option<Ref<Control>> {
347        DateTimePickerPanel::get_control_in_direction(self, direction, from)
348    }
349
350    fn raise_scroll_invalidated(&self) {
351        DateTimePickerPanel::raise_scroll_invalidated(self)
352    }
353}
354
355ferro_properties! {
356    impl DateTimePickerPanel {
357        /// Defines the `ItemHeight` property.
358        pub fn item_height_property() -> StyledProperty<f64> {
359            FerroProperty::register::<DateTimePickerPanel, _>("ItemHeight", 40.0)
360        }
361
362        /// Defines the `PanelType` property.
363        pub fn panel_type_property() -> StyledProperty<DateTimePickerPanelType> {
364            FerroProperty::register::<DateTimePickerPanel, _>("PanelType", DateTimePickerPanelType::Year)
365        }
366
367        /// Defines the `ItemFormat` property.
368        pub fn item_format_property() -> StyledProperty<String> {
369            FerroProperty::register::<DateTimePickerPanel, _>("ItemFormat", "yyyy".to_string())
370        }
371
372        /// Defines the `ShouldLoop` property.
373        pub fn should_loop_property() -> StyledProperty<bool> {
374            FerroProperty::register::<DateTimePickerPanel, _>("ShouldLoop", false)
375        }
376    }
377}
378
379impl DateTimePickerPanel {
380    fn static_constructor() {
381        InputElement::focusable_property().override_default_value::<DateTimePickerPanel>(true);
382        Panel::background_property()
383            .override_default_value::<DateTimePickerPanel>(Some(Brushes::transparent() as Rc<dyn IBrush>));
384        Layoutable::affects_measure::<DateTimePickerPanel>(&[Self::item_height_property().as_property()]);
385        register_logical_scrollable::<DateTimePickerPanel>();
386    }
387
388    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
389    pub fn construct() -> Self {
390        Self {
391            base: Panel::construct(),
392            minimum_value: Cell::new(1),
393            maximum_value: Cell::new(2),
394            selected_value: Cell::new(1),
395            increment: Cell::new(1),
396            selected_index: Cell::new(0),
397            total_items: Cell::new(0),
398            num_items_above_below_selected: Cell::new(0),
399            range: Cell::new(0),
400            extent_one: Cell::new(0.0),
401            extent: Cell::new(Size::default()),
402            offset: Cell::new(Vector::default()),
403            has_init: Cell::new(false),
404            suppress_update_offset: Cell::new(false),
405            parent_scroller: RefCell::new(None),
406            scroll_gesture_ended_handler: Cell::new(None),
407            format_date: Cell::new(DateTime::now()),
408            scroll_invalidated: Rc::new(HandlerList::new()),
409            selection_changed: HandlerList::new(),
410        }
411    }
412
413    pub fn new() -> Ref<Self> {
414        instantiate(Self::construct())
415    }
416
417    /// What this panel displays, in date or time units.
418    pub fn panel_type(&self) -> DateTimePickerPanelType {
419        self.get_value(Self::panel_type_property())
420    }
421
422    pub fn set_panel_type(&self, value: DateTimePickerPanelType) {
423        self.set_value(Self::panel_type_property(), value)
424    }
425
426    /// The height of each item.
427    pub fn item_height(&self) -> f64 {
428        self.get_value(Self::item_height_property())
429    }
430
431    pub fn set_item_height(&self, value: f64) {
432        self.set_value(Self::item_height_property(), value)
433    }
434
435    /// The string format for the items, using the standard date and time or
436    /// time span formatting. The format must match the panel type.
437    pub fn item_format(&self) -> String {
438        self.get_value(Self::item_format_property())
439    }
440
441    pub fn set_item_format(&self, value: &str) {
442        self.set_value(Self::item_format_property(), value.to_string())
443    }
444
445    /// Whether the panel should loop.
446    pub fn should_loop(&self) -> bool {
447        self.get_value(Self::should_loop_property())
448    }
449
450    pub fn set_should_loop(&self, value: bool) {
451        self.set_value(Self::should_loop_property(), value)
452    }
453
454    /// The minimum value.
455    pub fn minimum_value(&self) -> i32 {
456        self.minimum_value.get()
457    }
458
459    /// Sets the minimum value. Panics when it is greater than the maximum.
460    pub fn set_minimum_value(&self, value: i32) {
461        if value > self.maximum_value() {
462            panic!("Minimum cannot be greater than Maximum");
463        }
464        self.minimum_value.set(value);
465        self.update_helper_info();
466        let sel = self.coerce_selected(self.selected_value());
467        if sel != self.selected_value() {
468            self.set_selected_value(sel);
469        }
470        self.update_items();
471        self.invalidate_arrange();
472        self.raise_scroll_invalidated();
473    }
474
475    /// The maximum value.
476    pub fn maximum_value(&self) -> i32 {
477        self.maximum_value.get()
478    }
479
480    /// Sets the maximum value. Panics when it is less than the minimum.
481    pub fn set_maximum_value(&self, value: i32) {
482        if value < self.minimum_value() {
483            panic!("Maximum cannot be less than Minimum");
484        }
485        self.maximum_value.set(value);
486        self.update_helper_info();
487        let sel = self.coerce_selected(self.selected_value());
488        if sel != self.selected_value() {
489            self.set_selected_value(sel);
490        }
491        self.update_items();
492        self.invalidate_arrange();
493        self.raise_scroll_invalidated();
494    }
495
496    /// The selected value.
497    pub fn selected_value(&self) -> i32 {
498        self.selected_value.get()
499    }
500
501    /// Sets the selected value. Panics when it is outside of the range of
502    /// the minimum and maximum values.
503    pub fn set_selected_value(&self, value: i32) {
504        if value > self.maximum_value() || value < self.minimum_value() {
505            panic!("Specified argument was out of the range of valid values. (Parameter 'value')");
506        }
507
508        let sel = self.coerce_selected(value);
509        self.selected_value.set(sel);
510        self.selected_index.set((value - self.minimum_value()) / self.increment());
511
512        if !self.should_loop() {
513            self.create_or_destroy_items(&self.children());
514        }
515
516        if !self.suppress_update_offset.get() {
517            let item_height = self.item_height();
518            let selected_index = f64::from(self.selected_index.get());
519            self.offset.set(Vector::new(
520                0.0,
521                if self.should_loop() {
522                    selected_index * item_height + (self.extent_one.get() * 50.0)
523                } else {
524                    selected_index * item_height
525                },
526            ));
527        }
528
529        self.update_items();
530        self.invalidate_arrange();
531        self.raise_scroll_invalidated();
532
533        for (_, handler) in self.selection_changed.snapshot().iter() {
534            handler();
535        }
536    }
537
538    /// The increment.
539    pub fn increment(&self) -> i32 {
540        self.increment.get()
541    }
542
543    /// Sets the increment. Panics when it is not positive or greater than
544    /// the range of the values.
545    pub fn set_increment(&self, value: i32) {
546        if value <= 0 || value > self.range.get() {
547            panic!("Specified argument was out of the range of valid values. (Parameter 'value')");
548        }
549        self.increment.set(value);
550        self.update_helper_info();
551        let sel = self.coerce_selected(self.selected_value());
552        if sel != self.selected_value() {
553            self.set_selected_value(sel);
554        }
555        self.update_items();
556        self.invalidate_arrange();
557        self.raise_scroll_invalidated();
558    }
559
560    /// Used to help format the date (if applicable): to display, for
561    /// example, the day of week, the month and the year are needed as
562    /// context; this is that context.
563    pub(crate) fn format_date(&self) -> DateTime {
564        self.format_date.get()
565    }
566
567    pub(crate) fn set_format_date(&self, value: DateTime) {
568        self.format_date.set(value)
569    }
570
571    /// The current scroll offset.
572    pub fn offset(&self) -> Vector {
573        self.offset.get()
574    }
575
576    /// Sets the scroll offset: moves the items and selects the value at the
577    /// offset.
578    pub fn set_offset(&self, value: Vector) {
579        let old = self.offset.get();
580        self.offset.set(value);
581        let dy = value.y - old.y;
582        let children = self.children();
583
584        if dy > 0.0 {
585            // Scroll Down
586            let snapshot = children.snapshot();
587            let mut num_counts_to_move = 0;
588            for child in snapshot.iter() {
589                if child.bounds().bottom() - dy < 0.0 {
590                    num_counts_to_move += 1;
591                } else {
592                    break;
593                }
594            }
595            self.move_children(&children, 0, num_counts_to_move, snapshot.len().checked_sub(1));
596
597            let scroll_height = self.extent.get().height - self.viewport().height;
598            if self.should_loop() && value.y >= scroll_height - self.extent_one.get() {
599                self.offset.set(Vector::new(0.0, value.y - (self.extent_one.get() * 50.0)));
600            }
601        } else if dy < 0.0 {
602            // Scroll Up
603            let snapshot = children.snapshot();
604            let bounds_height = self.bounds().height;
605            let mut num_counts_to_move = 0;
606            for child in snapshot.iter().rev() {
607                if child.bounds().y - dy > bounds_height {
608                    num_counts_to_move += 1;
609                } else {
610                    break;
611                }
612            }
613            self.move_children(&children, snapshot.len() - num_counts_to_move, num_counts_to_move, Some(0));
614            if self.should_loop() && value.y < self.extent_one.get() {
615                self.offset.set(Vector::new(0.0, value.y + (self.extent_one.get() * 50.0)));
616            }
617        }
618
619        // Setting selection will handle all invalidation
620        let new_sel = (self.offset.get().y / self.item_height()) % f64::from(self.total_items.get());
621        self.suppress_update_offset.set(true);
622        self.set_selected_value(new_sel as i32 * self.increment() + self.minimum_value());
623        self.suppress_update_offset.set(false);
624    }
625
626    /// Moves a range of the children. A move of no items, or to the index
627    /// the items already have, changes nothing in the list but is still a
628    /// change of the children upstream, which invalidates the measure of the
629    /// panel; `new_index` is `None` for the index before the first item
630    /// (scrolling down a panel without items), where the move inserts
631    /// upstream and fails.
632    fn move_children(&self, children: &Controls, old_index: usize, count: usize, new_index: Option<usize>) {
633        match new_index {
634            None => panic!(
635                "Index was out of range. Must be non-negative and less than or equal to the size of the collection. (Parameter 'index')"
636            ),
637            Some(new_index) if count != 0 && old_index != new_index => {
638                children.move_range(old_index, count, new_index);
639            }
640            _ => self.invalidate_measure_on_children_changed(),
641        }
642    }
643
644    pub fn can_horizontally_scroll(&self) -> bool {
645        false
646    }
647
648    pub fn set_can_horizontally_scroll(&self, _value: bool) {}
649
650    pub fn can_vertically_scroll(&self) -> bool {
651        true
652    }
653
654    pub fn set_can_vertically_scroll(&self, _value: bool) {}
655
656    pub fn is_logical_scroll_enabled(&self) -> bool {
657        true
658    }
659
660    pub fn scroll_size(&self) -> Size {
661        Size::new(0.0, self.item_height())
662    }
663
664    pub fn page_scroll_size(&self) -> Size {
665        Size::new(0.0, self.item_height() * 4.0)
666    }
667
668    pub fn extent(&self) -> Size {
669        self.extent.get()
670    }
671
672    pub fn viewport(&self) -> Size {
673        self.bounds().size()
674    }
675
676    /// Raised when the selected value is set. Disposing the returned handle
677    /// removes the handler.
678    pub fn selection_changed(&self, handler: impl Fn() + 'static) -> Rc<dyn IDisposable> {
679        let token = self.selection_changed.add(Rc::new(handler));
680        let weak = self.to_ref().downgrade();
681        Disposable::create(move || {
682            if let Some(this) = weak.upgrade() {
683                this.selection_changed.remove(token);
684            }
685        })
686    }
687
688    /// Refreshes the content of the visible items.
689    pub fn refresh_items(&self) {
690        self.update_items();
691    }
692
693    /// Scrolls up the specified number of items.
694    pub fn scroll_up(&self, num_items: i32) {
695        let new_y = (self.offset.get().y - (f64::from(num_items) * self.item_height())).max(0.0);
696        self.set_offset(Vector::new(0.0, new_y));
697    }
698
699    /// Scrolls down the specified number of items.
700    pub fn scroll_down(&self, num_items: i32) {
701        let scroll_height = (self.extent.get().height - self.item_height()).max(0.0);
702        let new_y = (self.offset.get().y + (f64::from(num_items) * self.item_height())).min(scroll_height);
703        self.set_offset(Vector::new(0.0, new_y));
704    }
705
706    /// Updates helper fields used in various calculations.
707    fn update_helper_info(&self) {
708        self.range.set(self.maximum_value.get() - self.minimum_value.get() + 1);
709        self.total_items.set((f64::from(self.range.get()) / f64::from(self.increment.get())).ceil() as i32);
710
711        let item_hgt = self.item_height();
712        let should_loop = self.should_loop();
713        let total_items = f64::from(self.total_items.get());
714        // If looping, measure 100x as many items as we actually have
715        self.extent.set(Size::new(
716            0.0,
717            if should_loop { total_items * item_hgt * 100.0 } else { total_items * item_hgt },
718        ));
719
720        // Height of 1 "set" of items
721        self.extent_one.set(total_items * item_hgt);
722        let selected_index = f64::from(self.selected_index.get());
723        self.offset.set(Vector::new(
724            0.0,
725            if should_loop {
726                self.extent_one.get() * 50.0 + selected_index * item_hgt
727            } else {
728                selected_index * item_hgt
729            },
730        ));
731    }
732
733    /// Ensures enough containers are visible in the viewport.
734    fn create_or_destroy_items(&self, children: &Controls) {
735        let num_items_above_below_selected = self.num_items_above_below_selected.get();
736        let mut total_items_in_viewport = num_items_above_below_selected * 2 + 1;
737
738        if !self.should_loop() {
739            let selected_index = self.selected_index.get();
740            let total_items = self.total_items.get();
741
742            let mut num_item_above_select = num_items_above_below_selected;
743            if selected_index - num_items_above_below_selected < 0 {
744                num_item_above_select = selected_index;
745            }
746            let mut num_item_below_select = num_items_above_below_selected;
747            if selected_index + num_items_above_below_selected >= total_items {
748                num_item_below_select = total_items - selected_index - 1;
749            }
750
751            total_items_in_viewport = num_item_below_select + num_item_above_select + 1;
752        }
753
754        while (children.count() as i64) < i64::from(total_items_in_viewport) {
755            let item = ListBoxItem::new();
756            item.set_height(self.item_height());
757            item.classes().add(self.panel_type().item_class());
758            item.set_vertical_content_alignment(VerticalAlignment::Center);
759            item.set_focusable(false);
760            children.add(item);
761        }
762        if (children.count() as i64) > i64::from(total_items_in_viewport) {
763            // A negative number of items leaves no item: the range of the
764            // removal starts before the first one upstream, which fails.
765            let total_items_in_viewport = usize::try_from(total_items_in_viewport)
766                .unwrap_or_else(|_| panic!("Index was out of range. Must be non-negative."));
767            let num_to_remove = children.count() - total_items_in_viewport;
768            children.remove_range(children.count() - num_to_remove, num_to_remove);
769        }
770    }
771
772    /// Updates item content based on the current selection and the panel
773    /// type.
774    fn update_items(&self) {
775        let children = self.children().snapshot();
776        let min = self.minimum_value();
777        let panel_type = self.panel_type();
778        let selected = self.selected_value();
779        let max = self.maximum_value();
780        let increment = self.increment();
781
782        let mut first;
783        if self.should_loop() {
784            let total_items = self.total_items.get();
785            first = (self.selected_index.get() - self.num_items_above_below_selected.get()) % total_items;
786            first = if first < 0 { min + (first + total_items) * increment } else { min + first * increment };
787        } else {
788            first = min + (self.selected_index.get() - self.num_items_above_below_selected.get()).max(0) * increment;
789        }
790
791        for child in children.iter() {
792            let item = child.cast::<ListBoxItem>().expect("the children of the panel are list box items");
793            item.set_content(Some(Rc::new(self.format_content(first, panel_type)) as BoxedValue));
794            item.set_tag(Some(Rc::new(first) as BoxedValue));
795            item.set_is_selected(first == selected);
796            first += increment;
797            if first > max {
798                first = min;
799            }
800        }
801    }
802
803    fn format_content(&self, value: i32, panel_type: DateTimePickerPanelType) -> String {
804        match panel_type {
805            DateTimePickerPanelType::Year => DateTime::new(value, 1, 1).to_string_with(&self.item_format()),
806            DateTimePickerPanelType::Month => {
807                DateTime::new(self.format_date().year(), value, 1).to_string_with(&self.item_format())
808            }
809            DateTimePickerPanelType::Day => {
810                let format_date = self.format_date();
811                DateTime::new(format_date.year(), format_date.month(), value).to_string_with(&self.item_format())
812            }
813            DateTimePickerPanelType::Hour => TimeSpan::from_hms(value, 0, 0).to_string_format(&self.item_format()),
814            DateTimePickerPanelType::Minute => TimeSpan::from_hms(0, value, 0).to_string_format(&self.item_format()),
815            DateTimePickerPanelType::Second => TimeSpan::from_hms(0, 0, value).to_string_format(&self.item_format()),
816            DateTimePickerPanelType::TimePeriod => {
817                if value == self.minimum_value() {
818                    TimeUtils::get_am_designator()
819                } else {
820                    TimeUtils::get_pm_designator()
821                }
822            }
823        }
824    }
825
826    /// Ensures the selected value is within the bounds and follows the
827    /// current increment.
828    fn coerce_selected(&self, new_value: i32) -> i32 {
829        let minimum = self.minimum_value();
830        let maximum = self.maximum_value();
831        if new_value < minimum {
832            return minimum;
833        }
834        if new_value > maximum {
835            return maximum;
836        }
837
838        let increment = self.increment();
839        if new_value % increment != 0 {
840            // The candidates are the multiples of the increment among the
841            // `maximum + 1` values that start at the minimum (the second
842            // argument of the upstream range is a count), and the result is
843            // the index of the nearest candidate times the increment.
844            let count = i64::from(maximum) + 1;
845            if count < 0 {
846                panic!("Specified argument was out of the range of valid values. (Parameter 'count')");
847            }
848            let distance = |candidate: i64| (candidate - i64::from(new_value)).abs();
849            let mut nearest: Option<(i32, i64)> = None;
850            let candidates = (i64::from(minimum)..i64::from(minimum) + count).filter(|i| i % i64::from(increment) == 0);
851            for (index, candidate) in candidates.enumerate() {
852                nearest = match nearest {
853                    Some((_, current)) if distance(current) <= distance(candidate) => nearest,
854                    _ => Some((index as i32, candidate)),
855                };
856            }
857            let (index, _) = nearest.unwrap_or_else(|| panic!("Sequence contains no elements"));
858            return index * increment;
859        }
860        new_value
861    }
862
863    fn on_item_tapped(&self, e: &TappedEventArgs) {
864        let Some(source) = e.source().and_then(|source| source.cast::<Visual>()) else {
865            return;
866        };
867        let Some(list_box_item) = Self::get_item_from_source(source) else {
868            return;
869        };
870        let Some(tag) = list_box_item.tag() else {
871            return;
872        };
873        let tag: &dyn AnyValue = &*tag;
874        if let Some(tag) = tag.downcast_ref::<i32>() {
875            self.set_selected_value(*tag);
876            e.set_handled(true);
877        }
878    }
879
880    /// Helper to get the list box item from the source of a pointer event.
881    fn get_item_from_source(src: Ref<Visual>) -> Option<Ref<ListBoxItem>> {
882        let mut item = Some(src);
883        while let Some(current) = item {
884            if let Some(list_box_item) = current.cast::<ListBoxItem>() {
885                return Some(list_box_item);
886            }
887            item = current.visual_parent();
888        }
889        None
890    }
891
892    pub fn bring_into_view(&self, _target: &Ref<Control>, _target_rect: Rect) -> bool {
893        false
894    }
895
896    pub fn get_control_in_direction(
897        &self,
898        _direction: NavigationDirection,
899        _from: Option<&Ref<Control>>,
900    ) -> Option<Ref<Control>> {
901        None
902    }
903
904    /// Raises the `ScrollInvalidated` event.
905    pub fn raise_scroll_invalidated(&self) {
906        for (_, handler) in self.scroll_invalidated.snapshot().iter() {
907            handler();
908        }
909    }
910
911    fn on_scroll_gesture_ended(&self, _e: &ScrollGestureEndedEventArgs) {
912        let offset = self.offset.get();
913        let item_height = self.item_height();
914        let snap_y = (offset.y / item_height).round_ties_even() * item_height;
915
916        if snap_y != offset.y {
917            self.set_offset(Vector::new(offset.x, snap_y));
918        }
919    }
920}