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

@ -49,15 +49,27 @@ public sealed class UiHost : System.IDisposable
public IKeyboard? Keyboard { get; private set; }
private long _startTicks = System.Environment.TickCount64;
private readonly List<System.Action> _inputUnsubscribers = new();
private readonly HostQuiescenceGate _quiescence;
private readonly List<IRetainedUiInputBinding> _inputBindings = new();
private ResourceShutdownTransaction? _inputShutdown;
private ResourceShutdownTransaction? _shutdown;
private bool _disposeRequested;
private bool _disposed;
public UiHost(GL gl, string shaderDir, BitmapFont? defaultFont = null)
: this(gl, shaderDir, defaultFont, new HostQuiescenceGate())
{
}
internal UiHost(
GL gl,
string shaderDir,
BitmapFont? defaultFont,
HostQuiescenceGate quiescence)
{
TextRenderer = new TextRenderer(gl, shaderDir);
DefaultFont = defaultFont;
_quiescence = quiescence ?? throw new ArgumentNullException(nameof(quiescence));
}
// ── Per-frame ──────────────────────────────────────────────────────
@ -86,25 +98,21 @@ public sealed class UiHost : System.IDisposable
System.ObjectDisposedException.ThrowIf(_disposeRequested || _disposed, this);
System.ArgumentNullException.ThrowIfNull(mouse);
void OnMouseDown(IMouse sender, MouseButton button) =>
Root.OnMouseDown(MapButton(button), (int)sender.Position.X, (int)sender.Position.Y);
void OnMouseUp(IMouse sender, MouseButton button) =>
Root.OnMouseUp(MapButton(button), (int)sender.Position.X, (int)sender.Position.Y);
void OnMouseMove(IMouse sender, Vector2 position) =>
Root.OnMouseMove((int)position.X, (int)position.Y);
void OnScroll(IMouse sender, ScrollWheel scroll) => Root.OnScroll((int)scroll.Y);
mouse.MouseDown += OnMouseDown;
mouse.MouseUp += OnMouseUp;
mouse.MouseMove += OnMouseMove;
mouse.Scroll += OnScroll;
_inputUnsubscribers.Add(() =>
var binding = new RetainedMouseInputBinding(
new SilkRetainedMouseSurface(mouse),
Root,
_quiescence);
_inputBindings.Add(binding);
try
{
mouse.MouseDown -= OnMouseDown;
mouse.MouseUp -= OnMouseUp;
mouse.MouseMove -= OnMouseMove;
mouse.Scroll -= OnScroll;
});
binding.Attach();
}
catch
{
if (binding.IsDisposalComplete)
_inputBindings.Remove(binding);
throw;
}
}
public void WireKeyboard(IKeyboard kb)
@ -112,28 +120,54 @@ public sealed class UiHost : System.IDisposable
System.ObjectDisposedException.ThrowIf(_disposeRequested || _disposed, this);
System.ArgumentNullException.ThrowIfNull(kb);
Keyboard = kb; // last wired keyboard wins (one-keyboard desktop)
void OnKeyDown(IKeyboard sender, Key key, int scanCode) => Root.OnKeyDown((int)key);
void OnKeyUp(IKeyboard sender, Key key, int scanCode) => Root.OnKeyUp((int)key);
void OnKeyChar(IKeyboard sender, char value) => Root.OnChar(value);
kb.KeyDown += OnKeyDown;
kb.KeyUp += OnKeyUp;
kb.KeyChar += OnKeyChar;
_inputUnsubscribers.Add(() =>
var binding = new RetainedKeyboardInputBinding(
new SilkRetainedKeyboardSurface(kb),
Root,
_quiescence);
_inputBindings.Add(binding);
try
{
kb.KeyDown -= OnKeyDown;
kb.KeyUp -= OnKeyUp;
kb.KeyChar -= OnKeyChar;
});
binding.Attach();
}
catch
{
if (binding.IsDisposalComplete)
_inputBindings.Remove(binding);
throw;
}
}
private static UiMouseButton MapButton(MouseButton b) => b switch
/// <summary>
/// Stops retained-device delivery without retiring the window tree or GL
/// renderer. Safe to call before a potentially long live-session close.
/// </summary>
public void QuiesceInput()
{
MouseButton.Left => UiMouseButton.Left,
MouseButton.Right => UiMouseButton.Right,
MouseButton.Middle => UiMouseButton.Middle,
_ => UiMouseButton.Left,
};
_disposeRequested = true;
foreach (IRetainedUiInputBinding binding in _inputBindings)
binding.Deactivate();
Keyboard = null;
}
/// <summary>Physically removes every retained input edge after quiescence.</summary>
public void DeactivateInput()
{
QuiesceInput();
_inputShutdown ??= new ResourceShutdownTransaction(
new ResourceShutdownStage(
"retained UI input subscriptions",
_inputBindings
.AsEnumerable()
.Reverse()
.Select((binding, index) => new ResourceShutdownOperation(
$"input binding {index}",
binding.Dispose))
.ToArray()));
_inputShutdown.CompleteOrThrow();
if (_inputShutdown.IsComplete)
_inputBindings.Clear();
}
// ── Window manager forwarders (delegate to UiRoot) ─────────────────
@ -169,12 +203,8 @@ public sealed class UiHost : System.IDisposable
_disposeRequested = true;
_shutdown ??= CreateShutdownTransaction(
_inputUnsubscribers.AsEnumerable().Reverse().ToArray(),
() =>
{
_inputUnsubscribers.Clear();
Keyboard = null;
},
[DeactivateInput],
() => { },
WindowManager.Dispose,
TextRenderer.Dispose);
_shutdown.CompleteOrThrow();