From 35db70c8d4905eba47f0fd73e2de0830d5e8e2fe Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 12 May 2022 14:37:23 +0200 Subject: [PATCH 1/4] Don't expose viewbox container as logical child. #7735 introduced an internal container control which hosts the child, but it exposed this child in the logical tree, breaking any styles which relied on the `Viewbox.Child` being the logical child of the `Viewbox`. Fix this by introducing a simple internal `ViewboxContainer` control as the container. --- src/Avalonia.Controls/Viewbox.cs | 43 +++++++++++++++++-- .../ViewboxTests.cs | 20 +++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Viewbox.cs b/src/Avalonia.Controls/Viewbox.cs index 33a05f126d..f3ec53ed2d 100644 --- a/src/Avalonia.Controls/Viewbox.cs +++ b/src/Avalonia.Controls/Viewbox.cs @@ -8,7 +8,7 @@ namespace Avalonia.Controls /// public class Viewbox : Control { - private Decorator _containerVisual; + private ViewboxContainer _containerVisual; /// /// Defines the property. @@ -37,9 +37,8 @@ namespace Avalonia.Controls public Viewbox() { - _containerVisual = new Decorator(); + _containerVisual = new ViewboxContainer(); _containerVisual.RenderTransformOrigin = RelativePoint.TopLeft; - LogicalChildren.Add(_containerVisual); VisualChildren.Add(_containerVisual); } @@ -88,7 +87,22 @@ namespace Avalonia.Controls if (change.Property == ChildProperty) { + var (oldChild, newChild) = change.GetOldAndNewValue(); + + if (oldChild is not null) + { + ((ISetLogicalParent)oldChild).SetParent(null); + LogicalChildren.Remove(oldChild); + } + _containerVisual.Child = change.GetNewValue(); + + if (newChild is not null) + { + ((ISetLogicalParent)newChild).SetParent(this); + LogicalChildren.Add(newChild); + } + InvalidateMeasure(); } } @@ -129,5 +143,28 @@ namespace Avalonia.Controls return finalSize; } + + private class ViewboxContainer : Control + { + private IControl? _child; + + public IControl? Child + { + get => _child; + set + { + if (_child != value) + { + if (_child is not null) + VisualChildren.Remove(_child); + + _child = value; + + if (_child is not null) + VisualChildren.Add(_child); + } + } + } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs b/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs index d33e55341b..39a14a6a7e 100644 --- a/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs @@ -1,4 +1,5 @@ using Avalonia.Controls.Shapes; +using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.UnitTests; using Xunit; @@ -170,5 +171,24 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(expectedScale, scaleTransform.ScaleX); Assert.Equal(expectedScale, scaleTransform.ScaleY); } + + [Fact] + public void Child_Should_Be_Logical_Child_Of_Viewbox() + { + var target = new Viewbox(); + + Assert.Empty(target.GetLogicalChildren()); + + var child = new Canvas(); + target.Child = child; + + Assert.Single(target.GetLogicalChildren(), child); + Assert.Same(child.GetLogicalParent(), target); + + target.Child = null; + + Assert.Empty(target.GetLogicalChildren()); + Assert.Null(child.GetLogicalParent()); + } } } From 36fefd871704a48ef6fd553845ad6c03e70ea9ad Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 12 May 2022 22:44:29 +0200 Subject: [PATCH 2/4] Fix copypasta. Co-authored-by: Max Katz --- src/Avalonia.Controls/Viewbox.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Viewbox.cs b/src/Avalonia.Controls/Viewbox.cs index f3ec53ed2d..55c52d8ed9 100644 --- a/src/Avalonia.Controls/Viewbox.cs +++ b/src/Avalonia.Controls/Viewbox.cs @@ -95,7 +95,7 @@ namespace Avalonia.Controls LogicalChildren.Remove(oldChild); } - _containerVisual.Child = change.GetNewValue(); + _containerVisual.Child = newChild; if (newChild is not null) { From 8d3af8a9b45f67b01aea4b6e60183c883e6b6d63 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 17 May 2022 10:01:55 +0200 Subject: [PATCH 3/4] Added some explanatory comments. --- src/Avalonia.Controls/Viewbox.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Avalonia.Controls/Viewbox.cs b/src/Avalonia.Controls/Viewbox.cs index 55c52d8ed9..0dea6359ba 100644 --- a/src/Avalonia.Controls/Viewbox.cs +++ b/src/Avalonia.Controls/Viewbox.cs @@ -37,6 +37,8 @@ namespace Avalonia.Controls public Viewbox() { + // The Child control is hosted inside a ViewboxContainer control so that the transform + // can be applied independently of the Viewbox and Child transforms. _containerVisual = new ViewboxContainer(); _containerVisual.RenderTransformOrigin = RelativePoint.TopLeft; VisualChildren.Add(_containerVisual); @@ -144,6 +146,9 @@ namespace Avalonia.Controls return finalSize; } + /// + /// A simple container control which hosts its child as a visual but not logical child. + /// private class ViewboxContainer : Control { private IControl? _child; From 220a8a8df9946c98cfd9b6e6e5a3fb4860f8cfd8 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Tue, 17 May 2022 13:21:21 +0200 Subject: [PATCH 4/4] Use cheaper transforms in Viewbox. --- src/Avalonia.Controls/Viewbox.cs | 5 +- .../ViewboxTests.cs | 86 ++++++++++--------- 2 files changed, 49 insertions(+), 42 deletions(-) diff --git a/src/Avalonia.Controls/Viewbox.cs b/src/Avalonia.Controls/Viewbox.cs index 0dea6359ba..01a41a0157 100644 --- a/src/Avalonia.Controls/Viewbox.cs +++ b/src/Avalonia.Controls/Viewbox.cs @@ -1,4 +1,5 @@ using Avalonia.Media; +using Avalonia.Media.Immutable; using Avalonia.Metadata; namespace Avalonia.Controls @@ -8,7 +9,7 @@ namespace Avalonia.Controls /// public class Viewbox : Control { - private ViewboxContainer _containerVisual; + private readonly ViewboxContainer _containerVisual; /// /// Defines the property. @@ -136,7 +137,7 @@ namespace Avalonia.Controls var childSize = child.DesiredSize; var scale = Stretch.CalculateScaling(finalSize, childSize, StretchDirection); - InternalTransform = new ScaleTransform(scale.X, scale.Y); + InternalTransform = new ImmutableTransform(Matrix.CreateScale(scale.X, scale.Y)); child.Arrange(new Rect(childSize)); diff --git a/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs b/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs index 39a14a6a7e..3cebe142b6 100644 --- a/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs @@ -19,11 +19,10 @@ namespace Avalonia.Controls.UnitTests target.Arrange(new Rect(new Point(0, 0), target.DesiredSize)); Assert.Equal(new Size(200, 100), target.DesiredSize); - var scaleTransform = target.InternalTransform as ScaleTransform; - - Assert.NotNull(scaleTransform); - Assert.Equal(2.0, scaleTransform.ScaleX); - Assert.Equal(2.0, scaleTransform.ScaleY); + + Assert.True(TryGetScale(target, out Vector scale)); + Assert.Equal(2.0, scale.X); + Assert.Equal(2.0, scale.Y); } [Fact] @@ -37,11 +36,10 @@ namespace Avalonia.Controls.UnitTests target.Arrange(new Rect(new Point(0, 0), target.DesiredSize)); Assert.Equal(new Size(100, 50), target.DesiredSize); - var scaleTransform = target.InternalTransform as ScaleTransform; - - Assert.NotNull(scaleTransform); - Assert.Equal(1.0, scaleTransform.ScaleX); - Assert.Equal(1.0, scaleTransform.ScaleY); + + Assert.True(TryGetScale(target, out Vector scale)); + Assert.Equal(1.0, scale.X); + Assert.Equal(1.0, scale.Y); } [Fact] @@ -55,11 +53,10 @@ namespace Avalonia.Controls.UnitTests target.Arrange(new Rect(new Point(0, 0), target.DesiredSize)); Assert.Equal(new Size(200, 200), target.DesiredSize); - var scaleTransform = target.InternalTransform as ScaleTransform; - - Assert.NotNull(scaleTransform); - Assert.Equal(2.0, scaleTransform.ScaleX); - Assert.Equal(4.0, scaleTransform.ScaleY); + + Assert.True(TryGetScale(target, out Vector scale)); + Assert.Equal(2.0, scale.X); + Assert.Equal(4.0, scale.Y); } [Fact] @@ -73,11 +70,10 @@ namespace Avalonia.Controls.UnitTests target.Arrange(new Rect(new Point(0, 0), target.DesiredSize)); Assert.Equal(new Size(200, 200), target.DesiredSize); - var scaleTransform = target.InternalTransform as ScaleTransform; - - Assert.NotNull(scaleTransform); - Assert.Equal(4.0, scaleTransform.ScaleX); - Assert.Equal(4.0, scaleTransform.ScaleY); + + Assert.True(TryGetScale(target, out Vector scale)); + Assert.Equal(4.0, scale.X); + Assert.Equal(4.0, scale.Y); } [Fact] @@ -91,11 +87,10 @@ namespace Avalonia.Controls.UnitTests target.Arrange(new Rect(new Point(0, 0), target.DesiredSize)); Assert.Equal(new Size(400, 200), target.DesiredSize); - var scaleTransform = target.InternalTransform as ScaleTransform; - - Assert.NotNull(scaleTransform); - Assert.Equal(4.0, scaleTransform.ScaleX); - Assert.Equal(4.0, scaleTransform.ScaleY); + + Assert.True(TryGetScale(target, out Vector scale)); + Assert.Equal(4.0, scale.X); + Assert.Equal(4.0, scale.Y); } [Fact] @@ -109,11 +104,10 @@ namespace Avalonia.Controls.UnitTests target.Arrange(new Rect(new Point(0, 0), target.DesiredSize)); Assert.Equal(new Size(200, 100), target.DesiredSize); - var scaleTransform = target.InternalTransform as ScaleTransform; - - Assert.NotNull(scaleTransform); - Assert.Equal(2.0, scaleTransform.ScaleX); - Assert.Equal(2.0, scaleTransform.ScaleY); + + Assert.True(TryGetScale(target, out Vector scale)); + Assert.Equal(2.0, scale.X); + Assert.Equal(2.0, scale.Y); } [Theory] @@ -137,11 +131,9 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Size(expectedWidth, expectedHeight), target.DesiredSize); - var scaleTransform = target.InternalTransform as ScaleTransform; - - Assert.NotNull(scaleTransform); - Assert.Equal(expectedScale, scaleTransform.ScaleX); - Assert.Equal(expectedScale, scaleTransform.ScaleY); + Assert.True(TryGetScale(target, out Vector scale)); + Assert.Equal(expectedScale, scale.X); + Assert.Equal(expectedScale, scale.Y); } [Theory] @@ -165,11 +157,9 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Size(expectedWidth, expectedHeight), target.DesiredSize); - var scaleTransform = target.InternalTransform as ScaleTransform; - - Assert.NotNull(scaleTransform); - Assert.Equal(expectedScale, scaleTransform.ScaleX); - Assert.Equal(expectedScale, scaleTransform.ScaleY); + Assert.True(TryGetScale(target, out Vector scale)); + Assert.Equal(expectedScale, scale.X); + Assert.Equal(expectedScale, scale.Y); } [Fact] @@ -190,5 +180,21 @@ namespace Avalonia.Controls.UnitTests Assert.Empty(target.GetLogicalChildren()); Assert.Null(child.GetLogicalParent()); } + + private bool TryGetScale(Viewbox viewbox, out Vector scale) + { + if (viewbox.InternalTransform is null) + { + scale = default; + return false; + } + + var matrix = viewbox.InternalTransform.Value; + + Matrix.TryDecomposeTransform(matrix, out var decomposed); + + scale = decomposed.Scale; + return true; + } } }