mirror of
https://github.com/ppy/osu.git
synced 2025-02-13 22:35:23 +08:00
Merge pull request #10494 from peppy/beatmap-carousel-refactor
Add beatmap set level pooling to beatmap carousel
This commit is contained in:
commit
f35611b452
@ -11,6 +11,7 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
@ -40,6 +41,12 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
this.rulesets = rulesets;
|
this.rulesets = rulesets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestManyPanels()
|
||||||
|
{
|
||||||
|
loadBeatmaps(count: 5000, randomDifficulties: true);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestKeyRepeat()
|
public void TestKeyRepeat()
|
||||||
{
|
{
|
||||||
@ -707,21 +714,22 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
checkVisibleItemCount(true, 15);
|
checkVisibleItemCount(true, 15);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadBeatmaps(List<BeatmapSetInfo> beatmapSets = null, Func<FilterCriteria> initialCriteria = null, Action<BeatmapCarousel> carouselAdjust = null)
|
private void loadBeatmaps(List<BeatmapSetInfo> beatmapSets = null, Func<FilterCriteria> initialCriteria = null, Action<BeatmapCarousel> carouselAdjust = null, int? count = null, bool randomDifficulties = false)
|
||||||
{
|
{
|
||||||
createCarousel(carouselAdjust);
|
bool changed = false;
|
||||||
|
|
||||||
|
createCarousel(c =>
|
||||||
|
{
|
||||||
|
carouselAdjust?.Invoke(c);
|
||||||
|
|
||||||
if (beatmapSets == null)
|
if (beatmapSets == null)
|
||||||
{
|
{
|
||||||
beatmapSets = new List<BeatmapSetInfo>();
|
beatmapSets = new List<BeatmapSetInfo>();
|
||||||
|
|
||||||
for (int i = 1; i <= set_count; i++)
|
for (int i = 1; i <= (count ?? set_count); i++)
|
||||||
beatmapSets.Add(createTestBeatmapSet(i));
|
beatmapSets.Add(createTestBeatmapSet(i, randomDifficulties));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool changed = false;
|
|
||||||
AddStep($"Load {(beatmapSets.Count > 0 ? beatmapSets.Count.ToString() : "some")} beatmaps", () =>
|
|
||||||
{
|
|
||||||
carousel.Filter(initialCriteria?.Invoke() ?? new FilterCriteria());
|
carousel.Filter(initialCriteria?.Invoke() ?? new FilterCriteria());
|
||||||
carousel.BeatmapSetsChanged = () => changed = true;
|
carousel.BeatmapSetsChanged = () => changed = true;
|
||||||
carousel.BeatmapSets = beatmapSets;
|
carousel.BeatmapSets = beatmapSets;
|
||||||
@ -807,7 +815,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
|
|
||||||
private bool selectedBeatmapVisible()
|
private bool selectedBeatmapVisible()
|
||||||
{
|
{
|
||||||
var currentlySelected = carousel.Items.Find(s => s.Item is CarouselBeatmap && s.Item.State.Value == CarouselItemState.Selected);
|
var currentlySelected = carousel.Items.FirstOrDefault(s => s.Item is CarouselBeatmap && s.Item.State.Value == CarouselItemState.Selected);
|
||||||
if (currentlySelected == null)
|
if (currentlySelected == null)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
@ -820,7 +828,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
AddAssert("Selection is visible", selectedBeatmapVisible);
|
AddAssert("Selection is visible", selectedBeatmapVisible);
|
||||||
}
|
}
|
||||||
|
|
||||||
private BeatmapSetInfo createTestBeatmapSet(int id)
|
private BeatmapSetInfo createTestBeatmapSet(int id, bool randomDifficultyCount = false)
|
||||||
{
|
{
|
||||||
return new BeatmapSetInfo
|
return new BeatmapSetInfo
|
||||||
{
|
{
|
||||||
@ -834,42 +842,37 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
Title = $"test set #{id}!",
|
Title = $"test set #{id}!",
|
||||||
AuthorString = string.Concat(Enumerable.Repeat((char)('z' - Math.Min(25, id - 1)), 5))
|
AuthorString = string.Concat(Enumerable.Repeat((char)('z' - Math.Min(25, id - 1)), 5))
|
||||||
},
|
},
|
||||||
Beatmaps = new List<BeatmapInfo>(new[]
|
Beatmaps = getBeatmaps(randomDifficultyCount ? RNG.Next(1, 20) : 3).ToList()
|
||||||
{
|
|
||||||
new BeatmapInfo
|
|
||||||
{
|
|
||||||
OnlineBeatmapID = id * 10,
|
|
||||||
Version = "Normal",
|
|
||||||
StarDifficulty = 2,
|
|
||||||
BaseDifficulty = new BeatmapDifficulty
|
|
||||||
{
|
|
||||||
OverallDifficulty = 3.5f,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new BeatmapInfo
|
|
||||||
{
|
|
||||||
OnlineBeatmapID = id * 10 + 1,
|
|
||||||
Version = "Hard",
|
|
||||||
StarDifficulty = 5,
|
|
||||||
BaseDifficulty = new BeatmapDifficulty
|
|
||||||
{
|
|
||||||
OverallDifficulty = 5,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new BeatmapInfo
|
|
||||||
{
|
|
||||||
OnlineBeatmapID = id * 10 + 2,
|
|
||||||
Version = "Insane",
|
|
||||||
StarDifficulty = 6,
|
|
||||||
BaseDifficulty = new BeatmapDifficulty
|
|
||||||
{
|
|
||||||
OverallDifficulty = 7,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IEnumerable<BeatmapInfo> getBeatmaps(int count)
|
||||||
|
{
|
||||||
|
int id = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
float diff = (float)i / count * 10;
|
||||||
|
|
||||||
|
string version = "Normal";
|
||||||
|
if (diff > 6.6)
|
||||||
|
version = "Insane";
|
||||||
|
else if (diff > 3.3)
|
||||||
|
version = "Hard";
|
||||||
|
|
||||||
|
yield return new BeatmapInfo
|
||||||
|
{
|
||||||
|
OnlineBeatmapID = id++ * 10,
|
||||||
|
Version = version,
|
||||||
|
StarDifficulty = diff,
|
||||||
|
BaseDifficulty = new BeatmapDifficulty
|
||||||
|
{
|
||||||
|
OverallDifficulty = diff,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private BeatmapSetInfo createTestBeatmapSetWithManyDifficulties(int id)
|
private BeatmapSetInfo createTestBeatmapSetWithManyDifficulties(int id)
|
||||||
{
|
{
|
||||||
var toReturn = new BeatmapSetInfo
|
var toReturn = new BeatmapSetInfo
|
||||||
@ -908,10 +911,25 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
|
|
||||||
private class TestBeatmapCarousel : BeatmapCarousel
|
private class TestBeatmapCarousel : BeatmapCarousel
|
||||||
{
|
{
|
||||||
public new List<DrawableCarouselItem> Items => base.Items;
|
|
||||||
|
|
||||||
public bool PendingFilterTask => PendingFilter != null;
|
public bool PendingFilterTask => PendingFilter != null;
|
||||||
|
|
||||||
|
public IEnumerable<DrawableCarouselItem> Items
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
foreach (var item in ScrollableContent)
|
||||||
|
{
|
||||||
|
yield return item;
|
||||||
|
|
||||||
|
if (item is DrawableCarouselBeatmapSet set)
|
||||||
|
{
|
||||||
|
foreach (var difficulty in set.DrawableBeatmaps)
|
||||||
|
yield return difficulty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override IEnumerable<BeatmapSetInfo> GetLoadableBeatmaps() => Enumerable.Empty<BeatmapSetInfo>();
|
protected override IEnumerable<BeatmapSetInfo> GetLoadableBeatmaps() => Enumerable.Empty<BeatmapSetInfo>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -507,7 +507,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
var selectedPanel = songSelect.Carousel.ChildrenOfType<DrawableCarouselBeatmapSet>().First(s => s.Item.State.Value == CarouselItemState.Selected);
|
var selectedPanel = songSelect.Carousel.ChildrenOfType<DrawableCarouselBeatmapSet>().First(s => s.Item.State.Value == CarouselItemState.Selected);
|
||||||
|
|
||||||
// special case for converts checked here.
|
// special case for converts checked here.
|
||||||
return selectedPanel.ChildrenOfType<DrawableCarouselBeatmapSet.FilterableDifficultyIcon>().All(i =>
|
return selectedPanel.ChildrenOfType<FilterableDifficultyIcon>().All(i =>
|
||||||
i.IsFiltered || i.Item.Beatmap.Ruleset.ID == targetRuleset || i.Item.Beatmap.Ruleset.ID == 0);
|
i.IsFiltered || i.Item.Beatmap.Ruleset.ID == targetRuleset || i.Item.Beatmap.Ruleset.ID == 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -606,10 +606,10 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
set = songSelect.Carousel.ChildrenOfType<DrawableCarouselBeatmapSet>().First();
|
set = songSelect.Carousel.ChildrenOfType<DrawableCarouselBeatmapSet>().First();
|
||||||
});
|
});
|
||||||
|
|
||||||
DrawableCarouselBeatmapSet.FilterableDifficultyIcon difficultyIcon = null;
|
FilterableDifficultyIcon difficultyIcon = null;
|
||||||
AddStep("Find an icon", () =>
|
AddStep("Find an icon", () =>
|
||||||
{
|
{
|
||||||
difficultyIcon = set.ChildrenOfType<DrawableCarouselBeatmapSet.FilterableDifficultyIcon>()
|
difficultyIcon = set.ChildrenOfType<FilterableDifficultyIcon>()
|
||||||
.First(icon => getDifficultyIconIndex(set, icon) != getCurrentBeatmapIndex());
|
.First(icon => getDifficultyIconIndex(set, icon) != getCurrentBeatmapIndex());
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -634,13 +634,13 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
BeatmapInfo filteredBeatmap = null;
|
BeatmapInfo filteredBeatmap = null;
|
||||||
DrawableCarouselBeatmapSet.FilterableDifficultyIcon filteredIcon = null;
|
FilterableDifficultyIcon filteredIcon = null;
|
||||||
|
|
||||||
AddStep("Get filtered icon", () =>
|
AddStep("Get filtered icon", () =>
|
||||||
{
|
{
|
||||||
filteredBeatmap = songSelect.Carousel.SelectedBeatmapSet.Beatmaps.First(b => b.BPM < maxBPM);
|
filteredBeatmap = songSelect.Carousel.SelectedBeatmapSet.Beatmaps.First(b => b.BPM < maxBPM);
|
||||||
int filteredBeatmapIndex = getBeatmapIndex(filteredBeatmap.BeatmapSet, filteredBeatmap);
|
int filteredBeatmapIndex = getBeatmapIndex(filteredBeatmap.BeatmapSet, filteredBeatmap);
|
||||||
filteredIcon = set.ChildrenOfType<DrawableCarouselBeatmapSet.FilterableDifficultyIcon>().ElementAt(filteredBeatmapIndex);
|
filteredIcon = set.ChildrenOfType<FilterableDifficultyIcon>().ElementAt(filteredBeatmapIndex);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("Click on a filtered difficulty", () =>
|
AddStep("Click on a filtered difficulty", () =>
|
||||||
@ -674,10 +674,10 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
return set != null;
|
return set != null;
|
||||||
});
|
});
|
||||||
|
|
||||||
DrawableCarouselBeatmapSet.FilterableDifficultyIcon difficultyIcon = null;
|
FilterableDifficultyIcon difficultyIcon = null;
|
||||||
AddStep("Find an icon for different ruleset", () =>
|
AddStep("Find an icon for different ruleset", () =>
|
||||||
{
|
{
|
||||||
difficultyIcon = set.ChildrenOfType<DrawableCarouselBeatmapSet.FilterableDifficultyIcon>()
|
difficultyIcon = set.ChildrenOfType<FilterableDifficultyIcon>()
|
||||||
.First(icon => icon.Item.Beatmap.Ruleset.ID == 3);
|
.First(icon => icon.Item.Beatmap.Ruleset.ID == 3);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -725,10 +725,10 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
return set != null;
|
return set != null;
|
||||||
});
|
});
|
||||||
|
|
||||||
DrawableCarouselBeatmapSet.FilterableGroupedDifficultyIcon groupIcon = null;
|
FilterableGroupedDifficultyIcon groupIcon = null;
|
||||||
AddStep("Find group icon for different ruleset", () =>
|
AddStep("Find group icon for different ruleset", () =>
|
||||||
{
|
{
|
||||||
groupIcon = set.ChildrenOfType<DrawableCarouselBeatmapSet.FilterableGroupedDifficultyIcon>()
|
groupIcon = set.ChildrenOfType<FilterableGroupedDifficultyIcon>()
|
||||||
.First(icon => icon.Items.First().Beatmap.Ruleset.ID == 3);
|
.First(icon => icon.Items.First().Beatmap.Ruleset.ID == 3);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -821,9 +821,9 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
|
|
||||||
private int getCurrentBeatmapIndex() => getBeatmapIndex(songSelect.Carousel.SelectedBeatmapSet, songSelect.Carousel.SelectedBeatmap);
|
private int getCurrentBeatmapIndex() => getBeatmapIndex(songSelect.Carousel.SelectedBeatmapSet, songSelect.Carousel.SelectedBeatmap);
|
||||||
|
|
||||||
private int getDifficultyIconIndex(DrawableCarouselBeatmapSet set, DrawableCarouselBeatmapSet.FilterableDifficultyIcon icon)
|
private int getDifficultyIconIndex(DrawableCarouselBeatmapSet set, FilterableDifficultyIcon icon)
|
||||||
{
|
{
|
||||||
return set.ChildrenOfType<DrawableCarouselBeatmapSet.FilterableDifficultyIcon>().ToList().FindIndex(i => i == icon);
|
return set.ChildrenOfType<FilterableDifficultyIcon>().ToList().FindIndex(i => i == icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addRulesetImportStep(int id) => AddStep($"import test map for ruleset {id}", () => importForRuleset(id));
|
private void addRulesetImportStep(int id) => AddStep($"import test map for ruleset {id}", () => importForRuleset(id));
|
||||||
|
@ -1,29 +1,29 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osuTK;
|
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Graphics.Containers;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using osu.Game.Configuration;
|
|
||||||
using osuTK.Input;
|
|
||||||
using osu.Framework.Utils;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Caching;
|
using osu.Framework.Caching;
|
||||||
using osu.Framework.Threading;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Pooling;
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Framework.Threading;
|
||||||
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Extensions;
|
using osu.Game.Extensions;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Graphics.Cursor;
|
using osu.Game.Graphics.Cursor;
|
||||||
using osu.Game.Input.Bindings;
|
using osu.Game.Input.Bindings;
|
||||||
using osu.Game.Screens.Select.Carousel;
|
using osu.Game.Screens.Select.Carousel;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Select
|
namespace osu.Game.Screens.Select
|
||||||
{
|
{
|
||||||
@ -74,6 +74,18 @@ namespace osu.Game.Screens.Select
|
|||||||
public override bool PropagatePositionalInputSubTree => AllowSelection;
|
public override bool PropagatePositionalInputSubTree => AllowSelection;
|
||||||
public override bool PropagateNonPositionalInputSubTree => AllowSelection;
|
public override bool PropagateNonPositionalInputSubTree => AllowSelection;
|
||||||
|
|
||||||
|
private (int first, int last) displayedRange;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extend the range to retain already loaded pooled drawables.
|
||||||
|
/// </summary>
|
||||||
|
private const float distance_offscreen_before_unload = 1024;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extend the range to update positions / retrieve pooled drawables outside of visible range.
|
||||||
|
/// </summary>
|
||||||
|
private const float distance_offscreen_to_preload = 512; // todo: adjust this appropriately once we can make set panel contents load while off-screen.
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether carousel items have completed asynchronously loaded.
|
/// Whether carousel items have completed asynchronously loaded.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -94,16 +106,13 @@ namespace osu.Game.Screens.Select
|
|||||||
{
|
{
|
||||||
CarouselRoot newRoot = new CarouselRoot(this);
|
CarouselRoot newRoot = new CarouselRoot(this);
|
||||||
|
|
||||||
beatmapSets.Select(createCarouselSet).Where(g => g != null).ForEach(newRoot.AddChild);
|
newRoot.AddChildren(beatmapSets.Select(createCarouselSet).Where(g => g != null));
|
||||||
|
|
||||||
// preload drawables as the ctor overhead is quite high currently.
|
|
||||||
_ = newRoot.Drawables;
|
|
||||||
|
|
||||||
root = newRoot;
|
root = newRoot;
|
||||||
if (selectedBeatmapSet != null && !beatmapSets.Contains(selectedBeatmapSet.BeatmapSet))
|
if (selectedBeatmapSet != null && !beatmapSets.Contains(selectedBeatmapSet.BeatmapSet))
|
||||||
selectedBeatmapSet = null;
|
selectedBeatmapSet = null;
|
||||||
|
|
||||||
scrollableContent.Clear(false);
|
ScrollableContent.Clear(false);
|
||||||
itemsCache.Invalidate();
|
itemsCache.Invalidate();
|
||||||
scrollPositionCache.Invalidate();
|
scrollPositionCache.Invalidate();
|
||||||
|
|
||||||
@ -118,11 +127,12 @@ namespace osu.Game.Screens.Select
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly List<float> yPositions = new List<float>();
|
private readonly List<CarouselItem> visibleItems = new List<CarouselItem>();
|
||||||
|
|
||||||
private readonly Cached itemsCache = new Cached();
|
private readonly Cached itemsCache = new Cached();
|
||||||
private readonly Cached scrollPositionCache = new Cached();
|
private readonly Cached scrollPositionCache = new Cached();
|
||||||
|
|
||||||
private readonly Container<DrawableCarouselItem> scrollableContent;
|
protected readonly Container<DrawableCarouselItem> ScrollableContent;
|
||||||
|
|
||||||
public Bindable<bool> RightClickScrollingEnabled = new Bindable<bool>();
|
public Bindable<bool> RightClickScrollingEnabled = new Bindable<bool>();
|
||||||
|
|
||||||
@ -130,8 +140,6 @@ namespace osu.Game.Screens.Select
|
|||||||
private readonly List<CarouselBeatmapSet> previouslyVisitedRandomSets = new List<CarouselBeatmapSet>();
|
private readonly List<CarouselBeatmapSet> previouslyVisitedRandomSets = new List<CarouselBeatmapSet>();
|
||||||
private readonly Stack<CarouselBeatmap> randomSelectedBeatmaps = new Stack<CarouselBeatmap>();
|
private readonly Stack<CarouselBeatmap> randomSelectedBeatmaps = new Stack<CarouselBeatmap>();
|
||||||
|
|
||||||
protected List<DrawableCarouselItem> Items = new List<DrawableCarouselItem>();
|
|
||||||
|
|
||||||
private CarouselRoot root;
|
private CarouselRoot root;
|
||||||
|
|
||||||
private IBindable<WeakReference<BeatmapSetInfo>> itemUpdated;
|
private IBindable<WeakReference<BeatmapSetInfo>> itemUpdated;
|
||||||
@ -139,6 +147,8 @@ namespace osu.Game.Screens.Select
|
|||||||
private IBindable<WeakReference<BeatmapInfo>> itemHidden;
|
private IBindable<WeakReference<BeatmapInfo>> itemHidden;
|
||||||
private IBindable<WeakReference<BeatmapInfo>> itemRestored;
|
private IBindable<WeakReference<BeatmapInfo>> itemRestored;
|
||||||
|
|
||||||
|
private readonly DrawablePool<DrawableCarouselBeatmapSet> setPool = new DrawablePool<DrawableCarouselBeatmapSet>(100);
|
||||||
|
|
||||||
public BeatmapCarousel()
|
public BeatmapCarousel()
|
||||||
{
|
{
|
||||||
root = new CarouselRoot(this);
|
root = new CarouselRoot(this);
|
||||||
@ -149,11 +159,15 @@ namespace osu.Game.Screens.Select
|
|||||||
{
|
{
|
||||||
Masking = false,
|
Masking = false,
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Child = scrollableContent = new Container<DrawableCarouselItem>
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
setPool,
|
||||||
|
ScrollableContent = new Container<DrawableCarouselItem>
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,6 +192,7 @@ namespace osu.Game.Screens.Select
|
|||||||
itemRestored = beatmaps.BeatmapRestored.GetBoundCopy();
|
itemRestored = beatmaps.BeatmapRestored.GetBoundCopy();
|
||||||
itemRestored.BindValueChanged(beatmapRestored);
|
itemRestored.BindValueChanged(beatmapRestored);
|
||||||
|
|
||||||
|
if (!beatmapSets.Any())
|
||||||
loadBeatmapSets(GetLoadableBeatmaps());
|
loadBeatmapSets(GetLoadableBeatmaps());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -558,71 +573,101 @@ namespace osu.Game.Screens.Select
|
|||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
if (!itemsCache.IsValid)
|
bool revalidateItems = !itemsCache.IsValid;
|
||||||
updateItems();
|
|
||||||
|
|
||||||
// Remove all items that should no longer be on-screen
|
// First we iterate over all non-filtered carousel items and populate their
|
||||||
scrollableContent.RemoveAll(p => p.Y < visibleUpperBound - p.DrawHeight || p.Y > visibleBottomBound || !p.IsPresent);
|
// vertical position data.
|
||||||
|
if (revalidateItems)
|
||||||
|
updateYPositions();
|
||||||
|
|
||||||
// Find index range of all items that should be on-screen
|
// This data is consumed to find the currently displayable range.
|
||||||
Trace.Assert(Items.Count == yPositions.Count);
|
// This is the range we want to keep drawables for, and should exceed the visible range slightly to avoid drawable churn.
|
||||||
|
var newDisplayRange = getDisplayRange();
|
||||||
|
|
||||||
int firstIndex = yPositions.BinarySearch(visibleUpperBound - DrawableCarouselItem.MAX_HEIGHT);
|
// If the filtered items or visible range has changed, pooling requirements need to be checked.
|
||||||
if (firstIndex < 0) firstIndex = ~firstIndex;
|
// This involves fetching new items from the pool, returning no-longer required items.
|
||||||
int lastIndex = yPositions.BinarySearch(visibleBottomBound);
|
if (revalidateItems || newDisplayRange != displayedRange)
|
||||||
if (lastIndex < 0) lastIndex = ~lastIndex;
|
|
||||||
|
|
||||||
int notVisibleCount = 0;
|
|
||||||
|
|
||||||
// Add those items within the previously found index range that should be displayed.
|
|
||||||
for (int i = firstIndex; i < lastIndex; ++i)
|
|
||||||
{
|
{
|
||||||
DrawableCarouselItem item = Items[i];
|
displayedRange = newDisplayRange;
|
||||||
|
|
||||||
if (!item.Item.Visible)
|
if (visibleItems.Count > 0)
|
||||||
{
|
{
|
||||||
if (!item.IsPresent)
|
var toDisplay = visibleItems.GetRange(displayedRange.first, displayedRange.last - displayedRange.first + 1);
|
||||||
notVisibleCount++;
|
|
||||||
|
foreach (var panel in ScrollableContent.Children)
|
||||||
|
{
|
||||||
|
if (toDisplay.Remove(panel.Item))
|
||||||
|
{
|
||||||
|
// panel already displayed.
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
float depth = i + (item is DrawableCarouselBeatmapSet ? -Items.Count : 0);
|
// panel loaded as drawable but not required by visible range.
|
||||||
|
// remove but only if too far off-screen
|
||||||
// Only add if we're not already part of the content.
|
if (panel.Y + panel.DrawHeight < visibleUpperBound - distance_offscreen_before_unload || panel.Y > visibleBottomBound + distance_offscreen_before_unload)
|
||||||
if (!scrollableContent.Contains(item))
|
|
||||||
{
|
{
|
||||||
// Makes sure headers are always _below_ items,
|
// may want a fade effect here (could be seen if a huge change happens, like a set with 20 difficulties becomes selected).
|
||||||
// and depth flows downward.
|
panel.ClearTransforms();
|
||||||
item.Depth = depth;
|
panel.Expire();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch (item.LoadState)
|
// Add those items within the previously found index range that should be displayed.
|
||||||
|
foreach (var item in toDisplay)
|
||||||
{
|
{
|
||||||
case LoadState.NotLoaded:
|
var panel = setPool.Get(p => p.Item = item);
|
||||||
LoadComponentAsync(item);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case LoadState.Loading:
|
panel.Depth = item.CarouselYPosition;
|
||||||
break;
|
panel.Y = item.CarouselYPosition;
|
||||||
|
|
||||||
default:
|
ScrollableContent.Add(panel);
|
||||||
scrollableContent.Add(item);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
{
|
{
|
||||||
scrollableContent.ChangeChildDepth(item, depth);
|
foreach (DrawableCarouselItem panel in ScrollableContent.Children)
|
||||||
|
{
|
||||||
|
panel.MoveToY(panel.Item.CarouselYPosition, 800, Easing.OutQuint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// this is not actually useful right now, but once we have groups may well be.
|
// Update externally controlled state of currently visible items (e.g. x-offset and opacity).
|
||||||
if (notVisibleCount > 50)
|
// This is a per-frame update on all drawable panels.
|
||||||
itemsCache.Invalidate();
|
foreach (DrawableCarouselItem item in ScrollableContent.Children)
|
||||||
|
{
|
||||||
|
updateItem(item);
|
||||||
|
|
||||||
// Update externally controlled state of currently visible items
|
if (item is DrawableCarouselBeatmapSet set)
|
||||||
// (e.g. x-offset and opacity).
|
{
|
||||||
foreach (DrawableCarouselItem p in scrollableContent.Children)
|
foreach (var diff in set.DrawableBeatmaps)
|
||||||
updateItem(p);
|
updateItem(diff, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly CarouselBoundsItem carouselBoundsItem = new CarouselBoundsItem();
|
||||||
|
|
||||||
|
private (int firstIndex, int lastIndex) getDisplayRange()
|
||||||
|
{
|
||||||
|
// Find index range of all items that should be on-screen
|
||||||
|
carouselBoundsItem.CarouselYPosition = visibleUpperBound - distance_offscreen_to_preload;
|
||||||
|
int firstIndex = visibleItems.BinarySearch(carouselBoundsItem);
|
||||||
|
if (firstIndex < 0) firstIndex = ~firstIndex;
|
||||||
|
|
||||||
|
carouselBoundsItem.CarouselYPosition = visibleBottomBound + distance_offscreen_to_preload;
|
||||||
|
int lastIndex = visibleItems.BinarySearch(carouselBoundsItem);
|
||||||
|
if (lastIndex < 0) lastIndex = ~lastIndex;
|
||||||
|
|
||||||
|
// as we can't be 100% sure on the size of individual carousel drawables,
|
||||||
|
// always play it safe and extend bounds by one.
|
||||||
|
firstIndex = Math.Max(0, firstIndex - 1);
|
||||||
|
lastIndex = Math.Clamp(lastIndex + 1, firstIndex, Math.Max(0, visibleItems.Count - 1));
|
||||||
|
|
||||||
|
return (firstIndex, lastIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void UpdateAfterChildren()
|
protected override void UpdateAfterChildren()
|
||||||
@ -633,15 +678,6 @@ namespace osu.Game.Screens.Select
|
|||||||
updateScrollPosition();
|
updateScrollPosition();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
|
||||||
{
|
|
||||||
base.Dispose(isDisposing);
|
|
||||||
|
|
||||||
// aggressively dispose "off-screen" items to reduce GC pressure.
|
|
||||||
foreach (var i in Items)
|
|
||||||
i.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void beatmapRemoved(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakItem)
|
private void beatmapRemoved(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakItem)
|
||||||
{
|
{
|
||||||
if (weakItem.NewValue.TryGetTarget(out var item))
|
if (weakItem.NewValue.TryGetTarget(out var item))
|
||||||
@ -698,79 +734,62 @@ namespace osu.Game.Screens.Select
|
|||||||
return set;
|
return set;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const float panel_padding = 5;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Computes the target Y positions for every item in the carousel.
|
/// Computes the target Y positions for every item in the carousel.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The Y position of the currently selected item.</returns>
|
/// <returns>The Y position of the currently selected item.</returns>
|
||||||
private void updateItems()
|
private void updateYPositions()
|
||||||
{
|
{
|
||||||
Items = root.Drawables.ToList();
|
visibleItems.Clear();
|
||||||
|
|
||||||
yPositions.Clear();
|
|
||||||
|
|
||||||
float currentY = visibleHalfHeight;
|
float currentY = visibleHalfHeight;
|
||||||
DrawableCarouselBeatmapSet lastSet = null;
|
|
||||||
|
|
||||||
scrollTarget = null;
|
scrollTarget = null;
|
||||||
|
|
||||||
foreach (DrawableCarouselItem d in Items)
|
foreach (CarouselItem item in root.Children)
|
||||||
{
|
{
|
||||||
if (d.IsPresent)
|
if (item.Filtered.Value)
|
||||||
{
|
continue;
|
||||||
switch (d)
|
|
||||||
{
|
|
||||||
case DrawableCarouselBeatmapSet set:
|
|
||||||
{
|
|
||||||
lastSet = set;
|
|
||||||
|
|
||||||
set.MoveToX(set.Item.State.Value == CarouselItemState.Selected ? -100 : 0, 500, Easing.OutExpo);
|
switch (item)
|
||||||
set.MoveToY(currentY, 750, Easing.OutExpo);
|
{
|
||||||
break;
|
case CarouselBeatmapSet set:
|
||||||
}
|
{
|
||||||
|
visibleItems.Add(set);
|
||||||
case DrawableCarouselBeatmap beatmap:
|
set.CarouselYPosition = currentY;
|
||||||
|
|
||||||
|
if (item.State.Value == CarouselItemState.Selected)
|
||||||
{
|
{
|
||||||
if (beatmap.Item.State.Value == CarouselItemState.Selected)
|
|
||||||
// scroll position at currentY makes the set panel appear at the very top of the carousel's screen space
|
// scroll position at currentY makes the set panel appear at the very top of the carousel's screen space
|
||||||
// move down by half of visible height (height of the carousel's visible extent, including semi-transparent areas)
|
// move down by half of visible height (height of the carousel's visible extent, including semi-transparent areas)
|
||||||
// then reapply the top semi-transparent area (because carousel's screen space starts below it)
|
// then reapply the top semi-transparent area (because carousel's screen space starts below it)
|
||||||
// and finally add half of the panel's own height to achieve vertical centering of the panel itself
|
scrollTarget = currentY + DrawableCarouselBeatmapSet.HEIGHT - visibleHalfHeight + BleedTop;
|
||||||
scrollTarget = currentY - visibleHalfHeight + BleedTop + beatmap.DrawHeight / 2;
|
|
||||||
|
|
||||||
void performMove(float y, float? startY = null)
|
foreach (var b in set.Beatmaps)
|
||||||
{
|
{
|
||||||
if (startY != null) beatmap.MoveTo(new Vector2(0, startY.Value));
|
if (!b.Visible)
|
||||||
beatmap.MoveToX(beatmap.Item.State.Value == CarouselItemState.Selected ? -50 : 0, 500, Easing.OutExpo);
|
continue;
|
||||||
beatmap.MoveToY(y, 750, Easing.OutExpo);
|
|
||||||
|
if (b.State.Value == CarouselItemState.Selected)
|
||||||
|
{
|
||||||
|
scrollTarget += b.TotalHeight / 2;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
Debug.Assert(lastSet != null);
|
scrollTarget += b.TotalHeight;
|
||||||
|
}
|
||||||
float? setY = null;
|
|
||||||
if (!d.IsLoaded || beatmap.Alpha == 0) // can't use IsPresent due to DrawableCarouselItem override.
|
|
||||||
setY = lastSet.Y + lastSet.DrawHeight + 5;
|
|
||||||
|
|
||||||
if (d.IsLoaded)
|
|
||||||
performMove(currentY, setY);
|
|
||||||
else
|
|
||||||
{
|
|
||||||
float y = currentY;
|
|
||||||
d.OnLoadComplete += _ => performMove(y, setY);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentY += set.TotalHeight + panel_padding;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
yPositions.Add(currentY);
|
|
||||||
|
|
||||||
if (d.Item.Visible)
|
|
||||||
currentY += d.DrawHeight + 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentY += visibleHalfHeight;
|
currentY += visibleHalfHeight;
|
||||||
scrollableContent.Height = currentY;
|
ScrollableContent.Height = currentY;
|
||||||
|
|
||||||
if (BeatmapSetsLoaded && (selectedBeatmapSet == null || selectedBeatmap == null || selectedBeatmapSet.State.Value != CarouselItemState.Selected))
|
if (BeatmapSetsLoaded && (selectedBeatmapSet == null || selectedBeatmap == null || selectedBeatmapSet.State.Value != CarouselItemState.Selected))
|
||||||
{
|
{
|
||||||
@ -821,21 +840,31 @@ namespace osu.Game.Screens.Select
|
|||||||
/// Update a item's x position and multiplicative alpha based on its y position and
|
/// Update a item's x position and multiplicative alpha based on its y position and
|
||||||
/// the current scroll position.
|
/// the current scroll position.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="p">The item to be updated.</param>
|
/// <param name="item">The item to be updated.</param>
|
||||||
private void updateItem(DrawableCarouselItem p)
|
/// <param name="parent">For nested items, the parent of the item to be updated.</param>
|
||||||
|
private void updateItem(DrawableCarouselItem item, DrawableCarouselItem parent = null)
|
||||||
{
|
{
|
||||||
float itemDrawY = p.Position.Y - visibleUpperBound + p.DrawHeight / 2;
|
Vector2 posInScroll = ScrollableContent.ToLocalSpace(item.Header.ScreenSpaceDrawQuad.Centre);
|
||||||
|
float itemDrawY = posInScroll.Y - visibleUpperBound;
|
||||||
float dist = Math.Abs(1f - itemDrawY / visibleHalfHeight);
|
float dist = Math.Abs(1f - itemDrawY / visibleHalfHeight);
|
||||||
|
|
||||||
// Setting the origin position serves as an additive position on top of potential
|
// adjusting the item's overall X position can cause it to become masked away when
|
||||||
// local transformation we may want to apply (e.g. when a item gets selected, we
|
// child items (difficulties) are still visible.
|
||||||
// may want to smoothly transform it leftwards.)
|
item.Header.X = offsetX(dist, visibleHalfHeight) - (parent?.X ?? 0);
|
||||||
p.OriginPosition = new Vector2(-offsetX(dist, visibleHalfHeight), 0);
|
|
||||||
|
|
||||||
// We are applying a multiplicative alpha (which is internally done by nesting an
|
// We are applying a multiplicative alpha (which is internally done by nesting an
|
||||||
// additional container and setting that container's alpha) such that we can
|
// additional container and setting that container's alpha) such that we can
|
||||||
// layer transformations on top, with a similar reasoning to the previous comment.
|
// layer alpha transformations on top.
|
||||||
p.SetMultiplicativeAlpha(Math.Clamp(1.75f - 1.5f * dist, 0, 1));
|
item.SetMultiplicativeAlpha(Math.Clamp(1.75f - 1.5f * dist, 0, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A carousel item strictly used for binary search purposes.
|
||||||
|
/// </summary>
|
||||||
|
private class CarouselBoundsItem : CarouselItem
|
||||||
|
{
|
||||||
|
public override DrawableCarouselItem CreateDrawableRepresentation() =>
|
||||||
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
private class CarouselRoot : CarouselGroupEagerSelect
|
private class CarouselRoot : CarouselGroupEagerSelect
|
||||||
@ -869,6 +898,7 @@ namespace osu.Game.Screens.Select
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool UserScrolling { get; private set; }
|
public bool UserScrolling { get; private set; }
|
||||||
|
|
||||||
|
// 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)
|
protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default)
|
||||||
{
|
{
|
||||||
UserScrolling = true;
|
UserScrolling = true;
|
||||||
|
@ -10,6 +10,8 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
{
|
{
|
||||||
public class CarouselBeatmap : CarouselItem
|
public class CarouselBeatmap : CarouselItem
|
||||||
{
|
{
|
||||||
|
public override float TotalHeight => DrawableCarouselBeatmap.HEIGHT;
|
||||||
|
|
||||||
public readonly BeatmapInfo Beatmap;
|
public readonly BeatmapInfo Beatmap;
|
||||||
|
|
||||||
public CarouselBeatmap(BeatmapInfo beatmap)
|
public CarouselBeatmap(BeatmapInfo beatmap)
|
||||||
@ -18,7 +20,7 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
State.Value = CarouselItemState.Collapsed;
|
State.Value = CarouselItemState.Collapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DrawableCarouselItem CreateDrawableRepresentation() => new DrawableCarouselBeatmap(this);
|
public override DrawableCarouselItem CreateDrawableRepresentation() => new DrawableCarouselBeatmap(this);
|
||||||
|
|
||||||
public override void Filter(FilterCriteria criteria)
|
public override void Filter(FilterCriteria criteria)
|
||||||
{
|
{
|
||||||
|
@ -12,6 +12,21 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
{
|
{
|
||||||
public class CarouselBeatmapSet : CarouselGroupEagerSelect
|
public class CarouselBeatmapSet : CarouselGroupEagerSelect
|
||||||
{
|
{
|
||||||
|
public override float TotalHeight
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
switch (State.Value)
|
||||||
|
{
|
||||||
|
case CarouselItemState.Selected:
|
||||||
|
return DrawableCarouselBeatmapSet.HEIGHT + Children.Count(c => c.Visible) * DrawableCarouselBeatmap.HEIGHT;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return DrawableCarouselBeatmapSet.HEIGHT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public IEnumerable<CarouselBeatmap> Beatmaps => InternalChildren.OfType<CarouselBeatmap>();
|
public IEnumerable<CarouselBeatmap> Beatmaps => InternalChildren.OfType<CarouselBeatmap>();
|
||||||
|
|
||||||
public BeatmapSetInfo BeatmapSet;
|
public BeatmapSetInfo BeatmapSet;
|
||||||
@ -28,8 +43,6 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
.ForEach(AddChild);
|
.ForEach(AddChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DrawableCarouselItem CreateDrawableRepresentation() => new DrawableCarouselBeatmapSet(this);
|
|
||||||
|
|
||||||
protected override CarouselItem GetNextToSelect()
|
protected override CarouselItem GetNextToSelect()
|
||||||
{
|
{
|
||||||
if (LastSelected == null)
|
if (LastSelected == null)
|
||||||
|
@ -11,7 +11,7 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class CarouselGroup : CarouselItem
|
public class CarouselGroup : CarouselItem
|
||||||
{
|
{
|
||||||
protected override DrawableCarouselItem CreateDrawableRepresentation() => null;
|
public override DrawableCarouselItem CreateDrawableRepresentation() => null;
|
||||||
|
|
||||||
public IReadOnlyList<CarouselItem> Children => InternalChildren;
|
public IReadOnlyList<CarouselItem> Children => InternalChildren;
|
||||||
|
|
||||||
@ -23,22 +23,6 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private ulong currentChildID;
|
private ulong currentChildID;
|
||||||
|
|
||||||
public override List<DrawableCarouselItem> Drawables
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var drawables = base.Drawables;
|
|
||||||
|
|
||||||
// if we are explicitly not present, don't ever present children.
|
|
||||||
// without this check, children drawables can potentially be presented without their group header.
|
|
||||||
if (DrawableRepresentation.Value?.IsPresent == false) return drawables;
|
|
||||||
|
|
||||||
foreach (var c in InternalChildren)
|
|
||||||
drawables.AddRange(c.Drawables);
|
|
||||||
return drawables;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual void RemoveChild(CarouselItem i)
|
public virtual void RemoveChild(CarouselItem i)
|
||||||
{
|
{
|
||||||
InternalChildren.Remove(i);
|
InternalChildren.Remove(i);
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Select.Carousel
|
namespace osu.Game.Screens.Select.Carousel
|
||||||
@ -54,6 +55,14 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
updateSelectedIndex();
|
updateSelectedIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void AddChildren(IEnumerable<CarouselItem> items)
|
||||||
|
{
|
||||||
|
foreach (var i in items)
|
||||||
|
base.AddChild(i);
|
||||||
|
|
||||||
|
attemptSelection();
|
||||||
|
}
|
||||||
|
|
||||||
public override void AddChild(CarouselItem i)
|
public override void AddChild(CarouselItem i)
|
||||||
{
|
{
|
||||||
base.AddChild(i);
|
base.AddChild(i);
|
||||||
|
114
osu.Game/Screens/Select/Carousel/CarouselHeader.cs
Normal file
114
osu.Game/Screens/Select/Carousel/CarouselHeader.cs
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
// 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.Framework.Allocation;
|
||||||
|
using osu.Framework.Audio;
|
||||||
|
using osu.Framework.Audio.Sample;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Effects;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Select.Carousel
|
||||||
|
{
|
||||||
|
public class CarouselHeader : Container
|
||||||
|
{
|
||||||
|
private SampleChannel sampleHover;
|
||||||
|
|
||||||
|
private readonly Box hoverLayer;
|
||||||
|
|
||||||
|
public Container BorderContainer;
|
||||||
|
|
||||||
|
public readonly Bindable<CarouselItemState> State = new Bindable<CarouselItemState>(CarouselItemState.NotSelected);
|
||||||
|
|
||||||
|
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
|
||||||
|
|
||||||
|
public CarouselHeader()
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X;
|
||||||
|
Height = DrawableCarouselItem.MAX_HEIGHT;
|
||||||
|
|
||||||
|
InternalChild = BorderContainer = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Masking = true,
|
||||||
|
CornerRadius = 10,
|
||||||
|
BorderColour = new Color4(221, 255, 255, 255),
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
Content,
|
||||||
|
hoverLayer = new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Alpha = 0,
|
||||||
|
Blending = BlendingParameters.Additive,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(AudioManager audio, OsuColour colours)
|
||||||
|
{
|
||||||
|
sampleHover = audio.Samples.Get($@"SongSelect/song-ping-variation-{RNG.Next(1, 5)}");
|
||||||
|
hoverLayer.Colour = colours.Blue.Opacity(0.1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
State.BindValueChanged(updateState, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateState(ValueChangedEvent<CarouselItemState> state)
|
||||||
|
{
|
||||||
|
switch (state.NewValue)
|
||||||
|
{
|
||||||
|
case CarouselItemState.Collapsed:
|
||||||
|
case CarouselItemState.NotSelected:
|
||||||
|
BorderContainer.BorderThickness = 0;
|
||||||
|
BorderContainer.EdgeEffect = new EdgeEffectParameters
|
||||||
|
{
|
||||||
|
Type = EdgeEffectType.Shadow,
|
||||||
|
Offset = new Vector2(1),
|
||||||
|
Radius = 10,
|
||||||
|
Colour = Color4.Black.Opacity(100),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CarouselItemState.Selected:
|
||||||
|
BorderContainer.BorderThickness = 2.5f;
|
||||||
|
BorderContainer.EdgeEffect = new EdgeEffectParameters
|
||||||
|
{
|
||||||
|
Type = EdgeEffectType.Glow,
|
||||||
|
Colour = new Color4(130, 204, 255, 150),
|
||||||
|
Radius = 20,
|
||||||
|
Roundness = 10,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnHover(HoverEvent e)
|
||||||
|
{
|
||||||
|
sampleHover?.Play();
|
||||||
|
|
||||||
|
hoverLayer.FadeIn(100, Easing.OutQuint);
|
||||||
|
return base.OnHover(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnHoverLost(HoverLostEvent e)
|
||||||
|
{
|
||||||
|
hoverLayer.FadeOut(1000, Easing.OutQuint);
|
||||||
|
base.OnHoverLost(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,13 +2,19 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Select.Carousel
|
namespace osu.Game.Screens.Select.Carousel
|
||||||
{
|
{
|
||||||
public abstract class CarouselItem
|
public abstract class CarouselItem : IComparable<CarouselItem>
|
||||||
{
|
{
|
||||||
|
public virtual float TotalHeight => 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An externally defined value used to determine this item's vertical display offset relative to the carousel.
|
||||||
|
/// </summary>
|
||||||
|
public float CarouselYPosition;
|
||||||
|
|
||||||
public readonly BindableBool Filtered = new BindableBool();
|
public readonly BindableBool Filtered = new BindableBool();
|
||||||
|
|
||||||
public readonly Bindable<CarouselItemState> State = new Bindable<CarouselItemState>(CarouselItemState.NotSelected);
|
public readonly Bindable<CarouselItemState> State = new Bindable<CarouselItemState>(CarouselItemState.NotSelected);
|
||||||
@ -18,23 +24,8 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Visible => State.Value != CarouselItemState.Collapsed && !Filtered.Value;
|
public bool Visible => State.Value != CarouselItemState.Collapsed && !Filtered.Value;
|
||||||
|
|
||||||
public virtual List<DrawableCarouselItem> Drawables
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var items = new List<DrawableCarouselItem>();
|
|
||||||
|
|
||||||
var self = DrawableRepresentation.Value;
|
|
||||||
if (self?.IsPresent == true) items.Add(self);
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected CarouselItem()
|
protected CarouselItem()
|
||||||
{
|
{
|
||||||
DrawableRepresentation = new Lazy<DrawableCarouselItem>(CreateDrawableRepresentation);
|
|
||||||
|
|
||||||
Filtered.ValueChanged += filtered =>
|
Filtered.ValueChanged += filtered =>
|
||||||
{
|
{
|
||||||
if (filtered.NewValue && State.Value == CarouselItemState.Selected)
|
if (filtered.NewValue && State.Value == CarouselItemState.Selected)
|
||||||
@ -42,23 +33,23 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readonly Lazy<DrawableCarouselItem> DrawableRepresentation;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Used as a default sort method for <see cref="CarouselItem"/>s of differing types.
|
/// Used as a default sort method for <see cref="CarouselItem"/>s of differing types.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal ulong ChildID;
|
internal ulong ChildID;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a fresh drawable version of this item. If you wish to consume the current representation, use <see cref="DrawableRepresentation"/> instead.
|
/// Create a fresh drawable version of this item.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected abstract DrawableCarouselItem CreateDrawableRepresentation();
|
public abstract DrawableCarouselItem CreateDrawableRepresentation();
|
||||||
|
|
||||||
public virtual void Filter(FilterCriteria criteria)
|
public virtual void Filter(FilterCriteria criteria)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual int CompareTo(FilterCriteria criteria, CarouselItem other) => ChildID.CompareTo(other.ChildID);
|
public virtual int CompareTo(FilterCriteria criteria, CarouselItem other) => ChildID.CompareTo(other.ChildID);
|
||||||
|
|
||||||
|
public int CompareTo(CarouselItem other) => CarouselYPosition.CompareTo(other.CarouselYPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum CarouselItemState
|
public enum CarouselItemState
|
||||||
|
@ -31,6 +31,15 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
{
|
{
|
||||||
public class DrawableCarouselBeatmap : DrawableCarouselItem, IHasContextMenu
|
public class DrawableCarouselBeatmap : DrawableCarouselItem, IHasContextMenu
|
||||||
{
|
{
|
||||||
|
public const float CAROUSEL_BEATMAP_SPACING = 5;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The height of a carousel beatmap, including vertical spacing.
|
||||||
|
/// </summary>
|
||||||
|
public const float HEIGHT = height + CAROUSEL_BEATMAP_SPACING;
|
||||||
|
|
||||||
|
private const float height = MAX_HEIGHT * 0.6f;
|
||||||
|
|
||||||
private readonly BeatmapInfo beatmap;
|
private readonly BeatmapInfo beatmap;
|
||||||
|
|
||||||
private Sprite background;
|
private Sprite background;
|
||||||
@ -58,15 +67,16 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
private CancellationTokenSource starDifficultyCancellationSource;
|
private CancellationTokenSource starDifficultyCancellationSource;
|
||||||
|
|
||||||
public DrawableCarouselBeatmap(CarouselBeatmap panel)
|
public DrawableCarouselBeatmap(CarouselBeatmap panel)
|
||||||
: base(panel)
|
|
||||||
{
|
{
|
||||||
beatmap = panel.Beatmap;
|
beatmap = panel.Beatmap;
|
||||||
Height *= 0.60f;
|
Item = panel;
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader(true)]
|
[BackgroundDependencyLoader(true)]
|
||||||
private void load(BeatmapManager manager, SongSelect songSelect)
|
private void load(BeatmapManager manager, SongSelect songSelect)
|
||||||
{
|
{
|
||||||
|
Header.Height = height;
|
||||||
|
|
||||||
if (songSelect != null)
|
if (songSelect != null)
|
||||||
{
|
{
|
||||||
startRequested = b => songSelect.FinaliseSelection(b);
|
startRequested = b => songSelect.FinaliseSelection(b);
|
||||||
@ -77,7 +87,7 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
if (manager != null)
|
if (manager != null)
|
||||||
hideRequested = manager.Hide;
|
hideRequested = manager.Hide;
|
||||||
|
|
||||||
Children = new Drawable[]
|
Header.Children = new Drawable[]
|
||||||
{
|
{
|
||||||
background = new Box
|
background = new Box
|
||||||
{
|
{
|
||||||
@ -168,6 +178,8 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
{
|
{
|
||||||
base.Selected();
|
base.Selected();
|
||||||
|
|
||||||
|
MovementContainer.MoveToX(-50, 500, Easing.OutExpo);
|
||||||
|
|
||||||
background.Colour = ColourInfo.GradientVertical(
|
background.Colour = ColourInfo.GradientVertical(
|
||||||
new Color4(20, 43, 51, 255),
|
new Color4(20, 43, 51, 255),
|
||||||
new Color4(40, 86, 102, 255));
|
new Color4(40, 86, 102, 255));
|
||||||
@ -179,6 +191,8 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
{
|
{
|
||||||
base.Deselected();
|
base.Deselected();
|
||||||
|
|
||||||
|
MovementContainer.MoveToX(0, 500, Easing.OutExpo);
|
||||||
|
|
||||||
background.Colour = new Color4(20, 43, 51, 255);
|
background.Colour = new Color4(20, 43, 51, 255);
|
||||||
triangles.Colour = OsuColour.Gray(0.5f);
|
triangles.Colour = OsuColour.Gray(0.5f);
|
||||||
}
|
}
|
||||||
|
@ -3,32 +3,24 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Colour;
|
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Cursor;
|
using osu.Framework.Graphics.Cursor;
|
||||||
using osu.Framework.Graphics.Shapes;
|
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osu.Framework.Input.Events;
|
|
||||||
using osu.Framework.Localisation;
|
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Beatmaps.Drawables;
|
|
||||||
using osu.Game.Collections;
|
using osu.Game.Collections;
|
||||||
using osu.Game.Graphics;
|
|
||||||
using osu.Game.Graphics.Sprites;
|
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Rulesets;
|
|
||||||
using osuTK;
|
|
||||||
using osuTK.Graphics;
|
|
||||||
|
|
||||||
namespace osu.Game.Screens.Select.Carousel
|
namespace osu.Game.Screens.Select.Carousel
|
||||||
{
|
{
|
||||||
public class DrawableCarouselBeatmapSet : DrawableCarouselItem, IHasContextMenu
|
public class DrawableCarouselBeatmapSet : DrawableCarouselItem, IHasContextMenu
|
||||||
{
|
{
|
||||||
|
public const float HEIGHT = MAX_HEIGHT;
|
||||||
|
|
||||||
private Action<BeatmapSetInfo> restoreHiddenRequested;
|
private Action<BeatmapSetInfo> restoreHiddenRequested;
|
||||||
private Action<int> viewDetails;
|
private Action<int> viewDetails;
|
||||||
|
|
||||||
@ -41,99 +33,139 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
[Resolved(CanBeNull = true)]
|
[Resolved(CanBeNull = true)]
|
||||||
private ManageCollectionsDialog manageCollectionsDialog { get; set; }
|
private ManageCollectionsDialog manageCollectionsDialog { get; set; }
|
||||||
|
|
||||||
private readonly BeatmapSetInfo beatmapSet;
|
public IEnumerable<DrawableCarouselItem> DrawableBeatmaps => beatmapContainer?.Children ?? Enumerable.Empty<DrawableCarouselItem>();
|
||||||
|
|
||||||
public DrawableCarouselBeatmapSet(CarouselBeatmapSet set)
|
private Container<DrawableCarouselItem> beatmapContainer;
|
||||||
: base(set)
|
|
||||||
|
private BeatmapSetInfo beatmapSet;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private BeatmapManager manager { get; set; }
|
||||||
|
|
||||||
|
protected override void FreeAfterUse()
|
||||||
{
|
{
|
||||||
beatmapSet = set.BeatmapSet;
|
base.FreeAfterUse();
|
||||||
|
|
||||||
|
Item = null;
|
||||||
|
|
||||||
|
ClearTransforms();
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader(true)]
|
[BackgroundDependencyLoader(true)]
|
||||||
private void load(BeatmapManager manager, BeatmapSetOverlay beatmapOverlay)
|
private void load(BeatmapSetOverlay beatmapOverlay)
|
||||||
{
|
{
|
||||||
restoreHiddenRequested = s => s.Beatmaps.ForEach(manager.Restore);
|
restoreHiddenRequested = s => s.Beatmaps.ForEach(manager.Restore);
|
||||||
|
|
||||||
if (beatmapOverlay != null)
|
if (beatmapOverlay != null)
|
||||||
viewDetails = beatmapOverlay.FetchAndShowBeatmapSet;
|
viewDetails = beatmapOverlay.FetchAndShowBeatmapSet;
|
||||||
|
}
|
||||||
|
|
||||||
Children = new Drawable[]
|
protected override void UpdateItem()
|
||||||
{
|
{
|
||||||
new DelayedLoadUnloadWrapper(() =>
|
base.UpdateItem();
|
||||||
|
|
||||||
|
Content.Clear();
|
||||||
|
beatmapContainer = null;
|
||||||
|
|
||||||
|
if (Item == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
beatmapSet = ((CarouselBeatmapSet)Item).BeatmapSet;
|
||||||
|
|
||||||
|
DelayedLoadWrapper background;
|
||||||
|
DelayedLoadWrapper mainFlow;
|
||||||
|
|
||||||
|
Header.Children = new Drawable[]
|
||||||
{
|
{
|
||||||
var background = new PanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.FirstOrDefault()))
|
background = new DelayedLoadWrapper(new SetPanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.FirstOrDefault()))
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
}, 300),
|
||||||
|
mainFlow = new DelayedLoadWrapper(new SetPanelContent((CarouselBeatmapSet)Item), 100),
|
||||||
};
|
};
|
||||||
|
|
||||||
background.OnLoadComplete += d => d.FadeInFromZero(1000, Easing.OutQuint);
|
background.DelayedLoadComplete += fadeContentIn;
|
||||||
|
mainFlow.DelayedLoadComplete += fadeContentIn;
|
||||||
|
}
|
||||||
|
|
||||||
return background;
|
private void fadeContentIn(Drawable d) => d.FadeInFromZero(750, Easing.OutQuint);
|
||||||
}, 300, 5000
|
|
||||||
),
|
protected override void Deselected()
|
||||||
new FillFlowContainer
|
|
||||||
{
|
{
|
||||||
Direction = FillDirection.Vertical,
|
base.Deselected();
|
||||||
Padding = new MarginPadding { Top = 5, Left = 18, Right = 10, Bottom = 10 },
|
|
||||||
AutoSizeAxes = Axes.Both,
|
MovementContainer.MoveToX(0, 500, Easing.OutExpo);
|
||||||
Children = new Drawable[]
|
|
||||||
|
if (beatmapContainer != null)
|
||||||
{
|
{
|
||||||
new OsuSpriteText
|
foreach (var beatmap in beatmapContainer)
|
||||||
{
|
beatmap.MoveToY(0, 800, Easing.OutQuint);
|
||||||
Text = new LocalisedString((beatmapSet.Metadata.TitleUnicode, beatmapSet.Metadata.Title)),
|
|
||||||
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true),
|
|
||||||
Shadow = true,
|
|
||||||
},
|
|
||||||
new OsuSpriteText
|
|
||||||
{
|
|
||||||
Text = new LocalisedString((beatmapSet.Metadata.ArtistUnicode, beatmapSet.Metadata.Artist)),
|
|
||||||
Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true),
|
|
||||||
Shadow = true,
|
|
||||||
},
|
|
||||||
new FillFlowContainer
|
|
||||||
{
|
|
||||||
Direction = FillDirection.Horizontal,
|
|
||||||
AutoSizeAxes = Axes.Both,
|
|
||||||
Margin = new MarginPadding { Top = 5 },
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
new BeatmapSetOnlineStatusPill
|
|
||||||
{
|
|
||||||
Origin = Anchor.CentreLeft,
|
|
||||||
Anchor = Anchor.CentreLeft,
|
|
||||||
Margin = new MarginPadding { Right = 5 },
|
|
||||||
TextSize = 11,
|
|
||||||
TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 },
|
|
||||||
Status = beatmapSet.Status
|
|
||||||
},
|
|
||||||
new FillFlowContainer<DifficultyIcon>
|
|
||||||
{
|
|
||||||
AutoSizeAxes = Axes.Both,
|
|
||||||
Spacing = new Vector2(3),
|
|
||||||
ChildrenEnumerable = getDifficultyIcons(),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void Selected()
|
||||||
|
{
|
||||||
|
base.Selected();
|
||||||
|
|
||||||
|
MovementContainer.MoveToX(-100, 500, Easing.OutExpo);
|
||||||
|
|
||||||
|
updateBeatmapDifficulties();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateBeatmapDifficulties()
|
||||||
|
{
|
||||||
|
var carouselBeatmapSet = (CarouselBeatmapSet)Item;
|
||||||
|
|
||||||
|
var visibleBeatmaps = carouselBeatmapSet.Children.Where(c => c.Visible).ToArray();
|
||||||
|
|
||||||
|
// if we are already displaying all the correct beatmaps, only run animation updates.
|
||||||
|
// note that the displayed beatmaps may change due to the applied filter.
|
||||||
|
// a future optimisation could add/remove only changed difficulties rather than reinitialise.
|
||||||
|
if (beatmapContainer != null && visibleBeatmaps.Length == beatmapContainer.Count && visibleBeatmaps.All(b => beatmapContainer.Any(c => c.Item == b)))
|
||||||
|
{
|
||||||
|
updateBeatmapYPositions();
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// on selection we show our child beatmaps.
|
||||||
|
// for now this is a simple drawable construction each selection.
|
||||||
|
// can be improved in the future.
|
||||||
|
beatmapContainer = new Container<DrawableCarouselItem>
|
||||||
|
{
|
||||||
|
X = 100,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
ChildrenEnumerable = visibleBeatmaps.Select(c => c.CreateDrawableRepresentation())
|
||||||
};
|
};
|
||||||
|
|
||||||
|
LoadComponentAsync(beatmapContainer, loaded =>
|
||||||
|
{
|
||||||
|
// make sure the pooled target hasn't changed.
|
||||||
|
if (carouselBeatmapSet != Item)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Content.Child = loaded;
|
||||||
|
updateBeatmapYPositions();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private const int maximum_difficulty_icons = 18;
|
void updateBeatmapYPositions()
|
||||||
|
|
||||||
private IEnumerable<DifficultyIcon> getDifficultyIcons()
|
|
||||||
{
|
{
|
||||||
var beatmaps = ((CarouselBeatmapSet)Item).Beatmaps.ToList();
|
float yPos = DrawableCarouselBeatmap.CAROUSEL_BEATMAP_SPACING;
|
||||||
|
|
||||||
return beatmaps.Count > maximum_difficulty_icons
|
foreach (var panel in beatmapContainer.Children)
|
||||||
? (IEnumerable<DifficultyIcon>)beatmaps.GroupBy(b => b.Beatmap.Ruleset).Select(group => new FilterableGroupedDifficultyIcon(group.ToList(), group.Key))
|
{
|
||||||
: beatmaps.Select(b => new FilterableDifficultyIcon(b));
|
panel.MoveToY(yPos, 800, Easing.OutQuint);
|
||||||
|
yPos += panel.Item.TotalHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public MenuItem[] ContextMenuItems
|
public MenuItem[] ContextMenuItems
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
|
Debug.Assert(beatmapSet != null);
|
||||||
|
|
||||||
List<MenuItem> items = new List<MenuItem>();
|
List<MenuItem> items = new List<MenuItem>();
|
||||||
|
|
||||||
if (Item.State.Value == CarouselItemState.NotSelected)
|
if (Item.State.Value == CarouselItemState.NotSelected)
|
||||||
@ -162,6 +194,8 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
|
|
||||||
private MenuItem createCollectionMenuItem(BeatmapCollection collection)
|
private MenuItem createCollectionMenuItem(BeatmapCollection collection)
|
||||||
{
|
{
|
||||||
|
Debug.Assert(beatmapSet != null);
|
||||||
|
|
||||||
TernaryState state;
|
TernaryState state;
|
||||||
|
|
||||||
var countExisting = beatmapSet.Beatmaps.Count(b => collection.Beatmaps.Contains(b));
|
var countExisting = beatmapSet.Beatmaps.Count(b => collection.Beatmaps.Contains(b));
|
||||||
@ -196,116 +230,5 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
State = { Value = state }
|
State = { Value = state }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private class PanelBackground : BufferedContainer
|
|
||||||
{
|
|
||||||
public PanelBackground(WorkingBeatmap working)
|
|
||||||
{
|
|
||||||
CacheDrawnFrameBuffer = true;
|
|
||||||
RedrawOnScale = false;
|
|
||||||
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
new BeatmapBackgroundSprite(working)
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
FillMode = FillMode.Fill,
|
|
||||||
},
|
|
||||||
new FillFlowContainer
|
|
||||||
{
|
|
||||||
Depth = -1,
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Direction = FillDirection.Horizontal,
|
|
||||||
// This makes the gradient not be perfectly horizontal, but diagonal at a ~40° angle
|
|
||||||
Shear = new Vector2(0.8f, 0),
|
|
||||||
Alpha = 0.5f,
|
|
||||||
Children = new[]
|
|
||||||
{
|
|
||||||
// The left half with no gradient applied
|
|
||||||
new Box
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Colour = Color4.Black,
|
|
||||||
Width = 0.4f,
|
|
||||||
},
|
|
||||||
// Piecewise-linear gradient with 3 segments to make it appear smoother
|
|
||||||
new Box
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Colour = ColourInfo.GradientHorizontal(Color4.Black, new Color4(0f, 0f, 0f, 0.9f)),
|
|
||||||
Width = 0.05f,
|
|
||||||
},
|
|
||||||
new Box
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.9f), new Color4(0f, 0f, 0f, 0.1f)),
|
|
||||||
Width = 0.2f,
|
|
||||||
},
|
|
||||||
new Box
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.1f), new Color4(0, 0, 0, 0)),
|
|
||||||
Width = 0.05f,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class FilterableDifficultyIcon : DifficultyIcon
|
|
||||||
{
|
|
||||||
private readonly BindableBool filtered = new BindableBool();
|
|
||||||
|
|
||||||
public bool IsFiltered => filtered.Value;
|
|
||||||
|
|
||||||
public readonly CarouselBeatmap Item;
|
|
||||||
|
|
||||||
public FilterableDifficultyIcon(CarouselBeatmap item)
|
|
||||||
: base(item.Beatmap)
|
|
||||||
{
|
|
||||||
filtered.BindTo(item.Filtered);
|
|
||||||
filtered.ValueChanged += isFiltered => Schedule(() => this.FadeTo(isFiltered.NewValue ? 0.1f : 1, 100));
|
|
||||||
filtered.TriggerChange();
|
|
||||||
|
|
||||||
Item = item;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool OnClick(ClickEvent e)
|
|
||||||
{
|
|
||||||
Item.State.Value = CarouselItemState.Selected;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class FilterableGroupedDifficultyIcon : GroupedDifficultyIcon
|
|
||||||
{
|
|
||||||
public readonly List<CarouselBeatmap> Items;
|
|
||||||
|
|
||||||
public FilterableGroupedDifficultyIcon(List<CarouselBeatmap> items, RulesetInfo ruleset)
|
|
||||||
: base(items.Select(i => i.Beatmap).ToList(), ruleset, Color4.White)
|
|
||||||
{
|
|
||||||
Items = items;
|
|
||||||
|
|
||||||
foreach (var item in items)
|
|
||||||
item.Filtered.BindValueChanged(_ => Scheduler.AddOnce(updateFilteredDisplay));
|
|
||||||
|
|
||||||
updateFilteredDisplay();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool OnClick(ClickEvent e)
|
|
||||||
{
|
|
||||||
Items.First().State.Value = CarouselItemState.Selected;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateFilteredDisplay()
|
|
||||||
{
|
|
||||||
// for now, fade the whole group based on the ratio of hidden items.
|
|
||||||
this.FadeTo(1 - 0.9f * ((float)Items.Count(i => i.Filtered.Value) / Items.Count), 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,106 +1,133 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Framework.Allocation;
|
using System.Diagnostics;
|
||||||
using osu.Framework.Audio;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Audio.Sample;
|
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Effects;
|
using osu.Framework.Graphics.Pooling;
|
||||||
using osu.Framework.Graphics.Shapes;
|
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Framework.Utils;
|
|
||||||
using osu.Game.Graphics;
|
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
|
||||||
|
|
||||||
namespace osu.Game.Screens.Select.Carousel
|
namespace osu.Game.Screens.Select.Carousel
|
||||||
{
|
{
|
||||||
public abstract class DrawableCarouselItem : Container
|
public abstract class DrawableCarouselItem : PoolableDrawable
|
||||||
{
|
{
|
||||||
public const float MAX_HEIGHT = 80;
|
public const float MAX_HEIGHT = 80;
|
||||||
|
|
||||||
public override bool RemoveWhenNotAlive => false;
|
public override bool IsPresent => base.IsPresent || Item?.Visible == true;
|
||||||
|
|
||||||
public override bool IsPresent => base.IsPresent || Item.Visible;
|
public readonly CarouselHeader Header;
|
||||||
|
|
||||||
public readonly CarouselItem Item;
|
/// <summary>
|
||||||
|
/// Optional content which sits below the header.
|
||||||
|
/// </summary>
|
||||||
|
protected readonly Container<Drawable> Content;
|
||||||
|
|
||||||
private Container nestedContainer;
|
protected readonly Container MovementContainer;
|
||||||
private Container borderContainer;
|
|
||||||
|
|
||||||
private Box hoverLayer;
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
|
||||||
|
Header.ReceivePositionalInputAt(screenSpacePos);
|
||||||
|
|
||||||
protected override Container<Drawable> Content => nestedContainer;
|
private CarouselItem item;
|
||||||
|
|
||||||
protected DrawableCarouselItem(CarouselItem item)
|
public CarouselItem Item
|
||||||
{
|
{
|
||||||
Item = item;
|
get => item;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (item == value)
|
||||||
|
return;
|
||||||
|
|
||||||
Height = MAX_HEIGHT;
|
if (item != null)
|
||||||
RelativeSizeAxes = Axes.X;
|
{
|
||||||
Alpha = 0;
|
item.Filtered.ValueChanged -= onStateChange;
|
||||||
|
item.State.ValueChanged -= onStateChange;
|
||||||
|
|
||||||
|
Header.State.UnbindFrom(item.State);
|
||||||
|
|
||||||
|
if (item is CarouselGroup group)
|
||||||
|
{
|
||||||
|
foreach (var c in group.Children)
|
||||||
|
c.Filtered.ValueChanged -= onStateChange;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private SampleChannel sampleHover;
|
item = value;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
if (IsLoaded)
|
||||||
private void load(AudioManager audio, OsuColour colours)
|
UpdateItem();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected DrawableCarouselItem()
|
||||||
{
|
{
|
||||||
InternalChild = borderContainer = new Container
|
RelativeSizeAxes = Axes.X;
|
||||||
|
|
||||||
|
Alpha = 0;
|
||||||
|
|
||||||
|
InternalChildren = new Drawable[]
|
||||||
|
{
|
||||||
|
MovementContainer = new Container
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Masking = true,
|
|
||||||
CornerRadius = 10,
|
|
||||||
BorderColour = new Color4(221, 255, 255, 255),
|
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
nestedContainer = new Container
|
Header = new CarouselHeader(),
|
||||||
|
Content = new Container
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
},
|
|
||||||
hoverLayer = new Box
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Alpha = 0,
|
|
||||||
Blending = BlendingParameters.Additive,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
sampleHover = audio.Samples.Get($@"SongSelect/song-ping-variation-{RNG.Next(1, 5)}");
|
|
||||||
hoverLayer.Colour = colours.Blue.Opacity(0.1f);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnHover(HoverEvent e)
|
public void SetMultiplicativeAlpha(float alpha) => Header.BorderContainer.Alpha = alpha;
|
||||||
{
|
|
||||||
sampleHover?.Play();
|
|
||||||
|
|
||||||
hoverLayer.FadeIn(100, Easing.OutQuint);
|
|
||||||
return base.OnHover(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnHoverLost(HoverLostEvent e)
|
|
||||||
{
|
|
||||||
hoverLayer.FadeOut(1000, Easing.OutQuint);
|
|
||||||
base.OnHoverLost(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetMultiplicativeAlpha(float alpha) => borderContainer.Alpha = alpha;
|
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
ApplyState();
|
UpdateItem();
|
||||||
Item.Filtered.ValueChanged += _ => Schedule(ApplyState);
|
|
||||||
Item.State.ValueChanged += _ => Schedule(ApplyState);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
Content.Y = Header.Height;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void UpdateItem()
|
||||||
|
{
|
||||||
|
if (item == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Scheduler.AddOnce(ApplyState);
|
||||||
|
|
||||||
|
Item.Filtered.ValueChanged += onStateChange;
|
||||||
|
Item.State.ValueChanged += onStateChange;
|
||||||
|
|
||||||
|
Header.State.BindTo(Item.State);
|
||||||
|
|
||||||
|
if (Item is CarouselGroup group)
|
||||||
|
{
|
||||||
|
foreach (var c in group.Children)
|
||||||
|
c.Filtered.ValueChanged += onStateChange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onStateChange(ValueChangedEvent<CarouselItemState> obj) => Scheduler.AddOnce(ApplyState);
|
||||||
|
|
||||||
|
private void onStateChange(ValueChangedEvent<bool> _) => Scheduler.AddOnce(ApplyState);
|
||||||
|
|
||||||
protected virtual void ApplyState()
|
protected virtual void ApplyState()
|
||||||
{
|
{
|
||||||
if (!IsLoaded) return;
|
// Use the fact that we know the precise height of the item from the model to avoid the need for AutoSize overhead.
|
||||||
|
// Additionally, AutoSize doesn't work well due to content starting off-screen and being masked away.
|
||||||
|
Height = Item.TotalHeight;
|
||||||
|
|
||||||
|
Debug.Assert(Item != null);
|
||||||
|
|
||||||
switch (Item.State.Value)
|
switch (Item.State.Value)
|
||||||
{
|
{
|
||||||
@ -121,30 +148,11 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
|
|
||||||
protected virtual void Selected()
|
protected virtual void Selected()
|
||||||
{
|
{
|
||||||
Item.State.Value = CarouselItemState.Selected;
|
Debug.Assert(Item != null);
|
||||||
|
|
||||||
borderContainer.BorderThickness = 2.5f;
|
|
||||||
borderContainer.EdgeEffect = new EdgeEffectParameters
|
|
||||||
{
|
|
||||||
Type = EdgeEffectType.Glow,
|
|
||||||
Colour = new Color4(130, 204, 255, 150),
|
|
||||||
Radius = 20,
|
|
||||||
Roundness = 10,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void Deselected()
|
protected virtual void Deselected()
|
||||||
{
|
{
|
||||||
Item.State.Value = CarouselItemState.NotSelected;
|
|
||||||
|
|
||||||
borderContainer.BorderThickness = 0;
|
|
||||||
borderContainer.EdgeEffect = new EdgeEffectParameters
|
|
||||||
{
|
|
||||||
Type = EdgeEffectType.Shadow,
|
|
||||||
Offset = new Vector2(1),
|
|
||||||
Radius = 10,
|
|
||||||
Colour = Color4.Black.Opacity(100),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnClick(ClickEvent e)
|
protected override bool OnClick(ClickEvent e)
|
||||||
|
35
osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs
Normal file
35
osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// 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.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Beatmaps.Drawables;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Select.Carousel
|
||||||
|
{
|
||||||
|
public class FilterableDifficultyIcon : DifficultyIcon
|
||||||
|
{
|
||||||
|
private readonly BindableBool filtered = new BindableBool();
|
||||||
|
|
||||||
|
public bool IsFiltered => filtered.Value;
|
||||||
|
|
||||||
|
public readonly CarouselBeatmap Item;
|
||||||
|
|
||||||
|
public FilterableDifficultyIcon(CarouselBeatmap item)
|
||||||
|
: base(item.Beatmap)
|
||||||
|
{
|
||||||
|
filtered.BindTo(item.Filtered);
|
||||||
|
filtered.ValueChanged += isFiltered => Schedule(() => this.FadeTo(isFiltered.NewValue ? 0.1f : 1, 100));
|
||||||
|
filtered.TriggerChange();
|
||||||
|
|
||||||
|
Item = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnClick(ClickEvent e)
|
||||||
|
{
|
||||||
|
Item.State.Value = CarouselItemState.Selected;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
// 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 System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Beatmaps.Drawables;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Select.Carousel
|
||||||
|
{
|
||||||
|
public class FilterableGroupedDifficultyIcon : GroupedDifficultyIcon
|
||||||
|
{
|
||||||
|
public readonly List<CarouselBeatmap> Items;
|
||||||
|
|
||||||
|
public FilterableGroupedDifficultyIcon(List<CarouselBeatmap> items, RulesetInfo ruleset)
|
||||||
|
: base(items.Select(i => i.Beatmap).ToList(), ruleset, Color4.White)
|
||||||
|
{
|
||||||
|
Items = items;
|
||||||
|
|
||||||
|
foreach (var item in items)
|
||||||
|
item.Filtered.BindValueChanged(_ => Scheduler.AddOnce(updateFilteredDisplay));
|
||||||
|
|
||||||
|
updateFilteredDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnClick(ClickEvent e)
|
||||||
|
{
|
||||||
|
Items.First().State.Value = CarouselItemState.Selected;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateFilteredDisplay()
|
||||||
|
{
|
||||||
|
// for now, fade the whole group based on the ratio of hidden items.
|
||||||
|
this.FadeTo(1 - 0.9f * ((float)Items.Count(i => i.Filtered.Value) / Items.Count), 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
72
osu.Game/Screens/Select/Carousel/SetPanelBackground.cs
Normal file
72
osu.Game/Screens/Select/Carousel/SetPanelBackground.cs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
// 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.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Colour;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Beatmaps.Drawables;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Select.Carousel
|
||||||
|
{
|
||||||
|
public class SetPanelBackground : BufferedContainer
|
||||||
|
{
|
||||||
|
public SetPanelBackground(WorkingBeatmap working)
|
||||||
|
{
|
||||||
|
CacheDrawnFrameBuffer = true;
|
||||||
|
RedrawOnScale = false;
|
||||||
|
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new BeatmapBackgroundSprite(working)
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
FillMode = FillMode.Fill,
|
||||||
|
},
|
||||||
|
new FillFlowContainer
|
||||||
|
{
|
||||||
|
Depth = -1,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Direction = FillDirection.Horizontal,
|
||||||
|
// This makes the gradient not be perfectly horizontal, but diagonal at a ~40° angle
|
||||||
|
Shear = new Vector2(0.8f, 0),
|
||||||
|
Alpha = 0.5f,
|
||||||
|
Children = new[]
|
||||||
|
{
|
||||||
|
// The left half with no gradient applied
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Colour = Color4.Black,
|
||||||
|
Width = 0.4f,
|
||||||
|
},
|
||||||
|
// Piecewise-linear gradient with 3 segments to make it appear smoother
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Colour = ColourInfo.GradientHorizontal(Color4.Black, new Color4(0f, 0f, 0f, 0.9f)),
|
||||||
|
Width = 0.05f,
|
||||||
|
},
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.9f), new Color4(0f, 0f, 0f, 0.1f)),
|
||||||
|
Width = 0.2f,
|
||||||
|
},
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.1f), new Color4(0, 0, 0, 0)),
|
||||||
|
Width = 0.05f,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
93
osu.Game/Screens/Select/Carousel/SetPanelContent.cs
Normal file
93
osu.Game/Screens/Select/Carousel/SetPanelContent.cs
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
// 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 System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
|
using osu.Game.Beatmaps.Drawables;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Select.Carousel
|
||||||
|
{
|
||||||
|
public class SetPanelContent : CompositeDrawable
|
||||||
|
{
|
||||||
|
private readonly CarouselBeatmapSet carouselSet;
|
||||||
|
|
||||||
|
public SetPanelContent(CarouselBeatmapSet carouselSet)
|
||||||
|
{
|
||||||
|
this.carouselSet = carouselSet;
|
||||||
|
|
||||||
|
// required to ensure we load as soon as any part of the panel comes on screen
|
||||||
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
var beatmapSet = carouselSet.BeatmapSet;
|
||||||
|
|
||||||
|
InternalChild = new FillFlowContainer
|
||||||
|
{
|
||||||
|
// required to ensure we load as soon as any part of the panel comes on screen
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Direction = FillDirection.Vertical,
|
||||||
|
Padding = new MarginPadding { Top = 5, Left = 18, Right = 10, Bottom = 10 },
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new OsuSpriteText
|
||||||
|
{
|
||||||
|
Text = new LocalisedString((beatmapSet.Metadata.TitleUnicode, beatmapSet.Metadata.Title)),
|
||||||
|
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true),
|
||||||
|
Shadow = true,
|
||||||
|
},
|
||||||
|
new OsuSpriteText
|
||||||
|
{
|
||||||
|
Text = new LocalisedString((beatmapSet.Metadata.ArtistUnicode, beatmapSet.Metadata.Artist)),
|
||||||
|
Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true),
|
||||||
|
Shadow = true,
|
||||||
|
},
|
||||||
|
new FillFlowContainer
|
||||||
|
{
|
||||||
|
Direction = FillDirection.Horizontal,
|
||||||
|
AutoSizeAxes = Axes.Both,
|
||||||
|
Margin = new MarginPadding { Top = 5 },
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new BeatmapSetOnlineStatusPill
|
||||||
|
{
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Margin = new MarginPadding { Right = 5 },
|
||||||
|
TextSize = 11,
|
||||||
|
TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 },
|
||||||
|
Status = beatmapSet.Status
|
||||||
|
},
|
||||||
|
new FillFlowContainer<DifficultyIcon>
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.Both,
|
||||||
|
Spacing = new Vector2(3),
|
||||||
|
ChildrenEnumerable = getDifficultyIcons(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private const int maximum_difficulty_icons = 18;
|
||||||
|
|
||||||
|
private IEnumerable<DifficultyIcon> getDifficultyIcons()
|
||||||
|
{
|
||||||
|
var beatmaps = carouselSet.Beatmaps.ToList();
|
||||||
|
|
||||||
|
return beatmaps.Count > maximum_difficulty_icons
|
||||||
|
? (IEnumerable<DifficultyIcon>)beatmaps.GroupBy(b => b.Beatmap.Ruleset).Select(group => new FilterableGroupedDifficultyIcon(group.ToList(), group.Key))
|
||||||
|
: beatmaps.Select(b => new FilterableDifficultyIcon(b));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user