Skip to main content

ferroui_base/utilities/
weak_events.rs

1use super::{EventArgs, WeakEvent, WeakEventArgs};
2use crate::data::model::{CollectionChange, INotifyCollectionChanged, INotifyPropertyChanged};
3use crate::input::ICommand;
4use crate::reactive::IDisposable;
5use crate::{FerroObject, FerroPropertyChangedEventArgs, ObjectType, Ref};
6use std::any::Any;
7use std::cell::RefCell;
8use std::rc::{Rc, Weak};
9
10impl WeakEventArgs for CollectionChange {
11    type Args<'a> = CollectionChange;
12}
13
14impl WeakEventArgs for FerroPropertyChangedEventArgs<'static> {
15    type Args<'a> = FerroPropertyChangedEventArgs<'a>;
16}
17
18/// The weak events of the framework's notification contracts.
19///
20/// Each event is a per-thread static, as everything the event refers to
21/// belongs to one thread; an accessor returns the event of the calling
22/// thread.
23pub struct WeakEvents;
24
25crate::ferro_thread_local! {
26    static COLLECTION_CHANGED: Rc<WeakEvent<Rc<dyn INotifyCollectionChanged>, CollectionChange>> =
27        WeakEvent::<Rc<dyn INotifyCollectionChanged>, CollectionChange>::register_returning_unsubscribe(|c, s| {
28            // The handler belongs to the collection, so it refers to it weakly.
29            let sender = Rc::downgrade(c);
30            let handler = c.collection_changed().add(Rc::new(move |e: &CollectionChange| {
31                let c = sender.upgrade();
32                s(c.as_ref().map(|c| c as &dyn Any), e)
33            }));
34            let c = Rc::downgrade(c);
35            Box::new(move || {
36                if let Some(c) = c.upgrade() {
37                    c.collection_changed().remove(handler);
38                }
39            })
40        });
41
42    static THREAD_SAFE_PROPERTY_CHANGED: Rc<WeakEvent<Rc<dyn INotifyPropertyChanged>, str>> =
43        WeakEvent::<Rc<dyn INotifyPropertyChanged>, str>::register_returning_unsubscribe(|s, h| {
44            let sender = Rc::downgrade(s);
45            let handler = s.property_changed().add(Rc::new(move |e: &str| {
46                let s = sender.upgrade();
47                h(s.as_ref().map(|s| s as &dyn Any), e)
48            }));
49            let s = Rc::downgrade(s);
50            Box::new(move || {
51                if let Some(s) = s.upgrade() {
52                    s.property_changed().remove(handler);
53                }
54            })
55        });
56
57    static FERRO_PROPERTY_CHANGED: Rc<WeakEvent<Ref<FerroObject>, FerroPropertyChangedEventArgs<'static>>> =
58        WeakEvent::<Ref<FerroObject>, FerroPropertyChangedEventArgs<'static>>::register_returning_unsubscribe(
59            |s, h| {
60                let sender = s.downgrade();
61                let handler = s.property_changed(move |e: &FerroPropertyChangedEventArgs<'_>| {
62                    let s = sender.upgrade();
63                    h(s.as_ref().map(|s| s as &dyn Any), e)
64                });
65                Box::new(move || handler.dispose())
66            },
67        );
68
69    static COMMAND_CAN_EXECUTE_CHANGED: Rc<WeakEvent<Rc<dyn ICommand>, EventArgs>> =
70        WeakEvent::<Rc<dyn ICommand>, EventArgs>::register_returning_unsubscribe(|s, h| {
71            let sender = Rc::downgrade(s);
72            let handler = s.can_execute_changed(Rc::new(move || {
73                if let Some(s) = sender.upgrade() {
74                    h(Some(&s as &dyn Any), &EventArgs::EMPTY)
75                }
76            }));
77            Box::new(move || handler.dispose())
78        });
79}
80
81impl WeakEvents {
82    /// Represents CollectionChanged event from [`INotifyCollectionChanged`]
83    pub fn collection_changed() -> Rc<WeakEvent<Rc<dyn INotifyCollectionChanged>, CollectionChange>> {
84        COLLECTION_CHANGED.with(Rc::clone)
85    }
86
87    /// Represents PropertyChanged event from [`INotifyPropertyChanged`] with
88    /// auto-dispatching to the UI thread.
89    ///
90    /// A model object is bound to the thread it was created on (it is held
91    /// through `Rc`), so its notifications are raised on that thread and are
92    /// forwarded there directly.
93    pub fn thread_safe_property_changed() -> Rc<WeakEvent<Rc<dyn INotifyPropertyChanged>, str>> {
94        THREAD_SAFE_PROPERTY_CHANGED.with(Rc::clone)
95    }
96
97    /// Represents PropertyChanged event from [`FerroObject`]
98    pub fn ferro_property_changed() -> Rc<WeakEvent<Ref<FerroObject>, FerroPropertyChangedEventArgs<'static>>> {
99        FERRO_PROPERTY_CHANGED.with(Rc::clone)
100    }
101
102    /// Represents CanExecuteChanged event from [`ICommand`]
103    pub fn command_can_execute_changed() -> Rc<WeakEvent<Rc<dyn ICommand>, EventArgs>> {
104        COMMAND_CAN_EXECUTE_CHANGED.with(Rc::clone)
105    }
106
107    /// Subscribes `subscriber` to the "can execute changed" event of
108    /// `command`. `handler` is called with the subscriber each time the
109    /// event is raised.
110    ///
111    /// Not in the original: the command sources (button, menu item, split
112    /// button, native menu item) subscribe through this handle-based form.
113    ///
114    /// The subscriber keeps the returned handle (normally in a field). The
115    /// subscription ends when the handle is disposed, when the handle is
116    /// dropped (so with the subscriber), or, if the handle is kept alive
117    /// elsewhere, the first time the event is raised after the subscriber
118    /// is gone. The command never holds a strong reference to the
119    /// subscriber.
120    pub fn subscribe_command_can_execute_changed<T: ObjectType>(
121        command: &Rc<dyn ICommand>,
122        subscriber: &Ref<T>,
123        handler: fn(&T),
124    ) -> Rc<dyn IDisposable> {
125        let subscription = Rc::new(WeakEventSubscription { inner: RefCell::new(None) });
126
127        let weak_subscriber = subscriber.downgrade();
128        let weak_subscription: Weak<WeakEventSubscription> = Rc::downgrade(&subscription);
129        let inner = command.can_execute_changed(Rc::new(move || match weak_subscriber.upgrade() {
130            Some(subscriber) => handler(&subscriber),
131            None => {
132                if let Some(subscription) = weak_subscription.upgrade() {
133                    subscription.dispose();
134                }
135            }
136        }));
137        *subscription.inner.borrow_mut() = Some(inner);
138
139        subscription
140    }
141}
142
143/// The handle of a weak event subscription: unsubscribes when disposed and
144/// when dropped.
145struct WeakEventSubscription {
146    inner: RefCell<Option<Rc<dyn IDisposable>>>,
147}
148
149impl IDisposable for WeakEventSubscription {
150    fn dispose(&self) {
151        let inner = self.inner.borrow_mut().take();
152        if let Some(inner) = inner {
153            inner.dispose();
154        }
155    }
156}
157
158impl Drop for WeakEventSubscription {
159    fn drop(&mut self) {
160        self.dispose();
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use crate::input::KeyBinding;
168    use crate::reactive::Disposable;
169    use crate::BoxedValue;
170    use std::cell::Cell;
171
172    /// A command that counts its subscribers.
173    struct CountingCommand {
174        this: Weak<CountingCommand>,
175        handlers: RefCell<Vec<(u64, Rc<dyn Fn()>)>>,
176        next_token: Cell<u64>,
177    }
178
179    impl CountingCommand {
180        fn new() -> Rc<Self> {
181            Rc::new_cyclic(|this| Self { this: this.clone(), handlers: RefCell::new(Vec::new()), next_token: Cell::new(0) })
182        }
183
184        fn subscription_count(&self) -> usize {
185            self.handlers.borrow().len()
186        }
187
188        fn raise(&self) {
189            let handlers: Vec<Rc<dyn Fn()>> = self.handlers.borrow().iter().map(|(_, h)| h.clone()).collect();
190            for handler in handlers {
191                handler();
192            }
193        }
194    }
195
196    impl ICommand for CountingCommand {
197        fn can_execute(&self, _parameter: Option<&BoxedValue>) -> bool {
198            true
199        }
200
201        fn execute(&self, _parameter: Option<&BoxedValue>) {}
202
203        fn can_execute_changed(&self, handler: Rc<dyn Fn()>) -> Rc<dyn IDisposable> {
204            let token = self.next_token.get();
205            self.next_token.set(token + 1);
206            self.handlers.borrow_mut().push((token, handler));
207            let weak = self.this.clone();
208            Disposable::create(move || {
209                if let Some(this) = weak.upgrade() {
210                    this.handlers.borrow_mut().retain(|(t, _)| *t != token);
211                }
212            })
213        }
214    }
215
216    crate::ferro_thread_local! {
217        static CALLS: Cell<i32> = const { Cell::new(0) };
218    }
219
220    fn on_event(_subscriber: &KeyBinding) {
221        CALLS.with(|calls| calls.set(calls.get() + 1));
222    }
223
224    #[test]
225    fn handler_is_called_while_the_subscriber_is_alive() {
226        CALLS.with(|calls| calls.set(0));
227        let command = CountingCommand::new();
228        let as_command: Rc<dyn ICommand> = command.clone();
229        let subscriber = KeyBinding::new();
230
231        let _subscription = WeakEvents::subscribe_command_can_execute_changed(&as_command, &subscriber, on_event);
232        command.raise();
233        command.raise();
234
235        assert_eq!(1, command.subscription_count());
236        assert_eq!(2, CALLS.with(Cell::get));
237    }
238
239    #[test]
240    fn subscription_does_not_keep_the_subscriber_alive() {
241        let command = CountingCommand::new();
242        let as_command: Rc<dyn ICommand> = command.clone();
243        let subscriber = KeyBinding::new();
244        let weak = subscriber.downgrade();
245
246        let _subscription = WeakEvents::subscribe_command_can_execute_changed(&as_command, &subscriber, on_event);
247        drop(subscriber);
248
249        assert!(weak.upgrade().is_none());
250    }
251
252    #[test]
253    fn dropping_the_handle_unsubscribes() {
254        let command = CountingCommand::new();
255        let as_command: Rc<dyn ICommand> = command.clone();
256        let subscriber = KeyBinding::new();
257
258        let subscription = WeakEvents::subscribe_command_can_execute_changed(&as_command, &subscriber, on_event);
259        assert_eq!(1, command.subscription_count());
260        drop(subscription);
261
262        assert_eq!(0, command.subscription_count());
263    }
264
265    #[test]
266    fn disposing_the_handle_unsubscribes_once() {
267        let command = CountingCommand::new();
268        let as_command: Rc<dyn ICommand> = command.clone();
269        let subscriber = KeyBinding::new();
270
271        let subscription = WeakEvents::subscribe_command_can_execute_changed(&as_command, &subscriber, on_event);
272        subscription.dispose();
273        subscription.dispose();
274
275        assert_eq!(0, command.subscription_count());
276    }
277
278    #[test]
279    fn subscription_removes_itself_on_the_next_raise_after_the_subscriber_is_gone() {
280        CALLS.with(|calls| calls.set(0));
281        let command = CountingCommand::new();
282        let as_command: Rc<dyn ICommand> = command.clone();
283        let subscriber = KeyBinding::new();
284
285        // The handle is kept alive by someone other than the subscriber.
286        let subscription = WeakEvents::subscribe_command_can_execute_changed(&as_command, &subscriber, on_event);
287        drop(subscriber);
288        assert_eq!(1, command.subscription_count());
289
290        command.raise();
291
292        assert_eq!(0, command.subscription_count());
293        assert_eq!(0, CALLS.with(Cell::get));
294        drop(subscription);
295    }
296}