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}