Browse Source

Merge branch 'master' into PitchToZoom

pull/6564/head
Max Katz 4 years ago
committed by GitHub
parent
commit
dd602aca8c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      src/Avalonia.Controls.DataGrid/DataGridColumn.cs
  2. 21
      src/Avalonia.Controls.DataGrid/Utils/KeyboardHelper.cs
  3. 23
      src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
  4. 10
      src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
  5. 1
      src/Avalonia.Visuals/Properties/AssemblyInfo.cs
  6. 2
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  7. 27
      src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs
  8. 8
      src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs
  9. 64
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts
  10. 1
      tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj
  11. 96
      tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs
  12. 158
      tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs
  13. 147
      tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs
  14. 6
      tests/Avalonia.UnitTests/TestServices.cs
  15. 11
      tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs
  16. 6
      tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs

2
src/Avalonia.Controls.DataGrid/DataGridColumn.cs

@ -680,7 +680,7 @@ namespace Avalonia.Controls
public void ClearSort()
{
//InvokeProcessSort is already validating if sorting is possible
_headerCell?.InvokeProcessSort(Input.KeyModifiers.Control);
_headerCell?.InvokeProcessSort(KeyboardHelper.GetPlatformCtrlOrCmdKeyModifier());
}
/// <summary>

21
src/Avalonia.Controls.DataGrid/Utils/KeyboardHelper.cs

@ -4,22 +4,29 @@
// All other rights reserved.
using Avalonia.Input;
using Avalonia.Input.Platform;
namespace Avalonia.Controls.Utils
{
internal static class KeyboardHelper
{
public static void GetMetaKeyState(KeyModifiers modifiers, out bool ctrl, out bool shift)
public static void GetMetaKeyState(KeyModifiers modifiers, out bool ctrlOrCmd, out bool shift)
{
ctrl = (modifiers & KeyModifiers.Control) == KeyModifiers.Control;
shift = (modifiers & KeyModifiers.Shift) == KeyModifiers.Shift;
ctrlOrCmd = modifiers.HasFlag(GetPlatformCtrlOrCmdKeyModifier());
shift = modifiers.HasFlag(KeyModifiers.Shift);
}
public static void GetMetaKeyState(KeyModifiers modifiers, out bool ctrl, out bool shift, out bool alt)
public static void GetMetaKeyState(KeyModifiers modifiers, out bool ctrlOrCmd, out bool shift, out bool alt)
{
ctrl = (modifiers & KeyModifiers.Control) == KeyModifiers.Control;
shift = (modifiers & KeyModifiers.Shift) == KeyModifiers.Shift;
alt = (modifiers & KeyModifiers.Alt) == KeyModifiers.Alt;
ctrlOrCmd = modifiers.HasFlag(GetPlatformCtrlOrCmdKeyModifier());
shift = modifiers.HasFlag(KeyModifiers.Shift);
alt = modifiers.HasFlag(KeyModifiers.Alt);
}
public static KeyModifiers GetPlatformCtrlOrCmdKeyModifier()
{
var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
return keymap?.CommandModifiers ?? KeyModifiers.Control;
}
}
}

23
src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs

@ -193,7 +193,7 @@ namespace Avalonia.Media.TextFormatting
{
var currentRun = textRuns[i];
if (currentLength + currentRun.GlyphRun.Characters.Length < length)
if (currentLength + currentRun.GlyphRun.Characters.Length <= length)
{
currentLength += currentRun.GlyphRun.Characters.Length;
continue;
@ -283,26 +283,26 @@ namespace Avalonia.Media.TextFormatting
{
var shapedCharacters = previousLineBreak.RemainingCharacters[index];
if (shapedCharacters == null)
{
continue;
}
textRuns.Add(shapedCharacters);
if (TryGetLineBreak(shapedCharacters, out var runLineBreak))
{
var splitResult = SplitTextRuns(textRuns, currentLength + runLineBreak.PositionWrap);
if (splitResult.Second == null)
{
return splitResult.First;
}
if (++index < previousLineBreak.RemainingCharacters.Count)
{
for (; index < previousLineBreak.RemainingCharacters.Count; index++)
{
splitResult.Second!.Add(previousLineBreak.RemainingCharacters[index]);
splitResult.Second.Add(previousLineBreak.RemainingCharacters[index]);
}
}
nextLineBreak = new TextLineBreak(splitResult.Second!);
nextLineBreak = new TextLineBreak(splitResult.Second);
return splitResult.First;
}
@ -346,7 +346,10 @@ namespace Avalonia.Media.TextFormatting
{
var splitResult = SplitTextRuns(textRuns, currentLength + runLineBreak.PositionWrap);
nextLineBreak = new TextLineBreak(splitResult.Second!);
if (splitResult.Second != null)
{
nextLineBreak = new TextLineBreak(splitResult.Second);
}
return splitResult.First;
}
@ -532,7 +535,7 @@ namespace Avalonia.Media.TextFormatting
/// <returns>The text range that is covered by the text runs.</returns>
private static TextRange GetTextRange(IReadOnlyList<TextRun> textRuns)
{
if (textRuns is null || textRuns.Count == 0)
if (textRuns.Count == 0)
{
return new TextRange();
}

10
src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs

@ -401,14 +401,12 @@ namespace Avalonia.Media.TextFormatting
previousLine = textLine;
if (currentPosition != _text.Length || textLine.TextLineBreak?.RemainingCharacters == null)
if (currentPosition == _text.Length && textLine.NewLineLength > 0)
{
continue;
}
var emptyTextLine = CreateEmptyTextLine(currentPosition);
var emptyTextLine = CreateEmptyTextLine(currentPosition);
textLines.Add(emptyTextLine);
textLines.Add(emptyTextLine);
}
}
Size = new Size(width, height);

1
src/Avalonia.Visuals/Properties/AssemblyInfo.cs

@ -11,4 +11,5 @@ using Avalonia.Metadata;
[assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Skia.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Web.Blazor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]

2
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

@ -304,7 +304,7 @@ namespace Avalonia.Rendering
}
}
private void Render(bool forceComposite)
internal void Render(bool forceComposite)
{
using (var l = _lock.TryLock())
{

27
src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs

@ -3,6 +3,7 @@ using Avalonia.Controls.Embedding;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Avalonia.Rendering;
using Avalonia.Web.Blazor.Interop;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
@ -299,6 +300,18 @@ namespace Avalonia.Web.Blazor
_topLevel.Prepare();
_topLevel.Renderer.Start();
// Note: this is technically a hack, but it's a kinda unique use case when
// we want to blit the previous frame
// renderer doesn't have much control over the render target
// we render on the UI thread
// We also don't want to have it as a meaningful public API.
// Therefore we have InternalsVisibleTo hack here.
if (_topLevel.Renderer is DeferredRenderer dr)
{
dr.Render(true);
}
Invalidate();
});
}
@ -327,6 +340,11 @@ namespace Avalonia.Web.Blazor
_dpi = newDpi;
_topLevelImpl.SetClientSize(_canvasSize, _dpi);
if (_topLevel.Renderer is DeferredRenderer dr)
{
dr.Render(true);
}
Invalidate();
}
@ -334,9 +352,16 @@ namespace Avalonia.Web.Blazor
private void OnSizeChanged(SKSize newSize)
{
_canvasSize = newSize;
_interop.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi));
_topLevelImpl.SetClientSize(_canvasSize, _dpi);
if (_topLevel.Renderer is DeferredRenderer dr)
{
dr.Render(true);
}
Invalidate();
}
@ -348,7 +373,7 @@ namespace Avalonia.Web.Blazor
return;
}
_interop.RequestAnimationFrame(true, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi));
_interop.RequestAnimationFrame(true);
}
public void SetActive(bool active)

8
src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs

@ -11,6 +11,7 @@ namespace Avalonia.Web.Blazor.Interop
private const string InitRasterSymbol = "SKHtmlCanvas.initRaster";
private const string DeinitSymbol = "SKHtmlCanvas.deinit";
private const string RequestAnimationFrameSymbol = "SKHtmlCanvas.requestAnimationFrame";
private const string SetCanvasSizeSymbol = "SKHtmlCanvas.setCanvasSize";
private const string PutImageDataSymbol = "SKHtmlCanvas.putImageData";
private readonly ElementReference htmlCanvas;
@ -68,8 +69,11 @@ namespace Avalonia.Web.Blazor.Interop
callbackReference?.Dispose();
}
public void RequestAnimationFrame(bool enableRenderLoop, int rawWidth, int rawHeight) =>
Invoke(RequestAnimationFrameSymbol, htmlCanvas, enableRenderLoop, rawWidth, rawHeight);
public void RequestAnimationFrame(bool enableRenderLoop) =>
Invoke(RequestAnimationFrameSymbol, htmlCanvas, enableRenderLoop);
public void SetCanvasSize(int rawWidth, int rawHeight) =>
Invoke(SetCanvasSizeSymbol, htmlCanvas, rawWidth, rawHeight);
public void PutImageData(IntPtr intPtr, SKSizeI rawSize) =>
Invoke(PutImageDataSymbol, htmlCanvas, intPtr.ToInt64(), rawSize.Width, rawSize.Height);

64
src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts

@ -25,6 +25,8 @@ export class SKHtmlCanvas {
renderFrameCallback: DotNet.DotNetObjectReference;
renderLoopEnabled: boolean = false;
renderLoopRequest: number = 0;
newWidth: number;
newHeight: number;
public static initGL(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObjectReference): SKGLViewInfo {
var view = SKHtmlCanvas.init(true, element, elementId, callback);
@ -75,13 +77,22 @@ export class SKHtmlCanvas {
htmlCanvas.SKHtmlCanvas = undefined;
}
public static requestAnimationFrame(element: HTMLCanvasElement, renderLoop?: boolean, width?: number, height?: number) {
public static requestAnimationFrame(element: HTMLCanvasElement, renderLoop?: boolean) {
const htmlCanvas = element as SKHtmlCanvasElement;
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
return;
htmlCanvas.SKHtmlCanvas.requestAnimationFrame(renderLoop, width, height);
htmlCanvas.SKHtmlCanvas.requestAnimationFrame(renderLoop);
}
public static setCanvasSize(element: HTMLCanvasElement, width: number, height: number)
{
const htmlCanvas = element as SKHtmlCanvasElement;
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
return;
htmlCanvas.SKHtmlCanvas.setCanvasSize(width, height);
}
public static setEnableRenderLoop(element: HTMLCanvasElement, enable: boolean) {
const htmlCanvas = element as SKHtmlCanvasElement;
@ -128,28 +139,53 @@ export class SKHtmlCanvas {
public deinit() {
this.setEnableRenderLoop(false);
}
public requestAnimationFrame(renderLoop?: boolean, width?: number, height?: number) {
public setCanvasSize(width: number, height: number)
{
this.newWidth = width;
this.newHeight = height;
if(this.htmlCanvas.width != this.newWidth)
{
this.htmlCanvas.width = this.newWidth;
}
if(this.htmlCanvas.height != this.newHeight)
{
this.htmlCanvas.height = this.newHeight;
}
if (this.glInfo) {
// make current
GL.makeContextCurrent(this.glInfo.context);
}
}
public requestAnimationFrame(renderLoop?: boolean) {
// optionally update the render loop
if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop)
this.setEnableRenderLoop(renderLoop);
// make sure the canvas is scaled correctly for the drawing
if (width && height) {
this.htmlCanvas.width = width;
this.htmlCanvas.height = height;
}
// skip because we have a render loop
if (this.renderLoopRequest !== 0)
return;
// add the draw to the next frame
this.renderLoopRequest = window.requestAnimationFrame(() => {
if (this.glInfo) {
// make current
GL.makeContextCurrent(this.glInfo.context);
}
if (this.glInfo) {
// make current
GL.makeContextCurrent(this.glInfo.context);
}
if(this.htmlCanvas.width != this.newWidth)
{
this.htmlCanvas.width = this.newWidth;
}
if(this.htmlCanvas.height != this.newHeight)
{
this.htmlCanvas.height = this.newHeight;
}
this.renderFrameCallback.invokeMethod('Invoke');
this.renderLoopRequest = 0;

1
tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj

@ -29,4 +29,5 @@
<Import Project="..\..\build\Moq.props" />
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\SharedVersion.props" />
<Import Project="..\..\build\HarfBuzzSharp.props" />
</Project>

96
tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs

@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Avalonia.Media;
using Avalonia.Media.Fonts;
using Avalonia.Platform;
namespace Avalonia.UnitTests
{
public class HarfBuzzFontManagerImpl : IFontManagerImpl
{
private readonly Typeface[] _customTypefaces;
private readonly string _defaultFamilyName;
private static readonly Typeface _defaultTypeface =
new Typeface("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Mono");
private static readonly Typeface _italicTypeface =
new Typeface("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Sans");
private static readonly Typeface _emojiTypeface =
new Typeface("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Twitter Color Emoji");
public HarfBuzzFontManagerImpl(string defaultFamilyName = "resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Mono")
{
_customTypefaces = new[] { _emojiTypeface, _italicTypeface, _defaultTypeface };
_defaultFamilyName = defaultFamilyName;
}
public string GetDefaultFontFamilyName()
{
return _defaultFamilyName;
}
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
{
return _customTypefaces.Select(x => x.FontFamily!.Name);
}
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily,
CultureInfo culture, out Typeface fontKey)
{
foreach (var customTypeface in _customTypefaces)
{
var glyphTypeface = customTypeface.GlyphTypeface;
if (!glyphTypeface.TryGetGlyph((uint)codepoint, out _))
{
continue;
}
fontKey = customTypeface;
return true;
}
fontKey = default;
return false;
}
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
{
var fontFamily = typeface.FontFamily;
if (fontFamily == null)
{
return null;
}
if (fontFamily.IsDefault)
{
fontFamily = _defaultTypeface.FontFamily;
}
if (fontFamily!.Key == null)
{
return null;
}
var fontAssets = FontFamilyLoader.LoadFontAssets(fontFamily.Key);
var asset = fontAssets.First();
var assetLoader = AvaloniaLocator.Current.GetService<IAssetLoader>();
if (assetLoader == null)
{
throw new NotSupportedException("IAssetLoader is not registered.");
}
var stream = assetLoader.Open(asset);
return new HarfBuzzGlyphTypefaceImpl(stream);
}
}
}

158
tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs

@ -0,0 +1,158 @@
using System;
using System.IO;
using Avalonia.Platform;
using HarfBuzzSharp;
namespace Avalonia.UnitTests
{
public class HarfBuzzGlyphTypefaceImpl : IGlyphTypefaceImpl
{
private bool _isDisposed;
private Blob _blob;
public HarfBuzzGlyphTypefaceImpl(Stream data, bool isFakeBold = false, bool isFakeItalic = false)
{
_blob = Blob.FromStream(data);
Face = new Face(_blob, 0);
Font = new Font(Face);
Font.SetFunctionsOpenType();
Font.GetScale(out var scale, out _);
DesignEmHeight = (short)scale;
var metrics = Font.OpenTypeMetrics;
const double defaultFontRenderingEmSize = 12.0;
Ascent = (int)(metrics.GetXVariation(OpenTypeMetricsTag.HorizontalAscender) / defaultFontRenderingEmSize * DesignEmHeight);
Descent = (int)(metrics.GetXVariation(OpenTypeMetricsTag.HorizontalDescender) / defaultFontRenderingEmSize * DesignEmHeight);
LineGap = (int)(metrics.GetXVariation(OpenTypeMetricsTag.HorizontalLineGap) / defaultFontRenderingEmSize * DesignEmHeight);
UnderlinePosition = (int)(metrics.GetXVariation(OpenTypeMetricsTag.UnderlineOffset) / defaultFontRenderingEmSize * DesignEmHeight);
UnderlineThickness = (int)(metrics.GetXVariation(OpenTypeMetricsTag.UnderlineSize) / defaultFontRenderingEmSize * DesignEmHeight);
StrikethroughPosition = (int)(metrics.GetXVariation(OpenTypeMetricsTag.StrikeoutOffset) / defaultFontRenderingEmSize * DesignEmHeight);
StrikethroughThickness = (int)(metrics.GetXVariation(OpenTypeMetricsTag.StrikeoutSize) / defaultFontRenderingEmSize * DesignEmHeight);
IsFixedPitch = GetGlyphAdvance(GetGlyph('a')) == GetGlyphAdvance(GetGlyph('b'));
IsFakeBold = isFakeBold;
IsFakeItalic = isFakeItalic;
}
public Face Face { get; }
public Font Font { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public short DesignEmHeight { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int Ascent { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int Descent { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int LineGap { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int UnderlinePosition { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int UnderlineThickness { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int StrikethroughPosition { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int StrikethroughThickness { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public bool IsFixedPitch { get; }
public bool IsFakeBold { get; }
public bool IsFakeItalic { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public ushort GetGlyph(uint codepoint)
{
if (Font.TryGetGlyph(codepoint, out var glyph))
{
return (ushort)glyph;
}
return 0;
}
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints)
{
var glyphs = new ushort[codepoints.Length];
for (var i = 0; i < codepoints.Length; i++)
{
if (Font.TryGetGlyph(codepoints[i], out var glyph))
{
glyphs[i] = (ushort)glyph;
}
}
return glyphs;
}
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int GetGlyphAdvance(ushort glyph)
{
return Font.GetHorizontalGlyphAdvance(glyph);
}
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs)
{
var glyphIndices = new uint[glyphs.Length];
for (var i = 0; i < glyphs.Length; i++)
{
glyphIndices[i] = glyphs[i];
}
return Font.GetHorizontalGlyphAdvances(glyphIndices);
}
private void Dispose(bool disposing)
{
if (_isDisposed)
{
return;
}
_isDisposed = true;
if (!disposing)
{
return;
}
Font?.Dispose();
Face?.Dispose();
_blob?.Dispose();
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}

147
tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs

@ -0,0 +1,147 @@
using System;
using System.Globalization;
using Avalonia.Media;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
using Avalonia.Utilities;
using HarfBuzzSharp;
using Buffer = HarfBuzzSharp.Buffer;
namespace Avalonia.UnitTests
{
public class HarfBuzzTextShaperImpl : ITextShaperImpl
{
public GlyphRun ShapeText(ReadOnlySlice<char> text, Typeface typeface, double fontRenderingEmSize,
CultureInfo culture)
{
using (var buffer = new Buffer())
{
FillBuffer(buffer, text);
buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture);
buffer.GuessSegmentProperties();
var glyphTypeface = typeface.GlyphTypeface;
var font = ((HarfBuzzGlyphTypefaceImpl)glyphTypeface.PlatformImpl).Font;
font.Shape(buffer);
font.GetScale(out var scaleX, out _);
var textScale = fontRenderingEmSize / scaleX;
var bufferLength = buffer.Length;
var glyphInfos = buffer.GetGlyphInfoSpan();
var glyphPositions = buffer.GetGlyphPositionSpan();
var glyphIndices = new ushort[bufferLength];
var clusters = new ushort[bufferLength];
double[] glyphAdvances = null;
Vector[] glyphOffsets = null;
for (var i = 0; i < bufferLength; i++)
{
glyphIndices[i] = (ushort)glyphInfos[i].Codepoint;
clusters[i] = (ushort)glyphInfos[i].Cluster;
if (!glyphTypeface.IsFixedPitch)
{
SetAdvance(glyphPositions, i, textScale, ref glyphAdvances);
}
SetOffset(glyphPositions, i, textScale, ref glyphOffsets);
}
return new GlyphRun(glyphTypeface, fontRenderingEmSize,
new ReadOnlySlice<ushort>(glyphIndices),
new ReadOnlySlice<double>(glyphAdvances),
new ReadOnlySlice<Vector>(glyphOffsets),
text,
new ReadOnlySlice<ushort>(clusters),
buffer.Direction == Direction.LeftToRight ? 0 : 1);
}
}
private static void FillBuffer(Buffer buffer, ReadOnlySlice<char> text)
{
buffer.ContentType = ContentType.Unicode;
var i = 0;
while (i < text.Length)
{
var codepoint = Codepoint.ReadAt(text, i, out var count);
var cluster = (uint)(text.Start + i);
if (codepoint.IsBreakChar)
{
if (i + 1 < text.Length)
{
var nextCodepoint = Codepoint.ReadAt(text, i + 1, out _);
if (nextCodepoint == '\n' && codepoint == '\r')
{
count++;
buffer.Add('\u200C', cluster);
buffer.Add('\u200D', cluster);
}
else
{
buffer.Add('\u200C', cluster);
}
}
else
{
buffer.Add('\u200C', cluster);
}
}
else
{
buffer.Add(codepoint, cluster);
}
i += count;
}
}
private static void SetOffset(ReadOnlySpan<GlyphPosition> glyphPositions, int index, double textScale,
ref Vector[] offsetBuffer)
{
var position = glyphPositions[index];
if (position.XOffset == 0 && position.YOffset == 0)
{
return;
}
offsetBuffer ??= new Vector[glyphPositions.Length];
var offsetX = position.XOffset * textScale;
var offsetY = position.YOffset * textScale;
offsetBuffer[index] = new Vector(offsetX, offsetY);
}
private static void SetAdvance(ReadOnlySpan<GlyphPosition> glyphPositions, int index, double textScale,
ref double[] advanceBuffer)
{
advanceBuffer ??= new double[glyphPositions.Length];
// Depends on direction of layout
// advanceBuffer[index] = buffer.GlyphPositions[index].YAdvance * textScale;
advanceBuffer[index] = glyphPositions[index].XAdvance * textScale;
}
}
}

6
tests/Avalonia.UnitTests/TestServices.cs

@ -58,6 +58,12 @@ namespace Avalonia.UnitTests
public static readonly TestServices RealStyler = new TestServices(
styler: new Styler());
public static readonly TestServices TextServices = new TestServices(
assetLoader: new AssetLoader(),
renderInterface: new MockPlatformRenderInterface(),
fontManagerImpl: new HarfBuzzFontManagerImpl(),
textShaperImpl: new HarfBuzzTextShaperImpl());
public TestServices(
IAssetLoader assetLoader = null,
IFocusManager focusManager = null,

11
tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs

@ -48,7 +48,16 @@ namespace Avalonia.Visuals.UnitTests.Media
[Fact]
public void Should_Use_FontManagerOptions_FontFallback()
{
var options = new FontManagerOptions { FontFallbacks = new[] { new FontFallback { FontFamily = new FontFamily("MyFont"), UnicodeRange = UnicodeRange.Default} } };
var options = new FontManagerOptions
{
FontFallbacks = new[]
{
new FontFallback
{
FontFamily = new FontFamily("MyFont"), UnicodeRange = UnicodeRange.Default
}
}
};
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
.With(fontManagerImpl: new MockFontManagerImpl())))

6
tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs

@ -22,6 +22,7 @@ namespace Avalonia.Visuals.UnitTests.Media
[Theory]
public void Should_Get_Distance_From_CharacterHit(double[] advances, ushort[] clusters, int start, int trailingLength, double expectedDistance)
{
using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
using (var glyphRun = CreateGlyphRun(advances, clusters))
{
var characterHit = new CharacterHit(start, trailingLength);
@ -40,6 +41,7 @@ namespace Avalonia.Visuals.UnitTests.Media
public void Should_Get_CharacterHit_FromDistance(double[] advances, ushort[] clusters, double distance, int start,
int trailingLengthExpected, bool isInsideExpected)
{
using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
using (var glyphRun = CreateGlyphRun(advances, clusters))
{
var textBounds = glyphRun.GetCharacterHitFromDistance(distance, out var isInside);
@ -63,6 +65,7 @@ namespace Avalonia.Visuals.UnitTests.Media
public void Should_Find_Nearest_CharacterHit(double[] advances, ushort[] clusters, int bidiLevel,
int index, int expectedIndex, int expectedLength, double expectedWidth)
{
using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel))
{
var textBounds = glyphRun.FindNearestCharacterHit(index, out var width);
@ -87,6 +90,7 @@ namespace Avalonia.Visuals.UnitTests.Media
int nextIndex, int nextLength,
int bidiLevel)
{
using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel))
{
var characterHit = glyphRun.GetNextCaretCharacterHit(new CharacterHit(currentIndex, currentLength));
@ -109,6 +113,7 @@ namespace Avalonia.Visuals.UnitTests.Media
int previousIndex, int previousLength,
int bidiLevel)
{
using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel))
{
var characterHit = glyphRun.GetPreviousCaretCharacterHit(new CharacterHit(currentIndex, currentLength));
@ -128,6 +133,7 @@ namespace Avalonia.Visuals.UnitTests.Media
[Theory]
public void Should_Find_Glyph_Index(double[] advances, ushort[] clusters, int bidiLevel)
{
using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel))
{
if (glyphRun.IsLeftToRight)

Loading…
Cancel
Save