// Assets/_Project/Scripts/Dev/DebugConsoleUI.cs using System.Collections.Generic; using UnityEngine; using UnityEngine.UIElements; namespace TD.Dev { /// /// Builds and draws 's VisualElement tree: the input bar and /// its autocomplete list. Owns presentation only — element construction, styling, and /// row rendering — no command grammar, keybinding, or suggestion-selection logic; that /// stays in , which drives this class through its public /// members and reads/writes directly for focus and value. /// internal sealed class DebugConsoleUI { // Beyond this many candidates the list is truncated with a "+N more" row so a // large DraftPool can't grow the console bar off the top of the screen. private const int MaxVisibleSuggestions = 12; /// One row of the autocomplete list: a candidate token plus an optional dim hint. public readonly struct SuggestionRow { public readonly string Text; public readonly string Hint; public SuggestionRow(string text, string hint) { Text = text; Hint = hint; } } public TextField CommandInput { get; } private readonly VisualElement consoleContainer; private readonly VisualElement suggestionList; public DebugConsoleUI(VisualElement uiRoot) { // Top-anchored bar, closed by default. Distinct position from the HUD's // bottom-left chat panel so the two never overlap. consoleContainer = new VisualElement(); consoleContainer.pickingMode = PickingMode.Ignore; consoleContainer.style.position = Position.Absolute; consoleContainer.style.left = 0; consoleContainer.style.right = 0; consoleContainer.style.top = 0; consoleContainer.style.paddingTop = 8; consoleContainer.style.paddingBottom = 8; consoleContainer.style.paddingLeft = 12; consoleContainer.style.paddingRight = 12; // Fully opaque: the console is a text-entry overlay that has to stay readable // over the level, the HUD, and whatever else is on screen. consoleContainer.style.backgroundColor = new Color(0f, 0f, 0f, 1f); consoleContainer.style.display = DisplayStyle.None; CommandInput = new TextField(); CommandInput.style.minHeight = 28; CommandInput.style.width = Length.Percent(100); CommandInput.maxLength = 200; CommandInput.isDelayed = false; StyleConsoleInput(CommandInput); suggestionList = new VisualElement(); suggestionList.pickingMode = PickingMode.Ignore; suggestionList.style.flexDirection = FlexDirection.Column; suggestionList.style.marginTop = 4; suggestionList.style.display = DisplayStyle.None; consoleContainer.Add(CommandInput); consoleContainer.Add(suggestionList); uiRoot.Add(consoleContainer); } // Mirrors HUDController.StyleChatInputDark — dark background, white text, // a thin border, and inner padding so ascenders/descenders don't clip. // Green border (vs. chat's neutral gray) as a quick visual "this is the // debug console, not chat" cue. private static void StyleConsoleInput(TextField field) { var darkBg = new Color(0.05f, 0.05f, 0.05f, 1f); var borderClr = new Color(0.35f, 0.75f, 0.35f); field.style.backgroundColor = darkBg; field.style.color = Color.white; field.style.borderTopWidth = field.style.borderBottomWidth = field.style.borderLeftWidth = field.style.borderRightWidth = 1; field.style.borderTopColor = field.style.borderBottomColor = field.style.borderLeftColor = field.style.borderRightColor = borderClr; var inner = field.Q("unity-text-input"); if (inner != null) { inner.style.backgroundColor = darkBg; inner.style.color = Color.white; inner.style.paddingTop = 4; inner.style.paddingBottom = 4; inner.style.paddingLeft = 6; inner.style.paddingRight = 6; } } // One autocomplete row: the candidate token plus an optional dim hint. Rows are // pickingMode Ignore and keyboard-driven only — nothing here is clickable, which // avoids the recursive picking-mode problem documented in HUDController's chat // panel (clicks falling through to the game world via a ScrollView's internal // wrapper element). private static VisualElement BuildSuggestionRow(string text, string hint, bool selected) { var row = new VisualElement(); row.pickingMode = PickingMode.Ignore; row.style.flexDirection = FlexDirection.Row; row.style.minHeight = 20; row.style.paddingLeft = 6; row.style.paddingRight = 6; row.style.backgroundColor = selected ? new Color(0.16f, 0.38f, 0.16f, 1f) // matches the green console cue : new Color(0.05f, 0.05f, 0.05f, 1f); var label = new Label(text); label.pickingMode = PickingMode.Ignore; label.style.color = Color.white; label.style.unityFontStyleAndWeight = selected ? FontStyle.Bold : FontStyle.Normal; row.Add(label); if (!string.IsNullOrEmpty(hint)) { var hintLabel = new Label(hint); hintLabel.pickingMode = PickingMode.Ignore; hintLabel.style.color = new Color(0.6f, 0.6f, 0.6f); hintLabel.style.marginLeft = 12; row.Add(hintLabel); } return row; } public void Show() { consoleContainer.style.display = DisplayStyle.Flex; consoleContainer.pickingMode = PickingMode.Position; } public void Hide() { consoleContainer.style.display = DisplayStyle.None; consoleContainer.pickingMode = PickingMode.Ignore; } // Windows the list around selectedIndex so arrowing past MaxVisibleSuggestions // keeps the selected row on screen instead of scrolling it out of the truncated view. public void RenderSuggestions(List rows, int selectedIndex) { suggestionList.Clear(); if (rows == null || rows.Count == 0) { suggestionList.style.display = DisplayStyle.None; return; } suggestionList.style.display = DisplayStyle.Flex; int start = rows.Count > MaxVisibleSuggestions ? Mathf.Clamp(selectedIndex - MaxVisibleSuggestions / 2, 0, rows.Count - MaxVisibleSuggestions) : 0; int end = Mathf.Min(start + MaxVisibleSuggestions, rows.Count); for (int i = start; i < end; i++) suggestionList.Add(BuildSuggestionRow(rows[i].Text, rows[i].Hint, i == selectedIndex)); int hidden = rows.Count - (end - start); if (hidden > 0) suggestionList.Add(BuildSuggestionRow($"+{hidden} more", null, false)); } public void ClearSuggestions() { suggestionList.Clear(); suggestionList.style.display = DisplayStyle.None; } } }