diff --git a/build/ExternalConsumers.props b/build/ExternalConsumers.props index 12c747f3f0..96cf5cc608 100644 --- a/build/ExternalConsumers.props +++ b/build/ExternalConsumers.props @@ -29,5 +29,6 @@ + diff --git a/readme.md b/readme.md index fc5e218a92..783e0aaddc 100644 --- a/readme.md +++ b/readme.md @@ -2,7 +2,7 @@ ![Header](https://user-images.githubusercontent.com/552074/235865745-2a8e7274-4f66-4f77-8f05-feeb76e7d478.png) [![Telegram](https://raw.githubusercontent.com/Patrolavia/telegram-badge/master/chat.svg)](https://t.me/Avalonia) -[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) [![Discord](https://img.shields.io/badge/discord-join%20chat-46BC99)]( https://aka.ms/dotnet-discord) [![Build Status](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_apis/build/status/AvaloniaUI.Avalonia)](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) [![Backers on Open Collective](https://opencollective.com/Avalonia/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/Avalonia/sponsors/badge.svg)](#sponsors) ![License](https://img.shields.io/github/license/avaloniaui/avalonia.svg) +[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) [![Discord](https://img.shields.io/badge/discord-join%20chat-46BC99)]( https://aka.ms/dotnet-discord) [![Build Status](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_apis/build/status/AvaloniaUI.Avalonia)](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) [![Backers on Open Collective](https://img.shields.io/opencollective/backers/Avalonia?logo=opencollective)](#backers) [![Sponsors on Open Collective](https://img.shields.io/opencollective/sponsors/Avalonia?logo=opencollective)](#sponsors) [![GitHub Sponsors](https://img.shields.io/github/sponsors/AvaloniaUI?logo=github)](https://github.com/sponsors/AvaloniaUI) ![License](https://img.shields.io/github/license/avaloniaui/avalonia.svg)
[![NuGet](https://img.shields.io/nuget/v/Avalonia.svg)](https://www.nuget.org/packages/Avalonia) [![downloads](https://img.shields.io/nuget/dt/avalonia)](https://www.nuget.org/packages/Avalonia) ![Size](https://img.shields.io/github/repo-size/avaloniaui/avalonia.svg) diff --git a/samples/ControlCatalog/Pages/NotificationsPage.xaml b/samples/ControlCatalog/Pages/NotificationsPage.xaml index 46c1fe52de..b4094d8a2e 100644 --- a/samples/ControlCatalog/Pages/NotificationsPage.xaml +++ b/samples/ControlCatalog/Pages/NotificationsPage.xaml @@ -1,11 +1,28 @@ - - + + + + diff --git a/samples/ControlCatalog/Pages/NotificationsPage.xaml.cs b/samples/ControlCatalog/Pages/NotificationsPage.xaml.cs index 6f83e5c366..fac1989976 100644 --- a/samples/ControlCatalog/Pages/NotificationsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/NotificationsPage.xaml.cs @@ -1,5 +1,6 @@ using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Notifications; using Avalonia.Markup.Xaml; using ControlCatalog.ViewModels; @@ -27,7 +28,12 @@ namespace ControlCatalog.Pages { base.OnAttachedToVisualTree(e); - _viewModel.NotificationManager = new Avalonia.Controls.Notifications.WindowNotificationManager(TopLevel.GetTopLevel(this)); + _viewModel.NotificationManager = new WindowNotificationManager(TopLevel.GetTopLevel(this)!); + } + + public void NotificationOnClick() + { + this.Get("ControlNotifications").Show("Notification clicked"); } } } diff --git a/samples/ControlCatalog/ViewModels/NotificationViewModel.cs b/samples/ControlCatalog/ViewModels/NotificationViewModel.cs index bcbcb345ef..40af6e033a 100644 --- a/samples/ControlCatalog/ViewModels/NotificationViewModel.cs +++ b/samples/ControlCatalog/ViewModels/NotificationViewModel.cs @@ -11,7 +11,7 @@ namespace ControlCatalog.ViewModels { ShowCustomManagedNotificationCommand = MiniCommand.Create(() => { - NotificationManager?.Show(new NotificationViewModel() { Title = "Hey There!", Message = "Did you know that Avalonia now supports Custom In-Window Notifications?" , NotificationManager = NotificationManager}); + NotificationManager?.Show(new NotificationViewModel() { Title = "Hey There!", Message = "Did you know that Avalonia now supports Custom In-Window Notifications?" , NotificationManager = NotificationManager}, NotificationType.Warning); }); ShowManagedNotificationCommand = MiniCommand.Create(() => @@ -19,11 +19,6 @@ namespace ControlCatalog.ViewModels NotificationManager?.Show(new Avalonia.Controls.Notifications.Notification("Welcome", "Avalonia now supports Notifications.", NotificationType.Information)); }); - ShowNativeNotificationCommand = MiniCommand.Create(() => - { - NotificationManager?.Show(new Avalonia.Controls.Notifications.Notification("Error", "Native Notifications are not quite ready. Coming soon.", NotificationType.Error)); - }); - YesCommand = MiniCommand.Create(() => { NotificationManager?.Show(new Avalonia.Controls.Notifications.Notification("Avalonia Notifications", "Start adding notifications to your app today.")); @@ -45,8 +40,5 @@ namespace ControlCatalog.ViewModels public MiniCommand ShowCustomManagedNotificationCommand { get; } public MiniCommand ShowManagedNotificationCommand { get; } - - public MiniCommand ShowNativeNotificationCommand { get; } - } } diff --git a/src/Avalonia.Base/Animation/Transition.cs b/src/Avalonia.Base/Animation/Transition.cs index 519ed52578..2ad92c9183 100644 --- a/src/Avalonia.Base/Animation/Transition.cs +++ b/src/Avalonia.Base/Animation/Transition.cs @@ -1,53 +1,22 @@ using System; -using System.Diagnostics.CodeAnalysis; -using Avalonia.Animation.Easings; namespace Avalonia.Animation { /// /// Defines how a property should be animated using a transition. /// - public abstract class Transition : AvaloniaObject, ITransition + public abstract class Transition : TransitionBase { - private AvaloniaProperty? _prop; - - /// - /// Gets or sets the duration of the transition. - /// - public TimeSpan Duration { get; set; } - - /// - /// Gets or sets delay before starting the transition. - /// - public TimeSpan Delay { get; set; } = TimeSpan.Zero; - - /// - /// Gets the easing class to be used. - /// - public Easing Easing { get; set; } = new LinearEasing(); - - /// - [DisallowNull] - public AvaloniaProperty? Property + static Transition() { - get - { - return _prop; - } - set - { - if (!(value.PropertyType.IsAssignableFrom(typeof(T)))) - throw new InvalidCastException - ($"Invalid property type \"{typeof(T).Name}\" for this transition: {GetType().Name}."); - - _prop = value; - } + PropertyProperty.Changed.AddClassHandler>((x, e) => x.OnPropertyPropertyChanged(e)); } - AvaloniaProperty ITransition.Property + private void OnPropertyPropertyChanged(AvaloniaPropertyChangedEventArgs e) { - get => Property ?? throw new InvalidOperationException("Transition has no property specified."); - set => Property = value; + if ((e.NewValue is AvaloniaProperty newValue) && !newValue.PropertyType.IsAssignableFrom(typeof(T))) + throw new InvalidCastException + ($"Invalid property type \"{typeof(T).Name}\" for this transition: {GetType().Name}."); } /// @@ -55,11 +24,7 @@ namespace Avalonia.Animation /// internal abstract IObservable DoTransition(IObservable progress, T oldValue, T newValue); - /// - IDisposable ITransition.Apply(Animatable control, IClock clock, object? oldValue, object? newValue) - => Apply(control, clock, oldValue, newValue); - - internal virtual IDisposable Apply(Animatable control, IClock clock, object? oldValue, object? newValue) + internal override IDisposable Apply(Animatable control, IClock clock, object? oldValue, object? newValue) { if (Property is null) throw new InvalidOperationException("Transition has no property specified."); diff --git a/src/Avalonia.Base/Animation/TransitionBase.cs b/src/Avalonia.Base/Animation/TransitionBase.cs new file mode 100644 index 0000000000..8cc06ce7ed --- /dev/null +++ b/src/Avalonia.Base/Animation/TransitionBase.cs @@ -0,0 +1,100 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Animation.Easings; + +namespace Avalonia.Animation +{ + /// + /// Defines how a property should be animated using a transition. + /// + public abstract class TransitionBase : AvaloniaObject, ITransition + { + /// + /// Defines the property. + /// + public static readonly DirectProperty DurationProperty = + AvaloniaProperty.RegisterDirect( + nameof(Duration), + o => o._duration, + (o, v) => o._duration = v); + + /// + /// Defines the property. + /// + public static readonly DirectProperty DelayProperty = + AvaloniaProperty.RegisterDirect( + nameof(Delay), + o => o._delay, + (o, v) => o._delay = v); + + /// + /// Defines the property. + /// + public static readonly DirectProperty EasingProperty = + AvaloniaProperty.RegisterDirect( + nameof(Easing), + o => o._easing, + (o, v) => o._easing = v); + + /// + /// Defines the property. + /// + public static readonly DirectProperty PropertyProperty = + AvaloniaProperty.RegisterDirect( + nameof(Property), + o => o._prop, + (o, v) => o._prop = v); + + private TimeSpan _duration; + private TimeSpan _delay = TimeSpan.Zero; + private Easing _easing = new LinearEasing(); + private AvaloniaProperty? _prop; + + /// + /// Gets or sets the duration of the transition. + /// + public TimeSpan Duration + { + get { return _duration; } + set { SetAndRaise(DurationProperty, ref _duration, value); } + } + + /// + /// Gets or sets delay before starting the transition. + /// + public TimeSpan Delay + { + get { return _delay; } + set { SetAndRaise(DelayProperty, ref _delay, value); } + } + + /// + /// Gets the easing class to be used. + /// + public Easing Easing + { + get { return _easing; } + set { SetAndRaise(EasingProperty, ref _easing, value); } + } + + /// + [DisallowNull] + public AvaloniaProperty? Property + { + get { return _prop; } + set { SetAndRaise(PropertyProperty, ref _prop, value); } + } + + AvaloniaProperty ITransition.Property + { + get => Property ?? throw new InvalidOperationException("Transition has no property specified."); + set => Property = value; + } + + /// + IDisposable ITransition.Apply(Animatable control, IClock clock, object? oldValue, object? newValue) + => Apply(control, clock, oldValue, newValue); + + internal abstract IDisposable Apply(Animatable control, IClock clock, object? oldValue, object? newValue); + } +} diff --git a/src/Avalonia.Base/Media/DrawingContext.cs b/src/Avalonia.Base/Media/DrawingContext.cs index c55d6de10f..5d258cb040 100644 --- a/src/Avalonia.Base/Media/DrawingContext.cs +++ b/src/Avalonia.Base/Media/DrawingContext.cs @@ -283,7 +283,8 @@ namespace Avalonia.Media Opacity, Clip, GeometryClip, - OpacityMask + OpacityMask, + RenderOptions } public RestoreState(DrawingContext context, PushedStateType type) @@ -308,6 +309,8 @@ namespace Avalonia.Media _context.PopGeometryClipCore(); else if (_type == PushedStateType.OpacityMask) _context.PopOpacityMaskCore(); + else if (_type == PushedStateType.RenderOptions) + _context.PopRenderOptionsCore(); } } @@ -400,6 +403,20 @@ namespace Avalonia.Media return new PushedState(this); } + /// + /// Pushes render options. + /// + /// The render options. + /// A disposable to undo the render options. + public PushedState PushRenderOptions(RenderOptions renderOptions) + { + PushRenderOptionsCore(renderOptions); + _states ??= StateStackPool.Get(); + _states.Push(new RestoreState(this, RestoreState.PushedStateType.RenderOptions)); + return new PushedState(this); + } + protected abstract void PushRenderOptionsCore(RenderOptions renderOptions); + [Obsolete("Use PushTransform"), EditorBrowsable(EditorBrowsableState.Never)] public PushedState PushPreTransform(Matrix matrix) => PushTransform(matrix); [Obsolete("Use PushTransform"), EditorBrowsable(EditorBrowsableState.Never)] @@ -415,6 +432,7 @@ namespace Avalonia.Media protected abstract void PopOpacityCore(); protected abstract void PopOpacityMaskCore(); protected abstract void PopTransformCore(); + protected abstract void PopRenderOptionsCore(); private static bool PenIsVisible(IPen? pen) { diff --git a/src/Avalonia.Base/Media/DrawingGroup.cs b/src/Avalonia.Base/Media/DrawingGroup.cs index 1f3c74c51c..7299bff850 100644 --- a/src/Avalonia.Base/Media/DrawingGroup.cs +++ b/src/Avalonia.Base/Media/DrawingGroup.cs @@ -53,6 +53,8 @@ namespace Avalonia.Media set => SetValue(OpacityMaskProperty, value); } + internal RenderOptions? RenderOptions { get; set; } + /// /// Gets or sets the collection that contains the child geometries. /// @@ -75,6 +77,7 @@ namespace Avalonia.Media using (context.PushOpacity(Opacity)) using (ClipGeometry != null ? context.PushGeometryClip(ClipGeometry) : default) using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, bounds) : default) + using (RenderOptions != null ? context.PushRenderOptions(RenderOptions.Value) : default) { foreach (var drawing in Children) { @@ -313,6 +316,15 @@ namespace Avalonia.Media drawingGroup.Transform = new MatrixTransform(matrix); } + protected override void PushRenderOptionsCore(RenderOptions renderOptions) + { + // Instantiate a new drawing group and set it as the _currentDrawingGroup + var drawingGroup = PushNewDrawingGroup(); + + // Set the render options on the new DrawingGroup + drawingGroup.RenderOptions = renderOptions; + } + protected override void PopClipCore() => Pop(); protected override void PopGeometryClipCore() => Pop(); @@ -323,6 +335,8 @@ namespace Avalonia.Media protected override void PopTransformCore() => Pop(); + protected override void PopRenderOptionsCore() => Pop(); + /// /// Creates a new DrawingGroup for a Push* call by setting the /// _currentDrawingGroup to a newly instantiated DrawingGroup, diff --git a/src/Avalonia.Base/Media/Geometry.cs b/src/Avalonia.Base/Media/Geometry.cs index a66cd616a3..d8fa3bb07f 100644 --- a/src/Avalonia.Base/Media/Geometry.cs +++ b/src/Avalonia.Base/Media/Geometry.cs @@ -21,6 +21,7 @@ namespace Avalonia.Media AvaloniaProperty.Register(nameof(Transform)); private bool _isDirty = true; + private bool _canInvaldate = true; private IGeometryImpl? _platformImpl; static Geometry() @@ -30,9 +31,14 @@ namespace Avalonia.Media internal Geometry() { - } - + + private protected Geometry(IGeometryImpl? platformImpl) + { + _platformImpl = platformImpl; + _isDirty = _canInvaldate = false; + } + /// /// Raised when the geometry changes. /// @@ -118,6 +124,17 @@ namespace Avalonia.Media return PlatformImpl?.StrokeContains(pen, point) == true; } + /// + /// Gets a that is the shape defined by the stroke on the Geometry + /// produced by the specified Pen. + /// + /// The pen to use. + /// The outlined geometry. + public Geometry GetWidenedGeometry(IPen pen) + { + return new ImmutableGeometry(PlatformImpl?.GetWidenedGeometry(pen)); + } + /// /// Marks a property as affecting the geometry's . /// @@ -146,6 +163,9 @@ namespace Avalonia.Media /// protected void InvalidateGeometry() { + if (!_canInvaldate) + return; + _isDirty = true; _platformImpl = null; Changed?.Invoke(this, EventArgs.Empty); diff --git a/src/Avalonia.Base/Media/ImmutableGeometry.cs b/src/Avalonia.Base/Media/ImmutableGeometry.cs new file mode 100644 index 0000000000..ba16329abb --- /dev/null +++ b/src/Avalonia.Base/Media/ImmutableGeometry.cs @@ -0,0 +1,19 @@ +using System; +using Avalonia.Platform; + +namespace Avalonia.Media; + +internal class ImmutableGeometry : Geometry +{ + public ImmutableGeometry(IGeometryImpl? platformImpl) + : base(platformImpl) + { + } + + public override Geometry Clone() => new ImmutableGeometry(PlatformImpl); + + private protected override IGeometryImpl? CreateDefiningGeometry() + { + return PlatformImpl; + } +} diff --git a/src/Avalonia.Base/Media/PlatformDrawingContext.cs b/src/Avalonia.Base/Media/PlatformDrawingContext.cs index b81582f845..410d996db2 100644 --- a/src/Avalonia.Base/Media/PlatformDrawingContext.cs +++ b/src/Avalonia.Base/Media/PlatformDrawingContext.cs @@ -90,6 +90,8 @@ internal sealed class PlatformDrawingContext : DrawingContext _impl.Transform = matrix * current; } + protected override void PushRenderOptionsCore(RenderOptions renderOptions) => _impl.PushRenderOptions(renderOptions); + protected override void PopClipCore() => _impl.PopClip(); protected override void PopGeometryClipCore() => _impl.PopGeometryClip(); @@ -102,6 +104,8 @@ internal sealed class PlatformDrawingContext : DrawingContext _impl.Transform = (_transforms ?? throw new ObjectDisposedException(nameof(PlatformDrawingContext))).Pop(); + protected override void PopRenderOptionsCore() => _impl.PopRenderOptions(); + protected override void DisposeCore() { if (_ownsImpl) diff --git a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs index b52378d3b3..fe411c350d 100644 --- a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs @@ -160,6 +160,17 @@ namespace Avalonia.Platform /// void PopGeometryClip(); + /// + /// Pushes render options. + /// + /// The render options. + void PushRenderOptions(RenderOptions renderOptions); + + /// + /// Pops the latest render options. + /// + void PopRenderOptions(); + /// /// Attempts to get an optional feature from the drawing context implementation. /// diff --git a/src/Avalonia.Base/Platform/IGeometryImpl.cs b/src/Avalonia.Base/Platform/IGeometryImpl.cs index d1964bf07e..0d1e7b972c 100644 --- a/src/Avalonia.Base/Platform/IGeometryImpl.cs +++ b/src/Avalonia.Base/Platform/IGeometryImpl.cs @@ -28,6 +28,14 @@ namespace Avalonia.Platform /// The bounding rectangle. Rect GetRenderBounds(IPen? pen); + /// + /// Gets a geometry that is the shape defined by the stroke on the geometry + /// produced by the specified Pen. + /// + /// The pen to use. + /// The outlined geometry. + IGeometryImpl GetWidenedGeometry(IPen pen); + /// /// Indicates whether the geometry's fill contains the specified point. /// diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataNodes.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataNodes.cs index 2d3b5b0f22..44c8f66bb3 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataNodes.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataNodes.cs @@ -211,3 +211,18 @@ abstract class RenderDataBrushAndPenNode : IRenderDataItemWithServerResources public abstract Rect? Bounds { get; } public abstract bool HitTest(Point p); } + +class RenderDataRenderOptionsNode : RenderDataPushNode +{ + public RenderOptions RenderOptions { get; set; } + + public override void Push(ref RenderDataNodeRenderContext context) + { + context.Context.PushRenderOptions(RenderOptions); + } + + public override void Pop(ref RenderDataNodeRenderContext context) + { + context.Context.PopRenderOptions(); + } +} diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataDrawingContext.cs index e7b14f138d..870a084d31 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataDrawingContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataDrawingContext.cs @@ -259,6 +259,12 @@ internal class RenderDataDrawingContext : DrawingContext }); } + protected override void PushRenderOptionsCore(RenderOptions renderOptions) => Push(new RenderDataRenderOptionsNode() + { + RenderOptions = renderOptions + }); + + protected override void PopClipCore() => Pop(); protected override void PopGeometryClipCore() => Pop(); @@ -269,6 +275,8 @@ internal class RenderDataDrawingContext : DrawingContext protected override void PopTransformCore() => Pop(); + protected override void PopRenderOptionsCore() => Pop(); + internal override void DrawBitmap(IRef? source, double opacity, Rect sourceRect, Rect destRect) { if (source == null || sourceRect.IsEmpty() || destRect.IsEmpty()) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index 61f8c8451f..56a572b343 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -122,6 +122,11 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, _impl.PushOpacityMask(mask, bounds); } + public void PushRenderOptions(RenderOptions renderOptions) + { + _impl.PushRenderOptions(renderOptions); + } + public void PopOpacityMask() { _impl.PopOpacityMask(); @@ -137,6 +142,11 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, _impl.PopGeometryClip(); } + public void PopRenderOptions() + { + _impl.PopRenderOptions(); + } + public object? GetFeature(Type t) => _impl.GetFeature(t); diff --git a/src/Avalonia.Base/Utilities/HashCode.cs b/src/Avalonia.Base/Utilities/HashCode.cs new file mode 100644 index 0000000000..c68c42a9c7 --- /dev/null +++ b/src/Avalonia.Base/Utilities/HashCode.cs @@ -0,0 +1,149 @@ +// Taken from: +// https://github.com/mono/SkiaSharp/blob/main/binding/Binding.Shared/HashCode.cs +// Partial code copied from: +// https://github.com/dotnet/runtime/blob/6072e4d3a7a2a1493f514cdf4be75a3d56580e84/src/libraries/System.Private.CoreLib/src/System/HashCode.cs + +#if NETSTANDARD2_0 +#nullable disable + +using System.Runtime.CompilerServices; + +namespace System; + +internal unsafe struct HashCode +{ + private static readonly uint s_seed = GenerateGlobalSeed(); + + private const uint Prime1 = 2654435761U; + private const uint Prime2 = 2246822519U; + private const uint Prime3 = 3266489917U; + private const uint Prime4 = 668265263U; + private const uint Prime5 = 374761393U; + + private uint _v1, _v2, _v3, _v4; + private uint _queue1, _queue2, _queue3; + private uint _length; + + private static unsafe uint GenerateGlobalSeed() + { + var rnd = new Random(); + var result = rnd.Next(); + return unchecked((uint)result); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Initialize(out uint v1, out uint v2, out uint v3, out uint v4) + { + v1 = s_seed + Prime1 + Prime2; + v2 = s_seed + Prime2; + v3 = s_seed; + v4 = s_seed - Prime1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint Round(uint hash, uint input) => + RotateLeft(hash + input * Prime2, 13) * Prime1; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint QueueRound(uint hash, uint queuedValue) => + RotateLeft(hash + queuedValue * Prime3, 17) * Prime4; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint MixState(uint v1, uint v2, uint v3, uint v4) => + RotateLeft(v1, 1) + RotateLeft(v2, 7) + RotateLeft(v3, 12) + RotateLeft(v4, 18); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint RotateLeft(uint value, int offset) => + (value << offset) | (value >> (32 - offset)); + + private static uint MixEmptyState() => + s_seed + Prime5; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint MixFinal(uint hash) + { + hash ^= hash >> 15; + hash *= Prime2; + hash ^= hash >> 13; + hash *= Prime3; + hash ^= hash >> 16; + return hash; + } + + public void Add(void* value) => + Add(value == null ? 0 : ((IntPtr)value).GetHashCode()); + + public void Add(T value) => + Add(value?.GetHashCode() ?? 0); + + private void Add(int value) + { + uint val = (uint)value; + + // Storing the value of _length locally shaves of quite a few bytes + // in the resulting machine code. + uint previousLength = _length++; + uint position = previousLength % 4; + + // Switch can't be inlined. + + if (position == 0) + _queue1 = val; + else if (position == 1) + _queue2 = val; + else if (position == 2) + _queue3 = val; + else // position == 3 + { + if (previousLength == 3) + Initialize(out _v1, out _v2, out _v3, out _v4); + + _v1 = Round(_v1, _queue1); + _v2 = Round(_v2, _queue2); + _v3 = Round(_v3, _queue3); + _v4 = Round(_v4, val); + } + } + + public int ToHashCode() + { + // Storing the value of _length locally shaves of quite a few bytes + // in the resulting machine code. + uint length = _length; + + // position refers to the *next* queue position in this method, so + // position == 1 means that _queue1 is populated; _queue2 would have + // been populated on the next call to Add. + uint position = length % 4; + + // If the length is less than 4, _v1 to _v4 don't contain anything + // yet. xxHash32 treats this differently. + + uint hash = length < 4 ? MixEmptyState() : MixState(_v1, _v2, _v3, _v4); + + // _length is incremented once per Add(Int32) and is therefore 4 + // times too small (xxHash length is in bytes, not ints). + + hash += length * 4; + + // Mix what remains in the queue + + // Switch can't be inlined right now, so use as few branches as + // possible by manually excluding impossible scenarios (position > 1 + // is always false if position is not > 0). + if (position > 0) + { + hash = QueueRound(hash, _queue1); + if (position > 1) + { + hash = QueueRound(hash, _queue2); + if (position > 2) + hash = QueueRound(hash, _queue3); + } + } + + hash = MixFinal(hash); + return (int)hash; + } +} +#endif diff --git a/src/Avalonia.Controls/Documents/InlineUIContainer.cs b/src/Avalonia.Controls/Documents/InlineUIContainer.cs index f06c8515ee..7f5aa55959 100644 --- a/src/Avalonia.Controls/Documents/InlineUIContainer.cs +++ b/src/Avalonia.Controls/Documents/InlineUIContainer.cs @@ -58,6 +58,11 @@ namespace Avalonia.Controls.Documents internal override void BuildTextRun(IList textRuns) { + if(!Child.IsMeasureValid) + { + Child.Measure(Size.Infinity); + } + textRuns.Add(new EmbeddedControlRun(Child, CreateTextRunProperties())); } diff --git a/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs b/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs index b2e6e9e80b..bd57f3a86f 100644 --- a/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs @@ -1,4 +1,5 @@ -using Avalonia.Metadata; +using System; +using Avalonia.Metadata; namespace Avalonia.Controls.Notifications { diff --git a/src/Avalonia.Controls/Notifications/Notification.cs b/src/Avalonia.Controls/Notifications/Notification.cs index 376df175f3..ace8cba243 100644 --- a/src/Avalonia.Controls/Notifications/Notification.cs +++ b/src/Avalonia.Controls/Notifications/Notification.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; namespace Avalonia.Controls.Notifications { @@ -9,8 +12,10 @@ namespace Avalonia.Controls.Notifications /// This class represents a notification that can be displayed either in a window using /// or by the host operating system (to be implemented). /// - public class Notification : INotification + public class Notification : INotification, INotifyPropertyChanged { + private string? _title, _message; + /// /// Initializes a new instance of the class. /// @@ -35,23 +40,59 @@ namespace Avalonia.Controls.Notifications OnClick = onClick; OnClose = onClose; } + + /// + /// Initializes a new instance of the class. + /// + public Notification() : this(null, null) + { + } /// - public string? Title { get; private set; } + public string? Title + { + get => _title; + set + { + if (_title != value) + { + _title = value; + OnPropertyChanged(); + } + } + } /// - public string? Message { get; private set; } + public string? Message + { + get => _message; + set + { + if (_message != value) + { + _message = value; + OnPropertyChanged(); + } + } + } /// - public NotificationType Type { get; private set; } + public NotificationType Type { get; set; } /// - public TimeSpan Expiration { get; private set; } + public TimeSpan Expiration { get; set; } /// - public Action? OnClick { get; private set; } + public Action? OnClick { get; set; } /// - public Action? OnClose { get; private set; } + public Action? OnClose { get; set; } + + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } } } diff --git a/src/Avalonia.Controls/Notifications/NotificationCard.cs b/src/Avalonia.Controls/Notifications/NotificationCard.cs index 7d5b6cc0ca..d233525056 100644 --- a/src/Avalonia.Controls/Notifications/NotificationCard.cs +++ b/src/Avalonia.Controls/Notifications/NotificationCard.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using Avalonia.Reactive; using Avalonia.Controls.Metadata; @@ -25,42 +25,7 @@ namespace Avalonia.Controls.Notifications /// public NotificationCard() { - this.GetObservable(IsClosedProperty) - .Subscribe(x => - { - if (!IsClosing && !IsClosed) - { - return; - } - - RaiseEvent(new RoutedEventArgs(NotificationClosedEvent)); - }); - - this.GetObservable(ContentProperty) - .Subscribe(x => - { - if (x is INotification notification) - { - switch (notification.Type) - { - case NotificationType.Error: - PseudoClasses.Add(":error"); - break; - - case NotificationType.Information: - PseudoClasses.Add(":information"); - break; - - case NotificationType.Success: - PseudoClasses.Add(":success"); - break; - - case NotificationType.Warning: - PseudoClasses.Add(":warning"); - break; - } - } - }); + UpdateNotificationType(); } /// @@ -93,6 +58,21 @@ namespace Avalonia.Controls.Notifications public static readonly StyledProperty IsClosedProperty = AvaloniaProperty.Register(nameof(IsClosed)); + /// + /// Gets or sets the type of the notification + /// + public NotificationType NotificationType + { + get { return GetValue(NotificationTypeProperty); } + set { SetValue(NotificationTypeProperty, value); } + } + + /// + /// Defines the property + /// + public static readonly StyledProperty NotificationTypeProperty = + AvaloniaProperty.Register(nameof(NotificationType)); + /// /// Defines the event. /// @@ -163,5 +143,52 @@ namespace Avalonia.Controls.Notifications IsClosing = true; } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + base.OnPropertyChanged(e); + + if (e.Property == ContentProperty && e.NewValue is INotification notification) + { + SetValue(NotificationTypeProperty, notification.Type); + } + + if (e.Property == NotificationTypeProperty) + { + UpdateNotificationType(); + } + + if (e.Property == IsClosedProperty) + { + if (!IsClosing && !IsClosed) + { + return; + } + + RaiseEvent(new RoutedEventArgs(NotificationClosedEvent)); + } + } + + private void UpdateNotificationType() + { + switch (NotificationType) + { + case NotificationType.Error: + PseudoClasses.Add(":error"); + break; + + case NotificationType.Information: + PseudoClasses.Add(":information"); + break; + + case NotificationType.Success: + PseudoClasses.Add(":success"); + break; + + case NotificationType.Warning: + PseudoClasses.Add(":warning"); + break; + } + } } } diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index b03099f750..6d9a030ead 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -1,11 +1,12 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Linq; -using Avalonia.Reactive; using System.Threading.Tasks; +using Avalonia.Collections; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; -using Avalonia.Rendering; +using Avalonia.Threading; using Avalonia.VisualTree; namespace Avalonia.Controls.Notifications @@ -18,7 +19,6 @@ namespace Avalonia.Controls.Notifications public class WindowNotificationManager : TemplatedControl, IManagedNotificationManager { private IList? _items; - /// /// Defines the property. /// @@ -49,18 +49,24 @@ namespace Avalonia.Controls.Notifications get { return GetValue(MaxItemsProperty); } set { SetValue(MaxItemsProperty, value); } } - + /// /// Initializes a new instance of the class. /// - /// The window that will host the control. - public WindowNotificationManager(TopLevel? host) + /// The TopLevel that will host the control. + public WindowNotificationManager(TopLevel? host) : this() { - if (host != null) + if (host is not null) { - Install(host); + InstallFromTopLevel(host); } + } + /// + /// Initializes a new instance of the class. + /// + public WindowNotificationManager() + { UpdatePseudoClasses(Position); } @@ -73,6 +79,8 @@ namespace Avalonia.Controls.Notifications /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { + base.OnApplyTemplate(e); + var itemsControl = e.NameScope.Find("PART_Items"); _items = itemsControl?.Children; } @@ -80,49 +88,85 @@ namespace Avalonia.Controls.Notifications /// public void Show(INotification content) { - Show(content as object); + Show(content, content.Type, content.Expiration, content.OnClick, content.OnClose); } /// public async void Show(object content) { - var notification = content as INotification; - + if (content is INotification notification) + { + Show(notification, notification.Type, notification.Expiration, notification.OnClick, notification.OnClose); + } + else + { + Show(content, NotificationType.Information); + } + } + + /// + /// Shows a Notification + /// + /// the content of the notification + /// the type of the notification + /// the expiration time of the notification after which it will automatically close. If the value is Zero then the notification will remain open until the user closes it + /// an Action to be run when the notification is clicked + /// an Action to be run when the notification is closed + /// style classes to apply + public async void Show(object content, + NotificationType type, + TimeSpan? expiration = null, + Action? onClick = null, + Action? onClose = null, + string[]? classes = null) + { + Dispatcher.UIThread.VerifyAccess(); + var notificationControl = new NotificationCard { - Content = content + Content = content, + NotificationType = type }; + // Add style classes if any + if (classes != null) + { + foreach (var @class in classes) + { + notificationControl.Classes.Add(@class); + } + } + notificationControl.NotificationClosed += (sender, args) => { - notification?.OnClose?.Invoke(); + onClose?.Invoke(); _items?.Remove(sender); }; notificationControl.PointerPressed += (sender, args) => { - if (notification != null && notification.OnClick != null) - { - notification.OnClick.Invoke(); - } + onClick?.Invoke(); (sender as NotificationCard)?.Close(); }; - _items?.Add(notificationControl); - - if (_items?.OfType().Count(i => !i.IsClosing) > MaxItems) + Dispatcher.UIThread.Post(() => { - _items.OfType().First(i => !i.IsClosing).Close(); - } + _items?.Add(notificationControl); - if (notification != null && notification.Expiration == TimeSpan.Zero) + if (_items?.OfType().Count(i => !i.IsClosing) > MaxItems) + { + _items.OfType().First(i => !i.IsClosing).Close(); + } + }); + + if (expiration == TimeSpan.Zero) { return; } - await Task.Delay(notification?.Expiration ?? TimeSpan.FromSeconds(5)); + await Task.Delay(expiration ?? TimeSpan.FromSeconds(5)); notificationControl.Close(); } @@ -139,18 +183,30 @@ namespace Avalonia.Controls.Notifications /// /// Installs the within the - /// of the host . /// - /// The that will be the host. - private void Install(TemplatedControl host) + private void InstallFromTopLevel(TopLevel topLevel) { - var adornerLayer = host.FindDescendantOfType()?.AdornerLayer; + topLevel.TemplateApplied += TopLevelOnTemplateApplied; + var adorner = topLevel.FindDescendantOfType()?.AdornerLayer; + if (adorner is not null) + { + adorner.Children.Add(this); + AdornerLayer.SetAdornedElement(this, adorner); + } + } - if (adornerLayer is not null) + private void TopLevelOnTemplateApplied(object? sender, TemplateAppliedEventArgs e) + { + if (Parent is AdornerLayer adornerLayer) { - adornerLayer.Children.Add(this); - AdornerLayer.SetAdornedElement(this, adornerLayer); + adornerLayer.Children.Remove(this); + AdornerLayer.SetAdornedElement(this, null); } + + // Reinstall notification manager on template reapplied. + var topLevel = (TopLevel)sender!; + topLevel.TemplateApplied -= TopLevelOnTemplateApplied; + InstallFromTopLevel(topLevel); } private void UpdatePseudoClasses(NotificationPosition position) diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index 37c46e1e7d..f21b608667 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -61,7 +61,7 @@ namespace Avalonia.Controls.Primitives return adorner.GetValue(AdornedElementProperty); } - public static void SetAdornedElement(Visual adorner, Visual adorned) + public static void SetAdornedElement(Visual adorner, Visual? adorned) { adorner.SetValue(AdornedElementProperty, adorned); } diff --git a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs index 43785212de..d63759cc42 100644 --- a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs +++ b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs @@ -78,7 +78,7 @@ namespace Avalonia.Controls.Primitives Rect? rect = null) { _positionerParameters.ConfigurePosition((TopLevel)_overlayLayer.GetVisualRoot()!, target, placement, offset, anchor, - gravity, constraintAdjustment | PopupPositionerConstraintAdjustment.All, rect, FlowDirection); + gravity, constraintAdjustment, rect, FlowDirection); UpdatePosition(); } diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index 86bea8daa5..264b944566 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -195,14 +195,15 @@ namespace Avalonia.Controls.Primitives } /// - /// Locates the first ancestor and binds to its properties. Properties which have been set through other means are not bound. + /// Try to attach to TemplatedParent if it is a and binds to its properties. + /// Properties which have been set through other means are not bound. /// /// /// This method is automatically called when the control is attached to a visual tree. /// internal void AttachToScrollViewer() { - var owner = this.FindAncestorOfType(); + var owner = this.TemplatedParent as ScrollViewer; if (owner == null) { diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index cc92bdd752..6af572cc7a 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -679,14 +679,18 @@ namespace Avalonia.Controls _textRuns = textRuns; - foreach (var textRun in _textRuns) + foreach (var textLine in TextLayout.TextLines) { - if (textRun is EmbeddedControlRun controlRun && - controlRun.Control is Control control) + foreach (var run in textLine.TextRuns) { - VisualChildren.Add(control); - - control.Measure(Size.Infinity); + if (run is DrawableTextRun drawable) + { + if (drawable is EmbeddedControlRun controlRun + && controlRun.Control is Control control) + { + VisualChildren.Add(control); + } + } } } } diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj index 779c41757a..28a288f22a 100644 --- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -13,7 +13,7 @@ - + @@ -26,19 +26,22 @@ - - + + + + - - + + + diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 77f9d31273..b9a11b865c 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -58,6 +58,7 @@ namespace Avalonia.FreeDesktop private DBusMenuExporterImpl() { + BackingProperties.Version = 4; BackingProperties.Status = string.Empty; BackingProperties.TextDirection = string.Empty; BackingProperties.IconThemePath = Array.Empty(); @@ -280,11 +281,9 @@ namespace Avalonia.FreeDesktop if (loader is not null) { var icon = loader.LoadIcon(item.Icon.PlatformImpl.Item); - using var ms = new MemoryStream(); icon.Save(ms); - return new DBusVariantItem("ay", - new DBusArrayItem(DBusType.Byte, ms.ToArray().Select(static x => new DBusByteItem(x)))); + return new DBusVariantItem("ay", new DBusByteArrayItem(ms.ToArray())); } } } @@ -325,7 +324,7 @@ namespace Avalonia.FreeDesktop children[c] = new DBusVariantItem("(ia{sv}av)", new DBusStructItem(new DBusItem[] { new DBusInt32Item(layout.Item1), - new DBusArrayItem(DBusType.DictEntry, layout.Item2.Select(static x => new DBusDictEntryItem(new DBusStringItem(x.Key), x.Value))), + new DBusArrayItem(DBusType.DictEntry, layout.Item2.Select(static x => new DBusDictEntryItem(new DBusStringItem(x.Key), x.Value)).ToArray()), new DBusArrayItem(DBusType.Variant, layout.Item3) })); } diff --git a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs index f6964ab92a..096093347b 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs @@ -88,7 +88,7 @@ namespace Avalonia.FreeDesktop if (options.SuggestedFileName is { } currentName) chooserOptions.Add("current_name", new DBusVariantItem("s", new DBusStringItem(currentName))); if (options.SuggestedStartLocation?.TryGetLocalPath() is { } folderPath) - chooserOptions.Add("current_folder", new DBusVariantItem("ay", new DBusArrayItem(DBusType.Byte, Encoding.UTF8.GetBytes(folderPath).Select(static x => new DBusByteItem(x))))); + chooserOptions.Add("current_folder", new DBusVariantItem("ay", new DBusByteArrayItem(Encoding.UTF8.GetBytes(folderPath)))); objectPath = await _fileChooser.SaveFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); var request = new OrgFreedesktopPortalRequest(_connection, "org.freedesktop.portal.Desktop", objectPath); @@ -149,7 +149,7 @@ namespace Avalonia.FreeDesktop if (fileTypes is null) return null; - var filters = new DBusArrayItem(DBusType.Struct, new List()); + var filters = new List(); foreach (var fileType in fileTypes) { @@ -173,7 +173,7 @@ namespace Avalonia.FreeDesktop })); } - return filters.Count > 0 ? new DBusVariantItem("a(sa(us))", filters) : null; + return filters.Count > 0 ? new DBusVariantItem("a(sa(us))", new DBusArrayItem(DBusType.Struct, filters)) : null; } } } diff --git a/src/Avalonia.FreeDesktop/DBusXml/DBusMenu.xml b/src/Avalonia.FreeDesktop/DBusXml/com.canonical.dbusmenu.xml similarity index 100% rename from src/Avalonia.FreeDesktop/DBusXml/DBusMenu.xml rename to src/Avalonia.FreeDesktop/DBusXml/com.canonical.dbusmenu.xml diff --git a/src/Avalonia.FreeDesktop/DBusXml/DBus.xml b/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.DBus.xml similarity index 100% rename from src/Avalonia.FreeDesktop/DBusXml/DBus.xml rename to src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.DBus.xml diff --git a/src/Avalonia.FreeDesktop/DBusXml/StatusNotifierItem.xml b/src/Avalonia.FreeDesktop/DBusXml/org.kde.StatusNotifierItem.xml similarity index 100% rename from src/Avalonia.FreeDesktop/DBusXml/StatusNotifierItem.xml rename to src/Avalonia.FreeDesktop/DBusXml/org.kde.StatusNotifierItem.xml diff --git a/src/Avalonia.FreeDesktop/DBusXml/StatusNotifierWatcher.xml b/src/Avalonia.FreeDesktop/DBusXml/org.kde.StatusNotifierWatcher.xml similarity index 100% rename from src/Avalonia.FreeDesktop/DBusXml/StatusNotifierWatcher.xml rename to src/Avalonia.FreeDesktop/DBusXml/org.kde.StatusNotifierWatcher.xml diff --git a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs index cefc2a4c06..c427ecf38b 100644 --- a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs +++ b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs @@ -108,8 +108,6 @@ namespace Avalonia.OpenGL.Controls _visual.Size = new Vector(Bounds.Width, Bounds.Height); _visual.Surface = _resources.Surface; ElementComposition.SetElementChildVisual(this, _visual); - using (_resources.Context.MakeCurrent()) - OnOpenGlInit(_resources.Context.GlInterface); return true; } diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 7293874671..63940294a9 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -182,6 +182,8 @@ namespace Avalonia.Headless return Bounds.Inflate(pen.Thickness / 2); } + public IGeometryImpl GetWidenedGeometry(IPen pen) => this; + public bool StrokeContains(IPen? pen, Point point) { return false; @@ -542,6 +544,16 @@ namespace Avalonia.Headless { } + + public void PushRenderOptions(RenderOptions renderOptions) + { + + } + + public void PopRenderOptions() + { + + } } private class HeadlessRenderTarget : IRenderTarget diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs index f95d086bf6..4d93408ce8 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs @@ -1,6 +1,7 @@ using System.Linq; using XamlX.Ast; using XamlX.Transform; +using XamlX.Transform.Transformers; using XamlX.TypeSystem; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers @@ -22,26 +23,19 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers IXamlAstTypeReference targetType; var templatableBaseType = context.Configuration.TypeSystem.GetType("Avalonia.Controls.Control"); - - if ((tt?.Values.FirstOrDefault() is XamlTypeExtensionNode tn)) - { - targetType = tn.Value; - } - else + + targetType = tt?.Values.FirstOrDefault() switch { - var parentScope = context.ParentNodes().OfType() - .FirstOrDefault(); - if (parentScope?.ScopeType == AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style) - targetType = parentScope.TargetType; - else if (context.ParentNodes().Skip(1).FirstOrDefault() is XamlAstObjectNode directParentNode - && templatableBaseType.IsAssignableFrom(directParentNode.Type.GetClrType())) - targetType = directParentNode.Type; - else - targetType = new XamlAstClrTypeReference(node, - templatableBaseType, false); - } - - + XamlTypeExtensionNode tn => tn.Value, + XamlAstTextNode textNode => TypeReferenceResolver.ResolveType(context, textNode.Text, false, textNode, true), + _ when context.ParentNodes() + .OfType() + .FirstOrDefault() is { ScopeType: AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style } parentScope => parentScope.TargetType, + _ when context.ParentNodes().Skip(1).FirstOrDefault() is XamlAstObjectNode directParentNode + && templatableBaseType.IsAssignableFrom(directParentNode.Type.GetClrType()) => directParentNode.Type, + _ => new XamlAstClrTypeReference(node, + templatableBaseType, false) + }; return new AvaloniaXamlIlTargetTypeMetadataNode(on, targetType, AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.ControlTemplate); @@ -59,7 +53,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers ControlTemplate, Transitions } - + public AvaloniaXamlIlTargetTypeMetadataNode(IXamlAstValueNode value, IXamlAstTypeReference targetType, ScopeTypes type) : base(value, value) diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 76d236e18a..c3778d8426 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -6,6 +6,7 @@ using System.Threading; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering.Utilities; +using Avalonia.Skia.Helpers; using Avalonia.Utilities; using SkiaSharp; using ISceneBrush = Avalonia.Media.ISceneBrush; @@ -23,6 +24,7 @@ namespace Avalonia.Skia private readonly Vector _dpi; private readonly Stack _maskStack = new(); private readonly Stack _opacityStack = new(); + private readonly Stack _renderOptionsStack = new(); private readonly Matrix? _postTransform; private double _currentOpacity = 1.0f; private readonly bool _disableSubpixelTextRendering; @@ -634,6 +636,21 @@ namespace Avalonia.Skia _currentOpacity = _opacityStack.Pop(); } + /// + public void PushRenderOptions(RenderOptions renderOptions) + { + CheckLease(); + + _renderOptionsStack.Push(RenderOptions); + + RenderOptions = RenderOptions.MergeWith(renderOptions); + } + + public void PopRenderOptions() + { + RenderOptions = _renderOptionsStack.Pop(); + } + /// public virtual void Dispose() { @@ -1252,25 +1269,10 @@ namespace Avalonia.Skia paint.StrokeMiter = (float) pen.MiterLimit; - if (pen.DashStyle?.Dashes != null && pen.DashStyle.Dashes.Count > 0) + if (DrawingContextHelper.TryCreateDashEffect(pen, out var dashEffect)) { - var srcDashes = pen.DashStyle.Dashes; - - var count = srcDashes.Count % 2 == 0 ? srcDashes.Count : srcDashes.Count * 2; - - var dashesArray = new float[count]; - - for (var i = 0; i < count; ++i) - { - dashesArray[i] = (float) srcDashes[i % srcDashes.Count] * paint.StrokeWidth; - } - - var offset = (float)(pen.DashStyle.Offset * pen.Thickness); - - var pe = SKPathEffect.CreateDash(dashesArray, offset); - - paint.PathEffect = pe; - rv.AddDisposable(pe); + paint.PathEffect = dashEffect; + rv.AddDisposable(dashEffect); } return rv; diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs index c1ce4a661f..a5797d7fd5 100644 --- a/src/Skia/Avalonia.Skia/GeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs @@ -2,6 +2,8 @@ using System; using System.Diagnostics.CodeAnalysis; using Avalonia.Media; using Avalonia.Platform; +using Avalonia.Skia.Helpers; +using Avalonia.Utilities; using SkiaSharp; namespace Avalonia.Skia @@ -75,6 +77,19 @@ namespace Avalonia.Skia return _pathCache.RenderBounds; } + public IGeometryImpl GetWidenedGeometry(IPen pen) + { + if (StrokePath is not null && SKPathHelper.CreateStrokedPath(StrokePath, pen) is { } path) + { + // The path returned to us by skia here does not have closed figures. + // Fix that by calling CreateClosedPath. + var closed = SKPathHelper.CreateClosedPath(path); + return new StreamGeometryImpl(closed, closed); + } + + return new StreamGeometryImpl(new SKPath(), null); + } + /// public ITransformedGeometryImpl WithTransform(Matrix transform) { @@ -143,58 +158,36 @@ namespace Avalonia.Skia _pathCache = default; } - private struct PathCache + private struct PathCache : IDisposable { - private double _width, _miterLimit; - private PenLineCap _cap; - private PenLineJoin _join; + private int _penHash; private SKPath? _path, _cachedFor; private Rect? _renderBounds; private static readonly SKPath s_emptyPath = new(); - public Rect RenderBounds => _renderBounds ??= (_path ?? _cachedFor ?? s_emptyPath).Bounds.ToAvaloniaRect(); public SKPath ExpandedPath => _path ?? s_emptyPath; public void UpdateIfNeeded(SKPath? strokePath, IPen? pen) { - var strokeWidth = pen?.Thickness ?? 0; - var miterLimit = pen?.MiterLimit ?? 0; - var cap = pen?.LineCap ?? default; - var join = pen?.LineJoin ?? default; - - if (_cachedFor == strokePath - && _path != null - && cap == _cap - && join == _join - && Math.Abs(_width - strokeWidth) < float.Epsilon - && (join != PenLineJoin.Miter || Math.Abs(_miterLimit - miterLimit) > float.Epsilon)) + if (PenHelper.GetHashCode(pen, includeBrush: false) is { } penHash && + penHash == _penHash && + strokePath == _cachedFor) + { // We are up to date return; + } _renderBounds = null; _cachedFor = strokePath; - _width = strokeWidth; - _cap = cap; - _join = join; - _miterLimit = miterLimit; - - if (strokePath == null || Math.Abs(strokeWidth) < float.Epsilon) - { - _path = null; - return; - } + _penHash = penHash; + _path?.Dispose(); - var paint = SKPaintCache.Shared.Get(); - paint.IsStroke = true; - paint.StrokeWidth = (float)_width; - paint.StrokeCap = cap.ToSKStrokeCap(); - paint.StrokeJoin = join.ToSKStrokeJoin(); - paint.StrokeMiter = (float)miterLimit; - _path = new SKPath(); - paint.GetFillPath(strokePath, _path); + if (strokePath is not null && pen is not null) + _path = SKPathHelper.CreateStrokedPath(strokePath, pen); + else + _path = null; - SKPaintCache.Shared.ReturnReset(paint); } public void Dispose() @@ -202,7 +195,6 @@ namespace Avalonia.Skia _path?.Dispose(); _path = null; } - } } } diff --git a/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs b/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs index 6a726dc9dc..44caa8ae5a 100644 --- a/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs +++ b/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs @@ -1,5 +1,6 @@ -using Avalonia.Platform; -using Avalonia.Rendering; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Media; +using Avalonia.Platform; using SkiaSharp; namespace Avalonia.Skia.Helpers @@ -26,5 +27,28 @@ namespace Avalonia.Skia.Helpers return new DrawingContextImpl(createInfo); } + public static bool TryCreateDashEffect(IPen? pen, [NotNullWhen(true)] out SKPathEffect? effect) + { + if (pen?.DashStyle?.Dashes != null && pen.DashStyle.Dashes.Count > 0) + { + var srcDashes = pen.DashStyle.Dashes; + + var count = srcDashes.Count % 2 == 0 ? srcDashes.Count : srcDashes.Count * 2; + + var dashesArray = new float[count]; + + for (var i = 0; i < count; ++i) + { + dashesArray[i] = (float)srcDashes[i % srcDashes.Count] * (float)pen.Thickness; + } + + var offset = (float)(pen.DashStyle.Offset * pen.Thickness); + effect = SKPathEffect.CreateDash(dashesArray, offset); + return true; + } + + effect = null; + return false; + } } } diff --git a/src/Skia/Avalonia.Skia/Helpers/PenHelper.cs b/src/Skia/Avalonia.Skia/Helpers/PenHelper.cs new file mode 100644 index 0000000000..2f7b6d771d --- /dev/null +++ b/src/Skia/Avalonia.Skia/Helpers/PenHelper.cs @@ -0,0 +1,38 @@ +using System; +using Avalonia.Media; + +namespace Avalonia.Skia.Helpers; + +internal static class PenHelper +{ + /// + /// Gets a hash code for a pen, optionally including the brush. + /// + /// The pen. + /// Whether to include the brush in the hash code. + /// The hash code. + public static int GetHashCode(IPen? pen, bool includeBrush) + { + if (pen is null) + return 0; + + var hash = new HashCode(); + hash.Add(pen.LineCap); + hash.Add(pen.LineJoin); + hash.Add(pen.MiterLimit); + hash.Add(pen.Thickness); + + if (pen.DashStyle is { } dashStyle) + { + hash.Add(dashStyle.Offset); + + for (var i = 0; i < dashStyle.Dashes?.Count; i++) + hash.Add(dashStyle.Dashes[i]); + } + + if (includeBrush) + hash.Add(pen.Brush); + + return hash.ToHashCode(); + } +} diff --git a/src/Skia/Avalonia.Skia/Helpers/SKPathHelper.cs b/src/Skia/Avalonia.Skia/Helpers/SKPathHelper.cs new file mode 100644 index 0000000000..e4584cc858 --- /dev/null +++ b/src/Skia/Avalonia.Skia/Helpers/SKPathHelper.cs @@ -0,0 +1,68 @@ +using System; +using Avalonia.Media; +using Avalonia.Utilities; +using SkiaSharp; + +namespace Avalonia.Skia.Helpers; + +internal static class SKPathHelper +{ + /// + /// Creates a new path that is a closed version of the source path. + /// + /// The source path. + /// A closed path. + public static SKPath CreateClosedPath(SKPath path) + { + using var iter = path.CreateIterator(true); + SKPathVerb verb; + var points = new SKPoint[4]; + var rv = new SKPath(); + while ((verb = iter.Next(points)) != SKPathVerb.Done) + { + if (verb == SKPathVerb.Move) + rv.MoveTo(points[0]); + else if (verb == SKPathVerb.Line) + rv.LineTo(points[1]); + else if (verb == SKPathVerb.Close) + rv.Close(); + else if (verb == SKPathVerb.Quad) + rv.QuadTo(points[1], points[2]); + else if (verb == SKPathVerb.Cubic) + rv.CubicTo(points[1], points[2], points[3]); + else if (verb == SKPathVerb.Conic) + rv.ConicTo(points[1], points[2], iter.ConicWeight()); + + } + + return rv; + } + + /// + /// Creates a path that is the result of a pen being applied to the stroke of the given path. + /// + /// The path to stroke. + /// The pen to use to stroke the path. + /// The resulting path, or null if the pen has 0 thickness. + public static SKPath? CreateStrokedPath(SKPath path, IPen pen) + { + if (MathUtilities.IsZero(pen.Thickness)) + return null; + + var paint = SKPaintCache.Shared.Get(); + paint.IsStroke = true; + paint.StrokeWidth = (float)pen.Thickness; + paint.StrokeCap = pen.LineCap.ToSKStrokeCap(); + paint.StrokeJoin = pen.LineJoin.ToSKStrokeJoin(); + paint.StrokeMiter = (float)pen.MiterLimit; + + if (DrawingContextHelper.TryCreateDashEffect(pen, out var dashEffect)) + paint.PathEffect = dashEffect; + + var result = new SKPath(); + paint.GetFillPath(path, result); + paint.PathEffect?.Dispose(); + SKPaintCache.Shared.ReturnReset(paint); + return result; + } +} diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 5579278f21..8b288598ab 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -24,6 +24,8 @@ namespace Avalonia.Direct2D1.Media private readonly SharpDX.DXGI.SwapChain1 _swapChain; private readonly Action _finishedCallback; + private readonly Stack _renderOptionsStack = new(); + /// /// Initializes a new instance of the class. /// @@ -488,6 +490,18 @@ namespace Avalonia.Direct2D1.Media PopLayer(); } + public void PushRenderOptions(RenderOptions renderOptions) + { + _renderOptionsStack.Push(RenderOptions); + + RenderOptions = RenderOptions.MergeWith(renderOptions); + } + + public void PopRenderOptions() + { + RenderOptions = _renderOptionsStack.Pop(); + } + private void PopLayer() { var layer = _layers.Pop(); diff --git a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs index e1c08e0814..fec9b37aac 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs @@ -47,6 +47,23 @@ namespace Avalonia.Direct2D1.Media } } + public IGeometryImpl GetWidenedGeometry(IPen pen) + { + var result = new PathGeometry(Direct2D1Platform.Direct2D1Factory); + + using (var sink = result.Open()) + { + Geometry.Widen( + (float)pen.Thickness, + pen.ToDirect2DStrokeStyle(Direct2D1Platform.Direct2D1Factory), + 0.25f, + sink); + sink.Close(); + } + + return new StreamGeometryImpl(result); + } + /// public bool FillContains(Point point) { diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 9244da5064..9650c44bef 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -49,7 +49,6 @@ namespace Avalonia.Win32 public Win32Platform() { - SetDpiAwareness(); CreateMessageWindow(); _dispatcher = new Win32DispatcherImpl(_hwnd); } @@ -80,6 +79,9 @@ namespace Avalonia.Win32 public static void Initialize(Win32PlatformOptions options) { s_options = options; + + SetDpiAwareness(); + var renderTimer = options.ShouldRenderOnUIThread ? new UiThreadRenderTimer(60) : new DefaultRenderTimer(60); AvaloniaLocator.CurrentMutable @@ -264,12 +266,31 @@ namespace Avalonia.Win32 var user32 = LoadLibrary("user32.dll"); var method = GetProcAddress(user32, nameof(SetProcessDpiAwarenessContext)); + var dpiAwareness = Options.DpiAwareness; + if (method != IntPtr.Zero) { - if (SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) || - SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE)) + if (dpiAwareness == Win32DpiAwareness.Unaware) { - return; + if (SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_UNAWARE)) + { + return; + } + } + else if (dpiAwareness == Win32DpiAwareness.SystemDpiAware) + { + if (SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_SYSTEM_AWARE)) + { + return; + } + } + else if (dpiAwareness == Win32DpiAwareness.PerMonitorDpiAware) + { + if (SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) || + SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE)) + { + return; + } } } @@ -278,11 +299,20 @@ namespace Avalonia.Win32 if (method != IntPtr.Zero) { - SetProcessDpiAwareness(PROCESS_DPI_AWARENESS.PROCESS_PER_MONITOR_DPI_AWARE); + var awareness = (dpiAwareness) switch + { + Win32DpiAwareness.Unaware => PROCESS_DPI_AWARENESS.PROCESS_DPI_UNAWARE, + Win32DpiAwareness.SystemDpiAware => PROCESS_DPI_AWARENESS.PROCESS_SYSTEM_DPI_AWARE, + Win32DpiAwareness.PerMonitorDpiAware => PROCESS_DPI_AWARENESS.PROCESS_PER_MONITOR_DPI_AWARE, + _ => PROCESS_DPI_AWARENESS.PROCESS_PER_MONITOR_DPI_AWARE, + }; + + SetProcessDpiAwareness(awareness); return; } - SetProcessDPIAware(); + if (dpiAwareness != Win32DpiAwareness.Unaware) + SetProcessDPIAware(); } } } diff --git a/src/Windows/Avalonia.Win32/Win32PlatformOptions.cs b/src/Windows/Avalonia.Win32/Win32PlatformOptions.cs index bbb4c37d7e..eac18dc080 100644 --- a/src/Windows/Avalonia.Win32/Win32PlatformOptions.cs +++ b/src/Windows/Avalonia.Win32/Win32PlatformOptions.cs @@ -25,6 +25,27 @@ public enum Win32RenderingMode Wgl = 3 } +/// +/// Represents the DPI Awareness for the application. +/// +public enum Win32DpiAwareness +{ + /// + /// The application is DPI unaware. + /// + Unaware, + + /// + /// The application is system DPI aware. It will query DPI once and will not adjust to new DPI changes + /// + SystemDpiAware, + + /// + /// The application is per-monitor DPI aware. It adjust its scale factor whenever DPI changes. + /// + PerMonitorDpiAware +} + /// /// Represents the Win32 window composition mode. /// @@ -137,4 +158,9 @@ public class Win32PlatformOptions /// and only accepts null or . /// public IPlatformGraphics? CustomPlatformGraphics { get; set; } + + /// + /// Gets or sets the application's DPI awareness. + /// + public Win32DpiAwareness DpiAwareness { get; set; } = Win32DpiAwareness.PerMonitorDpiAware; } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index e9156e553d..0a75732182 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -552,7 +552,7 @@ namespace Avalonia.Win32 get { // Windows 10 and 11 add a 7 pixel invisible border on the left/right/bottom of windows for resizing - if (Win32Platform.WindowsVersion.Major < 10 || !HasFullDecorations) + if (Win32Platform.WindowsVersion.Major < 10 || !HasFullDecorations || GetStyle().HasFlag(WindowStyles.WS_POPUP)) { return PixelSize.Empty; } diff --git a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs index fd3d80729a..c0afe0c7ff 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs @@ -170,6 +170,7 @@ namespace Avalonia.Controls.UnitTests target.Inlines.Add("123456"); target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); Assert.True(button.IsMeasureValid); Assert.Equal(80, button.DesiredSize.Width); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs index ea2407e18d..e21f6cd276 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs @@ -286,6 +286,24 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.IsType(typeof(ContentPresenter), template.Build(new ContentControl()).Result); } + [Fact] + public void ControlTemplate_With_String_TargetType() + { + var xaml = @" + + + +"; + var template = AvaloniaRuntimeXamlLoader.Parse(xaml); + + Assert.Equal(typeof(ContentControl), template.TargetType); + + Assert.IsType(typeof(ContentPresenter), template.Build(new ContentControl()).Result); + } + + [Fact] public void ControlTemplate_With_Panel_Children_Are_Added() { diff --git a/tests/Avalonia.RenderTests/Shapes/PathTests.cs b/tests/Avalonia.RenderTests/Shapes/PathTests.cs index bf375121de..4f1412990b 100644 --- a/tests/Avalonia.RenderTests/Shapes/PathTests.cs +++ b/tests/Avalonia.RenderTests/Shapes/PathTests.cs @@ -434,5 +434,55 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes await RenderToFile(target); CompareImages(); } + + [Fact] + public async Task GetWidenedPathGeometry_Line() + { + var pen = new Pen(Brushes.Black, 10); + var geometry = StreamGeometry.Parse("M 0,0 L 180,180").GetWidenedGeometry(pen); + + Decorator target = new Decorator + { + Width = 200, + Height = 200, + Child = new Path + { + Stroke = Brushes.Red, + StrokeThickness = 1, + Fill = Brushes.Green, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Data = geometry, + } + }; + + await RenderToFile(target); + CompareImages(); + } + + [Fact] + public async Task GetWidenedPathGeometry_Line_Dash() + { + var pen = new Pen(Brushes.Black, 10, DashStyle.Dash); + var geometry = StreamGeometry.Parse("M 0,0 L 180,180").GetWidenedGeometry(pen); + + Decorator target = new Decorator + { + Width = 200, + Height = 200, + Child = new Path + { + Stroke = Brushes.Red, + StrokeThickness = 1, + Fill = Brushes.Green, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Data = geometry, + } + }; + + await RenderToFile(target); + CompareImages(); + } } } diff --git a/tests/TestFiles/Direct2D1/Shapes/Path/GetWidenedPathGeometry_Line.expected.png b/tests/TestFiles/Direct2D1/Shapes/Path/GetWidenedPathGeometry_Line.expected.png new file mode 100644 index 0000000000..8399b9e790 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Shapes/Path/GetWidenedPathGeometry_Line.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Shapes/Path/GetWidenedPathGeometry_Line_Dash.expected.png b/tests/TestFiles/Direct2D1/Shapes/Path/GetWidenedPathGeometry_Line_Dash.expected.png new file mode 100644 index 0000000000..a09d404883 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Shapes/Path/GetWidenedPathGeometry_Line_Dash.expected.png differ diff --git a/tests/TestFiles/Skia/Shapes/Path/GetWidenedPathGeometry_Line.expected.png b/tests/TestFiles/Skia/Shapes/Path/GetWidenedPathGeometry_Line.expected.png new file mode 100644 index 0000000000..d7e050f689 Binary files /dev/null and b/tests/TestFiles/Skia/Shapes/Path/GetWidenedPathGeometry_Line.expected.png differ diff --git a/tests/TestFiles/Skia/Shapes/Path/GetWidenedPathGeometry_Line_Dash.expected.png b/tests/TestFiles/Skia/Shapes/Path/GetWidenedPathGeometry_Line_Dash.expected.png new file mode 100644 index 0000000000..a09d404883 Binary files /dev/null and b/tests/TestFiles/Skia/Shapes/Path/GetWidenedPathGeometry_Line_Dash.expected.png differ