diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs index e07c933039..38d559a031 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs @@ -218,6 +218,8 @@ namespace Avalonia.Controls.Primitives { // No explicit height values were set so we can autosize autoSizeHeight = true; + // We need to invalidate desired height in order to grow or shrink as needed + InvalidateDesiredHeight(); measureHeight = double.PositiveInfinity; } else 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/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs index f9737b461d..39ddd9d769 100644 --- a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs +++ b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs @@ -36,6 +36,13 @@ 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 string UnescapeDeviceLabel(string input) => UnescapeString(input, @"\\x([0-9a-f]{2})", 16); + private void Poll(long _) { var fProcPartitions = File.ReadAllLines(ProcPartitionsDir) @@ -47,14 +54,14 @@ 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) ? 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 @@ -64,7 +71,7 @@ namespace Avalonia.FreeDesktop { VolumePath = mount.Item2, VolumeSizeBytes = device.Item1, - VolumeLabel = x.Name + VolumeLabel = x.Item2 }; var mountVolInfos = q1.ToArray(); 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); 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; } } }