From 9d335c3a96b3ca1d6c9aedcd6a5c1408375d4e78 Mon Sep 17 00:00:00 2001 From: v-yadli Date: Sun, 13 Dec 2020 02:11:28 +0800 Subject: [PATCH 01/32] 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/32] 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 03e01a6e554227613ce5579cf24884e8a94ad629 Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Mon, 30 May 2022 13:04:46 +0300 Subject: [PATCH 03/32] 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 04/32] 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 05/32] 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 06/32] 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 07/32] 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 08/32] 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 09/32] 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 10/32] 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 11/32] 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 12/32] 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 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 13/32] 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 14/32] 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 15/32] 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 16/32] 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 17/32] 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 18/32] 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 19/32] 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 20/32] 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 21/32] 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 22/32] 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 23/32] 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 24/32] 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 25/32] 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 26/32] 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 27/32] 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 28/32] 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 29/32] 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 30/32] 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 31/32] 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 32/32] 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, } } }