using System; using System.Collections.Generic; using System.Threading; using AcDream.UI.Abstractions.Input; using Silk.NET.Input; namespace AcDream.UI.Abstractions.Tests.Input; /// /// Tests for double-click detection added to /// in Phase B.4b. The dispatcher tracks the most-recent mouse-down button + /// timestamp; a same-button press within DoubleClickThresholdMs (500 ms) /// additionally fires for the matching /// binding on top of the normal . /// public class InputDispatcherDoubleClickTests { /// /// Build a dispatcher wired with LMB Press → SelectLeft, /// LMB DoubleClick → SelectDblLeft, and RMB Press → SelectRight. /// private static (InputDispatcher dispatcher, FakeMouseSource mouse, List<(InputAction, ActivationType)> fired) Build() { var kb = new FakeKeyboardSource(); var mouse = new FakeMouseSource(); var bindings = new KeyBindings(); var lmbChord = new KeyChord(InputDispatcher.MouseButtonToKey(MouseButton.Left), ModifierMask.None, Device: 1); var rmbChord = new KeyChord(InputDispatcher.MouseButtonToKey(MouseButton.Right), ModifierMask.None, Device: 1); bindings.Add(new Binding(lmbChord, InputAction.SelectLeft)); bindings.Add(new Binding(lmbChord, InputAction.SelectDblLeft, ActivationType.DoubleClick)); bindings.Add(new Binding(rmbChord, InputAction.SelectRight)); var dispatcher = new InputDispatcher(kb, mouse, bindings); var fired = new List<(InputAction, ActivationType)>(); dispatcher.Fired += (a, t) => fired.Add((a, t)); return (dispatcher, mouse, fired); } /// /// Two LMB clicks in rapid succession (~10 ms) → Press fires twice AND /// DoubleClick fires once (on the second click). /// [Fact] public void SecondClick_WithinThreshold_FiresDoubleClick() { var (_, mouse, fired) = Build(); mouse.EmitMouseDown(MouseButton.Left, ModifierMask.None); Thread.Sleep(10); mouse.EmitMouseDown(MouseButton.Left, ModifierMask.None); // Two SelectLeft Press events (one per click). Assert.Equal(2, fired.FindAll(e => e == (InputAction.SelectLeft, ActivationType.Press)).Count); // One SelectDblLeft DoubleClick event on the second click. Assert.Single(fired, e => e == (InputAction.SelectDblLeft, ActivationType.DoubleClick)); } /// /// Two LMB clicks 600 ms apart → Press fires twice but NO DoubleClick /// (interval exceeds the 500 ms threshold). /// [Fact] public void SecondClick_BeyondThreshold_DoesNotFireDoubleClick() { var (_, mouse, fired) = Build(); mouse.EmitMouseDown(MouseButton.Left, ModifierMask.None); Thread.Sleep(600); mouse.EmitMouseDown(MouseButton.Left, ModifierMask.None); Assert.Equal(2, fired.FindAll(e => e == (InputAction.SelectLeft, ActivationType.Press)).Count); Assert.Empty(fired.FindAll(e => e.Item2 == ActivationType.DoubleClick)); } /// /// LMB then RMB in rapid succession → no DoubleClick (different buttons). /// [Fact] public void DifferentButtons_DoNotFireDoubleClick() { var (_, mouse, fired) = Build(); mouse.EmitMouseDown(MouseButton.Left, ModifierMask.None); Thread.Sleep(10); mouse.EmitMouseDown(MouseButton.Right, ModifierMask.None); Assert.Empty(fired.FindAll(e => e.Item2 == ActivationType.DoubleClick)); } /// /// Three rapid LMB clicks → exactly one DoubleClick (between clicks 1 and 2). /// The third click resets the pair-state, so it acts as the "first click" of /// a new potential double-click rather than firing a second DoubleClick. /// [Fact] public void ThirdClick_AfterDoubleClick_RequiresFreshPair() { var (_, mouse, fired) = Build(); mouse.EmitMouseDown(MouseButton.Left, ModifierMask.None); // click 1 Thread.Sleep(10); mouse.EmitMouseDown(MouseButton.Left, ModifierMask.None); // click 2 → DoubleClick fires, state reset Thread.Sleep(10); mouse.EmitMouseDown(MouseButton.Left, ModifierMask.None); // click 3 → no DoubleClick (fresh pair started) // Three Press events total. Assert.Equal(3, fired.FindAll(e => e == (InputAction.SelectLeft, ActivationType.Press)).Count); // Exactly one DoubleClick (between clicks 1 and 2). Assert.Single(fired.FindAll(e => e == (InputAction.SelectDblLeft, ActivationType.DoubleClick))); } }