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:
parent
d09e246d3a
commit
8b8afeefa3
42 changed files with 4029 additions and 461 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue