127 lines
3.8 KiB
C#
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);
|
|
}
|
|
}
|