Browse Source

merge master

ui-automation-test
Takoooooo 4 years ago
parent
commit
fae61f2ac2
  1. 31
      native/Avalonia.Native/src/OSX/window.mm
  2. 220
      samples/ControlCatalog/Pages/PointersPage.cs
  3. 7
      src/Avalonia.Base/Threading/Dispatcher.cs
  4. 15
      src/Avalonia.Base/Threading/JobRunner.cs
  5. 2
      src/Avalonia.Controls.DataGrid/DataGridColumn.cs
  6. 21
      src/Avalonia.Controls.DataGrid/Utils/KeyboardHelper.cs
  7. 3
      src/Avalonia.Controls/ApiCompatBaseline.txt
  8. 96
      src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
  9. 6
      src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs
  10. 2
      src/Avalonia.Controls/LayoutTransformControl.cs
  11. 22
      src/Avalonia.Controls/Primitives/Popup.cs
  12. 12
      src/Avalonia.Input/Gestures.cs
  13. 6
      src/Avalonia.Input/InputElement.cs
  14. 83
      src/Avalonia.Input/MouseDevice.cs
  15. 19
      src/Avalonia.Input/PointerDeltaEventArgs.cs
  16. 44
      src/Avalonia.Input/PointerEventArgs.cs
  17. 2
      src/Avalonia.Input/Raw/RawInputEventArgs.cs
  18. 14
      src/Avalonia.Input/Raw/RawPointerEventArgs.cs
  19. 19
      src/Avalonia.Input/Raw/RawPointerGestureEventArgs.cs
  20. 2
      src/Avalonia.Input/TouchDevice.cs
  21. 8
      src/Avalonia.Native/AvaloniaNativeMenuExporter.cs
  22. 21
      src/Avalonia.Native/WindowImplBase.cs
  23. 5
      src/Avalonia.Native/avn.idl
  24. 3
      src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml
  25. 3
      src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml
  26. 4
      src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml
  27. 8
      src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml
  28. 3
      src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml
  29. 8
      src/Avalonia.Themes.Fluent/Controls/Expander.xaml
  30. 4
      src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml
  31. 6
      src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
  32. 3
      src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml
  33. 8
      src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml
  34. 3
      src/Avalonia.Themes.Fluent/Controls/TabItem.xaml
  35. 3
      src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml
  36. 2
      src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
  37. 3
      src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml
  38. 3
      src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml
  39. 4
      src/Avalonia.Themes.Fluent/Controls/WindowNotificationManager.xaml
  40. 45
      src/Avalonia.Visuals/Media/PreciseEllipticArcHelper.cs
  41. 24
      src/Avalonia.Visuals/Media/StreamGeometryContext.cs
  42. 23
      src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
  43. 10
      src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
  44. 1
      src/Avalonia.Visuals/Properties/AssemblyInfo.cs
  45. 2
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  46. 1
      src/Avalonia.X11/Avalonia.X11.csproj
  47. 99
      src/Avalonia.X11/X11Window.cs
  48. 1
      src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj
  49. 38
      src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs
  50. 31
      src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs
  51. 43
      src/Linux/Avalonia.LinuxFramebuffer/Input/RawEventGroupingThreadingHelper.cs
  52. 129
      src/Shared/RawEventGrouping.cs
  53. 1
      src/Shared/RenderHelpers/RenderHelpers.projitems
  54. 126
      src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs
  55. 10
      src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderTarget.cs
  56. 8
      src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs
  57. 64
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts
  58. 10
      src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs
  59. 176
      tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs
  60. 237
      tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
  61. 56
      tests/Avalonia.RenderTests/Media/StreamGeometryTests.cs
  62. 1
      tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj
  63. 96
      tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs
  64. 158
      tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs
  65. 147
      tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs
  66. 6
      tests/Avalonia.UnitTests/TestServices.cs
  67. 11
      tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs
  68. 6
      tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs
  69. BIN
      tests/TestFiles/Direct2D1/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.deferred.expected.png
  70. BIN
      tests/TestFiles/Direct2D1/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.immediate.expected.png
  71. BIN
      tests/TestFiles/Skia/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.deferred.expected.png
  72. BIN
      tests/TestFiles/Skia/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.immediate.expected.png

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

@ -1641,6 +1641,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]];
@ -1767,6 +1780,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;

220
samples/ControlCatalog/Pages/PointersPage.cs

@ -1,15 +1,37 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Threading;
using Avalonia.VisualTree;
namespace ControlCatalog.Pages
namespace ControlCatalog.Pages;
public class PointersPage : Decorator
{
public class PointersPage : Control
public PointersPage()
{
Child = new TabControl
{
Items = new[]
{
new TabItem() { Header = "Contacts", Content = new PointerContactsTab() },
new TabItem() { Header = "IntermediatePoints", Content = new PointerIntermediatePointsTab() }
}
};
}
class PointerContactsTab : Control
{
class PointerInfo
{
@ -45,7 +67,7 @@ namespace ControlCatalog.Pages
private Dictionary<IPointer, PointerInfo> _pointers = new Dictionary<IPointer, PointerInfo>();
public PointersPage()
public PointerContactsTab()
{
ClipToBounds = true;
}
@ -104,4 +126,196 @@ namespace ControlCatalog.Pages
}
}
}
public class PointerIntermediatePointsTab : Decorator
{
public PointerIntermediatePointsTab()
{
this[TextBlock.ForegroundProperty] = Brushes.Black;
var slider = new Slider
{
Margin = new Thickness(5),
Minimum = 0,
Maximum = 500
};
var status = new TextBlock()
{
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
};
Child = new Grid
{
Children =
{
new PointerCanvas(slider, status),
new Border
{
Background = Brushes.LightYellow,
Child = new StackPanel
{
Children =
{
new StackPanel
{
Orientation = Orientation.Horizontal,
Children =
{
new TextBlock { Text = "Thread sleep:" },
new TextBlock()
{
[!TextBlock.TextProperty] =slider.GetObservable(Slider.ValueProperty)
.Select(x=>x.ToString()).ToBinding()
}
}
},
slider
}
},
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Top,
Width = 300,
Height = 60
},
status
}
};
}
class PointerCanvas : Control
{
private readonly Slider _slider;
private readonly TextBlock _status;
private int _events;
private Stopwatch _stopwatch = Stopwatch.StartNew();
private Dictionary<int, PointerPoints> _pointers = new();
class PointerPoints
{
struct CanvasPoint
{
public IBrush Brush;
public Point Point;
public double Radius;
}
readonly CanvasPoint[] _points = new CanvasPoint[1000];
int _index;
public void Render(DrawingContext context)
{
CanvasPoint? prev = null;
for (var c = 0; c < _points.Length; c++)
{
var i = (c + _index) % _points.Length;
var pt = _points[i];
if (prev.HasValue && prev.Value.Brush != null && pt.Brush != null)
context.DrawLine(new Pen(Brushes.Black), prev.Value.Point, pt.Point);
prev = pt;
if (pt.Brush != null)
context.DrawEllipse(pt.Brush, null, pt.Point, pt.Radius, pt.Radius);
}
}
void AddPoint(Point pt, IBrush brush, double radius)
{
_points[_index] = new CanvasPoint { Point = pt, Brush = brush, Radius = radius };
_index = (_index + 1) % _points.Length;
}
public void HandleEvent(PointerEventArgs e, Visual v)
{
e.Handled = true;
if (e.RoutedEvent == PointerPressedEvent)
AddPoint(e.GetPosition(v), Brushes.Green, 10);
else if (e.RoutedEvent == PointerReleasedEvent)
AddPoint(e.GetPosition(v), Brushes.Red, 10);
else
{
var pts = e.GetIntermediatePoints(v);
for (var c = 0; c < pts.Count; c++)
{
var pt = pts[c];
AddPoint(pt.Position, c == pts.Count - 1 ? Brushes.Blue : Brushes.Black,
c == pts.Count - 1 ? 5 : 2);
}
}
}
}
public PointerCanvas(Slider slider, TextBlock status)
{
_slider = slider;
_status = status;
DispatcherTimer.Run(() =>
{
if (_stopwatch.Elapsed.TotalSeconds > 1)
{
_status.Text = "Events per second: " + (_events / _stopwatch.Elapsed.TotalSeconds);
_stopwatch.Restart();
_events = 0;
}
return this.GetVisualRoot() != null;
}, TimeSpan.FromMilliseconds(10));
}
void HandleEvent(PointerEventArgs e)
{
_events++;
Thread.Sleep((int)_slider.Value);
InvalidateVisual();
if (e.RoutedEvent == PointerReleasedEvent && e.Pointer.Type == PointerType.Touch)
{
_pointers.Remove(e.Pointer.Id);
return;
}
if (!_pointers.TryGetValue(e.Pointer.Id, out var pt))
_pointers[e.Pointer.Id] = pt = new PointerPoints();
pt.HandleEvent(e, this);
}
public override void Render(DrawingContext context)
{
context.FillRectangle(Brushes.White, Bounds);
foreach(var pt in _pointers.Values)
pt.Render(context);
base.Render(context);
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
if (e.ClickCount == 2)
{
_pointers.Clear();
InvalidateVisual();
return;
}
HandleEvent(e);
base.OnPointerPressed(e);
}
protected override void OnPointerMoved(PointerEventArgs e)
{
HandleEvent(e);
base.OnPointerMoved(e);
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
HandleEvent(e);
base.OnPointerReleased(e);
}
}
}
}

7
src/Avalonia.Base/Threading/Dispatcher.cs

@ -74,6 +74,13 @@ namespace Avalonia.Threading
/// </summary>
/// <param name="minimumPriority"></param>
public void RunJobs(DispatcherPriority minimumPriority) => _jobRunner.RunJobs(minimumPriority);
/// <summary>
/// Use this method to check if there are more prioritized tasks
/// </summary>
/// <param name="minimumPriority"></param>
public bool HasJobsWithPriority(DispatcherPriority minimumPriority) =>
_jobRunner.HasJobsWithPriority(minimumPriority);
/// <inheritdoc/>
public Task InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal)

15
src/Avalonia.Base/Threading/JobRunner.cs

@ -121,6 +121,21 @@ namespace Avalonia.Threading
return null;
}
public bool HasJobsWithPriority(DispatcherPriority minimumPriority)
{
for (int c = (int)minimumPriority; c < (int)DispatcherPriority.MaxValue; c++)
{
var q = _queues[c];
lock (q)
{
if (q.Count > 0)
return true;
}
}
return false;
}
private interface IJob
{
/// <summary>

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()
{

22
src/Avalonia.Controls/Primitives/Popup.cs

@ -430,16 +430,20 @@ namespace Avalonia.Controls.Primitives
(x, handler) => x.LostFocus += handler,
(x, handler) => x.LostFocus -= handler).DisposeWith(handlerCleanup);
SubscribeToEventHandler<IWindowImpl, Action<PixelPoint>>(window.PlatformImpl, WindowPositionChanged,
(x, handler) => x.PositionChanged += handler,
(x, handler) => x.PositionChanged -= handler).DisposeWith(handlerCleanup);
if (placementTarget is Layoutable layoutTarget)
// Recalculate popup position on parent moved/resized, but not if placement was on pointer
if (PlacementMode != PlacementMode.Pointer)
{
// If the placement target is moved, update the popup position
SubscribeToEventHandler<Layoutable, EventHandler>(layoutTarget, PlacementTargetLayoutUpdated,
(x, handler) => x.LayoutUpdated += handler,
(x, handler) => x.LayoutUpdated -= handler).DisposeWith(handlerCleanup);
SubscribeToEventHandler<IWindowImpl, Action<PixelPoint>>(window.PlatformImpl, WindowPositionChanged,
(x, handler) => x.PositionChanged += handler,
(x, handler) => x.PositionChanged -= handler).DisposeWith(handlerCleanup);
if (placementTarget is Layoutable layoutTarget)
{
// If the placement target is moved, update the popup position
SubscribeToEventHandler<Layoutable, EventHandler>(layoutTarget, PlacementTargetLayoutUpdated,
(x, handler) => x.LayoutUpdated += handler,
(x, handler) => x.LayoutUpdated -= handler).DisposeWith(handlerCleanup);
}
}
}
else if (topLevel is PopupRoot parentPopupRoot)

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>

83
src/Avalonia.Input/MouseDevice.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Input.Raw;
@ -159,7 +160,7 @@ namespace Avalonia.Input
case RawPointerEventType.XButton1Down:
case RawPointerEventType.XButton2Down:
if (ButtonCount(props) > 1)
e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers);
e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints);
else
e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position,
props, keyModifiers);
@ -170,16 +171,25 @@ namespace Avalonia.Input
case RawPointerEventType.XButton1Up:
case RawPointerEventType.XButton2Up:
if (ButtonCount(props) != 0)
e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers);
e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints);
else
e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers);
break;
case RawPointerEventType.Move:
e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers);
e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints);
break;
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;
}
}
@ -263,7 +273,7 @@ namespace Avalonia.Input
}
private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties,
KeyModifiers inputModifiers)
KeyModifiers inputModifiers, IReadOnlyList<Point>? intermediatePoints)
{
device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root));
@ -283,7 +293,7 @@ namespace Avalonia.Input
if (source is object)
{
var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root,
p, timestamp, properties, inputModifiers);
p, timestamp, properties, inputModifiers, intermediatePoints);
source.RaiseEvent(e);
return e.Handled;
@ -334,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;
}
}
}

44
src/Avalonia.Input/PointerEventArgs.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using Avalonia.Input.Raw;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
@ -10,6 +11,7 @@ namespace Avalonia.Input
private readonly IVisual? _rootVisual;
private readonly Point _rootVisualPosition;
private readonly PointerPointProperties _properties;
private readonly IReadOnlyList<Point>? _previousPoints;
public PointerEventArgs(RoutedEvent routedEvent,
IInteractive? source,
@ -28,6 +30,20 @@ namespace Avalonia.Input
Timestamp = timestamp;
KeyModifiers = modifiers;
}
public PointerEventArgs(RoutedEvent routedEvent,
IInteractive? source,
IPointer pointer,
IVisual? rootVisual, Point rootVisualPosition,
ulong timestamp,
PointerPointProperties properties,
KeyModifiers modifiers,
IReadOnlyList<Point>? previousPoints)
: this(routedEvent, source, pointer, rootVisual, rootVisualPosition, timestamp, properties, modifiers)
{
_previousPoints = previousPoints;
}
class EmulatedDevice : IPointerDevice
{
@ -76,14 +92,16 @@ namespace Avalonia.Input
public KeyModifiers KeyModifiers { get; }
public Point GetPosition(IVisual? relativeTo)
private Point GetPosition(Point pt, IVisual? relativeTo)
{
if (_rootVisual == null)
return default;
if (relativeTo == null)
return _rootVisualPosition;
return _rootVisualPosition * _rootVisual.TransformToVisual(relativeTo) ?? default;
return pt;
return pt * _rootVisual.TransformToVisual(relativeTo) ?? default;
}
public Point GetPosition(IVisual? relativeTo) => GetPosition(_rootVisualPosition, relativeTo);
[Obsolete("Use GetCurrentPoint")]
public PointerPoint GetPointerPoint(IVisual? relativeTo) => GetCurrentPoint(relativeTo);
@ -96,6 +114,26 @@ namespace Avalonia.Input
public PointerPoint GetCurrentPoint(IVisual? relativeTo)
=> new PointerPoint(Pointer, GetPosition(relativeTo), _properties);
/// <summary>
/// Returns the PointerPoint associated with the current event
/// </summary>
/// <param name="relativeTo">The visual which coordinate system to use. Pass null for toplevel coordinate system</param>
/// <returns></returns>
public IReadOnlyList<PointerPoint> GetIntermediatePoints(IVisual? relativeTo)
{
if (_previousPoints == null || _previousPoints.Count == 0)
return new[] { GetCurrentPoint(relativeTo) };
var points = new PointerPoint[_previousPoints.Count + 1];
for (var c = 0; c < _previousPoints.Count; c++)
{
var pt = _previousPoints[c];
points[c] = new PointerPoint(Pointer, GetPosition(pt, relativeTo), _properties);
}
points[points.Length - 1] = GetCurrentPoint(relativeTo);
return points;
}
/// <summary>
/// Returns the current pointer point properties
/// </summary>

2
src/Avalonia.Input/Raw/RawInputEventArgs.cs

@ -51,6 +51,6 @@ namespace Avalonia.Input.Raw
/// <summary>
/// Gets the timestamp associated with the event.
/// </summary>
public ulong Timestamp { get; private set; }
public ulong Timestamp { get; set; }
}
}

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

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
namespace Avalonia.Input.Raw
{
@ -21,7 +22,10 @@ namespace Avalonia.Input.Raw
TouchBegin,
TouchUpdate,
TouchEnd,
TouchCancel
TouchCancel,
Magnify,
Rotate,
Swipe
}
/// <summary>
@ -68,6 +72,12 @@ namespace Avalonia.Input.Raw
/// <summary>
/// Gets the input modifiers.
/// </summary>
public RawInputModifiers InputModifiers { get; private set; }
public RawInputModifiers InputModifiers { get; set; }
/// <summary>
/// Points that were traversed by a pointer since the previous relevant event,
/// only valid for Move and TouchUpdate
/// </summary>
public IReadOnlyList<Point>? IntermediatePoints { get; set; }
}
}

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

2
src/Avalonia.Input/TouchDevice.cs

@ -104,7 +104,7 @@ namespace Avalonia.Input
target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root,
args.Position, ev.Timestamp,
new PointerPointProperties(GetModifiers(args.InputModifiers, true), PointerUpdateKind.Other),
GetKeyModifiers(args.InputModifiers)));
GetKeyModifiers(args.InputModifiers), args.IntermediatePoints));
}

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

@ -321,11 +321,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

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

3
src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml

@ -1,7 +1,8 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=netstandard"
xmlns:converters="clr-namespace:Avalonia.Controls.Converters;assembly=Avalonia.Controls">
xmlns:converters="clr-namespace:Avalonia.Controls.Converters;assembly=Avalonia.Controls"
x:CompileBindings="True">
<Design.PreviewWith>
<Border Padding="20"
Background="Black">

3
src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml

@ -7,7 +7,8 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=netstandard">
xmlns:sys="clr-namespace:System;assembly=netstandard"
x:CompileBindings="True">
<Design.PreviewWith>
<Border Margin="20, 20, 20, 200">
<CalendarDatePicker Width="200"

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

@ -6,7 +6,9 @@
-->
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:CompileBindings="True"
x:DataType="CalendarItem">
<Design.PreviewWith>
<Border Padding="20">
<Calendar />

8
src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml

@ -1,6 +1,8 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="using:System">
xmlns:sys="using:System"
x:CompileBindings="True"
x:DataType="DataValidationErrors">
<Design.PreviewWith>
<Border Padding="20">
<StackPanel Spacing="20">
@ -27,7 +29,7 @@
<Style Selector="DataValidationErrors">
<Style.Resources>
<DataTemplate x:Key="InlineDataValidationErrorTemplate">
<ItemsControl Items="{Binding}" TextBlock.Foreground="{DynamicResource SystemControlErrorTextForegroundBrush}">
<ItemsControl Items="{Binding}" x:DataType="DataValidationErrors" TextBlock.Foreground="{DynamicResource SystemControlErrorTextForegroundBrush}">
<ItemsControl.Styles>
<Style Selector="TextBlock">
<Setter Property="TextWrapping" Value="Wrap" />
@ -54,7 +56,7 @@
</DockPanel>
</ControlTemplate>
<DataTemplate x:Key="TooltipDataValidationErrorTemplate">
<DataTemplate x:DataType="DataValidationErrors" x:Key="TooltipDataValidationErrorTemplate">
<Panel Name="PART_InlineErrorTemplatePanel" Background="Transparent">
<Panel.Styles>
<Style Selector="Panel#PART_InlineErrorTemplatePanel">

3
src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml

@ -7,7 +7,8 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=netstandard">
xmlns:sys="clr-namespace:System;assembly=netstandard"
x:CompileBindings="True">
<Design.PreviewWith>
<Border Padding="20">
<DatePicker CornerRadius="10" />

8
src/Avalonia.Themes.Fluent/Controls/Expander.xaml

@ -1,4 +1,6 @@
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:CompileBindings="True">
<Design.PreviewWith>
<Border Padding="20">
<StackPanel Orientation="Vertical" Spacing="20" Width="350">
@ -69,13 +71,13 @@
BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
IsChecked="{Binding Path=IsExpanded, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
IsChecked="{TemplateBinding IsExpanded, Mode=TwoWay}"
IsEnabled="{TemplateBinding IsEnabled}" />
<Border x:Name="ExpanderContent"
Padding="{DynamicResource ExpanderContentPadding}"
Background="{DynamicResource ExpanderDropDownBackground}"
BorderBrush="{DynamicResource ExpanderDropDownBorderBrush}"
IsVisible="{Binding Path=IsExpanded, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}">
IsVisible="{TemplateBinding IsExpanded, Mode=TwoWay}">
<ContentPresenter x:Name="PART_ContentPresenter"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"

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

@ -142,7 +142,7 @@
<Rectangle Fill="{DynamicResource SystemControlHighlightAltBaseMediumLowBrush}" Height="1" Margin="0,5,0,0" DockPanel.Dock="Bottom"/>
<DockPanel Margin="4,0">
<Button Command="{Binding GoUp}" DockPanel.Dock="Left">
<Path Data="M 0 7 L 7 0 L 14 7 M 7 0 L 7 16" Stroke="{Binding $parent[Button].Foreground}" StrokeThickness="1" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,1,0,-1"/>
<Path Data="M 0 7 L 7 0 L 14 7 M 7 0 L 7 16" Stroke="{CompiledBinding $parent[Button].Foreground}" StrokeThickness="1" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,1,0,-1"/>
</Button>
<TextBox x:Name="Location" Text="{Binding Location}">
<TextBox.KeyBindings>
@ -321,4 +321,4 @@
<Style Selector="dialogs|ManagedFileChooser /template/ UniformGrid#Finalize > Button /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
</Style>
</Styles>
</Styles>

6
src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml

@ -1,7 +1,9 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="clr-namespace:Avalonia.Controls.Converters;assembly=Avalonia.Controls"
xmlns:sys="clr-namespace:System;assembly=netstandard">
xmlns:sys="clr-namespace:System;assembly=netstandard"
x:DataType="MenuItem"
x:CompileBindings="True">
<Design.PreviewWith>
<Border Padding="20"
Width="400"
@ -158,7 +160,7 @@
</ContentPresenter>
<Popup Name="PART_Popup"
WindowManagerAddShadowHint="False"
MinWidth="{Binding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}"
MinWidth="{ReflectionBinding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}"
IsLightDismissEnabled="True"
IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}"
OverlayInputPassThroughElement="{Binding $parent[Menu]}">

3
src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml

@ -1,6 +1,7 @@
<Style xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Avalonia.Themes.Fluent"
x:CompileBindings="True"
Selector="NativeMenuBar">
<Style.Resources>
<local:InverseBooleanValueConverter x:Key="AvaloniaThemesDefaultNativeMenuBarInverseBooleanValueConverter" Default="True"/>
@ -11,7 +12,7 @@
IsVisible="{Binding $parent[TopLevel].(NativeMenu.IsNativeMenuExported), Converter={StaticResource AvaloniaThemesDefaultNativeMenuBarInverseBooleanValueConverter}}"
Items="{Binding $parent[TopLevel].(NativeMenu.Menu).Items}">
<Menu.Styles>
<Style Selector="MenuItem">
<Style x:DataType="NativeMenuItem" Selector="MenuItem">
<Setter Property="Header" Value="{Binding Header}"/>
<Setter Property="InputGesture" Value="{Binding Gesture}"/>
<Setter Property="Items" Value="{Binding Menu.Items}"/>

8
src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml

@ -1,4 +1,6 @@
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:CompileBindings="True">
<Design.PreviewWith>
<Border Padding="20">
<StackPanel Spacing="10">
@ -29,8 +31,8 @@
<Border x:Name="IndeterminateProgressBarIndicator" CornerRadius="{TemplateBinding CornerRadius}" Margin="{TemplateBinding Padding}" Background="{TemplateBinding Foreground}" />
<Border x:Name="IndeterminateProgressBarIndicator2" CornerRadius="{TemplateBinding CornerRadius}" Margin="{TemplateBinding Padding}" Background="{TemplateBinding Foreground}" />
</Panel>
<LayoutTransformControl x:Name="PART_LayoutTransformControl" HorizontalAlignment="Center" VerticalAlignment="Center" IsVisible="{Binding ShowProgressText, RelativeSource={RelativeSource TemplatedParent}}">
<TextBlock Foreground="{DynamicResource SystemControlForegroundBaseHighBrush}" Text="{Binding Value, RelativeSource={RelativeSource TemplatedParent}, StringFormat={}{0:0}%}" />
<LayoutTransformControl x:Name="PART_LayoutTransformControl" HorizontalAlignment="Center" VerticalAlignment="Center" IsVisible="{TemplateBinding ShowProgressText}">
<TextBlock Foreground="{DynamicResource SystemControlForegroundBaseHighBrush}" Text="{ReflectionBinding Value, RelativeSource={RelativeSource TemplatedParent}, StringFormat={}{0:0}%}" />
</LayoutTransformControl>
</Panel>
</Border>

3
src/Avalonia.Themes.Fluent/Controls/TabItem.xaml

@ -1,5 +1,6 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:CompileBindings="True">
<Design.PreviewWith>
<Border Padding="20">
<StackPanel Spacing="20">

3
src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml

@ -1,5 +1,6 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:CompileBindings="True">
<Design.PreviewWith>
<Border Padding="20">
<StackPanel Spacing="20">

2
src/Avalonia.Themes.Fluent/Controls/TextBox.xaml

@ -166,7 +166,7 @@
<Setter Property="InnerRightContent">
<Template>
<ToggleButton Classes="passwordBoxRevealButton"
IsChecked="{Binding $parent[TextBox].RevealPassword, Mode=TwoWay}" />
IsChecked="{CompiledBinding $parent[TextBox].RevealPassword, Mode=TwoWay}" />
</Template>
</Setter>
</Style>

3
src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml

@ -7,7 +7,8 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=netstandard">
xmlns:sys="clr-namespace:System;assembly=netstandard"
x:CompileBindings="True">
<Styles.Resources>
<x:Double x:Key="TimePickerFlyoutPresenterItemHeight">40</x:Double>
<x:Double x:Key="TimePickerSpacerThemeWidth">1</x:Double>

3
src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml

@ -1,6 +1,7 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=netstandard">
xmlns:sys="clr-namespace:System;assembly=netstandard"
x:CompileBindings="True">
<Design.PreviewWith>
<Grid RowDefinitions="Auto,Auto"

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

@ -1,5 +1,7 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:DataType="WindowNotificationManager"
x:CompileBindings="True">
<Style Selector="WindowNotificationManager">
<Setter Property="Margin" Value="0 0"/>
<Setter Property="Template">

45
src/Shared/RenderHelpers/ArcToHelper.cs → src/Avalonia.Visuals/Media/PreciseEllipticArcHelper.cs

@ -1,5 +1,6 @@
// Copyright © 2003-2004, Luc Maisonobe
// 2015 - Alexey Rozanov <thehdotx@gmail.com> - Adaptations for Avalonia and oval center computations
// 2022 - Alexey Rozanov <thehdotx@gmail.com> - Fix for arcs sometimes drawn in inverted order.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with
@ -49,12 +50,10 @@
// Adapted from http://www.spaceroots.org/documents/ellipse/EllipticalArc.java
using System;
using Avalonia.Media;
using Avalonia.Platform;
namespace Avalonia.RenderHelpers
namespace Avalonia.Media
{
static class ArcToHelper
static class PreciseEllipticArcHelper
{
/// <summary>
/// This class represents an elliptical arc on a 2D plane.
@ -292,6 +291,8 @@ namespace Avalonia.RenderHelpers
/// </summary>
internal double G2;
public bool DrawInOppositeDirection { get; set; }
/// <summary>
/// Builds an elliptical arc composed of the full unit circle around (0,0)
/// </summary>
@ -850,7 +851,7 @@ namespace Avalonia.RenderHelpers
/// Builds the arc outline using given StreamGeometryContext and default (max) Bezier curve degree and acceptable error of half a pixel (0.5)
/// </summary>
/// <param name="path">A StreamGeometryContext to output the path commands to</param>
public void BuildArc(IStreamGeometryContextImpl path)
public void BuildArc(StreamGeometryContext path)
{
BuildArc(path, _maxDegree, _defaultFlatness, true);
}
@ -862,7 +863,7 @@ namespace Avalonia.RenderHelpers
/// <param name="degree">degree of the Bezier curve to use</param>
/// <param name="threshold">acceptable error</param>
/// <param name="openNewFigure">if true, a new figure will be started in the specified StreamGeometryContext</param>
public void BuildArc(IStreamGeometryContextImpl path, int degree, double threshold, bool openNewFigure)
public void BuildArc(StreamGeometryContext path, int degree, double threshold, bool openNewFigure)
{
if (degree < 1 || degree > _maxDegree)
throw new ArgumentException($"degree should be between {1} and {_maxDegree}", nameof(degree));
@ -888,8 +889,18 @@ namespace Avalonia.RenderHelpers
}
n = n << 1;
}
dEta = (Eta2 - Eta1) / n;
etaB = Eta1;
if (!DrawInOppositeDirection)
{
dEta = (Eta2 - Eta1) / n;
etaB = Eta1;
}
else
{
dEta = (Eta1 - Eta2) / n;
etaB = Eta2;
}
double cosEtaB = Math.Cos(etaB);
double sinEtaB = Math.Sin(etaB);
double aCosEtaB = A * cosEtaB;
@ -922,6 +933,7 @@ namespace Avalonia.RenderHelpers
*/
//otherwise we're supposed to be already at the (xB,yB)
double t = Math.Tan(0.5 * dEta);
double alpha = Math.Sin(dEta) * (Math.Sqrt(4 + 3 * t * t) - 1) / 3;
@ -1012,7 +1024,7 @@ namespace Avalonia.RenderHelpers
/// <param name="theta">Ellipse theta (angle measured from the abscissa)</param>
/// <param name="isLargeArc">Large Arc Indicator</param>
/// <param name="clockwise">Clockwise direction flag</param>
public static void BuildArc(IStreamGeometryContextImpl path, Point p1, Point p2, Size size, double theta, bool isLargeArc, bool clockwise)
public static void BuildArc(StreamGeometryContext path, Point p1, Point p2, Size size, double theta, bool isLargeArc, bool clockwise)
{
// var orthogonalizer = new RotateTransform(-theta);
@ -1058,7 +1070,7 @@ namespace Avalonia.RenderHelpers
}
double multiplier = Math.Sqrt(numerator / denominator);
double multiplier = Math.Sqrt(Math.Abs(numerator / denominator));
Point mulVec = new Point(rx * p1S.Y / ry, -ry * p1S.X / rx);
int sign = (clockwise != isLargeArc) ? 1 : -1;
@ -1104,9 +1116,16 @@ namespace Avalonia.RenderHelpers
// path.LineTo(c, true, true);
// path.LineTo(clockwise ? p1 : p2, true,true);
path.LineTo(clockwise ? p1 : p2);
var arc = new EllipticalArc(c.X, c.Y, rx, ry, theta, thetaStart, thetaEnd, false);
double ManhattanDistance(Point p1, Point p2) => Math.Abs(p1.X - p2.X) + Math.Abs(p1.Y - p2.Y);
if (ManhattanDistance(p2, new Point(arc.X2, arc.Y2)) > ManhattanDistance(p2, new Point(arc.X1, arc.Y1)))
{
arc.DrawInOppositeDirection = true;
}
arc.BuildArc(path, arc._maxDegree, arc._defaultFlatness, false);
//path.LineTo(p2);
//uncomment this to draw a pie
//path.LineTo(c, true, true);
@ -1136,9 +1155,9 @@ namespace Avalonia.RenderHelpers
}
}
public static void ArcTo(IStreamGeometryContextImpl streamGeometryContextImpl, Point currentPoint, Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection)
public static void ArcTo(StreamGeometryContext streamGeometryContextImpl, Point currentPoint, Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection)
{
EllipticalArc.BuildArc(streamGeometryContextImpl, currentPoint, point, size, rotationAngle*Math.PI/180,
EllipticalArc.BuildArc(streamGeometryContextImpl, currentPoint, point, size, rotationAngle*(Math.PI/180),
isLargeArc,
sweepDirection == SweepDirection.Clockwise);
}

24
src/Avalonia.Visuals/Media/StreamGeometryContext.cs

@ -15,6 +15,8 @@ namespace Avalonia.Media
{
private readonly IStreamGeometryContextImpl _impl;
private Point _currentPoint;
/// <summary>
/// Initializes a new instance of the <see cref="StreamGeometryContext"/> class.
/// </summary>
@ -47,6 +49,24 @@ namespace Avalonia.Media
public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection)
{
_impl.ArcTo(point, size, rotationAngle, isLargeArc, sweepDirection);
_currentPoint = point;
}
/// <summary>
/// Draws an arc to the specified point using polylines, quadratic or cubic Bezier curves
/// Significantly more precise when drawing elliptic arcs with extreme width:height ratios.
/// </summary>
/// <param name="point">The destination point.</param>
/// <param name="size">The radii of an oval whose perimeter is used to draw the angle.</param>
/// <param name="rotationAngle">The rotation angle of the oval that specifies the curve.</param>
/// <param name="isLargeArc">true to draw the arc greater than 180 degrees; otherwise, false.</param>
/// <param name="sweepDirection">
/// A value that indicates whether the arc is drawn in the Clockwise or Counterclockwise direction.
/// </param>
public void PreciseArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection)
{
PreciseEllipticArcHelper.ArcTo(this, _currentPoint, point, size, rotationAngle, isLargeArc, sweepDirection);
}
/// <summary>
@ -57,6 +77,7 @@ namespace Avalonia.Media
public void BeginFigure(Point startPoint, bool isFilled)
{
_impl.BeginFigure(startPoint, isFilled);
_currentPoint = startPoint;
}
/// <summary>
@ -68,6 +89,7 @@ namespace Avalonia.Media
public void CubicBezierTo(Point point1, Point point2, Point point3)
{
_impl.CubicBezierTo(point1, point2, point3);
_currentPoint = point3;
}
/// <summary>
@ -78,6 +100,7 @@ namespace Avalonia.Media
public void QuadraticBezierTo(Point control, Point endPoint)
{
_impl.QuadraticBezierTo(control, endPoint);
_currentPoint = endPoint;
}
/// <summary>
@ -87,6 +110,7 @@ namespace Avalonia.Media
public void LineTo(Point point)
{
_impl.LineTo(point);
_currentPoint = point;
}
/// <summary>

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())
{

1
src/Avalonia.X11/Avalonia.X11.csproj

@ -9,6 +9,7 @@
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
<ProjectReference Include="..\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj" />
<Compile Include="..\Shared\RawEventGrouping.cs" />
</ItemGroup>
</Project>

99
src/Avalonia.X11/X11Window.cs

@ -7,6 +7,7 @@ using System.Reactive.Disposables;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Automation.Peers;
using System.Threading;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives.PopupPositioning;
@ -50,15 +51,7 @@ namespace Avalonia.X11
private double? _scalingOverride;
private bool _disabled;
private TransparencyHelper _transparencyHelper;
public object SyncRoot { get; } = new object();
class InputEventContainer
{
public RawInputEventArgs Event;
}
private readonly Queue<InputEventContainer> _inputQueue = new Queue<InputEventContainer>();
private InputEventContainer _lastEvent;
private RawEventGrouper _rawEventGrouper;
private bool _useRenderWindow = false;
public X11Window(AvaloniaX11Platform platform, IWindowImpl popupParent)
{
@ -182,6 +175,8 @@ namespace Avalonia.X11
UpdateMotifHints();
UpdateSizeHints(null);
_rawEventGrouper = new RawEventGrouper(e => Input?.Invoke(e));
_transparencyHelper = new TransparencyHelper(_x11, _handle, platform.Globals);
_transparencyHelper.SetTransparencyRequest(WindowTransparencyLevel.None);
@ -318,13 +313,8 @@ namespace Avalonia.X11
public double RenderScaling
{
get
{
lock (SyncRoot)
return _scaling;
}
private set => _scaling = value;
get => Interlocked.CompareExchange(ref _scaling, 0.0, 0.0);
private set => Interlocked.Exchange(ref _scaling, value);
}
public double DesktopScaling => RenderScaling;
@ -379,11 +369,6 @@ namespace Avalonia.X11
}
void OnEvent(ref XEvent ev)
{
lock (SyncRoot)
OnEventSync(ref ev);
}
void OnEventSync(ref XEvent ev)
{
if (ev.type == XEventName.MapNotify)
{
@ -545,32 +530,29 @@ namespace Avalonia.X11
private bool UpdateScaling(bool skipResize = false)
{
lock (SyncRoot)
double newScaling;
if (_scalingOverride.HasValue)
newScaling = _scalingOverride.Value;
else
{
double newScaling;
if (_scalingOverride.HasValue)
newScaling = _scalingOverride.Value;
else
{
var monitor = _platform.X11Screens.Screens.OrderBy(x => x.PixelDensity)
.FirstOrDefault(m => m.Bounds.Contains(Position));
newScaling = monitor?.PixelDensity ?? RenderScaling;
}
if (RenderScaling != newScaling)
{
var oldScaledSize = ClientSize;
RenderScaling = newScaling;
ScalingChanged?.Invoke(RenderScaling);
UpdateImePosition();
SetMinMaxSize(_scaledMinMaxSize.minSize, _scaledMinMaxSize.maxSize);
if(!skipResize)
Resize(oldScaledSize, true, PlatformResizeReason.DpiChange);
return true;
}
var monitor = _platform.X11Screens.Screens.OrderBy(x => x.PixelDensity)
.FirstOrDefault(m => m.Bounds.Contains(Position));
newScaling = monitor?.PixelDensity ?? RenderScaling;
}
return false;
if (RenderScaling != newScaling)
{
var oldScaledSize = ClientSize;
RenderScaling = newScaling;
ScalingChanged?.Invoke(RenderScaling);
UpdateImePosition();
SetMinMaxSize(_scaledMinMaxSize.minSize, _scaledMinMaxSize.maxSize);
if(!skipResize)
Resize(oldScaledSize, true, PlatformResizeReason.DpiChange);
return true;
}
return false;
}
private WindowState _lastWindowState;
@ -736,33 +718,14 @@ namespace Avalonia.X11
if (args is RawDragEvent drag)
drag.Location = drag.Location / RenderScaling;
_lastEvent = new InputEventContainer() {Event = args};
_inputQueue.Enqueue(_lastEvent);
if (_inputQueue.Count == 1)
{
Dispatcher.UIThread.Post(() =>
{
while (_inputQueue.Count > 0)
{
Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1);
var ev = _inputQueue.Dequeue();
Input?.Invoke(ev.Event);
}
}, DispatcherPriority.Input);
}
_rawEventGrouper.HandleEvent(args);
}
void MouseEvent(RawPointerEventType type, ref XEvent ev, XModifierMask mods)
{
var mev = new RawPointerEventArgs(
_mouse, (ulong)ev.ButtonEvent.time.ToInt64(), _inputRoot,
type, new Point(ev.ButtonEvent.x, ev.ButtonEvent.y), TranslateModifiers(mods));
if(type == RawPointerEventType.Move && _inputQueue.Count>0 && _lastEvent.Event is RawPointerEventArgs ma)
if (ma.Type == RawPointerEventType.Move)
{
_lastEvent.Event = mev;
return;
}
type, new Point(ev.ButtonEvent.x, ev.ButtonEvent.y), TranslateModifiers(mods));
ScheduleInput(mev, ref ev);
}
@ -790,6 +753,12 @@ namespace Avalonia.X11
void Cleanup()
{
if (_rawEventGrouper != null)
{
_rawEventGrouper.Dispose();
_rawEventGrouper = null;
}
if (_transparencyHelper != null)
{
_transparencyHelper.Dispose();

1
src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj

@ -7,5 +7,6 @@
<ItemGroup>
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
<Compile Include="..\..\Shared\RawEventGrouping.cs" />
</ItemGroup>
</Project>

38
src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs

@ -13,15 +13,16 @@ namespace Avalonia.LinuxFramebuffer.Input.EvDev
private readonly EvDevDeviceDescription[] _deviceDescriptions;
private readonly List<EvDevDeviceHandler> _handlers = new List<EvDevDeviceHandler>();
private int _epoll;
private Queue<RawInputEventArgs> _inputQueue = new Queue<RawInputEventArgs>();
private bool _isQueueHandlerTriggered;
private object _lock = new object();
private Action<RawInputEventArgs> _onInput;
private IInputRoot _inputRoot;
private RawEventGroupingThreadingHelper _inputQueue;
public EvDevBackend(EvDevDeviceDescription[] devices)
{
_deviceDescriptions = devices;
_inputQueue = new RawEventGroupingThreadingHelper(e => _onInput?.Invoke(e));
}
unsafe void InputThread()
@ -49,42 +50,9 @@ namespace Avalonia.LinuxFramebuffer.Input.EvDev
private void OnRawEvent(RawInputEventArgs obj)
{
lock (_lock)
{
_inputQueue.Enqueue(obj);
TriggerQueueHandler();
}
_inputQueue.OnEvent(obj);
}
void TriggerQueueHandler()
{
if (_isQueueHandlerTriggered)
return;
_isQueueHandlerTriggered = true;
Dispatcher.UIThread.Post(InputQueueHandler, DispatcherPriority.Input);
}
void InputQueueHandler()
{
RawInputEventArgs ev;
lock (_lock)
{
_isQueueHandlerTriggered = false;
if(_inputQueue.Count == 0)
return;
ev = _inputQueue.Dequeue();
}
_onInput?.Invoke(ev);
lock (_lock)
{
if (_inputQueue.Count > 0)
TriggerQueueHandler();
}
}
public void Initialize(IScreenInfoProvider info, Action<RawInputEventArgs> onInput)
{

31
src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs

@ -17,15 +17,15 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput
private TouchDevice _touch = new TouchDevice();
private MouseDevice _mouse = new MouseDevice();
private Point _mousePosition;
private readonly Queue<RawInputEventArgs> _inputQueue = new Queue<RawInputEventArgs>();
private readonly RawEventGroupingThreadingHelper _inputQueue;
private Action<RawInputEventArgs> _onInput;
private Dictionary<int, Point> _pointers = new Dictionary<int, Point>();
public LibInputBackend()
{
var ctx = libinput_path_create_context();
_inputQueue = new(e => _onInput?.Invoke(e));
new Thread(()=>InputThread(ctx)).Start();
}
@ -66,30 +66,7 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput
}
}
private void ScheduleInput(RawInputEventArgs ev)
{
lock (_inputQueue)
{
_inputQueue.Enqueue(ev);
if (_inputQueue.Count == 1)
{
Dispatcher.UIThread.Post(() =>
{
while (true)
{
Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1);
RawInputEventArgs dequeuedEvent = null;
lock(_inputQueue)
if (_inputQueue.Count != 0)
dequeuedEvent = _inputQueue.Dequeue();
if (dequeuedEvent == null)
return;
_onInput?.Invoke(dequeuedEvent);
}
}, DispatcherPriority.Input);
}
}
}
private void ScheduleInput(RawInputEventArgs ev) => _inputQueue.OnEvent(ev);
private void HandleTouch(IntPtr ev, LibInputEventType type)
{

43
src/Linux/Avalonia.LinuxFramebuffer/Input/RawEventGroupingThreadingHelper.cs

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using Avalonia.Input.Raw;
using Avalonia.Threading;
namespace Avalonia.LinuxFramebuffer.Input;
internal class RawEventGroupingThreadingHelper : IDisposable
{
private readonly RawEventGrouper _grouper;
private readonly Queue<RawInputEventArgs> _rawQueue = new();
private readonly Action _queueHandler;
public RawEventGroupingThreadingHelper(Action<RawInputEventArgs> eventCallback)
{
_grouper = new RawEventGrouper(eventCallback);
_queueHandler = QueueHandler;
}
private void QueueHandler()
{
lock (_rawQueue)
{
while (_rawQueue.Count > 0)
_grouper.HandleEvent(_rawQueue.Dequeue());
}
}
public void OnEvent(RawInputEventArgs args)
{
lock (_rawQueue)
{
_rawQueue.Enqueue(args);
if (_rawQueue.Count == 1)
{
Dispatcher.UIThread.Post(_queueHandler, DispatcherPriority.Input);
}
}
}
public void Dispose() =>
Dispatcher.UIThread.Post(() => _grouper.Dispose(), DispatcherPriority.Input + 1);
}

129
src/Shared/RawEventGrouping.cs

@ -0,0 +1,129 @@
#nullable enable
using System;
using System.Collections.Generic;
using Avalonia.Collections.Pooled;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Threading;
using JetBrains.Annotations;
namespace Avalonia;
/*
This helper maintains an input queue for backends that handle input asynchronously.
While doing that it groups Move and TouchUpdate events so we could provide GetIntermediatePoints API
*/
internal class RawEventGrouper : IDisposable
{
private readonly Action<RawInputEventArgs> _eventCallback;
private readonly Queue<RawInputEventArgs> _inputQueue = new();
private readonly Action _dispatchFromQueue;
readonly Dictionary<long, RawTouchEventArgs> _lastTouchPoints = new();
RawInputEventArgs? _lastEvent;
public RawEventGrouper(Action<RawInputEventArgs> eventCallback)
{
_eventCallback = eventCallback;
_dispatchFromQueue = DispatchFromQueue;
}
private void AddToQueue(RawInputEventArgs args)
{
_lastEvent = args;
_inputQueue.Enqueue(args);
if (_inputQueue.Count == 1)
Dispatcher.UIThread.Post(_dispatchFromQueue, DispatcherPriority.Input);
}
private void DispatchFromQueue()
{
while (true)
{
if(_inputQueue.Count == 0)
return;
var ev = _inputQueue.Dequeue();
if (_lastEvent == ev)
_lastEvent = null;
if (ev is RawTouchEventArgs { Type: RawPointerEventType.TouchUpdate } touchUpdate)
_lastTouchPoints.Remove(touchUpdate.TouchPointId);
_eventCallback?.Invoke(ev);
if (ev is RawPointerEventArgs { IntermediatePoints: PooledList<Point> list })
list.Dispose();
if (Dispatcher.UIThread.HasJobsWithPriority(DispatcherPriority.Input + 1))
{
Dispatcher.UIThread.Post(_dispatchFromQueue, DispatcherPriority.Input);
return;
}
}
}
public void HandleEvent(RawInputEventArgs args)
{
/*
Try to update already enqueued events if
1) they are still not handled (_lastEvent and _lastTouchPoints shouldn't contain said event in that case)
2) previous event belongs to the same "event block", events in the same block:
- belong from the same device
- are pointer move events (Move/TouchUpdate)
- have the same type
- have same modifiers
Even if nothing is updated and the event is actually enqueued, we need to update the relevant tracking info
*/
if (
args is RawPointerEventArgs pointerEvent
&& _lastEvent != null
&& _lastEvent.Device == args.Device
&& _lastEvent is RawPointerEventArgs lastPointerEvent
&& lastPointerEvent.InputModifiers == pointerEvent.InputModifiers
&& lastPointerEvent.Type == pointerEvent.Type
&& lastPointerEvent.Type is RawPointerEventType.Move or RawPointerEventType.TouchUpdate)
{
if (args is RawTouchEventArgs touchEvent)
{
if (_lastTouchPoints.TryGetValue(touchEvent.TouchPointId, out var lastTouchEvent))
MergeEvents(lastTouchEvent, touchEvent);
else
{
_lastTouchPoints[touchEvent.TouchPointId] = touchEvent;
AddToQueue(touchEvent);
}
}
else
MergeEvents(lastPointerEvent, pointerEvent);
return;
}
else
{
_lastTouchPoints.Clear();
if (args is RawTouchEventArgs { Type: RawPointerEventType.TouchUpdate } touchEvent)
_lastTouchPoints[touchEvent.TouchPointId] = touchEvent;
}
AddToQueue(args);
}
private static void MergeEvents(RawPointerEventArgs last, RawPointerEventArgs current)
{
last.IntermediatePoints ??= new PooledList<Point>();
((PooledList<Point>)last.IntermediatePoints).Add(last.Position);
last.Position = current.Position;
last.Timestamp = current.Timestamp;
last.InputModifiers = current.InputModifiers;
}
public void Dispose()
{
_inputQueue.Clear();
_lastEvent = null;
_lastTouchPoints.Clear();
}
}

1
src/Shared/RenderHelpers/RenderHelpers.projitems

@ -9,7 +9,6 @@
<Import_RootNamespace>Avalonia.RenderHelpers</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)ArcToHelper.cs" />
<Compile Include="$(MSBuildThisFileDirectory)QuadBezierHelper.cs" />
</ItemGroup>
</Project>

126
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;
@ -25,8 +26,8 @@ namespace Avalonia.Web.Blazor
private InputHelperInterop _canvasHelper = null!;
private ElementReference _htmlCanvas;
private ElementReference _inputElement;
private double _dpi;
private SKSize _canvasSize;
private double _dpi = 1;
private SKSize _canvasSize = new (100, 100);
private GRContext? _context;
private GRGlInterface? _glInterface;
@ -248,58 +249,59 @@ namespace Avalonia.Web.Blazor
[Parameter(CaptureUnmatchedValues = true)]
public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; }
protected override void OnAfterRender(bool firstRender)
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
Threading.Dispatcher.UIThread.Post(async () =>
{
_inputHelper = await InputHelperInterop.ImportAsync(Js, _inputElement);
_canvasHelper = await InputHelperInterop.ImportAsync(Js, _htmlCanvas);
_inputHelper.Hide();
_canvasHelper.SetCursor("default");
_topLevelImpl.SetCssCursor = x =>
{
_inputHelper.SetCursor(x);//macOS
_canvasHelper.SetCursor(x);//windows
};
Console.WriteLine("starting html canvas setup");
_interop = await SKHtmlCanvasInterop.ImportAsync(Js, _htmlCanvas, OnRenderFrame);
_inputHelper = await InputHelperInterop.ImportAsync(Js, _inputElement);
_canvasHelper = await InputHelperInterop.ImportAsync(Js, _htmlCanvas);
Console.WriteLine("Interop created");
_jsGlInfo = _interop.InitGL();
_inputHelper.Hide();
_canvasHelper.SetCursor("default");
_topLevelImpl.SetCssCursor = x =>
{
_inputHelper.SetCursor(x); //macOS
_canvasHelper.SetCursor(x); //windows
};
Console.WriteLine("jsglinfo created - init gl");
Console.WriteLine("starting html canvas setup");
_interop = await SKHtmlCanvasInterop.ImportAsync(Js, _htmlCanvas, OnRenderFrame);
_sizeWatcher = await SizeWatcherInterop.ImportAsync(Js, _htmlCanvas, OnSizeChanged);
_dpiWatcher = await DpiWatcherInterop.ImportAsync(Js, OnDpiChanged);
Console.WriteLine("Interop created");
_jsGlInfo = _interop.InitGL();
Console.WriteLine("watchers created.");
Console.WriteLine("jsglinfo created - init gl");
// create the SkiaSharp context
if (_context == null)
{
Console.WriteLine("create glcontext");
_glInterface = GRGlInterface.Create();
_context = GRContext.CreateGl(_glInterface);
var options = AvaloniaLocator.Current.GetService<SkiaOptions>();
// bump the default resource cache limit
_context.SetResourceCacheLimit(options?.MaxGpuResourceSizeBytes ?? 32 * 1024 * 1024);
Console.WriteLine("glcontext created and resource limit set");
}
// create the SkiaSharp context
if (_context == null)
{
Console.WriteLine("create glcontext");
_glInterface = GRGlInterface.Create();
_context = GRContext.CreateGl(_glInterface);
var options = AvaloniaLocator.Current.GetService<SkiaOptions>();
// bump the default resource cache limit
_context.SetResourceCacheLimit(options?.MaxGpuResourceSizeBytes ?? 32 * 1024 * 1024);
Console.WriteLine("glcontext created and resource limit set");
}
_topLevelImpl.SetSurface(_context, _jsGlInfo, ColorType,
new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi);
_topLevelImpl.SetSurface(_context, _jsGlInfo, ColorType,
new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi);
_interop.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi));
_initialised = true;
_initialised = true;
Threading.Dispatcher.UIThread.Post(async () =>
{
_interop.RequestAnimationFrame(true);
_sizeWatcher = await SizeWatcherInterop.ImportAsync(Js, _htmlCanvas, OnSizeChanged);
_dpiWatcher = await DpiWatcherInterop.ImportAsync(Js, OnDpiChanged);
_topLevel.Prepare();
_topLevel.Renderer.Start();
Invalidate();
});
}
}
@ -322,33 +324,47 @@ namespace Avalonia.Web.Blazor
_interop.Dispose();
}
private void ForceBlit()
{
// 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);
}
}
private void OnDpiChanged(double newDpi)
{
_dpi = newDpi;
if (Math.Abs(_dpi - newDpi) > 0.0001)
{
_dpi = newDpi;
_interop.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi));
_topLevelImpl.SetClientSize(_canvasSize, _dpi);
_topLevelImpl.SetClientSize(_canvasSize, _dpi);
Invalidate();
ForceBlit();
}
}
private void OnSizeChanged(SKSize newSize)
{
_canvasSize = newSize;
if (_canvasSize != newSize)
{
_canvasSize = newSize;
_topLevelImpl.SetClientSize(_canvasSize, _dpi);
_interop.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi));
Invalidate();
}
_topLevelImpl.SetClientSize(_canvasSize, _dpi);
public void Invalidate()
{
if (!_initialised || _canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0 || _jsGlInfo == null)
{
Console.WriteLine("invalidate ignored");
return;
ForceBlit();
}
_interop.RequestAnimationFrame(true, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi));
}
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);
}
}
}
}

237
tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs

@ -16,6 +16,8 @@ using Avalonia.VisualTree;
using Xunit;
using Avalonia.Input;
using Avalonia.Rendering;
using System.Threading.Tasks;
using Avalonia.Threading;
namespace Avalonia.Controls.UnitTests.Primitives
{
@ -598,27 +600,44 @@ namespace Avalonia.Controls.UnitTests.Primitives
}
[Fact]
public void Popup_Should_Follow_Placement_Target_On_Window_Move()
public void Popup_Should_Not_Follow_Placement_Target_On_Window_Move_If_Pointer()
{
using (CreateServices())
{
var popup = new Popup { Width = 400, Height = 200 };
var popup = new Popup
{
Width = 400,
Height = 200,
PlacementMode = PlacementMode.Pointer
};
var window = PreparedWindow(popup);
window.Show();
popup.Open();
Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout);
var raised = false;
if (popup.Host is PopupRoot popupRoot)
{
// Moving the window must move the popup (screen coordinates have changed)
var raised = false;
popupRoot.PositionChanged += (_, args) =>
{
Assert.Equal(new PixelPoint(10, 10), args.Point);
raised = true;
};
window.Position = new PixelPoint(10, 10);
Assert.True(raised);
}
else if (popup.Host is OverlayPopupHost overlayPopupHost)
{
overlayPopupHost.PropertyChanged += (_, args) =>
{
if (args.Property == Canvas.TopProperty
|| args.Property == Canvas.LeftProperty)
{
raised = true;
}
};
}
window.Position = new PixelPoint(10, 10);
Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout);
Assert.False(raised);
}
}
@ -635,30 +654,222 @@ namespace Avalonia.Controls.UnitTests.Primitives
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
};
var popup = new Popup() { PlacementTarget = placementTarget, Width = 10, Height = 10 };
var popup = new Popup()
{
PlacementTarget = placementTarget,
PlacementMode = PlacementMode.Bottom,
Width = 10,
Height = 10
};
((ISetLogicalParent)popup).SetParent(popup.PlacementTarget);
var window = PreparedWindow(placementTarget);
window.Show();
popup.Open();
Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout);
// The target's initial placement is (395,295) which is a 10x10 panel centered in a 800x600 window
Assert.Equal(placementTarget.Bounds, new Rect(395D, 295D, 10, 10));
var raised = false;
// Resizing the window to 700x500 must move the popup to (345,255) as this is the new
// location of the placement target
if (popup.Host is PopupRoot popupRoot)
{
// Resizing the window to 700x500 must move the popup to (345,255) as this is the new
// location of the placement target
var raised = false;
popupRoot.PositionChanged += (_, args) =>
{
Assert.Equal(new PixelPoint(345, 255), args.Point);
raised = true;
};
window.PlatformImpl?.Resize(new Size(700D, 500D), PlatformResizeReason.Unspecified);
Assert.True(raised);
}
else if (popup.Host is OverlayPopupHost overlayPopupHost)
{
overlayPopupHost.PropertyChanged += (_, args) =>
{
if ((args.Property == Canvas.TopProperty
|| args.Property == Canvas.LeftProperty)
&& Canvas.GetLeft(overlayPopupHost) == 345
&& Canvas.GetTop(overlayPopupHost) == 255)
{
raised = true;
}
};
}
window.PlatformImpl?.Resize(new Size(700D, 500D), PlatformResizeReason.Unspecified);
Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout);
Assert.True(raised);
}
}
[Fact]
public void Popup_Should_Not_Follow_Placement_Target_On_Window_Resize_If_Pointer_If_Pointer()
{
using (CreateServices())
{
var placementTarget = new Panel()
{
Width = 10,
Height = 10,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
};
var popup = new Popup()
{
PlacementTarget = placementTarget,
PlacementMode = PlacementMode.Pointer,
Width = 10,
Height = 10
};
((ISetLogicalParent)popup).SetParent(popup.PlacementTarget);
var window = PreparedWindow(placementTarget);
window.Show();
popup.Open();
Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout);
// The target's initial placement is (395,295) which is a 10x10 panel centered in a 800x600 window
Assert.Equal(placementTarget.Bounds, new Rect(395D, 295D, 10, 10));
var raised = false;
if (popup.Host is PopupRoot popupRoot)
{
popupRoot.PositionChanged += (_, args) =>
{
raised = true;
};
}
else if (popup.Host is OverlayPopupHost overlayPopupHost)
{
overlayPopupHost.PropertyChanged += (_, args) =>
{
if (args.Property == Canvas.TopProperty
|| args.Property == Canvas.LeftProperty)
{
raised = true;
}
};
}
window.PlatformImpl?.Resize(new Size(700D, 500D), PlatformResizeReason.Unspecified);
Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout);
Assert.False(raised);
}
}
[Fact]
public void Popup_Should_Follow_Placement_Target_On_Target_Moved()
{
using (CreateServices())
{
var placementTarget = new Panel()
{
Width = 10,
Height = 10,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
};
var popup = new Popup()
{
PlacementTarget = placementTarget,
PlacementMode = PlacementMode.Bottom,
Width = 10,
Height = 10
};
((ISetLogicalParent)popup).SetParent(popup.PlacementTarget);
var window = PreparedWindow(placementTarget);
window.Show();
popup.Open();
Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout);
// The target's initial placement is (395,295) which is a 10x10 panel centered in a 800x600 window
Assert.Equal(placementTarget.Bounds, new Rect(395D, 295D, 10, 10));
var raised = false;
// Margin will move placement target
if (popup.Host is PopupRoot popupRoot)
{
popupRoot.PositionChanged += (_, args) =>
{
Assert.Equal(new PixelPoint(400, 305), args.Point);
raised = true;
};
}
else if (popup.Host is OverlayPopupHost overlayPopupHost)
{
overlayPopupHost.PropertyChanged += (_, args) =>
{
if ((args.Property == Canvas.TopProperty
|| args.Property == Canvas.LeftProperty)
&& Canvas.GetLeft(overlayPopupHost) == 400
&& Canvas.GetTop(overlayPopupHost) == 305)
{
raised = true;
}
};
}
placementTarget.Margin = new Thickness(10, 0, 0, 0);
Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout);
Assert.True(raised);
}
}
[Fact]
public void Popup_Should_Not_Follow_Placement_Target_On_Target_Moved_If_Pointer()
{
using (CreateServices())
{
var placementTarget = new Panel()
{
Width = 10,
Height = 10,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
};
var popup = new Popup()
{
PlacementTarget = placementTarget,
PlacementMode = PlacementMode.Pointer,
Width = 10,
Height = 10
};
((ISetLogicalParent)popup).SetParent(popup.PlacementTarget);
var window = PreparedWindow(placementTarget);
window.Show();
popup.Open();
Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout);
// The target's initial placement is (395,295) which is a 10x10 panel centered in a 800x600 window
Assert.Equal(placementTarget.Bounds, new Rect(395D, 295D, 10, 10));
var raised = false;
if (popup.Host is PopupRoot popupRoot)
{
popupRoot.PositionChanged += (_, args) =>
{
raised = true;
};
}
else if (popup.Host is OverlayPopupHost overlayPopupHost)
{
overlayPopupHost.PropertyChanged += (_, args) =>
{
if (args.Property == Canvas.TopProperty
|| args.Property == Canvas.LeftProperty)
{
raised = true;
}
};
}
placementTarget.Margin = new Thickness(10, 0, 0, 0);
Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout);
Assert.False(raised);
}
}

56
tests/Avalonia.RenderTests/Media/StreamGeometryTests.cs

@ -0,0 +1,56 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Xunit;
#if AVALONIA_SKIA
namespace Avalonia.Skia.RenderTests
#else
namespace Avalonia.Direct2D1.RenderTests.Media
#endif
{
public class StreamGeometryTests : TestBase
{
public StreamGeometryTests()
: base(@"Media\StreamGeometry")
{
}
[Fact]
public async Task PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions()
{
var grid = new Avalonia.Controls.Primitives.UniformGrid() { Columns = 2, Rows = 4, Width = 320, Height = 400 };
foreach (var sweepDirection in new[] { SweepDirection.Clockwise, SweepDirection.CounterClockwise })
foreach (var isLargeArc in new[] { false, true })
foreach (var isPrecise in new[] { false, true })
{
Point Pt(double x, double y) => new Point(x, y);
Size Sz(double w, double h) => new Size(w, h);
var streamGeometry = new StreamGeometry();
using (var context = streamGeometry.Open())
{
context.BeginFigure(Pt(20, 20), true);
if(isPrecise)
context.PreciseArcTo(Pt(40, 40), Sz(20, 20), 0, isLargeArc, sweepDirection);
else
context.ArcTo(Pt(40, 40), Sz(20, 20), 0, isLargeArc, sweepDirection);
context.LineTo(Pt(40, 20));
context.LineTo(Pt(20, 20));
context.EndFigure(true);
}
var pathShape = new Avalonia.Controls.Shapes.Path();
pathShape.Data = streamGeometry;
pathShape.Stroke = new SolidColorBrush(Colors.CornflowerBlue);
pathShape.Fill = new SolidColorBrush(Colors.Gold);
pathShape.StrokeThickness = 2;
pathShape.Margin = new Thickness(20);
grid.Children.Add(pathShape);
}
await RenderToFile(grid);
}
}
}

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)

BIN
tests/TestFiles/Direct2D1/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.deferred.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
tests/TestFiles/Direct2D1/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.immediate.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
tests/TestFiles/Skia/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.deferred.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
tests/TestFiles/Skia/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.immediate.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Loading…
Cancel
Save