|
|
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using UnityEngine;
- using CommonMark;
- using CommonMark.Syntax;
- using UnityEngine.UI;
- using TMPro;
- using CustomUI.BeatSaber;
- using IPA.Utilities;
- using System.Reflection;
- using UnityEngine.EventSystems;
- using System.Diagnostics;
- using System.Collections;
- using System.IO;
- using System.Text;
- using System.Text.RegularExpressions;
-
- namespace BSIPA_ModList.UI.ViewControllers
- {
- /// <summary>
- /// A UI component that renders CommonMark Markdown in-game.
- /// </summary>
- [RequireComponent(typeof(RectTransform))]
- public class MarkdownView : MonoBehaviour
- {
- private class TagTypeComponent : MonoBehaviour
- {
- internal BlockTag Tag;
- internal HeadingData hData;
- internal ListData lData;
- internal int listCount;
- internal int listIndex;
- }
-
- private string markdown = "";
- private bool mdDirty = false;
-
- /// <summary>
- /// The text to be rendered.
- /// </summary>
- /// <remarks>
- /// When this is assigned, the object is marked dirty. It will re-render on the next Update tick.
- /// </remarks>
- /// <value>the text to render as Markdown</value>
- public string Markdown
- {
- get => markdown;
- set
- {
- markdown = value;
- mdDirty = true;
- }
- }
-
- /// <summary>
- /// A convenience property to access the <see cref="RectTransform"/> on the <see cref="GameObject"/> this is on.
- /// </summary>
- /// <value>the <see cref="RectTransform"/> associated with this component</value>
- public RectTransform rectTransform => GetComponent<RectTransform>();
-
- private ScrollView scrView;
- private RectTransform content;
- private RectTransform viewport;
-
- private CommonMarkSettings settings;
-
- /// <summary>
- /// Creates a new <see cref="MarkdownView"/>. Should never be called directly. Instead, use <see cref="GameObject.AddComponent{T}"/>.
- /// </summary>
- public MarkdownView()
- {
- settings = CommonMarkSettings.Default.Clone();
- settings.AdditionalFeatures = CommonMarkAdditionalFeatures.All;
- settings.RenderSoftLineBreaksAsLineBreaks = false;
- settings.UriResolver = ResolveUri;
- }
-
- /// <summary>
- /// This function will be called whenever attempting to resolve an image URI, to ensure that the image exists in the embedded assembly.
- /// </summary>
- /// <value>a delegate for the function to call</value>
- public Func<string, bool> HasEmbeddedImage;
-
- private string ResolveUri(string arg)
- {
- var name = arg.Substring(3);
- if (!arg.StartsWith("!::") && !arg.StartsWith("w::"))
- { // !:: means embedded, w:: means web
- // this block is for when neither is specified
-
- Logger.md.Debug($"Resolving nonspecific URI {arg}");
- // check if its embedded
- if (HasEmbeddedImage != null && HasEmbeddedImage(arg))
- return "!::" + arg;
- else
- return "w::" + arg;
- }
-
- Logger.md.Debug($"Resolved specific URI {arg}");
- return arg;
- }
-
- private static string GetLinkUri(string uri)
- {
- if (uri[0] == '!')
- {
- Logger.md.Error($"Cannot link to embedded resource in mod description");
- return null;
- }
- else
- return uri.Substring(3);
- }
-
- private static Stream ConsolasAssetBundleFontStream => Assembly.GetExecutingAssembly().GetManifestResourceStream("BSIPA_ModList.Bundles.consolas.font");
-
- private static AssetBundleCreateRequest _bundleRequest;
- private static AssetBundle _bundle;
- private static AssetBundle Bundle
- {
- get
- {
- if (_bundle == null && _bundleRequest != null)
- throw new InvalidOperationException("Asset bundle is being loaded asynchronously; please wait for that to complete");
- if (_bundle == null)
- _bundle = AssetBundle.LoadFromStream(ConsolasAssetBundleFontStream);
- return _bundle;
- }
- }
-
- private static AssetBundleRequest _consolasRequest;
- private static TMP_FontAsset _unsetConsolas;
- private static TMP_FontAsset _consolas;
- private static TMP_FontAsset Consolas
- {
- get
- {
- if (_unsetConsolas == null && _consolasRequest != null)
- throw new InvalidOperationException("Asset is being loaded asynchronously; please wait for that to complete");
- if (_unsetConsolas == null)
- _unsetConsolas = Bundle?.LoadAsset<TMP_FontAsset>("CONSOLAS");
- if (_consolas == null && _unsetConsolas != null)
- _consolas = SetupFont(_unsetConsolas);
- return _consolas;
- }
- }
-
- private static TMP_FontAsset SetupFont(TMP_FontAsset f)
- {
- var originalFont = Resources.FindObjectsOfTypeAll<TMP_FontAsset>().Last(f2 => f2.name == "Teko-Medium SDF No Glow");
- var matCopy = Instantiate(originalFont.material);
- matCopy.mainTexture = f.material.mainTexture;
- matCopy.mainTextureOffset = f.material.mainTextureOffset;
- matCopy.mainTextureScale = f.material.mainTextureScale;
- f.material = matCopy;
- f = Instantiate(f);
- MaterialReferenceManager.AddFontAsset(f);
- return f;
- }
-
- internal static void StartLoadResourcesAsync()
- {
- SharedCoroutineStarter.instance.StartCoroutine(LoadResourcesAsync());
- }
- private static IEnumerator LoadResourcesAsync()
- {
- Logger.md.Debug("Starting to load resources");
-
- _bundleRequest = AssetBundle.LoadFromStreamAsync(ConsolasAssetBundleFontStream);
- yield return _bundleRequest;
- _bundle = _bundleRequest.assetBundle;
-
- Logger.md.Debug("Bundle loaded");
-
- _consolasRequest = _bundle.LoadAssetAsync<TMP_FontAsset>("CONSOLAS");
- yield return _consolasRequest;
- _unsetConsolas = _consolasRequest.asset as TMP_FontAsset;
-
- Logger.md.Debug("Font loaded");
- }
-
- internal void Awake()
- {
- if (Consolas == null)
- Logger.md.Error($"Loading of Consolas font failed");
-
- gameObject.SetActive(false);
-
- var vpgo = new GameObject("Viewport");
- viewport = vpgo.AddComponent<RectTransform>();
- viewport.SetParent(transform);
- viewport.localPosition = Vector2.zero;
- viewport.anchorMin = Vector2.zero;
- viewport.anchorMax = Vector2.one;
- viewport.anchoredPosition = new Vector2(.5f, .5f);
- viewport.sizeDelta = Vector2.zero;
- var vpmask = vpgo.AddComponent<Mask>();
- var vpim = vpgo.AddComponent<Image>(); // supposedly Mask needs an Image?
- vpmask.showMaskGraphic = false;
- vpim.color = Color.white;
- vpim.sprite = WhitePixel;
- vpim.material = CustomUI.Utilities.UIUtilities.NoGlowMaterial;
-
- content = new GameObject("Content Wrapper").AddComponent<RectTransform>();
- content.SetParent(viewport);
- content.gameObject.AddComponent<TagTypeComponent>();
- content.localPosition = Vector2.zero;
- content.anchorMin = new Vector2(0f, 1f);
- content.anchorMax = new Vector2(1f, 1f);
- content.anchoredPosition = Vector2.zero;
- var contentLayout = content.gameObject.AddComponent<VerticalLayoutGroup>();
- var contentFitter = content.gameObject.AddComponent<ContentSizeFitter>();
- contentFitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize;
- contentFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
- contentLayout.childControlHeight = true;
- contentLayout.childControlWidth = false;
- contentLayout.childForceExpandHeight = false;
- contentLayout.childForceExpandWidth = true;
- contentLayout.childAlignment = TextAnchor.UpperCenter;
- contentLayout.spacing = 0f;
-
- var pageUp = Instantiate(Resources.FindObjectsOfTypeAll<Button>().Last((Button x) => x.name == "PageUpButton"), rectTransform, false);
- var pageDown = Instantiate(Resources.FindObjectsOfTypeAll<Button>().Last((Button x) => x.name == "PageDownButton"), rectTransform, false);
-
- {
- var pup_rt = pageUp.transform as RectTransform;
- var pup_sof = pup_rt.sizeDelta.y;
- var pup_xoff = (rectTransform.sizeDelta.x / 2) + (pup_sof / 2);
- pup_rt.anchoredPosition = new Vector2(pup_xoff, pup_rt.anchoredPosition.y);
- var pup_bg_rt = pup_rt.Find("BG") as RectTransform;
- pup_bg_rt.sizeDelta = new Vector2(pup_bg_rt.sizeDelta.y, pup_bg_rt.sizeDelta.y);
-
- // fix hitbox
- pup_rt.anchorMin = new Vector2(.5f, pup_rt.anchorMin.y);
- pup_rt.anchorMax = new Vector2(.5f, pup_rt.anchorMax.y);
- pup_rt.sizeDelta = new Vector2(pup_rt.sizeDelta.y, pup_rt.sizeDelta.y);
- }
- {
- var pdn_rt = pageDown.transform as RectTransform;
- var pdn_sof = pdn_rt.sizeDelta.y;
- var pdn_xoff = (rectTransform.sizeDelta.x / 2) + (pdn_sof / 2);
- pdn_rt.anchoredPosition = new Vector2(pdn_xoff, pdn_rt.anchoredPosition.y);
- var pdn_bg_rt = pdn_rt.Find("BG") as RectTransform;
- pdn_bg_rt.sizeDelta = new Vector2(pdn_bg_rt.sizeDelta.y, pdn_bg_rt.sizeDelta.y);
-
- // fix hitbox
- pdn_rt.anchorMin = new Vector2(.5f, pdn_rt.anchorMin.y);
- pdn_rt.anchorMax = new Vector2(.5f, pdn_rt.anchorMax.y);
- pdn_rt.sizeDelta = new Vector2(pdn_rt.sizeDelta.y, pdn_rt.sizeDelta.y);
- }
-
- scrView = gameObject.AddComponent<ScrollView>();
- scrView.SetField("_pageUpButton", pageUp);
- scrView.SetField("_pageDownButton", pageDown);
- scrView.SetField("_contentRectTransform", content);
- scrView.SetField("_viewport", viewport);
-
- gameObject.SetActive(true);
- }
-
- private static Sprite whitePixel;
- private static Sprite WhitePixel
- {
- get
- {
- if (whitePixel == null)
- whitePixel = Resources.FindObjectsOfTypeAll<Sprite>().First(s => s.name == "WhitePixel");
- return whitePixel;
- }
- }
-
- private static Sprite blockQuoteBackground;
- private static Sprite BlockQuoteBackground
- {
- get
- {
- if (blockQuoteBackground == null)
- blockQuoteBackground = Resources.FindObjectsOfTypeAll<Sprite>().First(s => s.name == "RoundRectNormal");
- return blockQuoteBackground;
- }
- }
-
- private static readonly Color BlockQuoteColor = new Color( 30f / 255, 109f / 255, 178f / 255, .25f);
- private static readonly Color BlockCodeColor = new Color(135f / 255, 135f / 255, 135f / 255, .5f);
-
- #if DEBUG
- #if UI_CONFIGURE_MARKDOWN_THEMATIC_BREAK
- private byte tbreakSettings = 0;
- #endif
- #endif
- internal void Update()
- {
- #if DEBUG && UI_CONFIGURE_MARKDOWN_THEMATIC_BREAK
- if (Input.GetKeyDown(KeyCode.K))
- {
- tbreakSettings = (byte)((tbreakSettings + 1) % 16);
- UpdateMd();
- Logger.md.Info(tbreakSettings.ToString());
- }
- #endif
- if (mdDirty)
- StartCoroutine(UpdateMd());
- }
-
- [Flags]
- private enum CurrentTextFlags
- {
- None = 0, Bold = 1, Italic = 2, Underline = 4, Strikethrough = 8,
- }
-
- private const string LinkDefaultColor = "#59B0F4";
- // private const string LinkHoverColor = "#009dff";
-
- private static readonly Regex linkRefTitleColorRegex = new Regex(@"\{\s*color:\s*#?([a-fA-F0-9]{6})\s*\}", RegexOptions.Compiled | RegexOptions.Singleline);
-
- const float PSize = 3.5f;
- const float BlockCodeSize = PSize - .5f;
- const float H1Size = 5.5f;
- const float HLevelDecrease = 0.5f;
-
- private IEnumerator UpdateMd()
- {
- mdDirty = false;
- Clear();
-
- // enable so it will set stuff up
- content.gameObject.GetComponent<VerticalLayoutGroup>().enabled = true;
-
- var doc = CommonMarkConverter.Parse(markdown, settings);
-
- Stack<RectTransform> layout = new Stack<RectTransform>();
- layout.Push(content);
- TextMeshProUGUI currentText = null;
- List<TextMeshProUGUI> texts = new List<TextMeshProUGUI>();
- CurrentTextFlags textFlags = 0;
- foreach (var node in doc.AsEnumerable())
- {
- Logger.md.Debug($"node {node}");
-
- if (node.Block != null)
- {
- var block = node.Block;
-
- const float BreakHeight = .5f;
- const int TextInset = 1;
- const int BlockQuoteInset = TextInset * 2;
- const int BlockCodeInset = BlockQuoteInset;
- const int ListInset = TextInset;
-
- void Spacer(float size = 1.5f)
- {
- var go = new GameObject("Spacer", typeof(RectTransform));
- var vlayout = go.GetComponent<RectTransform>();
- vlayout.SetParent(layout.Peek());
- vlayout.anchorMin = new Vector2(.5f, .5f);
- vlayout.anchorMax = new Vector2(.5f, .5f);
- vlayout.localScale = Vector3.one;
- vlayout.localPosition = Vector3.zero;
-
- var l = go.AddComponent<LayoutElement>();
- l.minHeight = l.preferredHeight = size;
- }
-
- HorizontalOrVerticalLayoutGroup BlockNode(string name, float spacing, bool isVertical, Action<TagTypeComponent> apply = null, float? spacer = null, bool isDoc = false, bool matchWidth = false, float matchWidthDiff = 0f)
- {
- if (node.IsOpening)
- {
- Logger.md.Debug($"Creating block container {name}");
-
- currentText = null;
- var go = new GameObject(name, typeof(RectTransform));
- var vlayout = go.GetComponent<RectTransform>();
- vlayout.SetParent(layout.Peek());
- //vlayout.anchoredPosition = new Vector2(.5f, .5f);
- vlayout.anchorMin = new Vector2(.5f, .5f);
- vlayout.anchorMax = new Vector2(.5f, .5f);
- vlayout.localScale = Vector3.one;
- vlayout.localPosition = Vector3.zero;
- if (isDoc)
- {
- vlayout.sizeDelta = new Vector2(rectTransform.rect.width, 0f);
- vlayout.anchorMin = new Vector2(0f, 1f);
- vlayout.anchorMax = new Vector2(1f, 1f);
- }
- if (matchWidth)
- vlayout.sizeDelta = new Vector2(layout.Peek().rect.width-matchWidthDiff, 0f);
-
- var tt = go.AddComponent<TagTypeComponent>();
- tt.Tag = block.Tag;
- apply?.Invoke(tt);
- layout.Push(vlayout);
-
- HorizontalOrVerticalLayoutGroup l;
- if (isVertical)
- l = go.AddComponent<VerticalLayoutGroup>();
- else
- l = go.AddComponent<HorizontalLayoutGroup>();
-
- l.childControlHeight = l.childControlWidth = true;
- l.childForceExpandHeight = false;
- l.childForceExpandWidth = isDoc;
- l.spacing = spacing;
-
- if (isDoc)
- vlayout.anchoredPosition = new Vector2(0f, -vlayout.rect.height);
-
- return l;
- }
- else if (node.IsClosing)
- {
- currentText = null;
- layout.Pop();
-
- if (spacer.HasValue)
- Spacer(spacer.Value);
- }
- return null;
- }
-
- void ThematicBreak(float spacerSize = 1.5f)
- { // TODO: Fix positioning
- var go = new GameObject("ThematicBreak", typeof(RectTransform), typeof(HorizontalLayoutGroup));
- var vlayout = go.GetComponent<RectTransform>();
- vlayout.SetParent(layout.Peek());
- var l = go.GetComponent<HorizontalLayoutGroup>();
- #if DEBUG && UI_CONFIGURE_MARKDOWN_THEMATIC_BREAK
- l.childControlHeight = (tbreakSettings & 0b0001) != 0; // if set, not well behaved
- l.childControlWidth = (tbreakSettings & 0b0010) != 0;
- l.childForceExpandHeight = (tbreakSettings & 0b0100) != 0; // if set, not well behaved
- l.childForceExpandWidth = (tbreakSettings & 0b1000) != 0;
- #else
- l.childControlHeight = false;
- l.childControlWidth = false;
- l.childForceExpandHeight = false;
- l.childForceExpandWidth = false;
- #endif
- l.childAlignment = TextAnchor.UpperCenter;
- l.spacing = 0f;
-
- vlayout.localScale = Vector3.one;
- vlayout.anchoredPosition = Vector2.zero;
- vlayout.anchorMin = new Vector2(.5f, .5f);
- vlayout.anchorMax = new Vector2(.5f, .5f);
- vlayout.sizeDelta = new Vector2(layout.Peek().rect.width, BreakHeight);
- vlayout.localPosition = Vector3.zero;
-
- currentText = null;
- go = new GameObject("ThematicBreak Bar", typeof(RectTransform), typeof(Image), typeof(LayoutElement));
- var im = go.GetComponent<Image>();
- im.color = Color.white;
- // i think i need to copy the sprite because i'm using the same one for the mask
- im.sprite = Sprite.Create(WhitePixel.texture, WhitePixel.rect, Vector2.zero);
- im.material = CustomUI.Utilities.UIUtilities.NoGlowMaterial;
- var rt = go.GetComponent<RectTransform>();
- rt.SetParent(vlayout);
- var le = go.GetComponent<LayoutElement>();
- le.minWidth = le.preferredWidth = layout.Peek().rect.width;
- le.minHeight = le.preferredHeight = BreakHeight;
- le.flexibleHeight = le.flexibleWidth = 1f;
- rt.localScale = Vector3.one;
- rt.localPosition = Vector3.zero;
- rt.anchoredPosition = Vector3.zero;
- rt.anchorMin = Vector2.zero;
- rt.anchorMax = Vector2.one;
- rt.sizeDelta = new Vector2(layout.Peek().rect.width, BreakHeight);
-
- if (spacerSize != 0f)
- Spacer(spacerSize);
- }
-
- switch (block.Tag)
- {
- case BlockTag.Document:
- BlockNode("DocumentRoot", .5f, true, isDoc: true);
- break;
- case BlockTag.SetextHeading:
- var l = BlockNode("SeHeading", 0f, false, t => t.hData = block.Heading, spacer: 0f);
- if (l)
- {
- l.childAlignment = TextAnchor.UpperCenter;
- l.padding = new RectOffset(TextInset, TextInset, 0, 0);
- }
- else
- ThematicBreak(2f);
- break;
- case BlockTag.AtxHeading:
- l = BlockNode("AtxHeading", .1f, false, t => t.hData = block.Heading);
- if (l) l.padding = new RectOffset(TextInset, TextInset, 0, 0);
- if (l && block.Heading.Level == 1)
- l.childAlignment = TextAnchor.UpperCenter;
- break;
- case BlockTag.Paragraph:
- l = BlockNode("Paragraph", .1f, false, spacer: 1.5f);
- if (l) l.padding = new RectOffset(TextInset, TextInset, 0, 0);
- break;
- case BlockTag.ThematicBreak:
- ThematicBreak();
- break;
- // TODO: add the rest of the tag types
- case BlockTag.BlockQuote:
- l = BlockNode("BlockQuote", .1f, true, matchWidth: true, matchWidthDiff: BlockQuoteInset*2, spacer: 1.5f);
- if (l)
- {
- l.childForceExpandWidth = true;
- l.padding = new RectOffset(BlockQuoteInset, BlockQuoteInset, BlockQuoteInset, 0);
- var go = l.gameObject;
-
- var im = go.AddComponent<Image>();
- im.material = CustomUI.Utilities.UIUtilities.NoGlowMaterial;
- im.type = Image.Type.Sliced;
- im.sprite = Instantiate(BlockQuoteBackground);
- im.color = BlockQuoteColor;
- }
- break;
- case BlockTag.IndentedCode:
- case BlockTag.FencedCode:
- {
- currentText = null;
- var go = new GameObject("CodeBlock", typeof(RectTransform));
- var vlayout = go.GetComponent<RectTransform>();
- vlayout.SetParent(layout.Peek());
-
- vlayout.anchorMin = new Vector2(.5f, .5f);
- vlayout.anchorMax = new Vector2(.5f, .5f);
- vlayout.localScale = Vector3.one;
- vlayout.localPosition = Vector3.zero;
- vlayout.sizeDelta = new Vector2(layout.Peek().rect.width - BlockCodeInset * 2, 0f);
-
- var tt = go.AddComponent<TagTypeComponent>();
- tt.Tag = block.Tag;
-
- l = go.AddComponent<VerticalLayoutGroup>();
-
- l.childControlHeight = l.childControlWidth = true;
- l.childForceExpandHeight = false;
- l.childForceExpandWidth = true;
- l.spacing = 1.5f;
- l.padding = new RectOffset(BlockCodeInset, BlockCodeInset, BlockCodeInset, BlockCodeInset);
-
- var im = go.AddComponent<Image>();
- im.material = CustomUI.Utilities.UIUtilities.NoGlowMaterial;
- im.type = Image.Type.Sliced;
- im.sprite = Instantiate(BlockQuoteBackground);
- im.color = BlockCodeColor;
-
- var text = BeatSaberUI.CreateText(vlayout, $"<noparse>{block.StringContent}</noparse>", Vector2.zero);
- text.fontSize = BlockCodeSize;
- text.font = Consolas;
- }
- break;
-
- case BlockTag.List:
- l = BlockNode("List", .05f, true, t => t.lData = block.ListData, matchWidth: true, matchWidthDiff: ListInset * 2, spacer: 1.5f);
- if (l)
- {
- l.childForceExpandWidth = true;
- l.padding = new RectOffset(ListInset, ListInset, 0, 0);
- var go = l.gameObject;
- var tt = go.GetComponent<TagTypeComponent>();
-
- // count up children
- var count = 0;
- for (var c = block.FirstChild; c != null; c = c.NextSibling) count++;
- tt.listCount = count;
- tt.listIndex = 0;
- }
- break;
- case BlockTag.ListItem:
- l = BlockNode("ListItem", .05f, false, matchWidth: true, spacer: null);
- if (l)
- { // TODO: this is mega scuffed
- l.childForceExpandWidth = true;
- var go = l.gameObject;
- var rt = go.GetComponent<RectTransform>();
- var tt = go.GetComponent<TagTypeComponent>();
- var ptt = rt.parent.gameObject.GetComponent<TagTypeComponent>();
-
- var index = ptt.listIndex++;
-
- var listCount = ptt.listCount;
- var maxNum = listCount + ptt.lData.Start;
- var numChars = (int)Math.Floor(Math.Log10(maxNum) + 1);
-
- var cNum = index + ptt.lData.Start;
-
- var lt = ptt.lData.ListType;
-
- var id = lt == ListType.Bullet ? ptt.lData.BulletChar.ToString() : (cNum + (ptt.lData.Delimiter == ListDelimiter.Parenthesis ? ")" : "."));
- var ident = BeatSaberUI.CreateText(rt, $"<nobr>{id} </nobr>\n", Vector2.zero);
- if (lt == ListType.Ordered) // pad it out to fill space
- ident.text += $"<nobr><mspace=1em>{new string(' ', numChars + 1)}</mspace></nobr>";
-
- var contGo = new GameObject("Content", typeof(RectTransform));
- var vlayout = contGo.GetComponent<RectTransform>();
- vlayout.SetParent(rt);
-
- vlayout.anchorMin = new Vector2(.5f, .5f);
- vlayout.anchorMax = new Vector2(.5f, .5f);
- vlayout.localScale = Vector3.one;
- vlayout.localPosition = Vector3.zero;
- //vlayout.sizeDelta = new Vector2(rt.rect.width, 0f);
-
- var tt2 = contGo.AddComponent<TagTypeComponent>();
- tt2.Tag = block.Tag;
-
- var csf = contGo.AddComponent<ContentSizeFitter>();
- csf.horizontalFit = ContentSizeFitter.FitMode.PreferredSize;
- csf.verticalFit = ContentSizeFitter.FitMode.Unconstrained;
-
- l = contGo.AddComponent<VerticalLayoutGroup>();
- l.childAlignment = TextAnchor.UpperLeft;
- l.childControlHeight = l.childControlWidth = true;
- l.childForceExpandHeight = false;
- l.childForceExpandWidth = true;
- l.spacing = .5f;
-
- layout.Push(vlayout);
- }
- else
- layout.Pop(); // pop one more to clear content rect from stack
- break;
-
- case BlockTag.HtmlBlock:
- break;
-
- case BlockTag.ReferenceDefinition: // i have no idea what the state looks like here
- break;
- }
- }
- else if (node.Inline != null)
- { // inline element
- var inl = node.Inline;
-
- void Flag(CurrentTextFlags flag)
- {
- if (node.IsOpening)
- textFlags |= flag;
- else if (node.IsClosing)
- textFlags &= ~flag;
- }
-
- void EnsureText()
- {
- if (currentText == null)
- {
- Logger.md.Debug($"Adding new text element");
-
- var tt = layout.Peek().gameObject.GetComponent<TagTypeComponent>();
- currentText = BeatSaberUI.CreateText(layout.Peek(), "", Vector2.zero);
- currentText.gameObject.AddComponent<TextLinkDecoder>();
-
- currentText.enableWordWrapping = true;
-
- switch (tt.Tag)
- {
- case BlockTag.List:
- case BlockTag.ListItem:
- case BlockTag.Paragraph:
- currentText.fontSize = PSize;
- break;
- case BlockTag.AtxHeading:
- var size = H1Size;
- size -= HLevelDecrease * (tt.hData.Level - 1);
- currentText.fontSize = size;
- if (tt.hData.Level == 1)
- currentText.alignment = TextAlignmentOptions.Center;
- break;
- case BlockTag.SetextHeading:
- currentText.fontSize = H1Size;
- currentText.alignment = TextAlignmentOptions.Center;
- break;
- // TODO: add other relevant types
- }
-
- texts.Add(currentText);
- }
- }
- switch (inl.Tag)
- {
- case InlineTag.String:
- EnsureText();
-
- string head = "<noparse>", tail = "</noparse>";
- if (textFlags.HasFlag(CurrentTextFlags.Bold))
- { head = "<b>" + head; tail += "</b>"; }
- if (textFlags.HasFlag(CurrentTextFlags.Italic))
- { head = "<i>" + head; tail += "</i>"; }
- if (textFlags.HasFlag(CurrentTextFlags.Strikethrough))
- { head = "<s>" + head; tail += "</s>"; }
- if (textFlags.HasFlag(CurrentTextFlags.Underline))
- { head = "<u>" + head; tail += "</u>"; }
-
- currentText.text += head + inl.LiteralContent + tail;
- break;
- case InlineTag.Strong:
- Flag(CurrentTextFlags.Bold);
- break;
- case InlineTag.Strikethrough:
- Flag(CurrentTextFlags.Strikethrough);
- break;
- case InlineTag.Emphasis:
- Flag(CurrentTextFlags.Italic);
- break;
- case InlineTag.Code:
- EnsureText();
- currentText.text += $"<font=\"CONSOLAS\"><size=80%><mark=#A0A0C080><noparse>{inl.LiteralContent}</noparse></mark></size></font>";
- break;
- case InlineTag.SoftBreak:
- EnsureText();
- currentText.text += " "; // soft breaks translate to a space
- break;
- case InlineTag.LineBreak:
- EnsureText();
- currentText.text += "\n"; // line breaks translate to a new line
- break;
- case InlineTag.Link:
- EnsureText();
- Flag(CurrentTextFlags.Underline);
-
- var color = LinkDefaultColor;
- var targetUrl = ResolveUri(inl.TargetUrl);
-
- var m = linkRefTitleColorRegex.Match(inl.LiteralContent);
- if (m.Success)
- color = "#" + m.Groups[1].Value;
-
- if (node.IsOpening)
- currentText.text += $"<color={color}><link=\"{targetUrl}\">";
- else if (node.IsClosing)
- currentText.text += "</link></color>";
- break;
- case InlineTag.RawHtml:
- EnsureText();
- currentText.text += inl.LiteralContent;
- break;
- case InlineTag.Placeholder:
-
- break;
- }
- }
- }
-
- yield return null; // delay one frame
-
- scrView.Setup();
-
- // this is the bullshit I have to use to make it work properly
- content.gameObject.GetComponent<VerticalLayoutGroup>().enabled = false;
- var childRt = content.GetChild(0) as RectTransform;
- childRt.anchoredPosition = new Vector2(0f, childRt.anchoredPosition.y);
- }
-
- private class TextLinkDecoder : MonoBehaviour, IPointerClickHandler
- {
- private TextMeshProUGUI tmp;
-
- public void Awake()
- {
- tmp = GetComponent<TextMeshProUGUI>();
- }
-
- public void OnPointerClick(PointerEventData eventData)
- {
- // this may not actually get me what i want
- int linkIndex = TMP_TextUtilities.FindIntersectingLink(tmp, eventData.pointerPressRaycast.worldPosition, null);
- if (linkIndex != -1)
- { // was a link clicked?
- TMP_LinkInfo linkInfo = tmp.textInfo.linkInfo[linkIndex];
-
- // open the link id as a url, which is the metadata we added in the text field
- var qualifiedUrl = linkInfo.GetLinkID();
- if (qualifiedUrl.StartsWith("$$"))
- return; // this means its used for something else
-
- Logger.md.Debug($"Link pressed {qualifiedUrl}");
-
- var uri = GetLinkUri(qualifiedUrl);
- if (uri != null)
- Process.Start(uri);
- }
- }
-
- private List<Color32[]> SetLinkToColor(int linkIndex, Color32 color)
- {
- TMP_LinkInfo linkInfo = tmp.textInfo.linkInfo[linkIndex];
-
- var oldVertColors = new List<Color32[]>(); // store the old character colors
-
- for (int i = 0; i < linkInfo.linkTextLength; i++)
- { // for each character in the link string
- int characterIndex = linkInfo.linkTextfirstCharacterIndex + i; // the character index into the entire text
- var charInfo = tmp.textInfo.characterInfo[characterIndex];
- int meshIndex = charInfo.materialReferenceIndex; // Get the index of the material / sub text object used by this character.
- int vertexIndex = charInfo.vertexIndex; // Get the index of the first vertex of this character.
-
- Color32[] vertexColors = tmp.textInfo.meshInfo[meshIndex].colors32; // the colors for this character
- oldVertColors.Add(vertexColors.ToArray());
-
- if (charInfo.isVisible)
- {
- vertexColors[vertexIndex + 0] = color;
- vertexColors[vertexIndex + 1] = color;
- vertexColors[vertexIndex + 2] = color;
- vertexColors[vertexIndex + 3] = color;
- }
- }
-
- // Update Geometry
- tmp.UpdateVertexData(TMP_VertexDataUpdateFlags.All);
-
- return oldVertColors;
- }
- }
-
- private void Clear()
- {
- content.gameObject.SetActive(false);
- void Clear(Transform target)
- {
- foreach (Transform child in target)
- {
- Clear(child);
- Logger.md.Debug($"Destroying {child.name}");
- child.SetParent(null);
- Destroy(child.gameObject);
- }
- }
- Clear(content);
- content.gameObject.SetActive(true);
- }
- }
- }
|