using System;
using System.Collections.Generic;
using Uigenx.Actions;
using Uigenx.Binding;
using UnityEngine;
using UnityEngine.UIElements;
namespace Uigenx
{
/// One entry of the screen stack.
public sealed class UgxOpenScreen
{
public string Key { get; internal set; }
public UgxLayer Layer { get; internal set; }
public UgxScreen Screen { get; internal set; }
public VisualElement Container { get; internal set; }
internal UgxModalFocusTrap Trap { get; set; }
}
///
/// Opens and closes screens on five layers, each with its own panel and sort order. The Modal
/// layer additionally blocks picking below it and traps focus.
///
[AddComponentMenu("UI Creator/Screen Manager")]
[DisallowMultipleComponent]
public sealed class UgxScreenManager : MonoBehaviour
{
[SerializeField] UgxScreenRegistry registry;
[Tooltip("Panel used for a layer. Empty entries clone the default panel from UgxSettings.")]
[SerializeField] PanelSettings[] layerPanels = new PanelSettings[UgxLayers.Count];
[Tooltip("Object implementing IUgxDataSource that every screen binds to.")]
[SerializeField] UnityEngine.Object dataSourceObject;
readonly List _stack = new List(4);
readonly VisualElement[] _surfaces = new VisualElement[UgxLayers.Count];
readonly UIDocument[] _documents = new UIDocument[UgxLayers.Count];
readonly PanelSettings[] _ownedPanels = new PanelSettings[UgxLayers.Count];
IUgxDataSource _dataSource;
public static UgxScreenManager Instance { get; private set; }
/// Raised after any push, pop or replace. The bridge uses it to send `screen.state`.
public event Action StackChanged;
public IReadOnlyList Stack => _stack;
public UgxScreenRegistry Registry
{
get => registry != null ? registry : UgxScreenRegistry.Active;
set => registry = value;
}
public IUgxDataSource DataSource
{
get => _dataSource ?? (_dataSource = dataSourceObject as IUgxDataSource);
set => _dataSource = value;
}
void Awake()
{
if (Instance != null && Instance != this)
{
Debug.LogWarning("[ugx] a second UgxScreenManager was enabled, the first one keeps the singleton",
this);
}
else
{
Instance = this;
}
if (dataSourceObject != null && !(dataSourceObject is IUgxDataSource))
{
Debug.LogError("[ugx] " + dataSourceObject.name + " does not implement IUgxDataSource", this);
dataSourceObject = null;
}
}
void OnDestroy()
{
CloseAll();
for (int i = 0; i < _ownedPanels.Length; i++)
{
if (_ownedPanels[i] == null) continue;
Destroy(_ownedPanels[i]);
_ownedPanels[i] = null;
}
if (Instance == this) Instance = null;
}
public bool IsOpen(string key)
{
for (int i = 0; i < _stack.Count; i++)
if (_stack[i].Key == key) return true;
return false;
}
public UgxOpenScreen Top => _stack.Count > 0 ? _stack[_stack.Count - 1] : null;
public UgxOpenScreen Push(string key) => Push(key, null);
public UgxOpenScreen Push(string key, UgxLayer layer) => Push(key, (UgxLayer?)layer);
/// Opens a screen. A null layer uses the layer recorded in the registry.
public UgxOpenScreen Push(string key, UgxLayer? layer)
{
var current = Registry;
if (current == null)
{
Debug.LogError("[ugx] no screen registry is available, cannot open '" + key + "'", this);
return null;
}
var entry = current.GetOrThrow(key);
var target = layer ?? entry.Layer;
var surface = GetOrCreateSurface(target, entry.Panel);
if (surface == null) return null;
// Nothing joins the stack or the panel until the screen is attached and visible: a
// generated screen that asks for an element its UXML no longer has throws out of
// Attach, and a half open entry would leave an orphan container on screen forever.
var screen = UgxScreens.CreateOrThrow(key);
var container = BuildContainer(entry);
surface.Add(container);
try
{
screen.Attach(container, DataSource);
screen.Show();
}
catch (Exception)
{
screen.Detach();
container.RemoveFromHierarchy();
throw;
}
var open = new UgxOpenScreen { Key = key, Layer = target, Screen = screen, Container = container };
_stack.Add(open);
if (target == UgxLayer.Modal)
{
open.Trap = UgxModalFocusTrap.Install(surface, container);
UgxActionBus.Emit(UgxSystemActions.ModalOpened,
UgxActionPayload.Of(UgxSystemActions.ScreenKey, key,
UgxSystemActions.LayerKey, target.ToWireName()));
}
RaiseStackChanged();
return open;
}
public bool Pop()
{
if (_stack.Count == 0) return false;
Close(_stack[_stack.Count - 1]);
_stack.RemoveAt(_stack.Count - 1);
RaiseStackChanged();
return true;
}
/// Closes the top screen and opens another one on the same layer.
public UgxOpenScreen Replace(string key)
{
var top = Top;
var layer = top?.Layer;
if (top != null)
{
Close(top);
_stack.RemoveAt(_stack.Count - 1);
}
return Push(key, layer);
}
public bool Close(string key)
{
for (int i = _stack.Count - 1; i >= 0; i--)
{
if (_stack[i].Key != key) continue;
Close(_stack[i]);
_stack.RemoveAt(i);
RaiseStackChanged();
return true;
}
return false;
}
public void CloseAll()
{
if (_stack.Count == 0) return;
for (int i = _stack.Count - 1; i >= 0; i--) Close(_stack[i]);
_stack.Clear();
RaiseStackChanged();
}
///
/// Uses an existing element as a layer surface instead of creating a UIDocument. Lets a game
/// that already owns its documents plug them in, and lets tests run without panels.
///
public void InstallLayerSurface(UgxLayer layer, VisualElement surface)
{
_surfaces[(int)layer] = surface;
}
public VisualElement GetSurface(UgxLayer layer) => _surfaces[(int)layer];
public UIDocument GetDocument(UgxLayer layer) => _documents[(int)layer];
VisualElement GetOrCreateSurface(UgxLayer layer, PanelSettings panelOverride)
{
int index = (int)layer;
if (_surfaces[index] != null) return _surfaces[index];
var panel = panelOverride != null ? panelOverride : ResolvePanel(layer);
if (panel == null)
{
Debug.LogError("[ugx] layer " + layer + " has no PanelSettings. Assign one on the manager or set " +
"a default in UgxSettings.", this);
return null;
}
var host = new GameObject("UgxLayer." + layer);
host.transform.SetParent(transform, false);
var document = host.AddComponent();
document.panelSettings = panel;
document.sortingOrder = UgxLayers.SortOrder(layer);
_documents[index] = document;
var root = document.rootVisualElement;
root.pickingMode = PickingMode.Ignore;
root.style.flexGrow = 1f;
_surfaces[index] = root;
return root;
}
PanelSettings ResolvePanel(UgxLayer layer)
{
int index = (int)layer;
if (layerPanels != null && index < layerPanels.Length && layerPanels[index] != null)
return layerPanels[index];
if (_ownedPanels[index] != null) return _ownedPanels[index];
var template = UgxSettings.Instance.DefaultPanelSettings;
if (template == null) return null;
// Each layer needs its own sort order, so the shared default is cloned per layer and
// destroyed with the manager.
var clone = Instantiate(template);
clone.name = template.name + " (" + layer + ")";
clone.sortingOrder = UgxLayers.SortOrder(layer);
clone.hideFlags = HideFlags.HideAndDontSave;
_ownedPanels[index] = clone;
return clone;
}
static VisualElement BuildContainer(UgxScreenEntry entry)
{
VisualElement container;
if (entry.Uxml != null)
{
container = entry.Uxml.Instantiate();
}
else
{
Debug.LogWarning("[ugx] screen '" + entry.Key + "' has no VisualTreeAsset, opening an empty root");
container = new VisualElement();
}
container.name = entry.Key;
container.style.position = Position.Absolute;
container.style.left = 0f;
container.style.top = 0f;
container.style.right = 0f;
container.style.bottom = 0f;
var styles = entry.Styles;
if (styles != null)
foreach (var sheet in styles)
if (sheet != null)
container.styleSheets.Add(sheet);
return container;
}
void Close(UgxOpenScreen open)
{
open.Trap?.Dispose();
open.Trap = null;
open.Screen?.Detach();
open.Container?.RemoveFromHierarchy();
if (open.Layer == UgxLayer.Modal)
{
UgxActionBus.Emit(UgxSystemActions.ModalClosed,
UgxActionPayload.Of(UgxSystemActions.ScreenKey, open.Key,
UgxSystemActions.LayerKey, open.Layer.ToWireName()));
}
}
void RaiseStackChanged()
{
// Stack listeners are consumer and bridge code.
try
{
StackChanged?.Invoke();
}
catch (Exception e)
{
Debug.LogError("[ugx] a screen stack listener threw: " + e);
}
}
}
}