mirror of
https://github.com/SK-la/osu-framework.git
synced 2026-03-15 03:20:30 +00:00
Could crash if the ID wasn't in the dictionary. The old code also didn't properly close everything (it would close either the jostick or the gamepad, but not both).
820 lines
31 KiB
C#
820 lines
31 KiB
C#
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
// See the LICENCE file in the repository root for full licence text.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Drawing;
|
|
using System.Threading;
|
|
using osu.Framework.Bindables;
|
|
using osu.Framework.Configuration;
|
|
using osu.Framework.Extensions.EnumExtensions;
|
|
using osu.Framework.Graphics.Primitives;
|
|
using osu.Framework.Input;
|
|
using osu.Framework.Input.StateChanges;
|
|
using osu.Framework.Input.States;
|
|
using osu.Framework.Logging;
|
|
using osuTK;
|
|
using osuTK.Input;
|
|
using SDL;
|
|
using RectangleF = osu.Framework.Graphics.Primitives.RectangleF;
|
|
using static SDL.SDL3;
|
|
|
|
namespace osu.Framework.Platform.SDL3
|
|
{
|
|
internal unsafe partial class SDL3Window
|
|
{
|
|
private void setupInput(FrameworkConfigManager config)
|
|
{
|
|
config.BindWith(FrameworkSetting.ConfineMouseMode, ConfineMouseMode);
|
|
|
|
WindowMode.BindValueChanged(_ => updateConfineMode());
|
|
ConfineMouseMode.BindValueChanged(_ => updateConfineMode());
|
|
}
|
|
|
|
private bool relativeMouseMode;
|
|
|
|
/// <summary>
|
|
/// Set the state of SDL3's RelativeMouseMode (https://wiki.libsdl.org/SDL_SetRelativeMouseMode).
|
|
/// On all platforms, this will lock the mouse to the window (although escaping by setting <see cref="ConfineMouseMode"/> is still possible via a local implementation).
|
|
/// On windows, this will use raw input if available.
|
|
/// </summary>
|
|
public bool RelativeMouseMode
|
|
{
|
|
get => relativeMouseMode;
|
|
set
|
|
{
|
|
if (relativeMouseMode == value)
|
|
return;
|
|
|
|
if (value && !CursorState.HasFlagFast(CursorState.Hidden))
|
|
throw new InvalidOperationException($"Cannot set {nameof(RelativeMouseMode)} to true when the cursor is not hidden via {nameof(CursorState)}.");
|
|
|
|
relativeMouseMode = value;
|
|
ScheduleCommand(() => SDL_SetWindowRelativeMouseMode(SDLWindowHandle, value).LogErrorIfFailed());
|
|
updateCursorConfinement();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Controls whether the mouse is automatically captured when buttons are pressed and the cursor is outside the window.
|
|
/// Only works with <see cref="RelativeMouseMode"/> disabled.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If the cursor leaves the window while it's captured, <see cref="SDL_EventType.SDL_EVENT_WINDOW_MOUSE_LEAVE"/> is not sent until the button(s) are released.
|
|
/// And if the cursor leaves and enters the window while captured, <see cref="SDL_EventType.SDL_EVENT_WINDOW_MOUSE_ENTER"/> is not sent either.
|
|
/// We disable relative mode when the cursor exits window bounds (not on the event), but we only enable it again on <see cref="SDL_EventType.SDL_EVENT_WINDOW_MOUSE_ENTER"/>.
|
|
/// The above culminate in <see cref="RelativeMouseMode"/> staying off when the cursor leaves and enters the window bounds when any buttons are pressed.
|
|
/// This is an invalid state, as the cursor is inside the window, and <see cref="RelativeMouseMode"/> is off.
|
|
/// </remarks>
|
|
public bool MouseAutoCapture
|
|
{
|
|
set => ScheduleCommand(() => SDL_SetHint(SDL_HINT_MOUSE_AUTO_CAPTURE, value ? "1"u8 : "0"u8).LogErrorIfFailed());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Provides a bindable that controls the window's <see cref="CursorStateBindable"/>.
|
|
/// </summary>
|
|
public Bindable<CursorState> CursorStateBindable { get; } = new Bindable<CursorState>();
|
|
|
|
public CursorState CursorState
|
|
{
|
|
get => CursorStateBindable.Value;
|
|
set => CursorStateBindable.Value = value;
|
|
}
|
|
|
|
private RectangleF? cursorConfineRect;
|
|
|
|
public RectangleF? CursorConfineRect
|
|
{
|
|
get => cursorConfineRect;
|
|
set
|
|
{
|
|
cursorConfineRect = value;
|
|
updateCursorConfinement();
|
|
}
|
|
}
|
|
|
|
private readonly Dictionary<SDL_JoystickID, SDL3ControllerBindings> controllers = new Dictionary<SDL_JoystickID, SDL3ControllerBindings>();
|
|
|
|
private readonly Dictionary<SDL_PenID, TabletPenDeviceType> penDeviceTypes = new Dictionary<SDL_PenID, TabletPenDeviceType>();
|
|
|
|
private void updateCursorVisibility(bool cursorVisible) =>
|
|
ScheduleCommand(() =>
|
|
{
|
|
if (cursorVisible)
|
|
SDL_ShowCursor().LogErrorIfFailed();
|
|
else
|
|
SDL_HideCursor().LogErrorIfFailed();
|
|
});
|
|
|
|
/// <summary>
|
|
/// Updates OS cursor confinement based on the current <see cref="CursorState"/>, <see cref="CursorConfineRect"/> and <see cref="RelativeMouseMode"/>.
|
|
/// </summary>
|
|
private void updateCursorConfinement()
|
|
{
|
|
bool confined = CursorState.HasFlagFast(CursorState.Confined);
|
|
|
|
ScheduleCommand(() => SDL_SetWindowMouseGrab(SDLWindowHandle, confined).LogErrorIfFailed());
|
|
|
|
// Don't use SDL_SetWindowMouseRect when relative mode is enabled, as relative mode already confines the OS cursor to the window.
|
|
// This is fine for our use case, as UserInputManager will clamp the mouse position.
|
|
if (CursorConfineRect != null && confined && !RelativeMouseMode)
|
|
{
|
|
ScheduleCommand(() =>
|
|
{
|
|
var rect = ((RectangleI)(CursorConfineRect / Scale)).ToSDLRect();
|
|
SDL_SetWindowMouseRect(SDLWindowHandle, &rect).LogErrorIfFailed();
|
|
});
|
|
}
|
|
else
|
|
{
|
|
ScheduleCommand(() => SDL_SetWindowMouseRect(SDLWindowHandle, null).LogErrorIfFailed());
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bound to <see cref="FrameworkSetting.ConfineMouseMode"/>.
|
|
/// </summary>
|
|
public readonly Bindable<ConfineMouseMode> ConfineMouseMode = new Bindable<ConfineMouseMode>();
|
|
|
|
private void enqueueJoystickAxisInput(JoystickAxisSource axisSource, short axisValue)
|
|
{
|
|
// SDL reports axis values in the range short.MinValue to short.MaxValue, so we scale and clamp it to the range of -1f to 1f
|
|
float clamped = Math.Clamp((float)axisValue / short.MaxValue, -1f, 1f);
|
|
JoystickAxisChanged?.Invoke(axisSource, clamped);
|
|
}
|
|
|
|
private void enqueueJoystickButtonInput(JoystickButton button, bool isPressed)
|
|
{
|
|
if (isPressed)
|
|
JoystickButtonDown?.Invoke(button);
|
|
else
|
|
JoystickButtonUp?.Invoke(button);
|
|
}
|
|
|
|
private PointF previousPolledPoint = PointF.Empty;
|
|
|
|
private volatile uint pressedButtons;
|
|
|
|
private void pollMouse()
|
|
{
|
|
float x, y;
|
|
var pressed = (SDL_MouseButtonFlags)pressedButtons;
|
|
SDL_MouseButtonFlags globalButtons = SDL_GetGlobalMouseState(&x, &y);
|
|
|
|
if (previousPolledPoint.X != x || previousPolledPoint.Y != y)
|
|
{
|
|
previousPolledPoint = new PointF(x, y);
|
|
|
|
var pos = WindowMode.Value == Configuration.WindowMode.Windowed ? Position : windowDisplayBounds.Location;
|
|
float rx = x - pos.X;
|
|
float ry = y - pos.Y;
|
|
|
|
MouseMove?.Invoke(new Vector2(rx * Scale, ry * Scale));
|
|
}
|
|
|
|
// a button should be released if it was pressed and its current global state differs (its bit in globalButtons is set to 0)
|
|
SDL_MouseButtonFlags buttonsToRelease = pressed & (globalButtons ^ pressed);
|
|
|
|
// the outer if just optimises for the common case that there are no buttons to release.
|
|
if (buttonsToRelease != 0)
|
|
{
|
|
Interlocked.And(ref pressedButtons, (uint)~buttonsToRelease);
|
|
|
|
if (buttonsToRelease.HasFlagFast(SDL_MouseButtonFlags.SDL_BUTTON_LMASK)) MouseUp?.Invoke(MouseButton.Left);
|
|
if (buttonsToRelease.HasFlagFast(SDL_MouseButtonFlags.SDL_BUTTON_MMASK)) MouseUp?.Invoke(MouseButton.Middle);
|
|
if (buttonsToRelease.HasFlagFast(SDL_MouseButtonFlags.SDL_BUTTON_RMASK)) MouseUp?.Invoke(MouseButton.Right);
|
|
if (buttonsToRelease.HasFlagFast(SDL_MouseButtonFlags.SDL_BUTTON_X1MASK)) MouseUp?.Invoke(MouseButton.Button1);
|
|
if (buttonsToRelease.HasFlagFast(SDL_MouseButtonFlags.SDL_BUTTON_X2MASK)) MouseUp?.Invoke(MouseButton.Button2);
|
|
}
|
|
}
|
|
|
|
private SDL_PropertiesID? currentTextInputProperties;
|
|
|
|
public virtual void StartTextInput(TextInputProperties properties) => ScheduleCommand(() =>
|
|
{
|
|
currentTextInputProperties ??= SDL_CreateProperties().ThrowIfFailed();
|
|
|
|
var props = currentTextInputProperties.Value;
|
|
SDL_SetNumberProperty(props, SDL_PROP_TEXTINPUT_TYPE_NUMBER, (long)properties.Type.ToSDLTextInputType()).LogErrorIfFailed();
|
|
|
|
if (!properties.AutoCapitalisation)
|
|
SDL_SetNumberProperty(props, SDL_PROP_TEXTINPUT_CAPITALIZATION_NUMBER, (long)SDL_Capitalization.SDL_CAPITALIZE_NONE).LogErrorIfFailed();
|
|
else
|
|
SDL_ClearProperty(props, SDL_PROP_TEXTINPUT_CAPITALIZATION_NUMBER).LogErrorIfFailed();
|
|
|
|
if (properties.Type == TextInputType.Code)
|
|
SDL_SetBooleanProperty(props, SDL_PROP_TEXTINPUT_AUTOCORRECT_BOOLEAN, false).LogErrorIfFailed();
|
|
else
|
|
SDL_ClearProperty(props, SDL_PROP_TEXTINPUT_AUTOCORRECT_BOOLEAN).LogErrorIfFailed();
|
|
|
|
SDL_StartTextInputWithProperties(SDLWindowHandle, props).LogErrorIfFailed();
|
|
});
|
|
|
|
public void StopTextInput() => ScheduleCommand(() => SDL_StopTextInput(SDLWindowHandle).LogErrorIfFailed());
|
|
|
|
/// <summary>
|
|
/// Resets internal state of the platform-native IME.
|
|
/// This will clear its composition text and prepare it for new input.
|
|
/// </summary>
|
|
public virtual void ResetIme() => ScheduleCommand(() =>
|
|
{
|
|
SDL_StopTextInput(SDLWindowHandle).LogErrorIfFailed();
|
|
|
|
if (currentTextInputProperties is SDL_PropertiesID props)
|
|
SDL_StartTextInputWithProperties(SDLWindowHandle, props).LogErrorIfFailed();
|
|
else
|
|
SDL_StartTextInput(SDLWindowHandle).LogErrorIfFailed();
|
|
});
|
|
|
|
public void SetTextInputRect(RectangleF rect) => ScheduleCommand(() =>
|
|
{
|
|
// TODO: SDL3 allows apps to set cursor position through the third parameter of SDL_SetTextInputArea.
|
|
var sdlRect = ((RectangleI)(rect / Scale)).ToSDLRect();
|
|
SDL_SetTextInputArea(SDLWindowHandle, &sdlRect, 0).LogErrorIfFailed();
|
|
});
|
|
|
|
#region SDL Event Handling
|
|
|
|
private void handleDropEvent(SDL_DropEvent evtDrop)
|
|
{
|
|
switch (evtDrop.type)
|
|
{
|
|
case SDL_EventType.SDL_EVENT_DROP_FILE:
|
|
string? str = evtDrop.GetData();
|
|
if (str != null)
|
|
TriggerDragDrop(str);
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
private readonly SDL_FingerID?[] activeTouches = new SDL_FingerID?[TouchState.MAX_NATIVE_TOUCH_COUNT];
|
|
|
|
private TouchSource? getTouchSource(SDL_FingerID fingerId)
|
|
{
|
|
for (int i = 0; i < activeTouches.Length; i++)
|
|
{
|
|
if (fingerId == activeTouches[i])
|
|
return (TouchSource)i;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private TouchSource? assignNextAvailableTouchSource(SDL_FingerID fingerId)
|
|
{
|
|
for (int i = 0; i < activeTouches.Length; i++)
|
|
{
|
|
if (activeTouches[i] != null) continue;
|
|
|
|
activeTouches[i] = fingerId;
|
|
return (TouchSource)i;
|
|
}
|
|
|
|
// we only handle up to TouchState.MAX_NATIVE_TOUCH_COUNT. Ignore any further touches for now.
|
|
return null;
|
|
}
|
|
|
|
private void handleTouchFingerEvent(SDL_TouchFingerEvent evtTfinger)
|
|
{
|
|
var existingSource = getTouchSource(evtTfinger.fingerID);
|
|
|
|
if (evtTfinger.type == SDL_EventType.SDL_EVENT_FINGER_DOWN)
|
|
{
|
|
// TODO: remove when upstream fixes https://github.com/libsdl-org/SDL/issues/9591
|
|
// ignore SDL_EVENT_FINGER_DOWN for fingers that are already pressed
|
|
if (existingSource != null)
|
|
return;
|
|
|
|
Debug.Assert(existingSource == null);
|
|
existingSource = assignNextAvailableTouchSource(evtTfinger.fingerID);
|
|
}
|
|
|
|
if (existingSource == null)
|
|
return;
|
|
|
|
float x = evtTfinger.x * ClientSize.Width;
|
|
float y = evtTfinger.y * ClientSize.Height;
|
|
|
|
var touch = new Touch(existingSource.Value, new Vector2(x, y));
|
|
|
|
switch (evtTfinger.type)
|
|
{
|
|
case SDL_EventType.SDL_EVENT_FINGER_DOWN:
|
|
case SDL_EventType.SDL_EVENT_FINGER_MOTION:
|
|
TouchDown?.Invoke(touch);
|
|
break;
|
|
|
|
case SDL_EventType.SDL_EVENT_FINGER_UP:
|
|
case SDL_EventType.SDL_EVENT_FINGER_CANCELED:
|
|
TouchUp?.Invoke(touch);
|
|
activeTouches[(int)existingSource] = null;
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void handleControllerDeviceEvent(SDL_GamepadDeviceEvent evtCdevice)
|
|
{
|
|
switch (evtCdevice.type)
|
|
{
|
|
case SDL_EventType.SDL_EVENT_GAMEPAD_ADDED:
|
|
addJoystick(evtCdevice.which);
|
|
break;
|
|
|
|
case SDL_EventType.SDL_EVENT_GAMEPAD_REMOVED:
|
|
removeJoystick(evtCdevice.which);
|
|
break;
|
|
|
|
case SDL_EventType.SDL_EVENT_GAMEPAD_REMAPPED:
|
|
if (controllers.TryGetValue(evtCdevice.which, out var state))
|
|
state.PopulateBindings();
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void handleControllerButtonEvent(SDL_GamepadButtonEvent evtCbutton)
|
|
{
|
|
var button = evtCbutton.Button.ToJoystickButton();
|
|
|
|
switch (evtCbutton.type)
|
|
{
|
|
case SDL_EventType.SDL_EVENT_GAMEPAD_BUTTON_DOWN:
|
|
enqueueJoystickButtonInput(button, true);
|
|
break;
|
|
|
|
case SDL_EventType.SDL_EVENT_GAMEPAD_BUTTON_UP:
|
|
enqueueJoystickButtonInput(button, false);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void handleControllerAxisEvent(SDL_GamepadAxisEvent evtCaxis) =>
|
|
enqueueJoystickAxisInput(evtCaxis.Axis.ToJoystickAxisSource(), evtCaxis.value);
|
|
|
|
private void addJoystick(SDL_JoystickID instanceID)
|
|
{
|
|
// if the joystick is already opened, ignore it
|
|
if (controllers.ContainsKey(instanceID))
|
|
return;
|
|
|
|
SDL_Joystick* joystick = SDL3Extensions.LogErrorIfFailed(SDL_OpenJoystick(instanceID));
|
|
|
|
SDL_Gamepad* controller = null;
|
|
if (SDL_IsGamepad(instanceID))
|
|
controller = SDL3Extensions.LogErrorIfFailed(SDL_OpenGamepad(instanceID));
|
|
|
|
controllers[instanceID] = new SDL3ControllerBindings(joystick, controller);
|
|
}
|
|
|
|
private void removeJoystick(SDL_JoystickID instanceID)
|
|
{
|
|
if (controllers.Remove(instanceID, out var controller))
|
|
{
|
|
if (controller.GamepadHandle != null)
|
|
SDL_CloseGamepad(controller.GamepadHandle);
|
|
|
|
if (controller.JoystickHandle != null)
|
|
SDL_CloseJoystick(controller.JoystickHandle);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Populates <see cref="controllers"/> with joysticks that are already connected.
|
|
/// </summary>
|
|
private void populateJoysticks()
|
|
{
|
|
using var joysticks = SDL_GetJoysticks().LogErrorIfFailed();
|
|
|
|
if (joysticks == null)
|
|
return;
|
|
|
|
for (int i = 0; i < joysticks.Count; i++)
|
|
{
|
|
addJoystick(joysticks[i]);
|
|
}
|
|
}
|
|
|
|
private void handleJoyDeviceEvent(SDL_JoyDeviceEvent evtJdevice)
|
|
{
|
|
switch (evtJdevice.type)
|
|
{
|
|
case SDL_EventType.SDL_EVENT_JOYSTICK_ADDED:
|
|
addJoystick(evtJdevice.which);
|
|
break;
|
|
|
|
case SDL_EventType.SDL_EVENT_JOYSTICK_REMOVED:
|
|
removeJoystick(evtJdevice.which);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void handleJoyButtonEvent(SDL_JoyButtonEvent evtJbutton)
|
|
{
|
|
// if this button exists in the controller bindings, skip it
|
|
if (controllers.TryGetValue(evtJbutton.which, out var state) && state.IsJoystickButtonBound(evtJbutton.button))
|
|
return;
|
|
|
|
var button = JoystickButton.FirstButton + evtJbutton.button;
|
|
|
|
switch (evtJbutton.type)
|
|
{
|
|
case SDL_EventType.SDL_EVENT_JOYSTICK_BUTTON_DOWN:
|
|
enqueueJoystickButtonInput(button, true);
|
|
break;
|
|
|
|
case SDL_EventType.SDL_EVENT_JOYSTICK_BUTTON_UP:
|
|
enqueueJoystickButtonInput(button, false);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// ReSharper disable once UnusedParameter.Local
|
|
private void handleJoyHatEvent(SDL_JoyHatEvent evtJhat)
|
|
{
|
|
}
|
|
|
|
// ReSharper disable once UnusedParameter.Local
|
|
private void handleJoyBallEvent(SDL_JoyBallEvent evtJball)
|
|
{
|
|
}
|
|
|
|
private void handleJoyAxisEvent(SDL_JoyAxisEvent evtJaxis)
|
|
{
|
|
// if this axis exists in the controller bindings, skip it
|
|
if (controllers.TryGetValue(evtJaxis.which, out var state) && state.IsJoystickAxisBound(evtJaxis.axis))
|
|
return;
|
|
|
|
enqueueJoystickAxisInput(JoystickAxisSource.Axis1 + evtJaxis.axis, evtJaxis.axis);
|
|
}
|
|
|
|
private ulong lastPreciseScroll;
|
|
private const uint precise_scroll_debounce = 100;
|
|
|
|
private void handleMouseWheelEvent(SDL_MouseWheelEvent evtWheel)
|
|
{
|
|
bool isPrecise(float f) => f % 1 != 0;
|
|
|
|
bool precise;
|
|
|
|
if (isPrecise(evtWheel.x) || isPrecise(evtWheel.y))
|
|
{
|
|
precise = true;
|
|
lastPreciseScroll = evtWheel.timestamp;
|
|
}
|
|
else
|
|
{
|
|
precise = evtWheel.timestamp < lastPreciseScroll + precise_scroll_debounce;
|
|
}
|
|
|
|
// SDL reports horizontal scroll opposite of what framework expects (in non-"natural" mode, scrolling to the right gives positive deltas while we want negative).
|
|
TriggerMouseWheel(new Vector2(-evtWheel.x, evtWheel.y), precise);
|
|
}
|
|
|
|
private void handleMouseButtonEvent(SDL_MouseButtonEvent evtButton)
|
|
{
|
|
MouseButton button = mouseButtonFromEvent(evtButton.Button);
|
|
SDL_MouseButtonFlags mask = SDL_BUTTON(evtButton.Button);
|
|
Debug.Assert(Enum.IsDefined(mask));
|
|
|
|
switch (evtButton.type)
|
|
{
|
|
case SDL_EventType.SDL_EVENT_MOUSE_BUTTON_DOWN:
|
|
MouseDown?.Invoke(button);
|
|
Interlocked.Or(ref pressedButtons, (uint)mask);
|
|
break;
|
|
|
|
case SDL_EventType.SDL_EVENT_MOUSE_BUTTON_UP:
|
|
Interlocked.And(ref pressedButtons, (uint)~mask);
|
|
MouseUp?.Invoke(button);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void handleMouseMotionEvent(SDL_MouseMotionEvent evtMotion)
|
|
{
|
|
if (!SDL_GetWindowRelativeMouseMode(SDLWindowHandle))
|
|
MouseMove?.Invoke(new Vector2(evtMotion.x * Scale, evtMotion.y * Scale));
|
|
else
|
|
MouseMoveRelative?.Invoke(new Vector2(evtMotion.xrel * Scale, evtMotion.yrel * Scale));
|
|
}
|
|
|
|
private void handleTextInputEvent(SDL_TextInputEvent evtText)
|
|
{
|
|
string? text = evtText.GetText();
|
|
Debug.Assert(text != null);
|
|
TextInput?.Invoke(text);
|
|
}
|
|
|
|
private void handleTextEditingEvent(SDL_TextEditingEvent evtEdit)
|
|
{
|
|
string? text = evtEdit.GetText();
|
|
Debug.Assert(text != null);
|
|
TextEditing?.Invoke(text, evtEdit.start, evtEdit.length);
|
|
}
|
|
|
|
private void handleKeyboardEvent(SDL_KeyboardEvent evtKey)
|
|
{
|
|
Key key = evtKey.ToKey();
|
|
|
|
if (key == Key.Unknown)
|
|
{
|
|
Logger.Log($"Unknown SDL key: {evtKey.scancode}, {evtKey.key}");
|
|
return;
|
|
}
|
|
|
|
switch (evtKey.type)
|
|
{
|
|
case SDL_EventType.SDL_EVENT_KEY_DOWN:
|
|
KeyDown?.Invoke(key);
|
|
break;
|
|
|
|
case SDL_EventType.SDL_EVENT_KEY_UP:
|
|
KeyUp?.Invoke(key);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void handleKeymapChangedEvent() => KeymapChanged?.Invoke();
|
|
|
|
private readonly bool penProximityWorkaround = RuntimeInfo.OS == RuntimeInfo.Platform.Android;
|
|
|
|
private bool tryGetPenDeviceType(SDL_PenID penID, out TabletPenDeviceType deviceType)
|
|
{
|
|
if (penDeviceTypes.TryGetValue(penID, out deviceType))
|
|
return true;
|
|
|
|
// Workaround for Android: after clicking with a pen, it can send motion and touch events even though we've received SDL_EVENT_PEN_PROXIMITY_OUT.
|
|
// It's either a bug in Android or SDL.
|
|
// Instead of ignoring those events, fetch the type and store it.
|
|
if (penProximityWorkaround)
|
|
{
|
|
var sdlType = SDL_GetPenDeviceType(penID);
|
|
|
|
if (sdlType == SDL_PenDeviceType.SDL_PEN_DEVICE_TYPE_INVALID)
|
|
return false;
|
|
|
|
deviceType = sdlType.ToTabletPenDeviceType();
|
|
penDeviceTypes[penID] = deviceType;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void handlePenProximityEvent(SDL_PenProximityEvent evtPenProximity)
|
|
{
|
|
if (evtPenProximity.type == SDL_EventType.SDL_EVENT_PEN_PROXIMITY_IN)
|
|
{
|
|
if (!penProximityWorkaround && penDeviceTypes.ContainsKey(evtPenProximity.which))
|
|
Logger.Log($"Unexpected SDL_EVENT_PEN_PROXIMITY_IN for pen id={evtPenProximity.which}. Pen already in proximity.", level: LogLevel.Important);
|
|
|
|
penDeviceTypes[evtPenProximity.which] = SDL_GetPenDeviceType(evtPenProximity.which).ThrowIfFailed().ToTabletPenDeviceType();
|
|
}
|
|
else
|
|
{
|
|
if (!penDeviceTypes.Remove(evtPenProximity.which))
|
|
Logger.Log($"Unexpected SDL_EVENT_PEN_PROXIMITY_OUT for pen id={evtPenProximity.which}. Pen not in proximity.", level: LogLevel.Important);
|
|
}
|
|
}
|
|
|
|
private void handlePenMotionEvent(SDL_PenMotionEvent evtPenMotion)
|
|
{
|
|
if (tryGetPenDeviceType(evtPenMotion.which, out var type))
|
|
PenMove?.Invoke(type, new Vector2(evtPenMotion.x, evtPenMotion.y) * Scale, evtPenMotion.pen_state.HasFlagFast(SDL_PenInputFlags.SDL_PEN_INPUT_DOWN));
|
|
else
|
|
Logger.Log($"Unexpected SDL_EVENT_PEN_MOTION for pen id={evtPenMotion.which}. Pen not in proximity.", level: LogLevel.Important);
|
|
}
|
|
|
|
private void handlePenTouchEvent(SDL_PenTouchEvent evtPenTouch)
|
|
{
|
|
if (tryGetPenDeviceType(evtPenTouch.which, out var type))
|
|
PenTouch?.Invoke(type, evtPenTouch.down, new Vector2(evtPenTouch.x, evtPenTouch.y) * Scale);
|
|
else
|
|
Logger.Log($"Unexpected {evtPenTouch.type} for pen id={evtPenTouch.which}. Pen not in proximity.", level: LogLevel.Important);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The first SDL pen button as defined in https://wiki.libsdl.org/SDL3/SDL_PenButtonEvent.
|
|
/// </summary>
|
|
private const byte first_pen_button = 1;
|
|
|
|
private void handlePenButtonEvent(SDL_PenButtonEvent evtPenButton)
|
|
{
|
|
var button = (TabletPenButton)(evtPenButton.button - first_pen_button);
|
|
|
|
if (button >= TabletPenButton.Primary && button <= TabletPenButton.Button8)
|
|
PenButton?.Invoke(button, evtPenButton.down);
|
|
else
|
|
Logger.Log($"Dropping SDL_PenButtonEvent with button index={evtPenButton.button} (out of range).");
|
|
}
|
|
|
|
private MouseButton mouseButtonFromEvent(SDLButton button)
|
|
{
|
|
switch (button)
|
|
{
|
|
case SDLButton.SDL_BUTTON_LEFT:
|
|
return MouseButton.Left;
|
|
|
|
case SDLButton.SDL_BUTTON_RIGHT:
|
|
return MouseButton.Right;
|
|
|
|
case SDLButton.SDL_BUTTON_MIDDLE:
|
|
return MouseButton.Middle;
|
|
|
|
case SDLButton.SDL_BUTTON_X1:
|
|
return MouseButton.Button1;
|
|
|
|
case SDLButton.SDL_BUTTON_X2:
|
|
return MouseButton.Button2;
|
|
|
|
default:
|
|
Logger.Log($"unknown mouse button: {button}, defaulting to left button");
|
|
return MouseButton.Left;
|
|
}
|
|
}
|
|
|
|
private void mouseButtonFromPen(bool pressed, byte penButton, out MouseButton button, out SDL_MouseButtonFlags buttonFlag)
|
|
{
|
|
switch (penButton)
|
|
{
|
|
case 0:
|
|
button = MouseButton.Left;
|
|
buttonFlag = SDL_MouseButtonFlags.SDL_BUTTON_LMASK;
|
|
break;
|
|
|
|
case 1:
|
|
button = MouseButton.Right;
|
|
buttonFlag = SDL_MouseButtonFlags.SDL_BUTTON_RMASK;
|
|
break;
|
|
|
|
case 2:
|
|
button = MouseButton.Middle;
|
|
buttonFlag = SDL_MouseButtonFlags.SDL_BUTTON_MMASK;
|
|
break;
|
|
|
|
case 3:
|
|
button = MouseButton.Button1;
|
|
buttonFlag = SDL_MouseButtonFlags.SDL_BUTTON_X1MASK;
|
|
break;
|
|
|
|
case 4:
|
|
button = MouseButton.Button2;
|
|
buttonFlag = SDL_MouseButtonFlags.SDL_BUTTON_X2MASK;
|
|
break;
|
|
|
|
default:
|
|
Logger.Log($"unknown pen button index: {penButton}, ignoring...");
|
|
button = MouseButton.Button3;
|
|
buttonFlag = 0;
|
|
break;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Update the host window manager's cursor position based on a location relative to window coordinates.
|
|
/// </summary>
|
|
/// <param name="mousePosition">A position inside the window.</param>
|
|
public void UpdateMousePosition(Vector2 mousePosition) => ScheduleCommand(() => SDL_WarpMouseInWindow(SDLWindowHandle, mousePosition.X / Scale, mousePosition.Y / Scale));
|
|
|
|
private void updateConfineMode()
|
|
{
|
|
bool confine = false;
|
|
|
|
switch (ConfineMouseMode.Value)
|
|
{
|
|
case Input.ConfineMouseMode.Fullscreen:
|
|
confine = WindowMode.Value != Configuration.WindowMode.Windowed;
|
|
break;
|
|
|
|
case Input.ConfineMouseMode.Always:
|
|
confine = true;
|
|
break;
|
|
}
|
|
|
|
if (confine)
|
|
CursorStateBindable.Value |= CursorState.Confined;
|
|
else
|
|
CursorStateBindable.Value &= ~CursorState.Confined;
|
|
}
|
|
|
|
#region Events
|
|
|
|
/// <summary>
|
|
/// Invoked when the mouse cursor enters the window.
|
|
/// </summary>
|
|
public event Action? MouseEntered;
|
|
|
|
/// <summary>
|
|
/// Invoked when the mouse cursor leaves the window.
|
|
/// </summary>
|
|
public event Action? MouseLeft;
|
|
|
|
/// <summary>
|
|
/// Invoked when the user scrolls the mouse wheel over the window.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Delta is positive when mouse wheel scrolled to the up or left, in non-"natural" scroll mode (ie. the classic way).
|
|
/// </remarks>
|
|
public event Action<Vector2, bool>? MouseWheel;
|
|
|
|
protected void TriggerMouseWheel(Vector2 delta, bool precise) => MouseWheel?.Invoke(delta, precise);
|
|
|
|
/// <summary>
|
|
/// Invoked when the user moves the mouse cursor within the window.
|
|
/// </summary>
|
|
public event Action<Vector2>? MouseMove;
|
|
|
|
protected void TriggerMouseMove(float x, float y) => MouseMove?.Invoke(new Vector2(x, y));
|
|
|
|
/// <summary>
|
|
/// Invoked when the user moves the mouse cursor within the window (via relative / raw input).
|
|
/// </summary>
|
|
public event Action<Vector2>? MouseMoveRelative;
|
|
|
|
/// <summary>
|
|
/// Invoked when the user presses a mouse button.
|
|
/// </summary>
|
|
public event Action<MouseButton>? MouseDown;
|
|
|
|
protected void TriggerMouseDown(MouseButton button) => MouseDown?.Invoke(button);
|
|
|
|
/// <summary>
|
|
/// Invoked when the user releases a mouse button.
|
|
/// </summary>
|
|
public event Action<MouseButton>? MouseUp;
|
|
|
|
protected void TriggerMouseUp(MouseButton button) => MouseUp?.Invoke(button);
|
|
|
|
/// <summary>
|
|
/// Invoked when the user presses a key.
|
|
/// </summary>
|
|
public event Action<Key>? KeyDown;
|
|
|
|
/// <summary>
|
|
/// Invoked when the user releases a key.
|
|
/// </summary>
|
|
public event Action<Key>? KeyUp;
|
|
|
|
/// <summary>
|
|
/// Invoked when the user enters text.
|
|
/// </summary>
|
|
public event Action<string>? TextInput;
|
|
|
|
/// <summary>
|
|
/// Invoked when an IME text editing event occurs.
|
|
/// </summary>
|
|
public event TextEditingDelegate? TextEditing;
|
|
|
|
/// <inheritdoc cref="IWindow.KeymapChanged"/>
|
|
public event Action? KeymapChanged;
|
|
|
|
/// <summary>
|
|
/// Invoked when a joystick axis changes.
|
|
/// </summary>
|
|
public event Action<JoystickAxisSource, float>? JoystickAxisChanged;
|
|
|
|
/// <summary>
|
|
/// Invoked when the user presses a button on a joystick.
|
|
/// </summary>
|
|
public event Action<JoystickButton>? JoystickButtonDown;
|
|
|
|
/// <summary>
|
|
/// Invoked when the user releases a button on a joystick.
|
|
/// </summary>
|
|
public event Action<JoystickButton>? JoystickButtonUp;
|
|
|
|
/// <summary>
|
|
/// Invoked when a finger moves or touches a touchscreen.
|
|
/// </summary>
|
|
public event Action<Touch>? TouchDown;
|
|
|
|
/// <summary>
|
|
/// Invoked when a finger leaves the touchscreen.
|
|
/// </summary>
|
|
public event Action<Touch>? TouchUp;
|
|
|
|
/// <summary>
|
|
/// Invoked when a pen moves. Passes pen position and whether the pen is touching the tablet surface.
|
|
/// </summary>
|
|
public event Action<TabletPenDeviceType, Vector2, bool>? PenMove;
|
|
|
|
/// <summary>
|
|
/// Invoked when a pen touches (<c>true</c>) or lifts (<c>false</c>) from the tablet surface.
|
|
/// Also passes the current position of the pen.
|
|
/// </summary>
|
|
public event Action<TabletPenDeviceType, bool, Vector2>? PenTouch;
|
|
|
|
/// <summary>
|
|
/// Invoked when a <see cref="TabletPenButton">pen button</see> is pressed (<c>true</c>) or released (<c>false</c>).
|
|
/// </summary>
|
|
public event Action<TabletPenButton, bool>? PenButton;
|
|
|
|
#endregion
|
|
}
|
|
}
|