Browse Source

Introduce TextLine.Justify for custom justification

pull/8347/head
Benedikt Stebner 4 years ago
parent
commit
6ab5774b80
  1. 109
      src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
  2. 16
      src/Avalonia.Base/Media/TextFormatting/JustificationProperties.cs
  3. 29
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  4. 17
      src/Avalonia.Base/Media/TextFormatting/TextLine.cs
  5. 118
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

109
src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs

@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using Avalonia.Media.TextFormatting.Unicode;
namespace Avalonia.Media.TextFormatting
{
internal class InterWordJustification : JustificationProperties
{
public InterWordJustification(double width)
{
Width = width;
}
public override double Width { get; }
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;
}
}
}
}
}

16
src/Avalonia.Base/Media/TextFormatting/JustificationProperties.cs

@ -0,0 +1,16 @@
namespace Avalonia.Media.TextFormatting
{
public abstract class JustificationProperties
{
/// <summary>
/// Gets the width in which the range is justified.
/// </summary>
public abstract double Width { get; }
/// <summary>
/// Justifies given text line.
/// </summary>
/// <param name="textLine">Text line to collapse.</param>
public abstract void Justify(TextLine textLine);
}
}

29
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@ -501,6 +501,35 @@ namespace Avalonia.Media.TextFormatting
Bounds = new Rect(left, 0, width, height);
if(_paragraphProperties.TextAlignment == TextAlignment.Justify)
{
var whitespaceWidth = 0d;
foreach (var line in textLines)
{
var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace;
if(lineWhitespaceWidth > whitespaceWidth)
{
whitespaceWidth = lineWhitespaceWidth;
}
}
var justificationWidth = width - whitespaceWidth;
if(justificationWidth > 0)
{
var justificationProperties = new InterWordJustification(justificationWidth);
for (var i = 0; i < textLines.Count - 1; i++)
{
var line = textLines[i];
line.Justify(justificationProperties);
}
}
}
return textLines;
}

17
src/Avalonia.Base/Media/TextFormatting/TextLine.cs

@ -16,8 +16,14 @@ namespace Avalonia.Media.TextFormatting
/// </value>
public abstract IReadOnlyList<TextRun> TextRuns { get; }
/// <summary>
/// Gets the first TextSource position of the current line.
/// </summary>
public abstract int FirstTextSourceIndex { get; }
/// <summary>
/// Gets the total number of TextSource positions of the current line.
/// </summary>
public abstract int Length { get; }
/// <summary>
@ -56,7 +62,7 @@ namespace Avalonia.Media.TextFormatting
/// Gets a value that indicates whether content of the line overflows the specified paragraph width.
/// </summary>
/// <returns>
/// <c>true</c>, it the line overflows the specified paragraph width; otherwise, <c>false</c>.
/// <c>true</c>, the line overflows the specified paragraph width; otherwise, <c>false</c>.
/// </returns>
public abstract bool HasOverflowed { get; }
@ -149,6 +155,15 @@ namespace Avalonia.Media.TextFormatting
/// </returns>
public abstract TextLine Collapse(params TextCollapsingProperties[] collapsingPropertiesList);
/// <summary>
/// Create a justified line based on justification text properties.
/// </summary>
/// <param name="justificationProperties">An object that represent the justification text properties.</param>
/// <returns>
/// A <see cref="TextLine"/> value that represents a justified line that can be displayed.
/// </returns>
public abstract void Justify(JustificationProperties justificationProperties);
/// <summary>
/// Gets the character hit corresponding to the specified distance from the beginning of the line.
/// </summary>

118
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
@ -145,7 +144,14 @@ namespace Avalonia.Media.TextFormatting
}
return collapsedLine;
}
/// <inheritdoc/>
public override void Justify(JustificationProperties justificationProperties)
{
justificationProperties.Justify(this);
_textLineMetrics = CreateLineMetrics();
}
/// <inheritdoc/>
@ -709,121 +715,11 @@ namespace Avalonia.Media.TextFormatting
{
_textLineMetrics = CreateLineMetrics();
Justify();
BidiReorder();
return this;
}
private void Justify()
{
if (_paragraphProperties.TextAlignment != TextAlignment.Justify)
{
return;
}
var paragraphWidth = _paragraphWidth;
if (double.IsInfinity(paragraphWidth))
{
return;
}
if(_textLineMetrics.NewLineLength > 0)
{
return;
}
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 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 - WidthIncludingTrailingWhitespace);
var spacing = remainingSpace / breakOportunities.Count;
foreach (var textRun in 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;
}
}
var trailingWhitespaceWidth = _textLineMetrics.WidthIncludingTrailingWhitespace - _textLineMetrics.Width;
_textLineMetrics = new TextLineMetrics(
_textLineMetrics.HasOverflowed,
_textLineMetrics.Height,
_textLineMetrics.NewLineLength,
_textLineMetrics.Start,
_textLineMetrics.TextBaseline,
_textLineMetrics.TrailingWhitespaceLength,
paragraphWidth - trailingWhitespaceWidth,
paragraphWidth);
}
private static sbyte GetRunBidiLevel(DrawableTextRun run, FlowDirection flowDirection)
{
if (run is ShapedTextCharacters shapedTextCharacters)

Loading…
Cancel
Save