Skip to content

Commit c3a701a

Browse files
authored
Merge pull request #6467 from peppy/scroll-container-double-precision
Use `double` in `ScrollContainer` for scroll tracking
2 parents 13b63f7 + 0e558e2 commit c3a701a

File tree

4 files changed

+193
-32
lines changed

4 files changed

+193
-32
lines changed

osu.Framework.Tests/Visual/Containers/TestSceneScrollContainer.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -525,7 +525,7 @@ private void scrollTo(float position, float scrollContentHeight, float extension
525525
AddStep($"scroll to {position}", () =>
526526
{
527527
scrollContainer.ScrollTo(position, false);
528-
immediateScrollPosition = scrollContainer.Current;
528+
immediateScrollPosition = (float)scrollContainer.Current;
529529
});
530530

531531
AddAssert($"immediately scrolled to {clampedTarget}", () => Precision.AlmostEquals(clampedTarget, immediateScrollPosition, 1));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2+
// See the LICENCE file in the repository root for full licence text.
3+
4+
using System;
5+
using System.Diagnostics;
6+
using System.Linq;
7+
using NUnit.Framework;
8+
using osu.Framework.Graphics;
9+
using osu.Framework.Graphics.Containers;
10+
using osu.Framework.Graphics.Shapes;
11+
using osu.Framework.Testing;
12+
using osu.Framework.Utils;
13+
using osuTK;
14+
using osuTK.Graphics;
15+
16+
namespace osu.Framework.Tests.Visual.Containers
17+
{
18+
public partial class TestSceneScrollContainerDoublePrecision : ManualInputManagerTestScene
19+
{
20+
private const float item_height = 5000;
21+
private const int item_count = 8000;
22+
23+
private ScrollContainer<Drawable> scrollContainer = null!;
24+
25+
[SetUp]
26+
public void Setup() => Schedule(Clear);
27+
28+
[Test]
29+
public void TestStandard()
30+
{
31+
AddStep("Create scroll container", () =>
32+
{
33+
Add(scrollContainer = new BasicScrollContainer
34+
{
35+
Anchor = Anchor.Centre,
36+
Origin = Anchor.Centre,
37+
ScrollbarVisible = true,
38+
RelativeSizeAxes = Axes.Both,
39+
Size = new Vector2(0.7f, 0.9f),
40+
});
41+
42+
for (int i = 0; i < item_count; i++)
43+
{
44+
scrollContainer.Add(new BoxWithDouble
45+
{
46+
Colour = new Color4(RNG.NextSingle(1), RNG.NextSingle(1), RNG.NextSingle(1), 1),
47+
RelativeSizeAxes = Axes.X,
48+
Height = item_height,
49+
Y = i * item_height,
50+
});
51+
}
52+
});
53+
54+
scrollIntoView(item_count - 2);
55+
scrollIntoView(item_count - 1);
56+
}
57+
58+
[Test]
59+
public void TestDoublePrecision()
60+
{
61+
AddStep("Create scroll container", () =>
62+
{
63+
Add(scrollContainer = new DoubleScrollContainer
64+
{
65+
Anchor = Anchor.Centre,
66+
Origin = Anchor.Centre,
67+
ScrollbarVisible = true,
68+
RelativeSizeAxes = Axes.Both,
69+
Size = new Vector2(0.7f, 0.9f),
70+
});
71+
72+
for (int i = 0; i < item_count; i++)
73+
{
74+
scrollContainer.Add(new BoxWithDouble
75+
{
76+
Colour = new Color4(RNG.NextSingle(1), RNG.NextSingle(1), RNG.NextSingle(1), 1),
77+
RelativeSizeAxes = Axes.X,
78+
Height = item_height,
79+
DoubleLocation = i * item_height,
80+
});
81+
}
82+
});
83+
84+
scrollIntoView(item_count - 2);
85+
scrollIntoView(item_count - 1);
86+
}
87+
88+
private void scrollIntoView(int index)
89+
{
90+
AddStep($"scroll {index} into view", () => scrollContainer.ScrollIntoView(scrollContainer.ChildrenOfType<BoxWithDouble>().Skip(index).First()));
91+
AddUntilStep($"{index} is visible", () => !scrollContainer.ChildrenOfType<BoxWithDouble>().Skip(index).First().IsMaskedAway);
92+
}
93+
94+
public partial class DoubleScrollContainer : BasicScrollContainer
95+
{
96+
private readonly Container<BoxWithDouble> layoutContent;
97+
98+
public override void Add(Drawable drawable)
99+
{
100+
if (drawable is not BoxWithDouble boxWithDouble)
101+
throw new InvalidOperationException();
102+
103+
Add(boxWithDouble);
104+
}
105+
106+
public void Add(BoxWithDouble drawable)
107+
{
108+
if (drawable is not BoxWithDouble boxWithDouble)
109+
throw new InvalidOperationException();
110+
111+
layoutContent.Height = (float)Math.Max(layoutContent.Height, boxWithDouble.DoubleLocation + boxWithDouble.DrawHeight);
112+
layoutContent.Add(drawable);
113+
}
114+
115+
public DoubleScrollContainer()
116+
{
117+
// Managing our own custom layout within ScrollContent causes feedback with internal ScrollContainer calculations,
118+
// so we must maintain one level of separation from ScrollContent.
119+
base.Add(layoutContent = new Container<BoxWithDouble>
120+
{
121+
RelativeSizeAxes = Axes.X,
122+
});
123+
}
124+
125+
public override double GetChildPosInContent(Drawable d, Vector2 offset)
126+
{
127+
if (d is not BoxWithDouble boxWithDouble)
128+
return base.GetChildPosInContent(d, offset);
129+
130+
return boxWithDouble.DoubleLocation + offset.X;
131+
}
132+
133+
protected override void ApplyCurrentToContent()
134+
{
135+
Debug.Assert(ScrollDirection == Direction.Vertical);
136+
137+
double scrollableExtent = -Current + ScrollableExtent * ScrollContent.RelativeAnchorPosition.Y;
138+
139+
foreach (var d in layoutContent)
140+
d.Y = (float)(d.DoubleLocation + scrollableExtent);
141+
}
142+
}
143+
144+
public partial class BoxWithDouble : Box
145+
{
146+
public double DoubleLocation { get; set; }
147+
}
148+
}
149+
}

osu.Framework.Tests/Visual/UserInterface/TestSceneRearrangeableListContainer.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,7 @@ private BasicRearrangeableListItem<int>.Button getDragger(int index)
467467

468468
private partial class TestRearrangeableList : BasicRearrangeableListContainer<int>
469469
{
470-
public float ScrollPosition => ScrollContainer.Current;
470+
public float ScrollPosition => (float)ScrollContainer.Current;
471471

472472
public new IReadOnlyDictionary<int, RearrangeableListItem<int>> ItemMap => base.ItemMap;
473473

osu.Framework/Graphics/Containers/ScrollContainer.cs

+42-30
Original file line numberDiff line numberDiff line change
@@ -118,35 +118,35 @@ public bool ScrollbarOverlapsContent
118118
/// <summary>
119119
/// The current scroll position.
120120
/// </summary>
121-
public float Current { get; private set; }
121+
public double Current { get; private set; }
122122

123123
/// <summary>
124124
/// The target scroll position which is exponentially approached by current via a rate of distance decay.
125125
/// </summary>
126126
/// <remarks>
127127
/// When not animating scroll position, this will always be equal to <see cref="Current"/>.
128128
/// </remarks>
129-
public float Target { get; private set; }
129+
public double Target { get; private set; }
130130

131131
/// <summary>
132132
/// The maximum distance that can be scrolled in the scroll direction.
133133
/// </summary>
134-
public float ScrollableExtent => Math.Max(AvailableContent - DisplayableContent, 0);
134+
public double ScrollableExtent => Math.Max(AvailableContent - DisplayableContent, 0);
135135

136136
/// <summary>
137137
/// The maximum distance that the scrollbar can move in the scroll direction.
138138
/// </summary>
139139
/// <remarks>
140140
/// May not be accurate to actual display of scrollbar if <see cref="ToScrollbarPosition"/> or <see cref="FromScrollbarPosition"/> are overridden.
141141
/// </remarks>
142-
protected float ScrollbarMovementExtent => Math.Max(DisplayableContent - Scrollbar.DrawSize[ScrollDim], 0);
142+
protected double ScrollbarMovementExtent => Math.Max(DisplayableContent - Scrollbar.DrawSize[ScrollDim], 0);
143143

144144
/// <summary>
145145
/// Clamp a value to the available scroll range.
146146
/// </summary>
147147
/// <param name="position">The value to clamp.</param>
148148
/// <param name="extension">An extension value beyond the normal extent.</param>
149-
protected float Clamp(float position, float extension = 0) => Math.Max(Math.Min(position, ScrollableExtent + extension), -extension);
149+
protected double Clamp(double position, double extension = 0) => Math.Max(Math.Min(position, ScrollableExtent + extension), -extension);
150150

151151
protected override Container<T> Content => ScrollContent;
152152

@@ -345,8 +345,8 @@ protected override void OnDrag(DragEvent e)
345345

346346
Vector2 childDelta = ToLocalSpace(e.ScreenSpaceMousePosition) - ToLocalSpace(e.ScreenSpaceLastMousePosition);
347347

348-
float scrollOffset = -childDelta[ScrollDim];
349-
float clampedScrollOffset = Clamp(Target + scrollOffset) - Clamp(Target);
348+
double scrollOffset = -childDelta[ScrollDim];
349+
double clampedScrollOffset = Clamp(Target + scrollOffset) - Clamp(Target);
350350

351351
// If we are dragging past the extent of the scrollable area, half the offset
352352
// such that the user can feel it.
@@ -424,7 +424,7 @@ public void OffsetScrollPosition(float offset)
424424
Current += offset;
425425
}
426426

427-
private void scrollByOffset(float value, bool animated, double distanceDecay = float.PositiveInfinity) =>
427+
private void scrollByOffset(double value, bool animated, double distanceDecay = float.PositiveInfinity) =>
428428
OnUserScroll(Target + value, animated, distanceDecay);
429429

430430
/// <summary>
@@ -454,15 +454,15 @@ public void ScrollToEnd(bool animated = true, bool allowDuringDrag = false)
454454
/// </summary>
455455
/// <param name="offset">The amount by which we should scroll.</param>
456456
/// <param name="animated">Whether to animate the movement.</param>
457-
public void ScrollBy(float offset, bool animated = true) => scrollTo(Target + offset, animated);
457+
public void ScrollBy(double offset, bool animated = true) => scrollTo(Target + offset, animated);
458458

459459
/// <summary>
460460
/// Handle a scroll to an absolute position from a user input.
461461
/// </summary>
462462
/// <param name="value">The position to scroll to.</param>
463463
/// <param name="animated">Whether to animate the movement.</param>
464464
/// <param name="distanceDecay">Controls the rate with which the target position is approached after jumping to a specific location. Default is <see cref="DistanceDecayJump"/>.</param>
465-
protected virtual void OnUserScroll(float value, bool animated = true, double? distanceDecay = null) =>
465+
protected virtual void OnUserScroll(double value, bool animated = true, double? distanceDecay = null) =>
466466
ScrollTo(value, animated, distanceDecay);
467467

468468
/// <summary>
@@ -471,9 +471,9 @@ protected virtual void OnUserScroll(float value, bool animated = true, double? d
471471
/// <param name="value">The position to scroll to.</param>
472472
/// <param name="animated">Whether to animate the movement.</param>
473473
/// <param name="distanceDecay">Controls the rate with which the target position is approached after jumping to a specific location. Default is <see cref="DistanceDecayJump"/>.</param>
474-
public void ScrollTo(float value, bool animated = true, double? distanceDecay = null) => scrollTo(value, animated, distanceDecay ?? DistanceDecayJump);
474+
public void ScrollTo(double value, bool animated = true, double? distanceDecay = null) => scrollTo(value, animated, distanceDecay ?? DistanceDecayJump);
475475

476-
private void scrollTo(float value, bool animated, double distanceDecay = float.PositiveInfinity)
476+
private void scrollTo(double value, bool animated, double distanceDecay = double.PositiveInfinity)
477477
{
478478
Target = Clamp(value, ClampExtension);
479479

@@ -497,11 +497,11 @@ private void scrollTo(float value, bool animated, double distanceDecay = float.P
497497
/// <param name="animated">Whether to animate the movement.</param>
498498
public void ScrollIntoView(Drawable d, bool animated = true)
499499
{
500-
float childPos0 = GetChildPosInContent(d);
501-
float childPos1 = GetChildPosInContent(d, d.DrawSize);
500+
double childPos0 = GetChildPosInContent(d);
501+
double childPos1 = GetChildPosInContent(d, d.DrawSize);
502502

503-
float minPos = Math.Min(childPos0, childPos1);
504-
float maxPos = Math.Max(childPos0, childPos1);
503+
double minPos = Math.Min(childPos0, childPos1);
504+
double maxPos = Math.Max(childPos0, childPos1);
505505

506506
if (minPos < Current || (minPos > Current && d.DrawSize[ScrollDim] > DisplayableContent))
507507
ScrollTo(minPos, animated);
@@ -515,14 +515,14 @@ public void ScrollIntoView(Drawable d, bool animated = true)
515515
/// <param name="d">The child to get the position from.</param>
516516
/// <param name="offset">Positional offset in the child's space.</param>
517517
/// <returns>The position of the child.</returns>
518-
public float GetChildPosInContent(Drawable d, Vector2 offset) => d.ToSpaceOfOtherDrawable(offset, ScrollContent)[ScrollDim];
518+
public virtual double GetChildPosInContent(Drawable d, Vector2 offset) => d.ToSpaceOfOtherDrawable(offset, ScrollContent)[ScrollDim];
519519

520520
/// <summary>
521521
/// Determines the position of a child in the content.
522522
/// </summary>
523523
/// <param name="d">The child to get the position from.</param>
524524
/// <returns>The position of the child.</returns>
525-
public float GetChildPosInContent(Drawable d) => GetChildPosInContent(d, Vector2.Zero);
525+
public double GetChildPosInContent(Drawable d) => GetChildPosInContent(d, Vector2.Zero);
526526

527527
private void updatePosition()
528528
{
@@ -544,15 +544,15 @@ private void updatePosition()
544544
localDistanceDecay = distance_decay_clamping * 2;
545545

546546
// Lastly, we gradually nudge the target towards valid bounds.
547-
Target = (float)Interpolation.Lerp(Clamp(Target), Target, Math.Exp(-distance_decay_clamping * Time.Elapsed));
547+
Target = Interpolation.Lerp(Clamp(Target), Target, Math.Exp(-distance_decay_clamping * Time.Elapsed));
548548

549-
float clampedTarget = Clamp(Target);
549+
double clampedTarget = Clamp(Target);
550550
if (Precision.AlmostEquals(clampedTarget, Target))
551551
Target = clampedTarget;
552552
}
553553

554554
// Exponential interpolation between the target and our current scroll position.
555-
Current = (float)Interpolation.Lerp(Target, Current, Math.Exp(-localDistanceDecay * Time.Elapsed));
555+
Current = Interpolation.Lerp(Target, Current, Math.Exp(-localDistanceDecay * Time.Elapsed));
556556

557557
// This prevents us from entering the de-normalized range of floating point numbers when approaching target closely.
558558
if (Precision.AlmostEquals(Current, Target))
@@ -578,28 +578,40 @@ protected override void UpdateAfterChildren()
578578
}
579579

580580
if (ScrollDirection == Direction.Horizontal)
581-
{
582581
Scrollbar.X = ToScrollbarPosition(Current);
583-
ScrollContent.X = -Current + ScrollableExtent * ScrollContent.RelativeAnchorPosition.X;
584-
}
585582
else
586-
{
587583
Scrollbar.Y = ToScrollbarPosition(Current);
588-
ScrollContent.Y = -Current + ScrollableExtent * ScrollContent.RelativeAnchorPosition.Y;
589-
}
584+
585+
ApplyCurrentToContent();
586+
}
587+
588+
/// <summary>
589+
/// This is the final internal step of updating the scroll container, which takes
590+
/// <see cref="Current"/> and applies it to <see cref="ScrollContent"/> in order to
591+
/// correctly offset children.
592+
///
593+
/// Overriding this method can be used to inhibit this default behaviour, to for instance
594+
/// redirect the positioning to another container or change the way it is applied.
595+
/// </summary>
596+
protected virtual void ApplyCurrentToContent()
597+
{
598+
if (ScrollDirection == Direction.Horizontal)
599+
ScrollContent.X = (float)(-Current + (ScrollableExtent * ScrollContent.RelativeAnchorPosition.X));
600+
else
601+
ScrollContent.Y = (float)(-Current + (ScrollableExtent * ScrollContent.RelativeAnchorPosition.Y));
590602
}
591603

592604
/// <summary>
593605
/// Converts a scroll position to a scrollbar position.
594606
/// </summary>
595607
/// <param name="scrollPosition">The absolute scroll position (e.g. <see cref="Current"/>).</param>
596608
/// <returns>The scrollbar position.</returns>
597-
protected virtual float ToScrollbarPosition(float scrollPosition)
609+
protected virtual float ToScrollbarPosition(double scrollPosition)
598610
{
599611
if (Precision.AlmostEquals(0, ScrollableExtent))
600612
return 0;
601613

602-
return ScrollbarMovementExtent * (scrollPosition / ScrollableExtent);
614+
return (float)(ScrollbarMovementExtent * (scrollPosition / ScrollableExtent));
603615
}
604616

605617
/// <summary>
@@ -612,7 +624,7 @@ protected virtual float FromScrollbarPosition(float scrollbarPosition)
612624
if (Precision.AlmostEquals(0, ScrollbarMovementExtent))
613625
return 0;
614626

615-
return ScrollableExtent * (scrollbarPosition / ScrollbarMovementExtent);
627+
return (float)(ScrollableExtent * (scrollbarPosition / ScrollbarMovementExtent));
616628
}
617629

618630
/// <summary>

0 commit comments

Comments
 (0)