mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 10:52:53 +08:00
Merge branch 'master' into settings-reduce-visual-clutter
This commit is contained in:
commit
c0a9d88a14
@ -30,6 +30,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
|
||||
public void UpdateResult() => base.UpdateResult(true);
|
||||
|
||||
protected override double MaximumJudgementOffset => base.MaximumJudgementOffset * release_window_lenience;
|
||||
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
{
|
||||
Debug.Assert(HitObject.HitWindows != null);
|
||||
|
@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
DrawableManiaHitObject maniaObject = (DrawableManiaHitObject)hitObject;
|
||||
maniaObject.CheckHittable = hitPolicy.IsHittable;
|
||||
|
||||
HitObjectContainer.Add(hitObject);
|
||||
base.Add(hitObject);
|
||||
}
|
||||
|
||||
public override bool Remove(DrawableHitObject h)
|
||||
|
@ -9,9 +9,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
@ -61,13 +59,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
PositionBindable.BindTo(HitObject.PositionBindable);
|
||||
StackHeightBindable.BindTo(HitObject.StackHeightBindable);
|
||||
ScaleBindable.BindTo(HitObject.ScaleBindable);
|
||||
|
||||
// Manually set to reduce the number of future alive objects to a bare minimum.
|
||||
LifetimeStart = HitObject.StartTime - HitObject.TimePreempt;
|
||||
|
||||
// Arbitrary lifetime end to prevent past objects in idle states remaining alive in non-frame-stable contexts.
|
||||
// An extra 1000ms is added to always overestimate the true lifetime, and a more exact value is set by hit transforms and the following expiry.
|
||||
LifetimeEnd = HitObject.GetEndTime() + HitObject.HitWindows.WindowFor(HitResult.Miss) + 1000;
|
||||
}
|
||||
|
||||
protected override void OnFree()
|
||||
|
@ -1,6 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
public class DrawableSpinnerTick : DrawableOsuHitObject
|
||||
@ -17,6 +19,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
}
|
||||
|
||||
private DrawableSpinner drawableSpinner;
|
||||
|
||||
protected override void OnParentReceived(DrawableHitObject parent)
|
||||
{
|
||||
base.OnParentReceived(parent);
|
||||
drawableSpinner = (DrawableSpinner)parent;
|
||||
}
|
||||
|
||||
protected override double MaximumJudgementOffset => drawableSpinner.HitObject.Duration;
|
||||
|
||||
/// <summary>
|
||||
/// Apply a judgement result.
|
||||
/// </summary>
|
||||
|
@ -176,6 +176,8 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
public OsuHitObjectLifetimeEntry(HitObject hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
// Prevent past objects in idles states from remaining alive as their end times are skipped in non-frame-stable contexts.
|
||||
LifetimeEnd = HitObject.GetEndTime() + HitObject.HitWindows.WindowFor(HitResult.Miss);
|
||||
}
|
||||
|
||||
protected override double InitialLifetimeOffset => ((OsuHitObject)HitObject).TimePreempt;
|
||||
|
@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
Filled = HitObject.FirstTick
|
||||
});
|
||||
|
||||
protected override double MaximumJudgementOffset => HitObject.HitWindow;
|
||||
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
{
|
||||
if (!userTriggered)
|
||||
|
@ -21,13 +21,13 @@ using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
@ -46,6 +46,50 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() => testClock.CurrentTime = 0);
|
||||
|
||||
[TestCase("pooled")]
|
||||
[TestCase("non-pooled")]
|
||||
public void TestHitObjectLifetime(string pooled)
|
||||
{
|
||||
var beatmap = createBeatmap(_ => pooled == "pooled" ? new TestPooledHitObject() : new TestHitObject());
|
||||
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range });
|
||||
createTest(beatmap);
|
||||
|
||||
assertPosition(0, 0f);
|
||||
assertDead(3);
|
||||
|
||||
setTime(3 * time_range);
|
||||
assertPosition(3, 0f);
|
||||
assertDead(0);
|
||||
|
||||
setTime(0 * time_range);
|
||||
assertPosition(0, 0f);
|
||||
assertDead(3);
|
||||
}
|
||||
|
||||
[TestCase("pooled")]
|
||||
[TestCase("non-pooled")]
|
||||
public void TestNestedHitObject(string pooled)
|
||||
{
|
||||
var beatmap = createBeatmap(i =>
|
||||
{
|
||||
var h = pooled == "pooled" ? new TestPooledParentHitObject() : new TestParentHitObject();
|
||||
h.Duration = 300;
|
||||
h.ChildTimeOffset = i % 3 * 100;
|
||||
return h;
|
||||
});
|
||||
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range });
|
||||
createTest(beatmap);
|
||||
|
||||
assertPosition(0, 0f);
|
||||
assertHeight(0);
|
||||
assertChildPosition(0);
|
||||
|
||||
setTime(5 * time_range);
|
||||
assertPosition(5, 0f);
|
||||
assertHeight(5);
|
||||
assertChildPosition(5);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRelativeBeatLengthScaleSingleTimingPoint()
|
||||
{
|
||||
@ -147,8 +191,37 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
assertPosition(1, 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a <see cref="DrawableTestHitObject" /> corresponding to the <paramref name="index"/>'th <see cref="TestHitObject"/>.
|
||||
/// When the hit object is not alive, `null` is returned.
|
||||
/// </summary>
|
||||
[CanBeNull]
|
||||
private DrawableTestHitObject getDrawableHitObject(int index)
|
||||
{
|
||||
var hitObject = drawableRuleset.Beatmap.HitObjects.ElementAt(index);
|
||||
return (DrawableTestHitObject)drawableRuleset.Playfield.HitObjectContainer.AliveObjects.FirstOrDefault(obj => obj.HitObject == hitObject);
|
||||
}
|
||||
|
||||
private float yScale => drawableRuleset.Playfield.HitObjectContainer.DrawHeight;
|
||||
|
||||
private void assertDead(int index) => AddAssert($"hitobject {index} is dead", () => getDrawableHitObject(index) == null);
|
||||
|
||||
private void assertHeight(int index) => AddAssert($"hitobject {index} height", () =>
|
||||
{
|
||||
var d = getDrawableHitObject(index);
|
||||
return d != null && Precision.AlmostEquals(d.DrawHeight, yScale * (float)(d.HitObject.Duration / time_range), 0.1f);
|
||||
});
|
||||
|
||||
private void assertChildPosition(int index) => AddAssert($"hitobject {index} child position", () =>
|
||||
{
|
||||
var d = getDrawableHitObject(index);
|
||||
return d is DrawableTestParentHitObject && Precision.AlmostEquals(
|
||||
d.NestedHitObjects.First().DrawPosition.Y,
|
||||
yScale * (float)((TestParentHitObject)d.HitObject).ChildTimeOffset / time_range, 0.1f);
|
||||
});
|
||||
|
||||
private void assertPosition(int index, float relativeY) => AddAssert($"hitobject {index} at {relativeY}",
|
||||
() => Precision.AlmostEquals(drawableRuleset.Playfield.AllHitObjects.ElementAt(index).DrawPosition.Y, drawableRuleset.Playfield.HitObjectContainer.DrawHeight * relativeY));
|
||||
() => Precision.AlmostEquals(getDrawableHitObject(index)?.DrawPosition.Y ?? -1, yScale * relativeY));
|
||||
|
||||
private void setTime(double time)
|
||||
{
|
||||
@ -160,12 +233,16 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
/// The hitobjects are spaced <see cref="time_range"/> milliseconds apart.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="IBeatmap"/>.</returns>
|
||||
private IBeatmap createBeatmap()
|
||||
private IBeatmap createBeatmap(Func<int, TestHitObject> createAction = null)
|
||||
{
|
||||
var beatmap = new Beatmap<HitObject> { BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo } };
|
||||
var beatmap = new Beatmap<TestHitObject> { BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo } };
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
beatmap.HitObjects.Add(new HitObject { StartTime = i * time_range });
|
||||
{
|
||||
var h = createAction?.Invoke(i) ?? new TestHitObject();
|
||||
h.StartTime = i * time_range;
|
||||
beatmap.HitObjects.Add(h);
|
||||
}
|
||||
|
||||
return beatmap;
|
||||
}
|
||||
@ -225,7 +302,21 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
TimeRange.Value = time_range;
|
||||
}
|
||||
|
||||
public override DrawableHitObject<TestHitObject> CreateDrawableRepresentation(TestHitObject h) => new DrawableTestHitObject(h);
|
||||
public override DrawableHitObject<TestHitObject> CreateDrawableRepresentation(TestHitObject h)
|
||||
{
|
||||
switch (h)
|
||||
{
|
||||
case TestPooledHitObject _:
|
||||
case TestPooledParentHitObject _:
|
||||
return null;
|
||||
|
||||
case TestParentHitObject p:
|
||||
return new DrawableTestParentHitObject(p);
|
||||
|
||||
default:
|
||||
return new DrawableTestHitObject(h);
|
||||
}
|
||||
}
|
||||
|
||||
protected override PassThroughInputManager CreateInputManager() => new PassThroughInputManager();
|
||||
|
||||
@ -265,6 +356,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
RegisterPool<TestPooledHitObject, DrawableTestPooledHitObject>(1);
|
||||
RegisterPool<TestPooledParentHitObject, DrawableTestPooledParentHitObject>(1);
|
||||
}
|
||||
}
|
||||
|
||||
@ -277,30 +371,46 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
public override bool CanConvert() => true;
|
||||
|
||||
protected override IEnumerable<TestHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
|
||||
{
|
||||
yield return new TestHitObject
|
||||
{
|
||||
StartTime = original.StartTime,
|
||||
Duration = (original as IHasDuration)?.Duration ?? 100
|
||||
};
|
||||
}
|
||||
protected override IEnumerable<TestHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) =>
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region HitObject
|
||||
|
||||
private class TestHitObject : ConvertHitObject, IHasDuration
|
||||
private class TestHitObject : HitObject, IHasDuration
|
||||
{
|
||||
public double EndTime => StartTime + Duration;
|
||||
|
||||
public double Duration { get; set; }
|
||||
public double Duration { get; set; } = 100;
|
||||
}
|
||||
|
||||
private class TestPooledHitObject : TestHitObject
|
||||
{
|
||||
}
|
||||
|
||||
private class TestParentHitObject : TestHitObject
|
||||
{
|
||||
public double ChildTimeOffset;
|
||||
|
||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||
{
|
||||
AddNested(new TestHitObject { StartTime = StartTime + ChildTimeOffset });
|
||||
}
|
||||
}
|
||||
|
||||
private class TestPooledParentHitObject : TestParentHitObject
|
||||
{
|
||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||
{
|
||||
AddNested(new TestPooledHitObject { StartTime = StartTime + ChildTimeOffset });
|
||||
}
|
||||
}
|
||||
|
||||
private class DrawableTestHitObject : DrawableHitObject<TestHitObject>
|
||||
{
|
||||
public DrawableTestHitObject(TestHitObject hitObject)
|
||||
public DrawableTestHitObject([CanBeNull] TestHitObject hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
Anchor = Anchor.TopCentre;
|
||||
@ -324,6 +434,52 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Update() => LifetimeEnd = HitObject.EndTime;
|
||||
}
|
||||
|
||||
private class DrawableTestPooledHitObject : DrawableTestHitObject
|
||||
{
|
||||
public DrawableTestPooledHitObject()
|
||||
: base(null)
|
||||
{
|
||||
InternalChildren[0].Colour = Color4.LightSkyBlue;
|
||||
InternalChildren[1].Colour = Color4.Blue;
|
||||
}
|
||||
}
|
||||
|
||||
private class DrawableTestParentHitObject : DrawableTestHitObject
|
||||
{
|
||||
private readonly Container<DrawableHitObject> container;
|
||||
|
||||
public DrawableTestParentHitObject([CanBeNull] TestHitObject hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
InternalChildren[0].Colour = Color4.LightYellow;
|
||||
InternalChildren[1].Colour = Color4.Yellow;
|
||||
|
||||
AddInternal(container = new Container<DrawableHitObject>
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
});
|
||||
}
|
||||
|
||||
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) =>
|
||||
new DrawableTestHitObject((TestHitObject)hitObject);
|
||||
|
||||
protected override void AddNestedHitObject(DrawableHitObject hitObject) => container.Add(hitObject);
|
||||
|
||||
protected override void ClearNestedHitObjects() => container.Clear(false);
|
||||
}
|
||||
|
||||
private class DrawableTestPooledParentHitObject : DrawableTestParentHitObject
|
||||
{
|
||||
public DrawableTestPooledParentHitObject()
|
||||
: base(null)
|
||||
{
|
||||
InternalChildren[0].Colour = Color4.LightSeaGreen;
|
||||
InternalChildren[1].Colour = Color4.Green;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
@ -917,7 +917,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
get
|
||||
{
|
||||
foreach (var item in ScrollableContent)
|
||||
foreach (var item in Scroll.Children)
|
||||
{
|
||||
yield return item;
|
||||
|
||||
|
@ -12,7 +12,19 @@ using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Graphics.Containers
|
||||
{
|
||||
public class OsuScrollContainer : ScrollContainer<Drawable>
|
||||
public class OsuScrollContainer : OsuScrollContainer<Drawable>
|
||||
{
|
||||
public OsuScrollContainer()
|
||||
{
|
||||
}
|
||||
|
||||
public OsuScrollContainer(Direction direction)
|
||||
: base(direction)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class OsuScrollContainer<T> : ScrollContainer<T> where T : Drawable
|
||||
{
|
||||
public const float SCROLL_BAR_HEIGHT = 10;
|
||||
public const float SCROLL_BAR_PADDING = 3;
|
||||
|
@ -710,6 +710,18 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
UpdateResult(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The maximum offset from the end time of <see cref="HitObject"/> at which this <see cref="DrawableHitObject"/> can be judged.
|
||||
/// The time offset of <see cref="Result"/> will be clamped to this value during <see cref="ApplyResult"/>.
|
||||
/// <para>
|
||||
/// Defaults to the miss window of <see cref="HitObject"/>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This does not affect the time offset provided to invocations of <see cref="CheckForResult"/>.
|
||||
/// </remarks>
|
||||
protected virtual double MaximumJudgementOffset => HitObject.HitWindows?.WindowFor(HitResult.Miss) ?? 0;
|
||||
|
||||
/// <summary>
|
||||
/// Applies the <see cref="Result"/> of this <see cref="DrawableHitObject"/>, notifying responders such as
|
||||
/// the <see cref="ScoreProcessor"/> of the <see cref="JudgementResult"/>.
|
||||
@ -749,14 +761,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
$"{GetType().ReadableName()} applied an invalid hit result (was: {Result.Type}, expected: [{Result.Judgement.MinResult} ... {Result.Judgement.MaxResult}]).");
|
||||
}
|
||||
|
||||
// Ensure that the judgement is given a valid time offset, because this may not get set by the caller
|
||||
var endTime = HitObject.GetEndTime();
|
||||
|
||||
Result.TimeOffset = Time.Current - endTime;
|
||||
|
||||
double missWindow = HitObject.HitWindows.WindowFor(HitResult.Miss);
|
||||
if (missWindow > 0)
|
||||
Result.TimeOffset = Math.Min(Result.TimeOffset, missWindow);
|
||||
Result.TimeOffset = Math.Min(MaximumJudgementOffset, Time.Current - HitObject.GetEndTime());
|
||||
|
||||
if (Result.HasResult)
|
||||
updateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss);
|
||||
@ -778,8 +783,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
if (Judged)
|
||||
return false;
|
||||
|
||||
var endTime = HitObject.GetEndTime();
|
||||
CheckForResult(userTriggered, Time.Current - endTime);
|
||||
CheckForResult(userTriggered, Time.Current - HitObject.GetEndTime());
|
||||
|
||||
return Judged;
|
||||
}
|
||||
|
@ -114,6 +114,7 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
bindStartTime(drawable);
|
||||
AddInternal(drawableMap[entry] = drawable, false);
|
||||
OnAdd(drawable);
|
||||
|
||||
HitObjectUsageBegan?.Invoke(entry.HitObject);
|
||||
}
|
||||
@ -129,6 +130,7 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
drawableMap.Remove(entry);
|
||||
|
||||
OnRemove(drawable);
|
||||
unbindStartTime(drawable);
|
||||
RemoveInternal(drawable);
|
||||
|
||||
@ -147,10 +149,12 @@ namespace osu.Game.Rulesets.UI
|
||||
hitObject.OnRevertResult += onRevertResult;
|
||||
|
||||
AddInternal(hitObject);
|
||||
OnAdd(hitObject);
|
||||
}
|
||||
|
||||
public virtual bool Remove(DrawableHitObject hitObject)
|
||||
{
|
||||
OnRemove(hitObject);
|
||||
if (!RemoveInternal(hitObject))
|
||||
return false;
|
||||
|
||||
@ -178,6 +182,26 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a <see cref="DrawableHitObject"/> is added to this container.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is not invoked for nested <see cref="DrawableHitObject"/>s.
|
||||
/// </remarks>
|
||||
protected virtual void OnAdd(DrawableHitObject drawableHitObject)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a <see cref="DrawableHitObject"/> is removed from this container.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is not invoked for nested <see cref="DrawableHitObject"/>s.
|
||||
/// </remarks>
|
||||
protected virtual void OnRemove(DrawableHitObject drawableHitObject)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void Clear(bool disposeChildren = true)
|
||||
{
|
||||
lifetimeManager.ClearEntries();
|
||||
|
@ -135,10 +135,8 @@ namespace osu.Game.Rulesets.UI
|
||||
/// <param name="h">The DrawableHitObject to add.</param>
|
||||
public virtual void Add(DrawableHitObject h)
|
||||
{
|
||||
if (h.IsInitialized)
|
||||
throw new InvalidOperationException($"{nameof(Add)} doesn't support {nameof(DrawableHitObject)} reuse. Use pooling instead.");
|
||||
|
||||
onNewDrawableHitObject(h);
|
||||
if (!h.IsInitialized)
|
||||
onNewDrawableHitObject(h);
|
||||
|
||||
HitObjectContainer.Add(h);
|
||||
OnHitObjectAdded(h.HitObject);
|
||||
|
@ -2,13 +2,10 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Layout;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osuTK;
|
||||
@ -19,7 +16,16 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
{
|
||||
private readonly IBindable<double> timeRange = new BindableDouble();
|
||||
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
|
||||
private readonly Dictionary<DrawableHitObject, InitialState> hitObjectInitialStateCache = new Dictionary<DrawableHitObject, InitialState>();
|
||||
|
||||
/// <summary>
|
||||
/// Hit objects which require lifetime computation in the next update call.
|
||||
/// </summary>
|
||||
private readonly HashSet<DrawableHitObject> toComputeLifetime = new HashSet<DrawableHitObject>();
|
||||
|
||||
/// <summary>
|
||||
/// A set containing all <see cref="HitObjectContainer.AliveObjects"/> which have an up-to-date layout.
|
||||
/// </summary>
|
||||
private readonly HashSet<DrawableHitObject> layoutComputed = new HashSet<DrawableHitObject>();
|
||||
|
||||
[Resolved]
|
||||
private IScrollingInfo scrollingInfo { get; set; }
|
||||
@ -27,10 +33,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
// Responds to changes in the layout. When the layout changes, all hit object states must be recomputed.
|
||||
private readonly LayoutValue layoutCache = new LayoutValue(Invalidation.RequiredParentSizeToFit | Invalidation.DrawInfo);
|
||||
|
||||
// A combined cache across all hit object states to reduce per-update iterations.
|
||||
// When invalidated, one or more (but not necessarily all) hitobject states must be re-validated.
|
||||
private readonly Cached combinedObjCache = new Cached();
|
||||
|
||||
public ScrollingHitObjectContainer()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
@ -48,37 +50,12 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
timeRange.ValueChanged += _ => layoutCache.Invalidate();
|
||||
}
|
||||
|
||||
public override void Add(DrawableHitObject hitObject)
|
||||
{
|
||||
combinedObjCache.Invalidate();
|
||||
hitObject.DefaultsApplied += onDefaultsApplied;
|
||||
base.Add(hitObject);
|
||||
}
|
||||
|
||||
public override bool Remove(DrawableHitObject hitObject)
|
||||
{
|
||||
var result = base.Remove(hitObject);
|
||||
|
||||
if (result)
|
||||
{
|
||||
combinedObjCache.Invalidate();
|
||||
hitObjectInitialStateCache.Remove(hitObject);
|
||||
|
||||
hitObject.DefaultsApplied -= onDefaultsApplied;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public override void Clear(bool disposeChildren = true)
|
||||
{
|
||||
foreach (var h in Objects)
|
||||
h.DefaultsApplied -= onDefaultsApplied;
|
||||
|
||||
base.Clear(disposeChildren);
|
||||
|
||||
combinedObjCache.Invalidate();
|
||||
hitObjectInitialStateCache.Clear();
|
||||
toComputeLifetime.Clear();
|
||||
layoutComputed.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -173,15 +150,40 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
}
|
||||
}
|
||||
|
||||
private void onDefaultsApplied(DrawableHitObject drawableObject)
|
||||
protected override void OnAdd(DrawableHitObject drawableHitObject) => onAddRecursive(drawableHitObject);
|
||||
|
||||
protected override void OnRemove(DrawableHitObject drawableHitObject) => onRemoveRecursive(drawableHitObject);
|
||||
|
||||
private void onAddRecursive(DrawableHitObject hitObject)
|
||||
{
|
||||
// The cache may not exist if the hitobject state hasn't been computed yet (e.g. if the hitobject was added + defaults applied in the same frame).
|
||||
// In such a case, combinedObjCache will take care of updating the hitobject.
|
||||
if (hitObjectInitialStateCache.TryGetValue(drawableObject, out var state))
|
||||
{
|
||||
combinedObjCache.Invalidate();
|
||||
state.Cache.Invalidate();
|
||||
}
|
||||
invalidateHitObject(hitObject);
|
||||
|
||||
hitObject.DefaultsApplied += invalidateHitObject;
|
||||
|
||||
foreach (var nested in hitObject.NestedHitObjects)
|
||||
onAddRecursive(nested);
|
||||
}
|
||||
|
||||
private void onRemoveRecursive(DrawableHitObject hitObject)
|
||||
{
|
||||
toComputeLifetime.Remove(hitObject);
|
||||
layoutComputed.Remove(hitObject);
|
||||
|
||||
hitObject.DefaultsApplied -= invalidateHitObject;
|
||||
|
||||
foreach (var nested in hitObject.NestedHitObjects)
|
||||
onRemoveRecursive(nested);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Make this <see cref="DrawableHitObject"/> lifetime and layout computed in next update.
|
||||
/// </summary>
|
||||
private void invalidateHitObject(DrawableHitObject hitObject)
|
||||
{
|
||||
// Lifetime computation is delayed until next update because
|
||||
// when the hit object is not pooled this container is not loaded here and `scrollLength` cannot be computed.
|
||||
toComputeLifetime.Add(hitObject);
|
||||
layoutComputed.Remove(hitObject);
|
||||
}
|
||||
|
||||
private float scrollLength;
|
||||
@ -192,17 +194,18 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
|
||||
if (!layoutCache.IsValid)
|
||||
{
|
||||
foreach (var state in hitObjectInitialStateCache.Values)
|
||||
state.Cache.Invalidate();
|
||||
combinedObjCache.Invalidate();
|
||||
toComputeLifetime.Clear();
|
||||
|
||||
foreach (var hitObject in Objects)
|
||||
{
|
||||
if (hitObject.HitObject != null)
|
||||
toComputeLifetime.Add(hitObject);
|
||||
}
|
||||
|
||||
layoutComputed.Clear();
|
||||
|
||||
scrollingInfo.Algorithm.Reset();
|
||||
|
||||
layoutCache.Validate();
|
||||
}
|
||||
|
||||
if (!combinedObjCache.IsValid)
|
||||
{
|
||||
switch (direction.Value)
|
||||
{
|
||||
case ScrollingDirection.Up:
|
||||
@ -215,32 +218,24 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
break;
|
||||
}
|
||||
|
||||
foreach (var obj in Objects)
|
||||
{
|
||||
if (!hitObjectInitialStateCache.TryGetValue(obj, out var state))
|
||||
state = hitObjectInitialStateCache[obj] = new InitialState(new Cached());
|
||||
|
||||
if (state.Cache.IsValid)
|
||||
continue;
|
||||
|
||||
state.ScheduledComputation?.Cancel();
|
||||
state.ScheduledComputation = computeInitialStateRecursive(obj);
|
||||
|
||||
computeLifetimeStartRecursive(obj);
|
||||
|
||||
state.Cache.Validate();
|
||||
}
|
||||
|
||||
combinedObjCache.Validate();
|
||||
layoutCache.Validate();
|
||||
}
|
||||
}
|
||||
|
||||
private void computeLifetimeStartRecursive(DrawableHitObject hitObject)
|
||||
{
|
||||
hitObject.LifetimeStart = computeOriginAdjustedLifetimeStart(hitObject);
|
||||
foreach (var hitObject in toComputeLifetime)
|
||||
hitObject.LifetimeStart = computeOriginAdjustedLifetimeStart(hitObject);
|
||||
|
||||
foreach (var obj in hitObject.NestedHitObjects)
|
||||
computeLifetimeStartRecursive(obj);
|
||||
toComputeLifetime.Clear();
|
||||
|
||||
// only AliveObjects need to be considered for layout (reduces overhead in the case of scroll speed changes).
|
||||
foreach (var obj in AliveObjects)
|
||||
{
|
||||
if (layoutComputed.Contains(obj))
|
||||
continue;
|
||||
|
||||
updateLayoutRecursive(obj);
|
||||
|
||||
layoutComputed.Add(obj);
|
||||
}
|
||||
}
|
||||
|
||||
private double computeOriginAdjustedLifetimeStart(DrawableHitObject hitObject)
|
||||
@ -271,7 +266,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
return scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, originAdjustment, timeRange.Value, scrollLength);
|
||||
}
|
||||
|
||||
private ScheduledDelegate computeInitialStateRecursive(DrawableHitObject hitObject) => hitObject.Schedule(() =>
|
||||
private void updateLayoutRecursive(DrawableHitObject hitObject)
|
||||
{
|
||||
if (hitObject.HitObject is IHasDuration e)
|
||||
{
|
||||
@ -291,12 +286,12 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
|
||||
foreach (var obj in hitObject.NestedHitObjects)
|
||||
{
|
||||
computeInitialStateRecursive(obj);
|
||||
updateLayoutRecursive(obj);
|
||||
|
||||
// Nested hitobjects don't need to scroll, but they do need accurate positions
|
||||
updatePosition(obj, hitObject.HitObject.StartTime);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override void UpdateAfterChildrenLife()
|
||||
{
|
||||
@ -328,19 +323,5 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private class InitialState
|
||||
{
|
||||
[NotNull]
|
||||
public readonly Cached Cache;
|
||||
|
||||
[CanBeNull]
|
||||
public ScheduledDelegate ScheduledComputation;
|
||||
|
||||
public InitialState(Cached cache)
|
||||
{
|
||||
Cache = cache;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,8 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
{
|
||||
protected readonly IBindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
|
||||
|
||||
public new ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)base.HitObjectContainer;
|
||||
|
||||
[Resolved]
|
||||
protected IScrollingInfo ScrollingInfo { get; private set; }
|
||||
|
||||
@ -27,14 +29,12 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
/// <summary>
|
||||
/// Given a position in screen space, return the time within this column.
|
||||
/// </summary>
|
||||
public virtual double TimeAtScreenSpacePosition(Vector2 screenSpacePosition) =>
|
||||
((ScrollingHitObjectContainer)HitObjectContainer).TimeAtScreenSpacePosition(screenSpacePosition);
|
||||
public virtual double TimeAtScreenSpacePosition(Vector2 screenSpacePosition) => HitObjectContainer.TimeAtScreenSpacePosition(screenSpacePosition);
|
||||
|
||||
/// <summary>
|
||||
/// Given a time, return the screen space position within this column.
|
||||
/// </summary>
|
||||
public virtual Vector2 ScreenSpacePositionAtTime(double time)
|
||||
=> ((ScrollingHitObjectContainer)HitObjectContainer).ScreenSpacePositionAtTime(time);
|
||||
public virtual Vector2 ScreenSpacePositionAtTime(double time) => HitObjectContainer.ScreenSpacePositionAtTime(time);
|
||||
|
||||
protected sealed override HitObjectContainer CreateHitObjectContainer() => new ScrollingHitObjectContainer();
|
||||
}
|
||||
|
@ -91,7 +91,7 @@ namespace osu.Game.Screens.Select
|
||||
/// </summary>
|
||||
public bool BeatmapSetsLoaded { get; private set; }
|
||||
|
||||
private readonly CarouselScrollContainer scroll;
|
||||
protected readonly CarouselScrollContainer Scroll;
|
||||
|
||||
private IEnumerable<CarouselBeatmapSet> beatmapSets => root.Children.OfType<CarouselBeatmapSet>();
|
||||
|
||||
@ -112,9 +112,9 @@ namespace osu.Game.Screens.Select
|
||||
if (selectedBeatmapSet != null && !beatmapSets.Contains(selectedBeatmapSet.BeatmapSet))
|
||||
selectedBeatmapSet = null;
|
||||
|
||||
ScrollableContent.Clear(false);
|
||||
Scroll.Clear(false);
|
||||
itemsCache.Invalidate();
|
||||
scrollPositionCache.Invalidate();
|
||||
ScrollToSelected();
|
||||
|
||||
// apply any pending filter operation that may have been delayed (see applyActiveCriteria's scheduling behaviour when BeatmapSetsLoaded is false).
|
||||
FlushPendingFilterOperations();
|
||||
@ -130,9 +130,7 @@ namespace osu.Game.Screens.Select
|
||||
private readonly List<CarouselItem> visibleItems = new List<CarouselItem>();
|
||||
|
||||
private readonly Cached itemsCache = new Cached();
|
||||
private readonly Cached scrollPositionCache = new Cached();
|
||||
|
||||
protected readonly Container<DrawableCarouselItem> ScrollableContent;
|
||||
private PendingScrollOperation pendingScrollOperation = PendingScrollOperation.None;
|
||||
|
||||
public Bindable<bool> RightClickScrollingEnabled = new Bindable<bool>();
|
||||
|
||||
@ -155,17 +153,12 @@ namespace osu.Game.Screens.Select
|
||||
InternalChild = new OsuContextMenuContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = scroll = new CarouselScrollContainer
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Masking = false,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
setPool,
|
||||
Scroll = new CarouselScrollContainer
|
||||
{
|
||||
setPool,
|
||||
ScrollableContent = new Container<DrawableCarouselItem>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
}
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -180,7 +173,7 @@ namespace osu.Game.Screens.Select
|
||||
config.BindWith(OsuSetting.RandomSelectAlgorithm, RandomAlgorithm);
|
||||
config.BindWith(OsuSetting.SongSelectRightMouseScroll, RightClickScrollingEnabled);
|
||||
|
||||
RightClickScrollingEnabled.ValueChanged += enabled => scroll.RightMouseScrollbar = enabled.NewValue;
|
||||
RightClickScrollingEnabled.ValueChanged += enabled => Scroll.RightMouseScrollbar = enabled.NewValue;
|
||||
RightClickScrollingEnabled.TriggerChange();
|
||||
|
||||
itemUpdated = beatmaps.ItemUpdated.GetBoundCopy();
|
||||
@ -421,12 +414,12 @@ namespace osu.Game.Screens.Select
|
||||
/// <summary>
|
||||
/// The position of the lower visible bound with respect to the current scroll position.
|
||||
/// </summary>
|
||||
private float visibleBottomBound => scroll.Current + DrawHeight + BleedBottom;
|
||||
private float visibleBottomBound => Scroll.Current + DrawHeight + BleedBottom;
|
||||
|
||||
/// <summary>
|
||||
/// The position of the upper visible bound with respect to the current scroll position.
|
||||
/// </summary>
|
||||
private float visibleUpperBound => scroll.Current - BleedTop;
|
||||
private float visibleUpperBound => Scroll.Current - BleedTop;
|
||||
|
||||
public void FlushPendingFilterOperations()
|
||||
{
|
||||
@ -468,8 +461,8 @@ namespace osu.Game.Screens.Select
|
||||
root.Filter(activeCriteria);
|
||||
itemsCache.Invalidate();
|
||||
|
||||
if (alwaysResetScrollPosition || !scroll.UserScrolling)
|
||||
ScrollToSelected();
|
||||
if (alwaysResetScrollPosition || !Scroll.UserScrolling)
|
||||
ScrollToSelected(true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -478,7 +471,12 @@ namespace osu.Game.Screens.Select
|
||||
/// <summary>
|
||||
/// Scroll to the current <see cref="SelectedBeatmap"/>.
|
||||
/// </summary>
|
||||
public void ScrollToSelected() => scrollPositionCache.Invalidate();
|
||||
/// <param name="immediate">
|
||||
/// Whether the scroll position should immediately be shifted to the target, delegating animation to visible panels.
|
||||
/// This should be true for operations like filtering - where panels are changing visibility state - to avoid large jumps in animation.
|
||||
/// </param>
|
||||
public void ScrollToSelected(bool immediate = false) =>
|
||||
pendingScrollOperation = immediate ? PendingScrollOperation.Immediate : PendingScrollOperation.Standard;
|
||||
|
||||
#region Key / button selection logic
|
||||
|
||||
@ -488,12 +486,12 @@ namespace osu.Game.Screens.Select
|
||||
{
|
||||
case Key.Left:
|
||||
if (!e.Repeat)
|
||||
beginRepeatSelection(() => SelectNext(-1, true), e.Key);
|
||||
beginRepeatSelection(() => SelectNext(-1), e.Key);
|
||||
return true;
|
||||
|
||||
case Key.Right:
|
||||
if (!e.Repeat)
|
||||
beginRepeatSelection(() => SelectNext(1, true), e.Key);
|
||||
beginRepeatSelection(() => SelectNext(), e.Key);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -580,6 +578,11 @@ namespace osu.Game.Screens.Select
|
||||
if (revalidateItems)
|
||||
updateYPositions();
|
||||
|
||||
// if there is a pending scroll action we apply it without animation and transfer the difference in position to the panels.
|
||||
// this is intentionally applied before updating the visible range below, to avoid animating new items (sourced from pool) from locations off-screen, as it looks bad.
|
||||
if (pendingScrollOperation != PendingScrollOperation.None)
|
||||
updateScrollPosition();
|
||||
|
||||
// This data is consumed to find the currently displayable range.
|
||||
// This is the range we want to keep drawables for, and should exceed the visible range slightly to avoid drawable churn.
|
||||
var newDisplayRange = getDisplayRange();
|
||||
@ -594,7 +597,7 @@ namespace osu.Game.Screens.Select
|
||||
{
|
||||
var toDisplay = visibleItems.GetRange(displayedRange.first, displayedRange.last - displayedRange.first + 1);
|
||||
|
||||
foreach (var panel in ScrollableContent.Children)
|
||||
foreach (var panel in Scroll.Children)
|
||||
{
|
||||
if (toDisplay.Remove(panel.Item))
|
||||
{
|
||||
@ -620,24 +623,14 @@ namespace osu.Game.Screens.Select
|
||||
panel.Depth = item.CarouselYPosition;
|
||||
panel.Y = item.CarouselYPosition;
|
||||
|
||||
ScrollableContent.Add(panel);
|
||||
Scroll.Add(panel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, if the filtered items have changed, animate drawables to their new locations.
|
||||
// This is common if a selected/collapsed state has changed.
|
||||
if (revalidateItems)
|
||||
{
|
||||
foreach (DrawableCarouselItem panel in ScrollableContent.Children)
|
||||
{
|
||||
panel.MoveToY(panel.Item.CarouselYPosition, 800, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
// Update externally controlled state of currently visible items (e.g. x-offset and opacity).
|
||||
// This is a per-frame update on all drawable panels.
|
||||
foreach (DrawableCarouselItem item in ScrollableContent.Children)
|
||||
foreach (DrawableCarouselItem item in Scroll.Children)
|
||||
{
|
||||
updateItem(item);
|
||||
|
||||
@ -670,14 +663,6 @@ namespace osu.Game.Screens.Select
|
||||
return (firstIndex, lastIndex);
|
||||
}
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
|
||||
if (!scrollPositionCache.IsValid)
|
||||
updateScrollPosition();
|
||||
}
|
||||
|
||||
private void beatmapRemoved(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakItem)
|
||||
{
|
||||
if (weakItem.NewValue.TryGetTarget(out var item))
|
||||
@ -789,7 +774,8 @@ namespace osu.Game.Screens.Select
|
||||
}
|
||||
|
||||
currentY += visibleHalfHeight;
|
||||
ScrollableContent.Height = currentY;
|
||||
|
||||
Scroll.ScrollContent.Height = currentY;
|
||||
|
||||
if (BeatmapSetsLoaded && (selectedBeatmapSet == null || selectedBeatmap == null || selectedBeatmapSet.State.Value != CarouselItemState.Selected))
|
||||
{
|
||||
@ -809,12 +795,31 @@ namespace osu.Game.Screens.Select
|
||||
if (firstScroll)
|
||||
{
|
||||
// reduce movement when first displaying the carousel.
|
||||
scroll.ScrollTo(scrollTarget.Value - 200, false);
|
||||
Scroll.ScrollTo(scrollTarget.Value - 200, false);
|
||||
firstScroll = false;
|
||||
}
|
||||
|
||||
scroll.ScrollTo(scrollTarget.Value);
|
||||
scrollPositionCache.Validate();
|
||||
switch (pendingScrollOperation)
|
||||
{
|
||||
case PendingScrollOperation.Standard:
|
||||
Scroll.ScrollTo(scrollTarget.Value);
|
||||
break;
|
||||
|
||||
case PendingScrollOperation.Immediate:
|
||||
// in order to simplify animation logic, rather than using the animated version of ScrollTo,
|
||||
// we take the difference in scroll height and apply to all visible panels.
|
||||
// this avoids edge cases like when the visible panels is reduced suddenly, causing ScrollContainer
|
||||
// to enter clamp-special-case mode where it animates completely differently to normal.
|
||||
float scrollChange = scrollTarget.Value - Scroll.Current;
|
||||
|
||||
Scroll.ScrollTo(scrollTarget.Value, false);
|
||||
|
||||
foreach (var i in Scroll.Children)
|
||||
i.Y += scrollChange;
|
||||
break;
|
||||
}
|
||||
|
||||
pendingScrollOperation = PendingScrollOperation.None;
|
||||
}
|
||||
}
|
||||
|
||||
@ -844,7 +849,7 @@ namespace osu.Game.Screens.Select
|
||||
/// <param name="parent">For nested items, the parent of the item to be updated.</param>
|
||||
private void updateItem(DrawableCarouselItem item, DrawableCarouselItem parent = null)
|
||||
{
|
||||
Vector2 posInScroll = ScrollableContent.ToLocalSpace(item.Header.ScreenSpaceDrawQuad.Centre);
|
||||
Vector2 posInScroll = Scroll.ScrollContent.ToLocalSpace(item.Header.ScreenSpaceDrawQuad.Centre);
|
||||
float itemDrawY = posInScroll.Y - visibleUpperBound;
|
||||
float dist = Math.Abs(1f - itemDrawY / visibleHalfHeight);
|
||||
|
||||
@ -858,6 +863,13 @@ namespace osu.Game.Screens.Select
|
||||
item.SetMultiplicativeAlpha(Math.Clamp(1.75f - 1.5f * dist, 0, 1));
|
||||
}
|
||||
|
||||
private enum PendingScrollOperation
|
||||
{
|
||||
None,
|
||||
Standard,
|
||||
Immediate,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A carousel item strictly used for binary search purposes.
|
||||
/// </summary>
|
||||
@ -889,7 +901,7 @@ namespace osu.Game.Screens.Select
|
||||
}
|
||||
}
|
||||
|
||||
private class CarouselScrollContainer : OsuScrollContainer
|
||||
protected class CarouselScrollContainer : OsuScrollContainer<DrawableCarouselItem>
|
||||
{
|
||||
private bool rightMouseScrollBlocked;
|
||||
|
||||
@ -898,6 +910,12 @@ namespace osu.Game.Screens.Select
|
||||
/// </summary>
|
||||
public bool UserScrolling { get; private set; }
|
||||
|
||||
public CarouselScrollContainer()
|
||||
{
|
||||
// size is determined by the carousel itself, due to not all content necessarily being loaded.
|
||||
ScrollContent.AutoSizeAxes = Axes.None;
|
||||
}
|
||||
|
||||
// ReSharper disable once OptionalParameterHierarchyMismatch 2020.3 EAP4 bug. (https://youtrack.jetbrains.com/issue/RSRP-481535?p=RIDER-51910)
|
||||
protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default)
|
||||
{
|
||||
|
@ -10,6 +10,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
@ -60,6 +61,25 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
viewDetails = beatmapOverlay.FetchAndShowBeatmapSet;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// position updates should not occur if the item is filtered away.
|
||||
// this avoids panels flying across the screen only to be eventually off-screen or faded out.
|
||||
if (!Item.Visible)
|
||||
return;
|
||||
|
||||
float targetY = Item.CarouselYPosition;
|
||||
|
||||
if (Precision.AlmostEquals(targetY, Y))
|
||||
Y = targetY;
|
||||
else
|
||||
// algorithm for this is taken from ScrollContainer.
|
||||
// while it doesn't necessarily need to match 1:1, as we are emulating scroll in some cases this feels most correct.
|
||||
Y = (float)Interpolation.Lerp(targetY, Y, Math.Exp(-0.01 * Time.Elapsed));
|
||||
}
|
||||
|
||||
protected override void UpdateItem()
|
||||
{
|
||||
base.UpdateItem();
|
||||
|
Loading…
Reference in New Issue
Block a user