using System.Collections.Generic; using AcDream.App.UI; using AcDream.Plugin.Abstractions; namespace AcDream.App.Plugins; /// /// Buffers plugin calls (which run in /// Program.cs before the GL window opens) until GameWindow drains them into the /// UiHost tree after construction. /// 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 _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); } /// Returns each not-yet-drained active registration once. public IReadOnlyList Drain() { lock (_gate) { var pending = new List(_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); } }