acdream/src/AcDream.App/Plugins/BufferedUiRegistry.cs

127 lines
3.8 KiB
C#

using System.Collections.Generic;
using AcDream.App.UI;
using AcDream.Plugin.Abstractions;
namespace AcDream.App.Plugins;
/// <summary>
/// Buffers plugin <see cref="IUiRegistry.AddMarkupPanel"/> calls (which run in
/// Program.cs before the GL window opens) until GameWindow drains them into the
/// UiHost tree after construction.
/// </summary>
public sealed class BufferedUiRegistry : IScopedUiRegistry
{
public readonly record struct Pending(string MarkupPath, object Binding)
{
internal long RegistrationId { get; init; }
}
private sealed class Registration(string markupPath, object binding)
{
internal string MarkupPath { get; } = markupPath;
internal object Binding { get; } = binding;
internal bool Drained { get; set; }
internal UiRoot? Root { get; set; }
internal UiElement? Element { get; set; }
}
private readonly object _gate = new();
private readonly Dictionary<long, Registration> _registrations = [];
private long _nextRegistrationId;
public void AddMarkupPanel(string markupPath, object binding)
=> _ = RegisterMarkupPanel(markupPath, binding);
public IDisposable RegisterMarkupPanel(string markupPath, object binding)
{
ArgumentException.ThrowIfNullOrWhiteSpace(markupPath);
ArgumentNullException.ThrowIfNull(binding);
long id;
lock (_gate)
{
id = checked(++_nextRegistrationId);
_registrations.Add(id, new Registration(markupPath, binding));
}
return new RegistrationToken(this, id);
}
/// <summary>Returns each not-yet-drained active registration once.</summary>
public IReadOnlyList<Pending> Drain()
{
lock (_gate)
{
var pending = new List<Pending>(_registrations.Count);
foreach ((long id, Registration registration) in _registrations)
{
if (registration.Drained)
continue;
registration.Drained = true;
pending.Add(new Pending(
registration.MarkupPath,
registration.Binding)
{
RegistrationId = id,
});
}
return pending;
}
}
internal void CompleteMount(Pending pending, UiRoot root, UiElement element)
{
bool stillRegistered;
lock (_gate)
{
stillRegistered = _registrations.TryGetValue(
pending.RegistrationId,
out Registration? registration);
if (stillRegistered)
{
registration!.Root = root;
registration.Element = element;
}
}
// A plugin can fail/disable while markup is being built. Never leave
// the just-built child mounted if its host-owned token was rolled back.
if (!stillRegistered)
root.RemoveChild(element);
}
internal void FailMount(Pending pending) => Remove(pending.RegistrationId);
internal int RegistrationCount
{
get
{
lock (_gate)
return _registrations.Count;
}
}
private void Remove(long id)
{
UiRoot? root;
UiElement? element;
lock (_gate)
{
if (!_registrations.Remove(id, out Registration? registration))
return;
root = registration.Root;
element = registration.Element;
}
if (root is not null && element is not null)
root.RemoveChild(element);
}
private sealed class RegistrationToken(
BufferedUiRegistry owner,
long registrationId) : IDisposable
{
private BufferedUiRegistry? _owner = owner;
public void Dispose() =>
Interlocked.Exchange(ref _owner, null)?.Remove(registrationId);
}
}