/// <summary>Computes the length of the next line, and whether the line is valid for justification.</summary> /// <param name="node"></param> /// <param name="maxLength"></param> /// <param name="justifable"></param> /// <returns></returns> internal float TextNodeLineLength(TextNode node, float maxLength) { if (node == null) return 0; bool atLeastOneNodeCosumedOnLine = false; float length = 0; for (; node != null; node = node.Next) { if (node.Type == TextNodeType.LineBreak) break; if (SkipTrailingSpace(node, length, maxLength) && atLeastOneNodeCosumedOnLine) break; if (length + node.Length <= maxLength || !atLeastOneNodeCosumedOnLine) { atLeastOneNodeCosumedOnLine = true; length += node.Length; } else { break; } } return length; }
/// <summary>Computes the length of the next line, and whether the line is valid for justification.</summary> internal void JustifyLine(TextNode node, float targetLength) { bool justifiable = false; if (node == null) return; var headNode = node; //keep track of the head node //start by finding the length of the block of text that we know will actually fit: int charGaps = 0; int spaceGaps = 0; bool atLeastOneNodeCosumedOnLine = false; float length = 0; var expandEndNode = node; //the node at the end of the smaller list (before adding additional word) for (; node != null; node = node.Next) { if (node.Type == TextNodeType.LineBreak) break; if (SkipTrailingSpace(node, length, targetLength) && atLeastOneNodeCosumedOnLine) { justifiable = true; break; } if (length + node.Length < targetLength || !atLeastOneNodeCosumedOnLine) { expandEndNode = node; if (node.Type == TextNodeType.Space) spaceGaps++; if (node.Type == TextNodeType.Word) { charGaps += (node.Text.Length - 1); //word was part of a crumbled word, so there's an extra char cap between the two words if (CrumbledWord(node)) charGaps++; } atLeastOneNodeCosumedOnLine = true; length += node.Length; } else { justifiable = true; break; } } //now we check how much additional length is added by adding an additional word to the line float extraLength = 0f; int extraSpaceGaps = 0; int extraCharGaps = 0; bool contractPossible = false; TextNode contractEndNode = null; for (node = expandEndNode.Next; node != null; node = node.Next) { if (node.Type == TextNodeType.LineBreak) break; if (node.Type == TextNodeType.Space) { extraLength += node.Length; extraSpaceGaps++; } else if (node.Type == TextNodeType.Word) { contractEndNode = node; contractPossible = true; extraLength += node.Length; extraCharGaps += (node.Text.Length - 1); break; } } if (justifiable) { //last part of this condition is to ensure that the full contraction is possible (it is all or nothing with contractions, since it looks really bad if we don't manage the full) bool contract = contractPossible && (extraLength + length - targetLength) * Options.JustifyContractionPenalty < (targetLength - length) && ((targetLength - (length + extraLength + 1)) / targetLength > -Options.JustifyCapContract); if ((!contract && length < targetLength) || (contract && length + extraLength > targetLength)) //calculate padding pixels per word and char { if (contract) { length += extraLength + 1; charGaps += extraCharGaps; spaceGaps += extraSpaceGaps; } int totalPixels = (int)(targetLength - length); //the total number of pixels that need to be added to line to justify it int spacePixels = 0; //number of pixels to spread out amongst spaces int charPixels = 0; //number of pixels to spread out amongst char gaps if (contract) { if (totalPixels / targetLength < -Options.JustifyCapContract) totalPixels = (int)(-Options.JustifyCapContract * targetLength); } else { if (totalPixels / targetLength > Options.JustifyCapExpand) totalPixels = (int)(Options.JustifyCapExpand * targetLength); } //work out how to spread pixles between character gaps and word spaces if (charGaps == 0) { spacePixels = totalPixels; } else if (spaceGaps == 0) { charPixels = totalPixels; } else { if (contract) charPixels = (int)(totalPixels * Options.JustifyCharacterWeightForContract * charGaps / spaceGaps); else charPixels = (int)(totalPixels * Options.JustifyCharacterWeightForExpand * charGaps / spaceGaps); if ((!contract && charPixels > totalPixels) || (contract && charPixels < totalPixels)) charPixels = totalPixels; spacePixels = totalPixels - charPixels; } int pixelsPerChar = 0; //minimum number of pixels to add per char int leftOverCharPixels = 0; //number of pixels remaining to only add for some chars if (charGaps != 0) { pixelsPerChar = charPixels / charGaps; leftOverCharPixels = charPixels - pixelsPerChar * charGaps; } int pixelsPerSpace = 0; //minimum number of pixels to add per space int leftOverSpacePixels = 0; //number of pixels remaining to only add for some spaces if (spaceGaps != 0) { pixelsPerSpace = spacePixels / spaceGaps; leftOverSpacePixels = spacePixels - pixelsPerSpace * spaceGaps; } //now actually iterate over all nodes and set tweaked length for (node = headNode; node != null; node = node.Next) { if (node.Type == TextNodeType.Space) { node.LengthTweak = pixelsPerSpace; if (leftOverSpacePixels > 0) { node.LengthTweak += 1; leftOverSpacePixels--; } else if (leftOverSpacePixels < 0) { node.LengthTweak -= 1; leftOverSpacePixels++; } } else if (node.Type == TextNodeType.Word) { int cGaps = (node.Text.Length - 1); if (CrumbledWord(node)) cGaps++; node.LengthTweak = cGaps * pixelsPerChar; if (leftOverCharPixels >= cGaps) { node.LengthTweak += cGaps; leftOverCharPixels -= cGaps; } else if (leftOverCharPixels <= -cGaps) { node.LengthTweak -= cGaps; leftOverCharPixels += cGaps; } else { node.LengthTweak += leftOverCharPixels; leftOverCharPixels = 0; } } if ((!contract && node == expandEndNode) || (contract && node == contractEndNode)) break; } } } }
/// <summary> /// Checks whether to skip trailing space on line because the next word does not fit. /// We only check one space - the assumption is that if there is more than one, /// it is a deliberate attempt to insert spaces. /// </summary> /// <param name="node"></param> /// <param name="lengthSoFar"></param> /// <param name="boundWidth"></param> /// <returns></returns> internal bool SkipTrailingSpace(TextNode node, float lengthSoFar, float boundWidth) { if (node.Type == TextNodeType.Space && node.Next != null && node.Next.Type == TextNodeType.Word && node.ModifiedLength + node.Next.ModifiedLength + lengthSoFar > boundWidth) return true; return false; }
public void Reset() { currentNode = null; }
internal bool CrumbledWord(TextNode node) { return (node.Type == TextNodeType.Word && node.Next != null && node.Next.Type == TextNodeType.Word); }
private float MeasureTextNodeLength(TextNode node, FontData fontData, FontRenderOptions options) { bool monospaced = fontData.IsMonospacingActive(options); float monospaceWidth = fontData.GetMonoSpaceWidth(options); if (node.Type == TextNodeType.Space) { if (monospaced) return monospaceWidth; return (float)Math.Ceiling(fontData.meanGlyphWidth * options.WordSpacing); } float length = 0f; if (node.Type == TextNodeType.Word) { for (int i = 0; i < node.Text.Length; i++) { char c = node.Text[i]; if (fontData.CharSetMapping.ContainsKey(c)) { if (monospaced) length += monospaceWidth; else length += (float)Math.Ceiling(fontData.CharSetMapping[c].rect.Width + fontData.meanGlyphWidth * options.CharacterSpacing + fontData.GetKerningPairCorrection(i, node.Text, node)); } } } return length; }
public virtual bool MoveNext() { if (currentNode == null) currentNode = targetList.Head; else currentNode = currentNode.Next; return currentNode != null; }
/// <summary> /// Splits a word node in two, adding both new nodes to the list in sequence. /// </summary> /// <param name="node"></param> /// <returns>The first new node</returns> public TextNode SplitNode(TextNode node) { if (node.Type != TextNodeType.Word) throw new Exception("Cannot slit text node of type: " + node.Type); int midPoint = node.Text.Length / 2; string newFirstHalf = node.Text.Substring(0, midPoint); string newSecondHalf = node.Text.Substring(midPoint, node.Text.Length - midPoint); TextNode newFirst = new TextNode(TextNodeType.Word, newFirstHalf); TextNode newSecond = new TextNode(TextNodeType.Word, newSecondHalf); newFirst.Next = newSecond; newSecond.Previous = newFirst; //node is head if (node.Previous == null) Head = newFirst; else { node.Previous.Next = newFirst; newFirst.Previous = node.Previous; } //node is tail if (node.Next == null) Tail = newSecond; else { node.Next.Previous = newSecond; newSecond.Next = node.Next; } return newFirst; }
/// <summary> /// Splits a word into sub-words of size less than or equal to baseCaseSize /// </summary> /// <param name="node"></param> /// <param name="baseCaseSize"></param> public void Crumble(TextNode node, int baseCaseSize) { //base case if (node.Text.Length <= baseCaseSize) return; var left = SplitNode(node); var right = left.Next; Crumble(left, baseCaseSize); Crumble(right, baseCaseSize); }
public void Add(TextNode node) { //new node is head (and tail) if (Head == null) { Head = node; Tail = node; } else { Tail.Next = node; node.Previous = Tail; Tail = node; } }
/// <summary> /// Returns the kerning length correction for the character at the given index in the given string. /// Also, if the text is part of a textNode list, the nextNode is given so that the following /// node can be checked incase of two adjacent word nodes. /// </summary> /// <param name="index"></param> /// <param name="text"></param> /// <param name="textNode"></param> /// <returns></returns> public int GetKerningPairCorrection(int index, string text, TextNode textNode) { if (KerningPairs == null) return 0; var chars = new char[2]; if (index + 1 == text.Length) { if (textNode != null && textNode.Next != null && textNode.Next.Type == TextNodeType.Word) chars[1] = textNode.Next.Text[0]; else return 0; } else { chars[1] = text[index + 1]; } chars[0] = text[index]; String str = new String(chars); if (KerningPairs.ContainsKey(str)) return KerningPairs[str]; return 0; }
private void RenderWord(BitmapFont font, TextNode node, float x, float y, float xOffset, float yOffset, Color4 color, float rotation, float scale, bool flipHorizontally, bool flipVertically) { if (node.Type != TextNodeType.Word) return; int charGaps = node.Text.Length - 1; bool isCrumbleWord = font.CrumbledWord(node); if (isCrumbleWord) charGaps++; int pixelsPerGap = 0; int leftOverPixels = 0; if (charGaps != 0) { pixelsPerGap = (int)node.LengthTweak / charGaps; leftOverPixels = (int)node.LengthTweak - pixelsPerGap * charGaps; } for (int i = 0; i < node.Text.Length; i++) { char c = node.Text[i]; if (font.fontData.CharSetMapping.ContainsKey(c)) { var glyph = font.fontData.CharSetMapping[c]; RenderGlyph(font, c, x, y, xOffset, yOffset, color, rotation, scale, flipHorizontally, flipVertically); if (font.IsMonospacingActive) xOffset += font.MonoSpaceWidth; else xOffset += (int)Math.Ceiling(glyph.rect.Width + font.fontData.meanGlyphWidth * font.Options.CharacterSpacing + font.fontData.GetKerningPairCorrection(i, node.Text, node)); xOffset += pixelsPerGap; if (leftOverPixels > 0) { xOffset += 1.0f; leftOverPixels--; } else if (leftOverPixels < 0) { xOffset -= 1.0f; leftOverPixels++; } } } }