Browse Source

Merge branch 'master' into feature/intermediate-points

pull/7413/head
Nikita Tsukanov 4 years ago
committed by GitHub
parent
commit
b21fb4f4ff
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 31
      native/Avalonia.Native/src/OSX/window.mm
  2. 2
      src/Avalonia.Controls.DataGrid/DataGridColumn.cs
  3. 21
      src/Avalonia.Controls.DataGrid/Utils/KeyboardHelper.cs
  4. 3
      src/Avalonia.Controls/ApiCompatBaseline.txt
  5. 96
      src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
  6. 6
      src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs
  7. 2
      src/Avalonia.Controls/LayoutTransformControl.cs
  8. 12
      src/Avalonia.Input/Gestures.cs
  9. 6
      src/Avalonia.Input/InputElement.cs
  10. 72
      src/Avalonia.Input/MouseDevice.cs
  11. 19
      src/Avalonia.Input/PointerDeltaEventArgs.cs
  12. 5
      src/Avalonia.Input/Raw/RawPointerEventArgs.cs
  13. 19
      src/Avalonia.Input/Raw/RawPointerGestureEventArgs.cs
  14. 8
      src/Avalonia.Native/AvaloniaNativeMenuExporter.cs
  15. 21
      src/Avalonia.Native/WindowImplBase.cs
  16. 5
      src/Avalonia.Native/avn.idl
  17. 23
      src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
  18. 10
      src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
  19. 1
      src/Avalonia.Visuals/Properties/AssemblyInfo.cs
  20. 2
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  21. 27
      src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs
  22. 10
      src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderTarget.cs
  23. 8
      src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs
  24. 64
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts
  25. 10
      src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs
  26. 176
      tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs
  27. 1
      tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj
  28. 96
      tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs
  29. 158
      tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs
  30. 147
      tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs
  31. 6
      tests/Avalonia.UnitTests/TestServices.cs
  32. 11
      tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs
  33. 6
      tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs

31
native/Avalonia.Native/src/OSX/window.mm

@ -1627,6 +1627,19 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
return;
}
}
else if (type == Magnify)
{
delta.X = delta.Y = [event magnification];
}
else if (type == Rotate)
{
delta.X = delta.Y = [event rotation];
}
else if (type == Swipe)
{
delta.X = [event deltaX];
delta.Y = [event deltaY];
}
auto timestamp = [event timestamp] * 1000;
auto modifiers = [self getModifiers:[event modifierFlags]];
@ -1753,6 +1766,24 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
[super scrollWheel:event];
}
- (void)magnifyWithEvent:(NSEvent *)event
{
[self mouseEvent:event withType:Magnify];
[super magnifyWithEvent:event];
}
- (void)rotateWithEvent:(NSEvent *)event
{
[self mouseEvent:event withType:Rotate];
[super rotateWithEvent:event];
}
- (void)swipeWithEvent:(NSEvent *)event
{
[self mouseEvent:event withType:Swipe];
[super swipeWithEvent:event];
}
- (void)mouseEntered:(NSEvent *)event
{
_isMouseOver = true;

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;
}
}
}

3
src/Avalonia.Controls/ApiCompatBaseline.txt

@ -36,6 +36,7 @@ CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.WindowBase' does not i
InterfacesShouldHaveSameMembers : Interface member 'public System.EventHandler<Avalonia.Controls.ApplicationLifetimes.ShutdownRequestedEventArgs> Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.ShutdownRequested' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.add_ShutdownRequested(System.EventHandler<Avalonia.Controls.ApplicationLifetimes.ShutdownRequestedEventArgs>)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.remove_ShutdownRequested(System.EventHandler<Avalonia.Controls.ApplicationLifetimes.ShutdownRequestedEventArgs>)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.TryShutdown(System.Int32)' is present in the implementation but not in the contract.
CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Embedding.EmbeddableControlRoot' does not implement interface 'Avalonia.Utilities.IWeakSubscriber<Avalonia.Controls.ResourcesChangedEventArgs>' in the implementation but it does in the contract.
MembersMustExist : Member 'public System.Action<Avalonia.Size> Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.set(System.Action<Avalonia.Size>)' does not exist in the implementation but it does exist in the contract.
@ -62,4 +63,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platfor
MembersMustExist : Member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size, Avalonia.Platform.PlatformResizeReason)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.ITrayIconImpl Avalonia.Platform.IWindowingPlatform.CreateTrayIcon()' is present in the implementation but not in the contract.
Total Issues: 63
Total Issues: 64

96
src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs

@ -76,36 +76,21 @@ namespace Avalonia.Controls.ApplicationLifetimes
return;
if (ShutdownMode == ShutdownMode.OnLastWindowClose && _windows.Count == 0)
Shutdown();
else if (ShutdownMode == ShutdownMode.OnMainWindowClose && window == MainWindow)
Shutdown();
TryShutdown();
else if (ShutdownMode == ShutdownMode.OnMainWindowClose && ReferenceEquals(window, MainWindow))
TryShutdown();
}
public void Shutdown(int exitCode = 0)
{
if (_isShuttingDown)
throw new InvalidOperationException("Application is already shutting down.");
_exitCode = exitCode;
_isShuttingDown = true;
DoShutdown(new ShutdownRequestedEventArgs(), true, exitCode);
}
try
{
foreach (var w in Windows)
w.Close();
var e = new ControlledApplicationLifetimeExitEventArgs(exitCode);
Exit?.Invoke(this, e);
_exitCode = e.ApplicationExitCode;
}
finally
{
_cts?.Cancel();
_cts = null;
_isShuttingDown = false;
}
public bool TryShutdown(int exitCode = 0)
{
return DoShutdown(new ShutdownRequestedEventArgs(), false, exitCode);
}
public int Start(string[] args)
{
Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args));
@ -114,7 +99,10 @@ namespace Avalonia.Controls.ApplicationLifetimes
if(options != null && options.ProcessUrlActivationCommandLine && args.Length > 0)
{
((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(args);
if (Application.Current is IApplicationPlatformEvents events)
{
events.RaiseUrlsOpened(args);
}
}
var lifetimeEvents = AvaloniaLocator.Current.GetService<IPlatformLifetimeEventsImpl>();
@ -145,23 +133,57 @@ namespace Avalonia.Controls.ApplicationLifetimes
if (_activeLifetime == this)
_activeLifetime = null;
}
private void OnShutdownRequested(object sender, ShutdownRequestedEventArgs e)
private bool DoShutdown(ShutdownRequestedEventArgs e, bool force = false, int exitCode = 0)
{
ShutdownRequested?.Invoke(this, e);
if (!force)
{
ShutdownRequested?.Invoke(this, e);
if (e.Cancel)
return;
if (e.Cancel)
return false;
if (_isShuttingDown)
throw new InvalidOperationException("Application is already shutting down.");
}
_exitCode = exitCode;
_isShuttingDown = true;
// When an OS shutdown request is received, try to close all non-owned windows. Windows can cancel
// shutdown by setting e.Cancel = true in the Closing event. Owned windows will be shutdown by their
// owners.
foreach (var w in Windows)
if (w.Owner is null)
w.Close();
if (Windows.Count > 0)
e.Cancel = true;
try
{
// When an OS shutdown request is received, try to close all non-owned windows. Windows can cancel
// shutdown by setting e.Cancel = true in the Closing event. Owned windows will be shutdown by their
// owners.
foreach (var w in Windows)
{
if (w.Owner is null)
{
w.Close();
}
}
if (!force && Windows.Count > 0)
{
e.Cancel = true;
return false;
}
var args = new ControlledApplicationLifetimeExitEventArgs(exitCode);
Exit?.Invoke(this, args);
_exitCode = args.ApplicationExitCode;
}
finally
{
_cts?.Cancel();
_cts = null;
_isShuttingDown = false;
}
return true;
}
private void OnShutdownRequested(object sender, ShutdownRequestedEventArgs e) => DoShutdown(e);
}
public class ClassicDesktopStyleApplicationLifetimeOptions

6
src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs

@ -9,6 +9,12 @@ namespace Avalonia.Controls.ApplicationLifetimes
/// </summary>
public interface IClassicDesktopStyleApplicationLifetime : IControlledApplicationLifetime
{
/// <summary>
/// Tries to Shutdown the application. <see cref="ShutdownRequested" /> event can be used to cancel the shutdown.
/// </summary>
/// <param name="exitCode">An integer exit code for an application. The default exit code is 0.</param>
bool TryShutdown(int exitCode = 0);
/// <summary>
/// Gets the arguments passed to the
/// <see cref="ClassicDesktopStyleApplicationLifetimeExtensions.StartWithClassicDesktopLifetime{T}(T, string[], ShutdownMode)"/>

2
src/Avalonia.Controls/LayoutTransformControl.cs

@ -18,7 +18,7 @@ namespace Avalonia.Controls
AvaloniaProperty.Register<LayoutTransformControl, ITransform>(nameof(LayoutTransform));
public static readonly StyledProperty<bool> UseRenderTransformProperty =
AvaloniaProperty.Register<LayoutTransformControl, bool>(nameof(LayoutTransform));
AvaloniaProperty.Register<LayoutTransformControl, bool>(nameof(UseRenderTransform));
static LayoutTransformControl()
{

12
src/Avalonia.Input/Gestures.cs

@ -29,6 +29,18 @@ namespace Avalonia.Input
public static readonly RoutedEvent<ScrollGestureEventArgs> ScrollGestureEndedEvent =
RoutedEvent.Register<ScrollGestureEventArgs>(
"ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures));
public static readonly RoutedEvent<PointerDeltaEventArgs> PointerTouchPadGestureMagnifyEvent =
RoutedEvent.Register<PointerDeltaEventArgs>(
"PointerMagnifyGesture", RoutingStrategies.Bubble, typeof(Gestures));
public static readonly RoutedEvent<PointerDeltaEventArgs> PointerTouchPadGestureRotateEvent =
RoutedEvent.Register<PointerDeltaEventArgs>(
"PointerRotateGesture", RoutingStrategies.Bubble, typeof(Gestures));
public static readonly RoutedEvent<PointerDeltaEventArgs> PointerTouchPadGestureSwipeEvent =
RoutedEvent.Register<PointerDeltaEventArgs>(
"PointerSwipeGesture", RoutingStrategies.Bubble, typeof(Gestures));
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
private static readonly WeakReference<IInteractive> s_lastPress = new WeakReference<IInteractive>(null);

6
src/Avalonia.Input/InputElement.cs

@ -196,7 +196,7 @@ namespace Avalonia.Input
/// Defines the <see cref="DoubleTapped"/> event.
/// </summary>
public static readonly RoutedEvent<TappedEventArgs> DoubleTappedEvent = Gestures.DoubleTappedEvent;
private bool _isEffectivelyEnabled = true;
private bool _isFocused;
private bool _isKeyboardFocusWithin;
@ -349,14 +349,14 @@ namespace Avalonia.Input
}
/// <summary>
/// Occurs when the mouse wheen is scrolled over the control.
/// Occurs when the mouse is scrolled over the control.
/// </summary>
public event EventHandler<PointerWheelEventArgs> PointerWheelChanged
{
add { AddHandler(PointerWheelChangedEvent, value); }
remove { RemoveHandler(PointerWheelChangedEvent, value); }
}
/// <summary>
/// Occurs when a tap gesture occurs on the control.
/// </summary>

72
src/Avalonia.Input/MouseDevice.cs

@ -181,6 +181,15 @@ namespace Avalonia.Input
case RawPointerEventType.Wheel:
e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, keyModifiers);
break;
case RawPointerEventType.Magnify:
e.Handled = GestureMagnify(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers);
break;
case RawPointerEventType.Rotate:
e.Handled = GestureRotate(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers);
break;
case RawPointerEventType.Swipe:
e.Handled = GestureSwipe(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers);
break;
}
}
@ -335,6 +344,69 @@ namespace Avalonia.Input
return false;
}
private bool GestureMagnify(IMouseDevice device, ulong timestamp, IInputRoot root, Point p,
PointerPointProperties props, Vector delta, KeyModifiers inputModifiers)
{
device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root));
var hit = HitTest(root, p);
if (hit != null)
{
var source = GetSource(hit);
var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureMagnifyEvent, source,
_pointer, root, p, timestamp, props, inputModifiers, delta);
source?.RaiseEvent(e);
return e.Handled;
}
return false;
}
private bool GestureRotate(IMouseDevice device, ulong timestamp, IInputRoot root, Point p,
PointerPointProperties props, Vector delta, KeyModifiers inputModifiers)
{
device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root));
var hit = HitTest(root, p);
if (hit != null)
{
var source = GetSource(hit);
var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureRotateEvent, source,
_pointer, root, p, timestamp, props, inputModifiers, delta);
source?.RaiseEvent(e);
return e.Handled;
}
return false;
}
private bool GestureSwipe(IMouseDevice device, ulong timestamp, IInputRoot root, Point p,
PointerPointProperties props, Vector delta, KeyModifiers inputModifiers)
{
device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root));
var hit = HitTest(root, p);
if (hit != null)
{
var source = GetSource(hit);
var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureSwipeEvent, source,
_pointer, root, p, timestamp, props, inputModifiers, delta);
source?.RaiseEvent(e);
return e.Handled;
}
return false;
}
private IInteractive? GetSource(IVisual? hit)
{

19
src/Avalonia.Input/PointerDeltaEventArgs.cs

@ -0,0 +1,19 @@
using Avalonia.Interactivity;
using Avalonia.VisualTree;
namespace Avalonia.Input
{
public class PointerDeltaEventArgs : PointerEventArgs
{
public Vector Delta { get; set; }
public PointerDeltaEventArgs(RoutedEvent routedEvent, IInteractive? source,
IPointer pointer, IVisual rootVisual, Point rootVisualPosition, ulong timestamp,
PointerPointProperties properties, KeyModifiers modifiers, Vector delta)
: base(routedEvent, source, pointer, rootVisual, rootVisualPosition,
timestamp, properties, modifiers)
{
Delta = delta;
}
}
}

5
src/Avalonia.Input/Raw/RawPointerEventArgs.cs

@ -22,7 +22,10 @@ namespace Avalonia.Input.Raw
TouchBegin,
TouchUpdate,
TouchEnd,
TouchCancel
TouchCancel,
Magnify,
Rotate,
Swipe
}
/// <summary>

19
src/Avalonia.Input/Raw/RawPointerGestureEventArgs.cs

@ -0,0 +1,19 @@
namespace Avalonia.Input.Raw
{
public class RawPointerGestureEventArgs : RawPointerEventArgs
{
public RawPointerGestureEventArgs(
IInputDevice device,
ulong timestamp,
IInputRoot root,
RawPointerEventType gestureType,
Point position,
Vector delta, RawInputModifiers inputModifiers)
: base(device, timestamp, root, gestureType, position, inputModifiers)
{
Delta = delta;
}
public Vector Delta { get; private set; }
}
}

8
src/Avalonia.Native/AvaloniaNativeMenuExporter.cs

@ -133,9 +133,13 @@ namespace Avalonia.Native
var quitItem = new NativeMenuItem("Quit") { Gesture = new KeyGesture(Key.Q, KeyModifiers.Meta) };
quitItem.Click += (_, _) =>
{
if (Application.Current is { ApplicationLifetime: IControlledApplicationLifetime lifetime })
if (Application.Current is { ApplicationLifetime: IClassicDesktopStyleApplicationLifetime lifetime })
{
lifetime.Shutdown();
lifetime.TryShutdown();
}
else if(Application.Current is {ApplicationLifetime: IControlledApplicationLifetime controlledLifetime})
{
controlledLifetime.Shutdown();
}
};

21
src/Avalonia.Native/WindowImplBase.cs

@ -306,11 +306,28 @@ namespace Avalonia.Native
switch (type)
{
case AvnRawMouseEventType.Wheel:
Input?.Invoke(new RawMouseWheelEventArgs(_mouse, timeStamp, _inputRoot, point.ToAvaloniaPoint(), new Vector(delta.X, delta.Y), (RawInputModifiers)modifiers));
Input?.Invoke(new RawMouseWheelEventArgs(_mouse, timeStamp, _inputRoot,
point.ToAvaloniaPoint(), new Vector(delta.X, delta.Y), (RawInputModifiers)modifiers));
break;
case AvnRawMouseEventType.Magnify:
Input?.Invoke(new RawPointerGestureEventArgs(_mouse, timeStamp, _inputRoot, RawPointerEventType.Magnify,
point.ToAvaloniaPoint(), new Vector(delta.X, delta.Y), (RawInputModifiers)modifiers));
break;
case AvnRawMouseEventType.Rotate:
Input?.Invoke(new RawPointerGestureEventArgs(_mouse, timeStamp, _inputRoot, RawPointerEventType.Rotate,
point.ToAvaloniaPoint(), new Vector(delta.X, delta.Y), (RawInputModifiers)modifiers));
break;
case AvnRawMouseEventType.Swipe:
Input?.Invoke(new RawPointerGestureEventArgs(_mouse, timeStamp, _inputRoot, RawPointerEventType.Swipe,
point.ToAvaloniaPoint(), new Vector(delta.X, delta.Y), (RawInputModifiers)modifiers));
break;
default:
var e = new RawPointerEventArgs(_mouse, timeStamp, _inputRoot, (RawPointerEventType)type, point.ToAvaloniaPoint(), (RawInputModifiers)modifiers);
var e = new RawPointerEventArgs(_mouse, timeStamp, _inputRoot, (RawPointerEventType)type,
point.ToAvaloniaPoint(), (RawInputModifiers)modifiers);
if(!ChromeHitTest(e))
{

5
src/Avalonia.Native/avn.idl

@ -295,7 +295,10 @@ enum AvnRawMouseEventType
TouchBegin,
TouchUpdate,
TouchEnd,
TouchCancel
TouchCancel,
Magnify,
Rotate,
Swipe,
}
enum AvnRawKeyEventType

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)

10
src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderTarget.cs

@ -34,14 +34,6 @@ namespace Avalonia.Web.Blazor
return new BlazorSkiaGpuRenderSession(_blazorSkiaSurface, _renderTarget);
}
public bool IsCorrupted
{
get
{
var result = _size.Width != _renderTarget.Width || _size.Height != _renderTarget.Height;
return result;
}
}
public bool IsCorrupted => _blazorSkiaSurface.Size != _size;
}
}

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;

10
src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs

@ -44,6 +44,16 @@ namespace Avalonia.Web.Blazor
{
var newSize = new Size(size.Width, size.Height);
if (Math.Abs(RenderScaling - dpi) > 0.0001)
{
if (_currentSurface is { })
{
_currentSurface.Scaling = dpi;
}
ScalingChanged?.Invoke(dpi);
}
if (newSize != _clientSize)
{
_clientSize = newSize;

176
tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs

@ -1,9 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform;
using Avalonia.Threading;
using Avalonia.UnitTests;
using Moq;
using Xunit;
@ -57,7 +55,7 @@ namespace Avalonia.Controls.UnitTests
var hasExit = false;
lifetime.Exit += (s, e) => hasExit = true;
lifetime.Exit += (_, _) => hasExit = true;
var windowA = new Window();
@ -91,7 +89,7 @@ namespace Avalonia.Controls.UnitTests
var hasExit = false;
lifetime.Exit += (s, e) => hasExit = true;
lifetime.Exit += (_, _) => hasExit = true;
var mainWindow = new Window();
@ -119,7 +117,7 @@ namespace Avalonia.Controls.UnitTests
var hasExit = false;
lifetime.Exit += (s, e) => hasExit = true;
lifetime.Exit += (_, _) => hasExit = true;
var windowA = new Window();
@ -226,7 +224,7 @@ namespace Avalonia.Controls.UnitTests
window.Show();
lifetime.ShutdownRequested += (s, e) =>
lifetime.ShutdownRequested += (_, e) =>
{
e.Cancel = true;
++raised;
@ -238,5 +236,171 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new[] { window }, lifetime.Windows);
}
}
[Fact]
public void MainWindow_Closed_Shutdown_Should_Be_Cancellable()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
lifetime.ShutdownMode = ShutdownMode.OnMainWindowClose;
var hasExit = false;
lifetime.Exit += (_, _) => hasExit = true;
var mainWindow = new Window();
mainWindow.Show();
lifetime.MainWindow = mainWindow;
var window = new Window();
window.Show();
var raised = 0;
lifetime.ShutdownRequested += (_, e) =>
{
e.Cancel = true;
++raised;
};
mainWindow.Close();
Assert.Equal(1, raised);
Assert.False(hasExit);
}
}
[Fact]
public void LastWindow_Closed_Shutdown_Should_Be_Cancellable()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
lifetime.ShutdownMode = ShutdownMode.OnLastWindowClose;
var hasExit = false;
lifetime.Exit += (_, _) => hasExit = true;
var windowA = new Window();
windowA.Show();
var windowB = new Window();
windowB.Show();
var raised = 0;
lifetime.ShutdownRequested += (_, e) =>
{
e.Cancel = true;
++raised;
};
windowA.Close();
Assert.False(hasExit);
windowB.Close();
Assert.Equal(1, raised);
Assert.False(hasExit);
}
}
[Fact]
public void TryShutdown_Cancellable_By_Preventing_Window_Close()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
var hasExit = false;
lifetime.Exit += (_, _) => hasExit = true;
var windowA = new Window();
windowA.Show();
var windowB = new Window();
windowB.Show();
var raised = 0;
windowA.Closing += (_, e) =>
{
e.Cancel = true;
++raised;
};
lifetime.TryShutdown();
Assert.Equal(1, raised);
Assert.False(hasExit);
}
}
[Fact]
public void Shutdown_NotCancellable_By_Preventing_Window_Close()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
var hasExit = false;
lifetime.Exit += (_, _) => hasExit = true;
var windowA = new Window();
windowA.Show();
var windowB = new Window();
windowB.Show();
var raised = 0;
windowA.Closing += (_, e) =>
{
e.Cancel = true;
++raised;
};
lifetime.Shutdown();
Assert.Equal(1, raised);
Assert.True(hasExit);
}
}
[Fact]
public void Shutdown_Doesnt_Raise_Shutdown_Requested()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
var hasExit = false;
lifetime.Exit += (_, _) => hasExit = true;
var raised = 0;
lifetime.ShutdownRequested += (_, _) =>
{
++raised;
};
lifetime.Shutdown();
Assert.Equal(0, raised);
Assert.True(hasExit);
}
}
}
}

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