private static bool TryGetLineBreak(TextRun textRun, out LineBreak lineBreak) { lineBreak = default; if (textRun.Text.IsEmpty) { return(false); } var lineBreakEnumerator = new LineBreakEnumerator(textRun.Text); while (lineBreakEnumerator.MoveNext()) { if (!lineBreakEnumerator.Current.Required) { continue; } lineBreak = lineBreakEnumerator.Current; return(lineBreak.PositionWrap >= textRun.Text.Length || true); } return(false); }
public void BasicLatinTest() { var lineBreaker = new LineBreakEnumerator("Hello World\r\nThis is a test.".AsMemory()); Assert.True(lineBreaker.MoveNext()); Assert.Equal(6, lineBreaker.Current.PositionWrap); Assert.False(lineBreaker.Current.Required); Assert.True(lineBreaker.MoveNext()); Assert.Equal(13, lineBreaker.Current.PositionWrap); Assert.True(lineBreaker.Current.Required); Assert.True(lineBreaker.MoveNext()); Assert.Equal(18, lineBreaker.Current.PositionWrap); Assert.False(lineBreaker.Current.Required); Assert.True(lineBreaker.MoveNext()); Assert.Equal(21, lineBreaker.Current.PositionWrap); Assert.False(lineBreaker.Current.Required); Assert.True(lineBreaker.MoveNext()); Assert.Equal(23, lineBreaker.Current.PositionWrap); Assert.False(lineBreaker.Current.Required); Assert.True(lineBreaker.MoveNext()); Assert.Equal(28, lineBreaker.Current.PositionWrap); Assert.False(lineBreaker.Current.Required); Assert.False(lineBreaker.MoveNext()); }
public void Should_Split_Text_By_Explicit_Breaks() { //ABC [0 3] //DEF\r[4 7] //\r[8] //Hello\r\n[9 15] const string text = "ABC DEF\r\rHELLO\r\n"; var buffer = new ReadOnlySlice <char>(text.AsMemory()); var lineBreaker = new LineBreakEnumerator(buffer); var current = 0; Assert.True(lineBreaker.MoveNext()); var a = text.Substring(current, lineBreaker.Current.PositionMeasure - current + 1); Assert.Equal("ABC ", a); current += a.Length; Assert.True(lineBreaker.MoveNext()); var b = text.Substring(current, lineBreaker.Current.PositionMeasure - current + 1); Assert.Equal("DEF\r", b); current += b.Length; Assert.True(lineBreaker.MoveNext()); var c = text.Substring(current, lineBreaker.Current.PositionMeasure - current + 1); Assert.Equal("\r", c); current += c.Length; Assert.True(lineBreaker.MoveNext()); var d = text.Substring(current, text.Length - current); Assert.Equal("HELLO\r\n", d); }
public void Should_Enumerate_LineBreaks(string text, int expectedLength) { var textMemory = text.AsMemory(); var enumerator = new LineBreakEnumerator(textMemory); Assert.True(enumerator.MoveNext()); Assert.Equal(expectedLength, enumerator.Current.PositionWrap); }
private static List <LineBreak> GetBreaks(LineBreakEnumerator lineBreaker) { var breaks = new List <LineBreak>(); while (lineBreaker.MoveNext()) { breaks.Add(lineBreaker.Current); } return(breaks); }
public void Should_Wrap(string text, string familyName, int numberOfCharactersPerLine) { using (Start()) { var lineBreaker = new LineBreakEnumerator(text.AsMemory()); var expected = new List <int>(); while (lineBreaker.MoveNext()) { expected.Add(lineBreaker.Current.PositionWrap - 1); } var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#" + familyName); var defaultProperties = new GenericTextRunProperties(Typeface.Default); var textSource = new SingleBufferTextSource(text, defaultProperties); var formatter = new TextFormatterImpl(); var glyph = typeface.GlyphTypeface.GetGlyph('a'); var advance = typeface.GlyphTypeface.GetGlyphAdvance(glyph) * (12.0 / typeface.GlyphTypeface.DesignEmHeight); var paragraphWidth = advance * numberOfCharactersPerLine; var currentPosition = 0; while (currentPosition < text.Length) { var textLine = formatter.FormatLine(textSource, currentPosition, paragraphWidth, new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap)); var end = textLine.FirstTextSourceIndex + textLine.Length - 1; Assert.True(expected.Contains(end)); var index = expected.IndexOf(end); for (var i = 0; i <= index; i++) { expected.RemoveAt(0); } currentPosition += textLine.Length; } } }
public void ShouldFindBreaks(int lineNumber, int[] codePoints, int[] breakPoints) { var text = string.Join(null, codePoints.Select(char.ConvertFromUtf32)); var lineBreaker = new LineBreakEnumerator(text.AsMemory()); var foundBreaks = new List <int>(); while (lineBreaker.MoveNext()) { foundBreaks.Add(lineBreaker.Current.PositionWrap); } // Check the same var pass = true; if (foundBreaks.Count != breakPoints.Length) { pass = false; } else { for (var i = 0; i < foundBreaks.Count; i++) { if (foundBreaks[i] != breakPoints[i]) { pass = false; } } } if (!pass) { _outputHelper.WriteLine($"Failed test on line {lineNumber}"); _outputHelper.WriteLine(""); _outputHelper.WriteLine($" Code Points: {string.Join(" ", codePoints)}"); _outputHelper.WriteLine($"Expected Breaks: {string.Join(" ", breakPoints)}"); _outputHelper.WriteLine($" Actual Breaks: {string.Join(" ", foundBreaks)}"); _outputHelper.WriteLine($" Text: {text}"); _outputHelper.WriteLine($" Char Props: {string.Join(" ", codePoints.Select(x => new Codepoint(x).LineBreakClass))}"); _outputHelper.WriteLine(""); } Assert.True(pass); }
private static bool TryGetLineBreak(TextRun textRun, out LineBreak lineBreak) { lineBreak = default; if (textRun.Text.IsEmpty) { return(false); } var lineBreakEnumerator = new LineBreakEnumerator(textRun.Text); while (lineBreakEnumerator.MoveNext()) { if (!lineBreakEnumerator.Current.Required) { continue; } lineBreak = lineBreakEnumerator.Current; if (lineBreak.PositionWrap >= textRun.Text.Length) { return(true); } //The line breaker isn't treating \n\r as a pair so we have to fix that here. if (textRun.Text[lineBreak.PositionMeasure] == '\n' && textRun.Text[lineBreak.PositionWrap] == '\r') { lineBreak = new LineBreak(lineBreak.PositionMeasure, lineBreak.PositionWrap + 1, lineBreak.Required); } return(true); } return(false); }
private static int FindPositionForTextWrapping(StringRange range, int maxIndex) { if (maxIndex > range.Length - 1) { maxIndex = range.Length - 1; } LineBreakEnumerator lineBreakEnumerator = new LineBreakEnumerator( new ReadOnlySlice <char>(range.String.AsMemory().Slice(range.OffsetToFirstChar, range.Length))); LineBreak?lineBreak = null; while (lineBreakEnumerator.MoveNext()) { if (lineBreakEnumerator.Current.PositionWrap > maxIndex) { break; } lineBreak = lineBreakEnumerator.Current; } return(lineBreak.HasValue ? lineBreak.Value.PositionWrap : maxIndex); }
/// <summary> /// Updates the layout and applies specified text style overrides. /// </summary> private void UpdateLayout() { if (_text.IsEmpty || Math.Abs(MaxWidth) < double.Epsilon || Math.Abs(MaxHeight) < double.Epsilon) { var textLine = CreateEmptyTextLine(0); TextLines = new List <TextLine> { textLine }; Bounds = new Rect(textLine.LineMetrics.BaselineOrigin.X, 0, 0, textLine.LineMetrics.Size.Height); } else { var textLines = new List <TextLine>(); double left = 0.0, right = 0.0, bottom = 0.0; var lineBreaker = new LineBreakEnumerator(_text); var currentPosition = 0; while (currentPosition < _text.Length && (MaxLines == 0 || textLines.Count < MaxLines)) { int length; if (lineBreaker.MoveNext()) { if (!lineBreaker.Current.Required) { continue; } length = lineBreaker.Current.PositionWrap - currentPosition; if (currentPosition + length < _text.Length) { //The line breaker isn't treating \n\r as a pair so we have to fix that here. if (_text[lineBreaker.Current.PositionMeasure] == '\n' && _text[lineBreaker.Current.PositionWrap] == '\r') { length++; } } } else { length = _text.Length - currentPosition; } var remainingLength = length; while (remainingLength > 0 && (MaxLines == 0 || textLines.Count < MaxLines)) { var textSlice = _text.AsSlice(currentPosition, remainingLength); var textSource = new FormattedTextSource(textSlice, _paragraphProperties.DefaultTextStyle, _textStyleOverrides); var textLine = TextFormatter.Current.FormatLine(textSource, 0, MaxWidth, _paragraphProperties); UpdateBounds(textLine, ref left, ref right, ref bottom); textLines.Add(textLine); if (!double.IsPositiveInfinity(MaxHeight) && bottom + textLine.LineMetrics.Size.Height > MaxHeight) { currentPosition = _text.Length; break; } if (_paragraphProperties.TextTrimming != TextTrimming.None) { currentPosition += remainingLength; break; } remainingLength -= textLine.Text.Length; currentPosition += textLine.Text.Length; } } if (lineBreaker.Current.Required && currentPosition == _text.Length) { var emptyTextLine = CreateEmptyTextLine(currentPosition); UpdateBounds(emptyTextLine, ref left, ref right, ref bottom); textLines.Add(emptyTextLine); } Bounds = new Rect(left, 0, right, bottom); TextLines = textLines; } }
/// <inheritdoc/> public override TextLine Collapse(params TextCollapsingProperties[] collapsingPropertiesList) { if (collapsingPropertiesList.Length == 0) { return(this); } var collapsingProperties = collapsingPropertiesList[0]; var runIndex = 0; var currentWidth = 0.0; var textRange = TextRange; var collapsedLength = 0; var shapedSymbol = TextFormatterImpl.CreateSymbol(collapsingProperties.Symbol, _paragraphProperties.FlowDirection); var availableWidth = collapsingProperties.Width - shapedSymbol.GlyphRun.Size.Width; while (runIndex < _textRuns.Count) { var currentRun = _textRuns[runIndex]; currentWidth += currentRun.Size.Width; if (currentWidth > availableWidth) { if (currentRun.TryMeasureCharacters(availableWidth, out var measuredLength)) { if (collapsingProperties.Style == TextCollapsingStyle.TrailingWord && measuredLength < textRange.End) { var currentBreakPosition = 0; var lineBreaker = new LineBreakEnumerator(currentRun.Text); while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) { var nextBreakPosition = lineBreaker.Current.PositionMeasure; if (nextBreakPosition == 0) { break; } if (nextBreakPosition >= measuredLength) { break; } currentBreakPosition = nextBreakPosition; } measuredLength = currentBreakPosition; } } collapsedLength += measuredLength; var splitResult = TextFormatterImpl.SplitShapedRuns(_textRuns, collapsedLength); var shapedTextCharacters = new List <ShapedTextCharacters>(splitResult.First.Count + 1); shapedTextCharacters.AddRange(splitResult.First); SortRuns(shapedTextCharacters); shapedTextCharacters.Add(shapedSymbol); textRange = new TextRange(textRange.Start, collapsedLength); var textLine = new TextLineImpl(shapedTextCharacters, textRange, _paragraphWidth, _paragraphProperties, _flowDirection, TextLineBreak, true); return(textLine.FinalizeLine()); } availableWidth -= currentRun.Size.Width; collapsedLength += currentRun.GlyphRun.Characters.Length; runIndex++; } return(this); }
/// <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> /// 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 currentLength = 0; while (runIndex < textRuns.Count) { var currentRun = textRuns[runIndex]; if (currentWidth + currentRun.GlyphRun.Bounds.Width > availableWidth) { var measuredLength = MeasureCharacters(currentRun, paragraphWidth - currentWidth); var breakFound = false; var currentBreakPosition = 0; 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; } } 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 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; currentLength += currentRun.GlyphRun.Characters.Length; runIndex++; } return(new TextLineImpl(textRuns, TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties))); }
/// <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))); }
public static List <DrawableTextRun>?Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis) { if (textLine.TextRuns is not List <DrawableTextRun> textRuns || textRuns.Count == 0) { return(null); } var runIndex = 0; var currentWidth = 0.0; var collapsedLength = 0; var shapedSymbol = TextFormatterImpl.CreateSymbol(properties.Symbol, FlowDirection.LeftToRight); if (properties.Width < shapedSymbol.GlyphRun.Size.Width) { //Not enough space to fit in the symbol return(new List <DrawableTextRun>(0)); } var availableWidth = properties.Width - shapedSymbol.Size.Width; while (runIndex < textRuns.Count) { var currentRun = textRuns[runIndex]; switch (currentRun) { case ShapedTextCharacters shapedRun: { currentWidth += shapedRun.Size.Width; if (currentWidth > availableWidth) { if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength)) { if (isWordEllipsis && measuredLength < textLine.Length) { var currentBreakPosition = 0; var lineBreaker = new LineBreakEnumerator(currentRun.Text); while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) { var nextBreakPosition = lineBreaker.Current.PositionMeasure; if (nextBreakPosition == 0) { break; } if (nextBreakPosition >= measuredLength) { break; } currentBreakPosition = nextBreakPosition; } measuredLength = currentBreakPosition; } } collapsedLength += measuredLength; var collapsedRuns = new List <DrawableTextRun>(textRuns.Count); if (collapsedLength > 0) { var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength); collapsedRuns.AddRange(splitResult.First); } collapsedRuns.Add(shapedSymbol); return(collapsedRuns); } availableWidth -= currentRun.Size.Width; break; } case { } drawableRun: { //The whole run needs to fit into available space if (currentWidth + drawableRun.Size.Width > availableWidth) { var collapsedRuns = new List <DrawableTextRun>(textRuns.Count); if (collapsedLength > 0) { var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength); collapsedRuns.AddRange(splitResult.First); } collapsedRuns.Add(shapedSymbol); return(collapsedRuns); } break; } } collapsedLength += currentRun.TextSourceLength; runIndex++; } return(null); }
public static List <ShapedTextCharacters>?Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis) { var shapedTextRuns = textLine.TextRuns as List <ShapedTextCharacters>; if (shapedTextRuns is null) { return(null); } var runIndex = 0; var currentWidth = 0.0; var collapsedLength = 0; var textRange = textLine.TextRange; var shapedSymbol = TextFormatterImpl.CreateSymbol(properties.Symbol, FlowDirection.LeftToRight); if (properties.Width < shapedSymbol.GlyphRun.Size.Width) { return(new List <ShapedTextCharacters>(0)); } var availableWidth = properties.Width - shapedSymbol.Size.Width; while (runIndex < shapedTextRuns.Count) { var currentRun = shapedTextRuns[runIndex]; currentWidth += currentRun.Size.Width; if (currentWidth > availableWidth) { if (currentRun.TryMeasureCharacters(availableWidth, out var measuredLength)) { if (isWordEllipsis && measuredLength < textRange.End) { var currentBreakPosition = 0; var lineBreaker = new LineBreakEnumerator(currentRun.Text); while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) { var nextBreakPosition = lineBreaker.Current.PositionMeasure; if (nextBreakPosition == 0) { break; } if (nextBreakPosition >= measuredLength) { break; } currentBreakPosition = nextBreakPosition; } measuredLength = currentBreakPosition; } } collapsedLength += measuredLength; var shapedTextCharacters = new List <ShapedTextCharacters>(shapedTextRuns.Count); if (collapsedLength > 0) { var splitResult = TextFormatterImpl.SplitShapedRuns(shapedTextRuns, collapsedLength); shapedTextCharacters.AddRange(splitResult.First); TextLineImpl.SortRuns(shapedTextCharacters); } shapedTextCharacters.Add(shapedSymbol); return(shapedTextCharacters); } availableWidth -= currentRun.Size.Width; collapsedLength += currentRun.GlyphRun.Characters.Length; runIndex++; } return(null); }
/// <inheritdoc/> public override TextLine Collapse(params TextCollapsingProperties[] collapsingPropertiesList) { if (collapsingPropertiesList == null || collapsingPropertiesList.Length == 0) { return(this); } var collapsingProperties = collapsingPropertiesList[0]; var runIndex = 0; var currentWidth = 0.0; var textRange = TextRange; var collapsedLength = 0; TextLineMetrics textLineMetrics; var shapedSymbol = CreateShapedSymbol(collapsingProperties.Symbol); var availableWidth = collapsingProperties.Width - shapedSymbol.Size.Width; while (runIndex < _textRuns.Count) { var currentRun = _textRuns[runIndex]; currentWidth += currentRun.Size.Width; if (currentWidth > availableWidth) { if (TextFormatterImpl.TryMeasureCharacters(currentRun, availableWidth, out var measuredLength)) { if (collapsingProperties.Style == TextCollapsingStyle.TrailingWord && measuredLength < textRange.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; } } collapsedLength += measuredLength; var splitResult = TextFormatterImpl.SplitTextRuns(_textRuns, collapsedLength); var shapedTextCharacters = new List <ShapedTextCharacters>(splitResult.First.Count + 1); shapedTextCharacters.AddRange(splitResult.First); shapedTextCharacters.Add(shapedSymbol); textRange = new TextRange(textRange.Start, collapsedLength); var shapedWidth = GetShapedWidth(shapedTextCharacters); textLineMetrics = new TextLineMetrics(new Size(shapedWidth, LineMetrics.Size.Height), LineMetrics.TextBaseline, textRange, false); return(new TextLineImpl(shapedTextCharacters, textLineMetrics, TextLineBreak, true)); } availableWidth -= currentRun.Size.Width; collapsedLength += currentRun.GlyphRun.Characters.Length; runIndex++; } textLineMetrics = new TextLineMetrics(LineMetrics.Size.WithWidth(LineMetrics.Size.Width + shapedSymbol.Size.Width), LineMetrics.TextBaseline, TextRange, LineMetrics.HasOverflowed); return(new TextLineImpl(new List <ShapedTextCharacters>(_textRuns) { shapedSymbol }, textLineMetrics, null, true)); }
/// <summary> /// Performs text wrapping returns a list of text lines. /// </summary> /// <param name="textRuns"></param> /// <param name="firstTextSourceIndex">The first text source index.</param> /// <param name="paragraphWidth">The paragraph width.</param> /// <param name="paragraphProperties">The text paragraph properties.</param> /// <param name="resolvedFlowDirection"></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 <DrawableTextRun> textRuns, int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection, TextLineBreak?currentLineBreak) { if (textRuns.Count == 0) { return(CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties)); } if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength)) { measuredLength = 1; } 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 = SplitDrawableRuns(textRuns, measuredLength); var remainingCharacters = splitResult.Second; var lineBreak = remainingCharacters?.Count > 0 ? new TextLineBreak(currentLineBreak?.TextEndOfLine, resolvedFlowDirection, remainingCharacters) : null; if (lineBreak is null && currentLineBreak?.TextEndOfLine != null) { lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection); } var textLine = new TextLineImpl(splitResult.First, firstTextSourceIndex, measuredLength, paragraphWidth, paragraphProperties, resolvedFlowDirection, lineBreak); return(textLine.FinalizeLine()); }
public override void Justify(TextLine textLine) { var paragraphWidth = Width; if (double.IsInfinity(paragraphWidth)) { return; } if (textLine.NewLineLength > 0) { return; } var textLineBreak = textLine.TextLineBreak; if (textLineBreak is not null && textLineBreak.TextEndOfLine is not null) { if (textLineBreak.RemainingRuns is null || textLineBreak.RemainingRuns.Count == 0) { return; } } var breakOportunities = new Queue <int>(); foreach (var textRun in textLine.TextRuns) { var text = textRun.Text; if (text.IsEmpty) { continue; } var start = text.Start; var lineBreakEnumerator = new LineBreakEnumerator(text); while (lineBreakEnumerator.MoveNext()) { var currentBreak = lineBreakEnumerator.Current; if (!currentBreak.Required && currentBreak.PositionWrap != text.Length) { breakOportunities.Enqueue(start + currentBreak.PositionMeasure); } } } if (breakOportunities.Count == 0) { return; } var remainingSpace = Math.Max(0, paragraphWidth - textLine.WidthIncludingTrailingWhitespace); var spacing = remainingSpace / breakOportunities.Count; foreach (var textRun in textLine.TextRuns) { var text = textRun.Text; if (text.IsEmpty) { continue; } if (textRun is ShapedTextCharacters shapedText) { var glyphRun = shapedText.GlyphRun; var shapedBuffer = shapedText.ShapedBuffer; var currentPosition = text.Start; while (breakOportunities.Count > 0) { var characterIndex = breakOportunities.Dequeue(); if (characterIndex < currentPosition) { continue; } var glyphIndex = glyphRun.FindGlyphIndex(characterIndex); var glyphInfo = shapedBuffer.GlyphInfos[glyphIndex]; shapedBuffer.GlyphInfos[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex, glyphInfo.GlyphCluster, glyphInfo.GlyphAdvance + spacing); } glyphRun.GlyphAdvances = shapedBuffer.GlyphAdvances; } } }
/// <summary> /// Performs text trimming and returns a trimmed line. /// </summary> /// <param name="textRuns">The text runs to perform the trimming on.</param> /// <param name="textRange">The text range that is covered by the text runs.</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></returns> private static TextLine PerformTextTrimming(IReadOnlyList <ShapedTextCharacters> textRuns, TextRange textRange, 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.Properties); var measuredLength = MeasureText(currentRun, availableWidth - ellipsisRun.GlyphRun.Bounds.Width); if (textTrimming == TextTrimming.WordEllipsis) { if (measuredLength < textRange.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 <ShapedTextCharacters>(splitResult.First.Count + 1); trimmedRuns.AddRange(splitResult.First); trimmedRuns.Add(ellipsisRun); var textLineMetrics = TextLineMetrics.Create(trimmedRuns, textRange, paragraphWidth, paragraphProperties); return(new TextLineImpl(trimmedRuns, textLineMetrics)); } availableWidth -= currentRun.GlyphRun.Bounds.Width; runIndex++; } return(new TextLineImpl(textRuns, TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties))); }