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

Merged
merged 14 commits into from
Jan 22, 2025
Merged
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 TabletPenPositionAbsoluteInput { 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 TabletPenInput(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 TabletPenInput(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 ITabletPenInput:
PenEvents++;
break;

default:
MouseEvents++;
break;
}

return base.OnMouseMove(e);
}

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

default:
MouseEvents++;
break;
}

return base.OnMouseDown(e);
}

protected override void OnMouseUp(MouseUpEvent e)
{
switch (e.CurrentState.Mouse.LastSource)
{
case ITabletPenInput:
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 TabletPenPositionAbsoluteInput
{
Position = position,
DeviceType = device_type
});
}

private void handlePenTouch(bool pressed)
{
enqueueInput(new TabletPenInput(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 TabletPenPositionAbsoluteInput { 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 TabletPenPositionRelativeInput { Delta = new Vector2(delta.X, delta.Y), DeviceType = lastTabletDeviceType = TabletPenDeviceType.Indirect });

void IPressureHandler.SetPressure(float percentage) => enqueueInput(new TabletPenInput(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 ITabletPenInput;

switch (e)
{
case MouseDownEvent penDown when penInput != null:
Debug.Assert(penDown.Button == MouseButton.Left);
new TabletPenInput(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 TabletPenInput(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 TabletPenPositionAbsoluteInput { 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/ITabletPenInput.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 ITabletPenInput : IInput
frenzibyte marked this conversation as resolved.
Show resolved Hide resolved
{
public TabletPenDeviceType DeviceType { get; }
}
}
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
}
}
17 changes: 17 additions & 0 deletions osu.Framework/Input/StateChanges/TabletPenInput.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 TabletPenInput : MouseButtonInput, ITabletPenInput
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "Tablet" prefix has been ingrained with graphic tablets in the framework codebase, I don't think it's a good fit for these classes.

I would just drop it, I think the name "Pen" is clear and general enough to indicate any kind of stylus input, direct or non-direct.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I originally had it as just PenInput, but I changed it to TabletPen to keep it consistent with existing TabletPenButtonInput. I can change it all to just Pen{…}Input, but it'll be a breaking one.

Some windows tablets have a screen, the pen input is just as direct as with an iPad, and they are still called graphics tablets.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer PenInput I think.

{
public TabletPenInput(bool isPressed)
: base(MouseButton.Left, isPressed)
{
}

public required TabletPenDeviceType DeviceType { get; init; }
}
}
10 changes: 10 additions & 0 deletions osu.Framework/Input/StateChanges/TabletPenPositionAbsoluteInput.cs
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 TabletPenPositionAbsoluteInput : MousePositionAbsoluteInput, ITabletPenInput
{
public required TabletPenDeviceType DeviceType { get; init; }
}
}
10 changes: 10 additions & 0 deletions osu.Framework/Input/StateChanges/TabletPenPositionRelativeInput.cs
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 TabletPenPositionRelativeInput : MousePositionRelativeInput, ITabletPenInput
{
public required TabletPenDeviceType DeviceType { get; init; }
}
}
Loading
Loading