From 2b11e17c6de077e370631ac830e60d583209fbce Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 30 May 2023 17:46:59 +0200 Subject: [PATCH 01/38] feat: Also allows using string instead of {x.Type} in ControlTemplate.TargetType --- ...olTemplateTargetTypeMetadataTransformer.cs | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) 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) From 46c4a1395227f0e40ba4bfa95ea42eeb2fbea6e2 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 31 May 2023 10:29:07 +0200 Subject: [PATCH 02/38] Add Test --- .../Xaml/ControlTemplateTests.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) 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() { From 8b0f2588fa70df49dfd72233f75c4de60428adc5 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 28 Aug 2023 12:30:02 +0200 Subject: [PATCH 03/38] Only add embedded controls to the visual tree if they are part of the TextLayout --- .../Documents/InlineUIContainer.cs | 5 +++++ src/Avalonia.Controls/TextBlock.cs | 16 ++++++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) 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/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index ea420c7c45..46503b9185 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); + } + } } } } From 43aff2d6805cd6085e6e942a5eef86e312eb972b Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Mon, 28 Aug 2023 20:11:28 +0200 Subject: [PATCH 04/38] Fix inconsistent naming --- src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj | 11 +++++++---- .../{DBusMenu.xml => com.canonical.dbusmenu.xml} | 0 .../DBusXml/{DBus.xml => org.freedesktop.DBus.xml} | 0 ...otifierItem.xml => org.kde.StatusNotifierItem.xml} | 0 ...rWatcher.xml => org.kde.StatusNotifierWatcher.xml} | 0 5 files changed, 7 insertions(+), 4 deletions(-) rename src/Avalonia.FreeDesktop/DBusXml/{DBusMenu.xml => com.canonical.dbusmenu.xml} (100%) rename src/Avalonia.FreeDesktop/DBusXml/{DBus.xml => org.freedesktop.DBus.xml} (100%) rename src/Avalonia.FreeDesktop/DBusXml/{StatusNotifierItem.xml => org.kde.StatusNotifierItem.xml} (100%) rename src/Avalonia.FreeDesktop/DBusXml/{StatusNotifierWatcher.xml => org.kde.StatusNotifierWatcher.xml} (100%) diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj index 779c41757a..8b472b7722 100644 --- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -26,19 +26,22 @@ - - + + + + - - + + + 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 From d40ce406fc134c7e89a9d4396257a3c1c41a8e0f Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Mon, 28 Aug 2023 20:11:47 +0200 Subject: [PATCH 05/38] Fix vala-panel global menu --- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 77f9d31273..c0ffbc400e 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(); From fa53674842994c86e48bbb72286cdd1c14905551 Mon Sep 17 00:00:00 2001 From: Mrxx99 <33566379+Mrxx99@users.noreply.github.com> Date: Mon, 28 Aug 2023 20:35:34 +0200 Subject: [PATCH 06/38] Add github sponsors badge to readme --- readme.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index fc5e218a92..2c6aae65f3 100644 --- a/readme.md +++ b/readme.md @@ -2,7 +2,9 @@ ![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) From f0ca7afd785b0904aa745c99c41276f48aacf0f0 Mon Sep 17 00:00:00 2001 From: Mrxx99 <33566379+Mrxx99@users.noreply.github.com> Date: Mon, 28 Aug 2023 20:37:36 +0200 Subject: [PATCH 07/38] removed unintentional new lines --- readme.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/readme.md b/readme.md index 2c6aae65f3..783e0aaddc 100644 --- a/readme.md +++ b/readme.md @@ -2,9 +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://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) +[![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) From 089d326d169f376e0b6fdc784370db768591c743 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Tue, 29 Aug 2023 09:23:44 +0000 Subject: [PATCH 08/38] remove hardcoded popup constraint adjustment --- src/Avalonia.Controls/Primitives/OverlayPopupHost.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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(); } From 90d7e937a8bf69a6ace3ef398445406681e76fcc Mon Sep 17 00:00:00 2001 From: jona <93538252+wannkunstbeikor@users.noreply.github.com> Date: Tue, 29 Aug 2023 16:40:40 +0200 Subject: [PATCH 09/38] Fix OnOpenGlInit getting called twice --- src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs | 2 -- 1 file changed, 2 deletions(-) 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; } From 5919aa836df9f38b205fdfcccd3c1a7e12af9dff Mon Sep 17 00:00:00 2001 From: Bill Henning Date: Mon, 28 Aug 2023 15:24:36 -0400 Subject: [PATCH 10/38] Updated Transition to use direct properties via use of new TransitionBase class. --- src/Avalonia.Base/Animation/Transition.cs | 51 ++------- src/Avalonia.Base/Animation/TransitionBase.cs | 100 ++++++++++++++++++ 2 files changed, 108 insertions(+), 43 deletions(-) create mode 100644 src/Avalonia.Base/Animation/TransitionBase.cs 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); + } +} From 21cc95a014abf8060165445473b1298a5287d58a Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 30 Aug 2023 08:46:43 +0200 Subject: [PATCH 11/38] Fix failing test --- tests/Avalonia.Controls.UnitTests/TextBlockTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs index eb1d6f5ea4..fc798fb737 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); From 45e5d25ccfe4c99bc4968963a576e1d805348e34 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 30 Aug 2023 14:25:48 +0200 Subject: [PATCH 12/38] Implemented Geometry.GetWidenedGeometry. --- src/Avalonia.Base/Media/Geometry.cs | 24 ++++++++- src/Avalonia.Base/Media/ImmutableGeometry.cs | 19 +++++++ src/Avalonia.Base/Platform/IGeometryImpl.cs | 8 +++ .../HeadlessPlatformRenderInterface.cs | 2 + src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 22 ++------ src/Skia/Avalonia.Skia/GeometryImpl.cs | 21 ++++++++ .../Helpers/DrawingContextHelper.cs | 28 +++++++++- .../Avalonia.Skia/Helpers/SKPathHelper.cs | 32 +++++++++++ .../Avalonia.Direct2D1/Media/GeometryImpl.cs | 17 ++++++ .../Avalonia.RenderTests/Shapes/PathTests.cs | 50 ++++++++++++++++++ .../GetWidenedPathGeometry_Line.expected.png | Bin 0 -> 1440 bytes ...WidenedPathGeometry_Line_Dash.expected.png | Bin 0 -> 2443 bytes .../GetWidenedPathGeometry_Line.expected.png | Bin 0 -> 1370 bytes ...WidenedPathGeometry_Line_Dash.expected.png | Bin 0 -> 2443 bytes 14 files changed, 201 insertions(+), 22 deletions(-) create mode 100644 src/Avalonia.Base/Media/ImmutableGeometry.cs create mode 100644 src/Skia/Avalonia.Skia/Helpers/SKPathHelper.cs create mode 100644 tests/TestFiles/Direct2D1/Shapes/Path/GetWidenedPathGeometry_Line.expected.png create mode 100644 tests/TestFiles/Direct2D1/Shapes/Path/GetWidenedPathGeometry_Line_Dash.expected.png create mode 100644 tests/TestFiles/Skia/Shapes/Path/GetWidenedPathGeometry_Line.expected.png create mode 100644 tests/TestFiles/Skia/Shapes/Path/GetWidenedPathGeometry_Line_Dash.expected.png 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/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/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 7293874671..a7a2b486b4 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; diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 76d236e18a..9260e102ee 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; @@ -1252,25 +1253,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..0cee36204d 100644 --- a/src/Skia/Avalonia.Skia/GeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs @@ -2,6 +2,7 @@ using System; using System.Diagnostics.CodeAnalysis; using Avalonia.Media; using Avalonia.Platform; +using Avalonia.Skia.Helpers; using SkiaSharp; namespace Avalonia.Skia @@ -75,6 +76,22 @@ namespace Avalonia.Skia return _pathCache.RenderBounds; } + public IGeometryImpl GetWidenedGeometry(IPen pen) + { + var cache = new PathCache(); + cache.UpdateIfNeeded(StrokePath, pen); + + if (cache.ExpandedPath 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) { @@ -191,6 +208,10 @@ namespace Avalonia.Skia paint.StrokeCap = cap.ToSKStrokeCap(); paint.StrokeJoin = join.ToSKStrokeJoin(); paint.StrokeMiter = (float)miterLimit; + + if (DrawingContextHelper.TryCreateDashEffect(pen, out var dashEffect)) + paint.PathEffect = dashEffect; + _path = new SKPath(); paint.GetFillPath(strokePath, _path); 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/SKPathHelper.cs b/src/Skia/Avalonia.Skia/Helpers/SKPathHelper.cs new file mode 100644 index 0000000000..3bbb80e305 --- /dev/null +++ b/src/Skia/Avalonia.Skia/Helpers/SKPathHelper.cs @@ -0,0 +1,32 @@ +using SkiaSharp; + +namespace Avalonia.Skia.Helpers; + +internal static class SKPathHelper +{ + 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; + } +} 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/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 0000000000000000000000000000000000000000..8399b9e790d6031461f36fded22720beeba4933e GIT binary patch literal 1440 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 Date: Tue, 11 Jul 2023 16:29:41 +0200 Subject: [PATCH 13/38] feat: Customize Notifications --- .../ViewModels/NotificationViewModel.cs | 2 +- .../IManagedNotificationManager.cs | 13 +++- .../Notifications/NotificationCard.cs | 65 ++++++++++++++++++- .../WindowNotificationManager.cs | 24 +++---- 4 files changed, 88 insertions(+), 16 deletions(-) diff --git a/samples/ControlCatalog/ViewModels/NotificationViewModel.cs b/samples/ControlCatalog/ViewModels/NotificationViewModel.cs index bcbcb345ef..84a3c74dbf 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(() => diff --git a/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs b/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs index b2e6e9e80b..8d1cb644be 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 { @@ -18,6 +19,14 @@ namespace Avalonia.Controls.Notifications /// Shows a notification. /// /// The content to be displayed. - void Show(object content); + /// The of the notification. + /// the expiration time of the notification after which it will automatically close. If the value is 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. + void Show(object content, + NotificationType type = NotificationType.Information, + TimeSpan? expiration = null, + Action? onClick = null, + Action? onClose = null); } } diff --git a/src/Avalonia.Controls/Notifications/NotificationCard.cs b/src/Avalonia.Controls/Notifications/NotificationCard.cs index 7d5b6cc0ca..da9e15a7f3 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; @@ -61,6 +61,7 @@ namespace Avalonia.Controls.Notifications } } }); + UpdateNotificationType(); } /// @@ -93,6 +94,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 +179,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..d764f2bd78 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -80,32 +80,32 @@ 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) + public async void Show(object content, + NotificationType type = NotificationType.Information, + TimeSpan? expiration = null, + Action? onClick = null, + Action? onClose = null) { - var notification = content as INotification; - var notificationControl = new NotificationCard { - Content = content + Content = content, + NotificationType = type }; 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(); }; @@ -117,12 +117,12 @@ namespace Avalonia.Controls.Notifications _items.OfType().First(i => !i.IsClosing).Close(); } - if (notification != null && notification.Expiration == TimeSpan.Zero) + if (expiration == TimeSpan.Zero) { return; } - await Task.Delay(notification?.Expiration ?? TimeSpan.FromSeconds(5)); + await Task.Delay(expiration ?? TimeSpan.FromSeconds(5)); notificationControl.Close(); } From 7514d7085c8b60583d59de86d5d87048197e3eec Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 13 Jul 2023 16:02:43 +0200 Subject: [PATCH 14/38] Added an option to specify classes --- .../Notifications/IManagedNotificationManager.cs | 4 +++- .../Notifications/WindowNotificationManager.cs | 12 +++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs b/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs index 8d1cb644be..5eba36dbe6 100644 --- a/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs @@ -23,10 +23,12 @@ namespace Avalonia.Controls.Notifications /// the expiration time of the notification after which it will automatically close. If the value is 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 ba added to the notification card void Show(object content, NotificationType type = NotificationType.Information, TimeSpan? expiration = null, Action? onClick = null, - Action? onClose = null); + Action? onClose = null, + string[]? classes = null); } } diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index d764f2bd78..0b7a19707b 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -88,7 +88,8 @@ namespace Avalonia.Controls.Notifications NotificationType type = NotificationType.Information, TimeSpan? expiration = null, Action? onClick = null, - Action? onClose = null) + Action? onClose = null, + string[]? classes = null) { var notificationControl = new NotificationCard { @@ -96,6 +97,15 @@ namespace Avalonia.Controls.Notifications NotificationType = type }; + // Add style classes if any + if (classes != null) + { + foreach (var @class in classes) + { + notificationControl.Classes.Add(@class); + } + } + notificationControl.NotificationClosed += (sender, args) => { onClose?.Invoke(); From 540bade45a0d4c31fbb0dcb6483e6b68c9355e5e Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 21 Jul 2023 15:00:53 +0200 Subject: [PATCH 15/38] Improve WinodwNotificationManager - Allow usage from XAML - Allow any Visual as Host, so it can be used inside UserControl for example - Allow adding style classes for NotificationCards --- .../WindowNotificationManager.cs | 78 +++++++++++++++---- 1 file changed, 61 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index 0b7a19707b..80931d1865 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -1,11 +1,10 @@ using System; using System.Collections; using System.Linq; -using Avalonia.Reactive; using System.Threading.Tasks; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; -using Avalonia.Rendering; +using Avalonia.Threading; using Avalonia.VisualTree; namespace Avalonia.Controls.Notifications @@ -18,6 +17,7 @@ namespace Avalonia.Controls.Notifications public class WindowNotificationManager : TemplatedControl, IManagedNotificationManager { private IList? _items; + private AdornerLayer? adornerLayer; /// /// Defines the property. @@ -50,17 +50,40 @@ namespace Avalonia.Controls.Notifications set { SetValue(MaxItemsProperty, value); } } + /// + /// Defines the property + /// + public static readonly DirectProperty HostProperty = + AvaloniaProperty.RegisterDirect( + nameof(Host), + o => o.Host, + (o, v) => o.Host = v); + + private Visual? _Host; + + /// + /// The Host that this NotificationManger should register to. If the Host is null, the Parent will be used. + /// + public Visual? Host + { + get { return _Host; } + set { SetAndRaise(HostProperty, ref _Host, value); } + } + /// /// Initializes a new instance of the class. /// - /// The window that will host the control. - public WindowNotificationManager(TopLevel? host) + /// The visual that will host the control. + public WindowNotificationManager(Visual? host) : this() { - if (host != null) - { - Install(host); - } + Host = host; + } + /// + /// Initializes a new instance of the class. + /// + public WindowNotificationManager() + { UpdatePseudoClasses(Position); } @@ -73,6 +96,8 @@ namespace Avalonia.Controls.Notifications /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { + base.OnApplyTemplate(e); + var itemsControl = e.NameScope.Find("PART_Items"); _items = itemsControl?.Children; } @@ -120,12 +145,15 @@ namespace Avalonia.Controls.Notifications (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 (_items?.OfType().Count(i => !i.IsClosing) > MaxItems) + { + _items.OfType().First(i => !i.IsClosing).Close(); + } + }); if (expiration == TimeSpan.Zero) { @@ -145,16 +173,32 @@ namespace Avalonia.Controls.Notifications { UpdatePseudoClasses(change.GetNewValue()); } + + if (change.Property == HostProperty) + { + Install(); + } } /// /// Installs the within the - /// of the host . /// - /// The that will be the host. - private void Install(TemplatedControl host) + private void Install() { - var adornerLayer = host.FindDescendantOfType()?.AdornerLayer; + // unregister from AdornerLayer if this control was already installed + if (adornerLayer is not null && !adornerLayer.Children.Contains(this)) + { + adornerLayer.Children.Remove(this); + } + + // Try to get the host. If host was null, use the TopLevel instead. + var host = Host ?? Parent as Visual; + + if (host is null) throw new InvalidOperationException("NotificationControl cannot be installed. Host was not found."); + + adornerLayer = host is TopLevel + ? host.FindDescendantOfType()?.AdornerLayer + : AdornerLayer.GetAdornerLayer(host); if (adornerLayer is not null) { From eadf63ce450a5daa9f61e8fd72f995616210ee6a Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 24 Jul 2023 12:15:49 +0200 Subject: [PATCH 16/38] Revert breaking change --- .../IManagedNotificationManager.cs | 8 ++++++- .../WindowNotificationManager.cs | 21 +++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs b/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs index 5eba36dbe6..94d927f8ca 100644 --- a/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs @@ -15,6 +15,12 @@ namespace Avalonia.Controls.Notifications [NotClientImplementable] public interface IManagedNotificationManager : INotificationManager { + /// + /// Shows a notification. + /// + /// The content to be displayed. + void Show(object content); + /// /// Shows a notification. /// @@ -25,7 +31,7 @@ namespace Avalonia.Controls.Notifications /// an Action to be run when the notification is closed. /// Style-classes to ba added to the notification card void Show(object content, - NotificationType type = NotificationType.Information, + NotificationType type, TimeSpan? expiration = null, Action? onClick = null, Action? onClose = null, diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index 80931d1865..26ca8abb42 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -73,8 +73,8 @@ namespace Avalonia.Controls.Notifications /// /// Initializes a new instance of the class. /// - /// The visual that will host the control. - public WindowNotificationManager(Visual? host) : this() + /// The TopLevel that will host the control. + public WindowNotificationManager(TopLevel? host) : this() { Host = host; } @@ -108,9 +108,22 @@ namespace Avalonia.Controls.Notifications Show(content, content.Type, content.Expiration, content.OnClick, content.OnClose); } + /// + public async void Show(object content) + { + if (content is INotification notification) + { + Show(notification, notification.Type, notification.Expiration, notification.OnClick, notification.OnClose); + } + else + { + Show(content, NotificationType.Information); + } + } + /// public async void Show(object content, - NotificationType type = NotificationType.Information, + NotificationType type, TimeSpan? expiration = null, Action? onClick = null, Action? onClose = null, @@ -202,7 +215,7 @@ namespace Avalonia.Controls.Notifications if (adornerLayer is not null) { - adornerLayer.Children.Add(this); + // adornerLayer.Children.Add(this); AdornerLayer.SetAdornedElement(this, adornerLayer); } } From 7477aa7330fad59caa68e475e09ace45b0698946 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 24 Jul 2023 13:03:08 +0200 Subject: [PATCH 17/38] Revert more breaking changes --- .../Notifications/IManagedNotificationManager.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs b/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs index 94d927f8ca..bd57f3a86f 100644 --- a/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs @@ -20,21 +20,5 @@ namespace Avalonia.Controls.Notifications /// /// The content to be displayed. void Show(object content); - - /// - /// Shows a notification. - /// - /// The content to be displayed. - /// The of the notification. - /// the expiration time of the notification after which it will automatically close. If the value is 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 ba added to the notification card - void Show(object content, - NotificationType type, - TimeSpan? expiration = null, - Action? onClick = null, - Action? onClose = null, - string[]? classes = null); } } From e83ebc2bb33315b7c033d02fcfd0fae0d455bcf6 Mon Sep 17 00:00:00 2001 From: Tim <47110241+timunie@users.noreply.github.com> Date: Wed, 30 Aug 2023 15:34:05 +0200 Subject: [PATCH 18/38] Handle merge conflicts comming from current master --- .../Notifications/NotificationCard.cs | 36 ------------------- 1 file changed, 36 deletions(-) diff --git a/src/Avalonia.Controls/Notifications/NotificationCard.cs b/src/Avalonia.Controls/Notifications/NotificationCard.cs index da9e15a7f3..d233525056 100644 --- a/src/Avalonia.Controls/Notifications/NotificationCard.cs +++ b/src/Avalonia.Controls/Notifications/NotificationCard.cs @@ -25,42 +25,6 @@ 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(); } From 3f0769a1f44c49aa8e933dfd0ffc2cecfcdda9cb Mon Sep 17 00:00:00 2001 From: Tim <47110241+timunie@users.noreply.github.com> Date: Wed, 30 Aug 2023 15:46:37 +0200 Subject: [PATCH 19/38] Uncomment code that was commented by accident --- .../Notifications/WindowNotificationManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index 26ca8abb42..cb6a844d32 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -215,7 +215,7 @@ namespace Avalonia.Controls.Notifications if (adornerLayer is not null) { - // adornerLayer.Children.Add(this); + adornerLayer.Children.Add(this); AdornerLayer.SetAdornedElement(this, adornerLayer); } } From bfeeb10adf8fa52c4f358ecf6c9c7d8f66dbcf37 Mon Sep 17 00:00:00 2001 From: Tim <47110241+timunie@users.noreply.github.com> Date: Wed, 30 Aug 2023 15:46:57 +0200 Subject: [PATCH 20/38] improved help text --- .../Notifications/WindowNotificationManager.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index cb6a844d32..889e17ba03 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -121,7 +121,15 @@ namespace Avalonia.Controls.Notifications } } - /// + /// + /// 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, From b6e3cc095b11a43b68fa2d91a2fe4a554c523554 Mon Sep 17 00:00:00 2001 From: Bill Henning Date: Wed, 30 Aug 2023 10:46:24 -0400 Subject: [PATCH 21/38] Fix popups in Windows being offset incorrectly by a workaround for another issue. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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; } From 91126ec6f4fd8c2c580d741e48fae8e3390be9b7 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Wed, 30 Aug 2023 14:54:42 +0000 Subject: [PATCH 22/38] add win32 platform options to select process dpi awareness --- src/Windows/Avalonia.Win32/Win32Platform.cs | 42 ++++++++++++++++--- .../Avalonia.Win32/Win32PlatformOptions.cs | 23 ++++++++++ 2 files changed, 59 insertions(+), 6 deletions(-) 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..45e3cb77f7 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,6 @@ public class Win32PlatformOptions /// and only accepts null or . /// public IPlatformGraphics? CustomPlatformGraphics { get; set; } + + public Win32DpiAwareness DpiAwareness { get; set; } = Win32DpiAwareness.PerMonitorDpiAware; } From cc91baa96434fdaf5dc6c30e28b6f8764dddd179 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Wed, 30 Aug 2023 14:59:59 +0000 Subject: [PATCH 23/38] add missing doc --- src/Windows/Avalonia.Win32/Win32PlatformOptions.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Windows/Avalonia.Win32/Win32PlatformOptions.cs b/src/Windows/Avalonia.Win32/Win32PlatformOptions.cs index 45e3cb77f7..eac18dc080 100644 --- a/src/Windows/Avalonia.Win32/Win32PlatformOptions.cs +++ b/src/Windows/Avalonia.Win32/Win32PlatformOptions.cs @@ -159,5 +159,8 @@ public class Win32PlatformOptions /// public IPlatformGraphics? CustomPlatformGraphics { get; set; } + /// + /// Gets or sets the application's DPI awareness. + /// public Win32DpiAwareness DpiAwareness { get; set; } = Win32DpiAwareness.PerMonitorDpiAware; } From 2ec917d665d283326ba4eb35e5bcd24dfc8b8643 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 30 Aug 2023 17:57:52 +0200 Subject: [PATCH 24/38] Dispose of the PathCache. --- src/Skia/Avalonia.Skia/GeometryImpl.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs index 0cee36204d..4f2e8f30d8 100644 --- a/src/Skia/Avalonia.Skia/GeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs @@ -78,7 +78,7 @@ namespace Avalonia.Skia public IGeometryImpl GetWidenedGeometry(IPen pen) { - var cache = new PathCache(); + using var cache = new PathCache(); cache.UpdateIfNeeded(StrokePath, pen); if (cache.ExpandedPath is { } path) @@ -160,7 +160,7 @@ namespace Avalonia.Skia _pathCache = default; } - private struct PathCache + private struct PathCache : IDisposable { private double _width, _miterLimit; private PenLineCap _cap; From 47edb343fb29fc094504f227f6699f32d42ad811 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 30 Aug 2023 18:29:31 +0200 Subject: [PATCH 25/38] Only include dash style when widening bounds. Otherwise we'd be changing the behavior of an existing API (`StrokeContains`) which should be discussed first. --- src/Skia/Avalonia.Skia/GeometryImpl.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs index 4f2e8f30d8..cd6fd76b3c 100644 --- a/src/Skia/Avalonia.Skia/GeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs @@ -79,7 +79,7 @@ namespace Avalonia.Skia public IGeometryImpl GetWidenedGeometry(IPen pen) { using var cache = new PathCache(); - cache.UpdateIfNeeded(StrokePath, pen); + cache.UpdateIfNeeded(StrokePath, pen, includeDashStyle: true); if (cache.ExpandedPath is { } path) { @@ -173,7 +173,7 @@ namespace Avalonia.Skia public Rect RenderBounds => _renderBounds ??= (_path ?? _cachedFor ?? s_emptyPath).Bounds.ToAvaloniaRect(); public SKPath ExpandedPath => _path ?? s_emptyPath; - public void UpdateIfNeeded(SKPath? strokePath, IPen? pen) + public void UpdateIfNeeded(SKPath? strokePath, IPen? pen, bool includeDashStyle = false) { var strokeWidth = pen?.Thickness ?? 0; var miterLimit = pen?.MiterLimit ?? 0; @@ -209,7 +209,7 @@ namespace Avalonia.Skia paint.StrokeJoin = join.ToSKStrokeJoin(); paint.StrokeMiter = (float)miterLimit; - if (DrawingContextHelper.TryCreateDashEffect(pen, out var dashEffect)) + if (includeDashStyle && DrawingContextHelper.TryCreateDashEffect(pen, out var dashEffect)) paint.PathEffect = dashEffect; _path = new SKPath(); From cc9cc71e23a48ecb369696b033c5b21fbd1c9342 Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Thu, 31 Aug 2023 02:26:50 +0200 Subject: [PATCH 26/38] Use Tmds.DBus.SourceGenerator 0.0.10: - Fix global menu - Optimize byte[] variants (e.g icons) --- src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj | 2 +- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 6 ++---- src/Avalonia.FreeDesktop/DBusSystemDialog.cs | 6 +++--- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj index 8b472b7722..28a288f22a 100644 --- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index c0ffbc400e..b9a11b865c 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -281,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())); } } } @@ -326,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; } } } From 8b8b137c30af56285c18acdfdf507f2003b7aea0 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 30 Aug 2023 17:46:12 -0700 Subject: [PATCH 27/38] Remove "Show Native Notification" as we won't have them soon --- samples/ControlCatalog/Pages/NotificationsPage.xaml | 3 +-- .../ControlCatalog/ViewModels/NotificationViewModel.cs | 8 -------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/samples/ControlCatalog/Pages/NotificationsPage.xaml b/samples/ControlCatalog/Pages/NotificationsPage.xaml index 46c1fe52de..425837d4b9 100644 --- a/samples/ControlCatalog/Pages/NotificationsPage.xaml +++ b/samples/ControlCatalog/Pages/NotificationsPage.xaml @@ -6,6 +6,5 @@ + + + + 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"); } } } From b465af28c5b40608e07bed44bdc9e645ea2fbd2f Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 31 Aug 2023 07:52:51 +0200 Subject: [PATCH 33/38] Introduce DrawingContext Push/PopRenderOptions --- src/Avalonia.Base/Media/DrawingContext.cs | 20 +++++++++++++++++- src/Avalonia.Base/Media/DrawingGroup.cs | 21 +++++++++++++++++++ .../Media/PlatformDrawingContext.cs | 4 ++++ .../Platform/IDrawingContextImpl.cs | 11 ++++++++++ .../Drawing/Nodes/RenderDataNodes.cs | 15 +++++++++++++ .../Drawing/RenderDataDrawingContext.cs | 8 +++++++ .../Composition/Server/DrawingContextProxy.cs | 10 +++++++++ .../HeadlessPlatformRenderInterface.cs | 10 +++++++++ src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 16 ++++++++++++++ .../Media/DrawingContextImpl.cs | 14 +++++++++++++ 10 files changed, 128 insertions(+), 1 deletion(-) 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..0a8487b83b 100644 --- a/src/Avalonia.Base/Media/DrawingGroup.cs +++ b/src/Avalonia.Base/Media/DrawingGroup.cs @@ -21,6 +21,9 @@ namespace Avalonia.Media public static readonly StyledProperty OpacityMaskProperty = AvaloniaProperty.Register(nameof(OpacityMask)); + public static readonly StyledProperty RenderOptionsProperty = + AvaloniaProperty.Register(nameof(RenderOptions)); + public static readonly DirectProperty ChildrenProperty = AvaloniaProperty.RegisterDirect( nameof(Children), @@ -53,6 +56,12 @@ namespace Avalonia.Media set => SetValue(OpacityMaskProperty, value); } + public RenderOptions? RenderOptions + { + get => GetValue(RenderOptionsProperty); + set => SetValue(RenderOptionsProperty, value); + } + /// /// Gets or sets the collection that contains the child geometries. /// @@ -75,6 +84,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 +323,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 +342,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/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/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/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 7293874671..b03ae8a2ca 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -542,6 +542,16 @@ namespace Avalonia.Headless { } + + public void PushRenderOptions(RenderOptions renderOptions) + { + + } + + public void PopRenderOptions() + { + + } } private class HeadlessRenderTarget : IRenderTarget diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 76d236e18a..5736b7110c 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -23,6 +23,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 +635,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() { 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(); From 8953db23e4229df64af67365549694a5a2465c17 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 31 Aug 2023 08:19:39 +0200 Subject: [PATCH 34/38] Make DrawingGroup.RenderOptions internal --- src/Avalonia.Base/Media/DrawingGroup.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/Avalonia.Base/Media/DrawingGroup.cs b/src/Avalonia.Base/Media/DrawingGroup.cs index 0a8487b83b..7299bff850 100644 --- a/src/Avalonia.Base/Media/DrawingGroup.cs +++ b/src/Avalonia.Base/Media/DrawingGroup.cs @@ -21,9 +21,6 @@ namespace Avalonia.Media public static readonly StyledProperty OpacityMaskProperty = AvaloniaProperty.Register(nameof(OpacityMask)); - public static readonly StyledProperty RenderOptionsProperty = - AvaloniaProperty.Register(nameof(RenderOptions)); - public static readonly DirectProperty ChildrenProperty = AvaloniaProperty.RegisterDirect( nameof(Children), @@ -56,11 +53,7 @@ namespace Avalonia.Media set => SetValue(OpacityMaskProperty, value); } - public RenderOptions? RenderOptions - { - get => GetValue(RenderOptionsProperty); - set => SetValue(RenderOptionsProperty, value); - } + internal RenderOptions? RenderOptions { get; set; } /// /// Gets or sets the collection that contains the child geometries. From 4e011d4fedb75304f4b29eea6f45bf4af553164a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 31 Aug 2023 11:56:53 +0200 Subject: [PATCH 35/38] Refactor creating a stroked path. - Move the logic into `SKPathHelper.CreateStrokedPath`; called from `PathHelper` and `GetWidenedGeometry` - Use a pen hash code to check if we're up-to-date in `PathHelper` and include the dash style in that --- src/Skia/Avalonia.Skia/GeometryImpl.cs | 59 +++++-------------- src/Skia/Avalonia.Skia/Helpers/PenHelper.cs | 38 ++++++++++++ .../Avalonia.Skia/Helpers/SKPathHelper.cs | 37 +++++++++++- 3 files changed, 89 insertions(+), 45 deletions(-) create mode 100644 src/Skia/Avalonia.Skia/Helpers/PenHelper.cs diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs index cd6fd76b3c..a5797d7fd5 100644 --- a/src/Skia/Avalonia.Skia/GeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Skia.Helpers; +using Avalonia.Utilities; using SkiaSharp; namespace Avalonia.Skia @@ -78,10 +79,7 @@ namespace Avalonia.Skia public IGeometryImpl GetWidenedGeometry(IPen pen) { - using var cache = new PathCache(); - cache.UpdateIfNeeded(StrokePath, pen, includeDashStyle: true); - - if (cache.ExpandedPath is { } path) + 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. @@ -162,60 +160,34 @@ namespace Avalonia.Skia 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, bool includeDashStyle = false) + 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; - } - - var paint = SKPaintCache.Shared.Get(); - paint.IsStroke = true; - paint.StrokeWidth = (float)_width; - paint.StrokeCap = cap.ToSKStrokeCap(); - paint.StrokeJoin = join.ToSKStrokeJoin(); - paint.StrokeMiter = (float)miterLimit; - - if (includeDashStyle && DrawingContextHelper.TryCreateDashEffect(pen, out var dashEffect)) - paint.PathEffect = dashEffect; + _penHash = penHash; + _path?.Dispose(); - _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() @@ -223,7 +195,6 @@ namespace Avalonia.Skia _path?.Dispose(); _path = null; } - } } } 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 index 3bbb80e305..228d68a4d8 100644 --- a/src/Skia/Avalonia.Skia/Helpers/SKPathHelper.cs +++ b/src/Skia/Avalonia.Skia/Helpers/SKPathHelper.cs @@ -1,9 +1,17 @@ -using SkiaSharp; +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); @@ -29,4 +37,31 @@ internal static class SKPathHelper 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); + SKPaintCache.Shared.ReturnReset(paint); + return result; + } } From 568613d21d271010572d1c9fee7b10dc4fc0708f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 31 Aug 2023 12:25:11 +0200 Subject: [PATCH 36/38] Added polyfil of System.HashCode. --- src/Avalonia.Base/Utilities/HashCode.cs | 149 ++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 src/Avalonia.Base/Utilities/HashCode.cs 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 From 23a676dbdd1025d7aa470021a6f851acb6961bbe Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 31 Aug 2023 13:20:31 +0200 Subject: [PATCH 37/38] Dispose path effect. --- src/Skia/Avalonia.Skia/Helpers/SKPathHelper.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Skia/Avalonia.Skia/Helpers/SKPathHelper.cs b/src/Skia/Avalonia.Skia/Helpers/SKPathHelper.cs index 228d68a4d8..e4584cc858 100644 --- a/src/Skia/Avalonia.Skia/Helpers/SKPathHelper.cs +++ b/src/Skia/Avalonia.Skia/Helpers/SKPathHelper.cs @@ -61,6 +61,7 @@ internal static class SKPathHelper var result = new SKPath(); paint.GetFillPath(path, result); + paint.PathEffect?.Dispose(); SKPaintCache.Shared.ReturnReset(paint); return result; } From 3433e57190d77064ec7f5704c27b0a29b510c61d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 1 Sep 2023 11:01:00 +0300 Subject: [PATCH 38/38] Added Avalonia.ForTestingOnly to InternalsVisibleTo list --- build/ExternalConsumers.props | 1 + 1 file changed, 1 insertion(+) 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 @@ +