diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index 20e0f96ff7..985ee2b6f3 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -155,6 +155,8 @@ namespace Avalonia.Media /// public Rect Bounds => new Rect(new Size(Metrics.WidthIncludingTrailingWhitespace, Metrics.Height)); + public Rect InkBounds => PlatformImpl.Item.Bounds; + /// /// /// @@ -728,7 +730,7 @@ namespace Avalonia.Media clusterLength++; i--; - if(characterIndex >= 0) + if (characterIndex >= 0) { codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out characterLength); @@ -827,8 +829,7 @@ namespace Avalonia.Media GlyphTypeface, FontRenderingEmSize, GlyphInfos, - BaselineOrigin, - Bounds); + BaselineOrigin); _platformImpl = RefCountable.Create(platformImpl); diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs index 2f28c3f954..ef528ba73b 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs @@ -64,7 +64,7 @@ namespace Avalonia.Media.TextFormatting if (Properties.BackgroundBrush != null) { - drawingContext.DrawRectangle(Properties.BackgroundBrush, null, new Rect(Size)); + drawingContext.DrawRectangle(Properties.BackgroundBrush, null, GlyphRun.Bounds); } drawingContext.DrawGlyphRun(Properties.ForegroundBrush, GlyphRun); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index f373e0178a..0697831987 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -13,6 +13,7 @@ namespace Avalonia.Media.TextFormatting private readonly TextParagraphProperties _paragraphProperties; private readonly TextTrimming _textTrimming; private readonly TextLine[] _textLines; + private readonly CachedMetrics _metrics = new(); private int _textSourceLength; @@ -151,12 +152,95 @@ namespace Avalonia.Media.TextFormatting => _textLines; /// - /// Gets the bounds of the layout. + /// The distance from the top of the first line to the bottom of the last line. /// - /// - /// The bounds. - /// - public Rect Bounds { get; private set; } + public double Height + { + get + { + return _metrics.Height; + } + } + + /// + /// The distance from the topmost black pixel of the first line + /// to the bottommost black pixel of the last line. + /// + public double Extent + { + get + { + return _metrics.Extent; + } + } + + /// + /// The distance from the top of the first line to the baseline of the first line. + /// + public double Baseline + { + get + { + return _metrics.Baseline; + } + } + + /// + /// The distance from the bottom of the last line to the extent bottom. + /// + public double OverhangAfter + { + get + { + return _metrics.OverhangAfter; + } + } + + /// + /// The maximum distance from the leading black pixel to the leading alignment point of a line. + /// + public double OverhangLeading + { + get + { + return _metrics.OverhangLeading; + } + } + + /// + /// The maximum distance from the trailing black pixel to the trailing alignment point of a line. + /// + public double OverhangTrailing + { + get + { + return _metrics.OverhangTrailing; + } + } + + /// + /// 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. + /// + public double Width + { + get + { + return _metrics.Width; + } + } + + /// + /// 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. + /// + public double WidthIncludingTrailingWhitespace + { + get + { + return _metrics.WidthIncludingTrailingWhitespace; + } + } /// /// Draws the text layout. @@ -382,7 +466,7 @@ namespace Avalonia.Media.TextFormatting var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; var isTrailing = lastTrailingIndex == textPosition && characterHit.TrailingLength > 0 || - y > Bounds.Bottom; + y > Height; if (textPosition == textLine.FirstTextSourceIndex + textLine.Length) { @@ -422,41 +506,25 @@ namespace Avalonia.Media.TextFormatting textRunStyle, textWrapping, lineHeight, 0, letterSpacing); } - /// - /// Updates the current bounds. - /// - /// The text line. - /// The current left. - /// The current width. - /// The current height. - private static void UpdateBounds(TextLine textLine, ref double left, ref double width, ref double height) + private TextLine[] CreateTextLines() { - var lineWidth = textLine.WidthIncludingTrailingWhitespace; - - if (width < lineWidth) - { - width = lineWidth; - } + var objectPool = FormattingObjectPool.Instance; - var start = textLine.Start; + var lineStartOfLongestLine = double.MaxValue; + var origin = new Point(); + var first = true; - if (left > start) - { - left = start; - } - - height += textLine.Height; - } + double accBlackBoxLeft, accBlackBoxTop, accBlackBoxRight, accBlackBoxBottom; - private TextLine[] CreateTextLines() - { - var objectPool = FormattingObjectPool.Instance; + accBlackBoxLeft = accBlackBoxTop = double.MaxValue; + accBlackBoxRight = accBlackBoxBottom = double.MinValue; if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight)) { var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties); - Bounds = new Rect(0, 0, 0, textLine.Height); + UpdateMetrics(textLine, ref lineStartOfLongestLine, ref origin, ref first, + ref accBlackBoxLeft, ref accBlackBoxTop, ref accBlackBoxRight, ref accBlackBoxBottom); return new TextLine[] { textLine }; } @@ -465,8 +533,6 @@ namespace Avalonia.Media.TextFormatting try { - double left = double.PositiveInfinity, width = 0.0, height = 0.0; - _textSourceLength = 0; TextLine? previousLine = null; @@ -487,7 +553,8 @@ namespace Avalonia.Media.TextFormatting textLines.Add(emptyTextLine); - UpdateBounds(emptyTextLine, ref left, ref width, ref height); + UpdateMetrics(emptyTextLine, ref lineStartOfLongestLine, ref origin, ref first, + ref accBlackBoxLeft, ref accBlackBoxTop, ref accBlackBoxRight, ref accBlackBoxBottom); } break; @@ -497,7 +564,7 @@ namespace Avalonia.Media.TextFormatting //Fulfill max height constraint if (textLines.Count > 0 && !double.IsPositiveInfinity(MaxHeight) - && height + textLine.Height > MaxHeight) + && Height + textLine.Height > MaxHeight) { if (previousLine?.TextLineBreak != null && _textTrimming != TextTrimming.None) { @@ -519,7 +586,8 @@ namespace Avalonia.Media.TextFormatting textLines.Add(textLine); - UpdateBounds(textLine, ref left, ref width, ref height); + UpdateMetrics(textLine, ref lineStartOfLongestLine, ref origin, ref first, + ref accBlackBoxLeft, ref accBlackBoxTop, ref accBlackBoxRight, ref accBlackBoxBottom); previousLine = textLine; @@ -528,7 +596,7 @@ namespace Avalonia.Media.TextFormatting { if (textLine.TextLineBreak is { IsSplit: true }) { - textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(width)); + textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(WidthIncludingTrailingWhitespace)); } break; @@ -546,18 +614,17 @@ namespace Avalonia.Media.TextFormatting textLines.Add(textLine); - UpdateBounds(textLine, ref left, ref width, ref height); + UpdateMetrics(textLine, ref lineStartOfLongestLine, ref origin, ref first, + ref accBlackBoxLeft, ref accBlackBoxTop, ref accBlackBoxRight, ref accBlackBoxBottom); } - Bounds = new Rect(left, 0, width, height); - if (_paragraphProperties.TextAlignment == TextAlignment.Justify) { var justificationWidth = MaxWidth; if (_paragraphProperties.TextWrapping != TextWrapping.NoWrap) { - justificationWidth = width; + justificationWidth = WidthIncludingTrailingWhitespace; } if (justificationWidth > 0) @@ -582,6 +649,46 @@ namespace Avalonia.Media.TextFormatting } } + private void UpdateMetrics( + TextLine currentLine, + ref double lineStartOfLongestLine, + ref Point origin, + ref bool first, + ref double accBlackBoxLeft, + ref double accBlackBoxTop, + ref double accBlackBoxRight, + ref double accBlackBoxBottom) + { + 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); + + _metrics.Extent = accBlackBoxBottom - accBlackBoxTop; + _metrics.OverhangLeading = accBlackBoxLeft - lineStartOfLongestLine; + _metrics.OverhangTrailing = _metrics.Width - (accBlackBoxRight - lineStartOfLongestLine); + + if (first) + { + _metrics.Baseline = currentLine.Baseline; + first = false; + } + + origin = origin.WithY(origin.Y + currentLine.Height); + } + /// /// Gets the for current text trimming mode. /// @@ -605,5 +712,24 @@ namespace Avalonia.Media.TextFormatting line.Dispose(); } } + + 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; + } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 1234067844..a0d7cabefd 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -51,7 +51,7 @@ namespace Avalonia.Media.TextFormatting public override double Baseline => _textLineMetrics.TextBaseline; /// - public override double Extent => _textLineMetrics.Height; + public override double Extent => _textLineMetrics.Extent; /// public override double Height => _textLineMetrics.Height; @@ -60,13 +60,13 @@ namespace Avalonia.Media.TextFormatting public override int NewLineLength => _textLineMetrics.NewlineLength; /// - public override double OverhangAfter => 0; + public override double OverhangAfter => _textLineMetrics.OverhangAfter; /// - public override double OverhangLeading => 0; + public override double OverhangLeading => _textLineMetrics.OverhangLeading; /// - public override double OverhangTrailing => 0; + public override double OverhangTrailing => _textLineMetrics.OverhangTrailing; /// public override int TrailingWhitespaceLength => _textLineMetrics.TrailingWhitespaceLength; @@ -87,13 +87,18 @@ namespace Avalonia.Media.TextFormatting foreach (var textRun in _textRuns) { - if (textRun is DrawableTextRun drawable) + switch (textRun) { - var offsetY = GetBaselineOffset(this, drawable); + case DrawableTextRun drawableTextRun: + { + var offsetY = GetBaselineOffset(this, drawableTextRun); + + drawableTextRun.Draw(drawingContext, new Point(currentX, currentY + offsetY)); - drawable.Draw(drawingContext, new Point(currentX, currentY + offsetY)); + currentX += drawableTextRun.Size.Width; - currentX += drawable.Size.Width; + break; + } } } } @@ -174,10 +179,12 @@ namespace Avalonia.Media.TextFormatting distance -= Start; var lastIndex = _textRuns.Length - 1; + var lineLength = Length; - if (_textRuns[lastIndex] is TextEndOfLine) + if (_textRuns[lastIndex] is TextEndOfLine textEndOfLine) { lastIndex--; + lineLength -= textEndOfLine.Length; } var currentPosition = FirstTextSourceIndex; @@ -205,7 +212,7 @@ namespace Avalonia.Media.TextFormatting if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight) { - currentPosition = Length - lastRun.Length; + currentPosition = lineLength - lastRun.Length; } return GetRunCharacterHit(lastRun, currentPosition, distance); @@ -703,7 +710,7 @@ namespace Avalonia.Media.TextFormatting //In case a run only contains a linebreak we don't want to skip it. if (currentRun is ShapedTextRun shaped) { - if(currentRun.Length - shaped.GlyphRun.Metrics.NewLineLength > 0) + if (currentRun.Length - shaped.GlyphRun.Metrics.NewLineLength > 0) { continue; } @@ -1431,9 +1438,10 @@ namespace Avalonia.Media.TextFormatting var lineGap = fontMetrics.LineGap * scale; var height = descent - ascent + lineGap; - var lineHeight = _paragraphProperties.LineHeight; + var bounds = new Rect(); + for (var index = 0; index < _textRuns.Length; index++) { switch (_textRuns[index]) @@ -1441,6 +1449,9 @@ namespace Avalonia.Media.TextFormatting case ShapedTextRun textRun: { var textMetrics = textRun.TextMetrics; + var glyphRun = textRun.GlyphRun; + + bounds = bounds.Union(glyphRun.InkBounds); if (fontRenderingEmSize < textMetrics.FontRenderingEmSize) { @@ -1486,18 +1497,22 @@ namespace Avalonia.Media.TextFormatting ascent = -drawableTextRun.Baseline; } + bounds = bounds.Union(new Rect(new Point(bounds.Right, 0), drawableTextRun.Size)); + break; } } } + var overhangAfter = Math.Max(0, bounds.Bottom - height); + var width = widthIncludingWhitespace; for (var i = _textRuns.Length - 1; i >= 0; i--) { var currentRun = _textRuns[i]; - if(currentRun is ShapedTextRun shapedText) + if (currentRun is ShapedTextRun shapedText) { var glyphRun = shapedText.GlyphRun; var glyphRunMetrics = glyphRun.Metrics; @@ -1518,6 +1533,9 @@ namespace Avalonia.Media.TextFormatting } var start = GetParagraphOffsetX(width, widthIncludingWhitespace); + var overhangLeading = Math.Max(0, bounds.Left - start); + var overhangTrailing = Math.Max(0, bounds.Width - widthIncludingWhitespace); + var hasOverflowed = overhangLeading + widthIncludingWhitespace + overhangTrailing > _paragraphWidth; if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight)) { @@ -1527,8 +1545,21 @@ namespace Avalonia.Media.TextFormatting } } - return new TextLineMetrics(widthIncludingWhitespace > _paragraphWidth, height, newLineLength, start, - -ascent, trailingWhitespaceLength, width, widthIncludingWhitespace); + return new TextLineMetrics + { + HasOverflowed = hasOverflowed, + Height = height, + Extent = bounds.Height, + NewlineLength = newLineLength, + Start = start, + TextBaseline = -ascent, + TrailingWhitespaceLength = trailingWhitespaceLength, + Width = width, + WidthIncludingTrailingWhitespace = widthIncludingWhitespace, + OverhangLeading= overhangLeading, + OverhangTrailing= overhangTrailing, + OverhangAfter = overhangAfter + }; } /// diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs index cb21e27696..a1a6d309dd 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs @@ -5,59 +5,65 @@ /// that holds information about ascent, descent, line gap, size and origin of the text line. /// public readonly record struct TextLineMetrics - { - public TextLineMetrics(bool hasOverflowed, double height, int newlineLength, double start, double textBaseline, - int trailingWhitespaceLength, double width, - double widthIncludingTrailingWhitespace) - { - HasOverflowed = hasOverflowed; - Height = height; - NewlineLength = newlineLength; - Start = start; - TextBaseline = textBaseline; - TrailingWhitespaceLength = trailingWhitespaceLength; - Width = width; - WidthIncludingTrailingWhitespace = widthIncludingTrailingWhitespace; - } - + { /// /// Gets a value that indicates whether content of the line overflows the specified paragraph width. /// - public bool HasOverflowed { get; } + public bool HasOverflowed { get; init; } /// /// Gets the height of a line of text. /// - public double Height { get; } + public double Height { get; init; } /// /// Gets the number of newline characters at the end of a line. /// - public int NewlineLength { get; } + public int NewlineLength { get; init; } /// /// Gets the distance from the start of a paragraph to the starting point of a line. /// - public double Start { get; } + public double Start { get; init; } /// /// Gets the distance from the top to the baseline of the line of text. /// - public double TextBaseline { get; } + public double TextBaseline { get; init; } /// /// Gets the number of whitespace code points beyond the last non-blank character in a line. /// - public int TrailingWhitespaceLength { get; } + public int TrailingWhitespaceLength { get; init; } /// /// Gets the width of a line of text, excluding trailing whitespace characters. /// - public double Width { get; } + public double Width { get; init; } /// /// Gets the width of a line of text, including trailing whitespace characters. /// - public double WidthIncludingTrailingWhitespace { get; } + public double WidthIncludingTrailingWhitespace { get; init; } + + /// + /// Gets the distance from the top-most to bottom-most black pixel in a line. + /// + public double Extent { get; init; } + + /// + /// Gets the distance that black pixels extend beyond the bottom alignment edge of a line. + /// + public double OverhangAfter { get; init; } + + /// + /// Gets the distance that black pixels extend prior to the left leading alignment edge of the line. + /// + public double OverhangLeading { get; init; } + + /// + /// Gets the distance that black pixels extend following the right trailing alignment edge of the line. + /// + public double OverhangTrailing { get; init; } } } diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index b0d17f9c85..6f62c3be1d 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -169,9 +169,8 @@ namespace Avalonia.Platform /// The font rendering em size. /// The list of glyphs. /// The baseline origin of the run. Can be null. - /// the conservative bounding box of the run /// An . - IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos, Point baselineOrigin, Rect bounds); + IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos, Point baselineOrigin); /// /// Creates a backend-specific object using a low-level API graphics context diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index e4167425cd..6fb05463c0 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -341,7 +341,7 @@ namespace Avalonia.Controls.Presenters var top = 0d; var left = 0.0; - var textHeight = TextLayout.Bounds.Height; + var textHeight = TextLayout.Height; if (Bounds.Height < textHeight) { @@ -571,14 +571,14 @@ namespace Avalonia.Controls.Presenters InvalidateArrange(); - var measuredSize = TextLayout.Bounds.Size; + var textWidth = TextLayout.OverhangLeading + TextLayout.WidthIncludingTrailingWhitespace + TextLayout.OverhangTrailing; - return measuredSize; + return new Size(textWidth, TextLayout.Height); } protected override Size ArrangeOverride(Size finalSize) { - var textWidth = Math.Ceiling(TextLayout.Bounds.Width); + var textWidth = Math.Ceiling(TextLayout.OverhangLeading + TextLayout.WidthIncludingTrailingWhitespace + TextLayout.OverhangTrailing); if (finalSize.Width < textWidth) { diff --git a/src/Avalonia.Controls/SelectableTextBlock.cs b/src/Avalonia.Controls/SelectableTextBlock.cs index 830c0a671d..8b32638835 100644 --- a/src/Avalonia.Controls/SelectableTextBlock.cs +++ b/src/Avalonia.Controls/SelectableTextBlock.cs @@ -323,8 +323,8 @@ namespace Avalonia.Controls var point = e.GetPosition(this) - new Point(padding.Left, padding.Top); point = new Point( - MathUtilities.Clamp(point.X, 0, Math.Max(TextLayout.Bounds.Width, 0)), - MathUtilities.Clamp(point.Y, 0, Math.Max(TextLayout.Bounds.Height, 0))); + MathUtilities.Clamp(point.X, 0, Math.Max(TextLayout.WidthIncludingTrailingWhitespace, 0)), + MathUtilities.Clamp(point.Y, 0, Math.Max(TextLayout.Height, 0))); var hit = TextLayout.HitTestPoint(point); var textPosition = hit.TextPosition; diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 155d7d5f56..adef052db8 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -567,7 +567,7 @@ namespace Avalonia.Controls var scale = LayoutHelper.GetLayoutScale(this); var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale); var top = padding.Top; - var textHeight = TextLayout.Bounds.Height; + var textHeight = TextLayout.Height; if (Bounds.Height < textHeight) { @@ -588,7 +588,7 @@ namespace Avalonia.Controls protected virtual void RenderTextLayout(DrawingContext context, Point origin) { - TextLayout.Draw(context, origin); + TextLayout.Draw(context, origin + new Point(TextLayout.OverhangLeading, 0)); } private bool _clearTextInternal; @@ -702,7 +702,9 @@ namespace Avalonia.Controls } } - return TextLayout.Bounds.Size.Inflate(padding); + var width = TextLayout.OverhangLeading + TextLayout.WidthIncludingTrailingWhitespace + TextLayout.OverhangTrailing; + + return new Size(width, TextLayout.Height).Inflate(padding); } protected override Size ArrangeOverride(Size finalSize) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 9bb68ba419..2a8fdd30c7 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -19,6 +19,8 @@ using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Automation.Peers; using Avalonia.Threading; using Avalonia.Platform; +using System.Reflection; +using static System.Net.Mime.MediaTypeNames; namespace Avalonia.Controls { @@ -100,7 +102,7 @@ namespace Avalonia.Controls /// Defines the property /// public static readonly StyledProperty SelectionStartProperty = - AvaloniaProperty.Register(nameof(SelectionStart), + AvaloniaProperty.Register(nameof(SelectionStart), coerce: CoerceCaretIndex); /// @@ -475,7 +477,7 @@ namespace Avalonia.Controls get => GetValue(SelectionEndProperty); set => SetValue(SelectionEndProperty, value); } - + private void OnSelectionEndChanged(AvaloniaPropertyChangedEventArgs e) { UpdateCommandStates(); @@ -486,7 +488,7 @@ namespace Avalonia.Controls SetCurrentValue(CaretIndexProperty, value); } } - + /// /// Gets or sets the maximum character length of the TextBox /// @@ -536,7 +538,7 @@ namespace Avalonia.Controls private static string? CoerceText(AvaloniaObject sender, string? value) { var textBox = (TextBox)sender; - + // Before #9490, snapshot here was done AFTER text change - this doesn't make sense // since intial state would never be no text and you'd always have to make a text // change before undo would be available @@ -988,7 +990,7 @@ namespace Avalonia.Controls { var textBuilder = StringBuilderCache.Acquire(Math.Max(currentText.Length, newLength)); textBuilder.Append(currentText); - + var caretIndex = CaretIndex; if (selectionLength != 0) @@ -1050,7 +1052,7 @@ namespace Avalonia.Controls if (clipboard == null) return; - + await clipboard.SetTextAsync(text); DeleteSelection(); } @@ -1417,7 +1419,7 @@ namespace Avalonia.Controls protected override void OnPointerPressed(PointerPressedEventArgs e) { - if (_presenter == null ) + if (_presenter == null) { return; } @@ -1434,11 +1436,10 @@ namespace Avalonia.Controls _presenter.MoveCaretToPoint(point); - var index = _presenter.CaretIndex; - + var caretIndex = _presenter.CaretIndex; var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift); - - SetCurrentValue(CaretIndexProperty, index); + var selectionStart = SelectionStart; + var selectionEnd = SelectionEnd; switch (e.ClickCount) { @@ -1447,49 +1448,44 @@ namespace Avalonia.Controls { if (_wordSelectionStart >= 0) { - var previousWord = StringUtils.PreviousWord(text, index); - - if (index > _wordSelectionStart) - { - SetCurrentValue(SelectionEndProperty, StringUtils.NextWord(text, index)); - } + UpdateWordSelectionRange(caretIndex, ref selectionStart, ref selectionEnd); - if (index < _wordSelectionStart || previousWord == _wordSelectionStart) - { - SetCurrentValue(SelectionStartProperty, previousWord); - } + SetCurrentValue(SelectionStartProperty, selectionStart); + SetCurrentValue(SelectionEndProperty, selectionEnd); } else { - SetCurrentValue(SelectionStartProperty, Math.Min(oldIndex, index)); - SetCurrentValue(SelectionEndProperty, Math.Max(oldIndex, index)); + SetCurrentValue(SelectionEndProperty, caretIndex); } } else { - if(_wordSelectionStart == -1 || index < SelectionStart || index > SelectionEnd) - { - SetCurrentValue(SelectionStartProperty, index); - SetCurrentValue(SelectionEndProperty, index); - _wordSelectionStart = -1; - } + SetCurrentValue(SelectionStartProperty, caretIndex); + SetCurrentValue(SelectionEndProperty, caretIndex); + _wordSelectionStart = -1; } break; - case 2: + case 2: - if (!StringUtils.IsStartOfWord(text, index)) + if (!StringUtils.IsStartOfWord(text, caretIndex)) { - SetCurrentValue(SelectionStartProperty, StringUtils.PreviousWord(text, index)); + selectionStart = StringUtils.PreviousWord(text, caretIndex); } - _wordSelectionStart = SelectionStart; + if (!StringUtils.IsEndOfWord(text, caretIndex)) + { + selectionEnd = StringUtils.NextWord(text, caretIndex); + } - if (!StringUtils.IsEndOfWord(text, index)) + if (selectionStart != selectionEnd) { - SetCurrentValue(SelectionEndProperty, StringUtils.NextWord(text, index)); + _wordSelectionStart = selectionStart; } + SetCurrentValue(SelectionStartProperty, selectionStart); + SetCurrentValue(SelectionEndProperty, selectionEnd); + break; case 3: _wordSelectionStart = -1; @@ -1519,30 +1515,19 @@ namespace Avalonia.Controls MathUtilities.Clamp(point.X, 0, Math.Max(_presenter.Bounds.Width - 1, 0)), MathUtilities.Clamp(point.Y, 0, Math.Max(_presenter.Bounds.Height - 1, 0))); - _presenter.MoveCaretToPoint(point); + _presenter.MoveCaretToPoint(point); var caretIndex = _presenter.CaretIndex; + + var selectionStart = SelectionStart; + var selectionEnd = SelectionEnd; - var text = Text; - - if (text != null && _wordSelectionStart >= 0) + if (_wordSelectionStart >= 0) { - var distance = caretIndex - _wordSelectionStart; - - if (distance <= 0) - { - SetCurrentValue(SelectionStartProperty, StringUtils.PreviousWord(text, caretIndex)); - } + UpdateWordSelectionRange(caretIndex, ref selectionStart, ref selectionEnd); - if (distance >= 0) - { - if(SelectionStart != _wordSelectionStart) - { - SetCurrentValue(SelectionStartProperty, _wordSelectionStart); - } - - SetCurrentValue(SelectionEndProperty, StringUtils.NextWord(text, caretIndex)); - } + SetCurrentValue(SelectionStartProperty, selectionStart); + SetCurrentValue(SelectionEndProperty, selectionEnd); } else { @@ -1551,6 +1536,32 @@ namespace Avalonia.Controls } } + private void UpdateWordSelectionRange(int caretIndex, ref int selectionStart, ref int selectionEnd) + { + var text = Text; + + if (string.IsNullOrEmpty(text)) + { + return; + } + + if (caretIndex > _wordSelectionStart) + { + var nextWord = StringUtils.NextWord(text, caretIndex); + + selectionEnd = nextWord; + + selectionStart = _wordSelectionStart; + } + else + { + var previousWord = StringUtils.PreviousWord(text, caretIndex); + selectionStart = previousWord; + + selectionEnd = StringUtils.NextWord(text, _wordSelectionStart); + } + } + protected override void OnPointerReleased(PointerReleasedEventArgs e) { if (_presenter == null) diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index ab157f8062..138721b2f7 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -126,10 +126,9 @@ namespace Avalonia.Headless IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos, - Point baselineOrigin, - Rect bounds) + Point baselineOrigin) { - return new HeadlessGlyphRunStub(glyphTypeface, fontRenderingEmSize, baselineOrigin, bounds); + return new HeadlessGlyphRunStub(glyphTypeface, fontRenderingEmSize, baselineOrigin); } internal class HeadlessGlyphRunStub : IGlyphRunImpl @@ -137,13 +136,11 @@ namespace Avalonia.Headless public HeadlessGlyphRunStub( IGlyphTypeface glyphTypeface, double fontRenderingEmSize, - Point baselineOrigin, - Rect bounds) + Point baselineOrigin) { GlyphTypeface = glyphTypeface; FontRenderingEmSize = fontRenderingEmSize; BaselineOrigin = baselineOrigin; - Bounds =bounds; } public Rect Bounds { get; } diff --git a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs index 7331740b56..f6d84f0b12 100644 --- a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers; using System.Collections.Generic; using Avalonia.Media; using Avalonia.Media.TextFormatting; @@ -16,7 +17,7 @@ namespace Avalonia.Skia private readonly Dictionary _textBlobCache = new(1); public GlyphRunImpl(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, - IReadOnlyList glyphInfos, Point baselineOrigin, Rect bounds) + IReadOnlyList glyphInfos, Point baselineOrigin) { if (glyphTypeface == null) { @@ -48,9 +49,30 @@ namespace Avalonia.Skia currentX += glyphInfos[i].GlyphAdvance; } + _glyphTypefaceImpl.SKFont.Size = (float)fontRenderingEmSize; + + var runBounds = new Rect(); + var glyphBounds = ArrayPool.Shared.Rent(glyphInfos.Count); + + _glyphTypefaceImpl.SKFont.GetGlyphWidths(_glyphIndices, null, glyphBounds); + + currentX = 0; + + for (var i = 0; i < glyphInfos.Count; i++) + { + var gBounds = glyphBounds[i]; + var advance = glyphInfos[i].GlyphAdvance; + + runBounds = runBounds.Union(new Rect(currentX + gBounds.Left, baselineOrigin.Y + gBounds.Top, gBounds.Width, gBounds.Height)); + + currentX += advance; + } + + ArrayPool.Shared.Return(glyphBounds); + FontRenderingEmSize = fontRenderingEmSize; BaselineOrigin = baselineOrigin; - Bounds = bounds; + Bounds = runBounds; } public IGlyphTypeface GlyphTypeface => _glyphTypefaceImpl; @@ -83,16 +105,12 @@ namespace Avalonia.Skia return textBlob; } - var font = SKFontCache.Shared.Get(); + var font = _glyphTypefaceImpl.SKFont; - font.LinearMetrics = true; + font.Hinting = SKFontHinting.Full; font.Subpixel = edging == SKFontEdging.SubpixelAntialias; font.Edging = edging; - font.Hinting = SKFontHinting.Full; font.Size = (float)FontRenderingEmSize; - font.Typeface = _glyphTypefaceImpl.Typeface; - font.Embolden = (_glyphTypefaceImpl.FontSimulations & FontSimulations.Bold) != 0; - font.SkewX = (_glyphTypefaceImpl.FontSimulations & FontSimulations.Oblique) != 0 ? -0.2f : 0; var builder = SKTextBlobBuilderCache.Shared.Get(); @@ -101,8 +119,6 @@ namespace Avalonia.Skia runBuffer.SetPositions(_glyphPositions); runBuffer.SetGlyphs(_glyphIndices); - SKFontCache.Shared.Return(font); - textBlob = builder.Build(); SKTextBlobBuilderCache.Shared.Return(builder); diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs index 8b13cae802..abadc2624a 100644 --- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs @@ -9,24 +9,28 @@ namespace Avalonia.Skia internal class GlyphTypefaceImpl : IGlyphTypeface { private bool _isDisposed; + private readonly SKTypeface _typeface; public GlyphTypefaceImpl(SKTypeface typeface, FontSimulations fontSimulations) { - Typeface = typeface ?? throw new ArgumentNullException(nameof(typeface)); + _typeface = typeface ?? throw new ArgumentNullException(nameof(typeface)); + + SKFont = new SKFont(typeface) + { + LinearMetrics = true, + Embolden = (fontSimulations & FontSimulations.Bold) != 0, + SkewX = (fontSimulations & FontSimulations.Oblique) != 0 ? -0.2f : 0 + }; Face = new Face(GetTable) { - UnitsPerEm = Typeface.UnitsPerEm + UnitsPerEm = typeface.UnitsPerEm }; Font = new Font(Face); Font.SetFunctionsOpenType(); - var metrics = Typeface.ToFont().Metrics; - - const double defaultFontRenderingEmSize = 12.0; - Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.HorizontalAscender, out var ascent); Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.HorizontalDescender, out var descent); Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.HorizontalLineGap, out var lineGap); @@ -34,10 +38,10 @@ namespace Avalonia.Skia Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutSize, out var strikethroughSize); Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineOffset, out var underlineOffset); Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineSize, out var underlineSize); - + Metrics = new FontMetrics { - DesignEmHeight = (short)Typeface.UnitsPerEm, + DesignEmHeight = (short)Face.UnitsPerEm, Ascent = -ascent, Descent = -descent, LineGap = lineGap, @@ -45,25 +49,25 @@ namespace Avalonia.Skia UnderlineThickness = underlineSize, StrikethroughPosition = -strikethroughOffset, StrikethroughThickness = strikethroughSize, - IsFixedPitch = Typeface.IsFixedPitch + IsFixedPitch = typeface.IsFixedPitch }; - GlyphCount = Typeface.GlyphCount; + GlyphCount = typeface.GlyphCount; FontSimulations = fontSimulations; - Weight = (FontWeight)Typeface.FontWeight; + Weight = (FontWeight)typeface.FontWeight; - Style = Typeface.FontSlant.ToAvalonia(); + Style = typeface.FontSlant.ToAvalonia(); - Stretch = (FontStretch)Typeface.FontStyle.Width; + Stretch = (FontStretch)typeface.FontStyle.Width; } public Face Face { get; } public Font Font { get; } - public SKTypeface Typeface { get; } + public SKFont SKFont { get; } public FontSimulations FontSimulations { get; } @@ -73,7 +77,7 @@ namespace Avalonia.Skia public int GlyphCount { get; } - public string FamilyName => Typeface.FamilyName; + public string FamilyName => _typeface.FamilyName; public FontWeight Weight { get; } @@ -89,7 +93,7 @@ namespace Avalonia.Skia { return false; } - + metrics = new GlyphMetrics { XBearing = extents.XBearing, @@ -97,7 +101,7 @@ namespace Avalonia.Skia Width = extents.Width, Height = extents.Height }; - + return true; } @@ -156,13 +160,13 @@ namespace Avalonia.Skia private Blob? GetTable(Face face, Tag tag) { - var size = Typeface.GetTableSize(tag); + var size = _typeface.GetTableSize(tag); var data = Marshal.AllocCoTaskMem(size); var releaseDelegate = new ReleaseDelegate(() => Marshal.FreeCoTaskMem(data)); - return Typeface.TryGetTableData(tag, 0, size, data) ? + return _typeface.TryGetTableData(tag, 0, size, data) ? new Blob(data, size, MemoryMode.ReadOnly, releaseDelegate) : null; } @@ -192,7 +196,7 @@ namespace Avalonia.Skia public bool TryGetTable(uint tag, out byte[] table) { - return Typeface.TryGetTableData(tag, out table); + return _typeface.TryGetTableData(tag, out table); } } } diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 9e912db797..f0a25cc1cb 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -77,13 +77,10 @@ namespace Avalonia.Skia var fontRenderingEmSize = (float)glyphRun.FontRenderingEmSize; - var skFont = SKFontCache.Shared.Get(); + var skFont = glyphTypeface.SKFont; - skFont.Typeface = glyphTypeface.Typeface; skFont.Size = fontRenderingEmSize; - skFont.Edging = SKFontEdging.Alias; skFont.Hinting = SKFontHinting.None; - skFont.LinearMetrics = true; SKPath path = new SKPath(); @@ -102,8 +99,6 @@ namespace Avalonia.Skia currentX += glyphRun.GlyphInfos[i].GlyphAdvance; } - SKFontCache.Shared.Return(skFont); - return new StreamGeometryImpl(path, path); } @@ -201,9 +196,9 @@ namespace Avalonia.Skia } public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, - IReadOnlyList glyphInfos, Point baselineOrigin, Rect bounds) + IReadOnlyList glyphInfos, Point baselineOrigin) { - return new GlyphRunImpl(glyphTypeface, fontRenderingEmSize, glyphInfos, baselineOrigin, bounds); + return new GlyphRunImpl(glyphTypeface, fontRenderingEmSize, glyphInfos, baselineOrigin); } } } diff --git a/src/Skia/Avalonia.Skia/SKFontCache.cs b/src/Skia/Avalonia.Skia/SKFontCache.cs deleted file mode 100644 index 348e085253..0000000000 --- a/src/Skia/Avalonia.Skia/SKFontCache.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Collections.Concurrent; -using SkiaSharp; - -namespace Avalonia.Skia -{ - /// - /// Cache for SKFonts. - /// - internal class SKFontCache : SKCacheBase - { - - } -} diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index fc1a358492..7750a8ed4e 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -160,9 +160,9 @@ namespace Avalonia.Direct2D1 public IStreamGeometryImpl CreateStreamGeometry() => new StreamGeometryImpl(); public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => new GeometryGroupImpl(fillRule, children); public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) => new CombinedGeometryImpl(combineMode, g1, g2); - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos, Point baselineOrigin, Rect bounds) + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos, Point baselineOrigin) { - return new GlyphRunImpl(glyphTypeface, fontRenderingEmSize, glyphInfos, baselineOrigin, bounds); + return new GlyphRunImpl(glyphTypeface, fontRenderingEmSize, glyphInfos, baselineOrigin); } class D2DApi : IPlatformRenderInterfaceContext diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs index df147c4525..875164df96 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs @@ -20,13 +20,12 @@ namespace Avalonia.Direct2D1.Media private SharpDX.DirectWrite.GlyphRun? _glyphRun; public GlyphRunImpl(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, - IReadOnlyList glyphInfos, Point baselineOrigin, Rect bounds) + IReadOnlyList glyphInfos, Point baselineOrigin) { _glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface; FontRenderingEmSize = fontRenderingEmSize; BaselineOrigin = baselineOrigin; - Bounds = bounds; var glyphCount = glyphInfos.Count; @@ -62,6 +61,12 @@ namespace Avalonia.Direct2D1.Media AscenderOffset = (float)y }; } + + var scale = fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight; + + var height = glyphTypeface.Metrics.LineSpacing * scale; + + Bounds = new Rect(baselineOrigin.X, 0, width, height); } public SharpDX.DirectWrite.GlyphRun GlyphRun diff --git a/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs b/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs index 4210ee8238..1a61a10919 100644 --- a/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs +++ b/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs @@ -121,7 +121,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls Decorator target = new Decorator { Padding = new Thickness(8), - Width = 180, + Width = 190, Height = 80, Child = new StackPanel() diff --git a/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs index 65fd670415..ea47cf0c68 100644 --- a/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs @@ -95,8 +95,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media { var fmt = Create(input, fontSize); - Assert.Equal(expWidth, fmt.Bounds.Width, 2); - Assert.Equal(expHeight, fmt.Bounds.Height, 2); + Assert.Equal(expWidth, fmt.WidthIncludingTrailingWhitespace, 2); + Assert.Equal(expHeight, fmt.Height, 2); } [Theory] @@ -253,8 +253,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media Assert.Equal(exr.Height, r.Height, 2); } } - - [Fact] + + [Fact] public async Task TextLayout_Basic() { // Skip test on OSX: text rendering is subtly different. @@ -274,7 +274,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media Background = Brushes.White, Child = new DrawnControl(c => { - var textRect = t.Bounds; + var textRect = new Rect(0, 0, t.WidthIncludingTrailingWhitespace, t.Height); var bounds = new Rect(0, 0, 200, 200); var rect = bounds.CenterRect(textRect); c.DrawRectangle(Brushes.Yellow, null, rect); @@ -306,7 +306,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media Background = Brushes.White, Child = new DrawnControl(c => { - var textRect = t.Bounds; + var textRect = new Rect(0, 0, t.WidthIncludingTrailingWhitespace, t.Height); var bounds = new Rect(0, 0, 200, 200); var rect = bounds.CenterRect(textRect); var rotate = Matrix.CreateTranslation(-100, -100) * diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index 543dd0805e..71aeb4397e 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -597,7 +597,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(1, layout.TextLines.Count); - Assert.Equal(lineHeight, layout.Bounds.Height); + Assert.Equal(lineHeight, layout.Height); } } @@ -725,7 +725,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var selectedRect = rects[0]; - Assert.Equal(selectedText.Bounds.Width, selectedRect.Width, 2); + Assert.Equal(selectedText.WidthIncludingTrailingWhitespace, selectedRect.Width, 2); } } @@ -847,7 +847,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting 12, Brushes.Black); - Assert.True(layout.Bounds.Height > 0); + Assert.True(layout.Height > 0); } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index 1feaefbcc1..aa5d707d0f 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -708,6 +708,28 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [Fact] + public void Should_Get_CharacterHit_For_Distance_With_TextEndOfLine() + { + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + + var textSource = new SingleBufferTextSource("Hello World", defaultProperties, true); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, 1000, + new GenericTextParagraphProperties(defaultProperties)); + + var characterHit = textLine.GetCharacterHitFromDistance(1000); + + Assert.Equal(10, characterHit.FirstCharacterIndex); + Assert.Equal(1, characterHit.TrailingLength); + } + } + private class MixedTextBufferTextSource : ITextSource { public TextRun? GetTextRun(int textSourceIndex) diff --git a/tests/TestFiles/Direct2D1/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png b/tests/TestFiles/Direct2D1/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png index 8ca7acb845..c2466dcd8e 100644 Binary files a/tests/TestFiles/Direct2D1/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png and b/tests/TestFiles/Direct2D1/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png differ diff --git a/tests/TestFiles/Skia/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png b/tests/TestFiles/Skia/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png index e5a74cf0e6..c8320cdb97 100644 Binary files a/tests/TestFiles/Skia/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png and b/tests/TestFiles/Skia/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png differ