Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a way to distinguish mouse from tablet pen inputs #6488

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
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
5 changes: 5 additions & 0 deletions osu.Framework.Tests/Visual/Input/TestSceneInputManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,11 @@ private void load()
setCursorConfineRect(false);

AddStep("Reset handlers", () => host.ResetInputHandlers());

AddLabel("Input handlers");

foreach (var h in host.AvailableInputHandlers)
AddToggleStep($"{h.Description} enabled", v => h.Enabled.Value = v);
}

private void setCursorSensitivityConfig(double sensitivity)
Expand Down
113 changes: 113 additions & 0 deletions osu.Framework.Tests/Visual/Input/TestScenePassThroughInputManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Input.StateChanges;
using osu.Framework.Input.States;
using osu.Framework.Testing;
using osu.Framework.Testing.Input;
Expand Down Expand Up @@ -272,6 +273,64 @@ public void TestMouseTouchProductionOnPassThrough()
AddAssert("pass-through handled mouse", () => testInputManager.CurrentState.Mouse.Buttons.Single() == MouseButton.Left);
}

[Test]
public void TestPenInputPassThrough()
{
MouseBox outer = null!;
MouseBox inner = null!;

addTestInputManagerStep();
AddStep("setup hierarchy", () =>
{
Add(outer = new MouseBox
{
Alpha = 0.5f,
Depth = 1,
RelativeSizeAxes = Axes.Both,
});

testInputManager.Add(inner = new MouseBox
{
Alpha = 0.5f,
RelativeSizeAxes = Axes.Both,
});
});

AddStep("move pen to box",
() => InputManager.Input(new MousePositionAbsoluteInputFromPen { Position = testInputManager.ScreenSpaceDrawQuad.Centre, DeviceType = TabletPenDeviceType.Unknown }));

AddAssert("ensure parent manager produced mouse", () => InputManager.CurrentState.Mouse.Position == testInputManager.ScreenSpaceDrawQuad.Centre);
AddAssert("ensure pass-through produced mouse", () => testInputManager.CurrentState.Mouse.Position == testInputManager.ScreenSpaceDrawQuad.Centre);

AddAssert("outer box received 1 pen event", () => outer.PenEvents, () => Is.EqualTo(1));
AddAssert("outer box received no mouse events", () => outer.MouseEvents, () => Is.EqualTo(0));

AddAssert("inner box received 1 pen event", () => inner.PenEvents, () => Is.EqualTo(1));
AddAssert("inner box received no mouse events", () => inner.MouseEvents, () => Is.EqualTo(0));

AddStep("click pen", () => InputManager.Input(new MouseButtonInputFromPen(true) { DeviceType = TabletPenDeviceType.Unknown }));

AddAssert("ensure parent manager produced mouse", () => InputManager.CurrentState.Mouse.Buttons.Single() == MouseButton.Left);
AddAssert("ensure pass-through produced mouse", () => testInputManager.CurrentState.Mouse.Buttons.Single() == MouseButton.Left);

AddAssert("outer box received 2 pen events", () => outer.PenEvents, () => Is.EqualTo(2));
AddAssert("outer box received no mouse events", () => outer.MouseEvents, () => Is.EqualTo(0));

AddAssert("inner box received 2 pen events", () => inner.PenEvents, () => Is.EqualTo(2));
AddAssert("inner box received no mouse events", () => inner.MouseEvents, () => Is.EqualTo(0));

AddStep("release pen", () => InputManager.Input(new MouseButtonInputFromPen(false) { DeviceType = TabletPenDeviceType.Unknown }));

AddAssert("ensure parent manager produced mouse", () => InputManager.CurrentState.Mouse.Buttons.HasAnyButtonPressed, () => Is.False);
AddAssert("ensure pass-through produced mouse", () => testInputManager.CurrentState.Mouse.Buttons.HasAnyButtonPressed, () => Is.False);

AddAssert("outer box received 3 pen events", () => outer.PenEvents, () => Is.EqualTo(3));
AddAssert("outer box received no mouse events", () => outer.MouseEvents, () => Is.EqualTo(0));

AddAssert("inner box received 3 pen events", () => inner.PenEvents, () => Is.EqualTo(3));
AddAssert("inner box received no mouse events", () => inner.MouseEvents, () => Is.EqualTo(0));
}

public partial class TestInputManager : ManualInputManager
{
public readonly TestSceneInputManager.ContainingInputManagerStatusText Status;
Expand All @@ -291,5 +350,59 @@ public partial class HandlingBox : Box

protected override bool Handle(UIEvent e) => OnHandle?.Invoke(e) ?? false;
}

public partial class MouseBox : Box
{
public int MouseEvents { get; private set; }
public int PenEvents { get; private set; }

protected override bool OnMouseMove(MouseMoveEvent e)
{
switch (e.CurrentState.Mouse.LastSource)
{
case ISourcedFromPen:
PenEvents++;
break;

default:
MouseEvents++;
break;
}

return base.OnMouseMove(e);
}

protected override bool OnMouseDown(MouseDownEvent e)
{
switch (e.CurrentState.Mouse.LastSource)
{
case ISourcedFromPen:
PenEvents++;
break;

default:
MouseEvents++;
break;
}

return base.OnMouseDown(e);
}

protected override void OnMouseUp(MouseUpEvent e)
{
switch (e.CurrentState.Mouse.LastSource)
{
case ISourcedFromPen:
PenEvents++;
break;

default:
MouseEvents++;
break;
}

base.OnMouseUp(e);
}
}
}
}
78 changes: 78 additions & 0 deletions osu.Framework/Input/Handlers/Pen/PenHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using osu.Framework.Input.StateChanges;
using osu.Framework.Platform;
using osu.Framework.Platform.SDL3;
using osu.Framework.Statistics;
using osuTK;

namespace osu.Framework.Input.Handlers.Pen
{
/// <summary>
/// SDL3 pen handler
/// </summary>
public class PenHandler : InputHandler
{
private static readonly GlobalStatistic<ulong> statistic_total_events = GlobalStatistics.Get<ulong>(StatisticGroupFor<PenHandler>(), "Total events");

public override bool IsActive => true;

public override bool Initialize(GameHost host)
{
if (!base.Initialize(host))
return false;

if (host.Window is not SDL3Window window)
return false;

Enabled.BindValueChanged(enabled =>
{
if (enabled.NewValue)
{
window.PenMove += handlePenMove;
window.PenTouch += handlePenTouch;
window.PenButton += handlePenButton;
}
else
{
window.PenMove -= handlePenMove;
window.PenTouch -= handlePenTouch;
window.PenButton -= handlePenButton;
}
}, true);

return true;
}

// iPadOS doesn't support external tablets, so we are sure it's direct Apple Pencil input.
// Other platforms support both direct and indirect tablet input, but SDL doesn't provide any information on the current device type.
private static readonly TabletPenDeviceType device_type = RuntimeInfo.OS == RuntimeInfo.Platform.iOS ? TabletPenDeviceType.Direct : TabletPenDeviceType.Unknown;

private void handlePenMove(Vector2 position)
{
enqueueInput(new MousePositionAbsoluteInputFromPen
{
Position = position,
DeviceType = device_type
});
}

private void handlePenTouch(bool pressed)
{
enqueueInput(new MouseButtonInputFromPen(pressed) { DeviceType = device_type });
}

private void handlePenButton(TabletPenButton button, bool pressed)
{
enqueueInput(new TabletPenButtonInput(button, pressed));
}

private void enqueueInput(IInput input)
{
PendingInputs.Enqueue(input);
FrameStatistics.Increment(StatisticsCounterType.TabletEvents);
statistic_total_events.Value++;
}
}
}
10 changes: 7 additions & 3 deletions osu.Framework/Input/Handlers/Tablet/OpenTabletDriverHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,15 @@ public override bool Initialize(GameHost host)
return true;
}

void IAbsolutePointer.SetPosition(System.Numerics.Vector2 pos) => enqueueInput(new MousePositionAbsoluteInput { Position = new Vector2(pos.X, pos.Y) });
private TabletPenDeviceType lastTabletDeviceType = TabletPenDeviceType.Unknown;

void IRelativePointer.SetPosition(System.Numerics.Vector2 delta) => enqueueInput(new MousePositionRelativeInput { Delta = new Vector2(delta.X, delta.Y) });
void IAbsolutePointer.SetPosition(System.Numerics.Vector2 pos)
=> enqueueInput(new MousePositionAbsoluteInputFromPen { Position = new Vector2(pos.X, pos.Y), DeviceType = lastTabletDeviceType = TabletPenDeviceType.Unknown });

void IPressureHandler.SetPressure(float percentage) => enqueueInput(new MouseButtonInput(osuTK.Input.MouseButton.Left, percentage > 0));
void IRelativePointer.SetPosition(System.Numerics.Vector2 delta)
=> enqueueInput(new MousePositionRelativeInputFromPen { Delta = new Vector2(delta.X, delta.Y), DeviceType = lastTabletDeviceType = TabletPenDeviceType.Indirect });

void IPressureHandler.SetPressure(float percentage) => enqueueInput(new MouseButtonInputFromPen(percentage > 0) { DeviceType = lastTabletDeviceType });

private void handleTabletsChanged(object? sender, IEnumerable<TabletReference> tablets)
{
Expand Down
18 changes: 18 additions & 0 deletions osu.Framework/Input/PassThroughInputManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
Expand Down Expand Up @@ -99,16 +100,33 @@ protected override bool Handle(UIEvent e)
if (e is MouseEvent && e.CurrentState.Mouse.LastSource is ISourcedFromTouch)
return false;

var penInput = e.CurrentState.Mouse.LastSource as ISourcedFromPen;

switch (e)
{
case MouseDownEvent penDown when penInput != null:
Debug.Assert(penDown.Button == MouseButton.Left);
new MouseButtonInputFromPen(true) { DeviceType = penInput.DeviceType }.Apply(CurrentState, this);
break;

case MouseDownEvent mouseDown:
new MouseButtonInput(mouseDown.Button, true).Apply(CurrentState, this);
break;

case MouseUpEvent penUp when penInput != null:
Debug.Assert(penUp.Button == MouseButton.Left);
new MouseButtonInputFromPen(false) { DeviceType = penInput.DeviceType }.Apply(CurrentState, this);
break;

case MouseUpEvent mouseUp:
new MouseButtonInput(mouseUp.Button, false).Apply(CurrentState, this);
break;

case MouseMoveEvent penMove when penInput != null:
if (penMove.ScreenSpaceMousePosition != CurrentState.Mouse.Position)
new MousePositionAbsoluteInputFromPen { Position = penMove.ScreenSpaceMousePosition, DeviceType = penInput.DeviceType }.Apply(CurrentState, this);
break;

case MouseMoveEvent mouseMove:
if (mouseMove.ScreenSpaceMousePosition != CurrentState.Mouse.Position)
new MousePositionAbsoluteInput { Position = mouseMove.ScreenSpaceMousePosition }.Apply(CurrentState, this);
Expand Down
13 changes: 13 additions & 0 deletions osu.Framework/Input/StateChanges/ISourcedFromPen.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

namespace osu.Framework.Input.StateChanges
{
/// <summary>
/// Denotes a simulated mouse input that was made by a tablet/pen device.
/// </summary>
public interface ISourcedFromPen : IInput
{
public TabletPenDeviceType DeviceType { get; }
}
}
17 changes: 17 additions & 0 deletions osu.Framework/Input/StateChanges/MouseButtonInputFromPen.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using osuTK.Input;

namespace osu.Framework.Input.StateChanges
{
public class MouseButtonInputFromPen : MouseButtonInput, ISourcedFromPen
{
public MouseButtonInputFromPen(bool isPressed)
: base(MouseButton.Left, isPressed)
{
}

public required TabletPenDeviceType DeviceType { get; init; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

namespace osu.Framework.Input.StateChanges
{
public class MousePositionAbsoluteInputFromPen : MousePositionAbsoluteInput, ISourcedFromPen
{
public required TabletPenDeviceType DeviceType { get; init; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

namespace osu.Framework.Input.StateChanges
{
public class MousePositionRelativeInputFromPen : MousePositionRelativeInput, ISourcedFromPen
{
public required TabletPenDeviceType DeviceType { get; init; }
}
}
29 changes: 29 additions & 0 deletions osu.Framework/Input/StateChanges/TabletPenDeviceType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

namespace osu.Framework.Input.StateChanges
{
public enum TabletPenDeviceType
{
/// <summary>
/// This tablet device might be <see cref="Direct"/> or <see cref="Indirect"/>, the input handler doesn't have enough information to decide.
/// </summary>
Unknown,

/// <summary>
/// A tablet device with a built-in display.
/// </summary>
/// <remarks>
/// The pen is physically pointing at the screen, so it's unnecessary to show a cursor.
/// </remarks>
Direct,

/// <summary>
/// An indirect tablet device, the pen is not pointing at a screen, but a separate surface.
/// </summary>
/// <remarks>
/// You may want to show a cursor so the user can see where the pen is pointing.
/// </remarks>
Indirect
}
}
Loading
Loading