Skip to main content

ferroui_xaml_interactivity/triggers/
event_trigger_base.rs

1//! Port of `Triggers/EventTriggerBase.cs`.
2
3use crate::helpers::member_lookup::{find_plain_event, find_routed_event, object_of, subscribe_plain_event};
4use crate::{
5    AddEventHandlerRegistry, DisposableAction, Interaction, StyledElementBehaviorImpl, StyledElementBehaviorImplExt,
6    StyledElementTrigger,
7};
8use ferroui_base::data::core::{ValueTypes, WeakValue};
9use ferroui_base::interactivity::{IRoutedEventArgs, Interactive, RouteHandler};
10use ferroui_base::metadata::{MarkupDelegate, MarkupValue};
11use ferroui_base::reactive::IDisposable;
12use ferroui_base::utilities::EventArgs;
13use ferroui_base::{
14    ferro_class, ferro_class_info, ferro_properties, BoxedValue, FerroObjectImpl, FerroObjectImplExt, FerroProperty,
15    FerroPropertyChangedEventArgs, StyledElementImpl, StyledProperty, WeakRef,
16};
17use ferroui_controls::Control;
18use std::cell::{Cell, RefCell};
19use std::rc::Rc;
20
21const EVENT_NAME_DEFAULT_VALUE: &str = "AttachedToVisualTree";
22
23/// A behavior that listens for a specified event on its source and executes its actions when that
24/// event is fired.
25///
26/// The event is found by name: first in the [`AddEventHandlerRegistry`], then among the routed
27/// events of the source and the plain events its markup metadata declares (the reflection of the
28/// reference; see [`member_lookup`](crate::helpers::member_lookup)). The default event,
29/// `AttachedToVisualTree`, fires when a control without a parent is attached to the visual tree.
30#[repr(C)]
31pub struct EventTriggerBase {
32    base: StyledElementTrigger,
33    // Deviation (DEVIATIONS.md, Behaviors): the source is held weakly when it is an object (it is
34    // usually the host, which holds the trigger).
35    resolved_source: RefCell<Option<WeakValue>>,
36    event_handler: RefCell<Option<Rc<dyn IDisposable>>>,
37    is_loaded_event_registered: Cell<bool>,
38    loaded_event_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
39    try_register_event_handler_disposable: RefCell<Option<Rc<dyn IDisposable>>>,
40}
41
42ferro_class! {
43    EventTriggerBase: StyledElementTrigger, virtuals EventTriggerBaseImpl: StyledElementBehaviorImpl {
44        /// Raised when the event fires (by its name in the reference, the handler of the default
45        /// event `AttachedToVisualTree`).
46        fn attached_to_visual_tree(this, sender: Option<&BoxedValue>, event_args: &BoxedValue);
47        /// Executes the actions associated with this behavior.
48        fn on_event(this, event_args: Option<&BoxedValue>);
49    }
50}
51ferro_class_info!(EventTriggerBase {
52    markup: {
53        property_attributes: [
54            SourceObject: [ResolveByName],
55        ],
56    },
57});
58
59ferroui_base::ferro_impl_classes!(EventTriggerBase: StyledElementImpl);
60
61impl EventTriggerBaseImpl for EventTriggerBase {
62    fn attached_to_visual_tree(this: &Self, _sender: Option<&BoxedValue>, event_args: &BoxedValue) {
63        this.on_event(Some(event_args));
64    }
65
66    fn on_event(this: &Self, event_args: Option<&BoxedValue>) {
67        if !this.is_enabled() {
68            return;
69        }
70
71        let resolved_source = this.resolved_source();
72        Interaction::execute_actions(resolved_source.as_ref(), this.actions().as_ref(), event_args);
73    }
74}
75
76ferro_properties! {
77    impl EventTriggerBase {
78        /// Identifies the `EventName` property.
79        pub fn event_name_property() -> StyledProperty<Option<String>> {
80            FerroProperty::register::<EventTriggerBase, _>("EventName", Some(EVENT_NAME_DEFAULT_VALUE.to_string()))
81        }
82
83        /// Identifies the `SourceObject` property.
84        pub fn source_object_property() -> StyledProperty<Option<BoxedValue>> {
85            FerroProperty::register::<EventTriggerBase, _>("SourceObject", None)
86        }
87    }
88}
89
90ferroui_base::ferro_overrides! {
91    impl FerroObjectImpl for EventTriggerBase {
92        fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
93            Self::parent_on_property_changed(this, change);
94
95            if change.property() == Self::event_name_property().as_property() {
96                let resolved_source = this.resolved_source();
97                let old_event_name = change.get_old_value::<Option<String>>().flatten();
98                if let Some(old_event_name) = old_event_name {
99                    this.unregister_event(Some(&old_event_name), resolved_source.as_ref());
100                }
101
102                let new_event_name = change.get_new_value::<Option<String>>();
103                if let Some(new_event_name) = new_event_name {
104                    this.register_event(Some(&new_event_name), resolved_source.as_ref());
105                }
106            } else if change.property() == Self::source_object_property().as_property() {
107                this.set_resolved_source(this.event_name().as_deref(), this.source_object().or_else(|| this.associated_source()));
108            }
109        }
110    }
111}
112
113ferroui_base::ferro_overrides! {
114    impl StyledElementBehaviorImpl for EventTriggerBase {
115        /// Called after the behavior is attached to the associated object.
116        fn on_attached(this: &Self) {
117            Self::parent_on_attached(this);
118
119            this.set_resolved_source(this.event_name().as_deref(), this.source_object().or_else(|| this.associated_source()));
120        }
121
122        /// Called when the behavior is being detached from its associated object.
123        fn on_detaching(this: &Self) {
124            Self::parent_on_detaching(this);
125            this.set_resolved_source(this.event_name().as_deref(), None);
126        }
127    }
128}
129
130/// Whether two sources are the same object (`==` of the reference: reference equality).
131fn same_source(a: Option<&BoxedValue>, b: Option<&BoxedValue>) -> bool {
132    match (a, b) {
133        (None, None) => true,
134        (Some(a), Some(b)) => match (object_of(a), object_of(b)) {
135            (Some(a), Some(b)) => a.ptr_eq(&b),
136            (None, None) => ValueTypes::identity_equals(Some(a), Some(b)),
137            _ => false,
138        },
139        _ => false,
140    }
141}
142
143impl EventTriggerBase {
144    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
145    pub fn construct() -> Self {
146        Self {
147            base: StyledElementTrigger::construct(),
148            resolved_source: RefCell::new(None),
149            event_handler: RefCell::new(None),
150            is_loaded_event_registered: Cell::new(false),
151            loaded_event_subscription: RefCell::new(None),
152            try_register_event_handler_disposable: RefCell::new(None),
153        }
154    }
155
156    /// Gets or sets the name of the event to listen for.
157    pub fn event_name(&self) -> Option<String> {
158        self.get_value(Self::event_name_property())
159    }
160
161    pub fn set_event_name(&self, value: Option<String>) {
162        self.set_value(Self::event_name_property(), value)
163    }
164
165    /// Gets or sets the source object from which this behavior listens for events. If it is not
166    /// set, the source defaults to the associated object. Markup resolves a name given for it to
167    /// the named element.
168    pub fn source_object(&self) -> Option<BoxedValue> {
169        self.get_value(Self::source_object_property())
170    }
171
172    pub fn set_source_object(&self, value: Option<BoxedValue>) {
173        self.set_value(Self::source_object_property(), value)
174    }
175
176    /// The source the trigger listens to.
177    pub fn resolved_source(&self) -> Option<BoxedValue> {
178        self.resolved_source.borrow().as_ref().and_then(WeakValue::upgrade)
179    }
180
181    /// The associated object as an untyped value.
182    fn associated_source(&self) -> Option<BoxedValue> {
183        self.associated_object().map(|associated_object| Rc::new(associated_object) as BoxedValue)
184    }
185
186    fn set_resolved_source(&self, event_name: Option<&str>, new_source: Option<BoxedValue>) {
187        let resolved_source = self.resolved_source();
188        if same_source(resolved_source.as_ref(), new_source.as_ref()) {
189            return;
190        }
191
192        if resolved_source.is_some() {
193            self.unregister_event(event_name, resolved_source.as_ref());
194        }
195
196        *self.resolved_source.borrow_mut() = new_source.as_ref().map(WeakValue::new);
197
198        if new_source.is_some() {
199            self.register_event(event_name, new_source.as_ref());
200        }
201    }
202
203    fn register_event(&self, event_name: Option<&str>, resolved_source: Option<&BoxedValue>) {
204        let Some(event_name) = event_name.filter(|name| !name.is_empty()) else {
205            return;
206        };
207
208        if event_name != EVENT_NAME_DEFAULT_VALUE {
209            self.register_user_event(event_name, resolved_source);
210        } else if !self.is_loaded_event_registered.get() {
211            self.register_default_event(resolved_source);
212        }
213    }
214
215    fn unregister_event(&self, event_name: Option<&str>, resolved_source: Option<&BoxedValue>) {
216        let Some(event_name) = event_name.filter(|name| !name.is_empty()) else {
217            return;
218        };
219
220        if event_name != EVENT_NAME_DEFAULT_VALUE {
221            self.unregister_user_event(event_name, resolved_source);
222        } else if self.is_loaded_event_registered.get() {
223            self.unregister_default_event(resolved_source);
224        }
225    }
226
227    /// The handler of the event: calls [`attached_to_visual_tree`](Self::attached_to_visual_tree)
228    /// on the trigger while it is alive.
229    fn handler(&self) -> Rc<dyn Fn(Option<&BoxedValue>, &BoxedValue)> {
230        let this: WeakRef<EventTriggerBase> = self.to_ref().downgrade();
231        Rc::new(move |sender: Option<&BoxedValue>, event_args: &BoxedValue| {
232            if let Some(this) = this.upgrade() {
233                this.attached_to_visual_tree(sender, event_args);
234            }
235        })
236    }
237
238    fn register_user_event(&self, event_name: &str, resolved_source: Option<&BoxedValue>) {
239        let Some(resolved_source) = resolved_source else {
240            return;
241        };
242
243        let disposable = AddEventHandlerRegistry::try_register_event_handler(resolved_source, event_name, self.handler());
244        if let Some(disposable) = disposable {
245            *self.try_register_event_handler_disposable.borrow_mut() = Some(disposable);
246            return;
247        }
248
249        self.add_event_handler(event_name, resolved_source);
250    }
251
252    fn unregister_user_event(&self, _event_name: &str, resolved_source: Option<&BoxedValue>) {
253        let disposable = self.try_register_event_handler_disposable.borrow_mut().take();
254        if let Some(disposable) = disposable {
255            disposable.dispose();
256            return;
257        }
258
259        if self.event_handler.borrow().is_none() {
260            return;
261        }
262
263        let event_handler = self.event_handler.borrow_mut().take();
264        if resolved_source.is_some() {
265            if let Some(event_handler) = event_handler {
266                event_handler.dispose();
267            }
268        }
269    }
270
271    /// Subscribes to the event of the source by name: a routed event of the class of the source
272    /// (or of a base class), else a plain event its markup metadata declares (`GetRuntimeEvent`
273    /// and `EventInfo.AddEventHandler` of the reference). Does nothing when the source has no
274    /// such event.
275    fn add_event_handler(&self, event_name: &str, resolved_source: &BoxedValue) {
276        let handler = self.handler();
277
278        if let Some(interactive) = object_of(resolved_source).and_then(|object| object.cast::<Interactive>()) {
279            if let Some(routed_event) = find_routed_event(interactive.get_type(), event_name) {
280                let route_handler: RouteHandler = Rc::new(move |sender: &Interactive, event_args: &dyn IRoutedEventArgs| {
281                    let sender: BoxedValue = Rc::new(sender.to_ref());
282                    let event_args: BoxedValue = Rc::new(event_args.share());
283                    handler(Some(&sender), &event_args);
284                });
285                let token =
286                    interactive.add_route_handler(&routed_event, route_handler, Interactive::DEFAULT_ROUTES, false);
287                let interactive = interactive.downgrade();
288                *self.event_handler.borrow_mut() = Some(DisposableAction::create(move || {
289                    if let Some(interactive) = interactive.upgrade() {
290                        interactive.remove_handler(&routed_event, token);
291                    }
292                }));
293                return;
294            }
295        }
296
297        let Some(event_info) = find_plain_event(&**resolved_source, event_name) else {
298            return;
299        };
300
301        let delegate = MarkupDelegate::new(move |arguments: &[MarkupValue]| -> MarkupValue {
302            let (sender, event_args) = match arguments {
303                [] => (None, None),
304                [event_args] => (None, event_args.clone()),
305                [sender, event_args, ..] => (sender.clone(), event_args.clone()),
306            };
307            let event_args = event_args.unwrap_or_else(|| Rc::new(EventArgs::EMPTY) as BoxedValue);
308            handler(sender.as_ref(), &event_args);
309            None
310        });
311        *self.event_handler.borrow_mut() = subscribe_plain_event(event_info, resolved_source, delegate);
312    }
313
314    fn register_default_event(&self, resolved_source: Option<&BoxedValue>) {
315        let Some(element) = resolved_source.and_then(object_of).and_then(|object| object.cast::<Control>()) else {
316            return;
317        };
318
319        if !Self::is_element_loaded(&element) {
320            self.is_loaded_event_registered.set(true);
321            let handler = self.handler();
322            let sender = element.downgrade();
323            let subscription = element.attached_to_visual_tree(move |e| {
324                let sender: Option<BoxedValue> = sender.upgrade().map(|sender| Rc::new(sender) as BoxedValue);
325                let event_args: BoxedValue = Rc::new(e.clone());
326                handler(sender.as_ref(), &event_args);
327            });
328            *self.loaded_event_subscription.borrow_mut() = Some(subscription);
329        }
330    }
331
332    fn unregister_default_event(&self, resolved_source: Option<&BoxedValue>) {
333        self.is_loaded_event_registered.set(false);
334        let subscription = self.loaded_event_subscription.borrow_mut().take();
335        let is_control = resolved_source.and_then(object_of).is_some_and(|object| object.is::<Control>());
336        if let (true, Some(subscription)) = (is_control, subscription) {
337            subscription.dispose();
338        }
339    }
340
341    fn is_element_loaded(element: &Control) -> bool {
342        element.parent().is_some()
343    }
344}