Skip to main content

ferroui_xaml_interactivity/
interaction.rs

1//! Port of `Interaction.cs`.
2
3use crate::{ActionCollection, BehaviorCollection, IAction};
4use ferroui_base::reactive::{Disposable, IDisposable};
5use ferroui_base::threading::{Dispatcher, DispatcherPriority};
6use ferroui_base::{
7    ferro_properties, ferro_static_type, AttachedProperty, BoxedValue, FerroObject, FerroProperty,
8    FerroPropertyChangedEventArgs, Ref, StyledElement, Visual, WeakRef,
9};
10use ferroui_controls::{Control, TopLevel};
11use std::cell::RefCell;
12use std::rc::Rc;
13
14/// Defines a [`BehaviorCollection`] attached property and provides a method for executing an
15/// [`ActionCollection`].
16pub struct Interaction;
17
18ferro_static_type!(Interaction);
19
20ferro_properties! {
21    impl Interaction {
22        /// Gets or sets the [`BehaviorCollection`] associated with a specified object.
23        pub fn behaviors_property() -> AttachedProperty<Option<BehaviorCollection>> {
24            let property = FerroProperty::register_attached::<Interaction, FerroObject, Option<BehaviorCollection>>(
25                "Behaviors",
26                None,
27            );
28            // The static constructor of the reference: `BehaviorsProperty.Changed.Subscribe(..)`.
29            let _ = property.changed().subscribe(Interaction::behaviors_changed);
30            property
31        }
32    }
33}
34
35/// The owner of the handlers [`Interaction`] installs on a host.
36///
37/// Deviation (DEVIATIONS.md, Behaviors): the reference installs static event handlers on the
38/// host, which it removes and adds again by delegate identity (`-=` then `+=`). A subscription of
39/// the framework is a disposable, so the subscriptions of a host are kept with the host, in an
40/// attached property of this private type, and replaced as a set.
41struct InteractionHostHandlers;
42
43ferro_static_type!(InteractionHostHandlers);
44
45ferro_properties! {
46    impl InteractionHostHandlers {
47        fn host_handlers_property() -> AttachedProperty<Option<HostHandlers>> {
48            FerroProperty::register_attached::<InteractionHostHandlers, FerroObject, Option<HostHandlers>>(
49                "InteractionHostHandlers",
50                None,
51            )
52        }
53    }
54}
55
56/// The subscriptions [`Interaction`] made on a host.
57#[derive(Clone)]
58struct HostHandlers(Rc<RefCell<Vec<Rc<dyn IDisposable>>>>);
59
60impl PartialEq for HostHandlers {
61    fn eq(&self, other: &Self) -> bool {
62        Rc::ptr_eq(&self.0, &other.0)
63    }
64}
65
66/// Which set of handlers is installed on a host: the set of a collection created by the getter
67/// (markup's `<Interaction.Behaviors>`), or the set of a collection assigned to the property.
68#[derive(Clone, Copy, PartialEq, Eq)]
69enum HandlerSet {
70    FromGetter,
71    FromChangedEvent,
72}
73
74impl Interaction {
75    /// Gets the [`BehaviorCollection`] associated with a specified object.
76    ///
77    /// A collection is created and set if the object has none: the collection of markup's
78    /// property element syntax (`<Interaction.Behaviors>`), which adds the behaviors to the
79    /// collection the getter returns.
80    pub fn get_behaviors(obj: &FerroObject) -> BehaviorCollection {
81        let behavior_collection = obj.get_value(Self::behaviors_property());
82        match behavior_collection {
83            Some(behavior_collection) => behavior_collection,
84            None => {
85                let behavior_collection = BehaviorCollection::new();
86                obj.set_value(Self::behaviors_property(), Some(behavior_collection.clone()));
87                Self::set_visual_tree_event_handlers_from_getter(obj);
88                behavior_collection
89            }
90        }
91    }
92
93    fn get_existing_behaviors(obj: &FerroObject) -> Option<BehaviorCollection> {
94        obj.get_value(Self::behaviors_property())
95    }
96
97    /// Sets the [`BehaviorCollection`] associated with a specified object.
98    pub fn set_behaviors(obj: &FerroObject, value: Option<BehaviorCollection>) {
99        obj.set_value(Self::behaviors_property(), value);
100    }
101
102    /// Executes all actions in the [`ActionCollection`] and returns their results.
103    ///
104    /// `sender` is passed on to the actions; the value of `parameter` is determined by the calling
105    /// behavior. Returns the results of the actions that returned one, in the order of the
106    /// actions.
107    pub fn execute_actions(
108        sender: Option<&BoxedValue>,
109        actions: Option<&ActionCollection>,
110        parameter: Option<&BoxedValue>,
111    ) -> Vec<BoxedValue> {
112        let Some(actions) = actions else {
113            return Vec::new();
114        };
115
116        let mut results = Vec::new();
117
118        for ferro_object in actions.snapshot().iter() {
119            let Some(action) = <dyn IAction>::of(ferro_object) else {
120                continue;
121            };
122
123            if let Some(result) = action.execute(sender, parameter) {
124                results.push(result);
125            }
126        }
127
128        results
129    }
130
131    fn behaviors_changed(e: &FerroPropertyChangedEventArgs<'_>) {
132        let old_collection = e.get_old_value::<Option<BehaviorCollection>>().flatten();
133        let new_collection = e.get_new_value::<Option<BehaviorCollection>>();
134
135        if old_collection == new_collection {
136            return;
137        }
138
139        let sender = e.sender().to_ref();
140        let is_attached_to_visual_tree =
141            sender.downcast_ref::<Visual>().is_some_and(|visual| visual.is_attached_to_visual_tree());
142
143        if let Some(old_collection) = &old_collection {
144            if old_collection.associated_object().is_some() {
145                if is_attached_to_visual_tree {
146                    old_collection.detached_from_visual_tree();
147                }
148
149                old_collection.detach();
150            }
151        }
152
153        if let Some(new_collection) = &new_collection {
154            Self::set_visual_tree_event_handlers_from_changed_event(&sender);
155            new_collection.attach(&sender);
156
157            if is_attached_to_visual_tree {
158                new_collection.attached_to_visual_tree();
159            }
160        }
161    }
162
163    fn set_visual_tree_event_handlers_from_getter(obj: &FerroObject) {
164        Self::set_visual_tree_event_handlers(obj, HandlerSet::FromGetter);
165    }
166
167    fn set_visual_tree_event_handlers_from_changed_event(obj: &FerroObject) {
168        Self::set_visual_tree_event_handlers(obj, HandlerSet::FromChangedEvent);
169    }
170
171    /// Installs a set of handlers on the host, after removing the set installed before (the
172    /// reference removes the handlers of both sets and adds the ones of `set`, event by event,
173    /// which puts them last on each event, as here).
174    fn set_visual_tree_event_handlers(obj: &FerroObject, set: HandlerSet) {
175        let property = InteractionHostHandlers::host_handlers_property();
176        let handlers = match obj.get_value(property) {
177            Some(handlers) => handlers,
178            None => {
179                let handlers = HostHandlers(Rc::new(RefCell::new(Vec::new())));
180                obj.set_value(property, Some(handlers.clone()));
181                handlers
182            }
183        };
184        let previous = std::mem::take(&mut *handlers.0.borrow_mut());
185        for subscription in previous {
186            subscription.dispose();
187        }
188
189        let host = obj.to_weak();
190        let mut subscriptions: Vec<Rc<dyn IDisposable>> = Vec::new();
191
192        if let Some(visual) = obj.downcast_ref::<Visual>() {
193            // AttachedToVisualTree / DetachedFromVisualTree
194
195            let weak = host.clone();
196            subscriptions.push(visual.attached_to_visual_tree(move |_| {
197                if let Some(d) = weak.upgrade() {
198                    match set {
199                        HandlerSet::FromGetter => Self::visual_attached_to_visual_tree_from_getter(&d),
200                        HandlerSet::FromChangedEvent => Self::visual_attached_to_visual_tree_from_changed_event(&d),
201                    }
202                }
203            }));
204            let weak = host.clone();
205            subscriptions.push(visual.detached_from_visual_tree(move |_| {
206                if let Some(d) = weak.upgrade() {
207                    match set {
208                        HandlerSet::FromGetter => Self::visual_detached_from_visual_tree_from_getter(&d),
209                        HandlerSet::FromChangedEvent => Self::visual_detached_from_visual_tree_from_changed_event(&d),
210                    }
211                }
212            }));
213        }
214
215        if let Some(styled_element) = obj.downcast_ref::<StyledElement>() {
216            // AttachedToLogicalTree / DetachedFromLogicalTree
217
218            let weak = host.clone();
219            subscriptions.push(styled_element.attached_to_logical_tree(move |_| {
220                Self::with_existing_behaviors(&weak, BehaviorCollection::attached_to_logical_tree);
221            }));
222            let weak = host.clone();
223            subscriptions.push(styled_element.detached_from_logical_tree(move |_| {
224                Self::with_existing_behaviors(&weak, BehaviorCollection::detached_from_logical_tree);
225            }));
226
227            // Initialized
228
229            let weak = host.clone();
230            subscriptions.push(styled_element.initialized(move || {
231                Self::with_existing_behaviors(&weak, BehaviorCollection::initialized);
232            }));
233
234            // DataContextChanged
235
236            let weak = host.clone();
237            subscriptions.push(styled_element.data_context_changed(move || {
238                Self::with_existing_behaviors(&weak, BehaviorCollection::data_context_changed);
239            }));
240
241            // ResourcesChanged
242
243            let weak = host.clone();
244            subscriptions.push(styled_element.resources_changed(move |_| {
245                Self::with_existing_behaviors(&weak, BehaviorCollection::resources_changed);
246            }));
247
248            // ActualThemeVariantChanged
249
250            let weak = host.clone();
251            subscriptions.push(styled_element.actual_theme_variant_changed(move || {
252                Self::with_existing_behaviors(&weak, BehaviorCollection::actual_theme_variant_changed);
253            }));
254        }
255
256        if let Some(control) = obj.downcast_ref::<Control>() {
257            // Loaded / Unloaded
258
259            let weak = host.clone();
260            let token = control.loaded(move |_, _| {
261                Self::with_existing_behaviors(&weak, BehaviorCollection::loaded);
262            });
263            let control_weak = control.to_ref().downgrade();
264            subscriptions.push(Disposable::create(move || {
265                if let Some(control) = control_weak.upgrade() {
266                    control.remove_handler(Control::loaded_event(), token);
267                }
268            }));
269
270            let weak = host.clone();
271            let token = control.unloaded(move |_, _| {
272                Self::with_existing_behaviors(&weak, BehaviorCollection::unloaded);
273            });
274            let control_weak = control.to_ref().downgrade();
275            subscriptions.push(Disposable::create(move || {
276                if let Some(control) = control_weak.upgrade() {
277                    control.remove_handler(Control::unloaded_event(), token);
278                }
279            }));
280        }
281
282        if let Some(top_level) = obj.downcast_ref::<TopLevel>() {
283            let weak = host.clone();
284            subscriptions.push(top_level.opened(move || {
285                if let Some(d) = weak.upgrade() {
286                    match set {
287                        HandlerSet::FromGetter => Self::top_level_opened_from_getter(&d),
288                        HandlerSet::FromChangedEvent => Self::top_level_opened_from_changed_event(&d),
289                    }
290                }
291            }));
292        }
293
294        *handlers.0.borrow_mut() = subscriptions;
295    }
296
297    /// Calls `f` with the collection of the host, if the host is alive and has one. The handlers
298    /// of the logical tree, `Loaded` / `Unloaded`, `Initialized`, `DataContextChanged`,
299    /// `ResourcesChanged` and `ActualThemeVariantChanged` are the same in both sets.
300    fn with_existing_behaviors(host: &WeakRef<FerroObject>, f: fn(&BehaviorCollection)) {
301        let Some(d) = host.upgrade() else {
302            return;
303        };
304
305        if let Some(behaviors) = Self::get_existing_behaviors(&d) {
306            f(&behaviors);
307        }
308    }
309
310    // AttachedToVisualTree / DetachedFromVisualTree
311
312    fn visual_attached_to_visual_tree_from_getter(d: &Ref<FerroObject>) {
313        let behaviors = Self::get_existing_behaviors(d);
314        if let Some(behaviors) = &behaviors {
315            behaviors.attach(d);
316            behaviors.attached_to_visual_tree();
317        }
318    }
319
320    fn visual_detached_from_visual_tree_from_getter(d: &Ref<FerroObject>) {
321        let Some(behaviors) = Self::get_existing_behaviors(d) else {
322            return;
323        };
324
325        behaviors.detached_from_visual_tree();
326
327        if let Some(top_level) = d.cast::<TopLevel>() {
328            Self::schedule_top_level_behavior_detach(top_level, behaviors);
329        } else {
330            behaviors.detach();
331        }
332    }
333
334    fn visual_attached_to_visual_tree_from_changed_event(d: &Ref<FerroObject>) {
335        if let Some(behaviors) = Self::get_existing_behaviors(d) {
336            behaviors.attached_to_visual_tree();
337        }
338    }
339
340    fn visual_detached_from_visual_tree_from_changed_event(d: &Ref<FerroObject>) {
341        let Some(behaviors) = Self::get_existing_behaviors(d) else {
342            return;
343        };
344
345        behaviors.detached_from_visual_tree();
346
347        if let Some(top_level) = d.cast::<TopLevel>() {
348            Self::schedule_top_level_behavior_detach(top_level, behaviors);
349        }
350    }
351
352    fn schedule_top_level_behavior_detach(top_level: Ref<TopLevel>, behaviors: BehaviorCollection) {
353        Dispatcher::ui_thread().post_local(
354            move || {
355                if !top_level.is_attached_to_visual_tree()
356                    && top_level.get_value(Self::behaviors_property()).as_ref() == Some(&behaviors)
357                    && behaviors.associated_object().is_some()
358                {
359                    behaviors.detach();
360                }
361            },
362            DispatcherPriority::DEFAULT,
363        );
364    }
365
366    // TopLevel Opened
367
368    fn top_level_opened_from_getter(d: &Ref<FerroObject>) {
369        let behaviors = Self::get_existing_behaviors(d);
370        if let Some(behaviors) = &behaviors {
371            behaviors.attach(d);
372            behaviors.opened();
373        }
374    }
375
376    fn top_level_opened_from_changed_event(d: &Ref<FerroObject>) {
377        if let Some(behaviors) = Self::get_existing_behaviors(d) {
378            behaviors.opened();
379        }
380    }
381}