diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index 888c8a4910..e7551d8e87 100644 --- a/src/Avalonia.Layout/LayoutManager.cs +++ b/src/Avalonia.Layout/LayoutManager.cs @@ -199,7 +199,9 @@ namespace Avalonia.Layout void ILayoutManager.RegisterEffectiveViewportListener(ILayoutable control) { _effectiveViewportChangedListeners ??= new List(); - _effectiveViewportChangedListeners.Add(new EffectiveViewportChangedListener(control)); + _effectiveViewportChangedListeners.Add(new EffectiveViewportChangedListener( + control, + CalculateEffectiveViewport(control))); } void ILayoutManager.UnregisterEffectiveViewportListener(ILayoutable control) @@ -331,8 +333,7 @@ namespace Avalonia.Layout for (var i = 0; i < _effectiveViewportChangedListeners.Count; ++i) { var l = _effectiveViewportChangedListeners[i]; - var viewport = new Rect(0, 0, double.PositiveInfinity, double.PositiveInfinity); - CalculateEffectiveViewport(l.Listener, ref viewport); + var viewport = CalculateEffectiveViewport(l.Listener); if (viewport != l.Viewport) { @@ -345,6 +346,13 @@ namespace Avalonia.Layout return startCount != _toMeasure.Count + _toMeasure.Count; } + private Rect CalculateEffectiveViewport(IVisual control) + { + var viewport = new Rect(0, 0, double.PositiveInfinity, double.PositiveInfinity); + CalculateEffectiveViewport(control, ref viewport); + return viewport; + } + private void CalculateEffectiveViewport(IVisual control, ref Rect viewport) { if (control.VisualParent is object) @@ -364,12 +372,6 @@ namespace Avalonia.Layout private readonly struct EffectiveViewportChangedListener { - public EffectiveViewportChangedListener(ILayoutable listener) - { - Listener = listener; - Viewport = new Rect(double.NaN, double.NaN, double.NaN, double.NaN); - } - public EffectiveViewportChangedListener(ILayoutable listener, Rect viewport) { Listener = listener; diff --git a/tests/Avalonia.Layout.UnitTests/LayoutableTests_EffectiveViewportChanged.cs b/tests/Avalonia.Layout.UnitTests/LayoutableTests_EffectiveViewportChanged.cs index 8226eb9c2a..0cd0f6c96a 100644 --- a/tests/Avalonia.Layout.UnitTests/LayoutableTests_EffectiveViewportChanged.cs +++ b/tests/Avalonia.Layout.UnitTests/LayoutableTests_EffectiveViewportChanged.cs @@ -1,7 +1,10 @@ -using Avalonia.Controls; +using System; +using System.Threading.Tasks; +using Avalonia.Controls; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Avalonia.Media; using Avalonia.UnitTests; using Xunit; @@ -10,257 +13,302 @@ namespace Avalonia.Layout.UnitTests public class LayoutableTests_EffectiveViewportChanged { [Fact] - public void EffectiveViewportChanged_Not_Raised_When_Control_Added_To_Tree() + public async Task EffectiveViewportChanged_Not_Raised_When_Control_Added_To_Tree() { - var root = new TestRoot(); - var canvas = new Canvas(); - var raised = 0; - - canvas.EffectiveViewportChanged += (s, e) => + await RunOnUIThread.Execute(async () => { - ++raised; - }; + var root = CreateRoot(); + var target = new Canvas(); + var raised = 0; + + target.EffectiveViewportChanged += (s, e) => + { + ++raised; + }; - root.Child = canvas; + root.Child = target; - Assert.Equal(0, raised); + Assert.Equal(0, raised); + }); } [Fact] - public void EffectiveViewportChanged_Raised_Before_LayoutUpdated() + public async Task EffectiveViewportChanged_Raised_Before_LayoutUpdated() { - var root = new TestRoot(); - var canvas = new Canvas(); - var raised = 0; - var layoutUpdatedRaised = 0; - - canvas.LayoutUpdated += (s, e) => + await RunOnUIThread.Execute(async () => { - Assert.Equal(1, raised); - ++layoutUpdatedRaised; - }; + var root = CreateRoot(); + var target = new Canvas(); + var raised = 0; - canvas.EffectiveViewportChanged += (s, e) => - { - ++raised; - }; + target.EffectiveViewportChanged += (s, e) => + { + ++raised; + }; - root.Child = canvas; - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.Child = target; - Assert.Equal(1, layoutUpdatedRaised); - Assert.Equal(1, raised); + await ExecuteInitialLayoutPass(root); + + Assert.Equal(1, raised); + }); } [Fact] - public void Invalidating_In_Handler_Causes_Layout_To_Be_Rerun_Before_LayoutUpdated() + public async Task Parent_Affects_EffectiveViewport() { - var root = new TestRoot(); - var canvas = new TestCanvas(); - var raised = 0; - var layoutUpdatedRaised = 0; - - canvas.LayoutUpdated += (s, e) => + await RunOnUIThread.Execute(async () => { - Assert.Equal(2, canvas.MeasureCount); - Assert.Equal(2, canvas.ArrangeCount); - ++layoutUpdatedRaised; - }; + var root = CreateRoot(); + var target = new Canvas { Width = 100, Height = 100 }; + var parent = new Border { Width = 200, Height = 200, Child = target }; + var raised = 0; - canvas.EffectiveViewportChanged += (s, e) => - { - canvas.InvalidateMeasure(); - ++raised; - }; + root.Child = parent; - root.Child = canvas; - root.LayoutManager.ExecuteInitialLayoutPass(root); + target.EffectiveViewportChanged += (s, e) => + { + Assert.Equal(new Rect(-550, -400, 1200, 900), e.EffectiveViewport); + ++raised; + }; - Assert.Equal(1, raised); - Assert.Equal(1, layoutUpdatedRaised); + await ExecuteInitialLayoutPass(root); + }); } [Fact] - public void Viewport_Extends_Beyond_Centered_Control() + public async Task Invalidating_In_Handler_Causes_Layout_To_Be_Rerun_Before_LayoutUpdated_Raised() { - var root = new TestRoot + await RunOnUIThread.Execute(async () => { - Width = 1200, - Height = 900, - }; + var root = CreateRoot(); + var target = new TestCanvas(); + var raised = 0; + var layoutUpdatedRaised = 0; - var canvas = new Canvas - { - Width = 52, - Height = 52, - }; - var raised = 0; + root.LayoutUpdated += (s, e) => + { + Assert.Equal(2, target.MeasureCount); + Assert.Equal(2, target.ArrangeCount); + ++layoutUpdatedRaised; + }; - canvas.EffectiveViewportChanged += (s, e) => - { - Assert.Equal(new Rect(-574, -424, 1200, 900), e.EffectiveViewport); - ++raised; - }; + target.EffectiveViewportChanged += (s, e) => + { + target.InvalidateMeasure(); + ++raised; + }; - root.Child = canvas; - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.Child = target; + + await ExecuteInitialLayoutPass(root); - Assert.Equal(1, raised); + Assert.Equal(1, raised); + Assert.Equal(1, layoutUpdatedRaised); + }); } [Fact] - public void Viewport_Extends_Beyond_Nested_Centered_Control() + public async Task Viewport_Extends_Beyond_Centered_Control() { - var root = new TestRoot + await RunOnUIThread.Execute(async () => { - Width = 1200, - Height = 900, - }; + var root = CreateRoot(); + var target = new Canvas { Width = 52, Height = 52, }; + var raised = 0; - var canvas = new Canvas - { - Width = 52, - Height = 52, - }; + target.EffectiveViewportChanged += (s, e) => + { + Assert.Equal(new Rect(-574, -424, 1200, 900), e.EffectiveViewport); + ++raised; + }; - var outer = new Border - { - Width = 100, - Height = 100, - Child = canvas, - }; + root.Child = target; - var raised = 0; + await ExecuteInitialLayoutPass(root); + Assert.Equal(1, raised); + }); + } - canvas.EffectiveViewportChanged += (s, e) => + [Fact] + public async Task Viewport_Extends_Beyond_Nested_Centered_Control() + { + await RunOnUIThread.Execute(async () => { - Assert.Equal(new Rect(-574, -424, 1200, 900), e.EffectiveViewport); - ++raised; - }; + var root = CreateRoot(); + var target = new Canvas { Width = 52, Height = 52 }; + var parent = new Border { Width = 100, Height = 100, Child = target }; + var raised = 0; - root.Child = outer; - root.LayoutManager.ExecuteInitialLayoutPass(root); + target.EffectiveViewportChanged += (s, e) => + { + Assert.Equal(new Rect(-574, -424, 1200, 900), e.EffectiveViewport); + ++raised; + }; + + root.Child = parent; - Assert.Equal(1, raised); + await ExecuteInitialLayoutPass(root); + Assert.Equal(1, raised); + }); } [Fact] - public void ScrollViewer_Determines_EffectiveViewport() + public async Task ScrollViewer_Determines_EffectiveViewport() { - var root = new TestRoot + await RunOnUIThread.Execute(async () => { - Width = 1200, - Height = 900, - }; + var root = CreateRoot(); + var target = new Canvas { Width = 200, Height = 200 }; + var scroller = new ScrollViewer { Width = 100, Height = 100, Content = target, Template = ScrollViewerTemplate() }; + var raised = 0; - var canvas = new Canvas - { - Width = 200, - Height = 200, - }; + target.EffectiveViewportChanged += (s, e) => + { + Assert.Equal(new Rect(0, 0, 100, 100), e.EffectiveViewport); + ++raised; + }; - var outer = new ScrollViewer - { - Width = 100, - Height = 100, - Content = canvas, - Template = ScrollViewerTemplate(), - }; + root.Child = scroller; - var raised = 0; + await ExecuteInitialLayoutPass(root); + Assert.Equal(1, raised); + }); + } - canvas.EffectiveViewportChanged += (s, e) => + [Fact] + public async Task Scrolled_ScrollViewer_Determines_EffectiveViewport() + { + await RunOnUIThread.Execute(async () => { - Assert.Equal(new Rect(0, 0, 100, 100), e.EffectiveViewport); - ++raised; - }; + var root = CreateRoot(); + var target = new Canvas { Width = 200, Height = 200 }; + var scroller = new ScrollViewer { Width = 100, Height = 100, Content = target, Template = ScrollViewerTemplate() }; + var raised = 0; - root.Child = outer; - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.Child = scroller; + + await ExecuteInitialLayoutPass(root); + scroller.Offset = new Vector(0, 10); - Assert.Equal(1, raised); + await ExecuteScrollerLayoutPass(root, scroller, target, (s, e) => + { + Assert.Equal(new Rect(0, 10, 100, 100), e.EffectiveViewport); + ++raised; + }); + + Assert.Equal(1, raised); + }); } [Fact] - public void Scrolled_ScrollViewer_Determines_EffectiveViewport() + public async Task Moving_Parent_Updates_EffectiveViewport() { - var root = new TestRoot + await RunOnUIThread.Execute(async () => { - Width = 1200, - Height = 900, - }; + var root = CreateRoot(); + var target = new Canvas { Width = 100, Height = 100 }; + var parent = new Border { Width = 200, Height = 200, Child = target }; + var raised = 0; - var canvas = new Canvas - { - Width = 200, - Height = 200, - }; + root.Child = parent; - var outer = new ScrollViewer - { - Width = 100, - Height = 100, - Content = canvas, - Template = ScrollViewerTemplate(), - }; + await ExecuteInitialLayoutPass(root); - var raised = 0; + target.EffectiveViewportChanged += (s, e) => + { + Assert.Equal(new Rect(-554, -400, 1200, 900), e.EffectiveViewport); + ++raised; + }; - root.Child = outer; - root.LayoutManager.ExecuteInitialLayoutPass(root); + parent.Margin = new Thickness(8, 0, 0, 0); + await ExecuteLayoutPass(root); - canvas.EffectiveViewportChanged += (s, e) => + Assert.Equal(1, raised); + }); + } + + [Fact] + public async Task Translate_Transform_Doesnt_Affect_EffectiveViewport() + { + await RunOnUIThread.Execute(async () => { - Assert.Equal(new Rect(0, 10, 100, 100), e.EffectiveViewport); - ++raised; - }; - - outer.Offset = new Vector(0, 10); - root.LayoutManager.ExecuteLayoutPass(); + var root = CreateRoot(); + var target = new Canvas { Width = 100, Height = 100 }; + var parent = new Border { Width = 200, Height = 200, Child = target }; + var raised = 0; - Assert.Equal(1, raised); + root.Child = parent; + + await ExecuteInitialLayoutPass(root); + target.EffectiveViewportChanged += (s, e) => ++raised; + target.RenderTransform = new TranslateTransform { X = 8 }; + target.InvalidateMeasure(); + await ExecuteLayoutPass(root); + + Assert.Equal(0, raised); + }); } [Fact] - public void Moving_Parent_Updates_EffectiveViewport() + public async Task Translate_Transform_On_Parent_Affects_EffectiveViewport() { - var root = new TestRoot + await RunOnUIThread.Execute(async () => { - Width = 1200, - Height = 900, - }; + var root = CreateRoot(); + var target = new Canvas { Width = 100, Height = 100 }; + var parent = new Border { Width = 200, Height = 200, Child = target }; + var raised = 0; - var canvas = new Canvas - { - Width = 100, - Height = 100, - }; + root.Child = parent; - var outer = new Border - { - Width = 200, - Height = 200, - Child = canvas, - }; + await ExecuteInitialLayoutPass(root); - var raised = 0; + 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); + }); + } - root.Child = outer; + private TestRoot CreateRoot() => new TestRoot { Width = 1200, Height = 900 }; + + private Task ExecuteInitialLayoutPass(TestRoot root) + { root.LayoutManager.ExecuteInitialLayoutPass(root); + return Task.CompletedTask; + } + + private Task ExecuteLayoutPass(TestRoot root) + { + root.LayoutManager.ExecuteLayoutPass(); + return Task.CompletedTask; + } - canvas.EffectiveViewportChanged += (s, e) => + private Task ExecuteScrollerLayoutPass( + TestRoot root, + ScrollViewer scroller, + Control target, + Action handler) + { + void ViewportChanged(object sender, EffectiveViewportChangedEventArgs e) { - Assert.Equal(new Rect(-554, -400, 1200, 900), e.EffectiveViewport); - ++raised; - }; + handler(sender, e); + } - // Change the parent margin to move it. - outer.Margin = new Thickness(8, 0, 0, 0); + target.EffectiveViewportChanged += ViewportChanged; root.LayoutManager.ExecuteLayoutPass(); - - Assert.Equal(1, raised); + return Task.CompletedTask; } - private IControlTemplate ScrollViewerTemplate() { return new FuncControlTemplate((control, scope) => new Grid @@ -329,5 +377,13 @@ namespace Avalonia.Layout.UnitTests return base.ArrangeOverride(finalSize); } } + + private static class RunOnUIThread + { + public static async Task Execute(Func func) + { + await func(); + } + } } }