|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections;
using System.ComponentModel;
using System.Globalization;
using System.Windows.Media.TextFormatting;
using MS.Internal;
using MS.Internal.TextFormatting;
#pragma warning disable 1634, 1691
//Allow suppression of Presharp warnings
namespace System.Windows.Media
{
/// <summary>
/// The FormattedText class is targeted at programmers needing to add some simple text to a MIL visual.
/// </summary>
public class FormattedText
{
#region Construction
/// <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>
[Obsolete("Use the PixelsPerDip override", false)]
public FormattedText(
string textToFormat,
CultureInfo culture,
FlowDirection flowDirection,
Typeface typeface,
double emSize,
Brush foreground) : this(
textToFormat,
culture,
flowDirection,
typeface,
emSize,
foreground,
null,
TextFormattingMode.Ideal
)
{
}
/// <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>
/// <param name="pixelsPerDip">DPI scale on which to render text</param>
public FormattedText(
string textToFormat,
CultureInfo culture,
FlowDirection flowDirection,
Typeface typeface,
double emSize,
Brush foreground,
double pixelsPerDip) : this(
textToFormat,
culture,
flowDirection,
typeface,
emSize,
foreground,
null,
TextFormattingMode.Ideal,
pixelsPerDip
)
{
}
/// <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>
/// <param name="numberSubstitution">Number substitution behavior to apply to the text; can be null,
/// in which case the default number number method for the text culture is used.</param>
[Obsolete("Use the PixelsPerDip override", false)]
public FormattedText(
string textToFormat,
CultureInfo culture,
FlowDirection flowDirection,
Typeface typeface,
double emSize,
Brush foreground,
NumberSubstitution numberSubstitution) : this(
textToFormat,
culture,
flowDirection,
typeface,
emSize,
foreground,
numberSubstitution,
TextFormattingMode.Ideal
)
{
}
/// <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>
/// <param name="numberSubstitution">Number substitution behavior to apply to the text; can be null,
/// in which case the default number number method for the text culture is used.</param>
/// <param name="pixelsPerDip">DPI scale on which to render text.</param>
public FormattedText(
string textToFormat,
CultureInfo culture,
FlowDirection flowDirection,
Typeface typeface,
double emSize,
Brush foreground,
NumberSubstitution numberSubstitution,
double pixelsPerDip) : this(
textToFormat,
culture,
flowDirection,
typeface,
emSize,
foreground,
numberSubstitution,
TextFormattingMode.Ideal,
pixelsPerDip
)
{
}
/// <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>
/// <param name="numberSubstitution">Number substitution behavior to apply to the text; can be null,
/// in which case the default number number method for the text culture is used.</param>
[Obsolete("Use the PixelsPerDip override", false)]
public FormattedText(
string textToFormat,
CultureInfo culture,
FlowDirection flowDirection,
Typeface typeface,
double emSize,
Brush foreground,
NumberSubstitution numberSubstitution,
TextFormattingMode textFormattingMode)
{
InitFormattedText(textToFormat, culture, flowDirection, typeface, emSize, foreground, numberSubstitution, textFormattingMode, _pixelsPerDip);
}
/// <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>
/// <param name="numberSubstitution">Number substitution behavior to apply to the text; can be null,
/// in which case the default number number method for the text culture is used.</param>
/// <param name="pixelsPerDip">DPI scale on which to render text.</param>
public FormattedText(
string textToFormat,
CultureInfo culture,
FlowDirection flowDirection,
Typeface typeface,
double emSize,
Brush foreground,
NumberSubstitution numberSubstitution,
TextFormattingMode textFormattingMode,
double pixelsPerDip)
{
InitFormattedText(textToFormat, culture, flowDirection, typeface, emSize, foreground, numberSubstitution, textFormattingMode, pixelsPerDip);
}
private void InitFormattedText(string textToFormat, CultureInfo culture, FlowDirection flowDirection, Typeface typeface,
double emSize, Brush foreground, NumberSubstitution numberSubstitution, TextFormattingMode textFormattingMode, double pixelsPerDip)
{
ArgumentNullException.ThrowIfNull(textToFormat);
ArgumentNullException.ThrowIfNull(typeface);
ValidateCulture(culture);
ValidateFlowDirection(flowDirection, "flowDirection");
ValidateFontSize(emSize);
_pixelsPerDip = pixelsPerDip;
_textFormattingMode = textFormattingMode;
_text = textToFormat;
GenericTextRunProperties runProps = new GenericTextRunProperties(
typeface,
emSize,
12.0f, // default hinting size
_pixelsPerDip,
null, // decorations
foreground,
null, // highlight background
BaselineAlignment.Baseline,
culture,
numberSubstitution
);
_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
);
InvalidateMetrics();
}
/// <summary>
/// Returns the string of text to be displayed
/// </summary>
public string Text
{
get { return _text; }
}
/// <summary>
/// Sets the PixelsPerDip at which this text should be rendered. Must be set when creating FormattedObject and updated when DPI changes.
/// </summary>
public double PixelsPerDip
{
get { return _pixelsPerDip; }
set
{
_pixelsPerDip = value;
_defaultParaProps.DefaultTextRunProperties.PixelsPerDip = _pixelsPerDip;
}
}
#endregion
#region Formatting properties
private static void ValidateCulture(CultureInfo culture)
{
ArgumentNullException.ThrowIfNull(culture);
}
private static void ValidateFontSize(double emSize)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(emSize);
ArgumentOutOfRangeException.ThrowIfGreaterThan(emSize, MaxFontEmSize);
ArgumentOutOfRangeException.ThrowIfEqual(emSize, double.NaN);
}
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)
{
ArgumentOutOfRangeException.ThrowIfNegative(startIndex);
ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, _text.Length);
int limit = startIndex + count;
if (count < 0 || limit < startIndex || limit > _text.Length)
throw new ArgumentOutOfRangeException("count");
return limit;
}
private void InvalidateMetrics()
{
_metrics = null;
_minWidth = double.MinValue;
}
/// <summary>
/// Sets foreground brush used for drawing text
/// </summary>
/// <param name="foregroundBrush">Foreground brush</param>
public void SetForegroundBrush(Brush 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(Brush foregroundBrush, int startIndex, int count)
{
int limit = ValidateRange(startIndex, count);
for (int i = startIndex; i < limit;)
{
SpanRider 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
GenericTextRunProperties runProps = formatRider.CurrentElement as GenericTextRunProperties;
Invariant.Assert(runProps != null);
if (runProps.ForegroundBrush == foregroundBrush)
continue;
GenericTextRunProperties newProps = new GenericTextRunProperties(
runProps.Typeface,
runProps.FontRenderingEmSize,
runProps.FontHintingEmSize,
_pixelsPerDip,
runProps.TextDecorations,
foregroundBrush,
runProps.BackgroundBrush,
runProps.BaselineAlignment,
runProps.CultureInfo,
runProps.NumberSubstitution
);
#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)
{
ArgumentNullException.ThrowIfNull(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)
{
ArgumentNullException.ThrowIfNull(fontFamily);
int limit = ValidateRange(startIndex, count);
for (int i = startIndex; i < limit;)
{
SpanRider 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
GenericTextRunProperties runProps = formatRider.CurrentElement as GenericTextRunProperties;
Invariant.Assert(runProps != null);
Typeface oldTypeface = runProps.Typeface;
if (fontFamily.Equals(oldTypeface.FontFamily))
continue;
GenericTextRunProperties newProps = new GenericTextRunProperties(
new Typeface(fontFamily, oldTypeface.Style, oldTypeface.Weight, oldTypeface.Stretch),
runProps.FontRenderingEmSize,
runProps.FontHintingEmSize,
_pixelsPerDip,
runProps.TextDecorations,
runProps.ForegroundBrush,
runProps.BackgroundBrush,
runProps.BaselineAlignment,
runProps.CultureInfo,
runProps.NumberSubstitution
);
#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);
int limit = ValidateRange(startIndex, count);
for (int i = startIndex; i < limit;)
{
SpanRider 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
GenericTextRunProperties runProps = formatRider.CurrentElement as GenericTextRunProperties;
Invariant.Assert(runProps != null);
if (runProps.FontRenderingEmSize == emSize)
continue;
GenericTextRunProperties newProps = new GenericTextRunProperties(
runProps.Typeface,
emSize,
runProps.FontHintingEmSize,
_pixelsPerDip,
runProps.TextDecorations,
runProps.ForegroundBrush,
runProps.BackgroundBrush,
runProps.BaselineAlignment,
runProps.CultureInfo,
runProps.NumberSubstitution
);
_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)
{
ValidateCulture(culture);
int limit = ValidateRange(startIndex, count);
for (int i = startIndex; i < limit; )
{
SpanRider 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
GenericTextRunProperties runProps = formatRider.CurrentElement as GenericTextRunProperties;
Invariant.Assert(runProps != null);
if (runProps.CultureInfo.Equals(culture))
continue;
GenericTextRunProperties newProps = new GenericTextRunProperties(
runProps.Typeface,
runProps.FontRenderingEmSize,
runProps.FontHintingEmSize,
_pixelsPerDip,
runProps.TextDecorations,
runProps.ForegroundBrush,
runProps.BackgroundBrush,
runProps.BaselineAlignment,
culture,
runProps.NumberSubstitution
);
#pragma warning restore 6506
_latestPosition = _formatRuns.SetValue(formatRider.CurrentPosition, i - formatRider.CurrentPosition, newProps, formatRider.SpanPosition);
InvalidateMetrics();
}
}
/// <summary>
/// Sets or changes the number substitution behavior for the text.
/// </summary>
/// <param name="numberSubstitution">Number substitution behavior to apply to the text; can be null,
/// in which case the default number substitution method for the text culture is used.</param>
public void SetNumberSubstitution(
NumberSubstitution numberSubstitution
)
{
SetNumberSubstitution(numberSubstitution, 0, _text.Length);
}
/// <summary>
/// Sets or changes the number substitution behavior for a range of text.
/// </summary>
/// <param name="numberSubstitution">Number substitution behavior to apply to the text; can be null,
/// in which case the default number substitution method for the text culture is used.</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 SetNumberSubstitution(
NumberSubstitution numberSubstitution,
int startIndex,
int count
)
{
int limit = ValidateRange(startIndex, count);
for (int i = startIndex; i < limit; )
{
SpanRider 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
GenericTextRunProperties runProps = formatRider.CurrentElement as GenericTextRunProperties;
Invariant.Assert(runProps != null);
if (numberSubstitution != null)
{
if (numberSubstitution.Equals(runProps.NumberSubstitution))
continue;
}
else
{
if (runProps.NumberSubstitution == null)
continue;
}
GenericTextRunProperties newProps = new GenericTextRunProperties(
runProps.Typeface,
runProps.FontRenderingEmSize,
runProps.FontHintingEmSize,
_pixelsPerDip,
runProps.TextDecorations,
runProps.ForegroundBrush,
runProps.BackgroundBrush,
runProps.BaselineAlignment,
runProps.CultureInfo,
numberSubstitution
);
#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)
{
int limit = ValidateRange(startIndex, count);
for (int i = startIndex; i < limit;)
{
SpanRider 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
GenericTextRunProperties runProps = formatRider.CurrentElement as GenericTextRunProperties;
Invariant.Assert(runProps != null);
Typeface oldTypeface = runProps.Typeface;
if (oldTypeface.Weight == weight)
continue;
GenericTextRunProperties newProps = new GenericTextRunProperties(
new Typeface(oldTypeface.FontFamily, oldTypeface.Style, weight, oldTypeface.Stretch),
runProps.FontRenderingEmSize,
runProps.FontHintingEmSize,
_pixelsPerDip,
runProps.TextDecorations,
runProps.ForegroundBrush,
runProps.BackgroundBrush,
runProps.BaselineAlignment,
runProps.CultureInfo,
runProps.NumberSubstitution
);
#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)
{
int limit = ValidateRange(startIndex, count);
for (int i = startIndex; i < limit;)
{
SpanRider 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
GenericTextRunProperties runProps = formatRider.CurrentElement as GenericTextRunProperties;
Invariant.Assert(runProps != null);
Typeface oldTypeface = runProps.Typeface;
if (oldTypeface.Style == style)
continue;
GenericTextRunProperties newProps = new GenericTextRunProperties(
new Typeface(oldTypeface.FontFamily, style, oldTypeface.Weight, oldTypeface.Stretch),
runProps.FontRenderingEmSize,
runProps.FontHintingEmSize,
_pixelsPerDip,
runProps.TextDecorations,
runProps.ForegroundBrush,
runProps.BackgroundBrush,
runProps.BaselineAlignment,
runProps.CultureInfo,
runProps.NumberSubstitution
);
#pragma warning restore 6506
_latestPosition = _formatRuns.SetValue(formatRider.CurrentPosition, i - formatRider.CurrentPosition, newProps, formatRider.SpanPosition);
InvalidateMetrics(); // invalidate cached metrics
}
}
/// <summary>
/// Sets or changes the font stretch
/// </summary>
/// <param name="stretch">Font stretch</param>
public void SetFontStretch(FontStretch stretch)
{
SetFontStretch(stretch, 0, _text.Length);
}
/// <summary>
/// Sets or changes the font stretch
/// </summary>
/// <param name="stretch">Font stretch</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 SetFontStretch(FontStretch stretch, int startIndex, int count)
{
int limit = ValidateRange(startIndex, count);
for (int i = startIndex; i < limit;)
{
SpanRider 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
GenericTextRunProperties runProps = formatRider.CurrentElement as GenericTextRunProperties;
Invariant.Assert(runProps != null);
Typeface oldTypeface = runProps.Typeface;
if (oldTypeface.Stretch == stretch)
continue;
GenericTextRunProperties newProps = new GenericTextRunProperties(
new Typeface(oldTypeface.FontFamily, oldTypeface.Style, oldTypeface.Weight, stretch),
runProps.FontRenderingEmSize,
runProps.FontHintingEmSize,
_pixelsPerDip,
runProps.TextDecorations,
runProps.ForegroundBrush,
runProps.BackgroundBrush,
runProps.BaselineAlignment,
runProps.CultureInfo,
runProps.NumberSubstitution
);
_latestPosition = _formatRuns.SetValue(formatRider.CurrentPosition, i - formatRider.CurrentPosition, newProps, formatRider.SpanPosition);
#pragma warning restore 6506
InvalidateMetrics();
}
}
/// <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)
{
int limit = ValidateRange(startIndex, count);
for (int i = startIndex; i < limit;)
{
SpanRider 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
GenericTextRunProperties runProps = formatRider.CurrentElement as GenericTextRunProperties;
Invariant.Assert(runProps != null);
if (runProps.Typeface == typeface)
continue;
GenericTextRunProperties newProps = new GenericTextRunProperties(
typeface,
runProps.FontRenderingEmSize,
runProps.FontHintingEmSize,
_pixelsPerDip,
runProps.TextDecorations,
runProps.ForegroundBrush,
runProps.BackgroundBrush,
runProps.BaselineAlignment,
runProps.CultureInfo,
runProps.NumberSubstitution
);
#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)
{
int limit = ValidateRange(startIndex, count);
for (int i = startIndex; i < limit;)
{
SpanRider 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
GenericTextRunProperties runProps = formatRider.CurrentElement as GenericTextRunProperties;
Invariant.Assert(runProps != null);
if (runProps.TextDecorations == textDecorations)
continue;
GenericTextRunProperties newProps = new GenericTextRunProperties(
runProps.Typeface,
runProps.FontRenderingEmSize,
runProps.FontHintingEmSize,
_pixelsPerDip,
textDecorations,
runProps.ForegroundBrush,
runProps.BackgroundBrush,
runProps.BaselineAlignment,
runProps.CultureInfo,
runProps.NumberSubstitution
);
#pragma warning restore 6506
_latestPosition = _formatRuns.SetValue(formatRider.CurrentPosition, i - formatRider.CurrentPosition, newProps, formatRider.SpanPosition);
}
}
#endregion
#region Line enumerator
/// 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
{
int _textStorePosition;
int _lineCount;
double _totalHeight;
TextLine _currentLine;
TextLine _nextLine;
TextFormatter _formatter;
FormattedText _that;
// these are needed because _currentLine can be disposed before the next MoveNext() call
double _previousHeight;
int _previousLength;
// line break before _currentLine, needed in case we have to reformat it with collapsing symbol
TextLineBreak _previousLineBreak;
internal LineEnumerator(FormattedText text)
{
_previousHeight = 0;
_previousLength = 0;
_previousLineBreak = null;
_textStorePosition = 0;
_lineCount = 0;
_totalHeight = 0;
_currentLine = null;
_nextLine = null;
_formatter = TextFormatter.FromCurrentDispatcher(text._textFormattingMode);
_that = text;
if (_that._textSourceImpl == null)
_that._textSourceImpl = new TextSourceImplementation(_that);
}
public void Dispose()
{
if (_currentLine != null)
{
_currentLine.Dispose();
_currentLine = null;
}
if (_nextLine != null)
{
_nextLine.Dispose();
_nextLine = null;
}
}
internal int Position
{
get
{
return _textStorePosition;
}
}
internal int Length
{
get
{
return _previousLength;
}
}
/// <summary>
/// Gets the current text line in the collection
/// </summary>
public TextLine Current
{
get
{
return _currentLine;
}
}
/// <summary>
/// Gets the current text line in the collection
/// </summary>
object IEnumerator.Current
{
get
{
return (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 (_currentLine == null)
{ // this is the first line
if (_that._text.Length == 0)
return false;
_currentLine = FormatLine(
_that._textSourceImpl,
_textStorePosition,
MaxLineLength(_lineCount),
_that._defaultParaProps,
null // no previous line break
);
// check if this line fits the text height
if (_totalHeight + _currentLine.Height > _that._maxTextHeight)
{
_currentLine.Dispose();
_currentLine = 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;
_textStorePosition += _previousLength;
++_lineCount;
_currentLine = _nextLine;
_nextLine = null;
}
TextLineBreak currentLineBreak = _currentLine.GetTextLineBreak();
// this line is guaranteed to fit the text height
Debug.Assert(_totalHeight + _currentLine.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 (_textStorePosition + _currentLine.Length < _that._text.Length)
{
bool nextLineFits;
if (_lineCount + 1 >= _that._maxLineCount)
nextLineFits = false;
else
{
_nextLine = FormatLine(
_that._textSourceImpl,
_textStorePosition + _currentLine.Length,
MaxLineLength(_lineCount + 1),
_that._defaultParaProps,
currentLineBreak
);
nextLineFits = (_totalHeight + _currentLine.Height + _nextLine.Height <= _that._maxTextHeight);
}
if (!nextLineFits)
{
// next line doesn't fit
if (_nextLine != null)
{
_nextLine.Dispose();
_nextLine = null;
}
if (_that._trimming != TextTrimming.None && !_currentLine.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.
TextWrapping currentWrap = _that._defaultParaProps.TextWrapping;
_that._defaultParaProps.SetTextWrapping(TextWrapping.NoWrap);
if (currentLineBreak != null)
currentLineBreak.Dispose();
_currentLine.Dispose();
_currentLine = FormatLine(
_that._textSourceImpl,
_textStorePosition,
MaxLineLength(_lineCount),
_that._defaultParaProps,
_previousLineBreak
);
currentLineBreak = _currentLine.GetTextLineBreak();
_that._defaultParaProps.SetTextWrapping(currentWrap);
}
}
}
_previousHeight = _currentLine.Height;
_previousLength = _currentLine.Length;
if (_previousLineBreak != null)
_previousLineBreak.Dispose();
_previousLineBreak = currentLineBreak;
return true;
}
/// <summary>
/// Wrapper of TextFormatter.FormatLine that auto-collapses the line if needed.
/// </summary>
private TextLine FormatLine(TextSource textSource, int textSourcePosition, double maxLineLength, TextParagraphProperties paraProps, TextLineBreak lineBreak)
{
TextLine 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);
SpanRider thatFormatRider = new SpanRider(
_that._formatRuns,
_that._latestPosition,
Math.Min(textSourcePosition + line.Length - 1, _that._text.Length - 1)
);
GenericTextRunProperties lastRunProps = thatFormatRider.CurrentElement as GenericTextRunProperties;
TextCollapsingProperties trailingEllipsis;
if (_that._trimming == TextTrimming.CharacterEllipsis)
trailingEllipsis = new TextTrailingCharacterEllipsis(maxLineLength, lastRunProps);
else
{
Debug.Assert(_that._trimming == TextTrimming.WordEllipsis);
trailingEllipsis = new TextTrailingWordEllipsis(maxLineLength, lastRunProps);
}
TextLine collapsedLine = line.Collapse(trailingEllipsis);
if (collapsedLine != line)
{
line.Dispose();
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()
{
_textStorePosition = 0;
_lineCount = 0;
_totalHeight = 0;
_currentLine = 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)
{
double height = currentLine.Height;
// advance line origin according to the flow direction
switch (_defaultParaProps.FlowDirection)
{
case FlowDirection.LeftToRight:
case FlowDirection.RightToLeft:
lineOrigin.Y += height;
break;
}
}
#endregion
#region Measurement and layout properties
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
{
ArgumentOutOfRangeException.ThrowIfNegative(value);
_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
{
ArgumentOutOfRangeException.ThrowIfNegative(value);
_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("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) ? null : (double [])_maxTextWidths.Clone();
}
/// <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("value", SR.Format(SR.PropertyMustBeGreaterThanZero, "MaxTextHeight"));
if (double.IsNaN(value))
throw new ArgumentOutOfRangeException("value", SR.Format(SR.PropertyValueCannotBeNaN, "MaxTextHeight"));
_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
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value);
_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
{
if ((int)value < 0 || (int)value > (int)TextTrimming.WordEllipsis)
throw new InvalidEnumArgumentException("value", (int)value, typeof(TextTrimming));
_trimming = value;
if (_trimming == TextTrimming.None)
{
// if trimming is disabled, enforce emergency wrap
_defaultParaProps.SetTextWrapping(TextWrapping.Wrap);
}
else
{
_defaultParaProps.SetTextWrapping(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
{
if (_metrics == null)
{
// We need to obtain the metrics. DON'T compute black box metrics here because
// they probably won't be needed and computing them requires GlyphRun creation.
// In the common case where a client measures and then draws, we'll format twice
// but create GlyphRuns only during drawing.
_metrics = DrawAndCalculateMetrics(
null, // drawing context
new Point(), // drawing offset
false); // don't calculate black box metrics
}
return _metrics;
}
}
/// <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>
/// The minimum line width that can be specified without causing any word to break.
/// </summary>
public double MinWidth
{
get
{
if (_minWidth != double.MinValue)
return _minWidth;
if (_textSourceImpl == null)
_textSourceImpl = new TextSourceImplementation(this);
_minWidth = TextFormatter.FromCurrentDispatcher(_textFormattingMode).FormatMinMaxParagraphWidth(
_textSourceImpl,
0, // textSourceCharacterIndex
_defaultParaProps
).MinWidth;
return _minWidth;
}
}
/// <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>
/// 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;
Point lineOrigin = origin;
DrawingGroup drawing = new DrawingGroup();
DrawingContext ctx = drawing.Open();
// we can't use foreach because it requires GetEnumerator and associated classes to be public
// foreach (TextLine currentLine in this)
using (LineEnumerator enumerator = GetEnumerator())
{
while (enumerator.MoveNext())
{
using (TextLine currentLine = enumerator.Current)
{
currentLine.Draw(ctx, lineOrigin, InvertAxes.None);
AdvanceLineOrigin(ref lineOrigin, currentLine);
}
}
}
ctx.Close();
// recursively go down the DrawingGroup to build up the geometry
CombineGeometryRecursive(drawing, ref accumulatedGeometry);
// Make sure to always return Geometry.Empty from public methods for empty geometries.
if (accumulatedGeometry == null || accumulatedGeometry.IsEmpty())
return Geometry.Empty;
return accumulatedGeometry;
}
/// <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);
PathGeometry accumulatedBounds = null;
using (LineEnumerator enumerator = GetEnumerator())
{
Point lineOrigin = origin;
while (enumerator.MoveNext())
{
using (TextLine 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)
{
IList<TextBounds> highlightBounds = currentLine.GetTextBounds(
x0,
x1 - x0
);
if (highlightBounds != null)
{
foreach (TextBounds bound in highlightBounds)
{
Rect 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.X = enumerator.CurrentParagraphWidth - rect.Right;
}
rect.X += lineOrigin.X;
rect.Y += lineOrigin.Y;
RectangleGeometry rectangleGeometry = new RectangleGeometry(rect);
if (accumulatedBounds == null)
accumulatedBounds = rectangleGeometry.GetAsPathGeometry();
else
accumulatedBounds = Geometry.Combine(accumulatedBounds, rectangleGeometry, GeometryCombineMode.Union, null);
}
}
}
AdvanceLineOrigin(ref lineOrigin, currentLine);
}
}
}
if (accumulatedBounds == null || accumulatedBounds.IsEmpty())
return null;
return accumulatedBounds;
}
#endregion
#region Drawing
/// <summary>
/// Draws the text object
/// </summary>
internal void Draw(
DrawingContext dc,
Point origin
)
{
Point 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 (LineEnumerator enumerator = GetEnumerator())
{
while (enumerator.MoveNext())
{
using (TextLine currentLine = enumerator.Current)
{
currentLine.Draw(dc, lineOrigin, InvertAxes.None);
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(dc, origin, true);
}
}
private CachedMetrics DrawAndCalculateMetrics(DrawingContext dc, Point drawingOffset, bool getBlackBoxMetrics)
{
// The calculation for FormattedText.Width and Overhangs was wrong for Right and Center alignment.
// Thus the fix of this bug is based on the fact that FormattedText always had 0 indent and no
// TextMarkerProperties. These assumptions enabled us to remove TextLine.Start from the calculation
// of the Width. TextLine.Start caused the calculation of FormattedText to be incorrect in cases
// of Right and Center alignment because it took on -ve values when ParagraphWidth was 0 (which indicates infinite width).
// This was a result of how TextFormatter interprets TextLine.Start. In the simplest case, it computes
// TextLine.Start as Paragraph Width - Line Width (for Right alignment).
// So, the following two Debug.Asserts verify that the assumptions over which the bug fix was made are still valid
// and not changed by adding features to FormattedText. Incase these assumptions were invalidated, the bug fix
// should be revised and it will possibly involve alot of changes elsewhere.
Debug.Assert(_defaultParaProps.Indent == 0.0, "FormattedText was assumed to always have 0 indent. This assumption has changed and thus the calculation of Width and Overhangs should be revised.");
Debug.Assert(_defaultParaProps.TextMarkerProperties == null, "FormattedText was assumed to always have no TextMarkerProperties. This assumption has changed and thus the calculation of Width and Overhangs should be revised.");
CachedMetrics 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 (LineEnumerator enumerator = GetEnumerator())
{
bool first = true;
double accBlackBoxLeft, accBlackBoxTop, accBlackBoxRight, accBlackBoxBottom;
accBlackBoxLeft = accBlackBoxTop = double.MaxValue;
accBlackBoxRight = accBlackBoxBottom = double.MinValue;
Point 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.
double lineStartOfLongestLine = Double.MaxValue;
while (enumerator.MoveNext())
{
// enumerator will dispose the currentLine
using (TextLine currentLine = enumerator.Current)
{
// if we're drawing, do it first as this will compute black box metrics as a side-effect
if (dc != null)
{
currentLine.Draw(
dc,
new Point(origin.X + drawingOffset.X, origin.Y + drawingOffset.Y),
InvertAxes.None
);
}
if (getBlackBoxMetrics)
{
double blackBoxLeft = origin.X + currentLine.Start + currentLine.OverhangLeading;
double blackBoxRight = origin.X + currentLine.Start + currentLine.Width - currentLine.OverhangTrailing;
double blackBoxBottom = origin.Y + currentLine.Height + currentLine.OverhangAfter;
double 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;
}
#endregion
#region TextSource implementation
private class TextSourceImplementation : TextSource
{
private FormattedText _that;
public TextSourceImplementation(FormattedText text)
{
_that = text;
PixelsPerDip = _that.PixelsPerDip;
}
/// <summary>
/// TextFormatter to get a text run started at specified text source position
/// </summary>
/// <param name="textSourceCharacterIndex">character index to specify where in the source text the fetch is to start.</param>
public override TextRun GetTextRun(
int textSourceCharacterIndex
)
{
if (textSourceCharacterIndex >= _that._text.Length)
{
return new TextEndOfParagraph(1);
}
SpanRider thatFormatRider = new SpanRider(
_that._formatRuns,
_that._latestPosition,
textSourceCharacterIndex
);
TextRunProperties properties = thatFormatRider.CurrentElement as GenericTextRunProperties;
TextCharacters textCharacters = new TextCharacters(_that._text,
textSourceCharacterIndex,
thatFormatRider.Length,
properties
);
properties.PixelsPerDip = this.PixelsPerDip;
return textCharacters;
}
/// <summary>
/// TextFormatter to get text immediately before specified text source position.
/// </summary>
/// <param name="textSourceCharacterIndexLimit">character index to specify where in the source text the text retrieval stops.</param>
/// <returns>character string immediately before the specify text source character index.</returns>
public override TextSpan<CultureSpecificCharacterBufferRange> GetPrecedingText(
int textSourceCharacterIndexLimit
)
{
CharacterBufferRange charString = CharacterBufferRange.Empty;
CultureInfo culture = null;
if (textSourceCharacterIndexLimit > 0)
{
SpanRider thatFormatRider = new SpanRider(
_that._formatRuns,
_that._latestPosition,
textSourceCharacterIndexLimit - 1
);
charString = new CharacterBufferRange(
new CharacterBufferReference(_that._text, thatFormatRider.CurrentSpanStart),
textSourceCharacterIndexLimit - thatFormatRider.CurrentSpanStart
);
culture = ((TextRunProperties)thatFormatRider.CurrentElement).CultureInfo;
}
return new TextSpan<CultureSpecificCharacterBufferRange> (
charString.Length,
new CultureSpecificCharacterBufferRange(culture, charString)
);
}
/// <summary>
/// TextFormatter to map a text source character index to a text effect character index
/// </summary>
/// <param name="textSourceCharacterIndex"> text source character index </param>
/// <returns> the text effect index corresponding to the text effect character index </returns>
public override int GetTextEffectCharacterIndexFromTextSourceCharacterIndex(
int textSourceCharacterIndex
)
{
throw new NotSupportedException();
}
};
#endregion
#region private methods
private void CombineGeometryRecursive(Drawing drawing, ref GeometryGroup accumulatedGeometry)
{
DrawingGroup group = drawing as DrawingGroup;
if (group != null)
{
// recursively go down for DrawingGroup
foreach (Drawing child in group.Children)
{
CombineGeometryRecursive(child, ref accumulatedGeometry);
}
}
else
{
GlyphRunDrawing glyphRunDrawing = drawing as GlyphRunDrawing;
if (glyphRunDrawing != null)
{
// process glyph run
GlyphRun glyphRun = glyphRunDrawing.GlyphRun;
if (glyphRun != null)
{
Geometry glyphRunGeometry = glyphRun.BuildGeometry();
if (!glyphRunGeometry.IsEmpty())
{
if (accumulatedGeometry == null)
{
accumulatedGeometry = new GeometryGroup();
accumulatedGeometry.FillRule = FillRule.Nonzero;
}
accumulatedGeometry.Children.Add(glyphRunGeometry);
}
}
}
else
{
GeometryDrawing geometryDrawing = drawing as GeometryDrawing;
if (geometryDrawing != null)
{
// process geometry (i.e. TextDecoration on the line)
Geometry geometry = geometryDrawing.Geometry;
if (geometry != null)
{
LineGeometry lineGeometry = geometry as LineGeometry;
if (lineGeometry != null)
{
// 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
Rect bound = lineGeometry.Bounds;
if (bound.Height == 0)
{
bound.Height = geometryDrawing.Pen.Thickness;
}
else if (bound.Width == 0)
{
bound.Width = geometryDrawing.Pen.Thickness;
}
// convert the line geometry into a rectangle geometry
// we lost line cap info here
geometry = new RectangleGeometry(bound);
}
if (accumulatedGeometry == null)
{
accumulatedGeometry = new GeometryGroup();
accumulatedGeometry.FillRule = FillRule.Nonzero;
}
accumulatedGeometry.Children.Add(geometry);
}
}
}
}
}
#endregion
#region Private fields
// properties and format runs
private string _text;
private double _pixelsPerDip = MS.Internal.FontCache.Util.PixelsPerDip;
private SpanVector _formatRuns = new SpanVector(null);
private SpanPosition _latestPosition = new SpanPosition();
private GenericTextParagraphProperties _defaultParaProps;
private double _maxTextWidth;
private double [] _maxTextWidths;
private double _maxTextHeight = double.MaxValue;
private int _maxLineCount = int.MaxValue;
private TextTrimming _trimming = TextTrimming.WordEllipsis;
private TextFormattingMode _textFormattingMode;
// text source callbacks
private TextSourceImplementation _textSourceImpl;
// cached metrics
private CachedMetrics _metrics;
private double _minWidth;
#endregion
#region Constants
const double MaxFontEmSize = Constants.RealInfiniteWidth / Constants.GreatestMutiplierOfEm;
#endregion
}
}
|