diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm
index 0b88908252..40180274e1 100644
--- a/native/Avalonia.Native/src/OSX/window.mm
+++ b/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;
diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs
index 8501ce3896..a77b482436 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs
+++ b/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());
}
///
diff --git a/src/Avalonia.Controls.DataGrid/Utils/KeyboardHelper.cs b/src/Avalonia.Controls.DataGrid/Utils/KeyboardHelper.cs
index 351deceb48..d2b1fd4b8e 100644
--- a/src/Avalonia.Controls.DataGrid/Utils/KeyboardHelper.cs
+++ b/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();
+ return keymap?.CommandModifiers ?? KeyModifiers.Control;
}
}
}
diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt
index 9b7d37e108..a7560c37f2 100644
--- a/src/Avalonia.Controls/ApiCompatBaseline.txt
+++ b/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.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)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.remove_ShutdownRequested(System.EventHandler)' 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' in the implementation but it does in the contract.
MembersMustExist : Member 'public System.Action 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)' 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
diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
index 3a2fd68af5..edddf31d45 100644
--- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
+++ b/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();
@@ -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
diff --git a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs
index a70d5dd2f1..a83229b732 100644
--- a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs
+++ b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs
@@ -9,6 +9,12 @@ namespace Avalonia.Controls.ApplicationLifetimes
///
public interface IClassicDesktopStyleApplicationLifetime : IControlledApplicationLifetime
{
+ ///
+ /// Tries to Shutdown the application. event can be used to cancel the shutdown.
+ ///
+ /// An integer exit code for an application. The default exit code is 0.
+ bool TryShutdown(int exitCode = 0);
+
///
/// Gets the arguments passed to the
///
diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs
index 83ad2b3638..a8e15ee463 100644
--- a/src/Avalonia.Controls/LayoutTransformControl.cs
+++ b/src/Avalonia.Controls/LayoutTransformControl.cs
@@ -18,7 +18,7 @@ namespace Avalonia.Controls
AvaloniaProperty.Register(nameof(LayoutTransform));
public static readonly StyledProperty UseRenderTransformProperty =
- AvaloniaProperty.Register(nameof(LayoutTransform));
+ AvaloniaProperty.Register(nameof(UseRenderTransform));
static LayoutTransformControl()
{
diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs
index a5ee558f47..86e6e96a71 100644
--- a/src/Avalonia.Input/Gestures.cs
+++ b/src/Avalonia.Input/Gestures.cs
@@ -29,6 +29,18 @@ namespace Avalonia.Input
public static readonly RoutedEvent ScrollGestureEndedEvent =
RoutedEvent.Register(
"ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures));
+
+ public static readonly RoutedEvent PointerTouchPadGestureMagnifyEvent =
+ RoutedEvent.Register(
+ "PointerMagnifyGesture", RoutingStrategies.Bubble, typeof(Gestures));
+
+ public static readonly RoutedEvent PointerTouchPadGestureRotateEvent =
+ RoutedEvent.Register(
+ "PointerRotateGesture", RoutingStrategies.Bubble, typeof(Gestures));
+
+ public static readonly RoutedEvent PointerTouchPadGestureSwipeEvent =
+ RoutedEvent.Register(
+ "PointerSwipeGesture", RoutingStrategies.Bubble, typeof(Gestures));
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
private static readonly WeakReference s_lastPress = new WeakReference(null);
diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs
index def0a26b27..5aa1f1ff44 100644
--- a/src/Avalonia.Input/InputElement.cs
+++ b/src/Avalonia.Input/InputElement.cs
@@ -196,7 +196,7 @@ namespace Avalonia.Input
/// Defines the event.
///
public static readonly RoutedEvent DoubleTappedEvent = Gestures.DoubleTappedEvent;
-
+
private bool _isEffectivelyEnabled = true;
private bool _isFocused;
private bool _isKeyboardFocusWithin;
@@ -349,14 +349,14 @@ namespace Avalonia.Input
}
///
- /// Occurs when the mouse wheen is scrolled over the control.
+ /// Occurs when the mouse is scrolled over the control.
///
public event EventHandler PointerWheelChanged
{
add { AddHandler(PointerWheelChangedEvent, value); }
remove { RemoveHandler(PointerWheelChangedEvent, value); }
}
-
+
///
/// Occurs when a tap gesture occurs on the control.
///
diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs
index 166af44d04..34d2038d66 100644
--- a/src/Avalonia.Input/MouseDevice.cs
+++ b/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)
{
diff --git a/src/Avalonia.Input/PointerDeltaEventArgs.cs b/src/Avalonia.Input/PointerDeltaEventArgs.cs
new file mode 100644
index 0000000000..b3085a038d
--- /dev/null
+++ b/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;
+ }
+ }
+}
diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs
index 6cb8a10cf3..1fe19d9c55 100644
--- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs
+++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs
@@ -22,7 +22,10 @@ namespace Avalonia.Input.Raw
TouchBegin,
TouchUpdate,
TouchEnd,
- TouchCancel
+ TouchCancel,
+ Magnify,
+ Rotate,
+ Swipe
}
///
diff --git a/src/Avalonia.Input/Raw/RawPointerGestureEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerGestureEventArgs.cs
new file mode 100644
index 0000000000..5a6dbda9a7
--- /dev/null
+++ b/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; }
+ }
+}
diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs
index de6ba30a85..d8753efe25 100644
--- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs
+++ b/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();
}
};
diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs
index 87b7a7608e..1917b1575d 100644
--- a/src/Avalonia.Native/WindowImplBase.cs
+++ b/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))
{
diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl
index 112b4f636c..f2b9d4997e 100644
--- a/src/Avalonia.Native/avn.idl
+++ b/src/Avalonia.Native/avn.idl
@@ -295,7 +295,10 @@ enum AvnRawMouseEventType
TouchBegin,
TouchUpdate,
TouchEnd,
- TouchCancel
+ TouchCancel,
+ Magnify,
+ Rotate,
+ Swipe,
}
enum AvnRawKeyEventType
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
index 7c6af4eaa7..c97e36d5ff 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
+++ b/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
/// The text range that is covered by the text runs.
private static TextRange GetTextRange(IReadOnlyList textRuns)
{
- if (textRuns is null || textRuns.Count == 0)
+ if (textRuns.Count == 0)
{
return new TextRange();
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
index 40e12c8e99..0ed06e4e57 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
+++ b/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);
diff --git a/src/Avalonia.Visuals/Properties/AssemblyInfo.cs b/src/Avalonia.Visuals/Properties/AssemblyInfo.cs
index 881ddfd89f..ebff097199 100644
--- a/src/Avalonia.Visuals/Properties/AssemblyInfo.cs
+++ b/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")]
diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
index 7b9c515b97..c453181f65 100644
--- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
+++ b/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())
{
diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs
index 7644514687..dc8b091563 100644
--- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs
+++ b/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)
diff --git a/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderTarget.cs b/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderTarget.cs
index ee7374634f..fa6a39f210 100644
--- a/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderTarget.cs
+++ b/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;
}
}
diff --git a/src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs b/src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs
index 4f5d4cdf70..9cbbf24086 100644
--- a/src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs
+++ b/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);
diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts
index 147e2a963f..04d57a7756 100644
--- a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts
+++ b/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;
diff --git a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs
index 1d667c0f0c..3cadbfaa60 100644
--- a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs
+++ b/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;
diff --git a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs
index f7a3bdea1c..3a2e1c08bd 100644
--- a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs
+++ b/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);
+ }
+ }
}
}
diff --git a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj
index d4abf9416a..f5e502bca8 100644
--- a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj
+++ b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj
@@ -29,4 +29,5 @@
+
diff --git a/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs
new file mode 100644
index 0000000000..002da66070
--- /dev/null
+++ b/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 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();
+
+ if (assetLoader == null)
+ {
+ throw new NotSupportedException("IAssetLoader is not registered.");
+ }
+
+ var stream = assetLoader.Open(asset);
+
+ return new HarfBuzzGlyphTypefaceImpl(stream);
+ }
+ }
+}
diff --git a/tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs
new file mode 100644
index 0000000000..32e0434cd4
--- /dev/null
+++ b/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; }
+
+ ///
+ public short DesignEmHeight { get; }
+
+ ///
+ public int Ascent { get; }
+
+ ///
+ public int Descent { get; }
+
+ ///
+ public int LineGap { get; }
+
+ ///
+ public int UnderlinePosition { get; }
+
+ ///
+ public int UnderlineThickness { get; }
+
+ ///
+ public int StrikethroughPosition { get; }
+
+ ///
+ public int StrikethroughThickness { get; }
+
+ ///
+ public bool IsFixedPitch { get; }
+
+ public bool IsFakeBold { get; }
+
+ public bool IsFakeItalic { get; }
+
+ ///
+ public ushort GetGlyph(uint codepoint)
+ {
+ if (Font.TryGetGlyph(codepoint, out var glyph))
+ {
+ return (ushort)glyph;
+ }
+
+ return 0;
+ }
+
+ ///
+ public ushort[] GetGlyphs(ReadOnlySpan 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;
+ }
+
+ ///
+ public int GetGlyphAdvance(ushort glyph)
+ {
+ return Font.GetHorizontalGlyphAdvance(glyph);
+ }
+
+ ///
+ public int[] GetGlyphAdvances(ReadOnlySpan 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);
+ }
+ }
+}
diff --git a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs
new file mode 100644
index 0000000000..687fddd71a
--- /dev/null
+++ b/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 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(glyphIndices),
+ new ReadOnlySlice(glyphAdvances),
+ new ReadOnlySlice(glyphOffsets),
+ text,
+ new ReadOnlySlice(clusters),
+ buffer.Direction == Direction.LeftToRight ? 0 : 1);
+ }
+ }
+
+ private static void FillBuffer(Buffer buffer, ReadOnlySlice 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 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 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;
+ }
+ }
+}
diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs
index 1e586e3bb1..da678fd74b 100644
--- a/tests/Avalonia.UnitTests/TestServices.cs
+++ b/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,
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs
index 6e5b8eb637..a48639a426 100644
--- a/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs
+++ b/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())))
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs
index 58feb4714a..f52bdc39c8 100644
--- a/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs
+++ b/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)