GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
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() };