Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions src/DiffEngineViewer.Windows.Tests/WheelNotchesTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/// <summary>
/// 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.
/// </summary>
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);
}

/// <summary>
/// The reported bug: each message divided on its own is zero notches, so two finger scrolling
/// moved nothing at all.
/// </summary>
[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);
}

/// <summary>
/// What is left over is kept, rather than each notch starting from nothing.
/// </summary>
[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);
}

/// <summary>
/// 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.
/// </summary>
[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);
}
}
8 changes: 5 additions & 3 deletions src/DiffEngineViewer.Windows/ViewerCanvas.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
30 changes: 30 additions & 0 deletions src/DiffEngineViewer.Windows/WheelNotches.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/// <summary>
/// Turns wheel messages into whole notches, keeping what is left over.
/// <para>
/// 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.
/// </para>
/// <para>
/// Its own type for the same reason <see cref="QueueTips" /> is: what matters is an accumulation
/// across messages, and that can be tested where a window cannot.
/// </para>
/// </summary>
sealed class WheelNotches(int perNotch)
{
int remainder;

/// <summary>
/// The whole notches this message completes, which is usually none of one.
/// </summary>
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;
}
}
Loading