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.
 
 
 
 

832 lines
38 KiB

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);
}
}
}