Browse Source

[Text] Properly handle DrawableRun.Baseline (#19896)

* Add failing test

* Also handle decent
Benedikt Stebner 5 days ago
parent
commit
8c8545bc52

+ 9 - 2
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;

+ 121 - 25
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<TextLine>();
                 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<TextRun> _runs;
 
             public ListTextSource(params TextRun[] runs) : this((IEnumerable<TextRun>)runs)
             {
-                
+
             }
-            
+
             public ListTextSource(IEnumerable<TextRun> 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)