Browse Source

Merge pull request #4173 from AvaloniaUI/feature/effectiveviewportchanged

Added EffectiveViewportChanged event.
pull/4178/head
Dariusz Komosiński 6 years ago
committed by GitHub
parent
commit
69ee16adca
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 60
      src/Avalonia.Controls/Repeater/ViewportManager.cs
  2. 24
      src/Avalonia.Layout/EffectiveViewportChangedEventArgs.cs
  3. 12
      src/Avalonia.Layout/ILayoutManager.cs
  4. 7
      src/Avalonia.Layout/ILayoutable.cs
  5. 160
      src/Avalonia.Layout/LayoutManager.cs
  6. 70
      src/Avalonia.Layout/Layoutable.cs
  7. 424
      tests/Avalonia.Layout.UnitTests/LayoutableTests_EffectiveViewportChanged.cs

60
src/Avalonia.Controls/Repeater/ViewportManager.cs

@ -49,8 +49,8 @@ namespace Avalonia.Controls
// For non-virtualizing layouts, we do not need to keep
// updating viewports and invalidating measure often. So when
// a non virtualizing layout is used, we stop doing all that work.
bool _managingViewportDisabled;
private IDisposable _effectiveViewportChangedRevoker;
private bool _managingViewportDisabled;
private bool _effectiveViewportChangedSubscribed;
private bool _layoutUpdatedSubscribed;
public ViewportManager(ItemsRepeater owner)
@ -228,11 +228,15 @@ namespace Avalonia.Controls
_pendingViewportShift = default;
_unshiftableShift = default;
_effectiveViewportChangedRevoker?.Dispose();
if (!_managingViewportDisabled)
if (_managingViewportDisabled && _effectiveViewportChangedSubscribed)
{
_owner.EffectiveViewportChanged -= OnEffectiveViewportChanged;
_effectiveViewportChangedSubscribed = false;
}
else if (!_managingViewportDisabled && !_effectiveViewportChangedSubscribed)
{
_effectiveViewportChangedRevoker = SubscribeToEffectiveViewportChanged(_owner);
_owner.EffectiveViewportChanged += OnEffectiveViewportChanged;
_effectiveViewportChangedSubscribed = true;
}
}
@ -420,15 +424,15 @@ namespace Avalonia.Controls
_scroller = null;
}
_effectiveViewportChangedRevoker?.Dispose();
_effectiveViewportChangedRevoker = null;
_owner.EffectiveViewportChanged -= OnEffectiveViewportChanged;
_effectiveViewportChangedSubscribed = false;
_ensuredScroller = false;
}
private void OnEffectiveViewportChanged(Rect effectiveViewport)
private void OnEffectiveViewportChanged(object sender, EffectiveViewportChangedEventArgs e)
{
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: EffectiveViewportChanged event callback", _owner.Layout.LayoutId);
UpdateViewport(effectiveViewport);
UpdateViewport(e.EffectiveViewport);
_pendingViewportShift = default;
_unshiftableShift = default;
@ -473,8 +477,8 @@ namespace Avalonia.Controls
}
else if (!_managingViewportDisabled)
{
_effectiveViewportChangedRevoker?.Dispose();
_effectiveViewportChangedRevoker = SubscribeToEffectiveViewportChanged(_owner);
_owner.EffectiveViewportChanged += OnEffectiveViewportChanged;
_effectiveViewportChangedSubscribed = true;
}
_ensuredScroller = true;
@ -534,38 +538,6 @@ namespace Avalonia.Controls
}
}
private IDisposable SubscribeToEffectiveViewportChanged(IControl control)
{
// HACK: This is a bit of a hack. We need the effective viewport of the ItemsRepeater -
// we can get this from TransformedBounds, but this property is updated after layout has
// run, which is too late. Instead, for now lets just hook into an internal event on
// ScrollContentPresenter to find out what the offset and viewport will be after arrange
// and use those values. Note that this doesn't handle nested ScrollViewers at all, but
// it's enough to get scrolling to non-uniformly sized items working for now.
//
// UWP uses the EffectiveViewportChanged event (which I think was implemented specially
// for this case): we need to implement that in Avalonia, but the semantics of it aren't
// clear to me. Hopefully the source for this event will be released with WinUI 3.
if (control.VisualParent is ScrollContentPresenter scp)
{
scp.PreArrange += ScrollContentPresenterPreArrange;
return Disposable.Create(() => scp.PreArrange -= ScrollContentPresenterPreArrange);
}
return Disposable.Empty;
}
private void ScrollContentPresenterPreArrange(object sender, VectorEventArgs e)
{
var scp = (ScrollContentPresenter)sender;
var effectiveViewport = new Rect((Point)scp.Offset, new Size(e.Vector.X, e.Vector.Y));
if (effectiveViewport != _visibleWindow)
{
OnEffectiveViewportChanged(effectiveViewport);
}
}
private class ScrollerInfo
{
public ScrollerInfo(ScrollViewer scroller)

24
src/Avalonia.Layout/EffectiveViewportChangedEventArgs.cs

@ -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; }
}
}

12
src/Avalonia.Layout/ILayoutManager.cs

@ -54,5 +54,17 @@ namespace Avalonia.Layout
/// </remarks>
[Obsolete("Call ExecuteInitialLayoutPass without parameter")]
void ExecuteInitialLayoutPass(ILayoutRoot root);
/// <summary>
/// Registers a control as wanting to receive effective viewport notifications.
/// </summary>
/// <param name="control">The control.</param>
void RegisterEffectiveViewportListener(ILayoutable control);
/// <summary>
/// Registers a control as no longer wanting to receive effective viewport notifications.
/// </summary>
/// <param name="control">The control.</param>
void UnregisterEffectiveViewportListener(ILayoutable control);
}
}

7
src/Avalonia.Layout/ILayoutable.cs

@ -111,5 +111,12 @@ namespace Avalonia.Layout
/// </summary>
/// <param name="control">The child control.</param>
void ChildDesiredSizeChanged(ILayoutable control);
/// <summary>
/// Used by the <see cref="LayoutManager"/> to notify the control that its effective
/// viewport is changed.
/// </summary>
/// <param name="e">The viewport information.</param>
void EffectiveViewportChanged(EffectiveViewportChangedEventArgs e);
}
}

160
src/Avalonia.Layout/LayoutManager.cs

@ -1,7 +1,10 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using Avalonia.Logging;
using Avalonia.Threading;
using Avalonia.VisualTree;
#nullable enable
@ -12,10 +15,12 @@ namespace Avalonia.Layout
/// </summary>
public class LayoutManager : ILayoutManager, IDisposable
{
private const int MaxPasses = 3;
private readonly ILayoutRoot _owner;
private readonly LayoutQueue<ILayoutable> _toMeasure = new LayoutQueue<ILayoutable>(v => !v.IsMeasureValid);
private readonly LayoutQueue<ILayoutable> _toArrange = new LayoutQueue<ILayoutable>(v => !v.IsArrangeValid);
private readonly Action _executeLayoutPass;
private List<EffectiveViewportChangedListener>? _effectiveViewportChangedListeners;
private bool _disposed;
private bool _queued;
private bool _running;
@ -92,8 +97,6 @@ namespace Avalonia.Layout
/// <inheritdoc/>
public virtual void ExecuteLayoutPass()
{
const int MaxPasses = 3;
Dispatcher.UIThread.VerifyAccess();
if (_disposed)
@ -125,23 +128,15 @@ namespace Avalonia.Layout
_toMeasure.BeginLoop(MaxPasses);
_toArrange.BeginLoop(MaxPasses);
try
for (var pass = 0; pass < MaxPasses; ++pass)
{
for (var pass = 0; pass < MaxPasses; ++pass)
{
ExecuteMeasurePass();
ExecuteArrangePass();
InnerLayoutPass();
if (_toMeasure.Count == 0)
{
break;
}
if (!RaiseEffectiveViewportChanged())
{
break;
}
}
finally
{
_running = false;
}
_toMeasure.EndLoop();
_toArrange.EndLoop();
@ -202,6 +197,49 @@ namespace Avalonia.Layout
_toArrange.Dispose();
}
void ILayoutManager.RegisterEffectiveViewportListener(ILayoutable control)
{
_effectiveViewportChangedListeners ??= new List<EffectiveViewportChangedListener>();
_effectiveViewportChangedListeners.Add(new EffectiveViewportChangedListener(
control,
CalculateEffectiveViewport(control)));
}
void ILayoutManager.UnregisterEffectiveViewportListener(ILayoutable control)
{
if (_effectiveViewportChangedListeners is object)
{
for (var i = _effectiveViewportChangedListeners.Count - 1; i >= 0; --i)
{
if (_effectiveViewportChangedListeners[i].Listener == control)
{
_effectiveViewportChangedListeners.RemoveAt(i);
}
}
}
}
private void InnerLayoutPass()
{
try
{
for (var pass = 0; pass < MaxPasses; ++pass)
{
ExecuteMeasurePass();
ExecuteArrangePass();
if (_toMeasure.Count == 0)
{
break;
}
}
}
finally
{
_running = false;
}
}
private void ExecuteMeasurePass()
{
while (_toMeasure.Count > 0)
@ -285,5 +323,97 @@ namespace Avalonia.Layout
_queued = true;
}
}
private bool RaiseEffectiveViewportChanged()
{
var startCount = _toMeasure.Count + _toArrange.Count;
if (_effectiveViewportChangedListeners is object)
{
var count = _effectiveViewportChangedListeners.Count;
var pool = ArrayPool<EffectiveViewportChangedListener>.Shared;
var listeners = pool.Rent(count);
_effectiveViewportChangedListeners.CopyTo(listeners);
try
{
for (var i = 0; i < count; ++i)
{
var l = _effectiveViewportChangedListeners[i];
if (!l.Listener.IsAttachedToVisualTree)
{
continue;
}
var viewport = CalculateEffectiveViewport(l.Listener);
if (viewport != l.Viewport)
{
l.Listener.EffectiveViewportChanged(new EffectiveViewportChangedEventArgs(viewport));
_effectiveViewportChangedListeners[i] = new EffectiveViewportChangedListener(l.Listener, viewport);
}
}
}
finally
{
pool.Return(listeners, clearArray: true);
}
}
return startCount != _toMeasure.Count + _toMeasure.Count;
}
private Rect CalculateEffectiveViewport(IVisual control)
{
var viewport = new Rect(0, 0, double.PositiveInfinity, double.PositiveInfinity);
CalculateEffectiveViewport(control, control, ref viewport);
return viewport;
}
private void CalculateEffectiveViewport(IVisual target, IVisual control, ref Rect viewport)
{
// Recurse until the top level control.
if (control.VisualParent is object)
{
CalculateEffectiveViewport(target, control.VisualParent, ref viewport);
}
else
{
viewport = new Rect(control.Bounds.Size);
}
// Apply the control clip bounds if it's not the target control. We don't apply it to
// the target control because it may itself be clipped to bounds and if so the viewport
// we calculate would be of no use.
if (control != target && control.ClipToBounds)
{
viewport = control.Bounds.Intersect(viewport);
}
// Translate the viewport into this control's coordinate space.
viewport = viewport.Translate(-control.Bounds.Position);
if (control != target && control.RenderTransform is object)
{
var origin = control.RenderTransformOrigin.ToPixels(control.Bounds.Size);
var offset = Matrix.CreateTranslation(origin);
var renderTransform = (-offset) * control.RenderTransform.Value.Invert() * (offset);
viewport = viewport.TransformToAABB(renderTransform);
}
}
private readonly struct EffectiveViewportChangedListener
{
public EffectiveViewportChangedListener(ILayoutable listener, Rect viewport)
{
Listener = listener;
Viewport = viewport;
}
public ILayoutable Listener { get; }
public Rect Viewport { get; }
}
}
}

70
src/Avalonia.Layout/Layoutable.cs

@ -132,6 +132,7 @@ namespace Avalonia.Layout
private bool _measuring;
private Size? _previousMeasure;
private Rect? _previousArrange;
private EventHandler<EffectiveViewportChangedEventArgs>? _effectiveViewportChanged;
private EventHandler? _layoutUpdated;
/// <summary>
@ -152,6 +153,32 @@ namespace Avalonia.Layout
VerticalAlignmentProperty);
}
/// <summary>
/// Occurs when the element's effective viewport changes.
/// </summary>
public event EventHandler<EffectiveViewportChangedEventArgs>? EffectiveViewportChanged
{
add
{
if (_effectiveViewportChanged is null && VisualRoot is ILayoutRoot r)
{
r.LayoutManager.RegisterEffectiveViewportListener(this);
}
_effectiveViewportChanged += value;
}
remove
{
_effectiveViewportChanged -= value;
if (_effectiveViewportChanged is null && VisualRoot is ILayoutRoot r)
{
r.LayoutManager.UnregisterEffectiveViewportListener(this);
}
}
}
/// <summary>
/// Occurs when a layout pass completes for the control.
/// </summary>
@ -384,13 +411,6 @@ namespace Avalonia.Layout
}
}
/// <summary>
/// Called by InvalidateMeasure
/// </summary>
protected virtual void OnMeasureInvalidated()
{
}
/// <summary>
/// Invalidates the measurement of the control and queues a new layout pass.
/// </summary>
@ -436,6 +456,11 @@ namespace Avalonia.Layout
}
}
void ILayoutable.EffectiveViewportChanged(EffectiveViewportChangedEventArgs e)
{
_effectiveViewportChanged?.Invoke(this, e);
}
/// <summary>
/// Marks a property as affecting the control's measurement.
/// </summary>
@ -717,9 +742,17 @@ namespace Avalonia.Layout
{
base.OnAttachedToVisualTreeCore(e);
if (_layoutUpdated is object && e.Root is ILayoutRoot r)
if (e.Root is ILayoutRoot r)
{
r.LayoutManager.LayoutUpdated += LayoutManagedLayoutUpdated;
if (_layoutUpdated is object)
{
r.LayoutManager.LayoutUpdated += LayoutManagedLayoutUpdated;
}
if (_effectiveViewportChanged is object)
{
r.LayoutManager.RegisterEffectiveViewportListener(this);
}
}
}
@ -727,12 +760,27 @@ namespace Avalonia.Layout
{
base.OnDetachedFromVisualTreeCore(e);
if (_layoutUpdated is object && e.Root is ILayoutRoot r)
if (e.Root is ILayoutRoot r)
{
r.LayoutManager.LayoutUpdated -= LayoutManagedLayoutUpdated;
if (_layoutUpdated is object)
{
r.LayoutManager.LayoutUpdated -= LayoutManagedLayoutUpdated;
}
if (_effectiveViewportChanged is object)
{
r.LayoutManager.UnregisterEffectiveViewportListener(this);
}
}
}
/// <summary>
/// Called by InvalidateMeasure
/// </summary>
protected virtual void OnMeasureInvalidated()
{
}
/// <inheritdoc/>
protected sealed override void OnVisualParentChanged(IVisual oldParent, IVisual newParent)
{

424
tests/Avalonia.Layout.UnitTests/LayoutableTests_EffectiveViewportChanged.cs

@ -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…
Cancel
Save