diff --git a/Aetheria.State.Verify/Program.cs b/Aetheria.State.Verify/Program.cs index 83f36c2f9..f8d4dabdf 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -34,6 +34,7 @@ RequireNoRendererLocalDebugPanels(root); RequireMainMenuSettingsCommit(root); RequireMainMenuSettingsShellUsesEveSurface(root); RequireSectorMapZoneDetailsUseEveSurface(root); +RequireRuntimeMenuTabsUseEveSurface(root); RequirePlayerSettingsEveSurface(root); RequireMainMenuContinueRunState(root); RequirePropertiesPanelReadOnlyInspector(root); @@ -500,6 +501,7 @@ Console.WriteLine("Renderer-local debug panels: obsolete uGUI field tester autho Console.WriteLine("Main-menu settings authority: player name, gameplay, and graphics settings return through typed player-settings commits"); Console.WriteLine("Main-menu settings shell: settings/input/audio subpages lower through Eve UI Toolkit surfaces instead of PropertiesPanel buttons"); Console.WriteLine("Sector-map zone details shell: zone inspection lowers through an Eve UI Toolkit surface instead of PropertiesPanel rows"); +Console.WriteLine("Runtime menu tab shell: tab navigation lowers through an Eve UI Toolkit surface instead of MenuTabButton click wiring"); Console.WriteLine("Main-menu Continue authority: Continue selects typed run state instead of a null button"); Console.WriteLine("PropertiesPanel inspector authority: dead generic reflection inspector path is deleted"); Console.WriteLine("Typed behavior metadata authority: live heat/mining/thermotoggle payload kinds stay owned by package metadata"); @@ -2534,6 +2536,58 @@ static void RequireSectorMapZoneDetailsUseEveSurface(string root) } } +static void RequireRuntimeMenuTabsUseEveSurface(string root) +{ + var menuPanelPath = Path.Combine(root, "Assets", "Scripts", "UI", "Menu", "MenuPanel.cs"); + if (!File.Exists(menuPanelPath)) + { + throw new InvalidOperationException("Cannot verify runtime menu tab shell; MenuPanel.cs is missing."); + } + + var source = File.ReadAllText(menuPanelPath); + var requiredSymbols = new[] + { + "MenuTabsSurfaceId", + "RenderTabSurface(", + "HandleTabSurfaceCommand(", + "ResolveTabSurfaceDocument(", + "BuildTabSurfaceDefinition(", + "ResolveVisibleTabs(", + "GetTabCommand(", + "new EveUiToolkitSurfaceLowerer()", + "TabButtons.gameObject.SetActive(false)" + }; + + var missingSymbols = requiredSymbols + .Where(symbol => !source.Contains(symbol, StringComparison.Ordinal)) + .ToArray(); + + if (missingSymbols.Length > 0) + { + throw new InvalidOperationException( + "MenuPanel no longer lowers the runtime tab shell through Eve surfaces: " + + string.Join(", ", missingSymbols)); + } + + var forbiddenSymbols = new[] + { + "tabButton.Button.onClick.AddListener(", + "tabButton.gameObject.SetActive(!tabButton.RequireDock || GameManager.DockedEntity != null);", + "_tabs[MenuTab.Local].gameObject.SetActive(" + }; + + var hits = forbiddenSymbols + .Where(symbol => source.Contains(symbol, StringComparison.Ordinal)) + .ToArray(); + + if (hits.Length > 0) + { + throw new InvalidOperationException( + "MenuPanel still owns tab-shell behavior through the old MenuTabButton path: " + + string.Join(", ", hits)); + } +} + static void RequirePlayerSettingsEveSurface(string root) { var projectorPath = Path.Combine(root, "Aetheria.State", "AetheriaPlayerSettingsSurfaceProjector.cs"); diff --git a/Assets/Scripts/UI/Menu/MenuPanel.cs b/Assets/Scripts/UI/Menu/MenuPanel.cs index fbedf072a..478ee8fb1 100644 --- a/Assets/Scripts/UI/Menu/MenuPanel.cs +++ b/Assets/Scripts/UI/Menu/MenuPanel.cs @@ -5,11 +5,20 @@ using System; using System.Collections; using System.Collections.Generic; -using UniRx.Triggers; +using System.Linq; +using GameCult.Eve.Surface; +using GameCult.Eve.UnityUIToolkit; using UnityEngine; +using UnityEngine.UIElements; public class MenuPanel : MonoBehaviour { + private const string MenuTabsSurfaceType = "surface-state"; + private const string MenuTabsSurfaceSchema = "gamecult.eve.surface.v1"; + private const string MenuTabsSurfaceProviderId = "aetheria"; + private const string MenuTabsSurfaceProviderKind = "runtime.menu"; + private const string MenuTabsSurfaceId = "aetheria.runtime_menu.tabs"; + public ActionGameManager GameManager; public RectTransform TabButtons; public Color ActiveTabColor; @@ -19,37 +28,43 @@ public class MenuPanel : MonoBehaviour private Dictionary _tabs = new Dictionary(); private MenuTabButton _current; + private UIDocument _tabSurfaceDocument; public MenuTab CurrentTab { get; private set; } public void ShowTab(MenuTab tab) { gameObject.SetActive(true); - - if (_current == _tabs[tab]) return; + + if (!_tabs.TryGetValue(tab, out var next)) + return; + + if (_current == next) + { + RenderTabSurface(); + return; + } if(_current != null) { _current.TabContents.SetActive(false); - _current.Text.color = InactiveTabColor; } CurrentTab = tab; - _current = _tabs[tab]; + _current = next; _current.TabContents.SetActive(true); - _current.Text.color = ActiveTabColor; + RenderTabSurface(); TabChanged?.Invoke(tab); } private void OnEnable() { - foreach (var tabButton in _tabs.Values) - { - tabButton.gameObject.SetActive(!tabButton.RequireDock || GameManager.DockedEntity != null); - } - _tabs[MenuTab.Local].gameObject.SetActive((GameManager.DockedEntity as OrbitalEntity)?.Story!=null); + if (TabButtons != null) + TabButtons.gameObject.SetActive(false); + + RenderTabSurface(); } private void Awake() @@ -57,12 +72,202 @@ public class MenuPanel : MonoBehaviour foreach (var tabButton in TabButtons.GetComponentsInChildren()) { tabButton.TabContents.SetActive(false); - tabButton.Text.color = InactiveTabColor; _tabs.Add(tabButton.Tab, tabButton); - tabButton.Button.onClick.AddListener(() => ShowTab(tabButton.Tab)); } } + private void OnDisable() + { + HideTabSurface(); + } + + private void OnDestroy() + { + if (_tabSurfaceDocument != null) + { + Destroy(_tabSurfaceDocument.gameObject); + _tabSurfaceDocument = null; + } + } + + private void RenderTabSurface() + { + if (_tabs.Count == 0) + return; + + if (TabButtons != null) + TabButtons.gameObject.SetActive(false); + + var document = ResolveTabSurfaceDocument(); + document.gameObject.SetActive(true); + + var root = document.rootVisualElement; + root.Clear(); + root.style.flexGrow = 1; + root.style.position = Position.Absolute; + root.style.left = 0; + root.style.top = 0; + root.style.right = 0; + root.style.bottom = 0; + root.style.alignItems = Align.Center; + root.style.justifyContent = Justify.FlexStart; + root.style.paddingTop = 16; + root.pickingMode = PickingMode.Ignore; + + var shell = new VisualElement(); + shell.style.minWidth = 420; + shell.style.maxWidth = 760; + shell.style.backgroundColor = new Color(0.08f, 0.1f, 0.14f, 0.94f); + shell.style.borderTopLeftRadius = 8; + shell.style.borderTopRightRadius = 8; + shell.style.borderBottomLeftRadius = 8; + shell.style.borderBottomRightRadius = 8; + shell.style.paddingLeft = 16; + shell.style.paddingRight = 16; + shell.style.paddingTop = 12; + shell.style.paddingBottom = 12; + shell.style.borderLeftWidth = 1; + shell.style.borderRightWidth = 1; + shell.style.borderTopWidth = 1; + shell.style.borderBottomWidth = 1; + shell.style.borderLeftColor = new Color(0.3f, 0.47f, 0.71f, 0.8f); + shell.style.borderRightColor = new Color(0.3f, 0.47f, 0.71f, 0.8f); + shell.style.borderTopColor = new Color(0.3f, 0.47f, 0.71f, 0.8f); + shell.style.borderBottomColor = new Color(0.3f, 0.47f, 0.71f, 0.8f); + shell.pickingMode = PickingMode.Position; + root.Add(shell); + + var lowerer = new EveUiToolkitSurfaceLowerer(); + shell.Add(lowerer.Lower(BuildTabSurfaceDefinition(), HandleTabSurfaceCommand)); + } + + private void HandleTabSurfaceCommand(EveSurfaceCommandRequest request) + { + foreach (var tab in _tabs.Keys) + { + if (string.Equals(request.Command, GetTabCommand(tab), StringComparison.Ordinal)) + { + ShowTab(tab); + return; + } + } + + Debug.LogWarning($"Unknown runtime menu tab command: {request.Command}"); + } + + private void HideTabSurface() + { + if (_tabSurfaceDocument == null) + return; + + _tabSurfaceDocument.rootVisualElement.Clear(); + _tabSurfaceDocument.gameObject.SetActive(false); + } + + private UIDocument ResolveTabSurfaceDocument() + { + if (_tabSurfaceDocument != null) + return _tabSurfaceDocument; + + var host = new GameObject("Aetheria Runtime Menu Tabs Surface"); + host.transform.SetParent(transform, false); + var document = host.AddComponent(); + document.sortingOrder = 1000; + host.SetActive(false); + _tabSurfaceDocument = document; + return document; + } + + private EveSurfaceDocument BuildTabSurfaceDefinition() + { + var visibleTabs = ResolveVisibleTabs(); + var commands = visibleTabs + .Select(tabButton => new EveCommandTemplate( + GetTabCommand(tabButton.Tab), + tabButton.Text?.text ?? tabButton.Tab.ToString(), + "unity-uitoolkit")) + .ToArray(); + + var buttons = visibleTabs + .Select(tabButton => + { + var label = tabButton.Text?.text ?? tabButton.Tab.ToString(); + if (tabButton.Tab == CurrentTab) + label = $"{label} *"; + + return Button( + $"{MenuTabsSurfaceId}.{tabButton.Tab.ToString().ToLowerInvariant()}", + label, + GetTabCommand(tabButton.Tab)); + }) + .ToArray(); + + return new EveSurfaceDocument( + MenuTabsSurfaceType, + MenuTabsSurfaceSchema, + MenuTabsSurfaceProviderId, + MenuTabsSurfaceProviderKind, + "Runtime Menu Tabs", + version: 1, + DateTime.UtcNow.ToString("O"), + new EveSurfaceTree( + MenuTabsSurfaceId, + Node( + $"{MenuTabsSurfaceId}.root", + "surface", + Array.Empty<(string Key, string Value)>(), + Text( + $"{MenuTabsSurfaceId}.current", + $"Current: {CurrentTab}"), + ButtonRow($"{MenuTabsSurfaceId}.tabs", buttons)), + Array.Empty()), + commands); + } + + private MenuTabButton[] ResolveVisibleTabs() + { + return _tabs.Values + .Where(tabButton => !tabButton.RequireDock || GameManager.DockedEntity != null) + .Where(tabButton => tabButton.Tab != MenuTab.Local || (GameManager.DockedEntity as OrbitalEntity)?.Story != null) + .OrderBy(tabButton => (int)tabButton.Tab) + .ToArray(); + } + + private static string GetTabCommand(MenuTab tab) + { + return $"aetheria.runtime_menu.tab.{tab.ToString().ToLowerInvariant()}"; + } + + private static EveSurfaceComponent Text(string id, string value) + { + return Node(id, "text", new[] { ("value", value) }); + } + + private static EveSurfaceComponent Button(string id, string label, string command) + { + return Node(id, "control.button", new[] { ("label", label), ("command", command) }); + } + + private static EveSurfaceComponent ButtonRow( + string id, + params EveSurfaceComponent[] children) + { + return Node(id, "row", Array.Empty<(string Key, string Value)>(), children); + } + + private static EveSurfaceComponent Node( + string id, + string kind, + IEnumerable<(string Key, string Value)> props, + params EveSurfaceComponent[] children) + { + return new EveSurfaceComponent( + id, + kind, + props.ToDictionary(prop => prop.Key, prop => prop.Value, StringComparer.Ordinal), + children ?? Array.Empty()); + } + // void Start() // { // ShowTab(MenuTab.Inventory); @@ -76,4 +281,4 @@ public enum MenuTab Trade, Galaxy, Local -} \ No newline at end of file +} diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index 37d42dd83..b200fcec2 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -117,7 +117,9 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. old `PropertiesPanel` button shell, even though typed rebinding controls and typed audio controls are still future work. Sector-map zone inspection now lowers through a local Eve/UI Toolkit surface as well instead of rebuilding - `PropertiesPanel` rows on each zone click. + `PropertiesPanel` rows on each zone click. The runtime in-game menu tab strip + now lowers through a local Eve/UI Toolkit surface instead of wiring old + `MenuTabButton` clicks. - The combat schematic HUD uses typed runtime catalog weapon facets for its static weapon icon strip; missing typed weapon facets no longer fall back to legacy `WeaponItemData`. Schematic weapon-row selection now uses typed @@ -1552,6 +1554,9 @@ First Aetheria surfaces to publish: Toolkit surface. `SectorRenderer` still owns reveal/camera behavior, but zone inspection no longer rebuilds `PropertiesPanel` rows when the player clicks a zone. + - Done: replace the runtime in-game menu tab strip with an Eve-owned UI + Toolkit surface. `MenuPanel` still owns tab-content activation, but old + `MenuTabButton` click wiring no longer owns tab navigation. - Done: move the shared `org.gamecult.eve.surface` and `org.gamecult.eve.unity-uitoolkit` packages into the Eve repo and retarget Aetheria to import them from there instead of carrying local staged @@ -1913,6 +1918,9 @@ First Aetheria surfaces to publish: - Sector-map zone inspection now lowers through a shared UI Toolkit `UIDocument` host and a local Eve surface document instead of rebuilding `PropertiesPanel.AddProperty(...)` rows on each zone click. +- Runtime in-game menu tab navigation now lowers through a shared UI Toolkit + `UIDocument` host and a local Eve surface document instead of wiring + `MenuTabButton` click handlers. - Keyboard display layout parsing is no longer JSON-backed: `InputDisplayLayout` builds its static ANSI-104 display projection from typed `InputLayout` rows/columns. The dead commented Ink `ToJson` write path and