diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs index 6d54e13b6f..1d7d6041ae 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs @@ -17,10 +17,13 @@ using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; 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; using osuTK.Graphics; +using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel; namespace osu.Game.Tests.Visual.SongSelect { @@ -123,6 +126,11 @@ namespace osu.Game.Tests.Visual.SongSelect }, }; }); + + AddStep("sort by title", () => + { + carousel.Filter(new FilterCriteria { Sort = SortMode.Title }); + }); } [Test] @@ -139,6 +147,26 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("remove all beatmaps", () => beatmapSets.Clear()); } + [Test] + public void TestSorting() + { + AddStep("add 10 beatmaps", () => + { + for (int i = 0; i < 10; i++) + beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))); + }); + + AddStep("sort by difficulty", () => + { + carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }); + }); + + AddStep("sort by artist", () => + { + carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }); + }); + } + [Test] public void TestScrollPositionVelocityMaintained() { diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 3c431a6003..582933bbaf 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -31,8 +31,8 @@ namespace osu.Game.Screens.SelectV2 Filters = new ICarouselFilter[] { - new BeatmapCarouselFilterSorting(), - new BeatmapCarouselFilterGrouping(), + new BeatmapCarouselFilterSorting(() => Criteria), + new BeatmapCarouselFilterGrouping(() => Criteria), }; AddInternal(carouselPanelPool); diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index ee4b9ddb69..6cdd15d301 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -1,19 +1,36 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Game.Beatmaps; +using osu.Game.Screens.Select; namespace osu.Game.Screens.SelectV2 { public class BeatmapCarouselFilterGrouping : ICarouselFilter { + private readonly Func getCriteria; + + public BeatmapCarouselFilterGrouping(Func getCriteria) + { + this.getCriteria = getCriteria; + } + public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => { - // TODO: perform grouping based on FilterCriteria + var criteria = getCriteria(); + + if (criteria.SplitOutDifficulties) + { + foreach (var item in items) + ((BeatmapCarouselItem)item).HasGroupHeader = false; + + return items; + } CarouselItem? lastItem = null; @@ -23,15 +40,18 @@ namespace osu.Game.Screens.SelectV2 { cancellationToken.ThrowIfCancellationRequested(); - if (item.Model is BeatmapInfo b1) + if (item.Model is BeatmapInfo b) { // Add set header - if (lastItem == null || (lastItem.Model is BeatmapInfo b2 && b2.BeatmapSet!.OnlineID != b1.BeatmapSet!.OnlineID)) - newItems.Add(new BeatmapCarouselItem(b1.BeatmapSet!)); + if (lastItem == null || (lastItem.Model is BeatmapInfo b2 && b2.BeatmapSet!.OnlineID != b.BeatmapSet!.OnlineID)) + newItems.Add(new BeatmapCarouselItem(b.BeatmapSet!) { IsGroupHeader = true }); } newItems.Add(item); lastItem = item; + + var beatmapCarouselItem = (BeatmapCarouselItem)item; + beatmapCarouselItem.HasGroupHeader = true; } return newItems; diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs index a2fd774cf0..df41aa3e86 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs @@ -1,22 +1,59 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Game.Beatmaps; +using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; +using osu.Game.Utils; namespace osu.Game.Screens.SelectV2 { public class BeatmapCarouselFilterSorting : ICarouselFilter { + private readonly Func getCriteria; + + public BeatmapCarouselFilterSorting(Func getCriteria) + { + this.getCriteria = getCriteria; + } + public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => { + var criteria = getCriteria(); + return items.OrderDescending(Comparer.Create((a, b) => { + int comparison = 0; + if (a.Model is BeatmapInfo ab && b.Model is BeatmapInfo bb) - return ab.OnlineID.CompareTo(bb.OnlineID); + { + switch (criteria.Sort) + { + case SortMode.Artist: + comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(ab.BeatmapSet!.Metadata.Artist, bb.BeatmapSet!.Metadata.Artist); + if (comparison == 0) + goto case SortMode.Title; + break; + + case SortMode.Difficulty: + comparison = ab.StarRating.CompareTo(bb.StarRating); + break; + + case SortMode.Title: + comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(ab.BeatmapSet!.Metadata.Title, bb.BeatmapSet!.Metadata.Title); + break; + + default: + throw new ArgumentOutOfRangeException(); + } + } + + if (comparison != 0) return comparison; if (a is BeatmapCarouselItem aItem && b is BeatmapCarouselItem bItem) return aItem.ID.CompareTo(bItem.ID); diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs index adb5a19875..dd7aae3db9 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs @@ -11,7 +11,19 @@ namespace osu.Game.Screens.SelectV2 { public readonly Guid ID; - public override float DrawHeight => Model is BeatmapInfo ? 40 : 80; + /// + /// Whether this item has a header providing extra information for it. + /// When displaying items which don't have header, we should make sure enough information is included inline. + /// + public bool HasGroupHeader { get; set; } + + /// + /// Whether this item is a group header. + /// Group headers are generally larger in display. Setting this will account for the size difference. + /// + public bool IsGroupHeader { get; set; } + + public override float DrawHeight => IsGroupHeader ? 80 : 40; public BeatmapCarouselItem(object model) : base(model) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 02e87c7704..f0289d634d 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -29,7 +29,7 @@ namespace osu.Game.Screens.SelectV2 /// /// A collection of filters which should be run each time a is executed. /// - public IEnumerable Filters { get; init; } = Enumerable.Empty(); + protected IEnumerable Filters { get; init; } = Enumerable.Empty(); /// /// Height of the area above the carousel that should be treated as visible due to transparency of elements in front of it.