1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-17 22:33:47 +08:00

Merge branch 'master' into song-select-v2-scroll-to-selected

This commit is contained in:
Bartłomiej Dach
2025-05-30 14:27:38 +02:00
Unverified
32 changed files with 963 additions and 367 deletions
+4
View File
@@ -3,6 +3,10 @@
<PropertyGroup Label="C#">
<LangVersion>12.0</LangVersion>
<Nullable>enable</Nullable>
<!-- Stabilises hot reload, see: https://platform.uno/docs/articles/studio/Hot%20Reload/hot-reload-overview.html?tabs=vswin%2Cwindows%2Cskia-desktop%2Ccommon-issues -->
<GenerateAssemblyInfo Condition="'$(Configuration)'=='Debug'">false</GenerateAssemblyInfo>
<!-- Required due to the above -->
<NoWarn Condition="'$(Configuration)'=='Debug'">$(NoWarn);CA1416</NoWarn>
</PropertyGroup>
<PropertyGroup>
<ApplicationManifest>$(MSBuildThisFileDirectory)app.manifest</ApplicationManifest>
@@ -312,8 +312,8 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("set game volume to max", () => Game.Dependencies.Get<FrameworkConfigManager>().SetValue(FrameworkSetting.VolumeUniversal, 1d));
AddStep("move to metadata wedge", () => InputManager.MoveMouseTo(
songSelect.ChildrenOfType<BeatmapMetadataWedge>().Single()));
AddStep("move to details area", () => InputManager.MoveMouseTo(
songSelect.ChildrenOfType<BeatmapDetailsArea>().Single()));
AddStep("scroll down", () => InputManager.ScrollVerticalBy(-1));
AddAssert("carousel didn't move", getCarouselScrollPosition, () => Is.EqualTo(scrollPosition));
@@ -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<BeatmapSetInfo>(), Is.EquivalentTo(beatmapSets));
Assert.That(results.Select(r => r.Model).OfType<BeatmapInfo>(), Is.EquivalentTo(allBeatmaps));
assertTotal(results, beatmapSets.Count + allBeatmaps.Length);
@@ -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<BeatmapInfo> 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<IEnumerable<CarouselItem>> 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<BeatmapInfo>();
return items;
@@ -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!;
});
@@ -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<Panel>(), () => 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<PanelBeatmapStandalone>().Count(), () => Is.EqualTo(1));
}
}
}
@@ -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<PanelBeatmapStandalone>().Any());
@@ -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()
{
@@ -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 =>
+2 -2
View File
@@ -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);
+38 -8
View File
@@ -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
/// </summary>
public bool IsFiltering => !filterTask.IsCompleted;
/// <summary>
/// The number of times filter operations have been triggered.
/// </summary>
internal int FilterCount { get; private set; }
/// <summary>
/// The number of displayable items currently being tracked (before filtering).
/// </summary>
@@ -181,9 +187,13 @@ namespace osu.Game.Graphics.Carousel
/// <param name="clearExistingPanels">Whether all existing drawable panels should be reset post filter.</param>
protected virtual Task<IEnumerable<CarouselItem>> 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<IEnumerable<CarouselItem>> filterTask = Task.FromResult(Enumerable.Empty<CarouselItem>());
private CancellationTokenSource cancellationSource = new CancellationTokenSource();
/// <summary>
/// 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.
/// </summary>
private readonly Cached filterAfterItemsChanged = new Cached();
private async Task<IEnumerable<CarouselItem>> 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<CarouselItem> items = new List<CarouselItem>(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<CarouselItem> toDisplay = range.Last - range.First == 0
List<CarouselItem> toDisplay = range == DisplayRange.EMPTY
? new List<CarouselItem>()
: carouselItems.GetRange(range.First, range.Last - range.First + 1);
@@ -889,7 +916,10 @@ namespace osu.Game.Graphics.Carousel
/// <param name="Index">The index of the selection as of the last run of <see cref="Carousel{T}.refreshAfterSelection"/>. May be null if selection is not present as an item, or if <see cref="Carousel{T}.refreshAfterSelection"/> has not been run yet.</param>
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);
}
/// <summary>
/// Implementation of scroll container which handles very large vertical lists by internally using <c>double</c> precision
+10 -1
View File
@@ -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<ScreenFooterButton> 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();
@@ -0,0 +1,38 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Screens.Select
{
public enum BeatmapDetailTab
{
/// <summary>
/// Beatmap details.
/// </summary>
Details,
/// <summary>
/// Local leaderboards.
/// </summary>
Local,
/// <summary>
/// Country leaderboards.
/// </summary>
Country,
/// <summary>
/// Global leaderboards.
/// </summary>
Global,
/// <summary>
/// Friend leaderboards.
/// </summary>
Friends,
/// <summary>
/// Team leaderboards.
/// </summary>
Team
}
}
+10 -10
View File
@@ -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,
@@ -29,7 +29,7 @@ namespace osu.Game.Screens.Select
}
}
private Bindable<TabType> selectedTab;
private Bindable<BeatmapDetailTab> selectedTab;
private Bindable<bool> selectedModsFilter;
@@ -41,7 +41,7 @@ namespace osu.Game.Screens.Select
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
selectedTab = config.GetBindable<TabType>(OsuSetting.BeatmapDetailTab);
selectedTab = config.GetBindable<BeatmapDetailTab>(OsuSetting.BeatmapDetailTab);
selectedModsFilter = config.GetBindable<bool>(OsuSetting.BeatmapDetailModsFilter);
selectedTab.BindValueChanged(tab => CurrentTab.Value = getTabItemFromTabType(tab.NewValue), true);
@@ -86,26 +86,26 @@ namespace osu.Game.Screens.Select
new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(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>(BeatmapLeaderboardScope.Local);
case TabType.Global:
case BeatmapDetailTab.Global:
return new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Global);
case TabType.Country:
case BeatmapDetailTab.Country:
return new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Country);
case TabType.Friends:
case BeatmapDetailTab.Friends:
return new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Friend);
case TabType.Team:
case BeatmapDetailTab.Team:
return new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(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<BeatmapLeaderboardScope> 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
}
}
}
+37 -12
View File
@@ -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<IEnumerable<CarouselItem>> FilterAsync(bool clearExistingPanels = false)
{
if (Criteria == null)
return Task.FromResult(Enumerable.Empty<CarouselItem>());
return base.FilterAsync(clearExistingPanels);
}
#endregion
#region Drawable pooling
@@ -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<GroupMapping> getGroups(List<CarouselItem> items, FilterCriteria criteria)
{
switch (criteria.Group)
{
case GroupMode.NoGrouping:
case GroupMode.None:
return new List<GroupMapping> { 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<T>(BeatmapInfo b, IEnumerable<CarouselItem> items, Func<BeatmapInfo, T> func)
private static T? aggregateMax<T>(BeatmapInfo b, Func<BeatmapInfo, T> func)
{
var matchedBeatmaps = items.Select(i => i.Model).Cast<BeatmapInfo>().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<CarouselItem> ItemsInGroup);
@@ -27,19 +27,27 @@ namespace osu.Game.Screens.SelectV2
{
var criteria = getCriteria();
bool groupedSets = BeatmapCarouselFilterGrouping.ShouldGroupBeatmapsTogether(criteria);
return items.Order(Comparer<CarouselItem>.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);
@@ -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<BeatmapLeaderboardScope> Scope => scopeDropdown.Current;
private readonly Bindable<BeatmapDetailTab> configDetailTab = new Bindable<BeatmapDetailTab>();
public IBindable<bool> 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,
@@ -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;
@@ -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<BeatmapLeaderboardScope> Scope { get; } = new Bindable<BeatmapLeaderboardScope>();
public IBindable<bool> FilterBySelectedMods { get; } = new BindableBool();
@@ -70,7 +75,7 @@ namespace osu.Game.Screens.SelectV2
private readonly IBindable<LeaderboardScores?> fetchedScores = new Bindable<LeaderboardScores?>();
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)
@@ -253,7 +253,7 @@ namespace osu.Game.Screens.SelectV2
mapperText.Text = beatmap.Value.Metadata.Author.Username;
}
starRatingDisplay.Current = (Bindable<StarDifficulty>)difficultyCache.GetBindableDifficulty(beatmap.Value.BeatmapInfo, cancellationSource.Token, 200);
starRatingDisplay.Current = (Bindable<StarDifficulty>)difficultyCache.GetBindableDifficulty(beatmap.Value.BeatmapInfo, cancellationSource.Token, SongSelect.SELECTION_DEBOUNCE);
updateCountStatistics(cancellationSource.Token);
updateDifficultyStatistics();
+4 -5
View File
@@ -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<SortMode>(SortStrings.Default)
sortDropdown = new ShearedDropdown<SortMode>("Sort")
{
RelativeSizeAxes = Axes.X,
Items = Enum.GetValues<SortMode>(),
},
Empty(),
// todo: pending localisation
groupDropdown = new ShearedDropdown<GroupMode>("Group by")
groupDropdown = new ShearedDropdown<GroupMode>("Group")
{
RelativeSizeAxes = Axes.X,
Items = Enum.GetValues<GroupMode>(),
@@ -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
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using 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();
+44 -40
View File
@@ -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);
}
+120 -76
View File
@@ -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<StarDifficulty>? 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
@@ -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<BeatmapCollection>()
.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 }
};
}
}
}
@@ -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
@@ -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,
},
}
},
@@ -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)))
+56 -12
View File
@@ -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
/// </summary>
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<CarouselItem> 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<OsuMenuItem> 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<OsuMenuItem> CreateCollectionMenuActions(BeatmapInfo beatmap)
{
var collectionItems = realm.Realm.All<BeatmapCollection>()
.OrderBy(c => c.Name)
.AsEnumerable()
.Select(c => new CollectionToggleMenuItem(c.ToLive(realm), beatmap)).Cast<OsuMenuItem>().ToList();
collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, () => manageCollectionsDialog?.Show()));
yield return new OsuMenuItem("Collections") { Items = collectionItems };
}
public void ManageCollections() => collectionsDialog?.Show();