|
|
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using System.Threading.Tasks;
- using UnityEngine;
- using CommonMark;
- using CommonMark.Syntax;
- using UnityEngine.UI;
- using TMPro;
- using CustomUI.BeatSaber;
-
- namespace BSIPA_ModList.UI.ViewControllers
- {
- [RequireComponent(typeof(RectTransform), typeof(ScrollRect))]
- public class MarkdownView : MonoBehaviour
- {
- private class TagTypeComponent : MonoBehaviour
- {
- internal BlockTag Tag;
- internal HeadingData hData;
- }
-
- private string markdown = "";
- private bool mdDirty = false;
- public string Markdown
- {
- get => markdown;
- set
- {
- markdown = value;
- mdDirty = true;
- }
- }
-
- public RectTransform rectTransform => GetComponent<RectTransform>();
-
- private ScrollRect view;
- private RectTransform content;
- private RectTransform viewport;
- private Scrollbar scrollbar;
-
- private CommonMarkSettings settings;
- public MarkdownView()
- {
- settings = CommonMarkSettings.Default.Clone();
- settings.AdditionalFeatures = CommonMarkAdditionalFeatures.All;
- settings.RenderSoftLineBreaksAsLineBreaks = false;
- settings.UriResolver = ResolveUri;
- }
-
- 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;
- }
-
- protected void Awake()
- {
- view = GetComponent<ScrollRect>();
- view.verticalScrollbarVisibility = ScrollRect.ScrollbarVisibility.AutoHide;
- view.vertical = true;
- view.horizontal = false;
- view.scrollSensitivity = 0f;
- view.inertia = true;
- view.movementType = ScrollRect.MovementType.Clamped;
-
- scrollbar = new GameObject("Scrollbar", typeof(RectTransform)).AddComponent<Scrollbar>();
- scrollbar.transform.SetParent(transform);
- scrollbar.direction = Scrollbar.Direction.TopToBottom;
- scrollbar.interactable = true;
- view.verticalScrollbar = scrollbar;
-
- 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;
-
- view.viewport = viewport;
-
- content = new GameObject("Content Wrapper").AddComponent<RectTransform>();
- content.SetParent(viewport);
- var contentLayout = content.gameObject.AddComponent<LayoutElement>();
- var contentFitter = content.gameObject.AddComponent<ContentSizeFitter>();
- contentFitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize;
- contentFitter.verticalFit = ContentSizeFitter.FitMode.Unconstrained;
- contentLayout.preferredWidth = contentLayout.minWidth = rectTransform.sizeDelta.x; // to be adjusted
- content.gameObject.AddComponent<TagTypeComponent>();
- content.localPosition = Vector2.zero;
- content.anchorMin = new Vector2(.5f, .5f);
- content.anchorMax = new Vector2(.5f, .5f);
- content.anchoredPosition = Vector2.zero;
- //content.sizeDelta = Vector2.zero;
-
- view.content = content;
- }
-
- private static Sprite whitePixel;
- private static Sprite WhitePixel
- {
- get
- {
- if (whitePixel == null)
- whitePixel = Resources.FindObjectsOfTypeAll<Sprite>().First(s => s.name == "WhitePixel");
- return whitePixel;
- }
- }
-
- #if DEBUG
- #if UI_CONFIGURE_MARKDOWN_THEMATIC_BREAK
- private byte tbreakSettings = 0;
- #endif
- #endif
- public 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)
- UpdateMd();
- else if (resetContentPosition)
- {
- resetContentPosition = false;
- var v = content.anchoredPosition;
- v.y = -(content.rect.height / 2);
- content.anchoredPosition = v;
- }
-
- }
-
- private bool resetContentPosition = false;
- private void UpdateMd()
- {
- mdDirty = false;
- Clear();
-
- var doc = CommonMarkConverter.Parse(markdown, settings);
-
- Stack<RectTransform> layout = new Stack<RectTransform>();
- layout.Push(content);
- TextMeshProUGUI currentText = null;
- foreach (var node in doc.AsEnumerable())
- {
- Logger.md.Debug($"node {node}");
-
- if (node.Block != null)
- {
- var block = node.Block;
-
- 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)
- {
- 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 = Vector2.zero;
- vlayout.anchorMin = Vector2.zero;
- vlayout.anchorMax = Vector2.one;
- }
- 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 = l.childForceExpandWidth = false;
- l.childForceExpandWidth = isDoc;
- l.spacing = spacing;
- /*var cfit = go.AddComponent<ContentSizeFitter>();
- cfit.horizontalFit = ContentSizeFitter.FitMode.PreferredSize;
- cfit.verticalFit = ContentSizeFitter.FitMode.Unconstrained;*/
-
- return l;
- }
- else if (node.IsClosing)
- {
- currentText = null;
- layout.Pop();
-
- if (spacer.HasValue)
- Spacer(spacer.Value);
- }
- return null;
- }
-
- switch (block.Tag)
- {
- case BlockTag.Document:
- BlockNode("DocumentRoot", .5f, true, isDoc: true);
- break;
- case BlockTag.SetextHeading:
- var l = BlockNode("SeHeading", .1f, false, t => t.hData = block.Heading);
- if (l) l.childAlignment = TextAnchor.UpperCenter; // TODO: fix centering
- break;
- case BlockTag.AtxHeading:
- l = BlockNode("AtxHeading", .1f, false, t => t.hData = block.Heading);
- if (l && block.Heading.Level == 1)
- l.childAlignment = TextAnchor.UpperCenter;
- break;
- case BlockTag.Paragraph:
- l = BlockNode("Paragraph", .1f, false, spacer: 1.5f);
- break;
- case BlockTag.ThematicBreak:
- { // TODO: fix this, it doesn't want to actually show up properly
- const float BreakHeight = .5f;
-
- var go = new GameObject("ThematicBreak", typeof(RectTransform), typeof(HorizontalLayoutGroup));
- var vlayout = go.GetComponent<RectTransform>();
- vlayout.SetParent(layout.Peek());
- 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.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, 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 - BreakHeight;
- 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, BreakHeight);
-
- Spacer(1f);
- }
- break;
- // TODO: add the rest of the tag types
- }
- }
- else if (node.Inline != null)
- { // inline element
- var inl = node.Inline;
-
- const float PSize = 3.5f;
- const float H1Size = 4.8f;
- const float HLevelDecrease = 0.5f;
- switch (inl.Tag)
- {
- case InlineTag.String:
- if (currentText == null)
- {
- Logger.md.Debug($"Adding new text element");
-
- var tt = layout.Peek().gameObject.GetComponent<TagTypeComponent>();
- currentText = BeatSaberUI.CreateText(layout.Peek(), "", Vector2.zero);
- //var le = currentText.gameObject.AddComponent<LayoutElement>();
-
- switch (tt.Tag)
- {
- case BlockTag.List:
- case BlockTag.ListItem:
- case BlockTag.Paragraph:
- currentText.fontSize = PSize;
- currentText.enableWordWrapping = true;
- break;
- case BlockTag.AtxHeading:
- var size = H1Size;
- size -= HLevelDecrease * (tt.hData.Level - 1);
- currentText.fontSize = size;
- currentText.enableWordWrapping = true;
- break;
- case BlockTag.SetextHeading:
- currentText.fontSize = H1Size;
- currentText.enableWordWrapping = true;
- break;
- // TODO: add other relevant types
- }
- }
- Logger.md.Debug($"Appending '{inl.LiteralContent}' to current element");
- currentText.text += inl.LiteralContent;
- break;
- }
- }
- }
-
- resetContentPosition = true;
- }
-
- 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);
- }
- }
- }
|