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 7c83ca6760..ff14ca8dae 100644
--- a/Silk.NET.sln
+++ b/Silk.NET.sln
@@ -118,6 +118,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "OpenAL", "OpenAL", "{662A1A
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tutorial001.HelloSound", "examples\CSharp\OpenAL\Tutorial001.HelloSound\Tutorial001.HelloSound.csproj", "{946C912C-5BBB-446A-A566-0D1696D19F59}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Input", "Input", "{61DB4B9A-9EF6-4F31-A8F5-BEAF37FA8AD1}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Silk.NET.Input", "sources\Input\Input\Silk.NET.Input.csproj", "{DDADB0F1-DFC9-4297-B0B0-92984D6F6F4C}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -196,6 +200,10 @@ Global
{946C912C-5BBB-446A-A566-0D1696D19F59}.Debug|Any CPU.Build.0 = Debug|Any CPU
{946C912C-5BBB-446A-A566-0D1696D19F59}.Release|Any CPU.ActiveCfg = Release|Any CPU
{946C912C-5BBB-446A-A566-0D1696D19F59}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DDADB0F1-DFC9-4297-B0B0-92984D6F6F4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DDADB0F1-DFC9-4297-B0B0-92984D6F6F4C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DDADB0F1-DFC9-4297-B0B0-92984D6F6F4C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DDADB0F1-DFC9-4297-B0B0-92984D6F6F4C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -233,8 +241,8 @@ Global
{12B4D1CB-8938-4EC4-8895-79C4E6ABD1E8} = {6077EDD4-F16F-4CA4-B72E-E4627D64B104}
{662A1AEC-91F2-48FA-AA29-6F27038D30F2} = {12B4D1CB-8938-4EC4-8895-79C4E6ABD1E8}
{946C912C-5BBB-446A-A566-0D1696D19F59} = {662A1AEC-91F2-48FA-AA29-6F27038D30F2}
- {5E20252F-E2A0-46C9-BBEF-4CE5C96D0E07} = {DD29EA8F-B1A6-45AA-8D2E-B38DA56D9EF6}
- {E5E8FFBF-1319-4D33-B084-E732656E8A04} = {5E20252F-E2A0-46C9-BBEF-4CE5C96D0E07}
+ {61DB4B9A-9EF6-4F31-A8F5-BEAF37FA8AD1} = {DD29EA8F-B1A6-45AA-8D2E-B38DA56D9EF6}
+ {DDADB0F1-DFC9-4297-B0B0-92984D6F6F4C} = {61DB4B9A-9EF6-4F31-A8F5-BEAF37FA8AD1}
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 b6071aa7bd..a485d81551 100644
--- a/eng/build/Silk.NET.NUKE.csproj
+++ b/eng/build/Silk.NET.NUKE.csproj
@@ -19,4 +19,10 @@
+
+
+
+ Directory.Build\tests\Input\Input\Silk.NET.Input.UnitTests.csproj
+
+
diff --git a/eng/submodules/openal-soft b/eng/submodules/openal-soft
index 9c50193236..9f6fa42d90 160000
--- a/eng/submodules/openal-soft
+++ b/eng/submodules/openal-soft
@@ -1 +1 @@
-Subproject commit 9c50193236ad4d8c68f390a3151f8253709baabd
+Subproject commit 9f6fa42d902609bb9f77e4831d9183effad546f5
diff --git a/eng/submodules/silk.net-2.x b/eng/submodules/silk.net-2.x
index 3c0313b2d6..ca36450946 160000
--- a/eng/submodules/silk.net-2.x
+++ b/eng/submodules/silk.net-2.x
@@ -1 +1 @@
-Subproject commit 3c0313b2d69bbde12224c759a76bc2e7e064a893
+Subproject commit ca364509467f0fb40af40f7ed040b4c63670a4c6
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/net10.0/PublicAPI.Unshipped.txt b/sources/Core/Core/PublicAPI/net10.0/PublicAPI.Unshipped.txt
index d6847290ae..3875d06de8 100644
--- a/sources/Core/Core/PublicAPI/net10.0/PublicAPI.Unshipped.txt
+++ b/sources/Core/Core/PublicAPI/net10.0/PublicAPI.Unshipped.txt
@@ -473,6 +473,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/Core/Core/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/sources/Core/Core/PublicAPI/net8.0/PublicAPI.Unshipped.txt
index 8123f98932..2b7101b7a3 100644
--- a/sources/Core/Core/PublicAPI/net8.0/PublicAPI.Unshipped.txt
+++ b/sources/Core/Core/PublicAPI/net8.0/PublicAPI.Unshipped.txt
@@ -416,6 +416,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..0f004bc787
--- /dev/null
+++ b/sources/Input/Input/CustomCursor.cs
@@ -0,0 +1,38 @@
+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
+
+ // equality operator override
+ ///
+ /// Value-based equality operator
+ ///
+ /// Note that this operator does not consider reference equality
+ public static bool operator ==(CustomCursor left, CustomCursor right) => left.Width == right.Width &&
+ left.Height == right.Height &&
+ left.Data.Length == right.Data.Length &&
+ left.Data.SequenceEqual(right.Data);
+
+ ///
+ /// Value-based inequality operator
+ ///
+ /// Note that this operator does not consider reference equality
+ public static bool operator !=(CustomCursor left, CustomCursor right) => !(left == right);
+}
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..a8343fc9ff
--- /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
+///
+internal 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/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..f9687a12db
--- /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.Devices.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..db0ce06e9e
--- /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.Devices.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..a92010c6ad
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/Devices/Joysticks/SdlGamepad.cs
@@ -0,0 +1,359 @@
+// 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.Devices.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)
+ {
+ NativeBackend.Free(mappings);
+ }
+
+ SdlLog.Error("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..fc1ab2df30
--- /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.Devices.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..7587ffdf8f
--- /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.Devices.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..c3fcee7a76
--- /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.Devices.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/SdlCursor.cs b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlCursor.cs
new file mode 100644
index 0000000000..863fd060c7
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlCursor.cs
@@ -0,0 +1,295 @@
+// 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.Frozen;
+using System.Runtime.InteropServices;
+using Silk.NET.SDL;
+
+namespace Silk.NET.Input.SDL3.Devices.Pointers;
+
+internal unsafe class SdlCursor : ICursorConfiguration, IDisposable
+{
+ private readonly ISdl _sdl;
+
+ private CursorHandle _handle;
+
+ ///
+ /// Internal style of the current cursor handle - may differ from the property,
+ ///
+ private CursorStyles _handleStyle = _noStyle;
+ private const CursorStyles _noStyle = (CursorStyles)(-1);
+
+ private static readonly FrozenDictionary _cursorStyles =
+ new Dictionary {
+ [CursorStyles.Default] = SystemCursor.Default,
+ [CursorStyles.Arrow] = SystemCursor.Default,
+ [CursorStyles.IBeam] = SystemCursor.Text,
+ [CursorStyles.Crosshair] = SystemCursor.Crosshair,
+ [CursorStyles.Hand] = SystemCursor.Pointer,
+ [CursorStyles.HResize] = SystemCursor.EwResize,
+ [CursorStyles.VResize] = SystemCursor.NsResize
+ }.ToFrozenDictionary();
+
+
+ public SdlCursor(ISdl sdl)
+ {
+ _sdl = sdl;
+ Mode = CursorModes.Normal;
+ SupportedStyles = TestCursorCompatibility(sdl);
+ Style = CursorStyles.Arrow;
+ }
+
+ private bool SetCursorStyle(CursorStyles style)
+ {
+ CursorHandle handle;
+ if (style == CursorStyles.Custom)
+ {
+ if (_customCursorImage == null || _customCursorSurface == null)
+ {
+ return false;
+ }
+
+ var canReuseCurrentCursorHandle = _handleStyle == CursorStyles.Custom; // todo - compare cursor hotspot
+ if (canReuseCurrentCursorHandle)
+ {
+ return true;
+ }
+
+ // todo: cursor hotspot, not supported by sdl?
+ handle = _sdl.CreateColorCursor(_customCursorSurface, 0, 0);
+ }
+ else if (style == _handleStyle)
+ {
+ return true;
+ }
+ else
+ {
+ handle = _sdl.CreateSystemCursor(_cursorStyles[style]);
+ }
+
+ if (handle.Handle == null)
+ {
+ SdlLog.Error("Failed to create cursor");
+ return false;
+ }
+
+ if (_handle != handle)
+ {
+ FreeCurrentCursor();
+ }
+
+ _handle = handle;
+ _handleStyle = style;
+
+ if (_sdl.SetCursor(_handle))
+ {
+ return true;
+ }
+
+ SdlLog.Error("Failed to set cursor");
+ return false;
+ }
+
+ public void Dispose()
+ {
+ FreeCurrentCursor();
+ DisposeCursorSurface(ref _customCursorSurface);
+ }
+
+ private void FreeCurrentCursor()
+ {
+ if (_handle == default)
+ {
+ return;
+ }
+
+ _sdl.DestroyCursor(_handle);
+ _handle = default;
+
+ if (_handleStyle == CursorStyles.Custom)
+ {
+ DisposeCursorSurface(ref _customCursorSurface);
+ }
+
+ _handleStyle = _noStyle;
+ }
+
+ private void DisposeCursorSurface(ref Surface* surface)
+ {
+ if(surface != null)
+ {
+ _sdl.DestroySurface(surface);
+ _customCursorSurface = null;
+ }
+ }
+
+ private static CursorStyles TestCursorCompatibility(ISdl sdl)
+ {
+ // check cursor style availability
+ ReadOnlySpan mainStyles = [
+ CursorStyles.Arrow, CursorStyles.IBeam, CursorStyles.Crosshair, CursorStyles.Hand, CursorStyles.HResize,
+ CursorStyles.VResize
+ ];
+
+ // todo: is it necessary to check for the Default style? can some platforms just not support any cursor?
+ // if so, the result of this evaluation will still report that "Default" is available..
+ // lest we make it nullable... nah i'll leave it to the Sdl gods for now
+ var successfulStyles = CursorStyles.Default;
+ for (var i = 0; i < _cursorStyles.Count; i++)
+ {
+ var cursorStyle = mainStyles[i];
+ var sdlStyle = _cursorStyles[cursorStyle];
+ var cursor = sdl.CreateSystemCursor(sdlStyle);
+ if (cursor.Handle == null)
+ {
+ SdlLog.Debug($"System cursor style {sdlStyle} unavailable");
+ }
+ else
+ {
+ successfulStyles |= cursorStyle;
+ sdl.Free(cursor.Handle);
+ }
+ }
+
+ return successfulStyles;
+ }
+
+ public CursorModes SupportedModes =>
+ CursorModes.Normal | CursorModes.Confined | CursorModes.Unbounded;
+
+ public CursorModes Mode
+ {
+ get;
+ set
+ {
+ field = value;
+ try
+ {
+ ModeChanged?.Invoke(this, value);
+ }
+ catch (Exception e)
+ {
+ InputLog.Error(e.ToString());
+ }
+ }
+ }
+
+ public event EventHandler? ModeChanged;
+
+ public CursorStyles SupportedStyles { get; }
+
+ public CursorStyles Style
+ {
+ get;
+ set
+ {
+ if (value == CursorStyles.Hidden && field != CursorStyles.Hidden)
+ {
+ SetCursorVisibility(false);
+ return;
+ }
+
+ SetCursorStyle(value);
+ if(field == CursorStyles.Hidden)
+ {
+ SetCursorVisibility(true);
+ }
+
+ field = value;
+ }
+ }
+
+ private void SetCursorVisibility(bool visible)
+ {
+ if (_handle == default)
+ {
+ return;
+ }
+
+ if (visible ? _sdl.HideCursor() : _sdl.ShowCursor())
+ {
+ return;
+ }
+
+ SdlLog.Error("Failed to hide cursor");
+ }
+
+ public CustomCursor Image
+ {
+ get
+ {
+ var byteCount = _customCursorWidth * _customCursorHeight * 4;
+ var myBytes = _customCursorImage.AsSpan(..byteCount);
+ var asInts = MemoryMarshal.Cast(myBytes);
+ return new CustomCursor { Width = _customCursorWidth, Height = _customCursorHeight, Data = asInts };
+ }
+ set
+ {
+ var necessaryLength = value.Width * value.Height;
+ if(value.Data.Length < necessaryLength)
+ {
+ throw new ArgumentException($"Custom cursor image of size ({value.Width}, {value.Height}) " +
+ $"must be at least {value.Width * value.Height * 4} bytes long, " +
+ $"got {value.Data.Length} bytes instead");
+ }
+
+ // ensure we have a fixed byte array to work with so updates would automatically apply to sdl
+ _customCursorHeight = value.Height;
+ _customCursorWidth = value.Width;
+ var byteCount = necessaryLength * 4;
+ if (_customCursorImage is null || _customCursorImage.Length < byteCount)
+ {
+ _customCursorImage = GC.AllocateUninitializedArray(byteCount, pinned: true);
+ }
+
+ // copy the user data to our fixed array
+ var myBytes = _customCursorImage.AsSpan(..byteCount);
+ var providedBytes = MemoryMarshal.Cast(value.Data);
+ providedBytes.CopyTo(myBytes);
+
+ ApplyToCursorSurface(ref _customCursorSurface, value);
+
+ if (Style == CursorStyles.Custom && _handleStyle != CursorStyles.Custom)
+ {
+ SetCursorStyle(CursorStyles.Custom);
+ }
+
+ return;
+
+ void ApplyToCursorSurface(ref Surface* customCursorSurface, in CustomCursor val)
+ {
+ // create a new sdl surface if necessary
+ if(customCursorSurface != null)
+ {
+ if (customCursorSurface->H != val.Height || customCursorSurface->W != val.Width)
+ {
+ DisposeCursorSurface(ref customCursorSurface);
+ customCursorSurface = CreateSurface(val);
+ }
+ }
+ else
+ {
+ customCursorSurface = _sdl.CreateSurface(val.Width, val.Height, PixelFormat.Argb8888);
+ }
+
+ // ensure the surface's pixel data is our fixed array
+ fixed (byte* ptr = _customCursorImage)
+ {
+ customCursorSurface->Pixels = ptr;
+ }
+
+ return;
+
+ Ptr CreateSurface(CustomCursor customCursor)
+ {
+ return _sdl.CreateSurface(customCursor.Width, customCursor.Height, PixelFormat.Argb8888);
+ }
+ }
+ }
+ }
+
+ private Surface* _customCursorSurface;
+ private int _customCursorHeight, _customCursorWidth;
+ private byte[]? _customCursorImage;
+
+}
diff --git a/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlMouse.cs b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlMouse.cs
new file mode 100644
index 0000000000..9462abdba2
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlMouse.cs
@@ -0,0 +1,214 @@
+// 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.Devices.Pointers;
+
+internal class SdlMouse : SdlPointerDevice, IMouse, ISdlDevice
+{
+ public override PointerState State => _state;
+ public ICursorConfiguration Cursor { get; }
+
+ private readonly MouseState _state;
+
+ private SdlMouse(uint sdlDeviceId, nint uniqueId, SdlInputBackend backend, IPointerTarget unboundedPointerTarget, ICursorConfiguration cursor)
+ : base(backend, uniqueId, sdlDeviceId)
+ {
+ _state = new MouseState(new ButtonReadOnlyList(_buttons),
+ new InputReadOnlyList(_points), Vector2.Zero);
+ Cursor = cursor;
+ 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))
+ {
+ AddWindowPoint(pos, pressure, windowTarget);
+ }
+ else
+ {
+ AddUnboundedPoint(pos, pressure);
+ }
+ }
+
+ 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 SdlMouse 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 SdlMouse(sdlDeviceId, uniqueId, backend, backend.UnboundedPointerTarget, backend.CursorConfiguration);
+ }
+
+ public override string Name => NativeBackend.GetMouseNameForID(SdlDeviceId).ReadToString();
+
+ 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;
+ }
+ }
+
+ protected override bool IsBounded { get; }
+
+
+ public bool TrySetPosition(Vector2 position)
+ {
+ if (NativeBackend.WarpMouseGlobal(position.X, position.Y))
+ {
+ return true;
+ }
+
+ NativeBackend.ClearError();
+ return false;
+ }
+
+
+ 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 is < 0 or >= 32)
+ {
+ return false;
+ }
+
+ return (state & (1 << index)) != 0;
+ }
+
+ private uint _mouseWindowId;
+ private Vector2 _mouseScroll;
+ private Vector2 _accumulatedMotion;
+ 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/SdlPen.cs b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlPen.cs
new file mode 100644
index 0000000000..2f1488d110
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlPen.cs
@@ -0,0 +1,26 @@
+// 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.Devices.Pointers;
+
+internal class SdlPen : SdlPointerDevice, ISdlDevice
+{
+ public SdlPen(SdlInputBackend backend, nint silkId, uint sdlDeviceId, IReadOnlyList targets, string name) : base(backend, silkId, sdlDeviceId)
+ {
+ Targets = targets;
+ Name = name;
+ }
+
+ public static SdlPen CreateDevice(uint sdlDeviceId, SdlInputBackend backend)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override PointerState State => throw new NotImplementedException();
+ public override IReadOnlyList Targets { get; }
+ protected override bool IsBounded => true; // should this always be bounded?
+
+
+ public override string Name { get; }
+ protected override void Release() => throw new NotImplementedException();
+}
diff --git a/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlPointerDevice.cs b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlPointerDevice.cs
new file mode 100644
index 0000000000..5a131c8513
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlPointerDevice.cs
@@ -0,0 +1,29 @@
+// 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.Devices.Pointers.Targets;
+
+namespace Silk.NET.Input.SDL3.Devices.Pointers;
+
+///
+/// A base class for SDL input devices that operate in terms of a window's or DWMs bounds.
+///
+internal abstract class SdlPointerDevice : SdlDevice, IPointerDevice
+{
+ protected SdlPointerDevice(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.
+ ///
+ protected abstract bool IsBounded { get; }
+}
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..66ed7606b6
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlTouchScreen.cs
@@ -0,0 +1,52 @@
+// 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.Devices.Pointers;
+
+internal class SdlTouchScreen : SdlPointerDevice, ISdlDevice, IPointerDevice
+{
+ public static SdlTouchScreen CreateDevice(uint sdlDeviceId, SdlInputBackend backend)
+ {
+ throw new NotImplementedException();
+ }
+
+ public bool Equals(IInputDevice? other)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override string Name
+ {
+ get
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ protected override void Release()
+ {
+ throw new NotImplementedException();
+ }
+
+ public override PointerState State
+ {
+ get
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public override IReadOnlyList Targets
+ {
+ get
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ protected override bool IsBounded { get; }
+
+ public SdlTouchScreen(uint sdlDeviceId, nint uniqueId, SdlInputBackend backend) : base(backend, uniqueId, sdlDeviceId)
+ {
+ }
+}
diff --git a/sources/Input/Input/Implementations/SDL3/Devices/Pointers/Targets/SdlBoundedPointerTarget.cs b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/Targets/SdlBoundedPointerTarget.cs
new file mode 100644
index 0000000000..0e69bd34d5
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/Targets/SdlBoundedPointerTarget.cs
@@ -0,0 +1,134 @@
+// 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.Devices.Pointers.Targets;
+
+internal interface ISdlBoundedPointerTarget
+ where T : SdlBoundedPointerTarget
+ where THandle : unmanaged
+{
+ uint Id { get; }
+ THandle Handle { get; }
+
+ public static abstract T? Create(SdlInputBackend backend, uint id, THandle handle);
+ public void UpdateBounds();
+}
+
+internal abstract class SdlBoundedPointerTarget : IPointerTarget
+{
+ protected SdlBoundedPointerTarget(SdlInputBackend backend)
+ {
+ Backend = backend;
+ }
+
+ internal SdlInputBackend Backend { get; }
+ private Box2D Bounds2D
+ {
+ get
+ {
+ var bounds = Bounds;
+ return new Box2D(bounds.Min.X, bounds.Min.Y, bounds.Max.X, bounds.Max.Y);
+ }
+ }
+
+ public Box3D Bounds { get; private set; }
+
+ public void UpdateBounds() => Bounds = CalculateBounds();
+ protected abstract Box3D CalculateBounds();
+
+ ///
+ public int GetPointCount(IPointerDevice pointer) => PointerTargetExtensions.GetPointCount(this, pointer);
+
+ ///
+ public TargetPoint GetPoint(IPointerDevice pointer, int point) => PointerTargetExtensions.GetPoint(this, pointer, point);
+
+ public static unsafe Box2D CalculateAllDisplayBounds(ISdl sdl)
+ {
+ var displayCount = 0;
+ var displays = sdl.GetDisplays(&displayCount);
+ if (displays == nullptr)
+ {
+ // Looks like we can't support windowed mouse input.
+ sdl.ClearError();
+ return default;
+ }
+
+ if (displayCount == 0) // ???
+ {
+ sdl.Free(displays);
+ return default;
+ }
+
+ var bounds = new Box2D(float.MaxValue, float.MaxValue, float.MinValue, float.MinValue);
+
+ for (var i = 0; i < displayCount; i++)
+ {
+ var b = CalculateDisplayBounds(sdl, displays[i]);
+ bounds = bounds.ExtendBy(b);
+ }
+
+ sdl.Free(displays);
+ return default;
+ }
+
+ public static unsafe Box2D CalculateDisplayBounds(ISdl sdl, uint sdlDisplayId)
+ {
+ if (sdlDisplayId == 0)
+ {
+ // https://wiki.libsdl.org/SDL3/SDL_DisplayID
+ return default;
+ }
+
+ Rect rect = default;
+ var gotDisplayBounds = sdl.GetDisplayBounds(sdlDisplayId, &rect);
+ if (gotDisplayBounds == 0)
+ {
+ SdlLog.Error($"Failed to get display from ID {sdlDisplayId}.");
+ return default;
+ }
+
+ return new Box2D(rect.X, rect.Y, rect.X + rect.W, rect.Y + rect.H);
+ }
+
+ public static unsafe Box2D CalculateWindowBounds(ISdl sdl, WindowHandle window)
+ {
+ Vector2D windowSize = default;
+ var gotSize = sdl.GetWindowSize(window, &windowSize.X, &windowSize.Y);;
+ if (gotSize == 0)
+ {
+ SdlLog.Error("Failed to get window size for window.");
+ return default;
+ }
+
+ Vector2D windowPosition = default;
+ var gotPos = sdl.GetWindowPosition(window, &windowPosition.X, &windowPosition.Y);
+ if (gotPos == 0)
+ {
+ SdlLog.Error("Failed to get window position for window.");
+ return default;
+ }
+
+ var windowEndPos = windowPosition + windowSize;
+ return new Box2D(windowPosition.X, windowPosition.Y, windowEndPos.X, windowEndPos.Y);
+ }
+
+ public static Box2D CalculateWindowBounds(ISdl sdl, uint windowId)
+ {
+ var window = sdl.GetWindowFromID(windowId);
+ if (window == nullptr)
+ {
+ SdlLog.Error($"Failed to get window from ID {windowId}.");
+ return default;
+ }
+
+ return CalculateWindowBounds(sdl, window);
+ }
+
+ public void Dispose()
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/sources/Input/Input/Implementations/SDL3/Devices/Pointers/Targets/SdlDisplayTarget.cs b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/Targets/SdlDisplayTarget.cs
new file mode 100644
index 0000000000..e92a2e6323
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/Targets/SdlDisplayTarget.cs
@@ -0,0 +1,25 @@
+// 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.Devices.Pointers.Targets;
+
+internal sealed class SdlDisplayTarget : SdlBoundedPointerTarget, ISdlBoundedPointerTarget
+{
+ private SdlDisplayTarget(SdlInputBackend backend, SilkSdlDisplayHandle id) : base(backend)
+ {
+ Id = id.Id;
+ Handle = id;
+ }
+
+ protected override Box3D CalculateBounds()
+ {
+ var bounds2d = CalculateDisplayBounds(Backend.Sdl, Id);
+ return new Box3D(bounds2d.Min.X, bounds2d.Min.Y, 0, bounds2d.Max.X, bounds2d.Max.Y, 1);
+ }
+
+ public uint Id { get; }
+ public SilkSdlDisplayHandle Handle { get; }
+ public static SdlDisplayTarget? Create(SdlInputBackend backend, uint id, SilkSdlDisplayHandle handle) => new(backend, handle);
+}
diff --git a/sources/Input/Input/Implementations/SDL3/Devices/Pointers/Targets/SdlUnboundedPointerTarget.cs b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/Targets/SdlUnboundedPointerTarget.cs
new file mode 100644
index 0000000000..4b43cf3ddf
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/Targets/SdlUnboundedPointerTarget.cs
@@ -0,0 +1,45 @@
+// 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.Devices.Pointers.Targets;
+
+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) => PointerTargetExtensions.GetPointCount(this, pointer);
+
+ public TargetPoint GetPoint(IPointerDevice pointer, int pointIdx) => PointerTargetExtensions.GetPoint(this, pointer, 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, // in this case, should the position be provided at all?
+ NormalizedPosition: default,
+ Pointer: new Ray3D(),
+ Pressure: point.Pressure < 0 ? 0 : point.Pressure > 1 ? 1 : point.Pressure,
+ Target: this
+ );
+ }
+ */
+
+ // todo - do we really want to limit this to SDL devices? or to our specific sdl backend?
+ // i dont think so, but.... technically...
+ private bool IsValidDevice(IPointerDevice pointer) =>
+ pointer is SdlDevice device && device.Backend == backend &&
+ (device.Backend.CursorConfiguration.Mode & CursorModes.Unbounded) != 0;
+}
diff --git a/sources/Input/Input/Implementations/SDL3/Devices/Pointers/Targets/SdlWindowTarget.cs b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/Targets/SdlWindowTarget.cs
new file mode 100644
index 0000000000..ea9e6e0a3a
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/Targets/SdlWindowTarget.cs
@@ -0,0 +1,26 @@
+// 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.Devices.Pointers.Targets;
+
+internal sealed class SdlWindowTarget : SdlBoundedPointerTarget, ISdlBoundedPointerTarget
+{
+ private SdlWindowTarget(SdlInputBackend backend, uint id, WindowHandle handle) : base(backend)
+ {
+ Id = id;
+ Handle = handle;
+ }
+
+ protected override Box3D CalculateBounds()
+ {
+ var bounds2d = SdlBoundedPointerTarget.CalculateWindowBounds(Backend.Sdl, Handle);
+ return new Box3D(bounds2d.Min.X, bounds2d.Min.Y, 0, bounds2d.Max.X, bounds2d.Max.Y, 1);
+ }
+
+ public uint Id { get; }
+ public WindowHandle Handle { get; }
+ public static SdlWindowTarget? Create(SdlInputBackend backend, uint id, WindowHandle handle) => new(backend, id, handle);
+}
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..bbc9f1a4cc
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/Devices/SdlDevice.cs
@@ -0,0 +1,65 @@
+// 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.Devices.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 as SdlDevice)!.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/Devices/SilkSdlDisplayHandle.cs b/sources/Input/Input/Implementations/SDL3/Devices/SilkSdlDisplayHandle.cs
new file mode 100644
index 0000000000..a7d65afe56
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/Devices/SilkSdlDisplayHandle.cs
@@ -0,0 +1,11 @@
+// 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.Devices.Pointers.Targets;
+
+internal readonly struct SilkSdlDisplayHandle
+{
+ public static readonly SilkSdlDisplayHandle AllDisplays = new(uint.MaxValue);
+ public uint Id { get; }
+ public SilkSdlDisplayHandle(uint id) => Id = id;
+}
diff --git a/sources/Input/Input/Implementations/SDL3/Extensions/BackendExtensions.cs b/sources/Input/Input/Implementations/SDL3/Extensions/BackendExtensions.cs
new file mode 100644
index 0000000000..1c464f23ea
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/Extensions/BackendExtensions.cs
@@ -0,0 +1,65 @@
+// 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
+{
+ extension(SdlInputBackend backend)
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public nint FallbackUniqueId(uint sdlDeviceId, nint uniqueId)
+ {
+ Console.Error.WriteLine("Failed to create a deterministically unique identifier for joystick");
+ return uniqueId ^ ((nint)sdlDeviceId | ((nint)sdlDeviceId << 16));
+ }
+
+ public bool AttemptUniqueId(Ptr ptr, ref nint uniqueId1)
+ {
+ if (ptr.Native == null)
+ return false;
+
+ var name = ptr.ReadToString();
+ var bytes = Encoding.Default.GetBytes(name);
+ return AttemptUniqueId(backend, bytes, ref uniqueId1);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool AttemptUniqueId(T ptr, ref nint uniqueId1)
+ where T : unmanaged =>
+ AttemptUniqueId(backend, new ReadOnlySpan(&ptr, sizeof(T)), ref uniqueId1);
+
+ public bool AttemptUniqueId(ReadOnlySpan bytes, ref nint uniqueId1)
+ {
+ uniqueId1 = Modify(uniqueId1, bytes);
+ return backend.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/Extensions/InputWindowExtensions.cs b/sources/Input/Input/Implementations/SDL3/Extensions/InputWindowExtensions.cs
new file mode 100644
index 0000000000..bd515dc85e
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/Extensions/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/Extensions/MathExtensions.cs b/sources/Input/Input/Implementations/SDL3/Extensions/MathExtensions.cs
new file mode 100644
index 0000000000..f0b8c943df
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/Extensions/MathExtensions.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.
+
+using System.Runtime.CompilerServices;
+using Silk.NET.Maths;
+
+namespace Silk.NET.Input.SDL3.Devices.Pointers;
+
+internal static class MathExtensions
+{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Box2D ExtendBy(this Box2D box, Box2D other) => new(Scalar.Min(box.Min, other.Min), Scalar.Max(box.Max, other.Max));
+}
diff --git a/sources/Input/Input/Implementations/SDL3/Extensions/PointerTargetExtensions.cs b/sources/Input/Input/Implementations/SDL3/Extensions/PointerTargetExtensions.cs
new file mode 100644
index 0000000000..34032ea4ba
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/Extensions/PointerTargetExtensions.cs
@@ -0,0 +1,77 @@
+// 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 System.Runtime.CompilerServices;
+using Silk.NET.Maths;
+
+namespace Silk.NET.Input.SDL3.Devices.Pointers.Targets;
+
+internal static class PointerTargetExtensions
+{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool AppliesTo(this IPointerDevice device, IPointerTarget target) => device.Targets.Contains(target);
+
+ extension(IPointerTarget target)
+ {
+ ///
+ /// Normalizes a 2D position to the range [0, 1] in both dimensions.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Vector3 SimpleNormalizedPosition(in Vector3 input) =>
+ SimpleRelativePosition(target, input) / target.Bounds.Size.ToSystem();
+
+ public Vector3 SimpleRelativePosition(in Vector3 input)
+ {
+ var bounds = target.Bounds;
+ if (Scalar.IsInfinity(bounds.Min) || Scalar.IsInfinity(bounds.Max))
+ {
+ throw new InvalidOperationException("Target bounds are infinite.");
+ }
+
+ return input - bounds.Min.ToSystem();
+ }
+
+ ///
+ /// A default implementation of
+ /// that iterates over all points.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public int GetPointCount(IPointerDevice pointer)
+ {
+ var points = pointer.State.Points;
+ var count = 0;
+ var pointerPointsCount = points.Count;
+ for (var i = 0; i < pointerPointsCount; i++)
+ {
+ if (points[i].Target == target)
+ {
+ ++count;
+ }
+ }
+
+ return count;
+ }
+
+ ///
+ /// A default implementation of
+ /// that iterates over all points.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public TargetPoint GetPoint(IPointerDevice pointer, int point)
+ {
+ var points = pointer.State.Points;
+ var pointerPointsCount = points.Count;
+ for (var i = 0; i < pointerPointsCount; i++)
+ {
+ var targetPoint = points[i];
+ if (targetPoint.Target == target && point-- == 0)
+ {
+ return targetPoint;
+ }
+ }
+
+ return default;
+ }
+ }
+}
diff --git a/sources/Input/Input/Implementations/SDL3/InputLog.cs b/sources/Input/Input/Implementations/SDL3/InputLog.cs
new file mode 100644
index 0000000000..7e181e5982
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/InputLog.cs
@@ -0,0 +1,36 @@
+// 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.Runtime.CompilerServices;
+
+namespace Silk.NET.Input.SDL3;
+
+internal static class InputLog
+{
+ [Conditional("DEBUG")]
+ public static void Error(string? message = null,
+ [CallerFilePath] string? path = null,
+ [CallerLineNumber] int line = 0,
+ [CallerMemberName] string? member = null)
+ {
+ var log = GenerateLog(message, path, line, member);
+ Console.Error.WriteLine(log);
+ }
+
+ private static string GenerateLog(string? message, string? path, int line, string? member)
+ {
+ const string traceFmt = "{0} at {1}:{2}";
+ return $"{message} ({string.Format(traceFmt, member, path, line)})";
+ }
+
+ [Conditional("DEBUG")]
+ public static void Debug(string? message = null,
+ [CallerFilePath] string? path = null,
+ [CallerLineNumber] int line = 0,
+ [CallerMemberName] string? member = null)
+ {
+ var log = GenerateLog(message, path, line, member);
+ Console.WriteLine(log);
+ }
+}
diff --git a/sources/Input/Input/Implementations/SDL3/SdlArray.cs b/sources/Input/Input/Implementations/SDL3/SdlArray.cs
new file mode 100644
index 0000000000..6f2dd379bc
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/SdlArray.cs
@@ -0,0 +1,130 @@
+// 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;
+using System.Runtime.CompilerServices;
+using Silk.NET.SDL;
+
+namespace Silk.NET.Input.SDL3;
+
+// note - this probably doesn't need to be a ref struct, but is bc that's the extents
+// of the struct's current use cases
+// todo - can this struct be represented as a NativeArray where TBackend has a
+// Free(void*) or Free(void*, int) method?
+internal readonly unsafe ref struct SdlArray : IDisposable, IEquatable>, IReadOnlyList where T : unmanaged
+{
+ private readonly Ptr _ptr;
+ public int Count { get; }
+ private readonly ISdl? _sdl;
+
+ [MemberNotNullWhen(true, nameof(_sdl))]
+ private bool CanDispose { get; }
+
+ public static SdlArray Null => default;
+
+ public SdlArray(Ptr ptr, int count, ISdl? sdl, bool consumerCanDispose = true)
+ {
+ if (consumerCanDispose)
+ {
+ ArgumentNullException.ThrowIfNull(ptr.Native);
+ ArgumentNullException.ThrowIfNull(sdl);
+ }
+
+ ArgumentOutOfRangeException.ThrowIfNegative(count);
+
+ _ptr = ptr;
+ Count = count;
+ _sdl = sdl;
+ CanDispose = consumerCanDispose;
+ }
+
+ public void Dispose()
+ {
+ if (!CanDispose)
+ {
+ return;
+ }
+
+ if (_ptr.Native == null)
+ {
+ return;
+ }
+
+ _sdl.Free(_ptr.Native);
+ }
+
+ public static implicit operator Ptr(SdlArray array) => array._ptr;
+ public static implicit operator Span(SdlArray array) => array.AsSpan();
+ public static implicit operator ReadOnlySpan(SdlArray array) => array.AsReadOnlySpan();
+
+ // equality operators against null and other collections
+ public static bool operator ==(SdlArray left, SdlArray right) => left._sdl == right._sdl && left._ptr == right._ptr;
+ public static bool operator !=(SdlArray left, SdlArray right) => left._sdl == right._sdl && left._ptr != right._ptr;
+ public static bool operator ==(SdlArray left, NullPtr right) => left._ptr == right;
+ public static bool operator !=(SdlArray left, NullPtr right) => left._ptr != right;
+
+ public bool IsNull => _ptr.Native == null;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Span AsSpan() => _ptr.Native == null ? default : new Span(_ptr.Native, Count);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public ReadOnlySpan AsReadOnlySpan() => _ptr.Native == null ? default : new ReadOnlySpan(_ptr.Native, Count);
+
+ T IReadOnlyList.this[int index] => this[index];
+
+ public ref T this[int index]
+ {
+ get
+ {
+ if (_ptr.Native == null)
+ {
+ throw new NullReferenceException();
+ }
+
+ if (Count <= index)
+ {
+ throw new IndexOutOfRangeException(nameof(index));
+ }
+
+ ArgumentOutOfRangeException.ThrowIfNegative(index);
+
+ return ref _ptr.Native[index];
+ }
+ }
+
+ public ref T this[uint index]
+ {
+ get
+ {
+ if (_ptr.Native == null)
+ {
+ throw new NullReferenceException();
+ }
+
+ if (Count <= index)
+ {
+ throw new IndexOutOfRangeException(nameof(index));
+ }
+
+ return ref _ptr.Native[index];
+ }
+ }
+
+ public bool Equals(SdlArray other) => this == other;
+
+ public IEnumerator GetEnumerator()
+ {
+ if (_ptr == nullptr)
+ {
+ throw new NullReferenceException();
+ }
+
+ return (IEnumerator)AsReadOnlySpan().ToArray().GetEnumerator();
+ }
+
+ public override bool Equals(object? obj) => obj == null && _ptr.Native == null;
+ public override int GetHashCode() => HashCode.Combine(_ptr, Count, _sdl);
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+}
diff --git a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.Targets.cs b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.Targets.cs
new file mode 100644
index 0000000000..4178e9e4a3
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.Targets.cs
@@ -0,0 +1,169 @@
+// 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.Input.SDL3.Devices.Pointers.Targets;
+using Silk.NET.SDL;
+
+namespace Silk.NET.Input.SDL3;
+
+internal partial class SdlInputBackend
+{
+ private readonly List _windowTargets = [];
+ private readonly List _displayTargets = [];
+ private delegate SdlArray GetHandlesCallback(ISdl sdl) where T : unmanaged;
+ private readonly List _tempTargets = new();
+
+ private readonly GetHandlesCallback _getWindowHandles;
+ private readonly GetHandlesCallback _getDisplayHandles;
+ private readonly Func _getWindowId;
+ private readonly Func