From 9d335c3a96b3ca1d6c9aedcd6a5c1408375d4e78 Mon Sep 17 00:00:00 2001 From: v-yadli Date: Sun, 13 Dec 2020 02:11:28 +0800 Subject: [PATCH 01/45] fix #4996 --- native/Avalonia.Native/src/OSX/window.mm | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 8419258fe9..9cb5fcbb58 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -1590,7 +1590,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent if(_parent != nullptr) { - _lastKeyHandled = _parent->BaseEvents->RawKeyEvent(type, timestamp, modifiers, key); + auto handled = _parent->BaseEvents->RawKeyEvent(type, timestamp, modifiers, key); + if (key != LeftCtrl && key != RightCtrl) { + _lastKeyHandled = handled; + } else { + _lastKeyHandled = false; + } } } From d2af5dbcac4427c22830d0bb6a2764befa180a72 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 29 May 2022 22:59:50 -0400 Subject: [PATCH 02/45] Implicitly map x:DataType to a DataType on a DataTemplate. --- .../AvaloniaXamlIlCompiler.cs | 8 +- .../Transformers/XDataTypeTransformer.cs | 79 +++++++++++++++++++ .../Xaml/DataTemplateTests.cs | 58 ++++++++++++++ .../Xaml/TreeDataTemplateTests.cs | 17 ++++ 4 files changed, 158 insertions(+), 4 deletions(-) create mode 100644 src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/XDataTypeTransformer.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..7514b0e12e 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -31,10 +31,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions // Before everything else Transformers.Insert(0, new XNameTransformer()); - Transformers.Insert(1, new IgnoredDirectivesTransformer()); - Transformers.Insert(2, _designTransformer = new AvaloniaXamlIlDesignPropertiesTransformer()); - Transformers.Insert(3, _bindingTransformer = new AvaloniaBindingExtensionTransformer()); - + Transformers.Insert(1, new XDataTypeTransformer()); + Transformers.Insert(2, new IgnoredDirectivesTransformer()); + Transformers.Insert(3, _designTransformer = new AvaloniaXamlIlDesignPropertiesTransformer()); + Transformers.Insert(4, _bindingTransformer = new AvaloniaBindingExtensionTransformer()); // Targeted InsertBefore( diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/XDataTypeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/XDataTypeTransformer.cs new file mode 100644 index 0000000000..7b90164974 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/XDataTypeTransformer.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +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 +{ + internal class XDataTypeTransformer : IXamlAstTransformer + { + private const string DataTypePropertyName = "DataType"; + + /// + /// Converts x:DataType directives to regular DataType assignments if property with Avalonia.Metadata.DataTypeAttribute exists. + /// + /// + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (node is XamlAstObjectNode on) + { + for (var c = 0; c < on.Children.Count; c++) + { + var ch = on.Children[c]; + if (ch is XamlAstXmlDirective { Namespace: XamlNamespaces.Xaml2006, Name: DataTypePropertyName } d) + { + if (on.Children.OfType() + .Any(p => ((XamlAstNamePropertyReference)p.Property)?.Name == DataTypePropertyName)) + { + // Break iteration if any DataType property was already set by user code. + break; + } + + var templateDataTypeAttribute = context.GetAvaloniaTypes().DataTypeAttribute; + + var clrType = on.Type switch + { + XamlAstClrTypeReference clrRef => clrRef.Type, + XamlAstXmlTypeReference xmlRef => TypeReferenceResolver.ResolveType(context, xmlRef.Name, + on.Type.IsMarkupExtension, on, strict: false).Type, + _ => null + }; + if (clrType is null) + { + break; + } + + // Technically it's possible to map "x:DataType" to a property with [DataType] attribute regardless of its name, + // but we go explicitly strict here and check the name as well. + var (declaringType, dataTypeProperty) = GetAllProperties(clrType) + .FirstOrDefault(t => t.property.Name == DataTypePropertyName && t.property.CustomAttributes + .Any(a => a.Type == templateDataTypeAttribute)); + + if (dataTypeProperty is not null) + { + on.Children[c] = new XamlAstXamlPropertyValueNode(d, + new XamlAstNamePropertyReference(d, + new XamlAstClrTypeReference(ch, declaringType, false), dataTypeProperty.Name, + on.Type), + d.Values); + } + } + } + } + + return node; + } + + private static IEnumerable<(IXamlType declaringType, IXamlProperty property)> GetAllProperties(IXamlType t) + { + foreach (var p in t.Properties) + yield return (t, p); + if(t.BaseType!=null) + foreach (var tuple in GetAllProperties(t.BaseType)) + yield return tuple; + } + } +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs index 53881467e7..f9e1ce3054 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs @@ -1,5 +1,7 @@ +using System.Linq; using Avalonia.Controls; using Avalonia.Controls.Presenters; +using Avalonia.Markup.Xaml.Templates; using Avalonia.UnitTests; using Xunit; @@ -89,6 +91,62 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } + [Fact] + public void XDataType_Should_Be_Assigned_To_Clr_Property() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var target = window.FindControl("target"); + var template = (DataTemplate)window.DataTemplates.First(); + + window.ApplyTemplate(); + target.ApplyTemplate(); + ((ContentPresenter)target.Presenter).UpdateChild(); + + Assert.Equal(typeof(string), template.DataType); + Assert.IsType(target.Presenter.Child); + } + } + + [Fact] + public void XDataType_Should_Be_Ignored_If_DataType_Already_Set() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var target = window.FindControl("target"); + + window.ApplyTemplate(); + target.ApplyTemplate(); + ((ContentPresenter)target.Presenter).UpdateChild(); + + Assert.IsType(target.Presenter.Child); + } + } + [Fact] public void Can_Set_DataContext_In_DataTemplate() { diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TreeDataTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TreeDataTemplateTests.cs index 3fdac49f31..d4ab473d67 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TreeDataTemplateTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TreeDataTemplateTests.cs @@ -21,5 +21,22 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.IsType(template.ItemsSource); } } + + [Fact] + public void XDataType_Should_Be_Assigned_To_Clr_Property() + { + using (UnitTestApplication.Start(TestServices.MockPlatformWrapper)) + { + var xaml = @" + + +"; + var templates = (DataTemplates)AvaloniaRuntimeXamlLoader.Load(xaml); + var template = (TreeDataTemplate)(templates.First()); + + Assert.Equal(typeof(string), template.DataType); + } + } } } From 1d9645f01fda85fcbc80e11090cd672b536a559b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 29 May 2022 23:38:20 -0400 Subject: [PATCH 03/45] Validate DataTemplates --- .../Templates/DataTemplates.cs | 17 +++++++++++++++ .../Templates/ITypedDataTemplate.cs | 10 +++++++++ .../Templates/DataTemplate.cs | 2 +- .../Templates/TreeDataTemplate.cs | 2 +- .../CompiledBindingExtensionTests.cs | 4 ++-- .../Xaml/ControlBindingTests.cs | 8 +++---- .../Xaml/DataTemplateTests.cs | 21 +++++++++++++++++++ .../Xaml/TreeDataTemplateTests.cs | 2 +- 8 files changed, 57 insertions(+), 9 deletions(-) create mode 100644 src/Avalonia.Controls/Templates/ITypedDataTemplate.cs diff --git a/src/Avalonia.Controls/Templates/DataTemplates.cs b/src/Avalonia.Controls/Templates/DataTemplates.cs index f203539536..d4eeda7908 100644 --- a/src/Avalonia.Controls/Templates/DataTemplates.cs +++ b/src/Avalonia.Controls/Templates/DataTemplates.cs @@ -1,3 +1,4 @@ +using System; using Avalonia.Collections; namespace Avalonia.Controls.Templates @@ -13,6 +14,22 @@ namespace Avalonia.Controls.Templates public DataTemplates() { ResetBehavior = ResetBehavior.Remove; + + Validate += ValidateDataTemplate; + } + + private static void ValidateDataTemplate(IDataTemplate template) + { + var valid = template switch + { + ITypedDataTemplate typed => typed.DataType is not null, + _ => true + }; + + if (!valid) + { + throw new InvalidOperationException("DataTemplate inside of DataTemplates must have a DataType set. Set DataType property or use ItemTemplate with single template instead."); + } } } } \ No newline at end of file diff --git a/src/Avalonia.Controls/Templates/ITypedDataTemplate.cs b/src/Avalonia.Controls/Templates/ITypedDataTemplate.cs new file mode 100644 index 0000000000..239dbd79f4 --- /dev/null +++ b/src/Avalonia.Controls/Templates/ITypedDataTemplate.cs @@ -0,0 +1,10 @@ +using System; +using Avalonia.Metadata; + +namespace Avalonia.Controls.Templates; + +public interface ITypedDataTemplate : IDataTemplate +{ + [DataType] + Type? DataType { get; } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs index d2b24979cc..4da6b1b791 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs @@ -5,7 +5,7 @@ using Avalonia.Metadata; namespace Avalonia.Markup.Xaml.Templates { - public class DataTemplate : IRecyclingDataTemplate + public class DataTemplate : IRecyclingDataTemplate, ITypedDataTemplate { [DataType] public Type DataType { get; set; } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs index 10061c3d48..04e8b0a9c0 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs @@ -9,7 +9,7 @@ using Avalonia.Metadata; namespace Avalonia.Markup.Xaml.Templates { - public class TreeDataTemplate : ITreeDataTemplate + public class TreeDataTemplate : ITreeDataTemplate, ITypedDataTemplate { [DataType] public Type DataType { get; set; } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 7e721fd7b2..555a05638b 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -413,11 +413,11 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests' x:DataType='local:TestDataContext'> - + - + "; var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs index 8188b212e1..affa292a7d 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs @@ -74,18 +74,18 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml - + - + - + - + "; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs index 53881467e7..abbcf6c5a8 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs @@ -1,3 +1,4 @@ +using System; using Avalonia.Controls; using Avalonia.Controls.Presenters; using Avalonia.UnitTests; @@ -132,5 +133,25 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Same(viewModel.Child.Child, canvas.DataContext); } } + + [Fact] + public void DataTemplates_Without_Type_Should_Throw() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + +"; + Assert.Throws(() => (Window)AvaloniaRuntimeXamlLoader.Load(xaml)); + } + } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TreeDataTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TreeDataTemplateTests.cs index 3fdac49f31..807b37517a 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TreeDataTemplateTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TreeDataTemplateTests.cs @@ -14,7 +14,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml { using (UnitTestApplication.Start(TestServices.MockPlatformWrapper)) { - var xaml = ""; + var xaml = ""; var templates = (DataTemplates)AvaloniaRuntimeXamlLoader.Load(xaml); var template = (TreeDataTemplate)(templates.First()); From 03e01a6e554227613ce5579cf24884e8a94ad629 Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Mon, 30 May 2022 13:04:46 +0300 Subject: [PATCH 04/45] initial working --- src/Avalonia.Controls/ComboBox.cs | 21 ++-- src/Avalonia.Controls/Control.cs | 7 +- .../Rendering/SceneGraph/SceneBuilderTests.cs | 33 +++++++ .../ComboBoxTests.cs | 99 +++++++++++++++++++ .../FlowDirectionTests.cs | 41 ++++++++ 5 files changed, 181 insertions(+), 20 deletions(-) create mode 100644 tests/Avalonia.Controls.UnitTests/FlowDirectionTests.cs diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index cbf9b35a05..1f46a3b292 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -184,23 +184,10 @@ namespace Avalonia.Controls this.UpdateSelectionBoxItem(SelectedItem); } - // Because the SelectedItem isn't connected to the visual tree public override void InvalidateMirrorTransform() { base.InvalidateMirrorTransform(); - - if (SelectedItem is Control selectedControl) - { - selectedControl.InvalidateMirrorTransform(); - - foreach (var visual in selectedControl.GetVisualDescendants()) - { - if (visual is Control childControl) - { - childControl.InvalidateMirrorTransform(); - } - } - } + UpdateSelectionBoxItem(SelectedItem); } /// @@ -365,6 +352,8 @@ namespace Avalonia.Controls { parent.GetObservable(IsVisibleProperty).Subscribe(IsVisibleChanged).DisposeWith(_subscriptionsOnOpen); } + + UpdateSelectionBoxItem(SelectedItem); } private void IsVisibleChanged(bool isVisible) @@ -420,8 +409,12 @@ namespace Avalonia.Controls { control.Measure(Size.Infinity); + var flowDirection = control.IsAttachedToVisualTree ? + (control.VisualParent as Control)!.FlowDirection : FlowDirection.LeftToRight; + SelectionBoxItem = new Rectangle { + FlowDirection = flowDirection, Width = control.DesiredSize.Width, Height = control.DesiredSize.Height, Fill = new VisualBrush diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index d6a5fa0727..16d4ef5c15 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -378,17 +378,12 @@ namespace Avalonia.Controls bool bypassFlowDirectionPolicies = BypassFlowDirectionPolicies; bool parentBypassFlowDirectionPolicies = false; - var parent = this.FindAncestorOfType(); + var parent = ((IVisual)this).VisualParent as Control; if (parent != null) { parentFlowDirection = parent.FlowDirection; parentBypassFlowDirectionPolicies = parent.BypassFlowDirectionPolicies; } - else if (Parent is Control logicalParent) - { - parentFlowDirection = logicalParent.FlowDirection; - parentBypassFlowDirectionPolicies = logicalParent.BypassFlowDirectionPolicies; - } bool thisShouldBeMirrored = flowDirection == FlowDirection.RightToLeft && !bypassFlowDirectionPolicies; bool parentShouldBeMirrored = parentFlowDirection == FlowDirection.RightToLeft && !parentBypassFlowDirectionPolicies; diff --git a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs index 01afe85b8b..5cc9f57c8e 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs @@ -349,6 +349,39 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph } } + [Fact] + public void MirrorTransform_For_Control_With_RenderTransform_Should_Be_Correct() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + Border border; + var tree = new TestRoot + { + Width = 400, + Height = 200, + Child = border = new Border + { + HorizontalAlignment = HorizontalAlignment.Left, + Background = Brushes.Red, + Width = 100, + RenderTransform = new ScaleTransform(0.5, 1), + FlowDirection = FlowDirection.RightToLeft + } + }; + + tree.Measure(Size.Infinity); + tree.Arrange(new Rect(tree.DesiredSize)); + + var scene = new Scene(tree); + var sceneBuilder = new SceneBuilder(); + sceneBuilder.UpdateAll(scene); + + var expectedTransform = new Matrix(-1, 0, 0, 1, 100, 0) * Matrix.CreateScale(0.5, 1) * Matrix.CreateTranslation(25, 0); + var borderNode = scene.FindNode(border); + Assert.Equal(expectedTransform, borderNode.Transform); + } + } + [Fact] public void Should_Update_Border_Background_Node() { diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index 98695fe88e..0f1925f628 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs @@ -336,5 +336,104 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(1, count); } } + + [Fact] + public void FlowDirection_Of_RectangleContent_Shuold_Be_LeftToRight() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + var items = new[] + { + new ComboBoxItem() + { + Content = new Control() + } + }; + var target = new ComboBox + { + Items = items, + Template = GetTemplate() + }; + + var root = new TestRoot(target); + target.ApplyTemplate(); + target.SelectedIndex = 0; + + var rectangle = target.GetValue(ComboBox.SelectionBoxItemProperty) as Rectangle; + + Assert.Equal(FlowDirection.LeftToRight, rectangle.FlowDirection); + } + } + + [Fact] + public void FlowDirection_Of_RectangleContent_Updated_After_Change_ComboBox() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var items = new[] + { + new ComboBoxItem() + { + Content = new Control() + } + }; + var target = new ComboBox + { + FlowDirection = FlowDirection.RightToLeft, + Items = items, + Template = GetTemplate() + }; + + var root = new TestRoot(target); + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + target.SelectedIndex = 0; + + var rectangle = target.GetValue(ComboBox.SelectionBoxItemProperty) as Rectangle; + + // need help here, the 'rectangle' isn't connected to visual tree for some reason + + Assert.True(rectangle.HasMirrorTransform); + + target.FlowDirection = FlowDirection.LeftToRight; + + Assert.False(rectangle.HasMirrorTransform); + } + } + + [Fact] + public void FlowDirection_Of_RectangleContent_Updated_After_Content_In_VisualTree() + { + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + Control content; + var items = new[] + { + new ComboBoxItem() + { + Content = content = new Control() + } + }; + var target = new ComboBox + { + FlowDirection = FlowDirection.RightToLeft, + Items = items, + Template = GetTemplate() + }; + + var root = new TestRoot(target); + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + target.SelectedIndex = 0; + + // need help here how to connect 'content' tio visual tree, or how to + + + var rectangle = target.GetValue(ComboBox.SelectionBoxItemProperty) as Rectangle; + + Assert.Equal(FlowDirection.RightToLeft, rectangle.FlowDirection); + } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/FlowDirectionTests.cs b/tests/Avalonia.Controls.UnitTests/FlowDirectionTests.cs new file mode 100644 index 0000000000..6739eff638 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/FlowDirectionTests.cs @@ -0,0 +1,41 @@ +using Avalonia.Media; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class FlowDirectionTests + { + [Fact] + public void HasMirrorTransform_Should_Be_True() + { + var target = new Control + { + FlowDirection = FlowDirection.RightToLeft, + }; + + Assert.True(target.HasMirrorTransform); + } + + [Fact] + public void HasMirrorTransform_Of_Children_Is_Updated_After_Change() + { + Control child; + var target = new Decorator + { + FlowDirection = FlowDirection.LeftToRight, + Child = child = new Control() + { + FlowDirection = FlowDirection.LeftToRight, + } + }; + + Assert.False(target.HasMirrorTransform); + Assert.False(child.HasMirrorTransform); + + target.FlowDirection = FlowDirection.RightToLeft; + + Assert.True(target.HasMirrorTransform); + Assert.True(child.HasMirrorTransform); + } + } +} From 05db047d2909b96b264ffded9e882bb62278ba25 Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Mon, 30 May 2022 13:09:20 +0300 Subject: [PATCH 05/45] typo --- tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index 0f1925f628..905ded193c 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs @@ -427,7 +427,7 @@ namespace Avalonia.Controls.UnitTests target.Presenter.ApplyTemplate(); target.SelectedIndex = 0; - // need help here how to connect 'content' tio visual tree, or how to + // need help here how to connect 'content' to visual tree, or how to open popup var rectangle = target.GetValue(ComboBox.SelectionBoxItemProperty) as Rectangle; From ee4c0f97e6e4000f01a46b591ed43afa4eba939b Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Mon, 30 May 2022 14:23:13 +0300 Subject: [PATCH 06/45] remove OnAttachedToVisualTree because bug --- src/Avalonia.Controls/ComboBox.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 1f46a3b292..8a6fb361da 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -178,16 +178,10 @@ namespace Avalonia.Controls ComboBoxItem.ContentTemplateProperty); } - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnAttachedToVisualTree(e); - this.UpdateSelectionBoxItem(SelectedItem); - } - public override void InvalidateMirrorTransform() { base.InvalidateMirrorTransform(); - UpdateSelectionBoxItem(SelectedItem); + UpdateSelectionBoxItem(SelectedItem); } /// From 7e6edb0f32b753d226aba58889d56c2875b6f282 Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Mon, 30 May 2022 23:42:05 +0300 Subject: [PATCH 07/45] fixes bugs --- src/Avalonia.Controls/ComboBox.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 8a6fb361da..a3f87f7695 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -403,8 +403,8 @@ namespace Avalonia.Controls { control.Measure(Size.Infinity); - var flowDirection = control.IsAttachedToVisualTree ? - (control.VisualParent as Control)!.FlowDirection : FlowDirection.LeftToRight; + var flowDirection = + (control.VisualParent as Control)?.FlowDirection ?? FlowDirection.LeftToRight; SelectionBoxItem = new Rectangle { From 496b978cdbb830a0eb44d2b11915ff3c615c492a Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 30 May 2022 18:36:14 -0400 Subject: [PATCH 08/45] Run xDataType after TypeReferenceResolver --- .../CompilerExtensions/AvaloniaXamlIlCompiler.cs | 10 ++++++---- .../Transformers/XDataTypeTransformer.cs | 8 +------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index 7514b0e12e..04a61e5f10 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -31,10 +31,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions // Before everything else Transformers.Insert(0, new XNameTransformer()); - Transformers.Insert(1, new XDataTypeTransformer()); - Transformers.Insert(2, new IgnoredDirectivesTransformer()); - Transformers.Insert(3, _designTransformer = new AvaloniaXamlIlDesignPropertiesTransformer()); - Transformers.Insert(4, _bindingTransformer = new AvaloniaBindingExtensionTransformer()); + Transformers.Insert(1, new IgnoredDirectivesTransformer()); + Transformers.Insert(2, _designTransformer = new AvaloniaXamlIlDesignPropertiesTransformer()); + Transformers.Insert(3, _bindingTransformer = new AvaloniaBindingExtensionTransformer()); // Targeted InsertBefore( @@ -57,6 +56,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions new AvaloniaXamlIlResolveByNameMarkupExtensionReplacer() ); + InsertAfter( + new XDataTypeTransformer()); + // After everything else InsertBefore( new AddNameScopeRegistration(), diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/XDataTypeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/XDataTypeTransformer.cs index 7b90164974..845dc5f831 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/XDataTypeTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/XDataTypeTransformer.cs @@ -34,13 +34,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers var templateDataTypeAttribute = context.GetAvaloniaTypes().DataTypeAttribute; - var clrType = on.Type switch - { - XamlAstClrTypeReference clrRef => clrRef.Type, - XamlAstXmlTypeReference xmlRef => TypeReferenceResolver.ResolveType(context, xmlRef.Name, - on.Type.IsMarkupExtension, on, strict: false).Type, - _ => null - }; + var clrType = (on.Type as XamlAstClrTypeReference)?.Type; if (clrType is null) { break; From bc2179b337e969838942fcfea868934536d9ef33 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 30 May 2022 18:49:21 -0400 Subject: [PATCH 09/45] Add XDataType_Should_Be_Ignored_If_DataType_Has_Non_Standard_Name test --- .../CompiledBindingExtensionTests.cs | 3 +- .../Xaml/DataTemplateTests.cs | 35 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 7e721fd7b2..a8b95d3aaa 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -17,6 +17,7 @@ using Avalonia.Markup.Xaml.Templates; using Avalonia.Media; using Avalonia.Metadata; using Avalonia.UnitTests; +using JetBrains.Annotations; using XamlX; using Xunit; @@ -1527,7 +1528,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions [TemplateContent] public object Content { get; set; } - public bool Match(object data) => FancyDataType.IsInstanceOfType(data); + public bool Match(object data) => FancyDataType?.IsInstanceOfType(data) ?? true; public IControl Build(object data) => TemplateContent.Load(Content)?.Control; } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs index f9e1ce3054..6e99d9e3a6 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs @@ -1,7 +1,11 @@ +using System; using System.Linq; using Avalonia.Controls; using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; using Avalonia.Markup.Xaml.Templates; +using Avalonia.Markup.Xaml.UnitTests.MarkupExtensions; +using Avalonia.Metadata; using Avalonia.UnitTests; using Xunit; @@ -147,6 +151,37 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } + [Fact] + public void XDataType_Should_Be_Ignored_If_DataType_Has_Non_Standard_Name() + { + // We don't want DataType to be mapped to FancyDataType, avoid possible confusion. + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var target = window.FindControl("target"); + + window.ApplyTemplate(); + target.ApplyTemplate(); + ((ContentPresenter)target.Presenter).UpdateChild(); + + var dataTemplate = (CustomDataTemplate)target.ContentTemplate; + Assert.Null(dataTemplate.FancyDataType); + } + } + [Fact] public void Can_Set_DataContext_In_DataTemplate() { From 402790ba86bfb6fc0f9de817cb3032b681175f38 Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Tue, 31 May 2022 07:53:59 +0300 Subject: [PATCH 10/45] improve UpdateFlowDirection --- src/Avalonia.Controls/ComboBox.cs | 28 +++++++++++++++---- .../ComboBoxTests.cs | 5 ++-- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index a3f87f7695..fc7feca7f1 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -178,10 +178,16 @@ namespace Avalonia.Controls ComboBoxItem.ContentTemplateProperty); } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + UpdateSelectionBoxItem(SelectedItem); + } + public override void InvalidateMirrorTransform() { base.InvalidateMirrorTransform(); - UpdateSelectionBoxItem(SelectedItem); + UpdateFlowDirection(); } /// @@ -347,7 +353,7 @@ namespace Avalonia.Controls parent.GetObservable(IsVisibleProperty).Subscribe(IsVisibleChanged).DisposeWith(_subscriptionsOnOpen); } - UpdateSelectionBoxItem(SelectedItem); + UpdateFlowDirection(); } private void IsVisibleChanged(bool isVisible) @@ -403,12 +409,8 @@ namespace Avalonia.Controls { control.Measure(Size.Infinity); - var flowDirection = - (control.VisualParent as Control)?.FlowDirection ?? FlowDirection.LeftToRight; - SelectionBoxItem = new Rectangle { - FlowDirection = flowDirection, Width = control.DesiredSize.Width, Height = control.DesiredSize.Height, Fill = new VisualBrush @@ -419,6 +421,8 @@ namespace Avalonia.Controls } }; } + + UpdateFlowDirection(); } else { @@ -426,6 +430,18 @@ namespace Avalonia.Controls } } + private void UpdateFlowDirection() + { + var rectangle = SelectionBoxItem as Rectangle; + if (rectangle != null) + { + var content = (rectangle.Fill as VisualBrush)!.Visual as Control; + var flowDirection = (((IVisual)content!).VisualParent as Control)?.FlowDirection ?? FlowDirection.LeftToRight; + + rectangle.FlowDirection = flowDirection; + } + } + private void SelectFocusedItem() { foreach (ItemContainerInfo dropdownItem in ItemContainerGenerator.Containers) diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index 905ded193c..0804de3174 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs @@ -351,6 +351,7 @@ namespace Avalonia.Controls.UnitTests }; var target = new ComboBox { + FlowDirection = FlowDirection.RightToLeft, Items = items, Template = GetTemplate() }; @@ -368,7 +369,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void FlowDirection_Of_RectangleContent_Updated_After_Change_ComboBox() { - using (UnitTestApplication.Start(TestServices.StyledWindow)) + using (UnitTestApplication.Start(TestServices.RealStyler)) { var items = new[] { @@ -385,10 +386,10 @@ namespace Avalonia.Controls.UnitTests }; var root = new TestRoot(target); - target.ApplyTemplate(); target.Presenter.ApplyTemplate(); target.SelectedIndex = 0; + ((ContentPresenter)target.Presenter).UpdateChild(); var rectangle = target.GetValue(ComboBox.SelectionBoxItemProperty) as Rectangle; From c77c500bcd373fa3715cce5d5a851eaab89da408 Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Tue, 31 May 2022 14:35:55 +0300 Subject: [PATCH 11/45] fixes tests --- src/Avalonia.Controls/ComboBox.cs | 6 +- .../ComboBoxTests.cs | 116 +++++++++--------- 2 files changed, 61 insertions(+), 61 deletions(-) diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index fc7feca7f1..5ba1195159 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -432,11 +432,11 @@ namespace Avalonia.Controls private void UpdateFlowDirection() { - var rectangle = SelectionBoxItem as Rectangle; - if (rectangle != null) + if (SelectionBoxItem is Rectangle rectangle) { var content = (rectangle.Fill as VisualBrush)!.Visual as Control; - var flowDirection = (((IVisual)content!).VisualParent as Control)?.FlowDirection ?? FlowDirection.LeftToRight; + var flowDirection = (((IVisual)content!).VisualParent as Control)?.FlowDirection ?? + FlowDirection.LeftToRight; rectangle.FlowDirection = flowDirection; } diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index 0804de3174..70b713d6d0 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs @@ -8,7 +8,7 @@ using Avalonia.Data; using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Media; -using Avalonia.Threading; +using Avalonia.VisualTree; using Avalonia.UnitTests; using Xunit; @@ -340,80 +340,77 @@ namespace Avalonia.Controls.UnitTests [Fact] public void FlowDirection_Of_RectangleContent_Shuold_Be_LeftToRight() { - using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + var items = new[] { - var items = new[] - { - new ComboBoxItem() - { - Content = new Control() - } - }; - var target = new ComboBox - { - FlowDirection = FlowDirection.RightToLeft, - Items = items, - Template = GetTemplate() - }; + new ComboBoxItem() + { + Content = new Control() + } + }; + var target = new ComboBox + { + FlowDirection = FlowDirection.RightToLeft, + Items = items, + Template = GetTemplate() + }; - var root = new TestRoot(target); - target.ApplyTemplate(); - target.SelectedIndex = 0; + var root = new TestRoot(target); + target.ApplyTemplate(); + target.SelectedIndex = 0; - var rectangle = target.GetValue(ComboBox.SelectionBoxItemProperty) as Rectangle; + var rectangle = target.GetValue(ComboBox.SelectionBoxItemProperty) as Rectangle; - Assert.Equal(FlowDirection.LeftToRight, rectangle.FlowDirection); - } + Assert.Equal(FlowDirection.LeftToRight, rectangle.FlowDirection); } [Fact] - public void FlowDirection_Of_RectangleContent_Updated_After_Change_ComboBox() + public void FlowDirection_Of_RectangleContent_Updated_After_InvalidateMirrorTransform() { - using (UnitTestApplication.Start(TestServices.RealStyler)) + var parentContent = new Decorator() { - var items = new[] - { - new ComboBoxItem() - { - Content = new Control() - } - }; - var target = new ComboBox + Child = new Control() + }; + var items = new[] + { + new ComboBoxItem() { - FlowDirection = FlowDirection.RightToLeft, - Items = items, - Template = GetTemplate() - }; - - var root = new TestRoot(target); - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - target.SelectedIndex = 0; - ((ContentPresenter)target.Presenter).UpdateChild(); - - var rectangle = target.GetValue(ComboBox.SelectionBoxItemProperty) as Rectangle; - - // need help here, the 'rectangle' isn't connected to visual tree for some reason + Content = parentContent.Child + } + }; + var target = new ComboBox + { + FlowDirection = FlowDirection.RightToLeft, + Items = items, + Template = GetTemplate() + }; - Assert.True(rectangle.HasMirrorTransform); + var root = new TestRoot(target); + target.ApplyTemplate(); + target.SelectedIndex = 0; - target.FlowDirection = FlowDirection.LeftToRight; + var rectangle = target.GetValue(ComboBox.SelectionBoxItemProperty) as Rectangle; + Assert.Equal(FlowDirection.LeftToRight, rectangle.FlowDirection); - Assert.False(rectangle.HasMirrorTransform); - } + parentContent.FlowDirection = FlowDirection.RightToLeft; + target.InvalidateMirrorTransform(); + + Assert.Equal(FlowDirection.RightToLeft, rectangle.FlowDirection); } [Fact] - public void FlowDirection_Of_RectangleContent_Updated_After_Content_In_VisualTree() + public void FlowDirection_Of_RectangleContent_Updated_After_OpenPopup() { - using (UnitTestApplication.Start(TestServices.RealFocus)) + using (UnitTestApplication.Start(TestServices.StyledWindow)) { - Control content; - var items = new[] + var parentContent = new Decorator() { + Child = new Control() + }; + var items = new[] + { new ComboBoxItem() { - Content = content = new Control() + Content = parentContent.Child } }; var target = new ComboBox @@ -425,14 +422,17 @@ namespace Avalonia.Controls.UnitTests var root = new TestRoot(target); target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); target.SelectedIndex = 0; - // need help here how to connect 'content' to visual tree, or how to open popup - - var rectangle = target.GetValue(ComboBox.SelectionBoxItemProperty) as Rectangle; + Assert.Equal(FlowDirection.LeftToRight, rectangle.FlowDirection); + parentContent.FlowDirection = FlowDirection.RightToLeft; + + var popup = target.GetVisualDescendants().OfType().First(); + popup.PlacementTarget = new Window(); + popup.Open(); + Assert.Equal(FlowDirection.RightToLeft, rectangle.FlowDirection); } } From 5bf28b671d42b5682930e27014d5897fe90bda73 Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Tue, 31 May 2022 20:42:16 +0300 Subject: [PATCH 12/45] some fixes --- tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index 70b713d6d0..aa32af7e51 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs @@ -379,7 +379,6 @@ namespace Avalonia.Controls.UnitTests }; var target = new ComboBox { - FlowDirection = FlowDirection.RightToLeft, Items = items, Template = GetTemplate() }; @@ -392,7 +391,7 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(FlowDirection.LeftToRight, rectangle.FlowDirection); parentContent.FlowDirection = FlowDirection.RightToLeft; - target.InvalidateMirrorTransform(); + target.FlowDirection = FlowDirection.RightToLeft; Assert.Equal(FlowDirection.RightToLeft, rectangle.FlowDirection); } From b6e66047f21cec797302c305b487b93881b7fa1c Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Wed, 1 Jun 2022 13:54:50 +0300 Subject: [PATCH 13/45] add fixes --- src/Avalonia.Controls/ComboBox.cs | 11 ++++++----- .../FlowDirectionTests.cs | 18 +++++++++++++++++- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 5ba1195159..05be5ad00d 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -434,11 +434,12 @@ namespace Avalonia.Controls { if (SelectionBoxItem is Rectangle rectangle) { - var content = (rectangle.Fill as VisualBrush)!.Visual as Control; - var flowDirection = (((IVisual)content!).VisualParent as Control)?.FlowDirection ?? - FlowDirection.LeftToRight; - - rectangle.FlowDirection = flowDirection; + if ((rectangle.Fill as VisualBrush)?.Visual is Control content) + { + var flowDirection = (((IVisual)content!).VisualParent as Control)?.FlowDirection ?? + FlowDirection.LeftToRight; + rectangle.FlowDirection = flowDirection; + } } } diff --git a/tests/Avalonia.Controls.UnitTests/FlowDirectionTests.cs b/tests/Avalonia.Controls.UnitTests/FlowDirectionTests.cs index 6739eff638..6c43103ecb 100644 --- a/tests/Avalonia.Controls.UnitTests/FlowDirectionTests.cs +++ b/tests/Avalonia.Controls.UnitTests/FlowDirectionTests.cs @@ -17,7 +17,23 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void HasMirrorTransform_Of_Children_Is_Updated_After_Change() + public void HasMirrorTransform_Of_LTR_Children_Should_Be_True_For_RTL_Parent() + { + Control child; + var target = new Decorator + { + FlowDirection = FlowDirection.RightToLeft, + Child = child = new Control() + }; + + child.FlowDirection = FlowDirection.LeftToRight; + + Assert.True(target.HasMirrorTransform); + Assert.True(child.HasMirrorTransform); + } + + [Fact] + public void HasMirrorTransform_Of_Children_Is_Updated_After_Parent_Changeed() { Control child; var target = new Decorator From 51d158dc1b1c3f27f6868407307a0ff1ce5aea14 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 8 Jun 2022 01:39:25 +0100 Subject: [PATCH 14/45] clear layout jobs instead of explicit layout call. --- src/Avalonia.Controls/TopLevel.cs | 7 ++++++- src/Avalonia.Controls/WindowBase.cs | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 57fb82485c..31217c70a9 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -13,6 +13,7 @@ using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Styling; +using Avalonia.Threading; using Avalonia.Utilities; using Avalonia.VisualTree; @@ -412,7 +413,11 @@ namespace Avalonia.Controls FrameSize = PlatformImpl!.FrameSize; Width = clientSize.Width; Height = clientSize.Height; - LayoutManager.ExecuteLayoutPass(); + + // Setting ClientSize and Width / Height above caused ExecuteLayoutPass to be queued. + // Instead of explicitly calling LayoutManager.ExecuteLayoutPass here, we clear the job queue. + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); + Renderer?.Resized(clientSize); } diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 12ba143c8a..5e827cc08f 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -8,6 +8,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Platform; +using Avalonia.Threading; using JetBrains.Annotations; namespace Avalonia.Controls @@ -219,7 +220,11 @@ namespace Avalonia.Controls { ClientSize = clientSize; FrameSize = PlatformImpl?.FrameSize; - LayoutManager.ExecuteLayoutPass(); + + // Setting ClientSize and Width / Height above caused ExecuteLayoutPass to be queued. + // Instead of explicitly calling LayoutManager.ExecuteLayoutPass here, we clear the job queue. + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); + Renderer?.Resized(clientSize); } From 33c61fdac5272f6f7912e9a39d8a10c4530ba8a5 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 8 Jun 2022 01:41:18 +0100 Subject: [PATCH 15/45] reduce work done on osx native side. --- .../Avalonia.Native/src/OSX/WindowBaseImpl.mm | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index 7f2bb128da..f133fa34f6 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -298,14 +298,15 @@ HRESULT WindowBaseImpl::Resize(double x, double y, AvnPlatformResizeReason reaso } @try { - lastSize = NSSize {x, y}; - - if (!_shown) { - BaseEvents->Resized(AvnSize{x, y}, reason); - } - else if(Window != nullptr) { - [Window setContentSize:lastSize]; - [Window invalidateShadow]; + if(x != lastSize.width || y != lastSize.height) { + lastSize = NSSize{x, y}; + + if (!_shown) { + BaseEvents->Resized(AvnSize{x, y}, reason); + } else if (Window != nullptr) { + [Window setContentSize:lastSize]; + [Window invalidateShadow]; + } } } @finally { From 96931e2203840f8e99679cffd2722e4958f44e0a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 8 Jun 2022 11:29:55 +0100 Subject: [PATCH 16/45] prevent excess layout passes in layout manager to catch other scenarios causing excessive layout passes. --- src/Avalonia.Base/Layout/LayoutManager.cs | 14 ++++++++++++-- src/Avalonia.Controls/TopLevel.cs | 6 +----- src/Avalonia.Controls/WindowBase.cs | 6 +----- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Base/Layout/LayoutManager.cs b/src/Avalonia.Base/Layout/LayoutManager.cs index fc988a8d6c..446f135c83 100644 --- a/src/Avalonia.Base/Layout/LayoutManager.cs +++ b/src/Avalonia.Base/Layout/LayoutManager.cs @@ -28,7 +28,7 @@ namespace Avalonia.Layout public LayoutManager(ILayoutRoot owner) { _owner = owner ?? throw new ArgumentNullException(nameof(owner)); - _executeLayoutPass = ExecuteLayoutPass; + _executeLayoutPass = ExecuteQueuedLayoutPass; } public virtual event EventHandler? LayoutUpdated; @@ -94,6 +94,16 @@ namespace Avalonia.Layout QueueLayoutPass(); } + private void ExecuteQueuedLayoutPass() + { + if (!_queued) + { + return; + } + + ExecuteLayoutPass(); + } + /// public virtual void ExecuteLayoutPass() { @@ -319,8 +329,8 @@ namespace Avalonia.Layout { if (!_queued && !_running) { - Dispatcher.UIThread.Post(_executeLayoutPass, DispatcherPriority.Layout); _queued = true; + Dispatcher.UIThread.Post(_executeLayoutPass, DispatcherPriority.Layout); } } diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 31217c70a9..0bc4adf1b5 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -413,11 +413,7 @@ namespace Avalonia.Controls FrameSize = PlatformImpl!.FrameSize; Width = clientSize.Width; Height = clientSize.Height; - - // Setting ClientSize and Width / Height above caused ExecuteLayoutPass to be queued. - // Instead of explicitly calling LayoutManager.ExecuteLayoutPass here, we clear the job queue. - Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); - + LayoutManager.ExecuteLayoutPass(); Renderer?.Resized(clientSize); } diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 5e827cc08f..224aeea9e9 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -220,11 +220,7 @@ namespace Avalonia.Controls { ClientSize = clientSize; FrameSize = PlatformImpl?.FrameSize; - - // Setting ClientSize and Width / Height above caused ExecuteLayoutPass to be queued. - // Instead of explicitly calling LayoutManager.ExecuteLayoutPass here, we clear the job queue. - Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); - + LayoutManager.ExecuteLayoutPass(); Renderer?.Resized(clientSize); } From 726ac748ed1136156e564dfec23a65b172e71a47 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 8 Jun 2022 11:30:50 +0100 Subject: [PATCH 17/45] restore changes. --- src/Avalonia.Controls/TopLevel.cs | 1 - src/Avalonia.Controls/WindowBase.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 0bc4adf1b5..57fb82485c 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -13,7 +13,6 @@ using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Styling; -using Avalonia.Threading; using Avalonia.Utilities; using Avalonia.VisualTree; diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 224aeea9e9..12ba143c8a 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -8,7 +8,6 @@ using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Platform; -using Avalonia.Threading; using JetBrains.Annotations; namespace Avalonia.Controls From 0b4ea2b1eb09115951546e4992657e8b396e9316 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 8 Jun 2022 11:47:40 +0100 Subject: [PATCH 18/45] add unit test. --- .../Layout/LayoutManagerTests.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs b/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs index f0e8e1cd11..a097d395d8 100644 --- a/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs +++ b/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Avalonia.Controls; using Avalonia.Layout; +using Avalonia.Threading; using Xunit; namespace Avalonia.Base.UnitTests.Layout @@ -421,5 +422,22 @@ namespace Avalonia.Base.UnitTests.Layout Assert.Equal(new Size(200, 200), control.Bounds.Size); Assert.Equal(new Size(200, 200), control.DesiredSize); } + + [Fact] + public void LayoutManager_Execute_Layout_Pass_Should_Clear_Queued_LayoutPasses() + { + var control = new LayoutTestControl(); + var root = new LayoutTestRoot { Child = control }; + + int layoutCount = 0; + root.LayoutUpdated += (sender, args) => layoutCount++; + + root.LayoutManager.InvalidateArrange(control); + root.LayoutManager.ExecuteInitialLayoutPass(); + + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); + + Assert.Equal(1, layoutCount); + } } } From 236d10bf64b03e52a93183f6adbb26fcdf09ac04 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 8 Jun 2022 11:48:44 +0100 Subject: [PATCH 19/45] discard unused params --- tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs b/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs index a097d395d8..37e07c244e 100644 --- a/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs +++ b/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs @@ -430,7 +430,7 @@ namespace Avalonia.Base.UnitTests.Layout var root = new LayoutTestRoot { Child = control }; int layoutCount = 0; - root.LayoutUpdated += (sender, args) => layoutCount++; + root.LayoutUpdated += (_, _) => layoutCount++; root.LayoutManager.InvalidateArrange(control); root.LayoutManager.ExecuteInitialLayoutPass(); From 7a533700d0bcb54be20ec448e5b7802ad6ac677b Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 9 Jun 2022 12:24:39 +0200 Subject: [PATCH 20/45] Implement GlyphRun.BuildGeometry --- samples/RenderDemo/Pages/GlyphRunPage.xaml | 12 +- samples/RenderDemo/Pages/GlyphRunPage.xaml.cs | 113 +++++++++++++----- src/Avalonia.Base/Media/GlyphRun.cs | 19 +++ src/Avalonia.Base/Media/PlatformGeometry.cs | 24 ++++ .../Platform/IPlatformRenderInterface.cs | 8 ++ .../Avalonia.Skia/PlatformRenderInterface.cs | 32 +++++ .../Avalonia.Direct2D1/Direct2D1Platform.cs | 28 +++++ .../Media/GlyphRunTests.cs | 66 ++++++++++ ...ould_Render_GlyphRun_Geometry.expected.png | Bin 0 -> 1476 bytes ...ould_Render_GlyphRun_Geometry.expected.png | Bin 0 -> 1170 bytes 10 files changed, 269 insertions(+), 33 deletions(-) create mode 100644 src/Avalonia.Base/Media/PlatformGeometry.cs create mode 100644 tests/Avalonia.RenderTests/Media/GlyphRunTests.cs create mode 100644 tests/TestFiles/Direct2D1/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png create mode 100644 tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png diff --git a/samples/RenderDemo/Pages/GlyphRunPage.xaml b/samples/RenderDemo/Pages/GlyphRunPage.xaml index c2914e8847..7db58e5286 100644 --- a/samples/RenderDemo/Pages/GlyphRunPage.xaml +++ b/samples/RenderDemo/Pages/GlyphRunPage.xaml @@ -2,13 +2,13 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:local="clr-namespace:RenderDemo.Pages" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="RenderDemo.Pages.GlyphRunPage"> - - - - + + + diff --git a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs index 7f85606957..674ed8e61f 100644 --- a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs +++ b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs @@ -9,14 +9,6 @@ namespace RenderDemo.Pages { public class GlyphRunPage : UserControl { - private Image _imageControl; - private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface; - private readonly Random _rand = new Random(); - private ushort[] _glyphIndices = new ushort[1]; - private char[] _characters = new char[1]; - private float _fontSize = 20; - private int _direction = 10; - public GlyphRunPage() { this.InitializeComponent(); @@ -25,19 +17,43 @@ namespace RenderDemo.Pages private void InitializeComponent() { AvaloniaXamlLoader.Load(this); + } + } + + public class GlyphRunControl : Control + { + private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface; + private readonly Random _rand = new Random(); + private ushort[] _glyphIndices = new ushort[1]; + private char[] _characters = new char[1]; + private float _fontSize = 20; + private int _direction = 10; - _imageControl = this.FindControl("imageControl"); - _imageControl.Source = new DrawingImage(); + private DispatcherTimer _timer; - DispatcherTimer.Run(() => + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + _timer = new DispatcherTimer + { + Interval = TimeSpan.FromSeconds(1) + }; + + _timer.Tick += (s,e) => { - UpdateGlyphRun(); + InvalidateVisual(); + }; + + _timer.Start(); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + _timer.Stop(); - return true; - }, TimeSpan.FromSeconds(1)); + _timer = null; } - private void UpdateGlyphRun() + public override void Render(DrawingContext context) { var c = (char)_rand.Next(65, 90); @@ -57,27 +73,70 @@ namespace RenderDemo.Pages _characters[0] = c; - var scale = (double)_fontSize / _glyphTypeface.DesignEmHeight; + var glyphRun = new GlyphRun(_glyphTypeface, _fontSize, _characters, _glyphIndices); - var drawingGroup = new DrawingGroup(); + context.DrawGlyphRun(Brushes.Black, glyphRun); + } + } - var glyphRunDrawing = new GlyphRunDrawing + public class GlyphRunGeometryControl : Control + { + private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface; + private readonly Random _rand = new Random(); + private ushort[] _glyphIndices = new ushort[1]; + private char[] _characters = new char[1]; + private float _fontSize = 20; + private int _direction = 10; + + private DispatcherTimer _timer; + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + _timer = new DispatcherTimer { - Foreground = Brushes.Black, - GlyphRun = new GlyphRun(_glyphTypeface, _fontSize, _characters, _glyphIndices) + Interval = TimeSpan.FromSeconds(1) }; - drawingGroup.Children.Add(glyphRunDrawing); - - var geometryDrawing = new GeometryDrawing + _timer.Tick += (s, e) => { - Pen = new Pen(Brushes.Black), - Geometry = new RectangleGeometry { Rect = new Rect(glyphRunDrawing.GlyphRun.Size) } + InvalidateVisual(); }; - drawingGroup.Children.Add(geometryDrawing); + _timer.Start(); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + _timer.Stop(); + + _timer = null; + } + + public override void Render(DrawingContext context) + { + var c = (char)_rand.Next(65, 90); + + if (_fontSize + _direction > 200) + { + _direction = -10; + } + + if (_fontSize + _direction < 20) + { + _direction = 10; + } + + _fontSize += _direction; + + _glyphIndices[0] = _glyphTypeface.GetGlyph(c); + + _characters[0] = c; + + var glyphRun = new GlyphRun(_glyphTypeface, _fontSize, _characters, _glyphIndices); + + var geometry = glyphRun.BuildGeometry(); - (_imageControl.Source as DrawingImage).Drawing = drawingGroup; + context.DrawGeometry(Brushes.Green, null, geometry); } } } diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index 22be8d8865..25c35a28e5 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -194,6 +194,25 @@ namespace Avalonia.Media } } + /// + /// Obtains geometry for the glyph run. + /// + /// The geometry returned contains the combined geometry of all glyphs in the glyph run. + public Geometry BuildGeometry() + { + var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService(); + + var geometryImpl = platformRenderInterface.BuildGlyphRunGeometry(this, out var scale); + + var geometry = new PlatformGeometry(geometryImpl); + + var transform = new MatrixTransform(Matrix.CreateTranslation(geometry.Bounds.Left, -geometry.Bounds.Top) * scale); + + geometry.Transform = transform; + + return geometry; + } + /// /// Retrieves the offset from the leading edge of the /// to the leading or trailing edge of a caret stop containing the specified character hit. diff --git a/src/Avalonia.Base/Media/PlatformGeometry.cs b/src/Avalonia.Base/Media/PlatformGeometry.cs new file mode 100644 index 0000000000..f25a14540f --- /dev/null +++ b/src/Avalonia.Base/Media/PlatformGeometry.cs @@ -0,0 +1,24 @@ +using Avalonia.Platform; + +namespace Avalonia.Media +{ + internal class PlatformGeometry : Geometry + { + private readonly IGeometryImpl _geometryImpl; + + public PlatformGeometry(IGeometryImpl geometryImpl) + { + _geometryImpl = geometryImpl; + } + + public override Geometry Clone() + { + return new PlatformGeometry(_geometryImpl); + } + + protected override IGeometryImpl? CreateDefiningGeometry() + { + return _geometryImpl; + } + } +} diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index 0eeefddf0b..bfa9e70fce 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -58,6 +58,14 @@ namespace Avalonia.Platform /// A combined geometry. IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2); + /// + /// Created a geometry implementation for the glyph run. + /// + /// The glyph run to build a geometry from. + /// The scaling of the produces geometry. + /// The geometry returned contains the combined geometry of all glyphs in the glyph run. + IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale); + /// /// Creates a renderer. /// diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index d3c3585cd0..dc1b7785e2 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -62,6 +62,38 @@ namespace Avalonia.Skia return new CombinedGeometryImpl(combineMode, g1, g2); } + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) + { + if (glyphRun.GlyphTypeface.PlatformImpl is not GlyphTypefaceImpl glyphTypeface) + { + throw new InvalidOperationException("PlatformImpl can't be null."); + } + + var fontRenderingEmSize = (float)glyphRun.FontRenderingEmSize; + var glyphs = glyphRun.GlyphIndices.ToArray(); + var skFont = new SKFont(glyphTypeface.Typeface, fontRenderingEmSize) + { + Size = fontRenderingEmSize, + Edging = SKFontEdging.Antialias, + Hinting = SKFontHinting.None, + LinearMetrics = true + }; + + SKPath path = null; + var matrix = SKMatrix.Identity; + + skFont.GetGlyphPaths(glyphs, (p, m) => + { + matrix = m; + + path = p; + }); + + scale = Matrix.CreateScale(matrix.ScaleX, matrix.ScaleY); + + return new StreamGeometryImpl(path); + } + /// public IBitmapImpl LoadBitmap(string fileName) { diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index d9e992bb80..04025f92e4 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -159,6 +159,34 @@ namespace Avalonia.Direct2D1 public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => new GeometryGroupImpl(fillRule, children); public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2); + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) + { + if (glyphRun.GlyphTypeface.PlatformImpl is not GlyphTypefaceImpl glyphTypeface) + { + throw new InvalidOperationException("PlatformImpl can't be null."); + } + + var pathGeometry = new SharpDX.Direct2D1.PathGeometry(Direct2D1Factory); + + using (var sink = pathGeometry.Open()) + { + var glyphs = new short[glyphRun.GlyphIndices.Count]; + + for (int i = 0; i < glyphRun.GlyphIndices.Count; i++) + { + glyphs[i] = (short)glyphRun.GlyphIndices[i]; + } + + glyphTypeface.FontFace.GetGlyphRunOutline((float)glyphRun.FontRenderingEmSize, glyphs, null, null, false, !glyphRun.IsLeftToRight, sink); + + sink.Close(); + } + + scale = Matrix.Identity; + + return new StreamGeometryImpl(pathGeometry); + } + /// public IBitmapImpl LoadBitmap(string fileName) { diff --git a/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs b/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs new file mode 100644 index 0000000000..734e4d5012 --- /dev/null +++ b/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.Shapes; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Xunit; + +#if AVALONIA_SKIA +namespace Avalonia.Skia.RenderTests +#else +namespace Avalonia.Direct2D1.RenderTests.Media +#endif +{ + public class GlyphRunTests : TestBase + { + public GlyphRunTests() + : base(@"Media\GlyphRun") + { + } + + [Fact] + public async Task Should_Render_GlyphRun_Geometry() + { + Decorator target = new Decorator + { + Padding = new Thickness(8), + Width = 80, + Height = 90, + Child = new GlyphRunGeometryControl() + }; + + await RenderToFile(target); + + CompareImages(); + } + + public class GlyphRunGeometryControl : Control + { + private readonly Geometry _geometry; + + public GlyphRunGeometryControl() + { + var glyphTypeface = Typeface.Default.GlyphTypeface; + + var glyphIndices = new[] { glyphTypeface.GetGlyph('A') }; + + var characters = new[] { 'A' }; + + var glyphRun = new GlyphRun(glyphTypeface, 100, characters, glyphIndices); + + _geometry = glyphRun.BuildGeometry(); + } + + protected override Size MeasureOverride(Size availableSize) + { + return _geometry.Bounds.Size; + } + + public override void Render(DrawingContext context) + { + context.DrawGeometry(Brushes.Green, null, _geometry); + } + } + } +} diff --git a/tests/TestFiles/Direct2D1/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png b/tests/TestFiles/Direct2D1/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..0b40e667a408be1bda12d7749d0496a85a20ce3e GIT binary patch literal 1476 zcmV;#1v~nQP)Px#1ZP1_K>z@;j|==^1pojAZb?KzRCr$PoWE;SK@i9H#KOYD!a^ZJiWDg zks?A0g%l|w1Po~5fAQ}A0!vF3B18yiks^hKMT8V7QYr+rNMVt}!oos0XKwb)C3!nD z@4fvMvLCqjb|K*1cW2+ue0QIwP$(1%g+iflCF$9FK2&BJzSVczbFpLk=CN%j<(j?c zL(e?3EskXn`i^ZUB`*Oha~-Z5)M3&7U2t-Xz$N%hQB$C2p4wJY@)A%teaFl~#6Ga? zWaJ}YWp2au5p|R_6FdkxMc^8IZctOrDhPkmwv&*LfbF6Q#Y5qfk%xelnTP8fb!s`X z_lqQ+#(4A4mE38fB~4b?dYTzuo^x0)~Qp=z8RWtwp~xcD9j-OSKxDtnl%i~ z7r5?I$H_Vj1SZ$fNiU!rJ-cRL4&i=G-CD4$!-0rSS^?2Gpz6cLA&B?Gwm;f?J&7ul z)Dg(nvqSUFwy^!ywp^gWpeqACl2$-IYW~7!i<-c|9KiJ}b!x$9Ah#F=A)SELXzJn= zcJUcRJNw*|qAO!h)yb@o)YGfLqdEnw0 zqR%}|2af4INyKR!UqE@KlokppLJ>3=?%Q@y;tEK{0q@_*&p3=ij8Lw$ix!2kBM>cn zwC-ITfH+*TJjok-cd-Ej7TjQnE1;bFNmKI_`EPNcf*TC+1f)8ZGY+tHlb?TMC{lTw zD?()wGXl{#;HK0Lh;m}vI^-x^vI7^4cmn#IzNetsP@ixU!A~&65s>Or&N%o5A8y~H z9*Wpq>Np5IjpGO?{|I;KCLyOi({=!Z5MDqs4pyn-K$H8q!N}#GodKgTMg&SH4vrxD z_H4U3gLB4fJ$Qn_y>Zon@B(%-4s^&7ELT7+2`eDgqYKn=qR;(Y$aa6QX&hESxgOnv zAMZZ4(F8dP=k0)cC7gho)22SA?d$5BIPg1t$4GX+W?&l;Dgtp&f-?><@oiqZNoeeT&nFnd z2q-_SH{r)(0_-1MWIwm}&iEIQPR`Fz$3d9sOGLK&{lDtfH?QHE#Y-?OL-Wvz-3h-D zNE_O`agYn1azfuz80*cXuN^dC1qQsc!QfZGfVNQpyqsY0Cm@}amyCnF@UWQp6Yz3H z-xxF)xc5^;{0K_iO!S?DILegOV9~h5E&%A^yw^*FOce@AQ zuGOB59|8L#@wg%#zvSIu)dxr4yn^cs>NpshC-B``IYZs2Fui&&y{5XAL_9>ZVbx(8 z`*;RQ#sU6__qG+E!RYvhbYKZjFmU_UPPz*yk6yQ3Hy`M>O{+AauE+ol%?;}_yj{c0jrVYEd8G?_^#Ve+s=ak zIbk~iFU#U_9$ij22LZyp$8v_|0_uzdoG$ze(y8#gn6!yNT=^g<6^kNK3qQG(tYNoEnddNz(|FUR6NSIWKxNIpHD%jA$@4 z6Y#Q6-!ZzJP!)xG0m(R!4k)H~UbN`aEb)Ciy@0x5S`erhdK?hyU`shen>jF$BlgVTpXTyZ)boGd%}JK za^v9c&-6VJNUzQJggrBmWlIo$_Lk-bLmvT|ad5y3{0EDM$0Op#I)y7ASU$ZnFr9s? zWTxFD+JMFeLoWeYtqO@Yww-BeFtis?F%Ep<=oS~2Tk@17xYSQfoM>j?w?bQv2mICU=fsXP!Ri|+q0X002J$^RqAKfNQMqEfH~r1tMAr$Xt#m_)b$iPSMo(qQOvjv|K)g`xyr+ zL5Ojsom40Jdz7Ks{lx}@g@A1O?hS4GQi2duv?p99U~l6t7_!fO`^Ebc3N>K>nI45MP#gEPjS9L5Oj=oi52h zqFoD?42E0*mFXKY`|N{OryUE>1=Ng#{U?~duPAkzCQMESDm5Ww^w$?{PdLOMd7Mlp klgVT Date: Thu, 9 Jun 2022 12:44:22 +0200 Subject: [PATCH 21/45] Skia - Correctly translate current position when multiple glyphs are added to a path --- .../Avalonia.Skia/PlatformRenderInterface.cs | 15 ++++++---- .../Media/GlyphRunTests.cs | 27 ++++++++++++++---- ...ould_Render_GlyphRun_Geometry.expected.png | Bin 1476 -> 5566 bytes ...ould_Render_GlyphRun_Geometry.expected.png | Bin 1170 -> 4372 bytes 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index dc1b7785e2..727677c82e 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -70,7 +70,6 @@ namespace Avalonia.Skia } var fontRenderingEmSize = (float)glyphRun.FontRenderingEmSize; - var glyphs = glyphRun.GlyphIndices.ToArray(); var skFont = new SKFont(glyphTypeface.Typeface, fontRenderingEmSize) { Size = fontRenderingEmSize, @@ -79,15 +78,19 @@ namespace Avalonia.Skia LinearMetrics = true }; - SKPath path = null; + SKPath path = new SKPath(); var matrix = SKMatrix.Identity; - skFont.GetGlyphPaths(glyphs, (p, m) => + var currentX = 0f; + + foreach (var glyph in glyphRun.GlyphIndices) { - matrix = m; + var p = skFont.GetGlyphPath(glyph); + + path.AddPath(p, currentX, 0); - path = p; - }); + currentX += p.Bounds.Right; + } scale = Matrix.CreateScale(matrix.ScaleX, matrix.ScaleY); diff --git a/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs b/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs index 734e4d5012..2de2dda29b 100644 --- a/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Avalonia.Controls; +using Avalonia.Controls.Documents; using Avalonia.Controls.Shapes; using Avalonia.Media; using Avalonia.Media.Imaging; @@ -25,9 +26,21 @@ namespace Avalonia.Direct2D1.RenderTests.Media Decorator target = new Decorator { Padding = new Thickness(8), - Width = 80, - Height = 90, - Child = new GlyphRunGeometryControl() + Width = 200, + Height = 100, + Child = new GlyphRunGeometryControl + { + [TextElement.ForegroundProperty] = new LinearGradientBrush + { + StartPoint = new RelativePoint(0, 0.5, RelativeUnit.Relative), + EndPoint = new RelativePoint(1, 0.5, RelativeUnit.Relative), + GradientStops = + { + new GradientStop { Color = Colors.Red, Offset = 0 }, + new GradientStop { Color = Colors.Blue, Offset = 1 } + } + } + } }; await RenderToFile(target); @@ -43,9 +56,9 @@ namespace Avalonia.Direct2D1.RenderTests.Media { var glyphTypeface = Typeface.Default.GlyphTypeface; - var glyphIndices = new[] { glyphTypeface.GetGlyph('A') }; + var glyphIndices = new[] { glyphTypeface.GetGlyph('A'), glyphTypeface.GetGlyph('B'), glyphTypeface.GetGlyph('C') }; - var characters = new[] { 'A' }; + var characters = new[] { 'A', 'B', 'C' }; var glyphRun = new GlyphRun(glyphTypeface, 100, characters, glyphIndices); @@ -59,7 +72,9 @@ namespace Avalonia.Direct2D1.RenderTests.Media public override void Render(DrawingContext context) { - context.DrawGeometry(Brushes.Green, null, _geometry); + var foreground = TextElement.GetForeground(this); + + context.DrawGeometry(foreground, null, _geometry); } } } diff --git a/tests/TestFiles/Direct2D1/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png b/tests/TestFiles/Direct2D1/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png index 0b40e667a408be1bda12d7749d0496a85a20ce3e..40ccc9fdcddbfd3af4673109e9558f2a30f958e3 100644 GIT binary patch literal 5566 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 delta 1465 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 klgVT Date: Thu, 9 Jun 2022 13:53:16 +0200 Subject: [PATCH 22/45] Fix mocks --- src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs | 7 +++++++ .../VisualTree/MockRenderInterface.cs | 5 +++++ tests/Avalonia.Benchmarks/NullRenderingPlatform.cs | 5 +++++ tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs | 7 +++++++ 4 files changed, 24 insertions(+) diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index addc248d58..6471b87bfd 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -114,6 +114,13 @@ namespace Avalonia.Headless return new HeadlessGlyphRunStub(); } + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) + { + scale = Matrix.Identity; + + return new HeadlessGeometryStub(new Rect(glyphRun.Size)); + } + class HeadlessGeometryStub : IGeometryImpl { public HeadlessGeometryStub(Rect bounds) diff --git a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs index add8f7fd73..183177495a 100644 --- a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs @@ -121,6 +121,11 @@ namespace Avalonia.Base.UnitTests.VisualTree throw new NotImplementedException(); } + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) + { + throw new NotImplementedException(); + } + class MockStreamGeometry : IStreamGeometryImpl { private MockStreamGeometryContext _impl = new MockStreamGeometryContext(); diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs index 5cbb3b2c49..51e75b6611 100644 --- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs +++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs @@ -117,6 +117,11 @@ namespace Avalonia.Benchmarks return new NullGlyphRun(); } + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) + { + throw new NotImplementedException(); + } + public bool SupportsIndividualRoundRects => true; public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul; diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index 376121c269..c385e1c3eb 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -122,6 +122,13 @@ namespace Avalonia.UnitTests return Mock.Of(); } + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) + { + scale = Matrix.Identity; + + return Mock.Of(); + } + public bool SupportsIndividualRoundRects { get; set; } public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul; From e48c984443e60664db79752bb5f058e6df2b0bf3 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 9 Jun 2022 14:23:02 +0200 Subject: [PATCH 23/45] Update tests --- .../Media/GlyphRunTests.cs | 2 +- ...ould_Render_GlyphRun_Geometry.expected.png | Bin 5566 -> 5326 bytes ...ould_Render_GlyphRun_Geometry.expected.png | Bin 4372 -> 4228 bytes 3 files changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs b/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs index 2de2dda29b..6a8884a33a 100644 --- a/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs @@ -54,7 +54,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media public GlyphRunGeometryControl() { - var glyphTypeface = Typeface.Default.GlyphTypeface; + var glyphTypeface = new Typeface(TestFontFamily).GlyphTypeface; var glyphIndices = new[] { glyphTypeface.GetGlyph('A'), glyphTypeface.GetGlyph('B'), glyphTypeface.GetGlyph('C') }; diff --git a/tests/TestFiles/Direct2D1/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png b/tests/TestFiles/Direct2D1/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png index 40ccc9fdcddbfd3af4673109e9558f2a30f958e3..7f1e0d29a1940266121b7f610f3763d3e08e4268 100644 GIT binary patch delta 5310 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 diff --git a/tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png b/tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png index 5bd64a6ac79e49c34a25802415074442dd911e9b..a8f3aa927782575db62b7a370e9656a504898c29 100644 GIT binary patch delta 4215 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 delta 4361 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 From 49ee55fca4a0ae93862cf67f296454579739fd0a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Jun 2022 17:04:36 +0200 Subject: [PATCH 24/45] Make GridSplitter scaling aware. When comparing sizes in `GridSplitter.MoveSplitter`, account for `UseLayoutRounding` and DPI scaling by using an epsilon that is the size of a device pixel. --- src/Avalonia.Controls/GridSplitter.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index 216e43e1f0..00ab856c31 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -60,6 +60,7 @@ namespace Avalonia.Controls private static readonly Cursor s_rowSplitterCursor = new Cursor(StandardCursorType.SizeNorthSouth); private ResizeData? _resizeData; + private double _scaling = 1; /// /// Indicates whether the Splitter resizes the Columns, Rows, or Both. @@ -348,6 +349,12 @@ namespace Avalonia.Controls } } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + _scaling = e.Root.RenderScaling; + } + protected override void OnPointerEnter(PointerEventArgs e) { base.OnPointerEnter(e); @@ -630,13 +637,17 @@ namespace Avalonia.Controls { double actualLength1 = GetActualLength(definition1); double actualLength2 = GetActualLength(definition2); + double pixelLength = 1 / _scaling; + double epsilon = pixelLength + LayoutHelper.LayoutEpsilon; // When splitting, Check to see if the total pixels spanned by the definitions - // is the same as before starting resize. If not cancel the drag. + // is the same as before starting resize. If not cancel the drag. We need to account for + // layout rounding here, so ignore differences of less than a device pixel to avoid problems + // that WPF has, such as https://stackoverflow.com/questions/28464843. if (_resizeData.SplitBehavior == SplitBehavior.Split && !MathUtilities.AreClose( actualLength1 + actualLength2, - _resizeData.OriginalDefinition1ActualLength + _resizeData.OriginalDefinition2ActualLength, LayoutHelper.LayoutEpsilon)) + _resizeData.OriginalDefinition1ActualLength + _resizeData.OriginalDefinition2ActualLength, epsilon)) { CancelResize(); From 553c4bc114470d81b6ac91584f1787e56f2c7c53 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Jun 2022 18:45:37 +0200 Subject: [PATCH 25/45] Read scaling when starting a drag operation. Previous implementation didn't update the scaling when moving between monitors. --- src/Avalonia.Controls/GridSplitter.cs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index 00ab856c31..784d33ed58 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -60,7 +60,6 @@ namespace Avalonia.Controls private static readonly Cursor s_rowSplitterCursor = new Cursor(StandardCursorType.SizeNorthSouth); private ResizeData? _resizeData; - private double _scaling = 1; /// /// Indicates whether the Splitter resizes the Columns, Rows, or Both. @@ -222,7 +221,8 @@ namespace Avalonia.Controls ShowsPreview = showsPreview, ResizeDirection = resizeDirection, SplitterLength = Math.Min(Bounds.Width, Bounds.Height), - ResizeBehavior = GetEffectiveResizeBehavior(resizeDirection) + ResizeBehavior = GetEffectiveResizeBehavior(resizeDirection), + Scaling = (VisualRoot as ILayoutRoot)?.LayoutScaling ?? 1, }; // Store the rows and columns to resize on drag events. @@ -349,12 +349,6 @@ namespace Avalonia.Controls } } - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnAttachedToVisualTree(e); - _scaling = e.Root.RenderScaling; - } - protected override void OnPointerEnter(PointerEventArgs e) { base.OnPointerEnter(e); @@ -637,7 +631,7 @@ namespace Avalonia.Controls { double actualLength1 = GetActualLength(definition1); double actualLength2 = GetActualLength(definition2); - double pixelLength = 1 / _scaling; + double pixelLength = 1 / _resizeData.Scaling; double epsilon = pixelLength + LayoutHelper.LayoutEpsilon; // When splitting, Check to see if the total pixels spanned by the definitions @@ -809,6 +803,9 @@ namespace Avalonia.Controls // The minimum of Width/Height of Splitter. Used to ensure splitter // isn't hidden by resizing a row/column smaller than the splitter. public double SplitterLength; + + // The current layout scaling factor. + public double Scaling; } } From de3720ce77d181cab10be89f2f022c335b666dff Mon Sep 17 00:00:00 2001 From: ahmedmohammedfawzy <42243982+ahmedmohammedfawzy@users.noreply.github.com> Date: Thu, 9 Jun 2022 20:53:26 +0200 Subject: [PATCH 26/45] Update TextBox.cs Added the option to determine whether to ignore changes while the user is inputting or not --- src/Avalonia.Controls/TextBox.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 7652b23162..52e5da95b3 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -53,6 +53,9 @@ namespace Avalonia.Controls public static readonly StyledProperty PasswordCharProperty = AvaloniaProperty.Register(nameof(PasswordChar)); + + public static readonly StyledProperty IgnoreChangesWhileEditingProperty = + AvaloniaProperty.Register(nameof(IgnoreChangesWhileEditing)); public static readonly StyledProperty SelectionBrushProperty = AvaloniaProperty.Register(nameof(SelectionBrush)); @@ -276,6 +279,12 @@ namespace Avalonia.Controls get => GetValue(IsReadOnlyProperty); set => SetValue(IsReadOnlyProperty, value); } + + public bool IgnoreChangesWhileEditing + { + get => GetValue(IgnoreChangesWhileEditingProperty); + set => SetValue(IgnoreChangesWhileEditingProperty, value); + } public char PasswordChar { @@ -1501,7 +1510,9 @@ namespace Avalonia.Controls { try { - _ignoreTextChanges = true; + if (IgnoreChangesWhileEditing == true) + _ignoreTextChanges = true; + SetAndRaise(TextProperty, ref _text, value); } finally From 0868442ec92aa4d427a8e750e9b1b0232c9662ce Mon Sep 17 00:00:00 2001 From: ahmedmohammedfawzy <42243982+ahmedmohammedfawzy@users.noreply.github.com> Date: Thu, 9 Jun 2022 21:04:12 +0200 Subject: [PATCH 27/45] Update TextBox.cs --- src/Avalonia.Controls/TextBox.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 52e5da95b3..77be6bd9ee 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -55,7 +55,7 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(PasswordChar)); public static readonly StyledProperty IgnoreChangesWhileEditingProperty = - AvaloniaProperty.Register(nameof(IgnoreChangesWhileEditing)); + AvaloniaProperty.Register(nameof(IgnoreChangesWhileEditing), true); public static readonly StyledProperty SelectionBrushProperty = AvaloniaProperty.Register(nameof(SelectionBrush)); From 5ebba8e68c960016c7054e80073ff952a0bc77c1 Mon Sep 17 00:00:00 2001 From: ahmedmohammedfawzy <42243982+ahmedmohammedfawzy@users.noreply.github.com> Date: Fri, 10 Jun 2022 08:55:08 +0200 Subject: [PATCH 28/45] Update TextBox.cs --- src/Avalonia.Controls/TextBox.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 77be6bd9ee..52e5da95b3 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -55,7 +55,7 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(PasswordChar)); public static readonly StyledProperty IgnoreChangesWhileEditingProperty = - AvaloniaProperty.Register(nameof(IgnoreChangesWhileEditing), true); + AvaloniaProperty.Register(nameof(IgnoreChangesWhileEditing)); public static readonly StyledProperty SelectionBrushProperty = AvaloniaProperty.Register(nameof(SelectionBrush)); From 71aba01b1002580696721c2f236dc2c904e9706c Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 10 Jun 2022 16:20:11 +0200 Subject: [PATCH 29/45] Implement FormattedText.BuildGeometry --- .../Pages/FormattedTextPage.axaml.cs | 4 + src/Avalonia.Base/GeometryCollection.cs | 37 -- src/Avalonia.Base/Media/DrawingCollection.cs | 18 + src/Avalonia.Base/Media/DrawingGroup.cs | 420 +++++++++++++++++- src/Avalonia.Base/Media/FormattedText.cs | 129 +++++- src/Avalonia.Base/Media/GeometryCollection.cs | 45 ++ src/Avalonia.Base/Media/GeometryDrawing.cs | 12 +- .../{ => Media}/GeometryGroup.cs | 48 +- src/Avalonia.Base/Media/GlyphRun.cs | 10 +- .../Platform/IPlatformRenderInterface.cs | 3 +- .../HeadlessPlatformRenderInterface.cs | 4 +- .../Avalonia.Skia/PlatformRenderInterface.cs | 26 +- .../Avalonia.Direct2D1/Direct2D1Platform.cs | 22 +- .../Media/GeometryGroupTests.cs | 18 + .../VisualTree/MockRenderInterface.cs | 2 +- .../NullRenderingPlatform.cs | 2 +- .../Media/GlyphRunTests.cs | 35 +- .../MockPlatformRenderInterface.cs | 4 +- ...ould_Render_GlyphRun_Geometry.expected.png | Bin 5326 -> 5369 bytes ...ould_Render_GlyphRun_Geometry.expected.png | Bin 4228 -> 4149 bytes 20 files changed, 728 insertions(+), 111 deletions(-) delete mode 100644 src/Avalonia.Base/GeometryCollection.cs create mode 100644 src/Avalonia.Base/Media/DrawingCollection.cs create mode 100644 src/Avalonia.Base/Media/GeometryCollection.cs rename src/Avalonia.Base/{ => Media}/GeometryGroup.cs (69%) diff --git a/samples/RenderDemo/Pages/FormattedTextPage.axaml.cs b/samples/RenderDemo/Pages/FormattedTextPage.axaml.cs index 25e29c67a9..97a9320c95 100644 --- a/samples/RenderDemo/Pages/FormattedTextPage.axaml.cs +++ b/samples/RenderDemo/Pages/FormattedTextPage.axaml.cs @@ -55,6 +55,10 @@ namespace RenderDemo.Pages formattedText.SetFontStyle(FontStyle.Italic, 28, 28); context.DrawText(formattedText, new Point(10, 0)); + + var geometry = formattedText.BuildGeometry(new Point(10 + formattedText.Width + 10, 0)); + + context.DrawGeometry(gradient, null, geometry); } } } diff --git a/src/Avalonia.Base/GeometryCollection.cs b/src/Avalonia.Base/GeometryCollection.cs deleted file mode 100644 index 0bd02d5438..0000000000 --- a/src/Avalonia.Base/GeometryCollection.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using Avalonia.Animation; - -#nullable enable - -namespace Avalonia.Media -{ - public class GeometryCollection : Animatable, IList, IReadOnlyList - { - private List _inner; - - public GeometryCollection() => _inner = new List(); - public GeometryCollection(IEnumerable collection) => _inner = new List(collection); - public GeometryCollection(int capacity) => _inner = new List(capacity); - - public Geometry this[int index] - { - get => _inner[index]; - set => _inner[index] = value; - } - - public int Count => _inner.Count; - public bool IsReadOnly => false; - - public void Add(Geometry item) => _inner.Add(item); - public void Clear() => _inner.Clear(); - public bool Contains(Geometry item) => _inner.Contains(item); - public void CopyTo(Geometry[] array, int arrayIndex) => _inner.CopyTo(array, arrayIndex); - public IEnumerator GetEnumerator() => _inner.GetEnumerator(); - public int IndexOf(Geometry item) => _inner.IndexOf(item); - public void Insert(int index, Geometry item) => _inner.Insert(index, item); - public bool Remove(Geometry item) => _inner.Remove(item); - public void RemoveAt(int index) => _inner.RemoveAt(index); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } -} diff --git a/src/Avalonia.Base/Media/DrawingCollection.cs b/src/Avalonia.Base/Media/DrawingCollection.cs new file mode 100644 index 0000000000..a76f7743cc --- /dev/null +++ b/src/Avalonia.Base/Media/DrawingCollection.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using Avalonia.Collections; + +namespace Avalonia.Media +{ + public sealed class DrawingCollection : AvaloniaList + { + public DrawingCollection() + { + ResetBehavior = ResetBehavior.Remove; + } + + public DrawingCollection(IEnumerable items) : base(items) + { + ResetBehavior = ResetBehavior.Remove; + } + } +} diff --git a/src/Avalonia.Base/Media/DrawingGroup.cs b/src/Avalonia.Base/Media/DrawingGroup.cs index eeb6318ebd..603bb1c1c1 100644 --- a/src/Avalonia.Base/Media/DrawingGroup.cs +++ b/src/Avalonia.Base/Media/DrawingGroup.cs @@ -1,6 +1,10 @@ -using Avalonia.Collections; +using System; +using System.Collections.Generic; +using Avalonia.Media.Imaging; using Avalonia.Metadata; using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; namespace Avalonia.Media { @@ -18,6 +22,14 @@ namespace Avalonia.Media public static readonly StyledProperty OpacityMaskProperty = AvaloniaProperty.Register(nameof(OpacityMask)); + public static readonly DirectProperty ChildrenProperty = + AvaloniaProperty.RegisterDirect( + nameof(Children), + o => o.Children, + (o, v) => o.Children = v); + + private DrawingCollection _children = new DrawingCollection(); + public double Opacity { get => GetValue(OpacityProperty); @@ -42,8 +54,23 @@ namespace Avalonia.Media set => SetValue(OpacityMaskProperty, value); } + /// + /// Gets or sets the collection that contains the child geometries. + /// [Content] - public AvaloniaList Children { get; } = new AvaloniaList(); + public DrawingCollection Children + { + get => _children; + set + { + SetAndRaise(ChildrenProperty, ref _children, value); + } + } + + public DrawingContext Open() + { + return new DrawingContext(new DrawingGroupDrawingContext(this)); + } public override void Draw(DrawingContext context) { @@ -75,5 +102,394 @@ namespace Avalonia.Media return rect; } + + private class DrawingGroupDrawingContext : IDrawingContextImpl + { + private readonly DrawingGroup _drawingGroup; + private readonly IPlatformRenderInterface _platformRenderInterface = AvaloniaLocator.Current.GetRequiredService(); + + private Matrix _transform; + + private bool _disposed; + + // Root drawing created by this DrawingContext. + // + // If there is only a single child of the root DrawingGroup, _rootDrawing + // will reference the single child, and the root _currentDrawingGroup + // value will be null. Otherwise, _rootDrawing will reference the + // root DrawingGroup, and be the same value as the root _currentDrawingGroup. + // + // Either way, _rootDrawing always references the root drawing. + protected Drawing? _rootDrawing; + + // Current DrawingGroup that new children are added to + protected DrawingGroup? _currentDrawingGroup; + + // Previous values of _currentDrawingGroup + private Stack? _previousDrawingGroupStack; + + public DrawingGroupDrawingContext(DrawingGroup drawingGroup) + { + _drawingGroup = drawingGroup; + } + + public Matrix Transform + { + get => _transform; + set + { + _transform = value; + PushTransform(new MatrixTransform(value)); + } + } + + public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) + { + if ((brush == null) && (pen == null)) + { + return; + } + + // Instantiate the geometry + var geometry = _platformRenderInterface.CreateEllipseGeometry(rect); + + // Add Drawing to the Drawing graph + AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry)); + } + + public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) + { + if (((brush == null) && (pen == null)) || (geometry == null)) + { + return; + } + + AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry)); + } + + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + { + if (foreground == null || glyphRun == null) + { + return; + } + + // Add a GlyphRunDrawing to the Drawing graph + GlyphRunDrawing glyphRunDrawing = new GlyphRunDrawing + { + Foreground = foreground, + GlyphRun = glyphRun, + }; + + // Add Drawing to the Drawing graph + AddDrawing(glyphRunDrawing); + } + + public void DrawLine(IPen pen, Point p1, Point p2) + { + if (pen == null) + { + return; + } + + // Instantiate the geometry + var geometry = _platformRenderInterface.CreateLineGeometry(p1, p2); + + // Add Drawing to the Drawing graph + AddNewGeometryDrawing(null, pen, new PlatformGeometry(geometry)); + } + + public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadows = default) + { + if ((brush == null) && (pen == null)) + { + return; + } + + // Instantiate the geometry + var geometry = _platformRenderInterface.CreateRectangleGeometry(rect.Rect); + + // Add Drawing to the Drawing graph + AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry)); + } + + public void Clear(Color color) + { + throw new NotImplementedException(); + } + + public IDrawingContextLayerImpl CreateLayer(Size size) + { + throw new NotImplementedException(); + } + + public void Custom(ICustomDrawOperation custom) + { + throw new NotImplementedException(); + } + + public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) + { + throw new NotImplementedException(); + } + + public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) + { + throw new NotImplementedException(); + } + + public void PopBitmapBlendMode() + { + throw new NotImplementedException(); + } + + public void PopClip() + { + throw new NotImplementedException(); + } + + public void PopGeometryClip() + { + throw new NotImplementedException(); + } + + public void PopOpacity() + { + throw new NotImplementedException(); + } + + public void PopOpacityMask() + { + throw new NotImplementedException(); + } + + public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) + { + throw new NotImplementedException(); + } + + public void PushClip(Rect clip) + { + throw new NotImplementedException(); + } + + public void PushClip(RoundedRect clip) + { + throw new NotImplementedException(); + } + + public void PushGeometryClip(IGeometryImpl clip) + { + throw new NotImplementedException(); + } + + public void PushOpacity(double opacity) + { + throw new NotImplementedException(); + } + + public void PushOpacityMask(IBrush mask, Rect bounds) + { + throw new NotImplementedException(); + } + + public void Dispose() + { + // Dispose may be called multiple times without throwing + // an exception. + if (!_disposed) + { + // Match any outstanding Push calls with a Pop + if (_previousDrawingGroupStack != null) + { + int stackCount = _previousDrawingGroupStack.Count; + for (int i = 0; i < stackCount; i++) + { + Pop(); + } + } + + // Call CloseCore with the root DrawingGroup's children + DrawingCollection rootChildren; + + if (_currentDrawingGroup != null) + { + // If we created a root DrawingGroup because multiple elements + // exist at the root level, provide it's Children collection + // directly. + rootChildren = _currentDrawingGroup.Children; + } + else + { + // Create a new DrawingCollection if we didn't create a + // root DrawingGroup because the root level only contained + // a single child. + // + // This collection is needed by DrawingGroup.Open because + // Open always replaces it's Children collection. It isn't + // strictly needed for Append, but always using a collection + // simplifies the TransactionalAppend implementation (i.e., + // a seperate implemention isn't needed for a single element) + rootChildren = new DrawingCollection(); + + // + // We may need to opt-out of inheritance through the new Freezable. + // This is controlled by this.CanBeInheritanceContext. + // + if (_rootDrawing != null) + { + rootChildren.Add(_rootDrawing); + } + } + + // Inform our derived classes that Close was called + _drawingGroup.Children = rootChildren; + + _disposed = true; + } + } + + /// + /// Pop + /// + private void Pop() + { + // Verify that Pop hasn't been called too many times + if ((_previousDrawingGroupStack == null) || (_previousDrawingGroupStack.Count == 0)) + { + throw new InvalidOperationException("DrawingGroupStack count missmatch."); + } + + // Restore the previous value of the current drawing group + _currentDrawingGroup = _previousDrawingGroupStack.Pop(); + } + + /// + /// PushTransform - + /// Push a Transform which will apply to all drawing operations until the corresponding + /// Pop. + /// + /// The Transform to push. + private void PushTransform(Transform transform) + { + // Instantiate a new drawing group and set it as the _currentDrawingGroup + var drawingGroup = PushNewDrawingGroup(); + + // Set the transform on the new DrawingGroup + drawingGroup.Transform = transform; + } + + /// + /// Creates a new DrawingGroup for a Push* call by setting the + /// _currentDrawingGroup to a newly instantiated DrawingGroup, + /// and saving the previous _currentDrawingGroup value on the + /// _previousDrawingGroupStack. + /// + private DrawingGroup PushNewDrawingGroup() + { + // Instantiate a new drawing group + DrawingGroup drawingGroup = new DrawingGroup(); + + // Add it to the drawing graph, like any other Drawing + AddDrawing(drawingGroup); + + // Lazily allocate the stack when it is needed because many uses + // of DrawingDrawingContext will have a depth of one. + if (null == _previousDrawingGroupStack) + { + _previousDrawingGroupStack = new Stack(2); + } + + // Save the previous _currentDrawingGroup value. + // + // If this is the first call, the value of _currentDrawingGroup + // will be null because AddDrawing doesn't create a _currentDrawingGroup + // for the first drawing. Having null on the stack is valid, and simply + // denotes that this new DrawingGroup is the first child in the root + // DrawingGroup. It is also possible for the first value on the stack + // to be non-null, which means that the root DrawingGroup has other + // children. + _previousDrawingGroupStack.Push(_currentDrawingGroup); + + // Set this drawing group as the current one so that subsequent drawing's + // are added as it's children until Pop is called. + _currentDrawingGroup = drawingGroup; + + return drawingGroup; + } + + /// + /// Contains the functionality common to GeometryDrawing operations of + /// instantiating the GeometryDrawing, setting it's Freezable state, + /// and Adding it to the Drawing Graph. + /// + private void AddNewGeometryDrawing(IBrush? brush, IPen? pen, Geometry? geometry) + { + if (geometry == null) + { + throw new ArgumentNullException(nameof(geometry)); + } + + // Instantiate the GeometryDrawing + GeometryDrawing geometryDrawing = new GeometryDrawing + { + // We may need to opt-out of inheritance through the new Freezable. + // This is controlled by this.CanBeInheritanceContext. + Brush = brush, + Pen = pen, + Geometry = geometry + }; + + // Add it to the drawing graph + AddDrawing(geometryDrawing); + } + + /// + /// Adds a new Drawing to the DrawingGraph. + /// + /// This method avoids creating a DrawingGroup for the common case + /// where only a single child exists in the root DrawingGroup. + /// + private void AddDrawing(Drawing newDrawing) + { + if (newDrawing == null) + { + throw new ArgumentNullException(nameof(newDrawing)); + } + + if (_rootDrawing == null) + { + // When a DrawingGroup is set, it should be made the root if + // a root drawing didnt exist. + Contract.Requires(_currentDrawingGroup == null); + + // If this is the first Drawing being added, avoid creating a DrawingGroup + // and set this drawing as the root drawing. This optimizes the common + // case where only a single child exists in the root DrawingGroup. + _rootDrawing = newDrawing; + } + else if (_currentDrawingGroup == null) + { + // When the second drawing is added at the root level, set a + // DrawingGroup as the root and add both drawings to it. + + // Instantiate the DrawingGroup + _currentDrawingGroup = new DrawingGroup(); + + // Add both Children + _currentDrawingGroup.Children.Add(_rootDrawing); + _currentDrawingGroup.Children.Add(newDrawing); + + // Set the new DrawingGroup as the current + _rootDrawing = _currentDrawingGroup; + } + else + { + // If there already is a current drawing group, then simply add + // the new drawing too it. + _currentDrawingGroup.Children.Add(newDrawing); + } + } + } } } diff --git a/src/Avalonia.Base/Media/FormattedText.cs b/src/Avalonia.Base/Media/FormattedText.cs index 7bdf59def0..5480336f84 100644 --- a/src/Avalonia.Base/Media/FormattedText.cs +++ b/src/Avalonia.Base/Media/FormattedText.cs @@ -1223,7 +1223,7 @@ namespace Avalonia.Media public double OverhangTrailing { get - { + { return BlackBoxMetrics.OverhangTrailing; } } @@ -1252,6 +1252,46 @@ namespace Avalonia.Media } } + /// + /// Obtains geometry for the text, including underlines and strikethroughs. + /// + /// The left top origin of the resulting geometry. + /// The geometry returned contains the combined geometry + /// of all of the glyphs, underlines and strikeThroughs that represent the formatted text. + /// Overlapping contours are merged by performing a Boolean union operation. + public Geometry? BuildGeometry(Point origin) + { + GeometryGroup? accumulatedGeometry = null; + var lineOrigin = origin; + + DrawingGroup drawing = new DrawingGroup(); + + using (var ctx = drawing.Open()) + { + using (var enumerator = GetEnumerator()) + { + while (enumerator.MoveNext()) + { + var currentLine = enumerator.Current; + + if (currentLine != null) + { + currentLine.Draw(ctx, lineOrigin); + + AdvanceLineOrigin(ref lineOrigin, currentLine); + } + } + } + } + + Transform? transform = new TranslateTransform(origin.X, origin.Y); + + // recursively go down the DrawingGroup to build up the geometry + CombineGeometryRecursive(drawing, ref transform, ref accumulatedGeometry); + + return accumulatedGeometry; + } + /// /// Draws the text object /// @@ -1284,6 +1324,93 @@ namespace Avalonia.Media } } + private void CombineGeometryRecursive(Drawing drawing, ref Transform? transform, ref GeometryGroup? accumulatedGeometry) + { + if (drawing is DrawingGroup group) + { + transform = group.Transform; + + if (group.Children is DrawingCollection children) + { + // recursively go down for DrawingGroup + foreach (var child in children) + { + CombineGeometryRecursive(child, ref transform, ref accumulatedGeometry); + } + } + } + else + { + if (drawing is GlyphRunDrawing glyphRunDrawing) + { + // process glyph run + var glyphRun = glyphRunDrawing.GlyphRun; + + if (glyphRun != null) + { + var glyphRunGeometry = glyphRun.BuildGeometry(); + + glyphRunGeometry.Transform = transform; + + if (accumulatedGeometry == null) + { + accumulatedGeometry = new GeometryGroup + { + FillRule = FillRule.NonZero + }; + } + + accumulatedGeometry.Children.Add(glyphRunGeometry); + } + } + else + { + if (drawing is GeometryDrawing geometryDrawing) + { + // process geometry (i.e. TextDecoration on the line) + var geometry = geometryDrawing.Geometry; + + if (geometry != null) + { + geometry.Transform = transform; + + if (geometry is LineGeometry lineGeometry) + { + // For TextDecoration drawn by DrawLine(), the geometry is a LineGeometry which has no + // bounding area. So this line won't show up. Work aroud it by increase the Bounding rect + // to be Pen's thickness + + var bounds = lineGeometry.Bounds; + + if (bounds.Height == 0) + { + bounds = bounds.WithHeight(geometryDrawing.Pen?.Thickness ?? 0); + } + else if (bounds.Width == 0) + { + bounds = bounds.WithWidth(geometryDrawing.Pen?.Thickness ?? 0); + } + + // convert the line geometry into a rectangle geometry + // we lost line cap info here + geometry = new RectangleGeometry(bounds); + } + + if (accumulatedGeometry == null) + { + accumulatedGeometry = new GeometryGroup + { + FillRule = FillRule.NonZero + }; + } + + accumulatedGeometry.Children.Add(geometry); + } + } + } + } + } + private CachedMetrics DrawAndCalculateMetrics(DrawingContext? drawingContext, Point drawingOffset, bool getBlackBoxMetrics) { var metrics = new CachedMetrics(); diff --git a/src/Avalonia.Base/Media/GeometryCollection.cs b/src/Avalonia.Base/Media/GeometryCollection.cs new file mode 100644 index 0000000000..2afa191dcf --- /dev/null +++ b/src/Avalonia.Base/Media/GeometryCollection.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using Avalonia.Collections; + +#nullable enable + +namespace Avalonia.Media +{ + public sealed class GeometryCollection : AvaloniaList + { + public GeometryCollection() + { + ResetBehavior = ResetBehavior.Remove; + + this.ForEachItem( + x => + { + Parent?.Invalidate(); + }, + x => + { + Parent?.Invalidate(); + }, + () => throw new NotSupportedException()); + } + + public GeometryCollection(IEnumerable items) : base(items) + { + ResetBehavior = ResetBehavior.Remove; + + this.ForEachItem( + x => + { + Parent?.Invalidate(); + }, + x => + { + Parent?.Invalidate(); + }, + () => throw new NotSupportedException()); + } + + public GeometryGroup? Parent { get; set; } + } +} diff --git a/src/Avalonia.Base/Media/GeometryDrawing.cs b/src/Avalonia.Base/Media/GeometryDrawing.cs index 08e62df2cc..7df7d25954 100644 --- a/src/Avalonia.Base/Media/GeometryDrawing.cs +++ b/src/Avalonia.Base/Media/GeometryDrawing.cs @@ -21,14 +21,14 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty BrushProperty = - AvaloniaProperty.Register(nameof(Brush), Brushes.Transparent); + public static readonly StyledProperty BrushProperty = + AvaloniaProperty.Register(nameof(Brush), Brushes.Transparent); /// /// Defines the property. /// - public static readonly StyledProperty PenProperty = - AvaloniaProperty.Register(nameof(Pen)); + public static readonly StyledProperty PenProperty = + AvaloniaProperty.Register(nameof(Pen)); /// /// Gets or sets the that describes the shape of this . @@ -43,7 +43,7 @@ namespace Avalonia.Media /// /// Gets or sets the used to fill the interior of the shape described by this . /// - public IBrush Brush + public IBrush? Brush { get => GetValue(BrushProperty); set => SetValue(BrushProperty, value); @@ -52,7 +52,7 @@ namespace Avalonia.Media /// /// Gets or sets the used to stroke this . /// - public IPen Pen + public IPen? Pen { get => GetValue(PenProperty); set => SetValue(PenProperty, value); diff --git a/src/Avalonia.Base/GeometryGroup.cs b/src/Avalonia.Base/Media/GeometryGroup.cs similarity index 69% rename from src/Avalonia.Base/GeometryGroup.cs rename to src/Avalonia.Base/Media/GeometryGroup.cs index b90c9c6d8a..b3b807c0a0 100644 --- a/src/Avalonia.Base/GeometryGroup.cs +++ b/src/Avalonia.Base/Media/GeometryGroup.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Avalonia.Metadata; +using Avalonia.Metadata; using Avalonia.Platform; #nullable enable @@ -13,8 +10,8 @@ namespace Avalonia.Media /// public class GeometryGroup : Geometry { - public static readonly DirectProperty ChildrenProperty = - AvaloniaProperty.RegisterDirect ( + public static readonly DirectProperty ChildrenProperty = + AvaloniaProperty.RegisterDirect ( nameof(Children), o => o.Children, (o, v) => o.Children = v); @@ -22,20 +19,28 @@ namespace Avalonia.Media public static readonly StyledProperty FillRuleProperty = AvaloniaProperty.Register(nameof(FillRule)); - private GeometryCollection? _children; - private bool _childrenSet; + private GeometryCollection _children = new GeometryCollection(); /// /// Gets or sets the collection that contains the child geometries. /// [Content] - public GeometryCollection? Children + public GeometryCollection Children { - get => _children ??= (!_childrenSet ? new GeometryCollection() : null); + get => _children; set { + if(_children is GeometryCollection) + { + _children.Parent = null; + } + + if (value is GeometryCollection) + { + value.Parent = this; + } + SetAndRaise(ChildrenProperty, ref _children, value); - _childrenSet = true; } } @@ -52,16 +57,21 @@ namespace Avalonia.Media public override Geometry Clone() { var result = new GeometryGroup { FillRule = FillRule, Transform = Transform }; - if (_children?.Count > 0) + + if (_children.Count > 0) + { result.Children = new GeometryCollection(_children); + } + return result; } protected override IGeometryImpl? CreateDefiningGeometry() { - if (_children?.Count > 0) + if (_children.Count > 0) { var factory = AvaloniaLocator.Current.GetRequiredService(); + return factory.CreateGeometryGroup(FillRule, _children); } @@ -72,10 +82,18 @@ namespace Avalonia.Media { base.OnPropertyChanged(change); - if (change.Property == ChildrenProperty || change.Property == FillRuleProperty) + switch (change.Property.Name) { - InvalidateGeometry(); + case nameof(FillRule): + case nameof(Children): + InvalidateGeometry(); + break; } } + + internal void Invalidate() + { + InvalidateGeometry(); + } } } diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index 25c35a28e5..6f1fa03990 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -202,15 +202,9 @@ namespace Avalonia.Media { var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService(); - var geometryImpl = platformRenderInterface.BuildGlyphRunGeometry(this, out var scale); + var geometryImpl = platformRenderInterface.BuildGlyphRunGeometry(this); - var geometry = new PlatformGeometry(geometryImpl); - - var transform = new MatrixTransform(Matrix.CreateTranslation(geometry.Bounds.Left, -geometry.Bounds.Top) * scale); - - geometry.Transform = transform; - - return geometry; + return new PlatformGeometry(geometryImpl); } /// diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index bfa9e70fce..e39a4e23df 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -62,9 +62,8 @@ namespace Avalonia.Platform /// Created a geometry implementation for the glyph run. /// /// The glyph run to build a geometry from. - /// The scaling of the produces geometry. /// The geometry returned contains the combined geometry of all glyphs in the glyph run. - IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale); + IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun); /// /// Creates a renderer. diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 6471b87bfd..059a9a4e8f 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -114,10 +114,8 @@ namespace Avalonia.Headless return new HeadlessGlyphRunStub(); } - public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) { - scale = Matrix.Identity; - return new HeadlessGeometryStub(new Rect(glyphRun.Size)); } diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 727677c82e..91fe4fc085 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -62,7 +62,7 @@ namespace Avalonia.Skia return new CombinedGeometryImpl(combineMode, g1, g2); } - public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) { if (glyphRun.GlyphTypeface.PlatformImpl is not GlyphTypefaceImpl glyphTypeface) { @@ -79,21 +79,29 @@ namespace Avalonia.Skia }; SKPath path = new SKPath(); - var matrix = SKMatrix.Identity; - var currentX = 0f; + var (currentX, currentY) = glyphRun.BaselineOrigin; - foreach (var glyph in glyphRun.GlyphIndices) + for (var i = 0; i < glyphRun.GlyphIndices.Count; i++) { - var p = skFont.GetGlyphPath(glyph); + var glyph = glyphRun.GlyphIndices[i]; + var glyphPath = skFont.GetGlyphPath(glyph); - path.AddPath(p, currentX, 0); + if (!glyphPath.IsEmpty) + { + path.AddPath(glyphPath, (float)currentX, (float)currentY); + } - currentX += p.Bounds.Right; + if (glyphRun.GlyphAdvances != null) + { + currentX += glyphRun.GlyphAdvances[i]; + } + else + { + currentX += glyphPath.Bounds.Right; + } } - scale = Matrix.CreateScale(matrix.ScaleX, matrix.ScaleY); - return new StreamGeometryImpl(path); } diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 04025f92e4..7f1af46e97 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -13,6 +13,7 @@ using Avalonia.Media.Imaging; using SharpDX.DirectWrite; using GlyphRun = Avalonia.Media.GlyphRun; using TextAlignment = Avalonia.Media.TextAlignment; +using SharpDX.Mathematics.Interop; namespace Avalonia { @@ -159,7 +160,7 @@ namespace Avalonia.Direct2D1 public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => new GeometryGroupImpl(fillRule, children); public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2); - public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) { if (glyphRun.GlyphTypeface.PlatformImpl is not GlyphTypefaceImpl glyphTypeface) { @@ -182,10 +183,23 @@ namespace Avalonia.Direct2D1 sink.Close(); } - scale = Matrix.Identity; + var (baselineOriginX, baselineOriginY) = glyphRun.BaselineOrigin; - return new StreamGeometryImpl(pathGeometry); - } + var transformedGeometry = new SharpDX.Direct2D1.TransformedGeometry( + Direct2D1Factory, + pathGeometry, + new RawMatrix3x2(1.0f, 0.0f, 0.0f, 1.0f, (float)baselineOriginX, (float)baselineOriginY)); + + return new TransformedGeometryWrapper(transformedGeometry); + } + + private class TransformedGeometryWrapper : GeometryImpl + { + public TransformedGeometryWrapper(SharpDX.Direct2D1.TransformedGeometry geometry) : base(geometry) + { + + } + } /// public IBitmapImpl LoadBitmap(string fileName) diff --git a/tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs b/tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs index 8f80238903..a6078d7a4a 100644 --- a/tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs @@ -22,5 +22,23 @@ namespace Avalonia.Visuals.UnitTests.Media Assert.Null(target.Children); } + + [Fact] + public void Childrend_Change_Should_Raise_Changed() + { + var target = new GeometryGroup(); + + var children = new GeometryCollection(); + + target.Children = children; + + var isCalled = false; + + target.Changed += (s, e) => isCalled = true; + + children.Add(new StreamGeometry()); + + Assert.True(isCalled); + } } } diff --git a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs index 183177495a..1f0b82b465 100644 --- a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs @@ -121,7 +121,7 @@ namespace Avalonia.Base.UnitTests.VisualTree throw new NotImplementedException(); } - public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) { throw new NotImplementedException(); } diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs index 51e75b6611..0193f5d772 100644 --- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs +++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs @@ -117,7 +117,7 @@ namespace Avalonia.Benchmarks return new NullGlyphRun(); } - public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) { throw new NotImplementedException(); } diff --git a/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs b/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs index 6a8884a33a..1b0193bfdb 100644 --- a/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs @@ -23,26 +23,28 @@ namespace Avalonia.Direct2D1.RenderTests.Media [Fact] public async Task Should_Render_GlyphRun_Geometry() { - Decorator target = new Decorator + var control = new GlyphRunGeometryControl { - Padding = new Thickness(8), - Width = 200, - Height = 100, - Child = new GlyphRunGeometryControl + [TextElement.ForegroundProperty] = new LinearGradientBrush { - [TextElement.ForegroundProperty] = new LinearGradientBrush - { - StartPoint = new RelativePoint(0, 0.5, RelativeUnit.Relative), - EndPoint = new RelativePoint(1, 0.5, RelativeUnit.Relative), - GradientStops = + StartPoint = new RelativePoint(0, 0.5, RelativeUnit.Relative), + EndPoint = new RelativePoint(1, 0.5, RelativeUnit.Relative), + GradientStops = { new GradientStop { Color = Colors.Red, Offset = 0 }, new GradientStop { Color = Colors.Blue, Offset = 1 } } - } } }; + Decorator target = new Decorator + { + Padding = new Thickness(8), + Width = 190, + Height = 120, + Child = control + }; + await RenderToFile(target); CompareImages(); @@ -50,8 +52,6 @@ namespace Avalonia.Direct2D1.RenderTests.Media public class GlyphRunGeometryControl : Control { - private readonly Geometry _geometry; - public GlyphRunGeometryControl() { var glyphTypeface = new Typeface(TestFontFamily).GlyphTypeface; @@ -62,19 +62,16 @@ namespace Avalonia.Direct2D1.RenderTests.Media var glyphRun = new GlyphRun(glyphTypeface, 100, characters, glyphIndices); - _geometry = glyphRun.BuildGeometry(); + Geometry = glyphRun.BuildGeometry(); } - protected override Size MeasureOverride(Size availableSize) - { - return _geometry.Bounds.Size; - } + public Geometry Geometry { get; } public override void Render(DrawingContext context) { var foreground = TextElement.GetForeground(this); - context.DrawGeometry(foreground, null, _geometry); + context.DrawGeometry(foreground, null, Geometry); } } } diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index c385e1c3eb..bf4ac9c1f6 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -122,10 +122,8 @@ namespace Avalonia.UnitTests return Mock.Of(); } - public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) { - scale = Matrix.Identity; - return Mock.Of(); } diff --git a/tests/TestFiles/Direct2D1/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png b/tests/TestFiles/Direct2D1/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png index 7f1e0d29a1940266121b7f610f3763d3e08e4268..004265112f4595314a36a794e51da7ac7b9b8212 100644 GIT binary patch delta 5295 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 BhC=`V delta 5285 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 diff --git a/tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png b/tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png index a8f3aa927782575db62b7a370e9656a504898c29..407b67b8a0d49afd3cd0f616c45c01d9fd480222 100644 GIT binary patch delta 4087 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 p+z}0N_>)l(8wD-V?_P@>4QsYq6k$m669 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 JPDHLkV1m^t9DM)) From d64b8582738f2fac85c3cc86d2660d2d8f2a5428 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 10 Jun 2022 16:30:24 +0200 Subject: [PATCH 30/45] Simplify GeometryGroup.Children setter --- src/Avalonia.Base/Media/GeometryGroup.cs | 25 +++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Base/Media/GeometryGroup.cs b/src/Avalonia.Base/Media/GeometryGroup.cs index b3b807c0a0..dbd1b9c97c 100644 --- a/src/Avalonia.Base/Media/GeometryGroup.cs +++ b/src/Avalonia.Base/Media/GeometryGroup.cs @@ -14,7 +14,7 @@ namespace Avalonia.Media AvaloniaProperty.RegisterDirect ( nameof(Children), o => o.Children, - (o, v) => o.Children = v); + SetChildren); public static readonly StyledProperty FillRuleProperty = AvaloniaProperty.Register(nameof(FillRule)); @@ -29,18 +29,12 @@ namespace Avalonia.Media { get => _children; set - { - if(_children is GeometryCollection) - { - _children.Parent = null; - } - - if (value is GeometryCollection) - { - value.Parent = this; - } + { + _children.Parent = null; SetAndRaise(ChildrenProperty, ref _children, value); + + _children.Parent = this; } } @@ -66,6 +60,15 @@ namespace Avalonia.Media return result; } + private static void SetChildren(GeometryGroup geometryGroup, GeometryCollection children) + { + geometryGroup.Children.Parent = null; + + children.Parent = geometryGroup; + + geometryGroup.Children = children; + } + protected override IGeometryImpl? CreateDefiningGeometry() { if (_children.Count > 0) From a9b2dec6b643cb57ad54bcbe4a2dd74492db197c Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 10 Jun 2022 16:31:43 +0200 Subject: [PATCH 31/45] Remove redudant code --- src/Avalonia.Base/Media/GeometryGroup.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Avalonia.Base/Media/GeometryGroup.cs b/src/Avalonia.Base/Media/GeometryGroup.cs index dbd1b9c97c..cb47e61f11 100644 --- a/src/Avalonia.Base/Media/GeometryGroup.cs +++ b/src/Avalonia.Base/Media/GeometryGroup.cs @@ -29,12 +29,8 @@ namespace Avalonia.Media { get => _children; set - { - _children.Parent = null; - + { SetAndRaise(ChildrenProperty, ref _children, value); - - _children.Parent = this; } } From 9cfac4dd3fe2ed8b53cc785f846fe907ca90c57b Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 10 Jun 2022 16:46:59 +0200 Subject: [PATCH 32/45] FIx GeometryGroup.Children parent handling --- src/Avalonia.Base/Media/GeometryGroup.cs | 23 ++++++++++++------- .../Media/GeometryGroupTests.cs | 2 +- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Base/Media/GeometryGroup.cs b/src/Avalonia.Base/Media/GeometryGroup.cs index cb47e61f11..0326e606f4 100644 --- a/src/Avalonia.Base/Media/GeometryGroup.cs +++ b/src/Avalonia.Base/Media/GeometryGroup.cs @@ -14,12 +14,20 @@ namespace Avalonia.Media AvaloniaProperty.RegisterDirect ( nameof(Children), o => o.Children, - SetChildren); + (o, v)=> o.Children = v); public static readonly StyledProperty FillRuleProperty = AvaloniaProperty.Register(nameof(FillRule)); - private GeometryCollection _children = new GeometryCollection(); + private GeometryCollection _children; + + public GeometryGroup() + { + _children = new GeometryCollection + { + Parent = this + }; + } /// /// Gets or sets the collection that contains the child geometries. @@ -30,7 +38,8 @@ namespace Avalonia.Media get => _children; set { - SetAndRaise(ChildrenProperty, ref _children, value); + OnChildrenChanged(_children, value); + SetAndRaise(ChildrenProperty, ref _children, value); } } @@ -56,13 +65,11 @@ namespace Avalonia.Media return result; } - private static void SetChildren(GeometryGroup geometryGroup, GeometryCollection children) + protected void OnChildrenChanged(GeometryCollection oldChildren, GeometryCollection newChildren) { - geometryGroup.Children.Parent = null; - - children.Parent = geometryGroup; + oldChildren.Parent = null; - geometryGroup.Children = children; + newChildren.Parent = this; } protected override IGeometryImpl? CreateDefiningGeometry() diff --git a/tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs b/tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs index a6078d7a4a..91183cee4b 100644 --- a/tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs @@ -24,7 +24,7 @@ namespace Avalonia.Visuals.UnitTests.Media } [Fact] - public void Childrend_Change_Should_Raise_Changed() + public void Children_Change_Should_Raise_Changed() { var target = new GeometryGroup(); From 3dfad3bc79fcdceb680aabfa363ac528ef90fcef Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 10 Jun 2022 17:02:27 +0200 Subject: [PATCH 33/45] Remove redudant test --- .../Media/GeometryGroupTests.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs b/tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs index 91183cee4b..fb4c35a1a8 100644 --- a/tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs @@ -13,16 +13,6 @@ namespace Avalonia.Visuals.UnitTests.Media Assert.NotNull(target.Children); } - [Fact] - public void Children_Can_Be_Set_To_Null() - { - var target = new GeometryGroup(); - - target.Children = null; - - Assert.Null(target.Children); - } - [Fact] public void Children_Change_Should_Raise_Changed() { From 32e2043ec0f37327eb31c3db42dea9ca7ed661a1 Mon Sep 17 00:00:00 2001 From: Ahmed Fawzy Date: Fri, 10 Jun 2022 18:23:24 +0200 Subject: [PATCH 34/45] removed the _ignoreTextChanges field from the TextBox.cs --- src/Avalonia.Controls/TextBox.cs | 81 ++++++++++---------------------- 1 file changed, 24 insertions(+), 57 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 52e5da95b3..9531f719b9 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -54,9 +54,6 @@ namespace Avalonia.Controls public static readonly StyledProperty PasswordCharProperty = AvaloniaProperty.Register(nameof(PasswordChar)); - public static readonly StyledProperty IgnoreChangesWhileEditingProperty = - AvaloniaProperty.Register(nameof(IgnoreChangesWhileEditing)); - public static readonly StyledProperty SelectionBrushProperty = AvaloniaProperty.Register(nameof(SelectionBrush)); @@ -199,7 +196,6 @@ namespace Avalonia.Controls private TextBoxTextInputMethodClient _imClient = new TextBoxTextInputMethodClient(); private UndoRedoHelper _undoRedoHelper; private bool _isUndoingRedoing; - private bool _ignoreTextChanges; private bool _canCut; private bool _canCopy; private bool _canPaste; @@ -280,12 +276,6 @@ namespace Avalonia.Controls set => SetValue(IsReadOnlyProperty, value); } - public bool IgnoreChangesWhileEditing - { - get => GetValue(IgnoreChangesWhileEditingProperty); - set => SetValue(IgnoreChangesWhileEditingProperty, value); - } - public char PasswordChar { get => GetValue(PasswordCharProperty); @@ -377,21 +367,17 @@ namespace Avalonia.Controls get => _text; set { - if (!_ignoreTextChanges) - { - var caretIndex = CaretIndex; - var selectionStart = SelectionStart; - var selectionEnd = SelectionEnd; + var caretIndex = CaretIndex; + var selectionStart = SelectionStart; + var selectionEnd = SelectionEnd; - CaretIndex = CoerceCaretIndex(caretIndex, value); - SelectionStart = CoerceCaretIndex(selectionStart, value); - SelectionEnd = CoerceCaretIndex(selectionEnd, value); - - if (SetAndRaise(TextProperty, ref _text, value) && IsUndoEnabled && !_isUndoingRedoing) - { - _undoRedoHelper.Clear(); - SnapshotUndoRedo(); // so we always have an initial state - } + CaretIndex = CoerceCaretIndex(caretIndex, value); + SelectionStart = CoerceCaretIndex(selectionStart, value); + SelectionEnd = CoerceCaretIndex(selectionEnd, value); + if (SetAndRaise(TextProperty, ref _text, value) && IsUndoEnabled && !_isUndoingRedoing) + { + _undoRedoHelper.Clear(); + SnapshotUndoRedo(); // so we always have an initial state } } } @@ -745,32 +731,23 @@ namespace Avalonia.Controls { var oldText = _text; - _ignoreTextChanges = true; - - try - { - DeleteSelection(false); - var caretIndex = CaretIndex; - text = Text ?? string.Empty; - SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex)); - ClearSelection(); - - if (IsUndoEnabled) - { - _undoRedoHelper.DiscardRedo(); - } - - if (_text != oldText) - { - RaisePropertyChanged(TextProperty, oldText, _text); - } + DeleteSelection(false); + var caretIndex = CaretIndex; + text = Text ?? string.Empty; + SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex)); + ClearSelection(); - CaretIndex = caretIndex + input.Length; + if (IsUndoEnabled) + { + _undoRedoHelper.DiscardRedo(); } - finally + + if (_text != oldText) { - _ignoreTextChanges = false; + RaisePropertyChanged(TextProperty, oldText, _text); } + + CaretIndex = caretIndex + input.Length; } } @@ -1508,17 +1485,7 @@ namespace Avalonia.Controls { if (raiseTextChanged) { - try - { - if (IgnoreChangesWhileEditing == true) - _ignoreTextChanges = true; - - SetAndRaise(TextProperty, ref _text, value); - } - finally - { - _ignoreTextChanges = false; - } + SetAndRaise(TextProperty, ref _text, value); } else { From 45d726e0c9ddac4e4a7c821e7589d51f6947ce7f Mon Sep 17 00:00:00 2001 From: Mario Uhlmann Date: Fri, 10 Jun 2022 19:22:50 +0200 Subject: [PATCH 35/45] Style improvements - primary OnCollectionChanged refactored (removed unnecessary cast and null checks in loops) --- src/Avalonia.Base/Styling/Styles.cs | 119 +++++++++++++++------------- 1 file changed, 62 insertions(+), 57 deletions(-) diff --git a/src/Avalonia.Base/Styling/Styles.cs b/src/Avalonia.Base/Styling/Styles.cs index 7c0bc4ad7f..903db5ffc7 100644 --- a/src/Avalonia.Base/Styling/Styles.cs +++ b/src/Avalonia.Base/Styling/Styles.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; +using System.Linq; using Avalonia.Collections; using Avalonia.Controls; @@ -17,7 +18,7 @@ namespace Avalonia.Styling IStyle, IResourceProvider { - private readonly AvaloniaList _styles = new AvaloniaList(); + private readonly AvaloniaList _styles = new(); private IResourceHost? _owner; private IResourceDictionary? _resources; private StyleCache? _cache; @@ -62,16 +63,18 @@ namespace Avalonia.Styling { value = value ?? throw new ArgumentNullException(nameof(Resources)); - if (Owner is object) + var currentOwner = Owner; + + if (currentOwner is not null) { - _resources?.RemoveOwner(Owner); + _resources?.RemoveOwner(currentOwner); } _resources = value; - if (Owner is object) + if (currentOwner is not null) { - _resources.AddOwner(Owner); + _resources.AddOwner(currentOwner); } } } @@ -89,7 +92,7 @@ namespace Avalonia.Styling foreach (var i in this) { - if (i is IResourceProvider p && p.HasResources) + if (i is IResourceProvider { HasResources: true }) { return true; } @@ -188,9 +191,9 @@ namespace Avalonia.Styling /// void IResourceProvider.AddOwner(IResourceHost owner) { - owner = owner ?? throw new ArgumentNullException(nameof(owner)); + ArgumentNullException.ThrowIfNull(owner); - if (Owner != null) + if (Owner is not null) { throw new InvalidOperationException("The Styles already has a owner."); } @@ -210,7 +213,7 @@ namespace Avalonia.Styling /// void IResourceProvider.RemoveOwner(IResourceHost owner) { - owner = owner ?? throw new ArgumentNullException(nameof(owner)); + ArgumentNullException.ThrowIfNull(owner); if (Owner == owner) { @@ -227,70 +230,72 @@ namespace Avalonia.Styling } } - private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + private static IReadOnlyList ToReadOnlyList(ICollection list) { - static IReadOnlyList ToReadOnlyList(IList list) + if (list is IReadOnlyList readOnlyList) { - if (list is IReadOnlyList) - { - return (IReadOnlyList)list; - } - else - { - var result = new T[list.Count]; - list.CopyTo(result, 0); - return result; - } + return readOnlyList; } - void Add(IList items) + var result = new T[list.Count]; + list.CopyTo(result, 0); + return result; + } + + private static void InternalAdd(IList items, IResourceHost owner, ref StyleCache? cache) + { + foreach (var resourceProvider in items.OfType()) { - for (var i = 0; i < items.Count; ++i) - { - var style = (IStyle)items[i]!; + resourceProvider.AddOwner(owner); + } - if (Owner is object && style is IResourceProvider resourceProvider) - { - resourceProvider.AddOwner(Owner); - } + if (items.Count > 0) + { + cache = null; + } - _cache = null; - } + (owner as IStyleHost)?.StylesAdded(ToReadOnlyList(items)); + } - (Owner as IStyleHost)?.StylesAdded(ToReadOnlyList(items)); + private static void InternalRemove(IList items, IResourceHost owner, ref StyleCache? cache) + { + foreach (var resourceProvider in items.OfType()) + { + resourceProvider.RemoveOwner(owner); } - void Remove(IList items) + if (items.Count > 0) { - for (var i = 0; i < items.Count; ++i) - { - var style = (IStyle)items[i]!; - - if (Owner is object && style is IResourceProvider resourceProvider) - { - resourceProvider.RemoveOwner(Owner); - } + cache = null; + } - _cache = null; - } + (owner as IStyleHost)?.StylesRemoved(ToReadOnlyList(items)); + } - (Owner as IStyleHost)?.StylesRemoved(ToReadOnlyList(items)); + private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Reset) + { + throw new InvalidOperationException("Reset should not be called on Styles."); } - switch (e.Action) + var currentOwner = Owner; + + if (currentOwner is not null) { - case NotifyCollectionChangedAction.Add: - Add(e.NewItems!); - break; - case NotifyCollectionChangedAction.Remove: - Remove(e.OldItems!); - break; - case NotifyCollectionChangedAction.Replace: - Remove(e.OldItems!); - Add(e.NewItems!); - break; - case NotifyCollectionChangedAction.Reset: - throw new InvalidOperationException("Reset should not be called on Styles."); + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + InternalAdd(e.NewItems!, currentOwner, ref _cache); + break; + case NotifyCollectionChangedAction.Remove: + InternalRemove(e.OldItems!, currentOwner, ref _cache); + break; + case NotifyCollectionChangedAction.Replace: + InternalRemove(e.OldItems!, currentOwner, ref _cache); + InternalAdd(e.NewItems!, currentOwner, ref _cache); + break; + } } CollectionChanged?.Invoke(this, e); From 1d0d1f20841440fd2b3385638aa0f24e8d9825d3 Mon Sep 17 00:00:00 2001 From: Mario Uhlmann Date: Fri, 10 Jun 2022 19:35:14 +0200 Subject: [PATCH 36/45] compile fix - ArgumentNullException.ThrowIfNull (net standard 2.0 fail) --- src/Avalonia.Base/Styling/Styles.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Styling/Styles.cs b/src/Avalonia.Base/Styling/Styles.cs index 903db5ffc7..175068541b 100644 --- a/src/Avalonia.Base/Styling/Styles.cs +++ b/src/Avalonia.Base/Styling/Styles.cs @@ -191,7 +191,7 @@ namespace Avalonia.Styling /// void IResourceProvider.AddOwner(IResourceHost owner) { - ArgumentNullException.ThrowIfNull(owner); + owner = owner ?? throw new ArgumentNullException(nameof(owner)); if (Owner is not null) { @@ -213,7 +213,7 @@ namespace Avalonia.Styling /// void IResourceProvider.RemoveOwner(IResourceHost owner) { - ArgumentNullException.ThrowIfNull(owner); + owner = owner ?? throw new ArgumentNullException(nameof(owner)); if (Owner == owner) { From 7698505770347044562072fba43e9d73bcbc28cf Mon Sep 17 00:00:00 2001 From: Ahmed Fawzy Date: Fri, 10 Jun 2022 20:43:14 +0200 Subject: [PATCH 37/45] Removed tests which collided with removing the _ignoreTextChanges field --- .../MaskedTextBoxTests.cs | 28 ------------------- .../TextBoxTests.cs | 28 ------------------- 2 files changed, 56 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs index af54be61f7..d1fa522206 100644 --- a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs @@ -179,34 +179,6 @@ namespace Avalonia.Controls.UnitTests } } - [Fact] - public void Typing_Beginning_With_0_Should_Not_Modify_Text_When_Bound_To_Int() - { - using (Start()) - { - var source = new Class1(); - var target = new MaskedTextBox - { - DataContext = source, - Template = CreateTemplate(), - }; - - target.ApplyTemplate(); - target.Bind(TextBox.TextProperty, new Binding(nameof(Class1.Foo), BindingMode.TwoWay)); - - Assert.Equal("0", target.Text); - - target.CaretIndex = 1; - target.RaiseEvent(new TextInputEventArgs - { - RoutedEvent = InputElement.TextInputEvent, - Text = "2", - }); - - Assert.Equal("02", target.Text); - } - } - [Fact] public void Control_Backspace_Should_Remove_The_Word_Before_The_Caret_If_There_Is_No_Selection() { diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index f15da8e0c5..23a330c96f 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -180,34 +180,6 @@ namespace Avalonia.Controls.UnitTests } } - [Fact] - public void Typing_Beginning_With_0_Should_Not_Modify_Text_When_Bound_To_Int() - { - using (UnitTestApplication.Start(Services)) - { - var source = new Class1(); - var target = new TextBox - { - DataContext = source, - Template = CreateTemplate(), - }; - - target.ApplyTemplate(); - target.Bind(TextBox.TextProperty, new Binding(nameof(Class1.Foo), BindingMode.TwoWay)); - - Assert.Equal("0", target.Text); - - target.CaretIndex = 1; - target.RaiseEvent(new TextInputEventArgs - { - RoutedEvent = InputElement.TextInputEvent, - Text = "2", - }); - - Assert.Equal("02", target.Text); - } - } - [Fact] public void Control_Backspace_Should_Remove_The_Word_Before_The_Caret_If_There_Is_No_Selection() { From 76765f75856f32eae7a24c5ad3224b8d66725bf9 Mon Sep 17 00:00:00 2001 From: Mario Uhlmann Date: Sat, 11 Jun 2022 06:51:27 +0200 Subject: [PATCH 38/45] Old cache reset logic --- src/Avalonia.Base/Styling/Styles.cs | 51 +++++++++++++++-------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/src/Avalonia.Base/Styling/Styles.cs b/src/Avalonia.Base/Styling/Styles.cs index 175068541b..1d0be96ac9 100644 --- a/src/Avalonia.Base/Styling/Styles.cs +++ b/src/Avalonia.Base/Styling/Styles.cs @@ -242,34 +242,40 @@ namespace Avalonia.Styling return result; } - private static void InternalAdd(IList items, IResourceHost owner, ref StyleCache? cache) + private static void InternalAdd(IList items, IResourceHost? owner, ref StyleCache? cache) { - foreach (var resourceProvider in items.OfType()) + if (owner is not null) { - resourceProvider.AddOwner(owner); + foreach (var resourceProvider in items.OfType()) + { + resourceProvider.AddOwner(owner); + } + + (owner as IStyleHost)?.StylesAdded(ToReadOnlyList(items)); } if (items.Count > 0) { cache = null; } - - (owner as IStyleHost)?.StylesAdded(ToReadOnlyList(items)); } - private static void InternalRemove(IList items, IResourceHost owner, ref StyleCache? cache) + private static void InternalRemove(IList items, IResourceHost? owner, ref StyleCache? cache) { - foreach (var resourceProvider in items.OfType()) + if (owner is not null) { - resourceProvider.RemoveOwner(owner); + foreach (var resourceProvider in items.OfType()) + { + resourceProvider.RemoveOwner(owner); + } + + (owner as IStyleHost)?.StylesRemoved(ToReadOnlyList(items)); } if (items.Count > 0) { cache = null; } - - (owner as IStyleHost)?.StylesRemoved(ToReadOnlyList(items)); } private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) @@ -281,21 +287,18 @@ namespace Avalonia.Styling var currentOwner = Owner; - if (currentOwner is not null) + switch (e.Action) { - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - InternalAdd(e.NewItems!, currentOwner, ref _cache); - break; - case NotifyCollectionChangedAction.Remove: - InternalRemove(e.OldItems!, currentOwner, ref _cache); - break; - case NotifyCollectionChangedAction.Replace: - InternalRemove(e.OldItems!, currentOwner, ref _cache); - InternalAdd(e.NewItems!, currentOwner, ref _cache); - break; - } + case NotifyCollectionChangedAction.Add: + InternalAdd(e.NewItems!, currentOwner, ref _cache); + break; + case NotifyCollectionChangedAction.Remove: + InternalRemove(e.OldItems!, currentOwner, ref _cache); + break; + case NotifyCollectionChangedAction.Replace: + InternalRemove(e.OldItems!, currentOwner, ref _cache); + InternalAdd(e.NewItems!, currentOwner, ref _cache); + break; } CollectionChanged?.Invoke(this, e); From bbe7d0abb255863ed05b7ea1eee32be47e09df37 Mon Sep 17 00:00:00 2001 From: Mario Uhlmann Date: Sat, 11 Jun 2022 13:23:44 +0200 Subject: [PATCH 39/45] foreach .. "items.OfType<" replaced with traditional for-loop --- src/Avalonia.Base/Styling/Styles.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/Styling/Styles.cs b/src/Avalonia.Base/Styling/Styles.cs index 1d0be96ac9..e4c3371007 100644 --- a/src/Avalonia.Base/Styling/Styles.cs +++ b/src/Avalonia.Base/Styling/Styles.cs @@ -2,7 +2,6 @@ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; -using System.Linq; using Avalonia.Collections; using Avalonia.Controls; @@ -246,9 +245,12 @@ namespace Avalonia.Styling { if (owner is not null) { - foreach (var resourceProvider in items.OfType()) + for (var i = 0; i < items.Count; ++i) { - resourceProvider.AddOwner(owner); + if (items[i] is IResourceProvider provider) + { + provider.AddOwner(owner); + } } (owner as IStyleHost)?.StylesAdded(ToReadOnlyList(items)); @@ -264,9 +266,12 @@ namespace Avalonia.Styling { if (owner is not null) { - foreach (var resourceProvider in items.OfType()) + for (var i = 0; i < items.Count; ++i) { - resourceProvider.RemoveOwner(owner); + if (items[i] is IResourceProvider provider) + { + provider.RemoveOwner(owner); + } } (owner as IStyleHost)?.StylesRemoved(ToReadOnlyList(items)); From 8d52bd98af7c63a0d46560295ef0100964426302 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 12 Jun 2022 18:44:52 -0400 Subject: [PATCH 40/45] Add profiling to the control catalog android --- .../ControlCatalog.Android.csproj | 27 ++++++++----------- .../Properties/AndroidManifest.xml | 3 ++- .../environment.device.txt | 1 + .../environment.emulator.txt | 1 + 4 files changed, 15 insertions(+), 17 deletions(-) create mode 100644 samples/ControlCatalog.Android/environment.device.txt create mode 100644 samples/ControlCatalog.Android/environment.emulator.txt diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj index ec88852feb..e52430f50b 100644 --- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj +++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj @@ -9,42 +9,37 @@ 1.0 apk true + android-arm64;android-x64 - - - Resources\drawable\Icon.png - - False - False + True + + + True no-write-symbols,nodebug Hybrid True - - False - False - - - - True + + True + True - - + + - \ No newline at end of file + diff --git a/samples/ControlCatalog.Android/Properties/AndroidManifest.xml b/samples/ControlCatalog.Android/Properties/AndroidManifest.xml index aa570ec504..6f551d2b01 100644 --- a/samples/ControlCatalog.Android/Properties/AndroidManifest.xml +++ b/samples/ControlCatalog.Android/Properties/AndroidManifest.xml @@ -1,4 +1,5 @@  - + + diff --git a/samples/ControlCatalog.Android/environment.device.txt b/samples/ControlCatalog.Android/environment.device.txt new file mode 100644 index 0000000000..107d68ca1b --- /dev/null +++ b/samples/ControlCatalog.Android/environment.device.txt @@ -0,0 +1 @@ +DOTNET_DiagnosticPorts=127.0.0.1:9000,suspend diff --git a/samples/ControlCatalog.Android/environment.emulator.txt b/samples/ControlCatalog.Android/environment.emulator.txt new file mode 100644 index 0000000000..299a0ec30b --- /dev/null +++ b/samples/ControlCatalog.Android/environment.emulator.txt @@ -0,0 +1 @@ +DOTNET_DiagnosticPorts=10.0.2.2:9001,suspend From 6a5e0055393dd61dede522d533a537be8d6f67d1 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 22 Jun 2022 00:05:28 -0400 Subject: [PATCH 41/45] Add trim_trailing_whitespace to editorconfig --- .editorconfig | 1 + 1 file changed, 1 insertion(+) diff --git a/.editorconfig b/.editorconfig index 25e0135725..cb589a5ce1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -21,6 +21,7 @@ csharp_new_line_before_finally = true csharp_new_line_before_members_in_object_initializers = true csharp_new_line_before_members_in_anonymous_types = true csharp_new_line_between_query_expression_clauses = true +trim_trailing_whitespace = true # Indentation preferences csharp_indent_block_contents = true From 64518efc5184d67208a7d0c3b7df9c66a364a562 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 22 Jun 2022 12:59:40 +0200 Subject: [PATCH 42/45] Add failing test for #8372. --- .../AvaloniaObjectTests_SetValue.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs index 954a609315..72162a4d8e 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs @@ -17,6 +17,21 @@ namespace Avalonia.Base.UnitTests Assert.Equal("foodefault", target.GetValue(Class1.FooProperty)); } + [Fact] + public void ClearValue_Resets_Value_To_Style_value() + { + Class1 target = new Class1(); + + target.SetValue(Class1.FooProperty, "style", BindingPriority.Style); + target.SetValue(Class1.FooProperty, "local"); + + Assert.Equal("local", target.GetValue(Class1.FooProperty)); + + target.ClearValue(Class1.FooProperty); + + Assert.Equal("style", target.GetValue(Class1.FooProperty)); + } + [Fact] public void ClearValue_Raises_PropertyChanged() { From f33d4e881f3b4fb4db9a13279ad8bf1c648b3151 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 22 Jun 2022 13:01:37 +0200 Subject: [PATCH 43/45] Correctly clear local value in PriorityValue. --- src/Avalonia.Base/PropertyStore/PriorityValue.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Base/PropertyStore/PriorityValue.cs b/src/Avalonia.Base/PropertyStore/PriorityValue.cs index 112cf6619f..182b2638c4 100644 --- a/src/Avalonia.Base/PropertyStore/PriorityValue.cs +++ b/src/Avalonia.Base/PropertyStore/PriorityValue.cs @@ -121,6 +121,7 @@ namespace Avalonia.PropertyStore public void ClearLocalValue() { + _localValue = default; UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs( _owner, Property, From c9e10f0d2f88346caeb461b900199b6fb571653d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 22 Jun 2022 15:53:16 +0200 Subject: [PATCH 44/45] Added additional failing test. Exposed by the previous fix for #8372: re-entrancy in `PropertySetterInstance.Dispose()` is causing detaching a style to call `ClearValue` on the property. Previously this wasn't a problem as `ClearValue` didn't work, but now it is. (Also added one passing test which tests the same scenario in `PropertySetterBindingInstance` for future coverage) --- .../Styling/SetterTests.cs | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs b/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs index ed4c78aa3e..c684466200 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs @@ -150,13 +150,43 @@ namespace Avalonia.Base.UnitTests.Styling Assert.Equal(BindingPriority.StyleTrigger, control.GetDiagnostic(TextBlock.TagProperty).Priority); } - private IBinding CreateMockBinding(AvaloniaProperty property) + [Fact] + public void Disposing_Setter_Should_Preserve_LocalValue() { - var subject = new Subject(); - var descriptor = InstancedBinding.OneWay(subject); - var binding = Mock.Of(x => - x.Initiate(It.IsAny(), property, null, false) == descriptor); - return binding; + var control = new Canvas(); + var setter = new Setter(TextBlock.TagProperty, "foo"); + + var instance = setter.Instance(control); + instance.Start(true); + instance.Activate(); + + control.Tag = "bar"; + + instance.Dispose(); + + Assert.Equal("bar", control.Tag); + } + + [Fact] + public void Disposing_Binding_Setter_Should_Preserve_LocalValue() + { + var control = new Canvas(); + var source = new { Foo = "foo" }; + var setter = new Setter(TextBlock.TagProperty, new Binding + { + Source = source, + Path = nameof(source.Foo), + }); + + var instance = setter.Instance(control); + instance.Start(true); + instance.Activate(); + + control.Tag = "bar"; + + instance.Dispose(); + + Assert.Equal("bar", control.Tag); } private class TestConverter : IValueConverter From 857bfb5bd2b863825c09fbfb780dc379fba4d345 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 22 Jun 2022 16:00:29 +0200 Subject: [PATCH 45/45] Prevent re-entrancy in PropertySetterInstance.Dispose. The call to `_subscription.Dispose()` causes `BindingEntry.Dispose()` to call `_subscription.Dispose()`, but in this case the `BindingEntry._subscription` instance is the `PropertySetterInstance`! Except now `PropertySetterInstance._subscription` is null, and so `PropertySetterInstance.Dispose` called `ClearValue`, which is obviously wrong. --- .../Styling/PropertySetterInstance.cs | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Base/Styling/PropertySetterInstance.cs b/src/Avalonia.Base/Styling/PropertySetterInstance.cs index c4e8f47e67..9028224cc1 100644 --- a/src/Avalonia.Base/Styling/PropertySetterInstance.cs +++ b/src/Avalonia.Base/Styling/PropertySetterInstance.cs @@ -18,7 +18,7 @@ namespace Avalonia.Styling private readonly DirectPropertyBase? _directProperty; private readonly T _value; private IDisposable? _subscription; - private bool _isActive; + private State _state; public PropertySetterInstance( IStyleable target, @@ -40,6 +40,8 @@ namespace Avalonia.Styling _value = value; } + private bool IsActive => _state == State.Active; + public void Start(bool hasActivator) { if (hasActivator) @@ -70,31 +72,35 @@ namespace Avalonia.Styling public void Activate() { - if (!_isActive) + if (!IsActive) { - _isActive = true; + _state = State.Active; PublishNext(); } } public void Deactivate() { - if (_isActive) + if (IsActive) { - _isActive = false; + _state = State.Inactive; PublishNext(); } } public override void Dispose() { + if (_state == State.Disposed) + return; + _state = State.Disposed; + if (_subscription is object) { var sub = _subscription; _subscription = null; sub.Dispose(); } - else if (_isActive) + else if (IsActive) { if (_styledProperty is object) { @@ -114,7 +120,14 @@ namespace Avalonia.Styling private void PublishNext() { - PublishNext(_isActive ? new BindingValue(_value) : default); + PublishNext(IsActive ? new BindingValue(_value) : default); + } + + private enum State + { + Inactive, + Active, + Disposed, } } }