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
262
tests/AcDream.App.Tests/UI/RetainedUiInputBindingTests.cs
Normal file
262
tests/AcDream.App.Tests/UI/RetainedUiInputBindingTests.cs
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
using AcDream.App.Rendering;
|
||||
using AcDream.App.UI;
|
||||
using Silk.NET.Input;
|
||||
|
||||
namespace AcDream.App.Tests.UI;
|
||||
|
||||
public sealed class RetainedUiInputBindingTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(1)]
|
||||
[InlineData(2)]
|
||||
[InlineData(3)]
|
||||
[InlineData(4)]
|
||||
public void MouseAttachFailureRollsBackEveryPossiblyAcquiredPrefix(int failAdd)
|
||||
{
|
||||
var surface = new MouseSurface { FailAdd = failAdd };
|
||||
var binding = new RetainedMouseInputBinding(
|
||||
surface,
|
||||
new UiRoot(),
|
||||
new HostQuiescenceGate());
|
||||
|
||||
Assert.ThrowsAny<Exception>(binding.Attach);
|
||||
|
||||
Assert.True(binding.IsDisposalComplete);
|
||||
Assert.Equal(failAdd, surface.AddCalls);
|
||||
Assert.Equal(failAdd, surface.RemoveCalls);
|
||||
Assert.Equal(0, surface.LiveEdges);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1)]
|
||||
[InlineData(2)]
|
||||
[InlineData(3)]
|
||||
public void KeyboardAttachFailureRollsBackEveryPossiblyAcquiredPrefix(int failAdd)
|
||||
{
|
||||
var surface = new KeyboardSurface { FailAdd = failAdd };
|
||||
var binding = new RetainedKeyboardInputBinding(
|
||||
surface,
|
||||
new UiRoot(),
|
||||
new HostQuiescenceGate());
|
||||
|
||||
Assert.ThrowsAny<Exception>(binding.Attach);
|
||||
|
||||
Assert.True(binding.IsDisposalComplete);
|
||||
Assert.Equal(failAdd, surface.AddCalls);
|
||||
Assert.Equal(failAdd, surface.RemoveCalls);
|
||||
Assert.Equal(0, surface.LiveEdges);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MouseDeactivateSilencesCopiedDelegateBeforePhysicalDetach()
|
||||
{
|
||||
var surface = new MouseSurface();
|
||||
var root = new UiRoot();
|
||||
int worldClicks = 0;
|
||||
root.WorldMouseFallThrough += (_, _, _, _) => worldClicks++;
|
||||
var binding = new RetainedMouseInputBinding(
|
||||
surface,
|
||||
root,
|
||||
new HostQuiescenceGate());
|
||||
binding.Attach();
|
||||
Action<MouseButton, int, int> copied = surface.Down!;
|
||||
|
||||
copied(MouseButton.Left, 10, 20);
|
||||
binding.Deactivate();
|
||||
copied(MouseButton.Left, 10, 20);
|
||||
binding.Dispose();
|
||||
|
||||
Assert.Equal(1, worldClicks);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MouseEventRaisedReentrantlyDuringAttachCannotEnterPartialUiBinding()
|
||||
{
|
||||
var surface = new MouseSurface { RaiseDuringAdd = true };
|
||||
var root = new UiRoot();
|
||||
int worldClicks = 0;
|
||||
root.WorldMouseFallThrough += (_, _, _, _) => worldClicks++;
|
||||
var binding = new RetainedMouseInputBinding(
|
||||
surface,
|
||||
root,
|
||||
new HostQuiescenceGate());
|
||||
|
||||
binding.Attach();
|
||||
Assert.Equal(0, worldClicks);
|
||||
surface.Down!(MouseButton.Left, 1, 2);
|
||||
|
||||
Assert.Equal(1, worldClicks);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KeyboardDeactivateSilencesCopiedDelegateBeforePhysicalDetach()
|
||||
{
|
||||
var surface = new KeyboardSurface();
|
||||
var root = new UiRoot();
|
||||
int worldKeys = 0;
|
||||
root.WorldKeyFallThrough += (_, _) => worldKeys++;
|
||||
var binding = new RetainedKeyboardInputBinding(
|
||||
surface,
|
||||
root,
|
||||
new HostQuiescenceGate());
|
||||
binding.Attach();
|
||||
Action<Key> copied = surface.Down!;
|
||||
|
||||
copied(Key.A);
|
||||
binding.Deactivate();
|
||||
copied(Key.B);
|
||||
binding.Dispose();
|
||||
|
||||
Assert.Equal(1, worldKeys);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RetainedBindingsAreTerminalAfterDisposeBeforeAttach()
|
||||
{
|
||||
var mouseSurface = new MouseSurface();
|
||||
var mouse = new RetainedMouseInputBinding(
|
||||
mouseSurface,
|
||||
new UiRoot(),
|
||||
new HostQuiescenceGate());
|
||||
var keyboardSurface = new KeyboardSurface();
|
||||
var keyboard = new RetainedKeyboardInputBinding(
|
||||
keyboardSurface,
|
||||
new UiRoot(),
|
||||
new HostQuiescenceGate());
|
||||
|
||||
mouse.Dispose();
|
||||
keyboard.Dispose();
|
||||
|
||||
Assert.Throws<ObjectDisposedException>(mouse.Attach);
|
||||
Assert.Throws<ObjectDisposedException>(keyboard.Attach);
|
||||
mouse.Dispose();
|
||||
keyboard.Dispose();
|
||||
Assert.Equal(0, mouseSurface.LiveEdges);
|
||||
Assert.Equal(0, keyboardSurface.LiveEdges);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RetainedBindingsRetryOnlyFailedPhysicalEdges()
|
||||
{
|
||||
var mouseSurface = new MouseSurface();
|
||||
var mouse = new RetainedMouseInputBinding(
|
||||
mouseSurface,
|
||||
new UiRoot(),
|
||||
new HostQuiescenceGate());
|
||||
var keyboardSurface = new KeyboardSurface();
|
||||
var keyboard = new RetainedKeyboardInputBinding(
|
||||
keyboardSurface,
|
||||
new UiRoot(),
|
||||
new HostQuiescenceGate());
|
||||
mouse.Attach();
|
||||
keyboard.Attach();
|
||||
mouseSurface.FailRemoveMoveOnce = true;
|
||||
keyboardSurface.FailRemoveUpOnce = true;
|
||||
|
||||
mouse.Dispose();
|
||||
keyboard.Dispose();
|
||||
int mouseRemoves = mouseSurface.RemoveCalls;
|
||||
int keyboardRemoves = keyboardSurface.RemoveCalls;
|
||||
mouse.Dispose();
|
||||
keyboard.Dispose();
|
||||
|
||||
Assert.Equal(mouseRemoves, mouseSurface.RemoveCalls);
|
||||
Assert.Equal(keyboardRemoves, keyboardSurface.RemoveCalls);
|
||||
Assert.Equal(2, mouseSurface.MoveRemoveCalls);
|
||||
Assert.Equal(2, keyboardSurface.UpRemoveCalls);
|
||||
Assert.True(mouse.IsDisposalComplete);
|
||||
Assert.True(keyboard.IsDisposalComplete);
|
||||
}
|
||||
|
||||
private sealed class MouseSurface : IRetainedMouseSurface
|
||||
{
|
||||
public int FailAdd { get; init; }
|
||||
public bool RaiseDuringAdd { get; init; }
|
||||
public bool FailRemoveMoveOnce { get; set; }
|
||||
public int AddCalls { get; private set; }
|
||||
public int RemoveCalls { get; private set; }
|
||||
public int MoveRemoveCalls { get; private set; }
|
||||
public int LiveEdges { get; private set; }
|
||||
public Action<MouseButton, int, int>? Down { get; private set; }
|
||||
private Action<MouseButton, int, int>? _up;
|
||||
private Action<int, int>? _move;
|
||||
private Action<int>? _scroll;
|
||||
|
||||
public void AddMouseDown(Action<MouseButton, int, int> callback) => Add(() => Down = callback);
|
||||
public void AddMouseUp(Action<MouseButton, int, int> callback) => Add(() => _up = callback);
|
||||
public void AddMouseMove(Action<int, int> callback) => Add(() => _move = callback);
|
||||
public void AddScroll(Action<int> callback) => Add(() => _scroll = callback);
|
||||
public void RemoveMouseDown(Action<MouseButton, int, int> callback) => Remove(() => Down = null);
|
||||
public void RemoveMouseUp(Action<MouseButton, int, int> callback) => Remove(() => _up = null);
|
||||
public void RemoveMouseMove(Action<int, int> callback)
|
||||
{
|
||||
MoveRemoveCalls++;
|
||||
if (FailRemoveMoveOnce)
|
||||
{
|
||||
FailRemoveMoveOnce = false;
|
||||
throw new InvalidOperationException("remove move");
|
||||
}
|
||||
Remove(() => _move = null);
|
||||
}
|
||||
public void RemoveScroll(Action<int> callback) => Remove(() => _scroll = null);
|
||||
|
||||
private void Add(Action publish)
|
||||
{
|
||||
AddCalls++;
|
||||
publish();
|
||||
LiveEdges++;
|
||||
if (RaiseDuringAdd)
|
||||
Down?.Invoke(MouseButton.Left, 1, 2);
|
||||
if (AddCalls == FailAdd) throw new InvalidOperationException("add");
|
||||
}
|
||||
private void Remove(Action clear)
|
||||
{
|
||||
RemoveCalls++;
|
||||
clear();
|
||||
LiveEdges--;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class KeyboardSurface : IRetainedKeyboardSurface
|
||||
{
|
||||
public int FailAdd { get; init; }
|
||||
public bool FailRemoveUpOnce { get; set; }
|
||||
public int AddCalls { get; private set; }
|
||||
public int RemoveCalls { get; private set; }
|
||||
public int UpRemoveCalls { get; private set; }
|
||||
public int LiveEdges { get; private set; }
|
||||
public Action<Key>? Down { get; private set; }
|
||||
private Action<Key>? _up;
|
||||
private Action<char>? _char;
|
||||
|
||||
public void AddKeyDown(Action<Key> callback) => Add(() => Down = callback);
|
||||
public void AddKeyUp(Action<Key> callback) => Add(() => _up = callback);
|
||||
public void AddKeyChar(Action<char> callback) => Add(() => _char = callback);
|
||||
public void RemoveKeyDown(Action<Key> callback) => Remove(() => Down = null);
|
||||
public void RemoveKeyUp(Action<Key> callback)
|
||||
{
|
||||
UpRemoveCalls++;
|
||||
if (FailRemoveUpOnce)
|
||||
{
|
||||
FailRemoveUpOnce = false;
|
||||
throw new InvalidOperationException("remove up");
|
||||
}
|
||||
Remove(() => _up = null);
|
||||
}
|
||||
public void RemoveKeyChar(Action<char> callback) => Remove(() => _char = null);
|
||||
|
||||
private void Add(Action publish)
|
||||
{
|
||||
AddCalls++;
|
||||
publish();
|
||||
LiveEdges++;
|
||||
if (AddCalls == FailAdd) throw new InvalidOperationException("add");
|
||||
}
|
||||
private void Remove(Action clear)
|
||||
{
|
||||
RemoveCalls++;
|
||||
clear();
|
||||
LiveEdges--;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue