Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 37 additions & 10 deletions src/Avalonia.Controls/LayoutTransformControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,19 @@ protected override Size MeasureOverride(Size availableSize)
// Return result to allocate enough space for the transformation
return transformedDesiredSize;
}

protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
SubscribeLayoutTransform(LayoutTransform as Transform);
ApplyLayoutTransform();
}

protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
UnsubscribeLayoutTransform(LayoutTransform as Transform);
}

private IDisposable? _renderTransformChangedEvent;

Expand Down Expand Up @@ -224,7 +237,6 @@ private void OnChildChanged()
/// Transformation matrix corresponding to _matrixTransform.
/// </summary>
private Matrix _transformation = Matrix.Identity;
private IDisposable? _transformChangedEvent;

/// <summary>
/// Returns true if Size a is smaller than Size b in either dimension.
Expand Down Expand Up @@ -424,19 +436,34 @@ private Size ComputeLargestTransformedSize(Size arrangeBounds)

private void OnLayoutTransformChanged(AvaloniaPropertyChangedEventArgs e)
{
var newTransform = e.NewValue as Transform;

_transformChangedEvent?.Dispose();
_transformChangedEvent = null;

if (newTransform != null)
if (this.IsAttachedToVisualTree)
{
_transformChangedEvent = Observable.FromEventPattern(
v => newTransform.Changed += v, v => newTransform.Changed -= v)
.Subscribe(_ => ApplyLayoutTransform());
UnsubscribeLayoutTransform(e.OldValue as Transform);
SubscribeLayoutTransform(e.NewValue as Transform);
}

ApplyLayoutTransform();
}

private void OnTransformChanged(object? sender, EventArgs e)
{
ApplyLayoutTransform();
}

private void SubscribeLayoutTransform(Transform? transform)
{
if (transform != null)
{
transform.Changed += OnTransformChanged;
}
}

private void UnsubscribeLayoutTransform(Transform? transform)
{
if (transform != null)
{
transform.Changed -= OnTransformChanged;
}
}
}
}
29 changes: 29 additions & 0 deletions tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,35 @@ public void Should_Generate_SkewTransform_minus_45_degrees()
Assert.Equal(m.M31, res.M31, 3);
Assert.Equal(m.M32, res.M32, 3);
}

[Fact]
public void Should_Apply_Transform_On_Attach_To_VisualTree()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var transform = new SkewTransform() { AngleX = -45, AngleY = -45 };

LayoutTransformControl lt = CreateWithChildAndMeasureAndTransform(
100,
100,
transform);

transform.AngleX = 45;
transform.AngleY = 45;

var window = new Window { Content = lt };
window.Show();

Matrix actual = lt.TransformRoot.RenderTransform.Value;
Matrix expected = Matrix.CreateSkew(Matrix.ToRadians(45), Matrix.ToRadians(45));
Assert.Equal(expected.M11, actual.M11, 3);
Assert.Equal(expected.M12, actual.M12, 3);
Assert.Equal(expected.M21, actual.M21, 3);
Assert.Equal(expected.M22, actual.M22, 3);
Assert.Equal(expected.M31, actual.M31, 3);
Assert.Equal(expected.M32, actual.M32, 3);
}
}

private static void TransformMeasureSizeTest(Size size, Transform transform, Size expectedSize)
{
Expand Down
49 changes: 49 additions & 0 deletions tests/Avalonia.LeakTests/ControlTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Avalonia.Data;
using Avalonia.Diagnostics;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering;
Expand Down Expand Up @@ -1000,6 +1001,54 @@ public void Flyout_Is_Freed()
}
}

[Fact]
public void LayoutTransformControl_Is_Freed()
{
using (Start())
{
var transform = new RotateTransform { Angle = 90 };

Func<Window> run = () =>
{
var window = new Window
{
Content = new LayoutTransformControl
{
LayoutTransform = transform,
Child = new Canvas()
}
};

window.Show();

// Do a layout and make sure that LayoutTransformControl gets added to visual tree
window.LayoutManager.ExecuteInitialLayoutPass();
Assert.IsType<LayoutTransformControl>(window.Presenter.Child);
Assert.NotEmpty(window.Presenter.Child.GetVisualChildren());

// Clear the content and ensure the LayoutTransformControl is removed.
window.Content = null;
window.LayoutManager.ExecuteLayoutPass();
Assert.Null(window.Presenter.Child);

return window;
};

var result = run();

// Process all Loaded events to free control reference(s)
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);

dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<LayoutTransformControl>()).ObjectsCount));
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<Canvas>()).ObjectsCount));

// We are keeping transform alive to simulate a resource that outlives the control.
GC.KeepAlive(transform);
}
}

private FuncControlTemplate CreateWindowTemplate()
{
return new FuncControlTemplate<Window>((parent, scope) =>
Expand Down