Skip to main content

rustrails_record/
no_touching.rs

1use std::cell::Cell;
2
3thread_local! {
4    static NO_TOUCHING_DEPTH: Cell<usize> = const { Cell::new(0) };
5}
6
7/// Disables touch updates within the scope of a closure.
8pub struct NoTouching;
9
10impl NoTouching {
11    /// Runs `f` with touch propagation disabled on the current thread.
12    pub fn apply<F, R>(f: F) -> R
13    where
14        F: FnOnce() -> R,
15    {
16        struct Guard;
17
18        impl Drop for Guard {
19            fn drop(&mut self) {
20                NO_TOUCHING_DEPTH.with(|depth| depth.set(depth.get().saturating_sub(1)));
21            }
22        }
23
24        NO_TOUCHING_DEPTH.with(|depth| depth.set(depth.get() + 1));
25        let _guard = Guard;
26        f()
27    }
28
29    /// Returns `true` when touches are disabled on the current thread.
30    #[must_use]
31    pub fn is_active() -> bool {
32        NO_TOUCHING_DEPTH.with(|depth| depth.get() > 0)
33    }
34}
35
36pub(crate) fn is_disabled() -> bool {
37    NoTouching::is_active()
38}
39
40#[cfg(test)]
41mod tests {
42    use std::thread;
43
44    use super::{NoTouching, is_disabled};
45
46    #[test]
47    fn apply_enables_no_touching_for_block() {
48        let active = NoTouching::apply(NoTouching::is_active);
49        assert!(active);
50        assert!(!NoTouching::is_active());
51    }
52
53    #[test]
54    fn apply_restores_previous_state_after_return() {
55        NoTouching::apply(|| {
56            assert!(NoTouching::is_active());
57        });
58
59        assert!(!NoTouching::is_active());
60    }
61
62    #[test]
63    fn apply_restores_state_after_panic() {
64        let result = std::panic::catch_unwind(|| {
65            NoTouching::apply(|| panic!("boom"));
66        });
67
68        assert!(result.is_err());
69        assert!(!NoTouching::is_active());
70    }
71
72    #[test]
73    fn nested_apply_keeps_no_touching_active_until_outer_scope_exits() {
74        NoTouching::apply(|| {
75            assert!(NoTouching::is_active());
76            NoTouching::apply(|| assert!(NoTouching::is_active()));
77            assert!(NoTouching::is_active());
78        });
79
80        assert!(!NoTouching::is_active());
81    }
82
83    #[test]
84    fn state_is_thread_local() {
85        let handle = thread::spawn(NoTouching::is_active);
86        NoTouching::apply(|| {
87            assert!(NoTouching::is_active());
88            assert!(!handle.join().expect("thread should complete"));
89        });
90    }
91
92    #[test]
93    fn crate_helper_tracks_public_state() {
94        assert_eq!(is_disabled(), NoTouching::is_active());
95    }
96}