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