Skip to main content

ferroui_dynamic_data/binding/
observable_property_part.rs

1//! `ObservablePropertyPart` (`Binding/ObservablePropertyPart.cs`) and the parsing of expressions
2//! into their steps (`Binding/ExpressionBuilder.cs`): [`PropertyChain`], a path whose steps may be
3//! null.
4//!
5//! Upstream splits an expression `x => x.Child!.Age` into its steps by reflection, each with an
6//! invoker that reads it and a factory of the notifications of its property, and walks the steps
7//! from the root: a null step leaves the value "unobtainable". A [`PropertyPath`] of
8//! `ferroui-system` observes every step of a path whose objects are never null; a
9//! [`PropertyChain`] adds the steps that may be null, as [`PropertyChain::nullable`] (an
10//! `Option<..>` property), and the conversions of a step ([`PropertyChain::map`], upstream's
11//! `Convert` nodes).
12
13use ferroui_system::component_model::PropertyPath;
14use ferroui_system::IDisposable;
15use std::cell::RefCell;
16use std::rc::Rc;
17
18/// Observes the steps of a chain on an object; the callback runs when the value may have changed.
19type Observe<S> = Rc<dyn Fn(&S, Rc<dyn Fn()>) -> Rc<dyn IDisposable>>;
20
21/// A subscription made of a closure that ends it.
22struct OnDispose(RefCell<Option<Box<dyn FnOnce()>>>);
23
24impl IDisposable for OnDispose {
25    fn dispose(&self) {
26        let end = self.0.borrow_mut().take();
27        if let Some(end) = end {
28            end();
29        }
30    }
31}
32
33fn on_dispose(end: impl FnOnce() + 'static) -> Rc<dyn IDisposable> {
34    Rc::new(OnDispose(RefCell::new(Some(Box::new(end)))))
35}
36
37/// A path from an object to a value through properties, any step of which may be null (the
38/// expression of `WhenPropertyChanged` and `WhenValueChanged`, split into its
39/// `ObservablePropertyPart`s).
40///
41/// Its value is `None` when a step is null (upstream's `UnobtainableValue`). Each step that
42/// notifies its changes is observed on the object it is read from; when a step changes, the
43/// steps after it are observed again on the new objects, and none is observed past a null step.
44///
45/// ```
46/// use ferroui_dynamic_data::binding::PropertyChain;
47/// use ferroui_system::component_model::{INotifyPropertyChanged, PropertyPath};
48/// use ferroui_system::events::Event;
49/// use std::cell::{Cell, RefCell};
50/// use std::rc::Rc;
51///
52/// struct Child { age: Cell<i32>, changed: Event<str> }
53/// impl Child { fn age(&self) -> i32 { self.age.get() } }
54/// impl INotifyPropertyChanged for Child { fn property_changed(&self) -> &Event<str> { &self.changed } }
55///
56/// struct Parent { child: RefCell<Option<Rc<Child>>>, changed: Event<str> }
57/// impl Parent { fn child(&self) -> Option<Rc<Child>> { self.child.borrow().clone() } }
58/// impl INotifyPropertyChanged for Parent { fn property_changed(&self) -> &Event<str> { &self.changed } }
59///
60/// // x => x.Child!.Age
61/// let chain = PropertyChain::nullable(ferroui_system::prop!(Parent => child)).then(ferroui_system::prop!(Child => age));
62/// let parent = Rc::new(Parent { child: RefCell::new(None), changed: Event::new() });
63/// assert_eq!(chain.get(&parent), None);
64/// *parent.child.borrow_mut() = Some(Rc::new(Child { age: Cell::new(3), changed: Event::new() }));
65/// assert_eq!(chain.get(&parent), Some(3));
66/// assert_eq!(chain.path(), "Child.Age");
67/// ```
68pub struct PropertyChain<S, V> {
69    names: Rc<[String]>,
70    get: Rc<dyn Fn(&S) -> Option<V>>,
71    observe: Observe<S>,
72}
73
74impl<S, V> Clone for PropertyChain<S, V> {
75    fn clone(&self) -> Self {
76        Self { names: self.names.clone(), get: self.get.clone(), observe: self.observe.clone() }
77    }
78}
79
80impl<S: 'static, V: 'static> PropertyChain<S, V> {
81    /// The chain of a path whose steps are never null.
82    pub fn new(path: PropertyPath<S, V>) -> Self {
83        let names: Rc<[String]> = path.names().iter().cloned().collect();
84        let getter = path.getter();
85        Self { names, get: Rc::new(move |source: &S| Some(getter(source))), observe: Rc::new(move |source: &S, changed: Rc<dyn Fn()>| path.observe(source, changed)) }
86    }
87
88    /// The chain of a path whose last step may be null (`x => x.Child!`): its value is `None`
89    /// when the step is.
90    pub fn nullable(path: PropertyPath<S, Option<V>>) -> Self {
91        let names: Rc<[String]> = path.names().iter().cloned().collect();
92        let getter = path.getter();
93        Self { names, get: getter, observe: Rc::new(move |source: &S, changed: Rc<dyn Fn()>| path.observe(source, changed)) }
94    }
95
96    /// The names of the properties along the chain.
97    pub fn names(&self) -> &[String] {
98        &self.names
99    }
100
101    /// The chain as text, `Child.Age`.
102    pub fn path(&self) -> String {
103        self.names.join(".")
104    }
105
106    /// The value of the chain on `source`; `None` when a step is null (the walk of
107    /// `ReadCurrent`).
108    pub fn get(&self, source: &S) -> Option<V> {
109        (self.get)(source)
110    }
111
112    /// The getter of the chain.
113    pub fn getter(&self) -> Rc<dyn Fn(&S) -> Option<V>> {
114        self.get.clone()
115    }
116
117    /// Runs `changed` whenever the value of the chain on `source` may have changed, until the
118    /// result is disposed (the subscriptions of the levels of `DeepChainSubscription`).
119    pub fn observe(&self, source: &S, changed: Rc<dyn Fn()>) -> Rc<dyn IDisposable> {
120        (self.observe)(source, changed)
121    }
122
123    /// The chain that continues with `next` from the value of this one (`x => x.A.B`): when the
124    /// object of this step changes, the steps of `next` are observed on the new object, and on
125    /// none when it is null.
126    pub fn then<P: IntoPropertyChain<V>>(self, next: P) -> PropertyChain<S, P::Value>
127    where
128        S: Clone,
129        V: Clone,
130        P::Value: 'static,
131    {
132        let next = next.into_property_chain();
133        let names: Rc<[String]> = self.names.iter().chain(next.names.iter()).cloned().collect();
134        let (first_get, next_get) = (self.get.clone(), next.get.clone());
135        let (first, second) = (self, next);
136        PropertyChain {
137            names,
138            get: Rc::new(move |source: &S| first_get(source).and_then(|value| next_get(&value))),
139            observe: Rc::new(move |source: &S, changed: Rc<dyn Fn()>| {
140                let inner: Rc<RefCell<Option<Rc<dyn IDisposable>>>> = Rc::new(RefCell::new(None));
141                let (source_for_change, inner_for_change, second_for_change, first_get) = (source.clone(), inner.clone(), second.clone(), first.get.clone());
142                let changed_for_change = changed.clone();
143                // The levels are subscribed from the root to the leaf (`ResubscribeFrom`).
144                let outer = first.observe(
145                    source,
146                    Rc::new(move || {
147                        let changed = changed_for_change.clone();
148                        // The steps after a null one are not observed (`ResubscribeFrom`).
149                        let next = first_get(&source_for_change).map(|value| second_for_change.observe(&value, changed.clone()));
150                        let previous = std::mem::replace(&mut *inner_for_change.borrow_mut(), next);
151                        if let Some(previous) = previous {
152                            previous.dispose();
153                        }
154                        changed();
155                    }),
156                );
157                let initial = first.get(source).map(|value| second.observe(&value, changed.clone()));
158                let replaced = std::mem::replace(&mut *inner.borrow_mut(), initial);
159                if let Some(replaced) = replaced {
160                    replaced.dispose();
161                }
162                on_dispose(move || {
163                    outer.dispose();
164                    let inner = inner.borrow_mut().take();
165                    if let Some(inner) = inner {
166                        inner.dispose();
167                    }
168                })
169            }),
170        }
171    }
172
173    /// [`then`](Self::then) with a step that may be null (`x => x.A!.B`).
174    pub fn then_nullable<W: 'static>(self, next: PropertyPath<V, Option<W>>) -> PropertyChain<S, W>
175    where
176        S: Clone,
177        V: Clone,
178    {
179        self.then(PropertyChain::nullable(next))
180    }
181
182    /// The chain whose value is converted by `convert` (an expression's `Convert` node, such as
183    /// `x => (decimal)x.Amount` or `x => ((IHasAge)x.Child).Age`); the steps observed are the
184    /// same.
185    pub fn map<W: 'static>(self, convert: impl Fn(V) -> W + 'static) -> PropertyChain<S, W> {
186        let get = self.get.clone();
187        PropertyChain { names: self.names.clone(), get: Rc::new(move |source: &S| get(source).map(&convert)), observe: self.observe.clone() }
188    }
189}
190
191/// What the operators that observe properties take: a [`PropertyPath`] (from `prop!`), or a
192/// [`PropertyChain`] with steps that may be null.
193pub trait IntoPropertyChain<S> {
194    /// The type of the value at the end of the path.
195    type Value;
196
197    /// The path as a chain.
198    fn into_property_chain(self) -> PropertyChain<S, Self::Value>;
199}
200
201impl<S: 'static, V: 'static> IntoPropertyChain<S> for PropertyPath<S, V> {
202    type Value = V;
203
204    fn into_property_chain(self) -> PropertyChain<S, V> {
205        PropertyChain::new(self)
206    }
207}
208
209impl<S, V> IntoPropertyChain<S> for PropertyChain<S, V> {
210    type Value = V;
211
212    fn into_property_chain(self) -> PropertyChain<S, V> {
213        self
214    }
215}