git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

src/motion.rs (3K)

 1 //! The DE-wide animations switch.
 2 //!
 3 //! One flag, followed by every cce-ui widget that eases and by the
 4 //! compositor: when it is off, anything that would glide, fade, slide or
 5 //! coast lands on its target in the same frame instead. Disabled means
 6 //! "snap", never "freeze" — a dropdown still opens, a scroll still moves.
 7 //!
 8 //! The switch is a file, [`STATE_PATH`], holding `on` or `off`. The System
 9 //! Interface's Power page sets it per power mode and `cce-power-apply`
10 //! writes it as root whenever the mode changes (plug, unplug, boot), which
11 //! is why it lives under /run rather than in `~/.config/cce`: the writer has
12 //! no session and no `$HOME`. No file means on.
13 //!
14 //! [`enabled`] is cheap enough for per-frame use: it re-reads the file at
15 //! most every [`RECHECK`], so a running client follows a mode change within
16 //! half a second and nothing needs restarting or reloading. `CCE_ANIMATIONS`
17 //! (`0`/`off` or `1`/`on`) overrides the file for one process, for testing.
18 
19 use std::sync::Mutex;
20 use std::time::{Duration, Instant};
21 
22 /// Where the switch lives. Shared with `cce-power-apply`, the writer.
23 pub const STATE_PATH: &str = "/run/cce/animations";
24 
25 /// How stale [`enabled`] may be. A mode change is a plug or an unplug, so
26 /// half a second is instant to a person, and the stat stays off the frame.
27 pub const RECHECK: Duration = Duration::from_millis(500);
28 
29 /// `on` / `off` (surrounding whitespace ignored); anything else says nothing.
30 pub fn parse(text: &str) -> Option<bool> {
31     match text.trim() {
32         "on" | "1" | "true" => Some(true),
33         "off" | "0" | "false" => Some(false),
34         _ => None,
35     }
36 }
37 
38 /// What the state file says right now, uncached. `None` when there is no
39 /// file or it holds nothing recognizable — both of which mean "animate".
40 pub fn read_state() -> Option<bool> {
41     parse(&std::fs::read_to_string(STATE_PATH).ok()?)
42 }
43 
44 static CACHE: Mutex<Option<(Instant, bool)>> = Mutex::new(None);
45 
46 /// Whether to animate. Every easing in the toolkit asks this before it
47 /// steps, and snaps to its target when the answer is no.
48 pub fn enabled() -> bool {
49     static ENV: std::sync::OnceLock<Option<bool>> = std::sync::OnceLock::new();
50     if let Some(forced) = *ENV.get_or_init(|| std::env::var("CCE_ANIMATIONS").ok().and_then(|v| parse(&v))) {
51         return forced;
52     }
53     let mut cache = CACHE.lock().unwrap_or_else(|e| e.into_inner());
54     let now = Instant::now();
55     match *cache {
56         Some((at, value)) if now.duration_since(at) < RECHECK => value,
57         _ => {
58             let value = read_state().unwrap_or(true);
59             *cache = Some((now, value));
60             value
61         }
62     }
63 }
64 
65 #[cfg(test)]
66 mod tests {
67     use super::*;
68 
69     #[test]
70     fn parse_reads_the_helpers_spelling_and_nothing_else() {
71         assert_eq!(parse("off\n"), Some(false));
72         assert_eq!(parse("on"), Some(true));
73         assert_eq!(parse(" 0 "), Some(false));
74         assert_eq!(parse(""), None);
75         assert_eq!(parse("disabled"), None);
76     }
77 }