ferroui_xaml_interactions/core/
data_trigger_behavior.rs1use 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#[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 pub fn binding_property() -> StyledProperty<Option<BoxedValue>> {
41 FerroProperty::register::<DataTriggerBehavior, _>("Binding", None)
42 }
43
44 pub fn comparison_condition_property() -> StyledProperty<ComparisonConditionType> {
46 FerroProperty::register::<DataTriggerBehavior, _>("ComparisonCondition", ComparisonConditionType::Equal)
47 }
48
49 pub fn value_property() -> StyledProperty<Option<BoxedValue>> {
51 FerroProperty::register::<DataTriggerBehavior, _>("Value", None)
52 }
53
54 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(¶meter_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(¶meter_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(¶meter_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
138fn parameter_of(change: &FerroPropertyChangedEventArgs<'_>) -> BoxedValue {
140 Rc::new(change.to_owned_args())
141}
142
143impl DataTriggerBehavior {
144 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 pub fn new() -> Ref<Self> {
157 instantiate(Self::construct())
158 }
159
160 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 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 pub fn value(&self) -> Option<BoxedValue> {
183 self.get_value(Self::value_property())
184 }
185
186 pub fn set_trigger_value(&self, value: Option<BoxedValue>) {
189 self.set_value(Self::value_property(), value)
190 }
191
192 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(¶meter));
213 },
214 DispatcherPriority::DEFAULT,
215 );
216 }
217
218 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 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(¶meter));
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(¶meter));
335 },
336 DispatcherPriority::DEFAULT,
337 );
338 }
339}