Skip to main content

ferroui_base/
ferro_object_extensions.rs

1//! Extension members for [`FerroObject`]: observables over property values
2//! and property-kind-agnostic access to typed properties.
3
4use crate::data::core::{UntypedObservableBindingExpression, ValueTypes};
5use crate::data::{BindingBase, BindingExpressionBase, BindingPriority, BindingValue};
6use crate::reactive::{Disposable, IDisposable, IObservable, IObserver, ObservableExt};
7use crate::{
8    AttachedProperty, BoxedValue, DirectProperty, DirectPropertyBase, FerroObject, FerroProperty,
9    FerroPropertyChangedEventArgs, ObjectType, PropertyValue, StyledProperty, WeakRef,
10};
11use std::cell::{Cell, RefCell};
12use std::rc::{Rc, Weak};
13
14/// Converts an observable to a binding (`source.ToBinding()`).
15///
16/// The values of the observable are delivered untyped: a nullable value
17/// (`Option<T>`) as null or the value it holds.
18pub fn to_binding<T: PropertyValue>(source: Rc<dyn IObservable<T>>) -> Rc<dyn BindingBase> {
19    Rc::new(BindingAdaptor { source: source.select(|x| ValueTypes::normalize(Rc::new(x))) })
20}
21
22/// The binding [`to_binding`] returns: its instances read the values of the
23/// observable with local value priority.
24struct BindingAdaptor {
25    source: Rc<dyn IObservable<Option<BoxedValue>>>,
26}
27
28impl BindingBase for BindingAdaptor {
29    fn create_instance(
30        &self,
31        _target: &FerroObject,
32        _target_property: Option<&'static FerroProperty>,
33        _anchor: Option<&crate::Ref<FerroObject>>,
34    ) -> Rc<dyn BindingExpressionBase> {
35        let expression: Rc<dyn BindingExpressionBase> =
36            UntypedObservableBindingExpression::new(self.source.clone(), BindingPriority::LocalValue);
37        expression
38    }
39}
40
41/// A property with a statically known value type: a styled, attached or
42/// direct property.
43///
44/// This is what lets the members of [`FerroObjectExtensions`] accept any kind
45/// of typed property and route to the matching [`FerroObject`] member.
46pub trait TypedProperty: 'static {
47    /// The value type of the property.
48    type Value: PropertyValue;
49
50    /// The untyped base of the property.
51    fn untyped(&'static self) -> &'static FerroProperty;
52
53    #[doc(hidden)]
54    fn typed_get_value(&'static self, target: &FerroObject) -> Self::Value;
55
56    #[doc(hidden)]
57    fn typed_get_base_value(&'static self, target: &FerroObject) -> Option<Self::Value>;
58
59    #[doc(hidden)]
60    fn typed_bind(
61        &'static self,
62        target: &FerroObject,
63        source: Rc<dyn IObservable<Self::Value>>,
64        priority: BindingPriority,
65    ) -> Rc<dyn IDisposable>;
66
67    #[doc(hidden)]
68    fn typed_bind_value(
69        &'static self,
70        target: &FerroObject,
71        source: Rc<dyn IObservable<BindingValue<Self::Value>>>,
72        priority: BindingPriority,
73    ) -> Rc<dyn IDisposable>;
74}
75
76impl<T: PropertyValue> TypedProperty for StyledProperty<T> {
77    type Value = T;
78
79    fn untyped(&'static self) -> &'static FerroProperty {
80        self
81    }
82
83    fn typed_get_value(&'static self, target: &FerroObject) -> T {
84        target.get_value(self)
85    }
86
87    fn typed_get_base_value(&'static self, target: &FerroObject) -> Option<T> {
88        target.get_base_value(self)
89    }
90
91    fn typed_bind(
92        &'static self,
93        target: &FerroObject,
94        source: Rc<dyn IObservable<T>>,
95        priority: BindingPriority,
96    ) -> Rc<dyn IDisposable> {
97        target.bind(self, source, priority)
98    }
99
100    fn typed_bind_value(
101        &'static self,
102        target: &FerroObject,
103        source: Rc<dyn IObservable<BindingValue<T>>>,
104        priority: BindingPriority,
105    ) -> Rc<dyn IDisposable> {
106        target.bind_value(self, source, priority)
107    }
108}
109
110impl<T: PropertyValue> TypedProperty for AttachedProperty<T> {
111    type Value = T;
112
113    fn untyped(&'static self) -> &'static FerroProperty {
114        self
115    }
116
117    fn typed_get_value(&'static self, target: &FerroObject) -> T {
118        target.get_value(self)
119    }
120
121    fn typed_get_base_value(&'static self, target: &FerroObject) -> Option<T> {
122        target.get_base_value(self)
123    }
124
125    fn typed_bind(
126        &'static self,
127        target: &FerroObject,
128        source: Rc<dyn IObservable<T>>,
129        priority: BindingPriority,
130    ) -> Rc<dyn IDisposable> {
131        target.bind(self, source, priority)
132    }
133
134    fn typed_bind_value(
135        &'static self,
136        target: &FerroObject,
137        source: Rc<dyn IObservable<BindingValue<T>>>,
138        priority: BindingPriority,
139    ) -> Rc<dyn IDisposable> {
140        target.bind_value(self, source, priority)
141    }
142}
143
144impl<T: PropertyValue> TypedProperty for DirectPropertyBase<T> {
145    type Value = T;
146
147    fn untyped(&'static self) -> &'static FerroProperty {
148        self
149    }
150
151    fn typed_get_value(&'static self, target: &FerroObject) -> T {
152        target.get_direct_value(self)
153    }
154
155    fn typed_get_base_value(&'static self, target: &FerroObject) -> Option<T> {
156        Some(target.get_direct_value(self))
157    }
158
159    fn typed_bind(
160        &'static self,
161        target: &FerroObject,
162        source: Rc<dyn IObservable<T>>,
163        _priority: BindingPriority,
164    ) -> Rc<dyn IDisposable> {
165        target.bind_direct(self, source)
166    }
167
168    fn typed_bind_value(
169        &'static self,
170        target: &FerroObject,
171        source: Rc<dyn IObservable<BindingValue<T>>>,
172        _priority: BindingPriority,
173    ) -> Rc<dyn IDisposable> {
174        target.bind_direct_value(self, source)
175    }
176}
177
178impl<TOwner: ObjectType, T: PropertyValue> TypedProperty for DirectProperty<TOwner, T> {
179    type Value = T;
180
181    fn untyped(&'static self) -> &'static FerroProperty {
182        self
183    }
184
185    fn typed_get_value(&'static self, target: &FerroObject) -> T {
186        target.get_direct_value(self)
187    }
188
189    fn typed_get_base_value(&'static self, target: &FerroObject) -> Option<T> {
190        Some(target.get_direct_value(self))
191    }
192
193    fn typed_bind(
194        &'static self,
195        target: &FerroObject,
196        source: Rc<dyn IObservable<T>>,
197        _priority: BindingPriority,
198    ) -> Rc<dyn IDisposable> {
199        target.bind_direct(self, source)
200    }
201
202    fn typed_bind_value(
203        &'static self,
204        target: &FerroObject,
205        source: Rc<dyn IObservable<BindingValue<T>>>,
206        _priority: BindingPriority,
207    ) -> Rc<dyn IDisposable> {
208        target.bind_direct_value(self, source)
209    }
210}
211
212/// Extension members for [`FerroObject`].
213pub trait FerroObjectExtensions {
214    /// Gets an observable for a property.
215    ///
216    /// The observable fires with the current value of the property on the
217    /// object immediately upon subscription, and then every time the value
218    /// changes. It holds a weak reference to the object.
219    fn get_observable<P: TypedProperty>(&self, property: &'static P) -> Rc<dyn IObservable<P::Value>>;
220
221    /// Gets an observable for a property, projecting each value with
222    /// `converter`.
223    fn get_observable_with<P: TypedProperty, TResult: PropertyValue>(
224        &self,
225        property: &'static P,
226        converter: impl Fn(P::Value) -> TResult + 'static,
227    ) -> Rc<dyn IObservable<TResult>>;
228
229    /// Gets an observable of untyped values for a property.
230    fn get_observable_untyped(&self, property: &'static FerroProperty) -> Rc<dyn IObservable<BoxedValue>>;
231
232    /// Gets an observable of untyped values for a property, projecting each
233    /// value with `converter`.
234    fn get_observable_untyped_with<TResult: PropertyValue>(
235        &self,
236        property: &'static FerroProperty,
237        converter: impl Fn(BoxedValue) -> TResult + 'static,
238    ) -> Rc<dyn IObservable<TResult>>;
239
240    /// Gets an observable of binding values for a property.
241    fn get_binding_observable<P: TypedProperty>(
242        &self,
243        property: &'static P,
244    ) -> Rc<dyn IObservable<BindingValue<P::Value>>>;
245
246    /// Gets an observable of binding values for a property, projecting each
247    /// value with `converter`.
248    fn get_binding_observable_with<P: TypedProperty, TResult: PropertyValue>(
249        &self,
250        property: &'static P,
251        converter: impl Fn(P::Value) -> TResult + 'static,
252    ) -> Rc<dyn IObservable<BindingValue<TResult>>>;
253
254    /// Gets an observable of untyped binding values for a property.
255    fn get_binding_observable_untyped(
256        &self,
257        property: &'static FerroProperty,
258    ) -> Rc<dyn IObservable<BindingValue<BoxedValue>>>;
259
260    /// Gets an observable of untyped binding values for a property,
261    /// projecting each value with `converter`.
262    fn get_binding_observable_untyped_with<TResult: PropertyValue>(
263        &self,
264        property: &'static FerroProperty,
265        converter: impl Fn(BoxedValue) -> TResult + 'static,
266    ) -> Rc<dyn IObservable<BindingValue<TResult>>>;
267
268    /// Gets an observable that listens for property changed events of a
269    /// property on this object. It fires each time the property changes and
270    /// does not fire with the current value on subscription.
271    fn get_property_changed_observable(&self, property: &'static FerroProperty) -> ObjectPropertyChangedObservable;
272
273    /// Binds a typed property of any kind to an observable. The priority is
274    /// ignored for direct properties.
275    fn bind_typed<P: TypedProperty>(
276        &self,
277        property: &'static P,
278        source: Rc<dyn IObservable<P::Value>>,
279        priority: BindingPriority,
280    ) -> Rc<dyn IDisposable>;
281
282    /// Binds a typed property of any kind to an observable of binding values.
283    /// The priority is ignored for direct properties.
284    fn bind_typed_value<P: TypedProperty>(
285        &self,
286        property: &'static P,
287        source: Rc<dyn IObservable<BindingValue<P::Value>>>,
288        priority: BindingPriority,
289    ) -> Rc<dyn IDisposable>;
290
291    /// Gets the value of a typed property of any kind.
292    fn get_typed_value<P: TypedProperty>(&self, property: &'static P) -> P::Value;
293
294    /// Gets the base value of a typed property of any kind: the value
295    /// excluding animated values. For direct properties this is the value.
296    fn get_typed_base_value<P: TypedProperty>(&self, property: &'static P) -> Option<P::Value>;
297
298    /// Gets the base value of a property as an untyped value; the unset
299    /// marker when there is none.
300    fn get_base_value_untyped(&self, property: &'static FerroProperty) -> BoxedValue;
301}
302
303impl FerroObjectExtensions for FerroObject {
304    fn get_observable<P: TypedProperty>(&self, property: &'static P) -> Rc<dyn IObservable<P::Value>> {
305        PropertyObservable::create(
306            self,
307            property.untyped(),
308            move |o| property.typed_get_value(o),
309            |a, b| a == b,
310            true,
311        )
312    }
313
314    fn get_observable_with<P: TypedProperty, TResult: PropertyValue>(
315        &self,
316        property: &'static P,
317        converter: impl Fn(P::Value) -> TResult + 'static,
318    ) -> Rc<dyn IObservable<TResult>> {
319        PropertyObservable::create(
320            self,
321            property.untyped(),
322            move |o| converter(property.typed_get_value(o)),
323            |a, b| a == b,
324            true,
325        )
326    }
327
328    fn get_observable_untyped(&self, property: &'static FerroProperty) -> Rc<dyn IObservable<BoxedValue>> {
329        PropertyObservable::create(self, property, move |o| o.get_value_untyped(property), |a, b| a == b, true)
330    }
331
332    fn get_observable_untyped_with<TResult: PropertyValue>(
333        &self,
334        property: &'static FerroProperty,
335        converter: impl Fn(BoxedValue) -> TResult + 'static,
336    ) -> Rc<dyn IObservable<TResult>> {
337        PropertyObservable::create(
338            self,
339            property,
340            move |o| converter(o.get_value_untyped(property)),
341            |a, b| a == b,
342            true,
343        )
344    }
345
346    fn get_binding_observable<P: TypedProperty>(
347        &self,
348        property: &'static P,
349    ) -> Rc<dyn IObservable<BindingValue<P::Value>>> {
350        PropertyObservable::create(
351            self,
352            property.untyped(),
353            move |o| BindingValue::new(property.typed_get_value(o)),
354            |a, b| a == b,
355            false,
356        )
357    }
358
359    fn get_binding_observable_with<P: TypedProperty, TResult: PropertyValue>(
360        &self,
361        property: &'static P,
362        converter: impl Fn(P::Value) -> TResult + 'static,
363    ) -> Rc<dyn IObservable<BindingValue<TResult>>> {
364        PropertyObservable::create(
365            self,
366            property.untyped(),
367            move |o| BindingValue::new(converter(property.typed_get_value(o))),
368            |a, b| a == b,
369            false,
370        )
371    }
372
373    fn get_binding_observable_untyped(
374        &self,
375        property: &'static FerroProperty,
376    ) -> Rc<dyn IObservable<BindingValue<BoxedValue>>> {
377        PropertyObservable::create(
378            self,
379            property,
380            move |o| BindingValue::new(o.get_value_untyped(property)),
381            |a, b| a == b,
382            false,
383        )
384    }
385
386    fn get_binding_observable_untyped_with<TResult: PropertyValue>(
387        &self,
388        property: &'static FerroProperty,
389        converter: impl Fn(BoxedValue) -> TResult + 'static,
390    ) -> Rc<dyn IObservable<BindingValue<TResult>>> {
391        PropertyObservable::create(
392            self,
393            property,
394            move |o| BindingValue::new(converter(o.get_value_untyped(property))),
395            |a, b| a == b,
396            false,
397        )
398    }
399
400    fn get_property_changed_observable(&self, property: &'static FerroProperty) -> ObjectPropertyChangedObservable {
401        ObjectPropertyChangedObservable { target: self.to_weak(), property }
402    }
403
404    fn bind_typed<P: TypedProperty>(
405        &self,
406        property: &'static P,
407        source: Rc<dyn IObservable<P::Value>>,
408        priority: BindingPriority,
409    ) -> Rc<dyn IDisposable> {
410        property.typed_bind(self, source, priority)
411    }
412
413    fn bind_typed_value<P: TypedProperty>(
414        &self,
415        property: &'static P,
416        source: Rc<dyn IObservable<BindingValue<P::Value>>>,
417        priority: BindingPriority,
418    ) -> Rc<dyn IDisposable> {
419        property.typed_bind_value(self, source, priority)
420    }
421
422    fn get_typed_value<P: TypedProperty>(&self, property: &'static P) -> P::Value {
423        property.typed_get_value(self)
424    }
425
426    fn get_typed_base_value<P: TypedProperty>(&self, property: &'static P) -> Option<P::Value> {
427        property.typed_get_base_value(self)
428    }
429
430    fn get_base_value_untyped(&self, property: &'static FerroProperty) -> BoxedValue {
431        property.routes().route_get_base_value(self)
432    }
433}
434
435/// The change notifications of one property on one object.
436///
437/// Returned by
438/// [`get_property_changed_observable`](FerroObjectExtensions::get_property_changed_observable).
439/// Change notifications borrow their values, so handlers are closures rather
440/// than observers. The object is held weakly.
441#[derive(Clone)]
442pub struct ObjectPropertyChangedObservable {
443    target: WeakRef<FerroObject>,
444    property: &'static FerroProperty,
445}
446
447impl ObjectPropertyChangedObservable {
448    /// Subscribes to changes of the property on the object. Disposing the
449    /// returned handle unsubscribes.
450    pub fn subscribe(&self, handler: impl Fn(&FerroPropertyChangedEventArgs<'_>) + 'static) -> Rc<dyn IDisposable> {
451        let property = self.property;
452        match self.target.upgrade() {
453            Some(target) => target.property_changed(move |e| {
454                if e.property() == property {
455                    handler(e);
456                }
457            }),
458            None => Disposable::empty(),
459        }
460    }
461
462    /// Subscribes a handler that is only invoked when the object is of class
463    /// `TTarget` (or a class derived from it).
464    pub fn add_class_handler<TTarget: ObjectType>(
465        &self,
466        handler: impl Fn(&TTarget, &FerroPropertyChangedEventArgs<'_>) + 'static,
467    ) -> Rc<dyn IDisposable> {
468        self.subscribe(move |e| {
469            if let Some(target) = e.sender().downcast_ref::<TTarget>() {
470                handler(target, e);
471            }
472        })
473    }
474}
475
476/// An observable over the value of a property on an object.
477///
478/// Listens to the object's change notifications while it has observers, and
479/// replays the current value to each new observer.
480struct PropertyObservable<TResult: Clone + 'static> {
481    this: Weak<PropertyObservable<TResult>>,
482    target: WeakRef<FerroObject>,
483    property: &'static FerroProperty,
484    read: Box<dyn Fn(&FerroObject) -> TResult>,
485    same: fn(&TResult, &TResult) -> bool,
486    /// Whether the cached value is dropped when the last observer leaves.
487    clear_value_on_deinitialize: bool,
488    value: RefCell<Option<TResult>>,
489    observers: RefCell<Vec<(u64, Rc<dyn IObserver<TResult>>)>>,
490    next_id: Cell<u64>,
491    subscription: RefCell<Option<Rc<dyn IDisposable>>>,
492}
493
494impl<TResult: Clone + 'static> PropertyObservable<TResult> {
495    fn create(
496        target: &FerroObject,
497        property: &'static FerroProperty,
498        read: impl Fn(&FerroObject) -> TResult + 'static,
499        same: fn(&TResult, &TResult) -> bool,
500        clear_value_on_deinitialize: bool,
501    ) -> Rc<dyn IObservable<TResult>> {
502        Rc::new_cyclic(|this| Self {
503            this: this.clone(),
504            target: target.to_weak(),
505            property,
506            read: Box::new(read),
507            same,
508            clear_value_on_deinitialize,
509            value: RefCell::new(None),
510            observers: RefCell::new(Vec::new()),
511            next_id: Cell::new(0),
512            subscription: RefCell::new(None),
513        })
514    }
515
516    fn initialize(&self) {
517        let Some(target) = self.target.upgrade() else { return };
518        let value = (self.read)(&target);
519        *self.value.borrow_mut() = Some(value);
520        // The handler keeps the observable alive for as long as it has
521        // observers, even if the caller dropped its own reference.
522        let Some(this) = self.this.upgrade() else { return };
523        let subscription = target.property_changed(move |e| this.property_changed(e));
524        *self.subscription.borrow_mut() = Some(subscription);
525    }
526
527    fn deinitialize(&self) {
528        let subscription = self.subscription.take();
529        if let Some(subscription) = subscription {
530            subscription.dispose();
531        }
532        if self.clear_value_on_deinitialize {
533            *self.value.borrow_mut() = None;
534        }
535    }
536
537    fn property_changed(&self, e: &FerroPropertyChangedEventArgs<'_>) {
538        if e.property() != self.property {
539            return;
540        }
541        let new_value = (self.read)(e.sender());
542        let changed = match &*self.value.borrow() {
543            Some(current) => !(self.same)(&new_value, current),
544            None => true,
545        };
546        if changed {
547            *self.value.borrow_mut() = Some(new_value.clone());
548            let observers: Vec<Rc<dyn IObserver<TResult>>> =
549                self.observers.borrow().iter().map(|(_, o)| o.clone()).collect();
550            match observers.len() {
551                0 => {}
552                1 => observers[0].on_next(new_value),
553                _ => {
554                    for observer in observers {
555                        observer.on_next(new_value.clone());
556                    }
557                }
558            }
559        }
560    }
561
562    fn remove(&self, id: u64) {
563        let now_empty = {
564            let mut observers = self.observers.borrow_mut();
565            let before = observers.len();
566            observers.retain(|(i, _)| *i != id);
567            before != observers.len() && observers.is_empty()
568        };
569        if now_empty {
570            self.deinitialize();
571        }
572    }
573}
574
575impl<TResult: Clone + 'static> IObservable<TResult> for PropertyObservable<TResult> {
576    fn subscribe(&self, observer: Rc<dyn IObserver<TResult>>) -> Rc<dyn IDisposable> {
577        let id = self.next_id.get();
578        self.next_id.set(id + 1);
579        let first = {
580            let mut observers = self.observers.borrow_mut();
581            observers.push((id, observer.clone()));
582            observers.len() == 1
583        };
584        if first {
585            self.initialize();
586        }
587        let current = self.value.borrow().clone();
588        if let Some(value) = current {
589            observer.on_next(value);
590        }
591        match self.this.upgrade() {
592            Some(this) => Disposable::create(move || this.remove(id)),
593            None => Disposable::empty(),
594        }
595    }
596}