Skip to main content

ferroui_base/controls/
resource_node_extensions.rs

1use super::{
2    IResourceHost, IResourceProvider, ResourceHostRef, ResourceKey, ResourceValue, ResourcesChangedEventArgs,
3    WeakResourceHost,
4};
5use crate::reactive::{Disposable, IDisposable, IObservable, IObserver};
6use crate::styling::ThemeVariant;
7use crate::{BoxedValue, FerroProperty};
8use std::cell::{Cell, RefCell};
9use std::rc::{Rc, Weak};
10
11/// A function applied to a resource value before it is published by a
12/// resource observable.
13pub type ResourceConverter = Rc<dyn Fn(Option<BoxedValue>) -> Option<BoxedValue>>;
14
15impl dyn IResourceHost + '_ {
16    /// Finds the specified resource by searching up the logical tree and then
17    /// global styles.
18    ///
19    /// Returns the resource, or the unset value marker if not found.
20    pub fn find_resource(&self, key: &ResourceKey) -> ResourceValue {
21        match self.try_find_resource(key, None) {
22            Some(value) => value,
23            None => Some(FerroProperty::unset_value()),
24        }
25    }
26
27    /// Finds the specified resource for a theme variant by searching up the
28    /// logical tree and then global styles.
29    ///
30    /// Returns the resource, or the unset value marker if not found.
31    pub fn find_resource_for_theme(&self, theme: Option<&ThemeVariant>, key: &ResourceKey) -> ResourceValue {
32        match self.try_find_resource(key, theme) {
33            Some(value) => value,
34            None => Some(FerroProperty::unset_value()),
35        }
36    }
37
38    /// Tries to find the specified resource by searching up the logical tree
39    /// and then global styles.
40    pub fn try_find_resource(&self, key: &ResourceKey, theme: Option<&ThemeVariant>) -> Option<ResourceValue> {
41        crate::perf_count!(ResourceLookups);
42        crate::perf_count!(ResourceHostsProbed);
43        if let Some(value) = self.try_get_resource(key, theme) {
44            return Some(value);
45        }
46
47        let mut current = self.as_style_host().and_then(|h| h.styling_parent());
48        while let Some(host) = current {
49            crate::perf_count!(ResourceHostsProbed);
50            if let Some(value) = host.try_get_resource(key, theme) {
51                return Some(value);
52            }
53            current = host.styling_parent();
54        }
55
56        None
57    }
58}
59
60impl ResourceHostRef {
61    /// Gets an observable for the resource with the specified key.
62    ///
63    /// The observable produces the current value on subscription and a new
64    /// value whenever the resources visible to the host, or its theme
65    /// variant, change. It holds the host weakly; while it has observers the
66    /// host keeps it alive.
67    pub fn resource_observable(
68        &self,
69        key: impl Into<ResourceKey>,
70        converter: Option<ResourceConverter>,
71    ) -> Rc<dyn IObservable<Option<BoxedValue>>> {
72        ResourceObservable::create(Target::Host(self.downgrade()), key.into(), None, converter)
73    }
74}
75
76/// Gets an observable for the resource with the specified key as seen by the
77/// owner of a resource provider.
78///
79/// The observable produces a value whenever the provider has an owner and the
80/// resources visible to that owner, the owner itself or (unless
81/// `default_theme_variant` is given) its theme variant change.
82///
83/// While it has observers the observable and the provider keep each other
84/// alive; dispose the subscriptions to release them.
85pub fn get_floating_resource_observable(
86    resource_provider: Rc<dyn IResourceProvider>,
87    key: impl Into<ResourceKey>,
88    default_theme_variant: Option<ThemeVariant>,
89    converter: Option<ResourceConverter>,
90) -> Rc<dyn IObservable<Option<BoxedValue>>> {
91    ResourceObservable::create(Target::Provider(resource_provider), key.into(), default_theme_variant, converter)
92}
93
94type ResourceObserver = Rc<dyn IObserver<Option<BoxedValue>>>;
95
96enum Target {
97    Host(WeakResourceHost),
98    Provider(Rc<dyn IResourceProvider>),
99}
100
101struct ResourceObservable {
102    this: Weak<ResourceObservable>,
103    target: Target,
104    key: ResourceKey,
105    override_theme_variant: Option<ThemeVariant>,
106    converter: Option<ResourceConverter>,
107    observers: RefCell<Vec<(u64, ResourceObserver)>>,
108    next_id: Cell<u64>,
109    owner_changed_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
110    host_subscriptions: RefCell<Vec<Rc<dyn IDisposable>>>,
111}
112
113impl ResourceObservable {
114    fn create(
115        target: Target,
116        key: ResourceKey,
117        override_theme_variant: Option<ThemeVariant>,
118        converter: Option<ResourceConverter>,
119    ) -> Rc<dyn IObservable<Option<BoxedValue>>> {
120        Rc::new_cyclic(|this| ResourceObservable {
121            this: this.clone(),
122            target,
123            key,
124            override_theme_variant,
125            converter,
126            observers: RefCell::new(Vec::new()),
127            next_id: Cell::new(0),
128            owner_changed_subscription: RefCell::new(None),
129            host_subscriptions: RefCell::new(Vec::new()),
130        })
131    }
132
133    fn host(&self) -> Option<ResourceHostRef> {
134        match &self.target {
135            Target::Host(host) => host.upgrade(),
136            Target::Provider(provider) => provider.owner(),
137        }
138    }
139
140    fn has_host(&self) -> bool {
141        match &self.target {
142            Target::Host(_) => true,
143            Target::Provider(provider) => provider.owner().is_some(),
144        }
145    }
146
147    fn initialize(&self) {
148        if let Target::Provider(provider) = &self.target {
149            // The subscriptions keep the observable alive, so that it keeps
150            // publishing while it has observers even if nothing else holds
151            // it. They are released when the last observer unsubscribes.
152            let Some(this) = self.this.upgrade() else { return };
153            let subscription = provider.owner_changed(Rc::new(move || {
154                this.unsubscribe_host();
155                this.subscribe_host();
156                this.publish_next();
157            }));
158            *self.owner_changed_subscription.borrow_mut() = Some(subscription);
159        }
160        self.subscribe_host();
161    }
162
163    fn deinitialize(&self) {
164        let subscription = self.owner_changed_subscription.borrow_mut().take();
165        if let Some(subscription) = subscription {
166            subscription.dispose();
167        }
168        self.unsubscribe_host();
169    }
170
171    fn subscribe_host(&self) {
172        let Some(host) = self.host() else { return };
173        let mut subscriptions = Vec::with_capacity(2);
174
175        let Some(this) = self.this.upgrade() else { return };
176        let observable = this.clone();
177        subscriptions.push(host.resources_changed(Rc::new(move |_: &ResourcesChangedEventArgs| {
178            observable.publish_next();
179        })));
180
181        if self.override_theme_variant.is_none() {
182            if let Some(theme_host) = host.as_theme_variant_host() {
183                subscriptions.push(theme_host.actual_theme_variant_changed(Rc::new(move || {
184                    this.publish_next();
185                })));
186            }
187        }
188
189        *self.host_subscriptions.borrow_mut() = subscriptions;
190    }
191
192    fn unsubscribe_host(&self) {
193        let subscriptions = std::mem::take(&mut *self.host_subscriptions.borrow_mut());
194        for subscription in subscriptions {
195            subscription.dispose();
196        }
197    }
198
199    fn publish_next(&self) {
200        if !self.has_host() {
201            return;
202        }
203        let value = self.get_value();
204        let observers: Vec<_> = self.observers.borrow().iter().map(|(_, o)| o.clone()).collect();
205        for observer in observers {
206            observer.on_next(value.clone());
207        }
208    }
209
210    fn get_value(&self) -> Option<BoxedValue> {
211        let value = match self.host() {
212            Some(host) => {
213                let theme = self
214                    .override_theme_variant
215                    .clone()
216                    .or_else(|| host.as_theme_variant_host().and_then(|h| h.actual_theme_variant()));
217                host.find_resource_for_theme(theme.as_ref(), &self.key)
218            }
219            None => Some(FerroProperty::unset_value()),
220        };
221        // A null resource is published as the unset value, as is a missing one.
222        let value = value.or_else(|| Some(FerroProperty::unset_value()));
223        match &self.converter {
224            Some(converter) => converter(value.clone()).or(value),
225            None => value,
226        }
227    }
228}
229
230impl IObservable<Option<BoxedValue>> for ResourceObservable {
231    fn subscribe(&self, observer: Rc<dyn IObserver<Option<BoxedValue>>>) -> Rc<dyn IDisposable> {
232        let first = self.observers.borrow().is_empty();
233        let id = self.next_id.get();
234        self.next_id.set(id + 1);
235        self.observers.borrow_mut().push((id, observer.clone()));
236
237        if first {
238            self.initialize();
239        }
240
241        if self.has_host() {
242            observer.on_next(self.get_value());
243        }
244
245        let weak = self.this.clone();
246        Disposable::create(move || {
247            if let Some(this) = weak.upgrade() {
248                let empty = {
249                    let mut observers = this.observers.borrow_mut();
250                    observers.retain(|(i, _)| *i != id);
251                    observers.is_empty()
252                };
253                if empty {
254                    this.deinitialize();
255                }
256            }
257        })
258    }
259}