GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
fix: the test suite's animations switch is its own, not the machine's
motion::enabled() read /run/cce/animations in the test binary too, so the
spreadsheet's two wheel-glide tests and the scroll region's fade test
passed on mains and failed on battery, reading as a broken glide rather
than a borrowed switch. Under cfg(test) it now answers on, or whatever
motion::force_for_test set on the calling thread — thread-local, because
libtest runs tests in parallel and a process-wide override would race
every other test's read. The shipped path is unchanged
(enabled_on_this_machine: the CCE_ANIMATIONS override, else the file,
re-read at most every RECHECK).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CLAUDE.md | 15 +++++++++++++++
src/motion.rs | 49 +++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 64 insertions(+)
diff --git a/CLAUDE.md b/CLAUDE.md
index 72e3ab5..729e080 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -910,3 +910,18 @@ re-layout on every rect assignment (`apply_label_layout` /
`relabel_rows`), not a flag; `Adapted::clear_label` is the way a label
comes OFF a widget, and `slider::detached_strip` reads an empty label as
none for the widgets that store whatever they are handed.
+
+### The suite's animations switch is its own
+
+`motion::enabled()` reads `/run/cce/animations` in a shipped binary (see
+"Animations switch" in `../cce-compositor/WORKSPACE.md`). Under `cfg(test)`
+it does NOT: it answers on, or whatever `motion::force_for_test` set on the
+calling thread. Until 2026-09-28 the test binary read the machine's file,
+so the spreadsheet's wheel-glide tests and the scroll region's fade test
+passed on mains and failed on battery — the same lesson as cce-designer's
+pinned settings path and lattice, one layer down. A test wanting the
+snap-instead-of-ease path forces it for its thread; nothing sets
+`CCE_ANIMATIONS`, which is process-wide and would race the parallel suite.
+This pins only cce-ui's own suite: a dependent's test binary links cce-ui
+without `cfg(test)`, so a dependent test that eases still follows the
+machine — none does today.
diff --git a/src/motion.rs b/src/motion.rs
index 443bf19..4153f63 100644
--- a/src/motion.rs
+++ b/src/motion.rs
@@ -43,9 +43,44 @@ pub fn read_state() -> Option<bool> {
static CACHE: Mutex<Option<(Instant, bool)>> = Mutex::new(None);
+/// Under `cfg(test)` the switch is the SUITE's, not the machine's: on,
+/// unless a test forces it with [`force_for_test`]. Until 2026-09-28
+/// `enabled` read `/run/cce/animations` in the test binary too, so three
+/// glide and fade tests passed or failed with the laptop's power mode —
+/// off on battery, on when plugged in — and read as a broken glide rather
+/// than a borrowed switch. Thread-local rather than the shared cache,
+/// because libtest runs tests in parallel and a process-wide override set
+/// by one test would race every other test's read; a test that forces it
+/// does so for its own thread only, and the value resets with the thread.
+#[cfg(test)]
+thread_local! {
+ static FORCED: std::cell::Cell<bool> = const { std::cell::Cell::new(true) };
+}
+
+/// Set what [`enabled`] answers on this thread, for a test that exercises
+/// the snap-instead-of-ease path. Tests never set `CCE_ANIMATIONS`, since
+/// an environment variable is process-wide.
+#[cfg(test)]
+pub fn force_for_test(value: bool) {
+ FORCED.with(|f| f.set(value));
+}
+
/// Whether to animate. Every easing in the toolkit asks this before it
/// steps, and snaps to its target when the answer is no.
pub fn enabled() -> bool {
+ #[cfg(test)]
+ {
+ return FORCED.with(|f| f.get());
+ }
+ #[cfg(not(test))]
+ enabled_on_this_machine()
+}
+
+/// [`enabled`] as the shipped binary answers it: the `CCE_ANIMATIONS`
+/// override for this process, else the state file, re-read at most every
+/// [`RECHECK`].
+#[cfg(not(test))]
+fn enabled_on_this_machine() -> bool {
static ENV: std::sync::OnceLock<Option<bool>> = std::sync::OnceLock::new();
if let Some(forced) = *ENV.get_or_init(|| std::env::var("CCE_ANIMATIONS").ok().and_then(|v| parse(&v))) {
return forced;
@@ -74,4 +109,18 @@ mod tests {
assert_eq!(parse(""), None);
assert_eq!(parse("disabled"), None);
}
+
+ /// The suite's switch is on whatever the machine's file says, a test
+ /// can force it off for its own thread only, and another thread still
+ /// sees it on.
+ #[test]
+ fn the_suite_animates_whatever_the_machine_says() {
+ assert!(enabled(), "on by default, not read off {STATE_PATH}");
+ force_for_test(false);
+ assert!(!enabled(), "a test can force the snap path");
+ let elsewhere = std::thread::spawn(enabled).join().unwrap();
+ assert!(elsewhere, "the force is this thread's alone");
+ force_for_test(true);
+ assert!(enabled());
+ }
}