1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-31 14:25:10 +08:00

Merge pull request #31632 from peppy/beatmap-carousel-v2-cleanups

Initial carousel additions in preparation for selection logic
This commit is contained in:
Bartłomiej Dach 2025-01-23 10:43:12 +01:00 committed by GitHub
commit beaadfa1fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 83 additions and 58 deletions

View File

@ -16,6 +16,7 @@ using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter; using osu.Game.Screens.Select.Filter;
@ -117,12 +118,11 @@ namespace osu.Game.Tests.Visual.SongSelect
} }
} }
}, },
stats = new OsuTextFlowContainer(cp => cp.Font = FrameworkFont.Regular.With()) stats = new OsuTextFlowContainer
{ {
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding(10), Padding = new MarginPadding(10),
TextAnchor = Anchor.CentreLeft, TextAnchor = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
}, },
}; };
}); });
@ -258,16 +258,29 @@ namespace osu.Game.Tests.Visual.SongSelect
if (carousel.IsNull()) if (carousel.IsNull())
return; return;
stats.Text = $""" stats.Clear();
store createHeader("beatmap store");
sets: {beatmapSets.Count} stats.AddParagraph($"""
beatmaps: {beatmapCount} sets: {beatmapSets.Count}
carousel: beatmaps: {beatmapCount}
sorting: {carousel.IsFiltering} """);
tracked: {carousel.ItemsTracked} createHeader("carousel");
displayable: {carousel.DisplayableItems} stats.AddParagraph($"""
displayed: {carousel.VisibleItems} sorting: {carousel.IsFiltering}
"""; tracked: {carousel.ItemsTracked}
displayable: {carousel.DisplayableItems}
displayed: {carousel.VisibleItems}
selected: {carousel.CurrentSelection}
""");
void createHeader(string text)
{
stats.AddParagraph(string.Empty);
stats.AddParagraph(text, cp =>
{
cp.Font = cp.Font.With(size: 18, weight: FontWeight.Bold);
});
}
} }
} }
} }

View File

@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -14,7 +13,6 @@ using osu.Framework.Graphics.Pooling;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
namespace osu.Game.Screens.SelectV2 namespace osu.Game.Screens.SelectV2
@ -93,14 +91,8 @@ namespace osu.Game.Screens.SelectV2
public void Filter(FilterCriteria criteria) public void Filter(FilterCriteria criteria)
{ {
Criteria = criteria; Criteria = criteria;
FilterAsync().FireAndForget();
}
protected override async Task FilterAsync()
{
loading.Show(); loading.Show();
await base.FilterAsync().ConfigureAwait(true); FilterAsync().ContinueWith(_ => Schedule(() => loading.Hide()));
loading.Hide();
} }
} }
} }

View File

@ -30,10 +30,7 @@ namespace osu.Game.Screens.SelectV2
/// </summary> /// </summary>
public abstract partial class Carousel<T> : CompositeDrawable public abstract partial class Carousel<T> : CompositeDrawable
{ {
/// <summary> #region Properties and methods for external usage
/// A collection of filters which should be run each time a <see cref="FilterAsync"/> is executed.
/// </summary>
protected IEnumerable<ICarouselFilter> Filters { get; init; } = Enumerable.Empty<ICarouselFilter>();
/// <summary> /// <summary>
/// Height of the area above the carousel that should be treated as visible due to transparency of elements in front of it. /// Height of the area above the carousel that should be treated as visible due to transparency of elements in front of it.
@ -75,22 +72,13 @@ namespace osu.Game.Screens.SelectV2
/// <summary> /// <summary>
/// The number of carousel items currently in rotation for display. /// The number of carousel items currently in rotation for display.
/// </summary> /// </summary>
public int DisplayableItems => displayedCarouselItems?.Count ?? 0; public int DisplayableItems => carouselItems?.Count ?? 0;
/// <summary> /// <summary>
/// The number of items currently actualised into drawables. /// The number of items currently actualised into drawables.
/// </summary> /// </summary>
public int VisibleItems => scroll.Panels.Count; public int VisibleItems => scroll.Panels.Count;
/// <summary>
/// All items which are to be considered for display in this carousel.
/// Mutating this list will automatically queue a <see cref="FilterAsync"/>.
/// </summary>
/// <remarks>
/// Note that an <see cref="ICarouselFilter"/> may add new items which are displayed but not tracked in this list.
/// </remarks>
protected readonly BindableList<T> Items = new BindableList<T>();
/// <summary> /// <summary>
/// The currently selected model. /// The currently selected model.
/// </summary> /// </summary>
@ -114,20 +102,31 @@ namespace osu.Game.Screens.SelectV2
} }
} }
private List<CarouselItem>? displayedCarouselItems; #endregion
private readonly CarouselScrollContainer scroll; #region Properties and methods concerning implementations
protected Carousel() /// <summary>
{ /// A collection of filters which should be run each time a <see cref="FilterAsync"/> is executed.
InternalChild = scroll = new CarouselScrollContainer /// </summary>
{ /// <remarks>
RelativeSizeAxes = Axes.Both, /// Implementations should add all required filters as part of their initialisation.
Masking = false, ///
}; /// Importantly, each filter is sequentially run in the order provided.
/// Each filter receives the output of the previous filter.
///
/// A filter may add, mutate or remove items.
/// </remarks>
protected IEnumerable<ICarouselFilter> Filters { get; init; } = Enumerable.Empty<ICarouselFilter>();
Items.BindCollectionChanged((_, _) => FilterAsync()); /// <summary>
} /// All items which are to be considered for display in this carousel.
/// Mutating this list will automatically queue a <see cref="FilterAsync"/>.
/// </summary>
/// <remarks>
/// Note that an <see cref="ICarouselFilter"/> may add new items which are displayed but not tracked in this list.
/// </remarks>
protected readonly BindableList<T> Items = new BindableList<T>();
/// <summary> /// <summary>
/// Queue an asynchronous filter operation. /// Queue an asynchronous filter operation.
@ -151,8 +150,29 @@ namespace osu.Game.Screens.SelectV2
/// <returns>A <see cref="CarouselItem"/> representing the model.</returns> /// <returns>A <see cref="CarouselItem"/> representing the model.</returns>
protected abstract CarouselItem CreateCarouselItemForModel(T model); protected abstract CarouselItem CreateCarouselItemForModel(T model);
#endregion
#region Initialisation
private readonly CarouselScrollContainer scroll;
protected Carousel()
{
InternalChild = scroll = new CarouselScrollContainer
{
RelativeSizeAxes = Axes.Both,
Masking = false,
};
Items.BindCollectionChanged((_, _) => FilterAsync());
}
#endregion
#region Filtering and display preparation #region Filtering and display preparation
private List<CarouselItem>? carouselItems;
private Task filterTask = Task.CompletedTask; private Task filterTask = Task.CompletedTask;
private CancellationTokenSource cancellationSource = new CancellationTokenSource(); private CancellationTokenSource cancellationSource = new CancellationTokenSource();
@ -202,7 +222,7 @@ namespace osu.Game.Screens.SelectV2
return; return;
log("Items ready for display"); log("Items ready for display");
displayedCarouselItems = items.ToList(); carouselItems = items.ToList();
displayedRange = null; displayedRange = null;
updateSelection(); updateSelection();
@ -233,9 +253,9 @@ namespace osu.Game.Screens.SelectV2
{ {
currentSelectionCarouselItem = null; currentSelectionCarouselItem = null;
if (displayedCarouselItems == null) return; if (carouselItems == null) return;
foreach (var item in displayedCarouselItems) foreach (var item in carouselItems)
{ {
bool isSelected = item.Model == currentSelection; bool isSelected = item.Model == currentSelection;
@ -286,7 +306,7 @@ namespace osu.Game.Screens.SelectV2
{ {
base.Update(); base.Update();
if (displayedCarouselItems == null) if (carouselItems == null)
return; return;
var range = getDisplayRange(); var range = getDisplayRange();
@ -336,15 +356,15 @@ namespace osu.Game.Screens.SelectV2
private DisplayRange getDisplayRange() private DisplayRange getDisplayRange()
{ {
Debug.Assert(displayedCarouselItems != null); Debug.Assert(carouselItems != null);
// Find index range of all items that should be on-screen // Find index range of all items that should be on-screen
carouselBoundsItem.CarouselYPosition = visibleUpperBound - DistanceOffscreenToPreload; carouselBoundsItem.CarouselYPosition = visibleUpperBound - DistanceOffscreenToPreload;
int firstIndex = displayedCarouselItems.BinarySearch(carouselBoundsItem); int firstIndex = carouselItems.BinarySearch(carouselBoundsItem);
if (firstIndex < 0) firstIndex = ~firstIndex; if (firstIndex < 0) firstIndex = ~firstIndex;
carouselBoundsItem.CarouselYPosition = visibleBottomBound + DistanceOffscreenToPreload; carouselBoundsItem.CarouselYPosition = visibleBottomBound + DistanceOffscreenToPreload;
int lastIndex = displayedCarouselItems.BinarySearch(carouselBoundsItem); int lastIndex = carouselItems.BinarySearch(carouselBoundsItem);
if (lastIndex < 0) lastIndex = ~lastIndex; if (lastIndex < 0) lastIndex = ~lastIndex;
firstIndex = Math.Max(0, firstIndex - 1); firstIndex = Math.Max(0, firstIndex - 1);
@ -355,11 +375,11 @@ namespace osu.Game.Screens.SelectV2
private void updateDisplayedRange(DisplayRange range) private void updateDisplayedRange(DisplayRange range)
{ {
Debug.Assert(displayedCarouselItems != null); Debug.Assert(carouselItems != null);
List<CarouselItem> toDisplay = range.Last - range.First == 0 List<CarouselItem> toDisplay = range.Last - range.First == 0
? new List<CarouselItem>() ? new List<CarouselItem>()
: displayedCarouselItems.GetRange(range.First, range.Last - range.First + 1); : carouselItems.GetRange(range.First, range.Last - range.First + 1);
// Iterate over all panels which are already displayed and figure which need to be displayed / removed. // Iterate over all panels which are already displayed and figure which need to be displayed / removed.
foreach (var panel in scroll.Panels) foreach (var panel in scroll.Panels)
@ -395,9 +415,9 @@ namespace osu.Game.Screens.SelectV2
// Update the total height of all items (to make the scroll container scrollable through the full height even though // Update the total height of all items (to make the scroll container scrollable through the full height even though
// most items are not displayed / loaded). // most items are not displayed / loaded).
if (displayedCarouselItems.Count > 0) if (carouselItems.Count > 0)
{ {
var lastItem = displayedCarouselItems[^1]; var lastItem = carouselItems[^1];
scroll.SetLayoutHeight((float)(lastItem.CarouselYPosition + lastItem.DrawHeight + visibleHalfHeight)); scroll.SetLayoutHeight((float)(lastItem.CarouselYPosition + lastItem.DrawHeight + visibleHalfHeight));
} }
else else