Skip to main content

ferroui_xaml_interactivity/styled_element/
styled_element_behavior.rs

1//! Port of `StyledElement/StyledElementBehavior.cs`.
2
3use crate::helpers::TemplatedParentHelper;
4use crate::{DisposableAction, IBehavior, IBehaviorEventsHandler};
5use ferroui_base::reactive::{IDisposable, ObservableExt};
6use ferroui_base::{
7    ferro_class, ferro_properties, BoxedValue, ElementRef, FerroObject, FerroObjectExtensions, FerroObjectImpl,
8    FerroProperty, Nullable, Ref, StyledElement, StyledElementImpl, StyledProperty, WeakRef,
9};
10use ferroui_controls::primitives::FlyoutBase;
11use ferroui_controls::{Control, TopLevel};
12use std::cell::{Cell, RefCell};
13use std::rc::Rc;
14
15/// A base class for behaviors, implementing the basic plumbing of [`IBehavior`].
16///
17/// The behavior is a styled element: while its host is in the logical tree, the behavior is a
18/// logical child of the host (one way: it is not in the logical children of the host), so it is
19/// styled, resolves resources, follows the data context of its host and binds with `$parent`
20/// and `TemplateBinding`.
21///
22/// The class is abstract: deriving classes override the members of [`StyledElementBehaviorImpl`].
23/// See [`Behavior`](crate::Behavior) for the names of the hooks of the logical tree.
24#[repr(C)]
25pub struct StyledElementBehavior {
26    base: StyledElement,
27    data_context_disposable: RefCell<Option<Rc<dyn IDisposable>>>,
28    // Deviation (DEVIATIONS.md, Behaviors): the host is held weakly (the host holds its
29    // behaviors through its behavior collection).
30    associated_object: RefCell<Option<WeakRef<FerroObject>>>,
31    is_attached_to_logical_tree: Cell<bool>,
32    is_attached_to_visual_tree: Cell<bool>,
33    is_initialized_notified: Cell<bool>,
34    is_loaded: Cell<bool>,
35}
36
37ferro_class! {
38    StyledElementBehavior: StyledElement, virtuals StyledElementBehaviorImpl: StyledElementImpl {
39        /// Called after the behavior is attached to the associated object.
40        ///
41        /// Override this to hook up functionality to the associated object.
42        fn on_attached(this);
43        /// Called when the behavior is being detached from its associated object.
44        ///
45        /// Override this to unhook functionality from the associated object.
46        fn on_detaching(this);
47        /// Called after the associated object is attached to the visual tree.
48        ///
49        /// Invoked only when the associated object is a visual.
50        fn on_attached_to_visual_tree(this);
51        /// Called when the associated object is being detached from the visual tree.
52        ///
53        /// Invoked only when the associated object is a visual.
54        fn on_detached_from_visual_tree(this);
55        /// Called after the associated object is attached to the logical tree
56        /// (`OnAttachedToLogicalTree`).
57        ///
58        /// Invoked only when the associated object is a styled element.
59        fn on_attached_to_logical_tree_event(this);
60        /// Called when the associated object is being detached from the logical tree
61        /// (`OnDetachedFromLogicalTree`).
62        ///
63        /// Invoked only when the associated object is a styled element.
64        fn on_detached_from_logical_tree_event(this);
65        /// Called after the associated object is loaded.
66        ///
67        /// Invoked only when the associated object is a control.
68        fn on_loaded(this);
69        /// Called when the associated object is unloaded.
70        ///
71        /// Invoked only when the associated object is a control.
72        fn on_unloaded(this);
73        /// Called when the associated object is initialized.
74        ///
75        /// Invoked only when the associated object is a styled element.
76        fn on_initialized_event(this);
77        /// Called when the data context of the associated object changed.
78        ///
79        /// Invoked only when the associated object is a styled element.
80        fn on_data_context_changed_event(this);
81        /// Called when the resources of the associated object changed.
82        ///
83        /// Invoked only when the associated object is a styled element.
84        fn on_resources_changed_event(this);
85        /// Called when the actual theme variant of the associated object changed.
86        ///
87        /// Invoked only when the associated object is a styled element.
88        fn on_actual_theme_variant_changed_event(this);
89        /// Initializes the behavior (an internal member of the reference).
90        #[doc(hidden)]
91        fn initialize(this);
92        /// Makes the behavior a logical child of its host (an internal member of the reference).
93        #[doc(hidden)]
94        fn attach_behavior_to_logical_tree(this);
95        /// Removes the behavior from the logical tree (an internal member of the reference).
96        #[doc(hidden)]
97        fn detach_behavior_from_logical_tree(this);
98    }
99}
100
101ferroui_base::ferro_impl_classes!(StyledElementBehavior: FerroObjectImpl, StyledElementImpl);
102
103impl StyledElementBehaviorImpl for StyledElementBehavior {
104    fn on_attached(_this: &Self) {}
105
106    fn on_detaching(_this: &Self) {}
107
108    fn on_attached_to_visual_tree(_this: &Self) {}
109
110    fn on_detached_from_visual_tree(_this: &Self) {}
111
112    fn on_attached_to_logical_tree_event(_this: &Self) {}
113
114    fn on_detached_from_logical_tree_event(_this: &Self) {}
115
116    fn on_loaded(_this: &Self) {}
117
118    fn on_unloaded(_this: &Self) {}
119
120    fn on_initialized_event(_this: &Self) {}
121
122    fn on_data_context_changed_event(_this: &Self) {}
123
124    fn on_resources_changed_event(_this: &Self) {}
125
126    fn on_actual_theme_variant_changed_event(_this: &Self) {}
127
128    fn initialize(this: &Self) {
129        this.initialize_if_needed();
130    }
131
132    fn attach_behavior_to_logical_tree(this: &Self) {
133        let mut parent: Option<Ref<StyledElement>> = None;
134        let mut templated_parent: Option<Ref<FerroObject>> = None;
135
136        if let Some(associated_object) = this.associated_object() {
137            if let Some(top_level) = associated_object.cast::<TopLevel>() {
138                templated_parent = top_level.templated_parent();
139                parent = Some(top_level.upcast());
140            } else if let Some(flyout) = associated_object.downcast_ref::<FlyoutBase>() {
141                if let Some(target) = flyout.target() {
142                    templated_parent = target.templated_parent();
143                    parent = Some(target.upcast());
144                }
145            } else if let Some(styled_element) = associated_object.cast::<StyledElement>() {
146                if styled_element.parent().is_some() {
147                    templated_parent = styled_element.templated_parent();
148                    parent = Some(styled_element);
149                }
150            }
151        }
152
153        let Some(parent) = parent else {
154            return;
155        };
156
157        // Required for $parent binding in markup.
158        this.set_parent(&parent);
159
160        // Required for TemplateBinding in markup.
161        if templated_parent.is_some() {
162            TemplatedParentHelper::set_templated_parent(this, templated_parent);
163        }
164    }
165
166    fn detach_behavior_from_logical_tree(this: &Self) {
167        this.set_parent(None);
168
169        if this.templated_parent().is_some() {
170            TemplatedParentHelper::set_templated_parent(this, None);
171        }
172    }
173}
174
175ferro_properties! {
176    impl StyledElementBehavior {
177        /// Identifies the `IsEnabled` property.
178        pub fn is_enabled_property() -> StyledProperty<bool> {
179            FerroProperty::register::<StyledElementBehavior, _>("IsEnabled", true)
180        }
181    }
182}
183
184/// Removes the behavior from the logical tree when dropped, unless its host is a top level: the
185/// `finally` of the detach handlers of the reference, which runs when the hook panics too.
186struct DetachFromLogicalTree<'a>(&'a StyledElementBehavior);
187
188impl Drop for DetachFromLogicalTree<'_> {
189    fn drop(&mut self) {
190        if !self.0.associated_object().is_some_and(|associated_object| associated_object.is::<TopLevel>()) {
191            self.0.detach_behavior_from_logical_tree();
192        }
193    }
194}
195
196impl StyledElementBehavior {
197    /// The static constructor: the class implements the behavior contracts.
198    fn static_constructor() {
199        <dyn IBehavior>::register::<StyledElementBehavior>();
200        <dyn IBehaviorEventsHandler>::register::<StyledElementBehavior>();
201    }
202
203    /// Creates the class data; see [`FerroObject::construct`].
204    pub fn construct() -> Self {
205        Self {
206            base: StyledElement::construct(),
207            data_context_disposable: RefCell::new(None),
208            associated_object: RefCell::new(None),
209            is_attached_to_logical_tree: Cell::new(false),
210            is_attached_to_visual_tree: Cell::new(false),
211            is_initialized_notified: Cell::new(false),
212            is_loaded: Cell::new(false),
213        }
214    }
215
216    /// Gets the object to which the behavior is attached.
217    pub fn associated_object(&self) -> Option<Ref<FerroObject>> {
218        self.associated_object.borrow().as_ref().and_then(WeakRef::upgrade)
219    }
220
221    /// Gets the styled element to which this behavior is attached.
222    pub fn associated_styled_element(&self) -> Option<Ref<StyledElement>> {
223        self.associated_object().and_then(|associated_object| associated_object.cast::<StyledElement>())
224    }
225
226    /// Gets or sets a value indicating whether this instance is enabled.
227    pub fn is_enabled(&self) -> bool {
228        self.get_value(Self::is_enabled_property())
229    }
230
231    pub fn set_is_enabled(&self, value: bool) {
232        self.set_value(Self::is_enabled_property(), value)
233    }
234
235    /// Attaches the behavior to the specified object.
236    ///
237    /// # Panics
238    /// Panics if the behavior is attached to another object.
239    pub fn attach(&self, associated_object: impl Into<Nullable<FerroObject>>) {
240        let associated_object = associated_object.into().0;
241        let current = self.associated_object();
242        if associated_object == current {
243            return;
244        }
245
246        if current.is_some() {
247            panic!("An instance of a behavior cannot be attached to more than one object at a time.");
248        }
249
250        debug_assert!(associated_object.is_some(), "Cannot attach the behavior to a null object.");
251        let Some(associated_object) = associated_object else {
252            panic!("Value cannot be null. (Parameter 'associatedObject')");
253        };
254        *self.associated_object.borrow_mut() = Some(associated_object.downgrade());
255        let data_context_disposable = self.synchronize_data_context(&associated_object);
256        *self.data_context_disposable.borrow_mut() = data_context_disposable;
257
258        // NOTE: Special case handling for TopLevel as it does not trigger attached to logical or
259        // visual tree events.
260        if associated_object.is::<TopLevel>() {
261            self.attach_behavior_to_logical_tree();
262        }
263
264        self.on_attached();
265    }
266
267    /// Detaches the behaviors from the [`associated_object`](Self::associated_object).
268    pub fn detach(&self) {
269        self.on_detaching();
270
271        if self.parent().is_some() || self.templated_parent().is_some() {
272            self.detach_behavior_from_logical_tree();
273        }
274
275        let data_context_disposable = self.data_context_disposable.borrow_mut().take();
276        if let Some(data_context_disposable) = data_context_disposable {
277            data_context_disposable.dispose();
278        }
279        self.is_attached_to_logical_tree.set(false);
280        self.is_attached_to_visual_tree.set(false);
281        self.is_initialized_notified.set(false);
282        self.is_loaded.set(false);
283        *self.associated_object.borrow_mut() = None;
284    }
285
286    fn synchronize_data_context(&self, associated_object: &Ref<FerroObject>) -> Option<Rc<dyn IDisposable>> {
287        let this = self.to_ref().downgrade();
288        let data_context_property = StyledElement::data_context_property();
289
290        if let Some(styled_element) = associated_object.downcast_ref::<StyledElement>() {
291            // Setting the initial data context value immediately, so that bindings are
292            // available before the first data context change, is disabled in the reference
293            // (`#if false`); the subscription below sets it on subscription.
294
295            // Required for data context binding in markup.
296            return Some(styled_element.get_observable(data_context_property).subscribe_fn(
297                move |x: Option<BoxedValue>| {
298                    if let Some(this) = this.upgrade() {
299                        this.set_current_value(data_context_property, x);
300                    }
301                },
302            ));
303        }
304
305        if let Some(flyout) = associated_object.downcast_ref::<FlyoutBase>() {
306            let target_data_context_subscription: Rc<RefCell<Option<Rc<dyn IDisposable>>>> = Rc::new(RefCell::new(None));
307
308            let subscription = target_data_context_subscription.clone();
309            let target_changed = move |target: Option<Ref<Control>>| {
310                let Some(this) = this.upgrade() else {
311                    return;
312                };
313
314                let previous = subscription.borrow_mut().take();
315                if let Some(previous) = previous {
316                    previous.dispose();
317                }
318
319                if this.parent().is_some() || this.templated_parent().is_some() {
320                    this.detach_behavior_from_logical_tree();
321                }
322
323                let Some(target) = target else {
324                    this.set_current_value(data_context_property, None);
325                    return;
326                };
327
328                this.attach_behavior_to_logical_tree();
329                let behavior = this.downgrade();
330                let target_object: &FerroObject = &target;
331                let target_subscription = FerroObjectExtensions::get_observable(target_object, data_context_property)
332                    .subscribe_fn(move |x: Option<BoxedValue>| {
333                        if let Some(behavior) = behavior.upgrade() {
334                            behavior.set_current_value(data_context_property, x);
335                        }
336                    });
337                *subscription.borrow_mut() = Some(target_subscription);
338            };
339
340            let target_subscription = flyout
341                .get_observable(FlyoutBase::target_property())
342                .subscribe_fn(move |target: Option<ElementRef<Control>>| target_changed(ElementRef::resolve(&target)));
343
344            return Some(DisposableAction::create(move || {
345                target_subscription.dispose();
346                let target_data_context = target_data_context_subscription.borrow().clone();
347                if let Some(target_data_context) = target_data_context {
348                    target_data_context.dispose();
349                }
350            }));
351        }
352
353        None
354    }
355}
356
357impl IBehavior for StyledElementBehavior {
358    fn associated_object(&self) -> Option<Ref<FerroObject>> {
359        StyledElementBehavior::associated_object(self)
360    }
361
362    fn attach(&self, associated_object: Option<Ref<FerroObject>>) {
363        StyledElementBehavior::attach(self, associated_object)
364    }
365
366    fn detach(&self) {
367        StyledElementBehavior::detach(self)
368    }
369}
370
371impl IBehaviorEventsHandler for StyledElementBehavior {
372    fn attached_to_visual_tree_event_handler(&self) {
373        if self.is_attached_to_visual_tree.get() {
374            return;
375        }
376
377        self.is_attached_to_visual_tree.set(true);
378        self.attach_behavior_to_logical_tree();
379
380        self.on_attached_to_visual_tree();
381    }
382
383    fn detached_from_visual_tree_event_handler(&self) {
384        if !self.is_attached_to_visual_tree.get() {
385            return;
386        }
387
388        self.is_attached_to_visual_tree.set(false);
389        let _finally = DetachFromLogicalTree(self);
390        self.on_detached_from_visual_tree();
391    }
392
393    fn attached_to_logical_tree_event_handler(&self) {
394        if self.is_attached_to_logical_tree.get() {
395            return;
396        }
397
398        self.is_attached_to_logical_tree.set(true);
399        self.attach_behavior_to_logical_tree();
400
401        self.on_attached_to_logical_tree_event();
402    }
403
404    fn detached_from_logical_tree_event_handler(&self) {
405        if !self.is_attached_to_logical_tree.get() {
406            return;
407        }
408
409        self.is_attached_to_logical_tree.set(false);
410        let _finally = DetachFromLogicalTree(self);
411        self.on_detached_from_logical_tree_event();
412    }
413
414    fn loaded_event_handler(&self) {
415        if self.is_loaded.get() {
416            return;
417        }
418
419        self.is_loaded.set(true);
420        self.on_loaded();
421    }
422
423    fn unloaded_event_handler(&self) {
424        if !self.is_loaded.get() {
425            return;
426        }
427
428        self.is_loaded.set(false);
429        self.on_unloaded();
430    }
431
432    fn initialized_event_handler(&self) {
433        if self.is_initialized_notified.get() {
434            return;
435        }
436
437        self.is_initialized_notified.set(true);
438        self.initialize();
439
440        self.on_initialized_event();
441    }
442
443    fn data_context_changed_event_handler(&self) {
444        self.on_data_context_changed_event();
445    }
446
447    fn resources_changed_event_handler(&self) {
448        self.on_resources_changed_event();
449    }
450
451    fn actual_theme_variant_changed_event_handler(&self) {
452        self.on_actual_theme_variant_changed_event();
453    }
454}