diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 03efa87b74..393d187924 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,10 +3,11 @@ "isRoot": true, "tools": { "csharpier": { - "version": "0.29.2", + "version": "0.30.6", "commands": [ "dotnet-csharpier" - ] + ], + "rollForward": false } } } \ No newline at end of file diff --git a/Silk.NET.sln b/Silk.NET.sln index 17850fdc6f..364d408733 100644 --- a/Silk.NET.sln +++ b/Silk.NET.sln @@ -102,6 +102,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Windowing", "Windowing", "{ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Silk.NET.Windowing", "sources\Windowing\Windowing\Silk.NET.Windowing.csproj", "{EF07CBB5-D253-4CA9-A5DA-8B3DF2B0DF8E}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Input", "Input", "{33ED9765-8C36-4A9D-95E8-AF037FE104B3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Silk.NET.Input", "sources\Input\Input\Silk.NET.Input.csproj", "{49A42CE3-94C5-4239-B0FC-F1FF8D7AAADA}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Input", "Input", "{4E0EF53A-76BC-4729-8E3B-4768E86E357E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Silk.NET.Input.UnitTests", "tests\Input\Input\Silk.NET.Input.UnitTests.csproj", "{00B9B6E6-776E-480C-B3ED-D6420C5B4E8E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -168,6 +176,14 @@ Global {EF07CBB5-D253-4CA9-A5DA-8B3DF2B0DF8E}.Debug|Any CPU.Build.0 = Debug|Any CPU {EF07CBB5-D253-4CA9-A5DA-8B3DF2B0DF8E}.Release|Any CPU.ActiveCfg = Release|Any CPU {EF07CBB5-D253-4CA9-A5DA-8B3DF2B0DF8E}.Release|Any CPU.Build.0 = Release|Any CPU + {49A42CE3-94C5-4239-B0FC-F1FF8D7AAADA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {49A42CE3-94C5-4239-B0FC-F1FF8D7AAADA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {49A42CE3-94C5-4239-B0FC-F1FF8D7AAADA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {49A42CE3-94C5-4239-B0FC-F1FF8D7AAADA}.Release|Any CPU.Build.0 = Release|Any CPU + {00B9B6E6-776E-480C-B3ED-D6420C5B4E8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {00B9B6E6-776E-480C-B3ED-D6420C5B4E8E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {00B9B6E6-776E-480C-B3ED-D6420C5B4E8E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {00B9B6E6-776E-480C-B3ED-D6420C5B4E8E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -200,6 +216,10 @@ Global {F16C0AB9-DE7E-4C09-9EE9-DAA8B8E935A6} = {EC4D7B06-D277-4411-BD7B-71A6D37683F0} {FE4414F8-5370-445D-9F24-C3AD3223F299} = {DD29EA8F-B1A6-45AA-8D2E-B38DA56D9EF6} {EF07CBB5-D253-4CA9-A5DA-8B3DF2B0DF8E} = {FE4414F8-5370-445D-9F24-C3AD3223F299} + {33ED9765-8C36-4A9D-95E8-AF037FE104B3} = {DD29EA8F-B1A6-45AA-8D2E-B38DA56D9EF6} + {49A42CE3-94C5-4239-B0FC-F1FF8D7AAADA} = {33ED9765-8C36-4A9D-95E8-AF037FE104B3} + {4E0EF53A-76BC-4729-8E3B-4768E86E357E} = {A5578D12-9E77-4647-8C22-0DBD17760BFF} + {00B9B6E6-776E-480C-B3ED-D6420C5B4E8E} = {4E0EF53A-76BC-4729-8E3B-4768E86E357E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {78D2CF6A-60A1-43E3-837B-00B73C9DA384} diff --git a/docs/silk.net/diagnostics/ST0001.md b/docs/silk.net/diagnostics/ST0001.md new file mode 100644 index 0000000000..f04906d857 --- /dev/null +++ b/docs/silk.net/diagnostics/ST0001.md @@ -0,0 +1,21 @@ +# ST0001 - ProcessClass failure + +## Overview + +This internal error was raised by SilkTouch when failing to generate an implementation for a binding at source +generation time. It provided details regarding the exception that led to the entire native API class failing to have its +implementation generated. + +| Attribute | Value | +|--------------------|----------------------| +| Diagnostic ID | ST0001 | +| Title | ProcessClass failure | +| Category | SilkTouch.Internal | +| Default Severity | Error | +| Enabled by Default | Yes | + +Example message: `ProcessClass failed. Exception: '...'` + +## Explanation & Solutions + +This functionality is no longer supported in 3.0, where this diagnostic is never raised. diff --git a/docs/silk.net/diagnostics/ST0002.md b/docs/silk.net/diagnostics/ST0002.md new file mode 100644 index 0000000000..89742a49a7 --- /dev/null +++ b/docs/silk.net/diagnostics/ST0002.md @@ -0,0 +1,21 @@ +# ST0002 - MethodClass failure + +## Overview + +This internal error was raised by SilkTouch when failing to generate an implementation for a binding at source +generation time. It provided details regarding the exception that led to a specific native API method failing to have +its implementation generated. + +| Attribute | Value | +|--------------------|---------------------| +| Diagnostic ID | ST0002 | +| Title | MethodClass failure | +| Category | SilkTouch.Internal | +| Default Severity | Error | +| Enabled by Default | Yes | + +Example message: `MethodClass failed. Exception: '...'` + +## Explanation & Solutions + +This functionality is no longer supported in 3.0, where this diagnostic is never raised. diff --git a/docs/silk.net/diagnostics/ST0003.md b/docs/silk.net/diagnostics/ST0003.md new file mode 100644 index 0000000000..d722f5f6d7 --- /dev/null +++ b/docs/silk.net/diagnostics/ST0003.md @@ -0,0 +1,20 @@ +# ST0003 - Silk.NET.Core is Missing + +## Overview + +This internal diagnostic was raised by SilkTouch when failing to generate an implementation for bindings at source +generation time due to the binding project missing a reference to Silk.NET.Core. + +| Attribute | Value | +|--------------------|---------------------| +| Diagnostic ID | ST0003 | +| Title | MethodClass failure | +| Category | SilkTouch.Internal | +| Default Severity | Info | +| Enabled by Default | Yes | + +Example message: `Silk.NET.Core is missing from references. You should use SilkTouch with Silk.NET.Core` + +## Explanation & Solutions + +This functionality is no longer supported in 3.0, where this diagnostic is never raised. diff --git a/docs/silk.net/diagnostics/ST0004.md b/docs/silk.net/diagnostics/ST0004.md new file mode 100644 index 0000000000..4680d3faa3 --- /dev/null +++ b/docs/silk.net/diagnostics/ST0004.md @@ -0,0 +1,20 @@ +# ST0004 - Build Info + +## Overview + +This internal diagnostic was raised by SilkTouch when configured to do so. It provided diagnostic information relating +to the performance and characteristics of SilkTouch's internals. + +| Attribute | Value | +|--------------------|--------------------| +| Diagnostic ID | ST0004 | +| Title | Build Info | +| Category | SilkTouch.Internal | +| Default Severity | Warning | +| Enabled by Default | Yes | + +Example message: `GCSlotCount: '127'. Time: '6437ms'` + +## Explanation & Solutions + +This functionality is no longer supported in 3.0, where this diagnostic is never raised. diff --git a/docs/silk.net/diagnostics/ST0005.md b/docs/silk.net/diagnostics/ST0005.md new file mode 100644 index 0000000000..8a7c730766 --- /dev/null +++ b/docs/silk.net/diagnostics/ST0005.md @@ -0,0 +1,15 @@ +# ST0005 - Intentionally Unstable API + +## Overview + +This diagnostic is raised when trying to use a Silk.NET API that has been marked with the `Experimental` attribute due +to its API and/or ABI being unstable. When this diagnostic ID is used, it indicates that it is intentional that this is +the case and that this API is extremely unlikely to ever graduate to a stable, versioned API. + +## Explanation & Solutions + +Typically, APIs meeting this description are internal APIs and are not intended for use outside of the assembly they're +defined in. As a result, where this diagnostic is raised, you should cease use of this API or at least only continue if +you can guarantee that you will never update Silk.NET ever again and that your downstream consumers, if applicable, will +lock their version to the same version referenced by your project. However, please reconsider use of the API if this is +the case. diff --git a/eng/build/Silk.NET.NUKE.csproj b/eng/build/Silk.NET.NUKE.csproj index 1f3ae0857e..2a1f1a1c28 100644 --- a/eng/build/Silk.NET.NUKE.csproj +++ b/eng/build/Silk.NET.NUKE.csproj @@ -18,4 +18,10 @@ <PackageReference Include="Octokit" Version="13.0.1" /> <PackageReference Include="System.Linq.Async" Version="6.0.1" /> </ItemGroup> + + <ItemGroup> + <Content Include="..\..\tests\Input\Input\Silk.NET.Input.UnitTests.csproj"> + <Link>Directory.Build\tests\Input\Input\Silk.NET.Input.UnitTests.csproj</Link> + </Content> + </ItemGroup> </Project> 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; + +/// <summary> +/// Represents a button the user can push. +/// </summary> +/// <param name="Name">The name of the button.</param> +/// <param name="IsDown">Whether the user is pushing the button.</param> +/// <param name="Pressure"> +/// The pressure with which the user is pushing the button, where <c>0.0</c> is the smallest measurable pressure and +/// <c>1.0</c> is the largest measurable pressure. +/// </param> +/// <typeparam name="T"> +/// The button type (e.g. <see cref="JoystickButton"/>, <see cref="PointerButton"/>, etc). +/// </typeparam> +public readonly record struct Button<T>(T Name, bool IsDown, float Pressure) + where T : unmanaged, Enum +{ + /// <summary> + /// Collapses this <see cref="Button{T}"/> struct into just its <see cref="IsDown"/> value. + /// </summary> + /// <param name="state">The button state.</param> + /// <returns>The <see cref="IsDown"/> value.</returns> + public static implicit operator bool(Button<T> 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; + +/// <summary> +/// Contains information pertaining to a button state change (e.g. press, depress, etc). +/// </summary> +/// <param name="Device">The device on which the button being pressed or depressed resides.</param> +/// <param name="Timestamp"> +/// The timestamp (as retrieved from <see cref="Stopwatch.GetTimestamp"/>) at which the event occurred. +/// </param> +/// <param name="Button">The new state of the button being pressed or depressed.</param> +/// <param name="Previous">The previous state of the button.</param> +/// <typeparam name="T">The button type e.g. <see cref="JoystickButton"/>, <see cref="PointerButton"/>, etc.</typeparam> +public readonly record struct ButtonChangedEvent<T>( + IButtonDevice<T> Device, + long Timestamp, + Button<T> Button, + Button<T> 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..f93504197c --- /dev/null +++ b/sources/Input/Input/ButtonReadOnlyList.cs @@ -0,0 +1,43 @@ +using System.Collections; + +namespace Silk.NET.Input; + +/// <summary> +/// An implementation of <see cref="IReadOnlyList{T}"/> providing utility APIs for getting a <see cref="Button{T}"/> +/// given a button name <typeparamref name="T"/>, that is optimised for storing <see cref="Button{T}"/>s with the +/// given button name type <typeparamref name="T"/> using the most memory-efficient mechanism available. +/// </summary> +/// <typeparam name="T"> +/// The button type (e.g. <see cref="JoystickButton"/>, <see cref="PointerButton"/>, etc). +/// </typeparam> +public struct ButtonReadOnlyList<T> : IReadOnlyList<Button<T>> + where T : unmanaged, Enum +{ + private InputReadOnlyList<Button<T>> _list; + + internal ButtonReadOnlyList(InputReadOnlyList<Button<T>> list) => _list = list; + + /// <summary> + /// Creates an <see cref="ButtonReadOnlyList{T}"/> from a <see cref="IReadOnlyList{T}"/>. + /// </summary> + /// <param name="other">The list to copy.</param> + public ButtonReadOnlyList(IReadOnlyList<Button<T>> other) => + InputMarshal.Clone(other).List.AsButtonList(); + + /// <summary> + /// Gets the state for the button with the given name. + /// </summary> + /// <param name="name">The button name.</param> + public Button<T> this[T name] => InputMarshal.GetButtonState(_list, name); + + /// <inheritdoc /> + public IEnumerator<Button<T>> GetEnumerator() => _list.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// <inheritdoc /> + public int Count => _list.Count; + + /// <inheritdoc /> + public Button<T> this[int index] => _list[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; + +/// <summary> +/// Contains information pertaining to a device connection or disconnection event. +/// </summary> +/// <param name="Device">The device that has disconnected or connected.</param> +/// <param name="Timestamp"> +/// The timestamp (as retrieved from <see cref="Stopwatch.GetTimestamp"/>) at which the event occurred. +/// </param> +/// <param name="IsConnected">Whether the device has connected (<c>true</c>) or disconnected (<c>false</c>).</param> +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; + +/// <summary> +/// Enumerates the modes in which a mouse cursor can operate. +/// </summary> +/// <remarks> +/// <see cref="IPointerDevice"/> implementations for <see cref="IMouse"/> implementations typically have two +/// <see cref="IPointerDevice.Targets"/>: +/// <list type="bullet"> +/// <item> +/// <term>Bounded <see cref="IPointerTarget"/></term> +/// <description> +/// An <see cref="IPointerTarget"/> that is bounded to the desktop environment i.e. the +/// <see cref="IPointerTarget.Bounds"/> 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 <see cref="TargetPoint"/>. +/// This target is used for every cursor mode except <see cref="Unbounded"/>. +/// </description> +/// </item> +/// <item> +/// <term>Unbounded <see cref="IPointerTarget"/></term> +/// <description> +/// An <see cref="IPointerTarget"/> that is unbounded and operates in an arbitrary coordinate space. This target is used +/// for <b>raw mouse mode</b> and points on this target represent the net mouse movement from a mouse. Implementations +/// are more likely to be able to give multiple <see cref="TargetPoint"/>s for each mouse when this target is used. This +/// target is used when the <see cref="Unbounded"/> cursor mode is enabled. <see cref="IPointerTarget.Bounds"/> will +/// represent an infinitely large unbounded target. +/// </description> +/// </item> +/// </list> +/// </remarks> +[Flags] +public enum CursorModes +{ + /// <summary> + /// The cursor is visible to the user and operating within the bounds of the <b>desktop environment</b>. The + /// coordinates received are in desktop coordinates, operating in the same coordinate space as the window + /// position/size. + /// </summary> + Normal = 1 << 0, + + /// <summary> + /// The cursor is visible to the user but is constrained to the <b>window's client area</b>. The coordinates + /// received are in desktop coordinates, operating in the same coordinate space as the window position/size. + /// The <see cref="IPointerTarget"/> bounded to the desktop environment is used. + /// </summary> + Confined = 1 << 1, + + /// <summary> + /// The cursor is invisible to the user and is <b>unconstrained/unbounded</b>. The coordinates received are + /// arbitrary values that have no bounds representing the net mouse movement since entering into this cursor mode. + /// The unbounded <see cref="IPointerTarget"/> is used. This is the equivalent of <b>raw mouse mode</b>. + /// </summary> + 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; + +/// <summary> +/// Enumerates the cursor styles with which the desktop environment should render the cursor. +/// </summary> +[Flags] +public enum CursorStyles +{ + /// <summary> + /// The cursor should be rendered using its default image. + /// </summary> + Default, + + /// <summary> + /// The cursor should be rendered using an arrow cursor image. + /// </summary> + Arrow = 1 << 0, + + /// <summary> + /// 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. + /// </summary> + IBeam = 1 << 1, + + /// <summary> + /// The cursor should be rendered using a crosshair cursor image. + /// </summary> + Crosshair = 1 << 2, + + /// <summary> + /// The cursor should be rendered using a hand cursor image, typically used when hovering over a web link. + /// </summary> + Hand = 1 << 3, + + /// <summary> + /// The cursor should be rendered using a two-headed horizontal sizing cursor image. + /// </summary> + HResize = 1 << 4, + + /// <summary> + /// The cursor should be rendered using a two-headed vertical sizing cursor image. + /// </summary> + VResize = 1 << 5, + + /// <summary> + /// The cursor should not be rendered. + /// </summary> + /// <remarks> + /// When <see cref="CursorModes.Unbounded"/> is used, the cursor ceases to exist anyway. As such, while the + /// <see cref="ICursorConfiguration.Style"/> property may not reflect this (as it is retained across changes to + /// <see cref="ICursorConfiguration.Mode"/> and just ignored when <see cref="CursorModes.Unbounded"/> is used), + /// <see cref="ICursorConfiguration.Style"/> can be implied as being <see cref="CursorStyles.Hidden"/> when + /// <see cref="CursorModes.Unbounded"/> is used. + /// </remarks> + Hidden = 1 << 6, + + /// <summary> + /// The cursor should be rendered using a custom application-provided image. + /// </summary> + 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..8780f934af --- /dev/null +++ b/sources/Input/Input/CustomCursor.cs @@ -0,0 +1,22 @@ +namespace Silk.NET.Input; + +/// <summary> +/// Represents a custom image for a mouse cursor. +/// </summary> +public readonly ref struct CustomCursor +{ + /// <summary> + /// The number of pixels in the X axis. + /// </summary> + public int Width { get; init; } + + /// <summary> + /// The number of pixels in the Y axis. + /// </summary> + public int Height { get; init; } + + /// <summary> + /// The row-major 32-bit RGBA pixel data (i.e. 8 bytes for each colour component). + /// </summary> + public ReadOnlySpan<int> Data { get; init; } // Rgba32 +} \ No newline at end of file diff --git a/sources/Input/Input/DualReadOnlyList.cs b/sources/Input/Input/DualReadOnlyList.cs new file mode 100644 index 0000000000..9639703823 --- /dev/null +++ b/sources/Input/Input/DualReadOnlyList.cs @@ -0,0 +1,39 @@ +using System.Collections; + +namespace Silk.NET.Input; + +/// <summary> +/// Represents a list that has exactly two elements. +/// </summary> +/// <typeparam name="T">The element type.</typeparam> +public readonly struct DualReadOnlyList<T> : IReadOnlyList<T> +{ + /// <summary> + /// The first/leftmost element. + /// </summary> + public readonly T Left; + + /// <summary> + /// The second/rightmost element. + /// </summary> + public readonly T Right; + + /// <inheritdoc /> + public IEnumerator<T> GetEnumerator() + { + yield return Left; + yield return Right; + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// <inheritdoc /> + public int Count => 2; + + /// <inheritdoc /> + public T this[int index] => index switch { + 0 => Left, + 1 => Right, + _ => throw new IndexOutOfRangeException() + }; +} \ No newline at end of file diff --git a/sources/Input/Input/GamepadState.cs b/sources/Input/Input/GamepadState.cs new file mode 100644 index 0000000000..05d0d9bb2a --- /dev/null +++ b/sources/Input/Input/GamepadState.cs @@ -0,0 +1,24 @@ +using System.Numerics; + +namespace Silk.NET.Input; + +/// <summary> +/// Contains user input received from an <see cref="IGamepad"/>. +/// </summary> +public class GamepadState +{ + /// <summary> + /// Gets the gamepad button state denoting the buttons being pressed or depressed. + /// </summary> + public ButtonReadOnlyList<JoystickButton> Buttons { get; } + + /// <summary> + /// Gets the state of the twin sticks on the gamepad. + /// </summary> + public DualReadOnlyList<Vector2> Thumbsticks { get; } + + /// <summary> + /// Gets the state of the triggers on the gamepad. + /// </summary> + public DualReadOnlyList<float> Triggers { get; } +} \ No newline at end of file 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; + +/// <summary> +/// Contains information pertaining to the movement of a thumbstick. +/// </summary> +/// <param name="Gamepad">The gamepad on which the thumbstick resides.</param> +/// <param name="Timestamp"> +/// The timestamp (as retrieved from <see cref="Stopwatch.GetTimestamp"/>) at which the event occurred. +/// </param> +/// <param name="Value"> +/// The new position of the thumbstick, where each axis is between <c>-1.0</c> and <c>1.0</c>. +/// </param> +/// <param name="Delta">The change in <see cref="Value"/> as a result of this event.</param> +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; + +/// <summary> +/// Contains information pertaining to the movement of a trigger. +/// </summary> +/// <param name="Gamepad">The gamepad on which the trigger resides.</param> +/// <param name="Timestamp"> +/// The timestamp (as retrieved from <see cref="Stopwatch.GetTimestamp"/>) at which the event occurred. +/// </param> +/// <param name="Axis">The index of the trigger that has moved.</param> +/// <param name="Value"> +/// The new value of the trigger, between <c>0.0</c> (fully depressed) and <c>1.0</c> (fully pressed). +/// </param> +/// <param name="Delta">The change in <see cref="Value"/> as a result of this event.</param> +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; + +/// <summary> +/// Represents a collection of <see cref="IGamepad"/>s from which input events can be received. +/// </summary> +public sealed class Gamepads : InputContextDeviceList<IGamepad>, IGamepadInputHandler +{ + internal Gamepads(InputContext ctx) + : base(ctx) { } + + /// <summary> + /// Raised when state pertaining to a pushable button on the gamepad changes (e.g. button up, button down). + /// </summary> + public event Action<ButtonChangedEvent<JoystickButton>>? ButtonChanged; + + /// <summary> + /// Raised when a thumbstick on the gamepad moves. + /// </summary> + public event Action<GamepadThumbstickMoveEvent>? ThumbstickMove; + + /// <summary> + /// Raised when a trigger on the gamepad moves. + /// </summary> + public event Action<GamepadTriggerMoveEvent>? TriggerMove; + + internal void HandleButtonChanged(ButtonChangedEvent<JoystickButton> @event) => + ButtonChanged?.Invoke(@event); + + void IButtonInputHandler<JoystickButton>.HandleButtonChanged( + ButtonChangedEvent<JoystickButton> @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; + +/// <summary> +/// Represents an input device that has buttons. +/// </summary> +/// <typeparam name="T">The type of buttons the input device has.</typeparam> +public interface IButtonDevice<T> : IInputDevice + where T : unmanaged, Enum +{ + /// <summary> + /// Gets the current button state for this device. + /// </summary> + /// <remarks> + /// Only updated when <see cref="IInputBackend.Update"/> is called. + /// </remarks> + ButtonReadOnlyList<T> 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; + +/// <summary> +/// An <see cref="IInputHandler"/> that also receives <see cref="IButtonDevice{T}"/> events. +/// </summary> +/// <typeparam name="T">The device's button type.</typeparam> +public interface IButtonInputHandler<T> : IInputHandler + where T : unmanaged, Enum +{ + /// <summary> + /// Called when a button's state changes (e.g. button down, button up). + /// </summary> + /// <param name="event">The event details.</param> + void HandleButtonChanged(ButtonChangedEvent<T> @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; + +/// <summary> +/// Configuration for the behaviour of a mouse cursor. +/// </summary> +public interface ICursorConfiguration +{ + /// <summary> + /// Gets a bitmask denoting the supported values for <see cref="Mode"/>. + /// </summary> + CursorModes SupportedModes { get; } + + /// <summary> + /// Gets or sets the current cursor mode. Only one bit shall be set at a time. + /// </summary> + /// <remarks> + /// Note that this property affects the <see cref="IPointerDevice.Targets"/> in use, see the + /// <see cref="CursorModes"/> documentation for more info. + /// </remarks> + CursorModes Mode { get; set; } + + /// <summary> + /// Gets a bitmask denoting the supported values for <see cref="Style"/>. + /// </summary> + CursorStyles SupportedStyles { get; } + + /// <summary> + /// Gets or sets the current cursor style. Only one bit shall be set at a time. <see cref="CursorStyles.Custom"/> + /// shall use the <see cref="Image"/> provided. + /// </summary> + /// <remarks> + /// When <see cref="CursorModes.Unbounded"/> is used, the cursor ceases to exist anyway. As such, while the + /// <see cref="ICursorConfiguration.Style"/> property may not reflect this (as it is retained across changes to + /// <see cref="ICursorConfiguration.Mode"/> and just ignored when <see cref="CursorModes.Unbounded"/> is used), + /// <see cref="ICursorConfiguration.Style"/> can be implied as being <see cref="CursorStyles.Hidden"/> when + /// <see cref="CursorModes.Unbounded"/> is used. + /// </remarks> + CursorStyles Style { get; set; } + + /// <summary> + /// Gets or sets the current custom cursor image. This has no effect if <see cref="CursorStyles.Custom"/> is not + /// used, but the value is stored nonetheless for use when that is the case. + /// </summary> + 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; + +/// <summary> +/// Represents a gamepad that follows a typical layout. +/// </summary> +public interface IGamepad : IButtonDevice<JoystickButton> +{ + /// <summary> + /// Gets the device state. + /// </summary> + /// <remarks> + /// Only updated when <see cref="IInputBackend.Update"/> is called. + /// </remarks> + new GamepadState State { get; } + ButtonReadOnlyList<JoystickButton> IButtonDevice<JoystickButton>.State => State.Buttons; + /// <summary> + /// Gets a collection enumerating the vibration motors available to the application to enable haptics. + /// </summary> + IReadOnlyList<IMotor> 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; + +/// <summary> +/// An <see cref="IInputHandler"/> that also receives <see cref="IGamepad"/> input. +/// </summary> +public interface IGamepadInputHandler : IButtonInputHandler<JoystickButton> +{ + /// <summary> + /// Called when one of the twin sticks moves. + /// </summary> + /// <param name="event">The event details.</param> + void HandleThumbstickMove(GamepadThumbstickMoveEvent @event); + + /// <summary> + /// Called when one of the two triggers moves. + /// </summary> + /// <param name="event">The event details.</param> + 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; + +/// <summary> +/// Represents an input backend capable of receiving human input from Human Input Devices (HIDs). +/// </summary> +/// <remarks> +/// 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 <see cref="Update"/> can be called +/// on - the user is responsible for respecting these threading rules as well. +/// </remarks> +public interface IInputBackend : IDisposable +{ + /// <summary> + /// Gets a rough human-readable description of the input backend. Its value is not intrinsically meaningful. + /// </summary> + string Name { get; } + + /// <summary> + /// Gets a globally-unique integral identifier for this device. + /// </summary> + nint Id { get; } + + /// <summary> + /// Get a list containing all the <b>connected</b> devices available from this input backend. + /// </summary> + /// <remarks> + /// When a device is disconnected, its <see cref="IInputDevice"/> shall no longer function and will not be + /// enumerated by this list. When a device is connected, an <see cref="IInputDevice"/> with that physical device ID + /// shall be added to this list. In addition, upon connection any past <see cref="IInputDevice"/> objects previously + /// enumerated by this list on this <see cref="IInputBackend"/> 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 <see cref="IInputDevice"/> 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 <see cref="Devices"/> (checked using <see cref="IInputDevice"/>s + /// <see cref="IEquatable{IInputDevice}"/> implementation) list is sufficient evidence that a device has been + /// disconnected. + /// </remarks> + IReadOnlyList<IInputDevice> Devices { get; } + + /// <summary> + /// Polls and updates the state of the <see cref="IInputDevice"/> objects connected using this backend, sending + /// input events to the given <see cref="IInputHandler"/> to reflect the human input received. + /// </summary> + /// <remarks> + /// The value of the <c>State</c> properties on each device must not change until this method is called. + /// </remarks> + /// <param name="handler">The input handler.</param> + 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; + +/// <summary> +/// Represents a connected Human Input Device (HID). +/// </summary> +/// <remarks> +/// All devices originate from a backend.<br /> +/// <br /> +/// An <see cref="IInputDevice"/> object shall be equatable to any such object retrieved from the same backend where +/// <see cref="Id"/> is equal.<br /> +/// <br /> +/// <see cref="IInputDevice"/> 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 <see cref="IInputDevice"/> objects are equatable based on <see cref="Id"/>s, if a physical +/// device disconnects and reconnects the <see cref="IInputBackend"/> does not provide a guarantee that the same object +/// will be returned (primarily because doing so would require the <see cref="IInputBackend"/> 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 <see cref="IInputBackend"/> +/// implementation instead in the form of a mapping of physical device IDs (<see cref="Id"/>) to those handles. This +/// solves the object lifetime problem while also not adding undue complications to user code. +/// </remarks> +public interface IInputDevice : IEquatable<IInputDevice> +{ + /// <summary> + /// Gets a globally-unique integral identifier for this device. + /// </summary> + nint Id { get; } + + /// <summary> + /// Gets a rough human-readable description of the input device. Its value is not intrinsically meaningful. + /// </summary> + 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; + +/// <summary> +/// 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. +/// </summary> +public interface IInputHandler +{ + /// <summary> + /// Called when an <see cref="IInputDevice"/> disconnects from the application. + /// </summary> + /// <param name="event">The event details.</param> + 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; + +/// <summary> +/// Represents a joystick with axes, buttons, and hats. +/// </summary> +public interface IJoystick : IButtonDevice<JoystickButton> +{ + /// <summary> + /// Gets the device state. + /// </summary> + /// <remarks> + /// Only updated when <see cref="IInputBackend.Update"/> is called. + /// </remarks> + new JoystickState State { get; } + ButtonReadOnlyList<JoystickButton> IButtonDevice<JoystickButton>.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; + +/// <summary> +/// An <see cref="IInputHandler"/> that also receives <see cref="IJoystick"/> input. +/// </summary> +public interface IJoystickInputHandler : IButtonInputHandler<JoystickButton> +{ + /// <summary> + /// Called when an axis on the joystick moves. + /// </summary> + /// <param name="event">The event details.</param> + void HandleAxisMove(JoystickAxisMoveEvent @event); + + /// <summary> + /// Called when a hat on the joystick moves. + /// </summary> + /// <param name="event">The event details.</param> + 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; + +/// <summary> +/// Represents a keyboard device. +/// </summary> +public interface IKeyboard : IButtonDevice<KeyName> +{ + /// <summary> + /// Gets the device state. + /// </summary> + /// <remarks> + /// Only updated when <see cref="IInputBackend.Update"/> is called. + /// </remarks> + new KeyboardState State { get; } + + ButtonReadOnlyList<KeyName> IButtonDevice<KeyName>.State => State.Keys; + + /// <summary> + /// Gets or sets the current text on the clipboard. + /// </summary> + string? ClipboardText { get; set; } + + /// <summary> + /// Attempts to get a user-displayable string in the user's locale for the key at the physical position represented + /// by <paramref name="key"/> in the user's current keyboard layout. + /// </summary> + /// <param name="key">The physical key name. Consult <see cref="KeyName"/> documentation for more info.</param> + /// <param name="name">The user-displayable name of the key.</param> + /// <returns>Whether the name was successfully retrieved.</returns> + bool TryGetKeyName(KeyName key, [NotNullWhen(true)] out string? name); + + /// <summary> + /// Begins recording keyboard input. Without <see cref="BeginInput"/>/<see cref="EndInput"/>, there is no + /// guarantee that <see cref="IKeyboardInputHandler.HandleKeyChar"/> will be raised as this might require displaying + /// a concept/touchscreen keyboard on certain platforms (e.g. phones). It is recommended that you call + /// <see cref="BeginInput"/> when you'd like to capture text input (e.g. in a text box), followed by + /// <see cref="EndInput"/> when you have completed collecting such input. + /// </summary> + void BeginInput(); + + /// <summary> + /// Concludes recording keyboard input. Without <see cref="BeginInput"/>/<see cref="EndInput"/>, there is no + /// guarantee that <see cref="IKeyboardInputHandler.HandleKeyChar"/> will be raised as this might require displaying + /// a concept/touchscreen keyboard on certain platforms (e.g. phones). It is recommended that you call + /// <see cref="BeginInput"/> when you'd like to capture text input (e.g. in a text box), followed by + /// <see cref="EndInput"/> when you have completed collecting such input. + /// </summary> + 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; + +/// <summary> +/// An <see cref="IInputHandler"/> that also receives <see cref="IKeyboard"/> events. +/// </summary> +public interface IKeyboardInputHandler : IButtonInputHandler<KeyName> +{ + /// <summary> + /// Called when a key is pressed or depressed. + /// </summary> + /// <param name="event">The event details.</param> + void HandleKeyChanged(KeyChangedEvent @event); + + /// <summary> + /// Called when a character is typed. + /// </summary> + /// <remarks> + /// Ensure you have called <see cref="IKeyboard.BeginInput"/> to start receiving text, after which events will be + /// sent for each character until <see cref="IKeyboard.EndInput"/> is called. + /// </remarks> + /// <param name="event">The event details.</param> + 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; + +/// <summary> +/// Represents a vibration motor. +/// </summary> +public interface IMotor +{ + /// <summary> + /// Gets or sets the speed at which the motor is operating, where <c>0.0</c> represents no vibration and <c>1.0</c> + /// represents the maximum amount of vibration. + /// </summary> + 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; + +/// <summary> +/// Represents a mouse - a type of pointer device. +/// </summary> +public interface IMouse : IPointerDevice +{ + /// <summary> + /// Gets the device state. + /// </summary> + /// <remarks> + /// Only updated when <see cref="IInputBackend.Update"/> is called. + /// </remarks> + new MouseState State { get; } + + PointerState IPointerDevice.State => State; + + /// <summary> + /// Gets the cursor configuration of the mouse. + /// </summary> + /// <remarks> + /// This will determine which <see cref="IPointerTarget"/> points shall lie on. + /// </remarks> + ICursorConfiguration Cursor { get; } + + /// <summary> + /// Attempts to set the position of the mouse. + /// </summary> + /// <param name="position">The position of the mouse in window coordinates.</param> + /// <returns>Whether the requested position has been applied.</returns> + 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; + +/// <summary> +/// An <see cref="IInputHandler"/> that receives input from an <see cref="IMouse"/>. +/// </summary> +public interface IMouseInputHandler : IButtonInputHandler<PointerButton> +{ + /// <summary> + /// Called when the user scrolls using the scroll wheel. + /// </summary> + /// <param name="event">The event details.</param> + 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; + +/// <summary> +/// Represents a device with which the user can point at a target. +/// </summary> +public interface IPointerDevice : IButtonDevice<PointerButton> +{ + /// <summary> + /// Gets the device state. + /// </summary> + /// <remarks> + /// Only updated when <see cref="IInputBackend.Update"/> is called. + /// </remarks> + new PointerState State { get; } + ButtonReadOnlyList<PointerButton> IButtonDevice<PointerButton>.State => State.Buttons; + /// <summary> + /// Gets the targets at which the user can point with their pointer. + /// </summary> + IReadOnlyList<IPointerTarget> 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; + +/// <summary> +/// An <see cref="IInputHandler"/> that also receives <see cref="IPointerDevice"/> events. +/// </summary> +public interface IPointerInputHandler : IButtonInputHandler<PointerButton> +{ + /// <summary> + /// 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. + /// </summary> + /// <param name="event">The event details.</param> + void HandleTargetChanged(PointerTargetChangedEvent @event); + + /// <summary> + /// Called when the user adds, removes, or changes a point at which they're pointing at a target. + /// </summary> + /// <param name="event">The event details.</param> + void HandlePointChanged(PointChangedEvent @event); + + /// <summary> + /// Called when the user changes the pressure with which they're gripping the pointer device. + /// </summary> + /// <param name="event">The event details.</param> + 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; + +/// <summary> +/// Represents a target at which the user can point using their pointer device. +/// </summary> +public interface IPointerTarget +{ + /// <summary> + /// The boundary in which positions of points on this target shall fall. For <see cref="Box3D{T}.Min" />, + /// <see cref="float.NegativeInfinity" /> shall represent the lack of a lower bound on a particular axis. For + /// For <see cref="Box3D{T}.Max" />, <see cref="float.PositiveInfinity" /> shall represent the lack of a lower bound + /// on a particular axis. <c>0</c> represents an unused axis that axis is <c>0</c> on both + /// <see cref="Box3D{T}.Min" /> and <see cref="Box3D{T}.Max" />. + /// </summary> + Box3D<float> Bounds { get; } + + /// <summary> + /// Gets the number of points with which the given pointer is pointing at this target. + /// </summary> + /// <returns>The number of points.</returns> + /// <remarks> + /// 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. + /// </remarks> + int GetPointCount(IPointerDevice pointer); + + /// <summary> + /// Gets a point with which the given pointer is pointing at this target. + /// </summary> + /// <param name="pointer">The pointer device.</param> + /// <param name="point"> + /// The index of the point, between <c>0</c> and the number sourced from <see cref="GetPointCount" />. + /// </param> + /// <returns>The point at the given index with which the given pointer device is pointing at the target.</returns> + TargetPoint GetPoint(IPointerDevice pointer, int point); +} \ No newline at end of file diff --git a/sources/Input/Input/Implementations/SDL3/InputWindowExtensions.cs b/sources/Input/Input/Implementations/SDL3/InputWindowExtensions.cs new file mode 100644 index 0000000000..9d2f55ce37 --- /dev/null +++ b/sources/Input/Input/Implementations/SDL3/InputWindowExtensions.cs @@ -0,0 +1,16 @@ +// 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 +namespace Silk.NET.Input; + +/// <summary> +/// Contains extensions for creating input backends and contexts from <see cref="INativeWindow"/>s. +/// </summary> +public static partial class InputWindowExtensions +{ + public static partial IInputBackend CreateInputBackend(this INativeWindow window) + { + throw new NotImplementedException(); + } +} diff --git a/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs b/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs new file mode 100644 index 0000000000..64cd37ba8a --- /dev/null +++ b/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs @@ -0,0 +1,17 @@ +// 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; + +internal class SdlGamepad : IGamepad +{ + public bool Equals(IInputDevice? other) => throw new NotImplementedException(); + + public IntPtr Id => throw new NotImplementedException(); + + public string Name => throw new NotImplementedException(); + + public GamepadState State => throw new NotImplementedException(); + + public IReadOnlyList<IMotor> VibrationMotors => throw new NotImplementedException(); +} diff --git a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs new file mode 100644 index 0000000000..24d0a8fdd9 --- /dev/null +++ b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs @@ -0,0 +1,47 @@ +// 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; + +internal class SdlInputBackend : IInputBackend +{ + public unsafe SdlInputBackend() + { + var ptr = new EventFilter(OnEvent); + Sdl.AddEventWatch(ptr, nullptr); + Id = (nint)ptr.Handle; + } + + private unsafe byte OnEvent(void* arg0, Event* arg1) + { + throw new NotImplementedException(); + } + + public string Name => + $"Silk.NET.Input Reference Implementation using SDL3 ({Sdl.GetPlatform().ReadToString()})"; + + public nint Id { get; } + + public IReadOnlyList<IInputDevice> Devices => throw new NotImplementedException(); + + public void Update(IInputHandler? handler = null) => throw new NotImplementedException(); + + private unsafe void ReleaseUnmanagedResources() + { + Sdl.RemoveEventWatch( + new EventFilter((delegate* unmanaged<void*, Event*, byte>)(void*)Id), + nullptr + ); + SilkMarshal.Free((Ptr)Id); + } + + public void Dispose() + { + ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + ~SdlInputBackend() => ReleaseUnmanagedResources(); +} diff --git a/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs b/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs new file mode 100644 index 0000000000..b4ecb54675 --- /dev/null +++ b/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs @@ -0,0 +1,15 @@ +// 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; + +internal class SdlJoystick : IJoystick +{ + public bool Equals(IInputDevice? other) => throw new NotImplementedException(); + + public IntPtr Id => throw new NotImplementedException(); + + public string Name => throw new NotImplementedException(); + + public JoystickState State => throw new NotImplementedException(); +} diff --git a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs new file mode 100644 index 0000000000..9bd7de6d81 --- /dev/null +++ b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Silk.NET.Input.SDL3; + +internal class SdlKeyboard : IKeyboard +{ + public bool Equals(IInputDevice? other) => throw new NotImplementedException(); + + public IntPtr Id => throw new NotImplementedException(); + + public string Name => throw new NotImplementedException(); + + public KeyboardState State => throw new NotImplementedException(); + + public string? ClipboardText + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + + public bool TryGetKeyName(KeyName key, [NotNullWhen(true)] out string? name) => + throw new NotImplementedException(); + + public void BeginInput() => throw new NotImplementedException(); + + public string? EndInput() => throw new NotImplementedException(); +} diff --git a/sources/Input/Input/Implementations/SDL3/SdlMotor.cs b/sources/Input/Input/Implementations/SDL3/SdlMotor.cs new file mode 100644 index 0000000000..3e551826a6 --- /dev/null +++ b/sources/Input/Input/Implementations/SDL3/SdlMotor.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; + +internal class SdlMotor : IMotor +{ + public float Speed + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } +} diff --git a/sources/Input/Input/Implementations/SDL3/SdlMouse.cs b/sources/Input/Input/Implementations/SDL3/SdlMouse.cs new file mode 100644 index 0000000000..25ac5cdb4f --- /dev/null +++ b/sources/Input/Input/Implementations/SDL3/SdlMouse.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Numerics; + +namespace Silk.NET.Input.SDL3; + +internal class SdlMouse : IMouse +{ + public bool Equals(IInputDevice? other) => throw new NotImplementedException(); + + public IntPtr Id => throw new NotImplementedException(); + + public string Name => throw new NotImplementedException(); + + public MouseState State => throw new NotImplementedException(); + + public ICursorConfiguration Cursor => throw new NotImplementedException(); + + public bool TrySetPosition(Vector2 position) => throw new NotImplementedException(); + + public IReadOnlyList<IPointerTarget> Targets => throw new NotImplementedException(); +} diff --git a/sources/Input/Input/Implementations/SDL3/SdlPen.cs b/sources/Input/Input/Implementations/SDL3/SdlPen.cs new file mode 100644 index 0000000000..d061bc86ca --- /dev/null +++ b/sources/Input/Input/Implementations/SDL3/SdlPen.cs @@ -0,0 +1,17 @@ +// 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; + +internal class SdlPen : IPointerDevice +{ + public bool Equals(IInputDevice? other) => throw new NotImplementedException(); + + public IntPtr Id => throw new NotImplementedException(); + + public string Name => throw new NotImplementedException(); + + public PointerState State => throw new NotImplementedException(); + + public IReadOnlyList<IPointerTarget> Targets => throw new NotImplementedException(); +} diff --git a/sources/Input/Input/Implementations/SDL3/SdlTouchScreen.cs b/sources/Input/Input/Implementations/SDL3/SdlTouchScreen.cs new file mode 100644 index 0000000000..7219f1a1d9 --- /dev/null +++ b/sources/Input/Input/Implementations/SDL3/SdlTouchScreen.cs @@ -0,0 +1,17 @@ +// 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; + +internal class SdlTouchScreen : IPointerDevice +{ + public bool Equals(IInputDevice? other) => throw new NotImplementedException(); + + public IntPtr Id => throw new NotImplementedException(); + + public string Name => throw new NotImplementedException(); + + public PointerState State => throw new NotImplementedException(); + + public IReadOnlyList<IPointerTarget> Targets => throw new NotImplementedException(); +} diff --git a/sources/Input/Input/InputContext.cs b/sources/Input/Input/InputContext.cs new file mode 100644 index 0000000000..402735e702 --- /dev/null +++ b/sources/Input/Input/InputContext.cs @@ -0,0 +1,241 @@ +using System.Collections; + +namespace Silk.NET.Input; + +/// <summary> +/// Represents an "input context" containing multiple <see cref="IInputBackend"/>s from which +/// <see cref="IInputDevice"/>s, their state, and their events are aggregated and laid-out in a user-friendly fashion. +/// </summary> +/// <remarks> +/// 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 <see cref="Update"/> can be called +/// on - the user is responsible for respecting these threading rules as well. +/// </remarks> +public class InputContext + : IJoystickInputHandler, + IGamepadInputHandler, + IMouseInputHandler, + IPointerInputHandler, + IKeyboardInputHandler, + IList<IInputBackend> +{ + // These are lazy-initialized as they contain their own device lists in addition to the device list stored here and + // the device lists stored in each of the backends. You could argue having this many duplicated lists is inefficient + // and you'd be absolutely right, but realistically: how many devices will the average user have connected to their + // PC? If you're worried about your game's memory consumption, you're probably not looking at the small lists that + // input allocates... This way we can also provide sane/consistent indices. + private Pointers? _pointers; + private Keyboards? _keyboards; + private Gamepads? _gamepads; + private Joysticks? _joysticks; + private List<IInputBackend> _backends = []; + private List<IInputDevice>? _devices; + + /// <summary> + /// Gets the <see cref="IPointerDevice"/>s enumerated by the <see cref="IInputBackend"/>s attached to this context. + /// </summary> + public Pointers Pointers => _pointers ??= new Pointers(this); + + /// <summary> + /// Gets the <see cref="IKeyboard"/>s enumerated by the <see cref="IInputBackend"/>s attached to this context. + /// </summary> + public Keyboards Keyboards => _keyboards ??= new Keyboards(this); + + /// <summary> + /// Gets the <see cref="IGamepad"/>s enumerated by the <see cref="IInputBackend"/>s attached to this context. + /// </summary> + public Gamepads Gamepads => _gamepads ??= new Gamepads(this); + + /// <summary> + /// Gets the <see cref="IJoystick"/>s enumerated by the <see cref="IInputBackend"/>s attached to this context. + /// </summary> + public Joysticks Joysticks => _joysticks ??= new Joysticks(this); + + /// <summary> + /// Gets the <see cref="IInputDevice"/>s enumerated by the <see cref="IInputBackend"/>s attached to this context. + /// </summary> + public IReadOnlyList<IInputDevice> Devices + { + get + { + if (_devices is not null) + { + return _devices; + } + + foreach (var backend in Backends) + { + _devices ??= new List<IInputDevice>(backend.Devices.Count); + _devices.AddRange(backend.Devices); + } + + return _devices ??= []; + } + } + + /// <summary> + /// Gets a list denoting the <see cref="IInputBackend"/> attached to this context. + /// </summary> + public IList<IInputBackend> Backends => this; + + /// <summary> + /// Raised when a device is added or removed from the list of connected <see cref="Devices"/>. + /// </summary> + public event Action<ConnectionEvent>? ConnectionChanged; + + /// <summary> + /// Polls and updates the state of the <see cref="IInputDevice"/> objects connected to each + /// <see cref="IInputBackend"/> attached to this context, raising appropriate events for each state change. + /// </summary> + /// <remarks> + /// This calls <see cref="IInputBackend.Update"/> for each <see cref="IInputBackend"/> attached to this context. + /// </remarks> + public void Update() + { + foreach (var backend in Backends) + { + backend.Update(this); + } + + _pointers?.HandleUpdate(); + } + + private void HandleBackendRemoval(IInputBackend backend) + { + foreach (var device in backend.Devices) + { + HandleDeviceConnectionChanged(new ConnectionEvent(device, 0, false)); + } + } + + private void HandleBackendAddition(IInputBackend backend) + { + foreach (var device in backend.Devices) + { + HandleDeviceConnectionChanged(new ConnectionEvent(device, 0, true)); + } + } + + private void HandleDeviceConnectionChanged(ConnectionEvent e) + { + _pointers?.HandleDeviceConnectionChanged(e); + _joysticks?.HandleDeviceConnectionChanged(e); + _gamepads?.HandleDeviceConnectionChanged(e); + _keyboards?.HandleDeviceConnectionChanged(e); + if (_devices is null) + { + return; + } + + if (e.IsConnected) + { + _devices?.Add(e.Device); + } + else + { + _devices?.Remove(e.Device); + } + } + + void IButtonInputHandler<JoystickButton>.HandleButtonChanged( + ButtonChangedEvent<JoystickButton> @event + ) => _joysticks?.HandleButtonChanged(@event); + + void IJoystickInputHandler.HandleAxisMove(JoystickAxisMoveEvent @event) => + _joysticks?.HandleAxisMove(@event); + + void IJoystickInputHandler.HandleHatMove(JoystickHatMoveEvent @event) => + _joysticks?.HandleHatMove(@event); + + void IGamepadInputHandler.HandleThumbstickMove(GamepadThumbstickMoveEvent @event) => + _gamepads?.HandleThumbstickMove(@event); + + void IGamepadInputHandler.HandleTriggerMove(GamepadTriggerMoveEvent @event) => + _gamepads?.HandleTriggerMove(@event); + + void IButtonInputHandler<PointerButton>.HandleButtonChanged( + ButtonChangedEvent<PointerButton> @event + ) => _pointers?.HandleButtonChanged(@event); + + void IMouseInputHandler.HandleScroll(MouseScrollEvent @event) => + _pointers?.HandleScroll(@event); + + void IPointerInputHandler.HandleTargetChanged(PointerTargetChangedEvent @event) => + _pointers?.HandleTargetChanged(@event); + + void IPointerInputHandler.HandlePointChanged(PointChangedEvent @event) => + _pointers?.HandlePointChanged(@event); + + void IPointerInputHandler.HandleGripChanged(PointerGripChangedEvent @event) => + _pointers?.HandleGripChanged(@event); + + void IButtonInputHandler<KeyName>.HandleButtonChanged(ButtonChangedEvent<KeyName> @event) => + _keyboards?.HandleButtonChanged(@event); + + void IKeyboardInputHandler.HandleKeyChanged(KeyChangedEvent @event) => + _keyboards?.HandleKeyChanged(@event); + + void IKeyboardInputHandler.HandleKeyChar(KeyCharEvent @event) => + _keyboards?.HandleKeyChar(@event); + + void IInputHandler.HandleDeviceConnectionChanged(ConnectionEvent @event) + { + HandleDeviceConnectionChanged(@event); + ConnectionChanged?.Invoke(@event); + } + + IEnumerator<IInputBackend> IEnumerable<IInputBackend>.GetEnumerator() => + _backends.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => _backends.GetEnumerator(); + + void ICollection<IInputBackend>.Add(IInputBackend item) + { + HandleBackendAddition(item); + _backends.Add(item); + } + + void ICollection<IInputBackend>.Clear() + { + foreach (var backend in Backends) + { + HandleBackendRemoval(backend); + } + } + + bool ICollection<IInputBackend>.Contains(IInputBackend item) => _backends.Contains(item); + + void ICollection<IInputBackend>.CopyTo(IInputBackend[] array, int arrayIndex) => + _backends.CopyTo(array, arrayIndex); + + bool ICollection<IInputBackend>.Remove(IInputBackend item) + { + HandleBackendRemoval(item); + return _backends.Remove(item); + } + + int ICollection<IInputBackend>.Count => _backends.Count; + + bool ICollection<IInputBackend>.IsReadOnly => false; + + int IList<IInputBackend>.IndexOf(IInputBackend item) => _backends.IndexOf(item); + + void IList<IInputBackend>.Insert(int index, IInputBackend item) + { + HandleBackendAddition(item); + _backends.Insert(index, item); + } + + void IList<IInputBackend>.RemoveAt(int index) + { + var backend = _backends[index]; + HandleBackendRemoval(backend); + _backends.RemoveAt(index); + } + + IInputBackend IList<IInputBackend>.this[int index] + { + get => _backends[index]; + set => _backends[index] = value; + } +} diff --git a/sources/Input/Input/InputContextDeviceList.cs b/sources/Input/Input/InputContextDeviceList.cs new file mode 100644 index 0000000000..1ae312526b --- /dev/null +++ b/sources/Input/Input/InputContextDeviceList.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using System.Diagnostics.CodeAnalysis; + +namespace Silk.NET.Input; + +/// <summary> +/// An internal class that represents a list of <see cref="InputContext.Devices"/> that are assignable to +/// <typeparamref name="T"/>. The backing list is lazily initialized. +/// </summary> +/// <typeparam name="T">The device type.</typeparam> +/// <remarks> +/// This type is not intended for public consumption and has no API/ABI stability guarantees. +/// </remarks> +[Experimental( + "ST0005", + UrlFormat = "https://dotnet.github.io/Silk.NET/docs/v3/silk.net/diagnostics/{0}" +)] +public abstract class InputContextDeviceList<T> : IReadOnlyList<T>, IInputHandler +{ + private readonly InputContext _ctx; + private List<T>? _list; + + internal InputContextDeviceList(InputContext ctx) => _ctx = ctx; + + private List<T> List => _list ??= _ctx.Devices.OfType<T>().ToList(); + + /// <inheritdoc /> + public IEnumerator<T> GetEnumerator() => throw new NotImplementedException(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// <inheritdoc /> + public int Count => List.Count; + + /// <inheritdoc /> + public T this[int index] => List[index]; + + void IInputHandler.HandleDeviceConnectionChanged(ConnectionEvent @event) => + HandleDeviceConnectionChanged(@event); + + /// <inheritdoc cref="IInputHandler.HandleDeviceConnectionChanged"/> + protected internal virtual void HandleDeviceConnectionChanged(ConnectionEvent @event) + { + if (_list is null || @event.Device is not T t) + { + return; + } + + if (@event.IsConnected) + { + _list.Add(t); + } + else + { + _list.Remove(t); + } + } +} diff --git a/sources/Input/Input/InputMarshal.cs b/sources/Input/Input/InputMarshal.cs new file mode 100644 index 0000000000..0d9731e8e7 --- /dev/null +++ b/sources/Input/Input/InputMarshal.cs @@ -0,0 +1,636 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using System.Diagnostics; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Silk.NET.Input; + +/// <summary> +/// Contains utilities for creating and manipulating <see cref="InputReadOnlyList{T}"/>s. This is a very unsafe set of +/// APIs that are extremely prone to misuse, and therefore is not recommended to be consumed by anyone other than input +/// backends. +/// </summary> +/// <remarks> +/// This class is ABI/API stable, but new APIs that obsolete the old ones may be added at any time as more efficient +/// implementations are discovered. +/// </remarks> +// NOTE: Not experimental so that we don't eliminate the prospects of third-party implementations. +public static class InputMarshal +{ + /// <summary> + /// A wrapper class denoting ownership of a <see cref="InputReadOnlyList{T}"/>. This is used to attempt to stop + /// misuse of these methods, but of course it's fairly trivial to work around this for a user determined to do + /// terrible things. + /// </summary> + /// <typeparam name="T">The list element type.</typeparam> + public struct ListOwner<T> + { + internal ListOwner(InputReadOnlyList<T> list) => List = list; + + /// <summary> + /// Gets the list owned by this owner. + /// </summary> + public InputReadOnlyList<T> List { get; } + } + + internal class ButtonList<T>(uint[] binary, Dictionary<T, Button<T>>? other) + : IReadOnlyList<Button<T>> + where T : unmanaged, Enum + { + private Dictionary<T, Button<T>>? _other = other; + + public ButtonList() + : this(new uint[(GetButtonListCount<T>() + 32 - 1) / 32], null) { } + + public ButtonList<T> Clone() => + new([.. binary], _other is null ? null : new Dictionary<T, Button<T>>(_other)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] // <-- generic specialisation + public IEnumerator<Button<T>> GetEnumerator() => + typeof(T) == typeof(KeyName) ? GetKeyNameEnumerator() : GetButtonEnumerator(); + + private IEnumerator<Button<T>> GetKeyNameEnumerator() + { + var idx = 0; + var bit = 0; + // To determine the gaps, run the GetButtonCount unit test. The equality check is the LHS from the output + + // 1, and the assignment is the RHS - 1. Example output below: + // 0 (Unknown), 4 (A) + // 129 (VolumeDown), 133 (KeypadComma) + // 164 (ExtendSelect), 176 (Keypad00) + // 221 (KeypadHexadecimal), 224 (ControlLeft) + // 231 (SuperRight), 257 (Mode) + // 286 (ApplicationBookmarks), 501 (SoftLeft) + for (var cur = (int)KeyName.A; cur <= (int)KeyName.EndCall; cur++) + { + switch (cur) + { + case (int)KeyName.VolumeDown + 1: + cur = (int)KeyName.KeypadComma - 1; + continue; + case (int)KeyName.ExtendSelect + 1: + cur = (int)KeyName.Keypad00 - 1; + continue; + case (int)KeyName.KeypadHexadecimal + 1: + cur = (int)KeyName.ControlLeft - 1; + continue; + case (int)KeyName.SuperRight + 1: + cur = (int)KeyName.Mode - 1; + continue; + case (int)KeyName.ApplicationBookmarks + 1: + cur = (int)KeyName.SoftLeft - 1; + continue; + } + + var ret = ElementAt((T)(object)(KeyName)cur, idx, bit); + (idx, bit) = BitIterate(idx, bit); + yield return ret; + } + } + + private IEnumerator<Button<T>> GetButtonEnumerator() + { + var max = GetButtonListCount<T>(); + int idx = 0, + bit = 0; + for (var i = 1; i <= max; i++) + { + var ret = ElementAt(SilkMarshal.ConstCast<int, T>(i), idx, bit); + (idx, bit) = BitIterate(idx, bit); + yield return ret; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Button<T> ElementAt(T name, int idx, int bit) + { + var ret = new Button<T>(name, false, 0); + var isBinaryDown = BitOperations.PopCount(binary[idx] & (1U << (7 - bit))) > 0; + if (isBinaryDown) + { + ret = ret with { IsDown = true, Pressure = 1 }; + } + else + { + _other?.TryGetValue(name, out ret); + } + + return ret; + } + + [MethodImpl( + MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization + )] + private static (int, int) BitIterate(int idx, int bit) => + ++bit == 32 ? (++idx, 0) : (idx, bit); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public int Count => GetButtonListCount<T>(); + + [MethodImpl( + MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization + )] + internal static T IndexName(int index) + { + var name = index; + if (typeof(T) == typeof(KeyName)) + { + // To determine the gaps, run the GetButtonCount unit test. The condition is to check whether name + // is greater than the LHS, and if so add the RHS less the LHS less 1. Example output: + // 0 (Unknown), 4 (A) + // 129 (VolumeDown), 133 (KeypadComma) + // 164 (ExtendSelect), 176 (Keypad00) + // 221 (KeypadHexadecimal), 224 (ControlLeft) + // 231 (SuperRight), 257 (Mode) + // 286 (ApplicationBookmarks), 501 (SoftLeft) + name += 4; + if (name > 129) + { + name += 133 - 129 - 1; + } + + if (name > 164) + { + name += 176 - 164 - 1; + } + + if (name > 221) + { + name += 224 - 221 - 1; + } + + if (name > 231) + { + name += 257 - 231 - 1; + } + + if (name > 286) + { + name += 501 - 286 - 1; + } + } + else + { + // To account for Unknown = 0. + name++; + } + + return SilkMarshal.ConstCast<int, T>(name); + } + + [MethodImpl( + MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization + )] + internal static int NameIndex(T name) + { + var index = SilkMarshal.ConstCast<T, int>(name); + if (typeof(T) == typeof(KeyName)) + { + // To determine the gaps, run the GetButtonCount unit test. The condition is to check whether name + // is greater than the LHS, and if so subtract the RHS less the LHS less 1. Note that the conditions + // should be in reverse order i.e. from the last output line to the first output line. Example output: + // 0 (Unknown), 4 (A) + // 129 (VolumeDown), 133 (KeypadComma) + // 164 (ExtendSelect), 176 (Keypad00) + // 221 (KeypadHexadecimal), 224 (ControlLeft) + // 231 (SuperRight), 257 (Mode) + // 286 (ApplicationBookmarks), 501 (SoftLeft) + if (index > 286) + { + index -= 501 - 286 - 1; + } + + if (index > 231) + { + index -= 257 - 231 - 1; + } + + if (index > 221) + { + index -= 224 - 221 - 1; + } + + if (index > 164) + { + index -= 176 - 164 - 1; + } + + if (index > 129) + { + index -= 133 - 129 - 1; + } + index -= 4; + } + else + { + // To account for Unknown = 0. + index--; + } + + return index; + } + + public Button<T> this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + ArgumentOutOfRangeException.ThrowIfNegative(index); + ArgumentOutOfRangeException.ThrowIfGreaterThan(index, Count); + return ElementAt( + IndexName(index), + Math.DivRem(index, 32, out var remainder), + remainder + ); + } + } + + public Button<T> this[T name] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + var index = NameIndex(name); + if (index >= 0 && index < GetButtonListCount<T>()) + { + return ElementAt(name, Math.DivRem(index, 32, out var remainder), remainder); + } + + Throw(); + return default; + [StackTraceHidden] + static void Throw() => throw new ArgumentOutOfRangeException(nameof(name)); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Set(Button<T> btn, bool isBinary) + { + if (btn.IsDown && isBinary) + { + binary[Math.DivRem(NameIndex(btn.Name), 32, out var bit)] |= 1U << (7 - bit); + _other?.Remove(btn.Name); + } + else + { + binary[Math.DivRem(NameIndex(btn.Name), 32, out var bit)] &= ~(1U << (7 - bit)); + } + + if (!isBinary) + { + (_other ??= [])[btn.Name] = btn; + } + } + } + + /// <summary> + /// Gets the <see cref="IReadOnlyCollection{T}.Count"/> reported by <see cref="InputReadOnlyList{T}"/>s created with + /// <see cref="CreateList{T}"/> for the given <typeparamref name="T"/>. + /// </summary> + /// <typeparam name="T">The button name type.</typeparam> + /// <returns> + /// The number of buttons that will be in a button list created with <see cref="CreateList{T}"/>, or <c>-1</c> if + /// <typeparamref name="T"/> is not a supported button name type. + /// </returns> + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static int GetButtonListCount<T>() + { + if (typeof(T) == typeof(JoystickButton)) + { + return (int)JoystickButton.DPadLeft; + } + + if (typeof(T) == typeof(PointerButton)) + { + return (int)PointerButton.Button32; + } + + if (typeof(T) == typeof(KeyName)) + { + // To determine the ranges, run the GetButtonCount unit test. The RHS of the subtraction statements below + // are the RHS of the line output, and the LHS is the LHS of the following line in its output. There is a + // final addition that is the number of preceding additions to account for the boundary values. Example + // output from that test: + // 0 (Unknown), 4 (A) + // 129 (VolumeDown), 133 (KeypadComma) + // 164 (ExtendSelect), 176 (Keypad00) + // 221 (KeypadHexadecimal), 224 (ControlLeft) + // 231 (SuperRight), 257 (Mode) + // 286 (ApplicationBookmarks), 501 (SoftLeft) + // ReSharper disable once ArrangeRedundantParentheses <-- stylistic choice + return ((int)KeyName.VolumeDown - (int)KeyName.A) + + ((int)KeyName.ExtendSelect - (int)KeyName.KeypadComma) + + ((int)KeyName.KeypadHexadecimal - (int)KeyName.Keypad00) + + ((int)KeyName.SuperRight - (int)KeyName.ControlLeft) + + ((int)KeyName.ApplicationBookmarks - (int)KeyName.Mode) + + ((int)KeyName.EndCall - (int)KeyName.SoftLeft) + + 6; + } + + return -1; + } + + /// <summary> + /// Creates a <see cref="ButtonReadOnlyList{T}"/> wrapping the given button <see cref="InputReadOnlyList{T}"/>. + /// </summary> + /// <param name="list">The list.</param> + /// <typeparam name="T">The button name type.</typeparam> + /// <returns>The button list.</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] // <-- generic specialisation + public static ButtonReadOnlyList<T> AsButtonList<T>(this InputReadOnlyList<Button<T>> list) + where T : unmanaged, Enum => new(list); + + /// <summary> + /// Creates a new <see cref="InputReadOnlyList{T}"/> for the given <typeparamref name="T" />, optionally with the + /// given <paramref name="capacity"/> where <see cref="GetUnderlyingList{T}"/> is applicable for this + /// <typeparamref name="T"/>. + /// </summary> + /// <param name="capacity">The capacity.</param> + /// <typeparam name="T">The element type.</typeparam> + /// <returns>The list.</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] // <-- generic specialisation + public static ListOwner<T> CreateList<T>(int capacity = 0) + { + if (typeof(T) == typeof(Button<KeyName>)) + { + return (ListOwner<T>) + (object) + new ListOwner<Button<KeyName>>( + new InputReadOnlyList<Button<KeyName>>((object)new ButtonList<KeyName>()) + ); + } + + if (typeof(T) == typeof(Button<JoystickButton>)) + { + return (ListOwner<T>) + (object) + new ListOwner<Button<JoystickButton>>( + new InputReadOnlyList<Button<JoystickButton>>( + (object)new ButtonList<JoystickButton>() + ) + ); + } + + if (typeof(T) == typeof(Button<PointerButton>)) + { + return (ListOwner<T>) + (object) + new ListOwner<Button<PointerButton>>( + new InputReadOnlyList<Button<PointerButton>>( + (object)new ButtonList<PointerButton>() + ) + ); + } + + return new ListOwner<T>(new InputReadOnlyList<T>((object)new List<T>(capacity))); + } + + /// <summary> + /// Creates a new <see cref="InputReadOnlyList{T}"/> from the given <see cref="IReadOnlyList{T}"/>. This is + /// equivalent to <see cref="InputReadOnlyList{T}(IReadOnlyList{T})"/>, but returns a <see cref="ListOwner{T}"/> + /// instead. + /// </summary> + /// <param name="other">The elements to populate the list with.</param> + /// <typeparam name="T"></typeparam> + /// <returns></returns> + public static ListOwner<T> Clone<T>(IReadOnlyList<T> other) + { + // ReSharper disable once InvertIf <-- starting to really dislike this as it duplicates code + if (other is InputReadOnlyList<T> irl) + { + if (typeof(T) == typeof(Button<KeyName>)) + { + return new ListOwner<T>( + new InputReadOnlyList<T>(Unsafe.As<ButtonList<KeyName>>(irl.Data).Clone()) + ); + } + + if (typeof(T) == typeof(Button<PointerButton>)) + { + return new ListOwner<T>( + new InputReadOnlyList<T>(Unsafe.As<ButtonList<PointerButton>>(irl.Data).Clone()) + ); + } + + if (typeof(T) == typeof(Button<JoystickButton>)) + { + return new ListOwner<T>( + new InputReadOnlyList<T>( + Unsafe.As<ButtonList<JoystickButton>>(irl.Data).Clone() + ) + ); + } + } + + if (typeof(T) == typeof(Button<KeyName>)) + { + return new ListOwner<T>( + new InputReadOnlyList<T>(CloneButtonList((IReadOnlyList<Button<KeyName>>)other)) + ); + } + + if (typeof(T) == typeof(Button<PointerButton>)) + { + return new ListOwner<T>( + new InputReadOnlyList<T>( + CloneButtonList((IReadOnlyList<Button<PointerButton>>)other) + ) + ); + } + + // ReSharper disable once ConvertIfStatementToReturnStatement <-- stylistic choice + if (typeof(T) == typeof(Button<JoystickButton>)) + { + return new ListOwner<T>( + new InputReadOnlyList<T>( + CloneButtonList((IReadOnlyList<Button<JoystickButton>>)other) + ) + ); + } + + return new ListOwner<T>(new InputReadOnlyList<T>((object)new List<T>(other))); + static ButtonList<TEnum> CloneButtonList<TEnum>(IReadOnlyList<Button<TEnum>> list) + where TEnum : unmanaged, Enum + { + var ret = new ButtonList<TEnum>(); + foreach (var button in list) + { + ret.Set( + button, + (button.IsDown && button.Pressure >= 1.0) + || (!button.IsDown && button.Pressure <= 0.0) + ); + } + + return ret; + } + } + + /// <summary> + /// Sets the button state in the given button list. + /// </summary> + /// <param name="list">The list to update.</param> + /// <param name="value">The new state of the button.</param> + /// <param name="isBinary"> + /// Whether the <see cref="Button{T}.Pressure"/> of <paramref name="value"/> can only be <c>1.0</c> when + /// <see cref="Button{T}.IsDown"/> is <c>true</c>, and <c>0.0</c> when <see cref="Button{T}.IsDown"/> is + /// <c>false</c>. + /// </param> + /// <typeparam name="T">The button type.</typeparam> + [MethodImpl(MethodImplOptions.AggressiveInlining)] // <-- generic specialisation + public static void SetButtonState<T>(ListOwner<Button<T>> list, Button<T> value, bool isBinary) + where T : unmanaged, Enum + { + if ( + typeof(T) == typeof(KeyName) + || typeof(T) == typeof(JoystickButton) + || typeof(T) == typeof(PointerButton) + ) + { + Unsafe.As<ButtonList<T>>(list.List.Data).Set(value, isBinary); + return; + } + + var underlying = GetUnderlyingList(list)!; + for (var i = 0; i < underlying.Count; i++) + { + // ReSharper disable once InvertIf <-- this literally results in more lines of code!!!!! + if (underlying[i].Name.Equals(value.Name)) + { + underlying[i] = value; + return; + } + } + + underlying.Add(value); + } + + /// <summary> + /// Attempts to retrieve the underlying <see cref="IList{T}"/> implementation, provided that + /// <see cref="InputReadOnlyList{T}"/> for the given <typeparamref name="T"/> is implemented as a sequential list + /// with individually addressable and a variable number of elements. + /// </summary> + /// <param name="list">The list.</param> + /// <typeparam name="T">The list element type.</typeparam> + /// <returns> + /// The list, or <c>null</c> if the optimised implementation of <see cref="InputReadOnlyList{T}"/> cannot be + /// expressed as an <see cref="IList{T}" />. + /// </returns> + /// <remarks> + /// Currently, this can be assumed to not null except for the following types: + /// <list type="bullet"> + /// <item><description><see cref="Button{T}"/> where <c>T</c> is <see cref="KeyName"/></description></item> + /// <item><description><see cref="Button{T}"/> where <c>T</c> is <see cref="JoystickButton"/></description></item> + /// <item><description><see cref="Button{T}"/> where <c>T</c> is <see cref="PointerButton"/></description></item> + /// </list> + /// It is a breaking change to change the underlying implementation of the list such that this method returns + /// <c>null</c> where it previously did not return <c>null</c>, therefore Silk.NET will only do this in a + /// <i>major</i> release. As a result, it is safe to use the <c>!</c> operator for code targeting a specific major + /// release. Ideally, this is also the case for major releases, but the Silk.NET team cannot guarantee this at this + /// time. + /// </remarks> + [MethodImpl(MethodImplOptions.AggressiveInlining)] // <-- generic specialisation + public static IList<T>? GetUnderlyingList<T>(this ListOwner<T> list) => + typeof(T) == typeof(Button<KeyName>) + || typeof(T) == typeof(Button<PointerButton>) + || typeof(T) == typeof(Button<JoystickButton>) + ? null + : Unsafe.As<List<T>>(list.List.Data); + + // These are APIs defined on InputReadOnlyList or ButtonReadOnlyList but are implemented here to keep the + // implementation of the backing list in one file, the hope being that this decreases the likelihood of bugs. + + [MethodImpl(MethodImplOptions.AggressiveInlining)] // <-- generic specialisation + internal static Button<T> GetButtonState<T>(InputReadOnlyList<Button<T>> list, T name) + where T : unmanaged, Enum + { + if ( + typeof(T) == typeof(KeyName) + || typeof(T) == typeof(JoystickButton) + || typeof(T) == typeof(PointerButton) + ) + { + return Unsafe.As<ButtonList<T>>(list.Data)[name]; + } + + var underlying = Unsafe.As<List<Button<T>>>(list.Data); + foreach (var t in underlying) + { + // ReSharper disable once InvertIf <-- this literally results in more lines of code!!!!! + if (t.Name.Equals(name)) + { + return t; + } + } + + return new Button<T>(name, false, 0); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int GetListCount<T>(InputReadOnlyList<T> list) + { + if (typeof(T) == typeof(Button<KeyName>)) + { + return GetButtonListCount<KeyName>(); + } + + if (typeof(T) == typeof(Button<PointerButton>)) + { + return GetButtonListCount<PointerButton>(); + } + + if (typeof(T) == typeof(Button<JoystickButton>)) + { + return GetButtonListCount<JoystickButton>(); + } + + return Unsafe.As<List<T>>(list.Data).Count; + } + + // ReSharper disable NotDisposedResourceIsReturned - Nope, sorry, not adding a reference to JetBrains.Annotations. + internal static IEnumerator<T> EnumerateList<T>(InputReadOnlyList<T> list) + { + if (typeof(T) == typeof(Button<KeyName>)) + { + return (IEnumerator<T>)Unsafe.As<ButtonList<KeyName>>(list.Data).GetEnumerator(); + } + + if (typeof(T) == typeof(Button<PointerButton>)) + { + return (IEnumerator<T>)Unsafe.As<ButtonList<PointerButton>>(list.Data).GetEnumerator(); + } + + if (typeof(T) == typeof(Button<JoystickButton>)) + { + return (IEnumerator<T>)Unsafe.As<ButtonList<JoystickButton>>(list.Data).GetEnumerator(); + } + + return Unsafe.As<List<T>>(list.Data).GetEnumerator(); + } // ReSharper restore NotDisposedResourceIsReturned + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static T ElementAt<T>(InputReadOnlyList<T> list, int index) + { + if (typeof(T) == typeof(Button<KeyName>)) + { + return (T)(object)Unsafe.As<ButtonList<KeyName>>(list.Data)[index]; + } + + if (typeof(T) == typeof(Button<PointerButton>)) + { + return (T)(object)Unsafe.As<ButtonList<PointerButton>>(list.Data)[index]; + } + + if (typeof(T) == typeof(Button<JoystickButton>)) + { + return (T)(object)Unsafe.As<ButtonList<JoystickButton>>(list.Data)[index]; + } + + return Unsafe.As<List<T>>(list.Data)[index]; + } +} diff --git a/sources/Input/Input/InputReadOnlyList.cs b/sources/Input/Input/InputReadOnlyList.cs new file mode 100644 index 0000000000..db3f1546a3 --- /dev/null +++ b/sources/Input/Input/InputReadOnlyList.cs @@ -0,0 +1,32 @@ +using System.Collections; + +namespace Silk.NET.Input; + +/// <summary> +/// An opaque implementation of <see cref="IReadOnlyList{T}"/> that is optimised for storing a <c>Silk.NET.Input</c> +/// type specified by <typeparamref name="T"/> using the most memory-efficient mechanism available. +/// </summary> +/// <typeparam name="T">The <c>Silk.NET.Input</c> type to store.</typeparam> +public readonly struct InputReadOnlyList<T> : IReadOnlyList<T> +{ + internal object Data { get; } + + internal InputReadOnlyList(object data) => Data = data; + + /// <summary> + /// Creates an <see cref="InputReadOnlyList{T}"/> from a <see cref="IReadOnlyList{T}"/>. + /// </summary> + /// <param name="other">The list to copy.</param> + public InputReadOnlyList(IReadOnlyList<T> other) => this = InputMarshal.Clone(other).List; + + /// <inheritdoc /> + public IEnumerator<T> GetEnumerator() => InputMarshal.EnumerateList(this); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// <inheritdoc /> + public int Count => InputMarshal.GetListCount(this); + + /// <inheritdoc /> + public T this[int index] => InputMarshal.ElementAt(this, index); +} diff --git a/sources/Input/Input/InputWindowExtensions.cs b/sources/Input/Input/InputWindowExtensions.cs new file mode 100644 index 0000000000..2933bc7527 --- /dev/null +++ b/sources/Input/Input/InputWindowExtensions.cs @@ -0,0 +1,55 @@ +// 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 System.Numerics; +using Silk.NET.Maths; + +namespace Silk.NET.Input; + +/// <summary> +/// Contains extensions for creating input backends and contexts from <see cref="INativeWindow"/>s. +/// </summary> +public static partial class InputWindowExtensions +{ + /// <summary> + /// Creates an instance of the "reference implementation" of <see cref="IInputBackend"/> for the given + /// <see cref="INativeWindow"/>, provided that this was also sourced from the "reference implementation" of the + /// windowing API. + /// </summary> + /// <remarks> + /// Regarding the threading rules documented on <see cref="IInputBackend"/>, <see cref="IInputBackend.Update"/> + /// must only be called on the "main thread," i.e. the same thread that windowing operates on. + /// </remarks> + /// <param name="window">The window to create an input backend from.</param> + /// <returns>The input backend.</returns> + /// <exception cref="NotSupportedException"> + /// If the given <see cref="INativeWindow"/> is not compatible with the reference implementation for this platform. + /// </exception> + public static partial IInputBackend CreateInputBackend(this INativeWindow window); + + /// <summary> + /// Creates an <see cref="InputContext"/> that uses the "reference implementation" of <see cref="IInputBackend"/> + /// for the given <see cref="INativeWindow"/> as its only backend, provided that the <see cref="INativeWindow"/> was + /// also sourced from the "reference implementation" of the windowing API. + /// </summary> + /// <remarks> + /// Regarding the threading rules documented on <see cref="InputContext"/>, <see cref="InputContext.Update"/> + /// must only be called on the "main thread," i.e. the same thread that windowing operates on. + /// </remarks> + /// <param name="window">The window to create an input backend from.</param> + /// <returns> + /// The <see cref="InputContext"/> created with the instantiated input backend as its only backend. + /// </returns> + /// <exception cref="NotSupportedException"> + /// If the given <see cref="INativeWindow"/> is not compatible with the reference implementation for this platform. + /// </exception> + public static InputContext CreateInput(this INativeWindow window) + { + var ret = new InputContext(); + ret.Backends.Add(window.CreateInputBackend()); + return ret; + } +} diff --git a/sources/Input/Input/JoystickAxisMoveEvent.cs b/sources/Input/Input/JoystickAxisMoveEvent.cs new file mode 100644 index 0000000000..18e8a1f72c --- /dev/null +++ b/sources/Input/Input/JoystickAxisMoveEvent.cs @@ -0,0 +1,15 @@ +using System.Diagnostics; + +namespace Silk.NET.Input; + +/// <summary> +/// Contains information pertaining to the movement of a joystick axis. +/// </summary> +/// <param name="Joystick">The joystick on which the axis being moved resides.</param> +/// <param name="Timestamp"> +/// The timestamp (as retrieved from <see cref="Stopwatch.GetTimestamp"/>) at which the event occurred. +/// </param> +/// <param name="Axis">The index of the axis being moved.</param> +/// <param name="Value">The new value of the axis, typically between <c>0.0</c> and <c>1.0</c>.</param> +/// <param name="Delta">The change in <see cref="Value"/> as a result of this event.</param> +public readonly record struct JoystickAxisMoveEvent(IJoystick Joystick, long Timestamp, int Axis, float Value, float Delta); \ No newline at end of file diff --git a/sources/Input/Input/JoystickButton.cs b/sources/Input/Input/JoystickButton.cs new file mode 100644 index 0000000000..f76482034e --- /dev/null +++ b/sources/Input/Input/JoystickButton.cs @@ -0,0 +1,109 @@ +namespace Silk.NET.Input; + +/// <summary> +/// Enumerates the buttons of a joystick. +/// </summary> +public enum JoystickButton +{ + /// <summary> + /// The button was not recognised. + /// </summary> + Unknown, + + /// <summary> + /// The down-most button of the primary button cluster. + /// </summary> + ButtonDown, + + /// <summary> + /// The "A" button on Xbox (and similar) controllers. Equivalent to <see cref="ButtonDown"/>. + /// </summary> + A = ButtonDown, + + /// <summary> + /// The rightmost button of the primary button cluster. + /// </summary> + ButtonRight, + + /// <summary> + /// The "B" button on Xbox (and similar) controllers. Equivalent to <see cref="ButtonRight"/>. + /// </summary> + B = ButtonRight, + + /// <summary> + /// The leftmost button of the primary button cluster. + /// </summary> + ButtonLeft, + + /// <summary> + /// The "X" button on Xbox (and similar) controllers. Equivalent to <see cref="ButtonLeft"/>. + /// </summary> + X = ButtonLeft, + + /// <summary> + /// The upmost button of the primary button cluster. + /// </summary> + ButtonUp, + + /// <summary> + /// The "Y" button on Xbox (and similar) controllers. Equivalent to <see cref="ButtonUp"/>. + /// </summary> + Y = ButtonUp, + + /// <summary> + /// The leftmost bumper/shoulder button. + /// </summary> + LeftBumper, + + /// <summary> + /// The rightmost bumper/shoulder button. + /// </summary> + RightBumper, + + /// <summary> + /// The "back" button. + /// </summary> + Back, + + /// <summary> + /// The "start" button. + /// </summary> + Start, + + /// <summary> + /// The "home" button. + /// </summary> + Home, + + /// <summary> + /// The leftmost thumbstick. This button represents the stick being pressed down. + /// </summary> + LeftStick, + + /// <summary> + /// The rightmost thumbstick. This button represents the stick being pressed down. + /// </summary> + RightStick, + + /// <summary> + /// The upmost button of the D-Pad button cluster. + /// </summary> + DPadUp, + + /// <summary> + /// The rightmost button of the D-Pad button cluster. + /// </summary> + DPadRight, + + /// <summary> + /// The down-most button of the D-Pad button cluster. + /// </summary> + DPadDown, + + /// <summary> + /// The leftmost button of the D-Pad button cluster. + /// </summary> + DPadLeft, + + // BEFORE ADDING A NEW ITEM MAKE SURE YOU CHANGE LastJoystickButton IN InputMarshal +} diff --git a/sources/Input/Input/JoystickHatMoveEvent.cs b/sources/Input/Input/JoystickHatMoveEvent.cs new file mode 100644 index 0000000000..67b3defd95 --- /dev/null +++ b/sources/Input/Input/JoystickHatMoveEvent.cs @@ -0,0 +1,15 @@ +using System.Diagnostics; +using System.Numerics; + +namespace Silk.NET.Input; + +/// <summary> +/// Contains information pertaining to the movement of a joystick hat. +/// </summary> +/// <param name="Joystick">The joystick on which the hat being moved resides.</param> +/// <param name="Timestamp"> +/// The timestamp (as retrieved from <see cref="Stopwatch.GetTimestamp"/>) at which the event occurred. +/// </param> +/// <param name="Value">The position of the hat after this event.</param> +/// <param name="Delta">The change in <see cref="Value"/> as a result of this event.</param> +public readonly record struct JoystickHatMoveEvent(IJoystick Joystick, long Timestamp, Vector2 Value, Vector2 Delta); \ No newline at end of file diff --git a/sources/Input/Input/JoystickState.cs b/sources/Input/Input/JoystickState.cs new file mode 100644 index 0000000000..9d80287b7b --- /dev/null +++ b/sources/Input/Input/JoystickState.cs @@ -0,0 +1,24 @@ +using System.Numerics; + +namespace Silk.NET.Input; + +/// <summary> +/// Contains user input received from an <see cref="IJoystick"/>. +/// </summary> +public class JoystickState +{ + /// <summary> + /// Gets the state of the joystick axes between <c>-1.0</c> and <c>1.0</c> + /// </summary> + public InputReadOnlyList<float> Axes { get; } + + /// <summary> + /// Gets the joystick button state, denoting which buttons are pressed/depressed. + /// </summary> + public ButtonReadOnlyList<JoystickButton> Buttons { get; } + + /// <summary> + /// Gets the state of the joystick hats as vectors between <c>-1.0</c> and <c>1.0</c>. + /// </summary> + public InputReadOnlyList<Vector2> Hats { get; } +} \ No newline at end of file diff --git a/sources/Input/Input/Joysticks.cs b/sources/Input/Input/Joysticks.cs new file mode 100644 index 0000000000..f576e6f85f --- /dev/null +++ b/sources/Input/Input/Joysticks.cs @@ -0,0 +1,41 @@ +namespace Silk.NET.Input; + +/// <summary> +/// Represents a collection of <see cref="IJoystick"/>s from which input events can be received. +/// </summary> +public sealed class Joysticks : InputContextDeviceList<IJoystick>, IJoystickInputHandler +{ + internal Joysticks(InputContext ctx) + : base(ctx) { } + + /// <summary> + /// Raised when state pertaining to a pushable button on the joystick changes (e.g. button up, button down). + /// </summary> + public event Action<ButtonChangedEvent<JoystickButton>>? ButtonChanged; + + /// <summary> + /// Raised when a movable axis on the joystick changes position. + /// </summary> + public event Action<JoystickAxisMoveEvent>? AxisMove; + + /// <summary> + /// Raised when a joystick hat moves. + /// </summary> + public event Action<JoystickHatMoveEvent>? HatMove; + + internal void HandleButtonChanged(ButtonChangedEvent<JoystickButton> @event) => + ButtonChanged?.Invoke(@event); + + void IButtonInputHandler<JoystickButton>.HandleButtonChanged( + ButtonChangedEvent<JoystickButton> @event + ) => HandleButtonChanged(@event); + + internal void HandleAxisMove(JoystickAxisMoveEvent @event) => AxisMove?.Invoke(@event); + + void IJoystickInputHandler.HandleAxisMove(JoystickAxisMoveEvent @event) => + HandleAxisMove(@event); + + internal void HandleHatMove(JoystickHatMoveEvent @event) => HatMove?.Invoke(@event); + + void IJoystickInputHandler.HandleHatMove(JoystickHatMoveEvent @event) => HandleHatMove(@event); +} diff --git a/sources/Input/Input/KeyChangedEvent.cs b/sources/Input/Input/KeyChangedEvent.cs new file mode 100644 index 0000000000..3868ceaccd --- /dev/null +++ b/sources/Input/Input/KeyChangedEvent.cs @@ -0,0 +1,16 @@ +using System.Diagnostics; + +namespace Silk.NET.Input; + +/// <summary> +/// Contains information pertaining to a key press state change. +/// </summary> +/// <param name="Keyboard">The keyboard on which the key being pressed or depressed resides.</param> +/// <param name="Timestamp"> +/// The timestamp (as retrieved from <see cref="Stopwatch.GetTimestamp"/>) at which the event occurred. +/// </param> +/// <param name="Key">The new state of the key being pressed or depressed.</param> +/// <param name="Previous">The previous state of the key.</param> +/// <param name="IsRepeat">Whether this is an event that has been repeated at an implementation-defined rate.</param> +/// <param name="Modifiers">The active key modifiers at the time the event was raised.</param> +public readonly record struct KeyChangedEvent(IKeyboard Keyboard, long Timestamp, Button<KeyName> Key, Button<KeyName> Previous, bool IsRepeat, KeyModifiers Modifiers); \ No newline at end of file diff --git a/sources/Input/Input/KeyCharEvent.cs b/sources/Input/Input/KeyCharEvent.cs new file mode 100644 index 0000000000..8e557d06df --- /dev/null +++ b/sources/Input/Input/KeyCharEvent.cs @@ -0,0 +1,13 @@ +using System.Diagnostics; + +namespace Silk.NET.Input; + +/// <summary> +/// Contains information pertaining to a character being typed on a keyboard. +/// </summary> +/// <param name="Keyboard">The keyboard with which the end user typed a character.</param> +/// <param name="Timestamp"> +/// The timestamp (as retrieved from <see cref="Stopwatch.GetTimestamp"/>) at which the event occurred. +/// </param> +/// <param name="Character">The character that was typed. A <c>null</c> character denotes a backspace.</param> +public readonly record struct KeyCharEvent(IKeyboard Keyboard, long Timestamp, char? Character); \ No newline at end of file diff --git a/sources/Input/Input/KeyModifiers.cs b/sources/Input/Input/KeyModifiers.cs new file mode 100644 index 0000000000..8b6660641f --- /dev/null +++ b/sources/Input/Input/KeyModifiers.cs @@ -0,0 +1,40 @@ +namespace Silk.NET.Input; + +/// <summary> +/// A bitmask denoting the modifier keys that can be active when a key press occurs to modify its behaviour. +/// </summary> +[Flags] +public enum KeyModifiers +{ + /// <summary>No modifier keys are active.</summary> + None = 0, + /// <summary>The left "shift" key.</summary> + ShiftLeft = 1 << 0, + + /// <summary>The right "shift" key.</summary> + ShiftRight = 1 << 1, + + /// <summary>The left "control" key.</summary> + ControlLeft = 1 << 2, + + /// <summary>The right "control" key.</summary> + ControlRight = 1 << 3, + + /// <summary>The left "alt" key.</summary> + AltLeft = 1 << 4, + + /// <summary>The right "alt" key.</summary> + AltRight = 1 << 5, + + /// <summary>The left "super" (e.g. Windows/Start) key.</summary> + SuperLeft = 1 << 6, + + /// <summary>The right "super" (e.g. Windows/Start) key.</summary> + SuperRight = 1 << 7, + + /// <summary>The "num lock" key.</summary> + NumLock = 1 << 8, + + /// <summary>The "caps lock" key.</summary> + CapsLock = 1 << 9 +} \ No newline at end of file diff --git a/sources/Input/Input/KeyName.cs b/sources/Input/Input/KeyName.cs new file mode 100644 index 0000000000..2def1ba7d7 --- /dev/null +++ b/sources/Input/Input/KeyName.cs @@ -0,0 +1,828 @@ +namespace Silk.NET.Input; + +/// <summary> +/// Enumerates names for physical key positions as defined by the +/// <see href="https://www.usb.org/sites/default/files/documents/hut1_12v2.pdf">USB HID Usage Tables</see> published by +/// the USB-IF. Note that these denote an en-US-centric definition of the keys that reside at each physical position, +/// and does not take account of keyboard layout. That is, <see cref="Q"/> represents the <c>Q</c> key on a QWERTY +/// keyboard but represents the <c>"</c> key on a Dvorak keyboard. Use <see cref="IKeyboard.TryGetKeyName"/> to +/// determine the localised name of a physical key position name (<see cref="KeyName"/>) when taking account of the +/// user's selected keyboard layout. +/// </summary> +public enum KeyName +{ + // BEFORE ADDING ANYTHING TO THIS FILE MAKE SURE YOU REALISE THAT InputMarshal RELIES ON ASSUMPTIONS ON THE VALUES + + // These values are from usage page 0x07 (USB keyboard page). + /// <summary> + /// A key that was not recognised. + /// </summary> + Unknown = 0, + + /// <summary>The "A" key.</summary> + A = 4, + + /// <summary>The "B" key.</summary> + B = 5, + + /// <summary>The "C" key.</summary> + C = 6, + + /// <summary>The "D" key.</summary> + D = 7, + + /// <summary>The "E" key.</summary> + E = 8, + + /// <summary>The "F" key.</summary> + F = 9, + + /// <summary>The "G" key.</summary> + G = 10, + + /// <summary>The "H" key.</summary> + H = 11, + + /// <summary>The "I" key.</summary> + I = 12, + + /// <summary>The "J" key.</summary> + J = 13, + + /// <summary>The "K" key.</summary> + K = 14, + + /// <summary>The "L" key.</summary> + L = 15, + + /// <summary>The "M" key.</summary> + M = 16, + + /// <summary>The "N" key.</summary> + N = 17, + + /// <summary>The "O" key.</summary> + O = 18, + + /// <summary>The "P" key.</summary> + P = 19, + + /// <summary>The "Q" key.</summary> + Q = 20, + + /// <summary>The "R" key.</summary> + R = 21, + + /// <summary>The "S" key.</summary> + S = 22, + + /// <summary>The "T" key.</summary> + T = 23, + + /// <summary>The "U" key.</summary> + U = 24, + + /// <summary>The "V" key.</summary> + V = 25, + + /// <summary>The "W" key.</summary> + W = 26, + + /// <summary>The "X" key.</summary> + X = 27, + + /// <summary>The "Y" key.</summary> + Y = 28, + + /// <summary>The "Z" key.</summary> + Z = 29, + + /// <summary>The "1" key.</summary> + Number1 = 30, + + /// <summary>The "2" key.</summary> + Number2 = 31, + + /// <summary>The "3" key.</summary> + Number3 = 32, + + /// <summary>The "4" key.</summary> + Number4 = 33, + + /// <summary>The "5" key.</summary> + Number5 = 34, + + /// <summary>The "6" key.</summary> + Number6 = 35, + + /// <summary>The "7" key.</summary> + Number7 = 36, + + /// <summary>The "8" key.</summary> + Number8 = 37, + + /// <summary>The "9" key.</summary> + Number9 = 38, + + /// <summary>The "0" key.</summary> + Number0 = 39, + + /// <summary>The "return" key.</summary> + Return = 40, + + /// <summary>The "escape" key.</summary> + Escape = 41, + + /// <summary>The "backspace" key.</summary> + Backspace = 42, + + /// <summary>The "tab" key.</summary> + Tab = 43, + + /// <summary>The "space" key.</summary> + Space = 44, + + /// <summary>The "minus" key.</summary> + Minus = 45, + + /// <summary>The "equals" key.</summary> + Equals = 46, + + /// <summary>The "left bracket" key.</summary> + LeftBracket = 47, + + /// <summary>The "right bracket" key.</summary> + RightBracket = 48, + + /// <summary>The "backslash" key.</summary> + Backslash = 49, + + /// <summary> + /// A key with region-specific meanings. + /// </summary> + /// <remarks> + /// <list type="bullet"> + /// <item><term>American</term><description> \|</description></item> + /// <item><term>Belgium</term><description> µ`£</description></item> + /// <item><term>Canadian-French</term><description> <}></description></item> + /// <item><term>Danish</term><description>’*</description></item> + /// <item><term>Dutch</term><description> <></description></item> + /// <item><term>French</term><description>*µ</description></item> + /// <item><term>German</term><description> #’</description></item> + /// <item><term>Italian</term><description> ù§</description></item> + /// <item><term>Latin-American</term><description> }`]</description></item> + /// <item><term>Norwegian</term><description>,*</description></item> + /// <item><term>Spanish</term><description> }Ç</description></item> + /// <item><term>Swedish</term><description> , *</description></item> + /// <item><term>Swiss</term><description> $£</description></item> + /// <item><term>British</term><description> #~.</description></item> + /// </list> + /// </remarks> + NonUs1 = 50, + + /// <summary>The "semicolon" key.</summary> + Semicolon = 51, + + /// <summary>The "apostrophe" key.</summary> + Apostrophe = 52, + + /// <summary>The "grave" key.</summary> + Grave = 53, + + /// <summary>The "comma" key.</summary> + Comma = 54, + + /// <summary>The "period" key.</summary> + Period = 55, + + /// <summary>The "slash" key.</summary> + Slash = 56, + + /// <summary>The "caps lock" key.</summary> + CapsLock = 57, + + /// <summary>The first function key.</summary> + F1 = 58, + + /// <summary>The second function key.</summary> + F2 = 59, + + /// <summary>The third function key.</summary> + F3 = 60, + + /// <summary>The fourth function key.</summary> + F4 = 61, + + /// <summary>The fifth function key.</summary> + F5 = 62, + + /// <summary>The sixth function key.</summary> + F6 = 63, + + /// <summary>The seventh function key.</summary> + F7 = 64, + + /// <summary>The eighth function key.</summary> + F8 = 65, + + /// <summary>The ninth function key.</summary> + F9 = 66, + + /// <summary>The tenth function key.</summary> + F10 = 67, + + /// <summary>The eleventh function key.</summary> + F11 = 68, + + /// <summary>The twelfth function key.</summary> + F12 = 69, + + /// <summary>The "print screen" key.</summary> + PrintScreen = 70, + + /// <summary>The "scroll lock" key.</summary> + ScrollLock = 71, + + /// <summary>The "pause" key.</summary> + Pause = 72, + + /// <summary>The "insert" key.</summary> + Insert = 73, + + /// <summary>The "home" key.</summary> + Home = 74, + + /// <summary>The "page up" key.</summary> + PageUp = 75, + + /// <summary>The "delete" key.</summary> + Delete = 76, + + /// <summary>The "end" key.</summary> + End = 77, + + /// <summary>The "page down" key.</summary> + PageDown = 78, + + /// <summary>The "right" key.</summary> + Right = 79, + + /// <summary>The "left" key.</summary> + Left = 80, + + /// <summary>The "down" key.</summary> + Down = 81, + + /// <summary>The "up" key.</summary> + Up = 82, + + /// <summary>The "num lock clear" key.</summary> + NumLockClear = 83, + + /// <summary>The "divide" key on the keypad.</summary> + KeypadDivide = 84, + + /// <summary>The "multiply" key on the keypad.</summary> + KeypadMultiply = 85, + + /// <summary>The "minus" key on the keypad.</summary> + KeypadMinus = 86, + + /// <summary>The "plus" key on the keypad.</summary> + KeypadPlus = 87, + + /// <summary>The "enter" key on the keypad.</summary> + KeypadEnter = 88, + + /// <summary>The "1" key on the keypad.</summary> + Keypad1 = 89, + + /// <summary>The "2" key on the keypad.</summary> + Keypad2 = 90, + + /// <summary>The "3" key on the keypad.</summary> + Keypad3 = 91, + + /// <summary>The "4" key on the keypad.</summary> + Keypad4 = 92, + + /// <summary>The "5" key on the keypad.</summary> + Keypad5 = 93, + + /// <summary>The "6" key on the keypad.</summary> + Keypad6 = 94, + + /// <summary>The "7" key on the keypad.</summary> + Keypad7 = 95, + + /// <summary>The "8" key on the keypad.</summary> + Keypad8 = 96, + + /// <summary>The "9" key on the keypad.</summary> + Keypad9 = 97, + + /// <summary>The "0" key on the keypad.</summary> + Keypad0 = 98, + + /// <summary>The "period" key on the keypad.</summary> + KeypadPeriod = 99, + + /// <summary> + /// A key with region-specific meanings, typically near the Left-Shift key in AT-102 implementations. + /// </summary> + /// <remarks> + /// <item><term>Belg</term><description><\></description></item> + /// <item><term>FrCa</term><description>«°»</description></item> + /// <item><term>Dan</term><description><\></description></item> + /// <item><term>Dutch</term><description>]|[</description></item> + /// <item><term>Fren</term><description><></description></item> + /// <item><term>Ger</term><description><|></description></item> + /// <item><term>Ital</term><description><></description></item> + /// <item><term>LatAm</term><description><></description></item> + /// <item><term>Nor</term><description><></description></item> + /// <item><term>Span</term><description><></description></item> + /// <item><term>Swed</term><description><|></description></item> + /// <item><term>Swiss</term><description><\></description></item> + /// <item><term>UK</term><description>\|</description></item> + /// <item><term>Brazil</term><description>\|</description></item> + /// </remarks> + NonUs2 = 100, + + /// <summary>A key for application-defined functions.</summary> + Application = 101, + + /// <summary>The "power" key.</summary> + Power = 102, + + /// <summary>The "equals" key on the keypad.</summary> + KeypadEquals = 103, + + /// <summary>The thirteenth function key.</summary> + F13 = 104, + + /// <summary>The fourteenth function key.</summary> + F14 = 105, + + /// <summary>The fifteenth function key.</summary> + F15 = 106, + + /// <summary>The sixteenth function key.</summary> + F16 = 107, + + /// <summary>The seventeenth function key.</summary> + F17 = 108, + + /// <summary>The eighteenth function key.</summary> + F18 = 109, + + /// <summary>The nineteenth function key.</summary> + F19 = 110, + + /// <summary>The twentieth function key.</summary> + F20 = 111, + + /// <summary>The twenty-first function key.</summary> + F21 = 112, + + /// <summary>The twenty-second function key.</summary> + F22 = 113, + + /// <summary>The twenty-third function key.</summary> + F23 = 114, + + /// <summary>The twenty-fourth function key.</summary> + F24 = 115, + + /// <summary>The "execute" key.</summary> + Execute = 116, + + /// <summary>The "help" key.</summary> + Help = 117, + + /// <summary>The "menu" key.</summary> + Menu = 118, + + /// <summary>The "select" key.</summary> + Select = 119, + + /// <summary>The "stop" key.</summary> + Stop = 120, + + /// <summary>The "again" key.</summary> + Again = 121, + + /// <summary>The "undo" key.</summary> + Undo = 122, + + /// <summary>The "cut" key.</summary> + Cut = 123, + + /// <summary>The "copy" key.</summary> + Copy = 124, + + /// <summary>The "paste" key.</summary> + Paste = 125, + + /// <summary>The "find" key.</summary> + Find = 126, + + /// <summary>The "mute" key.</summary> + Mute = 127, + + /// <summary>The "volume up" key.</summary> + VolumeUp = 128, + + /// <summary>The "volume down" key.</summary> + VolumeDown = 129, + + // BEFORE ADDING ANYTHING TO THIS FILE MAKE SURE YOU REALISE THAT InputMarshal RELIES ON ASSUMPTIONS ON THE VALUES + + /// <summary>The "comma" key on the keypad.</summary> + KeypadComma = 133, + + /// <summary>The alternative "equals" key on the keypad as typically found on AS-400 keyboards.</summary> + OtherKeypadEquals = 134, + + /// <summary>The first international key.</summary> + International1 = 135, + + /// <summary>The second international key.</summary> + International2 = 136, + + /// <summary>The third international key.</summary> + International3 = 137, + + /// <summary>The fourth international key.</summary> + International4 = 138, + + /// <summary>The fifth international key.</summary> + International5 = 139, + + /// <summary>The sixth international key.</summary> + International6 = 140, + + /// <summary>The seventh international key.</summary> + International7 = 141, + + /// <summary>The eighth international key.</summary> + International8 = 142, + + /// <summary>The ninth international key.</summary> + International9 = 143, + + /// <summary>The first language key.</summary> + Lang1 = 144, + + /// <summary>The second language key.</summary> + Lang2 = 145, + + /// <summary>The third language key.</summary> + Lang3 = 146, + + /// <summary>The fourth language key.</summary> + Lang4 = 147, + + /// <summary>The fifth language key.</summary> + Lang5 = 148, + + /// <summary>The sixth language key.</summary> + Lang6 = 149, + + /// <summary>The seventh language key.</summary> + Lang7 = 150, + + /// <summary>The eighth language key.</summary> + Lang8 = 151, + + /// <summary>The ninth language key.</summary> + Lang9 = 152, + + /// <summary>The alternative "erase" key, for example an Erase-Eaze™ key.</summary> + AlternativeErase = 153, + + /// <summary>The "system request" key.</summary> + SystemRequest = 154, + + /// <summary>The "cancel" key.</summary> + Cancel = 155, + + /// <summary>The "clear" key.</summary> + Clear = 156, + + /// <summary>The "prior" key.</summary> + Prior = 157, + + /// <summary>An alternative "return" key.</summary> + Return2 = 158, + + /// <summary>The "separator" key.</summary> + Separator = 159, + + /// <summary>The "out" key.</summary> + Out = 160, + + /// <summary>The "operation" key.</summary> + Oper = 161, + + /// <summary>The "clear again" key.</summary> + ClearAgain = 162, + + /// <summary>The "cursor select" key.</summary> + /// <remarks> + /// For more information consult IBM's "3174 Establishment Controller - Terminal User's Reference for Expanded + /// Functions" (GA23-03320-02, May 1989) + /// </remarks> + CursorSelect = 163, + + /// <summary>The "extend select" key.</summary> + /// <remarks> + /// For more information consult IBM's "3174 Establishment Controller - Terminal User's Reference for Expanded + /// Functions" (GA23-03320-02, May 1989) + /// </remarks> + ExtendSelect = 164, + + // BEFORE ADDING ANYTHING TO THIS FILE MAKE SURE YOU REALISE THAT InputMarshal RELIES ON ASSUMPTIONS ON THE VALUES + + /// <summary>The "00" key on the keypad.</summary> + Keypad00 = 176, + + /// <summary>The "000" key on the keypad.</summary> + Keypad000 = 177, + + /// <summary>The "thousands separator" key.</summary> + /// <remarks>Interpreted as a comma for en-US.</remarks> + ThousandsSeparator = 178, + + /// <summary>The "decimal separator" key.</summary> + /// <remarks>Interpreted as a period for en-US.</remarks> + DecimalSeparator = 179, + + /// <summary>The "currency unit" key.</summary> + /// <remarks>Interpreted as a dollar sign for en-US.</remarks> + CurrencyUnit = 180, + + /// <summary>The "currencySubunit" key.</summary> + /// <remarks>Interpreted as a cents symbol for en-US.</remarks> + CurrencySubunit = 181, + + /// <summary>The "leftParenthesis" key on the keypad.</summary> + KeypadLeftParenthesis = 182, + + /// <summary>The "rightParenthesis" key on the keypad.</summary> + KeypadRightParenthesis = 183, + + /// <summary>The "leftBrace" key on the keypad.</summary> + KeypadLeftBrace = 184, + + /// <summary>The "rightBrace" key on the keypad.</summary> + KeypadRightBrace = 185, + + /// <summary>The "tab" key on the keypad.</summary> + KeypadTab = 186, + + /// <summary>The "backspace" key on the keypad.</summary> + KeypadBackspace = 187, + + /// <summary>The "a" key on the keypad.</summary> + KeypadA = 188, + + /// <summary>The "b" key on the keypad.</summary> + KeypadB = 189, + + /// <summary>The "c" key on the keypad.</summary> + KeypadC = 190, + + /// <summary>The "d" key on the keypad.</summary> + KeypadD = 191, + + /// <summary>The "e" key on the keypad.</summary> + KeypadE = 192, + + /// <summary>The "f" key on the keypad.</summary> + KeypadF = 193, + + /// <summary>The "xor" key on the keypad.</summary> + KeypadXor = 194, + + /// <summary>The "power" key on the keypad.</summary> + KeypadPower = 195, + + /// <summary>The "percent" key on the keypad.</summary> + KeypadPercent = 196, + + /// <summary>The "less" key on the keypad.</summary> + KeypadLess = 197, + + /// <summary>The "greater" key on the keypad.</summary> + KeypadGreater = 198, + + /// <summary>The "ampersand" key on the keypad.</summary> + KeypadAmpersand = 199, + + /// <summary>The "doubleAmpersand" key on the keypad.</summary> + KeypadDoubleAmpersand = 200, + + /// <summary>The "vertical bar" key on the keypad.</summary> + KeypadVerticalBar = 201, + + /// <summary>The "double vertical bar" key on the keypad.</summary> + KeypadDoubleVerticalBar = 202, + + /// <summary>The "colon" key on the keypad.</summary> + KeypadColon = 203, + + /// <summary>The "hash" key on the keypad.</summary> + KeypadHash = 204, + + /// <summary>The "space" key on the keypad.</summary> + KeypadSpace = 205, + + /// <summary>The "@" key on the keypad.</summary> + KeypadAt = 206, + + /// <summary>The "exclamation" key on the keypad.</summary> + KeypadExclamation = 207, + + /// <summary>The "memory store" key on the keypad.</summary> + KeypadMemoryStore = 208, + + /// <summary>The "memory recall" key on the keypad.</summary> + KeypadMemoryRecall = 209, + + /// <summary>The "memory clear" key on the keypad.</summary> + KeypadMemoryClear = 210, + + /// <summary>The "memory add" key on the keypad.</summary> + KeypadMemoryAdd = 211, + + /// <summary>The "memory subtract" key on the keypad.</summary> + KeypadMemorySubtract = 212, + + /// <summary>The "memory multiply" key on the keypad.</summary> + KeypadMemoryMultiply = 213, + + /// <summary>The "memory divide" key on the keypad.</summary> + KeypadMemoryDivide = 214, + + /// <summary>The "plus/minus" key on the keypad.</summary> + KeypadPlusMinus = 215, + + /// <summary>The "clear" key on the keypad.</summary> + KeypadClear = 216, + + /// <summary>The "clear entry" key on the keypad.</summary> + KeypadClearEntry = 217, + + /// <summary>The "binary" key on the keypad.</summary> + KeypadBinary = 218, + + /// <summary>The "octal" key on the keypad.</summary> + KeypadOctal = 219, + + /// <summary>The "decimal" key on the keypad.</summary> + KeypadDecimal = 220, + + /// <summary>The "hexadecimal" key on the keypad.</summary> + KeypadHexadecimal = 221, + + // BEFORE ADDING ANYTHING TO THIS FILE MAKE SURE YOU REALISE THAT InputMarshal RELIES ON ASSUMPTIONS ON THE VALUES + + /// <summary>The left "control" key.</summary> + ControlLeft = 224, + + /// <summary>The left "shift" key.</summary> + ShiftLeft = 225, + + /// <summary>The left "alt" key.</summary> + AltLeft = 226, + + /// <summary>The left "super" (e.g. Windows/Start) key.</summary> + SuperLeft = 227, + + /// <summary>The right "control" key.</summary> + ControlRight = 228, + + /// <summary>The right "shift" key.</summary> + ShiftRight = 229, + + /// <summary>The right "alt" key.</summary> + AltRight = 230, + + /// <summary>The right "super" (e.g. Windows/Start) key.</summary> + SuperRight = 231, + + // 232-256..... wtf? + // BEFORE ADDING ANYTHING TO THIS FILE MAKE SURE YOU REALISE THAT InputMarshal RELIES ON ASSUMPTIONS ON THE VALUES + + /// <summary>The "mode" key.</summary> + Mode = 257, + + // These values are mapped from usage page 0x0C (USB consumer page). + /// <summary>The "sleep" key.</summary> + Sleep = 258, + + /// <summary>The "wake" key.</summary> + Wake = 259, + + /// <summary>The "channel increment" key.</summary> + ChannelIncrement = 260, + + /// <summary>The "channel decrement" key.</summary> + ChannelDecrement = 261, + + /// <summary>The "play" media key.</summary> + MediaPlay = 262, + + /// <summary>The "pause" media key.</summary> + MediaPause = 263, + + /// <summary>The "record" media key.</summary> + MediaRecord = 264, + + /// <summary>The "fast forward" media key.</summary> + MediaFastForward = 265, + + /// <summary>The "rewind" media key.</summary> + MediaRewind = 266, + + /// <summary>The "next track" media key.</summary> + MediaNextTrack = 267, + + /// <summary>The "previous track" media key.</summary> + MediaPreviousTrack = 268, + + /// <summary>The "stop" media key.</summary> + MediaStop = 269, + + /// <summary>The "eject" media key.</summary> + MediaEject = 270, + + /// <summary>The "play/pause" media key.</summary> + MediaPlayPause = 271, + + /// <summary>The "select" media key.</summary> + MediaSelect = 272, + + /// <summary>The "new" application key.</summary> + ApplicationNew = 273, + + /// <summary>The "open" application key.</summary> + ApplicationOpen = 274, + + /// <summary>The "close" application key.</summary> + ApplicationClose = 275, + + /// <summary>The "exit" application key.</summary> + ApplicationExit = 276, + + /// <summary>The "save" application key.</summary> + ApplicationSave = 277, + + /// <summary>The "print" application key.</summary> + ApplicationPrint = 278, + + /// <summary>The "properties" application key.</summary> + ApplicationProperties = 279, + + /// <summary>The "search" application key.</summary> + ApplicationSearch = 280, + + /// <summary>The "home" application key.</summary> + ApplicationHome = 281, + + /// <summary>The "back" application key.</summary> + ApplicationBack = 282, + + /// <summary>The "forward" application key.</summary> + ApplicationForward = 283, + + /// <summary>The "stop" application key.</summary> + ApplicationStop = 284, + + /// <summary>The "refresh" application key.</summary> + ApplicationRefresh = 285, + + /// <summary>The "bookmarks" application key.</summary> + ApplicationBookmarks = 286, + + // BEFORE ADDING ANYTHING TO THIS FILE MAKE SURE YOU REALISE THAT InputMarshal RELIES ON ASSUMPTIONS ON THE VALUES + + // 501-512 is reserved for non-standard (i.e. not from an industry-standard HID page) keys. + /// <summary>The left soft key e.g. the left button on a mobile phone.</summary> + /// <remarks>This is not from an industry-standard HID page.</remarks> + SoftLeft = 501, + + /// <summary>The right soft key e.g. the right button on a mobile phone.</summary> + /// <remarks>This is not from an industry-standard HID page.</remarks> + SoftRight = 502, + + /// <summary>The "call" key.</summary> + /// <remarks>This is not from an industry-standard HID page.</remarks> + Call = 503, + + /// <summary>The "end call" key.</summary> + /// <remarks>This is not from an industry-standard HID page.</remarks> + EndCall = 504, + + // BEFORE ADDING ANYTHING TO THIS FILE MAKE SURE YOU REALISE THAT InputMarshal RELIES ON ASSUMPTIONS ON THE VALUES +} diff --git a/sources/Input/Input/KeyboardState.cs b/sources/Input/Input/KeyboardState.cs new file mode 100644 index 0000000000..a9e28ac478 --- /dev/null +++ b/sources/Input/Input/KeyboardState.cs @@ -0,0 +1,23 @@ +namespace Silk.NET.Input; + +/// <summary> +/// Contains user input received from an <see cref="IKeyboard"/>. +/// </summary> +public class KeyboardState +{ + /// <summary> + /// Gets the text that has been typed since <see cref="IKeyboard.BeginInput"/> has been called. This will be cleared + /// when <see cref="IKeyboard.EndInput"/> is called. + /// </summary> + public InputReadOnlyList<char>? Text { get; } + + /// <summary> + /// Gets the key state, denoting which keys are pressed on the keyboard. + /// </summary> + public ButtonReadOnlyList<KeyName> Keys { get; } + + /// <summary> + /// Gets the active modifier keys. + /// </summary> + public KeyModifiers Modifiers { get; } +} \ No newline at end of file diff --git a/sources/Input/Input/Keyboards.cs b/sources/Input/Input/Keyboards.cs new file mode 100644 index 0000000000..34c59a25c6 --- /dev/null +++ b/sources/Input/Input/Keyboards.cs @@ -0,0 +1,33 @@ +namespace Silk.NET.Input; + +/// <summary> +/// Represents a collection of <see cref="IKeyboard"/>s from which input events can be received. +/// </summary> +public sealed class Keyboards : InputContextDeviceList<IKeyboard>, IKeyboardInputHandler +{ + internal Keyboards(InputContext ctx) + : base(ctx) { } + + /// <summary> + /// Raised when state pertaining to a pushable key on the keyboard changes (e.g. key up, key down, key repeat). + /// </summary> + public event Action<KeyChangedEvent>? KeyChanged; + + /// <summary> + /// Raised when the user types a character using the keyboard. + /// </summary> + public event Action<KeyCharEvent>? KeyChar; + + internal void HandleButtonChanged(ButtonChangedEvent<KeyName> @event) { } + + void IButtonInputHandler<KeyName>.HandleButtonChanged(ButtonChangedEvent<KeyName> @event) => + HandleButtonChanged(@event); + + internal void HandleKeyChanged(KeyChangedEvent @event) => KeyChanged?.Invoke(@event); + + void IKeyboardInputHandler.HandleKeyChanged(KeyChangedEvent @event) => HandleKeyChanged(@event); + + internal void HandleKeyChar(KeyCharEvent @event) => KeyChar?.Invoke(@event); + + void IKeyboardInputHandler.HandleKeyChar(KeyCharEvent @event) => HandleKeyChar(@event); +} diff --git a/sources/Input/Input/MouseScrollEvent.cs b/sources/Input/Input/MouseScrollEvent.cs new file mode 100644 index 0000000000..737950479e --- /dev/null +++ b/sources/Input/Input/MouseScrollEvent.cs @@ -0,0 +1,18 @@ +using System.Diagnostics; +using System.Numerics; + +namespace Silk.NET.Input; + +/// <summary> +/// Contains information pertaining to the user scrolling using a mouse scroll wheel. +/// </summary> +/// <param name="Mouse">The mouse on which the scroll wheel resides.</param> +/// <param name="Timestamp"> +/// The timestamp (as retrieved from <see cref="Stopwatch.GetTimestamp"/>) at which the event occurred. +/// </param> +/// <param name="Point">The mouse's active point when the scroll event occurred.</param> +/// <param name="WheelPosition">The <see cref="MouseState.WheelPosition"/> after the event occurred.</param> +/// <param name="Delta"> +/// The change in <see cref="WheelPosition"/> as a result of this event represented as a number of ratchets. +/// </param> +public readonly record struct MouseScrollEvent(IMouse Mouse, long Timestamp, TargetPoint Point, Vector2 WheelPosition, Vector2 Delta); \ No newline at end of file diff --git a/sources/Input/Input/MouseState.cs b/sources/Input/Input/MouseState.cs new file mode 100644 index 0000000000..fe6a776b0e --- /dev/null +++ b/sources/Input/Input/MouseState.cs @@ -0,0 +1,14 @@ +using System.Numerics; + +namespace Silk.NET.Input; + +/// <summary> +/// Contains user input received from an <see cref="IMouse"/>. +/// </summary> +public class MouseState : PointerState +{ + /// <summary> + /// Gets the current position of the scroll wheel in number of ratchets. + /// </summary> + public Vector2 WheelPosition { get; } +} \ No newline at end of file diff --git a/sources/Input/Input/PointChangedEvent.cs b/sources/Input/Input/PointChangedEvent.cs new file mode 100644 index 0000000000..cf9ae15e8c --- /dev/null +++ b/sources/Input/Input/PointChangedEvent.cs @@ -0,0 +1,26 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace Silk.NET.Input; + +/// <summary> +/// Contains information pertaining to a <see cref="TargetPoint"/> change on a <see cref="IPointerDevice"/>, +/// </summary> +/// <param name="Pointer">The pointer device with which the user is pointing.</param> +/// <param name="Timestamp"> +/// The timestamp (as retrieved from <see cref="Stopwatch.GetTimestamp"/>) at which the event occurred. +/// </param> +/// <param name="OldPoint"> +/// The previous state for this <see cref="TargetPoint"/>. If this is a new point (e.g. a finger has only just touched a +/// touch screen), this shall be <c>null</c>. +/// </param> +/// <param name="NewPoint"> +/// The new state for this <see cref="TargetPoint"/>. If the point is no longer valid (e.g. a finger is no longer +/// touching a touch screen), this shall be <c>null</c>. +/// </param> +public readonly record struct PointChangedEvent( + IPointerDevice Pointer, + long Timestamp, + TargetPoint? OldPoint, + TargetPoint? NewPoint +); diff --git a/sources/Input/Input/PointerButton.cs b/sources/Input/Input/PointerButton.cs new file mode 100644 index 0000000000..f0874e646e --- /dev/null +++ b/sources/Input/Input/PointerButton.cs @@ -0,0 +1,184 @@ +namespace Silk.NET.Input; + +/// <summary> +/// Enumerates the buttons available on pointer devices. +/// </summary> +public enum PointerButton +{ + /// <summary> + /// An unrecognised button. + /// </summary> + Unknown, + + /// <summary> + /// The primary button e.g. left click. + /// </summary> + Primary, + + /// <summary> + /// The secondary button e.g. right click. + /// </summary> + Secondary, + + /// <summary> + /// The third button. + /// </summary> + Button3, + + /// <summary> + /// The middle button i.e. clicking the scroll wheel down. This acts as the third button. + /// </summary> + MiddleButton = Button3, + + /// <summary> + /// The fourth button. + /// </summary> + Button4, + + /// <summary> + /// The fifth button. + /// </summary> + Button5, + + /// <summary> + /// The sixth button. + /// </summary> + Button6, + + /// <summary> + /// The seventh button. + /// </summary> + Button7, + + /// <summary> + /// The eighth button. + /// </summary> + Button8, + + /// <summary> + /// The ninth button. + /// </summary> + Button9, + + /// <summary> + /// The tenth button. + /// </summary> + Button10, + + /// <summary> + /// The eleventh button. + /// </summary> + Button11, + + /// <summary> + /// The twelveth button. + /// </summary> + Button12, + + /// <summary> + /// The thirteenth button. + /// </summary> + Button13, + + /// <summary> + /// The fourteenth button. + /// </summary> + Button14, + + /// <summary> + /// The fifteenth button. + /// </summary> + Button15, + + /// <summary> + /// The sixteenth button. + /// </summary> + Button16, + + /// <summary> + /// The seventeenth button. + /// </summary> + Button17, + + /// <summary> + /// The eighteenth button. + /// </summary> + Button18, + + /// <summary> + /// The nineteenth button. + /// </summary> + Button19, + + /// <summary> + /// The twentieth button. + /// </summary> + Button20, + + /// <summary> + /// The twenty-first button. + /// </summary> + Button21, + + /// <summary> + /// The twenty-second button. + /// </summary> + Button22, + + /// <summary> + /// The twenty-third button. + /// </summary> + Button23, + + /// <summary> + /// The twenty-fourth button. + /// </summary> + Button24, + + /// <summary> + /// The twenty-fifth button. + /// </summary> + Button25, + + /// <summary> + /// The twenty-sixth button. + /// </summary> + Button26, + + /// <summary> + /// The twenty-seventh button. + /// </summary> + Button27, + + /// <summary> + /// The twenty-eighth button. + /// </summary> + Button28, + + /// <summary> + /// The twenty-ninth button. + /// </summary> + Button29, + + /// <summary> + /// The thirtieth button. + /// </summary> + Button30, + + /// <summary> + /// The eraser tip of a pen pointer device. This acts as the thirtieth button. + /// </summary> + EraserTip = Button30, + + /// <summary> + /// The thirty-first button. + /// </summary> + Button31, + + /// <summary> + /// The thirty-second button. + /// </summary> + Button32, + + // BEFORE ADDING MORE BUTTONS, ENSURE YOU CHANGE InputMarshal TO ACCOUNT FOR THE NEW MAX +} diff --git a/sources/Input/Input/PointerClickConfiguration.cs b/sources/Input/Input/PointerClickConfiguration.cs new file mode 100644 index 0000000000..4612da0780 --- /dev/null +++ b/sources/Input/Input/PointerClickConfiguration.cs @@ -0,0 +1,19 @@ +namespace Silk.NET.Input; + +/// <summary> +/// Denotes the configuration for recognising <see cref="Pointers.DoubleClick"/> events apart from single +/// <see cref="Pointers.Click"/> events. +/// </summary> +/// <param name="DoubleClickTime"> +/// The maximum time in milliseconds between two consecutive clicks to count as a double click. +/// </param> +/// <param name="DoubleClickRange"> +/// The maximum distance in pixels between two consecutive clicks to count as a double click. +/// </param> +public record struct PointerClickConfiguration(int DoubleClickTime, float DoubleClickRange) +{ + /// <summary> + /// Gets the default configuration. + /// </summary> + public static PointerClickConfiguration Default => new(500, 4); +} diff --git a/sources/Input/Input/PointerClickEvent.cs b/sources/Input/Input/PointerClickEvent.cs new file mode 100644 index 0000000000..b5c75eece6 --- /dev/null +++ b/sources/Input/Input/PointerClickEvent.cs @@ -0,0 +1,17 @@ +using System.Diagnostics; + +namespace Silk.NET.Input; + +/// <summary> +/// Contains information pertaining to a pointer button being pressed and released (i.e. clicked). +/// </summary> +/// <param name="Pointer">The pointer device on which the button being pressed and released resides.</param> +/// <param name="Timestamp"> +/// The timestamp (as retrieved from <see cref="Stopwatch.GetTimestamp"/>) at which the event occurred. +/// </param> +/// <param name="Point"> +/// A specific <see cref="TargetPoint"/> for which the button press occurred, check <see cref="TargetPoint.IsValid"/> to +/// validate if such a point was available. +/// </param> +/// <param name="Button">The button that was pressed and released in succession.</param> +public readonly record struct PointerClickEvent(IPointerDevice Pointer, long Timestamp, TargetPoint Point, PointerButton Button); \ No newline at end of file diff --git a/sources/Input/Input/PointerGripChangedEvent.cs b/sources/Input/Input/PointerGripChangedEvent.cs new file mode 100644 index 0000000000..079e1e4e8a --- /dev/null +++ b/sources/Input/Input/PointerGripChangedEvent.cs @@ -0,0 +1,18 @@ +using System.Diagnostics; + +namespace Silk.NET.Input; + +/// <summary> +/// Contains information pertaining to the user changing the pressure with which they're applying their grip on the +/// given pointer device. +/// </summary> +/// <param name="Pointer">The pointer device the user is gripping.</param> +/// <param name="Timestamp"> +/// The timestamp (as retrieved from <see cref="Stopwatch.GetTimestamp"/>) at which the event occurred. +/// </param> +/// <param name="GripPressure"> +/// The grip pressure being applied to the device, where <c>0.0</c> is the lowest amount of pressure measurable by the +/// device and <c>1.0</c> is the maximum amount of pressure measurable by the device. +/// </param> +/// <param name="Delta">The change in <see cref="GripPressure"/> from its previous value.</param> +public readonly record struct PointerGripChangedEvent(IPointerDevice Pointer, long Timestamp, float GripPressure, float Delta); \ No newline at end of file diff --git a/sources/Input/Input/PointerState.cs b/sources/Input/Input/PointerState.cs new file mode 100644 index 0000000000..b69372214c --- /dev/null +++ b/sources/Input/Input/PointerState.cs @@ -0,0 +1,23 @@ +namespace Silk.NET.Input; + +/// <summary> +/// Contains user input state received from an <see cref="IPointerDevice"/>. +/// </summary> +public class PointerState +{ + /// <summary> + /// Gets the captured state of each of the buttons on the device. + /// </summary> + public ButtonReadOnlyList<PointerButton> Buttons { get; } + + /// <summary> + /// Gets the points on the targets at which the user is pointing using the device. + /// </summary> + public InputReadOnlyList<TargetPoint> Points { get; } + + /// <summary> + /// Gets the pressure the user is applying to the grip of the pointer device, where <c>0.0</c> is the lowest + /// measurable pressure and <c>1.0</c> is the highest measurable pressure. + /// </summary> + public float GripPressure { get; } +} \ No newline at end of file diff --git a/sources/Input/Input/PointerTargetChangedEvent.cs b/sources/Input/Input/PointerTargetChangedEvent.cs new file mode 100644 index 0000000000..49a97e5950 --- /dev/null +++ b/sources/Input/Input/PointerTargetChangedEvent.cs @@ -0,0 +1,27 @@ +using System.Diagnostics; +using Silk.NET.Maths; + +namespace Silk.NET.Input; + +/// <summary> +/// Contains information pertaining to changes to a "target" at which the user can point using a pointer device. +/// </summary> +/// <param name="Pointer">The pointer with which the user can point at the given target.</param> +/// <param name="Timestamp"> +/// The timestamp (as retrieved from <see cref="Stopwatch.GetTimestamp"/>) at which the event occurred. +/// </param> +/// <param name="Target">The target at which the user can point.</param> +/// <param name="IsAdded"> +/// <c>true</c> if this is a newly-added target to <see cref="IPointerDevice.Targets"/>, +/// <c>false</c> if this target has been removed from the list of available <see cref="IPointerDevice.Targets"/>, +/// <c>null</c> if there has been no change to the target's validity. +/// </param> +/// <param name="OldBounds"> +/// The old <see cref="IPointerTarget.Bounds"/> of the target. This may be the same as <see cref="NewBounds"/> if there +/// has been no change. +/// </param> +/// <param name="NewBounds"> +/// The new <see cref="IPointerTarget.Bounds"/> of the target. This may be the same as <see cref="OldBounds"/> if there +/// has been no change. +/// </param> +public readonly record struct PointerTargetChangedEvent(IPointerDevice Pointer, long Timestamp, IPointerTarget Target, bool? IsAdded, Box3D<float> OldBounds, Box3D<float> NewBounds); \ No newline at end of file diff --git a/sources/Input/Input/Pointers.cs b/sources/Input/Input/Pointers.cs new file mode 100644 index 0000000000..f1d5e84181 --- /dev/null +++ b/sources/Input/Input/Pointers.cs @@ -0,0 +1,367 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Runtime.InteropServices; + +namespace Silk.NET.Input; + +/// <summary> +/// Represents a collection of <see cref="IPointerDevice"/>s from which input events can be received. +/// </summary> +public sealed class Pointers + : InputContextDeviceList<IPointerDevice>, + IMouseInputHandler, + IPointerInputHandler +{ + private long _doubleClickTime; + private float _doubleClickRange; + private List<ClickData>? _clicks; + + internal Pointers(InputContext ctx) + : base(ctx) => ClickConfiguration = PointerClickConfiguration.Default; + + /// <summary> + /// Gets or sets the configuration that denotes the behaviour of <see cref="Click"/>/<see cref="DoubleClick"/>. + /// </summary> + public PointerClickConfiguration ClickConfiguration + { + get => new((int)((double)_doubleClickTime / Stopwatch.Frequency * 1000), _doubleClickRange); + set => + (_doubleClickTime, _doubleClickRange) = ( + (long)((double)value.DoubleClickTime / 1000 * Stopwatch.Frequency), + value.DoubleClickRange + ); + } + + /// <summary> + /// Raised when state pertaining to a pushable button on the pointer device changes (e.g. button up, button down). + /// </summary> + public event Action<ButtonChangedEvent<PointerButton>>? ButtonChanged; + + /// <summary> + /// Raised when one or more <see cref="ButtonChanged"/> events indicate a single click as defined by the + /// <see cref="ClickConfiguration"/>. + /// </summary> + public event Action<PointerClickEvent>? Click; + + /// <summary> + /// Raised when one or more <see cref="ButtonChanged"/> events indicate a double click as defined by the + /// <see cref="ClickConfiguration"/>. + /// </summary> + public event Action<PointerClickEvent>? DoubleClick; + + /// <summary> + /// Raised when a <see cref="TargetPoint"/>'s state changes (e.g. mouse move). + /// </summary> + public event Action<PointChangedEvent>? PointChanged; + + /// <summary> + /// Raised when a user scrolls using a pointer device's mouse wheel. + /// </summary> + public event Action<MouseScrollEvent>? MouseScroll; + + /// <summary> + /// Raised when a "target" at which the user can point using a pointer device changes. + /// </summary> + public event Action<PointerTargetChangedEvent>? TargetChanged; + + /// <summary> + /// Raised when the user adjusts their grip on the pointer device. + /// </summary> + public event Action<PointerGripChangedEvent>? GripChanged; + + void IButtonInputHandler<PointerButton>.HandleButtonChanged( + ButtonChangedEvent<PointerButton> @event + ) => HandleButtonChanged(@event); + + internal void HandleButtonChanged(ButtonChangedEvent<PointerButton> @event) + { + if (@event.Device is not IPointerDevice device) + { + return; + } + + ButtonChanged?.Invoke(@event); + if (@event.Previous.IsDown || !@event.Button.IsDown) + { + return; + } + + foreach (var target in device.Targets) + { + var pointCnt = target.GetPointCount(device); + for (var i = 0; i < pointCnt; i++) + { + HandlePointerDown( + device, + target.GetPoint(device, i), + @event.Button.Name, + @event.Timestamp + ); + } + } + } + + void IMouseInputHandler.HandleScroll(MouseScrollEvent @event) => HandleScroll(@event); + + internal void HandleScroll(MouseScrollEvent @event) => MouseScroll?.Invoke(@event); + + void IPointerInputHandler.HandleTargetChanged(PointerTargetChangedEvent @event) => + HandleTargetChanged(@event); + + internal void HandleTargetChanged(PointerTargetChangedEvent @event) + { + TargetChanged?.Invoke(@event); + if (_clicks is null || @event.IsAdded is not false) + { + return; + } + + var clicks = CollectionsMarshal.AsSpan(_clicks); + for (var i = 0; i < clicks.Length; i++) + { + ref var click = ref clicks[i]; + if (click.FirstClickPosition.Target != @event.Target) + { + continue; + } + + // Raise a click event for posterity. + HandleDoubleClickExceedsParameters(ref click); + _clicks.RemoveAt(i--); + + // SAFETY: We have to replace the span now as the RemoveAt could've in theory reallocated. + clicks = CollectionsMarshal.AsSpan(_clicks); + } + } + + void IPointerInputHandler.HandlePointChanged(PointChangedEvent @event) => + HandlePointChanged(@event); + + internal void HandlePointChanged(PointChangedEvent @event) + { + PointChanged?.Invoke(@event); + if (_clicks is null || @event is not { OldPoint: not null, NewPoint: { } @new }) + { + return; + } + + var span = CollectionsMarshal.AsSpan(_clicks); + for (var i = 0; i < _clicks.Count; i++) + { + ref var click = ref span[i]; + if (!click.IsMatch(@event.Pointer, in @new)) + { + continue; + } + + if (!click.HasMovedTooFar(_doubleClickRange, @new.Position)) + { + return; + } + + HandleDoubleClickExceedsParameters(ref click); + _clicks.RemoveAt(i); + return; + } + } + + void IPointerInputHandler.HandleGripChanged(PointerGripChangedEvent @event) => + HandleGripChanged(@event); + + internal void HandleGripChanged(PointerGripChangedEvent @event) => GripChanged?.Invoke(@event); + + private record struct ClickData( + IPointerDevice Device, + PointerButton? FirstClickButton, + TargetPoint FirstClickPosition, + long? FirstClickTime, + bool IsFirstClick + ) + { + public bool IsMatch(IPointerDevice device, ref readonly TargetPoint point) => + point.Id == FirstClickPosition.Id + && Device == device + && point.Target == FirstClickPosition.Target; + + public bool HasMovedTooFar(float range, Vector3 position) + { + var fcp = FirstClickPosition.Position; + return MathF.Abs(position.X - fcp.X) >= range + && MathF.Abs(position.Y - fcp.Y) >= range + && MathF.Abs(position.Z - fcp.Z) >= range; + } + } + + [MemberNotNull(nameof(_clicks))] + private ref ClickData GetClickData( + IPointerDevice device, + ref readonly TargetPoint point, + out int idx + ) + { + idx = 0; + foreach (ref var ret in CollectionsMarshal.AsSpan(_clicks ??= [])) + { + if (ret.IsMatch(device, in point)) + { + return ref ret; + } + + idx++; + } + + _clicks.Add( + new ClickData( + device, + null, + default(TargetPoint) with + { + Target = point.Target, + Id = point.Id, + }, + null, + true + ) + ); + return ref CollectionsMarshal.AsSpan(_clicks)[idx]; + } + + private void HandlePointerDown( + IPointerDevice device, + TargetPoint point, + PointerButton button, + long timestamp + ) + { + if ((_clicks is null && DoubleClick is null && Click is null) || point.Target is null) + { + return; + } + + ref var click = ref GetClickData(device, in point, out var idx); + if (click.IsFirstClick || (click.FirstClickButton is { } firstBtn && firstBtn != button)) + { + // This is the first click with the given mouse button. + var time = click.FirstClickTime; + click.FirstClickTime = null; + + if ( + click is { IsFirstClick: false, FirstClickButton: { } prevBtn } + && time is { } clickTime + ) + { + // Only the mouse buttons differ so treat last click as a single click. + Click?.Invoke( + new PointerClickEvent(device, clickTime, click.FirstClickPosition, prevBtn) + ); + } + } + else + { + // This is the second click with the same mouse button. + if (click.FirstClickTime is { } fct && timestamp - fct <= _doubleClickTime) + { + // Within the maximum double click time. + click.FirstClickTime = null; + if (!click.HasMovedTooFar(_doubleClickRange, point.Position)) + { + // Second click was in time and in range -> double click. + DoubleClick?.Invoke(new PointerClickEvent(device, timestamp, point, button)); + + // SAFETY: Must not use the click ref from now on! Returning instantly. + _clicks.RemoveAt(idx); + return; + } + + // Second click was in time but outside range -> single click. + // The second click is another "first click". + Click?.Invoke(new PointerClickEvent(device, timestamp, point, button)); + } + else + { + // The double click time elapsed. + + // If Update() would have detected the time elapse before, + // it would have set _firstClick back to true and we won't be here. + // Therefore Update() has not detected time elapse here and we have + // to handle it. + HandleDoubleClickExceedsParameters(ref click); + } + } + + // Process the first click. We process the second click as another "first click" if: + // - the double click time elapsed + // - the pointer moved too much before doing the second click + ProcessFirstClick(ref click, button, point, timestamp); + } + + private static void ProcessFirstClick( + ref ClickData click, + PointerButton button, + TargetPoint point, + long timestamp + ) + { + click.IsFirstClick = false; // for next time... + click.FirstClickButton = button; + click.FirstClickPosition = point; + click.FirstClickTime = timestamp; + } + + private void HandleDoubleClickExceedsParameters(ref ClickData click) + { + click.FirstClickTime = null; + click.IsFirstClick = true; + if (click is { FirstClickButton: { } fcb, FirstClickTime: { } fct }) + { + Click?.Invoke(new PointerClickEvent(click.Device, fct, click.FirstClickPosition, fcb)); + } + } + + internal void HandleUpdate() + { + if (_clicks is null) + { + return; + } + + var updateTime = Stopwatch.GetTimestamp(); + var clicks = CollectionsMarshal.AsSpan(_clicks); + for (var i = 0; i < clicks.Length; i++) + { + ref var click = ref clicks[i]; + if (click.FirstClickTime is not { } fct || updateTime - fct <= _doubleClickTime) + { + continue; + } + + // No second click in maximum double click time. + HandleDoubleClickExceedsParameters(ref click); + _clicks.RemoveAt(i--); + + // SAFETY: We have to replace the span now as the RemoveAt could've in theory reallocated. + clicks = CollectionsMarshal.AsSpan(_clicks); + } + } + + /// <inheritdoc /> + protected internal override void HandleDeviceConnectionChanged(ConnectionEvent @event) + { + base.HandleDeviceConnectionChanged(@event); + if (_clicks is null || @event.IsConnected || @event.Device is not IPointerDevice) + { + return; + } + + for (var i = 0; i < _clicks.Count; i++) + { + if (_clicks[i].Device != @event.Device) + { + continue; + } + + _clicks.RemoveAt(i--); + } + } +} diff --git a/sources/Input/Input/Silk.NET.Input.csproj b/sources/Input/Input/Silk.NET.Input.csproj new file mode 100644 index 0000000000..d20067094e --- /dev/null +++ b/sources/Input/Input/Silk.NET.Input.csproj @@ -0,0 +1,17 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFrameworks>net8.0;net9.0</TargetFrameworks> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + <NoWarn>ST0005;$(NoWarn)</NoWarn> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\..\Core\Core\Silk.NET.Core.csproj" /> + <ProjectReference Include="..\..\Maths\Maths\Silk.NET.Maths.csproj" /> + <ProjectReference Include="..\..\SDL\SDL\Silk.NET.SDL.csproj" /> + </ItemGroup> + <Import Project="..\..\Core\Core\Silk.NET.Core.targets" /> + +</Project> diff --git a/sources/Input/Input/Silk.NET.Input.csproj.DotSettings b/sources/Input/Input/Silk.NET.Input.csproj.DotSettings new file mode 100644 index 0000000000..10361e88b3 --- /dev/null +++ b/sources/Input/Input/Silk.NET.Input.csproj.DotSettings @@ -0,0 +1,2 @@ +<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> + <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=implementations/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> \ No newline at end of file diff --git a/sources/Input/Input/TargetPoint.cs b/sources/Input/Input/TargetPoint.cs new file mode 100644 index 0000000000..4607643ae7 --- /dev/null +++ b/sources/Input/Input/TargetPoint.cs @@ -0,0 +1,54 @@ +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using Silk.NET.Maths; + +namespace Silk.NET.Input; + +/// <summary> +/// Represents a point on a target at which a pointer is pointing. +/// </summary> +/// <param name="Id"> +/// An integral identifier for the point. This point must be the only point for the device currently pointing at a +/// target with this identifier at any given time. If this point ceases to point at the target, then the identifier +/// becomes free for another device point. This means that this identifier can just be a counter, but may be globally +/// unique depending on the backend's capabilities. If an index is used, points with greater indices should not be +/// "moved" into this point's place should it no longer point at the target. This is to allow applications to track +/// distinct points. +/// </param> +/// <param name="Flags">Flags describing the state of the point.</param> +/// <param name="Position">The absolute position on the target at which the pointer is pointing.</param> +/// <param name="NormalizedPosition"> +/// The normalized position on the target at which the pointer is pointing, if applicable. If this is not available +/// (e.g. due to the target being infinitely large a.k.a. "unbounded"), then this property shall have a value of +/// <c>default</c>. +/// </param> +/// <param name="Pointer"> +/// A ray representing the distance and angle at which the pointer is pointing at the point on the target. A ray with an +/// orientation equivalent to an identity quaternion shall be interpreted as the point directly perpendicular to and +/// facing towards the target, with this being the default value should this information be unavailable. If distance +/// information is unavailable, this shall be equivalent to a <c>default</c> vector. +/// </param> +/// <param name="Pressure"> +/// The pressure applied to the point on the target by the pointer, between <c>0.0</c> representing the minimum amount +/// of pressure and <c>1.0</c> representing the maximum amount of pressure. This shall be <c>1.0</c> if such data is +/// unavailable but the point is otherwise valid. +/// </param> +/// <param name="Target">The pointer being pointed at.</param> +public readonly record struct TargetPoint( + int Id, + TargetPointFlags Flags, + Vector3 Position, + Vector3 NormalizedPosition, + Ray3D<float> Pointer, + float Pressure, + IPointerTarget? Target +) +{ + /// <summary> + /// Gets a value indicating whether this <see cref="TargetPoint"/> is a valid instance of a point on a + /// <see cref="Target"/> that the user is pointing at using their pointer device. + /// </summary> + [MemberNotNullWhen(true, nameof(Target))] + public bool IsValid => + (Flags & TargetPointFlags.PointingAtTarget) != TargetPointFlags.NotPointingAtTarget; +} diff --git a/sources/Input/Input/TargetPointFlags.cs b/sources/Input/Input/TargetPointFlags.cs new file mode 100644 index 0000000000..091cb74fb4 --- /dev/null +++ b/sources/Input/Input/TargetPointFlags.cs @@ -0,0 +1,18 @@ +namespace Silk.NET.Input; + +/// <summary> +/// Flags describing a <see cref="TargetPoint" /> state. +/// </summary> +[Flags] +public enum TargetPointFlags +{ + /// <summary> + /// No flags are set, indicating that the point is not being pointed at and therefore may not be valid. + /// </summary> + NotPointingAtTarget = 0, + + /// <summary> + /// Indicates that the point has been resolved as a valid point at which the pointer is pointing. + /// </summary> + PointingAtTarget = 1 << 0 +} \ No newline at end of file diff --git a/tests/Input/Input/InputMarshalTests.cs b/tests/Input/Input/InputMarshalTests.cs new file mode 100644 index 0000000000..e75c730077 --- /dev/null +++ b/tests/Input/Input/InputMarshalTests.cs @@ -0,0 +1,160 @@ +// 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.Reflection; +using NUnit.Framework; + +namespace Silk.NET.Input.UnitTests; + +[TestFixture] +public class InputMarshalTests +{ + [TestCase(TypeArgs = [typeof(PointerButton)])] + [TestCase(TypeArgs = [typeof(JoystickButton)])] + [TestCase(TypeArgs = [typeof(KeyName)])] + public void GetButtonCount<T>() + where T : unmanaged, Enum + { + // This is to determine the gaps in KeyName. + var prev = -1; + foreach (var @enum in Enum.GetValues<T>().Order()) + { + var val = SilkMarshal.ConstCast<T, int>(@enum); + if (val - 1 != prev) + { + Console.WriteLine( + $"{prev} ({SilkMarshal.ConstCast<int, T>(prev)}), {val} ({@enum})" + ); + } + + prev = val; + } + } + + [TestCase(TypeArgs = [typeof(PointerButton)])] + [TestCase(TypeArgs = [typeof(JoystickButton)])] + [TestCase(TypeArgs = [typeof(KeyName)])] + public void EnumerateButtonList<T>() + where T : unmanaged, Enum + { + var list = InputMarshal.CreateList<Button<T>>(); + var expectedCount = Enum.GetNames(typeof(T)) + .DistinctBy(Enum.Parse<T>) + .Count(x => x != "Unknown"); + Assert.That(list.List, Has.Count.EqualTo(expectedCount)); + var encountered = 0; + var values = Enum.GetValues<T>() + .Where(x => x.ToString() != "Unknown") + .Distinct() + .Order() + .GetEnumerator(); + foreach (var btn in list.List) + { + encountered++; + Assert.Multiple(() => + { + Assert.That(values.MoveNext(), Is.True); + Assert.That(btn.Name, Is.EqualTo(values.Current)); + Assert.That(btn.Pressure, Is.EqualTo(0)); + Assert.That(btn.IsDown, Is.False); + }); + } + + Assert.That(encountered, Is.EqualTo(expectedCount)); + } + + [TestCase(TypeArgs = [typeof(PointerButton)])] + [TestCase(TypeArgs = [typeof(JoystickButton)])] + [TestCase(TypeArgs = [typeof(KeyName)])] + public void IndexButtonList<T>() + where T : unmanaged, Enum + { + var list = InputMarshal.CreateList<Button<T>>(); + var idx = 0; + foreach ( + var name in Enum.GetValues<T>().Where(x => x.ToString() != "Unknown").Distinct().Order() + ) + { + var btn = list.List[idx++]; + Assert.Multiple(() => + { + Assert.That(btn.Name, Is.EqualTo(name)); + Assert.That(btn.Pressure, Is.EqualTo(0)); + Assert.That(btn.IsDown, Is.False); + }); + } + } + + [TestCase(TypeArgs = [typeof(PointerButton)])] + [TestCase(TypeArgs = [typeof(JoystickButton)])] + [TestCase(TypeArgs = [typeof(KeyName)])] + public void GetButtonState<T>() + where T : unmanaged, Enum + { + var list = InputMarshal.CreateList<Button<T>>().List.AsButtonList(); + foreach ( + var name in Enum.GetValues<T>().Where(x => x.ToString() != "Unknown").Distinct().Order() + ) + { + var btn = list[name]; + Assert.Multiple(() => + { + Assert.That(btn.Name, Is.EqualTo(name)); + Assert.That(btn.Pressure, Is.EqualTo(0)); + Assert.That(btn.IsDown, Is.False); + }); + } + } + + [TestCase(TypeArgs = [typeof(PointerButton)])] + [TestCase(TypeArgs = [typeof(JoystickButton)])] + [TestCase(TypeArgs = [typeof(KeyName)])] + public void IndexNameTranslationRoundTrip<T>() + where T : unmanaged, Enum + { + var values = Enum.GetValues<T>() + .Where(x => x.ToString() != "Unknown") + .Distinct() + .Order() + .GetEnumerator(); + for (var i = 0; i < InputMarshal.GetButtonListCount<T>(); i++) + { + Assert.That(values.MoveNext(), Is.True); + var name = InputMarshal.ButtonList<T>.IndexName(i); + Assert.Multiple(() => + { + Assert.That(name, Is.EqualTo(values.Current)); + Assert.That(InputMarshal.ButtonList<T>.NameIndex(name), Is.EqualTo(i)); + }); + } + } + + [TestCase(TypeArgs = [typeof(PointerButton)])] + [TestCase(TypeArgs = [typeof(JoystickButton)])] + [TestCase(TypeArgs = [typeof(KeyName)])] + public void SetGetBinaryButtonState<T>() + where T : unmanaged, Enum + { + var arr = Enum.GetValues<T>() + .Where(x => x.ToString() != "Unknown") + .Distinct() + .Order() + .ToArray(); + foreach (var name in arr) + { + var list = InputMarshal.CreateList<Button<T>>(); + InputMarshal.SetButtonState(list, new Button<T>(name, true, 1), true); + foreach (var testName in arr) + { + var btn = list.List.AsButtonList()[testName]; + Assert.Multiple(() => + { + Assert.That(btn.Name, Is.EqualTo(testName)); + Assert.That(btn.Pressure, Is.EqualTo(testName.Equals(name) ? 1 : 0)); + Assert.That(btn.IsDown, Is.EqualTo(testName.Equals(name))); + }); + } + } + } +} diff --git a/tests/Input/Input/Silk.NET.Input.UnitTests.csproj b/tests/Input/Input/Silk.NET.Input.UnitTests.csproj new file mode 100644 index 0000000000..2daa2e92e6 --- /dev/null +++ b/tests/Input/Input/Silk.NET.Input.UnitTests.csproj @@ -0,0 +1,19 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net8.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\..\..\sources\Input\Input\Silk.NET.Input.csproj" /> + </ItemGroup> + + <ItemGroup> + <Compile Include="..\..\..\sources\Core\Core\Silk.NET.Core.cs"> + <Link>Silk.NET.Core.cs</Link> + </Compile> + </ItemGroup> + +</Project>