Skip to main content

ferroui_markup/markup/data/
delayed_binding.rs

1use ferroui_base::data::core::IPropertyInfo;
2use ferroui_base::data::{BindingBase, BindingError};
3use ferroui_base::logging::{LogArea, LogEventLevel, Logger};
4use ferroui_base::reactive::IDisposable;
5use ferroui_base::{BoxedValue, FerroObject, FerroProperty, Ref, StyledElement, WeakRef};
6use std::any::Any;
7use std::cell::RefCell;
8use std::rc::Rc;
9
10/// Provides delayed bindings for elements.
11///
12/// The markup engine applies its bindings in a delayed manner: bindings are
13/// only applied when an element has finished initializing. Applying bindings
14/// as soon as elements are created would mean that long-form bindings
15/// (binding objects whose properties are set one by one after creation)
16/// don't work, as the binding would be applied to the property before the
17/// properties of the binding object are set.
18pub struct DelayedBinding;
19
20enum Entry {
21    Binding { property: &'static FerroProperty, binding: Rc<dyn BindingBase> },
22    ClrPropertyValue { property: Rc<dyn IPropertyInfo>, value: Rc<dyn Fn(&StyledElement) -> Option<BoxedValue>> },
23}
24
25impl Entry {
26    fn apply(&self, control: &StyledElement) {
27        match self {
28            Entry::Binding { property, binding } => {
29                control.bind_binding(property, &**binding);
30            }
31            Entry::ClrPropertyValue { property, value } => {
32                let target: Ref<FerroObject> = control.to_ref().upcast();
33                if let Err(e) = property.set(&target, value(control).as_ref()) {
34                    if let Some(log) = Logger::try_get(LogEventLevel::Error, LogArea::PROPERTY) {
35                        let target_type = control.get_type().name();
36                        log.log_with_values(
37                            Some(control as &dyn Any),
38                            "Error setting {Property} on {Target}: {Exception}",
39                            &[&property.name(), &target_type, &e],
40                        );
41                    }
42                }
43            }
44        }
45    }
46}
47
48struct Pending {
49    target: WeakRef<StyledElement>,
50    entries: Vec<Entry>,
51    subscription: Option<Rc<dyn IDisposable>>,
52}
53
54ferroui_base::ferro_thread_local! {
55    /// The pending entries per element. Elements are held weakly.
56    static ENTRIES: RefCell<Vec<Pending>> = const { RefCell::new(Vec::new()) };
57}
58
59impl DelayedBinding {
60    /// Adds a delayed binding to an element: the binding is applied to
61    /// `property` when the element has finished initializing, or immediately
62    /// if it already has.
63    pub fn add(target: &StyledElement, property: &'static FerroProperty, binding: Rc<dyn BindingBase>) {
64        if target.is_initialized() {
65            target.bind_binding(property, &*binding);
66        } else {
67            Self::add_entry(target, Entry::Binding { property, binding });
68        }
69    }
70
71    /// Adds a delayed value to an element: `value` is evaluated and written
72    /// to `property` when the element has finished initializing, or
73    /// immediately if it already has.
74    ///
75    /// When the value is written immediately, an error from the property
76    /// setter is returned (the equivalent of the setter throwing to the
77    /// caller); when it is written later, the error is logged.
78    pub fn add_value(
79        target: &StyledElement,
80        property: Rc<dyn IPropertyInfo>,
81        value: impl Fn(&StyledElement) -> Option<BoxedValue> + 'static,
82    ) -> Result<(), BindingError> {
83        if target.is_initialized() {
84            let object: Ref<FerroObject> = target.to_ref().upcast();
85            property.set(&object, value(target).as_ref())
86        } else {
87            Self::add_entry(target, Entry::ClrPropertyValue { property, value: Rc::new(value) });
88            Ok(())
89        }
90    }
91
92    fn add_entry(target: &StyledElement, entry: Entry) {
93        let target_ref = target.to_ref();
94        let exists = ENTRIES.with(|entries| {
95            let mut entries = entries.borrow_mut();
96            // Drop the entries of elements that no longer exist.
97            entries.retain(|p| p.target.upgrade().is_some());
98            match entries.iter_mut().find(|p| p.target.points_to(&target_ref)) {
99                Some(pending) => {
100                    pending.entries.push(entry);
101                    None
102                }
103                None => Some(entry),
104            }
105        });
106
107        if let Some(entry) = exists {
108            ENTRIES.with(|entries| {
109                entries.borrow_mut().push(Pending {
110                    target: target_ref.downgrade(),
111                    entries: vec![entry],
112                    subscription: None,
113                })
114            });
115            let weak = target_ref.downgrade();
116            let subscription = target.initialized(move || {
117                if let Some(target) = weak.upgrade() {
118                    Self::apply_bindings(&target);
119                }
120            });
121            ENTRIES.with(|entries| {
122                if let Some(pending) = entries.borrow_mut().iter_mut().find(|p| p.target.points_to(&target_ref)) {
123                    pending.subscription = Some(subscription);
124                }
125            });
126        }
127    }
128
129    /// Applies any delayed bindings to an element.
130    pub fn apply_bindings(control: &StyledElement) {
131        let control_ref = control.to_ref();
132        let pending = ENTRIES.with(|entries| {
133            let mut entries = entries.borrow_mut();
134            let index = entries.iter().position(|p| p.target.points_to(&control_ref))?;
135            Some(entries.remove(index))
136        });
137
138        if let Some(pending) = pending {
139            for entry in &pending.entries {
140                entry.apply(control);
141            }
142            if let Some(subscription) = pending.subscription {
143                subscription.dispose();
144            }
145        }
146    }
147}