diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 9882d9eb9c..d6e7375772 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -1306,9 +1306,16 @@ namespace Avalonia.Media.TextFormatting case DrawableTextRun drawableTextRun: { - if (drawableTextRun.Size.Height > -ascent) + if (drawableTextRun.Baseline > -ascent) { - ascent = -drawableTextRun.Size.Height; + ascent = -drawableTextRun.Baseline; + } + + var bottom = drawableTextRun.Size.Height - drawableTextRun.Baseline; + + if (bottom > descent) + { + descent = bottom; } break; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index b75cdf96cd..faacaa7d6d 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -905,7 +905,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.NotNull(textLine.TextLineBreak.TextEndOfLine); } } - + [Fact] public void Should_HitTestStringWithInvisibleRuns() { @@ -913,7 +913,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties); //var textSource = new ListTextSource( - + using (Start()) { @@ -923,7 +923,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Red)); var source = new ListTextSource(new InvisibleRun(1), hello, new InvisibleRun(1), world); - + var textLine = TextFormatter.Current.FormatLine(source, 0, double.PositiveInfinity, paragraphProperties); @@ -939,7 +939,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting VerifyHit(8); } } - + [Fact] public void GetTextBounds_For_TextLine_With_ZeroWidthSpaces_Does_Not_Freeze() { @@ -952,7 +952,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black)); var source = new ListTextSource(text, new InvisibleRun(1), new TextEndOfParagraph()); - + var textLine = TextFormatter.Current.FormatLine(source, 0, double.PositiveInfinity, paragraphProperties); @@ -968,9 +968,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } - + [Theory] - [InlineData(TextWrapping.NoWrap),InlineData(TextWrapping.Wrap),InlineData(TextWrapping.WrapWithOverflow)] + [InlineData(TextWrapping.NoWrap), InlineData(TextWrapping.Wrap), InlineData(TextWrapping.WrapWithOverflow)] public void Line_Formatting_For_Oversized_Embedded_Runs_Does_Not_Produce_Empty_Lines(TextWrapping wrapping) { var defaultRunProperties = new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black); @@ -985,25 +985,25 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(200d, textLine.WidthIncludingTrailingWhitespace); } } - + [Theory] - [InlineData(TextWrapping.NoWrap),InlineData(TextWrapping.Wrap),InlineData(TextWrapping.WrapWithOverflow)] + [InlineData(TextWrapping.NoWrap), InlineData(TextWrapping.Wrap), InlineData(TextWrapping.WrapWithOverflow)] public void Line_Formatting_For_Oversized_Embedded_Runs_Inside_Normal_Text_Does_Not_Produce_Empty_Lines( TextWrapping wrapping) { var defaultRunProperties = new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black); var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties, textWrapping: wrapping); - + using (Start()) { var typeface = new Typeface(FontFamily.Parse("resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#DejaVu Sans")); - + var text1 = new TextCharacters("Hello", new GenericTextRunProperties(typeface, foregroundBrush: Brushes.Black)); var text2 = new TextCharacters("world", new GenericTextRunProperties(typeface, foregroundBrush: Brushes.Black)); - + var source = new ListTextSource( text1, new RectangleRun(new Rect(0, 0, 200, 10), Brushes.Aqua), @@ -1014,15 +1014,15 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var lines = new List(); var dcp = 0; - for (var c = 0;; c++) + for (var c = 0; ; c++) { Assert.True(c < 1000, "Infinite loop"); var textLine = TextFormatter.Current.FormatLine(source, dcp, 30, paragraphProperties); Assert.NotNull(textLine); lines.Add(textLine); dcp += textLine.Length; - - if (textLine.TextLineBreak is {} eol && eol.TextEndOfLine is TextEndOfParagraph) + + if (textLine.TextLineBreak is { } eol && eol.TextEndOfLine is TextEndOfParagraph) break; } @@ -1046,25 +1046,25 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting public override double Indent => default; public override double DefaultIncrementalTab => 64; } - + [Fact] public void Line_With_IncrementalTab_Should_Return_Correct_Backspace_Position() { using (Start()) { var typeface = new Typeface(FontFamily.Parse("resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#DejaVu Sans")); - + var defaultRunProperties = new GenericTextRunProperties(typeface, foregroundBrush: Brushes.Black); var paragraphProperties = new IncrementalTabProperties(defaultRunProperties); var text = new TextCharacters("ff", new GenericTextRunProperties(typeface, foregroundBrush: Brushes.Black)); - + var source = new ListTextSource(text); - + var textLine = TextFormatter.Current.FormatLine(source, 0, double.PositiveInfinity, paragraphProperties); Assert.NotNull(textLine); - + var backspaceHit = textLine.GetBackspaceCaretCharacterHit(new CharacterHit(2)); Assert.Equal(1, backspaceHit.FirstCharacterIndex); Assert.Equal(0, backspaceHit.TrailingLength); @@ -1114,6 +1114,102 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [Fact] + public void DrawableRun_With_Same_Baseline_And_Size_Should_Not_Alter_LineHeight() + { + using (Start()) + { + var text = "ABC"; + + var typeface = new Typeface(new FontFamily(new Uri("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests"), "Noto Mono")); + var defaultRunProperties = new GenericTextRunProperties(typeface); + var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties, textWrapping: TextWrapping.Wrap); + + var embeddedTextLine = TextFormatter.Current.FormatLine(new SimpleTextSource(text, defaultRunProperties), 0, 120, paragraphProperties); + + Assert.NotNull(embeddedTextLine); + + var expectedHeight = embeddedTextLine.Height; + var expectedBaseline = embeddedTextLine.Baseline; + + var textSource = new ListTextSource(new TextCharacters("ABC", defaultRunProperties), new EmbeddedTextLineRun(embeddedTextLine)); + + var textLine = TextFormatter.Current.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProperties); + + Assert.NotNull(textLine); + + Assert.Equal(expectedHeight, textLine.Height); + + Assert.Equal(expectedBaseline, textLine.Baseline); + } + } + + [Fact] + public void DrawableRun_With_Same_Baseline_And_BiggerHeight_Should_Not_Alter_Baseline() + { + using (Start()) + { + var text = "ABC"; + + var typeface = new Typeface(new FontFamily(new Uri("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests"), "Noto Mono")); + var defaultRunProperties = new GenericTextRunProperties(typeface); + var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties, textWrapping: TextWrapping.Wrap); + + var embeddedTextLine = TextFormatter.Current.FormatLine(new SimpleTextSource(text, defaultRunProperties), 0, 120, paragraphProperties); + + Assert.NotNull(embeddedTextLine); + + var expectedHeight = embeddedTextLine.Height + 10; + + var embeddedSize = new Size(embeddedTextLine.Width, expectedHeight); + + var expectedBaseline = embeddedTextLine.Baseline; + + var textSource = new ListTextSource(new TextCharacters("ABC", defaultRunProperties), new CustomDrawableRun(embeddedSize, expectedBaseline)); + + var textLine = TextFormatter.Current.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProperties); + + Assert.NotNull(textLine); + + Assert.Equal(expectedHeight, textLine.Height); + + Assert.Equal(expectedBaseline, textLine.Baseline); + } + } + + private class CustomDrawableRun : DrawableTextRun + { + public CustomDrawableRun(Size size, double baseLine) + { + Size = size; + Baseline = baseLine; + } + + public override Size Size { get; } + + public override double Baseline { get; } + + public override void Draw(DrawingContext drawingContext, Point origin) + { + // no op + } + } + + private class EmbeddedTextLineRun : DrawableTextRun + { + private readonly TextLine _textLine; + public EmbeddedTextLineRun(TextLine textLine) + { + _textLine = textLine; + } + public override Size Size => new Size(_textLine.Width, _textLine.Height); + public override double Baseline => _textLine.Baseline; + public override void Draw(DrawingContext drawingContext, Point origin) + { + _textLine.Draw(drawingContext, origin); + } + } + protected readonly record struct SimpleTextSource : ITextSource { private readonly string _text; @@ -1183,21 +1279,21 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting return new TextCharacters(_text, new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black)); } } - + internal class ListTextSource : ITextSource { private readonly List _runs; public ListTextSource(params TextRun[] runs) : this((IEnumerable)runs) { - + } - + public ListTextSource(IEnumerable runs) { _runs = runs.ToList(); } - + public TextRun? GetTextRun(int textSourceIndex) { var off = 0; @@ -1240,7 +1336,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } } - + private class InvisibleRun : TextRun { public InvisibleRun(int length)