Skip to main content

ferroui_base/data/
compiled_binding.rs

1use crate::controls::INameScope;
2use crate::data::converters::IValueConverter;
3use crate::data::core::expression_nodes::ExpressionNode;
4use crate::data::core::parsers::ExpressionNodeFactory;
5use crate::data::compiled_binding_path::TypedPropertyElement;
6use crate::data::core::plugins::property_value_type;
7use crate::data::core::{BindingExpression, BindingExpressionOptions, TargetTypeConverter};
8use crate::data::{
9    BindingBase, BindingExpressionBase, BindingMode, BindingPriority, CompiledBindingPath, UpdateSourceTrigger,
10};
11use crate::utilities::CultureInfo;
12use crate::{BoxedValue, FerroObject, FerroProperty, Ref, StyledElement, WeakRef};
13use std::rc::{Rc, Weak};
14
15/// Resolves the binding mode and update source trigger defaults of a binding
16/// against the metadata of its target property.
17pub(crate) fn resolve_defaults_from_metadata(
18    mode: BindingMode,
19    update_source_trigger: UpdateSourceTrigger,
20    target: &FerroObject,
21    target_property: Option<&'static FerroProperty>,
22) -> (BindingMode, UpdateSourceTrigger) {
23    let trigger = if update_source_trigger == UpdateSourceTrigger::Default {
24        UpdateSourceTrigger::PropertyChanged
25    } else {
26        update_source_trigger
27    };
28    let mode = if mode == BindingMode::Default {
29        match target_property {
30            Some(p) => p.get_metadata_for(target).default_binding_mode(),
31            None => BindingMode::OneWay,
32        }
33    } else {
34        mode
35    };
36    (mode, trigger)
37}
38
39/// The data context source node for a binding to `target_property`.
40pub(crate) fn create_data_context_node(target_property: Option<&'static FerroProperty>) -> Rc<dyn ExpressionNode> {
41    ExpressionNodeFactory::create_data_context(target_property)
42}
43
44
45/// Declares the properties of a binding class: the fields (with interior
46/// mutability, as the binding is a mutable reference object), the getter and
47/// setter named after the property, and the chaining form of the setter used
48/// to configure a binding in one expression (`X::new(..).with_mode(..)`).
49macro_rules! binding_properties {
50    (
51        $(#[$struct_meta:meta])*
52        pub struct $name:ident {
53            $(
54                $(#[$meta:meta])*
55                $field:ident / $set:ident / $with:ident : $ty:ty = $default:expr,
56            )*
57        }
58    ) => {
59        $(#[$struct_meta])*
60        pub struct $name {
61            $($field: ::std::cell::RefCell<$ty>,)*
62        }
63
64        impl Default for $name {
65            fn default() -> Self {
66                Self { $($field: ::std::cell::RefCell::new($default),)* }
67            }
68        }
69
70        /// Compares by identity (reference equality), as the reference type
71        /// this mirrors.
72        impl PartialEq for $name {
73            #[inline]
74            fn eq(&self, other: &Self) -> bool {
75                ::std::ptr::eq(self, other)
76            }
77        }
78
79        impl $name {
80            $(
81                $(#[$meta])*
82                pub fn $field(&self) -> $ty {
83                    self.$field.borrow().clone()
84                }
85
86                pub fn $set(&self, value: $ty) {
87                    *self.$field.borrow_mut() = value;
88                }
89
90                /// The chaining form of the setter.
91                pub fn $with(self: ::std::rc::Rc<Self>, value: $ty) -> ::std::rc::Rc<Self> {
92                    self.$set(value);
93                    self
94                }
95            )*
96        }
97    };
98}
99pub(crate) use binding_properties;
100
101/// Whether an untyped binding value is the unset marker.
102pub(crate) fn is_unset(value: &Option<BoxedValue>) -> bool {
103    value.as_ref().is_some_and(|v| v.is::<crate::UnsetValueType>())
104}
105
106/// The source of a binding in the form the source nodes take: `None` when
107/// the source is the unset marker.
108pub(crate) fn explicit_source(source: Option<BoxedValue>) -> Option<Option<BoxedValue>> {
109    if is_unset(&source) {
110        None
111    } else {
112        Some(source)
113    }
114}
115
116binding_properties! {
117    /// A binding that uses compiled paths: typed accessors instead of member
118    /// lookup by name.
119    ///
120    /// A binding is a mutable reference object: it is created with
121    /// [`CompiledBinding::empty`] or [`CompiledBinding::new`], configured
122    /// property by property (or with the chaining `with_*` forms) and shared
123    /// as `Rc<CompiledBinding>`, which converts to `Rc<dyn BindingBase>`.
124    /// Handles compare by identity. The properties are read when the binding
125    /// is instantiated on a target: changing them afterwards affects later
126    /// instances only.
127    pub struct CompiledBinding {
128        /// The converter to use.
129        converter / set_converter / with_converter_value: Option<Rc<dyn IValueConverter>> = None,
130        /// The culture in which to evaluate the converter. `None` (the
131        /// default) uses the current culture.
132        converter_culture / set_converter_culture / with_converter_culture: Option<CultureInfo> = None,
133        /// A parameter to pass to the converter.
134        converter_parameter / set_converter_parameter / with_converter_parameter: Option<BoxedValue> = None,
135        /// The amount of time, in milliseconds, to wait before updating the
136        /// binding source after the value on the target changes.
137        delay / set_delay / with_delay: i32 = 0,
138        /// The value to use when the binding is unable to produce a value;
139        /// the unset marker for none.
140        fallback_value / set_fallback_value / with_fallback_value: Option<BoxedValue> = Some(FerroProperty::unset_value()),
141        /// The binding mode.
142        mode / set_mode / with_mode: BindingMode = BindingMode::Default,
143        /// The binding path.
144        path / set_path / with_path: Option<CompiledBindingPath> = None,
145        /// The binding priority.
146        priority / set_priority / with_priority: BindingPriority = BindingPriority::LocalValue,
147        /// The source for the binding; the unset marker (the default) binds
148        /// to the data context or to the element selected by the path.
149        source / set_source / with_source: Option<BoxedValue> = Some(FerroProperty::unset_value()),
150        /// The string format.
151        string_format / set_string_format / with_string_format: Option<String> = None,
152        /// The value to use when the binding produces null; the unset marker
153        /// for none.
154        target_null_value / set_target_null_value / with_target_null_value: Option<BoxedValue> = Some(FerroProperty::unset_value()),
155        /// What triggers updates of the binding source in two-way and
156        /// one-way-to-source bindings.
157        update_source_trigger / set_update_source_trigger / with_update_source_trigger: UpdateSourceTrigger = UpdateSourceTrigger::Default,
158        /// The anchor to use if the target is not an element.
159        default_anchor / set_default_anchor / with_default_anchor: Option<WeakRef<FerroObject>> = None,
160        /// The name scope of the place in markup where the binding was created.
161        name_scope / set_name_scope / with_name_scope: Option<Weak<dyn INameScope>> = None,
162    }
163}
164
165impl CompiledBinding {
166    /// Creates a binding without a path (the parameterless constructor).
167    pub fn empty() -> Rc<Self> {
168        Rc::new(Self::default())
169    }
170
171    /// Creates a binding with the given path.
172    pub fn new(path: CompiledBindingPath) -> Rc<Self> {
173        Self::empty().with_path(Some(path))
174    }
175
176    /// The chaining form of [`set_converter`](Self::set_converter), for a
177    /// converter.
178    pub fn with_converter(self: Rc<Self>, converter: Rc<dyn IValueConverter>) -> Rc<Self> {
179        self.with_converter_value(Some(converter))
180    }
181}
182
183impl CompiledBinding {
184    /// Decides whether the binding can be instantiated as a typed binding
185    /// expression, and returns the path element that creates it.
186    fn typed_element_for(
187        &self,
188        target: &FerroObject,
189        target_property: Option<&'static FerroProperty>,
190    ) -> Option<Rc<dyn TypedPropertyElement>> {
191        // A path with a single typed property element is needed.
192        let typed = self.path.borrow().as_ref()?.single_typed_element()?.clone();
193
194        // A data context binding is needed.
195        if !is_unset(&self.source.borrow()) {
196            return None;
197        }
198
199        // It cannot have a converter, fallback value, string format, target
200        // null value or a non-default update source trigger.
201        if self.converter.borrow().is_some()
202            || !is_unset(&self.fallback_value.borrow())
203            || self.string_format.borrow().is_some()
204            || !is_unset(&self.target_null_value.borrow())
205            || !matches!(
206                self.update_source_trigger(),
207                UpdateSourceTrigger::Default | UpdateSourceTrigger::PropertyChanged
208            )
209            || self.delay() != 0
210        {
211            return None;
212        }
213
214        // The value must be directly assignable to the target property.
215        let target_property = target_property?;
216        let target_type = property_value_type(target_property);
217        if !crate::data::core::ValueTypes::is_assignable(typed.value_type(), target_type) {
218            return None;
219        }
220
221        // The typed expression listens for data context changes of a styled
222        // element.
223        if !target.is::<StyledElement>() {
224            return None;
225        }
226
227        // Data context bindings read their source from the parent of the
228        // target, which the typed expression does not support.
229        if target_property.id() == StyledElement::data_context_property().id() {
230            return None;
231        }
232
233        // The typed expression does not support data validation.
234        if target_property.get_metadata_for(target).enable_data_validation() == Some(true) {
235            return None;
236        }
237
238        // For modes that write back to the source, the source property must
239        // be settable and the target value must be assignable back to the
240        // source property type.
241        let (mode, _) =
242            resolve_defaults_from_metadata(self.mode(), self.update_source_trigger(), target, Some(target_property));
243        if matches!(mode, BindingMode::TwoWay | BindingMode::OneWayToSource)
244            && (!typed.can_set() || !crate::data::core::ValueTypes::is_assignable(target_type, typed.value_type()))
245        {
246            return None;
247        }
248
249        Some(typed)
250    }
251}
252
253impl BindingBase for CompiledBinding {
254    fn as_any(&self) -> Option<&dyn std::any::Any> {
255        Some(self)
256    }
257
258    fn create_instance(
259        &self,
260        target: &FerroObject,
261        target_property: Option<&'static FerroProperty>,
262        anchor: Option<&Ref<FerroObject>>,
263    ) -> Rc<dyn BindingExpressionBase> {
264        if let Some(typed) = self.typed_element_for(target, target_property) {
265            // The update source trigger is constrained to property-changed
266            // by the check, so only the mode needs to be resolved here.
267            let (mode, _) =
268                resolve_defaults_from_metadata(self.mode(), self.update_source_trigger(), target, target_property);
269            return typed.create_expression(mode, self.priority());
270        }
271
272        let enable_data_validation =
273            target_property.is_some_and(|p| p.get_metadata_for(target).enable_data_validation() == Some(true));
274        let mut nodes: Vec<Rc<dyn ExpressionNode>> = Vec::new();
275        let path = self.path();
276        let is_rooted = path.as_ref().is_some_and(|p| p.build_expression(&mut nodes));
277        let explicit_source = explicit_source(self.source());
278
279        // If the binding isn't rooted (doesn't have a source or start with an
280        // element that selects one) a data context source node is needed.
281        if explicit_source.is_none() && !is_rooted {
282            nodes.insert(0, create_data_context_node(target_property));
283        }
284
285        // If the first node is a source node then allow it to select the
286        // source; otherwise use the binding source if specified, falling back
287        // to the target.
288        let target_ref = target.to_ref();
289        let default_anchor = self.default_anchor().as_ref().and_then(WeakRef::upgrade);
290        let anchor = anchor.or(default_anchor.as_ref());
291        let source = match nodes.first().and_then(|n| n.as_source_node()) {
292            Some(source_node) => source_node.select_source(explicit_source.as_ref(), &target_ref, anchor),
293            None => match &explicit_source {
294                Some(source) => source.clone(),
295                None => Some(Rc::new(target_ref.clone()) as BoxedValue),
296            },
297        };
298
299        let (mode, trigger) =
300            resolve_defaults_from_metadata(self.mode(), self.update_source_trigger(), target, target_property);
301
302        BindingExpression::new(
303            source,
304            nodes,
305            BindingExpressionOptions {
306                delay: self.delay(),
307                fallback_value: self.fallback_value(),
308                converter: self.converter(),
309                converter_culture: self.converter_culture(),
310                converter_parameter: self.converter_parameter(),
311                enable_data_validation,
312                mode,
313                priority: self.priority(),
314                string_format: self.string_format(),
315                target_null_value: self.target_null_value(),
316                target_property,
317                target_type_converter: Some(TargetTypeConverter::get_default_converter()),
318                update_source_trigger: trigger,
319            },
320        )
321    }
322}