From a26059630f19e5ec8ceb5d65aff29193ee74a036 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 1 Feb 2022 14:57:08 +0100 Subject: [PATCH 001/389] Add failing test for control template value priority. Add tests to check that template values are assigned with Style priority and move other XAML control template tests alongside it in a new test class. --- .../Xaml/BasicTests.cs | 66 +------- .../Xaml/ControlTemplateTests.cs | 143 ++++++++++++++++++ 2 files changed, 144 insertions(+), 65 deletions(-) create mode 100644 tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index f20faa2287..16685c5d2d 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs @@ -281,70 +281,6 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal(expected4, grid.RowDefinitions[3].Height); } - [Fact] - public void ControlTemplate_With_Nested_Child_Is_Operational() - { - var xaml = @" - - - - - -"; - var template = AvaloniaRuntimeXamlLoader.Parse(xaml); - - var parent = (ContentControl)template.Build(new ContentControl()).Control; - - Assert.Equal("parent", parent.Name); - - var child = parent.Content as ContentControl; - - Assert.NotNull(child); - - Assert.Equal("child", child.Name); - } - - [Fact] - public void ControlTemplate_With_TargetType_Is_Operational() - { - var xaml = @" - - - -"; - var template = AvaloniaRuntimeXamlLoader.Parse(xaml); - - Assert.Equal(typeof(ContentControl), template.TargetType); - - Assert.IsType(typeof(ContentPresenter), template.Build(new ContentControl()).Control); - } - - [Fact] - public void ControlTemplate_With_Panel_Children_Are_Added() - { - var xaml = @" - - - - - - -"; - var template = AvaloniaRuntimeXamlLoader.Parse(xaml); - - var panel = (Panel)template.Build(new ContentControl()).Control; - - Assert.Equal(2, panel.Children.Count); - - var foo = panel.Children[0]; - var bar = panel.Children[1]; - - Assert.Equal("Foo", foo.Name); - Assert.Equal("Bar", bar.Name); - } - [Fact] public void Named_x_Control_Is_Added_To_NameScope_Simple() { @@ -361,7 +297,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } [Fact] - public void Standart_TypeConverter_Is_Used() + public void Standard_TypeConverter_Is_Used() { var xaml = @""; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs new file mode 100644 index 0000000000..fbb16d9f2e --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs @@ -0,0 +1,143 @@ +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Data; +using Avalonia.Diagnostics; +using Avalonia.Markup.Xaml.Templates; +using Avalonia.Media; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Xaml +{ + public class ControlTemplateTests : XamlTestBase + { + [Fact] + public void Inline_ControlTemplate_Values_Are_Set_With_Style_Priority() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var button = (Button)window.Content; + + window.ApplyTemplate(); + button.ApplyTemplate(); + + var presenter = (ContentPresenter)button.Presenter; + Assert.Equal(Brushes.Red, presenter.Background); + + var diagnostic = presenter.GetDiagnostic(Button.BackgroundProperty); + Assert.Equal(BindingPriority.Style, diagnostic.Priority); + } + } + + [Fact] + public void Style_ControlTemplate_Values_Are_Set_With_Style_Priority() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var button = (Button)window.Content; + + window.ApplyTemplate(); + button.ApplyTemplate(); + + var presenter = (ContentPresenter)button.Presenter; + Assert.Equal(Dock.Top, DockPanel.GetDock(presenter)); + + var diagnostic = presenter.GetDiagnostic(DockPanel.DockProperty); + Assert.Equal(BindingPriority.Style, diagnostic.Priority); + } + } + + [Fact] + public void ControlTemplate_TemplateBindings_Are_Set_With_TemplatedParent_Priority() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var button = (Button)window.Content; + + window.ApplyTemplate(); + button.ApplyTemplate(); + + var presenter = (ContentPresenter)button.Presenter; + Assert.Equal("Foo", presenter.Content); + + var diagnostic = presenter.GetDiagnostic(ContentPresenter.ContentProperty); + Assert.Equal(BindingPriority.TemplatedParent, diagnostic.Priority); + } + } + [Fact] public void ControlTemplate_With_Nested_Child_Is_Operational() { From f7ff0a02893396f5ed28a033005d9907b97af096 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 20 Feb 2022 23:48:49 +0100 Subject: [PATCH 006/389] More failing tests for ControlTemplate resources. --- .../Xaml/ControlTemplateTests.cs | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs index f74aaaed76..2c38a82031 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs @@ -107,6 +107,74 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } + [Fact] + public void ControlTemplate_StaticResources_Are_Set_With_Style_Priority() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + Red + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var button = (Button)window.Content; + + window.ApplyTemplate(); + button.ApplyTemplate(); + + var presenter = (ContentPresenter)button.Presenter; + Assert.Equal(Brushes.Red, presenter.Background); + + var diagnostic = presenter.GetDiagnostic(Button.BackgroundProperty); + Assert.Equal(BindingPriority.Style, diagnostic.Priority); + } + } + + [Fact] + public void ControlTemplate_DynamicResources_Are_Set_With_Style_Priority() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + Red + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var button = (Button)window.Content; + + window.ApplyTemplate(); + button.ApplyTemplate(); + + var presenter = (ContentPresenter)button.Presenter; + Assert.Equal(Brushes.Red, presenter.Background); + + var diagnostic = presenter.GetDiagnostic(Button.BackgroundProperty); + Assert.Equal(BindingPriority.Style, diagnostic.Priority); + } + } + [Fact] public void ControlTemplate_TemplateBindings_Are_Set_With_TemplatedParent_Priority() { From cb7488baa7ed96fedd0308f8363c7003f4484893 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 21 Feb 2022 22:54:58 +0100 Subject: [PATCH 007/389] Use Style priority for StaticResource in ControlTemplate. --- ...amlIlControlTemplatePriorityTransformer.cs | 33 +++++++++++-------- .../XamlIlAvaloniaPropertyHelper.cs | 32 ++++++++++++++++-- .../Avalonia.Markup.Xaml.Loader/xamlil.github | 2 +- 3 files changed, 50 insertions(+), 17 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplatePriorityTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplatePriorityTransformer.cs index eede21396d..e0090eb2e8 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplatePriorityTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplatePriorityTransformer.cs @@ -18,26 +18,31 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers // The node is a candidate for transformation if: // - It's a property assignment to an Avalonia property // - There's a ControlTemplate ancestor - // - There's just a direct call setter available + // - The property has a single value if (node is XamlPropertyAssignmentNode prop && prop.Property is XamlIlAvaloniaProperty avaloniaProperty && context.ParentNodes().Any(IsControlTemplate) && - prop.PossibleSetters.Count == 1 && - prop.PossibleSetters[0] is XamlDirectCallPropertySetter) + prop.Values.Count == 1) { - // Check if there are any setters on the property which accept a binding priority - - // this filters the candidates down to styled and attached properties. If so, then - // use this setter with BindingPriority.Style. - var setPriorityValueSetter = - avaloniaProperty.Setters.FirstOrDefault(x => x.Parameters[0] == bindingPriorityType); - - if(setPriorityValueSetter != null - && prop.Values.Count == 1 - && setPriorityValueSetter.Parameters[1].IsAssignableFrom(prop.Values[0].Type.GetClrType())) + var priorityValueSetters = new List(); + + // Iterate through the possible setters, trying to find a setter on the property + // which has a BindingPriority parameter followed by the parameter of the existing + // setter. + foreach (var setter in prop.PossibleSetters) + { + var s = avaloniaProperty.Setters.FirstOrDefault(x => + x.Parameters[0] == bindingPriorityType && + x.Parameters[1] == setter.Parameters[0]); + if (s != null) + priorityValueSetters.Add(s); + } + + // If any BindingPriority setters were found, use those. + if (priorityValueSetters.Count > 0) { - prop.PossibleSetters = new List { setPriorityValueSetter }; + prop.PossibleSetters = priorityValueSetters; prop.Values.Insert(0, new XamlConstantNode(node, bindingPriorityType, (int)BindingPriority.Style)); - return node; } } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs index bb439b8816..5c7a80e680 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs @@ -185,9 +185,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions AvaloniaXamlIlWellKnownTypes types) :base(original, original.Name, original.DeclaringType, original.Getter, original.Setters) { + var assignBinding = original.CustomAttributes.Any(ca => ca.Type.Equals(types.AssignBindingAttribute)); + AvaloniaProperty = field; CustomAttributes = original.CustomAttributes; - if (!original.CustomAttributes.Any(ca => ca.Type.Equals(types.AssignBindingAttribute))) + if (!assignBinding) Setters.Insert(0, new BindingSetter(types, original.DeclaringType, field)); // Styled and attached properties can be set with a BindingPriority when they're @@ -196,7 +198,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions field.FieldType.GenericTypeDefinition == types.AvaloniaAttachedPropertyT) { var propertyType = field.FieldType.GenericArguments[0]; - Setters.Insert(1, new SetValueWithPrioritySetter(types, original.DeclaringType, field, propertyType)); + Setters.Insert(0, new SetValueWithPrioritySetter(types, original.DeclaringType, field, propertyType)); + if (!assignBinding) + Setters.Insert(1, new BindingWithPrioritySetter(types, original.DeclaringType, field)); } Setters.Insert(0, new UnsetValueSetter(types, original.DeclaringType, field)); @@ -249,6 +253,30 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions } } + class BindingWithPrioritySetter : AvaloniaPropertyCustomSetter + { + public BindingWithPrioritySetter(AvaloniaXamlIlWellKnownTypes types, + IXamlType declaringType, + IXamlField avaloniaProperty) : base(types, declaringType, avaloniaProperty) + { + Parameters = new[] { types.BindingPriority, types.IBinding }; + } + + public override void Emit(IXamlILEmitter emitter) + { + using (var bloc = emitter.LocalsPool.GetLocal(Types.IBinding)) + using (var priorityLocal = emitter.LocalsPool.GetLocal(Types.Int)) + emitter + .Stloc(bloc.Local) + .Stloc(priorityLocal.Local) + .Ldsfld(AvaloniaProperty) + .Ldloc(bloc.Local) + // TODO: provide anchor? + .Ldnull(); + emitter.EmitCall(Types.AvaloniaObjectBindMethod, true); + } + } + class SetValueWithPrioritySetter : AvaloniaPropertyCustomSetter { public SetValueWithPrioritySetter(AvaloniaXamlIlWellKnownTypes types, IXamlType declaringType, IXamlField avaloniaProperty, diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github index e0cfcd0ef6..a4897d581c 160000 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github @@ -1 +1 @@ -Subproject commit e0cfcd0ef687e613ef8905e8d0891974b68c565d +Subproject commit a4897d581c7f155543102f0d744a47863e1fbfea From a142b7ea04578e2b4cc5e521a38288fe9b3cd790 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 22 Feb 2022 23:14:46 +0100 Subject: [PATCH 008/389] Detect control template from markup extensions. Allow markup extensions to detect whether they're in a control template and use that info in `DynamicResourceExtension` to use `Style` priority. --- .../Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj | 1 + src/Markup/Avalonia.Markup.Xaml/Extensions.cs | 4 ++-- .../MarkupExtensions/DynamicResourceExtension.cs | 8 ++++++-- .../Runtime/IAvaloniaXamlIlControlTemplateProvider.cs | 8 ++++++++ .../XamlIl/Runtime/XamlIlRuntimeHelpers.cs | 5 ++++- 5 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/IAvaloniaXamlIlControlTemplateProvider.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 86132c5d27..4d9e216916 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -46,6 +46,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/Extensions.cs b/src/Markup/Avalonia.Markup.Xaml/Extensions.cs index 263750c316..d937a83010 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Extensions.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Extensions.cs @@ -24,10 +24,10 @@ namespace Avalonia.Markup.Xaml public static IEnumerable GetParents(this IServiceProvider sp) { return sp.GetService().Parents.OfType(); - - } + public static bool IsInControlTemplate(this IServiceProvider sp) => sp.GetService() != null; + public static Type ResolveType(this IServiceProvider ctx, string namespacePrefix, string type) { var tr = ctx.GetService(); diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs index 087611bd59..65e38508bc 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs @@ -11,6 +11,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions public class DynamicResourceExtension : IBinding { private object? _anchor; + private BindingPriority _priority; public DynamicResourceExtension() { @@ -25,6 +26,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions public IBinding ProvideValue(IServiceProvider serviceProvider) { + if (serviceProvider.IsInControlTemplate()) + _priority = BindingPriority.Style; + var provideTarget = serviceProvider.GetService(); if (!(provideTarget.TargetObject is IStyledElement)) @@ -53,12 +57,12 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions if (control != null) { var source = control.GetResourceObservable(ResourceKey, GetConverter(targetProperty)); - return InstancedBinding.OneWay(source); + return InstancedBinding.OneWay(source, _priority); } else if (_anchor is IResourceProvider resourceProvider) { var source = resourceProvider.GetResourceObservable(ResourceKey, GetConverter(targetProperty)); - return InstancedBinding.OneWay(source); + return InstancedBinding.OneWay(source, _priority); } return null; diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/IAvaloniaXamlIlControlTemplateProvider.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/IAvaloniaXamlIlControlTemplateProvider.cs new file mode 100644 index 0000000000..ed3f5bfdff --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/IAvaloniaXamlIlControlTemplateProvider.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Avalonia.Markup.Xaml.XamlIl.Runtime +{ + public interface IAvaloniaXamlIlControlTemplateProvider + { + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs index c48f386ffd..a0e7fd7dcf 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs @@ -42,7 +42,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime class DeferredParentServiceProvider : IAvaloniaXamlIlParentStackProvider, IServiceProvider, - IRootObjectProvider + IRootObjectProvider, + IAvaloniaXamlIlControlTemplateProvider { private readonly IServiceProvider _parentProvider; private readonly List _parentResourceNodes; @@ -75,6 +76,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime return this; if (serviceType == typeof(IRootObjectProvider)) return this; + if (serviceType == typeof(IAvaloniaXamlIlControlTemplateProvider)) + return this; return _parentProvider?.GetService(serviceType); } From beaf956af72b4ad38a56f92b6d9ec048fedb2815 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 22 Feb 2022 23:23:15 +0100 Subject: [PATCH 009/389] Use TemplatedParent priority in ControlTemplates. --- ...AvaloniaXamlIlControlTemplatePriorityTransformer.cs | 2 +- .../MarkupExtensions/DynamicResourceExtension.cs | 2 +- .../Xaml/ControlTemplateTests.cs | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplatePriorityTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplatePriorityTransformer.cs index e0090eb2e8..6cab68e756 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplatePriorityTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplatePriorityTransformer.cs @@ -42,7 +42,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers if (priorityValueSetters.Count > 0) { prop.PossibleSetters = priorityValueSetters; - prop.Values.Insert(0, new XamlConstantNode(node, bindingPriorityType, (int)BindingPriority.Style)); + prop.Values.Insert(0, new XamlConstantNode(node, bindingPriorityType, (int)BindingPriority.TemplatedParent)); } } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs index 65e38508bc..f13ecab4e1 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs @@ -27,7 +27,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions public IBinding ProvideValue(IServiceProvider serviceProvider) { if (serviceProvider.IsInControlTemplate()) - _priority = BindingPriority.Style; + _priority = BindingPriority.TemplatedParent; var provideTarget = serviceProvider.GetService(); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs index 2c38a82031..2d6ed1dc62 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs @@ -38,7 +38,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal(Brushes.Red, presenter.Background); var diagnostic = presenter.GetDiagnostic(Button.BackgroundProperty); - Assert.Equal(BindingPriority.Style, diagnostic.Priority); + Assert.Equal(BindingPriority.TemplatedParent, diagnostic.Priority); } } @@ -72,7 +72,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal(Brushes.Red, presenter.Background); var diagnostic = presenter.GetDiagnostic(Button.BackgroundProperty); - Assert.Equal(BindingPriority.Style, diagnostic.Priority); + Assert.Equal(BindingPriority.TemplatedParent, diagnostic.Priority); } } @@ -103,7 +103,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal(Dock.Top, DockPanel.GetDock(presenter)); var diagnostic = presenter.GetDiagnostic(DockPanel.DockProperty); - Assert.Equal(BindingPriority.Style, diagnostic.Priority); + Assert.Equal(BindingPriority.TemplatedParent, diagnostic.Priority); } } @@ -137,7 +137,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal(Brushes.Red, presenter.Background); var diagnostic = presenter.GetDiagnostic(Button.BackgroundProperty); - Assert.Equal(BindingPriority.Style, diagnostic.Priority); + Assert.Equal(BindingPriority.TemplatedParent, diagnostic.Priority); } } @@ -171,7 +171,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal(Brushes.Red, presenter.Background); var diagnostic = presenter.GetDiagnostic(Button.BackgroundProperty); - Assert.Equal(BindingPriority.Style, diagnostic.Priority); + Assert.Equal(BindingPriority.TemplatedParent, diagnostic.Priority); } } From b094699f760b1f760174d841fd3a5fddfb7e663d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 9 May 2022 19:36:06 +0300 Subject: [PATCH 010/389] Compositor works with X11. Somewhat --- samples/ControlCatalog.NetCore/Program.cs | 1 + src/Avalonia.Base/Avalonia.Base.csproj | 5 +- .../Collections/IAvaloniaReadOnlyList.cs | 1 + .../Collections/Pooled/PooledList.cs | 2 +- src/Avalonia.Base/Matrix.cs | 39 + .../Animations/AnimatedValueStore.cs | 40 + .../Animations/CompositionAnimation.cs | 63 ++ .../Animations/CompositionAnimationGroup.cs | 26 + .../Animations/ExpressionAnimation.cs | 34 + .../Animations/ExpressionAnimationInstance.cs | 44 ++ .../Animations/IAnimationInstance.cs | 11 + .../Animations/ICompositionAnimationBase.cs | 12 + .../Animations/ImplicitAnimationCollection.cs | 73 ++ .../Composition/Animations/Interpolators.cs | 73 ++ .../Animations/KeyFrameAnimation.cs | 53 ++ .../Animations/KeyFrameAnimationInstance.cs | 139 ++++ .../Composition/Animations/KeyFrames.cs | 80 ++ .../Animations/PropertySetSnapshot.cs | 46 ++ .../Composition/CompositingRenderer.cs | 194 +++++ .../Composition/CompositionDrawListVisual.cs | 44 ++ .../Composition/CompositionEasingFunction.cs | 97 +++ .../Composition/CompositionGradientBrush.cs | 16 + .../Composition/CompositionObject.cs | 125 +++ .../Composition/CompositionPropertySet.cs | 132 ++++ .../Composition/CompositionTarget.cs | 107 +++ .../Rendering/Composition/Compositor.cs | 143 ++++ .../Composition/CompositorRenderLoopTask.cs | 20 + .../Rendering/Composition/ContainerVisual.cs | 20 + .../Rendering/Composition/CustomDrawVisual.cs | 56 ++ .../Drawing/CompositionDrawList.cs | 81 ++ .../Drawing/CompositionDrawingContext.cs | 383 +++++++++ .../Rendering/Composition/Enums.cs | 120 +++ .../Expressions/BuiltInExpressionFfi.cs | 234 ++++++ .../Expressions/DelegateExpressionFfi.cs | 181 +++++ .../Composition/Expressions/Expression.cs | 331 ++++++++ .../ExpressionEvaluationContext.cs | 31 + .../Expressions/ExpressionParseException.cs | 14 + .../Expressions/ExpressionParser.cs | 298 +++++++ .../Expressions/ExpressionVariant.cs | 739 ++++++++++++++++++ .../Composition/Expressions/TokenParser.cs | 256 ++++++ .../Composition/ICompositionSurface.cs | 9 + .../Rendering/Composition/MatrixUtils.cs | 46 ++ .../Composition/Server/DrawingContextProxy.cs | 142 ++++ .../Composition/Server/ReadbackIndices.cs | 46 ++ .../Server/ServerCompositionBrush.cs | 7 + .../Server/ServerCompositionDrawListVisual.cs | 41 + .../Server/ServerCompositionGradientBrush.cs | 15 + .../ServerCompositionLinearGradientBrush.cs | 22 + .../Server/ServerCompositionSurface.cs | 9 + .../Server/ServerCompositionTarget.cs | 53 ++ .../Composition/Server/ServerCompositor.cs | 90 +++ .../Server/ServerContainerVisual.cs | 29 + .../Server/ServerCustomDrawVisual.cs | 31 + .../Composition/Server/ServerList.cs | 46 ++ .../Composition/Server/ServerObject.cs | 36 + .../Server/ServerSolidColorVisual.cs | 16 + .../Composition/Server/ServerSpriteVisual.cs | 20 + .../Composition/Server/ServerVisual.cs | 92 +++ .../Rendering/Composition/Transport/Batch.cs | 37 + .../Rendering/Composition/Transport/Change.cs | 82 ++ .../Composition/Transport/ChangeSet.cs | 36 + .../Composition/Transport/ChangeSetPool.cs | 42 + .../Transport/CustomDrawVisualChanges.cs | 20 + .../Transport/DrawListVisualChanges.cs | 48 ++ .../Composition/Transport/ListChange.cs | 19 + .../Composition/Transport/ListChangeSet.cs | 25 + .../Transport/ServerListProxyHelper.cs | 92 +++ .../Composition/Transport/VisualChanges.cs | 16 + .../Composition/Utils/CubicBezier.cs | 302 +++++++ .../Rendering/Composition/Utils/MathExt.cs | 23 + .../Rendering/Composition/Visual.cs | 64 ++ .../Rendering/Composition/VisualCollection.cs | 64 ++ src/Avalonia.Base/Rendering/IRenderer.cs | 6 + .../SceneGraph/DeferredDrawingContextImpl.cs | 1 + src/Avalonia.Base/Size.cs | 11 + .../Threading/DispatcherPriority.cs | 14 +- src/Avalonia.Base/Visual.cs | 25 +- src/Avalonia.Base/composition-schema.xml | 70 ++ .../HeadlessPlatformRenderInterface.cs | 2 + .../CompositionRoslynGenerator.cs | 21 + .../CompositionGenerator/Config.cs | 116 +++ .../CompositionGenerator/Extensions.cs | 90 +++ .../Generator.KeyFrameAnimation.cs | 59 ++ .../Generator.ListProxy.cs | 121 +++ .../CompositionGenerator/Generator.Utils.cs | 66 ++ .../CompositionGenerator/Generator.cs | 504 ++++++++++++ src/Avalonia.X11/X11Platform.cs | 8 +- src/Avalonia.X11/X11Window.cs | 17 +- .../Media/DrawingContextImpl.cs | 7 + 89 files changed, 7106 insertions(+), 16 deletions(-) create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/ICompositionAnimationBase.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/PropertySetSnapshot.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionGradientBrush.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionObject.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Compositor.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositorRenderLoopTask.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Enums.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParseException.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParser.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/ICompositionSurface.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionBrush.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionGradientBrush.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionLinearGradientBrush.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/Change.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Utils/CubicBezier.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Utils/MathExt.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Visual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/VisualCollection.cs create mode 100644 src/Avalonia.Base/composition-schema.xml create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/Generator.Utils.cs create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 4464413e63..2a0755b900 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -111,6 +111,7 @@ namespace ControlCatalog.NetCore EnableMultiTouch = true, UseDBusMenu = true, EnableIme = true, + UseCompositor = true }) .With(new Win32PlatformOptions { diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index 8e4755b4b7..bcebdd504c 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -3,10 +3,13 @@ net6.0;netstandard2.0 Avalonia.Base Avalonia - True + True + true + $(BaseIntermediateOutputPath)\GeneratedFiles + diff --git a/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs b/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs index a6a5953827..cefbf642be 100644 --- a/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs +++ b/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; diff --git a/src/Avalonia.Base/Collections/Pooled/PooledList.cs b/src/Avalonia.Base/Collections/Pooled/PooledList.cs index 200a52fb0d..ffda1bedca 100644 --- a/src/Avalonia.Base/Collections/Pooled/PooledList.cs +++ b/src/Avalonia.Base/Collections/Pooled/PooledList.cs @@ -1434,7 +1434,7 @@ namespace Avalonia.Collections.Pooled /// /// Returns the internal buffers to the ArrayPool. /// - public void Dispose() + public virtual void Dispose() { ReturnArray(); _size = 0; diff --git a/src/Avalonia.Base/Matrix.cs b/src/Avalonia.Base/Matrix.cs index b08a0eb98a..6f00b08d13 100644 --- a/src/Avalonia.Base/Matrix.cs +++ b/src/Avalonia.Base/Matrix.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.Numerics; using Avalonia.Utilities; namespace Avalonia @@ -106,6 +107,36 @@ namespace Avalonia (value1._m31 * value2.M12) + (value1._m32 * value2.M22) + value2._m32); } + public static Matrix operator +(Matrix value1, Matrix value2) + { + return new Matrix(value1.M11 + value2.M11, + value1.M12 + value2.M12, + value1.M21 + value2.M21, + value1.M22 + value2.M22, + value1.M31 + value2.M31, + value1.M32 + value2.M32); + } + + public static Matrix operator -(Matrix value1, Matrix value2) + { + return new Matrix(value1.M11 - value2.M11, + value1.M12 - value2.M12, + value1.M21 - value2.M21, + value1.M22 - value2.M22, + value1.M31 - value2.M31, + value1.M32 - value2.M32); + } + + public static Matrix operator *(Matrix value1, double value2) + { + return new Matrix(value1.M11 * value2, + value1.M12 * value2, + value1.M21 * value2, + value1.M22 * value2, + value1.M31 * value2, + value1.M32 * value2); + } + /// /// Negates the given matrix by multiplying all values by -1. /// @@ -427,6 +458,14 @@ namespace Avalonia return true; } +#if !BUILDTASK + public static implicit operator Matrix4x4(Matrix m) + { + return new Matrix4x4(new Matrix3x2((float)m._m11, (float)m._m12, (float)m._m21, (float)m._m22, + (float)m._m31, (float)m._m32)); + } +#endif + public struct Decomposed { public Vector Translate; diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs b/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs new file mode 100644 index 0000000000..180c45022f --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs @@ -0,0 +1,40 @@ +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Animations +{ + internal struct AnimatedValueStore where T : struct + { + private T _direct; + private IAnimationInstance _animation; + private T? _lastAnimated; + + public T Direct => _direct; + + public T GetAnimated(ServerCompositor compositor) + { + if (_animation == null) + return _direct; + var v = _animation.Evaluate(compositor.ServerNow, ExpressionVariant.Create(_direct)) + .CastOrDefault(); + _lastAnimated = v; + return v; + } + + private T LastAnimated => _animation != null ? _lastAnimated ?? _direct : _direct; + + public bool IsAnimation => _animation != null; + + public void SetAnimation(ChangeSet cs, IAnimationInstance animation) + { + _animation = animation; + _animation.Start(cs.Batch.CommitedAt, ExpressionVariant.Create(LastAnimated)); + } + + public static implicit operator AnimatedValueStore(T value) => new AnimatedValueStore() + { + _direct = value + }; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs new file mode 100644 index 0000000000..9375faaaae --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs @@ -0,0 +1,63 @@ +// ReSharper disable InconsistentNaming +// ReSharper disable CheckNamespace + +using System; +using System.Collections.Generic; +using System.Numerics; +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Animations +{ + public abstract class CompositionAnimation : CompositionObject, ICompositionAnimationBase + { + private readonly CompositionPropertySet _propertySet; + internal CompositionAnimation(Compositor compositor) : base(compositor, null!) + { + _propertySet = new CompositionPropertySet(compositor); + } + + private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException(); + + public void ClearAllParameters() => _propertySet.ClearAll(); + + public void ClearParameter(string key) => _propertySet.Clear(key); + + void SetVariant(string key, ExpressionVariant value) => _propertySet.Set(key, value); + + public void SetColorParameter(string key, Avalonia.Media.Color value) => SetVariant(key, value); + + public void SetMatrix3x2Parameter(string key, Matrix3x2 value) => SetVariant(key, value); + + public void SetMatrix4x4Parameter(string key, Matrix4x4 value) => SetVariant(key, value); + + public void SetQuaternionParameter(string key, Quaternion value) => SetVariant(key, value); + + public void SetReferenceParameter(string key, CompositionObject compositionObject) => + _propertySet.Set(key, compositionObject); + + public void SetScalarParameter(string key, float value) => SetVariant(key, value); + + public void SetVector2Parameter(string key, Vector2 value) => SetVariant(key, value); + + public void SetVector3Parameter(string key, Vector3 value) => SetVariant(key, value); + + public void SetVector4Parameter(string key, Vector4 value) => SetVariant(key, value); + + // TODO: void SetExpressionReferenceParameter(string parameterName, IAnimationObject source) + + public string? Target { get; set; } + + internal abstract IAnimationInstance CreateInstance(ServerObject targetObject, + ExpressionVariant? finalValue); + + internal PropertySetSnapshot CreateSnapshot(bool server) + => _propertySet.Snapshot(server, 1); + + void ICompositionAnimationBase.InternalOnly() + { + + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs new file mode 100644 index 0000000000..833f7e498c --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Transport; + + +namespace Avalonia.Rendering.Composition.Animations +{ + public class CompositionAnimationGroup : CompositionObject, ICompositionAnimationBase + { + internal List Animations { get; } = new List(); + void ICompositionAnimationBase.InternalOnly() + { + + } + + public void Add(CompositionAnimation value) => Animations.Add(value); + public void Remove(CompositionAnimation value) => Animations.Remove(value); + public void RemoveAll() => Animations.Clear(); + + public CompositionAnimationGroup(Compositor compositor) : base(compositor, null!) + { + } + + private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException(); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs new file mode 100644 index 0000000000..a6f24c2e35 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs @@ -0,0 +1,34 @@ +// ReSharper disable CheckNamespace +using System; +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Animations +{ + public class ExpressionAnimation : CompositionAnimation + { + private string? _expression; + private Expression? _parsedExpression; + + internal ExpressionAnimation(Compositor compositor) : base(compositor) + { + } + + public string? Expression + { + get => _expression; + set + { + _expression = value; + _parsedExpression = null; + } + } + + private Expression ParsedExpression => _parsedExpression ??= ExpressionParser.Parse(_expression.AsSpan()); + + internal override IAnimationInstance CreateInstance( + ServerObject targetObject, ExpressionVariant? finalValue) + => new ExpressionAnimationInstance(ParsedExpression, + targetObject, finalValue, CreateSnapshot(true)); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs new file mode 100644 index 0000000000..47b947b2e9 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs @@ -0,0 +1,44 @@ +using System; +using Avalonia.Rendering.Composition.Expressions; + +namespace Avalonia.Rendering.Composition.Animations +{ + internal class ExpressionAnimationInstance : IAnimationInstance + { + private readonly Expression _expression; + private readonly IExpressionObject _target; + private ExpressionVariant _startingValue; + private readonly ExpressionVariant? _finalValue; + private readonly PropertySetSnapshot _parameters; + + public ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue) + { + var ctx = new ExpressionEvaluationContext + { + Parameters = _parameters, + Target = _target, + ForeignFunctionInterface = BuiltInExpressionFfi.Instance, + StartingValue = _startingValue, + FinalValue = _finalValue ?? _startingValue, + CurrentValue = currentValue + }; + return _expression.Evaluate(ref ctx); + } + + public void Start(TimeSpan startedAt, ExpressionVariant startingValue) + { + _startingValue = startingValue; + } + + public ExpressionAnimationInstance(Expression expression, + IExpressionObject target, + ExpressionVariant? finalValue, + PropertySetSnapshot parameters) + { + _expression = expression; + _target = target; + _finalValue = finalValue; + _parameters = parameters; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs new file mode 100644 index 0000000000..a0b066ae0c --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs @@ -0,0 +1,11 @@ +using System; +using Avalonia.Rendering.Composition.Expressions; + +namespace Avalonia.Rendering.Composition.Animations +{ + internal interface IAnimationInstance + { + ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue); + void Start(TimeSpan startedAt, ExpressionVariant startingValue); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ICompositionAnimationBase.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ICompositionAnimationBase.cs new file mode 100644 index 0000000000..bf40fd3ad2 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ICompositionAnimationBase.cs @@ -0,0 +1,12 @@ +// ReSharper disable CheckNamespace + +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Animations +{ + public interface ICompositionAnimationBase + { + internal void InternalOnly(); + } + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs new file mode 100644 index 0000000000..be91352527 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Animations +{ + public class ImplicitAnimationCollection : CompositionObject, IDictionary + { + private Dictionary _inner = new Dictionary(); + private IDictionary _innerface; + internal ImplicitAnimationCollection(Compositor compositor) : base(compositor, null!) + { + _innerface = _inner; + } + + private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException(); + + public IEnumerator> GetEnumerator() => _inner.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable) _inner).GetEnumerator(); + + void ICollection>.Add(KeyValuePair item) => _innerface.Add(item); + + public void Clear() => _inner.Clear(); + + bool ICollection>.Contains(KeyValuePair item) => _innerface.Contains(item); + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) => _innerface.CopyTo(array, arrayIndex); + + bool ICollection>.Remove(KeyValuePair item) => _innerface.Remove(item); + + public int Count => _inner.Count; + + bool ICollection>.IsReadOnly => _innerface.IsReadOnly; + + public void Add(string key, ICompositionAnimationBase value) => _inner.Add(key, value); + + public bool ContainsKey(string key) => _inner.ContainsKey(key); + + public bool Remove(string key) => _inner.Remove(key); + + public bool TryGetValue(string key, [MaybeNullWhen(false)] out ICompositionAnimationBase value) => + _inner.TryGetValue(key, out value); + + public ICompositionAnimationBase this[string key] + { + get => _inner[key]; + set => _inner[key] = value; + } + + ICollection IDictionary.Keys => _innerface.Keys; + + ICollection IDictionary.Values => + _innerface.Values; + + // UWP compat + public uint Size => (uint) Count; + + public IReadOnlyDictionary GetView() => + new Dictionary(this); + + public bool HasKey(string key) => ContainsKey(key); + public void Insert(string key, ICompositionAnimationBase animation) => Add(key, animation); + + public ICompositionAnimationBase? Lookup(string key) + { + _inner.TryGetValue(key, out var rv); + return rv; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs b/src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs new file mode 100644 index 0000000000..62b790701a --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs @@ -0,0 +1,73 @@ +using System; +using System.Numerics; + +namespace Avalonia.Rendering.Composition.Animations +{ + internal interface IInterpolator + { + T Interpolate(T from, T to, float progress); + } + + class ScalarInterpolator : IInterpolator + { + public float Interpolate(float @from, float to, float progress) => @from + (to - @from) * progress; + + public static ScalarInterpolator Instance { get; } = new ScalarInterpolator(); + } + + class Vector2Interpolator : IInterpolator + { + public Vector2 Interpolate(Vector2 @from, Vector2 to, float progress) + => Vector2.Lerp(@from, to, progress); + + public static Vector2Interpolator Instance { get; } = new Vector2Interpolator(); + } + + class Vector3Interpolator : IInterpolator + { + public Vector3 Interpolate(Vector3 @from, Vector3 to, float progress) + => Vector3.Lerp(@from, to, progress); + + public static Vector3Interpolator Instance { get; } = new Vector3Interpolator(); + } + + class Vector4Interpolator : IInterpolator + { + public Vector4 Interpolate(Vector4 @from, Vector4 to, float progress) + => Vector4.Lerp(@from, to, progress); + + public static Vector4Interpolator Instance { get; } = new Vector4Interpolator(); + } + + class QuaternionInterpolator : IInterpolator + { + public Quaternion Interpolate(Quaternion @from, Quaternion to, float progress) + => Quaternion.Lerp(@from, to, progress); + + public static QuaternionInterpolator Instance { get; } = new QuaternionInterpolator(); + } + + class ColorInterpolator : IInterpolator + { + static byte Lerp(float a, float b, float p) => (byte) Math.Max(0, Math.Min(255, (p * (b - a) + a))); + + public static Avalonia.Media.Color + LerpRGB(Avalonia.Media.Color to, Avalonia.Media.Color from, float progress) => + new Avalonia.Media.Color(Lerp(to.A, @from.A, progress), + Lerp(to.R, @from.R, progress), + Lerp(to.G, @from.G, progress), + Lerp(to.B, @from.B, progress)); + + public Avalonia.Media.Color Interpolate(Avalonia.Media.Color @from, Avalonia.Media.Color to, float progress) + => LerpRGB(@from, to, progress); + + public static ColorInterpolator Instance { get; } = new ColorInterpolator(); + } + + class BooleanInterpolator : IInterpolator + { + public bool Interpolate(bool @from, bool to, float progress) => progress >= 1 ? to : @from; + + public static BooleanInterpolator Instance { get; } = new BooleanInterpolator(); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs new file mode 100644 index 0000000000..065dfd7a8e --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs @@ -0,0 +1,53 @@ +namespace Avalonia.Rendering.Composition.Animations +{ + public abstract class KeyFrameAnimation : CompositionAnimation + { + internal KeyFrameAnimation(Compositor compositor) : base(compositor) + { + } + + public AnimationDelayBehavior DelayBehavior { get; set; } + public System.TimeSpan DelayTime { get; set; } + public AnimationDirection Direction { get; set; } + public System.TimeSpan Duration { get; set; } + public AnimationIterationBehavior IterationBehavior { get; set; } + public int IterationCount { get; set; } = 1; + public AnimationStopBehavior StopBehavior { get; set; } + + private protected abstract IKeyFrames KeyFrames { get; } + + public void InsertExpressionKeyFrame(float normalizedProgressKey, string value, + CompositionEasingFunction easingFunction) => + KeyFrames.InsertExpressionKeyFrame(normalizedProgressKey, value, easingFunction); + + public void InsertExpressionKeyFrame(float normalizedProgressKey, string value) + => KeyFrames.InsertExpressionKeyFrame(normalizedProgressKey, value, new LinearEasingFunction(Compositor)); + } + + public enum AnimationDelayBehavior + { + SetInitialValueAfterDelay, + SetInitialValueBeforeDelay + } + + public enum AnimationDirection + { + Normal, + Reverse, + Alternate, + AlternateReverse + } + + public enum AnimationIterationBehavior + { + Count, + Forever + } + + public enum AnimationStopBehavior + { + LeaveCurrentValue, + SetToInitialValue, + SetToFinalValue + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs new file mode 100644 index 0000000000..b90a02148d --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs @@ -0,0 +1,139 @@ +using System; +using Avalonia.Rendering.Composition.Expressions; + +namespace Avalonia.Rendering.Composition.Animations +{ + class KeyFrameAnimationInstance : IAnimationInstance where T : struct + { + private readonly IInterpolator _interpolator; + private readonly ServerKeyFrame[] _keyFrames; + private readonly PropertySetSnapshot _snapshot; + private readonly ExpressionVariant? _finalValue; + private readonly IExpressionObject _target; + private readonly AnimationDelayBehavior _delayBehavior; + private readonly TimeSpan _delayTime; + private readonly AnimationDirection _direction; + private readonly TimeSpan _duration; + private readonly AnimationIterationBehavior _iterationBehavior; + private readonly int _iterationCount; + private readonly AnimationStopBehavior _stopBehavior; + private TimeSpan _startedAt; + private T _startingValue; + + public KeyFrameAnimationInstance( + IInterpolator interpolator, ServerKeyFrame[] keyFrames, + PropertySetSnapshot snapshot, ExpressionVariant? finalValue, + IExpressionObject target, + AnimationDelayBehavior delayBehavior, TimeSpan delayTime, + AnimationDirection direction, TimeSpan duration, + AnimationIterationBehavior iterationBehavior, + int iterationCount, AnimationStopBehavior stopBehavior) + { + _interpolator = interpolator; + _keyFrames = keyFrames; + _snapshot = snapshot; + _finalValue = finalValue; + _target = target; + _delayBehavior = delayBehavior; + _delayTime = delayTime; + _direction = direction; + _duration = duration; + _iterationBehavior = iterationBehavior; + _iterationCount = iterationCount; + _stopBehavior = stopBehavior; + if (_keyFrames.Length == 0) + throw new InvalidOperationException("Animation has no key frames"); + if(_duration.Ticks <= 0) + throw new InvalidOperationException("Invalid animation duration"); + } + + public ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue) + { + var elapsed = now - _startedAt; + var starting = ExpressionVariant.Create(_startingValue); + var ctx = new ExpressionEvaluationContext + { + Parameters = _snapshot, + Target = _target, + CurrentValue = currentValue, + FinalValue = _finalValue ?? starting, + StartingValue = starting, + ForeignFunctionInterface = BuiltInExpressionFfi.Instance + }; + + if (elapsed < _delayTime) + { + if (_delayBehavior == AnimationDelayBehavior.SetInitialValueBeforeDelay) + return ExpressionVariant.Create(GetKeyFrame(ref ctx, _keyFrames[0])); + return currentValue; + } + + elapsed -= _delayTime; + var iterationNumber = elapsed.Ticks / _duration.Ticks; + if (_iterationBehavior == AnimationIterationBehavior.Count + && iterationNumber >= _iterationCount) + return ExpressionVariant.Create(GetKeyFrame(ref ctx, _keyFrames[_keyFrames.Length - 1])); + + + var evenIterationNumber = iterationNumber % 2 == 0; + elapsed = TimeSpan.FromTicks(elapsed.Ticks % _duration.Ticks); + + var reverse = + _direction == AnimationDirection.Alternate + ? !evenIterationNumber + : _direction == AnimationDirection.AlternateReverse + ? evenIterationNumber + : _direction == AnimationDirection.Reverse; + + var iterationProgress = elapsed.TotalSeconds / _duration.TotalSeconds; + if (reverse) + iterationProgress = 1 - iterationProgress; + + var left = new ServerKeyFrame + { + Value = _startingValue + }; + var right = _keyFrames[_keyFrames.Length - 1]; + for (var c = 0; c < _keyFrames.Length; c++) + { + var kf = _keyFrames[c]; + if (kf.Key < iterationProgress) + { + // this is the last frame + if (c == _keyFrames.Length - 1) + return ExpressionVariant.Create(GetKeyFrame(ref ctx, kf)); + + left = kf; + right = _keyFrames[c + 1]; + break; + } + } + + var keyProgress = Math.Max(0, Math.Min(1, (iterationProgress - left.Key) / (right.Key - left.Key))); + + var easedKeyProgress = right.EasingFunction.Ease((float) keyProgress); + if (float.IsNaN(easedKeyProgress) || float.IsInfinity(easedKeyProgress)) + return currentValue; + + return ExpressionVariant.Create(_interpolator.Interpolate( + GetKeyFrame(ref ctx, left), + GetKeyFrame(ref ctx, right), + easedKeyProgress + )); + } + + T GetKeyFrame(ref ExpressionEvaluationContext ctx, ServerKeyFrame f) + { + if (f.Expression != null) + return f.Expression.Evaluate(ref ctx).CastOrDefault(); + else + return f.Value; + } + + public void Start(TimeSpan startedAt, ExpressionVariant startingValue) + { + _startedAt = startedAt; + _startingValue = startingValue.CastOrDefault(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs new file mode 100644 index 0000000000..d7f2504061 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Expressions; + +namespace Avalonia.Rendering.Composition.Animations +{ + class KeyFrames : List>, IKeyFrames + { + void Validate(float key) + { + if (key < 0 || key > 1) + throw new ArgumentException("Key frame key"); + if (Count > 0 && this[Count - 1].NormalizedProgressKey > key) + throw new ArgumentException("Key frame key " + key + " is less than the previous one"); + } + + public void InsertExpressionKeyFrame(float normalizedProgressKey, string value, + CompositionEasingFunction easingFunction) + { + Validate(normalizedProgressKey); + Add(new KeyFrame + { + NormalizedProgressKey = normalizedProgressKey, + Expression = Expression.Parse(value), + EasingFunction = easingFunction + }); + } + + public void Insert(float normalizedProgressKey, T value, CompositionEasingFunction easingFunction) + { + Validate(normalizedProgressKey); + Add(new KeyFrame + { + NormalizedProgressKey = normalizedProgressKey, + Value = value, + EasingFunction = easingFunction + }); + } + + public ServerKeyFrame[] Snapshot() + { + var frames = new ServerKeyFrame[Count]; + for (var c = 0; c < Count; c++) + { + var f = this[c]; + frames[c] = new ServerKeyFrame + { + Expression = f.Expression, + Value = f.Value, + EasingFunction = f.EasingFunction.Snapshot(), + Key = f.NormalizedProgressKey + }; + } + return frames; + } + } + + struct KeyFrame + { + public float NormalizedProgressKey; + public T Value; + public Expression Expression; + public CompositionEasingFunction EasingFunction; + } + + struct ServerKeyFrame + { + public T Value; + public Expression Expression; + public IEasingFunction EasingFunction; + public float Key; + } + + + + interface IKeyFrames + { + public void InsertExpressionKeyFrame(float normalizedProgressKey, string value, CompositionEasingFunction easingFunction); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/PropertySetSnapshot.cs b/src/Avalonia.Base/Rendering/Composition/Animations/PropertySetSnapshot.cs new file mode 100644 index 0000000000..ca703dfc6f --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/PropertySetSnapshot.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Expressions; + +namespace Avalonia.Rendering.Composition.Animations +{ + internal class PropertySetSnapshot : IExpressionParameterCollection, IExpressionObject + { + private readonly Dictionary _dic; + + public struct Value + { + public ExpressionVariant Variant; + public IExpressionObject Object; + + public Value(IExpressionObject o) + { + Object = o; + Variant = default; + } + + public static implicit operator Value(ExpressionVariant v) => new Value + { + Variant = v + }; + } + + public PropertySetSnapshot(Dictionary dic) + { + _dic = dic; + } + + public ExpressionVariant GetParameter(string name) + { + _dic.TryGetValue(name, out var v); + return v.Variant; + } + + public IExpressionObject GetObjectParameter(string name) + { + _dic.TryGetValue(name, out var v); + return v.Object; + } + + public ExpressionVariant GetProperty(string name) => GetParameter(name); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs new file mode 100644 index 0000000000..07ac54b634 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Runtime.InteropServices; +using Avalonia.Collections; +using Avalonia.Media; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Threading; +using Avalonia.VisualTree; + +namespace Avalonia.Rendering.Composition; + +public class CompositingRenderer : RendererBase, IRendererWithCompositor +{ + private readonly IRenderRoot _root; + private readonly Compositor _compositor; + private readonly IDeferredRendererLock? _rendererLock; + CompositionDrawingContext _recorder = new(); + DrawingContext _recordingContext; + private HashSet _dirty = new(); + private HashSet _recalculateChildren = new(); + private readonly CompositionTarget _target; + private bool _queuedUpdate; + private Action _update; + + public CompositingRenderer(IRenderRoot root, + Compositor compositor, + IDeferredRendererLock? rendererLock = null) + { + _root = root; + _compositor = compositor; + _recordingContext = new DrawingContext(_recorder); + _rendererLock = rendererLock ?? new ManagedDeferredRendererLock(); + _target = compositor.CreateCompositionTarget(root.CreateRenderTarget); + _target.Root = ((Visual)root!.VisualRoot!).AttachToCompositor(compositor); + _update = Update; + } + + public bool DrawFps { get; set; } + public bool DrawDirtyRects { get; set; } + public event EventHandler? SceneInvalidated; + + void QueueUpdate() + { + if(_queuedUpdate) + return; + _queuedUpdate = true; + Dispatcher.UIThread.Post(_update, DispatcherPriority.Composition); + } + public void AddDirty(IVisual visual) + { + _dirty.Add((Visual)visual); + QueueUpdate(); + } + + public IEnumerable HitTest(Point p, IVisual root, Func filter) + { + var res = _target.TryHitTest(new Vector2((float)p.X, (float)p.Y)); + if(res == null) + yield break; + for (var index = res.Count - 1; index >= 0; index--) + { + var v = res[index]; + if (v is CompositionDrawListVisual dv) + { + if (filter == null || filter(dv.Visual)) + yield return dv.Visual; + } + } + } + + public IVisual? HitTestFirst(Point p, IVisual root, Func filter) + { + // TODO: Optimize + return HitTest(p, root, filter).FirstOrDefault(); + } + + public void RecalculateChildren(IVisual visual) + { + _recalculateChildren.Add((Visual)visual); + QueueUpdate(); + } + + private void SyncChildren(Visual v) + { + //TODO: Optimize by moving that logic to Visual itself + if(v.CompositionVisual == null) + return; + var compositionChildren = v.CompositionVisual.Children; + var visualChildren = (AvaloniaList)v.GetVisualChildren(); + if (compositionChildren.Count == visualChildren.Count) + { + bool mismatch = false; + for(var c=0; c _target.IsEnabled = true; + + public void Stop() + { + _target.IsEnabled = true; + } + + public void Dispose() + { + Stop(); + _target.Dispose(); + // Wait for the composition batch to be applied and rendered to guarantee that + // render target is not used anymore and can be safely disposed + _compositor.RequestCommitAsync().Wait(); + } + + + public Compositor Compositor => _compositor; +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs new file mode 100644 index 0000000000..9f02055412 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs @@ -0,0 +1,44 @@ +using System.Numerics; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition; + +internal class CompositionDrawListVisual : CompositionContainerVisual +{ + public Visual Visual { get; } + + private new DrawListVisualChanges Changes => (DrawListVisualChanges)base.Changes; + private CompositionDrawList? _drawList; + public CompositionDrawList? DrawList + { + get => _drawList; + set + { + _drawList?.Dispose(); + _drawList = value; + Changes.DrawCommands = value?.Clone(); + } + } + + private protected override IChangeSetPool ChangeSetPool => DrawListVisualChanges.Pool; + + internal CompositionDrawListVisual(Compositor compositor, ServerCompositionContainerVisual server, Visual visual) : base(compositor, server) + { + Visual = visual; + } + + internal override bool HitTest(Vector2 point) + { + if (DrawList == null) + return false; + var pt = new Point(point.X, point.Y); + if (Visual is ICustomHitTest custom) + return custom.HitTest(pt); + foreach (var op in DrawList) + if (op.Item.HitTest(pt)) + return true; + return false; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs b/src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs new file mode 100644 index 0000000000..73db243e93 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs @@ -0,0 +1,97 @@ +using System; +using System.Numerics; +using Avalonia.Rendering.Composition.Transport; +using Avalonia.Rendering.Composition.Utils; + +namespace Avalonia.Rendering.Composition +{ + public abstract class CompositionEasingFunction : CompositionObject + { + internal CompositionEasingFunction(Compositor compositor) : base(compositor, null!) + { + } + + private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException(); + + internal abstract IEasingFunction Snapshot(); + } + + internal interface IEasingFunction + { + float Ease(float progress); + } + + public sealed class DelegateCompositionEasingFunction : CompositionEasingFunction + { + private readonly Easing _func; + + public delegate float EasingDelegate(float progress); + + internal DelegateCompositionEasingFunction(Compositor compositor, EasingDelegate func) : base(compositor) + { + _func = new Easing(func); + } + + class Easing : IEasingFunction + { + private readonly EasingDelegate _func; + + public Easing(EasingDelegate func) + { + _func = func; + } + + public float Ease(float progress) => _func(progress); + } + + internal override IEasingFunction Snapshot() => _func; + } + + public class LinearEasingFunction : CompositionEasingFunction + { + public LinearEasingFunction(Compositor compositor) : base(compositor) + { + } + + class Linear : IEasingFunction + { + public float Ease(float progress) => progress; + } + + private static readonly Linear Instance = new Linear(); + internal override IEasingFunction Snapshot() => Instance; + } + + public class CubicBezierEasingFunction : CompositionEasingFunction + { + private CubicBezier _bezier; + public Vector2 ControlPoint1 { get; } + public Vector2 ControlPoint2 { get; } + //cubic-bezier(0.25, 0.1, 0.25, 1.0) + internal CubicBezierEasingFunction(Compositor compositor, Vector2 controlPoint1, Vector2 controlPoint2) : base(compositor) + { + ControlPoint1 = controlPoint1; + ControlPoint2 = controlPoint2; + if (controlPoint1.X < 0 || controlPoint1.X > 1 || controlPoint2.X < 0 || controlPoint2.X > 1) + throw new ArgumentException(); + _bezier = new CubicBezier(controlPoint1.X, controlPoint1.Y, controlPoint2.X, controlPoint2.Y); + } + + class EasingFunction : IEasingFunction + { + private readonly CubicBezier _bezier; + + public EasingFunction(CubicBezier bezier) + { + _bezier = bezier; + } + + public float Ease(float progress) => (float)_bezier.Solve(progress); + } + + internal static IEasingFunction Ease { get; } = new EasingFunction(new CubicBezier(0.25, 0.1, 0.25, 1)); + + internal override IEasingFunction Snapshot() => new EasingFunction(_bezier); + } + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionGradientBrush.cs b/src/Avalonia.Base/Rendering/Composition/CompositionGradientBrush.cs new file mode 100644 index 0000000000..cf222550dd --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionGradientBrush.cs @@ -0,0 +1,16 @@ +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition +{ + public partial class CompositionGradientBrush : CompositionBrush + { + internal CompositionGradientBrush(Compositor compositor, ServerCompositionGradientBrush server) : base(compositor, server) + { + ColorStops = new CompositionGradientStopCollection(compositor, server.Stops); + } + + public CompositionGradientStopCollection ColorStops { get; } + } + + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs new file mode 100644 index 0000000000..2417ecaba8 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs @@ -0,0 +1,125 @@ +using System; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition +{ + public abstract class CompositionObject : IDisposable, IExpressionObject + { + public ImplicitAnimationCollection? ImplicitAnimations { get; set; } + internal CompositionObject(Compositor compositor, ServerObject server) + { + Compositor = compositor; + Server = server; + } + + public Compositor Compositor { get; } + internal ServerObject Server { get; } + public bool IsDisposed { get; private set; } + private ChangeSet? _changes; + + private static void ThrowInvalidOperation() => + throw new InvalidOperationException("There is no server-side counterpart for this object"); + + private protected ChangeSet Changes + { + get + { + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + if (Server == null) ThrowInvalidOperation(); + var currentBatch = Compositor.CurrentBatch; + if (_changes != null && _changes.Batch != currentBatch) + _changes = null; + if (_changes == null) + { + _changes = ChangeSetPool.Get(Server!, currentBatch); + currentBatch.Changes!.Add(_changes); + Compositor.QueueImplicitBatchCommit(); + } + + return _changes; + } + } + + private protected abstract IChangeSetPool ChangeSetPool { get; } + + public void Dispose() + { + Changes.Dispose = true; + IsDisposed = true; + } + + internal virtual ExpressionVariant GetPropertyForAnimation(string name) + { + return default; + } + + ExpressionVariant IExpressionObject.GetProperty(string name) => GetPropertyForAnimation(name); + + public void StartAnimation(string propertyName, CompositionAnimation animation) + => StartAnimation(propertyName, animation, null); + + internal virtual void StartAnimation(string propertyName, CompositionAnimation animation, ExpressionVariant? finalValue = null) + { + throw new ArgumentException("Unknown property " + propertyName); + } + + public void StartAnimationGroup(ICompositionAnimationBase grp) + { + if (grp is CompositionAnimation animation) + { + if(animation.Target == null) + throw new ArgumentException("Animation Target can't be null"); + StartAnimation(animation.Target, animation); + } + else if (grp is CompositionAnimationGroup group) + { + foreach (var a in group.Animations) + { + if (a.Target == null) + throw new ArgumentException("Animation Target can't be null"); + StartAnimation(a.Target, a); + } + } + } + + bool StartAnimationGroupPart(CompositionAnimation animation, string target, ExpressionVariant finalValue) + { + if(animation.Target == null) + throw new ArgumentException("Animation Target can't be null"); + if (animation.Target == target) + { + StartAnimation(animation.Target, animation, finalValue); + return true; + } + else + { + StartAnimation(animation.Target, animation); + return false; + } + } + + internal bool StartAnimationGroup(ICompositionAnimationBase grp, string target, ExpressionVariant finalValue) + { + if (grp is CompositionAnimation animation) + return StartAnimationGroupPart(animation, target, finalValue); + if (grp is CompositionAnimationGroup group) + { + var matched = false; + foreach (var a in group.Animations) + { + if (a.Target == null) + throw new ArgumentException("Animation Target can't be null"); + if (StartAnimationGroupPart(a, target, finalValue)) + matched = true; + } + + return matched; + } + + throw new ArgumentException(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs new file mode 100644 index 0000000000..004c2676ff --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition +{ + public class CompositionPropertySet : CompositionObject + { + private readonly Dictionary _variants = new Dictionary(); + private readonly Dictionary _objects = new Dictionary(); + + internal CompositionPropertySet(Compositor compositor) : base(compositor, null!) + { + } + + private protected override IChangeSetPool ChangeSetPool => throw new NotSupportedException(); + + internal void Set(string key, ExpressionVariant value) + { + _objects.Remove(key); + _variants[key] = value; + } + + internal void Set(string key, CompositionObject obj) + { + _objects[key] = obj ?? throw new ArgumentNullException(nameof(obj)); + _variants.Remove(key); + } + public void InsertColor(string propertyName, Avalonia.Media.Color value) => Set(propertyName, value); + + public void InsertMatrix3x2(string propertyName, Matrix3x2 value) => Set(propertyName, value); + + public void InsertMatrix4x4(string propertyName, Matrix4x4 value) => Set(propertyName, value); + + public void InsertQuaternion(string propertyName, Quaternion value) => Set(propertyName, value); + + public void InsertScalar(string propertyName, float value) => Set(propertyName, value); + public void InsertVector2(string propertyName, Vector2 value) => Set(propertyName, value); + + public void InsertVector3(string propertyName, Vector3 value) => Set(propertyName, value); + + public void InsertVector4(string propertyName, Vector4 value) => Set(propertyName, value); + + + CompositionGetValueStatus TryGetVariant(string key, out T value) where T : struct + { + value = default; + if (!_variants.TryGetValue(key, out var v)) + return _objects.ContainsKey(key) + ? CompositionGetValueStatus.TypeMismatch + : CompositionGetValueStatus.NotFound; + + return v.TryCast(out value) ? CompositionGetValueStatus.Succeeded : CompositionGetValueStatus.TypeMismatch; + } + + public CompositionGetValueStatus TryGetColor(string propertyName, out Avalonia.Media.Color value) + => TryGetVariant(propertyName, out value); + + public CompositionGetValueStatus TryGetMatrix3x2(string propertyName, out Matrix3x2 value) + => TryGetVariant(propertyName, out value); + + public CompositionGetValueStatus TryGetMatrix4x4(string propertyName, out Matrix4x4 value) + => TryGetVariant(propertyName, out value); + + public CompositionGetValueStatus TryGetQuaternion(string propertyName, out Quaternion value) + => TryGetVariant(propertyName, out value); + + + public CompositionGetValueStatus TryGetScalar(string propertyName, out float value) + => TryGetVariant(propertyName, out value); + + public CompositionGetValueStatus TryGetVector2(string propertyName, out Vector2 value) + => TryGetVariant(propertyName, out value); + + public CompositionGetValueStatus TryGetVector3(string propertyName, out Vector3 value) + => TryGetVariant(propertyName, out value); + + public CompositionGetValueStatus TryGetVector4(string propertyName, out Vector4 value) + => TryGetVariant(propertyName, out value); + + + public void InsertBoolean(string propertyName, bool value) => Set(propertyName, value); + + public CompositionGetValueStatus TryGetBoolean(string propertyName, out bool value) + => TryGetVariant(propertyName, out value); + + internal void ClearAll() + { + _objects.Clear(); + _variants.Clear(); + } + + internal void Clear(string key) + { + _objects.Remove(key); + _variants.Remove(key); + } + + internal PropertySetSnapshot Snapshot(bool server, int allowedNestingLevel) + { + var dic = new Dictionary(_objects.Count + _variants.Count); + foreach (var o in _objects) + { + if (o.Value is CompositionPropertySet ps) + { + if (allowedNestingLevel <= 0) + throw new InvalidOperationException("PropertySet depth limit reached"); + dic[o.Key] = new PropertySetSnapshot.Value(ps.Snapshot(server, allowedNestingLevel - 1)); + } + else if (o.Value.Server == null) + throw new InvalidOperationException($"Object of type {o.Value.GetType()} is not allowed"); + else + dic[o.Key] = new PropertySetSnapshot.Value(server ? (IExpressionObject) o.Value.Server : o.Value); + } + + foreach (var v in _variants) + dic[v.Key] = v.Value; + + return new PropertySetSnapshot(dic); + } + } + + public enum CompositionGetValueStatus + { + Succeeded, + TypeMismatch, + NotFound + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs new file mode 100644 index 0000000000..a8835ca668 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs @@ -0,0 +1,107 @@ +using System.Collections.Generic; +using System.Numerics; +using Avalonia.Collections.Pooled; + +namespace Avalonia.Rendering.Composition +{ + public partial class CompositionTarget + { + partial void OnRootChanged() + { + if (Root != null) + Root.Root = this; + } + + partial void OnRootChanging() + { + if (Root != null) + Root.Root = null; + } + + public PooledList? TryHitTest(Vector2 point) + { + Server.Readback.NextRead(); + if (Root == null) + return null; + var res = new PooledList(); + HitTestCore(Root, point, res); + return res; + } + + public Vector2? TryTransformToVisual(CompositionVisual visual, Vector2 point) + { + if (visual.Root != this) + return null; + var v = visual; + var m = Matrix3x2.Identity; + while (v != null) + { + if (!TryGetInvertedTransform(v, out var cm)) + return null; + m = m * cm; + v = v.Parent; + } + + return Vector2.Transform(point, m); + } + + bool TryGetInvertedTransform(CompositionVisual visual, out Matrix3x2 matrix) + { + var m = visual.TryGetServerTransform(); + if (m == null) + { + matrix = default; + return false; + } + + // TODO: Use Matrix3x3 + var m32 = new Matrix3x2(m.Value.M11, m.Value.M12, m.Value.M21, m.Value.M22, m.Value.M41, m.Value.M42); + + return Matrix3x2.Invert(m32, out matrix); + } + + bool TryTransformTo(CompositionVisual visual, ref Vector2 v) + { + if (TryGetInvertedTransform(visual, out var m)) + { + v = Vector2.Transform(v, m); + return true; + } + + return false; + } + + bool HitTestCore(CompositionVisual visual, Vector2 point, PooledList result) + { + //TODO: Check readback too + if (visual.Visible == false) + return false; + if (!TryTransformTo(visual, ref point)) + return false; + if (point.X >= 0 && point.Y >= 0 && point.X <= visual.Size.X && point.Y <= visual.Size.Y) + { + bool success = false; + // Hit-test the current node + if (visual.HitTest(point)) + { + result.Add(visual); + success = true; + } + + // Inspect children too + if(visual is CompositionContainerVisual cv) + for (var c = cv.Children.Count - 1; c >= 0; c--) + { + var ch = cv.Children[c]; + var hit = HitTestCore(ch, point, result); + if (hit) + return true; + } + + return success; + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs new file mode 100644 index 0000000000..217d8dd803 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Numerics; +using System.Threading.Tasks; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; +using Avalonia.Threading; + + +namespace Avalonia.Rendering.Composition +{ + public partial class Compositor + { + private ServerCompositor _server; + private Batch _currentBatch; + private bool _implicitBatchCommitQueued; + private Action _implicitBatchCommit; + + internal Batch CurrentBatch => _currentBatch; + internal ServerCompositor Server => _server; + internal CompositionEasingFunction DefaultEasing { get; } + + private Compositor(ServerCompositor server) + { + _server = server; + _currentBatch = new Batch(); + _implicitBatchCommit = ImplicitBatchCommit; + DefaultEasing = new CubicBezierEasingFunction(this, + new Vector2(0.25f, 0.1f), new Vector2(0.25f, 1f)); + } + + public CompositionTarget CreateCompositionTarget(Func renderTargetFactory) + { + return new CompositionTarget(this, new ServerCompositionTarget(_server, renderTargetFactory)); + } + + public Task RequestCommitAsync() + { + var batch = CurrentBatch; + _currentBatch = new Batch(); + batch.CommitedAt = Server.Clock.Elapsed; + _server.EnqueueBatch(batch); + return batch.Completed; + } + + public static Compositor Create(IRenderLoop timer) + { + return new Compositor(new ServerCompositor(timer)); + } + + public void Dispose() + { + + } + + public CompositionContainerVisual CreateContainerVisual() => new(this, new ServerCompositionContainerVisual(_server)); + + public CompositionSolidColorVisual CreateSolidColorVisual() => new CompositionSolidColorVisual(this, + new ServerCompositionSolidColorVisual(_server)); + + public CompositionSolidColorVisual CreateSolidColorVisual(Avalonia.Media.Color color) + { + var v = new CompositionSolidColorVisual(this, new ServerCompositionSolidColorVisual(_server)); + v.Color = color; + return v; + } + + public CompositionSpriteVisual CreateSpriteVisual() => new CompositionSpriteVisual(this, new ServerCompositionSpriteVisual(_server)); + + public CompositionLinearGradientBrush CreateLinearGradientBrush() + => new CompositionLinearGradientBrush(this, new ServerCompositionLinearGradientBrush(_server)); + + public CompositionColorGradientStop CreateColorGradientStop() + => new CompositionColorGradientStop(this, new ServerCompositionColorGradientStop(_server)); + + public CompositionColorGradientStop CreateColorGradientStop(float offset, Avalonia.Media.Color color) + { + var stop = CreateColorGradientStop(); + stop.Offset = offset; + stop.Color = color; + return stop; + } + + // We want to make it 100% async later + /* + public CompositionBitmapSurface LoadBitmapSurface(Stream stream) + { + var bmp = _server.Backend.LoadCpuMemoryBitmap(stream); + return new CompositionBitmapSurface(this, bmp); + } + + public async Task LoadBitmapSurfaceAsync(Stream stream) + { + var bmp = await Task.Run(() => _server.Backend.LoadCpuMemoryBitmap(stream)); + return new CompositionBitmapSurface(this, bmp); + } + */ + public CompositionColorBrush CreateColorBrush(Avalonia.Media.Color color) => + new CompositionColorBrush(this, new ServerCompositionColorBrush(_server)) {Color = color}; + + public CompositionSurfaceBrush CreateSurfaceBrush() => + new CompositionSurfaceBrush(this, new ServerCompositionSurfaceBrush(_server)); + + /* + public CompositionGaussianBlurEffectBrush CreateGaussianBlurEffectBrush() => + new CompositionGaussianBlurEffectBrush(this, new ServerCompositionGaussianBlurEffectBrush(_server)); + + public CompositionBackdropBrush CreateBackdropBrush() => + new CompositionBackdropBrush(this, new ServerCompositionBackdropBrush(Server));*/ + + public ExpressionAnimation CreateExpressionAnimation() => new ExpressionAnimation(this); + + public ExpressionAnimation CreateExpressionAnimation(string expression) => new ExpressionAnimation(this) + { + Expression = expression + }; + + public ImplicitAnimationCollection CreateImplicitAnimationCollection() => new ImplicitAnimationCollection(this); + + public CompositionAnimationGroup CreateAnimationGroup() => new CompositionAnimationGroup(this); + + internal CustomDrawVisual CreateCustomDrawVisual(ICustomDrawVisualRenderer renderer, + ICustomDrawVisualHitTest? hitTest = null) where T : IEquatable => + new CustomDrawVisual(this, renderer, hitTest); + + public void QueueImplicitBatchCommit() + { + if(_implicitBatchCommitQueued) + return; + _implicitBatchCommitQueued = true; + Dispatcher.UIThread.Post(_implicitBatchCommit, DispatcherPriority.CompositionBatch); + } + + private void ImplicitBatchCommit() + { + _implicitBatchCommitQueued = false; + RequestCommitAsync(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositorRenderLoopTask.cs b/src/Avalonia.Base/Rendering/Composition/CompositorRenderLoopTask.cs new file mode 100644 index 0000000000..074c0a9ccf --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositorRenderLoopTask.cs @@ -0,0 +1,20 @@ +using System; + +namespace Avalonia.Rendering.Composition; + +partial class Compositor +{ + class CompositorRenderLoopTask : IRenderLoopTask + { + public bool NeedsUpdate { get; } + public void Update(TimeSpan time) + { + throw new NotImplementedException(); + } + + public void Render() + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs new file mode 100644 index 0000000000..f650d3e995 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs @@ -0,0 +1,20 @@ +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition +{ + public class CompositionContainerVisual : CompositionVisual + { + public CompositionVisualCollection Children { get; } + internal CompositionContainerVisual(Compositor compositor, ServerCompositionContainerVisual server) : base(compositor, server) + { + Children = new CompositionVisualCollection(this, server.Children); + } + + private protected override void OnRootChanged() + { + foreach (var ch in Children) + ch.Root = Root; + base.OnRootChanged(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs b/src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs new file mode 100644 index 0000000000..0505d6a46c --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs @@ -0,0 +1,56 @@ +using System; +using System.Numerics; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition +{ + internal class CustomDrawVisual : CompositionContainerVisual where TData : IEquatable + { + private readonly ICustomDrawVisualHitTest? _hitTest; + + internal CustomDrawVisual(Compositor compositor, ICustomDrawVisualRenderer renderer, + ICustomDrawVisualHitTest? hitTest) : base(compositor, + new ServerCustomDrawVisual(compositor.Server, renderer)) + { + _hitTest = hitTest; + } + + private TData? _data; + + static bool Eq(TData? left, TData? right) + { + if (left == null && right == null) + return true; + if (left == null) + return false; + return left.Equals(right); + } + + public TData? Data + { + get => _data; + set + { + if (!Eq(_data, value)) + { + ((CustomDrawVisualChanges) Changes).Data.Value = value; + _data = value; + } + } + } + + private protected override IChangeSetPool ChangeSetPool => CustomDrawVisualChanges.Pool; + } + + public interface ICustomDrawVisualRenderer + { + void Render(IDrawingContextImpl canvas, TData? data); + } + + public interface ICustomDrawVisualHitTest + { + bool HitTest(TData data, Vector2 vector2); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs new file mode 100644 index 0000000000..aca8ef7c46 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs @@ -0,0 +1,81 @@ +using System; +using Avalonia.Collections.Pooled; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Drawing; + +internal class CompositionDrawList : PooledList> +{ + public CompositionDrawList() + { + + } + + public CompositionDrawList(int capacity) : base(capacity) + { + + } + + public override void Dispose() + { + foreach(var item in this) + item.Dispose(); + base.Dispose(); + } + + public CompositionDrawList Clone() + { + var clone = new CompositionDrawList(Count); + foreach (var r in this) + clone.Add(r.Clone()); + return clone; + } +} + +internal class CompositionDrawListBuilder +{ + private CompositionDrawList? _operations; + private bool _owns; + + public void Reset(CompositionDrawList? previousOperations) + { + _operations = previousOperations; + _owns = false; + } + + public CompositionDrawList DrawOperations => _operations ?? new CompositionDrawList(); + + void MakeWritable(int atIndex) + { + if(_owns) + return; + _owns = true; + var newOps = new CompositionDrawList(_operations?.Count ?? Math.Max(1, atIndex)); + if (_operations != null) + { + for (var c = 0; c < atIndex; c++) + newOps.Add(_operations[c].Clone()); + } + + _operations = newOps; + } + + public void ReplaceDrawOperation(int index, IDrawOperation node) + { + MakeWritable(index); + DrawOperations.Add(RefCountable.Create(node)); + } + + public void AddDrawOperation(IDrawOperation node) + { + MakeWritable(DrawOperations.Count); + DrawOperations.Add(RefCountable.Create(node)); + } + + public void TrimTo(int count) + { + if (count < DrawOperations.Count) + DrawOperations.RemoveRange(count, DrawOperations.Count - count); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs new file mode 100644 index 0000000000..c8e5d9e064 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs @@ -0,0 +1,383 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; +using Avalonia.VisualTree; +namespace Avalonia.Rendering.Composition; + +internal class CompositionDrawingContext : IDrawingContextImpl +{ + private CompositionDrawListBuilder _builder = new(); + private int _drawOperationIndex; + + /// + public Matrix Transform { get; set; } = Matrix.Identity; + + /// + public void Clear(Color color) + { + // Cannot clear a deferred scene. + } + + /// + public void Dispose() + { + // Nothing to do here since we allocate no unmanaged resources. + } + + public void BeginUpdate(CompositionDrawList list) + { + _builder.Reset(list); + _drawOperationIndex = 0; + } + + public CompositionDrawList EndUpdate() + { + _builder.TrimTo(_drawOperationIndex); + return _builder.DrawOperations!; + } + + /// + public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, brush, pen, geometry)) + { + Add(new GeometryNode(Transform, brush, pen, geometry, CreateChildScene(brush))); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode) + { + var next = NextDrawAs(); + + if (next == null || + !next.Item.Equals(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode)) + { + Add(new ImageNode(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode)); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect) + { + // This method is currently only used to composite layers so shouldn't be called here. + throw new NotSupportedException(); + } + + /// + public void DrawLine(IPen pen, Point p1, Point p2) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, pen, p1, p2)) + { + Add(new LineNode(Transform, pen, p1, p2, CreateChildScene(pen.Brush))); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, + BoxShadows boxShadows = default) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, brush, pen, rect, boxShadows)) + { + Add(new RectangleNode(Transform, brush, pen, rect, boxShadows, CreateChildScene(brush))); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, material, rect)) + { + Add(new ExperimentalAcrylicNode(Transform, material, rect)); + } + else + { + ++_drawOperationIndex; + } + } + + public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, brush, pen, rect)) + { + Add(new EllipseNode(Transform, brush, pen, rect, CreateChildScene(brush))); + } + else + { + ++_drawOperationIndex; + } + } + + public void Custom(ICustomDrawOperation custom) + { + var next = NextDrawAs(); + if (next == null || !next.Item.Equals(Transform, custom)) + Add(new CustomDrawOperation(custom, Transform)); + else + ++_drawOperationIndex; + } + + /// + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, foreground, glyphRun)) + { + Add(new GlyphRunNode(Transform, foreground, glyphRun, CreateChildScene(foreground))); + } + + else + { + ++_drawOperationIndex; + } + } + + public IDrawingContextLayerImpl CreateLayer(Size size) + { + throw new NotSupportedException("Creating layers on a deferred drawing context not supported"); + } + + /// + public void PopClip() + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(null)) + { + Add(new ClipNode()); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PopGeometryClip() + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(null)) + { + Add(new GeometryClipNode()); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PopBitmapBlendMode() + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(null)) + { + Add(new BitmapBlendModeNode()); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PopOpacity() + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(null)) + { + Add(new OpacityNode()); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PopOpacityMask() + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(null, null)) + { + Add(new OpacityMaskNode()); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PushClip(Rect clip) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, clip)) + { + Add(new ClipNode(Transform, clip)); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PushClip(RoundedRect clip) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, clip)) + { + Add(new ClipNode(Transform, clip)); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PushGeometryClip(IGeometryImpl? clip) + { + if (clip is null) + return; + + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, clip)) + { + Add(new GeometryClipNode(Transform, clip)); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PushOpacity(double opacity) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(opacity)) + { + Add(new OpacityNode(opacity)); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PushOpacityMask(IBrush mask, Rect bounds) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(mask, bounds)) + { + Add(new OpacityMaskNode(mask, bounds, CreateChildScene(mask))); + } + else + { + ++_drawOperationIndex; + } + } + + /// + public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(blendingMode)) + { + Add(new BitmapBlendModeNode(blendingMode)); + } + else + { + ++_drawOperationIndex; + } + } + + private void Add(T node) where T : class, IDrawOperation + { + if (_drawOperationIndex < _builder!.DrawOperations.Count) + { + _builder.ReplaceDrawOperation(_drawOperationIndex, node); + } + else + { + _builder.AddDrawOperation(node); + } + + ++_drawOperationIndex; + } + + private IRef? NextDrawAs() where T : class, IDrawOperation + { + return _drawOperationIndex < _builder!.DrawOperations.Count + ? _builder.DrawOperations[_drawOperationIndex] as IRef + : null; + } + + private IDictionary? CreateChildScene(IBrush? brush) + { + /* + var visualBrush = brush as VisualBrush; + + if (visualBrush != null) + { + var visual = visualBrush.Visual; + + if (visual != null) + { + (visual as IVisualBrushInitialize)?.EnsureInitialized(); + var scene = new Scene(visual); + _sceneBuilder.UpdateAll(scene); + return new Dictionary { { visualBrush.Visual, scene } }; + } + }*/ + + return null; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Enums.cs b/src/Avalonia.Base/Rendering/Composition/Enums.cs new file mode 100644 index 0000000000..e349845cbf --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Enums.cs @@ -0,0 +1,120 @@ +using System; + +namespace Avalonia.Rendering.Composition +{ + public enum CompositionBlendMode + { + /// No regions are enabled. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_clr.svg) + Clear, + + /// Only the source will be present. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src.svg) + Src, + + /// Only the destination will be present. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst.svg) + Dst, + + /// Source is placed over the destination. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src-over.svg) + SrcOver, + + /// Destination is placed over the source. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst-over.svg) + DstOver, + + /// The source that overlaps the destination, replaces the destination. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src-in.svg) + SrcIn, + + /// Destination which overlaps the source, replaces the source. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst-in.svg) + DstIn, + + /// Source is placed, where it falls outside of the destination. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src-out.svg) + SrcOut, + + /// Destination is placed, where it falls outside of the source. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst-out.svg) + DstOut, + + /// Source which overlaps the destination, replaces the destination. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src-atop.svg) + SrcATop, + + /// Destination which overlaps the source replaces the source. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst-atop.svg) + DstATop, + + /// The non-overlapping regions of source and destination are combined. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_xor.svg) + Xor, + + /// Display the sum of the source image and destination image. [Porter Duff Compositing Operators] + Plus, + + /// Multiplies all components (= alpha and color). [Separable Blend Modes] + Modulate, + + /// Multiplies the complements of the backdrop and source CompositionColorvalues, then complements the result. [Separable Blend Modes] + Screen, + + /// Multiplies or screens the colors, depending on the backdrop CompositionColorvalue. [Separable Blend Modes] + Overlay, + + /// Selects the darker of the backdrop and source colors. [Separable Blend Modes] + Darken, + + /// Selects the lighter of the backdrop and source colors. [Separable Blend Modes] + Lighten, + + /// Brightens the backdrop CompositionColorto reflect the source color. [Separable Blend Modes] + ColorDodge, + + /// Darkens the backdrop CompositionColorto reflect the source color. [Separable Blend Modes] + ColorBurn, + + /// Multiplies or screens the colors, depending on the source CompositionColorvalue. [Separable Blend Modes] + HardLight, + + /// Darkens or lightens the colors, depending on the source CompositionColorvalue. [Separable Blend Modes] + SoftLight, + + /// Subtracts the darker of the two constituent colors from the lighter color. [Separable Blend Modes] + Difference, + + /// Produces an effect similar to that of the Difference mode but lower in contrast. [Separable Blend Modes] + Exclusion, + + /// The source CompositionColoris multiplied by the destination CompositionColorand replaces the destination [Separable Blend Modes] + Multiply, + + /// Creates a CompositionColorwith the hue of the source CompositionColorand the saturation and luminosity of the backdrop color. [Non-Separable Blend Modes] + Hue, + + /// Creates a CompositionColorwith the saturation of the source CompositionColorand the hue and luminosity of the backdrop color. [Non-Separable Blend Modes] + Saturation, + + /// Creates a CompositionColorwith the hue and saturation of the source CompositionColorand the luminosity of the backdrop color. [Non-Separable Blend Modes] + Color, + + /// Creates a CompositionColorwith the luminosity of the source CompositionColorand the hue and saturation of the backdrop color. [Non-Separable Blend Modes] + Luminosity, + } + + public enum CompositionGradientExtendMode + { + Clamp, + Wrap, + Mirror + } + + [Flags] + public enum CompositionTileMode + { + None = 0, + TileX = 1, + TileY = 2, + FlipX = 4, + FlipY = 8, + Tile = TileX | TileY, + Flip = FlipX | FlipY + } + + public enum CompositionStretch + { + None = 0, + Fill = 1, + //TODO: Uniform, UniformToFill + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs new file mode 100644 index 0000000000..db9a26e301 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs @@ -0,0 +1,234 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Utils; + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal class BuiltInExpressionFfi : IExpressionForeignFunctionInterface + { + private readonly DelegateExpressionFfi _registry; + + static float Lerp(float a, float b, float p) => p * (b - a) + a; + + static Matrix3x2 Inverse(Matrix3x2 m) + { + Matrix3x2.Invert(m, out var r); + return r; + } + + static Matrix4x4 Inverse(Matrix4x4 m) + { + Matrix4x4.Invert(m, out var r); + return r; + } + + static float SmoothStep(float edge0, float edge1, float x) + { + var t = MathExt.Clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); + return t * t * (3.0f - 2.0f * t); + } + + static Vector2 SmoothStep(Vector2 edge0, Vector2 edge1, Vector2 x) + { + return new Vector2( + SmoothStep(edge0.X, edge1.X, x.X), + SmoothStep(edge0.Y, edge1.Y, x.Y) + + ); + } + static Vector3 SmoothStep(Vector3 edge0, Vector3 edge1, Vector3 x) + { + return new Vector3( + SmoothStep(edge0.X, edge1.X, x.X), + SmoothStep(edge0.Y, edge1.Y, x.Y), + SmoothStep(edge0.Z, edge1.Z, x.Z) + + ); + } + + static Vector4 SmoothStep(Vector4 edge0, Vector4 edge1, Vector4 x) + { + return new Vector4( + SmoothStep(edge0.X, edge1.X, x.X), + SmoothStep(edge0.Y, edge1.Y, x.Y), + SmoothStep(edge0.Z, edge1.Z, x.Z), + SmoothStep(edge0.W, edge1.W, x.W) + ); + } + + private BuiltInExpressionFfi() + { + _registry = new DelegateExpressionFfi + { + {"Abs", (float f) => Math.Abs(f)}, + {"Abs", (Vector2 v) => Vector2.Abs(v)}, + {"Abs", (Vector3 v) => Vector3.Abs(v)}, + {"Abs", (Vector4 v) => Vector4.Abs(v)}, + + {"ACos", (float f) => (float) Math.Acos(f)}, + {"ASin", (float f) => (float) Math.Asin(f)}, + {"ATan", (float f) => (float) Math.Atan(f)}, + {"Ceil", (float f) => (float) Math.Ceiling(f)}, + + {"Clamp", (float a1, float a2, float a3) => MathExt.Clamp(a1, a2, a3)}, + {"Clamp", (Vector2 a1, Vector2 a2, Vector2 a3) => Vector2.Clamp(a1, a2, a3)}, + {"Clamp", (Vector3 a1, Vector3 a2, Vector3 a3) => Vector3.Clamp(a1, a2, a3)}, + {"Clamp", (Vector4 a1, Vector4 a2, Vector4 a3) => Vector4.Clamp(a1, a2, a3)}, + + {"Concatenate", (Quaternion a1, Quaternion a2) => Quaternion.Concatenate(a1, a2)}, + {"Cos", (float a) => (float) Math.Cos(a)}, + + /* + TODO: + ColorHsl(Float h, Float s, Float l) + ColorLerpHSL(Color colorTo, CompositionColorcolorFrom, Float progress) + */ + + { + "ColorLerp", (Avalonia.Media.Color to, Avalonia.Media.Color from, float progress) => + ColorInterpolator.LerpRGB(to, from, progress) + }, + { + "ColorLerpRGB", (Avalonia.Media.Color to, Avalonia.Media.Color from, float progress) => + ColorInterpolator.LerpRGB(to, from, progress) + }, + { + "ColorRGB", (float a, float r, float g, float b) => Avalonia.Media.Color.FromArgb( + (byte) MathExt.Clamp(a, 0, 255), + (byte) MathExt.Clamp(r, 0, 255), + (byte) MathExt.Clamp(g, 0, 255), + (byte) MathExt.Clamp(b, 0, 255) + ) + }, + + {"Distance", (Vector2 a1, Vector2 a2) => Vector2.Distance(a1, a2)}, + {"Distance", (Vector3 a1, Vector3 a2) => Vector3.Distance(a1, a2)}, + {"Distance", (Vector4 a1, Vector4 a2) => Vector4.Distance(a1, a2)}, + + {"DistanceSquared", (Vector2 a1, Vector2 a2) => Vector2.DistanceSquared(a1, a2)}, + {"DistanceSquared", (Vector3 a1, Vector3 a2) => Vector3.DistanceSquared(a1, a2)}, + {"DistanceSquared", (Vector4 a1, Vector4 a2) => Vector4.DistanceSquared(a1, a2)}, + + {"Floor", (float v) => (float) Math.Floor(v)}, + + {"Inverse", (Matrix3x2 v) => Inverse(v)}, + {"Inverse", (Matrix4x4 v) => Inverse(v)}, + + + {"Length", (Vector2 a1) => a1.Length()}, + {"Length", (Vector3 a1) => a1.Length()}, + {"Length", (Vector4 a1) => a1.Length()}, + {"Length", (Quaternion a1) => a1.Length()}, + + {"LengthSquared", (Vector2 a1) => a1.LengthSquared()}, + {"LengthSquared", (Vector3 a1) => a1.LengthSquared()}, + {"LengthSquared", (Vector4 a1) => a1.LengthSquared()}, + {"LengthSquared", (Quaternion a1) => a1.LengthSquared()}, + + {"Lerp", (float a1, float a2, float a3) => Lerp(a1, a2, a3)}, + {"Lerp", (Vector2 a1, Vector2 a2, float a3) => Vector2.Lerp(a1, a2, a3)}, + {"Lerp", (Vector3 a1, Vector3 a2, float a3) => Vector3.Lerp(a1, a2, a3)}, + {"Lerp", (Vector4 a1, Vector4 a2, float a3) => Vector4.Lerp(a1, a2, a3)}, + + + {"Ln", (float f) => (float) Math.Log(f)}, + {"Log10", (float f) => (float) Math.Log10(f)}, + + {"Matrix3x2.CreateFromScale", (Vector2 v) => Matrix3x2.CreateScale(v)}, + {"Matrix3x2.CreateFromTranslation", (Vector2 v) => Matrix3x2.CreateTranslation(v)}, + {"Matrix3x2.CreateRotation", (float v) => Matrix3x2.CreateRotation(v)}, + {"Matrix3x2.CreateScale", (Vector2 v) => Matrix3x2.CreateScale(v)}, + {"Matrix3x2.CreateSkew", (float a1, float a2, Vector2 a3) => Matrix3x2.CreateSkew(a1, a2, a3)}, + {"Matrix3x2.CreateTranslation", (Vector2 v) => Matrix3x2.CreateScale(v)}, + { + "Matrix3x2", (float m11, float m12, float m21, float m22, float m31, float m32) => + new Matrix3x2(m11, m12, m21, m22, m31, m32) + }, + {"Matrix4x4.CreateFromAxisAngle", (Vector3 v, float angle) => Matrix4x4.CreateFromAxisAngle(v, angle)}, + {"Matrix4x4.CreateFromScale", (Vector3 v) => Matrix4x4.CreateScale(v)}, + {"Matrix4x4.CreateFromTranslation", (Vector3 v) => Matrix4x4.CreateTranslation(v)}, + {"Matrix4x4.CreateScale", (Vector3 v) => Matrix4x4.CreateScale(v)}, + {"Matrix4x4.CreateTranslation", (Vector3 v) => Matrix4x4.CreateScale(v)}, + {"Matrix4x4", (Matrix3x2 m) => new Matrix4x4(m)}, + { + "Matrix4x4", + (float m11, float m12, float m13, float m14, + float m21, float m22, float m23, float m24, + float m31, float m32, float m33, float m34, + float m41, float m42, float m43, float m44) => + new Matrix4x4( + m11, m12, m13, m14, + m21, m22, m23, m24, + m31, m32, m33, m34, + m41, m42, m43, m44) + }, + + + {"Max", (float a1, float a2) => Math.Max(a1, a2)}, + {"Max", (Vector2 a1, Vector2 a2) => Vector2.Max(a1, a2)}, + {"Max", (Vector3 a1, Vector3 a2) => Vector3.Max(a1, a2)}, + {"Max", (Vector4 a1, Vector4 a2) => Vector4.Max(a1, a2)}, + + + {"Min", (float a1, float a2) => Math.Min(a1, a2)}, + {"Min", (Vector2 a1, Vector2 a2) => Vector2.Min(a1, a2)}, + {"Min", (Vector3 a1, Vector3 a2) => Vector3.Min(a1, a2)}, + {"Min", (Vector4 a1, Vector4 a2) => Vector4.Min(a1, a2)}, + + {"Mod", (float a, float b) => a % b}, + + {"Normalize", (Quaternion a) => Quaternion.Normalize(a)}, + {"Normalize", (Vector2 a) => Vector2.Normalize(a)}, + {"Normalize", (Vector3 a) => Vector3.Normalize(a)}, + {"Normalize", (Vector4 a) => Vector4.Normalize(a)}, + + {"Pow", (float a, float b) => (float) Math.Pow(a, b)}, + {"Quaternion.CreateFromAxisAngle", (Vector3 a, float b) => Quaternion.CreateFromAxisAngle(a, b)}, + {"Quaternion", (float a, float b, float c, float d) => new Quaternion(a, b, c, d)}, + + {"Round", (float a) => (float) Math.Round(a)}, + + {"Scale", (Matrix3x2 a, float b) => a * b}, + {"Scale", (Matrix4x4 a, float b) => a * b}, + {"Scale", (Vector2 a, float b) => a * b}, + {"Scale", (Vector3 a, float b) => a * b}, + {"Scale", (Vector4 a, float b) => a * b}, + + {"Sin", (float a) => (float) Math.Sin(a)}, + + {"SmoothStep", (float a1, float a2, float a3) => SmoothStep(a1, a2, a3)}, + {"SmoothStep", (Vector2 a1, Vector2 a2, Vector2 a3) => SmoothStep(a1, a2, a3)}, + {"SmoothStep", (Vector3 a1, Vector3 a2, Vector3 a3) => SmoothStep(a1, a2, a3)}, + {"SmoothStep", (Vector4 a1, Vector4 a2, Vector4 a3) => SmoothStep(a1, a2, a3)}, + + // I have no idea how to do a spherical interpolation for a scalar value, so we are doing a linear one + {"Slerp", (float a1, float a2, float a3) => Lerp(a1, a2, a3)}, + {"Slerp", (Quaternion a1, Quaternion a2, float a3) => Quaternion.Slerp(a1, a2, a3)}, + + {"Sqrt", (float a) => (float) Math.Sqrt(a)}, + {"Square", (float a) => a * a}, + {"Tan", (float a) => (float) Math.Tan(a)}, + + {"ToRadians", (float a) => (float) (a * Math.PI / 180)}, + {"ToDegrees", (float a) => (float) (a * 180d / Math.PI)}, + + {"Transform", (Vector2 a, Matrix3x2 b) => Vector2.Transform(a, b)}, + {"Transform", (Vector3 a, Matrix4x4 b) => Vector3.Transform(a, b)}, + + {"Vector2", (float a, float b) => new Vector2(a, b)}, + {"Vector3", (float a, float b, float c) => new Vector3(a, b, c)}, + {"Vector3", (Vector2 v2, float z) => new Vector3(v2, z)}, + {"Vector4", (float a, float b, float c, float d) => new Vector4(a, b, c, d)}, + {"Vector4", (Vector2 v2, float z, float w) => new Vector4(v2, z, w)}, + {"Vector4", (Vector3 v3, float w) => new Vector4(v3, w)}, + }; + } + + public bool Call(string name, IReadOnlyList arguments, out ExpressionVariant result) => + _registry.Call(name, arguments, out result); + + public static BuiltInExpressionFfi Instance { get; } = new BuiltInExpressionFfi(); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs new file mode 100644 index 0000000000..002cf37522 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Avalonia.Media; + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal class DelegateExpressionFfi : IExpressionForeignFunctionInterface, IEnumerable + { + struct FfiRecord + { + public VariantType[] Types; + public Func, ExpressionVariant> Delegate; + } + + private readonly Dictionary>> + _registry = new Dictionary>>(); + + public bool Call(string name, IReadOnlyList arguments, out ExpressionVariant result) + { + result = default; + if (!_registry.TryGetValue(name, out var nameGroup)) + return false; + if (!nameGroup.TryGetValue(arguments.Count, out var countGroup)) + return false; + foreach (var record in countGroup) + { + var match = true; + for (var c = 0; c < arguments.Count; c++) + { + if (record.Types[c] != arguments[c].Type) + { + match = false; + break; + } + } + + if (match) + { + result = record.Delegate(arguments); + return true; + } + } + + return false; + } + + // Stub for collection initializer + IEnumerator IEnumerable.GetEnumerator() => Array.Empty().GetEnumerator(); + + void Add(string name, Func, ExpressionVariant> cb, + params Type[] types) + { + if (!_registry.TryGetValue(name, out var nameGroup)) + _registry[name] = nameGroup = + new Dictionary>(); + if (!nameGroup.TryGetValue(types.Length, out var countGroup)) + nameGroup[types.Length] = countGroup = new List(); + + countGroup.Add(new FfiRecord + { + Types = types.Select(t => TypeMap[t]).ToArray(), + Delegate = cb + }); + } + + static readonly Dictionary TypeMap = new Dictionary + { + [typeof(bool)] = VariantType.Boolean, + [typeof(float)] = VariantType.Scalar, + [typeof(Vector2)] = VariantType.Vector2, + [typeof(Vector3)] = VariantType.Vector3, + [typeof(Vector4)] = VariantType.Vector4, + [typeof(Matrix3x2)] = VariantType.Matrix3x2, + [typeof(Matrix4x4)] = VariantType.Matrix4x4, + [typeof(Quaternion)] = VariantType.Quaternion, + [typeof(Color)] = VariantType.Color + }; + + public void Add(string name, Func cb) where T1 : struct + { + Add(name, args => cb(args[0].CastOrDefault()), typeof(T1)); + } + + public void Add(string name, Func cb) where T1 : struct where T2 : struct + { + Add(name, args => cb(args[0].CastOrDefault(), args[1].CastOrDefault()), typeof(T1), typeof(T2)); + } + + + public void Add(string name, Func cb) + where T1 : struct where T2 : struct where T3 : struct + { + Add(name, args => cb(args[0].CastOrDefault(), args[1].CastOrDefault(), args[2].CastOrDefault()), typeof(T1), typeof(T2), + typeof(T3)); + } + + public void Add(string name, Func cb) + where T1 : struct where T2 : struct where T3 : struct where T4 : struct + { + Add(name, args => cb( + args[0].CastOrDefault(), + args[1].CastOrDefault(), + args[2].CastOrDefault(), + args[3].CastOrDefault()), + typeof(T1), typeof(T2), typeof(T3), typeof(T4)); + } + + public void Add(string name, Func cb) + where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct + { + Add(name, args => cb( + args[0].CastOrDefault(), + args[1].CastOrDefault(), + args[2].CastOrDefault(), + args[3].CastOrDefault(), + args[4].CastOrDefault()), + typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5)); + } + + public void Add(string name, Func cb) + where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct where T6 : struct + { + Add(name, args => cb( + args[0].CastOrDefault(), + args[1].CastOrDefault(), + args[2].CastOrDefault(), + args[3].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault()), + typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6)); + } + + + public void Add(string name, + Func cb) + where T1 : struct + where T2 : struct + where T3 : struct + where T4 : struct + where T5 : struct + where T6 : struct + where T7 : struct + where T8 : struct + where T9 : struct + where T10 : struct + where T11 : struct + where T12 : struct + where T13 : struct + where T14 : struct + where T15 : struct + where T16 : struct + { + Add(name, args => cb( + args[0].CastOrDefault(), + args[1].CastOrDefault(), + args[2].CastOrDefault(), + args[3].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault() + ), + typeof(T1), typeof(T2), typeof(T3), typeof(T4), + typeof(T5), typeof(T6), typeof(T7), typeof(T8), + typeof(T9), typeof(T10), typeof(T11), typeof(T12), + typeof(T13), typeof(T14), typeof(T15), typeof(T16) + ); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs new file mode 100644 index 0000000000..5577d2b52a --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs @@ -0,0 +1,331 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal abstract class Expression + { + public abstract ExpressionType Type { get; } + public static Expression Parse(string expression) + { + return ExpressionParser.Parse(expression.AsSpan()); + } + + public abstract ExpressionVariant Evaluate(ref ExpressionEvaluationContext context); + + protected abstract string Print(); + public override string ToString() => Print(); + + internal static string OperatorName(ExpressionType t) + { + var attr = typeof(ExpressionType).GetMember(t.ToString())[0] + .GetCustomAttribute(); + if (attr != null) + return attr.Name; + return t.ToString(); + } + } + + internal class PrettyPrintStringAttribute : Attribute + { + public string Name { get; } + + public PrettyPrintStringAttribute(string name) + { + Name = name; + } + } + + internal enum ExpressionType + { + // Binary operators + [PrettyPrintString("+")] + Add, + [PrettyPrintString("-")] + Subtract, + [PrettyPrintString("/")] + Divide, + [PrettyPrintString("*")] + Multiply, + [PrettyPrintString(">")] + MoreThan, + [PrettyPrintString("<")] + LessThan, + [PrettyPrintString(">=")] + MoreThanOrEqual, + [PrettyPrintString("<=")] + LessThanOrEqual, + [PrettyPrintString("&&")] + LogicalAnd, + [PrettyPrintString("||")] + LogicalOr, + [PrettyPrintString("%")] + Remainder, + [PrettyPrintString("==")] + Equals, + [PrettyPrintString("!=")] + NotEquals, + // Unary operators + [PrettyPrintString("!")] + Not, + [PrettyPrintString("-")] + UnaryMinus, + // The rest + MemberAccess, + Parameter, + FunctionCall, + Keyword, + Constant, + ConditionalExpression + } + + internal enum ExpressionKeyword + { + StartingValue, + CurrentValue, + FinalValue, + Target, + Pi, + True, + False + } + + internal class ConditionalExpression : Expression + { + public Expression Condition { get; } + public Expression TruePart { get; } + public Expression FalsePart { get; } + public override ExpressionType Type => ExpressionType.ConditionalExpression; + + public ConditionalExpression(Expression condition, Expression truePart, Expression falsePart) + { + Condition = condition; + TruePart = truePart; + FalsePart = falsePart; + } + + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + var cond = Condition.Evaluate(ref context); + if (cond.Type == VariantType.Boolean && cond.Boolean) + return TruePart.Evaluate(ref context); + return FalsePart.Evaluate(ref context); + } + + protected override string Print() => $"({Condition}) ? ({TruePart}) : ({FalsePart})"; + } + + internal class ConstantExpression : Expression + { + public float Constant { get; } + public override ExpressionType Type => ExpressionType.Constant; + + public ConstantExpression(float constant) + { + Constant = constant; + } + + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) => Constant; + protected override string Print() => Constant.ToString(CultureInfo.InvariantCulture); + } + + internal class FunctionCallExpression : Expression + { + public string Name { get; } + public List Parameters { get; } + public override ExpressionType Type => ExpressionType.FunctionCall; + + public FunctionCallExpression(string name, List parameters) + { + Name = name; + Parameters = parameters; + } + + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + if (context.ForeignFunctionInterface == null) + return default; + var args = new List(); + foreach (var expr in Parameters) + args.Add(expr.Evaluate(ref context)); + if (!context.ForeignFunctionInterface.Call(Name, args, out var res)) + return default; + return res; + } + + protected override string Print() + { + return Name + "( (" + string.Join("), (", Parameters) + ") )"; + } + } + + internal class MemberAccessExpression : Expression + { + public override ExpressionType Type => ExpressionType.MemberAccess; + public Expression Target { get; } + public string Member { get; } + + public MemberAccessExpression(Expression target, string member) + { + Target = target; + Member = string.Intern(member); + } + + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + if (Target is KeywordExpression ke + && ke.Keyword == ExpressionKeyword.Target) + return context.Target.GetProperty(Member); + if (Target is ParameterExpression pe) + { + var obj = context.Parameters?.GetObjectParameter(pe.Name); + if (obj != null) + return obj.GetProperty(Member); + } + + return Target.Evaluate(ref context).GetProperty(Member); + } + + protected override string Print() + { + return "(" + Target.ToString() + ")." + Member; + } + } + + internal class ParameterExpression : Expression + { + public string Name { get; } + public override ExpressionType Type => ExpressionType.Parameter; + + public ParameterExpression(string name) + { + Name = name; + } + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + return context.Parameters?.GetParameter(Name) ?? default; + } + + protected override string Print() + { + return "{" + Name + "}"; + } + } + + internal class KeywordExpression : Expression + { + public override ExpressionType Type => ExpressionType.Keyword; + public ExpressionKeyword Keyword { get; } + + public KeywordExpression(ExpressionKeyword keyword) + { + Keyword = keyword; + } + + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + if (Keyword == ExpressionKeyword.StartingValue) + return context.StartingValue; + if (Keyword == ExpressionKeyword.CurrentValue) + return context.CurrentValue; + if (Keyword == ExpressionKeyword.FinalValue) + return context.FinalValue; + if (Keyword == ExpressionKeyword.Target) + // should be handled by MemberAccess + return default; + if (Keyword == ExpressionKeyword.True) + return true; + if (Keyword == ExpressionKeyword.False) + return false; + if (Keyword == ExpressionKeyword.Pi) + return (float) Math.PI; + return default; + } + + protected override string Print() + { + return "[" + Keyword + "]"; + } + } + + internal class UnaryExpression : Expression + { + public Expression Parameter { get; } + public override ExpressionType Type { get; } + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + if (Type == ExpressionType.Not) + return !Parameter.Evaluate(ref context); + if (Type == ExpressionType.UnaryMinus) + return -Parameter.Evaluate(ref context); + return default; + } + + protected override string Print() + { + return OperatorName(Type) + Parameter; + } + + public UnaryExpression(Expression parameter, ExpressionType type) + { + Parameter = parameter; + Type = type; + } + } + + internal class BinaryExpression : Expression + { + public Expression Left { get; } + public Expression Right { get; } + public override ExpressionType Type { get; } + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + var left = Left.Evaluate(ref context); + var right = Right.Evaluate(ref context); + if (Type == ExpressionType.Add) + return left + right; + if (Type == ExpressionType.Subtract) + return left - right; + if (Type == ExpressionType.Multiply) + return left * right; + if (Type == ExpressionType.Divide) + return left / right; + if (Type == ExpressionType.Remainder) + return left % right; + if (Type == ExpressionType.MoreThan) + return left > right; + if (Type == ExpressionType.LessThan) + return left < right; + if (Type == ExpressionType.MoreThanOrEqual) + return left > right; + if (Type == ExpressionType.LessThanOrEqual) + return left < right; + if (Type == ExpressionType.LogicalAnd) + return left.And(right); + if (Type == ExpressionType.LogicalOr) + return left.Or(right); + if (Type == ExpressionType.Equals) + return left.EqualsTo(right); + if (Type == ExpressionType.NotEquals) + return left.NotEqualsTo(right); + return default; + } + + protected override string Print() + { + return "(" + Left + OperatorName(Type) + Right + ")"; + } + + public BinaryExpression(Expression left, Expression right, ExpressionType type) + { + Left = left; + Right = right; + Type = type; + } + } + + + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs new file mode 100644 index 0000000000..a7ddabd70d --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal struct ExpressionEvaluationContext + { + public ExpressionVariant StartingValue { get; set; } + public ExpressionVariant CurrentValue { get; set; } + public ExpressionVariant FinalValue { get; set; } + public IExpressionObject Target { get; set; } + public IExpressionParameterCollection Parameters { get; set; } + public IExpressionForeignFunctionInterface ForeignFunctionInterface { get; set; } + } + + internal interface IExpressionObject + { + ExpressionVariant GetProperty(string name); + } + + internal interface IExpressionParameterCollection + { + public ExpressionVariant GetParameter(string name); + + public IExpressionObject GetObjectParameter(string name); + } + + internal interface IExpressionForeignFunctionInterface + { + bool Call(string name, IReadOnlyList arguments, out ExpressionVariant result); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParseException.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParseException.cs new file mode 100644 index 0000000000..6a207a3bf7 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParseException.cs @@ -0,0 +1,14 @@ +using System; + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal class ExpressionParseException : Exception + { + public int Position { get; } + + public ExpressionParseException(string message, int position) : base(message) + { + Position = position; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParser.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParser.cs new file mode 100644 index 0000000000..5924bb8f1b --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParser.cs @@ -0,0 +1,298 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +// ReSharper disable StringLiteralTypo + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal class ExpressionParser + { + public static Expression Parse(ReadOnlySpan s) + { + var p = new TokenParser(s); + var parsed = ParseTillTerminator(ref p, "", false, false, out _); + p.SkipWhitespace(); + if (p.Length != 0) + throw new ExpressionParseException("Unexpected data ", p.Position); + return parsed; + } + + private static ReadOnlySpan Dot => ".".AsSpan(); + static bool TryParseAtomic(ref TokenParser parser, + [MaybeNullWhen(returnValue: false)] out Expression expr) + { + // We can parse keywords, parameter names and constants + expr = null; + if (parser.TryParseKeywordLowerCase("this.startingvalue")) + expr = new KeywordExpression(ExpressionKeyword.StartingValue); + else if(parser.TryParseKeywordLowerCase("this.currentvalue")) + expr = new KeywordExpression(ExpressionKeyword.CurrentValue); + else if(parser.TryParseKeywordLowerCase("this.finalvalue")) + expr = new KeywordExpression(ExpressionKeyword.FinalValue); + else if(parser.TryParseKeywordLowerCase("pi")) + expr = new KeywordExpression(ExpressionKeyword.Pi); + else if(parser.TryParseKeywordLowerCase("true")) + expr = new KeywordExpression(ExpressionKeyword.True); + else if(parser.TryParseKeywordLowerCase("false")) + expr = new KeywordExpression(ExpressionKeyword.False); + else if (parser.TryParseKeywordLowerCase("this.target")) + expr = new KeywordExpression(ExpressionKeyword.Target); + + if (expr != null) + return true; + + if (parser.TryParseIdentifier(out var identifier)) + { + expr = new ParameterExpression(identifier.ToString()); + return true; + } + + if(parser.TryParseFloat(out var scalar)) + { + expr = new ConstantExpression(scalar); + return true; + } + + return false; + + } + + static bool TryParseOperator(ref TokenParser parser, out ExpressionType op) + { + op = (ExpressionType) (-1); + if (parser.TryConsume("||")) + op = ExpressionType.LogicalOr; + else if (parser.TryConsume("&&")) + op = ExpressionType.LogicalAnd; + else if (parser.TryConsume(">=")) + op = ExpressionType.MoreThanOrEqual; + else if (parser.TryConsume("<=")) + op = ExpressionType.LessThanOrEqual; + else if (parser.TryConsume("==")) + op = ExpressionType.Equals; + else if (parser.TryConsume("!=")) + op = ExpressionType.NotEquals; + else if (parser.TryConsumeAny("+-/*><%".AsSpan(), out var sop)) + { +#pragma warning disable CS8509 + op = sop switch +#pragma warning restore CS8509 + { + '+' => ExpressionType.Add, + '-' => ExpressionType.Subtract, + '/' => ExpressionType.Divide, + '*' => ExpressionType.Multiply, + '<' => ExpressionType.LessThan, + '>' => ExpressionType.MoreThan, + '%' => ExpressionType.Remainder + }; + } + else + return false; + + return true; + } + + + struct ExpressionOperatorGroup + { + private List _expressions; + private List _operators; + private Expression? _first; + + public bool NotEmpty => !Empty; + public bool Empty => _expressions == null && _first == null; + + public void AppendFirst(Expression expr) + { + if (NotEmpty) + throw new InvalidOperationException(); + _first = expr; + } + + public void AppendWithOperator(Expression expr, ExpressionType op) + { + if (_expressions == null) + { + if (_first == null) + throw new InvalidOperationException(); + _expressions = new List(); + _expressions.Add(_first); + _first = null; + _operators = new List(); + } + _expressions.Add(expr); + _operators.Add(op); + } + + // https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/ + private static readonly ExpressionType[][] OperatorPrecedenceGroups = new[] + { + // multiplicative + new[] {ExpressionType.Multiply, ExpressionType.Divide, ExpressionType.Remainder}, + // additive + new[] {ExpressionType.Add, ExpressionType.Subtract}, + // relational + new[] {ExpressionType.MoreThan, ExpressionType.MoreThanOrEqual, ExpressionType.LessThan, ExpressionType.LessThanOrEqual}, + // equality + new[] {ExpressionType.Equals, ExpressionType.NotEquals}, + // conditional AND + new[] {ExpressionType.LogicalAnd}, + // conditional OR + new[]{ ExpressionType.LogicalOr}, + }; + + private static readonly ExpressionType[][] OperatorPrecedenceGroupsReversed = + OperatorPrecedenceGroups.Reverse().ToArray(); + + // a*b+c [a,b,c] [*,+], call with (0, 2) + // ToExpression(a*b) + ToExpression(c) + // a+b*c -> ToExpression(a) + ToExpression(b*c) + Expression ToExpression(int from, int to) + { + if (to - from == 0) + return _expressions[from]; + + if (to - from == 1) + return new BinaryExpression(_expressions[from], _expressions[to], _operators[from]); + + foreach (var grp in OperatorPrecedenceGroupsReversed) + { + for (var c = from; c < to; c++) + { + var currentOperator = _operators[c]; + foreach(var operatorFromGroup in grp) + if (currentOperator == operatorFromGroup) + { + // We are dividing the expression right here + var left = ToExpression(from, c); + var right = ToExpression(c + 1, to); + return new BinaryExpression(left, right, currentOperator); + } + } + } + + // We shouldn't ever get here, if we are, there is something wrong in the code + throw new ExpressionParseException("Expression parsing algorithm bug in ToExpression", 0); + } + + public Expression ToExpression() + { + if (_expressions == null) + return _first ?? throw new InvalidOperationException(); + return ToExpression(0, _expressions.Count - 1); + } + } + + static Expression ParseTillTerminator(ref TokenParser parser, string terminatorChars, + bool throwOnTerminator, + bool throwOnEnd, + out char? token) + { + ExpressionOperatorGroup left = default; + token = null; + while (true) + { + if (parser.TryConsumeAny(terminatorChars.AsSpan(), out var consumedToken)) + { + if (throwOnTerminator || left.Empty) + throw new ExpressionParseException($"Unexpected '{token}'", parser.Position - 1); + token = consumedToken; + return left.ToExpression(); + } + parser.SkipWhitespace(); + if (parser.Length == 0) + { + if (throwOnEnd || left.Empty) + throw new ExpressionParseException("Unexpected end of expression", parser.Position); + return left.ToExpression(); + } + + ExpressionType? op = null; + if (left.NotEmpty) + { + if (parser.TryConsume('?')) + { + var truePart = ParseTillTerminator(ref parser, ":", + false, true, out _); + // pass through the current parsing rules to consume the rest + var falsePart = ParseTillTerminator(ref parser, terminatorChars, throwOnTerminator, throwOnEnd, + out token); + + return new ConditionalExpression(left.ToExpression(), truePart, falsePart); + } + + // We expect a binary operator here + if (!TryParseOperator(ref parser, out var sop)) + throw new ExpressionParseException("Unexpected token", parser.Position); + op = sop; + } + + // We expect an expression to be parsed (either due to expecting a binary operator or parsing the first part + var applyNegation = false; + while (parser.TryConsume('!')) + applyNegation = !applyNegation; + + var applyUnaryMinus = false; + while (parser.TryConsume('-')) + applyUnaryMinus = !applyUnaryMinus; + + Expression? parsed; + + if (parser.TryConsume('(')) + parsed = ParseTillTerminator(ref parser, ")", false, true, out _); + else if (parser.TryParseCall(out var functionName)) + { + var parameterList = new List(); + while (true) + { + parameterList.Add(ParseTillTerminator(ref parser, ",)", false, true, out var closingToken)); + if (closingToken == ')') + break; + if (closingToken != ',') + throw new ExpressionParseException("Unexpected end of the expression", parser.Position); + } + + parsed = new FunctionCallExpression(functionName.ToString(), parameterList); + } + else if (TryParseAtomic(ref parser, out parsed)) + { + // do nothing + } + else + throw new ExpressionParseException("Unexpected token", parser.Position); + + + // Parse any following member accesses + while (parser.TryConsume('.')) + { + if(!parser.TryParseIdentifier(out var memberName)) + throw new ExpressionParseException("Unexpected token", parser.Position); + + parsed = new MemberAccessExpression(parsed, memberName.ToString()); + } + + // Apply ! operator + if (applyNegation) + parsed = new UnaryExpression(parsed, ExpressionType.Not); + + if (applyUnaryMinus) + { + if(parsed is ConstantExpression constexpr) + parsed = new ConstantExpression(-constexpr.Constant); + else parsed = new UnaryExpression(parsed, ExpressionType.UnaryMinus); + } + + if (left.Empty) + left.AppendFirst(parsed); + else + left.AppendWithOperator(parsed, op!.Value); + } + + + + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs new file mode 100644 index 0000000000..8c6af5cb0c --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs @@ -0,0 +1,739 @@ +using System; +using System.Globalization; +using System.Numerics; +using System.Runtime.InteropServices; +using Avalonia.Media; + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal enum VariantType + { + Invalid, + Boolean, + Scalar, + Double, + Vector2, + Vector3, + Vector4, + AvaloniaMatrix, + Matrix3x2, + Matrix4x4, + Quaternion, + Color + } + + [StructLayout(LayoutKind.Explicit)] + internal struct ExpressionVariant + { + [FieldOffset(0)] public VariantType Type; + + [FieldOffset(4)] public bool Boolean; + [FieldOffset(4)] public float Scalar; + [FieldOffset(4)] public double Double; + [FieldOffset(4)] public Vector2 Vector2; + [FieldOffset(4)] public Vector3 Vector3; + [FieldOffset(4)] public Vector4 Vector4; + [FieldOffset(4)] public Matrix AvaloniaMatrix; + [FieldOffset(4)] public Matrix3x2 Matrix3x2; + [FieldOffset(4)] public Matrix4x4 Matrix4x4; + [FieldOffset(4)] public Quaternion Quaternion; + [FieldOffset(4)] public Color Color; + + + public ExpressionVariant GetProperty(string property) + { + if (Type == VariantType.Vector2) + { + if (ReferenceEquals(property, "X")) + return Vector2.X; + if (ReferenceEquals(property, "Y")) + return Vector2.Y; + return default; + } + + if (Type == VariantType.Vector3) + { + if (ReferenceEquals(property, "X")) + return Vector3.X; + if (ReferenceEquals(property, "Y")) + return Vector3.Y; + if (ReferenceEquals(property, "Z")) + return Vector3.Z; + if(ReferenceEquals(property, "XY")) + return new Vector2(Vector3.X, Vector3.Y); + if(ReferenceEquals(property, "YX")) + return new Vector2(Vector3.Y, Vector3.X); + if(ReferenceEquals(property, "XZ")) + return new Vector2(Vector3.X, Vector3.Z); + if(ReferenceEquals(property, "ZX")) + return new Vector2(Vector3.Z, Vector3.X); + if(ReferenceEquals(property, "YZ")) + return new Vector2(Vector3.Y, Vector3.Z); + if(ReferenceEquals(property, "ZY")) + return new Vector2(Vector3.Z, Vector3.Y); + return default; + } + + if (Type == VariantType.Vector4) + { + if (ReferenceEquals(property, "X")) + return Vector4.X; + if (ReferenceEquals(property, "Y")) + return Vector4.Y; + if (ReferenceEquals(property, "Z")) + return Vector4.Z; + if (ReferenceEquals(property, "W")) + return Vector4.W; + return default; + } + + if (Type == VariantType.Matrix3x2) + { + if (ReferenceEquals(property, "M11")) + return Matrix3x2.M11; + if (ReferenceEquals(property, "M12")) + return Matrix3x2.M12; + if (ReferenceEquals(property, "M21")) + return Matrix3x2.M21; + if (ReferenceEquals(property, "M22")) + return Matrix3x2.M22; + if (ReferenceEquals(property, "M31")) + return Matrix3x2.M31; + if (ReferenceEquals(property, "M32")) + return Matrix3x2.M32; + return default; + } + + if (Type == VariantType.AvaloniaMatrix) + { + if (ReferenceEquals(property, "M11")) + return AvaloniaMatrix.M11; + if (ReferenceEquals(property, "M12")) + return AvaloniaMatrix.M12; + if (ReferenceEquals(property, "M21")) + return AvaloniaMatrix.M21; + if (ReferenceEquals(property, "M22")) + return AvaloniaMatrix.M22; + if (ReferenceEquals(property, "M31")) + return AvaloniaMatrix.M31; + if (ReferenceEquals(property, "M32")) + return AvaloniaMatrix.M32; + return default; + } + + if (Type == VariantType.Matrix4x4) + { + if (ReferenceEquals(property, "M11")) + return Matrix4x4.M11; + if (ReferenceEquals(property, "M12")) + return Matrix4x4.M12; + if (ReferenceEquals(property, "M13")) + return Matrix4x4.M13; + if (ReferenceEquals(property, "M14")) + return Matrix4x4.M14; + if (ReferenceEquals(property, "M21")) + return Matrix4x4.M21; + if (ReferenceEquals(property, "M22")) + return Matrix4x4.M22; + if (ReferenceEquals(property, "M23")) + return Matrix4x4.M23; + if (ReferenceEquals(property, "M24")) + return Matrix4x4.M24; + if (ReferenceEquals(property, "M31")) + return Matrix4x4.M31; + if (ReferenceEquals(property, "M32")) + return Matrix4x4.M32; + if (ReferenceEquals(property, "M33")) + return Matrix4x4.M33; + if (ReferenceEquals(property, "M34")) + return Matrix4x4.M34; + if (ReferenceEquals(property, "M41")) + return Matrix4x4.M41; + if (ReferenceEquals(property, "M42")) + return Matrix4x4.M42; + if (ReferenceEquals(property, "M43")) + return Matrix4x4.M43; + if (ReferenceEquals(property, "M44")) + return Matrix4x4.M44; + return default; + } + + if (Type == VariantType.Quaternion) + { + if (ReferenceEquals(property, "X")) + return Quaternion.X; + if (ReferenceEquals(property, "Y")) + return Quaternion.Y; + if (ReferenceEquals(property, "Z")) + return Quaternion.Z; + if (ReferenceEquals(property, "W")) + return Quaternion.W; + return default; + } + + if (Type == VariantType.Color) + { + if (ReferenceEquals(property, "A")) + return Color.A; + if (ReferenceEquals(property, "R")) + return Color.R; + if (ReferenceEquals(property, "G")) + return Color.G; + if (ReferenceEquals(property, "B")) + return Color.B; + return default; + } + + return default; + } + + public static implicit operator ExpressionVariant(bool value) => + new ExpressionVariant + { + Type = VariantType.Boolean, + Boolean = value + }; + + public static implicit operator ExpressionVariant(float scalar) => + new ExpressionVariant + { + Type = VariantType.Scalar, + Scalar = scalar + }; + + public static implicit operator ExpressionVariant(double d) => + new ExpressionVariant + { + Type = VariantType.Double, + Double = d + }; + + + public static implicit operator ExpressionVariant(Vector2 value) => + new ExpressionVariant + { + Type = VariantType.Vector2, + Vector2 = value + }; + + + public static implicit operator ExpressionVariant(Vector3 value) => + new ExpressionVariant + { + Type = VariantType.Vector3, + Vector3 = value + }; + + + public static implicit operator ExpressionVariant(Vector4 value) => + new ExpressionVariant + { + Type = VariantType.Vector4, + Vector4 = value + }; + + public static implicit operator ExpressionVariant(Matrix3x2 value) => + new ExpressionVariant + { + Type = VariantType.Matrix3x2, + Matrix3x2 = value + }; + + public static implicit operator ExpressionVariant(Matrix value) => + new ExpressionVariant + { + Type = VariantType.Matrix3x2, + AvaloniaMatrix = value + }; + + public static implicit operator ExpressionVariant(Matrix4x4 value) => + new ExpressionVariant + { + Type = VariantType.Matrix4x4, + Matrix4x4 = value + }; + + public static implicit operator ExpressionVariant(Quaternion value) => + new ExpressionVariant + { + Type = VariantType.Quaternion, + Quaternion = value + }; + + public static implicit operator ExpressionVariant(Avalonia.Media.Color value) => + new ExpressionVariant + { + Type = VariantType.Color, + Color = value + }; + + public static ExpressionVariant operator +(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type != right.Type || left.Type == VariantType.Invalid) + return default; + + if (left.Type == VariantType.Scalar) + return left.Scalar + right.Scalar; + + if (left.Type == VariantType.Double) + return left.Double + right.Double; + + if (left.Type == VariantType.Vector2) + return left.Vector2 + right.Vector2; + + if (left.Type == VariantType.Vector3) + return left.Vector3 + right.Vector3; + + if (left.Type == VariantType.Vector4) + return left.Vector4 + right.Vector4; + + if (left.Type == VariantType.Matrix3x2) + return left.Matrix3x2 + right.Matrix3x2; + + if (left.Type == VariantType.AvaloniaMatrix) + return left.AvaloniaMatrix + right.AvaloniaMatrix; + + if (left.Type == VariantType.Matrix4x4) + return left.Matrix4x4 + right.Matrix4x4; + + if (left.Type == VariantType.Quaternion) + return left.Quaternion + right.Quaternion; + + return default; + } + + public static ExpressionVariant operator -(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type != right.Type || left.Type == VariantType.Invalid) + return default; + + if (left.Type == VariantType.Scalar) + return left.Scalar - right.Scalar; + + if (left.Type == VariantType.Double) + return left.Double - right.Double; + + if (left.Type == VariantType.Vector2) + return left.Vector2 - right.Vector2; + + if (left.Type == VariantType.Vector3) + return left.Vector3 - right.Vector3; + + if (left.Type == VariantType.Vector4) + return left.Vector4 - right.Vector4; + + if (left.Type == VariantType.Matrix3x2) + return left.Matrix3x2 - right.Matrix3x2; + + if (left.Type == VariantType.AvaloniaMatrix) + return left.AvaloniaMatrix - right.AvaloniaMatrix; + + if (left.Type == VariantType.Matrix4x4) + return left.Matrix4x4 - right.Matrix4x4; + + if (left.Type == VariantType.Quaternion) + return left.Quaternion - right.Quaternion; + + return default; + } + + public static ExpressionVariant operator -(ExpressionVariant left) + { + + if (left.Type == VariantType.Scalar) + return -left.Scalar; + + if (left.Type == VariantType.Double) + return -left.Double; + + if (left.Type == VariantType.Vector2) + return -left.Vector2; + + if (left.Type == VariantType.Vector3) + return -left.Vector3; + + if (left.Type == VariantType.Vector4) + return -left.Vector4; + + if (left.Type == VariantType.Matrix3x2) + return -left.Matrix3x2; + + if (left.Type == VariantType.AvaloniaMatrix) + return -left.AvaloniaMatrix; + + if (left.Type == VariantType.Matrix4x4) + return -left.Matrix4x4; + + if (left.Type == VariantType.Quaternion) + return -left.Quaternion; + + return default; + } + + public static ExpressionVariant operator *(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type == VariantType.Invalid || right.Type == VariantType.Invalid) + return default; + + if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar) + return left.Scalar * right.Scalar; + + if (left.Type == VariantType.Double && right.Type == VariantType.Double) + return left.Double * right.Double; + + if (left.Type == VariantType.Vector2 && right.Type == VariantType.Vector2) + return left.Vector2 * right.Vector2; + + if (left.Type == VariantType.Vector2 && right.Type == VariantType.Scalar) + return left.Vector2 * right.Scalar; + + if (left.Type == VariantType.Vector3 && right.Type == VariantType.Vector3) + return left.Vector3 * right.Vector3; + + if (left.Type == VariantType.Vector3 && right.Type == VariantType.Scalar) + return left.Vector3 * right.Scalar; + + if (left.Type == VariantType.Vector4 && right.Type == VariantType.Vector4) + return left.Vector4 * right.Vector4; + + if (left.Type == VariantType.Vector4 && right.Type == VariantType.Scalar) + return left.Vector4 * right.Scalar; + + if (left.Type == VariantType.Matrix3x2 && right.Type == VariantType.Matrix3x2) + return left.Matrix3x2 * right.Matrix3x2; + + if (left.Type == VariantType.Matrix3x2 && right.Type == VariantType.Scalar) + return left.Matrix3x2 * right.Scalar; + + if (left.Type == VariantType.AvaloniaMatrix && right.Type == VariantType.AvaloniaMatrix) + return left.AvaloniaMatrix * right.AvaloniaMatrix; + + if (left.Type == VariantType.AvaloniaMatrix && right.Type == VariantType.Scalar) + return left.AvaloniaMatrix * (double)right.Scalar; + + if (left.Type == VariantType.AvaloniaMatrix && right.Type == VariantType.Double) + return left.AvaloniaMatrix * right.Double; + + if (left.Type == VariantType.Matrix4x4 && right.Type == VariantType.Matrix4x4) + return left.Matrix4x4 * right.Matrix4x4; + + if (left.Type == VariantType.Matrix4x4 && right.Type == VariantType.Scalar) + return left.Matrix4x4 * right.Scalar; + + if (left.Type == VariantType.Quaternion && right.Type == VariantType.Quaternion) + return left.Quaternion * right.Quaternion; + + if (left.Type == VariantType.Quaternion && right.Type == VariantType.Scalar) + return left.Quaternion * right.Scalar; + + return default; + } + + public static ExpressionVariant operator /(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type == VariantType.Invalid || right.Type == VariantType.Invalid) + return default; + + if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar) + return left.Scalar / right.Scalar; + + if (left.Type == VariantType.Double && right.Type == VariantType.Double) + return left.Double / right.Double; + + if (left.Type == VariantType.Vector2 && right.Type == VariantType.Vector2) + return left.Vector2 / right.Vector2; + + if (left.Type == VariantType.Vector2 && right.Type == VariantType.Scalar) + return left.Vector2 / right.Scalar; + + if (left.Type == VariantType.Vector3 && right.Type == VariantType.Vector3) + return left.Vector3 / right.Vector3; + + if (left.Type == VariantType.Vector3 && right.Type == VariantType.Scalar) + return left.Vector3 / right.Scalar; + + if (left.Type == VariantType.Vector4 && right.Type == VariantType.Vector4) + return left.Vector4 / right.Vector4; + + if (left.Type == VariantType.Vector4 && right.Type == VariantType.Scalar) + return left.Vector4 / right.Scalar; + + if (left.Type == VariantType.Quaternion && right.Type == VariantType.Quaternion) + return left.Quaternion / right.Quaternion; + + return default; + } + + public ExpressionVariant EqualsTo(ExpressionVariant right) + { + if (Type != right.Type || Type == VariantType.Invalid) + return default; + + if (Type == VariantType.Scalar) + return Scalar == right.Scalar; + + + if (Type == VariantType.Double) + return Double == right.Double; + + if (Type == VariantType.Vector2) + return Vector2 == right.Vector2; + + if (Type == VariantType.Vector3) + return Vector3 == right.Vector3; + + if (Type == VariantType.Vector4) + return Vector4 == right.Vector4; + + if (Type == VariantType.Boolean) + return Boolean == right.Boolean; + + if (Type == VariantType.Matrix3x2) + return Matrix3x2 == right.Matrix3x2; + + if (Type == VariantType.AvaloniaMatrix) + return AvaloniaMatrix == right.AvaloniaMatrix; + + if (Type == VariantType.Matrix4x4) + return Matrix4x4 == right.Matrix4x4; + + if (Type == VariantType.Quaternion) + return Quaternion == right.Quaternion; + + return default; + } + + public ExpressionVariant NotEqualsTo(ExpressionVariant right) + { + var r = EqualsTo(right); + if (r.Type == VariantType.Boolean) + return !r.Boolean; + return default; + } + + public static ExpressionVariant operator !(ExpressionVariant v) + { + if (v.Type == VariantType.Boolean) + return !v.Boolean; + return default; + } + + public static ExpressionVariant operator %(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar) + return left.Scalar % right.Scalar; + if (left.Type == VariantType.Double && right.Type == VariantType.Double) + return left.Double % right.Double; + return default; + } + + public static ExpressionVariant operator <(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar) + return left.Scalar < right.Scalar; + if (left.Type == VariantType.Double && right.Type == VariantType.Double) + return left.Double < right.Double; + return default; + } + + public static ExpressionVariant operator >(ExpressionVariant left, ExpressionVariant right) + { + if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar) + return left.Scalar > right.Scalar; + + if (left.Type == VariantType.Double && right.Type == VariantType.Double) + return left.Double > right.Double; + return default; + } + + public ExpressionVariant And(ExpressionVariant right) + { + if (Type == VariantType.Boolean && right.Type == VariantType.Boolean) + return Boolean && right.Boolean; + return default; + } + + public ExpressionVariant Or(ExpressionVariant right) + { + if (Type == VariantType.Boolean && right.Type == VariantType.Boolean) + return Boolean && right.Boolean; + return default; + } + + public bool TryCast(out T res) where T : struct + { + if (typeof(T) == typeof(bool)) + { + if (Type == VariantType.Boolean) + { + res = (T) (object) Boolean; + return true; + } + } + + if (typeof(T) == typeof(float)) + { + if (Type == VariantType.Scalar) + { + res = (T) (object) Scalar; + return true; + } + } + + if (typeof(T) == typeof(double)) + { + if (Type == VariantType.Double) + { + res = (T) (object) Double; + return true; + } + } + + if (typeof(T) == typeof(Vector2)) + { + if (Type == VariantType.Vector2) + { + res = (T) (object) Vector2; + return true; + } + } + + if (typeof(T) == typeof(Vector3)) + { + if (Type == VariantType.Vector3) + { + res = (T) (object) Vector3; + return true; + } + } + + if (typeof(T) == typeof(Vector4)) + { + if (Type == VariantType.Vector4) + { + res = (T) (object) Vector4; + return true; + } + } + + if (typeof(T) == typeof(Matrix3x2)) + { + if (Type == VariantType.Matrix3x2) + { + res = (T) (object) Matrix3x2; + return true; + } + } + + if (typeof(T) == typeof(Matrix)) + { + if (Type == VariantType.AvaloniaMatrix) + { + res = (T) (object) Matrix3x2; + return true; + } + } + + if (typeof(T) == typeof(Matrix4x4)) + { + if (Type == VariantType.Matrix4x4) + { + res = (T) (object) Matrix4x4; + return true; + } + } + + if (typeof(T) == typeof(Quaternion)) + { + if (Type == VariantType.Quaternion) + { + res = (T) (object) Quaternion; + return true; + } + } + + if (typeof(T) == typeof(Avalonia.Media.Color)) + { + if (Type == VariantType.Color) + { + res = (T) (object) Color; + return true; + } + } + + res = default(T); + return false; + } + + public static ExpressionVariant Create(T v) where T : struct + { + if (typeof(T) == typeof(bool)) + return (bool) (object) v; + + if (typeof(T) == typeof(float)) + return (float) (object) v; + + if (typeof(T) == typeof(Vector2)) + return (Vector2) (object) v; + + if (typeof(T) == typeof(Vector3)) + return (Vector3) (object) v; + + if (typeof(T) == typeof(Vector4)) + return (Vector4) (object) v; + + if (typeof(T) == typeof(Matrix3x2)) + return (Matrix3x2) (object) v; + + if (typeof(T) == typeof(Matrix)) + return (Matrix) (object) v; + + if (typeof(T) == typeof(Matrix4x4)) + return (Matrix4x4) (object) v; + + if (typeof(T) == typeof(Quaternion)) + return (Quaternion) (object) v; + + if (typeof(T) == typeof(Avalonia.Media.Color)) + return (Avalonia.Media.Color) (object) v; + + throw new ArgumentException("Invalid variant type: " + typeof(T)); + } + + public T CastOrDefault() where T : struct + { + TryCast(out var r); + return r; + } + + public override string ToString() + { + if (Type == VariantType.Boolean) + return Boolean.ToString(); + if (Type == VariantType.Scalar) + return Scalar.ToString(CultureInfo.InvariantCulture); + if (Type == VariantType.Double) + return Double.ToString(CultureInfo.InvariantCulture); + if (Type == VariantType.Vector2) + return Vector2.ToString(); + if (Type == VariantType.Vector3) + return Vector3.ToString(); + if (Type == VariantType.Vector4) + return Vector4.ToString(); + if (Type == VariantType.Quaternion) + return Quaternion.ToString(); + if (Type == VariantType.Matrix3x2) + return Matrix3x2.ToString(); + if (Type == VariantType.AvaloniaMatrix) + return AvaloniaMatrix.ToString(); + if (Type == VariantType.Matrix4x4) + return Matrix4x4.ToString(); + if (Type == VariantType.Color) + return Color.ToString(); + if (Type == VariantType.Invalid) + return "Invalid"; + return "Unknown"; + } + } + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs new file mode 100644 index 0000000000..1050c7274c --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs @@ -0,0 +1,256 @@ +using System; +using System.Globalization; + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal ref struct TokenParser + { + private ReadOnlySpan _s; + public int Position { get; private set; } + public TokenParser(ReadOnlySpan s) + { + _s = s; + Position = 0; + } + + public void SkipWhitespace() + { + while (true) + { + if (_s.Length > 0 && char.IsWhiteSpace(_s[0])) + Advance(1); + else + return; + } + } + + static bool IsAlphaNumeric(char ch) => (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || + (ch >= 'A' && ch <= 'Z'); + + public bool TryConsume(char c) + { + SkipWhitespace(); + if (_s.Length == 0 || _s[0] != c) + return false; + + Advance(1); + return true; + } + public bool TryConsume(string s) + { + SkipWhitespace(); + if (_s.Length < s.Length) + return false; + for (var c = 0; c < s.Length; c++) + { + if (_s[c] != s[c]) + return false; + } + + Advance(s.Length); + return true; + } + + public bool TryConsumeAny(ReadOnlySpan chars, out char token) + { + SkipWhitespace(); + token = default; + if (_s.Length == 0) + return false; + + foreach (var c in chars) + { + if (c == _s[0]) + { + token = c; + Advance(1); + return true; + } + } + + return false; + } + + + public bool TryParseKeyword(string keyword) + { + SkipWhitespace(); + if (keyword.Length > _s.Length) + return false; + for(var c=0; c keyword.Length && IsAlphaNumeric(_s[keyword.Length])) + return false; + + Advance(keyword.Length); + return true; + } + + public bool TryParseKeywordLowerCase(string keywordInLowerCase) + { + SkipWhitespace(); + if (keywordInLowerCase.Length > _s.Length) + return false; + for(var c=0; c keywordInLowerCase.Length && IsAlphaNumeric(_s[keywordInLowerCase.Length])) + return false; + + Advance(keywordInLowerCase.Length); + return true; + } + + public void Advance(int c) + { + _s = _s.Slice(c); + Position += c; + } + + public int Length => _s.Length; + + public bool TryParseIdentifier(ReadOnlySpan extraValidChars, out ReadOnlySpan res) + { + res = ReadOnlySpan.Empty; + SkipWhitespace(); + if (_s.Length == 0) + return false; + var first = _s[0]; + if (!((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z'))) + return false; + int len = 1; + for (var c = 1; c < _s.Length; c++) + { + var ch = _s[c]; + if (IsAlphaNumeric(ch)) + len++; + else + { + var found = false; + foreach(var vc in extraValidChars) + if (vc == ch) + { + found = true; + break; + } + + if (found) + len++; + else + break; + } + } + + res = _s.Slice(0, len); + Advance(len); + return true; + } + + public bool TryParseIdentifier(out ReadOnlySpan res) + { + res = ReadOnlySpan.Empty; + SkipWhitespace(); + if (_s.Length == 0) + return false; + var first = _s[0]; + if (!((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z'))) + return false; + int len = 1; + for (var c = 1; c < _s.Length; c++) + { + var ch = _s[c]; + if (IsAlphaNumeric(ch)) + len++; + else + break; + } + + res = _s.Slice(0, len); + Advance(len); + return true; + } + + public bool TryParseCall(out ReadOnlySpan res) + { + res = ReadOnlySpan.Empty; + SkipWhitespace(); + if (_s.Length == 0) + return false; + var first = _s[0]; + if (!((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z'))) + return false; + int len = 1; + for (var c = 1; c < _s.Length; c++) + { + var ch = _s[c]; + if ((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch<= 'Z') || ch == '.') + len++; + else + break; + } + + res = _s.Slice(0, len); + + // Find '(' + for (var c = len; c < _s.Length; c++) + { + if(char.IsWhiteSpace(_s[c])) + continue; + if(_s[c]=='(') + { + Advance(c + 1); + return true; + } + + return false; + + } + + return false; + + } + + + public bool TryParseFloat(out float res) + { + res = 0; + SkipWhitespace(); + if (_s.Length == 0) + return false; + + var len = 0; + var dotCount = 0; + for (var c = 0; c < _s.Length; c++) + { + var ch = _s[c]; + if (ch >= '0' && ch <= '9') + len = c + 1; + else if (ch == '.' && dotCount == 0) + { + len = c + 1; + dotCount++; + } + else + break; + } + + var span = _s.Slice(0, len); + +#if NETSTANDARD2_0 + if (!float.TryParse(span.ToString(), NumberStyles.Number, CultureInfo.InvariantCulture, out res)) + return false; +#else + if (!float.TryParse(span, NumberStyles.Number, CultureInfo.InvariantCulture, out res)) + return false; +#endif + Advance(len); + return true; + } + + public override string ToString() => _s.ToString(); + + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/ICompositionSurface.cs b/src/Avalonia.Base/Rendering/Composition/ICompositionSurface.cs new file mode 100644 index 0000000000..9ef31c30e0 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/ICompositionSurface.cs @@ -0,0 +1,9 @@ +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition +{ + public interface ICompositionSurface + { + internal ServerCompositionSurface Server { get; } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs b/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs new file mode 100644 index 0000000000..5e91bcb3d4 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs @@ -0,0 +1,46 @@ +using System.Numerics; + +namespace Avalonia.Rendering.Composition +{ + static class MatrixUtils + { + public static Matrix4x4 ComputeTransform(Vector2 size, Vector2 anchorPoint, Vector3 centerPoint, + Matrix4x4 transformMatrix, Vector3 scale, float rotationAngle, Quaternion orientation, Vector3 offset) + { + // The math here follows the *observed* UWP behavior since there are no docs on how it's supposed to work + + var anchor = size * anchorPoint; + var mat = Matrix4x4.CreateTranslation(-anchor.X, -anchor.Y, 0); + + var center = new Vector3(centerPoint.X, centerPoint.Y, centerPoint.Z); + + if (!transformMatrix.IsIdentity) + mat = transformMatrix * mat; + + + if (scale != new Vector3(1, 1, 1)) + mat *= Matrix4x4.CreateScale(scale, center); + + //TODO: RotationAxis support + if (rotationAngle != 0) + mat *= Matrix4x4.CreateRotationZ(rotationAngle, center); + + if (orientation != Quaternion.Identity) + { + if (centerPoint != default) + { + mat *= Matrix4x4.CreateTranslation(-center) + * Matrix4x4.CreateFromQuaternion(orientation) + * Matrix4x4.CreateTranslation(center); + } + else + mat *= Matrix4x4.CreateFromQuaternion(orientation); + } + + if (offset != default) + mat *= Matrix4x4.CreateTranslation(offset); + + return mat; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs new file mode 100644 index 0000000000..f5dfa92897 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -0,0 +1,142 @@ +using System.Numerics; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server; + +internal class CompositorDrawingContextProxy : IDrawingContextImpl +{ + private IDrawingContextImpl _impl; + + public CompositorDrawingContextProxy(IDrawingContextImpl impl) + { + _impl = impl; + } + + public Matrix PreTransform { get; set; } = Matrix.Identity; + + public void Dispose() + { + _impl.Dispose(); + } + + Matrix _transform; + public Matrix Transform + { + get => _transform; + set => _impl.Transform = PreTransform * (_transform = value); + } + + public void Clear(Color color) + { + _impl.Clear(color); + } + + public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) + { + _impl.DrawBitmap(source, opacity, sourceRect, destRect, bitmapInterpolationMode); + } + + public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) + { + _impl.DrawBitmap(source, opacityMask, opacityMaskRect, destRect); + } + + public void DrawLine(IPen pen, Point p1, Point p2) + { + _impl.DrawLine(pen, p1, p2); + } + + public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) + { + _impl.DrawGeometry(brush, pen, geometry); + } + + public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadows = default) + { + _impl.DrawRectangle(brush, pen, rect, boxShadows); + } + + public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) + { + _impl.DrawEllipse(brush, pen, rect); + } + + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + { + _impl.DrawGlyphRun(foreground, glyphRun); + } + + public IDrawingContextLayerImpl CreateLayer(Size size) + { + return _impl.CreateLayer(size); + } + + public void PushClip(Rect clip) + { + _impl.PushClip(clip); + } + + public void PushClip(RoundedRect clip) + { + _impl.PushClip(clip); + } + + public void PopClip() + { + _impl.PopClip(); + } + + public void PushOpacity(double opacity) + { + _impl.PushOpacity(opacity); + } + + public void PopOpacity() + { + _impl.PopOpacity(); + } + + public void PushOpacityMask(IBrush mask, Rect bounds) + { + _impl.PushOpacityMask(mask, bounds); + } + + public void PopOpacityMask() + { + _impl.PopOpacityMask(); + } + + public void PushGeometryClip(IGeometryImpl clip) + { + _impl.PushGeometryClip(clip); + } + + public void PopGeometryClip() + { + _impl.PopGeometryClip(); + } + + public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) + { + _impl.PushBitmapBlendMode(blendingMode); + } + + public void PopBitmapBlendMode() + { + _impl.PopBitmapBlendMode(); + } + + public void Custom(ICustomDrawOperation custom) + { + _impl.Custom(custom); + } + + public Matrix CutTransform(Matrix4x4 transform) => new Matrix(transform.M11, transform.M12, transform.M21, + transform.M22, transform.M41, + transform.M42); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs b/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs new file mode 100644 index 0000000000..372fa4d9ce --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs @@ -0,0 +1,46 @@ +namespace Avalonia.Rendering.Composition.Server +{ + internal class ReadbackIndices + { + private readonly object _lock = new object(); + public int ReadIndex { get; private set; } = 0; + public int WriteIndex { get; private set; } = -1; + public ulong ReadRevision { get; private set; } + public ulong WriteRevision { get; private set; } + private ulong[] _revisions = new ulong[3]; + + + public void NextRead() + { + lock (_lock) + { + for (var c = 0; c < 3; c++) + { + if (c != WriteIndex && c != ReadIndex && _revisions[c] > ReadRevision) + { + ReadIndex = c; + ReadRevision = _revisions[c]; + return; + } + } + } + } + + public void NextWrite(ulong revision) + { + lock (_lock) + { + for (var c = 0; c < 3; c++) + { + if (c != WriteIndex && c != ReadIndex) + { + WriteIndex = c; + WriteRevision = revision; + _revisions[c] = revision; + return; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionBrush.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionBrush.cs new file mode 100644 index 0000000000..eb041aaf88 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionBrush.cs @@ -0,0 +1,7 @@ +namespace Avalonia.Rendering.Composition.Server +{ + internal abstract partial class ServerCompositionBrush : ServerObject + { + + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs new file mode 100644 index 0000000000..397c968d04 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -0,0 +1,41 @@ +using System.Numerics; +using Avalonia.Collections.Pooled; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.Composition.Transport; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server; + +internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisual +{ + private CompositionDrawList? _renderCommands; + + public ServerCompositionDrawListVisual(ServerCompositor compositor) : base(compositor) + { + } + + protected override void ApplyCore(ChangeSet changes) + { + var ch = (DrawListVisualChanges)changes; + if (ch.DrawCommandsIsSet) + { + _renderCommands?.Dispose(); + _renderCommands = ch.AcquireDrawCommands(); + } + base.ApplyCore(changes); + } + + protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + if (_renderCommands != null) + { + foreach (var cmd in _renderCommands) + { + cmd.Item.Render(canvas); + } + } + base.RenderCore(canvas, transform); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionGradientBrush.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionGradientBrush.cs new file mode 100644 index 0000000000..0948b9692f --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionGradientBrush.cs @@ -0,0 +1,15 @@ +using System; + +namespace Avalonia.Rendering.Composition.Server +{ + internal abstract partial class ServerCompositionGradientBrush : ServerCompositionBrush + { + public ServerCompositionGradientStopCollection Stops { get; } + public ServerCompositionGradientBrush(ServerCompositor compositor) : base(compositor) + { + Stops = new ServerCompositionGradientStopCollection(compositor); + } + + public override long LastChangedBy => Math.Max(base.LastChangedBy, (long)Stops.LastChangedBy); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionLinearGradientBrush.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionLinearGradientBrush.cs new file mode 100644 index 0000000000..c421cdcfb0 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionLinearGradientBrush.cs @@ -0,0 +1,22 @@ +using Avalonia.Media; + +namespace Avalonia.Rendering.Composition.Server +{ + internal partial class ServerCompositionLinearGradientBrush + { + /* + protected override void UpdateBackendBrush(ICbBrush brush) + { + var stopColors = new Color[Stops.List.Count]; + var offsets = new float[Stops.List.Count]; + for (var c = 0; c < Stops.List.Count; c++) + { + stopColors[c] = Stops.List[c].Color; + offsets[c] = Stops.List[c].Offset; + } + + ((ICbLinearGradientBrush) brush).Update(StartPoint, EndPoint, stopColors, offsets, ExtendMode); + }*/ + + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs new file mode 100644 index 0000000000..462a193a86 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs @@ -0,0 +1,9 @@ +namespace Avalonia.Rendering.Composition.Server +{ + internal abstract class ServerCompositionSurface : ServerObject + { + protected ServerCompositionSurface(ServerCompositor compositor) : base(compositor) + { + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs new file mode 100644 index 0000000000..493529e111 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -0,0 +1,53 @@ +using System; +using System.Numerics; +using System.Threading; +using Avalonia.Media; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server +{ + internal partial class ServerCompositionTarget + { + private readonly ServerCompositor _compositor; + private readonly Func _renderTargetFactory; + private static long s_nextId = 1; + public long Id { get; } + private ulong _frame = 1; + private IRenderTarget? _renderTarget; + + public ReadbackIndices Readback { get; } = new(); + + public ServerCompositionTarget(ServerCompositor compositor, Func renderTargetFactory) : + base(compositor) + { + _compositor = compositor; + _renderTargetFactory = renderTargetFactory; + Id = Interlocked.Increment(ref s_nextId); + } + + partial void OnIsEnabledChanged() + { + if (IsEnabled) + _compositor.AddCompositionTarget(this); + else + _compositor.RemoveCompositionTarget(this); + } + + public void Render() + { + if (Root == null) + return; + _renderTarget ??= _renderTargetFactory(); + + Compositor.UpdateServerTime(); + using (var context = _renderTarget.CreateDrawingContext(null)) + { + context.Clear(Colors.Transparent); + Root.Render(new CompositorDrawingContextProxy(context), Root.CombinedTransformMatrix); + } + + Readback.NextWrite(_frame); + _frame++; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs new file mode 100644 index 0000000000..e56f85acdf --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Server +{ + internal class ServerCompositor : IRenderLoopTask + { + private readonly IRenderLoop _renderLoop; + private readonly Queue _batches = new Queue(); + public long LastBatchId { get; private set; } + public Stopwatch Clock { get; } = Stopwatch.StartNew(); + public TimeSpan ServerNow { get; private set; } + private List _activeTargets = new(); + + public ServerCompositor(IRenderLoop renderLoop) + { + _renderLoop = renderLoop; + _renderLoop.Add(this); + } + + public void EnqueueBatch(Batch batch) + { + lock (_batches) + _batches.Enqueue(batch); + } + + internal void UpdateServerTime() => ServerNow = Clock.Elapsed; + + List _reusableToCompleteList = new(); + void ApplyPendingBatches() + { + while (true) + { + Batch batch; + lock (_batches) + { + if(_batches.Count == 0) + break; + batch = _batches.Dequeue(); + } + + foreach (var change in batch.Changes) + { + if (change.Dispose) + { + //TODO + } + change.Target!.Apply(change); + change.Reset(); + } + + _reusableToCompleteList.Add(batch); + LastBatchId = batch.SequenceId; + } + } + + void CompletePendingBatches() + { + foreach(var batch in _reusableToCompleteList) + batch.Complete(); + _reusableToCompleteList.Clear(); + } + + bool IRenderLoopTask.NeedsUpdate => false; + void IRenderLoopTask.Update(TimeSpan time) + { + } + + void IRenderLoopTask.Render() + { + ApplyPendingBatches(); + foreach (var t in _activeTargets) + t.Render(); + + CompletePendingBatches(); + } + + public void AddCompositionTarget(ServerCompositionTarget target) + { + _activeTargets.Add(target); + } + + public void RemoveCompositionTarget(ServerCompositionTarget target) + { + _activeTargets.Remove(target); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs new file mode 100644 index 0000000000..ac112b846f --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs @@ -0,0 +1,29 @@ +using System.Numerics; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server +{ + internal class ServerCompositionContainerVisual : ServerCompositionVisual + { + public ServerCompositionVisualCollection Children { get; } + + + protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + base.RenderCore(canvas, transform); + + foreach (var ch in Children) + { + var t = transform; + + t = ch.CombinedTransformMatrix * t; + ch.Render(canvas, t); + } + } + + public ServerCompositionContainerVisual(ServerCompositor compositor) : base(compositor) + { + Children = new ServerCompositionVisualCollection(compositor); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs new file mode 100644 index 0000000000..5f3eb051a4 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs @@ -0,0 +1,31 @@ +using System.Numerics; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Server +{ + class ServerCustomDrawVisual : ServerCompositionContainerVisual + { + private readonly ICustomDrawVisualRenderer _renderer; + private TData? _data; + public ServerCustomDrawVisual(ServerCompositor compositor, ICustomDrawVisualRenderer renderer) : base(compositor) + { + _renderer = renderer; + } + + protected override void ApplyCore(ChangeSet changes) + { + var c = (CustomDrawVisualChanges) changes; + if (c.Data.IsSet) + _data = c.Data.Value; + + base.ApplyCore(changes); + } + + protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + _renderer.Render(canvas, _data); + base.RenderCore(canvas, transform); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs new file mode 100644 index 0000000000..09ef119e6b --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Server +{ + class ServerList : ServerObject where T : ServerObject + { + public List List { get; } = new List(); + protected override void ApplyCore(ChangeSet changes) + { + var c = (ListChangeSet) changes; + if (c.HasListChanges) + { + foreach (var lc in c.ListChanges) + { + if(lc.Action == ListChangeAction.Clear) + List.Clear(); + if(lc.Action == ListChangeAction.RemoveAt) + List.RemoveAt(lc.Index); + if(lc.Action == ListChangeAction.InsertAt) + List.Insert(lc.Index, lc.Added!); + if (lc.Action == ListChangeAction.ReplaceAt) + List[lc.Index] = lc.Added!; + } + } + } + + public override long LastChangedBy + { + get + { + var seq = base.LastChangedBy; + foreach (var i in List) + seq = Math.Max(i.LastChangedBy, seq); + return seq; + } + } + + public List.Enumerator GetEnumerator() => List.GetEnumerator(); + + public ServerList(ServerCompositor compositor) : base(compositor) + { + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs new file mode 100644 index 0000000000..072377cd7e --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs @@ -0,0 +1,36 @@ +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Server +{ + internal abstract class ServerObject : IExpressionObject + { + public ServerCompositor Compositor { get; } + + public virtual long LastChangedBy => ItselfLastChangedBy; + public long ItselfLastChangedBy { get; private set; } + + public ServerObject(ServerCompositor compositor) + { + Compositor = compositor; + } + + protected virtual void ApplyCore(ChangeSet changes) + { + + } + + public void Apply(ChangeSet changes) + { + ApplyCore(changes); + ItselfLastChangedBy = changes.Batch!.SequenceId; + } + + public virtual ExpressionVariant GetPropertyForAnimation(string name) + { + return default; + } + + ExpressionVariant IExpressionObject.GetProperty(string name) => GetPropertyForAnimation(name); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs new file mode 100644 index 0000000000..60569867de --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs @@ -0,0 +1,16 @@ +using System.Numerics; +using Avalonia.Media.Immutable; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server +{ + internal partial class ServerCompositionSolidColorVisual + { + protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + canvas.Transform = canvas.CutTransform(transform); + canvas.DrawRectangle(new ImmutableSolidColorBrush(Color), null, new RoundedRect(new Rect(new Size(Size)))); + base.RenderCore(canvas, transform); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs new file mode 100644 index 0000000000..c658dc8ae3 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs @@ -0,0 +1,20 @@ +using System.Numerics; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server +{ + internal partial class ServerCompositionSpriteVisual + { + + protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + if (Brush != null) + { + //SetTransform(canvas, transform); + //canvas.FillRect((Vector2)Size, (ICbBrush)Brush.Brush!); + } + + base.RenderCore(canvas, transform); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs new file mode 100644 index 0000000000..37e188fb47 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -0,0 +1,92 @@ +using System.Numerics; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Server +{ + unsafe partial class ServerCompositionVisual : ServerObject + { + protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + + } + + public void Render(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + if(Visible == false) + return; + if(Opacity == 0) + return; + canvas.PreTransform = canvas.CutTransform(transform); + canvas.Transform = Matrix.Identity; + if (Opacity != 1) + canvas.PushOpacity(Opacity); + if(ClipToBounds) + canvas.PushClip(new Rect(new Size(Size.X, Size.Y))); + if (Clip != null) + canvas.PushGeometryClip(Clip); + + RenderCore(canvas, transform); + + if (Clip != null) + canvas.PopGeometryClip(); + if (ClipToBounds) + canvas.PopClip(); + if(Opacity != 1) + canvas.PopOpacity(); + } + + private ReadbackData _readback0, _readback1, _readback2; + + + public ref ReadbackData GetReadback(int idx) + { + if (idx == 0) + return ref _readback0; + if (idx == 1) + return ref _readback1; + return ref _readback2; + } + + public Matrix4x4 CombinedTransformMatrix + { + get + { + if (Root == null) + return default; + + var res = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, + Scale, RotationAngle, Orientation, Offset); + var i = Root.Readback; + ref var readback = ref GetReadback(i.WriteIndex); + readback.Revision = i.WriteRevision; + readback.Matrix = res; + readback.TargetId = Root.Id; + + return res; + } + } + + public struct ReadbackData + { + public Matrix4x4 Matrix; + public bool Visible; + public ulong Revision; + public long TargetId; + } + + partial void ApplyChangesExtra(CompositionVisualChanges c) + { + if (c.Parent.IsSet) + Parent = c.Parent.Value; + if (c.Root.IsSet) + Root = c.Root.Value; + } + + public ServerCompositionTarget? Root { get; private set; } + + public ServerCompositionVisual? Parent { get; private set; } + } + + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs b/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs new file mode 100644 index 0000000000..7b64c01d09 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Avalonia.Rendering.Composition.Transport +{ + internal class Batch + { + private static long _nextSequenceId = 1; + private static ConcurrentBag> _pool = new ConcurrentBag>(); + public long SequenceId { get; } + + public Batch() + { + SequenceId = Interlocked.Increment(ref _nextSequenceId); + if (!_pool.TryTake(out var lst)) + lst = new List(); + Changes = lst; + } + private TaskCompletionSource _tcs = new TaskCompletionSource(); + public List Changes { get; private set; } + public TimeSpan CommitedAt { get; set; } + + public void Complete() + { + Changes.Clear(); + _pool.Add(Changes); + Changes = null!; + + _tcs.TrySetResult(0); + } + + public Task Completed => _tcs.Task; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/Change.cs b/src/Avalonia.Base/Rendering/Composition/Transport/Change.cs new file mode 100644 index 0000000000..cbee350ab3 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/Change.cs @@ -0,0 +1,82 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Rendering.Composition.Animations; + +namespace Avalonia.Rendering.Composition.Transport +{ + struct Change + { + private T? _value; + + public bool IsSet { get; private set; } + + public T? Value + { + get + { + if(!IsSet) + throw new InvalidOperationException(); + return _value; + } + set + { + IsSet = true; + _value = value; + } + } + + public void Reset() + { + _value = default; + IsSet = false; + } + } + + struct AnimatedChange + { + private T? _value; + private IAnimationInstance? _animation; + + public bool IsValue { get; private set; } + public bool IsAnimation { get; private set; } + + public T Value + { + get + { + if(!IsValue) + throw new InvalidOperationException(); + return _value!; + } + set + { + IsAnimation = false; + _animation = null; + IsValue = true; + _value = value; + } + } + + public IAnimationInstance Animation + { + get + { + if(!IsAnimation) + throw new InvalidOperationException(); + return _animation!; + } + set + { + IsValue = false; + _value = default; + IsAnimation = true; + _animation = value; + } + } + + public void Reset() + { + this = default; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs new file mode 100644 index 0000000000..898885dce6 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs @@ -0,0 +1,36 @@ +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport +{ + internal abstract class ChangeSet + { + private readonly IChangeSetPool _pool; + public Batch Batch = null!; + public ServerObject? Target; + public bool Dispose; + + public ChangeSet(IChangeSetPool pool) + { + _pool = pool; + } + + public virtual void Reset() + { + Batch = null!; + Target = null; + Dispose = false; + } + + public void Return() + { + _pool.Return(this); + } + } + + internal class CompositionObjectChanges : ChangeSet + { + public CompositionObjectChanges(IChangeSetPool pool) : base(pool) + { + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs new file mode 100644 index 0000000000..ea97cd7d44 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Concurrent; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport +{ + interface IChangeSetPool + { + void Return(ChangeSet changes); + ChangeSet Get(ServerObject target, Batch batch); + } + + class ChangeSetPool : IChangeSetPool where T : ChangeSet + { + private readonly Func _factory; + private readonly ConcurrentBag _pool = new ConcurrentBag(); + + public ChangeSetPool(Func factory) + { + _factory = factory; + } + + public void Return(T changes) + { + changes.Reset(); + _pool.Add(changes); + } + + void IChangeSetPool.Return(ChangeSet changes) => Return((T) changes); + ChangeSet IChangeSetPool.Get(ServerObject target, Batch batch) => Get(target, batch); + + public T Get(ServerObject target, Batch batch) + { + if (!_pool.TryTake(out var res)) + res = _factory(this); + res.Target = target; + res.Batch = batch; + res.Dispose = false; + return res; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs new file mode 100644 index 0000000000..aed041b62e --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs @@ -0,0 +1,20 @@ +namespace Avalonia.Rendering.Composition.Transport +{ + class CustomDrawVisualChanges : CompositionVisualChanges + { + public CustomDrawVisualChanges(IChangeSetPool pool) : base(pool) + { + } + + public Change Data; + + public override void Reset() + { + Data.Reset(); + base.Reset(); + } + + public new static ChangeSetPool> Pool { get; } = + new ChangeSetPool>(pool => new CustomDrawVisualChanges(pool)); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs new file mode 100644 index 0000000000..215c03b229 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using Avalonia.Collections.Pooled; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Transport; + +internal class DrawListVisualChanges : CompositionVisualChanges +{ + private CompositionDrawList? _drawCommands; + + public DrawListVisualChanges(IChangeSetPool pool) : base(pool) + { + } + + public CompositionDrawList? DrawCommands + { + get => _drawCommands; + set + { + _drawCommands?.Dispose(); + _drawCommands = value; + DrawCommandsIsSet = true; + } + } + + public bool DrawCommandsIsSet { get; private set; } + + public CompositionDrawList? AcquireDrawCommands() + { + var rv = _drawCommands; + _drawCommands = null; + DrawCommandsIsSet = false; + return rv; + } + + public override void Reset() + { + _drawCommands?.Dispose(); + _drawCommands = null; + DrawCommandsIsSet = false; + base.Reset(); + } + + public new static ChangeSetPool Pool { get; } = + new ChangeSetPool(pool => new(pool)); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs new file mode 100644 index 0000000000..ee6e4231f8 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs @@ -0,0 +1,19 @@ +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport +{ + internal class ListChange where T : ServerObject + { + public int Index; + public ListChangeAction Action; + public T? Added; + } + + internal enum ListChangeAction + { + InsertAt, + RemoveAt, + Clear, + ReplaceAt + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs new file mode 100644 index 0000000000..9bb101a080 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport +{ + class ListChangeSet : ChangeSet where T : ServerObject + { + private List>? _listChanges; + public List> ListChanges => _listChanges ??= new List>(); + public bool HasListChanges => _listChanges != null; + + public override void Reset() + { + _listChanges?.Clear(); + base.Reset(); + } + + public ListChangeSet(IChangeSetPool pool) : base(pool) + { + } + + public static readonly ChangeSetPool> Pool = + new ChangeSetPool>(pool => new ListChangeSet(pool)); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs new file mode 100644 index 0000000000..1add3aa990 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs @@ -0,0 +1,92 @@ +using System.Collections; +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport +{ + class ServerListProxyHelper : IList + where TServer : ServerObject + where TClient : CompositionObject + { + private readonly IGetChanges _parent; + private readonly List _list = new List(); + + public interface IGetChanges + { + ListChangeSet GetChanges(); + } + + public ServerListProxyHelper(IGetChanges parent) + { + _parent = parent; + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public List.Enumerator GetEnumerator() => _list.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public void Add(TClient item) => Insert(_list.Count, item); + + public void Clear() + { + _list.Clear(); + _parent.GetChanges().ListChanges.Add(new ListChange + { + Action = ListChangeAction.Clear + }); + } + + public bool Contains(TClient item) => _list.Contains(item); + + public void CopyTo(TClient[] array, int arrayIndex) => _list.CopyTo(array, arrayIndex); + + public bool Remove(TClient item) + { + var idx = _list.IndexOf(item); + if (idx == -1) + return false; + RemoveAt(idx); + return true; + } + + public int Count => _list.Count; + public bool IsReadOnly => false; + public int IndexOf(TClient item) => _list.IndexOf(item); + + public void Insert(int index, TClient item) + { + _list.Insert(index, item); + _parent.GetChanges().ListChanges.Add(new ListChange + { + Action = ListChangeAction.InsertAt, + Index = index, + Added = (TServer) item.Server + }); + } + + public void RemoveAt(int index) + { + _list.RemoveAt(index); + _parent.GetChanges().ListChanges.Add(new ListChange + { + Action = ListChangeAction.RemoveAt, + Index = index + }); + } + + public TClient this[int index] + { + get => _list[index]; + set + { + _list[index] = value; + _parent.GetChanges().ListChanges.Add(new ListChange + { + Action = ListChangeAction.ReplaceAt, + Index = index, + Added = (TServer) value.Server + }); + } + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs new file mode 100644 index 0000000000..c87fb96967 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs @@ -0,0 +1,16 @@ +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport +{ + partial class CompositionVisualChanges + { + public Change Parent; + public Change Root; + + partial void ResetExtra() + { + Parent.Reset(); + Root.Reset(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Utils/CubicBezier.cs b/src/Avalonia.Base/Rendering/Composition/Utils/CubicBezier.cs new file mode 100644 index 0000000000..8c85d7978b --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Utils/CubicBezier.cs @@ -0,0 +1,302 @@ +// ReSharper disable InconsistentNaming +// Ported from Chromium project https://github.com/chromium/chromium/blob/374d31b7704475fa59f7b2cb836b3b68afdc3d79/ui/gfx/geometry/cubic_bezier.cc + +using System; + +// ReSharper disable CompareOfFloatsByEqualityOperator +// ReSharper disable CommentTypo +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable TooWideLocalVariableScope +// ReSharper disable UnusedMember.Global +#pragma warning disable 649 + +namespace Avalonia.Rendering.Composition.Utils +{ + internal unsafe struct CubicBezier + { + const int CUBIC_BEZIER_SPLINE_SAMPLES = 11; + double ax_; + double bx_; + double cx_; + + double ay_; + double by_; + double cy_; + + double start_gradient_; + double end_gradient_; + + double range_min_; + double range_max_; + private bool monotonically_increasing_; + + fixed double spline_samples_[CUBIC_BEZIER_SPLINE_SAMPLES]; + + public CubicBezier(double p1x, double p1y, double p2x, double p2y) : this() + { + InitCoefficients(p1x, p1y, p2x, p2y); + InitGradients(p1x, p1y, p2x, p2y); + InitRange(p1y, p2y); + InitSpline(); + } + + public readonly double SampleCurveX(double t) + { + // `ax t^3 + bx t^2 + cx t' expanded using Horner's rule. + return ((ax_ * t + bx_) * t + cx_) * t; + } + + readonly double SampleCurveY(double t) + { + return ((ay_ * t + by_) * t + cy_) * t; + } + + readonly double SampleCurveDerivativeX(double t) + { + return (3.0 * ax_ * t + 2.0 * bx_) * t + cx_; + } + + readonly double SampleCurveDerivativeY(double t) + { + return (3.0 * ay_ * t + 2.0 * by_) * t + cy_; + } + + public readonly double SolveWithEpsilon(double x, double epsilon) + { + if (x < 0.0) + return 0.0 + start_gradient_ * x; + if (x > 1.0) + return 1.0 + end_gradient_ * (x - 1.0); + return SampleCurveY(SolveCurveX(x, epsilon)); + } + + void InitCoefficients(double p1x, + double p1y, + double p2x, + double p2y) + { + // Calculate the polynomial coefficients, implicit first and last control + // points are (0,0) and (1,1). + cx_ = 3.0 * p1x; + bx_ = 3.0 * (p2x - p1x) - cx_; + ax_ = 1.0 - cx_ - bx_; + + cy_ = 3.0 * p1y; + by_ = 3.0 * (p2y - p1y) - cy_; + ay_ = 1.0 - cy_ - by_; + +#if DEBUG + // Bezier curves with x-coordinates outside the range [0,1] for internal + // control points may have multiple values for t for a given value of x. + // In this case, calls to SolveCurveX may produce ambiguous results. + monotonically_increasing_ = p1x >= 0 && p1x <= 1 && p2x >= 0 && p2x <= 1; +#endif + } + + void InitGradients(double p1x, + double p1y, + double p2x, + double p2y) + { + // End-point gradients are used to calculate timing function results + // outside the range [0, 1]. + // + // There are four possibilities for the gradient at each end: + // (1) the closest control point is not horizontally coincident with regard to + // (0, 0) or (1, 1). In this case the line between the end point and + // the control point is tangent to the bezier at the end point. + // (2) the closest control point is coincident with the end point. In + // this case the line between the end point and the far control + // point is tangent to the bezier at the end point. + // (3) both internal control points are coincident with an endpoint. There + // are two special case that fall into this category: + // CubicBezier(0, 0, 0, 0) and CubicBezier(1, 1, 1, 1). Both are + // equivalent to linear. + // (4) the closest control point is horizontally coincident with the end + // point, but vertically distinct. In this case the gradient at the + // end point is Infinite. However, this causes issues when + // interpolating. As a result, we break down to a simple case of + // 0 gradient under these conditions. + + if (p1x > 0) + start_gradient_ = p1y / p1x; + else if (p1y == 0 && p2x > 0) + start_gradient_ = p2y / p2x; + else if (p1y == 0 && p2y == 0) + start_gradient_ = 1; + else + start_gradient_ = 0; + + if (p2x < 1) + end_gradient_ = (p2y - 1) / (p2x - 1); + else if (p2y == 1 && p1x < 1) + end_gradient_ = (p1y - 1) / (p1x - 1); + else if (p2y == 1 && p1y == 1) + end_gradient_ = 1; + else + end_gradient_ = 0; + } + + const double kBezierEpsilon = 1e-7; + + void InitRange(double p1y, double p2y) + { + range_min_ = 0; + range_max_ = 1; + if (0 <= p1y && p1y < 1 && 0 <= p2y && p2y <= 1) + return; + + double epsilon = kBezierEpsilon; + + // Represent the function's derivative in the form at^2 + bt + c + // as in sampleCurveDerivativeY. + // (Technically this is (dy/dt)*(1/3), which is suitable for finding zeros + // but does not actually give the slope of the curve.) + double a = 3.0 * ay_; + double b = 2.0 * by_; + double c = cy_; + + // Check if the derivative is constant. + if (Math.Abs(a) < epsilon && Math.Abs(b) < epsilon) + return; + + // Zeros of the function's derivative. + double t1; + double t2 = 0; + + if (Math.Abs(a) < epsilon) + { + // The function's derivative is linear. + t1 = -c / b; + } + else + { + // The function's derivative is a quadratic. We find the zeros of this + // quadratic using the quadratic formula. + double discriminant = b * b - 4 * a * c; + if (discriminant < 0) + return; + double discriminant_sqrt = Math.Sqrt(discriminant); + t1 = (-b + discriminant_sqrt) / (2 * a); + t2 = (-b - discriminant_sqrt) / (2 * a); + } + + double sol1 = 0; + double sol2 = 0; + + // If the solution is in the range [0,1] then we include it, otherwise we + // ignore it. + + // An interesting fact about these beziers is that they are only + // actually evaluated in [0,1]. After that we take the tangent at that point + // and linearly project it out. + if (0 < t1 && t1 < 1) + sol1 = SampleCurveY(t1); + + if (0 < t2 && t2 < 1) + sol2 = SampleCurveY(t2); + + range_min_ = Math.Min(Math.Min(range_min_, sol1), sol2); + range_max_ = Math.Max(Math.Max(range_max_, sol1), sol2); + } + + void InitSpline() + { + double delta_t = 1.0 / (CUBIC_BEZIER_SPLINE_SAMPLES - 1); + for (int i = 0; i < CUBIC_BEZIER_SPLINE_SAMPLES; i++) + { + spline_samples_[i] = SampleCurveX(i * delta_t); + } + } + + const int kMaxNewtonIterations = 4; + + + public readonly double SolveCurveX(double x, double epsilon) + { + if (x < 0 || x > 1) + throw new ArgumentException(); + + double t0 = 0; + double t1 = 0; + double t2 = x; + double x2 = 0; + double d2; + int i; + +#if DEBUG + if (!monotonically_increasing_) + throw new InvalidOperationException(); +#endif + + // Linear interpolation of spline curve for initial guess. + double delta_t = 1.0 / (CUBIC_BEZIER_SPLINE_SAMPLES - 1); + for (i = 1; i < CUBIC_BEZIER_SPLINE_SAMPLES; i++) + { + if (x <= spline_samples_[i]) + { + t1 = delta_t * i; + t0 = t1 - delta_t; + t2 = t0 + (t1 - t0) * (x - spline_samples_[i - 1]) / + (spline_samples_[i] - spline_samples_[i - 1]); + break; + } + } + + // Perform a few iterations of Newton's method -- normally very fast. + // See https://en.wikipedia.org/wiki/Newton%27s_method. + double newton_epsilon = Math.Min(kBezierEpsilon, epsilon); + for (i = 0; i < kMaxNewtonIterations; i++) + { + x2 = SampleCurveX(t2) - x; + if (Math.Abs(x2) < newton_epsilon) + return t2; + d2 = SampleCurveDerivativeX(t2); + if (Math.Abs(d2) < kBezierEpsilon) + break; + t2 = t2 - x2 / d2; + } + + if (Math.Abs(x2) < epsilon) + return t2; + + // Fall back to the bisection method for reliability. + while (t0 < t1) + { + x2 = SampleCurveX(t2); + if (Math.Abs(x2 - x) < epsilon) + return t2; + if (x > x2) + t0 = t2; + else + t1 = t2; + t2 = (t1 + t0) * .5; + } + + // Failure. + return t2; + } + + public readonly double Solve(double x) + { + return SolveWithEpsilon(x, kBezierEpsilon); + } + + public readonly double SlopeWithEpsilon(double x, double epsilon) + { + x = MathExt.Clamp(x, 0.0, 1.0); + double t = SolveCurveX(x, epsilon); + double dx = SampleCurveDerivativeX(t); + double dy = SampleCurveDerivativeY(t); + return dy / dx; + } + + public readonly double Slope(double x) + { + return SlopeWithEpsilon(x, kBezierEpsilon); + } + + public readonly double RangeMin => range_min_; + public readonly double RangeMax => range_max_; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Utils/MathExt.cs b/src/Avalonia.Base/Rendering/Composition/Utils/MathExt.cs new file mode 100644 index 0000000000..0be19a8e9d --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Utils/MathExt.cs @@ -0,0 +1,23 @@ +using System; + +namespace Avalonia.Rendering.Composition.Utils +{ + static class MathExt + { + public static float Clamp(float value, float min, float max) + { + var amax = Math.Max(min, max); + var amin = Math.Min(min, max); + return Math.Min(Math.Max(value, amin), amax); + } + + public static double Clamp(double value, double min, double max) + { + var amax = Math.Max(min, max); + var amin = Math.Min(min, max); + return Math.Min(Math.Max(value, amin), amax); + } + + + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Visual.cs b/src/Avalonia.Base/Rendering/Composition/Visual.cs new file mode 100644 index 0000000000..1e6d7f8abb --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Visual.cs @@ -0,0 +1,64 @@ +using System.Numerics; + +namespace Avalonia.Rendering.Composition +{ + public abstract partial class CompositionVisual + { + private CompositionVisual? _parent; + private CompositionTarget? _root; + + public CompositionVisual? Parent + { + get => _parent; + internal set + { + if (_parent == value) + return; + _parent = value; + Changes.Parent.Value = value?.Server; + Root = _parent?.Root; + } + } + + // TODO: hide behind private-ish API + public CompositionTarget? Root + { + get => _root; + internal set + { + var changed = _root != value; + _root = value; + Changes.Root.Value = value?.Server; + if (changed) + OnRootChanged(); + } + } + + private protected virtual void OnRootChanged() + { + } + + + internal Matrix4x4? TryGetServerTransform() + { + if (Root == null) + return null; + var i = Root.Server.Readback; + ref var readback = ref Server.GetReadback(i.ReadIndex); + + // CompositionVisual wasn't visible + if (readback.Revision < i.ReadRevision) + return null; + + // CompositionVisual was reparented (potential race here) + if (readback.TargetId != Root.Server.Id) + return null; + + return readback.Matrix; + } + + internal object? Tag { get; set; } + + internal virtual bool HitTest(Vector2 point) => true; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs new file mode 100644 index 0000000000..35f33c3b38 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs @@ -0,0 +1,64 @@ +using System; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition +{ + public partial class CompositionVisualCollection : CompositionObject + { + private CompositionVisual _owner; + internal CompositionVisualCollection(CompositionVisual parent, ServerCompositionVisualCollection server) : base(parent.Compositor, server) + { + _owner = parent; + InitializeDefaults(); + } + + public void InsertAbove(CompositionVisual newChild, CompositionVisual sibling) + { + var idx = _list.IndexOf(sibling); + if (idx == -1) + throw new InvalidOperationException(); + + Insert(idx + 1, newChild); + } + + public void InsertBelow(CompositionVisual newChild, CompositionVisual sibling) + { + var idx = _list.IndexOf(sibling); + if (idx == -1) + throw new InvalidOperationException(); + Insert(idx, newChild); + } + + public void InsertAtTop(CompositionVisual newChild) => Insert(_list.Count, newChild); + + public void InsertAtBottom(CompositionVisual newChild) => Insert(0, newChild); + + public void RemoveAll() => Clear(); + + partial void OnAdded(CompositionVisual item) => item.Parent = _owner; + + partial void OnBeforeReplace(CompositionVisual oldItem, CompositionVisual newItem) + { + if (oldItem != newItem) + OnBeforeAdded(newItem); + } + + partial void OnReplace(CompositionVisual oldItem, CompositionVisual newItem) + { + if (oldItem != newItem) + { + OnRemoved(oldItem); + OnAdded(newItem); + } + } + + partial void OnRemoved(CompositionVisual item) => item.Parent = null; + + + partial void OnBeforeAdded(CompositionVisual item) + { + if (item.Parent != null) + throw new InvalidOperationException("Visual already has a parent"); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/IRenderer.cs b/src/Avalonia.Base/Rendering/IRenderer.cs index e998f78d5c..8d6aabf440 100644 --- a/src/Avalonia.Base/Rendering/IRenderer.cs +++ b/src/Avalonia.Base/Rendering/IRenderer.cs @@ -1,6 +1,7 @@ using System; using Avalonia.VisualTree; using System.Collections.Generic; +using Avalonia.Rendering.Composition; namespace Avalonia.Rendering { @@ -87,4 +88,9 @@ namespace Avalonia.Rendering /// void Stop(); } + + public interface IRendererWithCompositor : IRenderer + { + Compositor Compositor { get; } + } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 5225b85020..3f495c619c 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Numerics; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Utilities; diff --git a/src/Avalonia.Base/Size.cs b/src/Avalonia.Base/Size.cs index 69c3ae7319..5f20206200 100644 --- a/src/Avalonia.Base/Size.cs +++ b/src/Avalonia.Base/Size.cs @@ -52,6 +52,17 @@ namespace Avalonia _width = width; _height = height; } + +#if !BUILDTASK + /// + /// Initializes a new instance of the structure. + /// + /// The vector to take values from. + public Size(System.Numerics.Vector2 vector2) : this(vector2.X, vector2.Y) + { + + } +#endif /// /// Gets the aspect ratio of the size. diff --git a/src/Avalonia.Base/Threading/DispatcherPriority.cs b/src/Avalonia.Base/Threading/DispatcherPriority.cs index a93e4f406d..b4bf603f74 100644 --- a/src/Avalonia.Base/Threading/DispatcherPriority.cs +++ b/src/Avalonia.Base/Threading/DispatcherPriority.cs @@ -62,10 +62,20 @@ namespace Avalonia.Threading /// public static readonly DispatcherPriority Render = new(5); + /// + /// The job will be processed with the same priority as composition batch commit. + /// + public static readonly DispatcherPriority CompositionBatch = new(6); + + /// + /// The job will be processed with the same priority as composition updates. + /// + public static readonly DispatcherPriority Composition = new(7); + /// /// The job will be processed with the same priority as render. /// - public static readonly DispatcherPriority Layout = new(6); + public static readonly DispatcherPriority Layout = new(8); /// /// The job will be processed with the same priority as data binding. @@ -75,7 +85,7 @@ namespace Avalonia.Threading /// /// The job will be processed before other asynchronous operations. /// - public static readonly DispatcherPriority Send = new(7); + public static readonly DispatcherPriority Send = new(9); /// /// Maximum possible priority diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 4fd21f02f9..64341be0c7 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -1,3 +1,7 @@ + + +#nullable enable + using System; using System.Collections; using System.Collections.Specialized; @@ -8,11 +12,10 @@ using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Metadata; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Server; using Avalonia.Utilities; using Avalonia.VisualTree; - -#nullable enable - namespace Avalonia { /// @@ -288,6 +291,8 @@ namespace Avalonia /// protected IRenderRoot? VisualRoot => _visualRoot ?? (this as IRenderRoot); + internal CompositionDrawListVisual? CompositionVisual { get; private set; } + /// /// Gets a value indicating whether this control is attached to a visual root. /// @@ -432,6 +437,10 @@ namespace Avalonia } EnableTransitions(); + if (_visualRoot.Renderer is IRendererWithCompositor compositingRenderer) + { + AttachToCompositor(compositingRenderer.Compositor); + } OnAttachedToVisualTree(e); AttachedToVisualTree?.Invoke(this, e); InvalidateVisual(); @@ -452,6 +461,14 @@ namespace Avalonia } } + internal CompositionVisual AttachToCompositor(Compositor compositor) + { + if (CompositionVisual == null || CompositionVisual.Compositor != compositor) + CompositionVisual = new CompositionDrawListVisual(compositor, + new ServerCompositionDrawListVisual(compositor.Server), this); + return CompositionVisual; + } + /// /// Calls the method /// for this control and all of its visual descendants. @@ -564,7 +581,7 @@ namespace Avalonia { newValue.Changed += sender.RenderTransformChanged; } - + sender.InvalidateVisual(); } } diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml new file mode 100644 index 0000000000..cf864dac2d --- /dev/null +++ b/src/Avalonia.Base/composition-schema.xml @@ -0,0 +1,70 @@ + + + System.Numerics + Avalonia.Rendering.Composition.Server + Avalonia.Rendering.Composition.Transport + Avalonia.Rendering.Composition.Animations + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index addc248d58..984f76adb6 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Numerics; using System.Runtime.InteropServices; using Avalonia.Media; using Avalonia.Platform; @@ -344,6 +345,7 @@ namespace Avalonia.Headless } public Matrix Transform { get; set; } + public void Clear(Color color) { diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs new file mode 100644 index 0000000000..f079a339df --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs @@ -0,0 +1,21 @@ +using System.IO; +using System.Xml.Serialization; +using Microsoft.CodeAnalysis; + +namespace Avalonia.SourceGenerator.CompositionGenerator; + +[Generator(LanguageNames.CSharp)] +public class CompositionRoslynGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var schema = context.AdditionalTextsProvider.Where(static file => file.Path.EndsWith("composition-schema.xml")); + var configs = schema.Select((t, _) => + (GConfig)new XmlSerializer(typeof(GConfig)).Deserialize(new StringReader(t.GetText().ToString()))); + context.RegisterSourceOutput(configs, (spc, config) => + { + var generator = new Generator(spc, config); + generator.Generate(); + }); + } +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs new file mode 100644 index 0000000000..8b6aca33cd --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs @@ -0,0 +1,116 @@ +#nullable disable +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + + + [XmlRoot("NComposition")] + public class GConfig + { + [XmlElement("Using")] + public List Usings { get; set; } = new List(); + + [XmlElement(typeof(GManualClass), ElementName = "Manual")] + public List ManualClasses { get; set; } = new List(); + + [XmlElement(typeof(GClass), ElementName = "Object")] + [XmlElement(typeof(GBrush), ElementName = "Brush")] + [XmlElement(typeof(GList), ElementName = "List")] + public List Classes { get; set; } = new List(); + + [XmlElement(typeof(GAnimationType), ElementName = "KeyFrameAnimation")] + public List KeyFrameAnimations { get; set; } = new List(); + } + + public class GUsing + { + [XmlText] + public string Name { get; set; } + } + + public class GManualClass + { + [XmlAttribute] + public string Name { get; set; } + + [XmlAttribute] + public string ServerName { get; set; } + } + + public class GImplements + { + [XmlAttribute] + public string Name { get; set; } + [XmlAttribute] + public string ServerName { get; set; } + } + + public class GClass + { + [XmlAttribute] + public string Name { get; set; } + + [XmlAttribute] + public string Inherits { get; set; } + + [XmlAttribute] + public string ChangesBase { get; set; } + + [XmlAttribute] + public string ServerBase { get; set; } + + [XmlAttribute] + public bool CustomCtor { get; set; } + + [XmlAttribute] + public bool CustomServerCtor { get; set; } + + [XmlElement(typeof(GImplements), ElementName = "Implements")] + public List Implements { get; set; } = new List(); + + [XmlAttribute] + public bool Abstract { get; set; } + + [XmlElement(typeof(GProperty), ElementName = "Property")] + public List Properties { get; set; } = new List(); + } + + public class GBrush : GClass + { + [XmlAttribute] + public bool CustomUpdate { get; set; } + + public GBrush() + { + Inherits = "CompositionBrush"; + } + } + + public class GList : GClass + { + [XmlAttribute] + public string ItemType { get; set; } + } + + public class GProperty + { + [XmlAttribute] + public string Name { get; set; } + [XmlAttribute] + public string Type { get; set; } + [XmlAttribute] + public string DefaultValue { get; set; } + [XmlAttribute] + public bool Animated { get; set; } + } + + public class GAnimationType + { + [XmlAttribute] + public string Name { get; set; } + [XmlAttribute] + public string Type { get; set; } + } +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs new file mode 100644 index 0000000000..43a4a4afa7 --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs @@ -0,0 +1,90 @@ +using System; +using System.Linq; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + public static class Extensions + { + public static ClassDeclarationSyntax AddModifiers(this ClassDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static MethodDeclarationSyntax AddModifiers(this MethodDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static PropertyDeclarationSyntax AddModifiers(this PropertyDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static ConstructorDeclarationSyntax AddModifiers(this ConstructorDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static AccessorDeclarationSyntax AddModifiers(this AccessorDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static string WithLowerFirst(this string s) + { + if (string.IsNullOrEmpty(s)) + return s; + return char.ToLowerInvariant(s[0]) + s.Substring(1); + } + + public static ExpressionSyntax MemberAccess(params string[] identifiers) + { + if (identifiers == null || identifiers.Length == 0) + throw new ArgumentException(); + var expr = (ExpressionSyntax)IdentifierName(identifiers[0]); + for (var c = 1; c < identifiers.Length; c++) + expr = MemberAccess(expr, identifiers[c]); + return expr; + } + + public static ExpressionSyntax MemberAccess(ExpressionSyntax expr, params string[] identifiers) + { + foreach (var i in identifiers) + expr = MemberAccess(expr, i); + return expr; + } + + public static MemberAccessExpressionSyntax MemberAccess(ExpressionSyntax expr, string identifier) => + MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, expr, IdentifierName(identifier)); + + public static ExpressionSyntax ConditionalMemberAccess(ExpressionSyntax expr, string member, bool checkNull) + { + if (checkNull) + return ConditionalAccessExpression(expr, MemberBindingExpression(IdentifierName(member))); + return MemberAccess(expr, member); + } + + public static ClassDeclarationSyntax WithBaseType(this ClassDeclarationSyntax cl, string bt) + { + return cl.AddBaseListTypes(SimpleBaseType(SyntaxFactory.ParseTypeName(bt))); + } + + public static string StripPrefix(this string s, string prefix) => string.IsNullOrEmpty(s) + ? s + : s.StartsWith(prefix) + ? s.Substring(prefix.Length) + : s; + } +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs new file mode 100644 index 0000000000..7d5146c5f5 --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs @@ -0,0 +1,59 @@ +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + public partial class Generator + { + void GenerateAnimations() + { + var code = $@"using System.Numerics; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Expressions; + +namespace Avalonia.Rendering.Composition +{{ +"; + + foreach (var a in _config.KeyFrameAnimations) + { + var name = a.Name ?? a.Type; + + code += $@" + public class {name}KeyFrameAnimation : KeyFrameAnimation + {{ + public {name}KeyFrameAnimation(Compositor compositor) : base(compositor) + {{ + }} + + internal override IAnimationInstance CreateInstance(Avalonia.Rendering.Composition.Server.ServerObject targetObject, ExpressionVariant? finalValue) + {{ + return new KeyFrameAnimationInstance<{a.Type}>({name}Interpolator.Instance, _keyFrames.Snapshot(), CreateSnapshot(true), + finalValue?.CastOrDefault<{a.Type}>(), targetObject, + DelayBehavior, DelayTime, Direction, Duration, IterationBehavior, + IterationCount, StopBehavior); + }} + + private KeyFrames<{a.Type}> _keyFrames = new KeyFrames<{a.Type}>(); + private protected override IKeyFrames KeyFrames => _keyFrames; + + public void InsertKeyFrame(float normalizedProgressKey, {a.Type} value, CompositionEasingFunction easingFunction) + {{ + _keyFrames.Insert(normalizedProgressKey, value, easingFunction); + }} + + public void InsertKeyFrame(float normalizedProgressKey, {a.Type} value) + {{ + _keyFrames.Insert(normalizedProgressKey, value, Compositor.DefaultEasing); + }} + }} + + public partial class Compositor + {{ + public {name}KeyFrameAnimation Create{name}KeyFrameAnimation() => new {name}KeyFrameAnimation(this); + }} +"; + } + + code += "}"; + _output.AddSource("CompositionAnimations.cs", code); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs new file mode 100644 index 0000000000..593386f713 --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs @@ -0,0 +1,121 @@ +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + public partial class Generator + { + private const string ListProxyTemplate = @" +class Template +{ + private ServerListProxyHelper _list = null!; + + ListChangeSet + ServerListProxyHelper.IGetChanges. + GetChanges() => Changes; + + public List.Enumerator GetEnumerator() => _list.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => + GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable) _list).GetEnumerator(); + + public void Add(ItemTypeName item) + { + OnBeforeAdded(item); + _list.Add(item); + OnAdded(item); + } + + public void Clear() + { + OnBeforeClear(); + _list.Clear(); + OnClear(); + } + + public bool Contains(ItemTypeName item) => _list.Contains(item); + + public void CopyTo(ItemTypeName[] array, int arrayIndex) => _list.CopyTo(array, arrayIndex); + + public bool Remove(ItemTypeName item) + { + var removed = _list.Remove(item); + if(removed) + OnRemoved(item); + return removed; + } + + public int Count => _list.Count; + + public bool IsReadOnly => _list.IsReadOnly; + + public int IndexOf(ItemTypeName item) => _list.IndexOf(item); + + public void Insert(int index, ItemTypeName item) + { + OnBeforeAdded(item); + _list.Insert(index, item); + OnAdded(item); + } + + public void RemoveAt(int index) + { + var item = _list[index]; + _list.RemoveAt(index); + OnRemoved(item); + } + + public ItemTypeName this[int index] + { + get => _list[index]; + set + { + var old = _list[index]; + OnBeforeReplace(old, value); + _list[index] = value; + OnReplace(old, value); + } + } + + partial void OnBeforeAdded(ItemTypeName item); + partial void OnAdded(ItemTypeName item); + partial void OnRemoved(ItemTypeName item); + partial void OnBeforeClear(); + partial void OnBeforeReplace(ItemTypeName oldItem, ItemTypeName newItem); + partial void OnReplace(ItemTypeName oldItem, ItemTypeName newItem); + partial void OnClear(); +} +"; + + private ClassDeclarationSyntax AppendListProxy(GList list, ClassDeclarationSyntax cl) + { + + var itemType = list.ItemType; + var serverItemType = ServerName(itemType); + + cl = cl.AddBaseListTypes(SimpleBaseType( + ParseTypeName("ServerListProxyHelper<" + itemType + ", " + serverItemType + ">.IGetChanges")), + SimpleBaseType(ParseTypeName("IList<" + itemType + ">")) + ); + var code = ListProxyTemplate.Replace("ListTypeName", list.Name) + .Replace("ItemTypeName", itemType); + + var parsed = ParseCompilationUnit(code); + var parsedClass = (ClassDeclarationSyntax)parsed.Members.First(); + + cl = cl.AddMembers(parsedClass.Members.ToArray()); + + var defs = cl.Members.OfType().First(m => m.Identifier.Text == "InitializeDefaults"); + + cl = cl.ReplaceNode(defs.Body, defs.Body.AddStatements( + + ParseStatement($"_list = new ServerListProxyHelper<{itemType}, {serverItemType}>(this);"))); + + return cl; + } + + } +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.Utils.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.Utils.cs new file mode 100644 index 0000000000..b53c247991 --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.Utils.cs @@ -0,0 +1,66 @@ +using System.IO; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + public partial class Generator + { + static void CleanDirectory(string path) + { + Directory.CreateDirectory(path); + Directory.Delete(path, true); + Directory.CreateDirectory(path); + } + + CompilationUnitSyntax Unit() + => CompilationUnit().WithUsings(List(new[] + { + "System", + "System.Text", + "System.Collections", + "System.Collections.Generic" + } + .Concat(_config.Usings + .Select(x => x.Name)).Select(u => UsingDirective(IdentifierName(u))))); + + void SaveTo(CompilationUnitSyntax unit, params string[] path) + { + var text = @" +#nullable enable +#pragma warning disable CS0108, CS0114 + +" + + + unit.NormalizeWhitespace().ToFullString(); + _output.AddSource(string.Join("_", path), text); + } + + + SyntaxToken Semicolon() => Token(SyntaxKind.SemicolonToken); + + + FieldDeclarationSyntax DeclareConstant(string type, string name, LiteralExpressionSyntax value) + => FieldDeclaration( + VariableDeclaration(ParseTypeName(type), + SingletonSeparatedList( + VariableDeclarator(name).WithInitializer(EqualsValueClause(value)) + )) + ).WithSemicolonToken(Semicolon()) + .WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.ConstKeyword))); + + FieldDeclarationSyntax DeclareField(string type, string name, params SyntaxKind[] modifiers) => + DeclareField(type, name, null, modifiers); + + FieldDeclarationSyntax DeclareField(string type, string name, EqualsValueClauseSyntax initializer, + params SyntaxKind[] modifiers) => + FieldDeclaration( + VariableDeclaration(ParseTypeName(type), + SingletonSeparatedList( + VariableDeclarator(name).WithInitializer(initializer)))) + .WithSemicolonToken(Semicolon()) + .WithModifiers(TokenList(modifiers.Select(x => Token(x)))); + } +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs new file mode 100644 index 0000000000..43ef4a96e8 --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs @@ -0,0 +1,504 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static Avalonia.SourceGenerator.CompositionGenerator.Extensions; +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + partial class Generator + { + private readonly SourceProductionContext _output; + private readonly GConfig _config; + private readonly HashSet _objects; + private readonly HashSet _brushes; + private readonly Dictionary _manuals; + public Generator(SourceProductionContext output, GConfig config) + { + _output = output; + _config = config; + _manuals = _config.ManualClasses.ToDictionary(x => x.Name); + _objects = new HashSet(_config.ManualClasses.Select(x => x.Name) + .Concat(_config.Classes.Select(x => x.Name))); + _brushes = new HashSet(_config.Classes.OfType().Select(x => x.Name)) {"CompositionBrush"}; + } + + + + public void Generate() + { + foreach (var cl in _config.Classes) + GenerateClass(cl); + + GenerateAnimations(); + } + + + + string ServerName(string c) => c != null ? ("Server" + c) : "ServerObject"; + string ChangesName(string c) => c != null ? (c + "Changes") : "ChangeSet"; + + void GenerateClass(GClass cl) + { + var list = cl as GList; + + var unit = Unit(); + + var clientNs = NamespaceDeclaration(IdentifierName("Avalonia.Rendering.Composition")); + var serverNs = NamespaceDeclaration(IdentifierName("Avalonia.Rendering.Composition.Server")); + var transportNs = NamespaceDeclaration(IdentifierName("Avalonia.Rendering.Composition.Transport")); + + var inherits = cl.Inherits ?? "CompositionObject"; + var abstractModifier = cl.Abstract ? new[] {SyntaxKind.AbstractKeyword} : null; + + var client = ClassDeclaration(cl.Name) + .AddModifiers(abstractModifier) + .AddModifiers(SyntaxKind.PublicKeyword, SyntaxKind.UnsafeKeyword, SyntaxKind.PartialKeyword) + .WithBaseType(inherits); + + var serverName = ServerName(cl.Name); + var serverBase = cl.ServerBase ?? ServerName(cl.Inherits); + if (list != null) + serverBase = "ServerList<" + ServerName(list.ItemType) + ">"; + + var server = ClassDeclaration(serverName) + .AddModifiers(abstractModifier) + .AddModifiers(SyntaxKind.UnsafeKeyword, SyntaxKind.PartialKeyword) + .WithBaseType(serverBase); + + string changesName = ChangesName(cl.Name); + var changesBase = ChangesName(cl.ChangesBase ?? cl.Inherits); + + if (list != null) + changesBase = "ListChangeSet<" + ServerName(list.ItemType) + ">"; + + var changeSetPoolType = "ChangeSetPool<" + changesName + ">"; + var transport = ClassDeclaration(changesName) + .AddModifiers(SyntaxKind.UnsafeKeyword, SyntaxKind.PartialKeyword) + .WithBaseType(changesBase) + .AddMembers(DeclareField(changeSetPoolType, "Pool", + EqualsValueClause( + ParseExpression($"new {changeSetPoolType}(pool => new {changesName}(pool))") + ), + SyntaxKind.PublicKeyword, + SyntaxKind.StaticKeyword, SyntaxKind.ReadOnlyKeyword)) + .AddMembers(ParseMemberDeclaration($"public {changesName}(IChangeSetPool pool) : base(pool){{}}")); + + client = client + .AddMembers( + PropertyDeclaration(ParseTypeName("IChangeSetPool"), "ChangeSetPool") + .AddModifiers(SyntaxKind.PrivateKeyword, SyntaxKind.ProtectedKeyword, + SyntaxKind.OverrideKeyword) + .WithExpressionBody( + ArrowExpressionClause(MemberAccess(changesName, "Pool"))) + .WithSemicolonToken(Semicolon())) + .AddMembers(PropertyDeclaration(ParseTypeName(changesName), "Changes") + .AddModifiers(SyntaxKind.PrivateKeyword, SyntaxKind.NewKeyword) + .WithExpressionBody(ArrowExpressionClause(CastExpression(ParseTypeName(changesName), + MemberAccess(BaseExpression(), "Changes")))) + .WithSemicolonToken(Semicolon())); + + if (!cl.CustomCtor) + { + client = client.AddMembers(PropertyDeclaration(ParseTypeName(serverName), "Server") + .AddModifiers(SyntaxKind.InternalKeyword, SyntaxKind.NewKeyword) + .AddAccessorListAccessors(AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) + .WithSemicolonToken(Semicolon()))); + client = client.AddMembers( + ConstructorDeclaration(cl.Name) + .AddModifiers(SyntaxKind.InternalKeyword) + .WithParameterList(ParameterList(SeparatedList(new[] + { + Parameter(Identifier("compositor")).WithType(ParseTypeName("Compositor")), + Parameter(Identifier("server")).WithType(ParseTypeName(serverName)), + }))) + .WithInitializer(ConstructorInitializer(SyntaxKind.BaseConstructorInitializer, + ArgumentList(SeparatedList(new[] + { + Argument(IdentifierName("compositor")), + Argument(IdentifierName("server")), + })))).WithBody(Block( + ExpressionStatement( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + IdentifierName("Server"), + CastExpression(ParseTypeName(serverName), IdentifierName("server")))), + ExpressionStatement(InvocationExpression(IdentifierName("InitializeDefaults"))) + ))); + } + + if (!cl.CustomServerCtor) + { + server = server.AddMembers( + ConstructorDeclaration(serverName) + .AddModifiers(SyntaxKind.InternalKeyword) + .WithParameterList(ParameterList(SeparatedList(new[] + { + Parameter(Identifier("compositor")).WithType(ParseTypeName("ServerCompositor")), + }))) + .WithInitializer(ConstructorInitializer(SyntaxKind.BaseConstructorInitializer, + ArgumentList(SeparatedList(new[] + { + Argument(IdentifierName("compositor")), + })))).WithBody(Block())); + } + + + var changesVarName = "c"; + var changesVar = IdentifierName(changesVarName); + + server = server.AddMembers( + MethodDeclaration(ParseTypeName("void"), "ApplyChangesExtra") + .AddParameterListParameters(Parameter(Identifier("c")).WithType(ParseTypeName(changesName))) + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + + transport = transport.AddMembers( + MethodDeclaration(ParseTypeName("void"), "ResetExtra") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + + var applyMethodBody = Block( + ExpressionStatement(InvocationExpression(MemberAccess(IdentifierName("base"), "ApplyCore"), + ArgumentList(SeparatedList(new[] {Argument(IdentifierName("changes"))})))), + LocalDeclarationStatement(VariableDeclaration(ParseTypeName("var")) + .WithVariables(SingletonSeparatedList( + VariableDeclarator(changesVarName) + .WithInitializer(EqualsValueClause(CastExpression(ParseTypeName(changesName), + IdentifierName("changes"))))))), + ExpressionStatement(InvocationExpression(IdentifierName("ApplyChangesExtra")) + .AddArgumentListArguments(Argument(IdentifierName("c")))) + ); + + var resetBody = Block(); + var startAnimationBody = Block(); + var getPropertyBody = Block(); + var serverGetPropertyBody = Block(); + + var defaultsMethodBody = Block(); + + foreach (var prop in cl.Properties) + { + var fieldName = "_" + prop.Name.WithLowerFirst(); + var propType = ParseTypeName(prop.Type); + var filteredPropertyType = prop.Type.TrimEnd('?'); + var isObject = _objects.Contains(filteredPropertyType); + var isNullable = prop.Type.EndsWith("?"); + + + + + client = client + .AddMembers(DeclareField(prop.Type, fieldName)) + .AddMembers(PropertyDeclaration(propType, prop.Name) + .AddModifiers(SyntaxKind.PublicKeyword) + .AddAccessorListAccessors( + AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, + Block(ReturnStatement(IdentifierName(fieldName)))), + AccessorDeclaration(SyntaxKind.SetAccessorDeclaration, + Block( + ParseStatement("var changed = false;"), + IfStatement(BinaryExpression(SyntaxKind.NotEqualsExpression, + IdentifierName(fieldName), + IdentifierName("value")), + Block( + ParseStatement("On" + prop.Name + "Changing();"), + ParseStatement("changed = true;"), + GeneratePropertySetterAssignment(prop, fieldName, isObject, isNullable)) + ), + ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + IdentifierName(fieldName), IdentifierName("value"))), + ParseStatement($"if(changed) On" + prop.Name + "Changed();") + )) + )) + .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changed") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())) + .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changing") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + + + var animatedServer = prop.Animated; + + var serverPropertyType = ((isObject ? "Server" : "") + prop.Type); + if (_manuals.TryGetValue(filteredPropertyType, out var manual) && manual.ServerName != null) + serverPropertyType = manual.ServerName + (isNullable ? "?" : ""); + + + transport = transport + .AddMembers(DeclareField((animatedServer ? "Animated" : "") + "Change<" + serverPropertyType + ">", + prop.Name, SyntaxKind.PublicKeyword)); + + if (animatedServer) + server = server.AddMembers( + DeclareField("AnimatedValueStore<" + serverPropertyType + ">", fieldName), + PropertyDeclaration(ParseTypeName(serverPropertyType), prop.Name) + .AddModifiers(SyntaxKind.PublicKeyword) + .WithExpressionBody(ArrowExpressionClause( + InvocationExpression(MemberAccess(fieldName, "GetAnimated"), + ArgumentList(SingletonSeparatedList(Argument(IdentifierName("Compositor"))))))) + .WithSemicolonToken(Semicolon()) + ); + else + { + server = server + .AddMembers(DeclareField(serverPropertyType, fieldName)) + .AddMembers(PropertyDeclaration(ParseTypeName(serverPropertyType), prop.Name) + .AddModifiers(SyntaxKind.PublicKeyword) + .AddAccessorListAccessors( + AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, + Block(ReturnStatement(IdentifierName(fieldName)))), + AccessorDeclaration(SyntaxKind.SetAccessorDeclaration, + Block( + ParseStatement("var changed = false;"), + IfStatement(BinaryExpression(SyntaxKind.NotEqualsExpression, + IdentifierName(fieldName), + IdentifierName("value")), + Block( + ParseStatement("On" + prop.Name + "Changing();"), + ParseStatement($"changed = true;")) + ), + ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + IdentifierName(fieldName), IdentifierName("value"))), + ParseStatement($"if(changed) On" + prop.Name + "Changed();") + )) + )) + .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changed") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())) + .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changing") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + } + + if (animatedServer) + applyMethodBody = applyMethodBody.AddStatements( + IfStatement(MemberAccess(changesVar, prop.Name, "IsValue"), + ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + IdentifierName(fieldName), MemberAccess(changesVar, prop.Name, "Value")))), + IfStatement(MemberAccess(changesVar, prop.Name, "IsAnimation"), + ExpressionStatement( + InvocationExpression(MemberAccess(fieldName, "SetAnimation"), + ArgumentList(SeparatedList(new[] + { + Argument(changesVar), + Argument(MemberAccess(changesVar, prop.Name, "Animation")) + }))))) + ); + else + applyMethodBody = applyMethodBody.AddStatements( + IfStatement(MemberAccess(changesVar, prop.Name, "IsSet"), + ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + IdentifierName(prop.Name), MemberAccess(changesVar, prop.Name, "Value")))) + + ); + + + resetBody = resetBody.AddStatements( + ExpressionStatement(InvocationExpression(MemberAccess(prop.Name, "Reset")))); + + if (animatedServer) + startAnimationBody = ApplyStartAnimation(startAnimationBody, prop, fieldName); + + getPropertyBody = ApplyGetProperty(getPropertyBody, prop); + serverGetPropertyBody = ApplyGetProperty(getPropertyBody, prop); + + if (prop.DefaultValue != null) + { + defaultsMethodBody = defaultsMethodBody.AddStatements( + ExpressionStatement( + AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + IdentifierName(prop.Name), ParseExpression(prop.DefaultValue)))); + } + } + + if (cl is GBrush brush && !cl.Abstract) + { + var brushName = brush.Name.StripPrefix("Composition"); + /* + server = server.AddMembers( + MethodDeclaration(ParseTypeName("ICbBrush"), "CreateBackendBrush") + .AddModifiers(SyntaxKind.ProtectedKeyword, SyntaxKind.OverrideKeyword) + .WithExpressionBody(ArrowExpressionClause( + InvocationExpression(MemberAccess("Compositor", "Backend", "Create" + brushName)) + )).WithSemicolonToken(Semicolon()) + ); + if (!brush.CustomUpdate) + server = server.AddMembers( + MethodDeclaration(ParseTypeName("void"), "UpdateBackendBrush") + .AddModifiers(SyntaxKind.ProtectedKeyword, SyntaxKind.OverrideKeyword) + .AddParameterListParameters(Parameter(Identifier("brush")) + .WithType(ParseTypeName("ICbBrush"))) + .AddBodyStatements( + ExpressionStatement( + InvocationExpression( + MemberAccess( + ParenthesizedExpression( + CastExpression(ParseTypeName("ICb" + brushName), IdentifierName("brush"))), "Update"), + ArgumentList(SeparatedList(cl.Properties.Select(x => + { + if(x.Type.TrimEnd('?') == "ICompositionSurface") + return Argument( + ConditionalAccessExpression(IdentifierName(x.Name), + MemberBindingExpression(IdentifierName("BackendSurface"))) + ); + if (_brushes.Contains(x.Type)) + return Argument( + ConditionalAccessExpression(IdentifierName(x.Name), + MemberBindingExpression(IdentifierName("Brush"))) + ); + return Argument(IdentifierName(x.Name)); + })))) + ))); + +*/ + } + + server = server.AddMembers( + MethodDeclaration(ParseTypeName("void"), "ApplyCore") + .AddModifiers(SyntaxKind.ProtectedKeyword, SyntaxKind.OverrideKeyword) + .AddParameterListParameters( + Parameter(Identifier("changes")).WithType(ParseTypeName("ChangeSet"))) + .WithBody(applyMethodBody)); + + client = client.AddMembers( + MethodDeclaration(ParseTypeName("void"), "InitializeDefaults").WithBody(defaultsMethodBody)); + + transport = transport.AddMembers(MethodDeclaration(ParseTypeName("void"), "Reset") + .AddModifiers(SyntaxKind.PublicKeyword, SyntaxKind.OverrideKeyword) + .WithBody(resetBody.AddStatements( + ExpressionStatement(InvocationExpression(IdentifierName("ResetExtra"))), + ExpressionStatement(InvocationExpression(MemberAccess("base", "Reset")))))); + + if (list != null) + client = AppendListProxy(list, client); + + if (startAnimationBody.Statements.Count != 0) + client = WithStartAnimation(client, startAnimationBody); + + client = WithGetProperty(client, getPropertyBody, false); + server = WithGetProperty(server, serverGetPropertyBody, true); + + if(cl.Implements.Count > 0) + foreach (var impl in cl.Implements) + { + client = client.WithBaseList(client.BaseList.AddTypes(SimpleBaseType(ParseTypeName(impl.Name)))); + if (impl.ServerName != null) + server = server.WithBaseList( + server.BaseList.AddTypes(SimpleBaseType(ParseTypeName(impl.ServerName)))); + + client = client.AddMembers( + ParseMemberDeclaration($"{impl.ServerName} {impl.Name}.Server => Server;")); + } + + + SaveTo(unit.AddMembers(clientNs.AddMembers(client)), + cl.Name + ".generated.cs"); + SaveTo(unit.AddMembers(serverNs.AddMembers(server)), + "Server", "Server" + cl.Name + ".generated.cs"); + SaveTo(unit.AddMembers(transportNs.AddMembers(transport)), + "Transport", cl.Name + "Changes.generated.cs"); + } + + StatementSyntax GeneratePropertySetterAssignment(GProperty prop, string fieldName, bool isObject, bool isNullable) + { + var normalChangesAssignment = (StatementSyntax)ExpressionStatement(AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + MemberAccess((ExpressionSyntax) IdentifierName("Changes"), prop.Name, + "Value"), + isObject + ? + ConditionalMemberAccess(IdentifierName("value"), "Server", isNullable) + : IdentifierName("value"))); + if (!prop.Animated) + return normalChangesAssignment; + + var code = $@" +{{ + if(animation is CompositionAnimation a) + Changes.{prop.Name}.Animation = a.CreateInstance(this.Server, value); + else + {{ + var saved = Changes.{prop.Name}; + if(!StartAnimationGroup(animation, ""{prop.Name}"", value)) + Changes.{prop.Name}.Value = value; + }} +}} + +"; + + return IfStatement( + ParseExpression( + $"ImplicitAnimations != null && ImplicitAnimations.TryGetValue(\"{prop.Name}\", out var animation) == true"), + ParseStatement(code), + ElseClause(normalChangesAssignment) + ); + } + + BlockSyntax ApplyStartAnimation(BlockSyntax body, GProperty prop, string fieldName) + { + var code = $@" +if (propertyName == ""{prop.Name}"") +{{ +var current = {fieldName}; +var server = animation.CreateInstance(this.Server, finalValue); +Changes.{prop.Name}.Animation = server; +return; +}} +"; + return body.AddStatements(ParseStatement(code)); + } + + private static HashSet VariantPropertyTypes = new HashSet + { + "bool", + "float", + "Vector2", + "Vector3", + "Vector4", + "Matrix3x2", + "Matrix4x4", + "Quaternion", + "CompositionColor" + }; + + BlockSyntax ApplyGetProperty(BlockSyntax body, GProperty prop) + { + if (VariantPropertyTypes.Contains(prop.Type)) + return body.AddStatements( + ParseStatement($"if(name == \"{prop.Name}\")\n return {prop.Name};\n") + ); + + return body; + } + + ClassDeclarationSyntax WithGetProperty(ClassDeclarationSyntax cl, BlockSyntax body, bool server) + { + if (body.Statements.Count == 0) + return cl; + body = body.AddStatements( + ParseStatement("return base.GetPropertyForAnimation(name);")); + var method = ((MethodDeclarationSyntax) ParseMemberDeclaration( + $"{(server ? "public" : "internal")} override Avalonia.Rendering.Composition.Expressions.ExpressionVariant GetPropertyForAnimation(string name){{}}")) + .WithBody(body); + + return cl.AddMembers(method); + } + + ClassDeclarationSyntax WithStartAnimation(ClassDeclarationSyntax cl, BlockSyntax body) + { + body = body.AddStatements( + ExpressionStatement(InvocationExpression(MemberAccess("base", "StartAnimation"), + ArgumentList(SeparatedList(new[] + { + Argument(IdentifierName("propertyName")), + Argument(IdentifierName("animation")), + Argument(IdentifierName("finalValue")), + })))) + ); + return cl.AddMembers( + ((MethodDeclarationSyntax) ParseMemberDeclaration( + "internal override void StartAnimation(string propertyName, CompositionAnimation animation, Avalonia.Rendering.Composition.Expressions.ExpressionVariant? finalValue){}")) + .WithBody(body)); + + + } + + } +} \ No newline at end of file diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index ec3f29c806..7a64b39575 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -13,6 +13,7 @@ using Avalonia.OpenGL; using Avalonia.OpenGL.Egl; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Avalonia.X11; using Avalonia.X11.Glx; using Avalonia.X11.NativeDialogs; @@ -29,6 +30,7 @@ namespace Avalonia.X11 public XI2Manager XI2; public X11Info Info { get; private set; } public IX11Screens X11Screens { get; private set; } + public Compositor Compositor { get; private set; } public IScreenImpl Screens { get; private set; } public X11PlatformOptions Options { get; private set; } public IntPtr OrphanedWindow { get; private set; } @@ -101,7 +103,9 @@ namespace Avalonia.X11 GlxPlatformOpenGlInterface.TryInitialize(Info, Options.GlProfiles); } - + if (options.UseCompositor) + Compositor = Compositor.Create(AvaloniaLocator.Current.GetService()!); + } public IntPtr DeferredDisplay { get; set; } @@ -222,6 +226,8 @@ namespace Avalonia /// Immediate re-renders the whole scene when some element is changed on the scene. Deferred re-renders only changed elements. /// public bool UseDeferredRendering { get; set; } = true; + + public bool UseCompositor { get; set; } /// /// Determines whether to use IME. diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 066156a652..37cfeb0624 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -18,6 +18,7 @@ using Avalonia.OpenGL; using Avalonia.OpenGL.Egl; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Avalonia.Threading; using Avalonia.X11.Glx; using static Avalonia.X11.XLib; @@ -360,13 +361,15 @@ namespace Avalonia.X11 if (customRendererFactory != null) return customRendererFactory.Create(root, loop); - - return _platform.Options.UseDeferredRendering ? - new DeferredRenderer(root, loop) - { - RenderOnlyOnRenderThread = true - } : - (IRenderer)new X11ImmediateRendererProxy(root, loop); + + return _platform.Options.UseDeferredRendering + ? _platform.Options.UseCompositor + ? new CompositingRenderer(root, this._platform.Compositor) + : new DeferredRenderer(root, loop) + { + RenderOnlyOnRenderThread = true + } + : (IRenderer)new X11ImmediateRendererProxy(root, loop); } void OnEvent(ref XEvent ev) diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index a259d8fab9..1243c90214 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Numerics; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; @@ -73,6 +74,12 @@ namespace Avalonia.Direct2D1.Media set { _deviceContext.Transform = value.ToDirect2D(); } } + public Matrix4x4 Transform4x4 + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + /// public void Clear(Color color) { From 4274e78a321b37412c77915638535e1d12304c6e Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 9 May 2022 21:43:22 +0300 Subject: [PATCH 011/389] Win32 --- samples/ControlCatalog.NetCore/Program.cs | 3 ++- .../Rendering/Composition/CompositingRenderer.cs | 4 ++-- src/Windows/Avalonia.Win32/Win32Platform.cs | 8 ++++++++ src/Windows/Avalonia.Win32/WindowImpl.cs | 4 ++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 2a0755b900..6196aac153 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -115,7 +115,8 @@ namespace ControlCatalog.NetCore }) .With(new Win32PlatformOptions { - EnableMultitouch = true + EnableMultitouch = true, + UseCompositor = true }) .UseSkia() .AfterSetup(builder => diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index 07ac54b634..65ed0d17ad 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -177,7 +177,7 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor public void Stop() { - _target.IsEnabled = true; + _target.IsEnabled = false; } public void Dispose() @@ -191,4 +191,4 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor public Compositor Compositor => _compositor; -} \ No newline at end of file +} diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index dc5e5324c4..32705a2cc6 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -14,6 +14,7 @@ using Avalonia.Input.Platform; using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Avalonia.Threading; using Avalonia.Utilities; using Avalonia.Win32.Input; @@ -48,6 +49,8 @@ namespace Avalonia /// Immediate re-renders the whole scene when some element is changed on the scene. Deferred re-renders only changed elements. /// public bool UseDeferredRendering { get; set; } = true; + + public bool UseCompositor { get; set; } /// /// Enables ANGLE for Windows. For every Windows version that is above Windows 7, the default is true otherwise it's false. @@ -132,6 +135,8 @@ namespace Avalonia.Win32 public static bool UseDeferredRendering => Options.UseDeferredRendering; internal static bool UseOverlayPopups => Options.OverlayPopups; public static Win32PlatformOptions Options { get; private set; } + + internal static Compositor Compositor { get; private set; } public Size DoubleClickSize => new Size( UnmanagedMethods.GetSystemMetrics(UnmanagedMethods.SystemMetric.SM_CXDOUBLECLK), @@ -181,6 +186,9 @@ namespace Avalonia.Win32 if (OleContext.Current != null) AvaloniaLocator.CurrentMutable.Bind().ToSingleton(); + + if (Options.UseCompositor) + Compositor = Compositor.Create(AvaloniaLocator.Current.GetRequiredService()); } public bool HasMessages() diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index f0036236ec..e61121f23e 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -14,6 +14,7 @@ using Avalonia.OpenGL.Egl; using Avalonia.OpenGL.Surfaces; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Avalonia.Win32.Automation; using Avalonia.Win32.Input; using Avalonia.Win32.Interop; @@ -500,6 +501,9 @@ namespace Avalonia.Win32 if (customRendererFactory != null) return customRendererFactory.Create(root, loop); + if (Win32Platform.Compositor != null) + return new CompositingRenderer(root, Win32Platform.Compositor); + return Win32Platform.UseDeferredRendering ? _isUsingComposition ? new DeferredRenderer(root, loop) From 7a3b5f051f02e2e0689df3232ffac5b76f6d986b Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 9 May 2022 23:20:39 +0300 Subject: [PATCH 012/389] Make ThreadSafeObjectPool actually thread safe --- src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs b/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs index c6845485dc..05995f2069 100644 --- a/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs +++ b/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs @@ -5,7 +5,6 @@ namespace Avalonia.Threading public class ThreadSafeObjectPool where T : class, new() { private Stack _stack = new Stack(); - private object _lock = new object(); public static ThreadSafeObjectPool Default { get; } = new ThreadSafeObjectPool(); public T Get() @@ -20,7 +19,7 @@ namespace Avalonia.Threading public void Return(T obj) { - lock (_stack) + lock (_lock) { _stack.Push(obj); } From 6d46006d075e05a4de377e8f85b1b93199e348f3 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 9 May 2022 23:30:02 +0300 Subject: [PATCH 013/389] Removed some matrix operations --- .../Composition/Expressions/ExpressionVariant.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs index 8c6af5cb0c..086c8ce276 100644 --- a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs @@ -290,9 +290,6 @@ namespace Avalonia.Rendering.Composition.Expressions if (left.Type == VariantType.Matrix3x2) return left.Matrix3x2 + right.Matrix3x2; - if (left.Type == VariantType.AvaloniaMatrix) - return left.AvaloniaMatrix + right.AvaloniaMatrix; - if (left.Type == VariantType.Matrix4x4) return left.Matrix4x4 + right.Matrix4x4; @@ -325,9 +322,6 @@ namespace Avalonia.Rendering.Composition.Expressions if (left.Type == VariantType.Matrix3x2) return left.Matrix3x2 - right.Matrix3x2; - if (left.Type == VariantType.AvaloniaMatrix) - return left.AvaloniaMatrix - right.AvaloniaMatrix; - if (left.Type == VariantType.Matrix4x4) return left.Matrix4x4 - right.Matrix4x4; @@ -407,12 +401,6 @@ namespace Avalonia.Rendering.Composition.Expressions if (left.Type == VariantType.AvaloniaMatrix && right.Type == VariantType.AvaloniaMatrix) return left.AvaloniaMatrix * right.AvaloniaMatrix; - - if (left.Type == VariantType.AvaloniaMatrix && right.Type == VariantType.Scalar) - return left.AvaloniaMatrix * (double)right.Scalar; - - if (left.Type == VariantType.AvaloniaMatrix && right.Type == VariantType.Double) - return left.AvaloniaMatrix * right.Double; if (left.Type == VariantType.Matrix4x4 && right.Type == VariantType.Matrix4x4) return left.Matrix4x4 * right.Matrix4x4; From 16dde385d0d8c6b9f7711a801b67a8f4440b3a57 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 11 May 2022 00:59:00 +0300 Subject: [PATCH 014/389] WIP: Matrix3x3, some invalidation support, dirty rects drawing --- samples/RenderDemo/App.xaml.cs | 4 + .../Composition/CompositingRenderer.cs | 26 ++++-- .../Composition/CompositionDrawListVisual.cs | 3 +- .../Composition/CompositionTarget.cs | 24 +++--- .../Rendering/Composition/Compositor.cs | 1 + .../Rendering/Composition/MatrixUtils.cs | 20 +++++ .../Composition/Server/DrawingContextProxy.cs | 4 - .../Composition/Server/FpsCounter.cs | 73 ++++++++++++++++ .../Server/ServerCompositionDrawListVisual.cs | 20 +++++ .../Server/ServerCompositionTarget.cs | 83 ++++++++++++++++++- .../Server/ServerContainerVisual.cs | 7 ++ .../Server/ServerSolidColorVisual.cs | 2 +- .../Composition/Server/ServerVisual.cs | 52 ++++++++---- .../Transport/CompositionTargetChanges.cs | 11 +++ .../Rendering/Composition/Visual.cs | 2 +- .../Rendering/Composition/VisualCollection.cs | 5 ++ .../Threading/ThreadSafeObjectPool.cs | 4 +- src/Avalonia.Base/composition-schema.xml | 4 + 18 files changed, 297 insertions(+), 48 deletions(-) create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/CompositionTargetChanges.cs diff --git a/samples/RenderDemo/App.xaml.cs b/samples/RenderDemo/App.xaml.cs index 8054b06964..8f4e02df01 100644 --- a/samples/RenderDemo/App.xaml.cs +++ b/samples/RenderDemo/App.xaml.cs @@ -29,6 +29,10 @@ namespace RenderDemo .With(new Win32PlatformOptions { OverlayPopups = true, + }) + .With(new X11PlatformOptions + { + UseCompositor = true }) .UsePlatformDetect() .LogToTrace(); diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index 07ac54b634..22fc6a77d4 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -37,9 +37,19 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor _target.Root = ((Visual)root!.VisualRoot!).AttachToCompositor(compositor); _update = Update; } - - public bool DrawFps { get; set; } - public bool DrawDirtyRects { get; set; } + + public bool DrawFps + { + get => _target.DrawFps; + set => _target.DrawFps = value; + } + + public bool DrawDirtyRects + { + get => _target.DrawDirtyRects; + set => _target.DrawDirtyRects = value; + } + public event EventHandler? SceneInvalidated; void QueueUpdate() @@ -57,7 +67,7 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor public IEnumerable HitTest(Point p, IVisual root, Func filter) { - var res = _target.TryHitTest(new Vector2((float)p.X, (float)p.Y)); + var res = _target.TryHitTest(p); if(res == null) yield break; for (var index = res.Count - 1; index >= 0; index--) @@ -146,7 +156,7 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor renderTransform *= mirrorMatrix; } - comp.TransformMatrix = renderTransform; + comp.TransformMatrix = MatrixUtils.ToMatrix4x4(renderTransform); _recorder.BeginUpdate(comp.DrawList ?? new CompositionDrawList()); visual.Render(_recordingContext); @@ -159,6 +169,8 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor SyncChildren(v); _dirty.Clear(); _recalculateChildren.Clear(); + _target.Size = _root.ClientSize; + _target.Scaling = _root.RenderScaling; } public void Resized(Size size) @@ -169,7 +181,7 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor { // We render only on the render thread for now Update(); - + _target.RequestRedraw(); Compositor.RequestCommitAsync().Wait(); } @@ -177,7 +189,7 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor public void Stop() { - _target.IsEnabled = true; + _target.IsEnabled = false; } public void Dispose() diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs index 9f02055412..b19c311663 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs @@ -29,11 +29,10 @@ internal class CompositionDrawListVisual : CompositionContainerVisual Visual = visual; } - internal override bool HitTest(Vector2 point) + internal override bool HitTest(Point pt) { if (DrawList == null) return false; - var pt = new Point(point.X, point.Y); if (Visual is ICustomHitTest custom) return custom.HitTest(pt); foreach (var op in DrawList) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs index a8835ca668..8d052389c2 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs @@ -18,7 +18,7 @@ namespace Avalonia.Rendering.Composition Root.Root = null; } - public PooledList? TryHitTest(Vector2 point) + public PooledList? TryHitTest(Point point) { Server.Readback.NextRead(); if (Root == null) @@ -28,12 +28,12 @@ namespace Avalonia.Rendering.Composition return res; } - public Vector2? TryTransformToVisual(CompositionVisual visual, Vector2 point) + public Point? TryTransformToVisual(CompositionVisual visual, Point point) { if (visual.Root != this) return null; var v = visual; - var m = Matrix3x2.Identity; + var m = Matrix.Identity; while (v != null) { if (!TryGetInvertedTransform(v, out var cm)) @@ -42,10 +42,10 @@ namespace Avalonia.Rendering.Composition v = v.Parent; } - return Vector2.Transform(point, m); + return point * m; } - bool TryGetInvertedTransform(CompositionVisual visual, out Matrix3x2 matrix) + bool TryGetInvertedTransform(CompositionVisual visual, out Matrix matrix) { var m = visual.TryGetServerTransform(); if (m == null) @@ -54,24 +54,22 @@ namespace Avalonia.Rendering.Composition return false; } - // TODO: Use Matrix3x3 - var m32 = new Matrix3x2(m.Value.M11, m.Value.M12, m.Value.M21, m.Value.M22, m.Value.M41, m.Value.M42); - - return Matrix3x2.Invert(m32, out matrix); + var m33 = MatrixUtils.ToMatrix(m.Value); + return m33.TryInvert(out matrix); } - bool TryTransformTo(CompositionVisual visual, ref Vector2 v) + bool TryTransformTo(CompositionVisual visual, ref Point v) { if (TryGetInvertedTransform(visual, out var m)) { - v = Vector2.Transform(v, m); + v = v * m; return true; } return false; } - bool HitTestCore(CompositionVisual visual, Vector2 point, PooledList result) + bool HitTestCore(CompositionVisual visual, Point point, PooledList result) { //TODO: Check readback too if (visual.Visible == false) @@ -103,5 +101,7 @@ namespace Avalonia.Rendering.Composition return false; } + + public void RequestRedraw() => Changes.RedrawRequested.Value = true; } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index 217d8dd803..14d779dbc4 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Numerics; using System.Threading.Tasks; +using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Server; diff --git a/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs b/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs index 5e91bcb3d4..2cb500cae4 100644 --- a/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs +++ b/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs @@ -42,5 +42,25 @@ namespace Avalonia.Rendering.Composition return mat; } + + public static Matrix4x4 ToMatrix4x4(Matrix matrix) => + new Matrix4x4( + (float)matrix.M11, (float)matrix.M12, 0, (float)matrix.M13, + (float)matrix.M21, (float)matrix.M22, 0, (float)matrix.M23, + 0, 0, 1, 0, + (float)matrix.M31, (float)matrix.M32, 0, (float)matrix.M33 + ); + + public static Matrix ToMatrix(Matrix4x4 matrix44) => + new Matrix( + matrix44.M11, + matrix44.M12, + matrix44.M14, + matrix44.M21, + matrix44.M22, + matrix44.M24, + matrix44.M41, + matrix44.M42, + matrix44.M44); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index f5dfa92897..7d061e86a9 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -135,8 +135,4 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl { _impl.Custom(custom); } - - public Matrix CutTransform(Matrix4x4 transform) => new Matrix(transform.M11, transform.M12, transform.M21, - transform.M22, transform.M41, - transform.M42); } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs new file mode 100644 index 0000000000..8c2e6e774a --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs @@ -0,0 +1,73 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using Avalonia.Media; +using Avalonia.Media.TextFormatting; +using Avalonia.Platform; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server; + +internal class FpsCounter +{ + private readonly GlyphTypeface _typeface; + private readonly bool _useManualFpsCounting; + private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); + private int _framesThisSecond; + private int _fps; + private TimeSpan _lastFpsUpdate; + private GlyphRun[] _runs = new GlyphRun[10]; + + public FpsCounter(GlyphTypeface typeface, bool useManualFpsCounting = false) + { + for (var c = 0; c <= 9; c++) + { + var s = c.ToString(); + var glyph = typeface.GetGlyph((uint)(s[0])); + _runs[c] = new GlyphRun(typeface, 18, new ReadOnlySlice(s.AsMemory()), new ushort[] { glyph }); + } + _typeface = typeface; + _useManualFpsCounting = useManualFpsCounting; + } + + public void FpsTick() => _framesThisSecond++; + + public void RenderFps(IDrawingContextImpl context) + { + var now = _stopwatch.Elapsed; + var elapsed = now - _lastFpsUpdate; + + if (!_useManualFpsCounting) + ++_framesThisSecond; + + if (elapsed.TotalSeconds > 1) + { + _fps = (int)(_framesThisSecond / elapsed.TotalSeconds); + _framesThisSecond = 0; + _lastFpsUpdate = now; + } + + var fpsLine = _fps.ToString("000"); + double width = 0; + double height = 0; + foreach (var ch in fpsLine) + { + var run = _runs[ch - '0']; + width += run.Size.Width; + height = Math.Max(height, run.Size.Height); + } + + var rect = new Rect(0, 0, width + 3, height + 3); + + context.DrawRectangle(Brushes.Black, null, rect); + + double offset = 0; + foreach (var ch in fpsLine) + { + var run = _runs[ch - '0']; + context.Transform = Matrix.CreateTranslation(offset, 0); + context.DrawGlyphRun(Brushes.White, run); + offset += run.Size.Width; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs index 397c968d04..f0384c36fc 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -16,6 +16,25 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua { } + Rect? _contentBounds; + + public override Rect ContentBounds + { + get + { + if (_contentBounds == null) + { + var rect = Rect.Empty; + if(_renderCommands!=null) + foreach (var cmd in _renderCommands) + rect = rect.Union(cmd.Item.Bounds); + _contentBounds = rect; + } + + return _contentBounds.Value; + } + } + protected override void ApplyCore(ChangeSet changes) { var ch = (DrawListVisualChanges)changes; @@ -23,6 +42,7 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua { _renderCommands?.Dispose(); _renderCommands = ch.AcquireDrawCommands(); + _contentBounds = null; } base.ApplyCore(changes); } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 493529e111..04ec711455 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -2,7 +2,10 @@ using System; using System.Numerics; using System.Threading; using Avalonia.Media; +using Avalonia.Media.Immutable; using Avalonia.Platform; +using Avalonia.Rendering.Composition.Transport; +using Avalonia.Utilities; namespace Avalonia.Rendering.Composition.Server { @@ -14,6 +17,13 @@ namespace Avalonia.Rendering.Composition.Server public long Id { get; } private ulong _frame = 1; private IRenderTarget? _renderTarget; + private FpsCounter _fpsCounter = new FpsCounter(Typeface.Default.GlyphTypeface); + private Rect _dirtyRect; + private Random _random = new(); + private Size _layerSize; + private IDrawingContextLayerImpl? _layer; + private bool _redrawRequested; + public ReadbackIndices Readback { get; } = new(); @@ -33,6 +43,11 @@ namespace Avalonia.Rendering.Composition.Server _compositor.RemoveCompositionTarget(this); } + partial void ApplyChangesExtra(CompositionTargetChanges c) + { + _redrawRequested = true; + } + public void Render() { if (Root == null) @@ -40,14 +55,76 @@ namespace Avalonia.Rendering.Composition.Server _renderTarget ??= _renderTargetFactory(); Compositor.UpdateServerTime(); - using (var context = _renderTarget.CreateDrawingContext(null)) + + Root.Update(this, Matrix4x4.Identity); + + if(_dirtyRect.IsEmpty && !_redrawRequested) + return; + _redrawRequested = false; + using (var targetContext = _renderTarget.CreateDrawingContext(null)) { - context.Clear(Colors.Transparent); - Root.Render(new CompositorDrawingContextProxy(context), Root.CombinedTransformMatrix); + var layerSize = Size * Scaling; + if (layerSize != _layerSize || _layer == null) + { + _layer?.Dispose(); + _layer = null; + _layer = targetContext.CreateLayer(layerSize); + _layerSize = layerSize; + } + + if (!_dirtyRect.IsEmpty) + { + using (var context = _layer.CreateDrawingContext(null)) + { + context.PushClip(_dirtyRect); + context.Clear(Colors.Transparent); + Root.Render(new CompositorDrawingContextProxy(context), Root.CombinedTransformMatrix); + context.PopClip(); + } + } + + targetContext.DrawBitmap(RefCountable.CreateUnownedNotClonable(_layer), 1, new Rect(_layerSize), + new Rect(_layerSize)); + + + if (DrawDirtyRects) + { + targetContext.DrawRectangle(new ImmutableSolidColorBrush( + new Color(30, (byte)_random.Next(255), (byte)_random.Next(255), + (byte)_random.Next(255))) + , null, _dirtyRect); + } + + if(DrawFps) + _fpsCounter.RenderFps(targetContext); + _dirtyRect = Rect.Empty; + } Readback.NextWrite(_frame); _frame++; } + + private static Rect SnapToDevicePixels(Rect rect, double scale) + { + return new Rect( + new Point( + Math.Floor(rect.X * scale) / scale, + Math.Floor(rect.Y * scale) / scale), + new Point( + Math.Ceiling(rect.Right * scale) / scale, + Math.Ceiling(rect.Bottom * scale) / scale)); + } + + public void AddDirtyRect(Rect rect) + { + var snapped = SnapToDevicePixels(rect, Scaling); + _dirtyRect = _dirtyRect.Union(snapped); + } + + public void Invalidate() + { + _redrawRequested = true; + } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs index ac112b846f..3f0995b257 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs @@ -21,6 +21,13 @@ namespace Avalonia.Rendering.Composition.Server } } + public override void Update(ServerCompositionTarget root, Matrix4x4 transform) + { + base.Update(root, transform); + foreach (var child in Children) + child.Update(root, GlobalTransformMatrix); + } + public ServerCompositionContainerVisual(ServerCompositor compositor) : base(compositor) { Children = new ServerCompositionVisualCollection(compositor); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs index 60569867de..786779bb2d 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs @@ -8,7 +8,7 @@ namespace Avalonia.Rendering.Composition.Server { protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) { - canvas.Transform = canvas.CutTransform(transform); + canvas.Transform = MatrixUtils.ToMatrix(transform); canvas.DrawRectangle(new ImmutableSolidColorBrush(Color), null, new RoundedRect(new Rect(new Size(Size)))); base.RenderCore(canvas, transform); } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 37e188fb47..90b580bfa7 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -6,6 +6,7 @@ namespace Avalonia.Rendering.Composition.Server { unsafe partial class ServerCompositionVisual : ServerObject { + private bool _isDirty; protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) { @@ -17,7 +18,7 @@ namespace Avalonia.Rendering.Composition.Server return; if(Opacity == 0) return; - canvas.PreTransform = canvas.CutTransform(transform); + canvas.PreTransform = MatrixUtils.ToMatrix(transform); canvas.Transform = Matrix.Identity; if (Opacity != 1) canvas.PushOpacity(Opacity); @@ -25,7 +26,8 @@ namespace Avalonia.Rendering.Composition.Server canvas.PushClip(new Rect(new Size(Size.X, Size.Y))); if (Clip != null) canvas.PushGeometryClip(Clip); - + + //TODO: Check clip RenderCore(canvas, transform); if (Clip != null) @@ -48,22 +50,31 @@ namespace Avalonia.Rendering.Composition.Server return ref _readback2; } - public Matrix4x4 CombinedTransformMatrix + public Matrix4x4 CombinedTransformMatrix { get; private set; } + public Matrix4x4 GlobalTransformMatrix { get; private set; } + + public virtual void Update(ServerCompositionTarget root, Matrix4x4 transform) { - get + var res = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, + Scale, RotationAngle, Orientation, Offset); + var i = Root!.Readback; + ref var readback = ref GetReadback(i.WriteIndex); + readback.Revision = i.WriteRevision; + readback.Matrix = res; + readback.TargetId = Root.Id; + //TODO: check effective opacity too + IsVisibleInFrame = Visible && Opacity > 0; + CombinedTransformMatrix = res; + GlobalTransformMatrix = res * transform; + //TODO: Cache + TransformedBounds = ContentBounds.TransformToAABB(MatrixUtils.ToMatrix(GlobalTransformMatrix)); + + if (!IsVisibleInFrame) + _isDirty = false; + else if (_isDirty) { - if (Root == null) - return default; - - var res = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, - Scale, RotationAngle, Orientation, Offset); - var i = Root.Readback; - ref var readback = ref GetReadback(i.WriteIndex); - readback.Revision = i.WriteRevision; - readback.Matrix = res; - readback.TargetId = Root.Id; - - return res; + Root.AddDirtyRect(TransformedBounds); + _isDirty = false; } } @@ -81,11 +92,20 @@ namespace Avalonia.Rendering.Composition.Server Parent = c.Parent.Value; if (c.Root.IsSet) Root = c.Root.Value; + _isDirty = true; + + if (IsVisibleInFrame) + Root?.AddDirtyRect(TransformedBounds); + else + Root?.Invalidate(); } public ServerCompositionTarget? Root { get; private set; } public ServerCompositionVisual? Parent { get; private set; } + public bool IsVisibleInFrame { get; set; } + public Rect TransformedBounds { get; set; } + public virtual Rect ContentBounds => new Rect(0, 0, Size.X, Size.Y); } diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/CompositionTargetChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/CompositionTargetChanges.cs new file mode 100644 index 0000000000..014adc7bbe --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/CompositionTargetChanges.cs @@ -0,0 +1,11 @@ +namespace Avalonia.Rendering.Composition.Transport; + +partial class CompositionTargetChanges +{ + public Change RedrawRequested; + + partial void ResetExtra() + { + RedrawRequested.Reset(); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Visual.cs b/src/Avalonia.Base/Rendering/Composition/Visual.cs index 1e6d7f8abb..25fce01de3 100644 --- a/src/Avalonia.Base/Rendering/Composition/Visual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Visual.cs @@ -59,6 +59,6 @@ namespace Avalonia.Rendering.Composition internal object? Tag { get; set; } - internal virtual bool HitTest(Vector2 point) => true; + internal virtual bool HitTest(Point point) => true; } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs index 35f33c3b38..fef4caf675 100644 --- a/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs +++ b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs @@ -54,6 +54,11 @@ namespace Avalonia.Rendering.Composition partial void OnRemoved(CompositionVisual item) => item.Parent = null; + partial void OnBeforeClear() + { + foreach (var i in this) + i.Parent = null; + } partial void OnBeforeAdded(CompositionVisual item) { diff --git a/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs b/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs index 05995f2069..827a02334a 100644 --- a/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs +++ b/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs @@ -9,7 +9,7 @@ namespace Avalonia.Threading public T Get() { - lock (_lock) + lock (_stack) { if(_stack.Count == 0) return new T(); @@ -19,7 +19,7 @@ namespace Avalonia.Threading public void Return(T obj) { - lock (_lock) + lock (_stack) { _stack.Push(obj); } diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml index cf864dac2d..eb1ffe1922 100644 --- a/src/Avalonia.Base/composition-schema.xml +++ b/src/Avalonia.Base/composition-schema.xml @@ -25,6 +25,10 @@ + + + + From 0e8672017b4ba40c78413dc73a1e2ba0dcad971a Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 11 May 2022 20:08:14 +0300 Subject: [PATCH 015/389] Workaround for #8118 --- src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 90b580bfa7..bf5b8bf292 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -30,6 +30,9 @@ namespace Avalonia.Rendering.Composition.Server //TODO: Check clip RenderCore(canvas, transform); + canvas.PreTransform = MatrixUtils.ToMatrix(transform); + canvas.Transform = Matrix.Identity; + if (Clip != null) canvas.PopGeometryClip(); if (ClipToBounds) From 9ac37065dca2a23006617dc649debd3392e23b38 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 24 May 2022 19:49:43 +0300 Subject: [PATCH 016/389] Animations now work... more or less? --- samples/ControlCatalog/MainView.xaml | 3 + .../Pages/CompositionPage.axaml | 45 +++++ .../Pages/CompositionPage.axaml.cs | 157 ++++++++++++++++++ .../Animations/AnimatedValueStore.cs | 88 +++++++++- .../Animations/AnimationInstanceBase.cs | 77 +++++++++ .../Animations/CompositionAnimation.cs | 2 +- .../Animations/ExpressionAnimationInstance.cs | 25 +-- .../Animations/IAnimationInstance.cs | 7 +- .../Animations/KeyFrameAnimationInstance.cs | 59 +++++-- .../Composition/Animations/KeyFrames.cs | 2 +- .../Composition/CompositionPropertySet.cs | 12 +- .../Composition/ElementCompositionPreview.cs | 6 + .../Composition/Expressions/Expression.cs | 45 ++++- .../ExpressionEvaluationContext.cs | 1 + .../Expressions/ExpressionTrackedValues.cs | 57 +++++++ .../Composition/Server/FpsCounter.cs | 27 ++- .../Server/ServerCompositionTarget.cs | 5 +- .../Composition/Server/ServerCompositor.cs | 20 +++ .../Composition/Server/ServerObject.cs | 82 ++++++++- .../Composition/Server/ServerVisual.cs | 22 ++- .../Rendering/Composition/Visual.cs | 1 - .../Utilities/RefTrackingDictionary.cs | 67 ++++++++ .../Avalonia.SourceGenerator.csproj | 1 + .../CompositionRoslynGenerator.cs | 26 +-- .../CompositionGenerator/Generator.cs | 102 ++++++++++-- .../ICompositionGeneratorSink.cs | 6 + .../RoslynCompositionGeneratorSink.cs | 15 ++ 27 files changed, 873 insertions(+), 87 deletions(-) create mode 100644 samples/ControlCatalog/Pages/CompositionPage.axaml create mode 100644 samples/ControlCatalog/Pages/CompositionPage.axaml.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/ElementCompositionPreview.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionTrackedValues.cs create mode 100644 src/Avalonia.Base/Utilities/RefTrackingDictionary.cs create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/ICompositionGeneratorSink.cs create mode 100644 src/Avalonia.SourceGenerator/CompositionGenerator/RoslynCompositionGeneratorSink.cs diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 59d724db69..2ce5ab3934 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -13,6 +13,9 @@ + + + diff --git a/samples/ControlCatalog/Pages/CompositionPage.axaml b/samples/ControlCatalog/Pages/CompositionPage.axaml new file mode 100644 index 0000000000..592290fde5 --- /dev/null +++ b/samples/ControlCatalog/Pages/CompositionPage.axaml @@ -0,0 +1,45 @@ + + + Implicit animations + + + + + + + + + + + + + + + + + + + + + + + Resize me + + + + + + + \ No newline at end of file diff --git a/samples/ControlCatalog/Pages/CompositionPage.axaml.cs b/samples/ControlCatalog/Pages/CompositionPage.axaml.cs new file mode 100644 index 0000000000..b37231243d --- /dev/null +++ b/samples/ControlCatalog/Pages/CompositionPage.axaml.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Avalonia.Markup.Xaml.Templates; +using Avalonia.Media; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.VisualTree; + +namespace ControlCatalog.Pages; + +public partial class CompositionPage : UserControl +{ + private ImplicitAnimationCollection _implicitAnimations; + + public CompositionPage() + { + AvaloniaXamlLoader.Load(this); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + this.FindControl("Items").Items = CreateColorItems(); + } + + private List CreateColorItems() + { + var list = new List(); + + list.Add(new ColorItem(Color.FromArgb(255, 255, 185, 0))); + list.Add(new ColorItem(Color.FromArgb(255, 231, 72, 86))); + list.Add(new ColorItem(Color.FromArgb(255, 0, 120, 215))); + list.Add(new ColorItem(Color.FromArgb(255, 0, 153, 188))); + list.Add(new ColorItem(Color.FromArgb(255, 122, 117, 116))); + list.Add(new ColorItem(Color.FromArgb(255, 118, 118, 118))); + list.Add(new ColorItem(Color.FromArgb(255, 255, 141, 0))); + list.Add(new ColorItem(Color.FromArgb(255, 232, 17, 35))); + list.Add(new ColorItem(Color.FromArgb(255, 0, 99, 177))); + list.Add(new ColorItem(Color.FromArgb(255, 45, 125, 154))); + list.Add(new ColorItem(Color.FromArgb(255, 93, 90, 88))); + list.Add(new ColorItem(Color.FromArgb(255, 76, 74, 72))); + list.Add(new ColorItem(Color.FromArgb(255, 247, 99, 12))); + list.Add(new ColorItem(Color.FromArgb(255, 234, 0, 94))); + list.Add(new ColorItem(Color.FromArgb(255, 142, 140, 216))); + list.Add(new ColorItem(Color.FromArgb(255, 0, 183, 195))); + list.Add(new ColorItem(Color.FromArgb(255, 104, 118, 138))); + list.Add(new ColorItem(Color.FromArgb(255, 105, 121, 126))); + list.Add(new ColorItem(Color.FromArgb(255, 202, 80, 16))); + list.Add(new ColorItem(Color.FromArgb(255, 195, 0, 82))); + list.Add(new ColorItem(Color.FromArgb(255, 107, 105, 214))); + list.Add(new ColorItem(Color.FromArgb(255, 3, 131, 135))); + list.Add(new ColorItem(Color.FromArgb(255, 81, 92, 107))); + list.Add(new ColorItem(Color.FromArgb(255, 74, 84, 89))); + list.Add(new ColorItem(Color.FromArgb(255, 218, 59, 1))); + list.Add(new ColorItem(Color.FromArgb(255, 227, 0, 140))); + list.Add(new ColorItem(Color.FromArgb(255, 135, 100, 184))); + list.Add(new ColorItem(Color.FromArgb(255, 0, 178, 148))); + list.Add(new ColorItem(Color.FromArgb(255, 86, 124, 115))); + list.Add(new ColorItem(Color.FromArgb(255, 100, 124, 100))); + list.Add(new ColorItem(Color.FromArgb(255, 239, 105, 80))); + list.Add(new ColorItem(Color.FromArgb(255, 191, 0, 119))); + list.Add(new ColorItem(Color.FromArgb(255, 116, 77, 169))); + list.Add(new ColorItem(Color.FromArgb(255, 1, 133, 116))); + list.Add(new ColorItem(Color.FromArgb(255, 72, 104, 96))); + list.Add(new ColorItem(Color.FromArgb(255, 82, 94, 84))); + list.Add(new ColorItem(Color.FromArgb(255, 209, 52, 56))); + list.Add(new ColorItem(Color.FromArgb(255, 194, 57, 179))); + list.Add(new ColorItem(Color.FromArgb(255, 177, 70, 194))); + list.Add(new ColorItem(Color.FromArgb(255, 0, 204, 106))); + list.Add(new ColorItem(Color.FromArgb(255, 73, 130, 5))); + list.Add(new ColorItem(Color.FromArgb(255, 132, 117, 69))); + list.Add(new ColorItem(Color.FromArgb(255, 255, 67, 67))); + list.Add(new ColorItem(Color.FromArgb(255, 154, 0, 137))); + list.Add(new ColorItem(Color.FromArgb(255, 136, 23, 152))); + list.Add(new ColorItem(Color.FromArgb(255, 16, 137, 62))); + list.Add(new ColorItem(Color.FromArgb(255, 16, 124, 16))); + list.Add(new ColorItem(Color.FromArgb(255, 126, 115, 95))); + + return list; + } + + public class ColorItem + { + public Color Color { get; private set; } + + public SolidColorBrush ColorBrush + { + get { return new SolidColorBrush(Color); } + } + + public String ColorHexValue + { + get { return Color.ToString().Substring(3).ToUpperInvariant(); } + } + + public ColorItem(Color color) + { + Color = color; + } + } + + private void EnsureImplicitAnimations() + { + if (_implicitAnimations == null) + { + var compositor = ElementCompositionPreview.GetElementVisual(this)!.Compositor; + + var offsetAnimation = compositor.CreateVector3KeyFrameAnimation(); + offsetAnimation.Target = "Offset"; + offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue"); + offsetAnimation.Duration = TimeSpan.FromMilliseconds(400); + + var rotationAnimation = compositor.CreateScalarKeyFrameAnimation(); + rotationAnimation.Target = "RotationAngle"; + rotationAnimation.InsertKeyFrame(.5f, 0.160f); + rotationAnimation.InsertKeyFrame(1f, 0f); + rotationAnimation.Duration = TimeSpan.FromMilliseconds(400); + + var animationGroup = compositor.CreateAnimationGroup(); + animationGroup.Add(offsetAnimation); + animationGroup.Add(rotationAnimation); + + _implicitAnimations = compositor.CreateImplicitAnimationCollection(); + _implicitAnimations["Offset"] = animationGroup; + } + } + + public static void SetEnableAnimations(Border border, bool value) + { + + var page = border.FindAncestorOfType(); + if (page == null) + { + border.AttachedToVisualTree += delegate { SetEnableAnimations(border, true); }; + return; + } + + if (ElementCompositionPreview.GetElementVisual(page) == null) + return; + + page.EnsureImplicitAnimations(); + ElementCompositionPreview.GetElementVisual((Visual)border.GetVisualParent()).ImplicitAnimations = + page._implicitAnimations; + } + + + + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs b/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs index 180c45022f..e877b50b20 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs @@ -1,19 +1,62 @@ +using System.Runtime.InteropServices; using Avalonia.Rendering.Composition.Expressions; using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.Composition.Transport; +using Avalonia.Utilities; namespace Avalonia.Rendering.Composition.Animations { - internal struct AnimatedValueStore where T : struct + internal struct ServerObjectSubscriptionStore { + public bool IsValid; + public RefTrackingDictionary Subscribers; + + public void Invalidate() + { + if (IsValid) + return; + IsValid = false; + if (Subscribers != null) + foreach (var sub in Subscribers) + sub.Key.Invalidate(); + } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct ServerValueStore + { + // HAS TO BE THE FIRST FIELD, accessed by field offset from ServerObject + private ServerObjectSubscriptionStore Subscriptions; + private T _value; + public T Value + { + set + { + _value = value; + Subscriptions.Invalidate(); + } + get + { + Subscriptions.IsValid = true; + return _value; + } + } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct ServerAnimatedValueStore where T : struct + { + // HAS TO BE THE FIRST FIELD, accessed by field offset from ServerObject + private ServerObjectSubscriptionStore Subscriptions; + private IAnimationInstance? _animation; private T _direct; - private IAnimationInstance _animation; private T? _lastAnimated; public T Direct => _direct; public T GetAnimated(ServerCompositor compositor) { + Subscriptions.IsValid = true; if (_animation == null) return _direct; var v = _animation.Evaluate(compositor.ServerNow, ExpressionVariant.Create(_direct)) @@ -22,19 +65,50 @@ namespace Avalonia.Rendering.Composition.Animations return v; } + public void Activate(ServerObject parent) + { + if (_animation != null) + _animation.Activate(); + } + + public void Deactivate(ServerObject parent) + { + if (_animation != null) + _animation.Deactivate(); + } + private T LastAnimated => _animation != null ? _lastAnimated ?? _direct : _direct; public bool IsAnimation => _animation != null; - public void SetAnimation(ChangeSet cs, IAnimationInstance animation) + public void SetAnimation(ServerObject target, ChangeSet cs, IAnimationInstance animation, int storeOffset) { + _direct = default; + if (_animation != null) + { + if (target.IsActive) + _animation.Deactivate(); + } + _animation = animation; - _animation.Start(cs.Batch.CommitedAt, ExpressionVariant.Create(LastAnimated)); + _animation.Initialize(cs.Batch.CommitedAt, ExpressionVariant.Create(LastAnimated), storeOffset); + if (target.IsActive) + _animation.Activate(); + + Subscriptions.Invalidate(); } - public static implicit operator AnimatedValueStore(T value) => new AnimatedValueStore() + public void SetValue(ServerObject target, T value) { - _direct = value - }; + if (_animation != null) + { + if (target.IsActive) + _animation.Deactivate(); + } + + _animation = null; + _direct = value; + Subscriptions.Invalidate(); + } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs b/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs new file mode 100644 index 0000000000..212237049f --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Animations; + +internal abstract class AnimationInstanceBase : IAnimationInstance +{ + private List<(ServerObject obj, int member)>? _trackedObjects; + protected PropertySetSnapshot Parameters { get; } + public ServerObject TargetObject { get; } + protected int StoreOffset { get; private set; } + private bool _invalidated; + + public AnimationInstanceBase(ServerObject target, PropertySetSnapshot parameters) + { + Parameters = parameters; + TargetObject = target; + } + + protected void Initialize(int storeOffset, HashSet<(string name, string member)> trackedObjects) + { + if (trackedObjects.Count > 0) + { + _trackedObjects = new (); + foreach (var t in trackedObjects) + { + var obj = Parameters.GetObjectParameter(t.name); + if (obj is ServerObject tracked) + { + var off = tracked.GetFieldOffset(t.member); + if (off == null) +#if DEBUG + throw new InvalidCastException("Attempting to subscribe to unknown field"); +#else + continue; +#endif + _trackedObjects.Add((tracked, off.Value)); + } + } + } + + StoreOffset = storeOffset; + } + + public abstract void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, int storeOffset); + protected abstract ExpressionVariant EvaluateCore(TimeSpan now, ExpressionVariant currentValue); + + public ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue) + { + _invalidated = false; + return EvaluateCore(now, currentValue); + } + + public virtual void Activate() + { + if (_trackedObjects != null) + foreach (var tracked in _trackedObjects) + tracked.obj.SubscribeToInvalidation(tracked.member, this); + } + + public virtual void Deactivate() + { + if (_trackedObjects != null) + foreach (var tracked in _trackedObjects) + tracked.obj.UnsubscribeFromInvalidation(tracked.member, this); + } + + public void Invalidate() + { + if (_invalidated) + return; + _invalidated = true; + TargetObject.NotifyAnimatedValueChanged(StoreOffset); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs index 9375faaaae..fe20115b38 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs @@ -53,7 +53,7 @@ namespace Avalonia.Rendering.Composition.Animations ExpressionVariant? finalValue); internal PropertySetSnapshot CreateSnapshot(bool server) - => _propertySet.Snapshot(server, 1); + => _propertySet.Snapshot(server); void ICompositionAnimationBase.InternalOnly() { diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs index 47b947b2e9..7944fe7990 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs @@ -1,22 +1,22 @@ using System; +using System.Collections.Generic; using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition.Animations { - internal class ExpressionAnimationInstance : IAnimationInstance + internal class ExpressionAnimationInstance : AnimationInstanceBase, IAnimationInstance { private readonly Expression _expression; - private readonly IExpressionObject _target; private ExpressionVariant _startingValue; private readonly ExpressionVariant? _finalValue; - private readonly PropertySetSnapshot _parameters; - public ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue) + protected override ExpressionVariant EvaluateCore(TimeSpan now, ExpressionVariant currentValue) { var ctx = new ExpressionEvaluationContext { - Parameters = _parameters, - Target = _target, + Parameters = Parameters, + Target = TargetObject, ForeignFunctionInterface = BuiltInExpressionFfi.Instance, StartingValue = _startingValue, FinalValue = _finalValue ?? _startingValue, @@ -25,20 +25,21 @@ namespace Avalonia.Rendering.Composition.Animations return _expression.Evaluate(ref ctx); } - public void Start(TimeSpan startedAt, ExpressionVariant startingValue) + public override void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, int storeOffset) { _startingValue = startingValue; + var hs = new HashSet<(string, string)>(); + _expression.CollectReferences(hs); + base.Initialize(storeOffset, hs); } - + public ExpressionAnimationInstance(Expression expression, - IExpressionObject target, + ServerObject target, ExpressionVariant? finalValue, - PropertySetSnapshot parameters) + PropertySetSnapshot parameters) : base(target, parameters) { _expression = expression; - _target = target; _finalValue = finalValue; - _parameters = parameters; } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs index a0b066ae0c..05d1b50953 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs @@ -1,11 +1,16 @@ using System; using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition.Animations { internal interface IAnimationInstance { + ServerObject TargetObject { get; } ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue); - void Start(TimeSpan startedAt, ExpressionVariant startingValue); + void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, int storeOffset); + void Activate(); + void Deactivate(); + void Invalidate(); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs index b90a02148d..9571cef0b4 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs @@ -1,15 +1,15 @@ using System; +using System.Collections.Generic; using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition.Animations { - class KeyFrameAnimationInstance : IAnimationInstance where T : struct + class KeyFrameAnimationInstance : AnimationInstanceBase, IAnimationInstance where T : struct { private readonly IInterpolator _interpolator; private readonly ServerKeyFrame[] _keyFrames; - private readonly PropertySetSnapshot _snapshot; private readonly ExpressionVariant? _finalValue; - private readonly IExpressionObject _target; private readonly AnimationDelayBehavior _delayBehavior; private readonly TimeSpan _delayTime; private readonly AnimationDirection _direction; @@ -19,21 +19,21 @@ namespace Avalonia.Rendering.Composition.Animations private readonly AnimationStopBehavior _stopBehavior; private TimeSpan _startedAt; private T _startingValue; + private readonly TimeSpan _totalDuration; + private bool _finished; public KeyFrameAnimationInstance( IInterpolator interpolator, ServerKeyFrame[] keyFrames, PropertySetSnapshot snapshot, ExpressionVariant? finalValue, - IExpressionObject target, + ServerObject target, AnimationDelayBehavior delayBehavior, TimeSpan delayTime, AnimationDirection direction, TimeSpan duration, AnimationIterationBehavior iterationBehavior, - int iterationCount, AnimationStopBehavior stopBehavior) + int iterationCount, AnimationStopBehavior stopBehavior) : base(target, snapshot) { _interpolator = interpolator; _keyFrames = keyFrames; - _snapshot = snapshot; _finalValue = finalValue; - _target = target; _delayBehavior = delayBehavior; _delayTime = delayTime; _direction = direction; @@ -41,26 +41,43 @@ namespace Avalonia.Rendering.Composition.Animations _iterationBehavior = iterationBehavior; _iterationCount = iterationCount; _stopBehavior = stopBehavior; + if (_iterationBehavior == AnimationIterationBehavior.Count) + _totalDuration = delayTime.Add(TimeSpan.FromTicks(iterationCount * _duration.Ticks)); if (_keyFrames.Length == 0) throw new InvalidOperationException("Animation has no key frames"); if(_duration.Ticks <= 0) throw new InvalidOperationException("Invalid animation duration"); } - public ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue) + + protected override ExpressionVariant EvaluateCore(TimeSpan now, ExpressionVariant currentValue) { - var elapsed = now - _startedAt; var starting = ExpressionVariant.Create(_startingValue); var ctx = new ExpressionEvaluationContext { - Parameters = _snapshot, - Target = _target, + Parameters = Parameters, + Target = TargetObject, CurrentValue = currentValue, FinalValue = _finalValue ?? starting, StartingValue = starting, ForeignFunctionInterface = BuiltInExpressionFfi.Instance }; + var elapsed = now - _startedAt; + var res = EvaluateImpl(elapsed, currentValue, ref ctx); + if (_iterationBehavior == AnimationIterationBehavior.Count + && !_finished + && elapsed > _totalDuration) + { + // Active check? + TargetObject.Compositor.RemoveFromClock(this); + _finished = true; + } + return res; + } + + private ExpressionVariant EvaluateImpl(TimeSpan elapsed, ExpressionVariant currentValue, ref ExpressionEvaluationContext ctx) + { if (elapsed < _delayTime) { if (_delayBehavior == AnimationDelayBehavior.SetInitialValueBeforeDelay) @@ -130,10 +147,28 @@ namespace Avalonia.Rendering.Composition.Animations return f.Value; } - public void Start(TimeSpan startedAt, ExpressionVariant startingValue) + public override void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, int storeOffset) { _startedAt = startedAt; _startingValue = startingValue.CastOrDefault(); + var hs = new HashSet<(string, string)>(); + + // TODO: Update subscriptions based on the current keyframe rather than keeping subscriptions to all of them + foreach (var frame in _keyFrames) + frame.Expression?.CollectReferences(hs); + Initialize(storeOffset, hs); + } + + public override void Activate() + { + TargetObject.Compositor.AddToClock(this); + base.Activate(); + } + + public override void Deactivate() + { + TargetObject.Compositor.RemoveFromClock(this); + base.Deactivate(); } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs index d7f2504061..26ba35409d 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs @@ -66,7 +66,7 @@ namespace Avalonia.Rendering.Composition.Animations struct ServerKeyFrame { public T Value; - public Expression Expression; + public Expression? Expression; public IEasingFunction EasingFunction; public float Key; } diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs index 004c2676ff..bc0ce804dc 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs @@ -24,11 +24,16 @@ namespace Avalonia.Rendering.Composition _variants[key] = value; } + /* + For INTERNAL USE by CompositionAnimation ONLY, we DON'T support expression + paths like SomeParam.SomePropertyObject.SomeValue + */ internal void Set(string key, CompositionObject obj) { _objects[key] = obj ?? throw new ArgumentNullException(nameof(obj)); _variants.Remove(key); } + public void InsertColor(string propertyName, Avalonia.Media.Color value) => Set(propertyName, value); public void InsertMatrix3x2(string propertyName, Matrix3x2 value) => Set(propertyName, value); @@ -99,7 +104,10 @@ namespace Avalonia.Rendering.Composition _variants.Remove(key); } - internal PropertySetSnapshot Snapshot(bool server, int allowedNestingLevel) + internal PropertySetSnapshot Snapshot(bool server) => + SnapshotCore(server, 1); + + private PropertySetSnapshot SnapshotCore(bool server, int allowedNestingLevel) { var dic = new Dictionary(_objects.Count + _variants.Count); foreach (var o in _objects) @@ -108,7 +116,7 @@ namespace Avalonia.Rendering.Composition { if (allowedNestingLevel <= 0) throw new InvalidOperationException("PropertySet depth limit reached"); - dic[o.Key] = new PropertySetSnapshot.Value(ps.Snapshot(server, allowedNestingLevel - 1)); + dic[o.Key] = new PropertySetSnapshot.Value(ps.SnapshotCore(server, allowedNestingLevel - 1)); } else if (o.Value.Server == null) throw new InvalidOperationException($"Object of type {o.Value.GetType()} is not allowed"); diff --git a/src/Avalonia.Base/Rendering/Composition/ElementCompositionPreview.cs b/src/Avalonia.Base/Rendering/Composition/ElementCompositionPreview.cs new file mode 100644 index 0000000000..afda314276 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/ElementCompositionPreview.cs @@ -0,0 +1,6 @@ +namespace Avalonia.Rendering.Composition; + +public static class ElementCompositionPreview +{ + public static CompositionVisual? GetElementVisual(Visual visual) => visual.CompositionVisual; +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs index 5577d2b52a..088771e1ba 100644 --- a/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Reflection; +using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition.Expressions { @@ -15,6 +16,11 @@ namespace Avalonia.Rendering.Composition.Expressions public abstract ExpressionVariant Evaluate(ref ExpressionEvaluationContext context); + public virtual void CollectReferences(HashSet<(string parameter, string property)> references) + { + + } + protected abstract string Print(); public override string ToString() => Print(); @@ -114,6 +120,13 @@ namespace Avalonia.Rendering.Composition.Expressions return FalsePart.Evaluate(ref context); } + public override void CollectReferences(HashSet<(string parameter, string property)> references) + { + Condition.CollectReferences(references); + TruePart.CollectReferences(references); + FalsePart.CollectReferences(references); + } + protected override string Print() => $"({Condition}) ? ({TruePart}) : ({FalsePart})"; } @@ -128,6 +141,7 @@ namespace Avalonia.Rendering.Composition.Expressions } public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) => Constant; + protected override string Print() => Constant.ToString(CultureInfo.InvariantCulture); } @@ -155,6 +169,12 @@ namespace Avalonia.Rendering.Composition.Expressions return res; } + public override void CollectReferences(HashSet<(string parameter, string property)> references) + { + foreach(var arg in Parameters) + arg.CollectReferences(references); + } + protected override string Print() { return Name + "( (" + string.Join("), (", Parameters) + ") )"; @@ -173,18 +193,30 @@ namespace Avalonia.Rendering.Composition.Expressions Member = string.Intern(member); } + public override void CollectReferences(HashSet<(string parameter, string property)> references) + { + Target.CollectReferences(references); + if (Target is ParameterExpression pe) + references.Add((pe.Name, Member)); + } + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) { if (Target is KeywordExpression ke && ke.Keyword == ExpressionKeyword.Target) + { return context.Target.GetProperty(Member); + } + if (Target is ParameterExpression pe) { var obj = context.Parameters?.GetObjectParameter(pe.Name); if (obj != null) + { return obj.GetProperty(Member); + } } - + // Those are considered immutable return Target.Evaluate(ref context).GetProperty(Member); } @@ -263,6 +295,11 @@ namespace Avalonia.Rendering.Composition.Expressions return default; } + public override void CollectReferences(HashSet<(string parameter, string property)> references) + { + Parameter.CollectReferences(references); + } + protected override string Print() { return OperatorName(Type) + Parameter; @@ -313,6 +350,12 @@ namespace Avalonia.Rendering.Composition.Expressions return default; } + public override void CollectReferences(HashSet<(string parameter, string property)> references) + { + Left.CollectReferences(references); + Right.CollectReferences(references); + } + protected override string Print() { return "(" + Left + OperatorName(Type) + Right + ")"; diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs index a7ddabd70d..9d23551e43 100644 --- a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition.Expressions { diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionTrackedValues.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionTrackedValues.cs new file mode 100644 index 0000000000..334f975aa0 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionTrackedValues.cs @@ -0,0 +1,57 @@ +using System.Collections; +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Expressions; + +internal class ExpressionTrackedObjects : IEnumerable +{ + private List _list = new(); + private HashSet _hashSet = new(); + + public void Add(IExpressionObject obj, string member) + { + if (_hashSet.Add(obj)) + _list.Add(obj); + } + + public void Clear() + { + _list.Clear(); + _hashSet.Clear(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _list.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_list).GetEnumerator(); + } + + public List.Enumerator GetEnumerator() => _list.GetEnumerator(); + + public struct Pool + { + private Stack _stack = new(); + + public Pool() + { + } + + public ExpressionTrackedObjects Get() + { + if (_stack.Count > 0) + return _stack.Pop(); + return new ExpressionTrackedObjects(); + } + + public void Return(ExpressionTrackedObjects obj) + { + _stack.Clear(); + _stack.Push(obj); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs index 8c2e6e774a..a60084d8f3 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs @@ -10,24 +10,23 @@ namespace Avalonia.Rendering.Composition.Server; internal class FpsCounter { - private readonly GlyphTypeface _typeface; - private readonly bool _useManualFpsCounting; private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); private int _framesThisSecond; + private int _totalFrames; private int _fps; private TimeSpan _lastFpsUpdate; - private GlyphRun[] _runs = new GlyphRun[10]; + const int FirstChar = 32; + const int LastChar = 126; + private GlyphRun[] _runs = new GlyphRun[LastChar - FirstChar + 1]; - public FpsCounter(GlyphTypeface typeface, bool useManualFpsCounting = false) + public FpsCounter(GlyphTypeface typeface) { - for (var c = 0; c <= 9; c++) + for (var c = FirstChar; c <= LastChar; c++) { - var s = c.ToString(); + var s = new string((char)c, 1); var glyph = typeface.GetGlyph((uint)(s[0])); - _runs[c] = new GlyphRun(typeface, 18, new ReadOnlySlice(s.AsMemory()), new ushort[] { glyph }); + _runs[c - FirstChar] = new GlyphRun(typeface, 18, new ReadOnlySlice(s.AsMemory()), new ushort[] { glyph }); } - _typeface = typeface; - _useManualFpsCounting = useManualFpsCounting; } public void FpsTick() => _framesThisSecond++; @@ -37,8 +36,8 @@ internal class FpsCounter var now = _stopwatch.Elapsed; var elapsed = now - _lastFpsUpdate; - if (!_useManualFpsCounting) - ++_framesThisSecond; + ++_framesThisSecond; + ++_totalFrames; if (elapsed.TotalSeconds > 1) { @@ -47,12 +46,12 @@ internal class FpsCounter _lastFpsUpdate = now; } - var fpsLine = _fps.ToString("000"); + var fpsLine = $"Frame #{_totalFrames:00000000} FPS: {_fps:000}"; double width = 0; double height = 0; foreach (var ch in fpsLine) { - var run = _runs[ch - '0']; + var run = _runs[ch - FirstChar]; width += run.Size.Width; height = Math.Max(height, run.Size.Height); } @@ -64,7 +63,7 @@ internal class FpsCounter double offset = 0; foreach (var ch in fpsLine) { - var run = _runs[ch - '0']; + var run = _runs[ch - FirstChar]; context.Transform = Matrix.CreateTranslation(offset, 0); context.DrawGlyphRun(Brushes.White, run); offset += run.Size.Width; diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 04ec711455..dab65fc8ed 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -56,10 +56,11 @@ namespace Avalonia.Rendering.Composition.Server Compositor.UpdateServerTime(); - Root.Update(this, Matrix4x4.Identity); - if(_dirtyRect.IsEmpty && !_redrawRequested) return; + + Root.Update(this, Matrix4x4.Identity); + _redrawRequested = false; using (var targetContext = _renderTarget.CreateDrawingContext(null)) { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index e56f85acdf..5dbe9cfb17 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Expressions; using Avalonia.Rendering.Composition.Transport; namespace Avalonia.Rendering.Composition.Server @@ -13,6 +15,8 @@ namespace Avalonia.Rendering.Composition.Server public Stopwatch Clock { get; } = Stopwatch.StartNew(); public TimeSpan ServerNow { get; private set; } private List _activeTargets = new(); + private HashSet _activeAnimations = new(); + private List _animationsToUpdate = new(); public ServerCompositor(IRenderLoop renderLoop) { @@ -64,6 +68,7 @@ namespace Avalonia.Rendering.Composition.Server } bool IRenderLoopTask.NeedsUpdate => false; + void IRenderLoopTask.Update(TimeSpan time) { } @@ -71,6 +76,15 @@ namespace Avalonia.Rendering.Composition.Server void IRenderLoopTask.Render() { ApplyPendingBatches(); + + foreach(var animation in _activeAnimations) + _animationsToUpdate.Add(animation); + + foreach(var animation in _animationsToUpdate) + animation.Invalidate(); + + _animationsToUpdate.Clear(); + foreach (var t in _activeTargets) t.Render(); @@ -86,5 +100,11 @@ namespace Avalonia.Rendering.Composition.Server { _activeTargets.Remove(target); } + + public void AddToClock(IAnimationInstance animationInstance) => + _activeAnimations.Add(animationInstance); + + public void RemoveFromClock(IAnimationInstance animationInstance) => + _activeAnimations.Remove(animationInstance); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs index 072377cd7e..5b2f58b186 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs @@ -1,5 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Expressions; using Avalonia.Rendering.Composition.Transport; +using Avalonia.Utilities; namespace Avalonia.Rendering.Composition.Server { @@ -9,7 +15,9 @@ namespace Avalonia.Rendering.Composition.Server public virtual long LastChangedBy => ItselfLastChangedBy; public long ItselfLastChangedBy { get; private set; } - + private uint _activationCount; + public bool IsActive => _activationCount != 0; + public ServerObject(ServerCompositor compositor) { Compositor = compositor; @@ -23,6 +31,7 @@ namespace Avalonia.Rendering.Composition.Server public void Apply(ChangeSet changes) { ApplyCore(changes); + ValuesInvalidated(); ItselfLastChangedBy = changes.Batch!.SequenceId; } @@ -32,5 +41,76 @@ namespace Avalonia.Rendering.Composition.Server } ExpressionVariant IExpressionObject.GetProperty(string name) => GetPropertyForAnimation(name); + + public void Activate() + { + _activationCount++; + if (_activationCount == 1) + Activated(); + } + + public void Deactivate() + { +#if DEBUG + if (_activationCount == 0) + throw new InvalidOperationException(); +#endif + _activationCount--; + if (_activationCount == 0) + Deactivated(); + } + + protected virtual void Activated() + { + + } + + protected virtual void Deactivated() + { + + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected int GetOffset(ref T field) where T : struct + { + return Unsafe.ByteOffset(ref Unsafe.As(ref _activationCount), + ref Unsafe.As(ref field)) + .ToInt32(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ref ServerObjectSubscriptionStore GetStoreFromOffset(int offset) + { + return ref Unsafe.As(ref Unsafe.AddByteOffset(ref _activationCount, + new IntPtr(offset))); + } + + public void NotifyAnimatedValueChanged(int offset) + { + ref var store = ref GetStoreFromOffset(offset); + store.Invalidate(); + ValuesInvalidated(); + } + + protected virtual void ValuesInvalidated() + { + + } + + public void SubscribeToInvalidation(int member, IAnimationInstance animation) + { + ref var store = ref GetStoreFromOffset(member); + if (store.Subscribers.AddRef(animation)) + Activate(); + } + + public void UnsubscribeFromInvalidation(int member, IAnimationInstance animation) + { + ref var store = ref GetStoreFromOffset(member); + if (store.Subscribers.ReleaseRef(animation)) + Deactivate(); + } + + public virtual int? GetFieldOffset(string fieldName) => null; } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index bf5b8bf292..05b63a7a73 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -7,6 +7,7 @@ namespace Avalonia.Rendering.Composition.Server unsafe partial class ServerCompositionVisual : ServerObject { private bool _isDirty; + private ServerCompositionTarget? _root; protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) { @@ -95,16 +96,31 @@ namespace Avalonia.Rendering.Composition.Server Parent = c.Parent.Value; if (c.Root.IsSet) Root = c.Root.Value; - _isDirty = true; + ValuesInvalidated(); + } + + public ServerCompositionTarget? Root + { + get => _root; + private set + { + if(_root != null) + Deactivate(); + _root = value; + if (_root != null) + Activate(); + } + } + protected override void ValuesInvalidated() + { + _isDirty = true; if (IsVisibleInFrame) Root?.AddDirtyRect(TransformedBounds); else Root?.Invalidate(); } - public ServerCompositionTarget? Root { get; private set; } - public ServerCompositionVisual? Parent { get; private set; } public bool IsVisibleInFrame { get; set; } public Rect TransformedBounds { get; set; } diff --git a/src/Avalonia.Base/Rendering/Composition/Visual.cs b/src/Avalonia.Base/Rendering/Composition/Visual.cs index 25fce01de3..f7c8078073 100644 --- a/src/Avalonia.Base/Rendering/Composition/Visual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Visual.cs @@ -38,7 +38,6 @@ namespace Avalonia.Rendering.Composition { } - internal Matrix4x4? TryGetServerTransform() { if (Root == null) diff --git a/src/Avalonia.Base/Utilities/RefTrackingDictionary.cs b/src/Avalonia.Base/Utilities/RefTrackingDictionary.cs new file mode 100644 index 0000000000..71305a8305 --- /dev/null +++ b/src/Avalonia.Base/Utilities/RefTrackingDictionary.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Avalonia.Utilities; + +internal class RefTrackingDictionary : Dictionary where TKey : class +{ + /// + /// Increase reference count for a key by 1. + /// + /// true if key was added to the dictionary, false otherwise + public bool AddRef(TKey key) + { +#if NET5_0_OR_GREATER + ref var count = ref CollectionsMarshal.GetValueRefOrAddDefault(this, key, out var _); + count++; +#else + TryGetValue(key, out var count); + count++; + this[key] = count; +#endif + return count == 1; + } + + /// + /// Decrease reference count for a key by 1. + /// + /// true if key was removed to the dictionary, false otherwise + public bool ReleaseRef(TKey key) + { +#if NET5_0_OR_GREATER + ref var count = ref CollectionsMarshal.GetValueRefOrNullRef(this, key); + if (Unsafe.IsNullRef(ref count)) +#if DEBUG + throw new InvalidOperationException("Attempting to release a non-referenced object"); +#else + return false; +#endif // DEBUG + count--; + if (count == 0) + { + Remove(key); + return true; + } + + return false; +#else + if (!TryGetValue(key, out var count)) +#if DEBUG + throw new InvalidOperationException("Attempting to release a non-referenced object"); +#else + return false; +#endif // DEBUG + count--; + if (count == 0) + { + Remove(key); + return true; + } + + this[key] = count; + return false; +#endif + } +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj b/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj index 97e58f8a64..3312f7a619 100644 --- a/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj +++ b/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj @@ -3,6 +3,7 @@ netstandard2.0 enable + false diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs index f079a339df..72311b4d18 100644 --- a/src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs @@ -2,20 +2,22 @@ using System.IO; using System.Xml.Serialization; using Microsoft.CodeAnalysis; -namespace Avalonia.SourceGenerator.CompositionGenerator; - -[Generator(LanguageNames.CSharp)] -public class CompositionRoslynGenerator : IIncrementalGenerator +namespace Avalonia.SourceGenerator.CompositionGenerator { - public void Initialize(IncrementalGeneratorInitializationContext context) + [Generator(LanguageNames.CSharp)] + public class CompositionRoslynGenerator : IIncrementalGenerator { - var schema = context.AdditionalTextsProvider.Where(static file => file.Path.EndsWith("composition-schema.xml")); - var configs = schema.Select((t, _) => - (GConfig)new XmlSerializer(typeof(GConfig)).Deserialize(new StringReader(t.GetText().ToString()))); - context.RegisterSourceOutput(configs, (spc, config) => + public void Initialize(IncrementalGeneratorInitializationContext context) { - var generator = new Generator(spc, config); - generator.Generate(); - }); + var schema = + context.AdditionalTextsProvider.Where(static file => file.Path.EndsWith("composition-schema.xml")); + var configs = schema.Select((t, _) => + (GConfig)new XmlSerializer(typeof(GConfig)).Deserialize(new StringReader(t.GetText().ToString()))); + context.RegisterSourceOutput(configs, (spc, config) => + { + var generator = new Generator(new RoslynCompositionGeneratorSink(spc), config); + generator.Generate(); + }); + } } } \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs index 43ef4a96e8..5a514a4eff 100644 --- a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs @@ -7,14 +7,14 @@ using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static Avalonia.SourceGenerator.CompositionGenerator.Extensions; namespace Avalonia.SourceGenerator.CompositionGenerator { - partial class Generator + public partial class Generator { - private readonly SourceProductionContext _output; + private readonly ICompositionGeneratorSink _output; private readonly GConfig _config; private readonly HashSet _objects; private readonly HashSet _brushes; private readonly Dictionary _manuals; - public Generator(SourceProductionContext output, GConfig config) + public Generator(ICompositionGeneratorSink output, GConfig config) { _output = output; _config = config; @@ -168,17 +168,35 @@ namespace Avalonia.SourceGenerator.CompositionGenerator ExpressionStatement(InvocationExpression(IdentifierName("ApplyChangesExtra")) .AddArgumentListArguments(Argument(IdentifierName("c")))) ); + + var uninitializedObjectName = "dummy"; + var serverStaticCtorBody = cl.Abstract + ? Block() + : Block( + ParseStatement( + $"var dummy = ({serverName})System.Runtime.Serialization.FormatterServices.GetUninitializedObject(typeof({serverName}));"), + ParseStatement($"System.GC.SuppressFinalize(dummy);"), + ParseStatement("InitializeFieldOffsets(dummy);") + ); + + var initializeFieldOffsetsBody = cl.ServerBase == null + ? Block() + : Block(ParseStatement($"{cl.ServerBase}.InitializeFieldOffsets(dummy);")); var resetBody = Block(); var startAnimationBody = Block(); var getPropertyBody = Block(); var serverGetPropertyBody = Block(); + var serverGetFieldOffsetBody = Block(); + var activatedBody = Block(ParseStatement("base.Activated();")); + var deactivatedBody = Block(ParseStatement("base.Deactivated();")); var defaultsMethodBody = Block(); foreach (var prop in cl.Properties) { var fieldName = "_" + prop.Name.WithLowerFirst(); + var fieldOffsetName = "s_OffsetOf" + fieldName; var propType = ParseTypeName(prop.Type); var filteredPropertyType = prop.Type.TrimEnd('?'); var isObject = _objects.Contains(filteredPropertyType); @@ -229,7 +247,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator if (animatedServer) server = server.AddMembers( - DeclareField("AnimatedValueStore<" + serverPropertyType + ">", fieldName), + DeclareField("ServerAnimatedValueStore<" + serverPropertyType + ">", fieldName), PropertyDeclaration(ParseTypeName(serverPropertyType), prop.Name) .AddModifiers(SyntaxKind.PublicKeyword) .WithExpressionBody(ArrowExpressionClause( @@ -240,24 +258,24 @@ namespace Avalonia.SourceGenerator.CompositionGenerator else { server = server - .AddMembers(DeclareField(serverPropertyType, fieldName)) + .AddMembers(DeclareField("ServerValueStore<" + serverPropertyType + ">", fieldName)) .AddMembers(PropertyDeclaration(ParseTypeName(serverPropertyType), prop.Name) .AddModifiers(SyntaxKind.PublicKeyword) .AddAccessorListAccessors( AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, - Block(ReturnStatement(IdentifierName(fieldName)))), + Block(ReturnStatement(MemberAccess(IdentifierName(fieldName), "Value")))), AccessorDeclaration(SyntaxKind.SetAccessorDeclaration, Block( ParseStatement("var changed = false;"), IfStatement(BinaryExpression(SyntaxKind.NotEqualsExpression, - IdentifierName(fieldName), + MemberAccess(IdentifierName(fieldName), "Value"), IdentifierName("value")), Block( ParseStatement("On" + prop.Name + "Changing();"), ParseStatement($"changed = true;")) ), ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, - IdentifierName(fieldName), IdentifierName("value"))), + MemberAccess(IdentifierName(fieldName), "Value"), IdentifierName("value"))), ParseStatement($"if(changed) On" + prop.Name + "Changed();") )) )) @@ -270,15 +288,22 @@ namespace Avalonia.SourceGenerator.CompositionGenerator if (animatedServer) applyMethodBody = applyMethodBody.AddStatements( IfStatement(MemberAccess(changesVar, prop.Name, "IsValue"), - ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, - IdentifierName(fieldName), MemberAccess(changesVar, prop.Name, "Value")))), + ExpressionStatement( + InvocationExpression(MemberAccess(fieldName, "SetValue"), + ArgumentList(SeparatedList(new[] + { + Argument(IdentifierName("this")), + Argument(MemberAccess(changesVar, prop.Name, "Value")), + }))))), IfStatement(MemberAccess(changesVar, prop.Name, "IsAnimation"), ExpressionStatement( InvocationExpression(MemberAccess(fieldName, "SetAnimation"), ArgumentList(SeparatedList(new[] { + Argument(IdentifierName("this")), Argument(changesVar), - Argument(MemberAccess(changesVar, prop.Name, "Animation")) + Argument(MemberAccess(changesVar, prop.Name, "Animation")), + Argument(IdentifierName(fieldOffsetName)) }))))) ); else @@ -288,16 +313,28 @@ namespace Avalonia.SourceGenerator.CompositionGenerator IdentifierName(prop.Name), MemberAccess(changesVar, prop.Name, "Value")))) ); - resetBody = resetBody.AddStatements( ExpressionStatement(InvocationExpression(MemberAccess(prop.Name, "Reset")))); if (animatedServer) + { startAnimationBody = ApplyStartAnimation(startAnimationBody, prop, fieldName); + activatedBody = activatedBody.AddStatements(ParseStatement($"{fieldName}.Activate(this);")); + deactivatedBody = deactivatedBody.AddStatements(ParseStatement($"{fieldName}.Deactivate(this);")); + } + getPropertyBody = ApplyGetProperty(getPropertyBody, prop); - serverGetPropertyBody = ApplyGetProperty(getPropertyBody, prop); + serverGetPropertyBody = ApplyGetProperty(serverGetPropertyBody, prop); + serverGetFieldOffsetBody = ApplyGetProperty(serverGetFieldOffsetBody, prop, fieldOffsetName); + + server = server.AddMembers(DeclareField("int", fieldOffsetName, SyntaxKind.StaticKeyword)); + initializeFieldOffsetsBody = initializeFieldOffsetsBody.AddStatements(ExpressionStatement( + AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, IdentifierName(fieldOffsetName), + InvocationExpression(MemberAccess(IdentifierName(uninitializedObjectName), "GetOffset"), + ArgumentList(SingletonSeparatedList(Argument( + RefExpression(MemberAccess(IdentifierName(uninitializedObjectName), fieldName))))))))); if (prop.DefaultValue != null) { @@ -357,6 +394,21 @@ namespace Avalonia.SourceGenerator.CompositionGenerator Parameter(Identifier("changes")).WithType(ParseTypeName("ChangeSet"))) .WithBody(applyMethodBody)); + server = server.AddMembers(ConstructorDeclaration(serverName) + .WithModifiers(TokenList(Token(SyntaxKind.StaticKeyword))) + .WithBody(serverStaticCtorBody)); + + server = server.AddMembers( + ((MethodDeclarationSyntax)ParseMemberDeclaration( + $"protected static void InitializeFieldOffsets({serverName} dummy){{}}")!) + .WithBody(initializeFieldOffsetsBody)); + + server = server + .AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( + $"protected override void Activated(){{}}")!).WithBody(activatedBody)) + .AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( + $"protected override void Deactivated(){{}}")!).WithBody(deactivatedBody)); + client = client.AddMembers( MethodDeclaration(ParseTypeName("void"), "InitializeDefaults").WithBody(defaultsMethodBody)); @@ -374,7 +426,8 @@ namespace Avalonia.SourceGenerator.CompositionGenerator client = WithGetProperty(client, getPropertyBody, false); server = WithGetProperty(server, serverGetPropertyBody, true); - + server = WithGetFieldOffset(server, serverGetFieldOffsetBody); + if(cl.Implements.Count > 0) foreach (var impl in cl.Implements) { @@ -452,17 +505,19 @@ return; "Vector2", "Vector3", "Vector4", + "Matrix", "Matrix3x2", "Matrix4x4", "Quaternion", - "CompositionColor" + "Color", + "Avalonia.Media.Color" }; - BlockSyntax ApplyGetProperty(BlockSyntax body, GProperty prop) + BlockSyntax ApplyGetProperty(BlockSyntax body, GProperty prop, string? expr = null) { if (VariantPropertyTypes.Contains(prop.Type)) return body.AddStatements( - ParseStatement($"if(name == \"{prop.Name}\")\n return {prop.Name};\n") + ParseStatement($"if(name == \"{prop.Name}\")\n return {expr ?? prop.Name};\n") ); return body; @@ -480,6 +535,19 @@ return; return cl.AddMembers(method); } + + ClassDeclarationSyntax WithGetFieldOffset(ClassDeclarationSyntax cl, BlockSyntax body) + { + if (body.Statements.Count == 0) + return cl; + body = body.AddStatements( + ParseStatement("return base.GetFieldOffset(name);")); + var method = ((MethodDeclarationSyntax)ParseMemberDeclaration( + $"public override int? GetFieldOffset(string name){{}}")) + .WithBody(body); + + return cl.AddMembers(method); + } ClassDeclarationSyntax WithStartAnimation(ClassDeclarationSyntax cl, BlockSyntax body) { diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/ICompositionGeneratorSink.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/ICompositionGeneratorSink.cs new file mode 100644 index 0000000000..085a4041be --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/ICompositionGeneratorSink.cs @@ -0,0 +1,6 @@ +namespace Avalonia.SourceGenerator.CompositionGenerator; + +public interface ICompositionGeneratorSink +{ + void AddSource(string name, string code); +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/RoslynCompositionGeneratorSink.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/RoslynCompositionGeneratorSink.cs new file mode 100644 index 0000000000..6fec3faf93 --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/RoslynCompositionGeneratorSink.cs @@ -0,0 +1,15 @@ +using Microsoft.CodeAnalysis; + +namespace Avalonia.SourceGenerator.CompositionGenerator; + +class RoslynCompositionGeneratorSink : ICompositionGeneratorSink +{ + private readonly SourceProductionContext _ctx; + + public RoslynCompositionGeneratorSink(SourceProductionContext ctx) + { + _ctx = ctx; + } + + public void AddSource(string name, string code) => _ctx.AddSource(name, code); +} \ No newline at end of file From bcbd86eca3bda0b3e6aeee03fb063a2ba6811c28 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 25 May 2022 21:15:45 +0300 Subject: [PATCH 017/389] Added VisualBrush support --- .../Composition/CompositionDrawListVisual.cs | 2 +- .../Drawing/CompositionDrawList.cs | 16 +++++++++- .../Drawing/CompositionDrawingContext.cs | 27 ++++++++++------- .../Composition/Server/DrawingContextProxy.cs | 30 ++++++++++++++++++- .../Server/ServerCompositionDrawListVisual.cs | 5 +--- .../Server/ServerCompositionTarget.cs | 5 ++-- .../Rendering/DeferredRenderer.cs | 6 ++-- .../SceneGraph/BrushDrawOperation.cs | 16 +++++++--- .../SceneGraph/DeferredDrawingContextImpl.cs | 4 +-- .../Rendering/SceneGraph/EllipseNode.cs | 7 ++--- .../Rendering/SceneGraph/GeometryNode.cs | 11 +++---- .../Rendering/SceneGraph/GlyphRunNode.cs | 11 +++---- .../Rendering/SceneGraph/LineNode.cs | 8 ++--- .../Rendering/SceneGraph/OpacityMaskNode.cs | 12 ++++---- .../Rendering/SceneGraph/RectangleNode.cs | 11 +++---- 15 files changed, 104 insertions(+), 67 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs index b19c311663..e4ed0abd29 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs @@ -24,7 +24,7 @@ internal class CompositionDrawListVisual : CompositionContainerVisual private protected override IChangeSetPool ChangeSetPool => DrawListVisualChanges.Pool; - internal CompositionDrawListVisual(Compositor compositor, ServerCompositionContainerVisual server, Visual visual) : base(compositor, server) + internal CompositionDrawListVisual(Compositor compositor, ServerCompositionDrawListVisual server, Visual visual) : base(compositor, server) { Visual = visual; } diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs index aca8ef7c46..1d416f5a8a 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Collections.Pooled; +using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.SceneGraph; using Avalonia.Utilities; @@ -7,6 +8,8 @@ namespace Avalonia.Rendering.Composition.Drawing; internal class CompositionDrawList : PooledList> { + public Size? Size { get; set; } + public CompositionDrawList() { @@ -26,11 +29,22 @@ internal class CompositionDrawList : PooledList> public CompositionDrawList Clone() { - var clone = new CompositionDrawList(Count); + var clone = new CompositionDrawList(Count) { Size = Size }; foreach (var r in this) clone.Add(r.Clone()); return clone; } + + public void Render(CompositorDrawingContextProxy canvas) + { + foreach (var cmd in this) + { + canvas.VisualBrushDrawList = (cmd.Item as BrushDrawOperation)?.Aux as CompositionDrawList; + cmd.Item.Render(canvas); + } + + canvas.VisualBrushDrawList = null; + } } internal class CompositionDrawListBuilder diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs index c8e5d9e064..96c0e22d56 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs @@ -359,25 +359,30 @@ internal class CompositionDrawingContext : IDrawingContextImpl ? _builder.DrawOperations[_drawOperationIndex] as IRef : null; } - - private IDictionary? CreateChildScene(IBrush? brush) + + private IDisposable? CreateChildScene(IBrush? brush) { - /* - var visualBrush = brush as VisualBrush; - - if (visualBrush != null) + if (brush is VisualBrush visualBrush) { var visual = visualBrush.Visual; if (visual != null) { + // TODO: This is a temporary solution to make visual brush to work like it does with DeferredRenderer + // We should directly reference the corresponding CompositionVisual (which should + // be attached to the same composition target) like UWP does. + // Render-able visuals shouldn't be dangling unattached (visual as IVisualBrushInitialize)?.EnsureInitialized(); - var scene = new Scene(visual); - _sceneBuilder.UpdateAll(scene); - return new Dictionary { { visualBrush.Visual, scene } }; - } - }*/ + var drawList = new CompositionDrawList() { Size = visual.Bounds.Size }; + var recorder = new CompositionDrawingContext(); + recorder.BeginUpdate(drawList); + ImmediateRenderer.Render(visual, new DrawingContext(recorder)); + recorder.EndUpdate(); + + return drawList; + } + } return null; } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index 7d061e86a9..8b6ac5b0c2 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -2,6 +2,7 @@ using System.Numerics; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Platform; +using Avalonia.Rendering.Composition.Drawing; using Avalonia.Rendering.SceneGraph; using Avalonia.Utilities; @@ -10,12 +11,21 @@ namespace Avalonia.Rendering.Composition.Server; internal class CompositorDrawingContextProxy : IDrawingContextImpl { private IDrawingContextImpl _impl; + private readonly VisualBrushRenderer _visualBrushRenderer; - public CompositorDrawingContextProxy(IDrawingContextImpl impl) + public CompositorDrawingContextProxy(IDrawingContextImpl impl, VisualBrushRenderer visualBrushRenderer) { _impl = impl; + _visualBrushRenderer = visualBrushRenderer; } + // This is a hack to make it work with the current way of handling visual brushes + public CompositionDrawList? VisualBrushDrawList + { + get => _visualBrushRenderer.VisualBrushDrawList; + set => _visualBrushRenderer.VisualBrushDrawList = value; + } + public Matrix PreTransform { get; set; } = Matrix.Identity; public void Dispose() @@ -135,4 +145,22 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl { _impl.Custom(custom); } + + public class VisualBrushRenderer : IVisualBrushRenderer + { + public CompositionDrawList? VisualBrushDrawList { get; set; } + public Size GetRenderTargetSize(IVisualBrush brush) + { + return VisualBrushDrawList?.Size ?? Size.Empty; + } + + public void RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush) + { + if (VisualBrushDrawList != null) + { + foreach (var cmd in VisualBrushDrawList) + cmd.Item.Render(context); + } + } + } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs index f0384c36fc..ba18211459 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -51,10 +51,7 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua { if (_renderCommands != null) { - foreach (var cmd in _renderCommands) - { - cmd.Item.Render(canvas); - } + _renderCommands.Render(canvas); } base.RenderCore(canvas, transform); } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index dab65fc8ed..d8a5de4f54 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -75,11 +75,12 @@ namespace Avalonia.Rendering.Composition.Server if (!_dirtyRect.IsEmpty) { - using (var context = _layer.CreateDrawingContext(null)) + var visualBrushHelper = new CompositorDrawingContextProxy.VisualBrushRenderer(); + using (var context = _layer.CreateDrawingContext(visualBrushHelper)) { context.PushClip(_dirtyRect); context.Clear(Colors.Transparent); - Root.Render(new CompositorDrawingContextProxy(context), Root.CombinedTransformMatrix); + Root.Render(new CompositorDrawingContextProxy(context, visualBrushHelper), Root.CombinedTransformMatrix); context.PopClip(); } } diff --git a/src/Avalonia.Base/Rendering/DeferredRenderer.cs b/src/Avalonia.Base/Rendering/DeferredRenderer.cs index 82be0a1a0f..4236763e3b 100644 --- a/src/Avalonia.Base/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Base/Rendering/DeferredRenderer.cs @@ -272,16 +272,18 @@ namespace Avalonia.Rendering } } + Scene? TryGetChildScene(IRef? op) => (op?.Item as BrushDrawOperation)?.Aux as Scene; + /// Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) { - return (_currentDraw?.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual]?.Size ?? Size.Empty; + return TryGetChildScene(_currentDraw)?.Size ?? Size.Empty; } /// void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush) { - var childScene = (_currentDraw?.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual]; + var childScene = TryGetChildScene(_currentDraw); if (childScene != null) { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs index cd3dac699a..e81966ce81 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Media; using Avalonia.VisualTree; @@ -9,14 +10,21 @@ namespace Avalonia.Rendering.SceneGraph /// internal abstract class BrushDrawOperation : DrawOperation { - public BrushDrawOperation(Rect bounds, Matrix transform) + public BrushDrawOperation(Rect bounds, Matrix transform, IDisposable? aux) : base(bounds, transform) { + Aux = aux; } /// - /// Gets a collection of child scenes that are needed to draw visual brushes. + /// Auxiliary data required to draw the brush /// - public abstract IDictionary? ChildScenes { get; } + public IDisposable? Aux { get; } + + public override void Dispose() + { + Aux?.Dispose(); + base.Dispose(); + } } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 3f495c619c..07082e4ac3 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -457,7 +457,7 @@ namespace Avalonia.Rendering.SceneGraph return _drawOperationindex < _node!.DrawOperations.Count ? _node.DrawOperations[_drawOperationindex] as IRef : null; } - private IDictionary? CreateChildScene(IBrush? brush) + private IDisposable? CreateChildScene(IBrush? brush) { var visualBrush = brush as VisualBrush; @@ -470,7 +470,7 @@ namespace Avalonia.Rendering.SceneGraph (visual as IVisualBrushInitialize)?.EnsureInitialized(); var scene = new Scene(visual); _sceneBuilder.UpdateAll(scene); - return new Dictionary { { visualBrush.Visual, scene } }; + return scene; } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs index c1fc6a81f6..4600653b9d 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs @@ -17,14 +17,13 @@ namespace Avalonia.Rendering.SceneGraph IBrush? brush, IPen? pen, Rect rect, - IDictionary? childScenes = null) - : base(rect.Inflate(pen?.Thickness ?? 0), transform) + IDisposable? aux = null) + : base(rect.Inflate(pen?.Thickness ?? 0), transform, aux) { Transform = transform; Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Rect = rect; - ChildScenes = childScenes; } /// @@ -47,8 +46,6 @@ namespace Avalonia.Rendering.SceneGraph /// public Rect Rect { get; } - public override IDictionary? ChildScenes { get; } - public bool Equals(Matrix transform, IBrush? brush, IPen? pen, Rect rect) { return transform == Transform && diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs index 70748989d6..4b43f93aee 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Platform; @@ -23,14 +24,13 @@ namespace Avalonia.Rendering.SceneGraph IBrush? brush, IPen? pen, IGeometryImpl geometry, - IDictionary? childScenes = null) - : base(geometry.GetRenderBounds(pen).CalculateBoundsWithLineCaps(pen), transform) + IDisposable? aux) + : base(geometry.GetRenderBounds(pen).CalculateBoundsWithLineCaps(pen), transform, aux) { Transform = transform; Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Geometry = geometry; - ChildScenes = childScenes; } /// @@ -53,9 +53,6 @@ namespace Avalonia.Rendering.SceneGraph /// public IGeometryImpl Geometry { get; } - /// - public override IDictionary? ChildScenes { get; } - /// /// Determines if this draw operation equals another. /// diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs index d6da087120..9199611ed6 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Media; using Avalonia.Media.Immutable; @@ -23,13 +24,12 @@ namespace Avalonia.Rendering.SceneGraph Matrix transform, IBrush foreground, GlyphRun glyphRun, - IDictionary? childScenes = null) - : base(new Rect(glyphRun.Size), transform) + IDisposable? aux = null) + : base(new Rect(glyphRun.Size), transform, aux) { Transform = transform; Foreground = foreground.ToImmutable(); GlyphRun = glyphRun; - ChildScenes = childScenes; } /// @@ -47,9 +47,6 @@ namespace Avalonia.Rendering.SceneGraph /// public GlyphRun GlyphRun { get; } - /// - public override IDictionary? ChildScenes { get; } - /// public override void Render(IDrawingContextImpl context) { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs index a9e1ce8ed7..ee5ec0a5fc 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs @@ -25,14 +25,13 @@ namespace Avalonia.Rendering.SceneGraph IPen pen, Point p1, Point p2, - IDictionary? childScenes = null) - : base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform) + IDisposable? aux = null) + : base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform, aux) { Transform = transform; Pen = pen.ToImmutable(); P1 = p1; P2 = p2; - ChildScenes = childScenes; } /// @@ -55,9 +54,6 @@ namespace Avalonia.Rendering.SceneGraph /// public Point P2 { get; } - /// - public override IDictionary? ChildScenes { get; } - /// /// Determines if this draw operation equals another. /// diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs index 4b6e7d2254..549c1fd7de 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Media; using Avalonia.Platform; using Avalonia.VisualTree; @@ -17,12 +18,11 @@ namespace Avalonia.Rendering.SceneGraph /// The opacity mask to push. /// The bounds of the mask. /// Child scenes for drawing visual brushes. - public OpacityMaskNode(IBrush mask, Rect bounds, IDictionary? childScenes = null) - : base(Rect.Empty, Matrix.Identity) + public OpacityMaskNode(IBrush mask, Rect bounds, IDisposable? aux = null) + : base(Rect.Empty, Matrix.Identity, aux) { Mask = mask.ToImmutable(); MaskBounds = bounds; - ChildScenes = childScenes; } /// @@ -30,7 +30,7 @@ namespace Avalonia.Rendering.SceneGraph /// opacity mask pop. /// public OpacityMaskNode() - : base(Rect.Empty, Matrix.Identity) + : base(Rect.Empty, Matrix.Identity, null) { } @@ -44,8 +44,6 @@ namespace Avalonia.Rendering.SceneGraph /// public Rect? MaskBounds { get; } - /// - public override IDictionary? ChildScenes { get; } /// public override bool HitTest(Point p) => false; diff --git a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs index 3279c3a549..7b79c446f9 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Platform; @@ -26,14 +27,13 @@ namespace Avalonia.Rendering.SceneGraph IPen? pen, RoundedRect rect, BoxShadows boxShadows, - IDictionary? childScenes = null) - : base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform) + IDisposable? aux = null) + : base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform, aux) { Transform = transform; Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Rect = rect; - ChildScenes = childScenes; BoxShadows = boxShadows; } @@ -62,9 +62,6 @@ namespace Avalonia.Rendering.SceneGraph /// public BoxShadows BoxShadows { get; } - /// - public override IDictionary? ChildScenes { get; } - /// /// Determines if this draw operation equals another. /// From 7e4fa1d84b7a8eac22fa72fb2d0c9c612e743bf3 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 25 May 2022 21:46:52 +0300 Subject: [PATCH 018/389] Fixed hit-testing for the first frame --- .../Composition/Server/ReadbackIndices.cs | 25 ++++++++----------- .../Server/ServerCompositionTarget.cs | 10 +++++--- .../Composition/Server/ServerVisual.cs | 3 +-- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs b/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs index 372fa4d9ce..1971451811 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs @@ -4,29 +4,24 @@ namespace Avalonia.Rendering.Composition.Server { private readonly object _lock = new object(); public int ReadIndex { get; private set; } = 0; - public int WriteIndex { get; private set; } = -1; + public int WriteIndex { get; private set; } = 1; + public int WrittenIndex { get; private set; } = 0; public ulong ReadRevision { get; private set; } - public ulong WriteRevision { get; private set; } - private ulong[] _revisions = new ulong[3]; - - + public ulong LastWrittenRevision { get; private set; } + public void NextRead() { lock (_lock) { - for (var c = 0; c < 3; c++) + if (ReadRevision < LastWrittenRevision) { - if (c != WriteIndex && c != ReadIndex && _revisions[c] > ReadRevision) - { - ReadIndex = c; - ReadRevision = _revisions[c]; - return; - } + ReadIndex = WrittenIndex; + ReadRevision = LastWrittenRevision; } } } - public void NextWrite(ulong revision) + public void CompleteWrite(ulong writtenRevision) { lock (_lock) { @@ -34,9 +29,9 @@ namespace Avalonia.Rendering.Composition.Server { if (c != WriteIndex && c != ReadIndex) { + WrittenIndex = WriteIndex; + LastWrittenRevision = writtenRevision; WriteIndex = c; - WriteRevision = revision; - _revisions[c] = revision; return; } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index d8a5de4f54..a50562eabc 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -15,7 +15,7 @@ namespace Avalonia.Rendering.Composition.Server private readonly Func _renderTargetFactory; private static long s_nextId = 1; public long Id { get; } - private ulong _frame = 1; + public ulong Revision { get; private set; } private IRenderTarget? _renderTarget; private FpsCounter _fpsCounter = new FpsCounter(Typeface.Default.GlyphTypeface); private Rect _dirtyRect; @@ -58,9 +58,14 @@ namespace Avalonia.Rendering.Composition.Server if(_dirtyRect.IsEmpty && !_redrawRequested) return; + + Revision++; + // Update happens in a separate phase to extend dirty rect if needed Root.Update(this, Matrix4x4.Identity); + Readback.CompleteWrite(Revision); + _redrawRequested = false; using (var targetContext = _renderTarget.CreateDrawingContext(null)) { @@ -102,9 +107,6 @@ namespace Avalonia.Rendering.Composition.Server _dirtyRect = Rect.Empty; } - - Readback.NextWrite(_frame); - _frame++; } private static Rect SnapToDevicePixels(Rect rect, double scale) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 05b63a7a73..4e320c34be 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -63,7 +63,7 @@ namespace Avalonia.Rendering.Composition.Server Scale, RotationAngle, Orientation, Offset); var i = Root!.Readback; ref var readback = ref GetReadback(i.WriteIndex); - readback.Revision = i.WriteRevision; + readback.Revision = root.Revision; readback.Matrix = res; readback.TargetId = Root.Id; //TODO: check effective opacity too @@ -85,7 +85,6 @@ namespace Avalonia.Rendering.Composition.Server public struct ReadbackData { public Matrix4x4 Matrix; - public bool Visible; public ulong Revision; public long TargetId; } From 7a9d9ea304e3e96f0b857afb9b3c14bb864fd6b9 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 25 May 2022 22:17:35 +0300 Subject: [PATCH 019/389] Invalidate visual's rect if it's moved in the global coordinate space --- .../Rendering/Composition/Server/ServerVisual.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 4e320c34be..9bfc909fe4 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -69,7 +69,14 @@ namespace Avalonia.Rendering.Composition.Server //TODO: check effective opacity too IsVisibleInFrame = Visible && Opacity > 0; CombinedTransformMatrix = res; - GlobalTransformMatrix = res * transform; + var newTransform = res * transform; + if (GlobalTransformMatrix != newTransform) + { + // Visual was moved alongside with its parent + _isDirty = true; + Root.AddDirtyRect(TransformedBounds); + } + GlobalTransformMatrix = newTransform; //TODO: Cache TransformedBounds = ContentBounds.TransformToAABB(MatrixUtils.ToMatrix(GlobalTransformMatrix)); From f0989357a01a1df28ad5b522aad180e08d5e539f Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 26 May 2022 15:29:56 +0300 Subject: [PATCH 020/389] Visiblity check fix --- src/Avalonia.Base/Rendering/Composition/Visual.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Visual.cs b/src/Avalonia.Base/Rendering/Composition/Visual.cs index f7c8078073..fa8d5d8f3b 100644 --- a/src/Avalonia.Base/Rendering/Composition/Visual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Visual.cs @@ -45,8 +45,8 @@ namespace Avalonia.Rendering.Composition var i = Root.Server.Readback; ref var readback = ref Server.GetReadback(i.ReadIndex); - // CompositionVisual wasn't visible - if (readback.Revision < i.ReadRevision) + // CompositionVisual wasn't visible or wasn't even attached to the composition target during the lat frame + if (!readback.Visible || readback.Revision < i.ReadRevision) return null; // CompositionVisual was reparented (potential race here) From f974859323c06b493226dc8d85de66349a5ac4a9 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 26 May 2022 15:30:12 +0300 Subject: [PATCH 021/389] Change stream WIP --- .../Composition/Server/ServerVisual.cs | 51 ++++-- .../Composition/Transport/BatchStream.cs | 173 ++++++++++++++++++ .../Transport/BatchStreamArrayPool.cs | 144 +++++++++++++++ 3 files changed, 354 insertions(+), 14 deletions(-) create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 9bfc909fe4..801bfb2f65 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -8,6 +8,7 @@ namespace Avalonia.Rendering.Composition.Server { private bool _isDirty; private ServerCompositionTarget? _root; + private bool _isBackface; protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) { @@ -59,23 +60,35 @@ namespace Avalonia.Rendering.Composition.Server public virtual void Update(ServerCompositionTarget root, Matrix4x4 transform) { - var res = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, + // Calculate new parent-relative transform + CombinedTransformMatrix = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, Scale, RotationAngle, Orientation, Offset); - var i = Root!.Readback; - ref var readback = ref GetReadback(i.WriteIndex); - readback.Revision = root.Revision; - readback.Matrix = res; - readback.TargetId = Root.Id; - //TODO: check effective opacity too - IsVisibleInFrame = Visible && Opacity > 0; - CombinedTransformMatrix = res; - var newTransform = res * transform; + + var newTransform = CombinedTransformMatrix * transform; + + // Check if visual was moved and recalculate face orientation + var positionChanged = false; if (GlobalTransformMatrix != newTransform) { - // Visual was moved alongside with its parent - _isDirty = true; - Root.AddDirtyRect(TransformedBounds); + _isBackface = Vector3.Transform( + new Vector3(0, 0, float.PositiveInfinity), GlobalTransformMatrix).Z <= 0; + positionChanged = true; + } + + var wasVisible = IsVisibleInFrame; + //TODO: check effective opacity too + IsVisibleInFrame = Visible && Opacity > 0 && !_isBackface; + + // Invalidate previous rect and queue new rect based on visibility + if (positionChanged) + { + if(wasVisible) + Root!.AddDirtyRect(TransformedBounds); + + if (IsVisibleInFrame) + _isDirty = true; } + GlobalTransformMatrix = newTransform; //TODO: Cache TransformedBounds = ContentBounds.TransformToAABB(MatrixUtils.ToMatrix(GlobalTransformMatrix)); @@ -84,9 +97,18 @@ namespace Avalonia.Rendering.Composition.Server _isDirty = false; else if (_isDirty) { - Root.AddDirtyRect(TransformedBounds); + Root!.AddDirtyRect(TransformedBounds); _isDirty = false; } + + // Update readback indices + var i = Root!.Readback; + ref var readback = ref GetReadback(i.WriteIndex); + readback.Revision = root.Revision; + readback.Matrix = CombinedTransformMatrix; + readback.TargetId = Root.Id; + readback.Visible = IsVisibleInFrame; + } public struct ReadbackData @@ -94,6 +116,7 @@ namespace Avalonia.Rendering.Composition.Server public Matrix4x4 Matrix; public ulong Revision; public long TargetId; + public bool Visible; } partial void ApplyChangesExtra(CompositionVisualChanges c) diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs new file mode 100644 index 0000000000..feb892d134 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport; + +internal class BatchStreamData +{ + public Queue> Objects { get; } = new(); + public Queue> Structs { get; } = new(); +} + +public struct BatchStreamSegment +{ + public TData Data { get; set; } + public int ElementCount { get; set; } +} + +internal class BatchStreamWriter : IDisposable +{ + private readonly BatchStreamData _output; + private readonly BatchStreamMemoryPool _memoryPool; + private readonly BatchStreamObjectPool _objectPool; + + private BatchStreamSegment _currentObjectSegment; + private BatchStreamSegment _currentDataSegment; + + public BatchStreamWriter(BatchStreamData output, BatchStreamMemoryPool memoryPool, BatchStreamObjectPool objectPool) + { + _output = output; + _memoryPool = memoryPool; + _objectPool = objectPool; + } + + void CommitDataSegment() + { + if (_currentDataSegment.Data != IntPtr.Zero) + _output.Structs.Enqueue(_currentDataSegment); + _currentDataSegment = new (); + } + + void NextDataSegment() + { + CommitDataSegment(); + _currentDataSegment.Data = _memoryPool.Get(); + } + + void CommitObjectSegment() + { + if (_currentObjectSegment.Data != null) + _output.Objects.Enqueue(_currentObjectSegment!); + _currentObjectSegment = new(); + } + + void NextObjectSegment() + { + CommitObjectSegment(); + _currentObjectSegment.Data = _objectPool.Get(); + } + + public unsafe void Write(T item) where T : unmanaged + { + var size = Unsafe.SizeOf(); + if (_currentDataSegment.Data == IntPtr.Zero || _currentDataSegment.ElementCount + size > _memoryPool.BufferSize) + NextDataSegment(); + *(T*)((byte*)_currentDataSegment.Data + _currentDataSegment.ElementCount) = item; + _currentDataSegment.ElementCount += size; + } + + public void Write(ServerObject item) + { + if (_currentObjectSegment.Data == null || + _currentObjectSegment.ElementCount >= _currentObjectSegment.Data.Length) + NextObjectSegment(); + _currentObjectSegment.Data![_currentObjectSegment.ElementCount] = item; + _currentObjectSegment.ElementCount++; + } + + public void Dispose() + { + CommitDataSegment(); + CommitObjectSegment(); + } +} + +internal class BatchStreamReader : IDisposable +{ + private readonly BatchStreamData _input; + private readonly BatchStreamMemoryPool _memoryPool; + private readonly BatchStreamObjectPool _objectPool; + + private BatchStreamSegment _currentObjectSegment; + private BatchStreamSegment _currentDataSegment; + private int _memoryOffset, _objectOffset; + + public BatchStreamReader(BatchStreamData _input, BatchStreamMemoryPool memoryPool, BatchStreamObjectPool objectPool) + { + this._input = _input; + _memoryPool = memoryPool; + _objectPool = objectPool; + } + + public unsafe T Read() where T : unmanaged + { + var size = Unsafe.SizeOf(); + if (_currentDataSegment.Data == IntPtr.Zero) + { + if (_input.Structs.Count == 0) + throw new EndOfStreamException(); + _currentDataSegment = _input.Structs.Dequeue(); + _memoryOffset = 0; + } + + if (_memoryOffset + size > _currentDataSegment.ElementCount) + throw new InvalidOperationException("Attempted to read more memory then left in the current segment"); + + var rv = *(T*)((byte*)_currentDataSegment.Data + size); + _memoryOffset += size; + if (_memoryOffset == _currentDataSegment.ElementCount) + { + _memoryPool.Return(_currentDataSegment.Data); + _currentDataSegment = new(); + } + + return rv; + } + + public ServerObject ReadObject() + { + if (_currentObjectSegment.Data == null) + { + if (_input.Objects.Count == 0) + throw new EndOfStreamException(); + _currentObjectSegment = _input.Objects.Dequeue()!; + _objectOffset = 0; + } + + var rv = _currentObjectSegment.Data![_objectOffset]; + _objectOffset++; + if (_objectOffset == _currentObjectSegment.ElementCount) + { + _objectPool.Return(_currentObjectSegment.Data); + _currentObjectSegment = new(); + } + + return rv; + } + + public bool IsStructEof => _currentDataSegment.Data == IntPtr.Zero && _input.Structs.Count == 0; + + public void Dispose() + { + if (_currentDataSegment.Data != IntPtr.Zero) + { + _memoryPool.Return(_currentDataSegment.Data); + _currentDataSegment = new(); + } + + while (_input.Structs.Count > 0) + _memoryPool.Return(_input.Structs.Dequeue().Data); + + if (_currentObjectSegment.Data != null) + { + _objectPool.Return(_currentObjectSegment.Data); + _currentObjectSegment = new(); + } + + while (_input.Objects.Count > 0) + _objectPool.Return(_input.Objects.Dequeue().Data); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs new file mode 100644 index 0000000000..913958765a --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; +using Avalonia.Threading; + +namespace Avalonia.Rendering.Composition.Transport; + +/// +/// A pool that keeps a number of elements that was used in the last 10 seconds +/// +internal abstract class BatchStreamPoolBase : IDisposable +{ + readonly Stack _pool = new(); + bool _disposed; + int _usage; + readonly int[] _usageStatistics = new int[10]; + int _usageStatisticsSlot; + + public BatchStreamPoolBase(bool needsFinalize = false) + { + if(!needsFinalize) + GC.SuppressFinalize(needsFinalize); + + var updateRef = new WeakReference>(this); + StartUpdateTimer(updateRef); + } + + static void StartUpdateTimer(WeakReference> updateRef) + { + DispatcherTimer.Run(() => + { + if (updateRef.TryGetTarget(out var target)) + { + target.UpdateStatistics(); + return true; + } + return false; + + }, TimeSpan.FromSeconds(1)); + } + + private void UpdateStatistics() + { + lock (_pool) + { + var maximumUsage = _usageStatistics.Max(); + var recentlyUsedPooledSlots = maximumUsage - _usage; + while (recentlyUsedPooledSlots < _pool.Count) + DestroyItem(_pool.Pop()); + + _usageStatistics[_usage] = 0; + _usageStatisticsSlot = (_usageStatisticsSlot + 1) % _usageStatistics.Length; + } + } + + protected abstract T CreateItem(); + + protected virtual void DestroyItem(T item) + { + + } + + public T Get() + { + lock (_pool) + { + _usage++; + if (_usageStatistics[_usageStatisticsSlot] < _usage) + _usageStatistics[_usageStatisticsSlot] = _usage; + + if (_pool.Count != 0) + return _pool.Pop(); + } + + return CreateItem(); + } + + public void Return(T item) + { + lock (_pool) + { + _usage--; + if (!_disposed) + { + _pool.Push(item); + return; + } + } + + DestroyItem(item); + } + + public void Dispose() + { + lock (_pool) + { + _disposed = true; + foreach (var item in _pool) + DestroyItem(item); + _pool.Clear(); + } + } + + ~BatchStreamPoolBase() + { + Dispose(); + } +} + +internal sealed class BatchStreamObjectPool : BatchStreamPoolBase where T : class +{ + private readonly int _arraySize; + + public BatchStreamObjectPool(int arraySize = 1024) + { + _arraySize = arraySize; + } + + protected override T[] CreateItem() + { + return new T[_arraySize]; + } + + protected override void DestroyItem(T[] item) + { + Array.Clear(item, 0, item.Length); + } +} + +internal sealed class BatchStreamMemoryPool : BatchStreamPoolBase +{ + public int BufferSize { get; } + + public BatchStreamMemoryPool(int bufferSize = 16384) + { + BufferSize = bufferSize; + } + + protected override IntPtr CreateItem() => Marshal.AllocHGlobal(BufferSize); + + protected override void DestroyItem(IntPtr item) => Marshal.FreeHGlobal(item); +} \ No newline at end of file From 4991d4f370acd7503260258722bcad328e619d65 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 26 May 2022 21:35:50 +0300 Subject: [PATCH 022/389] Switched to byte-stream based transport --- .../Animations/AnimatedValueStore.cs | 5 +- .../Animations/CompositionAnimation.cs | 5 +- .../Animations/CompositionAnimationGroup.cs | 2 - .../Animations/ExpressionAnimation.cs | 2 +- .../Animations/ImplicitAnimationCollection.cs | 2 - .../Composition/CompositionDrawListVisual.cs | 16 +- .../Composition/CompositionEasingFunction.cs | 4 +- .../Composition/CompositionObject.cs | 57 ++- .../Composition/CompositionPropertySet.cs | 12 +- .../Composition/CompositionTarget.cs | 2 +- .../Rendering/Composition/Compositor.cs | 50 ++- .../Rendering/Composition/ContainerVisual.cs | 13 +- .../Rendering/Composition/CustomDrawVisual.cs | 56 --- .../Server/ServerCompositionDrawListVisual.cs | 10 +- .../Server/ServerCompositionTarget.cs | 4 +- .../Composition/Server/ServerCompositor.cs | 23 +- .../Server/ServerContainerVisual.cs | 9 +- .../Server/ServerCustomDrawVisual.cs | 31 -- .../Composition/Server/ServerList.cs | 24 +- .../Composition/Server/ServerObject.cs | 28 +- .../Composition/Server/ServerVisual.cs | 31 +- .../Rendering/Composition/Transport/Batch.cs | 7 +- .../Composition/Transport/BatchStream.cs | 27 +- .../Transport/BatchStreamArrayPool.cs | 24 +- .../Transport/BatchStreamDebugMarker.cs | 9 + .../Rendering/Composition/Transport/Change.cs | 82 ---- .../Composition/Transport/ChangeSet.cs | 36 -- .../Composition/Transport/ChangeSetPool.cs | 42 -- .../Transport/CompositionTargetChanges.cs | 11 - .../Transport/CustomDrawVisualChanges.cs | 20 - .../Transport/DrawListVisualChanges.cs | 48 -- .../Composition/Transport/ListChange.cs | 19 - .../Composition/Transport/ListChangeSet.cs | 25 -- .../Transport/ServerListProxyHelper.cs | 55 +-- .../Composition/Transport/VisualChanges.cs | 16 - .../Rendering/Composition/Visual.cs | 35 +- .../Rendering/Composition/VisualCollection.cs | 1 + src/Avalonia.Base/composition-schema.xml | 7 +- .../CompositionGenerator/Config.cs | 6 + .../CompositionGenerator/Extensions.cs | 9 + .../Generator.KeyFrameAnimation.cs | 2 +- .../Generator.ListProxy.cs | 12 +- .../CompositionGenerator/Generator.cs | 413 ++++++++++-------- src/Avalonia.X11/X11Platform.cs | 2 +- src/Windows/Avalonia.Win32/Win32Platform.cs | 2 +- .../Composition/BatchStreamTests.cs | 45 ++ 46 files changed, 532 insertions(+), 809 deletions(-) delete mode 100644 src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamDebugMarker.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/Change.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/CompositionTargetChanges.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs create mode 100644 tests/Avalonia.Base.UnitTests/Composition/BatchStreamTests.cs diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs b/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs index e877b50b20..95bc384743 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs @@ -1,3 +1,4 @@ +using System; using System.Runtime.InteropServices; using Avalonia.Rendering.Composition.Expressions; using Avalonia.Rendering.Composition.Server; @@ -81,7 +82,7 @@ namespace Avalonia.Rendering.Composition.Animations public bool IsAnimation => _animation != null; - public void SetAnimation(ServerObject target, ChangeSet cs, IAnimationInstance animation, int storeOffset) + public void SetAnimation(ServerObject target, TimeSpan commitedAt, IAnimationInstance animation, int storeOffset) { _direct = default; if (_animation != null) @@ -91,7 +92,7 @@ namespace Avalonia.Rendering.Composition.Animations } _animation = animation; - _animation.Initialize(cs.Batch.CommitedAt, ExpressionVariant.Create(LastAnimated), storeOffset); + _animation.Initialize(commitedAt, ExpressionVariant.Create(LastAnimated), storeOffset); if (target.IsActive) _animation.Activate(); diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs index fe20115b38..cf81c6e656 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs @@ -18,8 +18,6 @@ namespace Avalonia.Rendering.Composition.Animations _propertySet = new CompositionPropertySet(compositor); } - private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException(); - public void ClearAllParameters() => _propertySet.ClearAll(); public void ClearParameter(string key) => _propertySet.Clear(key); @@ -52,8 +50,7 @@ namespace Avalonia.Rendering.Composition.Animations internal abstract IAnimationInstance CreateInstance(ServerObject targetObject, ExpressionVariant? finalValue); - internal PropertySetSnapshot CreateSnapshot(bool server) - => _propertySet.Snapshot(server); + internal PropertySetSnapshot CreateSnapshot() => _propertySet.Snapshot(); void ICompositionAnimationBase.InternalOnly() { diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs index 833f7e498c..89f8ba411d 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs @@ -20,7 +20,5 @@ namespace Avalonia.Rendering.Composition.Animations public CompositionAnimationGroup(Compositor compositor) : base(compositor, null!) { } - - private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException(); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs index a6f24c2e35..6a2c07e6ef 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs @@ -29,6 +29,6 @@ namespace Avalonia.Rendering.Composition.Animations internal override IAnimationInstance CreateInstance( ServerObject targetObject, ExpressionVariant? finalValue) => new ExpressionAnimationInstance(ParsedExpression, - targetObject, finalValue, CreateSnapshot(true)); + targetObject, finalValue, CreateSnapshot()); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs index be91352527..fa5b69dae9 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs @@ -15,8 +15,6 @@ namespace Avalonia.Rendering.Composition.Animations _innerface = _inner; } - private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException(); - public IEnumerator> GetEnumerator() => _inner.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable) _inner).GetEnumerator(); diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs index e4ed0abd29..069d888fbb 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs @@ -9,7 +9,7 @@ internal class CompositionDrawListVisual : CompositionContainerVisual { public Visual Visual { get; } - private new DrawListVisualChanges Changes => (DrawListVisualChanges)base.Changes; + private bool _drawListChanged; private CompositionDrawList? _drawList; public CompositionDrawList? DrawList { @@ -18,11 +18,21 @@ internal class CompositionDrawListVisual : CompositionContainerVisual { _drawList?.Dispose(); _drawList = value; - Changes.DrawCommands = value?.Clone(); + _drawListChanged = true; + RegisterForSerialization(); } } - private protected override IChangeSetPool ChangeSetPool => DrawListVisualChanges.Pool; + private protected override void SerializeChangesCore(BatchStreamWriter writer) + { + writer.Write((byte)(_drawListChanged ? 1 : 0)); + if (_drawListChanged) + { + writer.WriteObject(DrawList?.Clone()); + _drawListChanged = false; + } + base.SerializeChangesCore(writer); + } internal CompositionDrawListVisual(Compositor compositor, ServerCompositionDrawListVisual server, Visual visual) : base(compositor, server) { diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs b/src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs index 73db243e93..90b2bec268 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs @@ -10,9 +10,7 @@ namespace Avalonia.Rendering.Composition internal CompositionEasingFunction(Compositor compositor) : base(compositor, null!) { } - - private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException(); - + internal abstract IEasingFunction Snapshot(); } diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs index 2417ecaba8..d561338a36 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs @@ -6,7 +6,7 @@ using Avalonia.Rendering.Composition.Transport; namespace Avalonia.Rendering.Composition { - public abstract class CompositionObject : IDisposable, IExpressionObject + public abstract class CompositionObject : IDisposable { public ImplicitAnimationCollection? ImplicitAnimations { get; set; } internal CompositionObject(Compositor compositor, ServerObject server) @@ -18,46 +18,17 @@ namespace Avalonia.Rendering.Composition public Compositor Compositor { get; } internal ServerObject Server { get; } public bool IsDisposed { get; private set; } - private ChangeSet? _changes; + private bool _registeredForSerialization; private static void ThrowInvalidOperation() => throw new InvalidOperationException("There is no server-side counterpart for this object"); - - private protected ChangeSet Changes - { - get - { - // ReSharper disable once ConditionIsAlwaysTrueOrFalse - if (Server == null) ThrowInvalidOperation(); - var currentBatch = Compositor.CurrentBatch; - if (_changes != null && _changes.Batch != currentBatch) - _changes = null; - if (_changes == null) - { - _changes = ChangeSetPool.Get(Server!, currentBatch); - currentBatch.Changes!.Add(_changes); - Compositor.QueueImplicitBatchCommit(); - } - - return _changes; - } - } - - private protected abstract IChangeSetPool ChangeSetPool { get; } public void Dispose() { - Changes.Dispose = true; + //Changes.Dispose = true; IsDisposed = true; } - internal virtual ExpressionVariant GetPropertyForAnimation(string name) - { - return default; - } - - ExpressionVariant IExpressionObject.GetProperty(string name) => GetPropertyForAnimation(name); - public void StartAnimation(string propertyName, CompositionAnimation animation) => StartAnimation(propertyName, animation, null); @@ -121,5 +92,27 @@ namespace Avalonia.Rendering.Composition throw new ArgumentException(); } + + protected void RegisterForSerialization() + { + if (Server == null) + throw new InvalidOperationException("The object doesn't have an associated server counterpart"); + + if(_registeredForSerialization) + return; + _registeredForSerialization = true; + Compositor.RegisterForSerialization(this); + } + + internal void SerializeChanges(BatchStreamWriter writer) + { + _registeredForSerialization = false; + SerializeChangesCore(writer); + } + + private protected virtual void SerializeChangesCore(BatchStreamWriter writer) + { + + } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs index bc0ce804dc..584969cbc0 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs @@ -16,8 +16,6 @@ namespace Avalonia.Rendering.Composition { } - private protected override IChangeSetPool ChangeSetPool => throw new NotSupportedException(); - internal void Set(string key, ExpressionVariant value) { _objects.Remove(key); @@ -104,10 +102,10 @@ namespace Avalonia.Rendering.Composition _variants.Remove(key); } - internal PropertySetSnapshot Snapshot(bool server) => - SnapshotCore(server, 1); + internal PropertySetSnapshot Snapshot() => + SnapshotCore(1); - private PropertySetSnapshot SnapshotCore(bool server, int allowedNestingLevel) + private PropertySetSnapshot SnapshotCore(int allowedNestingLevel) { var dic = new Dictionary(_objects.Count + _variants.Count); foreach (var o in _objects) @@ -116,12 +114,12 @@ namespace Avalonia.Rendering.Composition { if (allowedNestingLevel <= 0) throw new InvalidOperationException("PropertySet depth limit reached"); - dic[o.Key] = new PropertySetSnapshot.Value(ps.SnapshotCore(server, allowedNestingLevel - 1)); + dic[o.Key] = new PropertySetSnapshot.Value(ps.SnapshotCore(allowedNestingLevel - 1)); } else if (o.Value.Server == null) throw new InvalidOperationException($"Object of type {o.Value.GetType()} is not allowed"); else - dic[o.Key] = new PropertySetSnapshot.Value(server ? (IExpressionObject) o.Value.Server : o.Value); + dic[o.Key] = new PropertySetSnapshot.Value(o.Value.Server); } foreach (var v in _variants) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs index 8d052389c2..c5cfaeacce 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs @@ -102,6 +102,6 @@ namespace Avalonia.Rendering.Composition return false; } - public void RequestRedraw() => Changes.RedrawRequested.Value = true; + public void RequestRedraw() => RegisterForSerialization(); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index 14d779dbc4..96564f0800 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -16,18 +16,17 @@ namespace Avalonia.Rendering.Composition public partial class Compositor { private ServerCompositor _server; - private Batch _currentBatch; private bool _implicitBatchCommitQueued; private Action _implicitBatchCommit; - - internal Batch CurrentBatch => _currentBatch; + private BatchStreamObjectPool _batchObjectPool = new(); + private BatchStreamMemoryPool _batchMemoryPool = new(); + private List _objectsForSerialization = new(); internal ServerCompositor Server => _server; internal CompositionEasingFunction DefaultEasing { get; } - - private Compositor(ServerCompositor server) + + public Compositor(IRenderLoop loop) { - _server = server; - _currentBatch = new Batch(); + _server = new ServerCompositor(loop, _batchObjectPool, _batchMemoryPool); _implicitBatchCommit = ImplicitBatchCommit; DefaultEasing = new CubicBezierEasingFunction(this, new Vector2(0.25f, 0.1f), new Vector2(0.25f, 1f)); @@ -40,18 +39,27 @@ namespace Avalonia.Rendering.Composition public Task RequestCommitAsync() { - var batch = CurrentBatch; - _currentBatch = new Batch(); + var batch = new Batch(); + + using (var writer = new BatchStreamWriter(batch.Changes, _batchMemoryPool, _batchObjectPool)) + { + foreach (var obj in _objectsForSerialization) + { + writer.WriteObject(obj.Server); + obj.SerializeChanges(writer); +#if DEBUG_COMPOSITOR_SERIALIZATION + writer.Write(BatchStreamDebugMarkers.ObjectEndMagic); + writer.WriteObject(BatchStreamDebugMarkers.ObjectEndMarker); +#endif + } + _objectsForSerialization.Clear(); + } + batch.CommitedAt = Server.Clock.Elapsed; _server.EnqueueBatch(batch); return batch.Completed; } - public static Compositor Create(IRenderLoop timer) - { - return new Compositor(new ServerCompositor(timer)); - } - public void Dispose() { @@ -122,12 +130,8 @@ namespace Avalonia.Rendering.Composition public ImplicitAnimationCollection CreateImplicitAnimationCollection() => new ImplicitAnimationCollection(this); public CompositionAnimationGroup CreateAnimationGroup() => new CompositionAnimationGroup(this); - - internal CustomDrawVisual CreateCustomDrawVisual(ICustomDrawVisualRenderer renderer, - ICustomDrawVisualHitTest? hitTest = null) where T : IEquatable => - new CustomDrawVisual(this, renderer, hitTest); - - public void QueueImplicitBatchCommit() + + private void QueueImplicitBatchCommit() { if(_implicitBatchCommitQueued) return; @@ -140,5 +144,11 @@ namespace Avalonia.Rendering.Composition _implicitBatchCommitQueued = false; RequestCommitAsync(); } + + internal void RegisterForSerialization(CompositionObject compositionObject) + { + _objectsForSerialization.Add(compositionObject); + QueueImplicitBatchCommit(); + } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs index f650d3e995..5b2a4be1bc 100644 --- a/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs @@ -2,19 +2,20 @@ using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition { - public class CompositionContainerVisual : CompositionVisual + public partial class CompositionContainerVisual : CompositionVisual { - public CompositionVisualCollection Children { get; } - internal CompositionContainerVisual(Compositor compositor, ServerCompositionContainerVisual server) : base(compositor, server) + public CompositionVisualCollection Children { get; private set; } = null!; + + partial void InitializeDefaultsExtra() { - Children = new CompositionVisualCollection(this, server.Children); + Children = new CompositionVisualCollection(this, Server.Children); } - private protected override void OnRootChanged() + private protected override void OnRootChangedCore() { foreach (var ch in Children) ch.Root = Root; - base.OnRootChanged(); + base.OnRootChangedCore(); } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs b/src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs deleted file mode 100644 index 0505d6a46c..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Numerics; -using Avalonia.Platform; -using Avalonia.Rendering.Composition.Server; -using Avalonia.Rendering.Composition.Transport; - -namespace Avalonia.Rendering.Composition -{ - internal class CustomDrawVisual : CompositionContainerVisual where TData : IEquatable - { - private readonly ICustomDrawVisualHitTest? _hitTest; - - internal CustomDrawVisual(Compositor compositor, ICustomDrawVisualRenderer renderer, - ICustomDrawVisualHitTest? hitTest) : base(compositor, - new ServerCustomDrawVisual(compositor.Server, renderer)) - { - _hitTest = hitTest; - } - - private TData? _data; - - static bool Eq(TData? left, TData? right) - { - if (left == null && right == null) - return true; - if (left == null) - return false; - return left.Equals(right); - } - - public TData? Data - { - get => _data; - set - { - if (!Eq(_data, value)) - { - ((CustomDrawVisualChanges) Changes).Data.Value = value; - _data = value; - } - } - } - - private protected override IChangeSetPool ChangeSetPool => CustomDrawVisualChanges.Pool; - } - - public interface ICustomDrawVisualRenderer - { - void Render(IDrawingContextImpl canvas, TData? data); - } - - public interface ICustomDrawVisualHitTest - { - bool HitTest(TData data, Vector2 vector2); - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs index ba18211459..f1b5032cd3 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -1,3 +1,4 @@ +using System; using System.Numerics; using Avalonia.Collections.Pooled; using Avalonia.Platform; @@ -35,16 +36,15 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua } } - protected override void ApplyCore(ChangeSet changes) + protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt) { - var ch = (DrawListVisualChanges)changes; - if (ch.DrawCommandsIsSet) + if (reader.Read() == 1) { _renderCommands?.Dispose(); - _renderCommands = ch.AcquireDrawCommands(); + _renderCommands = reader.ReadObject(); _contentBounds = null; } - base.ApplyCore(changes); + base.DeserializeChangesCore(reader, commitedAt); } protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index a50562eabc..7567eba534 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -42,8 +42,8 @@ namespace Avalonia.Rendering.Composition.Server else _compositor.RemoveCompositionTarget(this); } - - partial void ApplyChangesExtra(CompositionTargetChanges c) + + partial void DeserializeChangesExtra(BatchStreamReader c) { _redrawRequested = true; } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index 5dbe9cfb17..f7de704b23 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -17,10 +17,14 @@ namespace Avalonia.Rendering.Composition.Server private List _activeTargets = new(); private HashSet _activeAnimations = new(); private List _animationsToUpdate = new(); + private BatchStreamObjectPool _batchObjectPool; + private BatchStreamMemoryPool _batchMemoryPool; - public ServerCompositor(IRenderLoop renderLoop) + public ServerCompositor(IRenderLoop renderLoop, BatchStreamObjectPool batchObjectPool, BatchStreamMemoryPool batchMemoryPool) { _renderLoop = renderLoop; + _batchObjectPool = batchObjectPool; + _batchMemoryPool = batchMemoryPool; _renderLoop.Add(this); } @@ -45,14 +49,21 @@ namespace Avalonia.Rendering.Composition.Server batch = _batches.Dequeue(); } - foreach (var change in batch.Changes) + using (var stream = new BatchStreamReader(batch.Changes, _batchMemoryPool, _batchObjectPool)) { - if (change.Dispose) + while (!stream.IsObjectEof) { - //TODO + var target = (ServerObject)stream.ReadObject()!; + target.DeserializeChanges(stream, batch); +#if DEBUG_COMPOSITOR_SERIALIZATION + if (stream.ReadObject() != BatchStreamDebugMarkers.ObjectEndMarker) + throw new InvalidOperationException( + $"Object {target.GetType()} failed to deserialize properly on object stream"); + if(stream.Read() != BatchStreamDebugMarkers.ObjectEndMagic) + throw new InvalidOperationException( + $"Object {target.GetType()} failed to deserialize properly on data stream"); +#endif } - change.Target!.Apply(change); - change.Reset(); } _reusableToCompleteList.Add(batch); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs index 3f0995b257..a277450214 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs @@ -3,10 +3,9 @@ using Avalonia.Platform; namespace Avalonia.Rendering.Composition.Server { - internal class ServerCompositionContainerVisual : ServerCompositionVisual + internal partial class ServerCompositionContainerVisual : ServerCompositionVisual { - public ServerCompositionVisualCollection Children { get; } - + public ServerCompositionVisualCollection Children { get; private set; } = null!; protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) { @@ -28,9 +27,9 @@ namespace Avalonia.Rendering.Composition.Server child.Update(root, GlobalTransformMatrix); } - public ServerCompositionContainerVisual(ServerCompositor compositor) : base(compositor) + partial void Initialize() { - Children = new ServerCompositionVisualCollection(compositor); + Children = new ServerCompositionVisualCollection(Compositor); } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs deleted file mode 100644 index 5f3eb051a4..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Numerics; -using Avalonia.Platform; -using Avalonia.Rendering.Composition.Transport; - -namespace Avalonia.Rendering.Composition.Server -{ - class ServerCustomDrawVisual : ServerCompositionContainerVisual - { - private readonly ICustomDrawVisualRenderer _renderer; - private TData? _data; - public ServerCustomDrawVisual(ServerCompositor compositor, ICustomDrawVisualRenderer renderer) : base(compositor) - { - _renderer = renderer; - } - - protected override void ApplyCore(ChangeSet changes) - { - var c = (CustomDrawVisualChanges) changes; - if (c.Data.IsSet) - _data = c.Data.Value; - - base.ApplyCore(changes); - } - - protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) - { - _renderer.Render(canvas, _data); - base.RenderCore(canvas, transform); - } - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs index 09ef119e6b..4beea4715b 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs @@ -7,25 +7,19 @@ namespace Avalonia.Rendering.Composition.Server class ServerList : ServerObject where T : ServerObject { public List List { get; } = new List(); - protected override void ApplyCore(ChangeSet changes) + + protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt) { - var c = (ListChangeSet) changes; - if (c.HasListChanges) + if (reader.Read() == 1) { - foreach (var lc in c.ListChanges) - { - if(lc.Action == ListChangeAction.Clear) - List.Clear(); - if(lc.Action == ListChangeAction.RemoveAt) - List.RemoveAt(lc.Index); - if(lc.Action == ListChangeAction.InsertAt) - List.Insert(lc.Index, lc.Added!); - if (lc.Action == ListChangeAction.ReplaceAt) - List[lc.Index] = lc.Added!; - } + List.Clear(); + var count = reader.Read(); + for (var c = 0; c < count; c++) + List.Add(reader.ReadObject()); } + base.DeserializeChangesCore(reader, commitedAt); } - + public override long LastChangedBy { get diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs index 5b2f58b186..16f57d9059 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs @@ -23,18 +23,6 @@ namespace Avalonia.Rendering.Composition.Server Compositor = compositor; } - protected virtual void ApplyCore(ChangeSet changes) - { - - } - - public void Apply(ChangeSet changes) - { - ApplyCore(changes); - ValuesInvalidated(); - ItselfLastChangedBy = changes.Batch!.SequenceId; - } - public virtual ExpressionVariant GetPropertyForAnimation(string name) { return default; @@ -81,6 +69,10 @@ namespace Avalonia.Rendering.Composition.Server [MethodImpl(MethodImplOptions.AggressiveInlining)] private ref ServerObjectSubscriptionStore GetStoreFromOffset(int offset) { +#if DEBUG + if (offset == 0) + throw new InvalidOperationException(); +#endif return ref Unsafe.As(ref Unsafe.AddByteOffset(ref _activationCount, new IntPtr(offset))); } @@ -112,5 +104,17 @@ namespace Avalonia.Rendering.Composition.Server } public virtual int? GetFieldOffset(string fieldName) => null; + + protected virtual void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt) + { + + } + + public void DeserializeChanges(BatchStreamReader reader, Batch batch) + { + DeserializeChangesCore(reader, batch.CommitedAt); + ValuesInvalidated(); + ItselfLastChangedBy = batch.SequenceId; + } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 801bfb2f65..5717ab2f8c 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -7,7 +7,6 @@ namespace Avalonia.Rendering.Composition.Server unsafe partial class ServerCompositionVisual : ServerObject { private bool _isDirty; - private ServerCompositionTarget? _root; private bool _isBackface; protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) { @@ -119,28 +118,24 @@ namespace Avalonia.Rendering.Composition.Server public bool Visible; } - partial void ApplyChangesExtra(CompositionVisualChanges c) + + partial void DeserializeChangesExtra(BatchStreamReader c) { - if (c.Parent.IsSet) - Parent = c.Parent.Value; - if (c.Root.IsSet) - Root = c.Root.Value; ValuesInvalidated(); } - public ServerCompositionTarget? Root + partial void OnRootChanging() { - get => _root; - private set - { - if(_root != null) - Deactivate(); - _root = value; - if (_root != null) - Activate(); - } + if(Root != null) + Deactivate(); } - + + partial void OnRootChanged() + { + if (Root != null) + Activate(); + } + protected override void ValuesInvalidated() { _isDirty = true; @@ -149,8 +144,6 @@ namespace Avalonia.Rendering.Composition.Server else Root?.Invalidate(); } - - public ServerCompositionVisual? Parent { get; private set; } public bool IsVisibleInFrame { get; set; } public Rect TransformedBounds { get; set; } public virtual Rect ContentBounds => new Rect(0, 0, Size.X, Size.Y); diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs b/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs index 7b64c01d09..0714db5781 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs @@ -9,23 +9,22 @@ namespace Avalonia.Rendering.Composition.Transport internal class Batch { private static long _nextSequenceId = 1; - private static ConcurrentBag> _pool = new ConcurrentBag>(); + private static ConcurrentBag _pool = new(); public long SequenceId { get; } public Batch() { SequenceId = Interlocked.Increment(ref _nextSequenceId); if (!_pool.TryTake(out var lst)) - lst = new List(); + lst = new BatchStreamData(); Changes = lst; } private TaskCompletionSource _tcs = new TaskCompletionSource(); - public List Changes { get; private set; } + public BatchStreamData Changes { get; private set; } public TimeSpan CommitedAt { get; set; } public void Complete() { - Changes.Clear(); _pool.Add(Changes); Changes = null!; diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs index feb892d134..9e9ed739fb 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs @@ -2,13 +2,14 @@ using System; using System.Collections.Generic; using System.IO; using System.Runtime.CompilerServices; +using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition.Transport; internal class BatchStreamData { - public Queue> Objects { get; } = new(); + public Queue> Objects { get; } = new(); public Queue> Structs { get; } = new(); } @@ -22,12 +23,12 @@ internal class BatchStreamWriter : IDisposable { private readonly BatchStreamData _output; private readonly BatchStreamMemoryPool _memoryPool; - private readonly BatchStreamObjectPool _objectPool; + private readonly BatchStreamObjectPool _objectPool; - private BatchStreamSegment _currentObjectSegment; + private BatchStreamSegment _currentObjectSegment; private BatchStreamSegment _currentDataSegment; - public BatchStreamWriter(BatchStreamData output, BatchStreamMemoryPool memoryPool, BatchStreamObjectPool objectPool) + public BatchStreamWriter(BatchStreamData output, BatchStreamMemoryPool memoryPool, BatchStreamObjectPool objectPool) { _output = output; _memoryPool = memoryPool; @@ -69,7 +70,7 @@ internal class BatchStreamWriter : IDisposable _currentDataSegment.ElementCount += size; } - public void Write(ServerObject item) + public void WriteObject(object? item) { if (_currentObjectSegment.Data == null || _currentObjectSegment.ElementCount >= _currentObjectSegment.Data.Length) @@ -89,15 +90,15 @@ internal class BatchStreamReader : IDisposable { private readonly BatchStreamData _input; private readonly BatchStreamMemoryPool _memoryPool; - private readonly BatchStreamObjectPool _objectPool; + private readonly BatchStreamObjectPool _objectPool; - private BatchStreamSegment _currentObjectSegment; + private BatchStreamSegment _currentObjectSegment; private BatchStreamSegment _currentDataSegment; private int _memoryOffset, _objectOffset; - public BatchStreamReader(BatchStreamData _input, BatchStreamMemoryPool memoryPool, BatchStreamObjectPool objectPool) + public BatchStreamReader(BatchStreamData input, BatchStreamMemoryPool memoryPool, BatchStreamObjectPool objectPool) { - this._input = _input; + _input = input; _memoryPool = memoryPool; _objectPool = objectPool; } @@ -116,7 +117,7 @@ internal class BatchStreamReader : IDisposable if (_memoryOffset + size > _currentDataSegment.ElementCount) throw new InvalidOperationException("Attempted to read more memory then left in the current segment"); - var rv = *(T*)((byte*)_currentDataSegment.Data + size); + var rv = *(T*)((byte*)_currentDataSegment.Data + _memoryOffset); _memoryOffset += size; if (_memoryOffset == _currentDataSegment.ElementCount) { @@ -127,7 +128,9 @@ internal class BatchStreamReader : IDisposable return rv; } - public ServerObject ReadObject() + public T ReadObject() where T : class? => (T)ReadObject()!; + + public object? ReadObject() { if (_currentObjectSegment.Data == null) { @@ -148,6 +151,8 @@ internal class BatchStreamReader : IDisposable return rv; } + public bool IsObjectEof => _currentObjectSegment.Data == null && _input.Objects.Count == 0; + public bool IsStructEof => _currentDataSegment.Data == IntPtr.Zero && _input.Structs.Count == 0; public void Dispose() diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs index 913958765a..d76a9c609e 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs @@ -18,27 +18,31 @@ internal abstract class BatchStreamPoolBase : IDisposable readonly int[] _usageStatistics = new int[10]; int _usageStatisticsSlot; - public BatchStreamPoolBase(bool needsFinalize = false) + public BatchStreamPoolBase(bool needsFinalize, Action>? startTimer = null) { if(!needsFinalize) GC.SuppressFinalize(needsFinalize); var updateRef = new WeakReference>(this); - StartUpdateTimer(updateRef); + StartUpdateTimer(startTimer, updateRef); } - static void StartUpdateTimer(WeakReference> updateRef) + static void StartUpdateTimer(Action>? startTimer, WeakReference> updateRef) { - DispatcherTimer.Run(() => + Func timerProc = () => { if (updateRef.TryGetTarget(out var target)) { target.UpdateStatistics(); return true; } - return false; - }, TimeSpan.FromSeconds(1)); + return false; + }; + if (startTimer != null) + startTimer(timerProc); + else + DispatcherTimer.Run(timerProc, TimeSpan.FromSeconds(1)); } private void UpdateStatistics() @@ -50,7 +54,7 @@ internal abstract class BatchStreamPoolBase : IDisposable while (recentlyUsedPooledSlots < _pool.Count) DestroyItem(_pool.Pop()); - _usageStatistics[_usage] = 0; + _usageStatistics[_usageStatisticsSlot] = 0; _usageStatisticsSlot = (_usageStatisticsSlot + 1) % _usageStatistics.Length; } } @@ -109,11 +113,11 @@ internal abstract class BatchStreamPoolBase : IDisposable } } -internal sealed class BatchStreamObjectPool : BatchStreamPoolBase where T : class +internal sealed class BatchStreamObjectPool : BatchStreamPoolBase where T : class? { private readonly int _arraySize; - public BatchStreamObjectPool(int arraySize = 1024) + public BatchStreamObjectPool(int arraySize = 1024, Action>? startTimer = null) : base(false, startTimer) { _arraySize = arraySize; } @@ -133,7 +137,7 @@ internal sealed class BatchStreamMemoryPool : BatchStreamPoolBase { public int BufferSize { get; } - public BatchStreamMemoryPool(int bufferSize = 16384) + public BatchStreamMemoryPool(int bufferSize = 16384, Action>? startTimer = null) : base(true, startTimer) { BufferSize = bufferSize; } diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamDebugMarker.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamDebugMarker.cs new file mode 100644 index 0000000000..7d21b03f24 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamDebugMarker.cs @@ -0,0 +1,9 @@ +using System; + +namespace Avalonia.Rendering.Composition.Transport; + +internal class BatchStreamDebugMarkers +{ + public static object ObjectEndMarker = new object(); + public static Guid ObjectEndMagic = Guid.NewGuid(); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/Change.cs b/src/Avalonia.Base/Rendering/Composition/Transport/Change.cs deleted file mode 100644 index cbee350ab3..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/Change.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using Avalonia.Rendering.Composition.Animations; - -namespace Avalonia.Rendering.Composition.Transport -{ - struct Change - { - private T? _value; - - public bool IsSet { get; private set; } - - public T? Value - { - get - { - if(!IsSet) - throw new InvalidOperationException(); - return _value; - } - set - { - IsSet = true; - _value = value; - } - } - - public void Reset() - { - _value = default; - IsSet = false; - } - } - - struct AnimatedChange - { - private T? _value; - private IAnimationInstance? _animation; - - public bool IsValue { get; private set; } - public bool IsAnimation { get; private set; } - - public T Value - { - get - { - if(!IsValue) - throw new InvalidOperationException(); - return _value!; - } - set - { - IsAnimation = false; - _animation = null; - IsValue = true; - _value = value; - } - } - - public IAnimationInstance Animation - { - get - { - if(!IsAnimation) - throw new InvalidOperationException(); - return _animation!; - } - set - { - IsValue = false; - _value = default; - IsAnimation = true; - _animation = value; - } - } - - public void Reset() - { - this = default; - } - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs deleted file mode 100644 index 898885dce6..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Avalonia.Rendering.Composition.Server; - -namespace Avalonia.Rendering.Composition.Transport -{ - internal abstract class ChangeSet - { - private readonly IChangeSetPool _pool; - public Batch Batch = null!; - public ServerObject? Target; - public bool Dispose; - - public ChangeSet(IChangeSetPool pool) - { - _pool = pool; - } - - public virtual void Reset() - { - Batch = null!; - Target = null; - Dispose = false; - } - - public void Return() - { - _pool.Return(this); - } - } - - internal class CompositionObjectChanges : ChangeSet - { - public CompositionObjectChanges(IChangeSetPool pool) : base(pool) - { - } - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs deleted file mode 100644 index ea97cd7d44..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Concurrent; -using Avalonia.Rendering.Composition.Server; - -namespace Avalonia.Rendering.Composition.Transport -{ - interface IChangeSetPool - { - void Return(ChangeSet changes); - ChangeSet Get(ServerObject target, Batch batch); - } - - class ChangeSetPool : IChangeSetPool where T : ChangeSet - { - private readonly Func _factory; - private readonly ConcurrentBag _pool = new ConcurrentBag(); - - public ChangeSetPool(Func factory) - { - _factory = factory; - } - - public void Return(T changes) - { - changes.Reset(); - _pool.Add(changes); - } - - void IChangeSetPool.Return(ChangeSet changes) => Return((T) changes); - ChangeSet IChangeSetPool.Get(ServerObject target, Batch batch) => Get(target, batch); - - public T Get(ServerObject target, Batch batch) - { - if (!_pool.TryTake(out var res)) - res = _factory(this); - res.Target = target; - res.Batch = batch; - res.Dispose = false; - return res; - } - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/CompositionTargetChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/CompositionTargetChanges.cs deleted file mode 100644 index 014adc7bbe..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/CompositionTargetChanges.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Avalonia.Rendering.Composition.Transport; - -partial class CompositionTargetChanges -{ - public Change RedrawRequested; - - partial void ResetExtra() - { - RedrawRequested.Reset(); - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs deleted file mode 100644 index aed041b62e..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Avalonia.Rendering.Composition.Transport -{ - class CustomDrawVisualChanges : CompositionVisualChanges - { - public CustomDrawVisualChanges(IChangeSetPool pool) : base(pool) - { - } - - public Change Data; - - public override void Reset() - { - Data.Reset(); - base.Reset(); - } - - public new static ChangeSetPool> Pool { get; } = - new ChangeSetPool>(pool => new CustomDrawVisualChanges(pool)); - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs deleted file mode 100644 index 215c03b229..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Collections.Generic; -using Avalonia.Collections.Pooled; -using Avalonia.Rendering.Composition.Drawing; -using Avalonia.Rendering.SceneGraph; -using Avalonia.Utilities; - -namespace Avalonia.Rendering.Composition.Transport; - -internal class DrawListVisualChanges : CompositionVisualChanges -{ - private CompositionDrawList? _drawCommands; - - public DrawListVisualChanges(IChangeSetPool pool) : base(pool) - { - } - - public CompositionDrawList? DrawCommands - { - get => _drawCommands; - set - { - _drawCommands?.Dispose(); - _drawCommands = value; - DrawCommandsIsSet = true; - } - } - - public bool DrawCommandsIsSet { get; private set; } - - public CompositionDrawList? AcquireDrawCommands() - { - var rv = _drawCommands; - _drawCommands = null; - DrawCommandsIsSet = false; - return rv; - } - - public override void Reset() - { - _drawCommands?.Dispose(); - _drawCommands = null; - DrawCommandsIsSet = false; - base.Reset(); - } - - public new static ChangeSetPool Pool { get; } = - new ChangeSetPool(pool => new(pool)); -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs deleted file mode 100644 index ee6e4231f8..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Avalonia.Rendering.Composition.Server; - -namespace Avalonia.Rendering.Composition.Transport -{ - internal class ListChange where T : ServerObject - { - public int Index; - public ListChangeAction Action; - public T? Added; - } - - internal enum ListChangeAction - { - InsertAt, - RemoveAt, - Clear, - ReplaceAt - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs deleted file mode 100644 index 9bb101a080..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections.Generic; -using Avalonia.Rendering.Composition.Server; - -namespace Avalonia.Rendering.Composition.Transport -{ - class ListChangeSet : ChangeSet where T : ServerObject - { - private List>? _listChanges; - public List> ListChanges => _listChanges ??= new List>(); - public bool HasListChanges => _listChanges != null; - - public override void Reset() - { - _listChanges?.Clear(); - base.Reset(); - } - - public ListChangeSet(IChangeSetPool pool) : base(pool) - { - } - - public static readonly ChangeSetPool> Pool = - new ChangeSetPool>(pool => new ListChangeSet(pool)); - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs index 1add3aa990..2399bd71d7 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs @@ -8,19 +8,21 @@ namespace Avalonia.Rendering.Composition.Transport where TServer : ServerObject where TClient : CompositionObject { - private readonly IGetChanges _parent; - private readonly List _list = new List(); + private readonly IRegisterForSerialization _parent; + private bool _changed; - public interface IGetChanges + public interface IRegisterForSerialization { - ListChangeSet GetChanges(); + void RegisterForSerialization(); } - public ServerListProxyHelper(IGetChanges parent) + public ServerListProxyHelper(IRegisterForSerialization parent) { _parent = parent; } - + + private readonly List _list = new List(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); public List.Enumerator GetEnumerator() => _list.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); @@ -30,10 +32,8 @@ namespace Avalonia.Rendering.Composition.Transport public void Clear() { _list.Clear(); - _parent.GetChanges().ListChanges.Add(new ListChange - { - Action = ListChangeAction.Clear - }); + _changed = true; + _parent.RegisterForSerialization(); } public bool Contains(TClient item) => _list.Contains(item); @@ -56,22 +56,15 @@ namespace Avalonia.Rendering.Composition.Transport public void Insert(int index, TClient item) { _list.Insert(index, item); - _parent.GetChanges().ListChanges.Add(new ListChange - { - Action = ListChangeAction.InsertAt, - Index = index, - Added = (TServer) item.Server - }); + _changed = true; + _parent.RegisterForSerialization(); } public void RemoveAt(int index) { _list.RemoveAt(index); - _parent.GetChanges().ListChanges.Add(new ListChange - { - Action = ListChangeAction.RemoveAt, - Index = index - }); + _changed = true; + _parent.RegisterForSerialization(); } public TClient this[int index] @@ -80,13 +73,21 @@ namespace Avalonia.Rendering.Composition.Transport set { _list[index] = value; - _parent.GetChanges().ListChanges.Add(new ListChange - { - Action = ListChangeAction.ReplaceAt, - Index = index, - Added = (TServer) value.Server - }); + _changed = true; + _parent.RegisterForSerialization(); + } + } + + public void Serialize(BatchStreamWriter writer) + { + writer.Write((byte)(_changed ? 1 : 0)); + if (_changed) + { + writer.Write(_list.Count); + foreach (var el in _list) + writer.WriteObject(el.Server); } + _changed = false; } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs deleted file mode 100644 index c87fb96967..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Avalonia.Rendering.Composition.Server; - -namespace Avalonia.Rendering.Composition.Transport -{ - partial class CompositionVisualChanges - { - public Change Parent; - public Change Root; - - partial void ResetExtra() - { - Parent.Reset(); - Root.Reset(); - } - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Visual.cs b/src/Avalonia.Base/Rendering/Composition/Visual.cs index fa8d5d8f3b..5bf5dcee74 100644 --- a/src/Avalonia.Base/Rendering/Composition/Visual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Visual.cs @@ -4,39 +4,14 @@ namespace Avalonia.Rendering.Composition { public abstract partial class CompositionVisual { - private CompositionVisual? _parent; - private CompositionTarget? _root; - - public CompositionVisual? Parent - { - get => _parent; - internal set - { - if (_parent == value) - return; - _parent = value; - Changes.Parent.Value = value?.Server; - Root = _parent?.Root; - } - } - - // TODO: hide behind private-ish API - public CompositionTarget? Root + private protected virtual void OnRootChangedCore() { - get => _root; - internal set - { - var changed = _root != value; - _root = value; - Changes.Root.Value = value?.Server; - if (changed) - OnRootChanged(); - } } - private protected virtual void OnRootChanged() - { - } + partial void OnRootChanged() => OnRootChangedCore(); + + partial void OnParentChanged() => Root = Parent?.Root; + internal Matrix4x4? TryGetServerTransform() { diff --git a/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs index fef4caf675..42226a8b4d 100644 --- a/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs +++ b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs @@ -64,6 +64,7 @@ namespace Avalonia.Rendering.Composition { if (item.Parent != null) throw new InvalidOperationException("Visual already has a parent"); + item.Parent = item; } } } \ No newline at end of file diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml index eb1ffe1922..a7ae341bb3 100644 --- a/src/Avalonia.Base/composition-schema.xml +++ b/src/Avalonia.Base/composition-schema.xml @@ -4,10 +4,12 @@ Avalonia.Rendering.Composition.Server Avalonia.Rendering.Composition.Transport Avalonia.Rendering.Composition.Animations - - + + + + @@ -21,6 +23,7 @@ + diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs index 8b6aca33cd..096864e52a 100644 --- a/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs @@ -35,6 +35,10 @@ namespace Avalonia.SourceGenerator.CompositionGenerator [XmlAttribute] public string Name { get; set; } + + [XmlAttribute] + public bool Passthrough { get; set; } + [XmlAttribute] public string ServerName { get; set; } } @@ -104,6 +108,8 @@ namespace Avalonia.SourceGenerator.CompositionGenerator public string DefaultValue { get; set; } [XmlAttribute] public bool Animated { get; set; } + [XmlAttribute] + public bool InternalSet { get; set; } } public class GAnimationType diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs index 43a4a4afa7..d88e9b4600 100644 --- a/src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs @@ -41,6 +41,15 @@ namespace Avalonia.SourceGenerator.CompositionGenerator return cl; return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); } + + public static EnumDeclarationSyntax AddModifiers(this EnumDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static string WithLowerFirst(this string s) { diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs index 7d5146c5f5..314ac1acbf 100644 --- a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs @@ -25,7 +25,7 @@ namespace Avalonia.Rendering.Composition internal override IAnimationInstance CreateInstance(Avalonia.Rendering.Composition.Server.ServerObject targetObject, ExpressionVariant? finalValue) {{ - return new KeyFrameAnimationInstance<{a.Type}>({name}Interpolator.Instance, _keyFrames.Snapshot(), CreateSnapshot(true), + return new KeyFrameAnimationInstance<{a.Type}>({name}Interpolator.Instance, _keyFrames.Snapshot(), CreateSnapshot(), finalValue?.CastOrDefault<{a.Type}>(), targetObject, DelayBehavior, DelayTime, Direction, Duration, IterationBehavior, IterationCount, StopBehavior); diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs index 593386f713..e0ea5b20ae 100644 --- a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs @@ -11,9 +11,7 @@ class Template { private ServerListProxyHelper _list = null!; - ListChangeSet - ServerListProxyHelper.IGetChanges. - GetChanges() => Changes; + void ServerListProxyHelper.IRegisterForSerialization.RegisterForSerialization() => RegisterForSerialization(); public List.Enumerator GetEnumerator() => _list.GetEnumerator(); @@ -87,7 +85,11 @@ class Template partial void OnBeforeReplace(ItemTypeName oldItem, ItemTypeName newItem); partial void OnReplace(ItemTypeName oldItem, ItemTypeName newItem); partial void OnClear(); -} + private protected override void SerializeChangesCore(BatchStreamWriter writer) + {{ + _list.Serialize(writer); + base.SerializeChangesCore(writer); + }} "; private ClassDeclarationSyntax AppendListProxy(GList list, ClassDeclarationSyntax cl) @@ -97,7 +99,7 @@ class Template var serverItemType = ServerName(itemType); cl = cl.AddBaseListTypes(SimpleBaseType( - ParseTypeName("ServerListProxyHelper<" + itemType + ", " + serverItemType + ">.IGetChanges")), + ParseTypeName("ServerListProxyHelper<" + itemType + ", " + serverItemType + ">.IRegisterForSerialization")), SimpleBaseType(ParseTypeName("IList<" + itemType + ">")) ); var code = ListProxyTemplate.Replace("ListTypeName", list.Name) diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs index 5a514a4eff..3c38c0331e 100644 --- a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using Microsoft.CodeAnalysis; @@ -38,7 +39,12 @@ namespace Avalonia.SourceGenerator.CompositionGenerator string ServerName(string c) => c != null ? ("Server" + c) : "ServerObject"; string ChangesName(string c) => c != null ? (c + "Changes") : "ChangeSet"; - + string ChangedFieldsTypeName(GClass c) => c.Name + "ChangedFields"; + string ChangedFieldsFieldName(GClass c) => "_changedFieldsOf" + c.Name; + string PropertyBackingFieldName(GProperty prop) => "_" + prop.Name.WithLowerFirst(); + string ServerPropertyOffsetFieldName(GProperty prop) => "s_OffsetOf" + PropertyBackingFieldName(prop); + string PropertyPendingAnimationFieldName(GProperty prop) => "_pendingAnimationFor" + prop.Name; + void GenerateClass(GClass cl) { var list = cl as GList; @@ -68,36 +74,13 @@ namespace Avalonia.SourceGenerator.CompositionGenerator .WithBaseType(serverBase); string changesName = ChangesName(cl.Name); - var changesBase = ChangesName(cl.ChangesBase ?? cl.Inherits); + string changedFieldsTypeName = ChangedFieldsTypeName(cl); + string changedFieldsName = ChangedFieldsFieldName(cl); - if (list != null) - changesBase = "ListChangeSet<" + ServerName(list.ItemType) + ">"; - - var changeSetPoolType = "ChangeSetPool<" + changesName + ">"; - var transport = ClassDeclaration(changesName) - .AddModifiers(SyntaxKind.UnsafeKeyword, SyntaxKind.PartialKeyword) - .WithBaseType(changesBase) - .AddMembers(DeclareField(changeSetPoolType, "Pool", - EqualsValueClause( - ParseExpression($"new {changeSetPoolType}(pool => new {changesName}(pool))") - ), - SyntaxKind.PublicKeyword, - SyntaxKind.StaticKeyword, SyntaxKind.ReadOnlyKeyword)) - .AddMembers(ParseMemberDeclaration($"public {changesName}(IChangeSetPool pool) : base(pool){{}}")); - - client = client - .AddMembers( - PropertyDeclaration(ParseTypeName("IChangeSetPool"), "ChangeSetPool") - .AddModifiers(SyntaxKind.PrivateKeyword, SyntaxKind.ProtectedKeyword, - SyntaxKind.OverrideKeyword) - .WithExpressionBody( - ArrowExpressionClause(MemberAccess(changesName, "Pool"))) - .WithSemicolonToken(Semicolon())) - .AddMembers(PropertyDeclaration(ParseTypeName(changesName), "Changes") - .AddModifiers(SyntaxKind.PrivateKeyword, SyntaxKind.NewKeyword) - .WithExpressionBody(ArrowExpressionClause(CastExpression(ParseTypeName(changesName), - MemberAccess(BaseExpression(), "Changes")))) - .WithSemicolonToken(Semicolon())); + if (cl.Properties.Count > 0) + client = client + .AddMembers(DeclareField(changedFieldsTypeName, changedFieldsName)); + if (!cl.CustomCtor) { @@ -105,7 +88,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator .AddModifiers(SyntaxKind.InternalKeyword, SyntaxKind.NewKeyword) .AddAccessorListAccessors(AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) .WithSemicolonToken(Semicolon()))); - client = client.AddMembers( + client = client.AddMembers( ConstructorDeclaration(cl.Name) .AddModifiers(SyntaxKind.InternalKeyword) .WithParameterList(ParameterList(SeparatedList(new[] @@ -141,20 +124,20 @@ namespace Avalonia.SourceGenerator.CompositionGenerator ArgumentList(SeparatedList(new[] { Argument(IdentifierName("compositor")), - })))).WithBody(Block())); + })))).WithBody(Block(ParseStatement("Initialize();")))); } + server = server.AddMembers( + MethodDeclaration(ParseTypeName("void"), "Initialize") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + var changesVarName = "c"; var changesVar = IdentifierName(changesVarName); server = server.AddMembers( - MethodDeclaration(ParseTypeName("void"), "ApplyChangesExtra") - .AddParameterListParameters(Parameter(Identifier("c")).WithType(ParseTypeName(changesName))) - .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); - - transport = transport.AddMembers( - MethodDeclaration(ParseTypeName("void"), "ResetExtra") + MethodDeclaration(ParseTypeName("void"), "DeserializeChangesExtra") + .AddParameterListParameters(Parameter(Identifier("c")).WithType(ParseTypeName("BatchStreamReader"))) .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); var applyMethodBody = Block( @@ -168,7 +151,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator ExpressionStatement(InvocationExpression(IdentifierName("ApplyChangesExtra")) .AddArgumentListArguments(Argument(IdentifierName("c")))) ); - + var uninitializedObjectName = "dummy"; var serverStaticCtorBody = cl.Abstract ? Block() @@ -179,72 +162,51 @@ namespace Avalonia.SourceGenerator.CompositionGenerator ParseStatement("InitializeFieldOffsets(dummy);") ); - var initializeFieldOffsetsBody = cl.ServerBase == null + var initializeFieldOffsetsBody = cl.Inherits == null ? Block() - : Block(ParseStatement($"{cl.ServerBase}.InitializeFieldOffsets(dummy);")); + : Block(ParseStatement($"Server{cl.Inherits}.InitializeFieldOffsets(dummy);")); var resetBody = Block(); var startAnimationBody = Block(); - var getPropertyBody = Block(); var serverGetPropertyBody = Block(); var serverGetFieldOffsetBody = Block(); var activatedBody = Block(ParseStatement("base.Activated();")); var deactivatedBody = Block(ParseStatement("base.Deactivated();")); + var serializeMethodBody = SerializeChangesPrologue(cl); + var deserializeMethodBody = DeserializeChangesPrologue(cl); - var defaultsMethodBody = Block(); + var defaultsMethodBody = Block(ParseStatement("InitializeDefaultsExtra();")); foreach (var prop in cl.Properties) { - var fieldName = "_" + prop.Name.WithLowerFirst(); - var fieldOffsetName = "s_OffsetOf" + fieldName; + var fieldName = PropertyBackingFieldName(prop); + var animatedFieldName = PropertyPendingAnimationFieldName(prop); + var fieldOffsetName = ServerPropertyOffsetFieldName(prop); var propType = ParseTypeName(prop.Type); var filteredPropertyType = prop.Type.TrimEnd('?'); var isObject = _objects.Contains(filteredPropertyType); var isNullable = prop.Type.EndsWith("?"); - - - - - client = client - .AddMembers(DeclareField(prop.Type, fieldName)) - .AddMembers(PropertyDeclaration(propType, prop.Name) - .AddModifiers(SyntaxKind.PublicKeyword) - .AddAccessorListAccessors( - AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, - Block(ReturnStatement(IdentifierName(fieldName)))), - AccessorDeclaration(SyntaxKind.SetAccessorDeclaration, - Block( - ParseStatement("var changed = false;"), - IfStatement(BinaryExpression(SyntaxKind.NotEqualsExpression, - IdentifierName(fieldName), - IdentifierName("value")), - Block( - ParseStatement("On" + prop.Name + "Changing();"), - ParseStatement("changed = true;"), - GeneratePropertySetterAssignment(prop, fieldName, isObject, isNullable)) - ), - ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, - IdentifierName(fieldName), IdentifierName("value"))), - ParseStatement($"if(changed) On" + prop.Name + "Changed();") - )) - )) - .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changed") - .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())) - .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changing") - .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + bool isPassthrough = false; + if (prop.Animated) + client = client.AddMembers(DeclareField("IAnimationInstance?", animatedFieldName)); + client = GenerateClientProperty(client, cl, prop, propType, isObject, isNullable); var animatedServer = prop.Animated; var serverPropertyType = ((isObject ? "Server" : "") + prop.Type); - if (_manuals.TryGetValue(filteredPropertyType, out var manual) && manual.ServerName != null) - serverPropertyType = manual.ServerName + (isNullable ? "?" : ""); - - - transport = transport - .AddMembers(DeclareField((animatedServer ? "Animated" : "") + "Change<" + serverPropertyType + ">", - prop.Name, SyntaxKind.PublicKeyword)); + if (_manuals.TryGetValue(filteredPropertyType, out var manual)) + { + if (manual.Passthrough) + { + isPassthrough = true; + serverPropertyType = prop.Type; + } + if (manual.ServerName != null) + serverPropertyType = manual.ServerName + (isNullable ? "?" : ""); + } + if (animatedServer) server = server.AddMembers( DeclareField("ServerAnimatedValueStore<" + serverPropertyType + ">", fieldName), @@ -301,7 +263,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator ArgumentList(SeparatedList(new[] { Argument(IdentifierName("this")), - Argument(changesVar), + Argument(ParseExpression("c.Batch.CommitedAt")), Argument(MemberAccess(changesVar, prop.Name, "Animation")), Argument(IdentifierName(fieldOffsetName)) }))))) @@ -316,16 +278,18 @@ namespace Avalonia.SourceGenerator.CompositionGenerator resetBody = resetBody.AddStatements( ExpressionStatement(InvocationExpression(MemberAccess(prop.Name, "Reset")))); - + + serializeMethodBody = ApplySerializeField(serializeMethodBody,cl, prop, isObject, isPassthrough); + deserializeMethodBody = ApplyDeserializeField(deserializeMethodBody,cl, prop, serverPropertyType, isObject); + if (animatedServer) { - startAnimationBody = ApplyStartAnimation(startAnimationBody, prop, fieldName); + startAnimationBody = ApplyStartAnimation(startAnimationBody, cl, prop); activatedBody = activatedBody.AddStatements(ParseStatement($"{fieldName}.Activate(this);")); deactivatedBody = deactivatedBody.AddStatements(ParseStatement($"{fieldName}.Deactivate(this);")); } - - getPropertyBody = ApplyGetProperty(getPropertyBody, prop); + serverGetPropertyBody = ApplyGetProperty(serverGetPropertyBody, prop); serverGetFieldOffsetBody = ApplyGetProperty(serverGetFieldOffsetBody, prop, fieldOffsetName); @@ -345,55 +309,6 @@ namespace Avalonia.SourceGenerator.CompositionGenerator } } - if (cl is GBrush brush && !cl.Abstract) - { - var brushName = brush.Name.StripPrefix("Composition"); - /* - server = server.AddMembers( - MethodDeclaration(ParseTypeName("ICbBrush"), "CreateBackendBrush") - .AddModifiers(SyntaxKind.ProtectedKeyword, SyntaxKind.OverrideKeyword) - .WithExpressionBody(ArrowExpressionClause( - InvocationExpression(MemberAccess("Compositor", "Backend", "Create" + brushName)) - )).WithSemicolonToken(Semicolon()) - ); - if (!brush.CustomUpdate) - server = server.AddMembers( - MethodDeclaration(ParseTypeName("void"), "UpdateBackendBrush") - .AddModifiers(SyntaxKind.ProtectedKeyword, SyntaxKind.OverrideKeyword) - .AddParameterListParameters(Parameter(Identifier("brush")) - .WithType(ParseTypeName("ICbBrush"))) - .AddBodyStatements( - ExpressionStatement( - InvocationExpression( - MemberAccess( - ParenthesizedExpression( - CastExpression(ParseTypeName("ICb" + brushName), IdentifierName("brush"))), "Update"), - ArgumentList(SeparatedList(cl.Properties.Select(x => - { - if(x.Type.TrimEnd('?') == "ICompositionSurface") - return Argument( - ConditionalAccessExpression(IdentifierName(x.Name), - MemberBindingExpression(IdentifierName("BackendSurface"))) - ); - if (_brushes.Contains(x.Type)) - return Argument( - ConditionalAccessExpression(IdentifierName(x.Name), - MemberBindingExpression(IdentifierName("Brush"))) - ); - return Argument(IdentifierName(x.Name)); - })))) - ))); - -*/ - } - - server = server.AddMembers( - MethodDeclaration(ParseTypeName("void"), "ApplyCore") - .AddModifiers(SyntaxKind.ProtectedKeyword, SyntaxKind.OverrideKeyword) - .AddParameterListParameters( - Parameter(Identifier("changes")).WithType(ParseTypeName("ChangeSet"))) - .WithBody(applyMethodBody)); - server = server.AddMembers(ConstructorDeclaration(serverName) .WithModifiers(TokenList(Token(SyntaxKind.StaticKeyword))) .WithBody(serverStaticCtorBody)); @@ -408,26 +323,32 @@ namespace Avalonia.SourceGenerator.CompositionGenerator $"protected override void Activated(){{}}")!).WithBody(activatedBody)) .AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( $"protected override void Deactivated(){{}}")!).WithBody(deactivatedBody)); + if (cl.Properties.Count > 0) + server = server.AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( + $"protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt){{}}") + !) + .WithBody(deserializeMethodBody)); client = client.AddMembers( - MethodDeclaration(ParseTypeName("void"), "InitializeDefaults").WithBody(defaultsMethodBody)); - - transport = transport.AddMembers(MethodDeclaration(ParseTypeName("void"), "Reset") - .AddModifiers(SyntaxKind.PublicKeyword, SyntaxKind.OverrideKeyword) - .WithBody(resetBody.AddStatements( - ExpressionStatement(InvocationExpression(IdentifierName("ResetExtra"))), - ExpressionStatement(InvocationExpression(MemberAccess("base", "Reset")))))); - + MethodDeclaration(ParseTypeName("void"), "InitializeDefaults").WithBody(defaultsMethodBody)) + .AddMembers( + MethodDeclaration(ParseTypeName("void"), "InitializeDefaultsExtra") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + + if (cl.Properties.Count > 0) + client = client.AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( + $"private protected override void SerializeChangesCore(BatchStreamWriter writer){{}}")!) + .WithBody(serializeMethodBody)); + if (list != null) client = AppendListProxy(list, client); if (startAnimationBody.Statements.Count != 0) client = WithStartAnimation(client, startAnimationBody); - - client = WithGetProperty(client, getPropertyBody, false); - server = WithGetProperty(server, serverGetPropertyBody, true); + + server = WithGetPropertyForAnimation(server, serverGetPropertyBody); server = WithGetFieldOffset(server, serverGetFieldOffsetBody); - + if(cl.Implements.Count > 0) foreach (var impl in cl.Implements) { @@ -441,57 +362,121 @@ namespace Avalonia.SourceGenerator.CompositionGenerator } + SaveTo(unit.AddMembers(GenerateChangedFieldsEnum(cl)), "Transport", + ChangedFieldsTypeName(cl) + ".generated.cs"); + SaveTo(unit.AddMembers(clientNs.AddMembers(client)), cl.Name + ".generated.cs"); SaveTo(unit.AddMembers(serverNs.AddMembers(server)), "Server", "Server" + cl.Name + ".generated.cs"); - SaveTo(unit.AddMembers(transportNs.AddMembers(transport)), - "Transport", cl.Name + "Changes.generated.cs"); + } + + private ClassDeclarationSyntax GenerateClientProperty(ClassDeclarationSyntax client, GClass cl, GProperty prop, + TypeSyntax propType, bool isObject, bool isNullable) + { + var fieldName = PropertyBackingFieldName(prop); + return client + .AddMembers(DeclareField(prop.Type, fieldName)) + .AddMembers(PropertyDeclaration(propType, prop.Name) + .AddModifiers(SyntaxKind.PublicKeyword) + .AddAccessorListAccessors( + AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, + Block(ReturnStatement(IdentifierName(fieldName)))), + AccessorDeclaration(SyntaxKind.SetAccessorDeclaration, + Block( + ParseStatement("var changed = false;"), + IfStatement(BinaryExpression(SyntaxKind.NotEqualsExpression, + IdentifierName(fieldName), + IdentifierName("value")), + Block( + ParseStatement("On" + prop.Name + "Changing();"), + ParseStatement("changed = true;"), + GeneratePropertySetterAssignment(cl, prop, isObject, isNullable)) + ), + ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + IdentifierName(fieldName), IdentifierName("value"))), + ParseStatement($"if(changed) On" + prop.Name + "Changed();") + )).WithModifiers(TokenList(prop.InternalSet ? new[]{Token(SyntaxKind.InternalKeyword)} : Array.Empty())) + )) + .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changed") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())) + .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changing") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); } - StatementSyntax GeneratePropertySetterAssignment(GProperty prop, string fieldName, bool isObject, bool isNullable) + EnumDeclarationSyntax GenerateChangedFieldsEnum(GClass cl) { - var normalChangesAssignment = (StatementSyntax)ExpressionStatement(AssignmentExpression( - SyntaxKind.SimpleAssignmentExpression, - MemberAccess((ExpressionSyntax) IdentifierName("Changes"), prop.Name, - "Value"), - isObject - ? - ConditionalMemberAccess(IdentifierName("value"), "Server", isNullable) - : IdentifierName("value"))); - if (!prop.Animated) - return normalChangesAssignment; + var changedFieldsEnum = EnumDeclaration(Identifier(ChangedFieldsTypeName(cl))); + int count = 0; - var code = $@" -{{ - if(animation is CompositionAnimation a) - Changes.{prop.Name}.Animation = a.CreateInstance(this.Server, value); - else + void AddValue(string name) + { + var value = 1ul << count; + changedFieldsEnum = changedFieldsEnum.AddMembers( + EnumMemberDeclaration(name) + .WithEqualsValue(EqualsValueClause(ParseExpression(value.ToString())))); + count++; + } + + foreach (var prop in cl.Properties) + { + AddValue(prop.Name); + + if (prop.Animated) + AddValue(prop.Name + "Animated"); + } + + var baseType = count <= 8 ? "byte" : count <= 16 ? "ushort" : count <= 32 ? "uint" : "ulong"; + return changedFieldsEnum.AddBaseListTypes(SimpleBaseType(ParseTypeName(baseType))) + .AddAttributeLists(AttributeList(SingletonSeparatedList(Attribute(IdentifierName("System.Flags"))))); + } + + StatementSyntax GeneratePropertySetterAssignment(GClass cl, GProperty prop, bool isObject, bool isNullable) + { + var pendingAnimationField = PropertyPendingAnimationFieldName(prop); + + var code = @$" + // Update the backing value + {PropertyBackingFieldName(prop)} = value; + + // Register object for serialization in the next batch + {ChangedFieldsFieldName(cl)} |= {ChangedFieldsTypeName(cl)}.{prop.Name}; + RegisterForSerialization(); +"; + if (prop.Animated) + { + code += @$" + // Reset previous animation if any + {pendingAnimationField} = null; + {ChangedFieldsFieldName(cl)} &= ~{ChangedFieldsTypeName(cl)}.{prop.Name}Animated; + // Check for implicit animations + if(ImplicitAnimations != null && ImplicitAnimations.TryGetValue(""{prop.Name}"", out var animation) == true) {{ - var saved = Changes.{prop.Name}; - if(!StartAnimationGroup(animation, ""{prop.Name}"", value)) - Changes.{prop.Name}.Value = value; + // Animation affects only current property + if(animation is CompositionAnimation a) + {{ + {ChangedFieldsFieldName(cl)} |= {ChangedFieldsTypeName(cl)}.{prop.Name}Animated; + {pendingAnimationField} = a.CreateInstance(this.Server, value); + }} + // Animation is triggered by the current field, but does not necessary affects it + StartAnimationGroup(animation, ""{prop.Name}"", value); }} -}} - "; - - return IfStatement( - ParseExpression( - $"ImplicitAnimations != null && ImplicitAnimations.TryGetValue(\"{prop.Name}\", out var animation) == true"), - ParseStatement(code), - ElseClause(normalChangesAssignment) - ); + } + + return ParseStatement("{\n" + code + "\n}"); } - BlockSyntax ApplyStartAnimation(BlockSyntax body, GProperty prop, string fieldName) + BlockSyntax ApplyStartAnimation(BlockSyntax body, GClass cl, GProperty prop) { var code = $@" if (propertyName == ""{prop.Name}"") {{ -var current = {fieldName}; +var current = {PropertyBackingFieldName(prop)}; var server = animation.CreateInstance(this.Server, finalValue); -Changes.{prop.Name}.Animation = server; +{PropertyPendingAnimationFieldName(prop)} = server; +{ChangedFieldsFieldName(cl)} |= {ChangedFieldsTypeName(cl)}.{prop.Name}Animated; +RegisterForSerialization(); return; }} "; @@ -522,15 +507,75 @@ return; return body; } + + private BlockSyntax SerializeChangesPrologue(GClass cl) + { + return Block( + ParseStatement("base.SerializeChangesCore(writer);"), + ParseStatement($"writer.Write({ChangedFieldsFieldName(cl)});") + ); + } + + BlockSyntax ApplySerializeField(BlockSyntax body, GClass cl, GProperty prop, bool isObject, bool isPassthrough) + { + var changedFields = ChangedFieldsFieldName(cl); + var changedFieldsType = ChangedFieldsTypeName(cl); + + var code = ""; + if (prop.Animated) + { + code = $@" + if(({changedFields} & {changedFieldsType}.{prop.Name}Animated) == {changedFieldsType}.{prop.Name}Animated) + writer.WriteObject({PropertyPendingAnimationFieldName(prop)}); + else "; + } + + code += $@" + if(({changedFields} & {changedFieldsType}.{prop.Name}) == {changedFieldsType}.{prop.Name}) + writer.Write{(isObject ? "Object" : "")}({PropertyBackingFieldName(prop)}{(isObject && !isPassthrough ? "?.Server!":"")}); +"; + return body.AddStatements(ParseStatement(code)); + } + + private BlockSyntax DeserializeChangesPrologue(GClass cl) + { + return Block(ParseStatement($@" +base.DeserializeChangesCore(reader, commitedAt); +DeserializeChangesExtra(reader); +var changed = reader.Read<{ChangedFieldsTypeName(cl)}>(); +")); + } + + BlockSyntax ApplyDeserializeField(BlockSyntax body, GClass cl, GProperty prop, string serverType, bool isObject) + { + var changedFieldsType = ChangedFieldsTypeName(cl); + var code = ""; + if (prop.Animated) + { + code = $@" + if((changed & {changedFieldsType}.{prop.Name}Animated) == {changedFieldsType}.{prop.Name}Animated) + {PropertyBackingFieldName(prop)}.SetAnimation(this, commitedAt, reader.ReadObject(), {ServerPropertyOffsetFieldName(prop)}); + else "; + } + + var readValueCode = $"reader.Read{(isObject ? "Object" : "")}<{serverType}>()"; + code += $@" + if((changed & {changedFieldsType}.{prop.Name}) == {changedFieldsType}.{prop.Name}) +"; + if (prop.Animated) + code += $"{PropertyBackingFieldName(prop)}.SetValue(this, {readValueCode});"; + else code += $"{prop.Name} = {readValueCode};"; + return body.AddStatements(ParseStatement(code)); + } - ClassDeclarationSyntax WithGetProperty(ClassDeclarationSyntax cl, BlockSyntax body, bool server) + ClassDeclarationSyntax WithGetPropertyForAnimation(ClassDeclarationSyntax cl, BlockSyntax body) { if (body.Statements.Count == 0) return cl; body = body.AddStatements( ParseStatement("return base.GetPropertyForAnimation(name);")); var method = ((MethodDeclarationSyntax) ParseMemberDeclaration( - $"{(server ? "public" : "internal")} override Avalonia.Rendering.Composition.Expressions.ExpressionVariant GetPropertyForAnimation(string name){{}}")) + $"public override Avalonia.Rendering.Composition.Expressions.ExpressionVariant GetPropertyForAnimation(string name){{}}")) .WithBody(body); return cl.AddMembers(method); diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 7a64b39575..9c82288c8e 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -104,7 +104,7 @@ namespace Avalonia.X11 } if (options.UseCompositor) - Compositor = Compositor.Create(AvaloniaLocator.Current.GetService()!); + Compositor = new Compositor(AvaloniaLocator.Current.GetService()!); } diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 32705a2cc6..4b0350f40f 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -188,7 +188,7 @@ namespace Avalonia.Win32 AvaloniaLocator.CurrentMutable.Bind().ToSingleton(); if (Options.UseCompositor) - Compositor = Compositor.Create(AvaloniaLocator.Current.GetRequiredService()); + Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService()); } public bool HasMessages() diff --git a/tests/Avalonia.Base.UnitTests/Composition/BatchStreamTests.cs b/tests/Avalonia.Base.UnitTests/Composition/BatchStreamTests.cs new file mode 100644 index 0000000000..a1b55257e6 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Composition/BatchStreamTests.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks.Dataflow; +using Avalonia.Rendering.Composition.Transport; +using Xunit; + +namespace Avalonia.Base.UnitTests.Composition; + +public class BatchStreamTests +{ + [Fact] + public void BatchStreamCorrectlyWritesAndReadsData() + { + var data = new BatchStreamData(); + var memPool = new BatchStreamMemoryPool(100, _ => { }); + var objPool = new BatchStreamObjectPool(10, _ => { }); + + var guids = new List(); + var objects = new List(); + for (var c = 0; c < 453; c++) + { + guids.Add(Guid.NewGuid()); + objects.Add(new object()); + } + + using (var writer = new BatchStreamWriter(data, memPool, objPool)) + { + foreach(var guid in guids) + writer.Write(guid); + foreach (var obj in objects) + writer.WriteObject(obj); + } + + using (var reader = new BatchStreamReader(data, memPool, objPool)) + { + foreach (var guid in guids) + Assert.Equal(guid, reader.Read()); + foreach (var obj in objects) + Assert.Equal(obj, reader.ReadObject()); + } + + + + } +} \ No newline at end of file From 2d8be0dff68dc4036bc6e8b5c40894f0367e6271 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 26 May 2022 21:51:40 +0300 Subject: [PATCH 023/389] Fixes --- .../Composition/Transport/BatchStreamArrayPool.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs index d76a9c609e..97d05704af 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs @@ -51,11 +51,12 @@ internal abstract class BatchStreamPoolBase : IDisposable { var maximumUsage = _usageStatistics.Max(); var recentlyUsedPooledSlots = maximumUsage - _usage; - while (recentlyUsedPooledSlots < _pool.Count) + var keepSlots = Math.Max(recentlyUsedPooledSlots, 10); + while (keepSlots < _pool.Count) DestroyItem(_pool.Pop()); - _usageStatistics[_usageStatisticsSlot] = 0; _usageStatisticsSlot = (_usageStatisticsSlot + 1) % _usageStatistics.Length; + _usageStatistics[_usageStatisticsSlot] = 0; } } @@ -137,7 +138,7 @@ internal sealed class BatchStreamMemoryPool : BatchStreamPoolBase { public int BufferSize { get; } - public BatchStreamMemoryPool(int bufferSize = 16384, Action>? startTimer = null) : base(true, startTimer) + public BatchStreamMemoryPool(int bufferSize = 1024, Action>? startTimer = null) : base(true, startTimer) { BufferSize = bufferSize; } From dbbed2c70bcb7abff176edffa7a7b045a0dd914d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 26 May 2022 23:14:13 +0300 Subject: [PATCH 024/389] Added Dispose for CompositionTarget --- .../Composition/CompositionObject.cs | 5 ++-- .../Server/ServerCompositionTarget.cs | 27 ++++++++++++++++++- .../Composition/Server/ServerCompositor.cs | 2 ++ .../Composition/Server/ServerObject.cs | 4 ++- 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs index d561338a36..baf1bfcddf 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs @@ -25,7 +25,7 @@ namespace Avalonia.Rendering.Composition public void Dispose() { - //Changes.Dispose = true; + RegisterForSerialization(); IsDisposed = true; } @@ -112,7 +112,8 @@ namespace Avalonia.Rendering.Composition private protected virtual void SerializeChangesCore(BatchStreamWriter writer) { - + if (Server is IDisposable) + writer.Write((byte)(IsDisposed ? 1 : 0)); } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 7567eba534..9513fb58fa 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -9,7 +9,7 @@ using Avalonia.Utilities; namespace Avalonia.Rendering.Composition.Server { - internal partial class ServerCompositionTarget + internal partial class ServerCompositionTarget : IDisposable { private readonly ServerCompositor _compositor; private readonly Func _renderTargetFactory; @@ -23,6 +23,7 @@ namespace Avalonia.Rendering.Composition.Server private Size _layerSize; private IDrawingContextLayerImpl? _layer; private bool _redrawRequested; + private bool _disposed; public ReadbackIndices Readback { get; } = new(); @@ -50,6 +51,12 @@ namespace Avalonia.Rendering.Composition.Server public void Render() { + if (_disposed) + { + Compositor.RemoveCompositionTarget(this); + return; + } + if (Root == null) return; _renderTarget ??= _renderTargetFactory(); @@ -69,6 +76,12 @@ namespace Avalonia.Rendering.Composition.Server _redrawRequested = false; using (var targetContext = _renderTarget.CreateDrawingContext(null)) { + // This is a hack to safely dispose layer created by some other render target + // because we can only dispose layers with the corresponding GPU context being + // active on the current thread + while (Compositor.LayersToDispose.Count > 0) + Compositor.LayersToDispose.Dequeue().Dispose(); + var layerSize = Size * Scaling; if (layerSize != _layerSize || _layer == null) { @@ -130,5 +143,17 @@ namespace Avalonia.Rendering.Composition.Server { _redrawRequested = true; } + + public void Dispose() + { + _disposed = true; + if (_layer != null) + { + Compositor.LayersToDispose.Enqueue(_layer); + _layer = null; + } + _renderTarget?.Dispose(); + _renderTarget = null; + } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index f7de704b23..241be479ff 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using Avalonia.Platform; using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Expressions; using Avalonia.Rendering.Composition.Transport; @@ -19,6 +20,7 @@ namespace Avalonia.Rendering.Composition.Server private List _animationsToUpdate = new(); private BatchStreamObjectPool _batchObjectPool; private BatchStreamMemoryPool _batchMemoryPool; + public Queue LayersToDispose { get; } = new(); public ServerCompositor(IRenderLoop renderLoop, BatchStreamObjectPool batchObjectPool, BatchStreamMemoryPool batchMemoryPool) { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs index 16f57d9059..dde711c3b5 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs @@ -107,7 +107,9 @@ namespace Avalonia.Rendering.Composition.Server protected virtual void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt) { - + if (this is IDisposable disp + && reader.Read() == 1) + disp.Dispose(); } public void DeserializeChanges(BatchStreamReader reader, Batch batch) From 7790a513065b0e91676ba675bea4587ff428d402 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 26 May 2022 23:15:39 +0300 Subject: [PATCH 025/389] Fixed hittest filtering --- .../Rendering/Composition/CompositingRenderer.cs | 6 +++--- .../Composition/CompositionDrawListVisual.cs | 6 +++++- .../Rendering/Composition/CompositionTarget.cs | 13 ++++++++----- src/Avalonia.Base/Rendering/Composition/Visual.cs | 4 +++- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index 88c6948e48..b83f804a8f 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -65,9 +65,9 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor QueueUpdate(); } - public IEnumerable HitTest(Point p, IVisual root, Func filter) + public IEnumerable HitTest(Point p, IVisual root, Func? filter) { - var res = _target.TryHitTest(p); + var res = _target.TryHitTest(p, filter); if(res == null) yield break; for (var index = res.Count - 1; index >= 0; index--) @@ -81,7 +81,7 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor } } - public IVisual? HitTestFirst(Point p, IVisual root, Func filter) + public IVisual? HitTestFirst(Point p, IVisual root, Func? filter) { // TODO: Optimize return HitTest(p, root, filter).FirstOrDefault(); diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs index 069d888fbb..cc2b411822 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs @@ -1,7 +1,9 @@ +using System; using System.Numerics; using Avalonia.Rendering.Composition.Drawing; using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.Composition.Transport; +using Avalonia.VisualTree; namespace Avalonia.Rendering.Composition; @@ -39,10 +41,12 @@ internal class CompositionDrawListVisual : CompositionContainerVisual Visual = visual; } - internal override bool HitTest(Point pt) + internal override bool HitTest(Point pt, Func? filter) { if (DrawList == null) return false; + if (filter != null && !filter(Visual)) + return false; if (Visual is ICustomHitTest custom) return custom.HitTest(pt); foreach (var op in DrawList) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs index c5cfaeacce..3243934932 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs @@ -1,6 +1,8 @@ +using System; using System.Collections.Generic; using System.Numerics; using Avalonia.Collections.Pooled; +using Avalonia.VisualTree; namespace Avalonia.Rendering.Composition { @@ -18,13 +20,13 @@ namespace Avalonia.Rendering.Composition Root.Root = null; } - public PooledList? TryHitTest(Point point) + public PooledList? TryHitTest(Point point, Func? filter) { Server.Readback.NextRead(); if (Root == null) return null; var res = new PooledList(); - HitTestCore(Root, point, res); + HitTestCore(Root, point, res, filter); return res; } @@ -69,7 +71,8 @@ namespace Avalonia.Rendering.Composition return false; } - bool HitTestCore(CompositionVisual visual, Point point, PooledList result) + bool HitTestCore(CompositionVisual visual, Point point, PooledList result, + Func? filter) { //TODO: Check readback too if (visual.Visible == false) @@ -80,7 +83,7 @@ namespace Avalonia.Rendering.Composition { bool success = false; // Hit-test the current node - if (visual.HitTest(point)) + if (visual.HitTest(point, filter)) { result.Add(visual); success = true; @@ -91,7 +94,7 @@ namespace Avalonia.Rendering.Composition for (var c = cv.Children.Count - 1; c >= 0; c--) { var ch = cv.Children[c]; - var hit = HitTestCore(ch, point, result); + var hit = HitTestCore(ch, point, result, filter); if (hit) return true; } diff --git a/src/Avalonia.Base/Rendering/Composition/Visual.cs b/src/Avalonia.Base/Rendering/Composition/Visual.cs index 5bf5dcee74..3d6e3fdaeb 100644 --- a/src/Avalonia.Base/Rendering/Composition/Visual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Visual.cs @@ -1,4 +1,6 @@ +using System; using System.Numerics; +using Avalonia.VisualTree; namespace Avalonia.Rendering.Composition { @@ -33,6 +35,6 @@ namespace Avalonia.Rendering.Composition internal object? Tag { get; set; } - internal virtual bool HitTest(Point point) => true; + internal virtual bool HitTest(Point point, Func? filter) => true; } } \ No newline at end of file From fcb7d254f9d0f147dd85c38200a3ebd896c65b3c Mon Sep 17 00:00:00 2001 From: robloo Date: Fri, 20 May 2022 00:01:21 -0400 Subject: [PATCH 026/389] Add IColorPalette --- .../ColorPalette/IColorPalette.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/Avalonia.Controls.ColorPicker/ColorPalette/IColorPalette.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalette/IColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalette/IColorPalette.cs new file mode 100644 index 0000000000..7c6ebc3f6a --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorPalette/IColorPalette.cs @@ -0,0 +1,38 @@ +using Avalonia.Media; + +namespace Avalonia.Controls +{ + /// + /// Interface to define a color palette. + /// + public interface IColorPalette + { + /// + /// Gets the total number of colors in this palette. + /// A color is not necessarily a single value and may be composed of several shades. + /// + /// + /// Represents total columns in a table. + /// + int ColorCount { get; } + + /// + /// Gets the total number of shades for each color in this palette. + /// Shades are usually a variation of the color lightening or darkening it. + /// + /// + /// Represents total rows in a table. + /// + int ShadeCount { get; } + + /// + /// Gets a color in the palette by index. + /// + /// The index of the color in the palette. + /// The index must be between zero and . + /// The index of the color shade in the palette. + /// The index must be between zero and . + /// The color at the specified index or an exception. + Color GetColor(int colorIndex, int shadeIndex); + } +} From fa5a47b4426d32a00b01233540e25342f2013646 Mon Sep 17 00:00:00 2001 From: robloo Date: Fri, 20 May 2022 00:01:44 -0400 Subject: [PATCH 027/389] Add initial ColorView.Properties --- .../ColorSpectrum/ColorSpectrum.Properties.cs | 2 +- .../ColorView/ColorView.Properties.cs | 362 ++++++++++++++++++ .../ColorView/ColorView.cs | 21 + 3 files changed, 384 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs create mode 100644 src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs index 587a89ee38..00d84f5dd3 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs @@ -97,7 +97,7 @@ namespace Avalonia.Controls.Primitives /// Gets or sets the currently selected color in the RGB color model. /// /// - /// For control authors use instead to avoid loss + /// For control authors, use instead to avoid loss /// of precision and color drifting. /// public Color Color diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs new file mode 100644 index 0000000000..aa5dfb5fc4 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs @@ -0,0 +1,362 @@ +using System.Collections.ObjectModel; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Media; + +namespace Avalonia.Controls +{ + public partial class ColorView + { + // SelectedColorModel ActiveColorModel? + // SelectedTab + + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorProperty = + AvaloniaProperty.Register( + nameof(Color), + Colors.White, + defaultBindingMode: BindingMode.TwoWay); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorSpectrumComponentsProperty = + AvaloniaProperty.Register( + nameof(ColorSpectrumComponents), + ColorSpectrumComponents.HueSaturation); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorSpectrumShapeProperty = + AvaloniaProperty.Register( + nameof(ColorSpectrumShape), + ColorSpectrumShape.Box); + + /// + /// Defines the property. + /// + public static readonly DirectProperty> CustomPaletteColorsProperty = + AvaloniaProperty.RegisterDirect>( + nameof(CustomPaletteColors), + o => o.CustomPaletteColors); + + /// + /// Defines the property. + /// + public static readonly StyledProperty CustomPaletteColumnCountProperty = + AvaloniaProperty.Register( + nameof(CustomPaletteColumnCount), + 4); + + /// + /// Defines the property. + /// + public static readonly StyledProperty CustomPaletteProperty = + AvaloniaProperty.Register( + nameof(CustomPalette), + null); + + /// + /// Defines the property. + /// + public static readonly StyledProperty HsvColorProperty = + AvaloniaProperty.Register( + nameof(HsvColor), + Colors.White.ToHsv(), + defaultBindingMode: BindingMode.TwoWay); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsAlphaEnabledProperty = + AvaloniaProperty.Register( + nameof(IsAlphaEnabled), + false); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsAlphaSliderVisibleProperty = + AvaloniaProperty.Register( + nameof(IsAlphaSliderVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsAlphaTextInputVisibleProperty = + AvaloniaProperty.Register( + nameof(IsAlphaTextInputVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsColorChannelTextInputVisibleProperty = + AvaloniaProperty.Register( + nameof(IsColorChannelTextInputVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsColorPaletteVisibleProperty = + AvaloniaProperty.Register( + nameof(IsColorPaletteVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsColorPreviewVisibleProperty = + AvaloniaProperty.Register( + nameof(IsColorPreviewVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsColorSliderVisibleProperty = + AvaloniaProperty.Register( + nameof(IsColorSliderVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsColorSpectrumVisibleProperty = + AvaloniaProperty.Register( + nameof(IsColorSpectrumVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsHexInputVisibleProperty = + AvaloniaProperty.Register( + nameof(IsHexInputVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaxHueProperty = + AvaloniaProperty.Register( + nameof(MaxHue), + 359); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaxSaturationProperty = + AvaloniaProperty.Register( + nameof(MaxSaturation), + 100); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaxValueProperty = + AvaloniaProperty.Register( + nameof(MaxValue), + 100); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinHueProperty = + AvaloniaProperty.Register( + nameof(MinHue), + 0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinSaturationProperty = + AvaloniaProperty.Register( + nameof(MinSaturation), + 0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinValueProperty = + AvaloniaProperty.Register( + nameof(MinValue), + 0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ShowAccentColorsProperty = + AvaloniaProperty.Register( + nameof(ShowAccentColors), + true); + + /// + public Color Color + { + get => GetValue(ColorProperty); + set => SetValue(ColorProperty, value); + } + + /// + public ColorSpectrumComponents ColorSpectrumComponents + { + get => GetValue(ColorSpectrumComponentsProperty); + set => SetValue(ColorSpectrumComponentsProperty, value); + } + + /// + public ColorSpectrumShape ColorSpectrumShape + { + get => GetValue(ColorSpectrumShapeProperty); + set => SetValue(ColorSpectrumShapeProperty, value); + } + + /// + /// Gets the list of custom palette colors. + /// + public ObservableCollection CustomPaletteColors + { + get => _customPaletteColors; + } + + /// + /// Gets or sets the number of colors in each row (section) of the custom color palette. + /// Within a standard palette, rows are shades and columns are colors. + /// + public int CustomPaletteColumnCount + { + get => GetValue(CustomPaletteColumnCountProperty); + set => SetValue(CustomPaletteColumnCountProperty, value); + } + + /// + /// Gets or sets the custom color palette. + /// This will automatically set and + /// overwriting any existing values. + /// + public IColorPalette? CustomPalette + { + get => GetValue(CustomPaletteProperty); + set => SetValue(CustomPaletteProperty, value); + } + + /// + public HsvColor HsvColor + { + get => GetValue(HsvColorProperty); + set => SetValue(HsvColorProperty, value); + } + + public bool IsAlphaEnabled + { + get => GetValue(IsAlphaEnabledProperty); + set => SetValue(IsAlphaEnabledProperty, value); + } + + public bool IsAlphaSliderVisible + { + get => GetValue(IsAlphaSliderVisibleProperty); + set => SetValue(IsAlphaSliderVisibleProperty, value); + } + + public bool IsAlphaTextInputVisible + { + get => GetValue(IsAlphaTextInputVisibleProperty); + set => SetValue(IsAlphaTextInputVisibleProperty, value); + } + + public bool IsColorChannelTextInputVisible // TODO: Component + { + get => GetValue(IsColorChannelTextInputVisibleProperty); + set => SetValue(IsColorChannelTextInputVisibleProperty, value); + } + + /// + /// Gets or sets a value indicating whether the color palette is visible. + /// + public bool IsColorPaletteVisible + { + get => GetValue(IsColorPaletteVisibleProperty); + set => SetValue(IsColorPaletteVisibleProperty, value); + } + + public bool IsColorPreviewVisible + { + get => GetValue(IsColorPreviewVisibleProperty); + set => SetValue(IsColorPreviewVisibleProperty, value); + } + + public bool IsColorSliderVisible + { + get => GetValue(IsColorSliderVisibleProperty); + set => SetValue(IsColorSliderVisibleProperty, value); + } + + public bool IsColorSpectrumVisible + { + get => GetValue(IsColorSpectrumVisibleProperty); + set => SetValue(IsColorSpectrumVisibleProperty, value); + } + + public bool IsHexInputVisible + { + get => GetValue(IsHexInputVisibleProperty); + set => SetValue(IsHexInputVisibleProperty, value); + } + + /// + public int MaxHue + { + get => GetValue(MaxHueProperty); + set => SetValue(MaxHueProperty, value); + } + + /// + public int MaxSaturation + { + get => GetValue(MaxSaturationProperty); + set => SetValue(MaxSaturationProperty, value); + } + + /// + public int MaxValue + { + get => GetValue(MaxValueProperty); + set => SetValue(MaxValueProperty, value); + } + + /// + public int MinHue + { + get => GetValue(MinHueProperty); + set => SetValue(MinHueProperty, value); + } + + /// + public int MinSaturation + { + get => GetValue(MinSaturationProperty); + set => SetValue(MinSaturationProperty, value); + } + + /// + public int MinValue + { + get => GetValue(MinValueProperty); + set => SetValue(MinValueProperty, value); + } + + /// + public bool ShowAccentColors + { + get => GetValue(ShowAccentColorsProperty); + set => SetValue(ShowAccentColorsProperty, value); + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs new file mode 100644 index 0000000000..3aff4614a5 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Controls.Primitives; +using Avalonia.Media; + +namespace Avalonia.Controls +{ + /// + /// Presents a color for user editing using a spectrum, palette and component sliders. + /// + public partial class ColorView : TemplatedControl + { + private ObservableCollection _customPaletteColors = new ObservableCollection(); + + + } +} From d3bad7bd1d5612901ff0f3f74080ca5349091228 Mon Sep 17 00:00:00 2001 From: robloo Date: Fri, 20 May 2022 00:01:55 -0400 Subject: [PATCH 028/389] Add ColorViewTab enum --- .../ColorView/ColorViewTab.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs new file mode 100644 index 0000000000..d8c7d5163c --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs @@ -0,0 +1,23 @@ +namespace Avalonia.Controls +{ + /// + /// Defines a specific tab (subview) within the . + /// + public enum ColorViewTab + { + /// + /// The components view with sliders and numeric input boxes. + /// + Components, + + /// + /// The color palette view with a grid of colors and shades. + /// + Palette, + + /// + /// The color spectrum view with a box/ring spectrum and sliders. + /// + Spectrum, + } +} From 77e36914d4c5bb49e12c8a79bb7d5de30a71f19c Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 22 May 2022 12:06:58 -0400 Subject: [PATCH 029/389] Add FluentColorPalette --- .../ColorPalette/FluentColorPalette.cs | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs new file mode 100644 index 0000000000..89400280a9 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs @@ -0,0 +1,142 @@ +using Avalonia.Media; +using Avalonia.Utilities; + +namespace Avalonia.Controls +{ + /// + /// Implements the standard Windows 10 color palette. + /// + public class FluentColorPalette : IColorPalette + { + // Values were taken from the Settings App, Personalization > Colors which match with + // https://docs.microsoft.com/en-us/windows/uwp/whats-new/windows-docs-december-2017 + // + // The default ordering and grouping of colors was undesirable so was modified. + // Colors were transposed: the colors in rows within the Settings app became columns here. + // This is because columns in an IColorPalette generally should contain different shades of + // the same color. In the settings app this concept is somewhat loosely reversed. + // The first 'column' ordering, after being transposed, was then reversed so 'red' colors + // were near to each other. + // + // This new ordering most closely follows the Windows standard while: + // + // 1. Keeping colors in a 'spectrum' order + // 2. Keeping like colors next to each both in rows and columns + // (which is unique for the windows palette). + // For example, similar red colors are next to each other in both + // rows within the same column and rows within the column next to it. + // This follows a 'snake-like' pattern as illustrated below. + // 3. A downside of this ordering is colors don't follow strict 'shades' + // as in other palettes. + // + // The colors will be displayed in the below pattern. + // This pattern follows a spectrum while keeping like-colors near to one + // another across both rows and columns. + // + // ┌Red───┐ ┌Blue──┐ ┌Gray──┐ + // │ │ │ │ │ | + // │ │ │ │ │ | + // Yellow └Violet┘ └Green─┘ Brown + + private static Color[,] colorChart = new Color[,] + { + { + // Ordering reversed for this section only + Color.FromArgb(255, 255, 67, 67), /* #FF4343 */ + Color.FromArgb(255, 209, 52, 56), /* #D13438 */ + Color.FromArgb(255, 239, 105, 80), /* #EF6950 */ + Color.FromArgb(255, 218, 59, 1), /* #DA3B01 */ + Color.FromArgb(255, 202, 80, 16), /* #CA5010 */ + Color.FromArgb(255, 247, 99, 12), /* #F7630C */ + Color.FromArgb(255, 255, 140, 0), /* #FF8C00 */ + Color.FromArgb(255, 255, 185, 0), /* #FFB900 */ + }, + { + Color.FromArgb(255, 231, 72, 86), /* #E74856 */ + Color.FromArgb(255, 232, 17, 35), /* #E81123 */ + Color.FromArgb(255, 234, 0, 94), /* #EA005E */ + Color.FromArgb(255, 195, 0, 82), /* #C30052 */ + Color.FromArgb(255, 227, 0, 140), /* #E3008C */ + Color.FromArgb(255, 191, 0, 119), /* #BF0077 */ + Color.FromArgb(255, 194, 57, 179), /* #C239B3 */ + Color.FromArgb(255, 154, 0, 137), /* #9A0089 */ + }, + { + Color.FromArgb(255, 0, 120, 215), /* #0078D7 */ + Color.FromArgb(255, 0, 99, 177), /* #0063B1 */ + Color.FromArgb(255, 142, 140, 216), /* #8E8CD8 */ + Color.FromArgb(255, 107, 105, 214), /* #6B69D6 */ + Color.FromArgb(255, 135, 100, 184), /* #8764B8 */ + Color.FromArgb(255, 116, 77, 169), /* #744DA9 */ + Color.FromArgb(255, 177, 70, 194), /* #B146C2 */ + Color.FromArgb(255, 136, 23, 152), /* #881798 */ + }, + { + Color.FromArgb(255, 0, 153, 188), /* #0099BC */ + Color.FromArgb(255, 45, 125, 154), /* #2D7D9A */ + Color.FromArgb(255, 0, 183, 195), /* #00B7C3 */ + Color.FromArgb(255, 3, 131, 135), /* #038387 */ + Color.FromArgb(255, 0, 178, 148), /* #00B294 */ + Color.FromArgb(255, 1, 133, 116), /* #018574 */ + Color.FromArgb(255, 0, 204, 106), /* #00CC6A */ + Color.FromArgb(255, 16, 137, 62), /* #10893E */ + }, + { + Color.FromArgb(255, 122, 117, 116), /* #7A7574 */ + Color.FromArgb(255, 93, 90, 80), /* #5D5A58 */ + Color.FromArgb(255, 104, 118, 138), /* #68768A */ + Color.FromArgb(255, 81, 92, 107), /* #515C6B */ + Color.FromArgb(255, 86, 124, 115), /* #567C73 */ + Color.FromArgb(255, 72, 104, 96), /* #486860 */ + Color.FromArgb(255, 73, 130, 5), /* #498205 */ + Color.FromArgb(255, 16, 124, 16), /* #107C10 */ + }, + { + Color.FromArgb(255, 118, 118, 118), /* #767676 */ + Color.FromArgb(255, 76, 74, 72), /* #4C4A48 */ + Color.FromArgb(255, 105, 121, 126), /* #69797E */ + Color.FromArgb(255, 74, 84, 89), /* #4A5459 */ + Color.FromArgb(255, 100, 124, 100), /* #647C64 */ + Color.FromArgb(255, 82, 94, 84), /* #525E54 */ + Color.FromArgb(255, 132, 117, 69), /* #847545 */ + Color.FromArgb(255, 126, 115, 95), /* #7E735F */ + } + }; + + /// + /// Gets the index of the default shade of colors in this palette. + /// This has little meaning in this palette as colors are not strictly separated by shade. + /// + public const int DefaultShadeIndex = 0; + + /// + /// Gets the total number of colors in this palette. + /// A color is not necessarily a single value and may be composed of several shades. + /// This has little meaning in this palette as colors are not strictly separated. + /// + /// + public int ColorCount + { + get => colorChart.GetLength(0); + } + + /// + /// Gets the total number of shades for each color in this palette. + /// Shades are usually a variation of the color lightening or darkening it. + /// This has little meaning in this palette as colors are not strictly separated by shade. + /// + /// + public int ShadeCount + { + get => colorChart.GetLength(1); + } + + /// + public Color GetColor(int colorIndex, int shadeIndex) + { + return colorChart[ + MathUtilities.Clamp(colorIndex, 0, colorChart.GetLength(0)), + MathUtilities.Clamp(shadeIndex, 0, colorChart.GetLength(1))]; + } + } +} From 33637f651a3456f7791dbe01f3e16c79fd0725d6 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 22 May 2022 12:07:09 -0400 Subject: [PATCH 030/389] Fix ColorToHexConverter --- .../Converters/ColorToHexConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs index 9b09073d9d..8d5f2332be 100644 --- a/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs +++ b/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs @@ -42,7 +42,7 @@ namespace Avalonia.Controls.Converters return AvaloniaProperty.UnsetValue; } - string hexColor = color.ToString(); + string hexColor = color.ToUint32().ToString("x8", CultureInfo.InvariantCulture).ToUpperInvariant(); if (includeSymbol == false) { From 2835767b42ddfc23d40e64c34ea0719312ab8586 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 22 May 2022 12:09:28 -0400 Subject: [PATCH 031/389] Add incomplete ColorView control template --- .../ControlCatalog/Pages/ColorPickerPage.xaml | 9 +- .../ColorView/ColorView.Properties.cs | 4 +- .../ColorView/ColorView.cs | 86 ++++++ .../ColorView/ColorViewTab.cs | 6 +- .../Themes/Fluent/ColorView.xaml | 259 ++++++++++++++++++ .../Themes/Fluent/Fluent.xaml | 3 + 6 files changed, 360 insertions(+), 7 deletions(-) create mode 100644 src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml b/samples/ControlCatalog/Pages/ColorPickerPage.xaml index c0c83d6a35..47a407821c 100644 --- a/samples/ControlCatalog/Pages/ColorPickerPage.xaml +++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml @@ -13,8 +13,11 @@ - - + + - diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs index aa5dfb5fc4..eddbeaf112 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs @@ -292,7 +292,9 @@ namespace Avalonia.Controls set => SetValue(IsColorPreviewVisibleProperty, value); } - public bool IsColorSliderVisible + // IsColorComponentsVisible + + public bool IsColorSliderVisible // ColorSpectrumSlider { get => GetValue(IsColorSliderVisibleProperty); set => SetValue(IsColorSliderVisibleProperty, value); diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index 3aff4614a5..76e3cfe3e1 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -14,8 +14,94 @@ namespace Avalonia.Controls /// public partial class ColorView : TemplatedControl { + /// + /// Event for when the selected color changes within the slider. + /// + public event EventHandler? ColorChanged; + + private bool disableUpdates = false; + private ObservableCollection _customPaletteColors = new ObservableCollection(); + /// + /// Initializes a new instance of the class. + /// + public ColorView() : base() + { + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + this.CustomPalette = new FluentColorPalette(); + + base.OnApplyTemplate(e); + } + + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if (disableUpdates) + { + base.OnPropertyChanged(change); + return; + } + + // Always keep the two color properties in sync + if (change.Property == ColorProperty) + { + disableUpdates = true; + + HsvColor = Color.ToHsv(); + + OnColorChanged(new ColorChangedEventArgs( + change.GetOldValue(), + change.GetNewValue())); + + disableUpdates = false; + } + else if (change.Property == HsvColorProperty) + { + disableUpdates = true; + + Color = HsvColor.ToRgb(); + + OnColorChanged(new ColorChangedEventArgs( + change.GetOldValue().ToRgb(), + change.GetNewValue().ToRgb())); + + disableUpdates = false; + } + else if (change.Property == CustomPaletteProperty) + { + IColorPalette? palette = CustomPalette; + + // Any custom palette change must be automatically synced with the + // bound properties controlling the palette grid + if (palette != null) + { + CustomPaletteColumnCount = palette.ColorCount; + CustomPaletteColors.Clear(); + + for (int shadeIndex = 0; shadeIndex < palette.ShadeCount; shadeIndex++) + { + for (int colorIndex = 0; colorIndex < palette.ColorCount; colorIndex++) + { + CustomPaletteColors.Add(palette.GetColor(colorIndex, shadeIndex)); + } + } + } + } + + base.OnPropertyChanged(change); + } + /// + /// Called before the event occurs. + /// + /// The defining old/new colors. + protected virtual void OnColorChanged(ColorChangedEventArgs e) + { + ColorChanged?.Invoke(this, e); + } } } diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs index d8c7d5163c..677cdb8674 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs @@ -6,17 +6,17 @@ public enum ColorViewTab { /// - /// The components view with sliders and numeric input boxes. + /// The components subview with sliders and numeric input boxes. /// Components, /// - /// The color palette view with a grid of colors and shades. + /// The color palette subview with a grid of selectable colors. /// Palette, /// - /// The color spectrum view with a box/ring spectrum and sliders. + /// The color spectrum subview with a box/ring spectrum and sliders. /// Spectrum, } diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml new file mode 100644 index 0000000000..9f4324594d --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -0,0 +1,259 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml index c25d79727f..c55766e07c 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml @@ -25,4 +25,7 @@ + + + From 197ab047a510c58b30a32216fb96c8cfd2239c3a Mon Sep 17 00:00:00 2001 From: robloo Date: Fri, 27 May 2022 22:45:56 -0400 Subject: [PATCH 032/389] Add braces --- src/Avalonia.Controls/Primitives/TemplatedControl.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index db029d38c0..7c8c2f882f 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -291,7 +291,9 @@ namespace Avalonia.Controls.Primitives // Existing code kinda expect to see a NameScope even if it's empty if (nameScope == null) + { nameScope = new NameScope(); + } var e = new TemplateAppliedEventArgs(nameScope); OnApplyTemplate(e); From 5ed084123540fae249cc1881a21e67dad42ad0c1 Mon Sep 17 00:00:00 2001 From: robloo Date: Fri, 27 May 2022 22:46:12 -0400 Subject: [PATCH 033/389] Fix index clamping --- .../ColorPalette/FluentColorPalette.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs index 89400280a9..b6f9a244b1 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs @@ -135,8 +135,8 @@ namespace Avalonia.Controls public Color GetColor(int colorIndex, int shadeIndex) { return colorChart[ - MathUtilities.Clamp(colorIndex, 0, colorChart.GetLength(0)), - MathUtilities.Clamp(shadeIndex, 0, colorChart.GetLength(1))]; + MathUtilities.Clamp(colorIndex, 0, colorChart.GetLength(0) - 1), + MathUtilities.Clamp(shadeIndex, 0, colorChart.GetLength(1) - 1)]; } } } From e5b18d0b9c263b65b9121a25e19b6103e21d1d07 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 28 May 2022 01:25:59 -0400 Subject: [PATCH 034/389] Further work on ColorView --- .../Converters/ContrastBrushConverter.cs | 84 +++++++++ .../Themes/Fluent/ColorView.xaml | 165 +++++++++++++++++- 2 files changed, 246 insertions(+), 3 deletions(-) create mode 100644 src/Avalonia.Controls.ColorPicker/Converters/ContrastBrushConverter.cs diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ContrastBrushConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ContrastBrushConverter.cs new file mode 100644 index 0000000000..574f23dfae --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Converters/ContrastBrushConverter.cs @@ -0,0 +1,84 @@ +using System; +using System.Globalization; +using Avalonia.Controls.Converters; +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace Avalonia.Controls.Primitives.Converters +{ + /// + /// Gets a , either black or white, depending on the luminance of the supplied color. + /// A default color supplied in the converter parameter may be returned if alpha is below the set threshold. + /// + public class ContrastBrushConverter : IValueConverter + { + private ToColorConverter toColorConverter = new ToColorConverter(); + + /// + /// Gets or sets the alpha channel threshold below which a default color is used instead of black/white. + /// + public byte AlphaThreshold { get; set; } = 128; + + /// + public object? Convert( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + Color comparisonColor; + Color? defaultColor = null; + + // Get the changing color to compare against + var convertedValue = toColorConverter.Convert(value, targetType, parameter, culture); + if (convertedValue is Color valueColor) + { + comparisonColor = valueColor; + } + else + { + // Invalid color value provided + return AvaloniaProperty.UnsetValue; + } + + // Get the default color when transparency is high + var convertedParameter = toColorConverter.Convert(parameter, targetType, parameter, culture); + if (convertedParameter is Color parameterColor) + { + defaultColor = parameterColor; + } + + if (comparisonColor.A < AlphaThreshold && + defaultColor.HasValue) + { + // If the transparency is less than the threshold, just use the default brush + // This can commonly be something like the TextControlForeground brush + return new SolidColorBrush(defaultColor.Value); + } + else + { + // Chose a white/black brush based on contrast to the base color + if (ColorHelper.GetRelativeLuminance(comparisonColor) <= 0.5) + { + // Dark color, return light for contrast + return new SolidColorBrush(Colors.White); + } + else + { + // Bright color, return dark for contrast + return new SolidColorBrush(Colors.Black); + } + } + } + + /// + public object? ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + return AvaloniaProperty.UnsetValue; + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index 9f4324594d..5d3642a3b1 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -3,12 +3,15 @@ xmlns:converters="using:Avalonia.Controls.Converters" xmlns:primitives="using:Avalonia.Controls.Primitives" xmlns:pc="clr-namespace:Avalonia.Controls.Primitives.Converters;assembly=Avalonia.Controls.ColorPicker" + xmlns:globalization="clr-namespace:System.Globalization;assembly=mscorlib" x:CompileBindings="False"> + + + + + + + @@ -195,11 +230,11 @@ - + + + + + + + + + + + + + + + + From 53a08f126382f9a7eae4d5eda331efd1a7080bd5 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 28 May 2022 12:04:26 -0400 Subject: [PATCH 035/389] Add new EnumToBooleanConverter --- .../Converters/EnumToBooleanConverter.cs | 57 +++++++++++++++++++ .../Converters/EnumValueEqualsConverter.cs | 12 +++- 2 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 src/Avalonia.Controls/Converters/EnumToBooleanConverter.cs diff --git a/src/Avalonia.Controls/Converters/EnumToBooleanConverter.cs b/src/Avalonia.Controls/Converters/EnumToBooleanConverter.cs new file mode 100644 index 0000000000..ba1c4cab3e --- /dev/null +++ b/src/Avalonia.Controls/Converters/EnumToBooleanConverter.cs @@ -0,0 +1,57 @@ +using System; +using System.Globalization; +using Avalonia.Data; +using Avalonia.Data.Converters; + +namespace Avalonia.Controls.Converters +{ + /// + /// Converter to convert an enum value to bool by comparing to the given parameter. + /// Both value and parameter must be of the same enum type. + /// + /// + /// This converter is useful to enable binding of radio buttons with a selected enum value. + /// + public class EnumToBooleanConverter : IValueConverter + { + /// + public object? Convert( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + if (value == null && + parameter == null) + { + return true; + } + else if (value == null || + parameter == null) + { + return false; + } + else + { + return value!.Equals(parameter); + } + } + + /// + public object? ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + if (value is bool boolValue) + { + return boolValue ? parameter : BindingOperations.DoNothing; + } + else + { + return BindingOperations.DoNothing; + } + } + } +} diff --git a/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs b/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs index 1a33a82ca4..abd0fe1dfd 100644 --- a/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs +++ b/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs @@ -10,7 +10,11 @@ namespace Avalonia.Controls.Converters public class EnumValueEqualsConverter : IValueConverter { /// - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + public object? Convert( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) { // Note: Unlike string comparisons, null/empty is not supported // Both 'value' and 'parameter' must exist and if both are missing they are not considered equal @@ -46,7 +50,11 @@ namespace Avalonia.Controls.Converters } /// - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + public object? ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) { throw new System.NotImplementedException(); } From 13b82a0d1d4f70eba999e6e29ae075aed3b6cd1b Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 28 May 2022 12:08:16 -0400 Subject: [PATCH 036/389] Implement ColorModel switching in ColorView --- .../ColorSlider/ColorSlider.cs | 14 ++ .../ColorView/ColorView.Properties.cs | 20 +- .../Themes/Fluent/ColorView.xaml | 172 +++++++++++++----- 3 files changed, 163 insertions(+), 43 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index 3c38c6ed1b..78a796e93a 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -332,6 +332,20 @@ namespace Avalonia.Controls.Primitives disableUpdates = false; } + else if (change.Property == ColorModelProperty) + { + disableUpdates = true; + + if (IsAutoUpdatingEnabled) + { + SetColorToSliderValues(); + UpdateBackground(); + } + + UpdatePseudoClasses(); + + disableUpdates = false; + } else if (change.Property == HsvColorProperty) { disableUpdates = true; diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs index eddbeaf112..c56811b8a3 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs @@ -7,7 +7,6 @@ namespace Avalonia.Controls { public partial class ColorView { - // SelectedColorModel ActiveColorModel? // SelectedTab /// @@ -19,6 +18,14 @@ namespace Avalonia.Controls Colors.White, defaultBindingMode: BindingMode.TwoWay); + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorModelProperty = + AvaloniaProperty.Register( + nameof(ColorModel), + ColorModel.Rgba); + /// /// Defines the property. /// @@ -203,6 +210,17 @@ namespace Avalonia.Controls set => SetValue(ColorProperty, value); } + /// + /// + /// This property is only applicable to the components tab. + /// The spectrum tab must always be in HSV and the palette tab is pre-defined colors. + /// + public ColorModel ColorModel + { + get => GetValue(ColorModelProperty); + set => SetValue(ColorModelProperty, value); + } + /// public ColorSpectrumComponents ColorSpectrumComponents { diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index 5d3642a3b1..b037d14956 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -1,5 +1,6 @@  + @@ -156,6 +158,11 @@ + + + + + + + + + + + + + + + + + + + + Grid.Column="2"> @@ -260,12 +333,17 @@ BorderThickness="1,1,0,1" CornerRadius="4,0,0,4" VerticalAlignment="Center"> - + + + + - + + + + - + + + + Date: Sat, 28 May 2022 12:08:28 -0400 Subject: [PATCH 037/389] Improve comments --- .../Converters/AccentColorConverter.cs | 4 +++- .../Converters/ContrastBrushConverter.cs | 3 +++ .../Converters/ThirdComponentConverter.cs | 4 +++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs index 4d05222e31..2c8e09adc9 100644 --- a/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs +++ b/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs @@ -7,8 +7,10 @@ namespace Avalonia.Controls.Primitives.Converters { /// /// Creates an accent color for a given base color value and step parameter. - /// This is a highly-specialized converter for the color picker. /// + /// + /// This is a highly-specialized converter for the color picker. + /// public class AccentColorConverter : IValueConverter { /// diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ContrastBrushConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ContrastBrushConverter.cs index 574f23dfae..8b66b1a4e5 100644 --- a/src/Avalonia.Controls.ColorPicker/Converters/ContrastBrushConverter.cs +++ b/src/Avalonia.Controls.ColorPicker/Converters/ContrastBrushConverter.cs @@ -10,6 +10,9 @@ namespace Avalonia.Controls.Primitives.Converters /// Gets a , either black or white, depending on the luminance of the supplied color. /// A default color supplied in the converter parameter may be returned if alpha is below the set threshold. /// + /// + /// This is a highly-specialized converter for the color picker. + /// public class ContrastBrushConverter : IValueConverter { private ToColorConverter toColorConverter = new ToColorConverter(); diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs index 220a993f99..11e33c74f0 100644 --- a/src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs +++ b/src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs @@ -7,8 +7,10 @@ namespace Avalonia.Controls.Primitives.Converters /// /// Gets the third corresponding with a given /// that represents the other two components. - /// This is a highly-specialized converter for the color picker. /// + /// + /// This is a highly-specialized converter for the color picker. + /// public class ThirdComponentConverter : IValueConverter { /// From 4a267b69d61dbe8d784a3c7046c3af652632b9c8 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 28 May 2022 12:15:04 -0400 Subject: [PATCH 038/389] Set MaxHue of ColorSlider to the same value as ColorSpectrum --- .../ColorSlider/ColorSlider.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index 78a796e93a..4c7df0fda7 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -20,7 +20,15 @@ namespace Avalonia.Controls.Primitives /// public event EventHandler? ColorChanged; - private const double MaxHue = 359.99999999999999999; // 17 decimal places + /// + /// Defines the maximum hue component value + /// (other components are always 0..100 or 0.255). + /// + /// + /// This should match the default property. + /// + private const double MaxHue = 359; + private bool disableUpdates = false; /// From 36f85325bc7242487f20eeb3fb2fe3142aa0872a Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 28 May 2022 12:28:23 -0400 Subject: [PATCH 039/389] Remove EnumValueEqualsConverter replaced by EnumToBooleanConverter --- .../Themes/Default/ColorSpectrum.xaml | 15 ++--- .../Themes/Fluent/ColorSpectrum.xaml | 15 ++--- .../Converters/EnumValueEqualsConverter.cs | 62 ------------------- 3 files changed, 16 insertions(+), 76 deletions(-) delete mode 100644 src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml index 78e6da8aa3..891e040e9f 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml @@ -1,10 +1,11 @@  - + @@ -24,26 +25,26 @@ IsHitTestVisible="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" - IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumValueEquals}, ConverterParameter='Box'}" + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolean}, ConverterParameter={x:Static controls:ColorSpectrumShape.Box}}" RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}" RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolean}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolean}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolean}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml index ac8e2a9c06..779f228b97 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml @@ -1,10 +1,11 @@  - + @@ -24,26 +25,26 @@ IsHitTestVisible="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" - IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumValueEquals}, ConverterParameter='Box'}" + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolean}, ConverterParameter={x:Static controls:ColorSpectrumShape.Box}}" RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}" RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolean}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolean}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolean}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> diff --git a/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs b/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs deleted file mode 100644 index abd0fe1dfd..0000000000 --- a/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Globalization; -using Avalonia.Data.Converters; - -namespace Avalonia.Controls.Converters -{ - /// - /// Converter that checks if an enum value is equal to the given parameter enum value. - /// - public class EnumValueEqualsConverter : IValueConverter - { - /// - public object? Convert( - object? value, - Type targetType, - object? parameter, - CultureInfo culture) - { - // Note: Unlike string comparisons, null/empty is not supported - // Both 'value' and 'parameter' must exist and if both are missing they are not considered equal - if (value != null && - parameter != null) - { - Type type = value.GetType(); - - if (type.IsEnum) - { - var valueStr = value?.ToString(); - var paramStr = parameter?.ToString(); - - if (string.Equals(valueStr, paramStr, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - /* - // TODO: When .net Standard 2.0 is no longer supported the code can be changed to below - // This is a little more type safe - if (type.IsEnum && - Enum.TryParse(type, value?.ToString(), true, out object? valueEnum) && - Enum.TryParse(type, parameter?.ToString(), true, out object? paramEnum)) - { - return valueEnum == paramEnum; - } - */ - } - - return false; - } - - /// - public object? ConvertBack( - object? value, - Type targetType, - object? parameter, - CultureInfo culture) - { - throw new System.NotImplementedException(); - } - } -} From 043b56c7da836dc6e8976c91957ce82626ca5b26 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 28 May 2022 13:46:40 -0400 Subject: [PATCH 040/389] Remove IsAutoUpdatingEnabled property from ColorSlider This was unused and is an unnecessary complexity --- .../ColorSlider/ColorSlider.Properties.cs | 21 ------------- .../ColorSlider/ColorSlider.cs | 31 ++++++------------- 2 files changed, 9 insertions(+), 43 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs index 31bd296288..b1be794794 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs @@ -48,14 +48,6 @@ namespace Avalonia.Controls.Primitives nameof(IsAlphaMaxForced), true); - /// - /// Defines the property. - /// - public static readonly StyledProperty IsAutoUpdatingEnabledProperty = - AvaloniaProperty.Register( - nameof(IsAutoUpdatingEnabled), - true); - /// /// Defines the property. /// @@ -119,19 +111,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(IsAlphaMaxForcedProperty, value); } - /// - /// Gets or sets a value indicating whether automatic background and foreground updates will be - /// calculated when the set color changes. - /// - /// - /// This can be disabled for performance reasons when working with multiple sliders. - /// - public bool IsAutoUpdatingEnabled - { - get => GetValue(IsAutoUpdatingEnabledProperty); - set => SetValue(IsAutoUpdatingEnabledProperty, value); - } - /// /// Gets or sets a value indicating whether the saturation and value components are always forced to maximum values /// when using the HSVA color model. Only component values other than will be changed. diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index 4c7df0fda7..641516c474 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -327,13 +327,10 @@ namespace Avalonia.Controls.Primitives HsvColor = Color.ToHsv(); - if (IsAutoUpdatingEnabled) - { - SetColorToSliderValues(); - UpdateBackground(); - } - + SetColorToSliderValues(); + UpdateBackground(); UpdatePseudoClasses(); + OnColorChanged(new ColorChangedEventArgs( change.GetOldValue(), change.GetNewValue())); @@ -344,12 +341,8 @@ namespace Avalonia.Controls.Primitives { disableUpdates = true; - if (IsAutoUpdatingEnabled) - { - SetColorToSliderValues(); - UpdateBackground(); - } - + SetColorToSliderValues(); + UpdateBackground(); UpdatePseudoClasses(); disableUpdates = false; @@ -360,13 +353,10 @@ namespace Avalonia.Controls.Primitives Color = HsvColor.ToRgb(); - if (IsAutoUpdatingEnabled) - { - SetColorToSliderValues(); - UpdateBackground(); - } - + SetColorToSliderValues(); + UpdateBackground(); UpdatePseudoClasses(); + OnColorChanged(new ColorChangedEventArgs( change.GetOldValue().ToRgb(), change.GetNewValue().ToRgb())); @@ -375,10 +365,7 @@ namespace Avalonia.Controls.Primitives } else if (change.Property == BoundsProperty) { - if (IsAutoUpdatingEnabled) - { - UpdateBackground(); - } + UpdateBackground(); } else if (change.Property == ValueProperty || change.Property == MinimumProperty || From 8ef440ceda8237a746c409a3eaa9e230b820cdb9 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 28 May 2022 14:24:00 -0400 Subject: [PATCH 041/389] Finish ColorView design/style --- .../Themes/Fluent/ColorView.xaml | 215 +++++++++++------- 1 file changed, 136 insertions(+), 79 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index b037d14956..09feeb38f9 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -14,35 +14,125 @@ + 48 + + + M3 2C3.27614 2 3.5 2.22386 3.5 2.5V5.5C3.5 5.77614 3.72386 6 4 6H16C16.2761 6 16.5 5.77614 + 16.5 5.5V2.5C16.5 2.22386 16.7239 2 17 2C17.2761 2 17.5 2.22386 17.5 2.5V5.5C17.5 6.32843 + 16.8284 7 16 7H15.809L12.2236 14.1708C12.0615 14.4951 11.7914 14.7431 11.4695 + 14.8802C11.4905 15.0808 11.5 15.2891 11.5 15.5C11.5 16.0818 11.4278 16.6623 11.2268 + 17.1165C11.019 17.5862 10.6266 18 10 18C9.37343 18 8.98105 17.5862 8.77323 17.1165C8.57222 + 16.6623 8.5 16.0818 8.5 15.5C8.5 15.2891 8.50952 15.0808 8.53051 14.8802C8.20863 14.7431 + 7.93851 14.4951 7.77639 14.1708L4.19098 7H4C3.17157 7 2.5 6.32843 2.5 5.5V2.5C2.5 2.22386 + 2.72386 2 3 2ZM9.11803 14H10.882C11.0714 14 11.2445 13.893 11.3292 13.7236L14.691 + 7H5.30902L8.67082 13.7236C8.75552 13.893 8.92865 14 9.11803 14ZM9.52346 15C9.50787 15.1549 + 9.5 15.3225 9.5 15.5C9.5 16.0228 9.56841 16.4423 9.6877 16.7119C9.8002 16.9661 9.90782 17 + 10 17C10.0922 17 10.1998 16.9661 10.3123 16.7119C10.4316 16.4423 10.5 16.0228 10.5 + 15.5C10.5 15.3225 10.4921 15.1549 10.4765 15H9.52346Z + + + + M9.75003 6.5C10.1642 6.5 10.5 6.16421 10.5 5.75C10.5 5.33579 10.1642 5 9.75003 5C9.33582 + 5 9.00003 5.33579 9.00003 5.75C9.00003 6.16421 9.33582 6.5 9.75003 6.5ZM12.75 7.5C13.1642 + 7.5 13.5 7.16421 13.5 6.75C13.5 6.33579 13.1642 6 12.75 6C12.3358 6 12 6.33579 12 6.75C12 + 7.16421 12.3358 7.5 12.75 7.5ZM15.25 9C15.25 9.41421 14.9142 9.75 14.5 9.75C14.0858 9.75 + 13.75 9.41421 13.75 9C13.75 8.58579 14.0858 8.25 14.5 8.25C14.9142 8.25 15.25 8.58579 + 15.25 9ZM14.5 12.75C14.9142 12.75 15.25 12.4142 15.25 12C15.25 11.5858 14.9142 11.25 14.5 + 11.25C14.0858 11.25 13.75 11.5858 13.75 12C13.75 12.4142 14.0858 12.75 14.5 12.75ZM13.25 + 14C13.25 14.4142 12.9142 14.75 12.5 14.75C12.0858 14.75 11.75 14.4142 11.75 14C11.75 + 13.5858 12.0858 13.25 12.5 13.25C12.9142 13.25 13.25 13.5858 13.25 14ZM13.6972 + 2.99169C10.9426 1.57663 8.1432 1.7124 5.77007 3.16636C4.55909 3.9083 3.25331 5.46925 + 2.51605 7.05899C2.14542 7.85816 1.89915 8.70492 1.90238 9.49318C1.90566 10.2941 2.16983 + 11.0587 2.84039 11.6053C3.45058 12.1026 3.98165 12.353 4.49574 12.3784C5.01375 12.404 + 5.41804 12.1942 5.73429 12.0076C5.80382 11.9666 5.86891 11.927 5.93113 11.8892C6.17332 + 11.7421 6.37205 11.6214 6.62049 11.5426C6.90191 11.4534 7.2582 11.4205 7.77579 + 11.5787C7.96661 11.637 8.08161 11.7235 8.16212 11.8229C8.24792 11.9289 8.31662 12.0774 + 8.36788 12.2886C8.41955 12.5016 8.44767 12.7527 8.46868 13.0491C8.47651 13.1594 8.48379 + 13.2855 8.49142 13.4176C8.50252 13.6098 8.51437 13.8149 8.52974 14.0037C8.58435 14.6744 + 8.69971 15.4401 9.10362 16.1357C9.51764 16.8488 10.2047 17.439 11.307 17.8158C12.9093 + 18.3636 14.3731 17.9191 15.5126 17.0169C16.6391 16.125 17.4691 14.7761 17.8842 + 13.4272C19.1991 9.15377 17.6728 5.03394 13.6972 2.99169ZM6.29249 4.01905C8.35686 2.75426 + 10.7844 2.61959 13.2403 3.88119C16.7473 5.68275 18.1135 9.28161 16.9284 13.1332C16.5624 + 14.3227 15.8338 15.4871 14.8919 16.2329C13.963 16.9684 12.8486 17.286 11.6305 + 16.8696C10.7269 16.5607 10.2467 16.1129 9.96842 15.6336C9.68001 15.1369 9.57799 14.5556 + 9.52644 13.9225C9.51101 13.733 9.50132 13.5621 9.49147 13.3884C9.48399 13.2564 9.47642 + 13.1229 9.46618 12.9783C9.44424 12.669 9.41175 12.3499 9.33968 12.0529C9.26719 11.7541 + 9.14902 11.4527 8.93935 11.1937C8.72439 10.9282 8.43532 10.7346 8.06801 10.6223C7.36648 + 10.408 6.80266 10.4359 6.31839 10.5893C5.94331 10.7082 5.62016 10.9061 5.37179 + 11.0582C5.31992 11.0899 5.2713 11.1197 5.22616 11.1463C4.94094 11.3146 4.75357 11.39 + 4.54514 11.3796C4.33279 11.3691 4.00262 11.2625 3.47218 10.8301C3.0866 10.5158 2.90473 + 10.0668 2.90237 9.48908C2.89995 8.89865 3.08843 8.20165 3.42324 7.47971C4.09686 6.0272 + 5.28471 4.63649 6.29249 4.01905Z + + + + M14.95 5C14.7184 3.85888 13.7095 3 12.5 3C11.2905 3 10.2816 3.85888 10.05 5H2.5C2.22386 + 5 2 5.22386 2 5.5C2 5.77614 2.22386 6 2.5 6H10.05C10.2816 7.14112 11.2905 8 12.5 8C13.7297 + 8 14.752 7.11217 14.961 5.94254C14.9575 5.96177 14.9539 5.98093 14.95 6H17.5C17.7761 6 18 + 5.77614 18 5.5C18 5.22386 17.7761 5 17.5 5H14.95ZM12.5 7C11.6716 7 11 6.32843 11 5.5C11 + 4.67157 11.6716 4 12.5 4C13.3284 4 14 4.67157 14 5.5C14 6.32843 13.3284 7 12.5 7ZM9.94999 + 14C9.71836 12.8589 8.70948 12 7.5 12C6.29052 12 5.28164 12.8589 5.05001 14H2.5C2.22386 + 14 2 14.2239 2 14.5C2 14.7761 2.22386 15 2.5 15H5.05001C5.28164 16.1411 6.29052 17 7.5 + 17C8.70948 17 9.71836 16.1411 9.94999 15H17.5C17.7761 15 18 14.7761 18 14.5C18 14.2239 + 17.7761 14 17.5 14H9.94999ZM7.5 16C6.67157 16 6 15.3284 6 14.5C6 13.6716 6.67157 13 7.5 + 13C8.32843 13 9 13.6716 9 14.5C9 15.3284 8.32843 16 7.5 16Z + + - - + + + + RowDefinitions="Auto,*" + Margin="12"> - - + + + + SelectedItem="{Binding Color, ElementName=ColorSpectrum}" + UseLayoutRounding="False" + Margin="12"> @@ -281,17 +343,11 @@ @@ -497,7 +553,8 @@ + HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}" + Margin="12,0,12,12" /> From 12a8ecb9239f315767448bc5d434b7652b7987f8 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 28 May 2022 15:17:40 -0400 Subject: [PATCH 042/389] Implement the hex input TextBox in ColorView --- .../ColorView/ColorView.cs | 85 +++++++++++++++++-- .../Themes/Fluent/ColorView.xaml | 19 ++--- 2 files changed, 88 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index 76e3cfe3e1..0363d9c182 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -1,9 +1,8 @@ using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Globalization; +using Avalonia.Controls.Converters; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Media; @@ -12,6 +11,7 @@ namespace Avalonia.Controls /// /// Presents a color for user editing using a spectrum, palette and component sliders. /// + [TemplatePart("PART_HexTextBox", typeof(TextBox))] public partial class ColorView : TemplatedControl { /// @@ -19,20 +19,70 @@ namespace Avalonia.Controls /// public event EventHandler? ColorChanged; - private bool disableUpdates = false; + // XAML template parts + private TextBox? _hexTextBox; private ObservableCollection _customPaletteColors = new ObservableCollection(); + private ColorToHexConverter colorToHexConverter = new ColorToHexConverter(); + private bool disableUpdates = false; /// /// Initializes a new instance of the class. /// public ColorView() : base() { + this.CustomPalette = new FluentColorPalette(); + } + + /// + /// Gets the value of the hex TextBox and sets it as the current . + /// If invalid, the TextBox hex text will revert back to the last valid color. + /// + private void GetColorFromHexTextBox() + { + if (_hexTextBox != null) + { + var convertedColor = colorToHexConverter.ConvertBack(_hexTextBox.Text, typeof(Color), null, CultureInfo.CurrentCulture); + + if (convertedColor is Color color) + { + Color = color; + } + + // Re-apply the hex value + // This ensure the hex color value is always valid and formatted correctly + _hexTextBox.Text = colorToHexConverter.Convert(Color, typeof(string), null, CultureInfo.CurrentCulture) as string; + } + } + + /// + /// Sets the current to the hex TextBox. + /// + private void SetColorToHexTextBox() + { + if (_hexTextBox != null) + { + _hexTextBox.Text = colorToHexConverter.Convert(Color, typeof(string), null, CultureInfo.CurrentCulture) as string; + } } + /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { - this.CustomPalette = new FluentColorPalette(); + if (_hexTextBox != null) + { + _hexTextBox.KeyDown -= HexTextBox_KeyDown; + _hexTextBox.LostFocus -= HexTextBox_LostFocus; + } + + _hexTextBox = e.NameScope.Find("PART_HexTextBox"); + SetColorToHexTextBox(); + + if (_hexTextBox != null) + { + _hexTextBox.KeyDown += HexTextBox_KeyDown; + _hexTextBox.LostFocus += HexTextBox_LostFocus; + } base.OnApplyTemplate(e); } @@ -52,6 +102,7 @@ namespace Avalonia.Controls disableUpdates = true; HsvColor = Color.ToHsv(); + SetColorToHexTextBox(); OnColorChanged(new ColorChangedEventArgs( change.GetOldValue(), @@ -64,6 +115,7 @@ namespace Avalonia.Controls disableUpdates = true; Color = HsvColor.ToRgb(); + SetColorToHexTextBox(); OnColorChanged(new ColorChangedEventArgs( change.GetOldValue().ToRgb(), @@ -103,5 +155,26 @@ namespace Avalonia.Controls { ColorChanged?.Invoke(this, e); } + + /// + /// Event handler for when a key is pressed within the Hex RGB value TextBox. + /// This is used to trigger re-evaluation of the color based on the TextBox value. + /// + private void HexTextBox_KeyDown(object? sender, Input.KeyEventArgs e) + { + if (e.Key == Input.Key.Enter) + { + GetColorFromHexTextBox(); + } + } + + /// + /// Event handler for when the Hex RGB value TextBox looses focus. + /// This is used to trigger re-evaluation of the color based on the TextBox value. + /// + private void HexTextBox_LostFocus(object? sender, Interactivity.RoutedEventArgs e) + { + GetColorFromHexTextBox(); + } } } diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index 09feeb38f9..9800b1b3a5 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -152,8 +152,7 @@ Grid.Row="0" Grid.RowSpan="2" Components="{TemplateBinding ColorSpectrumComponents}" - Color="{TemplateBinding Color}" - HsvColor="{TemplateBinding HsvColor}" + HsvColor="{Binding $parent[ColorView].HsvColor}" MinHue="{TemplateBinding MinHue}" MaxHue="{TemplateBinding MaxHue}" MinSaturation="{TemplateBinding MinSaturation}" @@ -188,7 +187,7 @@ @@ -369,12 +368,12 @@ HorizontalAlignment="Center" VerticalAlignment="Center" /> - + @@ -419,7 +418,7 @@ Orientation="Horizontal" ColorComponent="Component1" ColorModel="{TemplateBinding ColorModel, Mode=OneWay}" - HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}" + HsvColor="{Binding $parent[ColorView].HsvColor}" Margin="12,0,0,0" HorizontalAlignment="Stretch" VerticalAlignment="Center" /> @@ -462,7 +461,7 @@ Orientation="Horizontal" ColorComponent="Component2" ColorModel="{TemplateBinding ColorModel, Mode=OneWay}" - HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}" + HsvColor="{Binding $parent[ColorView].HsvColor}" Margin="12,0,0,0" HorizontalAlignment="Stretch" VerticalAlignment="Center" /> @@ -505,7 +504,7 @@ Orientation="Horizontal" ColorComponent="Component3" ColorModel="{TemplateBinding ColorModel, Mode=OneWay}" - HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}" + HsvColor="{Binding $parent[ColorView].HsvColor}" Margin="12,0,0,0" HorizontalAlignment="Stretch" VerticalAlignment="Center" /> @@ -543,7 +542,7 @@ Orientation="Horizontal" ColorComponent="Alpha" ColorModel="{TemplateBinding ColorModel, Mode=OneWay}" - HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}" + HsvColor="{Binding $parent[ColorView].HsvColor}" Margin="12,0,0,0" HorizontalAlignment="Stretch" VerticalAlignment="Center" /> @@ -553,7 +552,7 @@ From a5b3bb9cb6c80b9d83c38ea372c9a64cc663acb6 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 28 May 2022 15:58:48 -0400 Subject: [PATCH 043/389] Implement ColorSlider component value rounding --- .../ColorSlider/ColorSlider.Properties.cs | 21 +++++++++++ .../ColorSlider/ColorSlider.cs | 35 +++++++++++++++++-- .../Themes/Fluent/ColorView.xaml | 12 +++++++ 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs index b1be794794..e2a34a7f90 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs @@ -48,6 +48,14 @@ namespace Avalonia.Controls.Primitives nameof(IsAlphaMaxForced), true); + /// + /// Defines the property. + /// + public static readonly StyledProperty IsRoundingEnabledProperty = + AvaloniaProperty.Register( + nameof(IsRoundingEnabled), + false); + /// /// Defines the property. /// @@ -111,6 +119,19 @@ namespace Avalonia.Controls.Primitives set => SetValue(IsAlphaMaxForcedProperty, value); } + /// + /// Gets or sets a value indicating whether rounding of color component values is enabled. + /// + /// + /// This is applicable for the HSV color model only. The struct uses double + /// values while the struct uses byte. Only double types need rounding. + /// + public bool IsRoundingEnabled + { + get => GetValue(IsRoundingEnabledProperty); + set => SetValue(IsRoundingEnabledProperty, value); + } + /// /// Gets or sets a value indicating whether the saturation and value components are always forced to maximum values /// when using the HSVA color model. Only component values other than will be changed. diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index 641516c474..957e5e7b77 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -115,6 +115,21 @@ namespace Avalonia.Controls.Primitives } } + /// + /// Rounds the component values of the given . + /// This is useful for user-display and to ensure a color matches user selection exactly. + /// + /// The to round component values for. + /// A new with rounded component values. + private HsvColor RoundComponentValues(HsvColor hsvColor) + { + return new HsvColor( + Math.Round(hsvColor.A, 2, MidpointRounding.AwayFromZero), + Math.Round(hsvColor.H, 0, MidpointRounding.AwayFromZero), + Math.Round(hsvColor.S, 2, MidpointRounding.AwayFromZero), + Math.Round(hsvColor.V, 2, MidpointRounding.AwayFromZero)); + } + /// /// Updates the slider property values by applying the current color. /// @@ -130,6 +145,11 @@ namespace Avalonia.Controls.Primitives if (ColorModel == ColorModel.Hsva) { + if (IsRoundingEnabled) + { + hsvColor = RoundComponentValues(hsvColor); + } + // Note: Components converted into a usable range for the user switch (component) { @@ -222,7 +242,7 @@ namespace Avalonia.Controls.Primitives } } - return (hsvColor.ToRgb(), hsvColor); + rgbColor = hsvColor.ToRgb(); } else { @@ -244,8 +264,15 @@ namespace Avalonia.Controls.Primitives break; } - return (rgbColor, rgbColor.ToHsv()); + hsvColor = rgbColor.ToHsv(); } + + if (IsRoundingEnabled) + { + hsvColor = RoundComponentValues(hsvColor); + } + + return (rgbColor, hsvColor); } /// @@ -363,6 +390,10 @@ namespace Avalonia.Controls.Primitives disableUpdates = false; } + else if (change.Property == IsRoundingEnabledProperty) + { + SetColorToSliderValues(); + } else if (change.Property == BoundsProperty) { UpdateBackground(); diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index 9800b1b3a5..1edc2768a5 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -416,6 +416,9 @@ Grid.Column="2" Grid.Row="2" Orientation="Horizontal" + IsRoundingEnabled="True" + IsSnapToTickEnabled="True" + TickFrequency="1" ColorComponent="Component1" ColorModel="{TemplateBinding ColorModel, Mode=OneWay}" HsvColor="{Binding $parent[ColorView].HsvColor}" @@ -459,6 +462,9 @@ Grid.Column="2" Grid.Row="3" Orientation="Horizontal" + IsRoundingEnabled="True" + IsSnapToTickEnabled="True" + TickFrequency="1" ColorComponent="Component2" ColorModel="{TemplateBinding ColorModel, Mode=OneWay}" HsvColor="{Binding $parent[ColorView].HsvColor}" @@ -502,6 +508,9 @@ Grid.Column="2" Grid.Row="4" Orientation="Horizontal" + IsRoundingEnabled="True" + IsSnapToTickEnabled="True" + TickFrequency="1" ColorComponent="Component3" ColorModel="{TemplateBinding ColorModel, Mode=OneWay}" HsvColor="{Binding $parent[ColorView].HsvColor}" @@ -540,6 +549,9 @@ Grid.Column="2" Grid.Row="5" Orientation="Horizontal" + IsRoundingEnabled="True" + IsSnapToTickEnabled="True" + TickFrequency="1" ColorComponent="Alpha" ColorModel="{TemplateBinding ColorModel, Mode=OneWay}" HsvColor="{Binding $parent[ColorView].HsvColor}" From 0824c874fc7c5bbc8b98f758e47f5e6194f7b595 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 28 May 2022 16:42:58 -0400 Subject: [PATCH 044/389] Add initial ColorPicker control --- .../ControlCatalog/Pages/ColorPickerPage.xaml | 5 +- .../ColorPicker/ColorPicker.cs | 15 ++++ .../Themes/Fluent/ColorPicker.xaml | 71 +++++++++++++++++++ .../Themes/Fluent/Fluent.xaml | 1 + 4 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs create mode 100644 src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml b/samples/ControlCatalog/Pages/ColorPickerPage.xaml index 47a407821c..eca52e796a 100644 --- a/samples/ControlCatalog/Pages/ColorPickerPage.xaml +++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml @@ -13,7 +13,10 @@ - + + diff --git a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs new file mode 100644 index 0000000000..cb84c77d20 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Avalonia.Controls +{ + /// + /// + /// + public class ColorPicker : ColorView + { + } +} diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml new file mode 100644 index 0000000000..3fa3ab5ead --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml index c55766e07c..03e8238a44 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml @@ -26,6 +26,7 @@ + From 5cd0ea68053394bb85f7db96c65e7203448dd1cf Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 30 May 2022 20:39:17 -0400 Subject: [PATCH 045/389] Complete initial ColorPicker --- .../ColorPicker/ColorPicker.cs | 3 +- .../Themes/Fluent/ColorPicker.xaml | 31 +++++++++++++------ .../Themes/Fluent/ColorView.xaml | 16 +++++----- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs index cb84c77d20..140a24d6a1 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs @@ -7,7 +7,8 @@ using System.Threading.Tasks; namespace Avalonia.Controls { /// - /// + /// Presents a color for user editing using a spectrum, palette and component sliders within a drop down. + /// Editing is available when the drop down flyout is opened; otherwise, only the preview color is shown. /// public class ColorPicker : ColorView { diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml index 3fa3ab5ead..a4f52f111c 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -2,10 +2,11 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:Avalonia.Controls" xmlns:converters="using:Avalonia.Controls.Converters" - x:CompileBindings="True"> + x:CompileBindings="False"> + - + @@ -150,7 +150,8 @@ HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}" HorizontalAlignment="Center" VerticalAlignment="Stretch" - Margin="0,0,12,0" /> + Margin="0,0,12,0" + IsVisible="{TemplateBinding IsColorSpectrumSliderVisible}"/> - + @@ -254,7 +255,7 @@ - + @@ -590,7 +591,7 @@ From adfac7b69dbcdc79a676b04aaebe1db4d4d63b40 Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 2 Jun 2022 01:22:39 -0400 Subject: [PATCH 049/389] Implement tab selection validation and automatic width --- .../ColorView/ColorView.cs | 93 ++++++++++++++++++- .../Themes/Fluent/ColorView.xaml | 23 +++-- 2 files changed, 105 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index 0363d9c182..96de734cc7 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -5,6 +5,7 @@ using Avalonia.Controls.Converters; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Media; +using Avalonia.Threading; namespace Avalonia.Controls { @@ -12,6 +13,7 @@ namespace Avalonia.Controls /// Presents a color for user editing using a spectrum, palette and component sliders. /// [TemplatePart("PART_HexTextBox", typeof(TextBox))] + [TemplatePart("PART_TabControl", typeof(TabControl))] public partial class ColorView : TemplatedControl { /// @@ -20,7 +22,8 @@ namespace Avalonia.Controls public event EventHandler? ColorChanged; // XAML template parts - private TextBox? _hexTextBox; + private TextBox? _hexTextBox; + private TabControl? _tabControl; private ObservableCollection _customPaletteColors = new ObservableCollection(); private ColorToHexConverter colorToHexConverter = new ColorToHexConverter(); @@ -31,7 +34,7 @@ namespace Avalonia.Controls /// public ColorView() : base() { - this.CustomPalette = new FluentColorPalette(); + CustomPalette = new FluentColorPalette(); } /// @@ -66,6 +69,77 @@ namespace Avalonia.Controls } } + /// + /// Validates the selected subview/tab taking into account the visibility of each subview/tab + /// as well as the current selection. + /// + private void ValidateSelectedTab() + { + if (_tabControl != null && + _tabControl.Items != null) + { + // Determine if any item is visible + bool isAnyItemVisible = false; + foreach (var item in _tabControl.Items) + { + if (item is Control control && + control.IsVisible) + { + isAnyItemVisible = true; + break; + } + } + + if (isAnyItemVisible) + { + object? selectedItem = null; + + if (_tabControl.SelectedItem == null && + _tabControl.ItemCount > 0) + { + // As a failsafe, forcefully select the first item + foreach (var item in _tabControl.Items) + { + selectedItem = item; + break; + } + } + else + { + selectedItem = _tabControl.SelectedItem; + } + + if (selectedItem is Control selectedControl && + selectedControl.IsVisible == false) + { + // Select the first visible item instead + foreach (var item in _tabControl.Items) + { + if (item is Control control && + control.IsVisible) + { + selectedItem = item; + break; + } + } + } + + _tabControl.SelectedItem = selectedItem; + _tabControl.IsVisible = true; + } + else + { + // Special case when all items are hidden + // If TabControl ever properly supports no selected item / + // all items hidden this can be removed + _tabControl.SelectedItem = null; + _tabControl.IsVisible = false; + } + } + + return; + } + /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { @@ -76,6 +150,8 @@ namespace Avalonia.Controls } _hexTextBox = e.NameScope.Find("PART_HexTextBox"); + _tabControl = e.NameScope.Find("PART_TabControl"); + SetColorToHexTextBox(); if (_hexTextBox != null) @@ -85,6 +161,7 @@ namespace Avalonia.Controls } base.OnApplyTemplate(e); + ValidateSelectedTab(); } /// @@ -143,6 +220,18 @@ namespace Avalonia.Controls } } } + else if (change.Property == IsColorComponentsVisibleProperty || + change.Property == IsColorPaletteVisibleProperty || + change.Property == IsColorSpectrumVisibleProperty) + { + // When the property changed notification is received here the visibility + // of individual tab items has not yet been updated though the bindings. + // Therefore, the validation is delayed until after bindings update. + Dispatcher.UIThread.Post(() => + { + ValidateSelectedTab(); + }, DispatcherPriority.Background); + } base.OnPropertyChanged(change); } diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index afc8682a66..420e3b2ae9 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -109,22 +109,29 @@ BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}" BorderThickness="0,1,0,0" />--> - 0,0,0,0 - - + @@ -181,8 +188,7 @@ - + @@ -257,8 +263,7 @@ - + From c02439aaaf5c1984f8aae75c777173c775c89257 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 30 May 2022 17:10:09 +0200 Subject: [PATCH 050/389] Refactored most of Style into StyleBase. Ready for `ControlTheme` class, which is a style without a selector. --- src/Avalonia.Base/Styling/Style.cs | 148 ++------------------- src/Avalonia.Base/Styling/StyleBase.cs | 137 +++++++++++++++++++ src/Avalonia.Base/Styling/StyleChildren.cs | 10 +- 3 files changed, 151 insertions(+), 144 deletions(-) create mode 100644 src/Avalonia.Base/Styling/StyleBase.cs diff --git a/src/Avalonia.Base/Styling/Style.cs b/src/Avalonia.Base/Styling/Style.cs index 000e588bad..c85c85fe21 100644 --- a/src/Avalonia.Base/Styling/Style.cs +++ b/src/Avalonia.Base/Styling/Style.cs @@ -1,23 +1,12 @@ using System; -using System.Collections.Generic; -using Avalonia.Animation; -using Avalonia.Controls; -using Avalonia.Metadata; namespace Avalonia.Styling { /// /// Defines a style. /// - public class Style : AvaloniaObject, IStyle, IResourceProvider + public class Style : StyleBase { - private IResourceHost? _owner; - private StyleChildren? _children; - private IResourceDictionary? _resources; - private List? _setters; - private List? _animations; - private StyleCache? _childCache; - /// /// Initializes a new instance of the class. /// @@ -34,114 +23,11 @@ namespace Avalonia.Styling Selector = selector(null); } - /// - /// Gets the children of the style. - /// - public IList Children => _children ??= new(this); - - /// - /// Gets the or Application that hosts the style. - /// - public IResourceHost? Owner - { - get => _owner; - private set - { - if (_owner != value) - { - _owner = value; - OwnerChanged?.Invoke(this, EventArgs.Empty); - } - } - } - - /// - /// Gets the parent style if this style is hosted in a collection. - /// - public Style? Parent { get; private set; } - - /// - /// Gets or sets a dictionary of style resources. - /// - public IResourceDictionary Resources - { - get => _resources ?? (Resources = new ResourceDictionary()); - set - { - value = value ?? throw new ArgumentNullException(nameof(value)); - - var hadResources = _resources?.HasResources ?? false; - - _resources = value; - - if (Owner is object) - { - _resources.AddOwner(Owner); - - if (hadResources || _resources.HasResources) - { - Owner.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); - } - } - } - } - /// /// Gets or sets the style's selector. /// public Selector? Selector { get; set; } - /// - /// Gets the style's setters. - /// - public IList Setters => _setters ??= new List(); - - /// - /// Gets the style's animations. - /// - public IList Animations => _animations ??= new List(); - - bool IResourceNode.HasResources => _resources?.Count > 0; - IReadOnlyList IStyle.Children => (IReadOnlyList?)_children ?? Array.Empty(); - - public event EventHandler? OwnerChanged; - - public void Add(ISetter setter) => Setters.Add(setter); - public void Add(IStyle style) => Children.Add(style); - - public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) - { - target = target ?? throw new ArgumentNullException(nameof(target)); - - var match = Selector is object ? Selector.Match(target, Parent) : - target == host ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance; - - if (match.IsMatch && (_setters is object || _animations is object)) - { - var instance = new StyleInstance(this, target, _setters, _animations, match.Activator); - target.StyleApplied(instance); - instance.Start(); - } - - var result = match.Result; - - if (_children is not null) - { - _childCache ??= new StyleCache(); - var childResult = _childCache.TryAttach(_children, target, host); - if (childResult > result) - result = childResult; - } - - return result; - } - - public bool TryGetResource(object key, out object? result) - { - result = null; - return _resources?.TryGetResource(key, out result) ?? false; - } - /// /// Returns a string representation of the style. /// @@ -158,33 +44,17 @@ namespace Avalonia.Styling } } - void IResourceProvider.AddOwner(IResourceHost owner) - { - owner = owner ?? throw new ArgumentNullException(nameof(owner)); - - if (Owner != null) - { - throw new InvalidOperationException("The Style already has a parent."); - } - - Owner = owner; - _resources?.AddOwner(owner); - } - - void IResourceProvider.RemoveOwner(IResourceHost owner) + protected override SelectorMatch Matches(IStyleable target, IStyleHost? host) { - owner = owner ?? throw new ArgumentNullException(nameof(owner)); - - if (Owner == owner) - { - Owner = null; - _resources?.RemoveOwner(owner); - } + return Selector?.Match(target, Parent) ?? + (target == host ? + SelectorMatch.AlwaysThisInstance : + SelectorMatch.NeverThisInstance); } - internal void SetParent(Style? parent) + internal override void SetParent(StyleBase? parent) { - if (parent?.Selector is not null) + if (parent is Style parentStyle && parentStyle.Selector is not null) { if (Selector is null) throw new InvalidOperationException("Child styles must have a selector."); @@ -192,7 +62,7 @@ namespace Avalonia.Styling throw new InvalidOperationException("Child styles must have a nesting selector."); } - Parent = parent; + base.SetParent(parent); } } } diff --git a/src/Avalonia.Base/Styling/StyleBase.cs b/src/Avalonia.Base/Styling/StyleBase.cs new file mode 100644 index 0000000000..0fc57da728 --- /dev/null +++ b/src/Avalonia.Base/Styling/StyleBase.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Metadata; + +namespace Avalonia.Styling +{ + /// + /// Base class for and . + /// + public abstract class StyleBase : AvaloniaObject, IStyle, IResourceProvider + { + private IResourceHost? _owner; + private StyleChildren? _children; + private IResourceDictionary? _resources; + private List? _setters; + private List? _animations; + private StyleCache? _childCache; + + public IList Children => _children ??= new(this); + + public IResourceHost? Owner + { + get => _owner; + private set + { + if (_owner != value) + { + _owner = value; + OwnerChanged?.Invoke(this, EventArgs.Empty); + } + } + } + + public IStyle? Parent { get; private set; } + + public IResourceDictionary Resources + { + get => _resources ?? (Resources = new ResourceDictionary()); + set + { + value = value ?? throw new ArgumentNullException(nameof(value)); + + var hadResources = _resources?.HasResources ?? false; + + _resources = value; + + if (Owner is object) + { + _resources.AddOwner(Owner); + + if (hadResources || _resources.HasResources) + { + Owner.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); + } + } + } + } + + public IList Setters => _setters ??= new List(); + public IList Animations => _animations ??= new List(); + + bool IResourceNode.HasResources => _resources?.Count > 0; + IReadOnlyList IStyle.Children => (IReadOnlyList?)_children ?? Array.Empty(); + + public void Add(ISetter setter) => Setters.Add(setter); + public void Add(IStyle style) => Children.Add(style); + + public event EventHandler? OwnerChanged; + + public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + + var result = SelectorMatchResult.NeverThisType; + + if (_setters?.Count > 0 || _animations?.Count > 0) + { + var match = Matches(target, host); + + if (match.IsMatch) + { + var instance = new StyleInstance(this, target, _setters, _animations, match.Activator); + target.StyleApplied(instance); + instance.Start(); + } + + result = match.Result; + } + + if (_children is not null) + { + _childCache ??= new StyleCache(); + var childResult = _childCache.TryAttach(_children, target, host); + if (childResult > result) + result = childResult; + } + + return result; + } + + public bool TryGetResource(object key, out object? result) + { + result = null; + return _resources?.TryGetResource(key, out result) ?? false; + } + + protected abstract SelectorMatch Matches(IStyleable target, IStyleHost? host); + + internal virtual void SetParent(StyleBase? parent) => Parent = parent; + + void IResourceProvider.AddOwner(IResourceHost owner) + { + owner = owner ?? throw new ArgumentNullException(nameof(owner)); + + if (Owner != null) + { + throw new InvalidOperationException("The Style already has a parent."); + } + + Owner = owner; + _resources?.AddOwner(owner); + } + + void IResourceProvider.RemoveOwner(IResourceHost owner) + { + owner = owner ?? throw new ArgumentNullException(nameof(owner)); + + if (Owner == owner) + { + Owner = null; + _resources?.RemoveOwner(owner); + } + } + } +} diff --git a/src/Avalonia.Base/Styling/StyleChildren.cs b/src/Avalonia.Base/Styling/StyleChildren.cs index 5f8635f155..42b0a331ee 100644 --- a/src/Avalonia.Base/Styling/StyleChildren.cs +++ b/src/Avalonia.Base/Styling/StyleChildren.cs @@ -5,20 +5,20 @@ namespace Avalonia.Styling { internal class StyleChildren : Collection { - private readonly Style _owner; + private readonly StyleBase _owner; - public StyleChildren(Style owner) => _owner = owner; + public StyleChildren(StyleBase owner) => _owner = owner; protected override void InsertItem(int index, IStyle item) { - (item as Style)?.SetParent(_owner); + (item as StyleBase)?.SetParent(_owner); base.InsertItem(index, item); } protected override void RemoveItem(int index) { var item = Items[index]; - (item as Style)?.SetParent(null); + (item as StyleBase)?.SetParent(null); if (_owner.Owner is IResourceHost host) (item as IResourceProvider)?.RemoveOwner(host); base.RemoveItem(index); @@ -26,7 +26,7 @@ namespace Avalonia.Styling protected override void SetItem(int index, IStyle item) { - (item as Style)?.SetParent(_owner); + (item as StyleBase)?.SetParent(_owner); base.SetItem(index, item); if (_owner.Owner is IResourceHost host) (item as IResourceProvider)?.AddOwner(host); From 088d8cfc5c147da723e7641cf77c8dc67646e786 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 1 Jun 2022 13:21:52 +0200 Subject: [PATCH 051/389] Initial implementation of control themes. --- src/Avalonia.Base/Styling/ControlTheme.cs | 27 ++++ src/Avalonia.Base/Styling/IStyle.cs | 2 +- src/Avalonia.Base/Styling/IThemed.cs | 13 ++ src/Avalonia.Base/Styling/NestingSelector.cs | 4 +- src/Avalonia.Base/Styling/Style.cs | 8 +- src/Avalonia.Base/Styling/StyleBase.cs | 20 ++- src/Avalonia.Base/Styling/StyleCache.cs | 2 +- src/Avalonia.Base/Styling/Styler.cs | 14 +++ src/Avalonia.Base/Styling/Styles.cs | 2 +- .../Primitives/TemplatedControl.cs | 25 +++- src/Avalonia.Themes.Default/SimpleTheme.cs | 2 +- src/Avalonia.Themes.Fluent/FluentTheme.cs | 2 +- .../Styling/StyleInclude.cs | 2 +- .../TemplatedControlTests_Theming.cs | 119 ++++++++++++++++++ 14 files changed, 227 insertions(+), 15 deletions(-) create mode 100644 src/Avalonia.Base/Styling/ControlTheme.cs create mode 100644 src/Avalonia.Base/Styling/IThemed.cs create mode 100644 tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests_Theming.cs diff --git a/src/Avalonia.Base/Styling/ControlTheme.cs b/src/Avalonia.Base/Styling/ControlTheme.cs new file mode 100644 index 0000000000..54fc972c31 --- /dev/null +++ b/src/Avalonia.Base/Styling/ControlTheme.cs @@ -0,0 +1,27 @@ +using System; + +namespace Avalonia.Styling +{ + /// + /// Defines a switchable theme for a control. + /// + public class ControlTheme : StyleBase + { + /// + /// Gets or sets the type for which this control theme is intended. + /// + public Type? TargetType { get; set; } + + internal override bool HasSelector => TargetType is not null; + + internal override SelectorMatch Match(IStyleable control, object? host, bool subscribe) + { + if (TargetType is null) + throw new InvalidOperationException("ControlTheme has no TargetType."); + + return control.StyleKey == TargetType ? + SelectorMatch.AlwaysThisType : + SelectorMatch.NeverThisType; + } + } +} diff --git a/src/Avalonia.Base/Styling/IStyle.cs b/src/Avalonia.Base/Styling/IStyle.cs index e9faf82c07..417739fb28 100644 --- a/src/Avalonia.Base/Styling/IStyle.cs +++ b/src/Avalonia.Base/Styling/IStyle.cs @@ -23,6 +23,6 @@ namespace Avalonia.Styling /// /// A describing how the style matches the control. /// - SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host); + SelectorMatchResult TryAttach(IStyleable target, object? host); } } diff --git a/src/Avalonia.Base/Styling/IThemed.cs b/src/Avalonia.Base/Styling/IThemed.cs new file mode 100644 index 0000000000..32ae515bcb --- /dev/null +++ b/src/Avalonia.Base/Styling/IThemed.cs @@ -0,0 +1,13 @@ +namespace Avalonia.Styling +{ + /// + /// Represents a themed element. + /// + public interface IThemed + { + /// + /// Gets the theme style for the element. + /// + public ControlTheme? Theme { get; } + } +} diff --git a/src/Avalonia.Base/Styling/NestingSelector.cs b/src/Avalonia.Base/Styling/NestingSelector.cs index 481a937867..6d31f7cb18 100644 --- a/src/Avalonia.Base/Styling/NestingSelector.cs +++ b/src/Avalonia.Base/Styling/NestingSelector.cs @@ -15,9 +15,9 @@ namespace Avalonia.Styling protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe) { - if (parent is Style s && s.Selector is Selector selector) + if (parent is StyleBase s && s.HasSelector) { - return selector.Match(control, (parent as Style)?.Parent, subscribe); + return s.Match(control, null, subscribe); } throw new InvalidOperationException( diff --git a/src/Avalonia.Base/Styling/Style.cs b/src/Avalonia.Base/Styling/Style.cs index c85c85fe21..ca20ff2b4b 100644 --- a/src/Avalonia.Base/Styling/Style.cs +++ b/src/Avalonia.Base/Styling/Style.cs @@ -28,6 +28,8 @@ namespace Avalonia.Styling /// public Selector? Selector { get; set; } + internal override bool HasSelector => Selector is not null; + /// /// Returns a string representation of the style. /// @@ -44,10 +46,10 @@ namespace Avalonia.Styling } } - protected override SelectorMatch Matches(IStyleable target, IStyleHost? host) + internal override SelectorMatch Match(IStyleable control, object? host, bool subscribe) { - return Selector?.Match(target, Parent) ?? - (target == host ? + return Selector?.Match(control, Parent, subscribe) ?? + (control == host ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance); } diff --git a/src/Avalonia.Base/Styling/StyleBase.cs b/src/Avalonia.Base/Styling/StyleBase.cs index 0fc57da728..b6bfec62bd 100644 --- a/src/Avalonia.Base/Styling/StyleBase.cs +++ b/src/Avalonia.Base/Styling/StyleBase.cs @@ -64,12 +64,14 @@ namespace Avalonia.Styling bool IResourceNode.HasResources => _resources?.Count > 0; IReadOnlyList IStyle.Children => (IReadOnlyList?)_children ?? Array.Empty(); + internal abstract bool HasSelector { get; } + public void Add(ISetter setter) => Setters.Add(setter); public void Add(IStyle style) => Children.Add(style); public event EventHandler? OwnerChanged; - public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) + public SelectorMatchResult TryAttach(IStyleable target, object? host) { target = target ?? throw new ArgumentNullException(nameof(target)); @@ -77,7 +79,7 @@ namespace Avalonia.Styling if (_setters?.Count > 0 || _animations?.Count > 0) { - var match = Matches(target, host); + var match = Match(target, host, subscribe: true); if (match.IsMatch) { @@ -106,7 +108,19 @@ namespace Avalonia.Styling return _resources?.TryGetResource(key, out result) ?? false; } - protected abstract SelectorMatch Matches(IStyleable target, IStyleHost? host); + /// + /// Evaluates the style's selector against the specified target element. + /// + /// The control. + /// The element that hosts the style. + /// + /// Whether the match should subscribe to changes in order to track the match over time, + /// or simply return an immediate result. + /// + /// + /// A describing how the style matches the control. + /// + internal abstract SelectorMatch Match(IStyleable control, object? host, bool subscribe); internal virtual void SetParent(StyleBase? parent) => Parent = parent; diff --git a/src/Avalonia.Base/Styling/StyleCache.cs b/src/Avalonia.Base/Styling/StyleCache.cs index 3285476880..81196f6a27 100644 --- a/src/Avalonia.Base/Styling/StyleCache.cs +++ b/src/Avalonia.Base/Styling/StyleCache.cs @@ -12,7 +12,7 @@ namespace Avalonia.Styling /// internal class StyleCache : Dictionary?> { - public SelectorMatchResult TryAttach(IList styles, IStyleable target, IStyleHost? host) + public SelectorMatchResult TryAttach(IList styles, IStyleable target, object? host) { if (TryGetValue(target.StyleKey, out var cached)) { diff --git a/src/Avalonia.Base/Styling/Styler.cs b/src/Avalonia.Base/Styling/Styler.cs index 74cf77ea40..b9359b3329 100644 --- a/src/Avalonia.Base/Styling/Styler.cs +++ b/src/Avalonia.Base/Styling/Styler.cs @@ -10,6 +10,20 @@ namespace Avalonia.Styling { target = target ?? throw new ArgumentNullException(nameof(target)); + // If the control has a themed templated parent then first apply the styles from + // the templated parent theme. + if (target.TemplatedParent is IThemed themedTemplatedParent) + { + themedTemplatedParent.Theme?.TryAttach(target, themedTemplatedParent); + } + + // If the control itself is themed, then next apply the control theme. + if (target is IThemed themed) + { + themed.Theme?.TryAttach(target, target); + } + + // Apply styles from the rest of the tree. if (target is IStyleHost styleHost) { ApplyStyles(target, styleHost); diff --git a/src/Avalonia.Base/Styling/Styles.cs b/src/Avalonia.Base/Styling/Styles.cs index 7c0bc4ad7f..4c011f1b0d 100644 --- a/src/Avalonia.Base/Styling/Styles.cs +++ b/src/Avalonia.Base/Styling/Styles.cs @@ -109,7 +109,7 @@ namespace Avalonia.Styling set => _styles[index] = value; } - public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) + public SelectorMatchResult TryAttach(IStyleable target, object? host) { _cache ??= new StyleCache(); return _cache.TryAttach(this, target, host); diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index db029d38c0..e1f42b6eb0 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -12,7 +12,7 @@ namespace Avalonia.Controls.Primitives /// /// A lookless control whose visual appearance is defined by its . /// - public class TemplatedControl : Control, ITemplatedControl + public class TemplatedControl : Control, IThemed, ITemplatedControl { /// /// Defines the property. @@ -86,6 +86,12 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty TemplateProperty = AvaloniaProperty.Register(nameof(Template)); + /// + /// Defines the property. + /// + public static readonly StyledProperty ThemeProperty = + AvaloniaProperty.Register(nameof(Theme)); + /// /// Defines the IsTemplateFocusTarget attached property. /// @@ -228,6 +234,15 @@ namespace Avalonia.Controls.Primitives set { SetValue(TemplateProperty, value); } } + /// + /// Gets or sets the theme to be applied to the control. + /// + public ControlTheme? Theme + { + get { return GetValue(ThemeProperty); } + set { SetValue(ThemeProperty, value); } + } + /// /// Gets the value of the IsTemplateFocusTargetProperty attached property on a control. /// @@ -365,6 +380,14 @@ namespace Avalonia.Controls.Primitives { } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ThemeProperty) + InvalidateStyles(); + } + /// /// Called when the control's template is applied. /// diff --git a/src/Avalonia.Themes.Default/SimpleTheme.cs b/src/Avalonia.Themes.Default/SimpleTheme.cs index 6929660757..d7939a68c1 100644 --- a/src/Avalonia.Themes.Default/SimpleTheme.cs +++ b/src/Avalonia.Themes.Default/SimpleTheme.cs @@ -103,7 +103,7 @@ namespace Avalonia.Themes.Default void IResourceProvider.RemoveOwner(IResourceHost owner) => (Loaded as IResourceProvider)?.RemoveOwner(owner); - public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) => Loaded.TryAttach(target, host); + public SelectorMatchResult TryAttach(IStyleable target, object? host) => Loaded.TryAttach(target, host); public bool TryGetResource(object key, out object? value) { diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.cs b/src/Avalonia.Themes.Fluent/FluentTheme.cs index f6b47a5466..befe669029 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.cs +++ b/src/Avalonia.Themes.Fluent/FluentTheme.cs @@ -164,7 +164,7 @@ namespace Avalonia.Themes.Fluent } } - public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) => Loaded.TryAttach(target, host); + public SelectorMatchResult TryAttach(IStyleable target, object? host) => Loaded.TryAttach(target, host); public bool TryGetResource(object key, out object? value) { diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index fa4a27fc50..109e85f1a4 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -82,7 +82,7 @@ namespace Avalonia.Markup.Xaml.Styling } } - public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) => Loaded.TryAttach(target, host); + public SelectorMatchResult TryAttach(IStyleable target, object? host) => Loaded.TryAttach(target, host); public bool TryGetResource(object key, out object? value) { diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests_Theming.cs b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests_Theming.cs new file mode 100644 index 0000000000..b24adfe7ab --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests_Theming.cs @@ -0,0 +1,119 @@ +using System.Linq; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Media; +using Avalonia.Styling; +using Avalonia.UnitTests; +using Avalonia.VisualTree; +using Xunit; + +#nullable enable + +namespace Avalonia.Controls.UnitTests.Primitives +{ + public class TemplatedControlTests_Theming + { + [Fact] + public void Theme_Is_Applied_When_Attached_To_Logical_Tree() + { + using var app = UnitTestApplication.Start(TestServices.RealStyler); + var target = CreateTarget(); + + Assert.Null(target.Template); + + var root = CreateRoot(target); + + Assert.NotNull(target.Template); + var border = Assert.IsType(target.VisualChild); + + Assert.Equal(border.Background, Brushes.Red); + + target.Classes.Add("foo"); + Assert.Equal(border.Background, Brushes.Green); + } + + [Fact] + public void Theme_Is_Detached_When_Theme_Property_Cleared() + { + using var app = UnitTestApplication.Start(TestServices.RealStyler); + var target = CreateTarget(); + var root = CreateRoot(target); + + Assert.NotNull(target.Template); + + target.Theme = null; + Assert.Null(target.Template); + } + + [Fact] + public void Theme_Is_Applied_On_Layout_After_Theme_Property_Changes() + { + using var app = UnitTestApplication.Start(TestServices.RealStyler); + var target = new ThemedControl(); + var root = CreateRoot(target); + + Assert.Null(target.Template); + + target.Theme = CreateTheme(); + Assert.Null(target.Template); + + root.LayoutManager.ExecuteLayoutPass(); + + var border = Assert.IsType(target.VisualChild); + Assert.NotNull(target.Template); + Assert.Equal(border.Background, Brushes.Red); + } + + private static ThemedControl CreateTarget() + { + return new ThemedControl + { + Theme = CreateTheme(), + }; + } + + private static ControlTheme CreateTheme() + { + var template = new FuncControlTemplate((o, n) => + new Border { Name = "PART_Border" }); + + return new ControlTheme + { + TargetType = typeof(ThemedControl), + Setters = + { + new Setter(ThemedControl.TemplateProperty, template), + }, + Children = + { + new Style(x => x.Nesting().Template().OfType()) + { + Setters = + { + new Setter(Border.BackgroundProperty, Brushes.Red), + } + }, + new Style(x => x.Nesting().Class("foo").Template().OfType()) + { + Setters = + { + new Setter(Border.BackgroundProperty, Brushes.Green), + } + }, + } + }; + } + + private static TestRoot CreateRoot(IControl child) + { + var result = new TestRoot(child); + result.LayoutManager.ExecuteInitialLayoutPass(); + return result; + } + + private class ThemedControl : TemplatedControl + { + public IVisual? VisualChild => VisualChildren?.SingleOrDefault(); + } + } +} From dee353bb9640278ab2364b1d9b4624d5cbe7a215 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 1 Jun 2022 15:24:15 +0200 Subject: [PATCH 052/389] Support ControlTheme in XAML compiler. --- .../AvaloniaXamlIlCompiler.cs | 1 + .../AvaloniaXamlIlControlThemeTransformer.cs | 39 ++++++++++ .../AvaloniaXamlIlSetterTransformer.cs | 75 +++++++++++++------ .../Xaml/ControlThemeTests.cs | 53 +++++++++++++ .../Xaml/TestTemplatedControl.cs | 8 ++ 5 files changed, 155 insertions(+), 21 deletions(-) create mode 100644 src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlThemeTransformer.cs create mode 100644 tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlThemeTests.cs create mode 100644 tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TestTemplatedControl.cs diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index 1ca7be67a7..20e035f8ff 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -48,6 +48,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions InsertBefore( new AvaloniaXamlIlBindingPathParser(), + new AvaloniaXamlIlControlThemeTransformer(), new AvaloniaXamlIlSelectorTransformer(), new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(), new AvaloniaXamlIlPropertyPathTransformer(), diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlThemeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlThemeTransformer.cs new file mode 100644 index 0000000000..1338dc7248 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlThemeTransformer.cs @@ -0,0 +1,39 @@ +using System.Linq; +using XamlX; +using XamlX.Ast; +using XamlX.Transform; +using XamlX.Transform.Transformers; +using XamlX.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + class AvaloniaXamlIlControlThemeTransformer : IXamlAstTransformer + { + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (!(node is XamlAstObjectNode on && on.Type.GetClrType().FullName == "Avalonia.Styling.ControlTheme")) + return node; + + // Check if we've already transformed this node. + if (context.ParentNodes().FirstOrDefault() is AvaloniaXamlIlTargetTypeMetadataNode) + return node; + + var targetTypeNode = on.Children.OfType() + .FirstOrDefault(p => p.Property.GetClrProperty().Name == "TargetType") ?? + throw new XamlParseException("ControlTheme must have a TargetType.", node); + + IXamlType targetType; + + if (targetTypeNode.Values[0] is XamlTypeExtensionNode extension) + targetType = extension.Value.GetClrType(); + else if (targetTypeNode.Values[0] is XamlAstTextNode text) + targetType = TypeReferenceResolver.ResolveType(context, text.Text, false, text, true).GetClrType(); + else + throw new XamlParseException("Could not determine TargetType for ControlTheme.", targetTypeNode); + + return new AvaloniaXamlIlTargetTypeMetadataNode(on, + new XamlAstClrTypeReference(targetTypeNode, targetType, false), + AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs index e816265422..06e34a85a2 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs @@ -1,19 +1,14 @@ -using System; using System.Collections.Generic; using System.Linq; -using Avalonia.Data.Core; -using XamlX; using XamlX.Ast; using XamlX.Emit; using XamlX.IL; using XamlX.Transform; -using XamlX.Transform.Transformers; using XamlX.TypeSystem; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { using XamlParseException = XamlX.XamlParseException; - using XamlLoadException = XamlX.XamlLoadException; class AvaloniaXamlIlSetterTransformer : IXamlAstTransformer { public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) @@ -22,21 +17,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers && on.Type.GetClrType().FullName == "Avalonia.Styling.Setter")) return node; - var parent = context.ParentNodes().OfType() - .FirstOrDefault(p => p.Type.GetClrType().FullName == "Avalonia.Styling.Style"); - - if (parent == null) - throw new XamlParseException( - "Avalonia.Styling.Setter is only valid inside Avalonia.Styling.Style", node); - var selectorProperty = parent.Children.OfType() - .FirstOrDefault(p => p.Property.GetClrProperty().Name == "Selector"); - if (selectorProperty == null) - throw new XamlParseException( - "Can not find parent Style Selector", node); - var selector = selectorProperty.Values.FirstOrDefault() as XamlIlSelectorNode; - if (selector?.TargetType == null) - throw new XamlParseException( - "Can not resolve parent Style Selector type", node); + var targetTypeNode = context.ParentNodes() + .OfType() + .FirstOrDefault(x => x.ScopeType == AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style) ?? + throw new XamlParseException("Can not find parent Style Selector or ControlTemplate TargetType", node); IXamlType propType = null; var property = @on.Children.OfType() @@ -50,7 +34,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers var avaloniaPropertyNode = XamlIlAvaloniaPropertyHelper.CreateNode(context, propertyName, - new XamlAstClrTypeReference(selector, selector.TargetType, false), property.Values[0]); + new XamlAstClrTypeReference(targetTypeNode, targetTypeNode.TargetType.GetClrType(), false), property.Values[0]); property.Values = new List {avaloniaPropertyNode}; propType = avaloniaPropertyNode.AvaloniaPropertyType; } @@ -84,6 +68,55 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers return node; } + private (IXamlLineInfo, IXamlType) GetTargetType(AstTransformationContext context, IXamlAstNode node) + { + foreach (var n in context.ParentNodes()) + { + if (n is XamlAstObjectNode parent) + { + switch (parent.Type.GetClrType().FullName) + { + case "Avalonia.Styling.Style": + var selectorProperty = parent.Children.OfType() + .FirstOrDefault(p => p.Property.GetClrProperty().Name == "Selector"); + if (selectorProperty == null) + throw new XamlParseException("Can not find parent Style Selector.", node); + var selector = selectorProperty.Values.FirstOrDefault() as XamlIlSelectorNode; + if (selector?.TargetType != null) + return (selector, selector.TargetType); + throw new XamlParseException( + "Can not resolve parent Style Selector type", node); + + case "Avalonia.Styling.ControlTheme": + var targetTypeProperty = parent.Children.OfType() + .FirstOrDefault(p => p.Property.GetClrProperty().Name == "TargetType"); + if (targetTypeProperty == null) + throw new XamlParseException("ControlTemplate has no TargetType.", parent); + break; + } + } + } + + throw new XamlParseException("'Setter' is only valid inside a 'Style' or 'ControlTheme'.", node); + //var parent = context.ParentNodes().OfType() + // .FirstOrDefault(p => p.Type.GetClrType().FullName == "Avalonia.Styling.Style" || + // p.Type.GetClrType().FullName == "Avalonia.Styling.ControlTheme"); + + //if (parent == null) + // throw new XamlParseException( + // "Avalonia.Styling.Setter is only valid inside Avalonia.Styling.Style", node); + //var selectorProperty = parent.Children.OfType() + // .FirstOrDefault(p => p.Property.GetClrProperty().Name == "Selector" || + // p.Property.GetClrProperty().Name == "TargetType"); + //if (selectorProperty == null) + // throw new XamlParseException( + // "Can not find parent Style Selector or ControlTemplate TargetType", node); + //var selector = selectorProperty.Values.FirstOrDefault() as XamlIlSelectorNode; + //if (selector?.TargetType == null) + // throw new XamlParseException( + // "Can not resolve parent Style Selector type", node); + } + class SetterValueProperty : XamlAstClrProperty { public SetterValueProperty(IXamlLineInfo line, IXamlType setterType, IXamlType targetType, diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlThemeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlThemeTests.cs new file mode 100644 index 0000000000..05083537cd --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlThemeTests.cs @@ -0,0 +1,53 @@ +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.UnitTests; +using Avalonia.VisualTree; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Xaml +{ + public class ControlThemeTests : XamlTestBase + { + [Fact] + public void ControlTheme_Can_Be_StaticResource() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = $@" + + + {ControlThemeXaml} + + + +"; + + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var button = Assert.IsType(window.Content); + + window.LayoutManager.ExecuteInitialLayoutPass(); + + Assert.NotNull(button.Template); + + var child = Assert.Single(button.GetVisualChildren()); + var border = Assert.IsType(child); + + Assert.Equal(Brushes.Red, border.Background); + } + } + + private const string ControlThemeXaml = @" + + + + + + + +"; + } +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TestTemplatedControl.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TestTemplatedControl.cs new file mode 100644 index 0000000000..0c862bb66a --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TestTemplatedControl.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls.Primitives; + +namespace Avalonia.Markup.Xaml.UnitTests.Xaml +{ + public class TestTemplatedControl : TemplatedControl + { + } +} From a6dc6b1c887c8a5139be7bf1abba1315c26af0d7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 1 Jun 2022 21:45:45 +0200 Subject: [PATCH 053/389] Prevent ControlTheme as a nested style. --- src/Avalonia.Base/Styling/ControlTheme.cs | 16 +++++++++++ .../Styling/ControlThemeTests.cs | 28 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 tests/Avalonia.Base.UnitTests/Styling/ControlThemeTests.cs diff --git a/src/Avalonia.Base/Styling/ControlTheme.cs b/src/Avalonia.Base/Styling/ControlTheme.cs index 54fc972c31..9dcbd7d2c4 100644 --- a/src/Avalonia.Base/Styling/ControlTheme.cs +++ b/src/Avalonia.Base/Styling/ControlTheme.cs @@ -7,6 +7,17 @@ namespace Avalonia.Styling /// public class ControlTheme : StyleBase { + /// + /// Initializes a new instance of the class. + /// + public ControlTheme() { } + + /// + /// Initializes a new instance of the class. + /// + /// The value for . + public ControlTheme(Type targetType) => TargetType = targetType; + /// /// Gets or sets the type for which this control theme is intended. /// @@ -23,5 +34,10 @@ namespace Avalonia.Styling SelectorMatch.AlwaysThisType : SelectorMatch.NeverThisType; } + + internal override void SetParent(StyleBase? parent) + { + throw new InvalidOperationException("ControlThemes cannot be added as a nested style."); + } } } diff --git a/tests/Avalonia.Base.UnitTests/Styling/ControlThemeTests.cs b/tests/Avalonia.Base.UnitTests/Styling/ControlThemeTests.cs new file mode 100644 index 0000000000..93a0e6c2fd --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Styling/ControlThemeTests.cs @@ -0,0 +1,28 @@ +using System; +using Avalonia.Controls; +using Avalonia.Styling; +using Xunit; + +namespace Avalonia.Base.UnitTests.Styling +{ + public class ControlThemeTests + { + [Fact] + public void ControlTheme_Cannot_Be_Added_To_Style_Children() + { + var target = new ControlTheme(typeof(Button)); + var style = new Style(); + + Assert.Throws(() => style.Children.Add(target)); + } + + [Fact] + public void ControlTheme_Cannot_Be_Added_To_ControlTheme_Children() + { + var target = new ControlTheme(typeof(Button)); + var other = new ControlTheme(typeof(CheckBox)); + + Assert.Throws(() => other.Children.Add(target)); + } + } +} From fc3c036b02afce41d8faca7e4c1e8219fb0c4ceb Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 1 Jun 2022 22:48:34 +0200 Subject: [PATCH 054/389] Move Theme to StyledElement. The WPF equivalent (`Style`) is in `FrameworkElement` so it would make sense. Will also make stuff a lot easier and removes the need for an `IThemed` interface. --- src/Avalonia.Base/StyledElement.cs | 25 +++++++++++++++++-- src/Avalonia.Base/Styling/IStyleable.cs | 7 ++++-- src/Avalonia.Base/Styling/IThemed.cs | 13 ---------- src/Avalonia.Base/Styling/Styler.cs | 23 ++++------------- .../Primitives/TemplatedControl.cs | 25 +------------------ .../AvaloniaPropertyConverterTest.cs | 5 ++++ 6 files changed, 39 insertions(+), 59 deletions(-) delete mode 100644 src/Avalonia.Base/Styling/IThemed.cs diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index f98d2cdbcc..4ead2470d7 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -12,8 +12,6 @@ using Avalonia.Logging; using Avalonia.LogicalTree; using Avalonia.Styling; -#nullable enable - namespace Avalonia { /// @@ -55,6 +53,12 @@ namespace Avalonia nameof(TemplatedParent), o => o.TemplatedParent, (o ,v) => o.TemplatedParent = v); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ThemeProperty = + AvaloniaProperty.Register(nameof(Theme)); private int _initCount; private string? _name; @@ -230,6 +234,15 @@ namespace Avalonia internal set => SetAndRaise(TemplatedParentProperty, ref _templatedParent, value); } + /// + /// Gets or sets the theme to be applied to the element. + /// + public ControlTheme? Theme + { + get { return GetValue(ThemeProperty); } + set { SetValue(ThemeProperty, value); } + } + /// /// Gets the styled element's logical children. /// @@ -590,6 +603,14 @@ namespace Avalonia { } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ThemeProperty) + InvalidateStyles(); + } + private static void DataContextNotifying(IAvaloniaObject o, bool updateStarted) { if (o is StyledElement element) diff --git a/src/Avalonia.Base/Styling/IStyleable.cs b/src/Avalonia.Base/Styling/IStyleable.cs index 5bc972e7ab..61fcbdf850 100644 --- a/src/Avalonia.Base/Styling/IStyleable.cs +++ b/src/Avalonia.Base/Styling/IStyleable.cs @@ -3,8 +3,6 @@ using System.Collections.Generic; using Avalonia.Collections; using Avalonia.Metadata; -#nullable enable - namespace Avalonia.Styling { /// @@ -28,6 +26,11 @@ namespace Avalonia.Styling /// ITemplatedControl? TemplatedParent { get; } + /// + /// Gets the theme to be applied to the control. + /// + public ControlTheme? Theme { get; } + /// /// Notifies the element that a style has been applied. /// diff --git a/src/Avalonia.Base/Styling/IThemed.cs b/src/Avalonia.Base/Styling/IThemed.cs deleted file mode 100644 index 32ae515bcb..0000000000 --- a/src/Avalonia.Base/Styling/IThemed.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Avalonia.Styling -{ - /// - /// Represents a themed element. - /// - public interface IThemed - { - /// - /// Gets the theme style for the element. - /// - public ControlTheme? Theme { get; } - } -} diff --git a/src/Avalonia.Base/Styling/Styler.cs b/src/Avalonia.Base/Styling/Styler.cs index b9359b3329..c9ea123bdc 100644 --- a/src/Avalonia.Base/Styling/Styler.cs +++ b/src/Avalonia.Base/Styling/Styler.cs @@ -1,33 +1,24 @@ using System; -#nullable enable - namespace Avalonia.Styling { public class Styler : IStyler { public void ApplyStyles(IStyleable target) { - target = target ?? throw new ArgumentNullException(nameof(target)); + _ = target ?? throw new ArgumentNullException(nameof(target)); // If the control has a themed templated parent then first apply the styles from // the templated parent theme. - if (target.TemplatedParent is IThemed themedTemplatedParent) - { - themedTemplatedParent.Theme?.TryAttach(target, themedTemplatedParent); - } + if (target.TemplatedParent is IStyleable styleableParent) + styleableParent.Theme?.TryAttach(target, styleableParent); - // If the control itself is themed, then next apply the control theme. - if (target is IThemed themed) - { - themed.Theme?.TryAttach(target, target); - } + // Next apply the control theme. + target.Theme?.TryAttach(target, target); // Apply styles from the rest of the tree. if (target is IStyleHost styleHost) - { ApplyStyles(target, styleHost); - } } private void ApplyStyles(IStyleable target, IStyleHost host) @@ -35,14 +26,10 @@ namespace Avalonia.Styling var parent = host.StylingParent; if (parent != null) - { ApplyStyles(target, parent); - } if (host.IsStylesInitialized) - { host.Styles.TryAttach(target, host); - } } } } diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index e1f42b6eb0..db029d38c0 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -12,7 +12,7 @@ namespace Avalonia.Controls.Primitives /// /// A lookless control whose visual appearance is defined by its . /// - public class TemplatedControl : Control, IThemed, ITemplatedControl + public class TemplatedControl : Control, ITemplatedControl { /// /// Defines the property. @@ -86,12 +86,6 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty TemplateProperty = AvaloniaProperty.Register(nameof(Template)); - /// - /// Defines the property. - /// - public static readonly StyledProperty ThemeProperty = - AvaloniaProperty.Register(nameof(Theme)); - /// /// Defines the IsTemplateFocusTarget attached property. /// @@ -234,15 +228,6 @@ namespace Avalonia.Controls.Primitives set { SetValue(TemplateProperty, value); } } - /// - /// Gets or sets the theme to be applied to the control. - /// - public ControlTheme? Theme - { - get { return GetValue(ThemeProperty); } - set { SetValue(ThemeProperty, value); } - } - /// /// Gets the value of the IsTemplateFocusTargetProperty attached property on a control. /// @@ -380,14 +365,6 @@ namespace Avalonia.Controls.Primitives { } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - base.OnPropertyChanged(change); - - if (change.Property == ThemeProperty) - InvalidateStyles(); - } - /// /// Called when the control's template is applied. /// diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs index 33bf72014c..ca59fe8480 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs @@ -137,6 +137,11 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters get { throw new NotImplementedException(); } } + public ControlTheme Theme + { + get { throw new NotImplementedException(); } + } + public void DetachStyles() { throw new NotImplementedException(); From 8c61f25188afe50b0785150e2e1fbf605c4652c5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Jun 2022 09:23:17 +0200 Subject: [PATCH 055/389] Promote theme to LocalValue if applied from style. --- src/Avalonia.Base/StyledElement.cs | 25 ++- .../TemplatedControlTests_Theming.cs | 146 ++++++++++++------ .../Xaml/ControlThemeTests.cs | 36 +++++ 3 files changed, 157 insertions(+), 50 deletions(-) diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index 4ead2470d7..75c4b94174 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -71,6 +71,7 @@ namespace Avalonia private List? _appliedStyles; private ITemplatedControl? _templatedParent; private bool _dataContextUpdating; + private bool _hasPromotedTheme; /// /// Initializes static members of the class. @@ -239,8 +240,8 @@ namespace Avalonia /// public ControlTheme? Theme { - get { return GetValue(ThemeProperty); } - set { SetValue(ThemeProperty, value); } + get => GetValue(ThemeProperty); + set => SetValue(ThemeProperty, value); } /// @@ -315,6 +316,7 @@ namespace Avalonia /// IStyleHost? IStyleHost.StylingParent => (IStyleHost?)InheritanceParent; + /// public virtual void BeginInit() { @@ -354,10 +356,15 @@ namespace Avalonia } finally { + _styled = true; EndBatchUpdate(); } - _styled = true; + if (_hasPromotedTheme) + { + _hasPromotedTheme = false; + ClearValue(ThemeProperty); + } } return _styled; @@ -608,7 +615,19 @@ namespace Avalonia base.OnPropertyChanged(change); if (change.Property == ThemeProperty) + { + // Changing the theme detaches all styles, meaning that if the theme property was + // set via a style, it will get cleared! To work around this, if the value was + // applied at less than local value priority then promote the value to local value + // priority until styling is re-applied. + if (change.Priority > BindingPriority.LocalValue) + { + Theme = change.GetNewValue(); + _hasPromotedTheme = true; + } + InvalidateStyles(); + } } private static void DataContextNotifying(IAvaloniaObject o, bool updateStarted) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests_Theming.cs b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests_Theming.cs index b24adfe7ab..74d75ff056 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests_Theming.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests_Theming.cs @@ -13,63 +13,122 @@ namespace Avalonia.Controls.UnitTests.Primitives { public class TemplatedControlTests_Theming { - [Fact] - public void Theme_Is_Applied_When_Attached_To_Logical_Tree() + public class InlineTheme { - using var app = UnitTestApplication.Start(TestServices.RealStyler); - var target = CreateTarget(); + [Fact] + public void Theme_Is_Applied_When_Attached_To_Logical_Tree() + { + using var app = UnitTestApplication.Start(TestServices.RealStyler); + var target = CreateTarget(); - Assert.Null(target.Template); + Assert.Null(target.Template); - var root = CreateRoot(target); + var root = CreateRoot(target); + Assert.NotNull(target.Template); - Assert.NotNull(target.Template); - var border = Assert.IsType(target.VisualChild); - - Assert.Equal(border.Background, Brushes.Red); + var border = Assert.IsType(target.VisualChild); + Assert.Equal(border.Background, Brushes.Red); - target.Classes.Add("foo"); - Assert.Equal(border.Background, Brushes.Green); - } + target.Classes.Add("foo"); + Assert.Equal(border.Background, Brushes.Green); + } - [Fact] - public void Theme_Is_Detached_When_Theme_Property_Cleared() - { - using var app = UnitTestApplication.Start(TestServices.RealStyler); - var target = CreateTarget(); - var root = CreateRoot(target); + [Fact] + public void Theme_Is_Detached_When_Theme_Property_Cleared() + { + using var app = UnitTestApplication.Start(TestServices.RealStyler); + var target = CreateTarget(); + var root = CreateRoot(target); - Assert.NotNull(target.Template); + Assert.NotNull(target.Template); - target.Theme = null; - Assert.Null(target.Template); - } + target.Theme = null; + Assert.Null(target.Template); + } - [Fact] - public void Theme_Is_Applied_On_Layout_After_Theme_Property_Changes() - { - using var app = UnitTestApplication.Start(TestServices.RealStyler); - var target = new ThemedControl(); - var root = CreateRoot(target); + [Fact] + public void Theme_Is_Applied_On_Layout_After_Theme_Property_Changes() + { + using var app = UnitTestApplication.Start(TestServices.RealStyler); + var target = new ThemedControl(); + var root = CreateRoot(target); + + Assert.Null(target.Template); - Assert.Null(target.Template); + target.Theme = CreateTheme(); + Assert.Null(target.Template); - target.Theme = CreateTheme(); - Assert.Null(target.Template); + root.LayoutManager.ExecuteLayoutPass(); - root.LayoutManager.ExecuteLayoutPass(); + var border = Assert.IsType(target.VisualChild); + Assert.NotNull(target.Template); + Assert.Equal(border.Background, Brushes.Red); + } - var border = Assert.IsType(target.VisualChild); - Assert.NotNull(target.Template); - Assert.Equal(border.Background, Brushes.Red); + private static ThemedControl CreateTarget() + { + return new ThemedControl + { + Theme = CreateTheme(), + }; + } + + private static TestRoot CreateRoot(IControl child) + { + var result = new TestRoot(child); + result.LayoutManager.ExecuteInitialLayoutPass(); + return result; + } } - private static ThemedControl CreateTarget() + public class ThemeFromStyle { - return new ThemedControl + [Fact] + public void Theme_Is_Applied_When_Attached_To_Logical_Tree() { - Theme = CreateTheme(), - }; + using var app = UnitTestApplication.Start(TestServices.RealStyler); + var target = CreateTarget(); + + Assert.Null(target.Theme); + Assert.Null(target.Template); + + var root = CreateRoot(target); + + Assert.NotNull(target.Theme); + Assert.NotNull(target.Template); + + var border = Assert.IsType(target.VisualChild); + Assert.Equal(border.Background, Brushes.Red); + + target.Classes.Add("foo"); + Assert.Equal(border.Background, Brushes.Green); + } + + private static ThemedControl CreateTarget() + { + return new ThemedControl(); + } + + private static TestRoot CreateRoot(IControl child) + { + var result = new TestRoot() + { + Styles = + { + new Style(x => x.OfType()) + { + Setters = + { + new Setter(TemplatedControl.ThemeProperty, CreateTheme()) + } + } + } + }; + + result.Child = child; + result.LayoutManager.ExecuteInitialLayoutPass(); + return result; + } } private static ControlTheme CreateTheme() @@ -104,13 +163,6 @@ namespace Avalonia.Controls.UnitTests.Primitives }; } - private static TestRoot CreateRoot(IControl child) - { - var result = new TestRoot(child); - result.LayoutManager.ExecuteInitialLayoutPass(); - return result; - } - private class ThemedControl : TemplatedControl { public IVisual? VisualChild => VisualChildren?.SingleOrDefault(); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlThemeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlThemeTests.cs index 05083537cd..9eb48311df 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlThemeTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlThemeTests.cs @@ -38,6 +38,42 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } + [Fact] + public void ControlTheme_Can_Be_Set_In_Style() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = $@" + + + {ControlThemeXaml} + + + + + + + +"; + + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var button = Assert.IsType(window.Content); + + window.LayoutManager.ExecuteInitialLayoutPass(); + + Assert.NotNull(button.Template); + + var child = Assert.Single(button.GetVisualChildren()); + var border = Assert.IsType(child); + + Assert.Equal(Brushes.Red, border.Background); + } + } + private const string ControlThemeXaml = @" From 5cd95320128fa97fff7af8223cf55b9d043086f8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Jun 2022 09:36:53 +0200 Subject: [PATCH 056/389] Move tests to correct place. --- .../Styling/StyledElementTests_Theming.cs | 169 +++++++++++++++++ .../TemplatedControlTests_Theming.cs | 171 ------------------ 2 files changed, 169 insertions(+), 171 deletions(-) create mode 100644 tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs delete mode 100644 tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests_Theming.cs diff --git a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs new file mode 100644 index 0000000000..539f9e6576 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs @@ -0,0 +1,169 @@ +using System.Linq; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Media; +using Avalonia.Styling; +using Avalonia.UnitTests; +using Avalonia.VisualTree; +using Xunit; + +namespace Avalonia.Base.UnitTests.Styling; + +public class StyledElementTests_Theming +{ + public class InlineTheme + { + [Fact] + public void Theme_Is_Applied_When_Attached_To_Logical_Tree() + { + using var app = UnitTestApplication.Start(TestServices.RealStyler); + var target = CreateTarget(); + + Assert.Null(target.Template); + + var root = CreateRoot(target); + Assert.NotNull(target.Template); + + var border = Assert.IsType(target.VisualChild); + Assert.Equal(border.Background, Brushes.Red); + + target.Classes.Add("foo"); + Assert.Equal(border.Background, Brushes.Green); + } + + [Fact] + public void Theme_Is_Detached_When_Theme_Property_Cleared() + { + using var app = UnitTestApplication.Start(TestServices.RealStyler); + var target = CreateTarget(); + var root = CreateRoot(target); + + Assert.NotNull(target.Template); + + target.Theme = null; + Assert.Null(target.Template); + } + + [Fact] + public void Theme_Is_Applied_On_Layout_After_Theme_Property_Changes() + { + using var app = UnitTestApplication.Start(TestServices.RealStyler); + var target = new ThemedControl(); + var root = CreateRoot(target); + + Assert.Null(target.Template); + + target.Theme = CreateTheme(); + Assert.Null(target.Template); + + root.LayoutManager.ExecuteLayoutPass(); + + var border = Assert.IsType(target.VisualChild); + Assert.NotNull(target.Template); + Assert.Equal(border.Background, Brushes.Red); + } + + private static ThemedControl CreateTarget() + { + return new ThemedControl + { + Theme = CreateTheme(), + }; + } + + private static TestRoot CreateRoot(IControl child) + { + var result = new TestRoot(child); + result.LayoutManager.ExecuteInitialLayoutPass(); + return result; + } + } + + public class ThemeFromStyle + { + [Fact] + public void Theme_Is_Applied_When_Attached_To_Logical_Tree() + { + using var app = UnitTestApplication.Start(TestServices.RealStyler); + var target = CreateTarget(); + + Assert.Null(target.Theme); + Assert.Null(target.Template); + + var root = CreateRoot(target); + + Assert.NotNull(target.Theme); + Assert.NotNull(target.Template); + + var border = Assert.IsType(target.VisualChild); + Assert.Equal(border.Background, Brushes.Red); + + target.Classes.Add("foo"); + Assert.Equal(border.Background, Brushes.Green); + } + + private static ThemedControl CreateTarget() + { + return new ThemedControl(); + } + + private static TestRoot CreateRoot(IControl child) + { + var result = new TestRoot() + { + Styles = + { + new Style(x => x.OfType()) + { + Setters = + { + new Setter(TemplatedControl.ThemeProperty, CreateTheme()) + } + } + } + }; + + result.Child = child; + result.LayoutManager.ExecuteInitialLayoutPass(); + return result; + } + } + + private static ControlTheme CreateTheme() + { + var template = new FuncControlTemplate((o, n) => + new Border { Name = "PART_Border" }); + + return new ControlTheme + { + TargetType = typeof(ThemedControl), + Setters = + { + new Setter(ThemedControl.TemplateProperty, template), + }, + Children = + { + new Style(x => x.Nesting().Template().OfType()) + { + Setters = + { + new Setter(Border.BackgroundProperty, Brushes.Red), + } + }, + new Style(x => x.Nesting().Class("foo").Template().OfType()) + { + Setters = + { + new Setter(Border.BackgroundProperty, Brushes.Green), + } + }, + } + }; + } + + private class ThemedControl : TemplatedControl + { + public IVisual? VisualChild => VisualChildren?.SingleOrDefault(); + } +} diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests_Theming.cs b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests_Theming.cs deleted file mode 100644 index 74d75ff056..0000000000 --- a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests_Theming.cs +++ /dev/null @@ -1,171 +0,0 @@ -using System.Linq; -using Avalonia.Controls.Primitives; -using Avalonia.Controls.Templates; -using Avalonia.Media; -using Avalonia.Styling; -using Avalonia.UnitTests; -using Avalonia.VisualTree; -using Xunit; - -#nullable enable - -namespace Avalonia.Controls.UnitTests.Primitives -{ - public class TemplatedControlTests_Theming - { - public class InlineTheme - { - [Fact] - public void Theme_Is_Applied_When_Attached_To_Logical_Tree() - { - using var app = UnitTestApplication.Start(TestServices.RealStyler); - var target = CreateTarget(); - - Assert.Null(target.Template); - - var root = CreateRoot(target); - Assert.NotNull(target.Template); - - var border = Assert.IsType(target.VisualChild); - Assert.Equal(border.Background, Brushes.Red); - - target.Classes.Add("foo"); - Assert.Equal(border.Background, Brushes.Green); - } - - [Fact] - public void Theme_Is_Detached_When_Theme_Property_Cleared() - { - using var app = UnitTestApplication.Start(TestServices.RealStyler); - var target = CreateTarget(); - var root = CreateRoot(target); - - Assert.NotNull(target.Template); - - target.Theme = null; - Assert.Null(target.Template); - } - - [Fact] - public void Theme_Is_Applied_On_Layout_After_Theme_Property_Changes() - { - using var app = UnitTestApplication.Start(TestServices.RealStyler); - var target = new ThemedControl(); - var root = CreateRoot(target); - - Assert.Null(target.Template); - - target.Theme = CreateTheme(); - Assert.Null(target.Template); - - root.LayoutManager.ExecuteLayoutPass(); - - var border = Assert.IsType(target.VisualChild); - Assert.NotNull(target.Template); - Assert.Equal(border.Background, Brushes.Red); - } - - private static ThemedControl CreateTarget() - { - return new ThemedControl - { - Theme = CreateTheme(), - }; - } - - private static TestRoot CreateRoot(IControl child) - { - var result = new TestRoot(child); - result.LayoutManager.ExecuteInitialLayoutPass(); - return result; - } - } - - public class ThemeFromStyle - { - [Fact] - public void Theme_Is_Applied_When_Attached_To_Logical_Tree() - { - using var app = UnitTestApplication.Start(TestServices.RealStyler); - var target = CreateTarget(); - - Assert.Null(target.Theme); - Assert.Null(target.Template); - - var root = CreateRoot(target); - - Assert.NotNull(target.Theme); - Assert.NotNull(target.Template); - - var border = Assert.IsType(target.VisualChild); - Assert.Equal(border.Background, Brushes.Red); - - target.Classes.Add("foo"); - Assert.Equal(border.Background, Brushes.Green); - } - - private static ThemedControl CreateTarget() - { - return new ThemedControl(); - } - - private static TestRoot CreateRoot(IControl child) - { - var result = new TestRoot() - { - Styles = - { - new Style(x => x.OfType()) - { - Setters = - { - new Setter(TemplatedControl.ThemeProperty, CreateTheme()) - } - } - } - }; - - result.Child = child; - result.LayoutManager.ExecuteInitialLayoutPass(); - return result; - } - } - - private static ControlTheme CreateTheme() - { - var template = new FuncControlTemplate((o, n) => - new Border { Name = "PART_Border" }); - - return new ControlTheme - { - TargetType = typeof(ThemedControl), - Setters = - { - new Setter(ThemedControl.TemplateProperty, template), - }, - Children = - { - new Style(x => x.Nesting().Template().OfType()) - { - Setters = - { - new Setter(Border.BackgroundProperty, Brushes.Red), - } - }, - new Style(x => x.Nesting().Class("foo").Template().OfType()) - { - Setters = - { - new Setter(Border.BackgroundProperty, Brushes.Green), - } - }, - } - }; - } - - private class ThemedControl : TemplatedControl - { - public IVisual? VisualChild => VisualChildren?.SingleOrDefault(); - } - } -} From 4bdcb8eeeaab2f790245ddb70a5cfca3df7886f8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Jun 2022 10:34:34 +0200 Subject: [PATCH 057/389] Invalidate template control styles when Theme changes. --- .../Primitives/TemplatedControl.cs | 11 +++ .../Styling/StyledElementTests_Theming.cs | 75 ++++++++++++++----- 2 files changed, 66 insertions(+), 20 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index db029d38c0..a07dd9ae27 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -365,6 +365,17 @@ namespace Avalonia.Controls.Primitives { } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ThemeProperty) + { + foreach (var child in this.GetTemplateChildren()) + child.InvalidateStyles(); + } + } + /// /// Called when the control's template is applied. /// diff --git a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs index 539f9e6576..0c0808987a 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs @@ -8,6 +8,8 @@ using Avalonia.UnitTests; using Avalonia.VisualTree; using Xunit; +#nullable enable + namespace Avalonia.Base.UnitTests.Styling; public class StyledElementTests_Theming @@ -45,6 +47,40 @@ public class StyledElementTests_Theming Assert.Null(target.Template); } + [Fact] + public void Theme_Is_Detached_From_Template_Controls_When_Theme_Property_Cleared() + { + using var app = UnitTestApplication.Start(TestServices.RealStyler); + + var theme = new ControlTheme + { + TargetType = typeof(ThemedControl), + Children = + { + new Style(x => x.Nesting().Template().OfType()) + { + Setters = + { + new Setter(Canvas.BackgroundProperty, Brushes.Red), + } + }, + } + }; + + var target = CreateTarget(theme); + target.Template = new FuncControlTemplate((o, n) => new Canvas()); + + var root = CreateRoot(target); + + var canvas = Assert.IsType(target.VisualChild); + Assert.Equal(canvas.Background, Brushes.Red); + + target.Theme = null; + + Assert.IsType(target.VisualChild); + Assert.Null(canvas.Background); + } + [Fact] public void Theme_Is_Applied_On_Layout_After_Theme_Property_Changes() { @@ -64,11 +100,11 @@ public class StyledElementTests_Theming Assert.Equal(border.Background, Brushes.Red); } - private static ThemedControl CreateTarget() + private static ThemedControl CreateTarget(ControlTheme? theme = null) { return new ThemedControl { - Theme = CreateTheme(), + Theme = theme ?? CreateTheme(), }; } @@ -132,33 +168,32 @@ public class StyledElementTests_Theming private static ControlTheme CreateTheme() { - var template = new FuncControlTemplate((o, n) => - new Border { Name = "PART_Border" }); + var template = new FuncControlTemplate((o, n) => new Border()); return new ControlTheme { TargetType = typeof(ThemedControl), Setters = - { - new Setter(ThemedControl.TemplateProperty, template), - }, - Children = - { - new Style(x => x.Nesting().Template().OfType()) { - Setters = - { - new Setter(Border.BackgroundProperty, Brushes.Red), - } + new Setter(ThemedControl.TemplateProperty, template), }, - new Style(x => x.Nesting().Class("foo").Template().OfType()) + Children = { - Setters = + new Style(x => x.Nesting().Template().OfType()) { - new Setter(Border.BackgroundProperty, Brushes.Green), - } - }, - } + Setters = + { + new Setter(Border.BackgroundProperty, Brushes.Red), + } + }, + new Style(x => x.Nesting().Class("foo").Template().OfType()) + { + Setters = + { + new Setter(Border.BackgroundProperty, Brushes.Green), + } + }, + } }; } From 55de1523c42ae438ef8997c0a72cc89b338ff2db Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 2 Jun 2022 20:20:43 -0400 Subject: [PATCH 058/389] Support ignoring property changes in derived Color controls --- .../ColorPicker/ColorPicker.cs | 9 ++++++++ .../ColorSlider/ColorSlider.cs | 22 +++++++++---------- .../ColorView/ColorView.cs | 12 +++++----- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs index 140a24d6a1..d34a91d1bb 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs @@ -12,5 +12,14 @@ namespace Avalonia.Controls /// public class ColorPicker : ColorView { + /// + /// Initializes a new instance of the class. + /// + public ColorPicker() : base() + { + // Completely ignore property changes here + // The ColorView in the control template is responsible to manage this + base.ignorePropertyChanged = true; + } } } diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index 957e5e7b77..a9ba5a20fa 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -29,7 +29,7 @@ namespace Avalonia.Controls.Primitives /// private const double MaxHue = 359; - private bool disableUpdates = false; + protected bool ignorePropertyChanged = false; /// /// Initializes a new instance of the class. @@ -135,7 +135,7 @@ namespace Avalonia.Controls.Primitives /// /// /// Warning: This will trigger property changed updates. - /// Consider using externally. + /// Consider using externally. /// private void SetColorToSliderValues() { @@ -341,7 +341,7 @@ namespace Avalonia.Controls.Primitives /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - if (disableUpdates) + if (ignorePropertyChanged) { base.OnPropertyChanged(change); return; @@ -350,7 +350,7 @@ namespace Avalonia.Controls.Primitives // Always keep the two color properties in sync if (change.Property == ColorProperty) { - disableUpdates = true; + ignorePropertyChanged = true; HsvColor = Color.ToHsv(); @@ -362,21 +362,21 @@ namespace Avalonia.Controls.Primitives change.GetOldValue(), change.GetNewValue())); - disableUpdates = false; + ignorePropertyChanged = false; } else if (change.Property == ColorModelProperty) { - disableUpdates = true; + ignorePropertyChanged = true; SetColorToSliderValues(); UpdateBackground(); UpdatePseudoClasses(); - disableUpdates = false; + ignorePropertyChanged = false; } else if (change.Property == HsvColorProperty) { - disableUpdates = true; + ignorePropertyChanged = true; Color = HsvColor.ToRgb(); @@ -388,7 +388,7 @@ namespace Avalonia.Controls.Primitives change.GetOldValue().ToRgb(), change.GetNewValue().ToRgb())); - disableUpdates = false; + ignorePropertyChanged = false; } else if (change.Property == IsRoundingEnabledProperty) { @@ -402,7 +402,7 @@ namespace Avalonia.Controls.Primitives change.Property == MinimumProperty || change.Property == MaximumProperty) { - disableUpdates = true; + ignorePropertyChanged = true; Color oldColor = Color; (var color, var hsvColor) = GetColorFromSliderValues(); @@ -421,7 +421,7 @@ namespace Avalonia.Controls.Primitives UpdatePseudoClasses(); OnColorChanged(new ColorChangedEventArgs(oldColor, Color)); - disableUpdates = false; + ignorePropertyChanged = false; } base.OnPropertyChanged(change); diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index 96de734cc7..c19daf5f40 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -27,7 +27,7 @@ namespace Avalonia.Controls private ObservableCollection _customPaletteColors = new ObservableCollection(); private ColorToHexConverter colorToHexConverter = new ColorToHexConverter(); - private bool disableUpdates = false; + protected bool ignorePropertyChanged = false; /// /// Initializes a new instance of the class. @@ -167,7 +167,7 @@ namespace Avalonia.Controls /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - if (disableUpdates) + if (ignorePropertyChanged) { base.OnPropertyChanged(change); return; @@ -176,7 +176,7 @@ namespace Avalonia.Controls // Always keep the two color properties in sync if (change.Property == ColorProperty) { - disableUpdates = true; + ignorePropertyChanged = true; HsvColor = Color.ToHsv(); SetColorToHexTextBox(); @@ -185,11 +185,11 @@ namespace Avalonia.Controls change.GetOldValue(), change.GetNewValue())); - disableUpdates = false; + ignorePropertyChanged = false; } else if (change.Property == HsvColorProperty) { - disableUpdates = true; + ignorePropertyChanged = true; Color = HsvColor.ToRgb(); SetColorToHexTextBox(); @@ -198,7 +198,7 @@ namespace Avalonia.Controls change.GetOldValue().ToRgb(), change.GetNewValue().ToRgb())); - disableUpdates = false; + ignorePropertyChanged = false; } else if (change.Property == CustomPaletteProperty) { From 36225e9132719d209d727cd4a6f7ef42c5b7f054 Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 2 Jun 2022 20:21:06 -0400 Subject: [PATCH 059/389] Use more standard binding in ColorPicker This doesn't currently work for some reason --- .../Themes/Fluent/ColorPicker.xaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml index b3424d2cc1..6e5228039d 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -36,7 +36,7 @@ HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="1,1,0,1" /> - Date: Thu, 2 Jun 2022 20:21:16 -0400 Subject: [PATCH 060/389] Small improvements --- src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index c19daf5f40..d0ee5f9acd 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -54,7 +54,7 @@ namespace Avalonia.Controls // Re-apply the hex value // This ensure the hex color value is always valid and formatted correctly - _hexTextBox.Text = colorToHexConverter.Convert(Color, typeof(string), null, CultureInfo.CurrentCulture) as string; + SetColorToHexTextBox(); } } @@ -225,7 +225,7 @@ namespace Avalonia.Controls change.Property == IsColorSpectrumVisibleProperty) { // When the property changed notification is received here the visibility - // of individual tab items has not yet been updated though the bindings. + // of individual tab items has not yet been updated through the bindings. // Therefore, the validation is delayed until after bindings update. Dispatcher.UIThread.Post(() => { From b8be7ba4cb59eb1c406d794d76a6ad74d1d21a3a Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 2 Jun 2022 21:05:47 -0400 Subject: [PATCH 061/389] Implement SelectedTabIndex in ColorView --- .../ColorView/ColorView.Properties.cs | 18 +++++++++++++++++- .../ColorView/ColorView.cs | 16 +++++++++++++++- .../ColorView/ColorViewTab.cs | 15 +++++++++------ .../Themes/Fluent/ColorView.xaml | 3 ++- 4 files changed, 43 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs index a3267d88f2..a4897c99a2 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs @@ -8,7 +8,6 @@ namespace Avalonia.Controls /// public partial class ColorView { - // SelectedTabIndex // IsColorModelSelectorVisible // IsComponentSliderVisible @@ -198,6 +197,14 @@ namespace Avalonia.Controls nameof(MinValue), 0); + /// + /// Defines the property. + /// + public static readonly StyledProperty SelectedTabIndexProperty = + AvaloniaProperty.Register( + nameof(SelectedTabIndex), + (int)ColorViewTab.Spectrum); + /// /// Defines the property. /// @@ -413,6 +420,15 @@ namespace Avalonia.Controls set => SetValue(MinValueProperty, value); } + /// + /// Gets or sets the index of the selected subview/tab. + /// + public int SelectedTabIndex + { + get => GetValue(SelectedTabIndexProperty); + set => SetValue(SelectedTabIndexProperty, value); + } + /// public bool ShowAccentColors { diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index d0ee5f9acd..9809f1312b 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -73,7 +73,10 @@ namespace Avalonia.Controls /// Validates the selected subview/tab taking into account the visibility of each subview/tab /// as well as the current selection. /// - private void ValidateSelectedTab() + /// + /// Derived controls may re-implement this based on their default style / control template. + /// + protected virtual void ValidateSelectedTab() { if (_tabControl != null && _tabControl.Items != null) @@ -135,6 +138,8 @@ namespace Avalonia.Controls _tabControl.SelectedItem = null; _tabControl.IsVisible = false; } + + SelectedTabIndex = _tabControl.SelectedIndex; } return; @@ -232,6 +237,15 @@ namespace Avalonia.Controls ValidateSelectedTab(); }, DispatcherPriority.Background); } + else if (change.Property == SelectedTabIndexProperty) + { + // Again, it is necessary to wait for the SelectedTabIndex value to + // be applied to the TabControl through binding before validation occurs. + Dispatcher.UIThread.Post(() => + { + ValidateSelectedTab(); + }, DispatcherPriority.Background); + } base.OnPropertyChanged(change); } diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs index 534e833631..8e3433d819 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs @@ -1,23 +1,26 @@ namespace Avalonia.Controls { /// - /// Defines a specific subview (tab) within the . + /// Defines a specific subview/tab within the . /// + /// + /// This is indexed to match the default control template ordering. + /// public enum ColorViewTab { /// - /// The components subview with sliders and numeric input boxes. + /// The color spectrum subview with a box/ring spectrum and sliders. /// - Components, + Spectrum = 0, /// /// The color palette subview with a grid of selectable colors. /// - Palette, + Palette = 1, /// - /// The color spectrum subview with a box/ring spectrum and sliders. + /// The components subview with sliders and numeric input boxes. /// - Spectrum, + Components = 2, } } diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index 420e3b2ae9..d6bf83ff44 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -112,7 +112,8 @@ + Width="350" + SelectedIndex="{Binding SelectedTabIndex, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"> 0,0,0,0 From 10bc7b38cbb4795ef4d7a2ad8a32e13e4194d125 Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 2 Jun 2022 22:20:44 -0400 Subject: [PATCH 062/389] Rename 'EnumToBooleanConverter' to 'EnumToBoolConverter' Bool is shorter and is also slightly more commonly used in other converters. Bool matches the type as written in C# as well. --- .../Themes/Default/ColorSpectrum.xaml | 14 +++++++------- .../Themes/Fluent/ColorSpectrum.xaml | 14 +++++++------- .../Themes/Fluent/ColorView.xaml | 18 +++++++++--------- ...leanConverter.cs => EnumToBoolConverter.cs} | 13 ++++++------- 4 files changed, 29 insertions(+), 30 deletions(-) rename src/Avalonia.Controls/Converters/{EnumToBooleanConverter.cs => EnumToBoolConverter.cs} (82%) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml index 891e040e9f..c29f8f51e5 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml @@ -5,7 +5,7 @@ x:CompileBindings="True"> - + @@ -25,26 +25,26 @@ IsHitTestVisible="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" - IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolean}, ConverterParameter={x:Static controls:ColorSpectrumShape.Box}}" + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static controls:ColorSpectrumShape.Box}}" RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}" RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml index 779f228b97..6dd7ddd373 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml @@ -5,7 +5,7 @@ x:CompileBindings="True"> - + @@ -25,26 +25,26 @@ IsHitTestVisible="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" - IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolean}, ConverterParameter={x:Static controls:ColorSpectrumShape.Box}}" + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static controls:ColorSpectrumShape.Box}}" RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}" RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> + IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" /> diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index d6bf83ff44..4b01de15a0 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -12,7 +12,7 @@ - + 48 @@ -351,12 +351,12 @@ Grid.Column="0" Content="RGB" CornerRadius="4,0,0,4" - IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolean}, ConverterParameter={x:Static local:ColorModel.Rgba}, Mode=TwoWay}" /> + IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static local:ColorModel.Rgba}, Mode=TwoWay}" /> + IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static local:ColorModel.Hsva}, Mode=TwoWay}" /> + IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static local:ColorModel.Rgba}, Mode=OneWay}"/> + IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static local:ColorModel.Hsva}, Mode=OneWay}" /> + IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static local:ColorModel.Rgba}, Mode=OneWay}" /> + IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static local:ColorModel.Hsva}, Mode=OneWay}" /> + IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static local:ColorModel.Rgba}, Mode=OneWay}" /> + IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBool}, ConverterParameter={x:Static local:ColorModel.Hsva}, Mode=OneWay}" /> /// This converter is useful to enable binding of radio buttons with a selected enum value. /// - public class EnumToBooleanConverter : IValueConverter + public class EnumToBoolConverter : IValueConverter { /// public object? Convert( @@ -44,14 +44,13 @@ namespace Avalonia.Controls.Converters object? parameter, CultureInfo culture) { - if (value is bool boolValue) + if (value is bool boolValue && + boolValue == true) { - return boolValue ? parameter : BindingOperations.DoNothing; - } - else - { - return BindingOperations.DoNothing; + return parameter; } + + return BindingOperations.DoNothing; } } } From e0c936dbb4e2d48bc3367e0f13e3e2ac1d15002e Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 2 Jun 2022 22:37:54 -0400 Subject: [PATCH 063/389] Add FlatColorPalette --- .../ColorPalette/FlatColorPalette.cs | 284 ++++++++++++++++++ .../ColorPalette/FluentColorPalette.cs | 6 - 2 files changed, 284 insertions(+), 6 deletions(-) create mode 100644 src/Avalonia.Controls.ColorPicker/ColorPalette/FlatColorPalette.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalette/FlatColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalette/FlatColorPalette.cs new file mode 100644 index 0000000000..130d7e0edd --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorPalette/FlatColorPalette.cs @@ -0,0 +1,284 @@ +using Avalonia.Media; +using Avalonia.Utilities; + +namespace Avalonia.Controls +{ + /// + /// Implements a reduced flat design or flat UI color palette. + /// + /// + /// See: + /// - https://htmlcolorcodes.com/color-chart/ + /// - https://htmlcolorcodes.com/color-chart/flat-design-color-chart/ + /// - http://designmodo.github.io/Flat-UI/ + /// + /// The GitHub project is licensed as MIT: https://github.com/designmodo/Flat-UI. + /// + /// + public class FlatColorPalette : IColorPalette + { + // The full Flat UI color chart has 10 rows and 20 columns + // See: https://htmlcolorcodes.com/assets/downloads/flat-design-colors/flat-design-color-chart.png + // This is a reduced palette for usability + private static Color[,] colorChart = new Color[,] + { + // Pomegranate + { + Color.FromArgb(0xFF, 0xF9, 0xEB, 0xEA), + Color.FromArgb(0xFF, 0xE6, 0xB0, 0xAA), + Color.FromArgb(0xFF, 0xCD, 0x61, 0x55), + Color.FromArgb(0xFF, 0xA9, 0x32, 0x26), + Color.FromArgb(0xFF, 0x7B, 0x24, 0x1C), + }, + + // Amethyst + { + Color.FromArgb(0xFF, 0xF5, 0xEE, 0xF8), + Color.FromArgb(0xFF, 0xD7, 0xBD, 0xE2), + Color.FromArgb(0xFF, 0xAF, 0x7A, 0xC5), + Color.FromArgb(0xFF, 0x88, 0x4E, 0xA0), + Color.FromArgb(0xFF, 0x63, 0x39, 0x74), + }, + + // Belize Hole + { + Color.FromArgb(0xFF, 0xEA, 0xF2, 0xF8), + Color.FromArgb(0xFF, 0xA9, 0xCC, 0xE3), + Color.FromArgb(0xFF, 0x54, 0x99, 0xC7), + Color.FromArgb(0xFF, 0x24, 0x71, 0xA3), + Color.FromArgb(0xFF, 0x1A, 0x52, 0x76), + }, + + // Turquoise + { + Color.FromArgb(0xFF, 0xE8, 0xF8, 0xF5), + Color.FromArgb(0xFF, 0xA3, 0xE4, 0xD7), + Color.FromArgb(0xFF, 0x48, 0xC9, 0xB0), + Color.FromArgb(0xFF, 0x17, 0xA5, 0x89), + Color.FromArgb(0xFF, 0x11, 0x78, 0x64), + }, + + // Nephritis + { + Color.FromArgb(0xFF, 0xE9, 0xF7, 0xEF), + Color.FromArgb(0xFF, 0xA9, 0xDF, 0xBF), + Color.FromArgb(0xFF, 0x52, 0xBE, 0x80), + Color.FromArgb(0xFF, 0x22, 0x99, 0x54), + Color.FromArgb(0xFF, 0x19, 0x6F, 0x3D), + }, + + // Sunflower + { + Color.FromArgb(0xFF, 0xFE, 0xF9, 0xE7), + Color.FromArgb(0xFF, 0xF9, 0xE7, 0x9F), + Color.FromArgb(0xFF, 0xF4, 0xD0, 0x3F), + Color.FromArgb(0xFF, 0xD4, 0xAC, 0x0D), + Color.FromArgb(0xFF, 0x9A, 0x7D, 0x0A), + }, + + // Carrot + { + Color.FromArgb(0xFF, 0xFD, 0xF2, 0xE9), + Color.FromArgb(0xFF, 0xF5, 0xCB, 0xA7), + Color.FromArgb(0xFF, 0xEB, 0x98, 0x4E), + Color.FromArgb(0xFF, 0xCA, 0x6F, 0x1E), + Color.FromArgb(0xFF, 0x93, 0x51, 0x16), + }, + + // Clouds + { + Color.FromArgb(0xFF, 0xFD, 0xFE, 0xFE), + Color.FromArgb(0xFF, 0xF7, 0xF9, 0xF9), + Color.FromArgb(0xFF, 0xF0, 0xF3, 0xF4), + Color.FromArgb(0xFF, 0xD0, 0xD3, 0xD4), + Color.FromArgb(0xFF, 0x97, 0x9A, 0x9A), + }, + + // Concrete + { + Color.FromArgb(0xFF, 0xF4, 0xF6, 0xF6), + Color.FromArgb(0xFF, 0xD5, 0xDB, 0xDB), + Color.FromArgb(0xFF, 0xAA, 0xB7, 0xB8), + Color.FromArgb(0xFF, 0x83, 0x91, 0x92), + Color.FromArgb(0xFF, 0x5F, 0x6A, 0x6A), + }, + + // Wet Asphalt + { + Color.FromArgb(0xFF, 0xEB, 0xED, 0xEF), + Color.FromArgb(0xFF, 0xAE, 0xB6, 0xBF), + Color.FromArgb(0xFF, 0x5D, 0x6D, 0x7E), + Color.FromArgb(0xFF, 0x2E, 0x40, 0x53), + Color.FromArgb(0xFF, 0x21, 0x2F, 0x3C), + }, + }; + + /// + /// Gets the index of the default shade of colors in this palette. + /// + public const int DefaultShadeIndex = 2; + + /// + /// The index in the color palette of the 'Pomegranate' color. + /// This index can correspond to multiple color shades. + /// + public const int PomegranateIndex = 0; + + /// + /// The index in the color palette of the 'Amethyst' color. + /// This index can correspond to multiple color shades. + /// + public const int AmethystIndex = 1; + + /// + /// The index in the color palette of the 'BelizeHole' color. + /// This index can correspond to multiple color shades. + /// + public const int BelizeHoleIndex = 2; + + /// + /// The index in the color palette of the 'Turquoise' color. + /// This index can correspond to multiple color shades. + /// + public const int TurquoiseIndex = 3; + + /// + /// The index in the color palette of the 'Nephritis' color. + /// This index can correspond to multiple color shades. + /// + public const int NephritisIndex = 4; + + /// + /// The index in the color palette of the 'Sunflower' color. + /// This index can correspond to multiple color shades. + /// + public const int SunflowerIndex = 5; + + /// + /// The index in the color palette of the 'Carrot' color. + /// This index can correspond to multiple color shades. + /// + public const int CarrotIndex = 6; + + /// + /// The index in the color palette of the 'Clouds' color. + /// This index can correspond to multiple color shades. + /// + public const int CloudsIndex = 7; + + /// + /// The index in the color palette of the 'Concrete' color. + /// This index can correspond to multiple color shades. + /// + public const int ConcreteIndex = 8; + + /// + /// The index in the color palette of the 'WetAsphalt' color. + /// This index can correspond to multiple color shades. + /// + public const int WetAsphaltIndex = 9; + + /// + public int ColorCount + { + // Table is transposed compared to the reference chart + get => colorChart.GetLength(0); + } + + /// + public int ShadeCount + { + // Table is transposed compared to the reference chart + get => colorChart.GetLength(1); + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFC0392B. + /// + public static Color Pomegranate + { + get => colorChart[PomegranateIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF9B59B6. + /// + public static Color Amethyst + { + get => colorChart[AmethystIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF2980B9. + /// + public static Color BelizeHole + { + get => colorChart[BelizeHoleIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF1ABC9C. + /// + public static Color Turquoise + { + get => colorChart[TurquoiseIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF27AE60. + /// + public static Color Nephritis + { + get => colorChart[NephritisIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFF1C40F. + /// + public static Color Sunflower + { + get => colorChart[SunflowerIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFE67E22. + /// + public static Color Carrot + { + get => colorChart[CarrotIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFECF0F1. + /// + public static Color Clouds + { + get => colorChart[CloudsIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF95A5A6. + /// + public static Color Concrete + { + get => colorChart[ConcreteIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF34495E. + /// + public static Color WetAsphalt + { + get => colorChart[WetAsphaltIndex, DefaultShadeIndex]; + } + + /// + public Color GetColor(int colorIndex, int shadeIndex) + { + // Table is transposed compared to the reference chart + return colorChart[ + MathUtilities.Clamp(colorIndex, 0, colorChart.GetLength(0) - 1), + MathUtilities.Clamp(shadeIndex, 0, colorChart.GetLength(1) - 1)]; + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs index b6f9a244b1..013e69ce20 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs @@ -103,12 +103,6 @@ namespace Avalonia.Controls } }; - /// - /// Gets the index of the default shade of colors in this palette. - /// This has little meaning in this palette as colors are not strictly separated by shade. - /// - public const int DefaultShadeIndex = 0; - /// /// Gets the total number of colors in this palette. /// A color is not necessarily a single value and may be composed of several shades. From fe2d51b111696298e35c3db629751006f8e5ceec Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 2 Jun 2022 22:42:56 -0400 Subject: [PATCH 064/389] Add SixteenColorPalette --- .../ColorPalette/SixteenColorPalette.cs | 302 ++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100644 src/Avalonia.Controls.ColorPicker/ColorPalette/SixteenColorPalette.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalette/SixteenColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalette/SixteenColorPalette.cs new file mode 100644 index 0000000000..f3abfdfd7f --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorPalette/SixteenColorPalette.cs @@ -0,0 +1,302 @@ +using Avalonia.Media; +using Avalonia.Utilities; + +namespace Avalonia.Controls +{ + /// + /// Implements the standard sixteen color palette from the HTML 4.01 specification. + /// + /// + /// See https://en.wikipedia.org/wiki/Web_colors#HTML_color_names. + /// + public class SixteenColorPalette : IColorPalette + { + // The 16 standard colors from HTML and early Windows computers + // https://en.wikipedia.org/wiki/List_of_software_palettes + // https://en.wikipedia.org/wiki/Web_colors#HTML_color_names + private static Color[,] colorChart = new Color[,] + { + { + Colors.White, + Colors.Silver + }, + { + Colors.Gray, + Colors.Black + }, + { + Colors.Red, + Colors.Maroon + }, + { + Colors.Yellow, + Colors.Olive + }, + { + Colors.Lime, + Colors.Green + }, + { + Colors.Aqua, + Colors.Teal + }, + { + Colors.Blue, + Colors.Navy + }, + { + Colors.Fuchsia, + Colors.Purple + } + }; + + /// + /// Gets the index of the default shade of colors in this palette. + /// + public const int DefaultShadeIndex = 0; + + /// + /// The index in the color palette of the 'White' color. + /// This index can correspond to multiple color shades. + /// + public const int WhiteIndex = 0; + + /// + /// The index in the color palette of the 'Silver' color. + /// This index can correspond to multiple color shades. + /// + public const int SilverIndex = 1; + + /// + /// The index in the color palette of the 'Gray' color. + /// This index can correspond to multiple color shades. + /// + public const int GrayIndex = 2; + + /// + /// The index in the color palette of the 'Black' color. + /// This index can correspond to multiple color shades. + /// + public const int BlackIndex = 3; + + /// + /// The index in the color palette of the 'Red' color. + /// This index can correspond to multiple color shades. + /// + public const int RedIndex = 4; + + /// + /// The index in the color palette of the 'Maroon' color. + /// This index can correspond to multiple color shades. + /// + public const int MaroonIndex = 5; + + /// + /// The index in the color palette of the 'Yellow' color. + /// This index can correspond to multiple color shades. + /// + public const int YellowIndex = 6; + + /// + /// The index in the color palette of the 'Olive' color. + /// This index can correspond to multiple color shades. + /// + public const int OliveIndex = 7; + + /// + /// The index in the color palette of the 'Lime' color. + /// This index can correspond to multiple color shades. + /// + public const int LimeIndex = 8; + + /// + /// The index in the color palette of the 'Green' color. + /// This index can correspond to multiple color shades. + /// + public const int GreenIndex = 9; + + /// + /// The index in the color palette of the 'Aqua' color. + /// This index can correspond to multiple color shades. + /// + public const int AquaIndex = 10; + + /// + /// The index in the color palette of the 'Teal' color. + /// This index can correspond to multiple color shades. + /// + public const int TealIndex = 11; + + /// + /// The index in the color palette of the 'Blue' color. + /// This index can correspond to multiple color shades. + /// + public const int BlueIndex = 12; + + /// + /// The index in the color palette of the 'Navy' color. + /// This index can correspond to multiple color shades. + /// + public const int NavyIndex = 13; + + /// + /// The index in the color palette of the 'Fuchsia' color. + /// This index can correspond to multiple color shades. + /// + public const int FuchsiaIndex = 14; + + /// + /// The index in the color palette of the 'Purple' color. + /// This index can correspond to multiple color shades. + /// + public const int PurpleIndex = 15; + + /// + public int ColorCount + { + get => colorChart.GetLength(0); + } + + /// + public int ShadeCount + { + get => colorChart.GetLength(1); + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFFFFFFF. + /// + public static Color White + { + get => colorChart[WhiteIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFC0C0C0. + /// + public static Color Silver + { + get => colorChart[SilverIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF808080. + /// + public static Color Gray + { + get => colorChart[GrayIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF000000. + /// + public static Color Black + { + get => colorChart[BlackIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFFF0000. + /// + public static Color Red + { + get => colorChart[RedIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF800000. + /// + public static Color Maroon + { + get => colorChart[MaroonIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFFFFF00. + /// + public static Color Yellow + { + get => colorChart[YellowIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF808000. + /// + public static Color Olive + { + get => colorChart[OliveIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF00FF00. + /// + public static Color Lime + { + get => colorChart[LimeIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF008000. + /// + public static Color Green + { + get => colorChart[GreenIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF00FFFF. + /// + public static Color Aqua + { + get => colorChart[AquaIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF008080. + /// + public static Color Teal + { + get => colorChart[TealIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF0000FF. + /// + public static Color Blue + { + get => colorChart[BlueIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF000080. + /// + public static Color Navy + { + get => colorChart[NavyIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFFF00FF. + /// + public static Color Fuchsia + { + get => colorChart[FuchsiaIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF800080. + /// + public static Color Purple + { + get => colorChart[PurpleIndex, DefaultShadeIndex]; + } + + /// + public Color GetColor(int colorIndex, int shadeIndex) + { + return colorChart[ + MathUtilities.Clamp(colorIndex, 0, colorChart.GetLength(0) - 1), + MathUtilities.Clamp(shadeIndex, 0, colorChart.GetLength(1) - 1)]; + } + } +} From 5e8f0fb70119e88335a555c80082311c9ef26664 Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 2 Jun 2022 23:00:08 -0400 Subject: [PATCH 065/389] Add comment explaining rounding value selection --- src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs index 32a898ee71..7dc340ea16 100644 --- a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs @@ -64,6 +64,10 @@ namespace Avalonia.Controls.Primitives // It is also needlessly large as there are only ~140 known/named colors. // Therefore, rounding of the input color's component values is done to // reduce the color space into something more useful. + // + // The rounding value of 5 is specially chosen. + // It is a factor of 255 and therefore evenly divisible which improves + // the quality of the calculations. double rounding = 5; var roundedColor = new Color( 0xFF, From 4dcf13623d7e78698141f5f64742ec949e62afbb Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 2 Jun 2022 23:02:18 -0400 Subject: [PATCH 066/389] Make ColorPalettes directory plural --- .../{ColorPalette => ColorPalettes}/FlatColorPalette.cs | 0 .../{ColorPalette => ColorPalettes}/FluentColorPalette.cs | 0 .../{ColorPalette => ColorPalettes}/IColorPalette.cs | 0 .../{ColorPalette => ColorPalettes}/SixteenColorPalette.cs | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename src/Avalonia.Controls.ColorPicker/{ColorPalette => ColorPalettes}/FlatColorPalette.cs (100%) rename src/Avalonia.Controls.ColorPicker/{ColorPalette => ColorPalettes}/FluentColorPalette.cs (100%) rename src/Avalonia.Controls.ColorPicker/{ColorPalette => ColorPalettes}/IColorPalette.cs (100%) rename src/Avalonia.Controls.ColorPicker/{ColorPalette => ColorPalettes}/SixteenColorPalette.cs (100%) diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalette/FlatColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalettes/FlatColorPalette.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorPalette/FlatColorPalette.cs rename to src/Avalonia.Controls.ColorPicker/ColorPalettes/FlatColorPalette.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalettes/FluentColorPalette.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorPalette/FluentColorPalette.cs rename to src/Avalonia.Controls.ColorPicker/ColorPalettes/FluentColorPalette.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalette/IColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalettes/IColorPalette.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorPalette/IColorPalette.cs rename to src/Avalonia.Controls.ColorPicker/ColorPalettes/IColorPalette.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalette/SixteenColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalettes/SixteenColorPalette.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorPalette/SixteenColorPalette.cs rename to src/Avalonia.Controls.ColorPicker/ColorPalettes/SixteenColorPalette.cs From 63d1bdec3c3b05964b4b3f158d3dfde6ddbc4bcf Mon Sep 17 00:00:00 2001 From: robloo Date: Thu, 2 Jun 2022 23:08:24 -0400 Subject: [PATCH 067/389] Add resources to control accent color section size in ColorPreviewer --- .../Themes/Default/ColorPreviewer.xaml | 10 ++++++---- .../Themes/Fluent/ColorPreviewer.xaml | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml index 15e5ca1655..ac1531fe7a 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml @@ -9,6 +9,8 @@ + 80 + 40 + Date: Fri, 3 Jun 2022 14:32:19 +0200 Subject: [PATCH 071/389] Fix nested :not selector. --- src/Avalonia.Base/Styling/NotSelector.cs | 2 +- .../Styling/SelectorTests_Nesting.cs | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Styling/NotSelector.cs b/src/Avalonia.Base/Styling/NotSelector.cs index cdc3254d38..76a0690e96 100644 --- a/src/Avalonia.Base/Styling/NotSelector.cs +++ b/src/Avalonia.Base/Styling/NotSelector.cs @@ -67,6 +67,6 @@ namespace Avalonia.Styling } protected override Selector? MovePrevious() => _previous; - internal override bool HasValidNestingSelector() => _argument.HasValidNestingSelector(); + internal override bool HasValidNestingSelector() => _previous?.HasValidNestingSelector() ?? false; } } diff --git a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs index d49fcf03a2..1520dc329d 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs @@ -257,6 +257,30 @@ namespace Avalonia.Base.UnitTests.Styling parent.Children.Add(child); } + + [Fact] + public void Nesting_Not_Class_Matches() + { + var control = new Control1 { Classes = { "foo" } }; + Style nested; + var parent = new Style(x => x.OfType()) + { + Children = + { + (nested = new Style(x => x.Nesting().Not(y => y.Class("foo")))), + } + }; + + var match = nested.Selector.Match(control, parent); + Assert.Equal(SelectorMatchResult.Sometimes, match.Result); + + var sink = new ActivatorSink(match.Activator); + + Assert.False(sink.Active); + control.Classes.Clear(); + Assert.True(sink.Active); + } + public class Control1 : Control { } From 0d7adc70d9e69334d2109b04a8d2ebade8c6461b Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 3 Jun 2022 15:47:01 +0300 Subject: [PATCH 072/389] Implemented ZIndex support --- .../Composition/CompositingRenderer.cs | 69 ++++++++++++++++--- .../Rendering/ImmediateRenderer.cs | 6 +- .../Rendering/SceneGraph/SceneBuilder.cs | 44 +++++++----- src/Avalonia.Base/Visual.cs | 8 +++ src/Avalonia.Base/VisualTree/IVisual.cs | 5 ++ 5 files changed, 104 insertions(+), 28 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index b83f804a8f..5aded281b3 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Numerics; using System.Runtime.InteropServices; using Avalonia.Collections; +using Avalonia.Collections.Pooled; using Avalonia.Media; using Avalonia.Rendering.Composition.Drawing; using Avalonia.Rendering.Composition.Server; @@ -100,26 +101,74 @@ public class CompositingRenderer : RendererBase, IRendererWithCompositor return; var compositionChildren = v.CompositionVisual.Children; var visualChildren = (AvaloniaList)v.GetVisualChildren(); + + PooledList<(IVisual visual, int index)>? sortedChildren = null; + if (v.HasNonUniformZIndexChildren && visualChildren.Count > 1) + { + sortedChildren = new (visualChildren.Count); + for (var c = 0; c < visualChildren.Count; c++) + sortedChildren.Add((visualChildren[c], c)); + + // Regular Array.Sort is unstable, we need to provide indices as well to avoid reshuffling elements. + sortedChildren.Sort(static (lhs, rhs) => + { + var result = lhs.visual.ZIndex.CompareTo(rhs.visual.ZIndex); + return result == 0 ? lhs.index.CompareTo(rhs.index) : result; + }); + } + if (compositionChildren.Count == visualChildren.Count) { bool mismatch = false; - for(var c=0; c x, ZIndexComparer.Instance)) + var childrenEnumerable = visual.HasNonUniformZIndexChildren + ? visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance) + : (IEnumerable)visual.VisualChildren; + + foreach (var child in childrenEnumerable) { var childBounds = GetTransformedBounds(child); diff --git a/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs index 019c3e0e9b..cc18400a4c 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs @@ -272,26 +272,36 @@ namespace Avalonia.Rendering.SceneGraph else if (visualChildren.Count > 1) { var count = visualChildren.Count; - var sortedChildren = new (IVisual visual, int index)[count]; - for (var i = 0; i < count; i++) + if (visual.HasNonUniformZIndexChildren) { - sortedChildren[i] = (visualChildren[i], i); - } - - // Regular Array.Sort is unstable, we need to provide indices as well to avoid reshuffling elements. - Array.Sort(sortedChildren, (lhs, rhs) => - { - var result = ZIndexComparer.Instance.Compare(lhs.visual, rhs.visual); - - return result == 0 ? lhs.index.CompareTo(rhs.index) : result; - }); - - foreach (var child in sortedChildren) - { - var childNode = GetOrCreateChildNode(scene, child.Item1, node); - Update(context, scene, (VisualNode)childNode, clip, forceRecurse); + var sortedChildren = new (IVisual visual, int index)[count]; + + for (var i = 0; i < count; i++) + { + sortedChildren[i] = (visualChildren[i], i); + } + + // Regular Array.Sort is unstable, we need to provide indices as well to avoid reshuffling elements. + Array.Sort(sortedChildren, (lhs, rhs) => + { + var result = ZIndexComparer.Instance.Compare(lhs.visual, rhs.visual); + + return result == 0 ? lhs.index.CompareTo(rhs.index) : result; + }); + + foreach (var child in sortedChildren) + { + var childNode = GetOrCreateChildNode(scene, child.Item1, node); + Update(context, scene, (VisualNode)childNode, clip, forceRecurse); + } } + else + foreach (var child in visualChildren) + { + var childNode = GetOrCreateChildNode(scene, child, node); + Update(context, scene, (VisualNode)childNode, clip, forceRecurse); + } } node.SubTreeUpdated = true; diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 64341be0c7..c11ea2c40a 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -293,6 +293,8 @@ namespace Avalonia internal CompositionDrawListVisual? CompositionVisual { get; private set; } + public bool HasNonUniformZIndexChildren { get; private set; } + /// /// Gets a value indicating whether this control is attached to a visual root. /// @@ -445,6 +447,9 @@ namespace Avalonia AttachedToVisualTree?.Invoke(this, e); InvalidateVisual(); + if (ZIndex != 0 && this.GetVisualParent() is Visual parent) + parent.HasNonUniformZIndexChildren = true; + var visualChildren = VisualChildren; if (visualChildren != null) @@ -611,6 +616,9 @@ namespace Avalonia { var sender = e.Sender as IVisual; var parent = sender?.VisualParent; + if (sender?.ZIndex != 0 && parent is Visual parentVisual) + parentVisual.HasNonUniformZIndexChildren = true; + sender?.InvalidateVisual(); parent?.VisualRoot?.Renderer?.RecalculateChildren(parent); } diff --git a/src/Avalonia.Base/VisualTree/IVisual.cs b/src/Avalonia.Base/VisualTree/IVisual.cs index 3b053fab38..fdd2d187b8 100644 --- a/src/Avalonia.Base/VisualTree/IVisual.cs +++ b/src/Avalonia.Base/VisualTree/IVisual.cs @@ -79,6 +79,11 @@ namespace Avalonia.VisualTree /// Gets a value indicating whether to apply mirror transform on this control. /// bool HasMirrorTransform { get; } + + /// + /// Gets a value indicating whether to sort children when rendering this control + /// + bool HasNonUniformZIndexChildren { get; } /// /// Gets or sets the render transform of the control. From 32c1eb41e44a097898355893ce53aa6f23e2319d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 3 Jun 2022 15:58:21 +0300 Subject: [PATCH 073/389] Hit-testing clip --- .../Composition/CompositionTarget.cs | 41 ++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs index 3243934932..31431a4e36 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs @@ -79,30 +79,33 @@ namespace Avalonia.Rendering.Composition return false; if (!TryTransformTo(visual, ref point)) return false; - if (point.X >= 0 && point.Y >= 0 && point.X <= visual.Size.X && point.Y <= visual.Size.Y) + + if (visual.ClipToBounds + && (point.X < 0 || point.Y < 0 || point.X > visual.Size.X || point.Y > visual.Size.Y)) + return false; + if (visual.Clip?.FillContains(point) == false) + return false; + + bool success = false; + // Hit-test the current node + if (visual.HitTest(point, filter)) { - bool success = false; - // Hit-test the current node - if (visual.HitTest(point, filter)) + result.Add(visual); + success = true; + } + + // Inspect children too + if (visual is CompositionContainerVisual cv) + for (var c = cv.Children.Count - 1; c >= 0; c--) { - result.Add(visual); - success = true; + var ch = cv.Children[c]; + var hit = HitTestCore(ch, point, result, filter); + if (hit) + return true; } - - // Inspect children too - if(visual is CompositionContainerVisual cv) - for (var c = cv.Children.Count - 1; c >= 0; c--) - { - var ch = cv.Children[c]; - var hit = HitTestCore(ch, point, result, filter); - if (hit) - return true; - } - return success; - } + return success; - return false; } public void RequestRedraw() => RegisterForSerialization(); From 10247f3dc2f80e00584e098964715e09a8d832f0 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 3 Jun 2022 16:03:05 +0300 Subject: [PATCH 074/389] Stop animations from ticking when window is hidden --- .../Server/ServerCompositionTarget.cs | 22 +++++++++++++++++++ .../Composition/Server/ServerVisual.cs | 6 ++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 9513fb58fa..e8c417a98d 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Numerics; using System.Threading; using Avalonia.Media; @@ -24,6 +25,7 @@ namespace Avalonia.Rendering.Composition.Server private IDrawingContextLayerImpl? _layer; private bool _redrawRequested; private bool _disposed; + private HashSet _attachedVisuals = new(); public ReadbackIndices Readback { get; } = new(); @@ -39,9 +41,17 @@ namespace Avalonia.Rendering.Composition.Server partial void OnIsEnabledChanged() { if (IsEnabled) + { _compositor.AddCompositionTarget(this); + foreach (var v in _attachedVisuals) + v.Activate(); + } else + { _compositor.RemoveCompositionTarget(this); + foreach (var v in _attachedVisuals) + v.Deactivate(); + } } partial void DeserializeChangesExtra(BatchStreamReader c) @@ -155,5 +165,17 @@ namespace Avalonia.Rendering.Composition.Server _renderTarget?.Dispose(); _renderTarget = null; } + + public void AddVisual(ServerCompositionVisual visual) + { + if (_attachedVisuals.Add(visual) && IsEnabled) + visual.Activate(); + } + + public void RemoveVisual(ServerCompositionVisual visual) + { + if (_attachedVisuals.Remove(visual) && IsEnabled) + visual.Deactivate(); + } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 5717ab2f8c..5c7067bfb1 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -126,14 +126,14 @@ namespace Avalonia.Rendering.Composition.Server partial void OnRootChanging() { - if(Root != null) - Deactivate(); + if (Root != null) + Root.RemoveVisual(this); } partial void OnRootChanged() { if (Root != null) - Activate(); + Root.AddVisual(this); } protected override void ValuesInvalidated() From 6e2214b6d42e6bf8f1913d9dd5fc509098769094 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 3 Jun 2022 17:22:02 +0300 Subject: [PATCH 075/389] Added adorner support --- .../Server/ServerCompositionDrawListVisual.cs | 19 +++++++++++++++--- .../Server/ServerCompositionTarget.cs | 14 ++++++++++++- .../Server/ServerContainerVisual.cs | 18 +++++++++-------- .../Server/ServerSolidColorVisual.cs | 5 ++--- .../Composition/Server/ServerSpriteVisual.cs | 4 ++-- .../Composition/Server/ServerVisual.cs | 12 +++++++---- src/Avalonia.Base/Visual.cs | 2 +- src/Avalonia.Base/composition-schema.xml | 1 + .../Primitives/AdornerLayer.cs | 20 ++++++++++++++----- .../CompositionGenerator/Config.cs | 2 ++ .../CompositionGenerator/Generator.cs | 2 +- 11 files changed, 71 insertions(+), 28 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs index f1b5032cd3..45062632c7 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -11,10 +11,16 @@ namespace Avalonia.Rendering.Composition.Server; internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisual { +#if DEBUG + public readonly Visual UiVisual; +#endif private CompositionDrawList? _renderCommands; - public ServerCompositionDrawListVisual(ServerCompositor compositor) : base(compositor) + public ServerCompositionDrawListVisual(ServerCompositor compositor, Visual v) : base(compositor) { +#if DEBUG + UiVisual = v; +#endif } Rect? _contentBounds; @@ -47,12 +53,19 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua base.DeserializeChangesCore(reader, commitedAt); } - protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + protected override void RenderCore(CompositorDrawingContextProxy canvas) { if (_renderCommands != null) { _renderCommands.Render(canvas); } - base.RenderCore(canvas, transform); + base.RenderCore(canvas); } + +#if DEBUG + public override string ToString() + { + return UiVisual.GetType().ToString(); + } +#endif } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index e8c417a98d..fb0d6fce60 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -26,6 +26,7 @@ namespace Avalonia.Rendering.Composition.Server private bool _redrawRequested; private bool _disposed; private HashSet _attachedVisuals = new(); + private Queue _adornerUpdateQueue = new(); public ReadbackIndices Readback { get; } = new(); @@ -80,6 +81,12 @@ namespace Avalonia.Rendering.Composition.Server // Update happens in a separate phase to extend dirty rect if needed Root.Update(this, Matrix4x4.Identity); + + while (_adornerUpdateQueue.Count > 0) + { + var adorner = _adornerUpdateQueue.Dequeue(); + adorner.Update(this, adorner.AdornedVisual?.GlobalTransformMatrix ?? Matrix4x4.Identity); + } Readback.CompleteWrite(Revision); @@ -108,7 +115,7 @@ namespace Avalonia.Rendering.Composition.Server { context.PushClip(_dirtyRect); context.Clear(Colors.Transparent); - Root.Render(new CompositorDrawingContextProxy(context, visualBrushHelper), Root.CombinedTransformMatrix); + Root.Render(new CompositorDrawingContextProxy(context, visualBrushHelper)); context.PopClip(); } } @@ -147,6 +154,7 @@ namespace Avalonia.Rendering.Composition.Server { var snapped = SnapToDevicePixels(rect, Scaling); _dirtyRect = _dirtyRect.Union(snapped); + _redrawRequested = true; } public void Invalidate() @@ -176,6 +184,10 @@ namespace Avalonia.Rendering.Composition.Server { if (_attachedVisuals.Remove(visual) && IsEnabled) visual.Deactivate(); + if(visual.IsVisibleInFrame) + AddDirtyRect(visual.TransformedBounds); } + + public void EnqueueAdornerUpdate(ServerCompositionVisual visual) => _adornerUpdateQueue.Enqueue(visual); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs index a277450214..bcfcfbe4f2 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs @@ -7,24 +7,26 @@ namespace Avalonia.Rendering.Composition.Server { public ServerCompositionVisualCollection Children { get; private set; } = null!; - protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + protected override void RenderCore(CompositorDrawingContextProxy canvas) { - base.RenderCore(canvas, transform); + base.RenderCore(canvas); foreach (var ch in Children) { - var t = transform; - - t = ch.CombinedTransformMatrix * t; - ch.Render(canvas, t); + ch.Render(canvas); } } public override void Update(ServerCompositionTarget root, Matrix4x4 transform) { base.Update(root, transform); - foreach (var child in Children) - child.Update(root, GlobalTransformMatrix); + foreach (var child in Children) + { + if (child.AdornedVisual != null) + root.EnqueueAdornerUpdate(child); + else + child.Update(root, GlobalTransformMatrix); + } } partial void Initialize() diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs index 786779bb2d..5720d80304 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs @@ -6,11 +6,10 @@ namespace Avalonia.Rendering.Composition.Server { internal partial class ServerCompositionSolidColorVisual { - protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + protected override void RenderCore(CompositorDrawingContextProxy canvas) { - canvas.Transform = MatrixUtils.ToMatrix(transform); canvas.DrawRectangle(new ImmutableSolidColorBrush(Color), null, new RoundedRect(new Rect(new Size(Size)))); - base.RenderCore(canvas, transform); + base.RenderCore(canvas); } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs index c658dc8ae3..2f4c446cfa 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs @@ -6,7 +6,7 @@ namespace Avalonia.Rendering.Composition.Server internal partial class ServerCompositionSpriteVisual { - protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + protected override void RenderCore(CompositorDrawingContextProxy canvas) { if (Brush != null) { @@ -14,7 +14,7 @@ namespace Avalonia.Rendering.Composition.Server //canvas.FillRect((Vector2)Size, (ICbBrush)Brush.Brush!); } - base.RenderCore(canvas, transform); + base.RenderCore(canvas); } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 5c7067bfb1..0b2ba6922b 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -8,17 +8,18 @@ namespace Avalonia.Rendering.Composition.Server { private bool _isDirty; private bool _isBackface; - protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + protected virtual void RenderCore(CompositorDrawingContextProxy canvas) { } - public void Render(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + public void Render(CompositorDrawingContextProxy canvas) { if(Visible == false) return; if(Opacity == 0) return; + var transform = GlobalTransformMatrix; canvas.PreTransform = MatrixUtils.ToMatrix(transform); canvas.Transform = Matrix.Identity; if (Opacity != 1) @@ -29,8 +30,9 @@ namespace Avalonia.Rendering.Composition.Server canvas.PushGeometryClip(Clip); //TODO: Check clip - RenderCore(canvas, transform); + RenderCore(canvas); + // Hack to force invalidation of SKMatrix canvas.PreTransform = MatrixUtils.ToMatrix(transform); canvas.Transform = Matrix.Identity; @@ -60,7 +62,9 @@ namespace Avalonia.Rendering.Composition.Server public virtual void Update(ServerCompositionTarget root, Matrix4x4 transform) { // Calculate new parent-relative transform - CombinedTransformMatrix = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, + CombinedTransformMatrix = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, + // HACK: Ignore RenderTransform set by the adorner layer + AdornedVisual != null ? Matrix4x4.Identity : TransformMatrix, Scale, RotationAngle, Orientation, Offset); var newTransform = CombinedTransformMatrix * transform; diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index c11ea2c40a..2f4fbbd3f0 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -470,7 +470,7 @@ namespace Avalonia { if (CompositionVisual == null || CompositionVisual.Compositor != compositor) CompositionVisual = new CompositionDrawListVisual(compositor, - new ServerCompositionDrawListVisual(compositor.Server), this); + new ServerCompositionDrawListVisual(compositor.Server, this), this); return CompositionVisual; } diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml index a7ae341bb3..b86ce35d5f 100644 --- a/src/Avalonia.Base/composition-schema.xml +++ b/src/Avalonia.Base/composition-schema.xml @@ -22,6 +22,7 @@ + diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index 5ad4e39baf..57fb7226e8 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -164,6 +164,9 @@ namespace Avalonia.Controls.Primitives private void UpdateAdornedElement(Visual adorner, Visual? adorned) { + if (adorner.CompositionVisual != null) + adorner.CompositionVisual.AdornedVisual = adorned?.CompositionVisual; + var info = adorner.GetValue(s_adornedElementInfoProperty); if (info != null) @@ -184,11 +187,18 @@ namespace Avalonia.Controls.Primitives adorner.SetValue(s_adornedElementInfoProperty, info); } - info.Subscription = adorned.GetObservable(TransformedBoundsProperty).Subscribe(x => - { - info.Bounds = x; - InvalidateMeasure(); - }); + if (adorner.CompositionVisual != null) + info.Subscription = adorned.GetObservable(BoundsProperty).Subscribe(x => + { + info.Bounds = new TransformedBounds(new Rect(adorned.Bounds.Size), new Rect(adorned.Bounds.Size), Matrix.Identity); + InvalidateMeasure(); + }); + else + info.Subscription = adorned.GetObservable(TransformedBoundsProperty).Subscribe(x => + { + info.Bounds = x; + InvalidateMeasure(); + }); } } diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs index 096864e52a..d1fc691a8b 100644 --- a/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs @@ -110,6 +110,8 @@ namespace Avalonia.SourceGenerator.CompositionGenerator public bool Animated { get; set; } [XmlAttribute] public bool InternalSet { get; set; } + [XmlAttribute] + public bool Internal { get; set; } } public class GAnimationType diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs index 3c38c0331e..f8f86265cf 100644 --- a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs @@ -378,7 +378,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator return client .AddMembers(DeclareField(prop.Type, fieldName)) .AddMembers(PropertyDeclaration(propType, prop.Name) - .AddModifiers(SyntaxKind.PublicKeyword) + .AddModifiers(prop.Internal ? SyntaxKind.InternalKeyword : SyntaxKind.PublicKeyword) .AddAccessorListAccessors( AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, Block(ReturnStatement(IdentifierName(fieldName)))), From 223a67543335365de9e7746ad5e9ad568a64f319 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 3 Jun 2022 18:50:32 +0300 Subject: [PATCH 076/389] IPlatformGpu --- src/Avalonia.Base/Platform/IPlatformGpu.cs | 16 +++++ .../Rendering/Composition/Compositor.cs | 4 +- .../Server/ServerCompositionTarget.cs | 22 +++---- .../Composition/Server/ServerCompositor.cs | 6 +- .../AvaloniaNativePlatformOpenGlInterface.cs | 2 + .../Egl/EglPlatformOpenGlInterface.cs | 2 + src/Avalonia.OpenGL/IGlContext.cs | 3 +- .../IPlatformOpenGlInterface.cs | 6 +- src/Avalonia.X11/Glx/GlxPlatformFeature.cs | 2 + src/Avalonia.X11/X11Platform.cs | 6 +- .../OpenGl/WglPlatformOpenGlInterface.cs | 2 + src/Windows/Avalonia.Win32/Win32GlManager.cs | 61 +++++++++++-------- src/Windows/Avalonia.Win32/Win32Platform.cs | 6 +- src/iOS/Avalonia.iOS/EaglDisplay.cs | 2 + 14 files changed, 92 insertions(+), 48 deletions(-) create mode 100644 src/Avalonia.Base/Platform/IPlatformGpu.cs diff --git a/src/Avalonia.Base/Platform/IPlatformGpu.cs b/src/Avalonia.Base/Platform/IPlatformGpu.cs new file mode 100644 index 0000000000..0507dea1d7 --- /dev/null +++ b/src/Avalonia.Base/Platform/IPlatformGpu.cs @@ -0,0 +1,16 @@ +using System; +using Avalonia.Metadata; + +namespace Avalonia.Platform; + +[Unstable] +public interface IPlatformGpu +{ + IPlatformGpuContext PrimaryContext { get; } +} + +[Unstable] +public interface IPlatformGpuContext : IDisposable +{ + IDisposable EnsureCurrent(); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index 96564f0800..03aebba907 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -24,9 +24,9 @@ namespace Avalonia.Rendering.Composition internal ServerCompositor Server => _server; internal CompositionEasingFunction DefaultEasing { get; } - public Compositor(IRenderLoop loop) + public Compositor(IRenderLoop loop, IPlatformGpu? gpu) { - _server = new ServerCompositor(loop, _batchObjectPool, _batchMemoryPool); + _server = new ServerCompositor(loop, gpu, _batchObjectPool, _batchMemoryPool); _implicitBatchCommit = ImplicitBatchCommit; DefaultEasing = new CubicBezierEasingFunction(this, new Vector2(0.25f, 0.1f), new Vector2(0.25f, 1f)); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index fb0d6fce60..d1d7e421a4 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -93,12 +93,6 @@ namespace Avalonia.Rendering.Composition.Server _redrawRequested = false; using (var targetContext = _renderTarget.CreateDrawingContext(null)) { - // This is a hack to safely dispose layer created by some other render target - // because we can only dispose layers with the corresponding GPU context being - // active on the current thread - while (Compositor.LayersToDispose.Count > 0) - Compositor.LayersToDispose.Dequeue().Dispose(); - var layerSize = Size * Scaling; if (layerSize != _layerSize || _layer == null) { @@ -164,14 +158,20 @@ namespace Avalonia.Rendering.Composition.Server public void Dispose() { + if(_disposed) + return; _disposed = true; - if (_layer != null) + using (_compositor.GpuContext?.EnsureCurrent()) { - Compositor.LayersToDispose.Enqueue(_layer); - _layer = null; + if (_layer != null) + { + _layer.Dispose(); + _layer = null; + } + + _renderTarget?.Dispose(); + _renderTarget = null; } - _renderTarget?.Dispose(); - _renderTarget = null; } public void AddVisual(ServerCompositionVisual visual) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index 241be479ff..c44150f756 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -20,10 +20,12 @@ namespace Avalonia.Rendering.Composition.Server private List _animationsToUpdate = new(); private BatchStreamObjectPool _batchObjectPool; private BatchStreamMemoryPool _batchMemoryPool; - public Queue LayersToDispose { get; } = new(); + public IPlatformGpuContext? GpuContext { get; } - public ServerCompositor(IRenderLoop renderLoop, BatchStreamObjectPool batchObjectPool, BatchStreamMemoryPool batchMemoryPool) + public ServerCompositor(IRenderLoop renderLoop, IPlatformGpu? platformGpu, + BatchStreamObjectPool batchObjectPool, BatchStreamMemoryPool batchMemoryPool) { + GpuContext = platformGpu?.PrimaryContext; _renderLoop = renderLoop; _batchObjectPool = batchObjectPool; _batchMemoryPool = batchMemoryPool; diff --git a/src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs b/src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs index 3b3d8836fd..14d27a90e9 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs @@ -3,6 +3,7 @@ using Avalonia.OpenGL; using Avalonia.Native.Interop; using System.Drawing; using Avalonia.OpenGL.Surfaces; +using Avalonia.Platform; using Avalonia.Threading; namespace Avalonia.Native @@ -37,6 +38,7 @@ namespace Avalonia.Native internal GlContext MainContext { get; } public IGlContext PrimaryContext => MainContext; + IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; public bool CanShareContexts => true; public bool CanCreateContexts => true; diff --git a/src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs b/src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs index 476f65a774..a6d8c1e98d 100644 --- a/src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs +++ b/src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Logging; +using Avalonia.Platform; using static Avalonia.OpenGL.Egl.EglConsts; namespace Avalonia.OpenGL.Egl @@ -12,6 +13,7 @@ namespace Avalonia.OpenGL.Egl public EglContext PrimaryEglContext { get; } public IGlContext PrimaryContext => PrimaryEglContext; + IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; public EglPlatformOpenGlInterface(EglDisplay display) { diff --git a/src/Avalonia.OpenGL/IGlContext.cs b/src/Avalonia.OpenGL/IGlContext.cs index 50868db873..a52a6535da 100644 --- a/src/Avalonia.OpenGL/IGlContext.cs +++ b/src/Avalonia.OpenGL/IGlContext.cs @@ -1,8 +1,9 @@ using System; +using Avalonia.Platform; namespace Avalonia.OpenGL { - public interface IGlContext : IDisposable + public interface IGlContext : IPlatformGpuContext { GlVersion Version { get; } GlInterface GlInterface { get; } diff --git a/src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs b/src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs index 5ee5df1e85..4ff7997b03 100644 --- a/src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs +++ b/src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs @@ -1,8 +1,10 @@ +using Avalonia.Platform; + namespace Avalonia.OpenGL { - public interface IPlatformOpenGlInterface + public interface IPlatformOpenGlInterface : IPlatformGpu { - IGlContext PrimaryContext { get; } + new IGlContext PrimaryContext { get; } IGlContext CreateSharedContext(); bool CanShareContexts { get; } bool CanCreateContexts { get; } diff --git a/src/Avalonia.X11/Glx/GlxPlatformFeature.cs b/src/Avalonia.X11/Glx/GlxPlatformFeature.cs index 6735a32ffe..0968adc799 100644 --- a/src/Avalonia.X11/Glx/GlxPlatformFeature.cs +++ b/src/Avalonia.X11/Glx/GlxPlatformFeature.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using Avalonia.Logging; using Avalonia.OpenGL; +using Avalonia.Platform; namespace Avalonia.X11.Glx { @@ -14,6 +15,7 @@ namespace Avalonia.X11.Glx public IGlContext CreateSharedContext() => Display.CreateContext(PrimaryContext); public GlxContext DeferredContext { get; private set; } public IGlContext PrimaryContext => DeferredContext; + IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; public static bool TryInitialize(X11Info x11, IList glProfiles) { diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 9c82288c8e..d882450259 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -103,8 +103,12 @@ namespace Avalonia.X11 GlxPlatformOpenGlInterface.TryInitialize(Info, Options.GlProfiles); } + var gl = AvaloniaLocator.Current.GetService(); + if (gl != null) + AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl); + if (options.UseCompositor) - Compositor = new Compositor(AvaloniaLocator.Current.GetService()!); + Compositor = new Compositor(AvaloniaLocator.Current.GetService()!, gl); } diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs b/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs index b948495b99..1d0880a468 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs @@ -2,12 +2,14 @@ using System; using System.Linq; using Avalonia.Logging; using Avalonia.OpenGL; +using Avalonia.Platform; namespace Avalonia.Win32.OpenGl { class WglPlatformOpenGlInterface : IPlatformOpenGlInterface { public WglContext PrimaryContext { get; } + IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; IGlContext IPlatformOpenGlInterface.PrimaryContext => PrimaryContext; public IGlContext CreateSharedContext() => WglDisplay.CreateContext(new[] { PrimaryContext.Version }, PrimaryContext); diff --git a/src/Windows/Avalonia.Win32/Win32GlManager.cs b/src/Windows/Avalonia.Win32/Win32GlManager.cs index 2a3f4a3384..39a742d1ac 100644 --- a/src/Windows/Avalonia.Win32/Win32GlManager.cs +++ b/src/Windows/Avalonia.Win32/Win32GlManager.cs @@ -1,6 +1,7 @@ using Avalonia.OpenGL; using Avalonia.OpenGL.Angle; using Avalonia.OpenGL.Egl; +using Avalonia.Platform; using Avalonia.Win32.OpenGl; using Avalonia.Win32.WinRT.Composition; @@ -9,45 +10,53 @@ namespace Avalonia.Win32 static class Win32GlManager { - public static void Initialize() + public static IPlatformOpenGlInterface Initialize() { - AvaloniaLocator.CurrentMutable.Bind().ToLazy(() => + var gl = InitializeCore(); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl); + return gl; + } + + static IPlatformOpenGlInterface InitializeCore() + { + + var opts = AvaloniaLocator.Current.GetService() ?? new Win32PlatformOptions(); + if (opts.UseWgl) { - var opts = AvaloniaLocator.Current.GetService() ?? new Win32PlatformOptions(); - if (opts.UseWgl) - { - var wgl = WglPlatformOpenGlInterface.TryCreate(); - return wgl; - } + var wgl = WglPlatformOpenGlInterface.TryCreate(); + return wgl; + } - if (opts.AllowEglInitialization ?? Win32Platform.WindowsVersion > PlatformConstants.Windows7) - { - var egl = EglPlatformOpenGlInterface.TryCreate(() => new AngleWin32EglDisplay()); + if (opts.AllowEglInitialization ?? Win32Platform.WindowsVersion > PlatformConstants.Windows7) + { + var egl = EglPlatformOpenGlInterface.TryCreate(() => new AngleWin32EglDisplay()); - if (egl != null) + if (egl != null) + { + if (opts.EglRendererBlacklist != null) { - if (opts.EglRendererBlacklist != null) + foreach (var item in opts.EglRendererBlacklist) { - foreach (var item in opts.EglRendererBlacklist) + if (egl.PrimaryEglContext.GlInterface.Renderer.Contains(item)) { - if (egl.PrimaryEglContext.GlInterface.Renderer.Contains(item)) - { - return null; - } + return null; } } - - if (opts.UseWindowsUIComposition) - { - WinUICompositorConnection.TryCreateAndRegister(egl, opts.CompositionBackdropCornerRadius); - } } - return egl; + if (opts.UseWindowsUIComposition) + { + WinUICompositorConnection.TryCreateAndRegister(egl, opts.CompositionBackdropCornerRadius); + } } - return null; - }); + return egl; + } + + return null; } + + } } diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 4b0350f40f..af38a92e26 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -180,15 +180,15 @@ namespace Avalonia.Win32 .Bind().ToConstant(new WindowsMountedVolumeInfoProvider()) .Bind().ToConstant(s_instance); - Win32GlManager.Initialize(); + var gl = Win32GlManager.Initialize(); _uiThread = Thread.CurrentThread; if (OleContext.Current != null) AvaloniaLocator.CurrentMutable.Bind().ToSingleton(); - + if (Options.UseCompositor) - Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService()); + Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService(), gl); } public bool HasMessages() diff --git a/src/iOS/Avalonia.iOS/EaglDisplay.cs b/src/iOS/Avalonia.iOS/EaglDisplay.cs index bd1969081d..906bbc29e7 100644 --- a/src/iOS/Avalonia.iOS/EaglDisplay.cs +++ b/src/iOS/Avalonia.iOS/EaglDisplay.cs @@ -1,6 +1,7 @@ using System; using System.Reactive.Disposables; using Avalonia.OpenGL; +using Avalonia.Platform; using OpenGLES; namespace Avalonia.iOS @@ -9,6 +10,7 @@ namespace Avalonia.iOS { public IGlContext PrimaryContext => Context; public IGlContext CreateSharedContext() => throw new NotSupportedException(); + IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; public bool CanShareContexts => false; public bool CanCreateContexts => false; public IGlContext CreateContext() => throw new System.NotSupportedException(); From 05fdc0446416a285c9067b56d858c018e4f7104d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 3 Jun 2022 23:43:55 +0200 Subject: [PATCH 077/389] Add ControlTheme.BasedOn. --- src/Avalonia.Base/Styling/ControlTheme.cs | 26 ++++++-- src/Avalonia.Base/Styling/NestingSelector.cs | 12 +++- src/Avalonia.Base/Styling/Style.cs | 35 ++++++++--- src/Avalonia.Base/Styling/StyleBase.cs | 61 ++++++------------- .../Styling/StyledElementTests_Theming.cs | 59 ++++++++++++++++-- 5 files changed, 129 insertions(+), 64 deletions(-) diff --git a/src/Avalonia.Base/Styling/ControlTheme.cs b/src/Avalonia.Base/Styling/ControlTheme.cs index 9dcbd7d2c4..aff6fad990 100644 --- a/src/Avalonia.Base/Styling/ControlTheme.cs +++ b/src/Avalonia.Base/Styling/ControlTheme.cs @@ -23,16 +23,32 @@ namespace Avalonia.Styling /// public Type? TargetType { get; set; } - internal override bool HasSelector => TargetType is not null; + /// + /// Gets or sets a control theme that is the basis of the current theme. + /// + public ControlTheme? BasedOn { get; set; } - internal override SelectorMatch Match(IStyleable control, object? host, bool subscribe) + public override SelectorMatchResult TryAttach(IStyleable target, object? host) { + _ = target ?? throw new ArgumentNullException(nameof(target)); + if (TargetType is null) throw new InvalidOperationException("ControlTheme has no TargetType."); - return control.StyleKey == TargetType ? - SelectorMatch.AlwaysThisType : - SelectorMatch.NeverThisType; + var result = BasedOn?.TryAttach(target, host) ?? SelectorMatchResult.NeverThisType; + + if (HasSettersOrAnimations && target.StyleKey == TargetType) + { + Attach(target, null); + result = SelectorMatchResult.AlwaysThisType; + } + + var childResult = TryAttachChildren(target, host); + + if (childResult > result) + result = childResult; + + return result; } internal override void SetParent(StyleBase? parent) diff --git a/src/Avalonia.Base/Styling/NestingSelector.cs b/src/Avalonia.Base/Styling/NestingSelector.cs index 6d31f7cb18..c8945a713d 100644 --- a/src/Avalonia.Base/Styling/NestingSelector.cs +++ b/src/Avalonia.Base/Styling/NestingSelector.cs @@ -15,9 +15,17 @@ namespace Avalonia.Styling protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe) { - if (parent is StyleBase s && s.HasSelector) + if (parent is Style s && s.Selector is not null) { - return s.Match(control, null, subscribe); + return s.Selector.Match(control, s.Parent, subscribe); + } + else if (parent is ControlTheme theme) + { + if (theme.TargetType is null) + throw new InvalidOperationException("ControlTheme has no TargetType."); + return control.StyleKey == theme.TargetType ? + SelectorMatch.AlwaysThisType : + SelectorMatch.NeverThisType; } throw new InvalidOperationException( diff --git a/src/Avalonia.Base/Styling/Style.cs b/src/Avalonia.Base/Styling/Style.cs index ca20ff2b4b..7a6b746488 100644 --- a/src/Avalonia.Base/Styling/Style.cs +++ b/src/Avalonia.Base/Styling/Style.cs @@ -28,7 +28,32 @@ namespace Avalonia.Styling /// public Selector? Selector { get; set; } - internal override bool HasSelector => Selector is not null; + public override SelectorMatchResult TryAttach(IStyleable target, object? host) + { + _ = target ?? throw new ArgumentNullException(nameof(target)); + + var result = SelectorMatchResult.NeverThisType; + + if (HasSettersOrAnimations) + { + var match = Selector?.Match(target, Parent, true) ?? + (target == host ? + SelectorMatch.AlwaysThisInstance : + SelectorMatch.NeverThisInstance); + + if (match.IsMatch) + Attach(target, match.Activator); + + result = match.Result; + } + + var childResult = TryAttachChildren(target, host); + + if (childResult > result) + result = childResult; + + return result; + } /// /// Returns a string representation of the style. @@ -46,14 +71,6 @@ namespace Avalonia.Styling } } - internal override SelectorMatch Match(IStyleable control, object? host, bool subscribe) - { - return Selector?.Match(control, Parent, subscribe) ?? - (control == host ? - SelectorMatch.AlwaysThisInstance : - SelectorMatch.NeverThisInstance); - } - internal override void SetParent(StyleBase? parent) { if (parent is Style parentStyle && parentStyle.Selector is not null) diff --git a/src/Avalonia.Base/Styling/StyleBase.cs b/src/Avalonia.Base/Styling/StyleBase.cs index b6bfec62bd..306a4cf010 100644 --- a/src/Avalonia.Base/Styling/StyleBase.cs +++ b/src/Avalonia.Base/Styling/StyleBase.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Avalonia.Animation; using Avalonia.Controls; using Avalonia.Metadata; +using Avalonia.Styling.Activators; namespace Avalonia.Styling { @@ -64,43 +65,14 @@ namespace Avalonia.Styling bool IResourceNode.HasResources => _resources?.Count > 0; IReadOnlyList IStyle.Children => (IReadOnlyList?)_children ?? Array.Empty(); - internal abstract bool HasSelector { get; } + internal bool HasSettersOrAnimations => _setters?.Count > 0 || _animations?.Count > 0; public void Add(ISetter setter) => Setters.Add(setter); public void Add(IStyle style) => Children.Add(style); public event EventHandler? OwnerChanged; - public SelectorMatchResult TryAttach(IStyleable target, object? host) - { - target = target ?? throw new ArgumentNullException(nameof(target)); - - var result = SelectorMatchResult.NeverThisType; - - if (_setters?.Count > 0 || _animations?.Count > 0) - { - var match = Match(target, host, subscribe: true); - - if (match.IsMatch) - { - var instance = new StyleInstance(this, target, _setters, _animations, match.Activator); - target.StyleApplied(instance); - instance.Start(); - } - - result = match.Result; - } - - if (_children is not null) - { - _childCache ??= new StyleCache(); - var childResult = _childCache.TryAttach(_children, target, host); - if (childResult > result) - result = childResult; - } - - return result; - } + public abstract SelectorMatchResult TryAttach(IStyleable target, object? host); public bool TryGetResource(object key, out object? result) { @@ -108,19 +80,20 @@ namespace Avalonia.Styling return _resources?.TryGetResource(key, out result) ?? false; } - /// - /// Evaluates the style's selector against the specified target element. - /// - /// The control. - /// The element that hosts the style. - /// - /// Whether the match should subscribe to changes in order to track the match over time, - /// or simply return an immediate result. - /// - /// - /// A describing how the style matches the control. - /// - internal abstract SelectorMatch Match(IStyleable control, object? host, bool subscribe); + internal void Attach(IStyleable target, IStyleActivator? activator) + { + var instance = new StyleInstance(this, target, _setters, _animations, activator); + target.StyleApplied(instance); + instance.Start(); + } + + internal SelectorMatchResult TryAttachChildren(IStyleable target, object? host) + { + if (_children is null || _children.Count == 0) + return SelectorMatchResult.NeverThisType; + _childCache ??= new StyleCache(); + return _childCache.TryAttach(_children, target, host); + } internal virtual void SetParent(StyleBase? parent) => Parent = parent; diff --git a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs index 0c0808987a..737cf1e048 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs @@ -28,10 +28,10 @@ public class StyledElementTests_Theming Assert.NotNull(target.Template); var border = Assert.IsType(target.VisualChild); - Assert.Equal(border.Background, Brushes.Red); + Assert.Equal(Brushes.Red, border.Background); target.Classes.Add("foo"); - Assert.Equal(border.Background, Brushes.Green); + Assert.Equal(Brushes.Green, border.Background); } [Fact] @@ -73,7 +73,7 @@ public class StyledElementTests_Theming var root = CreateRoot(target); var canvas = Assert.IsType(target.VisualChild); - Assert.Equal(canvas.Background, Brushes.Red); + Assert.Equal(Brushes.Red, canvas.Background); target.Theme = null; @@ -97,7 +97,28 @@ public class StyledElementTests_Theming var border = Assert.IsType(target.VisualChild); Assert.NotNull(target.Template); - Assert.Equal(border.Background, Brushes.Red); + Assert.Equal(Brushes.Red, border.Background); + } + + [Fact] + public void BasedOn_Theme_Is_Applied_When_Attached_To_Logical_Tree() + { + using var app = UnitTestApplication.Start(TestServices.RealStyler); + var target = CreateTarget(CreateDerivedTheme()); + + Assert.Null(target.Template); + + var root = CreateRoot(target); + Assert.NotNull(target.Template); + Assert.Equal(Brushes.Blue, target.BorderBrush); + + var border = Assert.IsType(target.VisualChild); + Assert.Equal(Brushes.Red, border.Background); + Assert.Equal(Brushes.Yellow, border.BorderBrush); + + target.Classes.Add("foo"); + Assert.Equal(Brushes.Green, border.Background); + Assert.Equal(Brushes.Cyan, border.BorderBrush); } private static ThemedControl CreateTarget(ControlTheme? theme = null) @@ -197,6 +218,36 @@ public class StyledElementTests_Theming }; } + private static ControlTheme CreateDerivedTheme() + { + return new ControlTheme + { + TargetType = typeof(ThemedControl), + BasedOn = CreateTheme(), + Setters = + { + new Setter(Border.BorderBrushProperty, Brushes.Blue), + }, + Children = + { + new Style(x => x.Nesting().Template().OfType()) + { + Setters = + { + new Setter(Border.BorderBrushProperty, Brushes.Yellow), + } + }, + new Style(x => x.Nesting().Class("foo").Template().OfType()) + { + Setters = + { + new Setter(Border.BorderBrushProperty, Brushes.Cyan), + } + }, + } + }; + } + private class ThemedControl : TemplatedControl { public IVisual? VisualChild => VisualChildren?.SingleOrDefault(); From f772e5fb5a09c0fdb30a916a0eb21b04a1a73524 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 4 Jun 2022 01:19:31 +0300 Subject: [PATCH 078/389] Don't transitively activate server objects, only objects directly attached to a composition target should receive updates --- .../Rendering/Composition/Server/ServerObject.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs index dde711c3b5..f55c9439e4 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs @@ -92,15 +92,13 @@ namespace Avalonia.Rendering.Composition.Server public void SubscribeToInvalidation(int member, IAnimationInstance animation) { ref var store = ref GetStoreFromOffset(member); - if (store.Subscribers.AddRef(animation)) - Activate(); + store.Subscribers.AddRef(animation); } public void UnsubscribeFromInvalidation(int member, IAnimationInstance animation) { ref var store = ref GetStoreFromOffset(member); - if (store.Subscribers.ReleaseRef(animation)) - Deactivate(); + store.Subscribers.ReleaseRef(animation); } public virtual int? GetFieldOffset(string fieldName) => null; From 1d1ef5ca9fdb42d2fbf029f122530dcca9b51a59 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 4 Jun 2022 12:45:19 +0200 Subject: [PATCH 079/389] Display control themes in devtools. --- src/Avalonia.Base/Styling/ControlTheme.cs | 8 ++++++++ .../Diagnostics/ViewModels/ControlDetailsViewModel.cs | 11 +++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Styling/ControlTheme.cs b/src/Avalonia.Base/Styling/ControlTheme.cs index aff6fad990..399eb9ae59 100644 --- a/src/Avalonia.Base/Styling/ControlTheme.cs +++ b/src/Avalonia.Base/Styling/ControlTheme.cs @@ -51,6 +51,14 @@ namespace Avalonia.Styling return result; } + public override string ToString() + { + if (TargetType is not null) + return "ControlTheme: " + TargetType.Name; + else + return "ControlTheme"; + } + internal override void SetParent(StyleBase? parent) { throw new InvalidOperationException("ControlThemes cannot be added as a nested style."); diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs index e383c160e3..795826e4f6 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -67,8 +67,15 @@ namespace Avalonia.Diagnostics.ViewModels var setters = new List(); - if (styleSource is Style style) + if (styleSource is StyleBase style) { + var selector = style switch + { + Style s => s.Selector?.ToString(), + ControlTheme t => t.TargetType?.Name.ToString(), + _ => null, + }; + foreach (var setter in style.Setters) { if (setter is Setter regularSetter @@ -105,7 +112,7 @@ namespace Avalonia.Diagnostics.ViewModels } } - AppliedStyles.Add(new StyleViewModel(appliedStyle, style.Selector?.ToString() ?? "No selector", setters)); + AppliedStyles.Add(new StyleViewModel(appliedStyle, selector ?? "No selector", setters)); } } From 95f70143ca319e5e6f49601d1e2de60bdbb39759 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 4 Jun 2022 14:53:31 +0200 Subject: [PATCH 080/389] Can apply control theme to derived types. --- src/Avalonia.Base/Styling/ControlTheme.cs | 2 +- src/Avalonia.Base/Styling/NestingSelector.cs | 2 +- .../Styling/StyledElementTests_Theming.cs | 25 +++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Styling/ControlTheme.cs b/src/Avalonia.Base/Styling/ControlTheme.cs index 399eb9ae59..644e8b32d4 100644 --- a/src/Avalonia.Base/Styling/ControlTheme.cs +++ b/src/Avalonia.Base/Styling/ControlTheme.cs @@ -37,7 +37,7 @@ namespace Avalonia.Styling var result = BasedOn?.TryAttach(target, host) ?? SelectorMatchResult.NeverThisType; - if (HasSettersOrAnimations && target.StyleKey == TargetType) + if (HasSettersOrAnimations && TargetType.IsAssignableFrom(target.StyleKey)) { Attach(target, null); result = SelectorMatchResult.AlwaysThisType; diff --git a/src/Avalonia.Base/Styling/NestingSelector.cs b/src/Avalonia.Base/Styling/NestingSelector.cs index c8945a713d..4393d3239f 100644 --- a/src/Avalonia.Base/Styling/NestingSelector.cs +++ b/src/Avalonia.Base/Styling/NestingSelector.cs @@ -23,7 +23,7 @@ namespace Avalonia.Styling { if (theme.TargetType is null) throw new InvalidOperationException("ControlTheme has no TargetType."); - return control.StyleKey == theme.TargetType ? + return theme.TargetType.IsAssignableFrom(control.StyleKey) ? SelectorMatch.AlwaysThisType : SelectorMatch.NeverThisType; } diff --git a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs index 737cf1e048..ab6c239393 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs @@ -34,6 +34,27 @@ public class StyledElementTests_Theming Assert.Equal(Brushes.Green, border.Background); } + [Fact] + public void Theme_Is_Applied_To_Derived_Class_When_Attached_To_Logical_Tree() + { + using var app = UnitTestApplication.Start(TestServices.RealStyler); + var target = new DerivedThemedControl + { + Theme = CreateTheme(), + }; + + Assert.Null(target.Template); + + var root = CreateRoot(target); + Assert.NotNull(target.Template); + + var border = Assert.IsType(target.VisualChild); + Assert.Equal(Brushes.Red, border.Background); + + target.Classes.Add("foo"); + Assert.Equal(Brushes.Green, border.Background); + } + [Fact] public void Theme_Is_Detached_When_Theme_Property_Cleared() { @@ -252,4 +273,8 @@ public class StyledElementTests_Theming { public IVisual? VisualChild => VisualChildren?.SingleOrDefault(); } + + private class DerivedThemedControl : ThemedControl + { + } } From d21e634ab308c1b63d1e2f2105de29c8af236b2a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Jun 2022 12:00:14 +0200 Subject: [PATCH 081/389] Added support for implicit themes. If no `Theme` property is provided, try to look up a resource keyed with the control's `StyleKey`. --- src/Avalonia.Base/StyledElement.cs | 28 ++++++++++++ src/Avalonia.Base/Styling/IStyleable.cs | 4 +- src/Avalonia.Base/Styling/Styler.cs | 4 +- .../Styling/StyledElementTests_Theming.cs | 43 +++++++++++++++++++ .../AvaloniaPropertyConverterTest.cs | 4 +- .../DynamicResourceExtensionTests.cs | 6 ++- 6 files changed, 81 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index 75c4b94174..f377eb848c 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -60,6 +60,7 @@ namespace Avalonia public static readonly StyledProperty ThemeProperty = AvaloniaProperty.Register(nameof(Theme)); + private static readonly ControlTheme s_invalidTheme = new ControlTheme(); private int _initCount; private string? _name; private readonly Classes _classes = new Classes(); @@ -72,6 +73,7 @@ namespace Avalonia private ITemplatedControl? _templatedParent; private bool _dataContextUpdating; private bool _hasPromotedTheme; + private ControlTheme? _implicitTheme; /// /// Initializes static members of the class. @@ -495,6 +497,31 @@ namespace Avalonia }; } + ControlTheme? IStyleable.GetEffectiveTheme() + { + var theme = Theme; + + // Explitly set Theme property takes precedence. + if (theme is not null) + return theme; + + // If the Theme property is not set, try to find a ControlTheme resource with our StyleKey. + if (_implicitTheme is null) + { + var key = ((IStyleable)this).StyleKey; + + if (this.TryFindResource(key, out var value) && value is ControlTheme t) + _implicitTheme = t; + else + _implicitTheme = s_invalidTheme; + } + + if (_implicitTheme != s_invalidTheme) + return _implicitTheme; + + return null; + } + void IStyleable.StyleApplied(IStyleInstance instance) { instance = instance ?? throw new ArgumentNullException(nameof(instance)); @@ -736,6 +763,7 @@ namespace Avalonia if (_logicalRoot != null) { _logicalRoot = null; + _implicitTheme = null; DetachStyles(); OnDetachedFromLogicalTree(e); DetachedFromLogicalTree?.Invoke(this, e); diff --git a/src/Avalonia.Base/Styling/IStyleable.cs b/src/Avalonia.Base/Styling/IStyleable.cs index 61fcbdf850..254da4d85c 100644 --- a/src/Avalonia.Base/Styling/IStyleable.cs +++ b/src/Avalonia.Base/Styling/IStyleable.cs @@ -27,9 +27,9 @@ namespace Avalonia.Styling ITemplatedControl? TemplatedParent { get; } /// - /// Gets the theme to be applied to the control. + /// Gets the effective theme for the control as used by the syling system. /// - public ControlTheme? Theme { get; } + ControlTheme? GetEffectiveTheme(); /// /// Notifies the element that a style has been applied. diff --git a/src/Avalonia.Base/Styling/Styler.cs b/src/Avalonia.Base/Styling/Styler.cs index c9ea123bdc..6ac2e8d372 100644 --- a/src/Avalonia.Base/Styling/Styler.cs +++ b/src/Avalonia.Base/Styling/Styler.cs @@ -11,10 +11,10 @@ namespace Avalonia.Styling // If the control has a themed templated parent then first apply the styles from // the templated parent theme. if (target.TemplatedParent is IStyleable styleableParent) - styleableParent.Theme?.TryAttach(target, styleableParent); + styleableParent.GetEffectiveTheme()?.TryAttach(target, styleableParent); // Next apply the control theme. - target.Theme?.TryAttach(target, target); + target.GetEffectiveTheme()?.TryAttach(target, target); // Apply styles from the rest of the tree. if (target is IStyleHost styleHost) diff --git a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs index ab6c239393..522937b669 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs @@ -158,6 +158,49 @@ public class StyledElementTests_Theming } } + public class ImplicitTheme + { + [Fact] + public void Implicit_Theme_Is_Applied_When_Attached_To_Logical_Tree() + { + using var app = UnitTestApplication.Start(TestServices.RealStyler); + var target = CreateTarget(); + var root = CreateRoot(target); + Assert.NotNull(target.Template); + + var border = Assert.IsType(target.VisualChild); + Assert.Equal(Brushes.Red, border.Background); + + target.Classes.Add("foo"); + Assert.Equal(Brushes.Green, border.Background); + } + + [Fact] + public void Implicit_Theme_Is_Cleared_When_Removed_From_Logical_Tree() + { + using var app = UnitTestApplication.Start(TestServices.RealStyler); + var target = CreateTarget(); + var root = CreateRoot(target); + + Assert.NotNull(((IStyleable)target).GetEffectiveTheme()); + + root.Child = null; + + Assert.Null(((IStyleable)target).GetEffectiveTheme()); + } + + private static ThemedControl CreateTarget() => new ThemedControl(); + + private static TestRoot CreateRoot(IControl child) + { + var result = new TestRoot(); + result.Resources.Add(typeof(ThemedControl), CreateTheme()); + result.Child = child; + result.LayoutManager.ExecuteInitialLayoutPass(); + return result; + } + } + public class ThemeFromStyle { [Fact] diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs index ca59fe8480..75e21a7138 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs @@ -137,9 +137,9 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters get { throw new NotImplementedException(); } } - public ControlTheme Theme + public ControlTheme GetEffectiveTheme() { - get { throw new NotImplementedException(); } + throw new NotImplementedException(); } public void DetachStyles() diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs index 592dbfc0d1..987725c314 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs @@ -845,7 +845,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions Assert.Equal("bar", border.Tag); var resourceProvider = (TrackingResourceProvider)window.Resources.MergedDictionaries[0]; - Assert.Equal(new[] { "bar" }, resourceProvider.RequestedResources); + Assert.Contains("bar", resourceProvider.RequestedResources); + Assert.DoesNotContain("foo", resourceProvider.RequestedResources); } [Fact] @@ -883,7 +884,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions Assert.Equal("bar", border.Tag); var resourceProvider = (TrackingResourceProvider)window.Resources.MergedDictionaries[0]; - Assert.Equal(new[] { "bar" }, resourceProvider.RequestedResources); + Assert.Contains("bar", resourceProvider.RequestedResources); + Assert.DoesNotContain("foo", resourceProvider.RequestedResources); } private IDisposable StyledWindow(params (string, string)[] assets) From 35440b0f2119a527c442a91afdbb254e425c70b2 Mon Sep 17 00:00:00 2001 From: robloo Date: Wed, 8 Jun 2022 22:53:53 -0400 Subject: [PATCH 082/389] Remove unused field to avoid warnings --- .../ColorSpectrum/ColorSpectrum.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index 245592207e..77994aa0c8 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -44,7 +44,6 @@ namespace Avalonia.Controls.Primitives private bool _updatingColor = false; private bool _updatingHsvColor = false; - private bool _isPointerOver = false; private bool _isPointerPressed = false; private bool _shouldShowLargeSelection = false; private List _hsvValues = new List(); @@ -851,7 +850,6 @@ namespace Avalonia.Controls.Primitives /// private void InputTarget_PointerEnter(object? sender, PointerEventArgs args) { - _isPointerOver = true; UpdatePseudoClasses(); args.Handled = true; } @@ -859,7 +857,6 @@ namespace Avalonia.Controls.Primitives /// private void InputTarget_PointerLeave(object? sender, PointerEventArgs args) { - _isPointerOver = false; UpdatePseudoClasses(); args.Handled = true; } From 8b4cf63be3ffbf29427bb16d15ff514c595d348b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Jun 2022 11:18:22 +0200 Subject: [PATCH 083/389] Additional validation for ControlTheme children. --- src/Avalonia.Base/Styling/ChildSelector.cs | 2 +- .../Styling/DescendentSelector.cs | 2 +- src/Avalonia.Base/Styling/NestingSelector.cs | 2 +- src/Avalonia.Base/Styling/NotSelector.cs | 2 +- src/Avalonia.Base/Styling/NthChildSelector.cs | 2 +- src/Avalonia.Base/Styling/OrSelector.cs | 12 +--- .../Styling/PropertyEqualsSelector.cs | 2 +- src/Avalonia.Base/Styling/Selector.cs | 31 ++++++++- src/Avalonia.Base/Styling/Style.cs | 9 ++- src/Avalonia.Base/Styling/Styles.cs | 5 ++ src/Avalonia.Base/Styling/TemplateSelector.cs | 2 +- .../Styling/TypeNameAndClassSelector.cs | 2 +- .../Styling/ControlThemeTests.cs | 64 +++++++++++++++++++ 13 files changed, 117 insertions(+), 20 deletions(-) diff --git a/src/Avalonia.Base/Styling/ChildSelector.cs b/src/Avalonia.Base/Styling/ChildSelector.cs index 34f3a76b61..9512dc34df 100644 --- a/src/Avalonia.Base/Styling/ChildSelector.cs +++ b/src/Avalonia.Base/Styling/ChildSelector.cs @@ -65,6 +65,6 @@ namespace Avalonia.Styling } protected override Selector? MovePrevious() => null; - internal override bool HasValidNestingSelector() => _parent.HasValidNestingSelector(); + protected override Selector? MovePreviousOrParent() => _parent; } } diff --git a/src/Avalonia.Base/Styling/DescendentSelector.cs b/src/Avalonia.Base/Styling/DescendentSelector.cs index 4ffaff6861..677a924189 100644 --- a/src/Avalonia.Base/Styling/DescendentSelector.cs +++ b/src/Avalonia.Base/Styling/DescendentSelector.cs @@ -70,6 +70,6 @@ namespace Avalonia.Styling } protected override Selector? MovePrevious() => null; - internal override bool HasValidNestingSelector() => _parent.HasValidNestingSelector(); + protected override Selector? MovePreviousOrParent() => _parent; } } diff --git a/src/Avalonia.Base/Styling/NestingSelector.cs b/src/Avalonia.Base/Styling/NestingSelector.cs index 4393d3239f..77c5b719c6 100644 --- a/src/Avalonia.Base/Styling/NestingSelector.cs +++ b/src/Avalonia.Base/Styling/NestingSelector.cs @@ -33,6 +33,6 @@ namespace Avalonia.Styling } protected override Selector? MovePrevious() => null; - internal override bool HasValidNestingSelector() => true; + protected override Selector? MovePreviousOrParent() => null; } } diff --git a/src/Avalonia.Base/Styling/NotSelector.cs b/src/Avalonia.Base/Styling/NotSelector.cs index 76a0690e96..c7727bb6b8 100644 --- a/src/Avalonia.Base/Styling/NotSelector.cs +++ b/src/Avalonia.Base/Styling/NotSelector.cs @@ -67,6 +67,6 @@ namespace Avalonia.Styling } protected override Selector? MovePrevious() => _previous; - internal override bool HasValidNestingSelector() => _previous?.HasValidNestingSelector() ?? false; + protected override Selector? MovePreviousOrParent() => _previous; } } diff --git a/src/Avalonia.Base/Styling/NthChildSelector.cs b/src/Avalonia.Base/Styling/NthChildSelector.cs index 047bf434da..f473791664 100644 --- a/src/Avalonia.Base/Styling/NthChildSelector.cs +++ b/src/Avalonia.Base/Styling/NthChildSelector.cs @@ -105,7 +105,7 @@ namespace Avalonia.Styling } protected override Selector? MovePrevious() => _previous; - internal override bool HasValidNestingSelector() => _previous?.HasValidNestingSelector() ?? false; + protected override Selector? MovePreviousOrParent() => _previous; public override string ToString() { diff --git a/src/Avalonia.Base/Styling/OrSelector.cs b/src/Avalonia.Base/Styling/OrSelector.cs index 913c27bf0c..af9249864f 100644 --- a/src/Avalonia.Base/Styling/OrSelector.cs +++ b/src/Avalonia.Base/Styling/OrSelector.cs @@ -103,18 +103,12 @@ namespace Avalonia.Styling } protected override Selector? MovePrevious() => null; + protected override Selector? MovePreviousOrParent() => null; - internal override bool HasValidNestingSelector() + internal override void ValidateNestingSelector(bool inControlTheme) { foreach (var selector in _selectors) - { - if (!selector.HasValidNestingSelector()) - { - return false; - } - } - - return true; + selector.ValidateNestingSelector(inControlTheme); } private Type? EvaluateTargetType() diff --git a/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs index 7a37daf087..48136ba2de 100644 --- a/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs +++ b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs @@ -90,7 +90,7 @@ namespace Avalonia.Styling } protected override Selector? MovePrevious() => _previous; - internal override bool HasValidNestingSelector() => _previous?.HasValidNestingSelector() ?? false; + protected override Selector? MovePreviousOrParent() => _previous; internal static bool Compare(Type propertyType, object? propertyValue, object? value) { diff --git a/src/Avalonia.Base/Styling/Selector.cs b/src/Avalonia.Base/Styling/Selector.cs index 1e06f3d375..7ce17518dd 100644 --- a/src/Avalonia.Base/Styling/Selector.cs +++ b/src/Avalonia.Base/Styling/Selector.cs @@ -86,7 +86,36 @@ namespace Avalonia.Styling /// protected abstract Selector? MovePrevious(); - internal abstract bool HasValidNestingSelector(); + /// + /// Moves to the previous selector or the parent selector. + /// + protected abstract Selector? MovePreviousOrParent(); + + internal virtual void ValidateNestingSelector(bool inControlTheme) + { + var s = this; + var templateCount = 0; + + do + { + if (inControlTheme) + { + if (!s.InTemplate && s.IsCombinator) + throw new InvalidOperationException( + "ControlTheme style may not directly contain a child or descendent selector."); + if (s is TemplateSelector && templateCount++ > 0) + throw new InvalidOperationException( + "ControlTemplate styles cannot contain multiple template selectors."); + } + + var previous = s.MovePreviousOrParent(); + + if (previous is null && s is not NestingSelector) + throw new InvalidOperationException("Child styles must have a nesting selector."); + + s = previous; + } while (s is not null); + } private static SelectorMatch MatchUntilCombinator( IStyleable control, diff --git a/src/Avalonia.Base/Styling/Style.cs b/src/Avalonia.Base/Styling/Style.cs index 7a6b746488..77c4e62d29 100644 --- a/src/Avalonia.Base/Styling/Style.cs +++ b/src/Avalonia.Base/Styling/Style.cs @@ -77,8 +77,13 @@ namespace Avalonia.Styling { if (Selector is null) throw new InvalidOperationException("Child styles must have a selector."); - if (!Selector.HasValidNestingSelector()) - throw new InvalidOperationException("Child styles must have a nesting selector."); + Selector.ValidateNestingSelector(false); + } + else if (parent is ControlTheme) + { + if (Selector is null) + throw new InvalidOperationException("Child styles must have a selector."); + Selector.ValidateNestingSelector(true); } base.SetParent(parent); diff --git a/src/Avalonia.Base/Styling/Styles.cs b/src/Avalonia.Base/Styling/Styles.cs index 4c011f1b0d..3a27275438 100644 --- a/src/Avalonia.Base/Styling/Styles.cs +++ b/src/Avalonia.Base/Styling/Styles.cs @@ -26,6 +26,11 @@ namespace Avalonia.Styling { _styles.ResetBehavior = ResetBehavior.Remove; _styles.CollectionChanged += OnCollectionChanged; + _styles.Validate = i => + { + if (i is ControlTheme) + throw new InvalidOperationException("ControlThemes cannot be added to a Styles collection."); + }; } public Styles(IResourceHost owner) diff --git a/src/Avalonia.Base/Styling/TemplateSelector.cs b/src/Avalonia.Base/Styling/TemplateSelector.cs index b0a2dae8d6..278e24a203 100644 --- a/src/Avalonia.Base/Styling/TemplateSelector.cs +++ b/src/Avalonia.Base/Styling/TemplateSelector.cs @@ -49,6 +49,6 @@ namespace Avalonia.Styling } protected override Selector? MovePrevious() => null; - internal override bool HasValidNestingSelector() => _parent?.HasValidNestingSelector() ?? false; + protected override Selector? MovePreviousOrParent() => _parent; } } diff --git a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs index 24d5d6bbbf..6681a7da36 100644 --- a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs @@ -140,7 +140,7 @@ namespace Avalonia.Styling } protected override Selector? MovePrevious() => _previous; - internal override bool HasValidNestingSelector() => _previous?.HasValidNestingSelector() ?? false; + protected override Selector? MovePreviousOrParent() => _previous; private string BuildSelectorString() { diff --git a/tests/Avalonia.Base.UnitTests/Styling/ControlThemeTests.cs b/tests/Avalonia.Base.UnitTests/Styling/ControlThemeTests.cs index 93a0e6c2fd..7a27a02fc4 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/ControlThemeTests.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/ControlThemeTests.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Controls; +using Avalonia.Controls.Primitives; using Avalonia.Styling; using Xunit; @@ -7,6 +8,15 @@ namespace Avalonia.Base.UnitTests.Styling { public class ControlThemeTests { + [Fact] + public void ControlTheme_Cannot_Be_Added_To_Styles() + { + var target = new ControlTheme(typeof(Button)); + var styles = new Styles(); + + Assert.Throws(() => styles.Add(target)); + } + [Fact] public void ControlTheme_Cannot_Be_Added_To_Style_Children() { @@ -24,5 +34,59 @@ namespace Avalonia.Base.UnitTests.Styling Assert.Throws(() => other.Children.Add(target)); } + + [Fact] + public void Style_Without_Selector_Cannot_Be_Added_To_Children() + { + var target = new ControlTheme(typeof(Button)); + var child = new Style(); + + Assert.Throws(() => target.Children.Add(child)); + } + + [Fact] + public void Style_Without_Nesting_Selector_Cannot_Be_Added_To_Children() + { + var target = new ControlTheme(typeof(Button)); + var child = new Style(x => x.OfType public ColorView() : base() { - CustomPalette = new FluentColorPalette(); } /// diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index 4b01de15a0..cf58b882e7 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -81,6 +81,9 @@ - + - + - + + @@ -316,40 +316,40 @@ VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" /> - - + - + - + - + - + - From 58f9b443d9a40aff415d1021835794f003b5c025 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 27 Jun 2022 00:45:54 -0400 Subject: [PATCH 130/389] Implement IsAlphaEnabled using coercion --- .../ColorView/ColorView.Properties.cs | 16 +++-- .../ColorView/ColorView.cs | 66 +++++++++++++++++++ .../Themes/Fluent/ColorView.xaml | 18 ++++- 3 files changed, 92 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs index c7652833bf..e1da427542 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs @@ -15,7 +15,8 @@ namespace Avalonia.Controls AvaloniaProperty.Register( nameof(Color), Colors.White, - defaultBindingMode: BindingMode.TwoWay); + defaultBindingMode: BindingMode.TwoWay, + coerce: CoerceColor) ; /// /// Defines the property. @@ -48,7 +49,8 @@ namespace Avalonia.Controls AvaloniaProperty.Register( nameof(HsvColor), Colors.White.ToHsv(), - defaultBindingMode: BindingMode.TwoWay); + defaultBindingMode: BindingMode.TwoWay, + coerce: CoerceHsvColor); /// /// Defines the property. @@ -267,7 +269,8 @@ namespace Avalonia.Controls /// /// Gets or sets a value indicating whether the alpha component is enabled. - /// When disabled (set to false) the alpha component will be fixed to maximum. + /// When disabled (set to false) the alpha component will be fixed to maximum and + /// editing controls hidden. /// public bool IsAlphaEnabled { @@ -277,7 +280,8 @@ namespace Avalonia.Controls /// /// Gets or sets a value indicating whether the alpha component editing controls - /// (both Slider and TextBox) are visible. + /// (Slider(s) and TextBox) are visible. When hidden, the existing alpha component + /// value is maintained. /// /// /// Note that also controls the alpha @@ -353,7 +357,7 @@ namespace Avalonia.Controls /// /// /// All color components are controlled by this property but alpha can also be - /// controlled with . + /// controlled with and . /// public bool IsComponentSliderVisible { @@ -366,7 +370,7 @@ namespace Avalonia.Controls /// /// /// All color components are controlled by this property but alpha can also be - /// controlled with . + /// controlled with and . /// public bool IsComponentTextInputVisible { diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index 4f95b4acfe..8fb80980d5 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -237,6 +237,12 @@ namespace Avalonia.Controls PaletteColors = newPaletteColors; } } + else if (change.Property == IsAlphaEnabledProperty) + { + // Manually coerce the HsvColor value + // (Color will be coerced automatically if HsvColor changes) + HsvColor = OnCoerceHsvColor(HsvColor); + } else if (change.Property == IsColorComponentsVisibleProperty || change.Property == IsColorPaletteVisibleProperty || change.Property == IsColorSpectrumVisibleProperty) @@ -271,6 +277,66 @@ namespace Avalonia.Controls ColorChanged?.Invoke(this, e); } + /// + /// Called when the property has to be coerced. + /// + /// The value to coerce. + protected virtual Color OnCoerceColor(Color value) + { + if (IsAlphaEnabled == false) + { + return new Color(255, value.R, value.G, value.B); + } + + return value; + } + + /// + /// Called when the property has to be coerced. + /// + /// The value to coerce. + protected virtual HsvColor OnCoerceHsvColor(HsvColor value) + { + if (IsAlphaEnabled == false) + { + return new HsvColor(1.0, value.H, value.S, value.V); + } + + return value; + } + + /// + /// Coerces/validates the property value. + /// + /// The instance. + /// The value to coerce. + /// The coerced/validated value. + private static Color CoerceColor(IAvaloniaObject instance, Color value) + { + if (instance is ColorView colorView) + { + return colorView.OnCoerceColor(value); + } + + return value; + } + + /// + /// Coerces/validates the property value. + /// + /// The instance. + /// The value to coerce. + /// The coerced/validated value. + private static HsvColor CoerceHsvColor(IAvaloniaObject instance, HsvColor value) + { + if (instance is ColorView colorView) + { + return colorView.OnCoerceHsvColor(value); + } + + return value; + } + /// /// Event handler for when a key is pressed within the Hex RGB value TextBox. /// This is used to trigger re-evaluation of the color based on the TextBox value. diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index af2c8e061f..97760ba94a 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -187,8 +187,16 @@ HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}" HorizontalAlignment="Center" VerticalAlignment="Stretch" - Margin="12,0,0,0" - IsVisible="{TemplateBinding IsAlphaVisible}" /> + Margin="12,0,0,0"> + + + + + + + @@ -565,6 +573,8 @@ VerticalAlignment="Center" /> + + + Date: Mon, 27 Jun 2022 00:49:59 -0400 Subject: [PATCH 131/389] Only set slider IsVisible once --- src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index 97760ba94a..d43bc7e004 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -618,8 +618,7 @@ ColorModel="{TemplateBinding ColorModel, Mode=OneWay}" HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" HorizontalAlignment="Stretch" - VerticalAlignment="Center" - IsVisible="{TemplateBinding IsAlphaVisible}"> + VerticalAlignment="Center"> Date: Mon, 27 Jun 2022 00:53:49 -0400 Subject: [PATCH 132/389] Only disable alpha editing controls when IsAlphaEnabled is false Rely on IsAlphaVisible for visibility --- .../ColorView/ColorView.Properties.cs | 6 +++--- .../Themes/Fluent/ColorView.xaml | 20 ++++++++----------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs index e1da427542..d59c4d544a 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs @@ -270,7 +270,7 @@ namespace Avalonia.Controls /// /// Gets or sets a value indicating whether the alpha component is enabled. /// When disabled (set to false) the alpha component will be fixed to maximum and - /// editing controls hidden. + /// editing controls disabled. /// public bool IsAlphaEnabled { @@ -357,7 +357,7 @@ namespace Avalonia.Controls /// /// /// All color components are controlled by this property but alpha can also be - /// controlled with and . + /// controlled with . /// public bool IsComponentSliderVisible { @@ -370,7 +370,7 @@ namespace Avalonia.Controls /// /// /// All color components are controlled by this property but alpha can also be - /// controlled with and . + /// controlled with . /// public bool IsComponentTextInputVisible { diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index d43bc7e004..563f230cc9 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -187,11 +187,10 @@ HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}" HorizontalAlignment="Center" VerticalAlignment="Stretch" - Margin="12,0,0,0"> + Margin="12,0,0,0" + IsEnabled="{TemplateBinding IsAlphaEnabled}"> - @@ -564,7 +563,8 @@ BorderBrush="{DynamicResource TextControlBorderBrush}" BorderThickness="1,1,0,1" CornerRadius="4,0,0,4" - VerticalAlignment="Center"> + VerticalAlignment="Center" + IsEnabled="{TemplateBinding IsAlphaEnabled}"> - + Value="{Binding Value, ElementName=AlphaComponentSlider}" + IsEnabled="{TemplateBinding IsAlphaEnabled}"> - + VerticalAlignment="Center" + IsEnabled="{TemplateBinding IsAlphaEnabled}"> - Date: Mon, 27 Jun 2022 00:56:56 -0400 Subject: [PATCH 133/389] Remove unused ValueConverterGroup --- .../Converters/ValueConverterGroup.cs | 50 ------------------- 1 file changed, 50 deletions(-) delete mode 100644 src/Avalonia.Controls.ColorPicker/Converters/ValueConverterGroup.cs diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ValueConverterGroup.cs b/src/Avalonia.Controls.ColorPicker/Converters/ValueConverterGroup.cs deleted file mode 100644 index 2710c220f4..0000000000 --- a/src/Avalonia.Controls.ColorPicker/Converters/ValueConverterGroup.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using Avalonia.Data.Converters; - -namespace Avalonia.Controls.Primitives.Converters -{ - /// - /// Converter to chain together multiple converters. - /// - public class ValueConverterGroup : List, IValueConverter - { - /// - /// - public object? Convert( - object? value, - Type targetType, - object? parameter, - CultureInfo culture) - { - object? curValue; - - curValue = value; - for (int i = 0; i < Count; i++) - { - curValue = this[i].Convert(curValue, targetType, parameter, culture); - } - - return curValue; - } - - /// - public object? ConvertBack( - object? value, - Type targetType, - object? parameter, - CultureInfo culture) - { - object? curValue; - - curValue = value; - for (int i = (Count - 1); i >= 0; i--) - { - curValue = this[i].ConvertBack(curValue, targetType, parameter, culture); - } - - return curValue; - } - } -} From f4a38437314b00f99122c2b36d14a016c26a8c52 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 27 Jun 2022 09:49:21 +0200 Subject: [PATCH 134/389] Fix unit test --- tests/Avalonia.Controls.UnitTests/TextBlockTests.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs index 6da011f062..37dde9fbac 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs @@ -20,13 +20,11 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Default_Text_Value_Should_Be_EmptyString() + public void Default_Text_Value_Should_Be_Null() { var textBlock = new TextBlock(); - Assert.Equal( - "", - textBlock.Text); + Assert.Equal(null, textBlock.Text); } [Fact] From 1849ca4caa660a172fa16a3e616a58be84d12340 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 28 Jun 2022 23:50:22 +0300 Subject: [PATCH 135/389] Fixed matrix multiplication order --- .../Rendering/Composition/Server/DrawingContextProxy.cs | 4 ++-- .../Rendering/Composition/Server/ServerVisual.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index 2fd87f6620..e261507f60 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -33,7 +33,7 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont set => _visualBrushRenderer.VisualBrushDrawList = value; } - public Matrix PreTransform { get; set; } = Matrix.Identity; + public Matrix PostTransform { get; set; } = Matrix.Identity; public void Dispose() { @@ -44,7 +44,7 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont public Matrix Transform { get => _transform; - set => _impl.Transform = PreTransform * (_transform = value); + set => _impl.Transform = (_transform = value) * PostTransform; } public void Clear(Color color) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 06f087258b..3b36dfb87e 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -41,7 +41,7 @@ namespace Avalonia.Rendering.Composition.Server Root!.RenderedVisuals++; var transform = GlobalTransformMatrix; - canvas.PreTransform = MatrixUtils.ToMatrix(transform); + canvas.PostTransform = MatrixUtils.ToMatrix(transform); canvas.Transform = Matrix.Identity; if (Opacity != 1) canvas.PushOpacity(Opacity); @@ -56,7 +56,7 @@ namespace Avalonia.Rendering.Composition.Server RenderCore(canvas, currentTransformedClip); // Hack to force invalidation of SKMatrix - canvas.PreTransform = MatrixUtils.ToMatrix(transform); + canvas.PostTransform = MatrixUtils.ToMatrix(transform); canvas.Transform = Matrix.Identity; if (OpacityMaskBrush != null) From f6506f19e83d6f8456a86d749324b18bb1cba4ee Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 29 Jun 2022 23:57:03 +0300 Subject: [PATCH 136/389] Fixed Ref finalizer --- src/Avalonia.Base/Utilities/Ref.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Utilities/Ref.cs b/src/Avalonia.Base/Utilities/Ref.cs index 7209f02720..95a1c23883 100644 --- a/src/Avalonia.Base/Utilities/Ref.cs +++ b/src/Avalonia.Base/Utilities/Ref.cs @@ -159,7 +159,7 @@ namespace Avalonia.Utilities ~Ref() { - _counter?.Release(); + Dispose(); } public T Item From b4512e0da8f2334b1a1b2826e600f2386a30a4af Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 30 Jun 2022 00:05:10 +0300 Subject: [PATCH 137/389] Clear the draw list before letting go of the visual --- src/Avalonia.Base/Visual.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 7db218d2df..716b5f261d 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -495,7 +495,12 @@ namespace Avalonia DisableTransitions(); OnDetachedFromVisualTree(e); - CompositionVisual = null; + if (CompositionVisual != null) + { + CompositionVisual.DrawList = null; + CompositionVisual = null; + } + DetachedFromVisualTree?.Invoke(this, e); e.Root?.Renderer?.AddDirty(this); From 508fad9d2c333fdca885dbb652665fd994f07d2f Mon Sep 17 00:00:00 2001 From: Lobster Uberlord Date: Wed, 15 Jun 2022 17:27:16 +0700 Subject: [PATCH 138/389] Enable use of Skia Raster backend for HTML canvas in Blazor To enable the raster backend set CustomGpuFactory to null in the existing SkiaOptions, by default Avalonia will use the GPU/GL Skia backend. --- samples/ControlCatalog.Web/App.razor.cs | 1 + .../Avalonia.Web.Blazor/AvaloniaView.razor.cs | 57 ++++++++---- .../BlazorSkiaRasterSurface.cs | 91 +++++++++++++++++++ .../Avalonia.Web.Blazor/BlazorSkiaSurface.cs | 2 +- .../Avalonia.Web.Blazor/IBlazorSkiaSurface.cs | 9 ++ .../RazorViewTopLevelImpl.cs | 7 +- 6 files changed, 149 insertions(+), 18 deletions(-) create mode 100644 src/Web/Avalonia.Web.Blazor/BlazorSkiaRasterSurface.cs create mode 100644 src/Web/Avalonia.Web.Blazor/IBlazorSkiaSurface.cs diff --git a/samples/ControlCatalog.Web/App.razor.cs b/samples/ControlCatalog.Web/App.razor.cs index c0b7ddbe1e..560e8079a6 100644 --- a/samples/ControlCatalog.Web/App.razor.cs +++ b/samples/ControlCatalog.Web/App.razor.cs @@ -11,6 +11,7 @@ public partial class App { ControlCatalog.Pages.EmbedSample.Implementation = new EmbedSampleWeb(); }) + //.With(new SkiaOptions { CustomGpuFactory = null }) // uncomment to disable GPU/GL rendering .SetupWithSingleViewLifetime(); base.OnParametersSet(); diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs index b575bc6dbb..0e5580ebe4 100644 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs +++ b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs @@ -37,6 +37,7 @@ namespace Avalonia.Web.Blazor private const SKColorType ColorType = SKColorType.Rgba8888; private bool _initialised; + private bool _useGL; [Inject] private IJSRuntime Js { get; set; } = null!; @@ -261,25 +262,44 @@ namespace Avalonia.Web.Blazor _interop = await SKHtmlCanvasInterop.ImportAsync(Js, _htmlCanvas, OnRenderFrame); Console.WriteLine("Interop created"); - _jsGlInfo = _interop.InitGL(); - - Console.WriteLine("jsglinfo created - init gl"); + + var skiaOptions = AvaloniaLocator.Current.GetService(); + _useGL = skiaOptions?.CustomGpuFactory != null; - // create the SkiaSharp context - if (_context == null) + if (_useGL) + { + _jsGlInfo = _interop.InitGL(); + Console.WriteLine("jsglinfo created - init gl"); + } + else { - Console.WriteLine("create glcontext"); - _glInterface = GRGlInterface.Create(); - _context = GRContext.CreateGl(_glInterface); - - var options = AvaloniaLocator.Current.GetService(); - // bump the default resource cache limit - _context.SetResourceCacheLimit(options?.MaxGpuResourceSizeBytes ?? 32 * 1024 * 1024); - Console.WriteLine("glcontext created and resource limit set"); + var rasterInitialized = _interop.InitRaster(); + Console.WriteLine("raster initialized: {0}", rasterInitialized); } - _topLevelImpl.SetSurface(_context, _jsGlInfo, ColorType, - new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi); + if (_useGL) + { + // create the SkiaSharp context + if (_context == null) + { + Console.WriteLine("create glcontext"); + _glInterface = GRGlInterface.Create(); + _context = GRContext.CreateGl(_glInterface); + + + // bump the default resource cache limit + _context.SetResourceCacheLimit(skiaOptions?.MaxGpuResourceSizeBytes ?? 32 * 1024 * 1024); + Console.WriteLine("glcontext created and resource limit set"); + } + + _topLevelImpl.SetSurface(_context, _jsGlInfo!, ColorType, + new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi); + } + else + { + _topLevelImpl.SetSurface(ColorType, + new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, _interop.PutImageData); + } _interop.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); @@ -301,7 +321,12 @@ namespace Avalonia.Web.Blazor private void OnRenderFrame() { - if (_canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0 || _jsGlInfo == null) + if (_useGL && (_jsGlInfo == null)) + { + Console.WriteLine("nothing to render"); + return; + } + if (_canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0) { Console.WriteLine("nothing to render"); return; diff --git a/src/Web/Avalonia.Web.Blazor/BlazorSkiaRasterSurface.cs b/src/Web/Avalonia.Web.Blazor/BlazorSkiaRasterSurface.cs new file mode 100644 index 0000000000..a286affe04 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/BlazorSkiaRasterSurface.cs @@ -0,0 +1,91 @@ +using System.Runtime.InteropServices; +using Avalonia.Controls.Platform.Surfaces; +using Avalonia.Platform; +using Avalonia.Skia; +using SkiaSharp; + +namespace Avalonia.Web.Blazor +{ + internal class BlazorSkiaRasterSurface : IBlazorSkiaSurface, IFramebufferPlatformSurface, IDisposable + { + public SKColorType ColorType { get; set; } + + public PixelSize Size { get; set; } + + public double Scaling { get; set; } + + private FramebufferData? _fbData; + private readonly Action _blitCallback; + private readonly Action _onDisposeAction; + + public BlazorSkiaRasterSurface( + SKColorType colorType, PixelSize size, double scaling, Action blitCallback) + { + ColorType = colorType; + Size = size; + Scaling = scaling; + _blitCallback = blitCallback; + _onDisposeAction = Blit; + } + + public void Dispose() + { + _fbData?.Dispose(); + _fbData = null; + } + + public ILockedFramebuffer Lock() + { + var bytesPerPixel = 4; // TODO: derive from ColorType + var dpi = Scaling * 96.0; + var width = (int)(Size.Width * Scaling); + var height = (int)(Size.Height * Scaling); + + if (_fbData is null || _fbData?.Size.Width != width || _fbData?.Size.Height != height) + { + _fbData?.Dispose(); + _fbData = new FramebufferData(width, height, bytesPerPixel); + } + + var pixelFormat = ColorType.ToPixelFormat(); + var data = _fbData.Value; + return new LockedFramebuffer( + data.Address, data.Size, data.RowBytes, + new Vector(dpi, dpi), pixelFormat, _onDisposeAction); + } + + private void Blit() + { + if (_fbData != null) + { + var data = _fbData.Value; + _blitCallback(data.Address, new SKSizeI(data.Size.Width, data.Size.Height)); + } + } + + private readonly struct FramebufferData + { + private readonly byte[]? _data; + private readonly GCHandle _dataHandle; + + public PixelSize Size { get; } + + public int RowBytes { get; } + + public IntPtr Address => _dataHandle.AddrOfPinnedObject(); + + public FramebufferData(int width, int height, int bytesPerPixel) + { + Size = new PixelSize(width, height); + RowBytes = width * bytesPerPixel; + _data = new byte[width * height * bytesPerPixel]; + _dataHandle = GCHandle.Alloc(_data, GCHandleType.Pinned); + } + + public void Dispose() + { + _dataHandle.Free(); + } + } + } +} diff --git a/src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs b/src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs index 512309cfe3..fb49df338b 100644 --- a/src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs +++ b/src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs @@ -3,7 +3,7 @@ using SkiaSharp; namespace Avalonia.Web.Blazor { - internal class BlazorSkiaSurface + internal class BlazorSkiaSurface : IBlazorSkiaSurface { public BlazorSkiaSurface(GRContext context, SKHtmlCanvasInterop.GLInfo glInfo, SKColorType colorType, PixelSize size, double scaling, GRSurfaceOrigin origin) { diff --git a/src/Web/Avalonia.Web.Blazor/IBlazorSkiaSurface.cs b/src/Web/Avalonia.Web.Blazor/IBlazorSkiaSurface.cs new file mode 100644 index 0000000000..5463893e27 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/IBlazorSkiaSurface.cs @@ -0,0 +1,9 @@ +namespace Avalonia.Web.Blazor +{ + internal interface IBlazorSkiaSurface + { + public PixelSize Size { get; set; } + + public double Scaling { get; set; } + } +} diff --git a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs index a8a1a970dc..e240f1554e 100644 --- a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs +++ b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs @@ -16,7 +16,7 @@ namespace Avalonia.Web.Blazor internal class RazorViewTopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost { private Size _clientSize; - private BlazorSkiaSurface? _currentSurface; + private IBlazorSkiaSurface? _currentSurface; private IInputRoot? _inputRoot; private readonly Stopwatch _sw = Stopwatch.StartNew(); private readonly AvaloniaView _avaloniaView; @@ -40,6 +40,11 @@ namespace Avalonia.Web.Blazor new BlazorSkiaSurface(context, glInfo, colorType, size, scaling, GRSurfaceOrigin.BottomLeft); } + internal void SetSurface(SKColorType colorType, PixelSize size, double scaling, Action blitCallback) + { + _currentSurface = new BlazorSkiaRasterSurface(colorType, size, scaling, blitCallback); + } + public void SetClientSize(SKSize size, double dpi) { var newSize = new Size(size.Width, size.Height); From e50b416d5bf1529544830d5a291a1fb2f1b0da64 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 30 Jun 2022 12:50:52 +0200 Subject: [PATCH 139/389] Fix parent selectors with /template/ at end. Previously the combinator state was lost when traversing nested selectors. To fix it, instead of running the parent selector in `NestingSelector.Evaluate`, return the parent selector with `MovePrevious`. This required `MovePrevious` to be aware of parent styles and because I had to change its signature, I also made it internal as it doesn't need to be a public API. --- src/Avalonia.Base/Styling/ChildSelector.cs | 2 +- .../Styling/DescendentSelector.cs | 2 +- src/Avalonia.Base/Styling/NestingSelector.cs | 8 +- src/Avalonia.Base/Styling/NotSelector.cs | 2 +- src/Avalonia.Base/Styling/NthChildSelector.cs | 2 +- src/Avalonia.Base/Styling/OrSelector.cs | 2 +- .../Styling/PropertyEqualsSelector.cs | 2 +- src/Avalonia.Base/Styling/Selector.cs | 12 ++- src/Avalonia.Base/Styling/TemplateSelector.cs | 2 +- .../Styling/TypeNameAndClassSelector.cs | 2 +- .../Styling/SelectorTests_Nesting.cs | 80 +++++++++++++++++++ 11 files changed, 103 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Base/Styling/ChildSelector.cs b/src/Avalonia.Base/Styling/ChildSelector.cs index 9512dc34df..bc1d257ce6 100644 --- a/src/Avalonia.Base/Styling/ChildSelector.cs +++ b/src/Avalonia.Base/Styling/ChildSelector.cs @@ -64,7 +64,7 @@ namespace Avalonia.Styling } } - protected override Selector? MovePrevious() => null; + private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (null, null); protected override Selector? MovePreviousOrParent() => _parent; } } diff --git a/src/Avalonia.Base/Styling/DescendentSelector.cs b/src/Avalonia.Base/Styling/DescendentSelector.cs index 677a924189..3a16574e04 100644 --- a/src/Avalonia.Base/Styling/DescendentSelector.cs +++ b/src/Avalonia.Base/Styling/DescendentSelector.cs @@ -69,7 +69,7 @@ namespace Avalonia.Styling } } - protected override Selector? MovePrevious() => null; + private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (null, null); protected override Selector? MovePreviousOrParent() => _parent; } } diff --git a/src/Avalonia.Base/Styling/NestingSelector.cs b/src/Avalonia.Base/Styling/NestingSelector.cs index 77c5b719c6..dd0bac31c6 100644 --- a/src/Avalonia.Base/Styling/NestingSelector.cs +++ b/src/Avalonia.Base/Styling/NestingSelector.cs @@ -17,7 +17,7 @@ namespace Avalonia.Styling { if (parent is Style s && s.Selector is not null) { - return s.Selector.Match(control, s.Parent, subscribe); + return SelectorMatch.AlwaysThisType; } else if (parent is ControlTheme theme) { @@ -32,7 +32,11 @@ namespace Avalonia.Styling "Nesting selector was specified but cannot determine parent selector."); } - protected override Selector? MovePrevious() => null; + private protected override (Selector?, IStyle?) MovePrevious(IStyle? parent) + { + return parent is Style parentStyle ? (parentStyle.Selector, parentStyle.Parent) : (null, null); + } + protected override Selector? MovePreviousOrParent() => null; } } diff --git a/src/Avalonia.Base/Styling/NotSelector.cs b/src/Avalonia.Base/Styling/NotSelector.cs index c7727bb6b8..ebde392a3b 100644 --- a/src/Avalonia.Base/Styling/NotSelector.cs +++ b/src/Avalonia.Base/Styling/NotSelector.cs @@ -66,7 +66,7 @@ namespace Avalonia.Styling } } - protected override Selector? MovePrevious() => _previous; + private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (_previous, nestingParent); protected override Selector? MovePreviousOrParent() => _previous; } } diff --git a/src/Avalonia.Base/Styling/NthChildSelector.cs b/src/Avalonia.Base/Styling/NthChildSelector.cs index f473791664..e6d9cf58a9 100644 --- a/src/Avalonia.Base/Styling/NthChildSelector.cs +++ b/src/Avalonia.Base/Styling/NthChildSelector.cs @@ -104,7 +104,7 @@ namespace Avalonia.Styling return match ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance; } - protected override Selector? MovePrevious() => _previous; + private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (_previous, nestingParent); protected override Selector? MovePreviousOrParent() => _previous; public override string ToString() diff --git a/src/Avalonia.Base/Styling/OrSelector.cs b/src/Avalonia.Base/Styling/OrSelector.cs index af9249864f..115e0aeb95 100644 --- a/src/Avalonia.Base/Styling/OrSelector.cs +++ b/src/Avalonia.Base/Styling/OrSelector.cs @@ -102,7 +102,7 @@ namespace Avalonia.Styling } } - protected override Selector? MovePrevious() => null; + private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (null, null); protected override Selector? MovePreviousOrParent() => null; internal override void ValidateNestingSelector(bool inControlTheme) diff --git a/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs index 48136ba2de..96f8c8dfeb 100644 --- a/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs +++ b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs @@ -89,7 +89,7 @@ namespace Avalonia.Styling } - protected override Selector? MovePrevious() => _previous; + private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (_previous, nestingParent); protected override Selector? MovePreviousOrParent() => _previous; internal static bool Compare(Type propertyType, object? propertyValue, object? value) diff --git a/src/Avalonia.Base/Styling/Selector.cs b/src/Avalonia.Base/Styling/Selector.cs index 7ce17518dd..cc8598c5e3 100644 --- a/src/Avalonia.Base/Styling/Selector.cs +++ b/src/Avalonia.Base/Styling/Selector.cs @@ -84,7 +84,13 @@ namespace Avalonia.Styling /// /// Moves to the previous selector. /// - protected abstract Selector? MovePrevious(); + /// + /// The parent style, if the selector is on a nested style. + /// + /// + /// The previous selector, and its nesting parent. + /// + private protected abstract (Selector?, IStyle?) MovePrevious(IStyle? nestingParent); /// /// Moves to the previous selector or the parent selector. @@ -142,14 +148,14 @@ namespace Avalonia.Styling ref AndActivatorBuilder activators, ref Selector? combinator) { - var previous = selector.MovePrevious(); + var (previous, previousParent) = selector.MovePrevious(parent); // Selectors are stored from right-to-left, so we recurse into the selector in order to // reverse this order, because the type selector will be on the left and is our best // opportunity to exit early. if (previous != null && !previous.IsCombinator) { - var previousMatch = Match(control, previous, parent, subscribe, ref activators, ref combinator); + var previousMatch = Match(control, previous, previousParent, subscribe, ref activators, ref combinator); if (previousMatch < SelectorMatchResult.Sometimes) { diff --git a/src/Avalonia.Base/Styling/TemplateSelector.cs b/src/Avalonia.Base/Styling/TemplateSelector.cs index 278e24a203..a68b7003b8 100644 --- a/src/Avalonia.Base/Styling/TemplateSelector.cs +++ b/src/Avalonia.Base/Styling/TemplateSelector.cs @@ -48,7 +48,7 @@ namespace Avalonia.Styling return _parent.Match(templatedParent, parent, subscribe); } - protected override Selector? MovePrevious() => null; + private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (null, null); protected override Selector? MovePreviousOrParent() => _parent; } } diff --git a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs index 6681a7da36..3a2150b1e9 100644 --- a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs @@ -139,7 +139,7 @@ namespace Avalonia.Styling return Name == null ? SelectorMatch.AlwaysThisType : SelectorMatch.AlwaysThisInstance; } - protected override Selector? MovePrevious() => _previous; + private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (_previous, nestingParent); protected override Selector? MovePreviousOrParent() => _previous; private string BuildSelectorString() diff --git a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs index 1520dc329d..bd7f0338e6 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Controls; +using Avalonia.Controls.Templates; using Avalonia.Styling; using Avalonia.Styling.Activators; using Xunit; @@ -148,6 +149,85 @@ namespace Avalonia.Base.UnitTests.Styling control.Classes.Remove("foo"); Assert.False(sink.Active); } + + [Fact] + public void Template_Nesting_OfType_Matches() + { + var control = new Control1 { Classes = { "foo" } }; + var button = new Button + { + Template = new FuncControlTemplate((x, _) => control), + }; + + button.ApplyTemplate(); + + Style nested; + var parent = new Style(x => x.OfType /// The control. - private void ApplyTemplatedParent(IControl control) + internal static void ApplyTemplatedParent(IStyledElement control, ITemplatedControl? templatedParent) { - control.SetValue(TemplatedParentProperty, this); + control.SetValue(TemplatedParentProperty, templatedParent); var children = control.LogicalChildren; var count = children.Count; for (var i = 0; i < count; i++) { - if (children[i] is IControl child) + if (children[i] is IStyledElement child) { - ApplyTemplatedParent(child); + ApplyTemplatedParent(child, templatedParent); } } } diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index 91c93c87c8..bb18bf4c64 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -271,8 +271,9 @@ namespace Avalonia.Controls _popupHost = OverlayPopupHost.CreatePopupHost(control, null); _popupHost.SetChild(this); ((ISetLogicalParent)_popupHost).SetParent(control); - - _popupHost.ConfigurePosition(control, GetPlacement(control), + ApplyTemplatedParent(this, control.TemplatedParent); + + _popupHost.ConfigurePosition(control, GetPlacement(control), new Point(GetHorizontalOffset(control), GetVerticalOffset(control))); WindowManagerAddShadowHintChanged(_popupHost, false); diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index 8c05f2a0a7..6fb7b1448c 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -7,6 +7,7 @@ using System.Reactive.Disposables; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; using Avalonia.Data; @@ -877,6 +878,110 @@ namespace Avalonia.LeakTests } } + [Fact] + public void ToolTip_Is_Freed() + { + using (Start()) + { + Func run = () => + { + var window = new Window(); + var source = new Button + { + Template = new FuncControlTemplate public string? DefaultExtension { get; set; } - /// - /// Gets or sets the initial location where the file open picker looks for files to present to the user. - /// - public IStorageFolder? SuggestedStartLocation { get; set; } - /// /// Gets or sets the collection of valid file types that the user can choose to assign to a file. /// diff --git a/src/Avalonia.Base/Platform/Storage/FolderPickerOpenOptions.cs b/src/Avalonia.Base/Platform/Storage/FolderPickerOpenOptions.cs index de90da30b2..df9fa8fd35 100644 --- a/src/Avalonia.Base/Platform/Storage/FolderPickerOpenOptions.cs +++ b/src/Avalonia.Base/Platform/Storage/FolderPickerOpenOptions.cs @@ -3,20 +3,10 @@ /// /// Options class for method. /// -public class FolderPickerOpenOptions +public class FolderPickerOpenOptions : PickerOptions { - /// - /// Gets or sets the text that appears in the title bar of a folder dialog. - /// - public string? Title { get; set; } - /// /// Gets or sets an option indicating whether open picker allows users to select multiple folders. /// public bool AllowMultiple { get; set; } - - /// - /// Gets or sets the initial location where the file open picker looks for files to present to the user. - /// - public IStorageFolder? SuggestedStartLocation { get; set; } } diff --git a/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs b/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs index 65811b7fbd..d21c950862 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs @@ -17,5 +17,4 @@ public interface IStorageBookmarkFile : IStorageFile, IStorageBookmarkItem [NotClientImplementable] public interface IStorageBookmarkFolder : IStorageFolder, IStorageBookmarkItem { - } diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFile.cs b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs index 2f12514e50..965caf8216 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageFile.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs @@ -14,7 +14,7 @@ public interface IStorageFile : IStorageItem /// Returns true, if file is readable. /// bool CanOpenRead { get; } - + /// /// Opens a stream for read access. /// @@ -24,9 +24,9 @@ public interface IStorageFile : IStorageItem /// Returns true, if file is writeable. /// bool CanOpenWrite { get; } - + /// /// Opens stream for writing to the file. /// Task OpenWrite(); -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs index 83b316bc3b..25b9f01a92 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs @@ -8,5 +8,4 @@ namespace Avalonia.Platform.Storage; [NotClientImplementable] public interface IStorageFolder : IStorageItem { - -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/Platform/Storage/IStorageItem.cs b/src/Avalonia.Base/Platform/Storage/IStorageItem.cs index 078311a286..8513ebc7d9 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageItem.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageItem.cs @@ -27,7 +27,7 @@ public interface IStorageItem : IDisposable /// Browser and iOS backends might return relative uris. /// bool TryGetUri([NotNullWhen(true)] out Uri? uri); - + /// /// Gets the basic properties of the current item. /// @@ -37,7 +37,7 @@ public interface IStorageItem : IDisposable /// Returns true is item can be bookmarked and reused later. /// bool CanBookmark { get; } - + /// /// Saves items to a bookmark. /// diff --git a/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs index 32fb148790..0f5cf931d9 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs @@ -11,7 +11,7 @@ public interface IStorageProvider /// Returns true if it's possible to open file picker on the current platform. /// bool CanOpen { get; } - + /// /// Opens file picker dialog. /// @@ -22,7 +22,7 @@ public interface IStorageProvider /// Returns true if it's possible to open save file picker on the current platform. /// bool CanSave { get; } - + /// /// Opens save file picker dialog. /// @@ -33,13 +33,13 @@ public interface IStorageProvider /// Returns true if it's possible to open folder picker on the current platform. /// bool CanPickFolder { get; } - + /// /// Opens folder picker dialog. /// /// Array of selected or empty collection if user canceled the dialog. Task> OpenFolderPickerAsync(FolderPickerOpenOptions options); - + /// /// Open from the bookmark ID. /// diff --git a/src/Avalonia.Base/Platform/Storage/PickerOptions.cs b/src/Avalonia.Base/Platform/Storage/PickerOptions.cs new file mode 100644 index 0000000000..6f97916a26 --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/PickerOptions.cs @@ -0,0 +1,17 @@ +namespace Avalonia.Platform.Storage; + +/// +/// Common options for , and methods. +/// +public class PickerOptions +{ + /// + /// Gets or sets the text that appears in the title bar of a folder dialog. + /// + public string? Title { get; set; } + + /// + /// Gets or sets the initial location where the file open picker looks for files to present to the user. + /// + public IStorageFolder? SuggestedStartLocation { get; set; } +} diff --git a/src/Avalonia.Base/Platform/Storage/StorageItemProperties.cs b/src/Avalonia.Base/Platform/Storage/StorageItemProperties.cs index 41b9bfa941..a63973ccf8 100644 --- a/src/Avalonia.Base/Platform/Storage/StorageItemProperties.cs +++ b/src/Avalonia.Base/Platform/Storage/StorageItemProperties.cs @@ -16,7 +16,7 @@ public class StorageItemProperties DateCreated = dateCreated; DateModified = dateModified; } - + /// /// Gets the size of the file in bytes. /// @@ -40,4 +40,4 @@ public class StorageItemProperties /// Can be null if property is not available. /// public DateTimeOffset? DateModified { get; } -} \ No newline at end of file +} From 6370ae38de78ab9c4bafa343e8870ee6d531b49c Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 5 Jul 2022 21:58:26 -0400 Subject: [PATCH 170/389] Switch to TemplateBinding in ColorPicker where possible --- .../Themes/Fluent/ColorPicker.xaml | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml index 878d7819eb..46d627b3a1 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -49,31 +49,31 @@ + Color="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" + ColorModel="{Binding ColorModel, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" + ColorSpectrumComponents="{TemplateBinding ColorSpectrumComponents}" + ColorSpectrumShape="{TemplateBinding ColorSpectrumShape}" + HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" + IsAlphaEnabled="{TemplateBinding IsAlphaEnabled}" + IsAlphaVisible="{TemplateBinding IsAlphaVisible}" + IsColorComponentsVisible="{TemplateBinding IsColorComponentsVisible}" + IsColorPaletteVisible="{TemplateBinding IsColorPaletteVisible}" + IsColorPreviewVisible="{TemplateBinding IsColorPreviewVisible}" + IsColorSpectrumVisible="{TemplateBinding IsColorSpectrumVisible}" + IsColorSpectrumSliderVisible="{TemplateBinding IsColorSpectrumSliderVisible}" + IsComponentTextInputVisible="{TemplateBinding IsComponentTextInputVisible}" + IsHexInputVisible="{TemplateBinding IsHexInputVisible}" + MaxHue="{TemplateBinding MaxHue}" + MaxSaturation="{TemplateBinding MaxSaturation}" + MaxValue="{TemplateBinding MaxValue}" + MinHue="{TemplateBinding MinHue}" + MinSaturation="{TemplateBinding MinSaturation}" + MinValue="{TemplateBinding MinValue}" + PaletteColors="{TemplateBinding PaletteColors}" + PaletteColumnCount="{TemplateBinding PaletteColumnCount}" + Palette="{TemplateBinding Palette}" + SelectedIndex="{Binding SelectedIndex, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" + ShowAccentColors="{TemplateBinding ShowAccentColors}" /> From a24720816f593e24bda2dd58ff6b6a56cdb23fd1 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 5 Jul 2022 22:12:18 -0400 Subject: [PATCH 171/389] Standardize ColorControl resource names This follows the Fluent standard where the resource is prefixed with the control itself --- .../Themes/Default/ColorPreviewer.xaml | 18 +++++------ .../Themes/Default/ColorSlider.xaml | 4 +-- .../Themes/Default/Default.xaml | 2 +- .../Themes/Fluent/ColorPicker.xaml | 2 +- .../Themes/Fluent/ColorPreviewer.xaml | 18 +++++------ .../Themes/Fluent/ColorSlider.xaml | 4 +-- .../Themes/Fluent/ColorView.xaml | 30 +++++++++---------- .../Themes/Fluent/Fluent.xaml | 2 +- 8 files changed, 40 insertions(+), 40 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml index 24242fc251..6ac0982408 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml @@ -9,8 +9,8 @@ - 80 - 40 + 80 + 40 + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index 867e0f223f..9e795d81a2 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -13,6 +13,8 @@ + + 48 30 @@ -90,29 +92,26 @@ - - + + - - - + CornerRadius="{TemplateBinding CornerRadius, Converter={StaticResource BottomCornerRadiusFilterConverter}}" + Background="Transparent" + BorderBrush="Transparent" + BorderThickness="0,1,0,0" /> Date: Tue, 5 Jul 2022 23:32:35 -0400 Subject: [PATCH 173/389] Add code to hide the tab strip when only one tab is visible --- .../ColorView/ColorView.cs | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index 8fb80980d5..bea982b7ea 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -4,9 +4,11 @@ using System.Collections.ObjectModel; using System.Globalization; using Avalonia.Controls.Converters; using Avalonia.Controls.Metadata; +using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Media; using Avalonia.Threading; +using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -82,19 +84,19 @@ namespace Avalonia.Controls if (_tabControl != null && _tabControl.Items != null) { - // Determine if any item is visible - bool isAnyItemVisible = false; + // Determine the number of visible tab items + int numVisibleItems = 0; foreach (var item in _tabControl.Items) { if (item is Control control && control.IsVisible) { - isAnyItemVisible = true; - break; + numVisibleItems++; } } - if (isAnyItemVisible) + // Verify the selection + if (numVisibleItems > 0) { object? selectedItem = null; @@ -140,6 +142,23 @@ namespace Avalonia.Controls _tabControl.IsVisible = false; } + // Hide the "tab strip" if there is only one tab + // This allows, for example, to view only the palette + /* + var itemsPresenter = _tabControl.FindDescendantOfType(); + if (itemsPresenter != null) + { + if (numVisibleItems == 1) + { + itemsPresenter.IsVisible = false; + } + else + { + itemsPresenter.IsVisible = true; + } + } + */ + // Note that if externally the SelectedIndex is set to 4 or something // outside the valid range, the TabControl will ignore it and replace it // with a valid SelectedIndex. This however is not propagated back through From 0c81cb24801b7c7761c0b00f0675c8ff08754822 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Thu, 9 Jun 2022 10:56:45 +0200 Subject: [PATCH 174/389] fix: DBus Null annotations --- Avalonia.sln | 1 + .../Avalonia.FreeDesktop.csproj | 4 ++ src/Avalonia.FreeDesktop/DBusCallQueue.cs | 20 +++----- src/Avalonia.FreeDesktop/DBusHelper.cs | 4 +- .../DBusIme/DBusTextInputMethodBase.cs | 46 ++++++++++--------- .../DBusIme/Fcitx/FcitxDBus.cs | 28 +++++------ .../DBusIme/Fcitx/FcitxICWrapper.cs | 31 +++++++------ .../DBusIme/Fcitx/FcitxX11TextInputMethod.cs | 38 ++++++++------- .../DBusIme/IBus/IBusDBus.cs | 38 +++++++-------- .../DBusIme/IBus/IBusX11TextInputMethod.cs | 30 ++++++++---- .../DBusIme/X11DBusImeHelper.cs | 2 +- src/Avalonia.FreeDesktop/DBusMenu.cs | 16 +++---- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 36 +++++++-------- src/Avalonia.FreeDesktop/DBusRequest.cs | 2 +- .../LinuxMountedVolumeInfoProvider.cs | 2 +- .../Avalonia.SourceGenerator.csproj | 1 + .../Output/DrmOutput.cs | 2 +- .../IsExternalInit.cs | 0 18 files changed, 161 insertions(+), 140 deletions(-) rename src/{Avalonia.SourceGenerator => Shared}/IsExternalInit.cs (100%) diff --git a/Avalonia.sln b/Avalonia.sln index 25c7daf080..8d2479a663 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -38,6 +38,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DEF5-D50F-4975-8B72-124C9EB54066}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + src\Shared\IsExternalInit.cs = src\Shared\IsExternalInit.cs src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs src\Shared\SourceGeneratorAttributes.cs = src\Shared\SourceGeneratorAttributes.cs EndProjectSection diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj index a5cb207223..3b1c6cc7b1 100644 --- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -5,6 +5,10 @@ enable + + + + diff --git a/src/Avalonia.FreeDesktop/DBusCallQueue.cs b/src/Avalonia.FreeDesktop/DBusCallQueue.cs index 5cd748be02..e7c07dcbf9 100644 --- a/src/Avalonia.FreeDesktop/DBusCallQueue.cs +++ b/src/Avalonia.FreeDesktop/DBusCallQueue.cs @@ -8,10 +8,9 @@ namespace Avalonia.FreeDesktop { private readonly Func _errorHandler; - class Item + record Item(Func Callback) { - public Func Callback; - public Action OnFinish; + public Action? OnFinish; } private Queue _q = new Queue(); private bool _processing; @@ -23,19 +22,15 @@ namespace Avalonia.FreeDesktop public void Enqueue(Func cb) { - _q.Enqueue(new Item - { - Callback = cb - }); + _q.Enqueue(new Item(cb)); Process(); } public Task EnqueueAsync(Func cb) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - _q.Enqueue(new Item + _q.Enqueue(new Item(cb) { - Callback = cb, OnFinish = e => { if (e == null) @@ -51,13 +46,12 @@ namespace Avalonia.FreeDesktop public Task EnqueueAsync(Func> cb) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - _q.Enqueue(new Item - { - Callback = async () => + _q.Enqueue(new Item(async () => { var res = await cb(); tcs.TrySetResult(res); - }, + }) + { OnFinish = e => { if (e != null) diff --git a/src/Avalonia.FreeDesktop/DBusHelper.cs b/src/Avalonia.FreeDesktop/DBusHelper.cs index 7204e51dbd..9f9d75b411 100644 --- a/src/Avalonia.FreeDesktop/DBusHelper.cs +++ b/src/Avalonia.FreeDesktop/DBusHelper.cs @@ -17,7 +17,7 @@ namespace Avalonia.FreeDesktop private readonly object _lock = new(); private SynchronizationContext? _ctx; - public override void Post(SendOrPostCallback d, object state) + public override void Post(SendOrPostCallback d, object? state) { lock (_lock) { @@ -29,7 +29,7 @@ namespace Avalonia.FreeDesktop } } - public override void Send(SendOrPostCallback d, object state) + public override void Send(SendOrPostCallback d, object? state) { lock (_lock) { diff --git a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs index 864c579319..eef865d458 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Reflection; using System.Threading.Tasks; -using Avalonia.FreeDesktop.DBusIme.Fcitx; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; using Avalonia.Logging; @@ -26,7 +24,7 @@ namespace Avalonia.FreeDesktop.DBusIme return (im, im); } } - + internal abstract class DBusTextInputMethodBase : IX11InputMethodControl, ITextInputMethodImpl { private List _disposables = new List(); @@ -34,7 +32,7 @@ namespace Avalonia.FreeDesktop.DBusIme protected Connection Connection { get; } private readonly string[] _knownNames; private bool _connecting; - private string _currentName; + private string? _currentName; private DBusCallQueue _queue; private bool _controlActive, _windowActive; private bool? _imeActive; @@ -42,9 +40,9 @@ namespace Avalonia.FreeDesktop.DBusIme private PixelRect? _lastReportedRect; private double _scaling = 1; private PixelPoint _windowPosition; - + protected bool IsConnected => _currentName != null; - + public DBusTextInputMethodBase(Connection connection, params string[] knownNames) { _queue = new DBusCallQueue(QueueOnError); @@ -58,18 +56,18 @@ namespace Avalonia.FreeDesktop.DBusIme foreach (var name in _knownNames) _disposables.Add(await Connection.ResolveServiceOwnerAsync(name, OnNameChange)); } - + protected abstract Task Connect(string name); protected string GetAppName() => - Application.Current.Name ?? Assembly.GetEntryAssembly()?.GetName()?.Name ?? "Avalonia"; - + Application.Current?.Name ?? Assembly.GetEntryAssembly()?.GetName()?.Name ?? "Avalonia"; + private async void OnNameChange(ServiceOwnerChangedEventArgs args) { if (args.NewOwner != null && _currentName == null) { _onlineNamesQueue.Enqueue(args.ServiceName); - if(!_connecting) + if (!_connecting) { _connecting = true; try @@ -98,25 +96,25 @@ namespace Avalonia.FreeDesktop.DBusIme _connecting = false; } } - + } - + // IME has crashed if (args.NewOwner == null && args.ServiceName == _currentName) { _currentName = null; - foreach(var s in _disposables) + foreach (var s in _disposables) s.Dispose(); _disposables.Clear(); - + OnDisconnected(); Reset(); - + // Watch again Watch(); } } - + protected virtual Task Disconnect() { return Task.CompletedTask; @@ -124,7 +122,7 @@ namespace Avalonia.FreeDesktop.DBusIme protected virtual void OnDisconnected() { - + } protected virtual void Reset() @@ -149,10 +147,14 @@ namespace Avalonia.FreeDesktop.DBusIme OnDisconnected(); _currentName = null; } - + protected void Enqueue(Func cb) => _queue.Enqueue(cb); - protected void AddDisposable(IDisposable d) => _disposables.Add(d); + protected void AddDisposable(IDisposable? d) + { + if(d is { }) + _disposables.Add(d); + } public void Dispose() { @@ -198,7 +200,7 @@ namespace Avalonia.FreeDesktop.DBusIme UpdateActive(); } - void ITextInputMethodImpl.SetClient(ITextInputMethodClient client) + void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client) { _controlActive = client is { }; UpdateActive(); @@ -225,7 +227,7 @@ namespace Avalonia.FreeDesktop.DBusIme } } - private Action _onCommit; + private Action? _onCommit; event Action IX11InputMethodControl.Commit { add => _onCommit += value; @@ -234,7 +236,7 @@ namespace Avalonia.FreeDesktop.DBusIme protected void FireCommit(string s) => _onCommit?.Invoke(s); - private Action _onForward; + private Action? _onForward; event Action IX11InputMethodControl.ForwardKey { add => _onForward += value; diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs index 7ce2339763..06afacaa29 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs @@ -31,15 +31,15 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor); Task DestroyICAsync(); Task ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, int Type, uint Time); - Task WatchEnableIMAsync(Action handler, Action onError = null); - Task WatchCloseIMAsync(Action handler, Action onError = null); - Task WatchCommitStringAsync(Action handler, Action onError = null); - Task WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action onError = null); - Task WatchUpdatePreeditAsync(Action<(string str, int cursorpos)> handler, Action onError = null); - Task WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action onError = null); - Task WatchUpdateClientSideUIAsync(Action<(string auxup, string auxdown, string preedit, string candidateword, string imname, int cursorpos)> handler, Action onError = null); - Task WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler, Action onError = null); - Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action onError = null); + Task WatchEnableIMAsync(Action handler, Action? onError = null); + Task WatchCloseIMAsync(Action handler, Action? onError = null); + Task WatchCommitStringAsync(Action handler, Action? onError = null); + Task WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action? onError = null); + Task WatchUpdatePreeditAsync(Action<(string str, int cursorpos)> handler, Action? onError = null); + Task WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action? onError = null); + Task WatchUpdateClientSideUIAsync(Action<(string auxup, string auxdown, string preedit, string candidateword, string imname, int cursorpos)> handler, Action? onError = null); + Task WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler, Action? onError = null); + Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action? onError = null); } [DBusInterface("org.fcitx.Fcitx.InputContext1")] @@ -54,11 +54,11 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor); Task DestroyICAsync(); Task ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, bool Type, uint Time); - Task WatchCommitStringAsync(Action handler, Action onError = null); - Task WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action onError = null); - Task WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action onError = null); - Task WatchForwardKeyAsync(Action<(uint keyval, uint state, bool type)> handler, Action onError = null); - Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action onError = null); + Task WatchCommitStringAsync(Action handler, Action? onError = null); + Task WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action? onError = null); + Task WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action? onError = null); + Task WatchForwardKeyAsync(Action<(uint keyval, uint state, bool type)> handler, Action? onError = null); + Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action? onError = null); } [DBusInterface("org.fcitx.Fcitx.InputMethod1")] diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs index a03ea213aa..6c503edb41 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs @@ -5,8 +5,8 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx { internal class FcitxICWrapper { - private readonly IFcitxInputContext1 _modern; - private readonly IFcitxInputContext _old; + private readonly IFcitxInputContext1? _modern; + private readonly IFcitxInputContext? _old; public FcitxICWrapper(IFcitxInputContext old) { @@ -18,34 +18,37 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx _modern = modern; } - public Task FocusInAsync() => _old?.FocusInAsync() ?? _modern.FocusInAsync(); + public Task FocusInAsync() => _old?.FocusInAsync() ?? _modern?.FocusInAsync() ?? Task.CompletedTask; - public Task FocusOutAsync() => _old?.FocusOutAsync() ?? _modern.FocusOutAsync(); + public Task FocusOutAsync() => _old?.FocusOutAsync() ?? _modern?.FocusOutAsync() ?? Task.CompletedTask; - public Task ResetAsync() => _old?.ResetAsync() ?? _modern.ResetAsync(); + public Task ResetAsync() => _old?.ResetAsync() ?? _modern?.ResetAsync() ?? Task.CompletedTask; public Task SetCursorRectAsync(int x, int y, int w, int h) => - _old?.SetCursorRectAsync(x, y, w, h) ?? _modern.SetCursorRectAsync(x, y, w, h); - public Task DestroyICAsync() => _old?.DestroyICAsync() ?? _modern.DestroyICAsync(); + _old?.SetCursorRectAsync(x, y, w, h) ?? _modern?.SetCursorRectAsync(x, y, w, h) ?? Task.CompletedTask; + public Task DestroyICAsync() => _old?.DestroyICAsync() ?? _modern?.DestroyICAsync() ?? Task.CompletedTask; public async Task ProcessKeyEventAsync(uint keyVal, uint keyCode, uint state, int type, uint time) { if(_old!=null) return await _old.ProcessKeyEventAsync(keyVal, keyCode, state, type, time) != 0; - return await _modern.ProcessKeyEventAsync(keyVal, keyCode, state, type > 0, time); + return await (_modern?.ProcessKeyEventAsync(keyVal, keyCode, state, type > 0, time) ?? Task.FromResult(false)); } - public Task WatchCommitStringAsync(Action handler) => - _old?.WatchCommitStringAsync(handler) ?? _modern.WatchCommitStringAsync(handler); + public Task WatchCommitStringAsync(Action handler) => + _old?.WatchCommitStringAsync(handler) + ?? _modern?.WatchCommitStringAsync(handler) + ?? Task.FromResult(default(IDisposable?)); - public Task WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler) + public Task WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler) { return _old?.WatchForwardKeyAsync(handler) - ?? _modern.WatchForwardKeyAsync(ev => - handler((ev.keyval, ev.state, ev.type ? 1 : 0))); + ?? _modern?.WatchForwardKeyAsync(ev => + handler((ev.keyval, ev.state, ev.type ? 1 : 0))) + ?? Task.FromResult(default(IDisposable?)); } public Task SetCapacityAsync(uint flags) => - _old?.SetCapacityAsync(flags) ?? _modern.SetCapabilityAsync(flags); + _old?.SetCapacityAsync(flags) ?? _modern?.SetCapabilityAsync(flags) ?? Task.CompletedTask; } } diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs index 0b85965de7..791431dfa7 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs @@ -12,7 +12,7 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx { internal class FcitxX11TextInputMethod : DBusTextInputMethodBase { - private FcitxICWrapper _context; + private FcitxICWrapper? _context; private FcitxCapabilityFlags? _lastReportedFlags; public FcitxX11TextInputMethod(Connection connection) : base(connection, @@ -49,7 +49,7 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx return true; } - protected override Task Disconnect() => _context.DestroyICAsync(); + protected override Task Disconnect() => _context?.DestroyICAsync() ?? Task.CompletedTask; protected override void OnDisconnected() => _context = null; @@ -60,18 +60,18 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx } protected override Task SetCursorRectCore(PixelRect cursorRect) => - _context.SetCursorRectAsync(cursorRect.X, cursorRect.Y, Math.Max(1, cursorRect.Width), - Math.Max(1, cursorRect.Height)); - - protected override Task SetActiveCore(bool active) - { - if (active) - return _context.FocusInAsync(); - else - return _context.FocusOutAsync(); - } + _context?.SetCursorRectAsync(cursorRect.X, cursorRect.Y, Math.Max(1, cursorRect.Width), + Math.Max(1, cursorRect.Height)) + ?? Task.CompletedTask; + + protected override Task SetActiveCore(bool active)=> (active + ? _context?.FocusInAsync() + : _context?.FocusOutAsync()) + ?? Task.CompletedTask; + - protected override Task ResetContextCore() => _context.ResetAsync(); + protected override Task ResetContextCore() => _context?.ResetAsync() + ?? Task.CompletedTask; protected override async Task HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode) { @@ -88,9 +88,15 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx var type = args.Type == RawKeyEventType.KeyDown ? FcitxKeyEventType.FCITX_PRESS_KEY : FcitxKeyEventType.FCITX_RELEASE_KEY; - - return await _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state, (int)type, - (uint)args.Timestamp).ConfigureAwait(false); + if (_context is { }) + { + return await _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state, (int)type, + (uint)args.Timestamp).ConfigureAwait(false); + } + else + { + return false; + } } public override void SetOptions(TextInputOptions options) => diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs index 26c0d249f3..4ef034adb9 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs @@ -22,25 +22,25 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus Task GetEngineAsync(); Task DestroyAsync(); Task SetSurroundingTextAsync(object Text, uint CursorPos, uint AnchorPos); - Task WatchCommitTextAsync(Action cb, Action onError = null); - Task WatchForwardKeyEventAsync(Action<(uint keyval, uint keycode, uint state)> handler, Action onError = null); - Task WatchRequireSurroundingTextAsync(Action handler, Action onError = null); - Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchars)> handler, Action onError = null); - Task WatchUpdatePreeditTextAsync(Action<(object text, uint cursorPos, bool visible)> handler, Action onError = null); - Task WatchShowPreeditTextAsync(Action handler, Action onError = null); - Task WatchHidePreeditTextAsync(Action handler, Action onError = null); - Task WatchUpdateAuxiliaryTextAsync(Action<(object text, bool visible)> handler, Action onError = null); - Task WatchShowAuxiliaryTextAsync(Action handler, Action onError = null); - Task WatchHideAuxiliaryTextAsync(Action handler, Action onError = null); - Task WatchUpdateLookupTableAsync(Action<(object table, bool visible)> handler, Action onError = null); - Task WatchShowLookupTableAsync(Action handler, Action onError = null); - Task WatchHideLookupTableAsync(Action handler, Action onError = null); - Task WatchPageUpLookupTableAsync(Action handler, Action onError = null); - Task WatchPageDownLookupTableAsync(Action handler, Action onError = null); - Task WatchCursorUpLookupTableAsync(Action handler, Action onError = null); - Task WatchCursorDownLookupTableAsync(Action handler, Action onError = null); - Task WatchRegisterPropertiesAsync(Action handler, Action onError = null); - Task WatchUpdatePropertyAsync(Action handler, Action onError = null); + Task WatchCommitTextAsync(Action cb, Action? onError = null); + Task WatchForwardKeyEventAsync(Action<(uint keyval, uint keycode, uint state)> handler, Action? onError = null); + Task WatchRequireSurroundingTextAsync(Action handler, Action? onError = null); + Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchars)> handler, Action? onError = null); + Task WatchUpdatePreeditTextAsync(Action<(object text, uint cursorPos, bool visible)> handler, Action? onError = null); + Task WatchShowPreeditTextAsync(Action handler, Action? onError = null); + Task WatchHidePreeditTextAsync(Action handler, Action? onError = null); + Task WatchUpdateAuxiliaryTextAsync(Action<(object text, bool visible)> handler, Action? onError = null); + Task WatchShowAuxiliaryTextAsync(Action handler, Action? onError = null); + Task WatchHideAuxiliaryTextAsync(Action handler, Action? onError = null); + Task WatchUpdateLookupTableAsync(Action<(object table, bool visible)> handler, Action? onError = null); + Task WatchShowLookupTableAsync(Action handler, Action? onError = null); + Task WatchHideLookupTableAsync(Action handler, Action? onError = null); + Task WatchPageUpLookupTableAsync(Action handler, Action? onError = null); + Task WatchPageDownLookupTableAsync(Action handler, Action? onError = null); + Task WatchCursorUpLookupTableAsync(Action handler, Action? onError = null); + Task WatchCursorDownLookupTableAsync(Action handler, Action? onError = null); + Task WatchRegisterPropertiesAsync(Action handler, Action? onError = null); + Task WatchUpdatePropertyAsync(Action handler, Action? onError = null); } diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs index 1397eaa57b..2324ca44a7 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs @@ -9,7 +9,7 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus { internal class IBusX11TextInputMethod : DBusTextInputMethodBase { - private IIBusInputContext _context; + private IIBusInputContext? _context; public IBusX11TextInputMethod(Connection connection) : base(connection, "org.freedesktop.portal.IBus") @@ -53,16 +53,16 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus private void OnCommitText(object wtf) { // Hello darkness, my old friend - var prop = wtf.GetType().GetField("Item3"); - if (prop != null) + if (wtf.GetType().GetField("Item3") is { } prop) { - var text = (string)prop.GetValue(wtf); + var text = prop.GetValue(wtf) as string; if (!string.IsNullOrEmpty(text)) - FireCommit(text); + FireCommit(text!); } } - protected override Task Disconnect() => _context.DestroyAsync(); + protected override Task Disconnect() => _context?.DestroyAsync() + ?? Task.CompletedTask; protected override void OnDisconnected() { @@ -71,13 +71,15 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus } protected override Task SetCursorRectCore(PixelRect rect) - => _context.SetCursorLocationAsync(rect.X, rect.Y, rect.Width, rect.Height); + => _context?.SetCursorLocationAsync(rect.X, rect.Y, rect.Width, rect.Height) + ?? Task.CompletedTask; protected override Task SetActiveCore(bool active) - => active ? _context.FocusInAsync() : _context.FocusOutAsync(); + => (active ? _context?.FocusInAsync() : _context?.FocusOutAsync()) + ?? Task.CompletedTask; protected override Task ResetContextCore() - => _context.ResetAsync(); + => _context?.ResetAsync() ?? Task.CompletedTask; protected override Task HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode) { @@ -94,7 +96,15 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus if (args.Type == RawKeyEventType.KeyUp) state |= IBusModifierMask.ReleaseMask; - return _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state); + if(_context is { }) + { + return _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state); + } + else + { + return Task.FromResult(false); + } + } public override void SetOptions(TextInputOptions options) diff --git a/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs b/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs index 7f71ecf0ff..86978c8b60 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs @@ -17,7 +17,7 @@ namespace Avalonia.FreeDesktop.DBusIme new DBusInputMethodFactory(_ => new IBusX11TextInputMethod(conn)) }; - static Func DetectInputMethod() + static Func? DetectInputMethod() { foreach (var name in new[] { "AVALONIA_IM_MODULE", "GTK_IM_MODULE", "QT_IM_MODULE" }) { diff --git a/src/Avalonia.FreeDesktop/DBusMenu.cs b/src/Avalonia.FreeDesktop/DBusMenu.cs index 7180345386..3a1c65e7c9 100644 --- a/src/Avalonia.FreeDesktop/DBusMenu.cs +++ b/src/Avalonia.FreeDesktop/DBusMenu.cs @@ -28,18 +28,18 @@ namespace Avalonia.FreeDesktop.DBusMenu Task EventGroupAsync((int id, string eventId, object data, uint timestamp)[] events); Task AboutToShowAsync(int Id); Task<(int[] updatesNeeded, int[] idErrors)> AboutToShowGroupAsync(int[] Ids); - Task WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> handler, Action onError = null); - Task WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action onError = null); - Task WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action onError = null); + Task WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> handler, Action? onError = null); + Task WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action? onError = null); + Task WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action? onError = null); } [Dictionary] class DBusMenuProperties { public uint Version { get; set; } = default (uint); - public string TextDirection { get; set; } = default (string); - public string Status { get; set; } = default (string); - public string[] IconThemePath { get; set; } = default (string[]); + public string? TextDirection { get; set; } = default (string); + public string? Status { get; set; } = default (string); + public string[]? IconThemePath { get; set; } = default (string[]); } @@ -50,7 +50,7 @@ namespace Avalonia.FreeDesktop.DBusMenu Task UnregisterWindowAsync(uint WindowId); Task<(string service, ObjectPath menuObjectPath)> GetMenuForWindowAsync(uint WindowId); Task<(uint, string, ObjectPath)[]> GetMenusAsync(); - Task WatchWindowRegisteredAsync(Action<(uint windowId, string service, ObjectPath menuObjectPath)> handler, Action onError = null); - Task WatchWindowUnregisteredAsync(Action handler, Action onError = null); + Task WatchWindowRegisteredAsync(Action<(uint windowId, string service, ObjectPath menuObjectPath)> handler, Action? onError = null); + Task WatchWindowUnregisteredAsync(Action handler, Action? onError = null); } } diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 206c24ad5e..c0511420a6 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -17,7 +17,7 @@ namespace Avalonia.FreeDesktop { public class DBusMenuExporter { - public static ITopLevelNativeMenuExporter TryCreateTopLevelNativeMenu(IntPtr xid) + public static ITopLevelNativeMenuExporter? TryCreateTopLevelNativeMenu(IntPtr xid) { if (DBusHelper.Connection == null) return null; @@ -37,10 +37,10 @@ namespace Avalonia.FreeDesktop { private readonly Connection _dbus; private readonly uint _xid; - private IRegistrar _registrar; + private IRegistrar? _registrar; private bool _disposed; private uint _revision = 1; - private NativeMenu _menu; + private NativeMenu? _menu; private readonly Dictionary _idsToItems = new Dictionary(); private readonly Dictionary _itemsToIds = new Dictionary(); private readonly HashSet _menus = new HashSet(); @@ -73,10 +73,10 @@ namespace Avalonia.FreeDesktop if (_appMenu) { await _dbus.RegisterObjectAsync(this); - _registrar = DBusHelper.Connection.CreateProxy( + _registrar = DBusHelper.Connection?.CreateProxy( "com.canonical.AppMenu.Registrar", "/com/canonical/AppMenu/Registrar"); - if (!_disposed) + if (!_disposed && _registrar is { }) await _registrar.RegisterWindowAsync(_xid, ObjectPath); } else @@ -109,9 +109,9 @@ namespace Avalonia.FreeDesktop public bool IsNativeMenuExported { get; private set; } - public event EventHandler OnIsNativeMenuExportedChanged; + public event EventHandler? OnIsNativeMenuExportedChanged; - public void SetNativeMenu(NativeMenu menu) + public void SetNativeMenu(NativeMenu? menu) { if (menu == null) menu = new NativeMenu(); @@ -153,7 +153,7 @@ namespace Avalonia.FreeDesktop Dispatcher.UIThread.Post(DoLayoutReset, DispatcherPriority.Background); } - private (NativeMenuItemBase item, NativeMenu menu) GetMenu(int id) + private (NativeMenuItemBase? item, NativeMenu? menu) GetMenu(int id) { if (id == 0) return (null, _menu); @@ -161,7 +161,7 @@ namespace Avalonia.FreeDesktop return (item, (item as NativeMenuItem)?.Menu); } - private void EnsureSubscribed(NativeMenu menu) + private void EnsureSubscribed(NativeMenu? menu) { if(menu!=null && _menus.Add(menu)) ((INotifyCollectionChanged)menu.Items).CollectionChanged += OnMenuItemsChanged; @@ -180,12 +180,12 @@ namespace Avalonia.FreeDesktop return id; } - private void OnMenuItemsChanged(object sender, NotifyCollectionChangedEventArgs e) + private void OnMenuItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) { QueueReset(); } - private void OnItemPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + private void OnItemPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) { QueueReset(); } @@ -216,7 +216,7 @@ namespace Avalonia.FreeDesktop "type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display", "toggle-state", "icon-data" }; - object GetProperty((NativeMenuItemBase item, NativeMenu menu) i, string name) + object? GetProperty((NativeMenuItemBase? item, NativeMenu? menu) i, string name) { var (it, menu) = i; @@ -302,7 +302,7 @@ namespace Avalonia.FreeDesktop } private List> _reusablePropertyList = new List>(); - KeyValuePair[] GetProperties((NativeMenuItemBase item, NativeMenu menu) i, string[] names) + KeyValuePair[] GetProperties((NativeMenuItemBase? item, NativeMenu? menu) i, string[] names) { if (names?.Length > 0 != true) names = AllProperties; @@ -336,7 +336,7 @@ namespace Avalonia.FreeDesktop return Task.FromResult(rv); } - (int, KeyValuePair[], object[]) GetLayout(NativeMenuItemBase item, NativeMenu menu, int depth, string[] propertyNames) + (int, KeyValuePair[], object[]) GetLayout(NativeMenuItemBase? item, NativeMenu? menu, int depth, string[] propertyNames) { var id = item == null ? 0 : GetId(item); var props = GetProperties((item, menu), propertyNames); @@ -414,22 +414,22 @@ namespace Avalonia.FreeDesktop private event Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> ItemsPropertiesUpdated { add { } remove { } } - private event Action<(uint revision, int parent)> LayoutUpdated; + private event Action<(uint revision, int parent)>? LayoutUpdated; private event Action<(int id, uint timestamp)> ItemActivationRequested { add { } remove { } } private event Action PropertiesChanged { add { } remove { } } - async Task IDBusMenu.WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> handler, Action onError) + async Task IDBusMenu.WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> handler, Action? onError) { ItemsPropertiesUpdated += handler; return Disposable.Create(() => ItemsPropertiesUpdated -= handler); } - async Task IDBusMenu.WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action onError) + async Task IDBusMenu.WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action? onError) { LayoutUpdated += handler; return Disposable.Create(() => LayoutUpdated -= handler); } - async Task IDBusMenu.WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action onError) + async Task IDBusMenu.WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action? onError) { ItemActivationRequested+= handler; return Disposable.Create(() => ItemActivationRequested -= handler); diff --git a/src/Avalonia.FreeDesktop/DBusRequest.cs b/src/Avalonia.FreeDesktop/DBusRequest.cs index 940a476916..d84905324f 100644 --- a/src/Avalonia.FreeDesktop/DBusRequest.cs +++ b/src/Avalonia.FreeDesktop/DBusRequest.cs @@ -11,6 +11,6 @@ namespace Avalonia.FreeDesktop internal interface IRequest : IDBusObject { Task CloseAsync(); - Task WatchResponseAsync(Action<(uint response, IDictionary results)> handler, Action onError = null); + Task WatchResponseAsync(Action<(uint response, IDictionary results)> handler, Action? onError = null); } } diff --git a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs index d68c02bfd6..b69ea68a76 100644 --- a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs +++ b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs @@ -10,7 +10,7 @@ namespace Avalonia.FreeDesktop public IDisposable Listen(ObservableCollection mountedDrives) { Contract.Requires(mountedDrives != null); - return new LinuxMountedVolumeInfoListener(ref mountedDrives); + return new LinuxMountedVolumeInfoListener(ref mountedDrives!); } } } diff --git a/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj b/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj index 97e58f8a64..b5c955a8a6 100644 --- a/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj +++ b/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj @@ -11,6 +11,7 @@ all + diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs index ee4125101c..46a985c0e8 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs @@ -133,7 +133,7 @@ namespace Avalonia.LinuxFramebuffer.Output var device = gbm_create_device(card.Fd); _gbmTargetSurface = gbm_surface_create(device, modeInfo.Resolution.Width, modeInfo.Resolution.Height, GbmColorFormats.GBM_FORMAT_XRGB8888, GbmBoFlags.GBM_BO_USE_SCANOUT | GbmBoFlags.GBM_BO_USE_RENDERING); - if(_gbmTargetSurface == null) + if(_gbmTargetSurface == IntPtr.Zero) throw new InvalidOperationException("Unable to create GBM surface"); _eglDisplay = new EglDisplay(new EglInterface(eglGetProcAddress), false, 0x31D7, device, null); diff --git a/src/Avalonia.SourceGenerator/IsExternalInit.cs b/src/Shared/IsExternalInit.cs similarity index 100% rename from src/Avalonia.SourceGenerator/IsExternalInit.cs rename to src/Shared/IsExternalInit.cs From 030c956e31d77e2572088b94daf69a8f0ec317a6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 5 Jul 2022 18:31:49 +0200 Subject: [PATCH 175/389] Revert "Make MovePreviousOrParent internal as well." This reverts commit 5e1f28f2a9e3a4f8606bf8efb6aa393d12f62d45. --- src/Avalonia.Base/Styling/ChildSelector.cs | 2 +- src/Avalonia.Base/Styling/DescendentSelector.cs | 2 +- src/Avalonia.Base/Styling/NestingSelector.cs | 2 +- src/Avalonia.Base/Styling/NotSelector.cs | 2 +- src/Avalonia.Base/Styling/NthChildSelector.cs | 2 +- src/Avalonia.Base/Styling/OrSelector.cs | 2 +- src/Avalonia.Base/Styling/PropertyEqualsSelector.cs | 2 +- src/Avalonia.Base/Styling/Selector.cs | 2 +- src/Avalonia.Base/Styling/TemplateSelector.cs | 2 +- src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Base/Styling/ChildSelector.cs b/src/Avalonia.Base/Styling/ChildSelector.cs index b3f6794154..bc1d257ce6 100644 --- a/src/Avalonia.Base/Styling/ChildSelector.cs +++ b/src/Avalonia.Base/Styling/ChildSelector.cs @@ -65,6 +65,6 @@ namespace Avalonia.Styling } private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (null, null); - private protected override Selector? MovePreviousOrParent() => _parent; + protected override Selector? MovePreviousOrParent() => _parent; } } diff --git a/src/Avalonia.Base/Styling/DescendentSelector.cs b/src/Avalonia.Base/Styling/DescendentSelector.cs index 7f36c27a37..3a16574e04 100644 --- a/src/Avalonia.Base/Styling/DescendentSelector.cs +++ b/src/Avalonia.Base/Styling/DescendentSelector.cs @@ -70,6 +70,6 @@ namespace Avalonia.Styling } private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (null, null); - private protected override Selector? MovePreviousOrParent() => _parent; + protected override Selector? MovePreviousOrParent() => _parent; } } diff --git a/src/Avalonia.Base/Styling/NestingSelector.cs b/src/Avalonia.Base/Styling/NestingSelector.cs index 60ad8106d9..dd0bac31c6 100644 --- a/src/Avalonia.Base/Styling/NestingSelector.cs +++ b/src/Avalonia.Base/Styling/NestingSelector.cs @@ -37,6 +37,6 @@ namespace Avalonia.Styling return parent is Style parentStyle ? (parentStyle.Selector, parentStyle.Parent) : (null, null); } - private protected override Selector? MovePreviousOrParent() => null; + protected override Selector? MovePreviousOrParent() => null; } } diff --git a/src/Avalonia.Base/Styling/NotSelector.cs b/src/Avalonia.Base/Styling/NotSelector.cs index be5cfaca49..ebde392a3b 100644 --- a/src/Avalonia.Base/Styling/NotSelector.cs +++ b/src/Avalonia.Base/Styling/NotSelector.cs @@ -67,6 +67,6 @@ namespace Avalonia.Styling } private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (_previous, nestingParent); - private protected override Selector? MovePreviousOrParent() => _previous; + protected override Selector? MovePreviousOrParent() => _previous; } } diff --git a/src/Avalonia.Base/Styling/NthChildSelector.cs b/src/Avalonia.Base/Styling/NthChildSelector.cs index 40fb5a4434..e6d9cf58a9 100644 --- a/src/Avalonia.Base/Styling/NthChildSelector.cs +++ b/src/Avalonia.Base/Styling/NthChildSelector.cs @@ -105,7 +105,7 @@ namespace Avalonia.Styling } private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (_previous, nestingParent); - private protected override Selector? MovePreviousOrParent() => _previous; + protected override Selector? MovePreviousOrParent() => _previous; public override string ToString() { diff --git a/src/Avalonia.Base/Styling/OrSelector.cs b/src/Avalonia.Base/Styling/OrSelector.cs index 2ab00d65f8..115e0aeb95 100644 --- a/src/Avalonia.Base/Styling/OrSelector.cs +++ b/src/Avalonia.Base/Styling/OrSelector.cs @@ -103,7 +103,7 @@ namespace Avalonia.Styling } private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (null, null); - private protected override Selector? MovePreviousOrParent() => null; + protected override Selector? MovePreviousOrParent() => null; internal override void ValidateNestingSelector(bool inControlTheme) { diff --git a/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs index 62caa9bab3..96f8c8dfeb 100644 --- a/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs +++ b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs @@ -90,7 +90,7 @@ namespace Avalonia.Styling } private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (_previous, nestingParent); - private protected override Selector? MovePreviousOrParent() => _previous; + protected override Selector? MovePreviousOrParent() => _previous; internal static bool Compare(Type propertyType, object? propertyValue, object? value) { diff --git a/src/Avalonia.Base/Styling/Selector.cs b/src/Avalonia.Base/Styling/Selector.cs index 67109961ba..cc8598c5e3 100644 --- a/src/Avalonia.Base/Styling/Selector.cs +++ b/src/Avalonia.Base/Styling/Selector.cs @@ -95,7 +95,7 @@ namespace Avalonia.Styling /// /// Moves to the previous selector or the parent selector. /// - private protected abstract Selector? MovePreviousOrParent(); + protected abstract Selector? MovePreviousOrParent(); internal virtual void ValidateNestingSelector(bool inControlTheme) { diff --git a/src/Avalonia.Base/Styling/TemplateSelector.cs b/src/Avalonia.Base/Styling/TemplateSelector.cs index 0af263b4e5..a68b7003b8 100644 --- a/src/Avalonia.Base/Styling/TemplateSelector.cs +++ b/src/Avalonia.Base/Styling/TemplateSelector.cs @@ -49,6 +49,6 @@ namespace Avalonia.Styling } private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (null, null); - private protected override Selector? MovePreviousOrParent() => _parent; + protected override Selector? MovePreviousOrParent() => _parent; } } diff --git a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs index 7783d3f653..3a2150b1e9 100644 --- a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs @@ -140,7 +140,7 @@ namespace Avalonia.Styling } private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (_previous, nestingParent); - private protected override Selector? MovePreviousOrParent() => _previous; + protected override Selector? MovePreviousOrParent() => _previous; private string BuildSelectorString() { From a341c33b5526c1b2fc886db3d07025e6fbb8110d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 5 Jul 2022 18:58:38 +0200 Subject: [PATCH 176/389] Revert "Fix parent selectors with /template/ at end." This reverts commit e50b416d5bf1529544830d5a291a1fb2f1b0da64 as it was not functioning correctly. --- src/Avalonia.Base/Styling/ChildSelector.cs | 2 +- src/Avalonia.Base/Styling/DescendentSelector.cs | 2 +- src/Avalonia.Base/Styling/NestingSelector.cs | 8 ++------ src/Avalonia.Base/Styling/NotSelector.cs | 2 +- src/Avalonia.Base/Styling/NthChildSelector.cs | 2 +- src/Avalonia.Base/Styling/OrSelector.cs | 2 +- src/Avalonia.Base/Styling/PropertyEqualsSelector.cs | 2 +- src/Avalonia.Base/Styling/Selector.cs | 12 +++--------- src/Avalonia.Base/Styling/TemplateSelector.cs | 2 +- .../Styling/TypeNameAndClassSelector.cs | 2 +- 10 files changed, 13 insertions(+), 23 deletions(-) diff --git a/src/Avalonia.Base/Styling/ChildSelector.cs b/src/Avalonia.Base/Styling/ChildSelector.cs index bc1d257ce6..9512dc34df 100644 --- a/src/Avalonia.Base/Styling/ChildSelector.cs +++ b/src/Avalonia.Base/Styling/ChildSelector.cs @@ -64,7 +64,7 @@ namespace Avalonia.Styling } } - private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (null, null); + protected override Selector? MovePrevious() => null; protected override Selector? MovePreviousOrParent() => _parent; } } diff --git a/src/Avalonia.Base/Styling/DescendentSelector.cs b/src/Avalonia.Base/Styling/DescendentSelector.cs index 3a16574e04..677a924189 100644 --- a/src/Avalonia.Base/Styling/DescendentSelector.cs +++ b/src/Avalonia.Base/Styling/DescendentSelector.cs @@ -69,7 +69,7 @@ namespace Avalonia.Styling } } - private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (null, null); + protected override Selector? MovePrevious() => null; protected override Selector? MovePreviousOrParent() => _parent; } } diff --git a/src/Avalonia.Base/Styling/NestingSelector.cs b/src/Avalonia.Base/Styling/NestingSelector.cs index dd0bac31c6..77c5b719c6 100644 --- a/src/Avalonia.Base/Styling/NestingSelector.cs +++ b/src/Avalonia.Base/Styling/NestingSelector.cs @@ -17,7 +17,7 @@ namespace Avalonia.Styling { if (parent is Style s && s.Selector is not null) { - return SelectorMatch.AlwaysThisType; + return s.Selector.Match(control, s.Parent, subscribe); } else if (parent is ControlTheme theme) { @@ -32,11 +32,7 @@ namespace Avalonia.Styling "Nesting selector was specified but cannot determine parent selector."); } - private protected override (Selector?, IStyle?) MovePrevious(IStyle? parent) - { - return parent is Style parentStyle ? (parentStyle.Selector, parentStyle.Parent) : (null, null); - } - + protected override Selector? MovePrevious() => null; protected override Selector? MovePreviousOrParent() => null; } } diff --git a/src/Avalonia.Base/Styling/NotSelector.cs b/src/Avalonia.Base/Styling/NotSelector.cs index ebde392a3b..c7727bb6b8 100644 --- a/src/Avalonia.Base/Styling/NotSelector.cs +++ b/src/Avalonia.Base/Styling/NotSelector.cs @@ -66,7 +66,7 @@ namespace Avalonia.Styling } } - private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (_previous, nestingParent); + protected override Selector? MovePrevious() => _previous; protected override Selector? MovePreviousOrParent() => _previous; } } diff --git a/src/Avalonia.Base/Styling/NthChildSelector.cs b/src/Avalonia.Base/Styling/NthChildSelector.cs index e6d9cf58a9..f473791664 100644 --- a/src/Avalonia.Base/Styling/NthChildSelector.cs +++ b/src/Avalonia.Base/Styling/NthChildSelector.cs @@ -104,7 +104,7 @@ namespace Avalonia.Styling return match ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance; } - private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (_previous, nestingParent); + protected override Selector? MovePrevious() => _previous; protected override Selector? MovePreviousOrParent() => _previous; public override string ToString() diff --git a/src/Avalonia.Base/Styling/OrSelector.cs b/src/Avalonia.Base/Styling/OrSelector.cs index 115e0aeb95..af9249864f 100644 --- a/src/Avalonia.Base/Styling/OrSelector.cs +++ b/src/Avalonia.Base/Styling/OrSelector.cs @@ -102,7 +102,7 @@ namespace Avalonia.Styling } } - private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (null, null); + protected override Selector? MovePrevious() => null; protected override Selector? MovePreviousOrParent() => null; internal override void ValidateNestingSelector(bool inControlTheme) diff --git a/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs index 96f8c8dfeb..48136ba2de 100644 --- a/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs +++ b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs @@ -89,7 +89,7 @@ namespace Avalonia.Styling } - private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (_previous, nestingParent); + protected override Selector? MovePrevious() => _previous; protected override Selector? MovePreviousOrParent() => _previous; internal static bool Compare(Type propertyType, object? propertyValue, object? value) diff --git a/src/Avalonia.Base/Styling/Selector.cs b/src/Avalonia.Base/Styling/Selector.cs index cc8598c5e3..7ce17518dd 100644 --- a/src/Avalonia.Base/Styling/Selector.cs +++ b/src/Avalonia.Base/Styling/Selector.cs @@ -84,13 +84,7 @@ namespace Avalonia.Styling /// /// Moves to the previous selector. /// - /// - /// The parent style, if the selector is on a nested style. - /// - /// - /// The previous selector, and its nesting parent. - /// - private protected abstract (Selector?, IStyle?) MovePrevious(IStyle? nestingParent); + protected abstract Selector? MovePrevious(); /// /// Moves to the previous selector or the parent selector. @@ -148,14 +142,14 @@ namespace Avalonia.Styling ref AndActivatorBuilder activators, ref Selector? combinator) { - var (previous, previousParent) = selector.MovePrevious(parent); + var previous = selector.MovePrevious(); // Selectors are stored from right-to-left, so we recurse into the selector in order to // reverse this order, because the type selector will be on the left and is our best // opportunity to exit early. if (previous != null && !previous.IsCombinator) { - var previousMatch = Match(control, previous, previousParent, subscribe, ref activators, ref combinator); + var previousMatch = Match(control, previous, parent, subscribe, ref activators, ref combinator); if (previousMatch < SelectorMatchResult.Sometimes) { diff --git a/src/Avalonia.Base/Styling/TemplateSelector.cs b/src/Avalonia.Base/Styling/TemplateSelector.cs index a68b7003b8..278e24a203 100644 --- a/src/Avalonia.Base/Styling/TemplateSelector.cs +++ b/src/Avalonia.Base/Styling/TemplateSelector.cs @@ -48,7 +48,7 @@ namespace Avalonia.Styling return _parent.Match(templatedParent, parent, subscribe); } - private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (null, null); + protected override Selector? MovePrevious() => null; protected override Selector? MovePreviousOrParent() => _parent; } } diff --git a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs index 3a2150b1e9..6681a7da36 100644 --- a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs @@ -139,7 +139,7 @@ namespace Avalonia.Styling return Name == null ? SelectorMatch.AlwaysThisType : SelectorMatch.AlwaysThisInstance; } - private protected override (Selector?, IStyle?) MovePrevious(IStyle? nestingParent) => (_previous, nestingParent); + protected override Selector? MovePrevious() => _previous; protected override Selector? MovePreviousOrParent() => _previous; private string BuildSelectorString() From 3e17bd066240ef2661ea305b7c16be78ac090877 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 6 Jul 2022 10:06:39 +0200 Subject: [PATCH 177/389] Skip these failing tests for now. I will work on a fix. --- .../Styling/SelectorTests_Nesting.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs index a60d21a018..747ad585e7 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs @@ -149,8 +149,8 @@ namespace Avalonia.Base.UnitTests.Styling control.Classes.Remove("foo"); Assert.False(sink.Active); } - - [Fact] + + [Fact(Skip = "Template selectors a the end of nesting parent selectors currently broken")] public void Template_Nesting_OfType_Matches() { var control = new Control1 { Classes = { "foo" } }; @@ -198,7 +198,7 @@ namespace Avalonia.Base.UnitTests.Styling Assert.Equal(SelectorMatchResult.Sometimes, match.Result); } - [Fact] + [Fact(Skip = "Template selectors a the end of nesting parent selectors currently broken")] public void Class_Template_Nesting_OfType_Matches() { var control = new Control1 { Classes = { "foo" } }; From b63b84860ca69e453b0c719873be381a1dba7099 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 6 Jul 2022 09:58:36 +0200 Subject: [PATCH 178/389] fix(DataGrid): CS8073 The result of the expression is always 'true' --- src/Avalonia.Controls.DataGrid/DataGrid.cs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index aaac3f8f9c..d42468f47e 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -5990,15 +5990,14 @@ namespace Avalonia.Controls /// The formatted string. private string FormatClipboardContent(DataGridRowClipboardEventArgs e) { - StringBuilder text = new StringBuilder(); - for (int cellIndex = 0; cellIndex < e.ClipboardRowContent.Count; cellIndex++) - { - DataGridClipboardCellContent cellContent = e.ClipboardRowContent[cellIndex]; - if (cellContent != null) - { - text.Append(cellContent.Content); - } - if (cellIndex < e.ClipboardRowContent.Count - 1) + var text = new StringBuilder(); + var clipboardRowContent = e.ClipboardRowContent; + var numberOfItem = clipboardRowContent.Count; + for (int cellIndex = 0; cellIndex < numberOfItem; cellIndex++) + { + var cellContent = clipboardRowContent[cellIndex]; + text.Append(cellContent.Content); + if (cellIndex < numberOfItem - 1) { text.Append('\t'); } From 407f761201a287e22bc34f086f7491d7632b42c4 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Jul 2022 12:34:38 +0100 Subject: [PATCH 179/389] add windows task --- azure-pipelines-integrationtests.yml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index 94a80eef44..4f3ac57e73 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -6,10 +6,16 @@ trigger: - master -pool: - name: 'AvaloniaMacPool' -steps: -- script: ./tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh - displayName: 'run integration tests' +- job: Mac + pool: + name: 'AvaloniaMacPool' + steps: + - script: ./tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh + displayName: 'run integration tests' + + +- job: Windows + pool: + vmImage: 'windows-2022' From 03b0fcb4549af35d142dcf9cb408fc49b6032e7d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Jul 2022 12:40:42 +0100 Subject: [PATCH 180/389] add windows integration tests --- azure-pipelines-integrationtests.yml | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index 4f3ac57e73..05872803b4 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -6,7 +6,7 @@ trigger: - master - +jobs: - job: Mac pool: name: 'AvaloniaMacPool' @@ -19,3 +19,20 @@ trigger: - job: Windows pool: vmImage: 'windows-2022' + + steps: + - task: Windows Application Driver@0 + inputs: + OperationType: 'Start' + AgentResolution: '1080p' + displayName: 'Start WinAppDriver' + + - task: DotNetCoreCLI@2 + inputs: + command: 'test' + projects: 'tests/Avalonia.IntegrationTests.Appium/' + + - task: Windows Application Driver@0 + inputs: + OperationType: 'Stop' + displayName: 'Stop WinAppDriver' From 7b12c76e1d79a8948bd1868018c1a829be1a5175 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Jul 2022 13:21:48 +0100 Subject: [PATCH 181/389] build test app first. --- azure-pipelines-integrationtests.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index 05872803b4..941dd01249 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -21,16 +21,26 @@ jobs: vmImage: 'windows-2022' steps: + - task: UseDotNet@2 + displayName: 'Use .NET Core SDK 6.0.202' + inputs: + version: 6.0.202 + - task: Windows Application Driver@0 inputs: OperationType: 'Start' AgentResolution: '1080p' displayName: 'Start WinAppDriver' + + - task: DotNetCoreCLI@2 + inputs: + command: 'build' + projects: 'samples/IntegrationTestApp/IntegrationTestApp.csproj' - task: DotNetCoreCLI@2 inputs: command: 'test' - projects: 'tests/Avalonia.IntegrationTests.Appium/' + projects: 'tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj' - task: Windows Application Driver@0 inputs: From 0bb62fa8092421d6bf2f72479041f6bc93fffe34 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Jul 2022 13:39:03 +0100 Subject: [PATCH 182/389] use 4k res on windows tests --- 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 941dd01249..c3f6292703 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -29,7 +29,7 @@ jobs: - task: Windows Application Driver@0 inputs: OperationType: 'Start' - AgentResolution: '1080p' + AgentResolution: '4K' displayName: 'Start WinAppDriver' - task: DotNetCoreCLI@2 From 230166d0bc894dc802dc85fc25ef19452caa414c Mon Sep 17 00:00:00 2001 From: Lubomir Tetak Date: Wed, 6 Jul 2022 14:54:33 +0200 Subject: [PATCH 183/389] EffectiveViewportChangedListeners thread safety improved --- src/Avalonia.Base/Layout/LayoutManager.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Layout/LayoutManager.cs b/src/Avalonia.Base/Layout/LayoutManager.cs index fc988a8d6c..dd2db9304d 100644 --- a/src/Avalonia.Base/Layout/LayoutManager.cs +++ b/src/Avalonia.Base/Layout/LayoutManager.cs @@ -340,7 +340,7 @@ namespace Avalonia.Layout { for (var i = 0; i < count; ++i) { - var l = _effectiveViewportChangedListeners[i]; + var l = listeners[i]; if (!l.Listener.IsAttachedToVisualTree) { @@ -352,7 +352,7 @@ namespace Avalonia.Layout if (viewport != l.Viewport) { l.Listener.EffectiveViewportChanged(new EffectiveViewportChangedEventArgs(viewport)); - _effectiveViewportChangedListeners[i] = new EffectiveViewportChangedListener(l.Listener, viewport); + l.Viewport = viewport; } } } @@ -404,7 +404,7 @@ namespace Avalonia.Layout } } - private readonly struct EffectiveViewportChangedListener + private struct EffectiveViewportChangedListener { public EffectiveViewportChangedListener(ILayoutable listener, Rect viewport) { @@ -413,7 +413,7 @@ namespace Avalonia.Layout } public ILayoutable Listener { get; } - public Rect Viewport { get; } + public Rect Viewport { get; set; } } } } From c034525d29dd2d6e4c95aebf17751ccfc6332c0e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Jul 2022 14:40:46 +0100 Subject: [PATCH 184/389] win32, keep window size at all scale settings when entering and exiting fs mode. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 82f1fa7a3e..259f5c996b 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -870,7 +870,14 @@ namespace Avalonia.Win32 if (fullscreen) { GetWindowRect(_hwnd, out var windowRect); - _savedWindowInfo.WindowRect = windowRect; + GetClientRect(_hwnd, out var clientRect); + + clientRect.left += windowRect.left; + clientRect.right += windowRect.left; + clientRect.top += windowRect.top; + clientRect.bottom += windowRect.top; + + _savedWindowInfo.WindowRect = clientRect; var current = GetStyle(); var currentEx = GetExtendedStyle(); @@ -907,10 +914,10 @@ namespace Avalonia.Win32 SetExtendedStyle(_savedWindowInfo.ExStyle, false); // On restore, resize to the previous saved rect size. - var new_rect = _savedWindowInfo.WindowRect.ToPixelRect(); + var newClientRect = _savedWindowInfo.WindowRect.ToPixelRect(); - SetWindowPos(_hwnd, IntPtr.Zero, new_rect.X, new_rect.Y, new_rect.Width, - new_rect.Height, + SetWindowPos(_hwnd, IntPtr.Zero, newClientRect.X, newClientRect.Y, newClientRect.Width, + newClientRect.Height, SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); UpdateWindowProperties(_windowProperties, true); From 84a4de5c070f9dde1e0e752194f51c76daa978d3 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Jul 2022 16:43:50 +0100 Subject: [PATCH 185/389] make splitview not culture sensitive. --- src/Avalonia.Controls/SplitView.cs | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/SplitView.cs b/src/Avalonia.Controls/SplitView.cs index 532cb1d329..caefad4af7 100644 --- a/src/Avalonia.Controls/SplitView.cs +++ b/src/Avalonia.Controls/SplitView.cs @@ -431,18 +431,40 @@ namespace Avalonia.Controls } } + private string GetPsuedoClass(SplitViewDisplayMode mode) + { + return mode switch + { + SplitViewDisplayMode.Inline => "inline", + SplitViewDisplayMode.CompactInline => "compactinline", + SplitViewDisplayMode.Overlay => "overlay", + SplitViewDisplayMode.CompactOverlay => "compactoverlay", + _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null) + }; + } + + private string GetPsuedoClass(SplitViewPanePlacement placement) + { + return placement switch + { + SplitViewPanePlacement.Left => "left", + SplitViewPanePlacement.Right => "right", + _ => throw new ArgumentOutOfRangeException(nameof(placement), placement, null) + }; + } + private void OnPanePlacementChanged(AvaloniaPropertyChangedEventArgs e) { - var oldState = e.OldValue!.ToString()!.ToLower(); - var newState = e.NewValue!.ToString()!.ToLower(); + var oldState = GetPsuedoClass(e.GetOldValue()); + var newState = GetPsuedoClass(e.GetNewValue()); PseudoClasses.Remove($":{oldState}"); PseudoClasses.Add($":{newState}"); } private void OnDisplayModeChanged(AvaloniaPropertyChangedEventArgs e) { - var oldState = e.OldValue!.ToString()!.ToLower(); - var newState = e.NewValue!.ToString()!.ToLower(); + var oldState = GetPsuedoClass(e.GetOldValue()); + var newState = GetPsuedoClass(e.GetNewValue()); PseudoClasses.Remove($":{oldState}"); PseudoClasses.Add($":{newState}"); From b7a3bce6f99202fce9128c9824d3157e91c6d615 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Jul 2022 17:21:41 +0100 Subject: [PATCH 186/389] fix compiler warning. --- src/Avalonia.Controls/SplitView.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/SplitView.cs b/src/Avalonia.Controls/SplitView.cs index caefad4af7..f560251e53 100644 --- a/src/Avalonia.Controls/SplitView.cs +++ b/src/Avalonia.Controls/SplitView.cs @@ -469,7 +469,7 @@ namespace Avalonia.Controls PseudoClasses.Remove($":{oldState}"); PseudoClasses.Add($":{newState}"); - var (closedPaneWidth, paneColumnGridLength) = (SplitViewDisplayMode)e.NewValue switch + var (closedPaneWidth, paneColumnGridLength) = e.GetNewValue() switch { SplitViewDisplayMode.Overlay => (0, new GridLength(0, GridUnitType.Pixel)), SplitViewDisplayMode.CompactOverlay => (CompactPaneLength, new GridLength(CompactPaneLength, GridUnitType.Pixel)), From cc2d791f3683ae77c21ba8d7b9ece37ac8852a7e Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 6 Jul 2022 19:45:01 +0200 Subject: [PATCH 187/389] Update samples/Sandbox/MainWindow.axaml Co-authored-by: Max Katz --- samples/Sandbox/MainWindow.axaml | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/samples/Sandbox/MainWindow.axaml b/samples/Sandbox/MainWindow.axaml index a834e3fef3..6929f192c7 100644 --- a/samples/Sandbox/MainWindow.axaml +++ b/samples/Sandbox/MainWindow.axaml @@ -1,19 +1,4 @@ - - - - This is a - TextBlock - with several - Span elements, - - using a variety of styles - . - - - - - From 45ae4221dc8cf2f40a92c4d57dbdca7a3aacdd64 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Jul 2022 20:58:36 +0100 Subject: [PATCH 188/389] correct spelling. --- src/Avalonia.Controls/SplitView.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/SplitView.cs b/src/Avalonia.Controls/SplitView.cs index f560251e53..c344dd795d 100644 --- a/src/Avalonia.Controls/SplitView.cs +++ b/src/Avalonia.Controls/SplitView.cs @@ -431,7 +431,7 @@ namespace Avalonia.Controls } } - private string GetPsuedoClass(SplitViewDisplayMode mode) + private string GetPseudoClass(SplitViewDisplayMode mode) { return mode switch { @@ -443,7 +443,7 @@ namespace Avalonia.Controls }; } - private string GetPsuedoClass(SplitViewPanePlacement placement) + private string GetPseudoClass(SplitViewPanePlacement placement) { return placement switch { @@ -455,16 +455,16 @@ namespace Avalonia.Controls private void OnPanePlacementChanged(AvaloniaPropertyChangedEventArgs e) { - var oldState = GetPsuedoClass(e.GetOldValue()); - var newState = GetPsuedoClass(e.GetNewValue()); + var oldState = GetPseudoClass(e.GetOldValue()); + var newState = GetPseudoClass(e.GetNewValue()); PseudoClasses.Remove($":{oldState}"); PseudoClasses.Add($":{newState}"); } private void OnDisplayModeChanged(AvaloniaPropertyChangedEventArgs e) { - var oldState = GetPsuedoClass(e.GetOldValue()); - var newState = GetPsuedoClass(e.GetNewValue()); + var oldState = GetPseudoClass(e.GetOldValue()); + var newState = GetPseudoClass(e.GetNewValue()); PseudoClasses.Remove($":{oldState}"); PseudoClasses.Add($":{newState}"); From 002377044aea087f4bf3ca4b2af088be469c7b2b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 7 Jul 2022 09:50:06 +0200 Subject: [PATCH 190/389] Disallow selectors with trailing /template/. It's an invalid selector: what does `Button /template/` select? --- src/Avalonia.Base/Styling/Style.cs | 16 ++++- .../Styling/SelectorTests_Nesting.cs | 72 ------------------- .../Styling/StyleTests.cs | 7 ++ 3 files changed, 22 insertions(+), 73 deletions(-) diff --git a/src/Avalonia.Base/Styling/Style.cs b/src/Avalonia.Base/Styling/Style.cs index 77c4e62d29..c61b08b2a1 100644 --- a/src/Avalonia.Base/Styling/Style.cs +++ b/src/Avalonia.Base/Styling/Style.cs @@ -7,6 +7,8 @@ namespace Avalonia.Styling /// public class Style : StyleBase { + private Selector? _selector; + /// /// Initializes a new instance of the class. /// @@ -26,7 +28,11 @@ namespace Avalonia.Styling /// /// Gets or sets the style's selector. /// - public Selector? Selector { get; set; } + public Selector? Selector + { + get => _selector; + set => _selector = ValidateSelector(value); + } public override SelectorMatchResult TryAttach(IStyleable target, object? host) { @@ -88,5 +94,13 @@ namespace Avalonia.Styling base.SetParent(parent); } + + private static Selector? ValidateSelector(Selector? selector) + { + if (selector is TemplateSelector) + throw new InvalidOperationException( + "Invalid selector: Template selector must be followed by control selector."); + return selector; + } } } diff --git a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs index 747ad585e7..9048b488b6 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs @@ -150,78 +150,6 @@ namespace Avalonia.Base.UnitTests.Styling Assert.False(sink.Active); } - [Fact(Skip = "Template selectors a the end of nesting parent selectors currently broken")] - public void Template_Nesting_OfType_Matches() - { - var control = new Control1 { Classes = { "foo" } }; - var button = new Button - { - Template = new FuncControlTemplate((x, _) => control), - }; - - button.ApplyTemplate(); - - Style nested; - var parent = new Style(x => x.OfType - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml b/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml index 62874f4884..3c45de18c6 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml @@ -1,72 +1,57 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/Carousel.xaml b/src/Avalonia.Themes.Fluent/Controls/Carousel.xaml index baba0649aa..6c05a62250 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Carousel.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Carousel.xaml @@ -1,17 +1,20 @@ - + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml b/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml index e9830fb228..2eee425cb0 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml @@ -1,297 +1,293 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - + + + + + + - + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + diff --git a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml index 93ecc438eb..2dc7439001 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml @@ -1,232 +1,228 @@ - - - - - - Item 1 - Item 2 - - - Item 1 - Item 2 - - - - - + + + + + + Item 1 + Item 2 + + + Item 1 + Item 2 + + + + + 0,0,0,4 15 7 - + 12,5,0,7 11,5,32,6 32 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Background="Transparent" + Margin="0,1,1,1" + Width="30" + IsVisible="False" + HorizontalAlignment="Right" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + diff --git a/src/Avalonia.Themes.Fluent/Controls/ComboBoxItem.xaml b/src/Avalonia.Themes.Fluent/Controls/ComboBoxItem.xaml index 0debe87445..88f9c0ea15 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ComboBoxItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ComboBoxItem.xaml @@ -1,90 +1,85 @@ - - - - - - Item 1 - Item 2 long - Item 3 - Item 4 - + + + + + + Item 1 + Item 2 long + Item 3 + Item 4 + + - - + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/Common.xaml b/src/Avalonia.Themes.Fluent/Controls/Common.xaml deleted file mode 100644 index e09e39d7cb..0000000000 --- a/src/Avalonia.Themes.Fluent/Controls/Common.xaml +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/src/Avalonia.Themes.Fluent/Controls/ContentControl.xaml b/src/Avalonia.Themes.Fluent/Controls/ContentControl.xaml index d32bc399b6..5777603074 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ContentControl.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ContentControl.xaml @@ -1,16 +1,19 @@ - + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/ContextMenu.xaml b/src/Avalonia.Themes.Fluent/Controls/ContextMenu.xaml index df800b4a06..fe0f0f3f6c 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ContextMenu.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ContextMenu.xaml @@ -1,72 +1,72 @@ - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml b/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml index 649a186c7e..6ed73148a7 100644 --- a/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml @@ -1,104 +1,103 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml index 11d6b9fdfc..a7093c1341 100644 --- a/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml @@ -5,17 +5,16 @@ // All other rights reserved. --> - - - - - - - - + + + + + + 0,0,0,4 40 40 @@ -27,328 +26,327 @@ 0,3,0,6 9,3,0,6 1 - - - - - - - - + + + + + + + + + + + - - + + + + + - + - + + + + + + + + + + + + - - + + + + + + + + + + - - - + + + + - + - + + + - + - - - - - + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + - - - - + diff --git a/src/Avalonia.Themes.Fluent/Controls/DropDownButton.xaml b/src/Avalonia.Themes.Fluent/Controls/DropDownButton.xaml index b96c689ab6..3206cb8cb1 100644 --- a/src/Avalonia.Themes.Fluent/Controls/DropDownButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/DropDownButton.xaml @@ -1,103 +1,101 @@ - - - - - - - - - - - - - 32 - + + + + + + + + + - - - - - - - - - - + 32 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml b/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml index 79560be933..89eaec70ad 100644 --- a/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml @@ -1,19 +1,23 @@ - + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/Expander.xaml b/src/Avalonia.Themes.Fluent/Controls/Expander.xaml index 33d502772e..36f6008782 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Expander.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Expander.xaml @@ -1,6 +1,6 @@ - + @@ -34,72 +34,34 @@ - - 16 - 16 + 16 + 16 - 1 + 1 - 1,1,0,1 - 1,1,1,0 - 0,1,1,1 - 1,0,1,1 + 1,1,0,1 + 1,1,1,0 + 0,1,1,1 + 1,0,1,1 - - + + - - + + - + - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index 5b217e4764..286b3e3fcd 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -1,70 +1,78 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml b/src/Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml index 92f8177ead..2dca5b0770 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml @@ -1,9 +1,7 @@ - - - 1 - - - - + + diff --git a/src/Avalonia.Themes.Fluent/Controls/FocusAdorner.xaml b/src/Avalonia.Themes.Fluent/Controls/FocusAdorner.xaml index 91bf71ed4d..c3f489da80 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FocusAdorner.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FocusAdorner.xaml @@ -1,4 +1,4 @@ - 0 diff --git a/src/Avalonia.Themes.Fluent/Controls/GridSplitter.xaml b/src/Avalonia.Themes.Fluent/Controls/GridSplitter.xaml index e3a7b04f33..ca57eccd13 100644 --- a/src/Avalonia.Themes.Fluent/Controls/GridSplitter.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/GridSplitter.xaml @@ -1,24 +1,22 @@ - - - - - + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/ItemsControl.xaml b/src/Avalonia.Themes.Fluent/Controls/ItemsControl.xaml index 19d13b6399..b8d7c2c4ef 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ItemsControl.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ItemsControl.xaml @@ -1,16 +1,19 @@ - + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/Label.xaml b/src/Avalonia.Themes.Fluent/Controls/Label.xaml index d41e4e2166..ad57239648 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Label.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Label.xaml @@ -1,18 +1,22 @@ - + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml b/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml index 8011ed9daf..b1d4b77a3f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml @@ -1,45 +1,47 @@ - - - - - Test - Test - Test - Test - - - - - + + + + + Test + Test + Test + Test + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/ListBoxItem.xaml b/src/Avalonia.Themes.Fluent/Controls/ListBoxItem.xaml index 11f3c12772..c00e920f75 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ListBoxItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ListBoxItem.xaml @@ -1,94 +1,79 @@ - - - - - Disabled - - Test - - Test - - - - + + + + + Disabled + + Test + + Test + + + 12,9,12,12 - - + + + + + + + + - + - - + + - - - + + - - - + + - + - - - + + - - - + + - - - - + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml b/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml index 60e16fcff2..dc454e4fdb 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml @@ -1,324 +1,324 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + - - - + + - - + + + + - - + + diff --git a/src/Avalonia.Themes.Fluent/Controls/Menu.xaml b/src/Avalonia.Themes.Fluent/Controls/Menu.xaml index 4eb724a926..d42e413f00 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Menu.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Menu.xaml @@ -1,36 +1,35 @@ - + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/MenuFlyoutPresenter.xaml b/src/Avalonia.Themes.Fluent/Controls/MenuFlyoutPresenter.xaml index ff50acab5e..7769f0a5c9 100644 --- a/src/Avalonia.Themes.Fluent/Controls/MenuFlyoutPresenter.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/MenuFlyoutPresenter.xaml @@ -1,35 +1,34 @@ - - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml index 33cf6bfdde..b7a9435749 100644 --- a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml @@ -1,4 +1,4 @@ - - -4 + -4 0,0,12,0 24,0,0,0 M 1,0 10,10 l -9,10 -1,-1 L 8,10 -0,1 Z diff --git a/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml b/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml index d40ba0cc1d..753c03992a 100644 --- a/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml @@ -1,30 +1,29 @@ - - - - - - + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/NotificationCard.xaml b/src/Avalonia.Themes.Fluent/Controls/NotificationCard.xaml index 924d977eb5..e55fcb90e8 100644 --- a/src/Avalonia.Themes.Fluent/Controls/NotificationCard.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/NotificationCard.xaml @@ -1,11 +1,11 @@ - + - + - - - - - - - - - - + + diff --git a/src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml b/src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml index 36ab07e3e3..74ffff0f5c 100644 --- a/src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml @@ -1,68 +1,67 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml b/src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml index 31b43c39cd..0b587e6037 100644 --- a/src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml @@ -1,20 +1,23 @@ - + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/PathIcon.xaml b/src/Avalonia.Themes.Fluent/Controls/PathIcon.xaml index d4952b3571..966c6c2632 100644 --- a/src/Avalonia.Themes.Fluent/Controls/PathIcon.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/PathIcon.xaml @@ -1,29 +1,26 @@ - - - - - M14 9.50006C11.5147 9.50006 9.5 11.5148 9.5 14.0001C9.5 16.4853 11.5147 18.5001 14 18.5001C15.3488 18.5001 16.559 17.9066 17.3838 16.9666C18.0787 16.1746 18.5 15.1365 18.5 14.0001C18.5 13.5401 18.431 13.0963 18.3028 12.6784C17.7382 10.8381 16.0253 9.50006 14 9.50006ZM11 14.0001C11 12.3432 12.3431 11.0001 14 11.0001C15.6569 11.0001 17 12.3432 17 14.0001C17 15.6569 15.6569 17.0001 14 17.0001C12.3431 17.0001 11 15.6569 11 14.0001Z M21.7093 22.3948L19.9818 21.6364C19.4876 21.4197 18.9071 21.4515 18.44 21.7219C17.9729 21.9924 17.675 22.4693 17.6157 23.0066L17.408 24.8855C17.3651 25.273 17.084 25.5917 16.7055 25.682C14.9263 26.1061 13.0725 26.1061 11.2933 25.682C10.9148 25.5917 10.6336 25.273 10.5908 24.8855L10.3834 23.0093C10.3225 22.4731 10.0112 21.9976 9.54452 21.7281C9.07783 21.4586 8.51117 21.4269 8.01859 21.6424L6.29071 22.4009C5.93281 22.558 5.51493 22.4718 5.24806 22.1859C4.00474 20.8536 3.07924 19.2561 2.54122 17.5137C2.42533 17.1384 2.55922 16.7307 2.8749 16.4977L4.40219 15.3703C4.83721 15.0501 5.09414 14.5415 5.09414 14.0007C5.09414 13.4598 4.83721 12.9512 4.40162 12.6306L2.87529 11.5051C2.55914 11.272 2.42513 10.8638 2.54142 10.4882C3.08038 8.74734 4.00637 7.15163 5.24971 5.82114C5.51684 5.53528 5.93492 5.44941 6.29276 5.60691L8.01296 6.36404C8.50793 6.58168 9.07696 6.54881 9.54617 6.27415C10.0133 6.00264 10.3244 5.52527 10.3844 4.98794L10.5933 3.11017C10.637 2.71803 10.9245 2.39704 11.3089 2.31138C12.19 2.11504 13.0891 2.01071 14.0131 2.00006C14.9147 2.01047 15.8128 2.11485 16.6928 2.31149C17.077 2.39734 17.3643 2.71823 17.4079 3.11017L17.617 4.98937C17.7116 5.85221 18.4387 6.50572 19.3055 6.50663C19.5385 6.507 19.769 6.45838 19.9843 6.36294L21.7048 5.60568C22.0626 5.44818 22.4807 5.53405 22.7478 5.81991C23.9912 7.1504 24.9172 8.74611 25.4561 10.487C25.5723 10.8623 25.4386 11.2703 25.1228 11.5035L23.5978 12.6297C23.1628 12.95 22.9 13.4586 22.9 13.9994C22.9 14.5403 23.1628 15.0489 23.5988 15.3698L25.1251 16.4965C25.441 16.7296 25.5748 17.1376 25.4586 17.5131C24.9198 19.2536 23.9944 20.8492 22.7517 22.1799C22.4849 22.4657 22.0671 22.5518 21.7093 22.3948ZM16.263 22.1966C16.4982 21.4685 16.9889 20.8288 17.6884 20.4238C18.5702 19.9132 19.6536 19.8547 20.5841 20.2627L21.9281 20.8526C22.791 19.8538 23.4593 18.7013 23.8981 17.4552L22.7095 16.5778L22.7086 16.5771C21.898 15.98 21.4 15.0277 21.4 13.9994C21.4 12.9719 21.8974 12.0195 22.7073 11.4227L22.7085 11.4218L23.8957 10.545C23.4567 9.2988 22.7881 8.14636 21.9248 7.1477L20.5922 7.73425L20.5899 7.73527C20.1844 7.91463 19.7472 8.00722 19.3039 8.00663C17.6715 8.00453 16.3046 6.77431 16.1261 5.15465L16.1259 5.15291L15.9635 3.69304C15.3202 3.57328 14.6677 3.50872 14.013 3.50017C13.3389 3.50891 12.6821 3.57367 12.0377 3.69328L11.8751 5.15452C11.7625 6.16272 11.1793 7.05909 10.3019 7.56986C9.41937 8.0856 8.34453 8.14844 7.40869 7.73694L6.07273 7.14893C5.20949 8.14751 4.54092 9.29983 4.10196 10.5459L5.29181 11.4233C6.11115 12.0269 6.59414 12.9837 6.59414 14.0007C6.59414 15.0173 6.11142 15.9742 5.29237 16.5776L4.10161 17.4566C4.54002 18.7044 5.2085 19.8585 6.07205 20.8587L7.41742 20.2682C8.34745 19.8613 9.41573 19.9215 10.2947 20.4292C11.174 20.937 11.7593 21.832 11.8738 22.84L11.8744 22.8445L12.0362 24.3088C13.3326 24.5638 14.6662 24.5638 15.9626 24.3088L16.1247 22.8418C16.1491 22.6217 16.1955 22.4055 16.263 22.1966Z - - - - - - + + + + + M14 9.50006C11.5147 9.50006 9.5 11.5148 9.5 14.0001C9.5 16.4853 11.5147 18.5001 14 18.5001C15.3488 18.5001 16.559 17.9066 17.3838 16.9666C18.0787 16.1746 18.5 15.1365 18.5 14.0001C18.5 13.5401 18.431 13.0963 18.3028 12.6784C17.7382 10.8381 16.0253 9.50006 14 9.50006ZM11 14.0001C11 12.3432 12.3431 11.0001 14 11.0001C15.6569 11.0001 17 12.3432 17 14.0001C17 15.6569 15.6569 17.0001 14 17.0001C12.3431 17.0001 11 15.6569 11 14.0001Z M21.7093 22.3948L19.9818 21.6364C19.4876 21.4197 18.9071 21.4515 18.44 21.7219C17.9729 21.9924 17.675 22.4693 17.6157 23.0066L17.408 24.8855C17.3651 25.273 17.084 25.5917 16.7055 25.682C14.9263 26.1061 13.0725 26.1061 11.2933 25.682C10.9148 25.5917 10.6336 25.273 10.5908 24.8855L10.3834 23.0093C10.3225 22.4731 10.0112 21.9976 9.54452 21.7281C9.07783 21.4586 8.51117 21.4269 8.01859 21.6424L6.29071 22.4009C5.93281 22.558 5.51493 22.4718 5.24806 22.1859C4.00474 20.8536 3.07924 19.2561 2.54122 17.5137C2.42533 17.1384 2.55922 16.7307 2.8749 16.4977L4.40219 15.3703C4.83721 15.0501 5.09414 14.5415 5.09414 14.0007C5.09414 13.4598 4.83721 12.9512 4.40162 12.6306L2.87529 11.5051C2.55914 11.272 2.42513 10.8638 2.54142 10.4882C3.08038 8.74734 4.00637 7.15163 5.24971 5.82114C5.51684 5.53528 5.93492 5.44941 6.29276 5.60691L8.01296 6.36404C8.50793 6.58168 9.07696 6.54881 9.54617 6.27415C10.0133 6.00264 10.3244 5.52527 10.3844 4.98794L10.5933 3.11017C10.637 2.71803 10.9245 2.39704 11.3089 2.31138C12.19 2.11504 13.0891 2.01071 14.0131 2.00006C14.9147 2.01047 15.8128 2.11485 16.6928 2.31149C17.077 2.39734 17.3643 2.71823 17.4079 3.11017L17.617 4.98937C17.7116 5.85221 18.4387 6.50572 19.3055 6.50663C19.5385 6.507 19.769 6.45838 19.9843 6.36294L21.7048 5.60568C22.0626 5.44818 22.4807 5.53405 22.7478 5.81991C23.9912 7.1504 24.9172 8.74611 25.4561 10.487C25.5723 10.8623 25.4386 11.2703 25.1228 11.5035L23.5978 12.6297C23.1628 12.95 22.9 13.4586 22.9 13.9994C22.9 14.5403 23.1628 15.0489 23.5988 15.3698L25.1251 16.4965C25.441 16.7296 25.5748 17.1376 25.4586 17.5131C24.9198 19.2536 23.9944 20.8492 22.7517 22.1799C22.4849 22.4657 22.0671 22.5518 21.7093 22.3948ZM16.263 22.1966C16.4982 21.4685 16.9889 20.8288 17.6884 20.4238C18.5702 19.9132 19.6536 19.8547 20.5841 20.2627L21.9281 20.8526C22.791 19.8538 23.4593 18.7013 23.8981 17.4552L22.7095 16.5778L22.7086 16.5771C21.898 15.98 21.4 15.0277 21.4 13.9994C21.4 12.9719 21.8974 12.0195 22.7073 11.4227L22.7085 11.4218L23.8957 10.545C23.4567 9.2988 22.7881 8.14636 21.9248 7.1477L20.5922 7.73425L20.5899 7.73527C20.1844 7.91463 19.7472 8.00722 19.3039 8.00663C17.6715 8.00453 16.3046 6.77431 16.1261 5.15465L16.1259 5.15291L15.9635 3.69304C15.3202 3.57328 14.6677 3.50872 14.013 3.50017C13.3389 3.50891 12.6821 3.57367 12.0377 3.69328L11.8751 5.15452C11.7625 6.16272 11.1793 7.05909 10.3019 7.56986C9.41937 8.0856 8.34453 8.14844 7.40869 7.73694L6.07273 7.14893C5.20949 8.14751 4.54092 9.29983 4.10196 10.5459L5.29181 11.4233C6.11115 12.0269 6.59414 12.9837 6.59414 14.0007C6.59414 15.0173 6.11142 15.9742 5.29237 16.5776L4.10161 17.4566C4.54002 18.7044 5.2085 19.8585 6.07205 20.8587L7.41742 20.2682C8.34745 19.8613 9.41573 19.9215 10.2947 20.4292C11.174 20.937 11.7593 21.832 11.8738 22.84L11.8744 22.8445L12.0362 24.3088C13.3326 24.5638 14.6662 24.5638 15.9626 24.3088L16.1247 22.8418C16.1491 22.6217 16.1955 22.4055 16.263 22.1966Z + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml b/src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml index f608cf55f5..f57ecd2013 100644 --- a/src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml @@ -1,28 +1,28 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml b/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml index a463334a76..3c7d22e13f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml @@ -1,164 +1,179 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/RadioButton.xaml b/src/Avalonia.Themes.Fluent/Controls/RadioButton.xaml index e967dc8807..a1c4ba4f2d 100644 --- a/src/Avalonia.Themes.Fluent/Controls/RadioButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/RadioButton.xaml @@ -1,4 +1,4 @@ - + @@ -9,7 +9,8 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml b/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml index 7fa515d3f7..670212c4b1 100644 --- a/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml @@ -1,63 +1,65 @@ - - - - - - - - - + + + + + + + + + 8,5,8,6 - - - + + + + + + + + + + + + + + + + - + - + - - + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/ScrollBar.xaml b/src/Avalonia.Themes.Fluent/Controls/ScrollBar.xaml index 4727ff72b9..7d87a3e35a 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ScrollBar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ScrollBar.xaml @@ -1,4 +1,4 @@ - + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/Separator.xaml b/src/Avalonia.Themes.Fluent/Controls/Separator.xaml index 5d95ccc404..afbc3770c1 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Separator.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Separator.xaml @@ -1,7 +1,7 @@ - - - - - + + diff --git a/src/Avalonia.Themes.Fluent/Controls/Slider.xaml b/src/Avalonia.Themes.Fluent/Controls/Slider.xaml index cd2c02c567..b468f7f4ad 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Slider.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Slider.xaml @@ -1,19 +1,20 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + 0,0,0,4 15 15 @@ -24,252 +25,364 @@ 20 20 20 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - + + + + + + - + - + + + + - + - - - - - - - - - + - - - - - - - - - - - + - - - - - - - - + - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml b/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml index 91c901f567..fb0460d9a1 100644 --- a/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml @@ -2,7 +2,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:CompileBindings="True" xmlns:converters="using:Avalonia.Controls.Converters"> - diff --git a/src/Avalonia.Themes.Fluent/Controls/SplitView.xaml b/src/Avalonia.Themes.Fluent/Controls/SplitView.xaml index 55d46e32a1..6b9b94852f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/SplitView.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/SplitView.xaml @@ -1,4 +1,4 @@ - diff --git a/src/Avalonia.Themes.Fluent/Controls/TabControl.xaml b/src/Avalonia.Themes.Fluent/Controls/TabControl.xaml index 322f6ce89e..1cc31f8e15 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TabControl.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TabControl.xaml @@ -1,65 +1,63 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - 0 0 0 2 - - - - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/TabItem.xaml b/src/Avalonia.Themes.Fluent/Controls/TabItem.xaml index ebe6f82917..110ffc2f48 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TabItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TabItem.xaml @@ -1,132 +1,133 @@ - - - - - - - - - - + + + + + + + + + + 48 24 2 - - + + + + + + + + + + + + + + + + + + + - - - + + + - - - - + + + + - - + + - - + + - - + + - - + + - - + + - - - + + + - + - - - - + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/TabStrip.xaml b/src/Avalonia.Themes.Fluent/Controls/TabStrip.xaml index 681ac48850..15c3ba2f37 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TabStrip.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TabStrip.xaml @@ -1,32 +1,34 @@ - - - - - Item 1 - Item 2 - Disabled - - - - - + + + + + Item 1 + Item 2 + Disabled + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml b/src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml index 59f68a1547..5ccd430d33 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml @@ -1,107 +1,108 @@ - - - - - Leaf - Arch - - - - + + + + + Leaf + Arch + + + + 48 2 - - + + + + + + + + + + + + + + + + + + + - + - - - - + + + + - - - - + + + + - - + + - - + + - - + + - - + + - - - + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml b/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml index 40d9b11f7c..0fd3758d77 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml @@ -1,4 +1,4 @@ - @@ -10,7 +10,7 @@ TextAlignment="Center"/> - + 0,0,0,4 M 11.416016,10 20,1.4160156 18.583984,0 10,8.5839846 1.4160156,0 0,1.4160156 8.5839844,10 0,18.583985 1.4160156,20 10,11.416015 18.583984,20 20,18.583985 Z @@ -36,7 +36,7 @@ - + @@ -73,32 +73,32 @@ VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}" IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}" AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}"> - - - - - + + + + + @@ -159,7 +159,7 @@ - + @@ -237,7 +237,7 @@ - + - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - diff --git a/src/Avalonia.Themes.Fluent/Controls/TitleBar.xaml b/src/Avalonia.Themes.Fluent/Controls/TitleBar.xaml index 4dba5b4ba4..2b97df020f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TitleBar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TitleBar.xaml @@ -1,53 +1,55 @@ - - - - - - - + + - + - + - + - + - + - - + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml b/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml index e0d54d7232..b53980b792 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml @@ -1,5 +1,5 @@ - + @@ -10,104 +10,111 @@ - 8,5,8,6 - - - - - + + + + + + + + + + + + + + + + - + - + - + - + - + - + - + - + + - + - - + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml b/src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml index a2b50a859d..533903bec7 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml @@ -1,288 +1,307 @@ - - + + + + + + + + + + + + + + + + + 0,0,0,6 6 6 0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml b/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml index 2d18be91cb..1070f3b68c 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml @@ -1,87 +1,71 @@ - - - - - - Hover Here - - + + + + Hover Here + + + - - - - ToolTip - A control which pops up a hint when a control is hovered - - - ToolTip bottom placement - - - + Padding="50" + ToolTip.Placement="Bottom"> + + + ToolTip + A control which pops up a hint when a control is hovered + + + ToolTip bottom placement + + + - 320 - - - - + + + + + + + + + + + + + + + + + + + + + + - + - - + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/TransitioningContentControl.xaml b/src/Avalonia.Themes.Fluent/Controls/TransitioningContentControl.xaml index 6a4d56ccb7..f28dc66a5f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TransitioningContentControl.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TransitioningContentControl.xaml @@ -1,20 +1,19 @@ - - - + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/TreeView.xaml b/src/Avalonia.Themes.Fluent/Controls/TreeView.xaml index 656b72e07b..4fcec79f25 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TreeView.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TreeView.xaml @@ -1,28 +1,31 @@ - + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml b/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml index f86b67bb6c..6510832eb3 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml @@ -1,4 +1,4 @@ - @@ -53,7 +53,6 @@ diff --git a/src/Avalonia.Themes.Fluent/Controls/Window.xaml b/src/Avalonia.Themes.Fluent/Controls/Window.xaml index 90963c606c..d78fd76a37 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Window.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Window.xaml @@ -1,27 +1,30 @@ - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/WindowNotificationManager.xaml b/src/Avalonia.Themes.Fluent/Controls/WindowNotificationManager.xaml index 8d14c2d972..d5d5114c1b 100644 --- a/src/Avalonia.Themes.Fluent/Controls/WindowNotificationManager.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/WindowNotificationManager.xaml @@ -1,47 +1,58 @@ - - + + + + + + + + + + + + + + + + + + + + - + - + - + - - + + + From afe3486e9f3ea7a493c7e66ca3e5b59682357dcb Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 7 Jul 2022 11:15:41 +0200 Subject: [PATCH 218/389] Remove trailing /template/ selectors. --- .../Controls/ButtonSpinner.xaml | 12 +-- .../Controls/CalendarDatePicker.xaml | 12 +-- .../Controls/CalendarDayButton.xaml | 6 +- .../Controls/CheckBox.xaml | 88 +++++++++---------- .../Controls/ComboBox.xaml | 50 +++++------ .../Controls/Slider.xaml | 56 ++++++------ .../Controls/TimePicker.xaml | 6 +- 7 files changed, 113 insertions(+), 117 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml b/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml index a5234f5771..f99f1040e5 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml @@ -87,20 +87,20 @@ - - - - diff --git a/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml index 83a254be56..eefd9e9b51 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml @@ -106,13 +106,13 @@ - - @@ -152,13 +152,13 @@ - - diff --git a/src/Avalonia.Themes.Fluent/Controls/CalendarDayButton.xaml b/src/Avalonia.Themes.Fluent/Controls/CalendarDayButton.xaml index b119dd1355..9f476d51bc 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CalendarDayButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CalendarDayButton.xaml @@ -84,11 +84,11 @@ - - diff --git a/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml b/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml index 2eee425cb0..44e1afefa4 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml @@ -63,64 +63,64 @@ - - - - - - - - - - - - @@ -145,64 +145,64 @@ - - - - - - - - - - - - @@ -227,64 +227,64 @@ - - - - - - - - - - - - diff --git a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml index 2dc7439001..8ef8388cf1 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml @@ -144,17 +144,15 @@ - - - + + + @@ -175,51 +173,51 @@ - - - - - - - - - - - - diff --git a/src/Avalonia.Themes.Fluent/Controls/Slider.xaml b/src/Avalonia.Themes.Fluent/Controls/Slider.xaml index b468f7f4ad..c74878e8be 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Slider.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Slider.xaml @@ -283,17 +283,15 @@ - - - + + + @@ -316,70 +314,70 @@ - - - - - - - - - - - - - - diff --git a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml index 69b2d32dda..dc09032a36 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml @@ -82,11 +82,11 @@ --> - - From 671b7ea83e75a2b442a4ee0d5877c0e269dfbc48 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 7 Jul 2022 11:50:33 +0200 Subject: [PATCH 219/389] WIP: HamburgerMenu. --- samples/ControlCatalog/App.xaml | 8 +- .../HamburgerMenu/HamburgerMenu.xaml | 260 +++++++++--------- 2 files changed, 139 insertions(+), 129 deletions(-) diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index d0e1bd885e..c7ad72d2c5 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -5,6 +5,13 @@ x:CompileBindings="True" Name="Avalonia ControlCatalog" x:Class="ControlCatalog.App"> + + + + + + + - diff --git a/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml b/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml index 1d58c465a0..5f8bfe4fb5 100644 --- a/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml +++ b/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml @@ -1,6 +1,6 @@ - + @@ -20,21 +20,84 @@ - - 40 - 220 - 36 - 36 - 32 - 12,0,0,0 - 52,0,0,0 - 212,0,0,0 - 1 1 1 1 #2000, 0 0 1 1 #2fff - 0 0 1 1 #2000 - + 40 + 220 + 36 + 36 + 32 + 12,0,0,0 + 52,0,0,0 + 212,0,0,0 + 1 1 1 1 #2000, 0 0 1 1 #2fff + 0 0 1 1 #2000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + + + From f6e8dda027e159820f744a03ebff8cca80d1c998 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 8 Jul 2022 17:26:17 +0200 Subject: [PATCH 260/389] Formatted CalendarDatePicker ControlTheme. --- .../Controls/CalendarDatePicker.xaml | 328 +++++++++--------- 1 file changed, 163 insertions(+), 165 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml index eefd9e9b51..54f6a200b8 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml @@ -2,179 +2,177 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sys="clr-namespace:System;assembly=netstandard" x:CompileBindings="True"> - - - - - - - 12 - 32 + + + + + - - - - - - - - - - - 12 + 32 + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - + - - + - - - + + + + + + + + + + + From 83140fe427b8ddcb691b7dc063e5af965de40180 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 9 Jul 2022 00:18:28 +0200 Subject: [PATCH 265/389] Reset button animations correctly. Otherwise they keep on animating way after the interaction. --- src/Avalonia.Themes.Fluent/Controls/Button.xaml | 1 + src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml | 1 + src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml | 1 + 3 files changed, 3 insertions(+) diff --git a/src/Avalonia.Themes.Fluent/Controls/Button.xaml b/src/Avalonia.Themes.Fluent/Controls/Button.xaml index 7ac5c4351a..7828fd52ed 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Button.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Button.xaml @@ -20,6 +20,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml b/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml index 5e789e7e10..a54187104b 100644 --- a/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml @@ -23,6 +23,7 @@ + + From 0d5b10ec3aa041b21d5cd4a785b37201dec03f7a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 9 Jul 2022 00:25:04 +0200 Subject: [PATCH 266/389] Tweaked DatePicker ControlTheme. --- .../Controls/DatePicker.xaml | 629 +++++++++--------- 1 file changed, 317 insertions(+), 312 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml index a7093c1341..0537feb60b 100644 --- a/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml @@ -9,344 +9,349 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sys="clr-namespace:System;assembly=netstandard" x:CompileBindings="True"> - - - - - + + + + + - 0,0,0,4 - 40 - 40 - 41 - 296 - 456 - 0,3,0,6 - 9,3,0,6 - 0,3,0,6 - 9,3,0,6 - 1 + 0,0,0,4 + 40 + 40 + 41 + 296 + 456 + 0,3,0,6 + 9,3,0,6 + 0,3,0,6 + 9,3,0,6 + 1 - - - - - - - - + + + + + + + - - + + + - - - - - - - - - + + + + + + + + + - - - - - + + + + + - + - - + - - - - - - - - - - + + - - - - - - - - - - + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + - + + + + + + + + + + + + + + - - - + - - - - + + + - + + + + - - - + - + + + - + - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + - + - - - + + + From c4541e27db84734a963dc785060ca023a2606283 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 9 Jul 2022 00:48:00 +0200 Subject: [PATCH 267/389] Ported ManagedFileChooser to ControlTheme. --- .../Controls/FluentControls.xaml | 4 +- .../Controls/ManagedFileChooser.xaml | 156 +++++++++--------- 2 files changed, 82 insertions(+), 78 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index 0b5f6a4a72..1c936ba097 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -59,6 +59,8 @@ + + @@ -72,7 +74,5 @@ - - diff --git a/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml b/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml index dc454e4fdb..55f4893057 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml @@ -1,8 +1,14 @@ - - + - + + + + - - - - - - - - - - + + + - - - + + + From da264ac4d0587b7cd6bd36cf0181caafacd83642 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 9 Jul 2022 02:20:14 +0200 Subject: [PATCH 268/389] WIP: Menu/Item. Something strange is happening when you open the menu a 3rd time. --- .../Controls/FluentControls.xaml | 2 +- src/Avalonia.Themes.Fluent/Controls/Menu.xaml | 82 ++++- .../Controls/MenuItem.xaml | 303 +++++++----------- 3 files changed, 191 insertions(+), 196 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index 1c936ba097..6d51dac17f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -26,6 +26,7 @@ + @@ -67,7 +68,6 @@ - diff --git a/src/Avalonia.Themes.Fluent/Controls/Menu.xaml b/src/Avalonia.Themes.Fluent/Controls/Menu.xaml index 11d21b6b23..3e0e81f870 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Menu.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Menu.xaml @@ -3,17 +3,95 @@ - - + + + 32 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + + 😊 + - - - - + + + + - - + + + + + - - + + - + - - - -4 - 0,0,12,0 - 24,0,0,0 - M 1,0 10,10 l -9,10 -1,-1 L 8,10 -0,1 Z - + + -4 + 0,0,12,0 + 24,0,0,0 + M 1,0 10,10 l -9,10 -1,-1 L 8,10 -0,1 Z - - - - - - - - - - - - - - - - - + - - - - - + + + + + - - - - + + + + + - + + + + + - + - + - - + + + From d2e4d58f178113d500ca5dffc06734be751ce403 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 9 Jul 2022 15:17:15 +0200 Subject: [PATCH 269/389] Formatted NativeMenuBar ControlTheme. --- src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml b/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml index 753c03992a..4f51ebfa7f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml @@ -25,5 +25,5 @@ - + From d902c3eb0ca5a7559d1e7843abd93f567fe065bc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 9 Jul 2022 15:53:41 +0200 Subject: [PATCH 270/389] Port ScrollViewer to ControlTheme. Including splitting out the menu scroller into a separate xaml file for separate preview. --- .../Controls/FluentControls.xaml | 1 + .../Controls/MenuItem.xaml | 28 ++- .../Controls/MenuScrollViewer.xaml | 98 +++++++++++ .../Controls/ScrollViewer.xaml | 164 +++++++++--------- 4 files changed, 204 insertions(+), 87 deletions(-) create mode 100644 src/Avalonia.Themes.Fluent/Controls/MenuScrollViewer.xaml diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index 6d51dac17f..3e4361465c 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -25,6 +25,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml index 3ec65c4936..8f77d1bc1b 100644 --- a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml @@ -5,8 +5,8 @@ x:DataType="MenuItem" x:CompileBindings="True"> - - + + @@ -22,8 +22,26 @@ - - + + + + + + + + + + + + + + + + + + + + @@ -113,7 +131,7 @@ MinHeight="{DynamicResource MenuFlyoutThemeMinHeight}" HorizontalAlignment="Stretch" CornerRadius="{DynamicResource OverlayCornerRadius}"> - + + + + + + Item 1 + Item 2 + Item 3 + Item 4 + Item 5 + Item 6 + Item 7 + Item 8 + Item 9 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml b/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml index a6e02d2769..55adb54d9b 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml @@ -1,87 +1,87 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + Item 1 + Item 2 + Item 3 + Item 4 + Item 5 + Item 6 + Item 7 + Item 8 + Item 9 + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + From 1d15d1b51343137a24171002b6f07ac98517b1af Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 10 Jul 2022 00:04:29 +0200 Subject: [PATCH 271/389] Ported SplitView to ControlTheme. --- .../Controls/FluentControls.xaml | 2 +- .../Controls/SplitView.xaml | 444 +++++++++--------- 2 files changed, 231 insertions(+), 215 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index 3e4361465c..fc89d7c21e 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -39,6 +39,7 @@ + @@ -72,7 +73,6 @@ - diff --git a/src/Avalonia.Themes.Fluent/Controls/SplitView.xaml b/src/Avalonia.Themes.Fluent/Controls/SplitView.xaml index 6b9b94852f..9feb6c7fc6 100644 --- a/src/Avalonia.Themes.Fluent/Controls/SplitView.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/SplitView.xaml @@ -1,4 +1,4 @@ - @@ -15,219 +15,235 @@ - - 320 - 48 - 00:00:00.2 - 00:00:00.1 - 0.1,0.9,0.2,1.0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From b1038f115b8abdc48ae884ec531a3ae385731010 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 10 Jul 2022 01:31:23 +0200 Subject: [PATCH 272/389] Ported TreeViewItem to ControlTheme. --- .../Controls/FluentControls.xaml | 4 +- .../Controls/TreeViewItem.xaml | 178 +++++++++--------- 2 files changed, 87 insertions(+), 95 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index fc89d7c21e..58c33bf26f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -51,6 +51,7 @@ + @@ -68,10 +69,7 @@ - - - diff --git a/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml b/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml index 6510832eb3..83abe7848c 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml @@ -1,4 +1,4 @@ - @@ -6,32 +6,29 @@ - - + + - + - + - - 16 - 12 - 12, 0, 12, 0 - M 1,0 10,10 l -9,10 -1,-1 L 8,10 -0,1 Z - M0,1 L10,10 20,1 19,0 10,8 1,0 Z - - + 16 + 12 + 12, 0, 12, 0 + M 1,0 10,10 l -9,10 -1,-1 L 8,10 -0,1 Z + M0,1 L10,10 20,1 19,0 10,8 1,0 Z + - - + + + @@ -78,7 +80,7 @@ @@ -98,83 +100,75 @@ - - - - - - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - - - + + + - + + + + From 7fcd73836940e0bf3654fe4e0a86493eb75e8386 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 10 Jul 2022 02:12:56 +0200 Subject: [PATCH 273/389] Tweaked TitleBar ControlTheme. --- src/Avalonia.Controls/Chrome/TitleBar.cs | 15 ++++++------ .../Controls/TitleBar.xaml | 24 +++++++++++-------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Controls/Chrome/TitleBar.cs b/src/Avalonia.Controls/Chrome/TitleBar.cs index b152a31587..428bf8f9b0 100644 --- a/src/Avalonia.Controls/Chrome/TitleBar.cs +++ b/src/Avalonia.Controls/Chrome/TitleBar.cs @@ -35,7 +35,8 @@ namespace Avalonia.Controls.Chrome } } - IsVisible = window.PlatformImpl?.NeedsManagedDecorations ?? false; + if (!Design.IsDesignMode) + IsVisible = window.PlatformImpl?.NeedsManagedDecorations ?? false; } } @@ -44,13 +45,13 @@ namespace Avalonia.Controls.Chrome base.OnApplyTemplate(e); _captionButtons?.Detach(); - + _captionButtons = e.NameScope.Get("PART_CaptionButtons"); - if (VisualRoot is Window window) + if (VisualRoot is Window window && !Design.IsDesignMode) { - _captionButtons?.Attach(window); - + _captionButtons?.Attach(window); + UpdateSize(window); } } @@ -59,7 +60,7 @@ namespace Avalonia.Controls.Chrome { base.OnAttachedToVisualTree(e); - if (VisualRoot is Window window) + if (VisualRoot is Window window && !Design.IsDesignMode) { _disposables = new CompositeDisposable { @@ -90,7 +91,7 @@ namespace Avalonia.Controls.Chrome base.OnDetachedFromVisualTree(e); _disposables?.Dispose(); - + _captionButtons?.Detach(); _captionButtons = null; } diff --git a/src/Avalonia.Themes.Fluent/Controls/TitleBar.xaml b/src/Avalonia.Themes.Fluent/Controls/TitleBar.xaml index 2b97df020f..5730b19e78 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TitleBar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TitleBar.xaml @@ -1,8 +1,8 @@  - - + + @@ -13,11 +13,19 @@ - - + + - - + + @@ -27,10 +35,6 @@ - - From 5864452d75a596bdd7bd0d0cdd6de8f3805d8159 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 10 Jul 2022 15:19:40 +0200 Subject: [PATCH 274/389] Undo changed to TitleBar. It was causing the title bar to be shown in all designer previews. --- src/Avalonia.Controls/Chrome/TitleBar.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Chrome/TitleBar.cs b/src/Avalonia.Controls/Chrome/TitleBar.cs index 428bf8f9b0..1bf13111a9 100644 --- a/src/Avalonia.Controls/Chrome/TitleBar.cs +++ b/src/Avalonia.Controls/Chrome/TitleBar.cs @@ -35,8 +35,7 @@ namespace Avalonia.Controls.Chrome } } - if (!Design.IsDesignMode) - IsVisible = window.PlatformImpl?.NeedsManagedDecorations ?? false; + IsVisible = window.PlatformImpl?.NeedsManagedDecorations ?? false; } } @@ -48,7 +47,7 @@ namespace Avalonia.Controls.Chrome _captionButtons = e.NameScope.Get("PART_CaptionButtons"); - if (VisualRoot is Window window && !Design.IsDesignMode) + if (VisualRoot is Window window) { _captionButtons?.Attach(window); @@ -60,7 +59,7 @@ namespace Avalonia.Controls.Chrome { base.OnAttachedToVisualTree(e); - if (VisualRoot is Window window && !Design.IsDesignMode) + if (VisualRoot is Window window) { _disposables = new CompositeDisposable { From 07cce63b8bd4b009bd384e727caaab16d339bc4a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 10 Jul 2022 16:57:55 +0200 Subject: [PATCH 275/389] Ported TimePicker to ControlTheme. And refactored shared date/time picker components into a separate file. --- .../Controls/DatePicker.xaml | 207 +++----- .../Controls/DateTimePickerShared.xaml | 116 +++++ .../Controls/FluentControls.xaml | 3 +- .../Controls/TimePicker.xaml | 465 ++++++++++++------ 4 files changed, 501 insertions(+), 290 deletions(-) create mode 100644 src/Avalonia.Themes.Fluent/Controls/DateTimePickerShared.xaml diff --git a/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml index 0537feb60b..0441415128 100644 --- a/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml @@ -10,8 +10,21 @@ xmlns:sys="clr-namespace:System;assembly=netstandard" x:CompileBindings="True"> - - + + + + + + + + + + Error + + + + + @@ -27,105 +40,45 @@ 9,3,0,6 1 - - - - - - - - - - - - - - - - - - + - - - - + - - - + - - - - - - - - - - - - - - - - - - - - - - - + - + @@ -148,7 +101,9 @@ HorizontalAlignment="Stretch" VerticalAlignment="Top"/> - - diff --git a/src/Avalonia.Themes.Fluent/Controls/DateTimePickerShared.xaml b/src/Avalonia.Themes.Fluent/Controls/DateTimePickerShared.xaml new file mode 100644 index 0000000000..77271a149f --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Controls/DateTimePickerShared.xaml @@ -0,0 +1,116 @@ + + + + + + + Standard Item + Month Item + Button + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index 58c33bf26f..262007082f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -59,7 +59,9 @@ + + @@ -72,6 +74,5 @@ - diff --git a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml index dc09032a36..b5f177b985 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml @@ -1,163 +1,320 @@ - - - - - 40 - 1 - 1 - 0,0,0,4 - 40 - 41 - 242 - 456 - 0,3,0,6 - 0,3,0,6 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + Error + + + + + + + + + 40 + 1 + 1 + 0,0,0,4 + 40 + 41 + 242 + 456 + 0,3,0,6 + 0,3,0,6 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + - - - - - + + From 2dda677fc8099a484806be21574e1a5bbbf721b8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 10 Jul 2022 22:26:28 +0200 Subject: [PATCH 276/389] Ported SplitButton to ControlTheme. Not happy with the Tag hack but no better way to add additional states to a control currently. --- .../Controls/FluentControls.xaml | 2 +- .../Controls/SplitButton.xaml | 388 ++++++++---------- 2 files changed, 173 insertions(+), 217 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index 262007082f..bc2352d5d0 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -39,6 +39,7 @@ + @@ -72,7 +73,6 @@ - diff --git a/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml b/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml index fb0460d9a1..9a93b2625b 100644 --- a/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml @@ -1,36 +1,120 @@ - - - + + Hello - + + + Hello + + + Disabled + + + Hello + + + Hello + - - 32 - 32 - 1 - 32 + 32 + 32 + 1 + 32 - - - - + + + - + + + + + + + + + + + + + + + + + + + + @@ -40,211 +124,83 @@ - - - - - - - - - - - @@ -147,9 +155,6 @@ - diff --git a/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml index 0441415128..bc47f3892e 100644 --- a/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml @@ -280,10 +280,6 @@ - diff --git a/src/Avalonia.Themes.Fluent/Controls/DateTimePickerShared.xaml b/src/Avalonia.Themes.Fluent/Controls/DateTimePickerShared.xaml index 77271a149f..be664b375d 100644 --- a/src/Avalonia.Themes.Fluent/Controls/DateTimePickerShared.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/DateTimePickerShared.xaml @@ -15,8 +15,8 @@ Standard Item Month Item Button - - + + @@ -90,6 +90,7 @@ +