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

commitb18d42b2e0b44ef36da0e5f22f7a4f018fbaf099
parent03d847b0f0
authorLucas Galante <lsgalante12@gmail.com>
date2026-09-29 20:37
feat: a flick coasts with animations off

The animations switch turned off a trackpad flick's coast along with a
wheel notch's glide, so on a power mode without animations a scroll
stopped dead at the lift in every pane. The switch stops the glide; the
coast follows input.kdl's kinetic_scroll alone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

 CLAUDE.md                   | 14 ++++++++++++++
 src/motion.rs               |  6 ++++--
 src/widget/scroll_motion.rs | 38 +++++++++++++++++++++++++++++---------
 3 files changed, 47 insertions(+), 11 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 494a204..321144f 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -959,6 +959,20 @@ assignment, a rebuild, a section collapsing (`apply_label_layout` /
 comes OFF a widget, and `slider::detached_strip` reads an empty label as
 none for the widgets that store whatever they are handed.
 
+### A flick coasts with animations off
+
+The animations switch (`motion::enabled`) stops a wheel notch's GLIDE and
+not a trackpad flick's COAST (`scroll_motion::with_animations`, since
+2026-09-29). Until then it turned both off, so on a power mode with
+animations off a two-finger scroll stopped dead at the lift in every
+pane that scrolls through `ScrollMotion` — lists, the params pane, the
+spreadsheet, a graph's pan. The glide is an animation the toolkit adds;
+the coast is the rest of a gesture the hand made, and its own setting is
+input.kdl's `kinetic_scroll`. Two value controls still follow the switch,
+deliberately left alone: a slider's and a ramp key's drift after a scroll
+over them, which changes a VALUE after the hand has stopped.
+`the_animations_switch_stops_the_glide_and_not_the_coast` is the test.
+
 ### The suite's animations switch is its own
 
 `motion::enabled()` reads `/run/cce/animations` in a shipped binary (see
diff --git a/src/motion.rs b/src/motion.rs
index 51a3f6f..7554c36 100644
--- a/src/motion.rs
+++ b/src/motion.rs
@@ -1,9 +1,11 @@
 //! The DE-wide animations switch.
 //!
 //! One flag, followed by every cce-ui widget that eases and by the
-//! compositor: when it is off, anything that would glide, fade, slide or
-//! coast lands on its target in the same frame instead. Disabled means
+//! compositor: when it is off, anything that would glide, fade or slide
+//! lands on its target in the same frame instead. Disabled means
 //! "snap", never "freeze" — a dropdown still opens, a scroll still moves.
+//! A trackpad flick's coast is NOT under it (`widget::scroll_motion`): that
+//! is the hand's gesture carried on, not an animation the toolkit adds.
 //!
 //! The switch is a file, [`STATE_PATH`], holding `on` or `off`. The System
 //! Interface's Power page sets it per power mode and `cce-power-apply`
diff --git a/src/widget/scroll_motion.rs b/src/widget/scroll_motion.rs
index f525744..3747b26 100644
--- a/src/widget/scroll_motion.rs
+++ b/src/widget/scroll_motion.rs
@@ -113,16 +113,24 @@ impl Default for ScrollSettings {
 static SETTINGS: std::sync::OnceLock<ScrollSettings> = std::sync::OnceLock::new();
 
 /// This app's effective smooth-scroll settings (`<app>` → `cce-ui` → defaults).
-/// With animations off ([`crate::motion`]) a wheel notch jumps and a flick
-/// stops at the lift — the legacy behavior — whatever input.kdl says; that
-/// is checked per call, so it follows the switch while the app runs.
+/// With animations off ([`crate::motion`]) a wheel notch jumps, whatever
+/// input.kdl says; that is checked per call, so it follows the switch while
+/// the app runs.
+///
+/// **A flick coasts either way.** Until 2026-09-29 the switch turned the
+/// coast off with the glide, so on a power mode that has animations off a
+/// trackpad scroll stopped dead at the lift in every pane. The glide of a
+/// notch is an animation the toolkit adds, and the switch is for those;
+/// the coast is the rest of a gesture the hand made, and `kinetic_scroll`
+/// in input.kdl is the setting for it.
 pub fn scroll_settings() -> ScrollSettings {
-    let configured = configured_scroll_settings();
-    if crate::motion::enabled() {
-        configured
-    } else {
-        ScrollSettings { smooth: false, kinetic: false, ..configured }
-    }
+    with_animations(configured_scroll_settings(), crate::motion::enabled())
+}
+
+/// `configured` as the animations switch leaves it: the glide follows the
+/// switch, the coast does not.
+fn with_animations(configured: ScrollSettings, animations: bool) -> ScrollSettings {
+    ScrollSettings { smooth: configured.smooth && animations, ..configured }
 }
 
 fn configured_scroll_settings() -> ScrollSettings {
@@ -595,6 +603,18 @@ mod tests {
         assert_eq!(a.pos(), 150.0);
     }
 
+    #[test]
+    fn the_animations_switch_stops_the_glide_and_not_the_coast() {
+        let off = with_animations(smooth(), false);
+        assert!(!off.smooth, "a notch jumps");
+        assert!(off.kinetic, "a flick still coasts");
+        assert_eq!((off.ease_rate, off.friction), (smooth().ease_rate, smooth().friction));
+        assert_eq!(with_animations(smooth(), true), smooth());
+        // What input.kdl turned off stays off.
+        let plain = ScrollSettings { smooth: false, kinetic: false, ..smooth() };
+        assert_eq!(with_animations(plain, true), plain);
+    }
+
     #[test]
     fn kinetic_off_stops_dead() {
         let s = ScrollSettings { kinetic: false, ..smooth() };