Skip to content
Open
4 changes: 2 additions & 2 deletions osu.Framework.Tests/Visual/Input/TestSceneTouchInput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,8 @@ public void TestMouseInputAppliedFromLatestTouch()

AddStep("retrieve receptors", () =>
{
firstReceptor = receptors[(int)TouchSource.Touch1];
lastReceptor = receptors[(int)TouchSource.Touch10];
firstReceptor = receptors.First();
lastReceptor = receptors.Last();
});

AddStep("activate first", () =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public TestSceneTouchVisualiser()

public partial class TouchVisualiser : CompositeDrawable
{
private readonly Drawable[] drawableTouches = new Drawable[10];
private readonly Drawable[] drawableTouches = new Drawable[TouchState.MAX_TOUCH_COUNT];

public TouchVisualiser()
{
Expand Down
22 changes: 10 additions & 12 deletions osu.Framework/Input/Handlers/Pen/PenHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,22 +45,20 @@ public override bool Initialize(GameHost host)
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)
private void handlePenMove(TabletPenDeviceType deviceType, Vector2 position, bool pressed)
{
enqueueInput(new MousePositionAbsoluteInputFromPen
{
Position = position,
DeviceType = device_type
});
if (pressed && deviceType == TabletPenDeviceType.Direct)
enqueueInput(new TouchInput(new Input.Touch(TouchSource.PenTouch, position), true));
else
enqueueInput(new MousePositionAbsoluteInputFromPen { DeviceType = deviceType, Position = position });
}

private void handlePenTouch(bool pressed)
private void handlePenTouch(TabletPenDeviceType deviceType, bool pressed, Vector2 position)
{
enqueueInput(new MouseButtonInputFromPen(pressed) { DeviceType = device_type });
if (deviceType == TabletPenDeviceType.Direct)
enqueueInput(new TouchInput(new Input.Touch(TouchSource.PenTouch, position), pressed));
else
enqueueInput(new MouseButtonInputFromPen(pressed) { DeviceType = deviceType });
}

private void handlePenButton(TabletPenButton button, bool pressed)
Expand Down
3 changes: 2 additions & 1 deletion osu.Framework/Input/StateChanges/ISourcedFromPen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ namespace osu.Framework.Input.StateChanges
/// <summary>
/// Denotes a simulated mouse input that was made by a tablet/pen device.
/// </summary>
public interface ISourcedFromPen : IInput
// todo: this is not ready to be used externally for distinguishing input, therefore it's internal for now.
internal interface ISourcedFromPen : IInput
{
/// <summary>
/// The type of the tablet or pen device that made this input.
Expand Down
7 changes: 6 additions & 1 deletion osu.Framework/Input/States/TouchState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,15 @@ namespace osu.Framework.Input.States
public class TouchState
{
/// <summary>
/// The maximum amount of touches this can handle.
/// The maximum number of touches this can handle.
/// </summary>
public static readonly int MAX_TOUCH_COUNT = Enum.GetValues<TouchSource>().Length;

/// <summary>
/// The maximum number of touches this can handle excluding the synthetic source <see cref="TouchSource.PenTouch"/>.
/// </summary>
internal static readonly int MAX_NATIVE_TOUCH_COUNT = 10;

/// <summary>
/// The list of currently active touch sources.
/// </summary>
Expand Down
5 changes: 5 additions & 0 deletions osu.Framework/Input/TouchSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,10 @@ public enum TouchSource
/// The tenth and last available touch source.
/// </summary>
Touch10,

/// <summary>
/// A touch source that represents a pen/stylus.
/// </summary>
PenTouch,
}
}
4 changes: 2 additions & 2 deletions osu.Framework/Platform/SDL2/SDL2Window_Input.cs
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ private void handleDropEvent(SDL_DropEvent evtDrop)
}
}

private readonly long?[] activeTouches = new long?[TouchState.MAX_TOUCH_COUNT];
private readonly long?[] activeTouches = new long?[TouchState.MAX_NATIVE_TOUCH_COUNT];

private TouchSource? getTouchSource(long fingerId)
{
Expand All @@ -230,7 +230,7 @@ private void handleDropEvent(SDL_DropEvent evtDrop)
return (TouchSource)i;
}

// we only handle up to TouchState.MAX_TOUCH_COUNT. Ignore any further touches for now.
// we only handle up to TouchState.MAX_NATIVE_TOUCH_COUNT. Ignore any further touches for now.
return null;
}

Expand Down
4 changes: 4 additions & 0 deletions osu.Framework/Platform/SDL3/SDL3DesktopWindow.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// 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 SDL;
using static SDL.SDL3;

namespace osu.Framework.Platform.SDL3
Expand All @@ -12,6 +14,8 @@ public SDL3DesktopWindow(GraphicsSurfaceType surfaceType, string appName)
{
}

protected override TabletPenDeviceType GetPenDeviceType(SDL_PenID id) => TabletPenDeviceType.Unknown;

protected override unsafe void UpdateWindowStateAndSize(WindowState state, Display display, DisplayMode displayMode)
{
// this reset is required even on changing from one fullscreen resolution to another.
Expand Down
6 changes: 6 additions & 0 deletions osu.Framework/Platform/SDL3/SDL3MobileWindow.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// 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 SDL;
using static SDL.SDL3;

namespace osu.Framework.Platform.SDL3
Expand All @@ -12,6 +14,10 @@ public SDL3MobileWindow(GraphicsSurfaceType surfaceType, string appName)
{
}

// Pen input is not necessarily direct on mobile platforms (specifically Android, where external tablets are supported),
// but until users experience issues with this, consider it "direct" for now.
protected override TabletPenDeviceType GetPenDeviceType(SDL_PenID id) => TabletPenDeviceType.Direct;

protected override unsafe void UpdateWindowStateAndSize(WindowState state, Display display, DisplayMode displayMode)
{
// This sets the status bar to hidden.
Expand Down
5 changes: 5 additions & 0 deletions osu.Framework/Platform/SDL3/SDL3Window.cs
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,11 @@ protected virtual void HandleEvent(SDL_Event e)
handleDropEvent(e.drop);
break;

case SDL_EventType.SDL_EVENT_PEN_PROXIMITY_IN:
case SDL_EventType.SDL_EVENT_PEN_PROXIMITY_OUT:
handlePenProximityEvent(e.pproximity);
break;

case SDL_EventType.SDL_EVENT_PEN_DOWN:
case SDL_EventType.SDL_EVENT_PEN_UP:
handlePenTouchEvent(e.ptouch);
Expand Down
47 changes: 40 additions & 7 deletions osu.Framework/Platform/SDL3/SDL3Window_Input.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Input;
using osu.Framework.Input.StateChanges;
using osu.Framework.Input.States;
using osu.Framework.Logging;
using osuTK;
Expand Down Expand Up @@ -247,7 +248,7 @@
}
}

private readonly SDL_FingerID?[] activeTouches = new SDL_FingerID?[TouchState.MAX_TOUCH_COUNT];
private readonly SDL_FingerID?[] activeTouches = new SDL_FingerID?[TouchState.MAX_NATIVE_TOUCH_COUNT];

private TouchSource? getTouchSource(SDL_FingerID fingerId)
{
Expand All @@ -270,7 +271,7 @@
return (TouchSource)i;
}

// we only handle up to TouchState.MAX_TOUCH_COUNT. Ignore any further touches for now.
// we only handle up to TouchState.MAX_NATIVE_TOUCH_COUNT. Ignore any further touches for now.
return null;
}

Expand Down Expand Up @@ -530,14 +531,40 @@

private void handleKeymapChangedEvent() => KeymapChanged?.Invoke();

protected abstract TabletPenDeviceType GetPenDeviceType(SDL_PenID id);

private readonly Dictionary<SDL_PenID, TabletPenDeviceType> penDeviceTypes = new Dictionary<SDL_PenID, TabletPenDeviceType>();

private void handlePenProximityEvent(SDL_PenProximityEvent evtPenProximity)
{
var id = evtPenProximity.which;

if (evtPenProximity.type == SDL_EventType.SDL_EVENT_PEN_PROXIMITY_IN)
{
Debug.Assert(!penDeviceTypes.ContainsKey(id));
penDeviceTypes[id] = GetPenDeviceType(id);
}
else
{
Debug.Assert(penDeviceTypes.ContainsKey(id));
penDeviceTypes.Remove(id);
}
}

private void handlePenMotionEvent(SDL_PenMotionEvent evtPenMotion)
{
PenMove?.Invoke(new Vector2(evtPenMotion.x, evtPenMotion.y) * Scale);
Debug.Assert(penDeviceTypes.ContainsKey(evtPenMotion.which));

if (penDeviceTypes.TryGetValue(evtPenMotion.which, out var deviceType))
PenMove?.Invoke(deviceType, new Vector2(evtPenMotion.x, evtPenMotion.y) * Scale, evtPenMotion.pen_state.HasFlagFast(SDL_PenInputFlags.SDL_PEN_INPUT_DOWN));
}

private void handlePenTouchEvent(SDL_PenTouchEvent evtPenTouch)
{
PenTouch?.Invoke(evtPenTouch.down);
Debug.Assert(penDeviceTypes.ContainsKey(evtPenTouch.which));

if (penDeviceTypes.TryGetValue(evtPenTouch.which, out var deviceType))
PenTouch?.Invoke(deviceType, evtPenTouch.down, new Vector2(evtPenTouch.x, evtPenTouch.y) * Scale);
}

/// <summary>
Expand Down Expand Up @@ -743,14 +770,20 @@
public event Action<Touch>? TouchUp;

/// <summary>
/// Invoked when a pen moves.
/// Invoked when a pen enters (<c>true</c>) or leaves (<c>false</c>) tablet hover detection.
/// </summary>
public event Action<bool>? PenProximity;

Check failure on line 775 in osu.Framework/Platform/SDL3/SDL3Window_Input.cs

View workflow job for this annotation

GitHub Actions / Code Quality

The event 'SDL3Window.PenProximity' is never used

Check failure on line 775 in osu.Framework/Platform/SDL3/SDL3Window_Input.cs

View workflow job for this annotation

GitHub Actions / Code Quality

The event 'SDL3Window.PenProximity' is never used

Check warning on line 775 in osu.Framework/Platform/SDL3/SDL3Window_Input.cs

View workflow job for this annotation

GitHub Actions / Build only (iOS)

The event 'SDL3Window.PenProximity' is never used

Check warning on line 775 in osu.Framework/Platform/SDL3/SDL3Window_Input.cs

View workflow job for this annotation

GitHub Actions / Build only (iOS)

The event 'SDL3Window.PenProximity' is never used

Check failure on line 775 in osu.Framework/Platform/SDL3/SDL3Window_Input.cs

View workflow job for this annotation

GitHub Actions / Test (Windows, windows-latest, Debug, SingleThread)

The event 'SDL3Window.PenProximity' is never used

Check failure on line 775 in osu.Framework/Platform/SDL3/SDL3Window_Input.cs

View workflow job for this annotation

GitHub Actions / Test (Windows, windows-latest, Debug, SingleThread)

The event 'SDL3Window.PenProximity' is never used

Check failure on line 775 in osu.Framework/Platform/SDL3/SDL3Window_Input.cs

View workflow job for this annotation

GitHub Actions / Test (Windows, windows-latest, Debug, MultiThreaded)

The event 'SDL3Window.PenProximity' is never used

Check failure on line 775 in osu.Framework/Platform/SDL3/SDL3Window_Input.cs

View workflow job for this annotation

GitHub Actions / Test (Windows, windows-latest, Debug, MultiThreaded)

The event 'SDL3Window.PenProximity' is never used

Check warning on line 775 in osu.Framework/Platform/SDL3/SDL3Window_Input.cs

View workflow job for this annotation

GitHub Actions / Build only (Android)

The event 'SDL3Window.PenProximity' is never used

Check warning on line 775 in osu.Framework/Platform/SDL3/SDL3Window_Input.cs

View workflow job for this annotation

GitHub Actions / Build only (Android)

The event 'SDL3Window.PenProximity' is never used

Check failure on line 775 in osu.Framework/Platform/SDL3/SDL3Window_Input.cs

View workflow job for this annotation

GitHub Actions / Test (macOS, macos-latest, Debug, SingleThread)

The event 'SDL3Window.PenProximity' is never used

Check failure on line 775 in osu.Framework/Platform/SDL3/SDL3Window_Input.cs

View workflow job for this annotation

GitHub Actions / Test (macOS, macos-latest, Debug, SingleThread)

The event 'SDL3Window.PenProximity' is never used

Check failure on line 775 in osu.Framework/Platform/SDL3/SDL3Window_Input.cs

View workflow job for this annotation

GitHub Actions / Test (macOS, macos-latest, Debug, MultiThreaded)

The event 'SDL3Window.PenProximity' is never used

Check failure on line 775 in osu.Framework/Platform/SDL3/SDL3Window_Input.cs

View workflow job for this annotation

GitHub Actions / Test (macOS, macos-latest, Debug, MultiThreaded)

The event 'SDL3Window.PenProximity' is never used

Check failure on line 775 in osu.Framework/Platform/SDL3/SDL3Window_Input.cs

View workflow job for this annotation

GitHub Actions / Test (Linux, ubuntu-latest, Debug, SingleThread)

The event 'SDL3Window.PenProximity' is never used

Check failure on line 775 in osu.Framework/Platform/SDL3/SDL3Window_Input.cs

View workflow job for this annotation

GitHub Actions / Test (Linux, ubuntu-latest, Debug, SingleThread)

The event 'SDL3Window.PenProximity' is never used

Check failure on line 775 in osu.Framework/Platform/SDL3/SDL3Window_Input.cs

View workflow job for this annotation

GitHub Actions / Test (Linux, ubuntu-latest, Debug, MultiThreaded)

The event 'SDL3Window.PenProximity' is never used

Check failure on line 775 in osu.Framework/Platform/SDL3/SDL3Window_Input.cs

View workflow job for this annotation

GitHub Actions / Test (Linux, ubuntu-latest, Debug, MultiThreaded)

The event 'SDL3Window.PenProximity' is never used

Check failure on line 775 in osu.Framework/Platform/SDL3/SDL3Window_Input.cs

View workflow job for this annotation

GitHub Actions / Test (Linux, ubuntu-latest, Release, SingleThread)

The event 'SDL3Window.PenProximity' is never used

Check failure on line 775 in osu.Framework/Platform/SDL3/SDL3Window_Input.cs

View workflow job for this annotation

GitHub Actions / Test (Linux, ubuntu-latest, Release, SingleThread)

The event 'SDL3Window.PenProximity' is never used

Check failure on line 775 in osu.Framework/Platform/SDL3/SDL3Window_Input.cs

View workflow job for this annotation

GitHub Actions / Test (Linux, ubuntu-latest, Release, MultiThreaded)

The event 'SDL3Window.PenProximity' is never used

Check failure on line 775 in osu.Framework/Platform/SDL3/SDL3Window_Input.cs

View workflow job for this annotation

GitHub Actions / Test (Linux, ubuntu-latest, Release, MultiThreaded)

The event 'SDL3Window.PenProximity' is never used

/// <summary>
/// Invoked when a pen moves. Passes pen position and whether the pen is touching the tablet surface.
/// </summary>
public event Action<Vector2>? PenMove;
public event Action<TabletPenDeviceType, Vector2, bool>? PenMove;

/// <summary>
/// Invoked when a pen touches (<c>true</c>) or lifts (<c>false</c>) from the tablet surface.
/// Also passes the current position of the pen.
/// </summary>
public event Action<bool>? PenTouch;
public event Action<TabletPenDeviceType, bool, Vector2>? PenTouch;

/// <summary>
/// Invoked when a <see cref="TabletPenButton">pen button</see> is pressed (<c>true</c>) or released (<c>false</c>).
Expand Down
13 changes: 13 additions & 0 deletions osu.Framework/Platform/Windows/Native/Input.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public static extern bool RegisterRawInputDevices(
public const int SM_YVIRTUALSCREEN = 77;
public const int SM_CXVIRTUALSCREEN = 78;
public const int SM_CYVIRTUALSCREEN = 79;
public const int SM_DIGITIZER = 94;

public const long MI_WP_SIGNATURE = 0xFF515700;
public const long MI_WP_SIGNATURE_MASK = 0xFFFFFF00;
Expand Down Expand Up @@ -374,4 +375,16 @@ public enum FeedbackType
TouchRightTap = 10,
GesturePressAndTap = 11,
}

[Flags]
public enum DigitizerType
{
None,
NID_INTEGRATED_TOUCH = 0x01,
NID_EXTERNAL_TOUCH = 0x02,
NID_INTEGRATED_PEN = 0x04,
NID_EXTERNAL_PEN = 0x08,
NID_MULTI_INPUT = 0x40,
NID_READY = 0x80,
}
}
20 changes: 20 additions & 0 deletions osu.Framework/Platform/Windows/SDL3WindowsWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
using System.Drawing;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Input;
using osu.Framework.Input.Handlers.Mouse;
using osu.Framework.Input.StateChanges;
using osu.Framework.Platform.SDL3;
using osu.Framework.Platform.Windows.Native;
using osuTK;
Expand Down Expand Up @@ -59,6 +61,24 @@ public override void Create()
Native.Input.SetWindowFeedbackSetting(WindowHandle, feedbackType, false);
}

protected override TabletPenDeviceType GetPenDeviceType(SDL_PenID id)
{
// query the global pen types, as we can't scope it to a specific SDL_PenID
var type = (DigitizerType)Native.Input.GetSystemMetrics(Native.Input.SM_DIGITIZER);

// some Wacom tablets report as both integrated and external, but are in fact external/indirect
if (type.HasFlagFast(DigitizerType.NID_INTEGRATED_PEN | DigitizerType.NID_EXTERNAL_PEN))
return TabletPenDeviceType.Indirect;

if (type.HasFlagFast(DigitizerType.NID_EXTERNAL_PEN))
return TabletPenDeviceType.Indirect;

if (type.HasFlagFast(DigitizerType.NID_INTEGRATED_PEN))
return TabletPenDeviceType.Direct;

return TabletPenDeviceType.Unknown;
}

protected override bool HandleEventFromFilter(SDL_Event e)
{
switch (e.Type)
Expand Down
Loading