Skip to content

Commit

Permalink
feat: InteractionTracker pointer wheel support
Browse files Browse the repository at this point in the history
  • Loading branch information
Youssef1313 committed Mar 6, 2024
1 parent c4ab2de commit 5769d1e
Show file tree
Hide file tree
Showing 13 changed files with 249 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Numerics;

namespace Microsoft.UI.Composition.Interactions;

internal interface IInteractionTrackerInertiaHandler
{
Vector3 InitialVelocity { get; }
Vector3 FinalPosition { get; }
Vector3 FinalModifiedPosition { get; }
float FinalScale { get; }

void Start();
void Stop();
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,16 @@ internal void ReceiveInertiaStarting(Point linearVelocity)
_state.ReceiveInertiaStarting(-linearVelocity);
}

internal void ReceivePointerWheel(int mouseWheelTicks, bool isHorizontal)
{
// On WinUI, this depends on mouse setting "how many lines to scroll each time"
// The default Windows setting is 3 lines, and each line is 16px.
// Note: the value for each line may vary depending on scaling.
// For now, we just use 16*3=48.
var delta = mouseWheelTicks * 48;
_state.ReceivePointerWheel(-delta, isHorizontal);
}

public int TryUpdatePosition(Vector3 value)
=> TryUpdatePosition(value, InteractionTrackerClampingOption.Auto);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace Microsoft.UI.Composition.Interactions;

internal sealed partial class InteractionTrackerInertiaHandler
internal sealed partial class InteractionTrackerActiveInputInertiaHandler
{
private enum Axis
{
Expand All @@ -19,7 +19,7 @@ private sealed class AxisHelper
private float? _dampingStateTimeInSeconds;
private float? _dampingStatePosition;

internal InteractionTrackerInertiaHandler Handler { get; }
internal InteractionTrackerActiveInputInertiaHandler Handler { get; }
internal float DecayRate { get; }
internal float InitialVelocity { get; }
internal float InitialValue { get; }
Expand All @@ -30,7 +30,7 @@ private sealed class AxisHelper

internal bool HasCompleted { get; private set; }

public AxisHelper(InteractionTrackerInertiaHandler handler, Vector3 velocities, Axis axis)
public AxisHelper(InteractionTrackerActiveInputInertiaHandler handler, Vector3 velocities, Axis axis)
{
Axis = axis;
Handler = handler;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

namespace Microsoft.UI.Composition.Interactions;

internal sealed partial class InteractionTrackerInertiaHandler
internal sealed partial class InteractionTrackerActiveInputInertiaHandler : IInteractionTrackerInertiaHandler
{
private readonly InteractionTracker _interactionTracker;
private readonly AxisHelper _xHelper;
Expand All @@ -28,7 +28,7 @@ internal sealed partial class InteractionTrackerInertiaHandler
public Vector3 FinalModifiedPosition => new Vector3(_xHelper.FinalModifiedValue, _yHelper.FinalModifiedValue, _zHelper.FinalModifiedValue);
public float FinalScale => _interactionTracker.Scale; // TODO: Scale not yet implemented

public InteractionTrackerInertiaHandler(InteractionTracker interactionTracker, Vector3 translationVelocities, int requestId)
public InteractionTrackerActiveInputInertiaHandler(InteractionTracker interactionTracker, Vector3 translationVelocities, int requestId)
{
_interactionTracker = interactionTracker;
_xHelper = new AxisHelper(this, translationVelocities, Axis.X);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,17 @@ internal override void ReceiveInertiaStarting(Point linearVelocity)
{
}

internal override void ReceivePointerWheel(int delta, bool isHorizontal)
{
}

internal override void TryUpdatePositionWithAdditionalVelocity(Vector3 velocityInPixelsPerSecond, int requestId)
{
// TODO: Stop current animation. Currently, the TryUpdate[Position|Scale]WithAnimation methods are not implemented.

// State changes to inertia with inertia modifiers evaluated using requested velocity as initial velocity.
// TODO: inertia modifiers not yet implemented.
_interactionTracker.ChangeState(new InteractionTrackerInertiaState(_interactionTracker, velocityInPixelsPerSecond, requestId));
_interactionTracker.ChangeState(new InteractionTrackerInertiaState(_interactionTracker, velocityInPixelsPerSecond, requestId, isFromPointerWheel: false));
}

internal override void TryUpdatePosition(Vector3 value, InteractionTrackerClampingOption option, int requestId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,19 @@ internal override void ReceiveInertiaStarting(Point linearVelocity)
{
}

internal override void ReceivePointerWheel(int delta, bool isHorizontal)
{
// Constant velocity for 250ms
var velocityValue = delta / 0.25f;
Vector3 velocity = isHorizontal ? new Vector3(velocityValue, 0, 0) : new Vector3(0, velocityValue, 0);
_interactionTracker.ChangeState(new InteractionTrackerInertiaState(_interactionTracker, velocity, requestId: 0, isFromPointerWheel: true));
}

internal override void TryUpdatePositionWithAdditionalVelocity(Vector3 velocityInPixelsPerSecond, int requestId)
{
// State changes to inertia and inertia modifiers are evaluated with requested velocity as initial velocity
// TODO: inertia modifiers not yet implemented.
_interactionTracker.ChangeState(new InteractionTrackerInertiaState(_interactionTracker, velocityInPixelsPerSecond, requestId));
_interactionTracker.ChangeState(new InteractionTrackerInertiaState(_interactionTracker, velocityInPixelsPerSecond, requestId, isFromPointerWheel: false));
}

internal override void TryUpdatePosition(Vector3 value, InteractionTrackerClampingOption option, int requestId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ namespace Microsoft.UI.Composition.Interactions;

internal sealed class InteractionTrackerInertiaState : InteractionTrackerState
{
private readonly InteractionTrackerInertiaHandler _handler;
private readonly IInteractionTrackerInertiaHandler _handler;
private readonly int _requestId;

public InteractionTrackerInertiaState(InteractionTracker interactionTracker, Vector3 translationVelocities, int requestId) : base(interactionTracker)
public InteractionTrackerInertiaState(InteractionTracker interactionTracker, Vector3 translationVelocities, int requestId, bool isFromPointerWheel) : base(interactionTracker)
{
_requestId = requestId;
_handler = new InteractionTrackerInertiaHandler(interactionTracker, translationVelocities, _requestId);
_handler = isFromPointerWheel
? new InteractionTrackerPointerWheelInertiaHandler(interactionTracker, translationVelocities)
: new InteractionTrackerActiveInputInertiaHandler(interactionTracker, translationVelocities, _requestId);
}

protected override void EnterState(IInteractionTrackerOwner? owner)
Expand Down Expand Up @@ -69,10 +71,19 @@ internal override void ReceiveInertiaStarting(Point linearVelocity)
{
}

internal override void ReceivePointerWheel(int delta, bool isHorizontal)
{
var newDelta = isHorizontal ? new Vector3(delta, 0, 0) : new Vector3(0, delta, 0);
var totalDelta = (_handler.FinalModifiedPosition - _interactionTracker.Position) + newDelta;
// Constant velocity for 250ms
var velocity = totalDelta / 0.25f;
_interactionTracker.ChangeState(new InteractionTrackerInertiaState(_interactionTracker, velocity, requestId: 0, isFromPointerWheel: true));
}

internal override void TryUpdatePositionWithAdditionalVelocity(Vector3 velocityInPixelsPerSecond, int requestId)
{
// Inertia is restarted (state re-enters inertia) and inertia modifiers are evaluated with requested velocity added to current velocity
_interactionTracker.ChangeState(new InteractionTrackerInertiaState(_interactionTracker, _handler.InitialVelocity + velocityInPixelsPerSecond, requestId));
_interactionTracker.ChangeState(new InteractionTrackerInertiaState(_interactionTracker, _handler.InitialVelocity + velocityInPixelsPerSecond, requestId, isFromPointerWheel: false));
}

internal override void TryUpdatePosition(Vector3 value, InteractionTrackerClampingOption option, int requestId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ internal override void CompleteUserManipulation(Vector3 linearVelocity)
this.Log().Error("Unexpected CompleteUserManipulation while in interacting state");
}

_interactionTracker.ChangeState(new InteractionTrackerInertiaState(_interactionTracker, linearVelocity, requestId: 0));
_interactionTracker.ChangeState(new InteractionTrackerInertiaState(_interactionTracker, linearVelocity, requestId: 0, isFromPointerWheel: false));
}

internal override void ReceiveManipulationDelta(Point translationDelta)
Expand All @@ -47,7 +47,11 @@ internal override void ReceiveManipulationDelta(Point translationDelta)

internal override void ReceiveInertiaStarting(Point linearVelocity)
{
_interactionTracker.ChangeState(new InteractionTrackerInertiaState(_interactionTracker, new Vector3((float)linearVelocity.X, (float)linearVelocity.Y, 0), requestId: 0));
_interactionTracker.ChangeState(new InteractionTrackerInertiaState(_interactionTracker, new Vector3((float)linearVelocity.X, (float)linearVelocity.Y, 0), requestId: 0, isFromPointerWheel: false));
}

internal override void ReceivePointerWheel(int delta, bool isHorizontal)
{
}

internal override void TryUpdatePositionWithAdditionalVelocity(Vector3 velocityInPixelsPerSecond, int requestId)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#nullable enable

using System;
using System.Diagnostics;
using System.Numerics;
using System.Threading;

namespace Microsoft.UI.Composition.Interactions;

internal class InteractionTrackerPointerWheelInertiaHandler : IInteractionTrackerInertiaHandler
{
// InteractionTracker works at 60 FPS, per documentation
// https://learn.microsoft.com/en-us/windows/uwp/composition/interaction-tracker-manipulations#why-use-interactiontracker
// > InteractionTracker was built to utilize the new Animation engine that operates on an independent thread at 60 FPS,resulting in smooth motion.
private const int IntervalInMilliseconds = 17; // Ceiling of 1000/60

private Timer? _timer;
private Stopwatch? _stopwatch;

private readonly InteractionTracker _interactionTracker;
private readonly Vector3 _minPosition;
private readonly Vector3 _maxPosition;
private readonly Vector3 _initialPosition;
private readonly Vector3 _calculatedFinalPosition;

public InteractionTrackerPointerWheelInertiaHandler(InteractionTracker interactionTracker, Vector3 translationVelocities)
{
_interactionTracker = interactionTracker;
_minPosition = interactionTracker.MinPosition;
_maxPosition = interactionTracker.MaxPosition;
_initialPosition = _interactionTracker.Position;

InitialVelocity = translationVelocities;

// This handler works with constant velocity for 0.25 second.
_calculatedFinalPosition = interactionTracker.Position + InitialVelocity * 0.25f;
}

public Vector3 InitialVelocity { get; }

public Vector3 FinalPosition => Vector3.Clamp(_calculatedFinalPosition, _minPosition, _maxPosition);

public Vector3 FinalModifiedPosition => FinalPosition;

public float FinalScale => _interactionTracker.Scale; // TODO: Scale not yet implemented

public void Start()
{
if (_timer is not null)
{
throw new InvalidOperationException("Cannot start inertia timer twice.");
}

_stopwatch = Stopwatch.StartNew();
_timer = new Timer(OnTick, null, 0, IntervalInMilliseconds);
}

public void Stop()
{
_timer?.Dispose();
_stopwatch?.Stop();
}

private void OnTick(object? state)
{
var currentElapsed = _stopwatch!.ElapsedMilliseconds;

if (currentElapsed >= 250)
{
_interactionTracker.SetPosition(FinalModifiedPosition, requestId: 0);
_interactionTracker.ChangeState(new InteractionTrackerIdleState(_interactionTracker, requestId: 0));
_timer!.Dispose();
_stopwatch!.Stop();
return;
}

var newPosition = _initialPosition + (currentElapsed / 1000.0f) * InitialVelocity;
var clampedNewPosition = Vector3.Clamp(newPosition, _minPosition, _maxPosition);

_interactionTracker.SetPosition(clampedNewPosition, requestId: 0);

if (clampedNewPosition.Equals(FinalModifiedPosition))
{
_interactionTracker.ChangeState(new InteractionTrackerIdleState(_interactionTracker, requestId: 0));
_timer!.Dispose();
_stopwatch!.Stop();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public InteractionTrackerState(InteractionTracker interactionTracker)
internal abstract void CompleteUserManipulation(Vector3 linearVelocity);
internal abstract void ReceiveManipulationDelta(Point translationDelta);
internal abstract void ReceiveInertiaStarting(Point linearVelocity);
internal abstract void ReceivePointerWheel(int delta, bool isHorizontal);
internal abstract void TryUpdatePositionWithAdditionalVelocity(Vector3 velocityInPixelsPerSecond, int requestId);
internal abstract void TryUpdatePosition(Vector3 value, InteractionTrackerClampingOption option, int requestId);
public virtual void Dispose() => _disposed = true;
Expand Down
2 changes: 2 additions & 0 deletions src/Uno.UI.Composition/Composition/VisualInteractionSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ private VisualInteractionSource(Visual source) : base(source.Compositor)
/// </summary>
public VisualInteractionSourceRedirectionMode ManipulationRedirectionMode { get; set; }

internal bool RedirectsPointerWheel => (ManipulationRedirectionMode & VisualInteractionSourceRedirectionMode.PointerWheelOnly) != 0;

public static VisualInteractionSource Create(Visual source)
{
// WinUI doesn't allow a second `VisualInteractionSource`s with the same source, unless the previous one is disposed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,4 +263,77 @@ public async Task When_UserInteraction()
Assert.IsTrue(captureLostRaised);
Assert.IsTrue(helper.IsDone);
}

#if HAS_UNO
[TestMethod]
[RequiresFullWindow]
#if !HAS_INPUT_INJECTOR
[Ignore("InputInjector is only supported on skia")]
#endif
public async Task When_MouseWheel()
{
var border = new Border()
{
Width = 200,
Height = 200,
Background = new SolidColorBrush(Microsoft.UI.Colors.Red),
};

var position = await UITestHelper.Load(border);

var visual = ElementCompositionPreview.GetElementVisual(border);
var tracker = SetupTracker(visual.Compositor);

Assert.AreEqual(Vector3.Zero, tracker.Position);

var vis = VisualInteractionSource.Create(visual);
vis.ManipulationRedirectionMode = VisualInteractionSourceRedirectionMode.PointerWheelOnly;
vis.PositionXSourceMode = InteractionSourceMode.EnabledWithInertia;
vis.PositionYSourceMode = InteractionSourceMode.EnabledWithInertia;
tracker.InteractionSources.Add(vis);

await TestServices.WindowHelper.WaitForIdle();

var injector = InputInjector.TryCreate() ?? throw new InvalidOperationException("Failed to init the InputInjector");
var finger = injector.GetMouse();
finger.MoveTo(new(position.Left + 100, position.Top + 100), steps: 1);
finger.WheelDown();

string logs = await WaitTrackerLogs(tracker);
var helper = new TrackerAssertHelper(logs);

Assert.AreEqual(
TrackerLogsConstructingHelper.GetInertiaStateEntered(
trackerPosition: new(0.0f, 0.0f, 0.0f),
requestId: 0,
naturalRestingPosition: new(0.0f, 48.0f, 0.0f),
modifiedRestingPosition: new(0.0f, 48.0f, 0.0f),
positionVelocityInPixelsPerSecond: new(0.0f, 192.0f, 0.0f)),
helper.Current);

helper.Advance();

var linesSkipped = helper.SkipLines(current => current.StartsWith("ValuesChanged:", StringComparison.Ordinal));
Assert.IsTrue(linesSkipped >= 2);
helper.Back();

Assert.AreEqual(
TrackerLogsConstructingHelper.GetValuesChanged(
trackerPosition: new(0.0f, 48.0f, 0.0f),
requestId: 0,
argsPosition: new(0.0f, 48.0f, 0.0f)),
helper.Current);

helper.Advance();

Assert.AreEqual(
TrackerLogsConstructingHelper.GetIdleStateEntered(
trackerPosition: new(0.0f, 48.0f, 0.0f),
requestId: 0),
helper.Current);

helper.Advance();
Assert.IsTrue(helper.IsDone);
}
#endif
}
20 changes: 20 additions & 0 deletions src/Uno.UI/UI/Xaml/Internal/InputManager.Pointers.Managed.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
using PointerDeviceType = Windows.Devices.Input.PointerDeviceType;
using PointerEventArgs = Windows.UI.Core.PointerEventArgs;
using PointerUpdateKind = Windows.UI.Input.PointerUpdateKind;
using Microsoft.UI.Composition.Interactions;
using Microsoft.UI.Composition;

namespace Uno.UI.Xaml.Core;

Expand Down Expand Up @@ -143,6 +145,24 @@ private void OnPointerWheelChanged(Windows.UI.Core.PointerEventArgs args)

UpdateLastInputType(args);

#if __SKIA__ // Currently, only Skia supports interaction tracker.
Visual? currentVisual = originalSource.Visual;
while (currentVisual is not null)
{
if (currentVisual.VisualInteractionSource is { RedirectsPointerWheel: true } vis)
{
foreach (var tracker in vis.Trackers)
{
tracker.ReceivePointerWheel(args.CurrentPoint.Properties.MouseWheelDelta / global::Microsoft.UI.Xaml.Controls.ScrollContentPresenter.ScrollViewerDefaultMouseWheelDelta, args.CurrentPoint.Properties.IsHorizontalMouseWheel);
}

return;
}

currentVisual = currentVisual.Parent;
}
#endif

var routedArgs = new PointerRoutedEventArgs(args, originalSource);

// First raise the event, either on the OriginalSource or on the capture owners if any
Expand Down

0 comments on commit 5769d1e

Please sign in to comment.