Skip to main content

ferroui_system/component_model/
property_path.rs

1//! Property paths: the counterpart of the expressions `x => x.Name` and `x => x.Address.Street`
2//! that upstream libraries take (`Expression<Func<TObject, TValue>>`), without reflection
3//! (docs/porting/reactive.md, section 5.5).
4//!
5//! A [`PropertyPath`] holds a getter of the whole path, the names of its properties, and a way to
6//! observe the changes of every step on an object: a change of an intermediate object
7//! subscribes to the new one. [`prop!`](crate::prop) writes paths from getters.
8//!
9//! A path also gives a type-erased view of its steps ([`PropertyPath::steps`], [`PathStep`]): the
10//! name of each property, how to read it from the value of the step before, and the object that
11//! owns it ([`PropertyOwner`]). That is what an observer chosen at run time (ReactiveUI's
12//! `ICreatesObservableForProperty`) works from, where upstream walks the member expressions of
13//! the expression tree.
14
15use super::INotifyPropertyChanged;
16use crate::IDisposable;
17use std::any::Any;
18use std::cell::RefCell;
19use std::fmt;
20use std::rc::Rc;
21
22/// Observes the steps of a path on an object; the callback runs when the value of the path may
23/// have changed.
24type Observe<S> = Rc<dyn Fn(&S, Rc<dyn Fn()>) -> Rc<dyn IDisposable>>;
25
26/// A typed path from an object to a value through its properties (`Expression<Func<S, V>>`).
27pub struct PropertyPath<S, V> {
28    names: Rc<[String]>,
29    get: Rc<dyn Fn(&S) -> V>,
30    observe: Observe<S>,
31    steps: Rc<[PathStep]>,
32}
33
34impl<S, V> Clone for PropertyPath<S, V> {
35    fn clone(&self) -> Self {
36        Self { names: self.names.clone(), get: self.get.clone(), observe: self.observe.clone(), steps: self.steps.clone() }
37    }
38}
39
40/// The object that owns the property of one step of a path, as an observer chosen at run time
41/// sees it (the `sender` of upstream's `ICreatesObservableForProperty`).
42///
43/// `object` is the object itself, which an observer downcasts to the types it knows; `notify` is
44/// its change notification, when it raises `PropertyChanged`. A step that cannot give its object
45/// (a [`PropertyPath::value`] step, or an [`optional_property`](PropertyPath::optional_property)
46/// step whose object is `None`) leaves both `None`.
47#[derive(Clone, Default)]
48pub struct PropertyOwner {
49    /// The object that owns the property.
50    pub object: Option<Rc<dyn Any>>,
51    /// The change notification of the object, when it raises `PropertyChanged`.
52    pub notify: Option<Rc<dyn INotifyPropertyChanged>>,
53}
54
55impl PropertyOwner {
56    /// Whether the two owners are the same object (reference equality, as .NET compares senders).
57    pub fn same_object(&self, other: &PropertyOwner) -> bool {
58        match (&self.object, &other.object) {
59            (Some(first), Some(second)) => Rc::ptr_eq(first, second),
60            (None, None) => match (&self.notify, &other.notify) {
61                (Some(first), Some(second)) => Rc::ptr_eq(first, second),
62                (None, None) => true,
63                _ => false,
64            },
65            _ => false,
66        }
67    }
68}
69
70impl fmt::Debug for PropertyOwner {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        f.debug_struct("PropertyOwner")
73            .field("object", &self.object.as_ref().map(|object| Rc::as_ptr(object) as *const ()))
74            .field("notify", &self.notify.is_some())
75            .finish()
76    }
77}
78
79/// One step of a path, type-erased (one member access of upstream's expression chain).
80///
81/// The steps of a path run in order: the first reads from the source of the path, each next one
82/// from the value the step before produced. `get` and `owner` take that value as `&dyn Any`; a
83/// value of another type is a programming error, and they panic.
84#[derive(Clone)]
85pub struct PathStep {
86    /// The name of the property (`SearchTerm`).
87    pub name: String,
88    /// The value of the step, from the value of the previous one (or the source of the path).
89    pub get: Rc<dyn Fn(&dyn Any) -> Box<dyn Any>>,
90    /// The object that owns the property, from the value of the previous step.
91    pub owner: Rc<dyn Fn(&dyn Any) -> PropertyOwner>,
92}
93
94impl fmt::Debug for PathStep {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        f.debug_struct("PathStep").field("name", &self.name).finish_non_exhaustive()
97    }
98}
99
100/// The value given to the step `name` as the type it reads from; any other type is a programming
101/// error of the caller of the type-erased steps.
102fn step_input<'a, T: 'static>(value: &'a dyn Any, name: &str) -> &'a T {
103    match value.downcast_ref::<T>() {
104        Some(value) => value,
105        None => panic!(
106            "the step {name} of a property path was given a value of another type than {}",
107            std::any::type_name::<T>()
108        ),
109    }
110}
111
112/// The owner of a property of an object that raises `PropertyChanged`.
113fn owner_of<M: INotifyPropertyChanged + 'static>(object: &Rc<M>) -> PropertyOwner {
114    PropertyOwner { object: Some(object.clone() as Rc<dyn Any>), notify: Some(object.clone() as Rc<dyn INotifyPropertyChanged>) }
115}
116
117/// Runs `changed` when `source` raises `PropertyChanged` for `watched`, or for every property (an
118/// empty name).
119fn observe_property<M: INotifyPropertyChanged + 'static>(source: &Rc<M>, watched: String, changed: Rc<dyn Fn()>) -> Rc<dyn IDisposable> {
120    let token = source.property_changed().add(Rc::new(move |property: &str| {
121        if property.is_empty() || property == watched {
122            changed();
123        }
124    }));
125    let source = source.clone();
126    on_dispose(move || {
127        source.property_changed().remove(token);
128    })
129}
130
131/// A subscription made of a closure that ends it.
132struct OnDispose(RefCell<Option<Box<dyn FnOnce()>>>);
133
134impl IDisposable for OnDispose {
135    fn dispose(&self) {
136        let end = self.0.borrow_mut().take();
137        if let Some(end) = end {
138            end();
139        }
140    }
141}
142
143fn on_dispose(end: impl FnOnce() + 'static) -> Rc<dyn IDisposable> {
144    Rc::new(OnDispose(RefCell::new(Some(Box::new(end)))))
145}
146
147/// The name of a property from the name of its Rust getter: `favorite_color` is `FavoriteColor`,
148/// the names that FerroUI's objects raise and its markup binds.
149pub fn property_name_of(getter: &str) -> String {
150    getter
151        .split('_')
152        .filter(|part| !part.is_empty())
153        .map(|part| {
154            let mut chars = part.chars();
155            match chars.next() {
156                Some(first) => first.to_uppercase().chain(chars).collect::<String>(),
157                None => String::new(),
158            }
159        })
160        .collect()
161}
162
163impl<M: INotifyPropertyChanged + 'static, V: 'static> PropertyPath<Rc<M>, V> {
164    /// A path of one property of an object that raises `PropertyChanged`: a change of `name`, or
165    /// of every property (an empty name), changes the value.
166    ///
167    /// Its step gives the object as its owner, with its change notification.
168    pub fn property(name: impl Into<String>, get: impl Fn(&Rc<M>) -> V + 'static) -> Self {
169        let name: String = name.into();
170        let watched = name.clone();
171        let get: Rc<dyn Fn(&Rc<M>) -> V> = Rc::new(get);
172        let step_get = get.clone();
173        let step_name = name.clone();
174        let owner_name = name.clone();
175        let step = PathStep {
176            name: name.clone(),
177            get: Rc::new(move |source: &dyn Any| Box::new(step_get(step_input::<Rc<M>>(source, &step_name))) as Box<dyn Any>),
178            owner: Rc::new(move |source: &dyn Any| owner_of(step_input::<Rc<M>>(source, &owner_name))),
179        };
180        Self {
181            names: Rc::from(vec![name]),
182            get,
183            observe: Rc::new(move |source: &Rc<M>, changed: Rc<dyn Fn()>| observe_property(source, watched.clone(), changed)),
184            steps: Rc::from(vec![step]),
185        }
186    }
187}
188
189impl<M: INotifyPropertyChanged + 'static, V: 'static> PropertyPath<Option<Rc<M>>, Option<V>> {
190    /// A path of one property of an object that may be absent (`x => x.Child.Name` where `Child`
191    /// may be null): `None` while there is no object, otherwise the property, observed as
192    /// [`property`](Self::property) observes it.
193    ///
194    /// Its step gives the object as its owner when there is one, and an empty owner otherwise.
195    pub fn optional_property(name: impl Into<String>, get: impl Fn(&Rc<M>) -> V + 'static) -> Self {
196        let name: String = name.into();
197        let watched = name.clone();
198        let get: Rc<dyn Fn(&Rc<M>) -> V> = Rc::new(get);
199        let path_get = get.clone();
200        let step_name = name.clone();
201        let owner_name = name.clone();
202        let step = PathStep {
203            name: name.clone(),
204            get: Rc::new(move |source: &dyn Any| {
205                Box::new(step_input::<Option<Rc<M>>>(source, &step_name).as_ref().map(|object| get(object))) as Box<dyn Any>
206            }),
207            owner: Rc::new(move |source: &dyn Any| match step_input::<Option<Rc<M>>>(source, &owner_name) {
208                Some(object) => owner_of(object),
209                None => PropertyOwner::default(),
210            }),
211        };
212        Self {
213            names: Rc::from(vec![name]),
214            get: Rc::new(move |source: &Option<Rc<M>>| source.as_ref().map(|object| path_get(object))),
215            observe: Rc::new(move |source: &Option<Rc<M>>, changed: Rc<dyn Fn()>| match source {
216                Some(object) => observe_property(object, watched.clone(), changed),
217                None => on_dispose(|| {}),
218            }),
219            steps: Rc::from(vec![step]),
220        }
221    }
222}
223
224impl<M: INotifyPropertyChanged + 'static, V: 'static> PropertyPath<Option<Rc<M>>, Option<V>> {
225    /// [`optional_property`](Self::optional_property) of a property that may itself be absent
226    /// (`x => x.A.B.C` where `A` and `B` may be null): the value is `None` while there is no object
227    /// or while the property is `None`, so the steps of a path of absent objects do not nest
228    /// their `Option`s.
229    pub fn optional_property_and_then(name: impl Into<String>, get: impl Fn(&Rc<M>) -> Option<V> + 'static) -> Self {
230        let name: String = name.into();
231        let watched = name.clone();
232        let get: Rc<dyn Fn(&Rc<M>) -> Option<V>> = Rc::new(get);
233        let path_get = get.clone();
234        let step_name = name.clone();
235        let owner_name = name.clone();
236        let step = PathStep {
237            name: name.clone(),
238            get: Rc::new(move |source: &dyn Any| {
239                Box::new(step_input::<Option<Rc<M>>>(source, &step_name).as_ref().and_then(|object| get(object))) as Box<dyn Any>
240            }),
241            owner: Rc::new(move |source: &dyn Any| match step_input::<Option<Rc<M>>>(source, &owner_name) {
242                Some(object) => owner_of(object),
243                None => PropertyOwner::default(),
244            }),
245        };
246        Self {
247            names: Rc::from(vec![name]),
248            get: Rc::new(move |source: &Option<Rc<M>>| source.as_ref().and_then(|object| path_get(object))),
249            observe: Rc::new(move |source: &Option<Rc<M>>, changed: Rc<dyn Fn()>| match source {
250                Some(object) => observe_property(object, watched.clone(), changed),
251                None => on_dispose(|| {}),
252            }),
253            steps: Rc::from(vec![step]),
254        }
255    }
256}
257
258impl<M: 'static, V: 'static> PropertyPath<Rc<M>, V> {
259    /// A path of one property of an object that does not raise `PropertyChanged` itself, but whose
260    /// changes an observer chosen at run time may see (an object of a user-interface framework,
261    /// through its own property system).
262    ///
263    /// Its step gives the object (the `Rc` of the source) as its owner, without a change
264    /// notification, so that the observer with the highest affinity for that object observes the
265    /// property. The path itself observes nothing, as [`value`](PropertyPath::value) does.
266    pub fn object_property(name: impl Into<String>, get: impl Fn(&Rc<M>) -> V + 'static) -> Self {
267        Self::owned_property(
268            name,
269            get,
270            |source: &Rc<M>| PropertyOwner { object: Some(source.clone() as Rc<dyn Any>), notify: None },
271            |_: &Rc<M>, _: Rc<dyn Fn()>| on_dispose(|| {}),
272        )
273    }
274}
275
276impl<S: 'static, V: 'static> PropertyPath<S, V> {
277    /// A path of one property whose owner and observation the caller gives: `owner` gives the
278    /// object that owns the property from the source of the step (what an observer chosen at run
279    /// time sees), and `observe` runs its callback when the value may have changed, until the
280    /// result is disposed.
281    ///
282    /// It is how a framework whose objects are not `Rc`s of a type that raises `PropertyChanged`
283    /// (handles of the objects of a user interface) writes the steps of its properties.
284    pub fn owned_property(
285        name: impl Into<String>,
286        get: impl Fn(&S) -> V + 'static,
287        owner: impl Fn(&S) -> PropertyOwner + 'static,
288        observe: impl Fn(&S, Rc<dyn Fn()>) -> Rc<dyn IDisposable> + 'static,
289    ) -> Self {
290        let name: String = name.into();
291        let get: Rc<dyn Fn(&S) -> V> = Rc::new(get);
292        let owner: Rc<dyn Fn(&S) -> PropertyOwner> = Rc::new(owner);
293        let step_get = get.clone();
294        let step_name = name.clone();
295        let owner_name = name.clone();
296        let step = PathStep {
297            name: name.clone(),
298            get: Rc::new(move |source: &dyn Any| Box::new(step_get(step_input::<S>(source, &step_name))) as Box<dyn Any>),
299            owner: Rc::new(move |source: &dyn Any| owner(step_input::<S>(source, &owner_name))),
300        };
301        Self { names: Rc::from(vec![name]), get, observe: Rc::new(observe), steps: Rc::from(vec![step]) }
302    }
303
304    /// A path of one value that does not notify its changes (a plain field, or an object that is
305    /// not `INotifyPropertyChanged`): it is read again only when an earlier step changes.
306    ///
307    /// Its step cannot give the object that owns the value (the source is not cloned), so its
308    /// owner is empty: an observer chosen at run time sees a value that does not notify.
309    pub fn value(name: impl Into<String>, get: impl Fn(&S) -> V + 'static) -> Self {
310        let name: String = name.into();
311        let get: Rc<dyn Fn(&S) -> V> = Rc::new(get);
312        let step_get = get.clone();
313        let step_name = name.clone();
314        let step = PathStep {
315            name: name.clone(),
316            get: Rc::new(move |source: &dyn Any| Box::new(step_get(step_input::<S>(source, &step_name))) as Box<dyn Any>),
317            owner: Rc::new(|_: &dyn Any| PropertyOwner::default()),
318        };
319        Self {
320            names: Rc::from(vec![name]),
321            get,
322            observe: Rc::new(|_: &S, _: Rc<dyn Fn()>| on_dispose(|| {})),
323            steps: Rc::from(vec![step]),
324        }
325    }
326
327    /// The steps of the path, type-erased, in order (the member accesses of the expression).
328    pub fn steps(&self) -> &[PathStep] {
329        &self.steps
330    }
331
332    /// The names of the properties along the path (the member names of the expression).
333    pub fn names(&self) -> &[String] {
334        &self.names
335    }
336
337    /// The path as text, `Address.Street`.
338    pub fn path(&self) -> String {
339        self.names.join(".")
340    }
341
342    /// The value of the path on `source` (the compiled expression).
343    pub fn get(&self, source: &S) -> V {
344        (self.get)(source)
345    }
346
347    /// The getter of the path.
348    pub fn getter(&self) -> Rc<dyn Fn(&S) -> V> {
349        self.get.clone()
350    }
351
352    /// Runs `changed` whenever the value of the path on `source` may have changed, until the
353    /// result is disposed.
354    pub fn observe(&self, source: &S, changed: Rc<dyn Fn()>) -> Rc<dyn IDisposable> {
355        (self.observe)(source, changed)
356    }
357
358    /// The path that continues with `next` from the value of this one (`x => x.A.B`): when the
359    /// object of this step changes, the steps of `next` are observed on the new object.
360    pub fn then<W: 'static>(self, next: PropertyPath<V, W>) -> PropertyPath<S, W>
361    where
362        S: Clone,
363        V: Clone,
364    {
365        let names: Vec<String> = self.names.iter().chain(next.names.iter()).cloned().collect();
366        let steps: Vec<PathStep> = self.steps.iter().chain(next.steps.iter()).cloned().collect();
367        let (first_get, next_get) = (self.get.clone(), next.get.clone());
368        let (first, second) = (self, next);
369        PropertyPath {
370            names: Rc::from(names),
371            steps: Rc::from(steps),
372            get: Rc::new(move |source: &S| next_get(&first_get(source))),
373            observe: Rc::new(move |source: &S, changed: Rc<dyn Fn()>| {
374                let inner: Rc<RefCell<Option<Rc<dyn IDisposable>>>> = Rc::new(RefCell::new(Some(second.observe(&first.get(source), changed.clone()))));
375                let (source_for_change, inner_for_change, second_for_change, first_get) = (source.clone(), inner.clone(), second.clone(), first.get.clone());
376                let outer = first.observe(
377                    source,
378                    Rc::new(move || {
379                        let next = second_for_change.observe(&first_get(&source_for_change), changed.clone());
380                        let previous = inner_for_change.borrow_mut().replace(next);
381                        if let Some(previous) = previous {
382                            previous.dispose();
383                        }
384                        changed();
385                    }),
386                );
387                on_dispose(move || {
388                    outer.dispose();
389                    let inner = inner.borrow_mut().take();
390                    if let Some(inner) = inner {
391                        inner.dispose();
392                    }
393                })
394            }),
395        }
396    }
397}
398
399impl<S: Clone + 'static, M: INotifyPropertyChanged + 'static> PropertyPath<S, Rc<M>> {
400    /// [`then`](PropertyPath::then) with one property of the object of this path
401    /// ([`PropertyPath::property`]); the object's type comes from this path.
402    pub fn then_property<W: 'static>(self, name: impl Into<String>, get: impl Fn(&Rc<M>) -> W + 'static) -> PropertyPath<S, W> {
403        self.then(PropertyPath::property(name, get))
404    }
405}
406
407impl<S: Clone + 'static, M: INotifyPropertyChanged + 'static> PropertyPath<S, Option<Rc<M>>> {
408    /// [`then`](PropertyPath::then) with one property of the object of this path, which may be
409    /// absent ([`PropertyPath::optional_property`]): the value is `None` while there is no object.
410    pub fn then_optional_property<W: 'static>(self, name: impl Into<String>, get: impl Fn(&Rc<M>) -> W + 'static) -> PropertyPath<S, Option<W>> {
411        self.then(PropertyPath::optional_property(name, get))
412    }
413}
414
415impl<S: Clone + 'static, M: INotifyPropertyChanged + 'static> PropertyPath<S, Option<Rc<M>>> {
416    /// [`then`](PropertyPath::then) with a property of the object of this path that may itself be
417    /// absent ([`PropertyPath::optional_property_and_then`]): `x => x.A.B.C` where each object may
418    /// be null is `prop!(X => a).then_optional_property_and_then("B", |a| a.b()).then_optional_property("C", |b| b.c())`.
419    pub fn then_optional_property_and_then<W: 'static>(self, name: impl Into<String>, get: impl Fn(&Rc<M>) -> Option<W> + 'static) -> PropertyPath<S, Option<W>> {
420        self.then(PropertyPath::optional_property_and_then(name, get))
421    }
422}
423
424/// A [`PropertyPath`] from getters (`x => x.Name`, `x => x.Address.Street`).
425///
426/// ```
427/// use ferroui_system::component_model::INotifyPropertyChanged;
428/// use ferroui_system::events::Event;
429/// use std::cell::Cell;
430/// use std::rc::Rc;
431///
432/// struct Person { age: Cell<i32>, changed: Event<str> }
433/// impl Person {
434///     fn age(&self) -> i32 { self.age.get() }
435/// }
436/// impl INotifyPropertyChanged for Person {
437///     fn property_changed(&self) -> &Event<str> { &self.changed }
438/// }
439///
440/// let path = ferroui_system::prop!(Person => age);
441/// let person = Rc::new(Person { age: Cell::new(3), changed: Event::new() });
442/// assert_eq!(path.get(&person), 3);
443/// assert_eq!(path.path(), "Age");
444/// ```
445///
446/// Each step is a getter method of an object that raises `PropertyChanged`; its property name is
447/// the getter's in PascalCase (`favorite_color` is `FavoriteColor`), or the one given with
448/// `as "Name"`. The steps after the first are objects held in `Rc`.
449///
450/// `prop!(object Type => getter)` is the path of one property of an object that does not raise
451/// `PropertyChanged` itself ([`PropertyPath::object_property`]): its step gives the object as its
452/// owner, for an observer chosen at run time.
453#[macro_export]
454macro_rules! prop {
455    (object $type_:ty => $first:ident $(as $name:literal)?) => {
456        $crate::component_model::PropertyPath::<::std::rc::Rc<$type_>, _>::object_property(
457            $crate::prop!(@name $first $($name)?),
458            |source: &::std::rc::Rc<$type_>| source.$first(),
459        )
460    };
461    ($type_:ty => $first:ident $(as $name:literal)? $(. $rest:ident $(as $rest_name:literal)?)*) => {
462        $crate::component_model::PropertyPath::<::std::rc::Rc<$type_>, _>::property(
463            $crate::prop!(@name $first $($name)?),
464            |source: &::std::rc::Rc<$type_>| source.$first(),
465        )
466        $(.then_property(
467            $crate::prop!(@name $rest $($rest_name)?),
468            |source| source.$rest(),
469        ))*
470    };
471    (@name $getter:ident $name:literal) => { $name };
472    (@name $getter:ident) => { $crate::component_model::property_name_of(::std::stringify!($getter)) };
473}