Skip to main content

ferroui_xaml_interactivity/collections/
behavior_collection.rs

1//! Port of `Collections/BehaviorCollection.cs`.
2
3use crate::{IBehavior, IBehaviorEventsHandler};
4use ferroui_base::collections::{FerroList, NotifyCollectionChangedAction, NotifyCollectionChangedEventArgs};
5use ferroui_base::{FerroObject, Nullable, Ref, StyledElement, Visual, WeakRef};
6use ferroui_controls::{Control, TopLevel};
7use std::cell::{Cell, RefCell};
8use std::ops::Deref;
9use std::rc::{Rc, Weak};
10
11/// Represents a collection of [`IBehavior`]s with a shared
12/// [`associated_object`](BehaviorCollection::associated_object).
13///
14/// The collection is a notifying list of objects (`FerroList<FerroObject>`); every item must be
15/// a behavior. It is a reference object: clones refer to the same collection and compare by
16/// identity.
17#[derive(Clone)]
18pub struct BehaviorCollection(Rc<BehaviorCollectionState>);
19
20struct BehaviorCollectionState {
21    list: FerroList<Ref<FerroObject>>,
22    // Deviation (DEVIATIONS.md, Behaviors): the reference holds its host strongly; the host
23    // holds the collection (the value of `Interaction.Behaviors`), so the collection refers to
24    // it weakly, as the framework refers to parents and templated parents.
25    associated_object: RefCell<Option<WeakRef<FerroObject>>>,
26    // After a collection change we need to compare the current state of the collection with the
27    // old collection so that we can call detach on all removed items.
28    old_collection: RefCell<Vec<Ref<FerroObject>>>,
29    pending_synchronizations: RefCell<Vec<Ref<FerroObject>>>,
30    is_attaching_collection: Cell<bool>,
31    is_synchronizing_collection: Cell<bool>,
32    has_observed_initialized: Cell<bool>,
33    has_observed_logical_attachment: Cell<bool>,
34    has_observed_visual_attachment: Cell<bool>,
35    has_observed_loaded: Cell<bool>,
36}
37
38impl PartialEq for BehaviorCollection {
39    fn eq(&self, other: &Self) -> bool {
40        Rc::ptr_eq(&self.0, &other.0)
41    }
42}
43
44impl Deref for BehaviorCollection {
45    type Target = FerroList<Ref<FerroObject>>;
46
47    fn deref(&self) -> &FerroList<Ref<FerroObject>> {
48        &self.0.list
49    }
50}
51
52impl Default for BehaviorCollection {
53    fn default() -> Self {
54        Self::new()
55    }
56}
57
58impl std::fmt::Debug for BehaviorCollection {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        write!(f, "BehaviorCollection({})", self.0.list.count())
61    }
62}
63
64/// The object as a behavior.
65fn behavior_of(item: &FerroObject) -> Option<&dyn IBehavior> {
66    <dyn IBehavior>::of(item)
67}
68
69/// The object as a handler of the lifecycle callbacks of its host.
70fn events_handler_of(item: &FerroObject) -> Option<&dyn IBehaviorEventsHandler> {
71    <dyn IBehaviorEventsHandler>::of(item)
72}
73
74/// Whether the object is a top level that is open (`TopLevel { IsLoaded: true }`).
75fn is_open_top_level(associated_object: Option<&Ref<FerroObject>>) -> bool {
76    associated_object.and_then(|o| o.downcast_ref::<TopLevel>()).is_some_and(|top_level| top_level.is_loaded())
77}
78
79/// `associatedObject is StyledElement { IsInitialized: true }`.
80fn is_initialized(associated_object: Option<&Ref<FerroObject>>) -> bool {
81    associated_object.and_then(|o| o.downcast_ref::<StyledElement>()).is_some_and(StyledElement::is_initialized)
82}
83
84/// `associatedObject is StyledElement styledElement && (styledElement.IsAttachedToLogicalTree || isOpenTopLevel)`.
85fn is_logically_attached(associated_object: Option<&Ref<FerroObject>>, is_open_top_level: bool) -> bool {
86    associated_object
87        .and_then(|o| o.downcast_ref::<StyledElement>())
88        .is_some_and(|styled_element| styled_element.is_attached_to_logical_tree() || is_open_top_level)
89}
90
91/// `associatedObject is Visual visual && (visual.IsAttachedToVisualTree() || isOpenTopLevel)`.
92fn is_visually_attached(associated_object: Option<&Ref<FerroObject>>, is_open_top_level: bool) -> bool {
93    associated_object
94        .and_then(|o| o.downcast_ref::<Visual>())
95        .is_some_and(|visual| visual.is_attached_to_visual_tree() || is_open_top_level)
96}
97
98/// `associatedObject is Control { IsLoaded: true }`.
99fn is_loaded(associated_object: Option<&Ref<FerroObject>>) -> bool {
100    associated_object.and_then(|o| o.downcast_ref::<Control>()).is_some_and(Control::is_loaded)
101}
102
103/// Resets a flag when dropped: the `finally` of a `try` that sets it.
104struct ResetFlag<'a>(&'a Cell<bool>, bool);
105
106impl Drop for ResetFlag<'_> {
107    fn drop(&mut self) {
108        self.0.set(self.1);
109    }
110}
111
112impl BehaviorCollection {
113    /// Initializes a new instance of the [`BehaviorCollection`] class.
114    pub fn new() -> Self {
115        let state = Rc::new(BehaviorCollectionState {
116            list: FerroList::new(),
117            associated_object: RefCell::new(None),
118            old_collection: RefCell::new(Vec::new()),
119            pending_synchronizations: RefCell::new(Vec::new()),
120            is_attaching_collection: Cell::new(false),
121            is_synchronizing_collection: Cell::new(false),
122            has_observed_initialized: Cell::new(false),
123            has_observed_logical_attachment: Cell::new(false),
124            has_observed_visual_attachment: Cell::new(false),
125            has_observed_loaded: Cell::new(false),
126        });
127        let weak: Weak<BehaviorCollectionState> = Rc::downgrade(&state);
128        state.list.add_collection_changed(Rc::new(move |e: &NotifyCollectionChangedEventArgs<'_, Ref<FerroObject>>| {
129            if let Some(state) = weak.upgrade() {
130                BehaviorCollection(state).behavior_collection_collection_changed(e);
131            }
132        }));
133        Self(state)
134    }
135
136    /// The list of the collection.
137    pub fn list(&self) -> &FerroList<Ref<FerroObject>> {
138        &self.0.list
139    }
140
141    /// Gets the object to which the [`BehaviorCollection`] is attached.
142    pub fn associated_object(&self) -> Option<Ref<FerroObject>> {
143        self.0.associated_object.borrow().as_ref().and_then(WeakRef::upgrade)
144    }
145
146    /// The behaviors of the collection (`this.OfType<IBehavior>().ToList()`).
147    fn behaviors(&self) -> Vec<Ref<FerroObject>> {
148        self.0.list.snapshot().iter().filter(|item| behavior_of(item).is_some()).cloned().collect()
149    }
150
151    /// Attaches the collection of behaviors to the specified object.
152    ///
153    /// # Panics
154    /// Panics if the [`BehaviorCollection`] is already attached to a different object.
155    pub fn attach(&self, associated_object: impl Into<Nullable<FerroObject>>) {
156        let associated_object = associated_object.into().0;
157        let current = self.associated_object();
158        if associated_object == current {
159            return;
160        }
161
162        if current.is_some() {
163            panic!("An instance of a behavior cannot be attached to more than one object at a time.");
164        }
165
166        debug_assert!(associated_object.is_some(), "The previous checks should keep us from ever setting null here.");
167        let Some(associated_object) = associated_object else {
168            return;
169        };
170        *self.0.associated_object.borrow_mut() = Some(associated_object.downgrade());
171        let behaviors = self.behaviors();
172        self.0.is_attaching_collection.set(true);
173        {
174            let _finally = ResetFlag(&self.0.is_attaching_collection, false);
175            for behavior in &behaviors {
176                let Some(associated_object_for_attach) = self.associated_object() else {
177                    break;
178                };
179
180                if !self.0.list.contains(behavior) {
181                    continue;
182                }
183
184                if let Some(behavior) = behavior_of(behavior) {
185                    behavior.attach(Some(associated_object_for_attach));
186                }
187            }
188        }
189
190        self.synchronize_behavior_events(self.behaviors());
191        self.capture_current_lifecycle_state();
192    }
193
194    /// Detaches the collection of behaviors from the
195    /// [`associated_object`](Self::associated_object).
196    pub fn detach(&self) {
197        for item in self.0.list.snapshot().iter() {
198            if let Some(behavior_item) = behavior_of(item) {
199                if behavior_item.associated_object().is_some() {
200                    behavior_item.detach();
201                }
202            }
203        }
204
205        *self.0.associated_object.borrow_mut() = None;
206        self.0.old_collection.borrow_mut().clear();
207        self.0.pending_synchronizations.borrow_mut().clear();
208        self.0.has_observed_initialized.set(false);
209        self.0.has_observed_logical_attachment.set(false);
210        self.0.has_observed_visual_attachment.set(false);
211        self.0.has_observed_loaded.set(false);
212    }
213
214    /// Dispatches the attachment of the host to the visual tree.
215    pub(crate) fn attached_to_visual_tree(&self) {
216        self.0.has_observed_visual_attachment.set(true);
217        self.dispatch_behavior_event(|handler| handler.attached_to_visual_tree_event_handler());
218    }
219
220    /// Dispatches the detachment of the host from the visual tree.
221    pub(crate) fn detached_from_visual_tree(&self) {
222        self.0.has_observed_visual_attachment.set(false);
223        self.dispatch_behavior_event(|handler| handler.detached_from_visual_tree_event_handler());
224    }
225
226    /// Dispatches the attachment of the host to the logical tree.
227    pub(crate) fn attached_to_logical_tree(&self) {
228        self.0.has_observed_logical_attachment.set(true);
229        self.dispatch_behavior_event(|handler| handler.attached_to_logical_tree_event_handler());
230    }
231
232    /// Dispatches the detachment of the host from the logical tree.
233    pub(crate) fn detached_from_logical_tree(&self) {
234        self.0.has_observed_logical_attachment.set(false);
235        self.dispatch_behavior_event(|handler| handler.detached_from_logical_tree_event_handler());
236    }
237
238    /// Dispatches the `Loaded` event of the host.
239    pub(crate) fn loaded(&self) {
240        self.0.has_observed_loaded.set(true);
241        self.dispatch_behavior_event(|handler| handler.loaded_event_handler());
242    }
243
244    /// Dispatches the `Unloaded` event of the host.
245    pub(crate) fn unloaded(&self) {
246        self.0.has_observed_loaded.set(false);
247        self.dispatch_behavior_event(|handler| handler.unloaded_event_handler());
248    }
249
250    /// Dispatches the `Initialized` event of the host.
251    pub(crate) fn initialized(&self) {
252        self.0.has_observed_initialized.set(true);
253        self.dispatch_behavior_event(|handler| handler.initialized_event_handler());
254    }
255
256    /// Dispatches the `DataContextChanged` event of the host.
257    pub(crate) fn data_context_changed(&self) {
258        self.dispatch_behavior_event(|handler| handler.data_context_changed_event_handler());
259    }
260
261    /// Dispatches the `ResourcesChanged` event of the host.
262    pub(crate) fn resources_changed(&self) {
263        self.dispatch_behavior_event(|handler| handler.resources_changed_event_handler());
264    }
265
266    /// Dispatches the `ActualThemeVariantChanged` event of the host.
267    pub(crate) fn actual_theme_variant_changed(&self) {
268        self.dispatch_behavior_event(|handler| handler.actual_theme_variant_changed_event_handler());
269    }
270
271    /// Dispatches the `Opened` event of a top level host: its attachment to the visual and the
272    /// logical tree, which a top level does not raise.
273    pub(crate) fn opened(&self) {
274        let was_synchronizing_collection = self.0.is_synchronizing_collection.replace(true);
275        let finally = FinishDispatch { collection: self, was_synchronizing_collection };
276        self.attached_to_visual_tree();
277        self.attached_to_logical_tree();
278        drop(finally);
279    }
280
281    fn dispatch_behavior_event(&self, dispatch: fn(&dyn IBehaviorEventsHandler)) {
282        let was_synchronizing_collection = self.0.is_synchronizing_collection.replace(true);
283        let finally = FinishDispatch { collection: self, was_synchronizing_collection };
284        for item in self.0.list.snapshot().iter() {
285            let (Some(behavior_events_handler), Some(behavior)) = (events_handler_of(item), behavior_of(item)) else {
286                continue;
287            };
288            if behavior.associated_object().is_some() && !self.is_pending(item) {
289                dispatch(behavior_events_handler);
290            }
291        }
292        drop(finally);
293    }
294
295    /// The end of a dispatch (its `finally`): restores the flag and replays the behaviors that
296    /// were added while the outermost dispatch ran.
297    fn finish_dispatch(&self, was_synchronizing_collection: bool) {
298        self.0.is_synchronizing_collection.set(was_synchronizing_collection);
299        if !was_synchronizing_collection && !self.0.pending_synchronizations.borrow().is_empty() {
300            let pending = std::mem::take(&mut *self.0.pending_synchronizations.borrow_mut());
301            self.synchronize_behavior_events(pending);
302        }
303    }
304
305    fn is_pending(&self, behavior: &Ref<FerroObject>) -> bool {
306        self.0.pending_synchronizations.borrow().iter().any(|pending| pending == behavior)
307    }
308
309    fn behavior_collection_collection_changed(&self, event_args: &NotifyCollectionChangedEventArgs<'_, Ref<FerroObject>>) {
310        if event_args.action == NotifyCollectionChangedAction::Reset {
311            let old_collection = std::mem::take(&mut *self.0.old_collection.borrow_mut());
312            for behavior in &old_collection {
313                if let Some(behavior) = behavior_of(behavior) {
314                    if behavior.associated_object().is_some() {
315                        behavior.detach();
316                    }
317                }
318            }
319
320            let mut attached_behaviors = Vec::with_capacity(self.0.list.count());
321            for new_item in self.0.list.snapshot().iter() {
322                self.verified_attach(new_item);
323                self.0.old_collection.borrow_mut().push(new_item.clone());
324                attached_behaviors.push(new_item.clone());
325            }
326
327            if !self.0.is_attaching_collection.get() {
328                self.queue_or_synchronize_behavior_events_many(attached_behaviors);
329            }
330            #[cfg(debug_assertions)]
331            self.verify_old_collection_integrity();
332            return;
333        }
334
335        match event_args.action {
336            NotifyCollectionChangedAction::Add => {
337                let event_index = event_args.new_starting_index.max(0) as usize;
338                let Some(changed_item) = event_args.new_items.first().cloned() else {
339                    return;
340                };
341                self.verified_attach(&changed_item);
342                {
343                    let mut old_collection = self.0.old_collection.borrow_mut();
344                    let index = event_index.min(old_collection.len());
345                    old_collection.insert(index, changed_item.clone());
346                }
347                if !self.0.is_attaching_collection.get() {
348                    self.queue_or_synchronize_behavior_events(changed_item);
349                }
350            }
351
352            NotifyCollectionChangedAction::Replace => {
353                let event_index = event_args.old_starting_index;
354                let event_index = if event_index == -1 { 0 } else { event_index as usize };
355
356                let Some(changed_item) = event_args.new_items.first().cloned() else {
357                    return;
358                };
359
360                let old_item = self.0.old_collection.borrow()[event_index].clone();
361                if let Some(old_item) = behavior_of(&old_item) {
362                    if old_item.associated_object().is_some() {
363                        old_item.detach();
364                    }
365                }
366
367                self.verified_attach(&changed_item);
368                self.0.old_collection.borrow_mut()[event_index] = changed_item.clone();
369                if !self.0.is_attaching_collection.get() {
370                    self.queue_or_synchronize_behavior_events(changed_item);
371                }
372            }
373
374            NotifyCollectionChangedAction::Remove => {
375                let event_index = event_args.old_starting_index.max(0) as usize;
376
377                let old_item = self.0.old_collection.borrow()[event_index].clone();
378                if let Some(old_item) = behavior_of(&old_item) {
379                    if old_item.associated_object().is_some() {
380                        old_item.detach();
381                    }
382                }
383
384                self.0.old_collection.borrow_mut().remove(event_index);
385            }
386
387            _ => {
388                debug_assert!(false, "Unsupported collection operation attempted.");
389            }
390        }
391        #[cfg(debug_assertions)]
392        self.verify_old_collection_integrity();
393    }
394
395    fn verified_attach(&self, item: &Ref<FerroObject>) {
396        let Some(behavior) = behavior_of(item) else {
397            panic!("Only IBehavior types are supported in a BehaviorCollection.");
398        };
399
400        if self.0.old_collection.borrow().iter().any(|old| old == item) {
401            panic!("Cannot add an instance of a behavior to a BehaviorCollection more than once.");
402        }
403
404        if let Some(associated_object) = self.associated_object() {
405            behavior.attach(Some(associated_object));
406        }
407    }
408
409    fn queue_or_synchronize_behavior_events(&self, behavior: Ref<FerroObject>) {
410        if self.0.is_synchronizing_collection.get() || self.has_pending_host_lifecycle_event() {
411            self.queue_pending_synchronization(behavior);
412            return;
413        }
414
415        self.synchronize_behavior_events(vec![behavior]);
416    }
417
418    fn queue_or_synchronize_behavior_events_many(&self, behaviors: Vec<Ref<FerroObject>>) {
419        if self.0.is_synchronizing_collection.get() || self.has_pending_host_lifecycle_event() {
420            for behavior in behaviors {
421                self.queue_pending_synchronization(behavior);
422            }
423
424            return;
425        }
426
427        self.synchronize_behavior_events(behaviors);
428    }
429
430    fn queue_pending_synchronization(&self, behavior: Ref<FerroObject>) {
431        if !self.is_pending(&behavior) {
432            self.0.pending_synchronizations.borrow_mut().push(behavior);
433        }
434    }
435
436    fn has_pending_host_lifecycle_event(&self) -> bool {
437        let associated_object = self.associated_object();
438        let Some(associated) = associated_object.as_ref() else {
439            return false;
440        };
441        let associated_object = Some(associated);
442
443        if !self.0.has_observed_initialized.get() && is_initialized(associated_object) {
444            return true;
445        }
446
447        let is_open_top_level = is_open_top_level(associated_object);
448        if !self.0.has_observed_logical_attachment.get() && is_logically_attached(associated_object, is_open_top_level) {
449            return true;
450        }
451
452        if !self.0.has_observed_visual_attachment.get() && is_visually_attached(associated_object, is_open_top_level) {
453            return true;
454        }
455
456        !self.0.has_observed_loaded.get() && is_loaded(associated_object)
457    }
458
459    fn capture_current_lifecycle_state(&self) {
460        let associated_object = self.associated_object();
461        let associated_object = associated_object.as_ref();
462        let is_open_top_level = is_open_top_level(associated_object);
463        self.0.has_observed_initialized.set(is_initialized(associated_object));
464        self.0.has_observed_logical_attachment.set(is_logically_attached(associated_object, is_open_top_level));
465        self.0.has_observed_visual_attachment.set(is_visually_attached(associated_object, is_open_top_level));
466        self.0.has_observed_loaded.set(is_loaded(associated_object));
467    }
468
469    fn synchronize_behavior_events(&self, behaviors: Vec<Ref<FerroObject>>) {
470        self.0.is_synchronizing_collection.set(true);
471        let _finally = FinishSynchronization(self);
472        let mut current_batch = behaviors;
473        for phase in 0..4 {
474            let mut index = 0;
475            while index < current_batch.len() {
476                let behavior = current_batch[index].clone();
477                Self::synchronize_phase(&behavior, phase);
478                self.drain_pending_synchronizations(&mut current_batch, phase);
479                index += 1;
480            }
481        }
482    }
483
484    fn drain_pending_synchronizations(&self, current_batch: &mut Vec<Ref<FerroObject>>, current_phase: i32) {
485        let mut catch_up_batch: Vec<(Ref<FerroObject>, i32)> = Vec::new();
486        loop {
487            if !self.0.pending_synchronizations.borrow().is_empty() {
488                let pending = std::mem::take(&mut *self.0.pending_synchronizations.borrow_mut());
489                for behavior in pending {
490                    if !current_batch.contains(&behavior) {
491                        current_batch.push(behavior.clone());
492                    }
493
494                    let is_already_queued = catch_up_batch.iter().any(|(queued, _)| queued.ptr_eq(&behavior));
495                    if !is_already_queued {
496                        catch_up_batch.push((behavior, 0));
497                    }
498                }
499            }
500
501            let mut next_index: Option<usize> = None;
502            let mut next_phase = i32::MAX;
503            for (index, (_, candidate_phase)) in catch_up_batch.iter().enumerate() {
504                if *candidate_phase <= current_phase && *candidate_phase < next_phase {
505                    next_index = Some(index);
506                    next_phase = *candidate_phase;
507                }
508            }
509
510            let Some(next_index) = next_index else {
511                return;
512            };
513
514            let (behavior, phase) = catch_up_batch[next_index].clone();
515            Self::synchronize_phase(&behavior, phase);
516            catch_up_batch[next_index] = (behavior, phase + 1);
517        }
518    }
519
520    fn synchronize_phase(behavior: &Ref<FerroObject>, phase: i32) {
521        match phase {
522            0 => Self::synchronize_initialized(behavior),
523            1 => Self::synchronize_logical_attachment(behavior),
524            2 => Self::synchronize_visual_attachment(behavior),
525            3 => Self::synchronize_loaded(behavior),
526            _ => {}
527        }
528    }
529
530    fn synchronize_initialized(behavior: &Ref<FerroObject>) {
531        let (Some(events_handler), Some(behavior)) = (events_handler_of(behavior), behavior_of(behavior)) else {
532            return;
533        };
534        if is_initialized(behavior.associated_object().as_ref()) {
535            events_handler.initialized_event_handler();
536        }
537    }
538
539    fn synchronize_logical_attachment(behavior: &Ref<FerroObject>) {
540        let (Some(events_handler), Some(behavior)) = (events_handler_of(behavior), behavior_of(behavior)) else {
541            return;
542        };
543        let associated_object = behavior.associated_object();
544        let is_open_top_level = is_open_top_level(associated_object.as_ref());
545        if is_logically_attached(associated_object.as_ref(), is_open_top_level) {
546            events_handler.attached_to_logical_tree_event_handler();
547        }
548    }
549
550    fn synchronize_visual_attachment(behavior: &Ref<FerroObject>) {
551        let (Some(events_handler), Some(behavior)) = (events_handler_of(behavior), behavior_of(behavior)) else {
552            return;
553        };
554        let associated_object = behavior.associated_object();
555        let is_open_top_level = is_open_top_level(associated_object.as_ref());
556        if is_visually_attached(associated_object.as_ref(), is_open_top_level) {
557            events_handler.attached_to_visual_tree_event_handler();
558        }
559    }
560
561    fn synchronize_loaded(behavior: &Ref<FerroObject>) {
562        let (Some(events_handler), Some(behavior)) = (events_handler_of(behavior), behavior_of(behavior)) else {
563            return;
564        };
565        if is_loaded(behavior.associated_object().as_ref()) {
566            events_handler.loaded_event_handler();
567        }
568    }
569
570    #[cfg(debug_assertions)]
571    fn verify_old_collection_integrity(&self) {
572        let items = self.0.list.snapshot();
573        let old_collection = self.0.old_collection.borrow();
574        let is_valid = items.len() == old_collection.len() && items.iter().zip(old_collection.iter()).all(|(a, b)| a == b);
575
576        debug_assert!(is_valid, "Referential integrity of the collection has been compromised.");
577    }
578}
579
580/// The `finally` of a dispatch of a host event ([`BehaviorCollection::finish_dispatch`]). While a
581/// panic unwinds, only the flag is restored: no behavior is called back.
582struct FinishDispatch<'a> {
583    collection: &'a BehaviorCollection,
584    was_synchronizing_collection: bool,
585}
586
587impl Drop for FinishDispatch<'_> {
588    fn drop(&mut self) {
589        if std::thread::panicking() {
590            self.collection.0.is_synchronizing_collection.set(self.was_synchronizing_collection);
591            return;
592        }
593        self.collection.finish_dispatch(self.was_synchronizing_collection);
594    }
595}
596
597/// The `finally` of a replay of the lifecycle ([`BehaviorCollection::synchronize_behavior_events`]).
598struct FinishSynchronization<'a>(&'a BehaviorCollection);
599
600impl Drop for FinishSynchronization<'_> {
601    fn drop(&mut self) {
602        (self.0).0.is_synchronizing_collection.set(false);
603        let pending = std::mem::take(&mut *(self.0).0.pending_synchronizations.borrow_mut());
604        drop(pending);
605    }
606}