Skip to main content

ferroui_xaml_interactions/core/
data_trigger_behavior.rs

1//! Port of `Core/DataTriggerBehavior.cs`.
2
3use super::ReversibleActionExecution;
4use ferroui_base::collections::NotifyCollectionChangedEventArgs;
5use ferroui_base::threading::{Dispatcher, DispatcherPriority};
6use ferroui_base::{
7    ferro_class, ferro_class_info, ferro_properties, instantiate, BoxedValue, FerroObject, FerroObjectImpl,
8    FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs, Ref, StyledElementImpl, StyledProperty,
9    UnsetValueType,
10};
11use ferroui_xaml_interactivity::helpers::ComparisonConditionTypeHelper;
12use ferroui_xaml_interactivity::{
13    ActionCollection, ComparisonConditionType, Interaction, StyledElementBehavior, StyledElementBehaviorImpl,
14    StyledElementBehaviorImplExt, StyledElementTrigger,
15};
16use std::cell::{Cell, RefCell};
17use std::rc::Rc;
18
19/// A behavior that performs actions when the bound data meets a specified condition.
20///
21/// The comparison of [`binding`](Self::binding) with [`value`](Self::value) runs in a job of the
22/// dispatcher after one of them changed. With [`revert_on_false`](Self::revert_on_false) the
23/// reversible actions are reverted when the condition stops being met.
24#[repr(C)]
25pub struct DataTriggerBehavior {
26    base: StyledElementTrigger,
27    is_condition_met: Cell<bool>,
28    has_condition_state: Cell<bool>,
29    applied_actions: RefCell<Vec<Ref<FerroObject>>>,
30    subscribed_actions: RefCell<Option<(ActionCollection, u64)>>,
31}
32
33ferro_class!(DataTriggerBehavior: StyledElementTrigger);
34ferro_class_info!(DataTriggerBehavior { new: DataTriggerBehavior::new });
35ferroui_base::ferro_impl_classes!(DataTriggerBehavior: StyledElementImpl);
36
37ferro_properties! {
38    impl DataTriggerBehavior {
39        /// Identifies the `Binding` property.
40        pub fn binding_property() -> StyledProperty<Option<BoxedValue>> {
41            FerroProperty::register::<DataTriggerBehavior, _>("Binding", None)
42        }
43
44        /// Identifies the `ComparisonCondition` property.
45        pub fn comparison_condition_property() -> StyledProperty<ComparisonConditionType> {
46            FerroProperty::register::<DataTriggerBehavior, _>("ComparisonCondition", ComparisonConditionType::Equal)
47        }
48
49        /// Identifies the `Value` property.
50        pub fn value_property() -> StyledProperty<Option<BoxedValue>> {
51            FerroProperty::register::<DataTriggerBehavior, _>("Value", None)
52        }
53
54        /// Identifies the `RevertOnFalse` property.
55        pub fn revert_on_false_property() -> StyledProperty<bool> {
56            FerroProperty::register::<DataTriggerBehavior, _>("RevertOnFalse", false)
57        }
58    }
59}
60
61ferroui_base::ferro_overrides! {
62    impl FerroObjectImpl for DataTriggerBehavior {
63        fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
64            Self::parent_on_property_changed(this, change);
65
66            let property = change.property();
67
68            if property == Self::binding_property().as_property() {
69                this.on_value_changed(change);
70            }
71
72            if property == Self::comparison_condition_property().as_property() {
73                this.on_value_changed(change);
74            }
75
76            if property == Self::value_property().as_property() {
77                this.on_value_changed(change);
78            }
79
80            if property == Self::revert_on_false_property().as_property() {
81                if change.get_old_value::<bool>().unwrap_or(false) && !change.get_new_value::<bool>() {
82                    this.revert_actions(Some(&parameter_of(change)));
83                }
84
85                this.has_condition_state.set(false);
86                this.on_value_changed(change);
87            }
88
89            if property == StyledElementBehavior::is_enabled_property().as_property() && this.revert_on_false() {
90                let is_enabled = change.get_new_value::<bool>();
91                if !is_enabled {
92                    this.revert_actions(Some(&parameter_of(change)));
93                }
94
95                this.has_condition_state.set(false);
96                if is_enabled {
97                    this.on_value_changed(change);
98                }
99            }
100
101            if property == StyledElementTrigger::actions_property().as_property() && this.associated_object().is_some() {
102                this.update_action_subscription(change.get_new_value::<Option<ActionCollection>>());
103                if this.revert_on_false() {
104                    this.revert_actions(Some(&parameter_of(change)));
105                    this.has_condition_state.set(false);
106                    this.on_value_changed(change);
107                }
108            }
109        }
110    }
111}
112
113ferroui_base::ferro_overrides! {
114    impl StyledElementBehaviorImpl for DataTriggerBehavior {
115        fn on_initialized_event(this: &Self) {
116            Self::parent_on_initialized_event(this);
117
118            this.execute(None);
119        }
120
121        fn on_attached(this: &Self) {
122            Self::parent_on_attached(this);
123            this.update_action_subscription(this.actions());
124        }
125
126        fn on_detaching(this: &Self) {
127            if this.revert_on_false() {
128                this.revert_actions(None);
129            }
130
131            this.has_condition_state.set(false);
132            this.update_action_subscription(None);
133            Self::parent_on_detaching(this);
134        }
135    }
136}
137
138/// The change as the parameter of the actions (the property changed event args of the reference).
139fn parameter_of(change: &FerroPropertyChangedEventArgs<'_>) -> BoxedValue {
140    Rc::new(change.to_owned_args())
141}
142
143impl DataTriggerBehavior {
144    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
145    pub fn construct() -> Self {
146        Self {
147            base: StyledElementTrigger::construct(),
148            is_condition_met: Cell::new(false),
149            has_condition_state: Cell::new(false),
150            applied_actions: RefCell::new(Vec::new()),
151            subscribed_actions: RefCell::new(None),
152        }
153    }
154
155    /// Creates a trigger.
156    pub fn new() -> Ref<Self> {
157        instantiate(Self::construct())
158    }
159
160    /// Gets or sets the bound object that the trigger will listen to. This is a property of the
161    /// object model.
162    pub fn binding(&self) -> Option<BoxedValue> {
163        self.get_value(Self::binding_property())
164    }
165
166    pub fn set_binding(&self, value: Option<BoxedValue>) {
167        self.set_value(Self::binding_property(), value)
168    }
169
170    /// Gets or sets the type of comparison to be performed between [`binding`](Self::binding) and
171    /// [`value`](Self::value). This is a property of the object model.
172    pub fn comparison_condition(&self) -> ComparisonConditionType {
173        self.get_value(Self::comparison_condition_property())
174    }
175
176    pub fn set_comparison_condition(&self, value: ComparisonConditionType) {
177        self.set_value(Self::comparison_condition_property(), value)
178    }
179
180    /// Gets or sets the value to be compared with the value of [`binding`](Self::binding). This is
181    /// a property of the object model.
182    pub fn value(&self) -> Option<BoxedValue> {
183        self.get_value(Self::value_property())
184    }
185
186    /// Sets the value to be compared (named apart from the setter of every property of the object,
187    /// as the condition of the object model names its own).
188    pub fn set_trigger_value(&self, value: Option<BoxedValue>) {
189        self.set_value(Self::value_property(), value)
190    }
191
192    /// Gets or sets a value indicating whether reversible actions should be reverted when the
193    /// condition becomes false. When false, the trigger matches the legacy semantics and only
194    /// executes actions when the condition is true.
195    pub fn revert_on_false(&self) -> bool {
196        self.get_value(Self::revert_on_false_property())
197    }
198
199    pub fn set_revert_on_false(&self, value: bool) {
200        self.set_value(Self::revert_on_false_property(), value)
201    }
202
203    fn on_value_changed(&self, change: &FerroPropertyChangedEventArgs<'_>) {
204        if change.sender().downcast_ref::<DataTriggerBehavior>().is_none() {
205            return;
206        }
207
208        let behavior: Ref<DataTriggerBehavior> = self.to_ref();
209        let parameter = parameter_of(change);
210        Dispatcher::ui_thread().post_local(
211            move || {
212                behavior.execute(Some(&parameter));
213            },
214            DispatcherPriority::DEFAULT,
215        );
216    }
217
218    /// The associated object as the sender of the actions.
219    fn associated_sender(&self) -> Option<BoxedValue> {
220        self.associated_object().map(|associated_object| Rc::new(associated_object) as BoxedValue)
221    }
222
223    fn execute(&self, parameter: Option<&BoxedValue>) {
224        let Some(associated_object) = self.associated_sender() else {
225            return;
226        };
227
228        if !self.is_enabled() {
229            return;
230        }
231
232        let binding = self.binding();
233        if !self.is_set(Self::binding_property())
234            || binding.as_ref().is_some_and(|binding| binding.is::<UnsetValueType>())
235        {
236            return;
237        }
238
239        if binding.is_none()
240            && !matches!(self.comparison_condition(), ComparisonConditionType::Equal | ComparisonConditionType::NotEqual)
241        {
242            return;
243        }
244
245        let compare = || {
246            ComparisonConditionTypeHelper::compare(binding.as_ref(), self.comparison_condition(), self.value().as_ref())
247                .unwrap_or_else(|error| panic!("{error}"))
248        };
249
250        if !self.revert_on_false() {
251            // Preserve legacy behavior: execute whenever condition evaluates true.
252            if compare() {
253                Interaction::execute_actions(Some(&associated_object), self.actions().as_ref(), parameter);
254            }
255
256            return;
257        }
258
259        let is_condition_met = compare();
260
261        if !self.has_condition_state.get() {
262            self.has_condition_state.set(true);
263            self.is_condition_met.set(is_condition_met);
264
265            if is_condition_met {
266                self.apply_actions(parameter);
267            }
268
269            return;
270        }
271
272        if self.is_condition_met.get() == is_condition_met {
273            return;
274        }
275
276        self.is_condition_met.set(is_condition_met);
277
278        if is_condition_met {
279            self.apply_actions(parameter);
280            return;
281        }
282
283        self.revert_actions(parameter);
284    }
285
286    fn revert_actions(&self, parameter: Option<&BoxedValue>) {
287        let Some(associated_object) = self.associated_sender() else {
288            return;
289        };
290
291        let applied_actions = std::mem::take(&mut *self.applied_actions.borrow_mut());
292        ReversibleActionExecution::revert(&applied_actions, Some(&associated_object), parameter);
293    }
294
295    fn apply_actions(&self, parameter: Option<&BoxedValue>) {
296        self.applied_actions.borrow_mut().clear();
297        let associated_object = self.associated_sender();
298        let applied = ReversibleActionExecution::execute(associated_object.as_ref(), self.actions().as_ref(), parameter);
299        self.applied_actions.borrow_mut().extend(applied);
300    }
301
302    fn update_action_subscription(&self, actions: Option<ActionCollection>) {
303        let previous = self.subscribed_actions.borrow_mut().take();
304        if let Some((subscribed_actions, token)) = previous {
305            subscribed_actions.remove_collection_changed(token);
306        }
307
308        let Some(actions) = actions else {
309            return;
310        };
311
312        let behavior = self.to_ref().downgrade();
313        let token = actions.add_collection_changed(Rc::new(
314            move |event_args: &NotifyCollectionChangedEventArgs<'_, Ref<FerroObject>>| {
315                if let Some(behavior) = behavior.upgrade() {
316                    behavior.actions_collection_changed(event_args);
317                }
318            },
319        ));
320        *self.subscribed_actions.borrow_mut() = Some((actions, token));
321    }
322
323    fn actions_collection_changed(&self, event_args: &NotifyCollectionChangedEventArgs<'_, Ref<FerroObject>>) {
324        if !self.revert_on_false() || self.associated_object().is_none() {
325            return;
326        }
327
328        let parameter: BoxedValue = Rc::new(event_args.share());
329        self.revert_actions(Some(&parameter));
330        self.has_condition_state.set(false);
331        let behavior: Ref<DataTriggerBehavior> = self.to_ref();
332        Dispatcher::ui_thread().post_local(
333            move || {
334                behavior.execute(Some(&parameter));
335            },
336            DispatcherPriority::DEFAULT,
337        );
338    }
339}