Skip to content

Commit

Permalink
Merge pull request ppy#31801 from peppy/carousel-v2-artist-grouping
Browse files Browse the repository at this point in the history
Add support for grouping by artist to beatmap carousel v2
  • Loading branch information
bdach authored Feb 6, 2025
2 parents 736fe78 + bff686f commit 91bc23e
Show file tree
Hide file tree
Showing 12 changed files with 496 additions and 221 deletions.
48 changes: 35 additions & 13 deletions osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
Expand All @@ -16,7 +17,6 @@
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
Expand Down Expand Up @@ -53,16 +53,6 @@ protected BeatmapCarouselV2TestScene()
Scheduler.AddDelayed(updateStats, 100, true);
}

[SetUpSteps]
public virtual void SetUpSteps()
{
RemoveAllBeatmaps();

CreateCarousel();

SortBy(new FilterCriteria { Sort = SortMode.Title });
}

protected void CreateCarousel()
{
AddStep("create components", () =>
Expand Down Expand Up @@ -146,6 +136,9 @@ protected void CreateCarousel()
protected void CheckNoSelection() => AddAssert("has no selection", () => Carousel.CurrentSelection, () => Is.Null);
protected void CheckHasSelection() => AddAssert("has selection", () => Carousel.CurrentSelection, () => Is.Not.Null);

protected BeatmapPanel? GetSelectedPanel() => Carousel.ChildrenOfType<BeatmapPanel>().SingleOrDefault(p => p.Selected.Value);
protected GroupPanel? GetKeyboardSelectedPanel() => Carousel.ChildrenOfType<GroupPanel>().SingleOrDefault(p => p.KeyboardSelected.Value);

protected void WaitForGroupSelection(int group, int panel)
{
AddUntilStep($"selected is group{group} panel{panel}", () =>
Expand Down Expand Up @@ -190,12 +183,41 @@ protected void ClickVisiblePanel<T>(int index)
/// </summary>
/// <param name="count">The count of beatmap sets to add.</param>
/// <param name="fixedDifficultiesPerSet">If not null, the number of difficulties per set. If null, randomised difficulty count will be used.</param>
protected void AddBeatmaps(int count, int? fixedDifficultiesPerSet = null) => AddStep($"add {count} beatmaps", () =>
/// <param name="randomMetadata">Whether to randomise the metadata to make groupings more uniform.</param>
protected void AddBeatmaps(int count, int? fixedDifficultiesPerSet = null, bool randomMetadata = false) => AddStep($"add {count} beatmaps{(randomMetadata ? " with random data" : "")}", () =>
{
for (int i = 0; i < count; i++)
BeatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4)));
{
var beatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4));

if (randomMetadata)
{
char randomCharacter = getRandomCharacter();

var metadata = new BeatmapMetadata
{
// Create random metadata, then we can check if sorting works based on these
Artist = $"{randomCharacter}ome Artist " + RNG.Next(0, 9),
Title = $"{randomCharacter}ome Song (set id {beatmapSetInfo.OnlineID:000}) {Guid.NewGuid()}",
Author = { Username = $"{randomCharacter}ome Guy " + RNG.Next(0, 9) },
};

foreach (var beatmap in beatmapSetInfo.Beatmaps)
beatmap.Metadata = metadata.DeepClone();
}

BeatmapSets.Add(beatmapSetInfo);
}
});

private static long randomCharPointer;

private static char getRandomCharacter()
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz*";
return chars[(int)((randomCharPointer++ / 2) % chars.Length)];
}

protected void RemoveAllBeatmaps() => AddStep("clear all beatmaps", () => BeatmapSets.Clear());

protected void RemoveFirstBeatmap() =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,100 +2,65 @@
// See the LICENCE file in the repository root for full licence text.

using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Resources;

namespace osu.Game.Tests.Visual.SongSelect
{
/// <summary>
/// Currently covers adding and removing of items and scrolling.
/// If we add more tests here, these two categories can likely be split out into separate scenes.
/// Covers common steps which can be used for manual testing.
/// </summary>
[TestFixture]
public partial class TestSceneBeatmapCarouselV2Basics : BeatmapCarouselV2TestScene
public partial class TestSceneBeatmapCarouselV2 : BeatmapCarouselV2TestScene
{
[Test]
[Explicit]
public void TestBasics()
{
AddBeatmaps(1);
AddBeatmaps(10);
RemoveFirstBeatmap();
CreateCarousel();
RemoveAllBeatmaps();
}

[Test]
public void TestOffScreenLoading()
{
AddStep("disable masking", () => Scroll.Masking = false);
AddStep("enable masking", () => Scroll.Masking = true);
}

[Test]
public void TestAddRemoveOneByOne()
{
AddRepeatStep("add beatmaps", () => BeatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))), 20);
AddRepeatStep("remove beatmaps", () => BeatmapSets.RemoveAt(RNG.Next(0, BeatmapSets.Count)), 20);
AddBeatmaps(10, randomMetadata: true);
AddBeatmaps(10);
AddBeatmaps(1);
}

[Test]
[Explicit]
public void TestSorting()
{
AddBeatmaps(10);
SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty });
SortBy(new FilterCriteria { Sort = SortMode.Artist });
SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty });
SortBy(new FilterCriteria { Group = GroupMode.Artist, Sort = SortMode.Artist });
}

[Test]
public void TestScrollPositionMaintainedOnAddSecondSelected()
[Explicit]
public void TestRemovals()
{
Quad positionBefore = default;

AddBeatmaps(10);
WaitForDrawablePanels();

AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2));
AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType<BeatmapPanel>().Single(p => p.Selected.Value)));

WaitForScrolling();

AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<BeatmapPanel>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);

RemoveFirstBeatmap();
WaitForSorting();

AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<BeatmapPanel>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
() => Is.EqualTo(positionBefore));
RemoveAllBeatmaps();
}

[Test]
public void TestScrollPositionMaintainedOnAddLastSelected()
[Explicit]
public void TestAddRemoveRepeatedOps()
{
Quad positionBefore = default;

AddBeatmaps(10);
WaitForDrawablePanels();

AddStep("scroll to last item", () => Scroll.ScrollToEnd(false));

AddStep("select last beatmap", () => Carousel.CurrentSelection = BeatmapSets.Last());

WaitForScrolling();

AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<BeatmapPanel>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
AddRepeatStep("add beatmaps", () => BeatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))), 20);
AddRepeatStep("remove beatmaps", () => BeatmapSets.RemoveAt(RNG.Next(0, BeatmapSets.Count)), 20);
}

RemoveFirstBeatmap();
WaitForSorting();
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<BeatmapPanel>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
() => Is.EqualTo(positionBefore));
[Test]
[Explicit]
public void TestMasking()
{
AddStep("disable masking", () => Scroll.Masking = false);
AddStep("enable masking", () => Scroll.Masking = true);
}

[Test]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;

namespace osu.Game.Tests.Visual.SongSelect
{
[TestFixture]
public partial class TestSceneBeatmapCarouselV2ArtistGrouping : BeatmapCarouselV2TestScene
{
[SetUpSteps]
public void SetUpSteps()
{
RemoveAllBeatmaps();
CreateCarousel();
SortBy(new FilterCriteria { Group = GroupMode.Artist, Sort = SortMode.Artist });

AddBeatmaps(10, 3, true);
WaitForDrawablePanels();
}

[Test]
public void TestOpenCloseGroupWithNoSelectionMouse()
{
AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
AddUntilStep("no sets visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
CheckNoSelection();

ClickVisiblePanel<GroupPanel>(0);

AddUntilStep("some sets visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
CheckNoSelection();

ClickVisiblePanel<GroupPanel>(0);

AddUntilStep("no sets visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
CheckNoSelection();
}

[Test]
public void TestOpenCloseGroupWithNoSelectionKeyboard()
{
AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
AddUntilStep("no sets visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
CheckNoSelection();

SelectNextPanel();
Select();

AddUntilStep("some sets visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
AddAssert("keyboard selected is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
CheckNoSelection();

Select();

AddUntilStep("no sets visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
AddAssert("keyboard selected is collapsed", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False);
CheckNoSelection();
}

[Test]
public void TestCarouselRemembersSelection()
{
SelectNextGroup();

object? selection = null;

AddStep("store drawable selection", () => selection = GetSelectedPanel()?.Item?.Model);

CheckHasSelection();
AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null);
AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));

RemoveAllBeatmaps();
AddUntilStep("no drawable selection", GetSelectedPanel, () => Is.Null);

AddBeatmaps(10);
WaitForDrawablePanels();

CheckHasSelection();
AddAssert("no drawable selection", GetSelectedPanel, () => Is.Null);

AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!));

AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection));
AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True);

ClickVisiblePanel<GroupPanel>(0);
AddUntilStep("carousel item not visible", GetSelectedPanel, () => Is.Null);

ClickVisiblePanel<GroupPanel>(0);
AddUntilStep("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True);
}

[Test]
public void TestGroupSelectionOnHeader()
{
SelectNextGroup();
WaitForGroupSelection(0, 1);

SelectPrevPanel();
SelectPrevPanel();

AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);

SelectPrevGroup();

WaitForGroupSelection(0, 1);
AddAssert("keyboard selected panel is contracted", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False);

SelectPrevGroup();

WaitForGroupSelection(0, 1);
AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
}

[Test]
public void TestKeyboardSelection()
{
SelectNextPanel();
SelectNextPanel();
SelectNextPanel();
SelectNextPanel();
CheckNoSelection();

// open first group
Select();
CheckNoSelection();
AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));

SelectNextPanel();
Select();
WaitForGroupSelection(3, 1);

SelectNextGroup();
WaitForGroupSelection(3, 5);

SelectNextGroup();
WaitForGroupSelection(4, 1);

SelectPrevGroup();
WaitForGroupSelection(3, 5);

SelectNextGroup();
WaitForGroupSelection(4, 1);

SelectNextGroup();
WaitForGroupSelection(4, 5);

SelectNextGroup();
WaitForGroupSelection(0, 1);

SelectNextPanel();
SelectNextPanel();
SelectNextPanel();
SelectNextPanel();

SelectNextGroup();
WaitForGroupSelection(0, 1);

SelectNextPanel();
SelectNextGroup();
WaitForGroupSelection(1, 1);
}
}
}
Loading

0 comments on commit 91bc23e

Please sign in to comment.