diff --git a/documentation/proposals/Proposal - Multi-Backend Input.md b/documentation/proposals/Proposal - Multi-Backend Input.md index aa13242989..377cec4641 100644 --- a/documentation/proposals/Proposal - Multi-Backend Input.md +++ b/documentation/proposals/Proposal - Multi-Backend Input.md @@ -33,7 +33,7 @@ Cases where the **user** word is used without the **end** prefix can be assumed # Usage Examples ```cs -IWindowHandlesSource someWindow = null!; +INativeWindow someWindow = null!; var inputContext = someWindow.CreateInput(); inputContext.Update(); inputContext.Gamepads.ThumbstickMove += @event => @@ -43,7 +43,7 @@ inputContext.Gamepads.ThumbstickMove += @event => var isButtonDown = inputContext.Gamepads.Any(gamepadState => gamepadState.Buttons[JoystickButton.A]); ``` ```cs -IWindowHandlesSource someWindow = null!; +INativeWindow someWindow = null!; var inputContext = new InputContext(); inputContext.Update(); inputContext.Backends.Add(someWindow.CreateInputBackend()); @@ -51,10 +51,9 @@ inputContext.Backends.Add(someWindow.CreateInputBackend()); // inputContext.Backends.Add(new OpenXRInputBackend(...)); ``` ```cs -class MyThing +class Program : ISurfaceApplication { - [SilkEntryPoint] - public static void Run(ISurface surface) + public static void Initialize(TSurface surface) where TSurface : Surface { var inputContext = surface.CreateInput(); surface.Update += _ => inputContext.Update(); @@ -62,8 +61,9 @@ class MyThing { Console.WriteLine($"Thumbstick {@event.Index} moved from {@event.OldValue} to {@event.NewValue}"); }; - surface.Run(); } + + public static void Main() => ISurfaceApplication.Run(); } ``` @@ -74,25 +74,23 @@ Similar to Windowing 3.0, a reference implementation will be included in the mai ```cs public static class InputWindowExtensions { - public static IInputBackend CreateInputBackend(this WindowHandles window); - public static IInputBackend CreateInputBackend(this IWindowHandlesSource window); - public static InputContext CreateInput(this WindowHandles window); - public static InputContext CreateInput(this IWindowHandlesSource window); + public static IInputBackend CreateInputBackend(this INativeWindow window); + public static InputContext CreateInput(this INativeWindow window); } ``` -The `CreateInputBackend` will create an instance of the reference implementation for the given `WindowHandles`. The `IWindowHandlesSource` overloads just forward to the `WindowHandles` overload. This is because `ISurface` will implement `IWindowHandlesSource`, so the extension methods will be usable on an `ISurface` without having a hard reference between Windowing and Input. +The `CreateInputBackend` will create an instance of the reference implementation for the given `INativeWindow`. `Surface` will implement `INativeWindow`, so the extension methods will be usable on a `Surface` without having a hard reference between Windowing and Input. The `CreateInput` methods simply return an `InputContext` preconfigured with the backend created by `CreateInputBackend` for ease of use. -Please see the Windowing 3.0 proposal for `IWindowHandlesSource` and `WindowHandles`. +Please see the Windowing 3.0 proposal for `INativeWindow`. # Devices Input devices all inherit from a root interface. ```cs -public interface IInputDevice +public interface IInputDevice : IEquatable { nint Id { get; } string Name { get; } @@ -105,6 +103,20 @@ public interface IInputDevice All devices originate from a backend. +An `IInputDevice` object shall be equatable to any such object retrieved from the same backend where `Id` is equal. + +`IInputDevice` objects must not store any managed state, and if there is a requirement for this in a future extension of +this API then this **must** be defined in such a way that the state storage and lifetime is user-controlled. While +`IInputDevice` objects are equatable based on `Id`s, if a physical device disconnects and reconnects the `IInputBackend` +does not provide a guarantee that the same object will be returned (primarily because doing so would require the +`IInputBackend` to keep track of every object it's ever created), rather a "compatible" one that acts identically to +the original object. This is completely benign if the object is nothing but a wrapper to the backend anyway. If there is +unmanaged state (e.g. a handle to a device that must be explicitly closed upon disconnection), then it is expected that +even in the event of reconnection, old objects (e.g. created with a now-disposed handle) **shall** still work for the +newly-reconnected device. A common way this could be implemented is storing the handles in the `IInputBackend` +implementation instead in the form of a mapping of physical device IDs (`Id`) to those handles. This solves the object +lifetime problem while also not adding undue complications to user code. + # Backends ```cs @@ -121,7 +133,15 @@ public interface IInputBackend `Id` is a globally-unique integral identifier for this backend. -`Devices` enumerates all of the **connected** devices available from this input backend. When a device is disconnected, its `IInputDevice` object should be discarded by all that consumed it, as it can not be relied upon for being reused by the input backend. An implementation is welcome to reuse old objects, but this is strictly implementation-defined. A device not being present in the `Devices` list is sufficient evidence that a device has been disconnected. +`Devices` enumerates all of the **connected** devices available from this input backend. When a device is disconnected, +its `IInputDevice` **shall** no longer function and will not be enumerated by this list. When a device is connected, an +`IInputDevice` with that physical device ID **shall** be added to this list. In addition, upon connection any past +`IInputDevice` objects previously enumerated by this list on this `IInputBackend` instance **shall** also regain +function 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 `IInputDevice` instance added to this list. An +implementation is welcome to reuse old objects, but this is strictly implementation-defined. A device not being present +in the `Devices` (checked using `IInputDevice`'s `IEquatable` implementation) list is sufficient evidence +that a device has been disconnected. `Update` will update the state of all devices contained within this input backend. The value of the `State` properties on each device must not change until this method is called. This is a departure from 1.0's and 2.0's model of updating state as soon as new information is available, which has resulted in lots of inconsistencies in the past. @@ -159,12 +179,12 @@ All handler methods are called in the order that the state changes happened in t The `IInputHandler` passed into `Update` may implement multiple other handler interfaces (as defined below), and if the actor implements an extra interface (such as `IMouseInputHandler` defined below) that would allow the backend to forward more events to the handler, the backend must do so via type checking. That is, if `handler` is an instance of `IMouseInputHandler`, any mouse events are delivered to that actor. But if `handler` does not implement `IMouseInputHandler`, no mouse events will be delivered. All events, including those that were not delivered due to the actor not implementing a necessary interface, must be discarded at the end of the `Update` call. Note that during the `Update` call, a backend must only update the device's state in the order that the events are delivered. For example when `IInputBackend.Update` is called: -1. The backend has a queued "mouse down" event. -2. The backend updates the `State` of the relevant `IMouse` for that button press. -3. The backend calls `HandleButtonDown` on the `IMouseInputHandler` (if applicable). -4. The backend has a queued "mouse up" event. -5. The backend updates the `State` of the relevant `IMouse` for that button release. -6. The backend calls `HandleButtonUp` on the `IMouseInputHandler` (if applicable). +1. The backend has a queued "pointer down" event for a mouse, for example. +2. The backend updates the `State` of the relevant `IPointer` for that button press. +3. The backend calls `HandleButtonChanged` with `IsDown` set to `true` on the `IPointerInputHandler` (if applicable). +4. The backend has a queued "pointer up" event on that mouse. +5. The backend updates the `State` of the relevant `IPointer` for that button release. +6. The backend calls `HandleButtonChanged` with `IsDown` set to `false` on the `IPointerInputHandler` (if applicable). This allows the actor to work with the whole device state with the device state being representative of the time that the original event occurred. @@ -177,7 +197,7 @@ All of the `Devices` and `Update`s are aggregated and coordinated by a central i ```cs public partial class InputContext { - public Mice Mice { get; } + public Pointers Pointers { get; } public Keyboards Keyboards { get; } public Gamepads Gamepads { get; } public Joysticks Joysticks { get; } @@ -190,11 +210,11 @@ public partial class InputContext The central input object acts as the main entry point into the Input API, and is responsible for comparing the state reported by the devices for differences between `Update` calls (raising events as necessary). -`Mice`, `Keyboards`, `Gamepads`, and `Joysticks` are all custom `IReadOnlyList` types for enumerating the devices. However, these custom types also contain the events. This is so we can "scope" the events, rather than putting them at the top-level and having to call the events `MouseButtonDown`, `JoystickButtonDown`, etc. +`Pointers`, `Keyboards`, `Gamepads`, and `Joysticks` are all custom `IReadOnlyList` types for enumerating the devices. However, these custom types also contain the events. This is so we can "scope" the events, rather than putting them at the top-level and having to call the events `PointerButtonChanged`, `JoystickButtonChanged`, etc. By virtue of the `State` properties not updating until `IInputBackend.Update` is called, the states of the devices enumerated by the lists will not change until `Update` is called. -`Update` will call `IInputBackend.Update` on each of the `Backends`, passing in a handler which implements `IInputHandler`, `IMouseInputHandler`, `IKeyboardInputHandler`, `IGamepadInputHandler`, and `IJoystickInputHandler` with each of the methods invoking a matching event defined in "Custom List Types" or on the input context itself (such as `ConnectionChanged`). +`Update` will call `IInputBackend.Update` on each of the `Backends`, passing in a handler which implements `IInputHandler`, `IPointerInputHandler`, `IKeyboardInputHandler`, `IGamepadInputHandler`, and `IJoystickInputHandler` with each of the methods invoking a matching event defined in "Custom List Types" or on the input context itself (such as `ConnectionChanged`). `Backends` is a mutable list of input backends. Until `Update` is called again, no device lists, state, etc on the context will be updated. The `ConnectionChanged` rules above will still be respected e.g. when you remove a backend, all of its devices will have a disconnected event raised for them. @@ -205,44 +225,40 @@ By virtue of the `State` properties not updating until `IInputBackend.Update` is These are relatively simple list wrappers with the events fired when state changes. ```cs -public partial class Mice : IReadOnlyList +public partial class Pointers : IReadOnlyList { - public MouseClickConfiguration ClickConfiguration { get; set; } - public event Action? ButtonDown; - public event Action? ButtonUp; - public event Action? Click; - public event Action? DoubleClick; - public event Action? CursorMove; - public event Action? Scroll; + public PointerClickConfiguration ClickConfiguration { get; set; } + public event Action>? ButtonChanged; + public event Action? Click; + public event Action? DoubleClick; + public event Action? PointChanged; + public event Action? MouseScroll; } public partial class Keyboards : IReadOnlyList { - public event Action? KeyDown; - public event Action? KeyUp; + public event Action? KeyChanged; public event Action? KeyChar; } public partial class Gamepads : IReadOnlyList { - public event Action? ButtonDown; - public event Action? ButtonUp; + public event Action>? ButtonChanged; public event Action? ThumbstickMove; public event Action? TriggerMove; } public partial class Joysticks : IReadOnlyList { - public event Action? ButtonDown; - public event Action? ButtonUp; + public event Action>? ButtonChanged; public event Action? AxisMove; public event Action? HatMove; } ``` -All events will be raised when their matching handler methods are called, with the exception of `Click` and `DoubleClick` which are implemented on top of `ButtonDown` and `ButtonUp` respectively (as in 2.X). +All events will be raised when their matching handler methods are called, with the exception of `Click` and `DoubleClick` which are implemented on top of `ButtonChanged` (as in 2.X). -`DoubleClick` will be raised if `Mice.ButtonDown` is raised two consecutive times within `MouseClickConfiguration.DoubleClickTime` milliseconds, and the `MouseState.Position`'s `X` or `Y` did not change more than `MouseClickConfiguration.DoubleClickRange` between the two events. If these conditions are not met, `Click` is raised instead. For the avoidance of doubt, the behaviour of the click implementation here is exactly as it is in 2.X. +`DoubleClick` will be raised if `Pointers.ButtonChanged` is raised two consecutive times with `IsDown` set to true within `MouseClickConfiguration.DoubleClickTime` milliseconds, and the `MouseState.Position`'s `X` or `Y` did not change more than `MouseClickConfiguration.DoubleClickRange` between the two events. If these conditions are not met, `Click` is raised instead. For the avoidance of doubt, the behaviour of the click implementation here is exactly as it is in 2.X. **INFORMATIVE TEXT:** The click implementation may also even be exactly the same implementation as it is 2.X copied and pasted into 3.0, given a lot of research and effort went into this by the community contributor that implemented it. @@ -257,34 +273,40 @@ This will be configurable on `Mice` (i.e. via `InputContext.Mice.ClickConfigurat Unlike 1.0 and 2.0, this proposal uses `readonly record struct`s as their only argument for the event action. This allows us to provide more information to the event handlers without breaking in the future. These types are farily simple: ```cs -public readonly record struct ConnectionEvent(IInputDevice Device, bool IsConnected); -public readonly record struct KeyDownEvent(IKeyboard Keyboard, Key Key, bool IsRepeat); -public readonly record struct KeyUpEvent(IKeyboard Keyboard, Key Key); -public readonly record struct KeyCharEvent(IKeyboard Keyboard, char Character); -public readonly record struct MouseDownEvent(IMouse Mouse, Vector2 Position, MouseButton Button); -public readonly record struct MouseUpEvent(IMouse Mouse, Vector2 Position, MouseButton Button); -public readonly record struct MouseMoveEvent(IMouse Mouse, Vector2 Position, Vector2 Delta); -public readonly record struct MouseScrollEvent(IMouse Mouse, Vector2 Position, Vector2 WheelPosition, Vector2 Delta); -public readonly record struct MouseClickEvent(IMouse Mouse, Vector2 Position, MouseButton Button); -public readonly record struct JoystickDownEvent(IJoystick Joystick, JoystickButton Button); -public readonly record struct JoystickUpEvent(IJoystick Joystick, JoystickButton Button); -public readonly record struct JoystickHatMoveEvent(IJoystick, Vector2 Value, Vector2 Delta); -public readonly record struct JoystickAxisMoveEvent(IJoystick Joystick, int Axis, float Value, float Delta); -public readonly record struct GamepadDownEvent(IGamepad Gamepad, JoystickButton Button); -public readonly record struct GamepadUpEvent(IGamepad Gamepad, JoystickButton Button); -public readonly record struct GamepadThumbstickMoveEvent(IJoystick, Vector2 Value, Vector2 Delta); -public readonly record struct GamepadTriggerMoveEvent(IJoystick Joystick, int Axis, float Value, float Delta); +public readonly record struct ConnectionEvent(IInputDevice Device, long Timestamp, bool IsConnected); +public readonly record struct KeyChangedEvent(IKeyboard Keyboard, long Timestamp, Button Key, Button Previous, bool IsRepeat, KeyModifiers Modifiers); +public readonly record struct KeyCharEvent(IKeyboard Keyboard, long Timestamp, char? Character); +public readonly record struct ButtonChangedEvent(IButtonDevice Device, long Timestamp, Button Button, Button Previous) where T : struct, Enum; +public readonly record struct PointChangedEvent(IPointer Pointer, long Timestamp, TargetPoint? OldPoint, TargetPoint? NewPoint); +public readonly record struct PointerGripChangedEvent(IPointer Pointer, long Timestamp, float GripPressure, float Delta); +public readonly record struct PointerTargetChangedEvent(IPointer Pointer, long Timestamp, IPointerTarget Target, bool IsAdded, Box3F OldBounds, Box3F NewBounds); +public readonly record struct MouseScrollEvent(IMouse Mouse, long Timestamp, TargetPoint Point, Vector2 WheelPosition, Vector2 Delta); +public readonly record struct PointerClickEvent(IPointer Pointer, long Timestamp, TargetPoint Point, MouseButton Button); +public readonly record struct JoystickHatMoveEvent(IJoystick Joystick, long Timestamp, Vector2 Value, Vector2 Delta); +public readonly record struct JoystickAxisMoveEvent(IJoystick Joystick, long Timestamp, int Axis, float Value, float Delta); +public readonly record struct GamepadThumbstickMoveEvent(IGamepad Gamepad, long Timestamp, Vector2 Value, Vector2 Delta); +public readonly record struct GamepadTriggerMoveEvent(IGamepad Gamepad, long Timestamp, int Axis, float Value, float Delta); ``` +`Timestamp` shall be the `Stopwatch.GetTimestamp()` at which the event was raised. This allows the user to get the +precise time of the event's occurrence, which is not otherwise possible given the requirement for state changes to be +enacted only upon a call to `Update`. + This is the part of this proposal that incorporates the ideas in Enhanced Input Events, and is why this proposal supersedes that one. -One final point to note is that throughout the rest of the proposal the following type will be used: +One final point to note is that throughout the rest of the proposal the following types will be used: ```cs public struct InputReadOnlyList : IReadOnlyList { public InputReadOnlyList(IReadOnlyList other); } + +public struct ButtonReadOnlyList : IReadOnlyList> where T : struct, Enum +{ + public ButtonReadOnlyList(IReadOnlyList> other); + public Button this[T name] { get; } +} ``` The Silk.NET team wishes to reserve the right to add more constructors to this type as it sees fit. @@ -293,84 +315,226 @@ This exists so that, should the Silk.NET choose to, we can optimize the lookup o **INFORMATIVE TEXT:** For example, for joystick and mouse buttons we could use a fixed-sized bit buffer where each bit represents an individual button: 1 for pressed, 0 for unpressed. But for something like keyboard input where there are a large amount of keys, we can't do that and will likely use `Memory` instead. -# Mouse Input +# Devices with Buttons + +We have decided to converge functionality relating to button presses given that these are common to most device types. + +```cs +public readonly record struct Button(T Name, bool IsDown, float Pressure) where T : struct, Enum +{ + public static implicit operator bool(Button state) => state.IsDown; +} + +public interface IButtonDevice : IInputDevice +{ + ButtonReadOnlyList State { get; } +} +``` + +A common input handler will be exposed for these types: +```cs +public interface IButtonInputHandler where T : struct, Enum +{ + void HandleButtonChanged(ButtonChangedEvent @event); +} +``` + +`HandleButtonChanged` must be called when any of the `Button` properties are changed. + +# Pointer Input As discussed earlier, the interface will be very simple. ```cs -public interface IMouse : IInputDevice +public interface IPointer : IButtonDevice { - ref readonly MouseState State { get; } - ICursorConfiguration Cursor { get; } - void SetPosition(Vector2 pos); + PointerState State { get; } + ButtonReadOnlyList IButtonDevice.State => State.Buttons; + IReadOnlyList Targets { get; } } ``` `State` is the device state as defined earlier. -`Cursor` contains the cursor configuration. This isn't actually state that the end user can change, and has been made an interface rather than a state struct accordingly. +`Targets` defines the targets this pointer device can naturally point to. For a touch screen, this could be a list of +displays. For a mouse, this will contain a target for the windowed cursor mode and a target for the "raw mouse input" +mode. -`SetPosition` allows moving the mouse cursor without the end user physically moving their mouse. Please note that this does not immediately update `State` with the new value - the changes will be reflected next time `IInputBackend.Update` is called. +A "pointer" is an abstraction over any mechanism by which a user can point to specific coordinates on a "target". +For instance, a user could use a mouse cursor to point to a target representing the window bounds. Alternatively, that +mouse could be used to point to an arbitrary place in an infinitely large ("unbounded" herein) target (this is "raw +mouse input"). Other examples of this abstraction's application are fingers being used to point to a specific place on a +touch surface, or pens pointing to a specific place on a surface. The base abstraction is deliberately extremely vague +to meet the need of "getting a position within certain bounds," which is all people really want in most cases for a +"cursor" anyway. -The device state returned by `State` fills out the following structure: +A target is defined as follows: +```cs +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 . + /// + Box3F 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 + /// 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(IPointer 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(IPointer pointer, int point); +} +``` +**FUTURE IMPROVEMENT:** This interface could be expanded to provide rotation of the target itself as well, representing +a full `Transform` structure for the space. At this time, this was not deemed necessary for inclusion, but should be a +trivial extension to add in the future. + +**INFORMATIVE TEXT**: Furthermore, it is our eventual goal to be able to support considering VR hands as pointer devices +through raycasting. Such a future proposal will involve a way to create a child target within the bounds of this target +a `IPointerTarget` from that which represents the 3D world (i.e. the entire VR world is a target, and the point +representing the hand is _within_ that target - with the `TargetPoint` being populated using ``XrPosef values), where +calculation of points on those child targets are calculated using raycasting from that position. + +The functionality of these APIs are described in the XML documentation inline. + +A point shall be defined as follows: ```cs -public readonly record struct MouseState -( - MouseButtonState Buttons, - Vector2 Position, - Vector2 WheelPosition -); +/// +/// 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, + Ray3F Pointer, + float Pressure, + IPointerTarget Target +) { + public bool IsValid => (Flags & Flags.PointingAtTarget) != Flags.NotPointingAtTarget; +} ``` -`MouseButtonState` is defined as: +The `PointerState` shall be defined as follows: ```cs -public readonly record struct MouseButtonState -( - InputReadOnlyList Down -) +public class PointerState { - public bool this[MouseButton btn] { get; } + public ButtonReadOnlyList Buttons { get; } + public InputReadOnlyList Points { get; } + public float GripPressure { get; } } ``` -The indexer returns `true` if a particular button is pressed, false otherwise. If the developer wishes to enumerate the button state, they must explicitly enumerate through the `Down` buttons. +`Points` represents the `TargetPoint`s this pointer is pointing at on its "native targets" i.e. that which is enumerated +by `IPointer.Targets`. -**INFORMATIVE TEXT:** This struct only exists so we can implement an indexer that accepts a `MouseButton`, given that `Down` is effectively just a list and only takes an `int` index as a result. +`GripPressure` represents the amount of pressure the user is applying to the device itself (e.g. the pen barrel) between +`0.0` and `1.0`. This shall be `1.0` if unavailable. -The indexer will be implemented in terms of `Down`, which is the only property that a backend will need to set. +Additional APIs to construct `PointerState` will be added as appropriate. -Changes to `MouseState` also have matching handler methods which are subject to the handler method rules i.e. the backend should call them in the order in which the backend received the events where possible etc (read the Input Handlers section). +Changes to `PointerState` also have matching handler methods which are subject to the handler method rules i.e. the backend should call them in the order in which the backend received the events where possible etc (read the Input Handlers section). For the avoidance of doubt, this implies that `Timestamp` in ascending order. +The handler for pointer inputs shall be defined as follows: ```cs -public interface IMouseInputHandler : IInputHandler +public interface IPointerInputHandler : IButtonInputHandler { - void HandleButtonDown(MouseDownEvent @event); - void HandleButtonUp(MouseUpEvent @event); - void HandleCursorMove(MouseCursorEvent @event); - void HandleScroll(MouseScrollEvent @event); + void HandleTargetChanged(PointerTargetChangedEvent @event); + void HandlePointChanged(PointChangedEvent @event); + void HandleGripChanged(PointerGripChangedEvent @event); } ``` -`HandleButtonDown` must be called when a button is added to `MouseState.Buttons.Down`. +`HandleTargetChanged` must be called when properties on an `IPointerTarget` within `IPointer.Targets` changes, or when +an `IPointerTarget` is added or removed to/from `IPointer.Targets`. `IsAdded` shall be `true` if it has been added, +`false` if it has been removed. -`HandleButtonUp` must be called when a button is removed from `MouseState.Buttons.Down`. +`HandlePointChanged` must be called when a point within `PointerState.Points` changes. -`HandleCursorMove` must be called when `MouseState.Position` changes. +`HandleGripChanged` must be called when `PointerState.GripPressure` changes. -`HandleScroll` must be called when `MouseState.WheelPosition` changes. +These device interfaces and related APIs are designed to mirror physical hardware that the user uses to point at a +target. -Note that the click events, just as in 2.X, are not implemented by the backend and instead implemented by the input context because it is not a requirement that backends can record clicks. **INFORMATIVE TEXT:** The original reason for this requirement in 2.X is because GLFW doesn't actually send click and double click events. +**FUTURE IMPROVEMENT:** There are many cases where applications would work better with an abstraction that creates +"virtual pointers" for each point, rather that the points being spread across many logical devices. These can be added +as non-breaking extensions to the `Pointers` class in the future, as the input context is intended to be the aggregator +of device inputs. -## Enums +**FUTURE IMPROVEMENT:** The `Pointers` class is also expected to be the site of gesture recognition when proposed in the +future. +`PointerButton` shall be defined as follows: ```cs -public enum MouseButton +public enum PointerButton { - Unknown, - LeftButton, - RightButton, - MiddleButton, + Primary, + Secondary, + Button3, + MiddleButton = Button3, Button4, Button5, Button6, @@ -398,24 +562,76 @@ public enum MouseButton Button28, Button29, Button30, - Button31 + EraserTip = Button30, + Button31, + Button32 +} +``` + +There will be derived types for different types of pointers. + +## Mouse Input + +```cs +public interface IMouse : IPointer +{ + MouseState State { get; } + PointerState IPointer.State => State; + ICursorConfiguration Cursor { get; } + bool TrySetPosition(Vector2 position); +} +``` + +`Cursor` contains the cursor configuration. This isn't actually state that the end user can change, and has been made an interface rather than a state struct accordingly. + +`TrySetPosition` allows moving the mouse cursor without the end user physically moving their mouse. Please note that this does not immediately update `State` with the new value - the changes will be reflected next time `IInputBackend.Update` is called. + +The device state returned by `State` fills out the following structure: + +```cs +public class MouseState : PointerState +{ + public Vector2 WheelPosition { get; } } ``` +`WheelPosition` represents the number of times the scroll wheel (e.g. ratchets) has scrolled in a particular direction +between the previous and latest times the input was captured by the backend. + +Additional APIs to construct `MouseState` will be added as appropriate. + +Changes to `MouseState` also have matching handler methods which are subject to the handler method rules i.e. the backend should call them in the order in which the backend received the events where possible etc (read the Input Handlers section). For the avoidance of doubt, this implies that `Timestamp` in ascending order. + +```cs +public interface IMouseInputHandler : IButtonInputHandler +{ + void HandleScroll(MouseScrollEvent @event); +} +``` + +`HandleScroll` must be called when `MouseState.WheelPosition` changes. + +Note that the click events, just as in 2.X, are not implemented by the backend and instead implemented by the input context because it is not a requirement that backends can record clicks. **INFORMATIVE TEXT:** The original reason for this requirement in 2.X is because GLFW doesn't actually send click and double click events. + ## Cursor Configuration `ICursorConfiguration` is defined as: ```cs +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; } - CursorFlags SupportedFlags { get; } - CursorFlags Flags { get; set; } - RawImage? Image { get; set; } + CustomCursor Image { get; set; } } ``` @@ -425,7 +641,7 @@ Please note that the `Hotspot` properties present in 1.X and 2.0 have been remov `SupportedStyles` is a bitmask containing all of the cursor styles that are supported by this backend. This must be queried before setting `Style` - the currently active cursor style. An exception should be thrown if an attempt is made to set `Style` to an unsupported style or multiple styles (i.e. multiple bits set). -`Image` uses `RawImage` as-is from Silk.NET.Core, and when set to a non-null value implicitly sets `Style` to custom. As such, you must query `SupportedStyles` before using this property as well. Setting `Image` to `null` will set `Style` back to a standard cursor style, defined by the implementation. It is therefore recommended you set `Style` explicitly when disabling a custom cursor. Note that setting `Style` to a non-`Custom` value will also implicitly set this property to `null`. Setting `Mode` **to** `Custom` explicitly is undefined behaviour, as `Image` won't be set at the time of setting `Mode`. +`Image` when set to a non-`default` value implicitly sets `Style` to custom. As such, you must query `SupportedStyles` before using this property as well. Setting `Image` to `default` will set `Style` back to a standard cursor style, defined by the implementation. It is therefore recommended you set `Style` explicitly when disabling a custom cursor. Note that setting `Style` to a non-`Custom` value will also implicitly set this property to `default`. Setting `Mode` **to** `Custom` explicitly is undefined behaviour, as `Image` won't be set at the time of setting `Mode`. `SupportedFlags` is a bitmask containing other supported options for the cursor which can be mixed and matched if supported. This must be queried before setting `Flags` - the currently active options. An exception should be thrown if an attempt is made to set an unsupported flag on `Flags`. Unlike the other properties, `Flags` can have multiple bits set. @@ -437,10 +653,9 @@ Please note that the `Hotspot` properties present in 1.X and 2.0 have been remov [Flags] public enum CursorModes { - Normal, - Hidden = 1 << 0, - Disabled = 1 << 1, - Raw = 1 << 2 + Normal = 1 << 0, + Confined = 1 << 1, + Unbounded = 1 << 2, } ``` ```cs @@ -454,7 +669,8 @@ public enum CursorStyles Hand = 1 << 3, HResize = 1 << 4, VResize = 1 << 5, - Custom = 1 << 6, + Hidden = 1 << 6, + Custom = 1 << 7, } ``` ```cs @@ -471,10 +687,11 @@ public enum CursorFlags Once again, the interface is very simple. ```cs -public interface IKeyboard : IInputDevice +public interface IKeyboard : IButtonDevice { - ref readonly KeyboardState State { get; } + KeyboardState State { get; } string? ClipboardText { get; set; } + bool TryGetKeyName(KeyName key, [NotNullWhen(true)] out string? name); void BeginInput(); void EndInput(); } @@ -489,197 +706,344 @@ public interface IKeyboard : IInputDevice `KeyboardState` is defined as follows: ```cs -public readonly record struct KeyboardState -( - InputReadOnlyList? Text, - KeyState Keys -); -``` - -`Text` contains the characters typed on the keyboard since `IKeyboard.BeginInput`, and accounts for backspaces. This is cleared (set to `null`) when `IKeyboard.EndInput` is called, and will not be non-`null` again until another `IKeyboard.BeginInput` call. Given that `KeyChar` events are raised one character at a time, this property will update one character at a time to keep the state consistent with the event. - -**INFORMATIVE TEXT:** This is something we can optimize in `InputList` to not be allocatey, rest assured it is not acceptable to the Silk.NET team to allocate a new list for every character. - -```cs -public readonly record struct KeyState -( - InputReadOnlyList Down -) +public class KeyboardState { - public bool this[KeyName btn] { get; } - public bool this[int scancode] { get; } + public InputReadOnlyList? Text { get; } + public ButtonReadOnlyList Keys { get; } + public KeyModifiers Modifiers { get; } } ``` -The indexer returns `true` if a particular key is pressed, false otherwise. If the developer wishes to enumerate the key state, they must explicitly enumerate through the `Down` buttons. - -**INFORMATIVE TEXT:** This struct only exists so we can implement an indexer that accepts a `KeyName` or scancode, given that `Down` is effectively just a list and only takes an `int` index as a result. - -The indexer will be implemented in terms of `Down`, which is the only property that a backend will need to set. - -Note because not all keys are named, and because some developers may prefer to use scancodes instead, a `Key` struct is used instead of just having the list be a list of key names. - -```cs -public readonly record struct Key(KeyName Name, int Scancode); -``` +`Text` contains the characters typed on the keyboard since `IKeyboard.BeginInput`, and accounts for backspaces. This is cleared (set to `null`) when `IKeyboard.EndInput` is called, and will not be non-`null` again until another `IKeyboard.BeginInput` call. Given that `KeyChar` events are raised one character at a time, this property will update one character at a time to keep the state consistent with the event. -`KeyName` will be `Unknown` for scancode-only, unnamed keys. +**INFORMATIVE TEXT:** This is something we can optimize in `InputList` to not be allocatey, rest assured it is not acceptable to the Silk.NET team to allocate a new list for every character. -Changes to `KeyboardState` also have matching handler methods which are subject to the handler method rules i.e. the backend should call them in the order in which the backend received the events where possible etc (read the Input Handlers section). +Changes to `KeyboardState` also have matching handler methods which are subject to the handler method rules i.e. the backend should call them in the order in which the backend received the events where possible etc (read the Input Handlers section). For the avoidance of doubt, this implies that `Timestamp` in ascending order. ```cs -public interface IKeyboardInputHandler : IInputHandler +public interface IKeyboardInputHandler : IButtonInputHandler { - void HandleKeyDown(KeyDownEvent @event); - void HandleKeyUp(KeyUpEvent @event); + void HandleKeyChanged(KeyChangedEvent @event); void HandleKeyChar(KeyCharEvent @event); } ``` -`HandleKeyDown` must be called when a `Key` is added to the `KeyState.Down` list. - -`HandleKeyUp` must be called when a `Key` is removed from the `KeyState.Down` list. +`HandleKeyChanged` must be called in the same circumstances as `IButtonInputHandler.HandleButtonChanged`. The +purpose of this event duplication is to provide more keyboard-specific information if the handler has a use for it. `HandleKeyChar` must be called when a character is added to `KeyboardState.Text`. - ## Enums ```cs public enum KeyName { + // These values are from usage page 0x07 (USB keyboard page). Unknown = 0, - Space, - Apostrophe /* ' */, - Comma /* , */, - Minus /* - */, - Period /* . */, - Slash /* / */, - Number0, - Number1, - Number2, - Number3, - Number4, - Number5, - Number6, - Number7, - Number8, - Number9, - Semicolon /* ; */, - Equal /* = */, - A, - B, - C, - D, - E, - F, - G, - H, - I, - J, - K, - L, - M, - N, - O, - P, - Q, - R, - S, - T, - U, - V, - W, - X, - Y, - Z, - LeftBracket /* [ */, - BackSlash /* \ */, - RightBracket /* ] */, - GraveAccent /* ` */, - Escape, - Enter, - Tab, - Backspace, - Insert, - Delete, - Right, - Left, - Down, - Up, - PageUp, - PageDown, - Home, - End, - CapsLock, - ScrollLock, - NumLock, - PrintScreen, - Pause, - F1, - F2, - F3, - F4, - F5, - F6, - F7, - F8, - F9, - F10, - F11, - F12, - F13, - F14, - F15, - F16, - F17, - F18, - F19, - F20, - F21, - F22, - F23, - F24, - F25, - Keypad0, - Keypad1, - Keypad2, - Keypad3, - Keypad4, - Keypad5, - Keypad6, - Keypad7, - Keypad8, - Keypad9, - KeypadDecimal, - KeypadDivide, - KeypadMultiply, - KeypadSubtract, - KeypadAdd, - KeypadEnter, - KeypadEqual, - ShiftLeft, - ControlLeft, - AltLeft, - SuperLeft, - ShiftRight, - ControlRight, - AltRight, - SuperRight, - Menu -} -``` - -The `KeyName` enum is exactly the same as the `Key` enum in 2.X. The integral values of each enumerant, not included here, must match the en-US scancode for that key. A backend must match a scancode to a `KeyName` as if it were an en-US scancode, as this is the keyboard layout from which these key names were derived. - -The Silk.NET team wishes to reserve the right to remove any key names which do not have a matching en-US scancode. This is because the above enum is just copied and pasted from 2.X, and has not been cross-referenced with the keyboard layout at this time. + 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, +} +``` + +```cs +[Flags] +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 +} +``` + +The `KeyName` enumerates standard USB HID usage IDs where possible - mappings to PS/2 are not included and should be +done manually based on a translation table. A `KeyName` value **must** always map to a _physical_ scancode and not be +layout-specific. In theory, no key will be missing from this enum. However, the backend **may** cast an integer value +between 1-500 to this enum in the case that there is a standard USB HID page/usage ID that hasn't been accounted for by +`KeyName` yet. The Silk.NET team reserves the right to update `KeyName` to reflect latest specifications as it sees fit, +and to add non-standard keys as deemed applicable for the userbase. + +**INFORMATIVE TEXT**: There has been lots of discussion about our localization approach. We remain of the opinion that +any such approaches should be scancode-oriented, as these are effectively immutable and standardised across every +keyboard. The intention with the `KeyName` enum is to reflect a set of scancodes that would be recognizable to an +English developer, such that they could map to what is WASD using this enum, and have that automatically translated to +the keys in the same physical location on the end user's keyboard regardless of keyboard layout in use. This is also why +`IKeyboard` exposes `GetKeyName`, as this will localize this QWERTY-biased enum to whatever the equivalent key is in the +same physical position for the end user (e.g. for configuration UIs). We believe this presents a natural approach for +both the developer and the end user. + +**FUTURE IMPROVEMENT**: We obviously acknowledge that this does not account for non-English developers, for which we +could investigate adding more `KeyName` enums for different keyboard layouts in the future i.e. so those developers can +develop their application in terms of their native layout. + +**INFORMATIVE TEXT**: There has been some questions on whether we should expose options to generalise keyboard input +even further to common key bindings or common use cases. Such ideas have included providing a way to consider a +`IKeyboard`/`IPointer` combination as an `IGamepad` implicitly, or having a `KeyName`-like enum that has more generic +names that are more closely aligned with the user's use case e.g. instead of `W` we have `Forward`. Both ideas are +shelved for now, but we believe we will explore the former in future proposals (namely Axis-Based Input and/or Input Actions). # Gamepad Input ```cs -public interface IGamepad : IInputDevice +public interface IGamepad : IButtonDevice { - ref readonly GamepadState State { get; } + GamepadState State { get; } + ButtonReadOnlyList IButtonDevice.State => State.Buttons; IReadOnlyList VibrationMotors { get; } } ``` @@ -702,12 +1066,12 @@ This is exactly as in 2.X. `GamepadState` is defined as follows: ```cs -public readonly record struct GamepadState -( - JoystickButtonState Buttons, - DualReadOnlyList Thumbsticks, - DualReadOnlyList Triggers, -); +public class GamepadState +{ + public ButtonReadOnlyList Buttons { get; } + public DualReadOnlyList Thumbsticks { get; } + public DualReadOnlyList Triggers { get; } +} ``` `GamepadState` reuses a lot of the joystick API types, which are defined later in this proposal. @@ -727,22 +1091,16 @@ public readonly struct DualReadOnlyList : IReadOnlyList This is used where the list will only ever have exactly two elements, mainly because the "gamepad" form factor is standard and it doesn't make sense to have multiple thumbsticks or triggers given a human only has two thumbs or index fingers. More exotic devices should be exposed using the joystick API. -Changes to `GamepadState` also have matching handler methods which are subject to the handler method rules i.e. the backend should call them in the order in which the backend received the events where possible etc (read the Input Handlers section). +Changes to `GamepadState` also have matching handler methods which are subject to the handler method rules i.e. the backend should call them in the order in which the backend received the events where possible etc (read the Input Handlers section). For the avoidance of doubt, this implies that `Timestamp` in ascending order. ```cs -public interface IGamepadInputHandler : IInputHandler +public interface IGamepadInputHandler : IButtonInputHandler { - void HandleButtonDown(GamepadDownEvent @event); - void HandleButtonUp(GamepadUpEvent @event); void HandleThumbstickMove(GamepadThumbstickMoveEvent @event); void HandleTriggerMove(GamepadTriggerMoveEvent @event); } ``` -`HandleButtonDown` must be called when a button is added to `GamepadState.Buttons.Down`. - -`HandleButtonUp` must be called when a button is removed from `GamepadState.Buttons.Down`. - `HandleThumbstickMove` must be called when any value of `GamepadState.Thumbsticks` changes. `HandleTriggerMove` must be called when any value of `GamepadState.Triggers` changes. @@ -752,17 +1110,18 @@ public interface IGamepadInputHandler : IInputHandler This is the polyglot interface for any other human input device that roughly meets the description of being "joystick". ```cs -public interface IJoystick : IInputDevice +public interface IJoystick : IButtonDevice { - ref readonly JoystickState State { get; } + JoystickState State { get; } + ButtonReadOnlyList IButtonDevice.State => State.Buttons; } ``` ```cs -public readonly record struct JoystickState +public class JoystickState { - InputReadOnlyList Axes, - JoystickButtonState Buttons, - InputReadOnlyList Hats + public InputReadOnlyList Axes { get; } + public ButtonReadOnlyList Buttons { get; } + public InputReadOnlyList Hats { get; } } ``` @@ -770,23 +1129,6 @@ This is pretty closely modeled as in 2.X: `Axes` containing the individual axes **INFORMATIVE TEXT:** The only difference is `Hats` is now a `Vector2` instead of a `Position2D`. It is still intended that the X and Y values are only ever `0` or `1`, but this is not a requirement for more exotic backends. -`JoystickButtonState` is defined as follows: -```cs -public readonly record struct JoystickButtonState -( - InputReadOnlyList Down -) -{ - public bool this[JoystickButton btn] { get; } -} -``` - -The indexer returns `true` if a particular button is pressed, false otherwise. If the developer wishes to enumerate the button state, they must explicitly enumerate through the `Down` buttons. - -**INFORMATIVE TEXT:** This struct only exists so we can implement an indexer that accepts a `JoystickButton`, given that `Down` is effectively just a list and only takes an `int` index as a result. - -The indexer will be implemented in terms of `Down`, which is the only property that a backend will need to set. - `JoystickButton` is defined as follows: ```cs public enum JoystickButton @@ -814,22 +1156,16 @@ public enum JoystickButton } ``` -Changes to `JoystickState` also have matching handler methods which are subject to the handler method rules i.e. the backend should call them in the order in which the backend received the events where possible etc (read the Input Handlers section). +Changes to `JoystickState` also have matching handler methods which are subject to the handler method rules i.e. the backend should call them in the order in which the backend received the events where possible etc (read the Input Handlers section). For the avoidance of doubt, this implies that `Timestamp` in ascending order. ```cs -public interface IJoystickInputHandler : IInputHandler +public interface IJoystickInputHandler : IButtonInputHandler { - void HandleButtonDown(JoystickDownEvent @event); - void HandleButtonUp(JoystickUpEvent @event); void HandleAxisMove(JoystickAxisMoveEvent @event); void HandleHatMove(JoystickHatMoveEvent @event); } ``` -`HandleButtonDown` must be called when a button is added to `JoystickState.Buttons.Down`. - -`HandleButtonUp` must be called when a button is removed from `JoystickState.Buttons.Down`. - `HandleAxisMove` must be called when any value of `JoystickState.Axes` changes. `HandleHatMove` must be called when any value of `JoystickState.Hats` changes. diff --git a/documentation/proposals/Proposal - Windowing 3.0.md b/documentation/proposals/Proposal - Windowing 3.0.md index 8dac36e2a0..3f909975ba 100644 --- a/documentation/proposals/Proposal - Windowing 3.0.md +++ b/documentation/proposals/Proposal - Windowing 3.0.md @@ -8,995 +8,1726 @@ Cross-platform windowing for Silk.NET rebuilt from the ground-up. # Current Status - [x] Proposed -- [x] Discussed with Community -- [x] Approved +- [ ] Discussed with Community +- [ ] Approved - [ ] Implemented -# Design Decisions -- This proposal assumes no knowledge of any previous iterations of Silk.NET Windowing. -- This is a complete rethink of Silk.NET Windowing built from the ground up to account for our goal of "write once run everywhere" for .NET 6 and Silk.NET 3.0. -- In this proposal, there are three parties: - - **User**: the person using the Silk.NET Windowing library and by extension a platform implementation. - - **Platform Implementor**: the party providing an implementation of the Silk.NET Windowing abstractions (abstractions such as the "Surface" as defined later in this proposal). This implementation will be referred to herein as the **Platform Implementation** (or the **Platform** for short) - - **Library Implementor**: the party providing the plubming around the surface abstraction (such as the platform selection mechanisms and accompanying source generators). This will be referred to herein as the **Library Implementation** (or the **Library** for short) - +# Dependencies + +This proposal assumes knowledge of the Generic Math proposal and the previous version of this proposal. + +# Background + +Since its inception, Silk.NET has offered high-level windowing, input, and maths APIs to help address common pain points +when using .NET to implement a cross-platform game/engine. These are referred to collectively as the High Level +Utilities (HLU). The Windowing HLU seeks to reduce as much friction as possible in targeting the .NET platform for a +game by providing a high-level, C#-friendly API that has *getting out of the developer's way* as the top priority. That +is, we didn't want our developers to have to learn the intricacies of each target platform and write code to account for +that, and we also didn't want to add unnecessary complexity to their applications and development process by making them +learn a cross-platform abstraction library such as SDL, increasing the amount of unfamiliar, non-C#-friendly code in +their codebase beyond the surface the developer is actually interested in: their game/engine code. The HLUs seek to +reduce boilerplate that fits this description. + +This tenet of "getting out of their way" shall guide the majority of this proposal. This forms the "Usability" tenet as +defined in the Working Group approved 3.0 Software Development Plan, and shall prevail above all else for the purposes +of designing the HLUs. The other relevant tenets for the purposes of the HLU are naturally derived from this +"getting out of their way" concept. + +For history, in Silk.NET 1.0 we sought to create a C#-friendly Windowing abstraction. Admittedly, we didn't think this +through beyond trying to match the feature set of OpenTK - Silk.NET's predecessor and the project for which most of +Silk.NET's founding team were originally the leading contributors. OpenTK implemented this API by having a `GameWindow` +class that would be inherited from by the user, and the user instantiating that class and calling a `Run` function +(inherited from `GameWindow`, which would do the actual platform interactions) in their `Main` function. This +separated the window interactions from the window creation. For this, and a variety of other reasons that sadly have +been lost to time (primarily because they were discussed at length in voice chat, not text chat), we were not fully +satisfied with our work at OpenTK and given that Silk.NET was a brand new project, it gave us an opportunity to +re-evaluate the needs for such an API and the design decisions therefor. + +Ultimately, we knew that creating a window to draw in is something that the vast majority Silk.NET users needed to do. +Users could use libraries such as GLFW and SDL to achieve this, but we acknowledged that these require a +not-insignificant amount of code and/or API-specific knowledge. As such, we sought to write a GLFW wrapper (as we felt +this was the best stable base available at the time for our needs, and there was no appetite for maintaining +platform-specific code ourselves and the bugs thereof), and we wanted to make this as "obvious" a design as possible. +To this end, the user would create a window (as a resource that they'd own and could fit into whatever designs they +pleased, as opposed to a component/class that they'd define and use through inheritance) and have a working render loop +in as little as three lines. This wrapper did not expose any of the underlying GLFW details through its API surface as +we wanted to have maximal portability should we choose to change the underlying library in use. + +This 1.0 implementation was very successful, and later in the 1.0 preview cycle we started considering how to implement +the windowing API on more platforms such as mobile and UWP. This resulted in the `IView` API being created, as `IWindow` +contained a lot of APIs that only made sense where you had the ability to create many windows within the context of a +window manager, as opposed to the mobile/UWP use case where a single window is bestowed upon the application _by_ the +window manager i.e. the application has no control over that window's attributes e.g. sizing, positioning, window modes, +etc. + +Silk.NET 1.0 shipped with this `IView`/`IWindow` split, followed by Silk.NET 2.0 which implemented only the former on +mobile platforms, as opposed to desktop platforms which implemented both (`IView` is a subset of `IWindow`). This left a +lot to be desired given that a lot of code (including our own examples) had already been written against `IWindow` +instead of `IView`, and writing for the latter is something that the developer would have to go out of their way to do. +This is contrary to 3.0's goal of promoting write-once-run-everywhere. + +# Overview + +In 3.0 we'd like to have an architecture that promotes write-once-run-everywhere, by encouraging our users to deal with +some feature sets not being available. With `IWindow` today, it's far too easy to assume that you're able to change the +window size for instance, and it is not immediately obvious that the user should actually create an `IView` and only use +those APIs if the user has checked `view is IWindow`. Instead, this proposal breaks the windowing API down into the +concept of having an abstract "surface" representing a draw area that has been allotted to the application by the +operating system/window manager, and splits the extant API surface up into distinct "components" that the user can +access on this "surface". For example, instead of `IWindow.Size = new(1280, 720)`, the user would use +`Surface.Window.ClientSize = new(1280, 720)` where `Bounds` is an optional component on that surface. Because that +component is optional, nullable reference types shall be used, which will encourage the user to deal with the scenario +in which that component is not present as a result of the IDE warnings that would be generated by making an assumption +of its presence. This provides the write-once-run-everywhere encouragement we're after. + +The Working Group may recall that they have already approved a version of this proposal that met some of these goals, +originally designed by Dylan P and Kai J. Both of us were not fully satisfied with this proposal as this still used the +type system to expose differing feature sets rather than having a component breakdown, and we were not convinced that +this will have the encouragement of write-once-run-everywhere we wanted due to the relative uncleanliness of using the +type system in this way, with non-obvious separations of functionality within the types exposed by that proposal. +Nonetheless, this was proposed to the Working Group and approved, lacking a better alternative at the time. +We believe that the component breakdown in this proposal presents that better alternative we are looking for however, +hence the new version of the proposal. + +## A Note on the Component-Based Design Regarding Implementations + +During the development of this proposal, we were keen to try and find a way to not only present the high-level API in +this component-based design, but also a lower-level API upon which the high-level API would be implemented. Development +of this lower-level API was taking time however, and a concrete shape for this API couldn't be determined. Given that +this is mostly an implementation detail/nicety that would make future expansion of the API easier, we chose not to make +this a blocker for the 3.0 release and will instead propose this at a later date in a separate proposal - we do not +believe that anything in this proposal restricts the retro-fitting of such a design. # Platforms -There will be a number of "reference" implementations for the APIs laid out in this proposal. These are: -- For `net6.0` and `net6.0-windows`: GLFW. -- For `net6.0-ios`, `net6.0-tvos` and `net6.0-maccatalyst`: UIKit. (NB: The latter two will be a target for Silk.NET 3.X) -- For `net6.0-macos`: AppKit (NB: This will be a target for Silk.NET 3.X, it is recommended developers targeting macOS use the regular `net6.0` TFM) -- For `net6.0-android`: EGL and ANativeWindow. -- For `net6.0-tizen`: TZSH. (NB: This will be a target for Silk.NET 3.X) - -The decision has been made to drop SDL due to complications and the fact that all non-desktop targets are distinctly unique in their own right. We believe that in creating platform-specific code for each of these will result in significantly more robust support for each of these platforms. - -- **QUESTION:** Should we drop our SDL bindings too? -- **MAINTAINERS' ANSWER:** Yes, it's no longer used by us and a library such as SDL is a large amount of maintenance weight to carry. Akin to us dropping EGL in the 1.X-2.0 transition, we will be dropping our SDL answer. - -All of the above is informative text, however, and no reference implementations are required or guaranteed to use these APIs under-the-hood. +There shall only be one `Silk.NET.Windowing` project for the Windowing HLU that defines the abstractions and exactly one +"reference implementation". This is a departure from previous versions where the abstractions were separate from the +implementations. If the reference implementation is not needed, it is expected that the linker shall remove it from the +end assembly. Generally, Silk.NET favours embracing readily available tooling such as the linker over architectural +decisions in lieu of tooling, and given that .NET 8 makes using the linker extremely easy, this has dictated a fair few +decisions in the library's development. -Unlike previous iterations, one reference implementation **MUST** be bundled with the main Silk.NET.Windowing assembly. Given that we can easily use platform-specific TFMs there's no reason to keep the fragmentation of different implentations across different assemblies. +The "reference implementation" shall use SDL3, but the Silk.NET team reserves the right to change this for some or all +of the target platforms. It is expected that platform differentiation is done by TFM (e.g. `net8.0`, `net8.0-windows`, +`net8.0-ios`, etc) but this could include runtime identifier based differentiation in the future. The Silk.NET team +reserves all rights to determine implementation details such as this. -The reference implementation will be accessed through the static `Surface` class. -- `IsPlatformSupported` **MAY** return false if there is no reference implementation available for the given environment. -- `GetOrCreate` **MUST NOT** return `null`. It **MUST** always return a valid `ISurface` if `IsPlatformSupported` is true and no exceptions or errors ocurred during surface creation. This method **MUST** always return the same `ISurface` instance. -- `CreateNew` **MUST NOT** return the same `ISurface` object, and **MUST NOT** return `null`. It **MUST** always return a valid `ISurface` if `IsPlatformSupported` is true and no exceptions or errors ocurred during surface creation. -- `ClearCurrentContexts` **MUST** call `ClearCurrent` on all `IGLSurface` or `IGlesSurface` instances this reference implementation has created. +Silk.NET 3.0 aims to support Windows, Linux (X/11 and Wayland), macOS, iOS, and Android. The goal of this proposal is to +do so in a way that requires no _modification_ of user code. It is highly likely that _additional_ boilerplate code will +be needed on some platforms (e.g. `MainActivity`) but this should not be variant based on the user's specific +application i.e. it should work as is when copied and pasted into the user's application. -For the reader's benefit, this `Surface` class isn't really intended for general consumption though it must be public for those applications don't fit into the model defined in the "Windowing Entry Points" section. +To promote write-once-run-everywhere, in order to use Silk.NET.Windowing the user must first implement +`ISurfaceApplication` and call `ISurfaceApplication.Run`. These are defined as follows: -# Surface - -In this proposal, a plane upon which graphics can be rendered on is represented by an `ISurface`. `ISurface` defines a minimal subset of basic APIs which **MUST** all be present on an **Platform Implementation**. The idea is `ISurface` just provides the bare necessities for rendering a game or application without knowing too much about the form factor, better encouraging cross-platform/"write once run everywhere" code. For a description of what this entials, see the defined API and the documentation comments therein. +```cs +namespace Silk.NET.Windowing; -`ISurface` **Platform Implementations** **SHOULD** also implement any extension interfaces that it can support for a given platform. Through these extension interfaces, if user code needs to access APIs which are more specific to certain form factors or platforms, they should use casts to get a more specific surface. +/// +/// Represents an application running within a surface. +/// +public interface ISurfaceApplication +{ + /// + /// An optional window class. + /// + static virtual string? WindowClass => null; -The "core `ISurface` API" is as minimalistic as possible to allow easy use in integrations into UI frameworks or other environments. For example, at some point in the future we'd like to make `ISurface` implementations atop WPF, MAUI, Avalonia, and more; as well as atop Blazor WASM (and by extension HTML5 and WebGL) + /// + /// Called upon initialization of the application. + /// + static abstract void Initialize(TSurface surface) where TSurface : Surface; -# Windowing Entry Points + /// + /// Runs an application using the reference implementation of Silk.NET.Windowing. + /// + [UnsupportedOSPlatform("android")] + public static sealed void Run() where T : ISurfaceApplication; +} -Even with the platform selection mechanism, there is a lot of plumbing required to get to the stage of acquiring a surface on the various platforms. In Silk.NET 3.0, the **Library** **MAY** decide to expose a Roslyn source generator (the "**Gluer**" as used herein) to assist with this. If it doesn't, this section and its requirements can be discarded. +#if __ANDROID__ +/// +/// Represents an Android activity that automatically runs the given application. +/// +public abstract class SilkActivity /* : undefined */ where TApplication : ISurfaceApplication; +#endif +``` -The **Gluer** **MUST** be distributed in package/namespace `Silk.NET.Windowing.Roslyn`. +Expected to be used as follows (e.g. in `Program.cs`): +```cs +public class Program : ISurfaceApplication +{ + public static void Initialize(TSurface surface) where TSurface : Surface + { + // Code goes here. The code must return for the render loop to begin. + } -The Gluer operates on **User** methods that: -- **MUST** be static -- **MUST** return `void` -- **MUST** have a single parameter of type `ISurface` or a type that inherits from `ISurface`. If the latter, the Glue **MUST** assert that the `ISurface` created is assignable to the parameter type or throw an exception if this is not the case. This allows applications to use `IDesktopSurface` only if that's their jam. -- **MUST** have the attribute `SilkEntryPoint` + public static void Main() => ISurfaceApplication.Run(); +} +``` -The idea is the Gluer will generate `Main` methods on .NET, `Activity`s on .NET for Android, etc... The **Gluer**, if generating **MUST** generate the necessary APIs to ensure that the method in question is called on application start-up (the "Glue"). If any of the above User requirements aren't met, the **Gluer** **MUST NOT** generate Glue. If a method is found with the `SilkEntryPoint` attribute but fails to meet one of the other requirements, the **Gluer** **MUST** generate a compiler error; otherwise it **MUST NOT** impact the compilation whatsoever. +In addition to the following `Platforms/Android/MainActivity.cs` file on Android: +```cs +[Activity(Label = "@string/app_name", MainLauncher = true)] +public class MainActivity : SilkActivity; +``` -If multiple methods are found with `SilkEntryPoint`, the **Gluer** **MUST** generate a compiler error. All attributes **MUST** only be name matched by the **Gluer**. +The application receives an `Surface`. This will be defined partially throughout this file. -The Gluer **MUST NOT** modify the original type (i.e. all Glue must be defined in its own self-contained type) +**NOTE**: The concept of the "gluer" - a source generator that generates the platform-specific classes - has been +omitted from the revised proposal due to there often being more platform-specific requirements in addition to just the +code, and given that the code-level platform-specific changes are as minimal as they are, adding one platform-specific +C# file has been deemed to be a benignant requirement. -The **User** **MUST NOT** define their own application entry point (such as a static `Main` method in the case of regular .NET) if they are using the Gluer, though the Gluer does not have to enforce this. +# The Update Loop -The method **MAY** be called multiple times throughout the lifetime of an application. This is because of operating system restrictions - the only way to definitively know whether this method is being called for the last time is the `IsTerminating` property or `Terminating` event on `ISurface`. +Like 2.X, Silk.NET 3.0 takes control of the application's update loop by running user code as fast as possible. Unlike +2.X, Silk.NET 3.0 will not expose a blocking call to do this (other than the top-level `Run` call of course on the +platforms where that is used). However, we still want to empower users to implement their own loop _timing_ (i.e. +instead of using `Render` or `Update` events) and for this we require a way to run user code as fast as possible. +In 2.X this was the `onFrame` callback to `IWindow.Run(Action)`. In 3.0 this shall be the `Tick` event. -The Glue is purposely left undefined as this is operating system & platform specific, and would not reflect any future platforms we decide to add. +**NOTE**: This proposal originally added a `ISurfaceActor` that implemented the actor pattern as used in the +Multi-Backend Input proposal, but given that there's no benefits today for windowing specifically in doing this it was +removed. The event structs first introduced in Enhanced Input Events and retained in Multi-Backend Input has been +carried forward into this proposal to ensure that we take advantage of the same breaking change resistance benefits. -A model example of what this looks like: ```cs -public class MyGame +namespace Silk.NET.Windowing; + +/// +/// Contains parameters for events executed at predictable intervals by a . +/// +/// The surface to which the event pertains. +/// The number of seconds that have elapsed since the last execution of this event. +public readonly record struct SurfaceTimingEvent(Surface Surface, double DeltaTime); + +/// +/// Contains parameters for events executed in response to an application lifecycle notification being issued to a +/// surface. +/// +public readonly record struct SurfaceLifecycleEvent(Surface Surface); + +/// +/// Contains properties pertaining to events a surface shall raise at predictable intervals. +/// +/// +/// The target number of seconds for the surface between the start of the previous execution of the event, and the start +/// of the next execution. +/// +/// +/// Note that timed events are executed in terms of the surface's tick frequency. If +/// is set to run the event at a lower frequency than is targeted by this event, it is +/// impossible to meet the . +/// +public readonly record struct SurfaceTimingOptions(double TargetDelta); + +/// +/// Contains properties pertaining to the regular execution of the event. +/// +/// +/// Whether should only execute in response to an event (of any kind) and/or +/// if true; if false the event shall be raised as frequently as the +/// implementation/platform can handle. +/// +public readonly record struct SurfaceTickOptions(bool IsEventDriven); + +/// +/// Contains properties pertaining to a resize event. +/// +/// The surface raising the resize event. +/// The previous size value. +/// The new size value. +public readonly record struct SurfaceResizeEvent(Surface Surface, Vector2 OldSize, Vector2 NewSize); + +/// +/// Represents a surface within which a user application can run. This class contains a modular, cross-platform +/// interface with which the platform's multimedia capabilities can be configured and the execution of the application +/// within the surface controlled. +/// +public abstract partial class Surface { - [SilkEntryPoint] - public static void RunGame(ISurface surface) - { - // do things with your surface - surface.Run(); - } -} -``` + /// + /// An event raised as frequently as possible (or in line with ). + /// + public event Action Tick { add; remove; } -# Optional Features -## Desktop Surface + /// + /// An event raised in accordance with with the intention of providing the application + /// an opportunity to redraw its graphics within the surface. + /// + public event Action Render { add; remove; } -A more rich set of APIs intended for use on desktop-style window management systems. For a description of what this entials, see the defined API and the documentation comments therein. + /// + /// An event raised in accordance with with the intention of providing the application + /// an opportunity to redraw its graphics within the surface. + /// + public event Action Update { add; remove; } -## OpenGL Surface + /// + /// An event executed when the surface is first loaded. + /// + public abstract event Action Created { add; remove; } -Enables OpenGL context creation atop a native surface. For a description of what this entials, see the defined API and the documentation comments therein. + /// + /// An event executed when the surface and the application thereof are about to terminate irrevocably. + /// + public abstract event Action Terminating { add; remove; } -## OpenGLES Surface + /// + /// An event executed when the surface and the application thereof are pausing on request of the operating system. + /// + public abstract event Action Pausing { add; remove; } -Enables OpenGLES context creation atop a native surface. For a description of what this entials, see the defined API and the documentation comments therein. + /// + /// An event executed when the surface and the application thereof are resuming on request of the operating system. + /// + public abstract event Action Resuming { add; remove; } -## Vulkan Surface + /// + /// An event executed when the operating system indicates the amount of memory that can be allocated for the + /// application running the surface is low. + /// + public abstract event Action LowMemory { add; remove; } -Enables Vulkan surface creation atop a native surface. For a description of what this entials, see the defined API and the documentation comments therein. + /// + /// Gets or sets additional configuration/constraints for the event. + /// + public abstract SurfaceTickOptions TickOptions { get; set; } -## OpenGL Surface with Framebuffer Transparency + /// + /// Gets or sets additional configuration/constraints for the event. + /// + public virtual SurfaceTimingOptions RenderOptions { get; set; } -Framebuffer transparency is an optional feature, and therefore has its own interface. This allows the a window to be created where the content area is transparent. For a description of what this entials, see the defined API and the documentation comments therein. + /// + /// Gets or sets additional configuration/constraints for the event. + /// + public virtual SurfaceTimingOptions UpdateOptions { get; set; } -NB: We've been discussing `IWebGLSurface` a lot recently, but this is left out of this proposal as this is a target for Silk.NET 3.X. + /// + /// Gets or sets a value representing . + /// as a number of executions per second. + /// + public int FramesPerSecond { get; set; } -# Proposed API -- Here you do some code blocks, this is the heart and soul of the proposal. DON'T DO ANY IMPLEMENTATIONS! Just declarations. + /// + /// Gets or sets a value representing . + /// as a number of executions per second. + /// + public int UpdatesPerSecond { get; set; } -## Delegates + /// + /// Provides a strong hint to the underlying platform that a tick should execute. This is mainly useful for + /// . where execution of this method + /// constitutes an event. + /// + public abstract void Continue(); -NB: instead of generic delegates like we've used in previous iterations, we use named delegates instead so the parameter names are auto-filled out by IDEs with indicative names, instead of `obj` or `argN`. + /// + /// Irrevocably terminates this surface and the application running within it, resulting in the immediate ceasing of + /// event execution and all derived events thereof. The surface cannot recover from this state, + /// requiring the relaunch of the if available on the platform. + /// + public abstract void Terminate(); -```cs -public delegate void Vector2DAction(Vector2D newValue); -public delegate void DeltaAction(double deltaTime); -public delegate void WindowStateAction(WindowState newState); -public delegate void FilePathsAction(string[] filePaths); -public delegate void ToggleAction(bool newValue); -``` + /// + /// Executes the event. This will also call and . + /// + protected internal virtual void OnTick(); -## `ISurface` -```cs -namespace Silk.NET.Windowing -{ - public interface ISurface : IWindowHandlesSource, IDisposable - { - /// - /// Determines whether the surface is being destroyed by the platform. - /// - bool IsTerminating { get; } - - /// - /// Determines whether the surface is being paused by the platform. - /// - bool IsPausing { get; } + /// + /// Executes the event if the constraints defined in are met. + /// + protected internal virtual void OnRender(); - /// - /// Elapsed time in seconds since the Run method last started. - /// - double Time { get; } + /// + /// Executes the event if the constraints defined in are met. + /// + protected internal virtual void OnUpdate(); - /// - /// The size of the surface's inner framebuffer. May differ from the surface size. - /// - // NB: This is not OpenGL specific and is valid in any case where there's a high DPI monitor. - Vector2D FramebufferSize { get; } + /// + /// Gets the size in pixels of the area drawable within the surface. + /// + public abstract Vector2 DrawableSize { get; } - /// - /// The size of the surface. - /// - Vector2D Size { get; } + /// + /// Gets a value indicating whether the surface is terminating irrevocably. + /// + /// + public abstract bool IsTerminating { get; } - /// - /// The number of rendering operations to run every second. - /// - double FramesPerSecond { get; set; } + /// + /// Raised when changes. + /// + public abstract event Action DrawableSizeChanged { add; remove; } - /// - /// The number of update operations to run every second. - /// - double UpdatesPerSecond { get; set; } + /// + /// Centers this window to the given monitor or, if null, the current monitor the window's on. + /// + /// The window to center. + /// The specific display to center the window to, if any. + public void Center(IDisplay? display = null); - /// - /// Raised when the surface is resized. - /// - event Vector2DAction? Resize; + /// + /// Converts a point that is defined in the same coordinate space as to instead be + /// defined relative to . Requires both and + /// components to be supported, if not shall be returned as is. + /// + /// The point to transform. + /// The transformed point. + Vector2 ScreenToClient(Vector2 point); - /// - /// Raised when the surface's framebuffer is resized. - /// - event Vector2DAction? FramebufferResize; + /// + /// Converts a point that is defined relative to to instead be defined in the + /// same coordinate space as . Requires both and + /// components to be supported, if not shall be returned as is. + /// + /// The point to transform. + /// The transformed point. + Vector2 ClientToScreen(Vector2 point); - /// - /// Raised when the surface is being terminated. - /// - event Action? Terminating; + /// + /// Converts a point that is defined relative to by multiplying it with the + /// division of by 's size. + /// + /// The point to transform. + /// The transformed point. + Vector2 ClientToDrawable(Vector2 point); +} +``` - /// - /// Raised when the surface is running low on memory. - /// - event Action? LowMemory; +The implementation shall call `Surface.OnTick` as fast as possible. On platforms where the application must be started +manually (any platform other than Android), it is expected that this continuous calling of `OnTick` will be called +continuously in `ISurfaceApplication.Run` for the reference implementation, or an equivalent API in an alternative +implementation. This could be through a `while` loop or through a platform-defined mechanism (the latter most notably +being the case for iOS and Android). This shall be the case except where `TickOptions.IsEventDriven` is `true`, in which +case `OnTick` shall only be called in response to an event or in response to `Continue` being called on any thread if +the platform supports this. + +All events defined throughout this document shall be raised by the implementation if they are declared with the +`abstract` keyword or are part of an `interface`. Any events not declared with `abstract` shall be raised by the common +`Surface` code. + +It is expected that the timing logic be common code i.e. all of these APIs shall be implemented in `Surface` and not a +derived type. `FramesPerSecond` and `UpdatesPerSecond` shall to forward to `RenderOptions.TargetDelta` and +`UpdateOptions.TargetDelta` respectively with the value of `1 / value`. `SurfaceTimingOptions` has been introduced for +future proofing in the event that we want to add more options such as executing one of the events on a different thread +(e.g. as in Silk.NET 1.X when the `UseSingleThreadedWindow` option was set to `false`). These settings shall default to +run as fast as possible. A `TargetDelta` value close to zero or a negative number is interpreted to mean run as fast as +possible. This shall be represented by `FramesPerSecond`/`UpdatesPerSecond` as a negative number. A `TargetDelta` value +close to `double.MaxValue` shall be interpreted to mean "never execute". This shall be represented by +`FramesPerSecond`/`UpdatesPerSecond` as zero. The `OnRender` and `OnUpdate` methods shall raise the `Render` and +`Update` events respectively, which the implementation has the opportunity to override. + +# Optional vs Mandatory APIs + +Mandatory APIs are denoted by the `abstract` keyword. Optional APIs are denoted by a `virtual` keyword. Generally, where +APIs are optional, they also have another mechanism to define runtime optionality e.g. gating the APIs through a +nullable property. In addition to the `abstract` members on `Surface` already defined above, the following mandatory +APIs are specified: - /// - /// Raised when the surface is about to pause. This is a good indicator that the Run method is about to exit, though this may not necessarily be the case, but the surface isn't terminating yet. - /// - event Action? Pausing; +```cs +namespace Silk.NET.Core; - /// - /// Raised when the surface is about to resume. This is a good indicator to expect the entry point to be called again, though this may not necessarily be the case. - /// - event Action? Resuming; +/// +/// Represents a window that possesses native handles or other platform-specific information. +/// +public interface INativeWindow +{ + /// + /// Attempts to obtain native platform information with type . + /// + /// + /// The platform-specific information, or default if the platform-specific information is not available for + /// this platform. + /// + /// True if contains the platform-specific information, false otherwise. + bool TryGetPlatformInfo([NotNullWhen(true)] out TPlatformInfo? info); +} - /// - /// Raised when the surface is initialized for the first time. - /// - event Action? Created; - - /// - /// Raised just before the Update event is raised. - /// - event Action? PreUpdate; +namespace Silk.NET.Windowing; - /// - /// Raised when an update should be run. - /// - event DeltaAction? Update; +public abstract partial class Surface : INativeWindow; +``` - /// - /// Raised when a frame should be rendered. - /// - event DeltaAction? Render; +The Silk.NET team reserve the right to define the acceptable types for `TPlatformInfo`. - /// - /// Creates the surface on the underlying platform. - /// - void Initialize(); +The only mandatory APIs are those inherited from `INativeWindow` and the `abstract` APIs already defined above. There +are no other useful APIs that are guaranteed to be available on all platforms. - /// - /// Calls the Render event. - /// - void DoRender(); +The rest of the APIs herein are optional, the conditions of their support denoted with their definition. These will be +categorised into logical subsets, or a "component" interface. These shall be accessed via nullable properties. - /// - /// Calls the Update event. - /// - void DoUpdate(); +Herein for simplicity the expected functionality will be listed in the documentation comments except where there are +complex interactions. - /// - /// Polls the underlying platform for events. - /// - void DoEvents(); +# The OpenGL Component - /// - /// Unloads the surface on the underlying platform. - /// - void Reset(); +Oftentimes the mechanism by which an OpenGL surface is created is platform-specific in nature, and our implementation +libraries often have abstractions as a result. To make this easier for the user, we shall introduce a similar +abstraction below as with all previous Silk.NET versions. - /// - /// Terminates this surface. - /// - void Terminate(); +```cs +namespace Silk.NET.Core; - /// - /// Converts this point to framebuffer coordinates. - /// - /// The point to transform. - /// The transformed point. - /// Expects client coordinates as input. - Vector2D PointToFramebuffer(Vector2D point); +/// +/// Represents an OpenGL context. +/// +public interface IGLContext : INativeContext +{ + /// + /// Whether the context is current on this thread. + /// + bool IsCurrent { get; set; } - /// - /// Initiates a render loop in which the given callback is called as fast as the underlying platform can manage. - /// - /// The callback to run each frame. - void Run(Action onFrame); - } + /// + /// The number of vertical blanks to wait for before sending another frame. + /// + int SwapInterval { get; set; } + + /// + /// Gets or sets a value indicating whether is non-zero. + /// + bool VSync { get => SwapInterval > 0; set => SwapInterval = value ? 1 : 0; } + + /// + /// Swaps the backbuffer to present the contents to the window. + /// + void SwapBuffers(); } -``` -## `IDesktopSurface` +/// +/// Represents a source of a +/// +// Same as in 1.X/2.X, just a different namespace. +public interface IGLContextSource +{ + /// + /// The OpenGL context. + /// + IGLContext? GLContext { get; } +} -```cs -namespace Silk.NET.Windowing +/// +/// A 32-bit version structure. +/// +public readonly struct Version32 { /// - /// A surface which wraps a Desktop Window. + /// The underlying Vulkan-compatible 32-bit version integer. /// - public interface IDesktopSurface : ISurface - { - /// - /// Whether or not the window is visible. - /// - bool IsVisible { get; set; } + public uint Value { get; } + + /// + /// Creates a Vulkan version structure from the given major, minor, and patch values. + /// + /// The major value. + /// The minor value. + /// The patch value. + public Version32(uint major, uint minor, uint patch); + + /// + /// Creates a Vulkan version structure from the given Vulkan-compatible value. + /// + /// The value. + private Version32(uint value); + + /// + /// Gets the major component of this version structure. + /// + public uint Major { get; } + + /// + /// Gets the minor component of this version structure. + /// + public uint Minor { get; } + + /// + /// Gets the patch component of this version structure. + /// + public uint Patch { get; } + + /// + /// Creates a 32-bit version structure from the given 32-bit unsigned integer. + /// + /// The uint value. + /// The 32-bit version structure. + public static explicit operator Version32(uint val); - /// - /// The position of the window. If set to -1, use the backend default. - /// - Vector2D Position { get; set; } + /// + /// Creates a 32-bit version structure from the given managed version class. + /// + /// The version instance. + /// The 32-bit version structure. + public static implicit operator Version32(Version version); - /// - /// The size of the window in pixels. - /// - new Vector2D Size { get; set; } + /// + /// Gets the 32-bit unsigned integer representation for this 32-bit version structure. + /// + /// The 32-bit version structure. + /// The 32-bit unsigned integer. + public static implicit operator uint(Version32 version); - /// - /// The window title. - /// - string Title { get; set; } + /// + /// Converts this 32-bit version structure to a managed version class. + /// + /// The 32-bit version structure. + /// The managed representation. + public static implicit operator Version(Version32 version); +} - /// - /// The window state. - /// - WindowState WindowState { get; set; } +namespace Silk.NET.OpenGL; - /// - /// The window border. - /// - WindowBorder WindowBorder { get; set; } +/// +/// Contains extensions for creating instances from s and related types. +/// +public static class GLContextExtensions +{ + /// + /// Creates a using function addresses sourced from the given . + /// + /// The context to use for function pointer loading. + /// A instance. + public static GL CreateOpenGL(this IGLContext ctx); - /// - /// The video mode. - /// - VideoMode VideoMode { get; set; } - - /// - /// Gets the screen on which this window is active. - /// - IScreen? CurrentScreen { get; set; } + /// + /// Creates a using function addresses sourced from the given . + /// + /// The source of the context to use for function pointer loading. + /// A instance. + public static GL CreateOpenGL(this IGLContextSource ctx); +} - /// - /// Gets the available screens for this surface. - /// - IEnumerable? AvailableScreens { get; } +namespace Silk.NET.Windowing; - /// - /// Gets or sets whether the window waits for an event to be posted before existing . - /// - bool IsEventDriven { get; set; } +/// +/// Represents flags related to the OpenGL context. +/// +[Flags] +public enum OpenGLContextFlags +{ + /// + /// No flags enabled. + /// + Default = 0, - /// - /// Gets or sets whether the window has been requested to close. - /// - bool IsCloseRequested { get; set; } + /// + /// Enables debug context; debug contexts provide more debugging info, but can run slower. + /// + Debug = 1 << 0, - /// - /// Gets whether the window is focused or not. - /// - bool IsFocused { get; } + /// + /// Enables forward compatibility; this context won't support anything marked as deprecated in the current + /// version. + /// + /// On OpenGL contexts older than 3.0, this flag does nothing. + ForwardCompatible = 1 << 1 +} - /// - /// Gets the distances in screen coordinates from the edges of the content area to the corresponding edges of - /// the full window. - /// - /// - /// Because these are distances and not coordinates, they are always zero or positive. - /// - /// - Rectangle BorderSize { get; } - - /// - /// Raised when the window has been requested to close. - /// - event Action CloseRequested; +/// +/// Represents the context profile OpenGL should use. +/// +public enum OpenGLContextProfile +{ + /// + /// An OpenGL context will not be created for this surface. + /// + None = 0, - /// - /// Raised when the window is moved. - /// - event Vector2DAction? Move; + /// + /// Use the platform default context profile e.g. on mobile platforms, + /// otherwise. + /// + Default, - /// - /// Raised when the window state is changed. - /// - event WindowStateAction? StateChanged; + /// + /// Uses a core OpenGL context, which removes some deprecated functionality. + /// + Core, - /// - /// Raised when the user drops files onto the window. - /// - event FilePathsAction? FileDrop; + /// + /// Uses a compatibility OpenGL context, allowing for some deprecated functionality. This should only ever be + /// used for maintaining legacy code; no newly-written software should use this. + /// + Compatibility, - /// - /// Raised when the window focus changes. - /// - event ToggleAction? FocusChanged; + /// + /// Uses an OpenGLES 2+ profile. + /// + ES2 +} - /// - /// Sets the window icons. - /// - /// Either a collection of window icons, or null to set to the default icon. - void SetWindowIcon(ReadOnlySpan icons); +/// +/// The OpenGL component of a . The methods can only be executed once +/// has executed. +/// +/// +/// These objects may be shared with child windows created using and vice versa i.e. +/// this object can be shared between all surfaces that share a common ancestor (the "root surface"). Beyond that, these +/// objects are not guaranteed to be valid across surfaces. This allows one event handler to enact changes on multiple +/// surfaces. This is important for purposes. +/// +public interface ISurfaceOpenGL : IGLContext +{ + /// + /// Gets or sets a value indicating whether OpenGL support is enabled for this surface. Setting + /// to a value other than will automatically set + /// this property to true, and likewise toggling the value assigned to this property will change the value of + /// . + /// + /// + /// This can only be set during the method. + /// + // Included for consistency with Vulkan. + bool IsEnabled + { + get => Profile != OpenGLContextProfile.None; + set => value + ? Profile == OpenGLContextProfile.None + ? OpenGLContextProfile.Default + : Profile + : OpenGLContextProfile.None; + } - /// - /// When using = true, wakes the main thread from - /// its blocking wait on incoming events. Can be called from any thread. - /// - void ContinueEvents(); + /// + /// Preferred depth buffer bits of the window's framebuffer. + /// + /// + /// Pass null or -1 to use the system default. + /// This can only be set during the method. + /// Setting this property will automatically set to + /// if it is currently . + /// + int? PreferredDepthBufferBits { get; set; } - /// - /// Converts this point to client coordinates. - /// - /// The point to transform. - /// The transformed point. - /// Expects screen coordinates as input. - Vector2D PointToClient(Vector2D point); + /// + /// Preferred stencil buffer bits of the window's framebuffer. + /// + /// + /// Pass null or -1 to use the system default. + /// + int? PreferredStencilBufferBits { get; set; } - /// - /// Converts this point to screen coordinates. - /// - /// The point to transform. - /// The transformed point. - /// Expects client coordinates as input. - Vector2D PointToScreen(Vector2D point); - } -} -``` + /// + /// Preferred red, green, blue, and alpha bits of the window's framebuffer. + /// + /// + /// Pass null or -1 for any of the channels to use the system default. + /// + Vector4D? PreferredBitDepth { get; set; } -## `INativeGLSurface` + /// + /// Preferred number of samples for multi-sample anti-aliasing. + /// + /// + /// This can only be set during the method. + /// + int? PreferredSampleCount { get; set; } -```cs -namespace Silk.NET.Windowing -{ - public interface INativeGLSurface : ISurface - { - nint Handle { get; } - bool IsContextCurrent { get; set; } - bool ShouldSwapAutomatically { get; set; } - - /// - /// Sets the number of vertical blanks to wait between calling and presenting the image, - /// a.k.a vertical synchronization (V-Sync). Set to 1 to enable V-Sync. - /// - /// - /// Due to platform restrictions, this value can only be set and not retrieved. - /// - int SwapInterval { set; } + /// + /// The API version to use. + /// + /// + /// This can only be set during the method. + /// + Version32? Version { get; set; } - /// - /// Preferred depth buffer bits of the window's framebuffer. - /// - /// - /// Pass null or -1 to use the system default. - /// - int? PreferredDepthBufferBits { get; set; } + /// + /// Flags used to create the OpenGL context. + /// + /// + /// This can only be set during the method. + /// + OpenGLContextFlags Flags { get; set; } - /// - /// Preferred stencil buffer bits of the window's framebuffer. - /// - /// - /// Pass null or -1 to use the system default. - /// - int? PreferredStencilBufferBits { get; set; } - - /// - /// Preferred red, green, blue, and alpha bits of the window's framebuffer. - /// - /// - /// Pass null or -1 for any of the channels to use the system default. - /// - Vector4D? PreferredBitDepth { get; set; } - - /// - /// The API version to use. - /// - Version32? ApiVersion { get; set; } + /// + /// The profile the OpenGL context should use. If is used, the OpenGL + /// component is effectively disabled, allowing for other graphics APIs/components to be used. If any of the other + /// properties on this class are set while this property is , this property + /// shall automatically be populated with the value . + /// + /// + /// This can only be set during the method. If the value is + /// , this shall be replaced with the actual value upon exit from + /// . + /// + OpenGLContextProfile Profile { get; set; } - nint? GetProcAddress(string proc); - void SwapBuffers(); - } -} -``` + /// + /// Gets a value indicating whether the current configuration is supported (e.g. version number). If + /// is not and this property is true, the + /// OpenGL context shall be created and accessible upon exit from + /// . + /// + bool IsSupported { get; } -## `IGLSurface` + /// + /// Gets or sets a value indicating whether the platform should automatically + /// after . Defaults to true. + /// + /// + /// This can be set at any point throughout the surface's execution. + /// + bool ShouldSwapAutomatically { get; set; } -```cs -namespace Silk.NET.Windowing -{ - public interface IGLSurface : INativeGLSurface - { - ContextFlags ContextFlags { get; set; } - ContextProfile ContextProfile { get; set; } - IGLSurface? SharedContext { get; set; } - - /// - /// Enables OpenGL support for this surface. This will create a surface upon initialization. - /// - bool TryEnableOpenGL(); - } + /// + /// Gets or sets the context with which this context should share resources. + /// + /// + /// This can only be set during the method. + /// + IGLContext? SharedContext { get; set; } } -``` -## `IGlesSurface` +public abstract partial class Surface : IGLContextSource +{ + /// + /// Gets the OpenGL configuration of the surface, if supported. + /// + public virtual ISurfaceOpenGL? OpenGL { get; } + IGLContext? IGLContextSource.GLContext => OpenGL; +} +``` +Expected to be used as follows: ```cs -namespace Silk.NET.Windowing +public class Program : ISurfaceApplication { - public interface IGlesSurface : INativeGLSurface + public static void Initialize(TSurface surface) where TSurface : Surface { - IGlesSurface? SharedContext { get; set; } - /// - /// Enables OpenGLES support for this surface. This will create a surface upon initialization. - /// - bool TryEnableOpenGLES(); + if (surface.OpenGL is null) + { + throw new NotSupportedException("OpenGL is not supported on this platform!"); + } + + // Try desktop GL (but also instead of doing this, try to use OpenGLContextProfile.Default which will + // automatically use either desktop GL or GLES) + surface.OpenGL.Profile = OpenGLContextProfile.Core; + surface.OpenGL.Version = new(3, 3); + if (!surface.OpenGL.IsSupported) + { + // Try GLES. + surface.OpenGL.Profile = OpenGLContextProfile.ES2; + surface.OpenGL.Version = new(3, 0); + if (!surface.OpenGL.IsSupported) + { + throw new NotSupportedException("OpenGL driver doesn't support a high enough version!") + } + } + + GL gl = null!; + surface.Created += _ => + { + gl = surface.CreateOpenGL(); + gl.Enable(EnableCap.DebugOutput); + // ... + }; + + // OpenGL context is created once this function exits. } + + public static void Main() => ISurfaceApplication.Run(); } ``` -## `IVkSurface` +# The Window Component + +A surface may be drawn within one window of many in a window manager. If the platform supports this, the window +component grants the user control over their presentation and behaviour within that window manager. ```cs -namespace Silk.NET.Windowing -{ - public interface IVkSurface : ISurface - { - /// Enables Vulkan support for this surface. - bool TryEnableVulkan(); +namespace Silk.NET.Windowing; + +/// +/// Contains properties pertaining to the window's position or size changing. +/// +/// The surface that owns the window the event pertains to. +/// The previous value of . +/// The new value of . +/// The previous value of . +/// The new value of . +public readonly record struct WindowCoordinatesEvent( + Surface Surface, + Silk.NET.Maths.RectangleF OldBounds, + Silk.NET.Maths.RectangleF NewBounds, + Silk.NET.Maths.RectangleF OldClientArea, + Silk.NET.Maths.RectangleF NewClientArea +) { + /// + /// The previous value of . + /// + public Vector2 OldSize => OldBounds.Size; - /// - /// Create a Vulkan surface. - /// - /// The Vulkan instance to create a surface for. - /// A custom Vulkan allocator. Can be omitted by passing null. - /// A handle to the Vulkan surface created - unsafe ulong Create(nint instance, void* allocator); + /// + /// The new value of . + /// + public Vector2 NewSize => NewBounds.Size; - /// - /// Get the extensions required for Vulkan to work on this platform. - /// - /// The number of extensions in the returned array - /// An array of strings, containing names for all required extensions - unsafe byte** GetRequiredExtensions(out uint count); - } -} -``` + /// + /// The previous value of . + /// + public Vector2 OldClientSize => OldClientArea.Size; -## `IGLDesktopSurface` + /// + /// The new value of . + /// + public Vector2 NewClientSize => NewClientArea.Size; -```cs -namespace Silk.NET.Windowing -{ - public interface IGLDesktopSurface : IDesktopSurface, IGLSurface { } + /// + /// The previous value of . + /// + public Vector2 OldPosition => OldBounds.Min; + + /// + /// The new value of . + /// + public Vector2 NewPosition => NewBounds.Min; } -``` +/// +/// Represents the current state of the window. +/// +public enum WindowState +{ + /// + /// The window is in its regular configuration. + /// + Normal = 0, -## `IGlesDesktopSurface` + /// + /// The window has been minimized to the task bar. + /// + Minimized, -```cs -namespace Silk.NET.Windowing -{ - public interface IGlesDesktopSurface : IDesktopSurface, IGlesSurface { } -} -``` + /// + /// The window has been maximized, covering the entire desktop, but not the taskbar. + /// + Maximized, -## `IVkDesktopSurface` + /// + /// The window has been fullscreened, covering the entire surface of the monitor without a border, with exclusive + /// control over the display. Note that changing to/from this state may enact an implicit change to + /// 's state if supported. + /// + ExclusiveFullscreen, -```cs -namespace Silk.NET.Windowing -{ - public interface IVkDesktopSurface : IDesktopSurface, IVkSurface { } + /// + /// The window has been fullscreened, covering the entire surface of the monitor, but still uses window management + /// to allow the user to interoperate with other applications easily. This setting leads the + /// setting to be ignored, as this setting is functionally equivalent to + /// and a border. + WindowedFullscreen } -``` -## `IGLTransparentFramebufferSurface` - -```cs -namespace Silk.NET.Windowing +/// +/// Contains properties pertaining to a boolean window property being toggled. +/// +/// The new value. +public readonly record struct WindowToggleEvent(Surface Surface, bool Value); + +/// +/// Contains properties pertaining to a change in window state. +/// +/// The surface that owns the window to which the event pertains. +/// The previous value of . +/// The new value of . +public readonly record struct WindowStateEvent(Surface Surface, WindowState OldState, WindowState NewState); + +/// +/// Contains properties pertaining to one or more files being dropped onto a window. +/// +/// The surface that owns the window to which the event pertains. +/// The paths of the files dropped onto the window. +public readonly record struct WindowFileEvent(Surface Surface, IReadOnlyList Files); + +/// +/// Represents a single window icon. +/// +public ref struct WindowIcon { - public interface IGLTransparentFramebuffer : INativeGLSurface - { - bool TransparentFramebuffer { get; set; } - } + /// + /// The width of the window icon. + /// + public required int Width { get; init; } + + /// + /// The height of the window icon. + /// + public required int Height { get; init; } + + /// + /// The window icon's pixel data in row-major order, where 4 bytes are allocated for each pixel and 1 byte + /// representing each of the red, green, blue, and alpha channels (in that order). + /// + public required ReadOnlySpan Data { get; init; } } -``` -## `ContextFlags` +/// +/// One or more s representing multiple variants (e.g. for size/DPI differences) of the same +/// window icon. +/// +public ref struct WindowIconVariants +#if NET9_0_OR_GREATER + : IEnumerable +#endif +{ + /// + /// The maximum number of variants. + /// + public const int MaxVariants = 16; + + /// + /// Creates a window icon with just one variant. + /// + public WindowIconVariants(WindowIcon icon); + + /// + /// Gets or sets the window icon variant at the given index. + /// + public WindowIcon this[int index] { get; set; } + + /// + /// Gets the number of variants within this window icon. + /// + public int Count { get; } + + /// + /// Adds a variant. + /// + /// The variant to add. + public void Add(WindowIcon icon); + + /// + /// Removes the variant at the given index. + /// + /// The index to remove at. All elements thereafter will be shifted left by one. + public void RemoveAt(int i); + + /// + /// Converts a into a with one variant. + /// + /// The icon. + /// The single-variant incorporating + public static implicit operator WindowIconVariants(WindowIcon icon); + + /// + /// Enumerates the variants contained within this . + /// + /// An enumerator. + public Enumerator GetEnumerator(); + +#if NET9_0_OR_GREATER + // Will return byte[,] elements, this is only implemented to make collection/initializer expressions to light up. + // Not implementing the generic interface to further discourage use. + // Ref structs can only implement interfaces as of C# 13/.NET 9 + IEnumerator IEnumerable.GetEnumerator(); +#endif -```cs -namespace Silk.NET.Windowing -{ /// - /// Represents flags related to the OpenGL context. + /// An enumerator over s contained in . This can only be + /// created using . /// - [Flags] - public enum ContextFlags + public ref struct Enumerator { /// - /// No flags enabled. + /// The output from if that returned true. /// - Default = 0, + public WindowIcon Current { get; } /// - /// Enables debug context; debug contexts provide more debugging info, but can run slower. + /// Retrieves the next variant. This must be called before reading the first element (and every subsequent + /// element). /// - Debug = 1, + public bool MoveNext(); /// - /// Enables forward compatability; this context won't support anything marked as deprecated in the current - /// version. + /// Resets the enumerator back to its initial state. Note that must be called before + /// retrieving an element again. /// - /// On OpenGL contexts older than 3.0, this flag does nothing. - ForwardCompatible = 2 + public void Reset(); } } -``` - -## `ContextProfile` -```cs -namespace Silk.NET.Windowing +/// +/// Represents the window border. +/// +public enum WindowBorder { /// - /// Represents the context profile OpenGL should use. + /// The window can be resized by clicking and dragging its border. /// - public enum ContextProfile - { - /// - /// Uses a core OpenGL context, which removes some deprecated functionality. - /// - Core = 0, + Resizable = 0, - /// - /// Uses a compatability OpenGL context, allowing for some deprecated functionality. This should only ever be - /// used for maintaining legacy code; no newly-written software should use this. - /// - Compatability - } -} -``` + /// + /// The window border is visible, but cannot be resized. All window-resizings must happen solely in the code. + /// + Fixed, -## `WindowBorder` + /// + /// The window border is hidden. + /// + Hidden +} -```cs -namespace Silk.NET.Windowing +public interface ISurfaceWindow { /// - /// Represents the window border. + /// Gets the window bounds including the window border. /// - public enum WindowBorder - { - /// - /// The window can be resized by clicking and dragging its border. - /// - Resizable = 0, + Silk.NET.Maths.RectangleF Bounds { get; set; } - /// - /// The window border is visible, but cannot be resized. All window-resizings must happen solely in the code. - /// - Fixed, + /// + /// Forwards to the component of . + /// + // DIM is required, but this implementation is for illustrative purposes only! Exact resize semantics of silk types + // are yet to be defined. + Vector2 Size { get => Bounds.Size; set => Bounds = Bounds with { Size = value }; } - /// - /// The window border is hidden. - /// - Hidden - } -} -``` + /// + /// Forwards to the component of . + /// + // DIM is required, but this implementation is for illustrative purposes only! Exact resize semantics of silk types + // are yet to be defined. + Vector2 Position { get => Bounds.Min; set => Bounds = Bounds with { Min = value }; } -## `WindowState` + /// + /// Gets only the inner client area of the window in screen coordinates. For pixels, use + /// or the relevant properties of . + /// + /// + /// Setting this property is interpreted to mean changing by the same delta in the hopes of + /// achieving the desired result. + /// + Silk.NET.Maths.RectangleF ClientArea { get; set; } -```cs -namespace Silk.NET.Windowing -{ /// - /// Represents the current state of the window. + /// Forwards to the component of . /// - public enum WindowState - { - /// - /// The window is in its regular configuration. - /// - Normal = 0, + /// + /// Setting this property is interpreted to mean changing by the same delta in the hopes of + /// achieving the desired result. + /// + Vector2 ClientSize { get => ClientArea.Size; set => ClientArea = ClientArea with { Size = value }; } - /// - /// The window has been minimized to the task bar. - /// - Minimized, + /// + /// Raised when and/or changes. + /// + event Action CoordinatesChanged { add; remove; } - /// - /// The window has been maximized, covering the entire desktop, but not the taskbar. - /// - Maximized, + /// + /// Gets or sets a value indicating whether, unless set to false before the next , + /// the window will close resulting in the irrevocable termination of the surface. + /// + bool IsCloseRequested { get; set; } - /// - /// The window has been fullscreened, covering the entire surface of the monitor. - /// - Fullscreen - } -} -``` + /// + /// Raised when is set to true. + /// + event Action CloseRequested { add; remove; } -## `IScreen` + /// + /// Gets or sets a value indicating whether the window is visible. + /// + bool IsVisible { get; set; } -```cs -namespace Silk.NET.Windowing -{ /// - /// An interface representing a screen. + /// Raised when changes. /// - public interface IScreen - { - /// - /// The name of this screen. - /// - string Name { get; } + event Action VisibilityChanged { add; remove; } - /// - /// The index of this screen. - /// - int Index { get; } + /// + /// Gets or sets a value indicating whether the window currently has input focus. If setting to true, the + /// window will likely be raised atop other windows in order to obtain input focus. Setting to false is not + /// guaranteed to do anything. + /// + bool IsFocused { get; set; } - /// - /// The workarea of this screen. - /// - Rectangle WorkArea { get; } + /// + /// An event raised when changes. + /// + event Action FocusChanged { add; remove; } - /// - /// The current video mode of this monitor. - /// - VideoMode VideoMode { get; } + /// + /// Gets or sets a title for the window. + /// + string Title { get; set; } - /// - /// This screen's gamma correction. - /// - float Gamma { get; set; } + /// + /// Gets or sets the state of the window within the context of the window manager. This setting can be changed by + /// the user through actions (e.g. the minimise button, maximise, etc). + /// + WindowState State { get; set; } - /// - /// Get all video modes that this screen supports. - /// - /// An array of all video modes. - IEnumerable GetAllVideoModes(); - } -} -``` + /// + /// An event raised when changes. + /// + event Action StateChanged { add; remove; } -## `VideoMode` + /// + /// Gets or sets the style of the window border around the client area. + /// + WindowBorder Border { get; set; } -```cs -namespace Silk.NET.Windowing -{ - public readonly struct VideoMode - { - public VideoMode(Vector2D? resolution = null, int? refreshRate = null); - public VideoMode(int? refreshRate); + /// + /// Gets or sets a value indicating whether this window shall be drawn atop all other windows in the window manager. + /// + bool IsTopMost { get; set; } - /// - /// Resolution of the full screen window. - /// - public Vector2D? Resolution { get; init; } + /// + /// An event raised when the user drops files onto the window. + /// + event Action FileDrop { add; remove; } - /// - /// Refresh rate of the full screen window in Hz. - /// - public int? RefreshRate { get; init; } + /// + /// Sets the window's icon to one of the window icons provided. The icon is selected using an undefined mechanism by + /// the underlying platform, typically taking into account the pixel size of each variant. If no icon variants are + /// provided, then the default icon shall be restored. + /// + /// The window icon variants to set. + /// A value indicating whether the operation was successful. + bool TrySetIcon(WindowIconVariants icon); +} - /// - /// The default video mode. This uses the window size for resolution and doesn't care about other values. - /// - public static VideoMode Default { get; } - } +public abstract partial class Surface +{ + /// + /// Gets the window in which the surface is rendering. + /// + public virtual ISurfaceWindow? Window { get; } } ``` -## `Surface` - +Expected to be used as follows: ```cs -namespace Silk.NET.Windowing +public class Program : ISurfaceApplication { - public static class Surface + public static void Initialize(TSurface surface) where TSurface : Surface { - public static bool IsPlatformSupported { get; } - public static ISurface GetOrCreate(); - public static ISurface CreateNew(); - public static void ClearCurrentContexts(); + if (surface.Window is not null) + { + // Window icon with a single variant + surface.Window.TrySetIcon(new WindowIcon { Width = 16, Height = 16, Data = /* etc */ }); + + // Window icon with multiple variants (pre-C# 13/.NET 9) + WindowIconVariants variants = new WindowIcon { Width = 16, Height = 16, Data = /* etc */ }; + variants.Add(new WindowIcon { Width = 32, Height = 32, Data = /* etc */ }); + surface.Window.TrySetIcon(variants); + + // Window icon with multiple variants (post-C# 13/.NET 9) + surface.Window.TrySetIcon([new WindowIcon { Width = 16, Height = 16, Data = /* etc */ }, new WindowIcon { Width = 32, Height = 32, Data = /* etc */ }]); + + // Request windowed fullscreen. + surface.Window.Border = WindowBorder.Hidden; + surface.Window.State = WindowState.Maximized; + } } + + public static void Main() => ISurfaceApplication.Run(); } ``` -## `SurfaceExtensions` +# The Displays Component + +A surface may be rendered on one of many displays. This component allows a surface to access information about the +display on which it's rendering, move to another display, and/or change the video mode of the display. ```cs -namespace Silk.NET.Windowing +namespace Silk.NET.Windowing; + +/// +/// Contains properties pertaining to a display being connected or disconnected. +/// +/// The surface for which display(s) were connected. +/// +/// Old display objects are not guaranteed to be valid or relevant after this event is raised. +/// +// Currently this event does not include the displays that were connected or disconnected. This is primarily because +// there's no clean way to expose such "diffs" from an API perspective (as disconnected IDisplay objects are likely to +// be invalid), and also why would we need to? If a use case arises and this can be implemented in a sound way, let's +// evaluate that then. +public readonly record struct DisplayAvailabilityChangeEvent(Surface Surface); + +/// +/// Contains properties pertaining to a surface changing to a different display. +/// +/// The surface changing to a different display. +/// The display the surface has changed to. +/// +/// It is expected that this event shall be raised for each logically substantial change to the display parameters and +/// this can be defined by each individual platform. For instance, if the underlying platform does not give the +/// application access to any displays other than the one it's currently being displayed on, then it is expected that +/// this event shall be raised if the display changed even if this is represented by the same object. Old display +/// objects are not guaranteed to be valid or relevant after this event is raised. +/// +public readonly record struct DisplayChangeEvent(Surface Surface, IDisplay Display); + +/// +/// Contains properties pertaining to a surface changing to a different video mode. +/// +/// The surface changing to a different video mode. +/// The video mode the surface has changed to. +public readonly record struct VideoModeChangeEvent(Surface Surface, VideoMode VideoMode); + +/// +/// Contains properties pertaining to a change in the available video modes for a display. +/// +/// The surface owning the display. +/// The display for which the video mode availability changed. +// I don't think we need to have a diff here either, why would old video modes be relevant? +public readonly record struct DisplayVideoModeAvailabilityChangeEvent(Surface Surface, IDisplay Display); + +/// +/// Contains properties pertaining to a change in the location and/or size of a display. +/// +/// The surface owning the display. +/// The display for which the location and/or size changed. +/// The previous value of . +/// The new value of . +/// The previous value of . +/// The new value of . +public readonly record struct DisplayCoordinatesEvent( + Surface Surface, + IDisplay Display, + Silk.NET.Maths.RectangleF OldBounds, + Silk.NET.Maths.RectangleF NewBounds, + Silk.NET.Maths.RectangleF OldWorkArea, + Silk.NET.Maths.RectangleF NewWorkArea +); + +/// +/// Represents the properties of a surface whose rendering is intrinsically linked to the composition of a specific +/// display. In most cases, this translates to "the surface is rendering in exclusive fullscreen mode". +/// +/// The index of the video mode in . +/// +/// The resolution the surface is rendering on its display at, if known. If null, it is highly likely that the +/// surface is not rendering in exclusive fullscreen mode or otherwise has its rendering intrinsically linked to the +/// composition of a specific display. +/// +/// +/// The rate (per second) at which the physical display will receive new renders from the surface, if known. If +/// null, the platform may not expose the refresh rate to surfaces or it is highly likely that the +/// surface is not rendering in fullscreen mode or otherwise has its rendering intrinsically linked to the composition +/// of a specific display. +/// +/// +/// If a default video mode is encountered, it is highly likely the surface is not rendering in exclusive +/// fullscreen mode. If an individual property is null, it is highly likely that property is not controllable +/// programmatically. +/// +public readonly record struct VideoMode(int Index, Vector2? Resolution, float? RefreshRate); + +/// +/// Represents a display on which a surface can be rendered. +/// +/// +/// Each surface shall get its own object for each display. This is primarily to ensure that +/// users get events dispatched with the surface they expect depending on which the +/// was sourced from. However, display objects can be somewhat shared between all surfaces that +/// share a common ancestor (the "root surface"). Specifically, an object at a given index in +/// on one surface shall be equatable to the object sourced from the same index +/// in on another surface with the same root surface. Furthermore, +/// on one surface shall be assignable to an object +/// sourced from another surface with the same root surface, where shall lookup +/// the equivalent object from its displays upon +/// assignment. +/// +public interface IDisplay : IEquatable { /// - /// Extensions for ISurface + /// Gets the position and resolution of the monitor in screen space. /// - public static class SurfaceExtensions - { - /// - /// Start the default event loop on this surface. - /// - /// The surface to begin the loop on. - public static void Run(this ISurface surface); - - /// - /// Gets the full size of the given window including its borders. - /// - /// The window to get size information from. - /// The full size of the window (including both content area and borders) - public static Vector2D GetFullSize(this IDesktopSurface window); - - /// - /// Centers this window to the given monitor or, if null, the current monitor the window's on. - /// - /// The window to center. - /// The specific screen to center the window to, if any. - public static void Center(this IDesktopSurface window, IScreen? screen = null); - - /// - /// Sets the window icon to default on the given window. - /// - /// The window. - public static void SetDefaultIcon(this IDesktopSurface window); + Silk.NET.Maths.RectangleF Bounds { get; } - /// - /// Sets a single window icon on the given window. - /// - /// The window. - /// The icon to set. - public static void SetWindowIcon(this IDesktopSurface window, ref RawImage icon); - } + /// + /// Gets the area within where surfaces are intended to be drawn. + /// + /// + /// This typically is the area left once you account for things like the menu bar and taskbar. + /// + Silk.NET.Maths.RectangleF WorkArea { get; } + + /// + /// Gets a list of video modes known to be available when this display is . + /// It may be the case that a list of video modes can't be determined until that's the case. Note that inclusion of + /// a in this list does not guarantee its presence in + /// , as this can depend on the state of other components. + /// + IReadOnlyList? KnownVideoModes { get; } + + /// + /// Gets a value indicating whether the user has designated this display their primary display. + /// + bool IsPrimary { get; } + + /// + /// Gets a colloquial name for the display. This may change, but hopefully not to something the end user won't recognise. + /// + string Name { get; } + + /// + /// An event raised when and/or changes. + /// + event Action CoordinatesChanged { add; remove; } + + /// + /// An event raised when changes. + /// + event Action KnownVideoModesChanged { add; remove; } +} + +/// +/// Provides the ability to configure displays on which the surface can render. +/// +public interface ISurfaceDisplay +{ + /// + /// Gets or sets display on which the surface is currently rendering. If setting, value must be contained in + /// . + /// + IDisplay Current { get; set; } + + /// + /// Gets a list of other displays that this surface can be moved to. If the surface cannot be programmatically moved + /// to another display, it is expected that this shall return a single element list containing + /// . + /// + IReadOnlyList Available { get; } + + /// + /// Gets or sets the video mode with which the surface is being rendered to the display. If setting, value + /// must be contained in . + /// + VideoMode VideoMode { get; set; } + + /// + /// Gets a list of video modes known to be available when this display is . + /// It may be the case that a list of video modes can't be determined until that's the case. Furthermore, if + /// is supported, needs to be + /// to get access to exclusive fullscreen video modes. + /// may be acceptable if the backend supports implicitly switching + /// between windowed and exclusive fullscreen states, but this is not a requirement. + /// + IReadOnlyList AvailableVideoModes { get; } + + /// + /// An event raised when changes. + /// + event Action CurrentDisplayChanged { add; remove; } + + /// + /// An event raised when changes. + /// + event Action AvailableChanged { add; remove; } + + /// + /// An event raised when changes. + /// + event Action AvailableVideoModesChanged { add; remove; } + + /// + /// An event raised when changes. + /// + event Action VideoModeChanged { add; remove; } } -``` -## `Version32` +public abstract partial class Surface +{ + /// + /// Gets the display configuration for the surface, if supported. + /// + public virtual ISurfaceDisplay? Display { get; } +} +``` -Exactly as is from 2.X. +`Gamma` has been removed due to upstream removal citing poor support in modern operating systems. + +This proposal repeals the Window Hosts (Monitors) proposal from Silk.NET 1.0 Preview 4. All child surfaces must be +hosted by another surface or be the "root surface". + +Note that there are some complex interactions with the `ISurfaceWindow.State` property and `VideoMode` given that the +former allows changing to exclusive fullscreen and the latter allows changing exclusive fullscreen mode. This raised a +number of questions about whether using the latter without the former should invoke implicit changes to the former. +Ultimately, it was decided that this would be bug prone and very hard to implement, as such the following requirements +are placed on implementors of this component: +- If the Window component is supported, `AvailableVideoModes` shall return a single-element list containing `default` + when `ISurfaceWindow.State` is not `WindowState.ExclusiveFullscreen` and not `WindowState.WindowedFullscreen`. +- If the Window component is supported, `AvailableVideoModes` shall either return a single-element list containing + `default` or return a list containing the `default` video mode along with all exclusive video modes when + `ISurfaceWindow.State` is `WindowState.WindowedFullscreen`. +- If the Window component is supported, `AvailableVideoModes` shall return a list of exclusive video modes when + `ISurfaceWindow.State` is `WindowState.ExclusiveFullscreen`, except where the `AvailableVideoModes` implementation + includes exclusive video modes when `ISurfaceWindow.State` is `WindowState.WindowedFullscreen`, in which case + `AvailableVideoModes` shall return the same list for both `WindowState.WindowedFullscreen` and + `WindowState.ExclusiveFullscreen`. +- If the Window component is supported and `ISurfaceWindow.State` is currently `WindowState.ExclusiveFullscreen`, + `ISurfaceWindow.State` shall be changed to `WindowState.WindowedFullscreen` (raising the appropriate events as + necessary) when `ISurfaceDisplay.VideoMode` is set to the `default` video mode. +- If the Window component is supported and `ISurfaceWindow.State` is currently `WindowState.WindowedFullscreen`, + `ISurfaceWindow.State` shall be changed to `WindowState.ExclusiveFullscreen` (raising the appropriate events as + necessary) when `ISurfaceDisplay.VideoMode` is set to a non-`default` video mode. +- If the Window component is not supported, `AvailableVideoModes` shall return a list of video modes supported in + exclusive fullscreen mode and may also include the `default` video mode for non-exclusive control of the underlying + window manager. What this means is open to the backend's interpretation, as in lieu of the Window component this could + simply mean windowed fullscreen, bordered windowed, or basically anything that isn't exclusive fullscreen. +- `IDisplay.KnownVideoModes`, if the implementation returns a non-`null` value for this property, shall contain all + video modes that may be included in `AvailableVideoModes` regardless of the `State` property i.e. it includes the + `default` video mode for non-exclusive and all exclusive fullscreen video modes. This is valid as per the + `KnownVideoModes` documentation. + +# The Vulkan Component ```cs -namespace Silk.NET.Core +namespace Silk.NET.Windowing; + +/// +/// The Vulkan component of a . +/// +public interface ISurfaceVulkan { /// - /// A 32-bit version structure. + /// Gets or sets a value indicating whether the Vulkan component is enabled for this surface. /// - public readonly struct Version32 - { - /// - /// The underlying Vulkan-compatible 32-bit version integer. - /// - public uint Value { get; } - - /// - /// Creates a Vulkan version structure from the given major, minor, and patch values. - /// - /// The major value. - /// The minor value. - /// The patch value. - public Version32(uint major, uint minor, uint patch); - - /// - /// Creates a Vulkan version structure from the given Vulkan-compatible value. - /// - /// The value. - private Version32(uint value); - - /// - /// Gets the major component of this version structure. - /// - public uint Major { get; } - - /// - /// Gets the minor component of this version structure. - /// - public uint Minor { get; } - - /// - /// Gets the patch component of this version structure. - /// - public uint Patch { get; } - - /// - /// Creates a 32-bit version structure from the given 32-bit unsigned integer. - /// - /// The uint value. - /// The 32-bit version structure. - public static explicit operator Version32(uint val); + /// + /// This can only be set during the method. + /// + bool IsEnabled { get; set; } - /// - /// Creates a 32-bit version structure from the given managed version class. - /// - /// The version instance. - /// The 32-bit version structure. - public static implicit operator Version32(Version version); + /// + /// Creates a VkSurface for this surface. + /// + /// + /// The VkInstance to use. Must have extensions specified in enabled. + /// + /// The VkAllocationCallbacks* to use. + /// + /// This can only be executed once the method has returned. + /// + /// The VkSurface. + ulong CreateSurface(nint instance, Ptr allocator); - /// - /// Gets the 32-bit unsigned integer representation for this 32-bit version structure. - /// - /// The 32-bit version structure. - /// The 32-bit unsigned integer. - public static implicit operator uint(Version32 version); + /// + /// Gets the instance extensions that are required to be enabled on instances used for . + /// + /// The number of pointers in the return value. + /// + /// The required extensions as a native pointer. The pointer is guaranteed to share the lifetime of the surface. + /// + Ptr2D GetRequiredExtensions(out uint count); +} - /// - /// Converts this 32-bit version structure to a managed version class. - /// - /// The 32-bit version structure. - /// The managed representation. - public static implicit operator Version(Version32 version); - } +public abstract partial class Surface +{ + /// + /// Gets the Vulkan component of this surface. + /// + public virtual ISurfaceVulkan? Vulkan { get; } } ``` -## `RawImage` +# The Scale Component -Exactly as is from 2.X. +This component provides the user the ability to account for high pixels-per-inch (PPI, also known as DPI) displays in +their rendering code. It provides no configuration but provides extra information to the user if supported. ```cs -namespace Silk.NET.Core +namespace Silk.NET.Windowing; + +/// +/// Contains properties pertaining to a change in a surface's scale. +/// +/// The surface to which the change in scale occurred. +/// The previous value for . +/// The new value for . +/// The previous value for . +/// The new value for . +/// The previous value for . +/// The new value for . +public readonly record struct ScaleChangedEvent( + Surface Surface, + float OldContent, + float NewContent, + float OldDraw, + float NewDraw, + float OldPixelDensity, + float NewPixelDensity +); + +/// +/// Provides information pertaining to the surface's graphical scaling. +/// +/// +/// is typically used to scale UI elements to the correct size for the end user. +/// on the other hand is used to scale the entire application to cover the entire client +/// area in cases where the window client size is smaller than the actual drawable size (i.e. it is high density). +/// If scaling content for legibility and scaling the application's rendering as a whole are not needed to be separated, +/// it is recommended to use . Implementations shall always request a high density surface if +/// given the choice, to account for the platforms where applications may not be able to opt-out of high density. +/// +public interface ISurfaceScale { /// - /// Represents loaded, uncompressed, processed image data. + /// Gets the factor with which the application should scale its content to make the content more legible for the + /// user. This has no influence on . /// - public readonly struct RawImage : IEquatable - { - /// - /// Creates a given pixel data and pixel dimensions. - /// - /// The width of the image. - /// The height of the image. - /// The image daqta. - public RawImage(int width, int height, Memory rgbaPixels); - - /// - /// The width of the image in pixels - /// - public int Width { get; } - - /// - /// The height of the image in pixels. - /// - public int Height { get; } + /// + float Content { get; } - /// - /// The image data. - /// - public Memory Pixels { get; } + /// + /// Gets the suggested amplification factor when drawing in terms of . This + /// represents the scale from the pixel resolution to the desired content size, and is typically the multiplication + /// of and . + /// + /// + /// For example, if is 2.0 (i.e. there are 2 pixels per screen coordinate) + /// and the window manager requests that applications scale their content up by 2.0 to meet the user's + /// settings as per , this would be 4.0. This is because we're scaling once to + /// account for the fact that the application has twice the amount of pixels available to it for the given window + /// size, and then scaling again so that what we are drawing appears zoomed in as per the user's request. Note that + /// it is rarely the case that an operating system employs both dense pixels and content scale. macOS for + /// instance, instead of setting , opts to scale the resolution in the cases where the + /// user wants magnified visuals instead of having the applications scale their content; whereas Windows sets + /// and instead always keeps as 1.0. This is down + /// to philosophical differences between the window coordinate systems on platforms as to whether they prefer to + /// deal in physical device pixels or physical content sizes. + /// + float Draw { get; } - /// - /// Checks whether the two given s are equal. - /// - /// The first raw image. - /// The second raw image to compare the first against. - /// True if they are equal, false otherwise. - /// - /// This does not check whether the byte arrays are equal, only whether their references are the same. - /// - public static bool operator ==(RawImage left, RawImage right); + /// + /// Gets the ratio of pixels rendered to window size. This shall be equivalent to + /// divided by . + /// + /// + float PixelDensity { get; } - /// - /// Checks whether the two given s are not equal. - /// - /// The first raw image. - /// The second raw image to compare the first against. - /// True if they are not equal, false otherwise. - /// - /// This does not check whether the byte arrays are equal, only whether their references are the same. - /// - public static bool operator !=(RawImage left, RawImage right); + /// + /// An event raised when any scale factor changes. + /// + event Action Changed { add; remove; } +} - /// - /// Checks whether the given is equal to this one. - /// - /// The raw image to compare this raw image against. - /// True if they are equal, false otherwise. - /// - /// This does not check whether the byte arrays have equal, only whether their references are the same. - /// - public bool Equals(RawImage other); - - /// - public override bool Equals(object obj); - - /// - public override int GetHashCode(); - } +public abstract partial class Surface +{ + /// + /// Gets the content scale configuration within the surface. + /// + public virtual ISurfaceScale? Scale { get; } } ``` -## `IWindowHandlesSource` +# The Children Component -Pretty much as is from 2.X's `INativeWindowSource` except using the new struct. +Some platforms allow surfaces to have children. If this is the case, the children component shall expose a mechanism to +spawn a new `ISurfaceApplication` and associated surface as a child of the currently running one. ```cs -namespace Silk.NET.Core +namespace Silk.NET.Windowing; + +/// +/// Provides the ability to spawn children surfaces. +/// +public interface ISurfaceChildren { - public interface IWindowHandlesSource - { - WindowHandles Native { get; } - } + /// + /// Spawns an application to run within a new child surface. This call shall not block. + /// + /// The application to run within the child surface. + void Spawn() where T : ISurfaceApplication; +} + +public abstract partial class Surface +{ + /// + /// Gets the "child surface" functionality if available. + /// + public virtual ISurfaceChildren? Children { get; } } ``` -## `WindowHandles` +Implementations are expected to be aware of the resource sharing/validity requirements set forth at numerous points +throughout this document - search for occurrences of ISurfaceChildren. + +# Detached Surfaces -Replaces 2.X's `INativeWindow` +When reviewing this proposal, we noticed that we hadn't accounted for the use case wherein users may want to take +advantage of desktop features (e.g. the ability to run one's own timing/game loop logic) if they are available. Up until +now, `ISurfaceApplication.Run` (or an alternative implementation's equivalent) is the only way to obtain a `Surface` +which involves taking control of the entire application and only giving the user control through the exposed callbacks. +For most use cases, this is fine as we've exposed fundamentals like calling into user code as fast as possible (`Tick`) +or on regular intervals (`Render`/`Update`). However, there will be some power users that will want to retain the +ability to control their own timing if possible, for which we propose the concept of "detached surfaces". + +"Detached surfaces" have a lifetime that is detached from that of the `ISurfaceApplication` they're associated with. +That is, the implementation's usual handling of an `ISurfaceApplication` and its lifecycle is instead user controlled. +This is notable as creation and destruction of a `Surface` is usually controlled by the implementation, whereas with +this API the destruction shall be delegated to the user. This is exposed as follows: ```cs -namespace Silk.NET.Core +namespace Silk.NET.Windowing; + +/// +/// Represents a surface with a user-controlled lifecycle. +/// +/// +/// This API is not guaranteed to be supported on all platforms and you should only use it if you know what +/// you're doing and know you need the granular control this API provides! Please use +/// instead where possible. If you insist on using this API, please fall back +/// to if returns false indicating a lack +/// of support. +/// +public interface IDetachedSurfaceLifecycle : IDisposable { - [StructLayout(LayoutKind.Auto)] - public struct WindowHandles - { - // ... - } -} -``` + /// + /// Gets the surface with which this lifecycle is associated. The destruction of this surface is handled by + /// the method of this implementation. + /// + Surface Surface { get; } + + /// + /// Gets a value indicating whether the surface is indicating that its lifecycle should conclude as a result of + /// its current configuration e.g. an entire tick passing with being + /// true. + /// + /// + /// It is expected that shall not be called if this property is true. + /// + bool ShouldTerminate { get; } -The Silk.NET team wishes to reserve the right to add any relevant window handles that arise when implementing the reference implementation as nullable fields in this struct. + /// + /// Steps the underlying implementation's surface lifecycle (i.e. event loop), running a single tick on the + /// . + /// + /// + /// It is expected that implementations shall return after doing as little work as possible. For instance, if the + /// underlying implementation exposes one-by-one event retrieval or otherwise allows customisation of the extent to + /// which the event pump is run, it is expected that a single event shall be pumped in this case. Note that this is + /// just an example and the exact details of this is implementation-defined. + /// + void Tick(); -The goal is to keep this type lightweight as it is in Silk.NET.Core (to ensure smooth interoperability between Silk.NET packages without creating hard references), hence why interfaces or type-system-driven approached were not used. + /// + /// Attempts to create a using the reference implementation. + /// + /// The created surface lifecycle on success, null otherwise. + /// + /// The application that shall be associated with the surface. Note that even with this API, + /// shall still be called for consistency and portability. However, + /// unlike , this method shall not block and will instead return an + /// on which is expected to be continuously called to + /// enact the same behaviour on the surface. The associated application is also used for any additional global + /// configuration, such as . + /// + /// + /// true if has been populated with an + /// object containing a valid , false otherwise. + /// + /// + /// This is the same reference implementation that would otherwise use. + /// + sealed static bool TryCreate([NotNullWhen(true)] out IDetachedSurfaceLifecycle? lifecycle) where T : ISurfaceApplication; +} +``` # Meeting Notes