Skip to main content

ferroui_controls/calendar/
selected_dates_collection.rs

1// (c) Copyright Microsoft Corporation.
2// This source is subject to the Microsoft Public License (Ms-PL).
3// Please see https://go.microsoft.com/fwlink/?LinkID=131993 for details.
4// All other rights reserved.
5//
6// Ported from the Silverlight Toolkit sources as adapted by the upstream
7// project; the license text is in the `NOTICE.md` of this crate.
8
9use super::{Calendar, CalendarSelectionMode, DateTimeHelper};
10use crate::primitives::SelectingItemsControl;
11use crate::SelectionChangedEventArgs;
12use ferroui_base::collections::{CollectionChangedHandler, FerroList};
13use ferroui_base::threading::Dispatcher;
14use ferroui_base::utilities::DateTime;
15use ferroui_base::{BoxedValue, Ref, WeakRef};
16use std::cell::{Cell, RefCell};
17use std::rc::Rc;
18
19/// Represents a set of selected dates in a [`Calendar`].
20///
21/// The value is a shared handle: clones refer to the same collection, and
22/// handles compare by identity. Every change goes through the collection,
23/// which validates it against the selection mode of the calendar that owns
24/// it and keeps the selected date of that calendar in step.
25#[derive(Clone)]
26pub struct SelectedDatesCollection(Rc<SelectedDatesCollectionData>);
27
28struct SelectedDatesCollectionData {
29    list: FerroList<DateTime>,
30    /// Inherited code: Requires comment.
31    added_items: RefCell<Vec<DateTime>>,
32    /// Inherited code: Requires comment.
33    is_cleared: Cell<bool>,
34    /// Inherited code: Requires comment.
35    is_range_added: Cell<bool>,
36    /// Inherited code: Requires comment.
37    owner: WeakRef<Calendar>,
38}
39
40impl PartialEq for SelectedDatesCollection {
41    fn eq(&self, other: &Self) -> bool {
42        Rc::ptr_eq(&self.0, &other.0)
43    }
44}
45
46/// The dates as the items of the arguments of a selection change.
47fn boxed_dates(dates: &[DateTime]) -> Vec<Option<BoxedValue>> {
48    dates.iter().map(|date| Some(Rc::new(*date) as BoxedValue)).collect()
49}
50
51impl SelectedDatesCollection {
52    /// Initializes a new instance of the [`SelectedDatesCollection`] class.
53    ///
54    /// `owner` is the [`Calendar`] associated with this object.
55    pub fn new(owner: &Ref<Calendar>) -> Self {
56        Self(Rc::new(SelectedDatesCollectionData {
57            list: FerroList::new(),
58            added_items: RefCell::new(Vec::new()),
59            is_cleared: Cell::new(false),
60            is_range_added: Cell::new(false),
61            owner: owner.downgrade(),
62        }))
63    }
64
65    fn owner(&self) -> Ref<Calendar> {
66        self.0.owner.upgrade().expect("the calendar of the selected dates is alive")
67    }
68
69    fn invoke_collection_changed(&self, removed_items: &[DateTime], added_items: &[DateTime]) {
70        self.owner().on_selected_dates_collection_changed(&SelectionChangedEventArgs::new(
71            Some(SelectingItemsControl::selection_changed_event()),
72            boxed_dates(removed_items),
73            boxed_dates(added_items),
74        ));
75    }
76
77    /// Adds all the dates in the specified range, which includes the first
78    /// and last dates, to the collection.
79    ///
80    /// `start` is the first date to add to the collection and `end` the
81    /// last one.
82    pub fn add_range(&self, start: DateTime, end: DateTime) {
83        let owner = self.owner();
84
85        // increment parameter specifies if the Days were selected in
86        // Descending order or Ascending order based on this value, we add
87        // the days in the range either in Ascending order or in Descending
88        // order
89        let increment = if DateTime::compare(end, start) >= 0 { 1 } else { -1 };
90
91        self.0.added_items.borrow_mut().clear();
92
93        let mut range_start = Some(start);
94        self.0.is_range_added.set(true);
95
96        if owner.is_mouse_selection() {
97            // In Mouse Selection we allow the user to be able to add
98            // multiple ranges in one action in MultipleRange Mode.  In
99            // SingleRange Mode, we only add the first selected range.
100            while let Some(current) = range_start {
101                if DateTime::compare(end, current) == -increment {
102                    break;
103                }
104
105                if Calendar::is_valid_date_selection(&owner, Some(current)) {
106                    self.add(current);
107                } else if owner.selection_mode() == CalendarSelectionMode::SingleRange {
108                    owner.set_hover_end(Some(current.add_days(-increment as f64)));
109                    break;
110                }
111
112                range_start = DateTimeHelper::add_days(current, increment);
113            }
114        } else {
115            // If CalendarSelectionMode.SingleRange and a user
116            // programmatically tries to add multiple ranges, we will throw
117            // away the old range and replace it with the new one.  In order
118            // to provide the removed items without an additional event, we
119            // are calling ClearInternal
120            if owner.selection_mode() == CalendarSelectionMode::SingleRange && self.count() > 0 {
121                owner.add_removed_items(&self.to_vec());
122                self.clear_internal();
123            }
124
125            while let Some(current) = range_start {
126                if DateTime::compare(end, current) == -increment {
127                    break;
128                }
129
130                self.add(current);
131                range_start = DateTimeHelper::add_days(current, increment);
132            }
133        }
134
135        // The arguments get snapshots of the removed and the added dates. The
136        // reference passes its live lists and clears the removed ones after
137        // the event: a handler that keeps the arguments of the reference
138        // sees them emptied later, here it keeps the dates.
139        let added_items = self.0.added_items.borrow().clone();
140        owner.on_selected_dates_collection_changed(&SelectionChangedEventArgs::new(
141            Some(SelectingItemsControl::selection_changed_event()),
142            boxed_dates(&owner.removed_items()),
143            boxed_dates(&added_items),
144        ));
145        owner.clear_removed_items();
146        owner.update_months();
147        self.0.is_range_added.set(false);
148    }
149
150    // --- the members of the collection ---------------------------------------
151
152    /// The number of dates in the collection.
153    pub fn count(&self) -> usize {
154        self.0.list.count()
155    }
156
157    /// Whether the collection has no dates.
158    pub fn is_empty(&self) -> bool {
159        self.0.list.is_empty()
160    }
161
162    /// The date at `index`. Panics when the index is out of range.
163    pub fn get(&self, index: usize) -> DateTime {
164        self.0.list.get(index)
165    }
166
167    /// The dates of the collection, as they are now.
168    pub fn to_vec(&self) -> Vec<DateTime> {
169        self.0.list.to_vec()
170    }
171
172    /// Whether the collection contains the date.
173    pub fn contains(&self, item: DateTime) -> bool {
174        self.0.list.contains(&item)
175    }
176
177    /// The index of the date in the collection.
178    pub fn index_of(&self, item: DateTime) -> Option<usize> {
179        self.0.list.index_of(&item)
180    }
181
182    /// Adds a date to the end of the collection.
183    ///
184    /// Panics when the selection mode of the calendar does not allow the
185    /// change or the date cannot be selected.
186    pub fn add(&self, item: DateTime) {
187        self.insert_item(self.count(), item);
188    }
189
190    /// Inserts a date into the collection at the specified index.
191    ///
192    /// Panics when the selection mode of the calendar does not allow the
193    /// change or the date cannot be selected.
194    pub fn insert(&self, index: usize, item: DateTime) {
195        assert!(index <= self.count(), "Index must be within the bounds of the List.");
196        self.insert_item(index, item);
197    }
198
199    /// Removes a date from the collection. Returns whether it was present.
200    pub fn remove(&self, item: DateTime) -> bool {
201        match self.index_of(item) {
202            Some(index) => {
203                self.remove_item(index);
204                true
205            }
206            None => false,
207        }
208    }
209
210    /// Removes the date at the specified index of the collection.
211    pub fn remove_at(&self, index: usize) {
212        assert!(index < self.count(), "Index was out of range.");
213        self.remove_item(index);
214    }
215
216    /// Replaces the date at the specified index.
217    pub fn set(&self, index: usize, item: DateTime) {
218        assert!(index < self.count(), "Index was out of range.");
219        self.set_item(index, item);
220    }
221
222    /// Removes all dates from the collection.
223    pub fn clear(&self) {
224        self.clear_items();
225    }
226
227    /// Moves the date at `old_index` to `new_index`.
228    pub fn move_item(&self, old_index: usize, new_index: usize) {
229        self.0.list.move_item(old_index, new_index);
230    }
231
232    /// Subscribes to the changes of the collection; returns the token of
233    /// the subscription.
234    pub fn add_collection_changed(&self, handler: Rc<CollectionChangedHandler<DateTime>>) -> u64 {
235        self.0.list.add_collection_changed(handler)
236    }
237
238    /// Ends a subscription to the changes of the collection.
239    pub fn remove_collection_changed(&self, token: u64) -> bool {
240        self.0.list.remove_collection_changed(token)
241    }
242
243    // --- the overrides of the collection --------------------------------------
244
245    /// Removes all items from the collection.
246    ///
247    /// This implementation raises the collection changed event.
248    fn clear_items(&self) {
249        Self::ensure_valid_thread();
250        let owner = self.owner();
251
252        let added_items: Vec<DateTime> = Vec::new();
253        let removed_items = self.to_vec();
254
255        self.0.list.clear();
256
257        // The event fires after SelectedDate changes
258        if owner.selection_mode() != CalendarSelectionMode::None && owner.selected_date().is_some() {
259            owner.set_selected_date(None);
260        }
261
262        if !removed_items.is_empty() {
263            self.invoke_collection_changed(&removed_items, &added_items);
264        }
265        owner.update_months();
266    }
267
268    /// Inserts an item into the collection at the specified index.
269    ///
270    /// This implementation raises the collection changed event.
271    fn insert_item(&self, index: usize, item: DateTime) {
272        Self::ensure_valid_thread();
273        let owner = self.owner();
274
275        if !self.contains(item) {
276            if self.check_selection_mode() {
277                if Calendar::is_valid_date_selection(&owner, Some(item)) {
278                    let mut index = index;
279
280                    // If the Collection is cleared since it is SingleRange
281                    // and it had another range set the index to 0
282                    if self.0.is_cleared.get() {
283                        index = 0;
284                        self.0.is_cleared.set(false);
285                    }
286
287                    self.0.list.insert(index, item);
288
289                    // The event fires after SelectedDate changes
290                    if index == 0
291                        && !owner.selected_date().is_some_and(|selected| DateTime::compare(selected, item) == 0)
292                    {
293                        owner.set_selected_date(Some(item));
294                    }
295
296                    if !self.0.is_range_added.get() {
297                        // A snapshot of the removed dates; the reference passes
298                        // its live list (see `add_range`).
299                        self.invoke_collection_changed(&owner.removed_items(), &[item]);
300                        owner.clear_removed_items();
301                        let month_difference =
302                            DateTimeHelper::compare_year_month(item, owner.display_date_internal());
303
304                        if month_difference < 2 && month_difference > -2 {
305                            owner.update_months();
306                        }
307                    } else {
308                        self.0.added_items.borrow_mut().push(item);
309                    }
310                } else {
311                    panic!("Specified argument was out of the range of valid values. (Parameter 'SelectedDate value is not valid.')");
312                }
313            }
314        }
315    }
316
317    /// Removes the item at the specified index of the collection.
318    ///
319    /// This implementation raises the collection changed event.
320    fn remove_item(&self, index: usize) {
321        Self::ensure_valid_thread();
322        let owner = self.owner();
323
324        if index >= self.count() {
325            self.0.list.remove_at(index);
326        } else {
327            let added_items: Vec<DateTime> = Vec::new();
328            let removed = self.get(index);
329            let month_difference = DateTimeHelper::compare_year_month(removed, owner.display_date_internal());
330
331            self.0.list.remove_at(index);
332
333            // The event fires after SelectedDate changes
334            if index == 0 {
335                if self.count() > 0 {
336                    owner.set_selected_date(Some(self.get(0)));
337                } else {
338                    owner.set_selected_date(None);
339                }
340            }
341
342            self.invoke_collection_changed(&[removed], &added_items);
343
344            if month_difference < 2 && month_difference > -2 {
345                owner.update_months();
346            }
347        }
348    }
349
350    /// Replaces the element at the specified index.
351    ///
352    /// This implementation raises the collection changed event.
353    fn set_item(&self, index: usize, item: DateTime) {
354        Self::ensure_valid_thread();
355        let owner = self.owner();
356
357        if !self.contains(item) {
358            if index >= self.count() {
359                self.0.list.set(index, item);
360            } else {
361                let current = self.get(index);
362                if DateTime::compare(current, item) != 0 && Calendar::is_valid_date_selection(&owner, Some(item)) {
363                    self.0.list.set(index, item);
364
365                    // The event fires after SelectedDate changes
366                    if index == 0
367                        && !owner.selected_date().is_some_and(|selected| DateTime::compare(selected, item) == 0)
368                    {
369                        owner.set_selected_date(Some(item));
370                    }
371
372                    self.invoke_collection_changed(&[current], &[item]);
373
374                    let month_difference = DateTimeHelper::compare_year_month(item, owner.display_date_internal());
375
376                    if month_difference < 2 && month_difference > -2 {
377                        owner.update_months();
378                    }
379                }
380            }
381        }
382    }
383
384    /// Removes all dates without the notifications of [`clear`](Self::clear)
385    /// to the calendar.
386    pub(crate) fn clear_internal(&self) {
387        self.0.list.clear();
388    }
389
390    fn check_selection_mode(&self) -> bool {
391        let owner = self.owner();
392
393        if owner.selection_mode() == CalendarSelectionMode::None {
394            panic!("The SelectedDate property cannot be set when the selection mode is None.");
395        }
396        if owner.selection_mode() == CalendarSelectionMode::SingleDate && self.count() > 0 {
397            panic!("The SelectedDates collection can be changed only in a multiple selection mode. Use the SelectedDate in a single selection mode.");
398        }
399
400        // if user tries to add an item into the SelectedDates in
401        // SingleRange mode, we throw away the old range and replace it with
402        // the new one in order to provide the removed items without an
403        // additional event, we are calling ClearInternal
404        if owner.selection_mode() == CalendarSelectionMode::SingleRange
405            && !self.0.is_range_added.get()
406            && self.count() > 0
407        {
408            owner.add_removed_items(&self.to_vec());
409            self.clear_internal();
410            self.0.is_cleared.set(true);
411        }
412        true
413    }
414
415    fn ensure_valid_thread() {
416        Dispatcher::ui_thread().verify_access();
417    }
418}