Files
FlowScope/Packages/com.flowscope.gamecore/Runtime/UI/UIManager.cs
2026-06-04 14:54:49 +08:00

267 lines
8.2 KiB
C#

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<string, UILayer> _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<TPanel> OpenAsync<TPanel, TViewModel>(
TViewModel viewModel,
CancellationToken cancellationToken)
where TPanel : UIPanelBase<TViewModel>
{
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<TPanel, TViewModel>(cached, layer, viewModel);
}
var path = ResolvePath(typeof(TPanel), attribute);
var handle = await LoadPanelHandleAsync<TPanel>(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<TPanel>(handle);
try
{
instance = UnityEngine.Object.Instantiate(handle.Asset, layer.Canvas.transform, false);
var panel = instance.GetComponent<TPanel>();
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<TPanel>()
{
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<TPanel>()
{
var panelType = typeof(TPanel);
foreach (var layer in _layers.Values)
{
if (layer.ContainsOpen(panelType, out _))
{
return true;
}
}
return false;
}
private static TPanel ReopenCached<TPanel, TViewModel>(
UIPanelRecord record,
UILayer layer,
TViewModel viewModel)
where TPanel : UIPanelBase<TViewModel>
{
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<IResourceHandle<GameObject>> LoadPanelHandleAsync<TPanel>(
string path,
CancellationToken cancellationToken)
{
if (_preloadService != null &&
_preloadService.TryGetPreloadedHandle<TPanel>(out var preloadedHandle))
{
return preloadedHandle;
}
return await _resources.LoadAsync<GameObject>(path, cancellationToken);
}
private bool IsPreloadedHandle<TPanel>(IResourceHandle<GameObject> handle)
{
return _preloadService != null &&
_preloadService.TryGetPreloadedHandle<TPanel>(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()
{
}
}
}
}