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.

798 lines
36 KiB

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using UnityEngine;
  5. using CommonMark;
  6. using CommonMark.Syntax;
  7. using UnityEngine.UI;
  8. using TMPro;
  9. using CustomUI.BeatSaber;
  10. using IPA.Utilities;
  11. using System.Reflection;
  12. using UnityEngine.EventSystems;
  13. using System.Diagnostics;
  14. using System.Collections;
  15. using System.IO;
  16. namespace BSIPA_ModList.UI.ViewControllers
  17. {
  18. [RequireComponent(typeof(RectTransform))]
  19. public class MarkdownView : MonoBehaviour
  20. {
  21. private class TagTypeComponent : MonoBehaviour
  22. {
  23. internal BlockTag Tag;
  24. internal HeadingData hData;
  25. internal ListData lData;
  26. internal int listCount;
  27. internal int listIndex;
  28. }
  29. private string markdown = "";
  30. private bool mdDirty = false;
  31. public string Markdown
  32. {
  33. get => markdown;
  34. set
  35. {
  36. markdown = value;
  37. mdDirty = true;
  38. }
  39. }
  40. public RectTransform rectTransform => GetComponent<RectTransform>();
  41. private ScrollView scrView;
  42. private RectTransform content;
  43. private RectTransform viewport;
  44. private CommonMarkSettings settings;
  45. public MarkdownView()
  46. {
  47. settings = CommonMarkSettings.Default.Clone();
  48. settings.AdditionalFeatures = CommonMarkAdditionalFeatures.All;
  49. settings.RenderSoftLineBreaksAsLineBreaks = false;
  50. settings.UriResolver = ResolveUri;
  51. }
  52. public Func<string, bool> HasEmbeddedImage;
  53. private string ResolveUri(string arg)
  54. {
  55. var name = arg.Substring(3);
  56. if (!arg.StartsWith("!::") && !arg.StartsWith("w::"))
  57. { // !:: means embedded, w:: means web
  58. // this block is for when neither is specified
  59. Logger.md.Debug($"Resolving nonspecific URI {arg}");
  60. // check if its embedded
  61. if (HasEmbeddedImage != null && HasEmbeddedImage(arg))
  62. return "!::" + arg;
  63. else
  64. return "w::" + arg;
  65. }
  66. Logger.md.Debug($"Resolved specific URI {arg}");
  67. return arg;
  68. }
  69. private static string GetLinkUri(string uri)
  70. {
  71. if (uri[0] == '!')
  72. {
  73. Logger.md.Error($"Cannot link to embedded resource in mod description");
  74. return null;
  75. }
  76. else
  77. return uri.Substring(3);
  78. }
  79. private static Stream ConsolasAssetBundleFontStream => Assembly.GetExecutingAssembly().GetManifestResourceStream("BSIPA_ModList.Bundles.consolas.font");
  80. private static AssetBundleCreateRequest _bundleRequest;
  81. private static AssetBundle _bundle;
  82. private static AssetBundle Bundle
  83. {
  84. get
  85. {
  86. if (_bundle == null && _bundleRequest != null)
  87. throw new InvalidOperationException("Asset bundle is being loaded asynchronously; please wait for that to complete");
  88. if (_bundle == null)
  89. _bundle = AssetBundle.LoadFromStream(ConsolasAssetBundleFontStream);
  90. return _bundle;
  91. }
  92. }
  93. private static AssetBundleRequest _consolasRequest;
  94. private static TMP_FontAsset _unsetConsolas;
  95. private static TMP_FontAsset _consolas;
  96. private static TMP_FontAsset Consolas
  97. {
  98. get
  99. {
  100. if (_unsetConsolas == null && _consolasRequest != null)
  101. throw new InvalidOperationException("Asset is being loaded asynchronously; please wait for that to complete");
  102. if (_unsetConsolas == null)
  103. _unsetConsolas = Bundle?.LoadAsset<TMP_FontAsset>("CONSOLAS");
  104. if (_consolas == null && _unsetConsolas != null)
  105. _consolas = SetupFont(_unsetConsolas);
  106. return _consolas;
  107. }
  108. }
  109. private static TMP_FontAsset SetupFont(TMP_FontAsset f)
  110. {
  111. var originalFont = Resources.FindObjectsOfTypeAll<TMP_FontAsset>().Last(f2 => f2.name == "Teko-Medium SDF No Glow");
  112. var matCopy = Instantiate(originalFont.material);
  113. matCopy.mainTexture = f.material.mainTexture;
  114. matCopy.mainTextureOffset = f.material.mainTextureOffset;
  115. matCopy.mainTextureScale = f.material.mainTextureScale;
  116. f.material = matCopy;
  117. f = Instantiate(f);
  118. MaterialReferenceManager.AddFontAsset(f);
  119. return f;
  120. }
  121. internal static void StartLoadResourcesAsync()
  122. {
  123. SharedCoroutineStarter.instance.StartCoroutine(LoadResourcesAsync());
  124. }
  125. private static IEnumerator LoadResourcesAsync()
  126. {
  127. Logger.md.Debug("Starting to load resources");
  128. _bundleRequest = AssetBundle.LoadFromStreamAsync(ConsolasAssetBundleFontStream);
  129. yield return _bundleRequest;
  130. _bundle = _bundleRequest.assetBundle;
  131. Logger.md.Debug("Bundle loaded");
  132. _consolasRequest = _bundle.LoadAssetAsync<TMP_FontAsset>("CONSOLAS");
  133. yield return _consolasRequest;
  134. _unsetConsolas = _consolasRequest.asset as TMP_FontAsset;
  135. Logger.md.Debug("Font loaded");
  136. }
  137. protected void Awake()
  138. {
  139. if (Consolas == null)
  140. Logger.md.Error($"Loading of Consolas font failed");
  141. gameObject.SetActive(false);
  142. var vpgo = new GameObject("Viewport");
  143. viewport = vpgo.AddComponent<RectTransform>();
  144. viewport.SetParent(transform);
  145. viewport.localPosition = Vector2.zero;
  146. viewport.anchorMin = Vector2.zero;
  147. viewport.anchorMax = Vector2.one;
  148. viewport.anchoredPosition = new Vector2(.5f, .5f);
  149. viewport.sizeDelta = Vector2.zero;
  150. var vpmask = vpgo.AddComponent<Mask>();
  151. var vpim = vpgo.AddComponent<Image>(); // supposedly Mask needs an Image?
  152. vpmask.showMaskGraphic = false;
  153. vpim.color = Color.white;
  154. vpim.sprite = WhitePixel;
  155. vpim.material = CustomUI.Utilities.UIUtilities.NoGlowMaterial;
  156. content = new GameObject("Content Wrapper").AddComponent<RectTransform>();
  157. content.SetParent(viewport);
  158. content.gameObject.AddComponent<TagTypeComponent>();
  159. content.localPosition = Vector2.zero;
  160. content.anchorMin = new Vector2(0f, 1f);
  161. content.anchorMax = new Vector2(1f, 1f);
  162. content.anchoredPosition = Vector2.zero;
  163. var contentLayout = content.gameObject.AddComponent<VerticalLayoutGroup>();
  164. var contentFitter = content.gameObject.AddComponent<ContentSizeFitter>();
  165. contentFitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize;
  166. contentFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
  167. contentLayout.childControlHeight = true;
  168. contentLayout.childControlWidth = false;
  169. contentLayout.childForceExpandHeight = false;
  170. contentLayout.childForceExpandWidth = true;
  171. contentLayout.childAlignment = TextAnchor.UpperCenter;
  172. contentLayout.spacing = 0f;
  173. var pageUp = Instantiate(Resources.FindObjectsOfTypeAll<Button>().Last((Button x) => x.name == "PageUpButton"), rectTransform, false);
  174. var pageDown = Instantiate(Resources.FindObjectsOfTypeAll<Button>().Last((Button x) => x.name == "PageDownButton"), rectTransform, false);
  175. {
  176. var pup_rt = pageUp.transform as RectTransform;
  177. var pup_sof = pup_rt.sizeDelta.y;
  178. var pup_xoff = (rectTransform.sizeDelta.x / 2) + (pup_sof / 2);
  179. pup_rt.anchoredPosition = new Vector2(pup_xoff, pup_rt.anchoredPosition.y);
  180. var pup_bg_rt = pup_rt.Find("BG") as RectTransform;
  181. pup_bg_rt.sizeDelta = new Vector2(pup_bg_rt.sizeDelta.y, pup_bg_rt.sizeDelta.y);
  182. // fix hitbox
  183. pup_rt.anchorMin = new Vector2(.5f, pup_rt.anchorMin.y);
  184. pup_rt.anchorMax = new Vector2(.5f, pup_rt.anchorMax.y);
  185. pup_rt.sizeDelta = new Vector2(pup_rt.sizeDelta.y, pup_rt.sizeDelta.y);
  186. }
  187. {
  188. var pdn_rt = pageDown.transform as RectTransform;
  189. var pdn_sof = pdn_rt.sizeDelta.y;
  190. var pdn_xoff = (rectTransform.sizeDelta.x / 2) + (pdn_sof / 2);
  191. pdn_rt.anchoredPosition = new Vector2(pdn_xoff, pdn_rt.anchoredPosition.y);
  192. var pdn_bg_rt = pdn_rt.Find("BG") as RectTransform;
  193. pdn_bg_rt.sizeDelta = new Vector2(pdn_bg_rt.sizeDelta.y, pdn_bg_rt.sizeDelta.y);
  194. // fix hitbox
  195. pdn_rt.anchorMin = new Vector2(.5f, pdn_rt.anchorMin.y);
  196. pdn_rt.anchorMax = new Vector2(.5f, pdn_rt.anchorMax.y);
  197. pdn_rt.sizeDelta = new Vector2(pdn_rt.sizeDelta.y, pdn_rt.sizeDelta.y);
  198. }
  199. scrView = gameObject.AddComponent<ScrollView>();
  200. scrView.SetPrivateField("_pageUpButton", pageUp);
  201. scrView.SetPrivateField("_pageDownButton", pageDown);
  202. scrView.SetPrivateField("_contentRectTransform", content);
  203. scrView.SetPrivateField("_viewport", viewport);
  204. gameObject.SetActive(true);
  205. }
  206. private static Sprite whitePixel;
  207. private static Sprite WhitePixel
  208. {
  209. get
  210. {
  211. if (whitePixel == null)
  212. whitePixel = Resources.FindObjectsOfTypeAll<Sprite>().First(s => s.name == "WhitePixel");
  213. return whitePixel;
  214. }
  215. }
  216. private static Sprite blockQuoteBackground;
  217. private static Sprite BlockQuoteBackground
  218. {
  219. get
  220. {
  221. if (blockQuoteBackground == null)
  222. blockQuoteBackground = Resources.FindObjectsOfTypeAll<Sprite>().First(s => s.name == "RoundRectNormal");
  223. return blockQuoteBackground;
  224. }
  225. }
  226. private static readonly Color BlockQuoteColor = new Color( 30f / 255, 109f / 255, 178f / 255, .25f);
  227. private static readonly Color BlockCodeColor = new Color(135f / 255, 135f / 255, 135f / 255, .5f);
  228. #if DEBUG
  229. #if UI_CONFIGURE_MARKDOWN_THEMATIC_BREAK
  230. private byte tbreakSettings = 0;
  231. #endif
  232. #endif
  233. public void Update()
  234. {
  235. #if DEBUG && UI_CONFIGURE_MARKDOWN_THEMATIC_BREAK
  236. if (Input.GetKeyDown(KeyCode.K))
  237. {
  238. tbreakSettings = (byte)((tbreakSettings + 1) % 16);
  239. UpdateMd();
  240. Logger.md.Info(tbreakSettings.ToString());
  241. }
  242. #endif
  243. if (mdDirty)
  244. StartCoroutine(UpdateMd());
  245. }
  246. [Flags]
  247. private enum CurrentTextFlags
  248. {
  249. None = 0, Bold = 1, Italic = 2, Underline = 4, Strikethrough = 8,
  250. }
  251. private const string LinkDefaultColor = "#0061ff";
  252. private const string LinkHoverColor = "#009dff";
  253. const float PSize = 3.5f;
  254. const float BlockCodeSize = PSize - .5f;
  255. const float H1Size = 5.5f;
  256. const float HLevelDecrease = 0.5f;
  257. private IEnumerator UpdateMd()
  258. {
  259. mdDirty = false;
  260. Clear();
  261. // enable so it will set stuff up
  262. content.gameObject.GetComponent<VerticalLayoutGroup>().enabled = true;
  263. var doc = CommonMarkConverter.Parse(markdown, settings);
  264. Stack<RectTransform> layout = new Stack<RectTransform>();
  265. layout.Push(content);
  266. TextMeshProUGUI currentText = null;
  267. List<TextMeshProUGUI> texts = new List<TextMeshProUGUI>();
  268. CurrentTextFlags textFlags = 0;
  269. foreach (var node in doc.AsEnumerable())
  270. {
  271. Logger.md.Debug($"node {node}");
  272. if (node.Block != null)
  273. {
  274. var block = node.Block;
  275. const float BreakHeight = .5f;
  276. const int TextInset = 1;
  277. const int BlockQuoteInset = TextInset * 2;
  278. const int BlockCodeInset = BlockQuoteInset;
  279. const int ListInset = TextInset;
  280. void Spacer(float size = 1.5f)
  281. {
  282. var go = new GameObject("Spacer", typeof(RectTransform));
  283. var vlayout = go.GetComponent<RectTransform>();
  284. vlayout.SetParent(layout.Peek());
  285. vlayout.anchorMin = new Vector2(.5f, .5f);
  286. vlayout.anchorMax = new Vector2(.5f, .5f);
  287. vlayout.localScale = Vector3.one;
  288. vlayout.localPosition = Vector3.zero;
  289. var l = go.AddComponent<LayoutElement>();
  290. l.minHeight = l.preferredHeight = size;
  291. }
  292. HorizontalOrVerticalLayoutGroup BlockNode(string name, float spacing, bool isVertical, Action<TagTypeComponent> apply = null, float? spacer = null, bool isDoc = false, bool matchWidth = false, float matchWidthDiff = 0f)
  293. {
  294. if (node.IsOpening)
  295. {
  296. Logger.md.Debug($"Creating block container {name}");
  297. currentText = null;
  298. var go = new GameObject(name, typeof(RectTransform));
  299. var vlayout = go.GetComponent<RectTransform>();
  300. vlayout.SetParent(layout.Peek());
  301. //vlayout.anchoredPosition = new Vector2(.5f, .5f);
  302. vlayout.anchorMin = new Vector2(.5f, .5f);
  303. vlayout.anchorMax = new Vector2(.5f, .5f);
  304. vlayout.localScale = Vector3.one;
  305. vlayout.localPosition = Vector3.zero;
  306. if (isDoc)
  307. {
  308. vlayout.sizeDelta = new Vector2(rectTransform.rect.width, 0f);
  309. vlayout.anchorMin = new Vector2(0f, 1f);
  310. vlayout.anchorMax = new Vector2(1f, 1f);
  311. }
  312. if (matchWidth)
  313. vlayout.sizeDelta = new Vector2(layout.Peek().rect.width-matchWidthDiff, 0f);
  314. var tt = go.AddComponent<TagTypeComponent>();
  315. tt.Tag = block.Tag;
  316. apply?.Invoke(tt);
  317. layout.Push(vlayout);
  318. HorizontalOrVerticalLayoutGroup l;
  319. if (isVertical)
  320. l = go.AddComponent<VerticalLayoutGroup>();
  321. else
  322. l = go.AddComponent<HorizontalLayoutGroup>();
  323. l.childControlHeight = l.childControlWidth = true;
  324. l.childForceExpandHeight = false;
  325. l.childForceExpandWidth = isDoc;
  326. l.spacing = spacing;
  327. if (isDoc)
  328. vlayout.anchoredPosition = new Vector2(0f, -vlayout.rect.height);
  329. return l;
  330. }
  331. else if (node.IsClosing)
  332. {
  333. currentText = null;
  334. layout.Pop();
  335. if (spacer.HasValue)
  336. Spacer(spacer.Value);
  337. }
  338. return null;
  339. }
  340. void ThematicBreak(float spacerSize = 1.5f)
  341. { // TODO: Fix positioning
  342. var go = new GameObject("ThematicBreak", typeof(RectTransform), typeof(HorizontalLayoutGroup));
  343. var vlayout = go.GetComponent<RectTransform>();
  344. vlayout.SetParent(layout.Peek());
  345. var l = go.GetComponent<HorizontalLayoutGroup>();
  346. #if DEBUG && UI_CONFIGURE_MARKDOWN_THEMATIC_BREAK
  347. l.childControlHeight = (tbreakSettings & 0b0001) != 0; // if set, not well behaved
  348. l.childControlWidth = (tbreakSettings & 0b0010) != 0;
  349. l.childForceExpandHeight = (tbreakSettings & 0b0100) != 0; // if set, not well behaved
  350. l.childForceExpandWidth = (tbreakSettings & 0b1000) != 0;
  351. #else
  352. l.childControlHeight = false;
  353. l.childControlWidth = false;
  354. l.childForceExpandHeight = false;
  355. l.childForceExpandWidth = false;
  356. #endif
  357. l.childAlignment = TextAnchor.UpperCenter;
  358. l.spacing = 0f;
  359. vlayout.localScale = Vector3.one;
  360. vlayout.anchoredPosition = Vector2.zero;
  361. vlayout.anchorMin = new Vector2(.5f, .5f);
  362. vlayout.anchorMax = new Vector2(.5f, .5f);
  363. vlayout.sizeDelta = new Vector2(layout.Peek().rect.width, BreakHeight);
  364. vlayout.localPosition = Vector3.zero;
  365. currentText = null;
  366. go = new GameObject("ThematicBreak Bar", typeof(RectTransform), typeof(Image), typeof(LayoutElement));
  367. var im = go.GetComponent<Image>();
  368. im.color = Color.white;
  369. // i think i need to copy the sprite because i'm using the same one for the mask
  370. im.sprite = Sprite.Create(WhitePixel.texture, WhitePixel.rect, Vector2.zero);
  371. im.material = CustomUI.Utilities.UIUtilities.NoGlowMaterial;
  372. var rt = go.GetComponent<RectTransform>();
  373. rt.SetParent(vlayout);
  374. var le = go.GetComponent<LayoutElement>();
  375. le.minWidth = le.preferredWidth = layout.Peek().rect.width;
  376. le.minHeight = le.preferredHeight = BreakHeight;
  377. le.flexibleHeight = le.flexibleWidth = 1f;
  378. rt.localScale = Vector3.one;
  379. rt.localPosition = Vector3.zero;
  380. rt.anchoredPosition = Vector3.zero;
  381. rt.anchorMin = Vector2.zero;
  382. rt.anchorMax = Vector2.one;
  383. rt.sizeDelta = new Vector2(layout.Peek().rect.width, BreakHeight);
  384. if (spacerSize != 0f)
  385. Spacer(spacerSize);
  386. }
  387. switch (block.Tag)
  388. {
  389. case BlockTag.Document:
  390. BlockNode("DocumentRoot", .5f, true, isDoc: true);
  391. break;
  392. case BlockTag.SetextHeading:
  393. var l = BlockNode("SeHeading", 0f, false, t => t.hData = block.Heading, spacer: 0f);
  394. if (l)
  395. {
  396. l.childAlignment = TextAnchor.UpperCenter;
  397. l.padding = new RectOffset(TextInset, TextInset, 0, 0);
  398. }
  399. else
  400. ThematicBreak(2f);
  401. break;
  402. case BlockTag.AtxHeading:
  403. l = BlockNode("AtxHeading", .1f, false, t => t.hData = block.Heading);
  404. if (l) l.padding = new RectOffset(TextInset, TextInset, 0, 0);
  405. if (l && block.Heading.Level == 1)
  406. l.childAlignment = TextAnchor.UpperCenter;
  407. break;
  408. case BlockTag.Paragraph:
  409. l = BlockNode("Paragraph", .1f, false, spacer: 1.5f);
  410. if (l) l.padding = new RectOffset(TextInset, TextInset, 0, 0);
  411. break;
  412. case BlockTag.ThematicBreak:
  413. ThematicBreak();
  414. break;
  415. // TODO: add the rest of the tag types
  416. case BlockTag.BlockQuote:
  417. l = BlockNode("BlockQuote", .1f, true, matchWidth: true, matchWidthDiff: BlockQuoteInset*2, spacer: 1.5f);
  418. if (l)
  419. {
  420. l.childForceExpandWidth = true;
  421. l.padding = new RectOffset(BlockQuoteInset, BlockQuoteInset, BlockQuoteInset, 0);
  422. var go = l.gameObject;
  423. var im = go.AddComponent<Image>();
  424. im.material = CustomUI.Utilities.UIUtilities.NoGlowMaterial;
  425. im.type = Image.Type.Sliced;
  426. im.sprite = Instantiate(BlockQuoteBackground);
  427. im.color = BlockQuoteColor;
  428. }
  429. break;
  430. case BlockTag.IndentedCode:
  431. case BlockTag.FencedCode:
  432. {
  433. currentText = null;
  434. var go = new GameObject("CodeBlock", typeof(RectTransform));
  435. var vlayout = go.GetComponent<RectTransform>();
  436. vlayout.SetParent(layout.Peek());
  437. vlayout.anchorMin = new Vector2(.5f, .5f);
  438. vlayout.anchorMax = new Vector2(.5f, .5f);
  439. vlayout.localScale = Vector3.one;
  440. vlayout.localPosition = Vector3.zero;
  441. vlayout.sizeDelta = new Vector2(layout.Peek().rect.width - BlockCodeInset * 2, 0f);
  442. var tt = go.AddComponent<TagTypeComponent>();
  443. tt.Tag = block.Tag;
  444. l = go.AddComponent<VerticalLayoutGroup>();
  445. l.childControlHeight = l.childControlWidth = true;
  446. l.childForceExpandHeight = false;
  447. l.childForceExpandWidth = true;
  448. l.spacing = 1.5f;
  449. l.padding = new RectOffset(BlockCodeInset, BlockCodeInset, BlockCodeInset, BlockCodeInset);
  450. var im = go.AddComponent<Image>();
  451. im.material = CustomUI.Utilities.UIUtilities.NoGlowMaterial;
  452. im.type = Image.Type.Sliced;
  453. im.sprite = Instantiate(BlockQuoteBackground);
  454. im.color = BlockCodeColor;
  455. var text = BeatSaberUI.CreateText(vlayout, $"<noparse>{block.StringContent}</noparse>", Vector2.zero);
  456. text.fontSize = BlockCodeSize;
  457. text.font = Consolas;
  458. }
  459. break;
  460. case BlockTag.List:
  461. l = BlockNode("List", .05f, true, t => t.lData = block.ListData, matchWidth: true, matchWidthDiff: ListInset * 2, spacer: 1.5f);
  462. if (l)
  463. {
  464. l.childForceExpandWidth = true;
  465. l.padding = new RectOffset(ListInset, ListInset, 0, 0);
  466. var go = l.gameObject;
  467. var tt = go.GetComponent<TagTypeComponent>();
  468. // count up children
  469. var count = 0;
  470. for (var c = block.FirstChild; c != null; c = c.NextSibling) count++;
  471. tt.listCount = count;
  472. tt.listIndex = 0;
  473. }
  474. break;
  475. case BlockTag.ListItem:
  476. l = BlockNode("ListItem", .05f, false, matchWidth: true, spacer: null);
  477. if (l)
  478. { // TODO: this is mega scuffed
  479. l.childForceExpandWidth = true;
  480. var go = l.gameObject;
  481. var rt = go.GetComponent<RectTransform>();
  482. var tt = go.GetComponent<TagTypeComponent>();
  483. var ptt = rt.parent.gameObject.GetComponent<TagTypeComponent>();
  484. var index = ptt.listIndex++;
  485. var listCount = ptt.listCount;
  486. var maxNum = listCount + ptt.lData.Start;
  487. var numChars = (int)Math.Floor(Math.Log10(maxNum) + 1);
  488. var cNum = index + ptt.lData.Start;
  489. var lt = ptt.lData.ListType;
  490. var id = lt == ListType.Bullet ? ptt.lData.BulletChar.ToString() : (cNum + (ptt.lData.Delimiter == ListDelimiter.Parenthesis ? ")" : "."));
  491. var ident = BeatSaberUI.CreateText(rt, $"<nobr>{id} </nobr>\n", Vector2.zero);
  492. if (lt == ListType.Ordered) // pad it out to fill space
  493. ident.text += $"<nobr><mspace=1em>{new string(' ', numChars + 1)}</mspace></nobr>";
  494. var contGo = new GameObject("Content", typeof(RectTransform));
  495. var vlayout = contGo.GetComponent<RectTransform>();
  496. vlayout.SetParent(rt);
  497. vlayout.anchorMin = new Vector2(.5f, .5f);
  498. vlayout.anchorMax = new Vector2(.5f, .5f);
  499. vlayout.localScale = Vector3.one;
  500. vlayout.localPosition = Vector3.zero;
  501. //vlayout.sizeDelta = new Vector2(rt.rect.width, 0f);
  502. var tt2 = contGo.AddComponent<TagTypeComponent>();
  503. tt2.Tag = block.Tag;
  504. var csf = contGo.AddComponent<ContentSizeFitter>();
  505. csf.horizontalFit = ContentSizeFitter.FitMode.PreferredSize;
  506. csf.verticalFit = ContentSizeFitter.FitMode.Unconstrained;
  507. l = contGo.AddComponent<VerticalLayoutGroup>();
  508. l.childAlignment = TextAnchor.UpperLeft;
  509. l.childControlHeight = l.childControlWidth = true;
  510. l.childForceExpandHeight = false;
  511. l.childForceExpandWidth = true;
  512. l.spacing = .5f;
  513. layout.Push(vlayout);
  514. }
  515. else
  516. layout.Pop(); // pop one more to clear content rect from stack
  517. break;
  518. case BlockTag.HtmlBlock:
  519. break;
  520. case BlockTag.ReferenceDefinition: // i have no idea what the state looks like here
  521. //block.DebugPrintTo(Logger.md.Info, 5);
  522. break;
  523. }
  524. }
  525. else if (node.Inline != null)
  526. { // inline element
  527. var inl = node.Inline;
  528. void Flag(CurrentTextFlags flag)
  529. {
  530. if (node.IsOpening)
  531. textFlags |= flag;
  532. else if (node.IsClosing)
  533. textFlags &= ~flag;
  534. }
  535. void EnsureText()
  536. {
  537. if (currentText == null)
  538. {
  539. Logger.md.Debug($"Adding new text element");
  540. var tt = layout.Peek().gameObject.GetComponent<TagTypeComponent>();
  541. currentText = BeatSaberUI.CreateText(layout.Peek(), "", Vector2.zero);
  542. currentText.gameObject.AddComponent<TextLinkDecoder>();
  543. currentText.enableWordWrapping = true;
  544. switch (tt.Tag)
  545. {
  546. case BlockTag.List:
  547. case BlockTag.ListItem:
  548. case BlockTag.Paragraph:
  549. currentText.fontSize = PSize;
  550. break;
  551. case BlockTag.AtxHeading:
  552. var size = H1Size;
  553. size -= HLevelDecrease * (tt.hData.Level - 1);
  554. currentText.fontSize = size;
  555. if (tt.hData.Level == 1)
  556. currentText.alignment = TextAlignmentOptions.Center;
  557. break;
  558. case BlockTag.SetextHeading:
  559. currentText.fontSize = H1Size;
  560. currentText.alignment = TextAlignmentOptions.Center;
  561. break;
  562. // TODO: add other relevant types
  563. }
  564. texts.Add(currentText);
  565. }
  566. }
  567. switch (inl.Tag)
  568. {
  569. case InlineTag.String:
  570. EnsureText();
  571. string head = "<noparse>", tail = "</noparse>";
  572. if (textFlags.HasFlag(CurrentTextFlags.Bold))
  573. { head = "<b>" + head; tail += "</b>"; }
  574. if (textFlags.HasFlag(CurrentTextFlags.Italic))
  575. { head = "<i>" + head; tail += "</i>"; }
  576. if (textFlags.HasFlag(CurrentTextFlags.Strikethrough))
  577. { head = "<s>" + head; tail += "</s>"; }
  578. if (textFlags.HasFlag(CurrentTextFlags.Underline))
  579. { head = "<u>" + head; tail += "</u>"; }
  580. currentText.text += head + inl.LiteralContent + tail;
  581. break;
  582. case InlineTag.Strong:
  583. Flag(CurrentTextFlags.Bold);
  584. break;
  585. case InlineTag.Strikethrough:
  586. Flag(CurrentTextFlags.Strikethrough);
  587. break;
  588. case InlineTag.Emphasis:
  589. Flag(CurrentTextFlags.Italic);
  590. break;
  591. case InlineTag.Code:
  592. EnsureText();
  593. currentText.text += $"<font=\"CONSOLAS\"><size=80%><mark=#A0A0C080><noparse>{inl.LiteralContent}</noparse></mark></size></font>";
  594. break;
  595. case InlineTag.SoftBreak:
  596. EnsureText();
  597. currentText.text += " "; // soft breaks translate to a space
  598. break;
  599. case InlineTag.LineBreak:
  600. EnsureText();
  601. currentText.text += "\n"; // line breaks translate to a new line
  602. break;
  603. case InlineTag.Link:
  604. EnsureText();
  605. Flag(CurrentTextFlags.Underline);
  606. if (node.IsOpening)
  607. currentText.text += $"<color={LinkDefaultColor}><link=\"{ResolveUri(inl.TargetUrl)}\">";
  608. else if (node.IsClosing)
  609. currentText.text += "</link></color>";
  610. break;
  611. case InlineTag.RawHtml:
  612. EnsureText();
  613. currentText.text += inl.LiteralContent;
  614. break;
  615. case InlineTag.Placeholder:
  616. break;
  617. }
  618. }
  619. }
  620. yield return null; // delay one frame
  621. scrView.Setup();
  622. // this is the bullshit I have to use to make it work properly
  623. content.gameObject.GetComponent<VerticalLayoutGroup>().enabled = false;
  624. var childRt = content.GetChild(0) as RectTransform;
  625. childRt.anchoredPosition = new Vector2(0f, childRt.anchoredPosition.y);
  626. }
  627. private class TextLinkDecoder : MonoBehaviour, IPointerClickHandler
  628. {
  629. private TextMeshProUGUI tmp;
  630. public void Awake()
  631. {
  632. tmp = GetComponent<TextMeshProUGUI>();
  633. }
  634. public void OnPointerClick(PointerEventData eventData)
  635. {
  636. // this may not actually get me what i want
  637. int linkIndex = TMP_TextUtilities.FindIntersectingLink(tmp, eventData.pointerPressRaycast.worldPosition, null);
  638. if (linkIndex != -1)
  639. { // was a link clicked?
  640. TMP_LinkInfo linkInfo = tmp.textInfo.linkInfo[linkIndex];
  641. // open the link id as a url, which is the metadata we added in the text field
  642. var qualifiedUrl = linkInfo.GetLinkID();
  643. if (qualifiedUrl.StartsWith("$$"))
  644. return; // this means its used for something else
  645. Logger.md.Debug($"Link pressed {qualifiedUrl}");
  646. var uri = GetLinkUri(qualifiedUrl);
  647. if (uri != null)
  648. Process.Start(uri);
  649. }
  650. }
  651. private List<Color32[]> SetLinkToColor(int linkIndex, Color32 color)
  652. {
  653. TMP_LinkInfo linkInfo = tmp.textInfo.linkInfo[linkIndex];
  654. var oldVertColors = new List<Color32[]>(); // store the old character colors
  655. for (int i = 0; i < linkInfo.linkTextLength; i++)
  656. { // for each character in the link string
  657. int characterIndex = linkInfo.linkTextfirstCharacterIndex + i; // the character index into the entire text
  658. var charInfo = tmp.textInfo.characterInfo[characterIndex];
  659. int meshIndex = charInfo.materialReferenceIndex; // Get the index of the material / sub text object used by this character.
  660. int vertexIndex = charInfo.vertexIndex; // Get the index of the first vertex of this character.
  661. Color32[] vertexColors = tmp.textInfo.meshInfo[meshIndex].colors32; // the colors for this character
  662. oldVertColors.Add(vertexColors.ToArray());
  663. if (charInfo.isVisible)
  664. {
  665. vertexColors[vertexIndex + 0] = color;
  666. vertexColors[vertexIndex + 1] = color;
  667. vertexColors[vertexIndex + 2] = color;
  668. vertexColors[vertexIndex + 3] = color;
  669. }
  670. }
  671. // Update Geometry
  672. tmp.UpdateVertexData(TMP_VertexDataUpdateFlags.All);
  673. return oldVertColors;
  674. }
  675. }
  676. private void Clear()
  677. {
  678. content.gameObject.SetActive(false);
  679. void Clear(Transform target)
  680. {
  681. foreach (Transform child in target)
  682. {
  683. Clear(child);
  684. Logger.md.Debug($"Destroying {child.name}");
  685. child.SetParent(null);
  686. Destroy(child.gameObject);
  687. }
  688. }
  689. Clear(content);
  690. content.gameObject.SetActive(true);
  691. }
  692. }
  693. }