diff --git a/src/DiffEngineViewer.Windows.Tests/WheelNotchesTests.cs b/src/DiffEngineViewer.Windows.Tests/WheelNotchesTests.cs new file mode 100644 index 00000000..16532290 --- /dev/null +++ b/src/DiffEngineViewer.Windows.Tests/WheelNotchesTests.cs @@ -0,0 +1,83 @@ +/// +/// Wheel messages into notches. A wheel click sends a whole one; a precision touchpad sends a +/// fraction of one per message, many times a second, for as long as the fingers move. +/// +public class WheelNotchesTests +{ + [Test] + public async Task A_wheel_click_is_a_notch() + { + var notches = new WheelNotches(120); + + await Assert.That(notches.Add(120)).IsEqualTo(1); + } + + /// + /// The reported bug: each message divided on its own is zero notches, so two finger scrolling + /// moved nothing at all. + /// + [Test] + public async Task Small_movements_add_up_to_one() + { + var notches = new WheelNotches(120); + + var before = new[] { notches.Add(20), notches.Add(20), notches.Add(20), notches.Add(20), notches.Add(20) }; + var last = notches.Add(20); + + await Assert.That(before).IsEquivalentTo([0, 0, 0, 0, 0]); + await Assert.That(last).IsEqualTo(1); + } + + [Test] + public async Task Small_movements_add_up_the_other_way_too() + { + var notches = new WheelNotches(120); + + var before = new[] { notches.Add(-40), notches.Add(-40) }; + var last = notches.Add(-40); + + await Assert.That(before).IsEquivalentTo([0, 0]); + await Assert.That(last).IsEqualTo(-1); + } + + /// + /// What is left over is kept, rather than each notch starting from nothing. + /// + [Test] + public async Task Keeps_the_remainder_across_notches() + { + var notches = new WheelNotches(120); + + notches.Add(100); + var first = notches.Add(100); + var second = notches.Add(100); + + await Assert.That(first).IsEqualTo(1); + // 300 in, one notch out, 60 held. The third message crosses 240 + await Assert.That(second).IsEqualTo(1); + } + + /// + /// A movement back the other way undoes what is held, rather than each direction keeping a + /// debt that the next movement has to pay off before anything happens. + /// + [Test] + public async Task A_movement_back_undoes_what_is_held() + { + var notches = new WheelNotches(120); + + notches.Add(60); + notches.Add(-60); + var up = new[] { notches.Add(60), notches.Add(60) }; + + await Assert.That(up).IsEquivalentTo([0, 1]); + } + + [Test] + public async Task A_fast_flick_is_several_notches() + { + var notches = new WheelNotches(120); + + await Assert.That(notches.Add(360)).IsEqualTo(3); + } +} diff --git a/src/DiffEngineViewer.Windows/ViewerCanvas.cs b/src/DiffEngineViewer.Windows/ViewerCanvas.cs index 70fb4fe7..fc8b926a 100644 --- a/src/DiffEngineViewer.Windows/ViewerCanvas.cs +++ b/src/DiffEngineViewer.Windows/ViewerCanvas.cs @@ -501,13 +501,15 @@ protected override void OnMouseLeave(EventArgs e) tips.Forget(this); } + readonly WheelNotches notches = new(SystemInformation.MouseWheelScrollDelta); + protected override void OnMouseWheel(MouseEventArgs e) { base.OnMouseWheel(e); - var notches = e.Delta / SystemInformation.MouseWheelScrollDelta; - if (notches != 0) + var scrolled = notches.Add(e.Delta); + if (scrolled != 0) { - Scrolled?.Invoke(notches); + Scrolled?.Invoke(scrolled); } } diff --git a/src/DiffEngineViewer.Windows/WheelNotches.cs b/src/DiffEngineViewer.Windows/WheelNotches.cs new file mode 100644 index 00000000..03aa3bf1 --- /dev/null +++ b/src/DiffEngineViewer.Windows/WheelNotches.cs @@ -0,0 +1,30 @@ +/// +/// Turns wheel messages into whole notches, keeping what is left over. +/// +/// A wheel click sends 120, but a precision touchpad sends a fraction of that per message - ten to +/// sixty for an ordinary two finger movement. Dividing each message on its own and dropping the +/// remainder threw all of those away, so the canvas did not move at all under a touchpad while the +/// docked scrollbar, which accumulates internally, worked. +/// +/// +/// Its own type for the same reason is: what matters is an accumulation +/// across messages, and that can be tested where a window cannot. +/// +/// +sealed class WheelNotches(int perNotch) +{ + int remainder; + + /// + /// The whole notches this message completes, which is usually none of one. + /// + public int Add(int delta) + { + remainder += delta; + // Truncates toward zero, so a run of small movements one way accumulates and a movement + // back the other way undoes it, rather than each direction keeping a debt of its own. + var notches = remainder / perNotch; + remainder -= notches * perNotch; + return notches; + } +}