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

commit77ea4968e44e7a586a49c259f1f7186066f6b7d4
parentf7cec964c3
authorLucas Galante <[email protected]>
date2026-07-31 10:19
fix: trackpad scrolling on sliders converts pixels at 15/notch, not 120

Pixel (trackpad) scroll deltas were divided by 120 — the Windows wheel
convention — before the 0.02-per-notch value step, so sweeping a slider's
full range took ~6000px of finger travel and small swipes moved nothing
visible. The DE's own convention is 15 axis units per notch (ccectl
pointer-scroll), which is what trackpad deltas actually resemble: 8x
more sensitive, one 0.01 readout tick per ~26px of swipe.

New MouseScrollDelta::notches_y() owns the conversion; Slider,
RangeSlider, and the ParametersBg float3 rows all use it.

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/widget/container/parameters_bg.rs |  5 +----
 src/widget/input/slider.rs            | 14 ++++----------
 src/widget/mod.rs                     | 14 ++++++++++++++
 3 files changed, 19 insertions(+), 14 deletions(-)

diff --git a/src/widget/container/parameters_bg.rs b/src/widget/container/parameters_bg.rs
index 2b018d7..1beea9d 100644
--- a/src/widget/container/parameters_bg.rs
+++ b/src/widget/container/parameters_bg.rs
@@ -2241,10 +2241,7 @@ impl Input for ParametersBg {
                                 for j in 0..3 {
                                     let r_inner = rects_inner[j];
                                     if py >= r_inner.1 && py <= r_inner.1 + r_inner.3 {
-                                        let scroll_amount = match delta {
-                                            MouseScrollDelta::LineDelta(_x, y) => *y,
-                                            MouseScrollDelta::PixelDelta(pos) => (pos.y as f32) / 120.0,
-                                        };
+                                        let scroll_amount = delta.notches_y();
                                         let step = 0.02;
                                         let new_val = (f.values[j] - scroll_amount * step).clamp(0.0, 1.0);
                                         if (new_val - f.values[j]).abs() > 0.0001 {
diff --git a/src/widget/input/slider.rs b/src/widget/input/slider.rs
index a1c8bfb..64e2346 100644
--- a/src/widget/input/slider.rs
+++ b/src/widget/input/slider.rs
@@ -11,7 +11,7 @@ use crate::scene::layout::{Rect, Size};
 use crate::scene::paint::PaintCtx;
 use crate::widget::{
     Adapted, ElementState, Event, EventCtx, Input, Key, Layout, MouseButton,
-    MouseScrollDelta, NamedKey, Paint, TextEditorState,
+    NamedKey, Paint, TextEditorState,
 };
 
 /// The track/readout/thumb geometry shared by the paint and input paths, derived from the
@@ -653,10 +653,7 @@ impl Input for Slider {
                         if band || ui.scroll_gesture_new {
                             ui.scroll_initiate_widget_id = Some(ectx.id);
                         }
-                        let scroll_amount = match delta {
-                            MouseScrollDelta::LineDelta(_x, y) => *y,
-                            MouseScrollDelta::PixelDelta(pos) => (pos.y as f32) / 120.0,
-                        };
+                        let scroll_amount = delta.notches_y();
                         let new_val = (self.value - scroll_amount * 0.02).clamp(0.0, 1.0);
                         self.set_value_marking(new_val);
                         return true;
@@ -910,10 +907,7 @@ impl Input for RangeSlider {
                     let center_high = x + self.value_high * range + thumb_size / 2.0;
                     let dist_low = (px - center_low).abs();
                     let dist_high = (px - center_high).abs();
-                    let scroll_amount = match delta {
-                        MouseScrollDelta::LineDelta(_x, y) => *y,
-                        MouseScrollDelta::PixelDelta(pos) => (pos.y as f32) / 120.0,
-                    };
+                    let scroll_amount = delta.notches_y();
                     let step = 0.02;
                     let adjust_low = if dist_low < dist_high {
                         true
@@ -1012,7 +1006,7 @@ impl Input for RangeSlider {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::widget::{WidgetHost, UiContext};
+    use crate::widget::{MouseScrollDelta, WidgetHost, UiContext};
 
     /// The legacy rangeslider interaction test, driven through the WidgetHost drag forwards
     /// (hosts call these directly): thumb selection by proximity, constrained updates.
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index 68c7153..ba01b73 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -26,6 +26,20 @@ pub enum MouseScrollDelta {
     PixelDelta(Position),
 }
 
+impl MouseScrollDelta {
+    /// Vertical scroll in wheel-notch equivalents. Pixel (trackpad) deltas
+    /// convert at the DE's 15-axis-units-per-notch convention (see `ccectl
+    /// pointer-scroll`: "15 = one notch") — NOT the 120-unit wheel standard,
+    /// which makes value widgets feel dead under trackpad swipes (a full
+    /// slider sweep would take ~6000px of finger travel).
+    pub fn notches_y(&self) -> f32 {
+        match self {
+            MouseScrollDelta::LineDelta(_x, y) => *y,
+            MouseScrollDelta::PixelDelta(pos) => (pos.y as f32) / 15.0,
+        }
+    }
+}
+
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
 pub enum Key {
     Named(NamedKey),