diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 03efa87b74..b9f6941232 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,10 +3,11 @@
"isRoot": true,
"tools": {
"csharpier": {
- "version": "0.29.2",
+ "version": "1.0.1",
"commands": [
- "dotnet-csharpier"
- ]
+ "csharpier"
+ ],
+ "rollForward": false
}
}
}
\ No newline at end of file
diff --git a/Silk.NET.sln b/Silk.NET.sln
index 17850fdc6f..364d408733 100644
--- a/Silk.NET.sln
+++ b/Silk.NET.sln
@@ -102,6 +102,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Windowing", "Windowing", "{
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Silk.NET.Windowing", "sources\Windowing\Windowing\Silk.NET.Windowing.csproj", "{EF07CBB5-D253-4CA9-A5DA-8B3DF2B0DF8E}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Input", "Input", "{33ED9765-8C36-4A9D-95E8-AF037FE104B3}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Silk.NET.Input", "sources\Input\Input\Silk.NET.Input.csproj", "{49A42CE3-94C5-4239-B0FC-F1FF8D7AAADA}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Input", "Input", "{4E0EF53A-76BC-4729-8E3B-4768E86E357E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Silk.NET.Input.UnitTests", "tests\Input\Input\Silk.NET.Input.UnitTests.csproj", "{00B9B6E6-776E-480C-B3ED-D6420C5B4E8E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -168,6 +176,14 @@ Global
{EF07CBB5-D253-4CA9-A5DA-8B3DF2B0DF8E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EF07CBB5-D253-4CA9-A5DA-8B3DF2B0DF8E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EF07CBB5-D253-4CA9-A5DA-8B3DF2B0DF8E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {49A42CE3-94C5-4239-B0FC-F1FF8D7AAADA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {49A42CE3-94C5-4239-B0FC-F1FF8D7AAADA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {49A42CE3-94C5-4239-B0FC-F1FF8D7AAADA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {49A42CE3-94C5-4239-B0FC-F1FF8D7AAADA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {00B9B6E6-776E-480C-B3ED-D6420C5B4E8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {00B9B6E6-776E-480C-B3ED-D6420C5B4E8E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {00B9B6E6-776E-480C-B3ED-D6420C5B4E8E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {00B9B6E6-776E-480C-B3ED-D6420C5B4E8E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -200,6 +216,10 @@ Global
{F16C0AB9-DE7E-4C09-9EE9-DAA8B8E935A6} = {EC4D7B06-D277-4411-BD7B-71A6D37683F0}
{FE4414F8-5370-445D-9F24-C3AD3223F299} = {DD29EA8F-B1A6-45AA-8D2E-B38DA56D9EF6}
{EF07CBB5-D253-4CA9-A5DA-8B3DF2B0DF8E} = {FE4414F8-5370-445D-9F24-C3AD3223F299}
+ {33ED9765-8C36-4A9D-95E8-AF037FE104B3} = {DD29EA8F-B1A6-45AA-8D2E-B38DA56D9EF6}
+ {49A42CE3-94C5-4239-B0FC-F1FF8D7AAADA} = {33ED9765-8C36-4A9D-95E8-AF037FE104B3}
+ {4E0EF53A-76BC-4729-8E3B-4768E86E357E} = {A5578D12-9E77-4647-8C22-0DBD17760BFF}
+ {00B9B6E6-776E-480C-B3ED-D6420C5B4E8E} = {4E0EF53A-76BC-4729-8E3B-4768E86E357E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {78D2CF6A-60A1-43E3-837B-00B73C9DA384}
diff --git a/docs/silk.net/diagnostics/ST0001.md b/docs/silk.net/diagnostics/ST0001.md
new file mode 100644
index 0000000000..f04906d857
--- /dev/null
+++ b/docs/silk.net/diagnostics/ST0001.md
@@ -0,0 +1,21 @@
+# ST0001 - ProcessClass failure
+
+## Overview
+
+This internal error was raised by SilkTouch when failing to generate an implementation for a binding at source
+generation time. It provided details regarding the exception that led to the entire native API class failing to have its
+implementation generated.
+
+| Attribute | Value |
+|--------------------|----------------------|
+| Diagnostic ID | ST0001 |
+| Title | ProcessClass failure |
+| Category | SilkTouch.Internal |
+| Default Severity | Error |
+| Enabled by Default | Yes |
+
+Example message: `ProcessClass failed. Exception: '...'`
+
+## Explanation & Solutions
+
+This functionality is no longer supported in 3.0, where this diagnostic is never raised.
diff --git a/docs/silk.net/diagnostics/ST0002.md b/docs/silk.net/diagnostics/ST0002.md
new file mode 100644
index 0000000000..89742a49a7
--- /dev/null
+++ b/docs/silk.net/diagnostics/ST0002.md
@@ -0,0 +1,21 @@
+# ST0002 - MethodClass failure
+
+## Overview
+
+This internal error was raised by SilkTouch when failing to generate an implementation for a binding at source
+generation time. It provided details regarding the exception that led to a specific native API method failing to have
+its implementation generated.
+
+| Attribute | Value |
+|--------------------|---------------------|
+| Diagnostic ID | ST0002 |
+| Title | MethodClass failure |
+| Category | SilkTouch.Internal |
+| Default Severity | Error |
+| Enabled by Default | Yes |
+
+Example message: `MethodClass failed. Exception: '...'`
+
+## Explanation & Solutions
+
+This functionality is no longer supported in 3.0, where this diagnostic is never raised.
diff --git a/docs/silk.net/diagnostics/ST0003.md b/docs/silk.net/diagnostics/ST0003.md
new file mode 100644
index 0000000000..d722f5f6d7
--- /dev/null
+++ b/docs/silk.net/diagnostics/ST0003.md
@@ -0,0 +1,20 @@
+# ST0003 - Silk.NET.Core is Missing
+
+## Overview
+
+This internal diagnostic was raised by SilkTouch when failing to generate an implementation for bindings at source
+generation time due to the binding project missing a reference to Silk.NET.Core.
+
+| Attribute | Value |
+|--------------------|---------------------|
+| Diagnostic ID | ST0003 |
+| Title | MethodClass failure |
+| Category | SilkTouch.Internal |
+| Default Severity | Info |
+| Enabled by Default | Yes |
+
+Example message: `Silk.NET.Core is missing from references. You should use SilkTouch with Silk.NET.Core`
+
+## Explanation & Solutions
+
+This functionality is no longer supported in 3.0, where this diagnostic is never raised.
diff --git a/docs/silk.net/diagnostics/ST0004.md b/docs/silk.net/diagnostics/ST0004.md
new file mode 100644
index 0000000000..4680d3faa3
--- /dev/null
+++ b/docs/silk.net/diagnostics/ST0004.md
@@ -0,0 +1,20 @@
+# ST0004 - Build Info
+
+## Overview
+
+This internal diagnostic was raised by SilkTouch when configured to do so. It provided diagnostic information relating
+to the performance and characteristics of SilkTouch's internals.
+
+| Attribute | Value |
+|--------------------|--------------------|
+| Diagnostic ID | ST0004 |
+| Title | Build Info |
+| Category | SilkTouch.Internal |
+| Default Severity | Warning |
+| Enabled by Default | Yes |
+
+Example message: `GCSlotCount: '127'. Time: '6437ms'`
+
+## Explanation & Solutions
+
+This functionality is no longer supported in 3.0, where this diagnostic is never raised.
diff --git a/docs/silk.net/diagnostics/ST0005.md b/docs/silk.net/diagnostics/ST0005.md
new file mode 100644
index 0000000000..8a7c730766
--- /dev/null
+++ b/docs/silk.net/diagnostics/ST0005.md
@@ -0,0 +1,15 @@
+# ST0005 - Intentionally Unstable API
+
+## Overview
+
+This diagnostic is raised when trying to use a Silk.NET API that has been marked with the `Experimental` attribute due
+to its API and/or ABI being unstable. When this diagnostic ID is used, it indicates that it is intentional that this is
+the case and that this API is extremely unlikely to ever graduate to a stable, versioned API.
+
+## Explanation & Solutions
+
+Typically, APIs meeting this description are internal APIs and are not intended for use outside of the assembly they're
+defined in. As a result, where this diagnostic is raised, you should cease use of this API or at least only continue if
+you can guarantee that you will never update Silk.NET ever again and that your downstream consumers, if applicable, will
+lock their version to the same version referenced by your project. However, please reconsider use of the API if this is
+the case.
diff --git a/eng/build/Silk.NET.NUKE.csproj b/eng/build/Silk.NET.NUKE.csproj
index 1f3ae0857e..2a1f1a1c28 100644
--- a/eng/build/Silk.NET.NUKE.csproj
+++ b/eng/build/Silk.NET.NUKE.csproj
@@ -18,4 +18,10 @@
+
+
+
+ Directory.Build\tests\Input\Input\Silk.NET.Input.UnitTests.csproj
+
+
diff --git a/sources/Core/Core/Pointers/PointerExtensions.cs b/sources/Core/Core/Pointers/PointerExtensions.cs
index cfacf2de27..41f57aa519 100644
--- a/sources/Core/Core/Pointers/PointerExtensions.cs
+++ b/sources/Core/Core/Pointers/PointerExtensions.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
@@ -232,6 +233,43 @@ public static string ReadToString(this Ptr @this)
}
}
+ ///
+ /// Populates the given span with the characters of this as a c-style string.
+ ///
+ ///
+ /// The span to populate characters into
+ /// True if the given span is of sufficient length and can be filled - false otherwise, in which case
+ /// no data has been modified in the given span
+ public static bool TryReadToSpan(this Ptr @this, ref Span span)
+ {
+ fixed (void* raw = @this)
+ {
+ var bytes = MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)raw);
+ var count = Encoding.UTF8.GetCharCount(bytes);
+ if (span.Length < count)
+ {
+ return false;
+ }
+
+ #if DEBUG
+ // This if-def is here to prevent this constant string from taking up space in extremely constrained
+ // release environments.
+ const string assertionLog = $"{nameof(Encoding)}.{nameof(Encoding.UTF8)}." +
+ $"{nameof(Encoding.UTF8.GetChars)}) returned an unexpected number of " +
+ $"characters";
+
+ var charCount = Encoding.UTF8.GetChars(bytes, span);
+ Debug.Assert(charCount == count, assertionLog);;
+ #else
+ Encoding.UTF8.GetChars(bytes, span);
+ #endif
+
+ span = span[..count];
+ return true;
+ }
+ }
+
+
///
/// Creates a string from this with the given length
///
diff --git a/sources/Core/Core/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/sources/Core/Core/PublicAPI/net8.0/PublicAPI.Unshipped.txt
index 14eb48880a..2daf4e53ee 100644
--- a/sources/Core/Core/PublicAPI/net8.0/PublicAPI.Unshipped.txt
+++ b/sources/Core/Core/PublicAPI/net8.0/PublicAPI.Unshipped.txt
@@ -414,6 +414,7 @@ static Silk.NET.Core.PointerExtensions.ReadToStringArray(this Silk.NET.Core.Ref3
static Silk.NET.Core.PointerExtensions.ReadToStringArray(this Silk.NET.Core.Ref3D this, int length, int[]! lengths) -> string?[]?[]?
static Silk.NET.Core.PointerExtensions.ReadToStringArray(this Silk.NET.Core.Ref3D this, int length, int[]! lengths) -> string?[]?[]?
static Silk.NET.Core.PointerExtensions.ReadToStringArray(this Silk.NET.Core.Ref3D this, int length, int[]! lengths) -> string?[]?[]?
+static Silk.NET.Core.PointerExtensions.TryReadToSpan(this Silk.NET.Core.Ptr this, ref System.Span span) -> bool
static Silk.NET.Core.Ptr.explicit operator nint(Silk.NET.Core.Ptr ptr) -> nint
static Silk.NET.Core.Ptr.explicit operator Silk.NET.Core.Ptr(nint ptr) -> Silk.NET.Core.Ptr
static Silk.NET.Core.Ptr.explicit operator Silk.NET.Core.Ptr(Silk.NET.Core.Ref ptr) -> Silk.NET.Core.Ptr
diff --git a/sources/Input/Input/Button.cs b/sources/Input/Input/Button.cs
new file mode 100644
index 0000000000..1ae8875c65
--- /dev/null
+++ b/sources/Input/Input/Button.cs
@@ -0,0 +1,24 @@
+namespace Silk.NET.Input;
+
+///
+/// Represents a button the user can push.
+///
+/// The name of the button.
+/// Whether the user is pushing the button.
+///
+/// The pressure with which the user is pushing the button, where 0.0 is the smallest measurable pressure and
+/// 1.0 is the largest measurable pressure.
+///
+///
+/// The button type (e.g. , , etc).
+///
+public readonly record struct Button(T Name, bool IsDown, float Pressure)
+ where T : unmanaged, Enum
+{
+ ///
+ /// Collapses this struct into just its value.
+ ///
+ /// The button state.
+ /// The value.
+ public static implicit operator bool(Button state) => state.IsDown;
+}
diff --git a/sources/Input/Input/ButtonChangedEvent.cs b/sources/Input/Input/ButtonChangedEvent.cs
new file mode 100644
index 0000000000..b030762606
--- /dev/null
+++ b/sources/Input/Input/ButtonChangedEvent.cs
@@ -0,0 +1,21 @@
+using System.Diagnostics;
+
+namespace Silk.NET.Input;
+
+///
+/// Contains information pertaining to a button state change (e.g. press, depress, etc).
+///
+/// The device on which the button being pressed or depressed resides.
+///
+/// The timestamp (as retrieved from ) at which the event occurred.
+///
+/// The new state of the button being pressed or depressed.
+/// The previous state of the button.
+/// The button type e.g. , , etc.
+public readonly record struct ButtonChangedEvent(
+ IButtonDevice Device,
+ long Timestamp,
+ Button Button,
+ Button Previous
+)
+ where T : unmanaged, Enum;
diff --git a/sources/Input/Input/ButtonReadOnlyList.cs b/sources/Input/Input/ButtonReadOnlyList.cs
new file mode 100644
index 0000000000..1cd98aa366
--- /dev/null
+++ b/sources/Input/Input/ButtonReadOnlyList.cs
@@ -0,0 +1,47 @@
+using System.Collections;
+
+namespace Silk.NET.Input;
+
+///
+/// An implementation of providing utility APIs for getting a
+/// given a button name , that is optimised for storing s with the
+/// given button name type using the most memory-efficient mechanism available.
+///
+///
+/// The button type (e.g. , , etc).
+///
+public readonly record struct ButtonReadOnlyList : IReadOnlyList>
+ where T : unmanaged, Enum
+{
+ private readonly Func _indexMap;
+ private readonly IReadOnlyList> _list;
+
+ ///
+ /// A constructor for an input list that takes in:
+ ///
+ /// A list of buttons that will be indexed
+ /// A pre-built mapping function, if required,
+ /// used for iterating through the button list in order, regardless of the backend's internal button order.
+ public ButtonReadOnlyList(IReadOnlyList> buttonList, Func? indexMap = null)
+ {
+ _list = buttonList;
+ _indexMap = indexMap ?? (i => i);
+ }
+
+ ///
+ /// Gets the state for the button with the given name.
+ ///
+ /// The button name.
+ public Button this[T name] => _list[EnumInfo.ValueIndexOf(name)];
+
+ ///
+ public IEnumerator> GetEnumerator() => _list.GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ ///
+ public int Count => _list.Count;
+
+ ///
+ public Button this[int index] => _list[_indexMap(index)];
+}
diff --git a/sources/Input/Input/ConnectionEvent.cs b/sources/Input/Input/ConnectionEvent.cs
new file mode 100644
index 0000000000..2da787cc70
--- /dev/null
+++ b/sources/Input/Input/ConnectionEvent.cs
@@ -0,0 +1,13 @@
+using System.Diagnostics;
+
+namespace Silk.NET.Input;
+
+///
+/// Contains information pertaining to a device connection or disconnection event.
+///
+/// The device that has disconnected or connected.
+///
+/// The timestamp (as retrieved from ) at which the event occurred.
+///
+/// Whether the device has connected (true ) or disconnected (false ).
+public readonly record struct ConnectionEvent(IInputDevice Device, long Timestamp, bool IsConnected);
\ No newline at end of file
diff --git a/sources/Input/Input/CursorModes.cs b/sources/Input/Input/CursorModes.cs
new file mode 100644
index 0000000000..a5ce021ea8
--- /dev/null
+++ b/sources/Input/Input/CursorModes.cs
@@ -0,0 +1,57 @@
+namespace Silk.NET.Input;
+
+///
+/// Enumerates the modes in which a mouse cursor can operate.
+///
+///
+/// implementations for implementations typically have two
+/// :
+///
+/// -
+///
Bounded
+///
+/// An that is bounded to the desktop environment i.e. the
+/// are not infinite and reflect the total screen space that is available to the
+/// running application in window coordinates. This is typically the sum of all monitor resolutions, with the positions
+/// being defined using an implementation-defined mechanism. The window bounds operate in this same coordinate space.
+/// It is highly unlikely that you will be unable to determine the individual points for multiple mice on this target,
+/// as desktop environments typically aggregate all movement from all mice into a single .
+/// This target is used for every cursor mode except .
+///
+///
+/// -
+///
Unbounded
+///
+/// An that is unbounded and operates in an arbitrary coordinate space. This target is used
+/// for raw mouse mode and points on this target represent the net mouse movement from a mouse. Implementations
+/// are more likely to be able to give multiple s for each mouse when this target is used. This
+/// target is used when the cursor mode is enabled. will
+/// represent an infinitely large unbounded target.
+///
+///
+///
+///
+[Flags]
+public enum CursorModes
+{
+ ///
+ /// The cursor is visible to the user and operating within the bounds of the desktop environment . The
+ /// coordinates received are in desktop coordinates, operating in the same coordinate space as the window
+ /// position/size.
+ ///
+ Normal = 1 << 0,
+
+ ///
+ /// The cursor is visible to the user but is constrained to the window's client area . The coordinates
+ /// received are in desktop coordinates, operating in the same coordinate space as the window position/size.
+ /// The bounded to the desktop environment is used.
+ ///
+ Confined = 1 << 1,
+
+ ///
+ /// The cursor is invisible to the user and is unconstrained/unbounded . The coordinates received are
+ /// arbitrary values that have no bounds representing the net mouse movement since entering into this cursor mode.
+ /// The unbounded is used. This is the equivalent of raw mouse mode .
+ ///
+ Unbounded = 1 << 2,
+}
\ No newline at end of file
diff --git a/sources/Input/Input/CursorStyles.cs b/sources/Input/Input/CursorStyles.cs
new file mode 100644
index 0000000000..65ecfc6f55
--- /dev/null
+++ b/sources/Input/Input/CursorStyles.cs
@@ -0,0 +1,61 @@
+namespace Silk.NET.Input;
+
+///
+/// Enumerates the cursor styles with which the desktop environment should render the cursor.
+///
+[Flags]
+public enum CursorStyles
+{
+ ///
+ /// The cursor should be rendered using its default image.
+ ///
+ Default,
+
+ ///
+ /// The cursor should be rendered using an arrow cursor image.
+ ///
+ Arrow = 1 << 0,
+
+ ///
+ /// The cursor should be rendered using an I-beam cursor image, which is used to show where the text cursor appears
+ /// when the mouse is clicked.
+ ///
+ IBeam = 1 << 1,
+
+ ///
+ /// The cursor should be rendered using a crosshair cursor image.
+ ///
+ Crosshair = 1 << 2,
+
+ ///
+ /// The cursor should be rendered using a hand cursor image, typically used when hovering over a web link.
+ ///
+ Hand = 1 << 3,
+
+ ///
+ /// The cursor should be rendered using a two-headed horizontal sizing cursor image.
+ ///
+ HResize = 1 << 4,
+
+ ///
+ /// The cursor should be rendered using a two-headed vertical sizing cursor image.
+ ///
+ VResize = 1 << 5,
+
+ ///
+ /// The cursor should not be rendered.
+ ///
+ ///
+ /// When is used, the cursor ceases to exist anyway. As such, while the
+ /// property may not reflect this (as it is retained across changes to
+ /// and just ignored when is used),
+ /// can be implied as being when
+ /// is used.
+ ///
+ Hidden = 1 << 6,
+
+ ///
+ /// The cursor should be rendered using a custom application-provided image.
+ ///
+ Custom = 1 << 7,
+}
\ No newline at end of file
diff --git a/sources/Input/Input/CustomCursor.cs b/sources/Input/Input/CustomCursor.cs
new file mode 100644
index 0000000000..b82b9d0337
--- /dev/null
+++ b/sources/Input/Input/CustomCursor.cs
@@ -0,0 +1,22 @@
+namespace Silk.NET.Input;
+
+///
+/// Represents a custom image for a mouse cursor.
+///
+public readonly ref struct CustomCursor
+{
+ ///
+ /// The number of pixels in the X axis.
+ ///
+ public int Width { get; init; }
+
+ ///
+ /// The number of pixels in the Y axis.
+ ///
+ public int Height { get; init; }
+
+ ///
+ /// The row-major 32-bit RGBA pixel data (i.e. 8 bits for each colour component).
+ ///
+ public ReadOnlySpan Data { get; init; } // Rgba32
+}
diff --git a/sources/Input/Input/DualReadOnlyList.cs b/sources/Input/Input/DualReadOnlyList.cs
new file mode 100644
index 0000000000..2181706f22
--- /dev/null
+++ b/sources/Input/Input/DualReadOnlyList.cs
@@ -0,0 +1,58 @@
+using System.Collections;
+
+namespace Silk.NET.Input;
+
+///
+/// Represents a list that has exactly two elements.
+///
+/// The element type.
+public readonly struct DualReadOnlyList : IReadOnlyList
+{
+ ///
+ /// Represents a list that has exactly two elements.
+ ///
+ /// The element type.
+
+ public DualReadOnlyList(Func left, Func right)
+ {
+ _left = left;
+ _right = right;
+ }
+
+ ///
+ /// The first/leftmost element.
+ ///
+ public T Left => _left();
+
+ ///
+ /// The second/rightmost element.
+ ///
+ public T Right => _right();
+
+
+ ///
+ public IEnumerator GetEnumerator()
+ {
+ yield return Left;
+ yield return Right;
+ }
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ ///
+ public int Count => 2;
+
+ ///
+ public T this[int index] =>
+ index switch
+ {
+ 0 => Left,
+ 1 => Right,
+ _ => throw new IndexOutOfRangeException(),
+ };
+
+
+
+ private readonly Func _left;
+ private readonly Func _right;
+}
diff --git a/sources/Input/Input/GamepadState.cs b/sources/Input/Input/GamepadState.cs
new file mode 100644
index 0000000000..62ea3e9605
--- /dev/null
+++ b/sources/Input/Input/GamepadState.cs
@@ -0,0 +1,59 @@
+using System.Numerics;
+
+namespace Silk.NET.Input;
+
+///
+/// Contains user input received from an .
+///
+public class GamepadState
+{
+ ///
+ /// The constructor for a new GamepadState object
+ ///
+ /// The list of buttons
+ /// The list of states of the controllers axes that the triggers and joysticks will
+ /// be read from via their specific indices in this array
+ /// The joystick X axes.
+ /// The Joystick Y axes.
+ ///
+ ///
+ /// For and , the must be either of length
+ /// 2 or 4.
+ ///
+ /// If two are provided, the first is assumed to be the left stick, and the second is assumed to be the right stick
+ ///
+ /// if 4 are provided, it is assumed that the first two are - and + sides of the first axis, and so on.
+ ///[leftX, rightX] OR [-leftX, +leftX, -rightX, +rightX]
+ ///
+ ///
+ ///
+ public GamepadState(IReadOnlyList> buttons, IReadOnlyList axisStates)
+ {
+ _axisStates = axisStates;
+ Buttons = new ButtonReadOnlyList(buttons);
+ Triggers = new DualReadOnlyList(
+ left: () => _axisStates[JoystickAxis.LeftTrigger.Index()],
+ right: () =>_axisStates[JoystickAxis.RightTrigger.Index()]);
+ Thumbsticks = new DualReadOnlyList(
+ left: () => new Vector2(_axisStates[JoystickAxis.LeftX.Index()], _axisStates[JoystickAxis.LeftY.Index()]),
+ right: () => new Vector2(_axisStates[JoystickAxis.RightX.Index()], _axisStates[JoystickAxis.RightY.Index()]));
+ }
+
+ ///
+ /// Gets the gamepad button state denoting the buttons being pressed or depressed.
+ ///
+ public ButtonReadOnlyList Buttons { get; }
+
+ ///
+ /// Gets the state of the twin sticks on the gamepad.
+ ///
+ public DualReadOnlyList Thumbsticks { get; internal set; }
+
+ ///
+ /// Gets the state of the triggers on the gamepad.
+ ///
+ public DualReadOnlyList Triggers { get; internal set; }
+
+ // ReSharper disable PrivateFieldCanBeConvertedToLocalVariable <- keeps closures consistent
+ private readonly IReadOnlyList _axisStates;
+}
diff --git a/sources/Input/Input/GamepadThumbstickMoveEvent.cs b/sources/Input/Input/GamepadThumbstickMoveEvent.cs
new file mode 100644
index 0000000000..b2ffeef3a3
--- /dev/null
+++ b/sources/Input/Input/GamepadThumbstickMoveEvent.cs
@@ -0,0 +1,17 @@
+using System.Diagnostics;
+using System.Numerics;
+
+namespace Silk.NET.Input;
+
+///
+/// Contains information pertaining to the movement of a thumbstick.
+///
+/// The gamepad on which the thumbstick resides.
+///
+/// The timestamp (as retrieved from ) at which the event occurred.
+///
+///
+/// The new position of the thumbstick, where each axis is between -1.0 and 1.0 .
+///
+/// The change in as a result of this event.
+public readonly record struct GamepadThumbstickMoveEvent(IGamepad Gamepad, long Timestamp, Vector2 Value, Vector2 Delta);
\ No newline at end of file
diff --git a/sources/Input/Input/GamepadTriggerMoveEvent.cs b/sources/Input/Input/GamepadTriggerMoveEvent.cs
new file mode 100644
index 0000000000..0cbca61581
--- /dev/null
+++ b/sources/Input/Input/GamepadTriggerMoveEvent.cs
@@ -0,0 +1,17 @@
+using System.Diagnostics;
+
+namespace Silk.NET.Input;
+
+///
+/// Contains information pertaining to the movement of a trigger.
+///
+/// The gamepad on which the trigger resides.
+///
+/// The timestamp (as retrieved from ) at which the event occurred.
+///
+/// The index of the trigger that has moved.
+///
+/// The new value of the trigger, between 0.0 (fully depressed) and 1.0 (fully pressed).
+///
+/// The change in as a result of this event.
+public readonly record struct GamepadTriggerMoveEvent(IGamepad Gamepad, long Timestamp, int Axis, float Value, float Delta);
\ No newline at end of file
diff --git a/sources/Input/Input/Gamepads.cs b/sources/Input/Input/Gamepads.cs
new file mode 100644
index 0000000000..ce56d20079
--- /dev/null
+++ b/sources/Input/Input/Gamepads.cs
@@ -0,0 +1,43 @@
+namespace Silk.NET.Input;
+
+///
+/// Represents a collection of s from which input events can be received.
+///
+public sealed class Gamepads : InputContextDeviceList, IGamepadInputHandler
+{
+ internal Gamepads(InputContext ctx)
+ : base(ctx) { }
+
+ ///
+ /// Raised when state pertaining to a pushable button on the gamepad changes (e.g. button up, button down).
+ ///
+ public event Action>? ButtonChanged;
+
+ ///
+ /// Raised when a thumbstick on the gamepad moves.
+ ///
+ public event Action? ThumbstickMove;
+
+ ///
+ /// Raised when a trigger on the gamepad moves.
+ ///
+ public event Action? TriggerMove;
+
+ internal void HandleButtonChanged(ButtonChangedEvent @event) =>
+ ButtonChanged?.Invoke(@event);
+
+ void IButtonInputHandler.HandleButtonChanged(
+ ButtonChangedEvent @event
+ ) => HandleButtonChanged(@event);
+
+ internal void HandleThumbstickMove(GamepadThumbstickMoveEvent @event) =>
+ ThumbstickMove?.Invoke(@event);
+
+ void IGamepadInputHandler.HandleThumbstickMove(GamepadThumbstickMoveEvent @event) =>
+ HandleThumbstickMove(@event);
+
+ internal void HandleTriggerMove(GamepadTriggerMoveEvent @event) => TriggerMove?.Invoke(@event);
+
+ void IGamepadInputHandler.HandleTriggerMove(GamepadTriggerMoveEvent @event) =>
+ HandleTriggerMove(@event);
+}
diff --git a/sources/Input/Input/IButtonDevice.cs b/sources/Input/Input/IButtonDevice.cs
new file mode 100644
index 0000000000..70b88af9d3
--- /dev/null
+++ b/sources/Input/Input/IButtonDevice.cs
@@ -0,0 +1,17 @@
+namespace Silk.NET.Input;
+
+///
+/// Represents an input device that has buttons.
+///
+/// The type of buttons the input device has.
+public interface IButtonDevice : IInputDevice
+ where T : unmanaged, Enum
+{
+ ///
+ /// Gets the current button state for this device.
+ ///
+ ///
+ /// Only updated when is called.
+ ///
+ ButtonReadOnlyList State { get; }
+}
diff --git a/sources/Input/Input/IButtonInputHandler.cs b/sources/Input/Input/IButtonInputHandler.cs
new file mode 100644
index 0000000000..0d02d1d675
--- /dev/null
+++ b/sources/Input/Input/IButtonInputHandler.cs
@@ -0,0 +1,15 @@
+namespace Silk.NET.Input;
+
+///
+/// An that also receives events.
+///
+/// The device's button type.
+public interface IButtonInputHandler : IInputHandler
+ where T : unmanaged, Enum
+{
+ ///
+ /// Called when a button's state changes (e.g. button down, button up).
+ ///
+ /// The event details.
+ void HandleButtonChanged(ButtonChangedEvent @event);
+}
diff --git a/sources/Input/Input/ICursorConfiguration.cs b/sources/Input/Input/ICursorConfiguration.cs
new file mode 100644
index 0000000000..0d0209d4e5
--- /dev/null
+++ b/sources/Input/Input/ICursorConfiguration.cs
@@ -0,0 +1,45 @@
+namespace Silk.NET.Input;
+
+///
+/// Configuration for the behaviour of a mouse cursor.
+///
+public interface ICursorConfiguration
+{
+ ///
+ /// Gets a bitmask denoting the supported values for .
+ ///
+ CursorModes SupportedModes { get; }
+
+ ///
+ /// Gets or sets the current cursor mode. Only one bit shall be set at a time.
+ ///
+ ///
+ /// Note that this property affects the in use, see the
+ /// documentation for more info.
+ ///
+ CursorModes Mode { get; set; }
+
+ ///
+ /// Gets a bitmask denoting the supported values for .
+ ///
+ CursorStyles SupportedStyles { get; }
+
+ ///
+ /// Gets or sets the current cursor style. Only one bit shall be set at a time.
+ /// shall use the provided.
+ ///
+ ///
+ /// When is used, the cursor ceases to exist anyway. As such, while the
+ /// property may not reflect this (as it is retained across changes to
+ /// and just ignored when is used),
+ /// can be implied as being when
+ /// is used.
+ ///
+ CursorStyles Style { get; set; }
+
+ ///
+ /// Gets or sets the current custom cursor image. This has no effect if is not
+ /// used, but the value is stored nonetheless for use when that is the case.
+ ///
+ CustomCursor Image { get; set; }
+}
\ No newline at end of file
diff --git a/sources/Input/Input/IGamepad.cs b/sources/Input/Input/IGamepad.cs
new file mode 100644
index 0000000000..1dc37823b6
--- /dev/null
+++ b/sources/Input/Input/IGamepad.cs
@@ -0,0 +1,20 @@
+namespace Silk.NET.Input;
+
+///
+/// Represents a gamepad that follows a typical layout.
+///
+public interface IGamepad : IButtonDevice
+{
+ ///
+ /// Gets the device state.
+ ///
+ ///
+ /// Only updated when is called.
+ ///
+ new GamepadState State { get; }
+ ButtonReadOnlyList IButtonDevice.State => State.Buttons;
+ ///
+ /// Gets a collection enumerating the vibration motors available to the application to enable haptics.
+ ///
+ IReadOnlyList VibrationMotors { get; }
+}
\ No newline at end of file
diff --git a/sources/Input/Input/IGamepadInputHandler.cs b/sources/Input/Input/IGamepadInputHandler.cs
new file mode 100644
index 0000000000..b1bf488d8d
--- /dev/null
+++ b/sources/Input/Input/IGamepadInputHandler.cs
@@ -0,0 +1,19 @@
+namespace Silk.NET.Input;
+
+///
+/// An that also receives input.
+///
+public interface IGamepadInputHandler : IButtonInputHandler
+{
+ ///
+ /// Called when one of the twin sticks moves.
+ ///
+ /// The event details.
+ void HandleThumbstickMove(GamepadThumbstickMoveEvent @event);
+
+ ///
+ /// Called when one of the two triggers moves.
+ ///
+ /// The event details.
+ void HandleTriggerMove(GamepadTriggerMoveEvent @event);
+}
\ No newline at end of file
diff --git a/sources/Input/Input/IInputBackend.cs b/sources/Input/Input/IInputBackend.cs
new file mode 100644
index 0000000000..e67ce94d0f
--- /dev/null
+++ b/sources/Input/Input/IInputBackend.cs
@@ -0,0 +1,49 @@
+namespace Silk.NET.Input;
+
+///
+/// Represents an input backend capable of receiving human input from Human Input Devices (HIDs).
+///
+///
+/// The onus is on the user to coordinate using this type across threads, as the input backend is not thread safe
+/// In addition, certain backends may have (unavoidable) restrictions on what thread can be called
+/// on - the user is responsible for respecting these threading rules as well.
+///
+public interface IInputBackend : IDisposable
+{
+ ///
+ /// Gets a rough human-readable description of the input backend. Its value is not intrinsically meaningful.
+ ///
+ string Name { get; }
+
+ ///
+ /// Gets a globally-unique integral identifier for this device.
+ ///
+ nint Id { get; }
+
+ ///
+ /// Get a list containing all the connected devices available from this input backend.
+ ///
+ ///
+ /// When a device is disconnected, its shall no longer function and will not be
+ /// enumerated by this list. When a device is connected, an with that physical device ID
+ /// shall be added to this list. In addition, upon connection any past objects previously
+ /// enumerated by this list on this instance shall also regain function if the device
+ /// being added to this list shares the same physical device ID as those previous instances. All such previous
+ /// instances shall be equatable to one another and to the instance added to this list.
+ /// An implementation is welcome to reuse old objects, but this is strictly implementation-defined. A device not
+ /// being present in the (checked using s
+ /// implementation) list is sufficient evidence that a device has been
+ /// disconnected.
+ ///
+ IReadOnlyList Devices { get; }
+
+ ///
+ /// Polls and updates the state of the objects connected using this backend, sending
+ /// input events to the given to reflect the human input received.
+ ///
+ ///
+ /// The value of the State properties on each device must not change until this method is called.
+ ///
+ /// The input handler.
+ void Update(IInputHandler? handler = null);
+}
diff --git a/sources/Input/Input/IInputDevice.cs b/sources/Input/Input/IInputDevice.cs
new file mode 100644
index 0000000000..a53a47d750
--- /dev/null
+++ b/sources/Input/Input/IInputDevice.cs
@@ -0,0 +1,36 @@
+namespace Silk.NET.Input;
+
+///
+/// Represents a connected Human Input Device (HID).
+///
+///
+/// All devices originate from a backend.
+///
+/// An object shall be equatable to any such object retrieved from the same backend where
+/// is equal.
+///
+/// objects must not store any managed state, and if there is a requirement for this in a
+/// future extension of this API then this must be defined in such a way that the state storage and lifetime is
+/// user-controlled. While objects are equatable based on s, if a physical
+/// device disconnects and reconnects the does not provide a guarantee that the same object
+/// will be returned (primarily because doing so would require the to keep track of every
+/// object it's ever created), rather a "compatible" one that acts identically to the original object. This is
+/// completely benign if the object is nothing but a wrapper to the backend anyway. If there is unmanaged state (e.g. a
+/// handle to a device that must be explicitly closed upon disconnection), then it is expected that even in the event of
+/// reconnection, old objects (e.g. created with a now-disposed handle) shall still work for the newly-reconnected
+/// device. A common way this could be implemented is storing the handles in the
+/// implementation instead in the form of a mapping of physical device IDs ( ) to those handles. This
+/// solves the object lifetime problem while also not adding undue complications to user code.
+///
+public interface IInputDevice : IEquatable
+{
+ ///
+ /// Gets a globally-unique integral identifier for this device.
+ ///
+ nint Id { get; }
+
+ ///
+ /// Gets a rough human-readable description of the input device. Its value is not intrinsically meaningful.
+ ///
+ string Name { get; }
+}
\ No newline at end of file
diff --git a/sources/Input/Input/IInputHandler.cs b/sources/Input/Input/IInputHandler.cs
new file mode 100644
index 0000000000..3a7c7bbccc
--- /dev/null
+++ b/sources/Input/Input/IInputHandler.cs
@@ -0,0 +1,15 @@
+namespace Silk.NET.Input;
+
+///
+/// Represents a handler of human input. Implementations of this type will receive a method call for each distinctive
+/// HID event received in the order they were received, to the best of the backend's ability. All visible changes to
+/// device state correspond to a method call using this interface.
+///
+public interface IInputHandler
+{
+ ///
+ /// Called when an disconnects from the application.
+ ///
+ /// The event details.
+ void HandleDeviceConnectionChanged(ConnectionEvent @event);
+}
\ No newline at end of file
diff --git a/sources/Input/Input/IJoystick.cs b/sources/Input/Input/IJoystick.cs
new file mode 100644
index 0000000000..df5bb9b3b3
--- /dev/null
+++ b/sources/Input/Input/IJoystick.cs
@@ -0,0 +1,16 @@
+namespace Silk.NET.Input;
+
+///
+/// Represents a joystick with axes, buttons, and hats.
+///
+public interface IJoystick : IButtonDevice
+{
+ ///
+ /// Gets the device state.
+ ///
+ ///
+ /// Only updated when is called.
+ ///
+ new JoystickState State { get; }
+ ButtonReadOnlyList IButtonDevice.State => State.Buttons;
+}
\ No newline at end of file
diff --git a/sources/Input/Input/IJoystickInputHandler.cs b/sources/Input/Input/IJoystickInputHandler.cs
new file mode 100644
index 0000000000..5dca7202d1
--- /dev/null
+++ b/sources/Input/Input/IJoystickInputHandler.cs
@@ -0,0 +1,19 @@
+namespace Silk.NET.Input;
+
+///
+/// An that also receives input.
+///
+public interface IJoystickInputHandler : IButtonInputHandler
+{
+ ///
+ /// Called when an axis on the joystick moves.
+ ///
+ /// The event details.
+ void HandleAxisMove(JoystickAxisMoveEvent @event);
+
+ ///
+ /// Called when a hat on the joystick moves.
+ ///
+ /// The event details.
+ void HandleHatMove(JoystickHatMoveEvent @event);
+}
\ No newline at end of file
diff --git a/sources/Input/Input/IKeyboard.cs b/sources/Input/Input/IKeyboard.cs
new file mode 100644
index 0000000000..5dcbf4896c
--- /dev/null
+++ b/sources/Input/Input/IKeyboard.cs
@@ -0,0 +1,51 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace Silk.NET.Input;
+
+///
+/// Represents a keyboard device.
+///
+public interface IKeyboard : IButtonDevice
+{
+ ///
+ /// Gets the device state.
+ ///
+ ///
+ /// Only updated when is called.
+ ///
+ new KeyboardState State { get; }
+
+ ButtonReadOnlyList IButtonDevice.State => State.Keys;
+
+ ///
+ /// Gets or sets the current text on the clipboard.
+ ///
+ string? ClipboardText { get; set; }
+
+ ///
+ /// Attempts to get a user-displayable string in the user's locale for the key at the physical position represented
+ /// by in the user's current keyboard layout.
+ ///
+ /// The physical key name. Consult documentation for more info.
+ /// The user-displayable name of the key.
+ /// Whether the name was successfully retrieved.
+ bool TryGetKeyName(KeyName key, [NotNullWhen(true)] out string? name);
+
+ ///
+ /// Begins recording keyboard input. Without / , there is no
+ /// guarantee that will be raised as this might require displaying
+ /// a concept/touchscreen keyboard on certain platforms (e.g. phones). It is recommended that you call
+ /// when you'd like to capture text input (e.g. in a text box), followed by
+ /// when you have completed collecting such input.
+ ///
+ void BeginInput();
+
+ ///
+ /// Concludes recording keyboard input. Without / , there is no
+ /// guarantee that will be raised as this might require displaying
+ /// a concept/touchscreen keyboard on certain platforms (e.g. phones). It is recommended that you call
+ /// when you'd like to capture text input (e.g. in a text box), followed by
+ /// when you have completed collecting such input.
+ ///
+ string? EndInput();
+}
diff --git a/sources/Input/Input/IKeyboardInputHandler.cs b/sources/Input/Input/IKeyboardInputHandler.cs
new file mode 100644
index 0000000000..6ca22ec632
--- /dev/null
+++ b/sources/Input/Input/IKeyboardInputHandler.cs
@@ -0,0 +1,23 @@
+namespace Silk.NET.Input;
+
+///
+/// An that also receives events.
+///
+public interface IKeyboardInputHandler : IButtonInputHandler
+{
+ ///
+ /// Called when a key is pressed or depressed.
+ ///
+ /// The event details.
+ void HandleKeyChanged(KeyChangedEvent @event);
+
+ ///
+ /// Called when a character is typed.
+ ///
+ ///
+ /// Ensure you have called to start receiving text, after which events will be
+ /// sent for each character until is called.
+ ///
+ /// The event details.
+ void HandleKeyChar(KeyCharEvent @event);
+}
\ No newline at end of file
diff --git a/sources/Input/Input/IMotor.cs b/sources/Input/Input/IMotor.cs
new file mode 100644
index 0000000000..f8875e7149
--- /dev/null
+++ b/sources/Input/Input/IMotor.cs
@@ -0,0 +1,13 @@
+namespace Silk.NET.Input;
+
+///
+/// Represents a vibration motor.
+///
+public interface IMotor
+{
+ ///
+ /// Gets or sets the speed at which the motor is operating, where 0.0 represents no vibration and 1.0
+ /// represents the maximum amount of vibration.
+ ///
+ float Speed { get; set; }
+}
\ No newline at end of file
diff --git a/sources/Input/Input/IMouse.cs b/sources/Input/Input/IMouse.cs
new file mode 100644
index 0000000000..e71c8d7162
--- /dev/null
+++ b/sources/Input/Input/IMouse.cs
@@ -0,0 +1,34 @@
+using System.Numerics;
+
+namespace Silk.NET.Input;
+
+///
+/// Represents a mouse - a type of pointer device.
+///
+public interface IMouse : IPointerDevice
+{
+ ///
+ /// Gets the device state.
+ ///
+ ///
+ /// Only updated when is called.
+ ///
+ new MouseState State { get; }
+
+ PointerState IPointerDevice.State => State;
+
+ ///
+ /// Gets the cursor configuration of the mouse.
+ ///
+ ///
+ /// This will determine which points shall lie on.
+ ///
+ ICursorConfiguration Cursor { get; }
+
+ ///
+ /// Attempts to set the position of the mouse.
+ ///
+ /// The position of the mouse in window coordinates.
+ /// Whether the requested position has been applied.
+ bool TrySetPosition(Vector2 position);
+}
\ No newline at end of file
diff --git a/sources/Input/Input/IMouseInputHandler.cs b/sources/Input/Input/IMouseInputHandler.cs
new file mode 100644
index 0000000000..71d02ad1c6
--- /dev/null
+++ b/sources/Input/Input/IMouseInputHandler.cs
@@ -0,0 +1,13 @@
+namespace Silk.NET.Input;
+
+///
+/// An that receives input from an .
+///
+public interface IMouseInputHandler : IButtonInputHandler
+{
+ ///
+ /// Called when the user scrolls using the scroll wheel.
+ ///
+ /// The event details.
+ void HandleScroll(MouseScrollEvent @event);
+}
\ No newline at end of file
diff --git a/sources/Input/Input/IPointerDevice.cs b/sources/Input/Input/IPointerDevice.cs
new file mode 100644
index 0000000000..de4e803f28
--- /dev/null
+++ b/sources/Input/Input/IPointerDevice.cs
@@ -0,0 +1,20 @@
+namespace Silk.NET.Input;
+
+///
+/// Represents a device with which the user can point at a target.
+///
+public interface IPointerDevice : IButtonDevice
+{
+ ///
+ /// Gets the device state.
+ ///
+ ///
+ /// Only updated when is called.
+ ///
+ new PointerState State { get; }
+ ButtonReadOnlyList IButtonDevice.State => State.Buttons;
+ ///
+ /// Gets the targets at which the user can point with their pointer.
+ ///
+ IReadOnlyList Targets { get; }
+}
\ No newline at end of file
diff --git a/sources/Input/Input/IPointerInputHandler.cs b/sources/Input/Input/IPointerInputHandler.cs
new file mode 100644
index 0000000000..f4bd67c0fc
--- /dev/null
+++ b/sources/Input/Input/IPointerInputHandler.cs
@@ -0,0 +1,26 @@
+namespace Silk.NET.Input;
+
+///
+/// An that also receives events.
+///
+public interface IPointerInputHandler : IButtonInputHandler
+{
+ ///
+ /// Called when the properties of a target at which the user can point using the pointer change. This includes the
+ /// addition and removal of targets.
+ ///
+ /// The event details.
+ void HandleTargetChanged(PointerTargetChangedEvent @event);
+
+ ///
+ /// Called when the user adds, removes, or changes a point at which they're pointing at a target.
+ ///
+ /// The event details.
+ void HandlePointChanged(PointChangedEvent @event);
+
+ ///
+ /// Called when the user changes the pressure with which they're gripping the pointer device.
+ ///
+ /// The event details.
+ void HandleGripChanged(PointerGripChangedEvent @event);
+}
\ No newline at end of file
diff --git a/sources/Input/Input/IPointerTarget.cs b/sources/Input/Input/IPointerTarget.cs
new file mode 100644
index 0000000000..5bf3595bda
--- /dev/null
+++ b/sources/Input/Input/IPointerTarget.cs
@@ -0,0 +1,41 @@
+using Silk.NET.Maths;
+
+namespace Silk.NET.Input;
+
+///
+/// Represents a target at which the user can point using their pointer device.
+///
+public interface IPointerTarget
+{
+ ///
+ /// The boundary in which positions of points on this target shall fall. For ,
+ /// shall represent the lack of a lower bound on a particular axis. For
+ /// For , shall represent the lack of a lower bound
+ /// on a particular axis. 0 represents an unused axis that axis is 0 on both
+ /// and .
+ ///
+ Box3D Bounds { get; }
+
+ ///
+ /// Gets the number of points with which the given pointer is pointing at this target.
+ ///
+ /// The number of points.
+ ///
+ /// A single "logical" pointer device may have many points, and can optionally represent multiple physical pointers
+ /// as a single logical device - this is the case where a backend supports multiple mice to control an
+ /// cursor on its "raw mouse input" target, but combines these all to a single point on its "windowed" target. This
+ /// is also true for touch input - a touch screen is represented as a single touch device,
+ /// where each finger is its own point.
+ ///
+ int GetPointCount(IPointerDevice pointer);
+
+ ///
+ /// Gets a point with which the given pointer is pointing at this target.
+ ///
+ /// The pointer device.
+ ///
+ /// The index of the point, between 0 and the number sourced from .
+ ///
+ /// The point at the given index with which the given pointer device is pointing at the target.
+ TargetPoint GetPoint(IPointerDevice pointer, int point);
+}
\ No newline at end of file
diff --git a/sources/Input/Input/Implementations/EnumInfo.cs b/sources/Input/Input/Implementations/EnumInfo.cs
new file mode 100644
index 0000000000..76144706eb
--- /dev/null
+++ b/sources/Input/Input/Implementations/EnumInfo.cs
@@ -0,0 +1,267 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+using System.Runtime.CompilerServices;
+
+namespace Silk.NET.Input;
+
+// ReSharper disable StaticMemberInGenericType
+// ^ that's the point
+///
+/// A helper class for quickly converting enum values into indexes, particularly
+/// when there is a possibility of unknown/unnamed enum values. See for an example
+/// of an appropriate implementation along with
+///
+///
+internal static class EnumInfo where T : unmanaged, Enum
+{
+ ///
+ /// All enum values sorted in increasing order (unstable sort)
+ ///
+ public static IReadOnlyList All => _all;
+
+ ///
+ /// All enum values with distinct numerical values sorted in increasing order.
+ /// In the case of multiple enum entries with the same numerical value, this makes no guarantees about
+ /// which version ends up here.
+ ///
+ public static readonly IReadOnlyList UniqueValues;
+
+
+ ///
+ /// The value with the highest numerical value
+ ///
+ public static readonly T MaxValue;
+
+ ///
+ /// The value with the lowest numerical value
+ ///
+ public static readonly T MinValue;
+
+ ///
+ /// The numerical type of the enum
+ ///
+ public static readonly Type UnderlyingType = typeof(T).GetEnumUnderlyingType();
+
+ private static readonly T[] _all;
+ private static readonly string[] _names;
+ private static readonly Dictionary _numericallyDistinctValues;
+ private static readonly ulong[] _allEnumValuesRaw;
+ private static bool _unnamedAreIndexable;
+
+ static unsafe EnumInfo()
+ {
+ var customAttributeDatas = typeof(T).CustomAttributes;
+ var hasFlagsAttribute = false;
+ foreach (var attr in customAttributeDatas)
+ {
+ if (attr.AttributeType == typeof(FlagsAttribute))
+ {
+ hasFlagsAttribute = true;
+ }
+
+ if (attr.AttributeType == typeof(OrderedIndexUsageAttribute))
+ {
+ _unnamedAreIndexable = true;
+ }
+ }
+
+ if (hasFlagsAttribute)
+ {
+ throw new InvalidOperationException("Enums with the FlagsAttribute cannot be used with EnumInfo");
+ }
+
+ var underlyingType = UnderlyingType;
+ T[] vals;
+ T[] all;
+ if (underlyingType == typeof(int))
+ {
+ all = OrderedValues(false);
+ vals = OrderedValues(true);
+ _allEnumValuesRaw = vals.Select(x => (ulong)*(uint*)&x).ToArray();
+ }
+ else if (underlyingType == typeof(uint))
+ {
+ all = OrderedValues(false);
+ vals = OrderedValues(true);
+ _allEnumValuesRaw = vals.Select(x => (ulong)*(uint*)&x).ToArray();
+ }
+ else if (underlyingType == typeof(byte))
+ {
+ all = OrderedValues(false);
+ vals = OrderedValues(true);
+ _allEnumValuesRaw = vals.Select(x => (ulong)*(byte*)&x).ToArray();
+ }
+ else if (underlyingType == typeof(sbyte))
+ {
+ all = OrderedValues(false);
+ vals = OrderedValues(true);
+ _allEnumValuesRaw = vals.Select(x => (ulong)*(byte*)&x).ToArray();
+ }
+ else if (underlyingType == typeof(short))
+ {
+ all = OrderedValues(false);
+ vals = OrderedValues(true);
+ _allEnumValuesRaw = vals.Select(x => (ulong)*(ushort*)&x).ToArray();
+ }
+ else if (underlyingType == typeof(ushort))
+ {
+ all = OrderedValues(false);
+ vals = OrderedValues(true);
+ _allEnumValuesRaw = vals.Select(x => (ulong)*(ushort*)&x).ToArray();
+ }
+ else if (underlyingType == typeof(long))
+ {
+ all = OrderedValues(false);
+ vals = OrderedValues(true);
+ _allEnumValuesRaw = vals.Select(x => *(ulong*)&x).ToArray();
+ }
+ else if (underlyingType == typeof(ulong))
+ {
+ all = OrderedValues(false);
+ vals = OrderedValues(true);
+ _allEnumValuesRaw = vals.Select(x => *(ulong*)&x).ToArray();
+ }
+ else
+ {
+ throw new InvalidOperationException("Enum provided uses an unknown numeric base??");
+ }
+
+
+ var names = new string[all.Length];
+ for (var index = 0; index < all.Length; index++)
+ {
+ names[index] = all[index].ToString(); // todo: readable name attributes?
+ }
+
+ var dict = new Dictionary(vals.Length);
+ for (var i = 0; i < vals.Length; i++)
+ {
+ var enumVal = vals[i];
+ dict.Add(enumVal, i);
+ }
+
+ _names = names;
+ _all = all;
+ UniqueValues = vals;
+ _numericallyDistinctValues = dict;
+ MinValue = All[0];
+ MaxValue = All[^1];
+ }
+
+ ///
+ /// Get the ordered index of the value provided.
+ /// Values with the same numerical value will *not* return the same index, and are not guaranteed to be
+ /// stably sorted across application runs.
+ /// The index provided
+ ///
+ ///
+ /// The index of the sorted enum value
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static int NameIndexOf(T value) => Array.IndexOf(_all, value);
+
+ ///
+
+ ///
+ /// Returns the names of an enum value, pre-allocated
+ ///
+ public static string NameOf(T value) => _names[NameIndexOf(value)];
+
+ ///
+ /// Get the ordered index of the value provided.
+ /// Values with the same numerical value will return the same index
+ ///
+ ///
+ /// The index of the sorted enum numerical value, or -1 if not a named enum member.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int ValueIndexOf(T value) => !_unnamedAreIndexable
+ ? ValueOf(value)
+ : _numericallyDistinctValues.GetValueOrDefault(value, -1);
+
+ ///
+ /// Gets the ordered index of the unnamed enum value provided. This index is calculated by:
+ /// (the number of named members in this enum type) + (the raw value of the number if unnamed)
+ ///
+ /// Negative values or values that are above the lowest enum value will return -1, as they cannot be used for indexing
+ ///
+ ///
+ ///
+ public static int ValueIndexOfUnnamed(T value)
+ {
+ if (!_unnamedAreIndexable)
+ {
+ return ValueOf(value);
+ }
+
+ if(_numericallyDistinctValues.TryGetValue(value, out var index))
+ {
+ return index;
+ }
+
+ var rawValue = ValueOf(value);
+
+ // todo - don't rely on joystickButton's unknown - find the MinValue
+ if (rawValue <= 0 || rawValue >= ValueOf(_allEnumValuesRaw[0]))
+ {
+ return -1;
+ }
+
+ return _all.Length + rawValue;
+ }
+
+ ///
+ /// Returns the numerical value of the enum value provided in a type-safe way
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static unsafe TNumber ValueOf(TValue value) where TNumber : unmanaged where TValue : unmanaged
+ {
+ if (sizeof(T) == sizeof(TNumber))
+ {
+ return Unsafe.Read(&value);
+ }
+
+ var minSize = Math.Min(sizeof(TNumber), sizeof(T));
+
+ var originalValuePtr = (byte*)&value;
+
+ var valuePtr = &originalValuePtr[Math.Abs(minSize - sizeof(T))]; // does this assume little-endianness?
+ var numberPtr = stackalloc byte[sizeof(TNumber)];
+
+ // ensure block is initialized (as it isnt guaranteed?) so any missing bytes of the output will stay 0
+ // if type TNumber is a larger size than type T
+ Unsafe.InitBlock(numberPtr, 0, (uint)sizeof(TNumber));
+
+ var copyToPtr = &numberPtr[Math.Abs(minSize - sizeof(TNumber))];
+ Buffer.MemoryCopy(valuePtr, copyToPtr, sizeof(TNumber), minSize);
+ return *(TNumber*)numberPtr;
+ }
+
+ private static T[] OrderedValues(bool byNumericValue)
+ where TNumber : unmanaged, IComparable
+ {
+ // numerically distinct numbers
+ var allValues = Enum.GetValues();
+
+ if (byNumericValue)
+ {
+ allValues = allValues.DistinctBy(ValueOf).ToArray();
+ }
+
+ // sort by increasing order
+ Array.Sort(allValues, (a, b) => {
+ var aNumber = ValueOf(a);
+ var bNumber = ValueOf(b);
+ return aNumber.CompareTo(bNumber);
+ });
+
+ return allValues;
+ }
+
+ public static unsafe bool HasValue(int value) => _allEnumValuesRaw.Contains(*(uint*)&value);
+}
+
+[AttributeUsage(AttributeTargets.Enum)]
+internal class OrderedIndexUsageAttribute : Attribute;
diff --git a/sources/Input/Input/Implementations/KeyHandling/ICharacterConverter.cs b/sources/Input/Input/Implementations/KeyHandling/ICharacterConverter.cs
new file mode 100644
index 0000000000..2ee4c27b12
--- /dev/null
+++ b/sources/Input/Input/Implementations/KeyHandling/ICharacterConverter.cs
@@ -0,0 +1,188 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+
+namespace Silk.NET.Input.KeyHandling;
+
+///
+/// A simple interface for an implementation that converts keyboard input into characters for text entry
+///
+internal interface ICharacterConverter
+{
+ public bool TryConvert(KeyName key, KeyModifiers modifiers, [NotNullWhen(true)] out char? c);
+}
+
+internal class DummyCharConverter : ICharacterConverter
+{
+ // todo - proper VK key support for various languages and layouts
+ public bool TryConvert(KeyName key, KeyModifiers modifiers, [NotNullWhen(true)] out char? c)
+ {
+ if (!key.IsChar())
+ {
+ c = null;
+ return false;
+ }
+
+ if (key is >= KeyName.A and <= KeyName.Z)
+ {
+ var diff = (int)key - (int)KeyName.A;
+ c = (char)('a' + diff);
+ if (modifiers.ShouldCapitalize())
+ {
+ c = CultureInfo.CurrentCulture.TextInfo.ToUpper(c.Value);
+ }
+
+ return true;
+ }
+
+ var isShifted = modifiers.IsShift();
+ switch (key)
+ {
+ case KeyName.Number1:
+ c = isShifted ? '!' : '1';
+ return true;
+ case KeyName.Keypad1:
+ c = '1';
+ return true;
+ case KeyName.Number2:
+ c = isShifted ? '@' : '2';
+ return true;
+ case KeyName.Keypad2:
+ c = '2';
+ return true;
+ case KeyName.Number3:
+ c = isShifted ? '#' : '3';
+ return true;
+ case KeyName.Keypad3:
+ c = '3';
+ return true;
+ case KeyName.Number4:
+ c = isShifted ? '$' : '4';
+ return true;
+ case KeyName.Keypad4:
+ c = '4';
+ return true;
+ case KeyName.Number5:
+ c = isShifted ? '%' : '5';
+ return true;
+ case KeyName.Keypad5:
+ c = '5';
+ return true;
+ case KeyName.Number6:
+ c = isShifted ? '^' : '6';
+ return true;
+ case KeyName.Keypad6:
+ c = '6';
+ return true;
+ case KeyName.Number7:
+ c = isShifted ? '&' : '7';
+ return true;
+ case KeyName.Keypad7:
+ c = '7';
+ return true;
+ case KeyName.Number8:
+ c = isShifted ? '*' : '8';
+ return true;
+ case KeyName.Keypad8:
+ c = '8';
+ return true;
+ case KeyName.Number9:
+ c = isShifted ? '(' : '9';
+ return true;
+ case KeyName.Keypad9:
+ c = '9';
+ return true;
+ case KeyName.Number0:
+ c = isShifted ? ')' : '0';
+ return true;
+ case KeyName.Keypad0:
+ c = '0';
+ return true;
+ case KeyName.Minus:
+ c = isShifted ? '_' : '-';
+ return true;
+ case KeyName.Equals:
+ c = isShifted ? '+' : '=';
+ return true;
+ case KeyName.Tab:
+ c = '\t';
+ return true;
+ case KeyName.Apostrophe:
+ c = isShifted ? '\"' : '\'';
+ return true;
+ case KeyName.Backslash:
+ c = isShifted ? '|' : '\\';
+ return true;
+ case KeyName.Semicolon:
+ c = isShifted ? ':' : ';';
+ return true;
+ case KeyName.Comma:
+ c = isShifted ? '<' : ',';
+ return true;
+ case KeyName.Period:
+ c = isShifted ? '>' : '.';
+ return true;
+ case KeyName.Slash:
+ c = isShifted ? '?' : '/';
+ return true;
+ case KeyName.Space:
+ c = ' ';
+ return true;
+ case KeyName.KeypadAmpersand:
+ c = '&';
+ return true;
+ case KeyName.KeypadPercent:
+ c = '%';
+ return true;
+ case KeyName.KeypadColon:
+ c = ':';
+ return true;
+ case KeyName.KeypadLeftParenthesis:
+ c = '(';
+ return true;
+ case KeyName.KeypadRightParenthesis:
+ c = ')';
+ return true;
+ case KeyName.KeypadPlus:
+ c = '+';
+ return true;
+ case KeyName.KeypadComma:
+ c = ',';
+ return true;
+ case KeyName.KeypadMinus:
+ c = '-';
+ return true;
+ case KeyName.KeypadPeriod:
+ c = '.';
+ return true;
+ case KeyName.KeypadDivide:
+ c = '/';
+ return true;
+ case KeyName.KeypadEquals:
+ c = '=';
+ return true;
+ case KeyName.KeypadEnter:
+ case KeyName.Return:
+ case KeyName.Return2:
+ c = '\n';
+ return true;
+ case KeyName.KeypadExclamation:
+ c = '!';
+ return true;
+ case KeyName.KeypadMultiply:
+ c = '*';
+ return true;
+ case KeyName.Grave:
+ c = '`';
+ return true;
+ case KeyName.CurrencyUnit:
+ c = RegionInfo.CurrentRegion.CurrencySymbol[0];
+ return true;
+ }
+
+ c = null;
+ return false;
+ }
+}
diff --git a/sources/Input/Input/Implementations/KeyHandling/KeyNameExtensions.cs b/sources/Input/Input/Implementations/KeyHandling/KeyNameExtensions.cs
new file mode 100644
index 0000000000..7ee140e867
--- /dev/null
+++ b/sources/Input/Input/Implementations/KeyHandling/KeyNameExtensions.cs
@@ -0,0 +1,111 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+
+namespace Silk.NET.Input.KeyHandling;
+
+///
+/// A series of extension methods for making sense of values
+///
+public static class KeyNameExtensions
+{
+ ///
+ /// Returns true if the key would produce a character in common text editing scenarios. Includes whitespace.
+ ///
+ public static bool IsChar(this KeyName name) =>
+ name is >= KeyName.A and <= KeyName.Return
+ or >= KeyName.KeypadDivide and <= KeyName.KeypadPeriod
+ or >= KeyName.Tab and <= KeyName.Slash
+ or >= KeyName.KeypadMultiply and <= KeyName.KeypadEnter
+ or >= KeyName.KeypadA and <= KeyName.KeypadExclamation
+ or >= KeyName.Keypad00 and <= KeyName.KeypadTab
+ or KeyName.Return2 or KeyName.Separator or KeyName.KeypadPlusMinus
+ or KeyName.KeypadComma
+ or KeyName.KeypadEquals or KeyName.OtherKeypadEquals;
+
+ ///
+ /// Returns true if the given key would produce a deletion of one or more characters in common text
+ /// editing scenarios.
+ ///
+ public static bool IsDeletion(this KeyName name) =>
+ name is KeyName.Backspace
+ or KeyName.Delete
+ or KeyName.KeypadBackspace
+ or KeyName.Clear
+ or KeyName.KeypadClear
+ or KeyName.KeypadClearEntry
+ or KeyName.ClearAgain;
+
+ public static TextDeletionType GetDeletionType(this KeyName name)
+ {
+ Debug.Assert(name.IsDeletion());
+
+ return name switch {
+ KeyName.Backspace => TextDeletionType.Back,
+ KeyName.Delete => TextDeletionType.Forward,
+ KeyName.KeypadBackspace => TextDeletionType.Back,
+ KeyName.Clear => TextDeletionType.All,
+ KeyName.KeypadClear => TextDeletionType.All,
+ KeyName.KeypadClearEntry => TextDeletionType.All,
+ KeyName.ClearAgain => TextDeletionType.All,
+ _ => TextDeletionType.None
+ };
+ }
+
+ ///
+ /// An enum representing the type of text deletion, if any. For example,
+ /// would be , would be , etc.
+ ///
+ public enum TextDeletionType
+ {
+ ///
+ /// Key represents a deletion of one (or more) character(s) behind the cursor.
+ ///
+ Back,
+
+ ///
+ /// Key represents a deletion of one (or more) character(s) ahead of the cursor.
+ ///
+ Forward,
+
+ ///
+ /// Key represents a deletion of all characters in current text entry context
+ ///
+ All,
+
+ ///
+ /// Key does not represent a deletion.
+ ///
+ None = -1
+ }
+
+ ///
+ /// Returns true if the modifiers signify that the next character should be capitalized.
+ ///
+ public static bool ShouldCapitalize(this KeyModifiers modifiers) =>
+ ((modifiers & KeyModifiers.CapsLock) == KeyModifiers.CapsLock) ^
+ ((modifiers & KeyModifiers.ShiftLeft) == KeyModifiers.ShiftLeft
+ || (modifiers & KeyModifiers.ShiftRight) == KeyModifiers.ShiftRight);
+
+ public static bool IsControl(this KeyName name) =>
+ name is KeyName.ControlLeft or KeyName.ControlRight;
+
+ public static bool IsControl(this KeyModifiers mod) =>
+ (mod & KeyModifiers.ControlLeft) == KeyModifiers.ControlLeft ||
+ (mod & KeyModifiers.ControlRight) == KeyModifiers.ControlRight;
+
+ public static bool IsShift(this KeyName name) =>
+ name is KeyName.ShiftLeft or KeyName.ShiftRight;
+
+ public static bool IsShift(this KeyModifiers mod) =>
+ (mod & KeyModifiers.ShiftLeft) == KeyModifiers.ShiftLeft ||
+ (mod & KeyModifiers.ShiftRight) == KeyModifiers.ShiftRight;
+
+ public static bool IsAlt(this KeyName name) =>
+ name is KeyName.AltLeft or KeyName.AltRight;
+
+ public static bool IsAlt(this KeyModifiers mod) =>
+ (mod & KeyModifiers.AltLeft) == KeyModifiers.AltLeft ||
+ (mod & KeyModifiers.AltRight) == KeyModifiers.AltRight;
+}
diff --git a/sources/Input/Input/Implementations/KeyHandling/TextRecorder.cs b/sources/Input/Input/Implementations/KeyHandling/TextRecorder.cs
new file mode 100644
index 0000000000..8df78933bf
--- /dev/null
+++ b/sources/Input/Input/Implementations/KeyHandling/TextRecorder.cs
@@ -0,0 +1,398 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using System.Text;
+using Silk.NET.SDL;
+
+namespace Silk.NET.Input.KeyHandling;
+
+///
+/// A utility class for recording text input.
+/// Where possible, it may be preferable to use and
+/// instead, but this requires the use of the SDL windowing API, which may not be available in all contexts.
+/// This class is a work in progress, and not yet sufficient for full text-editor support.
+///
+internal sealed class TextRecorder
+{
+ private readonly ICharacterConverter _converter;
+
+ ///
+ /// Constructor
+ ///
+ public TextRecorder(ICharacterConverter? converter)
+ {
+ _converter = converter ?? new DummyCharConverter();
+ }
+
+ ///
+ /// The number of characters currently in the buffer.
+ ///
+ public int Count => _sb.Length;
+
+ ///
+ /// Modify the buffer and recorder state based on the input key name and the state of the keyboard.
+ ///
+ /// The keystroke to add
+ /// The keyboard whose state we are recording
+ public void AddKeyStroke(KeyName name, IKeyboard keyboard)
+ {
+ var isChar = name.IsChar();
+ var isDeletion = name.IsDeletion();
+
+ if (!isChar && !isDeletion)
+ {
+ return;
+ }
+
+ if (name == KeyName.Paste)
+ {
+ var clipboardText = keyboard.ClipboardText;
+ if (!string.IsNullOrEmpty(clipboardText))
+ {
+ InsertText(clipboardText);
+ }
+
+ return;
+ }
+
+ var state = keyboard.State;
+ var activeModifiers = state.Modifiers;
+ if (name.IsChar())
+ {
+ if (activeModifiers.IsAlt() || activeModifiers.IsControl())
+ {
+ return;
+ }
+
+ // insert the appropriate character
+ // first, we need the virtual key represented by the scancode (KeyName)
+
+ if (_converter.TryConvert(name, activeModifiers, out var c))
+ {
+ InsertText(c.Value);
+ }
+
+ return;
+ }
+
+ if (name.IsDeletion())
+ {
+ var deletionType = name.GetDeletionType();
+ Debug.Assert(deletionType != KeyNameExtensions.TextDeletionType.None);
+ switch (deletionType)
+ {
+ case KeyNameExtensions.TextDeletionType.Back:
+ if (_selectionLength > 0)
+ {
+ RemoveSelectedTextAndClearSelection();
+ }
+ else if (_cursorStart > 0)
+ {
+ // remove from behind cursor and move cursor back accordingly
+ if (activeModifiers.IsControl())
+ {
+ // find first whitespace character prior to current cursor position
+ var cursorPos = _cursorStart;
+ while (cursorPos > 0 && !char.IsWhiteSpace(_sb[cursorPos - 1]))
+ {
+ --cursorPos;
+ }
+
+ var count = Math.Min(_cursorStart - cursorPos, 1);
+ _sb.Remove(cursorPos, count);
+ _cursorStart = cursorPos;
+ }
+ else
+ {
+ _sb.Remove(--_cursorStart, 1);
+ }
+ }
+
+ break;
+ case KeyNameExtensions.TextDeletionType.Forward:
+ if (_selectionLength > 0)
+ {
+ RemoveSelectedTextAndClearSelection();
+ }
+ else if (_cursorStart < _sb.Length)
+ {
+ // remove from front of cursor
+ if (activeModifiers.IsControl())
+ {
+ // find first whitespace character after current cursor position
+ var cursorPos = _cursorStart;
+ while (cursorPos < _sb.Length && !char.IsWhiteSpace(_sb[cursorPos]))
+ {
+ ++cursorPos;
+ }
+
+ var count = Math.Min(_sb.Length - cursorPos, 1);
+ _sb.Remove(cursorPos, count);
+ }
+ else
+ {
+ _sb.Remove(_cursorStart, 1);
+ }
+ }
+ break;
+ case KeyNameExtensions.TextDeletionType.All:
+ _sb.Clear();
+ SetCursorPositionRaw(0);
+ SetSelectionLength(0);
+ break;
+ default:
+ Console.Error.WriteLine("Unexpected text deletion type");
+ break;
+ }
+ }
+ }
+
+ ///
+ /// Removes the currently selected text and sets the current selection length to 0.
+ ///
+ private void RemoveSelectedTextAndClearSelection()
+ {
+ // remove the currently selected text
+ var selectedLength = _selectionLength;
+ SetSelectionLength(0);
+ if (selectedLength > 0 && _cursorStart < _sb.Length)
+ {
+ Debug.Assert(_cursorStart + selectedLength <= _sb.Length);
+ _sb.Remove(_cursorStart, selectedLength);
+ }
+ }
+
+ ///
+ /// Inserts the given text into the buffer at the current cursor/selection position.
+ ///
+ ///
+ public void InsertText(ReadOnlySpan str)
+ {
+ RemoveSelectedTextAndClearSelection();
+
+ if (str.Length > 0)
+ {
+ _sb.Insert(_cursorStart, str);
+ SetCursorPositionRaw(_cursorStart + str.Length);
+ }
+ }
+
+ ///
+ /// Inserts the given text into the buffer at the current cursor/selection position.
+ ///
+ ///
+ public void InsertText(char c)
+ {
+ ReadOnlySpan span = [c];
+ InsertText(span);
+ }
+
+ ///
+ /// Inserts the given text into the buffer at the given cursor position. Given cursor position is always
+ /// clamped to the bounds of the buffer. Clears any current selection if the actual cursor position is different from
+ /// the provided cursor position.
+ ///
+ ///
+ /// The cursor position in the buffer to inject
+ public void InsertTextAt(ReadOnlySpan str, int cursorStart)
+ {
+ if (_cursorStart != cursorStart)
+ {
+ SetCursorPositionRaw(cursorStart);
+ SetSelectionLength(0);
+ }
+
+ InsertText(str);
+ }
+
+ ///
+ /// Inserts the given text into the buffer at the given cursor position. Given cursor position is always
+ /// clamped to the bounds of the buffer. Clears any current selection if the actual cursor position is different from
+ /// the provided cursor position.
+ ///
+ ///
+ /// The cursor position in the buffer to inject
+ ///
+ public unsafe void InsertTextAt(sbyte* textPtrUnsafe, int cursorStart, int textLength) =>
+ InsertTextAt(textPtr: new Ptr(textPtrUnsafe), cursorStart, textLength);
+
+ ///
+ /// Inserts the given text into the buffer at the given cursor position. Given cursor position is always
+ /// clamped to the bounds of the buffer. Clears any current selection if the actual cursor position is different from
+ /// the provided cursor position.
+ ///
+ ///
+ /// The cursor position in the buffer to inject
+ public void InsertTextAt(Ptr textPtr, int cursorStart)
+ {
+ // count to end
+ const char terminator = '\0';
+ for (uint i = 0; i < int.MaxValue; ++i)
+ {
+ if (textPtr[i] == terminator)
+ {
+ InsertTextAt(textPtr, cursorStart, (int)i);
+ return;
+ }
+
+ ++i;
+ }
+ }
+
+ ///
+ /// Inserts the given text into the buffer at the given cursor position. Given cursor position is always
+ /// clamped to the bounds of the buffer. Clears any current selection if the actual cursor position is different from
+ /// the provided cursor position.
+ ///
+ ///
+ /// The cursor position in the buffer to inject
+ ///
+ public void InsertTextAt(Ptr textPtr, int cursorStart, int textLength)
+ {
+ Span textSpan = stackalloc char[textLength];
+
+ if (textPtr.TryReadToSpan(ref textSpan))
+ {
+ Debug.Assert(textSpan.Length == textLength);
+ InsertTextAt(textSpan, cursorStart);
+ }
+ else
+ {
+ Console.Error.WriteLine("Failed to read text from text editing event.");
+ // insert empty just to synchronize cursor position
+ SetSelection(cursorStart, 0);
+ }
+ }
+
+ ///
+ /// Sets the selection appropriately for the given positions. Positions can be provided in any order,
+ /// and resulting selection will be clamped to a valid range for the current buffer.
+ ///
+ ///
+ ///
+ public void SetSelectionPositions(int positionA, int positionB)
+ {
+ if (positionA > positionB)
+ {
+ SetSelection(positionB, positionA - positionB);
+ }
+ else
+ {
+ SetSelection(positionA, positionB - positionA);
+ }
+ }
+
+ ///
+ /// Set the selection to the given start and length. Position and length are clamped to the bounds of the buffer.
+ ///
+ ///
+ ///
+ public void SetSelection(int startPosition, int length)
+ {
+ SetCursorPositionRaw(startPosition);
+ SetSelectionLength(length);
+ }
+
+ ///
+ /// Moves the cursor start to the given position, and adjusts the selection length such that
+ /// the selection's end does not move
+ ///
+ ///
+ public void MoveCursorStart(int newPosition)
+ {
+ var pCursor = _cursorStart;
+ SetCursorPositionRaw(newPosition);
+ var diff = pCursor - _cursorStart;
+ SetSelectionLength(_selectionLength + diff);
+ }
+
+ ///
+ /// Adjusts the selection length based on a given end position. The end position is clamped to the bounds of the
+ /// buffer. This may modify the selection start position if the provided position is less than the current
+ /// start cursor position.
+ ///
+ ///
+ public void MoveCursorEnd(int endPosition)
+ {
+ if (endPosition < _cursorStart)
+ {
+ SetCursorPositionRaw(endPosition);
+ SetSelectionLength(0);
+ return;
+ }
+
+ var clampedEndPosition = Math.Clamp(endPosition, _cursorStart, _sb.Length);
+ SetSelectionLength(clampedEndPosition - _cursorStart);
+ }
+
+ ///
+ /// Sets the selection length to the given value. The value is clamped to the bounds of the buffer.
+ ///
+ ///
+ private void SetSelectionLength(int newLength) =>
+ _selectionLength = Math.Clamp(newLength, 0, _sb.Length - _cursorStart);
+
+ ///
+ /// Sets the value of to the given value. The value is clamped to the bounds of the buffer.
+ /// Selection length is not affected.
+ ///
+ ///
+ private void SetCursorPositionRaw(int newPosition) => _cursorStart = Math.Clamp(newPosition, 0, _sb.Length);
+
+ ///
+ /// Fills a span with current buffer contents. Will fill the given span up to the length of the contents or the
+ /// length of the span.
+ ///
+ ///
+ ///
+ public int GetCurrentBuffer(Span buffer)
+ {
+ var maxCount = Math.Min(buffer.Length, _sb.Length);
+ _sb.CopyTo(0, buffer, maxCount);
+ return maxCount;
+ }
+
+ ///
+ /// Fills a span with the currently selected text. Will fill the given span up to the length of the selection or the
+ /// length of the span.
+ ///
+ ///
+ ///
+ public int GetSelectedRegion(Span buffer)
+ {
+ var maxCount = Math.Min(buffer.Length, _selectionLength);
+ if (maxCount == 0)
+ {
+ return 0;
+ }
+
+ _sb.CopyTo(_cursorStart, buffer, maxCount);
+ return maxCount;
+ }
+
+ ///
+ /// Retrieves the current buffer contents and clears the buffer, resetting the cursor and selection positions.
+ ///
+ ///
+ public string ConsumeInput()
+ {
+ var result = _sb.ToString();
+ Clear();
+ return result;
+ }
+
+ ///
+ /// Clears the buffer and resets the cursor and selection positions.
+ ///
+ public void Clear()
+ {
+ _sb.Clear();
+ _cursorStart = 0;
+ _selectionLength = 0;
+ }
+
+
+ private int _cursorStart, _selectionLength;
+ private readonly StringBuilder _sb = new();
+}
diff --git a/sources/Input/Input/Implementations/SDL3/BackendExtensions.cs b/sources/Input/Input/Implementations/SDL3/BackendExtensions.cs
new file mode 100644
index 0000000000..25ecd6804e
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/BackendExtensions.cs
@@ -0,0 +1,62 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Text;
+using System.Runtime.CompilerServices;
+
+namespace Silk.NET.Input.SDL3;
+
+internal static unsafe class BackendExtensions
+{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static IntPtr FallbackUniqueId(this SdlInputBackend _, uint sdlDeviceId, nint uniqueId)
+ {
+ Console.Error.WriteLine("Failed to create a deterministically unique identifier for joystick");
+ return uniqueId ^ ((nint)sdlDeviceId | ((nint)sdlDeviceId << 16));
+ }
+
+ public static bool AttemptUniqueId(this SdlInputBackend sdlInputBackend, Ptr ptr, ref nint uniqueId1)
+ {
+ if (ptr.Native == null)
+ return false;
+
+ var name = ptr.ReadToString();
+ var bytes = Encoding.Default.GetBytes(name);
+ return AttemptUniqueId(sdlInputBackend, bytes, ref uniqueId1);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool AttemptUniqueId(this SdlInputBackend sdlInputBackend, T ptr, ref nint uniqueId1)
+ where T : unmanaged =>
+ AttemptUniqueId(sdlInputBackend, new ReadOnlySpan(&ptr, sizeof(T)), ref uniqueId1);
+
+ public static bool AttemptUniqueId(this SdlInputBackend sdlInputBackend, ReadOnlySpan bytes, ref nint uniqueId1)
+ {
+ uniqueId1 = Modify(uniqueId1, bytes);
+ return sdlInputBackend.DeviceRegistry.Add(uniqueId1);
+ static nint Modify(nint original, ReadOnlySpan withBytes)
+ {
+ if (sizeof(nint) == 4)
+ {
+ var hash = new HashCode();
+ foreach(var b in withBytes)
+ {
+ hash.Add(b);
+ }
+
+ var hashCode = hash.ToHashCode();
+ return original ^ *(nint*)(&hashCode);
+ }
+
+ var hash64Bytes = (byte*)&original;
+
+ for (int i = 0; i < withBytes.Length; i += 8)
+ {
+ hash64Bytes[i % 8] ^= withBytes[i];
+ }
+
+ return *(nint*)hash64Bytes;
+ }
+
+ }
+}
diff --git a/sources/Input/Input/Implementations/SDL3/Devices/ISdlDevice.cs b/sources/Input/Input/Implementations/SDL3/Devices/ISdlDevice.cs
new file mode 100644
index 0000000000..f9a38697d5
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/Devices/ISdlDevice.cs
@@ -0,0 +1,14 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Silk.NET.Input.SDL3;
+
+///
+/// An interface defining a generic constructor for managed SDL devices.
+///
+///
+internal interface ISdlDevice : IInputDevice where T : SdlDevice
+{
+ public static abstract T? CreateDevice(uint sdlDeviceId, SdlInputBackend backend);
+
+}
diff --git a/sources/Input/Input/Implementations/SDL3/Devices/Joysticks/IOrderedDevice.cs b/sources/Input/Input/Implementations/SDL3/Devices/Joysticks/IOrderedDevice.cs
new file mode 100644
index 0000000000..07d5598a38
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/Devices/Joysticks/IOrderedDevice.cs
@@ -0,0 +1,13 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Silk.NET.Input.SDL3.Joysticks;
+
+///
+/// For devices such as gamepads and joysticks, their SDL IDs are likely to change when other devices
+/// are removed.
+///
+internal interface IOrderedDevice
+{
+ public void RefreshSdlId();
+}
diff --git a/sources/Input/Input/Implementations/SDL3/Devices/Joysticks/ISdlJoystick.cs b/sources/Input/Input/Implementations/SDL3/Devices/Joysticks/ISdlJoystick.cs
new file mode 100644
index 0000000000..3a1e81b181
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/Devices/Joysticks/ISdlJoystick.cs
@@ -0,0 +1,37 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Silk.NET.SDL;
+
+namespace Silk.NET.Input.SDL3.Joysticks;
+
+///
+/// An interface for implementing different joystick types
+///
+/// Currently, only Gamepad is explicitly supported, however this interface leaves room
+/// for extensions such as those seen in .
+///
+internal interface ISdlJoystick : IOrderedDevice
+{
+ public SdlJoystick Joystick { get; }
+ ///
+ /// Raw joystick axis input events are forwarded here
+ ///
+ /// Input axis (which axis)
+ /// Input axis value
+ public void UpdateFromJoyAxis(int axis, short joystickInput);
+
+ ///
+ /// Raw joystick hat input events are forwarded here
+ ///
+ /// Input hat (which hat)
+ /// Input hat value
+ public void UpdateFromJoyHat(int hatIdx, SdlJoystick.HatState hatState);
+
+ ///
+ /// Raw joystick button input events are forwarded here
+ ///
+ /// Input button (which button)
+ /// Button state
+ public void UpdateFromJoyButton(int buttonIdx, bool down);
+}
diff --git a/sources/Input/Input/Implementations/SDL3/Devices/Joysticks/SdlGamepad.cs b/sources/Input/Input/Implementations/SDL3/Devices/Joysticks/SdlGamepad.cs
new file mode 100644
index 0000000000..bc279c79d6
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/Devices/Joysticks/SdlGamepad.cs
@@ -0,0 +1,368 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using Silk.NET.SDL;
+
+namespace Silk.NET.Input.SDL3.Joysticks;
+
+///
+/// provides the IGamepad implementation for a joystick
+///
+internal sealed unsafe class SdlGamepad : SdlDevice, IGamepad, ISdlDevice, ISdlJoystick, IJoystick
+{
+ private readonly GamepadHandle _gamepadHandle;
+
+ public SdlJoystick Joystick { get; }
+
+
+ // todo - do we want this to be an actual unique device? or should it have the same "unique id" as the joystick?
+ private SdlGamepad(SdlJoystick joystick, nint uniqueId) : base(joystick.Backend, uniqueId, joystick.SdlDeviceId)
+ {
+ Joystick = joystick;
+
+ var joystickHandle = joystick.JoystickHandle;
+ var gamepadHandle = *(GamepadHandle*)&joystickHandle; //NativeBackend.OpenGamepad(sdlDeviceId);
+ _gamepadHandle = gamepadHandle;
+ Remap(gamepadHandle);
+
+ GamepadState = new GamepadState(joystick.RawButtonState, joystick.RawAxisState);
+ Joystick.AddDeviceMapping(this);
+ }
+
+ private void Remap(GamepadHandle gamepadHandle)
+ {
+ _bindings.Clear();
+ _outputBindings.Clear();
+ _hatBindings.Clear();
+ var bindingsCount = 0;
+ var mappings = NativeBackend.GetGamepadBindings(gamepadHandle, &bindingsCount);
+
+ if (bindingsCount == 0)
+ {
+ if (mappings == null)
+ {
+ var error = NativeBackend.GetError();
+ if (error.Native != null)
+ {
+ Console.Error.WriteLine(error.ReadToString());
+ NativeBackend.Free(error.Native);
+ }
+ }
+ else
+ {
+ NativeBackend.Free(mappings);
+ }
+
+ Console.Error.WriteLine("No gamepad mappings found.");
+ return;
+ }
+
+ for (var i = 0; i < bindingsCount; i++)
+ {
+ var binding = mappings[i];
+
+ if (binding->OutputType == GamepadBindingType.None)
+ {
+ continue;
+ }
+
+ int? id = null;
+
+ switch (binding->InputType)
+ {
+ case GamepadBindingType.Button:
+ id = binding->Input.Button << _buttonShift;
+ break;
+ case GamepadBindingType.Axis:
+ id = binding->Input.Axis.Axis << _axisShift;
+ break;
+ case GamepadBindingType.Hat:
+ id = binding->Input.Hat.Hat;
+ break;
+ }
+
+ if (id == null)
+ {
+ continue;
+ }
+
+ switch (binding->OutputType)
+ {
+ case GamepadBindingType.Axis:
+ case GamepadBindingType.Button:
+ _outputBindings.Add(*binding);
+ break;
+ }
+
+ if (binding->InputType == GamepadBindingType.Hat)
+ {
+ while (_hatBindings.Count <= id.Value)
+ {
+ _hatBindings.Add(null);
+ }
+
+ _hatBindings[id.Value] ??= [];
+ _hatBindings[id.Value]!.Add(*binding);
+ }
+ else
+ {
+ _bindings.Add(id.Value, *binding);
+ }
+ }
+
+ NativeBackend.Free(mappings);
+ }
+
+ public void Remap() => Remap(_gamepadHandle);
+
+ public override uint SdlDeviceId => _sdlDeviceId;
+ private uint _sdlDeviceId;
+
+ public void RefreshSdlId() => _sdlDeviceId = NativeBackend.GetGamepadID(_gamepadHandle);
+
+ public override string Name => Joystick.Name;
+
+ protected override void Release()
+ {
+ Joystick.RemoveDeviceMapping(this);
+
+ // todo: does this close the joystick as well?
+ NativeBackend.CloseGamepad(_gamepadHandle);
+ }
+
+ #region IGamepad
+
+ GamepadState IGamepad.State => GamepadState;
+ private GamepadState GamepadState { get; }
+
+ public IReadOnlyList VibrationMotors => _rumbler ??= SdlRumble.Create(_gamepadHandle.Handle, NativeBackend, 2);
+ private SdlRumble? _rumbler;
+
+
+ #endregion
+
+ public static SdlGamepad? CreateDevice(uint sdlDeviceId, SdlInputBackend backend)
+ {
+ if (!backend.TryGetOrCreateDevice(sdlDeviceId, out var joystick))
+ {
+ return null;
+ }
+
+ var joystickUniqueId = joystick.Id;
+ var gpn = backend.Sdl.GetRealGamepadTypeForID(sdlDeviceId);
+
+ if (backend.AttemptUniqueId(gpn, ref joystickUniqueId))
+ {
+ return new SdlGamepad(joystick, uniqueId: joystickUniqueId);
+ }
+
+ // manipulate the joystick id to make a unique gamepad id
+ var guid = backend.Sdl.GetGamepadGuidForID(sdlDeviceId);
+ if (backend.AttemptUniqueId(guid, ref joystickUniqueId))
+ {
+ return new SdlGamepad(joystick, uniqueId: joystickUniqueId);
+ }
+
+ joystickUniqueId = backend.FallbackUniqueId(sdlDeviceId, joystickUniqueId);
+ return new SdlGamepad(joystick, uniqueId: joystickUniqueId);
+ }
+
+ private void UpdateGamepadAxis(GamepadAxis axis, int value, int min, int max)
+ {
+ var mappedValue = (float)(value + min) / (max - min);
+ switch (axis)
+ {
+ case GamepadAxis.Invalid:
+ return;
+ case GamepadAxis.Leftx:
+ {
+ Joystick.UpdateRawAxisState(JoystickAxis.LeftX, mappedValue);
+
+ var split = SdlJoystick.SplitValue(mappedValue);
+ Joystick.UpdateRawAxisState(JoystickAxis.MinusLeftX, split.minus);
+ Joystick.UpdateRawAxisState(JoystickAxis.PlusLeftX, split.plus);
+ break;
+ }
+ case GamepadAxis.Lefty:
+ {
+ Joystick.UpdateRawAxisState(JoystickAxis.LeftY, mappedValue);
+
+ var split = SdlJoystick.SplitValue(mappedValue);
+ Joystick.UpdateRawAxisState(JoystickAxis.MinusLeftY, split.minus);
+ Joystick.UpdateRawAxisState(JoystickAxis.PlusLeftY, split.plus);
+ break;
+ }
+ case GamepadAxis.Rightx:
+ {
+ Joystick.UpdateRawAxisState(JoystickAxis.RightX, mappedValue);
+
+ var split = SdlJoystick.SplitValue(mappedValue);
+ Joystick.UpdateRawAxisState(JoystickAxis.MinusRightX, split.minus);
+ Joystick.UpdateRawAxisState(JoystickAxis.PlusRightX, split.plus);
+ break;
+ }
+ case GamepadAxis.Righty:
+ {
+ Joystick.UpdateRawAxisState(JoystickAxis.RightY, mappedValue);
+
+ var split = SdlJoystick.SplitValue(mappedValue);
+ Joystick.UpdateRawAxisState(JoystickAxis.MinusRightY, split.minus);
+ Joystick.UpdateRawAxisState(JoystickAxis.PlusRightY, split.plus);
+ break;
+ }
+ case GamepadAxis.LeftTrigger:
+ {
+ Joystick.UpdateRawAxisState(JoystickAxis.LeftTrigger, mappedValue);
+ break;
+ }
+ case GamepadAxis.RightTrigger:
+ {
+ Joystick.UpdateRawAxisState(JoystickAxis.RightTrigger, mappedValue);
+ break;
+ }
+ default:
+ throw new ArgumentOutOfRangeException(nameof(axis), axis, null);
+ }
+ }
+
+ #region ISdlJoystick
+
+ public void UpdateFromJoyButton(int buttonIdx, bool down)
+ {
+ if (!_bindings.TryGetValue(buttonIdx << _buttonShift, out var binding))
+ {
+ return;
+ }
+
+ Debug.Assert(binding.InputType == GamepadBindingType.Button && binding.Input.Button == buttonIdx);
+ var bindingType = binding.OutputType;
+ var output = &binding.Output;
+ switch (bindingType)
+ {
+ case GamepadBindingType.Axis:
+ var axis = output->Axis;
+ UpdateGamepadAxis(
+ axis: axis.Axis,
+ value: down ? axis.AxisMax : axis.AxisMin,
+ min: axis.AxisMin,
+ max: axis.AxisMax);
+ break;
+
+ case GamepadBindingType.Button:
+ UpdateButtonBinding(output->Button, down);
+ break;
+ }
+ }
+
+ public void AddButtonEvent(byte sdlButtonId, byte sdlButtonDown) =>
+ UpdateButtonBinding((GamepadButton)sdlButtonId, sdlButtonDown > 0);
+
+ public void AddAxisEvent(byte evtAxis, short evtValue) =>
+ UpdateGamepadAxis((GamepadAxis)evtAxis, evtValue, Sdl.JoystickAxisMin, Sdl.JoystickAxisMax);
+
+ public void UpdateFromJoyAxis(int axis, short joystickInput)
+ {
+ if (!_bindings.TryGetValue(axis << _axisShift, out var binding))
+ {
+ return;
+ }
+
+ Debug.Assert(binding.InputType == GamepadBindingType.Axis);
+
+ var output = &binding.Output;
+ var input = &binding.Input.Axis;
+
+ switch (binding.OutputType)
+ {
+ case GamepadBindingType.Axis:
+ UpdateGamepadAxis(output->Axis.Axis, joystickInput, input->AxisMin, input->AxisMax);
+ break;
+ case GamepadBindingType.Button:
+ UpdateButtonBinding(output->Button, joystickInput >= input->AxisMin && joystickInput <= input->AxisMax);
+ break;
+ }
+ }
+
+ public void UpdateFromJoyHat(int hatIdx, SdlJoystick.HatState hatState)
+ {
+ if (_hatBindings.Count <= hatIdx)
+ {
+ return;
+ }
+
+ var bindings = _hatBindings[index: hatIdx];
+ if (bindings is not { Count: > 0 })
+ {
+ return;
+ }
+
+ foreach (var binding in bindings)
+ {
+ Debug.Assert(condition: binding.InputType == GamepadBindingType.Hat && binding.Input.Hat.Hat == hatIdx);
+ var input = &binding.Input.Hat;
+ var mask = (SdlJoystick.HatState)input->HatMask;
+ var bindingState = hatState & mask;
+ switch (binding.OutputType)
+ {
+ case GamepadBindingType.Axis:
+ var axis = binding.Output.Axis;
+ UpdateGamepadAxis(
+ axis: axis.Axis,
+ value: bindingState == SdlJoystick.HatState.Centered ? axis.AxisMin : axis.AxisMax,
+ min: axis.AxisMin,
+ max: axis.AxisMax);
+ break;
+ case GamepadBindingType.Button:
+ var button = binding.Output.Button;
+ UpdateButtonBinding(button, bindingState != SdlJoystick.HatState.Centered);
+ break;
+ }
+ }
+ }
+
+ #endregion
+
+ private void UpdateButtonBinding(GamepadButton button, bool value)
+ {
+ var asJoystickButton = AsJoystickButton(button);
+ Joystick.UpdateRawButtonState(asJoystickButton, value, value ? 1 : 0);
+ return;
+
+ static JoystickButton AsJoystickButton(GamepadButton buttonIndex) =>
+ buttonIndex switch {
+ GamepadButton.South => JoystickButton.ButtonDown,
+ GamepadButton.East => JoystickButton.ButtonRight,
+ GamepadButton.West => JoystickButton.ButtonLeft,
+ GamepadButton.North => JoystickButton.ButtonUp,
+ GamepadButton.Back => JoystickButton.Back,
+ GamepadButton.Guide => JoystickButton.Home,
+ GamepadButton.Start => JoystickButton.Start,
+ GamepadButton.LeftStick => JoystickButton.LeftStick,
+ GamepadButton.RightStick => JoystickButton.RightStick,
+ GamepadButton.LeftShoulder => JoystickButton.LeftBumper,
+ GamepadButton.RightShoulder => JoystickButton.RightBumper,
+ GamepadButton.DpadUp => JoystickButton.DPadUp,
+ GamepadButton.DpadDown => JoystickButton.DPadDown,
+ GamepadButton.DpadLeft => JoystickButton.DPadLeft,
+ GamepadButton.DpadRight => JoystickButton.DPadRight,
+ // TODO not exposed today
+ _ => (JoystickButton)buttonIndex
+ };
+ }
+
+ private readonly Dictionary _bindings = new();
+ private readonly List?> _hatBindings = [];
+ private readonly List _outputBindings = [];
+
+
+ // SDL indexes the 3 of these separately, but it is more convenient
+ // for us to index buttons/hats/axes as a single list.
+ // Since SDL only uses a single byte for a device index,
+ // we can safely use an integer key with a bit shift like this.
+ private const int _buttonShift = 0;
+ private const int _axisShift = 8;
+
+ JoystickState IJoystick.State => Joystick.State;
+ ButtonReadOnlyList IButtonDevice.State => GamepadState.Buttons;
+}
diff --git a/sources/Input/Input/Implementations/SDL3/Devices/Joysticks/SdlJoystick.Extended.cs b/sources/Input/Input/Implementations/SDL3/Devices/Joysticks/SdlJoystick.Extended.cs
new file mode 100644
index 0000000000..146dc37f0d
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/Devices/Joysticks/SdlJoystick.Extended.cs
@@ -0,0 +1,56 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Numerics;
+
+namespace Silk.NET.Input.SDL3.Joysticks;
+
+// This partial class contains the logic for handling joystick-based device types such as SdlGamepad.
+internal sealed partial class SdlJoystick
+{
+ public bool TryGetDevice([NotNullWhen(true)] out T? device) where T : class, ISdlJoystick
+ {
+ foreach (var d in _devices)
+ {
+ if (d is T typedDevice)
+ {
+ device = typedDevice;
+ return true;
+ }
+ }
+
+ device = null;
+ return false;
+ }
+
+ internal IReadOnlyList RawHatState => _rawHatState;
+ internal IReadOnlyList> RawButtonState => _rawButtonState;
+ internal IReadOnlyList RawAxisState => _rawAxisState;
+ internal void AddDeviceMapping(ISdlJoystick device) => _devices.Add(device);
+ internal void RemoveDeviceMapping(ISdlJoystick device) => _devices.Remove(device);
+
+ internal void UpdateRawButtonState(JoystickButton button, bool isDown, float pressure)
+ {
+ var idx = button.Index();
+ if (idx < 0)
+ {
+ throw new Exception("Received an invalid SDL button??");
+ }
+
+ _rawButtonState[idx] = new Button(button, isDown, pressure);
+ }
+
+ internal void UpdateRawAxisState(JoystickAxis axis, float value)
+ {
+ var index = axis.Index();
+ if (index < 0)
+ {
+ throw new Exception("Received an invalid SDL axis??");
+ }
+
+ _rawAxisState[axis.Index()] = value;
+ }
+
+ private readonly List _devices = [];
+}
diff --git a/sources/Input/Input/Implementations/SDL3/Devices/Joysticks/SdlJoystick.cs b/sources/Input/Input/Implementations/SDL3/Devices/Joysticks/SdlJoystick.cs
new file mode 100644
index 0000000000..e0676310bf
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/Devices/Joysticks/SdlJoystick.cs
@@ -0,0 +1,189 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Numerics;
+using Silk.NET.SDL;
+
+namespace Silk.NET.Input.SDL3.Joysticks;
+
+internal sealed unsafe partial class SdlJoystick : SdlDevice, IJoystick, ISdlDevice, IOrderedDevice
+{
+ public JoystickState State { get; }
+ internal readonly JoystickType JoystickType;
+ internal JoystickHandle JoystickHandle { get; }
+
+ public static SdlJoystick CreateDevice(uint sdlDeviceId, SdlInputBackend backend)
+ {
+ nint uniqueId = 0;
+
+ var guid = backend.Sdl.GetJoystickGuidForID(sdlDeviceId);
+ if (backend.AttemptUniqueId(new ReadOnlySpan(&guid, 16), ref uniqueId))
+ {
+ return new SdlJoystick(sdlDeviceId, uniqueId, backend);
+ }
+
+ var pathPtr = backend.Sdl.GetJoystickPathForID(sdlDeviceId);
+ if (backend.AttemptUniqueId(pathPtr, ref uniqueId))
+ {
+ return new SdlJoystick(sdlDeviceId, uniqueId, backend);
+ }
+
+ var name = backend.Sdl.GetJoystickNameForID(sdlDeviceId);
+ if (backend.AttemptUniqueId(name, ref uniqueId))
+ {
+ return new SdlJoystick(sdlDeviceId, uniqueId, backend);
+ }
+
+ var type = backend.Sdl.GetJoystickTypeForID(sdlDeviceId);
+ if (backend.AttemptUniqueId(type, ref uniqueId))
+ {
+ return new SdlJoystick(sdlDeviceId, uniqueId, backend);
+ }
+
+ uniqueId = backend.FallbackUniqueId(sdlDeviceId, uniqueId);
+ return new SdlJoystick(sdlDeviceId, uniqueId, backend);
+ }
+
+
+ public override string Name => NativeBackend.GetJoystickNameForID(SdlDeviceId).ReadToString();
+
+ public override uint SdlDeviceId => _sdlDeviceId;
+
+
+
+ private SdlJoystick(uint sdlDeviceId, nint uniqueId, SdlInputBackend backend) : base(backend, uniqueId, sdlDeviceId)
+ {
+ var joystickHandle = NativeBackend.OpenJoystick(sdlDeviceId);
+ _sdlDeviceId = sdlDeviceId;
+
+ if (joystickHandle.Handle == null)
+ {
+ var error = NativeBackend.GetError();
+ string? errorStr = null;
+ if (error.Native != null)
+ {
+ errorStr = error.ReadToString();
+ NativeBackend.Free(error.Native);
+ }
+
+ throw new Exception($"Failed to open joystick: {errorStr ?? "Unknown error."}");
+ }
+
+ JoystickHandle = joystickHandle;
+ JoystickType = NativeBackend.GetJoystickType(joystickHandle);
+
+
+ // init current joystick state
+ var buttonCount = NativeBackend.GetNumJoystickButtons(joystickHandle);
+ for (byte i = 0; i < buttonCount; i++)
+ {
+ var joystickInput = NativeBackend.GetJoystickButtonRaw(JoystickHandle, i);
+ AddButtonEvent(i, joystickInput);
+ }
+
+ var axisCount = NativeBackend.GetNumJoystickAxes(joystickHandle);
+ for (var i = 0; i < axisCount; i++)
+ {
+ var joystickInput = NativeBackend.GetJoystickAxis(JoystickHandle, i);
+ if (joystickInput == 0)
+ {
+ // this indicates an sdl error, so just set our internal axis to 0
+ joystickInput = short.MinValue;
+ }
+
+ AddAxisEvent(i, joystickInput);
+ }
+
+ var hatCount = NativeBackend.GetNumJoystickHats(joystickHandle);
+ for (var i = 0; i < hatCount; ++i)
+ {
+ var hatInput = NativeBackend.GetJoystickHat(joystickHandle, i);
+ AddHatEvent(i, hatInput);
+ }
+
+ _rawAxisState = new float[EnumInfo.UniqueValues.Count + axisCount];
+ _rawButtonState = new Button[EnumInfo.UniqueValues.Count + buttonCount];
+
+ State = new JoystickState(_rawAxisState, _rawButtonState, _rawHatState);
+ }
+
+
+ [Flags]
+ internal enum HatState : byte
+ {
+ Up = (byte)Sdl.HatUp,
+ Right = (byte)Sdl.HatRight,
+ Down = (byte)Sdl.HatDown,
+ Left = (byte)Sdl.HatLeft,
+ Centered = (byte)Sdl.HatCentered,
+ LeftUp = (byte)Sdl.HatLeftup,
+ RightUp = (byte)Sdl.HatRightup,
+ LeftDown = (byte)Sdl.HatLeftdown,
+ RightDown = (byte)Sdl.HatRightdown
+ }
+
+ #region Sdl Events
+
+ public void AddHatEvent(int hatIdx, byte hatInput)
+ {
+ var hatState = (HatState)hatInput;
+ var left = (hatState & HatState.Left) == HatState.Left;
+ var right = (hatState & HatState.Right) == HatState.Right;
+
+ var x = (float)(*(byte*)&right - *(byte*)&left);
+ var up = (hatState & HatState.Up) == HatState.Up;
+ var down = (hatState & HatState.Down) == HatState.Down;
+ var y = (float)(*(byte*)&up - *(byte*)&down);
+
+ _rawHatState[hatIdx] = new Vector2(x, y);
+
+ foreach(var device in _devices)
+ {
+ device.UpdateFromJoyHat(hatIdx, hatState);
+ }
+ }
+
+ public void AddAxisEvent(int axis, short joystickInput)
+ {
+ _rawAxisState[axis] = (float)(joystickInput + short.MaxValue) / ushort.MaxValue;
+ foreach (var device in _devices)
+ {
+ device.UpdateFromJoyAxis(axis, joystickInput);
+ }
+ }
+
+ public void AddButtonEvent(byte sdlButtonId, byte sdlButtonDown)
+ {
+ var down = sdlButtonDown > 0;
+ _rawButtonState[sdlButtonId] = new Button((JoystickButton)sdlButtonId, down, down ? 1 : 0);
+ foreach (var device in _devices)
+ {
+ device.UpdateFromJoyButton(sdlButtonId, down);
+ }
+ }
+
+ #endregion
+
+ internal static (float minus, float plus) SplitValue(float mappedValue)
+ {
+ mappedValue = (float)((mappedValue - 0.5d) * 2d);
+ return mappedValue > 0 ? (0, mappedValue) : (mappedValue, 0);
+ }
+
+
+ protected override void Release() => NativeBackend.CloseJoystick(JoystickHandle);
+
+ public void RefreshSdlId() => _sdlDeviceId = NativeBackend.GetJoystickID(JoystickHandle);
+ private uint _sdlDeviceId;
+
+ // State
+ private readonly Button[] _rawButtonState;
+ private readonly float[] _rawAxisState;
+ private readonly Vector2[] _rawHatState = [];
+
+ // Constants
+ internal const short DigitalThreshold = short.MaxValue / 8;
+
+ ButtonReadOnlyList IButtonDevice.State => State.Buttons;
+
+}
diff --git a/sources/Input/Input/Implementations/SDL3/Devices/Joysticks/SdlRumble.cs b/sources/Input/Input/Implementations/SDL3/Devices/Joysticks/SdlRumble.cs
new file mode 100644
index 0000000000..ed99d82212
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/Devices/Joysticks/SdlRumble.cs
@@ -0,0 +1,136 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections;
+using System.Runtime.CompilerServices;
+using Silk.NET.SDL;
+
+namespace Silk.NET.Input.SDL3.Joysticks;
+
+internal unsafe class SdlRumble : IReadOnlyList
+{
+ public IMotor this[int index] => _motors[index];
+ public int Count => _motors.Length;
+
+ public static SdlRumble Create(void* handle, ISdl sdl, int count) where T : unmanaged
+ {
+ SetRumbleDelegate setRumble;
+ if (typeof(T) == typeof(GamepadHandle))
+ {
+ setRumble = SetGamepadRumble;
+ }
+ else if (typeof(T) == typeof(JoystickHandle))
+ {
+ setRumble = SetJoystickRumble;
+ }
+ else
+ {
+ throw new InvalidOperationException("Invalid device type");
+ }
+
+ return new SdlRumble(handle, sdl, count, setRumble);
+ }
+
+ private SdlRumble(void* handle, ISdl nativeBackend, int count, SetRumbleDelegate setRumble)
+ {
+ _setRumble = setRumble;
+ _handle = handle;
+ _motors = new IMotor[count];
+ _motorFrequencies = new ushort[count];
+ _nativeBackend = nativeBackend;
+ CreateMotors(_motors);
+ }
+
+ private void CreateMotors(IMotor[] motors)
+ {
+ for (var i = 0; i < motors.Length; i++)
+ {
+ motors[i] = new Motor(this, i);
+ }
+ }
+
+ IEnumerator IEnumerable.GetEnumerator() => (IEnumerator)_motors.GetEnumerator();
+ IEnumerator IEnumerable.GetEnumerator() => _motors.GetEnumerator();
+
+ private float GetRumble01(int motor) => _motorFrequencies[motor] * _toFloat;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void SetRumble01(int motor, float value) =>
+ SetRumble01(motor, (ushort)(value * ushort.MaxValue), _motorFrequencies);
+
+ private void SetRumble01(int motor, ushort value, ushort[] motorFrequencies)
+ {
+ // todo - use Haptics API instead?
+ // todo - dispatch this to the correct input thread
+
+ // TODO this entire API needs to be redesigned as right now this is literally only ever going to be useful if it's
+ // just left or right. The original intention was that this would be useful for things like 3D haptics, but what did
+ // I know. The SDL people seem to have done a good job with their haptic API, let's see what we can do with it.
+ // For now, this has the same implementation as it always has.
+ var valueShort = value;
+ motorFrequencies[motor] = valueShort;
+ var left = motorFrequencies[0];
+ var right = motorFrequencies[1];
+ _setRumble(_nativeBackend, _handle, left, right);
+ }
+
+ private static void SetJoystickRumble(ISdl backend, void* handle, ushort left, ushort right)
+ {
+ var average = (ushort)((left + right) >> 2);
+ var joystickHandle = *(JoystickHandle*)&handle;
+ if (!backend.RumbleJoystick(joystickHandle, average, average, _durationMs))
+ {
+ backend.ThrowError();
+ }
+
+ if (!backend.RumbleJoystickTriggers(joystickHandle, left, right, _durationMs))
+ {
+ backend.ThrowError();
+ }
+ }
+
+ private static void SetGamepadRumble(ISdl backend, void* handle, ushort left, ushort right)
+ {
+ var average = (ushort)((left + right) >> 2);
+ var gamepadHandle = *(GamepadHandle*)&handle;
+ if (!backend.RumbleGamepad(gamepadHandle, average, average, _durationMs))
+ {
+ backend.ThrowError();
+ }
+
+ if (!backend.RumbleGamepadTriggers(gamepadHandle, left, right, _durationMs))
+ {
+ backend.ThrowError();
+ }
+ }
+
+ private readonly SetRumbleDelegate _setRumble;
+ private readonly void* _handle;
+ private readonly IMotor[] _motors;
+ private readonly ushort[] _motorFrequencies;
+ private readonly ISdl _nativeBackend;
+ private const float _toFloat = 1f / ushort.MaxValue;
+ private const uint _durationMs = uint.MaxValue;
+
+
+ private delegate void SetRumbleDelegate(ISdl nativeBackend, void* handle, ushort left, ushort right);
+
+ private class Motor : IMotor
+ {
+ private readonly int _freqIndex;
+ private readonly SdlRumble _rumbler;
+
+ public Motor(SdlRumble rumbler, int freqIdx)
+ {
+ _freqIndex = freqIdx;
+ _rumbler = rumbler;
+ }
+
+ public float Speed
+ {
+ get => _rumbler.GetRumble01(_freqIndex);
+ set => _rumbler.SetRumble01(_freqIndex, value);
+ }
+ }
+}
+
diff --git a/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlBoundedPointerDevice.cs b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlBoundedPointerDevice.cs
new file mode 100644
index 0000000000..e101d768ae
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlBoundedPointerDevice.cs
@@ -0,0 +1,28 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace Silk.NET.Input.SDL3.Pointers;
+
+///
+/// A base class for SDL input devices that operate in terms of a window's or DWMs bounds.
+///
+internal abstract class SdlBoundedPointerDevice : SdlDevice, IPointerDevice
+{
+ protected SdlBoundedPointerDevice(SdlInputBackend backend, nint silkId,
+ uint sdlDeviceId) : base(backend, silkId, sdlDeviceId)
+ {
+ }
+
+ public abstract PointerState State { get; }
+
+ public abstract IReadOnlyList Targets { get; }
+
+ ///
+ /// Determines whether the should interpret
+ /// as being bounded points. For all devices supported by this backend, only one target is supported at a time
+ /// today.
+ ///
+ public bool IsBounded => true;
+}
diff --git a/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlBoundedPointerTarget.cs b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlBoundedPointerTarget.cs
new file mode 100644
index 0000000000..cb7e353f4f
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlBoundedPointerTarget.cs
@@ -0,0 +1,94 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Silk.NET.Maths;
+using Silk.NET.SDL;
+
+namespace Silk.NET.Input.SDL3.Pointers;
+
+internal class SdlBoundedPointerTarget(SdlInputBackend backend) : IPointerTarget
+{
+ internal SdlInputBackend Backend { get; } = backend;
+ private Box2D Bounds2D { get; set; }
+
+ public Box3D Bounds =>
+ new(new Vector3D(Bounds2D.Min, 0), new Vector3D(Bounds2D.Max, 1));
+
+ public static Box2D CalculateBounds(ISdl sdl)
+ {
+ var minX = float.PositiveInfinity;
+ var minY = float.PositiveInfinity;
+ var maxX = float.NegativeInfinity;
+ var maxY = float.NegativeInfinity;
+ var displayCount = 0;
+ var displays = sdl.GetDisplays(displayCount.AsRef());
+ if (displays == nullptr)
+ {
+ // Looks like we can't support windowed mouse input.
+ sdl.ClearError();
+ return default;
+ }
+
+ if (displayCount == 0) // ???
+ {
+ sdl.Free((Ref)displays);
+ return default;
+ }
+
+ for (var i = 0; i < displayCount; i++)
+ {
+ Rect rect = default;
+ if (!sdl.GetDisplayBounds(displays[(nuint)i], rect.AsRef()))
+ {
+ return default;
+ }
+
+ minX = float.Min(minX, rect.X);
+ minY = float.Min(minY, rect.Y);
+ maxX = float.Max(maxX, rect.X + rect.W);
+ maxY = float.Max(maxY, rect.Y + rect.H);
+ }
+
+ sdl.Free((Ref)displays);
+ if (minX <= maxX && minY <= maxY)
+ {
+ return new Box2D(minX, minY, maxX, maxY);
+ }
+
+ return default;
+ }
+
+ public int GetPointCount(IPointerDevice pointer)
+ {
+ if (pointer is not SdlBoundedPointerDevice { IsBounded: true } device)
+ {
+ return 0;
+ }
+
+ if (device.Backend == Backend)
+ {
+ return Bounds != default ? device.BoundedPoints.List.Count : 0;
+ }
+
+ return device.Backend.BoundedPointerTarget.GetPointCount(pointer);
+ }
+
+ public TargetPoint GetPoint(IPointerDevice pointer, int point)
+ {
+ if (
+ pointer is not SdlBoundedPointerDevice { IsBounded: true } device
+ || point < 0
+ || point >= device.BoundedPoints.List.Count
+ )
+ {
+ return default;
+ }
+
+ if (device.Backend != Backend)
+ {
+ return device.Backend.BoundedPointerTarget.GetPoint(pointer, point);
+ }
+
+ return Bounds != default ? device.BoundedPoints.List[point] : default;
+ }
+}
diff --git a/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlPen.cs b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlPen.cs
new file mode 100644
index 0000000000..acbacce0ea
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlPen.cs
@@ -0,0 +1,21 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Guid = Silk.NET.SDL.Guid;
+
+namespace Silk.NET.Input.SDL3.Pointers;
+
+internal class SdlPen : SdlBoundedPointerDevice, ISdlDevice
+{
+ public static SdlPen CreateDevice(uint sdlDeviceId, SdlInputBackend backend)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override PointerState State => throw new NotImplementedException();
+
+ public SdlPen(uint sdlDeviceId, SdlInputBackend backend, IReadOnlyList targets, InputMarshal.ListOwner boundedPoints) : base(sdlDeviceId, backend, targets, boundedPoints)
+ {
+ }
+
+}
diff --git a/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlSharedMouse.cs b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlSharedMouse.cs
new file mode 100644
index 0000000000..e5f970c910
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlSharedMouse.cs
@@ -0,0 +1,211 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Numerics;
+using Silk.NET.SDL;
+
+namespace Silk.NET.Input.SDL3.Pointers;
+
+internal class SdlSharedMouse : SdlBoundedPointerDevice, IMouse, ISdlDevice
+{
+ private readonly MouseState _state;
+
+ public unsafe SdlSharedMouse(uint sdlDeviceId, nint uniqueId, SdlInputBackend backend)
+ : base(backend, uniqueId, sdlDeviceId)
+ {
+ IPointerTarget unboundedPointerTarget = backend.UnboundedPointerTarget;
+ _state = new MouseState(new ButtonReadOnlyList(_buttons),
+ new InputReadOnlyList(_points), Vector2.Zero);
+ float x = 0, y = 0;
+ var buttonMask = NativeBackend.GetMouseState(x.AsRef(), y.AsRef());
+ var pos = new Vector2(x, y);
+
+ for (var i = 0; i < EnumInfo.UniqueValues.Count; i++)
+ {
+ var button = EnumInfo.UniqueValues[i];
+ var pressed = IsPointerButtonPressedSdl(button, buttonMask);
+ _buttons.Add(new Button(button, pressed, pressed ? 1.0f : 0.0f));
+ }
+
+ var window = NativeBackend.GetMouseFocus();
+ var pressure = _state.Buttons[PointerButton.Primary].Pressure;
+ AddTargetPoint(window, pos, pressure);
+
+ // add unbounded target
+ // var point = _unboundedPointerTarget.GetPoint(this, 0);
+ _targetListNoWindow = [unboundedPointerTarget];
+ _targetListWithWindow = [null!, unboundedPointerTarget];
+ }
+
+ private void AddTargetPoint(WindowHandle window, Vector2 pos, float pressure)
+ {
+ if (!Backend.TryGetPointerTargetForWindow(window, out var windowTarget))
+ {
+ AddUnboundedPoint(pos, pressure);
+ }
+ else
+ {
+ AddWindowPoint(pos, pressure, windowTarget);
+ }
+ }
+
+ private void AddTargetPoint(uint windowId, Vector2 pos, float pressure)
+ {
+ if (!Backend.TryGetPointerTargetForWindow(windowId, out var windowTarget))
+ {
+ AddUnboundedPoint(pos, pressure);
+ }
+ else
+ {
+ AddWindowPoint(pos, pressure, windowTarget);
+ }
+ }
+
+ private void AddUnboundedPoint(Vector2 pos, float pressure) =>
+ // add raw position (likely just 0, but that's ok for now)
+ _points.Add(
+ new TargetPoint(0, // todo: use a unique id
+ Flags: TargetPointFlags.NotPointingAtTarget,
+ Position: new Vector3(pos, 0),
+ NormalizedPosition: default,
+ Pointer: default,
+ Pressure: pressure,
+ Target: null
+ )
+ );
+
+ private void AddWindowPoint(Vector2 pos, float pressure, IPointerTarget windowTarget)
+ {
+ var bounds = windowTarget.Bounds;
+ var min = new Vector2(bounds.Min.X, bounds.Min.Y);
+ var max = new Vector2(bounds.Max.X, bounds.Max.Y);
+
+ _points.Add(
+ new TargetPoint(
+ Id: 0, // todo - use a unique id
+ Flags: TargetPointFlags.PointingAtTarget,
+ Position: new Vector3(pos, 0),
+ NormalizedPosition: new Vector3((pos - min) / (max - min), 0),
+ Pointer: default,
+ Pressure: pressure,
+ Target: windowTarget
+ ));
+ }
+
+ public static unsafe SdlSharedMouse CreateDevice(uint sdlDeviceId, SdlInputBackend backend)
+ {
+ var deviceName = backend.Sdl.GetMouseNameForID(sdlDeviceId);
+ nint uniqueId = 0;
+ if (!backend.AttemptUniqueId(deviceName, ref uniqueId))
+ {
+ uniqueId = backend.FallbackUniqueId(sdlDeviceId, uniqueId);
+ }
+
+ backend.Sdl.Free(deviceName);
+ return new SdlSharedMouse(sdlDeviceId, uniqueId, backend);
+ }
+
+ public override string Name => $"{Backend.Name}: Shared/Global Mouse";
+
+ protected override void Release()
+ {
+ }
+
+ MouseState IMouse.State => _state;
+
+ public override unsafe IReadOnlyList Targets
+ {
+ get
+ {
+ if (_mouseWindowId == 0)
+ {
+ return _targetListNoWindow;
+ }
+
+ if (!Backend.TryGetPointerTargetForWindow(_mouseWindowId, out var target))
+ {
+ return _targetListNoWindow;
+ }
+
+ _targetListWithWindow[0] = target;
+ return _targetListWithWindow;
+ }
+ }
+
+ public ICursorConfiguration Cursor => Backend;
+
+ public bool TrySetPosition(Vector2 position)
+ {
+ if (NativeBackend.WarpMouseGlobal(position.X, position.Y))
+ {
+ return true;
+ }
+
+ NativeBackend.ClearError();
+ return false;
+ }
+
+ public override PointerState State => _state;
+
+ public void AddMotion(in MouseMotionEvent evtMotion)
+ {
+ _mouseWindowId = evtMotion.WindowID;
+ var movementRelative = new Vector2(evtMotion.Xrel, evtMotion.Yrel);
+ _accumulatedMotion += movementRelative;
+
+ // add clear old point, add new point
+ _points.Clear();
+ AddTargetPoint(_mouseWindowId, _accumulatedMotion, 0);
+ }
+
+ public void AddButtonEvent(in MouseButtonEvent evtButton)
+ {
+ var button = PointerButton.Primary + evtButton.Button;
+ var idx = EnumInfo.ValueIndexOfUnnamed(button);
+ const float mult = 1 / 255f;
+ _buttons[idx] = new Button(button, evtButton.Down > 0, evtButton.Down * mult);
+ }
+
+ public void AddWheelEvent(in MouseWheelEvent evtWheel)
+ {
+ _mouseScroll[0] += evtWheel.X;
+ _mouseScroll[1] += evtWheel.Y;
+
+ var hMagnitude = MathF.Abs(_mouseScroll[0]);
+ var vMagnitude = MathF.Abs(_mouseScroll[1]);
+
+ if (hMagnitude >= 1)
+ {
+ // horizontal scroll "tick"
+ _mouseScroll.X = 0;
+ }
+
+ if (vMagnitude >= 1)
+ {
+ // vertical scroll "tick"
+ _mouseScroll.Y = 0;
+ }
+
+ // todo - actually do stuff
+ throw new NotImplementedException();
+ }
+
+ private static bool IsPointerButtonPressedSdl(PointerButton button, uint state)
+ {
+ var index = EnumInfo.ValueIndexOf(button);
+ if (index < 0 || index >= 32)
+ {
+ return false;
+ }
+
+ return (state & (1 << index)) != 0;
+ }
+
+ private uint _mouseWindowId;
+ private Vector2 _mouseScroll = default;
+ private Vector2 _accumulatedMotion = default;
+ private readonly List> _buttons = [];
+ private readonly List _points = new();
+ private readonly IPointerTarget[] _targetListNoWindow;
+ private readonly IPointerTarget[] _targetListWithWindow;
+}
diff --git a/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlTouchScreen.cs b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlTouchScreen.cs
new file mode 100644
index 0000000000..859fa76eca
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlTouchScreen.cs
@@ -0,0 +1,49 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Silk.NET.Input.SDL3.Pointers;
+
+internal class SdlTouchScreen : SdlDevice, ISdlDevice, IPointerDevice
+{
+ public static SdlTouchScreen CreateDevice(uint sdlDeviceId, SdlInputBackend backend)
+ {
+ }
+
+ public bool Equals(IInputDevice? other)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override string Name
+ {
+ get
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ protected override void Release()
+ {
+ throw new NotImplementedException();
+ }
+
+ public PointerState State
+ {
+ get
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public IReadOnlyList Targets
+ {
+ get
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public SdlTouchScreen(uint sdlDeviceId, nint uniqueId, SdlInputBackend backend) : base(sdlDeviceId, uniqueId, backend)
+ {
+ }
+}
diff --git a/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlUnboundedMouse.cs b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlUnboundedMouse.cs
new file mode 100644
index 0000000000..a48289bd19
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlUnboundedMouse.cs
@@ -0,0 +1,33 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Numerics;
+
+namespace Silk.NET.Input.SDL3.Pointers;
+
+internal class SdlUnboundedMouse : SdlDevice, IMouse, ISdlDevice
+{
+ private SdlUnboundedMouse(uint sdlDeviceId, nint silkId, SdlInputBackend backend) : base(backend, silkId, sdlDeviceId)
+ {
+ }
+
+
+ public MouseState State => throw new NotImplementedException();
+ public static SdlUnboundedMouse? CreateDevice(uint sdlDeviceId, SdlInputBackend backend)
+ {
+
+ }
+
+ public override string Name => NativeBackend.GetMouseNameForID(SdlDeviceId).ReadToString();
+
+ public ICursorConfiguration Cursor => Backend;
+
+ public bool TrySetPosition(Vector2 position) => throw new NotImplementedException();
+
+ public IReadOnlyList Targets => throw new NotImplementedException();
+
+ protected override void Release()
+ {
+ // nothing?
+ }
+}
diff --git a/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlUnboundedPointerTarget.cs b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlUnboundedPointerTarget.cs
new file mode 100644
index 0000000000..1cee560d8e
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlUnboundedPointerTarget.cs
@@ -0,0 +1,50 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Silk.NET.Maths;
+
+namespace Silk.NET.Input.SDL3.Pointers;
+
+internal class SdlUnboundedPointerTarget(SdlInputBackend backend) : IPointerTarget
+{
+ private static readonly Box3D _bounds = new(
+ float.MinValue,
+ float.MinValue,
+ float.MinValue,
+ float.MaxValue,
+ float.MaxValue,
+ float.MaxValue
+ );
+
+ public Box3D Bounds => _bounds;
+
+ public int GetPointCount(IPointerDevice pointer) => IsValidDevice(pointer) ? pointer.State.Points.Count : 0;
+
+ public TargetPoint GetPoint(IPointerDevice pointer, int pointIdx)
+ {
+ var point = pointer.State.Points[pointIdx];
+ var valid = IsValidDevice(pointer);
+ return new TargetPoint(
+ Id: point.Id, // todo : follow spec with unique ids
+ Flags: valid ? TargetPointFlags.PointingAtTarget : TargetPointFlags.NotPointingAtTarget,
+ Position: point.Position,
+ NormalizedPosition: default,
+ Pointer: new Ray3D(),
+ Pressure: point.Pressure is > 1 or < 0
+ ? point.Pressure is 0
+ ? 0
+ : 1
+ : point.Pressure,
+ Target: this
+ );
+ }
+
+ private bool IsValidDevice(IPointerDevice pointer)
+ {
+ // todo - do we really want to limit this to SDL devices? or to our specific sdl backend?
+ // i dont think so, but.... technically...
+
+ var device = pointer as SdlDevice;
+ return device is not null && device.Backend == backend && (device.Backend.Mode & CursorModes.Unbounded) != 0;
+ }
+}
diff --git a/sources/Input/Input/Implementations/SDL3/Devices/SdlDevice.cs b/sources/Input/Input/Implementations/SDL3/Devices/SdlDevice.cs
new file mode 100644
index 0000000000..acdd557ad4
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/Devices/SdlDevice.cs
@@ -0,0 +1,66 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Silk.NET.Input.SDL3.Pointers;
+using Silk.NET.SDL;
+
+namespace Silk.NET.Input.SDL3;
+
+///
+/// A base class for all SDL input devices.
+///
+internal abstract class SdlDevice : IInputDevice, IDisposable
+{
+ bool IEquatable.Equals(IInputDevice? other) =>
+ other?.GetType() == GetType()
+ && other.Id == Id
+ && other is SdlBoundedPointerDevice dev
+ && dev.NativeBackend == NativeBackend;
+
+ public nint Id { get; }
+
+ public virtual uint SdlDeviceId { get; }
+
+ public SdlInputBackend Backend { get; }
+
+ ///
+ /// For readability and refactorability - provides the SDL interface instance.
+ ///
+ protected ISdl NativeBackend => Backend.Sdl;
+
+ public abstract string Name { get; }
+
+ protected SdlDevice(SdlInputBackend backend, nint uniqueId, uint sdlDeviceId)
+ {
+ Backend = backend;
+ Id = uniqueId;
+ SdlDeviceId = sdlDeviceId;
+ }
+
+ protected abstract void Release();
+
+ public void Dispose()
+ {
+ ObjectDisposedException.ThrowIf(_isDisposed, GetType());
+ _isDisposed = true;
+ Release();
+ #if DEBUG
+ if (!Backend.DeviceRegistry.Remove(Id))
+ {
+ Console.Error.WriteLine($"Failed to remove device {Id} from registry");
+ }
+ #else
+ Backend.DeviceRegistry.Remove(Id);
+ #endif
+
+ GC.SuppressFinalize(this);
+ }
+
+ ~SdlDevice()
+ {
+ _isDisposed = true;
+ Release();
+ }
+
+ private bool _isDisposed;
+}
diff --git a/sources/Input/Input/Implementations/SDL3/Devices/SdlKeyboard.cs b/sources/Input/Input/Implementations/SDL3/Devices/SdlKeyboard.cs
new file mode 100644
index 0000000000..7e4f3f84ac
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/Devices/SdlKeyboard.cs
@@ -0,0 +1,263 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using Silk.NET.Input.KeyHandling;
+using Silk.NET.SDL;
+
+namespace Silk.NET.Input.SDL3;
+
+internal class SdlKeyboard : SdlDevice, IKeyboard, ISdlDevice
+{
+ public KeyboardState State { get; }
+ public override string Name => NativeBackend.GetKeyboardNameForID(SdlDeviceId).ReadToString();
+ public string? ClipboardText
+ {
+ get => NativeBackend.HasClipboardText() ? NativeBackend.GetClipboardText().ReadToString() : null;
+ set => NativeBackend.SetClipboardText(value);
+ }
+
+ public static SdlKeyboard CreateDevice(uint sdlDeviceId, SdlInputBackend backend)
+ {
+ var namePtr = backend.Sdl.GetKeyboardNameForID(sdlDeviceId);
+ nint uniqueId = 0;
+ if (backend.AttemptUniqueId(namePtr, ref uniqueId))
+ {
+ return new SdlKeyboard(sdlDeviceId, uniqueId, backend);
+ }
+
+ uniqueId = backend.FallbackUniqueId(sdlDeviceId, uniqueId);
+ return new SdlKeyboard(sdlDeviceId, uniqueId, backend);
+ }
+
+ private SdlKeyboard(uint sdlDeviceId, nint uniqueId, SdlInputBackend backend) : base(backend, uniqueId, sdlDeviceId)
+ {
+ _modState = NativeBackend.GetModState();
+ _keyStates = new ButtonStates();
+
+ State = new KeyboardState(
+ keys: _keyStates,
+ capsLockActive: () => (_modState & Sdl.KmodCaps) == Sdl.KmodCaps,
+ numLockActive: () => (_modState & Sdl.KmodNum) == Sdl.KmodNum);
+ }
+
+
+ protected override void Release()
+ {
+ }
+
+ public bool TryGetKeyName(KeyName key, [NotNullWhen(true)] out string? name)
+ {
+ // todo: should 'asKeyEvent' be true?
+ var sdlKey = SdlKeyConversions.KeyNameToSdl(key, NativeBackend, true, _modState);
+ var namePtr = NativeBackend.GetKeyName(sdlKey);
+ name = namePtr.ReadToString();
+ return !string.IsNullOrWhiteSpace(name);
+ }
+
+
+ // todo - there should be a backend-independent way to do this text input handling via KeyboardState?
+ public void BeginInput()
+ {
+ var sdlWindow = Backend.FocusedWindow;
+ if (sdlWindow != null && NativeBackend.StartTextInput(sdlWindow.Value))
+ {
+ BeginRecordingSdl(sdlWindow.Value);
+ }
+ else
+ {
+ _textIsRecording = TextRecorderState.RecordingNoSdl;
+ }
+ }
+
+ private void BeginRecordingSdl(WindowHandle sdlWindow)
+ {
+ _textIsRecording = TextRecorderState.RecordingSdl;
+ _textEntryWindow = sdlWindow;
+ }
+
+ public unsafe string? EndInput()
+ {
+ switch (_textIsRecording)
+ {
+ case TextRecorderState.None:
+ return null;
+ case TextRecorderState.RecordingNoSdl:
+ _textIsRecording = TextRecorderState.None;
+ break;
+ case TextRecorderState.RecordingSdl:
+ _textIsRecording = TextRecorderState.None;
+ var sdlWindow = _textEntryWindow;
+ if (sdlWindow != null)
+ {
+ NativeBackend.StopTextInput(sdlWindow.Value);
+ }
+ break;
+ }
+ _textIsRecording = TextRecorderState.None;
+ return _textRecorder?.ConsumeInput();
+ }
+
+ ///
+ /// Updates the internal modifier state.
+ ///
+ ///
+ /// This should be called every frame the keyboard is updated in .
+ /// This mod state is purely used for sdl-related calls and modifiers that are independent of key state (e.g. numlock, caps lock)
+ /// - otherwise, we handle the modifier states with our standard key handling logic
+ ///
+ public void UpdateModState() => _modState = NativeBackend.GetModState();
+
+ public void AddKeyEvent(in KeyboardEvent key)
+ {
+ var keyName = SdlKeyConversions.ScancodeToKeyName(key.Scancode); // SdlToKeyName(key.Which);
+
+ if (ButtonStates.IsDefined(keyName))
+ {
+ var isDown = key.Down != 0;
+ var button = _keyStates[keyName];
+ var stateChanged = button.IsDown != isDown;
+ _keyStates.SetKeyState(keyName, key.Down);
+
+ var shouldRecord = _textIsRecording == TextRecorderState.RecordingNoSdl
+ && ((stateChanged && isDown) || (!stateChanged && key.Repeat != 0));
+ if (shouldRecord)
+ {
+ _textRecorder ??= new TextRecorder(null);
+ _textRecorder.AddKeyStroke(keyName, this);
+ }
+ }
+ }
+
+ public unsafe void AddTextEditingEvent(in TextEditingEvent evt)
+ {
+ if (_textEntryWindow == null)
+ {
+ var windowHandle = NativeBackend.GetWindowFromID(evt.WindowID);
+ if (windowHandle.Handle != null)
+ {
+ Console.Out.WriteLine("Unexpected text editing event");
+ BeginRecordingSdl(windowHandle);
+ }
+ }
+ else if (evt.WindowID != NativeBackend.GetWindowID(_textEntryWindow.Value))
+ {
+ Console.Error.WriteLine("Received text editing event for a different window than the " +
+ "one we're recording text for.");
+ }
+
+ _textRecorder ??= new TextRecorder(null);
+
+ if (evt.Length == 0)
+ {
+ _textRecorder.SetSelection(evt.Start, 0);
+ }
+ else
+ {
+ if (evt.Text == null)
+ {
+ return;
+ }
+
+ _textRecorder.InsertTextAt(evt.Text, evt.Start, evt.Length);
+ }
+ }
+
+ public unsafe void AddTextCandidatesEvent(in TextEditingCandidatesEvent evt)
+ {
+ if (evt.SelectedCandidate == -1 || evt.NumCandidates == 0)
+ {
+ return;
+ }
+
+ Debug.Assert(evt.NumCandidates > evt.SelectedCandidate);
+
+ var candidate = new Ptr(evt.Candidates[evt.SelectedCandidate]);
+ var str = candidate.ReadToString();
+ _textRecorder ??= new TextRecorder(null);
+ _textRecorder.InsertText(str);
+ }
+
+ public unsafe void AddTextInputEvent(in TextInputEvent evt)
+ {
+ if (_textEntryWindow == null)
+ {
+ var windowHandle = NativeBackend.GetWindowFromID(evt.WindowID);
+ if (windowHandle.Handle != null)
+ {
+ Console.Out.WriteLine("Unexpected text input event");
+ BeginRecordingSdl(windowHandle);
+ }
+ }
+ else if (evt.WindowID != NativeBackend.GetWindowID(_textEntryWindow.Value))
+ {
+ Console.Error.WriteLine("Received text input event for a different window than the " +
+ "one we're recording text for.");
+ }
+
+
+ var str = evt.Text == null ? "" : new Ptr(evt.Text).ReadToString();
+
+ _textRecorder ??= new TextRecorder(null);
+ _textRecorder.InsertText(str);
+ }
+
+
+ private WindowHandle? _textEntryWindow;
+ private TextRecorder? _textRecorder;
+ private enum TextRecorderState {None, RecordingNoSdl, RecordingSdl}
+ private TextRecorderState _textIsRecording;
+ private ushort _modState;
+ private const float _pressureMultiplier = 1f / 255f;
+ private readonly ButtonStates _keyStates;
+
+ private class ButtonStates : IReadOnlyList>
+ {
+ private static readonly int _keyCount;
+ private readonly byte[] _keyPressures = new byte[_keyCount];
+ private static readonly int[] _indices;
+
+ static ButtonStates()
+ {
+ _indices = new int[512];
+ for (var i = 0; i < 512; i++)
+ {
+ _indices[i] = Enum.IsDefined((KeyName)i) ? _keyCount++ : -1;
+ }
+ }
+
+ public int SetKeyState(KeyName key, byte pressure) => _keyPressures[_indices[(int)key]] = pressure;
+
+ public IEnumerator> GetEnumerator()
+ {
+ for (var i = 0; i < _keyCount; i++)
+ {
+ var index = _indices[i];
+ if(index != -1)
+ {
+ yield return GetButton(index);
+ }
+ }
+ }
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ public int Count => _keyCount;
+
+ public Button this[int index] => GetButton(index);
+ public Button this[KeyName key] => GetButton((int)key);
+
+ private Button GetButton(KeyName key) => GetButton((int)key);
+ private Button GetButton(int key)
+ {
+ var keyIdx = _indices[key];
+ return CreateButton((KeyName)key, _keyPressures[keyIdx]);
+ }
+
+ private Button CreateButton(KeyName key, byte pressure) => new(key, pressure > 0, pressure * _pressureMultiplier);
+
+ public static bool IsDefined(KeyName keyName) => _indices[(int)keyName] >= 0;
+ }
+}
diff --git a/sources/Input/Input/Implementations/SDL3/InputWindowExtensions.cs b/sources/Input/Input/Implementations/SDL3/InputWindowExtensions.cs
new file mode 100644
index 0000000000..bd515dc85e
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/InputWindowExtensions.cs
@@ -0,0 +1,28 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+// ReSharper disable CheckNamespace
+
+using Silk.NET.Input.SDL3;
+using Silk.NET.SDL;
+
+namespace Silk.NET.Input;
+
+///
+/// Contains extensions for creating input backends and contexts from s.
+///
+public static partial class InputWindowExtensions
+{
+ public static partial IInputBackend CreateInputBackend(this INativeWindow window)
+ {
+ if (!window.TryGetPlatformInfo(out SdlPlatformInfo info))
+ {
+ throw new ArgumentException(
+ "When using the Silk.NET.Input reference implementation, a native window compatible with that "
+ + "implementation (such as those sourced from Silk.NET.Windowing) must be used."
+ );
+ }
+
+ return new SdlInputBackend(info);
+ }
+}
diff --git a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs
new file mode 100644
index 0000000000..170278129f
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs
@@ -0,0 +1,591 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using Silk.NET.Input.SDL3.Joysticks;
+using Silk.NET.Input.SDL3.Pointers;
+using Silk.NET.Maths;
+using Silk.NET.SDL;
+
+namespace Silk.NET.Input.SDL3;
+
+internal class SdlInputBackend : IInputBackend, ICursorConfiguration
+{
+ private static readonly double _ticksPerNanosecond = Stopwatch.Frequency / 10e9d;
+
+ private bool _pumped;
+ private long _epoch;
+ private List _devices = [];
+ private readonly EventQueue _pumpedEvents = new();
+ private WindowHandle _focusedWindow;
+ private ISdl _sdl;
+
+ public unsafe WindowHandle? FocusedWindow => _focusedWindow.Handle == null ? null : _focusedWindow;
+
+ public unsafe SdlInputBackend(SdlPlatformInfo info)
+ {
+ ArgumentNullException.ThrowIfNull(info.Sdl);
+ ArgumentNullException.ThrowIfNull(info.Window.Handle);
+ var ptr = new EventFilter(OnEvent);
+ _sdl = info.Sdl;
+ _focusedWindow = info.Window;
+ // TODO overload resolution priority?
+ if (!Sdl.AddEventWatch(ptr, (Ref)nullptr))
+ {
+ Sdl.ThrowError();
+ }
+
+ Id = (nint)ptr.Handle;
+
+ // The epoch deals in nanoseconds, so we take multiple measurements for the most accurate timestamps.
+ const byte epochMeasurements = 3;
+ var epoch = 0L;
+ for (byte i = 0; i < epochMeasurements; i++)
+ {
+ // We know the ticks per nanosecond, so to get the epoch timestamp we multiply the TicksNS by the ticks per
+ // nanosecond to get the number of ticks relative to SDL's epoch, and then subtract that from the timestamp
+ // now to get the timestamp of SDL's epoch. From there, when we receive an event we can just report the
+ // timestamp as _epoch + (timestamp * _ticksPerNanosecond).
+ var nowTimestamp = Stopwatch.GetTimestamp();
+ var nowTicks = Sdl.GetTicksNS();
+ epoch += unchecked(nowTimestamp - (long)(nowTicks * _ticksPerNanosecond));
+ }
+
+ _epoch = epoch / epochMeasurements;
+
+ // ===============================================================================================
+ // === If we ever need to share common state across window-specific "backends", use the below: ===
+ // ===============================================================================================
+ // // Get the root surface - our windowing backend assumes there is only one root surface. If this is not the
+ // // case then this is undefined behaviour.
+ // var rootSurface = info.Window;
+ // var parent = rootSurface;
+ // while ((parent = Sdl.GetWindowParent(rootSurface)) != nullptr)
+ // {
+ // rootSurface = parent;
+ // }
+ // // Get the surface properties.
+ // var props = Sdl.GetWindowProperties(rootSurface);
+ // if (props == 0)
+ // {
+ // Sdl.ThrowError();
+ // }
+ // // Get or create the root object.
+ // Ref pname = "org.dotnetfoundation.silkdotnet.inputroot";
+ // var root = (nint)Sdl.GetPointerProperty(props, pname, nullptr);
+ // if (root != 0)
+ // {
+ // Root =
+ // GCHandle.FromIntPtr(root).Target as SdlBackendRoot
+ // ?? throw new InvalidOperationException(
+ // "The global input data for this ancestry of SDL windows was not in an expected format."
+ // );
+ // }
+ // else
+ // {
+ // Root = new SdlBackendRoot();
+ // var newHandle = GCHandle.Alloc(Root);
+ // if (
+ // Sdl.SetPointerPropertyWithCleanup(
+ // props,
+ // pname,
+ // (Ptr)GCHandle.ToIntPtr(newHandle),
+ // new CleanupPropertyCallback(&CleanupRoot),
+ // nullptr
+ // )
+ // )
+ // {
+ // return;
+ // }
+ // newHandle.Free();
+ // Sdl.ThrowError();
+ // }
+ // // Register ourselves with the root.
+ // Root.Backends.Add(this, null);
+ // Id = (nint)Root.EventFilter.Handle + Root.Backends.Count() - 1;
+ }
+
+ // [UnmanagedCallersOnly]
+ // private static unsafe void CleanupRoot(void* _, void* value)
+ // {
+ // var gch = GCHandle.FromIntPtr((nint)value);
+ // (gch.Target as SdlBackendRoot)?.Dispose();
+ // gch.Free();
+ // }
+ // public SdlBackendRoot Root { get; }
+
+ // NOTE: Be careful where these are used!
+ public SdlPlatformInfo Info { get; }
+
+ [field: MaybeNull]
+ public SdlUnboundedPointerTarget UnboundedPointerTarget =>
+ field ??= new SdlUnboundedPointerTarget(this);
+
+ public ISdl Sdl => Info.Sdl ?? SDL.Sdl.Instance;
+
+ public string Name =>
+ $"Silk.NET.Input Reference Implementation using SDL3 ({Sdl.GetPlatform().ReadToString()})";
+
+ public nint Id { get; }
+
+ public IReadOnlyList Devices => _devices;
+
+ // TODO we can't query support for these modes, but should we try-it-and-see to be accurate?
+ public CursorModes SupportedModes =>
+ CursorModes.Normal | CursorModes.Confined | CursorModes.Unbounded;
+
+ // TODO if you're using one input context for all windows, there is no way to specify a window for grabbed cursor mode
+
+ public CursorModes Mode
+ {
+ get => throw new NotImplementedException();
+ set => throw new NotImplementedException();
+ }
+
+ public CursorStyles SupportedStyles => throw new NotImplementedException();
+
+ public CursorStyles Style
+ {
+ get => throw new NotImplementedException();
+ set => throw new NotImplementedException();
+ }
+
+ public CustomCursor Image
+ {
+ get => throw new NotImplementedException();
+ set => throw new NotImplementedException();
+ }
+
+ public HashSet DeviceRegistry { get; } = [];
+
+ // This is complicated, as the input proposal mandates that nothing happens until Update is called (so the events
+ // can be received on the given actor) but to also track logical events that happen between calls (i.e. from a
+ // timestamp perspective). Compound this with the fact that the user might do something silly like make multiple
+ // input backends (which is feasible for multiple windows I guess), or not be running anything other than input
+ // (having obviously created a window beforehand but not actually polling events I guess)
+ public void Update(IInputHandler? handler = null)
+ {
+ if (!_pumped)
+ {
+ Sdl.PumpEvents();
+ }
+
+ _pumped = false;
+ if (handler == null)
+ {
+ _pumpedEvents.Clear();
+ return;
+ }
+
+ // process all events that have been queued?
+
+ while (_pumpedEvents.TryDequeue(out var evt))
+ {
+ ProcessEvent(in evt, handler);
+ }
+
+ foreach (var device in _devices)
+ {
+ if (device is SdlKeyboard keyboard)
+ {
+ keyboard.UpdateModState();
+ }
+ }
+ }
+
+ private enum QueuedEventType : byte
+ {
+ ///
+ /// The mouse has exited the window and the shared point should be marked inactive until proven otherwise by
+ /// further mouse motion (indicating it has entered another window).
+ ///
+ ///
+ /// We do not track the mouse enter events as this would cause us to fire twice for a mouse entering a window:
+ /// once for the entering, and once for new position.
+ ///
+ MouseExitedWindow,
+
+ ///
+ /// The display bounds have been changed, meaning that 's
+ /// will have changed.
+ ///
+ BoundedPointerTargetUpdate,
+ }
+
+ private ulong GetTimestamp(in Event @event) =>
+ unchecked((ulong)(_epoch + (@event.Common.Timestamp * _ticksPerNanosecond)));
+
+ private unsafe byte OnEvent(void* arg0, Event* arg1)
+ {
+ _pumped = true;
+ _pumpedEvents.Add(ref *arg1);
+ return 1;
+ }
+
+ private void ProcessEvent(in Event evt, IInputHandler handler)
+ {
+ var timestamp = GetTimestamp(in evt);
+ Debug.Assert(timestamp >= _previousTimestamp, "Events out of order");
+ _previousTimestamp = timestamp;
+
+ // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
+ var type = (EventType)evt.Common.Type;
+
+ switch (type)
+ {
+ case EventType.GamepadRemoved:
+ RemoveDevice(_devices, evt.Gdevice.Which);
+ return;
+ case EventType.JoystickRemoved:
+ RemoveDevice(_devices, evt.Jdevice.Which);
+ return;
+ case EventType.KeyboardRemoved:
+ RemoveDevice(_devices, evt.Kdevice.Which);
+ return;
+ case EventType.MouseRemoved:
+ RemoveDevice(_devices, evt.Mdevice.Which);
+ RemoveDevice(_devices, evt.Mdevice.Which);
+ return;
+ case >= EventType.KeyDown and <= EventType.TextEditingCandidates:
+ {
+ if (!TryGetOrCreateDevice(evt.Kdevice.Which, out var keyboard))
+ {
+ return;
+ }
+
+ if (type == EventType.KeyboardAdded)
+ {
+ return;
+ }
+
+ switch (type)
+ {
+ case EventType.KeyDown:
+ case EventType.KeyUp:
+ keyboard.AddKeyEvent(evt.Key);
+ break;
+ case EventType.TextEditing:
+ keyboard.AddTextEditingEvent(evt.Edit);
+ break;
+ case EventType.TextEditingCandidates:
+ keyboard.AddTextCandidatesEvent(evt.EditCandidates);
+ break;
+ case EventType.TextInput:
+ keyboard.AddTextInputEvent(evt.Text);
+ break;
+ }
+
+ break;
+ }
+ case >= EventType.GamepadAxisMotion and <= EventType.GamepadSteamHandleUpdated:
+ {
+ if (!TryGetOrCreateDevice(evt.Gdevice.Which, out var gamepad))
+ {
+ return;
+ }
+
+ if (type is EventType.GamepadAdded)
+ {
+ return;
+ }
+
+ switch (type)
+ {
+ case EventType.GamepadAxisMotion:
+ gamepad.AddAxisEvent(evt.Gaxis.Axis, evt.Gaxis.Value);
+ break;
+ case EventType.GamepadButtonDown:
+ case EventType.GamepadButtonUp:
+ gamepad.AddButtonEvent(evt.Gbutton.Button, evt.Gbutton.Down);
+ break;
+ case EventType.GamepadRemapped:
+ gamepad.Remap();
+ break;
+ case EventType.GamepadTouchpadDown:
+ break;
+ case EventType.GamepadTouchpadMotion:
+ break;
+ case EventType.GamepadTouchpadUp:
+ break;
+ case EventType.GamepadSensorUpdate:
+ break;
+ case EventType.GamepadUpdateComplete:
+ break;
+ case EventType.GamepadSteamHandleUpdated:
+ break;
+ }
+
+ break;
+ }
+ case >= EventType.JoystickAxisMotion and <= EventType.JoystickUpdateComplete:
+ {
+ if (!TryGetOrCreateDevice(evt.Jdevice.Which, out var joystick))
+ {
+ return;
+ }
+
+ if (type is EventType.JoystickAdded)
+ {
+ // already done
+ return;
+ }
+
+ switch (type)
+ {
+ case EventType.JoystickAxisMotion:
+ joystick.AddAxisEvent(evt.Jaxis.Axis, evt.Jaxis.Value);
+ break;
+ case EventType.JoystickBallMotion:
+ break;
+ case EventType.JoystickHatMotion:
+ joystick.AddHatEvent(evt.Jhat.Hat, evt.Jhat.Value);
+ break;
+ case EventType.JoystickButtonDown:
+ case EventType.JoystickButtonUp:
+ joystick.AddButtonEvent(evt.Jbutton.Button, evt.Jbutton.Down);
+ break;
+ case EventType.JoystickBatteryUpdated:
+ break;
+ case EventType.JoystickUpdateComplete:
+ break;
+ }
+ break;
+ }
+ case >= EventType.MouseMotion and <= EventType.MouseAdded:
+ {
+ if(!TryGetOrCreateDevice(evt.Mdevice.Which, out var mouse))
+ {
+ return;
+ }
+
+ if (type is EventType.MouseAdded)
+ {
+ return;
+ }
+
+ switch (type)
+ {
+ case EventType.MouseMotion:
+ mouse.AddMotion(evt.Motion);
+ break;
+ case EventType.MouseButtonDown:
+ case EventType.MouseButtonUp:
+ mouse.AddButtonEvent(evt.Button);
+ break;
+ case EventType.MouseWheel:
+ mouse.AddWheelEvent(evt.Wheel);
+ break;
+ }
+
+ break;
+ }
+ }
+
+ switch (type)
+ {
+ // Input events ----------------------------------------------------------
+
+ // sensor? for what?
+ case EventType.SensorUpdate:
+ break;
+
+ // ----- Pointer events
+
+ // mouse
+ case EventType.MouseMotion:
+ break;
+ case EventType.MouseButtonDown:
+ break;
+ case EventType.MouseButtonUp:
+ break;
+ case EventType.MouseWheel:
+ break;
+
+ // touch
+ case EventType.FingerDown:
+ break;
+ case EventType.FingerUp:
+ break;
+ case EventType.FingerMotion:
+ break;
+ case EventType.FingerCanceled:
+ break;
+
+ // pen
+ case EventType.PenProximityIn:
+ break;
+ case EventType.PenProximityOut:
+ break;
+ case EventType.PenDown:
+ break;
+ case EventType.PenUp:
+ break;
+ case EventType.PenButtonDown:
+ break;
+ case EventType.PenButtonUp:
+ break;
+ case EventType.PenMotion:
+ break;
+ case EventType.PenAxis:
+ break;
+
+ // Display & window (pointer target) events ----------------------------
+ case EventType.DisplayOrientation:
+ case EventType.DisplayAdded:
+ case EventType.DisplayRemoved:
+ case EventType.DisplayMoved:
+ case EventType.DisplayDesktopModeChanged:
+ case EventType.DisplayCurrentModeChanged:
+ case EventType.DisplayContentScaleChanged:
+ {
+ var bounds = SdlBoundedPointerTarget.CalculateBounds(Sdl);
+ var x = (QueuedEventType.BoundedPointerTargetUpdate,
+ timestamp,
+ bounds.Min.ToSystem(),
+ bounds.Max.ToSystem()
+ );
+ break;
+ }
+ case EventType.WindowMouseLeave
+ : // do we need to do anything? we should probably track the current window of the pointer
+ {
+ var x = (QueuedEventType.MouseExitedWindow, timestamp);
+ break;
+ }
+
+ // Text input events -------------------------------------------
+ // todo: attribute this to a keyboard device? or something else?
+ case EventType.TextEditing:
+ break;
+ case EventType.TextInput:
+ break;
+ case EventType.TextEditingCandidates:
+ break;
+ case EventType.ClipboardUpdate:
+ break;
+ }
+ }
+
+ internal bool TryGetOrCreateDevice(uint id, [NotNullWhen(true)] out T? device) where T : SdlDevice, ISdlDevice
+ {
+ // If we already have a device with this ID, return it.
+ for (var i = 0; i < _devices.Count; i++)
+ {
+ if (_devices[i] is T typedDevice && typedDevice.SdlDeviceId == id)
+ {
+ device = typedDevice;
+ return true;
+ }
+ }
+
+ try
+ {
+ device = T.CreateDevice(id, this);
+ }
+ catch (Exception e)
+ {
+ Console.Error.WriteLine($"Failed to create device {nameof(T)} with id '{id}': {e}");
+ device = null;
+ return false;
+ }
+
+ if (device is null)
+ {
+ Console.Error.WriteLine($"Failed to create device {nameof(T)} with id '{id}'");
+ return false;
+ }
+
+
+ _devices.Add(device);
+ Console.WriteLine($"Gamepad added: (sdl ID: {id})");
+ return true;
+ }
+
+ private static bool RemoveDevice(List devices, uint id)
+ {
+ var deviceIdx = devices.FindIndex(x => x is T && x.SdlDeviceId == id);
+
+ if (deviceIdx == -1)
+ {
+ // we never used this device to begin with, so just ignore its removal
+ return false;
+ }
+
+ var device = devices[deviceIdx];
+ device.Dispose();
+ devices.RemoveAt(deviceIdx);
+
+ // device IDs may have changed when a device was removed, so we need to refresh them
+ RefreshDeviceIds(devices);
+ return true;
+ }
+
+ private static void RefreshDeviceIds(List devices)
+ {
+ for (var i = 0; i < devices.Count; i++)
+ {
+ if (devices[i] is IOrderedDevice d)
+ {
+ d.RefreshSdlId();
+ }
+ }
+ }
+
+ private ulong _previousTimestamp = ulong.MinValue;
+
+ private unsafe void ReleaseUnmanagedResources()
+ {
+ Sdl.RemoveEventWatch(
+ new EventFilter((delegate* unmanaged)(void*)Id),
+ nullptr
+ );
+ SilkMarshal.Free((Ptr)Id);
+ }
+
+ public void Dispose()
+ {
+ ReleaseUnmanagedResources();
+ GC.SuppressFinalize(this);
+ }
+
+ ~SdlInputBackend() => ReleaseUnmanagedResources();
+
+ private class EventQueue
+ {
+ private readonly Queue _events = new(1024);
+ public void Add(ref Event p0) => _events.Enqueue(p0);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool TryDequeue(out Event p0) => _events.TryDequeue(out p0);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Clear() => _events.Clear();
+ }
+
+ internal unsafe bool TryGetPointerTargetForWindow(WindowHandle window, [NotNullWhen(true)] out IPointerTarget? target)
+ {
+ if (window.Handle == null)
+ {
+ target = null;
+ return false;
+ }
+
+ var id = _sdl.GetWindowID(window);
+ return TryGetPointerTargetForWindow(id, out target);
+ }
+
+ internal bool TryGetPointerTargetForWindow(uint id, [NotNullWhen(true)] out IPointerTarget? target)
+ {
+ if (id == 0)
+ {
+ target = null;
+ return false;
+ }
+
+ // todo : get the SDL window (or other window) with the given ID
+ target = null;
+ throw new NotImplementedException();
+ }
+}
diff --git a/sources/Input/Input/Implementations/SDL3/SdlKeyConversions.cs b/sources/Input/Input/Implementations/SDL3/SdlKeyConversions.cs
new file mode 100644
index 0000000000..fdcf6cb689
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/SdlKeyConversions.cs
@@ -0,0 +1,104 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.CompilerServices;
+using Silk.NET.SDL;
+
+namespace Silk.NET.Input.SDL3;
+
+internal static class SdlKeyConversions
+{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static KeyName ScancodeToKeyName(uint scancode) => (KeyName)scancode;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static KeyName ScancodeToKeyName(Scancode scancode) => ScancodeToKeyName((uint)scancode);
+
+ public static unsafe KeyName SdlToKeyName(uint key, ISdl sdl, ushort? modState = null)
+ {
+ modState ??= sdl.GetModState();
+ var modStateVal = modState.Value;
+ return (KeyName)sdl.GetScancodeFromKey(key, &modStateVal);
+ }
+
+ ///
+ /// Maps an SDL key id to a without a reference to an SDL backend instance.
+ ///
+ /// The sdl key id
+ /// The associated key name
+ public static KeyName SdlToKeyName(uint key) =>
+ // * indicates a shifted key
+ key switch {
+ Sdl.KApplication => KeyName.Application,
+
+ >= Sdl.K1 and <= Sdl.K9 => (KeyName)(key - _numKeyDiff),
+ >= Sdl.Ka and <= Sdl.Kz => (KeyName)(key - _letterKeyDiff),
+ >= Sdl.KCapslock and <= Sdl.KKpEqualsas400 => (KeyName)(key - _systemAndKeypadDiff),
+ >= Sdl.KCancel and <= Sdl.KRgui => (KeyName)(key - _systemAndKeypadDiff),
+ >= Sdl.KMode and <= Sdl.KAcBookmarks => (KeyName)(key - _systemAndKeypadDiff),
+ >= Sdl.KSoftleft and <= Sdl.KEndcall => (KeyName)(key - _systemNonHidKeyDiff),
+
+ Sdl.KDelete => KeyName.Delete,
+ Sdl.KUnknown => KeyName.Unknown,
+ Sdl.KReturn => KeyName.Return,
+ Sdl.KEscape => KeyName.Escape,
+ Sdl.KBackspace => KeyName.Backspace,
+ Sdl.KTab => KeyName.Tab,
+ Sdl.KSpace => KeyName.Space,
+ Sdl.KExclaim => KeyName.Number1, // *
+ Sdl.KDblapostrophe => KeyName.Apostrophe, // *
+ Sdl.KHash => KeyName.Number3, // *
+ Sdl.KDollar => KeyName.Number4, // *
+ Sdl.KPercent => KeyName.Number5, // *
+ Sdl.KAmpersand => KeyName.KeypadAmpersand,
+ Sdl.KApostrophe => KeyName.Apostrophe,
+ Sdl.KLeftparen => KeyName.KeypadLeftParenthesis,
+ Sdl.KRightparen => KeyName.KeypadRightParenthesis,
+ Sdl.KAsterisk => KeyName.KeypadAmpersand,
+ Sdl.KPlus => KeyName.Equals, // *
+ Sdl.KComma => KeyName.Comma,
+ Sdl.KMinus => KeyName.Minus,
+ Sdl.KPeriod => KeyName.Period,
+ Sdl.KSlash => KeyName.Slash,
+ Sdl.K0 => KeyName.Number0,
+ Sdl.KColon => KeyName.Semicolon, // *
+ Sdl.KSemicolon => KeyName.Semicolon,
+ Sdl.KLess => KeyName.Comma, // *
+ Sdl.KEquals => KeyName.Equals,
+ Sdl.KGreater => KeyName.Period, // *
+ Sdl.KQuestion => KeyName.Slash, // *
+ Sdl.KAt => KeyName.Number2, // *
+ Sdl.KLeftbracket => KeyName.LeftBracket,
+ Sdl.KBackslash => KeyName.Backslash,
+ Sdl.KRightbracket => KeyName.RightBracket,
+ Sdl.KCaret => KeyName.Number6, // *
+ Sdl.KUnderscore => KeyName.Minus, // *
+ Sdl.KGrave => KeyName.Grave,
+ Sdl.KLeftbrace => KeyName.LeftBracket, // *
+ Sdl.KPipe => KeyName.Backslash, // *
+ Sdl.KRightbrace => KeyName.RightBracket, // *
+ Sdl.KTilde => KeyName.Grave, // *
+ _ => (KeyName)key
+ };
+
+ ///
+ /// Converts a to an SDL key id.
+ ///
+ /// The name of the key you would like to get an Sdl key id for
+ /// Sdl backend instance
+ /// Will this key be used in a key event?
+ /// The current modifier key state
+ /// The sdl key id
+ public static uint KeyNameToSdl(KeyName key, ISdl sdl, bool asKeyEvent, ushort? modState = null)
+ {
+ modState ??= sdl.GetModState();
+ var scanCode = (uint)key;
+ var asKeyEventByte = asKeyEvent ? (byte)1 : (byte)0;
+ return sdl.GetKeyFromScancode((Scancode)scanCode, modState.Value, asKeyEventByte);
+ }
+
+ private const uint _letterKeyDiff = Sdl.Ka - (uint)KeyName.A;
+ private const uint _numKeyDiff = Sdl.K1 - (uint)KeyName.Number1;
+ private const uint _systemAndKeypadDiff = Sdl.KPrintscreen - (uint)KeyName.PrintScreen;
+ private const uint _systemNonHidKeyDiff = Sdl.KSoftleft - (uint)KeyName.SoftLeft;
+}
diff --git a/sources/Input/Input/InputContext.cs b/sources/Input/Input/InputContext.cs
new file mode 100644
index 0000000000..402735e702
--- /dev/null
+++ b/sources/Input/Input/InputContext.cs
@@ -0,0 +1,241 @@
+using System.Collections;
+
+namespace Silk.NET.Input;
+
+///
+/// Represents an "input context" containing multiple s from which
+/// s, their state, and their events are aggregated and laid-out in a user-friendly fashion.
+///
+///
+/// The onus is on the user to coordinate using this type across threads, as the input backend is not thread safe
+/// In addition, certain backends may have (unavoidable) restrictions on what thread can be called
+/// on - the user is responsible for respecting these threading rules as well.
+///
+public class InputContext
+ : IJoystickInputHandler,
+ IGamepadInputHandler,
+ IMouseInputHandler,
+ IPointerInputHandler,
+ IKeyboardInputHandler,
+ IList
+{
+ // These are lazy-initialized as they contain their own device lists in addition to the device list stored here and
+ // the device lists stored in each of the backends. You could argue having this many duplicated lists is inefficient
+ // and you'd be absolutely right, but realistically: how many devices will the average user have connected to their
+ // PC? If you're worried about your game's memory consumption, you're probably not looking at the small lists that
+ // input allocates... This way we can also provide sane/consistent indices.
+ private Pointers? _pointers;
+ private Keyboards? _keyboards;
+ private Gamepads? _gamepads;
+ private Joysticks? _joysticks;
+ private List _backends = [];
+ private List? _devices;
+
+ ///
+ /// Gets the s enumerated by the s attached to this context.
+ ///
+ public Pointers Pointers => _pointers ??= new Pointers(this);
+
+ ///
+ /// Gets the s enumerated by the s attached to this context.
+ ///
+ public Keyboards Keyboards => _keyboards ??= new Keyboards(this);
+
+ ///
+ /// Gets the s enumerated by the s attached to this context.
+ ///
+ public Gamepads Gamepads => _gamepads ??= new Gamepads(this);
+
+ ///
+ /// Gets the s enumerated by the s attached to this context.
+ ///
+ public Joysticks Joysticks => _joysticks ??= new Joysticks(this);
+
+ ///
+ /// Gets the s enumerated by the s attached to this context.
+ ///
+ public IReadOnlyList Devices
+ {
+ get
+ {
+ if (_devices is not null)
+ {
+ return _devices;
+ }
+
+ foreach (var backend in Backends)
+ {
+ _devices ??= new List(backend.Devices.Count);
+ _devices.AddRange(backend.Devices);
+ }
+
+ return _devices ??= [];
+ }
+ }
+
+ ///
+ /// Gets a list denoting the attached to this context.
+ ///
+ public IList Backends => this;
+
+ ///
+ /// Raised when a device is added or removed from the list of connected .
+ ///
+ public event Action? ConnectionChanged;
+
+ ///
+ /// Polls and updates the state of the objects connected to each
+ /// attached to this context, raising appropriate events for each state change.
+ ///
+ ///
+ /// This calls for each attached to this context.
+ ///
+ public void Update()
+ {
+ foreach (var backend in Backends)
+ {
+ backend.Update(this);
+ }
+
+ _pointers?.HandleUpdate();
+ }
+
+ private void HandleBackendRemoval(IInputBackend backend)
+ {
+ foreach (var device in backend.Devices)
+ {
+ HandleDeviceConnectionChanged(new ConnectionEvent(device, 0, false));
+ }
+ }
+
+ private void HandleBackendAddition(IInputBackend backend)
+ {
+ foreach (var device in backend.Devices)
+ {
+ HandleDeviceConnectionChanged(new ConnectionEvent(device, 0, true));
+ }
+ }
+
+ private void HandleDeviceConnectionChanged(ConnectionEvent e)
+ {
+ _pointers?.HandleDeviceConnectionChanged(e);
+ _joysticks?.HandleDeviceConnectionChanged(e);
+ _gamepads?.HandleDeviceConnectionChanged(e);
+ _keyboards?.HandleDeviceConnectionChanged(e);
+ if (_devices is null)
+ {
+ return;
+ }
+
+ if (e.IsConnected)
+ {
+ _devices?.Add(e.Device);
+ }
+ else
+ {
+ _devices?.Remove(e.Device);
+ }
+ }
+
+ void IButtonInputHandler.HandleButtonChanged(
+ ButtonChangedEvent @event
+ ) => _joysticks?.HandleButtonChanged(@event);
+
+ void IJoystickInputHandler.HandleAxisMove(JoystickAxisMoveEvent @event) =>
+ _joysticks?.HandleAxisMove(@event);
+
+ void IJoystickInputHandler.HandleHatMove(JoystickHatMoveEvent @event) =>
+ _joysticks?.HandleHatMove(@event);
+
+ void IGamepadInputHandler.HandleThumbstickMove(GamepadThumbstickMoveEvent @event) =>
+ _gamepads?.HandleThumbstickMove(@event);
+
+ void IGamepadInputHandler.HandleTriggerMove(GamepadTriggerMoveEvent @event) =>
+ _gamepads?.HandleTriggerMove(@event);
+
+ void IButtonInputHandler.HandleButtonChanged(
+ ButtonChangedEvent @event
+ ) => _pointers?.HandleButtonChanged(@event);
+
+ void IMouseInputHandler.HandleScroll(MouseScrollEvent @event) =>
+ _pointers?.HandleScroll(@event);
+
+ void IPointerInputHandler.HandleTargetChanged(PointerTargetChangedEvent @event) =>
+ _pointers?.HandleTargetChanged(@event);
+
+ void IPointerInputHandler.HandlePointChanged(PointChangedEvent @event) =>
+ _pointers?.HandlePointChanged(@event);
+
+ void IPointerInputHandler.HandleGripChanged(PointerGripChangedEvent @event) =>
+ _pointers?.HandleGripChanged(@event);
+
+ void IButtonInputHandler.HandleButtonChanged(ButtonChangedEvent @event) =>
+ _keyboards?.HandleButtonChanged(@event);
+
+ void IKeyboardInputHandler.HandleKeyChanged(KeyChangedEvent @event) =>
+ _keyboards?.HandleKeyChanged(@event);
+
+ void IKeyboardInputHandler.HandleKeyChar(KeyCharEvent @event) =>
+ _keyboards?.HandleKeyChar(@event);
+
+ void IInputHandler.HandleDeviceConnectionChanged(ConnectionEvent @event)
+ {
+ HandleDeviceConnectionChanged(@event);
+ ConnectionChanged?.Invoke(@event);
+ }
+
+ IEnumerator IEnumerable.GetEnumerator() =>
+ _backends.GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator() => _backends.GetEnumerator();
+
+ void ICollection.Add(IInputBackend item)
+ {
+ HandleBackendAddition(item);
+ _backends.Add(item);
+ }
+
+ void ICollection.Clear()
+ {
+ foreach (var backend in Backends)
+ {
+ HandleBackendRemoval(backend);
+ }
+ }
+
+ bool ICollection.Contains(IInputBackend item) => _backends.Contains(item);
+
+ void ICollection.CopyTo(IInputBackend[] array, int arrayIndex) =>
+ _backends.CopyTo(array, arrayIndex);
+
+ bool ICollection.Remove(IInputBackend item)
+ {
+ HandleBackendRemoval(item);
+ return _backends.Remove(item);
+ }
+
+ int ICollection.Count => _backends.Count;
+
+ bool ICollection.IsReadOnly => false;
+
+ int IList.IndexOf(IInputBackend item) => _backends.IndexOf(item);
+
+ void IList.Insert(int index, IInputBackend item)
+ {
+ HandleBackendAddition(item);
+ _backends.Insert(index, item);
+ }
+
+ void IList.RemoveAt(int index)
+ {
+ var backend = _backends[index];
+ HandleBackendRemoval(backend);
+ _backends.RemoveAt(index);
+ }
+
+ IInputBackend IList.this[int index]
+ {
+ get => _backends[index];
+ set => _backends[index] = value;
+ }
+}
diff --git a/sources/Input/Input/InputContextDeviceList.cs b/sources/Input/Input/InputContextDeviceList.cs
new file mode 100644
index 0000000000..1ae312526b
--- /dev/null
+++ b/sources/Input/Input/InputContextDeviceList.cs
@@ -0,0 +1,61 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Silk.NET.Input;
+
+///
+/// An internal class that represents a list of that are assignable to
+/// . The backing list is lazily initialized.
+///
+/// The device type.
+///
+/// This type is not intended for public consumption and has no API/ABI stability guarantees.
+///
+[Experimental(
+ "ST0005",
+ UrlFormat = "https://dotnet.github.io/Silk.NET/docs/v3/silk.net/diagnostics/{0}"
+)]
+public abstract class InputContextDeviceList : IReadOnlyList, IInputHandler
+{
+ private readonly InputContext _ctx;
+ private List? _list;
+
+ internal InputContextDeviceList(InputContext ctx) => _ctx = ctx;
+
+ private List List => _list ??= _ctx.Devices.OfType().ToList();
+
+ ///
+ public IEnumerator GetEnumerator() => throw new NotImplementedException();
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ ///
+ public int Count => List.Count;
+
+ ///
+ public T this[int index] => List[index];
+
+ void IInputHandler.HandleDeviceConnectionChanged(ConnectionEvent @event) =>
+ HandleDeviceConnectionChanged(@event);
+
+ ///
+ protected internal virtual void HandleDeviceConnectionChanged(ConnectionEvent @event)
+ {
+ if (_list is null || @event.Device is not T t)
+ {
+ return;
+ }
+
+ if (@event.IsConnected)
+ {
+ _list.Add(t);
+ }
+ else
+ {
+ _list.Remove(t);
+ }
+ }
+}
diff --git a/sources/Input/Input/InputMarshal.cs b/sources/Input/Input/InputMarshal.cs
new file mode 100644
index 0000000000..19c7d156c5
--- /dev/null
+++ b/sources/Input/Input/InputMarshal.cs
@@ -0,0 +1,685 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections;
+using System.Diagnostics;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+
+namespace Silk.NET.Input;
+
+///
+/// Contains utilities for creating and manipulating s. This is a very unsafe set of
+/// APIs that are extremely prone to misuse, and therefore is not recommended to be consumed by anyone other than input
+/// backends.
+///
+///
+/// This class is ABI/API stable, but new APIs that obsolete the old ones may be added at any time as more efficient
+/// implementations are discovered.
+///
+// NOTE: Not experimental so that we don't eliminate the prospects of third-party implementations.
+public static class InputMarshal
+{
+ ///
+ /// A wrapper class denoting ownership of a . This is used to attempt to stop
+ /// misuse of these methods, but of course it's fairly trivial to work around this for a user determined to do
+ /// terrible things.
+ ///
+ /// The list element type.
+ public struct ListOwner
+ {
+ internal ListOwner(InputReadOnlyList list) => List = list;
+
+ ///
+ /// Gets the list owned by this owner.
+ ///
+ public InputReadOnlyList List { get; }
+ }
+
+ internal class ButtonList(uint[] binary, Dictionary>? other)
+ : IReadOnlyList>
+ where T : unmanaged, Enum
+ {
+ private Dictionary>? _other = other;
+
+ public ButtonList()
+ : this(new uint[(GetButtonListCount() + 32 - 1) / 32], null) { }
+
+ public ButtonList Clone() =>
+ new([.. binary], _other is null ? null : new Dictionary>(_other));
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)] // <-- generic specialisation
+ public IEnumerator> GetEnumerator() =>
+ typeof(T) == typeof(KeyName) ? GetKeyNameEnumerator() : GetButtonEnumerator();
+
+ private IEnumerator