Browse Source

Handle Padding correctly in ScrollContentPresenter (#21872)

* Add failing ScrollViewer tests

* Handle Padding correctly in ScrollContentPresenter
pull/21880/head
Julien Lebosquain 2 months ago
committed by GitHub
parent
commit
06cf8efa0e
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 37
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  2. 102
      tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs
  3. 64
      tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs

37
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);
}
/// <inheritdoc/>
@ -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);

102
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()
{

64
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<ScrollViewer>(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<ScrollViewer>(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
{

Loading…
Cancel
Save