diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
index 395c24824f..7bd32dcee4 100644
--- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
@@ -403,9 +403,12 @@ namespace Avalonia.Controls.Presenters
return base.MeasureOverride(availableSize);
}
+ var padding = GetChildPadding();
+ var deflated = availableSize.Deflate(padding);
+
var constraint = new Size(
- CanHorizontallyScroll ? double.PositiveInfinity : availableSize.Width,
- CanVerticallyScroll ? double.PositiveInfinity : availableSize.Height);
+ CanHorizontallyScroll ? double.PositiveInfinity : deflated.Width,
+ CanVerticallyScroll ? double.PositiveInfinity : deflated.Height);
Child.Measure(constraint);
@@ -415,7 +418,7 @@ namespace Avalonia.Controls.Presenters
UpdateSnapPoints();
}
- return Child.DesiredSize.Constrain(availableSize);
+ return Child.DesiredSize.Inflate(padding).Constrain(availableSize);
}
///
@@ -431,9 +434,12 @@ namespace Avalonia.Controls.Presenters
private Size ArrangeWithAnchoring(Size finalSize)
{
+ var padding = GetChildPadding();
+ var desiredSize = Child!.DesiredSize.Inflate(padding);
+
var size = new Size(
- CanHorizontallyScroll ? Math.Max(Child!.DesiredSize.Width, finalSize.Width) : finalSize.Width,
- CanVerticallyScroll ? Math.Max(Child!.DesiredSize.Height, finalSize.Height) : finalSize.Height);
+ CanHorizontallyScroll ? Math.Max(desiredSize.Width, finalSize.Width) : finalSize.Width,
+ CanVerticallyScroll ? Math.Max(desiredSize.Height, finalSize.Height) : finalSize.Height);
Vector TrackAnchor()
{
@@ -501,13 +507,28 @@ namespace Avalonia.Controls.Presenters
}
Viewport = finalSize;
- Extent = ComputeExtent(finalSize);
+ Extent = ComputeExtent(finalSize, padding);
_isAnchorElementDirty = true;
return finalSize;
}
- private Size ComputeExtent(Size viewportSize)
+ private Thickness GetChildPadding()
+ {
+ var padding = Padding;
+ var borderThickness = BorderThickness;
+
+ if (UseLayoutRounding)
+ {
+ var scale = LayoutHelper.GetLayoutScale(this);
+ padding = LayoutHelper.RoundLayoutThickness(padding, scale);
+ borderThickness = LayoutHelper.RoundLayoutThickness(borderThickness, scale);
+ }
+
+ return padding + borderThickness;
+ }
+
+ private Size ComputeExtent(Size viewportSize, Thickness padding)
{
var childMargin = Child!.Margin;
@@ -517,7 +538,7 @@ namespace Avalonia.Controls.Presenters
childMargin = LayoutHelper.RoundLayoutThickness(childMargin, scale);
}
- var extent = Child!.Bounds.Size.Inflate(childMargin);
+ var extent = Child!.Bounds.Size.Inflate(childMargin).Inflate(padding);
if (MathUtilities.AreClose(extent.Width, viewportSize.Width, LayoutHelper.LayoutEpsilon))
extent = extent.WithWidth(viewportSize.Width);
diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs
index 58906c1127..d616f23fa1 100644
--- a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs
@@ -49,7 +49,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
}
[Fact]
- public void DesiredSize_Is_Content_Size_When_Smaller_Than_AvailableSize()
+ public void DesiredSize_Is_Content_Size_Plus_Padding_When_Smaller_Than_AvailableSize()
{
var target = new ScrollContentPresenter
{
@@ -65,7 +65,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
- Assert.Equal(new Size(16, 16), target.DesiredSize);
+ Assert.Equal(new Size(36, 36), target.DesiredSize);
}
[Fact]
@@ -226,6 +226,104 @@ namespace Avalonia.Controls.UnitTests.Presenters
Assert.Equal(100, child.Bounds.Width);
}
+ [Fact]
+ public void Measure_Should_Deflate_AvailableSize_By_Padding()
+ {
+ var child = new TestControl();
+ var target = new ScrollContentPresenter
+ {
+ Padding = new Thickness(10),
+ Content = child
+ };
+
+ target.UpdateChild();
+ target.Measure(new Size(100, 100));
+
+ Assert.Equal(new Size(80, 80), child.AvailableSize);
+ }
+
+ [Fact]
+ public void Extent_Should_Include_Padding()
+ {
+ var target = new ScrollContentPresenter
+ {
+ CanHorizontallyScroll = true,
+ CanVerticallyScroll = true,
+ Padding = new Thickness(10),
+ Content = new Border
+ {
+ Width = 100,
+ Height = 100
+ }
+ };
+
+ target.UpdateChild();
+ target.Measure(new Size(50, 50));
+ target.Arrange(new Rect(0, 0, 50, 50));
+
+ Assert.Equal(new Size(50, 50), target.Viewport);
+ Assert.Equal(new Size(120, 120), target.Extent);
+ }
+
+ [Fact]
+ public void Content_Larger_Than_Viewport_Should_Not_Be_Squashed_By_Padding()
+ {
+ StackPanel content;
+ var target = new ScrollContentPresenter
+ {
+ CanVerticallyScroll = true,
+ Padding = new Thickness(10),
+ Content = content = new StackPanel
+ {
+ Children =
+ {
+ new Border { Height = 50 },
+ new Border { Height = 50 }
+ }
+ },
+ };
+
+ target.UpdateChild();
+ target.Measure(new Size(50, 50));
+ target.Arrange(new Rect(0, 0, 50, 50));
+
+ Assert.Equal(new Rect(10, 10, 30, 100), content.Bounds);
+ Assert.Equal(new Size(50, 120), target.Extent);
+ }
+
+ [Fact]
+ public void Bottom_Padding_Should_Be_Visible_When_Scrolled_To_End()
+ {
+ StackPanel content;
+ var target = new ScrollContentPresenter
+ {
+ CanVerticallyScroll = true,
+ Padding = new Thickness(10),
+ Content = content = new StackPanel
+ {
+ Children =
+ {
+ new Border { Height = 50 },
+ new Border { Height = 50 }
+ }
+ }
+ };
+
+ target.UpdateChild();
+ target.Measure(new Size(50, 50));
+ target.Arrange(new Rect(0, 0, 50, 50));
+
+ // Scroll to the end: extent (120) - viewport (50).
+ target.Offset = new Vector(0, 70);
+ target.Arrange(new Rect(0, 0, 50, 50));
+
+ Assert.Equal(new Vector(0, 70), target.Offset);
+
+ // The whole content is scrolled into view and the bottom padding is still visible.
+ Assert.Equal(-60, content.Bounds.Top);
+ Assert.Equal(40, content.Bounds.Bottom);
+ }
+
[Fact]
public void Extent_Should_Include_Content_Margin()
{
diff --git a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs
index 35e25f6538..752f2ce567 100644
--- a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs
@@ -273,6 +273,69 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new Vector(20, 20), target.Offset);
}
+ [Fact]
+ public void Padding_Should_Be_Included_In_Extent()
+ {
+ const int itemCount = 19;
+ const double itemHeight = 32;
+ const double padding = 50;
+ const double viewportHeight = 200;
+
+ var content = new StackPanel();
+
+ for (var i = 0; i < itemCount; ++i)
+ {
+ content.Children.Add(new Border { Height = itemHeight });
+ }
+
+ var target = new ScrollViewer
+ {
+ Template = new FuncControlTemplate(CreateTemplate),
+ Padding = new Thickness(padding),
+ Height = viewportHeight,
+ Content = content
+ };
+
+ var root = new TestRoot(target);
+ root.LayoutManager.ExecuteInitialLayoutPass();
+
+ Assert.Equal(viewportHeight, target.Viewport.Height);
+ Assert.Equal(itemCount * itemHeight + 2 * padding, target.Extent.Height);
+
+ // The content is arranged below the top padding and isn't squashed by it.
+ Assert.Equal(padding, content.Bounds.Top);
+ Assert.Equal(itemCount * itemHeight, content.Bounds.Height);
+
+ target.ScrollToEnd();
+ root.LayoutManager.ExecuteLayoutPass();
+
+ Assert.Equal(itemCount * itemHeight + 2 * padding - viewportHeight, target.Offset.Y);
+
+ // Once scrolled to the end, the last item is fully visible and the bottom padding is still displayed below it.
+ Assert.Equal(viewportHeight - padding, content.Bounds.Bottom);
+ }
+
+ [Fact]
+ public void Padding_Should_Not_Make_Content_Scrollable_When_ScrollViewer_Is_Sized_To_Content()
+ {
+ var target = new ScrollViewer
+ {
+ Template = new FuncControlTemplate(CreateTemplate),
+ Padding = new Thickness(50),
+ HorizontalAlignment = HorizontalAlignment.Left,
+ VerticalAlignment = VerticalAlignment.Top,
+ Content = new Border { Width = 100, Height = 100 },
+ };
+
+ var root = new TestRoot(target);
+ root.LayoutManager.ExecuteInitialLayoutPass();
+
+ // The padding is included in the desired size, so the content still fits exactly.
+ Assert.Equal(new Size(200, 200), target.Bounds.Size);
+ Assert.Equal(new Size(200, 200), target.Viewport);
+ Assert.Equal(new Size(200, 200), target.Extent);
+ }
+
[Fact]
public void Scroll_Does_Not_Jump_When_Viewport_Becomes_Smaller_While_Dragging_ScrollBar_Thumb()
{
@@ -575,6 +638,7 @@ namespace Avalonia.Controls.UnitTests
new ScrollContentPresenter
{
Name = "PART_ContentPresenter",
+ [~ScrollContentPresenter.PaddingProperty] = control[~ScrollViewer.PaddingProperty],
}.RegisterInNameScope(scope),
new ScrollBar
{