Skip to content

Commit da2bf2b

Browse files
committed
Add debug UI window with debug tools
Add a debug UI system: - UIBuilder: core layout helpers with window management and input locking support - DebugWindow: main debug panel with draggable window - TexturesUnderCursor: live display of textures under the mouse with tooltips showing texture details - DumpTexturesButton, DumpSkinTexturesButton: export loaded textures and skin textures to disk for inspection - ReloadTexturesButton: hot-reload replacement textures - ToggleDebugButton: toggle debug overlay rendering - CloseButton, TextAlignment: UI utility components
1 parent d2c00d9 commit da2bf2b

10 files changed

Lines changed: 945 additions & 0 deletions

src/HUDReplacer/UI/CloseButton.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using UnityEngine;
2+
using UnityEngine.UI;
3+
4+
namespace HUDReplacer.UI;
5+
6+
internal class CloseButton : MonoBehaviour
7+
{
8+
public Button button;
9+
public GameObject target;
10+
11+
void Start()
12+
{
13+
button.onClick.AddListener(OnClick);
14+
}
15+
16+
void OnClick()
17+
{
18+
HUDReplacerDebug.Instance?.OnWindowClosed();
19+
Destroy(target);
20+
}
21+
}

src/HUDReplacer/UI/DebugWindow.cs

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
using System.IO;
2+
using System.Text;
3+
using TMPro;
4+
using UnityEngine;
5+
using UnityEngine.UI;
6+
7+
namespace HUDReplacer.UI;
8+
9+
internal class DebugWindow : MonoBehaviour
10+
{
11+
private const string WindowTitle = "HUDReplacer";
12+
13+
private static GameObject _prefab;
14+
private static DebugWindow _instance;
15+
16+
internal static DebugWindow Instance => _instance;
17+
18+
internal static void Toggle()
19+
{
20+
if (_instance == null)
21+
{
22+
if (_prefab == null)
23+
_prefab = BuildPrefab();
24+
25+
var go = Object.Instantiate(_prefab, MainCanvasUtil.MainCanvas.transform);
26+
_instance = go.GetComponent<DebugWindow>();
27+
go.SetActive(true);
28+
}
29+
else
30+
{
31+
_instance.gameObject.SetActive(!_instance.gameObject.activeSelf);
32+
}
33+
}
34+
35+
private static GameObject BuildPrefab()
36+
{
37+
// Root window
38+
var windowGo = UIBuilder.CreateWindow(
39+
null,
40+
"HUDReplacerDebugWindow",
41+
new Vector2(100, -100),
42+
new Vector2(400, 300)
43+
);
44+
45+
// Let the window grow vertically to fit content
46+
var windowFitter = windowGo.AddOrGetComponent<ContentSizeFitter>();
47+
windowFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
48+
49+
// Title bar with drag support
50+
var titleBarGo = UIBuilder.CreateTitleBar(windowGo.transform, WindowTitle);
51+
52+
// Close button
53+
var closeGo = UIBuilder.CreateButton(titleBarGo.transform, "X", fontSize: 12);
54+
var closeLE = closeGo.AddOrGetComponent<LayoutElement>();
55+
closeLE.preferredWidth = 24;
56+
closeLE.preferredHeight = 24;
57+
closeLE.flexibleWidth = 0;
58+
59+
// Content area
60+
var contentGo = UIBuilder.CreateVerticalLayout(windowGo.transform, "Content");
61+
var contentLE = contentGo.AddComponent<LayoutElement>();
62+
contentLE.flexibleHeight = 1;
63+
contentLE.flexibleWidth = 1;
64+
65+
// Toggle Debug button
66+
var toggleRow = CreateButtonRow(contentGo.transform);
67+
var toggleGo = ToggleDebugButton.Create(toggleRow.transform);
68+
toggleGo.AddOrGetComponent<LayoutElement>().flexibleWidth = 1;
69+
UIBuilder.CreateHelpButton(
70+
toggleRow.transform,
71+
"Enable debug keyboard shortcuts:\n"
72+
+ "D - Dump textures under cursor\n"
73+
+ "E - Dump all loaded textures\n"
74+
+ "Q - Reload textures"
75+
);
76+
77+
// Reload Textures button
78+
var reloadRow = CreateButtonRow(contentGo.transform);
79+
var reloadGo = ReloadTexturesButton.Create(reloadRow.transform);
80+
reloadGo.AddOrGetComponent<LayoutElement>().flexibleWidth = 1;
81+
UIBuilder.CreateHelpButton(
82+
reloadRow.transform,
83+
"Reload all replacement textures and re-apply them."
84+
);
85+
86+
// Dump Textures button
87+
var dumpRow = CreateButtonRow(contentGo.transform);
88+
var dumpTexturesGo = UIBuilder.CreateButton(dumpRow.transform, "Dump Loaded Textures");
89+
dumpTexturesGo.AddOrGetComponent<LayoutElement>().flexibleWidth = 1;
90+
var dumpTexturesBtn = dumpTexturesGo.AddComponent<DumpTexturesButton>();
91+
dumpTexturesBtn.button = dumpTexturesGo.GetComponent<Button>();
92+
UIBuilder.CreateHelpButton(
93+
dumpRow.transform,
94+
"Dump the names and sizes of all loaded textures to KSP.log."
95+
);
96+
97+
// Dump Skin Textures button
98+
var skinRow = CreateButtonRow(contentGo.transform);
99+
var skinTexturesGo = UIBuilder.CreateButton(skinRow.transform, "Dump IMGUI Skin Textures");
100+
skinTexturesGo.AddOrGetComponent<LayoutElement>().flexibleWidth = 1;
101+
var skinTexturesBtn = skinTexturesGo.AddComponent<DumpSkinTexturesButton>();
102+
skinTexturesBtn.button = skinTexturesGo.GetComponent<Button>();
103+
UIBuilder.CreateHelpButton(
104+
skinRow.transform,
105+
"Dump names and sizes of all textures used by the KSP IMGUI skin.\nOverride these to style <i>all</i> IMGUI UIs created by mods or KSP."
106+
);
107+
108+
// Textures under cursor panel
109+
TexturesUnderCursor.Create(contentGo.transform);
110+
111+
// Attach the DebugWindow component
112+
windowGo.AddComponent<DebugWindow>();
113+
114+
// Wire close button to destroy the window
115+
var closeButton = closeGo.AddComponent<CloseButton>();
116+
closeButton.button = closeGo.GetComponent<Button>();
117+
closeButton.target = windowGo;
118+
119+
return windowGo;
120+
}
121+
122+
private static GameObject CreateButtonRow(Transform parent)
123+
{
124+
var row = UIBuilder.CreateHorizontalLayout(
125+
parent,
126+
"ButtonRow",
127+
padding: new RectOffset(0, 0, 0, 0),
128+
spacing: 4
129+
);
130+
var layout = row.GetComponent<HorizontalLayoutGroup>();
131+
layout.childForceExpandHeight = false;
132+
layout.childAlignment = TextAnchor.MiddleLeft;
133+
return row;
134+
}
135+
136+
internal static void DumpWindow()
137+
{
138+
if (_instance == null)
139+
{
140+
Debug.Log("HUDReplacer: No active debug window to dump.");
141+
return;
142+
}
143+
144+
var sb = new StringBuilder();
145+
DumpGameObject(sb, _instance.gameObject, 0);
146+
147+
var path = Path.Combine(KSPUtil.ApplicationRootPath, "Logs/HUDReplacer");
148+
Directory.CreateDirectory(path);
149+
File.WriteAllText(Path.Combine(path, "DebugWindow.txt"), sb.ToString());
150+
Debug.Log("HUDReplacer: Debug window dump written to Logs/HUDReplacer/DebugWindow.txt");
151+
}
152+
153+
internal static void DumpPopupDialogBase()
154+
{
155+
var prefab = PopupDialogController.Instance?.popupDialogBase;
156+
if (prefab == null)
157+
{
158+
Debug.Log("HUDReplacer: PopupDialogController not available.");
159+
return;
160+
}
161+
162+
var sb = new StringBuilder();
163+
DumpGameObject(sb, prefab.gameObject, 0);
164+
165+
var path = Path.Combine(KSPUtil.ApplicationRootPath, "Logs/HUDReplacer");
166+
Directory.CreateDirectory(path);
167+
File.WriteAllText(Path.Combine(path, "PopupDialogBase.txt"), sb.ToString());
168+
Debug.Log(
169+
"HUDReplacer: PopupDialogBase dump written to Logs/HUDReplacer/PopupDialogBase.txt"
170+
);
171+
}
172+
173+
internal static void DumpPrefabs()
174+
{
175+
var sb = new StringBuilder();
176+
177+
foreach (var prefab in UISkinManager.fetch.prefabs)
178+
{
179+
if (prefab == null)
180+
continue;
181+
DumpGameObject(sb, prefab, 0);
182+
sb.AppendLine();
183+
}
184+
185+
var path = Path.Combine(KSPUtil.ApplicationRootPath, "Logs/HUDReplacer");
186+
Directory.CreateDirectory(path);
187+
File.WriteAllText(Path.Combine(path, "Prefabs.txt"), sb.ToString());
188+
Debug.Log($"HUDReplacer: Prefab dump written to Logs/HUDReplacer/Prefabs.txt");
189+
}
190+
191+
private static void DumpGameObject(StringBuilder sb, GameObject go, int depth)
192+
{
193+
var indent = new string(' ', depth * 2);
194+
var rect = go.GetComponent<RectTransform>();
195+
var rectInfo = "";
196+
if (rect != null)
197+
{
198+
var sd = rect.sizeDelta;
199+
var amin = rect.anchorMin;
200+
var amax = rect.anchorMax;
201+
var piv = rect.pivot;
202+
var ap = rect.anchoredPosition;
203+
rectInfo =
204+
$" [size={sd.x}x{sd.y} anchor=({amin.x},{amin.y})-({amax.x},{amax.y}) pivot=({piv.x},{piv.y}) pos=({ap.x},{ap.y})]";
205+
}
206+
207+
sb.AppendLine($"{indent}{go.name} (active={go.activeSelf}){rectInfo}");
208+
209+
foreach (var comp in go.GetComponents<Component>())
210+
{
211+
if (comp == null)
212+
continue;
213+
if (comp is Transform or RectTransform)
214+
continue;
215+
var compInfo = "";
216+
if (comp is TextMeshProUGUI tmp)
217+
compInfo =
218+
$" alignment={tmp.alignment} overflowMode={tmp.overflowMode} enableWordWrapping={tmp.enableWordWrapping}";
219+
sb.AppendLine($"{indent} + {comp.GetType().Name}{compInfo}");
220+
}
221+
222+
for (int i = 0; i < go.transform.childCount; i++)
223+
DumpGameObject(sb, go.transform.GetChild(i).gameObject, depth + 1);
224+
}
225+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using UnityEngine;
4+
using UnityEngine.UI;
5+
6+
namespace HUDReplacer.UI;
7+
8+
internal class DumpSkinTexturesButton : MonoBehaviour
9+
{
10+
public Button button;
11+
12+
void Start()
13+
{
14+
button.onClick.AddListener(DumpSkinTextures);
15+
}
16+
17+
static void DumpSkinTextures()
18+
{
19+
var skin = UISkinManager.defaultSkin;
20+
if (skin == null)
21+
{
22+
Debug.Log("HUDReplacer: UISkinManager.defaultSkin is null.");
23+
return;
24+
}
25+
26+
var entries = new List<(string source, Texture2D tex)>();
27+
28+
CollectFromStyle(entries, "box", skin.box);
29+
CollectFromStyle(entries, "button", skin.button);
30+
CollectFromStyle(entries, "horizontalScrollbar", skin.horizontalScrollbar);
31+
CollectFromStyle(
32+
entries,
33+
"horizontalScrollbarLeftButton",
34+
skin.horizontalScrollbarLeftButton
35+
);
36+
CollectFromStyle(entries, "horizontalScrollbarThumb", skin.horizontalScrollbarThumb);
37+
CollectFromStyle(entries, "horizontalSlider", skin.horizontalSlider);
38+
CollectFromStyle(entries, "horizontalSliderThumb", skin.horizontalSliderThumb);
39+
CollectFromStyle(entries, "label", skin.label);
40+
CollectFromStyle(entries, "scrollView", skin.scrollView);
41+
CollectFromStyle(entries, "textArea", skin.textArea);
42+
CollectFromStyle(entries, "textField", skin.textField);
43+
CollectFromStyle(entries, "toggle", skin.toggle);
44+
CollectFromStyle(entries, "verticalScrollbar", skin.verticalScrollbar);
45+
CollectFromStyle(entries, "verticalScrollbarDownButton", skin.verticalScrollbarDownButton);
46+
CollectFromStyle(entries, "verticalScrollbarThumb", skin.verticalScrollbarThumb);
47+
CollectFromStyle(entries, "verticalScrollbarUpButton", skin.verticalScrollbarUpButton);
48+
CollectFromStyle(entries, "verticalSlider", skin.verticalSlider);
49+
CollectFromStyle(entries, "verticalSliderThumb", skin.verticalSliderThumb);
50+
CollectFromStyle(entries, "window", skin.window);
51+
52+
if (skin.customStyles != null)
53+
{
54+
foreach (var style in skin.customStyles)
55+
{
56+
if (style == null)
57+
continue;
58+
CollectFromStyle(entries, style.name ?? "unnamed", style);
59+
}
60+
}
61+
62+
Debug.Log($"HUDReplacer: Dumping {entries.Count} skin texture entries...");
63+
foreach (var (source, tex) in entries.OrderBy(e => e.source))
64+
Debug.Log($" {source}: {tex.name} ({tex.width}x{tex.height})");
65+
Debug.Log("HUDReplacer: Skin texture dump finished.");
66+
}
67+
68+
static void CollectFromStyle(
69+
List<(string source, Texture2D tex)> entries,
70+
string styleName,
71+
UIStyle style
72+
)
73+
{
74+
if (style == null)
75+
return;
76+
77+
CollectFromState(entries, $"{styleName}.normal", style.normal);
78+
CollectFromState(entries, $"{styleName}.highlight", style.highlight);
79+
CollectFromState(entries, $"{styleName}.active", style.active);
80+
CollectFromState(entries, $"{styleName}.disabled", style.disabled);
81+
}
82+
83+
static void CollectFromState(
84+
List<(string source, Texture2D tex)> entries,
85+
string source,
86+
UIStyleState state
87+
)
88+
{
89+
if (state?.background == null)
90+
return;
91+
92+
var tex = state.background.texture;
93+
if (tex == null)
94+
return;
95+
96+
entries.Add((source, tex));
97+
}
98+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using Unity.Profiling;
2+
using UnityEngine;
3+
using UnityEngine.UI;
4+
5+
namespace HUDReplacer.UI;
6+
7+
internal class DumpTexturesButton : MonoBehaviour
8+
{
9+
static readonly ProfilerMarker Marker = new("HUDReplacer.DumpTextures");
10+
11+
public Button button;
12+
13+
void Start()
14+
{
15+
button.onClick.AddListener(DumpAllTextures);
16+
}
17+
18+
internal static void DumpAllTextures()
19+
{
20+
using var scope = Marker.Auto();
21+
Debug.Log("HUDReplacer: Dumping list of loaded texture2D objects...");
22+
var textures = Resources.FindObjectsOfTypeAll<Texture2D>();
23+
foreach (var tex in textures)
24+
Debug.Log($"{tex.name} - WxH={tex.width}x{tex.height}");
25+
Debug.Log("HUDReplacer: Dumping finished.");
26+
}
27+
}

0 commit comments

Comments
 (0)