You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

620 lines
27 KiB

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6. using UnityEngine;
  7. using CommonMark;
  8. using CommonMark.Syntax;
  9. using UnityEngine.UI;
  10. using TMPro;
  11. using CustomUI.BeatSaber;
  12. using IPA.Utilities;
  13. using System.Reflection;
  14. using UnityEngine.EventSystems;
  15. using System.Diagnostics;
  16. using System.Collections;
  17. namespace BSIPA_ModList.UI.ViewControllers
  18. {
  19. [RequireComponent(typeof(RectTransform))]
  20. public class MarkdownView : MonoBehaviour
  21. {
  22. private class TagTypeComponent : MonoBehaviour
  23. {
  24. internal BlockTag Tag;
  25. internal HeadingData hData;
  26. }
  27. private string markdown = "";
  28. private bool mdDirty = false;
  29. public string Markdown
  30. {
  31. get => markdown;
  32. set
  33. {
  34. markdown = value;
  35. mdDirty = true;
  36. }
  37. }
  38. public RectTransform rectTransform => GetComponent<RectTransform>();
  39. private ScrollView scrView;
  40. private RectTransform content;
  41. private RectTransform viewport;
  42. private CommonMarkSettings settings;
  43. public MarkdownView()
  44. {
  45. settings = CommonMarkSettings.Default.Clone();
  46. settings.AdditionalFeatures = CommonMarkAdditionalFeatures.All;
  47. settings.RenderSoftLineBreaksAsLineBreaks = false;
  48. settings.UriResolver = ResolveUri;
  49. }
  50. public Func<string, bool> HasEmbeddedImage;
  51. private string ResolveUri(string arg)
  52. {
  53. var name = arg.Substring(3);
  54. if (!arg.StartsWith("!::") && !arg.StartsWith("w::"))
  55. { // !:: means embedded, w:: means web
  56. // this block is for when neither is specified
  57. Logger.md.Debug($"Resolving nonspecific URI {arg}");
  58. // check if its embedded
  59. if (HasEmbeddedImage != null && HasEmbeddedImage(arg))
  60. return "!::" + arg;
  61. else
  62. return "w::" + arg;
  63. }
  64. Logger.md.Debug($"Resolved specific URI {arg}");
  65. return arg;
  66. }
  67. private static string GetLinkUri(string uri)
  68. {
  69. if (uri[0] == '!')
  70. {
  71. Logger.md.Error($"Cannot link to embedded resource in mod description");
  72. return null;
  73. }
  74. else
  75. return uri.Substring(3);
  76. }
  77. private static AssetBundle _bundle;
  78. private static AssetBundle Bundle
  79. {
  80. get
  81. {
  82. if (_bundle == null)
  83. _bundle = AssetBundle.LoadFromStream(Assembly.GetExecutingAssembly().GetManifestResourceStream("BSIPA_ModList.Bundles.consolas"));
  84. return _bundle;
  85. }
  86. }
  87. private static TMP_FontAsset _consolas;
  88. private static TMP_FontAsset Consolas
  89. {
  90. get
  91. {
  92. if (_consolas == null)
  93. {
  94. _consolas = Bundle?.LoadAsset<TMP_FontAsset>("CONSOLAS");
  95. if (_consolas != null)
  96. {
  97. _consolas.material.color = new Color(1f, 1f, 1f, 0f);
  98. _consolas.material.globalIlluminationFlags = MaterialGlobalIlluminationFlags.None;
  99. }
  100. }
  101. return _consolas;
  102. }
  103. }
  104. protected void Awake()
  105. {
  106. if (Consolas == null)
  107. Logger.md.Error($"Loading of Consolas font failed");
  108. gameObject.SetActive(false);
  109. var vpgo = new GameObject("Viewport");
  110. viewport = vpgo.AddComponent<RectTransform>();
  111. viewport.SetParent(transform);
  112. viewport.localPosition = Vector2.zero;
  113. viewport.anchorMin = Vector2.zero;
  114. viewport.anchorMax = Vector2.one;
  115. viewport.anchoredPosition = new Vector2(.5f, .5f);
  116. viewport.sizeDelta = Vector2.zero;
  117. var vpmask = vpgo.AddComponent<Mask>();
  118. var vpim = vpgo.AddComponent<Image>(); // supposedly Mask needs an Image?
  119. vpmask.showMaskGraphic = false;
  120. vpim.color = Color.white;
  121. vpim.sprite = WhitePixel;
  122. vpim.material = CustomUI.Utilities.UIUtilities.NoGlowMaterial;
  123. content = new GameObject("Content Wrapper").AddComponent<RectTransform>();
  124. content.SetParent(viewport);
  125. content.gameObject.AddComponent<TagTypeComponent>();
  126. content.localPosition = Vector2.zero;
  127. content.anchorMin = new Vector2(0f, 1f);
  128. content.anchorMax = new Vector2(1f, 1f);
  129. content.anchoredPosition = Vector2.zero;
  130. var contentLayout = content.gameObject.AddComponent<VerticalLayoutGroup>();
  131. var contentFitter = content.gameObject.AddComponent<ContentSizeFitter>();
  132. contentFitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize;
  133. contentFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
  134. contentLayout.childControlHeight = true;
  135. contentLayout.childControlWidth = false;
  136. contentLayout.childForceExpandHeight = false;
  137. contentLayout.childForceExpandWidth = true;
  138. contentLayout.childAlignment = TextAnchor.UpperCenter;
  139. contentLayout.spacing = 0f;
  140. var pageUp = Instantiate(Resources.FindObjectsOfTypeAll<Button>().Last((Button x) => x.name == "PageUpButton"), rectTransform, false);
  141. var pageDown = Instantiate(Resources.FindObjectsOfTypeAll<Button>().Last((Button x) => x.name == "PageDownButton"), rectTransform, false);
  142. {
  143. var pup_rt = pageUp.transform as RectTransform;
  144. var pup_sof = pup_rt.sizeDelta.y;
  145. var pup_xoff = (rectTransform.sizeDelta.x / 2) + (pup_sof / 2);
  146. pup_rt.anchoredPosition = new Vector2(pup_xoff, pup_rt.anchoredPosition.y);
  147. var pup_bg_rt = pup_rt.Find("BG") as RectTransform;
  148. pup_bg_rt.sizeDelta = new Vector2(pup_bg_rt.sizeDelta.y, pup_bg_rt.sizeDelta.y);
  149. // fix hitbox
  150. pup_rt.anchorMin = new Vector2(.5f, pup_rt.anchorMin.y);
  151. pup_rt.anchorMax = new Vector2(.5f, pup_rt.anchorMax.y);
  152. pup_rt.sizeDelta = new Vector2(pup_rt.sizeDelta.y, pup_rt.sizeDelta.y);
  153. }
  154. {
  155. var pdn_rt = pageDown.transform as RectTransform;
  156. var pdn_sof = pdn_rt.sizeDelta.y;
  157. var pdn_xoff = (rectTransform.sizeDelta.x / 2) + (pdn_sof / 2);
  158. pdn_rt.anchoredPosition = new Vector2(pdn_xoff, pdn_rt.anchoredPosition.y);
  159. var pdn_bg_rt = pdn_rt.Find("BG") as RectTransform;
  160. pdn_bg_rt.sizeDelta = new Vector2(pdn_bg_rt.sizeDelta.y, pdn_bg_rt.sizeDelta.y);
  161. // fix hitbox
  162. pdn_rt.anchorMin = new Vector2(.5f, pdn_rt.anchorMin.y);
  163. pdn_rt.anchorMax = new Vector2(.5f, pdn_rt.anchorMax.y);
  164. pdn_rt.sizeDelta = new Vector2(pdn_rt.sizeDelta.y, pdn_rt.sizeDelta.y);
  165. }
  166. scrView = gameObject.AddComponent<ScrollView>();
  167. scrView.SetPrivateField("_pageUpButton", pageUp);
  168. scrView.SetPrivateField("_pageDownButton", pageDown);
  169. scrView.SetPrivateField("_contentRectTransform", content);
  170. scrView.SetPrivateField("_viewport", viewport);
  171. gameObject.SetActive(true);
  172. }
  173. private static Sprite whitePixel;
  174. private static Sprite WhitePixel
  175. {
  176. get
  177. {
  178. if (whitePixel == null)
  179. whitePixel = Resources.FindObjectsOfTypeAll<Sprite>().First(s => s.name == "WhitePixel");
  180. return whitePixel;
  181. }
  182. }
  183. #if DEBUG
  184. #if UI_CONFIGURE_MARKDOWN_THEMATIC_BREAK
  185. private byte tbreakSettings = 0;
  186. #endif
  187. #endif
  188. public void Update()
  189. {
  190. #if DEBUG && UI_CONFIGURE_MARKDOWN_THEMATIC_BREAK
  191. if (Input.GetKeyDown(KeyCode.K))
  192. {
  193. tbreakSettings = (byte)((tbreakSettings + 1) % 16);
  194. UpdateMd();
  195. Logger.md.Info(tbreakSettings.ToString());
  196. }
  197. #endif
  198. if (mdDirty)
  199. StartCoroutine(UpdateMd());
  200. }
  201. [Flags]
  202. private enum CurrentTextFlags
  203. {
  204. None = 0, Bold = 1, Italic = 2, Underline = 4, Strikethrough = 8,
  205. }
  206. private const string LinkDefaultColor = "#0061ff";
  207. private const string LinkHoverColor = "#009dff";
  208. private bool resetContentPosition = false;
  209. private IEnumerator UpdateMd()
  210. {
  211. mdDirty = false;
  212. Clear();
  213. // enable so it will set stuff up
  214. content.gameObject.GetComponent<VerticalLayoutGroup>().enabled = true;
  215. var doc = CommonMarkConverter.Parse(markdown, settings);
  216. Stack<RectTransform> layout = new Stack<RectTransform>();
  217. layout.Push(content);
  218. TextMeshProUGUI currentText = null;
  219. List<TextMeshProUGUI> texts = new List<TextMeshProUGUI>();
  220. CurrentTextFlags textFlags = 0;
  221. foreach (var node in doc.AsEnumerable())
  222. {
  223. Logger.md.Debug($"node {node}");
  224. if (node.Block != null)
  225. {
  226. var block = node.Block;
  227. const float BreakHeight = .5f;
  228. const int TextInset = 1;
  229. void Spacer(float size = 1.5f)
  230. {
  231. var go = new GameObject("Spacer", typeof(RectTransform));
  232. var vlayout = go.GetComponent<RectTransform>();
  233. vlayout.SetParent(layout.Peek());
  234. vlayout.anchorMin = new Vector2(.5f, .5f);
  235. vlayout.anchorMax = new Vector2(.5f, .5f);
  236. vlayout.localScale = Vector3.one;
  237. vlayout.localPosition = Vector3.zero;
  238. var l = go.AddComponent<LayoutElement>();
  239. l.minHeight = l.preferredHeight = size;
  240. }
  241. HorizontalOrVerticalLayoutGroup BlockNode(string name, float spacing, bool isVertical, Action<TagTypeComponent> apply = null, float? spacer = null, bool isDoc = false)
  242. {
  243. if (node.IsOpening)
  244. {
  245. Logger.md.Debug($"Creating block container {name}");
  246. currentText = null;
  247. var go = new GameObject(name, typeof(RectTransform));
  248. var vlayout = go.GetComponent<RectTransform>();
  249. vlayout.SetParent(layout.Peek());
  250. //vlayout.anchoredPosition = new Vector2(.5f, .5f);
  251. vlayout.anchorMin = new Vector2(.5f, .5f);
  252. vlayout.anchorMax = new Vector2(.5f, .5f);
  253. vlayout.localScale = Vector3.one;
  254. vlayout.localPosition = Vector3.zero;
  255. if (isDoc)
  256. {
  257. vlayout.sizeDelta = new Vector2(rectTransform.rect.width, 0f);
  258. vlayout.anchorMin = new Vector2(0f, 1f);
  259. vlayout.anchorMax = new Vector2(1f, 1f);
  260. //vlayout.anchoredPosition = new Vector2(0f, -30f); // no idea where this -30 comes from, but it works for my use
  261. }
  262. var tt = go.AddComponent<TagTypeComponent>();
  263. tt.Tag = block.Tag;
  264. apply?.Invoke(tt);
  265. layout.Push(vlayout);
  266. HorizontalOrVerticalLayoutGroup l;
  267. if (isVertical)
  268. l = go.AddComponent<VerticalLayoutGroup>();
  269. else
  270. l = go.AddComponent<HorizontalLayoutGroup>();
  271. l.childControlHeight = l.childControlWidth = true;
  272. l.childForceExpandHeight = l.childForceExpandWidth = false;
  273. l.childForceExpandWidth = isDoc;
  274. l.spacing = spacing;
  275. if (isDoc)
  276. {
  277. vlayout.anchoredPosition = new Vector2(0f, -vlayout.rect.height);
  278. }
  279. return l;
  280. }
  281. else if (node.IsClosing)
  282. {
  283. currentText = null;
  284. layout.Pop();
  285. if (spacer.HasValue)
  286. Spacer(spacer.Value);
  287. }
  288. return null;
  289. }
  290. void ThematicBreak()
  291. { // TODO: Fix positioning
  292. var go = new GameObject("ThematicBreak", typeof(RectTransform), typeof(HorizontalLayoutGroup));
  293. var vlayout = go.GetComponent<RectTransform>();
  294. vlayout.SetParent(layout.Peek());
  295. var l = go.GetComponent<HorizontalLayoutGroup>();
  296. #if DEBUG && UI_CONFIGURE_MARKDOWN_THEMATIC_BREAK
  297. l.childControlHeight = (tbreakSettings & 0b0001) != 0; // if set, not well behaved
  298. l.childControlWidth = (tbreakSettings & 0b0010) != 0;
  299. l.childForceExpandHeight = (tbreakSettings & 0b0100) != 0; // if set, not well behaved
  300. l.childForceExpandWidth = (tbreakSettings & 0b1000) != 0;
  301. #else
  302. l.childControlHeight = false;
  303. l.childControlWidth = false;
  304. l.childForceExpandHeight = false;
  305. l.childForceExpandWidth = false;
  306. #endif
  307. l.childAlignment = TextAnchor.UpperCenter;
  308. l.spacing = 0f;
  309. vlayout.localScale = Vector3.one;
  310. vlayout.anchoredPosition = Vector2.zero;
  311. vlayout.anchorMin = new Vector2(.5f, .5f);
  312. vlayout.anchorMax = new Vector2(.5f, .5f);
  313. vlayout.sizeDelta = new Vector2(layout.Peek().rect.width, BreakHeight);
  314. vlayout.localPosition = Vector3.zero;
  315. currentText = null;
  316. go = new GameObject("ThematicBreak Bar", typeof(RectTransform), typeof(Image), typeof(LayoutElement));
  317. var im = go.GetComponent<Image>();
  318. im.color = Color.white;
  319. // i think i need to copy the sprite because i'm using the same one for the mask
  320. im.sprite = Sprite.Create(WhitePixel.texture, WhitePixel.rect, Vector2.zero);
  321. im.material = CustomUI.Utilities.UIUtilities.NoGlowMaterial;
  322. var rt = go.GetComponent<RectTransform>();
  323. rt.SetParent(vlayout);
  324. var le = go.GetComponent<LayoutElement>();
  325. le.minWidth = le.preferredWidth = layout.Peek().rect.width;
  326. le.minHeight = le.preferredHeight = BreakHeight;
  327. le.flexibleHeight = le.flexibleWidth = 1f;
  328. rt.localScale = Vector3.one;
  329. rt.localPosition = Vector3.zero;
  330. rt.anchoredPosition = Vector3.zero;
  331. rt.anchorMin = Vector2.zero;
  332. rt.anchorMax = Vector2.one;
  333. rt.sizeDelta = new Vector2(layout.Peek().rect.width, BreakHeight);
  334. Spacer(1f);
  335. }
  336. switch (block.Tag)
  337. {
  338. case BlockTag.Document:
  339. BlockNode("DocumentRoot", .5f, true, isDoc: true);
  340. break;
  341. case BlockTag.SetextHeading:
  342. var l = BlockNode("SeHeading", .1f, false, t => t.hData = block.Heading);
  343. if (l)
  344. {
  345. l.childAlignment = TextAnchor.UpperCenter;
  346. l.padding = new RectOffset(TextInset, TextInset, 0, 0);
  347. }
  348. else ThematicBreak();
  349. break;
  350. case BlockTag.AtxHeading:
  351. l = BlockNode("AtxHeading", .1f, false, t => t.hData = block.Heading);
  352. if (l) l.padding = new RectOffset(TextInset, TextInset, 0, 0);
  353. if (l && block.Heading.Level == 1)
  354. l.childAlignment = TextAnchor.UpperCenter;
  355. break;
  356. case BlockTag.Paragraph:
  357. l = BlockNode("Paragraph", .1f, false, spacer: 1.5f);
  358. if (l) l.padding = new RectOffset(TextInset, TextInset, 0, 0);
  359. break;
  360. case BlockTag.ThematicBreak:
  361. ThematicBreak();
  362. break;
  363. // TODO: add the rest of the tag types
  364. }
  365. }
  366. else if (node.Inline != null)
  367. { // inline element
  368. var inl = node.Inline;
  369. void Flag(CurrentTextFlags flag)
  370. {
  371. if (node.IsOpening)
  372. textFlags |= flag;
  373. else if (node.IsClosing)
  374. textFlags &= ~flag;
  375. }
  376. const float PSize = 3.5f;
  377. const float H1Size = 4.8f;
  378. const float HLevelDecrease = 0.5f;
  379. void EnsureText()
  380. {
  381. if (currentText == null)
  382. {
  383. Logger.md.Debug($"Adding new text element");
  384. var tt = layout.Peek().gameObject.GetComponent<TagTypeComponent>();
  385. currentText = BeatSaberUI.CreateText(layout.Peek(), "", Vector2.zero);
  386. currentText.gameObject.AddComponent<TextLinkDecoder>();
  387. /*if (Consolas != null)
  388. {
  389. // Set the font to Consolas so code blocks work
  390. currentText.font = Instantiate(Consolas);
  391. currentText.text = $"<font={DefaultFontName}>";
  392. }*/
  393. switch (tt.Tag)
  394. {
  395. case BlockTag.List:
  396. case BlockTag.ListItem:
  397. case BlockTag.Paragraph:
  398. currentText.fontSize = PSize;
  399. currentText.enableWordWrapping = true;
  400. break;
  401. case BlockTag.AtxHeading:
  402. var size = H1Size;
  403. size -= HLevelDecrease * (tt.hData.Level - 1);
  404. currentText.fontSize = size;
  405. currentText.enableWordWrapping = true;
  406. break;
  407. case BlockTag.SetextHeading:
  408. currentText.fontSize = H1Size;
  409. currentText.enableWordWrapping = true;
  410. break;
  411. // TODO: add other relevant types
  412. }
  413. texts.Add(currentText);
  414. }
  415. }
  416. switch (inl.Tag)
  417. {
  418. case InlineTag.String:
  419. EnsureText();
  420. string head = "<noparse>", tail = "</noparse>";
  421. if (textFlags.HasFlag(CurrentTextFlags.Bold))
  422. { head = "<b>" + head; tail += "</b>"; }
  423. if (textFlags.HasFlag(CurrentTextFlags.Italic))
  424. { head = "<i>" + head; tail += "</i>"; }
  425. if (textFlags.HasFlag(CurrentTextFlags.Strikethrough))
  426. { head = "<s>" + head; tail += "</s>"; }
  427. if (textFlags.HasFlag(CurrentTextFlags.Underline))
  428. { head = "<u>" + head; tail += "</u>"; }
  429. currentText.text += head + inl.LiteralContent + tail;
  430. break;
  431. case InlineTag.Strong:
  432. Flag(CurrentTextFlags.Bold);
  433. break;
  434. case InlineTag.Strikethrough:
  435. Flag(CurrentTextFlags.Strikethrough);
  436. break;
  437. case InlineTag.Emphasis:
  438. Flag(CurrentTextFlags.Italic);
  439. break;
  440. case InlineTag.Code:
  441. EnsureText();
  442. currentText.text += $"<link=\"$$codeBlock\"><noparse>{inl.LiteralContent}</noparse></link>";
  443. break;
  444. case InlineTag.Link:
  445. EnsureText();
  446. Flag(CurrentTextFlags.Underline);
  447. if (node.IsOpening)
  448. currentText.text += $"<color={LinkDefaultColor}><link=\"{ResolveUri(inl.TargetUrl)}\">";
  449. else if (node.IsClosing)
  450. currentText.text += "</link></color>";
  451. break;
  452. }
  453. }
  454. }
  455. yield return null; // delay one frame
  456. scrView.Setup();
  457. // this is the bullshit I have to use to make it work properly
  458. content.gameObject.GetComponent<VerticalLayoutGroup>().enabled = false;
  459. var childRt = content.GetChild(0) as RectTransform;
  460. childRt.anchoredPosition = new Vector2(0f, childRt.anchoredPosition.y);
  461. if (Consolas != null)
  462. {
  463. foreach (var link in texts.Select(t => t.textInfo.linkInfo).Aggregate<IEnumerable<TMP_LinkInfo>>(Enumerable.Concat).Where(l => l.GetLinkID() == "$$codeBlock"))
  464. {
  465. //link.textComponent.font = Consolas;
  466. var texinfo = link.textComponent.textInfo;
  467. texinfo.characterInfo[link.linkTextfirstCharacterIndex].DebugPrintTo(Logger.md.Debug, 2);
  468. for (int i = link.linkTextfirstCharacterIndex; i < link.linkTextfirstCharacterIndex + link.linkTextLength; i++)
  469. {
  470. texinfo.characterInfo[i].fontAsset = Consolas;
  471. texinfo.characterInfo[i].material = Consolas.material;
  472. texinfo.characterInfo[i].isUsingAlternateTypeface = true;
  473. }
  474. }
  475. foreach (var text in texts)
  476. {
  477. text.SetLayoutDirty();
  478. text.SetVerticesDirty();
  479. }
  480. }
  481. }
  482. private class TextLinkDecoder : MonoBehaviour, IPointerClickHandler
  483. {
  484. private TextMeshProUGUI tmp;
  485. public void Awake()
  486. {
  487. tmp = GetComponent<TextMeshProUGUI>();
  488. }
  489. public void OnPointerClick(PointerEventData eventData)
  490. {
  491. // this may not actually get me what i want
  492. int linkIndex = TMP_TextUtilities.FindIntersectingLink(tmp, eventData.pointerPressRaycast.worldPosition, null);
  493. if (linkIndex != -1)
  494. { // was a link clicked?
  495. TMP_LinkInfo linkInfo = tmp.textInfo.linkInfo[linkIndex];
  496. // open the link id as a url, which is the metadata we added in the text field
  497. var qualifiedUrl = linkInfo.GetLinkID();
  498. if (qualifiedUrl.StartsWith("$$"))
  499. return; // this means its used for something else
  500. Logger.md.Debug($"Link pressed {qualifiedUrl}");
  501. var uri = GetLinkUri(qualifiedUrl);
  502. if (uri != null)
  503. Process.Start(uri);
  504. }
  505. }
  506. private List<Color32[]> SetLinkToColor(int linkIndex, Color32 color)
  507. {
  508. TMP_LinkInfo linkInfo = tmp.textInfo.linkInfo[linkIndex];
  509. var oldVertColors = new List<Color32[]>(); // store the old character colors
  510. for (int i = 0; i < linkInfo.linkTextLength; i++)
  511. { // for each character in the link string
  512. int characterIndex = linkInfo.linkTextfirstCharacterIndex + i; // the character index into the entire text
  513. var charInfo = tmp.textInfo.characterInfo[characterIndex];
  514. int meshIndex = charInfo.materialReferenceIndex; // Get the index of the material / sub text object used by this character.
  515. int vertexIndex = charInfo.vertexIndex; // Get the index of the first vertex of this character.
  516. Color32[] vertexColors = tmp.textInfo.meshInfo[meshIndex].colors32; // the colors for this character
  517. oldVertColors.Add(vertexColors.ToArray());
  518. if (charInfo.isVisible)
  519. {
  520. vertexColors[vertexIndex + 0] = color;
  521. vertexColors[vertexIndex + 1] = color;
  522. vertexColors[vertexIndex + 2] = color;
  523. vertexColors[vertexIndex + 3] = color;
  524. }
  525. }
  526. // Update Geometry
  527. tmp.UpdateVertexData(TMP_VertexDataUpdateFlags.All);
  528. return oldVertColors;
  529. }
  530. }
  531. private void Clear()
  532. {
  533. content.gameObject.SetActive(false);
  534. void Clear(Transform target)
  535. {
  536. foreach (Transform child in target)
  537. {
  538. Clear(child);
  539. Logger.md.Debug($"Destroying {child.name}");
  540. child.SetParent(null);
  541. Destroy(child.gameObject);
  542. }
  543. }
  544. Clear(content);
  545. content.gameObject.SetActive(true);
  546. }
  547. }
  548. }