diff --git a/src/Avalonia.Controls/Viewbox.cs b/src/Avalonia.Controls/Viewbox.cs index 33a05f126d..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 Decorator _containerVisual; + private readonly ViewboxContainer _containerVisual; /// /// Defines the property. @@ -37,9 +38,10 @@ namespace Avalonia.Controls public Viewbox() { - _containerVisual = new Decorator(); + // 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; - LogicalChildren.Add(_containerVisual); VisualChildren.Add(_containerVisual); } @@ -88,7 +90,22 @@ namespace Avalonia.Controls if (change.Property == ChildProperty) { - _containerVisual.Child = change.GetNewValue(); + var (oldChild, newChild) = change.GetOldAndNewValue(); + + if (oldChild is not null) + { + ((ISetLogicalParent)oldChild).SetParent(null); + LogicalChildren.Remove(oldChild); + } + + _containerVisual.Child = newChild; + + if (newChild is not null) + { + ((ISetLogicalParent)newChild).SetParent(this); + LogicalChildren.Add(newChild); + } + InvalidateMeasure(); } } @@ -120,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)); @@ -129,5 +146,31 @@ 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; + + 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..3cebe142b6 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; @@ -18,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] @@ -36,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] @@ -54,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] @@ -72,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] @@ -90,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] @@ -108,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] @@ -136,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] @@ -164,11 +157,44 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Size(expectedWidth, expectedHeight), target.DesiredSize); - var scaleTransform = target.InternalTransform as ScaleTransform; + Assert.True(TryGetScale(target, out Vector scale)); + Assert.Equal(expectedScale, scale.X); + Assert.Equal(expectedScale, scale.Y); + } + + [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()); + } + + 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); - Assert.NotNull(scaleTransform); - Assert.Equal(expectedScale, scaleTransform.ScaleX); - Assert.Equal(expectedScale, scaleTransform.ScaleY); + scale = decomposed.Scale; + return true; } } }