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