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 @@

[](https://t.me/Avalonia)
-[](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) []( https://aka.ms/dotnet-discord) [](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) [](#backers) [](#sponsors) 
+[](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) []( https://aka.ms/dotnet-discord) [](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) [](#backers) [](#sponsors) [](https://github.com/sponsors/AvaloniaUI) 
[](https://www.nuget.org/packages/Avalonia) [](https://www.nuget.org/packages/Avalonia) 
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 @@
-
-
-
-
+
+ TopLevel bound notification manager.
+
+
+
+
+ XAML only notification manager.
+
+
+
+
+
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