Browse Source

Merge branch 'master' into extended-file-picker

pull/19783/head
Jumar Macato 3 months ago
committed by GitHub
parent
commit
d49334a0e3
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 8
      src/Avalonia.Base/Media/FontManager.cs
  2. 6
      src/Avalonia.Base/Media/GlyphRun.cs
  3. 2
      src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
  4. 148
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  5. 7
      src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs
  6. 5
      src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml
  7. 4
      src/Avalonia.Themes.Fluent/Controls/RadioButton.xaml
  8. 2
      src/Avalonia.Themes.Fluent/DensityStyles/Compact.xaml
  9. BIN
      tests/Avalonia.Skia.UnitTests/Fonts/Inter-Regular.LineGap800.ttf
  10. 51
      tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs
  11. 138
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

8
src/Avalonia.Base/Media/FontManager.cs

@ -163,7 +163,7 @@ namespace Avalonia.Media
}
//Nothing was found so use the default
return TryGetGlyphTypeface(new Typeface(FontFamily.DefaultFontFamilyName, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface);
return TryGetGlyphTypeface(new Typeface(DefaultFontFamily, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface);
FontFamily GetMappedFontFamily(FontFamily fontFamily)
{
@ -380,6 +380,12 @@ namespace Avalonia.Media
"Default font family name can't be null or empty.");
}
if (defaultFontFamilyName == FontFamily.DefaultFontFamilyName)
{
throw new InvalidOperationException(
$"'{FontFamily.DefaultFontFamilyName}' is a placeholder and cannot be used as the default font family name. Provide a concrete font family name via {nameof(FontManagerOptions)} or the platform implementation.");
}
return defaultFontFamilyName;
}

6
src/Avalonia.Base/Media/GlyphRun.cs

@ -708,9 +708,13 @@ namespace Avalonia.Media
}
}
var ascent = GlyphTypeface.Metrics.Ascent * Scale;
var lineGap = GlyphTypeface.Metrics.LineGap * Scale;
var baseline = -ascent + lineGap * 0.5;
return new GlyphRunMetrics
{
Baseline = -GlyphTypeface.Metrics.Ascent * Scale,
Baseline = baseline,
Width = width,
WidthIncludingTrailingWhitespace = widthIncludingTrailingWhitespace,
Height = height,

2
src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs

@ -36,7 +36,7 @@ namespace Avalonia.Media.TextFormatting
public TextMetrics TextMetrics { get; }
public override double Baseline => -TextMetrics.Ascent;
public override double Baseline => -TextMetrics.Ascent + TextMetrics.LineGap * 0.5;
public override Size Size => GlyphRun.Bounds.Size;

148
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@ -580,7 +580,7 @@ namespace Avalonia.Media.TextFormatting
/// </returns>
private int GetLastDirectionalRunIndex(int indexedRunIndex, FlowDirection flowDirection, ref double directionalWidth)
{
if(_indexedTextRuns is null)
if (_indexedTextRuns is null)
{
return -1;
}
@ -624,7 +624,7 @@ namespace Avalonia.Media.TextFormatting
public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceIndex, int textLength)
{
if(textLength == 0)
if (textLength == 0)
{
throw new ArgumentOutOfRangeException(nameof(textLength), textLength, $"{nameof(textLength)} ('0') must be a non-zero value. ");
}
@ -643,7 +643,7 @@ namespace Avalonia.Media.TextFormatting
var indexedTextRun = _indexedTextRuns[0];
var currentDirection = GetRunDirection(indexedTextRun.TextRun, _resolvedFlowDirection);
return [new TextBounds(new Rect(0,0,0, Height), currentDirection, [])];
return [new TextBounds(new Rect(0, 0, 0, Height), currentDirection, [])];
}
//We can return early if the requested text range is after the line's text range.
@ -667,7 +667,7 @@ namespace Avalonia.Media.TextFormatting
{
break;
}
var currentTextRun = currentIndexedRun.TextRun;
if (currentTextRun == null)
@ -691,7 +691,7 @@ namespace Avalonia.Media.TextFormatting
{
directionalWidth = currentDrawable.Size.Width;
}
var firstRunIndex = currentIndexedRun.RunIndex;
var lastRunIndex = GetLastDirectionalRunIndex(indexedRunIndex, currentDirection, ref directionalWidth);
@ -709,8 +709,8 @@ namespace Avalonia.Media.TextFormatting
}
default:
{
currentBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex,
currentPosition, remainingLength, out coveredLength, out currentPosition);
currentBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex,
currentPosition, remainingLength, out coveredLength, out currentPosition);
break;
}
@ -729,7 +729,7 @@ namespace Avalonia.Media.TextFormatting
lastBounds = currentBounds;
if(coveredLength <= 0)
if (coveredLength <= 0)
{
throw new InvalidOperationException("Covered length must be greater than zero.");
}
@ -814,7 +814,7 @@ namespace Avalonia.Media.TextFormatting
}
}
private CharacterHit GetPreviousCharacterHit(CharacterHit characterHit, bool useGraphemeBoundaries)
private CharacterHit GetPreviousCharacterHit(CharacterHit characterHit, bool isBackspaceDelete)
{
if (_textRuns.Length == 0 || _indexedTextRuns is null)
{
@ -833,8 +833,6 @@ namespace Avalonia.Media.TextFormatting
return new CharacterHit(FirstTextSourceIndex);
}
var currentCharacterHit = characterHit;
var currentRun = GetRunAtCharacterIndex(characterIndex, LogicalDirection.Backward, out var currentPosition);
var previousCharacterHit = characterHit;
@ -843,46 +841,38 @@ namespace Avalonia.Media.TextFormatting
{
case ShapedTextRun shapedRun:
{
var offset = Math.Max(0, currentPosition - shapedRun.GlyphRun.Metrics.FirstCluster);
//Determine the start of the first hit in local positions.
var runOffset = Math.Max(0, characterIndex - currentPosition);
if (offset > 0)
{
currentCharacterHit = new CharacterHit(Math.Max(0, characterHit.FirstCharacterIndex - offset), characterHit.TrailingLength);
}
var firstCluster = shapedRun.GlyphRun.Metrics.FirstCluster;
previousCharacterHit = shapedRun.GlyphRun.GetPreviousCaretCharacterHit(currentCharacterHit);
//Current position is a text source index and first cluster is relative to the GlyphRun's buffer.
var textSourceOffset = currentPosition - firstCluster;
if (useGraphemeBoundaries)
if (isBackspaceDelete)
{
var textPosition = Math.Max(0, previousCharacterHit.FirstCharacterIndex - shapedRun.GlyphRun.Metrics.FirstCluster);
var text = shapedRun.GlyphRun.Characters.Slice(textPosition);
var graphemeEnumerator = new GraphemeEnumerator(text.Span);
var length = 0;
var clusterLength = Math.Max(0, currentCharacterHit.FirstCharacterIndex + currentCharacterHit.TrailingLength -
previousCharacterHit.FirstCharacterIndex - previousCharacterHit.TrailingLength);
while (graphemeEnumerator.MoveNext(out var grapheme))
while (Codepoint.ReadAt(shapedRun.GlyphRun.Characters.Span, length, out var count) != Codepoint.ReplacementCodepoint)
{
if (length + grapheme.Length < clusterLength)
if (length + count >= runOffset)
{
length += grapheme.Length;
continue;
break;
}
previousCharacterHit = new CharacterHit(previousCharacterHit.FirstCharacterIndex + length);
break;
length += count;
}
}
if (offset > 0)
previousCharacterHit = new CharacterHit(characterIndex - runOffset + length);
}
else
{
previousCharacterHit = new CharacterHit(previousCharacterHit.FirstCharacterIndex + offset, previousCharacterHit.TrailingLength);
previousCharacterHit = shapedRun.GlyphRun.GetPreviousCaretCharacterHit(new CharacterHit(firstCluster + runOffset));
if(textSourceOffset > 0)
{
previousCharacterHit = new CharacterHit(textSourceOffset + previousCharacterHit.FirstCharacterIndex, previousCharacterHit.TrailingLength);
}
}
break;
@ -998,14 +988,14 @@ namespace Avalonia.Media.TextFormatting
{
var runBounds = GetRunBounds(shapedTextRun, endX, firstTextSourceIndex, remainingLength, currentPosition);
if(runBounds.TextSourceCharacterIndex < FirstTextSourceIndex + Length)
if (runBounds.TextSourceCharacterIndex < FirstTextSourceIndex + Length)
{
textRunBounds.Add(runBounds);
}
currentPosition = runBounds.TextSourceCharacterIndex + runBounds.Length;
if(i == firstRunIndex)
if (i == firstRunIndex)
{
startX = runBounds.Rectangle.Left;
}
@ -1122,7 +1112,7 @@ namespace Avalonia.Media.TextFormatting
var startHitIndex = startHit.FirstCharacterIndex;
//If the requested text range starts at the trailing edge we need to move at the end of the hit
if(startHitIndex < startIndex)
if (startHitIndex < startIndex)
{
startHitIndex += startHit.TrailingLength;
}
@ -1240,7 +1230,7 @@ namespace Avalonia.Media.TextFormatting
}
case not null:
{
if(direction == LogicalDirection.Forward)
if (direction == LogicalDirection.Forward)
{
if (textPosition == codepointIndex)
{
@ -1326,8 +1316,6 @@ namespace Avalonia.Media.TextFormatting
}
}
var height = descent - ascent + lineGap;
var inkBounds = new Rect();
for (var index = 0; index < _textRuns.Length; index++)
@ -1335,31 +1323,53 @@ namespace Avalonia.Media.TextFormatting
switch (_textRuns[index])
{
case ShapedTextRun textRun:
{
var glyphRun = textRun.GlyphRun;
//Align the ink bounds at the common baseline
var offsetY = -ascent - textRun.Baseline;
{
var glyphRun = textRun.GlyphRun;
//Align the ink bounds at the common baseline
var offsetY = -ascent - textRun.Baseline;
var runBounds = glyphRun.InkBounds.Translate(new Vector(widthIncludingWhitespace, offsetY));
var runBounds = glyphRun.InkBounds.Translate(new Vector(widthIncludingWhitespace, offsetY));
inkBounds = inkBounds.Union(runBounds);
inkBounds = inkBounds.Union(runBounds);
widthIncludingWhitespace += textRun.Size.Width;
widthIncludingWhitespace += textRun.Size.Width;
break;
}
break;
}
case DrawableTextRun drawableTextRun:
{
//Align the bounds at the common baseline
var offsetY = -ascent - drawableTextRun.Baseline;
{
//Align the bounds at the common baseline
var offsetY = -ascent - drawableTextRun.Baseline;
inkBounds = inkBounds.Union(new Rect(new Point(widthIncludingWhitespace, offsetY), drawableTextRun.Size));
inkBounds = inkBounds.Union(new Rect(new Point(widthIncludingWhitespace, offsetY), drawableTextRun.Size));
widthIncludingWhitespace += drawableTextRun.Size.Width;
break;
}
widthIncludingWhitespace += drawableTextRun.Size.Width;
break;
}
}
}
var halfLineGap = lineGap * 0.5;
var naturalHeight = descent - ascent + lineGap;
var baseline = -ascent + halfLineGap;
var height = naturalHeight;
if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight))
{
if (lineHeight <= naturalHeight)
{
//Clamp to the specified line height
height = lineHeight;
baseline = -ascent;
}
else
{
// Center the text vertically within the specified line height
height = lineHeight;
var extra = lineHeight - (descent - ascent);
baseline = -ascent + extra / 2;
}
}
@ -1395,24 +1405,14 @@ namespace Avalonia.Media.TextFormatting
}
var extent = inkBounds.Height;
//The width of overhanging pixels at the bottom
var overhangAfter = inkBounds.Bottom - height;
//The height of overhanging pixels at the bottom
var overhangAfter = inkBounds.Bottom - height + halfLineGap;
//The width of overhanging pixels at the natural alignment point. Positive value means we are inside.
var overhangLeading = inkBounds.Left;
//The width of overhanging pixels at the end of the natural bounds. Positive value means we are inside.
var overhangTrailing = widthIncludingWhitespace - inkBounds.Right;
var hasOverflowed = width > _paragraphWidth;
if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight))
{
//Center the line
var offset = (height - lineHeight) / 2;
ascent += offset;
height = lineHeight;
}
var start = GetParagraphOffsetX(width, widthIncludingWhitespace);
_inkBounds = inkBounds.Translate(new Vector(start, 0));
@ -1426,7 +1426,7 @@ namespace Avalonia.Media.TextFormatting
Extent = extent,
NewlineLength = newLineLength,
Start = start,
TextBaseline = -ascent,
TextBaseline = baseline,
TrailingWhitespaceLength = trailingWhitespaceLength,
Width = width,
WidthIncludingTrailingWhitespace = widthIncludingWhitespace,

7
src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs

@ -19,6 +19,8 @@
LineGap = fontMetrics.LineGap * scale;
Baseline = -Ascent + LineGap * 0.5;
LineHeight = Descent - Ascent + LineGap;
UnderlineThickness = fontMetrics.UnderlineThickness * scale;
@ -35,6 +37,11 @@
/// </summary>
public double FontRenderingEmSize { get; }
/// <summary>
/// Gets the distance from the top to the baseline of the line of text.
/// </summary>
public double Baseline { get; }
/// <summary>
/// Gets the recommended distance above the baseline.
/// </summary>

5
src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml

@ -13,6 +13,7 @@
</Design.PreviewWith>
<StreamGeometry x:Key="CheckMarkPathData">M5.5 10.586 1.707 6.793A1 1 0 0 0 .293 8.207l4.5 4.5a1 1 0 0 0 1.414 0l11-11A1 1 0 0 0 15.793.293L5.5 10.586Z</StreamGeometry>
<x:Double x:Key="CheckBoxMinHeight">32</x:Double>
<ControlTheme x:Key="{x:Type CheckBox}" TargetType="CheckBox">
<Setter Property="Padding" Value="8,0,0,0" />
@ -21,7 +22,7 @@
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
<Setter Property="MinHeight" Value="32" />
<Setter Property="MinHeight" Value="{DynamicResource CheckBoxMinHeight}" />
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundUnchecked}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundUnchecked}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushUnchecked}" />
@ -35,7 +36,7 @@
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}" />
<Grid VerticalAlignment="Top" Height="32">
<Grid VerticalAlignment="Top" Height="{DynamicResource CheckBoxMinHeight}">
<Border x:Name="NormalRectangle"
BorderBrush="{DynamicResource CheckBoxCheckBackgroundStrokeUnchecked}"
Background="{DynamicResource CheckBoxCheckBackgroundFillUnchecked}"

4
src/Avalonia.Themes.Fluent/Controls/RadioButton.xaml

@ -11,6 +11,8 @@
</Border>
</Design.PreviewWith>
<x:Double x:Key="RadioButtonMinHeight">32</x:Double>
<ControlTheme x:Key="{x:Type RadioButton}" TargetType="RadioButton">
<Setter Property="Background" Value="{DynamicResource RadioButtonBackground}" />
<Setter Property="Foreground" Value="{DynamicResource RadioButtonForeground}" />
@ -30,7 +32,7 @@
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid ColumnDefinitions="20,*">
<Grid Height="32" VerticalAlignment="Top">
<Grid Height="{DynamicResource RadioButtonMinHeight}" VerticalAlignment="Top">
<Ellipse
Name="OuterEllipse"

2
src/Avalonia.Themes.Fluent/DensityStyles/Compact.xaml

@ -21,4 +21,6 @@
<x:Double x:Key="TabItemMinHeight">28</x:Double>
<Thickness x:Key="TabItemHeaderMargin">6, 0</Thickness>
<Thickness x:Key="ButtonPadding">6,4</Thickness>
<x:Double x:Key="CheckBoxMinHeight">24</x:Double>
<x:Double x:Key="RadioButtonMinHeight">24</x:Double>
</ResourceDictionary>

BIN
tests/Avalonia.Skia.UnitTests/Fonts/Inter-Regular.LineGap800.ttf

Binary file not shown.

51
tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs

@ -70,7 +70,7 @@ namespace Avalonia.Skia.UnitTests.Media
var rects = BuildRects(glyphRun);
rects.Reverse();
if (glyphRun.IsLeftToRight)
{
foreach (var rect in rects)
@ -95,7 +95,7 @@ namespace Avalonia.Skia.UnitTests.Media
}
}
}
[InlineData("ABC012345", 0)] //LeftToRight
[InlineData("זה כיף סתם לשמוע איך תנצח קרפד עץ טוב בגן", 1)] //RightToLeft
[Theory]
@ -113,19 +113,19 @@ namespace Avalonia.Skia.UnitTests.Media
{
var characterHit =
glyphRun.GetCharacterHitFromDistance(glyphRun.Bounds.Width, out _);
Assert.Equal(glyphRun.Characters.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
}
else
{
var characterHit =
glyphRun.GetCharacterHitFromDistance(0, out _);
var characterHit =
glyphRun.GetCharacterHitFromDistance(0, out _);
Assert.Equal(glyphRun.Characters.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
}
var rects = BuildRects(glyphRun);
var lastCluster = -1;
var index = 0;
@ -379,12 +379,31 @@ namespace Avalonia.Skia.UnitTests.Media
}
}
[Fact]
public void Should_Add_Half_LineGap_To_Baseline()
{
using (Start())
{
var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Inter");
var options = new TextShaperOptions(typeface.GlyphTypeface, 14);
var shapedBuffer = TextShaper.Current.ShapeText("F", options);
var textMetrics = new TextMetrics(shapedBuffer.GlyphTypeface, 14);
var glyphRun = CreateGlyphRun(shapedBuffer);
var expectedBaseline = -textMetrics.Ascent + textMetrics.LineGap / 2;
Assert.Equal(expectedBaseline, glyphRun.Metrics.Baseline);
}
}
private static List<Rect> BuildRects(GlyphRun glyphRun)
{
var height = glyphRun.Bounds.Height;
var currentX = glyphRun.IsLeftToRight ? 0d : glyphRun.Bounds.Width;
var rects = new List<Rect>(glyphRun.GlyphInfos!.Count);
var lastCluster = -1;
@ -392,7 +411,7 @@ namespace Avalonia.Skia.UnitTests.Media
for (var index = 0; index < glyphRun.GlyphInfos.Count; index++)
{
var currentCluster = glyphRun.GlyphInfos[index].GlyphCluster;
var advance = glyphRun.GlyphInfos[index].GlyphAdvance;
if (lastCluster != currentCluster)
@ -412,11 +431,11 @@ namespace Avalonia.Skia.UnitTests.Media
rects.Remove(rect);
rect = glyphRun.IsLeftToRight ?
rect.WithWidth(rect.Width + advance) :
rect = glyphRun.IsLeftToRight ?
rect.WithWidth(rect.Width + advance) :
new Rect(rect.X - advance, 0, rect.Width + advance, height);
rects.Add(rect);
rects.Add(rect);
}
if (glyphRun.IsLeftToRight)
@ -436,14 +455,14 @@ namespace Avalonia.Skia.UnitTests.Media
private static GlyphRun CreateGlyphRun(ShapedBuffer shapedBuffer)
{
var glyphRun = new GlyphRun(
var glyphRun = new GlyphRun(
shapedBuffer.GlyphTypeface,
shapedBuffer.FontRenderingEmSize,
shapedBuffer.Text,
shapedBuffer,
biDiLevel: shapedBuffer.BidiLevel);
if(shapedBuffer.BidiLevel == 1)
if (shapedBuffer.BidiLevel == 1)
{
shapedBuffer.Reverse();
}

138
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

@ -9,7 +9,6 @@ using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.UnitTests;
using Xunit;
using static System.Net.Mime.MediaTypeNames;
namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
@ -905,7 +904,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.NotNull(textLine);
Assert.Throws<ArgumentOutOfRangeException>(() => textLine.GetTextBounds(0, 0));
}
}
}
[Fact]
@ -983,12 +982,12 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var defaultProperties = new GenericTextRunProperties(typeface);
var textSource = new CustomTextBufferTextSource(
new TextHidden(1),
new TextCharacters("Authenti", defaultProperties),
new TextHidden(1),
new TextHidden(1),
new TextCharacters("Authenti", defaultProperties),
new TextHidden(1),
new TextHidden(1),
new TextCharacters("ff", defaultProperties),
new TextHidden(1),
new TextHidden(1),
new TextHidden(1));
var formatter = new TextFormatterImpl();
@ -1138,7 +1137,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var typeface = new Typeface(FontFamily.Parse("resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Manrope"));
var defaultProperties = new GenericTextRunProperties(typeface);
var textSource = new CustomTextBufferTextSource(
new TextCharacters("He", defaultProperties),
new TextCharacters("He", defaultProperties),
new TextCharacters("Wo", defaultProperties),
new TextCharacters("ff", defaultProperties));
@ -1249,6 +1248,29 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
[Fact]
public void Should_Get_In_Cluster_Backspace_Hit()
{
using (Start())
{
var typeface = new Typeface(FontFamily.Parse("resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Manrope"));
var defaultProperties = new GenericTextRunProperties(typeface);
var textSource = new SingleBufferTextSource("ff", defaultProperties);
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
Assert.NotNull(textLine);
var backspaceHit = textLine.GetBackspaceCaretCharacterHit(new CharacterHit(1, 1));
Assert.Equal(1, backspaceHit.FirstCharacterIndex);
}
}
private class TextHidden : TextRun
{
public TextHidden(int length)
@ -1272,11 +1294,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
var pos = 0;
for(var i = 0; i < _textRuns.Count; i++)
for (var i = 0; i < _textRuns.Count; i++)
{
var currentRun = _textRuns[i];
if(pos + currentRun.Length > textSourceIndex)
if (pos + currentRun.Length > textSourceIndex)
{
return currentRun;
}
@ -1638,7 +1660,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.True(firstBounds.TextRunBounds.Count > 0);
}
}
[Fact]
public void Should_GetTextBounds_NotInfiniteLoop()
{
@ -1809,7 +1831,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var textSource = new TextFormatterTests.ListTextSource(new TextHidden(1) ,new TextCharacters(text, defaultProperties));
var textSource = new TextFormatterTests.ListTextSource(new TextHidden(1), new TextCharacters(text, defaultProperties));
var formatter = new TextFormatterImpl();
@ -1925,7 +1947,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textPosition = 0;
while(textPosition < text.Length)
while (textPosition < text.Length)
{
var bounds = textLine.GetTextBounds(textPosition, 1);
@ -2081,7 +2103,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
foreach (var glyphInfo in firstRun.ShapedBuffer)
{
if(lastCluster != glyphInfo.GlyphCluster)
if (lastCluster != glyphInfo.GlyphCluster)
{
clusterWidth.Add(currentAdvance);
distances.Add(currentDistance);
@ -2173,6 +2195,96 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
[Fact]
public void Should_Add_Half_LineGap_To_Baseline()
{
using (Start())
{
var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Inter");
var defaultProperties = new GenericTextRunProperties(typeface);
var textSource = new SingleBufferTextSource("F", defaultProperties);
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
Assert.NotNull(textLine);
var textMetrics = new TextMetrics(typeface.GlyphTypeface, 12);
var expectedBaseline = -textMetrics.Ascent + textMetrics.LineGap / 2;
Assert.Equal(expectedBaseline, textLine.Baseline);
}
}
[Fact]
public void Should_Clamp_Baseline_When_LineHeight_Is_Smaller_Than_Natural()
{
using (Start())
{
var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Inter");
var defaultProperties = new GenericTextRunProperties(typeface);
var textSource = new SingleBufferTextSource("F", defaultProperties);
var formatter = new TextFormatterImpl();
var textMetrics = new TextMetrics(typeface.GlyphTypeface, 12);
var natural = -textMetrics.Ascent + textMetrics.Descent + textMetrics.LineGap;
var smallerLineHeight = natural - 2;
// Force a smaller line height than ascent+descent+lineGap
var paragraphProps = new GenericTextParagraphProperties(defaultProperties, lineHeight: smallerLineHeight);
var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProps);
Assert.NotNull(textLine);
// In this case, baseline should equal -Ascent (lineGap ignored)
var expectedBaseline = -textMetrics.Ascent;
Assert.Equal(expectedBaseline, textLine.Baseline);
Assert.Equal(paragraphProps.LineHeight, textLine.Height);
}
}
[Fact]
public void Should_Distribute_Extra_Space_When_LineHeight_Is_Larger_Than_Natural()
{
using (Start())
{
var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Inter");
var defaultProperties = new GenericTextRunProperties(typeface);
var textSource = new SingleBufferTextSource("F", defaultProperties);
var formatter = new TextFormatterImpl();
var textMetrics = new TextMetrics(typeface.GlyphTypeface, 12);
var natural = -textMetrics.Ascent + textMetrics.Descent + textMetrics.LineGap;
var largerLineHeight = natural + 50;
var paragraphProps = new GenericTextParagraphProperties(defaultProperties, lineHeight: largerLineHeight);
var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProps);
Assert.NotNull(textLine);
// Extra space is distributed evenly above and below
var extra = largerLineHeight - (textMetrics.Descent - textMetrics.Ascent);
var expectedBaseline = -textMetrics.Ascent + extra / 2;
Assert.Equal(expectedBaseline, textLine.Baseline, 5);
Assert.Equal(largerLineHeight, textLine.Height, 5);
}
}
private class FixedRunsTextSource : ITextSource
{
private readonly IReadOnlyList<TextRun> _textRuns;

Loading…
Cancel
Save