From f5b5f1c8c1bd4b034115410ef6a7c31439b00b2f Mon Sep 17 00:00:00 2001 From: Dylan Perks Date: Sat, 15 Feb 2025 19:53:39 +0000 Subject: [PATCH 01/39] 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 02/39] 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 03/39] 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 + + + + From 2080e1b9d59e87bfbd37d800d0150c6fb11b7831 Mon Sep 17 00:00:00 2001 From: Dylan Perks Date: Sat, 5 Apr 2025 14:25:48 +0100 Subject: [PATCH 04/39] Start on SdlInputBackend --- .../SDL3/InputWindowExtensions.cs | 14 +- .../Implementations/SDL3/SdlInputBackend.cs | 239 +++++++++++++++++- .../Implementations/SDL3/SdlPointerTarget.cs | 48 ++++ sources/SDL/SDL/SdlPlatformInfo.cs | 11 + .../Implementations/SDL3/SdlSurface.cs | 6 + 5 files changed, 306 insertions(+), 12 deletions(-) create mode 100644 sources/Input/Input/Implementations/SDL3/SdlPointerTarget.cs create mode 100644 sources/SDL/SDL/SdlPlatformInfo.cs diff --git a/sources/Input/Input/Implementations/SDL3/InputWindowExtensions.cs b/sources/Input/Input/Implementations/SDL3/InputWindowExtensions.cs index 9d2f55ce37..c4ae397c8b 100644 --- a/sources/Input/Input/Implementations/SDL3/InputWindowExtensions.cs +++ b/sources/Input/Input/Implementations/SDL3/InputWindowExtensions.cs @@ -2,6 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. // ReSharper disable CheckNamespace + +using Silk.NET.Input.SDL3; +using Silk.NET.SDL; + namespace Silk.NET.Input; /// @@ -11,6 +15,14 @@ public static partial class InputWindowExtensions { public static partial IInputBackend CreateInputBackend(this INativeWindow window) { - throw new NotImplementedException(); + if (!window.TryGetPlatformInfo(out SdlPlatformInfo info)) + { + throw new ArgumentException( + "When using the Silk.NET.Input reference implementation, a native window compatible with that " + + "implementation (such as those sourced from Silk.NET.Windowing) must be used." + ); + } + + return new SdlInputBackend(window, info); } } diff --git a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs index 24d0a8fdd9..baaf8dd163 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs @@ -1,23 +1,74 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Runtime.InteropServices; using Silk.NET.SDL; namespace Silk.NET.Input.SDL3; internal class SdlInputBackend : IInputBackend { - public unsafe SdlInputBackend() + public unsafe SdlInputBackend(INativeWindow window, SdlPlatformInfo info) { var ptr = new EventFilter(OnEvent); + Info = info; Sdl.AddEventWatch(ptr, nullptr); Id = (nint)ptr.Handle; + Window = window; + // =============================================================================================== + // === If we ever need to share common state across window-specific "backends", use the below: === + // =============================================================================================== + // var rootSurface = info.Window; + // var parent = rootSurface; + // while ((parent = Sdl.GetWindowParent(rootSurface)) != nullptr) + // { + // rootSurface = parent; + // } + // var props = Sdl.GetWindowProperties(rootSurface); + // if (props == 0) + // { + // Sdl.ThrowError(); + // } + // Ref pname = "org.dotnetfoundation.silkdotnet.inputroot"; + // var root = (nint)Sdl.GetPointerProperty(props, pname, nullptr); + // if (root != 0) + // { + // Root = + // GCHandle.FromIntPtr(root).Target as SdlBackendRoot + // ?? throw new InvalidOperationException( + // "The global input data for this ancestry of SDL windows was not in an expected format." + // ); + // } + // else + // { + // Root = new SdlBackendRoot(); + // var newHandle = GCHandle.Alloc(Root); + // if ( + // Sdl.SetPointerPropertyWithCleanup( + // props, + // pname, + // (Ptr)GCHandle.ToIntPtr(newHandle), + // new CleanupPropertyCallback(&CleanupRoot), + // nullptr + // ) + // ) + // { + // return; + // } + // newHandle.Free(); + // Sdl.ThrowError(); + // } } - private unsafe byte OnEvent(void* arg0, Event* arg1) - { - throw new NotImplementedException(); - } + // [UnmanagedCallersOnly] + // private static unsafe void CleanupRoot(void* _, void* value) => + // GCHandle.FromIntPtr((nint)value).Free(); + + public INativeWindow Window { get; } + public SdlPlatformInfo Info { get; } + public ISdl Sdl => Info.Sdl ?? SDL.Sdl.Instance; + + // public SdlBackendRoot Root { get; } public string Name => $"Silk.NET.Input Reference Implementation using SDL3 ({Sdl.GetPlatform().ReadToString()})"; @@ -28,6 +79,178 @@ private unsafe byte OnEvent(void* arg0, Event* arg1) public void Update(IInputHandler? handler = null) => throw new NotImplementedException(); + public void Dispose() + { + ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + private unsafe byte OnEvent(void* arg0, Event* arg1) + { + // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault + switch ((EventType)arg1->Common.Type) + { + case EventType.WindowMouseEnter: + break; + case EventType.WindowMouseLeave: + break; + case EventType.KeyDown: + break; + case EventType.KeyUp: + break; + case EventType.TextEditing: + break; + case EventType.TextInput: + break; + case EventType.KeymapChanged: + break; + case EventType.KeyboardAdded: + break; + case EventType.KeyboardRemoved: + break; + case EventType.TextEditingCandidates: + break; + case EventType.MouseMotion: + break; + case EventType.MouseButtonDown: + break; + case EventType.MouseButtonUp: + break; + case EventType.MouseWheel: + break; + case EventType.MouseAdded: + break; + case EventType.MouseRemoved: + break; + case EventType.JoystickAxisMotion: + break; + case EventType.JoystickBallMotion: + break; + case EventType.JoystickHatMotion: + break; + case EventType.JoystickButtonDown: + break; + case EventType.JoystickButtonUp: + break; + case EventType.JoystickAdded: + break; + case EventType.JoystickRemoved: + break; + case EventType.JoystickBatteryUpdated: + break; + case EventType.JoystickUpdateComplete: + break; + case EventType.GamepadAxisMotion: + break; + case EventType.GamepadButtonDown: + break; + case EventType.GamepadButtonUp: + break; + case EventType.GamepadAdded: + break; + case EventType.GamepadRemoved: + break; + case EventType.GamepadRemapped: + break; + case EventType.GamepadTouchpadDown: + break; + case EventType.GamepadTouchpadMotion: + break; + case EventType.GamepadTouchpadUp: + break; + case EventType.GamepadSensorUpdate: + break; + case EventType.GamepadUpdateComplete: + break; + case EventType.GamepadSteamHandleUpdated: + break; + case EventType.FingerDown: + break; + case EventType.FingerUp: + break; + case EventType.FingerMotion: + break; + case EventType.FingerCanceled: + break; + case EventType.ClipboardUpdate: + break; + case EventType.SensorUpdate: + break; + case EventType.PenProximityIn: + break; + case EventType.PenProximityOut: + break; + case EventType.PenDown: + break; + case EventType.PenUp: + break; + case EventType.PenButtonDown: + break; + case EventType.PenButtonUp: + break; + case EventType.PenMotion: + break; + case EventType.PenAxis: + break; + } + + return 1; + } + + private bool IsSupported(EventType type) => + type + is EventType.WindowMouseEnter + or EventType.WindowMouseLeave + or EventType.KeyDown + or EventType.KeyUp + or EventType.TextEditing + or EventType.TextInput + or EventType.KeymapChanged + or EventType.KeyboardAdded + or EventType.KeyboardRemoved + or EventType.TextEditingCandidates + or EventType.MouseMotion + or EventType.MouseButtonDown + or EventType.MouseButtonUp + or EventType.MouseWheel + or EventType.MouseAdded + or EventType.MouseRemoved + or EventType.JoystickAxisMotion + or EventType.JoystickBallMotion + or EventType.JoystickHatMotion + or EventType.JoystickButtonDown + or EventType.JoystickButtonUp + or EventType.JoystickAdded + or EventType.JoystickRemoved + or EventType.JoystickBatteryUpdated + or EventType.JoystickUpdateComplete + or EventType.GamepadAxisMotion + or EventType.GamepadButtonDown + or EventType.GamepadButtonUp + or EventType.GamepadAdded + or EventType.GamepadRemoved + or EventType.GamepadRemapped + or EventType.GamepadTouchpadDown + or EventType.GamepadTouchpadMotion + or EventType.GamepadTouchpadUp + or EventType.GamepadSensorUpdate + or EventType.GamepadUpdateComplete + or EventType.GamepadSteamHandleUpdated + or EventType.FingerDown + or EventType.FingerUp + or EventType.FingerMotion + or EventType.FingerCanceled + or EventType.ClipboardUpdate + or EventType.SensorUpdate + or EventType.PenProximityIn + or EventType.PenProximityOut + or EventType.PenDown + or EventType.PenUp + or EventType.PenButtonDown + or EventType.PenButtonUp + or EventType.PenMotion + or EventType.PenAxis; + private unsafe void ReleaseUnmanagedResources() { Sdl.RemoveEventWatch( @@ -37,11 +260,5 @@ private unsafe void ReleaseUnmanagedResources() SilkMarshal.Free((Ptr)Id); } - public void Dispose() - { - ReleaseUnmanagedResources(); - GC.SuppressFinalize(this); - } - ~SdlInputBackend() => ReleaseUnmanagedResources(); } diff --git a/sources/Input/Input/Implementations/SDL3/SdlPointerTarget.cs b/sources/Input/Input/Implementations/SDL3/SdlPointerTarget.cs new file mode 100644 index 0000000000..2383eb7489 --- /dev/null +++ b/sources/Input/Input/Implementations/SDL3/SdlPointerTarget.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Silk.NET.Maths; +using Silk.NET.SDL; + +namespace Silk.NET.Input.SDL3; + +internal class SdlPointerTarget(SdlInputBackend backend, bool rawMouseMotion) : IPointerTarget +{ + private static readonly Box3D _infinity = new( + new Vector3D(float.NegativeInfinity), + new Vector3D(float.PositiveInfinity) + ); + + public Box3D Bounds + { + get + { + if (rawMouseMotion) + { + return _infinity; + } + + int w = 0, + h = 0, + x = 0, + y = 0; + if ( + !backend.Sdl.GetWindowSize(backend.Info.Window, w.AsRef(), h.AsRef()) + || !backend.Sdl.GetWindowPosition(backend.Info.Window, x.AsRef(), y.AsRef()) + ) + { + backend.Sdl.ThrowError(); + } + + return new Box3D( + new Vector3D(x, y, 0), + new Vector3D(x + w, y + h, 0) + ); + } + } + + public int GetPointCount(IPointerDevice pointer) => throw new NotImplementedException(); + + public TargetPoint GetPoint(IPointerDevice pointer, int point) => + throw new NotImplementedException(); +} diff --git a/sources/SDL/SDL/SdlPlatformInfo.cs b/sources/SDL/SDL/SdlPlatformInfo.cs new file mode 100644 index 0000000000..68deed5cf8 --- /dev/null +++ b/sources/SDL/SDL/SdlPlatformInfo.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Silk.NET.SDL; + +/// +/// The SDL platform-specific handles. +/// +/// . +/// The SDL API interface. If null, use . +public readonly record struct SdlPlatformInfo(WindowHandle Window, ISdl? Sdl = null); diff --git a/sources/Windowing/Windowing/Implementations/SDL3/SdlSurface.cs b/sources/Windowing/Windowing/Implementations/SDL3/SdlSurface.cs index 4def1f9e7a..e9e80726ef 100644 --- a/sources/Windowing/Windowing/Implementations/SDL3/SdlSurface.cs +++ b/sources/Windowing/Windowing/Implementations/SDL3/SdlSurface.cs @@ -89,6 +89,12 @@ public override bool TryGetPlatformInfo( return false; } + if (typeof(TPlatformInfo) == typeof(SdlPlatformInfo)) + { + info = (TPlatformInfo)(object)new SdlPlatformInfo(Impl.Handle); + return true; + } + var props = Sdl.GetWindowProperties(Impl.Handle); if (typeof(TPlatformInfo) == typeof(CocoaPlatformInfo)) { From f1880f0f62015d96e3a19ba5793097651838a802 Mon Sep 17 00:00:00 2001 From: Dylan Perks Date: Sun, 25 May 2025 23:32:45 +0100 Subject: [PATCH 05/39] Push latest work on SDL backend --- .config/dotnet-tools.json | 4 +- sources/Input/Input/CustomCursor.cs | 4 +- sources/Input/Input/DualReadOnlyList.cs | 27 +- sources/Input/Input/GamepadState.cs | 25 +- .../SDL3/InputWindowExtensions.cs | 2 +- .../SDL3/SdlBoundedPointerDevice.cs | 32 +++ .../SDL3/SdlBoundedPointerTarget.cs | 95 +++++++ .../Input/Implementations/SDL3/SdlDevice.cs | 17 ++ .../Input/Implementations/SDL3/SdlGamepad.cs | 120 ++++++++- .../Implementations/SDL3/SdlInputBackend.cs | 236 ++++++++++++------ .../Input/Implementations/SDL3/SdlJoystick.cs | 9 +- .../Input/Implementations/SDL3/SdlKeyboard.cs | 9 +- .../Input/Implementations/SDL3/SdlMotor.cs | 6 +- .../Input/Implementations/SDL3/SdlPen.cs | 16 +- .../Implementations/SDL3/SdlPointerTarget.cs | 48 ---- .../Implementations/SDL3/SdlSharedMouse.cs | 78 ++++++ .../{SdlMouse.cs => SdlUnboundedMouse.cs} | 11 +- .../SDL3/SdlUnboundedPointerTarget.cs | 38 +++ sources/Input/Input/InputEvent.cs | 126 ++++++++++ sources/Input/Input/InputEventType.cs | 65 +++++ sources/Input/Input/InputEventUnion.cs | 81 ++++++ sources/Input/Input/InputMarshal.cs | 50 ++++ sources/Input/Input/InputWindowExtensions.cs | 7 +- sources/Input/Input/MouseState.cs | 26 +- sources/Input/Input/PointerState.cs | 28 ++- sources/Windowing/Windowing/Surface.cs | 65 ++++- 26 files changed, 1045 insertions(+), 180 deletions(-) create mode 100644 sources/Input/Input/Implementations/SDL3/SdlBoundedPointerDevice.cs create mode 100644 sources/Input/Input/Implementations/SDL3/SdlBoundedPointerTarget.cs create mode 100644 sources/Input/Input/Implementations/SDL3/SdlDevice.cs delete mode 100644 sources/Input/Input/Implementations/SDL3/SdlPointerTarget.cs create mode 100644 sources/Input/Input/Implementations/SDL3/SdlSharedMouse.cs rename sources/Input/Input/Implementations/SDL3/{SdlMouse.cs => SdlUnboundedMouse.cs} (56%) create mode 100644 sources/Input/Input/Implementations/SDL3/SdlUnboundedPointerTarget.cs create mode 100644 sources/Input/Input/InputEvent.cs create mode 100644 sources/Input/Input/InputEventType.cs create mode 100644 sources/Input/Input/InputEventUnion.cs diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 393d187924..b9f6941232 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,9 +3,9 @@ "isRoot": true, "tools": { "csharpier": { - "version": "0.30.6", + "version": "1.0.1", "commands": [ - "dotnet-csharpier" + "csharpier" ], "rollForward": false } diff --git a/sources/Input/Input/CustomCursor.cs b/sources/Input/Input/CustomCursor.cs index 8780f934af..b82b9d0337 100644 --- a/sources/Input/Input/CustomCursor.cs +++ b/sources/Input/Input/CustomCursor.cs @@ -16,7 +16,7 @@ public readonly ref struct CustomCursor public int Height { get; init; } /// - /// The row-major 32-bit RGBA pixel data (i.e. 8 bytes for each colour component). + /// The row-major 32-bit RGBA pixel data (i.e. 8 bits 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 index 9639703823..1fdf87c91a 100644 --- a/sources/Input/Input/DualReadOnlyList.cs +++ b/sources/Input/Input/DualReadOnlyList.cs @@ -6,17 +6,24 @@ namespace Silk.NET.Input; /// Represents a list that has exactly two elements. /// /// The element type. -public readonly struct DualReadOnlyList : IReadOnlyList +public readonly struct DualReadOnlyList(T left, T right) : IReadOnlyList { + /// + /// Creates a copy of the given list. + /// + /// The list. + public DualReadOnlyList(DualReadOnlyList other) + : this(other.Left, other.Right) { } + /// /// The first/leftmost element. /// - public readonly T Left; + public readonly T Left = left; /// /// The second/rightmost element. /// - public readonly T Right; + public readonly T Right = right; /// public IEnumerator GetEnumerator() @@ -31,9 +38,11 @@ public IEnumerator 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 + public T this[int index] => + index switch + { + 0 => Left, + 1 => Right, + _ => throw new IndexOutOfRangeException(), + }; +} diff --git a/sources/Input/Input/GamepadState.cs b/sources/Input/Input/GamepadState.cs index 05d0d9bb2a..9031830548 100644 --- a/sources/Input/Input/GamepadState.cs +++ b/sources/Input/Input/GamepadState.cs @@ -5,20 +5,35 @@ namespace Silk.NET.Input; /// /// Contains user input received from an . /// -public class GamepadState +public class GamepadState( + ButtonReadOnlyList buttons, + DualReadOnlyList thumbsticks, + DualReadOnlyList triggers +) { + /// + /// Clones the given state. This is useful for creating an immutable copy of state from a mutable one. + /// + /// The other state. + public GamepadState(GamepadState other) + : this( + new ButtonReadOnlyList(other.Buttons), + new DualReadOnlyList(other.Thumbsticks), + new DualReadOnlyList(other.Triggers) + ) { } + /// /// Gets the gamepad button state denoting the buttons being pressed or depressed. /// - public ButtonReadOnlyList Buttons { get; } + public ButtonReadOnlyList Buttons { get; } = buttons; /// /// Gets the state of the twin sticks on the gamepad. /// - public DualReadOnlyList Thumbsticks { get; } + public DualReadOnlyList Thumbsticks { get; internal set; } = thumbsticks; /// /// Gets the state of the triggers on the gamepad. /// - public DualReadOnlyList Triggers { get; } -} \ No newline at end of file + public DualReadOnlyList Triggers { get; internal set; } = triggers; +} diff --git a/sources/Input/Input/Implementations/SDL3/InputWindowExtensions.cs b/sources/Input/Input/Implementations/SDL3/InputWindowExtensions.cs index c4ae397c8b..bd515dc85e 100644 --- a/sources/Input/Input/Implementations/SDL3/InputWindowExtensions.cs +++ b/sources/Input/Input/Implementations/SDL3/InputWindowExtensions.cs @@ -23,6 +23,6 @@ public static partial IInputBackend CreateInputBackend(this INativeWindow window ); } - return new SdlInputBackend(window, info); + return new SdlInputBackend(info); } } diff --git a/sources/Input/Input/Implementations/SDL3/SdlBoundedPointerDevice.cs b/sources/Input/Input/Implementations/SDL3/SdlBoundedPointerDevice.cs new file mode 100644 index 0000000000..9105612b64 --- /dev/null +++ b/sources/Input/Input/Implementations/SDL3/SdlBoundedPointerDevice.cs @@ -0,0 +1,32 @@ +// 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; + +/// +/// A base class for SDL input devices that operate in terms of a window's or DWMs bounds. +/// +/// The backend. +internal abstract class SdlBoundedPointerDevice(SdlInputBackend backend) + : SdlDevice(backend), + IPointerDevice +{ + public abstract PointerState State { get; } + + [field: MaybeNull] + public virtual IReadOnlyList Targets => + field ??= [Backend.BoundedPointerTarget]; + + /// + /// Determines whether the should interpret + /// as being bounded points. For all devices supported by this backend, only one target is supported at a time + /// today. + /// + public virtual bool IsBounded => true; + + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + public InputMarshal.ListOwner BoundedPoints => + field.List.Data is null ? field = InputMarshal.CreateList() : field; +} diff --git a/sources/Input/Input/Implementations/SDL3/SdlBoundedPointerTarget.cs b/sources/Input/Input/Implementations/SDL3/SdlBoundedPointerTarget.cs new file mode 100644 index 0000000000..f7aee7b94c --- /dev/null +++ b/sources/Input/Input/Implementations/SDL3/SdlBoundedPointerTarget.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Numerics; +using Silk.NET.Maths; +using Silk.NET.SDL; + +namespace Silk.NET.Input.SDL3; + +internal class SdlBoundedPointerTarget(SdlInputBackend backend) : IPointerTarget +{ + internal SdlInputBackend Backend { get; } = backend; + private Box2D Bounds2D { get; set; } + + public Box3D Bounds => + new(new Vector3D(Bounds2D.Min, 0), new Vector3D(Bounds2D.Max, 1)); + + public static Box2D CalculateBounds(ISdl sdl) + { + var minX = float.PositiveInfinity; + var minY = float.PositiveInfinity; + var maxX = float.NegativeInfinity; + var maxY = float.NegativeInfinity; + var displayCount = 0; + var displays = sdl.GetDisplays(displayCount.AsRef()); + if (displays == nullptr) + { + // Looks like we can't support windowed mouse input. + sdl.ClearError(); + return default; + } + + if (displayCount == 0) // ??? + { + sdl.Free((Ref)displays); + return default; + } + + for (var i = 0; i < displayCount; i++) + { + Rect rect = default; + if (!sdl.GetDisplayBounds(displays[(nuint)i], rect.AsRef())) + { + return default; + } + + minX = float.Min(minX, rect.X); + minY = float.Min(minY, rect.Y); + maxX = float.Max(maxX, rect.X + rect.W); + maxY = float.Max(maxY, rect.Y + rect.H); + } + + sdl.Free((Ref)displays); + if (minX <= maxX && minY <= maxY) + { + return new Box2D(minX, minY, maxX, maxY); + } + + return default; + } + + public int GetPointCount(IPointerDevice pointer) + { + if (pointer is not SdlBoundedPointerDevice { IsBounded: true } device) + { + return 0; + } + + if (device.Backend == Backend) + { + return Bounds != default ? device.BoundedPoints.List.Count : 0; + } + + return device.Backend.BoundedPointerTarget.GetPointCount(pointer); + } + + public TargetPoint GetPoint(IPointerDevice pointer, int point) + { + if ( + pointer is not SdlBoundedPointerDevice { IsBounded: true } device + || point < 0 + || point >= device.BoundedPoints.List.Count + ) + { + return default; + } + + if (device.Backend != Backend) + { + return device.Backend.BoundedPointerTarget.GetPoint(pointer, point); + } + + return Bounds != default ? device.BoundedPoints.List[point] : default; + } +} diff --git a/sources/Input/Input/Implementations/SDL3/SdlDevice.cs b/sources/Input/Input/Implementations/SDL3/SdlDevice.cs new file mode 100644 index 0000000000..9980e32b84 --- /dev/null +++ b/sources/Input/Input/Implementations/SDL3/SdlDevice.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 abstract class SdlDevice(SdlInputBackend backend) : IInputDevice +{ + public bool Equals(IInputDevice? other) => + other?.GetType() == GetType() + && other.Id == Id + && other is SdlBoundedPointerDevice dev + && dev.Backend.Sdl == Backend.Sdl; + + public abstract IntPtr Id { get; } + public abstract string Name { get; } + public SdlInputBackend Backend { get; } = backend; +} diff --git a/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs b/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs index 64cd37ba8a..d24b71a89d 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs @@ -1,17 +1,125 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Numerics; +using Silk.NET.SDL; + namespace Silk.NET.Input.SDL3; -internal class SdlGamepad : IGamepad +internal class SdlGamepad : SdlDevice, IGamepad, IDisposable { - public bool Equals(IInputDevice? other) => throw new NotImplementedException(); + private readonly GamepadHandle _gamepad; + + private static JoystickButton? GetSilkButton(GamepadButton btn) => + btn switch + { + GamepadButton.South => JoystickButton.ButtonDown, + GamepadButton.East => JoystickButton.ButtonRight, + GamepadButton.West => JoystickButton.ButtonLeft, + GamepadButton.North => JoystickButton.ButtonUp, + GamepadButton.Back => JoystickButton.Back, + GamepadButton.Guide => JoystickButton.Home, + GamepadButton.Start => JoystickButton.Start, + GamepadButton.LeftStick => JoystickButton.LeftStick, + GamepadButton.RightStick => JoystickButton.RightStick, + GamepadButton.LeftShoulder => JoystickButton.LeftBumper, + GamepadButton.RightShoulder => JoystickButton.RightBumper, + GamepadButton.DpadUp => JoystickButton.DPadUp, + GamepadButton.DpadDown => JoystickButton.DPadDown, + GamepadButton.DpadLeft => JoystickButton.DPadLeft, + GamepadButton.DpadRight => JoystickButton.DPadRight, + // TODO not exposed today + _ => null, + }; + + public SdlGamepad(SdlInputBackend backend, uint joystickId) + : base(backend) + { + _gamepad = backend.Sdl.OpenGamepad(joystickId); + if (_gamepad == nullptr) + { + backend.Sdl.ThrowError(); + } + + var buttons = InputMarshal.CreateList>(); + for (var i = 0; i < (int)GamepadButton.Count; i++) + { + if (GetSilkButton((GamepadButton)i) is not { } btn) + { + continue; + } + + var isDown = backend.Sdl.GetGamepadButton(_gamepad, (GamepadButton)i); + InputMarshal.SetButtonState( + buttons, + new Button(btn, isDown, isDown ? 1 : 0), + true + ); + } + + // For thumbsticks, the state is a value ranging from -32768 (up/left) to 32767 (down/right). + // Triggers range from 0 when released to 32767 when fully pressed, and never return a negative value. Note that + // this differs from the value reported by the lower-level SDL_GetJoystickAxis(), which normally uses the full + // range. + var triggers = new DualReadOnlyList( + (float)backend.Sdl.GetGamepadAxis(_gamepad, GamepadAxis.LeftTrigger) / short.MaxValue, + (float)backend.Sdl.GetGamepadAxis(_gamepad, GamepadAxis.RightTrigger) / short.MaxValue + ); + var thumbsticks = new DualReadOnlyList( + new Vector2( + (float)backend.Sdl.GetGamepadAxis(_gamepad, GamepadAxis.Leftx) / short.MaxValue, + (float)backend.Sdl.GetGamepadAxis(_gamepad, GamepadAxis.Lefty) / short.MaxValue + ), + new Vector2( + (float)backend.Sdl.GetGamepadAxis(_gamepad, GamepadAxis.Rightx) / short.MaxValue, + (float)backend.Sdl.GetGamepadAxis(_gamepad, GamepadAxis.Righty) / short.MaxValue + ) + ); + State = new GamepadState(buttons.List.AsButtonList(), thumbsticks, triggers); + } + + // TODO this is not spec compliant, we need to use a physical device ID + public override unsafe nint Id => (nint)_gamepad.Handle; + + public override string Name => Backend.Sdl.GetGamepadName(_gamepad).ReadToString(); + + public GamepadState State { get; } + + // TODO this entire API needs to be redesigned as right now this is literally only ever going to be useful if it's + // just left or right. The original intention was that this would be useful for things like 3D haptics, but what did + // I know. The SDL people seem to have done a good job with their haptic API, let's see what we can do with it. + // For now, this has the same implementation as it always has. + public IReadOnlyList VibrationMotors => + _motors ??= [new SdlMotor(this, 0), new SdlMotor(this, 1)]; + + private IMotor[]? _motors; + private ushort[]? _motorFrequencies; + + internal ushort GetRumble(int motor) => (_motorFrequencies ??= [0, 0])[motor]; - public IntPtr Id => throw new NotImplementedException(); + internal void SetRumble(int motor, ushort value) + { + (_motorFrequencies ??= [0, 0])[motor] = value; + if ( + !Backend.Sdl.RumbleGamepad( + _gamepad, + _motorFrequencies[0], + _motorFrequencies[1], + uint.MaxValue + ) + ) + { + Backend.Sdl.ThrowError(); + } + } - public string Name => throw new NotImplementedException(); + private void ReleaseUnmanagedResources() => Backend.Sdl.CloseGamepad(_gamepad); - public GamepadState State => throw new NotImplementedException(); + public void Dispose() + { + ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } - public IReadOnlyList VibrationMotors => throw new NotImplementedException(); + ~SdlGamepad() => ReleaseUnmanagedResources(); } diff --git a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs index baaf8dd163..78ee265472 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs @@ -1,34 +1,75 @@ // 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 System.Runtime.InteropServices; +using Silk.NET.Maths; using Silk.NET.SDL; namespace Silk.NET.Input.SDL3; -internal class SdlInputBackend : IInputBackend +internal class SdlInputBackend : IInputBackend, ICursorConfiguration { - public unsafe SdlInputBackend(INativeWindow window, SdlPlatformInfo info) + private static readonly double _ticksPerNanosecond = Stopwatch.Frequency / 10e9d; + + private bool _pumped; + private long _epoch; + private List _devices = []; + private List _eventQueue = []; + private WindowHandle _focusedWindow; + private ISdl _sdl; + + public unsafe SdlInputBackend(SdlPlatformInfo info) { + ArgumentNullException.ThrowIfNull(info.Sdl); + ArgumentNullException.ThrowIfNull(info.Window.Handle); var ptr = new EventFilter(OnEvent); - Info = info; - Sdl.AddEventWatch(ptr, nullptr); + _sdl = info.Sdl; + _focusedWindow = info.Window; + // TODO overload resolution priority? + if (!Sdl.AddEventWatch(ptr, (Ref)nullptr)) + { + Sdl.ThrowError(); + } + Id = (nint)ptr.Handle; - Window = window; + + // The epoch deals in nanoseconds, so we take multiple measurements for the most accurate timestamps. + const byte epochMeasurements = 3; + var epoch = 0L; + for (byte i = 0; i < epochMeasurements; i++) + { + // We know the ticks per nanosecond, so to get the epoch timestamp we multiply the TicksNS by the ticks per + // nanosecond to get the number of ticks relative to SDL's epoch, and then subtract that from the timestamp + // now to get the timestamp of SDL's epoch. From there, when we receive an event we can just report the + // timestamp as _epoch + (timestamp * _ticksPerNanosecond). + var nowTimestamp = Stopwatch.GetTimestamp(); + var nowTicks = Sdl.GetTicksNS(); + epoch += unchecked(nowTimestamp - (long)(nowTicks * _ticksPerNanosecond)); + } + + _epoch = epoch / epochMeasurements; + // =============================================================================================== // === If we ever need to share common state across window-specific "backends", use the below: === // =============================================================================================== + // // Get the root surface - our windowing backend assumes there is only one root surface. If this is not the + // // case then this is undefined behaviour. // var rootSurface = info.Window; // var parent = rootSurface; // while ((parent = Sdl.GetWindowParent(rootSurface)) != nullptr) // { // rootSurface = parent; // } + // // Get the surface properties. // var props = Sdl.GetWindowProperties(rootSurface); // if (props == 0) // { // Sdl.ThrowError(); // } + // // Get or create the root object. // Ref pname = "org.dotnetfoundation.silkdotnet.inputroot"; // var root = (nint)Sdl.GetPointerProperty(props, pname, nullptr); // if (root != 0) @@ -58,42 +99,143 @@ public unsafe SdlInputBackend(INativeWindow window, SdlPlatformInfo info) // newHandle.Free(); // Sdl.ThrowError(); // } + // // Register ourselves with the root. + // Root.Backends.Add(this, null); + // Id = (nint)Root.EventFilter.Handle + Root.Backends.Count() - 1; } // [UnmanagedCallersOnly] - // private static unsafe void CleanupRoot(void* _, void* value) => - // GCHandle.FromIntPtr((nint)value).Free(); + // private static unsafe void CleanupRoot(void* _, void* value) + // { + // var gch = GCHandle.FromIntPtr((nint)value); + // (gch.Target as SdlBackendRoot)?.Dispose(); + // gch.Free(); + // } + // public SdlBackendRoot Root { get; } - public INativeWindow Window { get; } + // NOTE: Be careful where these are used! public SdlPlatformInfo Info { get; } - public ISdl Sdl => Info.Sdl ?? SDL.Sdl.Instance; - // public SdlBackendRoot Root { get; } + [field: MaybeNull] + public SdlBoundedPointerTarget BoundedPointerTarget => + field ??= new SdlBoundedPointerTarget(this); + + [field: MaybeNull] + public SdlUnboundedPointerTarget UnboundedPointerTarget => + field ??= new SdlUnboundedPointerTarget(this); + + public ISdl Sdl => Info.Sdl ?? SDL.Sdl.Instance; public string Name => $"Silk.NET.Input Reference Implementation using SDL3 ({Sdl.GetPlatform().ReadToString()})"; public nint Id { get; } - public IReadOnlyList Devices => throw new NotImplementedException(); + public IReadOnlyList Devices => _devices; - public void Update(IInputHandler? handler = null) => throw new NotImplementedException(); + // TODO we can't query support for these modes, but should we try-it-and-see to be accurate? + public CursorModes SupportedModes => + CursorModes.Normal | CursorModes.Confined | CursorModes.Unbounded; - public void Dispose() + // TODO if you're using one input context for all windows, there is no way to specify a window for grabbed cursor mode + + public CursorModes Mode { - ReleaseUnmanagedResources(); - GC.SuppressFinalize(this); + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + + public CursorStyles SupportedStyles => throw new NotImplementedException(); + + public CursorStyles Style + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); } + public CustomCursor Image + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + + // This is complicated, as the input proposal mandates that nothing happens until Update is called (so the events + // can be received on the given actor) but to also track logical events that happen between calls (i.e. from a + // timestamp perspective). Compound this with the fact that the user might do something silly like make multiple + // input backends (which is feasible for multiple windows I guess), or not be running anything other than input + // (having obviously created a window beforehand but not actually polling events I guess) + public void Update(IInputHandler? handler = null) + { + if (!_pumped) + { + Sdl.PumpEvents(); + } + + _pumped = false; + throw new NotImplementedException(); + } + + private enum QueuedEventType : byte + { + /// + /// The mouse has exited the window and the shared point should be marked inactive until proven otherwise by + /// further mouse motion (indicating it has entered another window). + /// + /// + /// We do not track the mouse enter events as this would cause us to fire twice for a mouse entering a window: + /// once for the entering, and once for new position. + /// + MouseExitedWindow, + + /// + /// The display bounds have been changed, meaning that 's + /// will have changed. + /// + BoundedPointerTargetUpdate, + } + + private readonly record struct QueuedEvent( + QueuedEventType Type, + ulong Timestamp, + Vector2 Vector0 = default, + Vector2 Vector1 = default + ); + + private ulong GetTimestamp(ref readonly Event @event) => + unchecked((ulong)(_epoch + (@event.Common.Timestamp * _ticksPerNanosecond))); + private unsafe byte OnEvent(void* arg0, Event* arg1) { + _pumped = true; // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault switch ((EventType)arg1->Common.Type) { - case EventType.WindowMouseEnter: - break; + case EventType.DisplayOrientation: + case EventType.DisplayAdded: + case EventType.DisplayRemoved: + case EventType.DisplayMoved: + case EventType.DisplayDesktopModeChanged: + case EventType.DisplayCurrentModeChanged: + case EventType.DisplayContentScaleChanged: + { + var bounds = SdlBoundedPointerTarget.CalculateBounds(Sdl); + _eventQueue.Add( + new QueuedEvent( + QueuedEventType.BoundedPointerTargetUpdate, + GetTimestamp(ref *arg1), + bounds.Min.ToSystem(), + bounds.Max.ToSystem() + ) + ); + break; + } case EventType.WindowMouseLeave: + { + _eventQueue.Add( + new QueuedEvent(QueuedEventType.MouseExitedWindow, GetTimestamp(ref *arg1)) + ); break; + } case EventType.KeyDown: break; case EventType.KeyUp: @@ -197,60 +339,6 @@ private unsafe byte OnEvent(void* arg0, Event* arg1) return 1; } - private bool IsSupported(EventType type) => - type - is EventType.WindowMouseEnter - or EventType.WindowMouseLeave - or EventType.KeyDown - or EventType.KeyUp - or EventType.TextEditing - or EventType.TextInput - or EventType.KeymapChanged - or EventType.KeyboardAdded - or EventType.KeyboardRemoved - or EventType.TextEditingCandidates - or EventType.MouseMotion - or EventType.MouseButtonDown - or EventType.MouseButtonUp - or EventType.MouseWheel - or EventType.MouseAdded - or EventType.MouseRemoved - or EventType.JoystickAxisMotion - or EventType.JoystickBallMotion - or EventType.JoystickHatMotion - or EventType.JoystickButtonDown - or EventType.JoystickButtonUp - or EventType.JoystickAdded - or EventType.JoystickRemoved - or EventType.JoystickBatteryUpdated - or EventType.JoystickUpdateComplete - or EventType.GamepadAxisMotion - or EventType.GamepadButtonDown - or EventType.GamepadButtonUp - or EventType.GamepadAdded - or EventType.GamepadRemoved - or EventType.GamepadRemapped - or EventType.GamepadTouchpadDown - or EventType.GamepadTouchpadMotion - or EventType.GamepadTouchpadUp - or EventType.GamepadSensorUpdate - or EventType.GamepadUpdateComplete - or EventType.GamepadSteamHandleUpdated - or EventType.FingerDown - or EventType.FingerUp - or EventType.FingerMotion - or EventType.FingerCanceled - or EventType.ClipboardUpdate - or EventType.SensorUpdate - or EventType.PenProximityIn - or EventType.PenProximityOut - or EventType.PenDown - or EventType.PenUp - or EventType.PenButtonDown - or EventType.PenButtonUp - or EventType.PenMotion - or EventType.PenAxis; - private unsafe void ReleaseUnmanagedResources() { Sdl.RemoveEventWatch( @@ -260,5 +348,11 @@ private unsafe void ReleaseUnmanagedResources() 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 index b4ecb54675..0dd57a7c44 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs @@ -3,13 +3,14 @@ namespace Silk.NET.Input.SDL3; -internal class SdlJoystick : IJoystick +internal class SdlJoystick : SdlDevice, IJoystick { - public bool Equals(IInputDevice? other) => throw new NotImplementedException(); + public SdlJoystick(SdlInputBackend backend, uint joystick) + : base(backend) { } - public IntPtr Id => throw new NotImplementedException(); + public override IntPtr Id => throw new NotImplementedException(); - public string Name => throw new NotImplementedException(); + public override 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 index 9bd7de6d81..eb8ab4b208 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs @@ -5,13 +5,14 @@ namespace Silk.NET.Input.SDL3; -internal class SdlKeyboard : IKeyboard +internal class SdlKeyboard : SdlDevice, IKeyboard { - public bool Equals(IInputDevice? other) => throw new NotImplementedException(); + public SdlKeyboard(SdlInputBackend backend) + : base(backend) { } - public IntPtr Id => throw new NotImplementedException(); + public override nint Id => throw new NotImplementedException(); - public string Name => throw new NotImplementedException(); + public override string Name => throw new NotImplementedException(); public KeyboardState State => throw new NotImplementedException(); diff --git a/sources/Input/Input/Implementations/SDL3/SdlMotor.cs b/sources/Input/Input/Implementations/SDL3/SdlMotor.cs index 3e551826a6..77a508bf6e 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlMotor.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlMotor.cs @@ -3,11 +3,11 @@ namespace Silk.NET.Input.SDL3; -internal class SdlMotor : IMotor +internal class SdlMotor(SdlGamepad gamepad, int freqIdx) : IMotor { public float Speed { - get => throw new NotImplementedException(); - set => throw new NotImplementedException(); + get => (float)gamepad.GetRumble(freqIdx) / ushort.MaxValue; + set => gamepad.SetRumble(freqIdx, (ushort)(value * ushort.MaxValue)); } } diff --git a/sources/Input/Input/Implementations/SDL3/SdlPen.cs b/sources/Input/Input/Implementations/SDL3/SdlPen.cs index d061bc86ca..3acaa39c24 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlPen.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlPen.cs @@ -3,15 +3,19 @@ namespace Silk.NET.Input.SDL3; -internal class SdlPen : IPointerDevice +internal class SdlPen : SdlBoundedPointerDevice { - public bool Equals(IInputDevice? other) => throw new NotImplementedException(); + private uint _penId; - public IntPtr Id => throw new NotImplementedException(); + public SdlPen(SdlInputBackend backend, uint pen) + : base(backend) + { + _penId = pen; + } - public string Name => throw new NotImplementedException(); + public override IntPtr Id => HashCode.Combine(Backend.Id, _penId); - public PointerState State => throw new NotImplementedException(); + public override string Name => throw new NotImplementedException(); - public IReadOnlyList Targets => throw new NotImplementedException(); + public override PointerState State => throw new NotImplementedException(); } diff --git a/sources/Input/Input/Implementations/SDL3/SdlPointerTarget.cs b/sources/Input/Input/Implementations/SDL3/SdlPointerTarget.cs deleted file mode 100644 index 2383eb7489..0000000000 --- a/sources/Input/Input/Implementations/SDL3/SdlPointerTarget.cs +++ /dev/null @@ -1,48 +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 Silk.NET.Maths; -using Silk.NET.SDL; - -namespace Silk.NET.Input.SDL3; - -internal class SdlPointerTarget(SdlInputBackend backend, bool rawMouseMotion) : IPointerTarget -{ - private static readonly Box3D _infinity = new( - new Vector3D(float.NegativeInfinity), - new Vector3D(float.PositiveInfinity) - ); - - public Box3D Bounds - { - get - { - if (rawMouseMotion) - { - return _infinity; - } - - int w = 0, - h = 0, - x = 0, - y = 0; - if ( - !backend.Sdl.GetWindowSize(backend.Info.Window, w.AsRef(), h.AsRef()) - || !backend.Sdl.GetWindowPosition(backend.Info.Window, x.AsRef(), y.AsRef()) - ) - { - backend.Sdl.ThrowError(); - } - - return new Box3D( - new Vector3D(x, y, 0), - new Vector3D(x + w, y + h, 0) - ); - } - } - - public int GetPointCount(IPointerDevice pointer) => throw new NotImplementedException(); - - public TargetPoint GetPoint(IPointerDevice pointer, int point) => - throw new NotImplementedException(); -} diff --git a/sources/Input/Input/Implementations/SDL3/SdlSharedMouse.cs b/sources/Input/Input/Implementations/SDL3/SdlSharedMouse.cs new file mode 100644 index 0000000000..e40c95cc11 --- /dev/null +++ b/sources/Input/Input/Implementations/SDL3/SdlSharedMouse.cs @@ -0,0 +1,78 @@ +// 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 SdlSharedMouse : SdlBoundedPointerDevice, IMouse +{ + private readonly MouseState _state; + + public SdlSharedMouse(SdlInputBackend backend) + : base(backend) + { + var buttons = InputMarshal.CreateList>(32); + var points = InputMarshal.CreateList(1); + _state = new MouseState(buttons.List.AsButtonList(), points.List, Vector2.Zero); + float x = 0, + y = 0; + var buttonMask = backend.Sdl.GetMouseState(x.AsRef(), y.AsRef()); + for (var i = 0; i < 32; i++) + { + InputMarshal.SetButtonState( + buttons, + new Button( + i switch + { + 1 => PointerButton.MiddleButton, + 2 => PointerButton.Secondary, + _ => (PointerButton)(i + 1), + }, + (buttonMask & (1 << i)) != 0, + 0 + ), + true + ); + } + + var pos = new Vector2(x, y); + var bounds = backend.BoundedPointerTarget.Bounds; + var min = new Vector2(bounds.Min.X, bounds.Min.Y); + var max = new Vector2(bounds.Max.X, bounds.Max.Y); + points + .GetUnderlyingList()! + .Add( + new TargetPoint( + 0, + TargetPointFlags.PointingAtTarget, + new Vector3(pos - min, 0), + new Vector3((pos - min) / (max - min), 0), + default, + 1.0f, + backend.BoundedPointerTarget + ) + ); + } + + public override IntPtr Id => HashCode.Combine(Backend.Id); + + public override string Name => $"{Backend.Name}: Shared/Global Mouse"; + + MouseState IMouse.State => _state; + + public ICursorConfiguration Cursor => Backend; + + public bool TrySetPosition(Vector2 position) + { + if (Backend.Sdl.WarpMouseGlobal(position.X, position.Y)) + { + return true; + } + + Backend.Sdl.ClearError(); + return false; + } + + public override PointerState State => _state; +} diff --git a/sources/Input/Input/Implementations/SDL3/SdlMouse.cs b/sources/Input/Input/Implementations/SDL3/SdlUnboundedMouse.cs similarity index 56% rename from sources/Input/Input/Implementations/SDL3/SdlMouse.cs rename to sources/Input/Input/Implementations/SDL3/SdlUnboundedMouse.cs index 25ac5cdb4f..6d4b6d58d2 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlMouse.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlUnboundedMouse.cs @@ -5,17 +5,18 @@ namespace Silk.NET.Input.SDL3; -internal class SdlMouse : IMouse +internal class SdlUnboundedMouse : SdlDevice, IMouse { - public bool Equals(IInputDevice? other) => throw new NotImplementedException(); + public SdlUnboundedMouse(SdlInputBackend backend, uint mouseId) + : base(backend) { } - public IntPtr Id => throw new NotImplementedException(); + public override IntPtr Id => throw new NotImplementedException(); - public string Name => throw new NotImplementedException(); + public override string Name => throw new NotImplementedException(); public MouseState State => throw new NotImplementedException(); - public ICursorConfiguration Cursor => throw new NotImplementedException(); + public ICursorConfiguration Cursor => Backend; public bool TrySetPosition(Vector2 position) => throw new NotImplementedException(); diff --git a/sources/Input/Input/Implementations/SDL3/SdlUnboundedPointerTarget.cs b/sources/Input/Input/Implementations/SDL3/SdlUnboundedPointerTarget.cs new file mode 100644 index 0000000000..797453aef1 --- /dev/null +++ b/sources/Input/Input/Implementations/SDL3/SdlUnboundedPointerTarget.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Silk.NET.Maths; + +namespace Silk.NET.Input.SDL3; + +internal class SdlUnboundedPointerTarget(SdlInputBackend backend) : IPointerTarget +{ + private static readonly Box3D _bounds = new( + float.MinValue, + float.MinValue, + float.MinValue, + float.MaxValue, + float.MaxValue, + float.MaxValue + ); + + public Box3D Bounds => _bounds; + + public int GetPointCount(IPointerDevice pointer) + { + if (pointer is not SdlUnboundedMouse mouse) + { + return 0; + } + + if (mouse.Backend != backend) + { + return mouse.Backend.UnboundedPointerTarget.GetPointCount(pointer); + } + + return (mouse.Backend.Mode & CursorModes.Unbounded) != 0 ? 1 : 0; + } + + public TargetPoint GetPoint(IPointerDevice pointer, int point) => + throw new NotImplementedException(); +} diff --git a/sources/Input/Input/InputEvent.cs b/sources/Input/Input/InputEvent.cs new file mode 100644 index 0000000000..4cd195138b --- /dev/null +++ b/sources/Input/Input/InputEvent.cs @@ -0,0 +1,126 @@ +// 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; + +/// +/// A tagged with a . +/// +/// +/// 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}" +)] +internal struct InputEvent +{ + /// + /// The type of the event. + /// + public InputEventType Type; + + /// + /// The event data. + /// + public InputEventUnion Event; + + /// + /// Dispatches the event to the given handler. + /// + /// The handler. + public void Dispatch(IInputHandler handler) + { + switch (Type) + { + case InputEventType.ConnectionEvent: + { + handler.HandleDeviceConnectionChanged(Event.Connection); + break; + } + case InputEventType.PointerButtonChangedEvent + when handler is IButtonInputHandler typedHandler: + { + typedHandler.HandleButtonChanged(Event.PointerButtonChanged); + break; + } + case InputEventType.KeyButtonChangedEvent + when handler is IButtonInputHandler typedHandler: + { + typedHandler.HandleButtonChanged(Event.KeyButtonChanged); + break; + } + case InputEventType.JoystickButtonChangedEvent + when handler is IButtonInputHandler typedHandler: + { + typedHandler.HandleButtonChanged(Event.JoystickButtonChanged); + break; + } + case InputEventType.GamepadThumbstickMoveEvent + when handler is IGamepadInputHandler typedHandler: + { + typedHandler.HandleThumbstickMove(Event.GamepadThumbstickMove); + break; + } + case InputEventType.GamepadTriggerMoveEvent + when handler is IGamepadInputHandler typedHandler: + { + typedHandler.HandleTriggerMove(Event.GamepadTriggerMove); + break; + } + case InputEventType.JoystickAxisMoveEvent + when handler is IJoystickInputHandler typedHandler: + { + typedHandler.HandleAxisMove(Event.JoystickAxisMove); + break; + } + case InputEventType.JoystickHatMoveEvent + when handler is IJoystickInputHandler typedHandler: + { + typedHandler.HandleHatMove(Event.JoystickHatMove); + break; + } + case InputEventType.KeyChangedEvent when handler is IKeyboardInputHandler typedHandler: + { + typedHandler.HandleKeyChanged(Event.KeyChanged); + break; + } + case InputEventType.KeyCharEvent when handler is IKeyboardInputHandler typedHandler: + { + typedHandler.HandleKeyChar(Event.KeyChar); + break; + } + case InputEventType.MouseScrollEvent when handler is IMouseInputHandler typedHandler: + { + typedHandler.HandleScroll(Event.MouseScroll); + break; + } + case InputEventType.PointChangedEvent when handler is IPointerInputHandler typedHandler: + { + typedHandler.HandlePointChanged(Event.PointChanged); + break; + } + case InputEventType.PointerGripChangedEvent + when handler is IPointerInputHandler typedHandler: + { + typedHandler.HandleGripChanged(Event.PointerGripChanged); + break; + } + case InputEventType.PointerTargetChangedEvent + when handler is IPointerInputHandler typedHandler: + { + typedHandler.HandleTargetChanged(Event.PointerTargetChanged); + break; + } + default: + { + Throw(); + break; + + static void Throw() => throw new ArgumentOutOfRangeException(nameof(Type)); + } + } + } +} diff --git a/sources/Input/Input/InputEventType.cs b/sources/Input/Input/InputEventType.cs new file mode 100644 index 0000000000..0606d29061 --- /dev/null +++ b/sources/Input/Input/InputEventType.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Silk.NET.Input; + +/// +/// Enumerates types of events raised by Silk.NET.Input. +/// +/// +/// 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}" +)] +internal enum InputEventType : byte +{ + /// where T is . + PointerButtonChangedEvent, + + /// where T is . + KeyButtonChangedEvent, + + /// where T is . + JoystickButtonChangedEvent, + + /// . + ConnectionEvent, + + /// . + GamepadThumbstickMoveEvent, + + /// . + GamepadTriggerMoveEvent, + + /// . + JoystickAxisMoveEvent, + + /// . + JoystickHatMoveEvent, + + /// . + KeyChangedEvent, + + /// . + KeyCharEvent, + + /// . + MouseScrollEvent, + + /// . + PointChangedEvent, + + // Does not have a matching actor method. + // /// . + // PointerClickEvent, + + /// . + PointerGripChangedEvent, + + /// . + PointerTargetChangedEvent, +} diff --git a/sources/Input/Input/InputEventUnion.cs b/sources/Input/Input/InputEventUnion.cs new file mode 100644 index 0000000000..830cdd0418 --- /dev/null +++ b/sources/Input/Input/InputEventUnion.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +namespace Silk.NET.Input; + +/// +/// A union over input events. +/// +/// +/// 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}" +)] +[StructLayout(LayoutKind.Explicit)] +internal struct InputEventUnion +{ + /// where T is . + [FieldOffset(0)] + public ButtonChangedEvent PointerButtonChanged; + + /// where T is . + [FieldOffset(0)] + public ButtonChangedEvent KeyButtonChanged; + + /// where T is . + [FieldOffset(0)] + public ButtonChangedEvent JoystickButtonChanged; + + /// . + [FieldOffset(0)] + public ConnectionEvent Connection; + + /// . + [FieldOffset(0)] + public GamepadThumbstickMoveEvent GamepadThumbstickMove; + + /// . + [FieldOffset(0)] + public GamepadTriggerMoveEvent GamepadTriggerMove; + + /// . + [FieldOffset(0)] + public JoystickAxisMoveEvent JoystickAxisMove; + + /// . + [FieldOffset(0)] + public JoystickHatMoveEvent JoystickHatMove; + + /// . + [FieldOffset(0)] + public KeyChangedEvent KeyChanged; + + /// . + [FieldOffset(0)] + public KeyCharEvent KeyChar; + + /// . + [FieldOffset(0)] + public MouseScrollEvent MouseScroll; + + /// . + [FieldOffset(0)] + public PointChangedEvent PointChanged; + + /// . + [FieldOffset(0)] + public PointerClickEvent PointerClick; + + /// . + [FieldOffset(0)] + public PointerGripChangedEvent PointerGripChanged; + + /// . + [FieldOffset(0)] + public PointerTargetChangedEvent PointerTargetChanged; +} diff --git a/sources/Input/Input/InputMarshal.cs b/sources/Input/Input/InputMarshal.cs index 0d9731e8e7..d11ecff97a 100644 --- a/sources/Input/Input/InputMarshal.cs +++ b/sources/Input/Input/InputMarshal.cs @@ -345,6 +345,15 @@ public static int GetButtonListCount() public static ButtonReadOnlyList AsButtonList(this InputReadOnlyList> list) where T : unmanaged, Enum => new(list); + /// + /// Creates a wrapping the given . + /// + /// The list. + /// The button name type. + /// The button list. + public static InputReadOnlyList> AsInputList(this ButtonReadOnlyList list) + where T : unmanaged, Enum => new(list); + /// /// Creates a new for the given , optionally with the /// given where is applicable for this @@ -542,6 +551,47 @@ public static void SetButtonState(ListOwner> list, Button value, ? null : Unsafe.As>(list.List.Data); + /// + /// Unsafely creates a for the given list. Note that you should really only do this if + /// you are actually the owner of the list and are for some reason not storing the , using + /// this API to gain mutable access to an input list is almost always breaking assumptions throughout the input API. + /// + /// The list. + /// The type of the elements in the list. + /// The list owner. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ListOwner AsOwned(InputReadOnlyList list) => new(list); + + /// + /// Given an owned mutable , swaps out the value of . + /// Note that this should not be used unexpectedly, and if there are any doubts about ownership or usage then create + /// a new instance of for safe mutation. + /// + /// The . + /// The new triggers. + public static void SetTriggers(GamepadState state, DualReadOnlyList triggers) => + state.Triggers = triggers; + + /// + /// Given an owned mutable , swaps out the value of + /// . Note that this should not be used unexpectedly, and if there are any + /// doubts about ownership or usage then create a new instance of for safe mutation. + /// + /// The . + /// The new thumbsticks. + public static void SetThumbsticks(GamepadState state, DualReadOnlyList thumbsticks) => + state.Thumbsticks = thumbsticks; + + /// + /// Given an owned mutable , swaps out the value of + /// . Note that this should not be used unexpectedly, and if there are any + /// doubts about ownership or usage then create a new instance of for safe mutation. + /// + /// The . + /// The new grip pressure. + public static void SetGripPressure(PointerState state, float gripPressure) => + state.GripPressure = gripPressure; + // 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. diff --git a/sources/Input/Input/InputWindowExtensions.cs b/sources/Input/Input/InputWindowExtensions.cs index 2933bc7527..7eb3201841 100644 --- a/sources/Input/Input/InputWindowExtensions.cs +++ b/sources/Input/Input/InputWindowExtensions.cs @@ -17,7 +17,8 @@ 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. + /// windowing API. The returned backend will capture input for the provided and all + /// descendants of that i.e. spawned children do not need their own backend. /// /// /// Regarding the threading rules documented on , @@ -33,7 +34,9 @@ public static partial class InputWindowExtensions /// /// 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. + /// also sourced from the "reference implementation" of the windowing API. The returned backend will capture input + /// for the provided and all descendants of that i.e. + /// spawned children do not need their own backend. /// /// /// Regarding the threading rules documented on , diff --git a/sources/Input/Input/MouseState.cs b/sources/Input/Input/MouseState.cs index fe6a776b0e..545a7704ae 100644 --- a/sources/Input/Input/MouseState.cs +++ b/sources/Input/Input/MouseState.cs @@ -5,10 +5,30 @@ namespace Silk.NET.Input; /// /// Contains user input received from an . /// -public class MouseState : PointerState +public class MouseState( + ButtonReadOnlyList buttons, + InputReadOnlyList points, + Vector2 wheelPosition, + float gripPressure = 0 +) : PointerState(buttons, points, gripPressure) { + /// + /// Clones a object. + /// + /// The object to clone. + /// + /// This object will not receive any changes from the object it was created from as they are distinct clones. + /// + public MouseState(MouseState other) + : this( + new ButtonReadOnlyList(other.Buttons), + new InputReadOnlyList(other.Points), + other.WheelPosition, + other.GripPressure + ) { } + /// /// Gets the current position of the scroll wheel in number of ratchets. /// - public Vector2 WheelPosition { get; } -} \ No newline at end of file + public Vector2 WheelPosition { get; internal set; } = wheelPosition; +} diff --git a/sources/Input/Input/PointerState.cs b/sources/Input/Input/PointerState.cs index b69372214c..b77bc0af0e 100644 --- a/sources/Input/Input/PointerState.cs +++ b/sources/Input/Input/PointerState.cs @@ -3,21 +3,39 @@ namespace Silk.NET.Input; /// /// Contains user input state received from an . /// -public class PointerState +public class PointerState( + ButtonReadOnlyList buttons, + InputReadOnlyList points, + float gripPressure = 0 +) { + /// + /// Clones a object. + /// + /// The object to clone. + /// + /// This object will not receive any changes from the object it was created from as they are distinct clones. + /// + public PointerState(PointerState other) + : this( + new ButtonReadOnlyList(other.Buttons), + new InputReadOnlyList(other.Points), + other.GripPressure + ) { } + /// /// Gets the captured state of each of the buttons on the device. /// - public ButtonReadOnlyList Buttons { get; } + public ButtonReadOnlyList Buttons { get; } = buttons; /// /// Gets the points on the targets at which the user is pointing using the device. /// - public InputReadOnlyList Points { get; } + public InputReadOnlyList Points { get; } = points; /// /// 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 + public float GripPressure { get; internal set; } = gripPressure; +} diff --git a/sources/Windowing/Windowing/Surface.cs b/sources/Windowing/Windowing/Surface.cs index e2605e428d..28d38f47a1 100644 --- a/sources/Windowing/Windowing/Surface.cs +++ b/sources/Windowing/Windowing/Surface.cs @@ -284,7 +284,24 @@ protected internal virtual void OnUpdate() /// Centers this window to the given monitor or, if null, the current monitor the window's on. /// /// The specific display to center the window to, if any. - public void Center(IDisplay? display = null) => throw new NotImplementedException(); + public void Center(IDisplay? display = null) + { + if ( + Window is null + || Display is null + || display is not null && !Display.Available.Contains(display) + ) + { + return; + } + + if (display is not null) + { + Display.Current = display; + } + + Window.Position = (Vector2)Display.Current.WorkArea.Center - (Window.Size / 2); + } /// /// Converts a point that is defined in the same coordinate space as to instead be @@ -293,7 +310,15 @@ protected internal virtual void OnUpdate() /// /// The point to transform. /// The transformed point. - public Vector2 ScreenToClient(Vector2 point) => throw new NotImplementedException(); + public Vector2 ScreenToClient(Vector2 point) + { + if (Window is null || Display is null) + { + return point; + } + + return point - Window.Position; + } /// /// Converts a point that is defined relative to to instead be defined in the @@ -302,7 +327,15 @@ protected internal virtual void OnUpdate() /// /// The point to transform. /// The transformed point. - public Vector2 ClientToScreen(Vector2 point) => throw new NotImplementedException(); + public Vector2 ClientToScreen(Vector2 point) + { + if (Window is null || Display is null) + { + return point; + } + + return point + Window.Position; + } /// /// Converts a point that is defined relative to by multiplying it with the @@ -310,7 +343,31 @@ protected internal virtual void OnUpdate() /// /// The point to transform. /// The transformed point. - public Vector2 ClientToDrawable(Vector2 point) => throw new NotImplementedException(); + public Vector2 ClientToDrawable(Vector2 point) + { + if (Window is null) + { + return point; + } + + return point * (DrawableSize / Window.ClientSize); + } + + /// + /// Converts a point that is defined in terms of by dividing it by the division of + /// by 's size. + /// + /// The point to transform. + /// The transformed point. + public Vector2 DrawableToClient(Vector2 point) + { + if (Window is null) + { + return point; + } + + return point / (DrawableSize / Window.ClientSize); + } /// public abstract bool TryGetPlatformInfo( From 1d163176f510b10393f2e8dad71b699b9c496f56 Mon Sep 17 00:00:00 2001 From: Dylan Perks Date: Sun, 25 May 2025 23:38:35 +0100 Subject: [PATCH 06/39] Remove types I didn't use in the end --- sources/Input/Input/InputEvent.cs | 126 ------------------------- sources/Input/Input/InputEventType.cs | 65 ------------- sources/Input/Input/InputEventUnion.cs | 81 ---------------- 3 files changed, 272 deletions(-) delete mode 100644 sources/Input/Input/InputEvent.cs delete mode 100644 sources/Input/Input/InputEventType.cs delete mode 100644 sources/Input/Input/InputEventUnion.cs diff --git a/sources/Input/Input/InputEvent.cs b/sources/Input/Input/InputEvent.cs deleted file mode 100644 index 4cd195138b..0000000000 --- a/sources/Input/Input/InputEvent.cs +++ /dev/null @@ -1,126 +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.CodeAnalysis; - -namespace Silk.NET.Input; - -/// -/// A tagged with a . -/// -/// -/// 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}" -)] -internal struct InputEvent -{ - /// - /// The type of the event. - /// - public InputEventType Type; - - /// - /// The event data. - /// - public InputEventUnion Event; - - /// - /// Dispatches the event to the given handler. - /// - /// The handler. - public void Dispatch(IInputHandler handler) - { - switch (Type) - { - case InputEventType.ConnectionEvent: - { - handler.HandleDeviceConnectionChanged(Event.Connection); - break; - } - case InputEventType.PointerButtonChangedEvent - when handler is IButtonInputHandler typedHandler: - { - typedHandler.HandleButtonChanged(Event.PointerButtonChanged); - break; - } - case InputEventType.KeyButtonChangedEvent - when handler is IButtonInputHandler typedHandler: - { - typedHandler.HandleButtonChanged(Event.KeyButtonChanged); - break; - } - case InputEventType.JoystickButtonChangedEvent - when handler is IButtonInputHandler typedHandler: - { - typedHandler.HandleButtonChanged(Event.JoystickButtonChanged); - break; - } - case InputEventType.GamepadThumbstickMoveEvent - when handler is IGamepadInputHandler typedHandler: - { - typedHandler.HandleThumbstickMove(Event.GamepadThumbstickMove); - break; - } - case InputEventType.GamepadTriggerMoveEvent - when handler is IGamepadInputHandler typedHandler: - { - typedHandler.HandleTriggerMove(Event.GamepadTriggerMove); - break; - } - case InputEventType.JoystickAxisMoveEvent - when handler is IJoystickInputHandler typedHandler: - { - typedHandler.HandleAxisMove(Event.JoystickAxisMove); - break; - } - case InputEventType.JoystickHatMoveEvent - when handler is IJoystickInputHandler typedHandler: - { - typedHandler.HandleHatMove(Event.JoystickHatMove); - break; - } - case InputEventType.KeyChangedEvent when handler is IKeyboardInputHandler typedHandler: - { - typedHandler.HandleKeyChanged(Event.KeyChanged); - break; - } - case InputEventType.KeyCharEvent when handler is IKeyboardInputHandler typedHandler: - { - typedHandler.HandleKeyChar(Event.KeyChar); - break; - } - case InputEventType.MouseScrollEvent when handler is IMouseInputHandler typedHandler: - { - typedHandler.HandleScroll(Event.MouseScroll); - break; - } - case InputEventType.PointChangedEvent when handler is IPointerInputHandler typedHandler: - { - typedHandler.HandlePointChanged(Event.PointChanged); - break; - } - case InputEventType.PointerGripChangedEvent - when handler is IPointerInputHandler typedHandler: - { - typedHandler.HandleGripChanged(Event.PointerGripChanged); - break; - } - case InputEventType.PointerTargetChangedEvent - when handler is IPointerInputHandler typedHandler: - { - typedHandler.HandleTargetChanged(Event.PointerTargetChanged); - break; - } - default: - { - Throw(); - break; - - static void Throw() => throw new ArgumentOutOfRangeException(nameof(Type)); - } - } - } -} diff --git a/sources/Input/Input/InputEventType.cs b/sources/Input/Input/InputEventType.cs deleted file mode 100644 index 0606d29061..0000000000 --- a/sources/Input/Input/InputEventType.cs +++ /dev/null @@ -1,65 +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.CodeAnalysis; - -namespace Silk.NET.Input; - -/// -/// Enumerates types of events raised by Silk.NET.Input. -/// -/// -/// 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}" -)] -internal enum InputEventType : byte -{ - /// where T is . - PointerButtonChangedEvent, - - /// where T is . - KeyButtonChangedEvent, - - /// where T is . - JoystickButtonChangedEvent, - - /// . - ConnectionEvent, - - /// . - GamepadThumbstickMoveEvent, - - /// . - GamepadTriggerMoveEvent, - - /// . - JoystickAxisMoveEvent, - - /// . - JoystickHatMoveEvent, - - /// . - KeyChangedEvent, - - /// . - KeyCharEvent, - - /// . - MouseScrollEvent, - - /// . - PointChangedEvent, - - // Does not have a matching actor method. - // /// . - // PointerClickEvent, - - /// . - PointerGripChangedEvent, - - /// . - PointerTargetChangedEvent, -} diff --git a/sources/Input/Input/InputEventUnion.cs b/sources/Input/Input/InputEventUnion.cs deleted file mode 100644 index 830cdd0418..0000000000 --- a/sources/Input/Input/InputEventUnion.cs +++ /dev/null @@ -1,81 +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.CodeAnalysis; -using System.Runtime.InteropServices; - -namespace Silk.NET.Input; - -/// -/// A union over input events. -/// -/// -/// 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}" -)] -[StructLayout(LayoutKind.Explicit)] -internal struct InputEventUnion -{ - /// where T is . - [FieldOffset(0)] - public ButtonChangedEvent PointerButtonChanged; - - /// where T is . - [FieldOffset(0)] - public ButtonChangedEvent KeyButtonChanged; - - /// where T is . - [FieldOffset(0)] - public ButtonChangedEvent JoystickButtonChanged; - - /// . - [FieldOffset(0)] - public ConnectionEvent Connection; - - /// . - [FieldOffset(0)] - public GamepadThumbstickMoveEvent GamepadThumbstickMove; - - /// . - [FieldOffset(0)] - public GamepadTriggerMoveEvent GamepadTriggerMove; - - /// . - [FieldOffset(0)] - public JoystickAxisMoveEvent JoystickAxisMove; - - /// . - [FieldOffset(0)] - public JoystickHatMoveEvent JoystickHatMove; - - /// . - [FieldOffset(0)] - public KeyChangedEvent KeyChanged; - - /// . - [FieldOffset(0)] - public KeyCharEvent KeyChar; - - /// . - [FieldOffset(0)] - public MouseScrollEvent MouseScroll; - - /// . - [FieldOffset(0)] - public PointChangedEvent PointChanged; - - /// . - [FieldOffset(0)] - public PointerClickEvent PointerClick; - - /// . - [FieldOffset(0)] - public PointerGripChangedEvent PointerGripChanged; - - /// . - [FieldOffset(0)] - public PointerTargetChangedEvent PointerTargetChanged; -} From e497dde06d8f37e95ebc237e3230eb8ecb430552 Mon Sep 17 00:00:00 2001 From: dom Date: Sun, 22 Jun 2025 11:46:06 -0400 Subject: [PATCH 07/39] begin mucking about input event processing --- .../SDL3/SdlBoundedPointerDevice.cs | 5 +- .../Input/Implementations/SDL3/SdlDevice.cs | 10 +- .../Input/Implementations/SDL3/SdlGamepad.cs | 17 +- .../Implementations/SDL3/SdlInputBackend.cs | 243 +++++++++++++----- .../Input/Implementations/SDL3/SdlJoystick.cs | 6 +- .../Input/Implementations/SDL3/SdlKeyboard.cs | 31 ++- .../Input/Implementations/SDL3/SdlPen.cs | 10 - .../Implementations/SDL3/SdlSharedMouse.cs | 11 +- .../Implementations/SDL3/SdlUnboundedMouse.cs | 5 - sources/Playground/InputTesting.cs | 25 ++ sources/Playground/Playground.csproj | 1 + sources/Playground/Program.cs | 13 +- sources/SDL/SDL/InputHandleExtensions.cs | 36 +++ .../Implementations/SDL3/SdlSurface.cs | 2 +- 14 files changed, 296 insertions(+), 119 deletions(-) create mode 100644 sources/Playground/InputTesting.cs create mode 100644 sources/SDL/SDL/InputHandleExtensions.cs diff --git a/sources/Input/Input/Implementations/SDL3/SdlBoundedPointerDevice.cs b/sources/Input/Input/Implementations/SDL3/SdlBoundedPointerDevice.cs index 9105612b64..e3bdd94703 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlBoundedPointerDevice.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlBoundedPointerDevice.cs @@ -8,10 +8,7 @@ namespace Silk.NET.Input.SDL3; /// /// A base class for SDL input devices that operate in terms of a window's or DWMs bounds. /// -/// The backend. -internal abstract class SdlBoundedPointerDevice(SdlInputBackend backend) - : SdlDevice(backend), - IPointerDevice +internal abstract class SdlBoundedPointerDevice : SdlDevice, IPointerDevice { public abstract PointerState State { get; } diff --git a/sources/Input/Input/Implementations/SDL3/SdlDevice.cs b/sources/Input/Input/Implementations/SDL3/SdlDevice.cs index 9980e32b84..8a6fff08cd 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlDevice.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlDevice.cs @@ -3,7 +3,7 @@ namespace Silk.NET.Input.SDL3; -internal abstract class SdlDevice(SdlInputBackend backend) : IInputDevice +internal abstract unsafe class SdlDevice : IInputDevice { public bool Equals(IInputDevice? other) => other?.GetType() == GetType() @@ -11,7 +11,11 @@ public bool Equals(IInputDevice? other) => && other is SdlBoundedPointerDevice dev && dev.Backend.Sdl == Backend.Sdl; - public abstract IntPtr Id { get; } + public nint Id => Backend.AsSilkId(DeviceId); + public required uint DeviceId { get; init; } + public required SdlInputBackend Backend { get; init; } + public required void* DeviceHandle { get; init; } public abstract string Name { get; } - public SdlInputBackend Backend { get; } = backend; + + public abstract void Initialize(); } diff --git a/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs b/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs index d24b71a89d..9effe0a2f0 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs @@ -8,7 +8,11 @@ namespace Silk.NET.Input.SDL3; internal class SdlGamepad : SdlDevice, IGamepad, IDisposable { - private readonly GamepadHandle _gamepad; + public SdlGamepad() + { + + } + private unsafe GamepadHandle _gamepad => (GamepadHandle)DeviceHandle; private static JoystickButton? GetSilkButton(GamepadButton btn) => btn switch @@ -32,10 +36,10 @@ internal class SdlGamepad : SdlDevice, IGamepad, IDisposable _ => null, }; - public SdlGamepad(SdlInputBackend backend, uint joystickId) - : base(backend) + public sealed override void Initialize() { - _gamepad = backend.Sdl.OpenGamepad(joystickId); + _gamepad = Backend.Sdl.OpenGamepad(joystickId); + var backend = Backend; if (_gamepad == nullptr) { backend.Sdl.ThrowError(); @@ -78,12 +82,9 @@ public SdlGamepad(SdlInputBackend backend, uint joystickId) State = new GamepadState(buttons.List.AsButtonList(), thumbsticks, triggers); } - // TODO this is not spec compliant, we need to use a physical device ID - public override unsafe nint Id => (nint)_gamepad.Handle; - public override string Name => Backend.Sdl.GetGamepadName(_gamepad).ReadToString(); - public GamepadState State { get; } + public GamepadState State { get; private set; } = null!; // TODO this entire API needs to be redesigned as right now this is literally only ever going to be useful if it's // just left or right. The original intention was that this would be useful for things like 3D haptics, but what did diff --git a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs index 78ee265472..c7039201f3 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs @@ -3,8 +3,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Numerics; -using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; using Silk.NET.Maths; using Silk.NET.SDL; @@ -17,7 +16,7 @@ internal class SdlInputBackend : IInputBackend, ICursorConfiguration private bool _pumped; private long _epoch; private List _devices = []; - private List _eventQueue = []; + private readonly EventQueue _pumpedEvents = new(); private WindowHandle _focusedWindow; private ISdl _sdl; @@ -172,7 +171,12 @@ public void Update(IInputHandler? handler = null) } _pumped = false; - throw new NotImplementedException(); + if (handler == null) + { + return; + } + + // process all events that have been queued? } private enum QueuedEventType : byte @@ -194,76 +198,72 @@ private enum QueuedEventType : byte BoundedPointerTargetUpdate, } - private readonly record struct QueuedEvent( - QueuedEventType Type, - ulong Timestamp, - Vector2 Vector0 = default, - Vector2 Vector1 = default - ); - private ulong GetTimestamp(ref readonly Event @event) => unchecked((ulong)(_epoch + (@event.Common.Timestamp * _ticksPerNanosecond))); private unsafe byte OnEvent(void* arg0, Event* arg1) { _pumped = true; + _pumpedEvents.Add(ref *arg1); + return 1; + } + + private void ProcessEvent(ref Event arg1, IInputHandler handler) + { + var timestamp = GetTimestamp(ref arg1); // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault - switch ((EventType)arg1->Common.Type) + switch ((EventType)arg1.Common.Type) { - case EventType.DisplayOrientation: - case EventType.DisplayAdded: - case EventType.DisplayRemoved: - case EventType.DisplayMoved: - case EventType.DisplayDesktopModeChanged: - case EventType.DisplayCurrentModeChanged: - case EventType.DisplayContentScaleChanged: + // Device changed events ------------------------------------------------- + case EventType.KeymapChanged: + break; + case EventType.KeyboardAdded: { - var bounds = SdlBoundedPointerTarget.CalculateBounds(Sdl); - _eventQueue.Add( - new QueuedEvent( - QueuedEventType.BoundedPointerTargetUpdate, - GetTimestamp(ref *arg1), - bounds.Min.ToSystem(), - bounds.Max.ToSystem() - ) - ); + var id = arg1.Kdevice.Which; + Debug.Assert(_devices.All(x => x.Id != AsSilkId(id))); + _ = GetOrCreateKeyboard(id); break; } - case EventType.WindowMouseLeave: + case EventType.KeyboardRemoved: { - _eventQueue.Add( - new QueuedEvent(QueuedEventType.MouseExitedWindow, GetTimestamp(ref *arg1)) - ); + RemoveDevice(arg1.Kdevice.Which); break; } - case EventType.KeyDown: - break; - case EventType.KeyUp: - break; - case EventType.TextEditing: - break; - case EventType.TextInput: - break; - case EventType.KeymapChanged: - break; - case EventType.KeyboardAdded: + + + case EventType.MouseAdded: break; - case EventType.KeyboardRemoved: + case EventType.MouseRemoved: + RemoveDevice(arg1.Mdevice.Which); break; - case EventType.TextEditingCandidates: + + case EventType.GamepadAdded: + { + var id = arg1.Kdevice.Which; + Debug.Assert(_devices.All(x => x.Id != AsSilkId(id))); + _ = GetOrCreateDevice(id); break; - case EventType.MouseMotion: + } + case EventType.GamepadRemoved: + { + RemoveDevice(arg1.Gdevice.Which); break; - case EventType.MouseButtonDown: + } + case EventType.GamepadRemapped: break; - case EventType.MouseButtonUp: + + case EventType.JoystickAdded: + RemoveDevice(arg1.Jdevice.Which); break; - case EventType.MouseWheel: + case EventType.JoystickRemoved: break; - case EventType.MouseAdded: + + // Input events ---------------------------------------------------------- + case EventType.KeyDown: break; - case EventType.MouseRemoved: + case EventType.KeyUp: break; + case EventType.JoystickAxisMotion: break; case EventType.JoystickBallMotion: @@ -274,10 +274,6 @@ private unsafe byte OnEvent(void* arg0, Event* arg1) break; case EventType.JoystickButtonUp: break; - case EventType.JoystickAdded: - break; - case EventType.JoystickRemoved: - break; case EventType.JoystickBatteryUpdated: break; case EventType.JoystickUpdateComplete: @@ -288,12 +284,6 @@ private unsafe byte OnEvent(void* arg0, Event* arg1) break; case EventType.GamepadButtonUp: break; - case EventType.GamepadAdded: - break; - case EventType.GamepadRemoved: - break; - case EventType.GamepadRemapped: - break; case EventType.GamepadTouchpadDown: break; case EventType.GamepadTouchpadMotion: @@ -306,6 +296,22 @@ private unsafe byte OnEvent(void* arg0, Event* arg1) break; case EventType.GamepadSteamHandleUpdated: break; + + case EventType.SensorUpdate: + break; + + + // ----- Pointer events + + case EventType.MouseMotion: + break; + case EventType.MouseButtonDown: + break; + case EventType.MouseButtonUp: + break; + case EventType.MouseWheel: + break; + case EventType.FingerDown: break; case EventType.FingerUp: @@ -314,10 +320,7 @@ private unsafe byte OnEvent(void* arg0, Event* arg1) break; case EventType.FingerCanceled: break; - case EventType.ClipboardUpdate: - break; - case EventType.SensorUpdate: - break; + case EventType.PenProximityIn: break; case EventType.PenProximityOut: @@ -334,9 +337,111 @@ private unsafe byte OnEvent(void* arg0, Event* arg1) break; case EventType.PenAxis: break; + + // Display & window (pointer target) events ---------------------------- + case EventType.DisplayOrientation: + case EventType.DisplayAdded: + case EventType.DisplayRemoved: + case EventType.DisplayMoved: + case EventType.DisplayDesktopModeChanged: + case EventType.DisplayCurrentModeChanged: + case EventType.DisplayContentScaleChanged: + { + var bounds = SdlBoundedPointerTarget.CalculateBounds(Sdl); + var x = (QueuedEventType.BoundedPointerTargetUpdate, + timestamp, + bounds.Min.ToSystem(), + bounds.Max.ToSystem() + ); + break; + } + case EventType.WindowMouseLeave: // do we need to do anything? we should probably track the current window of the pointer + { + var x = (QueuedEventType.MouseExitedWindow, timestamp); + break; + } + + // Text input events ------------------------------------------- + case EventType.TextEditing: + break; + case EventType.TextInput: + break; + case EventType.TextEditingCandidates: + break; + case EventType.ClipboardUpdate: + break; + + } - return 1; + return; + + void RemoveDevice(uint id) + { + var silkId = AsSilkId(id); + var deviceIdx = _devices.FindIndex(x => x.Id == silkId); + + if (deviceIdx == -1) + return; // we never used this device to begin with, so just ignore its removal + + _devices.RemoveAt(deviceIdx); + } + + T GetOrCreateDevice(uint id) where T : SdlDevice, new() + { + // If we already have a device with this ID, return it. + for(var i = 0; i < _devices.Count; i++) + { + if (_devices[i] is T typedDevice && typedDevice.DeviceId == id) + { + return typedDevice; + } + } + + var device = new T() { DeviceId = id, DeviceHandle = , Backend = this}; + _devices.Add(device); + Console.WriteLine($"Gamepad added: (sdl ID: {id})"); + return device; + } + + SdlKeyboard GetOrCreateKeyboard(uint id) + { + // If we already have a device with this ID, return it. + for (var i = 0; i < _devices.Count; i++) + { + if (_devices[i] is SdlKeyboard keyboard && keyboard.DeviceId == id) + { + return keyboard; + } + } + + var device = new SdlKeyboard(); + _devices.Add(device); + Console.WriteLine($"Keyboard added: (sdl ID: {id})"); + return device; + } + } + + /// + /// Turns an sdl device id into a universally unique Silk.NET input id. + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public nint AsSilkId(uint which) + { + return Id + Unsafe.As(ref which) + 1; + } + + /// + /// Reverts the process of to get the original SDL id. + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint AsSdlId(nint id) + { + return (uint)(id - Id - 1); } private unsafe void ReleaseUnmanagedResources() @@ -355,4 +460,10 @@ public void Dispose() } ~SdlInputBackend() => ReleaseUnmanagedResources(); + + private class EventQueue + { + private readonly Queue _events = new(1024); + public void Add(ref Event p0) => _events.Enqueue(p0); + } } diff --git a/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs b/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs index 0dd57a7c44..730b7c5232 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs @@ -5,10 +5,8 @@ namespace Silk.NET.Input.SDL3; internal class SdlJoystick : SdlDevice, IJoystick { - public SdlJoystick(SdlInputBackend backend, uint joystick) - : base(backend) { } - - public override IntPtr Id => throw new NotImplementedException(); + public SdlJoystick() + : base() { } public override string Name => throw new NotImplementedException(); diff --git a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs index eb8ab4b208..59129249ea 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs @@ -2,29 +2,42 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Silk.NET.SDL; namespace Silk.NET.Input.SDL3; internal class SdlKeyboard : SdlDevice, IKeyboard { - public SdlKeyboard(SdlInputBackend backend) - : base(backend) { } - - public override nint Id => throw new NotImplementedException(); - - public override string Name => throw new NotImplementedException(); + public override string Name + { + get + { + var namePtr = Backend.Sdl.GetKeyboardNameForID(Backend.AsSdlId(Id)); + ref var casted = ref Unsafe.As(ref namePtr[0]); + var marshalled = SilkMarshal.NativeToString(ref casted); + return marshalled ?? "Unknown Sdl Keyboard"; + } + } - public KeyboardState State => throw new NotImplementedException(); + public KeyboardState State { get; } = new (); public string? ClipboardText { - get => throw new NotImplementedException(); - set => throw new NotImplementedException(); + get + { + if(Sdl.Instance.HasClipboardText() == 0) + return null; + + return Sdl.Instance.GetClipboardText().ReadToString(); + } + set => throw new NotImplementedException("Setting clipboard text is not implemented in SDL3 backend."); } public bool TryGetKeyName(KeyName key, [NotNullWhen(true)] out string? name) => throw new NotImplementedException(); + // todo - there should be a backend-independent way to do this text input handling via KeyboardState? public void BeginInput() => throw new NotImplementedException(); public string? EndInput() => throw new NotImplementedException(); diff --git a/sources/Input/Input/Implementations/SDL3/SdlPen.cs b/sources/Input/Input/Implementations/SDL3/SdlPen.cs index 3acaa39c24..81e3f27d69 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlPen.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlPen.cs @@ -5,16 +5,6 @@ namespace Silk.NET.Input.SDL3; internal class SdlPen : SdlBoundedPointerDevice { - private uint _penId; - - public SdlPen(SdlInputBackend backend, uint pen) - : base(backend) - { - _penId = pen; - } - - public override IntPtr Id => HashCode.Combine(Backend.Id, _penId); - public override string Name => throw new NotImplementedException(); public override PointerState State => throw new NotImplementedException(); diff --git a/sources/Input/Input/Implementations/SDL3/SdlSharedMouse.cs b/sources/Input/Input/Implementations/SDL3/SdlSharedMouse.cs index e40c95cc11..7f3707cd5a 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlSharedMouse.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlSharedMouse.cs @@ -9,15 +9,14 @@ internal class SdlSharedMouse : SdlBoundedPointerDevice, IMouse { private readonly MouseState _state; - public SdlSharedMouse(SdlInputBackend backend) - : base(backend) + public override void Initialize() { var buttons = InputMarshal.CreateList>(32); var points = InputMarshal.CreateList(1); _state = new MouseState(buttons.List.AsButtonList(), points.List, Vector2.Zero); float x = 0, y = 0; - var buttonMask = backend.Sdl.GetMouseState(x.AsRef(), y.AsRef()); + var buttonMask = Backend.Sdl.GetMouseState(x.AsRef(), y.AsRef()); for (var i = 0; i < 32; i++) { InputMarshal.SetButtonState( @@ -37,7 +36,7 @@ public SdlSharedMouse(SdlInputBackend backend) } var pos = new Vector2(x, y); - var bounds = backend.BoundedPointerTarget.Bounds; + var bounds = Backend.BoundedPointerTarget.Bounds; var min = new Vector2(bounds.Min.X, bounds.Min.Y); var max = new Vector2(bounds.Max.X, bounds.Max.Y); points @@ -50,13 +49,11 @@ public SdlSharedMouse(SdlInputBackend backend) new Vector3((pos - min) / (max - min), 0), default, 1.0f, - backend.BoundedPointerTarget + Backend.BoundedPointerTarget ) ); } - public override IntPtr Id => HashCode.Combine(Backend.Id); - public override string Name => $"{Backend.Name}: Shared/Global Mouse"; MouseState IMouse.State => _state; diff --git a/sources/Input/Input/Implementations/SDL3/SdlUnboundedMouse.cs b/sources/Input/Input/Implementations/SDL3/SdlUnboundedMouse.cs index 6d4b6d58d2..52401c44ca 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlUnboundedMouse.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlUnboundedMouse.cs @@ -7,11 +7,6 @@ namespace Silk.NET.Input.SDL3; internal class SdlUnboundedMouse : SdlDevice, IMouse { - public SdlUnboundedMouse(SdlInputBackend backend, uint mouseId) - : base(backend) { } - - public override IntPtr Id => throw new NotImplementedException(); - public override string Name => throw new NotImplementedException(); public MouseState State => throw new NotImplementedException(); diff --git a/sources/Playground/InputTesting.cs b/sources/Playground/InputTesting.cs new file mode 100644 index 0000000000..4dabddcef6 --- /dev/null +++ b/sources/Playground/InputTesting.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Silk.NET.Input; + +internal partial class InputTesting +{ + private static InputContext InitInput(INativeWindow window) => window.CreateInput(); + + private static void ExecuteInput(InputContext? context, INativeWindow window) + { + if (context == null) + return; + + context.Update(); + + } + + private static void OnInputConnectionChanged(ConnectionEvent obj) + { + Console.WriteLine($"{obj.Device.Name} {obj.Device.Id} {(obj.IsConnected ? "connected" : "disconnected")}"); + } + + +} diff --git a/sources/Playground/Playground.csproj b/sources/Playground/Playground.csproj index 1749875027..e402686b08 100644 --- a/sources/Playground/Playground.csproj +++ b/sources/Playground/Playground.csproj @@ -12,6 +12,7 @@ + diff --git a/sources/Playground/Program.cs b/sources/Playground/Program.cs index 2a7afc3c8d..0132c97b78 100644 --- a/sources/Playground/Program.cs +++ b/sources/Playground/Program.cs @@ -2,11 +2,12 @@ using System.Runtime.InteropServices; using Silk.NET.Core; +using Silk.NET.Input; using Silk.NET.OpenGL; using Silk.NET.Windowing; using Surface = Silk.NET.Windowing.Surface; -internal class MyApplication : ISurfaceApplication +internal partial class InputTesting : ISurfaceApplication { public static void Initialize(TSurface surface) where TSurface : Surface @@ -22,9 +23,11 @@ public static void Initialize(TSurface surface) surface.OpenGL.Profile = OpenGLContextProfile.Core; surface.OpenGL.Version = new Version32(3, 3); + var vbo = 0u; var vao = 0u; var prog = 0u; + InputContext? inputContext = null; surface.Created += _ => { // Make the OpenGL context current, this will allow us to use GL static functions. @@ -92,9 +95,15 @@ void main() GL.DeleteShader(vert); GL.DeleteShader(frag); GL.UseProgram(prog); + + + inputContext = InitInput(surface); + inputContext.ConnectionChanged += OnInputConnectionChanged; }; surface.Render += _ => { + ExecuteInput(inputContext, surface); + GL.Clear(ClearBufferMask.ColorBufferBit); GL.DrawArrays(PrimitiveType.Triangles, 0, 3); }; @@ -105,5 +114,5 @@ void main() }; } - public static void Main() => ISurfaceApplication.Run(); + public static void Main() => ISurfaceApplication.Run(); } diff --git a/sources/SDL/SDL/InputHandleExtensions.cs b/sources/SDL/SDL/InputHandleExtensions.cs new file mode 100644 index 0000000000..9b0fdfd812 --- /dev/null +++ b/sources/SDL/SDL/InputHandleExtensions.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Silk.NET.SDL; + +public partial struct GamepadHandle +{ + private unsafe GamepadHandle(void* handle) => Handle = handle; + + // conversion from void ptr + public static unsafe explicit operator GamepadHandle (void* handle) => new GamepadHandle(handle); +} + +public partial struct JoystickHandle +{ + private unsafe JoystickHandle(void* handle) => Handle = handle; + + // conversion from void ptr + public static unsafe explicit operator JoystickHandle (void* handle) => new JoystickHandle(handle); +} + +public partial struct SensorHandle +{ + private unsafe SensorHandle(void* handle) => Handle = handle; + + // conversion from void ptr + public static unsafe explicit operator SensorHandle (void* handle) => new SensorHandle(handle); +} + +public partial struct HidDeviceHandle +{ + private unsafe HidDeviceHandle(void* handle) => Handle = handle; + + // conversion from void ptr + public static unsafe explicit operator HidDeviceHandle (void* handle) => new HidDeviceHandle(handle); +} diff --git a/sources/Windowing/Windowing/Implementations/SDL3/SdlSurface.cs b/sources/Windowing/Windowing/Implementations/SDL3/SdlSurface.cs index e9e80726ef..233fb6bd22 100644 --- a/sources/Windowing/Windowing/Implementations/SDL3/SdlSurface.cs +++ b/sources/Windowing/Windowing/Implementations/SDL3/SdlSurface.cs @@ -91,7 +91,7 @@ public override bool TryGetPlatformInfo( if (typeof(TPlatformInfo) == typeof(SdlPlatformInfo)) { - info = (TPlatformInfo)(object)new SdlPlatformInfo(Impl.Handle); + info = (TPlatformInfo)(object)new SdlPlatformInfo(Impl.Handle, Sdl.Instance); return true; } From f129ae9b21be42649ef08a0f4d8cd4c0e8c166e3 Mon Sep 17 00:00:00 2001 From: dom Date: Tue, 24 Jun 2025 17:12:51 -0400 Subject: [PATCH 08/39] refine internal device creation abstractions --- .../{ => Pointers}/SdlBoundedPointerDevice.cs | 8 ++++- .../{ => Pointers}/SdlBoundedPointerTarget.cs | 3 +- .../Implementations/SDL3/Pointers/SdlPen.cs | 19 ++++++++++++ .../SDL3/{ => Pointers}/SdlSharedMouse.cs | 11 +++++-- .../SDL3/{ => Pointers}/SdlTouchScreen.cs | 2 +- .../SDL3/{ => Pointers}/SdlUnboundedMouse.cs | 8 ++++- .../SdlUnboundedPointerTarget.cs | 15 ++++++--- .../Input/Implementations/SDL3/SdlDevice.cs | 31 +++++++++++++++---- .../Input/Implementations/SDL3/SdlGamepad.cs | 20 ++++++------ .../Implementations/SDL3/SdlInputBackend.cs | 26 +++------------- .../Input/Implementations/SDL3/SdlJoystick.cs | 11 +++++-- .../Input/Implementations/SDL3/SdlKeyboard.cs | 10 +++++- .../Input/Implementations/SDL3/SdlPen.cs | 11 ------- 13 files changed, 111 insertions(+), 64 deletions(-) rename sources/Input/Input/Implementations/SDL3/{ => Pointers}/SdlBoundedPointerDevice.cs (78%) rename sources/Input/Input/Implementations/SDL3/{ => Pointers}/SdlBoundedPointerTarget.cs (98%) create mode 100644 sources/Input/Input/Implementations/SDL3/Pointers/SdlPen.cs rename sources/Input/Input/Implementations/SDL3/{ => Pointers}/SdlSharedMouse.cs (79%) rename sources/Input/Input/Implementations/SDL3/{ => Pointers}/SdlTouchScreen.cs (93%) rename sources/Input/Input/Implementations/SDL3/{ => Pointers}/SdlUnboundedMouse.cs (71%) rename sources/Input/Input/Implementations/SDL3/{ => Pointers}/SdlUnboundedPointerTarget.cs (66%) delete mode 100644 sources/Input/Input/Implementations/SDL3/SdlPen.cs diff --git a/sources/Input/Input/Implementations/SDL3/SdlBoundedPointerDevice.cs b/sources/Input/Input/Implementations/SDL3/Pointers/SdlBoundedPointerDevice.cs similarity index 78% rename from sources/Input/Input/Implementations/SDL3/SdlBoundedPointerDevice.cs rename to sources/Input/Input/Implementations/SDL3/Pointers/SdlBoundedPointerDevice.cs index e3bdd94703..497c728205 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlBoundedPointerDevice.cs +++ b/sources/Input/Input/Implementations/SDL3/Pointers/SdlBoundedPointerDevice.cs @@ -3,13 +3,19 @@ using System.Diagnostics.CodeAnalysis; -namespace Silk.NET.Input.SDL3; +namespace Silk.NET.Input.SDL3.Pointers; /// /// A base class for SDL input devices that operate in terms of a window's or DWMs bounds. /// internal abstract class SdlBoundedPointerDevice : SdlDevice, IPointerDevice { + protected SdlBoundedPointerDevice(uint sdlDeviceId, SdlInputBackend backend, IReadOnlyList targets, InputMarshal.ListOwner boundedPoints) : base(sdlDeviceId, backend) + { + Targets = targets; + BoundedPoints = boundedPoints; + } + public abstract PointerState State { get; } [field: MaybeNull] diff --git a/sources/Input/Input/Implementations/SDL3/SdlBoundedPointerTarget.cs b/sources/Input/Input/Implementations/SDL3/Pointers/SdlBoundedPointerTarget.cs similarity index 98% rename from sources/Input/Input/Implementations/SDL3/SdlBoundedPointerTarget.cs rename to sources/Input/Input/Implementations/SDL3/Pointers/SdlBoundedPointerTarget.cs index f7aee7b94c..cb7e353f4f 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlBoundedPointerTarget.cs +++ b/sources/Input/Input/Implementations/SDL3/Pointers/SdlBoundedPointerTarget.cs @@ -1,11 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Numerics; using Silk.NET.Maths; using Silk.NET.SDL; -namespace Silk.NET.Input.SDL3; +namespace Silk.NET.Input.SDL3.Pointers; internal class SdlBoundedPointerTarget(SdlInputBackend backend) : IPointerTarget { diff --git a/sources/Input/Input/Implementations/SDL3/Pointers/SdlPen.cs b/sources/Input/Input/Implementations/SDL3/Pointers/SdlPen.cs new file mode 100644 index 0000000000..27da866370 --- /dev/null +++ b/sources/Input/Input/Implementations/SDL3/Pointers/SdlPen.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Silk.NET.Input.SDL3.Pointers; + +internal class SdlPen : SdlBoundedPointerDevice, ISdlDevice +{ + public override unsafe void* DeviceHandle => throw new NotImplementedException(); + + public static SdlPen CreateDevice(SdlInputBackend backend, uint sdlDeviceId) => throw new NotImplementedException(); + + public override string Name => throw new NotImplementedException(); + + public override PointerState State => throw new NotImplementedException(); + + public SdlPen(uint sdlDeviceId, SdlInputBackend backend, IReadOnlyList targets, InputMarshal.ListOwner boundedPoints) : base(sdlDeviceId, backend, targets, boundedPoints) + { + } +} diff --git a/sources/Input/Input/Implementations/SDL3/SdlSharedMouse.cs b/sources/Input/Input/Implementations/SDL3/Pointers/SdlSharedMouse.cs similarity index 79% rename from sources/Input/Input/Implementations/SDL3/SdlSharedMouse.cs rename to sources/Input/Input/Implementations/SDL3/Pointers/SdlSharedMouse.cs index 7f3707cd5a..5c97330452 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlSharedMouse.cs +++ b/sources/Input/Input/Implementations/SDL3/Pointers/SdlSharedMouse.cs @@ -3,13 +3,14 @@ using System.Numerics; -namespace Silk.NET.Input.SDL3; +namespace Silk.NET.Input.SDL3.Pointers; -internal class SdlSharedMouse : SdlBoundedPointerDevice, IMouse +internal class SdlSharedMouse : SdlBoundedPointerDevice, IMouse, ISdlDevice { private readonly MouseState _state; - public override void Initialize() + public SdlSharedMouse(uint sdlDeviceId, SdlInputBackend backend, IReadOnlyList targets, InputMarshal.ListOwner boundedPoints) + : base(sdlDeviceId, backend, targets, boundedPoints) { var buttons = InputMarshal.CreateList>(32); var points = InputMarshal.CreateList(1); @@ -54,6 +55,10 @@ public override void Initialize() ); } + public override unsafe void* DeviceHandle => throw new NotImplementedException(); + + public static SdlSharedMouse CreateDevice(SdlInputBackend backend, uint sdlDeviceId) => throw new NotImplementedException(); + public override string Name => $"{Backend.Name}: Shared/Global Mouse"; MouseState IMouse.State => _state; diff --git a/sources/Input/Input/Implementations/SDL3/SdlTouchScreen.cs b/sources/Input/Input/Implementations/SDL3/Pointers/SdlTouchScreen.cs similarity index 93% rename from sources/Input/Input/Implementations/SDL3/SdlTouchScreen.cs rename to sources/Input/Input/Implementations/SDL3/Pointers/SdlTouchScreen.cs index 7219f1a1d9..6bbeab5525 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlTouchScreen.cs +++ b/sources/Input/Input/Implementations/SDL3/Pointers/SdlTouchScreen.cs @@ -1,7 +1,7 @@ // 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; +namespace Silk.NET.Input.SDL3.Pointers; internal class SdlTouchScreen : IPointerDevice { diff --git a/sources/Input/Input/Implementations/SDL3/SdlUnboundedMouse.cs b/sources/Input/Input/Implementations/SDL3/Pointers/SdlUnboundedMouse.cs similarity index 71% rename from sources/Input/Input/Implementations/SDL3/SdlUnboundedMouse.cs rename to sources/Input/Input/Implementations/SDL3/Pointers/SdlUnboundedMouse.cs index 52401c44ca..a4de78a485 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlUnboundedMouse.cs +++ b/sources/Input/Input/Implementations/SDL3/Pointers/SdlUnboundedMouse.cs @@ -3,10 +3,16 @@ using System.Numerics; -namespace Silk.NET.Input.SDL3; +namespace Silk.NET.Input.SDL3.Pointers; internal class SdlUnboundedMouse : SdlDevice, IMouse { + public SdlUnboundedMouse(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDeviceId, backend) + { + } + + public override unsafe void* DeviceHandle => throw new NotImplementedException(); + public override string Name => throw new NotImplementedException(); public MouseState State => throw new NotImplementedException(); diff --git a/sources/Input/Input/Implementations/SDL3/SdlUnboundedPointerTarget.cs b/sources/Input/Input/Implementations/SDL3/Pointers/SdlUnboundedPointerTarget.cs similarity index 66% rename from sources/Input/Input/Implementations/SDL3/SdlUnboundedPointerTarget.cs rename to sources/Input/Input/Implementations/SDL3/Pointers/SdlUnboundedPointerTarget.cs index 797453aef1..368bd3e096 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlUnboundedPointerTarget.cs +++ b/sources/Input/Input/Implementations/SDL3/Pointers/SdlUnboundedPointerTarget.cs @@ -3,7 +3,7 @@ using Silk.NET.Maths; -namespace Silk.NET.Input.SDL3; +namespace Silk.NET.Input.SDL3.Pointers; internal class SdlUnboundedPointerTarget(SdlInputBackend backend) : IPointerTarget { @@ -20,17 +20,22 @@ internal class SdlUnboundedPointerTarget(SdlInputBackend backend) : IPointerTarg public int GetPointCount(IPointerDevice pointer) { - if (pointer is not SdlUnboundedMouse mouse) + if (pointer is not SdlDevice device) { return 0; } - if (mouse.Backend != backend) + if (pointer.State.Points.Count == 0) { - return mouse.Backend.UnboundedPointerTarget.GetPointCount(pointer); + return 0; + } + + if (device.Backend != backend) + { + return device.Backend.UnboundedPointerTarget.GetPointCount(pointer); } - return (mouse.Backend.Mode & CursorModes.Unbounded) != 0 ? 1 : 0; + return (device.Backend.Mode & CursorModes.Unbounded) != 0 ? 1 : 0; } public TargetPoint GetPoint(IPointerDevice pointer, int point) => diff --git a/sources/Input/Input/Implementations/SDL3/SdlDevice.cs b/sources/Input/Input/Implementations/SDL3/SdlDevice.cs index 8a6fff08cd..580286767f 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlDevice.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlDevice.cs @@ -1,21 +1,40 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. + +using Silk.NET.Input.SDL3.Pointers; + namespace Silk.NET.Input.SDL3; +/// +/// A base class for all SDL input devices. +/// internal abstract unsafe class SdlDevice : IInputDevice { - public bool Equals(IInputDevice? other) => + bool IEquatable.Equals(IInputDevice? other) => other?.GetType() == GetType() && other.Id == Id && other is SdlBoundedPointerDevice dev && dev.Backend.Sdl == Backend.Sdl; - public nint Id => Backend.AsSilkId(DeviceId); - public required uint DeviceId { get; init; } - public required SdlInputBackend Backend { get; init; } - public required void* DeviceHandle { get; init; } + public nint Id => Backend.AsSilkId(SdlDeviceId); + public uint SdlDeviceId { get; } + public SdlInputBackend Backend { get; } + public abstract void* DeviceHandle { get; } public abstract string Name { get; } - public abstract void Initialize(); + public SdlDevice(uint sdlDeviceId, SdlInputBackend backend) + { + Backend = backend; + SdlDeviceId = sdlDeviceId; + } +} + +/// +/// An interface defining a generic constructor for managed SDL devices. +/// +/// +internal interface ISdlDevice : IInputDevice where T : SdlDevice +{ + public static abstract T CreateDevice(SdlInputBackend backend, uint sdlDeviceId); } diff --git a/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs b/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs index 9effe0a2f0..48171f915f 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs @@ -6,13 +6,9 @@ namespace Silk.NET.Input.SDL3; -internal class SdlGamepad : SdlDevice, IGamepad, IDisposable +internal sealed class SdlGamepad : SdlDevice, IGamepad, IDisposable, ISdlDevice { - public SdlGamepad() - { - - } - private unsafe GamepadHandle _gamepad => (GamepadHandle)DeviceHandle; + private readonly GamepadHandle _gamepad; private static JoystickButton? GetSilkButton(GamepadButton btn) => btn switch @@ -36,10 +32,9 @@ public SdlGamepad() _ => null, }; - public sealed override void Initialize() + private SdlGamepad(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDeviceId, backend) { - _gamepad = Backend.Sdl.OpenGamepad(joystickId); - var backend = Backend; + _gamepad = Backend.Sdl.OpenGamepad(sdlDeviceId); if (_gamepad == nullptr) { backend.Sdl.ThrowError(); @@ -82,6 +77,8 @@ public sealed override void Initialize() State = new GamepadState(buttons.List.AsButtonList(), thumbsticks, triggers); } + public override unsafe void* DeviceHandle => _gamepad.Handle; + public override string Name => Backend.Sdl.GetGamepadName(_gamepad).ReadToString(); public GamepadState State { get; private set; } = null!; @@ -122,5 +119,10 @@ public void Dispose() GC.SuppressFinalize(this); } + public static SdlGamepad CreateDevice(SdlInputBackend backend, uint sdlDeviceId) + { + throw new NotImplementedException(); + } + ~SdlGamepad() => ReleaseUnmanagedResources(); } diff --git a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs index c7039201f3..4f030349b4 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; +using Silk.NET.Input.SDL3.Pointers; using Silk.NET.Maths; using Silk.NET.SDL; @@ -221,7 +222,7 @@ private void ProcessEvent(ref Event arg1, IInputHandler handler) { var id = arg1.Kdevice.Which; Debug.Assert(_devices.All(x => x.Id != AsSilkId(id))); - _ = GetOrCreateKeyboard(id); + _ = GetOrCreateDevice(id); break; } case EventType.KeyboardRemoved: @@ -387,39 +388,22 @@ void RemoveDevice(uint id) _devices.RemoveAt(deviceIdx); } - T GetOrCreateDevice(uint id) where T : SdlDevice, new() + T GetOrCreateDevice(uint id) where T : SdlDevice, ISdlDevice { // If we already have a device with this ID, return it. for(var i = 0; i < _devices.Count; i++) { - if (_devices[i] is T typedDevice && typedDevice.DeviceId == id) + if (_devices[i] is T typedDevice && typedDevice.SdlDeviceId == id) { return typedDevice; } } - var device = new T() { DeviceId = id, DeviceHandle = , Backend = this}; + var device = T.CreateDevice(this, id); _devices.Add(device); Console.WriteLine($"Gamepad added: (sdl ID: {id})"); return device; } - - SdlKeyboard GetOrCreateKeyboard(uint id) - { - // If we already have a device with this ID, return it. - for (var i = 0; i < _devices.Count; i++) - { - if (_devices[i] is SdlKeyboard keyboard && keyboard.DeviceId == id) - { - return keyboard; - } - } - - var device = new SdlKeyboard(); - _devices.Add(device); - Console.WriteLine($"Keyboard added: (sdl ID: {id})"); - return device; - } } /// diff --git a/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs b/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs index 730b7c5232..5bb1e6aa3e 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs @@ -3,10 +3,15 @@ namespace Silk.NET.Input.SDL3; -internal class SdlJoystick : SdlDevice, IJoystick +internal class SdlJoystick : SdlDevice, IJoystick, ISdlDevice { - public SdlJoystick() - : base() { } + public SdlJoystick(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDeviceId, backend) + { + } + + public override unsafe void* DeviceHandle => throw new NotImplementedException(); + + public static SdlJoystick CreateDevice(SdlInputBackend backend, uint sdlDeviceId) => throw new NotImplementedException(); public override string Name => throw new NotImplementedException(); diff --git a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs index 59129249ea..d27761baea 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs @@ -7,8 +7,16 @@ namespace Silk.NET.Input.SDL3; -internal class SdlKeyboard : SdlDevice, IKeyboard +internal class SdlKeyboard : SdlDevice, IKeyboard, ISdlDevice { + public SdlKeyboard(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDeviceId, backend) + { + } + + public override unsafe void* DeviceHandle => throw new NotImplementedException(); + + public static SdlKeyboard CreateDevice(SdlInputBackend backend, uint sdlDeviceId) => throw new NotImplementedException(); + public override string Name { get diff --git a/sources/Input/Input/Implementations/SDL3/SdlPen.cs b/sources/Input/Input/Implementations/SDL3/SdlPen.cs deleted file mode 100644 index 81e3f27d69..0000000000 --- a/sources/Input/Input/Implementations/SDL3/SdlPen.cs +++ /dev/null @@ -1,11 +0,0 @@ -// 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 : SdlBoundedPointerDevice -{ - public override string Name => throw new NotImplementedException(); - - public override PointerState State => throw new NotImplementedException(); -} From fcf49f6a77965aa8f4ea384ae1741e85d0f7b895 Mon Sep 17 00:00:00 2001 From: dom Date: Tue, 24 Jun 2025 19:09:23 -0400 Subject: [PATCH 09/39] continued - move name back where it belongs --- .../SDL3/Pointers/SdlBoundedPointerDevice.cs | 2 ++ .../Input/Implementations/SDL3/Pointers/SdlPen.cs | 4 ---- .../SDL3/Pointers/SdlSharedMouse.cs | 2 -- .../SDL3/Pointers/SdlUnboundedMouse.cs | 4 +--- .../Input/Input/Implementations/SDL3/SdlDevice.cs | 12 +++++++++++- .../Input/Implementations/SDL3/SdlGamepad.cs | 8 +++----- .../Input/Implementations/SDL3/SdlJoystick.cs | 5 +---- .../Input/Implementations/SDL3/SdlKeyboard.cs | 15 +-------------- 8 files changed, 19 insertions(+), 33 deletions(-) diff --git a/sources/Input/Input/Implementations/SDL3/Pointers/SdlBoundedPointerDevice.cs b/sources/Input/Input/Implementations/SDL3/Pointers/SdlBoundedPointerDevice.cs index 497c728205..a2389d96f6 100644 --- a/sources/Input/Input/Implementations/SDL3/Pointers/SdlBoundedPointerDevice.cs +++ b/sources/Input/Input/Implementations/SDL3/Pointers/SdlBoundedPointerDevice.cs @@ -18,6 +18,8 @@ protected SdlBoundedPointerDevice(uint sdlDeviceId, SdlInputBackend backend, IRe public abstract PointerState State { get; } + public override string Name => Backend.Sdl.GetMouseNameForID(SdlDeviceId).ReadToString(); + [field: MaybeNull] public virtual IReadOnlyList Targets => field ??= [Backend.BoundedPointerTarget]; diff --git a/sources/Input/Input/Implementations/SDL3/Pointers/SdlPen.cs b/sources/Input/Input/Implementations/SDL3/Pointers/SdlPen.cs index 27da866370..7b37a1357e 100644 --- a/sources/Input/Input/Implementations/SDL3/Pointers/SdlPen.cs +++ b/sources/Input/Input/Implementations/SDL3/Pointers/SdlPen.cs @@ -5,12 +5,8 @@ namespace Silk.NET.Input.SDL3.Pointers; internal class SdlPen : SdlBoundedPointerDevice, ISdlDevice { - public override unsafe void* DeviceHandle => throw new NotImplementedException(); - public static SdlPen CreateDevice(SdlInputBackend backend, uint sdlDeviceId) => throw new NotImplementedException(); - public override string Name => throw new NotImplementedException(); - public override PointerState State => throw new NotImplementedException(); public SdlPen(uint sdlDeviceId, SdlInputBackend backend, IReadOnlyList targets, InputMarshal.ListOwner boundedPoints) : base(sdlDeviceId, backend, targets, boundedPoints) diff --git a/sources/Input/Input/Implementations/SDL3/Pointers/SdlSharedMouse.cs b/sources/Input/Input/Implementations/SDL3/Pointers/SdlSharedMouse.cs index 5c97330452..9388b32d86 100644 --- a/sources/Input/Input/Implementations/SDL3/Pointers/SdlSharedMouse.cs +++ b/sources/Input/Input/Implementations/SDL3/Pointers/SdlSharedMouse.cs @@ -55,8 +55,6 @@ public SdlSharedMouse(uint sdlDeviceId, SdlInputBackend backend, IReadOnlyList throw new NotImplementedException(); - public static SdlSharedMouse CreateDevice(SdlInputBackend backend, uint sdlDeviceId) => throw new NotImplementedException(); public override string Name => $"{Backend.Name}: Shared/Global Mouse"; diff --git a/sources/Input/Input/Implementations/SDL3/Pointers/SdlUnboundedMouse.cs b/sources/Input/Input/Implementations/SDL3/Pointers/SdlUnboundedMouse.cs index a4de78a485..ac24b783fe 100644 --- a/sources/Input/Input/Implementations/SDL3/Pointers/SdlUnboundedMouse.cs +++ b/sources/Input/Input/Implementations/SDL3/Pointers/SdlUnboundedMouse.cs @@ -11,11 +11,9 @@ public SdlUnboundedMouse(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDe { } - public override unsafe void* DeviceHandle => throw new NotImplementedException(); - - public override string Name => throw new NotImplementedException(); public MouseState State => throw new NotImplementedException(); + public override string Name => Backend.Sdl.GetMouseNameForID(SdlDeviceId).ReadToString(); public ICursorConfiguration Cursor => Backend; diff --git a/sources/Input/Input/Implementations/SDL3/SdlDevice.cs b/sources/Input/Input/Implementations/SDL3/SdlDevice.cs index 580286767f..bea3d90091 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlDevice.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlDevice.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. +using System.Runtime.CompilerServices; using Silk.NET.Input.SDL3.Pointers; namespace Silk.NET.Input.SDL3; @@ -20,8 +21,17 @@ bool IEquatable.Equals(IInputDevice? other) => public nint Id => Backend.AsSilkId(SdlDeviceId); public uint SdlDeviceId { get; } public SdlInputBackend Backend { get; } - public abstract void* DeviceHandle { get; } + public abstract string Name { get; } + /*{ + { + var namePtr = _sdlNameFunc(SdlDeviceId); + ref var casted = ref Unsafe.As(ref namePtr[0]); + var marshalled = SilkMarshal.NativeToString(ref casted); + return marshalled ?? "Unknown Sdl Keyboard"; + } + }*/ + public SdlDevice(uint sdlDeviceId, SdlInputBackend backend) { diff --git a/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs b/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs index 48171f915f..d030ca29f5 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs @@ -34,7 +34,7 @@ internal sealed class SdlGamepad : SdlDevice, IGamepad, IDisposable, ISdlDevice< private SdlGamepad(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDeviceId, backend) { - _gamepad = Backend.Sdl.OpenGamepad(sdlDeviceId); + _gamepad = backend.Sdl.OpenGamepad(sdlDeviceId); if (_gamepad == nullptr) { backend.Sdl.ThrowError(); @@ -77,11 +77,9 @@ private SdlGamepad(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDeviceId State = new GamepadState(buttons.List.AsButtonList(), thumbsticks, triggers); } - public override unsafe void* DeviceHandle => _gamepad.Handle; + public GamepadState State { get; } - public override string Name => Backend.Sdl.GetGamepadName(_gamepad).ReadToString(); - - public GamepadState State { get; private set; } = null!; + public override string Name => Backend.Sdl.GetGamepadNameForID(SdlDeviceId).ReadToString(); // TODO this entire API needs to be redesigned as right now this is literally only ever going to be useful if it's // just left or right. The original intention was that this would be useful for things like 3D haptics, but what did diff --git a/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs b/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs index 5bb1e6aa3e..645adc9515 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs @@ -8,12 +8,9 @@ internal class SdlJoystick : SdlDevice, IJoystick, ISdlDevice public SdlJoystick(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDeviceId, backend) { } - - public override unsafe void* DeviceHandle => throw new NotImplementedException(); + public override string Name => Backend.Sdl.GetJoystickNameForID(SdlDeviceId).ReadToString(); public static SdlJoystick CreateDevice(SdlInputBackend backend, uint sdlDeviceId) => throw new NotImplementedException(); - public override 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 index d27761baea..ffd78ff69f 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; using Silk.NET.SDL; namespace Silk.NET.Input.SDL3; @@ -13,23 +12,11 @@ public SdlKeyboard(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDeviceId { } - public override unsafe void* DeviceHandle => throw new NotImplementedException(); - public static SdlKeyboard CreateDevice(SdlInputBackend backend, uint sdlDeviceId) => throw new NotImplementedException(); - public override string Name - { - get - { - var namePtr = Backend.Sdl.GetKeyboardNameForID(Backend.AsSdlId(Id)); - ref var casted = ref Unsafe.As(ref namePtr[0]); - var marshalled = SilkMarshal.NativeToString(ref casted); - return marshalled ?? "Unknown Sdl Keyboard"; - } - } - public KeyboardState State { get; } = new (); + public override string Name => Backend.Sdl.GetKeyboardNameForID(SdlDeviceId).ReadToString(); public string? ClipboardText { get From 33cc20dc45a38a9901e39b1c3ce87a2ed9405f1f Mon Sep 17 00:00:00 2001 From: dom Date: Sun, 29 Jun 2025 12:12:23 -0400 Subject: [PATCH 10/39] cont'd --- .../SDL3/Pointers/SdlBoundedPointerDevice.cs | 5 ++ .../SDL3/Pointers/SdlTouchScreen.cs | 13 +++-- .../SDL3/Pointers/SdlUnboundedMouse.cs | 5 ++ .../Input/Implementations/SDL3/SdlDevice.cs | 4 +- .../Input/Implementations/SDL3/SdlGamepad.cs | 2 + .../Implementations/SDL3/SdlInputBackend.cs | 46 +++++++++++---- .../Input/Implementations/SDL3/SdlJoystick.cs | 14 ++++- .../Input/Implementations/SDL3/SdlKeyboard.cs | 19 +++++- sources/Input/Input/KeyboardState.cs | 58 ++++++++++++++++++- 9 files changed, 144 insertions(+), 22 deletions(-) diff --git a/sources/Input/Input/Implementations/SDL3/Pointers/SdlBoundedPointerDevice.cs b/sources/Input/Input/Implementations/SDL3/Pointers/SdlBoundedPointerDevice.cs index a2389d96f6..52f59143fd 100644 --- a/sources/Input/Input/Implementations/SDL3/Pointers/SdlBoundedPointerDevice.cs +++ b/sources/Input/Input/Implementations/SDL3/Pointers/SdlBoundedPointerDevice.cs @@ -34,4 +34,9 @@ protected SdlBoundedPointerDevice(uint sdlDeviceId, SdlInputBackend backend, IRe // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract public InputMarshal.ListOwner BoundedPoints => field.List.Data is null ? field = InputMarshal.CreateList() : field; + + public sealed override void Release() + { + + } } diff --git a/sources/Input/Input/Implementations/SDL3/Pointers/SdlTouchScreen.cs b/sources/Input/Input/Implementations/SDL3/Pointers/SdlTouchScreen.cs index 6bbeab5525..3e90e03e16 100644 --- a/sources/Input/Input/Implementations/SDL3/Pointers/SdlTouchScreen.cs +++ b/sources/Input/Input/Implementations/SDL3/Pointers/SdlTouchScreen.cs @@ -3,15 +3,20 @@ namespace Silk.NET.Input.SDL3.Pointers; -internal class SdlTouchScreen : IPointerDevice +internal class SdlTouchScreen : SdlDevice, ISdlDevice, IPointerDevice { - public bool Equals(IInputDevice? other) => throw new NotImplementedException(); + public static SdlTouchScreen CreateDevice(SdlInputBackend backend, uint sdlDeviceId) => throw new NotImplementedException(); - public IntPtr Id => throw new NotImplementedException(); + public bool Equals(IInputDevice? other) => throw new NotImplementedException(); - public string Name => throw new NotImplementedException(); + public override string Name => throw new NotImplementedException(); + public override void Release() => throw new NotImplementedException(); public PointerState State => throw new NotImplementedException(); public IReadOnlyList Targets => throw new NotImplementedException(); + + public SdlTouchScreen(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDeviceId, backend) + { + } } diff --git a/sources/Input/Input/Implementations/SDL3/Pointers/SdlUnboundedMouse.cs b/sources/Input/Input/Implementations/SDL3/Pointers/SdlUnboundedMouse.cs index ac24b783fe..a8d761b4fd 100644 --- a/sources/Input/Input/Implementations/SDL3/Pointers/SdlUnboundedMouse.cs +++ b/sources/Input/Input/Implementations/SDL3/Pointers/SdlUnboundedMouse.cs @@ -20,4 +20,9 @@ public SdlUnboundedMouse(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDe public bool TrySetPosition(Vector2 position) => throw new NotImplementedException(); public IReadOnlyList Targets => throw new NotImplementedException(); + + public override void Release() + { + // nothing? + } } diff --git a/sources/Input/Input/Implementations/SDL3/SdlDevice.cs b/sources/Input/Input/Implementations/SDL3/SdlDevice.cs index bea3d90091..4d25c4ce03 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlDevice.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlDevice.cs @@ -33,11 +33,13 @@ bool IEquatable.Equals(IInputDevice? other) => }*/ - public SdlDevice(uint sdlDeviceId, SdlInputBackend backend) + protected SdlDevice(uint sdlDeviceId, SdlInputBackend backend) { Backend = backend; SdlDeviceId = sdlDeviceId; } + + public abstract void Release(); } /// diff --git a/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs b/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs index d030ca29f5..46c2182cbd 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs @@ -77,6 +77,8 @@ private SdlGamepad(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDeviceId State = new GamepadState(buttons.List.AsButtonList(), thumbsticks, triggers); } + public override void Release() => Backend.Sdl.CloseGamepad(_gamepad); + public GamepadState State { get; } public override string Name => Backend.Sdl.GetGamepadNameForID(SdlDeviceId).ReadToString(); diff --git a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs index 4f030349b4..92d67502ee 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs @@ -16,7 +16,7 @@ internal class SdlInputBackend : IInputBackend, ICursorConfiguration private bool _pumped; private long _epoch; - private List _devices = []; + private List _devices = []; private readonly EventQueue _pumpedEvents = new(); private WindowHandle _focusedWindow; private ISdl _sdl; @@ -174,10 +174,16 @@ public void Update(IInputHandler? handler = null) _pumped = false; if (handler == null) { + _pumpedEvents.Clear(); return; } // process all events that have been queued? + + while (_pumpedEvents.TryDequeue(out var evt)) + { + ProcessEvent(ref evt, handler); + } } private enum QueuedEventType : byte @@ -211,7 +217,10 @@ private unsafe byte OnEvent(void* arg0, Event* arg1) private void ProcessEvent(ref Event arg1, IInputHandler handler) { - var timestamp = GetTimestamp(ref arg1); + var timestamp = GetTimestamp(ref arg1); + Debug.Assert(timestamp >= _previousTimestamp, "Events out of order"); + _previousTimestamp = timestamp; + // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault switch ((EventType)arg1.Common.Type) { @@ -261,9 +270,17 @@ private void ProcessEvent(ref Event arg1, IInputHandler handler) // Input events ---------------------------------------------------------- case EventType.KeyDown: + { + var keyboard = GetOrCreateDevice(arg1.Key.Which); + keyboard.AddKeyEvent(EventType.KeyDown, arg1.Key); break; + } case EventType.KeyUp: + { + var keyboard = GetOrCreateDevice(arg1.Key.Which); + keyboard.AddKeyEvent(EventType.KeyUp, arg1.Key); break; + } case EventType.JoystickAxisMotion: break; @@ -356,7 +373,8 @@ private void ProcessEvent(ref Event arg1, IInputHandler handler) ); break; } - case EventType.WindowMouseLeave: // do we need to do anything? we should probably track the current window of the pointer + case EventType.WindowMouseLeave + : // do we need to do anything? we should probably track the current window of the pointer { var x = (QueuedEventType.MouseExitedWindow, timestamp); break; @@ -385,13 +403,15 @@ void RemoveDevice(uint id) if (deviceIdx == -1) return; // we never used this device to begin with, so just ignore its removal + var device = _devices[deviceIdx]; + device.Release(); _devices.RemoveAt(deviceIdx); } T GetOrCreateDevice(uint id) where T : SdlDevice, ISdlDevice { // If we already have a device with this ID, return it. - for(var i = 0; i < _devices.Count; i++) + for (var i = 0; i < _devices.Count; i++) { if (_devices[i] is T typedDevice && typedDevice.SdlDeviceId == id) { @@ -412,10 +432,7 @@ T GetOrCreateDevice(uint id) where T : SdlDevice, ISdlDevice /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public nint AsSilkId(uint which) - { - return Id + Unsafe.As(ref which) + 1; - } + public nint AsSilkId(uint which) => Id + Unsafe.As(ref which) + 1; /// /// Reverts the process of to get the original SDL id. @@ -423,10 +440,9 @@ public nint AsSilkId(uint which) /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public uint AsSdlId(nint id) - { - return (uint)(id - Id - 1); - } + public uint AsSdlId(nint id) => (uint)(id - Id - 1); + + private ulong _previousTimestamp = ulong.MinValue; private unsafe void ReleaseUnmanagedResources() { @@ -449,5 +465,11 @@ private class EventQueue { private readonly Queue _events = new(1024); public void Add(ref Event p0) => _events.Enqueue(p0); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryDequeue(out Event p0) => _events.TryDequeue(out p0); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear() => _events.Clear(); } } diff --git a/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs b/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs index 645adc9515..cec0fe36cc 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs @@ -1,16 +1,28 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Silk.NET.SDL; + namespace Silk.NET.Input.SDL3; internal class SdlJoystick : SdlDevice, IJoystick, ISdlDevice { + private readonly JoystickHandle _joystickHandle; + private readonly JoystickType _joystickType; public SdlJoystick(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDeviceId, backend) { + _joystickHandle = backend.Sdl.OpenJoystick(sdlDeviceId); + _joystickType = backend.Sdl.GetJoystickType(_joystickHandle); + if (_joystickType == JoystickType.Gamepad) + { + throw new Exception("Joystick should have been created as a gamepad, not a joystick."); + } } + public override string Name => Backend.Sdl.GetJoystickNameForID(SdlDeviceId).ReadToString(); + public override void Release() => Backend.Sdl.CloseJoystick(_joystickHandle); - public static SdlJoystick CreateDevice(SdlInputBackend backend, uint sdlDeviceId) => throw new NotImplementedException(); + public static SdlJoystick CreateDevice(SdlInputBackend backend, uint sdlDeviceId) => new(sdlDeviceId, backend); public JoystickState State => throw new NotImplementedException(); } diff --git a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs index ffd78ff69f..edb69b945c 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs @@ -8,13 +8,22 @@ namespace Silk.NET.Input.SDL3; internal class SdlKeyboard : SdlDevice, IKeyboard, ISdlDevice { - public SdlKeyboard(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDeviceId, backend) + private readonly List> _keyStates; + public unsafe SdlKeyboard(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDeviceId, backend) { + _keyStates = new List>((int)Scancode.ScancodeCount); + for (var i = 0; i < 512; i++) + { + _keyStates.Add(new Button((KeyName)i, false, 0f)); + } + + State = new KeyboardState(_keyStates, () => false, () => false);// todo : how do i get the num lock/capslock? } public static SdlKeyboard CreateDevice(SdlInputBackend backend, uint sdlDeviceId) => throw new NotImplementedException(); - public KeyboardState State { get; } = new (); + public KeyboardState State { get; } + public override void Release() {} // empty? public override string Name => Backend.Sdl.GetKeyboardNameForID(SdlDeviceId).ReadToString(); public string? ClipboardText @@ -36,4 +45,10 @@ public bool TryGetKeyName(KeyName key, [NotNullWhen(true)] out string? name) => public void BeginInput() => throw new NotImplementedException(); public string? EndInput() => throw new NotImplementedException(); + + public void AddKeyEvent(EventType type, KeyboardEvent key) + { + const float fraction = 1f / 255f; + _keyStates[(int)key.Key] = new Button((KeyName)key.Key, key.Down != 0, key.Down * fraction); + } } diff --git a/sources/Input/Input/KeyboardState.cs b/sources/Input/Input/KeyboardState.cs index a9e28ac478..ed3778f660 100644 --- a/sources/Input/Input/KeyboardState.cs +++ b/sources/Input/Input/KeyboardState.cs @@ -19,5 +19,59 @@ public class KeyboardState /// /// Gets the active modifier keys. /// - public KeyModifiers Modifiers { get; } -} \ No newline at end of file + public KeyModifiers Modifiers + { + get + { + var state = KeyModifiers.None; + if(Keys[KeyName.ControlLeft]) + state |= KeyModifiers.ControlLeft; + + if(Keys[KeyName.ControlRight]) + state |= KeyModifiers.ControlRight; + + if(Keys[KeyName.AltLeft]) + state |= KeyModifiers.AltLeft; + + if(Keys[KeyName.AltRight]) + state |= KeyModifiers.AltRight; + + if(Keys[KeyName.ShiftLeft]) + state |= KeyModifiers.ShiftLeft; + + if(Keys[KeyName.ShiftRight]) + state |= KeyModifiers.ShiftRight; + + if(Keys[KeyName.SuperLeft]) + state |= KeyModifiers.SuperLeft; + + if(Keys[KeyName.SuperRight]) + state |= KeyModifiers.SuperRight; + + if(_capsLockActive()) + state |= KeyModifiers.CapsLock; + + if(_numLockActive()) + state |= KeyModifiers.NumLock; + + return state; + } + } + + /// + /// Constructor for the keyboard state - the provided button list should be continuously updated by the + /// implementation + /// + /// The collection of keys that are modified at runtime to give the current keyboard its state + /// Return true if caps lock is currently active, irrespective of pressed status + /// Return true if num lock is currently active, irrespective of pressed status + public KeyboardState(List> keys, Func capsLockActive, Func numLockActive) + { + Keys = new(keys); + _capsLockActive = capsLockActive; + _numLockActive = numLockActive; + } + + private readonly Func _numLockActive; + private readonly Func _capsLockActive; +} From e72bf554802e267d87bcda6b09821ed5e3802e33 Mon Sep 17 00:00:00 2001 From: dom Date: Fri, 4 Jul 2025 15:49:13 -0400 Subject: [PATCH 11/39] comments/clarity + enum helper class --- .../Input/Input/Implementations/EnumInfo.cs | 87 +++++++++++++++++++ .../Input/Implementations/SDL3/SdlDevice.cs | 2 +- .../Implementations/SDL3/SdlInputBackend.cs | 40 +++++---- 3 files changed, 111 insertions(+), 18 deletions(-) create mode 100644 sources/Input/Input/Implementations/EnumInfo.cs diff --git a/sources/Input/Input/Implementations/EnumInfo.cs b/sources/Input/Input/Implementations/EnumInfo.cs new file mode 100644 index 0000000000..d25fe87a47 --- /dev/null +++ b/sources/Input/Input/Implementations/EnumInfo.cs @@ -0,0 +1,87 @@ +// 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; + +// ReSharper disable StaticMemberInGenericType +// ^ that's the point +internal static class EnumInfo where T : unmanaged, Enum +{ + public static readonly T[] Values = Enum.GetValues(); + public static readonly int Count = Values.Length; + public static readonly T MaxValue; + public static readonly T MinValue; + + static EnumInfo() + { + if (typeof(T).CustomAttributes.Any(x => x.AttributeType == typeof(FlagsAttribute))) + { + throw new InvalidOperationException("Flags enums are not supported."); + } + + var underlyingType = typeof(T).GetEnumUnderlyingType(); + if (underlyingType == typeof(int)) + { + GetMinMaxValues(out MinValue, out MaxValue); + } + else if (underlyingType == typeof(uint)) + { + GetMinMaxValues(out MinValue, out MaxValue); + } + else if (underlyingType == typeof(byte)) + { + GetMinMaxValues(out MinValue, out MaxValue); + } + else if (underlyingType == typeof(sbyte)) + { + GetMinMaxValues(out MinValue, out MaxValue); + } + else if (underlyingType == typeof(short)) + { + GetMinMaxValues(out MinValue, out MaxValue); + } + else if (underlyingType == typeof(ushort)) + { + GetMinMaxValues(out MinValue, out MaxValue); + } + else if (underlyingType == typeof(long)) + { + GetMinMaxValues(out MinValue, out MaxValue); + } + else if (underlyingType == typeof(ulong)) + { + GetMinMaxValues(out MinValue, out MaxValue); + } + else + { + throw new InvalidOperationException("Enum provided uses an unknown numeric base??"); + } + } + + private static unsafe void GetMinMaxValues(out TEnum minT, out TEnum maxT) + where TEnum : unmanaged, Enum + where TNumber : unmanaged, IMinMaxValue, INumber + { +#if DEBUG + if (typeof(TEnum).GetEnumUnderlyingType() != typeof(TNumber)) + { + throw new InvalidOperationException("Type mismatch"); + } +#endif + + var maxValue = TNumber.MinValue; + var minValue = TNumber.MaxValue; + for (int i = 0; i < Values.Length; i++) + { + var value = Values[i]; + var asNumber = *(TNumber*)&value; + maxValue = asNumber > maxValue ? asNumber : maxValue; + minValue = asNumber < minValue ? asNumber : minValue; + } + + maxT = *(TEnum*)&maxValue; + minT = *(TEnum*)&minValue; + } +} diff --git a/sources/Input/Input/Implementations/SDL3/SdlDevice.cs b/sources/Input/Input/Implementations/SDL3/SdlDevice.cs index 4d25c4ce03..017571bf3c 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlDevice.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlDevice.cs @@ -10,7 +10,7 @@ namespace Silk.NET.Input.SDL3; /// /// A base class for all SDL input devices. /// -internal abstract unsafe class SdlDevice : IInputDevice +internal abstract class SdlDevice : IInputDevice { bool IEquatable.Equals(IInputDevice? other) => other?.GetType() == GetType() diff --git a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs index 92d67502ee..c70000047e 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs @@ -249,7 +249,7 @@ private void ProcessEvent(ref Event arg1, IInputHandler handler) case EventType.GamepadAdded: { - var id = arg1.Kdevice.Which; + var id = arg1.Gdevice.Which; Debug.Assert(_devices.All(x => x.Id != AsSilkId(id))); _ = GetOrCreateDevice(id); break; @@ -269,6 +269,8 @@ private void ProcessEvent(ref Event arg1, IInputHandler handler) break; // Input events ---------------------------------------------------------- + + // keyboard case EventType.KeyDown: { var keyboard = GetOrCreateDevice(arg1.Key.Which); @@ -282,6 +284,7 @@ private void ProcessEvent(ref Event arg1, IInputHandler handler) break; } + // Joystick case EventType.JoystickAxisMotion: break; case EventType.JoystickBallMotion: @@ -315,12 +318,13 @@ private void ProcessEvent(ref Event arg1, IInputHandler handler) case EventType.GamepadSteamHandleUpdated: break; + // sensor? for what? case EventType.SensorUpdate: break; - // ----- Pointer events + // mouse case EventType.MouseMotion: break; case EventType.MouseButtonDown: @@ -330,6 +334,7 @@ private void ProcessEvent(ref Event arg1, IInputHandler handler) case EventType.MouseWheel: break; + // touch case EventType.FingerDown: break; case EventType.FingerUp: @@ -339,6 +344,7 @@ private void ProcessEvent(ref Event arg1, IInputHandler handler) case EventType.FingerCanceled: break; + // pen case EventType.PenProximityIn: break; case EventType.PenProximityOut: @@ -381,6 +387,7 @@ private void ProcessEvent(ref Event arg1, IInputHandler handler) } // Text input events ------------------------------------------- + // todo: attribute this to a keyboard device? or something else? case EventType.TextEditing: break; case EventType.TextInput: @@ -389,25 +396,10 @@ private void ProcessEvent(ref Event arg1, IInputHandler handler) break; case EventType.ClipboardUpdate: break; - - } return; - void RemoveDevice(uint id) - { - var silkId = AsSilkId(id); - var deviceIdx = _devices.FindIndex(x => x.Id == silkId); - - if (deviceIdx == -1) - return; // we never used this device to begin with, so just ignore its removal - - var device = _devices[deviceIdx]; - device.Release(); - _devices.RemoveAt(deviceIdx); - } - T GetOrCreateDevice(uint id) where T : SdlDevice, ISdlDevice { // If we already have a device with this ID, return it. @@ -426,6 +418,20 @@ T GetOrCreateDevice(uint id) where T : SdlDevice, ISdlDevice } } + internal bool RemoveDevice(uint id) + { + var silkId = AsSilkId(id); + var deviceIdx = _devices.FindIndex(x => x.Id == silkId); + + if (deviceIdx == -1) + return false; // we never used this device to begin with, so just ignore its removal + + var device = _devices[deviceIdx]; + device.Release(); + _devices.RemoveAt(deviceIdx); + return true; + } + /// /// Turns an sdl device id into a universally unique Silk.NET input id. /// From 3e4e168b23698f66589c4d0840d01efef406a925 Mon Sep 17 00:00:00 2001 From: dom Date: Fri, 4 Jul 2025 16:00:42 -0400 Subject: [PATCH 12/39] simplify enum assistant and provide guaranteed sorted enum values --- .../Input/Input/Implementations/EnumInfo.cs | 55 ++++++++----------- 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/sources/Input/Input/Implementations/EnumInfo.cs b/sources/Input/Input/Implementations/EnumInfo.cs index d25fe87a47..d8b7556700 100644 --- a/sources/Input/Input/Implementations/EnumInfo.cs +++ b/sources/Input/Input/Implementations/EnumInfo.cs @@ -9,8 +9,8 @@ namespace Silk.NET.Input; // ^ that's the point internal static class EnumInfo where T : unmanaged, Enum { - public static readonly T[] Values = Enum.GetValues(); - public static readonly int Count = Values.Length; + public static readonly IReadOnlyList Values; + public static readonly int Count; public static readonly T MaxValue; public static readonly T MinValue; @@ -24,64 +24,55 @@ static EnumInfo() var underlyingType = typeof(T).GetEnumUnderlyingType(); if (underlyingType == typeof(int)) { - GetMinMaxValues(out MinValue, out MaxValue); + Values = OrderedValues(); } else if (underlyingType == typeof(uint)) { - GetMinMaxValues(out MinValue, out MaxValue); + Values = OrderedValues(); } else if (underlyingType == typeof(byte)) { - GetMinMaxValues(out MinValue, out MaxValue); + Values = OrderedValues(); } else if (underlyingType == typeof(sbyte)) { - GetMinMaxValues(out MinValue, out MaxValue); + Values = OrderedValues(); } else if (underlyingType == typeof(short)) { - GetMinMaxValues(out MinValue, out MaxValue); + Values = OrderedValues(); } else if (underlyingType == typeof(ushort)) { - GetMinMaxValues(out MinValue, out MaxValue); + Values = OrderedValues(); } else if (underlyingType == typeof(long)) { - GetMinMaxValues(out MinValue, out MaxValue); + Values = OrderedValues(); } else if (underlyingType == typeof(ulong)) { - GetMinMaxValues(out MinValue, out MaxValue); + Values = OrderedValues(); } else { throw new InvalidOperationException("Enum provided uses an unknown numeric base??"); } + + Count = Values.Count; + MinValue = Values[0]; + MaxValue = Values[^1]; } - private static unsafe void GetMinMaxValues(out TEnum minT, out TEnum maxT) - where TEnum : unmanaged, Enum - where TNumber : unmanaged, IMinMaxValue, INumber + private static unsafe T[] OrderedValues() where TNumber : unmanaged, IComparable { -#if DEBUG - if (typeof(TEnum).GetEnumUnderlyingType() != typeof(TNumber)) - { - throw new InvalidOperationException("Type mismatch"); - } -#endif - - var maxValue = TNumber.MinValue; - var minValue = TNumber.MaxValue; - for (int i = 0; i < Values.Length; i++) - { - var value = Values[i]; - var asNumber = *(TNumber*)&value; - maxValue = asNumber > maxValue ? asNumber : maxValue; - minValue = asNumber < minValue ? asNumber : minValue; - } - - maxT = *(TEnum*)&maxValue; - minT = *(TEnum*)&minValue; + var allValues = Enum.GetValues(); + // sort with a lambda expression + Array.Sort(allValues, (a, b) => { + var aNumber = *(TNumber*)&a; + var bNumber = *(TNumber*)&b; + return aNumber.CompareTo(bNumber); + }); + return allValues; } } From bce713600b0fb4397ac6b11750475e5fb3362f71 Mon Sep 17 00:00:00 2001 From: dom Date: Fri, 4 Jul 2025 16:07:16 -0400 Subject: [PATCH 13/39] simpler indexing --- .../Input/Input/Implementations/EnumInfo.cs | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/sources/Input/Input/Implementations/EnumInfo.cs b/sources/Input/Input/Implementations/EnumInfo.cs index d8b7556700..556515e5f5 100644 --- a/sources/Input/Input/Implementations/EnumInfo.cs +++ b/sources/Input/Input/Implementations/EnumInfo.cs @@ -9,10 +9,11 @@ namespace Silk.NET.Input; // ^ that's the point internal static class EnumInfo where T : unmanaged, Enum { - public static readonly IReadOnlyList Values; + public static IReadOnlyList Values => _values; public static readonly int Count; public static readonly T MaxValue; public static readonly T MinValue; + private static readonly T[] _values; static EnumInfo() { @@ -24,35 +25,35 @@ static EnumInfo() var underlyingType = typeof(T).GetEnumUnderlyingType(); if (underlyingType == typeof(int)) { - Values = OrderedValues(); + _values = OrderedValues(); } else if (underlyingType == typeof(uint)) { - Values = OrderedValues(); + _values = OrderedValues(); } else if (underlyingType == typeof(byte)) { - Values = OrderedValues(); + _values = OrderedValues(); } else if (underlyingType == typeof(sbyte)) { - Values = OrderedValues(); + _values = OrderedValues(); } else if (underlyingType == typeof(short)) { - Values = OrderedValues(); + _values = OrderedValues(); } else if (underlyingType == typeof(ushort)) { - Values = OrderedValues(); + _values = OrderedValues(); } else if (underlyingType == typeof(long)) { - Values = OrderedValues(); + _values = OrderedValues(); } else if (underlyingType == typeof(ulong)) { - Values = OrderedValues(); + _values = OrderedValues(); } else { @@ -64,6 +65,8 @@ static EnumInfo() MaxValue = Values[^1]; } + public static int IndexOf(T value) => Array.IndexOf(_values, value); + private static unsafe T[] OrderedValues() where TNumber : unmanaged, IComparable { var allValues = Enum.GetValues(); From 8f00a4bd50ffa5f5b9947b57f96160d3b58c296a Mon Sep 17 00:00:00 2001 From: dom Date: Fri, 4 Jul 2025 16:38:18 -0400 Subject: [PATCH 14/39] add support for enums with multiple names for the same value --- sources/Input/Input/ButtonReadOnlyList.cs | 2 +- .../Input/Input/Implementations/EnumInfo.cs | 106 +++++++++++++++--- 2 files changed, 92 insertions(+), 16 deletions(-) diff --git a/sources/Input/Input/ButtonReadOnlyList.cs b/sources/Input/Input/ButtonReadOnlyList.cs index f93504197c..6924969796 100644 --- a/sources/Input/Input/ButtonReadOnlyList.cs +++ b/sources/Input/Input/ButtonReadOnlyList.cs @@ -28,7 +28,7 @@ public ButtonReadOnlyList(IReadOnlyList> other) => /// Gets the state for the button with the given name. /// /// The button name. - public Button this[T name] => InputMarshal.GetButtonState(_list, name); + public Button this[T name] => _list[EnumInfo.ValueIndexOf(name)]; /// public IEnumerator> GetEnumerator() => _list.GetEnumerator(); diff --git a/sources/Input/Input/Implementations/EnumInfo.cs b/sources/Input/Input/Implementations/EnumInfo.cs index 556515e5f5..547f195008 100644 --- a/sources/Input/Input/Implementations/EnumInfo.cs +++ b/sources/Input/Input/Implementations/EnumInfo.cs @@ -1,19 +1,31 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Numerics; +using System.Runtime.CompilerServices; namespace Silk.NET.Input; // ReSharper disable StaticMemberInGenericType // ^ that's the point +/// +/// A helper class for quickly converting enum values into indexes +/// +/// internal static class EnumInfo where T : unmanaged, Enum { public static IReadOnlyList Values => _values; - public static readonly int Count; + public static int NameCount => _values.Length; + public static int ValueCount => _numericallyDistinctValues.Length; + public static readonly T MaxValue; public static readonly T MinValue; + private static readonly T[] _values; + private static readonly T[] _numericallyDistinctValues; + + private static readonly int _elementSize = Unsafe.SizeOf(); + + public static readonly Type UnderlyingType = typeof(T).GetEnumUnderlyingType(); static EnumInfo() { @@ -21,61 +33,125 @@ static EnumInfo() { throw new InvalidOperationException("Flags enums are not supported."); } - - var underlyingType = typeof(T).GetEnumUnderlyingType(); +#if DEBUG + if (_elementSize != 1 || _elementSize != 2 || _elementSize != 4 || _elementSize != 8) + { + throw new Exception("Enum provided uses an unknown numeric base??"); + } +#endif + var underlyingType = UnderlyingType; if (underlyingType == typeof(int)) { - _values = OrderedValues(); + _values = OrderedValues(false); + _numericallyDistinctValues = OrderedValues(true); } else if (underlyingType == typeof(uint)) { - _values = OrderedValues(); + _values = OrderedValues(false); + _numericallyDistinctValues = OrderedValues(true); } else if (underlyingType == typeof(byte)) { - _values = OrderedValues(); + _values = OrderedValues(false); + _numericallyDistinctValues = OrderedValues(true); } else if (underlyingType == typeof(sbyte)) { - _values = OrderedValues(); + _values = OrderedValues(false); + _numericallyDistinctValues = OrderedValues(true); } else if (underlyingType == typeof(short)) { - _values = OrderedValues(); + _values = OrderedValues(false); + _numericallyDistinctValues = OrderedValues(true); } else if (underlyingType == typeof(ushort)) { - _values = OrderedValues(); + _values = OrderedValues(false); + _numericallyDistinctValues = OrderedValues(true); } else if (underlyingType == typeof(long)) { - _values = OrderedValues(); + _values = OrderedValues(false); + _numericallyDistinctValues = OrderedValues(true); } else if (underlyingType == typeof(ulong)) { - _values = OrderedValues(); + _values = OrderedValues(false); + _numericallyDistinctValues = OrderedValues(true); } else { throw new InvalidOperationException("Enum provided uses an unknown numeric base??"); } - Count = Values.Count; MinValue = Values[0]; MaxValue = Values[^1]; } - public static int IndexOf(T value) => Array.IndexOf(_values, value); + /// + /// Get the ordered index of the value provided. + /// Values with the same numerical value will *not* return the same index, and are not guaranteed to be + /// stably sorted across application runs. + /// + /// + /// + public static int NameIndexOf(T value) => Array.IndexOf(_values, value); - private static unsafe T[] OrderedValues() where TNumber : unmanaged, IComparable + /// + /// Get the ordered index of the value provided. + /// Values with the same numerical value will return the same index + /// + /// + /// + /// + public static unsafe int ValueIndexOf(T value) { + switch (_elementSize) + { + case 1: + { + var values = Unsafe.As(_numericallyDistinctValues); + return Array.IndexOf(values, *(byte*)&value); + } + case 2: + { + var values = Unsafe.As(_numericallyDistinctValues); + return Array.IndexOf(values, *(ushort*)&value); + } + case 4: + { + var values = Unsafe.As(_numericallyDistinctValues); + return Array.IndexOf(values, *(uint*)&value); + } + case 8: + { + var values = Unsafe.As(_numericallyDistinctValues); + return Array.IndexOf(values, *(ulong*)&value); + } + default: + throw new InvalidOperationException("Enum provided uses an unknown numeric base??"); + } + } + + private static unsafe T[] OrderedValues(bool byNumericValue) + where TNumber : unmanaged, IComparable + { + // numerically distinct numbers var allValues = Enum.GetValues(); + + if (byNumericValue) + { + allValues = allValues.DistinctBy(x => *(TNumber*)&x).ToArray(); + } + // sort with a lambda expression Array.Sort(allValues, (a, b) => { var aNumber = *(TNumber*)&a; var bNumber = *(TNumber*)&b; return aNumber.CompareTo(bNumber); }); + return allValues; } } From bf382bcb8b2ab600c96f548212f01b2a734308ec Mon Sep 17 00:00:00 2001 From: dom Date: Sat, 5 Jul 2025 11:39:30 -0400 Subject: [PATCH 15/39] optimize enum performance for larger enums --- .../Input/Input/Implementations/EnumInfo.cs | 72 +++++++------------ 1 file changed, 24 insertions(+), 48 deletions(-) diff --git a/sources/Input/Input/Implementations/EnumInfo.cs b/sources/Input/Input/Implementations/EnumInfo.cs index 547f195008..0f897102eb 100644 --- a/sources/Input/Input/Implementations/EnumInfo.cs +++ b/sources/Input/Input/Implementations/EnumInfo.cs @@ -15,15 +15,13 @@ internal static class EnumInfo where T : unmanaged, Enum { public static IReadOnlyList Values => _values; public static int NameCount => _values.Length; - public static int ValueCount => _numericallyDistinctValues.Length; + public static int ValueCount => _numericallyDistinctValues.Count; public static readonly T MaxValue; public static readonly T MinValue; private static readonly T[] _values; - private static readonly T[] _numericallyDistinctValues; - - private static readonly int _elementSize = Unsafe.SizeOf(); + private static readonly Dictionary _numericallyDistinctValues; public static readonly Type UnderlyingType = typeof(T).GetEnumUnderlyingType(); @@ -33,58 +31,61 @@ static EnumInfo() { throw new InvalidOperationException("Flags enums are not supported."); } -#if DEBUG - if (_elementSize != 1 || _elementSize != 2 || _elementSize != 4 || _elementSize != 8) - { - throw new Exception("Enum provided uses an unknown numeric base??"); - } -#endif + var underlyingType = UnderlyingType; + T[] vals; if (underlyingType == typeof(int)) { _values = OrderedValues(false); - _numericallyDistinctValues = OrderedValues(true); + vals = OrderedValues(true); } else if (underlyingType == typeof(uint)) { _values = OrderedValues(false); - _numericallyDistinctValues = OrderedValues(true); + vals = OrderedValues(true); } else if (underlyingType == typeof(byte)) { _values = OrderedValues(false); - _numericallyDistinctValues = OrderedValues(true); + vals = OrderedValues(true); } else if (underlyingType == typeof(sbyte)) { _values = OrderedValues(false); - _numericallyDistinctValues = OrderedValues(true); + vals = OrderedValues(true); } else if (underlyingType == typeof(short)) { _values = OrderedValues(false); - _numericallyDistinctValues = OrderedValues(true); + vals = OrderedValues(true); } else if (underlyingType == typeof(ushort)) { _values = OrderedValues(false); - _numericallyDistinctValues = OrderedValues(true); + vals = OrderedValues(true); } else if (underlyingType == typeof(long)) { _values = OrderedValues(false); - _numericallyDistinctValues = OrderedValues(true); + vals = OrderedValues(true); } else if (underlyingType == typeof(ulong)) { _values = OrderedValues(false); - _numericallyDistinctValues = OrderedValues(true); + vals = OrderedValues(true); } else { throw new InvalidOperationException("Enum provided uses an unknown numeric base??"); } + var dict = new Dictionary(vals.Length); + for (int i = 0; i < vals.Length; i++) + { + dict.Add(vals[i], i); + } + + _numericallyDistinctValues = dict; MinValue = Values[0]; MaxValue = Values[^1]; } @@ -95,7 +96,8 @@ static EnumInfo() /// stably sorted across application runs. /// /// - /// + /// The index of the sorted enum value + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int NameIndexOf(T value) => Array.IndexOf(_values, value); /// @@ -103,36 +105,10 @@ static EnumInfo() /// Values with the same numerical value will return the same index /// /// - /// + /// The index of the sorted enum numerical value /// - public static unsafe int ValueIndexOf(T value) - { - switch (_elementSize) - { - case 1: - { - var values = Unsafe.As(_numericallyDistinctValues); - return Array.IndexOf(values, *(byte*)&value); - } - case 2: - { - var values = Unsafe.As(_numericallyDistinctValues); - return Array.IndexOf(values, *(ushort*)&value); - } - case 4: - { - var values = Unsafe.As(_numericallyDistinctValues); - return Array.IndexOf(values, *(uint*)&value); - } - case 8: - { - var values = Unsafe.As(_numericallyDistinctValues); - return Array.IndexOf(values, *(ulong*)&value); - } - default: - throw new InvalidOperationException("Enum provided uses an unknown numeric base??"); - } - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ValueIndexOf(T value) => _numericallyDistinctValues[value]; private static unsafe T[] OrderedValues(bool byNumericValue) where TNumber : unmanaged, IComparable From 555275b94f27fd4d1c0fa05267f9f35f0ecec24f Mon Sep 17 00:00:00 2001 From: dom Date: Sat, 5 Jul 2025 11:41:28 -0400 Subject: [PATCH 16/39] clarifying comment --- sources/Input/Input/Implementations/EnumInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sources/Input/Input/Implementations/EnumInfo.cs b/sources/Input/Input/Implementations/EnumInfo.cs index 0f897102eb..ad0a6be397 100644 --- a/sources/Input/Input/Implementations/EnumInfo.cs +++ b/sources/Input/Input/Implementations/EnumInfo.cs @@ -121,7 +121,7 @@ private static unsafe T[] OrderedValues(bool byNumericValue) allValues = allValues.DistinctBy(x => *(TNumber*)&x).ToArray(); } - // sort with a lambda expression + // sort by increasing order Array.Sort(allValues, (a, b) => { var aNumber = *(TNumber*)&a; var bNumber = *(TNumber*)&b; From fb889c6ad07346d0462e110f02d9c393a0e64913 Mon Sep 17 00:00:00 2001 From: dom Date: Sat, 5 Jul 2025 13:07:08 -0400 Subject: [PATCH 17/39] documentation/cleanup enum class --- .../Input/Input/Implementations/EnumInfo.cs | 76 ++++++++++++++----- 1 file changed, 58 insertions(+), 18 deletions(-) diff --git a/sources/Input/Input/Implementations/EnumInfo.cs b/sources/Input/Input/Implementations/EnumInfo.cs index ad0a6be397..cdbcd00040 100644 --- a/sources/Input/Input/Implementations/EnumInfo.cs +++ b/sources/Input/Input/Implementations/EnumInfo.cs @@ -13,17 +13,38 @@ namespace Silk.NET.Input; /// internal static class EnumInfo where T : unmanaged, Enum { - public static IReadOnlyList Values => _values; - public static int NameCount => _values.Length; - public static int ValueCount => _numericallyDistinctValues.Count; + /// + /// All enum values sorted in increasing order (unstable sort) + /// + public static IReadOnlyList All => _all; + /// + /// All enum values with distinct numerical values sorted in increasing order. + /// In the case of multiple enum entries with the same numerical value, this makes no guarantees about + /// which version ends up here. + /// + public static IReadOnlyList UniqueValues; + + + /// + /// The value with the highest numerical value + /// public static readonly T MaxValue; + + /// + /// The value with the lowest numerical value + /// public static readonly T MinValue; - private static readonly T[] _values; + /// + /// The numerical type of the enum + /// + public static readonly Type UnderlyingType = typeof(T).GetEnumUnderlyingType(); + + private static readonly T[] _all; + private static readonly string[] _names; private static readonly Dictionary _numericallyDistinctValues; - public static readonly Type UnderlyingType = typeof(T).GetEnumUnderlyingType(); static EnumInfo() { @@ -34,44 +55,45 @@ static EnumInfo() var underlyingType = UnderlyingType; T[] vals; + T[] all; if (underlyingType == typeof(int)) { - _values = OrderedValues(false); - vals = OrderedValues(true); + all = OrderedValues(false); + vals = OrderedValues(true); } else if (underlyingType == typeof(uint)) { - _values = OrderedValues(false); + all = OrderedValues(false); vals = OrderedValues(true); } else if (underlyingType == typeof(byte)) { - _values = OrderedValues(false); + all = OrderedValues(false); vals = OrderedValues(true); } else if (underlyingType == typeof(sbyte)) { - _values = OrderedValues(false); + all = OrderedValues(false); vals = OrderedValues(true); } else if (underlyingType == typeof(short)) { - _values = OrderedValues(false); + all = OrderedValues(false); vals = OrderedValues(true); } else if (underlyingType == typeof(ushort)) { - _values = OrderedValues(false); + all = OrderedValues(false); vals = OrderedValues(true); } else if (underlyingType == typeof(long)) { - _values = OrderedValues(false); + all = OrderedValues(false); vals = OrderedValues(true); } else if (underlyingType == typeof(ulong)) { - _values = OrderedValues(false); + all = OrderedValues(false); vals = OrderedValues(true); } else @@ -79,26 +101,44 @@ static EnumInfo() throw new InvalidOperationException("Enum provided uses an unknown numeric base??"); } + + var names = new string[all.Length]; + for (var index = 0; index < all.Length; index++) + { + names[index] = all[index].ToString(); // todo: readable name attributes? + } + var dict = new Dictionary(vals.Length); - for (int i = 0; i < vals.Length; i++) + for (var i = 0; i < vals.Length; i++) { dict.Add(vals[i], i); } + _names = names; + _all = all; + UniqueValues = vals; _numericallyDistinctValues = dict; - MinValue = Values[0]; - MaxValue = Values[^1]; + MinValue = All[0]; + MaxValue = All[^1]; } /// /// Get the ordered index of the value provided. /// Values with the same numerical value will *not* return the same index, and are not guaranteed to be /// stably sorted across application runs. + /// The index provided /// /// /// The index of the sorted enum value [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int NameIndexOf(T value) => Array.IndexOf(_values, value); + public static int NameIndexOf(T value) => Array.IndexOf(_all, value); + + /// + + /// + /// Returns the names of an enum value, pre-allocated + /// + public static string NameOf(T value) => _names[NameIndexOf(value)]; /// /// Get the ordered index of the value provided. From 021a8c9fc933c4456cb1493371ce878fae553e5e Mon Sep 17 00:00:00 2001 From: dom Date: Sun, 6 Jul 2025 11:10:14 -0400 Subject: [PATCH 18/39] begin constructing gamepads via the joystick API --- sources/Input/Input/ButtonReadOnlyList.cs | 22 +- .../Input/Input/Implementations/EnumInfo.cs | 26 +- .../SDL3/Pointers/SdlBoundedPointerDevice.cs | 4 +- .../Implementations/SDL3/Pointers/SdlPen.cs | 5 +- .../SDL3/Pointers/SdlSharedMouse.cs | 6 +- .../SDL3/Pointers/SdlTouchScreen.cs | 2 +- .../SDL3/Pointers/SdlUnboundedMouse.cs | 4 +- .../Input/Implementations/SDL3/SdlDevice.cs | 30 +- .../Input/Implementations/SDL3/SdlGamepad.cs | 104 ++---- .../Implementations/SDL3/SdlInputBackend.cs | 11 +- .../Input/Implementations/SDL3/SdlJoystick.cs | 342 +++++++++++++++++- .../Input/Implementations/SDL3/SdlKeyboard.cs | 4 +- sources/Input/Input/InputMarshal.cs | 5 +- sources/Input/Input/JoystickButton.cs | 20 +- 14 files changed, 459 insertions(+), 126 deletions(-) diff --git a/sources/Input/Input/ButtonReadOnlyList.cs b/sources/Input/Input/ButtonReadOnlyList.cs index 6924969796..ecd579a3c9 100644 --- a/sources/Input/Input/ButtonReadOnlyList.cs +++ b/sources/Input/Input/ButtonReadOnlyList.cs @@ -10,25 +10,23 @@ namespace Silk.NET.Input; /// /// The button type (e.g. , , etc). /// -public struct ButtonReadOnlyList : IReadOnlyList> +public readonly struct ButtonReadOnlyList : IReadOnlyList> where T : unmanaged, Enum { - private InputReadOnlyList> _list; + private readonly int _count; + private readonly Func _getIndexFunc; + private readonly Func _indexMap; - internal ButtonReadOnlyList(InputReadOnlyList> list) => _list = list; - - /// - /// Creates an from a . - /// - /// The list to copy. - public ButtonReadOnlyList(IReadOnlyList> other) => - InputMarshal.Clone(other).List.AsButtonList(); + public ButtonReadOnlyList(InputReadOnlyList> getIndexFunc) + { + throw new NotImplementedException(); + } /// /// Gets the state for the button with the given name. /// /// The button name. - public Button this[T name] => _list[EnumInfo.ValueIndexOf(name)]; + public Button this[T name] => _list[_getIndexFunc(name)]; /// public IEnumerator> GetEnumerator() => _list.GetEnumerator(); @@ -39,5 +37,5 @@ public ButtonReadOnlyList(IReadOnlyList> other) => public int Count => _list.Count; /// - public Button this[int index] => _list[index]; + public Button this[int index] => _list[_indexMap(index)]; } diff --git a/sources/Input/Input/Implementations/EnumInfo.cs b/sources/Input/Input/Implementations/EnumInfo.cs index cdbcd00040..e6480b614d 100644 --- a/sources/Input/Input/Implementations/EnumInfo.cs +++ b/sources/Input/Input/Implementations/EnumInfo.cs @@ -1,6 +1,5 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. - using System.Runtime.CompilerServices; namespace Silk.NET.Input; @@ -23,7 +22,7 @@ internal static class EnumInfo where T : unmanaged, Enum /// In the case of multiple enum entries with the same numerical value, this makes no guarantees about /// which version ends up here. /// - public static IReadOnlyList UniqueValues; + public static readonly IReadOnlyList UniqueValues; /// @@ -44,9 +43,9 @@ internal static class EnumInfo where T : unmanaged, Enum private static readonly T[] _all; private static readonly string[] _names; private static readonly Dictionary _numericallyDistinctValues; + private static readonly ulong[] _allEnumValuesRaw; - - static EnumInfo() + static unsafe EnumInfo() { if (typeof(T).CustomAttributes.Any(x => x.AttributeType == typeof(FlagsAttribute))) { @@ -60,41 +59,49 @@ static EnumInfo() { all = OrderedValues(false); vals = OrderedValues(true); + _allEnumValuesRaw = vals.Select(x => (ulong)*(uint*)&x).ToArray(); } else if (underlyingType == typeof(uint)) { all = OrderedValues(false); vals = OrderedValues(true); + _allEnumValuesRaw = vals.Select(x => (ulong)*(uint*)&x).ToArray(); } else if (underlyingType == typeof(byte)) { all = OrderedValues(false); vals = OrderedValues(true); + _allEnumValuesRaw = vals.Select(x => (ulong)*(byte*)&x).ToArray(); } else if (underlyingType == typeof(sbyte)) { all = OrderedValues(false); vals = OrderedValues(true); + _allEnumValuesRaw = vals.Select(x => (ulong)*(byte*)&x).ToArray(); } else if (underlyingType == typeof(short)) { all = OrderedValues(false); vals = OrderedValues(true); + _allEnumValuesRaw = vals.Select(x => (ulong)*(ushort*)&x).ToArray(); } else if (underlyingType == typeof(ushort)) { all = OrderedValues(false); vals = OrderedValues(true); + _allEnumValuesRaw = vals.Select(x => (ulong)*(ushort*)&x).ToArray(); } else if (underlyingType == typeof(long)) { all = OrderedValues(false); vals = OrderedValues(true); + _allEnumValuesRaw = vals.Select(x => *(ulong*)&x).ToArray(); } else if (underlyingType == typeof(ulong)) { all = OrderedValues(false); vals = OrderedValues(true); + _allEnumValuesRaw = vals.Select(x => *(ulong*)&x).ToArray(); } else { @@ -111,7 +118,8 @@ static EnumInfo() var dict = new Dictionary(vals.Length); for (var i = 0; i < vals.Length; i++) { - dict.Add(vals[i], i); + var enumVal = vals[i]; + dict.Add(enumVal, i); } _names = names; @@ -131,7 +139,7 @@ static EnumInfo() /// /// The index of the sorted enum value [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int NameIndexOf(T value) => Array.IndexOf(_all, value); + private static int NameIndexOf(T value) => Array.IndexOf(_all, value); /// @@ -148,7 +156,7 @@ static EnumInfo() /// The index of the sorted enum numerical value /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int ValueIndexOf(T value) => _numericallyDistinctValues[value]; + public static int ValueIndexOf(T value) => _numericallyDistinctValues.TryGetValue(value, out var index) ? index : -1; private static unsafe T[] OrderedValues(bool byNumericValue) where TNumber : unmanaged, IComparable @@ -170,4 +178,8 @@ private static unsafe T[] OrderedValues(bool byNumericValue) return allValues; } + + public static int ToUnknownIndex(TOther value) + + public static unsafe bool HasValue(int value) => _allEnumValuesRaw.Contains(*(uint*)&value); } diff --git a/sources/Input/Input/Implementations/SDL3/Pointers/SdlBoundedPointerDevice.cs b/sources/Input/Input/Implementations/SDL3/Pointers/SdlBoundedPointerDevice.cs index 52f59143fd..bedbc8740e 100644 --- a/sources/Input/Input/Implementations/SDL3/Pointers/SdlBoundedPointerDevice.cs +++ b/sources/Input/Input/Implementations/SDL3/Pointers/SdlBoundedPointerDevice.cs @@ -18,7 +18,7 @@ protected SdlBoundedPointerDevice(uint sdlDeviceId, SdlInputBackend backend, IRe public abstract PointerState State { get; } - public override string Name => Backend.Sdl.GetMouseNameForID(SdlDeviceId).ReadToString(); + public override string Name => NativeBackend.GetMouseNameForID(SdlDeviceId).ReadToString(); [field: MaybeNull] public virtual IReadOnlyList Targets => @@ -35,7 +35,7 @@ protected SdlBoundedPointerDevice(uint sdlDeviceId, SdlInputBackend backend, IRe public InputMarshal.ListOwner BoundedPoints => field.List.Data is null ? field = InputMarshal.CreateList() : field; - public sealed override void Release() + protected sealed override void Release() { } diff --git a/sources/Input/Input/Implementations/SDL3/Pointers/SdlPen.cs b/sources/Input/Input/Implementations/SDL3/Pointers/SdlPen.cs index 7b37a1357e..58ac83cf87 100644 --- a/sources/Input/Input/Implementations/SDL3/Pointers/SdlPen.cs +++ b/sources/Input/Input/Implementations/SDL3/Pointers/SdlPen.cs @@ -5,7 +5,10 @@ namespace Silk.NET.Input.SDL3.Pointers; internal class SdlPen : SdlBoundedPointerDevice, ISdlDevice { - public static SdlPen CreateDevice(SdlInputBackend backend, uint sdlDeviceId) => throw new NotImplementedException(); + public static SdlPen CreateDevice(SdlInputBackend backend, uint sdlDeviceId) + { + throw new NotImplementedException(); + } public override PointerState State => throw new NotImplementedException(); diff --git a/sources/Input/Input/Implementations/SDL3/Pointers/SdlSharedMouse.cs b/sources/Input/Input/Implementations/SDL3/Pointers/SdlSharedMouse.cs index 9388b32d86..bdcd43e091 100644 --- a/sources/Input/Input/Implementations/SDL3/Pointers/SdlSharedMouse.cs +++ b/sources/Input/Input/Implementations/SDL3/Pointers/SdlSharedMouse.cs @@ -17,7 +17,7 @@ public SdlSharedMouse(uint sdlDeviceId, SdlInputBackend backend, IReadOnlyList, IPointerD public bool Equals(IInputDevice? other) => throw new NotImplementedException(); public override string Name => throw new NotImplementedException(); - public override void Release() => throw new NotImplementedException(); + protected override void Release() => throw new NotImplementedException(); public PointerState State => throw new NotImplementedException(); diff --git a/sources/Input/Input/Implementations/SDL3/Pointers/SdlUnboundedMouse.cs b/sources/Input/Input/Implementations/SDL3/Pointers/SdlUnboundedMouse.cs index a8d761b4fd..4088a83cbd 100644 --- a/sources/Input/Input/Implementations/SDL3/Pointers/SdlUnboundedMouse.cs +++ b/sources/Input/Input/Implementations/SDL3/Pointers/SdlUnboundedMouse.cs @@ -13,7 +13,7 @@ public SdlUnboundedMouse(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDe public MouseState State => throw new NotImplementedException(); - public override string Name => Backend.Sdl.GetMouseNameForID(SdlDeviceId).ReadToString(); + public override string Name => NativeBackend.GetMouseNameForID(SdlDeviceId).ReadToString(); public ICursorConfiguration Cursor => Backend; @@ -21,7 +21,7 @@ public SdlUnboundedMouse(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDe public IReadOnlyList Targets => throw new NotImplementedException(); - public override void Release() + protected override void Release() { // nothing? } diff --git a/sources/Input/Input/Implementations/SDL3/SdlDevice.cs b/sources/Input/Input/Implementations/SDL3/SdlDevice.cs index 017571bf3c..0379d7e4ed 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlDevice.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlDevice.cs @@ -1,27 +1,31 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. - -using System.Runtime.CompilerServices; using Silk.NET.Input.SDL3.Pointers; +using Silk.NET.SDL; namespace Silk.NET.Input.SDL3; /// /// A base class for all SDL input devices. /// -internal abstract class SdlDevice : IInputDevice +internal abstract class SdlDevice : IInputDevice, IDisposable { bool IEquatable.Equals(IInputDevice? other) => other?.GetType() == GetType() && other.Id == Id && other is SdlBoundedPointerDevice dev - && dev.Backend.Sdl == Backend.Sdl; + && dev.NativeBackend == NativeBackend; public nint Id => Backend.AsSilkId(SdlDeviceId); public uint SdlDeviceId { get; } public SdlInputBackend Backend { get; } + /// + /// For readability and refactorability - provides the SDL interface instance. + /// + protected ISdl NativeBackend => Backend.Sdl; + public abstract string Name { get; } /*{ { @@ -39,7 +43,23 @@ protected SdlDevice(uint sdlDeviceId, SdlInputBackend backend) SdlDeviceId = sdlDeviceId; } - public abstract void Release(); + protected abstract void Release(); + + public void Dispose() + { + ObjectDisposedException.ThrowIf(_isDisposed, GetType()); + _isDisposed = true; + Release(); + GC.SuppressFinalize(this); + } + + ~SdlDevice() + { + _isDisposed = true; + Release(); + } + + private bool _isDisposed; } /// diff --git a/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs b/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs index 46c2182cbd..7d843ab374 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs @@ -6,82 +6,41 @@ namespace Silk.NET.Input.SDL3; -internal sealed class SdlGamepad : SdlDevice, IGamepad, IDisposable, ISdlDevice +internal sealed class SdlGenericJoystick : SdlJoystick, ISdlDevice { - private readonly GamepadHandle _gamepad; - - private static JoystickButton? GetSilkButton(GamepadButton btn) => - btn switch - { - GamepadButton.South => JoystickButton.ButtonDown, - GamepadButton.East => JoystickButton.ButtonRight, - GamepadButton.West => JoystickButton.ButtonLeft, - GamepadButton.North => JoystickButton.ButtonUp, - GamepadButton.Back => JoystickButton.Back, - GamepadButton.Guide => JoystickButton.Home, - GamepadButton.Start => JoystickButton.Start, - GamepadButton.LeftStick => JoystickButton.LeftStick, - GamepadButton.RightStick => JoystickButton.RightStick, - GamepadButton.LeftShoulder => JoystickButton.LeftBumper, - GamepadButton.RightShoulder => JoystickButton.RightBumper, - GamepadButton.DpadUp => JoystickButton.DPadUp, - GamepadButton.DpadDown => JoystickButton.DPadDown, - GamepadButton.DpadLeft => JoystickButton.DPadLeft, - GamepadButton.DpadRight => JoystickButton.DPadRight, - // TODO not exposed today - _ => null, - }; - - private SdlGamepad(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDeviceId, backend) + public SdlGenericJoystick(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDeviceId, backend) { - _gamepad = backend.Sdl.OpenGamepad(sdlDeviceId); - if (_gamepad == nullptr) + if (JoystickType == JoystickType.Gamepad) { - backend.Sdl.ThrowError(); + throw new Exception("Joystick should have been created as a gamepad, not a joystick."); } + } + + public static SdlGenericJoystick CreateDevice(SdlInputBackend backend, uint sdlDeviceId) => throw new NotImplementedException(); +} - var buttons = InputMarshal.CreateList>(); - for (var i = 0; i < (int)GamepadButton.Count; i++) +/// +/// provides the IGamepad implementation for a joystick +/// +internal sealed class SdlGamepad : SdlJoystick, IGamepad, ISdlDevice +{ + public SdlGamepad(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDeviceId, backend) + { + var _gamepad = NativeBackend.OpenGamepad(sdlDeviceId); + if (_gamepad == nullptr) { - if (GetSilkButton((GamepadButton)i) is not { } btn) - { - continue; - } - - var isDown = backend.Sdl.GetGamepadButton(_gamepad, (GamepadButton)i); - InputMarshal.SetButtonState( - buttons, - new Button(btn, isDown, isDown ? 1 : 0), - true - ); + NativeBackend.ThrowError(); } - // For thumbsticks, the state is a value ranging from -32768 (up/left) to 32767 (down/right). - // Triggers range from 0 when released to 32767 when fully pressed, and never return a negative value. Note that - // this differs from the value reported by the lower-level SDL_GetJoystickAxis(), which normally uses the full - // range. - var triggers = new DualReadOnlyList( - (float)backend.Sdl.GetGamepadAxis(_gamepad, GamepadAxis.LeftTrigger) / short.MaxValue, - (float)backend.Sdl.GetGamepadAxis(_gamepad, GamepadAxis.RightTrigger) / short.MaxValue - ); - var thumbsticks = new DualReadOnlyList( - new Vector2( - (float)backend.Sdl.GetGamepadAxis(_gamepad, GamepadAxis.Leftx) / short.MaxValue, - (float)backend.Sdl.GetGamepadAxis(_gamepad, GamepadAxis.Lefty) / short.MaxValue - ), - new Vector2( - (float)backend.Sdl.GetGamepadAxis(_gamepad, GamepadAxis.Rightx) / short.MaxValue, - (float)backend.Sdl.GetGamepadAxis(_gamepad, GamepadAxis.Righty) / short.MaxValue - ) - ); - State = new GamepadState(buttons.List.AsButtonList(), thumbsticks, triggers); + _gamepadState = new GamepadState(new ButtonReadOnlyList(Buttons), thumbsticks, triggers); } - public override void Release() => Backend.Sdl.CloseGamepad(_gamepad); + protected override void Release() => NativeBackend.CloseGamepad(_gamepad); - public GamepadState State { get; } + private readonly GamepadState _gamepadState; + GamepadState IGamepad.State => _gamepadState; - public override string Name => Backend.Sdl.GetGamepadNameForID(SdlDeviceId).ReadToString(); + public override string Name => NativeBackend.GetGamepadNameForID(SdlDeviceId).ReadToString(); // TODO this entire API needs to be redesigned as right now this is literally only ever going to be useful if it's // just left or right. The original intention was that this would be useful for things like 3D haptics, but what did @@ -99,7 +58,7 @@ internal void SetRumble(int motor, ushort value) { (_motorFrequencies ??= [0, 0])[motor] = value; if ( - !Backend.Sdl.RumbleGamepad( + !NativeBackend.RumbleGamepad( _gamepad, _motorFrequencies[0], _motorFrequencies[1], @@ -107,22 +66,21 @@ internal void SetRumble(int motor, ushort value) ) ) { - Backend.Sdl.ThrowError(); + NativeBackend.ThrowError(); } } - private void ReleaseUnmanagedResources() => Backend.Sdl.CloseGamepad(_gamepad); + public static SdlGamepad CreateDevice(SdlInputBackend backend, uint sdlDeviceId) => new(sdlDeviceId, backend); + + ~SdlGamepad() => Release(); - public void Dispose() + public void AddButtonEvent(GamepadButtonEvent sdlButton) { - ReleaseUnmanagedResources(); - GC.SuppressFinalize(this); + ProcessButtonEvent(this, sdlButton.Button, sdlButton.Down); } - public static SdlGamepad CreateDevice(SdlInputBackend backend, uint sdlDeviceId) + private static void ProcessButtonEvent(T device, byte sdlButtonId, byte sdlButtonDown) where T : SdlJoystick, ISdlDevice { - throw new NotImplementedException(); } - ~SdlGamepad() => ReleaseUnmanagedResources(); } diff --git a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs index c70000047e..b6855c3c2b 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs @@ -292,18 +292,23 @@ private void ProcessEvent(ref Event arg1, IInputHandler handler) case EventType.JoystickHatMotion: break; case EventType.JoystickButtonDown: - break; case EventType.JoystickButtonUp: + var joystick = GetOrCreateDevice(arg1.Jbutton.Which); + joystick.AddButtonEvent(arg1.Jbutton); break; case EventType.JoystickBatteryUpdated: break; case EventType.JoystickUpdateComplete: break; + + + // Gamepad inputs case EventType.GamepadAxisMotion: break; case EventType.GamepadButtonDown: - break; case EventType.GamepadButtonUp: + var gamepad = GetOrCreateDevice(arg1.Gbutton.Which); + gamepad.AddButtonEvent(arg1.Gbutton); break; case EventType.GamepadTouchpadDown: break; @@ -427,7 +432,7 @@ internal bool RemoveDevice(uint id) return false; // we never used this device to begin with, so just ignore its removal var device = _devices[deviceIdx]; - device.Release(); + device.Dispose(); _devices.RemoveAt(deviceIdx); return true; } diff --git a/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs b/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs index cec0fe36cc..111c640f4a 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs @@ -1,28 +1,350 @@ // 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.Numerics; using Silk.NET.SDL; namespace Silk.NET.Input.SDL3; -internal class SdlJoystick : SdlDevice, IJoystick, ISdlDevice +internal unsafe class SdlJoystick : SdlDevice, IJoystick, IGamepad { private readonly JoystickHandle _joystickHandle; - private readonly JoystickType _joystickType; - public SdlJoystick(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDeviceId, backend) + private protected readonly JoystickType JoystickType; + + private readonly JoystickState _joystickState; + private readonly GamepadState? _gamepadState; + private readonly GamepadHandle? _gamepadHandle; + public readonly bool HasGamepadImplementation; + + public sealed override string Name => NativeBackend.GetJoystickNameForID(SdlDeviceId).ReadToString(); + + protected SdlJoystick(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDeviceId, backend) { - _joystickHandle = backend.Sdl.OpenJoystick(sdlDeviceId); - _joystickType = backend.Sdl.GetJoystickType(_joystickHandle); - if (_joystickType == JoystickType.Gamepad) + var joystickHandle = NativeBackend.OpenJoystick(sdlDeviceId); + + if (joystickHandle.Handle == null) + { + var error = NativeBackend.GetError(); + string? errorStr = null; + if (error.Native != null) + { + errorStr = error.ReadToString(); + NativeBackend.Free(error.Native); + } + + throw new Exception($"Failed to open joystick: {errorStr ?? "Unknown error."}"); + } + + _joystickHandle = joystickHandle; + JoystickType = NativeBackend.GetJoystickType(joystickHandle); + + + var gamepadMapping = NativeBackend.GetGamepadBindings(sdlDeviceId); + if (gamepadMapping.Native != null) + { + NativeBackend.Free(gamepadMapping.Native); + } + + + int bindingsCount = 0; + var gamepadHandle = *(GamepadHandle*)&joystickHandle; //NativeBackend.OpenGamepad(sdlDeviceId); + var mappings = NativeBackend.GetGamepadBindings(gamepadHandle, &bindingsCount); + + if (bindingsCount != 0) + { + HasGamepadImplementation = true; + for (int i = 0; i < bindingsCount; i++) + { + var binding = mappings[i]; + + if (binding->OutputType == GamepadBindingType.None) + { + continue; + } + + int? id = null; + + switch (binding->InputType) + { + case GamepadBindingType.Button: + id = binding->Input.Button << ButtonShift; + break; + case GamepadBindingType.Axis: + id = binding->Input.Axis.Axis << AxisShift; + break; + case GamepadBindingType.Hat: + id = binding->Input.Hat.Hat; + break; + } + + switch (binding->OutputType) + { + case GamepadBindingType.Axis: + break; + case GamepadBindingType.Button: + break; + default: + // todo : throw? this should not be possible according to sdl + break; + } + + if (id != null) + { + if (binding->InputType == GamepadBindingType.Hat) + { + while (_hatBindings.Count <= id.Value) + _hatBindings.Add(null); + + _hatBindings[id.Value] ??= new List(); + _hatBindings[id.Value]!.Add(*binding); + } + else + { + _bindings.Add(id.Value, *binding); + } + } + } + + NativeBackend.Free(mappings); + } + else if (mappings == null) { - throw new Exception("Joystick should have been created as a gamepad, not a joystick."); + var error = NativeBackend.GetError(); + if (error.Native != null) + { + Console.Error.WriteLine(error.ReadToString()); + NativeBackend.Free(error.Native); + } } + + if (_bindings.Count > 0) + { + _gamepadButtonState = new bool[(int)GamepadButton.Count]; + _gamepadAxisState = new float[(int)GamepadAxis.Count]; + } + else + { + _gamepadState = null; + _gamepadButtonState = []; + _gamepadAxisState = []; + } + + // init current joystick state + var buttonCount = NativeBackend.GetNumJoystickButtons(joystickHandle); + for (var i = 0; i < buttonCount; i++) + { + var joystickInput = NativeBackend.GetJoystickButtonRaw(_joystickHandle, i); + UpdateButton(i, joystickInput); + } + + var axisCount = NativeBackend.GetNumJoystickAxes(joystickHandle); + _rawAxisState = new float[axisCount]; + for (int i = 0; i < axisCount; i++) + { + var joystickInput = NativeBackend.GetJoystickAxis(_joystickHandle, i); + if (joystickInput == 0) + { + // this indicates an sdl error, so just set our internal axis to 0 + joystickInput = short.MinValue; + } + + UpdateAxis(i, joystickInput); + } + + var hatCount = NativeBackend.GetNumJoystickHats(joystickHandle); + for (int i = 0; i < hatCount; ++i) + { + var hatInput = NativeBackend.GetJoystickHat(joystickHandle, i); + UpdateHat(i, hatInput); + } + + _joystickState = new JoystickState(_rawAxisState, _rawButtonState, _rawHatState); + _gamepadState = new GamepadState() + } + + [Flags] + private enum HatState : byte + { + Up = (byte)Sdl.HatUp, + Right = (byte)Sdl.HatRight, + Down = (byte)Sdl.HatDown, + Left = (byte)Sdl.HatLeft, + Centered = (byte)Sdl.HatCentered, + LeftUp = (byte)Sdl.HatLeftup, + RightUp = (byte)Sdl.HatRightup, + LeftDown = (byte)Sdl.HatLeftdown, + RightDown = (byte)Sdl.HatRightdown } - public override string Name => Backend.Sdl.GetJoystickNameForID(SdlDeviceId).ReadToString(); - public override void Release() => Backend.Sdl.CloseJoystick(_joystickHandle); - public static SdlJoystick CreateDevice(SdlInputBackend backend, uint sdlDeviceId) => new(sdlDeviceId, backend); + private void UpdateHat(int hatIdx, byte hatInput) + { + var hatState = (HatState)hatInput; + var left = (hatState & HatState.Left) == HatState.Left; + var right = (hatState & HatState.Right) == HatState.Right; + + var x = (float)(*(byte*)&right - *(byte*)&left); + var up = (hatState & HatState.Up) == HatState.Up; + var down = (hatState & HatState.Down) == HatState.Down; + var y = (float)(*(byte*)&up - *(byte*)&down); + + _rawHatState[hatIdx] = new Vector2(x, y); + if (_hatBindings.Count <= hatIdx) + { + return; + } + + var bindings = _hatBindings[index: hatIdx]; + if (bindings is not { Count: > 0 }) + { + return; + } + + foreach (var binding in bindings) + { + Debug.Assert(condition: binding.InputType == GamepadBindingType.Hat && binding.Input.Hat.Hat == hatIdx); + var input = &binding.Input.Hat; + var mask = (HatState)input->HatMask; + var bindingState = hatState & mask; + switch (binding.OutputType) + { + case GamepadBindingType.Axis: + var axis = binding.Output.Axis; + UpdateGamepadAxis( + axis: axis.Axis, + value: bindingState == HatState.Centered ? axis.AxisMin : axis.AxisMax, + min: axis.AxisMin, + max: axis.AxisMax); + break; + case GamepadBindingType.Button: + var button = binding.Output.Button; + UpdateGamepadButton(button, bindingState != HatState.Centered); + break; + } + } + } + + public void UpdateAxis(int axis, short joystickInput) + { + _rawAxisState[axis] = (float)(joystickInput + short.MaxValue) / ushort.MaxValue; + if (!_bindings.TryGetValue(axis << AxisShift, out var binding)) + { + return; + } + + Debug.Assert(binding.InputType == GamepadBindingType.Axis); + + var output = &binding.Output; + + switch (binding.OutputType) + { + case GamepadBindingType.Axis: + UpdateGamepadAxis(output->Axis.Axis, joystickInput, output->Axis.AxisMin, output->Axis.AxisMax ); + break; + case GamepadBindingType.Button: + UpdateGamepadButton(output->Button, joystickInput > 0); // todo : threshold + break; + } + } + + private void UpdateGamepadAxis(GamepadAxis axis, int value, int min, int max) + { + _gamepadAxisState[(int)axis] = (float)(value + min) / (max - min); + } + + private void UpdateGamepadButton(GamepadButton button, bool value) => _gamepadButtonState[(int)button] = value; + + public void UpdateButton(int buttonIdx, byte rawValue) + { + var down = rawValue > 0; + _rawButtonState[buttonIdx] = new Button((JoystickButton)buttonIdx, down, down ? 1 : 0); + + if (!_bindings.TryGetValue(buttonIdx << ButtonShift, out var binding)) + { + return; + } + + Debug.Assert(binding.InputType == GamepadBindingType.Button); + var bindingType = binding.OutputType; + var output = &binding.Output; + switch (bindingType) + { + case GamepadBindingType.Axis: + + var axis = output->Axis; + UpdateGamepadAxis( + axis: axis.Axis, + value: down ? axis.AxisMax : axis.AxisMin, + min: axis.AxisMin, + max: axis.AxisMax); + break; + + case GamepadBindingType.Button: + UpdateGamepadButton(output->Button, down); + break; + default: + // todo: throw? - this should not be possible + break; + } + } + + protected override void Release() + { + if (_gamepadHandle != null) + { + NativeBackend.CloseGamepad(_gamepadHandle.Value); + } + + NativeBackend.CloseJoystick(_joystickHandle); + } public JoystickState State => throw new NotImplementedException(); + + private readonly List> _rawButtonState = []; + private readonly float[] _rawAxisState; + private readonly Vector2[] _rawHatState = []; + private readonly bool[] _gamepadButtonState; + private readonly float[] _gamepadAxisState; + + private const float _buttonPressureMultiplier = 1 / 255f; + + private static JoystickButton AsGamepadButton(GamepadButton buttonIndex) => + buttonIndex switch { + GamepadButton.South => JoystickButton.ButtonDown, + GamepadButton.East => JoystickButton.ButtonRight, + GamepadButton.West => JoystickButton.ButtonLeft, + GamepadButton.North => JoystickButton.ButtonUp, + GamepadButton.Back => JoystickButton.Back, + GamepadButton.Guide => JoystickButton.Home, + GamepadButton.Start => JoystickButton.Start, + GamepadButton.LeftStick => JoystickButton.LeftStick, + GamepadButton.RightStick => JoystickButton.RightStick, + GamepadButton.LeftShoulder => JoystickButton.LeftBumper, + GamepadButton.RightShoulder => JoystickButton.RightBumper, + GamepadButton.DpadUp => JoystickButton.DPadUp, + GamepadButton.DpadDown => JoystickButton.DPadDown, + GamepadButton.DpadLeft => JoystickButton.DPadLeft, + GamepadButton.DpadRight => JoystickButton.DPadRight, + // TODO not exposed today + _ => (JoystickButton)buttonIndex + }; + + private readonly Dictionary _bindings = new(); + private readonly List?> _hatBindings = []; + private readonly List _outputBindings = []; + + // SDL indexes the 3 of these separately, but it is more convenient + // for us to index buttons/hats/axes as a single list. + // Since SDL only uses a single byte for a device index, + // we can safely use an integer key with a bit shift like this. + private const int ButtonShift = 0; + private const int AxisShift = 8; + + private const short _joystickDigitalThreshold = short.MaxValue / 8; + + private readonly record struct SDLGamepadState(List> Buttons, List Axes); + + ButtonReadOnlyList IButtonDevice.State => _joystickState.Buttons; } + diff --git a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs index edb69b945c..2f930a4bf5 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs @@ -23,9 +23,9 @@ public unsafe SdlKeyboard(uint sdlDeviceId, SdlInputBackend backend) : base(sdlD public static SdlKeyboard CreateDevice(SdlInputBackend backend, uint sdlDeviceId) => throw new NotImplementedException(); public KeyboardState State { get; } - public override void Release() {} // empty? + protected override void Release() {} // empty? - public override string Name => Backend.Sdl.GetKeyboardNameForID(SdlDeviceId).ReadToString(); + public override string Name => NativeBackend.GetKeyboardNameForID(SdlDeviceId).ReadToString(); public string? ClipboardText { get diff --git a/sources/Input/Input/InputMarshal.cs b/sources/Input/Input/InputMarshal.cs index d11ecff97a..19c7d156c5 100644 --- a/sources/Input/Input/InputMarshal.cs +++ b/sources/Input/Input/InputMarshal.cs @@ -5,7 +5,6 @@ using System.Diagnostics; using System.Numerics; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; namespace Silk.NET.Input; @@ -302,12 +301,12 @@ public static int GetButtonListCount() { if (typeof(T) == typeof(JoystickButton)) { - return (int)JoystickButton.DPadLeft; + return EnumInfo.UniqueValues.Count; } if (typeof(T) == typeof(PointerButton)) { - return (int)PointerButton.Button32; + return EnumInfo.UniqueValues.Count; } if (typeof(T) == typeof(KeyName)) diff --git a/sources/Input/Input/JoystickButton.cs b/sources/Input/Input/JoystickButton.cs index f76482034e..e5136a8153 100644 --- a/sources/Input/Input/JoystickButton.cs +++ b/sources/Input/Input/JoystickButton.cs @@ -3,12 +3,14 @@ namespace Silk.NET.Input; /// /// Enumerates the buttons of a joystick. /// -public enum JoystickButton +public enum JoystickButton // todo : should we include XInput, PSX, and Nintendo button names here? { /// /// The button was not recognised. /// - Unknown, + /// This is defined as such a large number such that unknown buttons can still be a JoystickButton, + /// and we can define up to predefined unique joystick buttons. + Unknown = int.MaxValue - ushort.MaxValue, /// /// The down-most button of the primary button cluster. @@ -107,3 +109,17 @@ public enum JoystickButton // BEFORE ADDING A NEW ITEM MAKE SURE YOU CHANGE LastJoystickButton IN InputMarshal } + +/// +/// Additional functions for making sense of s +/// +public static class JoystickButtonExtensions +{ + /// + /// Returns true if we have identified this button as a known button + /// + /// + /// + public static bool IsIdentified(this JoystickButton button) => button > JoystickButton.Unknown; +} + From 8e7eda4cdf04231cb6224e3272c2a17b450660f6 Mon Sep 17 00:00:00 2001 From: dom Date: Sat, 9 Aug 2025 18:52:17 -0400 Subject: [PATCH 19/39] some experimentation/progress re: axis/button indexing --- sources/Input/Input/ButtonReadOnlyList.cs | 18 ++- sources/Input/Input/DualReadOnlyList.cs | 24 +++- sources/Input/Input/GamepadState.cs | 52 ++++--- .../Input/Input/Implementations/EnumInfo.cs | 64 +++++++-- .../Input/Implementations/SDL3/SdlJoystick.cs | 131 ++++++++++++------ sources/Input/Input/JoystickAxis.cs | 13 ++ sources/Input/Input/JoystickButton.cs | 6 +- sources/Input/Input/JoystickState.cs | 16 ++- 8 files changed, 245 insertions(+), 79 deletions(-) create mode 100644 sources/Input/Input/JoystickAxis.cs diff --git a/sources/Input/Input/ButtonReadOnlyList.cs b/sources/Input/Input/ButtonReadOnlyList.cs index ecd579a3c9..1cd98aa366 100644 --- a/sources/Input/Input/ButtonReadOnlyList.cs +++ b/sources/Input/Input/ButtonReadOnlyList.cs @@ -10,23 +10,29 @@ namespace Silk.NET.Input; /// /// The button type (e.g. , , etc). /// -public readonly struct ButtonReadOnlyList : IReadOnlyList> +public readonly record struct ButtonReadOnlyList : IReadOnlyList> where T : unmanaged, Enum { - private readonly int _count; - private readonly Func _getIndexFunc; private readonly Func _indexMap; + private readonly IReadOnlyList> _list; - public ButtonReadOnlyList(InputReadOnlyList> getIndexFunc) + /// + /// A constructor for an input list that takes in: + /// + /// A list of buttons that will be indexed + /// A pre-built mapping function, if required, + /// used for iterating through the button list in order, regardless of the backend's internal button order. + public ButtonReadOnlyList(IReadOnlyList> buttonList, Func? indexMap = null) { - throw new NotImplementedException(); + _list = buttonList; + _indexMap = indexMap ?? (i => i); } /// /// Gets the state for the button with the given name. /// /// The button name. - public Button this[T name] => _list[_getIndexFunc(name)]; + public Button this[T name] => _list[EnumInfo.ValueIndexOf(name)]; /// public IEnumerator> GetEnumerator() => _list.GetEnumerator(); diff --git a/sources/Input/Input/DualReadOnlyList.cs b/sources/Input/Input/DualReadOnlyList.cs index 1fdf87c91a..2181706f22 100644 --- a/sources/Input/Input/DualReadOnlyList.cs +++ b/sources/Input/Input/DualReadOnlyList.cs @@ -6,24 +6,29 @@ namespace Silk.NET.Input; /// Represents a list that has exactly two elements. /// /// The element type. -public readonly struct DualReadOnlyList(T left, T right) : IReadOnlyList +public readonly struct DualReadOnlyList : IReadOnlyList { /// - /// Creates a copy of the given list. + /// Represents a list that has exactly two elements. /// - /// The list. - public DualReadOnlyList(DualReadOnlyList other) - : this(other.Left, other.Right) { } + /// The element type. + + public DualReadOnlyList(Func left, Func right) + { + _left = left; + _right = right; + } /// /// The first/leftmost element. /// - public readonly T Left = left; + public T Left => _left(); /// /// The second/rightmost element. /// - public readonly T Right = right; + public T Right => _right(); + /// public IEnumerator GetEnumerator() @@ -45,4 +50,9 @@ public IEnumerator GetEnumerator() 1 => Right, _ => throw new IndexOutOfRangeException(), }; + + + + private readonly Func _left; + private readonly Func _right; } diff --git a/sources/Input/Input/GamepadState.cs b/sources/Input/Input/GamepadState.cs index 9031830548..8a2aa616e6 100644 --- a/sources/Input/Input/GamepadState.cs +++ b/sources/Input/Input/GamepadState.cs @@ -5,35 +5,55 @@ namespace Silk.NET.Input; /// /// Contains user input received from an . /// -public class GamepadState( - ButtonReadOnlyList buttons, - DualReadOnlyList thumbsticks, - DualReadOnlyList triggers -) +public class GamepadState { /// - /// Clones the given state. This is useful for creating an immutable copy of state from a mutable one. + /// The constructor for a new GamepadState object /// - /// The other state. - public GamepadState(GamepadState other) - : this( - new ButtonReadOnlyList(other.Buttons), - new DualReadOnlyList(other.Thumbsticks), - new DualReadOnlyList(other.Triggers) - ) { } + /// The list of buttons + /// The list of states of the controllers axes that the triggers and joysticks will + /// be read from via their specific indices in this array + /// The joystick X axes. + /// The Joystick Y axes. + /// + /// + /// For and , the must be either of length + /// 2 or 4. + /// + /// If two are provided, the first is assumed to be the left stick, and the second is assumed to be the right stick + /// + /// if 4 are provided, it is assumed that the first two are - and + sides of the first axis, and so on. + ///[leftX, rightX] OR [-leftX, +leftX, -rightX, +rightX] + /// + /// + /// + public GamepadState(IReadOnlyList> buttons, float[] axisStates) + { + _axisStates = axisStates; + Buttons = new ButtonReadOnlyList(buttons); + Triggers = new DualReadOnlyList( + left: () => _axisStates[JoystickAxis.LeftTrigger.Index()], + right: () =>_axisStates[JoystickAxis.RightTrigger.Index()]); + Thumbsticks = new DualReadOnlyList( + left: () => new Vector2(_axisStates[JoystickAxis.LeftX.Index()], _axisStates[JoystickAxis.LeftY.Index()]), + right: () => new Vector2(_axisStates[JoystickAxis.RightX.Index()], _axisStates[JoystickAxis.RightY.Index()])); + } /// /// Gets the gamepad button state denoting the buttons being pressed or depressed. /// - public ButtonReadOnlyList Buttons { get; } = buttons; + public ButtonReadOnlyList Buttons { get; } /// /// Gets the state of the twin sticks on the gamepad. /// - public DualReadOnlyList Thumbsticks { get; internal set; } = thumbsticks; + public DualReadOnlyList Thumbsticks { get; internal set; } /// /// Gets the state of the triggers on the gamepad. /// - public DualReadOnlyList Triggers { get; internal set; } = triggers; + public DualReadOnlyList Triggers { get; internal set; } + + // ReSharper disable PrivateFieldCanBeConvertedToLocalVariable <- keeps closures consistent + private readonly float[] _axisStates; } diff --git a/sources/Input/Input/Implementations/EnumInfo.cs b/sources/Input/Input/Implementations/EnumInfo.cs index e6480b614d..7c3fb79fb7 100644 --- a/sources/Input/Input/Implementations/EnumInfo.cs +++ b/sources/Input/Input/Implementations/EnumInfo.cs @@ -153,12 +153,62 @@ static unsafe EnumInfo() /// Values with the same numerical value will return the same index /// /// - /// The index of the sorted enum numerical value + /// The index of the sorted enum numerical value, or -1 if not a named enum member. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int ValueIndexOf(T value) => _numericallyDistinctValues.TryGetValue(value, out var index) ? index : -1; + public static int ValueIndexOf(T value) => _numericallyDistinctValues.GetValueOrDefault(value, -1); - private static unsafe T[] OrderedValues(bool byNumericValue) + /// + /// Gets the ordered index of the unnamed enum value provided. This index is calculated by: + /// (the number of named members in this enum type) + (the raw value of the number) + /// + /// Negative values or values that are above the lowest enum value will return -1, as they cannot be used for indexing + /// + /// + /// + public static int ValueIndexOfUnnamed(T value) + { + if(_numericallyDistinctValues.TryGetValue(value, out var index)) + { + return index; + } + + var rawValue = ValueOf(value); + + // todo - don't rely on joystickButton's unknown - find the MinValue + if (rawValue <= 0 || rawValue >= ValueOf(_allEnumValuesRaw[0])) + { + return -1; + } + + return _all.Length + rawValue; + } + + private static unsafe TNumber ValueOf(TValue value) where TNumber : unmanaged where TValue : unmanaged + + { + if (sizeof(T) == sizeof(TNumber)) + { + return Unsafe.Read(&value); + } + + var minSize = Math.Min(sizeof(TNumber), sizeof(T)); + + var originalValuePtr = (byte*)&value; + + var valuePtr = &originalValuePtr[Math.Abs(minSize - sizeof(T))]; // does this assume little-endianness? + var numberPtr = stackalloc byte[sizeof(TNumber)]; + + // ensure block is initialized (as it isnt guaranteed?) so any missing bytes of the output will stay 0 + // if type TNumber is a larger size than type T + Unsafe.InitBlock(numberPtr, 0, (uint)sizeof(TNumber)); + + var copyToPtr = &numberPtr[Math.Abs(minSize - sizeof(TNumber))]; + Buffer.MemoryCopy(valuePtr, copyToPtr, sizeof(TNumber), minSize); + return *(TNumber*)numberPtr; + } + + private static T[] OrderedValues(bool byNumericValue) where TNumber : unmanaged, IComparable { // numerically distinct numbers @@ -166,20 +216,18 @@ private static unsafe T[] OrderedValues(bool byNumericValue) if (byNumericValue) { - allValues = allValues.DistinctBy(x => *(TNumber*)&x).ToArray(); + allValues = allValues.DistinctBy(ValueOf).ToArray(); } // sort by increasing order Array.Sort(allValues, (a, b) => { - var aNumber = *(TNumber*)&a; - var bNumber = *(TNumber*)&b; + var aNumber = ValueOf(a); + var bNumber = ValueOf(b); return aNumber.CompareTo(bNumber); }); return allValues; } - public static int ToUnknownIndex(TOther value) - public static unsafe bool HasValue(int value) => _allEnumValuesRaw.Contains(*(uint*)&value); } diff --git a/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs b/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs index 111c640f4a..5b2047d978 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs @@ -13,9 +13,9 @@ internal unsafe class SdlJoystick : SdlDevice, IJoystick, IGamepad private protected readonly JoystickType JoystickType; private readonly JoystickState _joystickState; - private readonly GamepadState? _gamepadState; private readonly GamepadHandle? _gamepadHandle; - public readonly bool HasGamepadImplementation; + public readonly bool HasGamepadMapping; + public GamepadState GamepadState { get; } public sealed override string Name => NativeBackend.GetJoystickNameForID(SdlDeviceId).ReadToString(); @@ -40,20 +40,21 @@ protected SdlJoystick(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDevic JoystickType = NativeBackend.GetJoystickType(joystickHandle); - var gamepadMapping = NativeBackend.GetGamepadBindings(sdlDeviceId); + /*var gamepadMapping = NativeBackend.GetGamepadBindings(sdlDeviceId); if (gamepadMapping.Native != null) { NativeBackend.Free(gamepadMapping.Native); - } + }*/ - int bindingsCount = 0; + var bindingsCount = 0; var gamepadHandle = *(GamepadHandle*)&joystickHandle; //NativeBackend.OpenGamepad(sdlDeviceId); var mappings = NativeBackend.GetGamepadBindings(gamepadHandle, &bindingsCount); if (bindingsCount != 0) { - HasGamepadImplementation = true; + HasGamepadMapping = true; + _gamepadHandle = gamepadHandle; for (int i = 0; i < bindingsCount; i++) { var binding = mappings[i]; @@ -81,11 +82,8 @@ protected SdlJoystick(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDevic switch (binding->OutputType) { case GamepadBindingType.Axis: - break; case GamepadBindingType.Button: - break; - default: - // todo : throw? this should not be possible according to sdl + _outputBindings.Add(*binding); break; } @@ -96,7 +94,7 @@ protected SdlJoystick(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDevic while (_hatBindings.Count <= id.Value) _hatBindings.Add(null); - _hatBindings[id.Value] ??= new List(); + _hatBindings[id.Value] ??= []; _hatBindings[id.Value]!.Add(*binding); } else @@ -118,18 +116,6 @@ protected SdlJoystick(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDevic } } - if (_bindings.Count > 0) - { - _gamepadButtonState = new bool[(int)GamepadButton.Count]; - _gamepadAxisState = new float[(int)GamepadAxis.Count]; - } - else - { - _gamepadState = null; - _gamepadButtonState = []; - _gamepadAxisState = []; - } - // init current joystick state var buttonCount = NativeBackend.GetNumJoystickButtons(joystickHandle); for (var i = 0; i < buttonCount; i++) @@ -139,7 +125,6 @@ protected SdlJoystick(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDevic } var axisCount = NativeBackend.GetNumJoystickAxes(joystickHandle); - _rawAxisState = new float[axisCount]; for (int i = 0; i < axisCount; i++) { var joystickInput = NativeBackend.GetJoystickAxis(_joystickHandle, i); @@ -159,8 +144,11 @@ protected SdlJoystick(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDevic UpdateHat(i, hatInput); } - _joystickState = new JoystickState(_rawAxisState, _rawButtonState, _rawHatState); - _gamepadState = new GamepadState() + _rawAxisState = new float[EnumInfo.UniqueValues.Count + axisCount]; + _rawButtonState = new (EnumInfo.UniqueValues.Count + buttonCount); + + _joystickState = new JoystickState(_rawAxisState, _rawButtonState, _hatStateVectors); + GamepadState = new GamepadState(_rawButtonState, _rawAxisState); } [Flags] @@ -189,7 +177,7 @@ private void UpdateHat(int hatIdx, byte hatInput) var down = (hatState & HatState.Down) == HatState.Down; var y = (float)(*(byte*)&up - *(byte*)&down); - _rawHatState[hatIdx] = new Vector2(x, y); + _hatStateVectors[hatIdx] = new Vector2(x, y); if (_hatBindings.Count <= hatIdx) { return; @@ -243,17 +231,87 @@ public void UpdateAxis(int axis, short joystickInput) UpdateGamepadAxis(output->Axis.Axis, joystickInput, output->Axis.AxisMin, output->Axis.AxisMax ); break; case GamepadBindingType.Button: - UpdateGamepadButton(output->Button, joystickInput > 0); // todo : threshold + UpdateGamepadButton(output->Button, joystickInput > _joystickDigitalThreshold); // todo : threshold smartlier break; } } private void UpdateGamepadAxis(GamepadAxis axis, int value, int min, int max) { - _gamepadAxisState[(int)axis] = (float)(value + min) / (max - min); + var mappedValue = (float)(value + min) / (max - min); + var positive = mappedValue > 0; + switch (axis) + { + case GamepadAxis.Invalid: + return; + case GamepadAxis.Leftx: + { + _rawAxisState[JoystickAxis.LeftX.Index()] = mappedValue; + + var split = SplitValue(mappedValue); + _rawAxisState[JoystickAxis.MinusLeftX.Index()] = split.minus; + _rawAxisState[JoystickAxis.PlusLeftX.Index()] = split.plus; + break; + } + case GamepadAxis.Lefty: + { + _rawAxisState[JoystickAxis.LeftY.Index()] = mappedValue; + + var split = SplitValue(mappedValue); + _rawAxisState[JoystickAxis.MinusLeftY.Index()] = split.minus; + _rawAxisState[JoystickAxis.PlusLeftY.Index()] = split.plus; + break; + } + case GamepadAxis.Rightx: + { + _rawAxisState[JoystickAxis.RightX.Index()] = mappedValue; + + var split = SplitValue(mappedValue); + _rawAxisState[JoystickAxis.MinusRightX.Index()] = split.minus; + _rawAxisState[JoystickAxis.PlusRightX.Index()] = split.plus; + break; + } + case GamepadAxis.Righty: + { + _rawAxisState[JoystickAxis.RightY.Index()] = mappedValue; + + var split = SplitValue(mappedValue); + _rawAxisState[JoystickAxis.MinusRightY.Index()] = split.minus; + _rawAxisState[JoystickAxis.PlusRightY.Index()] = split.plus; + break; + } + case GamepadAxis.LeftTrigger: + { + _rawAxisState[JoystickAxis.LeftTrigger.Index()] = mappedValue; + break; + } + case GamepadAxis.RightTrigger: + { + _rawAxisState[JoystickAxis.RightTrigger.Index()] = mappedValue; + break; + } + default: + throw new ArgumentOutOfRangeException(nameof(axis), axis, null); + } } - private void UpdateGamepadButton(GamepadButton button, bool value) => _gamepadButtonState[(int)button] = value; + private static (float minus, float plus) SplitValue(float mappedValue) + { + mappedValue = (float)((mappedValue - 0.5d) * 2d); + return mappedValue > 0 ? (0, mappedValue) : (mappedValue, 0); + } + + private void UpdateGamepadButton(GamepadButton button, bool value) + { + var asJoystickButton = AsJoystickButton(button); + var idx = EnumInfo.ValueIndexOfUnnamed(asJoystickButton); + if (idx < 0) + { + throw new Exception("Received an invalid SDL button??"); + } + + _rawButtonState[idx] = new Button(asJoystickButton, value, value ? 1 : 0); + } public void UpdateButton(int buttonIdx, byte rawValue) { @@ -265,7 +323,7 @@ public void UpdateButton(int buttonIdx, byte rawValue) return; } - Debug.Assert(binding.InputType == GamepadBindingType.Button); + Debug.Assert(binding.InputType == GamepadBindingType.Button && binding.Input.Button == buttonIdx); var bindingType = binding.OutputType; var output = &binding.Output; switch (bindingType) @@ -283,9 +341,6 @@ public void UpdateButton(int buttonIdx, byte rawValue) case GamepadBindingType.Button: UpdateGamepadButton(output->Button, down); break; - default: - // todo: throw? - this should not be possible - break; } } @@ -303,13 +358,11 @@ protected override void Release() private readonly List> _rawButtonState = []; private readonly float[] _rawAxisState; - private readonly Vector2[] _rawHatState = []; - private readonly bool[] _gamepadButtonState; - private readonly float[] _gamepadAxisState; + private readonly Vector2[] _hatStateVectors = []; private const float _buttonPressureMultiplier = 1 / 255f; - private static JoystickButton AsGamepadButton(GamepadButton buttonIndex) => + private static JoystickButton AsJoystickButton(GamepadButton buttonIndex) => buttonIndex switch { GamepadButton.South => JoystickButton.ButtonDown, GamepadButton.East => JoystickButton.ButtonRight, @@ -343,8 +396,6 @@ private static JoystickButton AsGamepadButton(GamepadButton buttonIndex) => private const short _joystickDigitalThreshold = short.MaxValue / 8; - private readonly record struct SDLGamepadState(List> Buttons, List Axes); - ButtonReadOnlyList IButtonDevice.State => _joystickState.Buttons; } diff --git a/sources/Input/Input/JoystickAxis.cs b/sources/Input/Input/JoystickAxis.cs new file mode 100644 index 0000000000..0f9f5bb798 --- /dev/null +++ b/sources/Input/Input/JoystickAxis.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; + +internal enum JoystickAxis +{ + Unknown = JoystickButton.Unknown, + MinusLeftX, PlusLeftX, MinusLeftY, PlusRightY, + MinusRightX, PlusRightX, MinusRightY, PlusLeftY, + LeftX, LeftY, RightX, RightY, + LeftTrigger, RightTrigger, +} diff --git a/sources/Input/Input/JoystickButton.cs b/sources/Input/Input/JoystickButton.cs index e5136a8153..a3a524983d 100644 --- a/sources/Input/Input/JoystickButton.cs +++ b/sources/Input/Input/JoystickButton.cs @@ -9,7 +9,8 @@ public enum JoystickButton // todo : should we include XInput, PSX, and Nintendo /// The button was not recognised. /// /// This is defined as such a large number such that unknown buttons can still be a JoystickButton, - /// and we can define up to predefined unique joystick buttons. + /// and we can define up to predefined unique joystick buttons. + /// Unknown = int.MaxValue - ushort.MaxValue, /// @@ -121,5 +122,8 @@ public static class JoystickButtonExtensions /// /// public static bool IsIdentified(this JoystickButton button) => button > JoystickButton.Unknown; + + /// + public static int Index(this T value) where T : unmanaged, Enum => EnumInfo.ValueIndexOfUnnamed(value); } diff --git a/sources/Input/Input/JoystickState.cs b/sources/Input/Input/JoystickState.cs index 9d80287b7b..290d5b1f61 100644 --- a/sources/Input/Input/JoystickState.cs +++ b/sources/Input/Input/JoystickState.cs @@ -21,4 +21,18 @@ public class JoystickState /// 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 + + /// + /// + /// + /// + /// + /// + public JoystickState(IReadOnlyList axes, IReadOnlyList> buttons, IReadOnlyList hats) + { + Axes = new InputReadOnlyList(axes); + Buttons = new ButtonReadOnlyList(buttons); + Hats = new InputReadOnlyList(hats); + + } +} From e637ba76a1300ec726711bd28294f89742120cb4 Mon Sep 17 00:00:00 2001 From: dom Date: Thu, 28 Aug 2025 16:05:42 -0400 Subject: [PATCH 20/39] joystick/gamepad abstraction progress, expand rumble system --- sources/Input/Input/GamepadState.cs | 4 +- .../Input/Input/Implementations/EnumInfo.cs | 2 +- .../Implementations/SDL3/ISdlJoystick.cs | 37 ++ .../Implementations/SDL3/Pointers/SdlPen.cs | 2 +- .../SDL3/Pointers/SdlSharedMouse.cs | 2 +- .../SDL3/Pointers/SdlTouchScreen.cs | 2 +- .../Input/Implementations/SDL3/SdlDevice.cs | 2 +- .../Input/Implementations/SDL3/SdlGamepad.cs | 335 ++++++++++++++--- .../Implementations/SDL3/SdlInputBackend.cs | 34 +- .../SDL3/SdlJoystick.Extended.cs | 56 +++ .../Input/Implementations/SDL3/SdlJoystick.cs | 339 +++--------------- .../Input/Implementations/SDL3/SdlKeyboard.cs | 2 +- .../Input/Implementations/SDL3/SdlMotor.cs | 13 - .../Input/Implementations/SDL3/SdlRumble.cs | 136 +++++++ sources/Input/Input/JoystickAxis.cs | 2 +- sources/Input/Input/JoystickButton.cs | 2 +- 16 files changed, 586 insertions(+), 384 deletions(-) create mode 100644 sources/Input/Input/Implementations/SDL3/ISdlJoystick.cs create mode 100644 sources/Input/Input/Implementations/SDL3/SdlJoystick.Extended.cs delete mode 100644 sources/Input/Input/Implementations/SDL3/SdlMotor.cs create mode 100644 sources/Input/Input/Implementations/SDL3/SdlRumble.cs diff --git a/sources/Input/Input/GamepadState.cs b/sources/Input/Input/GamepadState.cs index 8a2aa616e6..62ea3e9605 100644 --- a/sources/Input/Input/GamepadState.cs +++ b/sources/Input/Input/GamepadState.cs @@ -27,7 +27,7 @@ public class GamepadState /// /// /// - public GamepadState(IReadOnlyList> buttons, float[] axisStates) + public GamepadState(IReadOnlyList> buttons, IReadOnlyList axisStates) { _axisStates = axisStates; Buttons = new ButtonReadOnlyList(buttons); @@ -55,5 +55,5 @@ public GamepadState(IReadOnlyList> buttons, float[] axisS public DualReadOnlyList Triggers { get; internal set; } // ReSharper disable PrivateFieldCanBeConvertedToLocalVariable <- keeps closures consistent - private readonly float[] _axisStates; + private readonly IReadOnlyList _axisStates; } diff --git a/sources/Input/Input/Implementations/EnumInfo.cs b/sources/Input/Input/Implementations/EnumInfo.cs index 7c3fb79fb7..c8300a6796 100644 --- a/sources/Input/Input/Implementations/EnumInfo.cs +++ b/sources/Input/Input/Implementations/EnumInfo.cs @@ -160,7 +160,7 @@ static unsafe EnumInfo() /// /// Gets the ordered index of the unnamed enum value provided. This index is calculated by: - /// (the number of named members in this enum type) + (the raw value of the number) + /// (the number of named members in this enum type) + (the raw value of the number if unnamed) /// /// Negative values or values that are above the lowest enum value will return -1, as they cannot be used for indexing /// diff --git a/sources/Input/Input/Implementations/SDL3/ISdlJoystick.cs b/sources/Input/Input/Implementations/SDL3/ISdlJoystick.cs new file mode 100644 index 0000000000..217e9bcc60 --- /dev/null +++ b/sources/Input/Input/Implementations/SDL3/ISdlJoystick.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Silk.NET.SDL; + +namespace Silk.NET.Input.SDL3; + +/// +/// An interface for implementing different joystick types +/// +/// Currently, only Gamepad is explicitly supported, however this interface leaves room +/// for extensions such as those seen in . +/// +internal interface ISdlJoystick +{ + public SdlJoystick Joystick { get; } + /// + /// Raw joystick axis input events are forwarded here + /// + /// Input axis (which axis) + /// Input axis value + public void UpdateAxis(int axis, short joystickInput); + + /// + /// Raw joystick hat input events are forwarded here + /// + /// Input hat (which hat) + /// Input hat value + public void UpdateHat(int hatIdx, SdlJoystick.HatState hatState); + + /// + /// Raw joystick button input events are forwarded here + /// + /// Input button (which button) + /// Button state + public void UpdateButton(int buttonIdx, bool down); +} diff --git a/sources/Input/Input/Implementations/SDL3/Pointers/SdlPen.cs b/sources/Input/Input/Implementations/SDL3/Pointers/SdlPen.cs index 58ac83cf87..ef48cee019 100644 --- a/sources/Input/Input/Implementations/SDL3/Pointers/SdlPen.cs +++ b/sources/Input/Input/Implementations/SDL3/Pointers/SdlPen.cs @@ -5,7 +5,7 @@ namespace Silk.NET.Input.SDL3.Pointers; internal class SdlPen : SdlBoundedPointerDevice, ISdlDevice { - public static SdlPen CreateDevice(SdlInputBackend backend, uint sdlDeviceId) + public static SdlPen CreateDevice(uint sdlDeviceId, SdlInputBackend backend) { throw new NotImplementedException(); } diff --git a/sources/Input/Input/Implementations/SDL3/Pointers/SdlSharedMouse.cs b/sources/Input/Input/Implementations/SDL3/Pointers/SdlSharedMouse.cs index bdcd43e091..1acc798041 100644 --- a/sources/Input/Input/Implementations/SDL3/Pointers/SdlSharedMouse.cs +++ b/sources/Input/Input/Implementations/SDL3/Pointers/SdlSharedMouse.cs @@ -55,7 +55,7 @@ public SdlSharedMouse(uint sdlDeviceId, SdlInputBackend backend, IReadOnlyList throw new NotImplementedException(); + public static SdlSharedMouse CreateDevice(uint sdlDeviceId, SdlInputBackend backend) => throw new NotImplementedException(); public override string Name => $"{Backend.Name}: Shared/Global Mouse"; diff --git a/sources/Input/Input/Implementations/SDL3/Pointers/SdlTouchScreen.cs b/sources/Input/Input/Implementations/SDL3/Pointers/SdlTouchScreen.cs index d8329a0acd..7aa5d70f95 100644 --- a/sources/Input/Input/Implementations/SDL3/Pointers/SdlTouchScreen.cs +++ b/sources/Input/Input/Implementations/SDL3/Pointers/SdlTouchScreen.cs @@ -5,7 +5,7 @@ namespace Silk.NET.Input.SDL3.Pointers; internal class SdlTouchScreen : SdlDevice, ISdlDevice, IPointerDevice { - public static SdlTouchScreen CreateDevice(SdlInputBackend backend, uint sdlDeviceId) => throw new NotImplementedException(); + public static SdlTouchScreen CreateDevice(uint sdlDeviceId, SdlInputBackend backend) => throw new NotImplementedException(); public bool Equals(IInputDevice? other) => throw new NotImplementedException(); diff --git a/sources/Input/Input/Implementations/SDL3/SdlDevice.cs b/sources/Input/Input/Implementations/SDL3/SdlDevice.cs index 0379d7e4ed..923117afd3 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlDevice.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlDevice.cs @@ -68,5 +68,5 @@ public void Dispose() /// internal interface ISdlDevice : IInputDevice where T : SdlDevice { - public static abstract T CreateDevice(SdlInputBackend backend, uint sdlDeviceId); + public static abstract T CreateDevice(uint sdlDeviceId, SdlInputBackend backend); } diff --git a/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs b/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs index 7d843ab374..3c63f1503b 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs @@ -1,86 +1,325 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Numerics; +using System.Diagnostics; using Silk.NET.SDL; namespace Silk.NET.Input.SDL3; -internal sealed class SdlGenericJoystick : SdlJoystick, ISdlDevice +/// +/// provides the IGamepad implementation for a joystick +/// +internal sealed unsafe class SdlGamepad : SdlDevice, IGamepad, ISdlDevice, ISdlJoystick { - public SdlGenericJoystick(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDeviceId, backend) + private readonly GamepadHandle _gamepadHandle; + + public SdlJoystick Joystick { get; } + + private SdlGamepad(SdlJoystick joystick) : base(joystick.SdlDeviceId, joystick.Backend) { - if (JoystickType == JoystickType.Gamepad) + Joystick = joystick; + + var bindingsCount = 0; + var joystickHandle = joystick.JoystickHandle; + var gamepadHandle = *(GamepadHandle*)&joystickHandle; //NativeBackend.OpenGamepad(sdlDeviceId); + var mappings = NativeBackend.GetGamepadBindings(gamepadHandle, &bindingsCount); + + if (bindingsCount == 0) + { + if (mappings == null) + { + var error = NativeBackend.GetError(); + if (error.Native != null) + { + Console.Error.WriteLine(error.ReadToString()); + NativeBackend.Free(error.Native); + } + } + + throw new Exception("No gamepad mappings found."); + } + + _gamepadHandle = gamepadHandle; + for (int i = 0; i < bindingsCount; i++) { - throw new Exception("Joystick should have been created as a gamepad, not a joystick."); + var binding = mappings[i]; + + if (binding->OutputType == GamepadBindingType.None) + { + continue; + } + + int? id = null; + + switch (binding->InputType) + { + case GamepadBindingType.Button: + id = binding->Input.Button << _buttonShift; + break; + case GamepadBindingType.Axis: + id = binding->Input.Axis.Axis << _axisShift; + break; + case GamepadBindingType.Hat: + id = binding->Input.Hat.Hat; + break; + } + + switch (binding->OutputType) + { + case GamepadBindingType.Axis: + case GamepadBindingType.Button: + _outputBindings.Add(*binding); + break; + } + + if (id == null) + { + continue; + } + + if (binding->InputType == GamepadBindingType.Hat) + { + while (_hatBindings.Count <= id.Value) + { + _hatBindings.Add(null); + } + + _hatBindings[id.Value] ??= []; + _hatBindings[id.Value]!.Add(*binding); + } + else + { + _bindings.Add(id.Value, *binding); + } } + + NativeBackend.Free(mappings); + GamepadState = new GamepadState(joystick.RawButtonState, joystick.RawAxisState); + Joystick.AddDeviceMapping(this); } - public static SdlGenericJoystick CreateDevice(SdlInputBackend backend, uint sdlDeviceId) => throw new NotImplementedException(); -} + public override string Name => Joystick.Name; -/// -/// provides the IGamepad implementation for a joystick -/// -internal sealed class SdlGamepad : SdlJoystick, IGamepad, ISdlDevice -{ - public SdlGamepad(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDeviceId, backend) + protected override void Release() { - var _gamepad = NativeBackend.OpenGamepad(sdlDeviceId); - if (_gamepad == nullptr) - { - NativeBackend.ThrowError(); - } + Joystick.RemoveDeviceMapping(this); - _gamepadState = new GamepadState(new ButtonReadOnlyList(Buttons), thumbsticks, triggers); + // todo: does this close the joystick as well? + NativeBackend.CloseGamepad(_gamepadHandle); } - protected override void Release() => NativeBackend.CloseGamepad(_gamepad); + #region IGamepad + + GamepadState IGamepad.State => GamepadState; + private GamepadState GamepadState { get; } - private readonly GamepadState _gamepadState; - GamepadState IGamepad.State => _gamepadState; + #endregion - public override string Name => NativeBackend.GetGamepadNameForID(SdlDeviceId).ReadToString(); - // TODO this entire API needs to be redesigned as right now this is literally only ever going to be useful if it's - // just left or right. The original intention was that this would be useful for things like 3D haptics, but what did - // I know. The SDL people seem to have done a good job with their haptic API, let's see what we can do with it. - // For now, this has the same implementation as it always has. - public IReadOnlyList VibrationMotors => - _motors ??= [new SdlMotor(this, 0), new SdlMotor(this, 1)]; + #region Rumble - private IMotor[]? _motors; - private ushort[]? _motorFrequencies; + public IReadOnlyList VibrationMotors => _rumbler ??= SdlRumble.Create(_gamepadHandle.Handle, NativeBackend, 2); + private SdlRumble? _rumbler; - internal ushort GetRumble(int motor) => (_motorFrequencies ??= [0, 0])[motor]; - internal void SetRumble(int motor, ushort value) + #endregion + + public static SdlGamepad CreateDevice(uint sdlDeviceId, SdlInputBackend backend) + { + var joystick = backend.GetOrCreateDevice(sdlDeviceId); + return new SdlGamepad(joystick); + } + + ~SdlGamepad() => Release(); + + + private void UpdateGamepadAxis(GamepadAxis axis, int value, int min, int max) { - (_motorFrequencies ??= [0, 0])[motor] = value; - if ( - !NativeBackend.RumbleGamepad( - _gamepad, - _motorFrequencies[0], - _motorFrequencies[1], - uint.MaxValue - ) - ) + var mappedValue = (float)(value + min) / (max - min); + switch (axis) { - NativeBackend.ThrowError(); + case GamepadAxis.Invalid: + return; + case GamepadAxis.Leftx: + { + Joystick.UpdateRawAxisState(JoystickAxis.LeftX, mappedValue); + + var split = SdlJoystick.SplitValue(mappedValue); + Joystick.UpdateRawAxisState(JoystickAxis.MinusLeftX, split.minus); + Joystick.UpdateRawAxisState(JoystickAxis.PlusLeftX, split.plus); + break; + } + case GamepadAxis.Lefty: + { + Joystick.UpdateRawAxisState(JoystickAxis.LeftY, mappedValue); + + var split = SdlJoystick.SplitValue(mappedValue); + Joystick.UpdateRawAxisState(JoystickAxis.MinusLeftY, split.minus); + Joystick.UpdateRawAxisState(JoystickAxis.PlusLeftY, split.plus); + break; + } + case GamepadAxis.Rightx: + { + Joystick.UpdateRawAxisState(JoystickAxis.RightX, mappedValue); + + var split = SdlJoystick.SplitValue(mappedValue); + Joystick.UpdateRawAxisState(JoystickAxis.MinusRightX, split.minus); + Joystick.UpdateRawAxisState(JoystickAxis.PlusRightX, split.plus); + break; + } + case GamepadAxis.Righty: + { + Joystick.UpdateRawAxisState(JoystickAxis.RightY, mappedValue); + + var split = SdlJoystick.SplitValue(mappedValue); + Joystick.UpdateRawAxisState(JoystickAxis.MinusRightY, split.minus); + Joystick.UpdateRawAxisState(JoystickAxis.PlusRightY, split.plus); + break; + } + case GamepadAxis.LeftTrigger: + { + Joystick.UpdateRawAxisState(JoystickAxis.LeftTrigger, mappedValue); + break; + } + case GamepadAxis.RightTrigger: + { + Joystick.UpdateRawAxisState(JoystickAxis.RightTrigger, mappedValue); + break; + } + default: + throw new ArgumentOutOfRangeException(nameof(axis), axis, null); } } - public static SdlGamepad CreateDevice(SdlInputBackend backend, uint sdlDeviceId) => new(sdlDeviceId, backend); + #region ISdlJoystick - ~SdlGamepad() => Release(); + public void UpdateButton(int buttonIdx, bool down) + { + if (!_bindings.TryGetValue(buttonIdx << _buttonShift, out var binding)) + { + return; + } + + Debug.Assert(binding.InputType == GamepadBindingType.Button && binding.Input.Button == buttonIdx); + var bindingType = binding.OutputType; + var output = &binding.Output; + switch (bindingType) + { + case GamepadBindingType.Axis: + var axis = output->Axis; + UpdateGamepadAxis( + axis: axis.Axis, + value: down ? axis.AxisMax : axis.AxisMin, + min: axis.AxisMin, + max: axis.AxisMax); + break; + + case GamepadBindingType.Button: + UpdateButtonBinding(output->Button, down); + break; + } + } + + public void AddButtonEvent(byte sdlButtonId, byte sdlButtonDown) => UpdateButton(sdlButtonId, sdlButtonDown > 0); + + public void UpdateAxis(int axis, short joystickInput) + { + if (!_bindings.TryGetValue(axis << _axisShift, out var binding)) + { + return; + } - public void AddButtonEvent(GamepadButtonEvent sdlButton) + Debug.Assert(binding.InputType == GamepadBindingType.Axis); + + var output = &binding.Output; + var input = &binding.Input.Axis; + + switch (binding.OutputType) + { + case GamepadBindingType.Axis: + UpdateGamepadAxis(output->Axis.Axis, joystickInput, input->AxisMin, input->AxisMax); + break; + case GamepadBindingType.Button: + UpdateButtonBinding(output->Button, joystickInput >= input->AxisMin && joystickInput <= input->AxisMax); + break; + } + } + + public void UpdateHat(int hatIdx, SdlJoystick.HatState hatState) { - ProcessButtonEvent(this, sdlButton.Button, sdlButton.Down); + if (_hatBindings.Count <= hatIdx) + { + return; + } + + var bindings = _hatBindings[index: hatIdx]; + if (bindings is not { Count: > 0 }) + { + return; + } + + foreach (var binding in bindings) + { + Debug.Assert(condition: binding.InputType == GamepadBindingType.Hat && binding.Input.Hat.Hat == hatIdx); + var input = &binding.Input.Hat; + var mask = (SdlJoystick.HatState)input->HatMask; + var bindingState = hatState & mask; + switch (binding.OutputType) + { + case GamepadBindingType.Axis: + var axis = binding.Output.Axis; + UpdateGamepadAxis( + axis: axis.Axis, + value: bindingState == SdlJoystick.HatState.Centered ? axis.AxisMin : axis.AxisMax, + min: axis.AxisMin, + max: axis.AxisMax); + break; + case GamepadBindingType.Button: + var button = binding.Output.Button; + UpdateButtonBinding(button, bindingState != SdlJoystick.HatState.Centered); + break; + } + } } - private static void ProcessButtonEvent(T device, byte sdlButtonId, byte sdlButtonDown) where T : SdlJoystick, ISdlDevice + #endregion + + private void UpdateButtonBinding(GamepadButton button, bool value) { + var asJoystickButton = AsJoystickButton(button); + Joystick.UpdateRawButtonState(asJoystickButton, value, value ? 1 : 0); + return; + + static JoystickButton AsJoystickButton(GamepadButton buttonIndex) => + buttonIndex switch { + GamepadButton.South => JoystickButton.ButtonDown, + GamepadButton.East => JoystickButton.ButtonRight, + GamepadButton.West => JoystickButton.ButtonLeft, + GamepadButton.North => JoystickButton.ButtonUp, + GamepadButton.Back => JoystickButton.Back, + GamepadButton.Guide => JoystickButton.Home, + GamepadButton.Start => JoystickButton.Start, + GamepadButton.LeftStick => JoystickButton.LeftStick, + GamepadButton.RightStick => JoystickButton.RightStick, + GamepadButton.LeftShoulder => JoystickButton.LeftBumper, + GamepadButton.RightShoulder => JoystickButton.RightBumper, + GamepadButton.DpadUp => JoystickButton.DPadUp, + GamepadButton.DpadDown => JoystickButton.DPadDown, + GamepadButton.DpadLeft => JoystickButton.DPadLeft, + GamepadButton.DpadRight => JoystickButton.DPadRight, + // TODO not exposed today + _ => (JoystickButton)buttonIndex + }; } + + // SDL indexes the 3 of these separately, but it is more convenient + // for us to index buttons/hats/axes as a single list. + // Since SDL only uses a single byte for a device index, + // we can safely use an integer key with a bit shift like this. + private const int _buttonShift = 0; + private const int _axisShift = 8; + private readonly Dictionary _bindings = new(); + private readonly List?> _hatBindings = []; + private readonly List _outputBindings = []; } diff --git a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs index b6855c3c2b..cae1d2a3ba 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs @@ -222,7 +222,8 @@ private void ProcessEvent(ref Event arg1, IInputHandler handler) _previousTimestamp = timestamp; // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault - switch ((EventType)arg1.Common.Type) + var evt = (EventType)arg1.Common.Type; + switch (evt) { // Device changed events ------------------------------------------------- case EventType.KeymapChanged: @@ -240,7 +241,6 @@ private void ProcessEvent(ref Event arg1, IInputHandler handler) break; } - case EventType.MouseAdded: break; case EventType.MouseRemoved: @@ -294,7 +294,7 @@ private void ProcessEvent(ref Event arg1, IInputHandler handler) case EventType.JoystickButtonDown: case EventType.JoystickButtonUp: var joystick = GetOrCreateDevice(arg1.Jbutton.Which); - joystick.AddButtonEvent(arg1.Jbutton); + joystick.AddButtonEvent(arg1.Jbutton.Button, arg1.Jbutton.Down); break; case EventType.JoystickBatteryUpdated: break; @@ -308,7 +308,7 @@ private void ProcessEvent(ref Event arg1, IInputHandler handler) case EventType.GamepadButtonDown: case EventType.GamepadButtonUp: var gamepad = GetOrCreateDevice(arg1.Gbutton.Which); - gamepad.AddButtonEvent(arg1.Gbutton); + gamepad.AddButtonEvent(arg1.Gbutton.Button, arg1.Gbutton.Down); break; case EventType.GamepadTouchpadDown: break; @@ -402,25 +402,23 @@ private void ProcessEvent(ref Event arg1, IInputHandler handler) case EventType.ClipboardUpdate: break; } + } - return; - - T GetOrCreateDevice(uint id) where T : SdlDevice, ISdlDevice + internal T GetOrCreateDevice(uint id) where T : SdlDevice, ISdlDevice + { + // If we already have a device with this ID, return it. + for (var i = 0; i < _devices.Count; i++) { - // If we already have a device with this ID, return it. - for (var i = 0; i < _devices.Count; i++) + if (_devices[i] is T typedDevice && typedDevice.SdlDeviceId == id) { - if (_devices[i] is T typedDevice && typedDevice.SdlDeviceId == id) - { - return typedDevice; - } + return typedDevice; } - - var device = T.CreateDevice(this, id); - _devices.Add(device); - Console.WriteLine($"Gamepad added: (sdl ID: {id})"); - return device; } + + var device = T.CreateDevice(id, this); + _devices.Add(device); + Console.WriteLine($"Gamepad added: (sdl ID: {id})"); + return device; } internal bool RemoveDevice(uint id) diff --git a/sources/Input/Input/Implementations/SDL3/SdlJoystick.Extended.cs b/sources/Input/Input/Implementations/SDL3/SdlJoystick.Extended.cs new file mode 100644 index 0000000000..a0a3a01c8b --- /dev/null +++ b/sources/Input/Input/Implementations/SDL3/SdlJoystick.Extended.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Numerics; + +namespace Silk.NET.Input.SDL3; + +// This partial class contains the logic for handling joystick-based device types such as SdlGamepad. +internal sealed partial class SdlJoystick +{ + public bool TryGetDevice([NotNullWhen(true)] out T? device) where T : class, ISdlJoystick + { + foreach (var d in _devices) + { + if (d is T typedDevice) + { + device = typedDevice; + return true; + } + } + + device = null; + return false; + } + + internal IReadOnlyList RawHatState => _rawHatState; + internal IReadOnlyList> RawButtonState => _rawButtonState; + internal IReadOnlyList RawAxisState => _rawAxisState; + internal void AddDeviceMapping(ISdlJoystick device) => _devices.Add(device); + internal void RemoveDeviceMapping(ISdlJoystick device) => _devices.Remove(device); + + internal void UpdateRawButtonState(JoystickButton button, bool isDown, float pressure) + { + var idx = button.Index(); + if (idx < 0) + { + throw new Exception("Received an invalid SDL button??"); + } + + _rawButtonState[idx] = new Button(button, isDown, pressure); + } + + internal void UpdateRawAxisState(JoystickAxis axis, float value) + { + var index = axis.Index(); + if (index < 0) + { + throw new Exception("Received an invalid SDL axis??"); + } + + _rawAxisState[axis.Index()] = value; + } + + private readonly List _devices = []; +} diff --git a/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs b/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs index 5b2047d978..d332a34b41 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs @@ -1,25 +1,21 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics; using System.Numerics; using Silk.NET.SDL; namespace Silk.NET.Input.SDL3; -internal unsafe class SdlJoystick : SdlDevice, IJoystick, IGamepad +internal sealed unsafe partial class SdlJoystick : SdlDevice, IJoystick, ISdlDevice { - private readonly JoystickHandle _joystickHandle; - private protected readonly JoystickType JoystickType; + public JoystickState State { get; } + internal readonly JoystickType JoystickType; + internal JoystickHandle JoystickHandle { get; } + public static SdlJoystick CreateDevice(uint sdlDeviceId, SdlInputBackend backend) => new(sdlDeviceId, backend); - private readonly JoystickState _joystickState; - private readonly GamepadHandle? _gamepadHandle; - public readonly bool HasGamepadMapping; - public GamepadState GamepadState { get; } + public override string Name => NativeBackend.GetJoystickNameForID(SdlDeviceId).ReadToString(); - public sealed override string Name => NativeBackend.GetJoystickNameForID(SdlDeviceId).ReadToString(); - - protected SdlJoystick(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDeviceId, backend) + private SdlJoystick(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDeviceId, backend) { var joystickHandle = NativeBackend.OpenJoystick(sdlDeviceId); @@ -36,123 +32,47 @@ protected SdlJoystick(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDevic throw new Exception($"Failed to open joystick: {errorStr ?? "Unknown error."}"); } - _joystickHandle = joystickHandle; + JoystickHandle = joystickHandle; JoystickType = NativeBackend.GetJoystickType(joystickHandle); - /*var gamepadMapping = NativeBackend.GetGamepadBindings(sdlDeviceId); - if (gamepadMapping.Native != null) - { - NativeBackend.Free(gamepadMapping.Native); - }*/ - - - var bindingsCount = 0; - var gamepadHandle = *(GamepadHandle*)&joystickHandle; //NativeBackend.OpenGamepad(sdlDeviceId); - var mappings = NativeBackend.GetGamepadBindings(gamepadHandle, &bindingsCount); - - if (bindingsCount != 0) - { - HasGamepadMapping = true; - _gamepadHandle = gamepadHandle; - for (int i = 0; i < bindingsCount; i++) - { - var binding = mappings[i]; - - if (binding->OutputType == GamepadBindingType.None) - { - continue; - } - - int? id = null; - - switch (binding->InputType) - { - case GamepadBindingType.Button: - id = binding->Input.Button << ButtonShift; - break; - case GamepadBindingType.Axis: - id = binding->Input.Axis.Axis << AxisShift; - break; - case GamepadBindingType.Hat: - id = binding->Input.Hat.Hat; - break; - } - - switch (binding->OutputType) - { - case GamepadBindingType.Axis: - case GamepadBindingType.Button: - _outputBindings.Add(*binding); - break; - } - - if (id != null) - { - if (binding->InputType == GamepadBindingType.Hat) - { - while (_hatBindings.Count <= id.Value) - _hatBindings.Add(null); - - _hatBindings[id.Value] ??= []; - _hatBindings[id.Value]!.Add(*binding); - } - else - { - _bindings.Add(id.Value, *binding); - } - } - } - - NativeBackend.Free(mappings); - } - else if (mappings == null) - { - var error = NativeBackend.GetError(); - if (error.Native != null) - { - Console.Error.WriteLine(error.ReadToString()); - NativeBackend.Free(error.Native); - } - } - // init current joystick state var buttonCount = NativeBackend.GetNumJoystickButtons(joystickHandle); - for (var i = 0; i < buttonCount; i++) + for (byte i = 0; i < buttonCount; i++) { - var joystickInput = NativeBackend.GetJoystickButtonRaw(_joystickHandle, i); - UpdateButton(i, joystickInput); + var joystickInput = NativeBackend.GetJoystickButtonRaw(JoystickHandle, i); + AddButtonEvent(i, joystickInput); } var axisCount = NativeBackend.GetNumJoystickAxes(joystickHandle); - for (int i = 0; i < axisCount; i++) + for (var i = 0; i < axisCount; i++) { - var joystickInput = NativeBackend.GetJoystickAxis(_joystickHandle, i); + var joystickInput = NativeBackend.GetJoystickAxis(JoystickHandle, i); if (joystickInput == 0) { // this indicates an sdl error, so just set our internal axis to 0 joystickInput = short.MinValue; } - UpdateAxis(i, joystickInput); + AddAxisEvent(i, joystickInput); } var hatCount = NativeBackend.GetNumJoystickHats(joystickHandle); - for (int i = 0; i < hatCount; ++i) + for (var i = 0; i < hatCount; ++i) { var hatInput = NativeBackend.GetJoystickHat(joystickHandle, i); - UpdateHat(i, hatInput); + AddHatEvent(i, hatInput); } _rawAxisState = new float[EnumInfo.UniqueValues.Count + axisCount]; - _rawButtonState = new (EnumInfo.UniqueValues.Count + buttonCount); + _rawButtonState = new Button[EnumInfo.UniqueValues.Count + buttonCount]; - _joystickState = new JoystickState(_rawAxisState, _rawButtonState, _hatStateVectors); - GamepadState = new GamepadState(_rawButtonState, _rawAxisState); + State = new JoystickState(_rawAxisState, _rawButtonState, _rawHatState); } + [Flags] - private enum HatState : byte + internal enum HatState : byte { Up = (byte)Sdl.HatUp, Right = (byte)Sdl.HatRight, @@ -165,8 +85,9 @@ private enum HatState : byte RightDown = (byte)Sdl.HatRightdown } + #region Sdl Events - private void UpdateHat(int hatIdx, byte hatInput) + public void AddHatEvent(int hatIdx, byte hatInput) { var hatState = (HatState)hatInput; var left = (hatState & HatState.Left) == HatState.Left; @@ -177,225 +98,53 @@ private void UpdateHat(int hatIdx, byte hatInput) var down = (hatState & HatState.Down) == HatState.Down; var y = (float)(*(byte*)&up - *(byte*)&down); - _hatStateVectors[hatIdx] = new Vector2(x, y); - if (_hatBindings.Count <= hatIdx) - { - return; - } + _rawHatState[hatIdx] = new Vector2(x, y); - var bindings = _hatBindings[index: hatIdx]; - if (bindings is not { Count: > 0 }) + foreach(var device in _devices) { - return; - } - - foreach (var binding in bindings) - { - Debug.Assert(condition: binding.InputType == GamepadBindingType.Hat && binding.Input.Hat.Hat == hatIdx); - var input = &binding.Input.Hat; - var mask = (HatState)input->HatMask; - var bindingState = hatState & mask; - switch (binding.OutputType) - { - case GamepadBindingType.Axis: - var axis = binding.Output.Axis; - UpdateGamepadAxis( - axis: axis.Axis, - value: bindingState == HatState.Centered ? axis.AxisMin : axis.AxisMax, - min: axis.AxisMin, - max: axis.AxisMax); - break; - case GamepadBindingType.Button: - var button = binding.Output.Button; - UpdateGamepadButton(button, bindingState != HatState.Centered); - break; - } + device.UpdateHat(hatIdx, hatState); } } - public void UpdateAxis(int axis, short joystickInput) + public void AddAxisEvent(int axis, short joystickInput) { _rawAxisState[axis] = (float)(joystickInput + short.MaxValue) / ushort.MaxValue; - if (!_bindings.TryGetValue(axis << AxisShift, out var binding)) + foreach (var device in _devices) { - return; - } - - Debug.Assert(binding.InputType == GamepadBindingType.Axis); - - var output = &binding.Output; - - switch (binding.OutputType) - { - case GamepadBindingType.Axis: - UpdateGamepadAxis(output->Axis.Axis, joystickInput, output->Axis.AxisMin, output->Axis.AxisMax ); - break; - case GamepadBindingType.Button: - UpdateGamepadButton(output->Button, joystickInput > _joystickDigitalThreshold); // todo : threshold smartlier - break; + device.UpdateAxis(axis, joystickInput); } } - private void UpdateGamepadAxis(GamepadAxis axis, int value, int min, int max) + public void AddButtonEvent(byte sdlButtonId, byte sdlButtonDown) { - var mappedValue = (float)(value + min) / (max - min); - var positive = mappedValue > 0; - switch (axis) + var down = sdlButtonDown > 0; + _rawButtonState[sdlButtonId] = new Button((JoystickButton)sdlButtonId, down, down ? 1 : 0); + foreach (var device in _devices) { - case GamepadAxis.Invalid: - return; - case GamepadAxis.Leftx: - { - _rawAxisState[JoystickAxis.LeftX.Index()] = mappedValue; - - var split = SplitValue(mappedValue); - _rawAxisState[JoystickAxis.MinusLeftX.Index()] = split.minus; - _rawAxisState[JoystickAxis.PlusLeftX.Index()] = split.plus; - break; - } - case GamepadAxis.Lefty: - { - _rawAxisState[JoystickAxis.LeftY.Index()] = mappedValue; - - var split = SplitValue(mappedValue); - _rawAxisState[JoystickAxis.MinusLeftY.Index()] = split.minus; - _rawAxisState[JoystickAxis.PlusLeftY.Index()] = split.plus; - break; - } - case GamepadAxis.Rightx: - { - _rawAxisState[JoystickAxis.RightX.Index()] = mappedValue; - - var split = SplitValue(mappedValue); - _rawAxisState[JoystickAxis.MinusRightX.Index()] = split.minus; - _rawAxisState[JoystickAxis.PlusRightX.Index()] = split.plus; - break; - } - case GamepadAxis.Righty: - { - _rawAxisState[JoystickAxis.RightY.Index()] = mappedValue; - - var split = SplitValue(mappedValue); - _rawAxisState[JoystickAxis.MinusRightY.Index()] = split.minus; - _rawAxisState[JoystickAxis.PlusRightY.Index()] = split.plus; - break; - } - case GamepadAxis.LeftTrigger: - { - _rawAxisState[JoystickAxis.LeftTrigger.Index()] = mappedValue; - break; - } - case GamepadAxis.RightTrigger: - { - _rawAxisState[JoystickAxis.RightTrigger.Index()] = mappedValue; - break; - } - default: - throw new ArgumentOutOfRangeException(nameof(axis), axis, null); + device.UpdateButton(sdlButtonId, down); } } - private static (float minus, float plus) SplitValue(float mappedValue) + #endregion + + internal static (float minus, float plus) SplitValue(float mappedValue) { mappedValue = (float)((mappedValue - 0.5d) * 2d); return mappedValue > 0 ? (0, mappedValue) : (mappedValue, 0); } - private void UpdateGamepadButton(GamepadButton button, bool value) - { - var asJoystickButton = AsJoystickButton(button); - var idx = EnumInfo.ValueIndexOfUnnamed(asJoystickButton); - if (idx < 0) - { - throw new Exception("Received an invalid SDL button??"); - } - _rawButtonState[idx] = new Button(asJoystickButton, value, value ? 1 : 0); - } - - public void UpdateButton(int buttonIdx, byte rawValue) - { - var down = rawValue > 0; - _rawButtonState[buttonIdx] = new Button((JoystickButton)buttonIdx, down, down ? 1 : 0); - - if (!_bindings.TryGetValue(buttonIdx << ButtonShift, out var binding)) - { - return; - } - - Debug.Assert(binding.InputType == GamepadBindingType.Button && binding.Input.Button == buttonIdx); - var bindingType = binding.OutputType; - var output = &binding.Output; - switch (bindingType) - { - case GamepadBindingType.Axis: - - var axis = output->Axis; - UpdateGamepadAxis( - axis: axis.Axis, - value: down ? axis.AxisMax : axis.AxisMin, - min: axis.AxisMin, - max: axis.AxisMax); - break; - - case GamepadBindingType.Button: - UpdateGamepadButton(output->Button, down); - break; - } - } + protected override void Release() => NativeBackend.CloseJoystick(JoystickHandle); - protected override void Release() - { - if (_gamepadHandle != null) - { - NativeBackend.CloseGamepad(_gamepadHandle.Value); - } + // State + private readonly Button[] _rawButtonState; + private readonly float[] _rawAxisState; + private readonly Vector2[] _rawHatState = []; - NativeBackend.CloseJoystick(_joystickHandle); - } + // Constants + internal const short DigitalThreshold = short.MaxValue / 8; - public JoystickState State => throw new NotImplementedException(); + ButtonReadOnlyList IButtonDevice.State => State.Buttons; - private readonly List> _rawButtonState = []; - private readonly float[] _rawAxisState; - private readonly Vector2[] _hatStateVectors = []; - - private const float _buttonPressureMultiplier = 1 / 255f; - - private static JoystickButton AsJoystickButton(GamepadButton buttonIndex) => - buttonIndex switch { - GamepadButton.South => JoystickButton.ButtonDown, - GamepadButton.East => JoystickButton.ButtonRight, - GamepadButton.West => JoystickButton.ButtonLeft, - GamepadButton.North => JoystickButton.ButtonUp, - GamepadButton.Back => JoystickButton.Back, - GamepadButton.Guide => JoystickButton.Home, - GamepadButton.Start => JoystickButton.Start, - GamepadButton.LeftStick => JoystickButton.LeftStick, - GamepadButton.RightStick => JoystickButton.RightStick, - GamepadButton.LeftShoulder => JoystickButton.LeftBumper, - GamepadButton.RightShoulder => JoystickButton.RightBumper, - GamepadButton.DpadUp => JoystickButton.DPadUp, - GamepadButton.DpadDown => JoystickButton.DPadDown, - GamepadButton.DpadLeft => JoystickButton.DPadLeft, - GamepadButton.DpadRight => JoystickButton.DPadRight, - // TODO not exposed today - _ => (JoystickButton)buttonIndex - }; - - private readonly Dictionary _bindings = new(); - private readonly List?> _hatBindings = []; - private readonly List _outputBindings = []; - - // SDL indexes the 3 of these separately, but it is more convenient - // for us to index buttons/hats/axes as a single list. - // Since SDL only uses a single byte for a device index, - // we can safely use an integer key with a bit shift like this. - private const int ButtonShift = 0; - private const int AxisShift = 8; - - private const short _joystickDigitalThreshold = short.MaxValue / 8; - - ButtonReadOnlyList IButtonDevice.State => _joystickState.Buttons; } diff --git a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs index 2f930a4bf5..913559afbf 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs @@ -20,7 +20,7 @@ public unsafe SdlKeyboard(uint sdlDeviceId, SdlInputBackend backend) : base(sdlD State = new KeyboardState(_keyStates, () => false, () => false);// todo : how do i get the num lock/capslock? } - public static SdlKeyboard CreateDevice(SdlInputBackend backend, uint sdlDeviceId) => throw new NotImplementedException(); + public static SdlKeyboard CreateDevice(uint sdlDeviceId, SdlInputBackend backend) => throw new NotImplementedException(); public KeyboardState State { get; } protected override void Release() {} // empty? diff --git a/sources/Input/Input/Implementations/SDL3/SdlMotor.cs b/sources/Input/Input/Implementations/SDL3/SdlMotor.cs deleted file mode 100644 index 77a508bf6e..0000000000 --- a/sources/Input/Input/Implementations/SDL3/SdlMotor.cs +++ /dev/null @@ -1,13 +0,0 @@ -// 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(SdlGamepad gamepad, int freqIdx) : IMotor -{ - public float Speed - { - get => (float)gamepad.GetRumble(freqIdx) / ushort.MaxValue; - set => gamepad.SetRumble(freqIdx, (ushort)(value * ushort.MaxValue)); - } -} diff --git a/sources/Input/Input/Implementations/SDL3/SdlRumble.cs b/sources/Input/Input/Implementations/SDL3/SdlRumble.cs new file mode 100644 index 0000000000..11f53f7ae7 --- /dev/null +++ b/sources/Input/Input/Implementations/SDL3/SdlRumble.cs @@ -0,0 +1,136 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using System.Runtime.CompilerServices; +using Silk.NET.SDL; + +namespace Silk.NET.Input.SDL3; + +internal unsafe class SdlRumble : IReadOnlyList +{ + public IMotor this[int index] => _motors[index]; + public int Count => _motors.Length; + + public static SdlRumble Create(void* handle, ISdl sdl, int count) where T : unmanaged + { + SetRumbleDelegate setRumble; + if (typeof(T) == typeof(GamepadHandle)) + { + setRumble = SetGamepadRumble; + } + else if (typeof(T) == typeof(JoystickHandle)) + { + setRumble = SetJoystickRumble; + } + else + { + throw new InvalidOperationException("Invalid device type"); + } + + return new SdlRumble(handle, sdl, count, setRumble); + } + + private SdlRumble(void* handle, ISdl nativeBackend, int count, SetRumbleDelegate setRumble) + { + _setRumble = setRumble; + _handle = handle; + _motors = new IMotor[count]; + _motorFrequencies = new ushort[count]; + _nativeBackend = nativeBackend; + CreateMotors(_motors); + } + + private void CreateMotors(IMotor[] motors) + { + for (var i = 0; i < motors.Length; i++) + { + motors[i] = new Motor(this, i); + } + } + + IEnumerator IEnumerable.GetEnumerator() => (IEnumerator)_motors.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => _motors.GetEnumerator(); + + private float GetRumble01(int motor) => _motorFrequencies[motor] * _toFloat; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SetRumble01(int motor, float value) => + SetRumble01(motor, (ushort)(value * ushort.MaxValue), _motorFrequencies); + + private void SetRumble01(int motor, ushort value, ushort[] motorFrequencies) + { + // todo - use Haptics API instead? + // todo - dispatch this to the correct input thread + + // TODO this entire API needs to be redesigned as right now this is literally only ever going to be useful if it's + // just left or right. The original intention was that this would be useful for things like 3D haptics, but what did + // I know. The SDL people seem to have done a good job with their haptic API, let's see what we can do with it. + // For now, this has the same implementation as it always has. + var valueShort = value; + motorFrequencies[motor] = valueShort; + var left = motorFrequencies[0]; + var right = motorFrequencies[1]; + _setRumble(_nativeBackend, _handle, left, right); + } + + private static void SetJoystickRumble(ISdl backend, void* handle, ushort left, ushort right) + { + var average = (ushort)((left + right) >> 2); + var joystickHandle = *(JoystickHandle*)&handle; + if (!backend.RumbleJoystick(joystickHandle, average, average, _durationMs)) + { + backend.ThrowError(); + } + + if (!backend.RumbleJoystickTriggers(joystickHandle, left, right, _durationMs)) + { + backend.ThrowError(); + } + } + + private static void SetGamepadRumble(ISdl backend, void* handle, ushort left, ushort right) + { + var average = (ushort)((left + right) >> 2); + var gamepadHandle = *(GamepadHandle*)&handle; + if (!backend.RumbleGamepad(gamepadHandle, average, average, _durationMs)) + { + backend.ThrowError(); + } + + if (!backend.RumbleGamepadTriggers(gamepadHandle, left, right, _durationMs)) + { + backend.ThrowError(); + } + } + + private readonly SetRumbleDelegate _setRumble; + private readonly void* _handle; + private readonly IMotor[] _motors; + private readonly ushort[] _motorFrequencies; + private readonly ISdl _nativeBackend; + private const float _toFloat = 1f / ushort.MaxValue; + private const uint _durationMs = uint.MaxValue; + + + private delegate void SetRumbleDelegate(ISdl nativeBackend, void* handle, ushort left, ushort right); + + private class Motor : IMotor + { + private readonly int _freqIndex; + private readonly SdlRumble _rumbler; + + public Motor(SdlRumble rumbler, int freqIdx) + { + _freqIndex = freqIdx; + _rumbler = rumbler; + } + + public float Speed + { + get => _rumbler.GetRumble01(_freqIndex); + set => _rumbler.SetRumble01(_freqIndex, value); + } + } +} + diff --git a/sources/Input/Input/JoystickAxis.cs b/sources/Input/Input/JoystickAxis.cs index 0f9f5bb798..4c9e45148a 100644 --- a/sources/Input/Input/JoystickAxis.cs +++ b/sources/Input/Input/JoystickAxis.cs @@ -5,7 +5,7 @@ namespace Silk.NET.Input; internal enum JoystickAxis { - Unknown = JoystickButton.Unknown, + Unknown = int.MaxValue - ushort.MaxValue, MinusLeftX, PlusLeftX, MinusLeftY, PlusRightY, MinusRightX, PlusRightX, MinusRightY, PlusLeftY, LeftX, LeftY, RightX, RightY, diff --git a/sources/Input/Input/JoystickButton.cs b/sources/Input/Input/JoystickButton.cs index a3a524983d..56ca678f3c 100644 --- a/sources/Input/Input/JoystickButton.cs +++ b/sources/Input/Input/JoystickButton.cs @@ -11,7 +11,7 @@ public enum JoystickButton // todo : should we include XInput, PSX, and Nintendo /// This is defined as such a large number such that unknown buttons can still be a JoystickButton, /// and we can define up to predefined unique joystick buttons. /// - Unknown = int.MaxValue - ushort.MaxValue, + Unknown = JoystickAxis.Unknown, /// /// The down-most button of the primary button cluster. From d0f05aa24e253236fbaeebbe3ae820f460b24215 Mon Sep 17 00:00:00 2001 From: dom Date: Fri, 29 Aug 2025 13:57:27 -0400 Subject: [PATCH 21/39] cont'd --- .../Implementations/SDL3/ISdlJoystick.cs | 6 +- .../SDL3/Pointers/SdlBoundedPointerDevice.cs | 2 +- .../Implementations/SDL3/Pointers/SdlPen.cs | 6 + .../Input/Implementations/SDL3/SdlDevice.cs | 22 +- .../Input/Implementations/SDL3/SdlGamepad.cs | 89 ++++-- .../Implementations/SDL3/SdlInputBackend.cs | 265 ++++++++++-------- .../Input/Implementations/SDL3/SdlJoystick.cs | 13 +- .../Input/Implementations/SDL3/SdlKeyboard.cs | 11 +- 8 files changed, 254 insertions(+), 160 deletions(-) diff --git a/sources/Input/Input/Implementations/SDL3/ISdlJoystick.cs b/sources/Input/Input/Implementations/SDL3/ISdlJoystick.cs index 217e9bcc60..7cb6b21ac8 100644 --- a/sources/Input/Input/Implementations/SDL3/ISdlJoystick.cs +++ b/sources/Input/Input/Implementations/SDL3/ISdlJoystick.cs @@ -19,19 +19,19 @@ internal interface ISdlJoystick /// /// Input axis (which axis) /// Input axis value - public void UpdateAxis(int axis, short joystickInput); + public void UpdateFromJoyAxis(int axis, short joystickInput); /// /// Raw joystick hat input events are forwarded here /// /// Input hat (which hat) /// Input hat value - public void UpdateHat(int hatIdx, SdlJoystick.HatState hatState); + public void UpdateFromJoyHat(int hatIdx, SdlJoystick.HatState hatState); /// /// Raw joystick button input events are forwarded here /// /// Input button (which button) /// Button state - public void UpdateButton(int buttonIdx, bool down); + public void UpdateFromJoyButton(int buttonIdx, bool down); } diff --git a/sources/Input/Input/Implementations/SDL3/Pointers/SdlBoundedPointerDevice.cs b/sources/Input/Input/Implementations/SDL3/Pointers/SdlBoundedPointerDevice.cs index bedbc8740e..65922f56e6 100644 --- a/sources/Input/Input/Implementations/SDL3/Pointers/SdlBoundedPointerDevice.cs +++ b/sources/Input/Input/Implementations/SDL3/Pointers/SdlBoundedPointerDevice.cs @@ -10,7 +10,7 @@ namespace Silk.NET.Input.SDL3.Pointers; /// internal abstract class SdlBoundedPointerDevice : SdlDevice, IPointerDevice { - protected SdlBoundedPointerDevice(uint sdlDeviceId, SdlInputBackend backend, IReadOnlyList targets, InputMarshal.ListOwner boundedPoints) : base(sdlDeviceId, backend) + protected SdlBoundedPointerDevice(SdlInputBackend backend, IReadOnlyList targets, InputMarshal.ListOwner boundedPoints) : base(backend) { Targets = targets; BoundedPoints = boundedPoints; diff --git a/sources/Input/Input/Implementations/SDL3/Pointers/SdlPen.cs b/sources/Input/Input/Implementations/SDL3/Pointers/SdlPen.cs index ef48cee019..74d11c89c0 100644 --- a/sources/Input/Input/Implementations/SDL3/Pointers/SdlPen.cs +++ b/sources/Input/Input/Implementations/SDL3/Pointers/SdlPen.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Guid = Silk.NET.SDL.Guid; + namespace Silk.NET.Input.SDL3.Pointers; internal class SdlPen : SdlBoundedPointerDevice, ISdlDevice @@ -15,4 +17,8 @@ public static SdlPen CreateDevice(uint sdlDeviceId, SdlInputBackend backend) public SdlPen(uint sdlDeviceId, SdlInputBackend backend, IReadOnlyList targets, InputMarshal.ListOwner boundedPoints) : base(sdlDeviceId, backend, targets, boundedPoints) { } + + public override uint SdlDeviceId => NativeBackend.guid; + + protected override Guid SdlDeviceGuid => throw new NotImplementedException(); } diff --git a/sources/Input/Input/Implementations/SDL3/SdlDevice.cs b/sources/Input/Input/Implementations/SDL3/SdlDevice.cs index 923117afd3..cfc7ba94d6 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlDevice.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlDevice.cs @@ -17,8 +17,14 @@ bool IEquatable.Equals(IInputDevice? other) => && other is SdlBoundedPointerDevice dev && dev.NativeBackend == NativeBackend; - public nint Id => Backend.AsSilkId(SdlDeviceId); - public uint SdlDeviceId { get; } + public nint Id { get; } + + public uint SdlDeviceId => _sdlDeviceId ??= RefreshIdFromBackend(); + + private uint? _sdlDeviceId; + + public abstract uint RefreshIdFromBackend(); + public SdlInputBackend Backend { get; } /// @@ -37,10 +43,11 @@ bool IEquatable.Equals(IInputDevice? other) => }*/ - protected SdlDevice(uint sdlDeviceId, SdlInputBackend backend) + protected SdlDevice(SdlInputBackend backend, nint uniqueId, uint sdlDeviceId) { Backend = backend; - SdlDeviceId = sdlDeviceId; + Id = uniqueId; + _sdlDeviceId = sdlDeviceId; } protected abstract void Release(); @@ -68,5 +75,10 @@ public void Dispose() /// internal interface ISdlDevice : IInputDevice where T : SdlDevice { - public static abstract T CreateDevice(uint sdlDeviceId, SdlInputBackend backend); + public static abstract T? CreateDevice(uint sdlDeviceId, SdlInputBackend backend); + +} + +internal static class SdlDeviceFactory +{ } diff --git a/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs b/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs index 3c63f1503b..e3bb909a92 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs @@ -15,13 +15,27 @@ internal sealed unsafe class SdlGamepad : SdlDevice, IGamepad, ISdlDeviceOutputType) { case GamepadBindingType.Axis: @@ -72,11 +95,6 @@ private SdlGamepad(SdlJoystick joystick) : base(joystick.SdlDeviceId, joystick.B break; } - if (id == null) - { - continue; - } - if (binding->InputType == GamepadBindingType.Hat) { while (_hatBindings.Count <= id.Value) @@ -94,10 +112,12 @@ private SdlGamepad(SdlJoystick joystick) : base(joystick.SdlDeviceId, joystick.B } NativeBackend.Free(mappings); - GamepadState = new GamepadState(joystick.RawButtonState, joystick.RawAxisState); - Joystick.AddDeviceMapping(this); } + public void Remap() => Remap(_gamepadHandle); + + public override uint RefreshIdFromBackend() => NativeBackend.GetGamepadID(_gamepadHandle); + public override string Name => Joystick.Name; protected override void Release() @@ -113,25 +133,31 @@ protected override void Release() GamepadState IGamepad.State => GamepadState; private GamepadState GamepadState { get; } - #endregion - - - #region Rumble - public IReadOnlyList VibrationMotors => _rumbler ??= SdlRumble.Create(_gamepadHandle.Handle, NativeBackend, 2); private SdlRumble? _rumbler; #endregion - public static SdlGamepad CreateDevice(uint sdlDeviceId, SdlInputBackend backend) + public static SdlGamepad? CreateDevice(uint sdlDeviceId, SdlInputBackend backend) { - var joystick = backend.GetOrCreateDevice(sdlDeviceId); - return new SdlGamepad(joystick); - } + if (!backend.TryGetOrCreateDevice(sdlDeviceId, out var joystick)) + { + return null; + } + + var joystickUniqueId = joystick.Id; + // manipulate the joystick id to make a unique gamepad id + var uniqueId = joystickUniqueId; + var guid = backend.Sdl.GetGamepadGuidForID(sdlDeviceId); + const ulong gamepadType = (ulong)JoystickType.Gamepad; + const ulong mod = gamepadType << 24; - ~SdlGamepad() => Release(); + // todo + throw new NotImplementedException(); + return new SdlGamepad(joystick, uniqueId: uniqueId); + } private void UpdateGamepadAxis(GamepadAxis axis, int value, int min, int max) { @@ -193,7 +219,7 @@ private void UpdateGamepadAxis(GamepadAxis axis, int value, int min, int max) #region ISdlJoystick - public void UpdateButton(int buttonIdx, bool down) + public void UpdateFromJoyButton(int buttonIdx, bool down) { if (!_bindings.TryGetValue(buttonIdx << _buttonShift, out var binding)) { @@ -220,9 +246,13 @@ public void UpdateButton(int buttonIdx, bool down) } } - public void AddButtonEvent(byte sdlButtonId, byte sdlButtonDown) => UpdateButton(sdlButtonId, sdlButtonDown > 0); + public void AddButtonEvent(byte sdlButtonId, byte sdlButtonDown) => + UpdateButtonBinding((GamepadButton)sdlButtonId, sdlButtonDown > 0); - public void UpdateAxis(int axis, short joystickInput) + public void AddAxisEvent(byte evtAxis, short evtValue) => + UpdateGamepadAxis((GamepadAxis)evtAxis, evtValue, Sdl.JoystickAxisMin, Sdl.JoystickAxisMax); + + public void UpdateFromJoyAxis(int axis, short joystickInput) { if (!_bindings.TryGetValue(axis << _axisShift, out var binding)) { @@ -245,7 +275,7 @@ public void UpdateAxis(int axis, short joystickInput) } } - public void UpdateHat(int hatIdx, SdlJoystick.HatState hatState) + public void UpdateFromJoyHat(int hatIdx, SdlJoystick.HatState hatState) { if (_hatBindings.Count <= hatIdx) { @@ -312,6 +342,10 @@ static JoystickButton AsJoystickButton(GamepadButton buttonIndex) => }; } + private readonly Dictionary _bindings = new(); + private readonly List?> _hatBindings = []; + private readonly List _outputBindings = []; + // SDL indexes the 3 of these separately, but it is more convenient // for us to index buttons/hats/axes as a single list. @@ -319,7 +353,4 @@ static JoystickButton AsJoystickButton(GamepadButton buttonIndex) => // we can safely use an integer key with a bit shift like this. private const int _buttonShift = 0; private const int _axisShift = 8; - private readonly Dictionary _bindings = new(); - private readonly List?> _hatBindings = []; - private readonly List _outputBindings = []; } diff --git a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs index cae1d2a3ba..dc501db8f6 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs @@ -215,113 +215,137 @@ private unsafe byte OnEvent(void* arg0, Event* arg1) return 1; } - private void ProcessEvent(ref Event arg1, IInputHandler handler) + private void ProcessEvent(ref Event evt, IInputHandler handler) { - var timestamp = GetTimestamp(ref arg1); + var timestamp = GetTimestamp(ref evt); Debug.Assert(timestamp >= _previousTimestamp, "Events out of order"); _previousTimestamp = timestamp; // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault - var evt = (EventType)arg1.Common.Type; - switch (evt) + var type = (EventType)evt.Common.Type; + + switch (type) { - // Device changed events ------------------------------------------------- - case EventType.KeymapChanged: - break; - case EventType.KeyboardAdded: - { - var id = arg1.Kdevice.Which; - Debug.Assert(_devices.All(x => x.Id != AsSilkId(id))); - _ = GetOrCreateDevice(id); - break; - } + case EventType.GamepadRemoved: + RemoveDevice(_devices, evt.Gdevice.Which); + return; + case EventType.JoystickRemoved: + RemoveDevice(_devices, evt.Jdevice.Which); + return; case EventType.KeyboardRemoved: - { - RemoveDevice(arg1.Kdevice.Which); - break; - } - - case EventType.MouseAdded: - break; + RemoveDevice(_devices, evt.Kdevice.Which); + return; case EventType.MouseRemoved: - RemoveDevice(arg1.Mdevice.Which); - break; - - case EventType.GamepadAdded: + RemoveDevice(_devices, evt.Mdevice.Which); + RemoveDevice(_devices, evt.Mdevice.Which); + return; + case >= EventType.KeyDown and <= EventType.TextEditingCandidates: { - var id = arg1.Gdevice.Which; - Debug.Assert(_devices.All(x => x.Id != AsSilkId(id))); - _ = GetOrCreateDevice(id); + if (!TryGetOrCreateDevice(evt.Kdevice.Which, out var keyboard)) + { + return; + } + + if (type == EventType.KeyboardAdded) + { + return; + } + + switch (type) + { + case EventType.KeyDown: + case EventType.KeyUp: + keyboard.AddKeyEvent(evt.Key); + break; + case EventType.TextEditing: + break; + case EventType.TextInput: + break; + case EventType.TextEditingCandidates: + break; + } + break; } - case EventType.GamepadRemoved: + case >= EventType.GamepadAxisMotion and <= EventType.GamepadSteamHandleUpdated: { - RemoveDevice(arg1.Gdevice.Which); - break; - } - case EventType.GamepadRemapped: - break; + if (!TryGetOrCreateDevice(evt.Gdevice.Which, out var gamepad)) + { + return; + } + + if (type is EventType.GamepadAdded) + { + return; + } + + switch (type) + { + case EventType.GamepadAxisMotion: + gamepad.AddAxisEvent(evt.Gaxis.Axis, evt.Gaxis.Value); + break; + case EventType.GamepadButtonDown: + case EventType.GamepadButtonUp: + gamepad.AddButtonEvent(evt.Gbutton.Button, evt.Gbutton.Down); + break; + case EventType.GamepadRemapped: + gamepad.Remap(); + break; + case EventType.GamepadTouchpadDown: + break; + case EventType.GamepadTouchpadMotion: + break; + case EventType.GamepadTouchpadUp: + break; + case EventType.GamepadSensorUpdate: + break; + case EventType.GamepadUpdateComplete: + break; + case EventType.GamepadSteamHandleUpdated: + break; + } - case EventType.JoystickAdded: - RemoveDevice(arg1.Jdevice.Which); - break; - case EventType.JoystickRemoved: - break; - - // Input events ---------------------------------------------------------- - - // keyboard - case EventType.KeyDown: - { - var keyboard = GetOrCreateDevice(arg1.Key.Which); - keyboard.AddKeyEvent(EventType.KeyDown, arg1.Key); break; } - case EventType.KeyUp: + case >= EventType.JoystickAxisMotion and <= EventType.JoystickUpdateComplete: { - var keyboard = GetOrCreateDevice(arg1.Key.Which); - keyboard.AddKeyEvent(EventType.KeyUp, arg1.Key); + if (!TryGetOrCreateDevice(evt.Jdevice.Which, out var joystick)) + { + return; + } + + if (type is EventType.JoystickAdded) + { + // already done + return; + } + + switch (type) + { + case EventType.JoystickAxisMotion: + joystick.AddAxisEvent(evt.Jaxis.Axis, evt.Jaxis.Value); + break; + case EventType.JoystickBallMotion: + break; + case EventType.JoystickHatMotion: + joystick.AddHatEvent(evt.Jhat.Hat, evt.Jhat.Value); + break; + case EventType.JoystickButtonDown: + case EventType.JoystickButtonUp: + joystick.AddButtonEvent(evt.Jbutton.Button, evt.Jbutton.Down); + break; + case EventType.JoystickBatteryUpdated: + break; + case EventType.JoystickUpdateComplete: + break; + } break; } + } - // Joystick - case EventType.JoystickAxisMotion: - break; - case EventType.JoystickBallMotion: - break; - case EventType.JoystickHatMotion: - break; - case EventType.JoystickButtonDown: - case EventType.JoystickButtonUp: - var joystick = GetOrCreateDevice(arg1.Jbutton.Which); - joystick.AddButtonEvent(arg1.Jbutton.Button, arg1.Jbutton.Down); - break; - case EventType.JoystickBatteryUpdated: - break; - case EventType.JoystickUpdateComplete: - break; - - - // Gamepad inputs - case EventType.GamepadAxisMotion: - break; - case EventType.GamepadButtonDown: - case EventType.GamepadButtonUp: - var gamepad = GetOrCreateDevice(arg1.Gbutton.Which); - gamepad.AddButtonEvent(arg1.Gbutton.Button, arg1.Gbutton.Down); - break; - case EventType.GamepadTouchpadDown: - break; - case EventType.GamepadTouchpadMotion: - break; - case EventType.GamepadTouchpadUp: - break; - case EventType.GamepadSensorUpdate: - break; - case EventType.GamepadUpdateComplete: - break; - case EventType.GamepadSteamHandleUpdated: - break; + switch (type) + { + // Input events ---------------------------------------------------------- // sensor? for what? case EventType.SensorUpdate: @@ -404,52 +428,67 @@ private void ProcessEvent(ref Event arg1, IInputHandler handler) } } - internal T GetOrCreateDevice(uint id) where T : SdlDevice, ISdlDevice + internal bool TryGetOrCreateDevice(uint id, [NotNullWhen(true)] out T? device) where T : SdlDevice, ISdlDevice { // If we already have a device with this ID, return it. for (var i = 0; i < _devices.Count; i++) { if (_devices[i] is T typedDevice && typedDevice.SdlDeviceId == id) { - return typedDevice; + device = typedDevice; + return true; } } - var device = T.CreateDevice(id, this); + try + { + device = T.CreateDevice(id, this); + } + catch (Exception e) + { + Console.Error.WriteLine($"Failed to create device {nameof(T)} with id '{id}': {e}"); + device = null; + return false; + } + + if (device is null) + { + Console.Error.WriteLine($"Failed to create device {nameof(T)} with id '{id}'"); + return false; + } + + _devices.Add(device); Console.WriteLine($"Gamepad added: (sdl ID: {id})"); - return device; + return true; } - internal bool RemoveDevice(uint id) + private static bool RemoveDevice(List devices, uint id) { - var silkId = AsSilkId(id); - var deviceIdx = _devices.FindIndex(x => x.Id == silkId); + var deviceIdx = devices.FindIndex(x => x is T && x.SdlDeviceId == id); if (deviceIdx == -1) - return false; // we never used this device to begin with, so just ignore its removal + { + // we never used this device to begin with, so just ignore its removal + return false; + } - var device = _devices[deviceIdx]; + var device = devices[deviceIdx]; device.Dispose(); - _devices.RemoveAt(deviceIdx); + devices.RemoveAt(deviceIdx); + + // device IDs may have changed when a device was removed, so we need to refresh them + RefreshDeviceIds(devices); return true; } - /// - /// Turns an sdl device id into a universally unique Silk.NET input id. - /// - /// - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public nint AsSilkId(uint which) => Id + Unsafe.As(ref which) + 1; - - /// - /// Reverts the process of to get the original SDL id. - /// - /// - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public uint AsSdlId(nint id) => (uint)(id - Id - 1); + private static void RefreshDeviceIds(List devices) + { + for (var i = 0; i < devices.Count; i++) + { + devices[i].RefreshIdFromBackend(); + } + } private ulong _previousTimestamp = ulong.MinValue; diff --git a/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs b/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs index d332a34b41..8c23f8f28d 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs @@ -11,11 +11,12 @@ internal sealed unsafe partial class SdlJoystick : SdlDevice, IJoystick, ISdlDev public JoystickState State { get; } internal readonly JoystickType JoystickType; internal JoystickHandle JoystickHandle { get; } - public static SdlJoystick CreateDevice(uint sdlDeviceId, SdlInputBackend backend) => new(sdlDeviceId, backend); - + public static SdlJoystick CreateDevice(uint sdlDeviceId, SdlInputBackend backend) => new(sdlDeviceId, uniqueId, backend); public override string Name => NativeBackend.GetJoystickNameForID(SdlDeviceId).ReadToString(); + public override uint RefreshIdFromBackend() => NativeBackend.GetJoystickID(JoystickHandle); + - private SdlJoystick(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDeviceId, backend) + private SdlJoystick(uint sdlDeviceId, nint uniqueId, SdlInputBackend backend) : base(backend, uniqueId, sdlDeviceId) { var joystickHandle = NativeBackend.OpenJoystick(sdlDeviceId); @@ -102,7 +103,7 @@ public void AddHatEvent(int hatIdx, byte hatInput) foreach(var device in _devices) { - device.UpdateHat(hatIdx, hatState); + device.UpdateFromJoyHat(hatIdx, hatState); } } @@ -111,7 +112,7 @@ public void AddAxisEvent(int axis, short joystickInput) _rawAxisState[axis] = (float)(joystickInput + short.MaxValue) / ushort.MaxValue; foreach (var device in _devices) { - device.UpdateAxis(axis, joystickInput); + device.UpdateFromJoyAxis(axis, joystickInput); } } @@ -121,7 +122,7 @@ public void AddButtonEvent(byte sdlButtonId, byte sdlButtonDown) _rawButtonState[sdlButtonId] = new Button((JoystickButton)sdlButtonId, down, down ? 1 : 0); foreach (var device in _devices) { - device.UpdateButton(sdlButtonId, down); + device.UpdateFromJoyButton(sdlButtonId, down); } } diff --git a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs index 913559afbf..bb82d7537b 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs @@ -9,9 +9,10 @@ namespace Silk.NET.Input.SDL3; internal class SdlKeyboard : SdlDevice, IKeyboard, ISdlDevice { private readonly List> _keyStates; - public unsafe SdlKeyboard(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDeviceId, backend) + public unsafe SdlKeyboard(uint sdlDeviceId, SdlInputBackend backend) : base(backend, uniqueId, sdlDeviceId) { _keyStates = new List>((int)Scancode.ScancodeCount); + _sdlDeviceId = sdlDeviceId; for (var i = 0; i < 512; i++) { _keyStates.Add(new Button((KeyName)i, false, 0f)); @@ -25,6 +26,9 @@ public unsafe SdlKeyboard(uint sdlDeviceId, SdlInputBackend backend) : base(sdlD public KeyboardState State { get; } protected override void Release() {} // empty? + private readonly uint _sdlDeviceId; + public override uint RefreshIdFromBackend() => _sdlDeviceId; + public override string Name => NativeBackend.GetKeyboardNameForID(SdlDeviceId).ReadToString(); public string? ClipboardText { @@ -35,7 +39,8 @@ public string? ClipboardText return Sdl.Instance.GetClipboardText().ReadToString(); } - set => throw new NotImplementedException("Setting clipboard text is not implemented in SDL3 backend."); + set => Sdl.Instance.SetClipboardText(value); + //throw new NotImplementedException("Setting clipboard text is not implemented in SDL3 backend."); } public bool TryGetKeyName(KeyName key, [NotNullWhen(true)] out string? name) => @@ -46,7 +51,7 @@ public bool TryGetKeyName(KeyName key, [NotNullWhen(true)] out string? name) => public string? EndInput() => throw new NotImplementedException(); - public void AddKeyEvent(EventType type, KeyboardEvent key) + public void AddKeyEvent(KeyboardEvent key) { const float fraction = 1f / 255f; _keyStates[(int)key.Key] = new Button((KeyName)key.Key, key.Down != 0, key.Down * fraction); From 7942c92f1b1b66f7395bb60153852444b161f546 Mon Sep 17 00:00:00 2001 From: dom Date: Mon, 1 Sep 2025 18:30:09 -0400 Subject: [PATCH 22/39] start attempting to guarantee unique ids --- .../Implementations/SDL3/Pointers/SdlPen.cs | 3 - .../Input/Implementations/SDL3/SdlGamepad.cs | 22 +++-- .../Implementations/SDL3/SdlInputBackend.cs | 2 + .../Input/Implementations/SDL3/SdlJoystick.cs | 92 ++++++++++++++++++- .../Input/Implementations/SDL3/SdlKeyboard.cs | 15 ++- 5 files changed, 120 insertions(+), 14 deletions(-) diff --git a/sources/Input/Input/Implementations/SDL3/Pointers/SdlPen.cs b/sources/Input/Input/Implementations/SDL3/Pointers/SdlPen.cs index 74d11c89c0..acbacce0ea 100644 --- a/sources/Input/Input/Implementations/SDL3/Pointers/SdlPen.cs +++ b/sources/Input/Input/Implementations/SDL3/Pointers/SdlPen.cs @@ -18,7 +18,4 @@ public SdlPen(uint sdlDeviceId, SdlInputBackend backend, IReadOnlyList NativeBackend.guid; - - protected override Guid SdlDeviceGuid => throw new NotImplementedException(); } diff --git a/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs b/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs index e3bb909a92..e30cce6fb9 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using Silk.NET.SDL; +using Guid = System.Guid; namespace Silk.NET.Input.SDL3; @@ -147,16 +148,21 @@ protected override void Release() } var joystickUniqueId = joystick.Id; - // manipulate the joystick id to make a unique gamepad id - var uniqueId = joystickUniqueId; - var guid = backend.Sdl.GetGamepadGuidForID(sdlDeviceId); - const ulong gamepadType = (ulong)JoystickType.Gamepad; - const ulong mod = gamepadType << 24; + var gpn = backend.Sdl.GetRealGamepadTypeForID(sdlDeviceId); + + if (backend.AttemptUniqueId(gpn, ref joystickUniqueId)) + { + return new SdlGamepad(joystick, uniqueId: joystickUniqueId); + } - // todo - throw new NotImplementedException(); + var guid = backend.Sdl.GetGamepadGuidForID(sdlDeviceId); + if (backend.AttemptUniqueId(guid, ref joystickUniqueId)) + { + return new SdlGamepad(joystick, uniqueId: joystickUniqueId); + } - return new SdlGamepad(joystick, uniqueId: uniqueId); + joystickUniqueId = backend.FallbackUniqueId(sdlDeviceId, joystickUniqueId); + return new SdlGamepad(joystick, uniqueId: joystickUniqueId); } private void UpdateGamepadAxis(GamepadAxis axis, int value, int min, int max) diff --git a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs index dc501db8f6..cf0cb77ff7 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs @@ -159,6 +159,8 @@ public CustomCursor Image set => throw new NotImplementedException(); } + public HashSet DeviceRegistry { get; } = []; + // This is complicated, as the input proposal mandates that nothing happens until Update is called (so the events // can be received on the given actor) but to also track logical events that happen between calls (i.e. from a // timestamp perspective). Compound this with the fact that the user might do something silly like make multiple diff --git a/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs b/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs index 8c23f8f28d..fa19d590de 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Numerics; +using System.Runtime.CompilerServices; +using System.Text; using Silk.NET.SDL; namespace Silk.NET.Input.SDL3; @@ -11,7 +13,40 @@ internal sealed unsafe partial class SdlJoystick : SdlDevice, IJoystick, ISdlDev public JoystickState State { get; } internal readonly JoystickType JoystickType; internal JoystickHandle JoystickHandle { get; } - public static SdlJoystick CreateDevice(uint sdlDeviceId, SdlInputBackend backend) => new(sdlDeviceId, uniqueId, backend); + + public static SdlJoystick CreateDevice(uint sdlDeviceId, SdlInputBackend backend) + { + nint uniqueId = 0; + + var guid = backend.Sdl.GetJoystickGuidForID(sdlDeviceId); + if (backend.AttemptUniqueId(new ReadOnlySpan(&guid, 16), ref uniqueId)) + { + return new SdlJoystick(sdlDeviceId, uniqueId, backend); + } + + var pathPtr = backend.Sdl.GetJoystickPathForID(sdlDeviceId); + if (backend.AttemptUniqueId(pathPtr, ref uniqueId)) + { + return new SdlJoystick(sdlDeviceId, uniqueId, backend); + } + + var name = backend.Sdl.GetJoystickNameForID(sdlDeviceId); + if (backend.AttemptUniqueId(name, ref uniqueId)) + { + return new SdlJoystick(sdlDeviceId, uniqueId, backend); + } + + var type = backend.Sdl.GetJoystickTypeForID(sdlDeviceId); + if (backend.AttemptUniqueId(type, ref uniqueId)) + { + return new SdlJoystick(sdlDeviceId, uniqueId, backend); + } + + uniqueId = backend.FallbackUniqueId(sdlDeviceId, uniqueId); + return new SdlJoystick(sdlDeviceId, uniqueId, backend); + } + + public override string Name => NativeBackend.GetJoystickNameForID(SdlDeviceId).ReadToString(); public override uint RefreshIdFromBackend() => NativeBackend.GetJoystickID(JoystickHandle); @@ -149,3 +184,58 @@ internal static (float minus, float plus) SplitValue(float mappedValue) } +internal static unsafe class BackendExtensions +{ + public static IntPtr FallbackUniqueId(this SdlInputBackend backend, uint sdlDeviceId, nint uniqueId) + { + Console.Error.WriteLine("Failed to create a deterministically unique identifier for joystick"); + return uniqueId ^ ((nint)sdlDeviceId | ((nint)sdlDeviceId << 16)); + } + + public static bool AttemptUniqueId(this SdlInputBackend sdlInputBackend, Ptr ptr, ref nint uniqueId1) + { + if (ptr.Native == null) + return false; + + var name = ptr.ReadToString(); + var bytes = Encoding.Default.GetBytes(name); + return AttemptUniqueId(sdlInputBackend, bytes, ref uniqueId1); + } + + public static bool AttemptUniqueId(this SdlInputBackend sdlInputBackend, T ptr, ref nint uniqueId1) + where T : unmanaged + { + return AttemptUniqueId(sdlInputBackend, new ReadOnlySpan(&ptr, sizeof(T)), ref uniqueId1); + } + + public static bool AttemptUniqueId(this SdlInputBackend sdlInputBackend, ReadOnlySpan bytes, ref nint uniqueId1) + { + uniqueId1 = Modify(uniqueId1, bytes); + return sdlInputBackend.DeviceRegistry.Add(uniqueId1); + static nint Modify(nint original, ReadOnlySpan withBytes) + { + if (sizeof(nint) == 4) + { + var hash = new HashCode(); + foreach(var b in withBytes) + { + hash.Add(b); + } + + var hashCode = hash.ToHashCode(); + return original ^ *(nint*)(&hashCode); + } + + var hash64Bytes = (byte*)&original; + + for (int i = 0; i < withBytes.Length; i += 8) + { + hash64Bytes[i % 8] ^= withBytes[i]; + } + + return *(nint*)hash64Bytes; + } + + } +} + diff --git a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs index bb82d7537b..5de3985036 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs @@ -9,7 +9,7 @@ namespace Silk.NET.Input.SDL3; internal class SdlKeyboard : SdlDevice, IKeyboard, ISdlDevice { private readonly List> _keyStates; - public unsafe SdlKeyboard(uint sdlDeviceId, SdlInputBackend backend) : base(backend, uniqueId, sdlDeviceId) + public unsafe SdlKeyboard(uint sdlDeviceId, nint uniqueId, SdlInputBackend backend) : base(backend, uniqueId, sdlDeviceId) { _keyStates = new List>((int)Scancode.ScancodeCount); _sdlDeviceId = sdlDeviceId; @@ -21,7 +21,18 @@ public unsafe SdlKeyboard(uint sdlDeviceId, SdlInputBackend backend) : base(back State = new KeyboardState(_keyStates, () => false, () => false);// todo : how do i get the num lock/capslock? } - public static SdlKeyboard CreateDevice(uint sdlDeviceId, SdlInputBackend backend) => throw new NotImplementedException(); + public static SdlKeyboard CreateDevice(uint sdlDeviceId, SdlInputBackend backend) + { + var namePtr = backend.Sdl.GetKeyboardNameForID(sdlDeviceId); + nint uniqueId = 0; + if (backend.AttemptUniqueId(namePtr, ref uniqueId)) + { + return new SdlKeyboard(sdlDeviceId, uniqueId, backend); + } + + uniqueId = backend.FallbackUniqueId(sdlDeviceId, uniqueId); + return new SdlKeyboard(sdlDeviceId, uniqueId, backend); + } public KeyboardState State { get; } protected override void Release() {} // empty? From 7e84c1503929b7923c237bfa83788c6d9d530cd7 Mon Sep 17 00:00:00 2001 From: dom Date: Sat, 6 Sep 2025 10:42:56 -0400 Subject: [PATCH 23/39] move backendextensions to its own file, begin keyboard --- .../Implementations/SDL3/BackendExtensions.cs | 59 +++++++++++++++++++ .../SDL3/Pointers/SdlBoundedPointerDevice.cs | 2 +- .../Input/Implementations/SDL3/SdlGamepad.cs | 1 + .../Input/Implementations/SDL3/SdlJoystick.cs | 57 ------------------ .../Input/Implementations/SDL3/SdlKeyboard.cs | 17 ++++-- 5 files changed, 74 insertions(+), 62 deletions(-) create mode 100644 sources/Input/Input/Implementations/SDL3/BackendExtensions.cs diff --git a/sources/Input/Input/Implementations/SDL3/BackendExtensions.cs b/sources/Input/Input/Implementations/SDL3/BackendExtensions.cs new file mode 100644 index 0000000000..d9249d4fc3 --- /dev/null +++ b/sources/Input/Input/Implementations/SDL3/BackendExtensions.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; + +namespace Silk.NET.Input.SDL3; + +internal static unsafe class BackendExtensions +{ + public static IntPtr FallbackUniqueId(this SdlInputBackend backend, uint sdlDeviceId, nint uniqueId) + { + Console.Error.WriteLine("Failed to create a deterministically unique identifier for joystick"); + return uniqueId ^ ((nint)sdlDeviceId | ((nint)sdlDeviceId << 16)); + } + + public static bool AttemptUniqueId(this SdlInputBackend sdlInputBackend, Ptr ptr, ref nint uniqueId1) + { + if (ptr.Native == null) + return false; + + var name = ptr.ReadToString(); + var bytes = Encoding.Default.GetBytes(name); + return AttemptUniqueId(sdlInputBackend, bytes, ref uniqueId1); + } + + public static bool AttemptUniqueId(this SdlInputBackend sdlInputBackend, T ptr, ref nint uniqueId1) + where T : unmanaged => + AttemptUniqueId(sdlInputBackend, new ReadOnlySpan(&ptr, sizeof(T)), ref uniqueId1); + + public static bool AttemptUniqueId(this SdlInputBackend sdlInputBackend, ReadOnlySpan bytes, ref nint uniqueId1) + { + uniqueId1 = Modify(uniqueId1, bytes); + return sdlInputBackend.DeviceRegistry.Add(uniqueId1); + static nint Modify(nint original, ReadOnlySpan withBytes) + { + if (sizeof(nint) == 4) + { + var hash = new HashCode(); + foreach(var b in withBytes) + { + hash.Add(b); + } + + var hashCode = hash.ToHashCode(); + return original ^ *(nint*)(&hashCode); + } + + var hash64Bytes = (byte*)&original; + + for (int i = 0; i < withBytes.Length; i += 8) + { + hash64Bytes[i % 8] ^= withBytes[i]; + } + + return *(nint*)hash64Bytes; + } + + } +} diff --git a/sources/Input/Input/Implementations/SDL3/Pointers/SdlBoundedPointerDevice.cs b/sources/Input/Input/Implementations/SDL3/Pointers/SdlBoundedPointerDevice.cs index 65922f56e6..b31809a4eb 100644 --- a/sources/Input/Input/Implementations/SDL3/Pointers/SdlBoundedPointerDevice.cs +++ b/sources/Input/Input/Implementations/SDL3/Pointers/SdlBoundedPointerDevice.cs @@ -18,7 +18,7 @@ protected SdlBoundedPointerDevice(SdlInputBackend backend, IReadOnlyList NativeBackend.GetMouseNameForID(SdlDeviceId).ReadToString(); + //public override string Name => NativeBackend.GetMouseNameForID(SdlDeviceId).ReadToString(); [field: MaybeNull] public virtual IReadOnlyList Targets => diff --git a/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs b/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs index e30cce6fb9..bbc3c533af 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs @@ -155,6 +155,7 @@ protected override void Release() return new SdlGamepad(joystick, uniqueId: joystickUniqueId); } + // manipulate the joystick id to make a unique gamepad id var guid = backend.Sdl.GetGamepadGuidForID(sdlDeviceId); if (backend.AttemptUniqueId(guid, ref joystickUniqueId)) { diff --git a/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs b/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs index fa19d590de..fdd198b196 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs @@ -3,7 +3,6 @@ using System.Numerics; using System.Runtime.CompilerServices; -using System.Text; using Silk.NET.SDL; namespace Silk.NET.Input.SDL3; @@ -183,59 +182,3 @@ internal static (float minus, float plus) SplitValue(float mappedValue) ButtonReadOnlyList IButtonDevice.State => State.Buttons; } - -internal static unsafe class BackendExtensions -{ - public static IntPtr FallbackUniqueId(this SdlInputBackend backend, uint sdlDeviceId, nint uniqueId) - { - Console.Error.WriteLine("Failed to create a deterministically unique identifier for joystick"); - return uniqueId ^ ((nint)sdlDeviceId | ((nint)sdlDeviceId << 16)); - } - - public static bool AttemptUniqueId(this SdlInputBackend sdlInputBackend, Ptr ptr, ref nint uniqueId1) - { - if (ptr.Native == null) - return false; - - var name = ptr.ReadToString(); - var bytes = Encoding.Default.GetBytes(name); - return AttemptUniqueId(sdlInputBackend, bytes, ref uniqueId1); - } - - public static bool AttemptUniqueId(this SdlInputBackend sdlInputBackend, T ptr, ref nint uniqueId1) - where T : unmanaged - { - return AttemptUniqueId(sdlInputBackend, new ReadOnlySpan(&ptr, sizeof(T)), ref uniqueId1); - } - - public static bool AttemptUniqueId(this SdlInputBackend sdlInputBackend, ReadOnlySpan bytes, ref nint uniqueId1) - { - uniqueId1 = Modify(uniqueId1, bytes); - return sdlInputBackend.DeviceRegistry.Add(uniqueId1); - static nint Modify(nint original, ReadOnlySpan withBytes) - { - if (sizeof(nint) == 4) - { - var hash = new HashCode(); - foreach(var b in withBytes) - { - hash.Add(b); - } - - var hashCode = hash.ToHashCode(); - return original ^ *(nint*)(&hashCode); - } - - var hash64Bytes = (byte*)&original; - - for (int i = 0; i < withBytes.Length; i += 8) - { - hash64Bytes[i % 8] ^= withBytes[i]; - } - - return *(nint*)hash64Bytes; - } - - } -} - diff --git a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs index 5de3985036..fcd1f78d0c 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs @@ -54,17 +54,26 @@ public string? ClipboardText //throw new NotImplementedException("Setting clipboard text is not implemented in SDL3 backend."); } - public bool TryGetKeyName(KeyName key, [NotNullWhen(true)] out string? name) => - throw new NotImplementedException(); + public bool TryGetKeyName(KeyName key, [NotNullWhen(true)] out string? name) + { + var namePtr = NativeBackend.GetKeyName((uint)key); + name = namePtr.ReadToString(); + return !string.IsNullOrWhiteSpace(name); + } // todo - there should be a backend-independent way to do this text input handling via KeyboardState? public void BeginInput() => throw new NotImplementedException(); public string? EndInput() => throw new NotImplementedException(); - public void AddKeyEvent(KeyboardEvent key) + public void AddKeyEvent(in KeyboardEvent key) { const float fraction = 1f / 255f; - _keyStates[(int)key.Key] = new Button((KeyName)key.Key, key.Down != 0, key.Down * fraction); + _keyStates[(int)key.Key] = new Button(GetKeyName(key.Which), key.Down != 0, key.Down * fraction); + } + + private static KeyName GetKeyName(uint key) + { + return (KeyName)key; } } From 8c5f3da1543d627cfe8afa356fa843d88c560ad7 Mon Sep 17 00:00:00 2001 From: dom Date: Sat, 6 Sep 2025 17:30:50 -0400 Subject: [PATCH 24/39] start key conversions --- .../Input/Implementations/SDL3/SdlKeyboard.cs | 111 +++++++++++++++++- 1 file changed, 107 insertions(+), 4 deletions(-) diff --git a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs index fcd1f78d0c..14678811a1 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using Silk.NET.SDL; namespace Silk.NET.Input.SDL3; @@ -19,6 +20,7 @@ public unsafe SdlKeyboard(uint sdlDeviceId, nint uniqueId, SdlInputBackend backe } State = new KeyboardState(_keyStates, () => false, () => false);// todo : how do i get the num lock/capslock? + _modState = NativeBackend.GetModState(); } public static SdlKeyboard CreateDevice(uint sdlDeviceId, SdlInputBackend backend) @@ -56,24 +58,125 @@ public string? ClipboardText public bool TryGetKeyName(KeyName key, [NotNullWhen(true)] out string? name) { - var namePtr = NativeBackend.GetKeyName((uint)key); + // todo: should 'asKeyEvent' be true? + var sdlKey = KeyNameToSdl(key, NativeBackend, true, _modState); + var namePtr = NativeBackend.GetKeyName(sdlKey); name = namePtr.ReadToString(); return !string.IsNullOrWhiteSpace(name); } + // todo - there should be a backend-independent way to do this text input handling via KeyboardState? public void BeginInput() => throw new NotImplementedException(); public string? EndInput() => throw new NotImplementedException(); + public void UpdateModState() + { + // this mod state is purely used for sdl-related calls - otherwise, we handle the modifier states with our + // standard key handling logic + _modState = NativeBackend.GetModState(); + } + public void AddKeyEvent(in KeyboardEvent key) { const float fraction = 1f / 255f; - _keyStates[(int)key.Key] = new Button(GetKeyName(key.Which), key.Down != 0, key.Down * fraction); + var keyName = ScancodeToKeyName(key.Scancode); // SdlToKeyName(key.Which); + _keyStates[(int)key.Key] = new Button(keyName, key.Down != 0, key.Down * fraction); } - private static KeyName GetKeyName(uint key) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static KeyName ScancodeToKeyName(uint scancode) => (KeyName)scancode; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static KeyName ScancodeToKeyName(Scancode scancode) => ScancodeToKeyName((uint)scancode); + + public static unsafe KeyName SdlToKeyName(uint key, ISdl sdl, ushort? modState = null) { - return (KeyName)key; + modState ??= sdl.GetModState(); + var modStateVal = modState.Value; + return (KeyName)sdl.GetScancodeFromKey(key, &modStateVal); } + + /// + /// Maps an SDL key id to a without a reference to an SDL backend instance. + /// + /// The sdl key id + /// The associated key name + public static KeyName SdlToKeyName(uint key) => + // * indicates a shifted key + key switch { + Sdl.KApplication =>KeyName.Application, + + >= Sdl.K1 and <= Sdl.K9 => (KeyName)(key - _numKeyDiff), + >= Sdl.Ka and <= Sdl.Kz => (KeyName)(key - _letterKeyDiff), + >= Sdl.KCapslock and <= Sdl.KKpEqualsas400 => (KeyName)(key - _systemAndKeypadDiff), + >= Sdl.KCancel and <= Sdl.KRgui => (KeyName)(key - _systemAndKeypadDiff), + >= Sdl.KMode and <= Sdl.KAcBookmarks=> (KeyName)(key - _systemAndKeypadDiff), + >= Sdl.KSoftleft and <= Sdl.KEndcall => (KeyName)(key - _systemNonHidKeyDiff), + + Sdl.KDelete => KeyName.Delete, + Sdl.KUnknown => KeyName.Unknown, + Sdl.KReturn => KeyName.Return, + Sdl.KEscape => KeyName.Escape, + Sdl.KBackspace => KeyName.Backspace, + Sdl.KTab => KeyName.Tab, + Sdl.KSpace => KeyName.Space, + Sdl.KExclaim => KeyName.Number1, // * + Sdl.KDblapostrophe => KeyName.Apostrophe, // * + Sdl.KHash => KeyName.Number3, // * + Sdl.KDollar => KeyName.Number4, // * + Sdl.KPercent => KeyName.Number5, // * + Sdl.KAmpersand => KeyName.KeypadAmpersand, + Sdl.KApostrophe => KeyName.Apostrophe, + Sdl.KLeftparen => KeyName.KeypadLeftParenthesis, + Sdl.KRightparen => KeyName.KeypadRightParenthesis, + Sdl.KAsterisk => KeyName.KeypadAmpersand, + Sdl.KPlus => KeyName.Equals, // * + Sdl.KComma => KeyName.Comma, + Sdl.KMinus => KeyName.Minus, + Sdl.KPeriod => KeyName.Period, + Sdl.KSlash => KeyName.Slash, + Sdl.K0 => KeyName.Number0, + Sdl.KColon => KeyName.Semicolon, // * + Sdl.KSemicolon => KeyName.Semicolon, + Sdl.KLess => KeyName.Comma, // * + Sdl.KEquals => KeyName.Equals, + Sdl.KGreater => KeyName.Period, // * + Sdl.KQuestion => KeyName.Slash, // * + Sdl.KAt => KeyName.Number2, // * + Sdl.KLeftbracket => KeyName.LeftBracket, + Sdl.KBackslash => KeyName.Backslash, + Sdl.KRightbracket => KeyName.RightBracket, + Sdl.KCaret => KeyName.Number6, // * + Sdl.KUnderscore => KeyName.Minus, // * + Sdl.KGrave => KeyName.Grave, + Sdl.KLeftbrace => KeyName.LeftBracket, // * + Sdl.KPipe => KeyName.Backslash, // * + Sdl.KRightbrace => KeyName.RightBracket, // * + Sdl.KTilde => KeyName.Grave, // * + _ => (KeyName)key + }; + + /// + /// The reverse operation of , + /// + /// The name of the key you would like to get an Sdl key id for + /// Sdl backend instance + /// Will this key be used in a key event? + /// The current modifier key state + /// The sdl key id + public static uint KeyNameToSdl(KeyName key, ISdl sdl, bool asKeyEvent, ushort? modState = null) + { + modState ??= sdl.GetModState(); + var scanCode = (uint)key; + var asKeyEventByte = asKeyEvent ? (byte)1 : (byte)0; + return sdl.GetKeyFromScancode((Scancode)scanCode, modState.Value, asKeyEventByte); + } + + private ushort _modState; + private const uint _letterKeyDiff = Sdl.Ka - (uint)KeyName.A; + private const uint _numKeyDiff = Sdl.K1 - (uint)KeyName.Number1; + private const uint _systemAndKeypadDiff = Sdl.KPrintscreen - (uint)KeyName.PrintScreen; + private const uint _systemNonHidKeyDiff = Sdl.KSoftleft - (uint)KeyName.SoftLeft; } From e20d6407036f9672197d11dffac8a0e6cc8c9f92 Mon Sep 17 00:00:00 2001 From: dom Date: Sat, 6 Sep 2025 19:51:00 -0400 Subject: [PATCH 25/39] tweak device id abstraction --- .../Implementations/SDL3/BackendExtensions.cs | 4 ++- .../Implementations/SDL3/IOrderedDevice.cs | 13 ++++++++++ .../Implementations/SDL3/ISdlJoystick.cs | 2 +- .../Input/Implementations/SDL3/SdlDevice.cs | 26 ++++++++----------- .../Input/Implementations/SDL3/SdlGamepad.cs | 6 +++-- .../Input/Implementations/SDL3/SdlJoystick.cs | 11 +++++--- .../Input/Implementations/SDL3/SdlKeyboard.cs | 2 -- 7 files changed, 40 insertions(+), 24 deletions(-) create mode 100644 sources/Input/Input/Implementations/SDL3/IOrderedDevice.cs diff --git a/sources/Input/Input/Implementations/SDL3/BackendExtensions.cs b/sources/Input/Input/Implementations/SDL3/BackendExtensions.cs index d9249d4fc3..01a9d69e94 100644 --- a/sources/Input/Input/Implementations/SDL3/BackendExtensions.cs +++ b/sources/Input/Input/Implementations/SDL3/BackendExtensions.cs @@ -7,7 +7,8 @@ namespace Silk.NET.Input.SDL3; internal static unsafe class BackendExtensions { - public static IntPtr FallbackUniqueId(this SdlInputBackend backend, uint sdlDeviceId, nint uniqueId) + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public static IntPtr FallbackUniqueId(this SdlInputBackend _, uint sdlDeviceId, nint uniqueId) { Console.Error.WriteLine("Failed to create a deterministically unique identifier for joystick"); return uniqueId ^ ((nint)sdlDeviceId | ((nint)sdlDeviceId << 16)); @@ -23,6 +24,7 @@ public static bool AttemptUniqueId(this SdlInputBackend sdlInputBackend, Ptr(this SdlInputBackend sdlInputBackend, T ptr, ref nint uniqueId1) where T : unmanaged => AttemptUniqueId(sdlInputBackend, new ReadOnlySpan(&ptr, sizeof(T)), ref uniqueId1); diff --git a/sources/Input/Input/Implementations/SDL3/IOrderedDevice.cs b/sources/Input/Input/Implementations/SDL3/IOrderedDevice.cs new file mode 100644 index 0000000000..0de1c35560 --- /dev/null +++ b/sources/Input/Input/Implementations/SDL3/IOrderedDevice.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Silk.NET.Input.SDL3; + +/// +/// For devices such as gamepads and joysticks, their SDL IDs are likely to change when other devices +/// are removed. +/// +internal interface IOrderedDevice +{ + public void RefreshSdlId(); +} diff --git a/sources/Input/Input/Implementations/SDL3/ISdlJoystick.cs b/sources/Input/Input/Implementations/SDL3/ISdlJoystick.cs index 7cb6b21ac8..51b0865d97 100644 --- a/sources/Input/Input/Implementations/SDL3/ISdlJoystick.cs +++ b/sources/Input/Input/Implementations/SDL3/ISdlJoystick.cs @@ -11,7 +11,7 @@ namespace Silk.NET.Input.SDL3; /// Currently, only Gamepad is explicitly supported, however this interface leaves room /// for extensions such as those seen in . /// -internal interface ISdlJoystick +internal interface ISdlJoystick : IOrderedDevice { public SdlJoystick Joystick { get; } /// diff --git a/sources/Input/Input/Implementations/SDL3/SdlDevice.cs b/sources/Input/Input/Implementations/SDL3/SdlDevice.cs index cfc7ba94d6..27ae9c9411 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlDevice.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlDevice.cs @@ -19,11 +19,7 @@ bool IEquatable.Equals(IInputDevice? other) => public nint Id { get; } - public uint SdlDeviceId => _sdlDeviceId ??= RefreshIdFromBackend(); - - private uint? _sdlDeviceId; - - public abstract uint RefreshIdFromBackend(); + public virtual uint SdlDeviceId { get; } public SdlInputBackend Backend { get; } @@ -33,21 +29,12 @@ bool IEquatable.Equals(IInputDevice? other) => protected ISdl NativeBackend => Backend.Sdl; public abstract string Name { get; } - /*{ - { - var namePtr = _sdlNameFunc(SdlDeviceId); - ref var casted = ref Unsafe.As(ref namePtr[0]); - var marshalled = SilkMarshal.NativeToString(ref casted); - return marshalled ?? "Unknown Sdl Keyboard"; - } - }*/ - protected SdlDevice(SdlInputBackend backend, nint uniqueId, uint sdlDeviceId) { Backend = backend; Id = uniqueId; - _sdlDeviceId = sdlDeviceId; + SdlDeviceId = sdlDeviceId; } protected abstract void Release(); @@ -57,6 +44,15 @@ public void Dispose() ObjectDisposedException.ThrowIf(_isDisposed, GetType()); _isDisposed = true; Release(); + #if DEBUG + if (!Backend.DeviceRegistry.Remove(Id)) + { + Console.Error.WriteLine($"Failed to remove device {Id} from registry"); + } + #else + Backend.DeviceRegistry.Remove(Id); + #endif + GC.SuppressFinalize(this); } diff --git a/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs b/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs index bbc3c533af..6ed966eb52 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs @@ -3,7 +3,6 @@ using System.Diagnostics; using Silk.NET.SDL; -using Guid = System.Guid; namespace Silk.NET.Input.SDL3; @@ -117,7 +116,10 @@ private void Remap(GamepadHandle gamepadHandle) public void Remap() => Remap(_gamepadHandle); - public override uint RefreshIdFromBackend() => NativeBackend.GetGamepadID(_gamepadHandle); + public override uint SdlDeviceId => _sdlDeviceId; + private uint _sdlDeviceId; + + public void RefreshSdlId() => _sdlDeviceId = NativeBackend.GetGamepadID(_gamepadHandle); public override string Name => Joystick.Name; diff --git a/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs b/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs index fdd198b196..21b1025e44 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs @@ -2,12 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Numerics; -using System.Runtime.CompilerServices; using Silk.NET.SDL; namespace Silk.NET.Input.SDL3; -internal sealed unsafe partial class SdlJoystick : SdlDevice, IJoystick, ISdlDevice +internal sealed unsafe partial class SdlJoystick : SdlDevice, IJoystick, ISdlDevice, IOrderedDevice { public JoystickState State { get; } internal readonly JoystickType JoystickType; @@ -47,12 +46,15 @@ public static SdlJoystick CreateDevice(uint sdlDeviceId, SdlInputBackend backend public override string Name => NativeBackend.GetJoystickNameForID(SdlDeviceId).ReadToString(); - public override uint RefreshIdFromBackend() => NativeBackend.GetJoystickID(JoystickHandle); + + public override uint SdlDeviceId => _sdlDeviceId; + private SdlJoystick(uint sdlDeviceId, nint uniqueId, SdlInputBackend backend) : base(backend, uniqueId, sdlDeviceId) { var joystickHandle = NativeBackend.OpenJoystick(sdlDeviceId); + _sdlDeviceId = sdlDeviceId; if (joystickHandle.Handle == null) { @@ -171,6 +173,9 @@ internal static (float minus, float plus) SplitValue(float mappedValue) protected override void Release() => NativeBackend.CloseJoystick(JoystickHandle); + public void RefreshSdlId() => _sdlDeviceId = NativeBackend.GetJoystickID(JoystickHandle); + private uint _sdlDeviceId; + // State private readonly Button[] _rawButtonState; private readonly float[] _rawAxisState; diff --git a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs index 14678811a1..1473d68af0 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs @@ -39,8 +39,6 @@ public static SdlKeyboard CreateDevice(uint sdlDeviceId, SdlInputBackend backend public KeyboardState State { get; } protected override void Release() {} // empty? - private readonly uint _sdlDeviceId; - public override uint RefreshIdFromBackend() => _sdlDeviceId; public override string Name => NativeBackend.GetKeyboardNameForID(SdlDeviceId).ReadToString(); public string? ClipboardText From 4387adbfc9ca8f384a1914be8d50df37804c48e9 Mon Sep 17 00:00:00 2001 From: dom Date: Sat, 6 Sep 2025 21:02:34 -0400 Subject: [PATCH 26/39] begin keyboard text handling --- .../Implementations/SDL3/SdlInputBackend.cs | 8 + .../SDL3/SdlKeyboard.KeyNames.cs | 99 ++++++++++ .../Input/Implementations/SDL3/SdlKeyboard.cs | 178 ++++++------------ .../Input/Implementations/TextRecorder.cs | 124 ++++++++++++ sources/Input/Input/KeyboardState.cs | 4 +- 5 files changed, 291 insertions(+), 122 deletions(-) create mode 100644 sources/Input/Input/Implementations/SDL3/SdlKeyboard.KeyNames.cs create mode 100644 sources/Input/Input/Implementations/TextRecorder.cs diff --git a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs index cf0cb77ff7..517409fd09 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs @@ -186,6 +186,14 @@ public void Update(IInputHandler? handler = null) { ProcessEvent(ref evt, handler); } + + foreach (var device in _devices) + { + if (device is SdlKeyboard keyboard) + { + keyboard.UpdateModState(); + } + } } private enum QueuedEventType : byte diff --git a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.KeyNames.cs b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.KeyNames.cs new file mode 100644 index 0000000000..77a60f3a53 --- /dev/null +++ b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.KeyNames.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using Silk.NET.SDL; + +namespace Silk.NET.Input.SDL3; + +internal partial class SdlKeyboard +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static KeyName ScancodeToKeyName(uint scancode) => (KeyName)scancode; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static KeyName ScancodeToKeyName(Scancode scancode) => ScancodeToKeyName((uint)scancode); + + public static unsafe KeyName SdlToKeyName(uint key, ISdl sdl, ushort? modState = null) + { + modState ??= sdl.GetModState(); + var modStateVal = modState.Value; + return (KeyName)sdl.GetScancodeFromKey(key, &modStateVal); + } + + /// + /// Maps an SDL key id to a without a reference to an SDL backend instance. + /// + /// The sdl key id + /// The associated key name + public static KeyName SdlToKeyName(uint key) => + // * indicates a shifted key + key switch { + Sdl.KApplication => KeyName.Application, + + >= Sdl.K1 and <= Sdl.K9 => (KeyName)(key - _numKeyDiff), + >= Sdl.Ka and <= Sdl.Kz => (KeyName)(key - _letterKeyDiff), + >= Sdl.KCapslock and <= Sdl.KKpEqualsas400 => (KeyName)(key - _systemAndKeypadDiff), + >= Sdl.KCancel and <= Sdl.KRgui => (KeyName)(key - _systemAndKeypadDiff), + >= Sdl.KMode and <= Sdl.KAcBookmarks => (KeyName)(key - _systemAndKeypadDiff), + >= Sdl.KSoftleft and <= Sdl.KEndcall => (KeyName)(key - _systemNonHidKeyDiff), + + Sdl.KDelete => KeyName.Delete, + Sdl.KUnknown => KeyName.Unknown, + Sdl.KReturn => KeyName.Return, + Sdl.KEscape => KeyName.Escape, + Sdl.KBackspace => KeyName.Backspace, + Sdl.KTab => KeyName.Tab, + Sdl.KSpace => KeyName.Space, + Sdl.KExclaim => KeyName.Number1, // * + Sdl.KDblapostrophe => KeyName.Apostrophe, // * + Sdl.KHash => KeyName.Number3, // * + Sdl.KDollar => KeyName.Number4, // * + Sdl.KPercent => KeyName.Number5, // * + Sdl.KAmpersand => KeyName.KeypadAmpersand, + Sdl.KApostrophe => KeyName.Apostrophe, + Sdl.KLeftparen => KeyName.KeypadLeftParenthesis, + Sdl.KRightparen => KeyName.KeypadRightParenthesis, + Sdl.KAsterisk => KeyName.KeypadAmpersand, + Sdl.KPlus => KeyName.Equals, // * + Sdl.KComma => KeyName.Comma, + Sdl.KMinus => KeyName.Minus, + Sdl.KPeriod => KeyName.Period, + Sdl.KSlash => KeyName.Slash, + Sdl.K0 => KeyName.Number0, + Sdl.KColon => KeyName.Semicolon, // * + Sdl.KSemicolon => KeyName.Semicolon, + Sdl.KLess => KeyName.Comma, // * + Sdl.KEquals => KeyName.Equals, + Sdl.KGreater => KeyName.Period, // * + Sdl.KQuestion => KeyName.Slash, // * + Sdl.KAt => KeyName.Number2, // * + Sdl.KLeftbracket => KeyName.LeftBracket, + Sdl.KBackslash => KeyName.Backslash, + Sdl.KRightbracket => KeyName.RightBracket, + Sdl.KCaret => KeyName.Number6, // * + Sdl.KUnderscore => KeyName.Minus, // * + Sdl.KGrave => KeyName.Grave, + Sdl.KLeftbrace => KeyName.LeftBracket, // * + Sdl.KPipe => KeyName.Backslash, // * + Sdl.KRightbrace => KeyName.RightBracket, // * + Sdl.KTilde => KeyName.Grave, // * + _ => (KeyName)key + }; + + /// + /// The reverse operation of , + /// + /// The name of the key you would like to get an Sdl key id for + /// Sdl backend instance + /// Will this key be used in a key event? + /// The current modifier key state + /// The sdl key id + public static uint KeyNameToSdl(KeyName key, ISdl sdl, bool asKeyEvent, ushort? modState = null) + { + modState ??= sdl.GetModState(); + var scanCode = (uint)key; + var asKeyEventByte = asKeyEvent ? (byte)1 : (byte)0; + return sdl.GetKeyFromScancode((Scancode)scanCode, modState.Value, asKeyEventByte); + } +} diff --git a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs index 1473d68af0..7a2e5e0856 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs @@ -2,25 +2,18 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; using Silk.NET.SDL; namespace Silk.NET.Input.SDL3; -internal class SdlKeyboard : SdlDevice, IKeyboard, ISdlDevice +internal partial class SdlKeyboard : SdlDevice, IKeyboard, ISdlDevice { - private readonly List> _keyStates; - public unsafe SdlKeyboard(uint sdlDeviceId, nint uniqueId, SdlInputBackend backend) : base(backend, uniqueId, sdlDeviceId) + public KeyboardState State { get; } + public override string Name => NativeBackend.GetKeyboardNameForID(SdlDeviceId).ReadToString(); + public string? ClipboardText { - _keyStates = new List>((int)Scancode.ScancodeCount); - _sdlDeviceId = sdlDeviceId; - for (var i = 0; i < 512; i++) - { - _keyStates.Add(new Button((KeyName)i, false, 0f)); - } - - State = new KeyboardState(_keyStates, () => false, () => false);// todo : how do i get the num lock/capslock? - _modState = NativeBackend.GetModState(); + get => NativeBackend.HasClipboardText() ? NativeBackend.GetClipboardText().ReadToString() : null; + set => NativeBackend.SetClipboardText(value); } public static SdlKeyboard CreateDevice(uint sdlDeviceId, SdlInputBackend backend) @@ -36,22 +29,31 @@ public static SdlKeyboard CreateDevice(uint sdlDeviceId, SdlInputBackend backend return new SdlKeyboard(sdlDeviceId, uniqueId, backend); } - public KeyboardState State { get; } - protected override void Release() {} // empty? - - - public override string Name => NativeBackend.GetKeyboardNameForID(SdlDeviceId).ReadToString(); - public string? ClipboardText + private SdlKeyboard(uint sdlDeviceId, nint uniqueId, SdlInputBackend backend) : base(backend, uniqueId, sdlDeviceId) { - get + Span> keyStates = stackalloc Button[(int)KeyName.EndCall + 1]; + int keyCount = 0; + for (var i = 0; i < 512; i++) { - if(Sdl.Instance.HasClipboardText() == 0) - return null; - - return Sdl.Instance.GetClipboardText().ReadToString(); + var keyName = (KeyName)i; + if (Enum.IsDefined(keyName)) + { + keyStates[keyCount++] = new Button((KeyName)i, false, 0f); + } } - set => Sdl.Instance.SetClipboardText(value); - //throw new NotImplementedException("Setting clipboard text is not implemented in SDL3 backend."); + + _keyStates = keyStates[..keyCount].ToArray(); + _modState = NativeBackend.GetModState(); + + State = new KeyboardState( + keys: _keyStates, + capsLockActive: () => (_modState & Sdl.KmodCaps) == Sdl.KmodCaps, + numLockActive: () => (_modState & Sdl.KmodNum) == Sdl.KmodNum); + } + + + protected override void Release() + { } public bool TryGetKeyName(KeyName key, [NotNullWhen(true)] out string? name) @@ -65,114 +67,50 @@ public bool TryGetKeyName(KeyName key, [NotNullWhen(true)] out string? name) // todo - there should be a backend-independent way to do this text input handling via KeyboardState? - public void BeginInput() => throw new NotImplementedException(); - - public string? EndInput() => throw new NotImplementedException(); + public void BeginInput() + { + _textIsRecording = true; + _textRecorder ??= new TextRecorder(); + } - public void UpdateModState() + public string? EndInput() { - // this mod state is purely used for sdl-related calls - otherwise, we handle the modifier states with our - // standard key handling logic - _modState = NativeBackend.GetModState(); + _textIsRecording = false; + return _textRecorder?.ConsumeInput(); } + /// + /// Updates the internal modifier state. + /// + /// + /// This should be called every frame the keyboard is updated in . + /// This mod state is purely used for sdl-related calls and modifiers that are independent of key state (e.g. numlock, caps lock) + /// - otherwise, we handle the modifier states with our standard key handling logic + /// + public void UpdateModState() => _modState = NativeBackend.GetModState(); + public void AddKeyEvent(in KeyboardEvent key) { const float fraction = 1f / 255f; var keyName = ScancodeToKeyName(key.Scancode); // SdlToKeyName(key.Which); - _keyStates[(int)key.Key] = new Button(keyName, key.Down != 0, key.Down * fraction); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static KeyName ScancodeToKeyName(uint scancode) => (KeyName)scancode; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static KeyName ScancodeToKeyName(Scancode scancode) => ScancodeToKeyName((uint)scancode); + if (Enum.IsDefined(keyName)) + { + var index = EnumInfo.ValueIndexOf(keyName); + _keyStates[index] = new Button(keyName, key.Down != 0, key.Down * fraction); - public static unsafe KeyName SdlToKeyName(uint key, ISdl sdl, ushort? modState = null) - { - modState ??= sdl.GetModState(); - var modStateVal = modState.Value; - return (KeyName)sdl.GetScancodeFromKey(key, &modStateVal); + if (_textIsRecording) + { + _textRecorder!.AddKeyStroke(keyName, key.Down != 0); + } + } } - /// - /// Maps an SDL key id to a without a reference to an SDL backend instance. - /// - /// The sdl key id - /// The associated key name - public static KeyName SdlToKeyName(uint key) => - // * indicates a shifted key - key switch { - Sdl.KApplication =>KeyName.Application, - - >= Sdl.K1 and <= Sdl.K9 => (KeyName)(key - _numKeyDiff), - >= Sdl.Ka and <= Sdl.Kz => (KeyName)(key - _letterKeyDiff), - >= Sdl.KCapslock and <= Sdl.KKpEqualsas400 => (KeyName)(key - _systemAndKeypadDiff), - >= Sdl.KCancel and <= Sdl.KRgui => (KeyName)(key - _systemAndKeypadDiff), - >= Sdl.KMode and <= Sdl.KAcBookmarks=> (KeyName)(key - _systemAndKeypadDiff), - >= Sdl.KSoftleft and <= Sdl.KEndcall => (KeyName)(key - _systemNonHidKeyDiff), - - Sdl.KDelete => KeyName.Delete, - Sdl.KUnknown => KeyName.Unknown, - Sdl.KReturn => KeyName.Return, - Sdl.KEscape => KeyName.Escape, - Sdl.KBackspace => KeyName.Backspace, - Sdl.KTab => KeyName.Tab, - Sdl.KSpace => KeyName.Space, - Sdl.KExclaim => KeyName.Number1, // * - Sdl.KDblapostrophe => KeyName.Apostrophe, // * - Sdl.KHash => KeyName.Number3, // * - Sdl.KDollar => KeyName.Number4, // * - Sdl.KPercent => KeyName.Number5, // * - Sdl.KAmpersand => KeyName.KeypadAmpersand, - Sdl.KApostrophe => KeyName.Apostrophe, - Sdl.KLeftparen => KeyName.KeypadLeftParenthesis, - Sdl.KRightparen => KeyName.KeypadRightParenthesis, - Sdl.KAsterisk => KeyName.KeypadAmpersand, - Sdl.KPlus => KeyName.Equals, // * - Sdl.KComma => KeyName.Comma, - Sdl.KMinus => KeyName.Minus, - Sdl.KPeriod => KeyName.Period, - Sdl.KSlash => KeyName.Slash, - Sdl.K0 => KeyName.Number0, - Sdl.KColon => KeyName.Semicolon, // * - Sdl.KSemicolon => KeyName.Semicolon, - Sdl.KLess => KeyName.Comma, // * - Sdl.KEquals => KeyName.Equals, - Sdl.KGreater => KeyName.Period, // * - Sdl.KQuestion => KeyName.Slash, // * - Sdl.KAt => KeyName.Number2, // * - Sdl.KLeftbracket => KeyName.LeftBracket, - Sdl.KBackslash => KeyName.Backslash, - Sdl.KRightbracket => KeyName.RightBracket, - Sdl.KCaret => KeyName.Number6, // * - Sdl.KUnderscore => KeyName.Minus, // * - Sdl.KGrave => KeyName.Grave, - Sdl.KLeftbrace => KeyName.LeftBracket, // * - Sdl.KPipe => KeyName.Backslash, // * - Sdl.KRightbrace => KeyName.RightBracket, // * - Sdl.KTilde => KeyName.Grave, // * - _ => (KeyName)key - }; - - /// - /// The reverse operation of , - /// - /// The name of the key you would like to get an Sdl key id for - /// Sdl backend instance - /// Will this key be used in a key event? - /// The current modifier key state - /// The sdl key id - public static uint KeyNameToSdl(KeyName key, ISdl sdl, bool asKeyEvent, ushort? modState = null) - { - modState ??= sdl.GetModState(); - var scanCode = (uint)key; - var asKeyEventByte = asKeyEvent ? (byte)1 : (byte)0; - return sdl.GetKeyFromScancode((Scancode)scanCode, modState.Value, asKeyEventByte); - } + private TextRecorder? _textRecorder; + private bool _textIsRecording; private ushort _modState; + private readonly Button[] _keyStates; private const uint _letterKeyDiff = Sdl.Ka - (uint)KeyName.A; private const uint _numKeyDiff = Sdl.K1 - (uint)KeyName.Number1; private const uint _systemAndKeypadDiff = Sdl.KPrintscreen - (uint)KeyName.PrintScreen; diff --git a/sources/Input/Input/Implementations/TextRecorder.cs b/sources/Input/Input/Implementations/TextRecorder.cs new file mode 100644 index 0000000000..e930cb859d --- /dev/null +++ b/sources/Input/Input/Implementations/TextRecorder.cs @@ -0,0 +1,124 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Silk.NET.SDL; + +namespace Silk.NET.Input; + +/// +/// A utility class for recording text input. +/// Where possible, it may be preferable to use and +/// instead, but this requires the use of the SDL windowing API, which may not be available in all contexts. +/// This class is a work in progress, and not yet sufficient for full text-editor support. +/// +public class TextRecorder +{ + public void AddKeyStroke(KeyName name, bool isDown, IKeyboard keyboard) + { + var isChar = name.IsChar(); + var isDeletion = name.IsDeletion(); + + if (!isChar && !isDeletion) + { + return; + } + + if (name == KeyName.Paste) + { + var clipboardText = keyboard.ClipboardText; + if (!string.IsNullOrEmpty(clipboardText)) + { + OverwriteSelectionBeforeInsert(); + InsertString(clipboardText); + } + + return; + } + + var state = keyboard.State; + var activeModifiers = state.Modifiers; + + } + + private void OverwriteSelectionBeforeInsert() + { + if (_cursorStart == _cursorEnd) + { + return; + } + + _sb.Remove(_cursorStart, _cursorEnd - _cursorStart); + _cursorEnd = _cursorStart; + } + + public void InsertString(string str) + { + _sb.Insert(_cursorStart, str); + _cursorEnd = _cursorStart += str.Length; + } + + public void AddChar(char c) + { + OverwriteSelectionBeforeInsert(); + _sb.Insert(_cursorStart, c); + _cursorEnd = _cursorStart += 1; + } + + public string? ConsumeInput() + { + var result = _sb.ToString(); + _sb.Clear(); + return result; + } + + public void GetCurrentBuffer(Span buffer) + { + var maxCount = Math.Min(buffer.Length, _sb.Length); + _sb.CopyTo(0, buffer, maxCount); + } + + public void GetSelectedRegion(Span buffer) + { + + } + + public int Count => _sb.Length; + + private int _cursorStart, _cursorEnd; + private readonly StringBuilder _sb = new(); +} + +/// +/// A series of extension methods for making sense of values +/// +public static class KeyNameExtensions +{ + /// + /// Returns true if the key would produce a character in common text editing scenarios. Includes whitespace. + /// + public static bool IsChar(this KeyName name) => + name is >= KeyName.A and <= KeyName.Return + or >= KeyName.KeypadDivide and <= KeyName.KeypadPeriod + or >= KeyName.Tab and <= KeyName.Slash + or >= KeyName.KeypadMultiply and <= KeyName.KeypadEnter + or >= KeyName.KeypadA and <= KeyName.KeypadExclamation + or >= KeyName.Keypad00 and <= KeyName.KeypadTab + or KeyName.Return2 or KeyName.Separator or KeyName.KeypadPlusMinus + or KeyName.KeypadComma + or KeyName.KeypadEquals or KeyName.OtherKeypadEquals; + + /// + /// Returns true if the given key would produce a deletion of one or more characters in common text + /// editing scenarios. + /// + public static bool IsDeletion(this KeyName name) => + name is KeyName.Backspace + or KeyName.Delete + or KeyName.KeypadBackspace + or KeyName.Clear + or KeyName.KeypadClear + or KeyName.KeypadClearEntry + or KeyName.ClearAgain; + +} diff --git a/sources/Input/Input/KeyboardState.cs b/sources/Input/Input/KeyboardState.cs index ed3778f660..78ccbbc0f7 100644 --- a/sources/Input/Input/KeyboardState.cs +++ b/sources/Input/Input/KeyboardState.cs @@ -65,9 +65,9 @@ public KeyModifiers Modifiers /// The collection of keys that are modified at runtime to give the current keyboard its state /// Return true if caps lock is currently active, irrespective of pressed status /// Return true if num lock is currently active, irrespective of pressed status - public KeyboardState(List> keys, Func capsLockActive, Func numLockActive) + public KeyboardState(IReadOnlyList> keys, Func capsLockActive, Func numLockActive) { - Keys = new(keys); + Keys = new ButtonReadOnlyList(keys); _capsLockActive = capsLockActive; _numLockActive = numLockActive; } From 35677b07a15df500fe2f12674830767f6779e020 Mon Sep 17 00:00:00 2001 From: dom Date: Sat, 6 Sep 2025 21:04:48 -0400 Subject: [PATCH 27/39] improve text retrieval functions --- sources/Input/Input/Implementations/TextRecorder.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/sources/Input/Input/Implementations/TextRecorder.cs b/sources/Input/Input/Implementations/TextRecorder.cs index e930cb859d..7eadc157dd 100644 --- a/sources/Input/Input/Implementations/TextRecorder.cs +++ b/sources/Input/Input/Implementations/TextRecorder.cs @@ -72,15 +72,23 @@ public void AddChar(char c) return result; } - public void GetCurrentBuffer(Span buffer) + public int GetCurrentBuffer(Span buffer) { var maxCount = Math.Min(buffer.Length, _sb.Length); _sb.CopyTo(0, buffer, maxCount); + return maxCount; } - public void GetSelectedRegion(Span buffer) + public int GetSelectedRegion(Span buffer) { + var maxCount = Math.Min(buffer.Length, _cursorEnd - _cursorStart); + if (maxCount == 0) + { + return 0; + } + _sb.CopyTo(_cursorStart, buffer, maxCount); + return maxCount; } public int Count => _sb.Length; From e0abc159513df2a510f8b70f1ba58bc5ce06be34 Mon Sep 17 00:00:00 2001 From: dom Date: Sat, 6 Sep 2025 21:30:03 -0400 Subject: [PATCH 28/39] continue text recorder --- .../Input/Implementations/TextRecorder.cs | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/sources/Input/Input/Implementations/TextRecorder.cs b/sources/Input/Input/Implementations/TextRecorder.cs index 7eadc157dd..222c973069 100644 --- a/sources/Input/Input/Implementations/TextRecorder.cs +++ b/sources/Input/Input/Implementations/TextRecorder.cs @@ -68,10 +68,23 @@ public void AddChar(char c) public string? ConsumeInput() { var result = _sb.ToString(); - _sb.Clear(); + Clear(); return result; } + public void Clear() + { + _sb.Clear(); + _cursorStart = 0; + _cursorEnd = 0; + } + + public void SetCursorPosition(int position, int? endPosition = null) + { + _cursorStart = position; + _cursorEnd = endPosition ?? position; + } + public int GetCurrentBuffer(Span buffer) { var maxCount = Math.Min(buffer.Length, _sb.Length); @@ -129,4 +142,11 @@ or KeyName.KeypadClear or KeyName.KeypadClearEntry or KeyName.ClearAgain; + /// + /// Returns true if the modifiers signify that the next character should be capitalized. + /// + public static bool ShouldCapitalize(this KeyModifiers modifiers) => + ((modifiers & KeyModifiers.CapsLock) == KeyModifiers.CapsLock) ^ + ((modifiers & KeyModifiers.ShiftLeft) == KeyModifiers.ShiftLeft + || (modifiers & KeyModifiers.ShiftRight) == KeyModifiers.ShiftRight); } From c7159ad20e7ec0d2e6d5ec126b263ebbe2cfab59 Mon Sep 17 00:00:00 2001 From: dom Date: Fri, 12 Sep 2025 10:06:06 -0400 Subject: [PATCH 29/39] begin integration with sdl native text entry --- .../Implementations/SDL3/SdlInputBackend.cs | 2 + .../Input/Implementations/SDL3/SdlKeyboard.cs | 86 +++++++++++++++++-- .../Input/Implementations/TextRecorder.cs | 2 + 3 files changed, 83 insertions(+), 7 deletions(-) diff --git a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs index 517409fd09..dbdef63346 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs @@ -21,6 +21,8 @@ internal class SdlInputBackend : IInputBackend, ICursorConfiguration private WindowHandle _focusedWindow; private ISdl _sdl; + public unsafe WindowHandle? FocusedWindow => _focusedWindow.Handle == null ? null : _focusedWindow; + public unsafe SdlInputBackend(SdlPlatformInfo info) { ArgumentNullException.ThrowIfNull(info.Sdl); diff --git a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs index 7a2e5e0856..09fb260c69 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs @@ -69,13 +69,42 @@ public bool TryGetKeyName(KeyName key, [NotNullWhen(true)] out string? name) // todo - there should be a backend-independent way to do this text input handling via KeyboardState? public void BeginInput() { - _textIsRecording = true; - _textRecorder ??= new TextRecorder(); + var sdlWindow = Backend.FocusedWindow; + if (sdlWindow != null && NativeBackend.StartTextInput(sdlWindow.Value)) + { + BeginRecordingSdl(sdlWindow.Value); + } + else + { + _textIsRecording = TextRecorderState.Recording; + } + } + + private void BeginRecordingSdl(WindowHandle sdlWindow) + { + _textIsRecording = TextRecorderState.RecordingSdl; + _textEntryWindow = sdlWindow; } - public string? EndInput() + public unsafe string? EndInput() { - _textIsRecording = false; + switch (_textIsRecording) + { + case TextRecorderState.None: + return null; + case TextRecorderState.Recording: + _textIsRecording = TextRecorderState.None; + break; + case TextRecorderState.RecordingSdl: + _textIsRecording = TextRecorderState.None; + var sdlWindow = _textEntryWindow; + if (sdlWindow != null) + { + NativeBackend.StopTextInput(sdlWindow.Value); + } + break; + } + _textIsRecording = TextRecorderState.None; return _textRecorder?.ConsumeInput(); } @@ -99,16 +128,59 @@ public void AddKeyEvent(in KeyboardEvent key) var index = EnumInfo.ValueIndexOf(keyName); _keyStates[index] = new Button(keyName, key.Down != 0, key.Down * fraction); - if (_textIsRecording) + if (_textIsRecording == TextRecorderState.Recording) { - _textRecorder!.AddKeyStroke(keyName, key.Down != 0); + _textRecorder ??= new TextRecorder(); + _textRecorder!.AddKeyStroke(keyName, key.Down != 0, this); } } } + public unsafe void AddTextEditingEvent(in TextEditingEvent evt) => throw new NotImplementedException(); + public unsafe void AddTextCandidatesEvent(in TextEditingCandidatesEvent evt) => throw new NotImplementedException(); + + public unsafe void AddTextInputEvent(in TextInputEvent evt) + { + if (_textEntryWindow == null) + { + Console.Out.WriteLine("Unexpected text input event"); + var windowHandle = NativeBackend.GetWindowFromID(evt.WindowID); + if (windowHandle.Handle != null) + { + BeginRecordingSdl(windowHandle); + } + + return; + } + + if (evt.WindowID != NativeBackend.GetWindowID(_textEntryWindow.Value)) + { + Console.Error.WriteLine("Received text input event for a different window than the " + + "one we're recording text for."); + return; + } + + if (evt.Text == null) + { + return; + } + + var str = new Ptr(evt.Text).ReadToString(); + + if (string.IsNullOrEmpty(str)) + { + return; + } + + _textRecorder ??= new TextRecorder(); + _textRecorder.AddString(evt); + } + + private WindowHandle? _textEntryWindow; private TextRecorder? _textRecorder; - private bool _textIsRecording; + private enum TextRecorderState {None, Recording, RecordingSdl} + private TextRecorderState _textIsRecording; private ushort _modState; private readonly Button[] _keyStates; private const uint _letterKeyDiff = Sdl.Ka - (uint)KeyName.A; diff --git a/sources/Input/Input/Implementations/TextRecorder.cs b/sources/Input/Input/Implementations/TextRecorder.cs index 222c973069..d14afcec77 100644 --- a/sources/Input/Input/Implementations/TextRecorder.cs +++ b/sources/Input/Input/Implementations/TextRecorder.cs @@ -108,6 +108,8 @@ public int GetSelectedRegion(Span buffer) private int _cursorStart, _cursorEnd; private readonly StringBuilder _sb = new(); + + public void AddString(in TextInputEvent text) => _sb.Append(text); } /// From 436aa72b881d8b886b281d489a4ce34d2f01af7c Mon Sep 17 00:00:00 2001 From: dom Date: Sun, 14 Sep 2025 14:02:27 -0400 Subject: [PATCH 30/39] add span utility --- .../Core/Core/Pointers/PointerExtensions.cs | 38 +++++++++++++++++++ .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 1 + 2 files changed, 39 insertions(+) diff --git a/sources/Core/Core/Pointers/PointerExtensions.cs b/sources/Core/Core/Pointers/PointerExtensions.cs index cfacf2de27..41f57aa519 100644 --- a/sources/Core/Core/Pointers/PointerExtensions.cs +++ b/sources/Core/Core/Pointers/PointerExtensions.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; @@ -232,6 +233,43 @@ public static string ReadToString(this Ptr @this) } } + /// + /// Populates the given span with the characters of this as a c-style string. + /// + /// + /// The span to populate characters into + /// True if the given span is of sufficient length and can be filled - false otherwise, in which case + /// no data has been modified in the given span + public static bool TryReadToSpan(this Ptr @this, ref Span span) + { + fixed (void* raw = @this) + { + var bytes = MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)raw); + var count = Encoding.UTF8.GetCharCount(bytes); + if (span.Length < count) + { + return false; + } + + #if DEBUG + // This if-def is here to prevent this constant string from taking up space in extremely constrained + // release environments. + const string assertionLog = $"{nameof(Encoding)}.{nameof(Encoding.UTF8)}." + + $"{nameof(Encoding.UTF8.GetChars)}) returned an unexpected number of " + + $"characters"; + + var charCount = Encoding.UTF8.GetChars(bytes, span); + Debug.Assert(charCount == count, assertionLog);; + #else + Encoding.UTF8.GetChars(bytes, span); + #endif + + span = span[..count]; + return true; + } + } + + /// /// Creates a string from this with the given length /// diff --git a/sources/Core/Core/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/sources/Core/Core/PublicAPI/net8.0/PublicAPI.Unshipped.txt index 14eb48880a..2daf4e53ee 100644 --- a/sources/Core/Core/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/sources/Core/Core/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -414,6 +414,7 @@ static Silk.NET.Core.PointerExtensions.ReadToStringArray(this Silk.NET.Core.Ref3 static Silk.NET.Core.PointerExtensions.ReadToStringArray(this Silk.NET.Core.Ref3D this, int length, int[]! lengths) -> string?[]?[]? static Silk.NET.Core.PointerExtensions.ReadToStringArray(this Silk.NET.Core.Ref3D this, int length, int[]! lengths) -> string?[]?[]? static Silk.NET.Core.PointerExtensions.ReadToStringArray(this Silk.NET.Core.Ref3D this, int length, int[]! lengths) -> string?[]?[]? +static Silk.NET.Core.PointerExtensions.TryReadToSpan(this Silk.NET.Core.Ptr this, ref System.Span span) -> bool static Silk.NET.Core.Ptr.explicit operator nint(Silk.NET.Core.Ptr ptr) -> nint static Silk.NET.Core.Ptr.explicit operator Silk.NET.Core.Ptr(nint ptr) -> Silk.NET.Core.Ptr static Silk.NET.Core.Ptr.explicit operator Silk.NET.Core.Ptr(Silk.NET.Core.Ref ptr) -> Silk.NET.Core.Ptr From a35120ac3090283c47d99ec54d08c97d528f461d Mon Sep 17 00:00:00 2001 From: dom Date: Sun, 14 Sep 2025 14:02:56 -0400 Subject: [PATCH 31/39] text entry continued --- .../Implementations/ICharacterConverter.cs | 39 ++ .../Implementations/KeyNameExtensions.cs | 97 +++++ .../Implementations/SDL3/SdlInputBackend.cs | 7 +- .../Input/Implementations/SDL3/SdlKeyboard.cs | 82 +++- .../Input/Implementations/TextRecorder.cs | 380 ++++++++++++++---- 5 files changed, 510 insertions(+), 95 deletions(-) create mode 100644 sources/Input/Input/Implementations/ICharacterConverter.cs create mode 100644 sources/Input/Input/Implementations/KeyNameExtensions.cs diff --git a/sources/Input/Input/Implementations/ICharacterConverter.cs b/sources/Input/Input/Implementations/ICharacterConverter.cs new file mode 100644 index 0000000000..9442d08b73 --- /dev/null +++ b/sources/Input/Input/Implementations/ICharacterConverter.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Globalization; + +namespace Silk.NET.Input; + +/// +/// A simple interface for an implementation that converts keyboard input into characters for text entry +/// +public interface ICharacterConverter +{ + public bool TryConvert(KeyName key, KeyModifiers modifiers, [NotNullWhen(true)] out char? c); +} + + +public class DummyCharConverter: ICharacterConverter +{ + private static CultureInfo Culture => CultureInfo.CurrentUICulture; + private static int Layout => Culture.KeyboardLayoutId; + public bool TryConvert(KeyName key, KeyModifiers modifiers, [NotNullWhen(true)] out char? c) + { + if (!key.IsChar()) + { + c = null; + return false; + } + + char resultBeforeProcessing = '\0'; + bool isLetter = false; + var isShifted = modifiers.IsShift(); + switch (key) + { + case KeyName.A: + + } + } +} diff --git a/sources/Input/Input/Implementations/KeyNameExtensions.cs b/sources/Input/Input/Implementations/KeyNameExtensions.cs new file mode 100644 index 0000000000..29c91c93ab --- /dev/null +++ b/sources/Input/Input/Implementations/KeyNameExtensions.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace Silk.NET.Input; + +/// +/// A series of extension methods for making sense of values +/// +public static class KeyNameExtensions +{ + /// + /// Returns true if the key would produce a character in common text editing scenarios. Includes whitespace. + /// + public static bool IsChar(this KeyName name) => + name is >= KeyName.A and <= KeyName.Return + or >= KeyName.KeypadDivide and <= KeyName.KeypadPeriod + or >= KeyName.Tab and <= KeyName.Slash + or >= KeyName.KeypadMultiply and <= KeyName.KeypadEnter + or >= KeyName.KeypadA and <= KeyName.KeypadExclamation + or >= KeyName.Keypad00 and <= KeyName.KeypadTab + or KeyName.Return2 or KeyName.Separator or KeyName.KeypadPlusMinus + or KeyName.KeypadComma + or KeyName.KeypadEquals or KeyName.OtherKeypadEquals; + + /// + /// Returns true if the given key would produce a deletion of one or more characters in common text + /// editing scenarios. + /// + public static bool IsDeletion(this KeyName name) => + name is KeyName.Backspace + or KeyName.Delete + or KeyName.KeypadBackspace + or KeyName.Clear + or KeyName.KeypadClear + or KeyName.KeypadClearEntry + or KeyName.ClearAgain; + + public static TextDeletionType GetDeletionType(this KeyName name) + { + Debug.Assert(name.IsDeletion()); + + return name switch { + KeyName.Backspace => TextDeletionType.Back, + KeyName.Delete => TextDeletionType.Forward, + KeyName.KeypadBackspace => TextDeletionType.Back, + KeyName.Clear => TextDeletionType.All, + KeyName.KeypadClear => TextDeletionType.All, + KeyName.KeypadClearEntry => TextDeletionType.All, + KeyName.ClearAgain => TextDeletionType.All, + _ => TextDeletionType.None + }; + } + + /// + /// An enum representing the type of text deletion, if any. For example, + /// would be , would be , etc. + /// + public enum TextDeletionType + { + /// + /// Key represents a deletion of one (or more) character(s) behind the cursor. + /// + Back, + + /// + /// Key represents a deletion of one (or more) character(s) ahead of the cursor. + /// + Forward, + + /// + /// Key represents a deletion of all characters in current text entry context + /// + All, + + /// + /// Key does not represent a deletion. + /// + None = -1 + } + + /// + /// Returns true if the modifiers signify that the next character should be capitalized. + /// + public static bool ShouldCapitalize(this KeyModifiers modifiers) => + ((modifiers & KeyModifiers.CapsLock) == KeyModifiers.CapsLock) ^ + ((modifiers & KeyModifiers.ShiftLeft) == KeyModifiers.ShiftLeft + || (modifiers & KeyModifiers.ShiftRight) == KeyModifiers.ShiftRight); + + public static bool IsControl(this KeyName name) => + name is KeyName.ControlLeft or KeyName.ControlRight; + + public static bool IsControl(this KeyModifiers mod) => + (mod & KeyModifiers.ControlLeft) == KeyModifiers.ControlLeft || + (mod & KeyModifiers.ControlRight) == KeyModifiers.ControlRight; +} diff --git a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs index dbdef63346..3d7afe81ed 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs @@ -270,10 +270,13 @@ private void ProcessEvent(ref Event evt, IInputHandler handler) keyboard.AddKeyEvent(evt.Key); break; case EventType.TextEditing: - break; - case EventType.TextInput: + keyboard.AddTextEditingEvent(evt.Edit); break; case EventType.TextEditingCandidates: + keyboard.AddTextCandidatesEvent(evt.EditCandidates); + break; + case EventType.TextInput: + keyboard.AddTextInputEvent(evt.Text); break; } diff --git a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs index 09fb260c69..38c05eae41 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs @@ -1,6 +1,7 @@ // 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 Silk.NET.SDL; @@ -76,7 +77,7 @@ public void BeginInput() } else { - _textIsRecording = TextRecorderState.Recording; + _textIsRecording = TextRecorderState.RecordingNoSdl; } } @@ -92,7 +93,7 @@ private void BeginRecordingSdl(WindowHandle sdlWindow) { case TextRecorderState.None: return null; - case TextRecorderState.Recording: + case TextRecorderState.RecordingNoSdl: _textIsRecording = TextRecorderState.None; break; case TextRecorderState.RecordingSdl: @@ -125,61 +126,100 @@ public void AddKeyEvent(in KeyboardEvent key) if (Enum.IsDefined(keyName)) { + var isDown = key.Down != 0; + var index = EnumInfo.ValueIndexOf(keyName); + ref var button = ref _keyStates[index]; + var stateChanged = button.IsDown != isDown; _keyStates[index] = new Button(keyName, key.Down != 0, key.Down * fraction); - if (_textIsRecording == TextRecorderState.Recording) + var shouldRecord = _textIsRecording == TextRecorderState.RecordingSdl + && ((stateChanged && isDown) || (!stateChanged && key.Repeat != 0)); + if (shouldRecord) { _textRecorder ??= new TextRecorder(); - _textRecorder!.AddKeyStroke(keyName, key.Down != 0, this); + _textRecorder.AddKeyStroke(keyName, this); } } } - public unsafe void AddTextEditingEvent(in TextEditingEvent evt) => throw new NotImplementedException(); - public unsafe void AddTextCandidatesEvent(in TextEditingCandidatesEvent evt) => throw new NotImplementedException(); - - public unsafe void AddTextInputEvent(in TextInputEvent evt) + public unsafe void AddTextEditingEvent(in TextEditingEvent evt) { if (_textEntryWindow == null) { - Console.Out.WriteLine("Unexpected text input event"); var windowHandle = NativeBackend.GetWindowFromID(evt.WindowID); if (windowHandle.Handle != null) { + Console.Out.WriteLine("Unexpected text editing event"); BeginRecordingSdl(windowHandle); } - - return; } - - if (evt.WindowID != NativeBackend.GetWindowID(_textEntryWindow.Value)) + else if (evt.WindowID != NativeBackend.GetWindowID(_textEntryWindow.Value)) { - Console.Error.WriteLine("Received text input event for a different window than the " + + Console.Error.WriteLine("Received text editing event for a different window than the " + "one we're recording text for."); - return; } - if (evt.Text == null) + _textRecorder ??= new TextRecorder(); + + if (evt.Length == 0) { - return; + _textRecorder.SetSelection(evt.Start, 0); } + else + { + if (evt.Text == null) + { + return; + } - var str = new Ptr(evt.Text).ReadToString(); + _textRecorder.InsertTextAt(evt.Text, evt.Start, evt.Length); + } + } - if (string.IsNullOrEmpty(str)) + public unsafe void AddTextCandidatesEvent(in TextEditingCandidatesEvent evt) + { + if (evt.SelectedCandidate == -1 || evt.NumCandidates == 0) { return; } + Debug.Assert(evt.NumCandidates > evt.SelectedCandidate); + + var candidate = new Ptr(evt.Candidates[evt.SelectedCandidate]); + var str = candidate.ReadToString(); + _textRecorder ??= new TextRecorder(); + _textRecorder.InsertText(str); + } + + public unsafe void AddTextInputEvent(in TextInputEvent evt) + { + if (_textEntryWindow == null) + { + var windowHandle = NativeBackend.GetWindowFromID(evt.WindowID); + if (windowHandle.Handle != null) + { + Console.Out.WriteLine("Unexpected text input event"); + BeginRecordingSdl(windowHandle); + } + } + else if (evt.WindowID != NativeBackend.GetWindowID(_textEntryWindow.Value)) + { + Console.Error.WriteLine("Received text input event for a different window than the " + + "one we're recording text for."); + } + + + var str = evt.Text == null ? "" : new Ptr(evt.Text).ReadToString(); + _textRecorder ??= new TextRecorder(); - _textRecorder.AddString(evt); + _textRecorder.InsertText(str); } private WindowHandle? _textEntryWindow; private TextRecorder? _textRecorder; - private enum TextRecorderState {None, Recording, RecordingSdl} + private enum TextRecorderState {None, RecordingNoSdl, RecordingSdl} private TextRecorderState _textIsRecording; private ushort _modState; private readonly Button[] _keyStates; diff --git a/sources/Input/Input/Implementations/TextRecorder.cs b/sources/Input/Input/Implementations/TextRecorder.cs index d14afcec77..24f04ab6c4 100644 --- a/sources/Input/Input/Implementations/TextRecorder.cs +++ b/sources/Input/Input/Implementations/TextRecorder.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Text; using Silk.NET.SDL; @@ -12,9 +13,26 @@ namespace Silk.NET.Input; /// instead, but this requires the use of the SDL windowing API, which may not be available in all contexts. /// This class is a work in progress, and not yet sufficient for full text-editor support. /// -public class TextRecorder +public sealed class TextRecorder { - public void AddKeyStroke(KeyName name, bool isDown, IKeyboard keyboard) + private readonly ICharacterConverter _converter; + + /// + /// Constructor + /// + public TextRecorder(ICharacterConverter converter) => _converter = converter; + + /// + /// The number of characters currently in the buffer. + /// + public int Count => _sb.Length; + + /// + /// Modify the buffer and recorder state based on the input key name and the state of the keyboard. + /// + /// The keystroke to add + /// The keyboard whose state we are recording + public void AddKeyStroke(KeyName name, IKeyboard keyboard) { var isChar = name.IsChar(); var isDeletion = name.IsDeletion(); @@ -29,8 +47,7 @@ public void AddKeyStroke(KeyName name, bool isDown, IKeyboard keyboard) var clipboardText = keyboard.ClipboardText; if (!string.IsNullOrEmpty(clipboardText)) { - OverwriteSelectionBeforeInsert(); - InsertString(clipboardText); + InsertText(clipboardText); } return; @@ -38,53 +55,296 @@ public void AddKeyStroke(KeyName name, bool isDown, IKeyboard keyboard) var state = keyboard.State; var activeModifiers = state.Modifiers; + if (name.IsChar()) + { + if (_selectionLength > 0) + { + // overwrite current selected text + RemoveSelectedTextAndClearSelection(); + } + + // insert the appropriate character + // first, we need the virtual key represented by the scancode (KeyName) + + if (_converter.TryConvert(name, activeModifiers, out var c)) + { + InsertText(c.Value); + } + + return; + } + + if (name.IsDeletion()) + { + var deletionType = name.GetDeletionType(); + Debug.Assert(deletionType != KeyNameExtensions.TextDeletionType.None); + switch (deletionType) + { + case KeyNameExtensions.TextDeletionType.Back: + if (_selectionLength > 0) + { + RemoveSelectedTextAndClearSelection(); + } + else if (_cursorStart > 0) + { + // remove from behind cursor and move cursor back accordingly + if (activeModifiers.IsControl()) + { + // find first whitespace character prior to current cursor position + var cursorPos = _cursorStart; + while (cursorPos > 0 && !char.IsWhiteSpace(_sb[cursorPos - 1])) + { + --cursorPos; + } + + var count = Math.Min(_cursorStart - cursorPos, 1); + _sb.Remove(cursorPos, count); + _cursorStart = cursorPos; + } + else + { + _sb.Remove(--_cursorStart, 1); + } + } + break; + case KeyNameExtensions.TextDeletionType.Forward: + if (_selectionLength > 0) + { + RemoveSelectedTextAndClearSelection(); + } + else if (_cursorStart < _sb.Length) + { + // remove from front of cursor + if (activeModifiers.IsControl()) + { + // find first whitespace character after current cursor position + var cursorPos = _cursorStart; + while (cursorPos < _sb.Length && !char.IsWhiteSpace(_sb[cursorPos])) + { + ++cursorPos; + } + + var count = Math.Min(_sb.Length - cursorPos, 1); + _sb.Remove(cursorPos, count); + } + else + { + _sb.Remove(_cursorStart, 1); + } + } + break; + case KeyNameExtensions.TextDeletionType.All: + _sb.Clear(); + SetCursorPositionRaw(0); + SetSelectionLength(0); + break; + default: + Console.Error.WriteLine("Unexpected text deletion type"); + break; + } + } } - private void OverwriteSelectionBeforeInsert() + /// + /// Removes the currently selected text and sets the current selection length to 0. + /// + private void RemoveSelectedTextAndClearSelection() { - if (_cursorStart == _cursorEnd) + // remove the currently selected text + var selectedLength = _selectionLength; + SetSelectionLength(0); + if (selectedLength > 0 && _cursorStart < _sb.Length) { - return; + Debug.Assert(_cursorStart + selectedLength <= _sb.Length); + _sb.Remove(_cursorStart, selectedLength); } + } + + /// + /// Inserts the given text into the buffer at the current cursor/selection position. + /// + /// + public void InsertText(ReadOnlySpan str) + { + RemoveSelectedTextAndClearSelection(); - _sb.Remove(_cursorStart, _cursorEnd - _cursorStart); - _cursorEnd = _cursorStart; + if (str.Length > 0) + { + _sb.Insert(_cursorStart, str); + SetCursorPositionRaw(_cursorStart + str.Length); + ; + } } - public void InsertString(string str) + /// + /// Inserts the given text into the buffer at the current cursor/selection position. + /// + /// + public void InsertText(char c) { - _sb.Insert(_cursorStart, str); - _cursorEnd = _cursorStart += str.Length; + ReadOnlySpan span = [c]; + InsertText(span); } - public void AddChar(char c) + /// + /// Inserts the given text into the buffer at the given cursor position. Given cursor position is always + /// clamped to the bounds of the buffer. Clears any current selection if the actual cursor position is different from + /// the provided cursor position. + /// + /// + /// The cursor position in the buffer to inject + public void InsertTextAt(ReadOnlySpan str, int cursorStart) { - OverwriteSelectionBeforeInsert(); - _sb.Insert(_cursorStart, c); - _cursorEnd = _cursorStart += 1; + if (_cursorStart != cursorStart) + { + SetCursorPositionRaw(cursorStart); + SetSelectionLength(0); + } + + InsertText(str); } - public string? ConsumeInput() + /// + /// Inserts the given text into the buffer at the given cursor position. Given cursor position is always + /// clamped to the bounds of the buffer. Clears any current selection if the actual cursor position is different from + /// the provided cursor position. + /// + /// + /// The cursor position in the buffer to inject + /// + public unsafe void InsertTextAt(sbyte* textPtrUnsafe, int cursorStart, int textLength) => + InsertTextAt(textPtr: new Ptr(textPtrUnsafe), cursorStart, textLength); + + /// + /// Inserts the given text into the buffer at the given cursor position. Given cursor position is always + /// clamped to the bounds of the buffer. Clears any current selection if the actual cursor position is different from + /// the provided cursor position. + /// + /// + /// The cursor position in the buffer to inject + public void InsertTextAt(Ptr textPtr, int cursorStart) { - var result = _sb.ToString(); - Clear(); - return result; + // count to end + const char terminator = '\0'; + for (uint i = 0; i < int.MaxValue; ++i) + { + if (textPtr[i] == terminator) + { + InsertTextAt(textPtr, cursorStart, (int)i); + return; + } + + ++i; + } } - public void Clear() + /// + /// Inserts the given text into the buffer at the given cursor position. Given cursor position is always + /// clamped to the bounds of the buffer. Clears any current selection if the actual cursor position is different from + /// the provided cursor position. + /// + /// + /// The cursor position in the buffer to inject + /// + public void InsertTextAt(Ptr textPtr, int cursorStart, int textLength) { - _sb.Clear(); - _cursorStart = 0; - _cursorEnd = 0; + Span textSpan = stackalloc char[textLength]; + + if (textPtr.TryReadToSpan(ref textSpan)) + { + Debug.Assert(textSpan.Length == textLength); + InsertTextAt(textSpan, cursorStart); + } + else + { + Console.Error.WriteLine("Failed to read text from text editing event."); + // insert empty just to synchronize cursor position + SetSelection(cursorStart, 0); + } + } + + /// + /// Sets the selection appropriately for the given positions. Positions can be provided in any order, + /// and resulting selection will be clamped to a valid range for the current buffer. + /// + /// + /// + public void SetSelectionPositions(int positionA, int positionB) + { + if (positionA > positionB) + { + SetSelection(positionB, positionA - positionB); + } + else + { + SetSelection(positionA, positionB - positionA); + } + } + + /// + /// Set the selection to the given start and length. Position and length are clamped to the bounds of the buffer. + /// + /// + /// + public void SetSelection(int startPosition, int length) + { + SetCursorPositionRaw(startPosition); + SetSelectionLength(length); + } + + /// + /// Moves the cursor start to the given position, and adjusts the selection length such that + /// the selection's end does not move + /// + /// + public void MoveCursorStart(int newPosition) + { + var pCursor = _cursorStart; + SetCursorPositionRaw(newPosition); + var diff = pCursor - _cursorStart; + SetSelectionLength(_selectionLength + diff); } - public void SetCursorPosition(int position, int? endPosition = null) + /// + /// Adjusts the selection length based on a given end position. The end position is clamped to the bounds of the + /// buffer. This may modify the selection start position if the provided position is less than the current + /// start cursor position. + /// + /// + public void MoveCursorEnd(int endPosition) { - _cursorStart = position; - _cursorEnd = endPosition ?? position; + if (endPosition < _cursorStart) + { + SetCursorPositionRaw(endPosition); + SetSelectionLength(0); + return; + } + + var clampedEndPosition = Math.Clamp(endPosition, _cursorStart, _sb.Length); + SetSelectionLength(clampedEndPosition - _cursorStart); } + /// + /// Sets the selection length to the given value. The value is clamped to the bounds of the buffer. + /// + /// + private void SetSelectionLength(int newLength) => + _selectionLength = Math.Clamp(newLength, 0, _sb.Length - _cursorStart); + + /// + /// Sets the value of to the given value. The value is clamped to the bounds of the buffer. + /// Selection length is not affected. + /// + /// + private void SetCursorPositionRaw(int newPosition) => _cursorStart = Math.Clamp(newPosition, 0, _sb.Length); + + /// + /// Fills a span with current buffer contents. Will fill the given span up to the length of the contents or the + /// length of the span. + /// + /// + /// public int GetCurrentBuffer(Span buffer) { var maxCount = Math.Min(buffer.Length, _sb.Length); @@ -92,9 +352,15 @@ public int GetCurrentBuffer(Span buffer) return maxCount; } + /// + /// Fills a span with the currently selected text. Will fill the given span up to the length of the selection or the + /// length of the span. + /// + /// + /// public int GetSelectedRegion(Span buffer) { - var maxCount = Math.Min(buffer.Length, _cursorEnd - _cursorStart); + var maxCount = Math.Min(buffer.Length, _selectionLength); if (maxCount == 0) { return 0; @@ -104,51 +370,21 @@ public int GetSelectedRegion(Span buffer) return maxCount; } - public int Count => _sb.Length; - - private int _cursorStart, _cursorEnd; - private readonly StringBuilder _sb = new(); - - public void AddString(in TextInputEvent text) => _sb.Append(text); -} + public string ConsumeInput() + { + var result = _sb.ToString(); + Clear(); + return result; + } -/// -/// A series of extension methods for making sense of values -/// -public static class KeyNameExtensions -{ - /// - /// Returns true if the key would produce a character in common text editing scenarios. Includes whitespace. - /// - public static bool IsChar(this KeyName name) => - name is >= KeyName.A and <= KeyName.Return - or >= KeyName.KeypadDivide and <= KeyName.KeypadPeriod - or >= KeyName.Tab and <= KeyName.Slash - or >= KeyName.KeypadMultiply and <= KeyName.KeypadEnter - or >= KeyName.KeypadA and <= KeyName.KeypadExclamation - or >= KeyName.Keypad00 and <= KeyName.KeypadTab - or KeyName.Return2 or KeyName.Separator or KeyName.KeypadPlusMinus - or KeyName.KeypadComma - or KeyName.KeypadEquals or KeyName.OtherKeypadEquals; + public void Clear() + { + _sb.Clear(); + _cursorStart = 0; + _selectionLength = 0; + } - /// - /// Returns true if the given key would produce a deletion of one or more characters in common text - /// editing scenarios. - /// - public static bool IsDeletion(this KeyName name) => - name is KeyName.Backspace - or KeyName.Delete - or KeyName.KeypadBackspace - or KeyName.Clear - or KeyName.KeypadClear - or KeyName.KeypadClearEntry - or KeyName.ClearAgain; - /// - /// Returns true if the modifiers signify that the next character should be capitalized. - /// - public static bool ShouldCapitalize(this KeyModifiers modifiers) => - ((modifiers & KeyModifiers.CapsLock) == KeyModifiers.CapsLock) ^ - ((modifiers & KeyModifiers.ShiftLeft) == KeyModifiers.ShiftLeft - || (modifiers & KeyModifiers.ShiftRight) == KeyModifiers.ShiftRight); + private int _cursorStart, _selectionLength; + private readonly StringBuilder _sb = new(); } From 67df00fb91b17f1fd70faf5d2be50bbcc857de0b Mon Sep 17 00:00:00 2001 From: dom Date: Sun, 14 Sep 2025 14:03:55 -0400 Subject: [PATCH 32/39] additional modifier utility methods --- .../Input/Implementations/KeyNameExtensions.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/sources/Input/Input/Implementations/KeyNameExtensions.cs b/sources/Input/Input/Implementations/KeyNameExtensions.cs index 29c91c93ab..37c37742dc 100644 --- a/sources/Input/Input/Implementations/KeyNameExtensions.cs +++ b/sources/Input/Input/Implementations/KeyNameExtensions.cs @@ -94,4 +94,18 @@ public static bool IsControl(this KeyName name) => public static bool IsControl(this KeyModifiers mod) => (mod & KeyModifiers.ControlLeft) == KeyModifiers.ControlLeft || (mod & KeyModifiers.ControlRight) == KeyModifiers.ControlRight; + + public static bool IsShift(this KeyName name) => + name is KeyName.ShiftLeft or KeyName.ShiftRight; + + public static bool IsShift(this KeyModifiers mod) => + (mod & KeyModifiers.ShiftLeft) == KeyModifiers.ShiftLeft || + (mod & KeyModifiers.ShiftRight) == KeyModifiers.ShiftRight; + + public static bool IsAlt(this KeyName name) => + name is KeyName.AltLeft or KeyName.AltRight; + + public static bool IsAlt(this KeyModifiers mod) => + (mod & KeyModifiers.AltLeft) == KeyModifiers.AltLeft || + (mod & KeyModifiers.AltRight) == KeyModifiers.AltRight; } From dd81078b6666eda3a610570f423c35713d108c42 Mon Sep 17 00:00:00 2001 From: dom Date: Sun, 14 Sep 2025 14:16:24 -0400 Subject: [PATCH 33/39] create world's weakest fallback key -> character converter --- .../Implementations/ICharacterConverter.cs | 24 +++++++++++++------ .../Input/Implementations/SDL3/SdlKeyboard.cs | 6 ++--- .../Input/Implementations/TextRecorder.cs | 5 +++- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/sources/Input/Input/Implementations/ICharacterConverter.cs b/sources/Input/Input/Implementations/ICharacterConverter.cs index 9442d08b73..cc398e15df 100644 --- a/sources/Input/Input/Implementations/ICharacterConverter.cs +++ b/sources/Input/Input/Implementations/ICharacterConverter.cs @@ -15,10 +15,8 @@ public interface ICharacterConverter } -public class DummyCharConverter: ICharacterConverter +internal class DummyCharConverter: ICharacterConverter { - private static CultureInfo Culture => CultureInfo.CurrentUICulture; - private static int Layout => Culture.KeyboardLayoutId; public bool TryConvert(KeyName key, KeyModifiers modifiers, [NotNullWhen(true)] out char? c) { if (!key.IsChar()) @@ -27,13 +25,25 @@ public bool TryConvert(KeyName key, KeyModifiers modifiers, [NotNullWhen(true)] return false; } - char resultBeforeProcessing = '\0'; - bool isLetter = false; var isShifted = modifiers.IsShift(); - switch (key) + const char a = 'a'; + //const char A = 'A'; + const int aCode = (int)KeyName.A; + const int max = (int)KeyName.Z - (int)KeyName.A; + var diff = (int)key - aCode; + if (diff is >= 0 and <= max) { - case KeyName.A: + var baseChar = a; + c = (char)(baseChar + diff); + if (isShifted) + { + c = CultureInfo.CurrentCulture.TextInfo.ToUpper(c.Value); + } + return true; } + + c = null; + return false; } } diff --git a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs index 38c05eae41..b2cae6f9af 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs @@ -137,7 +137,7 @@ public void AddKeyEvent(in KeyboardEvent key) && ((stateChanged && isDown) || (!stateChanged && key.Repeat != 0)); if (shouldRecord) { - _textRecorder ??= new TextRecorder(); + _textRecorder ??= new TextRecorder(null); _textRecorder.AddKeyStroke(keyName, this); } } @@ -160,7 +160,7 @@ public unsafe void AddTextEditingEvent(in TextEditingEvent evt) "one we're recording text for."); } - _textRecorder ??= new TextRecorder(); + _textRecorder ??= new TextRecorder(null); if (evt.Length == 0) { @@ -188,7 +188,7 @@ public unsafe void AddTextCandidatesEvent(in TextEditingCandidatesEvent evt) var candidate = new Ptr(evt.Candidates[evt.SelectedCandidate]); var str = candidate.ReadToString(); - _textRecorder ??= new TextRecorder(); + _textRecorder ??= new TextRecorder(null); _textRecorder.InsertText(str); } diff --git a/sources/Input/Input/Implementations/TextRecorder.cs b/sources/Input/Input/Implementations/TextRecorder.cs index 24f04ab6c4..d222d36d59 100644 --- a/sources/Input/Input/Implementations/TextRecorder.cs +++ b/sources/Input/Input/Implementations/TextRecorder.cs @@ -20,7 +20,10 @@ public sealed class TextRecorder /// /// Constructor /// - public TextRecorder(ICharacterConverter converter) => _converter = converter; + public TextRecorder(ICharacterConverter? converter) + { + _converter = converter ?? new DummyCharConverter(); + } /// /// The number of characters currently in the buffer. From 0ba7c09968aa68a305f85a95694a61f3058bed60 Mon Sep 17 00:00:00 2001 From: dom Date: Sun, 14 Sep 2025 15:07:13 -0400 Subject: [PATCH 34/39] add joystick interface to gamepad, invoke id refresh --- sources/Input/Input/Implementations/SDL3/SdlGamepad.cs | 5 ++++- sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs b/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs index 6ed966eb52..bb37d7132b 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs @@ -9,7 +9,7 @@ namespace Silk.NET.Input.SDL3; /// /// provides the IGamepad implementation for a joystick /// -internal sealed unsafe class SdlGamepad : SdlDevice, IGamepad, ISdlDevice, ISdlJoystick +internal sealed unsafe class SdlGamepad : SdlDevice, IGamepad, ISdlDevice, ISdlJoystick, IJoystick { private readonly GamepadHandle _gamepadHandle; @@ -362,4 +362,7 @@ static JoystickButton AsJoystickButton(GamepadButton buttonIndex) => // we can safely use an integer key with a bit shift like this. private const int _buttonShift = 0; private const int _axisShift = 8; + + JoystickState IJoystick.State => Joystick.State; + ButtonReadOnlyList IButtonDevice.State => GamepadState.Buttons; } diff --git a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs index 3d7afe81ed..71919a9c1b 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs @@ -501,7 +501,10 @@ private static void RefreshDeviceIds(List devices) { for (var i = 0; i < devices.Count; i++) { - devices[i].RefreshIdFromBackend(); + if (devices[i] is IOrderedDevice d) + { + d.RefreshSdlId(); + } } } From ec0e48b260f879495288d59ff384e311ea969ba1 Mon Sep 17 00:00:00 2001 From: dom Date: Sun, 14 Sep 2025 16:36:07 -0400 Subject: [PATCH 35/39] increase number of characters supported --- .../Implementations/ICharacterConverter.cs | 164 ++++++++++++++++-- .../Input/Implementations/TextRecorder.cs | 8 +- 2 files changed, 154 insertions(+), 18 deletions(-) diff --git a/sources/Input/Input/Implementations/ICharacterConverter.cs b/sources/Input/Input/Implementations/ICharacterConverter.cs index cc398e15df..6eeea68b5b 100644 --- a/sources/Input/Input/Implementations/ICharacterConverter.cs +++ b/sources/Input/Input/Implementations/ICharacterConverter.cs @@ -9,13 +9,12 @@ namespace Silk.NET.Input; /// /// A simple interface for an implementation that converts keyboard input into characters for text entry /// -public interface ICharacterConverter +internal interface ICharacterConverter { public bool TryConvert(KeyName key, KeyModifiers modifiers, [NotNullWhen(true)] out char? c); } - -internal class DummyCharConverter: ICharacterConverter +internal class DummyCharConverter : ICharacterConverter { public bool TryConvert(KeyName key, KeyModifiers modifiers, [NotNullWhen(true)] out char? c) { @@ -25,17 +24,11 @@ public bool TryConvert(KeyName key, KeyModifiers modifiers, [NotNullWhen(true)] return false; } - var isShifted = modifiers.IsShift(); - const char a = 'a'; - //const char A = 'A'; - const int aCode = (int)KeyName.A; - const int max = (int)KeyName.Z - (int)KeyName.A; - var diff = (int)key - aCode; - if (diff is >= 0 and <= max) + if (key is >= KeyName.A and <= KeyName.Z) { - var baseChar = a; - c = (char)(baseChar + diff); - if (isShifted) + var diff = (int)key - (int)KeyName.A; + c = (char)('a' + diff); + if (modifiers.ShouldCapitalize()) { c = CultureInfo.CurrentCulture.TextInfo.ToUpper(c.Value); } @@ -43,6 +36,151 @@ public bool TryConvert(KeyName key, KeyModifiers modifiers, [NotNullWhen(true)] return true; } + var isShifted = modifiers.IsShift(); + switch (key) + { + case KeyName.Number1: + c = isShifted ? '!' : '1'; + return true; + case KeyName.Keypad1: + c = '1'; + return true; + case KeyName.Number2: + c = isShifted ? '@' : '2'; + return true; + case KeyName.Keypad2: + c = '2'; + return true; + case KeyName.Number3: + c = isShifted ? '#' : '3'; + return true; + case KeyName.Keypad3: + c = '3'; + return true; + case KeyName.Number4: + c = isShifted ? '$' : '4'; + return true; + case KeyName.Keypad4: + c = '4'; + return true; + case KeyName.Number5: + c = isShifted ? '%' : '5'; + return true; + case KeyName.Keypad5: + c = '5'; + return true; + case KeyName.Number6: + c = isShifted ? '^' : '6'; + return true; + case KeyName.Keypad6: + c = '6'; + return true; + case KeyName.Number7: + c = isShifted ? '&' : '7'; + return true; + case KeyName.Keypad7: + c = '7'; + return true; + case KeyName.Number8: + c = isShifted ? '*' : '8'; + return true; + case KeyName.Keypad8: + c = '8'; + return true; + case KeyName.Number9: + c = isShifted ? '(' : '9'; + return true; + case KeyName.Keypad9: + c = '9'; + return true; + case KeyName.Number0: + c = isShifted ? ')' : '0'; + return true; + case KeyName.Keypad0: + c = '0'; + return true; + case KeyName.Minus: + c = isShifted ? '_' : '-'; + return true; + case KeyName.Equals: + c = isShifted ? '+' : '='; + return true; + case KeyName.Tab: + c = '\t'; + return true; + case KeyName.Apostrophe: + c = isShifted ? '\"' : '\''; + return true; + case KeyName.Backslash: + c = isShifted ? '|' : '\\'; + return true; + case KeyName.Semicolon: + c = isShifted ? ':' : ';'; + return true; + case KeyName.Comma: + c = isShifted ? '<' : ','; + return true; + case KeyName.Period: + c = isShifted ? '>' : '.'; + return true; + case KeyName.Slash: + c = isShifted ? '?' : '/'; + return true; + case KeyName.Space: + c = ' '; + return true; + case KeyName.KeypadAmpersand: + c = '&'; + return true; + case KeyName.KeypadPercent: + c = '%'; + return true; + case KeyName.KeypadColon: + c = ':'; + return true; + case KeyName.KeypadLeftParenthesis: + c = '('; + return true; + case KeyName.KeypadRightParenthesis: + c = ')'; + return true; + case KeyName.KeypadPlus: + c = '+'; + return true; + case KeyName.KeypadComma: + c = ','; + return true; + case KeyName.KeypadMinus: + c = '-'; + return true; + case KeyName.KeypadPeriod: + c = '.'; + return true; + case KeyName.KeypadDivide: + c = '/'; + return true; + case KeyName.KeypadEquals: + c = '='; + return true; + case KeyName.KeypadEnter: + case KeyName.Return: + case KeyName.Return2: + c = '\n'; + return true; + case KeyName.KeypadExclamation: + c = '!'; + return true; + case KeyName.KeypadMultiply: + c = '*'; + return true; + case KeyName.Grave: + c = '`'; + return true; + case KeyName.CurrencyUnit: + c = RegionInfo.CurrentRegion.CurrencySymbol[0]; + return true; + } + c = null; return false; } diff --git a/sources/Input/Input/Implementations/TextRecorder.cs b/sources/Input/Input/Implementations/TextRecorder.cs index d222d36d59..9cdca83f42 100644 --- a/sources/Input/Input/Implementations/TextRecorder.cs +++ b/sources/Input/Input/Implementations/TextRecorder.cs @@ -13,7 +13,7 @@ namespace Silk.NET.Input; /// instead, but this requires the use of the SDL windowing API, which may not be available in all contexts. /// This class is a work in progress, and not yet sufficient for full text-editor support. /// -public sealed class TextRecorder +internal sealed class TextRecorder { private readonly ICharacterConverter _converter; @@ -60,10 +60,9 @@ public void AddKeyStroke(KeyName name, IKeyboard keyboard) var activeModifiers = state.Modifiers; if (name.IsChar()) { - if (_selectionLength > 0) + if (activeModifiers.IsAlt() || activeModifiers.IsControl()) { - // overwrite current selected text - RemoveSelectedTextAndClearSelection(); + return; } // insert the appropriate character @@ -176,7 +175,6 @@ public void InsertText(ReadOnlySpan str) { _sb.Insert(_cursorStart, str); SetCursorPositionRaw(_cursorStart + str.Length); - ; } } From 7983462947bd436625534bbb21dd637922c6503e Mon Sep 17 00:00:00 2001 From: dom Date: Sun, 14 Sep 2025 16:47:50 -0400 Subject: [PATCH 36/39] organization --- .../{ => KeyHandling}/ICharacterConverter.cs | 3 +- .../{ => KeyHandling}/KeyNameExtensions.cs | 2 +- .../{ => KeyHandling}/TextRecorder.cs | 9 +++- .../Implementations/SDL3/BackendExtensions.cs | 5 +- .../SDL3/Devices/ISdlDevice.cs | 14 +++++ .../{ => Devices/Joysticks}/IOrderedDevice.cs | 2 +- .../{ => Devices/Joysticks}/ISdlJoystick.cs | 2 +- .../{ => Devices/Joysticks}/SdlGamepad.cs | 2 +- .../Joysticks}/SdlJoystick.Extended.cs | 2 +- .../{ => Devices/Joysticks}/SdlJoystick.cs | 2 +- .../SDL3/{ => Devices/Joysticks}/SdlRumble.cs | 2 +- .../Pointers/SdlBoundedPointerDevice.cs | 0 .../Pointers/SdlBoundedPointerTarget.cs | 0 .../SDL3/{ => Devices}/Pointers/SdlPen.cs | 0 .../{ => Devices}/Pointers/SdlSharedMouse.cs | 0 .../SDL3/Devices/Pointers/SdlTouchScreen.cs | 54 +++++++++++++++++++ .../Pointers/SdlUnboundedMouse.cs | 0 .../Pointers/SdlUnboundedPointerTarget.cs | 0 .../SDL3/{ => Devices}/SdlDevice.cs | 14 ----- .../SDL3/{ => Devices}/SdlKeyboard.cs | 13 ++--- .../SDL3/Pointers/SdlTouchScreen.cs | 22 -------- .../Implementations/SDL3/SdlInputBackend.cs | 1 + ...board.KeyNames.cs => SdlKeyConversions.cs} | 11 ++-- 23 files changed, 102 insertions(+), 58 deletions(-) rename sources/Input/Input/Implementations/{ => KeyHandling}/ICharacterConverter.cs (98%) rename sources/Input/Input/Implementations/{ => KeyHandling}/KeyNameExtensions.cs (99%) rename sources/Input/Input/Implementations/{ => KeyHandling}/TextRecorder.cs (97%) create mode 100644 sources/Input/Input/Implementations/SDL3/Devices/ISdlDevice.cs rename sources/Input/Input/Implementations/SDL3/{ => Devices/Joysticks}/IOrderedDevice.cs (89%) rename sources/Input/Input/Implementations/SDL3/{ => Devices/Joysticks}/ISdlJoystick.cs (97%) rename sources/Input/Input/Implementations/SDL3/{ => Devices/Joysticks}/SdlGamepad.cs (99%) rename sources/Input/Input/Implementations/SDL3/{ => Devices/Joysticks}/SdlJoystick.Extended.cs (97%) rename sources/Input/Input/Implementations/SDL3/{ => Devices/Joysticks}/SdlJoystick.cs (99%) rename sources/Input/Input/Implementations/SDL3/{ => Devices/Joysticks}/SdlRumble.cs (99%) rename sources/Input/Input/Implementations/SDL3/{ => Devices}/Pointers/SdlBoundedPointerDevice.cs (100%) rename sources/Input/Input/Implementations/SDL3/{ => Devices}/Pointers/SdlBoundedPointerTarget.cs (100%) rename sources/Input/Input/Implementations/SDL3/{ => Devices}/Pointers/SdlPen.cs (100%) rename sources/Input/Input/Implementations/SDL3/{ => Devices}/Pointers/SdlSharedMouse.cs (100%) create mode 100644 sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlTouchScreen.cs rename sources/Input/Input/Implementations/SDL3/{ => Devices}/Pointers/SdlUnboundedMouse.cs (100%) rename sources/Input/Input/Implementations/SDL3/{ => Devices}/Pointers/SdlUnboundedPointerTarget.cs (100%) rename sources/Input/Input/Implementations/SDL3/{ => Devices}/SdlDevice.cs (82%) rename sources/Input/Input/Implementations/SDL3/{ => Devices}/SdlKeyboard.cs (92%) delete mode 100644 sources/Input/Input/Implementations/SDL3/Pointers/SdlTouchScreen.cs rename sources/Input/Input/Implementations/SDL3/{SdlKeyboard.KeyNames.cs => SdlKeyConversions.cs} (89%) diff --git a/sources/Input/Input/Implementations/ICharacterConverter.cs b/sources/Input/Input/Implementations/KeyHandling/ICharacterConverter.cs similarity index 98% rename from sources/Input/Input/Implementations/ICharacterConverter.cs rename to sources/Input/Input/Implementations/KeyHandling/ICharacterConverter.cs index 6eeea68b5b..2ee4c27b12 100644 --- a/sources/Input/Input/Implementations/ICharacterConverter.cs +++ b/sources/Input/Input/Implementations/KeyHandling/ICharacterConverter.cs @@ -4,7 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; -namespace Silk.NET.Input; +namespace Silk.NET.Input.KeyHandling; /// /// A simple interface for an implementation that converts keyboard input into characters for text entry @@ -16,6 +16,7 @@ internal interface ICharacterConverter internal class DummyCharConverter : ICharacterConverter { + // todo - proper VK key support for various languages and layouts public bool TryConvert(KeyName key, KeyModifiers modifiers, [NotNullWhen(true)] out char? c) { if (!key.IsChar()) diff --git a/sources/Input/Input/Implementations/KeyNameExtensions.cs b/sources/Input/Input/Implementations/KeyHandling/KeyNameExtensions.cs similarity index 99% rename from sources/Input/Input/Implementations/KeyNameExtensions.cs rename to sources/Input/Input/Implementations/KeyHandling/KeyNameExtensions.cs index 37c37742dc..7ee140e867 100644 --- a/sources/Input/Input/Implementations/KeyNameExtensions.cs +++ b/sources/Input/Input/Implementations/KeyHandling/KeyNameExtensions.cs @@ -3,7 +3,7 @@ using System.Diagnostics; -namespace Silk.NET.Input; +namespace Silk.NET.Input.KeyHandling; /// /// A series of extension methods for making sense of values diff --git a/sources/Input/Input/Implementations/TextRecorder.cs b/sources/Input/Input/Implementations/KeyHandling/TextRecorder.cs similarity index 97% rename from sources/Input/Input/Implementations/TextRecorder.cs rename to sources/Input/Input/Implementations/KeyHandling/TextRecorder.cs index 9cdca83f42..8df78933bf 100644 --- a/sources/Input/Input/Implementations/TextRecorder.cs +++ b/sources/Input/Input/Implementations/KeyHandling/TextRecorder.cs @@ -5,7 +5,7 @@ using System.Text; using Silk.NET.SDL; -namespace Silk.NET.Input; +namespace Silk.NET.Input.KeyHandling; /// /// A utility class for recording text input. @@ -371,6 +371,10 @@ public int GetSelectedRegion(Span buffer) return maxCount; } + /// + /// Retrieves the current buffer contents and clears the buffer, resetting the cursor and selection positions. + /// + /// public string ConsumeInput() { var result = _sb.ToString(); @@ -378,6 +382,9 @@ public string ConsumeInput() return result; } + /// + /// Clears the buffer and resets the cursor and selection positions. + /// public void Clear() { _sb.Clear(); diff --git a/sources/Input/Input/Implementations/SDL3/BackendExtensions.cs b/sources/Input/Input/Implementations/SDL3/BackendExtensions.cs index 01a9d69e94..25ecd6804e 100644 --- a/sources/Input/Input/Implementations/SDL3/BackendExtensions.cs +++ b/sources/Input/Input/Implementations/SDL3/BackendExtensions.cs @@ -2,12 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text; +using System.Runtime.CompilerServices; namespace Silk.NET.Input.SDL3; internal static unsafe class BackendExtensions { - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static IntPtr FallbackUniqueId(this SdlInputBackend _, uint sdlDeviceId, nint uniqueId) { Console.Error.WriteLine("Failed to create a deterministically unique identifier for joystick"); @@ -24,7 +25,7 @@ public static bool AttemptUniqueId(this SdlInputBackend sdlInputBackend, Ptr(this SdlInputBackend sdlInputBackend, T ptr, ref nint uniqueId1) where T : unmanaged => AttemptUniqueId(sdlInputBackend, new ReadOnlySpan(&ptr, sizeof(T)), ref uniqueId1); diff --git a/sources/Input/Input/Implementations/SDL3/Devices/ISdlDevice.cs b/sources/Input/Input/Implementations/SDL3/Devices/ISdlDevice.cs new file mode 100644 index 0000000000..f9a38697d5 --- /dev/null +++ b/sources/Input/Input/Implementations/SDL3/Devices/ISdlDevice.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Silk.NET.Input.SDL3; + +/// +/// An interface defining a generic constructor for managed SDL devices. +/// +/// +internal interface ISdlDevice : IInputDevice where T : SdlDevice +{ + public static abstract T? CreateDevice(uint sdlDeviceId, SdlInputBackend backend); + +} diff --git a/sources/Input/Input/Implementations/SDL3/IOrderedDevice.cs b/sources/Input/Input/Implementations/SDL3/Devices/Joysticks/IOrderedDevice.cs similarity index 89% rename from sources/Input/Input/Implementations/SDL3/IOrderedDevice.cs rename to sources/Input/Input/Implementations/SDL3/Devices/Joysticks/IOrderedDevice.cs index 0de1c35560..07d5598a38 100644 --- a/sources/Input/Input/Implementations/SDL3/IOrderedDevice.cs +++ b/sources/Input/Input/Implementations/SDL3/Devices/Joysticks/IOrderedDevice.cs @@ -1,7 +1,7 @@ // 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; +namespace Silk.NET.Input.SDL3.Joysticks; /// /// For devices such as gamepads and joysticks, their SDL IDs are likely to change when other devices diff --git a/sources/Input/Input/Implementations/SDL3/ISdlJoystick.cs b/sources/Input/Input/Implementations/SDL3/Devices/Joysticks/ISdlJoystick.cs similarity index 97% rename from sources/Input/Input/Implementations/SDL3/ISdlJoystick.cs rename to sources/Input/Input/Implementations/SDL3/Devices/Joysticks/ISdlJoystick.cs index 51b0865d97..3a1e81b181 100644 --- a/sources/Input/Input/Implementations/SDL3/ISdlJoystick.cs +++ b/sources/Input/Input/Implementations/SDL3/Devices/Joysticks/ISdlJoystick.cs @@ -3,7 +3,7 @@ using Silk.NET.SDL; -namespace Silk.NET.Input.SDL3; +namespace Silk.NET.Input.SDL3.Joysticks; /// /// An interface for implementing different joystick types diff --git a/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs b/sources/Input/Input/Implementations/SDL3/Devices/Joysticks/SdlGamepad.cs similarity index 99% rename from sources/Input/Input/Implementations/SDL3/SdlGamepad.cs rename to sources/Input/Input/Implementations/SDL3/Devices/Joysticks/SdlGamepad.cs index bb37d7132b..bc279c79d6 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs +++ b/sources/Input/Input/Implementations/SDL3/Devices/Joysticks/SdlGamepad.cs @@ -4,7 +4,7 @@ using System.Diagnostics; using Silk.NET.SDL; -namespace Silk.NET.Input.SDL3; +namespace Silk.NET.Input.SDL3.Joysticks; /// /// provides the IGamepad implementation for a joystick diff --git a/sources/Input/Input/Implementations/SDL3/SdlJoystick.Extended.cs b/sources/Input/Input/Implementations/SDL3/Devices/Joysticks/SdlJoystick.Extended.cs similarity index 97% rename from sources/Input/Input/Implementations/SDL3/SdlJoystick.Extended.cs rename to sources/Input/Input/Implementations/SDL3/Devices/Joysticks/SdlJoystick.Extended.cs index a0a3a01c8b..146dc37f0d 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlJoystick.Extended.cs +++ b/sources/Input/Input/Implementations/SDL3/Devices/Joysticks/SdlJoystick.Extended.cs @@ -4,7 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.Numerics; -namespace Silk.NET.Input.SDL3; +namespace Silk.NET.Input.SDL3.Joysticks; // This partial class contains the logic for handling joystick-based device types such as SdlGamepad. internal sealed partial class SdlJoystick diff --git a/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs b/sources/Input/Input/Implementations/SDL3/Devices/Joysticks/SdlJoystick.cs similarity index 99% rename from sources/Input/Input/Implementations/SDL3/SdlJoystick.cs rename to sources/Input/Input/Implementations/SDL3/Devices/Joysticks/SdlJoystick.cs index 21b1025e44..e0676310bf 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs +++ b/sources/Input/Input/Implementations/SDL3/Devices/Joysticks/SdlJoystick.cs @@ -4,7 +4,7 @@ using System.Numerics; using Silk.NET.SDL; -namespace Silk.NET.Input.SDL3; +namespace Silk.NET.Input.SDL3.Joysticks; internal sealed unsafe partial class SdlJoystick : SdlDevice, IJoystick, ISdlDevice, IOrderedDevice { diff --git a/sources/Input/Input/Implementations/SDL3/SdlRumble.cs b/sources/Input/Input/Implementations/SDL3/Devices/Joysticks/SdlRumble.cs similarity index 99% rename from sources/Input/Input/Implementations/SDL3/SdlRumble.cs rename to sources/Input/Input/Implementations/SDL3/Devices/Joysticks/SdlRumble.cs index 11f53f7ae7..ed99d82212 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlRumble.cs +++ b/sources/Input/Input/Implementations/SDL3/Devices/Joysticks/SdlRumble.cs @@ -5,7 +5,7 @@ using System.Runtime.CompilerServices; using Silk.NET.SDL; -namespace Silk.NET.Input.SDL3; +namespace Silk.NET.Input.SDL3.Joysticks; internal unsafe class SdlRumble : IReadOnlyList { diff --git a/sources/Input/Input/Implementations/SDL3/Pointers/SdlBoundedPointerDevice.cs b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlBoundedPointerDevice.cs similarity index 100% rename from sources/Input/Input/Implementations/SDL3/Pointers/SdlBoundedPointerDevice.cs rename to sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlBoundedPointerDevice.cs diff --git a/sources/Input/Input/Implementations/SDL3/Pointers/SdlBoundedPointerTarget.cs b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlBoundedPointerTarget.cs similarity index 100% rename from sources/Input/Input/Implementations/SDL3/Pointers/SdlBoundedPointerTarget.cs rename to sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlBoundedPointerTarget.cs diff --git a/sources/Input/Input/Implementations/SDL3/Pointers/SdlPen.cs b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlPen.cs similarity index 100% rename from sources/Input/Input/Implementations/SDL3/Pointers/SdlPen.cs rename to sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlPen.cs diff --git a/sources/Input/Input/Implementations/SDL3/Pointers/SdlSharedMouse.cs b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlSharedMouse.cs similarity index 100% rename from sources/Input/Input/Implementations/SDL3/Pointers/SdlSharedMouse.cs rename to sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlSharedMouse.cs diff --git a/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlTouchScreen.cs b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlTouchScreen.cs new file mode 100644 index 0000000000..00d902787b --- /dev/null +++ b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlTouchScreen.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Silk.NET.Input.SDL3.Pointers; + +internal class SdlTouchScreen : SdlDevice, ISdlDevice, IPointerDevice +{ + public static SdlTouchScreen CreateDevice(uint sdlDeviceId, SdlInputBackend backend) + { + } + + public bool Equals(IInputDevice? other) + { + throw new NotImplementedException(); + } + + public override uint RefreshIdFromBackend() + { + throw new NotImplementedException(); + } + + public override string Name + { + get + { + throw new NotImplementedException(); + } + } + + protected override void Release() + { + throw new NotImplementedException(); + } + + public PointerState State + { + get + { + throw new NotImplementedException(); + } + } + + public IReadOnlyList Targets + { + get + { + throw new NotImplementedException(); + } + } + + public SdlTouchScreen(uint sdlDeviceId, nint uniqueId, SdlInputBackend backend) : base(sdlDeviceId, uniqueId, backend) + { + } +} diff --git a/sources/Input/Input/Implementations/SDL3/Pointers/SdlUnboundedMouse.cs b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlUnboundedMouse.cs similarity index 100% rename from sources/Input/Input/Implementations/SDL3/Pointers/SdlUnboundedMouse.cs rename to sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlUnboundedMouse.cs diff --git a/sources/Input/Input/Implementations/SDL3/Pointers/SdlUnboundedPointerTarget.cs b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlUnboundedPointerTarget.cs similarity index 100% rename from sources/Input/Input/Implementations/SDL3/Pointers/SdlUnboundedPointerTarget.cs rename to sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlUnboundedPointerTarget.cs diff --git a/sources/Input/Input/Implementations/SDL3/SdlDevice.cs b/sources/Input/Input/Implementations/SDL3/Devices/SdlDevice.cs similarity index 82% rename from sources/Input/Input/Implementations/SDL3/SdlDevice.cs rename to sources/Input/Input/Implementations/SDL3/Devices/SdlDevice.cs index 27ae9c9411..acdd557ad4 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlDevice.cs +++ b/sources/Input/Input/Implementations/SDL3/Devices/SdlDevice.cs @@ -64,17 +64,3 @@ public void Dispose() private bool _isDisposed; } - -/// -/// An interface defining a generic constructor for managed SDL devices. -/// -/// -internal interface ISdlDevice : IInputDevice where T : SdlDevice -{ - public static abstract T? CreateDevice(uint sdlDeviceId, SdlInputBackend backend); - -} - -internal static class SdlDeviceFactory -{ -} diff --git a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs b/sources/Input/Input/Implementations/SDL3/Devices/SdlKeyboard.cs similarity index 92% rename from sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs rename to sources/Input/Input/Implementations/SDL3/Devices/SdlKeyboard.cs index b2cae6f9af..2ee6ee9818 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs +++ b/sources/Input/Input/Implementations/SDL3/Devices/SdlKeyboard.cs @@ -3,11 +3,12 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using Silk.NET.Input.KeyHandling; using Silk.NET.SDL; namespace Silk.NET.Input.SDL3; -internal partial class SdlKeyboard : SdlDevice, IKeyboard, ISdlDevice +internal class SdlKeyboard : SdlDevice, IKeyboard, ISdlDevice { public KeyboardState State { get; } public override string Name => NativeBackend.GetKeyboardNameForID(SdlDeviceId).ReadToString(); @@ -60,7 +61,7 @@ protected override void Release() public bool TryGetKeyName(KeyName key, [NotNullWhen(true)] out string? name) { // todo: should 'asKeyEvent' be true? - var sdlKey = KeyNameToSdl(key, NativeBackend, true, _modState); + var sdlKey = SdlKeyConversions.KeyNameToSdl(key, NativeBackend, true, _modState); var namePtr = NativeBackend.GetKeyName(sdlKey); name = namePtr.ReadToString(); return !string.IsNullOrWhiteSpace(name); @@ -122,7 +123,7 @@ private void BeginRecordingSdl(WindowHandle sdlWindow) public void AddKeyEvent(in KeyboardEvent key) { const float fraction = 1f / 255f; - var keyName = ScancodeToKeyName(key.Scancode); // SdlToKeyName(key.Which); + var keyName = SdlKeyConversions.ScancodeToKeyName(key.Scancode); // SdlToKeyName(key.Which); if (Enum.IsDefined(keyName)) { @@ -212,7 +213,7 @@ public unsafe void AddTextInputEvent(in TextInputEvent evt) var str = evt.Text == null ? "" : new Ptr(evt.Text).ReadToString(); - _textRecorder ??= new TextRecorder(); + _textRecorder ??= new TextRecorder(null); _textRecorder.InsertText(str); } @@ -223,8 +224,4 @@ private enum TextRecorderState {None, RecordingNoSdl, RecordingSdl} private TextRecorderState _textIsRecording; private ushort _modState; private readonly Button[] _keyStates; - private const uint _letterKeyDiff = Sdl.Ka - (uint)KeyName.A; - private const uint _numKeyDiff = Sdl.K1 - (uint)KeyName.Number1; - private const uint _systemAndKeypadDiff = Sdl.KPrintscreen - (uint)KeyName.PrintScreen; - private const uint _systemNonHidKeyDiff = Sdl.KSoftleft - (uint)KeyName.SoftLeft; } diff --git a/sources/Input/Input/Implementations/SDL3/Pointers/SdlTouchScreen.cs b/sources/Input/Input/Implementations/SDL3/Pointers/SdlTouchScreen.cs deleted file mode 100644 index 7aa5d70f95..0000000000 --- a/sources/Input/Input/Implementations/SDL3/Pointers/SdlTouchScreen.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Silk.NET.Input.SDL3.Pointers; - -internal class SdlTouchScreen : SdlDevice, ISdlDevice, IPointerDevice -{ - public static SdlTouchScreen CreateDevice(uint sdlDeviceId, SdlInputBackend backend) => throw new NotImplementedException(); - - public bool Equals(IInputDevice? other) => throw new NotImplementedException(); - - public override string Name => throw new NotImplementedException(); - protected override void Release() => throw new NotImplementedException(); - - public PointerState State => throw new NotImplementedException(); - - public IReadOnlyList Targets => throw new NotImplementedException(); - - public SdlTouchScreen(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDeviceId, backend) - { - } -} diff --git a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs index 71919a9c1b..e2791cc228 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; +using Silk.NET.Input.SDL3.Joysticks; using Silk.NET.Input.SDL3.Pointers; using Silk.NET.Maths; using Silk.NET.SDL; diff --git a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.KeyNames.cs b/sources/Input/Input/Implementations/SDL3/SdlKeyConversions.cs similarity index 89% rename from sources/Input/Input/Implementations/SDL3/SdlKeyboard.KeyNames.cs rename to sources/Input/Input/Implementations/SDL3/SdlKeyConversions.cs index 77a60f3a53..fdcf6cb689 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.KeyNames.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlKeyConversions.cs @@ -6,13 +6,13 @@ namespace Silk.NET.Input.SDL3; -internal partial class SdlKeyboard +internal static class SdlKeyConversions { [MethodImpl(MethodImplOptions.AggressiveInlining)] public static KeyName ScancodeToKeyName(uint scancode) => (KeyName)scancode; [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static KeyName ScancodeToKeyName(Scancode scancode) => ScancodeToKeyName((uint)scancode); + public static KeyName ScancodeToKeyName(Scancode scancode) => ScancodeToKeyName((uint)scancode); public static unsafe KeyName SdlToKeyName(uint key, ISdl sdl, ushort? modState = null) { @@ -82,7 +82,7 @@ public static KeyName SdlToKeyName(uint key) => }; /// - /// The reverse operation of , + /// Converts a to an SDL key id. /// /// The name of the key you would like to get an Sdl key id for /// Sdl backend instance @@ -96,4 +96,9 @@ public static uint KeyNameToSdl(KeyName key, ISdl sdl, bool asKeyEvent, ushort? var asKeyEventByte = asKeyEvent ? (byte)1 : (byte)0; return sdl.GetKeyFromScancode((Scancode)scanCode, modState.Value, asKeyEventByte); } + + private const uint _letterKeyDiff = Sdl.Ka - (uint)KeyName.A; + private const uint _numKeyDiff = Sdl.K1 - (uint)KeyName.Number1; + private const uint _systemAndKeypadDiff = Sdl.KPrintscreen - (uint)KeyName.PrintScreen; + private const uint _systemNonHidKeyDiff = Sdl.KSoftleft - (uint)KeyName.SoftLeft; } From e263b9b61818eeff2283af517fb7fc23306bccdf Mon Sep 17 00:00:00 2001 From: dom Date: Sun, 14 Sep 2025 17:23:40 -0400 Subject: [PATCH 37/39] constrained usage of special enum-as-indexes functionality via attribute, custom IReadOnlyList for key states --- .../Input/Input/Implementations/EnumInfo.cs | 44 +++++++++-- .../SDL3/Devices/SdlKeyboard.cs | 76 ++++++++++++++----- sources/Input/Input/JoystickButton.cs | 1 + 3 files changed, 96 insertions(+), 25 deletions(-) diff --git a/sources/Input/Input/Implementations/EnumInfo.cs b/sources/Input/Input/Implementations/EnumInfo.cs index c8300a6796..76144706eb 100644 --- a/sources/Input/Input/Implementations/EnumInfo.cs +++ b/sources/Input/Input/Implementations/EnumInfo.cs @@ -7,7 +7,9 @@ namespace Silk.NET.Input; // ReSharper disable StaticMemberInGenericType // ^ that's the point /// -/// A helper class for quickly converting enum values into indexes +/// A helper class for quickly converting enum values into indexes, particularly +/// when there is a possibility of unknown/unnamed enum values. See for an example +/// of an appropriate implementation along with /// /// internal static class EnumInfo where T : unmanaged, Enum @@ -44,12 +46,28 @@ internal static class EnumInfo where T : unmanaged, Enum private static readonly string[] _names; private static readonly Dictionary _numericallyDistinctValues; private static readonly ulong[] _allEnumValuesRaw; + private static bool _unnamedAreIndexable; static unsafe EnumInfo() { - if (typeof(T).CustomAttributes.Any(x => x.AttributeType == typeof(FlagsAttribute))) + var customAttributeDatas = typeof(T).CustomAttributes; + var hasFlagsAttribute = false; + foreach (var attr in customAttributeDatas) { - throw new InvalidOperationException("Flags enums are not supported."); + if (attr.AttributeType == typeof(FlagsAttribute)) + { + hasFlagsAttribute = true; + } + + if (attr.AttributeType == typeof(OrderedIndexUsageAttribute)) + { + _unnamedAreIndexable = true; + } + } + + if (hasFlagsAttribute) + { + throw new InvalidOperationException("Enums with the FlagsAttribute cannot be used with EnumInfo"); } var underlyingType = UnderlyingType; @@ -156,7 +174,9 @@ static unsafe EnumInfo() /// The index of the sorted enum numerical value, or -1 if not a named enum member. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int ValueIndexOf(T value) => _numericallyDistinctValues.GetValueOrDefault(value, -1); + public static int ValueIndexOf(T value) => !_unnamedAreIndexable + ? ValueOf(value) + : _numericallyDistinctValues.GetValueOrDefault(value, -1); /// /// Gets the ordered index of the unnamed enum value provided. This index is calculated by: @@ -168,6 +188,11 @@ static unsafe EnumInfo() /// public static int ValueIndexOfUnnamed(T value) { + if (!_unnamedAreIndexable) + { + return ValueOf(value); + } + if(_numericallyDistinctValues.TryGetValue(value, out var index)) { return index; @@ -184,8 +209,14 @@ public static int ValueIndexOfUnnamed(T value) return _all.Length + rawValue; } + /// + /// Returns the numerical value of the enum value provided in a type-safe way + /// + /// + /// + /// + /// private static unsafe TNumber ValueOf(TValue value) where TNumber : unmanaged where TValue : unmanaged - { if (sizeof(T) == sizeof(TNumber)) { @@ -231,3 +262,6 @@ private static T[] OrderedValues(bool byNumericValue) public static unsafe bool HasValue(int value) => _allEnumValuesRaw.Contains(*(uint*)&value); } + +[AttributeUsage(AttributeTargets.Enum)] +internal class OrderedIndexUsageAttribute : Attribute; diff --git a/sources/Input/Input/Implementations/SDL3/Devices/SdlKeyboard.cs b/sources/Input/Input/Implementations/SDL3/Devices/SdlKeyboard.cs index 2ee6ee9818..7d2ad257af 100644 --- a/sources/Input/Input/Implementations/SDL3/Devices/SdlKeyboard.cs +++ b/sources/Input/Input/Implementations/SDL3/Devices/SdlKeyboard.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Silk.NET.Input.KeyHandling; @@ -33,19 +34,8 @@ public static SdlKeyboard CreateDevice(uint sdlDeviceId, SdlInputBackend backend private SdlKeyboard(uint sdlDeviceId, nint uniqueId, SdlInputBackend backend) : base(backend, uniqueId, sdlDeviceId) { - Span> keyStates = stackalloc Button[(int)KeyName.EndCall + 1]; - int keyCount = 0; - for (var i = 0; i < 512; i++) - { - var keyName = (KeyName)i; - if (Enum.IsDefined(keyName)) - { - keyStates[keyCount++] = new Button((KeyName)i, false, 0f); - } - } - - _keyStates = keyStates[..keyCount].ToArray(); _modState = NativeBackend.GetModState(); + _keyStates = new ButtonStates(); State = new KeyboardState( keys: _keyStates, @@ -122,19 +112,16 @@ private void BeginRecordingSdl(WindowHandle sdlWindow) public void AddKeyEvent(in KeyboardEvent key) { - const float fraction = 1f / 255f; var keyName = SdlKeyConversions.ScancodeToKeyName(key.Scancode); // SdlToKeyName(key.Which); - if (Enum.IsDefined(keyName)) + if (_keyStates.IsDefined(keyName)) { var isDown = key.Down != 0; - - var index = EnumInfo.ValueIndexOf(keyName); - ref var button = ref _keyStates[index]; + var button = _keyStates[keyName]; var stateChanged = button.IsDown != isDown; - _keyStates[index] = new Button(keyName, key.Down != 0, key.Down * fraction); + _keyStates.SetKeyState(keyName, key.Down); - var shouldRecord = _textIsRecording == TextRecorderState.RecordingSdl + var shouldRecord = _textIsRecording == TextRecorderState.RecordingNoSdl && ((stateChanged && isDown) || (!stateChanged && key.Repeat != 0)); if (shouldRecord) { @@ -223,5 +210,54 @@ public unsafe void AddTextInputEvent(in TextInputEvent evt) private enum TextRecorderState {None, RecordingNoSdl, RecordingSdl} private TextRecorderState _textIsRecording; private ushort _modState; - private readonly Button[] _keyStates; + private const float _pressureMultiplier = 1f / 255f; + private readonly ButtonStates _keyStates; + + private class ButtonStates : IReadOnlyList> + { + private static readonly int _keyCount; + private readonly byte[] _keyPressures = new byte[_keyCount]; + private static readonly int[] _indices; + + static ButtonStates() + { + _indices = new int[512]; + for (var i = 0; i < 512; i++) + { + _indices[i] = Enum.IsDefined((KeyName)i) ? _keyCount++ : -1; + } + } + + public int SetKeyState(KeyName key, byte pressure) => _keyPressures[_indices[(int)key]] = pressure; + + public IEnumerator> GetEnumerator() + { + for (var i = 0; i < _keyCount; i++) + { + var index = _indices[i]; + if(index != -1) + { + yield return GetButton(index); + } + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public int Count => _keyCount; + + public Button this[int index] => GetButton(index); + public Button this[KeyName key] => GetButton((int)key); + + private Button GetButton(KeyName key) => GetButton((int)key); + private Button GetButton(int key) + { + var keyIdx = _indices[key]; + return CreateButton((KeyName)key, _keyPressures[keyIdx]); + } + + private Button CreateButton(KeyName key, byte pressure) => new(key, pressure > 0, pressure * _pressureMultiplier); + + public bool IsDefined(KeyName keyName) => _indices[(int)keyName] >= 0; + } } diff --git a/sources/Input/Input/JoystickButton.cs b/sources/Input/Input/JoystickButton.cs index 56ca678f3c..60bced499e 100644 --- a/sources/Input/Input/JoystickButton.cs +++ b/sources/Input/Input/JoystickButton.cs @@ -3,6 +3,7 @@ namespace Silk.NET.Input; /// /// Enumerates the buttons of a joystick. /// +[OrderedIndexUsage] public enum JoystickButton // todo : should we include XInput, PSX, and Nintendo button names here? { /// From 4d77d205490791350d88f33fcfd66197bf29b301 Mon Sep 17 00:00:00 2001 From: dom Date: Sun, 14 Sep 2025 17:24:45 -0400 Subject: [PATCH 38/39] mark method static --- .../Input/Input/Implementations/SDL3/Devices/SdlKeyboard.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sources/Input/Input/Implementations/SDL3/Devices/SdlKeyboard.cs b/sources/Input/Input/Implementations/SDL3/Devices/SdlKeyboard.cs index 7d2ad257af..7e4f3f84ac 100644 --- a/sources/Input/Input/Implementations/SDL3/Devices/SdlKeyboard.cs +++ b/sources/Input/Input/Implementations/SDL3/Devices/SdlKeyboard.cs @@ -114,7 +114,7 @@ public void AddKeyEvent(in KeyboardEvent key) { var keyName = SdlKeyConversions.ScancodeToKeyName(key.Scancode); // SdlToKeyName(key.Which); - if (_keyStates.IsDefined(keyName)) + if (ButtonStates.IsDefined(keyName)) { var isDown = key.Down != 0; var button = _keyStates[keyName]; @@ -258,6 +258,6 @@ private Button GetButton(int key) private Button CreateButton(KeyName key, byte pressure) => new(key, pressure > 0, pressure * _pressureMultiplier); - public bool IsDefined(KeyName keyName) => _indices[(int)keyName] >= 0; + public static bool IsDefined(KeyName keyName) => _indices[(int)keyName] >= 0; } } From 116ec2d1953addd0389d05e702b4c91f95c55d47 Mon Sep 17 00:00:00 2001 From: dom Date: Sun, 21 Sep 2025 18:15:49 -0400 Subject: [PATCH 39/39] begin pointer logic - mouse --- .../Pointers/SdlBoundedPointerDevice.cs | 22 +- .../SDL3/Devices/Pointers/SdlSharedMouse.cs | 209 ++++++++++++++---- .../SDL3/Devices/Pointers/SdlTouchScreen.cs | 5 - .../Devices/Pointers/SdlUnboundedMouse.cs | 9 +- .../Pointers/SdlUnboundedPointerTarget.cs | 45 ++-- .../Implementations/SDL3/SdlInputBackend.cs | 65 +++++- sources/Input/Input/InputReadOnlyList.cs | 14 +- sources/Input/Input/PointerButton.cs | 3 +- 8 files changed, 274 insertions(+), 98 deletions(-) diff --git a/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlBoundedPointerDevice.cs b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlBoundedPointerDevice.cs index b31809a4eb..e101d768ae 100644 --- a/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlBoundedPointerDevice.cs +++ b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlBoundedPointerDevice.cs @@ -10,33 +10,19 @@ namespace Silk.NET.Input.SDL3.Pointers; /// internal abstract class SdlBoundedPointerDevice : SdlDevice, IPointerDevice { - protected SdlBoundedPointerDevice(SdlInputBackend backend, IReadOnlyList targets, InputMarshal.ListOwner boundedPoints) : base(backend) + protected SdlBoundedPointerDevice(SdlInputBackend backend, nint silkId, + uint sdlDeviceId) : base(backend, silkId, sdlDeviceId) { - Targets = targets; - BoundedPoints = boundedPoints; } public abstract PointerState State { get; } - //public override string Name => NativeBackend.GetMouseNameForID(SdlDeviceId).ReadToString(); - - [field: MaybeNull] - public virtual IReadOnlyList Targets => - field ??= [Backend.BoundedPointerTarget]; + public abstract IReadOnlyList Targets { get; } /// /// Determines whether the should interpret /// as being bounded points. For all devices supported by this backend, only one target is supported at a time /// today. /// - public virtual bool IsBounded => true; - - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - public InputMarshal.ListOwner BoundedPoints => - field.List.Data is null ? field = InputMarshal.CreateList() : field; - - protected sealed override void Release() - { - - } + public bool IsBounded => true; } diff --git a/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlSharedMouse.cs b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlSharedMouse.cs index 1acc798041..e5f970c910 100644 --- a/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlSharedMouse.cs +++ b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlSharedMouse.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Numerics; +using Silk.NET.SDL; namespace Silk.NET.Input.SDL3.Pointers; @@ -9,58 +10,128 @@ internal class SdlSharedMouse : SdlBoundedPointerDevice, IMouse, ISdlDevice targets, InputMarshal.ListOwner boundedPoints) - : base(sdlDeviceId, backend, targets, boundedPoints) + public unsafe SdlSharedMouse(uint sdlDeviceId, nint uniqueId, SdlInputBackend backend) + : base(backend, uniqueId, sdlDeviceId) { - var buttons = InputMarshal.CreateList>(32); - var points = InputMarshal.CreateList(1); - _state = new MouseState(buttons.List.AsButtonList(), points.List, Vector2.Zero); - float x = 0, - y = 0; + IPointerTarget unboundedPointerTarget = backend.UnboundedPointerTarget; + _state = new MouseState(new ButtonReadOnlyList(_buttons), + new InputReadOnlyList(_points), Vector2.Zero); + float x = 0, y = 0; var buttonMask = NativeBackend.GetMouseState(x.AsRef(), y.AsRef()); - for (var i = 0; i < 32; i++) + var pos = new Vector2(x, y); + + for (var i = 0; i < EnumInfo.UniqueValues.Count; i++) { - InputMarshal.SetButtonState( - buttons, - new Button( - i switch - { - 1 => PointerButton.MiddleButton, - 2 => PointerButton.Secondary, - _ => (PointerButton)(i + 1), - }, - (buttonMask & (1 << i)) != 0, - 0 - ), - true - ); + var button = EnumInfo.UniqueValues[i]; + var pressed = IsPointerButtonPressedSdl(button, buttonMask); + _buttons.Add(new Button(button, pressed, pressed ? 1.0f : 0.0f)); } - var pos = new Vector2(x, y); - var bounds = Backend.BoundedPointerTarget.Bounds; + var window = NativeBackend.GetMouseFocus(); + var pressure = _state.Buttons[PointerButton.Primary].Pressure; + AddTargetPoint(window, pos, pressure); + + // add unbounded target + // var point = _unboundedPointerTarget.GetPoint(this, 0); + _targetListNoWindow = [unboundedPointerTarget]; + _targetListWithWindow = [null!, unboundedPointerTarget]; + } + + private void AddTargetPoint(WindowHandle window, Vector2 pos, float pressure) + { + if (!Backend.TryGetPointerTargetForWindow(window, out var windowTarget)) + { + AddUnboundedPoint(pos, pressure); + } + else + { + AddWindowPoint(pos, pressure, windowTarget); + } + } + + private void AddTargetPoint(uint windowId, Vector2 pos, float pressure) + { + if (!Backend.TryGetPointerTargetForWindow(windowId, out var windowTarget)) + { + AddUnboundedPoint(pos, pressure); + } + else + { + AddWindowPoint(pos, pressure, windowTarget); + } + } + + private void AddUnboundedPoint(Vector2 pos, float pressure) => + // add raw position (likely just 0, but that's ok for now) + _points.Add( + new TargetPoint(0, // todo: use a unique id + Flags: TargetPointFlags.NotPointingAtTarget, + Position: new Vector3(pos, 0), + NormalizedPosition: default, + Pointer: default, + Pressure: pressure, + Target: null + ) + ); + + private void AddWindowPoint(Vector2 pos, float pressure, IPointerTarget windowTarget) + { + var bounds = windowTarget.Bounds; var min = new Vector2(bounds.Min.X, bounds.Min.Y); var max = new Vector2(bounds.Max.X, bounds.Max.Y); - points - .GetUnderlyingList()! - .Add( - new TargetPoint( - 0, - TargetPointFlags.PointingAtTarget, - new Vector3(pos - min, 0), - new Vector3((pos - min) / (max - min), 0), - default, - 1.0f, - Backend.BoundedPointerTarget - ) - ); + + _points.Add( + new TargetPoint( + Id: 0, // todo - use a unique id + Flags: TargetPointFlags.PointingAtTarget, + Position: new Vector3(pos, 0), + NormalizedPosition: new Vector3((pos - min) / (max - min), 0), + Pointer: default, + Pressure: pressure, + Target: windowTarget + )); } - public static SdlSharedMouse CreateDevice(uint sdlDeviceId, SdlInputBackend backend) => throw new NotImplementedException(); + public static unsafe SdlSharedMouse CreateDevice(uint sdlDeviceId, SdlInputBackend backend) + { + var deviceName = backend.Sdl.GetMouseNameForID(sdlDeviceId); + nint uniqueId = 0; + if (!backend.AttemptUniqueId(deviceName, ref uniqueId)) + { + uniqueId = backend.FallbackUniqueId(sdlDeviceId, uniqueId); + } + + backend.Sdl.Free(deviceName); + return new SdlSharedMouse(sdlDeviceId, uniqueId, backend); + } public override string Name => $"{Backend.Name}: Shared/Global Mouse"; + protected override void Release() + { + } + MouseState IMouse.State => _state; + public override unsafe IReadOnlyList Targets + { + get + { + if (_mouseWindowId == 0) + { + return _targetListNoWindow; + } + + if (!Backend.TryGetPointerTargetForWindow(_mouseWindowId, out var target)) + { + return _targetListNoWindow; + } + + _targetListWithWindow[0] = target; + return _targetListWithWindow; + } + } + public ICursorConfiguration Cursor => Backend; public bool TrySetPosition(Vector2 position) @@ -75,4 +146,66 @@ public bool TrySetPosition(Vector2 position) } public override PointerState State => _state; + + public void AddMotion(in MouseMotionEvent evtMotion) + { + _mouseWindowId = evtMotion.WindowID; + var movementRelative = new Vector2(evtMotion.Xrel, evtMotion.Yrel); + _accumulatedMotion += movementRelative; + + // add clear old point, add new point + _points.Clear(); + AddTargetPoint(_mouseWindowId, _accumulatedMotion, 0); + } + + public void AddButtonEvent(in MouseButtonEvent evtButton) + { + var button = PointerButton.Primary + evtButton.Button; + var idx = EnumInfo.ValueIndexOfUnnamed(button); + const float mult = 1 / 255f; + _buttons[idx] = new Button(button, evtButton.Down > 0, evtButton.Down * mult); + } + + public void AddWheelEvent(in MouseWheelEvent evtWheel) + { + _mouseScroll[0] += evtWheel.X; + _mouseScroll[1] += evtWheel.Y; + + var hMagnitude = MathF.Abs(_mouseScroll[0]); + var vMagnitude = MathF.Abs(_mouseScroll[1]); + + if (hMagnitude >= 1) + { + // horizontal scroll "tick" + _mouseScroll.X = 0; + } + + if (vMagnitude >= 1) + { + // vertical scroll "tick" + _mouseScroll.Y = 0; + } + + // todo - actually do stuff + throw new NotImplementedException(); + } + + private static bool IsPointerButtonPressedSdl(PointerButton button, uint state) + { + var index = EnumInfo.ValueIndexOf(button); + if (index < 0 || index >= 32) + { + return false; + } + + return (state & (1 << index)) != 0; + } + + private uint _mouseWindowId; + private Vector2 _mouseScroll = default; + private Vector2 _accumulatedMotion = default; + private readonly List> _buttons = []; + private readonly List _points = new(); + private readonly IPointerTarget[] _targetListNoWindow; + private readonly IPointerTarget[] _targetListWithWindow; } diff --git a/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlTouchScreen.cs b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlTouchScreen.cs index 00d902787b..859fa76eca 100644 --- a/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlTouchScreen.cs +++ b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlTouchScreen.cs @@ -14,11 +14,6 @@ public bool Equals(IInputDevice? other) throw new NotImplementedException(); } - public override uint RefreshIdFromBackend() - { - throw new NotImplementedException(); - } - public override string Name { get diff --git a/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlUnboundedMouse.cs b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlUnboundedMouse.cs index 4088a83cbd..a48289bd19 100644 --- a/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlUnboundedMouse.cs +++ b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlUnboundedMouse.cs @@ -5,14 +5,19 @@ namespace Silk.NET.Input.SDL3.Pointers; -internal class SdlUnboundedMouse : SdlDevice, IMouse +internal class SdlUnboundedMouse : SdlDevice, IMouse, ISdlDevice { - public SdlUnboundedMouse(uint sdlDeviceId, SdlInputBackend backend) : base(sdlDeviceId, backend) + private SdlUnboundedMouse(uint sdlDeviceId, nint silkId, SdlInputBackend backend) : base(backend, silkId, sdlDeviceId) { } public MouseState State => throw new NotImplementedException(); + public static SdlUnboundedMouse? CreateDevice(uint sdlDeviceId, SdlInputBackend backend) + { + + } + public override string Name => NativeBackend.GetMouseNameForID(SdlDeviceId).ReadToString(); public ICursorConfiguration Cursor => Backend; diff --git a/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlUnboundedPointerTarget.cs b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlUnboundedPointerTarget.cs index 368bd3e096..1cee560d8e 100644 --- a/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlUnboundedPointerTarget.cs +++ b/sources/Input/Input/Implementations/SDL3/Devices/Pointers/SdlUnboundedPointerTarget.cs @@ -18,26 +18,33 @@ internal class SdlUnboundedPointerTarget(SdlInputBackend backend) : IPointerTarg public Box3D Bounds => _bounds; - public int GetPointCount(IPointerDevice pointer) + public int GetPointCount(IPointerDevice pointer) => IsValidDevice(pointer) ? pointer.State.Points.Count : 0; + + public TargetPoint GetPoint(IPointerDevice pointer, int pointIdx) { - if (pointer is not SdlDevice device) - { - return 0; - } - - if (pointer.State.Points.Count == 0) - { - return 0; - } - - if (device.Backend != backend) - { - return device.Backend.UnboundedPointerTarget.GetPointCount(pointer); - } - - return (device.Backend.Mode & CursorModes.Unbounded) != 0 ? 1 : 0; + var point = pointer.State.Points[pointIdx]; + var valid = IsValidDevice(pointer); + return new TargetPoint( + Id: point.Id, // todo : follow spec with unique ids + Flags: valid ? TargetPointFlags.PointingAtTarget : TargetPointFlags.NotPointingAtTarget, + Position: point.Position, + NormalizedPosition: default, + Pointer: new Ray3D(), + Pressure: point.Pressure is > 1 or < 0 + ? point.Pressure is 0 + ? 0 + : 1 + : point.Pressure, + Target: this + ); } - public TargetPoint GetPoint(IPointerDevice pointer, int point) => - throw new NotImplementedException(); + private bool IsValidDevice(IPointerDevice pointer) + { + // todo - do we really want to limit this to SDL devices? or to our specific sdl backend? + // i dont think so, but.... technically... + + var device = pointer as SdlDevice; + return device is not null && device.Backend == backend && (device.Backend.Mode & CursorModes.Unbounded) != 0; + } } diff --git a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs index e2791cc228..170278129f 100644 --- a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs +++ b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs @@ -119,10 +119,6 @@ public unsafe SdlInputBackend(SdlPlatformInfo info) // NOTE: Be careful where these are used! public SdlPlatformInfo Info { get; } - [field: MaybeNull] - public SdlBoundedPointerTarget BoundedPointerTarget => - field ??= new SdlBoundedPointerTarget(this); - [field: MaybeNull] public SdlUnboundedPointerTarget UnboundedPointerTarget => field ??= new SdlUnboundedPointerTarget(this); @@ -187,7 +183,7 @@ public void Update(IInputHandler? handler = null) while (_pumpedEvents.TryDequeue(out var evt)) { - ProcessEvent(ref evt, handler); + ProcessEvent(in evt, handler); } foreach (var device in _devices) @@ -218,7 +214,7 @@ private enum QueuedEventType : byte BoundedPointerTargetUpdate, } - private ulong GetTimestamp(ref readonly Event @event) => + private ulong GetTimestamp(in Event @event) => unchecked((ulong)(_epoch + (@event.Common.Timestamp * _ticksPerNanosecond))); private unsafe byte OnEvent(void* arg0, Event* arg1) @@ -228,9 +224,9 @@ private unsafe byte OnEvent(void* arg0, Event* arg1) return 1; } - private void ProcessEvent(ref Event evt, IInputHandler handler) + private void ProcessEvent(in Event evt, IInputHandler handler) { - var timestamp = GetTimestamp(ref evt); + var timestamp = GetTimestamp(in evt); Debug.Assert(timestamp >= _previousTimestamp, "Events out of order"); _previousTimestamp = timestamp; @@ -357,6 +353,34 @@ private void ProcessEvent(ref Event evt, IInputHandler handler) } break; } + case >= EventType.MouseMotion and <= EventType.MouseAdded: + { + if(!TryGetOrCreateDevice(evt.Mdevice.Which, out var mouse)) + { + return; + } + + if (type is EventType.MouseAdded) + { + return; + } + + switch (type) + { + case EventType.MouseMotion: + mouse.AddMotion(evt.Motion); + break; + case EventType.MouseButtonDown: + case EventType.MouseButtonUp: + mouse.AddButtonEvent(evt.Button); + break; + case EventType.MouseWheel: + mouse.AddWheelEvent(evt.Wheel); + break; + } + + break; + } } switch (type) @@ -539,4 +563,29 @@ private class EventQueue [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Clear() => _events.Clear(); } + + internal unsafe bool TryGetPointerTargetForWindow(WindowHandle window, [NotNullWhen(true)] out IPointerTarget? target) + { + if (window.Handle == null) + { + target = null; + return false; + } + + var id = _sdl.GetWindowID(window); + return TryGetPointerTargetForWindow(id, out target); + } + + internal bool TryGetPointerTargetForWindow(uint id, [NotNullWhen(true)] out IPointerTarget? target) + { + if (id == 0) + { + target = null; + return false; + } + + // todo : get the SDL window (or other window) with the given ID + target = null; + throw new NotImplementedException(); + } } diff --git a/sources/Input/Input/InputReadOnlyList.cs b/sources/Input/Input/InputReadOnlyList.cs index db3f1546a3..aa176623a4 100644 --- a/sources/Input/Input/InputReadOnlyList.cs +++ b/sources/Input/Input/InputReadOnlyList.cs @@ -9,24 +9,24 @@ namespace Silk.NET.Input; /// The Silk.NET.Input type to store. public readonly struct InputReadOnlyList : IReadOnlyList { - internal object Data { get; } - - internal InputReadOnlyList(object data) => Data = data; + internal object Data => _list; /// /// Creates an from a . /// /// The list to copy. - public InputReadOnlyList(IReadOnlyList other) => this = InputMarshal.Clone(other).List; + public InputReadOnlyList(IReadOnlyList other) => _list = other; /// - public IEnumerator GetEnumerator() => InputMarshal.EnumerateList(this); + public IEnumerator GetEnumerator() => _list.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// - public int Count => InputMarshal.GetListCount(this); + public int Count => _list.Count; /// - public T this[int index] => InputMarshal.ElementAt(this, index); + public T this[int index] => _list[index]; + + private readonly IReadOnlyList _list; } diff --git a/sources/Input/Input/PointerButton.cs b/sources/Input/Input/PointerButton.cs index f0874e646e..f9b498dd3f 100644 --- a/sources/Input/Input/PointerButton.cs +++ b/sources/Input/Input/PointerButton.cs @@ -3,12 +3,13 @@ namespace Silk.NET.Input; /// /// Enumerates the buttons available on pointer devices. /// +[OrderedIndexUsage] public enum PointerButton { /// /// An unrecognised button. /// - Unknown, + Unknown = JoystickAxis.Unknown, /// /// The primary button e.g. left click.