267 lines
8.2 KiB
C#
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()
|
|
{
|
|
}
|
|
}
|
|
}
|
|
}
|