csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1626 lines
60 KiB
1626 lines
60 KiB
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Media.TextFormatting;
|
|
using Avalonia.Utilities;
|
|
|
|
namespace Avalonia.Media
|
|
{
|
|
/// <summary>
|
|
/// The FormattedText class is targeted at programmers needing to add some simple text to a MIL visual.
|
|
/// </summary>
|
|
public class FormattedText
|
|
{
|
|
public const double DefaultRealToIdeal = 28800.0 / 96;
|
|
public const double DefaultIdealToReal = 1 / DefaultRealToIdeal;
|
|
public const int IdealInfiniteWidth = 0x3FFFFFFE;
|
|
public const double RealInfiniteWidth = IdealInfiniteWidth * DefaultIdealToReal;
|
|
|
|
public const double GreatestMultiplierOfEm = 100;
|
|
|
|
private const double MaxFontEmSize = RealInfiniteWidth / GreatestMultiplierOfEm;
|
|
|
|
// properties and format runs
|
|
private ReadOnlySlice<char> _text;
|
|
private readonly SpanVector _formatRuns = new SpanVector(null);
|
|
private SpanPosition _latestPosition;
|
|
|
|
private GenericTextParagraphProperties _defaultParaProps;
|
|
|
|
private double _maxTextWidth = double.PositiveInfinity;
|
|
private double[]? _maxTextWidths;
|
|
private double _maxTextHeight = double.PositiveInfinity;
|
|
private int _maxLineCount = int.MaxValue;
|
|
private TextTrimming _trimming = TextTrimming.WordEllipsis;
|
|
|
|
// text source callbacks
|
|
private TextSourceImplementation? _textSourceImpl;
|
|
|
|
// cached metrics
|
|
private CachedMetrics? _metrics;
|
|
|
|
/// <summary>
|
|
/// Construct a FormattedText object.
|
|
/// </summary>
|
|
/// <param name="textToFormat">String of text to be displayed.</param>
|
|
/// <param name="culture">Culture of text.</param>
|
|
/// <param name="flowDirection">Flow direction of text.</param>
|
|
/// <param name="typeface">Type face used to display text.</param>
|
|
/// <param name="emSize">Font em size in visual units (1/96 of an inch).</param>
|
|
/// <param name="foreground">Foreground brush used to render text.</param>
|
|
public FormattedText(
|
|
string textToFormat,
|
|
CultureInfo culture,
|
|
FlowDirection flowDirection,
|
|
Typeface typeface,
|
|
double emSize,
|
|
IBrush? foreground)
|
|
{
|
|
if (culture is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(culture));
|
|
}
|
|
|
|
ValidateFlowDirection(flowDirection, nameof(flowDirection));
|
|
|
|
ValidateFontSize(emSize);
|
|
|
|
_text = textToFormat != null ?
|
|
new ReadOnlySlice<char>(textToFormat.AsMemory()) :
|
|
throw new ArgumentNullException(nameof(textToFormat));
|
|
|
|
var runProps = new GenericTextRunProperties(
|
|
typeface,
|
|
emSize,
|
|
null, // decorations
|
|
foreground,
|
|
null, // highlight background
|
|
BaselineAlignment.Baseline,
|
|
culture
|
|
);
|
|
|
|
_latestPosition = _formatRuns.SetValue(0, _text.Length, runProps, _latestPosition);
|
|
|
|
_defaultParaProps = new GenericTextParagraphProperties(
|
|
flowDirection,
|
|
TextAlignment.Left,
|
|
false,
|
|
false,
|
|
runProps,
|
|
TextWrapping.WrapWithOverflow,
|
|
0, // line height not specified
|
|
0, // indentation not specified
|
|
0
|
|
);
|
|
|
|
InvalidateMetrics();
|
|
}
|
|
|
|
private static void ValidateFontSize(double emSize)
|
|
{
|
|
if (emSize <= 0)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(emSize), "The parameter value must be greater than zero.");
|
|
}
|
|
|
|
if (emSize > MaxFontEmSize)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(emSize), $"The parameter value cannot be greater than '{MaxFontEmSize}'");
|
|
}
|
|
|
|
if (double.IsNaN(emSize))
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(emSize), "The parameter value must be a number.");
|
|
}
|
|
}
|
|
|
|
private static void ValidateFlowDirection(FlowDirection flowDirection, string parameterName)
|
|
{
|
|
if ((int)flowDirection < 0 || (int)flowDirection > (int)FlowDirection.RightToLeft)
|
|
{
|
|
throw new InvalidEnumArgumentException(parameterName, (int)flowDirection, typeof(FlowDirection));
|
|
}
|
|
}
|
|
|
|
private int ValidateRange(int startIndex, int count)
|
|
{
|
|
if (startIndex < 0 || startIndex > _text.Length)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(startIndex));
|
|
}
|
|
|
|
var limit = startIndex + count;
|
|
|
|
if (count < 0 || limit < startIndex || limit > _text.Length)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(count));
|
|
}
|
|
|
|
return limit;
|
|
}
|
|
|
|
private void InvalidateMetrics()
|
|
{
|
|
_metrics = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets foreground brush used for drawing text
|
|
/// </summary>
|
|
/// <param name="foregroundBrush">Foreground brush</param>
|
|
public void SetForegroundBrush(IBrush foregroundBrush)
|
|
{
|
|
SetForegroundBrush(foregroundBrush, 0, _text.Length);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets foreground brush used for drawing text
|
|
/// </summary>
|
|
/// <param name="foregroundBrush">Foreground brush</param>
|
|
/// <param name="startIndex">The start index of initial character to apply the change to.</param>
|
|
/// <param name="count">The number of characters the change should be applied to.</param>
|
|
public void SetForegroundBrush(IBrush? foregroundBrush, int startIndex, int count)
|
|
{
|
|
var limit = ValidateRange(startIndex, count);
|
|
for (var i = startIndex; i < limit;)
|
|
{
|
|
var formatRider = new SpanRider(_formatRuns, _latestPosition, i);
|
|
i = Math.Min(limit, i + formatRider.Length);
|
|
|
|
#pragma warning disable 6506
|
|
// Presharp warns that runProps is not validated, but it can never be null
|
|
// because the rider is already checked to be in range
|
|
|
|
if (!(formatRider.CurrentElement is GenericTextRunProperties runProps))
|
|
{
|
|
throw new NotSupportedException($"{nameof(runProps)} can not be null.");
|
|
}
|
|
|
|
if (runProps.ForegroundBrush == foregroundBrush)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var newProps = new GenericTextRunProperties(
|
|
runProps.Typeface,
|
|
runProps.FontRenderingEmSize,
|
|
runProps.TextDecorations,
|
|
foregroundBrush,
|
|
runProps.BackgroundBrush,
|
|
runProps.BaselineAlignment,
|
|
runProps.CultureInfo
|
|
);
|
|
|
|
#pragma warning restore 6506
|
|
_latestPosition = _formatRuns.SetValue(formatRider.CurrentPosition, i - formatRider.CurrentPosition,
|
|
newProps, formatRider.SpanPosition);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets or changes the font family for the text object
|
|
/// </summary>
|
|
/// <param name="fontFamily">Font family name</param>
|
|
public void SetFontFamily(string fontFamily)
|
|
{
|
|
SetFontFamily(fontFamily, 0, _text.Length);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets or changes the font family for the text object
|
|
/// </summary>
|
|
/// <param name="fontFamily">Font family name</param>
|
|
/// <param name="startIndex">The start index of initial character to apply the change to.</param>
|
|
/// <param name="count">The number of characters the change should be applied to.</param>
|
|
public void SetFontFamily(string fontFamily, int startIndex, int count)
|
|
{
|
|
if (fontFamily == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(fontFamily));
|
|
}
|
|
|
|
SetFontFamily(new FontFamily(fontFamily), startIndex, count);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets or changes the font family for the text object
|
|
/// </summary>
|
|
/// <param name="fontFamily">Font family</param>
|
|
public void SetFontFamily(FontFamily fontFamily)
|
|
{
|
|
SetFontFamily(fontFamily, 0, _text.Length);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets or changes the font family for the text object
|
|
/// </summary>
|
|
/// <param name="fontFamily">Font family</param>
|
|
/// <param name="startIndex">The start index of initial character to apply the change to.</param>
|
|
/// <param name="count">The number of characters the change should be applied to.</param>
|
|
public void SetFontFamily(FontFamily fontFamily, int startIndex, int count)
|
|
{
|
|
if (fontFamily == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(fontFamily));
|
|
}
|
|
|
|
var limit = ValidateRange(startIndex, count);
|
|
|
|
for (var i = startIndex; i < limit;)
|
|
{
|
|
var formatRider = new SpanRider(_formatRuns, _latestPosition, i);
|
|
|
|
i = Math.Min(limit, i + formatRider.Length);
|
|
|
|
#pragma warning disable 6506
|
|
// Presharp warns that runProps is not validated, but it can never be null
|
|
// because the rider is already checked to be in range
|
|
|
|
if (!(formatRider.CurrentElement is GenericTextRunProperties runProps))
|
|
{
|
|
throw new NotSupportedException($"{nameof(runProps)} can not be null.");
|
|
}
|
|
|
|
var oldTypeface = runProps.Typeface;
|
|
|
|
if (fontFamily.Equals(oldTypeface.FontFamily))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var newProps = new GenericTextRunProperties(
|
|
new Typeface(fontFamily, oldTypeface.Style, oldTypeface.Weight),
|
|
runProps.FontRenderingEmSize,
|
|
runProps.TextDecorations,
|
|
runProps.ForegroundBrush,
|
|
runProps.BackgroundBrush,
|
|
runProps.BaselineAlignment,
|
|
runProps.CultureInfo
|
|
);
|
|
|
|
#pragma warning restore 6506
|
|
_latestPosition = _formatRuns.SetValue(formatRider.CurrentPosition, i - formatRider.CurrentPosition,
|
|
newProps, formatRider.SpanPosition);
|
|
|
|
InvalidateMetrics();
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Sets or changes the font em size measured in MIL units
|
|
/// </summary>
|
|
/// <param name="emSize">Font em size</param>
|
|
public void SetFontSize(double emSize)
|
|
{
|
|
SetFontSize(emSize, 0, _text.Length);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets or changes the font em size measured in MIL units
|
|
/// </summary>
|
|
/// <param name="emSize">Font em size</param>
|
|
/// <param name="startIndex">The start index of initial character to apply the change to.</param>
|
|
/// <param name="count">The number of characters the change should be applied to.</param>
|
|
public void SetFontSize(double emSize, int startIndex, int count)
|
|
{
|
|
ValidateFontSize(emSize);
|
|
|
|
var limit = ValidateRange(startIndex, count);
|
|
for (var i = startIndex; i < limit;)
|
|
{
|
|
var formatRider = new SpanRider(_formatRuns, _latestPosition, i);
|
|
|
|
i = Math.Min(limit, i + formatRider.Length);
|
|
|
|
#pragma warning disable 6506
|
|
// Presharp warns that runProps is not validated, but it can never be null
|
|
// because the rider is already checked to be in range
|
|
|
|
if (!(formatRider.CurrentElement is GenericTextRunProperties runProps))
|
|
{
|
|
throw new NotSupportedException($"{nameof(runProps)} can not be null.");
|
|
}
|
|
|
|
if (runProps.FontRenderingEmSize == emSize)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var newProps = new GenericTextRunProperties(
|
|
runProps.Typeface,
|
|
emSize,
|
|
runProps.TextDecorations,
|
|
runProps.ForegroundBrush,
|
|
runProps.BackgroundBrush,
|
|
runProps.BaselineAlignment,
|
|
runProps.CultureInfo
|
|
);
|
|
|
|
_latestPosition = _formatRuns.SetValue(formatRider.CurrentPosition, i - formatRider.CurrentPosition,
|
|
newProps, formatRider.SpanPosition);
|
|
|
|
#pragma warning restore 6506
|
|
InvalidateMetrics();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets or changes the culture for the text object.
|
|
/// </summary>
|
|
/// <param name="culture">The new culture for the text object.</param>
|
|
public void SetCulture(CultureInfo culture)
|
|
{
|
|
SetCulture(culture, 0, _text.Length);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets or changes the culture for the text object.
|
|
/// </summary>
|
|
/// <param name="culture">The new culture for the text object.</param>
|
|
/// <param name="startIndex">The start index of initial character to apply the change to.</param>
|
|
/// <param name="count">The number of characters the change should be applied to.</param>
|
|
public void SetCulture(CultureInfo culture, int startIndex, int count)
|
|
{
|
|
if (culture is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(culture));
|
|
}
|
|
|
|
var limit = ValidateRange(startIndex, count);
|
|
|
|
for (var i = startIndex; i < limit;)
|
|
{
|
|
var formatRider = new SpanRider(_formatRuns, _latestPosition, i);
|
|
|
|
i = Math.Min(limit, i + formatRider.Length);
|
|
|
|
#pragma warning disable 6506
|
|
// Presharp warns that runProps is not validated, but it can never be null
|
|
// because the rider is already checked to be in range
|
|
|
|
if (!(formatRider.CurrentElement is GenericTextRunProperties runProps))
|
|
{
|
|
throw new NotSupportedException($"{nameof(runProps)} can not be null.");
|
|
}
|
|
|
|
if (runProps.CultureInfo == culture)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var newProps = new GenericTextRunProperties(
|
|
runProps.Typeface,
|
|
runProps.FontRenderingEmSize,
|
|
runProps.TextDecorations,
|
|
runProps.ForegroundBrush,
|
|
runProps.BackgroundBrush,
|
|
runProps.BaselineAlignment,
|
|
culture
|
|
);
|
|
|
|
#pragma warning restore 6506
|
|
_latestPosition = _formatRuns.SetValue(formatRider.CurrentPosition, i - formatRider.CurrentPosition,
|
|
newProps, formatRider.SpanPosition);
|
|
|
|
InvalidateMetrics();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets or changes the font weight
|
|
/// </summary>
|
|
/// <param name="weight">Font weight</param>
|
|
public void SetFontWeight(FontWeight weight)
|
|
{
|
|
SetFontWeight(weight, 0, _text.Length);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets or changes the font weight
|
|
/// </summary>
|
|
/// <param name="weight">Font weight</param>
|
|
/// <param name="startIndex">The start index of initial character to apply the change to.</param>
|
|
/// <param name="count">The number of characters the change should be applied to.</param>
|
|
public void SetFontWeight(FontWeight weight, int startIndex, int count)
|
|
{
|
|
var limit = ValidateRange(startIndex, count);
|
|
|
|
for (var i = startIndex; i < limit;)
|
|
{
|
|
var formatRider = new SpanRider(_formatRuns, _latestPosition, i);
|
|
|
|
i = Math.Min(limit, i + formatRider.Length);
|
|
|
|
#pragma warning disable 6506
|
|
// Presharp warns that runProps is not validated, but it can never be null
|
|
// because the rider is already checked to be in range
|
|
|
|
if (!(formatRider.CurrentElement is GenericTextRunProperties runProps))
|
|
{
|
|
throw new NotSupportedException($"{nameof(runProps)} can not be null.");
|
|
}
|
|
|
|
var oldTypeface = runProps.Typeface;
|
|
|
|
if (oldTypeface.Weight == weight)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var newProps = new GenericTextRunProperties(
|
|
new Typeface(oldTypeface.FontFamily, oldTypeface.Style, weight),
|
|
runProps.FontRenderingEmSize,
|
|
runProps.TextDecorations,
|
|
runProps.ForegroundBrush,
|
|
runProps.BackgroundBrush,
|
|
runProps.BaselineAlignment,
|
|
runProps.CultureInfo
|
|
);
|
|
#pragma warning restore 6506
|
|
_latestPosition = _formatRuns.SetValue(formatRider.CurrentPosition, i - formatRider.CurrentPosition, newProps, formatRider.SpanPosition);
|
|
|
|
InvalidateMetrics();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets or changes the font style
|
|
/// </summary>
|
|
/// <param name="style">Font style</param>
|
|
public void SetFontStyle(FontStyle style)
|
|
{
|
|
SetFontStyle(style, 0, _text.Length);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets or changes the font style
|
|
/// </summary>
|
|
/// <param name="style">Font style</param>
|
|
/// <param name="startIndex">The start index of initial character to apply the change to.</param>
|
|
/// <param name="count">The number of characters the change should be applied to.</param>
|
|
public void SetFontStyle(FontStyle style, int startIndex, int count)
|
|
{
|
|
var limit = ValidateRange(startIndex, count);
|
|
for (var i = startIndex; i < limit;)
|
|
{
|
|
var formatRider = new SpanRider(_formatRuns, _latestPosition, i);
|
|
|
|
i = Math.Min(limit, i + formatRider.Length);
|
|
|
|
#pragma warning disable 6506
|
|
// Presharp warns that runProps is not validated, but it can never be null
|
|
// because the rider is already checked to be in range
|
|
|
|
if (!(formatRider.CurrentElement is GenericTextRunProperties runProps))
|
|
{
|
|
throw new NotSupportedException($"{nameof(runProps)} can not be null.");
|
|
}
|
|
|
|
var oldTypeface = runProps.Typeface;
|
|
|
|
if (oldTypeface.Style == style)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var newProps = new GenericTextRunProperties(
|
|
new Typeface(oldTypeface.FontFamily, style, oldTypeface.Weight),
|
|
runProps.FontRenderingEmSize,
|
|
runProps.TextDecorations,
|
|
runProps.ForegroundBrush,
|
|
runProps.BackgroundBrush,
|
|
runProps.BaselineAlignment,
|
|
runProps.CultureInfo
|
|
);
|
|
#pragma warning restore 6506
|
|
|
|
_latestPosition = _formatRuns.SetValue(formatRider.CurrentPosition, i - formatRider.CurrentPosition, newProps, formatRider.SpanPosition);
|
|
|
|
InvalidateMetrics(); // invalidate cached metrics
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets or changes the type face
|
|
/// </summary>
|
|
/// <param name="typeface">Typeface</param>
|
|
public void SetFontTypeface(Typeface typeface)
|
|
{
|
|
SetFontTypeface(typeface, 0, _text.Length);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets or changes the type face
|
|
/// </summary>
|
|
/// <param name="typeface">Typeface</param>
|
|
/// <param name="startIndex">The start index of initial character to apply the change to.</param>
|
|
/// <param name="count">The number of characters the change should be applied to.</param>
|
|
public void SetFontTypeface(Typeface typeface, int startIndex, int count)
|
|
{
|
|
var limit = ValidateRange(startIndex, count);
|
|
|
|
for (var i = startIndex; i < limit;)
|
|
{
|
|
var formatRider = new SpanRider(_formatRuns, _latestPosition, i);
|
|
|
|
i = Math.Min(limit, i + formatRider.Length);
|
|
|
|
#pragma warning disable 6506
|
|
// Presharp warns that runProps is not validated, but it can never be null
|
|
// because the rider is already checked to be in range
|
|
|
|
if (!(formatRider.CurrentElement is GenericTextRunProperties runProps))
|
|
{
|
|
throw new NotSupportedException($"{nameof(runProps)} can not be null.");
|
|
}
|
|
|
|
if (runProps.Typeface == typeface)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var newProps = new GenericTextRunProperties(
|
|
typeface,
|
|
runProps.FontRenderingEmSize,
|
|
runProps.TextDecorations,
|
|
runProps.ForegroundBrush,
|
|
runProps.BackgroundBrush,
|
|
runProps.BaselineAlignment,
|
|
runProps.CultureInfo
|
|
);
|
|
#pragma warning restore 6506
|
|
|
|
_latestPosition = _formatRuns.SetValue(formatRider.CurrentPosition, i - formatRider.CurrentPosition,
|
|
newProps, formatRider.SpanPosition);
|
|
|
|
InvalidateMetrics();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets or changes the text decorations
|
|
/// </summary>
|
|
/// <param name="textDecorations">Text decorations</param>
|
|
public void SetTextDecorations(TextDecorationCollection textDecorations)
|
|
{
|
|
SetTextDecorations(textDecorations, 0, _text.Length);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets or changes the text decorations
|
|
/// </summary>
|
|
/// <param name="textDecorations">Text decorations</param>
|
|
/// <param name="startIndex">The start index of initial character to apply the change to.</param>
|
|
/// <param name="count">The number of characters the change should be applied to.</param>
|
|
public void SetTextDecorations(TextDecorationCollection textDecorations, int startIndex, int count)
|
|
{
|
|
var limit = ValidateRange(startIndex, count);
|
|
|
|
for (var i = startIndex; i < limit;)
|
|
{
|
|
var formatRider = new SpanRider(_formatRuns, _latestPosition, i);
|
|
|
|
i = Math.Min(limit, i + formatRider.Length);
|
|
|
|
#pragma warning disable 6506
|
|
// Presharp warns that runProps is not validated, but it can never be null
|
|
// because the rider is already checked to be in range
|
|
|
|
if (!(formatRider.CurrentElement is GenericTextRunProperties runProps))
|
|
{
|
|
throw new NotSupportedException($"{nameof(runProps)} can not be null.");
|
|
}
|
|
|
|
if (runProps.TextDecorations == textDecorations)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var newProps = new GenericTextRunProperties(
|
|
runProps.Typeface,
|
|
runProps.FontRenderingEmSize,
|
|
textDecorations,
|
|
runProps.ForegroundBrush,
|
|
runProps.BackgroundBrush,
|
|
runProps.BaselineAlignment,
|
|
runProps.CultureInfo
|
|
);
|
|
#pragma warning restore 6506
|
|
|
|
_latestPosition = _formatRuns.SetValue(formatRider.CurrentPosition, i - formatRider.CurrentPosition,
|
|
newProps, formatRider.SpanPosition);
|
|
}
|
|
}
|
|
|
|
/// Note: enumeration is temporarily made private
|
|
/// because of PS #828532
|
|
///
|
|
/// <summary>
|
|
/// Strongly typed enumerator used for enumerating text lines
|
|
/// </summary>
|
|
private struct LineEnumerator : IEnumerator, IDisposable
|
|
{
|
|
private int _lineCount;
|
|
private double _totalHeight;
|
|
private TextLine? _nextLine;
|
|
private readonly TextFormatter _formatter;
|
|
private readonly FormattedText _that;
|
|
private readonly ITextSource _textSource;
|
|
|
|
// these are needed because _currentLine can be disposed before the next MoveNext() call
|
|
private double _previousHeight;
|
|
|
|
// line break before _currentLine, needed in case we have to reformat it with collapsing symbol
|
|
private TextLineBreak? _previousLineBreak;
|
|
private int _position;
|
|
private int _length;
|
|
|
|
internal LineEnumerator(FormattedText text)
|
|
{
|
|
_previousHeight = 0;
|
|
_length = 0;
|
|
_previousLineBreak = null;
|
|
|
|
_position = 0;
|
|
_lineCount = 0;
|
|
_totalHeight = 0;
|
|
Current = null;
|
|
_nextLine = null;
|
|
_formatter = TextFormatter.Current;
|
|
_that = text;
|
|
_textSource = _that._textSourceImpl ??= new TextSourceImplementation(_that);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Current = null;
|
|
|
|
_nextLine = null;
|
|
}
|
|
|
|
public int Position
|
|
{
|
|
get => _position;
|
|
private set => _position = value;
|
|
}
|
|
|
|
public int Length
|
|
{
|
|
get => _length;
|
|
private set => _length = value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the current text line in the collection
|
|
/// </summary>
|
|
public TextLine? Current { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets the current text line in the collection
|
|
/// </summary>
|
|
object? IEnumerator.Current => Current;
|
|
|
|
/// <summary>
|
|
/// Gets the paragraph width used to format the current text line
|
|
/// </summary>
|
|
internal double CurrentParagraphWidth
|
|
{
|
|
get
|
|
{
|
|
return MaxLineLength(_lineCount);
|
|
}
|
|
}
|
|
|
|
private double MaxLineLength(int line)
|
|
{
|
|
if (_that._maxTextWidths == null)
|
|
return _that._maxTextWidth;
|
|
return _that._maxTextWidths[Math.Min(line, _that._maxTextWidths.Length - 1)];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Advances the enumerator to the next text line of the collection
|
|
/// </summary>
|
|
/// <returns>true if the enumerator was successfully advanced to the next element;
|
|
/// false if the enumerator has passed the end of the collection</returns>
|
|
public bool MoveNext()
|
|
{
|
|
if (Current == null)
|
|
{ // this is the first line
|
|
if (_that._text.Length == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Current = FormatLine(
|
|
_textSource,
|
|
Position,
|
|
MaxLineLength(_lineCount),
|
|
_that._defaultParaProps!,
|
|
null // no previous line break
|
|
);
|
|
|
|
// check if this line fits the text height
|
|
if (_totalHeight + Current.Height > _that._maxTextHeight)
|
|
{
|
|
Current = null;
|
|
|
|
return false;
|
|
}
|
|
Debug.Assert(_nextLine == null);
|
|
}
|
|
else
|
|
{
|
|
// there is no next line or it didn't fit
|
|
// either way we're finished
|
|
if (_nextLine == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
_totalHeight += _previousHeight;
|
|
Position += Length;
|
|
++_lineCount;
|
|
|
|
Current = _nextLine;
|
|
_nextLine = null;
|
|
}
|
|
|
|
var currentLineBreak = Current.TextLineBreak;
|
|
|
|
// this line is guaranteed to fit the text height
|
|
Debug.Assert(_totalHeight + Current.Height <= _that._maxTextHeight);
|
|
|
|
// now, check if the next line fits, we need to do this on this iteration
|
|
// because we might need to add ellipsis to the current line
|
|
// as a result of the next line measurement
|
|
|
|
// maybe there is no next line at all
|
|
if (Position + Current.Length < _that._text.Length)
|
|
{
|
|
bool nextLineFits;
|
|
|
|
if (_lineCount + 1 >= _that._maxLineCount)
|
|
{
|
|
nextLineFits = false;
|
|
}
|
|
else
|
|
{
|
|
_nextLine = FormatLine(
|
|
_textSource,
|
|
Position + Current.Length,
|
|
MaxLineLength(_lineCount + 1),
|
|
_that._defaultParaProps,
|
|
currentLineBreak
|
|
);
|
|
|
|
nextLineFits = (_totalHeight + Current.Height + _nextLine.Height <= _that._maxTextHeight);
|
|
}
|
|
|
|
if (!nextLineFits)
|
|
{
|
|
_nextLine = null;
|
|
|
|
if (_that._trimming != TextTrimming.None && !Current.HasCollapsed)
|
|
{
|
|
// recreate the current line with ellipsis added
|
|
// Note: Paragraph ellipsis is not supported today. We'll workaround
|
|
// it here by faking a non-wrap text on finite column width.
|
|
var currentWrap = _that._defaultParaProps!.TextWrapping;
|
|
|
|
_that._defaultParaProps.SetTextWrapping(TextWrapping.NoWrap);
|
|
|
|
Current = FormatLine(
|
|
_that._textSourceImpl!,
|
|
Position,
|
|
MaxLineLength(_lineCount),
|
|
_that._defaultParaProps,
|
|
_previousLineBreak
|
|
);
|
|
|
|
currentLineBreak = Current.TextLineBreak;
|
|
|
|
_that._defaultParaProps.SetTextWrapping(currentWrap);
|
|
}
|
|
}
|
|
}
|
|
|
|
_previousHeight = Current.Height;
|
|
|
|
Length = Current.Length;
|
|
|
|
_previousLineBreak = currentLineBreak;
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Wrapper of TextFormatter.FormatLine that auto-collapses the line if needed.
|
|
/// </summary>
|
|
private TextLine FormatLine(ITextSource textSource, int textSourcePosition, double maxLineLength, TextParagraphProperties paraProps, TextLineBreak? lineBreak)
|
|
{
|
|
var line = _formatter.FormatLine(
|
|
textSource,
|
|
textSourcePosition,
|
|
maxLineLength,
|
|
paraProps,
|
|
lineBreak
|
|
);
|
|
|
|
if (_that._trimming != TextTrimming.None && line.HasOverflowed && line.Length > 0)
|
|
{
|
|
// what I really need here is the last displayed text run of the line
|
|
// textSourcePosition + line.Length - 1 works except the end of paragraph case,
|
|
// where line length includes the fake paragraph break run
|
|
Debug.Assert(_that._text.Length > 0 && textSourcePosition + line.Length <= _that._text.Length + 1);
|
|
|
|
var thatFormatRider = new SpanRider(
|
|
_that._formatRuns,
|
|
_that._latestPosition,
|
|
Math.Min(textSourcePosition + line.Length - 1, _that._text.Length - 1)
|
|
);
|
|
|
|
var lastRunProps = (GenericTextRunProperties)thatFormatRider.CurrentElement!;
|
|
|
|
TextCollapsingProperties collapsingProperties = _that._trimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(maxLineLength, lastRunProps));
|
|
|
|
var collapsedLine = line.Collapse(collapsingProperties);
|
|
|
|
line = collapsedLine;
|
|
}
|
|
return line;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Sets the enumerator to its initial position,
|
|
/// which is before the first element in the collection
|
|
/// </summary>
|
|
public void Reset()
|
|
{
|
|
Position = 0;
|
|
_lineCount = 0;
|
|
_totalHeight = 0;
|
|
Current = null;
|
|
_nextLine = null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns an enumerator that can iterate through the text line collection
|
|
/// </summary>
|
|
private LineEnumerator GetEnumerator()
|
|
{
|
|
return new LineEnumerator(this);
|
|
}
|
|
#if NEVER
|
|
/// <summary>
|
|
/// Returns an enumerator that can iterate through the text line collection
|
|
/// </summary>
|
|
IEnumerator IEnumerable.GetEnumerator()
|
|
{
|
|
return GetEnumerator();
|
|
}
|
|
#endif
|
|
|
|
private void AdvanceLineOrigin(ref Point lineOrigin, TextLine currentLine)
|
|
{
|
|
var height = currentLine.Height;
|
|
|
|
// advance line origin according to the flow direction
|
|
switch (_defaultParaProps.FlowDirection)
|
|
{
|
|
case FlowDirection.LeftToRight:
|
|
case FlowDirection.RightToLeft:
|
|
lineOrigin = lineOrigin.WithY(lineOrigin.Y + height);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private class CachedMetrics
|
|
{
|
|
// vertical
|
|
public double Height;
|
|
public double Baseline;
|
|
|
|
// horizontal
|
|
public double Width;
|
|
public double WidthIncludingTrailingWhitespace;
|
|
|
|
// vertical bounding box metrics
|
|
public double Extent;
|
|
public double OverhangAfter;
|
|
|
|
// horizontal bounding box metrics
|
|
public double OverhangLeading;
|
|
public double OverhangTrailing;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Defines the flow direction
|
|
/// </summary>
|
|
public FlowDirection FlowDirection
|
|
{
|
|
set
|
|
{
|
|
ValidateFlowDirection(value, "value");
|
|
_defaultParaProps.SetFlowDirection(value);
|
|
InvalidateMetrics();
|
|
}
|
|
get
|
|
{
|
|
return _defaultParaProps.FlowDirection;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Defines the alignment of text within the column
|
|
/// </summary>
|
|
public TextAlignment TextAlignment
|
|
{
|
|
set
|
|
{
|
|
_defaultParaProps.SetTextAlignment(value);
|
|
InvalidateMetrics();
|
|
}
|
|
get
|
|
{
|
|
return _defaultParaProps.TextAlignment;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the height of, or the spacing between, each line where
|
|
/// zero represents the default line height.
|
|
/// </summary>
|
|
public double LineHeight
|
|
{
|
|
set
|
|
{
|
|
if (value < 0)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(value), "Parameter must be greater than or equal to zero.");
|
|
}
|
|
|
|
_defaultParaProps.SetLineHeight(value);
|
|
|
|
InvalidateMetrics();
|
|
}
|
|
get
|
|
{
|
|
return _defaultParaProps.LineHeight;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The MaxTextWidth property defines the alignment edges for the FormattedText.
|
|
/// For example, left aligned text is wrapped such that the leftmost glyph alignment point
|
|
/// on each line falls exactly on the left edge of the rectangle.
|
|
/// Note that for many fonts, especially in italic style, some glyph strokes may extend beyond the edges of the alignment rectangle.
|
|
/// For this reason, it is recommended that clients draw text with at least 1/6 em (i.e of the font size) unused margin space either side.
|
|
/// Zero value of MaxTextWidth is equivalent to the maximum possible paragraph width.
|
|
/// </summary>
|
|
public double MaxTextWidth
|
|
{
|
|
set
|
|
{
|
|
if (value < 0)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(value), "Parameter must be greater than or equal to zero.");
|
|
}
|
|
|
|
_maxTextWidth = value;
|
|
|
|
InvalidateMetrics();
|
|
}
|
|
get
|
|
{
|
|
return _maxTextWidth;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the array of lengths,
|
|
/// which will be applied to each line of text in turn.
|
|
/// If the text covers more lines than there are entries in the length array,
|
|
/// the last entry is reused as many times as required.
|
|
/// The maxTextWidths array overrides the MaxTextWidth property.
|
|
/// </summary>
|
|
/// <param name="maxTextWidths">The max text width array</param>
|
|
public void SetMaxTextWidths(double[] maxTextWidths)
|
|
{
|
|
if (maxTextWidths == null || maxTextWidths.Length <= 0)
|
|
{
|
|
throw new ArgumentNullException(nameof(maxTextWidths));
|
|
}
|
|
|
|
_maxTextWidths = maxTextWidths;
|
|
|
|
InvalidateMetrics();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Obtains a copy of the array of lengths,
|
|
/// which will be applied to each line of text in turn.
|
|
/// If the text covers more lines than there are entries in the length array,
|
|
/// the last entry is reused as many times as required.
|
|
/// The maxTextWidths array overrides the MaxTextWidth property.
|
|
/// </summary>
|
|
/// <returns>The copy of max text width array</returns>
|
|
public double[] GetMaxTextWidths()
|
|
{
|
|
return _maxTextWidths != null ? (double[])_maxTextWidths.Clone() : Array.Empty<double>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the maximum length of a column of text.
|
|
/// The last line of text displayed is the last whole line that will fit within this limit,
|
|
/// or the nth line as specified by MaxLineCount, whichever occurs first.
|
|
/// Use the Trimming property to control how the omission of text is indicated.
|
|
/// </summary>
|
|
public double MaxTextHeight
|
|
{
|
|
set
|
|
{
|
|
if (value <= 0)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(value), $"'{nameof(MaxTextHeight)}' property value must be greater than zero.");
|
|
}
|
|
|
|
if (double.IsNaN(value))
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(value), $"'{nameof(MaxTextHeight)}' property value cannot be NaN.");
|
|
}
|
|
|
|
_maxTextHeight = value;
|
|
|
|
InvalidateMetrics();
|
|
}
|
|
get
|
|
{
|
|
return _maxTextHeight;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Defines the maximum number of lines to display.
|
|
/// The last line of text displayed is the lineCount-1'th line,
|
|
/// or the last whole line that will fit within the count set by MaxTextHeight,
|
|
/// whichever occurs first.
|
|
/// Use the Trimming property to control how the omission of text is indicated
|
|
/// </summary>
|
|
public int MaxLineCount
|
|
{
|
|
set
|
|
{
|
|
if (value <= 0)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(value), "The parameter value must be greater than zero.");
|
|
}
|
|
|
|
_maxLineCount = value;
|
|
|
|
InvalidateMetrics();
|
|
}
|
|
get
|
|
{
|
|
return _maxLineCount;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Defines how omission of text is indicated.
|
|
/// CharacterEllipsis trimming allows partial words to be displayed,
|
|
/// while WordEllipsis removes whole words to fit.
|
|
/// Both guarantee to include an ellipsis ('...') at the end of the lines
|
|
/// where text has been trimmed as a result of line and column limits.
|
|
/// </summary>
|
|
public TextTrimming Trimming
|
|
{
|
|
set
|
|
{
|
|
_trimming = value;
|
|
|
|
_defaultParaProps.SetTextWrapping(_trimming == TextTrimming.None ?
|
|
TextWrapping.Wrap :
|
|
TextWrapping.WrapWithOverflow);
|
|
|
|
InvalidateMetrics();
|
|
}
|
|
get
|
|
{
|
|
return _trimming;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lazily initializes the cached metrics EXCEPT for black box metrics and
|
|
/// returns the CachedMetrics structure.
|
|
/// </summary>
|
|
private CachedMetrics Metrics
|
|
{
|
|
get
|
|
{
|
|
return _metrics ??= DrawAndCalculateMetrics(
|
|
null, // drawing context
|
|
new Point(), // drawing offset
|
|
false);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lazily initializes the cached metrics INCLUDING black box metrics and
|
|
/// returns the CachedMetrics structure.
|
|
/// </summary>
|
|
private CachedMetrics BlackBoxMetrics
|
|
{
|
|
get
|
|
{
|
|
if (_metrics == null || double.IsNaN(_metrics.Extent))
|
|
{
|
|
// We need to obtain the metrics, including black box metrics.
|
|
|
|
_metrics = DrawAndCalculateMetrics(
|
|
null, // drawing context
|
|
new Point(), // drawing offset
|
|
true); // calculate black box metrics
|
|
}
|
|
return _metrics;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The distance from the top of the first line to the bottom of the last line.
|
|
/// </summary>
|
|
public double Height
|
|
{
|
|
get
|
|
{
|
|
return Metrics.Height;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The distance from the topmost black pixel of the first line
|
|
/// to the bottommost black pixel of the last line.
|
|
/// </summary>
|
|
public double Extent
|
|
{
|
|
get
|
|
{
|
|
return BlackBoxMetrics.Extent;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The distance from the top of the first line to the baseline of the first line.
|
|
/// </summary>
|
|
public double Baseline
|
|
{
|
|
get
|
|
{
|
|
return Metrics.Baseline;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The distance from the bottom of the last line to the extent bottom.
|
|
/// </summary>
|
|
public double OverhangAfter
|
|
{
|
|
get
|
|
{
|
|
return BlackBoxMetrics.OverhangAfter;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The maximum distance from the leading black pixel to the leading alignment point of a line.
|
|
/// </summary>
|
|
public double OverhangLeading
|
|
{
|
|
get
|
|
{
|
|
return BlackBoxMetrics.OverhangLeading;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The maximum distance from the trailing black pixel to the trailing alignment point of a line.
|
|
/// </summary>
|
|
public double OverhangTrailing
|
|
{
|
|
get
|
|
{
|
|
return BlackBoxMetrics.OverhangTrailing;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The maximum advance width between the leading and trailing alignment points of a line,
|
|
/// excluding the width of whitespace characters at the end of the line.
|
|
/// </summary>
|
|
public double Width
|
|
{
|
|
get
|
|
{
|
|
return Metrics.Width;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The maximum advance width between the leading and trailing alignment points of a line,
|
|
/// including the width of whitespace characters at the end of the line.
|
|
/// </summary>
|
|
public double WidthIncludingTrailingWhitespace
|
|
{
|
|
get
|
|
{
|
|
return Metrics.WidthIncludingTrailingWhitespace;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Obtains geometry for the text, including underlines and strikethroughs.
|
|
/// </summary>
|
|
/// <param name="origin">The left top origin of the resulting geometry.</param>
|
|
/// <returns>The geometry returned contains the combined geometry
|
|
/// of all of the glyphs, underlines and strikeThroughs that represent the formatted text.
|
|
/// Overlapping contours are merged by performing a Boolean union operation.</returns>
|
|
public Geometry? BuildGeometry(Point origin)
|
|
{
|
|
GeometryGroup? accumulatedGeometry = null;
|
|
var lineOrigin = origin;
|
|
|
|
DrawingGroup drawing = new DrawingGroup();
|
|
|
|
using (var ctx = drawing.Open())
|
|
{
|
|
using (var enumerator = GetEnumerator())
|
|
{
|
|
while (enumerator.MoveNext())
|
|
{
|
|
var currentLine = enumerator.Current;
|
|
|
|
if (currentLine != null)
|
|
{
|
|
currentLine.Draw(ctx, lineOrigin);
|
|
|
|
AdvanceLineOrigin(ref lineOrigin, currentLine);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Transform? transform = new TranslateTransform(origin.X, origin.Y);
|
|
|
|
// recursively go down the DrawingGroup to build up the geometry
|
|
CombineGeometryRecursive(drawing, ref transform, ref accumulatedGeometry);
|
|
|
|
return accumulatedGeometry;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds a highlight geometry object.
|
|
/// </summary>
|
|
/// <param name="origin">The origin of the highlight region</param>
|
|
/// <returns>Geometry that surrounds the text.</returns>
|
|
public Geometry? BuildHighlightGeometry(Point origin)
|
|
{
|
|
return BuildHighlightGeometry(origin, 0, _text.Length);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds a highlight geometry object for a given character range.
|
|
/// </summary>
|
|
/// <param name="origin">The origin of the highlight region.</param>
|
|
/// <param name="startIndex">The start index of initial character the bounds should be obtained for.</param>
|
|
/// <param name="count">The number of characters the bounds should be obtained for.</param>
|
|
/// <returns>Geometry that surrounds the specified character range.</returns>
|
|
public Geometry? BuildHighlightGeometry(Point origin, int startIndex, int count)
|
|
{
|
|
ValidateRange(startIndex, count);
|
|
|
|
Geometry? accumulatedBounds = null;
|
|
|
|
using (var enumerator = GetEnumerator())
|
|
{
|
|
var lineOrigin = origin;
|
|
|
|
while (enumerator.MoveNext())
|
|
{
|
|
var currentLine = enumerator.Current!;
|
|
|
|
int x0 = Math.Max(enumerator.Position, startIndex);
|
|
int x1 = Math.Min(enumerator.Position + enumerator.Length, startIndex + count);
|
|
|
|
// check if this line is intersects with the specified character range
|
|
if (x0 < x1)
|
|
{
|
|
var highlightBounds = currentLine.GetTextBounds(x0,x1 - x0);
|
|
|
|
if (highlightBounds != null)
|
|
{
|
|
foreach (var bound in highlightBounds)
|
|
{
|
|
var rect = bound.Rectangle;
|
|
|
|
if (FlowDirection == FlowDirection.RightToLeft)
|
|
{
|
|
// Convert logical units (which extend leftward from the right edge
|
|
// of the paragraph) to physical units.
|
|
//
|
|
// Note that since rect is in logical units, rect.Right corresponds to
|
|
// the visual *left* edge of the rectangle in the RTL case. Specifically,
|
|
// is the distance leftward from the right edge of the formatting rectangle
|
|
// whose width is the paragraph width passed to FormatLine.
|
|
//
|
|
rect = rect.WithX(enumerator.CurrentParagraphWidth - rect.Right);
|
|
}
|
|
|
|
rect = new Rect(new Point(rect.X + lineOrigin.X, rect.Y + lineOrigin.Y), rect.Size);
|
|
|
|
RectangleGeometry rectangleGeometry = new RectangleGeometry(rect);
|
|
|
|
if (accumulatedBounds == null)
|
|
{
|
|
accumulatedBounds = rectangleGeometry;
|
|
}
|
|
else
|
|
{
|
|
accumulatedBounds = Geometry.Combine(accumulatedBounds, rectangleGeometry, GeometryCombineMode.Union);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
AdvanceLineOrigin(ref lineOrigin, currentLine);
|
|
}
|
|
}
|
|
|
|
if (accumulatedBounds?.PlatformImpl == null || accumulatedBounds.PlatformImpl.Bounds.IsEmpty)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return accumulatedBounds;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Draws the text object
|
|
/// </summary>
|
|
internal void Draw(DrawingContext drawingContext, Point origin)
|
|
{
|
|
var lineOrigin = origin;
|
|
|
|
if (_metrics != null && !double.IsNaN(_metrics.Extent))
|
|
{
|
|
// we can't use foreach because it requires GetEnumerator and associated classes to be public
|
|
// foreach (TextLine currentLine in this)
|
|
using (var enumerator = GetEnumerator())
|
|
{
|
|
while (enumerator.MoveNext())
|
|
{
|
|
var currentLine = enumerator.Current!;
|
|
|
|
currentLine.Draw(drawingContext, lineOrigin);
|
|
|
|
AdvanceLineOrigin(ref lineOrigin, currentLine);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Calculate metrics as we draw to avoid formatting again if we need metrics later; we compute
|
|
// black box metrics too because these are already known as a side-effect of drawing
|
|
|
|
_metrics = DrawAndCalculateMetrics(drawingContext, origin, true);
|
|
}
|
|
}
|
|
|
|
private void CombineGeometryRecursive(Drawing drawing, ref Transform? transform, ref GeometryGroup? accumulatedGeometry)
|
|
{
|
|
if (drawing is DrawingGroup group)
|
|
{
|
|
transform = group.Transform;
|
|
|
|
if (group.Children is DrawingCollection children)
|
|
{
|
|
// recursively go down for DrawingGroup
|
|
foreach (var child in children)
|
|
{
|
|
CombineGeometryRecursive(child, ref transform, ref accumulatedGeometry);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (drawing is GlyphRunDrawing glyphRunDrawing)
|
|
{
|
|
// process glyph run
|
|
var glyphRun = glyphRunDrawing.GlyphRun;
|
|
|
|
if (glyphRun != null)
|
|
{
|
|
var glyphRunGeometry = glyphRun.BuildGeometry();
|
|
|
|
glyphRunGeometry.Transform = transform;
|
|
|
|
if (accumulatedGeometry == null)
|
|
{
|
|
accumulatedGeometry = new GeometryGroup
|
|
{
|
|
FillRule = FillRule.NonZero
|
|
};
|
|
}
|
|
|
|
accumulatedGeometry.Children.Add(glyphRunGeometry);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (drawing is GeometryDrawing geometryDrawing)
|
|
{
|
|
// process geometry (i.e. TextDecoration on the line)
|
|
var geometry = geometryDrawing.Geometry;
|
|
|
|
if (geometry != null)
|
|
{
|
|
geometry.Transform = transform;
|
|
|
|
if (geometry is LineGeometry lineGeometry)
|
|
{
|
|
// For TextDecoration drawn by DrawLine(), the geometry is a LineGeometry which has no
|
|
// bounding area. So this line won't show up. Work aroud it by increase the Bounding rect
|
|
// to be Pen's thickness
|
|
|
|
var bounds = lineGeometry.Bounds;
|
|
|
|
if (bounds.Height == 0)
|
|
{
|
|
bounds = bounds.WithHeight(geometryDrawing.Pen?.Thickness ?? 0);
|
|
}
|
|
else if (bounds.Width == 0)
|
|
{
|
|
bounds = bounds.WithWidth(geometryDrawing.Pen?.Thickness ?? 0);
|
|
}
|
|
|
|
// convert the line geometry into a rectangle geometry
|
|
// we lost line cap info here
|
|
geometry = new RectangleGeometry(bounds);
|
|
}
|
|
|
|
if (accumulatedGeometry == null)
|
|
{
|
|
accumulatedGeometry = new GeometryGroup
|
|
{
|
|
FillRule = FillRule.NonZero
|
|
};
|
|
}
|
|
|
|
accumulatedGeometry.Children.Add(geometry);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private CachedMetrics DrawAndCalculateMetrics(DrawingContext? drawingContext, Point drawingOffset, bool getBlackBoxMetrics)
|
|
{
|
|
var metrics = new CachedMetrics();
|
|
|
|
if (_text.Length == 0)
|
|
{
|
|
return metrics;
|
|
}
|
|
|
|
// we can't use foreach because it requires GetEnumerator and associated classes to be public
|
|
// foreach (TextLine currentLine in this)
|
|
|
|
using (var enumerator = GetEnumerator())
|
|
{
|
|
var first = true;
|
|
|
|
double accBlackBoxLeft, accBlackBoxTop, accBlackBoxRight, accBlackBoxBottom;
|
|
accBlackBoxLeft = accBlackBoxTop = double.MaxValue;
|
|
accBlackBoxRight = accBlackBoxBottom = double.MinValue;
|
|
|
|
var origin = new Point(0, 0);
|
|
|
|
// Holds the TextLine.Start of the longest line. Thus it will hold the minimum value
|
|
// of TextLine.Start among all the lines that forms the text. The overhangs (leading and trailing)
|
|
// are calculated with an offset as a result of the same issue with TextLine.Start.
|
|
// So, we compute this offset and remove it later from the values of the overhangs.
|
|
var lineStartOfLongestLine = double.MaxValue;
|
|
|
|
while (enumerator.MoveNext())
|
|
{
|
|
// enumerator will dispose the currentLine
|
|
var currentLine = enumerator.Current!;
|
|
|
|
// if we're drawing, do it first as this will compute black box metrics as a side-effect
|
|
if (drawingContext != null)
|
|
{
|
|
currentLine.Draw(drawingContext,
|
|
new Point(origin.X + drawingOffset.X, origin.Y + drawingOffset.Y));
|
|
}
|
|
|
|
if (getBlackBoxMetrics)
|
|
{
|
|
var blackBoxLeft = origin.X + currentLine.Start + currentLine.OverhangLeading;
|
|
var blackBoxRight = origin.X + currentLine.Start + currentLine.Width - currentLine.OverhangTrailing;
|
|
var blackBoxBottom = origin.Y + currentLine.Height + currentLine.OverhangAfter;
|
|
var blackBoxTop = blackBoxBottom - currentLine.Extent;
|
|
|
|
accBlackBoxLeft = Math.Min(accBlackBoxLeft, blackBoxLeft);
|
|
accBlackBoxRight = Math.Max(accBlackBoxRight, blackBoxRight);
|
|
accBlackBoxBottom = Math.Max(accBlackBoxBottom, blackBoxBottom);
|
|
accBlackBoxTop = Math.Min(accBlackBoxTop, blackBoxTop);
|
|
|
|
metrics.OverhangAfter = currentLine.OverhangAfter;
|
|
}
|
|
|
|
metrics.Height += currentLine.Height;
|
|
metrics.Width = Math.Max(metrics.Width, currentLine.Width);
|
|
metrics.WidthIncludingTrailingWhitespace = Math.Max(metrics.WidthIncludingTrailingWhitespace, currentLine.WidthIncludingTrailingWhitespace);
|
|
lineStartOfLongestLine = Math.Min(lineStartOfLongestLine, currentLine.Start);
|
|
|
|
if (first)
|
|
{
|
|
metrics.Baseline = currentLine.Baseline;
|
|
first = false;
|
|
}
|
|
|
|
AdvanceLineOrigin(ref origin, currentLine);
|
|
}
|
|
|
|
if (getBlackBoxMetrics)
|
|
{
|
|
metrics.Extent = accBlackBoxBottom - accBlackBoxTop;
|
|
metrics.OverhangLeading = accBlackBoxLeft - lineStartOfLongestLine;
|
|
metrics.OverhangTrailing = metrics.Width - (accBlackBoxRight - lineStartOfLongestLine);
|
|
}
|
|
else
|
|
{
|
|
// indicate that black box metrics are not known
|
|
metrics.Extent = double.NaN;
|
|
}
|
|
}
|
|
|
|
return metrics;
|
|
}
|
|
|
|
private class TextSourceImplementation : ITextSource
|
|
{
|
|
private readonly FormattedText _that;
|
|
|
|
public TextSourceImplementation(FormattedText text)
|
|
{
|
|
_that = text;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public TextRun? GetTextRun(int textSourceCharacterIndex)
|
|
{
|
|
if (textSourceCharacterIndex >= _that._text.Length)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var thatFormatRider = new SpanRider(_that._formatRuns, _that._latestPosition, textSourceCharacterIndex);
|
|
|
|
TextRunProperties properties = (GenericTextRunProperties)thatFormatRider.CurrentElement!;
|
|
|
|
var textCharacters = new TextCharacters(_that._text, textSourceCharacterIndex, thatFormatRider.Length,
|
|
properties);
|
|
|
|
return textCharacters;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|