src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs (797 lines of code) (raw):
using System;
using System.Collections.Generic;
using Avalonia.Reactive;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Input.GestureRecognizers;
using Avalonia.Utilities;
using Avalonia.VisualTree;
using System.Linq;
using Avalonia.Layout;
namespace Avalonia.Controls.Presenters
{
/// <summary>
/// Presents a scrolling view of content inside a <see cref="ScrollViewer"/>.
/// </summary>
public class ScrollContentPresenter : ContentPresenter, IScrollable, IScrollAnchorProvider
{
private const double EdgeDetectionTolerance = 0.1;
/// <summary>
/// Defines the <see cref="CanHorizontallyScroll"/> property.
/// </summary>
public static readonly StyledProperty<bool> CanHorizontallyScrollProperty =
AvaloniaProperty.Register<ScrollContentPresenter, bool>(nameof(CanHorizontallyScroll));
/// <summary>
/// Defines the <see cref="CanVerticallyScroll"/> property.
/// </summary>
public static readonly StyledProperty<bool> CanVerticallyScrollProperty =
AvaloniaProperty.Register<ScrollContentPresenter, bool>(nameof(CanVerticallyScroll));
/// <summary>
/// Defines the <see cref="Extent"/> property.
/// </summary>
public static readonly DirectProperty<ScrollContentPresenter, Size> ExtentProperty =
ScrollViewer.ExtentProperty.AddOwner<ScrollContentPresenter>(
o => o.Extent);
/// <summary>
/// Defines the <see cref="Offset"/> property.
/// </summary>
public static readonly StyledProperty<Vector> OffsetProperty =
ScrollViewer.OffsetProperty.AddOwner<ScrollContentPresenter>(new(coerce: ScrollViewer.CoerceOffset));
/// <summary>
/// Defines the <see cref="Viewport"/> property.
/// </summary>
public static readonly DirectProperty<ScrollContentPresenter, Size> ViewportProperty =
ScrollViewer.ViewportProperty.AddOwner<ScrollContentPresenter>(
o => o.Viewport);
/// <summary>
/// Defines the <see cref="HorizontalSnapPointsType"/> property.
/// </summary>
public static readonly StyledProperty<SnapPointsType> HorizontalSnapPointsTypeProperty =
ScrollViewer.HorizontalSnapPointsTypeProperty.AddOwner<ScrollContentPresenter>();
/// <summary>
/// Defines the <see cref="VerticalSnapPointsType"/> property.
/// </summary>
public static readonly StyledProperty<SnapPointsType> VerticalSnapPointsTypeProperty =
ScrollViewer.VerticalSnapPointsTypeProperty.AddOwner<ScrollContentPresenter>();
/// <summary>
/// Defines the <see cref="HorizontalSnapPointsAlignment"/> property.
/// </summary>
public static readonly StyledProperty<SnapPointsAlignment> HorizontalSnapPointsAlignmentProperty =
ScrollViewer.HorizontalSnapPointsAlignmentProperty.AddOwner<ScrollContentPresenter>();
/// <summary>
/// Defines the <see cref="VerticalSnapPointsAlignment"/> property.
/// </summary>
public static readonly StyledProperty<SnapPointsAlignment> VerticalSnapPointsAlignmentProperty =
ScrollViewer.VerticalSnapPointsAlignmentProperty.AddOwner<ScrollContentPresenter>();
/// <summary>
/// Defines the <see cref="IsScrollChainingEnabled"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsScrollChainingEnabledProperty =
ScrollViewer.IsScrollChainingEnabledProperty.AddOwner<ScrollContentPresenter>();
private bool _arranging;
private Size _extent;
private IDisposable? _logicalScrollSubscription;
private Size _viewport;
private Dictionary<int, Vector>? _activeLogicalGestureScrolls;
private Dictionary<int, Vector>? _scrollGestureSnapPoints;
private HashSet<Control>? _anchorCandidates;
private Control? _anchorElement;
private Rect _anchorElementBounds;
private bool _isAnchorElementDirty;
private bool _areVerticalSnapPointsRegular;
private bool _areHorizontalSnapPointsRegular;
private IReadOnlyList<double>? _horizontalSnapPoints;
private double _horizontalSnapPoint;
private IReadOnlyList<double>? _verticalSnapPoints;
private double _verticalSnapPoint;
private double _verticalSnapPointOffset;
private double _horizontalSnapPointOffset;
private CompositeDisposable? _ownerSubscriptions;
private ScrollViewer? _owner;
private IScrollSnapPointsInfo? _scrollSnapPointsInfo;
private bool _isSnapPointsUpdated;
/// <summary>
/// Initializes static members of the <see cref="ScrollContentPresenter"/> class.
/// </summary>
static ScrollContentPresenter()
{
ClipToBoundsProperty.OverrideDefaultValue(typeof(ScrollContentPresenter), true);
AffectsMeasure<ScrollContentPresenter>(CanHorizontallyScrollProperty, CanVerticallyScrollProperty);
}
/// <summary>
/// Initializes a new instance of the <see cref="ScrollContentPresenter"/> class.
/// </summary>
public ScrollContentPresenter()
{
AddHandler(RequestBringIntoViewEvent, BringIntoViewRequested);
AddHandler(Gestures.ScrollGestureEvent, OnScrollGesture);
AddHandler(Gestures.ScrollGestureEndedEvent, OnScrollGestureEnded);
AddHandler(Gestures.ScrollGestureInertiaStartingEvent, OnScrollGestureInertiaStartingEnded);
this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription);
}
/// <summary>
/// Gets or sets a value indicating whether the content can be scrolled horizontally.
/// </summary>
public bool CanHorizontallyScroll
{
get => GetValue(CanHorizontallyScrollProperty);
set => SetValue(CanHorizontallyScrollProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether the content can be scrolled horizontally.
/// </summary>
public bool CanVerticallyScroll
{
get => GetValue(CanVerticallyScrollProperty);
set => SetValue(CanVerticallyScrollProperty, value);
}
/// <summary>
/// Gets the extent of the scrollable content.
/// </summary>
public Size Extent
{
get => _extent;
private set => SetAndRaise(ExtentProperty, ref _extent, value);
}
/// <summary>
/// Gets or sets the current scroll offset.
/// </summary>
public Vector Offset
{
get => GetValue(OffsetProperty);
set => SetValue(OffsetProperty, value);
}
/// <summary>
/// Gets the size of the viewport on the scrollable content.
/// </summary>
public Size Viewport
{
get => _viewport;
private set => SetAndRaise(ViewportProperty, ref _viewport, value);
}
/// <summary>
/// Gets or sets how scroll gesture reacts to the snap points along the horizontal axis.
/// </summary>
public SnapPointsType HorizontalSnapPointsType
{
get => GetValue(HorizontalSnapPointsTypeProperty);
set => SetValue(HorizontalSnapPointsTypeProperty, value);
}
/// <summary>
/// Gets or sets how scroll gesture reacts to the snap points along the vertical axis.
/// </summary>
public SnapPointsType VerticalSnapPointsType
{
get => GetValue(VerticalSnapPointsTypeProperty);
set => SetValue(VerticalSnapPointsTypeProperty, value);
}
/// <summary>
/// Gets or sets how the existing snap points are horizontally aligned versus the initial viewport.
/// </summary>
public SnapPointsAlignment HorizontalSnapPointsAlignment
{
get => GetValue(HorizontalSnapPointsAlignmentProperty);
set => SetValue(HorizontalSnapPointsAlignmentProperty, value);
}
/// <summary>
/// Gets or sets how the existing snap points are vertically aligned versus the initial viewport.
/// </summary>
public SnapPointsAlignment VerticalSnapPointsAlignment
{
get => GetValue(VerticalSnapPointsAlignmentProperty);
set => SetValue(VerticalSnapPointsAlignmentProperty, value);
}
/// <summary>
/// Gets or sets if scroll chaining is enabled. The default value is true.
/// </summary>
/// <remarks>
/// After a user hits a scroll limit on an element that has been nested within another scrollable element,
/// you can specify whether that parent element should continue the scrolling operation begun in its child element.
/// This is called scroll chaining.
/// </remarks>
public bool IsScrollChainingEnabled
{
get => GetValue(IsScrollChainingEnabledProperty);
set => SetValue(IsScrollChainingEnabledProperty, value);
}
/// <inheritdoc/>
Control? IScrollAnchorProvider.CurrentAnchor
{
get
{
EnsureAnchorElementSelection();
return _anchorElement;
}
}
/// <summary>
/// Attempts to bring a portion of the target visual into view by scrolling the content.
/// </summary>
/// <param name="target">The target visual.</param>
/// <param name="targetRect">The portion of the target visual to bring into view.</param>
/// <returns>True if the scroll offset was changed; otherwise false.</returns>
public bool BringDescendantIntoView(Visual target, Rect targetRect)
{
if (Child?.IsEffectivelyVisible != true)
{
return false;
}
var scrollable = Child as ILogicalScrollable;
var control = target as Control;
if (scrollable?.IsLogicalScrollEnabled == true && control != null)
{
return scrollable.BringIntoView(control, targetRect);
}
var transform = target.TransformToVisual(this);
if (transform == null)
{
return false;
}
transform *= Matrix.CreateTranslation(Offset);
var rectangle = targetRect.TransformToAABB(transform.Value);
Rect viewport = new Rect(Offset.X, Offset.Y, Viewport.Width, Viewport.Height);
double minX = ComputeScrollOffsetWithMinimalScroll(viewport.Left, viewport.Right, rectangle.Left, rectangle.Right);
double minY = ComputeScrollOffsetWithMinimalScroll(viewport.Top, viewport.Bottom, rectangle.Top, rectangle.Bottom);
var offset = new Vector(minX, minY);
if (Offset.NearlyEquals(offset))
{
return false;
}
var oldOffset = Offset;
SetCurrentValue(OffsetProperty, offset);
// It's possible that the Offset coercion has changed the offset back to its previous value,
// this is common for floating point rounding errors.
return !Offset.NearlyEquals(oldOffset);
}
/// <summary>
/// Computes the closest offset to ensure most of the child is visible in the viewport along an axis.
/// </summary>
/// <param name="viewportStart">The left or top of the viewport</param>
/// <param name="viewportEnd">The right or bottom of the viewport</param>
/// <param name="childStart">The left or top of the child</param>
/// <param name="childEnd">The right or bottom of the child</param>
/// <returns></returns>
internal static double ComputeScrollOffsetWithMinimalScroll(
double viewportStart,
double viewportEnd,
double childStart,
double childEnd)
{
// If child is at least partially above viewport, i.e. top of child is above viewport top and bottom of child is above viewport bottom.
bool isChildAbove = MathUtilities.LessThan(childStart, viewportStart) && MathUtilities.LessThan(childEnd, viewportEnd);
// If child is at least partially below viewport, i.e. top of child is below viewport top and bottom of child is below viewport bottom.
bool isChildBelow = MathUtilities.GreaterThan(childEnd, viewportEnd) && MathUtilities.GreaterThan(childStart, viewportStart);
bool isChildLarger = (childEnd - childStart) > (viewportEnd - viewportStart);
// Value if no updates is needed. The child is fully visible in the viewport, or the viewport is completely within the child's bounds
var res = viewportStart;
// The child is above the viewport and is smaller than the viewport, or if the child's top is below the viewport top
// and is larger than the viewport, we align the child top to the top of the viewport
if ((isChildAbove && !isChildLarger)
|| (isChildBelow && isChildLarger))
{
res = childStart;
}
// The child is above the viewport and is larger than the viewport, or if the child's smaller but is below the viewport,
// we align the child's bottom to the bottom of the viewport
else if (isChildAbove || isChildBelow)
{
res = (childEnd - (viewportEnd - viewportStart));
}
return res;
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
AttachToScrollViewer();
}
/// <summary>
/// Locates the first <see cref="ScrollViewer"/> ancestor and binds to it. Properties which have been set through other means are not bound.
/// </summary>
/// <remarks>
/// This method is automatically called when the control is attached to a visual tree.
/// </remarks>
internal void AttachToScrollViewer()
{
var owner = this.FindAncestorOfType<ScrollViewer>();
if (owner == null)
{
_owner = null;
_ownerSubscriptions?.Dispose();
_ownerSubscriptions = null;
return;
}
if (owner == _owner)
{
return;
}
_ownerSubscriptions?.Dispose();
_owner = owner;
var subscriptionDisposables = new IDisposable?[]
{
IfUnset(CanHorizontallyScrollProperty, p => Bind(p, owner.GetObservable(ScrollViewer.HorizontalScrollBarVisibilityProperty, NotDisabled), Data.BindingPriority.Template)),
IfUnset(CanVerticallyScrollProperty, p => Bind(p, owner.GetObservable(ScrollViewer.VerticalScrollBarVisibilityProperty, NotDisabled), Data.BindingPriority.Template)),
IfUnset(OffsetProperty, p => Bind(p, owner.GetBindingObservable(ScrollViewer.OffsetProperty), Data.BindingPriority.Template)),
IfUnset(IsScrollChainingEnabledProperty, p => Bind(p, owner.GetBindingObservable(ScrollViewer.IsScrollChainingEnabledProperty), Data.BindingPriority.Template)),
IfUnset(ContentProperty, p => Bind(p, owner.GetBindingObservable(ContentProperty), Data.BindingPriority.Template)),
}.Where(d => d != null).Cast<IDisposable>().ToArray();
_ownerSubscriptions = new CompositeDisposable(subscriptionDisposables);
static bool NotDisabled(ScrollBarVisibility v) => v != ScrollBarVisibility.Disabled;
IDisposable? IfUnset<T>(T property, Func<T, IDisposable> func) where T : AvaloniaProperty => IsSet(property) ? null : func(property);
}
/// <inheritdoc/>
void IScrollAnchorProvider.RegisterAnchorCandidate(Control element)
{
if (!this.IsVisualAncestorOf(element))
{
throw new InvalidOperationException(
"An anchor control must be a visual descendent of the ScrollContentPresenter.");
}
_anchorCandidates ??= new();
_anchorCandidates.Add(element);
_isAnchorElementDirty = true;
}
/// <inheritdoc/>
void IScrollAnchorProvider.UnregisterAnchorCandidate(Control element)
{
_anchorCandidates?.Remove(element);
_isAnchorElementDirty = true;
if (_anchorElement == element)
{
_anchorElement = null;
}
}
/// <inheritdoc/>
protected override Size MeasureOverride(Size availableSize)
{
if (_logicalScrollSubscription != null || Child == null)
{
return base.MeasureOverride(availableSize);
}
var constraint = new Size(
CanHorizontallyScroll ? double.PositiveInfinity : availableSize.Width,
CanVerticallyScroll ? double.PositiveInfinity : availableSize.Height);
Child.Measure(constraint);
if (!_isSnapPointsUpdated)
{
_isSnapPointsUpdated = true;
UpdateSnapPoints();
}
return Child.DesiredSize.Constrain(availableSize);
}
/// <inheritdoc/>
protected override Size ArrangeOverride(Size finalSize)
{
if (_logicalScrollSubscription != null || Child == null)
{
return base.ArrangeOverride(finalSize);
}
return ArrangeWithAnchoring(finalSize);
}
private Size ArrangeWithAnchoring(Size finalSize)
{
var size = new Size(
CanHorizontallyScroll ? Math.Max(Child!.DesiredSize.Width, finalSize.Width) : finalSize.Width,
CanVerticallyScroll ? Math.Max(Child!.DesiredSize.Height, finalSize.Height) : finalSize.Height);
Vector TrackAnchor()
{
// If we have an anchor and its position relative to Child has changed during the
// arrange then that change wasn't just due to scrolling (as scrolling doesn't adjust
// relative positions within Child).
if (_anchorElement != null &&
TranslateBounds(_anchorElement, Child!, out var updatedBounds) &&
updatedBounds.Position != _anchorElementBounds.Position)
{
var offset = updatedBounds.Position - _anchorElementBounds.Position;
return offset;
}
return default;
}
var isAnchoring = Offset.X >= EdgeDetectionTolerance || Offset.Y >= EdgeDetectionTolerance;
if (isAnchoring)
{
// Calculate the new anchor element if necessary.
EnsureAnchorElementSelection();
// Do the arrange.
ArrangeOverrideImpl(size, -Offset);
// If the anchor moved during the arrange, we need to adjust the offset and do another arrange.
var anchorShift = TrackAnchor();
if (anchorShift != default)
{
var newOffset = Offset + anchorShift;
var newExtent = Extent;
var maxOffset = new Vector(Extent.Width - Viewport.Width, Extent.Height - Viewport.Height);
if (newOffset.X > maxOffset.X)
{
newExtent = newExtent.WithWidth(newOffset.X + Viewport.Width);
}
if (newOffset.Y > maxOffset.Y)
{
newExtent = newExtent.WithHeight(newOffset.Y + Viewport.Height);
}
Extent = newExtent;
try
{
_arranging = true;
SetCurrentValue(OffsetProperty, newOffset);
}
finally
{
_arranging = false;
}
ArrangeOverrideImpl(size, -Offset);
}
}
else
{
ArrangeOverrideImpl(size, -Offset);
}
Viewport = finalSize;
Extent = ComputeExtent(finalSize);
_isAnchorElementDirty = true;
return finalSize;
}
private Size ComputeExtent(Size viewportSize)
{
var childMargin = Child!.Margin;
if (Child.UseLayoutRounding)
{
var scale = LayoutHelper.GetLayoutScale(Child);
childMargin = LayoutHelper.RoundLayoutThickness(childMargin, scale);
}
var extent = Child!.Bounds.Size.Inflate(childMargin);
if (MathUtilities.AreClose(extent.Width, viewportSize.Width, LayoutHelper.LayoutEpsilon))
extent = extent.WithWidth(viewportSize.Width);
if (MathUtilities.AreClose(extent.Height, viewportSize.Height, LayoutHelper.LayoutEpsilon))
extent = extent.WithHeight(viewportSize.Height);
return extent;
}
private void OnScrollGesture(object? sender, ScrollGestureEventArgs e)
{
if (Extent.Height > Viewport.Height || Extent.Width > Viewport.Width)
{
var scrollable = Child as ILogicalScrollable;
var isLogical = scrollable?.IsLogicalScrollEnabled == true;
var logicalScrollItemSize = new Vector(1, 1);
double x = Offset.X;
double y = Offset.Y;
Vector delta = default;
if (isLogical)
_activeLogicalGestureScrolls?.TryGetValue(e.Id, out delta);
delta += AdjustDeltaForFlowDirection(e.Delta, FlowDirection);
if (isLogical && scrollable is object)
{
logicalScrollItemSize = Bounds.Size / scrollable.Viewport;
}
if (Extent.Height > Viewport.Height)
{
double dy;
if (isLogical)
{
var logicalUnits = delta.Y / logicalScrollItemSize.Y;
delta = delta.WithY(delta.Y - logicalUnits * logicalScrollItemSize.Y);
dy = logicalUnits;
}
else
dy = delta.Y;
y += dy;
y = Math.Max(y, 0);
y = Math.Min(y, Extent.Height - Viewport.Height);
}
if (Extent.Width > Viewport.Width)
{
double dx;
if (isLogical)
{
var logicalUnits = delta.X / logicalScrollItemSize.X;
delta = delta.WithX(delta.X - logicalUnits * logicalScrollItemSize.X);
dx = logicalUnits;
}
else
dx = delta.X;
x += dx;
x = Math.Max(x, 0);
x = Math.Min(x, Extent.Width - Viewport.Width);
}
if (isLogical)
{
if (_activeLogicalGestureScrolls == null)
_activeLogicalGestureScrolls = new Dictionary<int, Vector>();
_activeLogicalGestureScrolls[e.Id] = delta;
}
Vector newOffset = new Vector(x, y);
if (_scrollGestureSnapPoints?.TryGetValue(e.Id, out var snapPoint) == true)
{
double xOffset = x;
double yOffset = y;
if (HorizontalSnapPointsType != SnapPointsType.None)
{
xOffset = delta.X < 0 ? Math.Max(snapPoint.X, newOffset.X) : Math.Min(snapPoint.X, newOffset.X);
}
if (VerticalSnapPointsType != SnapPointsType.None)
{
yOffset = delta.Y < 0 ? Math.Max(snapPoint.Y, newOffset.Y) : Math.Min(snapPoint.Y, newOffset.Y);
}
newOffset = new Vector(xOffset, yOffset);
}
bool offsetChanged = newOffset != Offset;
SetCurrentValue(OffsetProperty, newOffset);
e.Handled = !IsScrollChainingEnabled || offsetChanged;
e.ShouldEndScrollGesture = !IsScrollChainingEnabled && !offsetChanged;
}
}
private void OnScrollGestureEnded(object? sender, ScrollGestureEndedEventArgs e)
{
_activeLogicalGestureScrolls?.Remove(e.Id);
_scrollGestureSnapPoints?.Remove(e.Id);
SetCurrentValue(OffsetProperty, SnapOffset(Offset));
}
private void OnScrollGestureInertiaStartingEnded(object? sender, ScrollGestureInertiaStartingEventArgs e)
{
var scrollable = Content;
if (Content is ItemsControl itemsControl)
scrollable = itemsControl.Presenter?.Panel;
if (scrollable is not IScrollSnapPointsInfo)
return;
if (_scrollGestureSnapPoints == null)
_scrollGestureSnapPoints = new Dictionary<int, Vector>();
var offset = Offset;
if (HorizontalSnapPointsType != SnapPointsType.None && VerticalSnapPointsType != SnapPointsType.None)
{
return;
}
double xDistance = 0;
double yDistance = 0;
if (HorizontalSnapPointsType != SnapPointsType.None)
{
xDistance = HorizontalSnapPointsType == SnapPointsType.Mandatory ? GetDistance(e.Inertia.X) : 0;
}
if (VerticalSnapPointsType != SnapPointsType.None)
{
yDistance = VerticalSnapPointsType == SnapPointsType.Mandatory ? GetDistance(e.Inertia.Y) : 0;
}
offset = new Vector(offset.X + xDistance, offset.Y + yDistance);
_scrollGestureSnapPoints.Add(e.Id, SnapOffset(offset));
double GetDistance(double speed)
{
var time = Math.Log(ScrollGestureRecognizer.InertialScrollSpeedEnd / Math.Abs(speed)) / Math.Log(ScrollGestureRecognizer.InertialResistance);
double timeElapsed = 0, distance = 0, step = 0;
while (timeElapsed <= time)
{
double s = speed * Math.Pow(ScrollGestureRecognizer.InertialResistance, timeElapsed);
distance += (s * step);
timeElapsed += 0.016f;
step = 0.016f;
}
return distance;
}
}
/// <inheritdoc/>
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
{
if (Extent.Height > Viewport.Height || Extent.Width > Viewport.Width)
{
var scrollable = Child as ILogicalScrollable;
var isLogical = scrollable?.IsLogicalScrollEnabled == true;
var x = Offset.X;
var y = Offset.Y;
var delta = e.Delta;
// KeyModifiers.Shift should scroll in horizontal direction. This does not work on every platform.
// If Shift-Key is pressed and X is close to 0 we swap the Vector.
if (e.KeyModifiers == KeyModifiers.Shift && MathUtilities.IsZero(delta.X))
{
delta = new Vector(delta.Y, delta.X);
}
else
{
delta = AdjustDeltaForFlowDirection(delta, FlowDirection);
}
if (Extent.Height > Viewport.Height)
{
double height = isLogical ? scrollable!.ScrollSize.Height : 50;
y += -delta.Y * height;
y = Math.Max(y, 0);
y = Math.Min(y, Extent.Height - Viewport.Height);
}
if (Extent.Width > Viewport.Width)
{
double width = isLogical ? scrollable!.ScrollSize.Width : 50;
x += -delta.X * width;
x = Math.Max(x, 0);
x = Math.Min(x, Extent.Width - Viewport.Width);
}
Vector newOffset = SnapOffset(new Vector(x, y), delta, true);
bool offsetChanged = newOffset != Offset;
SetCurrentValue(OffsetProperty, newOffset);
e.Handled = !IsScrollChainingEnabled || offsetChanged;
}
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
if (change.Property == OffsetProperty)
{
if (!_arranging)
{
InvalidateArrange();
}
_owner?.SetCurrentValue(OffsetProperty, change.GetNewValue<Vector>());
}
else if (change.Property == ChildProperty)
{
ChildChanged(change);
}
else if (change.Property == HorizontalSnapPointsAlignmentProperty ||
change.Property == VerticalSnapPointsAlignmentProperty)
{
UpdateSnapPoints();
}
else if (change.Property == ExtentProperty)
{
if (_owner != null)
{
_owner.Extent = change.GetNewValue<Size>();
}
CoerceValue(OffsetProperty);
}
else if (change.Property == ViewportProperty)
{
if (_owner != null)
{
_owner.Viewport = change.GetNewValue<Size>();
}
CoerceValue(OffsetProperty);
}
base.OnPropertyChanged(change);
}
private void ScrollSnapPointsInfoSnapPointsChanged(object? sender, Interactivity.RoutedEventArgs e)
{
UpdateSnapPoints();
}
private void BringIntoViewRequested(object? sender, RequestBringIntoViewEventArgs e)
{
if (e.TargetObject is not null)
e.Handled = BringDescendantIntoView(e.TargetObject, e.TargetRect);
}
private void ChildChanged(AvaloniaPropertyChangedEventArgs e)
{
UpdateScrollableSubscription((Control?)e.NewValue);
if (e.OldValue != null)
{
SetCurrentValue(OffsetProperty, default);
}
}
private void UpdateScrollableSubscription(Control? child)
{
var scrollable = child as ILogicalScrollable;
_logicalScrollSubscription?.Dispose();
_logicalScrollSubscription = null;
if (scrollable != null)
{
scrollable.ScrollInvalidated += ScrollInvalidated;
if (scrollable.IsLogicalScrollEnabled)
{
_logicalScrollSubscription = new CompositeDisposable(
this.GetObservable(CanHorizontallyScrollProperty)
.Subscribe(x => scrollable.CanHorizontallyScroll = x),
this.GetObservable(CanVerticallyScrollProperty)
.Subscribe(x => scrollable.CanVerticallyScroll = x),
this.GetObservable(OffsetProperty)
.Skip(1).Subscribe(x => scrollable.Offset = x),
Disposable.Create(() => scrollable.ScrollInvalidated -= ScrollInvalidated));
UpdateFromScrollable(scrollable);
}
}
}
private void ScrollInvalidated(object? sender, EventArgs e)
{
UpdateFromScrollable((ILogicalScrollable)sender!);
}
private void UpdateFromScrollable(ILogicalScrollable scrollable)
{
var logicalScroll = _logicalScrollSubscription != null;
if (logicalScroll != scrollable.IsLogicalScrollEnabled)
{
UpdateScrollableSubscription(Child);
SetCurrentValue(OffsetProperty, default);
InvalidateMeasure();
}
else if (scrollable.IsLogicalScrollEnabled)
{
Viewport = scrollable.Viewport;
Extent = scrollable.Extent;
SetCurrentValue(OffsetProperty, scrollable.Offset);
}
}
private void EnsureAnchorElementSelection()
{
if (!_isAnchorElementDirty || _anchorCandidates is null)
{
return;
}
_anchorElement = null;
_anchorElementBounds = default;
_isAnchorElementDirty = false;
var bestCandidate = default(Control);
var bestCandidateDistance = double.MaxValue;
// Find the anchor candidate that is scrolled closest to the top-left of this
// ScrollContentPresenter.
foreach (var element in _anchorCandidates)
{
if (element.IsVisible && GetViewportBounds(element, out var bounds))
{
var distance = (Vector)bounds.Position;
var candidateDistance = Math.Abs(distance.Length);
if (candidateDistance < bestCandidateDistance)
{
bestCandidate = element;
bestCandidateDistance = candidateDistance;
}
}
}
if (bestCandidate != null)
{
// We have a candidate, calculate its bounds relative to Child. Because these
// bounds aren't relative to the ScrollContentPresenter itself, if they change
// then we know it wasn't just due to scrolling.
var unscrolledBounds = TranslateBounds(bestCandidate, Child!);
_anchorElement = bestCandidate;
_anchorElementBounds = unscrolledBounds;
}
}
private bool GetViewportBounds(Control element, out Rect bounds)
{
if (TranslateBounds(element, Child!, out var childBounds))
{
// We want the bounds relative to the new Offset, regardless of whether the child
// control has actually been arranged to this offset yet, so translate first to the
// child control and then apply Offset rather than translating directly to this
// control.
var thisBounds = new Rect(Bounds.Size);
bounds = new Rect(childBounds.Position - Offset, childBounds.Size);
return bounds.Intersects(thisBounds);
}
bounds = default;
return false;
}
private Rect TranslateBounds(Control control, Control to)
{
if (TranslateBounds(control, to, out var bounds))
{
return bounds;
}
throw new InvalidOperationException("The control's bounds could not be translated to the requested control.");
}
private bool TranslateBounds(Control control, Control to, out Rect bounds)
{
if (!control.IsVisible)
{
bounds = default;
return false;
}
var p = control.TranslatePoint(default, to);
bounds = p.HasValue ? new Rect(p.Value, control.Bounds.Size) : default;
return p.HasValue;
}
private void UpdateSnapPoints()
{
var scrollable = GetScrollSnapPointsInfo(Content);
if (scrollable is IScrollSnapPointsInfo scrollSnapPointsInfo)
{
_areVerticalSnapPointsRegular = scrollSnapPointsInfo.AreVerticalSnapPointsRegular;
_areHorizontalSnapPointsRegular = scrollSnapPointsInfo.AreHorizontalSnapPointsRegular;
if (!_areVerticalSnapPointsRegular)
{
_verticalSnapPoints = scrollSnapPointsInfo.GetIrregularSnapPoints(Layout.Orientation.Vertical, VerticalSnapPointsAlignment);
}
else
{
_verticalSnapPoints = new List<double>();
_verticalSnapPoint = scrollSnapPointsInfo.GetRegularSnapPoints(Layout.Orientation.Vertical, VerticalSnapPointsAlignment, out _verticalSnapPointOffset);
}
if (!_areHorizontalSnapPointsRegular)
{
_horizontalSnapPoints = scrollSnapPointsInfo.GetIrregularSnapPoints(Layout.Orientation.Horizontal, HorizontalSnapPointsAlignment);
}
else
{
_horizontalSnapPoints = new List<double>();
_horizontalSnapPoint = scrollSnapPointsInfo.GetRegularSnapPoints(Layout.Orientation.Horizontal, HorizontalSnapPointsAlignment, out _horizontalSnapPointOffset);
}
}
else
{
_horizontalSnapPoints = new List<double>();
_verticalSnapPoints = new List<double>();
}
}
private Vector SnapOffset(Vector offset, Vector direction = default, bool snapToNext = false)
{
var scrollable = GetScrollSnapPointsInfo(Content);
if (scrollable is null || (VerticalSnapPointsType == SnapPointsType.None && HorizontalSnapPointsType == SnapPointsType.None))
return offset;
var diff = GetAlignmentDiff();
if (VerticalSnapPointsType != SnapPointsType.None && (_areVerticalSnapPointsRegular || _verticalSnapPoints?.Count > 0) && (!snapToNext || snapToNext && direction.Y != 0))
{
var estimatedOffset = new Vector(offset.X, offset.Y + diff.Y);
double previousSnapPoint = 0, nextSnapPoint = 0, midPoint = 0;
if (_areVerticalSnapPointsRegular)
{
previousSnapPoint = (int)(estimatedOffset.Y / _verticalSnapPoint) * _verticalSnapPoint + _verticalSnapPointOffset;
nextSnapPoint = previousSnapPoint + _verticalSnapPoint;
midPoint = (previousSnapPoint + nextSnapPoint) / 2;
}
else if (_verticalSnapPoints?.Count > 0)
{
(previousSnapPoint, nextSnapPoint) = FindNearestSnapPoint(_verticalSnapPoints, estimatedOffset.Y);
midPoint = (previousSnapPoint + nextSnapPoint) / 2;
}
var nearestSnapPoint = snapToNext ? (direction.Y > 0 ? previousSnapPoint : nextSnapPoint) :
estimatedOffset.Y < midPoint ? previousSnapPoint : nextSnapPoint;
offset = new Vector(offset.X, nearestSnapPoint - diff.Y);
}
if (HorizontalSnapPointsType != SnapPointsType.None && (_areHorizontalSnapPointsRegular || _horizontalSnapPoints?.Count > 0) && (!snapToNext || snapToNext && direction.X != 0))
{
var estimatedOffset = new Vector(offset.X + diff.X, offset.Y);
double previousSnapPoint = 0, nextSnapPoint = 0, midPoint = 0;
if (_areHorizontalSnapPointsRegular)
{
previousSnapPoint = (int)(estimatedOffset.X / _horizontalSnapPoint) * _horizontalSnapPoint + _horizontalSnapPointOffset;
nextSnapPoint = previousSnapPoint + _horizontalSnapPoint;
midPoint = (previousSnapPoint + nextSnapPoint) / 2;
}
else if (_horizontalSnapPoints?.Count > 0)
{
(previousSnapPoint, nextSnapPoint) = FindNearestSnapPoint(_horizontalSnapPoints, estimatedOffset.X);
midPoint = (previousSnapPoint + nextSnapPoint) / 2;
}
var nearestSnapPoint = snapToNext ? (direction.X > 0 ? previousSnapPoint : nextSnapPoint) :
estimatedOffset.X < midPoint ? previousSnapPoint : nextSnapPoint;
offset = new Vector(nearestSnapPoint - diff.X, offset.Y);
}
Vector GetAlignmentDiff()
{
var vector = default(Vector);
switch (VerticalSnapPointsAlignment)
{
case SnapPointsAlignment.Center:
vector += new Vector(0, Viewport.Height / 2);
break;
case SnapPointsAlignment.Far:
vector += new Vector(0, Viewport.Height);
break;
}
switch (HorizontalSnapPointsAlignment)
{
case SnapPointsAlignment.Center:
vector += new Vector(Viewport.Width / 2, 0);
break;
case SnapPointsAlignment.Far:
vector += new Vector(Viewport.Width, 0);
break;
}
return vector;
}
return offset;
}
private static (double previous, double next) FindNearestSnapPoint(IReadOnlyList<double> snapPoints, double value)
{
var point = snapPoints.BinarySearch(value, Comparer<double>.Default);
double previousSnapPoint, nextSnapPoint;
if (point < 0)
{
point = ~point;
previousSnapPoint = snapPoints[Math.Max(0, point - 1)];
nextSnapPoint = point >= snapPoints.Count ? snapPoints.Last() : snapPoints[Math.Max(0, point)];
}
else
{
previousSnapPoint = nextSnapPoint = snapPoints[Math.Max(0, point)];
}
return (previousSnapPoint, nextSnapPoint);
}
private IScrollSnapPointsInfo? GetScrollSnapPointsInfo(object? content)
{
var scrollable = content;
if (Content is ItemsControl itemsControl)
scrollable = itemsControl.Presenter?.Panel;
if (Content is ItemsPresenter itemsPresenter)
scrollable = itemsPresenter.Panel;
var snapPointsInfo = scrollable as IScrollSnapPointsInfo;
if (snapPointsInfo != _scrollSnapPointsInfo)
{
if (_scrollSnapPointsInfo != null)
{
_scrollSnapPointsInfo.VerticalSnapPointsChanged -= ScrollSnapPointsInfoSnapPointsChanged;
_scrollSnapPointsInfo.HorizontalSnapPointsChanged -= ScrollSnapPointsInfoSnapPointsChanged;
}
_scrollSnapPointsInfo = snapPointsInfo;
if (_scrollSnapPointsInfo != null)
{
_scrollSnapPointsInfo.VerticalSnapPointsChanged += ScrollSnapPointsInfoSnapPointsChanged;
_scrollSnapPointsInfo.HorizontalSnapPointsChanged += ScrollSnapPointsInfoSnapPointsChanged;
}
}
return snapPointsInfo;
}
private static Vector AdjustDeltaForFlowDirection(Vector delta, Media.FlowDirection flowDirection)
{
if (flowDirection == Media.FlowDirection.RightToLeft)
{
return delta.WithX(-delta.X);
}
return delta;
}
}
}