@ -7,6 +7,8 @@ using Avalonia.Controls.Primitives;
using Avalonia.Input ;
using Avalonia.VisualTree ;
# nullable enable
namespace Avalonia.Controls.Presenters
{
/// <summary>
@ -14,6 +16,8 @@ namespace Avalonia.Controls.Presenters
/// </summary>
public class ScrollContentPresenter : ContentPresenter , IPresenter , IScrollable , IScrollAnchorProvider
{
private const double EdgeDetectionTolerance = 0.1 ;
/// <summary>
/// Defines the <see cref="CanHorizontallyScroll"/> property.
/// </summary>
@ -64,11 +68,13 @@ namespace Avalonia.Controls.Presenters
private bool _ arranging ;
private Size _ extent ;
private Vector _ offset ;
private IDisposable _l ogicalScrollSubscription ;
private IDisposable ? _l ogicalScrollSubscription ;
private Size _ viewport ;
private Dictionary < int , Vector > _ activeLogicalGestureScrolls ;
private List < IControl > _ anchorCandidates ;
private ( IControl control , Rect bounds ) _ anchor ;
private Dictionary < int , Vector > ? _ activeLogicalGestureScrolls ;
private List < IControl > ? _ anchorCandidates ;
private IControl ? _ anchorElement ;
private Rect _ anchorElementBounds ;
private bool _ isAnchorElementDirty ;
/// <summary>
/// Initializes static members of the <see cref="ScrollContentPresenter"/> class.
@ -90,8 +96,6 @@ namespace Avalonia.Controls.Presenters
this . GetObservable ( ChildProperty ) . Subscribe ( UpdateScrollableSubscription ) ;
}
internal event EventHandler < VectorEventArgs > PreArrange ;
/// <summary>
/// Gets or sets a value indicating whether the content can be scrolled horizontally.
/// </summary>
@ -138,7 +142,14 @@ namespace Avalonia.Controls.Presenters
}
/// <inheritdoc/>
IControl IScrollAnchorProvider . CurrentAnchor = > _ anchor . control ;
IControl ? IScrollAnchorProvider . CurrentAnchor
{
get
{
EnsureAnchorElementSelection ( ) ;
return _ anchorElement ;
}
}
/// <summary>
/// Attempts to bring a portion of the target visual into view by scrolling the content.
@ -215,16 +226,18 @@ namespace Avalonia.Controls.Presenters
_ anchorCandidates ? ? = new List < IControl > ( ) ;
_ anchorCandidates . Add ( element ) ;
_ isAnchorElementDirty = true ;
}
/// <inheritdoc/>
void IScrollAnchorProvider . UnregisterAnchorCandidate ( IControl element )
{
_ anchorCandidates ? . Remove ( element ) ;
_ isAnchorElementDirty = true ;
if ( _ anchor . control = = element )
if ( _ anchorElement = = element )
{
_ anchor = default ;
_ anchorElement = null ;
}
}
@ -247,11 +260,6 @@ namespace Avalonia.Controls.Presenters
/// <inheritdoc/>
protected override Size ArrangeOverride ( Size finalSize )
{
PreArrange ? . Invoke ( this , new VectorEventArgs
{
Vector = new Vector ( finalSize . Width , finalSize . Height ) ,
} ) ;
if ( _l ogicalScrollSubscription ! = null | | Child = = null )
{
return base . ArrangeOverride ( finalSize ) ;
@ -271,59 +279,69 @@ namespace Avalonia.Controls.Presenters
// 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 ( _ anchor . control ! = null & &
TranslateBounds ( _ anchor . control , Child , out var updatedBounds ) & &
updatedBounds . Position ! = _ anchor . b ounds. Position )
if ( _ anchorElement ! = null & &
TranslateBounds ( _ anchorElement , Child , out var updatedBounds ) & &
updatedBounds . Position ! = _ anchorElementB ounds . Position )
{
var offset = updatedBounds . Position - _ anchor . b ounds. Position ;
var offset = updatedBounds . Position - _ anchorElementB ounds . Position ;
return offset ;
}
return default ;
}
// Calculate the new anchor element.
_ anchor = CalculateCurrentAnchor ( ) ;
var isAnchoring = Offset . X > = EdgeDetectionTolerance | | Offset . Y > = EdgeDetectionTolerance ;
// Do the arrange.
ArrangeOverrideImpl ( size , - Offset ) ;
if ( isAnchoring )
{
// Calculate the new anchor element if necessary.
EnsureAnchorElementSelection ( ) ;
// If the anchor moved during the arrange, we need to adjust the offset and do another arrange.
var anchorShift = TrackAnchor ( ) ;
// Do the arrange.
ArrangeOverrideImpl ( size , - Offset ) ;
if ( anchorShift ! = default )
{
var newOffset = Offset + anchorShift ;
var newExtent = Extent ;
var maxOffset = new Vector ( Extent . Width - Viewport . Width , Extent . Height - Viewport . Height ) ;
// If the anchor moved during the arrange, we need to adjust the offset and do another arrange.
var anchorShift = TrackAnchor ( ) ;
if ( newOffset . X > maxOffset . X )
if ( anchorShift ! = default )
{
newExtent = newExtent . WithWidth ( newOffset . X + Viewport . Width ) ;
}
var newOffset = Offset + anchorShift ;
var newExtent = Extent ;
var maxOffset = new Vector ( Extent . Width - Viewport . Width , Extent . Height - Viewport . Height ) ;
if ( newOffset . Y > maxOffset . Y )
{
newExtent = newExtent . WithHeight ( newOffset . Y + Viewport . Height ) ;
}
if ( newOffset . X > maxOffset . X )
{
newExtent = newExtent . WithWidth ( newOffset . X + Viewport . Width ) ;
}
Extent = newExtent ;
if ( newOffset . Y > maxOffset . Y )
{
newExtent = newExtent . WithHeight ( newOffset . Y + Viewport . Height ) ;
}
try
{
_ arranging = true ;
Offset = newOffset ;
}
finally
{
_ arranging = false ;
Extent = newExtent ;
try
{
_ arranging = true ;
Offset = newOffset ;
}
finally
{
_ arranging = false ;
}
ArrangeOverrideImpl ( size , - Offset ) ;
}
}
else
{
ArrangeOverrideImpl ( size , - Offset ) ;
}
Viewport = finalSize ;
Extent = Child . Bounds . Size . Inflate ( Child . Margin ) ;
_ isAnchorElementDirty = true ;
return finalSize ;
}
@ -350,7 +368,7 @@ namespace Avalonia.Controls.Presenters
{
var logicalUnits = delta . Y / LogicalScrollItemSize ;
delta = delta . WithY ( delta . Y - logicalUnits * LogicalScrollItemSize ) ;
dy = logicalUnits * scrollable . ScrollSize . Height ;
dy = logicalUnits * scrollable ! . ScrollSize . Height ;
}
else
dy = delta . Y ;
@ -368,7 +386,7 @@ namespace Avalonia.Controls.Presenters
{
var logicalUnits = delta . X / LogicalScrollItemSize ;
delta = delta . WithX ( delta . X - logicalUnits * LogicalScrollItemSize ) ;
dx = logicalUnits * scrollable . ScrollSize . Width ;
dx = logicalUnits * scrollable ! . ScrollSize . Width ;
}
else
dx = delta . X ;
@ -405,7 +423,7 @@ namespace Avalonia.Controls.Presenters
if ( Extent . Height > Viewport . Height )
{
double height = isLogical ? scrollable . ScrollSize . Height : 5 0 ;
double height = isLogical ? scrollable ! . ScrollSize . Height : 5 0 ;
y + = - e . Delta . Y * height ;
y = Math . Max ( y , 0 ) ;
y = Math . Min ( y , Extent . Height - Viewport . Height ) ;
@ -413,7 +431,7 @@ namespace Avalonia.Controls.Presenters
if ( Extent . Width > Viewport . Width )
{
double width = isLogical ? scrollable . ScrollSize . Width : 5 0 ;
double width = isLogical ? scrollable ! . ScrollSize . Width : 5 0 ;
x + = - e . Delta . X * width ;
x = Math . Max ( x , 0 ) ;
x = Math . Min ( x , Extent . Width - Viewport . Width ) ;
@ -441,7 +459,7 @@ namespace Avalonia.Controls.Presenters
private void ChildChanged ( AvaloniaPropertyChangedEventArgs e )
{
UpdateScrollableSubscription ( ( IControl ) e . NewValue ) ;
UpdateScrollableSubscription ( ( IControl ? ) e . NewValue ) ;
if ( e . OldValue ! = null )
{
@ -449,7 +467,7 @@ namespace Avalonia.Controls.Presenters
}
}
private void UpdateScrollableSubscription ( IControl child )
private void UpdateScrollableSubscription ( IControl ? child )
{
var scrollable = child as ILogicalScrollable ;
@ -498,13 +516,17 @@ namespace Avalonia.Controls.Presenters
}
}
private ( IControl , Rect ) CalculateCurrentAnchor ( )
private void EnsureAnchorElementSelection ( )
{
if ( _ anchorCandidates = = null )
if ( ! _ isAnchorElementDirty | | _ anchorCandidates is null )
{
return default ;
return ;
}
_ anchorElement = null ;
_ anchorElementBounds = default ;
_ isAnchorElementDirty = false ;
var bestCandidate = default ( IControl ) ;
var bestCandidateDistance = double . MaxValue ;
@ -531,10 +553,9 @@ namespace Avalonia.Controls.Presenters
// 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 ) ;
return ( bestCandidate , unscrolledBounds ) ;
_ anchorElement = bestCandidate ;
_ anchorElementBounds = unscrolledBounds ;
}
return default ;
}
private bool GetViewportBounds ( IControl element , out Rect bounds )