From 5778afd76aad5fa4c4a7c5aff7d6e16f84c3b1bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Sat, 25 Feb 2023 02:42:07 +0000 Subject: [PATCH 01/20] Added unit test for templated parent theme changed loop. --- .../Styling/StyledElementTests_Theming.cs | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs index 60603937d9..6ba648af78 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs @@ -569,6 +569,46 @@ public class StyledElementTests_Theming Assert.Equal(Brushes.Green, border.Background); } + [Fact] + public void TemplatedParent_Theme_Change_Applies_Recursively_To_VisualChildren() + { + var theme = CreateDerivedTheme(); + var target = CreateTarget(); + + Assert.Null(target.Theme); + Assert.Null(target.Template); + + var root = CreateRoot(target, theme.BasedOn); + + Assert.NotNull(target.Theme); + Assert.NotNull(target.Template); + + root.Styles.Add(new Style(x => x.OfType().Class("foo")) + { + Setters = { new Setter(StyledElement.ThemeProperty, theme) } + }); + + root.LayoutManager.ExecuteLayoutPass(); + + var border = Assert.IsType(target.VisualChild); + var inner = Assert.IsType(border.Child); + + Assert.Equal(Brushes.Red, border.Background); + Assert.Equal(Brushes.Red, inner.Background); + + Assert.Equal(null, inner.BorderBrush); + Assert.Equal(null, inner.BorderBrush); + + target.Classes.Add("foo"); + root.LayoutManager.ExecuteLayoutPass(); + + Assert.Equal(Brushes.Green, border.Background); + Assert.Equal(Brushes.Green, inner.Background); + + Assert.Equal(Brushes.Cyan, inner.BorderBrush); + Assert.Equal(Brushes.Cyan, inner.BorderBrush); + } + private static ThemedControl CreateTarget() { return new ThemedControl(); @@ -595,7 +635,8 @@ public class StyledElementTests_Theming private static ControlTheme CreateTheme(string tag = "theme") { - var template = new FuncControlTemplate((o, n) => new Border()); + var template = new FuncControlTemplate( + (o, n) => new Border() { Child = new Border() }); return new ControlTheme { From 12bf8bd1817ada201becf7d3eaf79d8dd0b8518e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Sat, 25 Feb 2023 02:43:13 +0000 Subject: [PATCH 02/20] Fixed templated parent theme changed loop. --- src/Avalonia.Base/Visual.cs | 17 +++++++++++++++++ .../Primitives/TemplatedControl.cs | 17 ----------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 8b0cc06136..df0c5b100f 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -732,6 +732,23 @@ namespace Avalonia } } + internal override void OnTemplatedParentControlThemeChanged() + { + base.OnTemplatedParentControlThemeChanged(); + + var count = VisualChildren.Count; + var templatedParent = TemplatedParent; + + for (var i = 0; i < count; ++i) + { + if (VisualChildren[i] is StyledElement child && + child.TemplatedParent == templatedParent) + { + child.OnTemplatedParentControlThemeChanged(); + } + } + } + /// /// Computes the value according to the /// and diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index d8874832bd..8253342782 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -405,22 +405,5 @@ namespace Avalonia.Controls.Primitives } } } - - internal override void OnTemplatedParentControlThemeChanged() - { - base.OnTemplatedParentControlThemeChanged(); - - var count = VisualChildren.Count; - var templatedParent = TemplatedParent; - - for (var i = 0; i < count; ++i) - { - if (VisualChildren[i] is TemplatedControl child && - child.TemplatedParent == templatedParent) - { - child.OnTemplatedParentControlThemeChanged(); - } - } - } } } From c315842c1e0dcd42a074800fe3cafacb05710333 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 25 Feb 2023 09:43:49 +0000 Subject: [PATCH 03/20] xcode-select --- azure-pipelines-integrationtests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index 5735da19ab..ffd7bb09c6 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -25,6 +25,7 @@ jobs: - script: system_profiler SPDisplaysDataType |grep Resolution - script: | + xcode-select -s /Applications/Xcode.app/Contents/Developer pkill node appium & pkill IntegrationTestApp From c1c1a10d2fba23857a1b133132ee5504d2d9d67e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 25 Feb 2023 09:45:46 +0000 Subject: [PATCH 04/20] sudo --- azure-pipelines-integrationtests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index ffd7bb09c6..81e1046a1e 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -25,7 +25,7 @@ jobs: - script: system_profiler SPDisplaysDataType |grep Resolution - script: | - xcode-select -s /Applications/Xcode.app/Contents/Developer + sudo xcode-select -s /Applications/Xcode.app/Contents/Developer pkill node appium & pkill IntegrationTestApp From b548ccb346aad761e1b40efbd38dcbf96bdd2d51 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 25 Feb 2023 10:08:01 +0000 Subject: [PATCH 05/20] fix integration test app, missing inter font package. --- samples/IntegrationTestApp/IntegrationTestApp.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/samples/IntegrationTestApp/IntegrationTestApp.csproj b/samples/IntegrationTestApp/IntegrationTestApp.csproj index 0a761d70ba..1356eeb526 100644 --- a/samples/IntegrationTestApp/IntegrationTestApp.csproj +++ b/samples/IntegrationTestApp/IntegrationTestApp.csproj @@ -19,6 +19,7 @@ + From 4a3c82b194f782d89c44a1670dcb8e8de1dce59d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 25 Feb 2023 11:19:46 +0000 Subject: [PATCH 06/20] remove trigger from yaml. --- azure-pipelines-integrationtests.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index 81e1046a1e..df6ecbbaf7 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -3,9 +3,6 @@ # Add steps that build, run tests, deploy, and more: # https://aka.ms/yaml -trigger: -- master - jobs: - job: Mac pool: From 4dbd9a91c8dedcfb16465ae5a0d97c80bf26c3d7 Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Sat, 25 Feb 2023 12:27:23 +0100 Subject: [PATCH 07/20] Add Avalonia.Fonts.Inter to Avalonia.Desktop.slnf --- Avalonia.Desktop.slnf | 1 + 1 file changed, 1 insertion(+) diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 4a7a329fc6..477aaec6a8 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -21,6 +21,7 @@ "src\\Avalonia.Desktop\\Avalonia.Desktop.csproj", "src\\Avalonia.Diagnostics\\Avalonia.Diagnostics.csproj", "src\\Avalonia.Dialogs\\Avalonia.Dialogs.csproj", + "src\\Avalonia.Fonts.Inter\\Avalonia.Fonts.Inter.csproj", "src\\Avalonia.FreeDesktop\\Avalonia.FreeDesktop.csproj", "src\\Avalonia.Headless.Vnc\\Avalonia.Headless.Vnc.csproj", "src\\Avalonia.Headless\\Avalonia.Headless.csproj", From 4592470047ce8b47b8ba43c09efcc189b2cab2c8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 25 Feb 2023 11:27:37 +0000 Subject: [PATCH 08/20] remove header not needed. --- azure-pipelines-integrationtests.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index df6ecbbaf7..7221fe4657 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -1,8 +1,3 @@ -# Starter pipeline -# Start with a minimal pipeline that you can customize to build and deploy your code. -# Add steps that build, run tests, deploy, and more: -# https://aka.ms/yaml - jobs: - job: Mac pool: From 5009fb9d9baf41bd4365df588c019fc1fa6a301d Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Sat, 25 Feb 2023 12:37:25 +0100 Subject: [PATCH 09/20] fix: CA1823 Unused field ```bash Warning CA1823 Unused field '_transform' Avalonia.Base (net6.0) .\src\Avalonia.Base\Media\DrawingGroup.cs 110 Active Warning CA1823 Unused field '_matrix' Avalonia.Base (netstandard2.0) .\src\Avalonia.Base\Media\DrawingContext.cs 280 Active ``` --- src/Avalonia.Base/Media/DrawingContext.cs | 1 - src/Avalonia.Base/Media/DrawingGroup.cs | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/Avalonia.Base/Media/DrawingContext.cs b/src/Avalonia.Base/Media/DrawingContext.cs index 31a16dc69c..f2106f2f86 100644 --- a/src/Avalonia.Base/Media/DrawingContext.cs +++ b/src/Avalonia.Base/Media/DrawingContext.cs @@ -277,7 +277,6 @@ namespace Avalonia.Media private readonly record struct RestoreState : IDisposable { private readonly DrawingContext _context; - private readonly Matrix _matrix; private readonly PushedStateType _type; public enum PushedStateType diff --git a/src/Avalonia.Base/Media/DrawingGroup.cs b/src/Avalonia.Base/Media/DrawingGroup.cs index 812d315912..a41054202e 100644 --- a/src/Avalonia.Base/Media/DrawingGroup.cs +++ b/src/Avalonia.Base/Media/DrawingGroup.cs @@ -107,8 +107,6 @@ namespace Avalonia.Media private readonly DrawingGroup _drawingGroup; private readonly IPlatformRenderInterface _platformRenderInterface = AvaloniaLocator.Current.GetRequiredService(); - private Matrix _transform; - private bool _disposed; // Root drawing created by this DrawingContext. From 5f98ec831907550bd9dea20f495776207288df41 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 24 Feb 2023 17:36:41 +0100 Subject: [PATCH 10/20] fix: Address Rule AVP1021 Name collision: {type} owns multiple Avalonia properties with the name '{PropertyName}' ```bash Warning AVP1021 Name collision: Avalonia.Controls.MaskedTextBox owns multiple Avalonia properties with the name 'PasswordChar' Avalonia.Controls.TextBox.PasswordCharProperty Avalonia.Controls.MaskedTextBox.PasswordCharProperty Avalonia.Controls (net6.0) .\src\Avalonia.Controls\MaskedTextBox.cs 134 Active Warning AVP1021 Name collision: Avalonia.Diagnostics.Controls.ThicknessEditor owns multiple Avalonia properties with the name 'Header' Avalonia.Diagnostics.Controls.ThicknessEditor.HeaderProperty Avalonia.Diagnostics.Controls.ThicknessEditor.IsPresentProperty Avalonia.Diagnostics (net6.0) .\src\Avalonia.Diagnostics\Diagnostics\Controls\ThicknessEditor.cs 53 Active ``` --- src/Avalonia.Controls/MaskedTextBox.cs | 10 +++++++++- .../Diagnostics/Controls/ThicknessEditor.cs | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/MaskedTextBox.cs b/src/Avalonia.Controls/MaskedTextBox.cs index 5a3eb47ce4..f086ce0ace 100644 --- a/src/Avalonia.Controls/MaskedTextBox.cs +++ b/src/Avalonia.Controls/MaskedTextBox.cs @@ -32,7 +32,9 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(Mask), string.Empty); public static new readonly StyledProperty PasswordCharProperty = - AvaloniaProperty.Register(nameof(PasswordChar), '\0'); +#pragma warning disable AVP1013 // AvaloniaProperty owners should not be added superfluously + TextBox.PasswordCharProperty.AddOwner(); +#pragma warning restore AVP1013 // AvaloniaProperty owners should not be added superfluously public static readonly StyledProperty PromptCharProperty = AvaloniaProperty.Register(nameof(PromptChar), '_'); @@ -51,6 +53,12 @@ namespace Avalonia.Controls private bool _resetOnSpace = true; + static MaskedTextBox() + { + PasswordCharProperty + .OverrideDefaultValue('\0'); + } + public MaskedTextBox() { } /// diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs index ec7e91c8be..c9189a886d 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs @@ -15,7 +15,7 @@ namespace Avalonia.Diagnostics.Controls (o, v) => o.Header = v); public static readonly DirectProperty IsPresentProperty = - AvaloniaProperty.RegisterDirect(nameof(Header), o => o.IsPresent, + AvaloniaProperty.RegisterDirect(nameof(IsPresent), o => o.IsPresent, (o, v) => o.IsPresent = v); public static readonly DirectProperty LeftProperty = From 0038f9ed966d17b30c1171cc0e76aa064889d37d Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 24 Feb 2023 18:32:28 +0100 Subject: [PATCH 11/20] fix: AVP1022 Bad name: An AvaloniaProperty named 'DrawOnlyPoints' is being assigned to ControlCatalog.Pages.PointerCanvas.StatusProperty --- samples/ControlCatalog/Pages/PointerCanvas.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/ControlCatalog/Pages/PointerCanvas.cs b/samples/ControlCatalog/Pages/PointerCanvas.cs index 653590fe64..32e46af9dd 100644 --- a/samples/ControlCatalog/Pages/PointerCanvas.cs +++ b/samples/ControlCatalog/Pages/PointerCanvas.cs @@ -114,7 +114,7 @@ public class PointerCanvas : Control private string? _status; public static readonly DirectProperty StatusProperty = - AvaloniaProperty.RegisterDirect(nameof(DrawOnlyPoints), c => c.Status, (c, v) => c.Status = v, + AvaloniaProperty.RegisterDirect(nameof(Status), c => c.Status, (c, v) => c.Status = v, defaultBindingMode: Avalonia.Data.BindingMode.TwoWay); public string? Status From ffe6fe8a39b0504cd2bd77eb9b1975c627406f61 Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Sat, 25 Feb 2023 13:34:29 +0100 Subject: [PATCH 12/20] Fix dependency of Avalonia.Fonts.Inter --- samples/ControlCatalog/ControlCatalog.csproj | 1 - src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj | 1 + src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index 0e84b3d182..c223bfe1a9 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -29,7 +29,6 @@ - diff --git a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj index 8d266ce82f..66fcce2163 100644 --- a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj +++ b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj b/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj index 4aa6b66743..aefa407645 100644 --- a/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj +++ b/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj @@ -8,6 +8,7 @@ + From dca96b5ad3a7f9acca903ca6f9a0191ba5271068 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Sat, 25 Feb 2023 13:55:38 +0100 Subject: [PATCH 13/20] Avoid boxing iterators in hot paths --- .../Composition/CompositingRenderer.cs | 2 +- src/Avalonia.Base/StyledElement.cs | 21 +++++++++++++------ src/Avalonia.Base/Styling/OrSelector.cs | 13 +++++++----- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index 01299e4ffa..df3a70b3e6 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -225,7 +225,7 @@ public class CompositingRenderer : IRendererWithCompositor sortedChildren.Dispose(); } else - foreach (var ch in v.GetVisualChildren()) + foreach (var ch in visualChildren) { var compositionChild = ch.CompositionVisual; if (compositionChild != null) diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index 82e948eea8..cbdf3c3c1e 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -803,8 +803,11 @@ namespace Avalonia if (theme.HasChildren) { - foreach (var child in theme.Children) - ApplyStyle(child, null, type); + var children = theme.Children; + for (var i = 0; i < children.Count; i++) + { + ApplyStyle(children[i], null, type); + } } } @@ -816,8 +819,11 @@ namespace Avalonia if (host.IsStylesInitialized) { - foreach (var style in host.Styles) - ApplyStyle(style, host, FrameType.Style); + var styles = host.Styles; + for (var i = 0; i < styles.Count; ++i) + { + ApplyStyle(styles[i], host, FrameType.Style); + } } } @@ -826,8 +832,11 @@ namespace Avalonia if (style is Style s) s.TryAttach(this, host, type); - foreach (var child in style.Children) - ApplyStyle(child, host, type); + var children = style.Children; + for (var i = 0; i < children.Count; i++) + { + ApplyStyle(children[i], host, type); + } } private void ReevaluateImplicitTheme() diff --git a/src/Avalonia.Base/Styling/OrSelector.cs b/src/Avalonia.Base/Styling/OrSelector.cs index c5ef9a0b2b..3b0aa03492 100644 --- a/src/Avalonia.Base/Styling/OrSelector.cs +++ b/src/Avalonia.Base/Styling/OrSelector.cs @@ -71,9 +71,9 @@ namespace Avalonia.Styling var activators = new OrActivatorBuilder(); var neverThisInstance = false; - foreach (var selector in _selectors) + for (var i = 0; i < _selectors.Count; i++) { - var match = selector.Match(control, parent, subscribe); + var match = _selectors[i].Match(control, parent, subscribe); switch (match.Result) { @@ -108,16 +108,19 @@ namespace Avalonia.Styling internal override void ValidateNestingSelector(bool inControlTheme) { - foreach (var selector in _selectors) - selector.ValidateNestingSelector(inControlTheme); + for (var i = 0; i < _selectors.Count; i++) + { + _selectors[i].ValidateNestingSelector(inControlTheme); + } } private Type? EvaluateTargetType() { Type? result = null; - foreach (var selector in _selectors) + for (var i = 0; i < _selectors.Count; i++) { + var selector = _selectors[i]; if (selector.TargetType == null) { return null; From 765bce78d78bb4aa90d2ce264a18e7b066e3a32f Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Sat, 25 Feb 2023 14:25:02 +0100 Subject: [PATCH 14/20] Avoid unneeded allocations in CompiledBindingPath --- .../CompiledBindings/CompiledBindingPath.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs index 2b62d33349..f196231a6b 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Reflection; using Avalonia.Controls; using Avalonia.Data.Core; @@ -13,13 +12,14 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings { public class CompiledBindingPath { - private readonly List _elements = new List(); + private readonly ICompiledBindingPathElement[] _elements; - public CompiledBindingPath() { } + public CompiledBindingPath() + => _elements = Array.Empty(); - internal CompiledBindingPath(IEnumerable bindingPath, object rawSource) + internal CompiledBindingPath(ICompiledBindingPathElement[] elements, object rawSource) { - _elements = new List(bindingPath); + _elements = elements; RawSource = rawSource; } @@ -78,13 +78,13 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings internal IEnumerable Elements => _elements; - internal SourceMode SourceMode => _elements.OfType().Any() + internal SourceMode SourceMode => Array.Exists(_elements, e => e is IControlSourceBindingPathElement) ? SourceMode.Control : SourceMode.Data; internal object RawSource { get; } public override string ToString() - => string.Concat(_elements); + => string.Concat((IEnumerable) _elements); } public class CompiledBindingPathBuilder @@ -169,7 +169,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings return this; } - public CompiledBindingPath Build() => new CompiledBindingPath(_elements, _rawSource); + public CompiledBindingPath Build() => new CompiledBindingPath(_elements.ToArray(), _rawSource); } public interface ICompiledBindingPathElement From 79af58b32f93312c5ba519e98f1192a8bd432796 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Sat, 25 Feb 2023 15:06:30 +0100 Subject: [PATCH 15/20] Adjusted AvaloniaPropertyDictionary growth rate to reduce allocations --- .../Utilities/AvaloniaPropertyDictionary.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs index 5cac2ef658..0589abb2dd 100644 --- a/src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs +++ b/src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs @@ -304,13 +304,9 @@ namespace Avalonia.Utilities { if (_entryCount == _entries!.Length) { - const double growthFactor = 1.2; - var newSize = (int)(_entryCount * growthFactor); - - if (newSize == _entryCount) - { - newSize++; - } + var newSize = _entryCount == DefaultInitialCapacity ? + DefaultInitialCapacity * 2 : + (int)(_entryCount * 1.5); var destEntries = new Entry[newSize]; From 45ba9c98dff7791f46aaea2cc3ba0b863707cbd8 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Sat, 25 Feb 2023 16:21:55 +0100 Subject: [PATCH 16/20] Reduce allocations in TypeNameAndClassSelector.Classes --- .../Styling/TypeNameAndClassSelector.cs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs index 7883fd23ab..f8670cfdb3 100644 --- a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Avalonia.Controls; using Avalonia.Styling.Activators; using Avalonia.Utilities; @@ -15,7 +14,7 @@ namespace Avalonia.Styling internal class TypeNameAndClassSelector : Selector { private readonly Selector? _previous; - private readonly Lazy> _classes = new Lazy>(() => new List()); + private List? _classes; private Type? _targetType; private string? _selectorString; @@ -81,7 +80,7 @@ namespace Avalonia.Styling /// /// The style classes which the selector matches. /// - public IList Classes => _classes.Value; + public IList Classes => _classes ??= new(); /// public override string ToString(Style? owner) @@ -122,16 +121,16 @@ namespace Avalonia.Styling return SelectorMatch.NeverThisInstance; } - if (_classes.IsValueCreated && _classes.Value.Count > 0) + if (_classes is { Count: > 0 }) { if (subscribe) { - var observable = new StyleClassActivator((Classes)control.Classes, _classes.Value); + var observable = new StyleClassActivator(control.Classes, _classes); return new SelectorMatch(observable); } - if (!StyleClassActivator.AreClassesMatching(control.Classes, Classes)) + if (!StyleClassActivator.AreClassesMatching(control.Classes, _classes)) { return SelectorMatch.NeverThisInstance; } @@ -172,9 +171,9 @@ namespace Avalonia.Styling builder.Append(Name); } - if (_classes.IsValueCreated && _classes.Value.Count > 0) + if (_classes is { Count: > 0 }) { - foreach (var c in Classes) + foreach (var c in _classes) { if (!c.StartsWith(":")) { From b9644c3068ca90196e61f69aa851fdd9f3fc18f9 Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Sun, 26 Feb 2023 02:51:39 +0100 Subject: [PATCH 18/20] Revert "Fix dependency of Avalonia.Fonts.Inter" This reverts commit ffe6fe8a39b0504cd2bd77eb9b1975c627406f61. --- samples/ControlCatalog/ControlCatalog.csproj | 1 + src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj | 1 - src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj | 1 - 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index c223bfe1a9..0e84b3d182 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -29,6 +29,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj index 66fcce2163..8d266ce82f 100644 --- a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj +++ b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj @@ -8,7 +8,6 @@ - diff --git a/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj b/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj index aefa407645..4aa6b66743 100644 --- a/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj +++ b/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj @@ -8,7 +8,6 @@ - From a9ef92bbeb54000102fef5655815e475d927b6c5 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 26 Feb 2023 23:22:52 +0600 Subject: [PATCH 19/20] Implemented BeginFigure(isFilled: false) --- .../Avalonia.Skia/CombinedGeometryImpl.cs | 43 +++++++-- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 9 +- src/Skia/Avalonia.Skia/EllipseGeometryImpl.cs | 5 +- src/Skia/Avalonia.Skia/GeometryGroupImpl.cs | 43 +++++++-- src/Skia/Avalonia.Skia/GeometryImpl.cs | 31 +++--- src/Skia/Avalonia.Skia/LineGeometryImpl.cs | 5 +- .../Avalonia.Skia/PlatformRenderInterface.cs | 4 +- .../Avalonia.Skia/RectangleGeometryImpl.cs | 5 +- src/Skia/Avalonia.Skia/StreamGeometryImpl.cs | 90 +++++++++++++----- .../Avalonia.Skia/TransformedGeometryImpl.cs | 21 +++- .../Avalonia.RenderTests/Shapes/PathTests.cs | 58 +++++++++++ ...nFigure_IsFilled_Is_Respected.expected.png | Bin 0 -> 993 bytes ...nFigure_IsFilled_Is_Respected.expected.png | Bin 0 -> 993 bytes 13 files changed, 238 insertions(+), 76 deletions(-) create mode 100644 tests/TestFiles/Direct2D1/Shapes/Path/BeginFigure_IsFilled_Is_Respected.expected.png create mode 100644 tests/TestFiles/Skia/Shapes/Path/BeginFigure_IsFilled_Is_Respected.expected.png diff --git a/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs b/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs index 170cc9d420..1a4f467f40 100644 --- a/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs @@ -8,11 +8,25 @@ namespace Avalonia.Skia /// internal class CombinedGeometryImpl : GeometryImpl { - public CombinedGeometryImpl(GeometryCombineMode combineMode, Geometry g1, Geometry g2) + public CombinedGeometryImpl(SKPath? stroke, SKPath? fill) { - var path1 = (g1.PlatformImpl as GeometryImpl)?.EffectivePath; - var path2 = (g2.PlatformImpl as GeometryImpl)?.EffectivePath; + StrokePath = stroke; + FillPath = fill; + Bounds = (stroke ?? fill)?.TightBounds.ToAvaloniaRect() ?? default; + } + + public static CombinedGeometryImpl ForceCreate(GeometryCombineMode combineMode, Geometry g1, Geometry g2) + { + if (g1.PlatformImpl is GeometryImpl i1 + && g2.PlatformImpl is GeometryImpl i2 + && TryCreate(combineMode, i1, i2) is { } result) + return result; + + return new(null, null); + } + public static CombinedGeometryImpl? TryCreate(GeometryCombineMode combineMode, GeometryImpl g1, GeometryImpl g2) + { var op = combineMode switch { GeometryCombineMode.Intersect => SKPathOp.Intersect, @@ -21,13 +35,28 @@ namespace Avalonia.Skia _ => SKPathOp.Union }; - var path = path1?.Op(path2, op); + var stroke = + g1.StrokePath != null && g2.StrokePath != null + ? g1.StrokePath.Op(g2.StrokePath, op) + : null; + + SKPath? fill = null; + if (g1.FillPath != null && g2.FillPath != null) + { + // Reuse stroke if fill paths are the same + if (ReferenceEquals(g1.FillPath, g1.StrokePath) && ReferenceEquals(g2.FillPath, g2.StrokePath)) + fill = stroke; + else + fill = g1.FillPath.Op(g2.FillPath, op); + } - EffectivePath = path; - Bounds = path?.Bounds.ToAvaloniaRect() ?? default; + if (stroke == null && fill == null) + return null; + return new CombinedGeometryImpl(stroke, fill); } public override Rect Bounds { get; } - public override SKPath? EffectivePath { get; } + public override SKPath? StrokePath { get; } + public override SKPath? FillPath { get; } } } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index db7b068543..af0231579c 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -230,20 +230,21 @@ namespace Avalonia.Skia var impl = (GeometryImpl) geometry; var size = geometry.Bounds.Size; - if (brush is not null) + if (brush is not null && impl.FillPath != null) { using (var fill = CreatePaint(_fillPaint, brush, size)) { - Canvas.DrawPath(impl.EffectivePath, fill.Paint); + Canvas.DrawPath(impl.FillPath, fill.Paint); } } if (pen is not null + && impl.StrokePath != null && TryCreatePaint(_strokePaint, pen, size.Inflate(new Thickness(pen.Thickness / 2))) is { } stroke) { using (stroke) { - Canvas.DrawPath(impl.EffectivePath, stroke.Paint); + Canvas.DrawPath(impl.StrokePath, stroke.Paint); } } } @@ -639,7 +640,7 @@ namespace Avalonia.Skia { CheckLease(); Canvas.Save(); - Canvas.ClipPath(((GeometryImpl)clip).EffectivePath, SKClipOperation.Intersect, true); + Canvas.ClipPath(((GeometryImpl)clip).FillPath, SKClipOperation.Intersect, true); } /// diff --git a/src/Skia/Avalonia.Skia/EllipseGeometryImpl.cs b/src/Skia/Avalonia.Skia/EllipseGeometryImpl.cs index ac05691c67..64976d4eea 100644 --- a/src/Skia/Avalonia.Skia/EllipseGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/EllipseGeometryImpl.cs @@ -8,14 +8,15 @@ namespace Avalonia.Skia internal class EllipseGeometryImpl : GeometryImpl { public override Rect Bounds { get; } - public override SKPath EffectivePath { get; } + public override SKPath StrokePath { get; } + public override SKPath FillPath => StrokePath; public EllipseGeometryImpl(Rect rect) { var path = new SKPath(); path.AddOval(rect.ToSKRect()); - EffectivePath = path; + StrokePath = path; Bounds = rect; } } diff --git a/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs b/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs index 2828f9a9c1..01be42bad0 100644 --- a/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs @@ -11,26 +11,51 @@ namespace Avalonia.Skia { public GeometryGroupImpl(FillRule fillRule, IReadOnlyList children) { - var path = new SKPath + var fillType = fillRule == FillRule.NonZero ? SKPathFillType.Winding : SKPathFillType.EvenOdd; + var count = children.Count; + + var stroke = new SKPath { - FillType = fillRule == FillRule.NonZero ? SKPathFillType.Winding : SKPathFillType.EvenOdd, + FillType = fillType }; - - var count = children.Count; + bool requiresFillPass = false; for (var i = 0; i < count; ++i) { - if (children[i].PlatformImpl is GeometryImpl { EffectivePath: { } effectivePath }) + if (children[i].PlatformImpl is GeometryImpl geo) { - path.AddPath(effectivePath); + if (geo.StrokePath != null) + stroke.AddPath(geo.StrokePath); + if (!ReferenceEquals(geo.StrokePath, geo.FillPath)) + requiresFillPass = true; } } + + StrokePath = stroke; + + if (requiresFillPass) + { + var fill = new SKPath + { + FillType = fillType + }; + + for (var i = 0; i < count; ++i) + { + if (children[i].PlatformImpl is GeometryImpl { FillPath: { } fillPath }) + fill.AddPath(fillPath); + } + + FillPath = fill; + } + else + FillPath = stroke; - EffectivePath = path; - Bounds = path.Bounds.ToAvaloniaRect(); + Bounds = stroke.TightBounds.ToAvaloniaRect(); } public override Rect Bounds { get; } - public override SKPath EffectivePath { get; } + public override SKPath StrokePath { get; } + public override SKPath FillPath { get; } } } diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs index 34270c2078..aee84d1346 100644 --- a/src/Skia/Avalonia.Skia/GeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs @@ -14,7 +14,7 @@ namespace Avalonia.Skia private PathCache _pathCache; private SKPathMeasure? _cachedPathMeasure; - private SKPathMeasure CachedPathMeasure => _cachedPathMeasure ??= new SKPathMeasure(EffectivePath!); + private SKPathMeasure CachedPathMeasure => _cachedPathMeasure ??= new SKPathMeasure(StrokePath!); /// public abstract Rect Bounds { get; } @@ -24,19 +24,20 @@ namespace Avalonia.Skia { get { - if (EffectivePath is null) + if (StrokePath is null) return 0; return CachedPathMeasure.Length; } } - public abstract SKPath? EffectivePath { get; } + public abstract SKPath? StrokePath { get; } + public abstract SKPath? FillPath { get; } /// public bool FillContains(Point point) { - return PathContainsCore(EffectivePath, point); + return PathContainsCore(FillPath, point); } /// @@ -74,7 +75,7 @@ namespace Avalonia.Skia var paint = SKPaintCache.Shared.Get(); paint.IsStroke = true; paint.StrokeWidth = strokeWidth; - paint.GetFillPath(EffectivePath, strokePath); + paint.GetFillPath(StrokePath, strokePath); SKPaintCache.Shared.ReturnReset(paint); @@ -96,14 +97,10 @@ namespace Avalonia.Skia /// public IGeometryImpl? Intersect(IGeometryImpl geometry) { - if (EffectivePath is { } path - && (geometry as GeometryImpl)?.EffectivePath is { } otherPath - && path.Op(otherPath, SKPathOp.Intersect) is { } result) - { - return new StreamGeometryImpl(result); - } - - return null; + var other = geometry as GeometryImpl; + if (other == null) + return null; + return CombinedGeometryImpl.TryCreate(GeometryCombineMode.Intersect, this, other); } /// @@ -128,7 +125,7 @@ namespace Avalonia.Skia /// public bool TryGetPointAtDistance(double distance, out Point point) { - if (EffectivePath is null) + if (StrokePath is null) { point = new Point(); return false; @@ -142,7 +139,7 @@ namespace Avalonia.Skia /// public bool TryGetPointAndTangentAtDistance(double distance, out Point point, out Point tangent) { - if (EffectivePath is null) + if (StrokePath is null) { point = new Point(); tangent = new Point(); @@ -158,7 +155,7 @@ namespace Avalonia.Skia public bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, [NotNullWhen(true)] out IGeometryImpl? segmentGeometry) { - if (EffectivePath is null) + if (StrokePath is null) { segmentGeometry = null; return false; @@ -172,7 +169,7 @@ namespace Avalonia.Skia if (res) { - segmentGeometry = new StreamGeometryImpl(_skPathSegment); + segmentGeometry = new StreamGeometryImpl(_skPathSegment, null); } return res; diff --git a/src/Skia/Avalonia.Skia/LineGeometryImpl.cs b/src/Skia/Avalonia.Skia/LineGeometryImpl.cs index b102a4c119..2ac0a2e18c 100644 --- a/src/Skia/Avalonia.Skia/LineGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/LineGeometryImpl.cs @@ -9,7 +9,8 @@ namespace Avalonia.Skia internal class LineGeometryImpl : GeometryImpl { public override Rect Bounds { get; } - public override SKPath EffectivePath { get; } + public override SKPath StrokePath { get; } + public override SKPath? FillPath => null; public LineGeometryImpl(Point p1, Point p2) { @@ -17,7 +18,7 @@ namespace Avalonia.Skia path.MoveTo(p1.ToSKPoint()); path.LineTo(p2.ToSKPoint()); - EffectivePath = path; + StrokePath = path; Bounds = new Rect( new Point(Math.Min(p1.X, p2.X), Math.Min(p1.Y, p2.Y)), new Point(Math.Max(p1.X, p2.X), Math.Max(p1.Y, p2.Y))); diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index ab1c6b8816..9c4b326f14 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -65,7 +65,7 @@ namespace Avalonia.Skia public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) { - return new CombinedGeometryImpl(combineMode, g1, g2); + return CombinedGeometryImpl.ForceCreate(combineMode, g1, g2); } public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) @@ -104,7 +104,7 @@ namespace Avalonia.Skia SKFontCache.Shared.Return(skFont); - return new StreamGeometryImpl(path); + return new StreamGeometryImpl(path, path); } /// diff --git a/src/Skia/Avalonia.Skia/RectangleGeometryImpl.cs b/src/Skia/Avalonia.Skia/RectangleGeometryImpl.cs index 93d453e8f0..2d127e07c9 100644 --- a/src/Skia/Avalonia.Skia/RectangleGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/RectangleGeometryImpl.cs @@ -8,14 +8,15 @@ namespace Avalonia.Skia internal class RectangleGeometryImpl : GeometryImpl { public override Rect Bounds { get; } - public override SKPath EffectivePath { get; } + public override SKPath StrokePath { get; } + public override SKPath? FillPath => StrokePath; public RectangleGeometryImpl(Rect rect) { var path = new SKPath(); path.AddRect(rect.ToSKRect()); - EffectivePath = path; + StrokePath = path; Bounds = rect; } } diff --git a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs index 0c3289767e..eb081c4f50 100644 --- a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Avalonia.Media; using Avalonia.Platform; using SkiaSharp; @@ -10,36 +11,39 @@ namespace Avalonia.Skia internal class StreamGeometryImpl : GeometryImpl, IStreamGeometryImpl { private Rect _bounds; - private readonly SKPath _effectivePath; + private readonly SKPath _strokePath; + private SKPath? _fillPath; /// /// Initializes a new instance of the class. /// - /// An existing Skia . + /// An existing Skia for the stroke. + /// An existing Skia for the fill, can also be null or the same as the stroke /// Precomputed path bounds. - public StreamGeometryImpl(SKPath path, Rect bounds) + public StreamGeometryImpl(SKPath stroke, SKPath? fill, Rect? bounds = null) { - _effectivePath = path; - _bounds = bounds; + _strokePath = stroke; + _fillPath = fill; + _bounds = bounds ?? stroke.TightBounds.ToAvaloniaRect(); } - /// - /// Initializes a new instance of the class. - /// - /// An existing Skia . - public StreamGeometryImpl(SKPath path) : this(path, path.TightBounds.ToAvaloniaRect()) + private StreamGeometryImpl(SKPath path) : this(path, path, default(Rect)) { + } /// /// Initializes a new instance of the class. /// - public StreamGeometryImpl() : this(CreateEmptyPath(), default) + public StreamGeometryImpl() : this(CreateEmptyPath()) { } - + /// - public override SKPath EffectivePath => _effectivePath; + public override SKPath? StrokePath => _strokePath; + + /// + public override SKPath? FillPath => _fillPath; /// public override Rect Bounds => _bounds; @@ -47,7 +51,9 @@ namespace Avalonia.Skia /// public IStreamGeometryImpl Clone() { - return new StreamGeometryImpl(_effectivePath.Clone(), Bounds); + var stroke = _strokePath.Clone(); + var fill = _fillPath == _strokePath ? stroke : _fillPath.Clone(); + return new StreamGeometryImpl(stroke, fill, Bounds); } /// @@ -74,7 +80,10 @@ namespace Avalonia.Skia private class StreamContext : IStreamGeometryContextImpl { private readonly StreamGeometryImpl _geometryImpl; - private readonly SKPath _path; + private SKPath Stroke => _geometryImpl._strokePath; + private SKPath Fill => _geometryImpl._fillPath ??= new(); + private bool _isFilled; + private bool Duplicate => _isFilled && !ReferenceEquals(_geometryImpl._fillPath, Stroke); /// /// Initializes a new instance of the class. @@ -83,52 +92,79 @@ namespace Avalonia.Skia public StreamContext(StreamGeometryImpl geometryImpl) { _geometryImpl = geometryImpl; - _path = _geometryImpl._effectivePath; } /// /// Will update bounds of passed geometry. public void Dispose() { - _geometryImpl._bounds = _path.TightBounds.ToAvaloniaRect(); + _geometryImpl._bounds = Stroke.TightBounds.ToAvaloniaRect(); _geometryImpl.InvalidateCaches(); } /// public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) { - _path.ArcTo( + var arc = isLargeArc ? SKPathArcSize.Large : SKPathArcSize.Small; + var sweep = sweepDirection == SweepDirection.Clockwise + ? SKPathDirection.Clockwise + : SKPathDirection.CounterClockwise; + Stroke.ArcTo( (float)size.Width, (float)size.Height, (float)rotationAngle, - isLargeArc ? SKPathArcSize.Large : SKPathArcSize.Small, - sweepDirection == SweepDirection.Clockwise ? SKPathDirection.Clockwise : SKPathDirection.CounterClockwise, + arc, + sweep, (float)point.X, (float)point.Y); + if(Duplicate) + Fill.ArcTo( + (float)size.Width, + (float)size.Height, + (float)rotationAngle, + arc, + sweep, + (float)point.X, + (float)point.Y); } /// public void BeginFigure(Point startPoint, bool isFilled) { - _path.MoveTo((float)startPoint.X, (float)startPoint.Y); + if (!isFilled) + { + if (Stroke == Fill) + _geometryImpl._fillPath = Stroke.Clone(); + } + + _isFilled = isFilled; + Stroke.MoveTo((float)startPoint.X, (float)startPoint.Y); + if(Duplicate) + Fill.MoveTo((float)startPoint.X, (float)startPoint.Y); } /// public void CubicBezierTo(Point point1, Point point2, Point point3) { - _path.CubicTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y, (float)point3.X, (float)point3.Y); + Stroke.CubicTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y, (float)point3.X, (float)point3.Y); + if(Duplicate) + Fill.CubicTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y, (float)point3.X, (float)point3.Y); } /// public void QuadraticBezierTo(Point point1, Point point2) { - _path.QuadTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y); + Stroke.QuadTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y); + if(Duplicate) + Fill.QuadTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y); } /// public void LineTo(Point point) { - _path.LineTo((float)point.X, (float)point.Y); + Stroke.LineTo((float)point.X, (float)point.Y); + if(Duplicate) + Fill.LineTo((float)point.X, (float)point.Y); } /// @@ -136,14 +172,16 @@ namespace Avalonia.Skia { if (isClosed) { - _path.Close(); + Stroke.Close(); + if (Duplicate) + Fill.Close(); } } /// public void SetFillRule(FillRule fillRule) { - _path.FillType = fillRule == FillRule.EvenOdd ? SKPathFillType.EvenOdd : SKPathFillType.Winding; + Fill.FillType = fillRule == FillRule.EvenOdd ? SKPathFillType.EvenOdd : SKPathFillType.Winding; } } } diff --git a/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs b/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs index fb3c2e403f..2dee8c318c 100644 --- a/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs @@ -17,16 +17,27 @@ namespace Avalonia.Skia { SourceGeometry = source; Transform = transform; + var matrix = transform.ToSKMatrix(); - var transformedPath = source.EffectivePath.Clone(); - transformedPath?.Transform(transform.ToSKMatrix()); - - EffectivePath = transformedPath; + var transformedPath = StrokePath = source.StrokePath.Clone(); + transformedPath?.Transform(matrix); + Bounds = transformedPath?.TightBounds.ToAvaloniaRect() ?? default; + + if (ReferenceEquals(source.StrokePath, source.FillPath)) + FillPath = transformedPath; + else if (source.FillPath != null) + { + FillPath = transformedPath = source.FillPath.Clone(); + transformedPath.Transform(matrix); + } } /// - public override SKPath? EffectivePath { get; } + public override SKPath? StrokePath { get; } + + /// + public override SKPath? FillPath { get; } /// public IGeometryImpl SourceGeometry { get; } diff --git a/tests/Avalonia.RenderTests/Shapes/PathTests.cs b/tests/Avalonia.RenderTests/Shapes/PathTests.cs index bac16cca88..bf375121de 100644 --- a/tests/Avalonia.RenderTests/Shapes/PathTests.cs +++ b/tests/Avalonia.RenderTests/Shapes/PathTests.cs @@ -376,5 +376,63 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes await RenderToFile(target); CompareImages(); } + + [Fact] + public async Task BeginFigure_IsFilled_Is_Respected() + { + var target = new Border + { + Width = 200, + Height = 200, + Background = Brushes.White, + Child = new Path + { + Fill = Brushes.Black, + Stroke = Brushes.Black, + StrokeThickness = 10, + Data = new PathGeometry() + { + Figures = new() + { + new PathFigure + { + IsFilled = false, IsClosed = false, + StartPoint = new Point(170,170), + Segments = new () + { + new LineSegment + { + Point = new Point(60, 170) + }, + new LineSegment + { + Point = new Point(60, 60) + } + } + }, + new PathFigure + { + IsFilled = true, IsClosed = true, + StartPoint = new Point(60,20), + Segments = new () + { + new LineSegment + { + Point = new Point(20, 60) + }, + new LineSegment + { + Point = new Point(100, 60) + } + } + } + } + } + } + }; + + await RenderToFile(target); + CompareImages(); + } } } diff --git a/tests/TestFiles/Direct2D1/Shapes/Path/BeginFigure_IsFilled_Is_Respected.expected.png b/tests/TestFiles/Direct2D1/Shapes/Path/BeginFigure_IsFilled_Is_Respected.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..d67d71c5200bc891ed02fb9dfabeedd97e58d2c5 GIT binary patch literal 993 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yu@pObhHwBu4M$1`0|WC6PZ!6K ziaBrZIA%Te5^)IpeVKb}xY+v;+f~czJ?>j{=6WtIe!$DZW}l*Gs_~&Ra@tdtniu=G ziZ|#AbFefz2w){zoLP(Z#`#}=UHaDa#>Dt+{SR8#UwkEJsWsvK zF~g3Q@5?x!EHC6ydYB)~@@a(y+eCvmb`#u>17$8}K5w)EnexVLLOM|3$&E6}6RRId zxD>_g=6-S&sK;_yEbFE)poM+u=8bD)k1I?{zUitE-I*+KvM|SP!rG%iO%i6ZC&C^} zxESp}%eLt&(EKyGlN;Zt0*(EAD@LIj=(v+}%tTMbft1WY%d+V^NJ*-2;~62Kag}T2 zPUHYBnt6Ct;~6QSz~5-y2{(aCH|4KpDVhRQ`sVs+X3Hp`6TZD%+9+cRH1_xBO%8o> zK()1%o(hkC*iU?O{4%rP@#8L=gii~{>P>%IVl{Kd>8GF0%x{pdic)waaFS=9P=|$! zQFlAL65qr#PZlC?!;y`11y8=dBidoH&0MLn*B@wmnY+sQ^0;ZKmcFlQ?CyWv*%{AhzvY*` z_MvHj{=6WtIe!$DZW}l*Gs_~&Ra@tdtniu=G ziZ|#AbFefz2w){zoLP(Z#`#}=UHaDa#>Dt+{SR8#UwkEJsWsvK zF~g3Q@5?x!EHC6ydYB)~@@a(y+eCvmb`#u>17$8}K5w)EnexVLLOM|3$&E6}6RRId zxD>_g=6-S&sK;_yEbFE)poM+u=8bD)k1I?{zUitE-I*+KvM|SP!rG%iO%i6ZC&C^} zxESp}%eLt&(EKyGlN;Zt0*(EAD@LIj=(v+}%tTMbft1WY%d+V^NJ*-2;~62Kag}T2 zPUHYBnt6Ct;~6QSz~5-y2{(aCH|4KpDVhRQ`sVs+X3Hp`6TZD%+9+cRH1_xBO%8o> zK()1%o(hkC*iU?O{4%rP@#8L=gii~{>P>%IVl{Kd>8GF0%x{pdic)waaFS=9P=|$! zQFlAL65qr#PZlC?!;y`11y8=dBidoH&0MLn*B@wmnY+sQ^0;ZKmcFlQ?CyWv*%{AhzvY*` z_MvH Date: Sun, 26 Feb 2023 19:57:00 +0100 Subject: [PATCH 20/20] fix: Address review --- src/Avalonia.Controls/MaskedTextBox.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Avalonia.Controls/MaskedTextBox.cs b/src/Avalonia.Controls/MaskedTextBox.cs index f086ce0ace..d397a581dc 100644 --- a/src/Avalonia.Controls/MaskedTextBox.cs +++ b/src/Avalonia.Controls/MaskedTextBox.cs @@ -31,11 +31,6 @@ namespace Avalonia.Controls public static readonly StyledProperty MaskProperty = AvaloniaProperty.Register(nameof(Mask), string.Empty); - public static new readonly StyledProperty PasswordCharProperty = -#pragma warning disable AVP1013 // AvaloniaProperty owners should not be added superfluously - TextBox.PasswordCharProperty.AddOwner(); -#pragma warning restore AVP1013 // AvaloniaProperty owners should not be added superfluously - public static readonly StyledProperty PromptCharProperty = AvaloniaProperty.Register(nameof(PromptChar), '_');