UIDebugTheme.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  1. using System.Collections.Generic;
  2. using TMPro;
  3. using UnityEditor;
  4. using UnityEngine;
  5. using UnityEngine.UI;
  6. [InitializeOnLoad]
  7. public static class UIDebugTheme
  8. {
  9. private static readonly Color BackgroundColor = new Color(0.11f, 0.15f, 0.25f, 0.90f);
  10. private static readonly Color PanelColor = new Color(0.17f, 0.24f, 0.31f, 0.85f);
  11. private static readonly Color ButtonColor = new Color(0.16f, 0.50f, 0.73f, 1.00f);
  12. private static readonly Color ToggleOnColor = new Color(0.53f, 0.81f, 0.98f, 1.00f); // light blue
  13. private static readonly Color ToggleOffColor = new Color(0.10f, 0.22f, 0.40f, 1.00f); // dark blue
  14. private static readonly Color RadioOnColor = new Color(0.15f, 0.85f, 0.55f, 1.00f); // bright green — distinct from all blues
  15. private static readonly Color LeafColor = new Color(0.36f, 0.43f, 0.49f, 1.00f);
  16. private static readonly Dictionary<int, Color> _originals = new Dictionary<int, Color>();
  17. private static readonly Dictionary<Toggle, bool> _lastToggleStates = new Dictionary<Toggle, bool>();
  18. private static readonly List<GameObject> _addedLabels = new List<GameObject>();
  19. private static readonly Dictionary<int, ColorBlock> _originalBlocks = new Dictionary<int, ColorBlock>();
  20. private static readonly Dictionary<int, Selectable.Transition> _originalTransitions = new Dictionary<int, Selectable.Transition>();
  21. private static readonly Dictionary<int, Material> _originalMaterials = new Dictionary<int, Material>();
  22. private static double _nextRescanTime = 0;
  23. // ColorBlock for regular interactables (buttons etc.). normalColor=white leaves
  24. // image.color unmodified in normal state; >1 values lighten on hover/select,
  25. // <1 darkens on press.
  26. private static readonly ColorBlock ThemedBlock = new ColorBlock
  27. {
  28. normalColor = Color.white,
  29. highlightedColor = new Color(1.20f, 1.20f, 1.20f, 1.0f),
  30. pressedColor = new Color(0.65f, 0.65f, 0.65f, 1.0f),
  31. selectedColor = new Color(1.15f, 1.15f, 1.15f, 1.0f),
  32. disabledColor = new Color(0.50f, 0.50f, 0.50f, 0.5f),
  33. colorMultiplier = 1f,
  34. fadeDuration = 0.1f,
  35. };
  36. // ColorBlock for input fields. normalColor is light gray so the background is
  37. // visible and differs from pure-white text, and selectedColor shows a blue tint
  38. // when the field has focus so the user can tell it is active.
  39. private static readonly ColorBlock InputFieldBlock = new ColorBlock
  40. {
  41. normalColor = new Color(0.82f, 0.82f, 0.82f, 1.00f),
  42. highlightedColor = new Color(0.90f, 0.90f, 0.90f, 1.00f),
  43. pressedColor = new Color(0.60f, 0.60f, 0.60f, 1.00f),
  44. selectedColor = new Color(0.65f, 0.83f, 0.98f, 1.00f), // light blue when focused
  45. disabledColor = new Color(0.50f, 0.50f, 0.50f, 0.50f),
  46. colorMultiplier = 1f,
  47. fadeDuration = 0.1f,
  48. };
  49. static UIDebugTheme()
  50. {
  51. EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
  52. }
  53. static void OnPlayModeStateChanged(PlayModeStateChange state)
  54. {
  55. switch (state)
  56. {
  57. case PlayModeStateChange.EnteredPlayMode:
  58. ApplyTheme();
  59. EditorApplication.update += PollToggleStates;
  60. break;
  61. case PlayModeStateChange.ExitingPlayMode:
  62. EditorApplication.update -= PollToggleStates;
  63. CleanupToggleTracking();
  64. _originals.Clear();
  65. _originalBlocks.Clear();
  66. _originalTransitions.Clear();
  67. _originalMaterials.Clear();
  68. break;
  69. }
  70. }
  71. static void ApplyTheme()
  72. {
  73. _originals.Clear();
  74. var images = Object.FindObjectsOfType<Image>(true);
  75. int count = 0;
  76. foreach (var image in images)
  77. {
  78. if (!PassesSpriteFilter(image))
  79. {
  80. continue;
  81. }
  82. int id = image.GetInstanceID();
  83. _originals[id] = image.color;
  84. image.color = ClassifyColor(image);
  85. count++;
  86. }
  87. SetupToggleTracking();
  88. AddIconButtonLabels();
  89. AddMissingAssetLabels();
  90. ApplySelectableColors();
  91. Debug.Log($"[UIDebugTheme] Applied debug colours to {count} Image component{(count == 1 ? "" : "s")} with missing sprites.");
  92. }
  93. // Returns true for Images that should receive debug theme colours:
  94. // sprite must be null AND color must be near-white (or have a direct text child
  95. // whose sprite was providing the label background).
  96. static bool PassesSpriteFilter(Image image)
  97. {
  98. if (image.sprite != null) { return false; }
  99. // Never theme images that live inside a CardImage hierarchy — those are
  100. // card art / key-card visuals and must keep their original colours.
  101. for (Transform t = image.transform; t != null; t = t.parent)
  102. {
  103. if (t.name == "CardImage" || t.name == "DetailCard") { return false; }
  104. }
  105. var c = image.color;
  106. bool isBlankWhite = c.a >= 0.5f && c.r >= 0.9f && c.g >= 0.9f && c.b >= 0.9f;
  107. if (isBlankWhite) { return true; }
  108. // Non-white but has a direct text child — the sprite was providing the label
  109. // background and must be replaced so text remains readable.
  110. for (int i = 0; i < image.transform.childCount; i++)
  111. {
  112. var child = image.transform.GetChild(i);
  113. if (child.GetComponent<Text>() != null || child.GetComponent<TextMeshProUGUI>() != null)
  114. return true;
  115. }
  116. return false;
  117. }
  118. static Color ClassifyColor(Image image)
  119. {
  120. var go = image.gameObject;
  121. if (go.GetComponent<Button>() != null)
  122. {
  123. return ButtonColor;
  124. }
  125. Toggle toggleSelf = go.GetComponent<Toggle>();
  126. Toggle toggleParent = go.transform.parent != null
  127. ? go.transform.parent.GetComponent<Toggle>()
  128. : null;
  129. Toggle owningToggle = toggleSelf ?? toggleParent;
  130. if (owningToggle != null && owningToggle.targetGraphic == image)
  131. {
  132. Color onColor = owningToggle.group != null ? RadioOnColor : ToggleOnColor;
  133. return owningToggle.isOn ? onColor : ToggleOffColor;
  134. }
  135. if (go.GetComponent<Scrollbar>() != null || go.GetComponent<Slider>() != null)
  136. {
  137. return ButtonColor;
  138. }
  139. var rt = image.rectTransform;
  140. if (rt.parent != null && rt.parent.GetComponent<Canvas>() != null)
  141. {
  142. return BackgroundColor;
  143. }
  144. bool isFullStretch =
  145. rt.anchorMin == Vector2.zero &&
  146. rt.anchorMax == Vector2.one &&
  147. rt.offsetMin.sqrMagnitude < 100f &&
  148. rt.offsetMax.sqrMagnitude < 100f;
  149. if (isFullStretch)
  150. {
  151. return BackgroundColor;
  152. }
  153. var childGraphics = go.GetComponentsInChildren<Graphic>(true);
  154. if (childGraphics.Length > 1)
  155. {
  156. // Distinguish containers (have child Image/RawImage) from labeled badges (text-only children).
  157. // Badges like player-number boxes have only a text child and should use ButtonColor.
  158. bool hasChildImage = false;
  159. foreach (var g in childGraphics)
  160. {
  161. if (g == image) { continue; }
  162. if (g is Image || g is RawImage) { hasChildImage = true; break; }
  163. }
  164. return hasChildImage ? PanelColor : ButtonColor;
  165. }
  166. return LeafColor;
  167. }
  168. static void SetupToggleTracking()
  169. {
  170. var toggles = Object.FindObjectsOfType<Toggle>(true);
  171. foreach (var toggle in toggles)
  172. {
  173. if (toggle.targetGraphic is not Image bg)
  174. {
  175. continue;
  176. }
  177. // For non-radio (non-group) toggles, skip if the targetGraphic has a valid
  178. // sprite — it has a real designed visual and we shouldn't clobber it.
  179. // Radio buttons (ToggleGroup members) are always processed so their state
  180. // color is enforced even when the sprite reference resolves to an object.
  181. if (bg.sprite != null && toggle.group == null)
  182. {
  183. continue;
  184. }
  185. if (_lastToggleStates.ContainsKey(toggle))
  186. {
  187. continue;
  188. }
  189. _lastToggleStates[toggle] = toggle.isOn;
  190. Color onCol = toggle.group != null ? RadioOnColor : ToggleOnColor;
  191. Color stateCol = toggle.isOn ? onCol : ToggleOffColor;
  192. // Force-color the targetGraphic even if ApplyTheme skipped it (e.g. the
  193. // Image had a non-white default color and no text child).
  194. int bgId = bg.GetInstanceID();
  195. if (!_originals.ContainsKey(bgId)) { _originals[bgId] = bg.color; }
  196. bg.color = stateCol;
  197. // Also color toggle.graphic (the checkmark image). Unity makes the checkmark
  198. // visible when isOn=true. If it got classified as BackgroundColor by ApplyTheme
  199. // it would overlay the indicator with the wrong color and mask our state color.
  200. if (toggle.graphic is Image checkmark && checkmark != bg)
  201. {
  202. int ckId = checkmark.GetInstanceID();
  203. if (!_originals.ContainsKey(ckId)) { _originals[ckId] = checkmark.color; }
  204. checkmark.color = stateCol;
  205. }
  206. // Radio buttons express state through color alone —
  207. // their sibling text already provides the label. Only non-grouped toggles
  208. // get an ON/OFF overlay.
  209. if (toggle.group == null)
  210. {
  211. SetToggleLabel(toggle, toggle.isOn);
  212. }
  213. }
  214. }
  215. static void AddIconButtonLabels()
  216. {
  217. var buttons = Object.FindObjectsOfType<Button>(true);
  218. foreach (var btn in buttons)
  219. {
  220. if (btn.targetGraphic is not Image bg)
  221. {
  222. continue;
  223. }
  224. if (!_originals.ContainsKey(bg.GetInstanceID()))
  225. {
  226. continue;
  227. }
  228. if (bg.transform.Find("UIDebugTheme_IconLabel") != null)
  229. {
  230. continue;
  231. }
  232. string label = GetIconLabel(btn, bg, bg.rectTransform);
  233. if (label == null)
  234. {
  235. continue;
  236. }
  237. if (btn.GetComponentsInChildren<Button>(true).Length > 1)
  238. {
  239. continue;
  240. }
  241. // Ensure settings buttons get a recognisable dark background.
  242. // Color the full subtree (button + all child Images) AND every ancestor
  243. // Image up to the Canvas, regardless of whether they were in _originals.
  244. if (label == "config")
  245. {
  246. foreach (var img in btn.GetComponentsInChildren<Image>(true))
  247. {
  248. int cid = img.GetInstanceID();
  249. if (!_originals.ContainsKey(cid)) { _originals[cid] = img.color; }
  250. img.color = ButtonColor;
  251. }
  252. for (Transform t = btn.transform.parent; t != null; t = t.parent)
  253. {
  254. if (t.GetComponent<Canvas>() != null) { break; }
  255. var parentImg = t.GetComponent<Image>();
  256. if (parentImg == null) { continue; }
  257. int pid = parentImg.GetInstanceID();
  258. if (!_originals.ContainsKey(pid)) { _originals[pid] = parentImg.color; }
  259. parentImg.color = ButtonColor;
  260. }
  261. }
  262. var labelGO = new GameObject("UIDebugTheme_IconLabel");
  263. labelGO.transform.SetParent(bg.transform, false);
  264. var lrt = labelGO.AddComponent<RectTransform>();
  265. lrt.anchorMin = Vector2.zero;
  266. lrt.anchorMax = Vector2.one;
  267. lrt.offsetMin = Vector2.zero;
  268. lrt.offsetMax = Vector2.zero;
  269. var tmp = labelGO.AddComponent<TextMeshProUGUI>();
  270. tmp.text = label;
  271. tmp.alignment = TextAlignmentOptions.Center;
  272. tmp.color = Color.white;
  273. tmp.fontStyle = FontStyles.Bold;
  274. tmp.enableAutoSizing = true;
  275. tmp.fontSizeMin = 8;
  276. tmp.fontSizeMax = 36;
  277. _addedLabels.Add(labelGO);
  278. }
  279. }
  280. // Builds a '>'-joined lowercase path string from root to the button's GO.
  281. static string GetButtonPath(Button btn)
  282. {
  283. var parts = new List<string>();
  284. for (Transform t = btn.transform; t != null; t = t.parent)
  285. {
  286. parts.Add(t.name.ToLowerInvariant());
  287. }
  288. parts.Reverse();
  289. return string.Join(">", parts);
  290. }
  291. // Returns the debug label for an icon-only button, or null to leave it unlabelled.
  292. // "config" is matched by a specific container name anywhere in the full hierarchy path
  293. // (no TMP filter — the button may have internal text layers).
  294. // "X" is matched by name/anchor but only on buttons with no pre-existing text children
  295. // so we don't overlay a label on top of real button text.
  296. static string GetIconLabel(Button btn, Image bg, RectTransform rt)
  297. {
  298. string path = GetButtonPath(btn);
  299. // Settings / option icon buttons — match by specific container name in the path.
  300. if (path.Contains("optionbutton"))
  301. {
  302. return "config";
  303. }
  304. // For X labels only operate on buttons with no pre-existing text children,
  305. // otherwise we'd overlay the label on real button text.
  306. bool hasText = bg.GetComponentInChildren<TextMeshProUGUI>() != null
  307. || bg.GetComponentInChildren<Text>() != null;
  308. if (hasText)
  309. {
  310. return null;
  311. }
  312. string selfName = btn.gameObject.name.ToLowerInvariant();
  313. string parentName = btn.transform.parent != null ? btn.transform.parent.name.ToLowerInvariant() : "";
  314. // Deck editor pagination — child GOs named "Left"/"Right" inside the EditDeck panel.
  315. if (path.Contains(">left>")) { return "<"; }
  316. if (path.Contains(">right>")) { return ">"; }
  317. // The EditDeck panel has no X close buttons, and many buttons anchored to
  318. // the top-right of their components (card pool pagination buttons, deck
  319. // card +/- buttons), so suppress any "X" behavior.
  320. if (path.Contains(">editdeck>")) { return null; }
  321. // Close button by name (button GO or direct parent).
  322. if (selfName.Contains("close") || selfName.Contains("dismiss") ||
  323. parentName.Contains("close") || parentName.Contains("dismiss"))
  324. {
  325. return "X";
  326. }
  327. // Anchor-based fallback: top-right anchored icon-only buttons are likely close buttons.
  328. if (rt.anchorMin.x >= 0.5f && rt.anchorMin.y >= 0.5f)
  329. {
  330. return "X";
  331. }
  332. return null;
  333. }
  334. static void PollToggleStates()
  335. {
  336. if (EditorApplication.timeSinceStartup >= _nextRescanTime)
  337. {
  338. _nextRescanTime = EditorApplication.timeSinceStartup + 1.0;
  339. RescanNewImages();
  340. }
  341. // Enforce correct colors every frame — game code (Awake/Start) can reset image
  342. // colors after ApplyTheme runs, and state-change detection alone won't catch that.
  343. // We color every tracked image in the toggle's entire subtree (not just targetGraphic)
  344. // so that the checkmark or any other child image cannot cover the indicator with
  345. // a misclassified color.
  346. foreach (var kvp in _lastToggleStates)
  347. {
  348. var toggle = kvp.Key;
  349. if (toggle == null) { continue; }
  350. Color onColor = toggle.group != null ? RadioOnColor : ToggleOnColor;
  351. Color stateColor = toggle.isOn ? onColor : ToggleOffColor;
  352. foreach (var img in toggle.GetComponentsInChildren<Image>(true))
  353. {
  354. if (_originals.ContainsKey(img.GetInstanceID()) && img.color != stateColor)
  355. img.color = stateColor;
  356. }
  357. }
  358. // Detect state changes to update ON/OFF labels and the tracked-state dict.
  359. List<Toggle> changed = null;
  360. foreach (var kvp in _lastToggleStates)
  361. {
  362. if (kvp.Key == null || kvp.Key.isOn == kvp.Value)
  363. {
  364. continue;
  365. }
  366. changed ??= new List<Toggle>();
  367. changed.Add(kvp.Key);
  368. }
  369. if (changed == null)
  370. {
  371. return;
  372. }
  373. foreach (var toggle in changed)
  374. {
  375. _lastToggleStates[toggle] = toggle.isOn;
  376. if (toggle.group == null)
  377. {
  378. SetToggleLabel(toggle, toggle.isOn);
  379. }
  380. }
  381. }
  382. static void RescanNewImages()
  383. {
  384. // Prune destroyed toggles so the dict doesn't grow unboundedly in scenes
  385. // with dynamic UI (object pooling, Destroy calls, etc.).
  386. var deadToggles = new List<Toggle>();
  387. foreach (var toggle in _lastToggleStates.Keys)
  388. if (toggle == null) deadToggles.Add(toggle);
  389. foreach (var t in deadToggles) _lastToggleStates.Remove(t);
  390. var images = Object.FindObjectsOfType<Image>(true);
  391. int count = 0;
  392. foreach (var image in images)
  393. {
  394. int id = image.GetInstanceID();
  395. if (_originals.ContainsKey(id))
  396. {
  397. continue;
  398. }
  399. if (!PassesSpriteFilter(image))
  400. {
  401. continue;
  402. }
  403. _originals[id] = image.color;
  404. image.color = ClassifyColor(image);
  405. count++;
  406. }
  407. if (count > 0)
  408. {
  409. SetupToggleTracking();
  410. AddIconButtonLabels();
  411. AddMissingAssetLabels();
  412. ApplySelectableColors();
  413. }
  414. }
  415. static void SetToggleLabel(Toggle toggle, bool isOn)
  416. {
  417. if (toggle.targetGraphic is not Image bg)
  418. {
  419. return;
  420. }
  421. var existing = bg.transform.Find("UIDebugTheme_Label");
  422. GameObject labelGO;
  423. if (existing != null)
  424. {
  425. labelGO = existing.gameObject;
  426. }
  427. else
  428. {
  429. labelGO = new GameObject("UIDebugTheme_Label");
  430. labelGO.transform.SetParent(bg.transform, false);
  431. var rt = labelGO.AddComponent<RectTransform>();
  432. rt.anchorMin = Vector2.zero;
  433. rt.anchorMax = Vector2.one;
  434. rt.offsetMin = Vector2.zero;
  435. rt.offsetMax = Vector2.zero;
  436. var tmp = labelGO.AddComponent<TextMeshProUGUI>();
  437. tmp.alignment = TextAlignmentOptions.Center;
  438. tmp.color = Color.white;
  439. tmp.fontStyle = FontStyles.Bold;
  440. tmp.enableAutoSizing = true;
  441. tmp.fontSizeMin = 8;
  442. tmp.fontSizeMax = 24;
  443. _addedLabels.Add(labelGO);
  444. }
  445. labelGO.GetComponent<TextMeshProUGUI>().text = isOn ? "ON" : "OFF";
  446. }
  447. // For Images named "index" whose sprite is missing, add a text label derived from
  448. // the parent GO's name. This covers numbered icon slots like player-index badges
  449. // where the sprite asset provides the numeral.
  450. static void AddMissingAssetLabels()
  451. {
  452. var images = Object.FindObjectsOfType<Image>(true);
  453. foreach (var image in images)
  454. {
  455. if (image.gameObject.name != "index") { continue; }
  456. if (!_originals.ContainsKey(image.GetInstanceID())) { continue; }
  457. if (image.transform.Find("UIDebugTheme_IconLabel") != null) { continue; }
  458. string text = image.transform.parent != null ? image.transform.parent.name : null;
  459. if (string.IsNullOrEmpty(text)) { continue; }
  460. var labelGO = new GameObject("UIDebugTheme_IconLabel");
  461. labelGO.transform.SetParent(image.transform, false);
  462. var lrt = labelGO.AddComponent<RectTransform>();
  463. lrt.anchorMin = Vector2.zero;
  464. lrt.anchorMax = Vector2.one;
  465. lrt.offsetMin = Vector2.zero;
  466. lrt.offsetMax = Vector2.zero;
  467. var tmp = labelGO.AddComponent<TextMeshProUGUI>();
  468. tmp.text = text;
  469. tmp.alignment = TextAlignmentOptions.Center;
  470. tmp.color = Color.white;
  471. tmp.fontStyle = FontStyles.Bold;
  472. tmp.enableAutoSizing = true;
  473. tmp.fontSizeMin = 8;
  474. tmp.fontSizeMax = 36;
  475. _addedLabels.Add(labelGO);
  476. }
  477. }
  478. static void ApplySelectableColors()
  479. {
  480. var selectables = Object.FindObjectsOfType<Selectable>(true);
  481. foreach (var sel in selectables)
  482. {
  483. if (sel.targetGraphic is not Image img) { continue; }
  484. bool isInputField = sel is TMP_InputField || sel is InputField;
  485. // For input fields, apply theming regardless of whether ApplyTheme tracked
  486. // the background image (it may have been skipped by the white/sprite filter).
  487. // For all other selectables, only theme images that have missing sprites.
  488. if (!isInputField && !_originals.ContainsKey(img.GetInstanceID())) { continue; }
  489. int id = sel.GetInstanceID();
  490. if (_originalBlocks.ContainsKey(id)) { continue; }
  491. _originalTransitions[id] = sel.transition;
  492. _originalBlocks[id] = sel.colors;
  493. int imgId = img.GetInstanceID();
  494. if (!_originals.ContainsKey(imgId)) { _originals[imgId] = img.color; }
  495. if (isInputField)
  496. {
  497. // If the image has a non-default material (e.g. UI/ColorAdditive) it will
  498. // render additively and always appear washed out white regardless of image.color.
  499. // Override it to null (Unity's default UI/Default alpha-blend shader) so our
  500. // color values actually show up.
  501. if (img.material != img.defaultMaterial && !_originalMaterials.ContainsKey(imgId))
  502. {
  503. _originalMaterials[imgId] = img.material;
  504. img.material = null;
  505. }
  506. // Light gray normal state so the field is visible; blue tint on focus.
  507. sel.transition = Selectable.Transition.ColorTint;
  508. sel.colors = InputFieldBlock;
  509. }
  510. else if (sel is Toggle)
  511. {
  512. // Toggle colors are managed entirely by RegisterToggleListeners/PollToggleStates.
  513. // Setting ColorTint here causes CrossFadeColor(normalColor) to overwrite those
  514. // colors after each click, so disable the transition instead.
  515. sel.transition = Selectable.Transition.None;
  516. }
  517. else
  518. {
  519. sel.transition = Selectable.Transition.ColorTint;
  520. sel.colors = ThemedBlock;
  521. }
  522. }
  523. }
  524. static void CleanupToggleTracking()
  525. {
  526. foreach (var label in _addedLabels)
  527. {
  528. if (label != null)
  529. {
  530. Object.Destroy(label);
  531. }
  532. }
  533. _addedLabels.Clear();
  534. _lastToggleStates.Clear();
  535. }
  536. }