/// <summary> /// Performs text wrapping returns a list of text lines. /// </summary> /// <param name="textRuns"></param> /// <param name="textRange">The text range that is covered by the text runs.</param> /// <param name="paragraphWidth">The paragraph width.</param> /// <param name="paragraphProperties">The text paragraph properties.</param> /// <param name="flowDirection"></param> /// <param name="currentLineBreak">The current line break if the line was explicitly broken.</param> /// <returns>The wrapped text line.</returns> private static TextLineImpl PerformTextWrapping(List <ShapedTextCharacters> textRuns, TextRange textRange, double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection flowDirection, TextLineBreak?currentLineBreak) { var measuredLength = MeasureLength(textRuns, textRange, paragraphWidth); var currentLength = 0; var lastWrapPosition = 0; var currentPosition = 0; for (var index = 0; index < textRuns.Count; index++) { var currentRun = textRuns[index]; var lineBreaker = new LineBreakEnumerator(currentRun.Text); var breakFound = false; while (lineBreaker.MoveNext()) { if (lineBreaker.Current.Required && currentLength + lineBreaker.Current.PositionMeasure <= measuredLength) { //Explicit break found breakFound = true; currentPosition = currentLength + lineBreaker.Current.PositionWrap; break; } if (currentLength + lineBreaker.Current.PositionMeasure > measuredLength) { if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow) { if (lastWrapPosition > 0) { currentPosition = lastWrapPosition; breakFound = true; break; } //Find next possible wrap position (overflow) if (index < textRuns.Count - 1) { if (lineBreaker.Current.PositionWrap != currentRun.Text.Length) { //We already found the next possible wrap position. breakFound = true; currentPosition = currentLength + lineBreaker.Current.PositionWrap; break; } while (lineBreaker.MoveNext() && index < textRuns.Count) { currentPosition += lineBreaker.Current.PositionWrap; if (lineBreaker.Current.PositionWrap != currentRun.Text.Length) { break; } index++; if (index >= textRuns.Count) { break; } currentRun = textRuns[index]; lineBreaker = new LineBreakEnumerator(currentRun.Text); } } else { currentPosition = currentLength + lineBreaker.Current.PositionWrap; } breakFound = true; break; } //We overflowed so we use the last available wrap position. currentPosition = lastWrapPosition == 0 ? measuredLength : lastWrapPosition; breakFound = true; break; } if (lineBreaker.Current.PositionMeasure != lineBreaker.Current.PositionWrap) { lastWrapPosition = currentLength + lineBreaker.Current.PositionWrap; } } if (!breakFound) { currentLength += currentRun.Text.Length; continue; } measuredLength = currentPosition; break; } var splitResult = SplitShapedRuns(textRuns, measuredLength); textRange = new TextRange(textRange.Start, measuredLength); var remainingCharacters = splitResult.Second; var lineBreak = remainingCharacters?.Count > 0 ? new TextLineBreak(currentLineBreak?.TextEndOfLine, flowDirection, remainingCharacters) : null; if (lineBreak is null && currentLineBreak?.TextEndOfLine != null) { lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, flowDirection); } TextLineImpl.SortRuns(splitResult.First); var textLine = new TextLineImpl(splitResult.First, textRange, paragraphWidth, paragraphProperties, flowDirection, lineBreak); return(textLine.FinalizeLine()); }
/// <summary> /// Performs text wrapping returns a list of text lines. /// </summary> /// <param name="paragraphProperties">The text paragraph properties.</param> /// <param name="textRuns">The text run'S.</param> /// <param name="text">The text to analyze for break opportunities.</param> /// <param name="paragraphWidth"></param> /// <returns></returns> private static TextLine PerformTextWrapping(TextPointer text, IReadOnlyList <ShapedTextRun> textRuns, double paragraphWidth, TextParagraphProperties paragraphProperties) { var availableWidth = paragraphWidth; var currentWidth = 0.0; var runIndex = 0; var length = 0; while (runIndex < textRuns.Count) { var currentRun = textRuns[runIndex]; if (currentWidth + currentRun.GlyphRun.Bounds.Width > availableWidth) { var measuredLength = MeasureText(currentRun, paragraphWidth - currentWidth); if (measuredLength < currentRun.Text.Length) { var currentBreakPosition = -1; var lineBreaker = new LineBreakEnumerator(currentRun.Text); while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) { var nextBreakPosition = lineBreaker.Current.PositionWrap; if (nextBreakPosition == 0) { break; } if (nextBreakPosition > measuredLength) { break; } currentBreakPosition = nextBreakPosition; } if (currentBreakPosition != -1) { measuredLength = currentBreakPosition; } } length += measuredLength; var splitResult = SplitTextRuns(textRuns, length); var textLineMetrics = TextLineMetrics.Create(splitResult.First, paragraphWidth, paragraphProperties.TextAlignment); return(new SimpleTextLine(text.Take(length), splitResult.First, textLineMetrics)); } currentWidth += currentRun.GlyphRun.Bounds.Width; length += currentRun.GlyphRun.Characters.Length; runIndex++; } return(new SimpleTextLine(text, textRuns, TextLineMetrics.Create(textRuns, paragraphWidth, paragraphProperties.TextAlignment))); }
/// <summary> /// Formats a text line. /// </summary> /// <param name="textSource">The text source.</param> /// <param name="firstTextSourceIndex">The first character index to start the text line from.</param> /// <param name="paragraphWidth">A <see cref="double"/> value that specifies the width of the paragraph that the line fills.</param> /// <param name="paragraphProperties">A <see cref="TextParagraphProperties"/> value that represents paragraph properties, /// such as TextWrapping, TextAlignment, or TextStyle.</param> /// <returns>The formatted line.</returns> public abstract TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties);
/// <summary> /// Performs text wrapping returns a list of text lines. /// </summary> /// <param name="textRuns">The text run's.</param> /// <param name="textRange">The text range that is covered by the text runs.</param> /// <param name="paragraphWidth">The paragraph width.</param> /// <param name="paragraphProperties">The text paragraph properties.</param> /// <param name="currentLineBreak">The current line break if the line was explicitly broken.</param> /// <returns>The wrapped text line.</returns> private static TextLine PerformTextWrapping(List <ShapedTextCharacters> textRuns, TextRange textRange, double paragraphWidth, TextParagraphProperties paragraphProperties, TextLineBreak currentLineBreak) { var availableWidth = paragraphWidth; var currentWidth = 0.0; var runIndex = 0; var currentLength = 0; while (runIndex < textRuns.Count) { var currentRun = textRuns[runIndex]; if (currentWidth + currentRun.Size.Width > availableWidth) { var breakFound = false; var currentBreakPosition = 0; if (TryMeasureCharacters(currentRun, paragraphWidth - currentWidth, out var measuredLength)) { if (measuredLength < currentRun.Text.Length) { var lineBreaker = new LineBreakEnumerator(currentRun.Text); while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) { var nextBreakPosition = lineBreaker.Current.PositionWrap; if (nextBreakPosition == 0 || nextBreakPosition > measuredLength) { break; } breakFound = lineBreaker.Current.Required || lineBreaker.Current.PositionWrap != currentRun.Text.Length; currentBreakPosition = nextBreakPosition; } } } else { // Make sure we wrap at least one character. if (currentLength == 0) { measuredLength = 1; } } if (breakFound) { measuredLength = currentBreakPosition; } else { if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow) { var lineBreaker = new LineBreakEnumerator(currentRun.Text.Skip(currentBreakPosition)); if (lineBreaker.MoveNext()) { measuredLength = currentBreakPosition + lineBreaker.Current.PositionWrap; } } } currentLength += measuredLength; var splitResult = SplitTextRuns(textRuns, currentLength); var textLineMetrics = TextLineMetrics.Create(splitResult.First, new TextRange(textRange.Start, currentLength), paragraphWidth, paragraphProperties); var remainingCharacters = splitResult.Second; if (currentLineBreak?.RemainingCharacters != null) { if (remainingCharacters != null) { remainingCharacters.AddRange(currentLineBreak.RemainingCharacters); } else { remainingCharacters = new List <ShapedTextCharacters>(currentLineBreak.RemainingCharacters); } } var lineBreak = remainingCharacters != null && remainingCharacters.Count > 0 ? new TextLineBreak(remainingCharacters) : null; return(new TextLineImpl(splitResult.First, textLineMetrics, lineBreak)); } currentWidth += currentRun.Size.Width; currentLength += currentRun.GlyphRun.Characters.Length; runIndex++; } return(new TextLineImpl(textRuns, TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties), currentLineBreak?.RemainingCharacters != null ? new TextLineBreak(currentLineBreak.RemainingCharacters) : null)); }
/// <summary> /// Performs text trimming and returns a trimmed line. /// </summary> /// <param name="paragraphWidth">A <see cref="double"/> value that specifies the width of the paragraph that the line fills.</param> /// <param name="paragraphProperties">A <see cref="TextParagraphProperties"/> value that represents paragraph properties, /// such as TextWrapping, TextAlignment, or TextStyle.</param> /// <param name="textRuns">The text runs to perform the trimming on.</param> /// <param name="text">The text that was used to construct the text runs.</param> /// <returns></returns> private static TextLine PerformTextTrimming(TextPointer text, IReadOnlyList <ShapedTextRun> textRuns, double paragraphWidth, TextParagraphProperties paragraphProperties) { var textTrimming = paragraphProperties.TextTrimming; var availableWidth = paragraphWidth; var currentWidth = 0.0; var runIndex = 0; while (runIndex < textRuns.Count) { var currentRun = textRuns[runIndex]; currentWidth += currentRun.GlyphRun.Bounds.Width; if (currentWidth > availableWidth) { var ellipsisRun = CreateEllipsisRun(currentRun.Style); var measuredLength = MeasureText(currentRun, availableWidth - ellipsisRun.GlyphRun.Bounds.Width); if (textTrimming == TextTrimming.WordEllipsis) { if (measuredLength < text.End) { var currentBreakPosition = 0; var lineBreaker = new LineBreakEnumerator(currentRun.Text); while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) { var nextBreakPosition = lineBreaker.Current.PositionWrap; if (nextBreakPosition == 0) { break; } if (nextBreakPosition > measuredLength) { break; } currentBreakPosition = nextBreakPosition; } measuredLength = currentBreakPosition; } } var splitResult = SplitTextRuns(textRuns, measuredLength); var trimmedRuns = new List <ShapedTextRun>(splitResult.First.Count + 1); trimmedRuns.AddRange(splitResult.First); trimmedRuns.Add(ellipsisRun); var textLineMetrics = TextLineMetrics.Create(trimmedRuns, paragraphWidth, paragraphProperties.TextAlignment); return(new SimpleTextLine(text.Take(measuredLength), trimmedRuns, textLineMetrics)); } availableWidth -= currentRun.GlyphRun.Bounds.Width; runIndex++; } return(new SimpleTextLine(text, textRuns, TextLineMetrics.Create(textRuns, paragraphWidth, paragraphProperties.TextAlignment))); }
/// <summary> /// Performs text wrapping returns a list of text lines. /// </summary> /// <param name="textRuns">The text run's.</param> /// <param name="textRange">The text range that is covered by the text runs.</param> /// <param name="paragraphWidth">The paragraph width.</param> /// <param name="paragraphProperties">The text paragraph properties.</param> /// <param name="currentLineBreak">The current line break if the line was explicitly broken.</param> /// <returns>The wrapped text line.</returns> private static TextLine PerformTextWrapping(List <ShapedTextCharacters> textRuns, TextRange textRange, double paragraphWidth, TextParagraphProperties paragraphProperties, TextLineBreak?currentLineBreak) { var availableWidth = paragraphWidth; var currentWidth = 0.0; var measuredLength = 0; foreach (var currentRun in textRuns) { if (currentWidth + currentRun.Size.Width > availableWidth) { if (TryMeasureCharacters(currentRun, paragraphWidth - currentWidth, out var count)) { measuredLength += count; } break; } currentWidth += currentRun.Size.Width; measuredLength += currentRun.Text.Length; } var currentLength = 0; var lastWrapPosition = 0; var currentPosition = 0; if (measuredLength == 0 && paragraphProperties.TextWrapping != TextWrapping.WrapWithOverflow) { measuredLength = 1; } else { for (var index = 0; index < textRuns.Count; index++) { var currentRun = textRuns[index]; var lineBreaker = new LineBreakEnumerator(currentRun.Text); var breakFound = false; while (lineBreaker.MoveNext()) { if (lineBreaker.Current.Required && currentLength + lineBreaker.Current.PositionMeasure <= measuredLength) { breakFound = true; currentPosition = currentLength + lineBreaker.Current.PositionWrap; break; } if ((paragraphProperties.TextWrapping != TextWrapping.WrapWithOverflow || lastWrapPosition != 0) && currentLength + lineBreaker.Current.PositionMeasure > measuredLength) { if (lastWrapPosition > 0) { currentPosition = lastWrapPosition; } else { currentPosition = currentLength + measuredLength; } breakFound = true; break; } if (currentLength + lineBreaker.Current.PositionWrap >= measuredLength) { currentPosition = currentLength + lineBreaker.Current.PositionWrap; if (index < textRuns.Count - 1 && lineBreaker.Current.PositionWrap == currentRun.Text.Length) { var nextRun = textRuns[index + 1]; lineBreaker = new LineBreakEnumerator(nextRun.Text); if (lineBreaker.MoveNext() && lineBreaker.Current.PositionMeasure == 0) { currentPosition += lineBreaker.Current.PositionWrap; } } breakFound = true; break; } lastWrapPosition = currentLength + lineBreaker.Current.PositionWrap; } if (!breakFound) { currentLength += currentRun.Text.Length; continue; } measuredLength = currentPosition; break; } } var splitResult = SplitTextRuns(textRuns, measuredLength); textRange = new TextRange(textRange.Start, measuredLength); var remainingCharacters = splitResult.Second; var lineBreak = remainingCharacters?.Count > 0 ? new TextLineBreak(remainingCharacters) : null; if (lineBreak is null && currentLineBreak?.TextEndOfLine != null) { lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine); } return(new TextLineImpl(splitResult.First, textRange, paragraphWidth, paragraphProperties, lineBreak)); }
/// <summary> /// Shape specified text runs with specified paragraph embedding. /// </summary> /// <param name="textRuns">The text runs to shape.</param> /// <param name="paragraphProperties">The default paragraph properties.</param> /// <param name="resolvedFlowDirection">The resolved flow direction.</param> /// <returns> /// A list of shaped text characters. /// </returns> private static List <DrawableTextRun> ShapeTextRuns(List <TextRun> textRuns, TextParagraphProperties paragraphProperties, out FlowDirection resolvedFlowDirection) { var flowDirection = paragraphProperties.FlowDirection; var drawableTextRuns = new List <DrawableTextRun>(); var biDiData = new BidiData((sbyte)flowDirection); foreach (var textRun in textRuns) { if (textRun.Text.IsEmpty) { var text = new char[textRun.TextSourceLength]; biDiData.Append(text); } else { biDiData.Append(textRun.Text); } } var biDi = BidiAlgorithm.Instance.Value !; biDi.Process(biDiData); var resolvedEmbeddingLevel = biDi.ResolveEmbeddingLevel(biDiData.Classes); resolvedFlowDirection = (resolvedEmbeddingLevel & 1) == 0 ? FlowDirection.LeftToRight : FlowDirection.RightToLeft; var processedRuns = new List <TextRun>(textRuns.Count); foreach (var coalescedRuns in CoalesceLevels(textRuns, biDi.ResolvedLevels)) { processedRuns.AddRange(coalescedRuns); } for (var index = 0; index < processedRuns.Count; index++) { var currentRun = processedRuns[index]; switch (currentRun) { case DrawableTextRun drawableRun: { drawableTextRuns.Add(drawableRun); break; } case ShapeableTextCharacters shapeableRun: { var groupedRuns = new List <ShapeableTextCharacters>(2) { shapeableRun }; var text = currentRun.Text; var start = currentRun.Text.Start; var length = currentRun.Text.Length; var bufferOffset = currentRun.Text.BufferOffset; while (index + 1 < processedRuns.Count) { if (processedRuns[index + 1] is not ShapeableTextCharacters nextRun) { break; } if (shapeableRun.CanShapeTogether(nextRun)) { groupedRuns.Add(nextRun); length += nextRun.Text.Length; if (start > nextRun.Text.Start) { start = nextRun.Text.Start; } if (bufferOffset > nextRun.Text.BufferOffset) { bufferOffset = nextRun.Text.BufferOffset; } text = new ReadOnlySlice <char>(text.Buffer, start, length, bufferOffset); index++; shapeableRun = nextRun; continue; } break; } var shaperOptions = new TextShaperOptions(currentRun.Properties !.Typeface.GlyphTypeface, currentRun.Properties.FontRenderingEmSize, shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, paragraphProperties.DefaultIncrementalTab); drawableTextRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions)); break; } } } return(drawableTextRuns); }
/// <summary> /// Formats a text line. /// </summary> /// <param name="textSource">The text source.</param> /// <param name="firstTextSourceIndex">The first character index to start the text line from.</param> /// <param name="paragraphWidth">A <see cref="double"/> value that specifies the width of the paragraph that the line fills.</param> /// <param name="paragraphProperties">A <see cref="TextParagraphProperties"/> value that represents paragraph properties, /// such as TextWrapping, TextAlignment, or TextStyle.</param> /// <param name="previousLineBreak">A <see cref="TextLineBreak"/> value that specifies the text formatter state, /// in terms of where the previous line in the paragraph was broken by the text formatting process.</param> /// <returns>The formatted line.</returns> public abstract TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties, TextLineBreak?previousLineBreak = null);
/// <summary> /// Performs text wrapping returns a list of text lines. /// </summary> /// <param name="textRuns">The text run's.</param> /// <param name="textRange">The text range that is covered by the text runs.</param> /// <param name="paragraphWidth">The paragraph width.</param> /// <param name="paragraphProperties">The text paragraph properties.</param> /// <returns>The wrapped text line.</returns> private static TextLine PerformTextWrapping(IReadOnlyList <ShapedTextCharacters> textRuns, TextRange textRange, double paragraphWidth, TextParagraphProperties paragraphProperties) { var availableWidth = paragraphWidth; var currentWidth = 0.0; var runIndex = 0; var length = 0; while (runIndex < textRuns.Count) { var currentRun = textRuns[runIndex]; if (currentWidth + currentRun.GlyphRun.Bounds.Width > availableWidth) { var measuredLength = MeasureText(currentRun, paragraphWidth - currentWidth); if (measuredLength < currentRun.Text.Length) { if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow) { var lineBreaker = new LineBreakEnumerator(currentRun.Text.Skip(measuredLength)); if (lineBreaker.MoveNext()) { measuredLength += lineBreaker.Current.PositionWrap; } else { measuredLength = currentRun.Text.Length; } } else { var currentBreakPosition = -1; var lineBreaker = new LineBreakEnumerator(currentRun.Text); while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) { var nextBreakPosition = lineBreaker.Current.PositionWrap; if (nextBreakPosition == 0) { break; } if (nextBreakPosition > measuredLength) { break; } currentBreakPosition = nextBreakPosition; } if (currentBreakPosition != -1) { measuredLength = currentBreakPosition; } } } length += measuredLength; var splitResult = SplitTextRuns(textRuns, length); var textLineMetrics = TextLineMetrics.Create(splitResult.First, new TextRange(textRange.Start, length), paragraphWidth, paragraphProperties); var lineBreak = splitResult.Second != null && splitResult.Second.Count > 0 ? new TextLineBreak(splitResult.Second) : null; return(new TextLineImpl(splitResult.First, textLineMetrics, lineBreak)); } currentWidth += currentRun.GlyphRun.Bounds.Width; length += currentRun.GlyphRun.Characters.Length; runIndex++; } return(new TextLineImpl(textRuns, TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties))); }
/// <summary> /// Creates an empty text line. /// </summary> /// <returns>The empty text line.</returns> public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties) { var flowDirection = paragraphProperties.FlowDirection; var properties = paragraphProperties.DefaultTextRunProperties; var glyphTypeface = properties.Typeface.GlyphTypeface; var text = new ReadOnlySlice <char>(s_empty, firstTextSourceIndex, 1); var glyph = glyphTypeface.GetGlyph(s_empty[0]); var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) }; var shapedBuffer = new ShapedBuffer(text, glyphInfos, glyphTypeface, properties.FontRenderingEmSize, (sbyte)flowDirection); var textRuns = new List <DrawableTextRun> { new ShapedTextCharacters(shapedBuffer, properties) }; return(new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection).FinalizeLine()); }