From 35db70c8d4905eba47f0fd73e2de0830d5e8e2fe Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 12 May 2022 14:37:23 +0200 Subject: [PATCH 1/8] 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/8] 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 a8809e04f95bf7c68fb96d0e708ee99592865883 Mon Sep 17 00:00:00 2001 From: Sviatoslav Peleshko Date: Sun, 15 May 2022 07:51:10 +0300 Subject: [PATCH 3/8] Use the correct size for the symlink path buffer This fixes crashes when the path has non-ASCII characters --- src/Avalonia.FreeDesktop/NativeMethods.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.FreeDesktop/NativeMethods.cs b/src/Avalonia.FreeDesktop/NativeMethods.cs index 8bbda98bb2..147955b6a3 100644 --- a/src/Avalonia.FreeDesktop/NativeMethods.cs +++ b/src/Avalonia.FreeDesktop/NativeMethods.cs @@ -14,15 +14,15 @@ namespace Avalonia.FreeDesktop public static string ReadLink(string path) { - var symlinkMaxSize = Encoding.ASCII.GetMaxByteCount(path.Length); + var symlinkSize = Encoding.UTF8.GetByteCount(path); var bufferSize = 4097; // PATH_MAX is (usually?) 4096, but we need to know if the result was truncated - var symlink = ArrayPool.Shared.Rent(symlinkMaxSize + 1); + var symlink = ArrayPool.Shared.Rent(symlinkSize + 1); var buffer = ArrayPool.Shared.Rent(bufferSize); try { - var symlinkSize = Encoding.UTF8.GetBytes(path, 0, path.Length, symlink, 0); + Encoding.UTF8.GetBytes(path, 0, path.Length, symlink, 0); symlink[symlinkSize] = 0; var size = readlink(symlink, buffer, bufferSize); From 56b5af0da6b033af423cc933278e6b669c0b049a Mon Sep 17 00:00:00 2001 From: Sviatoslav Peleshko Date: Sun, 15 May 2022 07:55:39 +0300 Subject: [PATCH 4/8] Unescape mount point paths read from /proc/mounts This makes mounts that have symbols " \t\n\\" actually appear in the volumes list in the ManagedFileChooser dialog. --- src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs index f9737b461d..1af44f9db6 100644 --- a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs +++ b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs @@ -36,6 +36,11 @@ namespace Avalonia.FreeDesktop private string GetSymlinkTarget(string x) => Path.GetFullPath(Path.Combine(DevByLabelDir, NativeMethods.ReadLink(x))); + private string UnescapeString(string input, string regexText, int escapeBase) => + new Regex(regexText).Replace(input, m => Convert.ToChar(Convert.ToByte(m.Groups[1].Value, escapeBase)).ToString()); + + private string UnescapePathFromProcMounts(string input) => UnescapeString(input, @"\\(\d{3})", 8); + private void Poll(long _) { var fProcPartitions = File.ReadAllLines(ProcPartitionsDir) @@ -47,7 +52,7 @@ namespace Avalonia.FreeDesktop var fProcMounts = File.ReadAllLines(ProcMountsDir) .Select(x => x.Split(' ')) - .Select(x => (x[0], x[1])) + .Select(x => (x[0], UnescapePathFromProcMounts(x[1]))) .Where(x => !x.Item2.StartsWith("/snap/", StringComparison.InvariantCultureIgnoreCase)); var labelDirEnum = Directory.Exists(DevByLabelDir) ? From 97765b522c19f3a726bd902b974af7501f45db18 Mon Sep 17 00:00:00 2001 From: Sviatoslav Peleshko Date: Sun, 15 May 2022 07:58:45 +0300 Subject: [PATCH 5/8] Unescape volumes labels read from /dev/disk/by-label folder This makes volumes with labels containing some special characters (e.g. whitespace, tab, etc.) to show up correctly in the ManagedFileChooser dialog. --- src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs index 1af44f9db6..39ddd9d769 100644 --- a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs +++ b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs @@ -41,6 +41,8 @@ namespace Avalonia.FreeDesktop private string UnescapePathFromProcMounts(string input) => UnescapeString(input, @"\\(\d{3})", 8); + private string UnescapeDeviceLabel(string input) => UnescapeString(input, @"\\x([0-9a-f]{2})", 16); + private void Poll(long _) { var fProcPartitions = File.ReadAllLines(ProcPartitionsDir) @@ -59,7 +61,7 @@ namespace Avalonia.FreeDesktop new DirectoryInfo(DevByLabelDir).GetFiles() : Enumerable.Empty(); var labelDevPathPairs = labelDirEnum - .Select(x => (GetSymlinkTarget(x.FullName), x.Name)); + .Select(x => (GetSymlinkTarget(x.FullName), UnescapeDeviceLabel(x.Name))); var q1 = from mount in fProcMounts join device in fProcPartitions on mount.Item1 equals device.Item2 @@ -69,7 +71,7 @@ namespace Avalonia.FreeDesktop { VolumePath = mount.Item2, VolumeSizeBytes = device.Item1, - VolumeLabel = x.Name + VolumeLabel = x.Item2 }; var mountVolInfos = q1.ToArray(); From 4c039cbf7ceda4005c5e9015fcf7b83a01ce8eee Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 16 May 2022 22:46:24 -0400 Subject: [PATCH 6/8] Move Fluent/Default themes out of main package --- build/CoreLibraries.props | 2 -- samples/BindingDemo/BindingDemo.csproj | 1 + samples/ControlCatalog/ControlCatalog.csproj | 2 ++ samples/IntegrationTestApp/IntegrationTestApp.csproj | 3 ++- samples/PlatformSanityChecks/PlatformSanityChecks.csproj | 1 + samples/Previewer/Previewer.csproj | 3 +++ samples/RenderDemo/RenderDemo.csproj | 1 + samples/Sandbox/Sandbox.csproj | 1 + samples/VirtualizationDemo/VirtualizationDemo.csproj | 1 + .../interop/Direct3DInteropSample/Direct3DInteropSample.csproj | 1 + samples/interop/NativeEmbedSample/NativeEmbedSample.csproj | 1 + 11 files changed, 14 insertions(+), 3 deletions(-) diff --git a/build/CoreLibraries.props b/build/CoreLibraries.props index 314d38190a..9448a31d73 100644 --- a/build/CoreLibraries.props +++ b/build/CoreLibraries.props @@ -3,8 +3,6 @@ - - diff --git a/samples/BindingDemo/BindingDemo.csproj b/samples/BindingDemo/BindingDemo.csproj index 2c6ff74e5e..bd6054327f 100644 --- a/samples/BindingDemo/BindingDemo.csproj +++ b/samples/BindingDemo/BindingDemo.csproj @@ -5,6 +5,7 @@ + diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index 7cbd8a3f9c..e5f07c90c3 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -25,6 +25,8 @@ + + diff --git a/samples/IntegrationTestApp/IntegrationTestApp.csproj b/samples/IntegrationTestApp/IntegrationTestApp.csproj index e8338adae6..4284399357 100644 --- a/samples/IntegrationTestApp/IntegrationTestApp.csproj +++ b/samples/IntegrationTestApp/IntegrationTestApp.csproj @@ -1,4 +1,4 @@ - + WinExe net6.0 @@ -18,6 +18,7 @@ + diff --git a/samples/PlatformSanityChecks/PlatformSanityChecks.csproj b/samples/PlatformSanityChecks/PlatformSanityChecks.csproj index 9660d2a90d..4f7f06b529 100644 --- a/samples/PlatformSanityChecks/PlatformSanityChecks.csproj +++ b/samples/PlatformSanityChecks/PlatformSanityChecks.csproj @@ -7,6 +7,7 @@ + diff --git a/samples/Previewer/Previewer.csproj b/samples/Previewer/Previewer.csproj index c1d14cba26..98560e9ab0 100644 --- a/samples/Previewer/Previewer.csproj +++ b/samples/Previewer/Previewer.csproj @@ -9,6 +9,9 @@ + + + diff --git a/samples/RenderDemo/RenderDemo.csproj b/samples/RenderDemo/RenderDemo.csproj index 18a4ee5662..3c62af1eaf 100644 --- a/samples/RenderDemo/RenderDemo.csproj +++ b/samples/RenderDemo/RenderDemo.csproj @@ -12,6 +12,7 @@ + diff --git a/samples/Sandbox/Sandbox.csproj b/samples/Sandbox/Sandbox.csproj index f3c38cd96e..eab654acb6 100644 --- a/samples/Sandbox/Sandbox.csproj +++ b/samples/Sandbox/Sandbox.csproj @@ -10,6 +10,7 @@ + diff --git a/samples/VirtualizationDemo/VirtualizationDemo.csproj b/samples/VirtualizationDemo/VirtualizationDemo.csproj index 2c6ff74e5e..bd6054327f 100644 --- a/samples/VirtualizationDemo/VirtualizationDemo.csproj +++ b/samples/VirtualizationDemo/VirtualizationDemo.csproj @@ -5,6 +5,7 @@ + diff --git a/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj b/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj index cd9963a2e5..81b94b4d09 100644 --- a/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj +++ b/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj @@ -22,6 +22,7 @@ + diff --git a/samples/interop/NativeEmbedSample/NativeEmbedSample.csproj b/samples/interop/NativeEmbedSample/NativeEmbedSample.csproj index c25442b52c..2f3ea85e46 100644 --- a/samples/interop/NativeEmbedSample/NativeEmbedSample.csproj +++ b/samples/interop/NativeEmbedSample/NativeEmbedSample.csproj @@ -9,6 +9,7 @@ + From 8d3af8a9b45f67b01aea4b6e60183c883e6b6d63 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 17 May 2022 10:01:55 +0200 Subject: [PATCH 7/8] 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 8/8] 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; + } } }