using System; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Layout; using Avalonia.Media; using Avalonia.UnitTests; using Xunit; namespace Avalonia.Base.UnitTests.Layout { public class LayoutableTests_EffectiveViewportChanged { [Fact] public async Task EffectiveViewportChanged_Not_Raised_When_Control_Added_To_Tree_And_Layout_Pass_Has_Not_Run() { #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously await RunOnUIThread.Execute(async () => #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously { var root = CreateRoot(); var target = new Canvas(); var raised = 0; target.EffectiveViewportChanged += (s, e) => { ++raised; }; root.Child = target; Assert.Equal(0, raised); }); } [Fact] public async Task EffectiveViewportChanged_Raised_When_Control_Added_To_Tree_And_Layout_Pass_Has_Run() { #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously await RunOnUIThread.Execute(async () => #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously { var root = CreateRoot(); var target = new Canvas(); var raised = 0; target.EffectiveViewportChanged += (s, e) => { ++raised; }; root.Child = target; Assert.Equal(0, raised); await ExecuteInitialLayoutPass(root); Assert.Equal(1, raised); }); } [Fact] public async Task EffectiveViewportChanged_Raised_When_Root_LayedOut_And_Then_Control_Added_To_Tree_And_Layout_Pass_Runs() { #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously await RunOnUIThread.Execute(async () => #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously { var root = CreateRoot(); var target = new Canvas(); var raised = 0; target.EffectiveViewportChanged += (s, e) => { ++raised; }; await ExecuteInitialLayoutPass(root); root.Child = target; Assert.Equal(0, raised); await ExecuteInitialLayoutPass(root); Assert.Equal(1, raised); }); } [Fact] public async Task EffectiveViewportChanged_Raised_Before_LayoutUpdated() { await RunOnUIThread.Execute(async () => { var root = CreateRoot(); var target = new Canvas(); var raised = 0; target.EffectiveViewportChanged += (s, e) => { ++raised; }; root.Child = target; await ExecuteInitialLayoutPass(root); Assert.Equal(1, raised); }); } [Fact] public async Task EffectiveViewportChanged_Should_Not_Be_Raised_Twice_If_Subcribed_In_AttachedToVisualTree() { await RunOnUIThread.Execute(async () => { var root = CreateRoot(); var target = new Canvas(); var raised = 0; target.AttachedToVisualTree += (_, _) => { target.EffectiveViewportChanged += (_, _) => ++raised; }; root.Child = target; await ExecuteInitialLayoutPass(root); Assert.Equal(1, raised); }); } [Fact] public async Task Parent_Affects_EffectiveViewport() { await RunOnUIThread.Execute(async () => { var root = CreateRoot(); var target = new Canvas { Width = 100, Height = 100 }; var parent = new Border { Width = 200, Height = 200, Child = target }; var raised = 0; root.Child = parent; target.EffectiveViewportChanged += (s, e) => { Assert.Equal(new Rect(-550, -400, 1200, 900), e.EffectiveViewport); ++raised; }; await ExecuteInitialLayoutPass(root); }); } [Fact] public async Task Invalidating_In_Handler_Causes_Layout_To_Be_Rerun_Before_LayoutUpdated_Raised() { await RunOnUIThread.Execute(async () => { var root = CreateRoot(); var target = new TestCanvas(); var raised = 0; var layoutUpdatedRaised = 0; root.LayoutUpdated += (s, e) => { Assert.Equal(2, target.MeasureCount); Assert.Equal(2, target.ArrangeCount); ++layoutUpdatedRaised; }; target.EffectiveViewportChanged += (s, e) => { target.InvalidateMeasure(); ++raised; }; root.Child = target; await ExecuteInitialLayoutPass(root); Assert.Equal(1, raised); Assert.Equal(1, layoutUpdatedRaised); }); } [Fact] public async Task Viewport_Extends_Beyond_Centered_Control() { await RunOnUIThread.Execute(async () => { var root = CreateRoot(); var target = new Canvas { Width = 52, Height = 52, }; var raised = 0; target.EffectiveViewportChanged += (s, e) => { Assert.Equal(new Rect(-574, -424, 1200, 900), e.EffectiveViewport); ++raised; }; root.Child = target; await ExecuteInitialLayoutPass(root); Assert.Equal(1, raised); }); } [Fact] public async Task Viewport_Extends_Beyond_Nested_Centered_Control() { await RunOnUIThread.Execute(async () => { var root = CreateRoot(); var target = new Canvas { Width = 52, Height = 52 }; var parent = new Border { Width = 100, Height = 100, Child = target }; var raised = 0; target.EffectiveViewportChanged += (s, e) => { Assert.Equal(new Rect(-574, -424, 1200, 900), e.EffectiveViewport); ++raised; }; root.Child = parent; await ExecuteInitialLayoutPass(root); Assert.Equal(1, raised); }); } [Fact] public async Task ScrollViewer_Determines_EffectiveViewport() { await RunOnUIThread.Execute(async () => { var root = CreateRoot(); var target = new Canvas { Width = 200, Height = 200 }; var scroller = new ScrollViewer { Width = 100, Height = 100, Content = target, Template = ScrollViewerTemplate(), HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden }; var raised = 0; target.EffectiveViewportChanged += (s, e) => { Assert.Equal(new Rect(0, 0, 100, 100), e.EffectiveViewport); ++raised; }; root.Child = scroller; await ExecuteInitialLayoutPass(root); Assert.Equal(1, raised); }); } [Fact] public async Task Scrolled_ScrollViewer_Determines_EffectiveViewport() { using var scope = AvaloniaLocator.EnterScope(); await RunOnUIThread.Execute(async () => { var root = CreateRoot(); var target = new Canvas { Width = 200, Height = 200 }; var scroller = new ScrollViewer { Width = 100, Height = 100, Content = target, Template = ScrollViewerTemplate(), HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden }; var raised = 0; root.Child = scroller; await ExecuteInitialLayoutPass(root); scroller.Offset = new Vector(0, 10); await ExecuteScrollerLayoutPass(root, scroller, target, (s, e) => { Assert.Equal(new Rect(0, 10, 100, 100), e.EffectiveViewport); ++raised; }); Assert.Equal(1, raised); }); } [Fact] public async Task Moving_Parent_Updates_EffectiveViewport() { using var scope = AvaloniaLocator.EnterScope(); await RunOnUIThread.Execute(async () => { var root = CreateRoot(); var target = new Canvas { Width = 100, Height = 100 }; var parent = new Border { Width = 200, Height = 200, Child = target }; var raised = 0; root.Child = parent; await ExecuteInitialLayoutPass(root); target.EffectiveViewportChanged += (s, e) => { Assert.Equal(new Rect(-554, -400, 1200, 900), e.EffectiveViewport); ++raised; }; parent.Margin = new Thickness(8, 0, 0, 0); await ExecuteLayoutPass(root); Assert.Equal(1, raised); }); } [Fact] public async Task Translate_Transform_Doesnt_Affect_EffectiveViewport() { using var scope = AvaloniaLocator.EnterScope(); await RunOnUIThread.Execute(async () => { var root = CreateRoot(); var target = new Canvas { Width = 100, Height = 100 }; var parent = new Border { Width = 200, Height = 200, Child = target }; var raised = 0; root.Child = parent; target.EffectiveViewportChanged += (s, e) => ++raised; await ExecuteInitialLayoutPass(root); raised = 0; // The initial layout pass is expected to raise. target.RenderTransform = new TranslateTransform { X = 8 }; target.InvalidateMeasure(); await ExecuteLayoutPass(root); Assert.Equal(0, raised); }); } [Fact] public async Task Translate_Transform_On_Parent_Affects_EffectiveViewport() { using var scope = AvaloniaLocator.EnterScope(); await RunOnUIThread.Execute(async () => { var root = CreateRoot(); var target = new Canvas { Width = 100, Height = 100 }; var parent = new Border { Width = 200, Height = 200, Child = target }; var raised = 0; root.Child = parent; await ExecuteInitialLayoutPass(root); target.EffectiveViewportChanged += (s, e) => { Assert.Equal(new Rect(-558, -400, 1200, 900), e.EffectiveViewport); ++raised; }; // Change the parent render transform to move it. A layout is then needed before // EffectiveViewportChanged is raised. parent.RenderTransform = new TranslateTransform { X = 8 }; parent.InvalidateMeasure(); await ExecuteLayoutPass(root); Assert.Equal(1, raised); }); } [Fact] public async Task Rotate_Transform_On_Parent_Affects_EffectiveViewport() { using var scope = AvaloniaLocator.EnterScope(); await RunOnUIThread.Execute(async () => { var root = CreateRoot(); var target = new Canvas { Width = 100, Height = 100 }; var parent = new Border { Width = 200, Height = 200, Child = target }; var raised = 0; root.Child = parent; await ExecuteInitialLayoutPass(root); target.EffectiveViewportChanged += (s, e) => { AssertArePixelEqual(new Rect(-651, -792, 1484, 1484), e.EffectiveViewport); ++raised; }; parent.RenderTransformOrigin = new RelativePoint(0, 0, RelativeUnit.Absolute); parent.RenderTransform = new RotateTransform { Angle = 45 }; parent.InvalidateMeasure(); await ExecuteLayoutPass(root); Assert.Equal(1, raised); }); } [Fact] public async Task Event_Unsubscribed_While_Inside_Callback() { await RunOnUIThread.Execute(async () => { var root = CreateRoot(); var target = new Canvas(); var raised = 0; void OnTargetOnEffectiveViewportChanged(object s, EffectiveViewportChangedEventArgs e) { target.EffectiveViewportChanged -= OnTargetOnEffectiveViewportChanged; ++raised; } target.EffectiveViewportChanged += OnTargetOnEffectiveViewportChanged; root.Child = target; await ExecuteInitialLayoutPass(root); Assert.Equal(1, raised); }); } // https://github.com/AvaloniaUI/Avalonia/issues/12452 [Fact] public async Task Zero_ScaleTransform_Sets_Empty_EffectiveViewport() { await RunOnUIThread.Execute(async () => { var effectiveViewport = new Rect(Size.Infinity); var root = CreateRoot(); var target = new Canvas { Width = 100, Height = 100 }; var parent = new Border { Width = 100, Height = 100, Child = target }; target.EffectiveViewportChanged += (_, e) => effectiveViewport = e.EffectiveViewport; root.Child = parent; await ExecuteInitialLayoutPass(root); parent.RenderTransform = new ScaleTransform(0, 0); await ExecuteLayoutPass(root); Assert.Equal(new Rect(0, 0, 0, 0), effectiveViewport); }); } private static TestRoot CreateRoot() => new TestRoot { Width = 1200, Height = 900 }; private static Task ExecuteInitialLayoutPass(TestRoot root) { root.LayoutManager.ExecuteInitialLayoutPass(); return Task.CompletedTask; } private static Task ExecuteLayoutPass(TestRoot root) { root.LayoutManager.ExecuteLayoutPass(); return Task.CompletedTask; } private static Task ExecuteScrollerLayoutPass( TestRoot root, ScrollViewer scroller, Control target, Action handler) { void ViewportChanged(object sender, EffectiveViewportChangedEventArgs e) { handler(sender, e); } target.EffectiveViewportChanged += ViewportChanged; root.LayoutManager.ExecuteLayoutPass(); return Task.CompletedTask; } private static IControlTemplate ScrollViewerTemplate() { return new FuncControlTemplate((control, scope) => new Grid { ColumnDefinitions = new ColumnDefinitions { new ColumnDefinition(1, GridUnitType.Star), new ColumnDefinition(GridLength.Auto), }, RowDefinitions = new RowDefinitions { new RowDefinition(1, GridUnitType.Star), new RowDefinition(GridLength.Auto), }, Children = { new ScrollContentPresenter { Name = "PART_ContentPresenter", }.RegisterInNameScope(scope), new ScrollBar { Name = "horizontalScrollBar", Orientation = Orientation.Horizontal, [Grid.RowProperty] = 1, }.RegisterInNameScope(scope), new ScrollBar { Name = "verticalScrollBar", Orientation = Orientation.Vertical, [Grid.ColumnProperty] = 1, }.RegisterInNameScope(scope), }, }); } private static void AssertArePixelEqual(Rect expected, Rect actual) { var expectedRounded = new Rect((int)expected.X, (int)expected.Y, (int)expected.Width, (int)expected.Height); var actualRounded = new Rect((int)actual.X, (int)actual.Y, (int)actual.Width, (int)actual.Height); Assert.Equal(expectedRounded, actualRounded); } private class TestCanvas : Canvas { public int MeasureCount { get; private set; } public int ArrangeCount { get; private set; } protected override Size MeasureOverride(Size availableSize) { ++MeasureCount; return base.MeasureOverride(availableSize); } protected override Size ArrangeOverride(Size finalSize) { ++ArrangeCount; return base.ArrangeOverride(finalSize); } } private static class RunOnUIThread { public static async Task Execute(Func func) { await func(); } } } }