diff --git a/Directory.Build.props b/Directory.Build.props
index 3acb86ee0c..580e61dafb 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -3,6 +3,10 @@
12.0
enable
+
+ false
+
+ $(NoWarn);CA1416
$(MSBuildThisFileDirectory)app.manifest
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
index 046840a691..7aa2ecb06c 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
@@ -312,8 +312,8 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("set game volume to max", () => Game.Dependencies.Get().SetValue(FrameworkSetting.VolumeUniversal, 1d));
- AddStep("move to metadata wedge", () => InputManager.MoveMouseTo(
- songSelect.ChildrenOfType().Single()));
+ AddStep("move to details area", () => InputManager.MoveMouseTo(
+ songSelect.ChildrenOfType().Single()));
AddStep("scroll down", () => InputManager.ScrollVerticalBy(-1));
AddAssert("carousel didn't move", getCarouselScrollPosition, () => Is.EqualTo(scrollPosition));
diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs
index ca2c5d415e..7f34d7a901 100644
--- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs
+++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs
@@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
..beatmap4.Beatmaps
];
- var results = await runGrouping(GroupMode.NoGrouping, beatmapSets);
+ var results = await runGrouping(GroupMode.None, beatmapSets);
Assert.That(results.Select(r => r.Model).OfType(), Is.EquivalentTo(beatmapSets));
Assert.That(results.Select(r => r.Model).OfType(), Is.EquivalentTo(allBeatmaps));
assertTotal(results, beatmapSets.Count + allBeatmaps.Length);
diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs
index f9f7f3e89c..f58d879141 100644
--- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs
+++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs
@@ -149,6 +149,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
TextAnchor = Anchor.CentreLeft,
},
};
+
+ Carousel.Filter(new FilterCriteria());
});
// Prefer title sorting so that order of carousel panels match order of BeatmapSets bindable.
@@ -171,7 +173,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
{
AddStep(description, () =>
{
- var criteria = Carousel.Criteria;
+ var criteria = Carousel.Criteria ?? new FilterCriteria();
apply?.Invoke(criteria);
Carousel.Filter(criteria);
});
@@ -191,6 +193,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2
protected void CheckNoSelection() => AddAssert("has no selection", () => Carousel.CurrentSelection, () => Is.Null);
protected void CheckHasSelection() => AddAssert("has selection", () => Carousel.CurrentSelection, () => Is.Not.Null);
+ protected void CheckRequestPresentCount(int expected) =>
+ AddAssert($"check present count is {expected}", () => Carousel.RequestPresentBeatmapCount, () => Is.EqualTo(expected));
+
+ protected void CheckActivationCount(int expected) =>
+ AddAssert($"check activation count is {expected}", () => Carousel.ActivationCount, () => Is.EqualTo(expected));
+
protected void CheckDisplayedBeatmapsCount(int expected)
{
AddAssert($"{expected} diffs displayed", () => Carousel.MatchedBeatmapsCount, () => Is.EqualTo(expected));
@@ -356,7 +364,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
""");
createHeader("carousel");
stats.AddParagraph($"""
- sorting: {Carousel.IsFiltering}
+ filtering: {Carousel.IsFiltering} (total {Carousel.FilterCount} times)
tracked: {Carousel.ItemsTracked}
displayable: {Carousel.DisplayableItems}
displayed: {Carousel.VisibleItems}
@@ -375,6 +383,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2
public partial class TestBeatmapCarousel : BeatmapCarousel
{
+ public int ActivationCount { get; private set; }
+ public int RequestPresentBeatmapCount { get; private set; }
+
public int FilterDelay = 0;
public IEnumerable PostFilterBeatmaps = null!;
@@ -385,12 +396,23 @@ namespace osu.Game.Tests.Visual.SongSelectV2
public new BeatmapSetInfo? ExpandedBeatmapSet => base.ExpandedBeatmapSet;
public new GroupDefinition? ExpandedGroup => base.ExpandedGroup;
+ public TestBeatmapCarousel()
+ {
+ RequestPresentBeatmap = _ => RequestPresentBeatmapCount++;
+ }
+
+ protected override void HandleItemActivated(CarouselItem item)
+ {
+ ActivationCount++;
+ base.HandleItemActivated(item);
+ }
+
protected override async Task> FilterAsync(bool clearExistingPanels = false)
{
- var items = await base.FilterAsync(clearExistingPanels);
+ var items = await base.FilterAsync(clearExistingPanels).ConfigureAwait(true);
if (FilterDelay != 0)
- await Task.Delay(FilterDelay);
+ await Task.Delay(FilterDelay).ConfigureAwait(true);
PostFilterBeatmaps = items.Select(i => i.Model).OfType();
return items;
diff --git a/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs
index c7c56f30f4..75996fe158 100644
--- a/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs
+++ b/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs
@@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
SelectedMods.SetDefault();
Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Title);
- Config.SetValue(OsuSetting.SongSelectGroupMode, GroupMode.NoGrouping);
+ Config.SetValue(OsuSetting.SongSelectGroupMode, GroupMode.None);
SongSelect = null!;
});
diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs
index 56351eed97..ce671c7e7f 100644
--- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs
+++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs
@@ -6,9 +6,12 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Screens.Select.Filter;
+using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.SongSelectV2
@@ -35,7 +38,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
[Explicit]
public void TestSorting()
{
- SortAndGroupBy(SortMode.Artist, GroupMode.NoGrouping);
+ SortAndGroupBy(SortMode.Artist, GroupMode.None);
SortAndGroupBy(SortMode.Difficulty, GroupMode.Difficulty);
SortAndGroupBy(SortMode.Artist, GroupMode.Artist);
}
@@ -53,7 +56,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
public void TestLoadingDisplay()
{
AddStep("induce slow filtering", () => Carousel.FilterDelay = 2000);
- SortAndGroupBy(SortMode.Artist, GroupMode.NoGrouping);
+ SortAndGroupBy(SortMode.Artist, GroupMode.None);
}
[Test]
@@ -92,6 +95,25 @@ namespace osu.Game.Tests.Visual.SongSelectV2
});
}
+ [Test]
+ public void TestHighChurnUpdatesStillShowsPanels()
+ {
+ ScheduledDelegate updateTask = null!;
+
+ AddBeatmaps(1, 1);
+
+ AddStep("start constantly updating beatmap in background", () =>
+ {
+ updateTask = Scheduler.AddDelayed(() => { BeatmapSets.ReplaceRange(0, 1, [BeatmapSets.First()]); }, 1, true);
+ });
+
+ CreateCarousel();
+
+ AddUntilStep("panels loaded", () => Carousel.ChildrenOfType(), () => Is.Not.Empty);
+
+ AddStep("end task", () => updateTask.Cancel());
+ }
+
[Test]
[Explicit]
public void TestPerformanceWithManyBeatmaps()
@@ -116,5 +138,16 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddStep("add all beatmaps", () => BeatmapSets.AddRange(generated));
}
+
+ [Test]
+ public void TestSingleItemDisplayed()
+ {
+ CreateCarousel();
+ RemoveAllBeatmaps();
+
+ SortAndGroupBy(SortMode.Difficulty, GroupMode.None);
+ AddBeatmaps(1, fixedDifficultiesPerSet: 1);
+ AddUntilStep("single item is shown", () => this.ChildrenOfType().Count(), () => Is.EqualTo(1));
+ }
}
}
diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs
index e72a373d63..ea9d396316 100644
--- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs
+++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs
@@ -190,6 +190,58 @@ namespace osu.Game.Tests.Visual.SongSelectV2
WaitForSelection(4, 0);
}
+ [Test]
+ public void TestSingleItemTraversal()
+ {
+ CheckNoSelection();
+ AddBeatmaps(1, 3);
+
+ WaitForSelection(0, 0);
+ CheckActivationCount(0);
+
+ SelectNextGroup();
+ WaitForSelection(0, 0);
+
+ // In the case of a grouped beatmap set, the header gets activated and re-selects the recommended difficulty.
+ // This is probably fine.
+ CheckActivationCount(1);
+ // We don't want it to request present though, which would start gameplay.
+ CheckRequestPresentCount(0);
+
+ SelectPrevGroup();
+ WaitForSelection(0, 0);
+
+ CheckActivationCount(1);
+ CheckRequestPresentCount(0);
+ }
+
+ [Test]
+ public void TestSingleItemTraversal_DifficultySplit()
+ {
+ SortBy(SortMode.Difficulty);
+
+ CheckNoSelection();
+ AddBeatmaps(1, 1);
+
+ WaitForSelection(0, 0);
+ CheckActivationCount(0);
+
+ SelectNextGroup();
+ WaitForSelection(0, 0);
+
+ // In the case of a grouped beatmap set, the header gets activated and re-selects the recommended difficulty.
+ // This is probably fine.
+ CheckActivationCount(0);
+ // We don't want it to request present though, which would start gameplay.
+ CheckRequestPresentCount(0);
+
+ SelectPrevGroup();
+ WaitForSelection(0, 0);
+
+ CheckActivationCount(0);
+ CheckRequestPresentCount(0);
+ }
+
[Test]
public void TestEmptyTraversal()
{
@@ -243,7 +295,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddBeatmaps(2, 3);
WaitForDrawablePanels();
- SortAndGroupBy(SortMode.Difficulty, GroupMode.NoGrouping);
+ SortAndGroupBy(SortMode.Difficulty, GroupMode.None);
WaitForFiltering();
AddUntilStep("standalone panels displayed", () => GetVisiblePanels().Any());
diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs
index b81484d3da..dcd745395b 100644
--- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs
+++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs
@@ -78,6 +78,15 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddUntilStep("wait for results screen", () => Stack.CurrentScreen is ResultsScreen);
}
+ [Test]
+ public void TestSingleFilterWhenEntering()
+ {
+ ImportBeatmapForRuleset(0);
+ LoadSongSelect();
+
+ AddAssert("single filter", () => Carousel.FilterCount, () => Is.EqualTo(1));
+ }
+
[Test]
public void TestCookieDoesNothingIfNothingSelected()
{
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneTabControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneTabControl.cs
index bf75d07c2c..49eb1f092c 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneTabControl.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneTabControl.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Position = new Vector2(275, 5)
});
- filter.PinItem(GroupMode.NoGrouping);
+ filter.PinItem(GroupMode.None);
filter.PinItem(GroupMode.LastPlayed);
filter.Current.ValueChanged += grouping =>
diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index 8f6fc214e1..df3e7d88af 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -40,14 +40,14 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.Ruleset, string.Empty);
SetDefault(OsuSetting.Skin, SkinInfo.ARGON_SKIN.ToString());
- SetDefault(OsuSetting.BeatmapDetailTab, PlayBeatmapDetailArea.TabType.Local);
+ SetDefault(OsuSetting.BeatmapDetailTab, BeatmapDetailTab.Local);
SetDefault(OsuSetting.BeatmapDetailModsFilter, false);
SetDefault(OsuSetting.ShowConvertedBeatmaps, true);
SetDefault(OsuSetting.DisplayStarsMinimum, 0.0, 0, 10, 0.1);
SetDefault(OsuSetting.DisplayStarsMaximum, 10.1, 0, 10.1, 0.1);
- SetDefault(OsuSetting.SongSelectGroupMode, GroupMode.NoGrouping);
+ SetDefault(OsuSetting.SongSelectGroupMode, GroupMode.None);
SetDefault(OsuSetting.SongSelectSortingMode, SortMode.Title);
SetDefault(OsuSetting.RandomSelectAlgorithm, RandomSelectAlgorithm.RandomPermutation);
diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs
index edfffad070..19e56bce59 100644
--- a/osu.Game/Graphics/Carousel/Carousel.cs
+++ b/osu.Game/Graphics/Carousel/Carousel.cs
@@ -12,6 +12,7 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Caching;
+using osu.Framework.Development;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -71,6 +72,11 @@ namespace osu.Game.Graphics.Carousel
///
public bool IsFiltering => !filterTask.IsCompleted;
+ ///
+ /// The number of times filter operations have been triggered.
+ ///
+ internal int FilterCount { get; private set; }
+
///
/// The number of displayable items currently being tracked (before filtering).
///
@@ -181,9 +187,13 @@ namespace osu.Game.Graphics.Carousel
/// Whether all existing drawable panels should be reset post filter.
protected virtual Task> FilterAsync(bool clearExistingPanels = false)
{
+ FilterCount++;
+
if (clearExistingPanels)
filterReusesPanels.Invalidate();
+ filterAfterItemsChanged.Validate();
+
filterTask = performFilter();
filterTask.FireAndForget();
return filterTask;
@@ -267,7 +277,7 @@ namespace osu.Game.Graphics.Carousel
RelativeSizeAxes = Axes.Both,
};
- Items.BindCollectionChanged((_, _) => FilterAsync());
+ Items.BindCollectionChanged((_, _) => filterAfterItemsChanged.Invalidate());
}
[BackgroundDependencyLoader]
@@ -290,22 +300,29 @@ namespace osu.Game.Graphics.Carousel
private Task> filterTask = Task.FromResult(Enumerable.Empty());
private CancellationTokenSource cancellationSource = new CancellationTokenSource();
+ ///
+ /// For background re-filters, ensure we wait for the previous filter operation to complete before starting another.
+ /// This avoids the carousel never updating its display in high churn scenarios.
+ ///
+ private readonly Cached filterAfterItemsChanged = new Cached();
+
private async Task> performFilter()
{
Stopwatch stopwatch = Stopwatch.StartNew();
var cts = new CancellationTokenSource();
var previousCancellationSource = Interlocked.Exchange(ref cancellationSource, cts);
- await previousCancellationSource.CancelAsync().ConfigureAwait(false);
+ await previousCancellationSource.CancelAsync().ConfigureAwait(true);
if (DebounceDelay > 0)
{
log($"Filter operation queued, waiting for {DebounceDelay} ms debounce");
- await Task.Delay(DebounceDelay, cts.Token).ConfigureAwait(false);
+ await Task.Delay(DebounceDelay, cts.Token).ConfigureAwait(true);
}
// Copy must be performed on update thread for now (see ConfigureAwait above).
// Could potentially be optimised in the future if it becomes an issue.
+ Debug.Assert(ThreadSafety.IsUpdateThread);
List items = new List(Items.Select(m => new CarouselItem(m)));
await Task.Run(async () =>
@@ -524,6 +541,10 @@ namespace osu.Game.Graphics.Carousel
do
{
newIndex = (newIndex + direction + carouselItems.Count) % carouselItems.Count;
+
+ if (newIndex == originalIndex)
+ break;
+
var newItem = carouselItems[newIndex];
if (CheckValidForGroupSelection(newItem))
@@ -531,7 +552,7 @@ namespace osu.Game.Graphics.Carousel
HandleItemActivated(newItem);
return;
}
- } while (newIndex != originalIndex);
+ } while (true);
}
#endregion
@@ -611,13 +632,13 @@ namespace osu.Game.Graphics.Carousel
{
var item = carouselItems[i];
- updateItemYPosition(item, ref lastVisible, ref yPos);
-
if (CheckModelEquality(item.Model, currentKeyboardSelection.Model!))
currentKeyboardSelection = new Selection(currentKeyboardSelection.Model, item, item.CarouselYPosition, i);
if (CheckModelEquality(item.Model, currentSelection.Model!))
currentSelection = new Selection(currentSelection.Model, item, item.CarouselYPosition, i);
+
+ updateItemYPosition(item, ref lastVisible, ref yPos);
}
// If a keyboard selection is currently made, we want to keep the view stable around the selection.
@@ -721,6 +742,9 @@ namespace osu.Game.Graphics.Carousel
c.KeyboardSelected.Value = c.Item == currentKeyboardSelection?.CarouselItem;
c.Expanded.Value = c.Item.IsExpanded;
}
+
+ if (!filterAfterItemsChanged.IsValid && !IsFiltering)
+ FilterAsync();
}
protected virtual float GetPanelXOffset(Drawable panel)
@@ -751,6 +775,9 @@ namespace osu.Game.Graphics.Carousel
{
Debug.Assert(carouselItems != null);
+ if (carouselItems.Count == 0)
+ return DisplayRange.EMPTY;
+
// Find index range of all items that should be on-screen
carouselBoundsItem.CarouselYPosition = visibleUpperBound - DistanceOffscreenToPreload;
int firstIndex = carouselItems.BinarySearch(carouselBoundsItem);
@@ -770,7 +797,7 @@ namespace osu.Game.Graphics.Carousel
{
Debug.Assert(carouselItems != null);
- List toDisplay = range.Last - range.First == 0
+ List toDisplay = range == DisplayRange.EMPTY
? new List()
: carouselItems.GetRange(range.First, range.Last - range.First + 1);
@@ -889,7 +916,10 @@ namespace osu.Game.Graphics.Carousel
/// The index of the selection as of the last run of . May be null if selection is not present as an item, or if has not been run yet.
private record Selection(object? Model = null, CarouselItem? CarouselItem = null, double? YPosition = null, int? Index = null);
- private record DisplayRange(int First, int Last);
+ private record DisplayRange(int First, int Last)
+ {
+ public static readonly DisplayRange EMPTY = new DisplayRange(-1, -1);
+ }
///
/// Implementation of scroll container which handles very large vertical lists by internally using double precision
diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs
index 3907907158..1baa4ae0ef 100644
--- a/osu.Game/Screens/Footer/ScreenFooter.cs
+++ b/osu.Game/Screens/Footer/ScreenFooter.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
+using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -49,8 +50,13 @@ namespace osu.Game.Screens.Footer
private Container hiddenButtonsContainer = null!;
private LogoTrackingContainer logoTrackingContainer = null!;
+ // TODO: This has some weird update logic local in this class, but it only works for overlay containers.
+ // This is not what we want. The footer is to be displayed on *screens* with different colour schemes.
+ // It needs to update on screen switch.
+ //
+ // For now it's locked to Blue to match song select (the most prominent usage).
[Cached]
- private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
+ private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
public ScreenFooter(BackReceptor? receptor = null)
{
@@ -167,6 +173,7 @@ namespace osu.Game.Screens.Footer
temporarilyHiddenButtons.Clear();
overlays.Clear();
+ this.HidePopover();
clearActiveOverlayContainer();
var oldButtons = buttonsFlow.ToArray();
@@ -312,6 +319,8 @@ namespace osu.Game.Screens.Footer
private void showOverlay(OverlayContainer overlay)
{
+ this.HidePopover();
+
foreach (var o in overlays.Where(o => o != overlay))
o.Hide();
diff --git a/osu.Game/Screens/Select/BeatmapDetailTab.cs b/osu.Game/Screens/Select/BeatmapDetailTab.cs
new file mode 100644
index 0000000000..cd219a4830
--- /dev/null
+++ b/osu.Game/Screens/Select/BeatmapDetailTab.cs
@@ -0,0 +1,38 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Screens.Select
+{
+ public enum BeatmapDetailTab
+ {
+ ///
+ /// Beatmap details.
+ ///
+ Details,
+
+ ///
+ /// Local leaderboards.
+ ///
+ Local,
+
+ ///
+ /// Country leaderboards.
+ ///
+ Country,
+
+ ///
+ /// Global leaderboards.
+ ///
+ Global,
+
+ ///
+ /// Friend leaderboards.
+ ///
+ Friends,
+
+ ///
+ /// Team leaderboards.
+ ///
+ Team
+ }
+}
diff --git a/osu.Game/Screens/Select/Filter/GroupMode.cs b/osu.Game/Screens/Select/Filter/GroupMode.cs
index 9f693177d8..b3a4f36c91 100644
--- a/osu.Game/Screens/Select/Filter/GroupMode.cs
+++ b/osu.Game/Screens/Select/Filter/GroupMode.cs
@@ -7,8 +7,8 @@ namespace osu.Game.Screens.Select.Filter
{
public enum GroupMode
{
- [Description("No Grouping")]
- NoGrouping,
+ [Description("None")]
+ None,
[Description("Artist")]
Artist,
@@ -19,8 +19,8 @@ namespace osu.Game.Screens.Select.Filter
[Description("BPM")]
BPM,
- [Description("Collections")]
- Collections,
+ // [Description("Collections")]
+ // Collections,
[Description("Date Added")]
DateAdded,
@@ -31,17 +31,17 @@ namespace osu.Game.Screens.Select.Filter
[Description("Difficulty")]
Difficulty,
- [Description("Favourites")]
- Favourites,
+ // [Description("Favourites")]
+ // Favourites,
[Description("Length")]
Length,
- [Description("My Maps")]
- MyMaps,
+ // [Description("My Maps")]
+ // MyMaps,
- [Description("Rank Achieved")]
- RankAchieved,
+ // [Description("Rank Achieved")]
+ // RankAchieved,
[Description("Ranked Status")]
RankedStatus,
diff --git a/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs b/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs
index 5b62d5e8d7..ae318de754 100644
--- a/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs
+++ b/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Screens.Select
}
}
- private Bindable selectedTab;
+ private Bindable selectedTab;
private Bindable selectedModsFilter;
@@ -41,7 +41,7 @@ namespace osu.Game.Screens.Select
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
- selectedTab = config.GetBindable(OsuSetting.BeatmapDetailTab);
+ selectedTab = config.GetBindable(OsuSetting.BeatmapDetailTab);
selectedModsFilter = config.GetBindable(OsuSetting.BeatmapDetailModsFilter);
selectedTab.BindValueChanged(tab => CurrentTab.Value = getTabItemFromTabType(tab.NewValue), true);
@@ -86,26 +86,26 @@ namespace osu.Game.Screens.Select
new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Team),
}).ToArray();
- private BeatmapDetailAreaTabItem getTabItemFromTabType(TabType type)
+ private BeatmapDetailAreaTabItem getTabItemFromTabType(BeatmapDetailTab type)
{
switch (type)
{
- case TabType.Details:
+ case BeatmapDetailTab.Details:
return new BeatmapDetailAreaDetailTabItem();
- case TabType.Local:
+ case BeatmapDetailTab.Local:
return new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Local);
- case TabType.Global:
+ case BeatmapDetailTab.Global:
return new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Global);
- case TabType.Country:
+ case BeatmapDetailTab.Country:
return new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Country);
- case TabType.Friends:
+ case BeatmapDetailTab.Friends:
return new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Friend);
- case TabType.Team:
+ case BeatmapDetailTab.Team:
return new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Team);
default:
@@ -113,30 +113,30 @@ namespace osu.Game.Screens.Select
}
}
- private TabType getTabTypeFromTabItem(BeatmapDetailAreaTabItem item)
+ private BeatmapDetailTab getTabTypeFromTabItem(BeatmapDetailAreaTabItem item)
{
switch (item)
{
case BeatmapDetailAreaDetailTabItem:
- return TabType.Details;
+ return BeatmapDetailTab.Details;
case BeatmapDetailAreaLeaderboardTabItem leaderboardTab:
switch (leaderboardTab.Scope)
{
case BeatmapLeaderboardScope.Local:
- return TabType.Local;
+ return BeatmapDetailTab.Local;
case BeatmapLeaderboardScope.Country:
- return TabType.Country;
+ return BeatmapDetailTab.Country;
case BeatmapLeaderboardScope.Global:
- return TabType.Global;
+ return BeatmapDetailTab.Global;
case BeatmapLeaderboardScope.Friend:
- return TabType.Friends;
+ return BeatmapDetailTab.Friends;
case BeatmapLeaderboardScope.Team:
- return TabType.Team;
+ return BeatmapDetailTab.Team;
default:
throw new ArgumentOutOfRangeException(nameof(item));
@@ -146,15 +146,5 @@ namespace osu.Game.Screens.Select
throw new ArgumentOutOfRangeException(nameof(item));
}
}
-
- public enum TabType
- {
- Details,
- Local,
- Country,
- Global,
- Friends,
- Team
- }
}
}
diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs
index e007ae54ce..b85b7cba45 100644
--- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs
+++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs
@@ -7,6 +7,7 @@ using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using System.Threading;
+using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
@@ -60,9 +61,25 @@ namespace osu.Game.Screens.SelectV2
if ((top.Model is GroupDefinition) ^ (bottom.Model is GroupDefinition))
return SPACING * 2;
- // Beatmap difficulty panels do not overlap with themselves or any other panel.
- if (grouping.BeatmapSetsGroupedTogether && (top.Model is BeatmapInfo || bottom.Model is BeatmapInfo))
- return SPACING;
+ if (grouping.BeatmapSetsGroupedTogether)
+ {
+ // Give some space around the expanded beatmap set, at the top..
+ if (bottom.Model is BeatmapSetInfo && bottom.IsExpanded)
+ return SPACING * 2;
+
+ // ..and the bottom.
+ if (top.Model is BeatmapInfo && bottom.Model is BeatmapSetInfo)
+ return SPACING * 2;
+
+ // Beatmap difficulty panels do not overlap with themselves or any other panel.
+ if (top.Model is BeatmapInfo || bottom.Model is BeatmapInfo)
+ return SPACING;
+ }
+ else
+ {
+ if (top == CurrentSelectionItem || bottom == CurrentSelectionItem)
+ return SPACING * 2;
+ }
return -SPACING;
}
@@ -74,9 +91,9 @@ namespace osu.Game.Screens.SelectV2
Filters = new ICarouselFilter[]
{
- matching = new BeatmapCarouselFilterMatching(() => Criteria),
- new BeatmapCarouselFilterSorting(() => Criteria),
- grouping = new BeatmapCarouselFilterGrouping(() => Criteria),
+ matching = new BeatmapCarouselFilterMatching(() => Criteria!),
+ new BeatmapCarouselFilterSorting(() => Criteria!),
+ grouping = new BeatmapCarouselFilterGrouping(() => Criteria!),
};
AddInternal(loading = new LoadingLayer());
@@ -86,20 +103,20 @@ namespace osu.Game.Screens.SelectV2
private void load(BeatmapStore beatmapStore, AudioManager audio, OsuConfigManager config, CancellationToken? cancellationToken)
{
setupPools();
- setupBeatmaps(beatmapStore, cancellationToken);
+ detachedBeatmaps = beatmapStore.GetBeatmapSets(cancellationToken);
loadSamples(audio);
config.BindWith(OsuSetting.RandomSelectAlgorithm, randomAlgorithm);
}
- #region Beatmap source hookup
-
- private void setupBeatmaps(BeatmapStore beatmapStore, CancellationToken? cancellationToken)
+ protected override void LoadComplete()
{
- detachedBeatmaps = beatmapStore.GetBeatmapSets(cancellationToken);
+ base.LoadComplete();
detachedBeatmaps.BindCollectionChanged(beatmapSetsChanged, true);
}
+ #region Beatmap source hookup
+
private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed)
{
// TODO: moving management of BeatmapInfo tracking to BeatmapStore might be something we want to consider.
@@ -467,7 +484,7 @@ namespace osu.Game.Screens.SelectV2
#region Filtering
- public FilterCriteria Criteria { get; private set; } = new FilterCriteria();
+ public FilterCriteria? Criteria { get; private set; }
private ScheduledDelegate? loadingDebounce;
@@ -493,6 +510,14 @@ namespace osu.Game.Screens.SelectV2
}));
}
+ protected override Task> FilterAsync(bool clearExistingPanels = false)
+ {
+ if (Criteria == null)
+ return Task.FromResult(Enumerable.Empty());
+
+ return base.FilterAsync(clearExistingPanels);
+ }
+
#endregion
#region Drawable pooling
diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs
index 86256ad99e..8720378ad6 100644
--- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs
+++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs
@@ -124,14 +124,22 @@ namespace osu.Game.Screens.SelectV2
public static bool ShouldGroupBeatmapsTogether(FilterCriteria criteria)
{
- return criteria.Sort != SortMode.Difficulty && criteria.Group != GroupMode.Difficulty;
+ // In certain cases, we intentionally split out difficulties
+ // where it's more relevant or convenient to view them as individual items.
+ if (criteria.Sort == SortMode.Difficulty || criteria.Group == GroupMode.Difficulty)
+ return false;
+ if (criteria.Sort == SortMode.LastPlayed && criteria.Group == GroupMode.LastPlayed)
+ return false;
+
+ // In the majority case we group sets together for display.
+ return true;
}
private List getGroups(List items, FilterCriteria criteria)
{
switch (criteria.Group)
{
- case GroupMode.NoGrouping:
+ case GroupMode.None:
return new List { new GroupMapping(null, items) };
case GroupMode.Artist:
@@ -152,12 +160,15 @@ namespace osu.Game.Screens.SelectV2
case GroupMode.LastPlayed:
return getGroupsBy(b =>
{
- DateTimeOffset? maxLastPlayed = aggregateMax(b, items, bb => bb.LastPlayed);
+ var date = b.LastPlayed;
- if (maxLastPlayed == null)
+ if (BeatmapSetsGroupedTogether)
+ date = aggregateMax(b, static b => (b.LastPlayed ?? DateTimeOffset.MinValue));
+
+ if (date == null || date == DateTimeOffset.MinValue)
return new GroupDefinition(int.MaxValue, "Never");
- return defineGroupByDate(maxLastPlayed.Value);
+ return defineGroupByDate(date.Value);
}, items);
case GroupMode.RankedStatus:
@@ -166,8 +177,12 @@ namespace osu.Game.Screens.SelectV2
case GroupMode.BPM:
return getGroupsBy(b =>
{
- double maxBPM = aggregateMax(b, items, bb => bb.BPM);
- return defineGroupByBPM(maxBPM);
+ double bpm = b.BPM;
+
+ if (BeatmapSetsGroupedTogether)
+ bpm = aggregateMax(b, bb => bb.BPM);
+
+ return defineGroupByBPM(bpm);
}, items);
case GroupMode.Difficulty:
@@ -176,25 +191,27 @@ namespace osu.Game.Screens.SelectV2
case GroupMode.Length:
return getGroupsBy(b =>
{
- double maxLength = aggregateMax(b, items, bb => bb.Length);
- return defineGroupByLength(maxLength);
+ double length = b.Length;
+
+ if (BeatmapSetsGroupedTogether)
+ length = aggregateMax(b, bb => bb.Length);
+
+ return defineGroupByLength(length);
}, items);
- case GroupMode.Collections:
- // TODO: needs implementation
- goto case GroupMode.NoGrouping;
-
- case GroupMode.Favourites:
- // TODO: needs implementation
- goto case GroupMode.NoGrouping;
-
- case GroupMode.MyMaps:
- // TODO: needs implementation
- goto case GroupMode.NoGrouping;
-
- case GroupMode.RankAchieved:
- // TODO: needs implementation
- goto case GroupMode.NoGrouping;
+ // TODO: need implementation
+ //
+ // case GroupMode.Collections:
+ // goto case GroupMode.None;
+ //
+ // case GroupMode.Favourites:
+ // goto case GroupMode.None;
+ //
+ // case GroupMode.MyMaps:
+ // goto case GroupMode.None;
+ //
+ // case GroupMode.RankAchieved:
+ // goto case GroupMode.None;
default:
throw new ArgumentOutOfRangeException();
@@ -334,10 +351,10 @@ namespace osu.Game.Screens.SelectV2
return new GroupDefinition(11, "Over 10 minutes");
}
- private static T? aggregateMax(BeatmapInfo b, IEnumerable items, Func func)
+ private static T? aggregateMax(BeatmapInfo b, Func func)
{
- var matchedBeatmaps = items.Select(i => i.Model).Cast().Where(beatmap => beatmap.BeatmapSet!.Equals(b.BeatmapSet));
- return matchedBeatmaps.Max(func);
+ var beatmaps = b.BeatmapSet!.Beatmaps.Where(bb => !bb.Hidden);
+ return beatmaps.Max(func);
}
private record GroupMapping(GroupDefinition? Group, List ItemsInGroup);
diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs
index eb39b499de..0ebfc084bd 100644
--- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs
+++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs
@@ -27,19 +27,27 @@ namespace osu.Game.Screens.SelectV2
{
var criteria = getCriteria();
+ bool groupedSets = BeatmapCarouselFilterGrouping.ShouldGroupBeatmapsTogether(criteria);
+
return items.Order(Comparer.Create((a, b) =>
{
var ab = (BeatmapInfo)a.Model;
var bb = (BeatmapInfo)b.Model;
- if (ab.BeatmapSet!.Equals(bb.BeatmapSet))
- return compareDifficulty(ab, bb);
+ if (groupedSets)
+ {
+ if (ab.BeatmapSet!.Equals(bb.BeatmapSet))
+ return compareDifficulty(ab, bb, criteria.Sort);
- return compare(ab, bb, criteria.Sort);
+ // If we're grouping by sets, all fallback sorts need to be aggregates for the set.
+ return compare(ab, bb, criteria.Sort, aggregate: true);
+ }
+
+ return compare(ab, bb, criteria.Sort, aggregate: false);
})).ToList();
}, cancellationToken).ConfigureAwait(false);
- private static int compare(BeatmapInfo a, BeatmapInfo b, SortMode sort)
+ private static int compare(BeatmapInfo a, BeatmapInfo b, SortMode sort, bool aggregate)
{
int comparison;
@@ -80,15 +88,24 @@ namespace osu.Game.Screens.SelectV2
break;
case SortMode.LastPlayed:
- comparison = -compareUsingAggregateMax(a, b, static b => (b.LastPlayed ?? DateTimeOffset.MinValue).ToUnixTimeSeconds());
+ if (aggregate)
+ comparison = compareUsingAggregateMax(b, a, static b => (b.LastPlayed ?? DateTimeOffset.MinValue).ToUnixTimeSeconds());
+ else
+ comparison = Nullable.Compare(b.LastPlayed, a.LastPlayed);
break;
case SortMode.BPM:
- comparison = compareUsingAggregateMax(a, b, static b => b.BPM);
+ if (aggregate)
+ comparison = compareUsingAggregateMax(a, b, static b => b.BPM);
+ else
+ comparison = a.BPM.CompareTo(b.BPM);
break;
case SortMode.Length:
- comparison = compareUsingAggregateMax(a, b, static b => b.Length);
+ if (aggregate)
+ comparison = compareUsingAggregateMax(a, b, static b => b.Length);
+ else
+ comparison = a.Length.CompareTo(b.Length);
break;
default:
@@ -108,7 +125,7 @@ namespace osu.Game.Screens.SelectV2
return comparison;
}
- private static int compareDifficulty(BeatmapInfo a, BeatmapInfo b)
+ private static int compareDifficulty(BeatmapInfo a, BeatmapInfo b, SortMode sort)
{
int comparison = a.Ruleset.CompareTo(b.Ruleset);
diff --git a/osu.Game/Screens/SelectV2/BeatmapDetailsArea_Header.cs b/osu.Game/Screens/SelectV2/BeatmapDetailsArea_Header.cs
index ee93001b86..76734e110f 100644
--- a/osu.Game/Screens/SelectV2/BeatmapDetailsArea_Header.cs
+++ b/osu.Game/Screens/SelectV2/BeatmapDetailsArea_Header.cs
@@ -7,8 +7,10 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
+using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Leaderboards;
using osuTK;
@@ -28,10 +30,12 @@ namespace osu.Game.Screens.SelectV2
public IBindable Scope => scopeDropdown.Current;
+ private readonly Bindable configDetailTab = new Bindable();
+
public IBindable FilterBySelectedMods => selectedModsToggle.Active;
[BackgroundDependencyLoader]
- private void load()
+ private void load(OsuConfigManager config)
{
InternalChildren = new Drawable[]
{
@@ -98,18 +102,95 @@ namespace osu.Game.Screens.SelectV2
},
},
};
+
+ config.BindWith(OsuSetting.BeatmapDetailTab, configDetailTab);
+ config.BindWith(OsuSetting.BeatmapDetailModsFilter, selectedModsToggle.Active);
}
protected override void LoadComplete()
{
base.LoadComplete();
+ scopeDropdown.Current.Value = tryMapDetailTabToLeaderboardScope(configDetailTab.Value) ?? scopeDropdown.Current.Value;
+ scopeDropdown.Current.BindValueChanged(_ => updateConfigDetailTab());
+
+ tabControl.Current.Value = configDetailTab.Value == BeatmapDetailTab.Details ? Selection.Details : Selection.Ranking;
tabControl.Current.BindValueChanged(v =>
{
leaderboardControls.FadeTo(v.NewValue == Selection.Ranking ? 1 : 0, 300, Easing.OutQuint);
+ updateConfigDetailTab();
}, true);
}
+ #region Reading / writing state from / to configuration
+
+ private void updateConfigDetailTab()
+ {
+ switch (tabControl.Current.Value)
+ {
+ case Selection.Details:
+ configDetailTab.Value = BeatmapDetailTab.Details;
+ return;
+
+ case Selection.Ranking:
+ configDetailTab.Value = mapLeaderboardScopeToDetailTab(scopeDropdown.Current.Value);
+ return;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(tabControl.Current.Value), tabControl.Current.Value, null);
+ }
+ }
+
+ private static BeatmapLeaderboardScope? tryMapDetailTabToLeaderboardScope(BeatmapDetailTab tab)
+ {
+ switch (tab)
+ {
+ case BeatmapDetailTab.Local:
+ return BeatmapLeaderboardScope.Local;
+
+ case BeatmapDetailTab.Country:
+ return BeatmapLeaderboardScope.Country;
+
+ case BeatmapDetailTab.Global:
+ return BeatmapLeaderboardScope.Global;
+
+ case BeatmapDetailTab.Friends:
+ return BeatmapLeaderboardScope.Friend;
+
+ case BeatmapDetailTab.Team:
+ return BeatmapLeaderboardScope.Team;
+
+ default:
+ return null;
+ }
+ }
+
+ private static BeatmapDetailTab mapLeaderboardScopeToDetailTab(BeatmapLeaderboardScope scope)
+ {
+ switch (scope)
+ {
+ case BeatmapLeaderboardScope.Local:
+ return BeatmapDetailTab.Local;
+
+ case BeatmapLeaderboardScope.Country:
+ return BeatmapDetailTab.Country;
+
+ case BeatmapLeaderboardScope.Global:
+ return BeatmapDetailTab.Global;
+
+ case BeatmapLeaderboardScope.Friend:
+ return BeatmapDetailTab.Friends;
+
+ case BeatmapLeaderboardScope.Team:
+ return BeatmapDetailTab.Team;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(scope), scope, null);
+ }
+ }
+
+ #endregion
+
public enum Selection
{
Details,
diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs
index 113894ab8a..6a810a83b4 100644
--- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs
+++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs
@@ -81,7 +81,7 @@ namespace osu.Game.Screens.SelectV2
private const float username_min_width = 120;
private const float statistics_regular_min_width = 165;
private const float statistics_compact_min_width = 90;
- private const float rank_label_width = 60;
+ private const float rank_label_width = 40;
private const int corner_radius = 10;
private const int transition_duration = 200;
@@ -117,6 +117,15 @@ namespace osu.Game.Screens.SelectV2
private readonly bool sheared;
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
+ {
+ var inputRectangle = DrawRectangle;
+
+ inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapLeaderboardWedge.SPACING_BETWEEN_SCORES / 2 });
+
+ return inputRectangle.Contains(ToLocalSpace(screenSpacePos));
+ }
+
public BeatmapLeaderboardScore(ScoreInfo score, bool sheared = true)
{
this.score = score;
@@ -144,6 +153,7 @@ namespace osu.Game.Screens.SelectV2
{
background = new Box
{
+ Alpha = 0.4f,
RelativeSizeAxes = Axes.Both,
Colour = backgroundColour
},
@@ -190,6 +200,7 @@ namespace osu.Game.Screens.SelectV2
{
foreground = new Box
{
+ Alpha = 0.4f,
RelativeSizeAxes = Axes.Both,
Colour = foregroundColour
},
@@ -312,8 +323,8 @@ namespace osu.Game.Screens.SelectV2
Child = statisticsContainer = new FillFlowContainer
{
Name = @"Statistics container",
- Padding = new MarginPadding { Right = 40 },
- Spacing = new Vector2(25, 0),
+ Padding = new MarginPadding { Right = 10 },
+ Spacing = new Vector2(20, 0),
Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
@@ -567,13 +578,13 @@ namespace osu.Game.Screens.SelectV2
private DisplayMode getCurrentDisplayMode()
{
- if (DrawWidth >= HEIGHT + username_min_width + statistics_regular_min_width + expanded_right_content_width + rank_label_width)
+ if (DrawWidth >= username_min_width + statistics_regular_min_width + expanded_right_content_width + rank_label_width)
return DisplayMode.Full;
- if (DrawWidth >= HEIGHT + username_min_width + statistics_regular_min_width + expanded_right_content_width)
+ if (DrawWidth >= username_min_width + statistics_regular_min_width + expanded_right_content_width)
return DisplayMode.Regular;
- if (DrawWidth >= HEIGHT + username_min_width + statistics_compact_min_width + expanded_right_content_width)
+ if (DrawWidth >= username_min_width + statistics_compact_min_width + expanded_right_content_width)
return DisplayMode.Compact;
return DisplayMode.Minimal;
diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs
index 29affaa9af..e3d52adef5 100644
--- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs
+++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs
@@ -3,14 +3,16 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.PolygonExtensions;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
@@ -27,11 +29,14 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.Select.Leaderboards;
using osuTK;
+using osuTK.Graphics;
namespace osu.Game.Screens.SelectV2
{
public partial class BeatmapLeaderboardWedge : VisibilityContainer
{
+ public const float SPACING_BETWEEN_SCORES = 4;
+
public IBindable Scope { get; } = new Bindable();
public IBindable FilterBySelectedMods { get; } = new BindableBool();
@@ -70,7 +75,7 @@ namespace osu.Game.Screens.SelectV2
private readonly IBindable fetchedScores = new Bindable();
- private const float personal_best_height = 80;
+ private const float personal_best_height = 100;
[BackgroundDependencyLoader]
private void load()
@@ -109,7 +114,10 @@ namespace osu.Game.Screens.SelectV2
RelativeSizeAxes = Axes.X,
Height = personal_best_height,
Shear = OsuGame.SHEAR,
- Margin = new MarginPadding { Left = -40f },
+ Margin = new MarginPadding
+ {
+ Left = -40f,
+ },
CornerRadius = 10f,
Masking = true,
// push the personal best 1px down to hide masking issues
@@ -118,11 +126,7 @@ namespace osu.Game.Screens.SelectV2
Alpha = 0f,
Children = new Drawable[]
{
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = colourProvider.Background4,
- },
+ new WedgeBackground(),
new Container
{
RelativeSizeAxes = Axes.X,
@@ -256,10 +260,10 @@ namespace osu.Game.Screens.SelectV2
foreach (var d in loadedScores)
{
- d.Y = (BeatmapLeaderboardScore.HEIGHT + 4f) * i;
+ d.Y = (BeatmapLeaderboardScore.HEIGHT + SPACING_BETWEEN_SCORES) * i;
// This is a bit of a weird one. We're already in a sheared state and don't want top-level
- // shear applied, but still need the `BeatmapLeadeboardScore` to be in "sheared" mode (see ctor).
+ // shear applied, but still need the `BeatmapLeaderboardScore` to be in "sheared" mode (see ctor).
d.Shear = Vector2.Zero;
scoresContainer.Add(d);
@@ -352,6 +356,59 @@ namespace osu.Game.Screens.SelectV2
placeholder.FadeInFromZero(300, Easing.OutQuint);
}
+ #region Fade handling
+
+ protected override void UpdateAfterChildren()
+ {
+ base.UpdateAfterChildren();
+
+ const int height = BeatmapLeaderboardScore.HEIGHT;
+
+ float fadeBottom = (float)(scoresScroll.Current + scoresScroll.DrawHeight);
+ float fadeTop = (float)(scoresScroll.Current);
+
+ if (!scoresScroll.IsScrolledToStart())
+ fadeTop += height;
+
+ foreach (var c in scoresContainer)
+ {
+ float topY = c.ToSpaceOfOtherDrawable(Vector2.Zero, scoresContainer).Y;
+ float bottomY = topY + height;
+
+ bool requireBottomFade = bottomY >= fadeBottom;
+ bool requireTopFade = topY < fadeTop;
+
+ if (!requireBottomFade && !requireTopFade)
+ {
+ c.Colour = Color4.White;
+ continue;
+ }
+
+ if (topY > fadeBottom + height || bottomY < fadeTop - height)
+ {
+ c.Colour = Color4.Transparent;
+ continue;
+ }
+
+ if (requireBottomFade)
+ {
+ c.Colour = ColourInfo.GradientVertical(
+ Color4.White.Opacity(Math.Min(1 - (topY - fadeBottom) / height, 1)),
+ Color4.White.Opacity(Math.Min(1 - (bottomY - fadeBottom) / height, 1)));
+ }
+ else
+ {
+ Debug.Assert(requireTopFade);
+
+ c.Colour = ColourInfo.GradientVertical(
+ Color4.White.Opacity(Math.Min(1 - (fadeTop - topY) / height, 1)),
+ Color4.White.Opacity(Math.Min(1 - (fadeTop - bottomY) / height, 1)));
+ }
+ }
+ }
+
+ #endregion
+
private Placeholder? getPlaceholderFor(LeaderboardState state)
{
switch (state)
diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs
index 8362f5b6a7..734d768241 100644
--- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs
+++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs
@@ -253,7 +253,7 @@ namespace osu.Game.Screens.SelectV2
mapperText.Text = beatmap.Value.Metadata.Author.Username;
}
- starRatingDisplay.Current = (Bindable)difficultyCache.GetBindableDifficulty(beatmap.Value.BeatmapInfo, cancellationSource.Token, 200);
+ starRatingDisplay.Current = (Bindable)difficultyCache.GetBindableDifficulty(beatmap.Value.BeatmapInfo, cancellationSource.Token, SongSelect.SELECTION_DEBOUNCE);
updateCountStatistics(cancellationSource.Token);
updateDifficultyStatistics();
diff --git a/osu.Game/Screens/SelectV2/FilterControl.cs b/osu.Game/Screens/SelectV2/FilterControl.cs
index 8b360688fa..05429c2c12 100644
--- a/osu.Game/Screens/SelectV2/FilterControl.cs
+++ b/osu.Game/Screens/SelectV2/FilterControl.cs
@@ -17,7 +17,6 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
-using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Select;
@@ -142,9 +141,9 @@ namespace osu.Game.Screens.SelectV2
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
ColumnDimensions = new[]
{
- new Dimension(maxSize: 210),
+ new Dimension(maxSize: 180),
new Dimension(GridSizeMode.Absolute, 5),
- new Dimension(maxSize: 230),
+ new Dimension(maxSize: 180),
new Dimension(GridSizeMode.Absolute, 5),
new Dimension(),
},
@@ -152,14 +151,14 @@ namespace osu.Game.Screens.SelectV2
{
new[]
{
- sortDropdown = new ShearedDropdown(SortStrings.Default)
+ sortDropdown = new ShearedDropdown("Sort")
{
RelativeSizeAxes = Axes.X,
Items = Enum.GetValues(),
},
Empty(),
// todo: pending localisation
- groupDropdown = new ShearedDropdown("Group by")
+ groupDropdown = new ShearedDropdown("Group")
{
RelativeSizeAxes = Axes.X,
Items = Enum.GetValues(),
diff --git a/osu.Game/Screens/SelectV2/FooterButtonOptions_Popover.cs b/osu.Game/Screens/SelectV2/FooterButtonOptions_Popover.cs
index 3031dcb8f7..039020d7c4 100644
--- a/osu.Game/Screens/SelectV2/FooterButtonOptions_Popover.cs
+++ b/osu.Game/Screens/SelectV2/FooterButtonOptions_Popover.cs
@@ -68,6 +68,10 @@ namespace osu.Game.Screens.SelectV2
foreach (OsuMenuItem item in SongSelect.GetForwardActions(beatmap.BeatmapInfo))
{
+ // We can't display menus with child items here, so just ignore them.
+ if (item.Items.Any())
+ continue;
+
if (item is OsuMenuItemSpacer)
{
buttonFlow.Add(new Container
diff --git a/osu.Game/Screens/SelectV2/NoResultsPlaceholder.cs b/osu.Game/Screens/SelectV2/NoResultsPlaceholder.cs
index 8caa559550..46f8859255 100644
--- a/osu.Game/Screens/SelectV2/NoResultsPlaceholder.cs
+++ b/osu.Game/Screens/SelectV2/NoResultsPlaceholder.cs
@@ -1,6 +1,7 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -20,10 +21,14 @@ namespace osu.Game.Screens.SelectV2
{
public partial class NoResultsPlaceholder : VisibilityContainer
{
+ public Action? RequestClearFilterText { get; init; }
+
private FilterCriteria? filter;
private LinkFlowContainer textFlow = null!;
+ private SpriteIcon icon = null!;
+
[Resolved]
private BeatmapManager beatmaps { get; set; } = null!;
@@ -50,8 +55,7 @@ namespace osu.Game.Screens.SelectV2
[BackgroundDependencyLoader]
private void load()
{
- Width = 400;
- AutoSizeAxes = Axes.Y;
+ RelativeSizeAxes = Axes.Both;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
@@ -61,11 +65,13 @@ namespace osu.Game.Screens.SelectV2
new FillFlowContainer
{
Direction = FillDirection.Vertical,
- RelativeSizeAxes = Axes.X,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Width = 300,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
- new SpriteIcon
+ icon = new SpriteIcon
{
Icon = FontAwesome.Solid.Ghost,
Anchor = Anchor.TopCentre,
@@ -78,7 +84,7 @@ namespace osu.Game.Screens.SelectV2
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Font = OsuFont.Style.Title,
- Text = "No beatmaps found"
+ Text = "No matching beatmaps"
},
textFlow = new LinkFlowContainer
{
@@ -115,6 +121,9 @@ namespace osu.Game.Screens.SelectV2
this.ScaleTo(0.9f)
.ScaleTo(1f, 1000, Easing.OutQuint);
+ icon.ScaleTo(new Vector2(-1, 1))
+ .ScaleTo(new Vector2(1, 1), 500, Easing.InOutSine);
+
textFlow.FadeInFromZero(800, Easing.OutQuint);
textFlow.Clear();
@@ -131,6 +140,18 @@ namespace osu.Game.Screens.SelectV2
textFlow.AddParagraph("No beatmaps match your filter criteria!");
textFlow.AddParagraph(string.Empty);
+ if (!string.IsNullOrEmpty(filter?.SearchText))
+ {
+ addBulletPoint();
+ textFlow.AddText("Try ");
+ textFlow.AddLink("clearing", () =>
+ {
+ RequestClearFilterText?.Invoke();
+ });
+
+ textFlow.AddText(" your current search criteria.");
+ }
+
if (filter?.UserStarDifficulty.HasFilter == true)
{
addBulletPoint();
diff --git a/osu.Game/Screens/SelectV2/Panel.cs b/osu.Game/Screens/SelectV2/Panel.cs
index de559be4a9..f17567f9ba 100644
--- a/osu.Game/Screens/SelectV2/Panel.cs
+++ b/osu.Game/Screens/SelectV2/Panel.cs
@@ -26,7 +26,7 @@ namespace osu.Game.Screens.SelectV2
{
private const float corner_radius = 10;
- private const float active_x_offset = 50f;
+ private const float active_x_offset = 25f;
protected const float DURATION = 400;
@@ -34,7 +34,6 @@ namespace osu.Game.Screens.SelectV2
private Box backgroundBorder = null!;
private Box backgroundGradient = null!;
- private Box backgroundAccentGradient = null!;
private Container backgroundLayerHorizontalPadding = null!;
private Container backgroundContainer = null!;
private Container iconContainer = null!;
@@ -65,7 +64,7 @@ namespace osu.Game.Screens.SelectV2
set
{
accentColour = value;
- updateDisplay();
+ updateAccentColour();
}
}
@@ -95,8 +94,8 @@ namespace osu.Game.Screens.SelectV2
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
- Offset = new Vector2(1f),
- Radius = 10,
+ Hollow = true,
+ Radius = 2,
},
Children = new Drawable[]
{
@@ -108,7 +107,7 @@ namespace osu.Game.Screens.SelectV2
backgroundBorder = new Box
{
RelativeSizeAxes = Axes.Both,
- Colour = Color4.White,
+ Colour = Color4.Black,
},
backgroundLayerHorizontalPadding = new Container
{
@@ -124,10 +123,6 @@ namespace osu.Game.Screens.SelectV2
{
RelativeSizeAxes = Axes.Both,
},
- backgroundAccentGradient = new Box
- {
- RelativeSizeAxes = Axes.Both,
- },
backgroundContainer = new Container
{
RelativeSizeAxes = Axes.Both,
@@ -158,10 +153,9 @@ namespace osu.Game.Screens.SelectV2
selectionLayer = new Box
{
Alpha = 0,
- Colour = ColourInfo.GradientHorizontal(colours.BlueDark.Opacity(0), colours.BlueDark.Opacity(0.6f)),
- Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
- Width = 0.3f,
+ Width = 0.6f,
+ Blending = BlendingParameters.Additive,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
},
@@ -190,15 +184,15 @@ namespace osu.Game.Screens.SelectV2
{
base.LoadComplete();
- Expanded.BindValueChanged(_ => updateDisplay(), true);
-
- Selected.BindValueChanged(selected =>
+ Expanded.BindValueChanged(_ =>
{
- if (selected.NewValue)
- selectionLayer.FadeIn(100, Easing.OutQuint);
- else
- selectionLayer.FadeOut(200, Easing.OutQuint);
+ updateSelectedState();
+ updateXOffset();
+ });
+ Selected.BindValueChanged(_ =>
+ {
+ updateSelectedState();
updateXOffset();
}, true);
@@ -217,6 +211,9 @@ namespace osu.Game.Screens.SelectV2
{
base.PrepareForUse();
+ updateAccentColour();
+ updateXOffset();
+
this.FadeIn(DURATION, Easing.OutQuint);
}
@@ -236,18 +233,28 @@ namespace osu.Game.Screens.SelectV2
return true;
}
- private void updateDisplay()
+ private void updateAccentColour()
{
var backgroundColour = accentColour ?? Color4.White;
+
+ backgroundBorder.Colour = backgroundColour;
+
+ selectionLayer.Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), backgroundColour.Opacity(0.5f));
+
+ updateSelectedState(animated: false);
+ }
+
+ private void updateSelectedState(bool animated = true)
+ {
+ bool selectedOrExpanded = Expanded.Value || Selected.Value;
+
var edgeEffectColour = accentColour ?? Color4Extensions.FromHex(@"4EBFFF");
+ TopLevelContent.FadeEdgeEffectTo(selectedOrExpanded ? edgeEffectColour.Opacity(0.8f) : Color4.Black.Opacity(0.4f), animated ? DURATION : 0, Easing.OutQuint);
- backgroundAccentGradient.FadeColour(ColourInfo.GradientHorizontal(backgroundColour.Opacity(0.25f), backgroundColour.Opacity(0f)), DURATION, Easing.OutQuint);
- backgroundBorder.FadeColour(backgroundColour, DURATION, Easing.OutQuint);
-
- TopLevelContent.FadeEdgeEffectTo(Expanded.Value ? edgeEffectColour.Opacity(0.5f) : Color4.Black.Opacity(0.4f), DURATION, Easing.OutQuint);
-
- updateXOffset();
- updateHover();
+ if (selectedOrExpanded)
+ selectionLayer.FadeIn(100, Easing.OutQuint);
+ else
+ selectionLayer.FadeOut(200, Easing.OutQuint);
}
private void updateXOffset()
@@ -255,31 +262,28 @@ namespace osu.Game.Screens.SelectV2
float x = PanelXOffset + corner_radius;
if (!Expanded.Value && !Selected.Value)
- x += active_x_offset;
+ {
+ if (this is PanelBeatmap)
+ x += active_x_offset * 2;
+ else
+ x += active_x_offset * 4;
+ }
if (!KeyboardSelected.Value)
- x += active_x_offset * 0.5f;
+ x += active_x_offset;
TopLevelContent.MoveToX(x, DURATION, Easing.OutQuint);
}
- private void updateHover()
- {
- if (IsHovered)
- hoverLayer.FadeIn(100, Easing.OutQuint);
- else
- hoverLayer.FadeOut(1000, Easing.OutQuint);
- }
-
protected override bool OnHover(HoverEvent e)
{
- updateHover();
+ hoverLayer.FadeIn(100, Easing.OutQuint);
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
- updateHover();
+ hoverLayer.FadeOut(1000, Easing.OutQuint);
base.OnHoverLost(e);
}
diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs
index 190e563c46..e785448c9a 100644
--- a/osu.Game/Screens/SelectV2/PanelBeatmap.cs
+++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs
@@ -7,12 +7,16 @@ using System.Diagnostics;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
+using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Carousel;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@@ -36,10 +40,15 @@ namespace osu.Game.Screens.SelectV2
private PanelLocalRankDisplay localRank = null!;
private OsuSpriteText difficultyText = null!;
private OsuSpriteText authorText = null!;
+ private FillFlowContainer mainFill = null!;
private IBindable? starDifficultyBindable;
private CancellationTokenSource? starDifficultyCancellationSource;
+ private Box backgroundAccentGradient = null!;
+
+ private TrianglesV2 triangles = null!;
+
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
@@ -58,6 +67,11 @@ namespace osu.Game.Screens.SelectV2
[Resolved]
private ISongSelect? songSelect { get; set; }
+ public PanelBeatmap()
+ {
+ PanelXOffset = 60;
+ }
+
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
{
var inputRectangle = TopLevelContent.DrawRectangle;
@@ -78,80 +92,108 @@ namespace osu.Game.Screens.SelectV2
Icon = difficultyIcon = new ConstrainedIconContainer
{
- Size = new Vector2(16f),
- Margin = new MarginPadding { Horizontal = 5f },
+ Size = new Vector2(9f),
+ Margin = new MarginPadding { Left = 2.5f, Right = 1.5f },
Colour = colourProvider.Background5,
};
- Content.Children = new[]
+ Background = new Container
{
- new FillFlowContainer
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
{
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- Padding = new MarginPadding { Left = 10f },
- Direction = FillDirection.Vertical,
- AutoSizeAxes = Axes.Both,
- Children = new Drawable[]
+ backgroundAccentGradient = new Box
{
- new FillFlowContainer
+ RelativeSizeAxes = Axes.Both,
+ },
+ triangles = new TrianglesV2
+ {
+ ScaleAdjust = 1.2f,
+ Thickness = 0.01f,
+ Velocity = 0.3f,
+ RelativeSizeAxes = Axes.Both,
+ },
+ }
+ };
+
+ Content.Child = new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Spacing = new Vector2(3),
+ Margin = new MarginPadding { Left = 5 },
+ Direction = FillDirection.Horizontal,
+ Children = new Drawable[]
+ {
+ localRank = new PanelLocalRankDisplay
+ {
+ Scale = new Vector2(0.8f),
+ Origin = Anchor.CentreLeft,
+ Anchor = Anchor.CentreLeft,
+ },
+ mainFill = new FillFlowContainer
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Direction = FillDirection.Vertical,
+ AutoSizeAxes = Axes.Both,
+ Children = new Drawable[]
{
- Direction = FillDirection.Horizontal,
- Spacing = new Vector2(3, 0),
- AutoSizeAxes = Axes.Both,
- Children = new Drawable[]
+ new FillFlowContainer
{
- localRank = new PanelLocalRankDisplay
+ Direction = FillDirection.Horizontal,
+ AutoSizeAxes = Axes.Both,
+ Padding = new MarginPadding { Bottom = 4 },
+ Children = new Drawable[]
{
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- Scale = new Vector2(0.65f)
- },
- starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small, animated: true)
- {
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- Scale = new Vector2(0.875f),
- },
- starCounter = new StarCounter
- {
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- Scale = new Vector2(0.4f)
+ keyCountText = new OsuSpriteText
+ {
+ Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ Alpha = 0,
+ },
+ difficultyText = new OsuSpriteText
+ {
+ Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ Margin = new MarginPadding { Right = 3f },
+ },
+ authorText = new OsuSpriteText
+ {
+ Colour = colourProvider.Content2,
+ Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft
+ }
}
- }
- },
- new FillFlowContainer
- {
- Direction = FillDirection.Horizontal,
- AutoSizeAxes = Axes.Both,
- Children = new[]
+ },
+ new FillFlowContainer
{
- keyCountText = new OsuSpriteText
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(3),
+ AutoSizeAxes = Axes.Both,
+ Children = new Drawable[]
{
- Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.BottomLeft,
- Alpha = 0,
+ starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small, animated: true)
+ {
+ Origin = Anchor.CentreLeft,
+ Anchor = Anchor.CentreLeft,
+ Scale = new Vector2(0.875f),
+ },
+ starCounter = new StarCounter
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Scale = new Vector2(0.4f)
+ }
},
- difficultyText = new OsuSpriteText
- {
- Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.BottomLeft,
- Margin = new MarginPadding { Right = 3f },
- },
- authorText = new OsuSpriteText
- {
- Colour = colourProvider.Content2,
- Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.BottomLeft
- }
}
}
}
- },
+ }
};
}
@@ -210,7 +252,11 @@ namespace osu.Game.Screens.SelectV2
var beatmap = (BeatmapInfo)Item.Model;
starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token, SongSelect.SELECTION_DEBOUNCE);
- starDifficultyBindable.BindValueChanged(_ => updateDisplay(), true);
+ starDifficultyBindable.BindValueChanged(starDifficulty =>
+ {
+ starRatingDisplay.Current.Value = starDifficulty.NewValue;
+ starCounter.Current = (float)starDifficulty.NewValue.Stars;
+ }, true);
}
protected override void Update()
@@ -225,7 +271,21 @@ namespace osu.Game.Screens.SelectV2
// Dirty hack to make sure we don't take up spacing in parent fill flow when not displaying a rank.
// I can't find a better way to do this.
- starRatingDisplay.Margin = new MarginPadding { Left = 1 / starRatingDisplay.Scale.X * (localRank.HasRank ? 0 : -3) };
+ mainFill.Margin = new MarginPadding { Left = 1 / starRatingDisplay.Scale.X * (localRank.HasRank ? 0 : -3) };
+
+ var diffColour = starRatingDisplay.DisplayedDifficultyColour;
+
+ if (AccentColour != diffColour)
+ {
+ AccentColour = diffColour;
+ starCounter.Colour = diffColour;
+
+ backgroundAccentGradient.Colour = ColourInfo.GradientHorizontal(diffColour.Opacity(0.25f), diffColour.Opacity(0f));
+
+ difficultyIcon.Colour = starRatingDisplay.DisplayedStars.Value > OsuColour.STAR_DIFFICULTY_DEFINED_COLOUR_CUTOFF ? colours.Orange1 : colourProvider.Background5;
+
+ triangles.Colour = ColourInfo.GradientVertical(diffColour.Opacity(0.25f), diffColour.Opacity(0f));
+ }
}
private void updateKeyCount()
@@ -249,22 +309,6 @@ namespace osu.Game.Screens.SelectV2
keyCountText.Alpha = 0;
}
- private void updateDisplay()
- {
- const float duration = 500;
-
- var starDifficulty = starDifficultyBindable?.Value ?? default;
-
- starRatingDisplay.Current.Value = starDifficulty;
- starCounter.Current = (float)starDifficulty.Stars;
-
- difficultyIcon.FadeColour(starDifficulty.Stars > OsuColour.STAR_DIFFICULTY_DEFINED_COLOUR_CUTOFF ? colours.Orange1 : colourProvider.Background5, duration, Easing.OutQuint);
-
- var starRatingColour = colours.ForStarDifficulty(starDifficulty.Stars);
- starCounter.FadeColour(starRatingColour, duration, Easing.OutQuint);
- AccentColour = starRatingColour;
- }
-
public override MenuItem[] ContextMenuItems
{
get
diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs
index a41c5c75ae..425ca02e5a 100644
--- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs
+++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs
@@ -14,6 +14,8 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
+using osu.Game.Collections;
+using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Carousel;
using osu.Game.Graphics.Sprites;
@@ -188,6 +190,12 @@ namespace osu.Game.Screens.SelectV2
difficultiesDisplay.BeatmapSet = null;
}
+ [Resolved]
+ private RealmAccess realm { get; set; } = null!;
+
+ [Resolved]
+ private ManageCollectionsDialog? manageCollectionsDialog { get; set; }
+
public override MenuItem[] ContextMenuItems
{
get
@@ -215,6 +223,17 @@ namespace osu.Game.Screens.SelectV2
items.Add(new OsuMenuItemSpacer());
}
+ var collectionItems = realm.Realm.All()
+ .OrderBy(c => c.Name)
+ .AsEnumerable()
+ .Select(createCollectionMenuItem)
+ .ToList();
+
+ if (manageCollectionsDialog != null)
+ collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show));
+
+ items.Add(new OsuMenuItem("Collections") { Items = collectionItems });
+
if (beatmapSet.Beatmaps.Any(b => b.Hidden))
items.Add(new OsuMenuItem("Restore all hidden", MenuItemType.Standard, () => songSelect?.RestoreAllHidden(beatmapSet)));
@@ -222,5 +241,51 @@ namespace osu.Game.Screens.SelectV2
return items.ToArray();
}
}
+
+ private MenuItem createCollectionMenuItem(BeatmapCollection collection)
+ {
+ var beatmapSet = (BeatmapSetInfo)Item!.Model;
+
+ Debug.Assert(beatmapSet != null);
+
+ TernaryState state;
+
+ int countExisting = beatmapSet.Beatmaps.Count(b => collection.BeatmapMD5Hashes.Contains(b.MD5Hash));
+
+ if (countExisting == beatmapSet.Beatmaps.Count)
+ state = TernaryState.True;
+ else if (countExisting > 0)
+ state = TernaryState.Indeterminate;
+ else
+ state = TernaryState.False;
+
+ var liveCollection = collection.ToLive(realm);
+
+ return new TernaryStateToggleMenuItem(collection.Name, MenuItemType.Standard, s =>
+ {
+ liveCollection.PerformWrite(c =>
+ {
+ foreach (var b in beatmapSet.Beatmaps)
+ {
+ switch (s)
+ {
+ case TernaryState.True:
+ if (c.BeatmapMD5Hashes.Contains(b.MD5Hash))
+ continue;
+
+ c.BeatmapMD5Hashes.Add(b.MD5Hash);
+ break;
+
+ case TernaryState.False:
+ c.BeatmapMD5Hashes.Remove(b.MD5Hash);
+ break;
+ }
+ }
+ });
+ })
+ {
+ State = { Value = state }
+ };
+ }
}
}
diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs
index 86f8374088..d461653dcb 100644
--- a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs
+++ b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs
@@ -63,13 +63,29 @@ namespace osu.Game.Screens.SelectV2
private BeatmapSetOnlineStatusPill statusPill = null!;
private ConstrainedIconContainer difficultyIcon = null!;
- private FillFlowContainer difficultyLine = null!;
private StarRatingDisplay starRatingDisplay = null!;
private StarCounter starCounter = null!;
private PanelLocalRankDisplay localRank = null!;
private OsuSpriteText keyCountText = null!;
private OsuSpriteText difficultyText = null!;
private OsuSpriteText authorText = null!;
+ private FillFlowContainer mainFill = null!;
+
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
+ {
+ var inputRectangle = TopLevelContent.DrawRectangle;
+
+ if (Selected.Value)
+ {
+ // Cover the gaps introduced by the spacing between BeatmapPanels so that clicks will not fall through the carousel.
+ //
+ // Caveat is that for simplicity, we are covering the full spacing, so panels with frontmost depth will have a slightly
+ // larger hit target.
+ inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING * 2 });
+ }
+
+ return inputRectangle.Contains(TopLevelContent.ToLocalSpace(screenSpacePos));
+ }
public PanelBeatmapStandalone()
{
@@ -83,8 +99,8 @@ namespace osu.Game.Screens.SelectV2
Icon = difficultyIcon = new ConstrainedIconContainer
{
- Size = new Vector2(16),
- Margin = new MarginPadding { Horizontal = 5f },
+ Size = new Vector2(12),
+ Margin = new MarginPadding { Left = 4f, Right = 3f },
Colour = colourProvider.Background5,
};
@@ -95,93 +111,105 @@ namespace osu.Game.Screens.SelectV2
Content.Child = new FillFlowContainer
{
+ AutoSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
- Padding = new MarginPadding { Left = 10f },
- Direction = FillDirection.Vertical,
- AutoSizeAxes = Axes.Both,
+ Spacing = new Vector2(3),
+ Margin = new MarginPadding { Left = 5 },
+ Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
- titleText = new OsuSpriteText
+ localRank = new PanelLocalRankDisplay
{
- Font = OsuFont.Style.Heading2.With(typeface: Typeface.TorusAlternate, weight: FontWeight.Bold),
+ Scale = new Vector2(0.8f),
+ Origin = Anchor.CentreLeft,
+ Anchor = Anchor.CentreLeft,
},
- artistText = new OsuSpriteText
+ mainFill = new FillFlowContainer
{
- Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
- Padding = new MarginPadding { Top = -2 },
- },
- difficultyLine = new FillFlowContainer
- {
- Direction = FillDirection.Horizontal,
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Direction = FillDirection.Vertical,
+ Padding = new MarginPadding { Bottom = 2 },
AutoSizeAxes = Axes.Both,
- Padding = new MarginPadding { Top = 4 },
Children = new Drawable[]
{
- statusPill = new BeatmapSetOnlineStatusPill
+ titleText = new OsuSpriteText
{
- Animated = false,
- Origin = Anchor.CentreLeft,
- Anchor = Anchor.CentreLeft,
- TextSize = OsuFont.Style.Caption2.Size,
- Margin = new MarginPadding { Right = 5f },
+ Font = OsuFont.Style.Heading2.With(typeface: Typeface.TorusAlternate, weight: FontWeight.Bold),
},
- updateButton = new PanelUpdateBeatmapButton
+ artistText = new OsuSpriteText
{
- Scale = new Vector2(0.7f),
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- Margin = new MarginPadding { Right = 5f, Top = -2f },
- },
- keyCountText = new OsuSpriteText
- {
- Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.BottomLeft,
- Alpha = 0,
- },
- difficultyText = new OsuSpriteText
- {
- Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.BottomLeft,
- Margin = new MarginPadding { Right = 3f },
- },
- authorText = new OsuSpriteText
- {
- Colour = colourProvider.Content2,
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.BottomLeft
+ Padding = new MarginPadding { Top = -2 },
+ },
+ new FillFlowContainer
+ {
+ Direction = FillDirection.Horizontal,
+ AutoSizeAxes = Axes.Both,
+ Padding = new MarginPadding { Top = 2, Bottom = 2 },
+ Children = new Drawable[]
+ {
+ statusPill = new BeatmapSetOnlineStatusPill
+ {
+ Animated = false,
+ Origin = Anchor.BottomLeft,
+ Anchor = Anchor.BottomLeft,
+ TextSize = OsuFont.Style.Caption2.Size,
+ Margin = new MarginPadding { Right = 4f },
+ },
+ updateButton = new PanelUpdateBeatmapButton
+ {
+ Scale = new Vector2(0.8f),
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ Margin = new MarginPadding { Right = 4f, Bottom = -1f },
+ },
+ keyCountText = new OsuSpriteText
+ {
+ Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ Alpha = 0,
+ },
+ difficultyText = new OsuSpriteText
+ {
+ Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ Margin = new MarginPadding { Right = 3f },
+ },
+ authorText = new OsuSpriteText
+ {
+ Colour = colourProvider.Content2,
+ Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft
+ }
+ }
+ },
+ new FillFlowContainer
+ {
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(3),
+ AutoSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small, animated: true)
+ {
+ Origin = Anchor.CentreLeft,
+ Anchor = Anchor.CentreLeft,
+ Scale = new Vector2(0.875f),
+ },
+ starCounter = new StarCounter
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Scale = new Vector2(0.4f)
+ }
+ },
}
}
- },
- new FillFlowContainer
- {
- Direction = FillDirection.Horizontal,
- Spacing = new Vector2(3),
- AutoSizeAxes = Axes.Both,
- Children = new Drawable[]
- {
- localRank = new PanelLocalRankDisplay
- {
- Scale = new Vector2(0.65f),
- Origin = Anchor.CentreLeft,
- Anchor = Anchor.CentreLeft,
- },
- starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small, animated: true)
- {
- Origin = Anchor.CentreLeft,
- Anchor = Anchor.CentreLeft,
- Scale = new Vector2(0.875f),
- },
- starCounter = new StarCounter
- {
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- Scale = new Vector2(0.4f)
- }
- },
}
}
};
@@ -229,9 +257,9 @@ namespace osu.Game.Screens.SelectV2
localRank.Beatmap = beatmap;
difficultyText.Text = beatmap.DifficultyName;
authorText.Text = BeatmapsetsStrings.ShowDetailsMappedBy(beatmap.Metadata.Author.Username);
- difficultyLine.Show();
computeStarRating();
+ updateKeyCount();
}
protected override void FreeAfterUse()
@@ -257,7 +285,11 @@ namespace osu.Game.Screens.SelectV2
var beatmap = (BeatmapInfo)Item.Model;
starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token, SongSelect.SELECTION_DEBOUNCE);
- starDifficultyBindable.BindValueChanged(_ => updateDisplay(), true);
+ starDifficultyBindable.BindValueChanged(starDifficulty =>
+ {
+ starRatingDisplay.Current.Value = starDifficulty.NewValue;
+ starCounter.Current = (float)starDifficulty.NewValue.Stars;
+ }, true);
}
protected override void Update()
@@ -272,7 +304,14 @@ namespace osu.Game.Screens.SelectV2
// Dirty hack to make sure we don't take up spacing in parent fill flow when not displaying a rank.
// I can't find a better way to do this.
- starRatingDisplay.Margin = new MarginPadding { Left = 1 / starRatingDisplay.Scale.X * (localRank.HasRank ? 0 : -3) };
+ mainFill.Margin = new MarginPadding { Left = 1 / starRatingDisplay.Scale.X * (localRank.HasRank ? 0 : -3) };
+
+ var diffColour = starRatingDisplay.DisplayedDifficultyColour;
+
+ AccentColour = diffColour;
+ starCounter.Colour = diffColour;
+
+ difficultyIcon.Colour = starRatingDisplay.DisplayedStars.Value > OsuColour.STAR_DIFFICULTY_DEFINED_COLOUR_CUTOFF ? colours.Orange1 : colourProvider.Background5;
}
private void updateKeyCount()
@@ -296,22 +335,6 @@ namespace osu.Game.Screens.SelectV2
keyCountText.Alpha = 0;
}
- private void updateDisplay()
- {
- const float duration = 500;
-
- var starDifficulty = starDifficultyBindable?.Value ?? default;
-
- starRatingDisplay.Current.Value = starDifficulty;
- starCounter.Current = (float)starDifficulty.Stars;
-
- difficultyIcon.FadeColour(starDifficulty.Stars > OsuColour.STAR_DIFFICULTY_DEFINED_COLOUR_CUTOFF ? colours.Orange1 : colourProvider.Background5, duration, Easing.OutQuint);
-
- var starRatingColour = colours.ForStarDifficulty(starDifficulty.Stars);
- starCounter.FadeColour(starRatingColour, duration, Easing.OutQuint);
- AccentColour = colours.ForStarDifficulty(starDifficulty.Stars);
- }
-
public override MenuItem[] ContextMenuItems
{
get
diff --git a/osu.Game/Screens/SelectV2/PanelSetBackground.cs b/osu.Game/Screens/SelectV2/PanelSetBackground.cs
index 99dbf90556..dd07be0410 100644
--- a/osu.Game/Screens/SelectV2/PanelSetBackground.cs
+++ b/osu.Game/Screens/SelectV2/PanelSetBackground.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
+using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
@@ -61,34 +62,27 @@ namespace osu.Game.Screens.SelectV2
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,
+ Colour = Color4.Black.Opacity(0.5f),
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)),
+ Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.5f), Color4.Black.Opacity(0.3f)),
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,
+ Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.3f), Color4.Black.Opacity(0.2f)),
+ // Slightly more than 1.0 in total to account for shear.
+ Width = 0.45f,
},
}
},
diff --git a/osu.Game/Screens/SelectV2/SoloSongSelect.cs b/osu.Game/Screens/SelectV2/SoloSongSelect.cs
index ea27ffef37..a136e682c4 100644
--- a/osu.Game/Screens/SelectV2/SoloSongSelect.cs
+++ b/osu.Game/Screens/SelectV2/SoloSongSelect.cs
@@ -73,6 +73,9 @@ namespace osu.Game.Screens.SelectV2
yield return new OsuMenuItemSpacer();
}
+ foreach (var i in CreateCollectionMenuActions(beatmap))
+ yield return i;
+
// TODO: replace with "remove from played" button when beatmap is already played.
yield return new OsuMenuItem(SongSelectStrings.MarkAsPlayed, MenuItemType.Standard, () => beatmaps.MarkPlayed(beatmap)) { Icon = FontAwesome.Solid.TimesCircle };
yield return new OsuMenuItem(SongSelectStrings.ClearAllLocalScores, MenuItemType.Standard, () => dialogOverlay?.Push(new BeatmapClearScoresDialog(beatmap)))
diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs
index 504a55a4f8..097abc7da8 100644
--- a/osu.Game/Screens/SelectV2/SongSelect.cs
+++ b/osu.Game/Screens/SelectV2/SongSelect.cs
@@ -21,6 +21,7 @@ using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Collections;
+using osu.Game.Database;
using osu.Game.Graphics.Carousel;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
@@ -70,15 +71,20 @@ namespace osu.Game.Screens.SelectV2
///
protected bool ControlGlobalMusic { get; init; } = true;
- private readonly ModSelectOverlay modSelectOverlay = new UserModSelectOverlay(OverlayColourScheme.Aquamarine)
+ // Colour scheme for mod overlay is left as default (green) to match mods button.
+ // Not sure about this, but we'll iterate based on feedback.
+ private readonly ModSelectOverlay modSelectOverlay = new UserModSelectOverlay
{
ShowPresets = true,
};
private ModSpeedHotkeyHandler modSpeedHotkeyHandler = null!;
+ // Blue is the most neutral choice, so I'm using that for now.
+ // Purple makes the most sense to match the "gameplay" flow, but it's a bit too strong for the current design.
+ // TODO: Colour scheme choice should probably be customisable by the user.
[Cached]
- private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
+ private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
private BeatmapCarousel carousel = null!;
@@ -146,9 +152,9 @@ namespace osu.Game.Screens.SelectV2
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
- new Dimension(GridSizeMode.Relative, 0.5f, maxSize: 850),
+ new Dimension(GridSizeMode.Relative, 0.5f, maxSize: 660),
new Dimension(),
- new Dimension(GridSizeMode.Relative, 0.5f, maxSize: 750),
+ new Dimension(GridSizeMode.Relative, 0.5f, maxSize: 620),
},
Content = new[]
{
@@ -162,6 +168,11 @@ namespace osu.Game.Screens.SelectV2
// screen-wide scroll handling.
Depth = float.MinValue,
Shear = OsuGame.SHEAR,
+ Padding = new MarginPadding
+ {
+ Top = -CORNER_RADIUS_HIDE_OFFSET,
+ Left = -CORNER_RADIUS_HIDE_OFFSET,
+ },
Children = new Drawable[]
{
new Container
@@ -177,11 +188,6 @@ namespace osu.Game.Screens.SelectV2
wedgesContainer = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
- Margin = new MarginPadding
- {
- Top = -CORNER_RADIUS_HIDE_OFFSET,
- Left = -CORNER_RADIUS_HIDE_OFFSET
- },
Spacing = new Vector2(0f, 4f),
Direction = FillDirection.Vertical,
Children = new Drawable[]
@@ -196,8 +202,13 @@ namespace osu.Game.Screens.SelectV2
new Container
{
RelativeSizeAxes = Axes.Both,
- Children = new CompositeDrawable[]
+ Children = new Drawable[]
{
+ new Box
+ {
+ Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.0f), Color4.Black.Opacity(0.5f)),
+ RelativeSizeAxes = Axes.Both,
+ },
new Container
{
RelativeSizeAxes = Axes.Both,
@@ -218,7 +229,10 @@ namespace osu.Game.Screens.SelectV2
RequestRecommendedSelection = selectRecommendedBeatmap,
NewItemsPresented = newItemsPresented,
},
- noResultsPlaceholder = new NoResultsPlaceholder(),
+ noResultsPlaceholder = new NoResultsPlaceholder
+ {
+ RequestClearFilterText = () => filterControl.Search(string.Empty)
+ }
}
},
filterControl = new FilterControl
@@ -401,6 +415,7 @@ namespace osu.Game.Screens.SelectV2
backgroundModeBeatmap.BlurAmount.Value = 0;
backgroundModeBeatmap.Beatmap = beatmap;
backgroundModeBeatmap.IgnoreUserSettings.Value = true;
+ backgroundModeBeatmap.DimWhenUserSettingsIgnored.Value = 0.1f;
backgroundModeBeatmap.FadeColour(Color4.White, 250);
});
}
@@ -547,12 +562,18 @@ namespace osu.Game.Screens.SelectV2
private void criteriaChanged(FilterCriteria criteria)
{
+ // The first filter needs to be applied immediately as this triggers the initial carousel load.
+ double filterDelay = filterDebounce == null ? 0 : filter_delay;
+
filterDebounce?.Cancel();
- filterDebounce = Scheduler.AddDelayed(() => { carousel.Filter(criteria); }, filter_delay);
+ filterDebounce = Scheduler.AddDelayed(() => { carousel.Filter(criteria); }, filterDelay);
}
private void newItemsPresented(IEnumerable carouselItems)
{
+ if (carousel.Criteria == null)
+ return;
+
int count = carousel.MatchedBeatmapsCount;
if (count == 0)
@@ -658,6 +679,12 @@ namespace osu.Game.Screens.SelectV2
#region Beatmap management
+ [Resolved]
+ private ManageCollectionsDialog? manageCollectionsDialog { get; set; }
+
+ [Resolved]
+ private RealmAccess realm { get; set; } = null!;
+
public virtual IEnumerable GetForwardActions(BeatmapInfo beatmap)
{
yield return new OsuMenuItem("Select", MenuItemType.Highlighted, () => SelectAndStart(beatmap))
@@ -674,6 +701,23 @@ namespace osu.Game.Screens.SelectV2
if (beatmap.GetOnlineURL(api, Ruleset.Value) is string url)
yield return new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => (game as OsuGame)?.CopyToClipboard(url));
}
+
+ yield return new OsuMenuItemSpacer();
+
+ foreach (var i in CreateCollectionMenuActions(beatmap))
+ yield return i;
+ }
+
+ protected IEnumerable CreateCollectionMenuActions(BeatmapInfo beatmap)
+ {
+ var collectionItems = realm.Realm.All()
+ .OrderBy(c => c.Name)
+ .AsEnumerable()
+ .Select(c => new CollectionToggleMenuItem(c.ToLive(realm), beatmap)).Cast().ToList();
+
+ collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, () => manageCollectionsDialog?.Show()));
+
+ yield return new OsuMenuItem("Collections") { Items = collectionItems };
}
public void ManageCollections() => collectionsDialog?.Show();