committed by
GitHub
9 changed files with 698 additions and 74 deletions
@ -0,0 +1,24 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Avalonia.Layout |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Provides data for the <see cref="Layoutable.EffectiveViewportChanged"/> event.
|
||||
|
/// </summary>
|
||||
|
public class EffectiveViewportChangedEventArgs : EventArgs |
||||
|
{ |
||||
|
public EffectiveViewportChangedEventArgs(Rect effectiveViewport) |
||||
|
{ |
||||
|
EffectiveViewport = effectiveViewport; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the <see cref="Rect"/> representing the effective viewport.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// The viewport is expressed in coordinates relative to the control that the event is
|
||||
|
/// raised on.
|
||||
|
/// </remarks>
|
||||
|
public Rect EffectiveViewport { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,424 @@ |
|||||
|
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; |
||||
|
|
||||
|
namespace Avalonia.Layout.UnitTests |
||||
|
{ |
||||
|
public class LayoutableTests_EffectiveViewportChanged |
||||
|
{ |
||||
|
[Fact] |
||||
|
public async Task EffectiveViewportChanged_Not_Raised_When_Control_Added_To_Tree() |
||||
|
{ |
||||
|
await RunOnUIThread.Execute(async () => |
||||
|
{ |
||||
|
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_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 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() }; |
||||
|
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() |
||||
|
{ |
||||
|
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() }; |
||||
|
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() |
||||
|
{ |
||||
|
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() |
||||
|
{ |
||||
|
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) => ++raised; |
||||
|
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() |
||||
|
{ |
||||
|
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() |
||||
|
{ |
||||
|
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); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private TestRoot CreateRoot() => new TestRoot { Width = 1200, Height = 900 }; |
||||
|
|
||||
|
private Task ExecuteInitialLayoutPass(TestRoot root) |
||||
|
{ |
||||
|
root.LayoutManager.ExecuteInitialLayoutPass(); |
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
private Task ExecuteLayoutPass(TestRoot root) |
||||
|
{ |
||||
|
root.LayoutManager.ExecuteLayoutPass(); |
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
private Task ExecuteScrollerLayoutPass( |
||||
|
TestRoot root, |
||||
|
ScrollViewer scroller, |
||||
|
Control target, |
||||
|
Action<object, EffectiveViewportChangedEventArgs> handler) |
||||
|
{ |
||||
|
void ViewportChanged(object sender, EffectiveViewportChangedEventArgs e) |
||||
|
{ |
||||
|
handler(sender, e); |
||||
|
} |
||||
|
|
||||
|
target.EffectiveViewportChanged += ViewportChanged; |
||||
|
root.LayoutManager.ExecuteLayoutPass(); |
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
private IControlTemplate ScrollViewerTemplate() |
||||
|
{ |
||||
|
return new FuncControlTemplate<ScrollViewer>((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", |
||||
|
[~ContentPresenter.ContentProperty] = control[~ContentControl.ContentProperty], |
||||
|
[~~ScrollContentPresenter.ExtentProperty] = control[~~ScrollViewer.ExtentProperty], |
||||
|
[~~ScrollContentPresenter.OffsetProperty] = control[~~ScrollViewer.OffsetProperty], |
||||
|
[~~ScrollContentPresenter.ViewportProperty] = control[~~ScrollViewer.ViewportProperty], |
||||
|
[~ScrollContentPresenter.CanHorizontallyScrollProperty] = control[~ScrollViewer.CanHorizontallyScrollProperty], |
||||
|
[~ScrollContentPresenter.CanVerticallyScrollProperty] = control[~ScrollViewer.CanVerticallyScrollProperty], |
||||
|
}.RegisterInNameScope(scope), |
||||
|
new ScrollBar |
||||
|
{ |
||||
|
Name = "horizontalScrollBar", |
||||
|
Orientation = Orientation.Horizontal, |
||||
|
[~RangeBase.MaximumProperty] = control[~ScrollViewer.HorizontalScrollBarMaximumProperty], |
||||
|
[~~RangeBase.ValueProperty] = control[~~ScrollViewer.HorizontalScrollBarValueProperty], |
||||
|
[~ScrollBar.ViewportSizeProperty] = control[~ScrollViewer.HorizontalScrollBarViewportSizeProperty], |
||||
|
[~ScrollBar.VisibilityProperty] = control[~ScrollViewer.HorizontalScrollBarVisibilityProperty], |
||||
|
[Grid.RowProperty] = 1, |
||||
|
}.RegisterInNameScope(scope), |
||||
|
new ScrollBar |
||||
|
{ |
||||
|
Name = "verticalScrollBar", |
||||
|
Orientation = Orientation.Vertical, |
||||
|
[~RangeBase.MaximumProperty] = control[~ScrollViewer.VerticalScrollBarMaximumProperty], |
||||
|
[~~RangeBase.ValueProperty] = control[~~ScrollViewer.VerticalScrollBarValueProperty], |
||||
|
[~ScrollBar.ViewportSizeProperty] = control[~ScrollViewer.VerticalScrollBarViewportSizeProperty], |
||||
|
[~ScrollBar.VisibilityProperty] = control[~ScrollViewer.VerticalScrollBarVisibilityProperty], |
||||
|
[Grid.ColumnProperty] = 1, |
||||
|
}.RegisterInNameScope(scope), |
||||
|
}, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private 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<Task> func) |
||||
|
{ |
||||
|
await func(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue