| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527 |
- using System;
- using System.Collections;
- using System.ComponentModel;
- using System.Diagnostics;
- using System.Globalization;
- 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
- );
- 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;
- 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;
- }
- private int Position { get; set; }
- private int Length { get; set; }
- /// <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>
- /// 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;
- }
- }
- }
- }
|