public InsertedTree(TagTree baseTree, string textContent, int baseTag) : base(baseTree.id, textContent, baseTag) { this.parentTree = baseTree; }
public static Vector2 SizeDeltaOf(TagTree tree) { return(new Vector2(tree.viewWidth, tree.viewHeight)); }
public static void ResetHideFlags(TagTree layoutedTree) { ResetRecursive(layoutedTree); }
public static string ShowContent(TagTree tree) { return("val:" + tree.tagValue + " type:" + tree.treeType.ToString()); }
public static Vector2 AnchoredPositionOf(TagTree tree) { return(new Vector2(tree.offsetX, -tree.offsetY)); }
/** * 与えられたstringから情報を抜き出し、パーツの親子構造を規定する。 * ParsedTreeを返してくる。 * * そのうち単一のArrayとしてindexのみで処理するように書き換えると、文字のコピーが減って楽。 */ private IEnumerator Parse(TagTree parentTree, string data) { // Debug.LogError("data:" + data + " parentTree:" + resLoader.GetTagFromValue(parentTree.tagValue)); var charIndex = 0; var readPoint = 0; while (true) { // consumed. if (data.Length <= charIndex) { break; } var chr = data[charIndex]; // Debug.LogError("chr:" + chr); switch (chr) { case '"': case '\'': { var nextChr = data.IndexOf(chr, charIndex + 1); if (nextChr == -1) { // close chr not found. throw new Exception("failed to find close chr:" + chr + " in data:" + data); } charIndex = nextChr; break; } case '<': { var foundTag = IsTag(data, charIndex); // Debug.LogError("foundTag:" + resLoader.GetTagFromValue(foundTag)); switch (foundTag) { // get depthAssetList from commented url. case (int)HTMLTag._COMMENT: { // <!--SOMETHING--> var endPos = -1; var contentStr = GetContentOfCommentTag(data, charIndex, out endPos); if (endPos == -1) { yield break; } var cor = ParseAsComment(parentTree, contentStr); while (cor.MoveNext()) { yield return(null); } charIndex = endPos; readPoint = charIndex; continue; } // !SOMETHING tag. case (int)HTMLTag._EXCLAMATION_TAG: { var cor = GetDocTypeDecl(data, charIndex); while (cor.MoveNext()) { if (cor.Current != -1) { break; } yield return(null); } charIndex = cor.Current; readPoint = charIndex; continue; } case (int)HTMLTag._NO_TAG_FOUND: { // no tag found. go to next char. charIndex++; continue; } // html tag will be parsed without creating html tag. case (int)HTMLTag.html: { var endTagStartPos = GetStartPointOfCloseTag(data, charIndex, foundTag); if (endTagStartPos == -1) { parseFailed((int)ParseErrors.CLOSETAG_NOT_FOUND, "the tag:" + resLoader.GetTagFromValue(foundTag) + " is not closed."); yield break; } // only content string should be parse. var contentStr = GetTagContent(data, charIndex, foundTag, endTagStartPos); var cor = Parse(parentTree, contentStr); while (cor.MoveNext()) { yield return(null); } charIndex = endTagStartPos; readPoint = charIndex; continue; } // ignore these tags. case (int)HTMLTag.head: case (int)HTMLTag.title: { charIndex = GetClosePointOfTag(data, charIndex, foundTag); if (charIndex == -1) { parseFailed((int)ParseErrors.CLOSETAG_NOT_FOUND, "the tag:" + resLoader.GetTagFromValue(foundTag) + " is not closed."); yield break; } readPoint = charIndex; continue; } default: { // pass. break; } } // Debug.LogError("foundTag:" + foundTag + " cont:" + data.Substring(charIndex)); var readingPointStartIndex = 0; var readingPointLength = 0; if (readPoint < charIndex) { // Debug.LogError("readPoint:" + readPoint + " vs charIndex:" + charIndex); var length = charIndex - readPoint; // reserve index and length. readingPointStartIndex = readPoint; readingPointLength = length; } var rawTagName = resLoader.GetRawTagFromValue(foundTag); // Debug.Log("rawTagName:" + rawTagName); // set tag. var tag = foundTag; // ここで、すでにtagは見つかっているので、ここまでのコンテンツは親タグのものとして整理できる。 { // add content before next tag start if exist. if (0 < readingPointLength) { var str = data.Substring(readingPointStartIndex, readingPointLength); // Debug.LogError("1 str:" + str + " parentTagPoint:" + parentTagPoint.tag + " current tag:" + foundTag); if (!string.IsNullOrEmpty(str)) { var contentTagPoint = new TagTree( str, parentTree.tagValue ); if (!contentTagPoint.SetParent(parentTree)) { parseFailed((int)ParseErrors.CANNOT_CONTAIN_TEXT_IN_BOX_DIRECTLY, "tag:" + tag + " could not contain text value directly. please wrap text content with some tag."); yield break; } } } } // read new tag. { // set to next char index. after '<tag' var tempCharIndex = charIndex + ("<" + rawTagName).Length; var tempReadPoint = readPoint; /* * collect attr and find start-tag end. */ { switch (data[tempCharIndex]) { case '/': { if (data[tempCharIndex + 1] != '>') { parseFailed((int)ParseErrors.ILLIGAL_CHAR, "the tag:" + resLoader.GetTagFromValue(tag) + " with '/' should be closed soon. > required."); yield break; } // > was found and <TAG[SOMETHING] is /. tag is closed directly. var treeType = resLoader.GetTreeType(tag); if (treeType == TreeType.NotFound) { parseFailed((int)ParseErrors.UNDEFINED_TAG, "the tag:" + resLoader.GetTagFromValue(tag) + " is not defined in both uuebTags and default tags."); yield break; } var tagPoint2 = new TagTree( tag, new AttributeKVs(), treeType ); tagPoint2.SetParent(parentTree); charIndex = tempCharIndex + 2 /* /> */; readPoint = charIndex; continue; } case ' ': { // <tag [attr]/> or <tag [attr]> var startTagEndIndex = data.IndexOf(">", tempCharIndex); // Debug.LogError("startTagEndIndex:" + startTagEndIndex); if (startTagEndIndex == -1) { // start tag never close. charIndex++; continue; } // Debug.LogError("' ' found at tag:" + tag + " startTagEndIndex:" + startTagEndIndex); var attrStr = data.Substring(tempCharIndex + 1, startTagEndIndex - tempCharIndex - 1); var kv = GetAttr(tag, attrStr); if (kv == null) { parseFailed((int)ParseErrors.FAILED_TO_PARSE_ATTRIBUTE, "the tag:" + resLoader.GetTagFromValue(tag) + " contains unnecessary space after tag."); yield break; } // tag closed point is tagEndIndex. next point is tagEndIndex + 1. tempCharIndex = startTagEndIndex + 1; tempReadPoint = tempCharIndex; // Debug.LogError("data[tempCharIndex]:" + data[tempCharIndex]); var treeType = resLoader.GetTreeType(tag); // Content_Imgかつデフォルトのimgタグの場合は事前に画像読み込みを行う switch (treeType) { case TreeType.Content_Img: // デフォルトのimgタグかチェックする // SRC keyを含んでいなかった場合は特に何もせずにスルー、エラー処理はLayoutMachineに任せる if (resLoader.IsDefaultTag(tag) && kv.ContainsKey(HTMLAttribute.SRC)) { // SRCを含んでいるので事前画像読み込みリクエストを行う defaultImageDownloader.RequestLoadImage(kv[HTMLAttribute.SRC] as string); } break; default: // treeTypeやtagが条件を満たしていなかった場合には何もせず次の処理へ break; } /* * single close tag found. * this tag content is just closed. */ if (data[startTagEndIndex - 1] == '/') { // <tag [attr]/> // Debug.LogError("-1 is / @tag:" + tag); if (treeType == TreeType.NotFound) { parseFailed((int)ParseErrors.UNDEFINED_TAG, "the tag:" + resLoader.GetTagFromValue(tag) + " is not defined in both uuebTags and default tags."); yield break; } var tagPoint2 = new TagTree( tag, kv, treeType ); tagPoint2.SetParent(parentTree); charIndex = tempCharIndex; readPoint = tempReadPoint; continue; } // Debug.LogError("not closed tag:" + tag + " in data:" + data); /* * finding end-tag of this tag. */ var endTag = "</" + rawTagName.ToLower() + ">"; var cascadedStartTagHead = "<" + rawTagName.ToLower(); var endTagIndex = FindEndTag(endTag, cascadedStartTagHead, data, tempCharIndex); if (endTagIndex == -1) { // retrieve single <p>. if (tag == (int)HTMLTag.p) { var singlePTree = new TagTree(tag, kv, treeType); singlePTree.SetParent(parentTree); charIndex = tempCharIndex; readPoint = charIndex; continue; } parseFailed((int)ParseErrors.CLOSETAG_NOT_FOUND, "the tag:" + resLoader.GetTagFromValue(foundTag) + " is not closed."); yield break; } // Debug.LogError("endTagIndex:" + endTagIndex); { if (treeType == TreeType.NotFound) { parseFailed((int)ParseErrors.UNDEFINED_TAG, "the tag:" + resLoader.GetTagFromValue(tag) + " is not defined in both uuebTags and default tags."); yield break; } var tagPoint = new TagTree( tag, kv, treeType ); tagPoint.SetParent(parentTree); var contents = data.Substring(tempCharIndex, endTagIndex - tempCharIndex); // Debug.LogError("contents1:" + contents); var cor = Parse(tagPoint, contents); while (cor.MoveNext()) { yield return(null); } // one tag start & end is detected. tempCharIndex = endTagIndex + endTag.Length; // Debug.LogError("tempCharIndex:" + tempCharIndex + " data:" + data[tempCharIndex]); tempReadPoint = tempCharIndex; /* * <T [ATTR]>V</T><SOMETHING... */ if (tempCharIndex < data.Length && data[tempCharIndex] == '<') { charIndex = tempCharIndex; readPoint = tempReadPoint; continue; } tempCharIndex++; charIndex = tempCharIndex; readPoint = tempReadPoint; continue; } } case '>': { // <tag> start tag is closed. // Debug.LogError("> found at tag:" + tag + " cont:" + data.Substring(tempCharIndex) + "___ finding end tag of tag:" + tag); // set to next char. tempCharIndex = tempCharIndex + 1; if (tag == (int)HTMLTag.br) { var brTree = new TagTree(tag); brTree.SetParent(parentTree); charIndex = tempCharIndex; readPoint = charIndex; continue; } /* * finding end-tag of this tag. */ var endTag = "</" + rawTagName.ToLower() + ">"; var cascadedStartTagHead = "<" + rawTagName.ToLower(); var endTagIndex = FindEndTag(endTag, cascadedStartTagHead, data, tempCharIndex); if (endTagIndex == -1) { // retrieve <p>. if (tag == (int)HTMLTag.p) { var singlePTree = new TagTree(tag); singlePTree.SetParent(parentTree); charIndex = tempCharIndex; readPoint = charIndex; continue; } parseFailed((int)ParseErrors.ILLIGAL_CHAR, "the tag:" + resLoader.GetTagFromValue(tag) + " is not closed."); yield break; } // treat tag contained contents. var contents = data.Substring(tempCharIndex, endTagIndex - tempCharIndex); var treeType = resLoader.GetTreeType(tag); if (treeType == TreeType.NotFound) { parseFailed((int)ParseErrors.UNDEFINED_TAG, "the tag:" + resLoader.GetTagFromValue(tag) + " is not defined in both uuebTags and default tags."); yield break; } var tree = new TagTree( tag, new AttributeKVs(), treeType ); tree.SetParent(parentTree); // Debug.LogError("contents2:" + contents); var cor = Parse(tree, contents); while (cor.MoveNext()) { yield return(null); } tempCharIndex = endTagIndex + endTag.Length; tempReadPoint = tempCharIndex; charIndex = tempCharIndex; readPoint = tempReadPoint; continue; } default: { parseFailed(-1, "parse error. unknown keyword found:" + data[charIndex] + " at tag:" + tag); yield break; } } } } } } charIndex++; } // all tags are found and rest contents are content of parent tag. if (readPoint < data.Length) { var restStr = data.Substring(readPoint); // Debug.LogError("restStr:" + restStr); if (!string.IsNullOrEmpty(restStr)) { var contentTree = new TagTree( restStr, parentTree.tagValue ); if (!contentTree.SetParent(parentTree)) { parseFailed((int)ParseErrors.CANNOT_CONTAIN_TEXT_IN_BOX_DIRECTLY, "tag:" + resLoader.GetTagFromValue(parentTree.tagValue) + " could not contain text value directly. please wrap text content with some tag."); yield break; } } } /* * expand customLayer to layer + box + children. */ switch (parentTree.treeType) { case TreeType.CustomLayer: { ExpandCustomTagToLayer(parentTree); break; } } }
public void RemoveChild(TagTree child) { this._children.Remove(child); }
// スクロールイベントから生成を行う ↓ // オブジェクトプールの再考(画面外に行ったオブジェクトのプール復帰、新しい要素のプールからの取得) // Layer系のオブジェクト、高さが0のツリー、hiddenを無視する(この辺は上記のチョイス時に削れると良さそう。) public IEnumerator Materialize(GameObject root, UUebViewCore core, TagTree tree, Vector2 viewRect, float yOffset, Action onLoaded) { var viewHeight = viewRect.y; { var rootRectTrans = root.GetComponent <RectTransform>(); this.core = core; // set anchor to left top. rootRectTrans.anchorMin = Vector2.up; rootRectTrans.anchorMax = Vector2.up; rootRectTrans.pivot = Vector2.up; rootRectTrans.sizeDelta = new Vector2(tree.viewWidth, tree.viewHeight); } // 描画範囲にあるツリーのidを集める。ここから一瞬でも外れたらskip。 var drawTreeIds = TraverseTree(tree, yOffset, viewHeight); // materialize root's children in parallel. var children = tree.GetChildren(); var cors = new List <IEnumerator>(); for (var i = 0; i < children.Count; i++) { var child = children[i]; var cor = MaterializeRecursive(child, root, drawTreeIds); cors.Add(cor); } // firstviewのmaterializeまでを並列で実行する while (true) { for (var i = 0; i < cors.Count; i++) { var cor = cors[i]; if (cor == null) { continue; } var cont = cor.MoveNext(); if (!cont) { cors[i] = null; } } var running = cors.Where(c => c != null).Any(); // wait all coroutine's end. if (!running) { break; } yield return(null); } onLoaded(); }
private void ExpandCustomTagToLayer(TagTree layerBaseTree) { var adoptedConstaints = resLoader.GetConstraints(layerBaseTree.tagValue); var children = layerBaseTree.GetChildren(); /* * これで、 * layer/child * -> * layer/box/child x N * になる。boxの数だけ増える。 */ foreach (var box in adoptedConstaints) { var boxName = box.boxName; var boxingChildren = children.Where(c => resLoader.GetLayerBoxName(layerBaseTree.tagValue, c.tagValue) == boxName).ToArray(); foreach (var boxingChild in boxingChildren) { var boxingChildChildren = boxingChild.GetChildren(); foreach (var boxingChildChild in boxingChildChildren) { if (!resLoader.IsDefaultTag(boxingChildChild.tagValue)) { switch (boxingChildChild.treeType) { case TreeType.Content_CRLF: case TreeType.Content_Text: { parseFailed((int)ParseErrors.CANNOT_CONTAIN_TEXT_IN_BOX_DIRECTLY, "tag:" + resLoader.GetTagFromValue(boxingChildChild.tagValue) + " could not contain text value directly. please wrap text content with some tag."); return; } } } } } if (boxingChildren.Any()) { var boxTag = resLoader.FindOrCreateTag(boxName); // 新規に中間box treeを作成する。 var newBoxTreeAttr = new AttributeKVs() { { HTMLAttribute._BOX, box.rect }, { HTMLAttribute._COLLISION, box.collisionGroupId } }; var boxTree = new TagTree(boxTag, newBoxTreeAttr, TreeType.CustomBox); // すでに入っているchildrenを取り除いて、boxを投入 layerBaseTree.ReplaceChildrenToBox(boxingChildren, boxTree); // boxTreeにchildを追加 boxTree.AddChildren(boxingChildren); // boxingChildがlayerな場合、parentがboxであるというマークをつける。 foreach (var child in boxingChildren) { switch (child.treeType) { case TreeType.CustomLayer: { child.keyValueStore[HTMLAttribute._LAYER_PARENT_TYPE] = "box"; break; } case TreeType.CustomBox: { child.keyValueStore[HTMLAttribute._LAYER_PARENT_TYPE] = "box"; break; } } } } } var errorTrees = layerBaseTree.GetChildren().Where(c => c.treeType != TreeType.CustomBox); if (errorTrees.Any()) { parseFailed((int)ParseErrors.NOT_RESERVED_TAG_IN_LAYER, "unexpected tag:" + string.Join(", ", errorTrees.Select(t => resLoader.GetTagFromValue(t.tagValue)).ToArray()) + " found at customLayer:" + resLoader.GetTagFromValue(layerBaseTree.tagValue) + ". please exclude not defined tags in this layer, or define it on this layer."); } }
public TagTree[] GetTreeById(string id) { return(TagTree.GetTreeById(layoutedTree, id)); }
/** * 現状は全ての子について1f内で各1度は実行する、という処理になっている。 * n-m-o * \p-q * \r * * みたいな数のツリーがある場合、 * nのツリーを処理する段階で、毎フレームm,qが1度ずつ展開される。 * mの展開時にはoが毎フレーム1度ずつ展開される。 * pの展開時には、qとrが毎フレーム1度ずつ展開される。 * * なので、全てのツリーが1fに1度は初期化されるようになる。 */ private IEnumerator MaterializeRecursive(TagTree tree, GameObject parent, List <string> drawTargetTreeIds) { // Debug.LogError("materialize:" + tree.treeType + " tag:" + resLoader.GetTagFromValue(tree.tagValue)); if (tree.keyValueStore.ContainsKey(HTMLAttribute.LISTEN) && tree.keyValueStore.ContainsKey(HTMLAttribute.HIDDEN)) { core.AddListener(tree, tree.keyValueStore[HTMLAttribute.LISTEN] as string); } if (tree.hidden || tree.treeType == TreeType.Content_CRLF) { // cancel materialize of this tree. yield break; } // if (!drawTargetTreeIds.Contains(tree.id)) // { // yield break; // } var objCor = resLoader.LoadGameObjectFromPrefab(tree.id, tree.tagValue, tree.treeType); while (objCor.MoveNext()) { if (objCor.Current != null) { break; } yield return(null); } // set pos and size. var newGameObject = objCor.Current; var cached = false; if (newGameObject.transform.parent != null) { cached = true; } newGameObject.transform.SetParent(parent.transform, false); var rectTrans = newGameObject.GetComponent <RectTransform>(); rectTrans.anchoredPosition = TagTree.AnchoredPositionOf(tree); rectTrans.sizeDelta = TagTree.SizeDeltaOf(tree); // set parameters and events by container type. button, link. var src = string.Empty; if (tree.keyValueStore.ContainsKey(HTMLAttribute.SRC)) { src = tree.keyValueStore[HTMLAttribute.SRC] as string; } switch (tree.treeType) { case TreeType.Content_Img: case TreeType.CustomLayer: { if (tree.viewHeight == 0) { break; } // 画像コンテンツはキャッシュ済みの場合再度画像取得を行わない。 if (!cached) { // 画像指定がある場合のみ読み込む if (!string.IsNullOrEmpty(src)) { var imageLoadCor = resLoader.LoadImageAsync(src); // combine coroutine. var setImageCor = SetImageCor(newGameObject, imageLoadCor); resLoader.LoadParallel(setImageCor); } } break; } case TreeType.Content_Text: { // テキストコンテンツは毎回内容が変わる可能性があるため、キャッシュに関わらず更新する。 if (tree.keyValueStore.ContainsKey(HTMLAttribute._CONTENT)) { var text = tree.keyValueStore[HTMLAttribute._CONTENT] as string; if (!string.IsNullOrEmpty(text)) { pluggable.SetText(newGameObject, text, tree.keyValueStore.ContainsKey(HTMLAttribute._IS_SINGLE_LINE)); } } // 文字コンテンツのリンク化(hrefがついてるとリンクになる。実態はボタン。) if (tree.keyValueStore.ContainsKey(HTMLAttribute.HREF)) { var href = tree.keyValueStore[HTMLAttribute.HREF] as string; var linkId = string.Empty; if (tree.keyValueStore.ContainsKey(HTMLAttribute.ID)) { linkId = tree.keyValueStore[HTMLAttribute.ID] as string; } eventObjectCache[linkId] = new KeyValuePair <GameObject, string>(newGameObject, href); // add button component. AddButton(newGameObject, () => core.OnLinkTapped(newGameObject, href, linkId)); } break; } default: { // do nothing. break; } } // button attrに応じたボタン化 if (tree.keyValueStore.ContainsKey(HTMLAttribute.BUTTON)) { var isButton = tree.keyValueStore[HTMLAttribute.BUTTON] as string == "true"; if (isButton) { var buttonId = string.Empty; if (tree.keyValueStore.ContainsKey(HTMLAttribute.ID)) { buttonId = tree.keyValueStore[HTMLAttribute.ID] as string; } eventObjectCache[buttonId] = new KeyValuePair <GameObject, string>(newGameObject, src); // add button component. AddButton(newGameObject, () => core.OnImageTapped(newGameObject, src, buttonId)); } } var children = tree.GetChildren(); var enums = new List <IEnumerator>(); for (var i = 0; i < children.Count; i++) { var child = children[i]; var cor = MaterializeRecursive(child, newGameObject, drawTargetTreeIds); enums.Add(cor); } while (0 < enums.Count) { for (var i = 0; i < enums.Count; i++) { var continuation = enums[i].MoveNext(); if (!continuation || enums[i].Current != null) { // 終わったので除外する enums.RemoveAt(i); } } yield return(null); } }
/** * コンテンツのパラメータを初期値に戻す */ public void Reset() { TagTree.ResetHideFlags(layoutedTree); }
/** TextMesh Proのレイアウトを決定して返す レイアウト、改行などの必要に応じて文字列を分割する。 */ private IEnumerator<ChildPos> DoTextMeshProComponentLayout(TagTree textTree, TMPro.TextMeshProUGUI textComponent, string text, ViewCursor textViewCursor, Func<InsertType, TagTree, ViewCursor> insertion = null) { // Debug.Log("DoTextMeshProComponentLayout text:" + text.Length + " textViewCursor:" + textViewCursor); textComponent.text = text; // textComponentに対してwidthをセットする必要がある。 textComponent.rectTransform.sizeDelta = new Vector2(textViewCursor.viewWidth, float.PositiveInfinity); // このメソッドは、コンポーネントがgoにアタッチされてcanvasに乗っている場合のみ動作する。 var textInfos = textComponent.GetTextInfo(text); // 各行の要素とパラメータを取得する。 var tmGeneratorLines = textInfos.lineInfo; var lineSpacing = textComponent.lineSpacing; var tmLineCount = textInfos.lineCount; var onLayoutPresetX = (float)textTree.keyValueStore[HTMLAttribute._ONLAYOUT_PRESET_X]; // Debug.Log("text:" + text + " textViewCursor.viewWidth:" + textViewCursor.viewWidth); // 1行以上のラインが画面内にある。 var isStartAtZeroOffset = onLayoutPresetX == 0 && textViewCursor.offsetX == 0; var isMultilined = 1 < tmLineCount; // このコンテナの1行目を別のコンテナの結果位置 = 行中から書いた結果、この1行の幅が画面幅を超えている場合、全体を次の行に送る。 // あ、この判定では無理だな、、分割されたコンテナの可能性が出てくる? 整列を下からではなく上からやる必要がある。 if (!isStartAtZeroOffset && textViewCursor.viewWidth < tmGeneratorLines[0].length) { // 行なかで、1行目のコンテンツがまるきり入らなかった。 // よって、改行を行なって次の行からコンテンツを開始する。 // textTree.keyValueStore[HTMLAttribute._ONLAYOUT_PRESET_X] = 0.0f; insertion(InsertType.RetryWithNextLine, null); // テキストとサイズを空に戻す textComponent.text = string.Empty; textComponent.rectTransform.sizeDelta = Vector2.zero; yield break; } // 複数行存在するんだけど、2行目のスタートが0文字目の場合、1行目に1文字も入っていない。 if (isMultilined && tmGeneratorLines[1].firstCharacterIndex == 0) { // 行頭でこれが起きる場合、コンテンツ幅が圧倒的に不足していて、一文字も入らないということが起きている。 // 1文字ずつ切り分けて表示する。 if (isStartAtZeroOffset) { // 最初の1文字目を強制的にセットする var bodyContent = text.Substring(0, 1); // 内容の反映 textTree.keyValueStore[HTMLAttribute._CONTENT] = bodyContent; // 最終行 var lastLineContent = text.Substring(1); // 最終行を分割して送り出す。追加されたコンテンツを改行後に処理する。 var nextLineContent = new InsertedTree(textTree, lastLineContent, textTree.tagValue); insertion(InsertType.InsertContentToNextLine, nextLineContent); var charHeight = (tmGeneratorLines[0].lineHeight + lineSpacing); // テキストとサイズを空に戻す textComponent.text = string.Empty; textComponent.rectTransform.sizeDelta = Vector2.zero; yield return textTree.SetPos(textViewCursor.offsetX, textViewCursor.offsetY, textViewCursor.viewWidth, charHeight); yield break; } // 行中からのコンテンツ追加で、複数行があるので、コンテンツ全体を次の行で開始させる。 insertion(InsertType.RetryWithNextLine, null); // テキストとサイズを空に戻す textComponent.text = string.Empty; textComponent.rectTransform.sizeDelta = Vector2.zero; yield break; } if (isStartAtZeroOffset) { if (isMultilined) { // Debug.LogError("行頭での折り返しのある複数行 text:" + text + " textViewCursor.offsetX:" + textViewCursor.offsetX + " tmLineCount:" + tmLineCount); /* TMProのtextInfo上のレイアウト指示と、実際にレイアウトした時に自動的に分割されるワードに差がある。 abc が a\nbcになることもあれば、レイアウト時には分割されずabcで入ってしまうこともある。 これは予知できないので、textInfoでの分割を正にする方向で対処する。 具体的に言うと、文章に人力で\nを入れる。 */ var bodyContent = string.Empty; var lastLineContent = string.Empty; for (var i = 0; i < tmLineCount; i++) { var lineInfo = tmGeneratorLines[i]; var lineText = text.Substring(lineInfo.firstCharacterIndex, lineInfo.lastCharacterIndex - lineInfo.firstCharacterIndex + 1); if (i == tmLineCount - 1) { lastLineContent = lineText; continue; } bodyContent += lineText; } // 内容の反映 textTree.keyValueStore[HTMLAttribute._CONTENT] = bodyContent; // 最終行を分割して送り出す。追加されたコンテンツを改行後に処理する。 var nextLineContent = new InsertedTree(textTree, lastLineContent, textTree.tagValue); insertion(InsertType.InsertContentToNextLine, nextLineContent); // 最終行以外はハコ型に収まった状態なので、ハコとして出力する。 // 最終一つ前までの高さを出して、このコンテンツの高さとして扱う。 var totalHeight = 0f; for (var i = 0; i < tmLineCount - 1; i++) { var line = tmGeneratorLines[i]; totalHeight += (line.lineHeight + lineSpacing); } // テキストとサイズを空に戻す textComponent.text = string.Empty; textComponent.rectTransform.sizeDelta = Vector2.zero; // このビューのポジションをセット yield return textTree.SetPos(textViewCursor.offsetX, textViewCursor.offsetY, textViewCursor.viewWidth, totalHeight); } else { // Debug.LogError("行頭の単一行 text:" + text); var currentLineWidth = textComponent.preferredWidth; var currentLineHeight = (tmGeneratorLines[0].lineHeight + lineSpacing); // 最終行かどうかの判断はここではできないので、単一行の入力が終わったことを親コンテナへと通知する。 insertion(InsertType.TailInsertedToLine, textTree); textTree.keyValueStore[HTMLAttribute._IS_SINGLE_LINE] = true; var childPos = textTree.SetPos(textViewCursor.offsetX, textViewCursor.offsetY, currentLineWidth, currentLineHeight); // テキストとサイズを空に戻す textComponent.text = string.Empty; textComponent.rectTransform.sizeDelta = Vector2.zero; yield return childPos; } } else { if (isMultilined) { // Debug.LogError("行中追加での折り返しのある複数行 text:" + text); var currentLineHeight = (tmGeneratorLines[0].lineHeight + lineSpacing); // 複数行が途中から出ている状態で、まず折り返しているところまでを分離して、後続の文章を新規にstringとしてinsertする。 var currentLineContent = text.Substring(0, tmGeneratorLines[1].firstCharacterIndex); textTree.keyValueStore[HTMLAttribute._CONTENT] = currentLineContent; textTree.keyValueStore[HTMLAttribute._IS_SINGLE_LINE] = true; // get preferredWidht of text from trimmed line. textComponent.text = currentLineContent; var currentLineWidth = textComponent.preferredWidth; var restContent = text.Substring(tmGeneratorLines[1].firstCharacterIndex); var nextLineContent = new InsertedTree(textTree, restContent, textTree.tagValue); // 次のコンテンツを新しい行から開始する。 insertion(InsertType.InsertContentToNextLine, nextLineContent); // テキストとサイズを空に戻す textComponent.text = string.Empty; textComponent.rectTransform.sizeDelta = Vector2.zero; yield return textTree.SetPos(textViewCursor.offsetX, textViewCursor.offsetY, currentLineWidth, currentLineHeight); } else { // Debug.LogError("行中追加の単一行 text:" + text); var width = textComponent.preferredWidth; var height = (tmGeneratorLines[0].lineHeight + lineSpacing); // Debug.LogError("行中の単一行 text:" + text + " textViewCursor:" + textViewCursor); // 最終行かどうかの判断はここでできないので、単一行の入力が終わったことを親コンテナへと通知する。 insertion(InsertType.TailInsertedToLine, textTree); textTree.keyValueStore[HTMLAttribute._IS_SINGLE_LINE] = true; // テキストとサイズを空に戻す textComponent.text = string.Empty; textComponent.rectTransform.sizeDelta = Vector2.zero; yield return textTree.SetPos(textViewCursor.offsetX, textViewCursor.offsetY, width, height); } } }
IEnumerator<ChildPos> IPluggable.TextLayoutCoroutine(Component sourceComponent, TagTree textTree, string text, ViewCursor textViewCursor, Func<InsertType, TagTree, ViewCursor> insertion = null) { if (sourceComponent is Text) { return defaultBehaviour.DoTextComponentLayout(textTree, (Text)sourceComponent, text, textViewCursor, insertion); } if (sourceComponent is TMPro.TextMeshProUGUI) { return DoTextMeshProComponentLayout(textTree, (TMPro.TextMeshProUGUI)sourceComponent, text, textViewCursor, insertion); } throw new Exception("component not supported:" + sourceComponent); }
/** * uGUIのText componentのレイアウトを決定して返す。 * レイアウト、改行などの必要に応じて文字列を分割する。 */ public IEnumerator <ChildPos> DoTextComponentLayout(TagTree textTree, Text textComponent, string text, ViewCursor textViewCursor, Func <InsertType, TagTree, ViewCursor> insertion = null) { // Debug.Log("DoTextComponentLayout text:" + text.Length + " textViewCursor:" + textViewCursor); // invalidate first. generator.Invalidate(); // set content to prefab. var defaultText = textComponent.text; textComponent.text = text; var setting = textComponent.GetGenerationSettings(new Vector2(textViewCursor.viewWidth, float.PositiveInfinity)); generator.Populate(text, setting); // この時点で、複数行に分かれるんだけど、最後の行のみ分離する必要がある。 var lineCount = generator.lineCount; // Debug.LogError("lineCount:" + lineCount); // Debug.LogError("default preferred width:" + textComponent.preferredWidth); // 0行だったら、入らなかったということなので、改行をしてもらってリトライを行う。 if (lineCount == 0 && !string.IsNullOrEmpty(textComponent.text)) { // Debug.LogError("このケース存在しないかも。"); insertion(InsertType.RetryWithNextLine, null); yield break; } // 1行以上のラインがある。 /* * ここで、このtreeに対するカーソルのoffsetXが0ではない場合、行の中間から行を書き出していることになる。 * * また上記に加え、親コンテナ自体のoffsetXが0ではない場合も、やはり、行の中間から行を書き出していることになる。 * 判定のために、親コンテナからtextTreeへ、親コンテナのoffsetX = 書き始め位置の書き込みをする。 * * 行が2行以上ある場合、1行目は右端まで到達しているのが確定する。 * 2行目以降はoffsetX=0の状態で書かれる必要がある。 * * コンテンツを分離し、それを叶える。 */ var onLayoutPresetX = (float)textTree.keyValueStore[HTMLAttribute._ONLAYOUT_PRESET_X]; var isStartAtZeroOffset = onLayoutPresetX == 0 && textViewCursor.offsetX == 0; var isMultilined = 1 < lineCount; // 複数行存在するんだけど、2行目のスタートが0文字目の場合、1行目に1文字も入っていない。 if (isMultilined && generator.lines[1].startCharIdx == 0) { /* * 行頭でこれが起きる場合、コンテンツ幅が圧倒的に不足していて、一文字も入らないということが起きている。 * が、ここで文字を一切消費しないとなると後続の処理でも無限に処理が終わらない可能性があるため、最低でも1文字を消費する。 */ if (isStartAtZeroOffset) { // 最初の1文字目を強制的にセットする var bodyContent = text.Substring(0, 1); // 1文字目だけをこの文字列の内容としてセットし直す。 textTree.keyValueStore[HTMLAttribute._CONTENT] = bodyContent; // 最終行として1文字目以降を取得、 var lastLineContent = text.Substring(1); // 最終行を分割して送り出す。追加されたコンテンツを改行後に処理する。 var nextLineContent = new InsertedTree(textTree, lastLineContent, textTree.tagValue); insertion(InsertType.InsertContentToNextLine, nextLineContent); var charHeight = GetCharHeight(bodyContent, textComponent); textComponent.text = defaultText; yield return(textTree.SetPos(textViewCursor.offsetX, textViewCursor.offsetY, textViewCursor.viewWidth, charHeight)); yield break; } // 行なかで、1行目のコンテンツがまるきり入らなかった。 // よって、改行を行なって次の行からコンテンツを開始する。 textComponent.text = defaultText; insertion(InsertType.RetryWithNextLine, null); yield break; } if (isStartAtZeroOffset) { if (isMultilined) { // Debug.LogError("行頭での折り返しのある複数行 text:" + text); // 複数行が頭から出ている状態で、改行を含んでいる。最終行が中途半端なところにあるのが確定しているので、切り離して別コンテンツとして処理する必要がある。 var bodyContent = text.Substring(0, generator.lines[generator.lineCount - 1].startCharIdx); // 内容の反映 textTree.keyValueStore[HTMLAttribute._CONTENT] = bodyContent; // 最終行 var lastLineContent = text.Substring(generator.lines[generator.lineCount - 1].startCharIdx); // 最終行を分割して送り出す。追加されたコンテンツを改行後に処理する。 var nextLineContent = new InsertedTree(textTree, lastLineContent, textTree.tagValue); insertion(InsertType.InsertContentToNextLine, nextLineContent); // 最終行以外はハコ型に収まった状態なので、ハコとして出力する。 // 最終一つ前までの高さを出して、このコンテンツの高さとして扱う。 var totalHeight = 0f; for (var i = 0; i < generator.lineCount - 1; i++) { var line = generator.lines[i]; var lineHeight = (line.height * textComponent.lineSpacing); totalHeight += lineHeight; } // Debug.Log("ここの値が計算上合わないみたいなやつがあって、もうcomponentにやらせてみよう。 totalHeight:" + totalHeight); // 実際に最終行までの文字列を取得して、textComponentにセットし、preferredHeightを得る。 var lastLineFirstStrIndex = generator.lines[generator.lineCount - 1].startCharIdx; var subText = text.Substring(0, lastLineFirstStrIndex); textComponent.text = subText; var rectTrans = textComponent.GetComponent <RectTransform>(); var defaultSize = rectTrans.sizeDelta; rectTrans.sizeDelta = new Vector2(textViewCursor.viewWidth, float.PositiveInfinity); // Debug.Log("vs textComponent:" + textComponent.preferredHeight); // この値は、linespacingによる下余白を剥奪する。文字はしっかりと出るが余白がない状態になってしまい、 // linespacingを編集するのが前提のフォントだと、そのlinespacingの余白を失った状態の高さを出してしまう。 // ここでトリッキーな手として、レイアウトで計算した高さ or preferredHeight のどちらか高い方を使う。 // linespacingをいじる場合、大体のケースが、totalHeightのほうが高いという結果に落ちつくと思うが、 // linespacingをいじらない場合、preferredHeightのほうが高い = totalHeightのままだと見切れる、という状態が発生する。 // そういうケースのどちらにもに対して、でかい方を採用することで「低すぎる」という問題を回避する。 if (totalHeight < textComponent.preferredHeight) { totalHeight = textComponent.preferredHeight; } rectTrans.sizeDelta = defaultSize; textComponent.text = defaultText; // このビューのポジションをセット yield return(textTree.SetPos(textViewCursor.offsetX, textViewCursor.offsetY, textViewCursor.viewWidth, totalHeight)); } else { // Debug.LogError("行頭の単一行 text:" + text + " textViewCursor.offsetY:" + textViewCursor.offsetY); var width = textComponent.preferredWidth; var height = (generator.lines[0].height * textComponent.lineSpacing); // 最終行かどうかの判断はここでできないので、単一行の入力が終わったことを親コンテナへと通知する。 insertion(InsertType.TailInsertedToLine, textTree); textTree.keyValueStore[HTMLAttribute._IS_SINGLE_LINE] = true; textComponent.text = defaultText; yield return(textTree.SetPos(textViewCursor.offsetX, textViewCursor.offsetY, width, height)); } } else { if (isMultilined) { // Debug.LogError("行中追加での折り返しのある複数行 text:" + text); var currentLineHeight = (generator.lines[0].height * textComponent.lineSpacing); // 複数行が途中から出ている状態で、まず折り返しているところまでを分離して、後続の文章を新規にstringとしてinsertする。 var currentLineContent = text.Substring(0, generator.lines[1].startCharIdx); textTree.keyValueStore[HTMLAttribute._CONTENT] = currentLineContent; textTree.keyValueStore[HTMLAttribute._IS_SINGLE_LINE] = true; // get preferredWidht of text from trimmed line. textComponent.text = currentLineContent; var currentLineWidth = textComponent.preferredWidth; var restContent = text.Substring(generator.lines[1].startCharIdx); var nextLineContent = new InsertedTree(textTree, restContent, textTree.tagValue); // 次のコンテンツを新しい行から開始する。 insertion(InsertType.InsertContentToNextLine, nextLineContent); textComponent.text = defaultText; yield return(textTree.SetPos(textViewCursor.offsetX, textViewCursor.offsetY, currentLineWidth, currentLineHeight)); } else { // Debug.LogError("行中追加の単一行 text:" + text); var width = textComponent.preferredWidth; var height = (generator.lines[0].height * textComponent.lineSpacing); // Debug.LogError("行中の単一行 text:" + text + " textViewCursor:" + textViewCursor); // 最終行かどうかの判断はここでできないので、単一行の入力が終わったことを親コンテナへと通知する。 insertion(InsertType.TailInsertedToLine, textTree); textTree.keyValueStore[HTMLAttribute._IS_SINGLE_LINE] = true; textComponent.text = defaultText; // Debug.LogError("newViewCursor:" + newViewCursor); yield return(textTree.SetPos(textViewCursor.offsetX, textViewCursor.offsetY, width, height)); } } }
IEnumerator <ChildPos> IPluggable.TextLayoutCoroutine(Component sourceComponent, TagTree textTree, string text, ViewCursor textViewCursor, Func <InsertType, TagTree, ViewCursor> insertion) { return(DoTextComponentLayout(textTree, (Text)sourceComponent, text, textViewCursor, insertion)); }