refactor(input): own pointer and callback lifetime

Move camera pointer, framebuffer resize, and retained/devtools input edges behind focused reversible owners. Preserve input priority while making shutdown deactivate callbacks before live-session retirement and retry physical detach without stranding transport teardown.
This commit is contained in:
Erik 2026-07-22 11:59:33 +02:00
parent d09e246d3a
commit 8b8afeefa3
42 changed files with 4029 additions and 461 deletions

View file

@ -0,0 +1,449 @@
using System.Numerics;
using AcDream.App.Rendering;
using AcDream.Core.Rendering;
using AcDream.UI.Abstractions.Input;
using Silk.NET.Input;
namespace AcDream.App.Input;
/// <summary>
/// Narrow raw-pointer surface. The production adapter keeps the Silk event
/// accessor out of the camera policy and lets failure tests exercise an event
/// add/remove without implementing Silk's complete mouse contract.
/// </summary>
internal interface IRawPointerSurface
{
void AddMouseMove(Action<Vector2> callback);
void RemoveMouseMove(Action<Vector2> callback);
}
internal sealed class SilkRawPointerSurface : IRawPointerSurface
{
private readonly IMouse _mouse;
private Action<Vector2>? _callback;
private readonly Action<IMouse, Vector2> _mouseMove;
public SilkRawPointerSurface(IMouse mouse)
{
_mouse = mouse ?? throw new ArgumentNullException(nameof(mouse));
_mouseMove = OnMouseMove;
}
public void AddMouseMove(Action<Vector2> callback)
{
ArgumentNullException.ThrowIfNull(callback);
if (_callback is not null)
throw new InvalidOperationException("The raw mouse surface is already attached.");
// Publish before invoking the custom event accessor: an accessor may
// perform its side effect and then throw, in which case rollback still
// has the exact delegate needed to remove it.
_callback = callback;
_mouse.MouseMove += _mouseMove;
}
public void RemoveMouseMove(Action<Vector2> callback)
{
ArgumentNullException.ThrowIfNull(callback);
if (!ReferenceEquals(_callback, callback))
return;
_mouse.MouseMove -= _mouseMove;
_callback = null;
}
private void OnMouseMove(IMouse _, Vector2 position) =>
_callback?.Invoke(position);
}
internal interface IPointerCursorModeTarget
{
CursorMode CursorMode { get; set; }
}
internal sealed class SilkPointerCursorModeTarget(IMouse mouse)
: IPointerCursorModeTarget
{
private readonly IMouse _mouse = mouse
?? throw new ArgumentNullException(nameof(mouse));
public CursorMode CursorMode
{
get => _mouse.Cursor.CursorMode;
set => _mouse.Cursor.CursorMode = value;
}
}
/// <summary>
/// Owns raw pointer subscriptions and the camera-facing pointer policy that
/// previously lived in GameWindow. Physical event removal is retriable; logical
/// deactivation is immediate, so a copied or stubborn Silk delegate cannot
/// re-enter gameplay while shutdown is closing the live session.
/// </summary>
internal sealed class CameraPointerInputController : IDisposable
{
private readonly IReadOnlyList<IRawPointerSurface> _surfaces;
private readonly IPointerCursorModeTarget _cursor;
private readonly HostQuiescenceGate _quiescence;
private readonly IInputCaptureSource _capture;
private readonly LocalPlayerModeState _playerMode;
private readonly CameraController _camera;
private readonly ChaseCameraInputState _chase;
private readonly IMouseSource _mouse;
private readonly PointerPositionState _pointer;
private readonly IInputMonotonicClock _clock;
private readonly Action<Vector2> _mouseMoved;
private readonly Action<bool> _cameraModeChanged;
private readonly bool[] _surfaceAttached;
private ResourceShutdownTransaction? _detach;
private GameplayInputFrameController? _gameplayFrame;
private bool _cameraAttached;
private bool _attachStarted;
private int _disposeRequested;
private int _active;
private float _flySensitivity = 1f;
private float _orbitSensitivity = 1f;
public CameraPointerInputController(
IReadOnlyList<IRawPointerSurface> surfaces,
IPointerCursorModeTarget cursor,
HostQuiescenceGate quiescence,
IInputCaptureSource capture,
LocalPlayerModeState playerMode,
CameraController camera,
ChaseCameraInputState chase,
IMouseSource mouse,
PointerPositionState pointer,
IInputMonotonicClock clock)
{
_surfaces = surfaces ?? throw new ArgumentNullException(nameof(surfaces));
if (_surfaces.Any(static surface => surface is null))
throw new ArgumentException("Raw pointer surfaces cannot contain null.", nameof(surfaces));
_cursor = cursor ?? throw new ArgumentNullException(nameof(cursor));
_quiescence = quiescence ?? throw new ArgumentNullException(nameof(quiescence));
_capture = capture ?? throw new ArgumentNullException(nameof(capture));
_playerMode = playerMode ?? throw new ArgumentNullException(nameof(playerMode));
_camera = camera ?? throw new ArgumentNullException(nameof(camera));
_chase = chase ?? throw new ArgumentNullException(nameof(chase));
_mouse = mouse ?? throw new ArgumentNullException(nameof(mouse));
_pointer = pointer ?? throw new ArgumentNullException(nameof(pointer));
_clock = clock ?? throw new ArgumentNullException(nameof(clock));
_surfaceAttached = new bool[_surfaces.Count];
_mouseMoved = OnMouseMoved;
_cameraModeChanged = OnCameraModeChanged;
}
public static CameraPointerInputController Create(
IReadOnlyList<IMouse> mice,
HostQuiescenceGate quiescence,
IInputCaptureSource capture,
LocalPlayerModeState playerMode,
CameraController camera,
ChaseCameraInputState chase,
IMouseSource mouse,
PointerPositionState pointer,
IInputMonotonicClock clock)
{
ArgumentNullException.ThrowIfNull(mice);
if (mice.Count == 0)
throw new ArgumentException("At least one mouse is required.", nameof(mice));
return new CameraPointerInputController(
mice.Select(static candidate =>
(IRawPointerSurface)new SilkRawPointerSurface(candidate)).ToArray(),
new SilkPointerCursorModeTarget(mice[0]),
quiescence,
capture,
playerMode,
camera,
chase,
mouse,
pointer,
clock);
}
public bool IsDisposalComplete =>
!_cameraAttached && _surfaceAttached.All(static attached => !attached);
public float ActiveSensitivity
{
get
{
if (_playerMode.IsPlayerMode && _camera.IsChaseMode)
return _chase.Sensitivity;
if (_camera.IsFlyMode)
return _flySensitivity;
return _orbitSensitivity;
}
}
public bool RmbOrbitHeld => _chase.RmbOrbitHeld;
public void AttachRaw()
{
ObjectDisposedException.ThrowIf(
Volatile.Read(ref _disposeRequested) != 0,
this);
if (_attachStarted)
throw new InvalidOperationException("Raw pointer attachment has already started.");
_attachStarted = true;
try
{
for (int i = 0; i < _surfaces.Count; i++)
{
_surfaceAttached[i] = true;
_surfaces[i].AddMouseMove(_mouseMoved);
}
_cameraAttached = true;
_camera.ModeChanged += _cameraModeChanged;
Volatile.Write(ref _active, 1);
}
catch (Exception attachError)
{
Deactivate();
try
{
EnsureDetachTransaction().CompleteOrThrow();
}
catch (Exception rollbackError)
{
throw new AggregateException(
"Raw pointer registration and rollback both failed.",
new InvalidOperationException(
"Raw pointer registration failed.", attachError),
rollbackError);
}
throw new InvalidOperationException(
"Raw pointer registration failed and was rolled back.",
attachError);
}
}
/// <summary>
/// Completes the deliberate two-phase composition edge. Binding the frame
/// owner never resubscribes or changes event priority.
/// </summary>
public void BindGameplayFrame(GameplayInputFrameController gameplayFrame)
{
ArgumentNullException.ThrowIfNull(gameplayFrame);
if (_gameplayFrame is not null
&& !ReferenceEquals(_gameplayFrame, gameplayFrame))
{
throw new InvalidOperationException(
"The raw pointer owner is already bound to another gameplay frame.");
}
_gameplayFrame = gameplayFrame;
}
public void UnbindGameplayFrame(GameplayInputFrameController gameplayFrame)
{
ArgumentNullException.ThrowIfNull(gameplayFrame);
if (ReferenceEquals(_gameplayFrame, gameplayFrame))
_gameplayFrame = null;
}
public void HandleFocusChanged(bool focused)
{
if (!focused)
_gameplayFrame?.EndMouseLook();
}
public void HandleScroll(InputAction action)
{
float direction = action switch
{
InputAction.ScrollUp => 1f,
InputAction.ScrollDown => -1f,
_ => throw new ArgumentOutOfRangeException(nameof(action)),
};
if (_playerMode.IsPlayerMode && _camera.IsChaseMode)
{
if (CameraDiagnostics.UseRetailChaseCamera && _chase.Retail is not null)
_chase.Retail.AdjustDistance(-direction * 0.8f);
else
_chase.Legacy?.AdjustDistance(-direction * 0.8f);
}
else if (!_camera.IsFlyMode)
{
_camera.Orbit.Distance = Math.Clamp(
_camera.Orbit.Distance - direction * 20f,
50f,
2000f);
}
}
public string AdjustSensitivity(float factor)
{
string mode;
float current;
if (_playerMode.IsPlayerMode && _camera.IsChaseMode)
{
mode = "Chase";
current = _chase.Sensitivity;
}
else if (_camera.IsFlyMode)
{
mode = "Fly";
current = _flySensitivity;
}
else
{
mode = "Orbit";
current = _orbitSensitivity;
}
float next = MathF.Min(3f, MathF.Max(0.005f, current * factor));
if (mode == "Chase")
_chase.Sensitivity = next;
else if (mode == "Fly")
_flySensitivity = next;
else
_orbitSensitivity = next;
return $"{mode} sens {next:F3}x";
}
/// <summary>
/// Immediate callback cutoff used before the live-session close. The
/// gameplay mouse-look transition is retained until the session has
/// retired so it cannot publish one last movement packet.
/// </summary>
public void Deactivate()
{
Interlocked.Exchange(ref _active, 0);
_chase.RmbOrbitHeld = false;
}
/// <summary>Release presentation state only after live-session retirement.</summary>
public void ReleaseMouseLookAfterSessionRetirement() =>
_gameplayFrame?.EndMouseLook();
public void Dispose()
{
Interlocked.Exchange(ref _disposeRequested, 1);
Deactivate();
EnsureDetachTransaction().CompleteOrThrow();
}
private void OnMouseMoved(Vector2 position) =>
_quiescence.Invoke(() => ProcessMouseMove(position));
private void ProcessMouseMove(Vector2 position)
{
if (Volatile.Read(ref _active) == 0)
return;
if (_capture.WantCaptureMouse)
{
_pointer.X = position.X;
_pointer.Y = position.Y;
return;
}
float dx = position.X - _pointer.X;
float dy = position.Y - _pointer.Y;
if (_playerMode.IsPlayerMode && _camera.IsChaseMode
&& _chase.Legacy is not null)
{
float sensitivity = _chase.Sensitivity;
if (_gameplayFrame?.MouseLookActive == true)
{
_gameplayFrame.QueueRawMouseDelta(dx, dy);
}
else if (_chase.RmbOrbitHeld)
{
if (CameraDiagnostics.UseRetailChaseCamera
&& _chase.Retail is not null)
{
var (filteredDx, filteredDy) = _chase.Retail.FilterMouseDelta(
rawX: dx,
rawY: dy,
weight: 0.5f,
nowSec: _clock.NowSeconds);
_chase.Retail.YawOffset -= filteredDx * 0.004f * sensitivity;
_chase.Retail.AdjustPitch(filteredDy * 0.003f * sensitivity);
}
else
{
_chase.Legacy.YawOffset -= dx * 0.004f * sensitivity;
_chase.Legacy.AdjustPitch(dy * 0.003f * sensitivity);
}
}
}
else if (_camera.IsFlyMode)
{
_camera.Fly.Look(dx * _flySensitivity, dy * _flySensitivity);
}
else if (_mouse.IsHeld(MouseButton.Left))
{
_camera.Orbit.Yaw -= dx * 0.005f * _orbitSensitivity;
_camera.Orbit.Pitch = Math.Clamp(
_camera.Orbit.Pitch + dy * 0.005f * _orbitSensitivity,
0.1f,
1.5f);
}
_pointer.X = position.X;
_pointer.Y = position.Y;
}
private void OnCameraModeChanged(bool _) =>
_quiescence.Invoke(ApplyCursorForCameraMode);
private void ApplyCursorForCameraMode()
{
if (Volatile.Read(ref _active) == 0)
return;
if (_gameplayFrame?.MouseLookActive == true && !_camera.IsChaseMode)
_gameplayFrame.EndMouseLook();
_cursor.CursorMode = _camera.IsFlyMode
? CursorMode.Raw
: CursorMode.Normal;
}
private ResourceShutdownTransaction EnsureDetachTransaction()
{
if (_detach is not null)
return _detach;
var operations = new List<ResourceShutdownOperation>
{
new("camera mode", RemoveCameraMode),
};
for (int i = _surfaces.Count - 1; i >= 0; i--)
{
int index = i;
operations.Add(new ResourceShutdownOperation(
$"raw mouse {index}",
() => RemoveSurface(index)));
}
_detach = new ResourceShutdownTransaction(
new ResourceShutdownStage("raw pointer callbacks", operations.ToArray()));
return _detach;
}
private void RemoveCameraMode()
{
if (!_cameraAttached)
return;
_camera.ModeChanged -= _cameraModeChanged;
_cameraAttached = false;
}
private void RemoveSurface(int index)
{
if (!_surfaceAttached[index])
return;
_surfaces[index].RemoveMouseMove(_mouseMoved);
_surfaceAttached[index] = false;
}
}

View file

@ -43,6 +43,13 @@ internal sealed class DispatcherCameraInputSource : ICameraFrameInputSource
_dispatcher = dispatcher;
}
public void Unbind(InputDispatcher dispatcher)
{
ArgumentNullException.ThrowIfNull(dispatcher);
if (ReferenceEquals(_dispatcher, dispatcher))
_dispatcher = null;
}
public FlyCameraInput CaptureFly()
{
if (_dispatcher is not { } dispatcher)

View file

@ -31,6 +31,13 @@ internal sealed class DispatcherMovementInputSource : IMovementInputSource
_dispatcher = dispatcher;
}
public void Unbind(InputDispatcher dispatcher)
{
ArgumentNullException.ThrowIfNull(dispatcher);
if (ReferenceEquals(_dispatcher, dispatcher))
_dispatcher = null;
}
public MovementInput Capture()
{
// Devtools owns the whole gameplay keyboard while active, including

View file

@ -52,23 +52,3 @@ internal sealed class CompositeInputCaptureSource : IInputCaptureSource
public bool DevToolsWantCaptureKeyboard =>
_devTools.WantCaptureKeyboard;
}
internal sealed class DelegateInputCaptureSource : IInputCaptureSource
{
private readonly Func<bool> _wantCaptureMouse;
private readonly Func<bool> _wantCaptureKeyboard;
public DelegateInputCaptureSource(
Func<bool> wantCaptureMouse,
Func<bool> wantCaptureKeyboard)
{
_wantCaptureMouse = wantCaptureMouse
?? throw new ArgumentNullException(nameof(wantCaptureMouse));
_wantCaptureKeyboard = wantCaptureKeyboard
?? throw new ArgumentNullException(nameof(wantCaptureKeyboard));
}
public bool WantCaptureMouse => _wantCaptureMouse();
public bool WantCaptureKeyboard => _wantCaptureKeyboard();
public bool DevToolsWantCaptureKeyboard => false;
}

View file

@ -0,0 +1,486 @@
using System.Numerics;
using AcDream.App.Rendering;
using Silk.NET.Input;
namespace AcDream.App.Input;
/// <summary>
/// Non-owning input-context view for third-party frontends such as Silk's
/// ImGuiController. It interposes named device relays so frontend callbacks can
/// be silenced before a long live-session close and physically detached later,
/// without disposing the canonical Silk input context.
/// </summary>
internal sealed class QuiescentInputContext : IInputContext
{
private readonly IInputContext _inner;
private readonly HostQuiescenceGate _quiescence;
private readonly QuiescentKeyboard[] _keyboards;
private readonly QuiescentMouse[] _mice;
private readonly Action<IInputDevice, bool> _connectionChanged;
private Action<IInputDevice, bool>? _connectionSubscribers;
private ResourceShutdownTransaction? _detach;
private bool _connectionAttached;
private int _active;
private bool _activationStarted;
private int _disposeRequested;
public QuiescentInputContext(
IInputContext inner,
HostQuiescenceGate quiescence)
{
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
_quiescence = quiescence ?? throw new ArgumentNullException(nameof(quiescence));
_keyboards = inner.Keyboards
.Select(keyboard => new QuiescentKeyboard(keyboard, quiescence))
.ToArray();
_mice = inner.Mice
.Select(mouse => new QuiescentMouse(mouse, quiescence))
.ToArray();
_connectionChanged = OnConnectionChanged;
}
public nint Handle => _inner.Handle;
public IReadOnlyList<IGamepad> Gamepads => _inner.Gamepads;
public IReadOnlyList<IJoystick> Joysticks => _inner.Joysticks;
public IReadOnlyList<IKeyboard> Keyboards => _keyboards;
public IReadOnlyList<IMouse> Mice => _mice;
public IReadOnlyList<IInputDevice> OtherDevices => _inner.OtherDevices;
public event Action<IInputDevice, bool>? ConnectionChanged
{
add
{
if (value is null) return;
ThrowIfDisposed();
_connectionSubscribers += value;
if (_connectionAttached) return;
_connectionAttached = true;
_inner.ConnectionChanged += _connectionChanged;
}
remove
{
if (value is null) return;
_connectionSubscribers -= value;
if (_connectionSubscribers is not null || !_connectionAttached) return;
_inner.ConnectionChanged -= _connectionChanged;
_connectionAttached = false;
}
}
public bool IsDisposalComplete =>
!_connectionAttached
&& _keyboards.All(static keyboard => keyboard.IsDisposalComplete)
&& _mice.All(static mouse => mouse.IsDisposalComplete);
/// <summary>
/// Opens delivery only after the third-party frontend constructor has
/// finished installing every callback. An event raised reentrantly from an
/// add accessor therefore cannot enter a partially constructed frontend.
/// </summary>
public void Activate()
{
ThrowIfDisposed();
if (_activationStarted)
throw new InvalidOperationException(
"Frontend input activation has already been attempted.");
_activationStarted = true;
foreach (QuiescentKeyboard keyboard in _keyboards)
keyboard.Activate();
foreach (QuiescentMouse mouse in _mice)
mouse.Activate();
Volatile.Write(ref _active, 1);
}
public void Deactivate()
{
Interlocked.Exchange(ref _active, 0);
foreach (QuiescentKeyboard keyboard in _keyboards)
keyboard.Deactivate();
foreach (QuiescentMouse mouse in _mice)
mouse.Deactivate();
}
public void Dispose()
{
Interlocked.Exchange(ref _disposeRequested, 1);
foreach (QuiescentKeyboard keyboard in _keyboards)
keyboard.RequestDisposal();
foreach (QuiescentMouse mouse in _mice)
mouse.RequestDisposal();
Deactivate();
EnsureDetachTransaction().CompleteOrThrow();
}
private void ThrowIfDisposed() =>
ObjectDisposedException.ThrowIf(
Volatile.Read(ref _disposeRequested) != 0,
this);
private void OnConnectionChanged(IInputDevice device, bool connected) =>
_quiescence.Invoke(() =>
{
if (Volatile.Read(ref _active) != 0)
_connectionSubscribers?.Invoke(device, connected);
});
private ResourceShutdownTransaction EnsureDetachTransaction()
{
if (_detach is not null)
return _detach;
var operations = new List<ResourceShutdownOperation>();
for (int i = _mice.Length - 1; i >= 0; i--)
{
int index = i;
operations.Add(new ResourceShutdownOperation(
$"frontend mouse {index}",
_mice[index].Dispose));
}
for (int i = _keyboards.Length - 1; i >= 0; i--)
{
int index = i;
operations.Add(new ResourceShutdownOperation(
$"frontend keyboard {index}",
_keyboards[index].Dispose));
}
operations.Add(new ResourceShutdownOperation(
"frontend connection event",
RemoveConnectionChanged));
_detach = new ResourceShutdownTransaction(
new ResourceShutdownStage("frontend input callbacks", operations.ToArray()));
return _detach;
}
private void RemoveConnectionChanged()
{
if (!_connectionAttached) return;
_inner.ConnectionChanged -= _connectionChanged;
_connectionAttached = false;
_connectionSubscribers = null;
}
private sealed class QuiescentKeyboard : IKeyboard, IDisposable
{
private readonly IKeyboard _inner;
private readonly HostQuiescenceGate _quiescence;
private readonly Action<IKeyboard, Key, int> _downRelay;
private readonly Action<IKeyboard, Key, int> _upRelay;
private readonly Action<IKeyboard, char> _charRelay;
private Action<IKeyboard, Key, int>? _downSubscribers;
private Action<IKeyboard, Key, int>? _upSubscribers;
private Action<IKeyboard, char>? _charSubscribers;
private readonly bool[] _attached = new bool[3];
private ResourceShutdownTransaction? _detach;
private int _disposeRequested;
private int _active;
public QuiescentKeyboard(IKeyboard inner, HostQuiescenceGate quiescence)
{
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
_quiescence = quiescence;
_downRelay = OnDown;
_upRelay = OnUp;
_charRelay = OnChar;
}
public string Name => _inner.Name;
public int Index => _inner.Index;
public bool IsConnected => _inner.IsConnected;
public IReadOnlyList<Key> SupportedKeys => _inner.SupportedKeys;
public string ClipboardText
{
get => _inner.ClipboardText;
set => _inner.ClipboardText = value;
}
public event Action<IKeyboard, Key, int>? KeyDown
{
add
{
if (value is null) return;
ThrowIfDisposed();
_downSubscribers += value;
if (_attached[0]) return;
_attached[0] = true;
_inner.KeyDown += _downRelay;
}
remove
{
if (value is null) return;
_downSubscribers -= value;
if (_downSubscribers is not null || !_attached[0]) return;
_inner.KeyDown -= _downRelay;
_attached[0] = false;
}
}
public event Action<IKeyboard, Key, int>? KeyUp
{
add
{
if (value is null) return;
ThrowIfDisposed();
_upSubscribers += value;
if (_attached[1]) return;
_attached[1] = true;
_inner.KeyUp += _upRelay;
}
remove
{
if (value is null) return;
_upSubscribers -= value;
if (_upSubscribers is not null || !_attached[1]) return;
_inner.KeyUp -= _upRelay;
_attached[1] = false;
}
}
public event Action<IKeyboard, char>? KeyChar
{
add
{
if (value is null) return;
ThrowIfDisposed();
_charSubscribers += value;
if (_attached[2]) return;
_attached[2] = true;
_inner.KeyChar += _charRelay;
}
remove
{
if (value is null) return;
_charSubscribers -= value;
if (_charSubscribers is not null || !_attached[2]) return;
_inner.KeyChar -= _charRelay;
_attached[2] = false;
}
}
public bool IsDisposalComplete => _attached.All(static value => !value);
public bool IsKeyPressed(Key key) => _inner.IsKeyPressed(key);
public bool IsScancodePressed(int scancode) => _inner.IsScancodePressed(scancode);
public void BeginInput() => _inner.BeginInput();
public void EndInput() => _inner.EndInput();
public void Activate()
{
ThrowIfDisposed();
Volatile.Write(ref _active, 1);
}
public void Deactivate() => Interlocked.Exchange(ref _active, 0);
public void RequestDisposal() =>
Interlocked.Exchange(ref _disposeRequested, 1);
public void Dispose()
{
RequestDisposal();
Deactivate();
_detach ??= new ResourceShutdownTransaction(
new ResourceShutdownStage("frontend keyboard callbacks",
[
new("character", () => Remove(2)),
new("up", () => Remove(1)),
new("down", () => Remove(0)),
]));
_detach.CompleteOrThrow();
}
private void ThrowIfDisposed() =>
ObjectDisposedException.ThrowIf(
Volatile.Read(ref _disposeRequested) != 0,
this);
private void OnDown(IKeyboard _, Key key, int scanCode) =>
Invoke(() => _downSubscribers?.Invoke(this, key, scanCode));
private void OnUp(IKeyboard _, Key key, int scanCode) =>
Invoke(() => _upSubscribers?.Invoke(this, key, scanCode));
private void OnChar(IKeyboard _, char value) =>
Invoke(() => _charSubscribers?.Invoke(this, value));
private void Invoke(Action callback) =>
_quiescence.Invoke(() =>
{
if (Volatile.Read(ref _active) != 0)
callback();
});
private void Remove(int index)
{
if (!_attached[index]) return;
switch (index)
{
case 0: _inner.KeyDown -= _downRelay; _downSubscribers = null; break;
case 1: _inner.KeyUp -= _upRelay; _upSubscribers = null; break;
case 2: _inner.KeyChar -= _charRelay; _charSubscribers = null; break;
default: throw new ArgumentOutOfRangeException(nameof(index));
}
_attached[index] = false;
}
}
private sealed class QuiescentMouse : IMouse, IDisposable
{
private readonly IMouse _inner;
private readonly HostQuiescenceGate _quiescence;
private readonly Action<IMouse, MouseButton> _downRelay;
private readonly Action<IMouse, MouseButton> _upRelay;
private readonly Action<IMouse, MouseButton, Vector2> _clickRelay;
private readonly Action<IMouse, MouseButton, Vector2> _doubleClickRelay;
private readonly Action<IMouse, Vector2> _moveRelay;
private readonly Action<IMouse, ScrollWheel> _scrollRelay;
private Action<IMouse, MouseButton>? _downSubscribers;
private Action<IMouse, MouseButton>? _upSubscribers;
private Action<IMouse, MouseButton, Vector2>? _clickSubscribers;
private Action<IMouse, MouseButton, Vector2>? _doubleClickSubscribers;
private Action<IMouse, Vector2>? _moveSubscribers;
private Action<IMouse, ScrollWheel>? _scrollSubscribers;
private readonly bool[] _attached = new bool[6];
private ResourceShutdownTransaction? _detach;
private int _disposeRequested;
private int _active;
public QuiescentMouse(IMouse inner, HostQuiescenceGate quiescence)
{
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
_quiescence = quiescence;
_downRelay = OnDown;
_upRelay = OnUp;
_clickRelay = OnClick;
_doubleClickRelay = OnDoubleClick;
_moveRelay = OnMove;
_scrollRelay = OnScroll;
}
public string Name => _inner.Name;
public int Index => _inner.Index;
public bool IsConnected => _inner.IsConnected;
public IReadOnlyList<MouseButton> SupportedButtons => _inner.SupportedButtons;
public IReadOnlyList<ScrollWheel> ScrollWheels => _inner.ScrollWheels;
public Vector2 Position { get => _inner.Position; set => _inner.Position = value; }
public ICursor Cursor => _inner.Cursor;
public int DoubleClickTime { get => _inner.DoubleClickTime; set => _inner.DoubleClickTime = value; }
public int DoubleClickRange { get => _inner.DoubleClickRange; set => _inner.DoubleClickRange = value; }
public event Action<IMouse, MouseButton> MouseDown
{
add => Add(ref _downSubscribers, value, 0, () => _inner.MouseDown += _downRelay);
remove => RemoveSubscriber(ref _downSubscribers, value, 0, () => _inner.MouseDown -= _downRelay);
}
public event Action<IMouse, MouseButton> MouseUp
{
add => Add(ref _upSubscribers, value, 1, () => _inner.MouseUp += _upRelay);
remove => RemoveSubscriber(ref _upSubscribers, value, 1, () => _inner.MouseUp -= _upRelay);
}
public event Action<IMouse, MouseButton, Vector2> Click
{
add => Add(ref _clickSubscribers, value, 2, () => _inner.Click += _clickRelay);
remove => RemoveSubscriber(ref _clickSubscribers, value, 2, () => _inner.Click -= _clickRelay);
}
public event Action<IMouse, MouseButton, Vector2> DoubleClick
{
add => Add(ref _doubleClickSubscribers, value, 3, () => _inner.DoubleClick += _doubleClickRelay);
remove => RemoveSubscriber(ref _doubleClickSubscribers, value, 3, () => _inner.DoubleClick -= _doubleClickRelay);
}
public event Action<IMouse, Vector2> MouseMove
{
add => Add(ref _moveSubscribers, value, 4, () => _inner.MouseMove += _moveRelay);
remove => RemoveSubscriber(ref _moveSubscribers, value, 4, () => _inner.MouseMove -= _moveRelay);
}
public event Action<IMouse, ScrollWheel> Scroll
{
add => Add(ref _scrollSubscribers, value, 5, () => _inner.Scroll += _scrollRelay);
remove => RemoveSubscriber(ref _scrollSubscribers, value, 5, () => _inner.Scroll -= _scrollRelay);
}
public bool IsDisposalComplete => _attached.All(static value => !value);
public bool IsButtonPressed(MouseButton button) => _inner.IsButtonPressed(button);
public void Activate()
{
ThrowIfDisposed();
Volatile.Write(ref _active, 1);
}
public void Deactivate() => Interlocked.Exchange(ref _active, 0);
public void RequestDisposal() =>
Interlocked.Exchange(ref _disposeRequested, 1);
public void Dispose()
{
RequestDisposal();
Deactivate();
_detach ??= new ResourceShutdownTransaction(
new ResourceShutdownStage("frontend mouse callbacks",
[
new("scroll", () => RemovePhysical(5)),
new("move", () => RemovePhysical(4)),
new("double click", () => RemovePhysical(3)),
new("click", () => RemovePhysical(2)),
new("up", () => RemovePhysical(1)),
new("down", () => RemovePhysical(0)),
]));
_detach.CompleteOrThrow();
}
private void ThrowIfDisposed() =>
ObjectDisposedException.ThrowIf(
Volatile.Read(ref _disposeRequested) != 0,
this);
private void OnDown(IMouse _, MouseButton button) =>
Invoke(() => _downSubscribers?.Invoke(this, button));
private void OnUp(IMouse _, MouseButton button) =>
Invoke(() => _upSubscribers?.Invoke(this, button));
private void OnClick(IMouse _, MouseButton button, Vector2 position) =>
Invoke(() => _clickSubscribers?.Invoke(this, button, position));
private void OnDoubleClick(IMouse _, MouseButton button, Vector2 position) =>
Invoke(() => _doubleClickSubscribers?.Invoke(this, button, position));
private void OnMove(IMouse _, Vector2 position) =>
Invoke(() => _moveSubscribers?.Invoke(this, position));
private void OnScroll(IMouse _, ScrollWheel scroll) =>
Invoke(() => _scrollSubscribers?.Invoke(this, scroll));
private void Invoke(Action callback) =>
_quiescence.Invoke(() =>
{
if (Volatile.Read(ref _active) != 0)
callback();
});
private void Add<T>(ref T? subscribers, T value, int index, Action attach)
where T : Delegate
{
ThrowIfDisposed();
subscribers = (T?)Delegate.Combine(subscribers, value);
if (_attached[index]) return;
_attached[index] = true;
attach();
}
private void RemoveSubscriber<T>(
ref T? subscribers,
T value,
int index,
Action detach)
where T : Delegate
{
subscribers = (T?)Delegate.Remove(subscribers, value);
if (subscribers is not null || !_attached[index]) return;
detach();
_attached[index] = false;
}
private void RemovePhysical(int index)
{
if (!_attached[index]) return;
switch (index)
{
case 0: _inner.MouseDown -= _downRelay; _downSubscribers = null; break;
case 1: _inner.MouseUp -= _upRelay; _upSubscribers = null; break;
case 2: _inner.Click -= _clickRelay; _clickSubscribers = null; break;
case 3: _inner.DoubleClick -= _doubleClickRelay; _doubleClickSubscribers = null; break;
case 4: _inner.MouseMove -= _moveRelay; _moveSubscribers = null; break;
case 5: _inner.Scroll -= _scrollRelay; _scrollSubscribers = null; break;
default: throw new ArgumentOutOfRangeException(nameof(index));
}
_attached[index] = false;
}
}
}

View file

@ -1,46 +1,218 @@
using System;
using AcDream.App.Rendering;
using AcDream.UI.Abstractions.Input;
using Silk.NET.Input;
namespace AcDream.App.Input;
/// <summary>
/// Bridges a Silk.NET <see cref="IKeyboard"/> to the test-fakeable
/// <see cref="IKeyboardSource"/> interface used by
/// <see cref="InputDispatcher"/>. K.1a wiring: GameWindow constructs
/// one of these wrapping the first keyboard from
/// <c>IInputContext.Keyboards</c>; the dispatcher fans events out to
/// subscribers without ever touching Silk types directly.
/// </summary>
public sealed class SilkKeyboardSource : IKeyboardSource
internal interface IKeyboardEventSurface
{
void AddKeyDown(Action<Key> callback);
void RemoveKeyDown(Action<Key> callback);
void AddKeyUp(Action<Key> callback);
void RemoveKeyUp(Action<Key> callback);
bool IsKeyPressed(Key key);
}
internal sealed class SilkKeyboardEventSurface : IKeyboardEventSurface
{
private readonly IKeyboard _keyboard;
private Action<Key>? _keyDownCallback;
private Action<Key>? _keyUpCallback;
private readonly Action<IKeyboard, Key, int> _keyDown;
private readonly Action<IKeyboard, Key, int> _keyUp;
public SilkKeyboardEventSurface(IKeyboard keyboard)
{
_keyboard = keyboard ?? throw new ArgumentNullException(nameof(keyboard));
_keyDown = OnKeyDown;
_keyUp = OnKeyUp;
}
public void AddKeyDown(Action<Key> callback)
{
ArgumentNullException.ThrowIfNull(callback);
_keyDownCallback = callback;
_keyboard.KeyDown += _keyDown;
}
public void RemoveKeyDown(Action<Key> callback)
{
if (!ReferenceEquals(_keyDownCallback, callback))
return;
_keyboard.KeyDown -= _keyDown;
_keyDownCallback = null;
}
public void AddKeyUp(Action<Key> callback)
{
ArgumentNullException.ThrowIfNull(callback);
_keyUpCallback = callback;
_keyboard.KeyUp += _keyUp;
}
public void RemoveKeyUp(Action<Key> callback)
{
if (!ReferenceEquals(_keyUpCallback, callback))
return;
_keyboard.KeyUp -= _keyUp;
_keyUpCallback = null;
}
public bool IsKeyPressed(Key key) => _keyboard.IsKeyPressed(key);
private void OnKeyDown(IKeyboard _, Key key, int __) =>
_keyDownCallback?.Invoke(key);
private void OnKeyUp(IKeyboard _, Key key, int __) =>
_keyUpCallback?.Invoke(key);
}
/// <summary>
/// Reversible Silk keyboard bridge. Production publishes the detached owner
/// before Attach so a side-effecting event accessor can never orphan a
/// subscription. Logical deactivation makes copied delegates inert while
/// physical removal is retried.
/// </summary>
public sealed class SilkKeyboardSource : IKeyboardSource, IDisposable
{
private readonly IKeyboardEventSurface _surface;
private readonly HostQuiescenceGate _quiescence;
private readonly Action<Key> _keyDown;
private readonly Action<Key> _keyUp;
private readonly bool[] _attached = new bool[2];
private ResourceShutdownTransaction? _detach;
private bool _attachStarted;
private int _disposeRequested;
private int _active;
public event Action<Key, ModifierMask>? KeyDown;
public event Action<Key, ModifierMask>? KeyUp;
public SilkKeyboardSource(IKeyboard keyboard)
private SilkKeyboardSource(
IKeyboardEventSurface surface,
HostQuiescenceGate quiescence)
{
_keyboard = keyboard ?? throw new ArgumentNullException(nameof(keyboard));
_keyboard.KeyDown += (_, key, _) => KeyDown?.Invoke(key, ReadModifiers());
_keyboard.KeyUp += (_, key, _) => KeyUp?.Invoke(key, ReadModifiers());
_surface = surface ?? throw new ArgumentNullException(nameof(surface));
_quiescence = quiescence ?? throw new ArgumentNullException(nameof(quiescence));
_keyDown = OnKeyDown;
_keyUp = OnKeyUp;
}
public bool IsHeld(Key key) => _keyboard.IsKeyPressed(key);
internal static SilkKeyboardSource CreateDetached(
IKeyboard keyboard,
HostQuiescenceGate quiescence) =>
new(
new SilkKeyboardEventSurface(keyboard),
quiescence);
internal static SilkKeyboardSource CreateDetached(
IKeyboardEventSurface surface,
HostQuiescenceGate quiescence) =>
new(surface, quiescence);
public bool IsDisposalComplete => _attached.All(static value => !value);
public void Attach()
{
ObjectDisposedException.ThrowIf(
Volatile.Read(ref _disposeRequested) != 0,
this);
if (_attachStarted)
throw new InvalidOperationException("Keyboard source attachment has already started.");
_attachStarted = true;
try
{
_attached[0] = true;
_surface.AddKeyDown(_keyDown);
_attached[1] = true;
_surface.AddKeyUp(_keyUp);
Volatile.Write(ref _active, 1);
}
catch (Exception attachError)
{
Deactivate();
RollBackOrThrow(attachError);
}
}
public bool IsHeld(Key key) => _surface.IsKeyPressed(key);
public ModifierMask CurrentModifiers => ReadModifiers();
public void Deactivate() => Interlocked.Exchange(ref _active, 0);
public void Dispose()
{
Interlocked.Exchange(ref _disposeRequested, 1);
Deactivate();
EnsureDetachTransaction().CompleteOrThrow();
}
private void OnKeyDown(Key key) =>
_quiescence.Invoke(() =>
{
if (Volatile.Read(ref _active) != 0)
KeyDown?.Invoke(key, ReadModifiers());
});
private void OnKeyUp(Key key) =>
_quiescence.Invoke(() =>
{
if (Volatile.Read(ref _active) != 0)
KeyUp?.Invoke(key, ReadModifiers());
});
private ModifierMask ReadModifiers()
{
ModifierMask m = ModifierMask.None;
if (_keyboard.IsKeyPressed(Key.ShiftLeft) || _keyboard.IsKeyPressed(Key.ShiftRight))
m |= ModifierMask.Shift;
if (_keyboard.IsKeyPressed(Key.ControlLeft) || _keyboard.IsKeyPressed(Key.ControlRight))
m |= ModifierMask.Ctrl;
if (_keyboard.IsKeyPressed(Key.AltLeft) || _keyboard.IsKeyPressed(Key.AltRight))
m |= ModifierMask.Alt;
if (_keyboard.IsKeyPressed(Key.SuperLeft) || _keyboard.IsKeyPressed(Key.SuperRight))
m |= ModifierMask.Win;
return m;
ModifierMask modifiers = ModifierMask.None;
if (_surface.IsKeyPressed(Key.ShiftLeft) || _surface.IsKeyPressed(Key.ShiftRight))
modifiers |= ModifierMask.Shift;
if (_surface.IsKeyPressed(Key.ControlLeft) || _surface.IsKeyPressed(Key.ControlRight))
modifiers |= ModifierMask.Ctrl;
if (_surface.IsKeyPressed(Key.AltLeft) || _surface.IsKeyPressed(Key.AltRight))
modifiers |= ModifierMask.Alt;
if (_surface.IsKeyPressed(Key.SuperLeft) || _surface.IsKeyPressed(Key.SuperRight))
modifiers |= ModifierMask.Win;
return modifiers;
}
private ResourceShutdownTransaction EnsureDetachTransaction() =>
_detach ??= new ResourceShutdownTransaction(
new ResourceShutdownStage("keyboard source callbacks",
[
new("key up", () => Remove(1)),
new("key down", () => Remove(0)),
]));
private void Remove(int index)
{
if (!_attached[index])
return;
if (index == 1)
_surface.RemoveKeyUp(_keyUp);
else
_surface.RemoveKeyDown(_keyDown);
_attached[index] = false;
}
private void RollBackOrThrow(Exception attachError)
{
try
{
EnsureDetachTransaction().CompleteOrThrow();
}
catch (Exception rollbackError)
{
throw new AggregateException(
"Keyboard source registration and rollback both failed.",
new InvalidOperationException(
"Keyboard source registration failed.", attachError),
rollbackError);
}
throw new InvalidOperationException(
"Keyboard source registration failed and was rolled back.",
attachError);
}
}

View file

@ -1,92 +1,295 @@
using System;
using System.Numerics;
using AcDream.App.Rendering;
using AcDream.UI.Abstractions.Input;
using Silk.NET.Input;
namespace AcDream.App.Input;
/// <summary>
/// Bridges a Silk.NET <see cref="IMouse"/> + the active ImGui IO
/// (for <see cref="WantCaptureMouse"/> / <see cref="WantCaptureKeyboard"/>)
/// to the test-fakeable <see cref="IMouseSource"/> used by
/// <see cref="InputDispatcher"/>.
///
/// <para>
/// Production receives a typed capture source, keeping retained-UI and
/// developer-UI ownership outside the Silk adapter. The public delegate
/// overload remains as a compatibility seam for standalone consumers.
/// </para>
/// </summary>
public sealed class SilkMouseSource : IMouseSource
internal interface IMouseEventSurface
{
void AddMouseDown(Action<MouseButton> callback);
void RemoveMouseDown(Action<MouseButton> callback);
void AddMouseUp(Action<MouseButton> callback);
void RemoveMouseUp(Action<MouseButton> callback);
void AddMouseMove(Action<Vector2> callback);
void RemoveMouseMove(Action<Vector2> callback);
void AddScroll(Action<float> callback);
void RemoveScroll(Action<float> callback);
bool IsButtonPressed(MouseButton button);
}
internal sealed class SilkMouseEventSurface : IMouseEventSurface
{
private readonly IMouse _mouse;
private Action<MouseButton>? _downCallback;
private Action<MouseButton>? _upCallback;
private Action<Vector2>? _moveCallback;
private Action<float>? _scrollCallback;
private readonly Action<IMouse, MouseButton> _down;
private readonly Action<IMouse, MouseButton> _up;
private readonly Action<IMouse, Vector2> _move;
private readonly Action<IMouse, ScrollWheel> _scroll;
public SilkMouseEventSurface(IMouse mouse)
{
_mouse = mouse ?? throw new ArgumentNullException(nameof(mouse));
_down = OnDown;
_up = OnUp;
_move = OnMove;
_scroll = OnScroll;
}
public void AddMouseDown(Action<MouseButton> callback)
{
_downCallback = callback ?? throw new ArgumentNullException(nameof(callback));
_mouse.MouseDown += _down;
}
public void RemoveMouseDown(Action<MouseButton> callback)
{
if (!ReferenceEquals(_downCallback, callback)) return;
_mouse.MouseDown -= _down;
_downCallback = null;
}
public void AddMouseUp(Action<MouseButton> callback)
{
_upCallback = callback ?? throw new ArgumentNullException(nameof(callback));
_mouse.MouseUp += _up;
}
public void RemoveMouseUp(Action<MouseButton> callback)
{
if (!ReferenceEquals(_upCallback, callback)) return;
_mouse.MouseUp -= _up;
_upCallback = null;
}
public void AddMouseMove(Action<Vector2> callback)
{
_moveCallback = callback ?? throw new ArgumentNullException(nameof(callback));
_mouse.MouseMove += _move;
}
public void RemoveMouseMove(Action<Vector2> callback)
{
if (!ReferenceEquals(_moveCallback, callback)) return;
_mouse.MouseMove -= _move;
_moveCallback = null;
}
public void AddScroll(Action<float> callback)
{
_scrollCallback = callback ?? throw new ArgumentNullException(nameof(callback));
_mouse.Scroll += _scroll;
}
public void RemoveScroll(Action<float> callback)
{
if (!ReferenceEquals(_scrollCallback, callback)) return;
_mouse.Scroll -= _scroll;
_scrollCallback = null;
}
public bool IsButtonPressed(MouseButton button) => _mouse.IsButtonPressed(button);
private void OnDown(IMouse _, MouseButton button) => _downCallback?.Invoke(button);
private void OnUp(IMouse _, MouseButton button) => _upCallback?.Invoke(button);
private void OnMove(IMouse _, Vector2 position) => _moveCallback?.Invoke(position);
private void OnScroll(IMouse _, ScrollWheel scroll) => _scrollCallback?.Invoke(scroll.Y);
}
/// <summary>Reversible Silk mouse bridge with immediate logical cutoff.</summary>
public sealed class SilkMouseSource : IMouseSource, IDisposable
{
private readonly IMouseEventSurface _surface;
private readonly IInputCaptureSource _capture;
private readonly HostQuiescenceGate _quiescence;
private readonly Action<MouseButton> _mouseDown;
private readonly Action<MouseButton> _mouseUp;
private readonly Action<Vector2> _mouseMove;
private readonly Action<float> _scroll;
private readonly bool[] _attached = new bool[4];
private ResourceShutdownTransaction? _detach;
private bool _attachStarted;
private int _disposeRequested;
private int _active;
private float _lastX;
private float _lastY;
private bool _haveLastPos;
private bool _haveLastPosition;
public event Action<MouseButton, ModifierMask>? MouseDown;
public event Action<MouseButton, ModifierMask>? MouseUp;
public event Action<float, float>? MouseMove;
public event Action<float>? Scroll;
/// <summary>The keyboard source that owns the current modifier mask, so
/// mouse events and keyboard events resolve identical chords.</summary>
public IKeyboardSource? ModifierSource { get; set; }
internal SilkMouseSource(
private SilkMouseSource(
IMouseEventSurface surface,
IInputCaptureSource capture,
IKeyboardSource? modifierSource,
HostQuiescenceGate quiescence)
{
_surface = surface ?? throw new ArgumentNullException(nameof(surface));
_capture = capture ?? throw new ArgumentNullException(nameof(capture));
ModifierSource = modifierSource;
_quiescence = quiescence ?? throw new ArgumentNullException(nameof(quiescence));
_mouseDown = OnMouseDown;
_mouseUp = OnMouseUp;
_mouseMove = OnMouseMove;
_scroll = OnScroll;
}
internal static SilkMouseSource CreateDetached(
IMouse mouse,
IInputCaptureSource capture,
IKeyboardSource modifierSource)
{
_mouse = mouse ?? throw new ArgumentNullException(nameof(mouse));
_capture = capture ?? throw new ArgumentNullException(nameof(capture));
ModifierSource = modifierSource
?? throw new ArgumentNullException(nameof(modifierSource));
Subscribe();
}
IKeyboardSource? modifierSource,
HostQuiescenceGate quiescence) =>
new(
new SilkMouseEventSurface(mouse),
capture,
modifierSource,
quiescence);
public SilkMouseSource(
IMouse mouse,
Func<bool> wantCaptureMouse,
Func<bool> wantCaptureKeyboard)
{
_mouse = mouse ?? throw new ArgumentNullException(nameof(mouse));
_capture = new DelegateInputCaptureSource(
wantCaptureMouse,
wantCaptureKeyboard);
Subscribe();
}
internal static SilkMouseSource CreateDetached(
IMouseEventSurface surface,
IInputCaptureSource capture,
IKeyboardSource? modifierSource,
HostQuiescenceGate quiescence) =>
new(surface, capture, modifierSource, quiescence);
private void Subscribe()
public bool IsDisposalComplete => _attached.All(static value => !value);
public void Attach()
{
_mouse.MouseDown += (_, btn) => MouseDown?.Invoke(btn, ReadModifiers());
_mouse.MouseUp += (_, btn) => MouseUp?.Invoke(btn, ReadModifiers());
_mouse.MouseMove += (_, pos) =>
ObjectDisposedException.ThrowIf(
Volatile.Read(ref _disposeRequested) != 0,
this);
if (_attachStarted)
throw new InvalidOperationException("Mouse source attachment has already started.");
_attachStarted = true;
try
{
float dx, dy;
if (_haveLastPos)
{
dx = pos.X - _lastX;
dy = pos.Y - _lastY;
}
else
{
dx = 0f;
dy = 0f;
_haveLastPos = true;
}
_lastX = pos.X;
_lastY = pos.Y;
MouseMove?.Invoke(dx, dy);
};
_mouse.Scroll += (_, scroll) => Scroll?.Invoke(scroll.Y);
_attached[0] = true;
_surface.AddMouseDown(_mouseDown);
_attached[1] = true;
_surface.AddMouseUp(_mouseUp);
_attached[2] = true;
_surface.AddMouseMove(_mouseMove);
_attached[3] = true;
_surface.AddScroll(_scroll);
Volatile.Write(ref _active, 1);
}
catch (Exception attachError)
{
Deactivate();
RollBackOrThrow(attachError);
}
}
public bool IsHeld(MouseButton button) => _surface.IsButtonPressed(button);
public bool WantCaptureMouse => _capture.WantCaptureMouse;
public bool WantCaptureKeyboard => _capture.WantCaptureKeyboard;
public void Deactivate() => Interlocked.Exchange(ref _active, 0);
public void Dispose()
{
Interlocked.Exchange(ref _disposeRequested, 1);
Deactivate();
EnsureDetachTransaction().CompleteOrThrow();
}
private ModifierMask ReadModifiers() =>
ModifierSource?.CurrentModifiers ?? ModifierMask.None;
public bool IsHeld(MouseButton button) => _mouse.IsButtonPressed(button);
private void OnMouseDown(MouseButton button) =>
_quiescence.Invoke(() =>
{
if (Volatile.Read(ref _active) != 0)
MouseDown?.Invoke(button, ReadModifiers());
});
public bool WantCaptureMouse => _capture.WantCaptureMouse;
public bool WantCaptureKeyboard => _capture.WantCaptureKeyboard;
private void OnMouseUp(MouseButton button) =>
_quiescence.Invoke(() =>
{
if (Volatile.Read(ref _active) != 0)
MouseUp?.Invoke(button, ReadModifiers());
});
private void OnMouseMove(Vector2 position) =>
_quiescence.Invoke(() =>
{
if (Volatile.Read(ref _active) == 0)
return;
float dx = 0f;
float dy = 0f;
if (_haveLastPosition)
{
dx = position.X - _lastX;
dy = position.Y - _lastY;
}
else
{
_haveLastPosition = true;
}
_lastX = position.X;
_lastY = position.Y;
MouseMove?.Invoke(dx, dy);
});
private void OnScroll(float delta) =>
_quiescence.Invoke(() =>
{
if (Volatile.Read(ref _active) != 0)
Scroll?.Invoke(delta);
});
private ResourceShutdownTransaction EnsureDetachTransaction() =>
_detach ??= new ResourceShutdownTransaction(
new ResourceShutdownStage("mouse source callbacks",
[
new("scroll", () => Remove(3)),
new("move", () => Remove(2)),
new("up", () => Remove(1)),
new("down", () => Remove(0)),
]));
private void Remove(int index)
{
if (!_attached[index])
return;
switch (index)
{
case 0: _surface.RemoveMouseDown(_mouseDown); break;
case 1: _surface.RemoveMouseUp(_mouseUp); break;
case 2: _surface.RemoveMouseMove(_mouseMove); break;
case 3: _surface.RemoveScroll(_scroll); break;
default: throw new ArgumentOutOfRangeException(nameof(index));
}
_attached[index] = false;
}
private void RollBackOrThrow(Exception attachError)
{
try
{
EnsureDetachTransaction().CompleteOrThrow();
}
catch (Exception rollbackError)
{
throw new AggregateException(
"Mouse source registration and rollback both failed.",
new InvalidOperationException(
"Mouse source registration failed.", attachError),
rollbackError);
}
throw new InvalidOperationException(
"Mouse source registration failed and was rolled back.",
attachError);
}
}