1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-24 07:23:26 +08:00
osu-lazer/osu.Game/Screens/SelectV2/BeatmapCarousel.cs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

260 lines
8.7 KiB
C#
Raw Normal View History

// 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;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Pooling;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Select;
namespace osu.Game.Screens.SelectV2
{
[Cached]
public partial class BeatmapCarousel : Carousel<BeatmapInfo>
{
private IBindableList<BeatmapSetInfo> detachedBeatmaps = null!;
private readonly LoadingLayer loading;
2025-01-23 15:11:02 +08:00
private readonly BeatmapCarouselFilterGrouping grouping;
public BeatmapCarousel()
{
DebounceDelay = 100;
DistanceOffscreenToPreload = 100;
Filters = new ICarouselFilter[]
{
new BeatmapCarouselFilterSorting(() => Criteria),
2025-01-23 15:11:02 +08:00
grouping = new BeatmapCarouselFilterGrouping(() => Criteria),
};
AddInternal(loading = new LoadingLayer(dimBackground: true));
}
[BackgroundDependencyLoader]
private void load(BeatmapStore beatmapStore, CancellationToken? cancellationToken)
2025-01-23 22:53:09 +08:00
{
setupPools();
setupBeatmaps(beatmapStore, cancellationToken);
}
#region Beatmap source hookup
private void setupBeatmaps(BeatmapStore beatmapStore, CancellationToken? cancellationToken)
{
detachedBeatmaps = beatmapStore.GetBeatmapSets(cancellationToken);
detachedBeatmaps.BindCollectionChanged(beatmapSetsChanged, true);
}
2025-01-23 22:53:09 +08:00
private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed)
{
// TODO: moving management of BeatmapInfo tracking to BeatmapStore might be something we want to consider.
// right now we are managing this locally which is a bit of added overhead.
IEnumerable<BeatmapSetInfo>? newBeatmapSets = changed.NewItems?.Cast<BeatmapSetInfo>();
IEnumerable<BeatmapSetInfo>? beatmapSetInfos = changed.OldItems?.Cast<BeatmapSetInfo>();
switch (changed.Action)
{
case NotifyCollectionChangedAction.Add:
Items.AddRange(newBeatmapSets!.SelectMany(s => s.Beatmaps));
break;
case NotifyCollectionChangedAction.Remove:
foreach (var set in beatmapSetInfos!)
{
foreach (var beatmap in set.Beatmaps)
Items.RemoveAll(i => i is BeatmapInfo bi && beatmap.Equals(bi));
}
break;
case NotifyCollectionChangedAction.Move:
case NotifyCollectionChangedAction.Replace:
throw new NotImplementedException();
case NotifyCollectionChangedAction.Reset:
Items.Clear();
break;
}
}
#endregion
#region Selection handling
private GroupDefinition? lastSelectedGroup;
private BeatmapInfo? lastSelectedBeatmap;
protected override bool HandleItemSelected(object? model)
2025-01-23 15:11:02 +08:00
{
base.HandleItemSelected(model);
switch (model)
2025-01-23 15:11:02 +08:00
{
case GroupDefinition group:
// Special case collapsing an open group.
if (lastSelectedGroup == group)
{
setExpansionStateOfGroup(lastSelectedGroup, false);
lastSelectedGroup = null;
return false;
}
setExpandedGroup(group);
return false;
2025-01-23 15:11:02 +08:00
case BeatmapSetInfo setInfo:
// Selecting a set isn't valid let's re-select the first difficulty.
CurrentSelection = setInfo.Beatmaps.First();
return false;
case BeatmapInfo beatmapInfo:
2025-02-05 17:59:29 +08:00
// Find any containing group. There should never be too many groups so iterating is efficient enough.
GroupDefinition? containingGroup = grouping.GroupItems.SingleOrDefault(kvp => kvp.Value.Any(i => ReferenceEquals(i.Model, beatmapInfo))).Key;
2025-02-05 17:59:29 +08:00
if (containingGroup != null)
setExpandedGroup(containingGroup);
setExpandedSet(beatmapInfo);
return true;
}
return true;
2025-01-23 22:58:51 +08:00
}
2025-01-23 15:11:02 +08:00
protected override bool CheckValidForGroupSelection(CarouselItem item)
{
switch (item.Model)
{
case BeatmapSetInfo:
return true;
case BeatmapInfo:
return Criteria.SplitOutDifficulties;
case GroupDefinition:
return false;
default:
throw new ArgumentException($"Unsupported model type {item.Model}");
}
}
private void setExpandedGroup(GroupDefinition group)
{
if (lastSelectedGroup != null)
setExpansionStateOfGroup(lastSelectedGroup, false);
lastSelectedGroup = group;
setExpansionStateOfGroup(group, true);
}
private void setExpansionStateOfGroup(GroupDefinition group, bool expanded)
2025-01-23 22:58:51 +08:00
{
if (grouping.GroupItems.TryGetValue(group, out var items))
{
2025-02-05 17:59:29 +08:00
// First pass ignoring set groupings.
foreach (var i in items)
{
if (i.Model is GroupDefinition)
i.IsExpanded = expanded;
else
i.IsVisible = expanded;
}
2025-02-05 17:59:29 +08:00
// Second pass to hide set children when not meant to be displayed.
if (expanded)
{
foreach (var i in items)
{
if (i.Model is BeatmapSetInfo set)
setExpansionStateOfSetItems(set, i.IsExpanded);
}
}
}
2025-01-23 22:58:51 +08:00
}
2025-01-23 15:11:02 +08:00
private void setExpandedSet(BeatmapInfo beatmapInfo)
{
if (lastSelectedBeatmap != null)
setExpansionStateOfSetItems(lastSelectedBeatmap.BeatmapSet!, false);
lastSelectedBeatmap = beatmapInfo;
setExpansionStateOfSetItems(beatmapInfo.BeatmapSet!, true);
}
private void setExpansionStateOfSetItems(BeatmapSetInfo set, bool expanded)
2025-01-23 22:58:51 +08:00
{
if (grouping.SetItems.TryGetValue(set, out var items))
2025-01-23 15:11:02 +08:00
{
foreach (var i in items)
{
if (i.Model is BeatmapSetInfo)
i.IsExpanded = expanded;
else
i.IsVisible = expanded;
}
2025-01-23 15:11:02 +08:00
}
}
2025-01-23 22:53:09 +08:00
#endregion
2025-01-23 22:53:09 +08:00
#region Filtering
public FilterCriteria Criteria { get; private set; } = new FilterCriteria();
public void Filter(FilterCriteria criteria)
{
Criteria = criteria;
loading.Show();
FilterAsync().ContinueWith(_ => Schedule(() => loading.Hide()));
}
2025-01-23 22:53:09 +08:00
#endregion
#region Drawable pooling
private readonly DrawablePool<BeatmapPanel> beatmapPanelPool = new DrawablePool<BeatmapPanel>(100);
private readonly DrawablePool<BeatmapSetPanel> setPanelPool = new DrawablePool<BeatmapSetPanel>(100);
private readonly DrawablePool<GroupPanel> groupPanelPool = new DrawablePool<GroupPanel>(100);
2025-01-23 22:53:09 +08:00
private void setupPools()
{
AddInternal(groupPanelPool);
AddInternal(beatmapPanelPool);
AddInternal(setPanelPool);
2025-01-23 22:53:09 +08:00
}
protected override Drawable GetDrawableForDisplay(CarouselItem item)
{
switch (item.Model)
{
case GroupDefinition:
return groupPanelPool.Get();
case BeatmapInfo:
// TODO: if beatmap is a group selection target, it needs to be a different drawable
// with more information attached.
return beatmapPanelPool.Get();
case BeatmapSetInfo:
return setPanelPool.Get();
}
throw new InvalidOperationException();
}
2025-01-23 22:53:09 +08:00
#endregion
}
public record GroupDefinition(string Title);
}