diff --git a/osu.Framework.Tests/Visual/Containers/TestSceneScrollContainer.cs b/osu.Framework.Tests/Visual/Containers/TestSceneScrollContainer.cs index 31f5d38004..6a7c741cee 100644 --- a/osu.Framework.Tests/Visual/Containers/TestSceneScrollContainer.cs +++ b/osu.Framework.Tests/Visual/Containers/TestSceneScrollContainer.cs @@ -525,7 +525,7 @@ private void scrollTo(float position, float scrollContentHeight, float extension AddStep($"scroll to {position}", () => { scrollContainer.ScrollTo(position, false); - immediateScrollPosition = scrollContainer.Current; + immediateScrollPosition = (float)scrollContainer.Current; }); AddAssert($"immediately scrolled to {clampedTarget}", () => Precision.AlmostEquals(clampedTarget, immediateScrollPosition, 1)); diff --git a/osu.Framework.Tests/Visual/Containers/TestSceneScrollContainerDoublePrecision.cs b/osu.Framework.Tests/Visual/Containers/TestSceneScrollContainerDoublePrecision.cs new file mode 100644 index 0000000000..7b995fdfae --- /dev/null +++ b/osu.Framework.Tests/Visual/Containers/TestSceneScrollContainerDoublePrecision.cs @@ -0,0 +1,149 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Diagnostics; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osuTK; +using osuTK.Graphics; + +namespace osu.Framework.Tests.Visual.Containers +{ + public partial class TestSceneScrollContainerDoublePrecision : ManualInputManagerTestScene + { + private const float item_height = 5000; + private const int item_count = 8000; + + private ScrollContainer scrollContainer = null!; + + [SetUp] + public void Setup() => Schedule(Clear); + + [Test] + public void TestStandard() + { + AddStep("Create scroll container", () => + { + Add(scrollContainer = new BasicScrollContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + ScrollbarVisible = true, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.7f, 0.9f), + }); + + for (int i = 0; i < item_count; i++) + { + scrollContainer.Add(new BoxWithDouble + { + Colour = new Color4(RNG.NextSingle(1), RNG.NextSingle(1), RNG.NextSingle(1), 1), + RelativeSizeAxes = Axes.X, + Height = item_height, + Y = i * item_height, + }); + } + }); + + scrollIntoView(item_count - 2); + scrollIntoView(item_count - 1); + } + + [Test] + public void TestDoublePrecision() + { + AddStep("Create scroll container", () => + { + Add(scrollContainer = new DoubleScrollContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + ScrollbarVisible = true, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.7f, 0.9f), + }); + + for (int i = 0; i < item_count; i++) + { + scrollContainer.Add(new BoxWithDouble + { + Colour = new Color4(RNG.NextSingle(1), RNG.NextSingle(1), RNG.NextSingle(1), 1), + RelativeSizeAxes = Axes.X, + Height = item_height, + DoubleLocation = i * item_height, + }); + } + }); + + scrollIntoView(item_count - 2); + scrollIntoView(item_count - 1); + } + + private void scrollIntoView(int index) + { + AddStep($"scroll {index} into view", () => scrollContainer.ScrollIntoView(scrollContainer.ChildrenOfType().Skip(index).First())); + AddUntilStep($"{index} is visible", () => !scrollContainer.ChildrenOfType().Skip(index).First().IsMaskedAway); + } + + public partial class DoubleScrollContainer : BasicScrollContainer + { + private readonly Container layoutContent; + + public override void Add(Drawable drawable) + { + if (drawable is not BoxWithDouble boxWithDouble) + throw new InvalidOperationException(); + + Add(boxWithDouble); + } + + public void Add(BoxWithDouble drawable) + { + if (drawable is not BoxWithDouble boxWithDouble) + throw new InvalidOperationException(); + + layoutContent.Height = (float)Math.Max(layoutContent.Height, boxWithDouble.DoubleLocation + boxWithDouble.DrawHeight); + layoutContent.Add(drawable); + } + + public DoubleScrollContainer() + { + // Managing our own custom layout within ScrollContent causes feedback with internal ScrollContainer calculations, + // so we must maintain one level of separation from ScrollContent. + base.Add(layoutContent = new Container + { + RelativeSizeAxes = Axes.X, + }); + } + + public override double GetChildPosInContent(Drawable d, Vector2 offset) + { + if (d is not BoxWithDouble boxWithDouble) + return base.GetChildPosInContent(d, offset); + + return boxWithDouble.DoubleLocation + offset.X; + } + + protected override void ApplyCurrentToContent() + { + Debug.Assert(ScrollDirection == Direction.Vertical); + + double scrollableExtent = -Current + ScrollableExtent * ScrollContent.RelativeAnchorPosition.Y; + + foreach (var d in layoutContent) + d.Y = (float)(d.DoubleLocation + scrollableExtent); + } + } + + public partial class BoxWithDouble : Box + { + public double DoubleLocation { get; set; } + } + } +} diff --git a/osu.Framework.Tests/Visual/UserInterface/TestSceneRearrangeableListContainer.cs b/osu.Framework.Tests/Visual/UserInterface/TestSceneRearrangeableListContainer.cs index 9b907e1755..8b7de51eb0 100644 --- a/osu.Framework.Tests/Visual/UserInterface/TestSceneRearrangeableListContainer.cs +++ b/osu.Framework.Tests/Visual/UserInterface/TestSceneRearrangeableListContainer.cs @@ -467,7 +467,7 @@ private BasicRearrangeableListItem.Button getDragger(int index) private partial class TestRearrangeableList : BasicRearrangeableListContainer { - public float ScrollPosition => ScrollContainer.Current; + public float ScrollPosition => (float)ScrollContainer.Current; public new IReadOnlyDictionary> ItemMap => base.ItemMap; diff --git a/osu.Framework/Graphics/Containers/ScrollContainer.cs b/osu.Framework/Graphics/Containers/ScrollContainer.cs index dbd89fed1c..0913a6d52d 100644 --- a/osu.Framework/Graphics/Containers/ScrollContainer.cs +++ b/osu.Framework/Graphics/Containers/ScrollContainer.cs @@ -118,7 +118,7 @@ public bool ScrollbarOverlapsContent /// /// The current scroll position. /// - public float Current { get; private set; } + public double Current { get; private set; } /// /// The target scroll position which is exponentially approached by current via a rate of distance decay. @@ -126,12 +126,12 @@ public bool ScrollbarOverlapsContent /// /// When not animating scroll position, this will always be equal to . /// - public float Target { get; private set; } + public double Target { get; private set; } /// /// The maximum distance that can be scrolled in the scroll direction. /// - public float ScrollableExtent => Math.Max(AvailableContent - DisplayableContent, 0); + public double ScrollableExtent => Math.Max(AvailableContent - DisplayableContent, 0); /// /// The maximum distance that the scrollbar can move in the scroll direction. @@ -139,14 +139,14 @@ public bool ScrollbarOverlapsContent /// /// May not be accurate to actual display of scrollbar if or are overridden. /// - protected float ScrollbarMovementExtent => Math.Max(DisplayableContent - Scrollbar.DrawSize[ScrollDim], 0); + protected double ScrollbarMovementExtent => Math.Max(DisplayableContent - Scrollbar.DrawSize[ScrollDim], 0); /// /// Clamp a value to the available scroll range. /// /// The value to clamp. /// An extension value beyond the normal extent. - protected float Clamp(float position, float extension = 0) => Math.Max(Math.Min(position, ScrollableExtent + extension), -extension); + protected double Clamp(double position, double extension = 0) => Math.Max(Math.Min(position, ScrollableExtent + extension), -extension); protected override Container Content => ScrollContent; @@ -345,8 +345,8 @@ protected override void OnDrag(DragEvent e) Vector2 childDelta = ToLocalSpace(e.ScreenSpaceMousePosition) - ToLocalSpace(e.ScreenSpaceLastMousePosition); - float scrollOffset = -childDelta[ScrollDim]; - float clampedScrollOffset = Clamp(Target + scrollOffset) - Clamp(Target); + double scrollOffset = -childDelta[ScrollDim]; + double clampedScrollOffset = Clamp(Target + scrollOffset) - Clamp(Target); // If we are dragging past the extent of the scrollable area, half the offset // such that the user can feel it. @@ -424,7 +424,7 @@ public void OffsetScrollPosition(float offset) Current += offset; } - private void scrollByOffset(float value, bool animated, double distanceDecay = float.PositiveInfinity) => + private void scrollByOffset(double value, bool animated, double distanceDecay = float.PositiveInfinity) => OnUserScroll(Target + value, animated, distanceDecay); /// @@ -454,7 +454,7 @@ public void ScrollToEnd(bool animated = true, bool allowDuringDrag = false) /// /// The amount by which we should scroll. /// Whether to animate the movement. - public void ScrollBy(float offset, bool animated = true) => scrollTo(Target + offset, animated); + public void ScrollBy(double offset, bool animated = true) => scrollTo(Target + offset, animated); /// /// Handle a scroll to an absolute position from a user input. @@ -462,7 +462,7 @@ public void ScrollToEnd(bool animated = true, bool allowDuringDrag = false) /// The position to scroll to. /// Whether to animate the movement. /// Controls the rate with which the target position is approached after jumping to a specific location. Default is . - protected virtual void OnUserScroll(float value, bool animated = true, double? distanceDecay = null) => + protected virtual void OnUserScroll(double value, bool animated = true, double? distanceDecay = null) => ScrollTo(value, animated, distanceDecay); /// @@ -471,9 +471,9 @@ protected virtual void OnUserScroll(float value, bool animated = true, double? d /// The position to scroll to. /// Whether to animate the movement. /// Controls the rate with which the target position is approached after jumping to a specific location. Default is . - public void ScrollTo(float value, bool animated = true, double? distanceDecay = null) => scrollTo(value, animated, distanceDecay ?? DistanceDecayJump); + public void ScrollTo(double value, bool animated = true, double? distanceDecay = null) => scrollTo(value, animated, distanceDecay ?? DistanceDecayJump); - private void scrollTo(float value, bool animated, double distanceDecay = float.PositiveInfinity) + private void scrollTo(double value, bool animated, double distanceDecay = double.PositiveInfinity) { Target = Clamp(value, ClampExtension); @@ -497,11 +497,11 @@ private void scrollTo(float value, bool animated, double distanceDecay = float.P /// Whether to animate the movement. public void ScrollIntoView(Drawable d, bool animated = true) { - float childPos0 = GetChildPosInContent(d); - float childPos1 = GetChildPosInContent(d, d.DrawSize); + double childPos0 = GetChildPosInContent(d); + double childPos1 = GetChildPosInContent(d, d.DrawSize); - float minPos = Math.Min(childPos0, childPos1); - float maxPos = Math.Max(childPos0, childPos1); + double minPos = Math.Min(childPos0, childPos1); + double maxPos = Math.Max(childPos0, childPos1); if (minPos < Current || (minPos > Current && d.DrawSize[ScrollDim] > DisplayableContent)) ScrollTo(minPos, animated); @@ -515,14 +515,14 @@ public void ScrollIntoView(Drawable d, bool animated = true) /// The child to get the position from. /// Positional offset in the child's space. /// The position of the child. - public float GetChildPosInContent(Drawable d, Vector2 offset) => d.ToSpaceOfOtherDrawable(offset, ScrollContent)[ScrollDim]; + public virtual double GetChildPosInContent(Drawable d, Vector2 offset) => d.ToSpaceOfOtherDrawable(offset, ScrollContent)[ScrollDim]; /// /// Determines the position of a child in the content. /// /// The child to get the position from. /// The position of the child. - public float GetChildPosInContent(Drawable d) => GetChildPosInContent(d, Vector2.Zero); + public double GetChildPosInContent(Drawable d) => GetChildPosInContent(d, Vector2.Zero); private void updatePosition() { @@ -544,15 +544,15 @@ private void updatePosition() localDistanceDecay = distance_decay_clamping * 2; // Lastly, we gradually nudge the target towards valid bounds. - Target = (float)Interpolation.Lerp(Clamp(Target), Target, Math.Exp(-distance_decay_clamping * Time.Elapsed)); + Target = Interpolation.Lerp(Clamp(Target), Target, Math.Exp(-distance_decay_clamping * Time.Elapsed)); - float clampedTarget = Clamp(Target); + double clampedTarget = Clamp(Target); if (Precision.AlmostEquals(clampedTarget, Target)) Target = clampedTarget; } // Exponential interpolation between the target and our current scroll position. - Current = (float)Interpolation.Lerp(Target, Current, Math.Exp(-localDistanceDecay * Time.Elapsed)); + Current = Interpolation.Lerp(Target, Current, Math.Exp(-localDistanceDecay * Time.Elapsed)); // This prevents us from entering the de-normalized range of floating point numbers when approaching target closely. if (Precision.AlmostEquals(Current, Target)) @@ -578,15 +578,27 @@ protected override void UpdateAfterChildren() } if (ScrollDirection == Direction.Horizontal) - { Scrollbar.X = ToScrollbarPosition(Current); - ScrollContent.X = -Current + ScrollableExtent * ScrollContent.RelativeAnchorPosition.X; - } else - { Scrollbar.Y = ToScrollbarPosition(Current); - ScrollContent.Y = -Current + ScrollableExtent * ScrollContent.RelativeAnchorPosition.Y; - } + + ApplyCurrentToContent(); + } + + /// + /// This is the final internal step of updating the scroll container, which takes + /// and applies it to in order to + /// correctly offset children. + /// + /// Overriding this method can be used to inhibit this default behaviour, to for instance + /// redirect the positioning to another container or change the way it is applied. + /// + protected virtual void ApplyCurrentToContent() + { + if (ScrollDirection == Direction.Horizontal) + ScrollContent.X = (float)(-Current + (ScrollableExtent * ScrollContent.RelativeAnchorPosition.X)); + else + ScrollContent.Y = (float)(-Current + (ScrollableExtent * ScrollContent.RelativeAnchorPosition.Y)); } /// @@ -594,12 +606,12 @@ protected override void UpdateAfterChildren() /// /// The absolute scroll position (e.g. ). /// The scrollbar position. - protected virtual float ToScrollbarPosition(float scrollPosition) + protected virtual float ToScrollbarPosition(double scrollPosition) { if (Precision.AlmostEquals(0, ScrollableExtent)) return 0; - return ScrollbarMovementExtent * (scrollPosition / ScrollableExtent); + return (float)(ScrollbarMovementExtent * (scrollPosition / ScrollableExtent)); } /// @@ -612,7 +624,7 @@ protected virtual float FromScrollbarPosition(float scrollbarPosition) if (Precision.AlmostEquals(0, ScrollbarMovementExtent)) return 0; - return ScrollableExtent * (scrollbarPosition / ScrollbarMovementExtent); + return (float)(ScrollableExtent * (scrollbarPosition / ScrollbarMovementExtent)); } ///