/// <summary> /// Word-wraps a given piece of CuteML, assuming that it is linearly flowing text. Newline (<c>\n</c>) characters can /// be used to split the text into multiple paragraphs. The special <c>[+...]</c> tag marks text that may not be /// broken by wrapping (effectively turning all spaces into non-breaking spaces).</summary> /// <typeparam name="TState"> /// The type of the text state that a tag can change, e.g. font or color.</typeparam> /// <param name="node"> /// The root node of the CuteML tree to word-wrap.</param> /// <param name="initialState"> /// The initial text state.</param> /// <param name="wrapWidth"> /// The maximum width at which to word-wrap. This width can be measured in any unit, as long as <paramref /// name="measure"/> uses the same unit.</param> /// <param name="measure"> /// A delegate that measures the width of any piece of text.</param> /// <param name="render"> /// A delegate that is called whenever a piece of text is ready to be rendered.</param> /// <param name="advanceToNextLine"> /// A delegate that is called to advance to the next line.</param> /// <param name="nextState"> /// A delegate that determines how each CuteML tag character modifies the state (font, color etc.).</param> /// <returns> /// The maximum width of the text.</returns> public static int WordWrap <TState>(CuteNode node, TState initialState, int wrapWidth, CuteMeasure <TState> measure, CuteRender <TState> render, CuteNextLine <TState> advanceToNextLine, CuteNextState <TState> nextState) { if (node == null) { throw new ArgumentNullException(nameof(node)); } if (wrapWidth <= 0) { throw new ArgumentException("Wrap width must be greater than zero.", nameof(wrapWidth)); } var data = new CuteWalkData <TState> { AtStartOfLine = true, WordPieces = new List <string>(), WordPiecesState = new List <TState>(), WordPiecesWidths = new List <int>(), WordPiecesWidthsSum = 0, Measure = measure, Render = render, AdvanceToNextLine = advanceToNextLine, NextState = nextState, X = 0, WrapWidth = wrapWidth, ActualWidth = 0 }; data.CuteWalkWordWrap(node, initialState); return(data.ActualWidth); }
public void CuteWalkWordWrap(CuteNode node, TState initialState) { if (node == null) { throw new ArgumentNullException(nameof(node)); } cuteWalkWordWrapRecursive(node, initialState, false); if (WordPieces.Count > 0) { renderPieces(initialState); } }
private void cuteWalkWordWrapRecursive(CuteNode node, TState state, bool curNowrap) { if (node is CuteTag tag) { var newState = state; if (tag.Tag == '+') { curNowrap = true; } else if (tag.Tag != null) { var tup = NextState(state, tag.Tag.Value, tag.Attribute); newState = tup.newState; X += tup.advance; } foreach (var child in tag.Children) { cuteWalkWordWrapRecursive(child, newState, curNowrap); } } else if (node is CuteText text) { var txt = text.Text; for (int i = 0; i < txt.Length; i++) { // Check whether we are looking at a whitespace character or not, and if not, find the end of the word. int lengthOfWord = 0; while (lengthOfWord + i < txt.Length && (curNowrap || !char.IsWhiteSpace(txt, lengthOfWord + i)) && txt[lengthOfWord + i] != '\n') { lengthOfWord++; } if (lengthOfWord > 0) { // We are looking at a word. (It doesn’t matter whether we’re at the beginning of the word or in the middle of one.) retry1: string fragment = txt.Substring(i, lengthOfWord); var fragmentWidth = Measure(state, fragment); retry2: // If we are at the start of a line, and the word itself doesn’t fit on a line by itself, we need to break the word up. if (AtStartOfLine && X + WordPiecesWidthsSum + fragmentWidth > WrapWidth) { // We don’t know exactly where to break the word, so use binary search to discover where that is. if (lengthOfWord > 1) { lengthOfWord /= 2; goto retry1; } // If we get to here, ‘WordPieces’ contains as much of the word as fits into one line, and the next letter makes it too long. // If ‘WordPieces’ is empty, we are at the beginning of a paragraph and the first letter already doesn’t fit. if (WordPieces.Count > 0) { // Render the part of the word that fits on the line and then move to the next line. renderPieces(state); advanceToNextLine(state, false); } } else if (!AtStartOfLine && X + Measure(state, " ") + WordPiecesWidthsSum + fragmentWidth > WrapWidth) { // We have already rendered some text on this line, but the word we’re looking at right now doesn’t // fit into the rest of the line, so leave the rest of this line blank and advance to the next line. advanceToNextLine(state, false); // In case the word also doesn’t fit on a line all by itself, go back to top (now that ‘AtStartOfLine’ is true) // where it will check whether we need to break the word apart. goto retry2; } // If we get to here, the current fragment fits on the current line (or it is a single character that overflows // the line all by itself). WordPieces.Add(fragment); WordPiecesState.Add(state); WordPiecesWidths.Add(fragmentWidth); WordPiecesWidthsSum += fragmentWidth; i += lengthOfWord - 1; continue; } // We encounter a whitespace character. All the word pieces fit on the current line, so render them. if (WordPieces.Count > 0) { renderPieces(state); AtStartOfLine = false; } SpaceState = state; if (txt[i] == '\n') { // If the whitespace character is actually a newline, start a new paragraph. advanceToNextLine(state, true); } else if (AtStartOfLine) { // Otherwise, if we are at the beginning of the line, treat this space as the paragraph’s indentation. CurParagraphIndent += renderSpace(state); } } } else { throw new InvalidOperationException("A CuteNode is expected to be either CuteTag or CuteText, not {0}.".Fmt(node.GetType().FullName)); } }