Skip to main content

ashlar_native/
modal.rs

1//! A hook around every call that blocks the calling thread in the OS until the user answers (the
2//! native file dialogs and message boxes). A frame-loop hang watchdog pauses itself here: a dialog
3//! left open for a minute is not a hung app. Always compiled, so code without the `dialogs`
4//! feature can install the hook too.
5
6use std::sync::{Arc, PoisonError, RwLock};
7
8/// Called with `true` just before a blocking native call and `false` just after it (also when the
9/// call failed or panicked). Must be cheap and must not block.
10pub type ModalHook = Arc<dyn Fn(bool) + Send + Sync>;
11
12static HOOK: RwLock<Option<ModalHook>> = RwLock::new(None);
13
14/// Install the process-wide hook (`None` removes it). Replaces any earlier one.
15pub fn set_hook(hook: Option<ModalHook>) {
16    *HOOK.write().unwrap_or_else(PoisonError::into_inner) = hook;
17}
18
19/// Remove `hook` if it is still the installed one (another may have replaced it since).
20pub fn clear_if(hook: &ModalHook) {
21    let mut slot = HOOK.write().unwrap_or_else(PoisonError::into_inner);
22    if slot.as_ref().is_some_and(|h| Arc::ptr_eq(h, hook)) {
23        *slot = None;
24    }
25}
26
27/// Run `f`, a blocking native call, between the hook's `true` and `false`.
28pub fn around<T>(f: impl FnOnce() -> T) -> T {
29    let hook = HOOK.read().unwrap_or_else(PoisonError::into_inner).clone();
30    let _end = Ended(hook.clone());
31    if let Some(h) = &hook {
32        h(true);
33    }
34    f()
35}
36
37/// Calls the hook's `false` when dropped, so a panic inside the call still ends the pause.
38struct Ended(Option<ModalHook>);
39
40impl Drop for Ended {
41    fn drop(&mut self) {
42        if let Some(h) = &self.0 {
43            h(false);
44        }
45    }
46}
47
48#[cfg(test)]
49mod tests {
50    use std::sync::Mutex;
51
52    use super::*;
53
54    #[test]
55    fn the_hook_brackets_the_call_even_when_it_panics() {
56        let seen: Arc<Mutex<Vec<bool>>> = Arc::default();
57        let sink = seen.clone();
58        set_hook(Some(Arc::new(move |on| sink.lock().unwrap().push(on))));
59        assert_eq!(around(|| 7), 7);
60        let r = std::panic::catch_unwind(|| around(|| -> u8 { panic!("inside the dialog") }));
61        assert!(r.is_err());
62        set_hook(None);
63        assert_eq!(around(|| 1), 1);
64        assert_eq!(*seen.lock().unwrap(), [true, false, true, false]);
65        // clear_if removes only the hook it is given
66        let a: ModalHook = Arc::new(|_| {});
67        let b: ModalHook = Arc::new(|_| {});
68        set_hook(Some(a.clone()));
69        clear_if(&b);
70        assert!(HOOK.read().unwrap().is_some());
71        clear_if(&a);
72        assert!(HOOK.read().unwrap().is_none());
73    }
74}