using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using FlowScope.Resources; using UnityEngine; namespace FlowScope.UI { public sealed class UIManager { private readonly IResourceService _resources; private readonly IUIPreloadService _preloadService; private readonly Dictionary _layers = new(); public UIManager(IResourceService resources, IUIPreloadService preloadService = null) { _resources = resources ?? throw new ArgumentNullException(nameof(resources)); _preloadService = preloadService; } public void RegisterLayer( string name, Canvas canvas, int sortOrder, PanelStrategy strategy = PanelStrategy.Destroy) { if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentException("Layer name is required.", nameof(name)); } if (canvas == null) { throw new ArgumentNullException(nameof(canvas)); } _layers[name] = new UILayer(name, canvas, sortOrder, strategy); } public async Task OpenAsync( TViewModel viewModel, CancellationToken cancellationToken) where TPanel : UIPanelBase { cancellationToken.ThrowIfCancellationRequested(); var attribute = GetPanelAttribute(typeof(TPanel)); if (!_layers.TryGetValue(attribute.Layer, out var layer)) { throw new InvalidOperationException($"UI layer is not registered: {attribute.Layer}"); } var strategy = attribute.OverrideStrategy ?? layer.Strategy; if (strategy == PanelStrategy.Cache && layer.TryGetCached(typeof(TPanel), out var cached) && !cached.IsOpen) { return ReopenCached(cached, layer, viewModel); } var path = ResolvePath(typeof(TPanel), attribute); var handle = await LoadPanelHandleAsync(path, cancellationToken); if (handle == null || handle.Asset == null) { handle?.Dispose(); throw new InvalidOperationException($"UI panel resource is missing: {path}"); } GameObject instance = null; var ownsHandle = !IsPreloadedHandle(handle); try { instance = UnityEngine.Object.Instantiate(handle.Asset, layer.Canvas.transform, false); var panel = instance.GetComponent(); if (panel == null) { throw new InvalidOperationException( $"UI panel prefab '{path}' does not contain component {typeof(TPanel).Name}."); } panel.Bind(viewModel); instance.SetActive(true); var recordHandle = ownsHandle ? (IDisposable)handle : RetainedPanelHandle.Instance; var record = new UIPanelRecord(typeof(TPanel), layer.Name, panel, recordHandle, strategy); if (strategy == PanelStrategy.Cache) { layer.Cache(record); } layer.Push(record); return panel; } catch { if (instance != null) { UnityEngine.Object.Destroy(instance); } if (ownsHandle) { handle.Dispose(); } throw; } } public void Close() { var panelType = typeof(TPanel); foreach (var layer in _layers.Values) { if (!layer.ContainsOpen(panelType, out var record)) { continue; } if (!ReferenceEquals(layer.Peek(), record)) { throw new InvalidOperationException( $"Cannot close non-top UI panel: {panelType.Name}"); } CloseTop(layer); return; } } public void CloseLayer(string layerName) { if (!_layers.TryGetValue(layerName, out var layer)) { throw new InvalidOperationException($"UI layer is not registered: {layerName}"); } while (layer.Count > 0) { CloseTop(layer); } } public void CloseAll() { foreach (var layer in _layers.Values) { while (layer.Count > 0) { CloseTop(layer); } } } public bool IsOpen() { var panelType = typeof(TPanel); foreach (var layer in _layers.Values) { if (layer.ContainsOpen(panelType, out _)) { return true; } } return false; } private static TPanel ReopenCached( UIPanelRecord record, UILayer layer, TViewModel viewModel) where TPanel : UIPanelBase { var panel = (TPanel)record.Panel; panel.Bind(viewModel); panel.gameObject.SetActive(true); layer.Push(record); return panel; } private static void CloseTop(UILayer layer) { var record = layer.Pop(); record.Panel.Unbind(); if (record.Strategy == PanelStrategy.Cache) { record.Panel.gameObject.SetActive(false); return; } layer.RemoveCached(record.PanelType); UnityEngine.Object.Destroy(record.Panel.gameObject); record.Handle.Dispose(); } private async Task> LoadPanelHandleAsync( string path, CancellationToken cancellationToken) { if (_preloadService != null && _preloadService.TryGetPreloadedHandle(out var preloadedHandle)) { return preloadedHandle; } return await _resources.LoadAsync(path, cancellationToken); } private bool IsPreloadedHandle(IResourceHandle handle) { return _preloadService != null && _preloadService.TryGetPreloadedHandle(out var preloadedHandle) && ReferenceEquals(preloadedHandle, handle); } internal static UIPanelAttribute GetPanelAttribute(Type panelType) { var attribute = (UIPanelAttribute)Attribute.GetCustomAttribute( panelType, typeof(UIPanelAttribute)); if (attribute == null) { throw new InvalidOperationException( $"UI panel must declare UIPanelAttribute: {panelType.Name}"); } if (string.IsNullOrWhiteSpace(attribute.Layer)) { throw new InvalidOperationException( $"UI panel layer is required: {panelType.Name}"); } return attribute; } internal static string ResolvePath(Type panelType, UIPanelAttribute attribute) { if (!string.IsNullOrWhiteSpace(attribute.Path)) { return attribute.Path; } var name = panelType.Name; const string suffix = "Panel"; if (name.EndsWith(suffix, StringComparison.Ordinal)) { name = name.Substring(0, name.Length - suffix.Length); } return $"UI/{name}/Prefab"; } private sealed class RetainedPanelHandle : IDisposable { public static readonly RetainedPanelHandle Instance = new(); public void Dispose() { } } } }