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); } } } }