Skip to main content

ferroui_base/data/
compiled_binding_path.rs

1use crate::controls::NameScopeRef;
2use crate::data::core::expression_nodes::{
3    ArrayIndexerNode, CastTarget, ExpressionNode, FerroPropertyAccessorNode, FuncTransformNode,
4    LogicalAncestorElementNode, LogicalNotNode, MethodCommandNode, NamedElementNode, PropertyAccessorNode,
5    StreamNode, TemplatedParentNode, VisualAncestorElementNode,
6};
7use crate::data::core::plugins::{
8    IPropertyAccessorPlugin, IStreamPlugin, ObservableStreamPlugin, PropertyAccessorFactory,
9    PropertyInfoAccessorFactory, PropertyInfoAccessorPlugin, TaskStreamPlugin, UntypedAccessorPlugin,
10    AsNotifyPropertyChanged, UntypedCanExecute, UntypedCreateDelegate, UntypedExecute, UntypedGetter,
11    UntypedMember, UntypedSetter,
12};
13use crate::data::core::{
14    ClrPropertyInfo, IPropertyInfo, PropertyKind, TypedBindingExpression, TypedClrPropertyInfo, Value, ValueType,
15    ValueTypes, INDEXER_NAME,
16};
17use crate::data::{BindingError, BindingExpressionBase, BindingMode, BindingPriority};
18use crate::data::model::{BindableDictionary, BindableList, INotifyPropertyChanged};
19use crate::{AnyValue, BoxedValue, FerroProperty, ObjectType, PropertyValue, TypeInfo};
20use std::cell::RefCell;
21use std::fmt;
22use std::hash::Hash;
23use std::rc::Rc;
24
25type Execute = Rc<dyn Fn(&dyn AnyValue, Option<&BoxedValue>)>;
26type CanExecute = Rc<dyn Fn(&dyn AnyValue, Option<&BoxedValue>) -> bool>;
27type InpcLookup = Rc<dyn Fn(&dyn AnyValue) -> Option<&dyn INotifyPropertyChanged>>;
28type Cast = Rc<dyn Fn(Option<&BoxedValue>) -> Option<BoxedValue>>;
29
30/// An element of a compiled binding path.
31#[derive(Clone)]
32enum PathElement {
33    Not,
34    /// A registered property of an object of the class hierarchy.
35    FerroProperty { property: &'static FerroProperty, accepts_null: bool },
36    /// A plain property with typed accessors: eligible for the typed
37    /// binding expression.
38    TypedProperty {
39        property: Rc<dyn IPropertyInfo>,
40        accessor_factory: PropertyAccessorFactory,
41        accepts_null: bool,
42        typed: Rc<dyn TypedPropertyElement>,
43    },
44    /// A plain property, accessed through closures.
45    Property { property: Rc<dyn IPropertyInfo>, accessor_factory: PropertyAccessorFactory, accepts_null: bool },
46    /// A member accessed through closures over untyped values.
47    Untyped {
48        name: Box<str>,
49        kind: CompiledBindingPathElementKind,
50        property_type: Option<ValueType>,
51        plugin: Rc<dyn IPropertyAccessorPlugin>,
52        accepts_null: bool,
53    },
54    MethodAsCommand {
55        method_name: Box<str>,
56        execute: Execute,
57        can_execute: Option<CanExecute>,
58        depends_on_properties: Vec<Box<str>>,
59        inpc: Option<InpcLookup>,
60    },
61    Stream(Rc<dyn IStreamPlugin>),
62    SelfElement,
63    Ancestor { ancestor_type: Option<&'static TypeInfo>, level: usize },
64    VisualAncestor { ancestor_type: Option<&'static TypeInfo>, level: usize },
65    // Deviation (DEVIATIONS.md, Bindings): the name scope is held weakly. The
66    // managed original holds it, and its
67    // collector frees a path that is held by what the scope names: the
68    // binding of a setter of a style declared under a named element (or
69    // under an element with a named ancestor) is held by that element, and
70    // the scope holds the element. The node created from the path has
71    // always held the scope weakly, as a reflection binding holds its
72    // `NameScope`; a path whose scope is gone creates a node without one.
73    ElementName { name_scope: std::rc::Weak<dyn crate::controls::INameScope>, name: Box<str> },
74    TemplatedParent,
75    ArrayElement { indices: Vec<i32> },
76    TypeCast { type_name: &'static str, cast: Cast },
77}
78
79/// What an element of a compiled binding path is: the element classes of
80/// the managed original.
81#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
82pub enum CompiledBindingPathElementKind {
83    /// Negates the result of the path (`!`).
84    Not,
85    /// A property: a registered property, or a plain property (with typed
86    /// or untyped accessors).
87    Property,
88    /// A method as a delegate value.
89    MethodAsDelegate,
90    /// A method as a command.
91    MethodAsCommand,
92    /// A stream (`^`).
93    Stream,
94    /// The binding target itself (`$self`).
95    SelfElement,
96    /// An ancestor in the logical tree.
97    Ancestor,
98    /// An ancestor in the visual tree.
99    VisualAncestor,
100    /// A named element.
101    ElementName,
102    /// The templated parent.
103    TemplatedParent,
104    /// An array element.
105    ArrayElement,
106    /// A type cast.
107    TypeCast,
108}
109
110/// A read-only view of an element of a [`CompiledBindingPath`].
111#[derive(Clone, Copy)]
112pub struct CompiledBindingPathElement<'a>(&'a PathElement);
113
114impl<'a> CompiledBindingPathElement<'a> {
115    /// What the element is.
116    pub fn kind(&self) -> CompiledBindingPathElementKind {
117        use CompiledBindingPathElementKind as Kind;
118        match self.0 {
119            PathElement::Not => Kind::Not,
120            PathElement::FerroProperty { .. } | PathElement::TypedProperty { .. } | PathElement::Property { .. } => {
121                Kind::Property
122            }
123            PathElement::Untyped { kind, .. } => *kind,
124            PathElement::MethodAsCommand { .. } => Kind::MethodAsCommand,
125            PathElement::Stream(_) => Kind::Stream,
126            PathElement::SelfElement => Kind::SelfElement,
127            PathElement::Ancestor { .. } => Kind::Ancestor,
128            PathElement::VisualAncestor { .. } => Kind::VisualAncestor,
129            PathElement::ElementName { .. } => Kind::ElementName,
130            PathElement::TemplatedParent => Kind::TemplatedParent,
131            PathElement::ArrayElement { .. } => Kind::ArrayElement,
132            PathElement::TypeCast { .. } => Kind::TypeCast,
133        }
134    }
135
136    /// The name of the member a property or method element reads, of the
137    /// element a named-element element selects.
138    pub fn name(&self) -> Option<&'a str> {
139        match self.0 {
140            PathElement::FerroProperty { property, .. } => Some(property.name()),
141            PathElement::TypedProperty { property, .. } | PathElement::Property { property, .. } => {
142                Some(property.name())
143            }
144            PathElement::Untyped { name, .. } => Some(name),
145            PathElement::MethodAsCommand { method_name, .. } => Some(method_name),
146            PathElement::ElementName { name, .. } => Some(name),
147            _ => None,
148        }
149    }
150
151    /// The description of the property a property element reads (the
152    /// `Property` of the managed original). `None` for other elements and
153    /// for a property that only has untyped accessors (its name and type
154    /// are [`name`](Self::name) and [`property_type`](Self::property_type)).
155    pub fn property(&self) -> Option<Rc<dyn IPropertyInfo>> {
156        match self.0 {
157            PathElement::FerroProperty { property, .. } => Some(property.as_property_info()),
158            PathElement::TypedProperty { property, .. } | PathElement::Property { property, .. } => {
159                Some(property.clone())
160            }
161            _ => None,
162        }
163    }
164
165    /// The type of the property a property element reads.
166    pub fn property_type(&self) -> Option<ValueType> {
167        match self.0 {
168            PathElement::Untyped { property_type, .. } => *property_type,
169            _ => self.property().map(|property| property.property_type()),
170        }
171    }
172
173    /// Whether the element is a property with typed accessors: the one a
174    /// path must consist of to be instantiated as a typed binding
175    /// expression.
176    pub fn is_typed_property(&self) -> bool {
177        matches!(self.0, PathElement::TypedProperty { .. })
178    }
179
180    /// Whether a null-conditional operator applies to the access.
181    pub fn accepts_null(&self) -> bool {
182        match self.0 {
183            PathElement::FerroProperty { accepts_null, .. }
184            | PathElement::TypedProperty { accepts_null, .. }
185            | PathElement::Property { accepts_null, .. }
186            | PathElement::Untyped { accepts_null, .. } => *accepts_null,
187            _ => false,
188        }
189    }
190
191    /// The ancestor type and level of an ancestor element.
192    pub fn ancestor(&self) -> Option<(Option<&'static TypeInfo>, usize)> {
193        match self.0 {
194            PathElement::Ancestor { ancestor_type, level } | PathElement::VisualAncestor { ancestor_type, level } => {
195                Some((*ancestor_type, *level))
196            }
197            _ => None,
198        }
199    }
200
201    /// The indexes of an array element.
202    pub fn array_indices(&self) -> Option<&'a [i32]> {
203        match self.0 {
204            PathElement::ArrayElement { indices } => Some(indices),
205            _ => None,
206        }
207    }
208}
209
210/// A path element that can create a typed binding expression.
211pub(crate) trait TypedPropertyElement {
212    fn value_type(&self) -> ValueType;
213    fn can_set(&self) -> bool;
214    fn create_expression(&self, mode: BindingMode, priority: BindingPriority) -> Rc<dyn BindingExpressionBase>;
215}
216
217impl<S: PartialEq + 'static, V: PropertyValue> TypedPropertyElement for Rc<TypedClrPropertyInfo<S, V>> {
218    fn value_type(&self) -> ValueType {
219        ValueType::of::<V>()
220    }
221
222    fn can_set(&self) -> bool {
223        IPropertyInfo::can_set(&**self)
224    }
225
226    fn create_expression(&self, mode: BindingMode, priority: BindingPriority) -> Rc<dyn BindingExpressionBase> {
227        TypedBindingExpression::new(self.clone(), mode, priority)
228    }
229}
230
231/// The path of a compiled binding: a sequence of typed accessors, built
232/// without any lookup by name.
233///
234/// A path is a reference object: clones refer to the same path and compare
235/// by identity.
236#[derive(Clone)]
237pub struct CompiledBindingPath {
238    elements: Rc<[PathElement]>,
239}
240
241impl Default for CompiledBindingPath {
242    fn default() -> Self {
243        Self::new()
244    }
245}
246
247impl PartialEq for CompiledBindingPath {
248    fn eq(&self, other: &Self) -> bool {
249        Rc::ptr_eq(&self.elements, &other.elements)
250    }
251}
252
253impl CompiledBindingPath {
254    /// An empty path: binds to the source itself.
255    pub fn new() -> Self {
256        Self { elements: Rc::new([]) }
257    }
258
259    /// Builds the expression nodes of the path. Returns whether the path is
260    /// rooted: starts with an element that selects the binding source.
261    pub(crate) fn build_expression(&self, result: &mut Vec<Rc<dyn ExpressionNode>>) -> bool {
262        let mut negated = 0;
263        let mut is_rooted = false;
264
265        for element in self.elements.iter() {
266            let node: Option<Rc<dyn ExpressionNode>> = match element {
267                PathElement::Not => {
268                    negated += 1;
269                    None
270                }
271                PathElement::FerroProperty { property, accepts_null } => {
272                    Some(FerroPropertyAccessorNode::new(property, *accepts_null))
273                }
274                PathElement::Property { property, accessor_factory, accepts_null }
275                | PathElement::TypedProperty { property, accessor_factory, accepts_null, .. } => Some(PropertyAccessorNode::new(
276                    property.name(),
277                    Rc::new(PropertyInfoAccessorPlugin::new(property.clone(), accessor_factory.clone())),
278                    *accepts_null,
279                )),
280                PathElement::MethodAsCommand { method_name, execute, can_execute, depends_on_properties, inpc } => {
281                    let depends: Vec<&str> = depends_on_properties.iter().map(|s| &**s).collect();
282                    Some(MethodCommandNode::new(
283                        method_name,
284                        execute.clone(),
285                        can_execute.clone(),
286                        &depends,
287                        inpc.clone(),
288                    ))
289                }
290                PathElement::Untyped { name, plugin, accepts_null, .. } => {
291                    Some(PropertyAccessorNode::new(name, plugin.clone(), *accepts_null))
292                }
293                PathElement::ArrayElement { indices } => Some(ArrayIndexerNode::new(indices.clone())),
294                PathElement::VisualAncestor { ancestor_type, level } => {
295                    is_rooted = true;
296                    Some(VisualAncestorElementNode::new(*ancestor_type, *level))
297                }
298                PathElement::Ancestor { ancestor_type, level } => {
299                    is_rooted = true;
300                    Some(LogicalAncestorElementNode::new(*ancestor_type, *level))
301                }
302                PathElement::SelfElement => {
303                    is_rooted = true;
304                    None
305                }
306                PathElement::ElementName { name_scope, name } => {
307                    is_rooted = true;
308                    let name_scope = name_scope.upgrade().map(NameScopeRef);
309                    Some(NamedElementNode::new(name_scope.as_ref(), name))
310                }
311                PathElement::Stream(plugin) => Some(StreamNode::new(plugin.clone())),
312                PathElement::TypeCast { cast, .. } => Some(FuncTransformNode::new(cast.clone())),
313                PathElement::TemplatedParent => {
314                    is_rooted = true;
315                    Some(TemplatedParentNode::new())
316                }
317            };
318            if let Some(node) = node {
319                result.push(node);
320            }
321        }
322
323        for _ in 0..negated {
324            result.push(LogicalNotNode::new());
325        }
326        is_rooted
327    }
328
329    /// The typed element of the path, if the path consists of exactly one
330    /// property with typed accessors.
331    pub(crate) fn single_typed_element(&self) -> Option<&Rc<dyn TypedPropertyElement>> {
332        match &*self.elements {
333            [PathElement::TypedProperty { typed, .. }] => Some(typed),
334            _ => None,
335        }
336    }
337
338    /// The elements of the path, in order (the `Elements` of the managed
339    /// original, as a read-only view).
340    pub fn elements(&self) -> impl ExactSizeIterator<Item = CompiledBindingPathElement<'_>> + '_ {
341        self.elements.iter().map(CompiledBindingPathElement)
342    }
343
344    /// The element at `index`.
345    pub fn element(&self, index: usize) -> Option<CompiledBindingPathElement<'_>> {
346        self.elements.get(index).map(CompiledBindingPathElement)
347    }
348
349    /// The number of elements in the path.
350    pub fn len(&self) -> usize {
351        self.elements.len()
352    }
353
354    pub fn is_empty(&self) -> bool {
355        self.elements.is_empty()
356    }
357}
358
359impl fmt::Display for CompiledBindingPath {
360    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
361        let mut first = true;
362        for element in self.elements.iter() {
363            let text = match element {
364                PathElement::Not => "!".to_string(),
365                PathElement::FerroProperty { property, .. } => {
366                    format!("{}{}", if first { "" } else { "." }, property.name())
367                }
368                PathElement::Property { property, .. } | PathElement::TypedProperty { property, .. } => {
369                    format!("{}{}", if first { "" } else { "." }, property.name())
370                }
371                PathElement::MethodAsCommand { method_name, .. } => {
372                    format!("{}{}()", if first { "" } else { "." }, method_name)
373                }
374                PathElement::Untyped { name, kind, .. } => {
375                    let is_method = *kind != CompiledBindingPathElementKind::Property;
376                    format!("{}{}{}", if first { "" } else { "." }, name, if is_method { "()" } else { "" })
377                }
378                PathElement::Stream(_) => "^".to_string(),
379                PathElement::SelfElement => "$self".to_string(),
380                PathElement::Ancestor { ancestor_type, level } => {
381                    format!("$parent[{}, {}]", ancestor_type.map_or("", |t| t.name()), level)
382                }
383                PathElement::VisualAncestor { ancestor_type, level } => {
384                    format!("$visualParent[{}, {}]", ancestor_type.map_or("", |t| t.name()), level)
385                }
386                PathElement::ElementName { name, .. } => format!("#{name}"),
387                PathElement::TemplatedParent => "$templatedParent".to_string(),
388                PathElement::ArrayElement { indices } => {
389                    format!("[{}]", indices.iter().map(i32::to_string).collect::<Vec<_>>().join(","))
390                }
391                PathElement::TypeCast { type_name, .. } => format!("({type_name})"),
392            };
393            f.write_str(&text)?;
394            first = matches!(element, PathElement::Not);
395        }
396        Ok(())
397    }
398}
399
400/// Builds a [`CompiledBindingPath`].
401///
402/// The builder is a reference object: a shared handle to the elements
403/// collected so far. Every method adds an element and returns the builder
404/// (another handle to it), so that calls can be chained, as well as made one
405/// by one through any clone of the handle. Handles compare by identity.
406#[derive(Clone, Default)]
407pub struct CompiledBindingPathBuilder {
408    elements: Rc<RefCell<Vec<PathElement>>>,
409}
410
411impl PartialEq for CompiledBindingPathBuilder {
412    fn eq(&self, other: &Self) -> bool {
413        Rc::ptr_eq(&self.elements, &other.elements)
414    }
415}
416
417impl CompiledBindingPathBuilder {
418    pub fn new() -> Self {
419        Self::default()
420    }
421
422    /// Negates the result of the path.
423    pub fn not(&self) -> Self {
424        self.elements.borrow_mut().push(PathElement::Not);
425        self.clone()
426    }
427
428    /// A registered property of an object of the class hierarchy.
429    pub fn ferro_property(&self, property: &'static FerroProperty) -> Self {
430        self.elements.borrow_mut().push(PathElement::FerroProperty { property, accepts_null: false });
431        self.clone()
432    }
433
434    /// A registered property of an object of the class hierarchy.
435    /// `accepts_null` applies a null-conditional operator to the access: a
436    /// null owner produces null instead of an error.
437    pub fn ferro_property_with(&self, property: &'static FerroProperty, accepts_null: bool) -> Self {
438        self.elements.borrow_mut().push(PathElement::FerroProperty { property, accepts_null });
439        self.clone()
440    }
441
442    /// A plain property, from its description and the factory of its
443    /// accessor.
444    pub fn property(&self, info: Rc<dyn IPropertyInfo>, accessor_factory: PropertyAccessorFactory) -> Self {
445        self.property_with(info, accessor_factory, false)
446    }
447
448    /// A plain property. `accepts_null` applies a null-conditional operator
449    /// to the access: a null owner produces null instead of an error.
450    pub fn property_with(
451        &self,
452        info: Rc<dyn IPropertyInfo>,
453        accessor_factory: PropertyAccessorFactory,
454        accepts_null: bool,
455    ) -> Self {
456        self.elements.borrow_mut().push(PathElement::Property { property: info, accessor_factory, accepts_null });
457        self.clone()
458    }
459
460    /// A property with typed accessors on a model type that raises property
461    /// change notifications. A binding whose path is this single element,
462    /// bound to a target property of exactly type `V`, reads and writes
463    /// values without boxing them.
464    pub fn typed_property<S: INotifyPropertyChanged + PartialEq + 'static, V: PropertyValue>(
465        &self,
466        name: &str,
467        get: impl Fn(&S) -> V + 'static,
468        set: Option<Rc<dyn Fn(&S, V)>>,
469    ) -> Self {
470        ValueTypes::register_reference::<S>();
471        let info = Rc::new(TypedClrPropertyInfo::<S, V>::new(name, Some(Rc::new(get)), set).notifying());
472        self.elements.borrow_mut().push(PathElement::TypedProperty {
473            property: info.clone(),
474            accessor_factory: PropertyInfoAccessorFactory::create_inpc_property_accessor::<S>(),
475            accepts_null: false,
476            typed: Rc::new(info),
477        });
478        self.clone()
479    }
480
481    /// A read-write property of a model type that raises property change
482    /// notifications, from typed accessors.
483    pub fn notifying_property<O: INotifyPropertyChanged + PartialEq + 'static, K: PropertyKind>(
484        &self,
485        name: &str,
486        get: impl Fn(&O) -> K::Typed + 'static,
487        set: impl Fn(&O, K::Typed) + 'static,
488    ) -> Self {
489        ValueTypes::register_reference::<O>();
490        self.property(
491            Rc::new(ClrPropertyInfo::read_write::<O, K>(name, get, set)),
492            PropertyInfoAccessorFactory::create_inpc_property_accessor::<O>(),
493        )
494    }
495
496    /// A read-only property of a model type that raises property change
497    /// notifications, from a typed getter.
498    pub fn notifying_read_only_property<O: INotifyPropertyChanged + PartialEq + 'static, K: PropertyKind>(
499        &self,
500        name: &str,
501        get: impl Fn(&O) -> K::Typed + 'static,
502    ) -> Self {
503        ValueTypes::register_reference::<O>();
504        self.property(
505            Rc::new(ClrPropertyInfo::read_only::<O, K>(name, get)),
506            PropertyInfoAccessorFactory::create_inpc_property_accessor::<O>(),
507        )
508    }
509
510    /// A method used as a command.
511    pub fn command<O: 'static>(
512        &self,
513        method_name: &str,
514        execute: impl Fn(&O, Option<&BoxedValue>) + 'static,
515        can_execute: Option<Rc<dyn Fn(&O, Option<&BoxedValue>) -> bool>>,
516        depends_on_properties: &[&str],
517    ) -> Self {
518        self.elements.borrow_mut().push(PathElement::MethodAsCommand {
519            method_name: method_name.into(),
520            execute: Rc::new(move |o, p| {
521                if let Some(o) = o.downcast_ref::<O>() {
522                    execute(o, p)
523                }
524            }),
525            can_execute: can_execute.map(|f| {
526                Rc::new(move |o: &dyn AnyValue, p: Option<&BoxedValue>| o.downcast_ref::<O>().is_some_and(|o| f(o, p)))
527                    as CanExecute
528            }),
529            depends_on_properties: depends_on_properties.iter().map(|s| (*s).into()).collect(),
530            inpc: None,
531        });
532        self.clone()
533    }
534
535    /// A method used as a command on an owner that raises property change
536    /// notifications: the command's state is re-queried when one of
537    /// `depends_on_properties` changes.
538    pub fn notifying_command<O: INotifyPropertyChanged + 'static>(
539        &self,
540        method_name: &str,
541        execute: impl Fn(&O, Option<&BoxedValue>) + 'static,
542        can_execute: Option<Rc<dyn Fn(&O, Option<&BoxedValue>) -> bool>>,
543        depends_on_properties: &[&str],
544    ) -> Self {
545        self.command::<O>(method_name, execute, can_execute, depends_on_properties);
546        if let Some(PathElement::MethodAsCommand { inpc, .. }) = self.elements.borrow_mut().last_mut() {
547            *inpc = Some(Rc::new(|v: &dyn AnyValue| v.downcast_ref::<O>().map(|o| o as &dyn INotifyPropertyChanged)));
548        }
549        self.clone()
550    }
551
552    /// Streams the outcome of a task (`^`).
553    pub fn stream_task(&self) -> Self {
554        self.elements.borrow_mut().push(PathElement::Stream(Rc::new(TaskStreamPlugin)));
555        self.clone()
556    }
557
558    /// Streams the values of an observable (`^`).
559    pub fn stream_observable(&self) -> Self {
560        self.elements.borrow_mut().push(PathElement::Stream(Rc::new(ObservableStreamPlugin)));
561        self.clone()
562    }
563
564    /// Streams values through a custom stream plugin.
565    pub fn stream(&self, plugin: Rc<dyn IStreamPlugin>) -> Self {
566        self.elements.borrow_mut().push(PathElement::Stream(plugin));
567        self.clone()
568    }
569
570    /// Roots the path at the binding target itself (`$self`).
571    pub fn self_(&self) -> Self {
572        self.elements.borrow_mut().push(PathElement::SelfElement);
573        self.clone()
574    }
575
576    /// Roots the path at an ancestor in the logical tree.
577    pub fn ancestor(&self, ancestor_type: Option<&'static TypeInfo>, level: usize) -> Self {
578        self.elements.borrow_mut().push(PathElement::Ancestor { ancestor_type, level });
579        self.clone()
580    }
581
582    /// Roots the path at an ancestor in the visual tree.
583    pub fn visual_ancestor(&self, ancestor_type: Option<&'static TypeInfo>, level: usize) -> Self {
584        self.elements.borrow_mut().push(PathElement::VisualAncestor { ancestor_type, level });
585        self.clone()
586    }
587
588    /// Roots the path at a named element.
589    pub fn element_name(&self, name_scope: NameScopeRef, name: &str) -> Self {
590        let name_scope = std::rc::Rc::downgrade(&name_scope.0);
591        self.elements.borrow_mut().push(PathElement::ElementName { name_scope, name: name.into() });
592        self.clone()
593    }
594
595    /// An indexer with one integer argument (`owner[index]`) on owner type
596    /// `O`, from typed accessors that fail for a bad index. The value is
597    /// re-read when the owner raises a collection change that affects
598    /// `index`, or a property change for the indexer.
599    pub fn indexer_property<O: 'static, K: PropertyKind>(
600        &self,
601        index: i32,
602        get: impl Fn(&O, i32) -> Result<K::Typed, BindingError> + 'static,
603        set: Option<Rc<dyn Fn(&O, i32, K::Typed) -> Result<(), BindingError>>>,
604    ) -> Self {
605        let info = match set {
606            Some(set) => ClrPropertyInfo::read_write_fallible::<O, K>(
607                INDEXER_NAME,
608                move |o| get(o, index),
609                move |o, v| set(o, index, v),
610            ),
611            None => ClrPropertyInfo::read_only_fallible::<O, K>(INDEXER_NAME, move |o| get(o, index)),
612        };
613        self.property(Rc::new(info), PropertyInfoAccessorFactory::create_indexer_property_accessor(index))
614    }
615
616    /// The item at `index` of a [`BindableList`] (`list[index]`).
617    pub fn list_item<T: PropertyValue>(&self, index: i32) -> Self {
618        fn position<T: PropertyValue>(list: &BindableList<T>, index: i32) -> Result<usize, BindingError> {
619            usize::try_from(index).ok().filter(|i| *i < list.items().count()).ok_or_else(|| {
620                BindingError::message(
621                    "Index was out of range. Must be non-negative and less than the size of the collection. (Parameter 'index')",
622                )
623            })
624        }
625        self.indexer_property::<BindableList<T>, Value<T>>(
626            index,
627            |list, index| {
628                let position = position(list, index)?;
629                list.items().try_get(position).ok_or_else(|| BindingError::message("Index was out of range."))
630            },
631            Some(Rc::new(|list, index, value| {
632                let position = position(list, index)?;
633                list.items().set(position, value);
634                Ok(())
635            })),
636        )
637    }
638
639    /// The entry for `key` of a [`BindableDictionary`] (`dictionary[key]`).
640    /// Reading a missing key is an error; writing adds the entry.
641    pub fn dictionary_item<K: Eq + Hash + Clone + fmt::Display + 'static, V: PropertyValue>(&self, key: K) -> Self {
642        // An integer key is an indexer with one integer argument.
643        let index = (&key as &dyn std::any::Any).downcast_ref::<i32>().copied();
644        let set_key = key.clone();
645        let info = ClrPropertyInfo::read_write_fallible::<BindableDictionary<K, V>, Value<V>>(
646            INDEXER_NAME,
647            move |dictionary| {
648                dictionary.items().try_get_value(&key).ok_or_else(|| {
649                    BindingError::message(format!("The given key '{key}' was not present in the dictionary."))
650                })
651            },
652            move |dictionary, value| {
653                dictionary.items().set(set_key.clone(), value);
654                Ok(())
655            },
656        );
657        let accessor_factory = match index {
658            Some(index) => PropertyInfoAccessorFactory::create_indexer_property_accessor(index),
659            None => PropertyInfoAccessorFactory::create_inpc_property_accessor::<BindableDictionary<K, V>>(),
660        };
661        self.property(Rc::new(info), accessor_factory)
662    }
663
664    /// Indexes an array with integer indexes, one per dimension.
665    pub fn array_element(&self, indices: &[i32]) -> Self {
666        self.elements.borrow_mut().push(PathElement::ArrayElement { indices: indices.to_vec() });
667        self.clone()
668    }
669
670    /// Casts to class `T`: passes an object on if it is a `T`, else null.
671    pub fn type_cast<T: ObjectType>(&self) -> Self {
672        self.elements.borrow_mut().push(PathElement::TypeCast {
673            type_name: T::TYPE.name(),
674            cast: Rc::new(|v| {
675                v.filter(|v| ValueTypes::as_object(&***v).is_some_and(|o| o.is::<T>())).cloned()
676            }),
677        });
678        self.clone()
679    }
680
681    /// Casts to a class, or to a value or model type: passes a value on if
682    /// it is an instance of the type ([`CastTarget::is_instance`]), else
683    /// null.
684    pub fn type_cast_value(&self, target: CastTarget) -> Self {
685        let type_name = match target {
686            CastTarget::Class(t) => t.name(),
687            CastTarget::Value(t) => t.name(),
688        };
689        let cast: Cast = Rc::new(move |v: Option<&BoxedValue>| v.filter(|v| target.is_instance(v)).cloned());
690        self.elements.borrow_mut().push(PathElement::TypeCast { type_name, cast });
691        self.clone()
692    }
693
694    /// Roots the path at the templated parent of the binding target.
695    pub fn templated_parent(&self) -> Self {
696        self.elements.borrow_mut().push(PathElement::TemplatedParent);
697        self.clone()
698    }
699
700    /// A property whose accessors are closures over untyped values: `get`
701    /// reads it from the handle of its owner, `set` (if the property can be
702    /// written) writes it, and `notifier` (if the owner raises property
703    /// change notifications) views the owner as a notifier. `property_type`
704    /// is the type of the property's value.
705    pub fn property_untyped(
706        &self,
707        name: &str,
708        property_type: ValueType,
709        get: UntypedGetter,
710        set: Option<UntypedSetter>,
711        notifier: Option<AsNotifyPropertyChanged>,
712    ) -> Self {
713        self.property_untyped_with(name, property_type, get, set, notifier, false)
714    }
715
716    /// As [`property_untyped`](Self::property_untyped). `accepts_null`
717    /// applies a null-conditional operator to the access: a null owner
718    /// produces null instead of an error.
719    pub fn property_untyped_with(
720        &self,
721        name: &str,
722        property_type: ValueType,
723        get: UntypedGetter,
724        set: Option<UntypedSetter>,
725        notifier: Option<AsNotifyPropertyChanged>,
726        accepts_null: bool,
727    ) -> Self {
728        self.untyped_member(name, UntypedMember::Property { property_type, get, set, notifier }, accepts_null)
729    }
730
731    /// A method used as a command, from closures over the handle of the
732    /// owner. The command's state is re-queried when the owner (viewed as a
733    /// notifier by `notifier`) reports a change of one of
734    /// `depends_on_properties`.
735    pub fn command_untyped(
736        &self,
737        method_name: &str,
738        execute: UntypedExecute,
739        can_execute: Option<UntypedCanExecute>,
740        depends_on_properties: &[&str],
741        notifier: Option<AsNotifyPropertyChanged>,
742    ) -> Self {
743        self.untyped_member(
744            method_name,
745            UntypedMember::Command {
746                execute,
747                can_execute,
748                depends_on_properties: depends_on_properties.iter().map(|s| (*s).into()).collect(),
749                notifier,
750            },
751            false,
752        )
753    }
754
755    /// A method as a delegate value: the path element produces the value
756    /// `create_delegate` makes for the handle of the owner, usually an
757    /// untyped callback ([`MarkupDelegate`](crate::metadata::MarkupDelegate))
758    /// that invokes the method on it. `accepts_null` applies a
759    /// null-conditional operator to the access: a null owner produces null
760    /// instead of an error.
761    pub fn method_untyped(&self, name: &str, create_delegate: UntypedCreateDelegate, accepts_null: bool) -> Self {
762        self.untyped_member(name, UntypedMember::Method { create_delegate }, accepts_null)
763    }
764
765    fn untyped_member(&self, name: &str, member: UntypedMember, accepts_null: bool) -> Self {
766        let (kind, property_type) = match &member {
767            UntypedMember::Property { property_type, .. } => {
768                (CompiledBindingPathElementKind::Property, Some(*property_type))
769            }
770            UntypedMember::Command { .. } => (CompiledBindingPathElementKind::MethodAsCommand, None),
771            UntypedMember::Method { .. } => (CompiledBindingPathElementKind::MethodAsDelegate, None),
772        };
773        self.elements.borrow_mut().push(PathElement::Untyped {
774            name: name.into(),
775            kind,
776            property_type,
777            plugin: Rc::new(UntypedAccessorPlugin::new(name, member)),
778            accepts_null,
779        });
780        self.clone()
781    }
782
783    /// A property with typed accessors on a shared model type that raises
784    /// NO property change notifications: as
785    /// [`typed_property_info_with`](Self::typed_property_info_with), with
786    /// the value read once per source (the typed property overload of the
787    /// managed original does not require a notifying source).
788    pub fn typed_plain_property_info_with<S: PartialEq + 'static, V: PropertyValue>(
789        &self,
790        info: TypedClrPropertyInfo<S, V>,
791        accepts_null: bool,
792    ) -> Self {
793        ValueTypes::register_reference::<S>();
794        let info = Rc::new(info);
795        self.elements.borrow_mut().push(PathElement::TypedProperty {
796            property: info.clone(),
797            accessor_factory: PropertyInfoAccessorFactory::create_plain_property_accessor(),
798            accepts_null,
799            typed: Rc::new(info),
800        });
801        self.clone()
802    }
803
804    /// The path of the elements added so far.
805    pub fn build(&self) -> CompiledBindingPath {
806        CompiledBindingPath { elements: self.elements.borrow().clone().into() }
807    }
808
809    /// As [`typed_property`](Self::typed_property), from a property
810    /// description (which may have a fallible getter).
811    pub fn typed_property_info<S: INotifyPropertyChanged + PartialEq + 'static, V: PropertyValue>(
812        &self,
813        info: TypedClrPropertyInfo<S, V>,
814    ) -> Self {
815        self.typed_property_info_with(info, false)
816    }
817
818    /// As [`typed_property_info`](Self::typed_property_info). `accepts_null`
819    /// applies a null-conditional operator to the access (the three
820    /// argument `Property<TSource, TValue>` of the managed original).
821    pub fn typed_property_info_with<S: INotifyPropertyChanged + PartialEq + 'static, V: PropertyValue>(
822        &self,
823        info: TypedClrPropertyInfo<S, V>,
824        accepts_null: bool,
825    ) -> Self {
826        ValueTypes::register_reference::<S>();
827        let info = Rc::new(info.notifying());
828        self.elements.borrow_mut().push(PathElement::TypedProperty {
829            property: info.clone(),
830            accessor_factory: PropertyInfoAccessorFactory::create_inpc_property_accessor::<S>(),
831            accepts_null,
832            typed: Rc::new(info),
833        });
834        self.clone()
835    }
836}
837
838#[cfg(test)]
839mod tests {
840    use super::*;
841    use crate::data::model::Event;
842    use crate::{StaticType, StyledElement};
843
844    struct PathVm {
845        text: RefCell<String>,
846        property_changed: Event<str>,
847    }
848
849    impl PartialEq for PathVm {
850        fn eq(&self, other: &Self) -> bool {
851            std::ptr::eq(self, other)
852        }
853    }
854
855    impl INotifyPropertyChanged for PathVm {
856        fn property_changed(&self) -> &Event<str> {
857            &self.property_changed
858        }
859    }
860
861    #[test]
862    fn elements_of_a_path_can_be_inspected() {
863        use CompiledBindingPathElementKind as Kind;
864
865        let path = CompiledBindingPathBuilder::new()
866            .typed_property::<PathVm, String>("Text", |vm| vm.text.borrow().clone(), None)
867            .build();
868        assert_eq!(path.elements().len(), 1);
869        let element = path.element(0).expect("the element");
870        assert_eq!(element.kind(), Kind::Property);
871        assert!(element.is_typed_property());
872        assert_eq!(element.name(), Some("Text"));
873        assert_eq!(element.property_type(), Some(ValueType::of::<String>()));
874        let property = element.property().expect("the property");
875        assert_eq!(property.name(), "Text");
876        assert!(!property.can_set());
877        assert!(path.element(1).is_none());
878
879        let get: UntypedGetter = Rc::new(|_| Ok(None));
880        let path = CompiledBindingPathBuilder::new()
881            .not()
882            .ancestor(Some(StyledElement::TYPE), 2)
883            .ferro_property_with(StyledElement::data_context_property().as_property(), true)
884            .notifying_property::<PathVm, Value<String>>("Text", |vm| vm.text.borrow().clone(), |vm, v| {
885                vm.text.replace(v);
886            })
887            .property_untyped("Length", ValueType::of::<i32>(), get, None, None)
888            .method_untyped("Run", Rc::new(|owner| owner.clone()), false)
889            .command_untyped("Save", Rc::new(|_, _| {}), None, &[], None)
890            .command::<PathVm>("Load", |_, _| {}, None, &[])
891            .stream_task()
892            .array_element(&[1, 2])
893            .type_cast::<StyledElement>()
894            .build();
895        let kinds: Vec<Kind> = path.elements().map(|element| element.kind()).collect();
896        assert_eq!(
897            kinds,
898            [
899                Kind::Not,
900                Kind::Ancestor,
901                Kind::Property,
902                Kind::Property,
903                Kind::Property,
904                Kind::MethodAsDelegate,
905                Kind::MethodAsCommand,
906                Kind::MethodAsCommand,
907                Kind::Stream,
908                Kind::ArrayElement,
909                Kind::TypeCast,
910            ]
911        );
912        let names: Vec<Option<&str>> = path.elements().map(|element| element.name()).collect();
913        assert_eq!(
914            names,
915            [None, None, Some("DataContext"), Some("Text"), Some("Length"), Some("Run"), Some("Save"), Some("Load"), None, None, None]
916        );
917        let ancestor = path.element(1).unwrap().ancestor().expect("an ancestor");
918        assert!(ancestor.0.is_some_and(|type_| std::ptr::eq(type_, StyledElement::TYPE)));
919        assert_eq!(ancestor.1, 2);
920
921        let registered = path.element(2).unwrap();
922        assert!(registered.accepts_null() && !registered.is_typed_property());
923        assert!(registered.property().and_then(|p| p.as_ferro_property()).is_some());
924
925        let plain = path.element(3).unwrap();
926        assert!(!plain.is_typed_property() && !plain.accepts_null());
927        assert!(plain.property().is_some_and(|property| property.can_set()));
928
929        let untyped = path.element(4).unwrap();
930        assert!(untyped.property().is_none());
931        assert_eq!(untyped.property_type(), Some(ValueType::of::<i32>()));
932        assert_eq!(path.element(5).unwrap().property_type(), None);
933        assert_eq!(path.element(9).unwrap().array_indices(), Some(&[1, 2][..]));
934        assert_eq!(path.to_string(), "!$parent[StyledElement, 2].DataContext.Text.Length.Run().Save().Load()^[1,2](StyledElement)");
935        assert_eq!(CompiledBindingPath::new().elements().len(), 0);
936    }
937}