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